[밑시딥 1] Chapter 4. 신경망 학습

Seong Woong Kim·2023년 11월 16일

4.1 데이터에서 학습한다!

학습이란 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것을 뜻한다.

신경망의 특징은 데이터를 보고 학습 할 수 있다는 점이다.

  • 데이터에서 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 자동으로 결정한다는 뜻

4.1.1 데이터 주도 학습

기계학습은 데이터가 생명이다. 데이터에서 답을 찾고 데이터에서 패턴을 발견하고 데이터로 이야기를 만드는 것이 기계학습.

  • 데이터가 이끄는 접근 방식 덕에 사람 중심 접근에서 벗어날 수 있음

위 이미지에서 5를 인식하고 싶다면 이미지에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다.

여기서의 특징이란, 입력 데이터에서 본질적인 데이터를 정확하게 추출할 수 있도록 설계된 변환기를 가리킨다.

이미지의 특징은 보통 벡터로 기술한다.

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

규칙을 '사람'이 만드는 방식에서 '기계'가 데이터로부터 배우는 방식으로의 패러다임 전환.

💡
딥러닝을 종단간 기계학습endtoend  machine  learning^{end-to-end \; machine \; learning}이라고도 함.

  • 여기서 종단간은 '처음부터 끝까지'라는 의미
  • 데이터(입력)에서 목표한 결과(출력)를 사람의 개입 없이 얻는다는 뜻을 답고 있음.

신경망은 주어진 데이터를 온전히 학습하고, 주어진 문제의 패턴을 발견하려고 시도한다.

  • 즉, 신경망은 모든 문제를 주어진 데이터 그대로를 입력 데이터로 활용해 end-to-end로 학습할 수 있다.

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

기계학습 문제는 데이터를 훈련 데이터시험 데이터로 나눠 학습과 실험을 수행하는 것이 일반적임.

  • 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾음
  • 그 다음, 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가함.

왜 나눌까??

  • 우리가 원하는 것은 범용적으로 사용할 수 있는 모델이기 때문 (Generalization)
    • 이 일반화 능력을 제대로 평가하기 위해서 훈련 데이터와 시험 데이터를 분리하는 것

범용 능력은 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력

  • 이 범용 능력을 획득하는 것이 기계학습의 최종 목표이다.

그래서 데이터셋 하나로만 매개변수의 학습과 평가를 수행하면 올바른 평가가 될 수 없음

  • 한 데이터셋에만 지나치게 최적화된 상태를 오버피팅이라고 함.


4.2 손실 함수

신경망 학습에서는 현재의 상태를 하나의 지표로 표현한다.

  • 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것이다.

신경망 학습에서 사용하는 지표는 손실 함수Loss  Function^{Loss \; Function}이라고 한다.

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

4.2.1 오차제곱합

가장 많이 쓰이는 손실 함수는 오차제곱합sum  of  squares  for  error,  SSE^{sum \;of\;squares\;for\;error,\;SSE}이다.

  • yky_k : 신경망의 출력(신경망이 추정한 값)
  • tkt_k : 정답 레이블
  • kk : 데이터의 차원 수

오차제곱합은 각 원소의 출력(추정 값)과 정답 레이블(참 값)의 차(yktky_k-t_k)를 제곱한 후, 그 총합을 구한다.

## MNIST 손글씨 SSE 예제
# y_k
# 첫 번째 인덱스부터 순서대로 숫자 '0','1',..일 때의 값
# Softmax 함수 출력값. (확률로 해석 가능)
# 이미지가 '0'일 확률은 0.1로 해석
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]

# t_k
# 정답을 가리키는 원소는 1 (숫자 2가 정답)
# 원-핫 인코딩 : 한 원소뫈 1로 하고 그 외는 0으로 표기
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# SSE
def sum_squares_error(y: np.array, t: np.array):
	return 0.5 ** np.sum((y-t)**2)
# 정답은 '2'
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# ex1: '2'일 확률이 가장 높다고 추정함 (0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
sum_squares_error(np.array(y), np.array(t)")
>>> 0.09750000....

# ex2: '7'일 확률이 가장 높다고 추정함 (0.6)
y = [0.1, 0.05, 0.0, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
sum_squares_error(np.array(y), np.array(t)")
>>> 0.5975000....
  • 오차제곱합 기준으로 첫 번째 추정 결과(ex1)가 오차가 더 작으니 정답에 더 가까울 것으로 판단할 수 있음

4.2.2 교차 엔트로피 오차

또 다른 손실 함수로서 교차 엔트로피 오차cross  entropy  error;CEE^{cross\;entropy\;error;CEE }도 자주 이용한다.

  • loglog : 밑이 ee인 자연로그
  • yky_k : 신경망으 ㅣ출력
  • tkt_k : 정답 레이블
    • 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0 (원-핫 인코딩)
    • 그래서 실질적으로 정답일 때의 추정(tkt_k가 1일 때의 yky_k)의 자연로그를 계산하는 식이 됨.

신경망 출력이 0.6이라면 교차 엔트로피 오차는 -log0.6으로 결과는 0.51이 된다.

신경망 출력이 0.1이라면 -log0.1으로 출력은 2.30이 된다.

즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 된다.

자연로그 yy = logxlogx의 그래프이다.

  • 그래프에서 보듯이 xx1일 때, yy0이 되고, xx0에 가까워질수록 yy의 값은 점점 작아진다.

마찬가지로, 정답에 해당하는 출력이 커질수록 0에 다가가다가, 그 출력이 1일 때 0이 된다.

  • 반대로 정답일 때의 출력이 작아질수록 오차는 커진다.
def cross_entory_error(y: np.array, t: np.array):
	delta = 1e-7
    return -np.sum(t*np.log(y + delta))

np.log를 계산할 때 delta를 더했는데 이것은 np.log() 함수에 0을 입력하면 -inf가 되어서 계산을 진행할 수 없게 되기 때문이다.

  • 절대 0이 되지 않도록 하기 위해서 delta를 더해주는 것이다.
# CEE 계산 (정답 2)
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t))
>>> 0.510825...

y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
cross_entropy_error(np.array(y), np.array(t))
>>> 2.3025840...
  • 결과(오차 값)가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로, 앞서 오차제곱합의 판단과 일치한다.

4.2.3 미니배치 학습

기계학습 문제는 훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개변수를 찾아내는 것이다.

  • 이렇게 하기 위해선 모든 훈련 데이터를 대상으로 손실 함수 값을 구해야 한다.
  • 훈련 데이터가 100개 있으면 그로부터 계산한 100개의 손실 함수 값들의 을 지표로 삼는 것

훈련 데이터 모두에 대한 손실 함수의 합을 구하는 방법을 생각해볼 때 교차 엔트로피의 오차는 다음과 같이 구해볼 수 있다.

데이터가 NN개라면

  • tnkt_{nk} : nn번째 데이터의 kk번째 값을 의미
  • ynky_{nk} : 신경망의 출력
  • tnkt_{nk} : 정답 레이블

마지막에 NN으로 나누어 정규화한다.

  • NN으로 나눔으로써 평균 손실 함수를 구하는 것
  • 이렇게 평균을 구해 사용하면 훈련 데이터 개수와 관계없이 언제든 통일된 지표를 얻을 수 있다.

하지만 빅데이터 수준이 되면 N이 매우 거대한 값이 되므로 일일이 손실 함수를 계산하는 것은 비현실적이다.

  • 그래서 데이터 중 일부를 추려 전체의 근사치로 이용한다.

이러한 일부를 미니배치minibatch^{mini-batch} 라고 한다.

  • 그리고 이것들을 학습시키는 것을 미니배치 학습이라고 한다. 그것을 코드로 구현하면 다음과 같다.
import sys,os

sys.path.append(os.pardir)
import numpy as np
from dataset.mnist import load_mnist # MNIST 데이터 셋을 읽어오는 함수(훈련 데이터와 시험 데이터를 읽는다)

(x_train, t_train), (x_test, t_test) = \
    load_mnist(normalize=True, one_hot_label=True) # one_hot_label = True 로 원-핫 인코딩을 한다. -> 정답위치만 1 나머지 0

print(x_train.shape) # (60000,784)  # 28x28x1 픽셀을 flatten
print(t_train.shape) # (60000,10)

train_size = x_train.shape[0]
batch_size = 10

# 훈련 데이터 중 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(60000,10)
>>> array([8013, 14666, 52210, 12420, 8107, 21411, 27210, 10153, 48920, 58223])

4.2.4 (배치용) 교차 엔트로피 오차 구현하기

미니배치 같은 배치 데이터를 지원하는 교차 엔트로피 오차를 구현하기 위해서는 교차 엔트로피 오차(데이터를 하나씩 처리하는 구현)를 조금만 바꿔주면 된다.

def cross_entropy_error(y: np.array, t: np.array):
	# 미니 배치 shape을 맞추기 위함
    if y.ndim == 1:
    	t = t.reshape(1, t.size)
    	y = y.reshape(1, y.size)
    
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size
  • y : 신경망의 출력
  • t : 정답 레이블

y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔준다.

그리고 배치의 크기로 나눠 정규화하고 이미지 1장당 평균의 교차엔트로피 오차를 계산한다.


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

우리의 궁극적인 목적은 높은 정확도를 끌어내는 매개변수를 찾는 것이다. 그렇다면 정확도라는 지표를 놔두고 손실 함수의 값이라는 우회적인 방법을 택하는 이유는 대체 뭘까??

이 의문은 신경망 학습에서의 미분의 역할에 주목하면 해결된다.

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

가중치 매개변수의 손실 함수의 미분이란 가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어떻게 변하나라는 의미이다.

만약 이 미분 값이 음수면 그 가중치 매개변수를 양의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다.

반대로, 미분 값이 양수면 가중치 매개변수를 음의 방향으로 변화시켜 손실 함수의 값을 줄일 수 있다.

만약 미분 값이 0이면 어느 쪽으로 움직여도 손실 함수의 값이 줄어들지 않으므로 가중치 매개변수의 갱신이 멈추게 된다.

정확도를 지표로 삼아서는 안 되는 이유는 미분 값이 대부분의 장소에서 0이 되어 매개변수를 갱신할 수 없기 때문이다.

정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화한다.

이는 계단 함수활성화 함수로 사용하지 않는 이유와도 연결이 된다.

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

계단 함수는 한순간만 변화를 일으키지만, 시그모이드 함수의 미분은 그래프와 같이 출력 (세로축의 값)이 연속적으로 변하고 곡선의 기울기도 연속적으로 변한다.

  • 즉, 시그모이드 함수의 미분은 어느 장소라도 0이 되지는 않는다.
    • 이는 신경망 학습에서 중요한 성질로, 기울기가 0이 되지 않는 덕분에 신경망이 올바르게 학습할 수 있는 것.


4.3 수치 미분

4.3.1 미분

미분은 특정 순간의 변화량을 뜻한다.

위 식은 함수의 미분을 나타낸 식이다.

  • 좌변 : f(x)f(x)xx에 대한 미분(xx에 대한 f(x)f(x)의 변화량)을 나타내는 기호
    • 결국, xx작은 변화가 함수 f(x)f(x)를 얼마나 변화시키느냐를 의미
  • limh0\displaystyle\lim_{h\rarr 0} : 시간을 뜻하는 hh를 한없이 0에 가깝게 한다는 의미

다음은 함수를 미분하는 나쁜 구현의 예이다.

#나쁜 구현 예
# 함수의 이름은 수치 미분에서 따온 numerical_diff
def numerical_diff(f, x):
	h = 1e-50
    return (f(x + h) - f(x)) / h

하지만 위 미분 계산에는 오차가 있다.

이 오차를 개선한 그래프를 그려보면 다음과 같다.

  • 진정한 미분 : xx 위치의 함수의 기울기(이를 접선이라 함)
  • 위 구현에서의 미분 : (x+h)(x + h)xx 사이의 기울기

이 오차를 줄이기 위해 (x+h)(x + h)(xh)(x - h)일 떄의 함수 ff의 차분을 계산하는 방법을 쓰기도 한다.

이 차분은 xx를 중심으로 그 전후의 차분을 계산한다는 의미에서 중심 차분 혹은 중앙 차분이라 한다.

이를 활용하여 수치 미분을 다시 구현하면 다음과 같이 작성할 수 있다.

def numerical_diff(f, x):
	h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)

4.3.2 수치 미분의 예

간단한 이차함수를 미분해보면 코드는 다음과 같다.

import numpy as np
import matplotlib.pylab as plt

def numerical_diff(f, x):
	h = 1e-4 # 0.0001
    return (f(x+h) - f(x-h)) / (2*h)
    
def function_1(x):
	return 0.01*x**2 + 0.1*x
    
x = np.arange(0.0,20.0,0.1)
y = function_1(x)
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x,y)
plt.show()

위 코드는 y=0.01x2+0.1xy=0.01x^2+0.1x를 나타낸 파이썬 코드이다.

그래프는 다음과 같다.

이렇게 계산된 미분 값은 xx에 대한 f(x)f(x)의 변화량, 즉 함수의 기울기에 해당한다.

xx가 5일때와 10일 때의 진정한 미분은 차례로 0.20.3이다.


4.3.3 편미분

인수들의 제곱 합을 계산하는 식은 다음과 같다.

f(x0,x1)=x02+x12f(x_0, x_1) = x^2_0 + x^2_1

코드로 구현하면 다음과 같다.

# 넘파이의 배열의 각 원소를 제곱하고 그 합을 구하는 구현
def function_2(x: np.array):
	return x[0]**2 + x[1]**2
	# 또는 return np.sum(x**2)
  • 여기서 주의할 점은 변수가 2개라는 것이다.

그래서 어느 변수에 대한 미분이냐, 즉 x0x_0x1x_1 중 어느 변수에 대한 미분이냐를 구별해야 한다.

이렇게 변수가 여럿인 함수에 대한 미분을 편미분이라고 한다.

편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구한다.

  • 단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정한다.


4.4 기울기

모든 변수의 편미분을 벡터로 정리한 것을 기울기gradient^{gradient}라고 한다.

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

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

def numerical_grdient(f: function, x: np.array):
	h = 1e-4  # -.---1
    grad = np.zeros_like(x)  # x와 형상이 같은 배열 생성
    
    # 넘파이 배열 x의 각 원소에 대해 수치 미분을 구함.
    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
    
 '''세 점 (3,4), (0,2), (3,0) 에서의 기울기'''
 numerical_gradient(function_2, np.array([3.0, 4.0]))
 numerical_gradient(function_2, np.array([0.0, 2.0]))
 numerical_gradient(function_2, np.array([3.0, 0.0]))

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

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

더 정확히 말하자면, 기울기가 가리키는 쪽은 각 장소에서 함수의 출력을 가장 크게 줄이는 방향이다.


4.4.1 경사법(경사 하강법)

기계학습 문제 대부분은 학습 단계에서 최적의 매개변수를 찾아낸다.

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

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

그러나, 일반적인 문제의 손실 함수는 매우 복잡하다.

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

이런 상황에서 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 것이 경사법이다.

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

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

이렇게 해서 함수의 값을 점차 줄이는 것이 경사법gradient  method^{gradient\;method}이다.

경사법을 수식으로 나타내면 다음과 같다.

  • η\eta : 갱신하는 양. 신경망 학습에서의 학습률learning  rate^{learning\;rate}

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

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

또한 학습률의 값은 미리 0.01, 0.001 등 특정 값으로 정해두어야 한다.

신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행한다.

경사하강법을 구현하면 다음과 같다.

def gradient_descent(f, init_x, lr=0.01, step_num=100):
	'''
    f: 최적화 하려는 함수
    init_x : 초깃값
    lr : learning rate
    step_num : 경사법에 따른 반복 횟수
    '''
    
    x = init_x
    
    # 함수의 기울기를 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 반복한다.
    for i in range(step_num):
    	grad = numerical_gradient(f, x)
        x -= lr * grad
    return x

학습률이 너무 크면 큰 값으로 발산하고, 반대로 너무 작으면 거의 갱신되지 않은 채 끝나버린다.

  • 학습률을 적절히 설정하는 것이 중요하다.

💡
학습률 같은 매개변수를 하이퍼파라미터hyper  parameter^{hyper\;parameter}라고 한다.

이는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수이다.

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

일반적으로 이 하이퍼파라미터들은 여러 후보 값 중에서 시험을 통해 가장 잘 학습하는 값을 찾는 과정을 거쳐야 한다.


4.4.2 신경망에서의 기울기

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

가중치가 WW, 손실 함수가 LL인 신경망 경우 편미분을 한다.

그리고 손실 함수 LL이 얼마나 변하는지에 대해서 알려주는 것이 w11w_{11}이다.

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):  # 손실 함수의 값을 구하는 함수
    	'''
        x: 입력 데이터
        t: 정답 레이블
        '''
    	z = self.predict(x)
        y = softmax(z)
        loss = cross_entropy_error(y, t)
        
        return loss
        
# 기울기 구하기
x = np.array([0.6, 0.9])
t = np.array([0, 0, 1])

net = simpleNet()

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


print(dW) # 기울기 출력
>>> [[ 0.21924763   0.14356247  -0.36281009]
     [ 0.32887144   0.2153437   -0.54421514]]
  • 예측을 수행하는 predict(x)와 손실 함수의 값을 구하는 loss(x,t)가 있다.

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

  • 기울기는 numerical_gradient(f,x)를 통해 구할 수 있다.
    f는 함수, x는 함수 f의 인수이다.

그래서 여기에서는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의했다.

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

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

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



4.5 학습 알고리즘 구현

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

전제

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

1단계 - 미니배치

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

2단계 - 기울기 산출

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

3단계 - 매개변수 갱신

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

4단계 - 반복

  • 1~3단계를 반복한다

이것이 신경망 학습이 이뤄지는 순서이며 경사 하강법으로 매개변수를 갱신하는 방법이다.

이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법stochastic  gradient  descent,  SGD^{stochastic\;gradient\;descent,\;SGD}이라고 부른다.

대부분의 딥러닝 프레임워크는 확률적 경사 하강법의 영어 머리글자를 딴 SGD라는 함수로 이 기능을 구현하고 있다.


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

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,
                 ouput_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, ouput_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'])  # (784, 100)
        grads['b1'] = numerical_gradient(loss_W, self.params['b1'])  # (100,)
        grads['W2'] = numerical_gradient(loss_W, self.params['W2'])  # (100, 10)
        grads['b2'] = numerical_gradient(loss_W, self.params['b2'])  # (10,)
        
        return grads

중요 변수와 메서드를 정리한 표는 다음과 같다.

params 변수에는 이 신경망에 필요한 매개변수가 모두 저장되고, params 변수에 저장된 가중치 매개변수가 예측 처리(순방향 처리)에서 사용된다.

grads 변수에는 params 변수에 대응하는 각 매개변수의 기울기가 저장된다.

TwoLayerNet의 메서드들을 살펴보면 init을 통해 클래스를 초기화한다.

  • 인수는 순서대로 입력층의 뉴런 수, 은닉층의 뉴런 수, 출력층의 뉴런 수이다. 여기에서 가중치 매개변수도 초기화한다.

loss(self, x ,t)는 손실 함수의 값을 계산하는 메서드이다.

  • 이 메서드에서는 predict()의 결과와 정답 레이블을 바탕으로 교차 엔트로피 오차를 구하도록 구현했다.

numerical_gradient(self, x, t) 메서드는 각 매개변수의 기울기를 계산한다.

마지막 gradient(self,x,t)는 오차역전파법을 사용하여 기울기를 효율적이고 빠르게 계산한다.


4.5.2 미니배치 학습 구현

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

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개의 데이터를 추려내고 그 미니배치를 대상으로 확률적 경사 하강법을 수행매개변수를 갱신한다.

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

이 손실함수의 값이 변화하는 추이를 그래프로 나타내면 다음과 같다.

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

  • 이는 학습이 잘되고 있다는 뜻으로, 신경망의 가중치 매개변수가 서서히 데이터에 적응하고 있음을 의미한다. (신경망이 학습하고 있음)
  • 다시 말해, 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있습니다.


4.5.3 시험 데이터로 평가하기

위 그래프를 통해 손실 함수의 값이 점점 내려가는 것을 확인했는데 이 손실 함수 값이란 훈련 데이터의 미니배치에 대한 손실 함수의 값이다.

다만 이 결과만으로는 다른 데이터셋에도 비슷한 학습효과를 발휘할 지는 확신할 수 없다.

그래서 신경망 학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야 한다.

  • 즉 overfitting을 일으키지 않는지 확인해야 한다.

신경망 학습의 원래 목표는 범용적인 능력을 익히는 것이다.

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

이를 위해 다음 구현에서는 학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록한다.

💡
에폭epoch^{epoch}은 하나의 단위이다.

  • 1에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당한다.
    • ex) 훈련 데이터 10,000개를 100개의 미니배치로 학습할 경우, 확률적 경사 하강법을 100회 반복하면 모든 훈련 데이터를 '소진'한 게 된다.
    • 이 경우, 100회가 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()

이 코드를 실행시켜 얻은 그래프는 다음과 같다.

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

  • 두 정확도에는 차이가 없음을 알 수 있다.
  • 즉, 이번 학습에서는 오버피팅이 일어나지 않았다.


4.6 정리

  • 기계학습에서 사용하는 데이터셋은 훈련 데이터와 시험 데이터로 나눠 사용한다.
  • 훈련 데이터로 학습한 모델의 범용 능력을 시험 데이터로 평가한다.
  • 신경망 학습은 손실 함수를 지표로, 손실 함수의 값이 작아지는 방향으로 가중치 매개변수를 갱신한다.
  • 가중치 매개변수를 갱신할 때는 가중치 매개변수의 기울기를 이용하고, 기울어진 방향으로 가중치의 값을 갱신하는 작업을 반복한다.
  • 아주 작은 값을 주었을 때의 차분으로 미분하는 것을 수치미분이라고 한다.
  • 수치 미분을 이용해 가중치 매개변수의 기울기를 구할 수 있다.
  • 수치 미분을 이용한 계산에는 시간이 걸리지만, 그 구현은 간단하다. (오차역전파는 기울기를 고속으로 구할 수 있다.)
profile
성장과 연구하는 자세를 추구하는 AI 연구개발자

0개의 댓글