Chapter 9장. 컴퓨터 비전을 위한 고급 딥러닝
작성자 : 이수원
들어가기 앞서,
앞 장에서는 간단한 모델과 예제를 통해 컴퓨터 비전에 딥러닝을 처음 적용해보았다.
하지만, 컴퓨터 비전에는 이미지 분류 외에도 더 많은 것이 있다.
이번 장에서는 컴퓨터 비전의 다양한 애플리케이션과 고급 모범 사례에 대해 조금 더 자세히 알아보도록 한다.
- 들어가기 앞서,
- 9.1 세가지 주요 컴퓨터 비전 작업
- 9.2 이미지 분할 예제
- 9.3 CNN 아키텍처 주요 유형
- 9.3.2 잔차 연결
- Conv2D 층은 padding=”same” 옵션을 사용하여 모양을 유지한다.
- 필터수가 변하는 경우, 잔차에 Conv2D 층을 이용하여 필터수를 맞춘다.
- 필터 크기는 1x1을 사용하며, 활성화 함수는 없다.
- 입력층
- 하나의 Conv2D 은닉층
- SeparableConv2D, BatchNormalization, MaxPooling2D 층으로 구성된 모듈 쌓기
- 잔차 연결 활용
- 마지막 은닉층은 GlobalAveragePooling2D과 Dropout
- 출력층
- 모델 지정
- 9.4 참고 문헌
- 9.5 연습 문제
### Computer Vision이 무엇인가요?
컴퓨터 비전은 시각적 세계를 해석하고 이해하도록 컴퓨터를 학습시키는 인공 지능 분야이다. 컴퓨터가 카메라와 동영상에서 디지털 이미지와 딥러닝 모델을 사용하여 정확하게 식별하고 분류하는 학습을 마치면, “관찰” 대상에도 반응을 할 수 있다.
외 컴퓨터 비전에 대해 자세히 알고 싶은 사람들은 아래 ‘Computer Vision’을 참조하여 글을 읽어보는 것도 좋다. 해당 글에서는 개념만 알고 넘어가는 것으로 한다.
9.1 세가지 주요 컴퓨터 비전 작업
지금까지 우리는 이미지 분류 모델을 주로 다루었다.
하지만 이미지 분류는 컴퓨터 비전에 적용할 수 있는 여러 딥러닝 애플리케이션 중 하나에 불과하다.
일반적으로 3개의 주요 컴퓨터 비전 작업을 알아 둘 필요가 있다.
-
이미지 분류(Image classification) : 이미지에
하나 이상의 레이블을 할당하는 것
이 목표이다. 단일 레이블 분류(하나의 이미지는 다른 범주에 배타적으로 한 범주에만 속한다.)이거나 다중 레이블 분류(이미지가 속한 모든 레이블을 할당한다.) 예를 들어, 구글 포토 앱에서 키워드 검색을 할 때 서버에서는 대규모 다중 레이블 분류 모델이 실행된다. 이 모델은 수백만 개의 이미지에서 훈련되고 2만 개 이상의 클래스를 가지고 있다.
-
이미지 분할(Image Segmentation) : 이미지를 다른 영역으로
나누거나 분할하는 것
이 목표이다. 예를 들어, 줌(Zoom)이나 구글 밋(Google Meet)은 화상 회의를 하는 동안 사용자가 지정한 이미지를 배경으로 출력한다. 이때 이미지 분할 모델을 사용하면 픽셀 수준에서 사용자의 얼굴과 배경을 분리하게 된다.
-
객체 탐지(Object Detection) : 이미지에 있는 관심 객체 주변에 (바운딩 박스(bounding box)라고 부르는) 사각형을 그리는 것이 목표이다. 각 사각형은 하나의 클래스에 연관된다. 예를 들어, 자율 주행 자동차는 객체 탐지 모델을 사용하여 카메라 화면에서 자동차, 보행자, 표지판 등을 감지할 수 있다.
또한, 컴퓨터 비전을 위한 딥러닝은 위 세 가지 외에도 여러 가지 틈새 분야에 해당하는 작업이 있다. 예를 들면,
- 이미지 유사도 평가(Image similarity scoring)
- 키포인트 감지(Keypoint Detection)
- 포즈 추정(Pose Estimation)
- 3D 메시 추정(3D Mesh Estimation)
등이 있다.
그러나 처음에 모든 머신 러닝 엔지니어가 알아야 할 기초는 이미지 분류, 이미지 분할, 객체 탐색 이 3가지이다. 대부분의 컴퓨터 비전 애플리케이션은 이 셋 중 하나로 분류 할 수가 있다.
이전 장에서는 이미지 분류 예제를 다루었을 것이다. 이번 장에서는 이미지 분할에 대해 알아보도록 할 예정이다.
_객체 탐지의 경우 입문서에 담기에 너무 전문적이고 복잡하기 때문에 keras.io에서 약 450줄의 케라스 코드로 객체 탐지 모델을 밑바닥부터 만들고 훈련하는 RetinaNet 예제를 참고하라.
9.2 이미지 분할 예제
딥러닝을 사용한 이미지 분할은 모델을 사용하여 이미지 안의 각 픽셀에 클래스를 할당하는 것
이다. 즉, 이미지를 여러 다른 영역(‘배경’과 ‘전경’, 또는 ‘도로’, ‘자동차’, ‘보도’)으로 분할한다. 이런 종류의 기술은 이미지와 비디오 편집, 자율 주행, 로봇 공학, 의료 영상 등 여러 가지 유용한 애플리케이션을 만드는데 사용되고 있다.
그렇다면 이미지 분할에 대해 자세히 알아보자.
이미지 분할에는 2가지 종류가 있다.
- 시맨틱 분할(Semantic Segmentation) : 각 픽셀이 독립적으로 ‘cat’과 같은 하나의의미를 가진 범주로 분류된다. 이미지에 2마리의 고양이가 있다면, 이에 해당되는 모든 픽셀은 동일한 ‘cat’ 범주로 매핑된다.
- 인스턴스 분할(Instance Segmentation) : 이미지 픽셀을 범주로 분류하는 것 뿐만 아니라 개별 객체 인스턴스를 구분한다. 이미지에 2개의 고양이가 있다면 이스턴스 분할은 ‘cat1’과 ‘cat2’를 2개의 별개의 클래스로 다루게 된다.
이번 예제에서는 Semantic Segmentation에 초점을 맞추도록 한다. 해당 예제에서도 고양이와 강아지 이미지를 사용하지만, 주피사체를 배경에서 분리하는 방법을 배울 것이다.
데이터셋은 Oxford-IIIT 애완동물 데이터셋을 이용한다.
데이터셋은 강아지와 고양이를 비롯해서 37종의 애완동물의 다양한 크기와 다양한 자세를 담은 7,390장의 사진으로 구성된다.
- 데이터셋 크기 : 7,390
- 총 클래스 수 : 37
- 클래스 별 사진 수 : 약 200 장
- 사진 별 레이블 : 종과 품종, 머리 표시 경계상자, 트라이맵 분할(trimap segmentaion) 마스크
트라이맵 분할 마스크?
트라이맵 분할 마스크는 원본 사진과 동일한 크기의 흑백 사진이며, 각각의 픽셀은 1, 2, 3 셋 중에 하나의 값을 갖는다.
- 1 : 사물
- 2 : 배경
- 3 : 윤각
이미지 분할 모델 구성
이미지 분할 모델의 구성은 기본적을 Conv2D
층으로 구성된 다운샘플링 블록(downsampling block)과 Conv2DTranspose
층으로 구성된 업샘플링 블록(upsampling block)으로 이루어진다.
<다운샘플링 블록 : Conv2D 층 활용>
1. 이미지 분류 모델과 동일한 기능
2. 보폭(strides=2)을 사용하는 경우와 그렇지 않은 경우를 연속적으로 적용. 따라서 MaxPolling2D는 사용하지 않음.
3. 패팅(padding="same")을 사용하지만, 보폭을 2로 두기 때문에 이미지 크기는 가로, 세로 모두 1/2로 줄어든다.
4. 채널 수는 동일한 방식으로 2배로 증가
<업샘플링 블록 : Conv2DTranspose 층 활용>
1. 이미지 분할 모델의 최종 출력값은 원본 이미지의 트라이맵 분할 이미지이다. 따라서 동일한 크기의 numpy array를 출력해야 함.
2. Conv2D 층을 통과하면서 작아진 Tensor를 원본 이미지 크기로 되돌리는 기능 수행.
3. 따라서 Conv2D 층이 적용된 역순으로 크기를 되돌려야 함.
4. 모델 훈련 과정에서 어떤 값들을 사용하여 모양을 되돌릴지 학습함.
코드 작성
먼저 wget과 tar 셀 명령으로 데이터셋을 내려받고 압축을 풀어준다.
!wget http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz
!wget http:///www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz
!tar -xf images.tar.gz
!tar -xf annotations.tar.gz
입력 파일 경로와 분할 마스크 파일 경로를 각각 리스트로 구성한다.
import os
input_dir = "/images/" # 입력 사진은 images/ 폴더에 저장
target_dir = "annotations/trimaps/" # 이미지에 해당하는 분할 마스크가 저장된 폴더
input_img_paths = sorted(
[os.path.join(input_dir, fname)
for fname in os.listdir(input_dir)
if fname.endswith(".jpg")]
)
target_img_paths = sorted(
[os.path.join(target_dir, fname)
for fname in os.listdir(target_dir)
if fname.endswith(".png") and not fname.startswith(".")]
)
여기서 입력과 분할 마스크의 모습을 확인하고 싶다면, 아래 코드를 작성한다.
import matplotlib.pyplot as plt
from tensorflow.keras.utils import load_img, img_to_array
plt.axls("off")
plt.imshow(load_img(input_image)paths[9])
# [] 안에 원하는 인덱스를 입력한다.
#그러면 해당하는 인덱스의 이미지가 출력된다.
이에 해당하는 분할 마스크(taget)은 다음과 같다.
def display_target(target_array):
normalized_array = (target_array.astype("uint8") - 1) * 127
plt.axis("off")
plt.imshow(normalized_array[:,:,0])
img = img_to_array(load_img(target_paths[9], color_mode="grayscale"))
display_target(img)
위 2개의 코드를 작성하면 어떤 결과가 나오는가?
위와 같이, 전경과 배경, 윤곽이 제대로 분리된 것을 알 수 있다.
다음으로는 입력과 타깃을 2개의 numpy 배열로 load하고, 이 배열을 훈련과 검증 세트로 나눈다.
import numpy as np
import random
img_size = (200, 200)
num_imgs = len(input_img_paths)
random.Random(1337).shuffle(input_img_paths)
random.Random(1337).shuffle(target_paths)
def path_to_input_image(path):
return img_to_array(load_img(path, target_size = img_size))
def path_to_target(path):
img = img_to_array(
load_img(path, target_size = img_size, color_mode = "grayscale"))
img = img.astype("uint8") - 1
return img
input_imgs = np.zeros((num_imgs,) + img_size + (3,), dytpe = "float32")
targets = np.zeros((num_imgs,) + img_size + (1, ), dtype = "unit8")
for i in range(num_imgs):
input_img[i] = path_to_input_image(input_img_paths[i])
targets[i] = path_to_target(target_paths[i])
num_val_samples = 1000
train_input_imgs = input_imgs[:-num_val_samples]
train_targets = targets[:-num_val_samples]
val_input_imgs = input_imgs[-num_val_samples:]
val_targets = targets[-num_val_samples:]
이제 모델을 정의해보자.
from tensorflow import keras
from tensorflow.keras import layers
def get_model(img_size, num_classes):
inputs = keras.Input(shape=img_size + (3,))
x = layers.Rescaling(1./255)(inputs)
x = layers.Conv2D(64, 3, strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, strides=2, activation="relu", padding="same")(x)
x = layers.Conv2D(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(256, 3, strides=2, padding="same", activation="relu")(x)
x = layers.Conv2D(256, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(256, 3, activation="relu", padding="same", strides=2)(x)
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(128, 3, activation="relu", padding="same", strides=2)(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same")(x)
x = layers.Conv2DTranspose(64, 3, activation="relu", padding="same", strides=2)(x)
outputs = layers.Conv2D(num_classes, 3, activation="softmax", padding="same")(x)
model = keras.Model(inputs, outputs)
return model
생성된 모델을 요약하면 다음과 같다.
model - get_model(img_size = img_size, num_classes = 3)
model.summary()
Model: "model"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) [(None, 200, 200, 3)] 0
_________________________________________________________________
rescaling (Rescaling) (None, 200, 200, 3) 0
_________________________________________________________________
conv2d (Conv2D) (None, 100, 100, 64) 1792
_________________________________________________________________
conv2d_1 (Conv2D) (None, 100, 100, 64) 36928
_________________________________________________________________
conv2d_2 (Conv2D) (None, 50, 50, 128) 73856
_________________________________________________________________
conv2d_3 (Conv2D) (None, 50, 50, 128) 147584
_________________________________________________________________
conv2d_4 (Conv2D) (None, 25, 25, 256) 295168
_________________________________________________________________
conv2d_5 (Conv2D) (None, 25, 25, 256) 590080
_________________________________________________________________
conv2d_transpose (Conv2DTran (None, 25, 25, 256) 590080
_________________________________________________________________
conv2d_transpose_1 (Conv2DTr (None, 50, 50, 256) 590080
_________________________________________________________________
conv2d_transpose_2 (Conv2DTr (None, 50, 50, 128) 295040
_________________________________________________________________
conv2d_transpose_3 (Conv2DTr (None, 100, 100, 128) 147584
_________________________________________________________________
conv2d_transpose_4 (Conv2DTr (None, 100, 100, 64) 73792
_________________________________________________________________
conv2d_transpose_5 (Conv2DTr (None, 200, 200, 64) 36928
_________________________________________________________________
conv2d_6 (Conv2D) (None, 200, 200, 3) 1731
=================================================================
Total params: 2,880,643
Trainable params: 2,880,643
Non-trainable params: 0
_________________________________________________________________
훈련 중 저장된 최고 성능의 모델을 불러와서 이미지 분할을 어떻게 진행했는지 하나의 이미지를 테스트 해보면 원본 이미지에 포함된 다른 요소들 때문에 약간의 잡음이 있지만, 대략적으로 이미지 분할을 잘 적용했음을 알 수 있다.
이번 예제는 모델 컴파일과 훈련 과정, 그리고 손실 그래프를 나타내는 과정은 생략한다.
9.3 CNN 아키텍처 주요 유형
모델의 ‘아키텍처(Architecture)’는 모델을 만드는데 사용된 일련의 선택이다. 사용할 층, 층의 설정, 층을 연결하는 방법 등이 포함된다. 이런 선택이 모델의 가설 공간(hypothesis space)을 정의한다. 경사 하강법이 검색할 수 있는 가능한 함수의 공간으로 파라미터는 모델의 가중치이다. 특성 공학과 마찬가지로 좋은 가설 공간은 현재 문제와 솔루션에 대한 사전 지식을 인코딩 한다. 예를 들어, 합성곱 층을 사용한다는 것은 입력 이미지에 있는 패턴이 이동 불변성이 있음을 미리 알고 있다는 뜻이다. 데이터에서 효율적으로 학습하기 위해 찾고 있는 것에 대한 가정을 해야한다.
즉, 모델 아키텍처(model architecture)는 모델 설계 방식
을 의미하며, 심층 신경망 모델을 구성할 때 매우 중요하다. 주어진 데이터셋과 해결해야 할 문제에 따라 적절한 층을 적절하게 구성해서 모델을 구현을 해야 하기 때문이다. 좋은 모델 아키텍처를 사용할 수록 적은 양의 데이터를 이용하여 보다 빠르게 좋은 성능의 모델을 얻을 가능성이 높아진다. 그러나 아쉽게도, 좋은 모델 아키텍처와 관련된 이론은 없으며, 많은 경험을 통환 직관
이 보다 중요한 역할을 수행한다.
여기서 실전에서 최고 성능을 발휘한 합성곱 신경망 아키텍처의 주요 유형을 몇 가지 살펴볼 것이다.
- 전차 연결(residual connections)
- 배치 정규화(batch normalization)
- 채널 분리 합성곱(depthwise separable convolutions)
9.3.1 모듈화, 계층화 그리고 재활용
복잡한 시스템을 단순하게 만들고 싶다면 일반적으로 사용할 수 있는 방법이 있다.
복잡한 구조를 모듈화(modularity)하고, 모듈을 계층화(hierarchy)하고, 같은 모듈을 적절하게 여러 곳에서 재사용(reuse)하는 것이다.
여기서 재사용은 추상화(abstraction)의 다른 말이다.
이것이 MHR(Modularit-Hierarchy-Reuse)
공식이다. 이는 ‘아키텍처(Architecture)’라는 용어가 사용되는 거의 모든 영역에 있는 시스템 구조의 기초가 된다.
합성공 신경망 모델 아키텍처의 특성
- 깊게 쌓아 올린 합성곱 신경망 모델은
기본적으로 모듈을 계층으로 쌓아 올린 구조
를 갖는다. 여기서 모듈은 여러 개의 층(layer)으로 구성되며, 블록(block)이라고 불리기도 한다. 하나의 모듈이 여러 번 재활용 되기도 한다. 예를 들어, 8장에서 다룬 VGG16 모델은 “Conv2D, Conv2D, MaxPooling2D”로 구성된 모듈을 계층으로 구성하였다. - 대부분의 합성곱 신경망 모델은
특성 피라미드 형식의 계층적 구조를 사용한다
는 점이다. VGG16의 경우, 필터 수를 32, 64, 128 등으로 수를 늘리는 반면에, 특성 맵(feature maps)의 크기는 그에 상응하여 줄여 나간다.
떄문에 계층 구조가 깊으면 특성 재사용과 이로 인한 추상화를 장려하기 때문에 본질적으로는 좋다. 일반적으로 작은 층을 깊게 쌓은 모델이 큰 층을 얇게 쌓는 것보다 성능이 좋다.
그러나 그레디언트 소실(vanishing gradient) 문제 때문에 층을 쌓을 수 있는 정도에 한계가 있다.
9.3.2 잔차 연결
일반적으로 많은 unit이 포함된 층을 몇 개 쌓는 것 보다, 적은 unit이 포하된 층을 높이 쌓을 때 모델의 성능이 좋아진다는 것을 이전 절에서 알았다. 하지만 층을 높이 쌓을수록 전달되어야 하는 손실값(오차)의 Vanishing Gradient Problem가 발생한다. 그리고 이를 극복하기 위해서 아키텍처(설계 방식)을 사용해야 한다. 즉, 역전파가 제대로 작동하지 못하는 상황을 말한다. 이를 위한 대표적인 아키텍처는 잔차 연결(Residual connections)
이다.
다음 그림은 모듈의 입력값을 모델을 통과하여 생성된 출력값과 합쳐서 다음 모델로 전달하는 아키텍처이다.
해당 방식을 통해 모델의 입력값에 대한 정보가 보다 정확하게 상위 모델에 전달되어, 그레디언트 소실 문제를 해결하는데 많은 도움을 준다. 실제로 잔차 연결을 이용하면, 모델을 매우 높게 쌓아도 모델 훈련이 가능할 수도 있다.
코드 작성
잔차 연결 핵심은 모양(shape)을 맞추는 것이다. 잔차 연결을 사용할 때 주의해야 할 기본사항은 모듈의 입력 Tensor와 출력 Tensor의 모양을 맞추는 일이다. 이때 MaxPooling 사용 여부에 따라 보폭(strides)의 크기가 달라진다.
- MaxPooling을 사용하지 않은 경우 :
Conv2D
에서 사용된 필터 수를 맞추는 데에만 주의하면 된다. ```pythonConv2D 층은 padding=”same” 옵션을 사용하여 모양을 유지한다.
필터수가 변하는 경우, 잔차에 Conv2D 층을 이용하여 필터수를 맞춘다.
필터 크기는 1x1을 사용하며, 활성화 함수는 없다.
from tensorflow import keras from tensorflow.keras import layer
inputs = keras.Input(shape=(32, 32, 3)) x = layers.Conv2D(32, 3, activation=”relu”)(inputs) residual = x x = layers.Conv2D(64, 3, activation=”relu”, padding=”same”)(x) # padding 사용 residual = layers.Conv2D(64, 1)(residual) # 필터 수 맞추기 x = layers.add([x, residual])
2. MaxPooling을 사용하는 경우 : 보폭을 활용해야 한다.
```python
# 잔차에 Conv2D층을 적용할 때 보폭을 사용한다.
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation="relu")(inputs)
residual = x
x = layers.Conv2D(64, 3, activation="relu", padding="same")(x)
x = layers.MaxPooling2D(2, padding="same")(x) # 맥스풀링
residual = layers.Conv2D(64, 1, strides=2)(residual) # 보폭 사용
x = layers.add([x, residual])
아래 코드는 잔차 연결을 사용하는 활용법을 보여준다.
MaxPooling과 필터 수에 따른 사용을 주의하라.
# 입력층
inputs = keras.Input(shape=(32, 32, 3))
x = layers.Rescaling(1./255)(inputs)
# 은닉층
def residual_block(x, filters, pooling=False):
residual = x
x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x)
x = layers.Conv2D(filters, 3, activation="relu", padding="same")(x)
if pooling: # 맥스풀링 사용하는 경우
x = layers.MaxPooling2D(2, padding="same")(x)
residual = layers.Conv2D(filters, 1, strides=2)(residual)
elif filters != residual.shape[-1]: # 필터 수가 변하는 경우
residual = layers.Conv2D(filters, 1)(residual)
x = layers.add([x, residual])
return x
x = residual_block(x, filters=32, pooling=True)
x = residual_block(x, filters=64, pooling=True)
x = residual_block(x, filters=128, pooling=False)
x = layers.GlobalAveragePooling2D()(x) # 채널 별로 하나의 값(채널 평균값) 선택
# 출력층
outputs = layers.Dense(1, activation="sigmoid")(x)
# 모델 설정
model = keras.Model(inputs=inputs, outputs=outputs)
잔차 연결을 사용하면 그레디언트 소실에 대해 걱정하지 않고 원하는 깊이의 네트워크를 만들 수 있다.
9.3.3 배치 정규화
정규화(normalization)는 다양한 모델의 샘플을 정규화를 통해 보다 유사하게 만들어 모델의 학습을 도와주고 훈련된 모델의 일반화 성능을 올려준다. 지금까지 살펴 본 정규화는 모델의 입력 데이터를 전처리 과정에서 평균을 0으로, 표준편차를 1로 만드는 방식이었다. 이는 데이터셋이 정규 분포를 따른다는 가정 하에서 진행된 정규화였다.
아래 그림은 주택가격 예측 데이터의 특성 중에 주택가격과 건축년수를 정규화한 경우(오른쪽)와 그렇지 않은 경우(왼쪽)의 데이터 분포의 변화를 보여준다.
하지만 입력 데이터셋에 대한 정규화가 층을 통과한 출력값의 정규화를 보장하는 것은 아니다. 따라서 다음 층으로 넘겨주기 전에 정규화를 먼저 진행하면 보다 훈련이 잘 될 수 있다. 더 나아가 출력값을 먼저 정규화 한 후에 활성화 함수를 적용할 때, 보다 좋은 성능의 모델이 구현될 수 있음이 밝혀졌다.
배치 정규화(Batch Normalization)가 바로 앞서 설명한 기능을 대신 처리하며, 2015년에 발표된 한 논문에서 소개가 되었다. 케라스의 경우 layers.BatchNormalization
층이 배치 정규화를 지원한다.
배치 정규화로 인한 모델 성능 향상에 대한 구체적인 이론은 아직 존재하지 않는다. 다만 경험적으로 많은 합성곱 신경망 모델의 성능에 도움을 준다는 사실만 알려져 있을 뿐이다. 잔차 연결과 함께 배치 정규화 또한 모델 훈련 과정에서 그레디언트 역전파에 도움을 주어 매우 깊은 심층 신경망 모델 훈련에 도움을 준다. 예를 들어 ResNet50, EfficientNet, Xxeption 모델 등은 배치 정규화 없이는 제대로 훈련이 되지 않는다.
배치 정규화 사용법
BatchNormalization
층을 Conv2D, Dense
등 임의의 층 다음에 사용할 수 있다.
주로 사용되는 형식은 다음과 같다.
x = ...
x = layers.Conv2D(32, 3, use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = ...
use_bias=False
옵션 : 배치 정규화에 의해 어차피 데이터의 평균값을 0으로 만들기에 굳이 편향 파라미터를 훈련 과정 중에 따로 학습 시킬 이유가 없다. 따라서 학습 되어야 할 파라미터 수가 아주 조금 줄어들어 학습 속도가 그만큼 빨라질 수 있다.- 활성화 함수 사용 위치 : 배치 정규화 이후에 활성화 함수를 실행한다. 이를 통해
relu()
활성화 함수의 기능을 극대화 될 수 있다.
모델 미세조정(Fine-Tuning)과 배치 정규화
배치 정규화 층이 포함된 모델을 미세조정할 때, 해당 배치 정규화 층을 동결(freeze)할 것을 추천한다. 미세조정의 경우, 모델 파라미터가 급격하게 변하지 않기에 배치 정규화에 사용된 평균값과 표준편차를 굳이 업데이트 할 필요가 없기 때문이라고 추정된다.
배치 정규화 예제
아래 그림은 2017년에 소개된 Xception 모델의 구조를 보여준다. 빨간 사각형으로 표시된 부분에 BatchNormalization
층이 사용되었다.
9.3.4 채널 분리 합성곱
케라스의 SeparableConv2D
층은 Conv2D
층 보다 적은 수의 가중치 파라미터를 사용하여, 보다 적은 양의 계산으로 성능이 더 좋은 모델을 생성한다.
2017년 Xception 모델 논문에서 소개되었으며, 당시 최고의 이미지 분류 성능을 보였다.
최신 이미지 분류 모델의 성능을 확인하고 싶으면 아래 ‘이미지 분류 모델의 성능 확인’을 참조하라.
SeparableConv2D 작동 과정
SeparableConv2D
는 필터를 채널 별로 적용한 후 나중에 채널 별 결과를 합친다. 이렇게 작동하는 층이 채널 분리 합성곱(Depthwise separable convolution) 층이며, 아래 그림처럼 채널 별로 생성된 결과를 합친 후 1x1
합성곱 신경망을 통과 시킨다.
책에서는 채널 분리 합성곱이 아닌 깊이별 분리 합성곱이라고 한다.
SeparableConv2D 작동 원리
이미지에 저장된 정보가 채널 별로 서로 독립적이라는 가정
을 사용한다. 따라서 채널 별로 서로 다른 필터를 사용한 후 결과들을 1x1
모양의 필터를 사용하여 합친다. 이떄 원하는 종류의 채널 수 만큼의 1x1
모양의 필터를 사용하여 다양한 정보를 추출한다.
Conv2D
와 SeparableConv2D
의 서로 다른 작동 과정은 다음과 같이 설명할 수 있다.
Conv2D
의 작동 원리
SeparableConv2D
의 작동원리
- padding = “same” 옵션을 사용함.
- 필터는 1개 사용함.
학습 가능한 파라미터 수 비교
채널 분리 합성곱 신경망이 Conv2D
층을 사용하는 경우보다 몇 배 이상 적은 수의 파라미터를 사용한다.
경우 1(위 작동 원리 비교) : 3x3
모양의 필터 64개를 3개의 채널을 갖는 입력 데이터에 사용할 경우 학습 가능한 파라미터 수는 각각 다음과 같다.
Conv2D
의 경우 : 3 * 3 * 3 * 64 = 1,728SeparableConv2D
의 경우 : 3 * 3 * 3 + ( 3 * 64 ) = 219
경우 2: 3x3
모양의 필터 64개를 10개의 채널을 갖는 입력 데이터에 사용할 경우 학습 가능한 파라미터 수는 각각 다음과 같다.
Conv2D
의 경우 : 3 * 3 * 10 * 64 = 5,760SeparableConv2D
의 경우 : 3 * 3 * 10 + ( 10 * 64 ) = 730
경우 3: 3x3
모양의 필터 64개를 64개의 채널을 갖는 입력 데이터에 사용할 경우 학습 가능한 파라미터 수는 각각 다음과 같다.
Conv2D
의 경우 : 3 * 3 * 64 * 64 = 36,864SeparableConv2D
의 경우 : 3 * 3 * 64 + ( 64 * 64 ) = 4,672
SeparableConv2D의 약점
채널 분리 합성곱에 사용되는 알고리즘이 CUDA에서 제대로 지원되지 않는다. 따라서 GPU를 사용하더라도 기존 Conv2D 층만을 사용한 모델에 비해 학습 속도에 별 차이가 없다. 즉 채널 분리 합성곱이 비록 훨씬 적은 수의 파라미터를 학습에 사용하지만, 이로 인해 시간 상의 이득은 주지 않는다. 하지만 적은 수의 파라미터를 사용하기에 일반화 성능이 보다 좋은 모델을 구현한다는 점이 중요하다.
참고 : CUDA와 cuDNN
- CUDA(Computer Unified Device Architecture) : CPU와 GPU를 동시에 활용하는 병렬 컴퓨팅을 지원하는 플랫폼으로, C, C++, FORTRAN 등의 저수준 언어에 활용한다.
- cuDNN(CUDA Deep Neural Network) : CUDA를 활용하여 딥러닝 알고리즘의 실행을 지원하는 라이브러리로, Conv2D 등 특정 딥러닝 알고리즘에 대해서만 최적화 되어있음.
예제 코드 : Xception 모델
케라스에서 지원하는 Xception 모델의 구성은 2017년 모델과는 조금 다르지만, 기본적으로 SeparableConv2D
와 BatchNormalization
층을 효율적으로 활용한 모듈을 잔차 연결과 함께 사용하여 깊게 쌓은 모델이다.
model = keras.applications.xception.Xception(
include_top=True, weights='imagenet', input_tensor=None,
input_shape=None, pooling=None, classes=1000,
classifier_activation='softmax'
)
9.3.5 예제 : 미니 Xception 모델
미니 Xception 모델을 직접 구현하여 강아지-고양이 이항 분류 작업을 실행해보자.
모델 구현에 사용되는 기법은 다음과 같다.
- 적절한 층으로 구성된 모듈을 쌓아 모듈을 구성한다.
- 모듈을 쌓을수록 필터 수는 증가시키고, Tensor 크기는 감소시킨다.
- 층의 unit은 적게 유지하고, 모듈은 높게 쌓는ㄷ.
- 잔차 연결을 활용한다.
- 모든 합성곱 층 이후에는 배치 정규화를 적용한다.
- 채널 분리 합성곱 신경망을 활용한다.
참고 : 여기서는 비록 이미지 분류 모델을 예제로 활용하지만 앞서 언급된 모든 기법은 컴퓨터 비전 프로젝트 일반에 적용될 수 있다. 예를 들어 DeepLabV3 모델은 Xception 모델을 이용하는 2021년 기준 최고의 이미지 분할 모델이다.
이미지 다운로드 및 데이터 적재
사용하는 데이터 셋은 8장에서 사용한 kaggle의 강아지-고양이 데이터셋이다.
모델 구현
- 데이터 증식 층
data_augmentation = keras.Sequential( [ layers.RandomFlip("horizontal"), layers.RandomRotation(0.1), layers.RandomZoom(0.2), ] )
- 미니 Xception 모델 구성
```python
입력층
inputs = keras.Input(shape=(180, 180, 3)) x = data_augmentation(inputs)
x = layers.Rescaling(1./255)(x)
하나의 Conv2D 은닉층
x = layers.Conv2D(filters=32, kernel_size=5, use_bias=False)(x)
SeparableConv2D, BatchNormalization, MaxPooling2D 층으로 구성된 모듈 쌓기
잔차 연결 활용
for size in [32, 64, 128, 256, 512]: # 필터 수 residual = x
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("relu")(x)
x = layers.SeparableConv2D(size, 3, padding="same", use_bias=False)(x)
x = layers.MaxPooling2D(3, strides=2, padding="same")(x)
# 잔차 연결
residual = layers.Conv2D(
size, 1, strides=2, padding="same", use_bias=False)(residual)
x = layers.add([x, residual])
마지막 은닉층은 GlobalAveragePooling2D과 Dropout
x = layers.GlobalAveragePooling2D()(x) # flatten 역할 수행(채널 별 평균값으로 구성) x = layers.Dropout(0.5)(x)
출력층
outputs = layers.Dense(1, activation=”sigmoid”)(x)
모델 지정
model = keras.Model(inputs=inputs, outputs=outputs)
3. 모델 훈련
``` python
model.compile(loss="binary_crossentropy",
optimizer="rmsprop",
metrics=["accuracy"])
callbacks = [
keras.callbacks.ModelCheckpoint(
filepath="mini_xception.keras",
save_best_only=True,
monitor="val_loss")
]
history = model.fit(
train_dataset,
epochs=100,
validation_data=validation_dataset,
callbacks=callbacks)
해당 예제 코드를 통해 모델 훈련을 시켜서 결과를 확인하면, 과대적합(overfiting)은 50번의 epoch 실행 후에 발생한다.
- 훈련 성능 평가 : 데이터셋에 대한 정확도가 90%정도 나오며, 8장에서 직접 구현해서 훈련했을 때 성능이 83%였던 것을 보면 훨씬 높은 정확도를 보인다.
성능 높이기
보다 성능을 높이기 위해서는 하이퍼파리미터 미세조정 및 앙상블 학습을 활용해야 한다.
9.4 참고 문헌
- KerasCV : KerasCV 소개
- High-performance image generation using Stable Diffusion in KerasCV
- ink detection with Keras : 이미지 분할에 특화된 합성곱 신경망인 U-net을 문서 인식(ink detection)에 활용하는 방법을 소개하는 튜토리얼
9.5 연습 문제
- (실습) 고급 컴퓨터 비전
- ink detection with Keras 내용 학습 후 깃허브 페이지에 블로그 작성하기