LITTLE BY LITTLE

[5] 케라스 창시자에게 배우는 딥러닝 - 5. 컴퓨터 비전을 위한 딥러닝( 합성곱 신경망 소개, MaxPooling, 고양이vs강아지 분류 예제, 데이터 증식) 본문

데이터 분석/케라스 창시자에게 배우는 딥러닝

[5] 케라스 창시자에게 배우는 딥러닝 - 5. 컴퓨터 비전을 위한 딥러닝( 합성곱 신경망 소개, MaxPooling, 고양이vs강아지 분류 예제, 데이터 증식)

위나 2022. 8. 16. 16:22

제 5부.컴퓨터 비전을 위한 딥러닝

  1. 합성곱 신경망 소개
  2. 소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기
  3. 사전 훈련된 컨브넷 사용하기
  4. 컨브넷 학습 시각화
  5. 요약
더보기

제 6부. 텍스트와 시퀀스를 위한 딥러닝

  1. 텍스트 데이터 다루기
  2. 순환 신경망 이해하기
  3. 순환 신경망의 고급 사용법
  4. 컨브넷을 사용한 시퀀스 처리
  5. 요약

제 7부. 딥러닝을 위한 고급 도구

  1. Sequential 모델을 넘어서 : 케라스의 함수형 API
  2. 케라스 콜백과 텐서보드를 사용한 딥러닝 모델 검사와 모니터링
  3. 모델의 성능을 최대화로 끌어올리기
  4. 요약

제 8부. 생성 모델을 위한 딥러닝

  1. LSTM으로 텍스트 생성하기
  2. 딥드림
  3. 뉴럴 스타일 트랜스퍼
  4. 변이형 오토인코더를 사용한 이미지 생성
  5. 적대적 생성 신경망 소개
  6. 요약

제 9부. 결론

  1. 핵심 개념 리뷰
  2. 딥러닝의 한계
  3. 딥러닝의 미래
  4. 빠른 변화에 뒤처지지 않기
  5. 맺음말

 


5-1. 합성곱 신경망 소개

2장에서 완전 연결 네트워크(densely-connected network)로 풀었던 예제에서의 테스트 정확도는 97.98%였다.

같은 예제를 컨브넷을 사용해서 풀어보자.

# 간단한 컨브넷 만들기
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32, (3,3),activation='relu', input_shape=(28,28,1)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64, (3,3), activation='relu'))
model.summary()

[Out]

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 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, 64)          36928     
                                                                 
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
  1. 컨브넷이
    1. (image_height, image_width, image_channels) 크기의 입력 텐서를 사용하고,
    2. 배치차원은 포함하지 않는다는 점이 중요 
    3. 이 예제에서는 MNIST 이미지 포맷인 (28,28,1) 크기의 입력을 처리하도록 컨브넷을 설정해야한다.
    4. 따라서 첫번째 층의 매개변수로 input_shape=(28,28,1)을 전달함
  2. model.summary()로 컨브넷의 구조를 살펴보면, 
    1. Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D텐서이다. 
    2. 높이와 너비 차원(height,width)은 네트워크가 깊어질수록 작아지는 경향
    3. 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절됨 (32개 또는 64개)
  3. 다음 단계에서 마지막 층의 ((3,3,64) 크기인) 출력 텐서를 완전 연결 네트워크에 주입해보자. 이 네트워크는 앞서 보았던 Dense 층을 쌓은 분류기이다.
    1. 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 3D 텐서이다.
    2. 따라서 먼저 3D 출력을 1D텐서로 펼쳐야한다.
    3. 그 다음에 몇 개의 Dense 층을 추가한다.
# 컨브넷 위에 분류기 추가하기
model.add(layers.Flatten())
model.add(layers.Dense(64, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))
model.summary()

 

[Out]

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 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, 64)          36928     
                                                                 
 flatten (Flatten)           (None, 576)               0         
                                                                 
 dense (Dense)               (None, 64)                36928     
                                                                 
 dense_1 (Dense)             (None, 10)                650       
                                                                 
 flatten_1 (Flatten)         (None, 10)                0         
                                                                 
 dense_2 (Dense)             (None, 64)                704       
                                                                 
 dense_3 (Dense)             (None, 10)                650       
                                                                 
=================================================================
Total params: 94,676
Trainable params: 94,676
Non-trainable params: 0
_________________________________________________________________
  1. 10개의 클래스를 분류하기 위해서, 마지막 층의 출력크기를 10으로 하고, 소프트맥스 활성화 함수를 사용하였다.     →model.add(layers.Dense(10, activation='soffmax'))
  2. (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후,  Dense 층으로 주입되었다.

위 코드 일부 캡쳐

이제 MNIST 숫자 이미지에 이 컨브넷을 훈련시키자. (2장의 MNIST 예제코드 재사용, https://noelee.tistory.com/59?category=1294039) 

 

[2] 케라스 창시자에게 배우는 딥러닝 - 신경망의 수학적 구성 요소

제 2부. 신경망의 수학적 구성 요소 신경망과의 첫 만남 신경망을 위한 데이터 표현 - 스칼라,벡터,행렬,3D텐서와 고차원 텐서, .. 신경망의 톱니바퀴 : 텐서 연산 신경망의 엔진 : 그래디언트 기반

noelee.tistory.com

# MNIST 이미지에 컨브넷 훈련하기
from keras.datasets import mnist
from keras.utils.np_utils import to_categorical

(train_images, train_labels), (test_images, test_labels) = mnist.load_data()

train_images = train_images.reshape((60000,28,28,1))
train_images = train_images.astype('float32')/255

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

train_labels = to_categorical(train_labels)
teset_labels = to_categorical(test_labels)

model.compile(optimizer = 'rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])
model.fit(train_images, train_labels, epochs=5, batch_size=64)

# 테스트 데이터에서 모델 평가
test_loss, test_acc = model.evaluate(test_images, test_labels)
test_acc

2장에서 완전 연결 네트워크로 풀었을 때에는 97.8%였던 반면, 컨브넷은 아주 기본적인 컨브넷인데에도 불구하고, 99.2%의 테스트 정확도를 얻었다.

완전 연결된 모델보다 간단한 컨브넷이 더 잘 작동하는 이유를 알아보기 위해 Conv2DMaxPooling2D층에 대해 더 살펴보자.


5-1-1. 합성곱 연산

  1. 완전 연결층과 합성곱 층 사이의 근본적인 차이는 다음과 같다.
    1. Dense 층은 입력 특성 공간에 있는 전역 패턴(ex. MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만, 합성곱 층은 지역 패턴을 위 그림처럼 지역 패턴을 학습한다.
    2. 합성곱 층은 이미지일 경우 작은 2D 윈도우로 입력해 패턴을 찾는다. 앞의 예에서는 이 윈도우는 모두 3X3크기였다.
    3. 이 핵심 특징은 컨브넷의 2가지 성질 제공
      1. 학습된 패턴을 '평행 이동 불변성(translation invaraint)'을 가진다.
      2. 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면, 다른 곳(ex. 왼쪽 위 모서리)에서도 이 패턴을 인식할 수 있다. (반면에 완전 연결 네트워크는 새로운 위치에 나타나면 새로운 패턴으로 학습해야함)
    4. 이러한 성질이 컨브넷이 이미지를 효율적으로 처리하게 만들어주고, 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있다. 
    5. 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있다. 1번째 합성곱 층이 Edge와 같은 작은 지역 패턴을 학습한다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성된 더 큰 패턴을 학습하는 식이다. → 매우 복잡하고 추상적인 시각적 개념 학습 가능
  2. 합성곱 연산은 특성 맵(feature map)이라 부르는 3D텐서에 적용된다. - 2개의 공간 축(높이와 너비)과 깊이 축(채널 축)으로 구성된다.
    1. RGB 이미지는 3개의 컬러 채널(빨,노,파)를 가지므로, 깊이 축의 차원이 3이 됨
    2. MNIST 숫자처럼 흑백 이미지의 경우, 깊이 축의 차원이 1(회색 톤)이다.
    3. 합성곱 연산은 입력 특성 맵에서 작은 패치들을 추출하고, 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature map)을 만든다.

우리가 보는 세상은 시각적 구성 요소들의 '공간적 계층 구조'로 구성되어 있으며, 아주 좁은 지역의 Edge들이 연결되어 눈이나 귀와 같은 국부적 구성요소를 만들고, 이들이 모여서 고양이처럼 '고수준'의 개념을 만든다.

2-4. 출력 특성 맵도 높이와 너비를 가진 3D텐서이다. 

  1. 출력 텐서의 깊이는 층의 매개변수로 결정 (input_shape=(28,28,1))
  2. 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. 그 대신, 일종의 필터를 의미함
  3. 필터는 입력 데이터의 어떤 특성을 인코딩한다. (ex. 고수준으로 보면 하나의 필터가 '입력에 얼굴이 있는지'를 인코딩할 수 있다. )
  4. MNIST 예제에서 1번째 합성곱 층은 (28,28,1)크기를 입력받아 (26,26,32)크기의 특성 맵 출력 

model.add 첫번째줄 input_shape=(28,28,1) 크기를 입력받아 [Out]의 1st layer에 Output sahpe=(26,26,32) 크기의 특성 맵 출력

→ 즉 입력에 대해 32개 필터 적용, 32개 출력 채널 각각은 26x26 크기의 배열 값 가짐 = '입력에 대한 필터의 응답 맵(response map)'이라 부르며, 입력의 각 위치에서 필터 패턴에 대한 응답을 나타냄, 아래 그림 참고

입력에 대한 필터의 응답 맵(response map)

- 즉, 특성맵의 의미는  '깊이 축에 있는 각 차원은 하나의 특성(=필터)이고, 2D 텐서 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵'이다.

- 합성곱은 핵심적인 2개의 파라미터로 정의된다.

  1. 입력으로부터 뽑아낼 패치의 크기 : 전형적으로 3x3,혹은 5x5 크기를 사용
  2. 특성 맵의 출력 깊이 : 합성 곱으로 계산할 필터의 수, 이 예시에서는 32로 시작해서 64로 끝났다.
  3. 작동 순서
    1. 케라스의 Con2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width))처럼 1번째와 2번째 매개변수로 전달된다.
    2. 3D 입력 특성 맵 위를 3x3 또는 5x5 크기의 윈도우가 슬라이딩하면서 모든 위치에서 3D 특성 패치((window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동한다. 
    3. 이런 3D 패치는 (output_depth, )크기 1D 벡터로 변환된다. ( 합성곱 터널[convolution kernel]이라 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통하여 변환됨 )
    4. 변환된 모든 벡터는 (height, width, ouptut_depth) 크기의 3D 특성 맵으로 재구성된다.출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응됨 (ex. 출력의 우측 아래 모서리는 입력의 우측 아래 부근에 해당하는 정보를 담고 있음)
    5. 3x3 윈도우를 사용하면 3D 패치 input[i-1,i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어진다. 아래그림 참고

위 순서를 보여주는 그림

- 출력 높이&너비가 입력 높이&너비와 다를 수 있다. 그이유는

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

입출력의 높이&너비가 다를 수 있는 첫번째 이유인 경계문제와 패딩에 대해 이해하기

5x5 입력 특성 맵에서 가능한 3x3 패치 위치

  1. 5x5 크기의 특성 맵을 생각해보자.(25개의 타일이 있다고 가정) 
  2. 3x3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3x3 격자를 형성하는 9개 뿐
  3. 따라서 출력 특성 맵은 3x3 크기가 된다. 크기가 조금 줄어들었다. 
  4. 여기에서는 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다.
  5. 앞선 예에서도 이런 경계문제가 있었다. 1번째 합성곱에서 28x28크기인 입력이 26x26 크기가 됨

25개의 3x3 패치를 뽑기 위해서 기존의 5x5 입력에 패딩을 추가한 모습

  1. 만약 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면, 패딩을 사용할 수 있다. 
  2. 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가함
  3. 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있다.
  4. 3x3 윈도우라면 위아래에 하나의 행을 추가하고, 오른쪽, 왼쪽에 하나의 열을 추가한다. 
  5. 5x5 윈도우라면 2개의 행과 열을 추가한다. 
  6. Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있다.
    1. 2개의 값이 가능하다.
    2. "valid"는 패딩을 사용하지 않겠다는 뜻이다.(윈도우를 놓을 수 있는 위치만 사용)
    3. "same"은 입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다라는 의미
    4. padding 매개변수의 기본 값은 valid

입출력의 높이&너비가 다를 수 있는 두번째 이유인 합성곱 스트라이드에 대해 이해하기

  1. 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정하였다.
  2. '2번의 연속적인 윈도우 사이의 거리'스트라이드라고 불리는 합성곱의 파라미터이다. 
  3. 스트라이드의 기본 값은 1이며, 1보다 큰 스트라이드 합성곱도 가능하다. 
  4. 5x5 크기의 입력(패딩 없음)에 스트라이드2를 사용한 3x3 크기의 윈도우로 합성곱하여 추출한 패치가 다음과 같다.

스트라이드를 사용한 3x3 합성곱의 패치

  1. '스트라이드2를 사용했다'는 것은 '특성 맵의 너비와 높이가 2의 배수로 down sampling되었다'는 의미이다. (경계 문제가 있을시 더 줄어듦)
  2. 실전에서는 많이 쓰이지 않으나, 어떤 모델에서는 유용하게 사용될 수 있다.
  3. 특성 맵을 다운샘플링 하기 위해서 스트라이드 대신에 첫 번째 컨브넷 예제에서 max pooling(최대 풀링)연산을 사용하는 경우가 많다. 최대풀링에 대해 알아보자.

5.1.2 최대 풀링 연산

  1. 앞의 컨브넷 예제에서 특성 맵의 크기가 MaxPooling 2D층마다 절반으로 줄어들었다. (26x26 → 13x13)
  2. 스트라이드 합성곱과 비슷하게 강제적으로 특성맵을 down sampling하는 것이 max pooling의 역할
  3. 특성 맵에서 윈도우에 맞는 패치를 추출하고, 각 채널별로 최댓값을 출력한다.
  4. 합성곱과 개념적응로 비슷하지만, 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신, 하드코딩된 최댓값 추출 연산을 사용
  5. 합성곱과의 가장 큰 차이점은 max pooling은 보통 2x2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 down sampling한다는 점, 반면 합성곱은 전형적으로 3x3 윈도우와 스트라이드 1 사용
  6. 최대 풀링 층을 빼고 큰 특성맵을 계속 유지하지 않고 이런식으로 down sampling하는 이유에 대해 알아보자.
from keras import layers
from keras import models

model_no_max_pool = models.Sequential()
model_no_max_pool.add(layers.Conv2D(32,(3,3), activation='relu',
                                    input_shape=(28,28,1)))
model_no_max_pool.add(layers.Conv2D(64,(3,3),activation='relu'))
model_no_max_pool.add(layers.Conv2D(64,(3,3),activation='relu'))
model_no_max_pool.summary()

[Out]

Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d_6 (Conv2D)           (None, 26, 26, 32)        320       
                                                                 
 conv2d_7 (Conv2D)           (None, 24, 24, 64)        18496     
                                                                 
 conv2d_8 (Conv2D)           (None, 22, 22, 64)        36928     
                                                                 
=================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
_________________________________________________________________
  1. 이 설정에서의 문제 두 가지 (최대 풀링 사용 X)
    1. 특성의 공간적 계층 구조 학습에 도움이 되지 않음
      1. 3번째 층의 3x3 윈도우는 초기 입력의 7x7 윈도우 영역에 대한 정보만 담고 있다. 
      2. 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분치 않음
      3. 마지막 합성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야함
    2. 최종 특성 맵은 22 x 22 x 64 = 30,976개의 원소를 가진다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결하면 약 15.8백만 개의 가중치 파라미터가 생긴다. 작은 모델치고는 너무 많은 가중치고, 심각한 과대적합이 발생할 것
  2. 간단히 말해 down sampling을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서
  3. 또한 연속적인 합성곱 층이 원본 입력에서 커버되는 영역 측면에서, 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적 계층 구조 구성
  4. max pooling, 스트라이드 이외에도 최댓값 대신 채널별 평균값을 계산하여 변환하는 평균 풀링을 사용할 수도 있다. 
  5. 하지만 특성이 특성맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문에, 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용
  6. 스트라이드가 없는 합성곱인 sub sampling 전략으로 조밀한 특성 맵을 만들고, 그 다음 작은 패치에 대해서 최대로 활성화된 특성을 고르는 것 (입력에 대해 스트라이드 합성곱으로 듬성듬성 윈도우를 슬라이딩하거나, 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 낫다.)

5.2 소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기

  1. 매우 적은 데이터로 이미지 분류 모델을 훈련하는 일은 흔한 경우이다. 적은 샘플이란 수백 개에서 수만 개 사이
  2. 2,000개의 고양이 사진과 2,000개의 강아지 사진으로 구성된 데이터셋에서 강아지 고양이 이미지를 분류해보자.
    1. 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련해보자. 
    2. 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만듦
    3. 71%의 분류 정확도를 달성할 것, 이 방법의 주요 이슈는 과대적합
    4. 그 다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 '데이터 증식'을 소개할 예정 (82%의 정확도)
    5. 그 다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두가지를 살펴볼 것
      1. 사전 훈련된 네트워크로 추출 특성 (90%의 정확도)
      2. 사전 훈련된 네트워크를 세밀하게 튜닝(92%의 정확도)
    6. 위와 같은 세가지 전략 (1)처음부터 작은 모델 훈련/ 2)사전 훈련된 모델 사용하여 특성 추출/ 3)사전 훈련된 모델 세밀하게 튜닝) 은 작은 데이트셋에서 이미지 분류 수행시 도구상자에 포함되어 있어야한다.

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

  1. 딥러닝은 데이터가 풍부할 때만 작동한다는 말이 있다. 딥러닝의 근본적 특징은 훈련데이터의 특성 공학의 수작업 없이 흥미로운 특성을 찾을 수 있는 것인데, 이는 훈련샘플이 많아야지만 가능하기 때문이다. (입력샘플이 이미지인 매우 고차원적인 문제에서 특히 그러하다.)
  2. 많은 샘플이 의미하는건 절대적이지 않다. 훈련하려는 네트워크의 크기와 깊이에 상대적이기 떄문
  3. 복잡한 문제를 푸는 컨브넷은 수십개의 샘플만 사용해 훈련하는 것은 불가능, 하지만 모델이 작고 규제가 잘 되어 있으면 간단한 작업이라면 수백 개의 샘플로도 충분할 수 있다. 
  4. 컨브넷은 지역적이고 평행이동으로 변하지 않는 특성을 학습하기 때문에, 지각에 관한 문제에서 매우 효율적으로 데이터를 사용함
  5. 매우 작은 이미지 데이터셋에서 어떤 종류의 특성 공학을 사용하지 않고, 컨브넷을 처음부터 훈련해도 납득할만한 결과를 만들 수 있음
  6. 딥러닝 모델은 태생적으로 매우 다목적이다. 대규모 데이터셋에서 훈련시킨 이미지 분류 모델이나 speech-to-text 모델을 조금만 변경해서 완전히 다른 문제에 사용 가능
  7. 특히 컴퓨터 비전에서는 사전 훈련된 모델들이 내려받을 수 있도록 많이 공개되어 있어 매우 적은데이터에서 강력한 비전 모델을 만드는데 사용 가능

5.2.2 데이터 내려받기

  1. 이 데이터셋은 2만 5,000개의 강아지와 고양이 이미지 (클래스마다 1만 2,500개)를 담고 있다.
  2. 3개의 서브셋이 들어있는 새로운 데이터셋을 만들 것이다.
  3. 클래스마다 1,000개의 샘플로 이루어진 훈련세트, 클래스마다 500개의 샘플로 이루어진 검증 세트, 클래스마다 500개의 샘플로 이루어진 테스트 세트

드라이브 마운트

from google.colab import drive
drive.mount('/content/gdrive')

압축 파일 올려서 압출 풀기 (이미지가 많아서 올리는데 시간이 오래 걸려서 이 방법이 더 빠름)

%cd /content/gdrive/MyDrive/Keras/cats_and_dogs #압축 풀 경로

!unzip -qq "/content/gdrive/MyDrive/train.zip"  #zip파일 올린 경로

이미지 디렉터리에 나누기

import os, shutil

# 원본 데이터셋을 압축 해제한 디렉터리 경로
original_dataset_dir = '/content/gdrive/MyDrive/Keras/cats_and_dogs/train'

# 소규모 데이터셋을 저장할 디렉터리
base_dir = '/content/gdrive/MyDrive/Keras/cats_and_dogs_small'
if os.path.exists(base_dir):  # 반복적인 실행을 위해 디렉토리를 삭제
    shutil.rmtree(base_dir)   # 이 코드는 책에 포함되어 있지 않습니다.
os.mkdir(base_dir)

# 훈련/검증/테스트 분할을 위한 디렉터리
train_dir = os.path.join(base_dir, 'train') 
os.mkdir(train_dir)
validation_dir = os.path.join(base_dir, 'validation') 
os.mkdir(validation_dir)
test_dir = os.path.join(base_dir, 'test') 
os.mkdir(test_dir)

train_cats_dir = os.path.join(train_dir, 'cats') # 훈련용 고양이 사진 디렉터리
os.mkdir(train_cats_dir)

train_dogs_dir = os.path.join(train_dir, 'dogs') # 훈련용 강아지 사진 디렉터리
os.mkdir(train_dogs_dir)

validation_cats_dir = os.path.join(validation_dir, 'cats')
os.mkdir(validation_cats_dir)

validation_dogs_dir = os.path.join(validation_dir, 'dogs')
os.mkdir(validation_dogs_dir)

test_cats_dir = os.path.join(test_dir, 'cats')
os.mkdir(test_cats_dir)

test_dogs_dir = os.path.join(test_dir, 'dogs')
os.mkdir(test_dogs_dir)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000)] # 처음 1,000개의 고양이 이미지를 train_cats_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname) 
    dst = os.path.join(train_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1000,1500)] # 다음 500개의 고양이 이미지를 validation_cat_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['cat.{}.jpg'.format(i) for i in range(1500,2000)] # 다음 500개의 고양이 이미지를 test_cat_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_cats_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1000)] # 처음 1,000개의 강아지 이미지를 train_dogs_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname) 
    dst = os.path.join(train_dogs_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1000,1500)] # 다음 500개의 강아지 이미지를 validation_dog_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(validation_dogs_dir, fname)
    shutil.copyfile(src, dst)

fnames = ['dog.{}.jpg'.format(i) for i in range(1500,2000)] # 다음 500개의 강아지 이미지를 test_dog_dir에 복사
for fname in fnames:
    src = os.path.join(original_dataset_dir, fname)
    dst = os.path.join(test_dogs_dir, fname)
    shutil.copyfile(src, dst)

잘 나뉘어졌나 확인

# 복사가 잘 되었는지 확인해보기 위해 각 분할 속 사진 개수 카운트
print('훈련용 고양이 이미지 전체 개수:', len(os.listdir(train_cats_dir)))
print('검증용 고양이 이미지 전체 개수:', len(os.listdir(validation_cats_dir)))
print('테스트용 고양이 이미지 전체 개수:', len(os.listdir(test_cats_dir)))
print('훈련용 강아지 이미지 전체 개수:', len(os.listdir(train_dogs_dir)))
print('검증용 강아지 이미지 전체 개수:', len(os.listdir(validation_dogs_dir)))
print('테스트용 강아지 이미지 전체 개수:', len(os.listdir(test_dogs_dir)))

[Out]

훈련용 고양이 이미지 전체 개수: 1000
검증용 고양이 이미지 전체 개수: 500
테스트용 고양이 이미지 전체 개수: 500
훈련용 강아지 이미지 전체 개수: 1000
검증용 강아지 이미지 전체 개수: 500
테스트용 강아지 이미지 전체 개수: 500

5.2.3 네트워크 구성하기

  1. 이전에 만든 간단한 컨브넷과 구조는 일반적으로 동일하다.
  2. Conv2D (relu 활성화 함수 사용)MaxPooling2D 층을 번갈아 쌓은 컨브넷을 만들자.
  3. 이전보다 이미지가 크고 복잡한 문제이기 때문에, 네트워크를 더 크게 만들어야한다. (Conv2D+MaxPooling2D 단계를 하나 더 추가) 
  4. → 이렇게 하면 네트워크의 용량을 늘리고, Flatten 층의 크기가 너무 커지지않도록 특성맵의 크기를 줄일 수 있다. 150x150 크기의 입력으로 시작할시, Flatten 층 이전에 7x7 크기의 특성맵으로 줄어듦

특성 맵의 "깊이"는 점진적으로 증가하지만, 특성 맵의 "크기"는 감소함

이진 분류 문제이므로 네트워크는 하나의 유닛 (크기가 1인 Dense층)과 sigmoid 활성화 함수로 끝난다. 이 유닛은 한 클래스에 대한 확률을 인코딩한다.

컨브넷 만들기

# 강아지 vs 고양이 분류를 위한 소규모 컨브넷 만들기
from keras import layers
from keras import models

model = models.Sequential()
model.add(layers.Conv2D(32,(3,3), activation='relu', 
                        input_shape=(150,150,3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64,(3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128,(3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.summary()

[Out]

Model: "sequential_5"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d_13 (Conv2D)          (None, 148, 148, 32)      896       
                                                                 
 max_pooling2d_6 (MaxPooling  (None, 74, 74, 32)       0         
 2D)                                                             
                                                                 
 conv2d_14 (Conv2D)          (None, 72, 72, 64)        18496     
                                                                 
 max_pooling2d_7 (MaxPooling  (None, 36, 36, 64)       0         
 2D)                                                             
                                                                 
 conv2d_15 (Conv2D)          (None, 34, 34, 128)       73856     
                                                                 
 max_pooling2d_8 (MaxPooling  (None, 17, 17, 128)      0         
 2D)                                                             
                                                                 
 flatten_1 (Flatten)         (None, 36992)             0         
                                                                 
 dense_2 (Dense)             (None, 512)               18940416  
                                                                 
 dense_3 (Dense)             (None, 1)                 513       
                                                                 
=================================================================
Total params: 19,034,177
Trainable params: 19,034,177
Non-trainable params: 0
_________________________________________________________________

컴파일 단계에서 이전과 같이 RMSprop 옵티마이저를 선택한다. 그리고 손실로는 마지막이 하나의 시그모이드 유닛이기 때문에 이진 크로스엔트로피(binary crossentropy)를 사용한다.

컴파일 단계

# 모델의 훈련 설정하기
# from keras import optimizers
from tensorflow.keras import optimizers #에러날 시 import 방식 바꿔주기

model.compile(loss='binary_crossentropy', 
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])

 


5.2.4 데이터 전처리

  1. 데이터는 네트워크에 주입되기 전에 부동 소수 타입의 텐서로 적절하게 전처리되어 있어야 한다.
  2. 지금은 데이터가 JPEG 파일로 되어있으므로 네트워크에 주입하기 위해 전처리 해주기
    1. 사진 파일을 읽는다.
    2. JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩한다.
    3. 그 다음 부동 소수 타입의 텐서로 변환한다.
    4. 픽셀 값(0에서 255 사이)의 스케일을 [0,1] 사이로 조정한다.(신경망은 작은 입력값을 선호함)

위 과정을 자동으로 처리하는 유틸리티가 케라스에 있다. 

  1. keras.preprocessing.image에 이미지 처리를 위한 헬퍼도구들도 있다. 특히 ImageDataGenerator 클래스는 디스크에 있는 이미지 파일을 전처리된 배치 텐서로 자동으로 바꾸어주는 파이썬 제너레이터를 만들어준다.

파이썬 제너레이터 이해하기 : 파이썬 제너레이터는 '반복자'처럼 작동하는 객체로, for...in 객체에 사용할 수 있다. 제너레이터는 yield 연산자를 사용하여 만든다.

def generator():
    i = 0
    while True:
        i += 1
        yield i
for item in generator():
    print(item)
    if item>4:
        break

[Out]

1
2
3
4
5

ImageDataGenerator을 사용하여 디렉터리에서 이미지 읽기

# ImageDataGenerator로 디렉터리에서 이미지 읽기
from keras.preprocessing.image import ImageDataGenerator

train_datagen = ImageDataGenerator (rescale = 1./255) # 모든 이미지를 1/255로 스케일 조정
test_datagen = ImageDataGenerator (rescale = 1./255)

train_generator = train_datagen.flow_from_directory(
    train_dir, # 타깃 디렉터리
    target_size=(150,150), #모든 이미지를 150x150 크기로 바꾼다.
    batch_size = 20,
    class_mode = 'binary')

validation_generator = test_datagen.flow_from_directory(
    validation_dir, target_size=(150,150),
    batch_size=20,
    class_mode = 'binary')

# 제너레이터의 출력을 살펴보자
for data_batch, labels_batch in train_generator:
    print('배치 데이터 크기:', data_batch.shape)
    print('배치 레이블 크기:', labels_batch.shape)
    break

[Out]

Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.

배치 데이터 크기: (20, 150, 150, 3)
배치 레이블 크기: (20,)
  1. 위 출력은 150x150 RGB 이미지의 배치(20, 150, 150, 3) 크기)와 이진 레이블의 배치((20,) 크기)이다.
  2. 각 배치에는 20개의 샘플(배치 크기)이 있다. 제너레이터는 이 배치를 무한정 만들어 내고, 타깃 폴더에 있는 이미지를 끊임없이 반복하기 때문에, 반복 루프 안 어디에선가 break 문을 사용해야 한다.
  3. 이제 제너레이터를 사용한 데이터에 모델을 훈련시켜보자. 
  4. fit_generator 메소드는 fit 메소드와 동일하되, 데이터 제너레이터를 사용할 수 있다.
    1. 이 메소드는 1번째 매개변수로 입력과 타깃의 배치를 끝없이 반환하는 파이썬 제너레이터를 받고
    2. 데이터가 끝없이 생성되기 때문에, 케라스 모델에 하나의 에포크를 정의하기 위해 제너레이터로부터 얼마나 많은 샘플을 뽑을 것인지 알려주어야 한다. steps_per_epoch 매개변수에서 이를 설정한다.
    3. 제너레이터로부터 steps_per_epoch개의 배치만큼 뽑는다.(=steps_per_epoch 횟수만큼 경사 하강법 단계를 실행한 후에 훈련 프로세스는 다음 에포크로 넘어간다.)
    4. 여기서는 20개의 샘플이 하나의 배치이므로 2,000개의 샘플을 모두 처리할 때까지 100개의 배치를 뽑는다.
    5. fit_generator을 사용할 때 fit메소드와 마찬가지로 validation_data 매개변수를 전달할 수 있다. (데이터 제너레이터도 가능하고, 넘파이 배열의 튜플도 가능)
    6. validation_data로 제너레이터를 전달하면, 검증 데이터의 배치를 끝없이 반환한다. 
    7. 따라서 검증 데이터 제너레이터에서 얼마나 많은 배치를 추출하여 평가할지 validation_steps 매개변수에 지정해야한다.

배치 제너레이터를 사용하여 모델 훈련 (fit_generator())

# 배치 제너레이터를 사용하여 모델 훈련하기
history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=30,
    validation_data=validation_generator,
    validation_steps=50)

[Out] 거의 1시간 걸린다 .....

Epoch 25/30
100/100 [==============================] - 111s 1s/step - loss: 0.0417 - acc: 0.9905 - val_loss: 1.0228 - val_acc: 0.7140
Epoch 26/30
100/100 [==============================] - 111s 1s/step - loss: 0.0394 - acc: 0.9920 - val_loss: 0.9982 - val_acc: 0.7200
Epoch 27/30
100/100 [==============================] - 111s 1s/step - loss: 0.0310 - acc: 0.9960 - val_loss: 1.1162 - val_acc: 0.7180
Epoch 28/30
100/100 [==============================] - 111s 1s/step - loss: 0.0259 - acc: 0.9950 - val_loss: 1.1019 - val_acc: 0.7230
Epoch 29/30
100/100 [==============================] - 111s 1s/step - loss: 0.0228 - acc: 0.9960 - val_loss: 1.1741 - val_acc: 0.7300
Epoch 30/30
100/100 [==============================] - 112s 1s/step - loss: 0.0190 - acc: 0.9950 - val_loss: 1.2328 - val_acc: 0.7260

훈련이 끝난 모델 저장하기

# 모델 저장하기 (훈련이 끝나면 항상 모델을 저장하자.)
model.save('cats_and_dogs_small_1.h5')

결과 그래프를 다시 그려보자. 

# 훈련의 정확도와 손실 그래프 그리기
import matplotlib.pyplot as plt

acc = history.history['acc']
val_acc = history.history['val_acc']
loss = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(1, len(acc) +1)

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
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('Trianing and validation loss')
plt.legend()

plt.show()

[Out]

- 이 그래프는 과대적합의 특성을 보여준다. Training acc는 시간이 지남에 따라 선형적으로 증가해서 거의 100%에 도달하는 반면, Validation acc는 70-72%에서 멈추었다. 

- Training loss 역시 계속 줄어들어 거의 0에 가깝게 줄어든 반면, Validation loss 역시 5번의 에포크만에 최솟값에 다다른 후, 더 이상 진전되지 않고 있다.

- 비교적 훈련 샘플의 수가 2,000개로 적은 편이기 때문에, 과대적합이 가장 중요한 문제이다.

- 이전에 배운 Drop out이나 가중치 감소(L2 규제)처럼 과대적합을 감소시킬 수 있는 여러가지 기법들을 배웠다.

- 여기에서는 컴퓨터비전에 특화되어 있어서, 딥러닝으로 이미지를 다룰 때 매우 일반적으로 사용되는 새로운 방법인 데이터 증식을 시도해보자.

 

5.2.5 데이터 증식 사용하기

  1. 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법
  2. 그럴듯한 이미지를 생성하도록 여러가지 랜덤한 변환을 적용하여 샘플을 늘린다.
  3. 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표
  4. 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 된다.

ImageDataGenerator가 읽은 이미지에 여러 종류의 랜덤 변환을 적용하도록 설정해보자.

# ImageDataGenerator을 이용하여 데이터 증식 설정하기
datagen = ImageDataGenerator(
    rotation_range=20,
    width_shift_range=0.1,
    height_shift_range=0.1,
    shear_range=0.1,
    zoom_range=0.1,
    horizontal_flip=True,
    fill_mode='nearest')
  1. 추가적인 매개변수에 대해 알아보자.
    1. rotation_range 랜덤하게 사진을 회전시킬 각도 범위(0-180도 사이)
    2. width_shift_rangeheight_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동시킬 범위이다. (전체 너비와 높이에 대한 비율)
    3. shear_range는 랜덤하게 전단 변환(sheering transformation)을 적용할 각도 범위
    4. zoom_range는 랜덤하게 사진을 확대할 범위
    5. horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집는다. 수평대칭을 가정할 수 있을 때 사용(ex.풍경/인물사진)
    6. fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야할 픽셀을 채울 전략
    7. 참고 https://tykimos.github.io/2017/06/10/CNN_Data_Augmentation/
# 랜덤하게 증식된 훈련 이미지 그리기
from keras.preprocessing import image # 이미지 전처리 유틸리티 모듈

fnames = sorted([os.path.join(train_cats_dir, fname) for # 특정 폴더의 특정 파일 리스트 가져오기
                 fname in os.listdir(train_cats_dir)]) # os의 listdr 모듈 이용
img_path = fnames[3] # 증식할 이미지 선택
img = image.load_img(img_path, target_size=(150,150))

x = image.img_to_array(img) # (150,150,3) 크기의 넘파이 배열로 변환
x = x.reshape((1,) + x.shape) # (1,150,150,3) 크기로 변환

i = 0
for batch in datagen.flow(x, batch_size=1):# 랜덤하게 변환된 이미지 배치를 생성한다.
    plt.figure(i)
    imgplot = plt.imshow(image.array_to_img(batch[0]))
    i +=1
    if i % 4 == 0:
        break # 무한 반복되기 때문에 어느 지점에서 중지해야 한다.
plt.show()

- os.listdir() : 특정 폴더의 특정 파일 리스트 읽어오기

- plt.imshow() : 이미지를 읽어오는 명령어

- datagen.flow(x_train, y_train, batch_size=1) : data와 label 배열을 가져온다. batch size만큼 data를 증가시킨다.

너무 귀엽다

- 데이터 증식을 사용하여 새로운 네트워크를 훈련시킬 때, 네트워크에 같은 입력데이터가 두 번 주입되지 않음

- 하지만 적은 수의 원본 이미지에서 만들어졌기 때문에, 여전히 입력 데이터들 사이에 상호연관성이 크다.

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

 

과대적합을 더 억제하기 위해 완전 연결 분류기 직전에 Drop out 층을 추가하자.(이전 절에서 과적합 방지 해결방안 중 하나, Dropout 추가하기)

# Dropout을 포함한 새로운 컨브넷 정의하기 (과적합 억제를 위해서)
model = model.Sequential()
model.add(layers.Conv2D(32,(3,3), activation='relu',
                        input_shape=(150,150,3)))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(64,(3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Conv2D(128,(3,3),activation='relu'))
model.add(layers.MaxPooling2D((2,2)))
model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(512, activation='relu'))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer = optimizers.RMSprop(lr=1e-4),
              metrics=['acc'])

데이터 증식과 드롭아웃을 사용하여 네트워크를 훈련시키자.

# 데이터 증식 제너레이터를 사용하여 컨브넷 훈련하기
train_datagen = ImageDataGenerator(
    rescale=1./255,
    rotation_range = 40,
    width_shift_range=0.2,
    height_shift_range=0.2,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1./255) # 검증 데이터는 증식되어서는 안된다.
train_generator = train_datagen.flow_from_directory(
    train_dir, # 타깃 디렉터리
    target_size=(150,150), # 모든 이미지를 150x150 크기로 바꿈
    batch_size=32,
    class_mode='binary') # binary_crossentropy 손실을 사용하기 때문에 이진 레이블을 만들어야 한다.

validation_generator = test_datagen.flow_from_directory(
    validation_dir,
    target_size=(150,150),
    batch_size=32,
    class_mode='binary')

history = model.fit_generator(
    train_generator,
    steps_per_epoch=100,
    epochs=100,
    validation_data = validation_generator,
    validation_steps=50)

- 검증 데이터는 증식되어서는 안된다는 점 주의

- binary_crossentropy 손실을 사용하기 때문에 train_datagen.flow_from_directory(class_mode='binary')로 이진레이블을 만들어주었다.

* 위 과정에서 에러날 경우

# 에러 해결
!pip uninstall Keras
!pip uninstall tensorflow

삭제 후 재설치 전에 런타임 재시작하기

!pip install keras==2.3.1
!pip install tensorflow==2.2.0

모델 저장하기

# 모델 저장하기
model.save('cats_and_dogs_small_2.h5')

그래프 다시 그려보기

[Out]

데이터 증식 사용 이후 과대적합이 개선되었다.

- 데이터 증식과 드롭아웃 덕분에 더이상 과대적합되지 않고 있다. (훈련 곡선이 검증 곡선에 가깝게 가고 있다.)

- 검증 데이터에서 82%의 정확도를 달성하였다. 규제하지 않은 모델과 비교했을 때 15%정도 향상되었다.

- 다른 규제기법을 더 사용하고, 네트워크의 파라미터를 튜닝하면 (ex. 합성곱층의 필터 수, 네트워크 층의 수 등..) 82%나 87%까지 높은 정확도를 얻을 수 있다.

- 하지만 데이터가 적기 때문에, 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성할 수 있으므로, 이런 상황에서 정확도를 높이기 위한 다음 단계는 사전 훈련된 모델을 사용하는 것이다.


 

Comments