728x90
반응형
SMALL
728x90
반응형
SMALL

지난 글로, 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보았습니다.

2024.01.31 - [Programming/Deep Learning] - [Python/DeepLearning] #9.2. 수치 미분과 경사하강법(하)

최종_최종_최종. 신경망 구현으로 오늘은 MNIST 데이터셋을 이용한 2층 신경망(은닉층이 1개인 네트워크)을 구현해 보겠습니다!

MNIST 신경망 구현하기
  • 2층 신경망
  • 1층 은닉층의 뉴런 개수는 100개
    • 활성화 함수로 시그모이드 사용
  • 2층 출력층의 뉴런 개수는 10개
    • softmax를 사용함
  • loss는 cross entropy error 사용할 것
  • predict에서 softmax 적용할 것
  • 내부에 기울기 배열을 구하는 numerical_gradient 구현
  • 경사하강법을 여기서 구현하는 것은 아닙니다!
from common.functions import *
from common.gradient import numerical_gradient

class TwoLayerNet:
  # input_size : 입력 데이터 shape( 이미지 크기 ( 28 * 28 ))
  # hidden_size : 은닉층의 뉴런 개수
  # output_size : 출력층의 뉴런 개수
  # weight_init_std :  정규분포 랜덤값에 표준편차를 적용하기 위함

  def __init__(self, input_size, hidden_size, output_size, weight_init_std=0.01):
    # 나중에 매개변수(W, B)들은 경사하강법으로 모두 한꺼번에 업데이트가 되어야 해요
    self.params = {}

    # 1층 매개변수 마련하기
    self.params["W1"] = weight_init_std * np.random.randn(input_size, hidden_size) # 정규분포 랜덤에다가 표준편차 0.01을 적용
    self.params["b1"] = np.zeros(hidden_size)

    # 2층(출력층) 매개변수 마련하기
    self.params["W2"] = weight_init_std * np.random.randn(hidden_size, output_size)
    self.params['b2'] = np.zeros(output_size)

  def predict(self, X):
    # 매개변수를 params에서 꺼내옵니다.
    W1, W2, b1, b2 = self.params["W1"], self.params["W2"], self.params["b1"], self.params["b2"]

    # 1층 계산
    z1 = np.dot(x, W1) + b1
    a1 = sigmoid(z1)

    # 2층 계산
    z2 = np.dot(a1, W2) + b2
    y = softmax(z2)

    return y

  def loss(self, x, t):
    # 1. predict
    y = self.predict(x)

    # 2. cee
    loss_val = cross_entropy_error(y, t)

    return loss_val
  
  # 구현 ㄴㄴ
  def accuracy(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_params(self, x, t):
    print("미분 시작")

    loss_W = lambda W : self.loss(x, t) # L값 ( 미분 대상 함수 )
    
    # 각 층에서 구해지는 기울기를 저장할 딕셔너리
    # 저장하는 이유 : 각 매개변수의 기울기를 저장 해야만 나중에 경사하강법을 수행할 수 있어요
    grads = {}

    # 1층 매개변수들의 기울기 구하기 (Loss에 대한 W1, b1의 기울기를 grads에 저장)
    grads["W1"] = numerical_gradient(loss_W, self.params["W1"])
    grads["b1"] = numerical_gradient(loss_W, self.params["b1"])

    # 2층 매개변수들의 기울기 구하기 (Loss에 대한 W2, b2의 기울기를 grads에 저장)
    grads["W2"] = numerical_gradient(loss_W, self.params["W2"])
    grads["b2"] = numerical_gradient(loss_W, self.params["b2"])
    
    print("미분 끝")

    return grads

코드가 상당히 길어지긴 했습니다. 하지만 지금까지 이야기 한것들을 모두 구현한 것에 불과하기 때문에 순전파 처리를 그대로 코드로 구현한 것입니다.

위 클래스가 사용하는 메소드와 변수들을 정리해 보겠습니다.

변수

  • params : 신경망의 매개변수를 보관합니다.

params ['W1']은 1번째 층의 가중치, params ['b1']은 1번째 층의 편향

params ['W2']는 2번째 층의 가중치, params ['b2']도 2번째 층의 편향

  • grads : 각 매개변수의 기울기를 보관합니다.

grads['W1']은 1번째 층의 가중치의 기울기, grads ['b1']은 1번째 층의 편향의 기울기

grads ['W2']은 2번째 층의 가중치의 기울기, grads ['b2']은 2번째 층의 편향의 기울기

 

메소드

  • init : 초기화 작업을 수행합니다.

input_size : 입력층의 뉴런 수

hidden_size : 은닉층의 뉴런 수

output_size : 출력층의 뉴런 수

weight_init_std : 생성되는 가중치가 정규분포를 가지기 위한 상숫값

 

  • predict : 예측(추론)을 수행합니다.

x : 이미지 데이터

 

  • loss : 손실 함수의 값을 구합니다.

x : 이미지 데이터

t : 정답 레이블

 

  • accuracy : 정확도를 구합니다. 매개변수는 loss와 같습니다.
  • numerical_gradient : 가중치 매개변수의 기울기를 구합니다. 매개변수는 loss와 같습니다.
  • gradient : numerical_gradient의 개선판입니다. 오차역전파법을 배우고 나서 구현할 예정입니다. 매개변수는 loss와 같습니다.
# 신경망 생성
input_size = 28*28
hidden_size = 100
output_size = 10

net = TwoLayerNet(input_size=input_size, hidden_size=hidden_size, output_size=output_size)
net.params["W1"].shape, net.params["W2"].shape

신경망을 구축하고 나서 신경망을 이해하는데 매개변수 개수를 세는 것만큼 좋은 공부법은 없다고 생각합니다!

총 매개변수의 개수는 몇 개일까요?

  • 1층 W : 78,400 1층 b : 100
  • 2층 W : 1,000 2층 b : 10

총 79,510개

# 신경망이 predict를 잘 하는지를 보자
x = np.random.rand(100, 784) # 784개의 feature를 가진 100개의 데이터를 랜덤으로 생성 (미니 배치)
y = net.predict(x)
x.shape, y.shape

미니 배치 학습 구현하기

 

실제 MNIST 데이터셋을 이용해 미니배치 학습을 활용해 보겠습니다.

니 배치 학습이란 훈련 데이터 중 일부를 무작위로 꺼내고(미니배치) 그 미니배치에 대해서 경사법으로 매개변수를 갱신하는 코드를 만들어 보도록 하겠습니다!

MNIST 데이터 로딩 및 전처리

from tensorflow.keras import datasets
mnist = datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train.shape, y_train.shape

데이터 전처리

from sklearn.preprocessing import OneHotEncoder

y_train_dummy = OneHotEncoder().fit_transform(y_train.reshape(-1, 1))
y_train_dummy = y_train_dummy.toarray()

y_test_dummy = OneHotEncoder().fit_transform(y_test.reshape(-1, 1))
y_test_dummy = y_test_dummy.toarray()
y_train_dummy.shape, y_test_dummy.shape

# feature 전처리
X_train = X_train.reshape(X_train.shape[0], -1)
X_train = X_train / 255.0 # 이미지 정규화 기법. 255.0 으로 나눠주면 모든 픽셀 데이터가 0 ~ 1사이의 값을 갖게 되고, 훈련이 쉽게 된다.

X_test = X_test.reshape(X_test.shape[0], -1)
X_test = X_test / 255.0

X_train.shape, X_test.shape

훈련(학습)

  • 미니 배치 선정
  • 반복 횟수 설정
  • 학습률 선정
# 반복문 돌릴 때 진행 상황을 프로그래스 바로 확인하게 해줌
from tqdm import tqdm_notebook

# 반복 횟수 설정
iter_nums = 10000

# 미니 배치 설정
train_size = X_train.shape[0]
batch_size = 100

# 학습률 설정
learning_rate = 0.1

network = TwoLayerNet(input_size=input_size, hidden_size=hidden_size, output_size=10)
for i in tqdm_notebook(range(iter_nums)):
  
  # 미니 배치 인덱스 선정
  batch_mask = np.random.choice(train_size, batch_size)

  # 미니 배치 만들기
  X_batch = X_train[batch_mask]
  t_batch = y_train_dummy[batch_mask]

  '''
    각 배치 마다의 기울기를 계산

    network의 numerical_gradient_params에서 하는 일
    1. 예측
    2. cross_entropy_error를 이용한 Loss 구하기
    3. 구해진 Loss값을 이용해 미분을 수행해서 << 각 층의 매개변수 기울기를 저장 >>
  '''

  grads = network.numerical_gradient_params(X_batch, t_batch)

  # 모든 매개변수의 기울기를 업데이트 ( 경사하강법 )

  # grads 딕셔너리와 params 딕셔너리의 매개변수 이름이 똑같죠?
  for key in grads.keys():
    network.params[key] -= learning_rate * grads[key] # 경사하강법 공식

  # 갱신된 Loss 확인
  loss = network.loss(X_batch, t_batch)
  print("Step {} -> Loss : {}".format(i+1, loss))

위 코드에서는 미니배치 크기를 100으로 지정하였습니다.

매번 60,000 개의 훈련 데이터에서 임의로 100개의 데이터를 추려내고, 그 100개의 미니배치를 대상으로 확률적 경사 하강법(SGD)을 수행해 매개변수를 갱신합니다.

경사법에 의한 갱신 횟수(반복 횟수)를 10,000번으로 설정하고, 갱신할 때마다 훈련데이터에 대한 손실 함수를 계산해 그 값을 추가하여 그래프를 그릴 용도로 만들어 놓았습니다.

학습 횟수가 늘어갈수록 손실 함수의 값은 줄어들기 시작하며, 이는 학습이 잘 되고 있다는 것을 뜻합니다.

즉, 신경망의 가중치 매개변수가 데이터에 적응해 나간다고 생각할 수 있습니다.

정리하자면! 데이터를 반복해서 학습함으로써 최적 가중치 매개변수로 서서히 다가서고 있다는 것을 의미합니다.

에폭(epoch)

1 에폭으로 훈련 데이터와 시험 데이터에 대한 정확도를 기록하겠습니다.

여기서 에폭이란 하나의 단위인데요, 1 에폭은 학습에서 훈련 데이터를 모두 소진했을 때의 횟수에 해당합니다.

예를 들어 10,000개의 데이터를 100개의 미니배치로 학습할 경우, 경사법을 100번 진행하면서 반복하면 모든 훈련 데이터를 소진하겠죠? 이 경우에는 100회 훈련했을 때를 1 에폭으로 생각합니다.

 

테스트 데이터로 평가하기

훈련 데이터를 이용해서 손실 함수의 값이 서서히 내려가는 것이 확인되었습니다.

여기서의 손실 함수의 값이란 정확히는 '훈련 데이터의 미니배치에 대한 손실 함수'의 값이라고 생각할 수 있는데요, 훈련 데이터의 손실 함숫값이 작아지는 것은 신경망이 잘 학습하고 있다는 방증이지만, 다른 데이터셋에도 비슷한 실력을 발휘할지는 아직 확실하지 않습니다.

학습에서는 훈련 데이터 외의 데이터를 올바르게 인식하는지를 확인해야 합니다. 다른 말로 과대적합(overfitting)을 일으키지 않는지를 확인해야 하는데요, 과대적합이 되어버리면 훈련 데이터에 있는 이미지만 잘 구분하고, 그렇지 않은 이미지는 식별할 수 없다는 뜻이 됩니다.

신경망의 학습의 원래 목표는 처음 보는 데이터도 잘 구분할 수 있는 '범용 능력'을 익히는 것입니다.

이 범용 능력을 평가하기 위해서는 훈련 데이터에 포함되지 않는 새로운 데이터인 테스트 데이터를 사용해서 평가해 보아야 합니다. 따라서 학습 도중 정기적으로 훈련 데이터와 시험 데이터를 대상으로 정확도를 기록합니다.

 

 위의 결과에서 알 수 있듯이, 1 에폭마다 (100 개의 미니배치 * 600 회씩 = 60,000 개의 데이터를 훈련용으로 사용할 때마다)의 모든 훈련 데이터와 시험 데이터에 대한 정확도를 계산하고, 그 결과를 기록했습니다.

보통 정확도는 1에폭마다 기록하는데요, for문 안에서 매번 계산하기에는 시간이 오래 걸리기도 하고, 또 그렇게까지 자주 기록할 필요도 없기 때문입니다. 그저 학습 횟수에 대한 정확도 변화 추이만 알 수 있으면 괜찮습니다.

학습이 진행되면서( 에폭이 진행될수록 ) 훈련 데이터와 시험 데이터를 사용하고 평가한 정확도가 모두 좋아지고 있다는 것이 확인됩니다. 또, 두 정확도에는 차이가 거의 없음을 알 수 있습니다. 이번 학습에서는 오버피팅이 일어났다고 보기 힘들다고 생각합니다!

이로써 신경망 학습을 마무리하였습니다!! 다음글에서는 역전파에 대하여 다뤄보도록 하겠습니다!

 

 

 

728x90
반응형
LIST
728x90
반응형
SMALL

앞서, 경사하강법을 위한 미분과 편미분을 알아보았습니다.

2024.01.29 - [Programming/Deep Learning] - [Python/DeepLearning] #9. 수치 미분과 경사하강법(상)

이에 이어, 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.

 

경사법(경사하강법)

머신러닝 문제 대부분은 학습 단계 "fit"에서 최적의 매개변수를 찾아냅니다.

신경망 역시 최적의 매개변수를 학습 시에 찾아야 하는데요, 이때의 매개변수는 가중치와 편향을 이야기합니다.

여기서 최적이란 손실 함수가 최솟값이 될 때의 매개변수 값을 의미하는데요, 일반적인 문제의 손실함수는 매우 복잡합니다.

왜냐하면 매개변수 공간(범위)이 광대하여 어디가 최솟값이 되는 곳인지를 알아내기가 힘들기 때문이죠.

이런 상황에서 손실 함수의 기울기를 잘 이용해 함수의 최솟값(또는 가능한 한 작은 값)을 찾으려는 방법이 경사 하강법 입니다.

여기서 미분이 활용됩니다! 각 지점에서 함수의 결괏값을 낮추는 방안을 제시하는 지표가 기울기이기 때문이죠. 하지만 어떤 특정 지점의 기울기가 가리키는 곳에 정말 함수의 최솟값이 있는지, 즉 그쪽으로 정말로 나아갈 방향인지는 보장할 수 없습니다.

실제로 복잡한 함수에서는 기울기가 가리키는 방향에 최솟값이 없는 경우가 대부분입니다.

 

주의할 점은 경사법은 기울기가 0인 장소를 찾지만, 그것이 반드시 최솟값이라고 할 수는 없습니다.

극솟값이나 안장점이 될 수도 있는데요, 찌그러진 모양의 함수라면 (사실 대부분이 찌그러진 함수) 함수의 그래프가 평평한 곳으로 파고들면서 고원이라고 하는, 학습이 진행되지 않는 정체기에 빠질 수가 있습니다.

즉, 기울어진 방향이 꼭 최솟값을 가리키는 것은 아니지만 그 방향으로 가야 함수의 값을 줄일 수 있습니다.

그래서 최솟값이 되는 장소를 찾는 문제에서는 기울기 정보를 단서로 나아가야 할 방향을 정해야 합니다.

 

경사법 원리

경사법은 현 위치에서 기울어진 방향으로 일정 거리만큼 이동합니다.

그런 다음 이동한 곳에서도 마찬가지로 기울기를 구하고, 또 그 기울어진 방향으로 계속 나아가기를 반복합니다.

이렇게 해서 함수의 결괏값을 줄이는 것이 경사법(gradient method) 입니다. 경사법은 머신러닝 모델을 최적화하는데 흔히 쓰이는 방법입니다. 특히나 저희가 해볼 신경망 학습에는 경사법을 많이 사용합니다. 경사법을 수식으로 나타내면 다음과 같습니다.

위의 식에서 𝜂(eta. 에타)는 갱신하는 양을 나타냅니다.

이를 신경망에서는 학습률(learning rate)라고 볼 수 있는데요. 한 번의 학습으로 얼마큼 학습할지, 즉 다음 지점을 얼마만큼 이동할지에 대한 거리가 됩니다. 위 식은 1회에 해당하는 갱신이고, 이 단계를 계속 반복합니다.

즉, 위 식 처럼 변수의 값을 갱신하는 단계를 여러 번 반복하면서 서서히 함수의 값을 줄이는 것입니다. 또 여기서는 변수가 2개인 경우를 이야기했지만, 변수가 늘어도 늘 같은 편미분 값으로 갱신하게 됩니다.

또한 학습률 𝜂 값은 미리 특정 값으로 고정시켜 놓아야 합니다. 0.01이나 0.001 같이 정해두게 되는데, 일반적으로 이 값이 너무 크거나 작으면 '좋은 장소'를 찾아갈 수 없습니다.

신경망 학습에서는 보통 이 학습률 값을 변경하면서 올바르게 학습하고 있는지를 확인하면서 진행합니다. 파이썬 코드로 위의 수식을 나타내 보겠습니다.

# 경사하강법 구현
# 편미분 대상 함수, 초기 x 좌표, lr, step_num, 
def gradient_descent(f, init_x, lr=0.01, step_num=100):
  x = init_x

  for _ in range(step_num):
    # step_num 만큼 반복 하면서 기울기 배열( 기울기 벡터 )를 구해서 x를 업데이트

    # 1. 기울기 배열을 구하자
    grad = numerical_gradient(f, x)
    
    x -= lr * grad
    print("기울기 : {} \t x 좌표 : {}".format(grad, x))
  return x

첫 번째 인수 f는 최적화할 함수, init_x는 초깃값, lr은 learning_rate로써 학습률, step_num은 경사법에 다른 반복 횟수를 뜻합니다.

함수의 기울기는 미분 함수 numerical_gradient로 구하고, 그 기울기에 학습률을 곱한 값으로 갱신하는 처리를 step_num번 for문을 통해 반복합니다.

이 함수를 사용하면 함수의 극솟값을 구할 수 있고 잘하면 최솟값을 구해볼 수도 있습니다.

gradient_descent(function_2, np.array([3.0, 4.0]), lr=0.1, step_num=100)

 

거의 0에 가까운 최솟값이 확인되었습니다.

실제로 진정한 최솟값은 (0, 0) 이므로 경사법으로 거의 정확한 결과를 얻었다고 볼 수 있겠습니다.

갱신 과정을 그림으로 나타내면 다음과 같습니다. 검사할 때마다 거의 원점에 가까워지는 것이 확인됩니다.

 

# coding: utf-8
import numpy as np
import matplotlib.pylab as plt

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)

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

학습률이 크거나 너무 작으면 좋은 결과를 얻을 수 없습니다... 두 경우를 실험해 보죠!

# 학습률이 너무 큰 예. lr = 10.0
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=10.0, step_num=100)
print("Learning Rate 10.0 : {}".format(result))


# 학습률이 너무 작은 예. lr = 1e-10
init_x = np.array([-3.0, 4.0])
result, _ = gradient_descent(function_2, init_x=init_x, lr=1e-10, step_num=100)
print("Learning Rate 1e-10 : {}".format(result))

하이퍼 파라미터인 learning rate를 적절하게 조절하는 것이 좋습니다! adam 같은 알고리즘은 이러한 learning rate도 적절하게 조절해 줍니다. (물리의 가속도 개념)

 

학습률이 너무 크면 큰 값으로 발산해 버리고, 반대로 너무 작으면 최솟값에 도달하지 않은 채 갱신이 거의 안되고 끝나버립니다.

이것이 학습률을 잘 조절해야 하는 이유입니다!

참고로 이런 학습률 같은 사람이 직접 지정해야 하는 매개변수를 하이퍼 파라미터 라고 합니다.

 

신경망에서의 기울기

신경망 학습에서의 기울기도 위와 마찬가지로 구할 수 있습니다. 여기서 말하는 기울기는 가중치 매개변수 W에 대한 손실함수의 기울기입니다.

  • 신경망의 학습이란? 손실함수 (Loss)의 값을 최소화시키는 가중치 W와 편향 B를 구하는 것

따라서 가중치의 기울기 배열을 구할 수 있지 않을까?

  • Loss에 대한 W의 미분값을 구하는 것
  • W가 변했을 때 Loss는 얼마만큼 변할 것인가?

예를 들어 2 X 3 형태의 가중치를 만들고, 손실함수를 L로 정의한 신경망을 생각해 볼 수 있습니다. 이때 경사도는 ∂𝐿 / ∂𝑊로 나타 낼 수 있습니다. 수식으로 자세히 살펴봅시다!

∂𝐿 / ∂𝑊의 각 원소는 각각의 원소에 대한 편미분입니다. 

∂𝐿 / ∂𝑤11은 𝑤11을 조금 변경했을 때 손실함수 𝐿이 얼마나 변화하느냐를 나타낼 수 있습니다. 여기서 중요한 점은 ∂𝐿 / ∂𝑊의 형상(shape)이 𝑊와 같다는 것입니다. 둘 다 2 X 3 형태의 행렬인 것을 확인할 수 있습니다. 그럼 간단한 신경망을 예로 들어 실제로 기울기는 구하는 코드를 구해보도록 하겠습니다.

# 클래스로 하는 이유
# tf, keras, pytorch 등을 사용하면 보통 개발자가 직접 커스터마이징 하는 경우가 많습니다.
# 아주 기본적인 CNN, RNN (LSTM) 같은 것들은 그냥 쓰면 되는데, 실제 실무에서는 이런일이 많이 없습니다.
# 특정한 비즈니스에 맞춰서 레이어를 여러분들이 커스터마이징 하는 것이 필요합니다

class SimpleNet:
  
  # 신경망에서의 초기화는 필요한 매개변수를 준비하는 과정
  def __init__(self):
    self.W = np.random.randn(2, 3) # 정규분포로 초기화
    # 실제 신경망은 np.random.randn(2, 3) * 0.01 로 만드는 경우가 많습니다. (표준편차를 0.01로 설정)
  
  def predict(self, x):
    return np.dot(x, self.W)

  def loss(self, x, t):
    # loss를 구하는 과정

    # 1. 예측
    z = self.predict(x)

    # 2. softmax 통과
    y = softmax(z)

    # 3. loss함수를 이용해 실제 loss값 구하기
    loss = cross_entropy_error(y, t)

    return loss

 

SimpleNet 클래스의 객체를 생성하면 자동으로 2 X 3 형태의 가중치 배열을 랜덤으로 생성하게 됩니다.

그다음 예측을 수행하는 predict는 각 뉴런에서 객체가 만들어질 때 생성된 가중치 행렬 W와 데이터인 x의 내적을 구하고, loss 메소드 에서는 예측된 결과(predict)에 softmax 활성화 함수를 적용시키고 나서 해당 결과에 손실 함수인 cross_entropy_error를 이용해 손실값을 찾습니다. x는 입력 데이터, t는 정답 레이블입니다.

net = SimpleNet()

print("가중치 확인 : \n{}".format(net.W))

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

# 예측값은 몇개가 나오지?
print("단순 예측 값 : \n{}".format(p))

print("최댓값의 인덱스 : {}".format(np.argmax(p)))

# 임의의 정답을 마련하자
t = np.array([0, 1, 0]) # 정답은 1

net.loss(x, t)

 

Loss에 대한 기울기를 구하는 함수를 따로 만들어보도록 하겠습니다! numerical_gradient(f, x)를 써서 구하면 됩니다.

# Loss의 대한 W의 기울기를 구하기 위한 함수
def f(W):
  return net.loss(x, t)
 
# 기울기에 대한 Loss의 기울기 배열을 구할 수 있어요
dW = numerical_gradient(f, net.W)
dW

numerical_gradient(f, x)의 인수 f는 함수, x는 함수 f의 인수입니다. 그래서 가중치를 의미하는 net.W를 인수로 받아 손실 함수를 계산하는 새로운 함수 f를 정의하였습니다.

numerical_gradient에서는 함수 f와 net.W를 입력받아 가중치들에 대해 한꺼번에 기울기를 구합니다. dW는 numericalgradient의 결과로써, 2 X 3차원의 배열입니다. dW를 내용을 보겠습니다.

예를 들어 𝑤11에 위치한 0.12 정도의 값은 𝑤11의 값을  만큼 늘리면 손실 함수의 값은 0.12ℎ 만큼 증가한다는 뜻이 됩니다.

마찬가지로 𝑤32는 대략 -0.47이니, 𝑤32를 만큼 늘리면 손실 함수의 값은 0.5ℎ만큼 감소한다고 볼 수 있습니다. 그래서 손실 함숫값을 줄인다는 관점에서 𝑤11은 음의 방향으로 갱신해야 하고, 𝑤32는 양의 방향으로 갱신해야 함을 알 수 있습니다.

또한 한 번에 갱신되는 양에는 𝑤32가 𝑤11보다 크게 바뀐다는 사실도 알 수 있죠 위 코드의 f(W)를 lambda 형태로 바꿀 수도 있습니다.

dW = numerical_gradient(lambda w : net.loss(x, t), net.W)
dW

Summary

신경망 학습에 대한 이야기는 여기까지면 충분합니다.

몇 가지 키워드로 정리해 보고, 다음 글에서 최종적으로 MNIST를 학습할 수 있는 신경망을 직접 구현하도록 해봅시다!

전제

신경망에는 적응 가능한 가중치와 편향이 있고, 이 가중치와 편향을 훈련 데이터에 적응하도록 조정하는 과정을 '학습'이라 합니다.

4 단계로 정리해 보겠습니다!

1단계 - 미니배치

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

2단계 - 기울기 산출

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

3단계 - 매개변수 갱신

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

4단계 - 반복

1 ~ 3 단계를 계속 반복합니다.

 

정리한 내용은 경사 하강법으로 매개변수를 갱신하는 방법이라고 볼 수 있으며, 데이터를 미니 배치로 무작위로 가져오기 때문에 이를 확률적 경사 하강법(stochastic gradient descent, SGD)라고 합니다.

확률적으로 무작위로 골라낸 데이터에 대해 수행하는 경사하강법이라는 의미가 됩니다. 대부분의 딥러닝 프레임워크에서는 SGD라는 함수로 해당 기능을 구현하고 있습니다.

다음 글에서 본격적으로 MNIST 데이터셋을 이용한 2층 신경망(은닉층이 1개인 네트워크)을 구현해 보겠습니다!

728x90
반응형
LIST
728x90
반응형
SMALL

지난 시간 손실 함수 Loss Function의 종류에 대하여 알아보았습니다.

2024.01.27 - [Programming/Deep Learning] - [Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

 

[Python/DeepLearning] #8. 손실 함수의 종류(Loss Function)

지난 신경망 내용에서 매끄럽게 변화하는 시그모이드 함수와 퍼셉트론에서는 갑자기 변화하는 계단 함수를 사용하는 신경망 학습의 차이에 대하여 알아보았습니다. 2023.11.03 - [Programming/Deep Learn

yuja-k.tistory.com

오늘은 신경망 학습의 마지막 수치 미분과 경사하강법에 대하여 설명해 보도록 하겠습니다!

이번 블로그로는 (상)&(하)로 나눠집니다. (상) 편에서 미분과 편미분에 대하여 설명하며, (하)에서 경사하강법과 신경망에서의 기울기에 대하여 알아보도록 하겠습니다. 

 

경사법(경사하강법)을 위한 미분

경사법에서는 기울기 값을 기준으로 최적화하기 위한 방향을 결정합니다. 기울기의 성질을 알아보기 앞서 미분을 먼저 살펴보겠습니다.

 

미분

우리가 트랙에서 달리는 모습을 상상해 보겠습니다.

처음부터 10분 동안 2km씩 달렸다고 가정했을 때 속도는 2(거리) / 10(시간) = 0.2(속도)만큼의 속도(변화)로 뛰었다고 해석할 수 있을 것입니다.

위의 예에서는 달린 거리가 시간에 대해서 얼마나 변화했는지를 계산한 것입니다. 다만 여기에서 10분에 2km를 뛰었다는 이야기는 정확하게는 10분 동안의 평균 속도를 구한 것이죠.

하지만 미분은 '특정 순간'의 변화량을 뜻합니다. 따라서 10분이라는 시간을 최대한 줄여 한 순간의 변화량을 얻는다고 볼 수 있습니다.

이처럼 미분은 한순간의 변화량을 표시한 것입니다.

위 식은 함수의 미분을 나타낸 식입니다.

좌변은 함수 𝑓(𝑥) 𝑥에 대한 미분(𝑥에 대한 𝑓(𝑥)의 변화량)을 나타낸 기호라고 볼 수 있습니다.

결국, 𝑥의 작은 변화가 함수 𝑓(𝑥)를 얼마나 변화시키느냐를 의미합니다. 이때 시간의 작은 변화, 즉 시간을 뜻하는 를 한없이 0에 가깝게 한다는 의미를 lim0로 나타내었습니다. 그렇다면 파이썬으로 미분 함수를 구현해 보겠습니다.

h에 최대한 작은 값을 대입해서 다음과 같이 계산이 가능합니다. 참고로  Δ𝑥로도 표현합니다.

import numpy as np

# 미분의 나쁜 구현
def numerical_diff(f, x):
  h = 10e-50 # 0.0000 0이 50개
  return (f(x+h) - f(x)) / h

 

함수의 이름은 수치 미분(numerical differentiation)에서 따왔습니다.

이 함수는 함수 𝑓와 함수 𝑓에 넘겨질 𝑥를 받아서 실제 미분을 구현하지만, 고쳐야 할 부분들이 약간 있습니다.

위의 예에서는 가능한 한 h에 작은 값을 넣기 위해 10e-50이라는 매우 작은 수를 이용했었는데, 이 방식은 반올림 오차(rounding error) 문제를 일으킵니다. 반올림 오차는 작은 값(보통 소수점 8자리)이 생략되어 최종 계산 결과에 오차가 됩니다.

즉, 너무나 작은 숫자로 나눗셈을 하게 되면, 반올림 오차가 발생하여 컴퓨팅 시스템상 부동소수점 오류가 나게 됩니다.

 

파이썬에서의 반올림 오차는 보통 다음 상황에서 발생합니다.

np.float32(10e-50)

1000 / np.float32(10e-50)

너무 작은 값을 파이썬에서 소수점으로 나타내면 0.0이 되는 모습을 확인할 수 있습니다. 너무 작은 값을 컴퓨터로 계산하면 문제가 되기 때문에 이 미세한 값 h를 10의 −4제곱 정도로 놓으면 좋은 값을 얻는다고 알려져 있습니다.

 

그다음 개선할 점은 함수 f의 차분(임의 두 점에서 함숫값들의 차이)에 관련한 것입니다. 

𝑥+ 𝑥 사이의 함수 𝑓의 차분을 계산하고 있지만, h 자체가 매우 작은 값이기 때문에 애당초 위 계산에는 오차가 있다고 생각해야 합니다.

미분 자체가 어떤 순간 지점(시간(ℎ)이 최대한 0이 되는 지점)의 기울기를 의미하기 때문에 진정한 미분은 𝑥 위치 에서의 함수의 기울기를 이야기해야 하지만, 가 절대 0이 될 수가 없기 때문에 ( 0에 가까워지기는 하지만 ) 컴퓨터를 통한 미분 계산은 언제나 오차를 동반해야만 합니다.

이 오차를 최대한 줄이기 위해 (𝑥+ℎ)(𝑥−ℎ)일 때의 함수 𝑓의 차분을 계산하는 방법을 사용하기도 합니다. 이 차분은 x를 중심으로 그 전후의 차분을 계산한다는 의미로, 중심 차분 혹은 중앙 차분이라 합니다. 참고로 (𝑥+) 𝑥의 차분은 전방 차분이라고도 합니다.

위 두 가지 개선사항을 개선한 후의 미분 함수를 구현해 보겠습니다.

# 좋은 수치미분 구현
def numerical_diff(f, x):
  h = 1e-4 # 0.0001
  return (f(x+h) - f(x-h)) / (2 * h)
  • 아주 작은 값을 의미하는 ℎ는 1e-4
  • 수학적인 기교를 이용해 컴퓨터를 활용한 미분에서의 나눗셈 오류를 해결할 수 있다 (중앙차분과 전방차분을 활용)

위처럼 아주 작은 차분으로 미분하는 것을 수치 미분이라고 합니다.

수치 미분과 기울기의 관계

 

위의 미분 함수를 이용해 실제 간단한 함수를 미분해 보겠습니다. 먼저. 다음과 같은 2차 함수가 있다고 가정합니다.

def function_1(x):
  return 0.01*x**2 + 0.1 * x
import numpy as np
import matplotlib.pylab as plt

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

plt.xlabel("x")
plt.ylabel("f(x)")
plt.plot(x, y)
plt.show()

이번엔 x가 5와 10일 때의 해당 함수의 미분값입니다.

# x가 5일 때의 미분값과 10일 때의 해당 함수의 미분값 구하기
print("x가 5일 때 미분 값 : {}".format(numerical_diff(function_1, 5)))
print("x가 10일 때 미분 값 : {}".format(numerical_diff(function_1, 10)))

def tangent_line(f, x):
    d = numerical_diff(f, x)
    y = f(x) - d*x
    return lambda t: d*t + y

fig, axes = plt.subplots(1,2, figsize=(20,8))

x = np.arange(0.0, 20.0, 0.1)
y = function_1(x)
axes[0].set_xlabel("x")
axes[0].set_ylabel("f(x)")

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

axes[0].plot(x, y)
axes[0].plot(x, y2)
axes[0].set_title("x = 5")

axes[1].set_xlabel("x")
axes[1].set_ylabel("f(x)")

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

axes[1].plot(x, y)
axes[1].plot(x, y2)
axes[1].set_title("x = 10")
plt.show()

해당하는 위치(x)에서의 접선이 그려지는 것이 확인됩니다.

편미분

한쪽 변수 방향으로만 미분하는 것입니다. 다른 쪽 변수는 상수로 취급합니다.

인수들이 1개가 아닌, 2개 이상의 인수가 있는 상황에서 의 미분은 편미분을 통해 해결할 수 있습니다. 다음 식을 확인해 보겠습니다.

def function_2(x):
    return x[0]**2 + x[1]**2
from mpl_toolkits.mplot3d import Axes3D
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111, projection='3d')

X0 = np.arange(-3, 3, 0.05)
X1 = np.arange(-3, 3, 0.05)

X0, X1 = np.meshgrid(X0, X1)
Z = function_2([X0,X1])

ax.plot_wireframe(X=X0, Y=X1, Z=Z,rstride=10, cstride=10)
ax.set_xlabel('x0')
ax.set_ylabel('x1')
ax.set_zlabel('f(x)')
plt.show()

  1. 미분을 함에 있어서 고려해야 할 방향이 2개
    • 𝑥0, 𝑥1 방향으로 고려
  2. 둘 중 하나의 방향으로만 미분을 수행 -> 편미분
    • 𝑥0의 미분을 수행할 때는 𝑥1을 상수처럼 취급 𝑓 / 𝑥0
    • 𝑥1의 미분을 수행할 때는 𝑥0을 상수처럼 취급 𝑓 / 𝑥1

 


 

𝑥0=3, 𝑥1=4일 때 𝑥0에 대한 편미분 𝑓 / 𝑥0을 구하는 코드를 구현

 

변수가 두 개인 함수의 미분은 '어느 변수에 대한 미분이냐'를 따져줘야 합니다.

즉, 𝑥0, 𝑥1 중 어느 변수에 대한 미분이냐를 구별해야 한다고 생각할 수 있는데요, 이와 같이 변수가 여럿인 함수에 대한 미분을 편미분이라고 합니다.

수식으로는 𝑓 / 𝑥0 또는 𝑓 / 𝑥1 처럼 표현합니다. 연습 삼아 편미분 문제를 풀어보겠습니다. 

𝑥0=3, 𝑥1=4 일 때 𝑥0에 대한 편미분 𝑓𝑥0를 구하는 코드를 구현해 보겠습니다.

def function_tmp1(x0):
  return x0 ** 2 + 4.0 ** 2

numerical_diff(function_tmp1, 3.0)

다음은 𝑥0=3, 𝑥1=4 일 때 𝑥1에 대한 편미분 𝑓 / 𝑥1를 구하는 코드를 구현해 보겠습니다.

def function_tmp2(x1):
  return 3.0 ** 2 + x1 ** 2

numerical_diff(function_tmp2, 4.0)

위의 문제를 풀기 위해 변수가 하나인 함수를 정의했으며, 그 함수를 미분하는 형태로 구현하여 풀어보았습니다.

가령, 𝑥1=4로 고정된 새로운 함수를 하나 정의하여 변수가 𝑥0 하나뿐인 함수에 대해 수치 미분(numerical_diff)을 적용시킵니다. 이처럼 편미분은 변수가 하나인 미분과 마찬가지로 특정 장소의 기울기를 구해낼 수 있습니다.

단, 여러 변수 중 목표 변수 하나에 초점을 맞추고 다른 변수는 값을 고정합니다(상수처럼 활용).

앞의 예에서는 목표 변수를 제외한 나머지를 특정 값에 고정하기 위해서 새로운 함수인 function_tmp1, function_tmp2를 정의한 것이지요! 그리고 그 새로 정의한 함수에 대해 그동안 사용한 미분값을 적용시켜 편미분을 구한 것입니다.

기울기에 대하여

앞서는 𝑥0와 𝑥1의 편미분을 변수별로 따로 계산해 봤습니다.

그렇다면 𝑥0와 𝑥1의 편미분을 동시에 계산하고 싶으면 어떻게 해야 할까요?

위의 문제처럼 𝑥0=3, 𝑥1=4일 때 (𝑥0,𝑥1) 양쪽의 편미분을 묶어서 (𝑓 / 𝑥0, 𝑓 / 𝑥1)을 계산한다고 생각해 볼 수 있습니다. (𝑓 / 𝑥0, 𝑓 / 𝑥1)처럼 모든 변수의 편미분을 벡터로 정리한 것을 기울기(gradient)라고 합니다.

기울기를 구하는 예를 구현해 보겠습니다.

&part;𝑓 /&nbsp;&part;𝑥0 ,&nbsp; &part;𝑓 /&nbsp;&part;𝑥1

𝑥0과 𝑥1 각각의 편미분 결과를 (𝑓 / 𝑥0, 𝑓 / 𝑥1)로 정리한 것을 기울기(gradient)라고 한다.

# 기울기를 구하는 코드 - 각각의 편미분을 배열로 만든다.
def numerical_gradient(f, x):
  h = 1e-4
  # 기울기를 저장할 배열
  grad = np.zeros_like(x) # x와 형상(shape)이 같은 배열을 생성

  for idx in range(x.size):
    # x의 값을 idx에 맞게 하나씩 빼온다.
    tmp_val = x[idx]

    # 각 좌표에서의 미분을 수행 -> 편미분

    # 1. f(x+h) 계산
    x[idx] = tmp_val + h
    fxh1 = f(x)

    # 2. f(x-h) 계산
    x[idx] = tmp_val - h
    fxh2 = f(x)

    # 3. 기울기 계산해서 grad 배열에 저장 ( 미분 수행 )
    grad[idx] = (fxh1 - fxh2) / 2*h

    # x[idx]를 원래 값으로 복구
    x[idx] = tmp_val

  return grad

numerical_gradient 함수가 약간 복잡하게 생기긴 했지만 간단하게 생각해 보면 인자로 들어온 모든 x의 값(배열)에 대한 함수 f를 미분한 코드입니다. 위 함수를 이용하면 인자가 몇 개가 들어오든 모든 기울기를 벡터로 나타내는 배열을 확인할 수 있습니다.

result = numerical_gradient(function_2, np.array([3.0, 4.0]))
print("x = [3, 4] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([0.0, 2.0]))
print("x = [0, 2] 일 때의 기울기 배열 : {}".format(result))

result = numerical_gradient(function_2, np.array([3.0, 0.0]))
print("x = [3, 0] 일 때의 기울기 배열 : {}".format(result))

이처럼 (𝑥2,𝑥1)의 각 점에서의 기울기를 계산할 수 있습니다. 각 값별 기울기를 시각적으로 확인해 보겠습니다.

# coding: utf-8
# cf.http://d.hatena.ne.jp/white_wheels/20100327/p3
import numpy as np
import matplotlib.pylab as plt
from mpl_toolkits.mplot3d import Axes3D


def _numerical_gradient_no_batch(f, x):
    h = 1e-4 # 0.0001
    grad = np.zeros_like(x) # x와 형상이 같은 배열을 생성
    
    for idx in range(x.size):
        tmp_val = x[idx]
        
        # f(x+h) 계산
        x[idx] = float(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


def numerical_gradient(f, X):
    if X.ndim == 1:
        return _numerical_gradient_no_batch(f, X)
    else:
        grad = np.zeros_like(X)
        
        for idx, x in enumerate(X):
            grad[idx] = _numerical_gradient_no_batch(f, x)
        
        return grad


def function_2(x):
    if x.ndim == 1:
        return np.sum(x**2)
    else:
        return np.sum(x**2, axis=1)


def tangent_line(f, x):
    d = numerical_gradient(f, x)
    print(d)
    y = f(x) - d*x
    return lambda t: d*t + y
     
if __name__ == '__main__':
    x0 = np.arange(-2, 2.5, 0.25)
    x1 = np.arange(-2, 2.5, 0.25)
    X, Y = np.meshgrid(x0, x1)
    
    X = X.flatten()
    Y = Y.flatten()
    
    grad = numerical_gradient(function_2, np.array([X, Y]) )
    
    plt.figure()
    plt.quiver(X, Y, -grad[0], -grad[1],  angles="xy",color="#666666")
    plt.xlim([-2, 2])
    plt.ylim([-2, 2])
    plt.xlabel('x0')
    plt.ylabel('x1')
    plt.grid()
    plt.draw()
    plt.show()

기울기에 대한 그림은 위처럼 방향을 가진 벡터(화살표)로 그려집니다.

이 그림을 보면 기울기는 함수의 '가장 낮은 장소(최솟값)'를 가리키는 것 같습니다. 한 점을 향하고 있는 것이 확인되는데요, 또한 가장 낮은 곳에서 멀어질수록 화살표의 크기가 커지는 것도 확인이 가능합니다.

중요한 것은 기울기 자체는 가장 낮은 장소를 가리키고 있으나, 실제는 반드시 그렇다고는 할 수 없습니다.

사실 기울기는 각 지점에서 낮아지는 방향을 가리키는데요, 더 확실히 말하자면 기울기가 가리키는 쪽은 각 장소에서 함수의 출력 값(결괏값)을 가장 크게 줄이는 방향입니다. 확실히 기억해야 합니다.

 

지금까지 경사하강법을 위한 미분과 편미분을 알아보며 준비를 하였습니다. 다음 블로그에서는 경사법(경사하강법)의 원리와 신경망에서의 기울기에 대한 신경망 학습을 알아보도록 하겠습니다.

728x90
반응형
LIST
728x90
반응형
SMALL

지난 신경망 내용에서 매끄럽게 변화하는 시그모이드 함수와 퍼셉트론에서는 갑자기 변화하는 계단 함수를 사용하는 신경망 학습의 차이에 대하여 알아보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

이번 블로그에서는 모델을 학습 때, 모델의 출력값과 예측값의 오차를 계산해 주는 함수, 손실 함수 Loss Function의 종류에 대하여 알아보도록 하겠습니다.

손실 함수 "Loss Function"

"배가 얼마나 고파요?"라는 질문에 뭐라고 대답할까요?

"배가 많이 고파요" 또는 "적당히 배가 고파요"라는 그냥 막연한 대답이 돌아와야 하는 게 보통입니다.

만약 "저는 지금 63.33 만큼 배가 고파요"라고 이야기하면 질문한 사람이 조금은 당황하겠죠?

이 사람이 개인적으로 만들어낸 이 사람만의 지표라는 것을 토대로 배고픔을 수치적으로 표현했기 때문입니다.

위 사람은 아마 일정 수치가 되면 "배가 고프다"라고 표현하고, 일정 수치가 되면 배가 고프지 않게 밥을 먹는 조금은 특이한 사람이죠.

위에서 이야기한 지표는 하나의 은유적인 비유이지만, 신경망 학습에서는 현재의 상태를 '하나의 지표'로 표현하고, 그 지표를 가장 좋게 만들어주는 가중치 매개변수의 값을 탐색하는 것입니다.

 

위에서 언급한 사람이 자신만의 '배고픔 지표'를 가지고 '최적의 배고픔'을 찾기 위해 노력하듯, 신경망도 '하나의 지표'를 기준으로 최적의 매개변수 값을 탐색합니다.

신경망 학습에서 사용하는 지표를 "손실 함수 Loss Function"라고 합니다. 이 손실 함수는 임의의 함수를 마음대로 사용할 수 있지만, 일반적으로는 평균 제곱 오차 (MSE)교차 엔트로피 오차를 사용합니다.

 

손실 함수는 신경망 성능의 '나쁨'을 나타내는 지표로써, 현재의 신경망이 훈련데이터를 얼마나 잘 처리하지 '못'하느냐를 나타냅니다.

'나쁨'을 지표로 한다는 것이 약간은 어색하지만 그냥 음수만 곱하게 되면 반대를 나타내기 때문에 크게 신경 쓰지 않아도 됩니다.

결론적으로 나쁨을 최소로 하는 것과 좋음을 최대로 하는 것은 같은 의미가 됩니다.

"'나쁨'을 지표로 삼아도 수행하는 일은 다르지 않다"라는 것입니다.

 

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

가장 많이 쓰이는 손실 함수로 평균 제곱 오차(Mean Squared Error)가 있습니다. 수식은 다음과 같습니다.

앞쪽이 1/2인 이유는 미분했을 때 남는 식이 (𝑦𝑘−𝑡𝑘) 이기 때문이다.

  • 𝑦𝑘는 예측값을 의미한다. (𝑦̂ 𝑘)
  • 𝑡𝑘는 정답(타깃값)을 의미한다.
import numpy as np
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

def mean_squared_error(y, t):
  return 0.5 * np.sum((y-t) ** 2)
  
print("정답을 2로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

이 배열들의 원소는 첫 번째 인덱스부터 순서대로 '0', '1', '2'를 의미합니다. 

  • y는 𝑠𝑜𝑓𝑡𝑚𝑎𝑥의 출력물로 가정하고, 정답이 2일 확률을 60%로 추정하고 있다.
  • t는 One-Hot Encoding 된 정답 레이블로써, 원래 정답이 2라고 정해져 있다.

소프트맥스 함수의 결과물은 확률이라고 해석할 수 있기 때문에 위 예에서 이미지가 0으로 분류될 확률은 0.1, '1'로 분류될 확률은 0.05, '2'로 분류될 확률은 0.6... 이런 식으로 해석해 보도록 합시다. 

𝑡는 정답 레이블로써, 정답을 가리키는 위치의 원소는 1로, 그 외에는 0으로 표기한 모습입니다. 숫자 '2'를 의미하는 원소 값이 1이기 때문에 정답은 '2'로 생각할 수 있습니다. 평균 제곱오차 수식을 직접 구현해 보겠습니다.

# 일부로 오답 예측을 만들어서 MSE 값을 측정하기. 예측값을 7이라고 가정한 경우로 테스트
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 MSE 값 : {:.3f}".format(mean_squared_error(np.array(y), np.array(t))))

두 가지 예를 살펴보았습니다. 첫 번째 예는 정답이 '2'이고, 신경망의 출력도 '2'에서 가장 높은 경우라고 생각하면 됩니다.

두 번째 예에서도 정답은 똑같이 '2' 지만, 출력이 '7'인 경우가 가장 높다고 가정한 것입니다.

결괏값을 확인해 보면 정답을 '2'로 예측했을 때의 손실 함수 오차가 더 작으며, 정답 레이블과의 오차도 작은 것을 알 수 있습니다.

즉, 평균 제곱 오차를 기준으로는 첫 번째 추정 결과가 오차가 더 작기 때문에 정답에 가깝다!라고 판단할 수 있을 것입니다.

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

또 다른 손실 함수로써 교차 엔트로피 오차(cross entropy error)도 많이 활용됩니다. 교차 엔트로피 오차 수식은 다음과 같습니다.

여기에서의 𝑙𝑜𝑔는 밑이 𝑒인 자연로그 (𝑙𝑜𝑔𝑒)입니다.

𝑦𝑘는 신경망의 출력, 𝑡𝑘는 정답 레이블입니다. 𝑡𝑘는 정답에 해당하는 인덱스의 원소만 1이고, 나머지는 0인 One-Hot-Encoding 된 형식의 데이터입니다.

그래서 위 식은 실질적으로 정답일 때의 추정(𝑡𝑘가 1일 때의 𝑦𝑘)의 자연로그를 계산하는 식이 됩니다.

정답이 아닌 나머지 모드는 𝑡𝑘가 0이므로 𝑙𝑜𝑔𝑦𝑘와 곱해도 0이 되어 결과에 영향을 주지 않습니다. 예를 들어 정답 레이블은 '2'가 정답이라 하고, 이때의 신경망 출력이 0.6이라면 교차 엔트로피 오차는 −𝑙𝑜𝑔0.6=0.51이 됩니다. 또한 같은 조건에서 신경망 출력이 0.1이라면 −𝑙𝑜𝑔0.1=2.30이 됩니다.

즉, 교차 엔트로피 오차는 정답일 때의 출력이 전체 값을 정하게 됩니다. 교차 엔트로피를 구현해 보도록 하겠습니다.

  • One Hot Encoding 된 𝑡𝑘가 곱해지기 때문에, 정답이 아닌 타깃은 신경을 전혀 쓰지 않는다. ( 𝑙𝑜𝑔𝑦𝑘 X '0 값' )
  • 예측값 자체가 오차 출력값이 된다.
def cross_entropy_error(y, t):
  delta = 1e-7
  return -np.sum(t * np.log(y + delta))

 

위에서 아주 작은 값인 delta를 더하는 이유는 np.log 함수에 0이 대입되면 마이너스 무한대를 나타내게 됩니다. 따라서 더 이상 계산을 할 수 없기 때문에 y에 아주 작은 값 delta를 더해 절대 0이 되지 않도록, 즉 마이너스 무한대가 되지 않도록 방지한 코드입니다.

위의 함수를 이용해서 2를 예측하는 간단한 계산을 해보겠습니다.

# 정답은 2
t = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

# 정답이 2일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.6, 0.0, 0.05, 0.1, 0.0, 0.1, 0.0, 0.0]
print("정답을 2로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

# 정답이 7일 확률이 가장 높다고 추정함(0.6)
y = [0.1, 0.05, 0.1, 0.0, 0.05, 0.1, 0.0, 0.6, 0.0, 0.0]
print("정답을 7로 추정했을 때의 CEE값 : {:.3f}".format(cross_entropy_error(np.array(y), np.array(t))))

 

MSE와 마찬가지로 첫 번째 예에서는 오차가 약 0.51, 두 번째 예에서는 2.30 정도로 많은 차이가 나타나고 있는 것이 확인됩니다.

즉, 결과가 더 작은 첫 번째 추정이 정답일 가능성이 높다고 판단한 것으로, 앞서 평균 제곱 오차의 판단과 일치하게 됩니다.

미니 배치 학습

일전에 배치에 대해 이야기해 보았습니다.

2023.11.03 - [Programming/Deep Learning] - [Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensor flow)

 

[Python/DeepLearning] #7. 출력층 뉴런 개수 정하기 & MNIST 데이터(Via Tensorflow)

지난 블로그, 신경망의 출력층에 대하여 알아보고 설계에 대하여 알아보았습니다. 2023.10.11 - [Programming/Deep Learning] - [Python/DeepLearning] #6. 출력층(output layer) 설계 [Python/DeepLearning] #6. 출력층(output lay

yuja-k.tistory.com

입력한 데이터의 묶음에 대한 결과물들의 묶음이었는데요, 이 데이터들을 모두 훈련 데이터로 사용하게 된다면, 모든 데이터에 대해 손실함수를 적용시켜야 할 것입니다.

즉, 훈련데이터가 100 묶음이라면, 그로부터 계산한 100개의 손실 함숫값들의 합을 지표로 삼아야 한다는 이야기가 될 것입니다.

지금까지 데이터 하나에 대한 손실 함수만 생각해 보았으니, 이제 훈련 데이터 모두에 대한 손실 함수를 적용시킨다고 생각해 보겠습니다.

그렇다면 교차 엔트로피의 오차는 다음과 같겠네요!

𝑁 개의 배치 데이터를 활용했을 때 바뀐 CEE 공식

식이 조금 복잡해진 것 같지만, 간단합니다.

데이터가 𝑁개라면 𝑡𝑛𝑘는 𝑛번째 데이터의 𝑘번째 값을 의미합니다.

(𝑦𝑛𝑘는 신경망의 출력, 𝑡𝑛𝑘는 정답 레이블) 단순히 교차 엔트로피 함수를 𝑁개로 확장하고, 그 오차를 모두 더해서 마지막에 𝑁으로 나누어 정규화하고 있을 뿐입니다.

즉, 제일 마지막에 𝑁으로 나눔으로써 평균 손실 함수를 구하는 것이다라고 생각할 수 있겠습니다.

이렇게 평균을 구해 사용하면 훈련 데이터의 개수와 관계없이 언제든 통일된 지표를 얻을 수 있을 것 같습니다. 예를 들어 훈련 데이터가 1,000개든, 10,000개든 상관없이 평균 손실 함수를 구할 수 있다는 뜻이 됩니다.

 

많은 양의 데이터셋에 대한 처리

일전에 사용했은 MNIST 데이터셋은 훈련 데이터가 무려 60,000개였습니다.

그래서 모든 데이터를 대상으로 손실 함수의 합을 구하려면 시간이 많이 걸리게 됩니다. 더 나아가 빅데이터 수준이 되면 그 수는 수백만에서 수천만도 넘는 거대한 값이 되기도 합니다.

이 많은 데이터를 대상으로 일일이 손실 함수를 계산하는 것은 비현실적이기 때문에, 데이터 일부를 추려 '근사치'로 이용할 수 있습니다.

이 일부가 되는 데이터를 미니배치(mini-batch)라고 합니다. MNIST에서 미니배치 학습을 구현하는 코드를 작성해 보도록 하겠습니다.

# mnist 데이터셋 로딩
from tensorflow.keras import datasets
mnist = datasets.mnist

(X_train, y_train), (X_test, y_test) = mnist.load_data()
X_train.shape

y_train.shape

 

Cross Entropy는 One Hot Encoding 이 되어 있어야 한다.

  • X_train.shape -> (60000, 784)
  • y_train.shape -> (60000, 10)
# step 1 : reshape 부터 진행 -> (60000, 1)
y_train_step_1 = y_train.reshape(-1, 1)
y_train_step_1.shape

y_train_step_1[:3]

# OneHotEncoder를 이용해서 원핫 인코딩 수행
from sklearn.preprocessing import OneHotEncoder

y_train_one_hot = OneHotEncoder().fit_transform(y_train_step_1)
y_train_one_hot = y_train_one_hot.toarray()

y_train_one_hot.shape

y_train_one_hot[:3]

X_train = X_train.reshape(60000, -1)
X_train.shape

미니 배치 구현하기
# 훈련 데이터에서 무작위로 10장만 빼내오기
train_size = X_train.shape[0] # 전체 훈련 데이터셋 크기
batch_size = 10 # 미니 배치의 사이즈
batch_mask = np.random.choice(train_size, batch_size) # train_size에서 무작위로 batch_size만큼의 정수를 선택

# 랜덤으로 선택된 인덱스에 있는 데이터만 추려내기
X_batch = X_train[batch_mask] 

# 원핫 인코딩된 y_train에서 선택된 인덱스에 있는 데이터만 추려내기
y_batch = y_train_one_hot[batch_mask]

np.random.choice 함수를 이용해 지정한 범위의 수 중에서 무작위로 원하는 개수만 꺼내는 역할을 합니다.

위의 예에서는 60000 미만의 수 중에서 10개의 무작위 한 숫자를 뽑아낸 것입니다.

실제 batch_mask를 확인하면 10개의 인덱스가 무작위로 들어있는 배열을 확인할 수 있습니다.

print("무작위로 선택된 인덱스 : {}".format(batch_mask))

 

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

미니배치 같은 배치데이터를 지원하는 교차 엔트로피 오차는 다음과 같이 구현합니다.

단순히 배열 하나만 처리하던 부분에서, 여러 개의 배열을 한꺼번에 처리할 수 있도록만 바꿔주면 간단히 구현 가능 합니다.

# 배치 및 배치가 아닐 때 까지 고려
def cross_entropy_error(y, t):

  # 1차 원일 때에 대한 처리( 배치가 아닐 때의 처리 )
  if y.ndim == 1:
    # 강제로 2차원 배열화 시키는 것
    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

위 코드에서 y는 신경망의 출력, t는 정답 레이블을 의미합니다.

y가 1차원이라면, 즉 데이터 하나당 교차 엔트로피 오차를 구하는 경우는 reshape 함수로 데이터의 형상을 바꿔줍니다. 그리고 배치의 크기로 나눠 정규화시키고 이미지 1장당 평균의 교차 엔트로피 오차를 계산합니다.

정답 레이블이 원 - 핫 인코딩이 아니라 '2'나 '7' 등의 숫자 레이블로 주어졌을 때의 교차 엔트로피 오차는 다음과 같이 구현 가능합니다.

# 원-핫 인코딩이 되어있지 않은 경우 대응
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(np.log(y[np.arange(batch_size), t])) / batch_size

위 구현에서는 원-핫 인코딩일 때 t가 0인 원소는 교차 엔트로피 오차도 0이기 때문에 그 계산은 무시해도 좋다는 점이 핵심입니다.

다시 말해, 정답에 해당하는 신경망의 출력만으로 교차 엔트로피 오차를 계산할 수 있다는 이야기가 됩니다.

그래서 원-핫 인코딩 시 t * np.log(y)였던 부분을 레이블 표현일 때는 np.log(y [np.arange(batch_size), t]) 로 구현합니다.

 

np.log(y[np.arange(batch_size), t]를 간단히 설명하면, np.arange(batch_size)는 0부터 batch_size - 1까지의 배열을 생성합니다.

즉, batch_size가 5라면 np.arange(batch_size)는 [0, 1, 2, 3, 4] 같은 Numpy 배열을 생성하게 되고, t에는 레이블이 [2, 7, 0, 9 ,8]와 같이 저장되어 있으므로 y [np.arange(batch_size), t]는 각 데이터의 정답 레이블에 해당하는 신경망의 출력을 추출합니다.

위 예에서의 출력은 [y [0, 2], y [1, 7], y [2, 0], y [3, 9], y [4, 8]]인 Numpy 배열이 만들어지는 것을 알 수 있습니다.

마지막 신경망 학습을 위한 경사법을 위한 미분은 다음 시간에 다뤄보도록 하겠습니다!

 

728x90
반응형
LIST

+ Recent posts