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

15. 다층 퍼셉트론(MLP) 등장 - 1.XOR 문제 해결(실습)

by Majestyblue 2022. 1. 19.

이전시간에 XOR 문제를 해결하기 위해 다층 퍼셉트론이 제시되었고 이를 순방향 전파와 역방향 전파 도함수를 수학적으로 구한 것을 확인하였다. 이번 시간에는 수학적으로 표현한 내용을 코드로 구현해 보겠다.

 

1. 데이터 준비

import numpy as np

np.random.seed(220132)

inputs = np.array([[0., 0.],
                   [1., 0.],
                   [0., 1.],
                   [1., 1.]], dtype = np.float32)

targets = np.array([[0.],
                    [1.],
                    [1.],
                    [0.]], dtype = np.float32)
                    
W1 = np.random.randn(2, 2) # [[-1.02877142  1.37536642] [-1.4391631  -0.1623922 ]]
B1 = np.random.randn(2, 1) # [[-0.18509103  0.4315507 ]]
W2 = np.random.randn(1, 2) # [[ 0.91429067 -0.88940343]]
B2 = np.random.randn(1, 1) # [[-1.11679554]]

learning_rate = 0.1

 

입력 데이터 inputs, 목표값 데이터 targets, 각 가중치와 편향 W1, W2, B1, B2, 학습률 learning_rate를 설정한다.

 

 

 

 

 

2. 순방향 연산

순방향 연산은 아래와 같이 정의했었다. 

 

 

 

def forward(input, target, W1, B1, W2, B2):
    
    X = np.transpose(input, (1, 0)) #(2, batch)
    
    G1 = np.dot(W1, X) + B1 #(2, batch)
    
    S1 = 1 / (1 + np.exp(-G1)) #(2, batch)
    
    G2 = np.dot(W2, S1) + B2 # (1, batch)
    
    S2 = 1 / (1 + np.exp(-G2)) # (1, batch) -> pred
    
    pred = np.transpose(S2, (1, 0)) #(batch, 1)

    loss = -target*np.log(pred) - (1-target)*np.log(1-pred) #sum(batch, 1) -> 1

    loss = np.mean(loss)

    return G1, S1, G2, S2, pred, loss

 

S2 출력은 (1, batch), target은 (batch, 1) 이므로 S2의 전치(1, batch)를 pred로 하여 오차를 구하였다.

 

 

 

 

 

3. 역방향 연산

아래 4개의 도함수를 구할것이다.

 

 

 

 

 

 

1) ∂loss/∂S2

∂loss/∂S2 의 식과 이를 수식으로 표현하면 아래와 같다.

 

 

target = np.transpose(target, (1, 0)) # (1, batch)
    
dL_dS2 = -(target / S2) + ((1-target) / (1-S2)) # (1 , batch)

 

 

 

2) ∂S2/∂G2

∂S2/∂G2 의 식과 이를 수식으로 표현하면 아래와 같다.

 

 

dS2_dG2 = S2 * (1 - S2) # (1, batch)

 

 

 

3) ∂G2/∂S1

∂G2/∂S1 의 식과 이를 수식으로 표현하면 아래와 같다.

 

 

dG2_dS1 = np.transpose(W2, (1, 0)) # (2, 1)

 

 

 

 

4) ∂S1/∂G1

∂S1/∂G1 의 식과 이를 수식으로 표현하면 아래와 같다.

 

 

dS1_dG1 = S1 * (1 - S1) # (2, batch)

 

 

 

 

 

5) ∂G1/∂W1

∂G1/∂W1 의 식과 이를 수식으로 표현하면 아래와 같다.

 

 

dG1_dW2 = input #(batch, 2)

 

 

 

6) ∂loss/∂W1

체인룰을 이용하여 아래와 같이 구할 수 있다.

 

dL_dG2 = dL_dS2 * dS2_dG2 # (1, batch)
    
dL_dS1 = np.dot(dG2_dS1, dL_dG2) # (2, batch)
    
dL_dG1 = dL_dS1 * dS1_dG1 # broad casting, (2, batch)
    
dL_dW1 = np.dot(dL_dG1, dG1_dW2)

 

 

 

7) ∂loss/∂B1

∂loss/∂B1 은 아래 식에서와 같이 dL_dG1의 요소합과 같다.

 

 

# 사실 dG1_dB1 를 곱해야 하는데 곱해봤자 dL_dG1이니까 생략
dL_dB1 = np.sum(dL_dG1, axis=1, keepdims=True) # (2, 1)

 

 

 

 

8) ∂loss/∂W2

아래 식과 같이 ∂loss/∂S2 와 ∂S2/∂G2 의 요소곱 결과와 S1 전치행렬의 행렬곱과 같다.

 

dG2_dW2 = np.transpose(S1, (1, 0)) # (batch, 2)

dL_dG2 = dL_dS2 * dS2_dG2 # (1, batch)

dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (1, 2)

 

 

 

 

9) ∂loss/∂B2

아래 식과 같이 ∂loss/∂S2 와 ∂S2/∂G2 의 요소곱 결과를 더한다.

 

 

dL_dG2 = dL_dS2 * dS2_dG2 # (1, batch)

# 사실 dG2_dB2 를 곱해야 하는데 곱해봤자 dL_dG2이니까 생략
dL_dB2 = np.sum(dL_dG2).reshape(1, 1) # (1, 1)

 

 

 

 

4. 전체코드 및 결과

import numpy as np

np.random.seed(220132)

inputs = np.array([[0., 0.],
                   [1., 0.],
                   [0., 1.],
                   [1., 1.]], dtype = np.float32)

targets = np.array([[0.],
                    [1.],
                    [1.],
                    [0.]], dtype = np.float32)

W1 = np.random.randn(2, 2) # [[-1.02877142  1.37536642] [-1.4391631  -0.1623922 ]]
B1 = np.random.randn(2, 1) # [[-0.18509103  0.4315507 ]]
W2 = np.random.randn(1, 2) # [[ 0.91429067 -0.88940343]]
B2 = np.random.randn(1, 1) # [[-1.11679554]]

learning_rate = 0.1

def forward(input, target, W1, B1, W2, B2):
    
    X = np.transpose(input, (1, 0)) #(2, batch)
    
    G1 = np.dot(W1, X) + B1 #(2, batch)
    
    S1 = 1 / (1 + np.exp(-G1)) #(2, batch)
    
    G2 = np.dot(W2, S1) + B2 # (1, batch)
    
    S2 = 1 / (1 + np.exp(-G2)) # (1, batch) -> pred
    
    pred = np.transpose(S2, (1, 0)) #(batch, 1)

    loss = -target*np.log(pred) - (1-target)*np.log(1-pred) #sum(batch, 1) -> 1

    loss = np.mean(loss)

    return G1, S1, G2, S2, pred, loss

def loss_gradient(input, target, W1, B1, W2, B2):
    G1, S1, G2, S2, _, _ = forward(inputs, targets, W1,B1,W2,B2)
    
    target = np.transpose(target, (1, 0)) # (1, batch)
    
    dL_dS2 = -(target / S2) + ((1-target) / (1-S2)) # (1 , batch)

    dS2_dG2 = S2 * (1 - S2) # (1, batch)
    
    dG2_dS1 = np.transpose(W2, (1, 0)) # (2, 1)
    
    dG2_dW2 = np.transpose(S1, (1, 0)) # (batch, 2)
    
    dG2_dB2 = np.ones_like(G2) # (1, 1)
    
    dS1_dG1 = S1 * (1 - S1) # (2, batch)
    
    dG1_dW2 = input #(batch, 2)
    
    dG1_dB1 = np.ones_like(G1) # (2, 1)
    
    # chain_rules
    dL_dG2 = dL_dS2 * dS2_dG2 # (1, batch)
    
    dL_dS1 = np.dot(dG2_dS1, dL_dG2) # (2, batch)
    
    dL_dG1 = dL_dS1 * dS1_dG1 # broad casting, (2, batch)
    
    dL_dW1 = np.dot(dL_dG1, dG1_dW2)
    
    # 사실 dG1_dB1 를 곱해야 하는데 곱해봤자 dL_dG1이니까 생략
    dL_dB1 = np.sum(dL_dG1, axis=1, keepdims=True) # (2, 1) 
    
    dL_dW2 = np.dot(dL_dG2, dG2_dW2) # (1, 2)
    
    # 사실 dG2_dB2 를 곱해야 하는데 곱해봤자 dL_dG2이니까 생략
    dL_dB2 = np.sum(dL_dG2).reshape(1, 1) # (1, 1)
    
    return dL_dW1, dL_dB1, dL_dW2, dL_dB2

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

for i in range(2000): 
    dL_dW1, dL_dB1, dL_dW2, dL_dB2 = loss_gradient(inputs, targets, W1, B1, W2, B2) 
    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
    
_, _, _, _, pred, loss = forward(inputs, targets, W1, B1, W2, B2)
print('after pred', pred) 
print('after loss', loss) 

"""
이건 성공
np.random.seed(220132)
before pred [[0.20601854]
 [0.15406817]
 [0.22655651]
 [0.16426513]]
before loss 3.7652599788184817
after pred [[0.03651696]
 [0.94875326]
 [0.94477372]
 [0.04154988]]
after loss 0.1890544867782224
"""

"""
이건 실패 
np.random.seed(220118)
before pred [[0.39046107]
 [0.25117422]
 [0.39236246]
 [0.27272831]]
before loss 3.1306852854640237
after pred [[0.02898241]
 [0.95838905]
 [0.03315866]
 [0.95648718]]
after loss 0.1501209263458848
"""

after pred가 순서대로 0, 1, 1, 0 으로 출력된다면 성공한 것이다.

 

np.random.seed에서 seed 번호에 주목하길 바란다. 220132 로는 성공하였는데, 220118로는 실패하였다. 왜냐하면 오차함수가 잘못된 값으로 수렴하였기 때문이다. 이 문제는 추후에 경사하강법 수정으로 보완해 보겠다.

 

 

 

 

5. pytorch 테스트 결과

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

input = torch.FloatTensor([[0., 0.],
                        [1., 0.],
                        [0., 1.],
                        [1., 1.]])
target = torch.FloatTensor([[0.],
                            [1.],
                            [1.],
                            [0.]])

model = nn.Sequential(
    nn.Linear(2, 2),
    nn.Sigmoid(),
    nn.Linear(2, 1),
    nn.Sigmoid()
)
pred = model(input)
print('before pred', pred)
optimizer = optim.SGD(model.parameters(), lr=1)
epoches = 2000

for epoche in range(epoches + 1):
    pred = model(input)
    loss = F.binary_cross_entropy(pred, target)
    
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    if epoche % 100 == 0:
        print('loss :', loss)
pred = model(input)
print('after pred', pred)

"""
before pred tensor([[0.3750],
        [0.3563],
        [0.3769],
        [0.3594]], grad_fn=<SigmoidBackward>)
loss : tensor(0.7308, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6934, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6932, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6932, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6932, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6931, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6931, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6930, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6929, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6925, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6908, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.6764, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.5921, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.5135, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.2414, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0848, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0475, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0325, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0245, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0197, grad_fn=<BinaryCrossEntropyBackward>)
loss : tensor(0.0164, grad_fn=<BinaryCrossEntropyBackward>)
after pred tensor([[0.0157],
        [0.9863],
        [0.9780],
        [0.0134]], grad_fn=<SigmoidBackward>)
"""

 

마지막 after pred가 결과이다. 0, 1, 1, 0 으로 가깝게 출력되어 성공적으로 예측한 것을 알 수 있다.