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
반응형

인공신경망(ANN)과 다층 퍼셉트론(MLP)

 이전 포스트에서 단층 퍼셉트론이 행렬 연산을 통해 다음 노드로 정보를 전달하는 과정을 살펴보았다. 이전에 학습했었던 퍼셉트론과의 차이점은 활성화 함수로 계단 함수가 아닌 시그노이드 함수를 사용한 것이다.

 이렇게 활성화 함수에 정보를 전달하느냐 마느냐인 계단 함수를 넣는 것이 아니라, 시그노이드, 소프트맥스, 하이퍼볼릭 탄젠트, 렐루 등 다양한 활성화 함수를 넣고, 단층 퍼셉트론이 아닌, 다층 퍼셉트론을 만들고, 가중치를 인간이 수동으로 만드는 것이 아닌, 자동으로 가장 적합한 값을 찾아내는 것이 바로 인공 신경망이다.

 

 

 

 

1. 신경망의 구조

  • "입력층 - 출력층"만 존재하는 단층 퍼셉트론(Single Layer Perceptron, SLP)과 달리 "입력층 - m개의 은닉층 - 출력층"이 존재하는 다층 퍼셉트론(Multi Layer Perceptron) "n-층 신경망"이라고도 부르며, 일반적으로 단층 퍼셉트론처럼 입력층을 제외하고 부르거나, 입력층을 0층으로 생각하고 "n = m + 1"로 은닉층의 개수 + 출력층 1개로 명명한다.
  • 즉, 아래 다층 퍼셉트론은 3-층 신경망이라고 부른다.
  • 이렇게 은닉층이 2개 이상인 신경망을 심층 신경망(Deep Neural Network)라 한다.
  • 그러나, 간혹 다른 책에선 입력층까지 포함하여 m+2 층 신경망, 아래 예시에선 4-층 신경망이라 부르는 경우도 있다. 본, 블로그에서는 "은닉층의 수(m) + 출력층 1개"인 m+1층 신경망이라 부르도록 하겠다.

  • 입력층(Input Layer): 
    파란색 노드로, 학습 데이터셋(Train dataset)이 입력되는 곳이다. 학습 데이터의 Feature의 차원 수만큼의 뉴런 개수를 가진다. 입력층은 단 한층만 존재한다.

  • 은닉층(Hidden Layer):
    연두색 노드로, 입력층과 출력층 사이의 모든 층이다. 은닉층이라 불리는 이유는 입력층과 출력층은 Input 되는 Dataset과 Output 된 Dataset을 눈으로 확인할 수 있지만, 은닉층은 보이지 않기 때문이다.

  • 출력층(Output Layer):
    주황색 노드로, 출력하고자 하는 데이터의 형태에 따라 노드의 수가 바뀐다. 예를 들어, 0부터 9까지 10 종류의 숫자가 있다고 할 때, 이를 분류하고자 한다면, 출력층의 노드 수는 10개가 되며, 시그모이드 함수를 사용하여, 이진 분류를 하고자 하는 경우엔 노드의 수가 1개가 된다.

 

 

 

 

2. 다층 퍼셉트론의 연산

  • 다층 퍼셉트론의 연산 방식은 앞서 다뤘던 단층 퍼셉트론의 연산 방식과 동일하나, 더 많이 실시하게 된다.
  • 가중치를 알아서 찾아내는 방식은 뒤에서 다루도록 하고, 이번에는 가중치를 임의로 만들어보자.

$$ X = (x_1, x_2, x_3) = (10, 2) $$

$$ W_1=
\begin{pmatrix}
w_{11}^{(1)} & w_{21}^{(1)} & w_{31}^{(1)}\\ 
w_{12}^{(1)} & w_{22}^{(1)} & w_{32}^{(1)}
\end{pmatrix}
=
\begin{pmatrix}
0.1 & 0.3 & 0.5\\ 
0.6 & 0.4 & 0.2 
\end{pmatrix} $$

$$W_2=
\begin{pmatrix}
w_{11}^{(2)} & w_{21}^{(2)}\\ 
w_{12}^{(2)} & w_{22}^{(2)}\\ 
w_{13}^{(2)} & w_{23}^{(2)}
\end{pmatrix}
=
\begin{pmatrix}
0.1 & 0.2\\ 
0.2 & 0.4\\
0.3 & 0.6
\end{pmatrix}$$

$$W_3=
\begin{pmatrix}
w_{11}^{(3)} & w_{21}^{(3)}\\ 
w_{12}^{(3)} & w_{22}^{(3)}\\ 
\end{pmatrix}
=
\begin{pmatrix}
0.2 & 0.4\\ 
0.4 & 0.8
\end{pmatrix}$$

$$B_1 = (b_{1}^{(1)}, b_{2}^{(1)}, b_{3}^{(1)})=(0.7, 0.6, 0.5)$$

$$B_2 = (b_{1}^{(2)}, b_{2}^{(2)})=(0.6, 0.4)$$

$$B_3 = (b_{1}^{(3)}, b_{2}^{(3)})=(0.5, 0.6)$$

  • 활성화 함수는 은닉층에서는 렐루 함수를 사용하고, 출력층에서는 시그모이드 함수를 사용해보자.
  • 즉, $f()$는 렐루 함수, $h()$는 시그모이드 함수이다.

 

 

 

 

3. 구현해보자.

  • 먼저 데이터를 생성하고, 데이터의 모양을 봐야 한다.
  • 기계학습에선 행렬곱이 주를 이르므로, 행렬의 모양을 파악하는 것이 최우선이다.
>>> X = np.array([10, 2])

>>> W1 = np.array([[0.1, 0.3, 0.5], [0.6, 0.4, 0.2]])
>>> W2 = np.array([[0.1, 0.2],[0.2, 0.4],[0.3, 0.6]])
>>> W3 = np.array([[0.2, 0.4],[0.4, 0.8]])

>>> B1 = np.array([0.7, 0.6, 0.5])
>>> B2 = np.array([0.6, 0.4])
>>> B3 = np.array([0.5, 0.6])

>>> print("X Shape:", X.shape)
>>> print("----"*20)
>>> print("W1 Shape:", W1.shape)
>>> print("W2 Shape:", W2.shape)
>>> print("W3 Shape:", W3.shape)
>>> print("----"*20)
>>> print("B1 Shape:", B1.shape)
>>> print("B2 Shape:", B2.shape)
>>> print("B3 Shape:", B3.shape)

X Shape: (2,)
--------------------------------------------------------------------------------
W1 Shape: (2, 3)
W2 Shape: (3, 2)
W3 Shape: (2, 2)
--------------------------------------------------------------------------------
B1 Shape: (3,)
B2 Shape: (2,)
B3 Shape: (2,)

  • 중요한 것은 노드와 가중치 엣지의 모양이다.
  • 편향은 위 노드와 가중치 엣지의 모양만 제대로 맞게 이루어져 있다면, 벡터 합이 당연히 되므로, 신경 쓰지 않아도 된다.
>>> def ReLU(x):
>>>     return np.maximum(0, x)

>>> def sigmoid(x):
>>>     return 1 / (1 + np.exp(-x))


# 입력층에서 출력층 방향으로 계산된다.
>>> A1 =  np.dot(X, W1) + B1
>>> Z1 = ReLU(A1)

>>> A2 = np.dot(Z1, W2) + B2
>>> Z2 = ReLU(A2)

>>> A3 = np.dot(Z2, W3) + B3
>>> Y = sigmoid(A2)

>>> Y

array([0.97180471, 0.9981301 ])
  • 위 코드를 보면, 입력층 X에서 출발한 데이터가 "행렬곱 > 활성화 함수 > 행렬곱 > 활성화 함수 > 행렬곱 > 활성화 함수 > 출력"의 형태로 진행된 것을 알 수 있다.
  • 이를 보면, 단층 퍼셉트론을 활성화 함수만 바꾸면서 층을 쌓듯 여러 번 수행된 것을 알 수 있다.

 

 

 

 

4. 순전파(Forward Propagation)

  • 위 신경망에서 데이터의 흐름은 입력층에서 출력층의 방향으로 전달되었는데, 이를 순전파라고 한다.
  • 위에서 인공 신경망은 스스로 가장 적합한 가중치를 찾아간다고 하였는데, 이를 우리는 학습이라고 하며, 이 학습은 역전파(Back Propagation) 과정을 통해 이루어진다.
  • 만약 내가 신경망의 틀을 만들고, 그 신경망에서 가장 적합한 가중치를 찾는 것을, 이는 학습을 한다고 하며, 모델을 만든다라고 한다. 지금 같이 이미 가중치를 알고 있는 상태는 학습이 끝난, 모델이 완성된 상태이며, 이 모델에 데이터를 집어넣어, 그 결과를 확인하는 것은 순전파 되어 실행된다.

 

 

 

 지금까지 이미 가중치가 얻어진 다층 퍼셉트론(MLP)을 이용해, 신경망이 어떻게 연산되는지를 알아보았다. 신경망의 연산은 행렬곱과 활성화 함수 이 두 가지를 통해서 구해지는, 생각보다 단순한 알고리즘인 것을 알 수 있다. 

 다음 포스트에서는 그렇다면 대체 그 학습이라는 과정은 어떻게 이루어지는 지에 대해 알아보도록 하겠다.

728x90
반응형
728x90
반응형

인공신경망(Artificial Neural Network, ANN)

 지금까지 퍼셉트론의 개념과 노드에 전달되어 합쳐진 신호들이 다음 노드로 전달될 때, 값이 어떤 방법으로 전달될지를 결정하는 활성화 함수에 대해 학습해보았다.

 앞서 학습했던, 퍼셉트론은 계단 함수를 이용해서 신호를 전달할지(출력값 = 1), 전달하지 않을지(출력 값 = 0)를 정하였으며, XOR 게이트 실현에서 층을 여러 개 쌓자 단일층으로 해결하지 못했던 문제를 해결할 수 있었다.

 여기서, 활성화 함수의 존재와 층을 여러 개 쌓는다. 이 부분에 초점을 맞추면, 인공신경망을 만들 수 있고, 이 인공신경망을 구현하고, 가장 적합한 가중치를 알아서 찾아내는 것이 바로 딥러닝(Deep Learning)이다.

 본격적으로 신경망을 공부하기 전에 단층 퍼셉트론의 연산이 어떻게 이루어지는지 확인해보도록 하자. 단층 퍼셉트론의 연산 방법을 알게 되면, 다층 퍼셉트론의 구현은 이를 쌓아가기만 하면 된다.

 

 

 

1. m : 1 단층 퍼셉트론의 연산

  • 위 그림에서 정보가 전달되는 방식을 수식으로 적어보면 다음과 같다.

$$ Z = w_1*x_1 + w_2*x_2 +b*1 $$

$$ y = h(Z) $$

  • 여기서, $x_1=2, x_2=5, w_1 = 0.4, w_2 = 0.2, b = 0.7$이라고 가정해보자
  • 위 수식에서 $Z$를 가장 빠르게 출력할 수 있는 방법은 백터 연산이다.
  • 활성화 함수를 시그모이드 함수로 해서 구현해보자.
>>> import numpy as np

>>> def sigmoid(x):
>>>     return 1 / (1 + np.exp(-x))

>>> x = np.array([2, 5, 1])
>>> w = np.array([0.4, 0.2, 0.7])
>>> Z = np.sum(x*w)
>>> sigmoid(Z)

0.9241418199787566
  • 도착점이 1개만 있는 경우, 입력층의 노드 수와 곱해지는 가중치 엣지의 수가 서로 동일하기 때문에, 길이가 동일한 벡터가 2개 나온다.
  • 때문에, 벡터연산으로 쉽게 해결할 수 있다.
    (numpy의 벡터 연산은 길이가 같은 벡터끼리 연산 시, 동일한 위치의 원소끼리 연산이 이루어지는 방식이다.)
  • m : 1 퍼셉트론은 이처럼 쉽게 연산이 가능했다. 그렇다면 입력층 노드의 수와 가중치 엣지의 수가 다른 m : n은 어떨까?

 

 

 

 

2. m : n 단층 퍼셉트론의 연산

  • 참고로 위에서 각 엣지(Edge)별 가중치에 써놓은 숫자들이 무슨 뜻인지 이해가 안 갈 수 있으니, 이를 간략히 설명해보겠다.

  • 가중치에서 위 괄호 안에 들어있는 값은 몇 번째 층(Layer)인지를 의미한다.
  • 아래 숫자는 앞은 다음 층, 뒤는 앞 층을 이야기한다.
  • 입력 노드의 값은 위에서부터 순서대로 1, 3, 5 라고 가정하자.
  • 가중치 엣지의 값은 위에서부터 순서대로 0.3, 0.5, 0.4, 0.2, 0.7, 0.3이라고 가정하자.
  • 편향 엣지의 값은 위에서부터 순서대로 0.2, 0.3이라 가정하자.
  • 이를, 벡터 연산으로 구하고자 한다면, 각 벡터의 길이가 다르고, 이를 잘라서 $y_1$, $y_2$를 따로따로 연산하기엔 시간도 많이 걸리고 공식도 지저분해진다.

 

 

 

 

3. 행렬 곱

  • 위와 같은 m : n 퍼셉트론은 행렬 곱을 사용한다면, 한방에 계산을 할 수 있다.
  • 위 퍼셉트론을 수식으로 간소화하면 다음과 같다.

$$ Y = WX + B$$

$$ X = (x_1, x_2, x_3) = (1, 3, 5)$$

$$ B = (b_{1}^{(1)}, b_{2}^{(1)}) = (0.2, 0.3)$$

$$ W=
\begin{pmatrix}
w_{11}^{(1)} & w_{21}^{(1)}\\ 
w_{12}^{(1)} & w_{22}^{(1)}\\ 
w_{13}^{(1)} & w_{23}^{(1)}
\end{pmatrix}
=
\begin{pmatrix}
0.3 & 0.5\\ 
0.4 & 0.2\\ 
0.7 & 0.3
\end{pmatrix} $$

  • 행렬 연산을 하기 위해선, 가장 먼저 배열이 어떻게 생겼는지를 확인해야 한다.
>>> X = np.array([1, 3, 5])
>>> B = np.array([0.2, 0.3])
>>> W = np.array([[0.3, 0.5],[0.4, 0.2],[0.7, 0.3]])

>>> print("X shape:", X.shape)
>>> print("B shape:", B.shape)
>>> print("W shape:", W.shape)

X shape: (3,)
B shape: (2,)
W shape: (3, 2)
  • 여기서 우리는 X와 W를 행렬곱할 것이다.
  • 행렬곱을 간단하게 짚고 가자면 다음과 같다.

  • 위 행렬 곱 방법을 보면, 서로 곱해지는 행렬에서 빨간 부분이 일치해야만 곱해지며, 출력되는 행렬의 녹색 부분이 행의 수, 파란색이 열의 수를 결정한다.
  • 여기서 지금까지의 행렬에 대한 인식을 조금만 틀어보자.
  • 행 = 데이터의 수
  • 열 = 변수의 수
  • 앞으로 이 인식을 하고 행렬 곱을 생각하게 된다면, 뒤에서 나올 n-차원 텐서의 연산에 대해서도 쉽게 이해할 수 있을 것이다(이에 대한 상세한 내용은 나중에 이야기하겠다.)
  • 자, 위 수식을 함수로 구현해보자.
# 단층 퍼셉트론 연산
>>> Z = np.dot(X, W) + B
>>> Z
array([5.2, 2.9])
  • np.dot(A,B): 행렬 A와 행렬 B를 행렬 곱한다.
  • 편향인 B는 행렬 X와 W의 곱과 길이가 동일한 벡터이므로(다음 노드의 수와 동일하다), 쉽게 벡터 합이 된다.
  • 신경망에서 신호가 흘러가는 것은 행렬 연산을 통해 진행되며, 때문에 딥러닝에서 행렬 연산에 매우 유리한 GPU가 사용되는 것이다.
  • 각 노드별 합산된 결과를 시그모이드 함수를 통해서 출력해보자.
# 시그모이드 함수를 활성화 함수로 사용
>>> sigmoid(Z)
array([0.9945137 , 0.94784644])

 

 

 

 

 지금까지 단층 퍼셉트론(SLP)을 이용해서 신경망에서 연산이 어떻게 이루어지는지 확인해보았다. 다음 포스트에서는 다층 퍼셉트론(MLP)을 이용해서 신경망 연산을 학습해보도록 하자.

728x90
반응형
728x90
반응형

 지금까지 계단 함수, 선형 함수, 시그모이드 함수, 소프트맥스 함수, 하이퍼볼릭 탄젠트 함수에 대해 다뤄보았다. 이들은 은닉층에서 사용해서는 안되거나, 사용할 수 있더라도 제한적으로 사용해야 하는 활성화 함수들이었다. 이번 포스트에서는 은닉층에서 많이 사용되는 렐루 함수에 대해 학습해 보겠다.

 

 

 

렐루 함수(Rectified Linear Unit, ReLU)

  • 렐루 함수는 딥러닝 역사에 있어 한 획을 그은 활성화 함수인데, 렐루 함수가 등장하기 이전엔 시그모이드 함수를 활성화 함수로 사용해서 딥러닝을 수행했다.
  • 그러나, 이전 포스트에서 언급했듯 시그모이드 함수는 출력하는 값의 범위가 0에서 1사이므로, 레이어를 거치면 거칠수록 값이 현저하게 작아지게 되어 기울기 소실(Vanishing gradient) 현상이 발생한다고 하였다.
    gooopy.tistory.com/52?category=824281
 

머신러닝-3.1. 활성화함수(2)-시그모이드 함수

 지난 포스트에서 퍼셉트론의 가장 기본이 되는 활성화 함수인 계단 함수(Step Function)를 학습하였으며, 선형 함수(Linear Function)의 한계점에 대해서도 학습해보았다.  선형 함수는 층을 쌓는 것이

gooopy.tistory.com

  • 이 문제는 1986년부터 2006년까지 해결되지 않았으나, 제프리 힌튼 교수가 제안한 렐루 함수로 인해, 시그모이드의 기울기 소실 문제가 해결되게 되었다.
  • 렐루 함수는 우리 말로, 정류된 선형 함수라고 하는데, 간단하게 말해서 +/-가 반복되는 신호에서 -흐름을 차단한다는 의미다.
  • 렐루 함수는 은닉층에서 굉장히 많이 사용되는데, 별생각 없이 다층 신경망을 쌓고, 은닉층에 어떤 활성화 함수를 써야 할지 모르겠다 싶으면, 그냥 렐루 함수를 쓰라고 할 정도로, 아주 많이 사용되는 활성화 함수이다(물론 신경망을 의도를 가지고 써보고 싶다면, 그래선 안된다.).

 

 

 

 

1. 렐루 함수의 생김새

  • 렐루 함수는 +신호는 그대로 -신호는 차단하는 함수라고 하였는데, 그 생김새는 아래와 같다.

$$ h(x) = \begin{cases}
 x \ \ \ (x>0) \\ 
 0 \ \ \ (x\leq 0) 
\end{cases} $$

  • 말 그대로, 양수면 자기 자신을 반환하고, 음수면 0을 반환한다.
  • 이번에는 이를 구현해보고, 어떻게 생겼는지 확인해보자.
>>> import numpy as np

# ReLU 함수를 구현해보자
>>> def ReLU(x):
    
>>>     return np.maximum(0, x)
  • 단순하게 최댓값 함수를 사용하여 지금 들어온 값(원소별 연산이 된다!)이 0보다 크면 자기 자신을 반환하고, 그렇지 않으면, 최댓값인 0을 반환하는 함수를 이용해서 구현하였다.
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = ReLU(x)

>>> fig = plt.figure(figsize=(8,6))
>>> fig.set_facecolor('white')

>>> plt.title("ReLU", fontsize=30)
>>> plt.xlabel('x', fontsize = 15)
>>> plt.ylabel('y', fontsize = 15, rotation = 0)
>>> plt.axvline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.axhline(0.0, color='gray', linestyle="--", alpha=0.8)
>>> plt.plot(x, y)
>>> plt.show()

  • 가장 많이 사용되는 활성화함수라기엔 지금까지 보아왔던 시그모이드, 소프트맥스, 하이퍼볼릭 탄젠트 등에 비해 너무 단순하게 생겼다는 생각이 들 것이다.
  • 그렇다면 왜 렐루 함수를 은닉층에서 많이 사용할까?

 

 

 

 

2. 렐루 함수를 은닉층에서 많이 사용하는 이유

 

기울기 소실(Vanishing Gradient) 문제가 발생하지 않는다.

  • 렐루 함수는 양수는 그대로, 음수는 0으로 반환하는데, 그러다 보니 특정 양수 값에 수렴하지 않는다. 
  • 즉, 출력값의 범위가 넓고, 양수인 경우 자기 자신을 그대로 반환하기 때문에, 심층 신경망인 딥러닝에서 시그모이드 함수를 활성화 함수로 사용해 발생한 문제였던 기울기 소실(Vanishing Gradient) 문제가 발생하지 않는다.

 

기존 활성화 함수에 비해 속도가 매우 빠르다

  • 동시에 렐루 함수의 공식은 음수면 0, 양수면 자기 자신을 반환하는 아주 단순한 공식이다 보니, 경사 하강 시 다른 활성화 함수에 비해 학습 속도가 매우 빠르다!
  • 확률적 경사하강법(SGD)을 쓴다고 할 때, 시그모이드 함수나 하이퍼볼릭 탄젠트 함수에 비해 수렴하는 속도가 약 6배 가까이 빠르다고 한다!
  • ReLU가 나오기 전에는 활성화 함수가 부드러워야(Smooth) 가중치 업데이트가 잘된다고 생각하여 exp 연산이 들어간 시그모이드나, 하이퍼볼릭 탄젠트 함수를 사용하여쓰나, 활성화 함수가 부드러운(Smooth)한 구간에 도달하는 순간 가중치 업데이트 속도가 매우 느려진다.
  • ReLU는 편미분(기울기) 시 1로 일정하므로, 가중치 업데이트 속도가 매우 빠르다.

 

 

 

 

3. 렐루 함수의 한계점

  • 렐루 함수의 그래프를 보면, 음수 값이 들어오는 경우 모두 0으로 반환하는 문제가 있다보니, 입력값이 음수인 경우 기울기도 모조리 0으로 나오게 된다.
  • 입력값이 음수인 경우에 한정되긴 하지만, 기울기가 0이 되어 가중치 업데이트가 안되는 현상이 발생할 수 있다.
  • 즉, 가중치가 업데이트 되는 과정에서 가중치 합이 음수가 되는 순간 ReLU는 0을 반환하기 때문에 해당 뉴런은 그 이후로 0만 반환하는 아무것도 변하지 않는 현상이 발생할 수 있다.
  • 이러한 죽은 뉴런(Dead Neuron)을 초래하는 현상을 죽어가는 렐루(Dying ReLU) 현상이라고 한다.
  • 또한 렐루 함수는 기울기 소실 문제 방지를 위해 사용하는 활성화 함수이기 때문에 은닉층에서만 사용하는 것을 추천한다.
  • ReLU의 출력값은 0 또는 양수이며, ReLU의 기울기도 0 또는 1이므로, 둘 다 양수이다. 이로 인해 시그모이드 함수처럼 가중치 업데이트 시 지그제그로 최적의 가중치를 찾아가는 지그재그 현상이 발생한다.
  • 또, ReLU의 미분은 0 초과 시, 1 0은 0으로 끊긴다는 문제가 있다. 즉, ReLU는 0에서 미분이 불가능하다.
    (이에 대해 활성화 함수로는 미분 불가능 하다할지라도, 출력값 문제는 아니고, 0에 걸릴 확률이 적으니, 이를 무시하고 사용한다.)

 

 

 

 지금까지 은닉층에서 주로 사용되는 활성화 함수인 렐루 함수에 대해 학습해보았다. 비록 렐루 함수가 입력값이 0일 때, 기울기가 0에 수렴해 가중치 업데이트가 안 되는 현상이 발생한다고는 하지만, 성능상 큰 문제가 없으며, 도리어 이를 해결하기 위해 만든 활성화 함수의 성능이 보다 안 나오는 경우도 있다고 한다.

 때문에 기본적으로 은닉층에서는 렐루 함수를 사용하지만, 때에 따라 렐루 함수의 단점이 두드러지는 경우도 존재하므로, 렐루 함수의 한계점을 보완하기 위한 렐루 함수의 형제 함수들이 있다. 

 다음 포스트에서는 렐루 함수의 한계점을 극복하기 위해 만들어진 다양한 활성화 함수에 대해 학습해보도록 하곘다.

728x90
반응형
728x90
반응형

 지난 포스트에서는 시그모이드 함수에서 발전한 소프트맥스 함수에 대해 학습해보았다. 이번 포스트에서는 시그모이드 함수와 꽤 유사하면서, 시그모이드 함수의 단점을 보완한 하이퍼볼릭 탄젠트 함수에 대해 학습해보겠다.

 

 

하이퍼볼릭 탄젠트(Hyperbolic Tangent, tanh)

 우리말로 쌍곡선 탄젠트 함수라고 말하는 하이퍼볼릭 탄젠트는 수학이나 물리학을 전공한 사람이 아니라면, 영 볼 일이 없는 함수다.

 시그모이드, 소프트맥스 함수는 워낙 자주 사용되고, 신경망의 핵심 알고리즘인 로지스틱 회귀 모형에서 유래되었으므로, 공식까지 세세하게 파고 들어갔으나, 하이퍼볼릭 탄젠트 함수는 그 정도까지 설명할 필요는 없다고 생각한다.

 가볍게 하이퍼볼릭 탄젠트가 어떻게 생겼고, 왜 시그모이드 함수의 단점을 보완했다는지만 알아보도록 하자.

 

 

 

1. 하이퍼볼릭 탄젠트란?

  • 하이퍼볼릭 함수는 우리말로 쌍곡선 함수라고도 하며, 삼각함수는 단위원 그래프를 매개변수로 표시할 때, 나오지만, 쌍곡선 함수는 표준 쌍곡선을 매개변수로 표시할 때 나온다는 특징이 있다.
  • 삼각함수에서 $tanx$ = $sinx$/$cosx$로 나왔듯, 쌍곡선 함수에서 쌍곡탄젠트(Hyperbolic tangent)는 $tanhx$ = $sinhx$/$coshx$를 통해서 구한다.
  • 공식은 다음과 같다.

$$ sinhx = sinhx = \frac{e^x - e^{-x}}{2} $$

$$ coshx = \frac{e^x + e^{-x}}{2} $$

$$ tanhx = \frac{sinhx}{coshx} = \frac{e^x - e^{-x}}{e^x + e^{-x}} $$

  • 이들을 명명하는 방식은 다음과 같다.
    • $sinhx$: 신치, 쌍곡 샤인, 하이퍼볼릭 샤인
    • $coshx$: 코시, 쌍곡 코샤인, 하이퍼볼릭 코샤인
    • $tanhx$: 텐치, 쌍곡 탄젠트, 하이퍼볼릭 탄젠트

 

 

 

 

2. 하이퍼볼릭 탄젠트의 구현.

  • 위 공식을 그대로 구현해보면 다음 코드와 같다.
>>> import numpy as np

# 하이퍼볼릭 탄젠트
>>> def tanh(x):
>>>     p_exp_x = np.exp(x)
>>>     m_exp_x = np.exp(-x)
    
>>>     y = (p_exp_x - m_exp_x)/(p_exp_x + m_exp_x)
    
>>>     return y

 

  • 시각화해보자
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = tanh(x)

# 캔버스 설정
>>> fig = plt.figure(figsize=(10,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.title("Hyperbolic Tangent", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)

>>> plt.yticks([-1.0, 0.0, 1.0]) # 특정 축에서 특정 값만 나오게
>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True) # y축에 있는 모든 숫자에 회색 점근선을 그음

>>> plt.show()

  • 위 그림을 보면, 어째서 하이퍼볼릭 탄젠트 함수가 시그모이드 함수를 일부 보완했다고 하였는지, 이해할 수 있겠는가?
  • 시그모이드 함수와 하이퍼볼릭 탄젠트 함수의 가장 큰 차이는 출력값의 범위로, 하이퍼볼릭 탄젠트 함수는 -1에서 1 사이의 값을 출력하며, 중앙값도 0이다!
  • 이를 정리해보면 다음과 같다.
  시그모이드 함수 하이퍼볼릭 탄젠트 함수
범위 0 ~ 1 -1 ~ 1
중앙값 0.5 0
미분 최댓값 0.3 1

 

 

 

 

3. 하이퍼볼릭 탄젠트와 시그모이드 함수

  • 하이퍼볼릭 탄젠트는 중앙값이 0이기 때문에, 경사하강법 사용 시 시그모이드 함수에서 발생하는 편향 이동이 발생하지 않는다.
  • 즉, 기울기가 양수 음수 모두 나올 수 있기 때문에 시그모이드 함수보다 학습 효율성이 뛰어나다.
  • 또한, 시그모이드 함수보다 범위가 넓기 때문에 출력값의 변화폭이 더 크고, 그로 인해 기울기 소실(Gradient Vanishing) 증상이 더 적은 편이다.
    (※ 기울기 소실(Gradient Vanishing): 미분 함수에 대하여, 값이 일정 이상 커지는 경우 미분값이 소실되는 현상)
  • 때문에 은닉층에서 시그모이드 함수와 같은 역할을 하는 레이어를 쌓고자 한다면, 하이퍼볼릭 탄젠트를 사용하는 것이 효과적이다.
  • 그러나, 시그모이드 함수보다 범위가 넓다 뿐이지 하이퍼볼릭 탄젠트 역시 그 구간이 그리 크지는 않은 편이므로, $x$가 -5보다 작고 5보다 큰 경우, 기울기(Gradient)가 0으로 작아져 소실되는 기울기 소실 현상 문제는 여전히 존재한다.
# 시그모이드 함수의 미분
def diff_sigmoid(x):
    
    return 1/(1+np.exp(-x)) * (1 - (1/(1+np.exp(-x))))

# 하이퍼볼릭 탄젠트의 미분
def diff_tanh(x):
    
    return 4 / (np.exp(2*x) + 2 + np.exp(-2*x))
>>> import matplotlib.pyplot as plt

>>> x = np.arange(-10.0, 10.0, 0.1)
>>> y1 = diff_sigmoid(x)
>>> y2 = diff_tanh(x)

>>> fig = plt.figure(figsize=(10,5))

>>> plt.plot(x, y1, c = 'blue', linestyle = "--", label = "diff_sigmoid")
>>> plt.plot(x, y2, c = 'green', label = "diff_tanh")

>>> plt.title("Sigmoid VS tanh", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)

>>> plt.ylim(-0.5, 2)
>>> plt.xlim(-7, 7)

>>> plt.legend(loc = "upper right")

>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True)
>>> ax.xaxis.grid(True)

>>> plt.show()

 

  • 위 그래프는 시그모이드의 도함수(파랑)와 하이퍼볼릭 탄젠트(녹색)의 미분 함수를 비교한 것으로, 시그모이드 함수의 미분보다 하이퍼볼릭 탄젠트의 미분이 상황이 더 낫긴 하지만, 하이퍼볼릭 탄젠트의 미분 역시 ±5부터 0이 되어버리므로, 기울기 소실 문제에서 안전하지 않다는 것을 알 수 있다.

 

 

 

 지금까지 하이퍼볼릭 탄젠트에 대해 알아보았다. 시그모이드 함수의 단점을 많이 보완한 활성화 함수이긴 하지만, 여전히 기울기 소실 문제가 발생할 가능성이 있으므로, 은닉층에서 쓰고자 하면, 쓰되 조심히 쓰기를 바란다.

 다음 포스트에서는 은닉층에서 가장 많이 사용되는 렐루(ReLU) 함수에 대해 알아보도록 하겠다.

728x90
반응형
728x90
반응형

 지난 포스트에선 활성화 함수에서 자주 사용되는 시그모이드 함수(Sigmoid Function)에 대해 학습해 보았다. 시그모이드 함수는 이진 분류에서 주로 사용되며, 보통 출력층에서만 사용된다. 은닉층에서 소프트맥스 함수가 사용되는 경우, 이전 포스트에서도 말했듯, 기울기 소실 문제 등 기울기를 제대로 찾지 못해, 학습 효율성이 감소한다는 단점이 있다.

 이번 포스트에서는 이전에 학습했던 이진 분류 활성화 함수인 시그모이드가 아닌 다중 분류에 주로 사용되는 활성화 함수인 소프트맥스(Softmax) 활성화 함수에 대해 살펴보도록 하겠다.

 

 

소프트맥스 함수(Softmax Function)

  • 소프트맥스는 세 개 이상으로 분류하는 다중 클래스 분류에서 사용되는 활성화 함수다.
  • 소프트맥스 함수는 분류될 클래스가 n개라 할 때, n차원의 벡터를 입력받아, 각 클래스에 속할 확률을 추정한다.

 

 

 

 

1. 소프트맥스 함수 공식

$$ y_k = \frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}} $$

  • $n$ = 출력층의 뉴런 수(총 클래스의 수), $k$ = $k$번째 클래스
  • 만약, 총 클래스의 수가 3개라고 한다면 다음과 같은 결과가 나오게 된다.

$$softmax(z) = [\frac{e^{z_1}}{e^{z_1}+e^{z_2}+e^{z_3}},\ \ \ \frac{e^{z_2}}{e^{z_1}+e^{z_2}+e^{z_3}},\ \ \  \frac{e^{z_3}}{e^{z_1}+e^{z_2}+e^{z_3}}] = [p_1, p_2, p_3]$$

  • 위 공식을 보면, 소프트맥스 함수의 생김세는 "k번일 확률 / 전체 확률"로 꽤나 단순하다는 것을 알 수 있다.

 

 

 

 

2. 소프트맥스 함수에서 $e^x$를 사용하여, 확률을 계산하는 이유

 위 소프트맥스 함수의 공식을 보면, 각 클래스에 속할 확률을 구하는 것은 알겠는데, 대체 왜 자연로그의 밑인 상수 e에 대한 지수함수를 사용하여 확률을 나타내는 것일까?

 

 이는 소프트맥스 함수는 시그모이드 함수로부터 유도된 것이기 때문이다. 

  • 시그모이드 함수를 $S$라고 가정하고 $e^{f(x)}$에 대한 공식으로 변환해보자.

$$S = \frac{1}{e^{-t}+1}, \ \ \  \frac{S}{1-S} = e^{t}$$

  • $\frac{S}{1-S}$에서 $S$는 "전체에서 $S$할 확률"이고 $1-S$는 "전체에서 $1-S$할 확률"이다. 즉, 앞서 봤던 오즈와 같다. 이 식은 각 집단이 독립이라는 가정하에 다음과 같이 변화시킬 수 있다.

$$ \frac{P(C_1)}{P(C_2)} = \frac{\frac{P(C_1)*P(X)}{P(X)}}{\frac{P(C_2)*P(X)}{P(X)}} = \frac{P(C_1|X)}{P(C_2|X)} = e^t$$

  • 위 식은 클래스가 2개일 때의 확률을 이야기하는 것이다. 그렇다면 클래스가 K개라면 어떨까?

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i} $$

  • 이 식에 대하여 양변을 i = 1 부터 i = K-1까지 더해보자.

$$\sum_{i=1}^{K-1}\frac{P(C_i|X)}{P(C_K|X)} = \frac{1}{P(C_K|X)}\sum_{i=1}^{K-1}P(C_i|X) = \sum_{i=1}^{K-1}e^{t_i} $$

  • 위 식에서 일부분을 이렇게 바꿀 수 있다.

$$ \sum_{i=1}^{K-1}P(C_i|X) = 1-P(C_K|X) $$

  • 이를 식에 반영해보면 이렇게 된다.

$$ \frac{1-P(C_K|X)}{P(C_K|X)} = \sum_{i=1}^{K-1}e^{t_i}, \ \ \ \frac{P(C_K|X)}{1-P(C_K|X)} = \frac{1}{\sum_{i=1}^{K-1}e^{t_i}} $$

$$ P(C_K|X)\sum_{i=1}^{K-1}e^{t_i}= 1-P(C_K|X), \ \ \ P(C_K|X)(\sum_{i=1}^{K-1}e^{t_i} + 1) = 1 $$

$$ P(C_K|X) = \frac{1}{\sum_{i=1}^{K-1}e^{t_i} + 1} $$

  • 위 식을 통해서 $P(C_K|X)$를 유도하였으며, 이제 처음에 만들었던 식을 이용해서 P(C_i|X)를 유도해보자.

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i}, \ \ \ P(C_i|X) = e^{t_i}P(C_K|X) = \frac{e^{t_i}}{\sum_{i=1}^{K-1}e^{t_i} + 1} $$

  • 분모에 있는 1은 다음과 같은 방법으로 제거한다.

$$ \frac{P(C_i|X)}{P(C_K|X)} = e^{t_i} $$

  • $i = K$ 이라면

$$ \frac{P(C_K|X)}{P(C_K|X)} = 1 = e^{t_K} $$

  • 위 식을 분모의 1에 넣어주자.

$$ P(C_i|X) = \frac{e^{t_i}}{\sum_{i=1}^{K-1}e^{t_i} + e^{t_K}} = \frac{e^{t_i}}{\sum_{i=1}^{K}e^{t_i}} $$

  • 시그모이드에서부터 지금까지의 과정을 보면, "로짓(Logit) > 시그모이드 함수 > 소프트맥스 함수" 순으로 유도되는 것을 알 수 있다.
  • 애초에 분류라는 것은 로지스틱 회귀 모델에 의해 파생되는 것이므로, 기계 학습에 대해 자세히 알기 위해선 로지스틱 회귀 모델에 대해 자세히 알 필요가 있다.

 

 

 

 

3. 소프트맥스 함수에서 $e^x$를 사용하여 얻어지는 장점

  1. 지수함수 단조 증가함수(계속 증가하는 함수)이기 때문에 소프트맥스에 들어가는 인자들의 대소 관계는 변하지 않는다.
  2. 지수함수를 적용하면, 아무리 작은 값의 차이라도 확실히 구별될 정도로 커진다.
  3. $e^x$의 미분은 원래 값과 동일하기 때문에, 미분을 하기 좋다.

$$\frac{d}{dx}e^x = e^x $$

# 가볍게 지수함수를 그려보자
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-5, 5, 0.1)
y = np.exp(x)

# 캔버스 설정
fig = plt.figure(figsize=(8,6)) # 캔버스 생성
fig.set_facecolor('white')      # 캔버스 색상 설정

plt.plot(x, y)
plt.title("Exponential Function", fontsize = 25)
plt.xlabel('x', fontsize = 15)
plt.ylabel('y', fontsize = 15, rotation = 0)
plt.show()

 

 

 

 

4. 소프트맥스 함수의 구현

  • 위 공식을 그대로 구현해보면 다음 코드와 같다.
# 소프트맥스
>>> def softmax(x):
    
>>>     exp_x = np.exp(x)
>>>     result = exp_x / np.sum(exp_x)
    
>>>     return result
  • 그러나 위 코드에는 상상치 못한 문제점이 하나 숨어있다.
  • 그것은 바로, 코드 내에 지수함수가 포함되어 있다는 것인데, 지수함수는 값을 더욱 확대한다는 특징을 가지고 있으며, 만약 지나치게 큰 값이 원소로 들어가게 된다면, 값이 너무 커서 연산이 되지 않는 오버플로 문제를 일으킬 위험이 있다.
  • 지수함수의 문제가 어떠한지 보기 위해 아래 코드를 보자.
>>> print(np.exp(10))
>>> print(np.exp(100))
>>> print(np.exp(1000))

22026.465794806718
2.6881171418161356e+43
inf
<ipython-input-15-2a14d587d35d>:3: RuntimeWarning: overflow encountered in exp
  print(np.exp(1000))
  • 여기서 np.exp(x)는 $e^x$를 의미하는 함수이다.
  • 고작 $e^1000$만 했을 뿐인데, 값이 무한대로 나와, RuntimeWarning이 뜨는 것을 볼 수 있다.
  • 이는 softmax에 들어가는 원소들에 대하여, 그 원소들의 최댓값을 빼는 것으로 쉽게 해결할 수 있다.
>>> def softmax(x):
>>>     """ 소프트맥스 함수
>>>     Input: array
>>>     Output: array
>>>     """
>>>     # Input 값에 Input 값의 최댓값을 뺀다.
>>>     array_x = x - np.max(x)
    
>>>     exp_x = np.(array_x)
>>>     result = exp_x / np.sum(exp_x)
    
>>>     return result
  • 위 방법이 가능한 것을 증명해보면 다음과 같다.

$$ y_k = \frac{e^{a_k}}{\sum_{i=1}^{n}e^{a_i}} = \frac{Ce^{a_k}}{C\sum_{i=1}^{n}e^{a_i}} = \frac{e^{a_k + lnC}}{\sum_{i=1}^{n}e^{a_i + lnC}} =  \frac{e^{a_k + C'}}{C\sum_{i=1}^{n}e^{a_i + C'}} $$

  • 위 코드를 보면 """ 주석 """를 만들어주었으며, Input, Output을 써주었다. 보다 자세히 코드에 대한 설명을 써주면 좋지만, 최소한 Input, Output되는 Data가 어떠한 형태인지는 써줄 필요가 있다.
  • 위 함수에 값을 넣어 그 효과와 형태를 보자.
# 소프트맥스 함수에 임의의 값을 넣어보자
>>> x = np.array([15, 10, 20, 30, 60])
>>> softmax(x)
array([2.86251858e-20, 1.92874985e-22, 4.24835426e-18, 9.35762297e-14,
       1.00000000e+00])
  • 가장 끝에 있는 5번째 값이 가장 크게 나오는 것을 볼 수 있다.
  • 이를 그래프로 그려보자.
# 소프트맥스 함수로 그래프를 그려보자.
>>> x = np.arange(-5.0, 5.0, 0.1)
>>> y = softmax(x)

>>> fig = plt.figure(figsize=(10,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(0, 0.1)
>>> plt.title("Softmax", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', fontsize=20, rotation=0)
>>> plt.show()

 

 

 

 소프트맥스 함수는 시그모이드 함수처럼 출력층에서 주로 사용되며, 이진 분류에서만 사용되는 시그모이드 함수와 달리 다중 분류에서 주로 사용된다. 

 무엇보다도 소프트맥스 함수의 큰 장점은 확률의 총합이 1이므로, 어떤 분류에 속할 확률이 가장 높을지를 쉽게 인지할 수 있다. 

 다음 포스트에서는 시그모이드 함수의 대체제로 사용되는 활성화 함수인 하이퍼볼릭 탄젠트 함수(tanh)에 대해 학습해 보겠다.

728x90
반응형
728x90
반응형

 지난 포스트에서 퍼셉트론의 가장 기본이 되는 활성화 함수인 계단 함수(Step Function)를 학습하였으며, 선형 함수(Linear Function)의 한계점에 대해서도 학습해보았다.

 선형 함수는 층을 쌓는 것이 무의미해진다는 단점이 있고, 비선형함수 중 하나인 계단 함수는 값의 크기에 대한 정보가 소실된다는 단점이 있다.

 이번 포스트에서는 이 두 단점이 해결된 비선형 함수 중 하나인 시그모이드 함수에 대해 학습해보겠다.

 

 

시그모이드 함수(Sigmoid Function)

계단 함수는 출력을 0과 1로 이진 값만 반환하며, 그 사이에 있는 값은 무시한다는 단점이 있었다. 그렇다면, 앞서 봤던 계단 함수의 각진 부분이 매끄러워진다면 어떨까?

 

 

 

1. 로지스틱 회귀 모델과 오즈(Odds)

  • 통계학을 조금이라도 공부해봤거나, 분석에 관심 있는 사람이라면, 이진 분류의 대표적인 모델 중 하나인 로지스틱 회귀(Logistic regression)에 대해 들어봤거나, 알고 있을 것이다.
  • 시그모이드 함수를 설명하기 앞서 로지스틱 회귀 모델의 오즈(Odds)를 이야기 해보겠다.

 

 

오즈(Odds)

  • 오즈는 성공과 실패의 비율이다.
  • 확률(Probability)과 뉘앙스가 꽤 다른 확률로
  • "사건 A가 일어날 확률 / 사건 A가 일어나지 않을 확률"을 말한다.

$$ Odds = \frac{P}{1-P} $$

 

 

오즈비(Odds ratio)

  • 참고로 오즈비(Odds ratio)와 오즈(Odds)를 헷갈리는 경우가 종종 있는데, 오즈비는 우리말로 교차비라고 하며, 서로 다른 집단의 오즈를 비교할 때 사용된다.
  • 예를 들어 약품의 성능 대한 오즈비는, "약품 A를 먹어서 호전될 오즈 / 위약을 먹어서 호전될 오즈"이다.
  Favorable UnFavorable Total
Test 60 40 100
Control 20 80 100

$$ Test\ favorable\ Odds = \frac{Test\ favorable\ ratio}{Test\ unfavorable\ ratio} = \frac{\frac{60}{100}}{\frac{40}{100}} = 1.5$$

$$ Control\ favorable\ Odds = \frac{Control\ favorable\ ratio}{Control\ unfavorable\ ratio} = \frac{\frac{20}{100}}{\frac{80}{100}}=0.25$$

$$ OddsRatio = \frac{Test\ favorable\ Odds}{Control\ favorable\ Odds} = \frac{1.5}{0.25} = 6.0 $$

  • 위 예시를 보면, 약품A의 효과는 위약의 효과의 6.0배임을 알 수 있다.

 

 

로짓 변환.

  • 오즈에 자연로그를 취해서 로짓(Logit) 함수를 만들어보자.

$$ logit(P) = ln\frac{p}{1-p} = f(x) $$

$$ \frac{p}{1-p} = e^{f(x)},\ \  p=e^{f(x)}(1-p),\ \  p=e^{f(x)} - pe^{f(x)}, \ \  p(1+e^{f(x)})=e^{f(x)} $$

$$ p = \frac{e^{f(x)}}{1+e^{f(x)}} = \frac{1}{1+e^{-{f(x)}}} $$

  • 위 함수를 로지스틱 시그모이드 함수(Logistic sigmoid function)이라고 하며, 줄여서 시그모이드 함수(Sigmoid function)이라고 한다.
  • 여기서 $f(x)$에 회귀 분석과 같은 함수 식을 넣으면, 로지스틱 회귀 모델(Logistic Regression model)이 된다.

$$ f(x) = w^Tx = w_0x_0 + w_1x_1 + w_2x_2 +\ ... + w_mx_m  $$

 

 

 

 

2. 시그모이드 함수(Sigmoid Function)

  • 시그모이드 함수를 파이썬에서 구현해보자.
# 시그모이드 함수
>>> def sigmoid(x):
>>>     return 1 / (1 + np.exp(-x))
>>> x = np.arange(-10.0, 10.0, 0.1)
>>> y = sigmoid(x)

# 캔버스 설정
>>> fig = plt.figure(figsize=(8,7)) # 캔버스 생성
>>> fig.set_facecolor('white')      # 캔버스 색상 설정

>>> plt.plot(x, y)
>>> plt.ylim(-0.1, 1.1)
>>> plt.xlim(-10, 10)
>>> plt.title("Sigmoid", fontsize=30)
>>> plt.xlabel('x', fontsize=20)
>>> plt.ylabel('y', rotation=0, fontsize=20)

>>> plt.yticks([0.0, 0.5, 1.0]) # 특정 축에서 특정 값만 나오게
>>> plt.axvline(0.0, color='k')
>>> ax = plt.gca()
>>> ax.yaxis.grid(True) # y축에 있는 모든 숫자에 회색 점근선을 그음

>>> plt.show()

  • 시그모이드 함수는 0에서 1 사이의 함수이며, 값이 들어왔을 때, 0~1 사이의 값을 반환한다.
  • 연속형 데이터이기 때문에 계단 함수가 끊기지 않는 매끄러운 모양으로 바뀐 것을 알 수 있다.
  • 동시에 이상치가 들어온다 할지라도, 시그모이드 함수는 0과 1에 수렴하므로, 이상치 문제도 해결하면서, 연속된 값을 전달할 수 있다.
  • 시그모이드 함수를 활성화 함수로 사용하면, 0과 1에 가까운 값을 통해 이진 분류를 할 수 있다.

 

 

 

 

3. 시그모이드 함수의 장점과 단점

A. 장점

  • 출력 값의 범위가 0 ~ 1 사이이며, 매우 매끄러운 곡선을 가지므로, 후술 할 경사하강법을 시행할 때, 기울기가 급격하게 변해서 발산하는, 기울기 폭주(Gradient Exploding)가 발생하지 않는다.
  • 분류는 0과 1로 나뉘며, 출력 값이 어느 값에 가까운지를 통해 어느 분류에 속하는지 쉽게 알 수 있다.

 

B. 단점

  • 입력값이 아무리 크더라도, 출력되는 값의 범위가 매우 좁기 때문에 경사하강법 수행 시에 범위가 너무 좁아, 0에 수렴하는 기울기 소실(Gradient Vanishing)이 발생할 수 있다.

  B.1. 기울기 소실(Gradient Vanishing) 문제

  • 시그모이드 함수는 아무리 큰 값이 들어온다 할지라도 0~1사이의 값만 반환하므로, 값이 일정 비율로 줄어들어 값의 왜곡이라 할 수는 없으나, 값이 현저하게 줄어들게 된다. 
  • 또한, 출력 값의 중앙값이 0이 아닌 0.5이며, 모두 양수기 때문에 출력의 가중치 합이 입력의 가중치 합보다 커지게 된다.
  • 이를 편향 이동(Bias Gradient)라 하고, 신호가 각 레이어를 통과할 때마다 분산이 계속 커지게 되어, 활성화 함수의 출력이 최댓값과 최솟값인 0과 1에 수렴하게 된다.
  • 시그모이드 함수의 도함수는 $\sigma(1-\sigma)$인데, 도함수에 들어가는 함수의 값이 0이나 1에 가까울수록 당연히 출력되는 값이 0에 가까워지게 된다.
  • 이로 인해 수렴되는 뉴련의 기울기(Gradient) 값이 0이 되고, 역전파 시 0이 곱해져서 기울기가 소멸(kill)되는 현상이 발생해버린다! 즉, 역전파가 진행될수록 아래 층(Layer)에 아무런 신호가 전달되지 않는 것이다!
  • 이를 기울기 소실(Gradient Vanishing)이라 하며, 렐루 함수가 등장하기 전까지인 1986년부터 2006년까지 해결되지 않은 문제다.

  B.2. 학습 속도 저하 문제

  • 시그모이드 함수의 출력값은 모두 양수기 때문에 경사하강법을 진행할 때, 그 기울기가 모두 양수거나 음수가 된다. 이는 기울기 업데이트가 지그재그로 변동하는 결과를 가지고 오고, 학습 효율성을 감소시켜 학습에 더 많은 시간이 들어가게 만든다.

 

 

 

 위 시그모이드 함수의 장단점을 간추려보면, 출력값이 너무 작아 제대로 학습이 안되는데다가 시간도 많이 잡아먹는다는 소리다.

 이는, 출력층에서 시그모이드 함수를 사용하는 것은 상관 없으나, 아래로 정보가 계속 흘러가는 은닉층(Hidden Layer)에서는 시그모이드 함수를 활성화 함수로 사용해서는 안된다는 소리다.

  • 은닉층(Hidden Layer)은 입력층(시작), 출력층(끝) 사이에 있는 부분이다.
  • 즉, 은닉층에는 앞서 말했던 선형 함수와 시그모이드 함수는 사용하지 않는 것이 좋다.
  • 시그모이드 함수는 이진 분류를 하고자 하는 경우 출력층에서만 사용하는 것을 권고한다.
  • 만약, 입력층에서 시그모이드 함수를 쓰고자 한다면, 이의 발전형인 하이퍼볼릭 탄젠트 함수를 사용하는 것을 추천한다.

 

 

 

 

 

 이번 포스트에서는 시그모이드 함수에 대해 간략하게 알아보았다. 시그모이드 함수는 이진 분류가 목적인 학습 모델에서 출력층에서 사용하는 것을 추천한다. 다음 포스트에서는 또 다른 활성화 함수인 소프트맥스 함수에 대해 학습해보겠다.

728x90
반응형

+ Recent posts