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

25 - Deep Neural Nets 구현하기

by Majestyblue 2022. 4. 20.

저번 시간까지 다층 퍼셉트론(Multi-Layer Perceptron, MLP)을 이용하여 학습을 시켜보았다. 

 

정의에 대해 조금 더 이야기를 해 보자면

일반적으로 MLP는 고전적인, 완전 연결 신경망으로 볼 수 있는데

'일반적으로' 3개의 층에 sigmoid, tanh의 활성화 함수를 가진 신경망을 이야기한다.

(본인의 예제에서 활성화 함수를 relu를 사용하였지만...)

 

여기서 DNN은 더 확장하여 순환(RNN, LSTM)을 할 수 있다던지, 완전 연결이 아니라던지, 활성화 함수가 0 또는 1이 아니라던지 등등 더욱 포괄적이고 상위적인 개념이 포함된다. 단순히 은닉 층 개수로 나눌 수는 없다.

 

제목은 DNN으로 거창하게 하였지만, 사실 MLP는 DNN의 하위개념이므로(...) 구현 상 큰 차이가 없다. DNN과 MLP의 차이는 무엇이냐에 대해 사실 많은 언쟁이 오간다. (https://stats.stackexchange.com/questions/315402/multi-layer-perceptron-vs-deep-neural-network) 참고해 보길 바란다.

 

이번 예제에서는 이전시간 MLP로 구현했던 콘크리트 강도 예측하기(https://toyourlight.tistory.com/38)를 히든 레이어 1개를 더 추가하여 2개의 히든 레이어로 학습하는 DNN으로 표현해 볼 것이다. 

 

구현 과정은 MLP 포스트를 참고해 보는 것이 좋다.

(17. 다층 퍼셉트론(MPL)의 등장-2.비선형 회귀식(심화이론))

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

 

 

 

1. 전처리 하기

데이터 전처리하기(https://toyourlight.tistory.com/38)를 참고해 보자. 표준화(Standardization) 해준다.

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

np.random.seed(20220214)
data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)
norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)

 

 

 

 

2. 초기화 및 가중치 설정

데이터 셋을 훈련 세트와 테스트 세트로 분류한다. 1000개를 훈련 세트, 마지막 30개를 테스트 세트로 할 것이다.

train_data = norm_data[:1000, :]
test_data = norm_data[1000:, :]

inputs = train_data[:, 0:8]
targets = train_data[:, -1:]

test_inputs = test_data[:, 0:8]
test_targets = test_data[:, -1:]

 

콘크리트 강도 예측하기 데이터셋의 특성(feature)는 8개임을 기억하자. 그렇다면 입력, 가중치, 편향 및 출력 shape은 아래와 같다.

 

 

다이어그램을 그리면 아래와 같다.

 

 

구현 코드이다.

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

learning_rate = 0.001

batch_size = 256
steps=0
epochs = 3000

 

 

 

 

만약 다른 노드 개수로 하고 싶으면 아래와 같이 구성봐도 나쁘지 않을 것이다. (그렇다면 learning_rate, batch_size, epochs 같은 Hyperparameter도 바뀌어야 한다.)

 

W1 = np.random.randn(32, 8)
B1 = np.random.randn(32, 1)
W2 = np.random.randn(16, 32)
B2 = np.random.randn(16, 1)
W3 = np.random.randn(8, 16)
B3 = np.random.randn(8, 1)
W4 = np.random.randn(1, 8)
B4 = np.random.randn(1, 1)

 

 

 

 

 

3. 순전파(forward) 정의

순전파를 정의한다. 순전파 함수에는 아래와 같은 순서도 진행되며 오차도 계산한다.

G연산과 R 연산은 위의 포스트를 참고하길 바란다.

 

오차함수(Loss function)는 MSE를 이용한다.

 

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

 

 

 

 

 

4. 역전파(backward)

MLP에서 히든 레이어가 1개만 더 추가된 경우이므로 가장 깊은 곳의 가중치와 편향인 W1, B1을 구해보겠다.

그 외의 내용은 위 포스트를 참고하시라. MLP와 큰 차이가 없다.

 

 

 

 

 

 

구현 방법은 MLP 포스트를 참고해 보자. 각 변수에 대한 도함수를 정의하고, 연쇄법칙(Chain rule)을 이용하여 오차에  대한 가중치와 편향의 도함수를 구하는 방법이다.

 

def backward(input, target, G1, R1, G2, R2, G3, R3, G4):
    target = np.transpose(target, (1, 0)) # (1, batch)
    
    dL_dG4 = 2*(G4-target) / len(target[0]) # (1, batch)
    
    dG4_dW4 = np.transpose(R3, (1, 0)) # (batch, 8)
    
    dG4_dB4 = np.ones_like(B4) # (1, batch)
    
    dG4_dR3 = np.transpose(W4, (1, 0)) # (8, 1)
    
    dR3_dG3 = np.where(R3>0, 1, 0) # (8, batch)
    
    dG3_dW3 = np.transpose(R2, (1, 0)) # (batch, 8)
    
    dG3_dB3 = np.ones_like(B3) # (8, batch)
    
    dG3_dR2 = np.transpose(W3, (1, 0)) # (8, 8)
    
    dR2_dG2 = np.where(R2>0, 1, 0) # (8, batch)
    
    dG2_dW2 = np.transpose(R1, (1, 0)) # (batch, 8)
    
    dG2_dB2 = np.ones_like(B2) # (8, batch)
    
    dG2_dR1 = np.transpose(W2, (1, 0)) # (8, 8)
    
    dR1_dG1 = np.where(R1>0, 1, 0) # (8, batch)
    
    dG1_dW1 = input # (batch, 8)
    
    dG1_dB1 = np.ones_like(B1) # (8, batch)
    
    #chain rule
    
    #operation W4, B4
    dL_dW4 = np.dot(dL_dG4, dG4_dW4) # (1, 8)
    
    dL_dB4 = np.sum(dL_dG4*dG4_dB4, keepdims=True) # (1, 1)
    
    #operation W3, B3
    dL_dG3 = np.dot(dG4_dR3, dL_dG4) * dR3_dG3
    
    dL_dW3 = np.dot(dL_dG3, dG3_dW3) # (8, 8)

    dL_dB3 = np.sum(dL_dG3*dG3_dB3, axis=1, keepdims=True) # (8, 1)

    #operation W2, B2
    dL_dG2 = np.dot(dG3_dR2, dL_dG3) * dR2_dG2
    
    dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (8, 8)
    
    dL_dB2 = np.sum(dL_dG2*dG2_dB2, axis=1, keepdims=True) # (8, 1)
    
    #operation W1, B1
    dL_dG1 = np.dot(dG2_dR1, dL_dG2) * dR1_dG1
    
    dL_dW1 = np.dot(dL_dG1, dG1_dW1) # (8, 8)
    
    dL_dB1 = np.sum(dL_dG1*dG1_dB1, axis=1, keepdims=True) # (8, 1)
    
    return dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3, dL_dW4, dL_dB4

 

 

 

결과는 어떻게 되었을까?

 

너무 큰 기대를 했을까? MLP랑 큰 차이가 없는 듯 하다. 사실 당연한 이유인게, 안정성이 매우 뒤떨어지는 날것 그대로의 코드이기 때문이다.

 

 

 

 

 

아래는 전체 코드이다.

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

np.random.seed(20220214)
data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)
norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)

train_data = norm_data[:1000, :]
test_data = norm_data[1000:, :]

inputs = train_data[:, 0:8]
targets = train_data[:, -1:]

test_inputs = test_data[:, 0:8]
test_targets = test_data[:, -1:]

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

learning_rate = 0.001

batch_size = 256
steps=0
epochs = 3000

def make_batch(input, target, step, batch_size):
    if len(input) >= step + batch_size:
        input_batch = input[step : step + batch_size]
        target_batch = target[step : step + batch_size]
    else:
        input_batch = input[step : ]
        target_batch = target[step : ]
        
    return input_batch, target_batch

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


def backward(input, target, G1, R1, G2, R2, G3, R3, G4):
    target = np.transpose(target, (1, 0)) # (1, batch)
    
    dL_dG4 = 2*(G4-target) / len(target[0]) # (1, batch)
    
    dG4_dW4 = np.transpose(R3, (1, 0)) # (batch, 8)
    
    dG4_dB4 = np.ones_like(B4) # (1, batch)
    
    dG4_dR3 = np.transpose(W4, (1, 0)) # (8, 1)
    
    dR3_dG3 = np.where(R3>0, 1, 0) # (8, batch)
    
    dG3_dW3 = np.transpose(R2, (1, 0)) # (batch, 64)
    
    dG3_dB3 = np.ones_like(B3) # (8, batch)
    
    dG3_dR2 = np.transpose(W3, (1, 0)) # (8, 8)
    
    dR2_dG2 = np.where(R2>0, 1, 0) # (8, batch)
    
    dG2_dW2 = np.transpose(R1, (1, 0)) # (batch, 8)
    
    dG2_dB2 = np.ones_like(B2) # (8, batch)
    
    dG2_dR1 = np.transpose(W2, (1, 0)) # (8, 8)
    
    dR1_dG1 = np.where(R1>0, 1, 0) # (8, batch)
    
    dG1_dW1 = input # (batch, 8)
    
    dG1_dB1 = np.ones_like(B1) # (8, batch)
    
    #chain rule
    
    #operation W4, B4
    dL_dW4 = np.dot(dL_dG4, dG4_dW4) # (1, 8)
    
    dL_dB4 = np.sum(dL_dG4*dG4_dB4, keepdims=True) # (1, 1)
    
    #operation W3, B3
    dL_dG3 = np.dot(dG4_dR3, dL_dG4) * dR3_dG3
    
    dL_dW3 = np.dot(dL_dG3, dG3_dW3) # (8, 8)

    dL_dB3 = np.sum(dL_dG3*dG3_dB3, axis=1, keepdims=True) # (8, 1)

    #operation W2, B2
    dL_dG2 = np.dot(dG3_dR2, dL_dG3) * dR2_dG2
    
    dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (8, 8)
    
    dL_dB2 = np.sum(dL_dG2*dG2_dB2, axis=1, keepdims=True) # (8, 1)
    
    #operation W1, B1
    dL_dG1 = np.dot(dG2_dR1, dL_dG2) * dR1_dG1
    
    dL_dW1 = np.dot(dL_dG1, dG1_dW1) # (8, 8)
    
    dL_dB1 = np.sum(dL_dG1*dG1_dB1, axis=1, keepdims=True) # (8, 1)
    
    return dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3, dL_dW4, dL_dB4

_, _, _, _, _, _, _, pred, loss = forward(inputs, targets)
print('before loss', loss) 


arr_loss = []
for i in range(2000): 
    while steps <= len(inputs):
        x_batch, y_batch = make_batch(inputs, targets, steps, batch_size)
    
        G1, R1, G2, R2, G3, R3, G4, _, loss = forward(x_batch, y_batch)
        dL_dW1, dL_dB1, dL_dW2, dL_dB2, dL_dW3, dL_dB3, dL_dW4, dL_dB4 \
            = backward(x_batch, y_batch, G1, R1, G2, R2, G3, R3, G4) 
        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
        W4 = W4 + -1*learning_rate * dL_dW4
        B4 = B4 + -1*learning_rate * dL_dB4
        arr_loss.append(loss)
        steps += batch_size
        if steps > len(inputs):
            steps = 0 
            np.random.shuffle(train_data)
            break

_, _, _, _, _, _, _, pred, loss = forward(inputs, targets)
print('after loss', loss) 

_, _, _, _, _, _, _, pred, test_loss = forward(test_inputs, test_targets)
print('test loss', test_loss)
norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)
# 역정규화, 정규화의 연산을 다시 풀어준다. data[:, -1:] 는 마지막 target 값을 의미한다.
test_targets = test_targets * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
pred = pred * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)

plt.plot(test_targets, 'ro', label='target')
plt.plot(pred, 'bo', label='pred')
#plt.xlabel('arry', size=15)
#plt.ylabel('value', size=15)
plt.legend()
#plt.plot(arr_loss)
plt.show()

 

 

 

 

 

 

5. Pytorch로 실행해 본다면?

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

torch.manual_seed(220214)
data = genfromtxt('ConcreteStrengthData.csv', delimiter=',', skip_header = 1)

norm_data = (data - np.mean(data, axis=0)) / np.std(data, axis=0)
    
train_data = torch.FloatTensor(norm_data[:1000, :])
test_data = torch.FloatTensor(norm_data[1000:, :])

inputs = train_data[:, 0:8]
targets = train_data[:, -1:]

test_inputs = test_data[:, 0:8]
test_targets = test_data[:, -1:]

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

pred = model(inputs)
loss = F.mse_loss(pred, targets)
print('before loss', loss)

optimizer = optim.SGD(model.parameters(), lr=0.15)
epoches = 3000

for epoche in range(epoches + 1):
    pred = model(inputs)
    
    loss = F.mse_loss(pred, targets)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
pred = model(inputs)
loss = F.mse_loss(pred, targets)
print('after loss', loss)

test_pred = model(test_inputs)

test_targets = test_targets * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
test_pred = test_pred.detach() * np.std(data[:, -1:], axis=0) + np.mean(data[:, -1:], axis=0)
plt.plot(test_targets, 'ro', label='target')
plt.plot(test_pred, 'bo', label='pred')
plt.legend()
plt.show()

 

 

 

 

Numpy DNN과 Pytorch DNN과 큰 차이가 없는 것 같다. 그런데, 일반적인 딥러닝 라이브러리는 손으로 짜는 것 보다 어마어마한 안정성을 자랑한다. 만약 가중치를 바꾸어 보면 어떻게 될까? 가중치와 학습률을 아래와 같이 바꾸어보자.

 

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

optimizer = optim.SGD(model.parameters(), lr=0.2)

 

 

결과는?

 

 

이래서 딥러닝 라이브러리를 쓰는 것이다. 위 가중치를 Numpy로 한다면 최적화 때문에 시간도 오래 걸리기도 하고 안정성이 낮아 overflow가 나기 쉽다.