Tensorflow 2.0 Tutorial ch9.5 - 이미지 분할

Page content

공지

  • 본 Tutorial은 교재 시작하세요 텐서플로 2.0 프로그래밍의 강사에게 국비교육 강의를 듣는 사람들에게 자료 제공을 목적으로 제작하였습니다.

  • 강사의 주관적인 판단으로 압축해서 자료를 정리하였기 때문에, 자세하게 공부를 하고 싶으신 분은 반드시 교재를 구매하실 것을 권해드립니다.

  • 본 교재 외에 강사가 추가한 내용에 대한 Reference를 확인하셔서, 추가적으로 학습하시는 것을 권유드립니다.

Tutorial

이전 강의가 궁금하신 분들은 아래에서 선택하여 추가 학습 하시기를 바랍니다.

I. 개요

  • 이미지에서 단순히 경계선을 추출하는 작업은 전통적인 알고리즘의 필터나 한 층의 컨볼루션 레이어로도 가능하지만, 의미 있는 부분과 그렇지 않은 부분으로 분할하기 위해서는 학습이 필요합니다.
  • 앞 절에서 정의한 REDNet을 조금만 수정하면 이미지 분할(Segmentation)에서 사용할 수 있습니다.
  • 이미지의 경계선과 내용, 그리고 외곽의 3가지로 분류하는 Oxford-IIIT Pet 데이터세트로 이미지 분할 문제를 학습합니다.
  • 교재에 있는 코드에서 몇몇 에러가 발생하였습니다. 내용상 텐서플로 홈페이지와 유사하여 텐서플로 공식 홈페이지에 있는 소스코드를 참고하였습니다.
  • 먼저 필수 파일들을 pip 도구를 활용하여 설치합니다.
!pip install git+https://github.com/tensorflow/examples.git
!pip install -U tfds-nightly
Collecting git+https://github.com/tensorflow/examples.git
  Cloning https://github.com/tensorflow/examples.git to /tmp/pip-req-build-36g0gu68
  Running command git clone -q https://github.com/tensorflow/examples.git /tmp/pip-req-build-36g0gu68
Requirement already satisfied: absl-py in /usr/local/lib/python3.6/dist-packages (from tensorflow-examples===63ee35adcc3e3dd2d228bc3283e27f6a1e2158ab-) (0.9.0)
Requirement already satisfied: six in /usr/local/lib/python3.6/dist-packages (from tensorflow-examples===63ee35adcc3e3dd2d228bc3283e27f6a1e2158ab-) (1.12.0)
Building wheels for collected packages: tensorflow-examples
  Building wheel for tensorflow-examples (setup.py) ... [?25l[?25hdone
  Created wheel for tensorflow-examples: filename=tensorflow_examples-63ee35adcc3e3dd2d228bc3283e27f6a1e2158ab_-cp36-none-any.whl size=125226 sha256=f2f0d0a9e57edde6593979e55a26983574dba25b3f7008be261173181109f5b8
  Stored in directory: /tmp/pip-ephem-wheel-cache-f32yqfdy/wheels/83/64/b3/4cfa02dc6f9d16bf7257892c6a7ec602cd7e0ff6ec4d7d714d
Successfully built tensorflow-examples
Installing collected packages: tensorflow-examples
Successfully installed tensorflow-examples-63ee35adcc3e3dd2d228bc3283e27f6a1e2158ab-
Collecting tfds-nightly
[?25l  Downloading https://files.pythonhosted.org/packages/8c/42/df5f05f2124f9d1b6e16b88c518f04a7d282d77daf3113ba548baadc4ce5/tfds_nightly-3.1.0.dev202005100106-py3-none-any.whl (3.3MB)
     |████████████████████████████████| 3.3MB 11.6MB/s 
[?25hRequirement already satisfied, skipping upgrade: wrapt in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (1.12.1)
Requirement already satisfied, skipping upgrade: requests>=2.19.0 in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (2.23.0)
Requirement already satisfied, skipping upgrade: tqdm in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (4.41.1)
Requirement already satisfied, skipping upgrade: attrs>=18.1.0 in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (19.3.0)
Requirement already satisfied, skipping upgrade: future in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (0.16.0)
Requirement already satisfied, skipping upgrade: absl-py in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (0.9.0)
Requirement already satisfied, skipping upgrade: dill in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (0.3.1.1)
Requirement already satisfied, skipping upgrade: promise in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (2.3)
Collecting tensorflow-metadata<0.16,>=0.15
  Downloading https://files.pythonhosted.org/packages/3b/0c/afb81ea6998f6e26521671585d1cd9d3f7945a8b9834764e91757453dc25/tensorflow_metadata-0.15.2-py2.py3-none-any.whl
Requirement already satisfied, skipping upgrade: six in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (1.12.0)
Requirement already satisfied, skipping upgrade: protobuf>=3.6.1 in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (3.10.0)
Requirement already satisfied, skipping upgrade: numpy in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (1.18.4)
Requirement already satisfied, skipping upgrade: termcolor in /usr/local/lib/python3.6/dist-packages (from tfds-nightly) (1.1.0)
Requirement already satisfied, skipping upgrade: chardet<4,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19.0->tfds-nightly) (3.0.4)
Requirement already satisfied, skipping upgrade: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19.0->tfds-nightly) (2020.4.5.1)
Requirement already satisfied, skipping upgrade: idna<3,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19.0->tfds-nightly) (2.9)
Requirement already satisfied, skipping upgrade: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests>=2.19.0->tfds-nightly) (1.24.3)
Requirement already satisfied, skipping upgrade: googleapis-common-protos in /usr/local/lib/python3.6/dist-packages (from tensorflow-metadata<0.16,>=0.15->tfds-nightly) (1.51.0)
Requirement already satisfied, skipping upgrade: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf>=3.6.1->tfds-nightly) (46.1.3)
Installing collected packages: tensorflow-metadata, tfds-nightly
  Found existing installation: tensorflow-metadata 0.21.2
    Uninstalling tensorflow-metadata-0.21.2:
      Successfully uninstalled tensorflow-metadata-0.21.2
Successfully installed tensorflow-metadata-0.15.2 tfds-nightly-3.1.0.dev202005100106

II. REDNet[^1]

  • REDNetResidual Encoder-Decoder Network의 약자이며, ResidualResNet등에서 사용하는 건너뛴 연결(skip-connection)입니다.
  • 다수의 레이어가 중첩되는 구조에서 앞쪽의 정보를 잃어버리기 않기 위해 뒤쪽에 정보를 그대로 전달해줄 때 건너뛴 연결이 사용됩니다.
import tensorflow as tf
from tensorflow_examples.models.pix2pix import pix2pix

import tensorflow_datasets as tfds
tfds.disable_progress_bar()

from IPython.display import clear_output
import matplotlib.pyplot as plt

III. 데이터 불러오기

  • tf.keras.utils.get_file() 데이터를 불러옵니다.
  • 교재에서는 oxford_iiit_pet:3.0.0으로 되어 있었는데, 버전을 3.*.*으로 수정하여 다운로드를 하기를 바랍니다.
dataset, info = tfds.load('oxford_iiit_pet:3.*.*', with_info=True)
Downloading and preparing dataset oxford_iiit_pet/3.2.0 (download: 773.52 MiB, generated: 774.69 MiB, total: 1.51 GiB) to /root/tensorflow_datasets/oxford_iiit_pet/3.2.0...
Shuffling and writing examples to /root/tensorflow_datasets/oxford_iiit_pet/3.2.0.incompleteONHCBY/oxford_iiit_pet-train.tfrecord
Shuffling and writing examples to /root/tensorflow_datasets/oxford_iiit_pet/3.2.0.incompleteONHCBY/oxford_iiit_pet-test.tfrecord
Dataset oxford_iiit_pet downloaded and prepared to /root/tensorflow_datasets/oxford_iiit_pet/3.2.0. Subsequent calls will reuse this data.
info
tfds.core.DatasetInfo(
    name='oxford_iiit_pet',
    version=3.2.0,
    description='The Oxford-IIIT pet dataset is a 37 category pet image dataset with roughly 200
images for each class. The images have large variations in scale, pose and
lighting. All images have an associated ground truth annotation of breed.',
    homepage='http://www.robots.ox.ac.uk/~vgg/data/pets/',
    features=FeaturesDict({
        'file_name': Text(shape=(), dtype=tf.string),
        'image': Image(shape=(None, None, 3), dtype=tf.uint8),
        'label': ClassLabel(shape=(), dtype=tf.int64, num_classes=37),
        'segmentation_mask': Image(shape=(None, None, 1), dtype=tf.uint8),
        'species': ClassLabel(shape=(), dtype=tf.int64, num_classes=2),
    }),
    total_num_examples=7349,
    splits={
        'test': 3669,
        'train': 3680,
    },
    supervised_keys=('image', 'label'),
    citation="""@InProceedings{parkhi12a,
      author       = "Parkhi, O. M. and Vedaldi, A. and Zisserman, A. and Jawahar, C.~V.",
      title        = "Cats and Dogs",
      booktitle    = "IEEE Conference on Computer Vision and Pattern Recognition",
      year         = "2012",
    }""",
    redistribution_info=,
)

Dataset의 주요 정보를 구성하고 있는 부분은 features입니다. 여기에는 image, label, segmentation_mask가 보입니다.

  • label은 각 애완동물에 대한 클래스 정보를 담고 있습니다.
  • image는 3차원인 것으로 보아 컬러 이미지일 것이라고 추측할 수 있습니다.
  • segmentation_mask는 마지막 차원이 1로 구성된 것을 볼 때 흑백 이미지처럼 다룰 수 있다고 추측해 볼 수 있습니다.

IV. REDNET 모형 학습

모형 학습을 위해 데이터 수집 부터 모델 정의까지 진행합니다.

(1) 데이터 정규화

The following code performs a simple augmentation of flipping an image. In addition, image is normalized to [0,1]. Finally, as mentioned above the pixels in the segmentation mask are labeled either {1, 2, 3}. For the sake of convenience, let’s subtract 1 from the segmentation mask, resulting in labels that are : {0, 1, 2}.

  • 다음 코드는 이미지를 뒤집는 Simple Augmentation을 수행합니다.
  • 또한 이미지는 [0,1]로 정규화 합니다.
  • 마지막으로, 위에서 언급한 바와 같이 분할 마스크의 픽셀은 {1, 2, 3} 중 하나로 라벨이 표시되어 있다.
  • 편의상 분할 마스크에서 1을 빼서 {0, 1, 2}의 레이블을 생성해 봅시다.
def normalize(input_image, input_mask):
  input_image = tf.cast(input_image, tf.float32) / 255.0
  input_mask -= 1
  return input_image, input_mask
  • 여기에서 마스크에서 1을 빼는 연산이 있습니다. 이 부분은 데이터를 출력 후 재 설명하도록 합니다.

(2) 훈련데이터, 테스트데이터 불러오기

  • 훈련데이터와 테스트 불러오는 함수를 작성합니다.
@tf.function
def load_image_train(datapoint):
  input_image = tf.image.resize(datapoint['image'], (128, 128))
  input_mask = tf.image.resize(datapoint['segmentation_mask'], (128, 128))

  if tf.random.uniform(()) > 0.5:
    input_image = tf.image.flip_left_right(input_image)
    input_mask = tf.image.flip_left_right(input_mask)

  input_image, input_mask = normalize(input_image, input_mask)

  return input_image, input_mask
def load_image_test(datapoint):
  input_image = tf.image.resize(datapoint['image'], (128, 128))
  input_mask = tf.image.resize(datapoint['segmentation_mask'], (128, 128))

  input_image, input_mask = normalize(input_image, input_mask)

  return input_image, input_mask
TRAIN_LENGTH = info.splits['train'].num_examples
BATCH_SIZE = 64
BUFFER_SIZE = 1000
STEPS_PER_EPOCH = TRAIN_LENGTH // BATCH_SIZE
  • 데이터를 불러오면서 imageflip을 주어 변형시켰다. 또한, imagemask를 통해 정규화를 진행하고, 128 X 128로 변환합니다. 크기가 다르면 tf.keras에서 학습이 되지 않기 때문에 그렇습니다.
  • 이제 데이터를 불러오도록 합니다.
train = dataset['train'].map(load_image_train, num_parallel_calls=tf.data.experimental.AUTOTUNE)
test = dataset['test'].map(load_image_test)

train_dataset = train.cache().shuffle(BUFFER_SIZE).batch(BATCH_SIZE).repeat()
train_dataset = train_dataset.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
test_dataset = test.batch(BATCH_SIZE)
  • 정의된 load_image_* 함수로 이미지와 마스크를 반환하고, 데이터를 계속 학습시킬 수 있도록 repeat() 함수를 적용합니다.
  • 그리고, 배치 사이즈를 각각 적용하도록 합니다.

(3) 시각화 함수 정의 및 시각화

우선 함수를 정의하도록 합니다.

def display(display_list):
  plt.figure(figsize=(15, 15))

  title = ['Input Image', 'True Mask', 'Predicted Mask']

  for i in range(len(display_list)):
    plt.subplot(1, len(display_list), i+1)
    plt.title(title[i])
    plt.imshow(tf.keras.preprocessing.image.array_to_img(display_list[i]))
    plt.axis('off')
  plt.colorbar()  
  plt.show()
  

정의된 함수를 사용하여 이미지와 마스크를 출력합니다.

for image, mask in train.take(1):
  sample_image, sample_mask = image, mask
display([sample_image, sample_mask])

png

  • 왼쪽은 원본 이미지가 보이고, 오른쪽에는 마스크와 각 숫자의 값을 나타내는 colorbar를 표시했습니다.
  • 마스크 데이터에는 중심부, 외곽선 배경을 뜻하는 1, 3, 2의 숫자가 각 픽셀에 대해 저장되어 있습니다.
  • 이미지 분할 문제는 기본적으로 각 픽셀을 분류하는 문제이기 때문에 라벨이 0부터 시작할 수 있게 1,2,3을 0,1,2로 바꿔줍니다.

(4) 모형의 정의

  • 이미지 분할 네트워크의 학습은 원본 이미지를 입력했을 때 마스크를 출력하게 합니다. 이를 위해서는 ch9_4에서 사용했던 REDNet에서 마지막 레이어를 수정합니다.
def REDNet_segmentation(num_layers):
    conv_layers = []
    deconv_layers = []
    residual_layers = []

    inputs = tf.keras.layers.Input(shape=(None, None, 3))
    conv_layers.append(tf.keras.layers.Conv2D(3, kernel_size=3, padding='same', activation='relu'))

    for i in range(num_layers-1):
        conv_layers.append(tf.keras.layers.Conv2D(64, kernel_size=3, padding='same', activation='relu'))
        deconv_layers.append(tf.keras.layers.Conv2DTranspose(64, kernel_size=3, padding='same', activation='relu'))

    deconv_layers.append(tf.keras.layers.Conv2DTranspose(3, kernel_size=3, padding='same', activation='softmax'))

    x = conv_layers[0](inputs)

    for i in range(num_layers-1):
        x = conv_layers[i+1](x)
        if i % 2 == 0:
            residual_layers.append(x)

    for i in range(num_layers-1):
        if i % 2 == 1:
            x = tf.keras.layers.Add()([x, residual_layers.pop()])
            x = tf.keras.layers.Activation('relu')(x)
        x = deconv_layers[i](x) 

    x = deconv_layers[-1](x)
    
    model = tf.keras.Model(inputs=inputs, outputs=x)
    return model
  • 수정된 부분은 마지막 레이어의 활성화함수입니다. 원래 활성화 함수가 없었기 때문에, linear 활성화함수로 입력값을 그대로 출력하던 것을 소프트맥스 활성화함수로 교체합니다.

  • 분류 문제로 바뀌었기 때문에 네트워크를 컴파일할 때의 loss도 바꿔줍니다.

model = REDNet_segmentation(15)
model.compile(optimizer=tf.optimizers.Adam(0.0001),
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
  • 분류를 위해 losssparse_categorical_crossentropy를 사용했고, 분류의 정확도를 측정하기 위해 metricsaccuracy를 넣었습니다.

  • 모형의 구조를 출력해봅니다.

tf.keras.utils.plot_model(model, show_shapes=True)

png

(5) 정의된 모형의 예측

  • 모형을 학습시키기 전에 간단하게 예측을 통해서 어떤 결과값이 나타나는지 확인해봅니다.
def create_mask(pred_mask):
  pred_mask = tf.argmax(pred_mask, axis=-1)
  pred_mask = pred_mask[..., tf.newaxis]
  return pred_mask[0]
def show_predictions(dataset=None, num=1):
  if dataset:
    for image, mask in dataset.take(num):
      pred_mask = model.predict(image)
      display([image[0], mask[0], create_mask(pred_mask)])
  else:
    display([sample_image, sample_mask,
             create_mask(model.predict(sample_image[tf.newaxis, ...]))])
show_predictions()

png

(6) 모형 학습

이제 본격적으로 네트워크를 학습해봅니다.

class DisplayCallback(tf.keras.callbacks.Callback):
  def on_epoch_end(self, epoch, logs=None):
    clear_output(wait=True)
    show_predictions()
    print ('\nSample Prediction after epoch {}\n'.format(epoch+1))
EPOCHS = 20
VAL_SUBSPLITS = 5
VALIDATION_STEPS = info.splits['test'].num_examples//BATCH_SIZE//VAL_SUBSPLITS

model_history = model.fit(train_dataset, epochs=EPOCHS,
                          steps_per_epoch=STEPS_PER_EPOCH,
                          validation_steps=VALIDATION_STEPS,
                          validation_data=test_dataset,
                          callbacks=[DisplayCallback()])

png

Sample Prediction after epoch 20

57/57 [==============================] - 112s 2s/step - loss: 0.5203 - accuracy: 0.7806 - val_loss: 0.5203 - val_accuracy: 0.7839
  • 이미지 분할은 초해상도 이미지를 얻는 문제보다는 쉽기 때문에 에포크를 20으로 설정했습니다.
  • 학습결과 훈련 데이터와 검증 데이터에서의 정확도는 각각 78%, 78%로 확인되었습니다.
  • 가운데 정답 마스크고, 가장 오른쪽이 예측결과인데, 몸통 부위에 대해 예측을 하지 못했습니다.
  • 학습을 더 시켜도 될 것 같습니다.

(7) 학습 모형 결과 시각화

학습된 모형에 대해 시각화를 진행하도록 합니다.

loss = model_history.history['loss']
val_loss = model_history.history['val_loss']

epochs = range(EPOCHS)

plt.figure()
plt.plot(epochs, loss, 'r', label='Training loss')
plt.plot(epochs, val_loss, 'bo', label='Validation loss')
plt.title('Training and Validation Loss')
plt.xlabel('Epoch')
plt.ylabel('Loss Value')
plt.ylim([0, 1])
plt.legend()
plt.show()

png

  • 위 시각화를 볼 때, 에포크를 늘려도 무난할 것 같습니다.
  • 이미지 보강 등의 방법을 사용하면 더 좋은 결과를 얻을 수 있을 것입니다.

(8) 테스트 이미지 분할 확인

  • 각 행의 가운데 세로줄이 정답 마스크이고, 가장 오른쪽 열이 재구성된 마스크입니다.
show_predictions(test_dataset, 3)

png

png

png

  • 위 3가지에서 조금 주목할만 것이 있다면, 첫번째 사진입니다. 정답 마스크에 비해 예측 마스크의 노란색이 조금 더 옅어진 것을 확인할 수 있습니다. 이는 오히려 정답 이미지에 비해 오히려 세밀한 예측을 하고 있다고 보여집니다.
  • 다만, 전반적으로 이미지가 깨지는 부분이 많아서 에포크를 늘리거나 이미지 보강등의 기법으로 진행하면 더 좋은 결과를 얻어낼 수 있을 것 같습니다.

V. 결론 및 정리

이번 9장에서는 컨볼루션 레이어와 디컨볼루션 레이어를 대칭적으로 쌓아올려 만든 오토인코더에 대해 살펴봅니다. 기본적인 형태의 오토인코더는 자기 자신을 재생성하는 특징이 있고, 오토인코더의 입력에 저해상도 이미지를 넣으면 초해상도 이미지를 얻을 수 있고, 출력에 분할 이미지를 넣으면 이미지 분할 문제를 학습시킬 수도 있습니다.

오토인코더의 중심에서 잠재변수를 추출하여, 정보를 압축하는 과정에서 데이터의 특징을 가장 잘 나타내도록 학습되기 때문에 이를 클러스터링에 사용해 고차원의 데이터를 저차원으로 시각화도 할 수 있습니다.

VI. Reference

김환희. (2020). 시작하세요! 텐서플로 2.0 프로그래밍: 기초 이론부터 실전 예제까지 한번에 끝내는 머신러닝, 딥러닝 핵심 가이드. 서울: 위키북스.