07.5 CNN 구현하기

합성곱 계층과 풀링 계층을 구현했으니, 이들 계층을 조합하여 손글씨 숫자를 인식하는 CNN을 조립해보겠습니다.

CNN 전체 연결 구조 지니가 Conv1-Relu1-Pool1에 이어 Affine-Relu2-Affine2-Softmax로 결합되는 전체 신경망의 파이프라인 흐름을 도식화해 삼총사에게 설명하고 있어요.

지니의 전체 CNN 레이어 아키텍처

  • 지니: “도로시, 우리가 순서가 있는 딕셔너리 layers에 합성곱(Conv), 활성화(ReLU), 풀링(Pooling) 계층을 차례로 쌓아올린 뒤에, 마지막 출력을 위해 AffineSoftmaxWithLoss 계층을 붙여주면 완전한 SimpleConvNet이 탄생하게 된단다!”
  • 도로시: “계층들이 순서대로 조립되어 있어서 predict 함수에서 단순히 for layer in self.layers.values(): x = layer.forward(x) 루프 한 번으로 순전파가 명쾌하게 실행되는구나!”

여기에서는 [그림 7-23]과 같은 CNN을 구현합니다.

그림 7-23 단순한 CNN의 네트워크 구성

그림 7-23

이 그림의 CNN 네트워크는 “Convolution-ReLU-Pooling-Affine-ReLU-Affine-Softmax” 순으로 흐릅니다. 이를 SimpleConvNet이라는 이름의 클래스로 구현하겠습니다.

우선 SimpleConvNet의 초기화(__init__)를 살펴봅시다. 초기화 때는 다음 인수들을 받습니다.

  • 초기화 때 받는 인수
    • input_dim - 입력 데이터(채널 수, 높이, 너비)의 차원
    • conv_param - 합성곱 계층의 하이퍼파라미터(딕셔너리). 딕셔너리의 키는 다음과 같다.
      • filter_num - 필터 수
      • filter_size - 필터 크기
      • stride - 스트라이드
      • pad - 패딩
    • hidden_size - 은닉층(완전연결)의 뉴런 수
    • output_size - 출력층(완전연결)의 뉴런 수
    • weight_init_std - 초기화 때의 가중치 표준편차

여기에서 합성곱 계층의 하이퍼파라미터는 딕셔너리 형태로 주어집니다(conv_param). 이것은 필요한 하이퍼파라미터의 값이 예컨대 {'filter_num': 30, 'filter_size': 5, 'pad': 0, 'stride': 1}처럼 저장된다는 뜻입니다.

자, SimpleConvNet의 초기화는 코드가 좀 길어지므로 세 부분으로 나눠 설명하겠습니다. 다음은 그중 첫 번째입니다.

class SimpleConvNet:
    def __init__(self, input_dim=(1, 28, 28),
                 conv_param={'filter_num':30, 'filter_size':5,
                             'pad':0, 'stride':1},
                 hidden_size=100, output_size=10, weight_init_std=0.01):
        filter_num = conv_param['filter_num']
        filter_size = conv_param['filter_size']
        filter_pad = conv_param['pad']
        filter_stride = conv_param['stride']
        input_size = input_dim[1]
        conv_output_size = (input_size - filter_size + 2*filter_pad) / \
                            filter_stride + 1
        pool_output_size = int(filter_num * (conv_output_size/2) *
                               (conv_output_size/2))

여기에서는 초기화 인수로 주어진 합성곱 계층의 하이퍼파라미터를 딕셔너리에서 꺼냅니다(나중에 쓰기 쉽도록). 그리고 합성곱 계층의 출력 크기를 계산합니다. 이어서 다음 코드는 가중치 매개변수를 초기화하는 부분입니다.

        self.params = {}
        self.params['W1'] = weight_init_std * \
                            np.random.randn(filter_num, input_dim[0],
                                            filter_size, filter_size)
        self.params['b1'] = np.zeros(filter_num)
        self.params['W2'] = weight_init_std * \
                            np.random.randn(pool_output_size,
                                            hidden_size)
        self.params['b2'] = np.zeros(hidden_size)
        self.params['W3'] = weight_init_std * \
                            np.random.randn(hidden_size, output_size)
        self.params['b3'] = np.zeros(output_size)

학습에 필요한 매개변수는 1번째 층의 합성곱 계층과 나머지 두 완전연결 계층의 가중치와 편향입니다. 이들 매개변수를 인스턴스 변수 params 딕셔너리에 저장합니다. 1번째 층의 합성곱 계층의 가중치를 W1, 편향을 b1이라는 키로 저장합니다. 마찬가지로 2번째 층의 완전연결 계층의 가중치와 편향을 W2b2, 마지막 3번째 층의 완전연결 계층의 가중치와 편향을 W3b3라는 키로 각각 저장합니다.

마지막으로 CNN을 구성하는 계층들을 생성합니다.

        self.layers = OrderedDict()
        self.layers['Conv1'] = Convolution(self.params['W1'],
                                           self.params['b1'],
                                           conv_param['stride'],
                                           conv_param['pad'])
        self.layers['Relu1'] = Relu()
        self.layers['Pool1'] = Pooling(pool_h=2, pool_w=2, stride=2)
        self.layers['Affine1'] = Affine(self.params['W2'],
                                        self.params['b2'])
        self.layers['Relu2'] = Relu()
        self.layers['Affine2'] = Affine(self.params['W3'],
                                        self.params['b3'])

        self.last_layer = SoftmaxWithLoss()

순서가 있는 딕셔너리(OrderedDict)인 layers에 계층들을 차례로 추가합니다. 마지막 SoftmaxWithLoss 계층만큼은 last_layer라는 별도 변수에 저장해둡니다.

이상이 SimpleConvNet의 초기화입니다. 이렇게 초기화를 마친 다음에는 추론을 수행하는 predict 메서드와 손실 함수의 값을 구하는 loss 메서드를 다음과 같이 구현할 수 있습니다.

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)
        return x
        
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

이 코드에서 인수 x는 입력 데이터, t는 정답 레이블입니다. 추론을 수행하는 predict 메서드는 초기화 때 layers에 추가한 계층을 맨 앞에서부터 차례로 forward 메서드를 호출하며 그 결과를 다음 계층에 전달합니다. 손실 함수를 구하는 loss 메서드는 predict 메서드의 결과를 인수로 마지막 층의 forward 메서드를 호출합니다. 즉, 첫 계층부터 마지막 계층까지 forward를 처리합니다.

이어서 오차역전파법으로 기울기를 구하는 구현은 다음과 같습니다.

    def gradient(self, x, t):
        # 순전파
        self.loss(x, t)

        # 역전파
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        grads['W1'] = self.layers['Conv1'].dW
        grads['b1'] = self.layers['Conv1'].db
        grads['W2'] = self.layers['Affine1'].dW
        grads['b2'] = self.layers['Affine1'].db
        grads['W3'] = self.layers['Affine2'].dW
        grads['b3'] = self.layers['Affine2'].db

        return grads

매개변수의 기울기는 오차역전파법(역전파)으로 구합니다. 이 과정은 순전파와 역전파를 반복합니다. 지금까지 각 계층의 순전파와 역전파 기능을 제대로 구현했다면, 여기에서는 단지 그것들을 적절한 순서로 호출만 해주면 됩니다. 마지막으로 grads라는 딕셔너리 변수에 각 가중치 매개변수의 기울기를 저장합니다. 이상이 SimpleConvNet의 구현입니다.

이제 이 SimpleConvNet으로 MNIST 데이터셋을 학습해볼 차례입니다. 학습을 위한 코드는 “4.5 학습 알고리즘 구현하기”에서 설명한 것과 거의 같으니, 지면도 아낄 겸 코드는 생략하겠습니다(해당 소스 코드는 ch07/train_convnet.py에 있습니다).

자, SimpleConvNet을 MNIST 데이터셋으로 학습하면 훈련 데이터에 대한 정확도는 $99.82\%$, 시험 데이터에 대한 정확도는 $98.96\%$가 됩니다(학습별로 정확도에는 약간의 오차가 발생합니다). 시험 데이터에 대한 정확도가 무려 $99\%$라는 것은 비교적 작은 네트워크로서는 아주 높다고 할 수 있습니다. 다음 장에서는 계층을 더 깊게 하여 시험 데이터에 대한 정확도가 $99\%$를 넘는 네트워크도 구현해볼 겁니다.

MNIST CNN 99퍼센트 인식 성공 도로시와 토토가 전광판에 표시된 MNIST 시험 정확도 99% 달성 결과를 보며 서로 기쁘게 박수를 치고 있어요.

도로시의 99% 분류 정확도 환호

  • 도로시: “지니! 우리가 직접 조립한 SimpleConvNet이 MNIST 손글씨 시험 데이터에서 정확도 99%를 기록했어! 완전연결 층만 썼을 때보다 정확도가 껑충 뛰어올랐어!”
  • 지니: “축하해, 도로시! 합성곱과 풀링 계층이 이미지의 가로·세로 연결 관계(공간 정보)를 제대로 보존하며 학습했기 때문에 얻은 값진 성과란다.”

지금까지 살펴본 것처럼 합성곱 계층과 풀링 계층은 이미지 인식에 필수적인 모듈입니다. 이미지라는 공간적인 형상에 담긴 특징을 CNN이 잘 파악하여 손글씨 숫자 인식에서 높은 정확도를 달성할 수 있었습니다.

CNN 하이퍼파라미터 튜닝 지니가 필터 개수(filter_num), 필터 크기(filter_size), 패딩(pad), 스트라이드(stride) 다이얼이 달린 제어 장치를 도로시에게 건네주며 하이퍼파라미터 튜닝 시범을 보이고 있어요.

지니의 합성곱 튜닝 요령

  • 지니: “도로시, CNN 모델의 표현력을 높이려면 필터의 개수를 늘리거나 크기를 조정하는 하이퍼파라미터 튜닝 다이얼을 세밀하게 조율해 줘야 해. 이때도 출력 공식인 $OH, OW$가 정수로 딱 떨어지는지 항상 확인하며 맞춰가야 한단다.”
  • 도로시: “입력 형상에 맞춰 필터 수와 크기를 최적화해 주는 게 핵심이네! 좋아, 개념을 확실히 정리했어!”
  • 토토: “왈왈! (다이얼 앞에서) 튜닝도 척척이다 멍!”
서브목차