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)