컴퓨터 비전(Computer vision)과 딥러닝

작성자 : 박정현

CH08.초창기 딥러닝의 큰 성공 스토리 : 컴퓨터 비전(Computer vision)

합성곱 신경망 (Convnet == CNN) 소개

8.1 합성곱 신경망 소개

이전에 손글씨 데이터를 MLP를 이용해서 해결했던 기억이 있을것임. (테스트 정확도 97.8%)

이번엔 합성곱 신경망을 적용해 손글씨 데이터 분류를 실시하고 결과를 비교하자.

과연. 합성곱 신경망이 왜 컴퓨터 비전에 잘 맞을까?

from tensorflow import keras
from tensorflow.keras import layers
import tensorflow as tf

inputs = keras.Input(shape=(28,28,1)) # 가로 세로 28, 색 채널 1

x = layers.Conv2D(filters = 32, kernel_size=3, activation = 'relu')(inputs) # inputs의 다음 층, x는 hidden layers
x = layers.MaxPooling2D(pool_size = 2)(x)
x = layers.Conv2D(filters = 64, kernel_size=3, activation = 'relu')(x) # 커널 사이즈 3x3
x = layers.MaxPooling2D(pool_size = 2)(x)
x = layers.Conv2D(filters = 128, kernel_size=3, activation = 'relu')(x)
x = layers.Flatten()(x)

outputs = layers.Dense(10, activation="softmax")(x) # 출력 층

model = keras.Model(inputs = inputs, outputs = outputs)

ConvNet은 배치 차원을 제외하고 (height, width, channels) 크기의 입력 텐서를 사용해야함.
이번 예제는 MNIST 데이터 이미지 크기인 28x28을 사용하고 흑백 이미지라서 채널은 1이다.

model.summary() # 모델 구조 출력
Model: "model"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 28, 28, 1)]       0         
                                                                 
 conv2d (Conv2D)             (None, 26, 26, 32)        320       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 13, 13, 32)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 11, 11, 64)        18496     
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 5, 5, 64)         0         
 2D)                                                             
                                                                 
 conv2d_2 (Conv2D)           (None, 3, 3, 128)         73856     
                                                                 
 flatten (Flatten)           (None, 1152)              0         
                                                                 
 dense (Dense)               (None, 10)                11530     
                                                                 
=================================================================
Total params: 104,202
Trainable params: 104,202
Non-trainable params: 0
_________________________________________________________________

구조 정보를 보면, Conv2DMaxPooling2D 층의 출력은 (height, width, channels) 크기의 Rank-3 tensor이다. (해당 용어가 기억이 안나면 이전 포스트를 찾아보시오)

높이, 너비는 모델이 깊어질수록 작아지는 경향이 있다. (점차 출어든다는 의미) => 모델이 사진을 확대해서 더 세밀한 영역을 학습한다 생각하자.

특이한 것은 채널 수는 점차 증가하는 것을 보이는 점이다. Conv2D 층에 전달된 첫 번째 매개변수 filters 에 의해 조절되어 이러한 현상이 있는 것임.

마지막 Conv2D 층을 보면 3, 3, 128 즉, 3x3 크기의 128개 채널을 가진 특성 맵 ( feature map) 이다.
이후 Conv2D의 출력을 Dense층으로 주입함.

이 분류기는 Dense층을 쌓은 것으로 이미 익숙한 구조이다.

음..? 나만 아닌가

이 분류기는 1-dim(1차원) 벡터를 처리하는데, 이전 층의 출력이 Rank-3 tensor이다. 따라서 Flatten을 이용해서 3-dim을 1-dim으로 펼쳐야한다.

마지막으로 숫자 10개의 클래스를 분류하기 위해 마지막 층 출력의 크기를 10으로 하고 softmax를 사용한다.

softmax와 sigmoid는 출력을 확률로 나타낸다는 점이 매우 비슷하다.
하지만, 보통 Multi-Class 분류의 경우 softmax를 사용한다.
간단한 이유는 sigmoid는 single class에 대한 확률을 보여주는 것임.
softmax의 경우 multi class에 대해 각각의 확률을 표시함.
그리고 sigmoid의 전체 확률은 1을 넘을 수 있다는 말이 있음. 반면 softmax는 총 확률의 합은 1이다. (activation func의 수식을 보면 알 수 있지만, 이부분은 검색을 따로 해보자)

# MNIST 이미지에서 컨브넷(ConvNet) 훈련
from tensorflow.keras.datasets import mnist

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
train_images = train_images.reshape((60000,28,28,1)) # num of samples, height, width, channels
train_images = train_images.astype("float32") / 255 # norm

test_images = test_images.reshape((10000,28,28,1))
test_images = test_images.astype("float32") / 255

model.compile(optimizer = "rmsprop",
              loss="sparse_categorical_crossentropy",
              metrics = ["accuracy"])

model.fit(train_images, train_labels, epochs=5, batch_size=64)
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11490434/11490434 [==============================] - 1s 0us/step
Epoch 1/5
938/938 [==============================] - 15s 5ms/step - loss: 0.1609 - accuracy: 0.9500
Epoch 2/5
938/938 [==============================] - 4s 4ms/step - loss: 0.0436 - accuracy: 0.9862
Epoch 3/5
938/938 [==============================] - 4s 4ms/step - loss: 0.0304 - accuracy: 0.9906
Epoch 4/5
938/938 [==============================] - 4s 4ms/step - loss: 0.0225 - accuracy: 0.9932
Epoch 5/5
938/938 [==============================] - 4s 5ms/step - loss: 0.0171 - accuracy: 0.9947





<keras.callbacks.History at 0x7fb7a410f0a0>
# 컨브넷 평가

test_loss, test_acc = model.evaluate(test_images, test_labels) # 새로운 데이터와, 정답을 넘겨준다.
print(f'테스트 정확도 : {test_acc:.3f}')
print(f'테스트 오차 : {test_loss:.3f}')
313/313 [==============================] - 1s 3ms/step - loss: 0.0249 - accuracy: 0.9928
테스트 정확도 : 0.993
테스트 오차 : 0.025

2장의 fully connected 네트워크는 97.8%의 테스트 정확도를 얻은 반면 기본적인 ConvNet은 99.1%의 테스트 정확도를 얻었다.

둘을 비교했을 때 에러율이 60%나 줄었다.

좋은건 알겠는데 정확하게 ConvNet이 어떤 구조인지 파악하자.

8.1.1 합성곱 연산


완전 연결 층(fully connected layers)과 합성곱 층 사이의 근본적 차이는 아래와 같다.

  • Dense : 입력 특성 공간의 전역 패턴 학습
  • Conv : 지역 패턴을 학습

지역 패턴 : 일부 영역의 패턴 전역 패턴 : 모든 픽셀의 패턴

책에서 ConvNet에 대해 2가지 흥미로운 성질을 제공한다고 말함.(난 전~~혀 흥미X)

  1. 학습된 패턴은 평행 이동 불변성을 갖는다. : 예를 들어 ConvNet이 오른쪽 아래 모서리의 지역 패턴을 학습했다면 다른 위치(왼쪽 위 모서리)에서도 패턴을 인식할 수 있음.
    • 반대로, FC의 경우 새로운 위치는 새로운 패턴으로 학습
  2. ConvNet은 패턴의 공간적 계층 구조를 학습함 : 첫 번째 Conv 층이 작은 지역 패턴을 학습한다. 두 번째 층은 첫 번째 층으로 구성된 더 큰 패턴을 학습함. 때문에 매우 복잡하고 추상적인 시각적 개념을 효과적으로 학습 가능.

이러한 연산은 특성 맵(feature map)이라 부르는 Rank-3 tensor에 적용됨.

이 텐서는 2개의 공간 축(h, w)와 깊이 축(= 채널 축)으로 구성됨.
합성곱 연산은 입력 특성 맵에서 작은 패치(patch)들을 추출하고 이런 모든 패치에 같은 변환을 적용해 출력 특성 맵(output feature map)을 만든다.

아래 그림 참고.
1

출력 특성 맵도 높이와 너비를 가진 Rank-3 tensor(높이, 너비, 채널)이다. 이것의 깊이는 층의 매개변수로 결정되어 상황에 따라 다르다.
때문에 깊이 축의 채널은 더이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. (RGB -> 3채널)

대신 일종이 필터(filter)를 의미함. 필터는 입력 데이터의 어떤 특성을 인코딩한다. (ex입력 이미지에 얼굴이 있는지)

예제에서는 첫 번째 합성곱 층이 (28, 28, 1)으로 입력을 받는다.(ipunts 변수) 이후 다음 층에서 (26, 26, 32)를 출력한다. (x 변수)

즉, 입력에 대해 32개의 필터를 적용하는 것임. (앞전에 깊이 축 채널이 RGB를 의미하지 않고 필터를 의미한다고 언급함)

32개의 출력 채널은 26x26 행렬을 32개 가진다.

이 값은 입력에 대한 필터의 응답 맵(response map)임. -> 필터에 대한 응답을 나타냄.

[정리]
특성 맵 : 깊이 축(채널 축)에 있는 각 차원은 하나의 특성(OR 필터)이고 Rank-2 tensor인 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵이다!

3차원 텐서를 output[:, :, n] 연산을 적용하면 2차원으로 출력됨.

2

합성곱은 핵심적인 2개의 파라미터로 정의됨

  1. 입력으로부터 뽑아낼 패치의 크기 : 전형적으로 3x3 또는 5x5 크기를 사용.
  2. 특성 맵의 출력 깊이 : 합성곱으로 계산할 필터 개수.

Keras의 Conv2D 층에서 이 파라미터는 아래 처럼 전달된다.
Conv2D(output_depth, (window_height, window_width))

3D 입력 특성 맵 위를 3x3 OR 5x5 크기의 윈도우가 슬라이딩(sliding)하면서 모든 위치에서 3D 특성 패치((window_width, window_height, input_depth),크기)를 추출하는 방식으로 합성곱이 작동한다.

이를 합성곱 커널(convolution kernel)이라고 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통해 (output_depth, ) 크기의 1D 벡터로 변환한다.(그림에서 2번, 3번 줄)

동일한 커널이 모든 패치에 걸쳐서 재사용됨.
변환한 모든 벡터는 (height, width, output_depth) 크기의 3D 특성 맵으로 재구성된다.(마지막 그림).

출력 특성 맵 공간상 위치는 입력 특성 맵의 같은 위치에 대응된다.(예를 들어, 출력의 오른쪽 아래 모서리는 입력의 오른쪽 아래 부근에 해당하는 정보를 담고 있음)

3x3 윈도우를 사용하면 3D 패치 input[i-1:i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 생성됨.

즉, 3x3 입력 패치 덩어리가 생성되어서, 이게 변환된 패치 그림 처럼 나옴. (세로 막대기)

3

때문에 출력 높이, 너비는 입력과 다를 수 있다.

이 특징 때문에 Super Resolution 이라고 부르는 연구 분야가 있음.
인턴하면서 지금 Super Resolution하는 중

  • 경계 문제, 입력 특성 맵에 패딩을 추가하여 대응할 수 있다.
  • 스타라이드(stride)의 사용 여부에 따라 다르다.

위 2가지 개념을 조금 살펴보자.

경계 문제와 패딩 이해


5x5 크기의 특성 맵을 생각해 보자. 3x3 크기인 윈도우 중앙을 맞출 수 있는 타일은 3x3 격자를 형성하는 9개다. (그림에서 3x3 격자가 몇개 나올 수 있을 지 본인이 직접 새어보셈)

따라서 출력 특성 맵은 3x3 크기로 변환됨(크기 감소). 여기에서는 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다. (그림은 평면인데 3차원이라 생각해봐)

첫 합성곱 층에서 28x28 크기의 입력이 26x26 크기가 되었다.

4

입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면 패딩(padding)을 사용할 수 있다.
패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가함.

css의 padding과 원리는 같아 보임.

5

즉, 입력이 5x5 = 25개의 타일이 있는데 출력도 25개가 나오게 하고싶으면 패딩을 통해서 모든 방향에 행 또는 열을 추가해서 꼼수를 씀.

Conv2D에서 padding은 매개변수로 사용이 가능함. valid는 사용하지 않는다. same입력과 동일한 높이와 너비를 갖도록 한다.

합성곱 스트라이드(stride)이해하기


출력 크기에 영향을 미치는 다른 요소는 스트라이드이다.
합성곱에 대한 설명은 윈도우의 중앙 타일이 연속적으로 지나간다고 가정한 것임.

두 번의 연속적인 윈도우 사이의 거리가 스트라이드라고 불리는 합성곱의 파라미터다.

몇 칸씩 뛰어서 쪼갤껀지 나타내는 듯.

6

스트라이드 2를 사용했다는 것은 특성 맵의 너비, 높이가 2의 배수로 다운샘플링되었다는 뜻임.

스트라이드 합성곱은 분류 모델에서 드물게 사용함.(다음 장 구체적 설명)

분류 모델에서는 특성 맵을 다운샘플링하기 위해 스트라이드 대신 최대 풀링(max pooling) 연산을 사용하는 경우가 많다.

모델 요약을 보면 첫 Conv2D층이 MaxPooling을 만나니 반토막 났음.(매개변수 값 = 2)

8.1.2 최대 풀링 연산


ConvNet 예제에서 특성 맵의 크기가 MaxPooling2D 층마다 절반으로 줄어들었음.

스트라이드 합성곱과 매우 유사하게 강제적으로 특성 맵을 다운샘플링하는 것이 최대 풀링임.

최대 풀링은 입력 특성 맵에서 윈도우에 맞는 패치를 추룰하고 각 채널별로 최댓값을 출력함.

합성곱과 최대 풀링의 가장 큰 차이점은 최대 풀링은 보통 2x2 윈도우와 스트라이드 2를 사용해서 다운샘플링 진행. 반면, 합성곱은 3x3 윈도우와 스트라이드 1을 사용함.

만약 다운샘플링을 하지 않을 경우?

from keras.api._v2.keras import activations
# 최대 풀링 층이 빠진 잘못된 구조의 컨브넷
inputs = keras.Input(shape=(28,28,1))
x = inputs
for _, filter in enumerate([32,64,128]):
    x = layers.Conv2D(filters=filter, kernel_size=3, activation="relu")(x)

x = layers.Flatten()(x)
outputs = layers.Dense(10, activation="softmax")(x)

model_no_max_pool = keras.Model(inputs=inputs, outputs=outputs)
model_no_max_pool.summary()
Model: "model_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_2 (InputLayer)        [(None, 28, 28, 1)]       0         
                                                                 
 conv2d_3 (Conv2D)           (None, 26, 26, 32)        320       
                                                                 
 conv2d_4 (Conv2D)           (None, 24, 24, 64)        18496     
                                                                 
 conv2d_5 (Conv2D)           (None, 22, 22, 128)       73856     
                                                                 
 flatten_1 (Flatten)         (None, 61952)             0         
                                                                 
 dense_1 (Dense)             (None, 10)                619530    
                                                                 
=================================================================
Total params: 712,202
Trainable params: 712,202
Non-trainable params: 0
_________________________________________________________________

일단 2가지 문제가 있다고 함.

  1. 특성의 공간적 계층 구조 학습에 도움이 되지 않음
    • 세 번째 층의 3x3 윈도우는 초기 입력의 7x7 윈도우 영역에 대한 정보를 갖는다.
    • 28x28 픽셀 짜리를 7x7 픽셀로 숫자를 분류한다?
  2. 최종 특성 맵은 22x22x128 = 61952개의 원소를 갖는다.(매우 많다.)
    • 심각한 과대적합 발생 야기함.

즉, 다운샘플링을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위함임. 또한 연속적인 합성곱 층이 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적인 계층 구조를 구성함.

8.2 소규모 데이터셋에서 밑바닥부터 ConvNet 훈련하기


매우 적은 데이터를 사용해서 이미지 분류 모델을 훈련하는 일은 흔한 경우임. 데이터 증식(data augmentation)을 사용해서 해결함.

8.2.1 작은 데이터셋 문제에서 딥러닝의 타당성


모델을 훈련하기에 충분한 샘플 -> 상대적임 뭐 컴퓨터 비전 쪽은 데이터가 잘 공유되어서 좋다 이런말임..

부럽다..

8.2.2 데이터 수집하기


케글에서 제공하는 API를 이용해 코랩으로 데이터를 내려받자.

from google.colab import files
files.upload()
!mkdir ~/.kaggle
!cp kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
!kaggle competitions download -c dogs-vs-cats # 여기서 403 에러 뜨면 책 보셈
Downloading dogs-vs-cats.zip to /content
100% 811M/812M [00:26<00:00, 36.2MB/s]
100% 812M/812M [00:26<00:00, 32.3MB/s]
!unzip -qq dogs-vs-cats.zip
!unzip -qq train.zip
# 이미지를 훈련 검증 테스트 디렉터리로 복사하기
import os, shutil, pathlib

original_dir = pathlib.Path("train")
new_base_dir = pathlib.Path("cats_vs_dogs_small")

def make_subset(subset_name, start_index, end_index):
    for category in ("cat", "dog"):
        dir = new_base_dir / subset_name / category
        os.makedirs(dir)
        fnames = [f"{category}.{i}.jpg" for i in range(start_index, end_index)]
        for fname in fnames:
            shutil.copyfile(src=original_dir / fname,
                            dst=dir / fname)

make_subset("train", start_index=0, end_index=1000)
make_subset("validation", start_index=1000, end_index=1500)
make_subset("test", start_index=1500, end_index=2500)

8.2.3 모델 만들기


이전보다 이미지가 좀 크고 복잡하니 층을 추가함.

참고로Conv2D랑 MaxPooling2D층을 추가하면 다운샘플링이 한번더 이뤄지는것이니 Flatten()을 했을 때 비교적 가벼움

# 강아지 vs 고양이 분류를 위한 소규모 ConvNet 제작
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180,180,3))

x = layers.Rescaling(1./255)(inputs)
for _, filter in enumerate([32, 64, 128, 256]):
    x = layers.Conv2D(filters=filter, kernel_size=3, activation="relu")(x)
    x = layers.MaxPooling2D(pool_size=2)(x)

x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
outputs = layers.Dense(1, activation ="sigmoid")(x) # sigmoid -> 강아지 vs 고양이니까 입력이 강아지일 확률은?! 이런거 구하는 거임
model = keras.Model(inputs=inputs, outputs=outputs)
model.summary()
Model: "model_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_3 (InputLayer)        [(None, 180, 180, 3)]     0         
                                                                 
 rescaling (Rescaling)       (None, 180, 180, 3)       0         
                                                                 
 conv2d_6 (Conv2D)           (None, 178, 178, 32)      896       
                                                                 
 max_pooling2d_2 (MaxPooling  (None, 89, 89, 32)       0         
 2D)                                                             
                                                                 
 conv2d_7 (Conv2D)           (None, 87, 87, 64)        18496     
                                                                 
 max_pooling2d_3 (MaxPooling  (None, 43, 43, 64)       0         
 2D)                                                             
                                                                 
 conv2d_8 (Conv2D)           (None, 41, 41, 128)       73856     
                                                                 
 max_pooling2d_4 (MaxPooling  (None, 20, 20, 128)      0         
 2D)                                                             
                                                                 
 conv2d_9 (Conv2D)           (None, 18, 18, 256)       295168    
                                                                 
 max_pooling2d_5 (MaxPooling  (None, 9, 9, 256)        0         
 2D)                                                             
                                                                 
 conv2d_10 (Conv2D)          (None, 7, 7, 256)         590080    
                                                                 
 flatten_2 (Flatten)         (None, 12544)             0         
                                                                 
 dense_2 (Dense)             (None, 1)                 12545     
                                                                 
=================================================================
Total params: 991,041
Trainable params: 991,041
Non-trainable params: 0
_________________________________________________________________
# 모델 훈련 설정
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

8.2.4 데이터 전처리


  1. 사진 읽기
  2. RGB 픽셀로 디코딩
  3. 부동 소수점 텐서로 변환
  4. 동일한 크기의 이미지로 변경
  5. 배치로 묶기

아래 사용한 라이브러리는 지정한 폴더의 하위 폴더에 있는 폴더가 1개의 클래스라 가정함

# image_dataset_from_directory 사용해 이미지 읽기
from tensorflow.keras.utils import image_dataset_from_directory

train_dataset = image_dataset_from_directory(
    new_base_dir / "train",
    image_size = (180, 180),
    batch_size = 32)

validation_dataset = image_dataset_from_directory(
    new_base_dir / "validation",
    image_size = (180, 180),
    batch_size = 32)

test_dataset = image_dataset_from_directory(
    new_base_dir / "test",
    image_size = (180, 180),
    batch_size = 32)
# Dataset이 반환하는 데이터와 레이블 크기 확인하기
for data_batch, labels_batch in train_dataset:
    print("데이터 배치 크기 :", data_batch.shape)
    print("레이블 배치 크기 :", labels_batch.shape)
    break

데이터 배치 크기 : (32, 180, 180, 3)
레이블 배치 크기 : (32,)

ModelCheckpoint 콜백을 사용해서 에포크가 끝날 때마다 모델을 저장한다.

훈련하는 동안 val_loss가 이전보다 더 낮을 때 콜백이 새로운 파일을 덮어쓴다. 이렇게 하면 저장된 파일은 검증 성능이 가장 좋은 모델의 상태가 들어간다.

# Dataset을 사용하여 모델 훈련
callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath = "convnet_from_scratch.keras", # 파일을 저장할 경로 참고로 파이토치는 .pt
        save_best_only = True, # 언제나 성능이 가장 좋은 훈련 에포크의 모델 상태가 저장됨
        moniter='val_loss') # val_loss가 이전보다 더 낮을 경우 콜백이 동작함.
]
with tf.device('/gpu:0'):
    history = model.fit(
        train_dataset,
        epochs = 30,
        validation_data = validation_dataset,
        callbacks=callbacks
    )
import matplotlib.pyplot as plt

accuracy = history.history["accuracy"]
val_accuracy = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(accuracy) + 1)
plt.plot(epochs, accuracy, "bo", label="Training accuracy")
plt.plot(epochs, val_accuracy, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()
plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()

png

png

# 테스트 세트에서 모델 평가
test_model = keras.models.load_model("convnet_from_scratch.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
63/63 [==============================] - 3s 34ms/step - loss: 0.5747 - accuracy: 0.6970
테스트 정확도: 0.697

8.2.5 데이터 증식


데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법이다.

이 방법은 그럴듯한 이미지를 생성하도록 여러 가지 랜덤한 변환을 적용해 샘플을 늘린다.

# 컨브넷에 추가할 데이터 증식 단계 정의
data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"), # 랜덤하게 50% 이미지를 수평으로 뒤집는다.
        layers.RandomRotation(0.1), # +-10% 범위 안에서 랜덤한 값만큼 입력 이미지를 회전함.
        layers.RandomZoom(0.2), # +-20% 범위 안에서 랜덤한 비율만큼 이미지를 확대 또는 축소함.
    ]
)
# 랜덤하게 증식된 훈련 이미지 출력
plt.figure(figsize=(10, 10))
for images, _ in train_dataset.take(1):
    for i in range(9):
        augmented_images = data_augmentation(images)
        ax = plt.subplot(3, 3, i + 1)
        plt.imshow(augmented_images[0].numpy().astype("uint8"))
        plt.axis("off")

png

데이터 증식을 사용해서 새로운 모델을 훈련시킬 때 모델에 같은 입력 데이터가 두 번 주입되지 않는다. 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에 여전히 입력 데이터들 사이에 상호 연관성이 크다.

즉, 새로운 정보를 만들어 낼 수 없고 단지 기존 정보의 재조합만 가능하다.

그렇게 때문에 완전히 과대적합을 제거하기에 충분하지 않을 수 있다. 과대적합을 더 억제하기 위해 Dense layers 직전에 Dropout layers 을 추가했다.

tf.keras.layers.Dropout은 Overfitting문제를 해결하는 방법 중 하나다.

7

해당 그림은 과적합된 상태를 보여주는 그림이다.
검은 선 => 데이터 추세
녹색 선은 데이터를 설명하는 것이라 가정하자.

빨간 파랑은 녹색 선에 의해 매우 잘 분류된 모습을 보인다. 하지만, 새로운 데이터가 들어올 경우 이것을 예측하는 능력이 떨어질 것이다.

데이터 추세를 반영하지 못 해서

모델의 과적합 문제는 Regularization 방법을 주로 사용해서 해결하는데, Dropput 함수는 정규화의 방식 중 하나인 드롭아웃을 쉽게 구현하는 함수다.

즉, tf.keras.layers.Dropout은 이러한 과정을 알아서 해준다.

단, 명심해야하는 것이 있다.

랜덤한 이미지 증식 층에 대해 Dropout 층처럼 추론할 때(predict(), evaluate() 메서드 호출 시)는 동작하지 않는다.

즉, 모델을 평가할 때는 데이터 증식과 드롭아웃이 없는 모델처럼 동작한다.

# 이미지 증식과 드롭아웃을 포함한 컨브넷 만들기
from tensorflow import keras
from tensorflow.keras import layers

inputs = keras.Input(shape=(180,180,3))

x = layers.Rescaling(1./255)(inputs)
for _, filter in enumerate([32, 64, 128, 256]):
    x = layers.Conv2D(filters=filter, kernel_size=3, activation="relu")(x)
    x = layers.MaxPooling2D(pool_size=2)(x)

x = layers.Conv2D(filters=256, kernel_size=3, activation="relu")(x)
x = layers.Flatten()(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation ="sigmoid")(x) # sigmoid -> 강아지 vs 고양이니까 입력이 강아지일 확률은?! 이런거 구하는 거임
model = keras.Model(inputs=inputs, outputs=outputs)

model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])
# 규제를 추가한 모델 훈련
with tf.device('/gpu:0'):
    callbacks = [
    keras.callbacks.ModelCheckpoint(
        filepath="convnet_from_scratch_with_augmentation.keras",
        save_best_only=True,
        monitor="val_loss")
    ]
    history = model.fit(
        train_dataset, # 모델에 사용할 데이터
        epochs=100, # 에포크
        validation_data=validation_dataset, # 모델에 사용할 검증 데이터
        callbacks=callbacks)
test_model = keras.models.load_model(
    "convnet_from_scratch_with_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
63/63 [==============================] - 3s 44ms/step - loss: 0.5646 - accuracy: 0.7445
테스트 정확도: 0.744

8.3 사전 훈련된 모델 활용하기


사전 훈련된 모델(pretrained model)은 일반적으로 대규모 이미지 분류 문제를 위해 대량의 데이터셋에서 미리 훈련된 모델임.

당연히 데이터를 매우 많이 가졌다면 필요없는 작업이지만 대부분의 상황은 부족하다 느낄 것임.

VGG16 이라는 모델을 살펴 볼 것임.

VGG16은 조금 오래되었고 최고 수준의 성능은 아니며 다른 모델보다 조금 무겁다.

하지만, 실습 단계에서는 해당 구조가 우리가 보고있는 ConvNet과 구조가 비슷해서 이해가 쉬울 것 같아 설명함.

사전 훈련된 모델을 사용하는 두 가지 방법이 있다.

  1. 특성 추출(feature extraction)
  2. 미세 조정(fine tuning)

8.3.1 사전 훈련된 모델을 사용한 특성 추출


특성 추출은 사전에 학습된 모델의 표현을 사용하여 새로운 샘플에서 흥미로운 특성을 뽑아내는 것임.

이를 활용해서 새로운 분류기를 처음부터 훈련한다.

앞서 보았듯이 ConvNet은 이미지 분류를 위해 두 부분으로 구성된다.

합성곱 기반 층 : 연속된 합성곱과 풀링 층으로시작해서 Dense 층(밀집 층)으로 끝난다.

  • Conv -> MaxPooling -> Dense

ConvNet의 경우 feature extraction은 사전에 훈련된 모델의 합성곱 기반 층을 선택하여 새로운 데이터를 통과시키고, 그 출력으로 새로운 분류기를 훈련함.

8

왜 합성곱 층만 재사용할까?

FC 분류기도 재사용할 수 있을까? -> 일반적으로 권장 X

합성곱 층에 의해 학습된 표현이 더 일반적이어서 재사용이 가능함. ConvNet의 특성 맵은 이미지에 대한 일반적인 콘셉트의 존재 여부를 기록한 맵이다.

때문에 주어진 컴퓨터 비전 문제에 상관없이 유용한 사용 가능

FC 분류기도 이미 학습한 표현은 모델이 훈련된 클래스 집합에 특화되어 있음 단, 전체 사진에 어떤 Class가 존재할 확률에 관한 정보를 갖는다.

앞서 말했지만 FC는 표현이 더 이상 입력 이미지에 있는 객체의 위치 정보를 가지고 있지 않다.

합성곱의 특성 맵은 객체 위치를 고려함.

특정 합성곱 층에서 추출한 표현의 일반성(그리고 재사용성) 수준은 모델에 있는 층의 깊이에 달려있다.

앞의 그림에서 봤듯이 층이 깊어질 수록 더 지역적인 패턴을 감지하려함.

새로운 데이터셋이 원본 모델이 훈련한 데이터셋과 많이 다르다면 전체 합성곱 층을 사용하기 보다는 하위 층 몇개만 특성 추출에 사용하는 것이 좋다.

하위 층이 매우 일반적인 지역적 패턴을 갖기 때문

ImageNet(책에서 사용할 데이터셋)의 클래스 집합에는 여러 종류의 강아지, 고양이를 포함한다.

만약 새로운 데이터셋의 이미지가 우리가 이미 학습한 데이터셋의 이미지와 같은 클래스라면(ex. 고양이, 강아지) 모델의 Dense층에 있는 정보 재사용이 도움됨

새로운 문제의 클래스가 원본 모델의 클래스 집합과 겹치지 않는 좀 더 일반적인 경우를 다루기 위해 여기서는 Dense층을 사용하지 않는다.

ImageNet 데이터셋에 훈련된 VGG16의 합성곱 기반 층을 사용해서 강아지와 고양이 이미지에서 유요한 특성을 추출해 보자. 이후 특성을 바탕으로 두 클래스를 분류한다.

VGG16은 Keras에 package로 포함되어 있다. keras.applications에서 import하여 사용함.

keras.applications에서 사용 가능한 이미지 분류 모델은(모두 ImageNet으로 훈련)

  • Xception
  • ResNet
  • MoblieNet
  • EfficientNet
  • DenseNet
  • etc
# VGG16 합성곱 기반 층 만들기
conv_base = keras.applications.vgg16.VGG16(
    weights="imagenet", # 모델을 초기화할 가중치 체크포인트 지정
    include_top=False, # 네트워크 상단에 Dense 층 포함여부
    input_shape=(180, 180, 3)) # 주입할 이미지 텐서 크기(만약, 지정하지 않으면 알아서 처리함)
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5
58889256/58889256 [==============================] - 2s 0us/step
conv_base.summary()

Model: "vgg16"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_5 (InputLayer)        [(None, 180, 180, 3)]     0         
                                                                 
 block1_conv1 (Conv2D)       (None, 180, 180, 64)      1792      
                                                                 
 block1_conv2 (Conv2D)       (None, 180, 180, 64)      36928     
                                                                 
 block1_pool (MaxPooling2D)  (None, 90, 90, 64)        0         
                                                                 
 block2_conv1 (Conv2D)       (None, 90, 90, 128)       73856     
                                                                 
 block2_conv2 (Conv2D)       (None, 90, 90, 128)       147584    
                                                                 
 block2_pool (MaxPooling2D)  (None, 45, 45, 128)       0         
                                                                 
 block3_conv1 (Conv2D)       (None, 45, 45, 256)       295168    
                                                                 
 block3_conv2 (Conv2D)       (None, 45, 45, 256)       590080    
                                                                 
 block3_conv3 (Conv2D)       (None, 45, 45, 256)       590080    
                                                                 
 block3_pool (MaxPooling2D)  (None, 22, 22, 256)       0         
                                                                 
 block4_conv1 (Conv2D)       (None, 22, 22, 512)       1180160   
                                                                 
 block4_conv2 (Conv2D)       (None, 22, 22, 512)       2359808   
                                                                 
 block4_conv3 (Conv2D)       (None, 22, 22, 512)       2359808   
                                                                 
 block4_pool (MaxPooling2D)  (None, 11, 11, 512)       0         
                                                                 
 block5_conv1 (Conv2D)       (None, 11, 11, 512)       2359808   
                                                                 
 block5_conv2 (Conv2D)       (None, 11, 11, 512)       2359808   
                                                                 
 block5_conv3 (Conv2D)       (None, 11, 11, 512)       2359808   
                                                                 
 block5_pool (MaxPooling2D)  (None, 5, 5, 512)         0         
                                                                 
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________

최종 특성 맵 크기는 (5,5,512)이다. 이 위에 밀집 연결 층을 냅둘 예정임.

여기서 이제 trade-off가 발동된다.

  1. 새로운 데이터셋에서 합성곱 기반 층을 실행하고 출력을 넘파이 배열로 디스크에 저장한다. 그다음 이 데이터를 이 책의 4장에서 보았던 것과 비슷한 독립된 밀집 연결 분류기에 입력으로 사용한다. 합성곱 연산은 전체 과정 중에서 가장 비싼 부분이다. 이 방식은 모든 입력 이미지에 대해 합성곱 기반 층을 한 번만 실행하는 것이라 빠르고 자원적 비용이 싸다. 하지만 데이터 증식은 불가하다.

  2. 현재 모델 위에 Dense 층을 쌓아 확장한다. 그다음 입력 데이터에서 end-to-end로 전체 모델을 실행한다. 모델에 노출된 모든 입력 이미지가 매번 합성곱 기반 층을 통과하기 때문에 데이터 증식이 가능하다. 단, 1번 방법보다 매우 무겁다.

데이터 증식을 사용하지 않는 빠른 특성 추출

먼저 훈련, 검증, 테스트 데이터셋에서 Conv_base 모델의 predict() 메서드를 호출하여 넘파이 배열로 특성을 추출하자.

데이터셋을 순회하면서 VGG16의 특성을 추출해 보자.

import numpy as np

def get_features_and_labels(dataset):
    all_features = []
    all_labels = []

    for images, labels in dataset:
        preprocessed_images = keras.applications.vgg16.preprocess_input(images)
        features = conv_base.predict(preprocessed_images)
        all_features.append(features)
        all_labels.append(labels)

    return np.concatenate(all_features), np.concatenate(all_labels)


train_features, train_labels = get_features_and_labels(train_dataset)
val_features, val_labels = get_features_and_labels(validation_dataset)
test_features, test_labels = get_features_and_labels(test_dataset)

중요한 점은 predict() 메서드가 레이블을 제외하고 이미지만 기대한다는 점.

But, 현재 데이터셋은 이미지와 레이블을 함께 담고 있는 배치를 반환.

VGG16은 적절한 범위로 픽셀 값을 조정해 주는 keras.applications.vgg16.preprocess_input 함수로 전처리된 입력을 기대함.

train_features.shape

이제 Dropout을 사용한 Dense 분류기를 정의하고 방금 저장한 데이터와 레이블에서 훈련하자

# 밀집 연결 분류기 정의하고 훈련하기 (trade-off에서 말한 1번)
inputs = keras.Input(shape=(5, 5, 512))
x = layers.Flatten()(inputs)
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])

callbacks = [
    keras.callbacks.ModelCheckpoint(
      filepath="feature_extraction.keras",
      save_best_only=True,
      monitor="val_loss")
]
history = model.fit(
    train_features, train_labels,
    epochs=20,
    validation_data=(val_features, val_labels),
    callbacks=callbacks)

2개의 Dense 층만 처리하면 되므로 훈련이 겁나 빠르다.

# 결과를 그래프로 나타냄.
import matplotlib.pyplot as plt

acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()
plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()

png

png

test_model = keras.models.load_model(
    "feature_extraction.keras")
test_loss, test_acc = test_model.evaluate(test_features,test_labels)
print(f"테스트 정확도: {test_acc:.3f}")
63/63 [==============================] - 1s 5ms/step - loss: 4.6087 - accuracy: 0.9730
테스트 정확도: 0.973

약 97%의 검증 정확도에 도달했음.

첫 결과인 60% 정도? 보다는 훨씬 좋아졌다.

하지만 ImageNet에는 개와 고양이 샘플이 많기 때문에 약간 공정하지 않은 비교다. 다시 말하면 사전 훈련된 모델이 현재 주어진 작업에 딱 맞는 지식을 이미 가지고 있다.

항상 그런건 아님

근데 첫 사진을 보면 시작과 거의 동시에 과적합되어버림. 데이터가 작은데 이미지 증식을 못 쓰니까 이럼

이제 합성곱 기반 층을 동결하는 방법을 사용하자

하나 이상의 층을 동결(freezing)한다는 것은 훈련하는 동안 가중치가 업데이트되지 않도록 막는다는 의미.

가중치 업데이트가 가능하면 사전에 학습한 모델 상태가 수정됨

맨 위 Dense 층은 랜덤하게 초기화되었기 때문에 매우 큰 가중치 업데이트 값이 네트워크에 전달될 것임.

  • 이는 사전에 학습된 표현을 크게 훼손함

keras는 trainable속성을 False로 설정해서 동결 가능

conv_base  = keras.applications.vgg16.VGG16(
    weights="imagenet",
    include_top=False) # 맨 위에 Dense층 x
conv_base.trainable = False # 모델을 동결해버림

동결 전/후 훈련 가능한 가중치 리스트 출력

conv_base.trainable = True
print("합성곱 기반 층을 동결하기 전의 훈련 가능한 가중치 개수:",
      len(conv_base.trainable_weights))
합성곱 기반 층을 동결하기 전의 훈련 가능한 가중치 개수: 26
conv_base.trainable = False
print("합성곱 기반 층을 동결한 후의 훈련 가능한 가중치 개수:",
      len(conv_base.trainable_weights))
합성곱 기반 층을 동결한 후의 훈련 가능한 가중치 개수: 0
# 데이터 증식 단계와 밀집 분류기를 합성곱 기반 층에 추가하기

# 데이터 증식
data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.2),
    ]
)

inputs = keras.Input(shape=(180, 180, 3))
x = data_augmentation(inputs) # 데이터 증식 적용
x = keras.applications.vgg16.preprocess_input(x)  # 입력 값의 스케일 조정
x = conv_base(x)
x = layers.Flatten()(x)
x = layers.Dense(256)(x)
x = layers.Dropout(0.5)(x)
outputs = layers.Dense(1, activation="sigmoid")(x)
model = keras.Model(inputs, outputs)
model.compile(loss="binary_crossentropy",
              optimizer="rmsprop",
              metrics=["accuracy"])
with tf.device('/gpu:0'):
    callbacks = [
        keras.callbacks.ModelCheckpoint(
            filepath="feature_extraction_with_data_augmentation.keras",
            save_best_only=True,
            monitor="val_loss")
    ]
    history = model.fit(
        train_dataset,
        epochs=50,
        validation_data=validation_dataset,
        callbacks=callbacks)
import matplotlib.pyplot as plt
acc = history.history["accuracy"]
val_acc = history.history["val_accuracy"]
loss = history.history["loss"]
val_loss = history.history["val_loss"]
epochs = range(1, len(acc) + 1)
plt.plot(epochs, acc, "bo", label="Training accuracy")
plt.plot(epochs, val_acc, "b", label="Validation accuracy")
plt.title("Training and validation accuracy")
plt.legend()
plt.figure()
plt.plot(epochs, loss, "bo", label="Training loss")
plt.plot(epochs, val_loss, "b", label="Validation loss")
plt.title("Training and validation loss")
plt.legend()
plt.show()

png

png

test_model = keras.models.load_model(
    "feature_extraction_with_data_augmentation.keras")
test_loss, test_acc = test_model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
63/63 [==============================] - 7s 99ms/step - loss: 2.0406 - accuracy: 0.9770
테스트 정확도: 0.977

정확도가 대략 97.5%를 얻었다. 이전과 비교해서 큰 차이는 없어도 미세하게 향상되었음. 검증 데이터의 좋은 결과를 감안할 때 약간 아쉽다.

모델의 정확도는 항상 평가하려는 샘플 세트에 따라 달라진다. 일부 샘플 세트는 다른 세트에 비해 어려울 수 있으며 한 세트에서 좋은 결과가 다른 모든 세트에 항상 적용되는 것은 아니다.

8.3.2 사전 훈련된 모델 미세 조정하기


모델을 재사용하는 데 널리 사용되는 또 하나의 기법은 특성 추출을 보완하는 미세 조정이다.

미세 조정은 특성 추출에 사용했던 동결 모델의 상위 층 몇개를 동겨에서 해제하고 모델에 새로 추가한 층이(여기선 Dense)과 함께 훈련하는 것임.

9

앞서 랜덤하게 초기화된 상단 분류기를 훈련하기 위해 VGG16의 합성곱 기반 층을 동결해야 한다고 말했다.

같은 이유로 맨 위에 있는 분류기가 훈련된 후 합성곱 기반의 상위 층을 미세 조정할 수 있다.

분류기가 미리 훈련되지 않으면 훈련되는 동안 너무 큰 오차 신호가 네트워크에 전파된다. -> 미세 조정될 층들이 사전에 학습한 모델 상태를 오염시킴.

[네트워크 미세 조정 단계]

  1. 사전에 훈련된 기반 네트워크 위에 새로운 네트워크를 추가함
  2. 기반 네트워크를 동결
  3. 새로 추가한 네트워크를 훈련
  4. 기반 네트워크에서 일부 층의 동결을 해제함.
    • 배치 정규화(batch normalization)층은 동결 해제하면 안됨.

      VGG16은 해당 x

  5. 동결을 해제한 층과 새로 추가한 층을 함께 훈련

block4_pool 까지는 동결하고 나머지 Conv층은 미세 조정함

왜 3개만?
합성곱 기반 층에 있는 하위 층들은 더 일반적이고 재사용 가능한 특성들을 인코딩함. but 상위 층은 특화된 특성을 인코딩함.
훈련할 파라미터가 많을 수록 과대적함의 위험이 커짐.

# 마지막에서 4 번째 층까지 모든 층 동결

conv_base.trainable = True
for layer in conv_base.layers[:-4]:
    layer.trainable = True
with tf.device('/gpu:0'):

    model.compile(loss="binary_crossentropy",
                optimizer=keras.optimizers.RMSprop(learning_rate=1e-5), # 학습률을 낮춘 RMSProp 사용
                 #(낮추는 이유는 미세 조정하는 3개 층에서 학습된 표현을 조금씩 수정하기 위함임.)
                metrics=["accuracy"])

    callbacks = [
        keras.callbacks.ModelCheckpoint(
            filepath="fine_tuning.keras",
            save_best_only=True,
            monitor="val_loss")
    ]
    history = model.fit(
        train_dataset,
        epochs=30,
        validation_data=validation_dataset,
        callbacks=callbacks)
model = keras.models.load_model("fine_tuning.keras")
test_loss, test_acc = model.evaluate(test_dataset)
print(f"테스트 정확도: {test_acc:.3f}")
63/63 [==============================] - 7s 94ms/step - loss: 0.5132 - accuracy: 0.9770
테스트 정확도: 0.977

Categories:

ai