합성곱 신경망(Convolutional neural network, CNN)은 대뇌의 시각 피질 연구에서 시작되었고, 이미지 인식 분야에서 사용되기 시작했다. 최근 몇 년 동안 컴퓨터 성능의 향상과 많은 양의 훈련 데이터, 11장에서 본 심층 신경망을 훈련시키기 위한 기술들을 바탕으로 CNN이 일부 복잡한 이미지 처리 문제에서 사람을 능가하는 성능을 달성하기 시작했고, 이 기술은 이미지 검색 서비스, 자율주행 자동차, 영상 자동 분류 시스템 등에 큰 기여를 했다.
데이비드 허블과 토르스텐 비셀은 시각 피질 안의 많은 뉴런이 작은 국부 수용장을 가진다는 것을 보았다. 이때, 어떤 뉴런은 수평성의 이미지에만 반응하거나 반면 다른 뉴런은 다른 각도의 선분에 반응한다는 점을 보였다. 또한 어떤 뉴런은 큰 수용장을 가져서 저수준 패턴이 조합된 더 복잡한 패턴에 반응한다는 것을 알았고, 이러한 관찰은 고수준 뉴런이 이웃한 저수준 뉴런의 출력에 기반한다는 아이디어를 이끌어냈다.
이러한 발견은 이후 합성곱 신경망으로 점진적으로 진화되었고, 1998년 얀 르쿤의 논문에 의해 전환점을 맞게 되었다. 여기서 합성곱 층(Convolution layer)와 풀링 층(Pooling layer) 라는 새로운 구성 요소가 등장하게 된다.
합성곱 층은 CNN의 가장 중요한 구성 요소이다. 첫 번째 합성곱 층의 뉴런은 입력 이미지의 모든 픽셀에 연결되는 것이 아니라 합성곱 층 뉴런의 수용장 안에 있는 픽셀에만 연결된다. 두 번째 합성곱 층에 있는 각 뉴런은 첫 번째 층의 작은 사각 영역 안에 위치한 뉴런에 연결된다. 이런 구조는 네트워크가 첫 번째 은닉층에서는 저수준 특성에 집중하고, 그 다음 은닉층에서는 더 큰 고수준 특성으로 조합해나가도록 도와준다.
이때, 아래 그림과 같이 높이와 너비를 이전 층과 같게 하기 위해 입력의 주위에 0을 추가하는데, 이를 제로 패딩(zero padding)이라고 한다.
(공백 부분을 0으로 채워주는 것.)
수용장 사이에 간격을 두어 큰 입력층을 훨씬 작은 층에 연결하는 것도 가능하다. 이렇게 한다면 모델의 계산 복잡도를 크게 낮춰준다. 한 수용장과 다음 수용장 사이의 간격을 스트라이드(Stride)라고 한다.
(가운데 처럼 CNN을 이용하면 Stride1, 오른쪽 처럼 CNN을 이용하면 Stride2)
뉴런의 가중치는 수용장 크기의 작은 이미지로 표현될 수 있다. 예를 들어 필터(또는 합성곱 커널)이라 부르는 가중치 세트를 말한다. 층의 전체 뉴런에 적용된 하나의 필터는 하나의 특성 맵(Feature map)을 만든다. 이 맵은 필터를 가장 크게 활성화시키는 이미지의 영역을 강조한다. 수동으로 이러한 필터를 정의할 필요 없이, 훈련하는 동안 합성곱 층이 자동으로 해당 문제에 가장 유용한 필터를 찾고 상위층은 이들을 연결하여 더 복잡한 패턴을 학습한다.
실제 합성곱 층은 여러 가지 필터를 가지고 필터마다 하나의 특성 맵을 출력하므로 3D로 표현하는 것이 더 정확하다. 각 특성 맵의 픽셀은 하나의 뉴런에 해당하고 하나의 특성 맵 안에서는 모든 뉴런이 같은 파라미터(즉, 동일한 가중치와 편향)를 공유하지만, 다른 특성 맵에 있는 뉴런은 다른 파라미터를 사용한다. 한 뉴런의 수용장은 앞서 설명한 것과 같지만, 이전 층에 있는 모든 특성 맵에 걸쳐 확장되는데, 간단히 말해 하나의 합성곱 층이 입력에 여러 필터를 적용하여 입력에 있는 여러 특성을 감지할 수 있다.
입력 이미지는 컬러 채널 마다 하나씩 여러 서브 층으로 구성되기도 한다. 컬러 채널은 보통 RGB를 말하는데, 흑백 이미지는 하나의 채널만 가질 수 있다. 하지만 가시광선 및 적외선 같은 이미지는 매우 많은 채널을 가질 수도 있다.
이는 합성곱 층에서 한 뉴런의 출력을 계산하는 법을 보여주는 식이다.
텐서플로에서 각 입력 이미지는 보통[높이, 너비, 채널]형태의 3D 텐서로 표현된다. 하나의 미니배치는 [미니배치 크기, 높이, 너비, 채널]형태의 4D 텐서로 표현된다. 합성곱 층의 가중치는 [f_h,f_w,f_n',f_n]형태의 4D 텐서 표현된다. 합성곱 층의 편향은 간단하게 [f_n] 형태의 1D 텐서로 나타난다.
사이킷런의 load_sample_images()를 사용하여 두 개의 샘플 이미지를 로드한다.
12345678910111213141516171819 import numpy as npfrom sklearn.datasets import load_sample_image# 샘플 이미지를 로드합니다.china = load_sample_image("china.jpg") / 255flower = load_sample_image("flower.jpg") / 255images = np.array([china, flower])batch_size, height, width, channels = images.shape# 2개의 필터를 만듭니다.filters = np.zeros(shape=(7, 7, channels, 2), dtype=np.float32)filters[:, 3, :, 0] = 1 # 수직선filters[3, :, :, 1] = 1 # 수평선outputs = tf.nn.conv2d(images, filters, strides=1, padding="SAME")plt.imshow(outputs[0, :, :, 1], cmap="gray") # 첫 번째 이미지의 두 번째 특성맵을 그립니다.plt.axis("off") # 책에는 없습니다.plt.show()cs
이때, tf.nn.con2vd()는 다음과 같은 함수이다.
위 예시에서는 필터를 직접 지정하였지만, 실제 CNN에서는 보통 훈련 가능한 변수로 필터를 정의하여 신경망이 가장 잘 맞는 필터를 학습한다. 이때 keras.layers.Con2D를 사용한다.
12 conv = keras.layers.Conv2D(filters=32, kernel_size=3, strides=1,padding="SAME", activation="relu")cs
이는 3X3 크기의 32개의 필터와 스트라이드1, 제로패딩을 사용하는 Conv2D층을 만들고 출력을 위해 ReLU활성화 함수를 적용한다.
CNN에 관련된 또 하나의 문제는 합성곱 층이 많은 양의 RAM을 필요로 한다는 점이다. 메모리 부족으로 훈련이 실패한다면 미니배치 크기를 줄여보거나, 스트라이드를 사용해 차원을 줄이는 등의 방법을 사용할 수 있다.
풀링 층의 목적은 계산량과 메모리 사용량, 과대적합의 위험을 줄여주는 파라미터 수를 줄이기 위해 입력 이미지의 서브샘플을 만드는 것이다.
풀링 층의 각 뉴런은 이전 층의 작은 사각 영역의 수용장 안에 있는 뉴런의 출력과 연결되어 있어, 이전과 동일하게 크기, 스트라이드, 패딩 유형을 지정해야 하지만 가중치는 없다. 즉, 최대나 평균 같은 합산 함수를 사용하여 입력값을 더하는 것이다.
이는 가장 널리 이용되는 최대 풀링 층(max pooling layer)이다. 2X2 풀링 커널과 스트라이드2를 사용하는 것.
계산량, 메모리 사용량, 파라미터 수를 감소하는 것 외에도 최대 풀링은 작은 변화에도 일정 수준의 불변성(Invariance)을 만들어 준다. 이러한 불변성은 분류 작업처럼 예측이 이런 작은 부분에서 영향을 받지 않는 경우 유용할 수 있다.
물론 풀링은 단점도 가지고 있다. 2X2 풀링 커널과 스트라이드2만을 사용하여도 출력은 양방향으로 절반이 줄어들어 Input 값의 75%를 잃게 된다. 또한 시맨틱 분할과 같은 어떤 애플리케이션에서는 불변성을 필요로 하지 않을 수도 있다.
다음 코드를 통해 2X2 커널을 사용하여 최대 풀링 층을 만든다. 스트라이드의 기본값은 커널 크기이므로, 이 층은 모두 스트라이드 2를 사용하고 기본적으로 "valid"패딩을 사용한다. 즉, 어떤 패딩도 하지 않는다.
1 max_pool = keras.layers.MaxPool2D(pool_size=2)cs
평균 풀링 층(Average pooling layer)을 만드려면 MaxPool2D 대신 AvgPool2D을 사용한다. 대부분 성능은 최대 풀링 층이 더 좋지만, 평균을 이용하게 되면 정보 손실이 더 적다. 그러나 최대 풀링은 의미 없는 것은 모두 제거하고 가장 큰 특징만 유지하므로 다음 층이 조금 더 명확한 신호로 작업할 수도 있으며, 최대 풀링은 평균 풀링보다 강력한 이동 불변성을 제공하고 연산 비용이 조금 덜 든다.
최대 풀링과 평균 풀링은 공간 차원이 아니라 깊이 차원으로 수행될 수 있다. 이를 통해 CNN은 다양한 특성에 대한 불변성을 학습할 수 있다. 이는 tf.nn.max_pool()함수를 사용하는데, 커널 크기와 스트라이드를 4개 원소를 가진 튜플로 지정한다. 이때, 첫 번째 세 값은 1이어야 하는데, 이는 배치, 높이, 너비 차원을 따라 커널 크기와 스트라이드가 1이라는 뜻이다.
1 output=tf.nn.max_pool(images,ksize=(1,1,1,3),strides=(1,1,1,3),padding='valid')cs
이를 케라스 모델의 층으로 만들고 싶으면 Lambda를 이용하면 된다.
12 depth_pool = keras.layers.Lambda(lambda X: tf.nn.max_pool(X, ksize=(1, 1, 1, 3), strides=(1, 1, 1, 3), padding="VALID"))cs
현대적인 신경망 구조에서 종종 보게될 마지막 풀링 층의 종류는 전역 평균 풀링 층이다. 이 층의 작동 방식은 각 특성 맵의 평균을 계산하는 것이다. 이는 입력과 공간 방향 차원이 같은 커널을 사용하는 풀링 층과 같다. 각 샘플의 특성 맵마다 하나의 숫자를 출력한다는 의미. 이는 keras.layers.GlobalAvgPool2D 클래스를 사용한다.
1 global_avg_pool = keras.layers.GlobalAvgPool2D()cs
1 output_global_avg2 = keras.layers.Lambda(lambda X: tf.reduce_mean(X, axis=[1, 2]))cs
전형적인 CNN 구조는 합성곱 층을 몇 개 쌓고(각 층마다 ReLU 층을 쌓는다), 그 다음 풀링층을 쌓고, 그 다음에 또 합성곱 층을 몇 개 더 쌓고, 그다음에 다시 풀링 층을 쌓는 식이다.
네트워크를 통과하여 진행할수록 이미지는 점점 작아지지만, 합성곱 층 때문에 일반적으로 점점 더 깊어지며 더 많은 특성 맵을 가진다. 맨 위층에서는 몇 개의 완전 연결 층과 ReLU로 구성된 일반적인 FNN이 추가되고, 마지막 층에서는 소프트 맥스 층 등을 통해 예측을 출력한다.
이때, 합성곱 층에 너무 큰 커널을 사용하는 것보다는 작은 커널을 여러 겹 쌓는 것이 더 좋은 성능을 낼 수 있다. 그러나 첫 번째 합성곱 층에서는 큰 크기의 커널을 사용한다.
다음 코드는 패션MNIST 테이터셋 문제를 해결하기 위한 간단한 CNN 코드이다.
12345678910111213141516 model = keras.models.Sequential([DefaultConv2D(filters=64, kernel_size=7, input_shape=[28, 28, 1]),keras.layers.MaxPooling2D(pool_size=2),DefaultConv2D(filters=128),DefaultConv2D(filters=128),keras.layers.MaxPooling2D(pool_size=2),DefaultConv2D(filters=256),DefaultConv2D(filters=256),keras.layers.MaxPooling2D(pool_size=2),keras.layers.Flatten(),keras.layers.Dense(units=128, activation='relu'),keras.layers.Dropout(0.5),keras.layers.Dense(units=64, activation='relu'),keras.layers.Dropout(0.5),keras.layers.Dense(units=10, activation='softmax'),])cs
가장 널리 알려진 CNN구조로 1998년 얀 르쿤에 의해 탄생.
2012sus 탄생한 모델로, 구조는 더 크고 깊을 뿐 LeNet-5과 비슷하다. 처음으로 합성곱 층 위에 풀링 층을 쌓지 않고 바로 합성곱 층끼리 쌓았다. 또한 과대적합을 줄위기 위해 드롭아웃과 데이터 증식(Data augmentation)을 사용하였다.
또한 LRN(local response normalization)이라 부르는 정규화 단계를 사용했다. 이는 가장 강하게 활성화된 뉴런이 다른 특성 맵에 있는 같은 위치의 뉴런을 억제한다. 이는 특성 맵을 각기 특별하게 다른 것과 구분되게 하고, 더 넓은 시각에서 특징을 탐색하도록 만들어 결국 일반화 성능을 향상하게 한다.
이는 이전 CNN보다 훨씬 더 깊은 네트워크 구조를 가진다. 또한 인셉션 모듈(Inception module)이라는 서브 네트워크를 가지고 있어 이전의 구조보다 훨씬 효과적으로 파라미터를 사용한다.
처음에 입력 신호가 복사되어 네 내의 다른 층에 주입된다. 모든 합성곱 층은 ReLU활성화 함수를 사용한다. 두 번째 합성곱 층은 각기 다른 커널 크기를 사용하여 다른 크기의 패턴을 잡는다. 이때, 모든 층은 스트라이드 1과 "same" 패딩을 사용하므로 출력의 높이와 너비가 모두 입력과 같다. 이렇게 연결 하면 모든 출력을 깊이 연결 층(depth concatenation layer)에서 깊이 방향으로 연결할 수 있다.
왜 처음에 1X1 커널의 합성곱 층을 가지는 것일까?
GoogLeNet은 다음과 같은 구조를 가진다.
이는 매우 단순하고 고전적인 구조를 가진다. 2개 또는 3개의 합성곡 층 뒤에 풀링이 나오고, 다시 2개 또는 3개의 합성곱 층과 풀링 층이 등장하는 식이다. 마지막 밀집 네트워크는 2개의 은닉층과 출력층으로 이루어지며, 오직 3X3의 필터만 사용한다.
이 모델은 더 적은 파라미터를 사용해 점점 더 깊은 네트워크로 모델을 구성하는 일반적인 트렌드를 만들었다. 이런 깊은 네트워크를 훈련시킬 수 있는 핵심 요소는 스킵 연결(또는 숏컷 연결)이다. 즉, 어떤 층에 주입되는 신호가 상위 층의 출력에도 더해지게 되는 구조를 가진다.
우리는 신경망을 훈련시킬 때 목적함수(F(x))를 모델링하는 것이 목표이다. 만약 입력 x를 네트워크의 출력에 더한다면(즉, 스킵 연결을 추가하면), 네트워크는 F(x)대신 H(x)=F(x)-x를 학습하게 될 것이다. 이를 잔차 학습(Residual learning)이라고 한다.
일반적인 신경망을 초기화할 때는 가중치가 0에 가깝기에 네트워크도 0에 가까운 값을 출력한다. 그러나 스킵 연결을 추가하면 이 네트워크는 입력과 같은 값을 출력한다. 즉, 초기에는 항등 함수(identity function)을 모델링한다. 목적 함수가 항등 함수에 매우 가깝다면, 훈련 속도가 매우 빨리질 것이다.
또한 스킵 연결을 많이 추가하면 일부 층이 아직 학습되지 않았더라도 네트워크는 훈련을 시작할 수 있다. 즉, 스킵 연결 덕분에 입력 신호가 전체 네트워크에 손쉽게 영향을 미친다. 심층 잔차 네트워크는 스킵 연결을 가진 작은 신경망인 잔차 유닛(Residual Unit)을 쌓은 것이다.
구조는 간단하다. 드랍아웃 층을 제외하면, GoogLeNet과 똑같이 시작하고 종료한다. 그러나 중간에 단순한 잔차 유닛을 매우 깊게 쌓은 것이다. 각 잔차 유닛은 배치 정규화(Batch Normalization)과 ReLU, 3X3 커널을 사용하고 공간 정보를 유지하는(스트라이드1, "same"패딩) 두 개의 합성곱 층으로 이루어져 있다.
GoogLeNet 구조의 변종인 모델이다. 대규모 비전 문제에서 Inception-v3 보다 훨씬 성능이 뛰어나며, GoogLeNet과 ResNet의 아이디어를 합쳤지만, 인셉션 모듈은 깊이별 분리 합성곱 층(Depthwise separable convolution layer)라는 층으로 대체하였다. 일반적인 합성곱 층이 공간상의 패턴(예를 들어 타원 형태)과 채널 사이의 패턴(예를 들어 입+눈+코=얼굴)을 동시에 잡기 위하여 필터를 사용한다. 분리 합성곱 층은 공간 패턴과 채널 사이 패턴을 분리하여 모델링 할 수 있다고 가정한다. 이 층은 두 개의 부분으로 구성되는데, 첫 번째 부분은 하나의 공간 필터를 각 입력 특성 맵에 적용하고, 두 번째 부분에서는 채널 사이 패턴만 조사한다. 이 부분은 1X1필터를 사용한 일반적인 합성곱 층이다.
분리 합성곱 층은 일반 합성곱 층보다 파라미터, 메모리, 연산을 더 적게 사용하고 일반적으로 성능은 더 높다. 그러나 분리 합성곱 층은 입력 채널마다 하나의 공간 필터만 가지기 때문에 입력층과 같이 채널이 너무 적은 층 다음에 사용하는 것은 피해야 한다.
이는 인셉션 네트워크와 ResNet같은 기존 구조를 확장하여 성능을 높인 모델이다. 이는 원래 구조에 있는 모든 유닛에 SE블록이라는 작은 신경망을 추가하여 성능을 향상했다.
SE블록이 추가된 부분의 유닛의 출력을 깊이 차원에 초점을 맞추어 분석한다. 어떤 특성이 일반적으로 동시에 가장 크게 활성화되는지 학습한다. 그 다음 이 정보를 사용하여 특성 맵을 보정한다. 하나의 SE블록은 3개의 층으로 구성된다. 이는 전역 평균 풀링 층과 ReLU활성화 함수를 사용하는 밀집 은닉층, 시그모이드 활성화 함수를 사용하는 밀집 출력 층이다.
ResNet-34 모델을 구현하기 위해서는 먼저 ResidualUnti층을 만들어야 한다.
123456789101112131415161718192021222324252627 DefaultConv2D = partial(keras.layers.Conv2D, kernel_size=3, strides=1,padding="SAME", use_bias=False)class ResidualUnit(keras.layers.Layer):def __init__(self, filters, strides=1, activation="relu", **kwargs):super().__init__(**kwargs)self.activation = keras.activations.get(activation)self.main_layers = [DefaultConv2D(filters, strides=strides),keras.layers.BatchNormalization(),self.activation,DefaultConv2D(filters),keras.layers.BatchNormalization()]self.skip_layers = []if strides > 1:self.skip_layers = [DefaultConv2D(filters, kernel_size=1, strides=strides),keras.layers.BatchNormalization()]def call(self, inputs):Z = inputsfor layer in self.main_layers:Z = layer(Z)skip_Z = inputsfor layer in self.skip_layers:skip_Z = layer(skip_Z)return self.activation(Z + skip_Z)cs
먼저 생성자에서 필요한 층을 만든다. Main layers가 이 그림의 왼쪽 모듈이며, Skip_layers는 오른쪽 모듈이다. call()메서드에서 입력을 main_layer와 skip_layers에 통과시킨 후 두 출력을 더하여 활성화 함수를 적용한다. 이때, skip layer는 스트라이드가 1보다 큰 경우에만 필요하다. 이 네트워크는 연속되어 길게 연결된 층이기 떄문에 Sequential 클래스를 사용하여 모델을 만든다.
1234567891011121314 model = keras.models.Sequential()model.add(DefaultConv2D(64, kernel_size=7, strides=2,input_shape=[224, 224, 3]))model.add(keras.layers.BatchNormalization())model.add(keras.layers.Activation("relu"))model.add(keras.layers.MaxPool2D(pool_size=3, strides=2, padding="SAME"))prev_filters = 64for filters in [64] * 3 + [128] * 4 + [256] * 6 + [512] * 3:strides = 1 if filters == prev_filters else 2model.add(ResidualUnit(filters, strides=strides))prev_filters = filtersmodel.add(keras.layers.GlobalAvgPool2D())model.add(keras.layers.Flatten())model.add(keras.layers.Dense(10, activation="softmax"))cs
여기서 for 문은 모델에 ResidualUnit층을 더하는 반복 루프이다. 청므 3개 RU는 64개의 필터를 가지고, 그 다음 4개 RU는 128개의 필터를 가지는 식이다. 필터 개수가 이전 RU와 동일할 경우는 스트라이드를 1로 설정하거나 아니면 2로 설정하게 되고, 이후 마지막에 prev_filters를 업데이트 한다.
keras.applications 패키지에 있는 사전훈련된 모델을 코드 한 줄로 불러올 수 있다. 다음은 이미지넷 데이터넷에서 사전훈련된 ResNet-50 모델을 로드하는 과정이다.
1 model = keras.applications.resnet50.ResNet50(weights="imagenet")cs
이때 사전훈련된 모델을 사용하려면 이미지가 적절한 크기인지 확인해야 한다. 이때 이미지의 크기가 다르면 tf.image.resize()함수로 적재한 이미지의 크기를 바꿀 수 있다.
1 images_resized = tf.image.resize(images, [224, 224])cs
사전훈련된 모델은 이미지가 적절한 방식으로 전처리되었다고 가정한다. 경우에 따라 0에서 1사이 또는 -1에서 1사이의 입력을 기대하므로 이를 위해 모델마다 이미지를 전처리 해주는 preprocess_input()함수를 제공한다. 이 함수는 픽셀값이 0에서 255사이라고 가정하기에 바뀐 이미지 입력을 다시 바꾸어야 하기 때문에 image_resized에 255를 곱한다.
1 inputs = keras.applications.resnet50.preprocess_input(images_resized * 255)cs
이제 사전 훈련된 모델을 사용하여 예측을 수행한다.
1 Y_proba = model.predict(inputs)cs
각 이미지에 대해 최상위 K개의 예측을 담은 리스트를 반환한다. 이때, 각 예측은 클래스 아이디, 이름, 확률을 포함하는 튜플이다.
123456 top_K = keras.applications.resnet50.decode_predictions(Y_proba, top=3)for image_index in range(len(images)):print("Image #{}".format(image_index))for class_id, name, y_proba in top_K[image_index]:print(" {} - {:12s} {:.2f}%".format(class_id, name, y_proba * 100))print()cs
충분하지 않은 훈련 데이터로 이미지 분류기를 훈련하려면 사전훈련된 모델의 하위층을 사용하는 것이 좋다. 이에 대한 예시를 들어보기 위하여 사전 훈련된 Xception 모델을 사용하여 꽃 이미지를 분류하는 모델을 훈련해보자.
이때, 먼저 데이터를 적재한 후, tfds.Split.TRAIN.subsplit함수를 이용하여 데이터셋을 나눈다.
1234567891011 import tensorflow_datasets as tfdsdataset, info = tfds.load("tf_flowers", as_supervised=True, with_info=True)dataset_size = info.splits["train"].num_examplesclass_names = info.features["label"].namesn_classes = info.features["label"].num_classestest_set_raw, valid_set_raw, train_set_raw = tfds.load("tf_flowers",split=["train[:10%]", "train[10%:25%]", "train[25%:]"],as_supervised=True)cs
이후 이미지를 전처리해야 한다. 이CNN 모델은 224X224 크기 이미지를 기대하므로 크기를 조정해야 한다.
1234 def preprocess(image, label):resized_image = tf.image.resize(image, [224, 224])final_image = keras.applications.xception.preprocess_input(resized_image)return final_image, labelcs
훈련 세트를 섞은 후, 이 전처리 함수들을 데이터셋에 모두 적용하고 배치 크기를 지정하고 프리페치를 적용. 이때 tf.image.random.crop()등을 이용해 데이터 증식 가능
1234567891011121314151617181920212223242526272829 def central_crop(image):shape = tf.shape(image)min_dim = tf.reduce_min([shape[0], shape[1]])top_crop = (shape[0] - min_dim) // 4bottom_crop = shape[0] - top_cropleft_crop = (shape[1] - min_dim) // 4right_crop = shape[1] - left_cropreturn image[top_crop:bottom_crop, left_crop:right_crop]def random_crop(image):shape = tf.shape(image)min_dim = tf.reduce_min([shape[0], shape[1]]) * 90 // 100return tf.image.random_crop(image, [min_dim, min_dim, 3])def preprocess(image, label, randomize=False):if randomize:cropped_image = random_crop(image)cropped_image = tf.image.random_flip_left_right(cropped_image)else:cropped_image = central_crop(image)resized_image = tf.image.resize(cropped_image, [224, 224])final_image = keras.applications.xception.preprocess_input(resized_image)return final_image, labelbatch_size = 32train_set = train_set_raw.shuffle(1000).repeat()train_set = train_set.map(partial(preprocess, randomize=True)).batch(batch_size).prefetch(1)valid_set = valid_set_raw.map(preprocess).batch(batch_size).prefetch(1)test_set = test_set_raw.map(preprocess).batch(batch_size).prefetch(1)cs
그 다음 이미지넷에서 사전훈련된 Xception 모델 로드. 이때 include_top=False로 지정하여 네트워크의 최상층에 해당하는 전역 평균 풀링 층과 밀집 출력 층은 제외시킨다. 이때, 훈련 초기에는 사전훈련된 층의 가중치를 동결.
1234567891011121314151617 base_model = keras.applications.xception.Xception(weights="imagenet",include_top=False)avg = keras.layers.GlobalAveragePooling2D()(base_model.output)output = keras.layers.Dense(n_classes, activation="softmax")(avg)model = keras.models.Model(inputs=base_model.input, outputs=output)for layer in base_model.layers:layer.trainable = Falseoptimizer = keras.optimizers.SGD(learning_rate=0.2, momentum=0.9, decay=0.01)model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,metrics=["accuracy"])history = model.fit(train_set,steps_per_epoch=int(0.75 * dataset_size / batch_size),validation_data=valid_set,validation_steps=int(0.15 * dataset_size / batch_size),epochs=5)cs
이때, 검증 정확도가 어느정도 선에 머물러서 더 나아지지 않는다면 모든 층의 가중치 동결을 해제하고 훈련. 이떄는 사전훈련된 가중치가 훼손되는 것을 피하기 위해 훨씬 작은 학습률을 사용한다.
123456789101112 for layer in base_model.layers:layer.trainable = Trueoptimizer = keras.optimizers.SGD(learning_rate=0.01, momentum=0.9,nesterov=True, decay=0.001)model.compile(loss="sparse_categorical_crossentropy", optimizer=optimizer,metrics=["accuracy"])history = model.fit(train_set,steps_per_epoch=int(0.75 * dataset_size / batch_size),validation_data=valid_set,validation_steps=int(0.15 * dataset_size / batch_size),epochs=40)cs
물체의 위치를 추정하는 것은 회귀 작업으로 나타낼 수 있다. 물체 주위의 바운딩 박스를 예측하는 일반적인 방법은 물체 중심의 수평, 수직 좌표와 높이, 너비를 예측하는데, 즉 네 개의 숫자를 예측해야 한다. 이에 네 개의 유닛을 가진 두 번째 밀집 출력 층을 추가하고 MSE 손실을 사용해 훈련
12345678910 base_model = keras.applications.xception.Xception(weights="imagenet",include_top=False)avg = keras.layers.GlobalAveragePooling2D()(base_model.output)class_output = keras.layers.Dense(n_classes, activation="softmax")(avg)loc_output = keras.layers.Dense(4)(avg)model = keras.models.Model(inputs=base_model.input,outputs=[class_output, loc_output])model.compile(loss=["sparse_categorical_crossentropy", "mse"],loss_weights=[0.8, 0.2], # 어떤 것을 중요하게 생각하느냐에 따라optimizer=optimizer, metrics=["accuracy"])cs
이때 레이블이 없는 데이터가 있을 수 있다. 레이블을 만드는 것은 머신러닝 프로젝트에서 가장 어렵고 비용이 많이 드는 작업이다. 이때, 이미지에 바운딩 박스를 추가하기 위한 다양한 레이블 도구가 존재하기도 하다. 또한 크라우드소싱 플랫폼 역시 고려할만 하다.
모든 이미지에 대해 바운딩 박스가 준비되었다면 클래스 레이블, 바운딩 박스와 함께 전처리된 이미지의 배치가 하나의 원소인 데이터셋을 만들어야 한다. 각 원소는(Images,(class_labels,bounding_boxes))형태의 튜플이 된다.
이때, 회귀 문제이므로 MSE를 손실 함수로 사용할 수 있으나 바운딩 박스에 관하여는 좋은 지표는 아니므로 IoU를 사용해야 한다. 이는 바운딩 박스와 타깃 바운딩 박스 사이에 중첩되는 영역을 전체 영역으로 나눈 것이다. tf.keras.metrics.MeanIoU로 사용 가능하다.
하나의 이미지에서 여러 물체를 분류하고 위치를 추정하는 작업을 객체 탐지라고 한다. 이전까지 널리 사용되던 방법은 하나의 물체를 분류하고 위치를 찾는 분류기를 훈련한 다음 이미지를 훑는 것. 이 방식은 매우 쉽지만 조금씩 다른 위치에서 동일한 물체를 여러 번 감지할 수 있어, 불필요한 바운딩 박스를 제거하기 위해 사후 처리가 필요하다. 이를 위한 방법은 NMS이다.
그러나 이러한 방법보다, 완전 합성곱 신경망(fully convolutional network,FCN)을 사용하면 훨씬 빠르게 이미지에 슬라이딩 할 수 있다.
CNN맨 위의 밀집 층을 합성곱 층으로 바꿀 수 있다고 하는 방법론을 제시한 것. 이러한 방식을 거치면 밀집 층은 입력 특성마다 하나의 가중치를 두므로 특정 입력 크기를 기대하지만 합성곱 층을ㄴ 어떤 크기의 이미지도 처리할 수 있다. 대신 합성곱 층은 입력 채널마다 커널 가중치가 달라서 특정 개수의 채널을 기대한다. 결국, FCN은 합성곱 층과 동일한 성질을 가진 풀링 층만 가지므로 어떤 크기의 이미지에서도 훈련하고 실행할 수 있게 된다. 또한 딱 한 번만 이미지를 처리하기 때문에 더욱 효율적이다(YOLO).
매우 빠르고 정확한 객체 탐지 구조. 이에 실시간으로 비디오에도 적용할 수 있다. YOLOv3은 다음과 같은 특성을 가진다.
이 외에도 SSD와 Faster R-CNN같은 모델이 존재한다. 탐지 시스템은 속도, 정확도, 복잡도 등 여러 요인에 기반해 선택되며, 테스트 환경에 유동성이 많아 기술이 매우 빠르게 발전한다.
객체 탐지에서 널리 사용되는 평가 지표는 mAP(mean average precision)이다. 보통 객체 탐지의 지표는 정밀도와 재현율의 반비례 관계를 가지게 된다. 이에 최소 재현율에서 분류기가 제공하는 최대 정밀도를 찾아야 한다. 이에 10%, 20%에서 100%까지 재현율에서의 최대 정밀도를 계산하고, 이 최대 정밀도를 평균하는데, 이를 평균 정밀도(average precision(AP))라고 한다. 두 개 이상의 클래스가 있을 때는 각 클래스에 대해 AP를 계산한 다음 평균AP를 내는데, 이를 mAP라고 한다.
시맨틱 분할(semantic segmentation)에서 각 픽셀은 픽셀이 속한 객체의 클래스로 분류된다(예를 들어 도로, 자동차, 보행자, 건물 등). 이때 클래스가 같은 물체는 구별되지 않는다. 이 작업에서 가장 어려운 점은 이미지가 일반적인 CNN을 통과할 때 점진적으로 위치 정보를 잃는 것이다(1 이상의 스트라이드를 사용하는 층 때문에).
이를 해결하기 위한 방법을 위해서는 먼저 CNN을 FCN으로 변환한다. 이때 이 CNN이 입력 이미지에 적용하는 전체 스트라이드는 32이다(1보다 큰 스트라이드를 모두 더한 것). 이는 마지막 층이 입력 이미지보다 32배나 작은 특성 맵을 출력한다는 것인데, 이러면 너무 듬성듬성 해지므로 해상도를 32배로 늘리는 업샘플링 층(upsampling layer)를 하나 추가한다.
업생플링을 위한 방법으로는 이중 선형 보간이나 전치 합성곱 층 등이 존재한다. 이때, 여기서 이 방법에서 사용된 전치 합성곱 층은 Conv2DTrasnpose를 이용하여 사용 가능하다. 이를 개선하기 위해 저자들은 아래쪽 층에서부터 스킵 연결을 추가했다. 이 방식으로 풀링 층에서 잃은 일부 공간 정보를 복원하고, 비슷하게 두 번째 스킵 연결을 사용해 더 낮은 층의 세부 정보를 복원하여 최상의 구조를 만든다. 이러한 방법으로 이미지의 해상도를 증가시킬 수 있으며, 이를 초해상도라고 부른다.
텐서플로 모델 프로젝트에서 사전훈련된 인스턴스 분할모델은 시맨틱 분할과 비슷하지만, 동일한 클래스 물체를 하나의 덩어리로 합치는 것이 아닌 각 물체를 구분하여 표시한다. 이는 Faster R-CNN을 확장하여 각 바운딩 박스에 대해 픽셀 마스크를 추가한 Mask R-CNN을 통하여 제안되었다. 따라서 물체마다 클래스 추정 확률과 바운딩 박스를 얻는 것뿐만 아니라 바운딩 박스 안에 들어 있는 물체의 픽셀을 구분하는 픽셀 마스크도 얻을 수 있다.
컴퓨터 비전 분야에는 적대적 학습(adversarial learning), 설명 가능성(explainability), 현실같은 이미지 생성(image generation), 객체를 한 번 본후 인식할 수 있는 시스템인 싱글-샷 학습(single-shot learning) 등이 연구되고 있다.
텐서플로의 합성곱 연산에는 다음과 같은 것이 제공된다.