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

합성곱 신경망의 구성 요소

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

CNN과 특성 계층 학습

  • 관련이 높은 핵심 특징을 올바르게 추출하는 것은 모든 머신 러닝 알고리즘의 성능에서 아주 중요한 요소이다.
    • 전통적인 머신 러닝 모델은 도메인 전문가가 만든 특성에 의존하거나 컴퓨터를 사용한 특성 추출 기법에 바탕을 두고 있다.
    • 신경망은 원본 데이터에서 작업에 가장 유용한 특성을 자동으로 학습한다. 이런 이유 때문에 신경망을 특성 추출 엔진으로 생각하기도 한다. 예컨대 입력에 가까운 층은 저수준 특성을 추출한다.
  • 다층 신경망과 특히 심층 합성곱 신경망은 각 층별로 저수준 특성을 연결하여 고수준 특성을 만듦으로써 소위 특성 계층을 구성한다.
    • 예컨대 이미지를 다룬다면 에지(edge)나 동그라미 같은 저수준 특성이 앞쪽 층에서 추출된다.
    • 이런 특성들이 연결되어 건물, 자동차, 강아지 같은 고수준 특성을 형성한다.
  • 아래 그림에서 보듯이 CNN은 입력 이미지에서 특성 맵(feature map)을 만든다. 이 맵의 각 원소는 입력 이미지의 국부적인 픽셀 패치에서 유도된다.

  • 이런 국부적인 픽셀 패치를 국부 수용장(local receptive field)라고 한다. CNN은 일반적으로 이미지 관련 작업을 매우 잘 수행한다. 이는 다음 두 개의 중요한 아이디어 때문이다.
    • 희소 연결: 특성 맵에 있는 하나의 우너소는 작은 픽셀 패치 하나에만 연결된다 (퍼셉트론 처럼 모든 입력 이미지에 있는 연결되는 것과 매우 다르다)
    • 파라미터 공유: 동일한 가중치가 입력 이미지의 모든 패치에 사용된다.
  • 이 두 아이디어 결과로 네트워크의 가중치(파라미터) 개수가 극적으로 감소하고 중요 특징을 잡아내는 능력이 향상된다. 당연히 가까이 있는 픽셀들이 멀리 있는 픽셀보다 연관성이 높다.
  • 일반적으로 CNN은 여러 개의 합성곱(conv) 층과 풀링(Pooling)이라고도 하는 서브샘플링(subsampling) 층으로 이루어져 있다. 마지막에는 하나 이상의 완전 연결(FC) 층이 따라온다.
    • 완전 연결 층은 모든 입력 유닛 i 가 모든 출력 유닛 j 에 가중치 w_{ij} 로 연결되어 있는 다층 퍼셉트론이다.
  • 풀링 층으로 알려진 서브샘플링 층은 학습되는 파라미터가 없다. 즉, 풀링 층에는 가중치나 절편 유닛이 없다. 합성곱이나 완전 연결 층은 가중치와 절편을 가진다.

이산 합성곱 수행

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

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

  • 앞으로 사용할 기본적인 정의와 기호를 설명하는 것부터 시작하겠다.
    • 두 개의 1차원 벡터 x w 에 대한 이산 합성곱은 y = x * w 로 나타낸다.
    • x 는 입력이고 (이따금 신호라고 부름) w 는 필터(filter) 또는 커널(kernel)이라 부른다.
    • 이산 합성곱의 수학적 정의는 아래와 같다.

y = x * w \to y[i] = \sum_{k=-\infty}^{+\infty} x[i-k] w[k]

  • 여기서 대괄호는 벡터 원소의 인덱스를 나타내는데 사용한다. 인덱스 i 는 출력 벡터 y 의 각 원소에 대응한다.
  • 위 공식에서 특이한 점이라면 -\infty 에서 +\infty 까지의 인덱스와 x 의 음수 인덱싱이다.
  • 인덱스 -\infty 에서 \infty 까지의 합은 특히 이상하게 보인다. 머신 러닝 애플리케이션은 항상 유한한 특성 벡터를 다루기 때문이다.
    • 예컨대 x 0, 1, 2, ... , 8, 9 인덱스로 열 개의 특성을 가지고 있다면 -\infty:-1 10:+\infty 인덱스는 x 의 범위 밖이다.
    • 이전 공식에 있는 덧셈을 올바르게 계산하려면 x w 가 0으로 채워져 있다고 가정해야 한다. 또 출력 벡터 y 도 0으로 채워진 무한 크기가 된다.
    • 이는 실제 상황에서는 유용하지 않기 때문에 유한한 개수의 0으로 x 가 패딩된다.
    • 이 과정을 제로 패딩(zero padding) 또는 패딩(padding)이라고 한다. 각 방향으로 추가된 패딩 수는 p 로 나타난다.
    • 1차원 벡터 x 의 패딩 예가 아래 그림에 나타나 있다.

  • 원본 입력 x 와 필터 w 가 각각 n m 개의 원소를 가지고 m \leq n 이라고 가정해 보자.
    • 패딩된 벡터 x^{p} 의 크기는 n + 2p 이다.
    • 이산 합성곱을 계산하기 위한 실제 공식은 다음과 같이 바뀐다.

y = x * w \to y[i] = \sum_{k=0}^{m-1} x^{p}[i+m-k] w[k]

  • 무한한 인덱스 문제를 해결했다. 둘째 이슈는 i+m-k x 를 인덱싱하는 것이다.
    • x w 가 이 식에서 다른 방향으로 인덱싱한다는 점이 중요하다.
    • 이 때문에 패딩된 후에 x 또는 w 벡터 중 하나를 뒤집어 간단히 점곱으로 계산할 수 있다.
  • 필터 w 를 뒤집어서 회전된 필터 w^{r} 을 얻었다고 가정해 보자.
    • 점곱 x[i:i+m] \cdot w^{r} 을 계산하면 y[i] 원소 하나가 얻어진다. x[i:i+m] 은 크기가 m x 의 패치이다.
    • 이 연산이 모든 출력 원소를 얻기 위해 슬라이딩 윈도우(sliding window) 방식으로 반복된다.
    • 아래 그림은 x = (3, 2, 1, 7, 1, 2, 5, 4) 이고 w = ({1 over 2}, {3 over 4}, 1, {1 over 4}) 일 때 처음 세 개의 출력 원소를 계산하는 경우를 보여준다.

  • 이 예에서 패딩 크기는 0이다. (p = 0 )
    • 회전된 필터 w^{r} 은 2칸씩 이동한다. 이동하는 양은 스트라이드(stride)라고 하며, 또 하나의 합성곱 하이퍼파라미터이다.  여기서 스트라이드는 2이다. (s = 2 )
    • 스트라이드는 입력 벡터의 크기보다 작은 양수 값이어야 한다.

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

  • 지금까지 유한한 크기의 출력 벡터를 얻기 위해 합성곱에 제로 패딩을 사용했다.
    • 기술적으로 p \geq 0 인 어떤 패딩도 적용할 수 있다. p 값에 따라 x 에서 경계에 있는 셀은 중간 셀과 다르게 처리된다.
  • n=5, m=3, p=0 인 경우를 생각해 보자. x[0] 은 하나의 출력 원소를 계산하는데만 사용된다. (예컨대 y[0] )
    • 반면 x[1] 은 두 개의 출력 원소를 계산하는데 사용된다. (y[0], y[1] )
    • x 원소를 이렇게 다르게 취급하기 때문에 가운데 있는 [2]x 가 대부분의 계산에 사용되어 강조되는 효과를 낸다.
    • 여기서는 p=2 를 사용하면 이 문제를 피할 수 있다.
    • x 의 각 원소가 세 개의 y 원소 계산에 참여한다.
  • 또 출력 y 크기는 사용한 패딩 방법에 따라 달라진다. 실전에서 자주 사용하는 세 개의 패딩 방법은 풀(full) 패딩, 세임(same) 패딩, 밸리드(valid) 패딩이다.
    • 풀 패딩은 패딩 파라미터 p p=m-1 로 설정한다. 풀 패딩은 출력 크기를 증가시키기 때문에 합성곱 신경망 구조에서는 거의 사용되지 않는다.
    • 세임 패딩은 출력 크기가 입력 벡터 x 와 같아야 할 때 사용한다. 이때 패딩 파라미터 p 는 입력과 출력 크기가 동일해야 하기 때문에 필터 크기에 따라 결정된다.
    • 마지막으로 밸리드 패딩 합성곱은 p=0 인 경우를 말한다. (패딩 없음)
  • 아래 그림은 세 개의 패딩 모드를 보여준다. 입력은 5×5 픽셀, 커널은 3×3 크기, 스트라이드는 1인 경우이다.

  • 합성곱 신경망에서 가장 많이 사용되는 패딩 방법은 세임 패딩이다. 다른 패딩 방식에 비해 장점은 세임 패딩이 입력 이미지나 텐서의 높이와 너비를 유지시킨다는 것이다. 이 때문에 네트워크 구조를 설계하기 쉽다.
  • 풀 패딩이나 세임 패딩에 비해 밸리드 패딩의 단점은 신경망에 층이 추가될수록 점진적으로 텐서 크기가 줄어든다는 것이다. 이는 신경망 성능을 나쁘게 만들 수 있다.
  • 실전에서는 세임 패딩으로 너비와 높이를 유지시키고 풀링에서 크기를 감소시킨다. 풀 패딩은 입력보다 출력 크기를 증가시키므로 경계 부분의 영향을 최소화하는 것이 중요한 신호 처리 애플리케이션에서 보통 사용된다.
    • 딥러닝에서는 경계 부분의 영향이 크지 않기 때문에 풀 패딩이 거의 사용되지 않는다.

합성곱 출력 크기 계산

  • 합성곱 출력 크기는 입력 벡터 위를 필터 w 가 이동하는 전체 횟수로 결정된다.
    • 입력 벡터의 크기는 n 이고 필터의 크기는 m , 패딩이 p , 스트라이드가 s x * w 출력 크기는 다음과 같이 계산된다.

o = \lfloor {n + 2p - m \over s} \rfloor + 1

  • 여기서 \lfloor \cdot \rfloor 는 버림 연산을 나타낸다.
  • 입력 벡터 크기가 10이고 합성곱 커널 크기가 5, 패딩이 2, 스트라이드가 1일 때 출력 크기는 다음과 같다.

n=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=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]

2D 이산 합성곱 수행

  • 앞서 배운 개념은 2차원으로 쉽게 확장 가능하다. m_{1} \leq n_{1} 이고 m_{2} \leq n_{2} 인 행렬 X_{n_{1} \times n_{2}} 와 필터 행렬 W_{m_{1} \times m_{2}} 같은 2차원 입력을 다룰 떄 X W 의 2D 합성곱 결과는 행렬 Y = X * W 가 된다.

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) 과 스트라이드 s = (2, 2) 일 때 입력 행렬 X_{3 \times 3} 과 커널 행렬 W_{3 \times 3} 사이의 2D 합성곱 계산을 보여준다.
    • 여기서는 입력 행렬의 네 면에 0이 한줄씩 추가되어 X_{5 \times 5}^{padded} 행렬을 만든다.

  • 필터를 뒤집으면 다음과 같다.

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] 처럼 쓴다. 그 다음 패딩된 입력 행렬 X^{padded} 를 따라 슬라이딩 윈도우처럼 역전된 필터를 이동하면서 원소별 곱의 합을 계산한다.
    • 아래 그림에 \odot 연산자로 표기했다.

  • 결과값 Y 는 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]]

서브샘플링

  • 서브샘플링은 전형적인 두 종류의 풀링 연산으로 합성곱 신경망에 적용된다. 최대 풀링(max-pooing)과 평균 풀링(mean-pooling 또는 average-pooling)이다.
    • 풀링 층은 보통 P_{n_{1} \times n_{2}} 로 표시한다.
    • 아래 첨자는 최댓값과 평균 연산이 수행되는 이웃한 픽셀 크기이다. (차원별로 인접 픽셀 개수)
    • 이런 이웃 픽셀 개수를 풀링 크기라고 한다.
  • 아래 그림에 이 연산을 나타냈다. 최대 풀링은 이웃한 픽셀에서 최댓값을 취하고 평균 풀링은 픽셀의 평균을 계산한다.

  • 풀링의 장점은 두가지 이다.
    • 풀링(최대 풀링)은 일종의 지역 불변경을 만든다. 국부적인 작은 변화가 최대 풀링의 결과를 바꾸지 못한다는 의미이다. 결국 입력 데이터에 있는 잡음에 좀 더 안정적인 특성을 생성한다. 아래에서 보듯 두 개의 다른 입력 행렬 X_{1} X_{2} 가 같은 결과를 만든다.
    • 풀링은 특성 크기를 줄이므로 계산 효율성을 높인다. 또 특성 개수가 줄어들면 과대적합도 감소된다.

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

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

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

  • 합성곱 층의 입력 샘플에는 N_{1} \times N_{2} 차원 (예컨대 이미지의 높이와 너비 픽셀)인 하나 이상의 2D 배열 또는 행렬이 포함될 수 있다.
    • 이런 N_{1} \times N_{2} 행렬을 채널(channel)이라고 한다.
    • 여러 개의 채널을 합성곱 층 입력에 사용하기 때문에 랭크 3 텐서 또는 3차원 배열 X_{N_{1} \times N_{2} \times C_{in}} 을 사용해야 한다.
    • 여기서 C_{in} 이 입력 채널 크기이다.
  • 예컨대 CNN의 첫 번째 층에 입력되는 이미지를 생각해 보자. RGB 모드의 컬러 이미지라면 C_{in} = 3 이다. (RGB의 빨간색, 초록색, 파란색 채널)
    • 이미지가 그레이스케일(grayscale)이라면 흑백의 픽셀 강도를 가진 하나의 채널만 있으므로 C_{in} = 1 이다.
  • 합성곱 연산에서 여러 개의 입력 채널을 어떻게 다룰 수 있을까?
    • 해답은 간단하다. 각 채널별로 합성곱 연산을 수행하고 행렬 덧셈으로 결과를 합친다.
    • 채널 (c)별 합성곱은 개별적인 커널 행렬 W[:,:,c] 를 사용한다.
    • 활성화 함수에 입력되는 결과값은 다음 공식으로 계산된다.

  • 최종 결과 h 를 특성 맵이라고 한다.
    • 보통 CNN의 합성곱 층은 하나 이상의 특성 맵을 만든다.
    • 여러 개의 특성 맵을 사용하면 커널 텐서는 width \times height \times C_{in} \times C_{out} 으로 4차원이 된다.
    • 너비와 높이는 커널의 크기고 C_{in} 은 입력 채널의 개수, C_{out} 은 출력 특성 맵의 개수이다.
    • 이전 공식에 출력 특성 맵의 개수를 포함시키면 아래와 같다.

  • 아래 그림에 나온 합성곱 층과 풀링 층이 포함된 예제를 통해 신경망의 합성곱 계산을 정리하겠다.
    • 이 예는 입력 채널이 3개이다. 커널 텐서는 4차원이다. 각 커널 행렬은 m_{1} \times m_{2} 크기고 입력 채널에 한 개씩 세 개 이다.
    • 이런 텐서가 다섯 개의 출력 특성 맵을 만들기 위해 다섯 개가 있다.
    • 마지막으로 특성 맵을 서브샘플링하기 위해 풀링 층이 있다.
    • 전체 구조는 아래 그림과 같다.

드롭아웃으로 신경망 규제

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

  • 여기서 보듯이 훈련 단계에서만 유닛이 랜덤하게 꺼진다는 것이 중요하다.
    • 평가 단계에서는 모든 은닉 유닛이 활성화 되어야 한다 (즉 P_{drop} = 0 이고 P_{keep} = 1 이다)
    • 훈련과 예측 단계의 전체 활성화 값의 스케일을 맞추기 위해 활성화된 뉴런 출력이 적절히 조정되어야 한다. (예컨대 훈련할 때 드롭아웃 확률이 p = 0.5 라면 테스트할 때 활성화 출력을 절반으로 낮춘다)
  • 실전에서 예측을 만들 때 활성화 값의 출력을 조정하는 것은 불편하기 때문에 텐서플로나 다른 라이브러리들은 훈련 단계의 활성화를 조정한다 (예컨대 드롭아웃 확률이 p = 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:]
  • 훈련 성능을 높이고 최적 값에 잘 수렴하려면 데이터를 정규화해야 한다.
    • 훈련 데이터의 특성마다 평균을 계산하고 모든 특성에 걸쳐 표준 편차를 계산한다.
    • 각 특성 별로 표준 편차를 계산하지 않는 이유는 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
  • 여기서는 이미지를 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))
  • 그 다음 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)
  • 훈련 데이터를 원하는 형태로 변환했기 때문에 CNN을 구현할 준비가 되었다.

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

  • 텐서플로에서 CNN을 구현하기 위해 tf.keras API로 합성곱 네트워크를 구현해 보겠다.
    • 먼저 tf.keras의 하위 모듈 중 layers, models를 임포트한다.
    • 그리고 13장에서 만들었던 것처럼 Sequential 모델을 만든다. 이전에는 완전 연결 층만 추가했지만, 이 예제에서는 합성곱을 위한 층을 추가한다.
from tensorflow.keras import layers, models

model = models.Sequential()
  • 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)))
  • 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 + 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)))
  • 풀링 층의 첫 번째 매개변수(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'))
  • 두 번째 합성곱 층의 출력 크기를 계산해 보자. 입력 크기는 12, 필터 크기는 5, 패딩은 0이고 스트라이드는 1이다.

o = {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)))
  • 여기서도 (2, 2) 크기로 풀링했기 때문에 특성 맵의 크기는 높이와 너비가 절반으로 줄어든다. 특성 맵의 개수는 변화가 없으므로 최종적으로 출력되는 특성 맵의 차원은 4 x 4 x 64가 된다.
  • 다음으로 완전 연결 층인 Dense 층에 연결하기 위해 4 x 4 x 64 차원의 텐서를 일렬로 펼쳐야 한다. 케라스 API는 이런 작업을 위해 Flatten 클래스를 제공한다.
    • 이 클래스는 매개변수가 필요하지 않다. 모델에 추가하면 이전 층의 출력을 일렬로 펼치는 작업을 한다. 당연하게 학습되는 가중치도 없다.
model.add(layers.Flatten())
  • 4 x 4 x 64 크기의 텐서를 펼쳤으므로 1,024차원의 텐서가 되었다. 이를 1,024개의 유닛을 가진 완전 연결 층에 연결하겠다. 이 층의 활성화 함수도 렐루 함수를 사용한다.
model.add(layers.Dense(1024, activation='relu'))
  • 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))
  • Dropout 클래스에는 유닛을 끌 확률을 매개변수로 지정한다. 편리하게도 fit 메서드에서만 드롭아웃이 적용된다. 테스트나 평가를 위해 따로 모델을 구성할 필요는 없다.
  • 마지막 층은 열 개의 손글씨 숫자에 대한 확률을 출력해야 하므로 열 개의 유닛을 가진 완전 연결 층이다. 다중 분류 문제를 위한 활성화 함수는 소프트맥스 함수이므로 activation 매개변수를 ‘softmax’로 설정한다.
model.add(layers.Dense(10, activation='softmax'))
  • 이 층의 가중치 개수는 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

합성곱 신경망 모델 훈련

  • 이제 모델을 컴파일할 차례이다.
    • 다중 분류 작업이므로 손실 함수는 이전 장에서 사용했던 것처럼 categorical_crossentropy를 사용한다.
    • 옵티마이저는 adam을 사용하겠다. 또 손실 점수와 더불어 정확도 값을 계산하기 위해 metrics 매개변수에 ‘acc’를 추가했다.
model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])
  • 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()]
  • 체크포인트 콜백은 검증 손실(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)
  • 훈련 데이터와 검증 데이터는 앞서 준비했던 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
  • 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()

  • 정확도 그래프는 다음과 같다.
plt.plot(epochs, history.history['acc'])
plt.plot(epochs, history.history['val_acc'])
plt.xlabel('epochs')
plt.ylabel('accuracy')
plt.show()

  • 텐서보드를 실행하고 브라우저에 http://localhost:6006/에 접속하면 훈련 과정의 손실과 정확도 그래프를 볼 수 있다.
    • 그래프 탭에 타나난 합성곱 신경망의 구조는 아래 그림과 같다.
tensorboard --logdir logs/

  • 훈련된 모델을 사용해서 테스트 세트를 평가하기 전에 모델과 가중치를 저장하자.
model.save('cnn_model.h5')
  • load_model 함수를 사용하여 저장된 모델을 불러 새로운 모델 객체를 만들고 이전에 훈련 과정에서 가장 높은 성능의 가중치가 저장된 체크포인트 파일을 복원한다.
from tensorflow.keras.models import load_model

restored_model = load_model('cnn_model.h5')
restored_model.load_weights('cnn_checkpoint.h5')
  • 복원된 restored_model을 사용하여 테스트 세트에서 평가해 보자.
restored_model.evaluate(X_test_centered, y_test_onehot)

### 결과
# =========] - 1s 62us/sample - loss: 0.0141 - acc: 0.9911
  • 99%가 넘는 예측 정확도가 나오는데 이는 13장에서 피드포워드 신경망으로 얻은 결과보다 훨씬 뛰어난 정확도이다.
  • 테스트 샘플 중 처음 열 개의 예측을 직접 확인해 보면 다음과 같다.
print(np.argmax(restored_model.predict(X_test_centered[:10]), axis=1))

### 결과
# [7 2 1 0 4 1 4 9 5 9]
  • 손글씨 숫자의 예제는 레이블 인덱스와 레이블 값이 같다. 일반적으로 두 값은 다르기 때문에 argmax 함수에서 반환된 인덱스를 사용해서 진짜 클래스 레이블을 구해야 한다.
    • 비교를 위해 처음 열 개의 테스트 레이블을 확인해 보자
print(y_test[:10])

### 결과
# [7 2 1 0 4 1 4 9 5 9]
  • 예측 결과와 모두 동일하다. 이 열 개의 숫자가 어떤 모습인지 확인하기 위해 샘플 이미지를 그려보자.
    • 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))

  • 출력된 결과를 보면 이 모델의 성능이 매우 뛰어나다는 것을 알 수 있다.

활성화 출력과 필터 시각화

  • 첫 번째 합성곱 층의 출력을 이미지로 시각화해 보자.
    • model 객체에 추가한 층은 layers 속성으로 참조할 수 있다. 첫 번째 층의 객체를 추출해서 출력해 보자.
first_layer = model.layers[0]
print(first_layer)

### 결과
# <tensorflow.python.keras.layers.convolutional.Conv2D object at 0x0000021BB186F988>
  • 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)
  • 첫 번째 합성곱 층을 추가할 때 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)
  • 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])

  • 숫자 7의 윤곽을 특성으로 잘 추출한 것으로 보인다.
  • 이번에는 네 번째 숫자의 특성 맵을 그려보자.
fig = plt.figure(figsize=(10, 15))

for i in range(32):
   fig.add_subplot(7, 5, i+1)
  plt.imshow(activation[3, :, :, i])

  • 특성 맵마다 조금씩 다른 숫자 0의 윤곽을 추출하고 있다. 특성 맵의 차이는 필터가 서로 다른 부분을 학습하기 때문이다.
  • 이번 에는 첫 번째 층의 필터를 출력해 보겠다.
    • 합성곱 필터는 합성곱 층의 kernel 속성에 저장되어 있다. 필터의 차원은 (높이, 너비, 입력 채널, 출력 채널)이다.
fig = plt.figure(figsize=(10, 15))

for i in range(32):
   fig.add_subplot(7, 5, i+1)
    plt.imshow(first_layer.kernel[:, :, 0, i])

  • 필터의 밝은 부분이 높은 값을 의미한다. 예컨대 아홉 번째 필터는 수평 에지를 학습하는 것으로 보인다.
    • 이 필터를 사용하여 숫자 7에서 추출된 특성은 수평 부분이 잘 나타나 있다. 반면 0은 수평 부분이 많지 않으므로 추출된 특성에 정보가 많이 담겨 있지 않다.
  • 합성곱 활성화 출력과 필터를 분석하면 중요한 통찰을 얻을 수 이는 경우가 많다. 층이 깊어질수록 합성곱의 활성화 출력 의미를 이해하기 어렵다는 것을 기억하라.
[ssba]

The author

지성을 추구하는 사람/ suyeongpark@abyne.com

댓글 남기기

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.