지금까지 다뤘던 내용은 사진파일에 분류하고자 하는 대상을 둘레 상자(bounding box)를 이용해서 표현을 하였다. 이번에 다룰 이미지 분할(image segmentation)은 이미지를 이루고 있는 모든 픽셀에 클래스를 부여하여 이미지를 인식하는 방식이다.
합성곱 신경망(CNN)의 경우는 마지막 부분에 밀집 층을 만들어 동작을 처리하게 된다. 기본적으로 합성곱 연산과 풀링 연산이 들어가기 때문에 입력값에 비해 출력갑의 차원이 감소하는 경우가 대부분이지만 유자형 신경망(U-Net)의 경우는 이미지 분할이 필요하기 때문에 전치 합성곱(transposed convolution)을 이용해서 확장 샘플링(upsampling)을 진행한다.
유자형 신경망의 이전 단계인 완전 합성곱 신경망(FCN)의 경우는 신경망의 마지막 부근에서 전치 합성곱을 이용해 원래의 이미지 크기로 복구하는 작업을 하게된다. 하지만 완전 합성곱 신경망의 경우는 많은 압축으로 인한 정보 손실에 의해서 정확한 결과값을 얻을 수 없다는 단점이 있다.
유자형 신경망의 경우는 완전 합성곱 신경망과 유사하지만 마지막 층에서 한번에 전치 합성곱을 통해 복구를 시키는 것이 아닌 축소(downsampling)과 확장(upsampling)의 수를 맞추어서 원본의 이미지 사이즈로 복구하게 된다. 또한 스킵 연결(skip connection)을 이용하여 확장 과정에서 계산 비용을 낮추면서 미세한 정보를 참고할 수 있도록 한다. 이는 오버피팅과 정보 손실을 막는 데에 도움을 준다.
합성곱 신경망과 활성함수(ReLU)를 이용해서 이미지를 축소해 나가며 일부 신경망에서 최대치 풀링과 드랍아웃을 적용한다. 인코딩 과정에서 사용한는 conv_block()
함수는 다음과 같다.
def conv_block(inputs=None, n_filters=32, dropout_prob=0, max_pooling=True):
conv = Conv2D(n_filters,
3,
activation='relu',
padding='same',
kernel_initializer=tf.keras.initializers.HeNormal())(inputs)
conv = Conv2D(n_filters,
3,
activation='relu',
padding='same',
kernel_initializer=tf.keras.initializers.HeNormal())(conv)
if dropout_prob > 0:
conv = Dropout(dropout_prob)(conv)
if max_pooling:
next_layer = MaxPooling2D(pool_size=2)(conv)
else:
next_layer = conv
skip_connection = conv
return next_layer, skip_connection
conv_block()
은 반환값으로 두 개의 결과값이 있는데, 첫번째 값은 다음 층에 쓰일 활성화 값이며 두번째 값은 스킵 연결을 위한 풀링 이전의 값이다. 필터의 초기값 선언은 He Initialization 방식을 사용한다.
확장 과정에서는 먼저 전치 합성곱 연산을 이용해 확장을 하고 스킵 연결을 통해 활성화 값을 붙이는 작업을 수행한다. 이후에 합성곱 신경망을 연결하여 다음 층으로 전달을 하게된다.
def upsampling_block(expansive_input, contractive_input, n_filters=32):
up = Conv2DTranspose(
n_filters,
3,
strides=2,
padding='same')(expansive_input)
merge = concatenate([up, contractive_input], axis=3)
conv = Conv2D(n_filters,
3,
activation='relu',
padding='same',
kernel_initializer=tf.keras.initializers.HeNormal())(merge)
conv = Conv2D(n_filters,
3,
activation='relu',
padding='same',
kernel_initializer=tf.keras.initializers.HeNormal())(conv)
return conv
concatenation
에서 채널의 연결을 해주며 이후 합성곱 신경망으로 연결된 merge
값을 전달한다. 마찬가지로 필터의 초기값 선언은 He Initialization 방식을 사용한다.
위에서 만든 두 함수를 연결하여 새로운 모델을 만들도록 하자. 유자형 신경망은 VGG에서 사용했던 방식처럼 높이와 너비가 절반씩 주는 반면에 채널의 수를 두배씩 증가시키는 방식을 사용하였다.
def unet_model(input_size=(96, 128, 3), n_filters=32, n_classes=23):
inputs = Input(input_size)
cblock1 = conv_block(inputs, n_filters)
cblock2 = conv_block(cblock1[0], n_filters*2)
cblock3 = conv_block(cblock2[0], n_filters*4)
cblock4 = conv_block(cblock3[0], n_filters*8, dropout_prob=0.3)
cblock5 = conv_block(cblock4[0], n_filters*16, dropout_prob=0.3, max_pooling=False)
ublock6 = upsampling_block(cblock5[0], cblock4[1], n_filters*8)
ublock7 = upsampling_block(ublock6, cblock3[1], n_filters*4)
ublock8 = upsampling_block(ublock7, cblock2[1], n_filters*2)
ublock9 = upsampling_block(ublock8, cblock1[1], n_filters)
conv9 = Conv2D(n_filters,
3,
activation='relu',
padding='same',
kernel_initializer='he_normal')(ublock9)
conv10 = Conv2D(n_classes, 1, padding='same')(conv9)
model = tf.keras.Model(inputs=inputs, outputs=conv10)
return model
위의 모델의 손실함수를 적용하고 최적화 방식을 정의하도록 하자.
unet.compile(optimizer='adam',
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
SparseCategoricalCrossentropy
는 내부적으로 원핫 인코딩을 통해서 가장 높은 값의 색인값을 통해서 손실함수를 정의하게 된다. 만약 예측값이 이미 원핫 인코딩이 돼있는 상태라면 CategoricalCrossentropy
방식을 이용해서 계산을 할 수 있다. fit()
함수를 이용해서 학습을 시키면 마무리가 된다. 추가로 예측한 분학 이미지를 출력할 때는 채널에서 가장 큰 값을 가지는 색인을 필터링하여 출력을 해야한다.