Search
Duplicate

머신 러닝 교과서/ 심층 합성곱 신경망으로 이미지 분류

합성곱 신경망의 구성 요소

합성곱 신경망 또는 CNN은 뇌의 시각 피질이 물체를 인식할 때 동작하는 방식에서 영감을 얻은 모델이다.
CNN 개발은 1990년대로 거슬러 올라간다. 이 시기에 얀 르쿤(Yann LeCun)과 그의 동료들은 손글씨 숫자를 분류하는 새로운 신경망 구조를 발표했다.
이미지 분류 작업에서 CNN이 탁월한 성능을 내기 때문에 크게 주목을 받았다. 이로 인해 머신 러닝과 컴퓨터 비전 애플리케이션에서 엄청난 발전을 이루었다.

CNN과 특성 계층 학습

관련이 높은 핵심 특징을 올바르게 추출하는 것은 모든 머신 러닝 알고리즘의 성능에서 아주 중요한 요소이다.
전통적인 머신 러닝 모델은 도메인 전문가가 만든 특성에 의존하거나 컴퓨터를 사용한 특성 추출 기법에 바탕을 두고 있다.
신경망은 원본 데이터에서 작업에 가장 유용한 특성을 자동으로 학습한다. 이런 이유 때문에 신경망을 특성 추출 엔진으로 생각하기도 한다. 예컨대 입력에 가까운 층은 저수준 특성을 추출한다.
다층 신경망과 특히 심층 합성곱 신경망은 각 층별로 저수준 특성을 연결하여 고수준 특성을 만듦으로써 소위 특성 계층을 구성한다.
예컨대 이미지를 다룬다면 에지(edge)나 동그라미 같은 저수준 특성이 앞쪽 층에서 추출된다.
이런 특성들이 연결되어 건물, 자동차, 강아지 같은 고수준 특성을 형성한다.
아래 그림에서 보듯이 CNN은 입력 이미지에서 특성 맵(feature map)을 만든다. 이 맵의 각 원소는 입력 이미지의 국부적인 픽셀 패치에서 유도된다.
이런 국부적인 픽셀 패치를 국부 수용장(local receptive field)라고 한다. CNN은 일반적으로 이미지 관련 작업을 매우 잘 수행한다. 이는 다음 두 개의 중요한 아이디어 때문이다.
희소 연결: 특성 맵에 있는 하나의 원소는 작은 픽셀 패치 하나에만 연결된다 (퍼셉트론 처럼 모든 입력 이미지에 있는 연결되는 것과 매우 다르다)
파라미터 공유: 동일한 가중치가 입력 이미지의 모든 패치에 사용된다.
이 두 아이디어 결과로 네트워크의 가중치(파라미터) 개수가 극적으로 감소하고 중요 특징을 잡아내는 능력이 향상된다. 당연히 가까이 있는 픽셀들이 멀리 있는 픽셀보다 연관성이 높다.
일반적으로 CNN은 여러 개의 합성곱(conv) 층과 풀링(Pooling)이라고도 하는 서브샘플링(subsampling) 층으로 이루어져 있다. 마지막에는 하나 이상의 완전 연결(FC) 층이 따라온다.
완전 연결 층은 모든 입력 유닛 ii가 모든 출력 유닛 jj에 가중치 wijw_{ij}로 연결되어 있는 다층 퍼셉트론이다.
풀링 층으로 알려진 서브샘플링 층은 학습되는 파라미터가 없다. 즉, 풀링 층에는 가중치나 절편 유닛이 없다. 합성곱이나 완전 연결 층은 가중치와 절편을 가진다.

이산 합성곱 수행

이산 합성공(discrete convolution)(또는 간단히 합성곱)이 CNN의 기본 연산이다. 이 연산의 작동 원리를 아는 것이 아주 중요하다.
여기서는 합성곱의 수학적 정의를 살펴보고 1차원 벡터나 2차원 행렬에서 합성곱을 계산하는 간단한 알고리즘을 설명하겠다.
여기서는 합성곱 연산의 작동 원리를 이해하는 것이 목적이다. 텐서플로 패키지의 실제 합성곱 연산은 훨씬 효율적으로 구현되어 있다.

1차원 이산 합성곱 연산 수행

앞으로 사용할 기본적인 정의와 기호를 설명하는 것부터 시작하겠다.
두 개의 1차원 벡터 xx와 ww에 대한 이산 합성곱은 y=xwy = x * w로 나타낸다.
xx는 입력이고 (이따금 신호라고 부름) ww는 필터(filter) 또는 커널(kernel)이라 부른다.
이산 합성곱의 수학적 정의는 아래와 같다.
y=xwy[i]=k=+x[ik]w[k]y = x * w \to y[i] = \sum_{k=-\infty}^{+\infty} x[i-k] w[k]
여기서 대괄호는 벡터 원소의 인덱스를 나타내는데 사용한다. 인덱스 ii는 출력 벡터 yy의 각 원소에 대응한다.
위 공식에서 특이한 점이라면 -\infty에서 ++\infty까지의 인덱스와 xx의 음수 인덱싱이다.
인덱스 -\infty에서 ++\infty까지의 합은 특히 이상하게 보인다. 머신 러닝 애플리케이션은 항상 유한한 특성 벡터를 다루기 때문이다.
예컨대 xx가 0,1,2,...,8,90, 1, 2, ..., 8, 9 인덱스로 열 개의 특성을 가지고 있다면 :1\infty:-1과 10:+10:+\infty인덱스는 xx의 범위 밖이다.
이전 공식에 있는 덧셈을 올바르게 계산하려면 xx와 ww가 0으로 채워져 있다고 가정해야 한다. 또 출력 벡터 yy도 0으로 채워진 무한 크기가 된다.
이는 실제 상황에서는 유용하지 않기 때문에 유한한 개수의 0으로 xx가 패딩된다.
이 과정을 제로 패딩(zero padding) 또는 패딩(padding)이라고 한다. 각 방향으로 추가된 패딩 수는 pp로 나타난다.
1차원 벡터 xx의 패딩 예가 아래 그림에 나타나 있다.
원본 입력 xx와 필터 ww가 각각 nn개 mm개의 원소를 가지고 mnm \leq n이라고 가정해 보자.
패딩된 벡터 xpx^{p}의 크기는 n+2pn + 2p이다.
이산 합성곱을 계산하기 위한 실제 공식은 다음과 같이 바뀐다.
y=xwy[i]=k=0m1xp[i+mk]w[k]y = x * w \to y[i] = \sum_{k=0}^{m-1} x^{p}[i+m-k] w[k]
무한한 인덱스 문제를 해결했다. 둘째 이슈는 i+mki + m - k로 xx를 인덱싱하는 것이다.
xx와 ww가 이 식에서 다른 방향으로 인덱싱한다는 점이 중요하다.
이 때문에 패딩된 후에 xx 또는 ww 벡터 중 하나를 뒤집어 간단히 점곱으로 계산할 수 있다.
필터 ww를 뒤집어서 회전된 필터 wrw^{r}을 얻었다고 가정해 보자.
점곱 x[i:i+m]wrx[i:i+m] \cdot w^{r}을 계산하면 y[i]y[i] 원소 하나가 얻어진다. x[i:i+m]x[i:i+m]은 크기가 mm인 xx의 패치이다.
이 연산이 모든 출력 원소를 얻기 위해 슬라이딩 윈도우(sliding window) 방식으로 반복된다.
아래 그림은 x=(3,2,1,7,1,2,5,4)x = (3, 2, 1, 7, 1, 2, 5, 4)이고 w=(12,34,1,14)w = ({1 \over 2}, {3 \over 4}, 1, {1 \over 4})일 때 처음 세 개의 출력 원소를 계산하는 경우를 보여준다.
이 예에서 패딩 크기는 0이다. (p=0)(p = 0)
회전된 필터 wrw^{r}은 2칸씩 이동한다. 이동하는 양은 스트라이드(stride)라고 하며, 또 하나의 합성곱 하이퍼파라미터이다. 여기서 스트라이드는 2이다. (s=2)(s = 2)
스트라이드는 입력 벡터의 크기보다 작은 양수 값이어야 한다.

합성곱에서 제로 패딩의 효과

지금까지 유한한 크기의 출력 벡터를 얻기 위해 합성곱에 제로 패딩을 사용했다.
기술적으로 p0p \geq 0인 어떤 패딩도 적용할 수 있다. pp값에 따라 xx에서 경계에 있는 셀은 중간 셀과 다르게 처리된다.
n=5,m=3,p=0n=5, m=3, p=0인 경우를 생각해 보자. x[0]x[0]은 하나의 출력 원소를 계산하는데만 사용된다. (예컨대 y[0]y[0])
반면 x[1]x[1]은 두 개의 출력 원소를 계산하는데 사용된다. (y[0],y[1])(y[0], y[1])
xx 원소를 이렇게 다르게 취급하기 때문에 가운데 있는  x[2]x[2]가 대부분의 계산에 사용되어 강조되는 효과를 낸다.
여기서는 p=2p = 2를 사용하면 이 문제를 피할 수 있다.
xx의 각 원소가 세 개의 yy 원소 계산에 참여한다.
또 출력 yy 크기는 사용한 패딩 방법에 따라 달라진다. 실전에서 자주 사용하는 세 개의 패딩 방법은 풀(full) 패딩, 세임(same) 패딩, 밸리드(valid) 패딩이다.
풀 패딩은 패딩 파라미터 pp를 p=m1p = m-1로 설정한다. 풀 패딩은 출력 크기를 증가시키기 때문에 합성곱 신경망 구조에서는 거의 사용되지 않는다.
세임 패딩은 출력 크기가 입력 벡터 xx와 같아야 할 때 사용한다. 이때 패딩 파라미터 pp는 입력과 출력 크기가 동일해야 하기 때문에 필터 크기에 따라 결정된다.
마지막으로 밸리드 패딩 합성곱은 p=0p = 0 인 경우를 말한다. (패딩 없음)
아래 그림은 세 개의 패딩 모드를 보여준다. 입력은 5×5 픽셀, 커널은 3×3 크기, 스트라이드는 1인 경우이다.
합성곱 신경망에서 가장 많이 사용되는 패딩 방법은 세임 패딩이다. 다른 패딩 방식에 비해 장점은 세임 패딩이 입력 이미지나 텐서의 높이와 너비를 유지시킨다는 것이다. 이 때문에 네트워크 구조를 설계하기 쉽다.
풀 패딩이나 세임 패딩에 비해 밸리드 패딩의 단점은 신경망에 층이 추가될수록 점진적으로 텐서 크기가 줄어든다는 것이다. 이는 신경망 성능을 나쁘게 만들 수 있다.
실전에서는 세임 패딩으로 너비와 높이를 유지시키고 풀링에서 크기를 감소시킨다. 풀 패딩은 입력보다 출력 크기를 증가시키므로 경계 부분의 영향을 최소화하는 것이 중요한 신호 처리 애플리케이션에서 보통 사용된다.
딥러닝에서는 경계 부분의 영향이 크지 않기 때문에 풀 패딩이 거의 사용되지 않는다.

합성곱 출력 크기 계산

합성곱 출력 크기는 입력 벡터 위를 필터 ww가 이동하는 전체 횟수로 결정된다.
입력 벡터의 크기는 nn이고 필터의 크기는 mm, 패딩이 pp, 스트라이드가 ss인 xwx * w 출력 크기는 다음과 같이 계산된다.
o=n+2pms+1o = \lfloor {n + 2p - m \over s} \rfloor + 1
여기서  \lfloor \cdot \rfloor는 버림 연산을 나타낸다.
입력 벡터 크기가 10이고 합성곱 커널 크기가 5, 패딩이 2, 스트라이드가 1일 때 출력 크기는 다음과 같다.
n=10,m=5,p=2,s=1o=10+2×251+1=10n=10, m=5, p=2, s=1 \to o = \lfloor {10 + 2 \times 2 - 5 \over 1} \rfloor + 1= 10
커널 크기가 3이고 스트라이드가 2이면 같은 입력 벡터일 때 출력 크기는 다음과 같다.
n=10,m=3,p=2,s=2o=10+2×232+1=6n=10, m=3, p=2, s=2 \to o = \lfloor {10 + 2 \times 2 - 3 \over 2} \rfloor + 1 = 6
1차원 합성곱의 계산 방법을 익히기 위해 단순하게 구현해 보고 이 결과를 numpy.convolve 함수와 비교해 보자.
import numpy as np def conv1d(x, w, p=0, s=1): w_rot = np.array(w[::-1]) x_padded = np.array(x) if p > 0: zero_pad = np.zeros(shape=p) x_padded = np.concatenate([zero_pad, x_padded, zero_pad]) res = [] for i in range(0, int(len(x)/s), s): res.append(np.sum(x_padded[i:i+w_rot.shape[0]] * w_rot)) return np.array(res) x = [1, 3, 2, 4, 5, 6, 1, 3] w = [1, 0, 3, 1, 2] print('Conv1d 구현:', conv1d(x, w, p=2, s=1)) ### 결과 # Conv1d 구현: [ 5. 14. 16. 26. 24. 34. 19. 22.] print('넘파이 결과:', np.convolve(x, w, mode='same')) ### 결과 # 넘파이 결과: [ 5 14 16 26 24 34 19 22]
Python

2D 이산 합성곱 수행

앞서 배운 개념은 2차원으로 쉽게 확장 가능하다. m1n1m_{1} \leq n_{1} 이고 m2n2m_{2} \leq n_{2}인 행렬 Xn1×n2X_{n_{1} \times n_{2}}와 필터 행렬 Wm1×m2W_{m_{1} \times m_{2}} 같은 2차원 입력을 다룰 때 XX와 WW의 2D 합성곱 결과는 행렬 Y=XWY = X * W가 된다.
Y=XWY[i,j]=k1=+k2=+X[ik1,jk2]W[k1,k2]Y = X * W \to Y[i, j] = \sum_{k_{1} = -\infty}^{+\infty} \sum_{k_{2} = -\infty}^{+\infty} X[i-k_{1}, j-k_{2}] W[k_{1}, k_{2}]
차원 하나를 제거하면 남은 공식이 이전의 1D 합성곱과 동일하다.
사실 제로 패딩, 필터 행렬의 회전, 스트라이드 같은 이전에 언급한 모든 기법도 2D 합성곱에 적용할 수 있다. 양쪽 차원에 독립적으로 확장된다.
다음 예는 패딩 p=(1,1)p = (1, 1)과 스트라이드 s=(2,2)s = (2, 2)일 때 입력 행렬 X3×3X_{3 \times 3}과 커널 행렬 W3×3W_{3 \times 3} 사이의 2D 합성곱 계산을 보여준다.
여기서는 입력 행렬의 네 면에 0이 한줄씩 추가되어 X5×5paddedX_{5 \times 5}^{padded} 행렬을 만든다.
필터를 뒤집으면 다음과 같다.
Wr=[0.510.50.10.40.30.40.70.5]W^{r} = \left[ \begin{array}{rrr} 0.5 & 1 & 0.5 \\ 0.1 & 0.4 & 0.3 \\ 0.4 & 0.7 & 0.5 \end{array} \right]
이 변환은 전치 행렬과 다르다. 넘파이에서 필터를 역전시키려면 W_rot = W[::-1, ::-1] 처럼 쓴다. 그 다음 패딩된 입력 행렬 XpaddedX^{padded}를 따라 슬라이딩 윈도우처럼 역전된 필터를 이동하면서 원소별 곱의 합을 계산한다.
아래 그림에 \odot 연산자로 표기했다.
결과값 YY는 2×2 행렬이다.
단순한 알고리즘을 사용하여 2D 합성곱도 구현해 보겠다. scipy.signal 패키지는 2D 합성곱을 계산할 수 있는 scipy.signal.convolve2d 함수를 제공한다.
import numpy as np import scipy.signal def conv2d(X, W, p=(0, 0), s=(1, 1)): W_rot = np.array(W)[::-1, ::-1] X_orig = np.array(X) n1 = X_orig.shape[0] + 2*p[0] n2 = X_orig.shape[1] + 2*p[1] X_padded = np.zeros(shape=(n1, n2)) X_padded[p[0]:p[0]+X_orig.shape[0], p[1]:p[1]+X_orig.shape[1]] = X_orig res = [] for i in range(0, int((X_padded.shape[0] - W_rot.shape[0])/s[0])+1, s[0]): res.append([]) for j in range(0, int((X_padded.shape[1] - W_rot.shape[1])/s[1])+1, s[1]): X_sub = X_padded[i:i+W_rot.shape[0], j:j+W_rot.shape[1]] res[-1].append(np.sum(X_sub * W_rot)) return (np.array(res)) X = [[1, 3, 2, 4], [5, 6, 1, 3], [1, 2, 0, 2], [3, 4, 3, 2]] W = [[1, 0, 3], [1, 2, 1], [0, 1, 1]] print('Conv2d 구현:\n', conv2d(X, W, p=(1, 1), s=(1,1))) ### 결과 # Conv2d 구현: # [[11. 25. 32. 13.] # [19. 25. 24. 13.] # [13. 28. 25. 17.] # [11. 17. 14. 9.]] print('사이파이 결과:\n', scipy.signal.convolve2d(X, W, mode='same')) ### 결과 # 사이파이 결과: # [[11 25 32 13] # [19 25 24 13] # [13 28 25 17] # [11 17 14 9]]
Python

서브샘플링

서브샘플링은 전형적인 두 종류의 풀링 연산으로 합성곱 신경망에 적용된다. 최대 풀링(max-pooing)과 평균 풀링(mean-pooling 또는 average-pooling)이다.
풀링 층은 보통 Pn1×n2P_{n_{1} \times n_{2}}로 표시한다.
아래 첨자는 최댓값과 평균 연산이 수행되는 이웃한 픽셀 크기이다. (차원별로 인접 픽셀 개수)
이런 이웃 픽셀 개수를 풀링 크기라고 한다.
아래 그림에 이 연산을 나타냈다. 최대 풀링은 이웃한 픽셀에서 최댓값을 취하고 평균 풀링은 픽셀의 평균을 계산한다.
풀링의 장점은 두가지 이다.
풀링(최대 풀링)은 일종의 지역 불변경을 만든다. 국부적인 작은 변화가 최대 풀링의 결과를 바꾸지 못한다는 의미이다. 결국 입력 데이터에 있는 잡음에 좀 더 안정적인 특성을 생성한다. 아래에서 보듯 두 개의 다른 입력 행렬 X1X_{1}과 X2X_{2}가 같은 결과를 만든다.
풀링은 특성 크기를 줄이므로 계산 효율성을 높인다. 또 특성 개수가 줄어들면 과대적합도 감소된다.

기본 구성 요소를 사용하여 심층 합성곱 신경망 구성

지금까지 합성곱 신경망의 기본 구성 요소를 배웠다. 이 장에서 설명한 개념들은 전통적인 다층 신경망보다 아주 어렵지 않다. 일반적인 신경망에서 가장 중요한 연산은 행렬-벡터 곱셈이다.
예컨대 행렬-벡터 곱셈을 사용하여 활성화 함수의 입력(또는 최종 입력) a=Wx+ba = Wx + b 을 계산한다.
여기서 xx는 픽셀을 나타내는 열 벡터고, WW는 입력 픽셀과 각 은닉 유닛을 연결하는 가중치 행렬이다.
합성곱 신경망에서 이 연산은 합성곱 연산 A=WX+bA = W * X + b로 바뀐다. XX는 높이 x 너비의 픽셀을 나타내는 행렬이다.
두 경우 모두 은닉 유닛의 활성화 출력 H=ϕ(A)H = \phi (A) 를 얻기 위해 활성화 함수에 입력으로 전달된다. 여기서 ϕ\phi는 활성화 함수이다.
이전 절에서 설명한 것처럼 풀링으로 표현되는 서브샘플링도 합성곱 신경망의 구성 요소 중 하나이다.

여러 개의 입력 또는 컬러 채널 다루기

합성곱 층의 입력 샘플에는 N1×N2N_{1} \times N_{2} 차원 (예컨대 이미지의 높이와 너비 픽셀)인 하나 이상의 2D 배열 또는 행렬이 포함될 수 있다.
이런 N1×N2N_{1} \times N_{2} 행렬을 채널(channel)이라고 한다.
여러 개의 채널을 합성곱 층 입력에 사용하기 때문에 랭크 3 텐서 또는 3차원 배열 XN1×N2×CinX_{N_{1} \times N_{2} \times C_{in}}을 사용해야 한다.
여기서 CinC_{in}이 입력 채널 크기이다.
예컨대 CNN의 첫 번째 층에 입력되는 이미지를 생각해 보자. RGB 모드의 컬러 이미지라면 Cin=3C_{in} = 3 이다. (RGB의 빨간색, 초록색, 파란색 채널)
이미지가 그레이스케일(grayscale)이라면 흑백의 픽셀 강도를 가진 하나의 채널만 있으므로 Cin=1C_{in} = 1 이다.
합성곱 연산에서 여러 개의 입력 채널을 어떻게 다룰 수 있을까?
해답은 간단하다. 각 채널별로 합성곱 연산을 수행하고 행렬 덧셈으로 결과를 합친다.
채널 (c)별 합성곱은 개별적인 커널 행렬 W[:,:,c]W[:,:,c]를 사용한다.
활성화 함수에 입력되는 결과값은 다음 공식으로 계산된다.
최종 결과 hh를 특성 맵이라고 한다.
보통 CNN의 합성곱 층은 하나 이상의 특성 맵을 만든다.
여러 개의 특성 맵을 사용하면 커널 텐서는 width×height×Cin×Coutwidth \times height \times C_{in} \times C_{out} 으로 4차원이 된다.
너비와 높이는 커널의 크기고 CinC_{in}은 입력 채널의 개수, CoutC_{out}은 출력 특성 맵의 개수이다.
이전 공식에 출력 특성 맵의 개수를 포함시키면 아래와 같다.
아래 그림에 나온 합성곱 층과 풀링 층이 포함된 예제를 통해 신경망의 합성곱 계산을 정리하겠다.
이 예는 입력 채널이 3개이다. 커널 텐서는 4차원이다. 각 커널 행렬은 m1×m2m_{1} \times m_{2} 크기고 입력 채널에 한 개씩 세 개 이다.
이런 텐서가 다섯 개의 출력 특성 맵을 만들기 위해 다섯 개가 있다.
마지막으로 특성 맵을 서브샘플링하기 위해 풀링 층이 있다.
전체 구조는 아래 그림과 같다.

드롭아웃으로 신경망 규제

일반적인 (완전 연결) 신경망 또는 CNN 중 어떤 것을 사용하든지 네트워크 크기를 결정하는 것은 항상 어려운 문제이다. 어느 정도 좋은 성능을 얻으려면 가중치 행렬 크기와 층 개수를 튜닝해야 한다.
파라미터 개수가 비교적 적은 네트워크는 용량이 작기 때문에 과소적합되기 쉽다. 이는 복잡한 데이터셋에 내재된 구조를 학습할 수 없기 때문에 성능이 나빠진다.
반면 아주 큰 네트워크는 과대적합될 가능성이 많다. 이런 네트워크가 훈련 데이터를 외워 버리면 훈련 세트에서는 잘 작동하지만 테스트 데이터에서는 나쁜 성능을 낼 것이다.
실제 머신 러닝 문제를 다룰 때는 얼마나 네트워크가 커야 하는지 사전에 알 수 없다.
이 문제를 해결하기 위한 한 가지 방법은 다음과 같다.
먼저 훈련 세트에서 잘 동작하도록 비교적 큰 용량의 네트워크를 구축한다 (실제로 필요한 것보다 좀 더 큰 용량을 선택한다)
그 다음 과대적합을 막기 위해 한 개 이상의 규제 방법을 적용하여 별도의 테스트 세트 같은 새로운 데이터에서 일반화 성능을 높인다.
널리 사용되는 규제 방법은 L2 규제이다.
최근에 드롭아웃(dropout)이라는 새로운 규제 기법이 (심층) 신경망을 규제하는데 매우 뛰어나다는 것이 밝혀졌다.
드롭아웃을 앙상블 모델의 (평균적인) 조합으로 생각할 수 있다. 앙상블 학습에서는 독립적으로 여러 개의 모델을 훈련 시킨다.
예측을 할 때는 훈련된 모델을 모두 사용하여 결정한다. 여러 개의 모델을 훈련하고 출력을 모아 평균으 ㄹ내는 작업은 계산 비용이 비싸다.
드롭아웃은 많은 모델을 동시에 훈련하고 테스트나 예측 시에 평균을 효율적으로 계산하는 효과적인 방법을 제공한다.
드롭아웃은 보통 깊은 층의 은닉 유닛에 적용한다. 신경망의 훈련 단계에서 반복마다 PdropP_{drop} 확률로 은닉 유닛의 일부가 랜덤하게 꺼진다 (또는 Pkeep=1PdropP_{keep} = 1 - P_{drop} 확률만큼 랜덤하게 켜진다)
드롭아웃 확률은 사용자가 지정해야 하며 보통 p=0.5p = 0.5 를 사용한다. 입력 뉴런의 일부를 끄면 남은 뉴런에 연결된 가중치가 누락된 뉴런 비율만큼 증가된다.
랜덤한 드롭아웃의 영향으로 네트워크는 데이터에서 여분의 표현을 학습한다. 따라서 네트워크가 일부 은닉 유닛의 활성화 값에 의존할 수 없다.
훈련 과정에서 언제든지 은닉 유닛이 꺼질 수 있기 때문이다.
이는 네트워크가 데이터에서 더 일반적이고 안정적인 패턴을 학습하게 만든다.
랜덤한 드롭아웃은 과대적합을 효과적으로 방지한다. 아래 그림은 훈련 단계에서 p=0.5p = 0.5 의 확률로 드롭아웃을 적용하는 사례를 보여준다.
절반의 뉴런은 랜덤하게 활성화 되지 않는다.
예측할 때는 모든 뉴런이 참여하여 다음 층의 활성화 함수 입력을 계산한다.
여기서 보듯이 훈련 단계에서만 유닛이 랜덤하게 꺼진다는 것이 중요하다.
평가 단계에서는 모든 은닉 유닛이 활성화 되어야 한다 (즉 Pdrop=0P_{drop} = 0이고 Pkeep=1P_{keep} = 1이다)
훈련과 예측 단계의 전체 활성화 값의 스케일을 맞추기 위해 활성화된 뉴런 출력이 적절히 조정되어야 한다. (예컨대 훈련할 때 드롭아웃 확률이 p=0.5p = 0.5 라면 테스트할 때 활성화 출력을 절반으로 낮춘다)
실전에서 예측을 만들 때 활성화 값의 출력을 조정하는 것은 불편하기 때문에 텐서플로나 다른 라이브러리들은 훈련 단계의 활성화를 조정한다 (예컨대 드롭아웃 확률이 p=0.5p = 0.5 라면 활성화 함수의 출력을 2배로 높인다)
드롭아웃과 앙상블 학습간에 어떤 관계가 있을까? 반복마다 다른 은닉 유닛을 끄기 때문에 다른 모델을 훈련하는 효과를 낸다.
이런 모델을 모두 훈련시킨 후 유지 확률을 1로 설정하고 모든 은닉 유닛을 사용한다.
이는 모든 은닉 유닛으로부터 평균적인 활성화 출력을 얻는다는 의미가 된다.

텐서플로를 사용하여 심층 합성곱 신경망 구현

다층 CNN 구조

여기서 구현할 네트워크는 아래 그림에 나타나 있다.
입력은 28×28 크기의 그레이스케일 이미지이다.
채널 개수와 입력 이미지의 배치를 생각하면 입력 텐서의 차원은 batchsize x 28 x 28 x 1이 된다.
입력 데이터 5×5 크기의 커널을 가진 두 개의 합성곱 층을 지난다. 첫 번째 합성곱은 32개의 특성 맵을 출력하고 두 번째는 64개의 특성 맵을 출력한다. 각 합성곱 층 다음에는 서브샘플링으로 최대 풀링 연산이 뒤따른다.
그 다음 완전 연결 층의 출력이 최종 소프트맥스 층인 두 번째 완전 연결 층으로 전달된다.
각 층의 텐서 차원은 다음과 같다.
입력: batchsize x 28 x 28 x 1
합성곱_1: batchsize x 24 x 24 x 32
풀링_1: batchsize x 12 x 12 x 32
합성곱_2: batchsize x 8 x 8 x 64
풀링_2: batchsize x 4 x 4 x 64
완전 연결_1: batchsize x 1024
완전 연결과 소프트맥스 층: batchsize x 10

데이터 적재와 전처리

13장에서 load_mnist 함수를 사용하여 MNIST 손글씨 데이터셋을 읽었는데, 여기서도 다음과 같은 과정을 반복하겠다.
X_data, y_data = mn.load_mnist('./mnist/', kind='train') X_test, y_test = mn.load_mnist('./mnist/', kind='t10k') count = 50000 X_train, y_train = X_data[:count,:], y_data[:count] X_valid, y_valid = X_data[count:, :], y_data[count:]
Python
훈련 성능을 높이고 최적 값에 잘 수렴하려면 데이터를 정규화해야 한다.
훈련 데이터의 특성마다 평균을 계산하고 모든 특성에 걸쳐 표준 편차를 계산한다.
각 특성 별로 표준 편차를 계산하지 않는 이유는 MNIST 같은 이미지 데이터셋에 있는 일부 특성(픽셀) 값은 모든 이미지에서 동일하게 255이기 때문이다.
모든 샘플에서 고정된 값이면 변동이 없고 표준 편차가 0이 되므로 0-나눗셈 에러가 발생한다. 이런 이유로 X_train 전체의 표준 편차를 계산하기 위해 np.std 함수의 axis 매개변수를 지정하지 않았다.
mean_vals = np.mean(X_train, axis=0) std_val = np.std(X_train) X_train_centered = (X_train - mean_vals) / std_val X_valid_centered = (X_valid - mean_vals) / std_val X_test_centered = (X_test - mean_vals) / std_val
Python
여기서는 이미지를 2차원 배열로 읽어 들였다. 샘플마다 하나의 행을 차지하며 784개의 픽셀에 해당하는 열이 있다.
합성곱 신경망에 데이터를 주입하려면 784개의 행을 원본 이미지의 차원과 동일한 28 x 28 x 1 크기로 바꾸어야 한다.
MNIST 이미지는 흑백 이미지이기 때문에 마지막 컬러 채널이 의미가 없지만 합성곱 연산에서는 마지막 채널 차원이 필요하다.
넘파이의 reshape 메서드를 사용하여 훈련 데이터, 검증 데이터, 테스트 데이터의 차원을 다음과 같이 변경하겠다.
첫 번째 차원은 샘플 차원이므로 변경하지 않고 나머지 차원에 따라 자동으로 결정된다.
X_train_centered = X_train_centered.reshape((-1, 28, 28, 1)) X_valid_centered = X_valid_centered.reshape((-1, 28, 28, 1)) X_test_centered = X_test_centered.reshape((-1, 28, 28, 1))
Python
그 다음 13장에서 했던 것처럼 클래스 레이블을 원-핫 인코딩으로 변경하겠다. to_categorical 함수를 사용하여 변환한다.
from tensorflow.keras.utils import to_categorical y_train_onehot = to_categorical(y_train) y_valid_onehot = to_categorical(y_valid) y_test_onehot = to_categorical(y_test)
Python
훈련 데이터를 원하는 형태로 변환했기 때문에 CNN을 구현할 준비가 되었다.

텐서플로 tf.keras API로 CNN 구성

텐서플로에서 CNN을 구현하기 위해 tf.keras API로 합성곱 네트워크를 구현해 보겠다.
먼저 tf.keras의 하위 모듈 중 layers, models를 임포트한다.
그리고 13장에서 만들었던 것처럼 Sequential 모델을 만든다. 이전에는 완전 연결 층만 추가했지만, 이 예제에서는 합성곱을 위한 층을 추가한다.
from tensorflow.keras import layers, models model = models.Sequential()
Python
layers 모듈 아래에는 Dense 층 외에 다양한 층이 이미 구현되어 있다.
대표적으로 2차원 합성곱을 위한 Conv2D 클래스가 있다. 또 드롭아웃을 위한 Dropout 클래스와 최대 풀링을 위한 MaxPool2D, 평균 풀링을 위한 AveragePool2D 클래스를 제공한다.
먼저 Conv2D 클래스를 모델에 추가해 보겠다. 이전 장에서 Dense 층을 추가했던 것과 비슷하게 Conv2D 클래스의 객체를 모델의 add 메서드에 전달한다.
model.add(layers.Conv2D(32, (5, 5), padding='valid', activation='relu', input_shape=(28, 28, 1)))
Python
Conv2D 클래스의 첫 번째 매개변수는 필터 개수이고 두 번째는 필터 크기이다. 그림 15-10에 나타난 CNN 네트워크 구조처럼 첫 번째 합성곱 층은 5×5 크기의 필터를 32개 가진다.
padding 매개변수에는 ‘valid’ 패딩을 지정한다. 세임 패딩을 선택하려면 ‘same’으로 지정한다. 대소문자는 구분하지 않는다.
padding 매개변수의 기본값이 ‘valid’이므로 설정하지 않아도 된다.
스트라이드를 설정하는 strides 매개변수는 정수 또는 정수 두 개로 이루어진 튜플로 지정한다. 튜플일 경우 높이와 너비 방향의 스트라이드를 각각 다르게 지정할 수 있다.
기본값은 1로 높이와 너비 방향으로 1칸씩 필터를 이동시킨다. 여기서는 strides를 지정하지 않았으므로 기본값을 사용한다.
활성화 함수는 Dense 층과 마찬가지로 activation 매개변수에서 지정한다. 여기서는 최근 이미지 분야에서 자주 사용되는 렐루(ReLu) 활성화 함수를 선택했다.
kernel_initializer와 bias_initializer 매개변수는 따로 지정하지 않았으므로 기본값으로 설정도니다.
kernel_initializer 매개변수는 세이비어(또는 글로럿) 초기화 방식인 ‘glorot_uniform’이 되고 bias_initializer는 ‘zeros’가 사용된다.
마지막으로 모델에 추가되는 첫 번째 층이므로 입력 크기를 input_shape 매개변수에 지정한다. 여기서도 첫 번째 배치 차원을 제외하고 28 x 28 x 1 크기를 지정했다.
앞서 합성곱의 출력을 계산하는 공식을 사용하여 출력 크기를 계산해 보겠다. 입력 크기는 28, 필터 크기는 5, 패딩은 0이고 스트라이드는 1이다.
o=n+2pms+1=28+051+1=24o = {n + 2p - m \over s} + 1 = {28 + 0 - 5 \over 1} + 1 = 24
이미지를 하나 생각해 보자. 28 x 28 x 1 크기의 이미지가 첫 번째 합성곱 연산을 거쳐 24 x 24 x 1 크기로 바뀐다. 첫 번째 층의 필터 개수가 32개이므로 최종적으로 출려되는 특성 맵의 크기는 24 x 24 x 32가 된다.
이 층의 전체 가중치 개수는 5 x 5 x 1 크기의 필터가 32개 있고 절편이 32개 있으므로 5 x 5 x 32 + 32 = 832개 이다.
그 다음 추가할 층은 최대 풀링 층이다. 코드는 다음과 같다.
model.add(layers.MaxPool2D((2, 2)))
Python
풀링 층의 첫 번째 매개변수(pool_size)는 풀링 크기로 높이와 너비를 튜플로 지정한다.
풀링 크기의 기본값은 (2, 2) 이다.
두 번째 매개변수는 스트라이드(strides)로 기본값은 None이다.
스트라이드가 none이면 풀링 크기를 사용하여 겹치지 않도록 풀링된다.
보통 풀링에서 스트라이드를 지정하는 경우는 드물다. 여기서도 스트라이드는 따로 지정하지 않았다.
(2, 2) 크기로 풀링했기 때문에 풀링 층을 통과한 특성 맵의 크기는 높이와 너비가 절반으로 줄어든다. 하지만 특성 맵의 개수는 변화가 없다. 따라서 최종적으로 출력되는 특성 맵의 차원은 12 x 12 x 32가 된다.
또 풀링 층은 가중치가 없다는 점도 잊지 말자
두 번째 합성곱 층을 추가할 차례이다. 필터 개수만 제외하고 첫 번째 합성곱의 매개변수와 동일하다. 여기서는 64개의 필터를 사용하겠다.
model.add(layers.Conv2D(64, (5, 5), padding='valid', activation='relu'))
Python
두 번째 합성곱 층의 출력 크기를 계산해 보자. 입력 크기는 12, 필터 크기는 5, 패딩은 0이고 스트라이드는 1이다.
o=n+2pms+1=12+051+1=8o = {n + 2p - m \over s} + 1 = {12 + 0 - 5 \over 1} + 1 = 8
12 x 12 x 32 크기의 이미지가 두 번째 합성곱 연산을 거쳐 8 x 8 x 1 크기로 바뀐다. 두 번째 층의 필터 개수가 64개이므로 최종적으로 출력되는 특성 맵의 크기는 8 x 8 x 64가 된다.
두 번째 합성곱 층의 필터 크기는 5 x 5 x 1이 아니라 5 x 5 x 32이다. 측, 채널 방향으로는 필터가 이동하지 않고 전체 채널이 한 번에 합성곱에 참여한다.
Conv2D의 필터 크기를 (5, 5)로 지정했지만, tf.keras API는 똑똑하게 이전 층의 출력 채널에 맞추어 필터를 생성한다.
그럼 두 번째 층의 가중치 개수는 얼마일까? 5 x 5 x32 크기의 필터가 64개 있고 절편이 64개 있다. 따라서 5 x 5 x 32 x 64 + 54 = 51,264개이다.
이제 두 번째 풀링 층을 추가해보자. 코드는 첫 번째 풀링 층과 동일하다.
model.add(layers.MaxPool2D((2, 2)))
Python
여기서도 (2, 2) 크기로 풀링했기 때문에 특성 맵의 크기는 높이와 너비가 절반으로 줄어든다. 특성 맵의 개수는 변화가 없으므로 최종적으로 출력되는 특성 맵의 차원은 4 x 4 x 64가 된다.
다음으로 완전 연결 층인 Dense 층에 연결하기 위해 4 x 4 x 64 차원의 텐서를 일렬로 펼쳐야 한다. 케라스 API는 이런 작업을 위해 Flatten 클래스를 제공한다.
이 클래스는 매개변수가 필요하지 않다. 모델에 추가하면 이전 층의 출력을 일렬로 펼치는 작업을 한다. 당연하게 학습되는 가중치도 없다.
model.add(layers.Flatten())
Python
4 x 4 x 64 크기의 텐서를 펼쳤으므로 1,024차원의 텐서가 되었다. 이를 1,024개의 유닛을 가진 완전 연결 층에 연결하겠다. 이 층의 활성화 함수도 렐루 함수를 사용한다.
model.add(layers.Dense(1024, activation='relu'))
Python
Dense 층의 kernel_initializer와 bias_initializer도 지정하지 않으면 기본값인 ‘glorot_uniform’과 ‘zeros’로 설정된다.
이 층의 가중치 개수는 1,024 텐서를 1,024개의 유닛에 완전 연결했으므로 1,024 x 1,024개와 절편 1,024개를 더하면 1,049,600개가 된다. 두 개의 합성곱 층에서 사용한 가중치를 합한 것보다 훨씬 많다.
마지막 층에 연결하기 전에 드롭아웃 층을 추가하겠다. 케라스의 Dropout 클래스도 가중치를 가지지 않는다.
model.add(layers.Dropout(0.5))
Python
Dropout 클래스에는 유닛을 끌 확률을 매개변수로 지정한다. 편리하게도 fit 메서드에서만 드롭아웃이 적용된다. 테스트나 평가를 위해 따로 모델을 구성할 필요는 없다.
마지막 층은 열 개의 손글씨 숫자에 대한 확률을 출력해야 하므로 열 개의 유닛을 가진 완전 연결 층이다. 다중 분류 문제를 위한 활성화 함수는 소프트맥스 함수이므로 activation 매개변수를 ‘softmax’로 설정한다.
model.add(layers.Dense(10, activation='softmax'))
Python
이 층의 가중치 개수는 1,024개의 이전 Dense 층 출력을 열 개의 유닛에 연결했으므로 절편과 합쳐서 1,024 x 10 + 10 = 10,250이 된다.
합성곱 신경망은 전형적으로 이렇게 마지막에 한 개 이상의 완전 연결 층으로 연결된다. 최종 출력 층의 유닛 개수는 클래스 레이블의 개수와 맞추어야 한다.
모델의 summary 메서드를 호출하여 합성곱 신경망의 구성을 확인해 보자.
model.summary() ### 결과 # Model: "sequential" # _________________________________________________________________ # Layer (type) Output Shape Param # # ================================================================= # conv2d (Conv2D) (None, 24, 24, 32) 832 # _________________________________________________________________ # max_pooling2d (MaxPooling2D) (None, 12, 12, 32) 0 # _________________________________________________________________ # conv2d_1 (Conv2D) (None, 8, 8, 64) 51264 # _________________________________________________________________ # max_pooling2d_1 (MaxPooling2 (None, 4, 4, 64) 0 # _________________________________________________________________ # flatten (Flatten) (None, 1024) 0 # _________________________________________________________________ # dense (Dense) (None, 1024) 1049600 # _________________________________________________________________ # dropout (Dropout) (None, 1024) 0 # _________________________________________________________________ # dense_1 (Dense) (None, 10) 10250 # ================================================================= # Total params: 1,111,946 # Trainable params: 1,111,946 # Non-trainable params: 0
Python

합성곱 신경망 모델 훈련

이제 모델을 컴파일할 차례이다.
다중 분류 작업이므로 손실 함수는 이전 장에서 사용했던 것처럼 categorical_crossentropy를 사용한다.
옵티마이저는 adam을 사용하겠다. 또 손실 점수와 더불어 정확도 값을 계산하기 위해 metrics 매개변수에 ‘acc’를 추가했다.
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
Python
13장에서 보았던 것처럼 모델을 훈련할 때 최선의 가중치를 저장하기 위해 ModelCheckpoint 콜백을 사용하겠다. 또 텐서보드를 사용하여 시각화하기 위해 TensorBoard 콜백도 추가하겠다.
import time from tensorflow.keras.callbacks import ModelCheckpoint, TensorBoard callback_list = [ModelCheckpoint(filepath='cnn_checkpoint.h5', monitor='val_loss', save_best_only=True), TensorBoard(log_dir='logs/{}'.format(time.asctime()))] # 위 코드에서 tensorboard가 폴더를 못 만든다면 아래처럼 시간을 빼고 돌리면 일단 실행은 된다. 윈도우에서 폴더 만드는 게 잘 안되는 것이라 생각 됨. # callback_list = [ModelCheckpoint(filepath='cnn_checkpoint.h5', monitor='val_loss', save_best_only=True), TensorBoard()]
Python
체크포인트 콜백은 검증 손실(val_loss)을 모니터링하고 최상의 가중치를 cnn_checkpoint.h5 파일에 저장한다.
텐서보드 콜백은 logs 디렉터리 하위에 서브디렉터리를 만들어 통계를 저장한다.
모델을 여러 번 훈련하는 경우 같은 디렉터리에 데이터가 저장되면 텐서보드에서 그래프를 보기가 불편하므로 실행할 때마다 다른 하위 디렉터리에 저장할 수 있게 time 모듈의 asctime 함수를 사용했다.
이 두 콜백을 연결하여 callback_list를 만들었다. 모델의 fit 메서드를 호출할 때 callbacks 매개변수로 전달하겠다.
history = model.fit(X_train_centered, y_train_onehot, batch_size=64, epochs=20, validation_data=(X_valid_centered, y_valid_onehot), callbacks=callback_list)
Python
훈련 데이터와 검증 데이터는 앞서 준비했던 X_train_centered, y_train_onehot, X_valid_centered, y_valid_onehot을 사용한다.
fit 메서드의 batch_size 기본값은 32이다. 즉 32개씩 미니배치를 만들어 네트워크를 훈련한다. 여기서는 64개로 늘렸다.
훈련 세트를 반복하여 학습하는 횟수는 20번으로 지정했다.
fit 메서드의 훈련 결과는 다음과 같다.
# Train on 50000 samples, validate on 10000 samples # Epoch 1/20 # 2020-05-03 11:18:07.989105: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library cublas64_100.dll # 2020-05-03 11:18:08.209138: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library cudnn64_7.dll # 2020-05-03 11:18:09.156252: W tensorflow/stream_executor/cuda/redzone_allocator.cc:312] Internal: Invoking ptxas not supported on Windows # Relying on driver to perform ptx compilation. This message will be only logged once. # 2020-05-03 11:18:09.217162: I tensorflow/core/profiler/lib/profiler_session.cc:184] Profiler session started. # 2020-05-03 11:18:09.221872: W tensorflow/stream_executor/platform/default/dso_loader.cc:55] Could not load dynamic library 'cupti64_100.dll'; dlerror: cupti64_100.dll not found # 2020-05-03 11:18:09.228706: W tensorflow/core/profiler/lib/profiler_session.cc:192] Encountered error while starting profiler: Unavailable: CUPTI error: CUPTI could not be loaded or symbol could not be found. # 64/50000 [..............................] - ETA: 22:04 - loss: 2.3739 - acc: 0.07812020-05-03 11:18:09.244288: I tensorflow/core/platform/default/device_tracer.cc:588] Collecting 0 kernel records, 0 memcpy records. # 2020-05-03 11:18:09.249098: E tensorflow/core/platform/default/device_tracer.cc:70] CUPTI error: CUPTI could not be loaded or symbol could not be found. # 50000/50000 [==============================] - 4s 87us/sample - loss: 0.1382 - acc: 0.9576 - val_loss: 0.0558 - val_acc: 0.9824 # Epoch 2/20 # 50000/50000 [==============================] - 3s 51us/sample - loss: 0.0516 - acc: 0.9840 - val_loss: 0.0520 - val_acc: 0.9848 # Epoch 3/20 # 50000/50000 [==============================] - 3s 51us/sample - loss: 0.0353 - acc: 0.9891 - val_loss: 0.0417 - val_acc: 0.9883 # ... # Epoch 20/20 # 50000/50000 [==============================] - 2s 49us/sample - loss: 0.0106 - acc: 0.9972 - val_loss: 0.0793 - val_acc: 0.9909
Python
fit 메서드에서 반환된 history 객체를 사용하여 훈련 세트와 테스트 세트에 대한 손실 그래프를 그리면 다음과 같다.
import matplotlib.pyplot as plt epochs = np.arange(1, 21) plt.plot(epochs, history.history['loss']) plt.plot(epochs, history.history['val_loss']) plt.xlabel('epochs') plt.ylabel('loss') plt.show()
Python
정확도 그래프는 다음과 같다.
plt.plot(epochs, history.history['acc']) plt.plot(epochs, history.history['val_acc']) plt.xlabel('epochs') plt.ylabel('accuracy') plt.show()
Python
텐서보드를 실행하고 브라우저에 http://localhost:6006/에 접속하면 훈련 과정의 손실과 정확도 그래프를 볼 수 있다.
그래프 탭에 타나난 합성곱 신경망의 구조는 아래 그림과 같다.
tensorboard --logdir logs/
Python
훈련된 모델을 사용해서 테스트 세트를 평가하기 전에 모델과 가중치를 저장하자.
model.save('cnn_model.h5')
Python
load_model 함수를 사용하여 저장된 모델을 불러 새로운 모델 객체를 만들고 이전에 훈련 과정에서 가장 높은 성능의 가중치가 저장된 체크포인트 파일을 복원한다.
from tensorflow.keras.models import load_model restored_model = load_model('cnn_model.h5') restored_model.load_weights('cnn_checkpoint.h5')
Python
복원된 restored_model을 사용하여 테스트 세트에서 평가해 보자.
restored_model.evaluate(X_test_centered, y_test_onehot) ### 결과 # =========] - 1s 62us/sample - loss: 0.0141 - acc: 0.9911
Python
99%가 넘는 예측 정확도가 나오는데 이는 13장에서 피드포워드 신경망으로 얻은 결과보다 훨씬 뛰어난 정확도이다.
테스트 샘플 중 처음 열 개의 예측을 직접 확인해 보면 다음과 같다.
print(np.argmax(restored_model.predict(X_test_centered[:10]), axis=1)) ### 결과 # [7 2 1 0 4 1 4 9 5 9]
Python
손글씨 숫자의 예제는 레이블 인덱스와 레이블 값이 같다. 일반적으로 두 값은 다르기 때문에 argmax 함수에서 반환된 인덱스를 사용해서 진짜 클래스 레이블을 구해야 한다.
비교를 위해 처음 열 개의 테스트 레이블을 확인해 보자
print(y_test[:10]) ### 결과 # [7 2 1 0 4 1 4 9 5 9]
Python
예측 결과와 모두 동일하다. 이 열 개의 숫자가 어떤 모습인지 확인하기 위해 샘플 이미지를 그려보자.
X_test_centered는 행을 따라 샘플이 놓여 있는 2차원 배열이다. 784개의 열을 28 x 28 배열로 바꾸어 그려보자.
fig = plt.figure(figsize=(10,5)) for i in range(10): fig.add_subplot(2, 5, i+1) plt.imshow(X_test_centered[i].reshape(28, 28))
Python
출력된 결과를 보면 이 모델의 성능이 매우 뛰어나다는 것을 알 수 있다.

활성화 출력과 필터 시각화

첫 번째 합성곱 층의 출력을 이미지로 시각화해 보자.
model 객체에 추가한 층은 layers 속성으로 참조할 수 있다. 첫 번째 층의 객체를 추출해서 출력해 보자.
first_layer = model.layers[0] print(first_layer) ### 결과 # <tensorflow.python.keras.layers.convolutional.Conv2D object at 0x0000021BB186F988>
Python
first_layer가 Conv2D 임을 알 수 있다. first_layer의 output 속성을 함수형 API의 출력으로 사용하면 첫 번째 층의 활성화 출력을 얻을 수 있다.
이제 함수형 API를 사용하기 위한 입력이 필요하다. 사실 Sequential 객체에 첫 번째 층을 추가하면 자동으로 model 객체 안에 input 속성이 정의된다. 이를 출력해서 확인해 보자.
print(model.input) ### 결과 # Tensor("conv2d_input:0", shape=(None, 28, 28, 1), dtype=float32)
Python
첫 번째 합성곱 층을 추가할 때 input_shape 매개변수에 입력 크기를 (28, 28, 1)로 지정했다. 이 때문에 model.input의 크기는 배치 차원이 추가되어 (None, 28, 28, 1)이다.
입력과 출력 텐서가 모두 준비되었으므로 이 둘을 연결할 새로운 모델을 만든다. 그 다음 테스트 세트에서 처음 열 개의 샘플을 주입하여 출력을 구하자.
first_activation = models.Model(inputs=model.input, outputs=first_layer.output) activation = first_activation.predict(X_test_centered[:10]) print(activation.shape) ### 결과 # (10, 24, 24, 32)
Python
predict 메서드에서 계산에 사용한 가중치는 앞서 fit 메서드로 훈련한 값이다.
열 개의 테스트 샘플을 입력했으므로 첫 번째 배치 차원은 10이고, 합성곱을 통과하며 높이와 너비가 각각 24 x 24로 줄었다. 첫 번째 합성곱 층의 필터가 32개이므로 마지막 차원이 32가 된다.
열 개의 샘플 중 첫 번째 샘플의 특성 맵 32개를 모두 그리면 아래와 같다.
fig = plt.figure(figsize=(10, 15)) for i in range(32): fig.add_subplot(7, 5, i+1) plt.imshow(activation[0, :, :, i])
Python
숫자 7의 윤곽을 특성으로 잘 추출한 것으로 보인다.
이번에는 네 번째 숫자의 특성 맵을 그려보자.
fig = plt.figure(figsize=(10, 15)) for i in range(32): fig.add_subplot(7, 5, i+1) plt.imshow(activation[3, :, :, i])
Python
특성 맵마다 조금씩 다른 숫자 0의 윤곽을 추출하고 있다. 특성 맵의 차이는 필터가 서로 다른 부분을 학습하기 때문이다.
이번 에는 첫 번째 층의 필터를 출력해 보겠다.
합성곱 필터는 합성곱 층의 kernel 속성에 저장되어 있다. 필터의 차원은 (높이, 너비, 입력 채널, 출력 채널)이다.
fig = plt.figure(figsize=(10, 15)) for i in range(32): fig.add_subplot(7, 5, i+1) plt.imshow(activation[3, :, :, i])
Python
필터의 밝은 부분이 높은 값을 의미한다. 예컨대 아홉 번째 필터는 수평 에지를 학습하는 것으로 보인다.
이 필터를 사용하여 숫자 7에서 추출된 특성은 수평 부분이 잘 나타나 있다. 반면 0은 수평 부분이 많지 않으므로 추출된 특성에 정보가 많이 담겨 있지 않다.
합성곱 활성화 출력과 필터를 분석하면 중요한 통찰을 얻을 수 이는 경우가 많다. 층이 깊어질수록 합성곱의 활성화 출력 의미를 이해하기 어렵다는 것을 기억하라.