Neural Net & Back Propagation 구현 (1)

GOAL : numpy를 사용하여 backpropagation을 구현하고, ‘train.txt’를 사용하여 잘 구현되었는지 확인하기

1. Importing libraries & dataset

import numpy as np
import matplotlib.pyplot as plt

%matplotlib inline
data = np.loadtxt('train.txt')

이 data는 0 또는 1의 라벨이 붙어져있는 1000개의 데이터이다. 우리는 Neural Net을 짜고 Back Propagation을 통해 이 데이터를 잘 분류할 수 있는 (binary) classifier를 만들 것이다.

print(data.shape)
data
(1000, 3)

array([[14.85418  , 10.31827  ,  0.       ],
       [20.981027 ,  3.4718131,  0.       ],
       [19.83997  , 10.857203 ,  0.       ],
       ...,
       [13.025853 ,  9.9031394,  1.       ],
       [ 6.3606368, 10.563107 ,  1.       ],
       [ 7.3325119, 12.256012 ,  1.       ]])

데이터의 y값에 0과 1이 고르게 섞여있지 않고, 앞에 500개에는 0이, 뒤에 500개에는 1이 있다. 이는 neural net에서 weight를 업데이트 할 때 제대로 이루어지지 않을 가능성이 있기 때문에, np.random.shuffle을 통해 data를 고르게 섞어준다.

 np.random.shuffle(data)

0 또는 1을 구분하는 문제이기 때문에 마지막 output neuron을 1개의 softmax function으로 만들 수 있지만, 그렇게 하지 않고 sigmoid function을 사용한 2개의 output neuron으로 만들 것이다. 따라서 다음과 같은 작업을 통해 label 0일 경우 [1, 0], label이 1일 경우 [0,1]이 나오게 변환시켜준다.

data_ = np.column_stack((data,1-data[:,2]))
X2 = data_[:,:2]
y2 = data_[:,-2:]
X2
array([[14.85418  , 10.31827  ],
       [20.981027 ,  3.4718131],
       [19.83997  , 10.857203 ],
       ...,
       [13.025853 ,  9.9031394],
       [ 6.3606368, 10.563107 ],
       [ 7.3325119, 12.256012 ]])

2. Basic Functions

자주 사용하는 transpose와 matrix multiplication은 따로 함수로 만들어준다

def _t(X):
    return np.transpose(X)

def _m(A,B):
    return np.matmul(A,B)

Activation Function으로는 Sigmoid만을 이용할 것이다. 하지만 추가로 ReLU와 Softmax 도 만들어서, 나중에 모델을 짤 때 원하는 것을 사용할 수 있게끔 한다.

이 세 함수 (sigmoid, ReLU, softmax)를 function으로 구현할 수도 있으나, dynamic learning을 하기 위해 class로 구현하였다. ( Dynamic Learning : data가 들어올 때마다 지속적 업데이트를 통해 데이터를 모델에 통합 )

class Sigmoid:
    def __init__(self):
        self.last_o = 1
    
    def __call__(self,X):
        self.last_o = 1/(1+np.exp(-X))
        return self.last_o
    
    def grad(self):
        return self.last_o*(1-self.last_o)
class ReLU:
    def __init__(self):
        self.last_o = 1
        
    def __call__(self,X):
        self.last_o = np.maximum(0,X)
        return self.last_o
    
    def grad(self):
        return np.where(self.last_o>0,1,0)
class Softmax:
    def __init__(self):
        self.last_o = 1
        
    def __call__(self,X):
        e_x = np.exp(X-np.max(X))
        self.last_o = e_x / e_x.sum()
        return self.last_o
    
    def grad(self):
        return self.last_o*(1-self.last_o)

마지막으로, MSE(Mean Squared Error) class를 구현한다. Binary Classifier이기 때문에 Log Loss (Binary Cross Entropy)로 구현하는 것이 나을 수 있으나, 주어진 과제에서는 MSE를 짜도록 요구했다.

class MSE:
    def __init__(self):
        self.dh = 1
        self.last_diff = 1
    
    def __call__(self,y,yhat):
        self.last_diff = y-yhat
        mse = 1/2*np.mean(np.square(y-yhat))
        return mse
    
    def grad(self):
        return self.last_diff

3. Network Architecture

1) Neuron

Layer를 구성하는 여러 개의 Neuron! 하나의 Neuron을 만드는 class를 생성한다.

class Neuron :
    def __init__(self,W,b,activation):
        self.W = W
        self.b = b
        self.act= activation()
        
        self.dW = np.zeros_like(self.W)  
        self.db = np.zeros_like(self.b)
        self.dh = np.zeros_like(_t(self.W)) # h =WT*X + b
        
        self.last_x = np.zeros((self.W.shape[0])) # to calculate grad_W, save the input(x)
        self.last_h = np.zeros((self.W.shape[1]))
        
    def __call__(self,x):
        self.last_x = x
        self.last_h = _m(_t(self.W),x) + self.b
        output = self.act(self.last_h)
        return output
    
    def grad(self): 
        grad = self.act.grad()*self.W
        return grad
    
    # let < u = WX+b > & < h = f(u) >
    def grad_W(self,dh): # dh/dW = dh/du * du/dW
        grad = np.ones_like(self.W) 
        grad_a = self.act.grad()   # dh/du     
        for j in range(grad.shape[1]):
            grad[:,j] = dh[j] * grad_a[j] * self.last_x     # previous gradient * dh/du * du/dW
        return grad
        
    def grad_b(self,dh) : # dh/db = dh/du * du/db
        grad = dh * self.act.grad() * 1  # previous gradient * dh/du * du/db
        return grad

2) Neural Network

Input 설명

  • input_num : input 뉴런의 개수
  • output_num : output 뉴런의 개수

  • hidden_depth : 얼마나 깊게 layer를 만들 것인지를 나타낸다. (즉, layer의 개수)
  • num_neuron : 하나의 layer를 구성하는 neuron의 개수
  • activation : activation function (1)
  • activation2 : activation function (2) ( network내에서 여러 종류의 activation function을 쓰고 싶은 경우 대비)
class NN:
    def __init__(self,input_num,output_num,  # 1) number of input &  2) number of output
                 hidden_depth,num_neuron, # 3) number of hidden layers & 4) neurons per layer
                 activation=Sigmoid, activation2=Softmax): # 5) 6) activation function
        def init_var(in_,out_):
            weight = np.random.normal(0,0.01,(in_,out_))
            bias = np.zeros((out_,))
            return weight,bias
           
    ## 1-1. Hidden Layer
        self.sequence = list() # lists to put neurons
        W,b = init_var(input_num,num_neuron)
        self.sequence.append(Neuron(W,b,activation)) # b ->0 ( no bias term in input-hidden layer )
    
        if hidden_depth>1 : # DNN
            for _ in range(hidden_depth-1):
                W,b = init_var(num_neuron,num_neuron)
                self.sequence.append(Neuron(W,b,activation)) # default : Sigmoid
    
    ## 1-2. Output Layer
        W,b = init_var(num_neuron,output_num)
        self.sequence.append(Neuron(W,b,activation2)) # default : Softmax
    
    def __call__(self,x):
        for layer in self.sequence:
            x = layer(x)
        return x
    
    def calc_grad(self,loss_fun):
        loss_fun.dh = loss_fun.grad()
        self.sequence.append(loss_fun)
        
        for i in range(len(self.sequence)-1, 0, -1):
            L1 = self.sequence[i]
            L0 = self.sequence[i-1]
            
            L0.dh = _m(L0.grad(), L1.dh)
            L0.dW = L0.grad_W(L1.dh)
            L0.db = L0.grad_b(L1.dh)
            
        self.sequence.remove(loss_fun)   

4. Gradient Descent

SGD (Stochastic Gradient Descent)를 사용하여 weight을 update할 것이다

def GD(nn,x,y,loss_fun,lr=0.01):
    loss = loss_fun(nn(x),y) # 1) FEED FORWARD
    nn.calc_grad(loss_fun) # 2) BACK PROPAGATION
    
    for layer in nn.sequence: # Update Equation
        layer.W += -lr*layer.dW
        layer.b += -lr*layer.db    
    return loss

5. Implement BackPropagation

  • num_input : 2
  • num_output : 2
  • hidden_depth : 1
  • num_neuron : 7
  • activation function은 전부 default값으로 sigmoid를 사용한다
  • 사용하는 loss function : MSE
  • EPOCH : 16
NeuralNet = NN(2,2,1,7)
loss_fun = MSE()
EPOCH = 16
loss_per_epoch = []
for epoch in range(EPOCH):
    for i in range(X2.shape[0]):
        loss = GD(NeuralNet,X2[i],y2[i],loss_fun,0.01)
    loss_per_epoch.append(loss)
    print('Epoch {} : Loss {}'.format(epoch+1, loss))
Epoch 1 : Loss 0.18218546700245103
Epoch 2 : Loss 0.1699588665083517
Epoch 3 : Loss 0.1317847633702473
Epoch 4 : Loss 0.0871812251857476
Epoch 5 : Loss 0.055367214705294654
Epoch 6 : Loss 0.036283073852796305
Epoch 7 : Loss 0.025060430768749416
Epoch 8 : Loss 0.01822353886098449
Epoch 9 : Loss 0.013846415453448049
Epoch 10 : Loss 0.010904192049834446
Epoch 11 : Loss 0.008839350381845591
Epoch 12 : Loss 0.007335897223110011
Epoch 13 : Loss 0.0062065922468843215
Epoch 14 : Loss 0.005335701415371787
Epoch 15 : Loss 0.004648900081571788
Epoch 16 : Loss 0.0040968068434452136

6. Error

loss가 급격히 줄어들어 0에 매우 가까워짐을 확인할 수 있다. (데이터가 단순하여 네트워크를 복잡하게 짜지 않아도 쉽게 풀 수 있는 문제이다 )

plt.plot(loss_per_epoch)
plt.xlabel('# of Epoch')
plt.ylabel('Loss')
plt.title('Shallow NN',fontsize=20)
Text(0.5, 1.0, 'Shallow NN')

7. 추가

  • Acitvation function을 Sigmoid 대신 ReLU로,
  • Hidden Layer를 1개 대신 여러 개로 (DNN) 만들 수 있다

EX)

  • activation function : ReLU
  • number of hidden layers : 3
  • number of nodes in one hidden layers : 4
NeuralNet2 = NN(2,2,3,4,activation=ReLU)
loss_fun = MSE()
EPOCH = 16
loss_per_epoch2 = []
for epoch in range(EPOCH):
    for i in range(X2.shape[0]):
        loss = GD(NeuralNet2,X2[0],y2[0],loss_fun,0.01)
    loss_per_epoch2.append(loss)
    print('Epoch {} : Loss {}'.format(epoch+1, loss))
Epoch 1 : Loss 0.0024158587895954558
Epoch 2 : Loss 0.001027153615391233
Epoch 3 : Loss 0.0006425978569413866
Epoch 4 : Loss 0.00046523067973551046
Epoch 5 : Loss 0.00036374133584255537
Epoch 6 : Loss 0.00029820837128012546
Epoch 7 : Loss 0.0002524756861422122
Epoch 8 : Loss 0.00021878345054632167
Epoch 9 : Loss 0.00019294880735867928
Epoch 10 : Loss 0.00017252085371608517
Epoch 11 : Loss 0.00015596965442570948
Epoch 12 : Loss 0.00014229145060395038
Epoch 13 : Loss 0.00013080070123516405
Epoch 14 : Loss 0.00012101342499682807
Epoch 15 : Loss 0.00011257830292046774
Epoch 16 : Loss 0.00010523420412829551

훨씬 더 빠르게 Error가 줄어듬을 확인할 수 있다 ( 모델을 더 복잡하게 짰기 때문에 당연한 결과일 수도 )

plt.plot(loss_per_epoch2)
plt.xlabel('# of Epoch')
plt.ylabel('Loss')
plt.title('Deep NN with 5 hidden layers & ReLU',fontsize=20)