구체적인 구현에 들어가기 전에 신경망 학습의 전체 그림을 복습해 보자. 다음은 신경망 학습의 순서이다.
전제
신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 한다. 신경망 학습은 다음과 같이 4단계로 수행한다.
1단계 - 미니배치
훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실함수 값을 줄이는 것이 목표이다.
2단계 - 기울기 산출
미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.
3단계 - 매개변수 갱신
가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다.
4단계 - 반복
1~3단계를 반복한다.
지금까지 설명한 오차역전파법이 등장하는 단계는 두 번째인 '기울기 산출'이다. 오차역전파법을 이용하면 느린 수치 미분과 달리 기울기를 효율적이고 빠르게 구할 수 있다.
[TwoLayerNet] 클래스의 인스턴스 변수
인스턴스 변수 | 설명 |
---|---|
params | 딕셔너리 변수로, 신경망의 매개변수를 보관. |
params['W1']은 1번째 층의 가중치, params['b1']은 1번째 층의 편향 | |
params['W2']은 2번째 층의 가중치, params['b2']은 2번째 층의 편향 | |
layers | 순서가 있는 딕셔너리 변수로, 신경망의 계층을 보관 |
layers['Affine1'], layers['Affine2']과 같이 각 계층을 순서대로 유지 | |
lastLayer | 신경망의 마지막 계층. 이 예에서는 SoftmaxWithLoss 계층 |
[TwoLayerNet] 클래스의 메서드
메서드 | 설명 |
---|---|
init(self, input_size, hidden_size, output_size, weight_init_std) | 초기화를 수행. 인수는 앞에서부터 입력층 뉴런 수, 은닉층 뉴런 수, 출력층 뉴런 수, 가중치 초기화 시 정규분포의 스케일 |
predict(self, x) | 예측(추론)을 수행한다. |
loss(self, x, t) | 손실 함수의 값을 구한다. |
인수 x는 이미지 데이터 t는 정답 레이블 | |
accuracy(self, x, t) | 정확도를 구한다 |
numerical_gradient(self, x, t) | 가중치 매개변수의 기울기를 수치 미분 방식으로 구한다(앞 장과 같음) |
gradient(self, x ,t) | 가중치 매개변수의 기울기를 오차역전파법으로 구한다 |
앞과 다른 부분은 계층을 사용한다는 것이다. 계층을 사용함으로써 인식 결과를 얻는 처리(predict())와 기울기를 구하는 처리(gradient()) 계층의 전파만으로 동작이 이루어지는 것이다.
# coding: utf-8
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 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
# 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
계층 생성 부분을 잘 보자. 신경망의 계층을 OrderedDict()에 보관하는 점이 중요하다. OrderedDict
는 순서가 있는 딕셔너리다. 그래서 순전파 때는 추가한 순서대로 각 계층의 forward()
메서드를 호출하기만 하면 처리가 완료된다. 마찬가지로 역전파 때는 계층을 반대 순서로 호출하기만 하면 된다.
이처럼 신경망을 '계층'으로 모듈화해서 구현한 효과는 아주 크다.
1층, 2층, 5층, 여러층과 같이 깊은 신경망을 만들고 싶다면, 단순히 필요한 만큼 계층을 더 추가하면 된다.
수치 미분은 느리지만, 구현이 간단하다. 그래서 오차역전파법으로 구한 기울기가 일치함을 체크함으로써 계산의 정확성을 확인할 수 있다. 이와 같이 수치미분의 결과와 오차역전파법의 결과를 비교하여 오차역전파법을 제대로 구현했는지 검증하는 작업을 기울기 확인(gradient check)이라고 한다.
# coding: utf-8
import sys, os
sys.path.append(os.pardir) # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
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)
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))
----------------------------------------------------------------
W1:4.992444822296182e-10 b1:2.7775308568029376e-09
W2:6.1427267257827926e-09 b2:1.4103044333468872e-07
각 가중치 매개변수의 차이의 절댓값을 구하고, 이를 평균한 값을 구한 것이다.
마지막으로 오차역전파법을 사용한 신경망 학습을 구현해 보자. 지금까지와 다른 부분은 기울기를 오차역전파법으로 구현한다는 것 뿐이다.
# coding: utf-8
import sys, os
sys.path.append(os.pardir)
import numpy as np
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 = []
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)