티스토리 뷰

  • 이 글은 글 하단의 Reference에 있는 강의, 교재를 보고 정리한 것입니다.

5.0 Intro

이번 장에서는 신경망 학습에 대해 다룬다. 신경망 학습은 구체적으로는 가중치 매개변수의 최적값을 학습하는 것이다. 가중치 매개변수의 최적값은 손실 함수를 기준으로 하며 이 값을 가급적 작게 만드는 방법으로 gradient descent를 소개한다.

5.1 Classification of data(데이터의 종류)

머신러닝에서는 데이터를 크게 두가지 종류로 구분한다.

  • 훈련 데이터(training data)
  • 시험 데이터(test data)

훈련 데이터는 가중치 매개변수의 값을 학습하는데 사용되는 데이터다. 또한, 모델과 훈련 데이터간 오류를 in-sample-error라 한다. 학습은 주로 이 in-sample error를 줄이는 방향으로 진행된다.

시험 데이터학습이 제대로 진행되었는지 검증하기 위해 사용되는 데이터다. 우리의 모델은 훈련 데이터를 통해 학습되었기 때문에 새로 보는 데이터들에 대해 효과적인 모델인지 알 수 없다. 예를 들어, 모델이 너무 훈련 데이터에 맞춰 있을 경우(overfitting) 새로 보는 데이터들을 잘 설명하지 못할 수 있다. 따라서 주어진 데이터 중 일부를 훈련에 사용하지 않고, 훈련 후의 모델이 새로운 데이터를 보았을 때 어느정도 정확도를 갖는지 확인해보기 위해 사용된다.

5.2 Loss function(손실 함수)

신경망에서는 학습을 위해, 현재 우리의 모델이 주어진 데이터를 얼마나 잘 설명할 수 있는지를 나타내는 지표가 필요하다. 학습은 이 지표를 좋은 방향으로 만드는 매개변수를 찾는 과정이다. 이 때 신경망 학습에서 사용하는 지표는 손실 함수(Loss function)라 하며, 이 손실 함수는 일반적으로 오차 제곱합교차 엔트로피 오차를 사용한다.

5.2.1 오차 제곱합(Sum of Squeares for Error, SSE)

가장 구현하기 쉬우며 많이 쓰이는 손실 함수는 오차 제곱합이며 수식으로는 다음과 같이 나타낼 수 있다.
$$
E = \frac{1}{2}\sum{(y_k-t_k)}^2
$$
이 때 yk신경망의 출력. tk는 실제 데이터의 정답 레이블이다. 만약 레이블이 one-hot-encoding되어있다면 y와 t는 vector간 뺄셈이 될 것이고 따라서 sigma 안의 항목은 벡터의 크기 제곱이 될 것이다.

-one-hot-encoding이란?

N개의 분류가 있는 시스템에서 어떤 분류를 크기가 1인 N차원 벡터로 나타내는 encoding이다. 예를 들어 손글씨 인식에서 0부터9까지 10개의 정답이 있는 시스템을 생각해보자. 이 때 7을 (0,0,0,0,0,0,0,1,0,0)으로 나타낼 수 있는데, 이것이 one-hot-encoding 방법이다.

5.2.2 교차 엔트로피 오차(Cross Entropy Error, CEE)

또 다른 손실 함수로서 교차 엔트로피 오차도 많이 이용되며 수식으로는 다음과 같이 나타낼 수 있다.
$$
E= -\sum{t_k\log{y_k}}
$$
예를 들어 손글씨 인식에서 정답은 '2'이고 출력은 y와 같은 데이터 하나를 생각해보자.

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]

이 경우 E는 다음과 같이 계산된다. t의 3번째 요소만 1이므로 y의 3번째 요소만 로그에 넣어 계산하면 될 것이다.
$$
E = -log(0.05)
$$
이 계산을 통해 CEE함수의 특징에 대해 생각해보면, 정답 레이블(t)에 해당하는 출력(y)이 클수록 -log안에 들어가는 값이 커지므로 E가 작아진다. 따라서 정답에 가까운 출력을 보일수록 E가 작은 특징을 가지므로 이 값을 어떤 error로 생각할 수 있게 된다.

또한 이 함수는 코드로 간단히 구현하면 다음과 같다.

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

이 때 log안에 매우 작은 값인 delta를 넣는 것은 log안의 항이 0이 될 때 log는 발산하므로 계산이 불가해져 그것을 방지하기 위해서다.

5.2.3 미니배치 학습

5.2.1과 5.2.2에서 다룬 함수들은 모두 하나의 데이터를 기준으로 한 것이었다. 그러나 학습은 훈련 데이터 안에 있는 여러개의 데이터를 보고 진행하게 되므로 손실 함수도 여러 데이터에 대해 계산되어야 한다. 따라서 훈련 데이터 모두에 대한 손실 함수의 값을 구하는 방법을 생각해보기로 하는데, 어렵지 않고 그냥 각 데이터에 대한 손실 함수를 더하는 것으로 계산한다. 예를 들어 CEE는 다음과 같은 형태로 바꿀 수 있다.

이 때, tnk는 정답 데이터의 n번째 data의 k번째 값을 의미하며 ynk도 동일하다. 그냥 덧셈으로 확장했을 뿐이며 데이터의 총 개수인 N으로 나누어 정규화 하고 있다.

그런데, MNIST 데이터셋은 훈련 데이터만 6만개였다. 이렇게 많은 데이터를 대상으로 손실 함수의 합을 구하려면 계산양이 많아진다. 이렇게 훈련 데이터가 많아서 계산이 힘들어질 경우 전체 중 데이터 일부를 랜덤하게 뽑아 전체의 '근사치'로 이용하는 방법이 있는데, 이런 학습 방법을 미니배치라 한다.

예를 들어 MNIST에서 다음과 같이 랜덤하게 100개의 데이터만 뽑을 수 있다.

import sys,os
sys.path.append(os.pardir)
import numpy as np
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) # (60000, 10)

batch_size = 100
batch_mask = np.random.choice(train_size, batch_size) # 0~train_size-1에서 batch_size개만큼 무작위로 뽑음
x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]

5.2.4 (배치용) 교차 엔트로피 오차 구현

데이터 하나당 교차 엔트로피 오차를 구하는 경우를 포함하여 다음과 같이 코드를 확장할 수 있다.

우선, 정답이 one-hot-encoding되어있는 경우는 다음과 같이 구현한다.

def cross_entropy_error(y,t): # one-hot-encoding version
    if y.ndim == 1: # data하나당 교차 엔트로피 오차를 구하는 경우
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    batch_size = y.shape[0] # Data의 개수
    return -np.sum(t*np.log(y+1e-7))/batch_size

정답인 t가 one-hot-encoding이 아니라 4,7처럼 data label로 encoding되어있는 경우는 다음과 같이 코드를 작성할 수 있다.

def cross_entropy_error2(y,t): # t 가 label인 경우
    if y.ndim==1:
        t = t.reshape(1,t.size)
        y = y.reshape(1,y.size)
    batch_size = y.shape[0]
    return -np.sum(np.log(y[np.arange(batch_size),t]+1e-7)) / batch_size

(참고로, y[np.arange(batch_size),t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출하는 부분이다.)

5.2.5 왜 손실함수가 필요한가?

왜 굳이 손실함수를 정의해야 할까? 그냥 신경망의 출력 '정확도'를 그 지표로 사용하면 왜 안될까? 이유부터 말하자면 손실 함수는 '연속적으로 변화하는 미분계수를 위한 것'이다. 우리가 gradient descent를 한 과정을 생각해보면 loss함수의 미분계수를 단서로 loss를 적게 만드는 방향으로 매개변수를 갱신하는 것이었다. 이런 과정은 정확도를 지표로 삼았을 때 불가한데, 왜냐하면 미분 값이 대부분의 장소에서 0이 되기 때문이다. 왜냐하면 정확도는 연속적인 값이 아니라 이산적인 값이기 때문에(불연속적인 값을 갖기 때문에) 정확도 함수는 step function의 꼴을 가질 것이다. 다음 <그림1>을 보면 이해될 것이다.

<그림1: step function의 꼴>

이런 함수는 대부분의 곳에서 미분계수가 0이므로 미분계수를 근거로 매개변수를 갱신하는 것이 불가하다. 그래서 연속적으로 변화하는 미분계수를 위해 손실함수를 정의하는 것이다.

5.3 Gradient(기울기)

  • 기본적인 미분의 개념과 미분법에 대해 이 글에서 다루지는 않는다. 편미분도 안다는 가정하에 다음 내용을 작성한다.

5.3.1 Gradient Descent(경사 하강법)

경사 하강법은 다음 글의 3.1.3에서도 다룬 바 있으나 간략하게 설명한다.

Gradient Descent에 대해 다룬 글: https://hezma.tistory.com/92

가중치 매개변수에 대한 Loss함수의 값을 작게 하기 위해 사용하는 방법으로 시작지점에서 local minimum을 찾아가는 방법이다. 주의해야 할 점은 global minimum이 아닌 local minimum을 찾는 알고리즘이라는 점. 알고리즘의 기본을 설명하면 다음과 같다.

  1. 현 지점에서 가중치 매개변수 w에 대한 손실함수 L의 미분 vector를 구한다.(v라 하자)
  2. 가중치 매개변수 = 가중치 매개변수 - 학습률 * v로 갱신
  3. 종료 조건을 확인한 후 루프를 돌지 나갈 지 결정

파이썬으로 코드를 작성해보면 다음과 같이 할 수 있다. numerical_gradient는 미분값을 반환하는 함수로 같이 정의한다.

import numpy as np
# gradient계산하는 함수
def numerical_gradient(f,x):
    h = 1e-4
    grad = np.zeros_like(x) # x와 형상이 같은 zero matrix생성
    for idx in range(x.size):
        temp_val = x[idx]
        x[idx] = temp_val + h
        fxh1 = f(x) # x[idx]만 +h되어 산출된 output
        x[idx] = temp_val -h
        fxh2 = f(x)# x[idx]만 -h되어 산출된 output
        grad[idx] = (fxh1-fxh2)/(2*h) # 편미분값
        x[idx] = temp_val # 복원
    return grad

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

5.3.2 Gradient in Neural Network

신경망에서 정의되는 Loss함수는 가중치 매개변수가 그 변수이므로 다음의 값이 Gradient descent에서 우리가 구할 기울기가 된다.
$$
\frac{\partial L}{ \partial w}
$$
예를 들어, 2x3의 형태를 가진 W에 대해 손실 함수가 L인 신경망은 다음과 같이 표시할 수 있다.

5.4 Implementing Learning Algorithm

이제 신경망 학습의 요소들을 차례대로 정리해보면 다음과 같다.

  1. 미니 배치 - 훈련 데이터 중 일부를 무작위로 가져온다. 이 데이터를 미니배치라 하며, 이 미니배치의 손실 함수 값을 줄이는 것을 목표로 한다.
  2. 기울기 산출 - 손실 함수의 값을 구하기 위해 각 가중치 매개변수에 대한 손실함수의 기울기 벡터를 구한다.
  3. 매개변수 갱신 - 가중치 매개변수를 기울기 방향으로 조금 갱신한다.
  4. 반복 - 1~3 반복

이 때 데이터 중 무작위의 미니 배치를 사용하므로 이 방법을 Stochastic Gradient Descent(확률적 경사 하강법)라 한다.

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

이제 MNIST에 대한 2층 신경망을 클래스로 구현하는 예제를 시작해보자. 다음은 2 layer NN class다.

import sys,os
sys.path.append(os.pardir)
import numpy as np
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):
        # initialize weights
        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)

        #FORWARD PROP
    def predict(self,x):
        W1,W2 = self.params['W1'], self.params['W2']
        b1,b2 = self.params['b1'], self.params['b2']

        z1 = np.dot(x,W1) + b1
        a1 = sigmoid(z1)
        z2 = np.dot(a1,W2) + b1
        y = softmax(z2)

        return y

        # loss function
    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) # axis는 0부터 시작.
        t = np.argmax(t,axis=1)
        #print("y: ",y)
        #print("t: ",t)
        accuracy = np.sum(y==t)/float(x.shape[0])
        return accuracy
        # 매개변수별 gradient를 다 구해줌.
    def numerical_gradient(self,x,t):
        loss_W = lambda W: self.loss(x,t)

        grads = {}
        # 각 parameter별로 gradient를 계산.
        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

클래스만 구현했고 아직 학습, 예측은 하지 않은 상태다.

5.4.2 미니배치 학습 구현하기

# implement minibatch learning
import numpy as np
from dataset.mnist import load_mnist
from two_layer_net import TwoLayerNet
import matplotlib.pyplot as plt
(x_train, t_train), (x_test,t_test) = load_mnist(normalize=True,one_hot_label=True)
# Hyper params - user defines

iters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1
tll=[] # train_loss_list
# make network
network = TwoLayerNet(input_size=784,hidden_size=50,output_size=10)
# learning
for i in range(iters_num):
    batch_mask = np.random.choice(train_size,batch_size) # chooce batch_size of random number in range(train_size-1)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    # Compute Gradient
    grad = network.gradient(x_batch,t_batch)

    # renew gradient
    for key in ('W1','b1','W2','b2'):
        network.params[key] -= learning_rate*grad[key]

    loss = network.loss(x_batch,t_batch)

    tll.append(loss)
#x = range(iters_num)
#y = tll
#plt.plot(x,y)

이 때 loss함수의 변화를 plot해보면 다음 <그림2>와 같다.

<그림2: loss함수의 변화>

5.4.3 시험 데이터로 평가하기

손실 함수의 값은 정확히는 '훈련 데이터의 미니 배치에 대한 손실함수'의 값이므로 그것이 줄었다고 해서 다른 데이터셋에도 우리의 신경망이 잘 작동하리라는 보장은 없다. 따라서 5.1에서 얘기한 것처럼 일부 데이터는 훈련에 사용하지 않고 훈련이 끝난 뒤 정확도를 평가하는데 사용한다.

1 epoch별로 훈련 데이터와 시험 데이터에 대한 정확도를 기록하는 코드는 다음과 같다.

- epoch

epoch는 단위로 1에폭은 훈련 데이터를 모두 소진했을 때의 횟수에 해당한다. 예를 들어 10000개의 데이터를 500개의 미니배치로 학습한다면 20이 epoch이 된다.

ters_num = 10000
train_size = x_train.shape[0]
batch_size = 100
learning_rate = 0.1

train_lost_list=[]
train_acc_list=[]
test_acc_list=[]

iter_per_epoch = max(train_size/batch_size,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]
    # print("come here")
    # Compute Gradient
    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_lost_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))

이 때 출력은 다음 <그림3>과 같았으며 train_acc_list와 test_acc_list를 plot해보면 <그림4>와 같았다. y1이 train, y2가 test다.

<그림3: text 출력>
<그림4: train_acc와 test_acc plot>

이 때 <그림4>를 보면 train, test data의 accuracy추이가 비슷하므로 제대로 학습이 진행된다는 것을 확인할 수 있다.

Reference: Deep learning from scratch by Saito Goki

'머신러닝' 카테고리의 다른 글

[ML] 7. Convolutional Neural Network  (0) 2020.08.22
[ML] 6. Back Propagation  (2) 2020.08.08
[ML] 4. Neural Network  (0) 2020.07.30
[ML] 3. Logistic Regression  (0) 2020.07.27
[ML] 2. Binary Classification & Linear Regression  (0) 2020.07.23
댓글