본문 바로가기
Study/책『밑바닥부터 시작하는 딥러닝』

4-2. 신경망 학습

by 코드포휴먼 2020. 3. 25.

4.4. 기울기

앞 절에서는 x0 x1 의 편미분을 변수별로 따로 계산했다. 

가령 x0 = 3, x1 = 4 일 때 (x0 , x1) 양쪽의 편미분을 묶어 계산한다고 생각해보자.

위처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 한다.

기울기는 다음과 같이 구현할 수 있다.

def numerical_gradient(f, x):
    h = 1e-4   #0.0001
    grad = np.zeros_like(x)   #x와 형상이 같은 zero 배열 생성
    
    for idx in range(x.size):
        tmp_val = x[idx]
        # f(x+h) 계산
        x[idx] = tmp_val + h
        fxh1 = f(x)
        
        # f(x-h) 계산
        x[idx] = tmp_val - h
        fxh2 = f(x)
        
        grad[idx] = (fxh1 - fxh2) / (2*h)
        x[idx] = tmp_val   #값 복원
        
    return grad

numerical_gradient(f, x) 함수의 동작 방식은 변수가 하나일 때의 수치 미분과 비슷하다. 

인수인 f는 함수이고 x는 넘파이 배열이므로 배열의 각 원소에 대해 수치 미분을 구한다. 

 

세 점 (3, 4), (0, 2), (3, 0)에서의 기울기를 구해본다.

print( numerical_gradient(function_2, np.array([3.0, 4.0])) )   #[6. 8.]
print( numerical_gradient(function_2, np.array([0.0, 2.0])) )   #[0. 4.]
print( numerical_gradient(function_2, np.array([3.0, 0.0])) )   #[6. 0.]

 이처럼 (x0 , x1)의 각 점에서의 기울기를 계산할 수 있다. 

점 (3, 4)에서의 기울기는 (6, 8)인데, 기울기라는게 뭘 의미할까.

 

기울기 결과에 마이너스를 붙인 벡터를 그리면 아래와 같다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 ch04/gradient_2d.py파일)

f(x0, x1) = x0의 제곱 + x1의 제곱 의 기울기

기울기 그림은 방향을 가진 벡터(화살표)로 그려진다.

화살표들은 한 점을 향하고 있으며, '가장 낮은 곳'에서 멀어질수록 화살표의 크기가 커진다. 

 

위 그림의 기울기는 함수의 '가장 낮은 장소(최솟값)'을 가리키지만, 반드시 그렇다고는 할 수 없다. 

기울기는 각 지점에서 낮아지는 방향을 가리킨다.

기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향이다. 

 

 

4.4.1. 경사법 (경사 하강법)

신경망은 최적의 매개변수(가중치와 편향)를 학습 시에 찾는다.

여기서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값이다.

그러나 매개변수 공간이 광대하여 어디가 최솟값이 되는 곳인지를 짐작할 수 없다.

이때 기울기를 잘 활용해 함수의 최솟값을 찾으려는 것이 경사법이다. 

 

각 지점에서 함수의 값을 낮추는 방안을 제시하는 지표가 기울기다.

그러나 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지, 즉 그쪽이 정말로 나아갈 방향인지는 보장할 수 없다.

실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분이다.

더보기

함수가 극솟값, 최솟값, 또 안장점(saddle point)이 되는 장소에서는 기울기가 0이다. 극솟값은 국소적인 최솟값, 즉 한정된 범위엣의 최솟값인 점이다. 안장점은 어느 방향에서 보면 극댓값이고 다른 방향에서 보면 극솟값이 되는 점이다. 경사법은 기울기가 0인 장소를 찾지만 그것이 반드시 최솟이라고는 할 수 없다(극솟값이나 안장점일 가능성이 있다). 또 복잡하고 찌그러진 모양의 함수라면 (대부분) 평평한 곳으로 파고들면서 고원(plateau, 플래토)이라 하는, 학습이 진행되지 않는 정체기에 빠질 수 있다.

 

기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만, 그 방향으로 가야 함수의 값을 줄일 수 있다.

그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아갈 방향을 정해야 한다.

 

경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동한다.

이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다.

이렇게 함수의 값을 점차줄이는 것이 경사법(gradient method)이다.

더보기

경사법은 기계학습을 최적화하는데 흔히 쓰며, 신경망 학습에서 많이 사용한다.

경사법은 최솟값을 찾는 경사 하강법(gradient descent method), 최댓값을 찾는 경사 상승법(gradient ascent method)으로 나뉜다. 다만 손실 함수의 부호를 반전시키면 최솟값을 찾는 문제와 최댓값을 찾는 문제는 같은 것이니 하강이냐 상승이냐는 본질적으로 중요하지 않다.

일반적으로 신경망(딥러닝) 분야에서의 경사법은 '경사 하강법'으로 등장할 떄가 많다.

 

경사법을 수식으로 나타내면 아래와 같다.

경사법 수식

n 기호(eta, 에타)는 갱신하는 양을 나타낸다. 이를 신경망 학습에서 학습률(learning rate)이라고 한다.

한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 얼마나 갱신하느냐를 정하는 것이 학습률이다.

학습률 값은 특정 값으로 미리 정해둬야 한다. 너무 크거나 작으면 '좋은 장소'를 찾아갈 수 없다.

 

위의 식은 1회에 해당하는 갱신이고, 이 단계를 반복한다.

변수 값 갱신을 여러 번 반복하며 서서히 함수의 값을 줄인다.

변수의 수가 늘어도 같은 식(각 변수의 편미분 값)으로 갱신한다.

 

경사 하강법은 아래와 같이 구현할 수 있다.

def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    
    for i in range(step_num):
        grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

인수 f는 최적화하려는 함수, init_x는 초깃값, lr은 learning rate를 의미하는 학습률, step_num은 반복 횟수를 뜻한다.

함수의 기울기는 numerical_gradient(f, x)로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다.

이 함수를 통해 함수의 극솟값을 구하고, 잘하면 최솟값을 구할 수도 있다.

 

위의 문제를 풀겠다.

def function_2(x):
    return x[0]**2 + x[1]**2

init_x = np.array([-3.0, 4.0])

gradient_descent(function_2, init_x=init_x, lr=0.1, step_num=100)   
#array([-6.11110793e-10,  8.14814391e-10])

초깃값을 (-3.0, -4.0)으로 설정한 후 경사법을 사용해 최솟값 탐색을 시작한다.

최종 결과는(-6.1e-10, -8.1e-10)으로, 거의 (0.0)에 가까운 결과다.

실제로 진정한 최솟값은 (0.0)이므로 경사법으로 거의 정확한 결과를 얻었다.

경사법을 사용한 이 갱신 과정을 그림으로 나타내면 아래와 같다. 

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 ch04/gradient_method.py파일)

 

학습률 설정은 중요하다. 

학습률이 너무 크면 큰 값으로 발산해버리고, 너무 작으면 거의 갱신되지 않은 채 끝난다.

# 학습률이 너무 큰 예 : lr=10.0
init_x = np.array([-3.0, 4.0])
print( gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100) )   
#array([-2.58983747e+13, -1.29524862e+12])

# 학습률이 너무 작은 예 : lr=1e-10
init_x = np.array([-3.0, 4.0])
print( gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100) )
#array([-2.99999994,  3.99999992])
더보기

학습률 같은 매개변수를 하이퍼파라미터(hyper parameter, 초매개변수)라고 한다. 이는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수다. 

신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 '자동'으로 획득되는 매개변수인 반연, 학습률 같은 하이퍼파라미터는 사람이 직접 설정해야 하는 매개변수다. 

 

 

4.4.2. 신경망에서의 기울기

신경망 학습에서도 기울기를 구해야 한다. 가중치 매개변수에 대한 손실 함수의 기울기다.

예를 들어 형상이 2X3, 가중치가 W, 손실 함수가 L인 신경망을 생각해본다. 

가중치와 경사 배열

경사 배열의 각 원소는 각각의 원소에 관한 편미분이다. 

1행 1번째 원소는 w11을 조금 변경했을 때 손실 함수 L이 얼마나 변화하느냐를 나타낸다.  

가중치와 경사는 배열의 형상이 같다. 

 

간단한 신경망을 예로 들어 실제로 기울기를 구하는 코드를 구현한다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 ch04/gradient_simplenet.py파일)

import sys, os
sys.path.append(os.pardir)  # 부모 디렉터리의 파일을 가져올 수 있도록 설정
import numpy as np
from common.functions import softmax, cross_entropy_error
from common.gradient import numerical_gradient


class simpleNet:
    def __init__(self):
        self.W = np.random.randn(2,3) # 정규분포로 초기화

    def predict(self, x):
        return np.dot(x, self.W)

    def loss(self, x, t):
        z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)

        return loss

여기에서는 common/functions.py에 정의한 softmax와 cross_entropy_error 메서드를 이용한다.

common/gradient.py에 정의한 numerical_gradient 메서드도 이용한다.

simpleNet 클래스는 형상이 2X3인 가중치 매개변수 하나를 인스턴스 변수로 갖는다. 

메서드는 2개 인데, 하나는 예측을 수행하는 predict(x), 다른 하나는 손실 함수의 값을 구하는 loss(x, t)다.

인수 x는 입력 데이터, t는 정답 레이블이다.

 

simpleNet을 사용해 몇 가지 시험을 해본다.

net = simpleNet()
print(net.W)   #가중치 매개변수
#[[ 0.96664203 -0.84718188  0.39608711]
# [ 0.27554852  0.82301459 -0.65741839]]

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)
#[ 0.82797889  0.23240401 -0.35402429]
print(np.argmax(p))   #최댓값의 인덱스
#0

t = np.array([0, 0, 1])
print(net.loss(x, t))
#1.8014544459221402

 

이어서 numerical_gradient(f, x)를 써서 기울기를 구해본다. 

아래에서 정의한 f(W) 함수의 인수 W는 더미(dummy)로 만든 것이다.

numerical_gradient(f, x) 내부에서 f(x)를 실행하는데, 그와의 일관성을 위해 f(W)를 정의한 것이다.

def f(W):
    return net.loss(x, t)

dW = numerical_gradient(f, net.W)
print(dW)
#[[ 0.32294343  0.17802114 -0.50096457]
# [ 0.48441515  0.26703171 -0.75144685]]

numerical_gradient(f, x)의 인수 f는 함수, x는 함수 f의 인수다.

net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의했다.

그리고 이 새로 정의한 함수를 numerical_gradient(f, x)에 넘긴다.

 

dW는 numerical_gradient(f, net.W)의 결과로, 그 형상은 2X3의 2차원 배열이다.

dW의 내용을 보면, 예를 들어 경사 배열의 1행 1열은 대략 0.3다. 이는 w11을 h만큼 늘리면 손실 함수의 값은 0.3h만큼 증가한다는 의미다. 

마찬가지로 2행 3열은 대략 -0.75다. w23을 h만큼 늘리면 손실 함수의 값은 0.75만큼 감소한다. 

손실 함수를 줄이는 관점에서는 경사 배열의 1행 1열은 음의 방향으로 갱신하고, 2행 3열을 양의 방향으로 갱신해야 한다.

 

파이썬에서 lambda 기법을 쓰면 더 쉽게 함수를 정의할 수 있다.

f = lambda w: net.loss(x, t)
dW = numerical_gradient(f, net.W)

신경망의 기울기를 구한 다음에는 경사법에 따라 가중치 매개변수를 갱신하기만 하면 된다.

 

 

4.5. 학습 알고리즘 구현하기

신경망 학습의 절차는 다음과 같다.

 

전제)

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

신경망 학습은 4단계로 수행한다.

1단계 - 미니배치)

훈련 데이터 중 일부를 무작위로 가져온다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함수 값을 줄이는 것이 목표다.

2단계 - 기울기 산출)

미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시한다.

3단계 - 매개변수 갱신)

가중치 매개변수를 기울기 방향으로 아주 조금 생신한다.

4단계 - 반복)

1~3단계를 반복한다.

 

경사 하강법으로 매개변수를 갱신하는 방법이며, 이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(stochastic gradient descent, SGD)이라고 부른다. '확률적으로 무작위로 골라낸 데이터'에 대해 수행하는 경사 하강법이라는 의미다.

대부분의 딥러닝 프레임워크는 SGD라는 함수로 이 기능을 구현한다. 

 

 

4.5.1. 2층 신경망 클래스 구현하기

손글씨 숫자를 학습하는 신경망을 구현한다.

2층 신경망(은닉층이 1개인 네트워크)을 대상으로 MNIST 데이터셋을 사용해 학습을 수행한다.

처음에는 2층 신경망을 하나의 클래스로 구현하는 것부터 시작한다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 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
        
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        da1 = np.dot(dy, W2.T)
        dz1 = sigmoid_grad(a1) * da1
        grads['W1'] = np.dot(x.T, dz1)
        grads['b1'] = np.sum(dz1, axis=0)

        return grads

신경망의 순전파 처리 구현과 공통되는 부분이 많다. 

 

<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번째 층의 편향의 기울기

 

<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)
print(net.params['W1'].shape)   #(784, 100)
print(net.params['b1'].shape)   #(100,)
print(net.params['W2'].shape)   #(100, 10)
print(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)   #기울기 계산

print(grads['W1'].shape)   #(784, 100)
print(grads['b1'].shape)   #(100,)
print(grads['W2'].shape)   #(100, 10)
print(grads['b2'].shape)   #(10,)

 

TwoLayerNet의 메서드를 살펴보겠다.

우선 __init__(self, input_size, hidden_size, output_size) 메서드는 클래스를 초기화한다.(초기화 메서드는 TwoLayerNet을 생성할 때 불리는 메서드다).

인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수다.

예를 들어 손글씨 숫자 인식에서는 크기가 28X28인 입력 이미지가 총 784개이고, 출력은 10개가 된다.

따라서 input_size=784, output_size=10으로 지정하고 은닉층 개수인 hidden_size는 적당한 값을 설정한다.

이 초기화 메서드에서는 가중치 매개변수도 초기화한다. 가중치 매개변수 초기화는 중요한 문제인데, 일단 정규분포를 따르는 난수로, 편향은 0으로 초기화한다.

손실 함수 값을 계산하는 loss(self, x, t)는 predict()의 결과와 정답 레이블을 바탕으로 교차 엔트로피 오차를 구한다.

numerical_gradient(self, x, t)는 각 매개변수의 기울기를 계산한다. 수치 미분 방식으로 각 매개변수의 손실 함수에 대한 기울기를 계산한다.

더보기

numerical_gradient(self, x, t)는 수치 미분 방식으로 매개변수의 기울기를 계산한다.

다음 장에서는 기울기 계산을 고속으로 수행하는 gradient(self, x, t) 메서드를 설명한다. gradient(self, x, t) 메서드는 오차역전파법을 사용한다. 

 

 

4.5.2. 미니배치 학습 구현하기

신경망 학습 구현에는 미니배치 학습을 활용한다.

미니배치 학습이란 훈련 데이터 중 일부를 무작위로 꺼내고(미니배치), 그 미니배치에 대해 경사법으로 매개변수를 갱신한다.

TwoLayerNet 클래스와 MNIST 데이터셋을 사용하여 학습을 수행해본다.

(깃허브 https://github.com/code4human/deep-learning-from-scratch 에서 ch04/train_neuralnet.py파일.

dataset.mnist와 two_layer_net 파일도 책에서 제공하는 파일로서, 깃허브에 있다.)

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)

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으로 했다. 즉 매번 60000개의 훈련 데이터에서 임의로 100개의 데이터(이미지 데이터와 정답)를 추려낸다. 그리고 그 100개의 미니배치를 대상으로 확률적 경사 하강법을 수행해 매개변수를 갱신한다.

경사법에 의한 갱신 횟수(반복 횟수)를 10000번으로 설정하고, 갱신할 때마다 훈련 데이터에 대한 손실 함수를 계산하고, 그 값을 배열에 추가한다.

 

손실 함수 값의 추이 : 왼쪽은 10000회 반복까지의 추이, 오른쪽은 1000회 반복까지의 추이

학습 횟수가 늘어가면서 손실 함수의 값이 줄어든다.

학습이 잘 되고 있다는 뜻으로, 신경망의 가중치 매개변수가 서서히 데이터에 적응하고 있음을 의미한다.

데이터를 반복 학습하여 최적 가중치 매개변수로 다가서고 있다.

 

 

4.5.3. 시험 데이터로 평가하기

학습을 반복함으로써 손실 함수의 값은 서서히 내려간다.

이때의 손실 함수의 값이란, '훈련 데이터의 미니배치에 대한 손실 함수'의 값이다.

훈련 데이터의 손실 함수 값이 작아지는 것은 신경망이 잘 학습하고 있다는 방증이지만, 이 결과만으로는 다른 데이터셋에도 비슷한 실력을 발휘할지는 확실하지 않다.

 

신경망 학습에서는 overfitting을 꺼린다. 

범용 능력을 평가하려면 훈련 데이터에 포함되지 않은 데이터를 사용해 평가해봐야 한다. 

학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록하겠다.

여기에선 1에폭별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록한다.

더보기

에폭(epoch)은 하나의 단위다. 1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당한다.

예컨대 훈련 데이터 10000개를 100개의 미니배치로 학습할 경우, 확률적 경사 하강법을 100회 반복하면 모든 훈련 데이터를 '소진'한게 된다. 이 경우 100회가 1에폭이 된다. 

 

아래의 예는 1에폭마다 모든 훈련 데이터와 시험 데이터에 대한 정확도를 계산하고, 그 결과를 기록한다.

정확도를 1에폭마다 계산하는 이유는 for문 안에서 매번 계산하기에는 시간이 오래 걸리고, 너무 자주 기록할 필요도 없기 때문이다.  

#오버피팅 경계하기 위해 1에폭 별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록한다.
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_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):
    # 미니배치 획득
    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))

# 그래프 그리기
markers = {'train': 'o', 'test': 's'}
x = 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(0, 1.0)
plt.legend(loc='lower right')
plt.show()

 

앞의 코드로 얻은 결과 그래프다.

훈련 데이터와 시험 데이터에 대한 정확도 추이

훈련 데이터 정확도를 실선으로, 시험 데이터 정확도를 점선으로 그렸다.

에폭이 진행될수록(학습이 진행될수록) 훈련 데이터와 시험 데이터를 사용하고 평가한 정확도가 모두 좋아진다. 

두 정확도에는 차이가 없다. 이번 학습에는 오버피팅이 일어나지 않았다. 

 

이번 장에서는 작은 손실 함수의 값을 찾는 방법으로 경사법을 알아보았다.

경사법은 함수의 기울기를 이용하는 방법이다. 

 


<참고 문헌>

사이토 고키(2019), 『밑바닥부터 시작하는 딥러닝』, 한빛미디어, pp.127-146.

'Study > 책『밑바닥부터 시작하는 딥러닝』' 카테고리의 다른 글

5-2. 오차역전파법  (0) 2020.04.09
5-1. 오차역전파법  (0) 2020.03.31
4-1. 신경망 학습  (0) 2020.03.16
3-2. 신경망  (0) 2020.03.09
보충) numpy 행렬의 형상 차이 - (N,) (N,1)(1,N)  (0) 2020.03.08

댓글