안녕, 세상!

5. 오차역전파법 본문

It공부/Deep learning

5. 오차역전파법

dev_Lumin 2020. 6. 19. 17:15

수치 미분은 단순하고 구현하기 쉽지만 계산시간이 오래 걸린다는 단점이 있습니다.

이를 보완하기 위해서 효율적으로 계산할 수 있는 오차역전파법을 이용합니다.

오차역전파법을 풀어쓰면 '오차를 역(반대 방향)으로 전파하는 방법' 입니다.

오차역전파법을 이해하는 방식은 수식을 통한 방식, 계산 그래프로 이해하는 방식으로 두 가지가 있는데 계산 그래프를 이용해서 이해는 방식이 더 시각적이고 직관적이라 계산 그래프를 이용해서 설명하겠습니다.

 

(1) 계산 그래프

계산 그래프는 계산 과정을 그래프로 나타낸 것입니다.

그래프는 노드(node)엣지(edge)로 표현됩니다. (노드 사이의 직선이 엣지)

계산 그래프에서 계산을 왼쪽에서 오른쪽으로 진행되는 계산을 순전파 라고 합니다.

반대로 오른쪽에서 왼쪽으로 진행되는 계산을 역전파 라고 합니다.

 

순전파 그래프의 예시는 다음과 같습니다.

문구점에서 연필 2자루, 지우개 3개를 샀습니다. 연필은 1개에 100원, 지우개는 1개에 200원이고 소비세가 10% 일 때 지불 금액을 구하는 계산 그래프는 아래와 같습니다.

 

국소적 계산

'국소적'이란 뜻은 자신과 직접 관계된 작은 범위 라는 뜻으로 국소적 계산은 전체에서 어떤 일이 벌어지든 상관없이 자신과 관계된 정보만으로 다음 결과를 출력할 수 있는 것입니다.

따라서 국소적 계산은 단순하지만, 그 결과를 전달함으로써 전체를 구성하는 복잡한 계산 결과를 얻을 수 있습니다.

 

 

계산 그래프의 이점

전체가 아무리 복잡해도 각 노드에서는 단순한 계산에 집중하여 문제를 단순화할 수 있는 국소적 계산이 가능합니다.

중간 계산 결과를 모두 보관할 수 있습니다.

계산 그래프를 사용하는 가장 큰 이유는 미분을 효율적으로 계산할 수 있습니다.

위의 예시를 통해 설명하자면 연필의 가격이 오를 때 최종 금액에 어떠한 영향을 끼치는지 알 수 있습니다.

이는 연필 가격에 대한 지불금액의 미분을 구할 수 있습니다.

이러한 미분 값은 역전파를 하면 구할 수 있습니다.

 

연필부분만 역전파를 표현하면 다음 그림과 같이 나타납니다.

이를 통해 연필 가격에 대한 지불 금액의 미분 값은 2.2라고 할 수 있습니다.

 

 

 

 

 

(2) 연쇄법칙

우선 역전파는 다음과 같이 계산 그래프로 표현할 수 있습니다.

역전파는 이러한 방식을 따르면 미분 값을 효율적으로 구할 수 있습니다.

이러한 방식이 가능한가는 연쇄법칙의 원리로 설명할 수 있습니다.

 

연쇄법칙은 합성 함수의 미분에 대한 성질이며, 다음과 같이 정의됩니다.

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

 

다음 예시의 식으로 설명하겠습니다.

수식a

이 식을 미분으로 표현해서 다음과 같은 과정으로 연쇄법칙을 나타낼 수 있습니다.

그러므로 수식a를 계산 그래프로 나타내면 다음과 같습니다.

 

(3) 역전파

① 덧셈 노드의 역전파

z=x+y 라는 덧셈 식으로 덧셈 노드의 역전파를 살펴보겠습니다.

z=x+y 의 미분은 다음과 같이 계산할 수 있습니다.

 이와 같이 dz/dx와 dz/dy가 모두 1이 되므로 계산 그래프의 덧셈 노드의 역전파는 1을 곱하기만 하면되서 입력된 값을 그대로 다음노드로 보내게 됩니다. 

 

② 곱셈 노드의 역전파

z=xy 라는 곱셈 식으로 곱셈 노드의 역전파를 살펴보겠습니다.

z=xy 의 미분은 다음과 같이 계산할 수 있습니다.

다음과 같은 결과에 의해 계산 그래프의 곱셈 노드 역전파상류의 값에 순전파 때의 입력 신호들을 서로 바꾼 값을 곱해서 하류로 보냅니다. (오른쪽을 상류라 표현)

 

 

③ 분기노드

같은 값이 복제되어 분기하는 형태의 순전파 역전파는 다음과 같습니다.

 

④ Repeat 노드

배열을 N개 만큼 복사하는 Repeat노드의 순전파 역전파는 다음 코드로 표현할 수 있습니다.

출력은 역전파부분만 했습니다.

axis=0이란 설정으로 2차원배열의 서로다른 행끼리 각각의 원소를 더하고 keepdims=True 설정으로 2차원의 차원을 유지했습니다.

 

 

 

⑤ Sum 노드

Sum노드는 범용 덧셈 노드로 Repeat와 반대입니다.

순전파와 역전파를 코드로 표현하면 다음과 같습니다.

역전파부분만 출력을 시켰고 역전파는 기울기를 가진 axis=0설정으로 np.repeat()이라는 매서드로 반복을 시켰습니다.

 

Sum노드의 순전파가 Repeat노드의 역전파가 되며, Sum노드의 역전파가 Repeat노드의 순전파가 되는 반대관계입니다.

 

 

 

 

 

(4) 단순 계층 구현

모든 계층은 forward()와 backward() 즉 순전파, 역전파의 메소드를 갖도록 구현시킬 것입니다.

 

① 덧셈 계층 

덧셈 계층의 코드는 다음과 같이 구현할 수 있습니다.

 

② 곱셈 계층

곱셈 계층의 코드는 다음과 같이 구현할 수 있습니다.

self.x, self.y로 따로 초기화 해준 이유는 순전파 때의 값이 역전파 때 사용되기 때문입니다.

 

 

 

(5) 활성화 함수 계층 구현

① ReLU 계층

활성화 함수 ReLU의 수식은 다음과 같습니다.

이 수식에 의한 ReLU 계산 그래프는 다음과 같이 나타낼 수 있습니다.

따라서 ReLU 활성화 함수 코드는 다음과 같습니다.

넘파이 배열 특징

넘파이 배열의 인덱스 영역에 bool형식(True, false)으로 넣을 수 있으며 True 인 인덱스에 특정 데이터를 넣을 수 있습니다.

 

 

 

 

② Sigmoid 계층

시그모이드 함수 수식은 다음과 같습니다.

이를 계산 그래프로 정리하면 다음과 같습니다.

역전파 노드중 '/' 과 'exp' 노드는 다음 식에 의해서 위와 같이 그래프가 형성이 됩니다.

 'exp' 의 노드 같은 경우 결국 미분에도 기존 y 값의 변화가 없다는 의미이므로 순전파 때의 출력(exp(-x))을 그대로 곱한것입니다.

 

역전파의 최종 출력을 식으로 정리하면 다음과 같습니다.

시그모이드 함수를 간소화 시키면 다음과 같습니다.

따라서 코드는 다음과 같습니다.

 

 

 

 

(6) Affine 계층

신경망의 순전파 때 수행하는 행렬의 내적은 기하학에서 어파인 변환(affine transformation) 이라고 합니다.

그러므로 어파인 변환을 수행하는 계층을 Affine 계층이라고 부릅니다.

 

Affine 계층의 계산 그래프를 정리하면 다음과 같습니다.

여기서 Wt는 전치행렬이고 수식은 다음과 같습니다.

Wt 가 나온이유는 역전파의 출력값 x 기준으로 설명하겠습니다.

노트 'dot' 도 결국 곱셈이므로 곱셈 계층의 역전파에 의해서 dL/dx = dL/dy * W 가 되는 듯해 보이지만 이렇게 되면 dL/dy*W는 내적이 성립되지 않아서 dL/dx의 형상과 x의 형상이 반드시 같아야만 하기 때문에 내적이 되고 형상이 같게 만들수 있는 Wt로 유도가 된것입니다.

 

 

 

배치용 Affine 계층

배치 N개일 경우 계산 그래프는 다음과 같습니다.

편향의 역전파 부분은 순전파의 편향 덧셈은 각각의 데이터에 더해지므로 역전파 때는 각 데이터의 역전파 값이 편향의 원소에 모여야 하기 때문에 배열의 같은 열의 원소들이 합이 되는 것입니다.

(ex) [[1,2,3],[4,5,6]]   ->   [5,7,9]

 

Affine 계층을 코드로 표현하면 다음과 같습니다.

Affine 계층에서 .dot으로 내적대신에 matmul 행렬곱을 사용해도 됩니다.,

Matmul을 사용한 Affine 코드는 다음과 같습니다.

여기서 self.grads[0] = dw라고 해도 상관 없습니다.

다만 차이는 얕은 복사(shallow copy)냐 깊은 복사(deep copy)냐의 차이입니다.

얕은 복사와 깊은 복사의 차이는 a=b와 a[...]=b로 설명하겠습니다. 

a변수에는 모두 [4,5,6]이 할당될 것입니다.

a=b 에서는 a가 가리키는 메모리의 위치가 b가 가리키는 위치와 같아집니다.

실제 데이터 (4,5,6)이 복사되지 않으므로 얕은복사라고 합니다.

반면, a[...]=b일 때는 a의 메모리 위치는 변하지 않고, a가 가리키는 메모리에 b의 원소가 복제됩니다.

실제 데이터가 복사되서 깊은 복사라고 합니다.

 

 

 

 

 

(7) Softmax-with-Loss계층

소프트맥스 함수의 역전파를 설명하는데 손실함수의 입력 값 일부분이 필요하기 때문에 손실함수인 교차 엔트로피 오차를 포함하여 Softmax-with-Loss계층이라는 이름으로 구현됩니다.

Softmax-with-Loss계층의 계산 그래프는 다음과 같습니다.

이를 간략화 시키면 다음과 같습니다.

Softmax의 계층 입력층은 a1,a2,a3 이며 이것이 Softmax계층을 거쳐 y1, y2, y3로 출력이 됩니다.

t1, t2, t3 는 손실함수에 들어가는 정답 레이블입니다.

Softmax 계층의 역전파는 소프트맥스 함수의 출력과 정답 레이블의 오차를 결과 ( y1-t1, y2-t2, y3-t3 ) 로 말끔하게 결과가 나옵니다.

소프트맥스 함수의 손실함수로 교차 엔트로피 오차를 사용해서 말끔히 떨어지는 결과는 우연이 아니라 교차 엔트로피 오차라는 함수가 그렇게 설계되었기 때문입니다.

마찬가지로 항등 함수의 손실함수로 평균 제곱 오차를 사용하면 역전파 결과가 ( y1-t1, y2-t2, y3-t3 )로 말끔히 떨어집니다.

 

Softmax-with-Loss 코드는 다음과 같습니다.

역전파 때는 전파하는 값을 배치의 수로 나눠서 데이터 1개당 오차를 앞 계층으로 전파합니다.

 

 

 

(8) 오차역전파법 구현

2층 신경망 구조

오차역전파법은 앞의 4장에서 numerical_gradient대신 gradient를 사용해서 학습을 구현시키면 됩니다.

이러한 과정에서 일부 수정된 2층 신경망 구조의 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.layers import *
from common.gradient import numerical_gradient
from collections import OrderedDict
 
 
class TwoLayerNet5:
 
    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
        
    # x : 입력 데이터, t : 정답 레이블
    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):
        # forward
        self.loss(x, t)
 
        # backward
        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'], grads['b1'= self.layers['Affine1'].dW, self.layers['Affine1'].db
        grads['W2'], grads['b2'= self.layers['Affine2'].dW, self.layers['Affine2'].db
 
        return grads
cs

 OrderedDict는 순서가 있는 딕셔너리로 딕셔너리에 추가한 순서를 기억합니다.

그래서 순전파 때는 추가한 순서대로 각 계층의 forward()메서드를 호출하기만 하면 처리가 완료됩니다.

마찬가지로 역전파 때는 계층을 반대로 호출하면 됩니다.

이처럼 신경망의 구성 요소를 '계층'으로 구현한 덕분에 신경망을 쉽게 구축할 수 있습니다.

'계층'으로 모듈화했으므로 추가 계층이 필요하면 필요한 만큼 계층을 추가하면 됩니다.

 

 

오차역전파법으로 구한 기울기 검증

기울기를 구하는 방법은 수치미분과 오차역전파법이 있는데 수치 미분은 느려서 오차역전파법을 주로 사용합니다.

수치 미분은 오차역전파법을 정확히 구현했는지 확인하기 위해서 필요합니다.

수치 미분의 구현에는 버그가 숨어 있기 어려운 반면, 오차역전파법은 구현하기 복잡해서 실수가 발생할 수 있습니다.

그래서 수치 미분의 결과와 오차역전파법의 결과를 비교하여 오차역전파법을 제대로 구현했는지 검증합니다.

손글씨 분류 데이터셋 일부로 구현한 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net5 import TwoLayerNet5
 
# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
 
network = TwoLayerNet5(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))
cs

 수치 미분으로 구한 기울기와 오차역전파법으로 구한 기울기의 오차를 확인합니다.

여기에서는 각 가중치 매개변수의 차이의 절댓값을 구하고, 그 평균을 낸 값이 오차가 됩니다.

 

 

다음코드는 오차역전파법을 사용한 학습을 구현하는 코드입니다.

이 역시 손글씨 분류 mnist 데이터셋을 이용했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import sys, os
sys.path.append(os.pardir)
 
import numpy as np
import matplotlib.pyplot as plt
from dataset.mnist import load_mnist
from two_layer_net5 import TwoLayerNet5
 
# 데이터 읽기
(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
 
network = TwoLayerNet5(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 = []
 
iter_per_epoch = max(train_size / batch_size, 1)
 
for i in range(iters_num):
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 기울기 계산
    #grad = network.numerical_gradient(x_batch, t_batch) # 수치 미분 방식
    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)
    
    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)
        
# 그래프 그리기
markers = {'train''o''test''s'}
= np.arange(len(train_acc_list))
plt.plot(x, train_acc_list, label='train acc')
plt.plot(x, test_acc_list, label='test acc', linestyle='--')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(01.0)
plt.legend(loc='lower right')
plt.show()
cs

 결과는 다음과 같습니다.

 

'It공부 > Deep learning' 카테고리의 다른 글

7. CNN  (0) 2020.06.26
6. 학습 관련 기술들  (0) 2020.06.21
4. 신경망 학습  (0) 2020.06.17
3-(1) 손글씨 숫자 인식 분석  (1) 2020.06.16
3. 신경망  (0) 2020.06.16
Comments