KoreanFoodie's Study

딥러닝 튜토리얼 4강 1부, 신경망 학습 - 밑바닥부터 시작하는 딥러닝 본문

Deep Learning/밑바닥부터 시작하는 딥러닝 1

딥러닝 튜토리얼 4강 1부, 신경망 학습 - 밑바닥부터 시작하는 딥러닝

GoldGiver 2019. 11. 7. 01:08

해당 포스팅은 한빛 미디어에서 출판한 '밑바닥부터 시작하는 딥러닝'이라는 교재의 내용을 따라가며 딥러닝 튜토리얼을 진행하고 있습니다.관련 자료는여기에서 찾거나 다운로드 받으실 수 있습니다.


이번 포스트에서는 신경망 학습에 대해 다루어 보도록 하겠다. 여기서 학습이란 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것을 뜻한다. 신경망이 학습할 수 있도록 해주는 지표에는 손실 함수가 있는데, 이 손실 함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 학습의 목표이다.


데이터에서 학습한다!

  • 데이터 주도 학습

기계학습은 데이터가 생명이다. 데이터를 통해 패턴을 발견하고 모델을 만들 수 있기 때문이다.

위의 예시에서 숫자 '5'를 판별한다고 해 보자. '5'를 분류하는 프로그램을 직접 고안하기는 쉽지 않다.

대신, 아미지에서 특징(feature)을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다. 여기서 말하는 특징은 입력 데이터(입력 이미지)에서 본질적인 데이터(중요한 데이터)를 정확하게 추출할 수 있도록 설계된 변환기를 가리킨다.

이미지의 특징은 보통 벡터로 기술하고, 컴퓨터 비전 분야에서는 SIFT, SURF, HOG 등의 특징을 많이 사용한다. 이런 특징을 사용하여 이미지 데이터를 벡터로 변환하고, 변환된 벡터를 가지고 지도 학습 방식의 대표 분류 기법인 SVM, KNN 등으로 학습할 수 있다.

위와 같은 기계학습에서는 데이터셋으로부터 규칙을 찾아내는 역할을 '기계'가 담당한다. 하지만 이미지를 벡터로 변환할 때 사용하는 특징은 여전히 '사람'이 설계한다.

그런데 신경망은 이미지를 '있는 그대로' 학습한다. 두 번째와 달리, 신경망은 이미지에 포함된 중요한 특징까지도 '기계'가 스스로 학습한다. 즉, 신경망은 모든 문제를 같은 맥락에서 풀 수 있는 장점이 있다. 예를 들어 '5'를 인식하든, '개'를 인식하든, 신경망은 주어진 데이터를 온전히 학습하고, 주어진 문제의 패턴을 'end-to-end'하게 학습할 수 있다.

딥러닝을 종단간 기계학습(end-to-end machine learning)이라고도 한다. 데이터에서 목표한 결과를 사람의 개입 없이 얻는다는 뜻을 담고 있다.

  • 훈련 데이터와 시험 데이터

기계학습은 데이터를 훈련 데이터(training data)와 시험 데이터(test data)로 나눠 학습과 실험을 수행한다. 먼저 훈련 데이터를 이용해 최적의 매개변수를 찾고, 시험 데이터를 이용해 모델의 범용 능력을 판단한다.

범용 능력은 아직 보지 못한 데이터(훈련 데이터에 없는 데이터)로도 문제를 올바르게 풀어내는 능력을 의미한다. 그래서 데이터셋 하나로만 매개변수의 학습과 평가를 수행하면 다른 데이터셋의 테스트에서는 제대로 된 결과를 산출하지 못할 수도 있다. 한 데이터셋에만 지나치게 최적화된 상태를 오버피팅(overfitting)이라고 하고, 이를 피하는 것이 기계학습의 중요한 과제 중 하나이다.


손실 함수

신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현한다. 그리고 그 지표를 가장 좋게 만들어 주는 가중치 매개변수의 값을 탐색한다. 이때, 신경망 학습에서 사용하는 지표는 손실 함수(loss function, 또는 cost function)이라고 한다.

손실 함수는 신경망 성능의 '나쁨'을 나타내는 지표로, 현재의 신경망이 훈련 데이터를 얼마나 잘 처리하지 '못'하느냐를 나타낸다.

  • 평균 제곱 오차

가장 많이 쓰이는 손실 함수는 평균 제곱 오차(mean squared error, MSE)이다.

여기서 yk는 신경망의 출력(신경망의 추정값), tk은 정답 레이블, k는 데이터의 차원 수를 나타낸다.

여기에서 신경망의 출력 y는 소프트맥스 함수의 출력이다(소프트맥스 함수의 출력은 확률로 해석할 수 있음을 기억하자). 정답 레이블 t는 정답인 것만 1로 표시한 것을 보아, 원-핫 인코딩을 사용했음을 알 수 있다.

해당 함수는 다음과 같이 구현할 수 있다.

def mean_square_error(y, t):
    return 0.5 * np.sum((y-t)**2)

즉, 평균 제곱 오차가 작은 쪽이 더 정답에 가까운 결과를 산출한다는 것을 추측할 수 있습니다.

  • 교차 엔트로피 오차

또다른 손실 함수로서 교차 엔트로피 오차(cross entropy error, CEE)도 자주 이용한다.

y는 신경망의 출력, t는 정답 레이블이다. 또, t는 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0이다(원-핫 인코딩). 그래서 실질적으로 정답일 때의 추정(t가 1일 때의 y)의 자연로그를 계산하는 식이 된다. 예를 들어, 정답 레이블은 '2'가 정답이라 하고 이떄의 신경망 출력이 0.6이라면 교차 엔트로피 오차는 -log0.6 = 0.51이 된다. 즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 된다.

이제 교차 엔트로피 함수를 구현해 보자.

def cross_entropy_error(y, t):
    delta = 1e-7
    return -np.sum(t * np.log(y + delta))

코드 마지막을 보면 np.log를 계산할 때 아주 작은 값인 delta를 더했다. 이는 np.log() 함수에 0을 입력하면 마이너스 무한대를 뜻하는 -inf가 되어 더 이상 계산을 진행할 수 없게 되기 때문이다.

  • 미니배치 학습

기계학습 문제는 훈련 데이터를 사용해 학습한다. 구체적으로는, 훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개 변수를 찾아낸다. 이렇게 하려면 모든 훈련 데이터를 대상으로 손실 함수 값을 구해야 한다. 즉, 훈련 데이터가 100개 있으면 그로부터 계산한 100개의 손실 함수 값들의 합을 지표로 삼는 것이다.

지금까지 데이터 하나에 대한 손실 함수만 생각해왔으니, 이제 훈련 데이터 모두에 대한 손실 함수의 합을 구하는 방법을 생각해 보자.

이때 데이터가 N개라면 tnk는 n번째 데이터의 k번째 값을 의미한다(y는 신경망의 출력, t는 정답 레이블이다).

이렇듯, 총 데이터셋 개수 N개에 대한 손실 함수 값의 합을 구하고 이를 N으로 나누는 방식은 '평균 손실 함수'를 구함으로써 통일된 지표를 얻을 수 있다.

하지만 데이터셋이 너무 많을 경우, 이 방식은 현실적이지 않다. 그래서 데이터 일부를 골라 학습을 수행한 후, 이를 전체의 '근사치'로 이용한다. 이때 사용되는 일부를 미니배치*(mini-batch)라고 한다.

먼저, 데이터셋을 가져와 보자.

import sys, os
sys.path.append(os.pardir)
import numpy as numpy 
from dataset.mnist import load_mnist

(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True)

print(x_train.shape)    # (60000, 784)
print(t_train.shape)    # (10000, 10)

이 훈련 데이터에서 무작위로 10장을 빼 보자.

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

np.random.choice(total, partial)을 사용하면, 전체에서 일부를 뽑아올 수 있다.

예시 :

>>> np.random.choice(60000, 10)
array([25209, 52364, 25075, 37923, 39538, 38346,  5213,  9911, 24732, 53500])
  • (배치용) 교차 엔트로피 오차 구현하기

이제 미니배치를 이용해서 교차 엔트로피 오차를 구해보자.

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = t.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

여기서 y.ndim 이 1일 경우 reshape()를 해 주는 것은, 예를 들어 y가 np.array([1], [2], [3]]) 이런 식일 경우, y를 np.array([1, 2, 3])이런 식으로 바꾸어 주는 것을 의미한다.

정답 레이블 t가 원-핫-인코딩 형식으로 나오지 않고, '2', '7' 처럼 레이블 값을 담고 있을 경우, 코드를 이런 식으로 수정해 주면 된다.

def cross_entropy_error(y, t):
    if y.ndim == 1:
        t = t.reshape(1, t.size)
        y = t.reshape(1, y.size)

    batch_size = y.shape[0]
    return -np.sum(t * np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size

np.arange(input)input값 길이의 넘파이를 생성한다. 예를 들어, [0, 1, 2, 3]이 생성되었다고 하자. 이때, t가 [2, 7, 2, 9]이면, y[...]는 [y[0,2], y[1,7], y[2,2], y[3,9]] 가 되어, array([0, 0, 1, 0])이 된다.

  • 왜 손실 함수를 설정하는가?

그런데 우리는 왜 '손실 함수의 값'에 집중을 하는걸까?

왜냐하면 신경망 학습에서는 '미분'이 중요한 역할을 하기 때문이다. 신경망 학습에서는 최적의 매개변수(가중치와 편향)을 탐색할 때, 손실함수의 값을 가능한 작게 하는 매개변수 값을 찾는다. 이때 매개변수의 미분(정확히는 기울기)을 계산하고, 그 미분 값을 단서로 매개변수의 값을 서서히 갱신하는 과정을 반복한다.

즉, 가중치 매개변수의 손실 함수의 미분이란 '가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어떻게 변하나'라는 의미이다.만약 이 미분 값이 음수면 그 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다. 반대로, 이 미분 값이 양수면 그 가중치 매개변수를 음의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다. 그러나 미분 값이 0이면 가중치 매개변수를 어느 쪽으로 움직여도 손실 함수의 값은 달라지지 않는다. 그래서 그 가중치 매개변수의 갱신은 거기서 멈춘다.

다만 한 가지는 기억하고 넘어가야 한다.

신경망을 학습할 때 정확도를 지표로 삼아서는 안된다. 정확도를 지표로 하면 매개변수의 미분이 대부분의 장소에서 0이 되기 때문이다.

정확도는 매개변수의 미세한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화한다(연속적으로 0.1, 0.2씩 증가/감소 하는 것이 아니라, 갑자기 2가 늘고, 3이 주는 식으로 변함). 이는 '계단 함수'를 활성화 함수로 사용하지 않는 이유와도 들어맞는다.

계단 함수의 미분은 위와 같이 대부분의 장소에서 0이다. 그 결과, 계단 함수를 이용하면 손실 함수를 지표로 삼는 게 아무 의미가 없게 된다. 매개변수의 작은 변화가 주는 파장을 계단 함수가 말살하여 손실 함수의 값에는 아무런 변화가 나타나지 않기 때문이다.

계단 함수는 한순간만 변화하지만, 시그모이드 함수의 미분(접선)은 출력이 연속적으로 변하고 곡선의 기울기도 연속적으로 변한다. 즉, 시그모이드 함수의 미분(기울기)은 어느 장소라도 0이 되지 않으므로, 신경망이 올바르게 학습할 수 있는 것이다!


다음 포스트에서는 수치 미분에 대해 조금 더 알아보고, 실제 훈련 알고리즘을 구현해 테스트를 진행해 보겠다.

Comments