오답노트

[밑딥] 신경망 학습 본문

Python/DL

[밑딥] 신경망 학습

권멋져 2023. 2. 27. 21:32

학습이란 훈련 데이터로부터 가중치 매개변수의 최적값을 자동으로 획득하는 것이다. 신경망이 학습할 수 있도록 해주는 지표는 손실 함수다. 손실 함수의 결과값을 가장 작게 만드는 가중치 매개변수를 찾는 것이 학습의 목표다.

 

데이터에서 학습한다

데이터 주도 학습

기계학습에서는 사람의 개입을 최소화하고 수집한 데이터로부터 패턴을 찾으려 시도한다. 데이터에서 특징을 추출하고 그 특징의 패턴을 기계학습 기술로 학습하는 방법이 있다. 여기서 말하는 특징은 입력 데이터에서 본질적인 데이터를 정확하게 추출할 수 있도록 설계된 변환기를 가리킨다.

하지만 문제에 적합한 특징을 쓰지 않으면 혹은 특징을 설계하지 않으면 좀 처럼 좋은 결과를 얻을 수 없다는 뜻이다.

신경망의 이점은 모든 문제를 같은 맥락에서 풀 수 있다. 세부사항과 관계없이 신경망은 주어진 데이털르 온전히 학습하고, 주어진 문제의 패턴을 발견하려 시도한다. 즉, 신경망은 모든 문제를 주어진 데이터 그대로를 입력 데이터로 활용해 'end-to-end'로 학습할 수 있다.

 

훈련 데이터와 시험 데이터

기계학습 문제는 데이터를 훈련 데이터와 시험 데이터로 나눠 학습과 실험을 수행하는 것이 일반적이다. 왜냐하면 우리가 원하는 것은 범용적으로 사용할 수 있는 모델이기 때문이다. 이 범용 능력을 제대로 평가하기 위해 훈련 데이터와 시험 데이터를 분리하는 것이다.

범용 능력은 아직 보지 못한 데이터 (훈련 데이터에 포함되지 않는 데이터)로도 문제를 올바르게 풀어내는 능력이다. 한 데이터셋에만 지나치게 최적화된 상태를 오버피팅(과적합)이라고 한다. 오버피팅 피하기는 기계학습의 중요한 과제이기도 하다.

 

손실 함수

신경망 학습에서 사용하는 지표는 손실 함수이다. 이 손실함수는 임의의 함수를 사용할 수도 있지만 평균적으로는 평균 제곱 오차와 교차 엔트로피 오차를 사용한다.

 

평균 제곱 오차 (Mean Squared Error, MSE)

여기서 yk는 신경망의 출력, tk는 정답 레이블, k는 데이터의 차원수를 나타낸다.

 

import numpy as np

def mean_squared_error(y, t):
  return 0.5 * np.sum((y-t)**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]

print(mean_squared_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]

print(mean_squared_error(np.array(y),np.array(t)))

위 결과를 통해 평균 제곱 오차를 기준으로 정답과 얼마나 거리가 있는지 알 수 있다.

 

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

오차가 클 수록 값이 커지고 오차가 작을 수록 값이 작아진다.

 

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]
y = [0.1,0.05,0.6,0.0,0.05,0.1,0.0,0.1,0.0,0.0]

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]

print(cross_entropy_error(np.array(y),np.array(t)))

미니배치 학습

훈련 데이터에 대한 손실 함수의 값을 구하고, 그 값을 최대한 줄여주는 매개변수를 찾아낸다. 즉, 훈련데이터가 100개 있으면 그로부터 계산한 100개의 손실 함수 값들의 합을 지표로 삼는 것이다.

평균을 구해 사용하면 훈련 데이터 개수와 관계없이 언제든 통일된 지표를 얻을 수 있다.

 

from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', as_frame=False, cache=False)

X = mnist.data.astype('float32')
y = mnist.target.astype('int64')

train_size = X.shape[0]
batch_size = 10
batch_mask = np.random.choice(train_size,batch_size)
x_batch = X[batch_mask]
y_batch = y[batch_mask]

 

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

def cross_entropy_error(y,t):
  if y.ndim == 1:
    t = t.reshape(1,t.size)
    y = t.reshape(1,y.size)
  
  batch_size = y.shape[0]
  return -np.sum(np.log(y[np.arange(batch_size),t] +  1e-7)) / batch_size

 

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

손실 함수를 사용해야하는 궁극적인 목적은 높은 '정확도'를 끌어내는 매개변수 값을 찾는 것이다. 신경망 학습에서는 최적의 매개변수 (가중치와 편향)를 탐색할 때 손실 함수의 값을 가능한 한 작게 하는 매개변수 값을 찾는다. 이때 매개변수의 미분을 계산하고, 그 미분 값을 단서로 매개변수의 값을 서서히 갱신하는 과정을 반복한다.

가중치 매개변수의 손실 함수의 미분이란 '가중치 매개변수의 값을 아주 조금 변화시켰을 때, 손실 함수가 어덯게 변하나' 라는 의미다. 그러나 미분 값이 0이면 가중치 매개변수를 어느쪽으로 움직여도 손실 함수의 값은 달라지지 않는다. 그래서 그 가중치 매개변수의 갱신은 거기서 멈춘다.

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

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

 

 

수치 미분

미분

미분은 한순간의 변화량을 표시하는 것이다.

# 나쁜 구현 예
def numerical_diff(f, x):
  h = 10e-50
  return (f(x+h) - f(x)) / h

# h 는 반올림 오차 문제를 일으킨다.

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

 

기울기

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

def numerical_gradient(f, x):
  h = 1e-4
  grad = np.zeros_like(x)

  for idx in range(x.size):
    tmp_val = x[idx]
    x[idx] = tmp_val + h
    fxh1 = f(x)

    x[idx] = tmp_val - h
    fxh2 = f(x)

    grad[idx] = (fxh1 - fxh2) / (2*h)
    x[idx] = tmp_val
  
  return grad
def function_2(x):
  return x[0]**2 + x[1]**2


numerical_gradient(function_2, np.array([3.0, 4.0]))

기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값을 가장 크게 줄이는 방향

 

경사법 (경사 하강법)

매개변수 공간이 광대하여 어디가 최솟값이 되는 곳인지 짐작할 수 없을 때 기울기를 잘 이용해 함수의 최솟값을 찾으려는 것이 경사법이다.

경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동한다. 그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 나아가기를 반복한다. 이렇게 해서 함수의 값을 점차 줄이는 것이 경사법이다.

η(에타)는 갱신하는 양을 나타낸다. 이를 신경망 학습에서는 학습률이라고 한다. 한 번의 학습으로 얼마만큼 학습해야 할지, 즉 매개변수 값을 어마나 갱신하느냐를 정하는 것이 학습률이다.

 

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

  for i in range(step_num):
    grad = numerical_gradient(f,x)
    x -= lr * grad
  
  return x
  
init_x = np.array([-3.0,4.0])
print(gradient_descent(function_2,init_x,0.1,100))

#학습률이 너무 큰 예 : lr = 10.0
init_x = np.array([-3.0,4.0])
print(gradient_descent(function_2,init_x,10.0,100))

#학습률이 너무 작은 예 : lr = 1e-10
init_x = np.array([-3.0,4.0])
print(gradient_descent(function_2,init_x,1e-10,100))

신경망에서의 기울기

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

from functions import *
from gradient import *

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
  
net = simpleNet()
print(net.W)

x = np.array([0.6, 0.9])
p = net.predict(x)
print(p)

print(np.argmax(p))

t = np.array([0,0,1])
print(net.loss(x, t))

def f(W):
  return net.loss(x, t)

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

학습 알고리즘 구현하기

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

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

 

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

가중치 매개변수의 초깃값을 무엇으로 설정하냐가 신경망 학습의 성공을 좌우하기도 한다.

from functions import *
from gradient import *

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
   
net = TwoLayerNet(784,100,10)
print(net.params['W1'].shape)
print(net.params['b1'].shape)
print(net.params['W2'].shape)
print(net.params['b2'].shape)

x = np.random.rand(100,784)
y = net.predict(x)

x = np.random.rand(100,784)
t = np.random.rand(100,10)

grads = net.numerical_gradient(x, t)

print(grads['W1'].shape)
print(grads['b1'].shape)
print(grads['W2'].shape)
print(grads['b2'].shape)

 

시험 데이터로 평가하기

손실 함수의 값이란, 정확히는 '훈련 데이터의 미니배치에 대한 손실 함수'의 값이다. 하지만 이것이 다른 데이터셋에도 비슷한 길력을 발휘할지는 확실하지 않다. 신경망 학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야한다. 다른 말로 오버피팅을 일으키지 않는지 확인해야한다. 신경망 학습의 원래 목표는 범용적인 능력을 익히는 것이다.

 

from two_layer_net import TwoLayerNet

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

iter_per_epoch = max(train_size / batch_size , 1)

network = TwoLayerNet(784,50,10)

for i in range(iters_num):
  batch_mask = np.random.choice(train_size, batch_size)
  x_batch = x_train[batch_mask]
  y_batch = y_train[batch_mask]

  grad = network.numerical_gradient(x_batch, y_batch)

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

  loss = network.loss(x_batch, y_batch)
  train_loss_list.append(loss)

  if i % iter_per_epoch == 0:
    train_acc = network.accuracy(x_train, y_train)
    test_acc = network.accuracy(x_test, y_test)
    train_acc_list.append(train_acc)
    test_acc_list.append(test_acc)
    print('train acc, test acc |' + str(train_acc) + ',' + str(test_acc))

 

정리

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

'Python > DL' 카테고리의 다른 글

[밑딥] 학습 관련 기술들  (0) 2023.02.27
[밑딥] 오차역전파법  (0) 2023.02.27
[밑딥] 신경망  (0) 2023.01.21
[밑딥] 퍼셉트론  (1) 2023.01.21
[NLP] koBERT 실습  (1) 2022.10.15