본문 바로가기

딥러닝/밑바닥부터 배우는 딥러닝

5장. 오차역전파법

1. 계산 그래프

계산 그래프 사용시 이점

  • 복잡한 문제를 국소적으로 단순화 (국소적: 자신과 직접 관계된 작은 범위만 계산 가능)
  • 중간 계산 결과를 모두 보관할 수 있음
  • 역전파를 통해 미분을 효율적으로 계산함

2. 연쇄 법칙

신호 E에 노드의 국소적 미분을 곱하여 노드를 계산함으로써 역전파를 계산할 수 있다. 

 

합성함수: 여러 함수로 구성된 함수

 

z라는 함수를 t를 이용하여 합성함수로 표현할 수 있다.

 

 

x의 z에 대한 미분은 t를 이용하여 연쇄법칙으로 표현할 수 있다. 

 

 

즉, 합성함수의 미분은 합성함수를 구성하는 각 함수의 미분의 곱으로 나타낼 수 있다. (편미분)

 

 

3. 역전파

1. 덧셈 노드의 역전파

 

덧셈 노드일 때 미분값이 다음과 같기 때문에 상류에서 정해진 미분값에 1을 곱하기만 하면 된다, 즉 입력값을 그대로 내보내면 된다.

 

2. 곱셈 노드의 역전파

 

곱셈노드의 역전파는 상류에서 흘러온 미분값에 곱해진 값을 서로 바꿔 곱해서 흘려보내주면 된다.

 

4. 단순한 계층 구현하기

1. 곱셈노드 구현하기

class MulLayer:
    
    def __init__(self):
        self.x=None
        self.y=None #x와 y변수 초기화
        
    def forward(self,x,y):
        self.x=x
        self.y=y
        out=x*y # 두 입력값을 곱해서 반환
        
        return out
    
    def backward(self,dout):
        dx=dout*self.y
        dy=dout*self.x
        
        return dx,dy #두개를 바꿔줌

forward는 입력받은 x와 y를 그대로 곱해서 출력하면 된다.

backward는 상류에서 흘러온 미분값인 dout에 x와 y를 바꾸어 곱한 값을 출력하면 된다.

 

apple=100
apple_num=2
tax=1.1

mul_apple_layer=MulLayer()
mul_tax_layer=MulLayer()

apple_price=mul_apple_layer.forward(apple,apple_num)
price=mul_tax_layer.forward(apple_price,tax)

print(price)  #220
dprice=1
dapple_price,dtax=mul_tax_layer.backward(dprice)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)
print(dapple, dapple_num,dtax) #2.2, 110, 200

 

2. 덧셈노드 구현하기

class AddLayer:
    def __init__(self):
        pass 
    
    def forward(self,x,y):
        out = x+y        
        return out
    
    def backward(self,dout):
        dx=dout*1
        dy=dout*1
        
        return dx,dy

덧셈노드의 경우 forward에서는 두 값을 더해줬고, backward에서는 dout에 1을 곱한 값을 보내준다.

 

apple=100
apple_num=2
orange=150
orange_num=3
tax=1.1

mul_apple_layer=MulLayer() #(1)
mul_orange_layer=MulLayer() #(2)
add_apple_orange_layer=AddLayer() #(3)
mul_tax_layer=MulLayer()  #(4)

#forward propagation
apple_price=mul_apple_layer.forward(apple,apple_num)
orange_price=mul_orange_layer.forward(orange,orange_num)
all_price=add_apple_orange_layer.forward(apple_price,orange_price)
price=mul_tax_layer.forward(all_price,tax)

#backward propagation
dprice=1
dall_price,dtax=mul_tax_layer.backward(dprice)
dapple_price,dorange_price=add_apple_orange_layer.backward(dall_price)
dapple,dapple_num=mul_apple_layer.backward(dapple_price)
dorange,dorange_num=mul_orange_layer.backward(dorange_price)

print(price) #715
print(dapple_num,dapple,dorange,dorange_num,dtax)  #110, 2.2, 3.3, 165, 650

 

각 계층마다 mul 또는 add layer을 구현해줬다.

forward propagation은 왼쪽에서 오른쪽으로 순서대로 사과 가격, 오렌지 가격, 총 가격, 최종 가격을 계산했다.

backward propagation에서는 dprice=1로 설정하고 오른쪽에서 왼쪽으로 미분을 계산했다.

 

5. 활성화 함수 계층 구현하기

1. ReLU 계층

ReLU 함수의 함숫값과 미분값은 다음과 같다.

 

 

x>0이면 dy/dx=1로 상류의 미분값이 그대로 흘러나온다.

x<0이면 dy/dx=0으로 더이상 미분값이 흘러나오지 않는다.

 

class Relu:
    def __init__(self):
        self.mask=None
        
    def forward(self,x):
        self.mask=(x<=0)
        out=x.copy()
        out[self.mask]=0
        return out
    
    def backward(self,dout):
        dout[self.mask]=0
        dx=dout
        return dx

forward에서 self.mask로 x>0인 원소는 True, x<0인 원소는 False가 된다.
out[self.mask]=0으로, x에서 x<0인 원소만 0으로 바꿔준다.
backward에서는 흘러 들어온 미분값의 x<0인 원소를 0으로 바꿔준다.

 

2. Sigmoid 계층

 

class Sigmoid:
    def __init__(self):
        self.out=None
        
    def forward(self,x):
        out=1/(1+np.exp(-x))
        self.out=out
        return out
    
    def backward(self,dout):
        dx=dout*(1.0-self.out)*self.out
        return dx

 

6. Affine/Softmax 계층 구현하기

1. Affine 계층

Affine transformation: 신경망의 순전파때 수행하는 행렬의 곱 (Z=A*W+B의 꼴)

위에서 스칼라값을 넣어서 곱셈/덧셈 연산을 수행한 것처럼 행렬일때도 계산 그래프를 통한 순방향/역방향 전파의 계산이 가능하다. 행렬끼리 곱셈을 해줄 땐 shape을 잘 맞춰줘야 한다.

참고로 (2, )는 (1,2)를 의미한다.

 

X의 shape과 dL/dX의 shape은 (2,)로 동일하고, W의 shape과 dL/dW의 shape은 (2,3)으로 동일하다.

 

1.2 배치용 Affine 계층

위에는 하나의 example에 대해 행렬의 affine 계층의 계산을 나타낸 거였고, 이번엔 N개의 example에 대한 배치 affine 계층을 살펴보겠다.

위와 비교했을 때 순전파와 역전파의 계산에서 달라진 것은 없고 shape만 달라졌다.

X는 각 example이 행벡터로 놓여있고 그런 행벡터가 세로로 N개 쌓여 있어서 (N,2)의 shape을 가진다.

Y는 X*W+B인데 (N,2)*(2,3)+(1,3) => (B는 broadcasting) (N,3)이 된다.

그리고 dL/dB는 그냥 dL/dY가 아니라 dL/dY의 열방향의 합이다. 

 

class Affine:
    def __init__(self,W,b):
        self.W=W
        self.b=b
        self.x=None
        self.dW=None
        self.db=None
        
    def forward(self,x):
        self.x=x
        out=np.dot(x,self.W)+self.b
        
        return out
    
    def backward(self,dout):
        dx=np.dot(dout,self.W.T)
        self.dW=np.dot(self.x.T,dout)
        self.db=np.sum(dout,axis=0)
        
        return dx

forward에서 Y=X*W+b로 계산해주고 backward에서 dL/dX, dL/dW, dL/dB를 계산해준다.

 

1.3 Softmax with Loss 계층

손글씨 숫자 인식에서 입력 이미지가 affine 계층과 relu 계층을 반복적으로 통과하여 변환되다가 마지막 softmax 함수에 의해 10개의 입력이 정규화되고, 정규화된 값은 각 label이 정답이 될 확률을 나타낸다. 마지막 affine 계층이 출력하는 값은 각 label의 점수이다.

X의 input에서 시작하여 Z[1]=W[1]*X+b[1] : affine 1, A[1]=g[1](Z[1]) : relu 1,

Z[2]=W[2]*A[1]+b[2] : affine 2, A[2]=g[2](Z[2]) : relu 2, ... , Z[L-1]=W[L-1]*A[L-2]+b[L-1] : affine p, Yhat=A[L]=sigmoid(Z[L-1])

 

- softmax와 cross entropy error의 역전파를 구해보면 예측값 y와 정답값 t의 차가 된다.

 

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = y.reshape(1, y.size)

    # 훈련 데이터가 원-핫 벡터라면 정답 레이블의 인덱스로 반환
    if t.size == y.size:
        t = t.argmax(axis=1)

    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size), t])) / batch_size
    
    
class SoftmaxWithLoss:
    def __init__(self):
        self.loss=None
        self.y=None
        self.t=None
        
    def forward(self,x,t):
        self.t=t
        self.y=softmax(x)
        self.loss=cross_entropy_error(self.y,self.t)
        return self.loss
    
    def backward(self,dout=1):
        batch_size=self.t.shape[0]
        dx=(self.y-self.t)/batch_size
        return dx

forward에서는 softmax와 cross entropy error을 거친 loss를 출력할 것이다.

backward에서는 dout=1일 때 dL/dx를 출력할 것이고 각각의 원소에 대해 생각해야 하므로 example의 수인 batch_size로 나눠준다. 

 

7. 오차 역전파법 구현하기

신경망 학습

전제: 신경망에는 적응 가능한 가중치와 편향 존재, 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 학습이라고 한다.

1단계 – 미니배치: 훈련 데이터 중 일부를 무작위로 선택 → 미니배치 목표: 미니배치의 손실 함수 값을 줄이는 것

2단계 – 기울기 산출 =>오차역전파법: 각 가중치 매개변수의 기울기를 구함 (기울기는 손실함수의 값을 가장 작게하는 방향을 제시)

3단계 – 매개변수 갱신: 가중치 매개변수를 기울기 방향으로 아주 조금 갱신

4단계 – 반복 1~3단계를 반복

 

class TwoLayerNet:
    def __init__(self, input_size, hidden_size, output_size,
        weight_init_std=0.01):
        # 가중치 초기화
        self.params = {}
        self.params['W1'] = weight_init_std * \
            np.random.randn(input_size, hidden_size)
        self.params['b1'] = np.zeros(hidden_size)
        self.params['W2'] = weight_init_std * \
            np.random.randn(hidden_size, output_size)
        self.params['b2'] = np.zeros(output_size)

        # 계층 생성
        self.layers = OrderedDict()
        self.layers['Affine1'] = \
            Affine(self.params['W1'], self.params['b1'])
        self.layers['Relu1'] = Relu()
        self.layers['Affine2'] = \
            Affine(self.params['W2'], self.params['b2'])
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for layer in self.layers.values():
            x = layer.forward(x)

        return x

    # x : 입력 데이터, t : 정답 레이블
    def loss(self, x, t):
        y = self.predict(x)
        return self.lastLayer.forward(y, t)

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1:
            t = np.argmax(t, axis=1)

        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    def numerical_gradient(self, x, t):
        loss_W = lambda W: self.loss(x, t)

        grads = {}
        grads['W1'] = numerical_gradient(loss_W, self.params['W1'])
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])

        return grads

    def gradient(self, x, t):
        # 순전파
        self.loss(x, t)

        # 역전파
        dout = 1
        dout = self.lastLayer.backward(dout)

        layers = list(self.layers.values())
        layers.reverse()
        for layer in layers:
            dout = layer.backward(dout)

        # 결과 저장
        grads = {}
        grads['W1'] = self.layers['Affine1'].dW
        grads['b1'] = self.layers['Affine1'].db
        grads['W2'] = self.layers['Affine2'].dW
        grads['b2'] = self.layers['Affine2'].db

        return grads
        
        
        
      

1. 가중치 초기화

parmas 딕셔너리 안에 W1, b1, W2, b2를 shape에 맞게 randomly initialize한다.


2. 계층 생성
layers라는 OrderedDict()를 만든다. OrderedDict()는 collections 패키지에서 불러온 것으로 딕셔너리에 저장한 순서를 기억할 수 있다.
layers (Affine1 -> Relu1 -> Affien2) ->SoftmaxWithLoss (Softmax -> Cross Entropy Error) 순서로 계층을 만들 것이다.
layers 딕셔너리 안에 Affine1, Relu1, Affine2의 layer을 만들고 Affine1에는 W1,b1을, Affine2에는 W2,b2 파라미터를 이용한 Affine layer을 만들고 Relu1, Relu2 layer은 Relu()로 만든다.

마지막으로 lastLayer 안에 SoftmaxWithLoss()를 넣어준다.

 

3. Predict
layers의 딕셔너리 안의 layer들 (Affine1, Relu1, Affine2)을 forward 처리 한다.

 

4. Loss
y는 layers에서 forward 된 predict 값이다. 이 값을 lastLayer에 넣어 SoftmaxWithLoss에서 forward 시켜 Loss 값을 얻는다.

 

5. Accuracy
y=self.predict(x)로 layers에서 forward 한 예측값을 얻는다.
y=np.argmax(y,axis=1)로, 각 example별로 예측값이 가장 크게 나온 label을 얻는다.
실제 정답값 t와 비교하여 정답 개수/전체 example 수로 accuracy를 얻는다.

 

6. numerical gradient
loss 값을 loss_W에 저장한뒤 numerical gradient로 grads 딕셔너리 안에 W1,b1,W2,b2의 grad를 저장한다.

?????

 

7. gradient
self.loss(x,t)로 loss 값을 얻는다.
dout=1이라고 두고 last layer -> layers 역순으로 역전파를 구할 것이다.
lastlayer에서 dout를 backward해서 새로운 backward를 얻는다.
layers를 reverse 하여 각 층에서 dout를 backward 시킨 것을 새로운 dout로 둔다.
Affine 1층의 W1,b1의 grad와 Affine 2층의 W2,b2의 grad를 저장한다.

 

수치 미분으로 기울기 확인하기


# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)

network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

x_batch = x_train[:3]
t_batch = t_train[:3]

grad_numerical = network.numerical_gradient(x_batch, t_batch)
grad_backprop = network.gradient(x_batch, t_batch)

# 각 가중치의 차이의 절댓값을 구한 후, 그 절댓값들의 평균을 낸다.
for key in grad_numerical.keys():
    diff = np.average(np.abs(grad_backprop[key] - grad_numerical[key]))
    print(key + ":" + str(diff))
        

 

W2:9.71260696544e-13
b2:1.20570232964e-10
W1:2.86152966578e-13
b1:1.19419626098e-12
수치 미분으로 구한 params와 오차 역전파법으로 구한 params의 차이가 거의 0이다.

 

오차 역전파법으로 학습 구현하기

import sys
import os
import numpy as np
sys.path.append(os.pardir)
from dataset.mnist import load_mnist
#from two_layer_net import TwoLayerNet

# 데이터 읽기
(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)

# 하이퍼 파라메터
iters_num = 10000  # 반복횟수
train_size = x_train.shape[0]
batch_size = 100  # 미니배치 크기
learning_rate = 0.1

train_loss_list = []
train_acc_list = []
test_acc_list = []

# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)

for i in range(iters_num):
    # print(i)
    # 미니배치 획득
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]

    # 오차역전파법으로 기울기 계산
    grad = network.gradient(x_batch, t_batch)

    # 매개변수 갱신
    for key in ('W1', 'b1', 'W2', 'b2'):
        network.params[key] -= learning_rate * grad[key]

    # 학습 경과 기록
    loss = network.loss(x_batch, t_batch)
    train_loss_list.append(loss)

    # 1에폭 당 정확도 계산
    if i % iter_per_epoch == 0:
        train_acc = network.accuracy(x_train, t_train)
        test_acc = network.accuracy(x_test, t_test)
        train_acc_list.append(train_acc)
        test_acc_list.append(test_acc)
        print("train acc, test acc | " + str(train_acc) + ", " + str(test_acc))

'딥러닝 > 밑바닥부터 배우는 딥러닝' 카테고리의 다른 글

7장. 합성곱 신경망(CNN)  (0) 2021.03.02
6장. 학습 관련 기술들  (0) 2021.02.23
4장 신경망 학습  (0) 2021.02.01
3장 신경망  (0) 2021.02.01