이번장은 신경망 학습이다.

 

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

 

신경망을 학습할 수 있도록 해주는 지표인 손실 함수를 알아보자.

 

4.1.0 데이터에서 학습한다

신경망의 특징은 데이터를 보고 학습할 수 있다는 것이다. 즉, 데이터에서 학습한다는 것은 가중치 매개변수의 값을 데이터를 보고 자동으로 결정한다는 의미이다. 

 

4.1.1 데이터 주도 학습

Machine Learning은 데이터가 생명이다. 데이터에서 답을 찾고 데이터에서 패턴을 발견하고 데이터로 이야기를 만드는, 그것이 바로 기계학습이다. 인간과 기계학습의 차이점을 본다면, 인간은 경험과 직관을 단서로 시행착오를 거듭하여 일을 진행한다. 이에 반해 기계학습은 사람의 개입을 최소화하고 수집한 데이터로부터 데이터를 찾으려 시도한다. 

우리가 흔히 아는 MNIST를 본다면, 이미지에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다. 이와 같이 기계학습에서는 모아진 데이터로부터 규칙을 찾아내는 역할을 '기계'가 담당한다. 하지만, 이미지를 벡터로 변환할 때 사용하는 특징은 여전히 '사람'이 설계한다.

 

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

기계학습에서는 문제를 다룰 때 데이터를 훈련데이터(Train Data)와 시험데이터(Test Data)로 나눠 학습과 실험을 수행하는 것이 일반적이다. 

1. 우선 훈련 데이터만 사용하여 학습하면서 최적의 매개변수를 찾는다.

2. 그 후, 시험 데이터를 사용하여 앞서 훈련한 모델의 실력을 평가한다.

이렇게 나누는 이유는 뭘까?

우리가 설계하는 이유는 갖고 있는 데이터를 통해서만 사용하는 것이 아닌, 범용적으로 사용하기 위한 모델을 만들기 위해서이다. 다시말해, 아직 보지 못한 데이터(훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력이다.

또한, 수중의 데이터셋은 제대로 맞히더라도 다른 데이터셋에는 엉망인 일도 벌어진다. 이와 관련해, 한 데이터셋에만 지나치게 최적화된 상태를 오버피팅(overfitting)이라고 한다.

오버피팅(overfitting): 특정 데이터셋에만 너무 많이 들어서 편견이 생겨버린 상태로 이해하자. 과적합, 과대적합, 과학습 등 다양하게 번역되어있다.

 

4.2 손실함수

인간이 예를 들어 행복의 지수를 수치로 파악하고, 이를 근거로 '최적의 인생'을 탐색하듯, 신경망도 '하나의 지표'를 기준으로 최적의 매개변수 값을 탐색한다. 신경망 학습에서 사용하는 지표는 손실함수(loss function)이라고 한다.

이 손실 함수는 임의의 함수를 사용할 수도 있지만, 일반적으로 평균 제곱 오차(MSE)와 교차 엔트로피 (Cross entropy error)를 사용한다.

 

4.2.1 평균 제곱 오차(Mean Squared Error)

가장 많이 쓰이는 손실 함수는 평균 제곱 오차이다. 

 

평균 제곱 오차 정의

여기서 yi는 신경망의 출력(신경망이 추정한 값), ti는 정답 레이블, i는 데이터의 차원수를 나타낸다.  코딩의 예를 통해 살펴보자. 

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

#예1: '2'일 확률이 가장 높다고 추정함(0.6)
y1=[.1,.05,.6,.0,.05,.1,.0,.1,.0,.0]
print(mean_squared_error(np.array(y1),np.array(t)))

#예2: '7'일 확률이 가장 높다고 추정함(0.6)
y2=[.1,.05,.1,.0,.05,.1,.0,.6,.0,.0,]
print(mean_squared_error(np.array(y2),np.array(t)))

0.09750000000000003
0.5975

첫 번째 예는 정답이 '2'고 신경망의 출력도 '2'에서 가장 높은 경우다.

두 번째 예는 정답은 똑같이 '2'지만, 신경망의 출력은 '7'에서 가장 높다. 위 실험의 결과로 첫 번째 손실함수 쪽 출력이 작으며 정답레이블과의 오차도 작은 것을 알 수 있다. 즉, 평균 제곱 오차를 기준으로는 첫 번째 추정 결과가 정답에 더 가까운 것으로 판단 할 수 있다.

4.2.2 교차 엔트로피(Cross Entropy Error)

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

교차 엔트로피 오차 정의

log는 밑이 e인 자연로그, yk는 신경망의 출력, tk는 정답 레이블이다. tk는 정답에 해당하는 인덱스의 원소만 1이고 나머지는 0이다(원-핫 인코딩). 코딩의 예를 통해 살펴보자.

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

t=[0,0,1,0,0,0,0,0,0,0]
y1=[.1,.05,.6,.0,.05,.1,.0,.1,.0,.0]
y2=[.1,.05,.1,.0,.05,.1,.0,.6,.0,.0]
print(cross_entropy_error(np.array(y1),np.array(t)))
print(cross_entropy_error(np.array(y2),np.array(t)))

0.510825457099338
2.302584092994546

첫 번째 예는 정답일 때의 출력이 .6인 경우로, 이때의 교차 엔트로피 오차는 약 .51이다. 

두 번째 예는 정답일 때의 출력이 .1인 경우로, 이때의 교차 엔트ㄹ로피 오차는 무려 2.3이다.

즉, 결과(오차)가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로 앞서 MSE와 일치하다.

 

4.2.3 미니배치 학습

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

앞선 내용에서는 데이터 하나에 대한 손실 함수만 생각했으니, 이제 훈련 데이터 모두에 대한 손실 함수의 합을 구하는 방법을 보자.

교차 엔트로피 공식은 다음과 같다.

교차엔트뢰 오차-배치 정의

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

앞선 한 개의 대한 CEE에서 단순히 N개의 데이터로 확장했을 뿐이다. 다만, 마지막에 N으로 나누어 정규화하고 있다.

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

생각해보자. 실제 빅데이터를 다루게 된다면 수천만도 넘는 거대한 데이터를 다루게 될텐데 과연 이를 일일이 손실함수를 계산하는게 현실적인가? 아니라고 본다. 그러므로 일부 데이터를 Random sampling을 통해 수집하고 학습하는게 더 효율적일 것이다. 이러한 학습 방법을 미니배치 학습이라고 한다.  코딩의 예를 통해 살펴보자.

from keras.datasets import mnist
(X_train,t_train),(X_test,t_test)=mnist.load_data()

Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/mnist.npz
11493376/11490434 [==============================] - 1s 0us/step

print(X_train.shape)
print(t_train.shape)
(60000, 28, 28)
(60000,)

여기서 mnist 데이터셋을 불러오는 방법은 keras,tensorflow등 다양하니 참고하길 바란다.

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(600000,10)
array([ 70533, 465580, 501527, 363874, 118036, 136283,  57982, 367151,
       514364, 529300])

훈련 데이터는 60000개고, 입력 데이터는 28*28인 이미지 데이터임을 알 수 있다. np.random.choice()로는 지정한 범위의 수 중에서 random sampling을 할 수 있다. 예를 들면, np.random.choice(60000,10)은 0 이상 60000미만의 수 중에서 무작위로 10개를 골라낸다는 의미이다.

 

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

#정답 레이블이 원-핫 인코딩일 경우
def input_by_onehot_cross_entropy_error(y,t):
    if y.dim==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))/batch_size

#정답 레이블이 '2'나 '7' 처럼 숫자 레이블일 경우
def input_by_shape_cross_entropy_error(y,t):
    if y.dim==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]))/batch_size

이 구현에서 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이므로, 그 계산은 무시해도 좋다는 것이 핵심이다. 다시 말해, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피의 오차를 계산할 수 있다.

 

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

앞선 내용에서는 왜 손실 함수를 설정하는가에 대한 상세한 설명이 없었다.

그렇다면 왜 굳이 손실 함수를 사용해야할까?

이를 위해서는 우리가 기계학습을 하는 궁극적 목표를 생각해봐야 한다. 우리의 목적은 높은 '정확도'를 끌어내는 매개변수 값을 찾는 것이다.

하지만 '정확도'라는 질표를 놔두고 '손실 함수의 값'이라는 우회적인 방법을 선택하는 이유는 무엇일까?

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

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

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

미분 값이 0이면 가중치 매개변수를 어느 쪽으로 움직여도 손실 함수의 값은 달라지지 않는다. 그래서 그 가중치 매개변수의 갱신은 거기서 멈춘다. 

즉, 이말은 미분 값이 대부분이 장소에서 0이 되어 매개변수를 갱신할 수 없기 때문이다.

정리하자면,

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

정확도는 매개변수의 미소한 변화에는 거의 반을을 보이지 않고, 반응이 있더라고 그 값이 불연속적으로 갑자기 변화한다. 이는 '계단 함수'를 활성화 함수로 사용하지 않는 이유와도 들어맞는다. 반대로 sigmoid 함수를 생각해보자. 시그모이드 함수는 출력(세로축의 값)이 연속적으로 변하고 곡선의 기울기도 연속적으로 변한다. 즉, 시그모이드 함수의 미분은 어느 장소라도 0이 되지는 않는다는 의미이다. 

시그모이드 함수 그래프

4.3,4.4 수치 미분, 기울기

미분과 관련하여는 자세하게 설명하지 않겠다. 본인은 수리통계학을 공부하기에 이는 따로 수리통계학에 정리하여 올리도록 하겠다.

대략 미분, 편미분,기울기,경사하강법,신경망에서의 기울기 등이 있는데 이는 생략한다.

 

하이퍼파라미터

학습률 같은 매개변수를 하이퍼파라미터(hyper parameter,초매개변수)라고 한다. 이는 가중치와 편향 같은 신경망의 매개변수와는 성질이 다른 매개변수이다. 신경망의 가중치 매개변수는 훈련 데이터와 학습 알고리즘에 의해서 '자동'으로 획득되는 매개변수인 반면, 학습률 같은 하이퍼파라미터는 사람이 직접 설정해야하는 매개변수인 것이다. 일반적으로 이들 하이퍼파라미터는 여러 후보 값 중에서 시험을 통해 가장 장 학습하는 값을 찾는 과정을 거쳐야 한다.

이와 관련하여 CNN Hyperparameter Optimization Based on CNN Visualization and Perception Hash Algorithm 논문을 조만간 리뷰하도록 하겠다.

 

4.5 학습 알고리즘 구현하기

Given

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

Stage1.-Mini Batch

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

Stage2.-Calculating Slope

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

Stage3.-Renewing Parameter

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

Stage4.-Repetition

Stage1~Stage3를 반복한다.

 

이것이 신경망 학습이 이뤄지는 순서이다. 이는 경사 하강법으로 매개변수를 갱신하는 방법이며, 이때 데이터를 미니배치로 무작위로 선정하기 때문에 확률적 경사 하강법(Stochastic Gradient descent, SGD)라고 부른다. 

즉, '확률적으로 무작위로 골라낸 데이터'에 대해 수행하는 경사 하강법이라는 의미이다. 대부분의 딥러닝 프레임워크는 확률적 경사 하강법의 영어 머리글자를 딴 SGD라는 함수로 이 기능을 구현한다.

 

4.5.1,2 전체 코딩 

2-Layer Net

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
    
    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
    
    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['b1'])
        
        return grads
    
    
    

미니배치 학습 구현하기

 

(x_train,t_train),(x_test,t_test)=mnist.load_data()

train_loss_list=[]

iters_num=10000
train_size=x_train.shape[0]
batch_size=100
learning_rate=.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)
    
    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)

 

+ Recent posts