• 기본적인 Deep Learning은 파라미터 수가 많고 네트워크가 깊어서 train data가 많이 필요
• 특히, biomedical의 특성상 data의 수가 많이 부족할 수 밖에 없음
• 이유 : 전문가에 의한 labeling 작업 및 환자 개인 정보 등의 문제로 원활한 train data 수집 어려움
특히, Cell Segmentation 작업의 경우, 같은 클래스가 인접해 있는 셀 사이의 경계 구분 필요
-> 아래 그림 (a)에서 “A” 와 “B”는 같은 클래스지만, (b)처럼 서로 다른 인스턴스(셀)로 구분을 요구하며, 이를 구분하기 위해 (c) 처럼 인스턴스(셀)의 경계를 테두리로 만들어야 하는 작업 필요 그러나, 일반적인 “Semantic Segmentation” 작업에서는 불가능
🤔No zero-padding 이면 특징맵(patch-size)의 크기가 감소하는 이유?
👉 Convolution 연산 시에 padding을 추가하여 입력과 동일한 크기의 출력을 얻을 수 있기 때문
ex) 입력이 크기라고 하고, Zero-padding 없이 커널을 사용하여 Convolution 연산을 하면, 특징맵의 크기가 로 줄어듭니다. 이를 반복하면 점차적으로 특징맵 크기(또는 patch size)가 줄어드는 것입니다.
Random Elastic deformations을 통해서 Augmentation을 수행
-> model이 invariance와 robustness를 학습할 수 있도록 하는 방법
같은 클래스를 가지는 인접한 셀을 분리하기 위해 해당 경계부분에 가중치를 제공
: 거리 값을 조절하는 파라미터
이 항이 큰 값을 갖게 되는 경우는, 두 개의 객체 경계 사이에 위치한 픽셀일 때입니다. 이 경우 𝑑1과 d2 가 작아서 지수 함수 값이 커지므로 해당 픽셀에 더 높은 가중치를 부여하게 됩니다. 이를 통해 네트워크가 경계에 있는 픽셀을 더 잘 학습하게 됨!
가만히 보면 크로스엔트로피랑 매우 유사하다.
크로스엔트로피 :
: 전체 손실 함수로, 각 픽셀의 손실을 가중합한 값입니다.
: 이미지 내의 모든 픽셀 위치 집합입니다.
: 위에서 정의한 가중치 맵으로, 특정 픽셀 위치 에서의 가중치를 나타냅니다.
: 에서 예측된 클래스 의 확률입니다. 네트워크가 특정 클래스에 대해 예측한 확률로, 픽셀별로 올바른 클래스를 예측할 확률 값을 갖습니다.
=>이 손실 함수는 각 픽셀의 손실에 𝑤(𝑥)가중치를 부여하여 경계에 있는 픽셀과 중요도가 높은 픽셀에 대한 학습을 강화합니다.
import torch
import torch.nn as nn
# CBR2d 정의
def CBR2d(in_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=True):
return nn.Sequential(
nn.Conv2d(in_channels=in_channels,
out_channels=out_channels,
kernel_size=kernel_size,
stride=stride,
padding=padding,
bias=bias),
nn.BatchNorm2d(num_features=out_channels),
nn.ReLU()
)
# U-Net 네트워크의 일부로 인코더와 디코더 정의
class UNet(nn.Module):
def __init__(self):
super(UNet, self).__init__()
# Encoder 1
self.enc1_1 = CBR2d(1, 64, kernel_size=3, stride=1, padding=0, bias=True)
self.enc1_2 = CBR2d(64, 64, kernel_size=3, stride=1, padding=0, bias=True)
self.pool1 = nn.MaxPool2d(kernel_size=2)
# Encoder 2
self.enc2_1 = CBR2d(64, 128, kernel_size=3, stride=1, padding=0, bias=True)
self.enc2_2 = CBR2d(128, 128, kernel_size=3, stride=1, padding=0, bias=True)
self.pool2 = nn.MaxPool2d(kernel_size=2)
# Encoder 3
self.enc3_1 = CBR2d(128, 256, kernel_size=3, stride=1, padding=0, bias=True)
self.enc3_2 = CBR2d(256, 256, kernel_size=3, stride=1, padding=0, bias=True)
self.pool3 = nn.MaxPool2d(kernel_size=2)
# Encoder 4
self.enc4_1 = CBR2d(256, 512, kernel_size=3, stride=1, padding=0, bias=True)
self.enc4_2 = CBR2d(512, 512, kernel_size=3, stride=1, padding=0, bias=True)
self.pool4 = nn.MaxPool2d(kernel_size=2)
# Encoder 5 and Decoder 5
self.enc5_1 = CBR2d(512, 1024, kernel_size=3, stride=1, padding=0, bias=True)
self.enc5_2 = CBR2d(1024, 1024, kernel_size=3, stride=1, padding=0, bias=True)
self.upconv4 = nn.ConvTranspose2d(1024, 512, kernel_size=2, stride=2, padding=0, bias=True)
# Decoder 4
self.dec4_2 = CBR2d(1024, 512, kernel_size=3, stride=1, padding=0, bias=True)
self.dec4_1 = CBR2d(512, 512, kernel_size=3, stride=1, padding=0, bias=True)
self.upconv3 = nn.ConvTranspose2d(512, 256, kernel_size=2, stride=2, padding=0, bias=True)
# Decoder 3
self.dec3_2 = CBR2d(512, 256, kernel_size=3, stride=1, padding=0, bias=True)
self.dec3_1 = CBR2d(256, 256, kernel_size=3, stride=1, padding=0, bias=True)
self.upconv2 = nn.ConvTranspose2d(256, 128, kernel_size=2, stride=2, padding=0, bias=True)
# Decoder 2
self.dec2_2 = CBR2d(256, 128, kernel_size=3, stride=1, padding=0, bias=True)
self.dec2_1 = CBR2d(128, 128, kernel_size=3, stride=1, padding=0, bias=True)
self.upconv1 = nn.ConvTranspose2d(128, 64, kernel_size=2, stride=2, padding=0, bias=True)
# Decoder 1
self.dec1_2 = CBR2d(128, 64, kernel_size=3, stride=1, padding=0, bias=True)
self.dec1_1 = CBR2d(64, 64, kernel_size=3, stride=1, padding=0, bias=True)
# Output Segmentation map
self.score_fr = nn.Conv2d(in_channels=64, out_channels=2, kernel_size=1, stride=1, padding=0, bias=True)
# 이미지 크기 맞추기
def crop_img(self, in_tensor, out_size):
dim1, dim2 = in_tensor.size()[2:]
out_tensor = in_tensor[:, :,
int((dim1 - out_size) / 2): int((dim1 + out_size) / 2),
int((dim2 - out_size) / 2): int((dim2 + out_size) / 2)]
return out_tensor
def forward(self, x):
# Encoder 1
enc1_1 = self.enc1_1(x)
enc1_2 = self.enc1_2(enc1_1)
pool1 = self.pool1(enc1_2)
# Encoder 2
enc2_1 = self.enc2_1(pool1)
enc2_2 = self.enc2_2(enc2_1)
pool2 = self.pool2(enc2_2)
# Encoder 3
enc3_1 = self.enc3_1(pool2)
enc3_2 = self.enc3_2(enc3_1)
pool3 = self.pool3(enc3_2)
# Encoder 4
enc4_1 = self.enc4_1(pool3)
enc4_2 = self.enc4_2(enc4_1)
pool4 = self.pool4(enc4_2)
# Encoder 5
enc5_1 = self.enc5_1(pool4)
enc5_2 = self.enc5_2(enc5_1)
upconv4 = self.upconv4(enc5_2)
# Decoder 4
crop_enc4_2 = self.crop_img(enc4_2, upconv4.size()[2])
cat4 = torch.cat([upconv4, crop_enc4_2], dim=1)
dec4_2 = self.dec4_2(cat4)
dec4_1 = self.dec4_1(dec4_2)
upconv3 = self.upconv3(dec4_1)
# Decoder 3
crop_enc3_2 = self.crop_img(enc3_2, upconv3.size()[2])
cat3 = torch.cat([upconv3, crop_enc3_2], dim=1)
dec3_2 = self.dec3_2(cat3)
dec3_1 = self.dec3_1(dec3_2)
upconv2 = self.upconv2(dec3_1)
# Decoder 2
crop_enc2_2 = self.crop_img(enc2_2, upconv2.size()[2])
cat2 = torch.cat([upconv2, crop_enc2_2], dim=1)
dec2_2 = self.dec2_2(cat2)
dec2_1 = self.dec2_1(dec2_2)
upconv1 = self.upconv1(dec2_1)
# Decoder 1
crop_enc1_2 = self.crop_img(enc1_2, upconv1.size()[2])
cat1 = torch.cat([upconv1, crop_enc1_2], dim=1)
dec1_2 = self.dec1_2(cat1)
dec1_1 = self.dec1_1(dec1_2)
# Output
output = self.score_fr(dec1_1)
return output
# 모델 생성
model = UNet()
print(model)
Concatnate를 함. 모든 곳에. 단순한 skip connection 극복
위 두가지 기법을 활용해서 ensemble 한 효과가 있는것임!
depth 1 + depth 2 + depth 3 + depth 4 인 모델들을 ensemble 하는 효과
hybrid loss = Pixel wise Cross Entropy + Soft Dice Coefficient
이 수식은 하이브리드 손실 함수로, 크로스 엔트로피 손실과 다른 손실 항을 결합하여 네트워크가 특정 목표를 더 잘 학습하도록 설계
수식의 의미
첫번째 항: 크로스엔트로피 손실
타겟 클래스와 예측 확률 간의 차이를 측정하며, 정확한 클래스를 예측할수록 손실이 낮아짐 -> 모델이 실제 타겟 클래스에 더 높은 확률로 예측하게끔 유도
두번째 항: 유사성 측정항
분자는 예측 값과 실제 값의 곱이고, 분모는 예측 값과 실제 값의 제곱합.
목적은 모델이 타겟 레이블에 가까운 값을 더 많이 예측하도록 유도!
👉 즉, 하이브리드 손실 함수는 크로스 엔트로피 손실을 기본으로 하면서, 두 번째 항을 추가하여 예측 값과 실제 값의 유사성을 더욱 강조합니다. 이를 통해 모델이 타겟 클래스뿐만 아니라 확률 분포 상에서 실제 값과 더 유사한 예측을 하도록 학습합니다
Deep Supervision은 네트워크의 중간 레이어에서도 손실(Loss)을 계산하여 학습하는 방식(다단계 손실함수를 계산함으로써 더 segmentic 하게 잘 하도록 함)
• U-Net에서의 decoder를 구성하는 방법은 같은 level의 encoder layer로 부터 feature map을 받는 simple skip connection 사용
• U-Net ++에서는 nested and dense skip connection을 사용하여 encoder–decoder 사이의 semantic gap을 줄임
-> 하지만 위에 parameter가 많고 메모리사용량이 높아지고 full scale에 명시적으로 학습을 못한다는 한계점이 있었음. 이걸 극복하기 위해서 U-Net 3+ 탄생
• Parameter를 줄이기 위해 모든 decoder layer의 channel 수 320 통일
• 모든 encoder layer 에서 skip connection 과정에서 64 channel, 3 x 3 conv 동일하게 적용
• U-Net, U-Net++, U-Net 3+ Architecture의 parameter 비교 (backbone : Vgg-16 / ResNet-101)
low-level layer에 남아있는 background의 noise발생하여, 많은 false-positive 문제 발생
• 정확도를 높이고자, extra classification task 진행
• high-level feature maps인 𝑋𝑑𝑒5 를 활용
• 경계 부분 잘 학습하기 위해서 Loss 여러가지 결합
Loss 결합: 경계 부분을 잘 학습하기 위해 다양한 손실 함수를 결합하여 최종 손실 함수 𝐿seg 를 정의합니다
구성요소
①
Focal Loss로, 학습이 어려운 샘플에 더 큰 가중치를 부여하여 모델이 불균형 데이터에 대해 더 잘 학습할 수 있게 합니다
②
Multi-Scale Structural Similarity (MS-SSIM) 손실로, 구조적 유사성을 높이며 경계 부분의 디테일을 잘 반영할 수 있게함(boundary 인식 강화)
③
IoU (Intersection over Union) 손실로, 예측된 경계와 실제 경계의 겹치는 비율을 나타내며, IoU 값을 최대화하여 정확도를 높입니다(IoU : 픽셀의 분류 정확도를 상승)
U-Net: Convolutional Networks for Biomedical Image Segmentation
https://arxiv.org/abs/1505.04597
UNet++: Redesigning Skip Connections to Exploit
Multiscale Features in Image Segmentation
https://arxiv.org/pdf/1912.05074
UNet 3+: A Full-Scale Connected UNet for Medical Image Segmentation
https://arxiv.org/abs/2004.08790
Eff-UNet: A Novel Architecture for Semantic Segmentation in Unstructured Environment
https://openaccess.thecvf.com/content_CVPRW_2020/papers/w22/Baheti_Eff-UNet_A_Novel_Architecture_for_Semantic_Segmentation_in_Unstructured_Environment_CVPRW_2020_paper.pdf
The One Hundred Layers Tiramisu: Fully Convolutional DenseNets for Semantic Segmentation
https://arxiv.org/abs/1611.09326
Road Extraction by Deep Residual U-Net
https://arxiv.org/pdf/1711.10684