일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- pmdarima
- 그로스 마케팅
- WITH ROLLUP
- 분석 패널
- DENSE_RANK()
- 인프런
- XGBoost
- python
- 그로스 해킹
- WITH CUBE
- tableau
- 그룹 연산
- 3기가 마지막이라니..!
- splitlines
- 캐글 신용카드 사기 검출
- 데이터 핸들링
- 컨브넷
- Growth hacking
- 스태킹 앙상블
- 로그 변환
- lightgbm
- 마케팅 보다는 취준 강연 같다(?)
- ARIMA
- 리프 중심 트리 분할
- 캐글 산탄데르 고객 만족 예측
- 데이터 증식
- 부트 스트래핑
- 데이터 정합성
- sql
- ImageDateGenerator
- Today
- Total
LITTLE BY LITTLE
[5] 케라스 창시자에게 배우는 딥러닝 - 5. 컴퓨터 비전을 위한 딥러닝( 합성곱 신경망 소개, MaxPooling, 고양이vs강아지 분류 예제, 데이터 증식) 본문
[5] 케라스 창시자에게 배우는 딥러닝 - 5. 컴퓨터 비전을 위한 딥러닝( 합성곱 신경망 소개, MaxPooling, 고양이vs강아지 분류 예제, 데이터 증식)
위나 2022. 8. 16. 16:22제 5부.컴퓨터 비전을 위한 딥러닝
- 합성곱 신경망 소개
- 소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기
- 사전 훈련된 컨브넷 사용하기
- 컨브넷 학습 시각화
- 요약
제 6부. 텍스트와 시퀀스를 위한 딥러닝
- 텍스트 데이터 다루기
- 순환 신경망 이해하기
- 순환 신경망의 고급 사용법
- 컨브넷을 사용한 시퀀스 처리
- 요약
제 7부. 딥러닝을 위한 고급 도구
- Sequential 모델을 넘어서 : 케라스의 함수형 API
- 케라스 콜백과 텐서보드를 사용한 딥러닝 모델 검사와 모니터링
- 모델의 성능을 최대화로 끌어올리기
- 요약
제 8부. 생성 모델을 위한 딥러닝
- LSTM으로 텍스트 생성하기
- 딥드림
- 뉴럴 스타일 트랜스퍼
- 변이형 오토인코더를 사용한 이미지 생성
- 적대적 생성 신경망 소개
- 요약
제 9부. 결론
- 핵심 개념 리뷰
- 딥러닝의 한계
- 딥러닝의 미래
- 빠른 변화에 뒤처지지 않기
- 맺음말
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
- 컨브넷이
- (image_height, image_width, image_channels) 크기의 입력 텐서를 사용하고,
- 배치차원은 포함하지 않는다는 점이 중요
- 이 예제에서는 MNIST 이미지 포맷인 (28,28,1) 크기의 입력을 처리하도록 컨브넷을 설정해야한다.
- 따라서 첫번째 층의 매개변수로 input_shape=(28,28,1)을 전달함
- model.summary()로 컨브넷의 구조를 살펴보면,
- Conv2D와 MaxPooling2D 층의 출력은 (height, width, channels) 크기의 3D텐서이다.
- 높이와 너비 차원(height,width)은 네트워크가 깊어질수록 작아지는 경향
- 채널의 수는 Conv2D 층에 전달된 첫 번째 매개변수에 의해 조절됨 (32개 또는 64개)
- 다음 단계에서 마지막 층의 ((3,3,64) 크기인) 출력 텐서를 완전 연결 네트워크에 주입해보자. 이 네트워크는 앞서 보았던 Dense 층을 쌓은 분류기이다.
- 이 분류기는 1D 벡터를 처리하는데, 이전 층의 출력이 3D 텐서이다.
- 따라서 먼저 3D 출력을 1D텐서로 펼쳐야한다.
- 그 다음에 몇 개의 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
_________________________________________________________________
- 10개의 클래스를 분류하기 위해서, 마지막 층의 출력크기를 10으로 하고, 소프트맥스 활성화 함수를 사용하였다. →model.add(layers.Dense(10, activation='soffmax'))
- (3, 3, 64) 출력이 (576,) 크기의 벡터로 펼쳐진 후, Dense 층으로 주입되었다.
이제 MNIST 숫자 이미지에 이 컨브넷을 훈련시키자. (2장의 MNIST 예제코드 재사용, https://noelee.tistory.com/59?category=1294039)
# 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%의 테스트 정확도를 얻었다.
완전 연결된 모델보다 간단한 컨브넷이 더 잘 작동하는 이유를 알아보기 위해 Conv2D와 MaxPooling2D층에 대해 더 살펴보자.
5-1-1. 합성곱 연산
- 완전 연결층과 합성곱 층 사이의 근본적인 차이는 다음과 같다.
- Dense 층은 입력 특성 공간에 있는 전역 패턴(ex. MNIST 숫자 이미지에서는 모든 픽셀에 걸친 패턴)을 학습하지만, 합성곱 층은 지역 패턴을 위 그림처럼 지역 패턴을 학습한다.
- 합성곱 층은 이미지일 경우 작은 2D 윈도우로 입력해 패턴을 찾는다. 앞의 예에서는 이 윈도우는 모두 3X3크기였다.
- 이 핵심 특징은 컨브넷의 2가지 성질 제공
- 학습된 패턴을 '평행 이동 불변성(translation invaraint)'을 가진다.
- 컨브넷이 이미지의 오른쪽 아래 모서리에서 어떤 패턴을 학습했다면, 다른 곳(ex. 왼쪽 위 모서리)에서도 이 패턴을 인식할 수 있다. (반면에 완전 연결 네트워크는 새로운 위치에 나타나면 새로운 패턴으로 학습해야함)
- 이러한 성질이 컨브넷이 이미지를 효율적으로 처리하게 만들어주고, 적은 수의 훈련 샘플을 사용해서 일반화 능력을 가진 표현을 학습할 수 있다.
- 컨브넷은 패턴의 공간적 계층 구조를 학습할 수 있다. 1번째 합성곱 층이 Edge와 같은 작은 지역 패턴을 학습한다. 두 번째 합성곱 층은 첫 번째 층의 특성으로 구성된 더 큰 패턴을 학습하는 식이다. → 매우 복잡하고 추상적인 시각적 개념 학습 가능
- 합성곱 연산은 특성 맵(feature map)이라 부르는 3D텐서에 적용된다. - 2개의 공간 축(높이와 너비)과 깊이 축(채널 축)으로 구성된다.
- RGB 이미지는 3개의 컬러 채널(빨,노,파)를 가지므로, 깊이 축의 차원이 3이 됨
- MNIST 숫자처럼 흑백 이미지의 경우, 깊이 축의 차원이 1(회색 톤)이다.
- 합성곱 연산은 입력 특성 맵에서 작은 패치들을 추출하고, 이런 모든 패치에 같은 변환을 적용하여 출력 특성 맵(output feature map)을 만든다.
2-4. 출력 특성 맵도 높이와 너비를 가진 3D텐서이다.
- 출력 텐서의 깊이는 층의 매개변수로 결정 (input_shape=(28,28,1))
- 깊이 축의 채널은 더 이상 RGB 입력처럼 특정 컬러를 의미하지 않는다. 그 대신, 일종의 필터를 의미함
- 필터는 입력 데이터의 어떤 특성을 인코딩한다. (ex. 고수준으로 보면 하나의 필터가 '입력에 얼굴이 있는지'를 인코딩할 수 있다. )
- MNIST 예제에서 1번째 합성곱 층은 (28,28,1)크기를 입력받아 (26,26,32)크기의 특성 맵 출력
→ 즉 입력에 대해 32개 필터 적용, 32개 출력 채널 각각은 26x26 크기의 배열 값 가짐 = '입력에 대한 필터의 응답 맵(response map)'이라 부르며, 입력의 각 위치에서 필터 패턴에 대한 응답을 나타냄, 아래 그림 참고
- 즉, 특성맵의 의미는 '깊이 축에 있는 각 차원은 하나의 특성(=필터)이고, 2D 텐서 output[:, :, n]은 입력에 대한 이 필터 응답을 나타내는 2D 공간상의 맵'이다.
- 합성곱은 핵심적인 2개의 파라미터로 정의된다.
- 입력으로부터 뽑아낼 패치의 크기 : 전형적으로 3x3,혹은 5x5 크기를 사용
- 특성 맵의 출력 깊이 : 합성 곱으로 계산할 필터의 수, 이 예시에서는 32로 시작해서 64로 끝났다.
- 작동 순서
- 케라스의 Con2D 층에서 이 파라미터는 Conv2D(output_depth, (window_height, window_width))처럼 1번째와 2번째 매개변수로 전달된다.
- 3D 입력 특성 맵 위를 3x3 또는 5x5 크기의 윈도우가 슬라이딩하면서 모든 위치에서 3D 특성 패치((window_height, window_width, input_depth) 크기)를 추출하는 방식으로 합성곱이 작동한다.
- 이런 3D 패치는 (output_depth, )크기의 1D 벡터로 변환된다. ( 합성곱 터널[convolution kernel]이라 불리는 하나의 학습된 가중치 행렬과의 텐서 곱셈을 통하여 변환됨 )
- 변환된 모든 벡터는 (height, width, ouptut_depth) 크기의 3D 특성 맵으로 재구성된다.출력 특성 맵의 공간상 위치는 입력 특성 맵의 같은 위치에 대응됨 (ex. 출력의 우측 아래 모서리는 입력의 우측 아래 부근에 해당하는 정보를 담고 있음)
- 3x3 윈도우를 사용하면 3D 패치 input[i-1,i+2, j-1:j+2, :]로부터 벡터 output[i, j, :]가 만들어진다. 아래그림 참고
- 출력 높이&너비가 입력 높이&너비와 다를 수 있다. 그이유는
- 경계 문제, 입력 특성 맵에 패딩을 추가하여 대응할 수 있다.
- 스트라이드의 사용여부에 따라 다르다.
입출력의 높이&너비가 다를 수 있는 첫번째 이유인 경계문제와 패딩에 대해 이해하기
- 5x5 크기의 특성 맵을 생각해보자.(25개의 타일이 있다고 가정)
- 3x3 크기인 윈도우의 중앙을 맞출 수 있는 타일은 3x3 격자를 형성하는 9개 뿐
- 따라서 출력 특성 맵은 3x3 크기가 된다. 크기가 조금 줄어들었다.
- 여기에서는 높이와 너비 차원을 따라 정확히 2개의 타일이 줄어들었다.
- 앞선 예에서도 이런 경계문제가 있었다. 1번째 합성곱에서 28x28크기인 입력이 26x26 크기가 됨
- 만약 입력과 동일한 높이와 너비를 가진 출력 특성 맵을 얻고 싶다면, 패딩을 사용할 수 있다.
- 패딩은 입력 특성 맵의 가장자리에 적절한 개수의 행과 열을 추가함
- 그래서 모든 입력 타일에 합성곱 윈도우의 중앙을 위치시킬 수 있다.
- 3x3 윈도우라면 위아래에 하나의 행을 추가하고, 오른쪽, 왼쪽에 하나의 열을 추가한다.
- 5x5 윈도우라면 2개의 행과 열을 추가한다.
- Conv2D 층에서 패딩은 padding 매개변수로 설정할 수 있다.
- 2개의 값이 가능하다.
- "valid"는 패딩을 사용하지 않겠다는 뜻이다.(윈도우를 놓을 수 있는 위치만 사용)
- "same"은 입력과 동일한 높이와 너비를 가진 출력을 만들기 위해 패딩한다라는 의미
- padding 매개변수의 기본 값은 valid
입출력의 높이&너비가 다를 수 있는 두번째 이유인 합성곱 스트라이드에 대해 이해하기
- 지금까지 합성곱에 대한 설명은 합성곱 윈도우의 중앙 타일이 연속적으로 지나간다고 가정하였다.
- '2번의 연속적인 윈도우 사이의 거리'가 스트라이드라고 불리는 합성곱의 파라미터이다.
- 스트라이드의 기본 값은 1이며, 1보다 큰 스트라이드 합성곱도 가능하다.
- 5x5 크기의 입력(패딩 없음)에 스트라이드2를 사용한 3x3 크기의 윈도우로 합성곱하여 추출한 패치가 다음과 같다.
- '스트라이드2를 사용했다'는 것은 '특성 맵의 너비와 높이가 2의 배수로 down sampling되었다'는 의미이다. (경계 문제가 있을시 더 줄어듦)
- 실전에서는 많이 쓰이지 않으나, 어떤 모델에서는 유용하게 사용될 수 있다.
- 특성 맵을 다운샘플링 하기 위해서 스트라이드 대신에 첫 번째 컨브넷 예제에서 max pooling(최대 풀링)연산을 사용하는 경우가 많다. 최대풀링에 대해 알아보자.
5.1.2 최대 풀링 연산
- 앞의 컨브넷 예제에서 특성 맵의 크기가 MaxPooling 2D층마다 절반으로 줄어들었다. (26x26 → 13x13)
- 스트라이드 합성곱과 비슷하게 강제적으로 특성맵을 down sampling하는 것이 max pooling의 역할
- 특성 맵에서 윈도우에 맞는 패치를 추출하고, 각 채널별로 최댓값을 출력한다.
- 합성곱과 개념적응로 비슷하지만, 추출한 패치에 학습된 선형 변환(합성곱 커널)을 적용하는 대신, 하드코딩된 최댓값 추출 연산을 사용
- 합성곱과의 가장 큰 차이점은 max pooling은 보통 2x2 윈도우와 스트라이드 2를 사용하여 특성 맵을 절반 크기로 down sampling한다는 점, 반면 합성곱은 전형적으로 3x3 윈도우와 스트라이드 1 사용
- 최대 풀링 층을 빼고 큰 특성맵을 계속 유지하지 않고 이런식으로 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
_________________________________________________________________
- 이 설정에서의 문제 두 가지 (최대 풀링 사용 X)
- 특성의 공간적 계층 구조 학습에 도움이 되지 않음
- 3번째 층의 3x3 윈도우는 초기 입력의 7x7 윈도우 영역에 대한 정보만 담고 있다.
- 컨브넷에 의해 학습된 고수준 패턴은 초기 입력에 관한 정보가 아주 적어 숫자 분류를 학습하기에 충분치 않음
- 마지막 합성곱 층의 특성이 전체 입력에 대한 정보를 가지고 있어야함
- 최종 특성 맵은 22 x 22 x 64 = 30,976개의 원소를 가진다. 이 컨브넷을 펼친 후 512 크기의 Dense 층과 연결하면 약 15.8백만 개의 가중치 파라미터가 생긴다. 작은 모델치고는 너무 많은 가중치고, 심각한 과대적합이 발생할 것
- 특성의 공간적 계층 구조 학습에 도움이 되지 않음
- 간단히 말해 down sampling을 사용하는 이유는 처리할 특성 맵의 가중치 개수를 줄이기 위해서
- 또한 연속적인 합성곱 층이 원본 입력에서 커버되는 영역 측면에서, 점점 커진 윈도우를 통해 바라보도록 만들어 필터의 공간적 계층 구조 구성
- max pooling, 스트라이드 이외에도 최댓값 대신 채널별 평균값을 계산하여 변환하는 평균 풀링을 사용할 수도 있다.
- 하지만 특성이 특성맵의 각 타일에서 어떤 패턴이나 개념의 존재 여부를 인코딩하는 경향이 있기 때문에, 특성의 평균값보다 여러 특성 중 최댓값을 사용하는 것이 더 유용
- 스트라이드가 없는 합성곱인 sub sampling 전략으로 조밀한 특성 맵을 만들고, 그 다음 작은 패치에 대해서 최대로 활성화된 특성을 고르는 것 (입력에 대해 스트라이드 합성곱으로 듬성듬성 윈도우를 슬라이딩하거나, 입력 패치를 평균해서 특성 정보를 놓치거나 희석시키는 것보다 낫다.)
5.2 소규모 데이터셋에서 밑바닥부터 컨브넷 훈련하기
- 매우 적은 데이터로 이미지 분류 모델을 훈련하는 일은 흔한 경우이다. 적은 샘플이란 수백 개에서 수만 개 사이
- 2,000개의 고양이 사진과 2,000개의 강아지 사진으로 구성된 데이터셋에서 강아지 고양이 이미지를 분류해보자.
- 보유한 소규모 데이터셋을 사용하여 처음부터 새로운 모델을 훈련해보자.
- 2,000개의 훈련 샘플에서 작은 컨브넷을 어떤 규제 방법도 사용하지 않고 훈련하여 기준이 되는 기본 성능을 만듦
- 71%의 분류 정확도를 달성할 것, 이 방법의 주요 이슈는 과대적합
- 그 다음 컴퓨터 비전에서 과대적합을 줄이기 위한 강력한 방법인 '데이터 증식'을 소개할 예정 (82%의 정확도)
- 그 다음 절에서 작은 데이터셋에 딥러닝을 적용하기 위한 핵심적인 기술 두가지를 살펴볼 것
- 사전 훈련된 네트워크로 추출 특성 (90%의 정확도)
- 사전 훈련된 네트워크를 세밀하게 튜닝(92%의 정확도)
- 위와 같은 세가지 전략 (1)처음부터 작은 모델 훈련/ 2)사전 훈련된 모델 사용하여 특성 추출/ 3)사전 훈련된 모델 세밀하게 튜닝) 은 작은 데이트셋에서 이미지 분류 수행시 도구상자에 포함되어 있어야한다.
5.2.1 작은 데이터셋 문제에서 딥러닝의 타당성
- 딥러닝은 데이터가 풍부할 때만 작동한다는 말이 있다. 딥러닝의 근본적 특징은 훈련데이터의 특성 공학의 수작업 없이 흥미로운 특성을 찾을 수 있는 것인데, 이는 훈련샘플이 많아야지만 가능하기 때문이다. (입력샘플이 이미지인 매우 고차원적인 문제에서 특히 그러하다.)
- 많은 샘플이 의미하는건 절대적이지 않다. 훈련하려는 네트워크의 크기와 깊이에 상대적이기 떄문
- 복잡한 문제를 푸는 컨브넷은 수십개의 샘플만 사용해 훈련하는 것은 불가능, 하지만 모델이 작고 규제가 잘 되어 있으면 간단한 작업이라면 수백 개의 샘플로도 충분할 수 있다.
- 컨브넷은 지역적이고 평행이동으로 변하지 않는 특성을 학습하기 때문에, 지각에 관한 문제에서 매우 효율적으로 데이터를 사용함
- 매우 작은 이미지 데이터셋에서 어떤 종류의 특성 공학을 사용하지 않고, 컨브넷을 처음부터 훈련해도 납득할만한 결과를 만들 수 있음
- 딥러닝 모델은 태생적으로 매우 다목적이다. 대규모 데이터셋에서 훈련시킨 이미지 분류 모델이나 speech-to-text 모델을 조금만 변경해서 완전히 다른 문제에 사용 가능
- 특히 컴퓨터 비전에서는 사전 훈련된 모델들이 내려받을 수 있도록 많이 공개되어 있어 매우 적은데이터에서 강력한 비전 모델을 만드는데 사용 가능
5.2.2 데이터 내려받기
- 이 데이터셋은 2만 5,000개의 강아지와 고양이 이미지 (클래스마다 1만 2,500개)를 담고 있다.
- 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 네트워크 구성하기
- 이전에 만든 간단한 컨브넷과 구조는 일반적으로 동일하다.
- Conv2D (relu 활성화 함수 사용)와 MaxPooling2D 층을 번갈아 쌓은 컨브넷을 만들자.
- 이전보다 이미지가 크고 복잡한 문제이기 때문에, 네트워크를 더 크게 만들어야한다. (Conv2D+MaxPooling2D 단계를 하나 더 추가)
- → 이렇게 하면 네트워크의 용량을 늘리고, 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 데이터 전처리
- 데이터는 네트워크에 주입되기 전에 부동 소수 타입의 텐서로 적절하게 전처리되어 있어야 한다.
- 지금은 데이터가 JPEG 파일로 되어있으므로 네트워크에 주입하기 위해 전처리 해주기
- 사진 파일을 읽는다.
- JPEG 콘텐츠를 RGB 픽셀 값으로 디코딩한다.
- 그 다음 부동 소수 타입의 텐서로 변환한다.
- 픽셀 값(0에서 255 사이)의 스케일을 [0,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,)
- 위 출력은 150x150 RGB 이미지의 배치(20, 150, 150, 3) 크기)와 이진 레이블의 배치((20,) 크기)이다.
- 각 배치에는 20개의 샘플(배치 크기)이 있다. 제너레이터는 이 배치를 무한정 만들어 내고, 타깃 폴더에 있는 이미지를 끊임없이 반복하기 때문에, 반복 루프 안 어디에선가 break 문을 사용해야 한다.
- 이제 제너레이터를 사용한 데이터에 모델을 훈련시켜보자.
- fit_generator 메소드는 fit 메소드와 동일하되, 데이터 제너레이터를 사용할 수 있다.
- 이 메소드는 1번째 매개변수로 입력과 타깃의 배치를 끝없이 반환하는 파이썬 제너레이터를 받고,
- 데이터가 끝없이 생성되기 때문에, 케라스 모델에 하나의 에포크를 정의하기 위해 제너레이터로부터 얼마나 많은 샘플을 뽑을 것인지 알려주어야 한다. steps_per_epoch 매개변수에서 이를 설정한다.
- 제너레이터로부터 steps_per_epoch개의 배치만큼 뽑는다.(=steps_per_epoch 횟수만큼 경사 하강법 단계를 실행한 후에 훈련 프로세스는 다음 에포크로 넘어간다.)
- 여기서는 20개의 샘플이 하나의 배치이므로 2,000개의 샘플을 모두 처리할 때까지 100개의 배치를 뽑는다.
- fit_generator을 사용할 때 fit메소드와 마찬가지로 validation_data 매개변수를 전달할 수 있다. (데이터 제너레이터도 가능하고, 넘파이 배열의 튜플도 가능)
- validation_data로 제너레이터를 전달하면, 검증 데이터의 배치를 끝없이 반환한다.
- 따라서 검증 데이터 제너레이터에서 얼마나 많은 배치를 추출하여 평가할지 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 데이터 증식 사용하기
- 데이터 증식은 기존 훈련 샘플로부터 더 많은 훈련 데이터를 생성하는 방법
- 그럴듯한 이미지를 생성하도록 여러가지 랜덤한 변환을 적용하여 샘플을 늘린다.
- 훈련할 때 모델이 정확히 같은 데이터를 두 번 만나지 않도록 하는 것이 목표
- 모델이 데이터의 여러 측면을 학습하면 일반화에 도움이 된다.
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')
- 추가적인 매개변수에 대해 알아보자.
- rotation_range 랜덤하게 사진을 회전시킬 각도 범위(0-180도 사이)
- width_shift_range와 height_shift_range는 사진을 수평과 수직으로 랜덤하게 평행 이동시킬 범위이다. (전체 너비와 높이에 대한 비율)
- shear_range는 랜덤하게 전단 변환(sheering transformation)을 적용할 각도 범위
- zoom_range는 랜덤하게 사진을 확대할 범위
- horizontal_flip은 랜덤하게 이미지를 수평으로 뒤집는다. 수평대칭을 가정할 수 있을 때 사용(ex.풍경/인물사진)
- fill_mode는 회전이나 가로/세로 이동으로 인해 새롭게 생성해야할 픽셀을 채울 전략
- 참고 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%까지 높은 정확도를 얻을 수 있다.
- 하지만 데이터가 적기 때문에, 컨브넷을 처음부터 훈련해서 더 높은 정확도를 달성할 수 있으므로, 이런 상황에서 정확도를 높이기 위한 다음 단계는 사전 훈련된 모델을 사용하는 것이다.
'데이터 분석 > 케라스 창시자에게 배우는 딥러닝' 카테고리의 다른 글
[5] 케라스 창시자에게 배우는 딥러닝 - 5. 컴퓨터 비전을 위한 딥러닝( 사전 훈련된 컨브넷 사용하기 : 특성추출(데이터 증식사용有 ,無)과 미세조정, 컨브넷 학습 시각화) (1) | 2022.09.05 |
---|---|
[4] 케라스 창시자에게 배우는 딥러닝 - 4. (머신러닝) 분류, 특성 공학, 특성 학습, 과/소적합, 해결법(가중치 규제,드롭아웃 추가..) (0) | 2022.08.11 |
[3,실습] 케라스 창시자에게 배우는 딥러닝 - 신경망 시작하기 (분류/회귀 문제 풀어보기 (0) | 2022.08.09 |
[3] 케라스 창시자에게 배우는 딥러닝 - 신경망 시작하기(분류/예측 예제 풀어보기) (0) | 2022.08.08 |
[2] 케라스 창시자에게 배우는 딥러닝 - 신경망의 수학적 구성 요소 (0) | 2022.08.08 |