deep learning

* 모든 코드는 제 깃허브 (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