본문 바로가기
파이썬 프로그래밍/Numpy 딥러닝

18. 다층 퍼셉트론(MPL)의 등장-2.비선형 회귀식(실습)

by Majestyblue 2022. 1. 21.

저번시간에 키와 몸무게의 관계를 예측하는 모델을 만들기 위해 다층 퍼셉트론과 ReLU 활성화함수를 이용하고 순방향과 역전파를 수학적으로 표현해 보았다. 이번 시간에는 수학적 표현을 Numpy 코드로 표현해 보고 Pytorch 결과와 비교해보도록 하겠다.

 

 

 

 

1. 데이터 정의

저번시간에 정의한 데이터, 가중치 및 편향 설정이다.

weight_height.csv
0.00MB
몸무게(weight)에 따른 키(height)

1) 입력 : input (batch_size, 1)

2) 목표값 : target (batch_size, 1)

3) 가중치 및 편향

(1) G1 가중치 : W1 (8, 1) , G1 편향 : B1 (8, 1)

(2) G2 가중치 : W2 (4, 8) , G1 편향 : B2 (4, 1)

(3) G3 가중치 : W3 (1, 4) , G1 편향 : B3 (1, 1)

 

 

코드로 표현하면 아래와 같다. loss 계산에서 overflow가 발생하여 0.01을 데이터에 곱해 주었다.

import numpy as np
from numpy import genfromtxt
import matplotlib.pyplot as plt

data = genfromtxt('weight_height.csv', delimiter=',', skip_header = 1)
inputs = data[:, 0].reshape(-1, 1) * 0.01 # np.power에서 overflow 방지
targets = data[:, 1].reshape(-1, 1) * 0.01 # np.power에서 overflow 방지

np.random.seed(220120)

W1 = np.random.randn(8, 1)
B1 = np.random.randn(8, 1)
W2 = np.random.randn(4, 8)
B2 = np.random.randn(4, 1)
W3 = np.random.randn(1, 4)
B3 = np.random.randn(1, 1)

learning_rate = 0.1

 

 

 

 

 

2. 순전파

저번시간에 순방향 전파 연산을 선형 연산 G, 활성화 함수 ReLU의 조합으로 아래와 같이 정의했었다. 순서대로 하나씩 코드로 표현해 보자.

 

순방향 전파 연산

 

 

1) G1 연산과 R1 연산은 아래와 같다.

 

G1 연산

 

R1연산

 

 X = np.transpose(input, (1, 0)) # (1, batch)
    
 G1 = np.dot(W1, X) + B1 # (8, batch)
 
 R1 = np.maximum(0, G1) # (8, batch)

 

 

 

 

2) G2연산과 R2 연산은 아래와 같다.

 

G2 연산

 

R2 연산

 

G2 = np.dot(W2, R1) + B2 # (4, batch)
    
R2 = np.maximum(0, G2) # (4, batch)

 

 

 

 

3) G3연산, pred, loss는 아래와 같다.

 

G3연산, 연산 결과를 transpose(batch, 1) 하여 Pred로 지정한다.

 

오차함수 loss

 

G3 = np.dot(W3, R2) + B3 # (1, batch)
    
pred = np.transpose(G3, (1, 0)) # (batch, 1)
    
loss = np.mean(np.power(pred-target, 2)) #(1, 1)

 

 

 

 

 

 

3. 역전파

역전파를 이용하여 가중치와 편향에 대한 오차함수의 도함수를 저번시간에 구하였다.

 

 

 

1) ∂loss/∂G3 도함수는 아래와 같다.

 

출력값(G3)에 대한 오차함수의 도함수

 

위의 식에서 pred는 출력값 G3다. (pred는 G3 출력값의 전치 transpose)

target = np.transpose(target, (1, 0)) # (1, batch)
    
dL_dG3 = 2*(G3-target) / len(target[0]) # (1, batch)

 

 

 

2) ∂G(i)/∂R(i-1), ∂R(i)/∂G(i), ∂G(i)/∂W(i), ∂G(i)/∂B(i) 는 아래와 같다.

 

선형연산 G(i)에 대한 이전 활성화함수R(i-1) 도함수, 활성화함수 R(i)에 대한 선형연산 G(i)의 도함수

 

선형연산 G(i) = W(i)X(i) + B(i)에 대한 W(i)와 B(i)의 도함수

 

dG3_dR2 = np.transpose(W3, (1, 0)) # (4, 1)
    
dG3_dW3 = np.transpose(R2, (1, 0)) # (batch, 4)
    
dG3_dB3 = np.ones_like(G3) # (1, 1)

dR2_dG2 = np.ones_like(G2) # (4, batch)
    
dG2_dR1 = np.transpose(W2, (1, 0)) # (8, 4)
    
dG2_dW2 = np.transpose(R1, (1, 0)) # (batch, 8)
    
dG2_dB2 = np.ones_like(G2) # (4, 1)
    
dR1_dG1 = np.ones_like(G1) # (8, batch)
    
dG1_dW1 = input # (batch, 1) 왜냐면 input의 전치가 연산되었는데 다시 전치하므로 원래 모양이 됨.
    
dG1_dB1 = np.ones_like(G1) # (8, 1)

 

 

 

 

3) ∂loss/∂W3, ∂loss/∂B3는 아래와 같다.

 

 

 

# operation W3, B3
dL_dW3 = np.dot(dL_dG3, dG3_dW3) # (1, 4)
    
dL_dB3 = np.sum(dL_dG3, keepdims=True) * dG3_dB3 # (1, 1)

 

 

 

 

 

 

4) ∂loss/∂W2, ∂loss/∂B2는 아래와 같다.

 

 

 

# operation W2, B2
dL_dG2 = np.dot(dG3_dR2, dL_dG3) * dR2_dG2 # (4, batch)
    
dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (4, 8)
    
dL_dB2 = np.sum(dL_dG2, axis = 1, keepdims=True) * dG2_dB2 # (4, 1)

 

 

 

 

 

 

5) ∂loss/∂W1, ∂loss/∂B1는 아래와 같다.

 

 

 

# operation W1, B1
dL_dG1 = np.dot(dG2_dR1, dL_dG2) * dR1_dG1 # (8, batch)
    
dL_dW1 = np.dot(dL_dG1, dG1_dW1) # (8, 1)
    
dL_dB1 = np.sum(dL_dG1, axis=1, keepdims=True) * dG1_dB1 # (8, 1)

 

 

 

 

 

 

4. 경사하강법

각 가중치와 편향에 대해 경사하강법을 실시한다.

 

for i in range(2000): 
    dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3 = loss_gradient(inputs, targets, W1, B1, W2, B2, W3, B3) 
    W1 = W1 + -1*learning_rate * dL_dW1
    B1 = B1 + -1*learning_rate * dL_dB1
    W2 = W2 + -1*learning_rate * dL_dW2
    B2 = B2 + -1*learning_rate * dL_dB2
    W3 = W3 + -1*learning_rate * dL_dW3
    B3 = B3 + -1*learning_rate * dL_dB3

 

 

 

 

 

 

5. 결과 확인하기

input에 대한 원래 목표값 targets 그래프를 빨간색 점('ro')로 지정하고

input에 대한 모델 예측값 pred 그래프를 파란색 점('bo')로 지정하였다.

# overflow 방지를 위해 0.01을 곱하였으므로 되돌리기 위해 100을 곱해줌
plt.plot(inputs*100, targets*100, 'ro', label='target')
plt.plot(inputs*100, pred*100, 'bo', label='pred')
plt.xlabel('weight', size=15)
plt.ylabel('height', size=15)
plt.legend()
plt.show()

 

 

 

 

키-몸무게 처럼 비교적 비규칙적이고 복잡한 데이터일 때 다중 퍼셉트론을 이용한 비선형 회귀가 매우 적합했다는 것을 알 수가 있다.

 

 

 

 

 

6. 성능을 올리고 싶다면

이 예제에서는 비교적 간단한 계산을 위해 노드 수를 제안하였다. 이 강의를 보거나 따라가면서 학습중이라면 노드 수를 바꾸어 보거나(증가가 좋을 것이다) 은닉층을 1개 더 늘려 2개의 은닉층을 만들어서 진행한다면 훨씬 더 좋은 예측 모델을 만들 수 있을 것이다.

데이터 feature의 수가 노드 개수라고 생각하면 쉽다. 

 

 

 

 

 

7. 전체코드

import numpy as np
from numpy import genfromtxt
import matplotlib.pyplot as plt

data = genfromtxt('weight_height.csv', delimiter=',', skip_header = 1)
inputs = data[:, 0].reshape(-1, 1) * 0.01 # np.power에서 overflow 방지
targets = data[:, 1].reshape(-1, 1) * 0.01 # np.power에서 overflow 방지

np.random.seed(220120)

W1 = np.random.randn(8, 1)
B1 = np.random.randn(8, 1)
W2 = np.random.randn(4, 8)
B2 = np.random.randn(4, 1)
W3 = np.random.randn(1, 4)
B3 = np.random.randn(1, 1)

learning_rate = 0.1

def forward(input, target, W1, B1, W2, B2, W3, B3):
    #G1 operation
    
    X = np.transpose(input, (1, 0)) # (1, batch)
    
    G1 = np.dot(W1, X) + B1 # (8, batch)
    
    R1 = np.maximum(0, G1) # (8, batch)
    
    G2 = np.dot(W2, R1) + B2 # (4, batch)
    
    R2 = np.maximum(0, G2) # (4, batch)
    
    G3 = np.dot(W3, R2) + B3 # (1, batch)
    
    pred = np.transpose(G3, (1, 0)) # (batch, 1)
    
    loss = np.mean(np.power(pred-target, 2)) #(1, 1)
    
    return G1, R1, G2, R2, G3, pred, loss


def loss_gradient(input, target, W1, B1, W2, B2, W3, B3):
    G1, R1, G2, R2, G3, _, _ = forward(inputs, targets, W1, B1, W2, B2, W3, B3)
    
    target = np.transpose(target, (1, 0)) # (1, batch)
    
    dL_dG3 = 2*(G3-target) / len(target[0]) # (1, batch)
    
    dG3_dR2 = np.transpose(W3, (1, 0)) # (4, 1)
    
    dG3_dW3 = np.transpose(R2, (1, 0)) # (batch, 4)
    
    dG3_dB3 = np.ones_like(B3) # (1, 1)

    dR2_dG2 = np.ones_like(G2) # (4, batch)
    
    dG2_dR1 = np.transpose(W2, (1, 0)) # (8, 4)
    
    dG2_dW2 = np.transpose(R1, (1, 0)) # (batch, 8)
    
    dG2_dB2 = np.ones_like(B2) # (4, 1)
    
    dR1_dG1 = np.ones_like(G1) # (8, batch)
    
    dG1_dW1 = input # (batch, 1) 왜냐면 input의 전치가 연산되었는데 다시 전치하므로 원래 모양이 됨.
    
    dG1_dB1 = np.ones_like(B3) # (8, 1)
    
    # chain rule
    # operation W3, B3
    dL_dW3 = np.dot(dL_dG3, dG3_dW3) # (1, 4)
    
    dL_dB3 = np.sum(dL_dG3, keepdims=True) * dG3_dB3 # (1, 1) 
    
    # operation W2, B2
    dL_dG2 = np.dot(dG3_dR2, dL_dG3) * dR2_dG2 # (4, batch)
    
    dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (4, 8)
    
    dL_dB2 = np.sum(dL_dG2, axis = 1, keepdims=True) * dG2_dB2 # (4, 1)
    
    # operation W1, B1
    dL_dG1 = np.dot(dG2_dR1, dL_dG2) * dR1_dG1 # (8, batch)
    
    dL_dW1 = np.dot(dL_dG1, dG1_dW1) # (8, 1)
    
    dL_dB1 = np.sum(dL_dG1, axis=1, keepdims=True) * dG1_dB1 # (8, 1)

    return dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3
    


_, _, _, _, _, pred, loss = forward(inputs, targets, W1, B1, W2, B2, W3, B3)
#print('before pred', pred) 
print('before loss', loss) # 1.12909730181823

for i in range(2000): 
    dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3 = loss_gradient(inputs, targets, W1, B1, W2, B2, W3, B3) 
    W1 = W1 + -1*learning_rate * dL_dW1
    B1 = B1 + -1*learning_rate * dL_dB1
    W2 = W2 + -1*learning_rate * dL_dW2
    B2 = B2 + -1*learning_rate * dL_dB2
    W3 = W3 + -1*learning_rate * dL_dW3
    B3 = B3 + -1*learning_rate * dL_dB3
    
_, _, _, _, _, pred, loss = forward(inputs, targets, W1, B1, W2, B2, W3, B3)
#print('after pred', pred) 
print('after loss', loss) # 0.0031031677478338834

# overflow 방지를 위해 0.01을 곱하였으므로 되돌리기 위해 100을 곱해줌
plt.plot(inputs*100, targets*100, 'ro', label='target')
plt.plot(inputs*100, pred*100, 'bo', label='pred')
plt.xlabel('weight', size=15)
plt.ylabel('height', size=15)
plt.legend()
plt.show()

 

 

 

 

 

 

8. Pytorch와 비교

Numpy로 만든 다층 퍼셉트론과 Pytorch의 다층 퍼셉트론을 비교해 보자. hyper parameters는 달라도 node 수는 같다. 아래는 전체 코드와 결과이다.

 

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from numpy import genfromtxt
import matplotlib.pyplot as plt

torch.manual_seed(220119)

data = genfromtxt('weight_height.csv', delimiter=',', skip_header = 1)
data_input = data[:, 0].reshape(-1, 1)
data_target = data[:, 1].reshape(-1, 1)

input = torch.FloatTensor(data_input)*0.1
target = torch.FloatTensor(data_target)*0.1


model = nn.Sequential(
    nn.Linear(1, 8),
    nn.ReLU(),
    nn.Linear(8, 4),
    nn.ReLU(),
    nn.Linear(4, 1)
)

pred = model(input)
#print('before pred', pred)
loss = F.mse_loss(pred, target)
print('before loss', loss)
optimizer = optim.SGD(model.parameters(), lr=0.01)
epoches = 3000

for epoche in range(epoches + 1):
    pred = model(input)
    
    loss = F.mse_loss(pred, target)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()


pred = model(input)
#print('after pred', pred)
loss = F.mse_loss(pred, target)
print('after loss', loss)
print(list(model.parameters()))


plt.plot(input*10, target*10, 'ro', label='target')
plt.plot(input*10, pred.detach()*10, 'bo', label='pred')
plt.xlabel('weight', size=15)
plt.ylabel('height', size=15)
plt.legend()
plt.show()

 

 

 

역시 대표적 머신러닝 라이브러리 답게 같은 구조의 다층 퍼셉트론을 사용했음에도 불구하고 학습은 Pytorch가 더 잘 된 것같다. (사실 Pytorch는 과적합 되었다고 볼 수 있으므로 새로운 데이터에서는 Numpy 다층 퍼셉트론보다 틀릴 가능성이 높다)

 

 

 

다음부터는 딥러닝 성능을 올리는 여러 가지 방법을 알아보자 첫 번째로 지금 사용하는 경사하강법은 보다 많고 복잡한 데이터에서 효율이 떨어지는데 어떻게 개선할 수 있는지 알아보겠다.