인공지능/딥러닝 직접 구현하기 프로젝트

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

 

이번 시간에는 Convolution 연산 (Forward, Backward)에 대해 다뤄보겠습니다.

 

Convolution Layer

Convolution 연산은 본질은 굉장히 단순합니다. filter를 왼쪽에서 오른쪽으로, 위에서 아래로 이미지에 대고 점곱을 하면 되기 때문입니다.

하지만, 이는 생각보다 그리 단순하지 않습니다.

우선, 위의 이미지의 경우는 단순히 1장의 1-Channel 이미지의 예시이기 때문에 간단하게 대략 이미지의 크기만큼만 연산을 해 주면 됩니다.

하지만, 우리가 train할 때 사용하는 이미지는 4차원 (batch_size, channel, h, w)이므로 연산이 매우 복잡해 집니다.

예를 들어, 위의 2차원 이미지는 단순히 2중 for문으로 구현이 가능하지만, 4차원의 경우에는 4중 for문이 필요하게 됩니다.

 

또, 시간 복잡도도 너무 커집니다.

저번 차시에 썼던 out_h, out_w를 가져와서 설명해 보겠습니다.

간단하게 생각해서, pad=0, stride=1인 경우, out_w는  1 + w - fw가 되고, out_h는 1 + h - fh가 됩니다.

이 때, 이미지 크기 h, w에 대하여, 단 한 장의 2차원 이미지의 연산을 위해서 out_w * out_h만큼의 점곱 연산이 필요합니다.

MNIST dataset의 이미지 크기가 28*28인 것을 생각해 보면, (3, 3) 필터를 사용하였을 경우 단 한장의 이미지의 convolution을 위해서는 무려 26*26, 즉 676회의 점곱 연산이 필요합니다.

만약 50000개의 이미지 전체를 모두 Convolution 연산을 한다면, 676*50000, 즉 33,800,000회라는 무시무시한 양의 연산이 필요하게 됩니다.

만약 Convolution Layer가 두개가 겹쳐져서 Channel이 32개가 되었다면? 저 횟수의 32배를 연산해야 하는 것이죠.

게다가 컴퓨터 (또는 numpy libarary)는 점곱 연산은 아주 빠르게 수행하지만, 해당 점곱을 각각의 배열에 집어넣고 더하고... 하는 과정은 상대적으로 느리게 수행됩니다.

따라서, 점곱 연산의 횟수를 줄이는 것이 시간 복잡도와, 우리 코드의 복잡도 모두를 해결할 수 있습니다.

 

 

 

그렇다면 어떻게 시간 복잡도를 최적화 할 수 있을까요? 잠시 생각해 봅시다.

 

우선, 위 연산의 본질을 생각해 봅시다.
필터의 각각의 부분 (위 이미지에선 1, 2, 3, -4, 7, 4, 2, -5, 1)은 사실, 이미지의 특정 픽셀들과만 연산을 하게 됩니다.
가령, 필터 왼쪽 위 1은 이미지의 우측 하단 부분을 아예 연산하지 않습니다.

즉, filter의 각 부분은 애초에 이미지의 어떤 부분과 곱셈을 수행하는지 정해져 있다는 것입니다.

 

그렇다면, filter의 각 부분에 연산해야 할 부분만을 빼서, 각 필터와의 단 한번의 곱셈으로 바꾸면 어떨까요?

filter의 각 부분에 해당하는 부분들을 각각 배서, filter와의 점곱으로 연산을 하면 단 한번의 점곱으로 마무리 지을 수 있지 않을까요?

 

 

Im2col

해당 방식을 구체화 한 것이 바로 Im2col 알고리즘입니다.

 

Im2col 방식은, 말 그대로 Image를 Column으로 바꾸는 알고리즘입니다.

4차원 이미지인 input image와 filter를 둘 다 2차원 행렬로 만들어서, 점곱으로 연산이 가능하게 하는 것입니다.

 

위 이미지를 예시로 설명하겠습니다.

위 이미지에서는 빨간색, 초록색, 파란색으로 channel을 구분하고 있습니다.

첫 번째 channel의 filter인 F0은 D0, D1, D3, D4와만 연산이 되니, 해당 이미지 값들을 빼서 하나의 Column으로 만들어 줍니다.

이 행동을 모든 filter F0, F1, F2, F3에 반복해 줍니다.

그렇게 하면 오른쪽처럼 이미지의 Column이 생성됩니다.

또, 이를 각각의 filter마다, 각각의 channel마다 반복해 주면, 행렬 두개가 만들어지게 됩니다.

(참고로, 모든 필터의 각각의 부분은 모두 동일한 부분과만 연산하므로, filter의 행렬은 filter를 reshape한 것과 동일한 결과가 나오게 됩니다.)

 

이 방법은 사실, 원래 단 한번씩만 등장하던 이미지의 픽셀값이 여러 번 중복되어 여러번 나타나기 때문에 공간 복잡도가 커지게 된다는 단점이 있습니다.

하지만, 그렇게 해서 낭비되는 공간보다 아낄 수 있는 시간 복잡도가 훨씬 높기에(원본 conv의 약 200배 속도), 이 방식은 자주 사용됩니다.

 

 

이제, 이 Im2col 함수를 코드로 구현해 볼까요?

 

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_data.shape
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1

    img = np.pad(input_data, [(0, 0), (0, 0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N * out_h * out_w, -1)
    return col

코드를 살펴봅시다.

 

우선, input data에서 이미지의 개수, channel, 그리고 가로세로 길이를 구한 뒤 out_h, out_w를 지정해 줍니다.

그 뒤, input_data에 padding을 해 줍니다. (np.pad...)

 

다음으로, 이미지의 행렬을 담을 col을 ((이미지 크기, channel, filter 세로, filter 가로, out 세로, out 가로))의 크기로 만들어 줍니다.

이 크기로 만들어 주는 이유는, 어차피 모든 이미지 개수(N)와 channel(C)마다 동일한 연산을 해 줄 것이고, filter 하나의 행렬의 크기가 바로 (filter_h, filter_w)이기 때문입니다.

 

 

그리고, filter_h와 filter_w의 크기에 대하여, for loop를 돌아줍니다.

각 filter의 원소마다 연산해 줄 이미지를 추출하기 위함입니다.

col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]가 바로 이미지를 추출하는 부분입니다.

y:y_max:stride는, y에서부터 y_max까지, stride식 건너뛰면서 index를 추출하라는 의미입니다.

(참고로, col의 순서를 위처럼 한 것도 이런 대입 연산이 필요하기 때문입니다.)

 

마지막으로, 이 column을 reshape해주기 위해 transpose해 줍니다.

이렇게 transpose하지 않으면 reshape 시에 원래 우리가 의도했던 행렬이 아니라, 어딘가 뒤틀린 행렬이 됩니다.

 

이렇게 만들어진 col을 return하면, 이 함수의 일이 끝나게 됩니다.

 

 

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    N, C, H, W = input_shape
    out_h = (H + 2 * pad - filter_h) // stride + 1
    out_w = (W + 2 * pad - filter_w) // stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2 * pad + stride - 1, W + 2 * pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride * out_h
        for x in range(filter_w):
            x_max = x + stride * out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

이 im2col의 자매품? 으로, col2im이라는 함수도 있습니다.

Convolution의 backward 연산에 활용되는 함수로, im2col로 만들어진 column에서 원본 이미지로 바꿔주는 함수입니다.

위의 im2col을 거의 그대로 반대로 했을 뿐이므로, 설명은 생략하겠습니다.

 

 

이렇게, Convolution 연산의 준비가 모두 끝났습니다! 이제 직접 Convolution 연산을 구현해 봅시다.

 

    def forward(self, x):
        # Convolution Calculation
        self.x = x
        fn, fc, fh, fw = self.param.shape
        n, c, h, w = x.shape

        out_h = int(1 + (h + 2 * self.pad - fh) / self.stride)
        out_w = int(1 + (w + 2 * self.pad - fw) / self.stride)

        # Conv Input Size: (Channel, Filter_num, kernel_h, kernel_w)
        # Change this using im2col
        col = im2col(x, self.kernel_size[0], self.kernel_size[1], self.stride, self.pad)
        col_param = self.param.reshape((fn, -1)).T

        self.col = col
        self.col_param = col_param
        out = np.dot(col, col_param)
        out = out.reshape(n, out_h, out_w, -1).transpose(0, 3, 1, 2)
        return out

윗부분 (~out_w... 부분까지)는 지금까지 계속 설명해 왔던 것이므로 설명을 생략하고, 연산 부분만 간단히 보겠습니다.

 

우선, im2col 함수를 활용하여 input_data인 x를 column으로 바꿔 줍니다.

그리고, Convolution parameter, 즉 filter 또한 행렬로 바꿔주어야 합니다.

그런데 이 연산은 매우 간단하게 수행이 가능한데, 위에서 봤듯이 filter의 경우는 그냥 filter의 개수만큼 쭉 펴주기만 하면 됩니다.

그리고 그냥 점곱을 한번 해 주면, 간단하게 output 값이 나오게 됩니다.

 

이제, 이 output 값을 원래 크기대로 reshape 및 transpose해주기만 하면, forward 연산은 종료됩니다.

 

    def backward(self, dout):
        fn, c, fh, fw = self.param.shape
        dout = dout.transpose(0, 2, 3, 1).reshape(-1, fn)

        self.grad = np.dot(self.col.T, dout)
        self.grad = self.grad.transpose(1, 0).reshape(fn, c, fh, fw)

        dcol = np.dot(dout, self.col_param.T)
        dx = col2im(dcol, self.x.shape, fh, fw, self.stride, self.pad)

        return dx

backward 연산도 비교적 간단합니다.

forward와 반대로, 들어온 dout 값을 transpose한 뒤 reshape해 줍니다.

그리고, MulLayer의 backward함수와 동일하게 (어차피 곱하기만 하므로...) grad를 구해주면 됩니다.

사실 연산 자체는 점곱을 하는 MulLayer와 동일하지만, 단지 transpose와 reshape, col2im만 더해졌다고 보면 됩니다.

 

 

 

이렇게, Convolution Layer의 forward와 backward 연산을 마칩니다.

다음 시간에는 Pooling Layer (Maxpooling, Average Pooling...)을 구현해 보겠습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

 

이번 시간부터는, Convolutional Neural Network를 구현해 보겠습니다.

그리고 이번 차시에서는 우선 간단하게 Convoluton Layer을 구현해 보도록 합시다.

 

 

 

Convolution Layer

Convolution Layer는 일반적으로는 5*5, 3*3, ... 정도의 크기를 가진 filter를, 대략 32개, 64개, ... 만큼 가지게 됩니다.

원래의 Mul Layer이나 Add Layer들이 1차원 크기의 parameter를 가진 것과 다르게, 이 친구는 무려 3차원 크기 (channel도 고려하면 4차원 크기)의 parameter를 가지게 됩니다.

그 점을 반영해서, 일단 ConvLayer의 Init부분부터 짜봅시다.

 

class ConvLayer:
    def __init__(self, filters, kernel_size, stride=1, pad=0, initializer='he', reg=0):
        self.activation = False
        self.reg = reg
        self.x = None
        self.param = None
        self.grad = None
        self.stride = stride
        self.pad = pad
        self.init = initializer
        self.kernel_size = kernel_size
        self.filters = filters
        self.col = None
        self.col_param = None
        if type(kernel_size) == int:
            kernel_size = (kernel_size, kernel_size)
        self.out = (filters, *kernel_size)

원래 다른 Layer이 공통적으로 가지는 reg, x, ... 등을 제외하고, 지금 단계에서 봐야 할 변수들은 (일단은) kernel_size, filters, self.out이 되겠습니다.

 

kernel_size는 Convolution Layer의 크기를 의미하고, filters는 filter의 개수를 의미합니다.

저 아래 if type... 부분은, kernel_size를 int형으로 입력했을 경우 out을 제대로 넣기 위한 부분입니다.

보통 conv layer는 3*3, 4*4... 와 같이 정사각형 모양이기에, 저렇게 input을 받아도 되도록 만들어 두었습니다.

 

self.out은 parameter가 가지는 shape를 의미합니다.

 

 

 

다음은, train 함수에서 이 Layer가 연산되기 위해 parameter size를 변환하는 코드를 보겠습니다.

    def train(self, x_train, y_train, optimizer, epoch, learning_rate, skip_init=False):
        if not skip_init:
            in_size = x_train.shape[1:]
            for name in self.layers:
                if not self.layers[name].activation:
                    out_size = (self.layers[name].out,)
                    if isinstance(self.layers[name], ConvLayer):
                        out_size = out_size[0]
                        fn, fh, fw = out_size
                        c, h, w = in_size
                        size = (fn, c, fh, fw)
                        out_h = int(1 + (h + 2 * self.layers[name].pad - fh) / self.layers[name].stride)
                        out_w = int(1 + (w + 2 * self.layers[name].pad - fw) / self.layers[name].stride)
                        in_size = (fn, out_h, out_w)

코드를 잠깐 설명해 보겠습니다.

 

우선, out_size[0]은 아까 받았던 self.out을 꺼내줍니다.

그러면, filter의 개수, filter의 높이, filter의 너비를 알 수 있습니다.

또한, in_size 안에는 input image의 channel, 높이, 너비를 알 수 있습니다.

 

그리고, 우리는 필터의 크기를 (filter 개수, channel, 높이, 너비)로 만들 것입니다.

(참고: channel을 위처럼 앞에 놓는 것을 channel_first라 하고, (fn, fh, fw, c) 처럼 뒤에 놓는 것을 channel_last라고 합니다. 이번 구현에선 channel_first로 구현합니다.)

 

다음은 out_h, out_w입니다.

위 out_h, out_w는 Convolution 연산 이후에 나오는 output의 크기입니다.

아직 우리는 padding layer와 stride를 구현하진 않았으나, 그냥 있는 셈 치고 out_h와 out_w를 계산해 봅시다.

 

이미지의 가로 크기를 w, filter의 가로 크기를 fw라 해 봅시다.

만약 이미지를 padding한다면 가로 크기는 w + 2*pad가 될 것이고, stride=1인 상태라면 연산 횟수는 1 + w + 2*pad - fw가 될 것입니다.

하지만 여기서 stride까지 생각해 준다면, 연산 횟수는 stride의 크기만큼 나눠준 값으로 바뀔 것이므로,

stride를 포함한 연산 횟수는 1 + (w + 2 * pad - fw) / stride가 됩니다.

그리고, output의 크기는 연산 횟수와 동일하므로, out_w는 위의 식으로 정리가 됩니다.

마찬가지의 방식으로, out_h도 동일하게 계산이 가능합니다.

 

그 뒤, 다음 layer가 받을 in_size의 크기를 (fn, out_h, out_w)로 두면서 size 연산을 끝내 줍니다.

 

 

그런데, 이렇게 연산을 하기 위해서는, Flatten Layer도 필요합니다.

사실 지금까지는 우리가 이미 Flatten된 MNIST dataset을 사용하고 있었기 때문에 Flatten Layer가 필요하지 않았지만, Convolution Layer를 구현하기 위해선 Flatten Layer가 필요합니다.

4차원의 이미지를, Fully-connected Layer 및 Softmax 함수가 받을 수 있게 1차원 벡터화 시키는 것입니다.

 

Flatten 구현은 간단하니 지금 바로 해봅시다.

class Flatten:
    def __init__(self):
        self.shape = None
        self.activation = True

    def forward(self, x):
        return x.reshape(x.shape[0], -1)

    def backward(self, dout):
        return dout.reshape(-1, *self.shape)

 

예, 그렇습니다. forward와 backward는 그저 받았던 input을 1차원으로 바꿔주고, 돌아오는 dout을 원래 형태로 펴주기만 하면 됩니다.

그러기 위해서, self.shape를 만들어서 train 전처리 과정에서 shape를 지정해 주도록 합시다.

 

                elif isinstance(self.layers[name], Flatten):
                    self.layers[name].shape = in_size
                    tmp_size = 1
                    for items in in_size:
                        tmp_size *= items
                    in_size = (tmp_size,)

(Model.py - train함수 - if not activation 뒷부분)

Flatten 함수는 이전 Layer의 input size - in_size를 shape로 가지게 됩니다.

그리고, 해당 shape를 가졌을 때의 1차원 벡터의 크기는 shape에 있는 모든 원소의 곱이므로, shape의 원소를 죄다 곱해 줍니다.

그리고 해당 값을 다음 layer의 in_size로 바꿔주면 끝입니다.

 

 

이제 Convolution Layer의 연산을 다룰 차례입니다만, 이 부분은 다음 포스트에서 더 자세히 다루도록 하겠습니다.

Convolution Layer의 연산이 그렇게까지 단순하게 이뤄지진 않기 때문입니다. (물론, 그냥 곱셈이긴 하지만 말이죠...)

 

 

 

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

 

 

이번에는 다음에 구현할 CNN의 구현을 위해, 코드를 예쁘게 바꿔 보는 시간을 가져보겠습니다.

당연히, 다른 이론 설명은 없습니다.

 

 

 

 

오늘 코드 정리의 첫 번째 목표는, 바로 모델 코드를 다음과 같이 바꾸는 겁니다.

model = Model()
model.addlayer(Layers.MulLayer(100, initializer='he', reg=0.01), name="w1")
model.addlayer(Layers.AddLayer(100), name='b1')
model.addlayer(Layers.ReLU(), name='relu1')
model.addlayer(Layers.Dropout(), name='dropout1')
# ...

어떻게 바뀌었나 잠깐 보면, 원래는 addlayer의 인자로 input_size를 주었다면, 이제는 Layers의 인자로 output_size만을 주게 됩니다.

또한, 그것 말고도 다른 initializer이나 regularizer 등도 Layer 안에 들어가게 되겠습니다.

 

 

 

이제 Layer에서, 어떻게 설계를 변경했는지 보겠습니다.

class MulLayer:
    def __init__(self, out, initializer='he', reg=0):
        self.x = None
        self.out = out
        self.param = None
        self.grad = None
        self.init = initializer
        self.reg = reg
        self.activation = False

    def forward(self, x):
        self.x = x
        return x.dot(self.param)

    def backward(self, dout):
        self.grad = np.dot(self.x.T, dout)
        return np.dot(dout, self.param.T)

(MulLayer과 Addlayer은 거의 동일하니 같이 설명하겠습니다.)

 

우선, MulLayer의 init에서 initializer, regularizer과 함께 activation function인지 아닌지 체크하는 변수가 들어가겠습니다.

이를 통해, layer의 선언만으로 initializer, regularizer를 확실히 설정할 수 있고, activation인지 아닌지를 굳이 함수 입력 시에 넣지 않아도 되게 만들었습니다.

그 외의 forward, backward는 동일합니다.

 

 

다음으로는, parameter의 초기화를 하게 됩니다.

이 parameter 초기화는 원래 addlayer에서 이뤄졌었으나, 이제는 input data가 입력되는 train에서 하게 되겠습니다.

 

    def addlayer(self, layer, name=None):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer

그 결과, addlayer은 다음과 같이 다이어트를 감행하게 되었습니다.

parameter의 선언부는 다 지우고, layers와 keys에 집어넣는 것들만 남게 되었습니다.

 

 

    def train(self, x_train, y_train, optimizer, epoch, learning_rate):
        in_size = x_train.shape[1:]
        for name in self.layers:
            if not self.layers[name].activation:
                out_size = self.layers[name].out
                if type(in_size) is int:
                    size = (in_size, out_size)
                else:
                    size = (*in_size, out_size)

                self.weight_decay_lambda[name] = self.layers[name].reg
                if isinstance(self.layers[name], AddLayer):
                    self.params[name] = np.zeros(self.layers[name].out)
                elif self.layers[name].init is 'xavier':
                    self.params[name] = xavier_initialization(size)
                elif self.layers[name].init is 'he':
                    self.params[name] = he_initialization(size)
                else:
                    self.params[name] = np.random.uniform(size)
                in_size = out_size

                self.layers[name].param = self.params[name]
        optimizer.train(x_train, y_train, epoch, learning_rate, self)

 

마지막으로, train 함수입니다.

이 부분에서 처음으로 input data가 들어오므로, 여기에서 parameter initialize를 하겠습니다.

코드를 대충 설명해 보자면...

우선, input size는 처음에 x_train.shape[1:] (이미지 개수를 제외한 픽셀 수)와 동일합니다.

그리고, out_size는 layer에서 이미 선언해 주었으므로, 그대로 가져옵니다.

 

 

여기서, in_size가 튜플로 들어올 수도 있고, int로 들어올 수도 있습니다.

단순한 MulLayer, AddLayer을 지나왔다면 int, 픽셀 개수 (ex: (28, 28))로 그대로 들어온다면 튜플로 들어오겠습니다.

그래서, 이를 체크해 주며 parameter의 size를 결정해 주었습니다.

 

그 외의 나머지 부분은 다 비슷합니다.

 

 

다음 목표는, model save / load 기능 구현과 Model의 일부 변경입니다.

코드가 워낙 많이 바뀌어서, 그냥 바뀐 부분은 간결하게만 짚고 넘어가고, load/save 함수 중심으로 설명하겠습니다.

 

    def save(self, path=''):
        f = open(path + "model.txt", 'w')
        f.write(path + "weight.npz\n")

        params = {}
        for name in self.layers:
            data = self.layers[name].__class__.__name__ + "\n" + name + "\n"
            if not self.layers[name].activation:
                params[name] = self.layers[name].param
            f.write(data)
        np.savez(path + "weight", **params)

    def load(self, path):
        f = open(path)
        weight_path = f.readline()[:-1]
        load = np.load(weight_path)
        while True:
            layer = f.readline()[:-1]
            if not layer:
                break
            name = f.readline()[:-1]

            self.addlayer(eval(layer)(), name)
            if name in load:
                self.layers[name].param = load[name]

 

save함수는 위와 같이 구현했습니다. (Model.py)

path가 주어지면, 해당 path에 model.txt와 weight.npz를 생성합니다.

model.txt에는 model의 전체적인 구조를 저장하고, weight.npz는 가중치의 numpy 배열을 저장합니다.

그리고, npz에 한번에 저장하기 위해 params dictionary를 생성해 줍니다.

 

그 뒤, self.layers에서 각각의 layer의 class명과, name을 하나씩 저장해 줍니다.

그러는 와중에, params에는 각각의 가중치값을 저장합니다.

 

그리고, 마지막으로 np.savez함수로 지금까지 모았던 params를 한번에 저장합니다.

 

 

load 함수는 이와 정 반대로 일어납니다.

weight_path(weight.npz 파일 경로)에서 np.load를 통해 numpy array를 불러옵니다.

그리고, model.txt를 읽어오면서 각각의 layer를 생성하고, 그 안에 불러왔던 array를 param에 넣습니다.

이렇게 지금까지 학습했던 가중치를 가져올 수 있습니다.

 

이 함수를 만들면서 바꿔야 했던 함수들이 몇 개 있습니다.

간결하게 짚고만 넘어가겠습니다.

 

class AddLayer:
    def __init__(self, out=None, initializer='he', reg=0):
    # ...

class MulLayer:
    def __init__(self, out=None, initializer='he', reg=0):
    # ...

(Layers.py)

우선, AddLayer와 MulLayer에 out(즉, size)가 들어오지 않을 수 있습니다.

out=None으로 설정해 줘서, out size 없이도 load할 수 있도록 만들었습니다.

 

    def train(self, x_train, y_train, optimizer, epoch, learning_rate, skip_init=False):
        if not skip_init:
        # ...

(Model.py)

그리고, train에서의 init을 skip할 수 있도록 위와 같이 함수를 선언했습니다.

이제, train의 마지막 부분에 True를 넣으면 변수를 원래 가지고 있던 그대로 학습시킬 수 있습니다.

 

    def forward(self, x, train_flag=True):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            if isinstance(self.layers[key], Dropout):
                x = self.layers[key].forward(x, train_flag)
            else:
                x = self.layers[key].forward(x)
            if key in self.weight_decay_lambda:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]
        return x

    def predict(self, x):
        x = self.forward(x)
        self.pred = softmax(x)
        return self.pred

    def eval(self, x, y, epoch=None):
        x = self.forward(x, False)
        self.loss = self.layers[self.keys[-1]].forward(x, y)
        self.loss += sum(self.l2.values()) / 2
        self.pred = softmax(x)

        if epoch is None:
            print("ACC : ", (self.pred.argmax(1) == y.argmax(1)).mean())
            print("LOSS : ", self.loss)
        else:
            print("ACC on epoch %d : " % epoch, (self.pred.argmax(1) == y.argmax(1)).mean())
            print("LOSS on epoch %d : " % epoch, self.loss)

(Model.py)

또, 원래 predict였던 함수를 forward / predict / eval 로 나누어 주었습니다.

predict는 input의 정답을 예측하는 함순데, 지금까지 y를 넣지 않으면 작동하지를 않았거든요..

위와 같이 바꿔줌으로써, 이미지만 넣어도 예측을 할 수 있도록 바뀌었습니다.

또한, eval 함수를 새로 만들어서 간단하게 loss와 acc를 알 수 있도록 하였습니다.

위 두 함수, predict와 eval에서 겹치는, 첫 x값을 마지막 layer까지 순전파법으로 가져가는 부분은 forward 함수로 따로 뺐습니다.

 

    def train(self, x_train, y_train, epoch, learning_rate, model):
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            z = model.forward(x)
            model.loss = model.layers[model.keys[-1]].forward(z, y)
            model.loss += sum(model.l2.values()) / 2
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad + model.weight_decay_lambda[key] * model.params[key]
                    model.params[key] -= learning_rate * model.grads[key]

            if epochs % (epoch / 10) == 0:
                model.eval(x, y, epoch=epochs)
        model.eval(x_train, y_train)

(Optimizer.py - SGD)

바뀐 부분은, z = model.forward와 그 밑 두줄, 그리고 가장 아래의 model.eval()입니다.

SGD뿐만 아니라 다른 optimizer들에도 동일하게 적용하면 되겠습니다.

 

 

 

이렇게 코드 정리와 Model Save/Load를 구현해 보았습니다.

다음 차시부터는 CNN을 구현해 보도록 하겠습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

저번 시간에 optimizer를 만들며 기본적인 딥 러닝 함수들은 모두 만들었습니다.

저 함수들만 사용하더라도 꽤나 좋은 인공지능을 제작할 수 있겠죠.

 

하지만, 이번 시간에는 여기서 조금 더 나아가서 인공지능의 정확도를 더욱 높일 수 있는 방법들에 대해 설명하고, 구현해 보겠습니다.

 

 

가장 먼저 구현할 것은, 가중치의 초기화입니다.

지금까지 보신 분들은 아시겠지만, 지금까지는 가중치 초기화를 단순히 랜덤값으로 선언했었습니다.

하지만, 이렇게 단순 랜덤으로 가중치를 초기화하면 Vanishing Gradient가 발생할 가능성이 매우 커집니다.

Vanishing Gradient란, 딥 러닝에서 많은 레이어들을 거치며 가중치에 값을 곱하고 활성화 함수를 거치는 과정에서, 다수의 Gradient들이 0으로 수렴하게 되는 현상을 의미합니다.

그 뿐만 아니라, 가중치를 너무 크게 초기화하면 레이어들을 거치며 Gradient가 무한으로 발산하는 경우도 발생할 수 있습니다.

 

또한, Sigmoid함수의 경우는 Saturation이라는 문제도 발생합니다.

이는 Gradient의 값의 범위가 너무 클 때 생기는 문제인데, Sigmoid의 경우 값이 너무 작으면 0에 수렴하고, 값이 너무 크면 1에 수렴하는 특징을 가지고 있습니다.

하지만 만약 가중치의 값을 -1 ~ 1로 초기화를 해 버리면, 들어오는 데이터에 따라 w*x의 값이 매우 커질수도 있습니다.

만약 이미지의 크기가 10*10이고, 모든 이미지 픽셀이 1이고 가중치도 1이라면, w*x는 최대 100이라는 수치가 나옵니다.

이러한 Gradient는 어떻게 해도 1에 수렴하게 되기 때문에, 학습에 아무런 도움도 주지 않습니다.

반대로, 가중치가 -1이었다면 w*x는 최소 -100이라는 수치가 나오게 되고, 그러면 마찬가지로 Gradient가 0으로 수렴하게 됩니다.

 

Xavier Initialization은 이러한 Saturation의 문제점을 해결할 수 있는 Initialization 방법 중 하나입니다.

이 방식은 w*x의 값의 범위를 -1~1로 제한해줄 뿐만 아니라, 앞 레이어와 뒤 레이어의 분산까지 맞춰줌으로써 학습이 더욱 원활하게 되게 해 줍니다.

 

 

이론적인 이야기는 여기까지만 하고, 바로 구현에 들어가 봅시다.

 

    def addlayer(self, layer, activation=False, input_size=None, name=None, initialization=None):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)
            self.layers[name].param = self.params[name]

Model.py의 addlayer 부분입니다.

initialization이 'xavier'로 선언되었을 경우에 xavier_initialization으로 가중치 값을 초기화 하게 됩니다.

또한, AddLayer, 즉 bias의 경우는 처음부터 그냥 0으로 초기화해 주었습니다. (원래 bias는 random 초기화를 하지 않습니다)

 

 

def xavier_initialization(input_size):
    fan_in = input_size[0]
    fan_out = input_size[1]
    n = np.sqrt(6 / (fan_in + fan_out))
    w = np.random.uniform(-n, n, input_size)
    return w

다음은 위에서 사용한 xavier_initialization 함수 내부입니다.

uniform xavier 공식을 그대로 사용했습니다. (공식 유도까지 하면 너무 오래 걸리는 관계로 생략하겠습니다.)

 

또한, 위에서 언급한 Xavier initialization은 보통 activation function, 즉 활성화 함수가 sigmoid함수일 때 자주 사용합니다.

활성화 함수가 ReLU일 때는, He Initialization을 사용하는데, 크게 달라지는 점은 없으니 그냥 바로 코드로 넘어가겠습니다.

 

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            elif initialization is 'he':
                self.params[name] = he_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)

            self.layers[name].param = self.params[name]

...그냥 아까 윗부분에서 elif ... he: 부분만 추가했고,

 

def he_initialization(input_size):
    fan_in = input_size[0]
    n = np.sqrt(6 / fan_in)
    w = np.random.uniform(-n, n, input_size)
    return w

xavier_initialization 아래 부분에 그냥 하나 새로 만들었습니다.

 

이제, 이렇게 함으로써 다층 Neural Network를 구현할 때에도 Vanishing Gradient나 Saturating 현상이 덜 일어나게 될 것입니다.

 

 

 

다음으로, 정규화 (Regularization)을 구현해 보겠습니다.

 

Regularization이란, 인공지능의 Overfitting을 막기 위해 사용되는 기법으로, 너무 특정 데이터들에만 강력하게 학습되는 것을 막기 위한 장치입니다.

Regularization에는 여러 가지 종류 (L1, L2 ...) 등이 있는데, 이번 포스팅에서는 L2 Regularization에 대해서만 다루겠습니다.

(다른 Regularization은 직접 구현해 보세요!)

 

overfitting 예시

 

Regularization은 Loss값에 W(가중치)에 비례한 값을 더해서 overfitting을 막는 기법입니다. (L2의 경우, W^2에 비례합니다.)

즉, W값이 크다면 그만큼 Loss값은 커지고, W값이 작다면 그만큼 Loss값이 줄어드는 것이죠.

그런데 어째서 W값에 비례한 값을 Loss에 더해주는 걸까요?

이를 직관적으로 한번 이해해 봅시다.

 

 

우리의 인공지능이 특정 데이터에만 치중되어 학습된다는 것은 무엇을 의미할까요?

지금까지 해 왔던 MNIST 데이터셋으로 예시를 들어 보겠습니다.

 

만약 우리가 숫자 "2"를 쓴 이미지를 인공지능에 넣었다고 합시다.

그러면, 인공지능은 이 이미지에서 2와 관련된 픽셀값의 특징을 확인해 가며, "2"에 해당하는 답으로 가중치를 바꿀 것입니다.

그런데 만약 우리가 지금 막 넣어준 "2"의 사진에, 어떤 특정 사람의 필체로 인해 아주 구석탱이가 검은 색으로 칠해져 있다고 생각해 봅시다.

그러면, 인공지능은 구석탱이에 칠해진 검은색 하나만을 보고도 이 사진이 2임을 추측할 수 있습니다.

이런 경우엔, 해당 구석탱이의 검은색에는 "2"라는 라벨이 붙는 가중치가 엄청 커지게 되고, 그 이후부터는 구석의 검은색만 보고도 그냥 "2"라고 해석하게 되는 것입니다.

이렇게 되어버리면, 당연히 문제가 발생하게 되겠죠.

 

이를 방지하기 위해, 특정 가중치 값이 너무 커지는 것을 방지하게 됩니다.

특히 L2의 경우, 전체적으로 가중치가 큰 것보다는 어떤 특정 가중치가 무식하게 커지는 것을 막는 것을 확인할 수 있습니다. (W^2에 비례하는 걸 보면 알 수 있습니다.)

 

 

L2 Regularization의 식은 간단합니다 - 간단하게는, $\lambda W^2$가 됩니다.

그리고, 이 값을 Loss값에 더해주면 됩니다.

 

 

설명은 이렇게 간단하게(?)만 하고, 코드를 봅시다.

 

 

class Model:
    def __init__(self):
        self.params = {}
        self.grads = {}
        self.keys = []
        self.layers = {}
        self.num = 0
        self.l2 = {}
        self.weight_decay_lambda = {}
        self.loss = None
        self.pred = None

    def addlayer(self, layer, activation=False, input_size=None, name=None, initialization=None, reg=0):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer
        self.weight_decay_lambda[name] = reg

        if not activation:
            if isinstance(layer, AddLayer):
                self.params[name] = np.zeros(input_size)
            elif initialization is 'xavier':
                self.params[name] = xavier_initialization(input_size)
            elif initialization is 'he':
                self.params[name] = he_initialization(input_size)
            else:
                self.params[name] = np.random.uniform(-1, 1, input_size)

            self.layers[name].param = self.params[name]

    def predict(self, x, y):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            x = self.layers[key].forward(x)
            if key in self.l2:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]
        self.loss = self.layers[self.keys[-1]].forward(x, y)
        self.loss += sum(self.l2.values())/2
        self.pred = softmax(x)

이번엔 아예 Model Class 자체를 조금 바꿔야 합니다.

 

우선, L2 regularization 값들이 들어갈 self.l2를 선언해 주고 나서,

addlayer()에는 reg값을 추가했습니다. 이 reg값이 바로 위에서 언급했던 $\lambda$값입니다.

이 $\lambda$값들을 저장하는 변수 self.weight_decay_lambda도 추가해 주었습니다.

그 뒤, 마지막 predict에서 loss값에 l2의 값들의 합을 더해줍니다.

 

 

                if key in model.params:
                    model.grads[key] = model.layers[key].grad + model.weight_decay_lambda[key] * model.params[key]
                    model.params[key] -= learning_rate * model.grads[key]

optimizer.py의 SGD함수도 다음과 같이 바꿔줍시다.

비교해 보시면 아시겠지만, model.grads[key]의 update rule에 model.weight_decay_lambda와 관련된 식이 추가된 것을 볼 수 있습니다.

 

 

다음으로는 Dropout을 구현해 보겠습니다.

Dropout 또한 Overfitting을 막기 위해 고안된 녀석으로, 훈련 과정에서 뉴런들을 강제로 비활성화 시키며 학습을 진행합니다.

학습 시에는 조금 느리게 학습되나, overfitting을 매우 잘 막아줍니다.

 

바로 구현부로 넘어가 봅시다.

class Dropout:
    def __init__(self, dropout_rate=0.2):
        self.drop_rate = dropout_rate
        self.mask = None

    def forward(self, x, train_flag=True):
        if train_flag:
            self.mask = np.random.rand(*x.shape) > self.drop_rate
            return x * self.mask
        else:
            return x * (1.0 - self.drop_rate)

    def backward(self, dout):
        return dout * self.mask

Dropout은 새로운 Layer로, Model 사이사이에 끼워 넣어 주며 뉴런을 버리게 만들 것입니다.

drop_rate는 버리는 정도를 의미하고, mask는 실제 버리는 index값들을 넣어줍니다.

 

그리고, 이 Dropout은 훈련 중에는 계속 뉴런을 조금씩 버리면서 학습하지만, 훈련 중이 아닐 때는 (즉, test때는) 뉴런을 버리는 대신 drop_rate만큼 줄여서 모든 뉴런을 내보냅니다.

또, 역전파 시에는 forward때 보내줬던 만큼의 뉴런만을 받아서 뒤로 보냅니다.

 

 

    def predict(self, x, y, train_flag=True):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            if isinstance(self.layers[key], Dropout):
                x = self.layers[key].forward(x, train_flag)
            else:
                x = self.layers[key].forward(x)
            if key in self.l2:
                self.l2[key] = np.sum(np.square(self.params[key])) * self.weight_decay_lambda[key]

Model.py의 predict 함수도 위와 같이 수정해 줍시다.

이렇게 수정하게 되면, predict 시에 train_flag를 설정하여 dropout을 발동시킬지, 발동시키지 않을지 결정할 수 있습니다.

        model.predict(x_train, y_train, train_flag=False)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

Optimizer.py의 함수들에는 위와 같이, 최종 test 시에는 train_flag를 False로 바꾸어서 나오게 해야 합니다.

 

 

 

import numpy as np
import pandas as pd
from Model import Model
import Layers
import Optimizer

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot[:300]
y_test = one_hot[300:600]
x_test = x_train[300:600]
x_train = x_train[:300]

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 100), name="w1", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b1')
model.addlayer(Layers.ReLU(), activation=True, name='relu1')
model.addlayer(Layers.Dropout(), activation=True, name='dropout1')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w2", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b2')
model.addlayer(Layers.ReLU(), activation=True, name='relu2')
model.addlayer(Layers.Dropout(), activation=True, name='dropout2')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w3", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b3')
model.addlayer(Layers.ReLU(), activation=True, name='relu3')
model.addlayer(Layers.Dropout(), activation=True, name='dropout3')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w4", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b4')
model.addlayer(Layers.ReLU(), activation=True, name='relu4')
model.addlayer(Layers.Dropout(), activation=True, name='dropout4')
model.addlayer(Layers.MulLayer(), input_size=(100, 100), name="w5", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=100, name='b5')
model.addlayer(Layers.ReLU(), activation=True, name='relu5')
model.addlayer(Layers.Dropout(), activation=True, name='dropout5')
model.addlayer(Layers.MulLayer(), input_size=(100, 10), name="w6", initialization='he', reg=0.01)
model.addlayer(Layers.AddLayer(), input_size=10, name='b6')
model.addlayer(Layers.Dropout(), activation=True, name='dropout6')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')

optimizer = Optimizer.SGD(batch_size=128)
model.train(x_train, y_train, optimizer, 3000, 0.01)

model.predict(x_test, y_test, train_flag=False)
print("Final test_ACC : ", (model.pred.argmax(1) == y_test.argmax(1)).mean())
print("Final test_LOSS : ", model.loss)

최종적으로, 다음의 코드를 돌려보시기 바랍니다.

overfitting을 유도하기 위해 데이터를 300개 가량으로 줄였고, model의 크기는 늘렸습니다.

reg값을 바꿔도 보고, Dropout rate를 바꿔도 보고 하며 어떤 값들이 가장 overfitting을 줄이는지 확인할 수 있을 겁니다.

 

 

 

여기까지 기본적인 딥러닝 코드가 끝났습니다!

다음 시간부터는 CNN을 직접 구현해 보는 시간을 가지도록 하겠습니다!

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

* 시작하기에 앞서, 해당 포스트는 "Gradient Descent Optimization Algorithms 정리" 포스팅

(http://shuuki4.github.io/deep%20learning/2016/05/20/Gradient-Descent-Algorithm-Overview.html)

을 참조하였습니다. 구현 중인 알고리즘에 대해 알고 싶다면, 위 포스팅에서 확인해 주세요.

또한, 이번 차시에 구현할 optimizer는 위 링크의 Ada시리즈와 RMSProp을 구현할 것입니다.

 

 

 

자, 저번 시간에 이어, 이번에는 바로 Adagrad를 구현해 봅시다.

우선 AdaGrad의 식은 다음과 같습니다.

 

$$ G_t = G_{t-1} + (\nabla_\theta J(\theta_t))^2 $$

$$ \theta_{t+1} = \theta_t - \frac {\eta} {\sqrt{G_t + \epsilon}} \cdot \nabla_\theta J(\theta_t) $$

 

여기서, $\epsilon$은 분모가 0이 되는 것을 방지하기 위한 값입니다.

 

 

간단하게만 설명하자면, $G_t$는 저번의 velocity와 같이, params(또는 grads)만큼의 크기를 갖는 새로운 변수입니다.

학습하면 할수록, 계속해서 grads의 제곱의 합을 받아오는 변수입니다.

이 변수는 해당 grads의 절대값이 크면 G의 값을 크게, grads의 절대값이 작으면 G의 값을 작게 둡니다.

그리고 update rule에서 보면 알겠지만 G의 값이 크면 learning rate가 작고, G의 값이 작으면 learning rate가 큽니다.

 

우리의 변수 params들은 grads의 값이 크면 많이 움직이고, 값이 작으면 조금 움직이게 됩니다.

그러므로, 위의 G가 가지는 의미는 "이미 많이 움직인 params는 좀 덜 움직이고, 많이 안 움직였던 params은 더 크게 움직이자" 하는 뜻입니다.

 

저번의 NAG처럼 복잡하게 수식을 정리할 필요는 없으니, 빠르게 구현해 봅시다.

 

 

class Adagrad:
    def __init__(self, batch_size):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        for p in model.params:
            G[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    G[key] += np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), model.grads[key])

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

(아무래도 코드가 많이 중복되는 것을 보아하니, 코드를 다시 한번 리팩토링을 언젠가 해야겠습니다.)

 

이번에 볼 부분은, 다음과 같습니다.

 

1. __init__의 epsilon 등장

2. 변수 G의 등장

3. Update Rule의 적용

 

 

이 때, 변수 G는 저번 Momentum / NAG 때와 동일하게 선언해 주시면 됩니다. (결국 변수의 크기 자체는 동일하기 때문)

 

그리고, 식을 그대로 코드에 적용시켰습니다.

                    model.grads[key] = model.layers[key].grad
                    G[key] += np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), model.grads[key])

update의 식은 다음과 같습니다.

G에는 grads의 제곱을 계속해서 더해주고, params에는 위 계산식 그대로 넣어주시면 되겠습니다.

 

 

 

하지만, Adagrad에는 큰 단점이 있는데, 아무리 학습이 안되었어도 / 잘되었어도 가면 갈수록 계속해서 learning rate가 작아집니다.

G에는 계속해서 grad의 제곱이 더해지므로, 가면 갈수록 점점 더 G가 커지기 때문입니다.

 

이를 보완한 알고리즘 중 하나가 바로 RMSProp입니다.

 

RMSProp의 식은 다음과 같습니다.

$$ G_t = \gamma G_{t-1} + (1-\gamma)(\nabla_\theta J(\theta_t))^2 $$

$$ \theta_{t+1} = \theta_t - \frac {\eta} {\sqrt{G_t + \epsilon}} \cdot \nabla_\theta J(\theta_t) $$

 

여기서 gamma는 보통 0.9, 0.99 등의 값으로 많이 놓습니다.

또한, update rule은 동일함을 알 수 있습니다.

이렇게 구현하게 되면, gamma로 인해 G값이 무한으로 뻗어나가는 일은 생기지 않게 됩니다.

 

식에 큰 변화가 없으니, 그냥 간단하게 짜봅시다.

 

class RMSProp:
    def __init__(self, batch_size, gamma=0.9):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.gamma = gamma

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        for p in model.params:
            G[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    G[key] = self.gamma * G[key] + (1 - self.gamma) * np.square(model.grads[key])
                    model.params[key] -= np.multiply(learning_rate / (np.sqrt(G[key] + self.epsilon)), model.grads[key])

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

 

__init__ 부분에 gamma가 생겼다는 것, 그리고 식에 gamma와 관련된 식이 생겼다는 것을 제외하고는 모두 그대로입니다.

 

 

 

다음은 AdaDelta입니다.

 

AdaDelta는 위의 두 알고리즘과는 달리 식 4개로 이루어져 있습니다.

$$ G = \gamma G + (1 - \gamma ) (\nabla_\theta J(\theta))^2 $$

$$ \Delta_\theta = \frac{\sqrt{s+\epsilon}}{\sqrt{G+\epsilon}} \cdot \nabla_\theta J(\theta)$$

$$ \theta = \theta - \Delta_\theta $$

$$ s = \gamma s + (1 - \gamma) \Delta^2 _\theta $$

식을 딱 보았을 때의 첫느낌은 아마, "대체 뭐라는거지?" 일 것 같습니다.

일단 G의 식은 지금까지와 같으나, $\theta$를 update하는 과정에서 분자에 learning rate $\eta$가 들어가는 것이 아니라, 새로운 인자 $\sqrt{s+\epsilon}$이 들어갑니다.

또한 눈치챌 수 있는 점은, learning rate는 AdaDelta에서는 사용하지 않습니다.

즉, 따로 learning rate를 설정해 주지 않아도 작동을 한다는 뜻입니다.

 

대충 정리하자면, learning rate를 변수 $\sqrt{s+\epsilon} = \sqrt{\gamma s + (1 - \gamma) \Delta^2 _\theta + \epsilon}$ 가 대체한다는 것입니다.

$\Delta_\theta^2 = \frac{s+\epsilon}{G+\epsilon} \cdot (\nabla_\theta J(\theta))^2$ 이므로, s에 관한 식을 다시 풀어 쓴다면 $s = \gamma s + (1 - \gamma)(\frac{s+\epsilon}{G+\epsilon} \cdot (\nabla_\theta J(\theta))^2)$가 되겠습니다.

이것을 보면, G와 s가 매우 비슷한 방식으로 update된다는 것을 알 수 있습니다.

 

그리고, G와 s를 간단하게나마 설명하자면, G는 "지금까지 grads의 변화의 합", s는 "지금까지의 params의 변화의 합"이 됩니다.

즉, params가 크게 변화하면 learning rate (즉, $\sqrt{s+\epsilon}$값)이 커지고, params가 작게 변화하면 learning rate가 작아지는 것입니다.

 

이를 직관적으로 보자면, local minima에 갇혀 있다가 풀려난 경우에는 갑자기 params가 변화해야 하는 정도가 커지는데, 그렇게 된다면 자동으로 learning rate를 증가시켜 더욱 빠르게 (또, 관성을 갖게) 됩니다.

그리고, global minima에 가까워지면 저절로 params의 변화 정도가 적어지므로, learning rate를 감소시켜 더욱 세밀한 update가 가능해 지는 것입니다.

 

 

코드는 이번에도 크게 바뀌진 않습니다.

class AdaDelta:
    def __init__(self, batch_size, gamma=0.9):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.gamma = gamma

    def train(self, x_train, y_train, epoch, learning_rate, model):
        G = {}
        s = {}
        for p in model.params:
            G[p] = np.zeros(model.params[p].shape)
            s[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    G[key] = self.gamma * G[key] + (1 - self.gamma) * np.square(model.grads[key])
                    d_t = np.multiply(np.sqrt(s[key] + self.epsilon) / np.sqrt(G[key] + self.epsilon), model.grads[key])
                    s[key] = self.gamma * s[key] + (1 - self.gamma) * np.square(d_t)
                    model.params[key] -= d_t

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

 

__init__은 동일하고,

train 함수에서 s변수를 새로 만들어서 s값을 저장하게 합니다.

그리고, 계산 식은 위에서 쓴 식과 동일하게 적용했습니다.

(d_t는 $\Delta_\theta$를 줄여 쓴 것입니다.)

 

 

 

마지막으로, Adam에 대해 설명하겠습니다.

Adam은 RMSProp와 Momentum을 섞은 듯한 알고리즘입니다.

식으로 먼저 볼까요?

 

$$ m_t = \beta _1m_{t-1} + (1-\beta _1)\nabla_\theta J(\theta) $$

$$ v_t = \beta _2v_{t-1} + (1-\beta _2)(\nabla_\theta J(\theta))^2 $$

 

여기서 $\beta_1, \beta_2$는 위의 $\gamma$와 동일한 역할을 합니다.

그러면... 지금까지 RMSProp과 Momentum만 제대로 이해하셨다면 어떤 느낌인지 아실 겁니다!

이제 이것들을 사용해서 $\theta$를 update를 할 텐데, 왜 $\theta$의 update 식을 안썼을까요?

 

잘 생각해 보시면 알겠지만, m와 v값은 처음에 0으로 초기화 될 겁니다.

그런데, 위 식을 그대로 따라간다면, m값과 v값은 초기에 0에 매우 가깝게 다가가므로, 이 값들이 편향성을 갖게 됩니다.

그래서 위 식을 그대로 가져다 쓰지 않고, 다음과 같은 식을 사용합니다.

 

$$ \hat{m_t} = \frac{m_t}{1-\beta_1^t}$$

$$ \hat{v_t} = \frac{v_t}{1-\beta_2^t} $$

$$ \theta = \theta - \frac{\eta}{\sqrt{\hat{v_t}+\epsilon}}\hat{m_t} $$

 

그리고, 보통 $\beta_1 = 0.9, \beta_2 = 0.999, \epsilon = 10^{-8}$를 사용합니다.

 

 

자, 이제 이를 토대로 코드를 구현해 봅시다!

 

 

class Adam:
    def __init__(self, batch_size, beta_1 = 0.9, beta_2 = 0.999):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.epsilon = 10e-8
        self.beta_1 = beta_1
        self.beta_2 = beta_2

    def train(self, x_train, y_train, epoch, learning_rate, model):
        m = {}
        m_hat = {}
        v = {}
        v_hat = {}
        for p in model.params:
            m[p] = np.zeros(model.params[p].shape)
            m_hat[p] = np.zeros(model.params[p].shape)

            v[p] = np.zeros(model.params[p].shape)
            v_hat[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    m[key] = self.beta_1 * m[key] + (1 - self.beta_1)*model.grads[key]
                    m_hat[key] = m[key] / (1 - self.beta_1 * self.beta_1)
                    v[key] = self.beta_2 * v[key] + (1 - self.beta_2) * model.grads[key] * model.grads[key]
                    v_hat[key] = v[key] / (1 - self.beta_2 * self.beta_2)
                    model.params[key] -= learning_rate * m_hat[key] / np.sqrt(v_hat[key] + self.epsilon)


            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

사실 여기도 위 식을 그냥 그대로 갖다 박은거 밖에 없어서 설명할 게 딱히 없습니다.

 

 

다음 시간에는, Regularization을 비롯한 다양한 테크닉들을 적용해 보도록 하겠습니다!

 

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

 * 시작하기에 앞서, 해당 포스트는 "Gradient Descent Optimization Algorithms 정리" 포스팅

(http://shuuki4.github.io/deep%20learning/2016/05/20/Gradient-Descent-Algorithm-Overview.html)

을 참조하였습니다. 구현 중인 알고리즘에 대해 알고 싶다면, 위 포스팅에서 확인해 주세요.

또한, 이번 차시에 구현할 optimizer는 위 링크의 Momentum과 NAG를 구현할 것입니다.

 

 

 

* 저번 시간의 코드 그대로 이어집니다.

 

import numpy as np
import pandas as pd
from Model import Model
import Layers
import Optimizer

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 32), name="w1")
model.addlayer(Layers.AddLayer(), input_size=32, name='b1')
model.addlayer(Layers.SigmoidLayer(), activation=True, name='sigmoid1')
model.addlayer(Layers.MulLayer(), input_size=(32, 10), name="w2")
model.addlayer(Layers.AddLayer(), input_size=10, name='b2')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')

optimizer = Optimizer.Momentum(batch_size=128, momentum=0.9, nesterov=True)
model.train(x_train, y_train, optimizer, 10000, 0.01)

위 코드는 원래 저번 코드에서 딱 두 부분만 바뀌었습니다. Optimizer를 import하였고, 새로 optimizer를 만들어 train하는 방식입니다.

 

또, Model.py의 train함수 부분을 다음과 같이 바꾸고 시작하겠습니다.

    def train(self, x_train, y_train, optimizer, epoch, learning_rate):
        optimizer.train(x_train, y_train, epoch, learning_rate, self)

 

전체적인 훈련은 Optimizer.py라는 코드에서 진행하도록 하고, 훈련시키는 방식에 대한 코드는 모두 Optimizer.py에 집어 넣는 방식입니다.

그리고, 위에서 바꾸기 전의 train 함수 부분을 복사해서, Optimizer.py에 다음과 같이 넣어봅시다.

import numpy as np


class SGD:
    def __init__(self, batch_size):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size

    def train(self, x_train, y_train, epoch, learning_rate, model):
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    model.params[key] -= learning_rate * model.grads[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)

 

Optimizer.py에서는 다양한 optimization function (SGD, Momentum, NAG, ...)들이 클래스 형식으로 구현될 것입니다.

 

SGD의 구현은 그냥 동일하니, 바로 Momentum을 만들어 봅시다.

 

 

Momentum optimization은 기존의 SGD와 다르게, 미분값에 일종의 "가속도"를 붙여 주며 학습시킵니다.

이는 학습 시의 자잘한 local minima들을 피하기 위함인데, 실제로 SGD의 경우 작은 local minima들에 gradient가 갇히게 되는 경우도 존재하고, 빠져나온다 하더라도 그 안에서 오랜 시간 머무르게 됩니다.

따라서, 진행 방향에 가속도를 붙여주어 빠르게 local minima를 벗어나고, global minima에 다가가게 하는 것이죠.

 

 

Momentum의 식은 다음과 같습니다.

$$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} - v_t $$

 

(참고로, 위 식은 아래와 같이 부호를 살짝 바꾸어 쓴 것과 동일합니다.)

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

이제, 각각의 파라미터들을 설명하겠습니다.

v는 velocity의 약어로, 파라미터가 이동할 속도 (방향과 속력)을 나타냅니다. 즉, 파라미터는 $v_t$만큼 이동하게 됩니다.

$\gamma$는 momentum의 hyperparameter인데, 0과 1 사이의 값을 넣습니다. (보통 0.9로 많이 넣습니다.) 해당하는 값이 높으면 가속도가 크고, 값이 낮으면 가속도가 낮습니다.

$\eta$는 learning rate의 값입니다.

$\theta$는 학습시킬 parameter (w1, w2, b1, b2, ..)입니다.

$\nabla_\theta J(\theta_{t-1})$ 는 현재 학습시킬 parameter에 대한, 오차함수인 $J(\theta)$의 미분값을 의미합니다.

 

이제 생각해야 할 것은 어떻게 velocity를 구현할지만 고민하면 됩니다.

 

 

class Momentum:
    def __init__(self, batch_size, momentum):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.momentum = momentum

    def train(self, x_train, y_train, epoch, learning_rate, model):
        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
                    model.params[key] -= velocity[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)
    

위 코드의 대부분은 SGD와 동일합니다.

특이할 부분만 살펴보자면,

 

1. __init__에 momentum을 입력한다. ($\gamma$)

2. 변수 velocity가 새로 생겼고, grad를 바로 학습시키는 대신 velocity의 연산을 한 후에 학습시킨다.

 

정도로만 보면 되겠습니다.

 

우선 momentum의 hyperparameter (변수 momentum)은 이 optimizer에만 사용될 것이므로, 클래스에 선언하였습니다.

 

 

        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)

velocity는 다른 grads나 params같이 딕셔너리로 만들었습니다.

그리고 model.params의 크기(model.grads의 크기와 동일함)만큼 그대로 0으로 초기화해서 만들어 줍니다.

$ v_t = \gamma v_{t-1} + \eta\nabla_\theta J(\theta_{t-1}) $ 였으므로, 초기값은 0이 되어야 그 뒤의 가속도 값을 구할 수 있기 때문입니다.

 

                    model.grads[key] = model.layers[key].grad
                    velocity[key] = self.momentum * velocity[key] + learning_rate * model.grads[key]
                    model.params[key] -= velocity[key]

그리고, 위에서 언급했던 식과 동일하게 구현해 주면 됩니다.

momentum은 이렇게나 구현이 간단합니다.

 

 

 

 

 

 

다음은 NAG (Nesterov Accelerated Gradient)입니다.

NAG는 기본 틀이 momentum과 동일하기 때문에, 보통 다른 라이브러리에서도 SGD 안에 momentum과 nesterov를 넣어서 하나로 만듭니다.

그래서, NAG를 구현할 때는 momentum에서 nesterov를 True로 할지, False로 할지만 정해서 NAG를 적용시킬지 아닐지 결정하는 방식으로 가겠습니다.

(사실 원래 위의 momentum도 SGD에 넣어서 하려 했는데, 그냥 느낌 내려고 뺐습니다 ㅎㅎ)

 

 

 

NAG는 위의 momentum보다는 조금 까다로운 면이 있습니다.

momentum은 그냥 가속도만 붙여서 간다면, NAG는 가속이 붙은 방향으로 갔을 때의 미분값까지 계산해야 합니다.

우선 수식으로 볼까요?

 

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

(파라미터들은 모두 위와 동일하므로 설명하진 않겠습니다.)

 

이게 무슨 뜻인고 하니, "가속도가 붙은 방향으로 그대로 돌진하지 말고, 돌진했을 때를 생각해서 적절하게 가자" 하는 것입니다.

위의 momentum만을 활용하게 되면, local minima도 빠르게 빠져나올 수 있지만, 사실 global minima에서도 빠져나올 수 있다는 단점도 있고, 가속도가 너무 크게 붙었을 경우 제동이 쉽지 않다는 단점도 있습니다.

하지만 미리 "내가 이 방향으로 갔을 때, 제동을 걸어야 할까?" 하는 것을 $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$로 계산해 줄 수 있는 것입니다.

 

하지만, NAG도 위의 momentum처럼 식만 보고 구현할 수만 있다면 정말 좋았겠으나 그렇게 간단하지는 않습니다.

우리가 구현한 Layer들은 오차함수를 각각의 파라미터에 대해서만 미분 가능하지, $\nabla_\theta J(\theta_{t-1} - \gamma v_{t-1})$의 값을 계산할 수는 없기 때문입니다.

 

물론, NAG용 파라미터를 하나 더 만들어서 $\theta = \theta - v_t$로 만들어서 미분을 처음부터 다시 하고 ... 하면 되겠습니다...만

그러면 한번 연산할 것이 두배가 되고, 그러면 애초에 시간복잡도가 (거의) 두배가 되어 버립니다.

조금 더 효율적인 방법은 없을까요?

 

*혹시라도 아래 설명이 이해가 잘 안된다면, 제가 참고한 아래 논문들의 원문을 읽어보시는 것을 추천드립니다.

https://arxiv.org/pdf/1212.0901v2.pdf (3.5. Simplified Nesterov Momentum)

https://www.cs.utoronto.ca/~ilya/pubs/ilya_sutskever_phd_thesis.pdf

 

 

자, 아까 위에 언급했던 NAG의 수식은 다음과 같았습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1} + \gamma v_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

그런데, 이는 위에서 언급했던 다음과 같은 momentum의 모습과 상당히 유사합니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1}) $$

$$ \theta_t = \theta_{t-1} + v_t $$

 

우리의 목적은 위의 NAG의 수식을 위의 momentum과 같이 theta에 대한 미분값만으로 계산할 수 있도록 하는 것입니다.

 

 

일단, $ \theta_{t-1} + \gamma v_{t-1} $ 를 $k_{t-1}$라는 새로운 파라미터로 생각해 봅시다.

그러면 위의 NAG 식은 다음과 같이 쓸 수 있습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_k J(k_{t-1})$$

$$ \theta_t = \theta_{t-1} + v_t $$

$$ \theta_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) $$

 

이렇게 하면 원래 momentum의 velocity update와 동일한 식을 만들 수 있었습니다.

또한, $k_t$의 식을 조금 써본다면...

$$k_t = \theta_t + \gamma v_t $$

$$k_t = \theta_{t-1} + \gamma v_{t-1} - \eta\nabla_k J(k_{t-1}) + \gamma v_t $$

$$k_t = k_{t-1} + \gamma v_t - \eta\nabla_k J(k_{t-1}) $$

 

자, 이렇게 $\theta$를 대체할 $k$라는 파라미터의 update식을 구할 수 있습니다!

또한, 아까 언급했듯 $k_{t-1} = \theta_{t-1} + \gamma v_{t-1}$이었습니다.

그런데 우선 최초의 velocity값인 $v_1$은 0이므로, $t=1$일 때의 $k_t$값은 $\theta_t$의 값과 동일합니다.

그리고, optimal convergence ($v_{t-1}=0$인 지점)에서는 $\theta_t$와 $k_t$는 완벽하게 동일합니다.

 

그러므로 우리는 원래의 파라미터 $\theta$를 우리가 새로 만든 파라미터 $t$로 근사가 가능합니다.

즉, update rule을 다음과 같이 정의할 수 있습니다.

$$ v_t = \gamma v_{t-1} - \eta\nabla_\theta J(\theta_{t-1})$$

$$ \theta_t = \theta_{t-1} + \gamma v_t - \eta\nabla_\theta J(\theta_{t-1}) $$

 

이제 이것을 코드로 구현해 봅시다.

 

class Momentum:
    def __init__(self, batch_size, momentum, nesterov=False):
        self.x = None
        self.y = None
        self.model = None
        self.batch_size = batch_size
        self.momentum = momentum
        self.nesterov = nesterov

    def train(self, x_train, y_train, epoch, learning_rate, model):
        velocity = {}
        for p in model.params:
            velocity[p] = np.zeros(model.params[p].shape)
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], self.batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            model.predict(x, y)
            dout = model.layers[model.keys[-1]].backward()
            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    if self.nesterov:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
                    else:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += velocity[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (model.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, model.loss)
        model.predict(x_train, y_train)
        print("Final train_ACC : ", (model.pred.argmax(1) == y_train.argmax(1)).mean())
        print("Final train_LOSS : ", model.loss)
    

 

위의 코드에서 바뀐 점만을 집중해서 봐 주시길 바랍니다.

 

1. __init__에 nesterov=False와, self.nesterov = nesterov를 넣어줌으로써 momentum 클래스에 nesterov를 키고 끌 수 있도록 했습니다.

 

2. if self.nesterov: 로 nesterov를 켰을 경우에 nesterov를 적용시켜 학습시킵니다.

 

 

            for i in reversed(range(len(model.keys) - 1)):
                key = model.keys[i]
                dout = model.layers[key].backward(dout)
                if key in model.params:
                    model.grads[key] = model.layers[key].grad
                    if self.nesterov:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += self.momentum * velocity[key] - learning_rate * model.grads[key]
                    else:
                        velocity[key] = self.momentum * velocity[key] - learning_rate * model.grads[key]
                        model.params[key] += velocity[key]

이 부분만 다시 자세히 보도록 하겠습니다.

if 구문으로 nesterov가 True일 때와 False일 때를 나누어서 계산합니다.

nesterov가 False인 경우에는 당연히 위의 Momentum과 동일한 식으로 나오게 됩니다.

 

nesterov가 True인 경우엔, 위에서 언급한 식 그대로 코드로 구현하시면 됩니다.

테스트하고 싶으시다면, 원래 코드에서 변수로 nesterov = True를 넣어주시면 됩니다.

 

 

이렇게, Momentum과 NAG의 구현을 마무리하도록 하겠습니다.

나머지 Adagrad, Adadelta, RMSProp, Adam은 다음 시간에 구현해 보도록 합시다!

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

원래는 바로 Optimizer를 제작하려 했으나, 생각해 보니 코드가 이래서는 바로 들어가기는 좀 그럴 것 같습니다.

 

따라서 이번에는 코드를 대충 정리를 하고 "예쁘게" 만들어 보겠습니다.

(이론 설명같은 건 없다는 뜻)

그냥 어떤 부분이 바뀌었는지, 어떻게, 왜 바꾸었는지 간결하게만 설명하고 넘어가겠습니다.

 

 

import numpy as np
import pandas as pd
from Model import Model
import Layers

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

 

일단 ModelTest.py (테스트용 파일)을 설명하겠습니다.

저번과 아주 동일한 훈련 방식을 거칠 것이니, 데이터도 그대로 가져옵니다. (복붙)

model = Model()
model.addlayer(Layers.MulLayer(), input_size=(784, 32), name="w1")
model.addlayer(Layers.AddLayer(), input_size=32, name='b1')
model.addlayer(Layers.SigmoidLayer(), activation=True, name='sigmoid1')
model.addlayer(Layers.MulLayer(), input_size=(32, 10), name="w2")
model.addlayer(Layers.AddLayer(), input_size=10, name='b2')
model.addlayer(Layers.SoftmaxLayer(), activation=True, name='softmax')

model.train(x_train, y_train, 10000, 0.01, 128)

달라진 부분은 이 부분입니다.

직접 데이터를 하나하나 넣어주는 것이 아니라, Model이라는 모듈을 만들어서 예쁘게 만들어 주자는 것이 주요 내용입니다.

import numpy as np
from Layers import *


class Model:
    def __init__(self):
        self.params = {}
        self.grads = {}
        self.keys = []
        self.layers = {}
        self.num = 0
        self.loss = None
        self.pred = None

    def addlayer(self, layer, activation=False, input_size=None, name=None):
        if name is None:
            name = str(self.num)

        self.keys.append(name)
        self.num += 1
        self.layers[name] = layer

        if not activation:
            self.params[name] = np.random.uniform(-1, 1, input_size)
            self.layers[name].param = self.params[name]

    def predict(self, x, y):
        for i in range(len(self.keys) - 1):
            key = self.keys[i]
            x = self.layers[key].forward(x)
        self.loss = self.layers[self.keys[-1]].forward(x, y)
        self.pred = softmax(x)

    def train(self, x_train, y_train, optimizer, epoch, learning_rate, batch_size):
        for epochs in range(epoch):
            batch_mask = np.random.choice(x_train.shape[0], batch_size)
            x = x_train[batch_mask]
            y = y_train[batch_mask]

            self.predict(x, y)
            dout = self.layers[self.keys[-1]].backward()
            for i in reversed(range(len(self.keys) - 1)):
                key = self.keys[i]
                dout = self.layers[key].backward(dout)
                if key in self.params:
                    self.grads[key] = self.layers[key].grad
                    self.params[key] -= learning_rate * self.grads[key]

            if epochs % (epoch / 10) == 0:
                print("ACC on epoch %d : " % epochs, (self.pred.argmax(1) == y.argmax(1)).mean())
                print("LOSS on epoch %d : " % epochs, self.loss)

 Model.py 부분입니다.

원래 Main에 있었던 grads, params 등등을 모두 이 Model 안에 넣었습니다.

그 뒤, addlayer로 Layer을 모델에 각각 추가하는 방식으로 모델의 레이어가 구현되도록 하였습니다.

이 때, activation 여부를 확인해서 activation function인 경우에는 grads와 params가 존재하지 않게 막아주었습니다.

(저번 코드의 if 'Relu'... 부분을 바꾼 것입니다.)

 

predict는 원래 train하는 과정에서의 forward 하는 과정을 따 온 함수입니다.

원래는 train 안에 그대로 넣고자 하였으나, model에서 직접 predict를 해야 할 일이 있을 것 같아서 따로 뺐습니다.

 

그리고 train 부분은 원래 훈련시키는 부분과 매우 유사합니다.

차이점은, lastlayer를 따로 빼지 않는 대싱 반복문을 range(len(self.keys)-1)로 바꾸어서, 마지막 layer은 따로 빼서 연산하도록 했습니다.

 

기반은 SGD로 구현하였으나, 다음 차시에 바로 train을 Optimizer 모듈에 구현하게 될 것 같습니다.

그래서 다음 차시에는, SGD 말고도 RMSPROP과 같은 다른 훈련 방식을 사용해 보도록 하겠습니다.

class MulLayer:
    def __init__(self, param=None):
        self.x = None
        self.param = param
        self.grad = None

    def forward(self, x):
        self.x = x
        return x.dot(self.param)

    def backward(self, dout):
        self.grad = np.dot(self.x.T, dout)
        return np.dot(dout, self.param.T)


class AddLayer:
    def __init__(self, param=None):
        self.x = None
        self.param = param
        self.grad = None

    def forward(self, x):
        self.x = x
        return x + self.param

    def backward(self, dout):
        self.grad = dout.mean()
        return dout

 

이 부분은 Layers.py 부분입니다.

MulLayer와 AddLayer에서 w, b를 그냥 param으로 통일하고, __init__시에 직접 parameter를 집어넣지 않아도 되게 만들었습니다.

param으로 이름을 통일한 것은 반복문에서의 train때의 편의성을 위함이고, __init__시에 직접 param을 집어넣지 않아도 되게 만든 것은 Model에서의 AddLayer를 만들기 위함입니다.

(위에서 보시면 아시겠지만, model.addlayer()부분에서 MulLayer과 AddLayer에 따로 params를 집어넣지 않고, Input_size만을 넣어서  자동으로 랜덤 값이 들어가도록 했습니다.)

 

 

이렇게 코드를 갈아엎어 보았습니다.

다음 시간에는 드디어, Optimizer를 구현해 보겠습니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

이번에는 ReLU 계열의 활성화 함수를 구현해 보겠습니다.

 

 

ReLU, Leaky ReLU, ELU

위 그래프는 ReLU 계열의 활성화 함수를 그래프로 나타낸 것입니다.

ReLU, Leaky ReLU, ELU 모두 입력값이 0보다 크면 그대로 y=x를 사용하지만,

0보다 작을때는 ReLU는 그냥 모든 입력에서 0, Leaky ReLU는 약 0.001x, ELU는 $e^x - 1$을 나타냅니다.

 

 

그렇다면 바로 Layers.py 구현부로 가서 활성화 함수들을 구현해 봅시다.

이번 시간에 구현할 것들은 좀 쉬워서, 사실 안보고도 하실 수 있을 겁니다. 글을 보지 않고, 스스로 한번 만들어 보셔도 좋겠습니다.

 

 

class ReLU:
    def __init__(self):
        self.out = None

    def forward(self, z):
        self.out = z
        self.out[self.out <= 0] = 0
        return self.out

    def backward(self, dout):
        self.out[self.out > 0] = 1
        return self.out * dout

우선, ReLU 함수입니다.

z값이 0보다 클때는 그대로 z를 출력하고, 0보다 작을 때는 0을 출력합니다.

forward의 구현은 그것을 그대로 따라갔습니다.

 

 

backward의 경우, 당연히 ReLU함수가 0보다 클 때는 1, 작을 때는 0이 됩니다만...

ReLU가 0일 경우 미분값을 어떻게 처리해 주어야 할까요?

사실 원래는 0일 경우 미분값을 구할 수는 없지만, 보통 그냥 0으로 만들어 줍니다.

딱히 큰 의미가 있는 것은 아니고, 어차피 뭘 해도 정확도에 큰 영향이 없기 때문입니다.

 

 

 

class LeakyReLU:
    def __init__(self):
        self.out = None

    def forward(self, z):
        self.out = z
        self.out[self.out <= 0] *= 0.001
        return self.out

    def backward(self, dout):
        self.out[self.out > 0] = 1
        self.out[self.out <= 0] = 0.001
        return self.out * dout

다음은 Leaky ReLU입니다.

위의 ReLU를 그대로 복사한 뒤, self.out <= 0 부분에서 0을 만들어 버리는 대신, 0.001을 곱해주면 됩니다.

backward도 마찬가지로, 0보다 작은 경우의 미분값은 당연히 0.001이 되게 됩니다.

 

 

class ELU:
    def __init__(self):
        self.out = None

    def forward(self, z):
        self.out = z
        self.out[self.out <= 0] = np.exp(self.out[self.out <= 0]) - 1
        return self.out

    def backward(self, dout):
        self.out[self.out > 0] = 1
        self.out[self.out <= 0] += 1
        return self.out * dout

ELU도 전혀 다를 것이 없습니다.

forward 함수에서는 값이 0보다 작으면 $e^x - 1$을 출력값으로 내놓습니다.

그리고 해당 값을 미분하면 $e^x$이 나오므로, 그냥 원래 $e^x - 1$이었던 값에다가 1만 더해주면, 그것이 미분값이 됩니다.

 

 

사실 이번 활성화 함수 제작은 여기서 끝입니다.

보통 실제 훈련 시킬 때는 ReLU계열의 활성화 함수만 사용하기 때문에, 이렇게만 준비해 봤습니다.

하지만, 다른 Activation function도 위의 방식을 따라만 간다면 만들기에 크게 어렵지 않을 겁니다.

다른 활성화 함수를 구현해 보고 싶으시다면, 직접 forward와 backward를 구현하는 것도 꽤 재밌는 일입니다.

 

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

자, 드디어 Multi-Layer로 향할 차례입니다!

저번에 한번 구현한 바가 있던 MNIST 손글씨 인식 프로그램을 이번엔 Multi-Layer로 구현하도록 하겠습니다.

 

이번에도 이론 설명보단 코드 설명에 집중되어 있으니, 가벼운 마음으로 읽고 복붙하시면 되겠습니다.

 

 

 

import numpy as np
import pandas as pd
import Layers

# load data
train = pd.read_csv("../Data/MNIST_data/mnist_train.csv")
y_train = train["label"]
x_train = train.drop("label", 1)
x_train = x_train.values / x_train.values.max()
y_train = y_train.values

# y to one-hot
one_hot = np.zeros((y_train.shape[0], y_train.max() + 1))
one_hot[np.arange(y_train.shape[0]), y_train] = 1
y_train = one_hot

# initialize parameters
num_classes = y_train.shape[1]
hidden = 50

keys = ['w1', 'b1', 'sigmoid', 'w2', 'b2']
params = {}
params['w1'] = np.random.uniform(-1, 1, (x_train.shape[1], hidden))
params['b1'] = np.random.uniform(-1, 1, (hidden))
params['w2'] = np.random.uniform(-1, 1, (hidden, num_classes))
params['b2'] = np.random.uniform(-1, 1, (num_classes))

layers = {}
layers['w1'] = Layers.MulLayer(params['w1'])
layers['b1'] = Layers.AddLayer(params['b1'])
layers['sigmoid'] = Layers.SigmoidLayer()
layers['w2'] = Layers.MulLayer(params['w2'])
layers['b2'] = Layers.AddLayer(params['b2'])
lastlayer = Layers.SoftmaxLayer()

grads = {}

# initialize hyperparameters
learning_rate = 0.01
epochs = 10000
batch_size = 128

우선, 파라미터 초기화 및 데이터 불러오기 등은 지금까지와 아주, 매우, 비슷합니다.

바뀐 점으로는, 기존에 w, b로 사용했던 인자들 대신 params라는 dictionary를 활용해서 학습을 진행하게 됩니다.

w와 b값이 여러개가 될 수 있기 때문이죠.

그리고, Perceptron에서 사용되는 Hidden Layer의 개수를 "hidden"이라는 변수를 통해 선언했습니다.

이 값을 늘리게 되면 연산은 느려지지만 비교적 정확하게 학습이 될 가능성이 높고, 낮춘다면 연산은 빨라지지만 조금 부정확한 학습이 될 수 있겠죠.

 

또, layer 사이에 sigmoid로 activation function을 넣었고, 더 빠른 학습을 위해 mini-batch를 활용한 SGD를 사용하도록 하겠습니다.

 

이번에 새로 추가된 Layer들을 소개하면, 각각 "SigmoidLayer"과 "SoftmaxLayer"입니다.

Layers.py에 가서 새로 추가된 Layer들을 확인해 보도록 합시다.

 

def sigmoid(x):
    return 1.0 / (1.0 + np.exp(-x))


def softmax(a):
    C = np.max(a)
    exp_a = np.exp(a - C)
    if a.ndim == 1:
        sum_exp_a = np.sum(exp_a)
        y = exp_a / sum_exp_a
    else:
        sum_exp_a = np.sum(exp_a, 1)
        sum_exp_a = sum_exp_a.reshape(sum_exp_a.shape[0], 1)
        y = exp_a / sum_exp_a
    return y


def cross_entropy_loss(y, t):
    C = 1e-7
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + C)) / batch_size

 

일단 당연하게도, sigmoid, softmax, cross_entropy_loss 함수를 선언해야 합니다.

참고로, 위 코드는 저번 코드와 이어지므로, numpy 선언 등은 제외하도록 하겠습니다.)

(혹시라도 위 코드에 대한 설명이 보고 싶으시다면, 이 포스팅이 포스팅을 참고해 주세요.)

 

 

class SigmoidLayer:
    def __init__(self):
        self.out = None

    def forward(self, z):
        self.out = sigmoid(z)
        return self.out

    def backward(self, dout):
        return (1 - self.out) * self.out * dout

우선, 비교적 간단한 SigmoidLayer부터 확인해 봅시다.

당연하게도, forward 함수에는 들어온 값을 sigmoid 함수를 거쳐 return해 줍니다.

 

그리고, Logistic Regression 구현부에서도 설명했듯, Sigmoid 함수를 미분하면 $pred * (1 - pred)$가 되므로, 위와 같은 코드가 나옵니다.

 

 

class SoftmaxLayer:
    def __init__(self):
        self.loss = None
        self.y = None
        self.t = None

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_loss(self.y, self.t)
        return self.loss

    def backward(self):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dx

다음으로, SoftmaxLayer입니다.

이 Layer에서는 Softmax함수 뿐만 아니라, Cross Entropy Loss까지 한번에 구현하였습니다.

 

그래서, forward 함수에서는 self.y에 softmax함수를 지나친 값, self.t에는 입력된 정답 라벨값을 넣어서, loss값을 구합니다.

 

그리고 backward함수에서는 저번 Softmax Classification 구현부에서 설명했던대로, y-t값을 뱉어냅니다.

 

사실 Layer 구현은 이론적인 부분이 없어서 그냥 설명을 빨리 넘기겠습니다. 혹시라도 이해가 안되신다면, 댓글로 남겨주시면 감사하겠습니다. 

 

 

for epoch in range(epochs):
    batch_mask = np.random.choice(x_train.shape[0], batch_size)
    x = x_train[batch_mask]
    y = y_train[batch_mask]

    for key in keys:
        x = layers[key].forward(x)

    loss = lastlayer.forward(x, y)

    if epoch % (epochs / 10) == 0:
        pred = Layers.softmax(x)
        print("ACC on epoch %d : " % epoch, (pred.argmax(1) == y.argmax(1)).mean())
        print("LOSS on epoch %d : " % epoch, loss)

    dout = lastlayer.backward()

    for key in reversed(keys):
        dout = layers[key].backward(dout)
        if key != 'sigmoid':
            grads[key] = layers[key].grad
            params[key] -= learning_rate * grads[key]

 

이제 본격적으로 학습을 시켜줍시다.

 

SGD를 사용한다고 했으니 batch_mask를 만들어서 계산해 주고, keys에 저장했던 key값을 토대로 지속적으로 forward를 해 나갑니다.

 

그렇게 forward를 다 한 뒤에, lastlayer (즉 softmax layer)에서 loss 값을 얻어냅니다.

 

그리고, 그 아래에서는 backward를 실행시켜 오차역전파법을 계산해 줍니다.

참고로, 아래 고드의 if key != 'sigmoid' 부분은 조금 예쁘진 않지만, 추후에 activation function을 구현하면서 같이 개선해 나갈 코드입니다.

 

# load data
test = pd.read_csv("../Data/MNIST_data/mnist_test.csv")
y_test = test["label"]
x_test = test.drop("label", 1)
x_test = x_test.values / x_test.values.max()
y_test = y_test.values

# y to one-hot
one_hot = np.zeros((y_test.shape[0], y_test.max() + 1))
one_hot[np.arange(y_test.shape[0]), y_test] = 1
y_test = one_hot

x = x_test
for key in keys:
    x = layers[key].forward(x)
pred = Layers.softmax(x)
print("ACC : ", (pred.argmax(1) == y_test.argmax(1)).mean())

 

이제 학습이 잘 되었는지, 마지막으로 test할 시간입니다!

test 코드는 pred값을 layers를 통해서 얻는 것 빼고는 저번 softmax classification과 동일합니다.

 

 

 

이렇게 Multi-Layer Softmax Classification을 직접 구현해 보았습니다.

저번에 구현했던 single-layer 보다 조금 더 좋은 성능을 내는 것을 확인하실 수 있을 것입니다.

한번 layer를 늘리거나, hidden의 크기를 늘리고 줄이면서 더욱 높은 성과를 낼 수 있을지 해 보는것도 재밌을 것입니다.

* 모든 코드는 제 깃허브 (cdjs1432/DeepLearningBasic: Deep Learning from scratch)에서 확인할 수 있습니다.

 

지금까지는 간단한 머신 러닝만 다뤘다면, 이제는 진짜 다중 계층의 딥 러닝을 다룰 시간입니다!

그 다중 계층을 향한 가장 첫 번째 순서는, 바로 Single-Layer Gradient Descent입니다.

직접 Layer를 구현해 보고, 제대로 학습이 되는지 보도록 합시다.

 

이번에는 이미 했던 계산들만 계속 나오니, 계산은 필요가 없고, 어떻게 구현하면 될지만 보도록 합시다.

 

 

import numpy as np
import Layers

train_x = np.random.uniform(-5, 5, (100, 10))
ans_w = np.random.uniform(-5, 5, (10, 1))
ans_b = 3
train_y = train_x.dot(ans_w) + ans_b
w = np.random.uniform(-5, 5, (10, 1))
b = np.random.uniform(-5, 5, (1, 1))
learning_rate = 0.001
epoch = 10000

우선 데이터는 저번 Multi-Variable Gradient Descent 부분과 동일하게 진행하도록 해 봅시다. (근본적으로 이번에 할 것이 그것과 똑같이 때문이죠.)

 

 

keys = ['w', 'b']
layers = {}
layers['w'] = Layers.MulLayer(w)
layers['b'] = Layers.AddLayer(b)
lastlayer = Layers.MSELayer(train_y)
grads = {}

 

이제 훈련할 때의 변수를 선언해 봅시다.

 

layers에는 당연히 layer들을 집어 넣을 것이고, keys에는 훈련의 코드를 조금 더 간결하게 하기 위한 'w','b'를 넣어 줍시다.

그러면 이제 Layers 함수에 가서 Layers 함수를 어떻게 구현할지 확인해 볼까요?

 

 

import numpy as np

class MulLayer:
    def __init__(self, w):
        self.x = None
        self.w = w
        self.grad = None

    def forward(self, x):
        self.x = x
        return x.dot(self.w)

    def backward(self, dout):
        self.grad = np.dot(self.x.T, dout)
        return np.dot(dout, self.w.T)


class AddLayer:
    def __init__(self, b):
        self.x = None
        self.b = b
        self.grad = None

    def forward(self, x):
        self.x = x
        return x + self.b

    def backward(self, dout):
        self.grad = dout.mean()
        return dout


class MSELayer:
    def __init__(self, y):
        self.x = None
        self.y = y
        self.loss = None

    def forward(self, x):
        self.x = x
        self.loss = np.square(x-self.y).mean()
        return self.loss

    def backward(self):
        return self.x - self.y

 

일단 지금까지의 Layer 모듈에는 이렇게 들어있습니다.

각각의 클래스 구조를 보시면, 레이어들은 __init__, forward, backward의 세 가지 함수로 이루어져 있습니다.

우선 __init__에는 해당 함수가 필요로 하는 값들을 지정하고, 선언해 줍니다.

가령 MulLayer같은 경우에는 w 파라미터를 init해주고, AddLayer같은 경우에는 b 파라미터는 init해줍니다.

 

forward는 순방향 계산입니다. 쉽게 말해, pred 값을 찾아가는 것입니다.

저번 Gradient Descent에서 pred = x.dot(w) + b 라는 식으로 prediction값을 구했다면, 이제는 각각의 layer에 대한 forward 연산으로 해당 값을 구할 것입니다.

 

backward는 역전파법 계산입니다. 이 연산을 통해 각각의 layer에 대한 gradient 값을 알아갈 수 있습니다.

 

그리고, 이 gradient 값들을 통해 인공지능 학습을 해야 하므로, 이 값들을 각각의 layer에 저장해 주어야 합니다. (grad 변수의 역할)

 

이제 클래스를 하나씩 순서대로 설명하도록 하겠습니다.

 

 

class MulLayer:
    def __init__(self, w):
        self.x = None
        self.w = w
        self.grad = None

    def forward(self, x):
        self.x = x
        return x.dot(self.w)

    def backward(self, dout):
        self.grad = np.dot(self.x.T, dout)
        return np.dot(dout, self.w.T)

 

MulLayer, 즉 곱연산 레이어를 먼저 설명하도록 하겠습니다.

이 Layer에서는 forward, 즉 순방향 연산 때는 input 값인 x에 대해 x*w (x.dot(w))의 값을 전달합니다.

그리고, x * w를 w에 대해 미분하면 x가 나오므로, gradient는 x가 됩니다.

그런데, 왜 x에 dout을 곱해주는 걸까요?

 

우리는 다중 레이어로 구성된 딥 러닝 모델을 만들 것이기 때문입니다.

나중에 전체 코드를 보시면 아시겠지만, 가장 첫 dout에서는 MSELayer, 즉 Loss 값에 대한 Layer의 pred값에 대한 미분값이 들어갑니다.

 

그리고, Chain Rule $ \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \frac{\partial y}{\partial x} $ 에 의하여, 역전파법 계산을 하면서 dout을 계속 곱해주면 됩니다.

그렇기에, 위 코드에서도 backward 함수에서 x와 dout을 곱해준 값이 grad가 되는 것입니다.

 

 

 

class AddLayer:
    def __init__(self, b):
        self.x = None
        self.b = b
        self.grad = None

    def forward(self, x):
        self.x = x
        return x + self.b

    def backward(self, dout):
        self.grad = dout.mean()
        return dout

 

AddLayer도 위와 비슷합니다.

AddLayer, 즉 합에서는 b에 대해 미분하면 1이 나오므로, backward는 dout 그대로 나오는 것을 알 수 있습니다.

 

class MSELayer:
    def __init__(self, y):
        self.x = None
        self.y = y
        self.loss = None

    def forward(self, x):
        self.x = x
        self.loss = np.square(x-self.y).mean()
        return self.loss

    def backward(self):
        return self.x - self.y

 

다음은 MSELayer입니다.

MSE가 뭐지? 하시는 분이 있었을 것도 같습니다. 하지만, 사실 이미 MSE는 본 적이 있습니다.

Gradient Descent에서 $\frac{1}{m}(pred - y)^2$로 loss 값을 설정한 적이 있었죠?

이것을 바로 Mean Squared Error, 줄여서 MSE라 합니다.

그리고 이를 pred에 대해 미분하면 $pred - y$값이 나온다는 것은 이미 계산해 본 적이 있습니다.

 

따라서, forward에는 loss값의 계산 식이 들어가고, backward에는 $pred - y$, 즉 위 함수에선 x - y가 들어갑니다.

 

이렇게 Layer 의 함수에 대한 설명은 끝내고, 나머지 메인 코드 부분을 끝장내러 가봅시다.

 

for epoch in range(epoch):
    x = train_x
    y = train_y

    for key in keys:
        x = layers[key].forward(x)

    loss = lastlayer.forward(x)
    if epoch % 100 == 0:
        print("err : ", loss)

    dout = lastlayer.backward()

    for key in reversed(keys):
        dout = layers[key].backward(dout)
        grads[key] = layers[key].grad

    db = layers['b'].grad
    dw = layers['w'].grad

    w -= learning_rate * dw
    b -= learning_rate * db

 

아까 전에 위에서 언급했듯, keys를 사용하여 forward와 backward, 그리고 grad값 저장을 쉽게 할 수 있습니다.

사실 이번에는 w와 b가 하나씩밖에 나오지 않았기에 db와 dw의 초기화를 저렇게 수동으로 했지만, 저 dw와 db의 초기화도 for문 안에 넣을 수도 있습니다.

 

 

다음 시간에는, 이를 활용한 Multi-Layer Gradient Descent로 MNIST 데이터셋을 다시 한번 훈련시켜 보겠습니다.

 

 

 

+ Recent posts