ML03. 소프트맥스 회귀 (Softmax Regression), 다중분류

개요

이전 글에서 다뤘던 Logistic Regression은

0 또는 1처럼 두 개의 클래스(Binary Classification) 를 구분하는 모델이었다.


그렇다면 클래스가 3개 이상이라면 어떻게 해야 할까?

예를 들면,

  • 성적(X) : 0~100, 등급(y) : A~F 학점
  • 숫자 이미지 분류 (0~9)
  • 감정 분류 (positive / neutral / negative)

여기서 등장하는 것이 바로 Softmax Regression 이다.


Softmax Regression은 다음과 같은 이름으로도 불린다.

  • Multinomial Logistic Regression
  • Softmax Classifier

즉, Logistic Regression을 다중 분류로 확장한 형태 라고 이해하면 된다.

아래 그림처럼 여러 군데 흩어진 값들을 다중 분류하는 모델이다.

[2차원 좌표들을 Softmax로 분류한 결과]


1. Softmax의 핵심 아이디어

Logistic Regression에서는 최종 출력이 하나였다.

z = w1*x1 + w2*x2 + b

선형결합(z) 의 범위는 음의 무한대에서 양의 무한대 사이의 값이다.


그리고 z를 sigmoid에 통과시켜 확률로 만들었다.

y_hat = sigmoid(z)


하지만 다중 분류에서는 클래스마다 점수가 필요하다.

예를 들어 클래스가 3개라면,

z = [
    2.1,   # class 0 score
    0.3,   # class 1 score
   -1.2    # class 2 score
]

이 점수들을 확률로 변환해야 한다.

그 역할을 하는 것이 Softmax 함수다.


2. Softmax 함수

Softmax는 각 클래스 점수를 확률로 바꿔준다.

수식은 다음과 같다.

\[
\text{Softmax}(z_i)
=
\frac{e^{z_i}}
{\sum_{j=1}^{K} e^{z_j}}
\]


핵심 특징은, 모든 값이 0~1 사이

따라서 전체 합은 항상 1

가장 큰 score(Z)가 가장 높은 확률이 되는 것이다.


아래 직접 필기한 내용에서 수치예시로 이해하면 쉽다.

[Softmax 수치 예시]

① 선형결합의 결과 Z = [1, 2, 3] 이면

② 각 값들을 밑이 자연상수 e인 지수함수(e^z)로 변환

    a = [2.718, 7.389, 20.085]

    여기서 a를 모두 합치면 ∑a = 30.192 가 되고, 이를 분모로, a를 분자로

    그럼 a = [0.09, 0.244, 0.665] 의 합이 1인 확률분포가 된다.

    이것이 Softmax 함수.

③ 만약 y 값이 [0, 0, 1] 이라면, 정답(2번 클래스)

④ a - y 를 진행하면 [0.09, 0.244, -0.335] 가 error가 된다

    정답 클래스의 가중치 결과가 음수지만 정상인 이유는

⑤  W 가중치를 경사하강하면

\[
W = W - \alpha \frac{\partial J}{\partial W}
\]

결국 위 식의  "W-..."에 의해 가중치가 증가되게 된다.  😀


3. 왜 exp(지수함수)를 사용할까?

다음 score를 보자.

z = [2.0, 1.0, 0.1]

그냥 합으로 나누면

[2.0/3.1, 1.0/3.1, 0.1/3.1]


이렇게 되긴 하지만 문제가 있다.

  • 음수가 들어오면 확률 해석이 어려움 (음수제거)
  • score 차이를 강하게 반영하지 못함 (큰 값 강조)
  • 미분 가능한 형태를 유지


그래서 exp를 사용한다.

exp([2.0, 1.0, 0.1]) = [7.39, 2.71, 1.10]

큰 값은 훨씬 더 커지고 작은 값은 상대적으로 작아진다.

즉, 모델의 "확신(confidence)" 을 강조할 수 있다.


4. 수치 안정성 (Numerical Stability)

Softmax 구현 시 매우 중요한 부분이 있다.

np.exp(1000)

이런 값은 overflow(inf)가 발생할 수 있다.


그래서 보통 최대값을 먼저 빼준다.

Z = Z - np.max(Z, axis=1, keepdims=True)

수치 예시 : 

[1000, 999, 998]
	↓
[0, -1, -2]

Softmax 결과는 동일하지만 overflow는 방지된다.

위 2번에서 설명한 대로 음수는 경사하강시 양수로 바뀌어 증가된다.

따라서 np.max() - Z 가 아님에 유의하자.


5. One-Hot Encoding

다중 분류에서는 정답을 보통 One-Hot 형태로 바꾼다.

예를 들어 클래스가 3개라면

y = [0, 1, 2]
	
y_onehot =	[	
				[1,0,0],
				[0,1,0],
				[0,0,1]
			]


numpy에서는 이렇게 만들 수 있다.

C = len(np.unique(y))
I = np.eye(C)
y_onehot = I[y]


np.unique(y)  = [0, 1, 2] 이므로 C는 3 이고,

np.eye()는 단위행렬을 생성하므로

[	
	[1, 0, 0],
	[0, 1, 0],
	[0, 0, 1]
]

y_onehot은 단위행렬 I의 [0], [1], [2] 번 인덱스가 된다.


6. 예측 과정

Softmax Regression의 전체 흐름은 다음과 같다.

(1) 선형 계산

Z = np.dot(X, W) + b
# X : (m, n)
# W : (n, C)
# Z : (m, C)

즉, 샘플 m개, 특징 n개, 클래스 C개의 점수 생성


(2) Softmax 적용

A = softmax(Z)

수치 예시

A = [
[0.9, 0.05, 0.05],
[0.1, 0.8 , 0.1 ],
[0.2, 0.3 , 0.5 ]
]

각 행의 합은 항상 1이다.


(3) 최종 예측

가장 큰 확률의 클래스를 선택한다.

y_pred = np.argmax(A, axis=1)


7. Cost Function (Cross Entropy)

다중 분류에서는 보통 Cross Entropy Loss를 사용한다.

cost = -np.sum(y * np.log(y_hat)) / m


왜 이렇게 될까?

정답 클래스의 확률만 남기기 때문이다.

수치예시 (1번 클래스가 정답이라면)

y = [0, 1, 0]
y_hat = [0.1, 0.8, 0.1]
# 곱하면
[0, log(0.8), 0]

즉, 정답 클래스 확률만 loss에 반영된다.


8. log(0) 문제

만약 예측 확률이 0이라면, 음의 무한대가 된다.

np.log(0) = -∞


이를 방지하기 위해 clip을 사용한다.

epsilon = 1e-15
y_hat = np.clip(y_hat, epsilon, 1-epsilon)

여기서 엡실론(ε)은 아주 작은 값이다.

y_hat이 0라면 ε을 사용하고 1이면 1-ε 을 사용한다.


9. Gradient

Softmax + Cross Entropy 조합은 미분이 굉장히 깔끔하게 정리된다.

최종 오차

err = y_hat - y


gradient

dj_dw = np.dot(X.T, err) / m
dj_db = np.sum(err, axis=0) / m

이 부분이 Softmax Regression의 핵심이다.

특히 axis=0을 쓰지 않으면 dj_db가 스칼라가 되므로 유의하자.


10. 전체 구현

아래 예제는 ML Lib. 사용을 지양하고 수식과 전개과정 이해를 위해 numpy로 작성.

(개념공부 시 PyTorch, TensorFlow 사용 X 추천)

두 파일을 같은 위치에 두고 main.py를 실행.


main.py

import numpy as np

X = np.array([0, 2, 4, 6, 8, 10]).reshape(-1, 1)
y = np.array([0, 0, 1, 1, 2, 2])

m, n = X.shape
#print(m, n)

# y -> one-hot vector
C = len(np.unique(y))
I = np.eye(C)
y_onehot = I[y]

W = np.random.randn(n, C)
b = np.zeros(C)

epochs = 5000
lr = 0.1

from softmax_func_01 import gradient_descent, softmax
W_final, b_final = gradient_descent(X, y_onehot, W, b, epochs, lr)

# predict
X_test = np.array([1, 5, 9]).reshape(-1, 1)

def predict(X, W, b):
    z = np.dot(X, W) + b
    return softmax(z)

y_pred = predict(X_test, W_final, b_final)
print(X_test)
print(y_pred)
print(np.argmax(y_pred, axis=1))


softmax_func.py

import numpy as np

def softmax(z):
    z = z - np.max(z, axis=1, keepdims=True)
    exp_z = np.exp(z)    
    A = exp_z / np.sum(exp_z, axis=1, keepdims=1)
    return A

def compute_cost(X, y, W, b):
    m, n = X.shape
    z = np.dot(X, W) + b
    y_hat = softmax(z) # (m, C)

    epsilon = 1e-15
    y_hat = np.clip(y_hat, epsilon, 1-epsilon)

    L = -np.sum(y * np.log(y_hat))
    cost = L/m
    return cost

def compute_gradient(X, y, W, b):
    m, n = X.shape
    z = np.dot(X, W) + b
    y_hat = softmax(z)
    err = y_hat -y

    dj_dw = np.dot(X.T, err) / m
    dj_db = np.sum(err, axis=0) / m
    return dj_dw, dj_db   


def gradient_descent(X, y, W, b, epochs, lr):
    for i in range(epochs):

        dj_dw, dj_db = compute_gradient(X, y, W, b)

        W = W - lr * dj_dw
        b = b - lr * dj_db

        if i%100==0:
            cost = compute_cost(X, y, W, b)
            print(f'epochs={i}, W={np.round(W, 3)}, b={np.round(b, 3)}, cost={cost:.3f}')

    return W, b


결과는 아래와 같다.

[학습이 잘 된 결과]


11. 정리

Softmax Regression은

  • Logistic Regression의 다중 클래스 버전
  • 각 클래스 score를 계산
  • Softmax로 확률화
  • Cross Entropy로 학습

하는 모델이다.


딥러닝의 마지막 출력층에서도 매우 자주 등장한다.

예를 들면, 

  • 이미지 분류 CNN
  • Transformer 분류기
  • NLP 다중 클래스 분류

등 대부분의 분류 모델 마지막에 Softmax가 사용된다.


12. 참조문헌

Andrew Ng. 교수님 수업을 들으며 직접 작성한 내용.

[페이지 1]

[페이지 2]

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

파이썬을 활용한 PID 제어기 GUI 구현

Android 15 앱 UI 겹침이슈 해결방법 및 원인분석