공돌이 공룡의 서재

[구현] 퍼셉트론 Numpy로만 구현하기 / Implement Perceptron by using Numpy Only 본문

딥러닝

[구현] 퍼셉트론 Numpy로만 구현하기 / Implement Perceptron by using Numpy Only

구름위의공룡 2021. 1. 2. 02:38

퍼셉트론을 tensorflow, keras, 또는 torch를 사용하지 않고 구현하려면 forwarding과 back propagation, activation function 등이 어떻게 이뤄지고 구성되어 있는지를 정확히 알고 있어야 한다. 수학을 공부할 때 모르는 개념이 있다면 증명을 한 번 해보듯이, 입문하는 분들이라면 해볼 만한 과제라고 생각한다.

 

딥러닝 모델 구현은 크게 다음과 같은 부분으로 나뉠 수 있다.

  • 모델 설정 : node의 수, weight의 초기값, bias의 초기값, 등을 설정한다.
  • 손실함수 : 손실 함수에 대한 미분으로 역전파를 할 수 있다.
  • feed forward : 입력층 - 은닉층 - 출력층까지 값을 주는 것을 말한다
  • 손실 함수 & back propagation : 층 사이의 최적의 weight를 찾아가는 과정이다. 
  • training : 적절한 epoch와 학습률을 설정하여 학습한다. 여러 종류의 optimizer가 사용된다.
  • validation / test : 딥러닝 모델은 결국 학습한 데이터 외에 다른 데이터에도 잘 적용되는지 여부가 중요하다. 만약 학습용 데이터에는 높은 정확률을 보이는데 그 외 데이터에는 그렇지 않다면 과대 적합(overfitting), 정확도 자체가 형편없으면 과소 적합(underfitting)이라고 한다.

 

1. 단층 퍼셉트론 (Single-layer Perceptron)

Input과 Output은 임의로 다음과 같이 설정해보겠다.

[0, 0, 1, 0] [0]
[0, 0, 1, 1] [0]
[0, 1, 0, 0] [0]
[0, 1, 1, 1] [0]
[0, 1, 0, 1] [1]
[1, 0, 1, 0] [1]
[1, 0, 1, 1] [1]
[1, 1, 1, 1] [1]

Test 셋은 따로 두지 않겠다. 

단층 퍼셉트론이므로 모델은 다음과 같이 만들어볼 수 있겠다. bias는 따로 두지 않겠다. 

사실 단층 퍼셉트론의 경우 입력층에서 바로 출력층으로 가기 때문에 은닉층이 있다고 보기 힘들다. 다만 W, 즉 weight가 입력과 곱해지는 과정이 있다. 

 

Activation Fuction으로는 sigmoid, Loss Function으로는 MSE를 사용하자. sigmoid(=s)의 경우, 역전파때 미분하면 s(1-s)가 됨을 알고 있자. 

 

import numpy as np
# for visualization
import matplotlib.pyplot as plt

def forward_propagation(x, w):
    # 각 input에 대해서 weighted_sum한 결과에 activation 함수까지 적용한 결과를 return한다
    activation = []
    for i in range(len(x)):
        weighted_sum = w.T @ x[i]
        act = 1 / (1 + np.exp(-weighted_sum))
        activation.append(act)
    activation = np.array(activation)
    return activation

def backward_propagation(d_mse, prediction, inputs, weights, lr):
    # loss function 에서 구한 d_mse를 이용한다. weights를 update한다.
    weights += lr * d_mse.reshape(4, 1)
    return weights

def loss_function(keys, prediction, inputs):
    d_mse = 0
    mse_loss = 0
    for i in range(len(keys)):
        # dEdW = 손실함수를 w로 미분한 것
        dEdW = inputs[i] * (np.exp(-prediction[i]) / \
                            np.power((1 + np.exp(-prediction[i])), 2))
        d_mse_i = (keys[i] - prediction[i]) * dEdW
        d_mse += d_mse_i
        mse_loss += np.power((keys[i] - prediction[i]), 2)
    return (d_mse * 2)/len(keys), mse_loss * (1/(2*len(keys)))

def visualization(data):
    plt.plot(data)
    
def train(inputs, keys, weights, lr):
    loss_list = []
    for iter in range(3000):
        prediction = forward_propagation(inputs, weights)
        d_mse, mse_loss = loss_function(keys, prediction, inputs)
        weights = backward_propagation(d_mse, prediction, inputs, weights, lr)
        loss_list.append(sum(mse_loss))
    result = np.where(prediction<0.5 , 0, 1)

    # 결과 print
    print(weights)
    print(prediction)
    print(result)
    return loss_list

np.random.seed(1)
inputs = np.array([[0, 0, 1, 0],
                   [0, 0, 1, 1],
                   [0, 1, 0, 0],
                   [0, 1, 1, 1],
                   [0, 1, 0, 1],
                   [1, 0, 1, 0],
                   [1, 0, 1, 1],
                   [1, 1, 1, 1]])

# keys : ground truth
keys = np.array([[0, 0, 0, 0, 1, 1, 1, 1]]).T
# weight initialization
weights = 2 * np.random.random((4, 1)) - 1
#learning rate
lr = 0.05
loss_list = train(inputs, keys, weights, lr)
visualization(loss_list)

 

Visualizatoin 결과

 

2. 다층 퍼셉트론(Multi layer Perceptron)

1번의 구조에서 Hidden layer를 추가해보자. 

Weight인 W 와 Z는 각각 (2,4), (1,2)인 matrix가 되겠다. 역전파 식이 좀 더 복잡해진다. 텐서플로우나 토치같은 프레임워크를 사용하면 model을 class 형태로 구성하는 경우가 많으므로, 이런 흐름을 따라가 보겠다. activation function과 loss function은 위에서 했던 것과 똑같이 하겠다.

 

import numpy as np
import matplotlib.pyplot as plt

class MP:
    def __init__(self, inputs, labels, learning_rate, hidden_layers_shape):
        self.inputs = inputs 
        self.labels = labels
        self.learning_rate = learning_rate
        self.hidden_layers_shape = hidden_layers_shape
        self.model_compose()
        self.history = [] # For visualization
        
    def model_compose(self):
        assert type(self.hidden_layers_shape) == list
        self.W = np.random.uniform(size=(self.hidden_layers_shape[0], self.hidden_layers_shape[1]))
        self.bias1 = np.random.uniform(size=(1, self.hidden_layers_shape[1]))
        self.Z = np.random.uniform(size=(self.hidden_layers_shape[1], self.hidden_layers_shape[2]))
        self.bias2 = np.random.uniform(size=(1, self.hidden_layers_shape[2]))
        
    def sigmoid(self, value):
        return 1 / (1+np.exp(-value))
    
    def sigmoid_d(self, value):
        return value * (1 - value)
     
    def forward(self):
        hidden_layer_output = self.sigmoid(self.inputs @ self.W + self.bias1)
        y_hat = self.sigmoid(hidden_layer_output @ self.Z + self.bias2)
        return hidden_layer_output, y_hat
    
    def back_propagation(self, h, o):
        """
        h = hidden_layer_output
        o = output
        """
        dLdo = o - self.labels.reshape(len(self.inputs),1)
        dLdZ= h.T @ (dLdo * self.sigmoid_d(o)) 
        dLdb2 = np.sum(dLdo * self.sigmoid_d(o), axis=0, keepdims=1)
        dLdW = self.inputs.T @ (dLdo * self.sigmoid_d(o) * self.Z.T * self.sigmoid_d(h))
        dLdb1 = np.sum(dLdo * self.sigmoid_d(o) * self.Z.T * self.sigmoid_d(h), axis=0, keepdims=1)
        self.W -= self.learning_rate * dLdW
        self.Z -= self.learning_rate * dLdZ
        self.bias1 -= self.learning_rate * dLdb1
        self.bias2 -= self.learning_rate * dLdb2

    def train(self, epochs:int):
        for epoch in range(epochs):
            h, o = self.forward()
            loss = np.mean(np.power((o- self.labels), 2)) / len(inputs)
            self.history.append(loss)
            self.back_propagation(h, o)
    
    def visualization(self):
        plt.plot(self.history)
        
    def check(self):
        h, o = self.forward()
        return h, o
        
        
        
 mp = MP(inputs=inputs, labels=labels, learning_rate = 0.05, hidden_layers_shape=[4 , 2, 1])
 inputs = np.array([[0, 0, 1, 0],
                   [0, 0, 1, 1],
                   [0, 1, 0, 0],
                   [0, 1, 1, 1],
                   [0, 1, 0, 1],
                   [1, 0, 1, 0],
                   [1, 0, 1, 1],
                   [1, 1, 1, 1]])

labels = np.array([[0], [0], [0], [0], [1], [1], [1], [1]])

mp.train(20000)

mp.check()

mp.visualization()

 

직접 구현하는데 있어서 back propagation 코드 작성이 생각보다 요구 사항이 많았다. 곱하는 행렬들 간의 차원들도 맞춰야 하고, 미분하는 식이 길어지다 보니 들어가야 할 변수나 값이 헷갈리긴 했다. 

 

 

Reference: towardsdatascience.com/implementing-the-xor-gate-using-backpropagation-in-neural-networks-c1f255b4f20d 

Comments