3 minute read

소프트맥스

  • 소프트맥스는 세 개 이상으로 분류하는 다중 클래스 분류에서 출력층에 사용되는 활성화 함수다.
  • 소프트맥스 함수는 분류될 클래스가 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가 있는게 아니라, 우측 상단에 있는데 어떻게 작성하는지 모름 ㅠ

식 전개과정

  1. C라는 임의의 정수를 분자와 분모 양쪽에 곱함. (같은 수를 곱했으니, 똑같은 계산)
  2. 그 다음으로 \(C\)를 지수 함수 \( \exp() \)안으로 옮겨 \( \log C \)로 만든다.
  3. 마지막으로 \( \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사용하는데

  • e 사용하는 것은 미분했을 때 값이 이쁘게 떨어져서 그런 것 같음.
  • 다음에 공부해서 추가하자

출처

밑러닝