안녕하세요! M_AI 입니다!
이전글에서는 당뇨망막병증(Diabetic Retinopathy)에서 무엇을 세그멘테이션할 지와 이를 위해 데이터셋을 어떻게 준비하는 지에 대해 설명했습니다.
이번 장에서는 ImageNet으로 Pre-trained된 모델을 Backbone으로 가지는 U-Net 모델을 구현 방법을 설명하도록 하겠습니다.
이전글 : [2D Multi-Class Semantic Segmentation] 1. Data Preprocessing
PC 화면으로 보는 것을 추천!!!!!
이 글을 읽으면 좋을 것 같은 사람들!
- 기본적인 분류(classfication) 모델을 Tensorflow 2.x 버전으로 구현할 줄 아는 사람
- Clssification을 넘어서 Object detection을 하길 원하는데 오픈 소스를 봐도 이해가 가질 않는 사람.
- 오픈 소스를 이해하더라도 입맛대로 수정하는 방법을 모르는 사람.
본 게시글의 목적
※ 본 게시글은 딥러닝 모델의 상세한 이론적 설명보다는 모델 구조를 바탕으로 코드 분석하고 이해하는 데에 목적에 있습니다!
※ 상세한 이론을 원하면 다른 곳에서 찾아보시길 바랍니다!
본 게시글의 특징
① Framework : Tensorflow 2.x
② 학습 환경
- Google Colab
③ Batch size = 1
④ IDRiD Diabetic Retinopathy, DIARETDB1 데이터셋 사용하여 Fundus Image에서 병변 분할(Lesion Segmentation) 목적
( IDRiD 데이터셋 출처 : https://idrid.grand-challenge.org/Data/)
( DIARETDB1 데이터셋 출처 : https://www.it.lut.fi/project/imageret/diaretdb1/#DOWNLOAD)
⑤ 게시글에서는 반말
2. U-Net with Backbone and Loss Function
2.1. Model and Loss Function
본 파트에서는 모델 구조를 설명한다. U-Net[1]은 2015년에 나온 논문인 “U-Net: Convolutional Networks for Biomedical Image Segmentation”에서 제시된 모델로, contracting path와 expansive path로 구성되어 있다.(Figure 6) 언뜻 보면 AutoEncoder 모델에 skip connection이 추가된 형태로 보인다.
실제로도 contracting path는 encoder 역할을 하며, expansive path는 decoder 역할 한다.
(Figure 6)에서 보다시피 contracting path 부분에서는 4번의 다운 샘플링이 발생하며, expansive path에서는 반대로 4번의 업샘플링이 일어난다. 그리고 업샘플링 후에는 contracting path에서의 feature map을 skip connection하여 손실된 위치적 정보와 추상적인 정보를 보완한다.
모델이 출력 하는 예측값(prediction value)차원 형태는 (Input Height, Input Width, Number of classes) 이다. 본 게시글에서는 입력 영상 크기가 (1024, 1024, 3)이고 클래스 수는 3이므로, 여기서의 예측값 차원은 (1024, 1024, 3)이다.
하지만 본 게시글에서는 contracting path 부분을 ImageNet으로 사전 학습된(pre-trained) 모델을 backbone으로 사용하여, backbone 뒤에 expansive path를 추가하여 이 부분만 학습하도록 한다.
Backbone으로 사용할 모델은 VGG16, ResNet50, 그리고 EfficientNet B0이다. 이들은 각 모델 시리즈 중에서 가장 심플한 구조를 가지는 모델이다.
2.1.1. VGG16
VGG 모델은 가장 유명한 CNN 모델 중 하나이다. 해당 모델이 의의 하는 것은 두 가지이다.
1. 모델이 깊을수록 좋은 성능을 보인다는 것
2. 3 x 3 필터만 사용하여 학습 파라미터 수를 줄여도 훌륭한 성능을 보인다는 것이다.
VGG16을 백본으로 가지는 U-Net 모델 구조는 (Figure 7)과 같다. (잘 보이지 않으면 첨부 파일을 다운로드하여 확인하길 바란다.)
(Figure 7)를 보면 VGG16은 5번은 다운 샘플링이 진행되는데, VGG16 뿐만 아니라 ResNet50, EfficientNet B0 모델 또한 5번의 다운 샘플링이 진행된다. 그래서 이에 따라 Skip connection은 6번 진행하도록 한다.
VGG16에서는 마지막 output에 대하여 별도의 컨볼루션 연산 없이 바로 Flatten이 진행되었기에 VGG16에는 별도로 컨볼루션 연산을 해주어야 한다.
2.1.4. Loss Function
세그멘테이션에서는 다양한 loss function들이 존재하지만 여기서는 사용할 Loss는 1개이다.
우선 대표적으로 사용하는 2개의 loss function을 소개하겠다.
1. Dice Loss function
dice coefficient의 수식은 다음과 같다.
$\frac{2TP}{2TP\ +\ FP\ +\ FN}=\frac{2\ \times \left|\combi{GT\ \cap \Pr ed}\right|}{\left|\combi{GT}\right|\ +\ \left|\combi{\Pr ed}\right|}$2TP2TP + FP + FN=2 ×|GT ∩Pred||GT| + |Pred|
GT : 실제 마스크
Pred : 예측한 마스크
여기서 dice loss function은 1에다가 dice coefficient를 빼준 값이다.
2. Jaccard Loss function
다른 명칭은 IoU Loss function이다. IoU 대한 수식은 다음과 같다.
여기서 Jaccard Loss function은 1에서 IoU를 배준 값이다.
이제 본 게시글에서 사용할 것은 Focal Loss function 이다.
해당 데이터에서 클래스 비율이 배경에 압도적으로 몰려 있기에, 학습이 편향이 일어날 가능성이 매우 크다.
이를 방지하기 위해 위의 두 함수에 Focal Loss function을 추가하여 학습한다.
Focal Loss 식은 다음과 같다.
2.2. Code analysis
2.2.1. Model
모델을 생성하는 코드는 총 3개의 함수로 구성되어있다.
1. def get_backbone
백본의 모델명을 입력으로 받아, 백본 모델에서의 각 컨볼루션 계층에서의 skip connection할 feature map을 반환하도록 한다.
2. def twice_Conv2D
반복되는 두 번의 컨볼루션 연산을 함수화한 것이다.
2. def Unet
def get_backbone 함수를 사용하여 받은 feature map들을 이용하여 U-Net의 expansive path를 구성한다. 백본 모델이 VGG16 일 경우, 위에서 언급했듯이 다른 두 모델과 다르게 추가적인 컨볼루션 연산이 필요하다고 하여 그에 대한 추가 컨볼루션 계층을 if 문으로 구현하였다.
그 이외에는 모두 공통되는 구조이며 upsampling과 3x3 Conv, Batchnormalization, ReLu가 등장하고 마지막 layer에는 1x1 필터가 클래스 개수만큼 존재하며, 활성 함수로는 multi-class이므로 softmax를 사용한다.
def get_backbone(width = WIDTH, height = HEIGHT):
backbone = keras.applications.VGG16(include_top=False, input_shape=(width, height, 3), weights = "imagenet")
output_0, output_1, output_2, output_3, output_4, fin_output = [
backbone.get_layer(layer_name).output
for layer_name in ["block1_conv2","block2_conv2", "block3_conv3", "block4_conv3", "block5_conv3", "block5_pool"]
]
return keras.Model(
inputs=[backbone.inputs],
outputs=[ fin_output, output_4, output_3, output_2, output_1, output_0 ],
)
def twice_Conv2D(input_image, num_filters, i):
for j in range(2):
output = keras.layers.Conv2D(num_filters, 3, 1,
padding = "same",
kernel_initializer='he_normal',
name=f"block{i+1}_Conv{j+1}")(input_image)
output = keras.layers.BatchNormalization(name=f"block{i+1}_BN{j+1}")(output)
output = keras.layers.ReLU(name = f"block{i+1}_ReLU{j+1}")(output)
return output
def Unet(num_classes = NUM_CLASSES, width = WIDTH, height = HEIGHT):
input_image = keras.Input(shape=(width, height, 3), name="Image")
output_list = get_backbone()(input_image, training = False)
output = output_list[0]
for i in range(2):
output = keras.layers.Conv2D(512, 3, 1,
padding = "same",
kernel_initializer='he_normal',
name=f"block{0}_Conv{i}")(output)
output = keras.layers.BatchNormalization(name=f"block{0}_BN{i}")(output)
output = keras.layers.ReLU(name=f"block{0}_ReLU{i}")(output)
for i, filters in enumerate([256, 128, 64, 32, 16]):
output = keras.layers.UpSampling2D(2, name=f"block{i+1}_UpSampling{0}")(output)
output = keras.layers.Conv2D(output.shape[-1], 2, 1,
padding = "same",
kernel_initializer='he_normal',
name=f"block{i+1}_Conv{0}")(output)
output = keras.layers.BatchNormalization(name=f"block{i+1}_BN{0}")(output)
output = keras.layers.ReLU(name=f"block{i+1}_ReLU{0}")(output)
output = keras.layers.concatenate([output, output_list[i+1]], name = f"block{i+1}_concat")
output = twice_Conv2D(output, filters, i)
output = keras.layers.Conv2D(num_classes, 1, 1,
padding = "same",
activation = "softmax",
kernel_initializer=tf.initializers.RandomNormal(0.0, 0.01),
bias_initializer = tf.constant_initializer(-np.log((1 - 0.01) / 0.01)),
name = f"block{5}_Conv_ReLU")(output)
model = keras.models.Model(inputs = input_image, outputs=output)
return model
아래 코드는 테스트 코드로, 위의 함수들을 사용하여 각 모델을 백본으로 한 U-Net을 생성하는 함수이다.
위의 코드를 실행시킨 후, 아래 코드를 실행시키면 U-Net 구조가 출력되니 확인하도록 바란다.
# 1. U-Net based on VGG16
backbone_name = "VGG16"
unet_vgg16 = Unet()
unet_vgg16.summary()
2.2.2. Loss Function
Loss function 구현은 매우 간단하다. 우선 1장 데이터셋 준비를 설명할 때, 마스크를 하나의 채널에 0, 1, 2, 3으로 픽셀값을 매겨 레이블링한다고 하였다.
하지만 모델의 출력값은 (Input Height, Input Width, Number of classes)라고 했는데, 이에 맞춰주기 위해서 loss function 내부에서 실제 마스크에 대해 one-hot encoding을 진행하여 차원 수를 맞춰준다.
1. Dice
아래 def Dice는 DiceLoss에서 사용되며, 또한 모델 성능 평가에도 사용되는 지표이다.
def Dice(y_true, y_pred, solo = True, num_classes = NUM_CLASSES):
"""
Dice
Dice = 2TP / (2TP + FP + FN) = 2|X∩Y| / (|X| + |Y|)
= sum(2 X*Y) / (sum(X) +sum(Y))
"""
if solo:
y_true = tf.one_hot(tf.cast(y_true, dtype=tf.int32),
depth=num_classes,
dtype=tf.float32,
)
numerator = 2 * tf.reduce_sum(y_true * y_pred)
denominator = tf.reduce_sum(y_true + y_pred)
return numerator / denominator
class DiceLoss(tf.losses.Loss):
"""
DiceLoss = 1- Dice
"""
def __init__(self, solo = True):
super(DiceLoss, self).__init__(reduction="none", name="DiceLoss")
self.solo = solo
def call(self, y_true, y_pred):
return 1 - Dice(y_true, y_pred, self.solo)
2. Jaccard
아래 def Jaccard는 JaccardLoss에서 사용되며, 또한 모델 성능 평가에도 사용되는 지표이다.
def Jaccard(y_true, y_pred, solo = True, num_classes = NUM_CLASSES):
"""
Jaccard
IoU = TP / (TP + FP + FN) = |X∩Y| / ( |X| + |Y| - |X∩Y| )
= sum(A*B) / (sum(A)+sum(B)-sum(A*B))
"""
smooth = 0.0001
if solo:
y_true = tf.one_hot(tf.cast(y_true, dtype=tf.int32),
depth=num_classes,
dtype=tf.float32,
)
intersection = tf.reduce_sum(y_true * y_pred, axis=-1)
sum_ = tf.reduce_sum(y_true + y_pred, axis=-1)
return ((intersection + smooth) / (sum_ - intersection + smooth))
class JaccardLoss(tf.losses.Loss):
"""
Jaccard Loss = 1- Jaccard
"""
def __init__(self, solo = True, num_classes = NUM_CLASSES):
super(JaccardLoss, self).__init__(reduction="none", name="JaccardLoss")
self._num_classes = num_classes
self.smooth = 0.0001
self.solo = solo
def call(self, y_true, y_pred):
if self.solo:
y_true = tf.one_hot(
tf.cast(y_true, dtype=tf.int32),
depth=self._num_classes,
dtype=tf.float32,
)
intersection = tf.reduce_sum(y_true * y_pred, axis=-1)
sum_ = tf.reduce_sum(y_true + y_pred, axis=-1)
return 1 - ((intersection + self.smooth) / (sum_ - intersection + self.smooth))
3. FocalLoss
class FocalLoss(tf.losses.Loss):
"""
식 : loss = - y_true * alpha * ((1 - y_pred)^gamma) * log(y_pred)
alpha: the same as weighting factor in balanced cross entropy, default 0.25
gamma: focusing parameter for modulating factor (1-p), default 2.0
"""
def __init__(self, num_classes = NUM_CLASSES, gamma=2.0, alpha=0.25):
super(FocalLoss, self).__init__(reduction = 'auto', name = "FocalLoss")
self._num_classes = num_classes
self._gamma = gamma
self._alpha = alpha
self._epsilon = 1e-07
def call(self, y_true, y_pred):
y_pred = tf.clip_by_value(y_pred, self._epsilon, 1.0-self._epsilon)
loss = - y_true * self._alpha * tf.math.pow((1 - y_pred), self._gamma) * tf.math.log(y_pred)
return tf.reduce_mean(loss)
다음 파트에서는 모델 훈련 코드와 결과 및 성능 비교 설명을 하도록 하겠다.
Reference
[1] “U-Net: Convolutional Networks for Biomedical Image Segmentation”, “Olaf Ronneberger and Philipp Fischer and Thomas Brox”, 2015, arXiv
[2] "Deep Residual Learning for Image Recognition", "Kaiming He and Xiangyu Zhang and Shaoqing Ren and Jian Sun", 2015, arXiv, cs.CV
[3] "EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks", "Mingxing Tan, Quoc V. Le", 2019, arXiv
[4] "EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks 리뷰", "hoya12", https://hoya012.github.io/blog/EfficientNet-review/, 2019.05.31
끝
'1. 모델 분석 > Segmentation' 카테고리의 다른 글
Segmentation of the lungs on a Chest X-ray image (0) | 2021.10.20 |
---|---|
[2D Multi-Class Semantic Segmentation] 3. Model Training, Evaluation and Result (0) | 2021.05.31 |
[2D Multi-Class Semantic Segmentation] 1. Data Preprocessing (0) | 2021.05.31 |