04.5 학습 알고리즘 구현하기
신경망 학습에 관한 기본적인 지식은 이만하면 충분합니다. ‘손실 함수’, ‘미니배치’, ‘기울기’, ‘경사 하강법’ 등의 중요한 키워드가 연달아 등장해서 숨 가쁘셨죠? 복습도 할 겸, 신경망 학습의 순서를 확인해보죠. 자, 신경망 학습의 절차는 다음과 같습니다.
-
전제 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 ‘학습’이라 합니다. 신경망 학습은 다음과 같이 4단계로 수행합니다.
-
1단계 - 미니배치 훈련 데이터 중 일부를 무작위로 가져옵니다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함수 값을 줄이는 것을 목표로 합니다.
-
2단계 - 기울기 산출 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구합니다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시합니다.
-
3단계 - 매개변수 갱신 가중치 매개변수를 기울기 방향으로 아주 조금 갱신합니다.
-
4단계 - 반복 1~3단계를 반복합니다.
이것이 신경망 학습이 이뤄지는 순서입니다. 이는 경사 하강법으로 매개변수를 갱신하는 방법이며, 이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법 stochastic gradient descent, SGD이라고 부릅니다. ‘확률적으로 무작위로 골라낸 데이터’에 대해 수행하는 경사 하강법이라는 의미죠. 대부분의 딥러닝 프레임워크는 확률적 경사 하강법의 영어 머리글자를 딴 SGD라는 함수로 이 기능을 구현하고 있습니다.
그럼 실제로 손글씨 숫자를 학습하는 신경망을 구현해봅시다. 여기에서는 2층 신경망(은닉층이 1개인 네트워크)을 대상으로 MNIST 데이터셋을 사용하여 학습을 수행합니다.
4.5.1 2층 신경망 클래스 구현하기
처음에는 2층 신경망을 하나의 클래스로 구현하는 것부터 시작합니다. 이 클래스의 이름은 TwoLayerNet입니다.* 소스 코드는 ch04/two_layer_net.py에 있습니다.
import sys, os
sys.path.append(os.pardir)
from common.functions import *
from common.gradient import numerical_gradient
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)
def predict(self, x):
W1, W2 = self.params['W1'], self.params['W2']
b1, b2 = self.params['b1'], self.params['b2']
a1 = np.dot(x, W1) + b1
z1 = sigmoid(a1)
a2 = np.dot(z1, W2) + b2
y = softmax(a2)
return y
# x : 입력 데이터, t : 정답 레이블
def loss(self, x, t):
y = self.predict(x)
return cross_entropy_error(y, t)
def accuracy(self, x, t):
y = self.predict(x)
y = np.argmax(y, axis=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
코드가 좀 길어졌습니다만, 앞에서 다룬 신경망의 순전파 처리 구현과 공통되는 부분이 많아 새로운 내용은 딱히 없습니다. 우선 이 클래스가 사용하는 변수와 메서드를 정리해보겠습니다. 중요한 변수를 선별하여 [표 4-1]에 정리했고, 메서드들은 모두를 [표 4-2]에 정리했습니다.
표 4-1 TwoLayerNet 클래스가 사용하는 변수
| 변수 | 설명 |
|---|---|
params |
신경망의 매개변수를 보관하는 딕셔너리 변수(인스턴스 변수)params['W1']은 1번째 층의 가중치, params['b1']은 1번째 층의 편향params['W2']는 2번째 층의 가중치, params['b2']는 2번째 층의 편향 |
grads |
기울기를 보관하는 딕셔너리 변수(numerical_gradient() 메서드의 반환 값)grads['W1']은 1번째 층의 가중치의 기울기, grads['b1']은 1번째 층의 편향의 기울기grads['W2']는 2번째 층의 가중치의 기울기, grads['b2']는 2번째 층의 편향의 기울기 |
표 4-2 TwoLayerNet 클래스의 메서드
| 메서드 | 설명 |
|---|---|
__init__(self, input_size, hidden_size, output_size) |
초기화를 수행한다. 인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수 |
predict(self, x) |
예측(추론)을 수행한다. 인수 x는 이미지 데이터 |
loss(self, x, t) |
손실 함수의 값을 구한다. 인수 x는 이미지 데이터, t는 정답 레이블(아래 칸의 세 메서드의 인수들도 마찬가지) |
accuracy(self, x, t) |
정확도를 구한다. |
numerical_gradient(self, x, t) |
가중치 매개변수의 기울기를 구한다. |
gradient(self, x, t) |
가중치 매개변수의 기울기를 구한다. numerical_gradient()의 성능 개선판! 구현은 다음 장에서… |
TwoLayerNet 클래스는 딕셔너리인 params와 grads를 인스턴스 변수로 갖습니다. params 변수에는 가중치 매개변수가 저장되는데, 예를 들어 1번째 층의 가중치 매개변수는 params['W1'] 키에 넘파이 배열로 저장됩니다. 마찬가지로 1번째 층의 편향은 params['b1'] 키로 접근합니다. 예를 하나 볼까요?
net = TwoLayerNet(input_size=784, hidden_size=100, output_size=10)
net.params['W1'].shape # (784, 100)
net.params['b1'].shape # (100,)
net.params['W2'].shape # (100, 10)
net.params['b2'].shape # (10,)
이와 같이 params 변수에는 이 신경망에 필요한 매개변수가 모두 저장됩니다. 그리고 params 변수에 저장된 가중치 매개변수가 예측 처리(순방향 처리)에서 사용됩니다. 참고로 예측 처리는 다음과 같이 실행할 수 있습니다.
x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량)
y = net.predict(x)
grads 변수에는 params 변수에 대응하는 각 매개변수의 기울기가 저장됩니다. 예를 들어 다음과 같이 numerical_gradient() 메서드를 사용해 기울기를 계산하면 grads 변수에 기울기 정보가 저장됩니다.
x = np.random.rand(100, 784) # 더미 입력 데이터(100장 분량)
t = np.random.rand(100, 10) # 더미 정답 레이블(100장 분량)
grads = net.numerical_gradient(x, t) # 기울기 계산
grads['W1'].shape # (784, 100)
grads['b1'].shape # (100,)
grads['W2'].shape # (100, 10)
grads['b2'].shape # (10,)
이어서 TwoLayerNet의 메서드들을 살펴보시죠. 우선 __init__(self, input_size, hidden_size, output_size) 메서드는 클래스를 초기화합니다(초기화 메서드는 TwoLayerNet을 생성할 때 불리는 메서드입니다). 인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수입니다. 예를 들어 손글씨 숫자 인식을 할 경우엔 크기가 $28 \times 28$인 입력 이미지가 총 784개이고, 출력은 10개가 됩니다. 따라서 input_size=784, output_size=10으로 지정하고 은닉층의 개수인 hidden_size는 적당한 값을 설정합니다.
이 초기화 메서드에서는 가중치 매개변수도 초기화합니다. 가중치 매개변수의 초깃값을 무엇으로 설정하느냐가 신경망 학습의 성공을 좌우하기도 합니다. 가중치 매개변수 초기화에 대한 자세한 내용은 나중에 살펴볼 겁니다. 호기심은 잠시 접어두시고, 당장은 정규분포를 따르는 난수로, 편향은 0으로 초기화한다고 이야기하고 넘어가겠습니다. predict(self, x)와 accuracy(self, x, t)의 구현은 앞에서 본 신경망의 예측 처리와 거의 같으니, 이해되지 않는 점이 있다면 앞 장을 확인해보세요. loss(self, x, t)는 손실 함수의 값을 계산하는 메서드입니다. 이 메서드는 predict()의 결과와 정답 레이블을 바탕으로 교차 엔트로피 오차를 구하도록 구현했습니다.
남은 numerical_gradient(self, x, t) 메서드는 각 매개변수의 기울기를 계산합니다. 수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기를 계산하죠. 마지막 gradient(self, x, t)는 다음 장에서 구현할 메서드입니다. 이 메서드는 오차역전파법을 사용하여 기울기를 효율적이고 빠르게 계산합니다.
NOTE_
numerical_gradient(self, x, t)는 수치 미분 방식으로 매개변수의 기울기를 계산합니다. 다음 장에서는 이 기울기 계산을 고속으로 수행하는 기법을 설명합니다. 그 방법은 바로 오차역전파법입니다. 오차역전파법을 쓰면 수치 미분을 사용할 때와 거의 같은 결과를 훨씬 빠르게 얻을 수 있죠. 오차역전파법으로 기울기를 구하는gradient(self, x, t)메서드는 다음 장에서 구현합니다. 신경망 학습은 시간이 오래 걸리니, 시간을 절약하려면numerical_gradient(self, x, t)대신gradient(self, x, t)을 쓰는 것이 좋습니다!
4.5.2 미니배치 학습 구현하기
신경망 학습 구현에는 앞에서 설명한 미니배치 학습을 활용하겠습니다. 미니배치 학습이란 훈련 데이터 중 일부를 무작위로 꺼내고(미니배치), 그 미니배치에 대해서 경사법으로 매개변수를 갱신합니다. 그럼 TwoLayerNet 클래스와 MNIST 데이터셋을 사용하여 학습을 수행해봅시다(소스 코드는 ch04/train_neuralnet.py에 있습니다).
지니의 요술 국자 (미니배치 추출)
- 지니: “도로시, 6만 개의 이미지 데이터 전체를 한꺼번에 학습하려면 컴퓨터 연산이 너무 무겁고 오래 걸려. 그래서 이 6만 개의 알록달록한 구슬(전체 데이터)이 담긴 거대한 단지에서 매번 100개씩만 무작위로 국자로 퍼올려서(미니배치), 그 묶음만 가지고 빠르게 기울기를 구해 가중치를 업데이트할 거야!”
- 도로시: “아하! 매번 전체를 샅샅이 보지 않아도, 100개만 무작위로 뽑아서 국자로 뜨면 그 평균적인 방향이 전체 오차를 줄이는 방향과 거의 비슷하겠구나! 정말 기발하고 실용적인 방법이야!”
- 토토: “왈왈! (국자를 가리키며) 국자로 푹 퍼서 100개씩 학습하자 멍!”
지니가 60,000개의 알록달록한 구슬이 든 ‘전체 데이터’ 단지에서 ‘미니배치’ 요술 국자로 한 번에 100개씩의 데이터를 퍼서 도로시의 바구니에 담아 주고 있어요.
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)
train_loss_list = []
# 하이퍼파라미터
iters_num = 10000 # 반복 횟수
train_size = x_train.shape[0]
batch_size = 100 # 미니배치 크기
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
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)
여기에서는 미니배치 크기를 100으로 했습니다. 즉, 매번 60,000개의 훈련 데이터에서 임의로 100개의 데이터(이미지 데이터와 정답 레이블 데이터)를 추려냅니다. 그리고 그 100개의 미니배치를 대상으로 확률적 경사 하강법을 수행해 매개변수를 갱신합니다. 경사법에 의한 갱신 횟수(반복 횟수)를 10,000번으로 설정하고, 갱신할 때마다 훈련 데이터에 대한 손실 함수를 계산하고, 그 값을 배열에 추가합니다. 이 손실 함수의 값이 변화하는 추이를 그래프로 나타내면 [그림 4-11]처럼 됩니다.
그림 4-11 손실 함수의 추이 : 왼쪽은 10,000회 반복까지의 추이, 오른쪽은 1,000회 반복까지의 추이

[그림 4-11]을 보면 학습 횟수가 늘어가면서 손실 함수의 값이 줄어듭니다. 이는 학습이 잘 되고 있다는 뜻으로, 신경망의 가중치 매개변수가 서서히 데이터에 적응하고 있음을 의미합니다. 바로 신경망이 학습하고 있는 것이죠. 다시 말해 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있습니다!
도로시의 쳇바퀴 학습 루프 (에폭과 루프)
- 도로시: “와! 쳇바퀴를 쉼 없이 돌리면서 1단계 미니배치 뜨기, 2단계 기울기 계산, 3단계 가중치 갱신, 4단계 반복을 계속 수행하니까 그래프의 오차가 스르륵 가라앉아! 진짜 학습이 잘 일어나고 있네!”
- 지니: “그렇단다! 훈련 데이터 6만 장을 한 바퀴 다 돌 때까지 이 과정을 반복하는 한 사이클 단위를 1에폭(Epoch)이라고 불러. 에폭이 돌 때마다 훈련용과 시험용 데이터를 각각 평가해서 두 실력이 모두 올라가고 있는지 모니터링해야 오버피팅을 방지할 수 있어!”
- 토토: “왈왈! (쳇바퀴 위에서 달리며) 쳇바퀴를 돌릴 때마다 손실 점수가 점점 내려간다 멍!”
도로시와 토토가 ‘학습 루프’ 회전 쳇바퀴 위를 신나게 달리며 반복 훈련을 진행하고 있고, 지니가 칠판 앞에서 미니배치 추출, 기울기 계산, 매개변수 갱신의 각 4단계를 짚어가며 1에폭의 개념을 지도해 주고 있어요.
4.5.3 시험 데이터로 평가하기
[그림 4-11]의 결과에서 학습을 반복함으로써 손실 함수의 값이 서서히 내려가는 것을 확인하셨나요? 이때의 손실 함수의 값이란, 정확히는 ‘훈련 데이터의 미니배치에 대한 손실 함수’의 값입니다. 훈련 데이터의 손실 함수 값이 작아지는 것은 신경망이 잘 학습하고 있다는 방증이지만, 이 결과만으로는 다른 데이터셋에도 비슷한 실력을 발휘할지는 확실하지 않습니다.
신경망 학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야 합니다. 다른 말로 ‘오버피팅’을 일으키지 않는지 확인해야 합니다. 오버피팅되었다는 것은, 예를 들어 훈련 데이터에 포함된 이미지만 제대로 구분하고, 그렇지 않은 이미지는 식별할 수 없다는 뜻입니다.
신경망 학습의 원래 목표는 범용적인 능력을 익히는 것입니다. 범용 능력을 평가하려면 훈련 데이터에 포함되지 않은 데이터를 사용해 평가해봐야겠죠? 이를 위해 다음 구현에서는 학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록합니다. 여기에서는 1에폭별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록합니다.
NOTE_ 에폭 epoch은 하나의 단위입니다. 1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당합니다. 예컨대 훈련 데이터 10,000개를 100개의 미니배치로 학습할 경우, 확률적 경사 하강법을 100회 반복하면 모든 훈련 데이터를 ‘소진’한 게 됩니다. 이 경우 100회가 1에폭이 됩니다.
그럼 평가가 제대로 이뤄질 수 있도록 앞의 구현으로부터 조금만 수정해보겠습니다. 이전 구현에서 달라진 부분은 굵게 표시했습니다.
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)
train_loss_list = []
train_acc_list = []
test_acc_list = []
# 1에폭당 반복 수
iter_per_epoch = max(train_size / batch_size, 1)
# 하이퍼파라미터
iters_num = 10000
batch_size = 100
learning_rate = 0.1
network = TwoLayerNet(input_size=784, hidden_size=50, output_size=10)
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)
# 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))
이 예에서는 1에폭마다 모든 훈련 데이터와 시험 데이터에 대한 정확도를 계산하고, 그 결과를 기록합니다. 정확도를 1에폭마다 계산하는 이유는 for 문 안에서 매번 계산하기에는 시간이 오래 걸리고, 또 그렇게까지 자주 기록할 필요도 없기 때문이죠. 더 큰 관점에서 그 추이를 알 수 있으면 충분합니다.
그럼 앞의 코드로 얻은 결과를 그래프로 그려봅시다(그림 4-12).
그림 4-12 훈련 데이터와 시험 데이터에 대한 정확도 추이

[그림 4-12]에서는 훈련 데이터에 대한 정확도를 실선으로, 시험 데이터에 대한 정확도를 점선으로 그렸습니다. 보다시피 에폭이 진행될수록(학습이 진행될수록) 훈련 데이터와 시험 데이터를 사용하고 평가한 정확도가 모두 좋아지고 있습니다. 또, 두 정확도에는 차이가 없음을 알 수 있죠(두 선이 거의 겹쳐 있습니다). 다시 말해 이번 학습에서는 오버피팅이 일어나지 않았습니다.*
* TwoLayerNet의 구현은 스탠퍼드 대학교의 CS231n 수업에서 제공한 파이썬 소스 코드를 참고했습니다.
* 옮긴이_ 만약 오버피팅이 일어난다면 이 모습은 어떻게 달라질까요? 훈련이란 훈련 데이터에 대한 정확도를 높이는 방향으로 학습하는 것이니 그 정확도는 에폭을 반복할수록 높아집니다. 반면 훈련 데이터에 지나치게 적응하면, 즉 오버피팅되면 훈련 데이터와는 다른 데이터를 보면 잘못된 판단을 하기 시작합니다. 어느 순간부터 시험 데이터에 대한 정확도가 점차 떨어지기 시작한다는 뜻입니다. 이 순간이 오버피팅이 시작되는 순간입니다. 여기서 중요한 인사이트! 이 순간을 포착해 학습을 중단하면 오버피팅을 효과적으로 예방할 수 있겠죠? 이 기법을 조기 종료(early stopping)라 하며, ‘6.4 바른 학습을 위해’에서 살펴볼 ‘가중치 감소’, ‘드롭아웃’과 함께 대표적인 오버피팅 예방법입니다.