본문 바로가기
  • 비둘기다
  • 비둘기다
  • 비둘기다
AI/Deep learning from Scratch

[머신러닝, 딥러닝] 합성곱 신경망 (3)

by parzival56 2023. 3. 1.

CNN 구현하기

이전까지 합성곱 신경망에 등장하는 계층들을 소개하고 이들을 구현해 보았습니다. 그럼 이제 손글씨 숫자를 인식하는 CNN을 조합하여 만들어보겠습니다.
 

위는 단순한 CNN 네트워크입니다. 
먼저 __init__으로 초기화를 하는 부분입니다. 

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)

위는 모두 params의 딕셔너리에 가중치와 편향을 저장하는 코드입니다.
다음은 위의 사진에서 본 계층들을 차례대로 생성해 줍니다.

        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()

순서가 있는 딕셔너리인 layers에 계층을 차례대로 입력해 줍니다. 그리고 마지막 계층인 softmax-with-loss는 last_layer라는 별도의 변수로 저장합니다. 흐름은 맨 위의 사진과 같습니다. 
변수들이 속한 계층을 한눈에 보면 다음과 같습니다.

Conv: (W1, b1, S, P), Pool: (PH, PW, S), Affine1: (W2, b2), Affine2: (W3, b3)

 
이상이 클래스의 초기화이고 이제 추론과 오차 그리고 기울기를 구현하기 위한 코드들입니다.

    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.last_layer.forward(y, t)

    def accuracy(self, x, t, batch_size=100):
        if t.ndim != 1:
            t = np.argmax(t, axis=1)

        acc = 0.0

        for i in range(int(x.shape[0] / batch_size)):
            tx = x[i*batch_size:(i+1)*batch_size]
            tt = t[i*batch_size:(i+1)*batch_size]
            y = self.predict(tx)
            y = np.argmax(y, axis=1)
            acc += np.sum(y == tt)

        return acc / x.shape[0]
        
    def gradient(self, x, t):
        """오차역전파법으로 기울기를 구함"""
        # 순전파
        self.loss(x, t)

        # 역전파
        dout = 1
        dout = self.last_layer.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

위의 4가지 함수들의 역할을 정리하면 다음과 같습니다.
 

predict : x가 각 layer을 거치면서 수행한 forward propagation의 결과 반환
loss : 예측값 y와 정답값 t을 last_layer(softmax with loss)에서 수행한 결과 반환
accuracy : batch에 따라 예측값 y와 정답값 tt를 비교했을 때의 정확도 반환
tx : x에서 batch만큼 가져온 것
tt: 정답값 t에서 batch만큼 가져온 것
y : tx에 대해 softmax를 처리한 예측값
acc : 예측값과 정답값 tt를 비교했을 때 정확도
gradient : 오차역전파법으로 기울기 구현


위에서 MNIST 데이터셋으로 CNN 학습을 진행해 보았는데 합성곱층의 가중치의 형상이 (30, 1, 5, 5)였습니다. 이는 채널이 1이기 때문에 그레이 스케일 이미지인데 여기서 학습 전과 후의 가중치를 비교하고 이를 시각화해 보겠습니다.
코드는 다음과 같습니다.

import numpy as np
import matplotlib.pyplot as plt
from simple_convnet import SimpleConvNet

def filter_show(filters, nx=8, margin=3, scale=10):
    """
    c.f. https://gist.github.com/aidiary/07d530d5e08011832b12#file-draw_weight-py
    """
    FN, C, FH, FW = filters.shape
    ny = int(np.ceil(FN / nx))

    fig = plt.figure()
    fig.subplots_adjust(left=0, right=1, bottom=0, top=1, hspace=0.05, wspace=0.05)

    for i in range(FN):
        ax = fig.add_subplot(ny, nx, i+1, xticks=[], yticks=[])
        ax.imshow(filters[i, 0], cmap=plt.cm.gray_r, interpolation='nearest')
    plt.show()


network = SimpleConvNet()
# 무작위(랜덤) 초기화 후의 가중치
filter_show(network.params['W1'])

# 학습된 가중치
network.load_params("params.pkl")
filter_show(network.params['W1'])

학습 이전에는 필터가 무작위로 초기화를 하고 있어서 흑백에 규칙성이 보이지 않습니다. 그러나 학습 이후에는 비교적 규칙성이 있는 이미지가 되었음을 알 수 있습니다. 바뀐 필터가 보고 있는 것은 에지와 블롭입니다. 에지는 색상이 바뀌는 경계선이고 블롭은 국소적으로 덩어리 진 영역을 말합니다. 에지에는 크게 두 가지가 있습니다. 하나는 가로 에지, 다른 하나는 세로 에지인데 이들은 각각 이미지를 가로 기준으로 보냐 세로 기준으로 보냐의 차이입니다. 그렇다면 왼쪽 절반이 흰색이고 오른쪽은 검은색인 필터는 세로 에지에 반응하는 필터라고 볼 수 있습니다.
 
- 층의 깊이에 따라서 추출의 정보가 변하기도 하는데 결과적으로는 층이 깊어질수록 추출되는 정보는 더욱 추상화된다는 것입니다.
 
마지막으로 대표적인 CNN들을 살펴보겠습니다.
 

1. LeNet

기존과의 차이라고 한다면 ReLU대신 시그모이드를 사용하고 원소를 줄이는데 풀링이 아닌 서브-샘플링 기법을 사용합니다. 
 

2. AlexNet

활성화 함수로 ReLU를 이용하고,  LRN으로 국소적 정규화를 실시하는 계층을 이용하고, 드롭아웃을 이용하여 효율적인 학습을 진행합니다.

 

이상으로 이전의 내용들을 종합하여 CNN을 구현해 보았습니다.

 
 

댓글