딥러닝 기초 - [6] softmax
소프트맥스
- 소프트맥스는 세 개 이상으로 분류하는 다중 클래스 분류에서 출력층에 사용되는 활성화 함수다.
- 소프트맥스 함수는 분류될 클래스가 n개라 할 때, n차원의 벡터를 입력받아, 각 클래스에 속할 확률을 추정한다.
- 입력받은 값들을 0~1 사이의 값으로 만들어 출력하며, 출력값들의 총합은 항상 1이 되는 특징을 가진 함수다.
- 분류하고 싶은 클래스의 수만큼 출력으로 구성한다. 가장 큰 출력값을 부여받은 클래스가 정답 확률이 가장 높은 것이다.
소프트맥스의 수식
소프트맥스 함수의 식은 다음과 같다.
\[y_k = \frac{\exp(a_k)}{\sum_{i=1}^n \exp(a_{i})}\]\( \)
\( \exp(x) \)는 \( e^x \)을 뜻하는 지수 함수다. \( n \)은 출력층의 뉴런 수, \( y_k \)는 그 중 \( k \)번째 출력임을 뜻한다. 소프트맥스 함수의 분자는 입력 신호\( a_{k} \)의 지수 함수, 분모는 모든 입력 신호의 지수 함수의 합으로 구성된다.
소프트맥스의 구현 코드
import numpy as np
def softmax(a):
exp_a = np.exp(a)
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
print(exp_a) # [ 1.34985881 18.17414537 54.59815003]
print(sum_exp_a) # 74.1221542101633
print(y) # [0.01821127 0.24519181 0.73659691]
return y
a = np.array([0.3, 2.9, 4.0])
y = softmax(a)
소프트 맥스의 식 변형
위 코드는 본래의 소프트맥스의 식을 제대로 표현하고 있지만, 컴퓨터로 계산할 때는 오버플로가 발생할 수 있다. 소프트맥스 함수는 지수 함수를 사용하는데, 지수 함수란 것이 큰 값을 쉽게 내뱉는다. \( e^{10} \)은 20,000이 넘고, \( e^{100} \)은 0이 40개 넘는 큰 값이 되고, \( e^{1000} \)은 무한대를 뜻하는 inf가 되어 돌아온다. 그리고 이런 큰 값끼리 나눗셈을 하면 결과 수치가 ‘불안정’해진다.
\[y_k = \frac{\exp(a_k)}{\sum_{i=1}^n \exp(a_{i})} = \frac{C\exp(a_k)}{C\sum_{i=1}^n \exp(a_{i})}\] \[= \frac{\exp(a_k + \log C)}{\sum_{i=1}^n \exp(a_{i} + \log C)}\] \[= \frac{\exp(a_k + \acute{C})}{\sum_{i=1}^n \exp(a_{i} + \acute{C})}\]C위에 acute가 있는게 아니라, 우측 상단에 있는데 어떻게 작성하는지 모름 ㅠ
식 전개과정
- C라는 임의의 정수를 분자와 분모 양쪽에 곱함. (같은 수를 곱했으니, 똑같은 계산)
- 그 다음으로 \(C\)를 지수 함수 \( \exp() \)안으로 옮겨 \( \log C \)로 만든다.
- 마지막으로 \( \log C \)를 \( \acute{C} \)라는 새로운 기호로 변경한다.
위 식이 말하는 것은 소프트맥스의 지수 함수를 계산할 대 어떤 정수를 더해도 (혹은 빼도) 결과는 바뀌지 않는다는 것이다. 여기서 \(\acute{C} \)에 어떤 값을 대입해도 상관없지만, 오버플로를 막을 목적으로는 입력 신호 중 최댓값을 이용하는 것이 일반적이다. 구체적인 예를 하나 보자.
>>> import numpy as np
>>> a = np.array([1010, 1000, 990])
>>> np.exp(a) / np.sum(np.exp(a))
<stdin>:1: RuntimeWarning: overflow encountered in exp
<stdin>:1: RuntimeWarning: invalid value encountered in divide
array([nan, nan, nan])
>>> c = np.max(a)
>>> a - c
array([ 0, -10, -20])
>>>
>>> np.exp(a-c) / np.sum(np.exp(a-c))
array([9.99954600e-01, 4.53978686e-05, 2.06106005e-09])
위 처럼 아무런 조치 없이 그냥 계산하면 nan이 출력된다. 하지만 입력 신호 중 최대값(이 예에서는 c)을 뺴주면 올바르게 계산할 수 있다. 이를 바탕으로 소프트맥스 함수를 다시 구현하면 다음과 같다.
오버플로 방지된 소프트맥스 함수
import numpy as np
def improved_softmax(a):
c = np.max(a)
exp_a = np.exp(a - c) # 오버플로 대책
sum_exp_a = np.sum(exp_a)
y = exp_a / sum_exp_a
return y
a = np.array([1010, 1000, 990])
y = improved_softmax(a)
print(y)
소프트맥스 함수의 특징
a = np.array([0.3, 2.9, 4.0])
y = improved_softmax(a)
print(y)
print(np.sum(y))
결과
[0.01821127 0.24519181 0.73659691]
1.0
소프트맥스 함수의 출력은 0.0 ~ 1.0 사이의 실수이고, 출력의 총합은 1인 것이 특징이라 이를 활용해 소프트맥스 함수의 출력을 ‘확률’로 해석할 수 있다.
- y[0]: 0.018 = 1.8%
- y[1]: 0.245 = 24.5%
- y[2]: 0.737 = 73.7%
여기서 y[2]의 확률이 가장 높으니 답은 2번째 클래스라고 생각할 수 있다.
단조 증가 함수이기에 원소의 대소 관계는 변하지 않는다.(지수함수 \( y = \exp(x) \)가 단조증가 함수이기 때문에)
- 그래서 어차피 대소만 비교하면 되니까 그냥 소프트맥스를 생략한다.
- 신경망을 이용한 분류에서 일반적으로 가장 큰 출력을 내는 뉴련에 해당하는 클래스로만 인식한다.
- 소프트맥스 함수를 적용해도 출력이 가장 큰 뉴런의 위치는 달라지지 않는다.
- 결과적으로 신경망으로 분류할 때는 출력층의 소프트맥스 함수를 생략하고, 지수 함수 계산에 드는 자원 낭비를 줄이고자 소프트맥스 함수는 생략하는 것이 일반적이다.
학습시킬 땐 출력층에서 소프트맥스 함수를 사용하고, 추론 단계에서는 출력층의 소프트맥스 함수를 생략하는 것이 일반적이다.
단조 증가 함수란 정의역 원소 \(a, b\)가 \( a \le b \)일 떄, \( f(a) \le f(b) \)가 성립하는 함수이다.
번외
- e 사용하는 것은 미분했을 때 값이 이쁘게 떨어져서 그런 것 같음.
- 다음에 공부해서 추가하자
출처
밑러닝