본문 바로가기

Deep Learning(강의 및 책)/밑바닥부터 시작하는 딥러닝

4. 신경망 학습

728x90

밑바닥부터 시작하는 딥러닝을 바탕으로 제작하였습니다.

 

4.1. 데이터로 학습

신경망은 데이터를 보고 학습할 수 있습니다. 데이터를 보고 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 자동으로 결정한다는 뜻입니다.

 ■ 기계학습이란?

데이터를 통해 가중치 매개변수의 최적 값을 자동으로 구하는 학습입니다. 이미지에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습해 매개변수의 최적 값을 찾습니다. 이러한 기계학습은 데이터로부터 특징을 찾아내는 역할을 기계가 하지만 이미지에 종류에 따라사 인간이 직접 이미지의 특징을 생각해내서 기계학습에 적용해야 합니다. 하지만 신경망은 이러한 과정 없이 신경망에 입력 데이터만 넣어주면 입력에 포함된 중요한 특징까지 스스로 학습을 합니다. 즉, 신경망은 모든 문제를 주어진 데이터 그대로를 입력 데이터로 활용해 end-to-end로 학습할 수 있습니다.

 

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

기계학습에서 데이터를 훈련 데이터와 시험 데이터로 나눠 학습과 실험을 수행합니다. 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾습니다. 그 다음 시험 데이터를 이용해 훈련한 모델의 실력을 평가합니다.

이러는 이유는 범용 능력(아직 보지 못한 데이터로도 문제를 올바르게 풀어내는 능력)을 제대로 평가하기 위해서입니다. 또한 구분없이 한 데이터셋으로만 학습을 해 지나치게 최적화된 상태(오버피팅, overfitting)가 될 수도 있기 때문에 훈련 데이터와 시험 데이터를 나누고 여러 데이터셋을 통해 학습을 합니다.

데이터를 훈련 데이터와 시험 데이터로 나눈 그림

 

4.2. 손실함수(loss function) 또는 비용함수(cost function)

신경망에서 하나의 지표를 기준으로 최적의 매개변수 값을 탐색하는데 신경망 학습에서 사용하는 지표는 손실함수(loss function)라고 합니다. 이 손실함수는 임의의 함수를 사용할 수도 있지만 일반적으로는 오차제곱합(SSE)과 교차 엔트로피(CEE) 오차를 사용합니다.

 ■ 오차 제곱합(SSE, Sum of Squares for Error)

가장 많이 쓰이는 오차함수로 수식은

이와 같습니다. 여기서 yk는 신경망의 출력을 의미하고 tk는 정답 레이블 또는 데이터를 의미합니다.

 

 

 

python을 통해 구현을 해보겠습니다.

import numpy as np

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

y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] # 2가 가장 높은 확률 0.6으로 예측
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 정답은 2
print(sum_squares_error(np.array(y), np.array(t)))

y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] # 7이 가장 높은 확률 0.6으로 예측
print(sum_squares_error(np.array(y), np.array(t)))

실행결과

올바르게 예측한 결과가 더 작은 오차를 보여줍니다. 즉, 첫 번째 추정 결과가 정답에 더 가깝다는 것을 의미합니다.

 

 

 ■ 교차 엔트로피 오차(CEE, Cross Entroy Error)

교차 엔트로피 오차도 손실함수로 자주 이용합니다. 수식은

이와 같습니다. 여기에서 log는 밑이 e인 자연로그 ln을 의미합니다. yk는 신경망의 출력, tk는 정답 레이블 또는 데이터를 의미합니다. 그리고 tk는 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0입니다(원-핫 인코딩). 그래서 실질적으로 정답일 때의 추정의 자연로그를 계산하는 식이 됩니다. 예를 들어 정답 레이블은 '2'가 정답이라 하고 이때의 신경망 출력이 0.6이라면 교차 엔트로피 오차는 -log0.6 = 0.51이 됩니다. 또한 같은 조건에서 신경망 출력이 0.1이라면 -log0.1 =2.30이 됩니다. 즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 됩니다.

자연로그 y = logx의 그래프

이 그래프를 봐도 x가 1일 때는 y는 0이 되고 x가 0에 가까워질수록 y의 값은 점점 작아집니다. 위에 식도 마찬가지로 정답에 해당하는 출력이 커질수록 0에 다가가다가, 그 출력이 1일 때 0이 됩니다. 반대로 정답일 때의 출력이 작아질수록 오차는 커집니다.

python을 통해 구현을 해보겠습니다.

import numpy as np

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

y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0] # 2가 가장 높은 확률 0.6으로 예측
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0] # 정답은 2
print(cross_entropy_error(np.array(y), np.array(t)))

y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0] # 7이 가장 높은 확률 0.6으로 예측
print(cross_entropy_error(np.array(y), np.array(t)))

실행결과

첫 번째 예는 정답일 때의 출력이 0.6인 경우로 오차는 약 0.51입니다. 그다음은 정답일 때의 출력이 0.1인 경우로 오차는 2.3이 됩니다. 즉 정답에 가까울수록 오차는 작아지는 것을 볼 수 있습니다.

 ■ 미니배치 학습

수많은 학습 데이터 중 정해진 일부분만 뽑아서 학습하는 것을 의미합니다. 모든 데이터를 대상으로 손실 함수의 합을 구하는 것은 시간이 오래 걸리게 됩니다. 그래서 데이터 중 일부를 뽑아 전체의 근사치로 이용해 시간을 단축시킬 수 있습니다.

식은 이와 같습니다. N개의 데이터라면 tnk는 n번째 데이터의 k번째 값을 의미합니다(ynk는 신경망의 출력, tnk는 정답 레이블입니다). 이 식은 단순히 N개의 데이터로 확장을 했을 뿐입니다. N으로 나누면서 정규화를 통해 평균 손실 함수를 구하는 식입니다. 이러한 배치 사이즈를 적용하는 교차 엔트로피 오차를 구현해보겠습니다.

 

def cross_entropy_error(y, t):
    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
    # return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size 이 식은 정답 레이블이 원-핫 인코딩이 아닌경우

이 코드에서 y는 신경망의 출력, t는 정답 레이블입니다. y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔줍니다. 그리고 배치 크기로 나눠 정규화하고 이미지 1장당 평균의 교차 엔트로피를 계산합니다.

 

 ■ 손실함수를 사용하는 이유는?

우리의 궁극적인 목적은 높은 정확도를 끌어내는 매개변수 값을 찾는 것입니다. 신경망 학습에서는 손실함수의 미분(기울기)을 계산하고, 미분 값이 0이 되는 쪽으로 매개변수 값을 갱신합니다. 만약 정확도를 지표 삼았다면 미분 값이 대부분의 장소에서 0이 되어 매개변수 값의 갱신이 올바르게 이루어지지 않습니다. 그리고 정확도는 매개변수의 미소한 변화에는 거의 반응을 보이지 않고, 반응이 있더라도 그 값이 불연속적으로 갑자기 변화합니다. 이러한 이유 때문에 우리는 손실함수를 사용합니다.

 

4.3. 수치 미분

경사법에서는 기울기값을 기준으로 나아갈 방향을 정합니다. 기울기가 무엇인지, 미분이 무엇인지에 대해서 알아보겠습니다.

 ■ 미분

미분은 '특정 순간'의 변화량을 뜻합니다. 미분의 수식은 이와 같습니다.

미분 수식

좌변은 f(x)의 x에 대한 미분(x에 대한 f(x)의 변화량)을 나타내는 기호입니다. 즉 x의 작은 변화가 함수 f(x)를 얼마나 변화시키느냐를 의미합니다. 이때 시간의 변화량 h를 한없이 0에 가깝게 합니다. 이러한 미분을 python으로 구현해보겠습니다.

def numerical_diff(f, x):
    h = 1e-50
    return (f(x+h) - f(x)) / h

이와 같이 작성할 수 있지만 개선점이 있습니다. h에 가급적 작은 값을 대입하고 싶었기에 매우 작은 값(1e-50)을 사용했는데 이는 반올림 오차 문제를 일으킵니다. 반올림 오차는 작은 값이 생략되어 최종 계산 결과에 오차가 생기게 합니다. 그래서 보통 h = 1e-4로 설정을 한다면 좋은 결과를 얻는다고 알려져 있습니다.

또 하나의 개선점이 있습니다. x+h와 x 사이의 함수 f의 차분을 계산하고 있지만 애당초 이 계산에는 오차가 있습니다. 진정한 미분은 x 위치의 함수의 기울기에 해당하지만 이번 구현에서는 x+h와 x 사이의 기울기에 해당합니다. 그래서 진정한 미분과는 일치하지 않습니다. 이를 해결하기 위해 x+h와 x-h일 때의 함수 f의 차분을 계산하는 방법도 쓰는데 이를 중심 차분 혹은 중앙 차분이라 합니다(한편, x+h와 x의 차분은 전방 차분이라 합니다). 이 두 개선점을 이용해 수치 미분을 다시 구현해보겠습니다.

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

이 함수를 사용해 수치 미분의 예를 작성해보겠습니다.

import numpy as np
import matplotlib .pylab as plt
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)")

tf = tangent_line(function_1, 5)
y2 = tf(x)

plt.plot(x, y)
plt.plot(x, y2)
plt.show()

실행결과

이와 같이 그래프가 작성됩니다. 그리고 출력 결과는 0.1999999999990898 이 나옵니다.

여기서 사용된 f(x) = 0.01x^2 + 0.1x 입니다. 이 함수를 x=5인 부분에서 미분을 해 기울기를 구한 결과가 0.1999999999990898입니다.

 

 

 

 

 

 

 

 

 

 

 

 ■ 편미분

편미분은 변수가 여럿인 함수에 대한 미분을 의미합니다. 예를 들어

이 식은 변수가 2개입니다. 이 식을 미분하기 위해서 x0에 대한 미분인지 x1에 대한 미분인지를 구별해야 합니다. 그리고 목표 변수가 아닌 다른 변수는 값을 고정합니다. 예를 들어 x0 = 3이고 x1 = 4일 때, x0에 대한 편미분을 구하면

def function_tmp1(x0):
    return x0*x0 + 4.0 **2.0
print(numerical_diff(function_tmp1, 3.0))

실행결과

이와 같이 코드를 작성할 수 있습니다. 결괏값을 보면 x0 = 3, x1 = 4일 때, x0에 대한 편미분을 구한 값이 나옵니다.

 

4.4. 기울기

이번에는 x0와 x1에 대한 편미분을 동시에 계산하는 코드를 작성하겠습니다. 존재하는 변수들에 대해 모두 편미분을 벡터로 정리한 것을 기울기라 합니다.

def _numerical_gradient(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x)
    
    for idx in range(x.size):
        tmp_val = x[idx]
        x[idx] = float(tmp_val) + h
        fxh1 = f(x) # f(x+h)
        
        x[idx] = tmp_val - h 
        fxh2 = f(x) # f(x-h)
        grad[idx] = (fxh1 - fxh2) / (2*h)
        
        x[idx] = tmp_val # 값 복원
        
    return grad

이와 같이 코드를 작성하면 편미분을 할 수 있습니다. 

이번에는 기울기라는 게 의미하는 것이 무엇인지 그림으로 표현해보겠습니다.

f(x0, x1) = x0^2 + x1^2의 기울기

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

 

 ■ 경사 하강법(SGD, Stochastic Gradient Descent)

신경망 역시 최적의 매개변수(가중치와 편향)를 학습 시에 찾아야 합니다. 여기에서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값입니다. 기울기를 이용해 최솟값(혹은 가능한 한 작은 값)을 찾는 방법이 경사 하강법입니다. 경사 하강법은 현재 위치에서 기울기를 구하고 그 기울어진 방향으로 일정 거리만큼 이동합니다. 그다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복합니다. 이렇게 해서 함수의 값을 점차 줄여나가는 방식입니다. 

경사법 수식

수식은 이와 같습니다. 가중치 매개변수에 대한 손실함수 변화량에 학습률(learning rate)를 곱해서 가중치를 갱신해줍니다. 기울기는 그저 가중치를 수정할 방향을 말해줄 뿐, 실직적으로 가중치 갱신 정도는 학습률에 비례합니다. 이번에는 경사법으로 f(x0, x1) - x0^2 + x1^2의 최솟값을 구해보겠습니다.

import numpy as np
import matplotlib.pylab as plt
from gradient_2d import numerical_gradient


def gradient_descent(f, init_x, lr=0.01, step_num=100):
    x = init_x
    x_history = []

    for i in range(step_num):
        x_history.append( x.copy() )

        grad = numerical_gradient(f, x)
        x -= lr * grad

    return x, np.array(x_history)


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

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

lr = 0.1
step_num = 20
x, x_history = gradient_descent(function_2, init_x, lr=lr, step_num=step_num)

plt.plot( [-5, 5], [0,0], '--b')
plt.plot( [0,0], [-5, 5], '--b')
plt.plot(x_history[:,0], x_history[:,1], 'o')

plt.xlim(-3.5, 3.5)
plt.ylim(-4.5, 4.5)
plt.xlabel("X0")
plt.ylabel("X1")
plt.show()

실행결과

이와 같이 경사 하강법으로 최솟값을 구할 수 있습니다. 만약 학습률이 너무 크거나 작으면 값은 최솟값을 구하지 못하거나 발산해버리는 결과를 얻게 됩니다. 즉 학습에서 학습률을 적절하게 설정하는 것이 매우 중요합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 ■ 신경망에서의 기울기

이번에는 신경망 학습에서 기울기를 구해보겠습니다.

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

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)

실행결과

실행결과로 dW를 구했습니다. 코드에 대해서 하나하나 차근히 보겠습니다. simpleNet을 통해 값을 예측하는 함수와 오차를 구하는 함수를 정의했습니다. 그리고 input으로 x = [0.6, 0.9]로 정해주고 정답 레이블을 t = [0, 0, 1]로 정해줬습니다. numerical_gradient에 f와 net.W를 넣어 오차에 대한 편미분들을 진행했습니다. dW[0][0] 값이 의미하는 것은 W[0][0]을 h만큼 늘리면 손실 함수의 값은 0.15525h 만큼 증가한다는 것을 의미합니다. 즉 손실함수를 줄인다는 관점에서 W[0][0]의 값을 음의 방향으로 이동해야 합니다.

 

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

신경망 학습에 관한 절차를 보겠습니다.

  • 전제: 신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 학습이라 합니다. 신경망 학습은 다음과 같이 4단계로 수행합니다.
  1. 미니배치: 훈련 데이터 중 일부를 무작위로 가져옵니다. 이렇게 선별한 데이터를 미니배치라 하며, 그 미니배치의 손실 함숫값을 줄이는 것이 목표입니다.
  2. 기울기 산출: 미니배치의 손실 함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구합니다. 기울기는 손실 함수의 값을 가장 작게 하는 방향을 제시합니다.
  3. 매개변수 갱신: 가중치 매개변수를 기울기 방향으로 아주 조금 갱신합니다.
  4. 반복: 1~3단계를 반복합니다.

이제 MNIST 데이터셋을 이용해 학습을 수행하겠습니다.

 

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

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, 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

params 변수에는 가중치 매개변수가 저장되는데 예를 들어 params['W1']은 1번째 층의 가중치 매개변수가 저장됩니다. accuracy함수에서는 정확도를 계산해줍니다. numerical_gradient 함수를 통해 loss를 구하고 각각 매개변수의 기울기를 grads변수에 저장합니다.

 

 ■ 미니배치 학습 구현하기

훈련 데이터 중 일부를 무작위로 꺼내고(미니배치), 그 미니배치에 대해서 경사 하강법으로 매개변수를 갱신하는 코드를 구현하겠습니다.

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()

실행결과

코드에 대해서 살펴보겠습니다. train에서 쓸 데이터와 test에서 쓸 데이터를 일단 받아줍니다. 총 반복횟수를 10000회로 정하고 미니배치 크기를 100으로 설정을 합니다. 그리고 학습률(learning rate)는 0.1로 정했습니다. 반복문 안에 batch_mask에는 미니배치를 설정한 만큼 100개의 값을 랜덤하게 받아주고 이를 x_train와 t_train에서 가져옵니다. grad를 통해 기울기를 구하고 반복문을 통해 매개변수들을 갱신합니다. 작게 할 값은 작게, 크게 할 값은 크게 매개변수들을 갱신합니다. 그다음 600회마다 test데이터를 이용해 정확도를 측정합니다. 그 결과를 그리면 위와 같은 그래프가 생성됩니다.

'Deep Learning(강의 및 책) > 밑바닥부터 시작하는 딥러닝' 카테고리의 다른 글

6. 학습 관련 기술들  (0) 2022.02.15
5. 오차역전파법  (0) 2022.02.14
3. 신경망  (0) 2022.02.07
2. 퍼셉트론(perceptron)  (0) 2022.02.06
1. 파이썬(Python) 기초  (0) 2022.02.06