오차역전파


수치미분을 이용한 딥러닝은 속도가 매우 느리기 때문에 Mnist와 같은 대규모 데이터를 처리하기에 부적합하다.

미분의 연쇄 법칙(Chain Rule)을 응용하여 편미분을 하지 않고 가중치와 바이어스의 변화율을 간단한 곱셈 형태로 나타낼 수 있는 방법이 있다. (공식 유도 방법은 이 영상을 참고)

이 방법을 역전오차파 방법이라고 하며 딥러닝의 성능을 비약적으로 향상시킨다. 역전파인 이유는 순전파(Propagation)인 피드 포워드(Feed Foward) 과정과 반대 방향으로 수행되기 때문이다.

오차역전파 일반 공식


출력층 손실값

$$
loss_{out} = (A_{out}-T)\cdot A_{out}\cdot (1-A_{out})
$$

은닉층 손실값

$$
loss_{h} = (loss_{h+1} \cdot (W_{h+1})^{T})\cdot A_{h} \cdot (1-A_h)
$$

학습 방법

$$
W_i = W_i - \alpha \cdot (A_{i-1})^T \cdot loss_i
$$

$$
b_i = b_i - \alpha \cdot loss_i
$$

역전오차파가 반영된 딥러닝 코드


인공 신경망 클래스

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime

class ANN:
def __init__(self, nodes):
self.W = [None]
self.b = [None]
self.Z = []
self.A = []
self.N = len(nodes)

for i in range(1, self.N):
self.W.append(np.random.randn(nodes[i-1], nodes[i]) / np.sqrt(nodes[i-1]/2))
self.b.append(np.random.rand(nodes[i]))

for i in range(self.N):
self.Z.append(np.zeros([1,nodes[i]]))
self.A.append(np.zeros([1,nodes[i]]))

def feed_forward(self):
delta = 1e-7
self.Z[0] = self.input_data
self.A[0] = self.input_data

for i in range(1, self.N):
self.Z[i] = np.dot(self.A[i-1], self.W[i]) + self.b[i]
self.A[i] = self.activate(self.Z[i])
y = self.A[-1]

return -np.sum(self.target_data*np.log(y+delta)+(1-self.target_data)*np.log((1-y)+delta))

def loss_val(self):
delta = 1e-7
self.Z[0] = self.input_data
self.A[0] = self.input_data

for i in range(1, self.N):
self.Z[i] = np.dot(self.A[i-1], self.W[i]) + self.b[i]
self.A[i] = self.activate(self.Z[i])
y = self.A[-1]

return -np.sum(self.target_data*np.log(y+delta)+(1-self.target_data)*np.log((1-y)+delta))


def train(self, input_data, target_data, learning_rate=1e-2, activate=None):
self.input_data = input_data
self.target_data = target_data
self.activate = activate or ANN.sigmoid

# 먼저 feed forward 를 통해서 최종 출력값과 이를 바탕으로 현재의 에러 값 계산
self.feed_forward()

# 출력층 역전오차파
loss = (self.A[-1]-self.target_data) * self.A[-1] * (1-self.A[-1])
self.W[-1] = self.W[-1] - learning_rate * np.dot(self.A[-2].T, loss)
self.b[-1] = self.b[-1] - learning_rate * loss

# 은닉층 역전오차파
for i in range(self.N-2, 0, -1):
loss = np.dot(loss, self.W[i+1].T) * self.A[i] * (1-self.A[i])
self.W[i] = self.W[i] - learning_rate * np.dot(self.A[i-1].T, loss)
self.b[i] = self.b[i] - learning_rate * loss

def predict(self, input_data):
a = input_data

for i in range(1, self.N):
z = np.dot(a, self.W[i]) + self.b[i]
a = self.activate(z)
y = a

return np.argmax(y)

def accuracy(self, test_input_data, test_target_data):
matched = []
unmatched = []

for index in range(len(test_input_data)):
label = int(test_target_data[index])
data = (test_input_data[index] / 255.0 * 0.99) + 0.01
predicted_num = self.predict(np.array(data, ndmin=2))

if label == predicted_num:
matched.append(index)
else:
unmatched.append(index)

print("Current Accuracy = ", (len(matched)/(len(test_input_data))))

return matched, unmatched

@staticmethod
def sigmoid(z):
return 1/(1+np.exp(-z))

노드의 개수를 자유롭게 설정할 수 있도록 설계하였다.

학습 코드

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
epochs = 5
i_nodes = 784
h_nodes_1 = 100
h_nodes_2 = 30
o_nodes = 10

print("mnist.shape = ", mnist.shape)

'''
784 -> 100 -> 30 -> 10 ->
'''
model = ANN((i_nodes, h_nodes_1, h_nodes_2, o_nodes))

start_time = datetime.now()

for i in range(epochs):
for step, x_image in enumerate(x_train): # 이미지 하나씩 학습시키기
target_data = np.zeros(o_nodes) + 0.01 # 정답 배열 초기화
target_data[int(t_train[step])] = 0.99 # 정답 체크
input_data = ((x_image / 255.0) * 0.99) + 0.01 # 정규화

model.train(np.array(input_data, ndmin=2), # 학습 시키기
np.array(target_data, ndmin=2),
learning_rate=0.3)

if step % 5000 == 0: # 진행률 체크
print("epochs = ", i, ", step = ", step, ", current loss_val = ", model.loss_val())

end_time = datetime.now()

print("\nelapsed time = ", end_time - start_time)

입력층 노드 784개

은닉층 노드 100, 30개

출력층 노드 10개 (분류)

로 설정하고 5 epoch * 20000 SET 학습을 진행하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mnist.shape =  (20000, 785)
epochs = 0 , step = 0 , current loss_val = 7.361997247821054
epochs = 0 , step = 5000 , current loss_val = 1.3882195712715544
epochs = 0 , step = 10000 , current loss_val = 0.6916458477579397
epochs = 0 , step = 15000 , current loss_val = 0.8437868636340018
epochs = 1 , step = 0 , current loss_val = 3.2593611260969086
epochs = 1 , step = 5000 , current loss_val = 1.2807547910852461
epochs = 1 , step = 10000 , current loss_val = 0.7702014136086927
epochs = 1 , step = 15000 , current loss_val = 0.8132574551438836
epochs = 2 , step = 0 , current loss_val = 2.8910412787403597
epochs = 2 , step = 5000 , current loss_val = 1.453845996607905
epochs = 2 , step = 10000 , current loss_val = 0.8160790710073001
epochs = 2 , step = 15000 , current loss_val = 0.8206369136905732
epochs = 3 , step = 0 , current loss_val = 1.7976268043815586
epochs = 3 , step = 5000 , current loss_val = 3.0248813198917284
epochs = 3 , step = 10000 , current loss_val = 0.8381474597698095
epochs = 3 , step = 15000 , current loss_val = 0.8639893987322178
epochs = 4 , step = 0 , current loss_val = 0.9221147701065991
epochs = 4 , step = 5000 , current loss_val = 2.93830546527445
epochs = 4 , step = 10000 , current loss_val = 0.8848080898919032
epochs = 4 , step = 15000 , current loss_val = 0.8649782969055154

elapsed time = 0:01:13.346241

학습하는데 약 1분 13초의 시간이 소요되었다.

정확도 테스트

1
2
3
4
5
6
7
8
test_data = np.loadtxt('/content/sample_data/mnist_test.csv', delimiter=',', dtype=np.float32)

x_test = test_data[:,1:]
t_test = test_data[:,:1]

print("test_data.shape = ", test_data.shape)

(matched, unmatched) = model.accuracy(x_test, t_test)
1
2
test_data.shape =  (10000, 785)
Current Accuracy = 0.9489

약 95%의 정확도가 나왔다.


Google Colab