본문 바로가기
데이터 청년 캠퍼스/연관분석, 인공신경망

[인공신경망] 신경망 학습 - 손실함수, 미니배치, 경사하강법

by 뚱뚜루뚱 2022. 7. 18.
데이터 주도 학습
  • 사람이 이미지의 특성을 고안하여 규칙 정의
  • 이미지에서 특징을 추출하고 특징의 패턴을 머신러닝 기술로 학습. 단, 사람이 특징 추출
  • 이미지에 포함된 중요한 특징까지도 '기계'가 스스로 학습

기계학습에서는 데이터를 훈련 데이터 Training Data와 시험 데이터 Test Data로 나누어 학습과 실험을 수행한다. 훈련 데이터만 사용해 학습하면서 최적의 매개변수를 찾은 뒤, 시험 데이터를 사용해 앞서 훈련한 모델의 실력을 평가한다. 이는 한 데이터 셋에만 지나치게 최적화 된 오버피팅을 피하고 범용 능력을 제대로 평가하기 위함이다. 이 외의 하이퍼 파라미터를 선택하기 위한 검증 데이터 Validation Data를 추가로 나누기도 한다.

 

 

 

손실 함수 Loss Function
  • 오차제곱합 SSE, Sum of Squared Error: 추정 값이 정답과 거리가 멀수록 값이 커진다

E = 1/2 sigma(y - t)^2  (y: 신경망이 추정한 값, t: 정답)

import numpy as np

def sum_squared_error(y, t) :
    return 0.5 * np.sum((y-t)**2)
    
    
# 예시 데이터
y = np.array ([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]) #정답
y1 =np.array ([0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]) #오답
t = np.array ([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

sum_squared_error (y, t)
sum_squared_error (y1, t)

 

  • 교차 엔트로피 오차 CEE, Cross Entropy Error: 정답 레이블(t = 1) y의 자연로그 계산식. y가 커질수록 0에 가까워지며, y가 작아질수록 오차가 커진다. 즉, 추정 값이 정답과 거리가 멀수록 값이 커진다

E = - sigma[ t * log(y) ]   (t는 one_hot_encoding)

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


# 예시 데이터
y = np.array ([0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]) #정답
y1 =np.array ([0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]) #오답
t = np.array ([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])

cross_entropy_error (y,t)
cross_entropy_error (y1, t)

 

 

 

미니배치 학습

기계학습은 훈련 데이터에 대한 손실 함수 값을 구하고 그 값을 최소로 만드는 매개변수를 찾는 과정이다. 이론적으로는 모든 훈련 데이터를 대상으로 그 손실 함수 값을 구한 뒤, 손실 함수 값들의 합을 지표 삼아 평균을 구한다.

 

E = - 1/N sigma ( sigma [ t log(y) ] )

 

전체 훈련 데이터에 대해 손실함수를 구하는 것은 거대한 값이 되므로, 신경망 학습에서는 훈련데이터의 일부만 무작위로 선택해 학습한다. 그 일부를 미니 배치 Mini Batch라고 한다. 

 

1. 필요한 데이터(MNIST) 불러오기

from google.colab import files
src = list(files.upload().values())[0]

from mnist import load_mnist

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

print (x_train.shape)
print (t_train.shape)

 

2. 미니배치 만들고 그에 따라 선택된 데이터 출력하기

train_size = x_train.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size, batch_size)
print ('batch_mask :', batch_mask) 

x_batch = x_train[batch_mask]
t_batch = t_train[batch_mask]
print (t_batch)

 

3. 배치용 CEE 구현하기

# One Hot Encoding
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
    

# Label
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]
    #2차원 fancy indexing
    return -np.sum(np.log(y[np.arange(batch_size), t] + 1e-7)) / batch_size
    
    
# 마지막줄 인덱싱 관련 부연설명
y = np.array([[10,11,12,13,] ,
              [20,21,22,23,] ] )
t = np.array ([2, 1] )

print(np.arange(2))
print([np.arange(2),t])
print ('y[np.arange(2), t]) --> y[[0,1],[2,1]]')
print (y[np.arange(2), t])

 

 

 

손실함수를 설정하는 이유

 지표를 정확도로 설정할 경우 최대값, 손실함수로 설정할 경우 최소값을 찾는다.

 정확도는 대부분의 장소에서 미분값이 0이 되어버린다. 또한 매개변수의 미세한 변화에 거의 반응하지 않고, 반응하는 경우 그 값이 불연속적으로 갑자기 변화한다. 이는 계단함수를 활성화함수로 사용하지 않는 이유와 동일하다.

 

1. 수치 미분 함수: 아주 작은 차분으로 미분

# 컴퓨터는 이와 같은 작은 값을 잘 처리하지 못한다
def numerical_diff(f,x) :
    h = 1e-50
    return (f(x+h) - f(x)) / h 
    
def numerical_diff(f,x) :
    h = 1e-4    # 적절한 크기의 수치
    return  (f(x+h) - f(x-h)) / 2*h   # 지점 앞 뒤의 값
    
    
# 함수 생성    
def function_1 (x):
    return 0.01*x**2 + 0.1*x #^2로 쓰는 경우 오류 발생
    
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)

plt.title ('$y=0.01x^2+0.1x$')
plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()


# 수치미분과 해석에 의한 미분 비교
numerical_diff(function_1, 5)
numerical_diff(function_1, 10)

 

2. 편미분: 특정한 변수를 기준으로 수행하는 미분

def function_2(x):
    return x[0]**2 + x[1]**2   
    
    
# 여러 위치에서의 함수 값 구하기: (1,1) (0,0) (1,0) (3,4)
x = np.array([ [ 1,0,1,3],[1,0,0,4]])
function_2(x)


# 다변량 함수의 그래프
x = np.arange(-3,3,0.1)
y = np.arange(-3,3,0.1)
X,Y = np.meshgrid(x,y)

Z = function_2(np.array([X,Y]))
Z

fig = plt.figure()
ax = plt.axes(projection = '3d')
ax.plot_wireframe(X,Y,Z, color = 'black') 
plt.show()

 

3. 기울기

# 수치 미분을 이용한 기울기
def numerical_gradient(f, x) :
    h = 1e-4
    grad = np.zeros_like(x) # X와 같은모양 배열
    
    for idx in range(x.size):  # 변수 마다 값을 약간씩 변경하여 미분 구하기
        # f(x+h)
        tmp_val = x[idx]
        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
    
# 주의: 정수가 아닌 실수를 입력해야함    
numerical_gradient (function_2, np.array([3.0, 4.0]))

 

 

 

경사하강법

 손실함수가 최소가 되는 매개변수를 기울기를 이용하여 찾는 방법. 현 위치에서 기울기를 구하고, 기울어진 방향으로 일정거리만큼 이동한 뒤, 다시 기울기를 구하고, 이동하기를 반복하며 함수 값을 줄여간다. 한 번의 학습으로 얼마나 학습할지 이동량을 정하고, 이동한 자리에서 동일 과정을 반복하며 서서히 함수 값을 줄여나간다. 변수의 개수가 늘어나도 각 변수의 편미분 값으로 구성된 기울기로 동일하게 진행한다.

 기울기가 가리키는 곳이 항상 최소값인지 보장할 수 없다는 단점이 있다. 경사하강법은 함수의 극소값, 안장점 Saddie Ploint에서 기울기가 0이 된다. 함수가 왜곡되고 복잡한 경우 고원 Plateau에서 학습이 진행되지 않는 정체기에 빠질 수 있다.

 

def gradient_descent(f, init_x, lr = 0.01, step_num = 1000) :
    x = init_x
    
    for i in range(step_num):
        grad = numerical_gradient(f,x)
        x -=  lr * grad
    
    return x
    
def function_2(x):
    return x[0]**2 + x[1]**2

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

gradient_descent(function_2, init_x, 0.01) # array([-5.04890207e-09,  6.73186943e-09])


# 학습률이 너무 큰 경우: 발산
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x, 10)

# 학습률이 너무 작은 경우: 이동 적음
init_x = np.array([-3.0, 4.0])
gradient_descent(function_2, init_x, 1e-10)

 

 

 

신경망 기울기

가중치 매개변수에 대한 손실함수의 기울기

1. 신경망에서 각 층의 뉴런에 대한 가중치를 저장하고, 신호를 전달하여 손실함수 값을 구하는 simpleNet 클래스 구현

import numpy as np
from functions import softmax, cross_entropy_error
from gradient import numerical_gradient

class simpleNet :
    def __init__ (self) :      # 가중치
        self.W = np.random.randn(2,3)
        
    def predict (self, x) :    # 신경망 신호전달 forward
        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

 

2. simpleNet 객체를 이용해여 예측값과 손실함수 확인

# SimpleNet의 가중치 
net = simpleNet()
print (net.W)

# SimpleNet의 예측값 
x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)

# SimpleNet의 예측 결과 
np.argmax(p)

# 가상의 정답 t 로 손실함수 구하기
t = np.array([0,0,1])
net.loss(x, t)

# 손실함수를 미분 대상 함수로 정의  
def f(W) :
    return net.loss(x, t)

 

3. numerical_gradient( )에 손실함수 전달하여 기울기 확인

dW = numerical_gradient (f, net.W)
print(dW)

 

 

 

실습: 학습 알고리즘 구현하기

신경항 학습 절차

  1. 미니배치: 훈련 데이터 중 일부를 무작위로 가져와 선별한 데이터를 미니배치라고 한다. 해당 미니배치의 손실함수 값을 줄이는 것을 목표이다
  2. 기울기 계산: 미니배치 손실함수 값을 줄이기 위해 각 가중치 매개변수의 기울기를 구한다. 이때 기울기는 손실함수의 값을 가장 작게 하는 방향으로 제시한다
  3. 매개변수 갱신: 가중치 매개변수를 기울기 방향으로 아주 조금 갱신한다
  4. 반복: 1 ~ 3단계를 반복한다

1. 2층 신경망 TwoLayerNet 구현

import sys, os
from functions import sigmoid
from 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
       
    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['b2'])
        
        return grads
         
    def gradient(self, x, t):
        W1, W2 = self.params['W1'], self.params['W2']
        b1, b2 = self.params['b1'], self.params['b2']
        grads = {}
        
        batch_num = x.shape[0]
        
        # forward
        a1 = np.dot(x, W1) + b1
        z1 = sigmoid(a1)
        a2 = np.dot(z1, W2) + b2
        y = softmax(a2)
        
        # backward
        dy = (y - t) / batch_num
        grads['W2'] = np.dot(z1.T, dy)
        grads['b2'] = np.sum(dy, axis=0)
        
        dz1 = np.dot(dy, W2.T)
        da1 = sigmoid_grad(a1) * dz1
        grads['W1'] = np.dot(x.T, da1)
        grads['b1'] = np.sum(da1, axis=0)

        return grads

 

2. TwoLayerNet 구조 살펴보기

# TwoLayerNet의 가중치 편향
net = TwoLayerNet (input_size = 784, hidden_size = 100, output_size = 10)
net.params['W1'].shape  # (784,100)
net.params['b1'].shape  # (100,)
net.params['W1'].shape  # (100,10)
net.params['W1'].shape  # (10,)

# 임의의 20 쌍의 입력데이터 정답레이블
x = np.random.rand(20,784) # 더미 입력데이터
t = np.random.rand(20,10)  # 더미 레이블

# 임의의 입력데이터 20개에 대한 예측 수행
y_pred = net.predict(x)
y_pred[-2:]

# 임의의 입력데이터/정답 20개에 대한 손실함수 구하기
y_loss = net.loss(x, t)
y_loss

# 임의의 20 쌍의 입력데이터 정답레이블에 대한 기울기 구하기
grads = net.numerical_gradient (x, t)  

# 20건에 대해서 한 번 미분 수행한는데 1분 이상 소요
print (grads['W1'].shape)  # (784,100)
print (grads['b1'].shape)  # (100,)
print (grads['W1'].shape)  # (100,10)
print (grads['W1'].shape)  # (10,)

 

3. 미니배치 학습 구현

from mnist import load_mnist

(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):
    # 1단계 미니배치
    batch_mask = np.random.choice(train_size, batch_size)
    x_batch = x_train[batch_mask]
    t_batch = t_train[batch_mask]
    
    # 2단계 기울기 계산: 기초 grad = network.numerical_gradient(x_batch, t_batch)
    grad = network.gradient(x_batch, t_batch)
    
    # 3단계 매개변수 갱신
    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)

 

3. 학습 중 손실함수 변화 그래프

import matplotlib.pyplot as plt

fig,ax = plt.subplots(1,2, sharey=True, figsize=(8,4))
x = np.arange(len(train_loss_list))
ax[0].plot(x, train_loss_list)
ax[0].set_title('10,000 iter')
ax[1].plot(x[:1000], train_loss_list[:1000]) 
ax[1].set_title('1,000 iter')
fig.suptitle ('loss @ iteration')

for axe in ax.flat:
  axe.set_xlabel('iteration')
  axe.set_ylabel('loss')
  
plt.show()

 

4. test 데이터로 평가하면서 학습하기

from mnist import load_mnist


(x_train, t_train), (x_test, t_test) = load_mnist(normalize=True, one_hot_label=True)
train_loss_list = []
train_acc_list = []
test_acc_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)
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.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 epoch 마다 정확도 계산 및 진행경과 출력
    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(f'train_acc={train_acc:.4f},', f' test_acc={test_acc:.4f}')


# 그래프 
import matplotlib.pyplot as plt

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.title ('Accuracy over iteration')
plt.xlabel("epochs")
plt.ylabel("accuracy")
plt.ylim(0, 1.0)
plt.legend(loc='lower right')
plt.show()