이번에는 기억을 되살려 tensorflow, pytorch를 사용하지 않고 파이썬만을 사용하여 Multi Layer Perceptron(MLP)를 구현해보도록 하겠다. 구현할 함수는 딱 4개밖에 없다.
구현할 것들
- backpropagation 역전파
- Mean Squared Error(MSE) loss
- sigmoid함수
- PReLU 함수
0. 기본
Linear Layer 의 기본 컨셉은 아래와 같다.
out = w*x + b
참 간단하다. x는 내가 넣는 Input값을, w는 weight(가중치), b는 bias(편향), out은 결과값이다. 이 선형층을 여러 개 쌓아 올리면 MLP가 된다.
Loss는 간단하게 MSELoss를 사용해보도록 하자.
MSELoss = 1/n * ∑(out - y)**2
MLP의 output으로 나온 값과 실제값의 평균제곱오차다. y와 label값이 같다면 loss는 0이다. 우리의 목적은 이 loss의 값을 최대한 줄이는 것이다.
여기서 이제 학습을 진행하기 위해서 backpropagation(역전파)를 진행한다. 역전파란 편미분 값을 이용해 w, b의 값을 경신해주는 것이다. 자세한 설명은 아래 코드를 보면서 이해해보도록 하자.
이해를 돕기 위해 1개의 layer로 학습을 진행한다고 생각을 해보자.
out = x*w + b
loss = (out-y)**2의 평균
##역전파 진행
w = w + learning_rate * dloss/dw
b = b + learning_rate * dloss/db
역전파 후 가중치 업데이트를 위해서 loss의 편미분값이 필요함을 알았다. 편미분은 chain rule을 통해 구할 수 있다. 이제부터 코드로 살펴보자.
1. numpy로 함수 구현하기
softmax
softmax는 딥러닝에서 자주 쓰이는 activate function(활성화 함수)다. 인자 값들의 합이 1이 되도록 맞춰주는 역할을 한다.
import numpy as np
def softmax(arr):
exps = np.exp(arr)
return exps / np.sum(exps)
sigmoid
softmax와 마찬가지로 쓰이는 활성화함수로, binary classification으로 쓰인다. 미분 함수가 간단하기 때문에 이번 구현에서는 이 함수를 사용할 예정이다.
def sigmoid(arr):
exps = np.exp(arr)
return exps / (1 + exps)
MSELoss
위에서 언급한 평균제곱오차를 구하는 손실 함수이다. np.square는 값을 제곱해주는 기능을 갖고 있다.
def MSELoss(preds, labels):
n = len(preds)
return np.sum(np.square(preds - labels)) / n
Backpropagation
위에서 역전파는 편미분을 통해 가중치를 갱신시키는 것으로 이해했다. 여기서 chain rule을 사용하면 되는데, 이 개념은 아래와 같이 간단히 이해할 수 있다.
chain rule을 이해해보자
dx/dy = 1
dy/dz = 2
dx/dz = dx/dy * dy/dz = 1 * 2 = 2
자 그럼 이 이해를 바탕으로 간단하게 역전파 함수를 작성할 수 있다.
def backpropagation(x, w, b, y, loss, learning_rate=1e-3):
dy_dw = x #shape : (n, in_size)
dy_db = 1
dloss_dy = np.sqrt(loss) * (-1/2) #shape : (n, out_size)
dloss_dw = np.matmul(np.transpose(dy_dw),dloss_dy)#shape : (in_size, out_size)
dloss_db = dloss_dy* dy_db
w = w + learning_rate * dloss_dw
b = b + learning_rate * dloss_db
return w, b
ReLU
딥러닝의 장점 중 하나는 비선형함수라는 것이다. 어라? 방금까지는 y = w*x + b에 기반하여 코드를 작성하지 않았는가. 선형층 바로 뒤에 ReLU와 같은 활성화 함수를 붙여주면 비선형으로 활용할 수 있다. ReLU가 뭐길래? 딥러닝을 공부하며 많이 봤을 그 함수다!
ReLU = x (if x>0)
0 (else)
하지만 relu를 쓰면 너무 뻔하니까, 이번엔 PReLU를 써보자.
PReLU = x (if x >0)
a*x (else)
pytorch 공식문서에서 a의 기본값을 0.25로 주었으니 나도 default a=0.25로 주겠다.
def PReLU(x, a = 0.25):
zeros = np.zeros(x.shape)
return np.max([zeros, x], axis = 0) + a * np.min([zeros, x], axis = 0)
2. Neural Network 학습코드
이제 이 single neural network를 학습해보자. 먼저 이 단일 layer의 클래스를 작성해주면 아래와 같다.
class Linear:
def __init__(self, in_size, out_size, epochs = 10, learning_rate = 1e-3):
self.epochs = epochs
self.learning_rate = learning_rate
self.in_size = in_size
self.out_size = out_size
self._initialize_weights()
def _initialize_weights(self):
self.weight = np.random.rand(self.in_size, self.out_size)
self.bias = np.random.rand(self.out_size)
def _forward(self, x):
out = np.matmul(x, self.weight) + self.bias
return out
def train(self, x, y):
for e in range(self.epochs):
out = self._forward(x)
loss, loss_matrix = MSELoss(out, y)
self.weight, self.bias = backpropagation(x, self.weight, self.bias, y, loss_matrix, learning_rate = self.learning_rate)
print(f'{e+1}번째 epoch의 loss는 {loss}')
위에서 구현했던 softmax와 backpropagation, MSELoss가 쓰였음을 알 수 있다. 실제로 작동이 되나 알아보기 위해 임의로 x와 y를 아무거나 주고 돌려보자. w와 b는 간단히 random으로 초기화해주자. epoch은 10, learning rate는 0.001을 주도록 하겠다.
x = np.array([[0,1,0,1], [10,10,10,2], [0,0,0,3]]) # shape 3,4
y = np.array([[0],[1],[0]])
linear = Linear(x.shape[-1], y.shape[-1])
linear.train(x, y)
생각보다 loss가 epoch마다 잘 줄어드는 것을 볼 수 있다.
cf.) 참고로 예시 코드에서는 가중치 초기화를 대충 해주었는데, 실전에선 당연히 initalize를 잘해줘야 한다. 괜히 he_initialize, kaiming_initiallize 함수가 있는 게 아니다. 초기 가중치를 잘 설정해주는 것이 학습에 중요하다는 것은 여러 논문에서 밝혀졌으니 제대로 구현할 때는 이 부분을 신경 쓰면 좋다.
3. MLP 학습 코드
자 이제 이번 포스팅의 제목이었던 MLP를 구현해보자. multi layer perceptron 이름 그대로 linear층을 여러 개 쌓으면 그게 MLP다. 코드 구조는 클래스 Linear와 비슷하게 가보도록 하겠다. 이번 Linear 클래스에는 활성화 함수를 인자로 받아보자. 그러기 위해서 함수로 정의했던 softmax와 PReLU 함수, 그리고 MSELoss도 클래스로 다시 정의하자.
클래스 내에 미분 값을 구하는 함수도 넣어주었다.
class PReLU:
def __init__(self, a=0.25):
self.a = a
def __call__(self, x):
zeros = np.zeros(x.shape)
self._z = np.max([zeros, x], axis = 0) + self.a * np.min([zeros, x], axis = 0)
return self._z
def derivative(self):
x, y = self._z.shape
zeros = np.zeros((x,y))
for i in range(x):
for j in range(y):
if self._z[i][j] >0:
zeros[i][j] = 1 #x를 미분하면 1
elif self._z[i][j] <0:
zeros[i][j] = self.a
return zeros
class Softmax:
def __call__(self, x):
exps = np.exp(x)
return exps / np.sum(exps)
class Sigmoid:
def __call__(self, x):
exps = np.exp(x)
self._sigmoid = exps / (1+exps)
return self._sigmoid
def derivative(self):
return self._sigmoid * (1-self._sigmoid)
class MSELoss:
def __call__(self, preds, labels):
self._loss = np.square(preds - labels)
return np.mean(self._loss)
def derivative(self):
return np.sqrt(self._loss) * (-1/2)
MLP예제에서는 활성화 함수를 쓸 것이다. 따라서 backpropagation 함수를 새로 정의해야 할 필요가 있다. 아래와 같이 간략히 생각해볼 수 있겠다.
이전 : y = w*x + b
loss = MSELoss(y,' y)
이후 : y = w*x + b
z = act_fn(y)
loss = MSELoss(y', z)
def backpropagation_with_actfn(x, w, b, z, dz_dy,learning_rate=1e-3):
#dz_dy shape : n, out_size
dy_dw = x #shape : n, in_size
dy_db = 1
dz_dw = np.matmul(np.transpose(dy_dw), dz_dy) #dz_dy * dy_dw , shape : in_size, out_size
dz_db = dz_dy * dy_db #dz_dy * dy_db , shape n, out_size
w = w + learning_rate * dz_dw
b = b + learning_rate * dz_db
return w, b
이에 따라 Linear함수도 새롭게 NewLinear 클래스로 작성해보자. 이전과 달라진 점은 활성화 함수를 받는다는 것이다.
class NewLinear:
def __init__(self, in_size, out_size, act_fn = 'PReLU'):
self.in_size = in_size
self.out_size = out_size
if act_fn == 'PReLU':
self.act_fn = PReLU()
elif act_fn == 'Softmax':
self.act_fn = Softmax()
elif act_fn == 'Sigmoid':
self.act_fn = Sigmoid()
self._initialize_weights()
def _initialize_weights(self):
self.weight = np.random.rand(self.in_size, self.out_size)
self.bias = np.random.rand(self.out_size)
def __call__(self, x):
out = np.matmul(x,self.weight) + self.bias
out = self.act_fn(out)
return out
def update(self, x, z, learning_rate):
dz_dy = self.act_fn.derivative()
self.weight, self.bias = backpropagation_with_actfn(x, self.weight, self.bias, z, dz_dy, learning_rate = learning_rate)
이제 train class를 정의하겠다. 간단하게 모든 층의 node 수가 32로 같다고 생각하고 단순하게 짜 보자. 마지막 레이어가 아닌 모든 층들의 활성화 함수는 PReLU를 줄 것이다.
class Train:
def __init__(self, x, y, n_layers =3, n_node = 32, epochs=10, learning_rate=1e-3):
self.epochs = epochs
self.learning_rate = learning_rate
self.layers = []
self.loss_fcn = MSELoss()
self.n_layers = n_layers
for i in range(n_layers):
act_fn = 'PReLU'
in_shape = n_node
out_shape = n_node
if i == 0: #첫번째 레이어
in_shape = x.shape[-1]
if i == n_layers-1: #마지막 레이어
out_shape = y.shape[-1]
act_fn = 'Sigmoid'
self.layers.append(NewLinear(in_shape, out_shape, act_fn))
self._train(x, y)
def _forward(self, x):
outs = []
out = x
for layer in self.layers:
out = layer(out)
outs.append(out)
return outs, out #outs에는 지금까지 layer들의 출력값이 담겨있다.
def _backpropagation(self, x, outs):
last = x
zipped = list(zip(self.layers, outs))
for i, (layer, out) in enumerate(zipped):
if i == self.n_layers - 1: #마지막 레이어일 때
out = out * self.loss_fcn.derivative()
layer.update(last, out, self.learning_rate)
last = out
def _train(self, x, y):
for e in range(self.epochs):
outs, out = self._forward(x)
loss = self.loss_fcn(out, y)
outs.append(loss) #각 layer의 역전파를 위해 쓰일 배열이다.
self._backpropagation(x, outs)
print(f'{e+1}번째 epoch의 loss는 {loss}')
이제 MLP를 실행해보자.
Train(x, y,n_layers=5, n_node=32)
잘 작동한다.
요즘 pytorch를 많이 쓰다 보니 구현이 pytorch 스럽게 된 듯하다. 개념을 알고 있다고 해도 항상 ML용 라이브러리만 꺼내 쓰다 보니 가끔씩은 이렇게 연습해볼까 한다. 간만에 하드코딩(?)을 하니 기분이 좋구만!
'머신러닝 > 파이썬 구현 머신러닝' 카테고리의 다른 글
pytorch 공식 구현체로 보는 transformer MultiheadAttention과 numpy로 구현하기 (0) | 2023.07.01 |
---|---|
딥러닝 이론 optimizer 정리 - GD , SGD, Momentum, Adam (0) | 2022.05.14 |
파이썬으로 기초 CNN 구현하기 1 - conv, pooling layer (2) | 2022.04.17 |
파이썬과 기초 딥러닝 개념- 이론편 1 (0) | 2022.04.03 |
파이썬으로 기초 RNN 구현하기 (1) | 2022.03.27 |