728x90
반응형

 이전 포스트에서 모델을 생성해보고, 생성된 모델의 정보를 살펴보았다. 이번 포스트에서는 모델을 컴파일에 대해 학습해보도록 하겠다.

 

 

모델 컴파일

0. 이전 코드 정리

# Import Module
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.layers import (Dense, BatchNormalization, Dropout, Flatten)
from tensorflow.keras.datasets.mnist import load_data

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Dataset 준비
(train_images, train_labels), (test_images, test_labels)= load_data()

# 무작위로 샘플 추출
np.random.seed(1234)
index_list = np.arange(0, len(train_labels))
valid_index = np.random.choice(index_list, size = 5000, replace = False)

# 검증셋 추출
valid_images = train_images[valid_index]
valid_labels = train_labels[valid_index]

# 학습셋에서 검증셋 제외
train_index = set(index_list) - set(valid_index)
train_images = train_images[list(train_index)]
train_labels = train_labels[list(train_index)]

# min-max scaling
min_key = np.min(train_images)
max_key = np.max(train_images)

train_images = (train_images - min_key)/(max_key - min_key)
valid_images = (valid_images - min_key)/(max_key - min_key)
test_images = (test_images - min_key)/(max_key - min_key)
# 모델 생성
model = keras.models.Sequential()
model.add(keras.layers.Flatten(input_shape=[28, 28], name="Flatten"))
model.add(Dense(300, activation="relu", name="Hidden1"))
model.add(Dense(200, activation="relu", name="Hidden2"))
model.add(Dense(100, activation="relu", name="Hidden3"))
model.add(Dense(10, activation="softmax", name="Output"))

 

 

 

 

1. 모델 컴파일

# 모델 컴파일
opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer = opt,
              loss = "sparse_categorical_crossentropy",
              metrics = ["accuracy"])
  • 모델을 어떤 방식으로 학습시킬지 결정하는 과정이다.
  • 모델 컴파일에서 지정하는 주요 항목은 최적화 방법인 옵티마이저(Optimizer)와 손실 함수(loss)이다.
  • 추가로, 훈련과 평가 시 계산할 지표를 추가로 지정할 수 있다(metrics).

 

 

 

 

2. Optimizer

  • 최적화 방법인 Optimizer는 경사 하강법(GD)을 어떤 방법으로 사용할지를 정한다고 생각하면 된다.
  • Optimizer를 정하는 이유는 Optimizer 방법을 무엇을 선택하느냐에 따라 최적해를 찾아가는 속도가 크게 달라진다.
  • 경사 하강법(GD)은 기본적으로 4가지 문제가 존재하며, 이는 다음과 같다.
    (좀 더 자세히 알고 싶은 사람은 다음 포스팅: "머신러닝-6.1. 최적화(2)-경사하강법의 한계점"을 참고하기 바란다.)
  1. 데이터가 많아질수록 계산량이 증가함
  2. Local minimum 문제
  3. Plateau 문제
  4. Zigzag 문제
  • 위 문제들을 간단하게 말하면, 경사 하강법이 가진 구조적 단점으로 인해, 최적해를 제대로 찾아가지 못하거나, 찾는 속도가 늦어진다는 것이다.
  • 이를 해결하기 위해선 데이터셋에 맞는 Optimizer를 사용해야 하며, 단순하게 가장 많이 사용하는 Optimizer가 Adam이므로, Adam을 사용하는 것은 그다지 추천할 수 없는 방법이다.
# Optimizer는
model.compile(optimizer = "Adam",
              loss = "sparse_categorical_crossentropy",
              metrics = ["accuracy"])
  • 위 방법으로 Optimizer를 하게 되면, 코드는 단순하지만, 학습률, Momentum과 같은 Optimizer 고유의 하이퍼 파라미터를 수정할 수 없다. 
# 모델 컴파일
opt = keras.optimizers.Adam(learning_rate=0.005)
model.compile(optimizer = opt,
              loss = "sparse_categorical_crossentropy",
              metrics = ["accuracy"])
  • 위 방법으로 Optimizer를 잡아줘야, 각종 하이퍼 파라미터를 수정할 수 있다.
  • keras.optimizers. 뒤에 원하는 optimizer를 넣으면 된다.

 

 

 

 

3. Optimizer의 종류

  • Optimizer는 기본적으로 SGD를 기반으로 하므로, 확률적 추출을 통해 경사 하강법을 시행한다.
  • Optimizer는 크게 Momentum 방식(관성 부여)과 Adagrad 방식(상황에 따른 이동 거리 조정)으로 나뉜다.
  • Momentum 방식과 Adagrad 방식을 하나로 합친 방법이 Adam과 Nadam이다.
  • 다른 Optimizer를 사용함으로 인해 최적해를 찾아가는 방법이 달라지게 되고, 그로 인해 학습 속도가 바뀌게 된다.
  • Local minimum 문제는 무작위 가중치 초기화로 인해 발생할 가능성이 매우 낮다.
  • 단순하게 Adam만 고집하지 말고, 여러 Optimizer를 사용하길 바란다.
  • Optimizer와 경사하강법에 대한 상세한 설명을 보고자 한다면, 다음 포스트를 참고하기 바란다.
  • 참고: "머신러닝-6.0. 최적화(1)-손실함수와 경사하강법"

Optimizer별 최적해 수렴 속도 차이

  • 별이 최적해라고 할 때, 각종 Optimizer가 최적해를 찾아가는 방식을 시각화한 것이다.
  • 해가 n개이므로, 파라미터는 평면이 아니라 입체이며, 이 입체를 이해하기 쉽도록 2차원 등고선으로 그린 것이다.

  • 말안장 그림이라 하여, 3차원으로 최적해를 찾아가는 과정을 그린 것이다.
  • SGD는 지역 최솟값(Local minimum)에 빠져 최적해를 찾아가지 못하였다.
  • 위 두 그림의 출처는 다음과 같으며, 보다 자세한 설명을 보고 싶은 경우 해당 사이트를 참고하기 바란다.
  • ruder.io/optimizing-gradient-descent/
 

An overview of gradient descent optimization algorithms

Gradient descent is the preferred way to optimize neural networks and many other machine learning algorithms but is often used as a black box. This post explores how many of the most popular gradient-based optimization algorithms such as Momentum, Adagrad,

ruder.io

 

 

 

 

 

4. loss

  • 손실 함수는 데이터셋과 라벨 데이터의 생김새에 따라 사용하는 방법이 달라진다.
  • 기본적으로 연속형 데이터를 대상으로는 제곱 오차(SE)에서 파생된 기법을 사용하며, 범주형 데이터를 대상으로는 크로스 엔트로피 오차(CEE)에서 파생된 기법을 사용한다.
  • 클래스의 수나 Label의 형태에 따라 사용하는 방법이 조금씩 달라진다.
  • 가장 많이 사용되는 손실 함수의 사용 예는 다음과 같다.
데이터 형태 Label의 형태 손실 함수
범주형 클래스 2개 binary_crossentropy
클래스
3개 이상
원-핫 벡터 categorical_crossentropy
단순 범주형 sparse_categorical_crossentropy
연속형 mean_squared_error
(=mse)
mean_squared_logarithmic_error
(=msle)
 

Module: tf.keras.losses  |  TensorFlow Core v2.4.1

Built-in loss functions.

www.tensorflow.org

 

 

 

 

 

5. metrics

  • 평가 기준으로 모델의 학습에는 영향을 미치지 않으나, 학습 중에 제대로 학습되고 있는지를 볼 수 있다.
  • metrics에 무엇을 넣느냐에 따라 학습 시, 히스토리에 나오는 출력 Log가 달라지게 된다.
  • 일반적으로 accuracy 즉, 정확도가 사용된다.
  • 이 역시 데이터 셋에 따라 바뀌며, 손실 함수와 유사한 것을 선택하면 된다.
  • metrics에 사용하는 하이퍼 파라미터는 아래 사이트를 참고하기 바란다.
  • keras.io/api/metrics/
 

Keras documentation: Metrics

Metrics A metric is a function that is used to judge the performance of your model. Metric functions are similar to loss functions, except that the results from evaluating a metric are not used when training the model. Note that you may use any loss functi

keras.io

 

 

 

 

 지금까지 Compile을 하는 방법에 대해 알아보았다. Compile은 일반적으로 사용하는 기법을 사용하여도 큰 차이를 느끼지 못할 수도 있으나, 제대로 모델을 학습시키기 위해선 데이터의 형태에 맞는 하이퍼 파라미터를 잡아주는 것이 좋다.

 다음 포스트에서는 모델을 실제로 학습시켜보고, 그 Log를 시각화하여 최적의 Epochs을 선택하는 방법에 대해 학습해보겠다.

 

728x90
반응형
728x90
반응형

 이전 포스트에서는 이진 분류에서 주로 사용되는 이진 교차 엔트로피 오차(Binary Cross Entropy Error, BCEE)에 대해 학습해보았다.

 이번 포스트에서는 다중 분류에서 사용되는 범주형 교차 엔트로피 오차(Categorical Cross Entropy error)에 대해 학습해보겠다.

 

 

 

범주형 교차 엔트로피 오차(Categorical Cross Entropy Error, CCEE)

  • 범주형 교차 엔트로피 오차는 클래스가 3개 이상인 데이터를 대상으로 사용하는 손실함수다.
  • CCEE는 주로, 소프트맥스(Softmax) 함수를 활성화 함수로 하여 사용된다.
  • 출력층의 노드 수는 클래스의 수와 동일하다.
  • 실제 데이터인 라벨은 원-핫 벡터로 구성되어 있다.
  • 출력된 벡터는 각 클래스에 속할 확률이 나오며, 총합은 1이다.
  • 처음 학습하였던 교차 엔트로피 오차를 N개의 데이터 셋에 대해 1개의 스칼라를 추출하는 방법이 CCEE다.

 

 

 

 

1.  범주형 교차 엔트로피 오차의 공식

  • 범주형 교차 엔트로피 오차 공식은 다음과 같다.

$$ Loss = -\frac{1}{N}\sum_{j=1}^{N}\sum_{i=1}^{C}t_{ij}log(y_{ij}) $$

  • 위 공식은 지금까지 잘 따라왔다면, 따로 풀이가 필요 없을 정도로 단순한 공식이다.
  • 앞서 학습하였던 교차 엔트로피 오차 공식을 데이터셋의 수 $N$개만큼 합하여 평균을 낸 것이다.
  • 이진형 교차 엔트로피 오차와의 차이는 출력층의 노드 수가 1개인지 $m$개$(m\geq3)$인지로, 출력층에서 데이터 하나당 클래스 수만큼의 원소를 가진 벡터가 나오므로, 각 벡터의 교차 엔트로피 오차들의 평균을 구하는 것이다.
  • 바로 구현으로 넘어가 보자.

 

 

 

 

2. 구현해보자.

>>> import numpy as np

>>> def CCEE(predict, label):
    
>>>     delta = 1e-7
>>>     log_pred = np.log(predict + delta)
    
>>>     return -(np.sum(np.sum(label * log_pred, axis = 1)))/label.shape[0]
  • np.sum() 함수를 보면 axis = 1이라는 것이 있다. 이는 0으로 설정하면, 열을 기준으로 해당 함수를 실행하고, 1으로 설정하면, 행을 기준으로 함수를 실행한다.
  •  이 부분은 헷갈리기 좋으므로, 익숙해지기 전이라면, 작게 데이터를 만들어서 한번 보고 실행해보는 것을 추천한다.
>>> predict = np.array([[0.1, 0.7, 0.05, 0.05, 0.1],
>>>                     [0.05, 0.0, 0.85, 0.1, 0.0],
>>>                     [0.05, 0.8, 0.05, 0.1, 0.1],
>>>                     [0.75, 0.15, 0.05, 0.05, 0.0],
>>>                     [0.0, 0.1, 0.1, 0.0, 0.8]])
                    
>>> label = np.array([[0, 1, 0, 0, 0],
>>>                   [0, 0, 1, 0, 0],
>>>                   [0, 1, 0, 0, 0],
>>>                   [1, 0, 0, 0, 0],
>>>                   [0, 0, 0, 0, 1]])
>>> CCEE(predict, label)
0.25063248093584295
  • 범주형 교차 엔트로피 오차의 구현은 아주 단순하다. 
  • 위에서 보듯, 교차 엔트로피는 각 벡터에 대해 일어나고, 교차 엔트로피 오차의 평균을 만들면 된다.
  • 실제 데이터와 예측 데이터를 아주 가깝게 해 보자.
>>> predict = np.array([[0.1, 0.85, 0.0, 0.05, 0.0],
>>>                     [0.05, 0.0, 0.9, 0.05, 0.0],
>>>                     [0.0, 0.95, 0.0, 0.1, 0.04],
>>>                     [0.9, 0.0, 0.05, 0.05, 0.0],
>>>                     [0.0, 0.1, 0.0, 0.0, 0.9]])

>>> label = np.array([[0, 1, 0, 0, 0],
>>>                   [0, 0, 1, 0, 0],
>>>                   [0, 1, 0, 0, 0],
>>>                   [1, 0, 0, 0, 0],
>>>                   [0, 0, 0, 0, 1]])

>>> CCEE(predict, label)
0.10597864292305711
  • 범주형 교차 엔트로피 오차 역시 편차가 줄어들수록 출력 값이 0에 가까워지는 것을 볼 수 있다.
  • 반대로 실제 데이터와 예측 데이터의 차이를 크게 만들어보자.
>>> predict = np.array([[0.1, 0.6, 0.2, 0.05, 0.05],
>>>                     [0.1, 0.2, 0.5, 0.2, 0.0],
>>>                     [0.1, 0.6, 0.0, 0.1, 0.2],
>>>                     [0.4, 0.0, 0.1, 0.3, 0.2],
>>>                     [0.05, 0.1, 0.05, 0.2, 0.6]])

>>> label = np.array([[0, 1, 0, 0, 0],
>>>                   [0, 0, 1, 0, 0],
>>>                   [0, 1, 0, 0, 0],
>>>                   [1, 0, 0, 0, 0],
>>>                   [0, 0, 0, 0, 1]])

>>> CCEE(predict, label)
0.6283827667464331
  • 앞서 교차 엔트로피 오차에서도 이야기하였지만, 원-핫 벡터에서 1에 해당하는 위치의 데이터만 가지고 연산을 한다.
  • 각 행의 총합은 1이다.

 

 

 

 

 지금까지 가장 기본이 되는 손실함수인 제곱오차(SE)에서 파생된 손실함수인 오차제곱합(SSE), 평균제곱오차(MSE), 평균제곱근오차(RMSE), 교차 엔트로피 오차에서 파생된 이진 교차 엔트로피 오차(BCEE), 범주형 교차 엔트로피 오차(CCEE)에 대하여 학습해보았다.

 이 밖에도 Huber나 Sparse Categorical Crossentropy 등이 여러 손실함수가 있으나, 이들까지 하나하나 다루다간 끝이 나지 않을지도 모른다. 이밖에 다른 손실함수에 대해 학습해보고자 한다면, TensorFlow의 keras에서 손실함수 API를 정리해놓은 아래 홈페이지를 참고하기를 바란다.

www.tensorflow.org/api_docs/python/tf/keras/losses

 

Module: tf.keras.losses  |  TensorFlow Core v2.4.1

Built-in loss functions.

www.tensorflow.org

 다음 포스트에서는 신경망의 핵심 알고리즘인 경사법에 대해 학습해보도록 하자.

728x90
반응형
728x90
반응형

 이전 포스트에서는 범주형 데이터를 분류하는데 주로 사용되는 손실함수인 교차 엔트로피 오차와 그 근간이 되는 정보 이론에서의 엔트로피가 무엇인지를 알아보았다.

 이번 포스트에서는 교차 엔트로피 오차 중에서도 이진 분류를 할 때, 주로 사용되는 이진 교차 엔트로피 오차에 대해 학습해보도록 하겠다.

 

 

이진 교차 엔트로피 오차(Binary Cross Entropy Error)

  • 교차 엔트로피 오차는 나누고자 하는 분류가 몇 개인지에 따라 사용하는 손실함수가 바뀌게 된다.
  • 이는 사용되는 활성화 함수가 다르기 때문으로, 범주가 2개인 데이터는 시그모이드(Sigmoid) 함수를 사용하여 0~1 사이의 값을 반환하거나, 하이퍼볼릭 탄젠트(Hyperbolic Tangent) 함수를 사용하여 -1~1 사이의 값을 반환한다. 이 두 활성화 함수 모두 출력값이 단 하나의 스칼라 값이다.
  • 반면에 범주가 3개 이상이라면, 총 합 1에 각 클래스에 속할 확률을 클래스의 수만큼 반환하는 소프트맥스(Softmax) 함수를 사용하여 클래스 수만큼의 원소가 들어있는 배열을 반환하므로, 이에 대한 평가 방법이 달라져야 한다.
  • 이진 교차 엔트로피 오차는 로그 손실(Log loss) 또는 로지스틱 손실(Logistic loss)라 불리며, 주로 로지스틱 회귀의 비용 함수로 사용된다.

 

 

 

 

1. 이진 교차 엔트로피 오차의 공식

  • 이진 교차 엔트로피 오차의 공식은 다음과 같다.

$$ Loss = -\frac{1}{N}\sum_{i=1}^{N}(y_i*ln\hat{y_i} + (1-y_i)*ln(1-\hat{y_i})) $$

  • $\hat{y_i}$는 예측값이며, $y_i$는 실제값이다.
  • 얼핏 보면, 꽤 어려워보이는데 앞서 우리가 학습했던 내용을 기반으로 보면 상당히 단순한 공식이다.
  • 먼저 앞서 학습헀던 공식들을 조금 더 이해해보자.
  • 엔트로피 공식은 다음과 같다.

$$H(X) = - \sum_{x}P(x)lnP(x) $$

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

$$H(P, Q) = - \sum_{x}P(x)lnQ(x) $$

  • 위 두 공식에서 엔트로피 공식과 교차 엔트로피 공식의 차이는 실제값($P(x)$)과 타깃이 되는 예측값($Q(x)$)의 정보량 비율 합으로 구해지는 것을 알 수 있다.
  • 여기서, 교차 엔트로피 오차는 분류할 클래스의 수가 $N>2$인 정수이므로, 클래스별 확률이 다 달랐으나, 이진 교차 엔트로피 오차는 클래스가 "y=0"와 "y=1" 단 두 가지만 존재하는 것을 알 수 있다.

$$ p = [y, 1-y] $$

$$ q = [\hat{y}, 1-\hat{y}] $$

  • 그렇다면, $y=0$의 교차 엔트로피 공식을 만들어보자.

$$ H(y)= -\sum_{i=1}^{N}(y_i*ln\hat{y_i}) $$

  • $y=1$의 교차 엔트로피 공식을 만들어보자.

$$ H(y-1)= -\sum_{i=1}^{N}((1-y_i)*ln(1-\hat{y_i})) $$

  • 밑과 위가 같은 시그마끼리는 서로 합칠 수 있다.

$$ H(y) + H(y-1)= -\sum_{i=1}^{N}(y_i*ln\hat{y_i} + (1-y_i)*ln(1-\hat{y_i})) $$

  • 여기서 $N$개의 학습 데이터 전체에 대한 교차 엔트로피를 구해주는 것이므로, 평균으로 만들어 값을 줄여주자!

$$ Loss = -\frac{1}{N}\sum_{i=1}^{N}(y_i*ln\hat{y_i} + (1-y_i)*ln(1-\hat{y_i})) $$

  • 앞서, 오차제곱합(SSE)와 평균제곱오차(MSE)에 대해 보았을 텐데, 합은 데이터의 수가 많아질수록 증가하므로, 데이터의 수로 나눠 평균으로 만들어야 이를 보정해줄 수 있다.
  • 여기서 데이터의 수는 입력 값의 벡터 크기가 아니라, Input되는 데이터의 수를 말한다.
  • 이진 교차 엔트로피 오차는 출력층의 노드 수를 하나로 하여 출력값을 하나로 받으므로, 실제값(Label)과 예측값(predict) 모두 하나의 스칼라 값이다.
  • 왜 교차 엔트로피 오차(CEE)에서는 왜 N으로 나눠주지 않았는지 의문이 들 수 있는데, 그 이유는 교차 엔트로피 오차는 하나의 데이터에 대해서만 실시한 것이기 때문이다.
  • 교차 엔트로피 오차(CEE)를 N개의 데이터에 대해 실시하면 범주형 교차 엔트로피 오차(Categorical Cross Entropy Error)가 된다.

 

 

 

 

2. 구현해보자!

  • 이진형 교차 엔트로피 에러(BCEE)는 앞서 학습 했던, 교차 엔트로피 에러와 꽤 유사하다.
>>> import numpy as np

>>> def BCEE(predict, label):
    
>>>     delta = 1e-7
>>>     pred_diff = 1 - predict
>>>     label_diff = 1 - label
>>>     result = -(np.sum(label*np.log(predict+delta)+label_diff*np.log(pred_diff+delta)))/len(label)
    
>>>     return result
>>> predict = np.array([0.8, 0.1, 0.05, 0.9, 0.05])
>>> label = np.array([1, 0, 0, 1, 0])
>>> BCEE(predict, label)
0.10729012273129139
  • 위 데이터를 보면 총 5개의 데이터 셋에 대한 이진 분류 결과를 보았다.
  • 이번에는 예측값과 실제 데이터를 더 유사하게 하여 결과를 내보자.
>>> predict = np.array([0.95, 0.05, 0.01, 0.95, 0.01])
>>> label = np.array([1, 0, 0, 1, 0])
>>> BCEE(predict, label)
0.03479600741200121
  • 보다 0에 가까워진 것을 알 수 있다.
  • 이번에는 좀 멀게 만들어보자.
>>> predict = np.array([0.30, 0.40, 0.20, 0.65, 0.2])
>>> label = np.array([1, 0, 0, 1, 0])
>>> BCEE(predict, label)

 

 

 

 

 지금까지 이진 교차 엔트로피 오차(Binary Cross Entropy Error, BCEE)에 대해 학습해보았다. BCEE는 앞서 봤던 CEE를 단순하게 "y=0"일 사건과 "y=1"일 사건에 대한 교차 엔트로피 오차 합의 평균을 낸 것으로, 큰 차이가 없다는 것을 알 수 있다.

 다음 포스트에서는 이진 교차 엔트로피 오차에 대응하는 다중 분류에 사용되는 범주형 교차 엔트로피 오차(Categorical Cross Entropy Error)에 대해 학습해보도록 하겠다.

728x90
반응형
728x90
반응형

 지난 포스트까지 제곱오차(SE)에서 파생된 "오차제곱합(SEE), 평균제곱오차(MSE), 평균제곱근오차(RMSE)"에 대하여 알아보았다. 해당 개념들이 연속형 데이터를 대상으로 하는 회귀분석의 모델 적합도를 평가할 때, 사용돼 듯, 머신러닝에서도 해당 손실함수는 연속형 데이터를 대상으로 사용된다.

 이번 포스트에서는 범주형 데이터를 대상으로 하는 손실함수의 기반이 되는 교차 엔트로피 오차에 대해 알아보도록 하겠다.

 

 

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

  • 연속형 데이터의 대표적인 손실함수인 제곱오차 시리즈와 달리 교차 엔트로피 오차(CEE)는 범주형 데이터를 분류할 때 주로 사용한다.
  • 교차 엔트로피 오차라는 단어를 풀이해보면, 서로 다른 엔트로피를 교차하여 그 오차를 본다는 말일 텐데, 그렇다면 엔트로피란 무엇일까?

 

 

 

 

1. 정보 이론에서 엔트로피란?

 엔트로피는 열역학과 정보 이론에서 사용되는 용어로, 현재 학습하는 분야는 열역학이 아닌 IT분야이므로, 정보 이론에서 엔트로피가 어떻게 사용되는지를 위키피디아를 참고해 알아보도록 하겠다.
ko.wikipedia.org/wiki/%EC%A0%95%EB%B3%B4_%EC%97%94%ED%8A%B8%EB%A1%9C%ED%94%BC

 

정보 엔트로피 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 2 섀넌의 엔트로피: 2 개의 공정한 동전을 던질 때 정보 엔트로피는 발생 가능한 모든 결과의 개수에 밑이 2 인 로그를 취한 것과 같다. 2 개의 동전을 던지면 4

ko.wikipedia.org

 

A. 정보 이론

  • 어떤 사람이 정보를 많이 알수록, 새롭게 알 수 있는 정보의 양(Information content)은 감소한다.

  • 위 그래프를 참고해보자, A라는 사건이 발생할 확률이 매우 크다면, 우리는 그 사건 A가 발생한다 할지라도 대수롭지 않게 느낀다. 우리가 대수롭지 않게 느낀다는 말은 사건 A에서 발생한 정보량이 작다는 소리다.
  • 반대로, B라는 사건은 발생할 확률이 매우 낮다면, 그 사건이 발생했을 때, 발생하는 정보량은 매우 크다.
  • 이를 이해하기 쉽게 예를 들자면, 정보를 "놀람의 정도"라고 생각해보자, 만약 당신이 동전을 던져서 앞면이 나온다면, 당신은 대수롭지 않게 느낄 것이다. 왜냐하면, 동전을 던졌을 때, 앞면이나 뒷면이 나올 확률은 같고, 평범하게 일어날 수 있는 일이기 때문이다. 때문에 당신에게 발생할 확률이 매우 높은 동전 던지기는 당신에게 매우 작은 정보를 준다.
  • 그러나, 만약 당신이 로또 1등에 당첨되었다고 해보자. 당신은 아마 기절할 정도로 놀랄지도 모른다. 왜냐면, 로또 1등에 당첨될 확률은 상상할 수 없을 정도로 낮기 때문이며, 이 상황에서 당신이 받은 정보량은 매우 크다고 할 수 있다.
  • 즉, 흔히 볼 수 있는 사건(확률이 낮은 사건)일수록, 정보량이 낮고, 매우 희귀하게 발생하는 사건은 정보량이 매우 크다.

 

B. 엔트로피

  • 엔트로피는 사건 A를 반복 실행하였을 때, 얻을 수 있는 "평균 정보량"으로, 어떤 사건에 대한 "정보량의 기댓값"이다.
  • 사건 A를 반복 실행하여 얻는 이유는 경험적 확률과 수학적 확률의 차이 때문으로, 정육면체 주사위를 던져서 1이 나올 확률은, 실행 횟수가 낮다면, 그 확률이 불규칙적으로 나올 수 있기 때문이다.
  • 간단히 말해서 주사위를 10번 던졌을 때, 우연히 1이 6번이나 나올 수 있고, 그로 인해 확률을 $\frac{6}{10}$으로 생각할 수 있다. 이를 많이 실행한다면, 그 확률은 $\frac{1}{6}$에 수렴하게 될 것이다.
  • 엔트로피의 크기는 정보량의 크기로, 예를 들어, 동전을 던져 앞면이 나올 확률과 주사위를 던져 1이 나올 확률을 비교해보면, 주사위 던지기의 확률 $\frac{1}{6}$이고, 동전 던지기의 확률은 $\frac{1}{2}$이므로, 주사위 던지기의 확률이 더 낮아 엔트로피가 더 높다.
  • 그러나, 주사위가 1, 2, 3, 4가 나올 확률이라면, 주사위 던지기 확률이 $\frac{4}{6}$이므로, 동전을 던져 앞면이 나올 확률의 엔트로피가 더 크다.
  • 즉, 엔트로피가 크다는 것은 사건 A의 확률이 낮다는 것으로, 엔트로피는 "어떤 상태에서의 불확실성"이라 할 수도 있다. 예측하기가 어려운 사건일수록 정보량이 많아지고, 엔트로피도 커지게 된다.

 

 

 

 

2. 엔트로피 공식

  • 위 내용을 간추려 이야기해보면, 엔트로피는 사건 A가 발생할 확률이 낮으면 낮을수록 커지는 존재로, 단순하게 확률이라고 생각해도 큰 문제가 없다.
  • 그렇다면, 엔트로피 공식이 어떻게 나오게 되었는지 보도록 하자.

정보량: $I(x) = ln(\frac{1}{p(x)}) = - ln(p(x)) $

엔트로피: $H(X) = -E[I(x)] = E[ln(\frac{1}{p(X)})] = -  \int_{E}p(x)ln(p(x))dx $

  • 만약 표본 공간 E가 이산 공간 $E = {x_1,...,x_n}$이라면, 르베그적분은 합이 되며, 정보 엔트로피는 다음과 같다.

엔트로피: $ H(X) = -\sum_{i}p(x_i)ln(p(x_i))$

  • 엔트로피는 정보량에 영향을 받으며, 정보량은 확률에 영향을 받는다.
  • 정보량은 역수를 취한 확률$\frac{1}{p(x)}$에 자연로그($log_e=ln$)를 사용하는데, 2진수 데이터가 대상인 경우, 밑이 $e$가 아니라 2인 로그를 사용하기도 한다.
    (밑이 2이면, 단위는 비트(bit)가 되고, 자연로그이면 단위는 내트(nat)가 된다.)
  • 정보량에 로그가 사용된 이유는 다음과 같다.

 

정보량에 로그가 사용되는 이유

 정보량은 다음과 같은 성질을 가져야 한다.

  1. 정보량은 항상 0보다 크다.
  2. 항상 발생하는 사건은 정보량이 0이다.
  3. 자주 일어나는 사건일수록 정보량은 0에 가깝다.
  4. 독립적인 사건들의 정보량 합은 각 사건의 합이어야 한다. 
  • 위 4가지 조건을 모두 만족하는 것이 로그인데, 이산확률분포에서의 확률은 $0< P(X) \leq 1$이므로, 정보량 $f(x) = -log(p(x)) \geq 0$을 만족하며, $p(x) = 1$이면, $f(x) = 0$이 된다.
  • 또, 독립 사건에 대하여, 사건 A가 일어날 확률 $p_A$와 사건 B가 일어날 확률 $p_B$가 동시에 일어날 확률인 두 사건의 교집합은 $p_{A}p_{B}$인데, 이를 로그 함수에 넣어보면, 로그의 성질에 의해 쉽게 분리가 된다.

$$ I(X_1, X_2) = -log(p_{1}p_{2}) = -log(p_1) - log(p_2) = I(X_1) + I(X_2)$$

  • 로그의 성질로 인해, 독립 사건인 A와 B이 동시에 발생한 정보량은 각 사건이 발생한 정보량의 합과 같다.

 

 

 

 

3. 교차 엔트로피 오차(Cross Entropy Error, CEE)의 공식

  • 교차 엔트로피 오차는 위 엔트로피 공식을 기반으로 각 사건이 발생할 확률이 몇 가지인지에 따라 조금씩 공식이 바뀐다.
  • 먼저 교차 엔트로피 오차의 공식을 보도록 하자.

$$H(P, Q) = -\sum_{x}P(x)lnQ(x)$$

  • 여기서 $Q(x)$는 신경망의 출력값, $P(x)$는 정답 레이블인데, 정답 레이블은 정답만 1이고 나머지는 0인 원-핫 벡터를 사용한다.
  • 원-핫 벡터는 이전 포스트인 "머신러닝-5.0. 손실함수(1)-제곱오차와 오차제곱합"에서 다뤘으므로, 넘어가도록 하겠다. gooopy.tistory.com/60?category=824281
 

머신러닝-5.0. 손실함수(1)-제곱오차와 오차제곱합

 이전 포스트에서 신경망 학습이 어떠한 원리에 의해 이루어지는지 간략하게 살펴보았다. 이번 포스트에서는 제곱 오차(Square Error)와 제곱 오차를 기반으로 만든 손실 함수 오차제곱합(SSE)에 대

gooopy.tistory.com

  • $P(X)$는 원-핫 벡터이므로, 정답 1이 있는 위치 $m$만 $1*lnQ(m)$이 나오게 되고 나머지는 $0*lnQ(n) = 0$으로 나와, 정답 위치에 해당하는 $lnQ(m)$의 값이 교차 엔트로피 오차로 출력되게 된다.
  • 이 부분이 앞서 학습한, 오차 제곱(SE) 시리즈와의 차이점인데, 오차 제곱 시리즈는 회귀식처럼 값의 흩어진 정도를 사용한다면, 교차 엔트로피 오차는 정답 레이블에서 정답에 해당하는 위치의 확률의 로그 값이 출력되게 된다.
  • 예를 들어 분류가 4개인 데이터를 사용한다고 해보자.

$$ label = [0, 0, 1, 0] $$

$$ pred = [0.1, 0.2, 0.6, 0.1] $$

  • 위 데이터를 교차 엔트로피 오차에 넣으면 다음과 같다.

$$ E = -(0*ln(0.1) + 0*ln(0.2) + 1*ln(0.6) + 0*ln(0.1)) = - ln(0.6) = 0.51$$

  • 이를 통해 교차 엔트로피 오차는 특정 클래스에 속할 정보량을 이용한다는 것을 알 수 있다.
  • 교차 엔트로피 오차 역시 정보량이 0에 가까워져 발생 확률이 1에 가깝게 만드는 것을 목적으로 한다.

 

 

 

 

4. 구현해보자!

>>> import numpy as np

>>> def CEE(predict, label):
>>>     delta = 1e-7
>>>     return -np.sum(label * np.log(predict + delta))
  • 로그 함수는 $x=0$에서 무한대로 발산하는 함수이기 때문에 $x=0$이 들어가서는 안된다.
  • 그러나, 원-핫 벡터는 정답 위치를 제외한 나머지 원소들이 모두 0이므로, 매우 작은 값을 넣어줘서 - 무한대가 나오는 것을 막아줘야 한다.

# 실제 데이터와 예측 데이터가 비슷하게 나온 경우
>>> label = np.array([0, 0, 1, 0, 0])
>>> predict = np.array([0.1, 0.1, 0.6, 0.1, 0.1])

>>> CEE(predict, label)
0.510825457099338


# 실제 데이터와 예측 데이터가 다르게 나온 경우
>>> label = np.array([0, 0, 1, 0, 0])
>>> predict = np.array([0.05, 0.4, 0.3, 0.2, 0.05])

>>> CEE(predict, label)
1.2039724709926583
  • 위 결과를 보면, 오차제곱(SE) 시리즈의 오차제곱합(SSE), 평균제곱오차(MSE), 평균제곱근오차(RMSE)와 마찬가지로 가중치로 인해 나온 예측값이 실제값과 얼마나 가까운지를 하나의 스칼라 값으로 출력한 것을 알 수 있다.

 

 

 

 

 지금까지 정보 이론에서 엔트로피가 무엇이고, 어떻게 교차 엔트로피 오차(CEE)라는 개념이 나오게 되었는지 알아보았다. 엔트로피에 대해 아주 단순하게 말하면, 확률을 조금 다르게 표현한 것이라 생각해도 무방하다. 

 앞서 학습하였던 오차제곱합(SEE), 평균제곱오차(MSE), 평균제곱근오차(RMSE)는 실제값과 예측값의 편차를 이용해서 가중치를 평가하므로, 연속형 데이터에 걸맞았으나, 교차 엔트로피 오차는 이산형 데이터임을 가정하고, 자신이 원하는 클래스에 해당하는 예측값이 나오는 확률을 이용해(엄밀히 따지면 확률과 약간 다르지만! 정보량이던 엔트로피던 확률의 영향을 받는다!) 가중치를 평가하였다.

 다음 포스트에서는 이진 교차 엔트로피 오차(Binary Cross Entropy Error)에 대해 알아보도록 하겠다.

728x90
반응형
728x90
반응형

 지난 포스트에서 "제곱오차(SE) > 오차제곱합(SSE) > 평균제곱오차(MSE)" 순으로 알아보았다. 이번 포스트에서는 SSE의 또 다른 파생 형제인 평균제곱근오차(RMSE)에 대해 알아보도록 하겠다.

 

 

평균제곱근오차(Root Mean Square Error, RMSE)

  • 평균제곱오차(MSE)는 "각 원소의 평균까지의 편차 제곱의 평균"인 분산과 굉장히 유사한 개념이다. 
  • 평균제곱오차 역시 분산과 마찬가지로 편차 제곱 합을 하였기 때문에 이것이 실제 편차라 보기 힘들며, 그로 인해 분산과 표준편차처럼 평균제곱오차에도 제곱근(Root)을 씌운 것이 평균제곱근오차다.
  • 그 공식은 다음과 같다.

$$RMSE = \sqrt{\frac{1}{n}\sum_{k}^{n}(y_k - \hat{y_k})^2}$$

 

 

 

 

1. 어째서 평균제곱근오차(RMSE)를 사용하는 것일까?

  • 분산 대신 표준편차를 사용하는 이유와 비슷한데, 평균제곱오차는 실제 오차의 편차 평균이 아니라, 오차의 편차 제곱의 평균이기 때문에, 실제 편차를 반영한다고 볼 수 없다.
  • 이는 평균제곱오차의 장점이자 단점으로 "큰 오류를 작은 오류에 비해 확대시킨다"는 것을 제곱근을 사용함으로써 어느 정도 보정할 수 있다.
  • 예를 들어, (1-0.01)과 (1.0.95)의 차이와 (1-0.01)^2과 (1-0.95)^2의 차이를 비교해보자.
>>> print((1-0.01) - (1-0.95))
>>> print(np.round((1-0.01)**2 - (1-0.95)**2, 3)) 
0.94
0.978
  • 위를 보면, 편차의 제곱을 하는 것이 그렇지 않은 것보다 차이가 크게 확대되는 것을 알 수 있다.
  • 때문에, 이를 보정해주기 위해  제곱근(Root)을 사용하는 것이다.

 

  • 물론, 제곱근을 사용한다고 하여, 평균절대값오차(MAE)에 비해 실제 편차라고 할 수는 없으나, MSE가 편차를 제곱시켜, 큰 오류를 작은 오류보다 확대시킨다는 장점은 제곱근을 사용하여도 유지되기 때문에 오차의 존재를 인지하는 데엔 더욱 도움이 된다.
  • 평균절대값오차(MAE)0에서 미분이 불가능하기 때문에 경사하강법을 이용해 최적의 값에 가까워지더라도 이동거리가 일정해 최적 값에 수렴하지 않으므로, 개인적으로는 추천하지 않는다.
  • 즉, "평균제곱근오차(RMSE)는 제곱근을 사용함으로써 평균제곱오차(MSE)의 왜곡을 줄여주기 때문에 오차를 보다 실제 편차와 유사하게 볼 수 있게 되어 사용한다"라고 할 수 있다.
  • 평균제곱근오차(RMSE) 역시 연속형 데이터를 대상으로 할 때 사용한다.

 

 

 

 

2.  구현해보자.

  • 지금까지 만들었던 오차제곱(SE)에서 파생된 손실함수들의 결과를 비교해보자.
  • Sample Dataset은 이전 포스트에서 만들었던 함수를 그대로 사용하겠다.
>>> import numpy as np

>>> def SSE(real, pred):
>>>     return 0.5 * np.sum((real - pred)**2)

>>> def MSE(real, pred):
>>>     return (1/len(real)) * np.sum((real - pred)**2)

>>> def RMSE(real, pred):
>>>     return np.sqrt((1/len(real)) * np.sum((real - pred)**2))


# sample Data를 만들어보자.
>>> def make_sample_dataset(data_len, one_index):

>>>     label = np.zeros((data_len,))
>>>     pred = np.full((data_len,), 0.05)

>>>     # 특정 index에 실제 데이터엔 1을 예측 데이터엔 0.8을 넣어보자.
>>>     label[one_index] = 1
>>>     pred[one_index] = 0.8
    
>>>     return label, pred
  • np.sqrt(x) 함수는 제곱근을 해준다.
>>> label, pred = make_sample_dataset(100, 30)

>>> print("SSE: ", np.round(SSE(label, pred), 5))
>>> print("MSE: ", np.round(MSE(label, pred), 5))
>>> print("RMSE: ", np.round(RMSE(label, pred), 5))

SSE:  0.14375
MSE:  0.00288
RMSE:  0.05362
  • 위 출력 결과를 보면 다음과 같이 해석할 수 있다.
  • SSE는 데이터의 수에 지나치게 영향을 받아, 오차가 가장 크게 나온다.
  • MSE는 편차를 지나치게 확대하므로, 오차가 가장 작게 나왔다.
  • RMSE는 MSE에 비해 편차가 확대된 정도를 보정하므로, 실제 편차와 어느 정도 유사한 결과가 나왔다고 할 수 있다.
  • 혹시, 데이터의 편차가 너무 일정해서 이런 결과가 나온 것이 아닐까? 하는 의구심이 들 수도 있으니, 이번엔 어느정도 랜덤한 데이터 셋을 사용해보자.
# sample Data를 만들어보자.
>>> def make_sample_dataset2(data_len, one_index):

>>>     label = np.zeros((data_len,))
    
>>>     # 0.01을 간격으로 0에서 0.3 사이인 값이 일부 섞인 배열을 만들어보자
>>>     pred_sample = np.arange(0, 0.3, 0.01)
    
>>>     # 전체 데이터의 절반은 값을 넣도록 하겠다.
>>>     random_data_len = int(data_len/2)
>>>     pred_1 = np.random.choice(pred_sample, random_data_len, replace = True)
>>>     pred_2 = np.zeros((data_len - random_data_len))
>>>     pred = np.concatenate((pred_1, pred_2), axis = 0)
>>>     np.random.shuffle(pred)

>>>     # 특정 index에 실제 데이터엔 1을 예측 데이터엔 0.95을 넣어보자.
>>>     label[one_index] = 1
>>>     pred[one_index] = 0.95
    
>>>     return label, pred
  • np.arange(시작, 끝, 간격) 함수를 이용해 샘플을 추출할 데이터 셋을 만들었다.
  • np.random.choice(데이터셋, 샘플 수, 복원 추출 여부) 함수를 이용해 랜덤한 배열을 만들었다.
  • np.concatenate((배열1, 배열2), axis=0) 함수를 이용해 배열을 합쳤다.
  • np.random.shuffle(배열) 함수를 이용해 배열을 섞었다.
>>> label, pred = make_sample_dataset2(10000, 30)
>>> pred[:100]
array([0.  , 0.33, 0.37, 0.  , 0.11, 0.  , 0.  , 0.  , 0.26, 0.  , 0.  ,
       0.  , 0.14, 0.1 , 0.26, 0.21, 0.1 , 0.07, 0.34, 0.  , 0.  , 0.  ,
       0.19, 0.14, 0.  , 0.  , 0.13, 0.17, 0.  , 0.  , 0.95, 0.  , 0.07,
       0.  , 0.03, 0.39, 0.  , 0.25, 0.32, 0.  , 0.  , 0.27, 0.  , 0.  ,
       0.  , 0.1 , 0.  , 0.3 , 0.  , 0.  , 0.  , 0.  , 0.19, 0.04, 0.2 ,
       0.28, 0.  , 0.  , 0.32, 0.  , 0.  , 0.  , 0.  , 0.03, 0.  , 0.26,
       0.08, 0.39, 0.  , 0.24, 0.  , 0.15, 0.  , 0.  , 0.  , 0.  , 0.  ,
       0.  , 0.14, 0.  , 0.  , 0.  , 0.  , 0.22, 0.  , 0.24, 0.  , 0.05,
       0.12, 0.12, 0.  , 0.09, 0.  , 0.19, 0.  , 0.  , 0.01, 0.23, 0.08,
       0.15])
  • 다음과 같은 형태의 데이터셋이 만들어졌다.
>>> print("SSE: ", np.round(SSE(label, pred), 5))
>>> print("MSE: ", np.round(MSE(label, pred), 5))
>>> print("RMSE: ", np.round(RMSE(label, pred), 5))

SSE:  127.74995
MSE:  0.02555
RMSE:  0.15984
  • 랜덤한 데이터셋을 사용한다 할지라도 손실함수가 비슷하게 반환되는 것을 알 수 있다.
  • 위 결과를 보면, 연속형 데이터를 대상으로 손실함수를 사용한다고 하면, SSE는 가능한 사용하지 않는 것을 추천하며, MSE는 실제 오차가 있는 수준보다 과소평가된 결과가 나올 수 있다. 반면에 RMSE는 오차를 보다 보정하여 나타내기 때문에 실제 오차와 꽤 가까운 것을 알 수 있다.

 

 

 

 

 지금까지 오차 제곱(SE)에서 파생된 손실함수들인 SSE, MSE, RMSE에 대해 알아보았다. 해당 손실함수는 연속형 데이터를 대상으로 사용하며, 평균절대오차(MAE)에 비해 미분이 잘되어, 학습률에 따른 이동 거리가 달라 학습에 유리하다. 가능하면 RMSE를 사용하길 추천한다.

 다음 포스트에서는 데이터를 분류하는 경우 사용되는 손실함수인 교차 엔트로피 오차(Cross Entropy Error, CEE)를 학습해보도록 하겠다.

728x90
반응형
728x90
반응형

 이전 포스트에서는 제곱오차(Square Error, SE)와 제곱오차를 기반으로 만들어진 손실함수인 오차제곱합(Sum of Squares for Error, SSE)에 대해 알아보았다. 이번 포스트에서는 이 SSE를 기반으로 만들어진 평균제곱오차(MSE)에 대해 알아보도록 하겠다.

 

 

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

  • 단순히 실제 데이터와 예측 데이터 편차의 제곱 합이었던 오차제곱합(SSE)을 데이터의 크기로 나눠 평균으로 만든 것이 평균제곱오차다.
  • 그 공식은 다음과 같다.

$$ \frac{1}{n}\sum_{i=1}^{n}(y_i - \hat{y_i})^2 $$

  • 이전에 봤던 오차제곱합(SSE)는 델타 규칙에 의해 $\frac{1}{2}$을 곱해주었으나, 평균제곱오차(MSE)는 $\frac{1}{n}$이 곱해지므로, 굳이 $\frac{1}{2}$를 곱하지 않아도 된다.

 

 

 

1. 오차제곱합(SSE) 대신 평균제곱오차(MSE)를 주로 사용하는 이유

  • 평균제곱합은 단순히 오차제곱합을 평균으로 만든 것에 지나지 않으므로, 이 둘은 사실상 같다고 볼 수 있다. 그럼에도 평균제곱오차(MSE)를 주로 사용하게 되는 이유는 다음과 같다.
  • 오차의 제곱 값은 항상 양수이며, 데이터가 많으면 많을수록 오차 제곱의 합은 기하급수적으로 커지게 된다.
  • 이로 인해, 오차제곱합으로는 실제 오차가 커서 값이 커지는 것인지 데이터의 양이 많아서 값이 커지는 것인지를 구분할 수 없게 된다.
  • 그러므로, 빅데이터를 대상으로 손실함수를 구한다면, 오차제곱합(SSE)보다 평균제곱오차(MSE)를 사용하는 것을 추천한다.

 

 

 

2. 평균제곱오차(MSE)는 언제 사용하는가?

  • 평균제곱오차(MSE)는 통계학을 한 사람이라면 굉장히 익숙한 단어일 것이다. 
  • 바로, 통계학의 꽃이라고 불리는 회귀분석에서 모델의 적합도를 판단할 때 사용하는 값인 결정 계수 $R^2$를 계산할 때, 분자로 사용되기 때문이다.
  • 딥러닝에서도 평균제곱오차(MSE)는 회귀분석과 유사한 용도로 사용된다.
  • 회귀분석이 연속형 데이터를 사용해 그 관계를 추정하는 방식이듯, 평균제곱오차(MSE) 역시 주식 가격 예측과 같은 연속형 데이터를 사용할 때 사용된다.

 

 

 

3. 구현해보자

  • MSE를 구현하고, SSE와의 차이를 비교해보자.
>>> import numpy as np

>>> def MSE(real, pred):
>>>     return (1/len(real)) * np.sum((real - pred)**2)

>>> def SSE(real, pred):
>>>     return 0.5 * np.sum((real - pred)**2)
# sample Data를 만들어보자.
>>> def make_sample_dataset(data_len, one_index):

>>>     label = np.zeros((data_len,))
>>>     pred = np.full((data_len,), 0.05)

>>>     # 특정 index에 실제 데이터엔 1을 예측 데이터엔 0.8을 넣어보자.
>>>     label[one_index] = 1
>>>     pred[one_index] = 0.8
    
>>>     return label, pred
    
>>> label1, pred1 = make_sample_dataset(100, 30)
>>> label2, pred2 = make_sample_dataset(10000, 30)
>>> label1
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
       0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
       
       
>>> pred1
array([0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.8 , 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05,
       0.05])
  • 자, 위에서 만든 샘플 데이터셋 함수는 개수만 다르지, 나머지 원소는 전부 동일한 데이터셋을 반환하는 함수다.
  • 즉, 데이터의 수만 다르지, 편차는 동일한 형태의 데이터셋을 반환한다.
>>> print("Data가 100개일 때, SSE의 결과:", np.round(SSE(label1, pred1), 5))
>>> print("Data가 1000개일 때, SSE의 결과:", np.round(SSE(label2, pred2), 5))
>>> print("----"*20)
>>> print("Data가 100개일 때, MSE의 결과:", np.round(MSE(label1, pred1), 5))
>>> print("Data가 1000개일 때, MSE의 결과:", np.round(MSE(label2, pred2), 5))


Data가 100개일 때, SSE의 결과: 0.14375
Data가 1000개일 때, SSE의 결과: 12.51875
--------------------------------------------------------------------------------
Data가 100개일 때, MSE의 결과: 0.00288
Data가 1000개일 때, MSE의 결과: 0.0025
  • 위 결과를 보면, 어째서 SSE를 사용하면 위험한지를 알 수 있다.
  • 두 데이터셋은 데이터의 양만 다를 뿐 편차는 같은데, SSE는 90배에 가까운 차이를 반환하였다.
  • 물론, 최적의 가중치를 찾아가면서 손실함수 SSE 역시 감소하긴 하겠으나, Data의 양이 지나치게 많다면, 실제로 오차가 거의 없다 할지라도 오차가 굉장히 크게 나올 위험이 있다.
  • 그러므로, 가능한 SSE보다는 MSE를 사용하길 바란다.

 

 

 

 지금까지 연속형 데이터를 다룰 때, 가장 많이 사용되는 손실함수 중 하나인 평균제곱오차(MSE)에 대하여 알아보았다. 다음 포스트에서는 MSE에서 유도되어 나온 또 다른 손실함수인 평균제곱근편차(RMSE)에 대하여 알아보도록 하겠다.

728x90
반응형
728x90
반응형

 이전 포스트에서 신경망 학습이 어떠한 원리에 의해 이루어지는지 간략하게 살펴보았다. 이번 포스트에서는 제곱 오차(Square Error)와 제곱 오차를 기반으로 만든 손실 함수 오차제곱합(SSE)에 대해 알아보도록 하겠다.

 

1. 제곱오차(Square Error, SE)

  • 자, 앞서 손실함수는 실제값과 예측값의 차이를 이용해서 가중치가 얼마나 적합하게 뽑혔는지를 평가하기 위해 만들어졌다고 했다.
  • 그렇다면 말 그대로 실제값과 예측값을 뺀 편차를 이용하면 이를 평가할 수 있지 않겠는가?
  • 이에, 통계학에서도 즐겨 사용하는 제곱 오차를 가져오게 되었다.

$$ SE = (y - \hat{y})^2 $$

  • 위 수식에서 $y$는 실제 값, $\hat{k}$는 예측한 값이고, 이를 제곱한 이유는 분산을 구할 때처럼, 부호를 없애기 위해서이다.
  • 예를 들어 실제 값이 4이고 예측값이 2일 때의 차이는 2, 실제값이 2이고 예측값이 4일 때 차이는 -2인데, 이 두 경우 모두 실제 값과 예측값의 크기의 차이는 2지만, 부호 때문에 서로 다르다고 인식할 수 있다. 편차에서 중요한 것은 두 값의 크기 차이지, 방향(부호)에는 의미가 없으므로, 절댓값을 씌우거나, 제곱하여 편차의 방향을 없앤다.

 

  • 참고로 이 제곱오차는 후술 할 최적화 기법 중 가장 대표적인 경사하강법에서 중요한 부분이므로, 숙지하고 있도록 하자.
  • 경사하강법은 미분을 통해 실시되는데, 만약 실제값과 오차값의 편차 제곱을 한 제곱오차(SE)가 아닌, 절댓값을 씌운 절대 오차 합계(SAE)를 사용하게 되면, 절댓값에 의해 구분되는 0에서 미분이 불가능하기 때문에 SAE는 사용해선 안된다.
    (미분 조건은 좌미분 = 우미분이 동일해야 한다!)

 

 

 

 

2. 오차제곱합(Sum of Squares for Error, SSE)

  • 자, 위에서 우리는 실제값과 예측값의 편차를 알기 위해 제곱오차를 사용하였다.
  • 만약, 이 오차제곱들을 모두 합한다면, 딱 한 값으로 이 가중치가 적절한지 알 수 있지 않겠는가.
    (어떠한 알고리즘을 판단할 때, 하나의 값인 스칼라로 만들어야 평가하기가 쉽다. 값이 하나란 의미는 판단하는 기준인 변수가 하나라는 소리이며, 변수의 수가 많아질수록, 그 알고리즘을 평가하는 것이 복잡해진다.)
  • 기본적으로 오차제곱합의 공식은 다음과 같다.

$$ SSE = \sum_{k}(y_k - \hat{y_k})^2 $$

  • 그러나, 우리가 딥러닝에서 사용할 오차제곱합은 아래 공식으로 조금 다르다.

$$  E = \frac{1}{2}\sum_{k}(y_k - \hat{y_k})^2  $$

  • 갑자기 쌩뚱맞게 $\frac{1}{2}$가 추가된 것을 볼 수 있다.
  • 이는 델타 규칙(Delta Rule) 때문인데, 최적의 가중치를 찾아가는 최적화(Optimizer)에서 사용되는 경사하강법은 기울기를 기반으로 실시되며, 이 과정에서 발생할 수 있는 오류를 최소화시키기 위해 $\frac{1}{2}$를 곱하는 것이다.
  • (en.wikipedia.org/wiki/Delta_rule)

 

 

 

 

3. 구현해보자.

>>> import numpy as np

>>> def SSE(real, pred):
>>>     return 0.5 * np.sum((real - pred)**2)
# 예시 1.
>>> label = np.array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0])
>>> predict = np.array([0.3, 0.05, 0.1, 0.1, 0.6, 0.05, 0.1, 0.2, 0.0, 0.1])

>>> SSE(label, predict)
0.1675
  • 위 데이터는 0부터 9까지의 숫자를 분류한 신경망의 출력값이다.
  • 위에서 label은 실제 값이고, predict는 예측된 값이다.
  • 여기서 label이라는 배열을 보면, 5번째 자리만 1이고 나머지는 0인데, 이를 원-핫 벡터(One-Hot Vector)라고 한다.
  • 이 예시를 기준으로 값을 조금씩 바꾸면서, 오차제곱합이 어떻게 변하는지 봐보자.
# 예시 2.
>>> label = np.array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0])
>>> predict = np.array([0.3, 0.05, 0.2, 0.3, 0.4, 0.1, 0.1, 0.2, 0.8, 0.1])

>>> SSE(label, predict)
0.64625
  • 첫 번째 예시에서는 실제 데이터에서 가장 큰 값의 위치와 예측 데이터에서 가장 큰 값의 위치가 동일했으며, 상대적으로 다른 위치의 값들이 그리 크지 않았다.
  • 그러나 두 번째 예시에서는 예측 데이터와 실제 데이터의 값의 배치가 상당히 다르다.
  • 그로 인해, 오차제곱합(SSE)가 0.1675에서 0.64625로 올라간 것을 볼 수 있다.
# 예시 3.
>>> label = np.array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0])
>>> predict = np.array([0.0, 0.01, 0.0, 0.05, 0.85, 0.01, 0.0, 0.05, 0.1, 0.0])

>>> SSE(label, predict)
0.01885
  • 세 번째 예시에서는 반대로 실제 데이터와 아주 가까운 형태로 예측 데이터를 만들어보았다.
  • 그로 인해 오차제곱합이 0.01885로 0에 가깝게 떨어진 것을 볼 수 있다.
  • 이러한, 실제 데이터와 예측 데이터의 편차의 제곱 합이 최소가 되는 점을 찾는 것이 학습의 목표가 된다.

 

원-핫 벡터란?

  • 원-핫 벡터는 문자를 벡터화하는 전처리 방법 중 하나로, 0부터 9까지의 숫자를 원-핫 벡터를 사용하여 벡터화 한다면, 다음과 같이 할 수 있다.
>>> label_0 = np.array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0])
>>> label_1 = np.array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0])
>>> label_2 = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0, 0])
>>> label_3 = np.array([0, 0, 0, 1, 0, 0, 0, 0, 0, 0])
>>> label_4 = np.array([0, 0, 0, 0, 1, 0, 0, 0, 0, 0])
>>> label_5 = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0])
>>> label_6 = np.array([0, 0, 0, 0, 0, 0, 1, 0, 0, 0])
>>> label_7 = np.array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0])
>>> label_8 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 1, 0])
>>> label_9 = np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
  • 원-핫 벡터는 먼저 유니크한 단어(숫자 역시 단어의 개념으로써 접근 가능하다!)의 넘버링된 사전을 만들고 총 단어의 수만큼 벡터 크기를 정하며, 각 단어에 넘버링된 위치만 1로 하고, 나머지는 다 0으로 채우는 방법이다.
  • 그다지 어려운 내용은 아니나, 문자를 벡터로 바꾸는 벡터화에 있어서 기본이 되는 방법이며, 자주 사용되는 방법 중 하나이므로, 추후 임베딩을 학습 할 때, 세세히 다루도록 하겠다.

 

 

 

 지금까지 손실함수에서 많이 사용되는 기법 중 하나인 오차제곱합(SSE)에 대해 학습해보앗다. 다음 포스트에서는 오차제곱(SE)에서 파생된 다른 손실함수인 평균제곱오차(MSE)와 평균제곱근오차(RMSE)에 대하여 학습해보도록 하겠다.

728x90
반응형
728x90
반응형

신경망 학습

 이전 포스트에서 다층 퍼셉트론에서 데이터가 흐르는 것에 대해 학습해보았고, 그 과정에서 석연치 않은 부분이 하나 있었다.

 바로 가중치가 이미 주어졌다는 것인데, 가중치를 저렇게 속 편하게 알고 있는 경우는 있을 수가 없으며, 그렇다고 가중치를 하나하나 찾아내는 것은 불가능에 가깝다.

 한 층의 노드 수는 입력되는 텐서의 크기이기 때문에 한층에 수백 수 천 개에 달하는 노드가 존재할 수 있으며, 그러한 층이 무수히 많이 쌓이게 된다면, 각 노드에서 다음 층의 노드로 연결되는 가중치 엣지의 수가 셀 수 없이 많아지므로, 일일이 이를 구해 입력된 데이터가 내가 전혀 알지 못하는 분류대로 나눠지게 만드는 것이 가능할 리가 없다.

 애초에 딥러닝이라는 기술은 엄청난 양의 데이터만 있고 거기에 숨겨진 함수 즉, 규칙을 모를 때 사용하는 것이며, 이 데이터 속에 막연한 현상이 숨어있을 것이라 추측하고 있는 상황에서, 어떻게 그 규칙을 찾아낼지도 모르고, 수많은 이론을 조합해 만들어낸 알고리즘이 정확할지도 모르기 때문에 사용하는 것이다.

 즉, 딥러닝은 순수하게 데이터만 가지고, 내가 분류하고자 하는 바에 가장 적합한 레이어를 쌓아 만들어낸 머신러닝 알고리즘에 데이터를 학습시켜, 최적의 가중치를 알아서 찾아내 모델을 만들어내고, 여기에 새로운 데이터들을 넣어 분류하는 것이다. 때문에 딥러닝을 데이터 주도 학습이라고도 한다.

 그렇다면, 어떻게 최적의 가중치를 찾을 수 있을까?

 

 

 

 

1. 손실 함수(Loss Function)

  • 자, 당신에게 1억 장에 달하는 고양이 사진과 강아지 사진이 있다고 생각해보자.
  • 당신은 고양이와 강아지를 구분할 수 있지만, 이 사진의 양이 지나치게 많아, 이걸 일일이 고양이와 강아지로 구분하는 것은 불가능하다.
  • 그렇다면, 당신이 만 장의 사진에 대해 고양이는 0, 강아지는 1이라 라벨(Label)을 붙였고(실제 값), 컴퓨터가 사진에서 찾아낸 특징을 기반으로 분류해낸 것(예측값)의 차이가 작다면, 최적의 가중치를 찾았다고 할 수 있지 않을까?
  • 바로 이 실제값과 예측값의 오차가 손실함수(Loss Function)다.
  • 오차가 클수록 손실 함수의 값이 커지고, 오차가 작아질수록 손실 함수의 값이 작아진다.
  • 즉, 이 손실 함수가 0에 가깝게 줄어들게 만드는 것이 학습의 목표라고 할 수 있다.
  • 손실함수는 이 오차를 비용이라고 판단하여, 비용함수(Cost Funtion)라고도 한다.

 

 

 

 

2. 최적화(Optimizer)

  • 자, 당신은 이제 손실 함수의 존재를 알았다. 그리고 손실함수를 이용해서 최적의 가중치를 찾을 수 있다고 했다.
  • 그렇다면, 어떻게 최적의 가중치를 찾아갈 수 있을까?
  • 먼저, 각 층에 임의의 가중치를 설정한다(보통 가중치는 0, 편향은 1로 설정한다.)
  • 학습 데이터셋을 모델에 통과시켜, 출력값을 계산한다.
  • 출력 값과 실제 값이 허용 오차 이내가 되도록 각층의 가중치를 업데이트한다.
  • 이 과정에서 출력 값과 실제값의 차이를 나타내는 지표로 사용되는 것이 손실함수다.
  • 손실함수를 최소화시키기 위해, 가중치의 미분(기울기)을 계산하고, 그 미분 값을 기반으로 가장 적합한 가중치 값을 갱신하는 과정을 반복한다.
  • 기울기를 기반으로 최적의 미분 값을 찾아가는 방식을 최적화(Optimizer)라고 하며, 그 유명한 경사하강법(Gradient Descent)이 여기에 해당한다.
  • 참고로 손실함수와 유사한 정확도(Accuracy)라는 것이 있는데, 손실함수는 연속적으로 변해 미분 가능하지만, 정확도는 가중치의 변화에 둔감하고, 불연속적으로 변해 미분이 불가능하여, 손실함수를 지표로 학습을 해나간다.
    (정확도는 출력된 값과 실제값이 일치하는 비율로, 나중에 텐서플로우로 실제 학습과 예측을 해보는 과정에서 다루도록 하겠다.)

 

 

 

 

3. 역전파(Back Propagation)

  • 당신은 최적화를 통해 최적의 가중치를 찾을 수 있다. 그렇다면, 어떻게 이 것을 모델에 반영해줄 것인가?
  • 역전파는 최적화를 효율적으로 할 수 있게 해주는 알고리즘으로, 순전파와 반대방향으로 실제값과 예측값의 오차를 전파하여, 가중치를 업데이트하고 최적의 학습 결과를 찾아간다.
  • 먼저 순전파를 통해 출력층에서 오차를 계산하고, 이를 다시 입력층으로 역전파시켜 가중치를 업데이트하고, 다시 입력값을 넣어 새로운 오차를 계산하고, 이를 또 역전파해서 가중치를 업데이트하는 것을 반복한다.
  • 즉, "순전파 > 역전파 > 가중치 업데이트 > 순전파 > 역전파 > 가중치 업데이트..."의 과정으로 학습은 이루어진다.

 

 

 

 

4.  정리해보면!

  • 손실함수(Loss Function): 가중치가 얼마나 잘 만들어졌는지를 확인하는 방법
  • 최적화(Optimizer): 손실함수를 기반으로 최적의 가중치를 찾는 방법
  • 역전파(Back Propagation): 가중치를 효율적으로 업데이트 하는 방법
  • 이 3가지 방법이 서로 앙상블을 이뤄 신경망에서 가장 적합한 가중치를 찾아낸다.

 

 

 

 지금까지 인공신경망을 학습시키는 3가지 개념에 대해 학습해보았다. 각 기법은 포스팅 하나로 설명하기엔 그 양이 활성화 함수 때처럼 만만치 않으므로, 하나하나 상세하게 다뤄보도록 하겠다.

 다음 포스트에서는 가장 대표적인 손실함수인 오차제곱합(Sum of Squareds for error, SSE)에 대해 학습해보도록 하겠다.

728x90
반응형

+ Recent posts