서론
『분석가를 위한 수리 최적화 입문』(이토 모토하루・하라다 코헤이・마츠오카 유키, 소시무, 2026년)의 제3장을 네 편으로 나누어 기사로 작성하고 있습니다. 이번 기사는 네 번째 글로, 세 개 이상의 선택지 중 하나를 예측하는 다항 로지스틱 회귀(Multinomial Logistic Regression)를 다룹니다.
로지스틱 회귀는 '지연할지/하지 않을지'와 같은 2지선다를 확률로 예측하는 방법이었습니다. 실제 분석에서는 '정상・주의・과부하'처럼 세 개 이상의 선택지 중 하나를 예측하고 싶은 경우가 많습니다. 이때 사용하는 것이 소프트맥스 함수(Softmax function)를 이용한 다항 로지스틱 회귀입니다. 본 기사에서는 그 목적 함수와 기울기(gradient)를 직접 유도하여 SciPy로 풀고, scikit-learn의 결과와 비교해 봅니다.
이 글을 읽고 나면 다음 질문에 답할 수 있기를 목표로 합니다.
- 소프트맥스 함수는 어떻게 세 개 이상의 값을 확률로 변환하는가?
- 다항 로지스틱 회귀의 목적 함수와 기울기는 어떤 형태를 갖는가?
- 선택지가 2개일 때, 로지스틱 회귀와 같은 이유는 무엇인가?
- 파라미터가 하나로 결정되지 않는 이유는 왜인가? 그래도 문제가 없는 이유는 무엇인가?
- 소프트맥스 함수를 공식대로 계산하면 왜
nan이 되는 경우가 있는가?
수식은 고등학교의 미분・로그, 그리고 대학교 1학년 정도의 편미분・행렬을 사용합니다. 새로운 도구가 나올 때마다 짧은 예시로 설명하겠습니다.
이번에 만들 것
웹 서버의 가상 모니터링 데이터(300분 분)에서 CPU 사용률과 메모리 사용률을 바탕으로, 해당 1분간의 부하 상태를 3단계로 예측합니다.
| 열 | 의미 | 값 |
|---|
| CPU 사용률 | 해당 1분간의 평균 (%) | 10~95 |
| ... | | |
| 데이터는 정답 파라미터를 결정한 후 만듭니다. 구한 파라미터와 정답을 비교하여 최적화가 올바르게 되었는지 확인합니다. | | |
만들 파일은 다음 네 가지입니다.
| 파일 | 역할 |
|---|
load_data.py | 가상 모니터링 데이터 생성 |
softmax_model.py | 소프트맥스 함수, 음의 로그 우도(Negative Log-Likelihood), 기울기 $ |
softmax_fit.py | 다항 로지스틱 회귀를 풀고, scikit-learn 및 정답과 비교 |
overflow.py | 소프트맥스 함수의 오버플로우 확인 |
전제 조건
환경 준비
1. 소프트맥스 함수로 세 값을 확률로 변환하기
1-1. 상태별 '점수' 계산
2지선다 로지스틱 회귀에서는 상태별로 하나씩 1차식을 준비합니다.
1-2. 소프트맥스 함수
점수
이 식에는 다음 세 가지 성질이 있습니다.
- 모두 참: $e^{z_k} > 0$ 이므로, $e^{z_k} s_k > 0$
- 합계가 1: 분모는 분자를 모두 더한 것이므로, $s_0 + s_1 + s_2 = 1$
- 점수가 높을수록 크다: $ ext{exp}(z)$는 증가함수이므로, $e^{z_k}$가 클수록 $z_k$도 크고 $s_k$도 커집니다.
예를 들어 점수가
1-3. 전부 같은 값을 더해도 확률은 변하지 않는다
소프트맥스 함수에는 또 하나의 중요한 성질이 있습니다. 점수 전체에 같은 값 $ ext{C}$를 더해도
분자와 분모에 같은
2. 음의 로그 우도와 기울기
2-1. 한 건 데이터의 우도
최대우도추정(Maximum Likelihood Estimation)에서는 '이 파라미터라면, 손안의 데이터가 얼마나 많이 나올까'(우도)가 최대가 되는 파라미터를 선택합니다.
전체 데이터의 우도는 각 데이터 확률의 곱입니다. 로그를 취해 합으로 만들고 마이너스를 붙여 최소화 문제로 만들면 다음 음의 로그 우도(Negative Log-Likelihood)가 됩니다.
logsumexp
을 이용하면 오버플로우 없이 계산할 수 있습니다.
2-2. 기울기는 '예측한 확률 - 정답'
1건 분의
뒤쪽의
예측한 확률에서, 정답이라면 1, 부정답이라면 0을 뺀 값입니다. 예를 들어 정답이 '주의'이고 예측이
2지선다 로지스틱 회귀의 기울기 $ ext{(예측한 확률} - ext{실제값)} imes$
2-3. 2지선다라면, 로지스틱 회귀와 같다
상태가 2개($s_0$과 $s_1$)인 경우 $ ext{(...)}$는 점수의 차이 2지선다의 소프트맥스는 계수의 차(1행目 - 0행目)를 계수로 하는 로지스틱 회귀와 같습니다. 5-3에서 수치로도 확인할 수 있습니다.
3. 풀이 방법을 선택하기
기울기를 0으로 놓은 식은 $ ext{(...)}$
본 기사에서는 SciPy의 minimize의 L-BFGS-B
사용합니다. 준뉴턴법(Quasi-Newton method)의 일종으로, 헤세 행렬(Hessian matrix, 2계 편미분을 나열한 행렬)을 계산하지 않고, 기울기 변화로부터 곡률 정도를 근사하면서 진행합니다. 변수가 많아도 사용하는 메모리가 적게 듭니다.
뉴턴법을 사용하지 않는 이유는 다음 장에 있습니다.
4. 파라미터가 하나로 결정되지 않음
1-3에서, 점수 모두에 같은 값을 더해도 확률이 변하지 않는 것을 보았습니다. 점수 $S$ .확률이 변하지 않으면, 음의 로그 우도(negative log-likelihood)도 변하지 않습니다.
즉, 음의 로그 우도를 최소화하는 것은
이 두 가지에 영향을 줍니다.
| 영향 | 내용 |
|---|
| 뉴턴법 사용이 어려움 | 움직여도 목적 함수가 변하지 않는 방향이 있어, 헤세 행렬이 역행렬을 갖지 못합니다 (특이함). 뉴턴법은 헤세 행렬의 연립 방정식을 풀어야 합니다. |
| 계수를 그대로 비교할 수 없음 | 툴에 따라 무수히 많은 답 중 어떤 것을 반환하는지가 다를 수 있습니다. |
하지만, 확률 예측에는 영향을 주지 않습니다. 어떤 답을 골라도 예측되는 확률은 같기 때문입니다. 계수를 비교하고 싶을 때는, 그중 하나의 상태(여기서는 '정상')의 행을 기준으로 삼아, 각 행에서 뺀 차이를 비교합니다. 모든 행에 더한 벡터는 빼기로 사라집니다.
scikit-learn 사용자 가이드에도,
5. Python으로 풀기
5-1. 데이터 만들기
"""웹 서버의 가상 모니터링 데이터: CPU 사용률・메모리 사용률과, 부하 상태(3단계)"""
import numpy as np
from scipy.special import softmax
...
| 부분 | 의미 |
|---|
TRUE_W | 정답 계수. '정상' 행을 0으로 하고, 나머지 2행을 결정했습니다. |
softmax(X @ TRUE_W.T, axis=1) | 각 분에 대해 3가지 상태가 될 확률을 계산합니다. |
(rng.uniform(...)[:, None] > np.cumsum(P, axis=1)).sum(axis=1) | 확률에 따라 상태를 하나 선택합니다. |
마지막 행은, 확률을 왼쪽부터 더해 나간 값(누적 합)을 사용합니다. 확률이
5-2. 소프트맥스 함수・목적 함수・기울기
"""다항 로지스틱 회귀: 소프트맥스 함수, 음의 로그 우도, 기울기"""
import numpy as np
from scipy.special import logsumexp
...
| 함수 | 식 | 대응하는 절 |
|---|---|
softmax | 각 행의 최댓값을 뺀 후, 1-2식으로 확률을 만듭니다. | 1-2, 5-4 |
neg_log_likelihood | 2-1 | |
gradient | |
2-2 |
np.eye(K)[y]는 정답 상태의 열만 1이고 나머지는 0인 행을 만드는 방식입니다 (one-hot 표현). 예를 들어 y = [1, 0]이면 [[0, 1, 0], [1, 0, 0]]이 됩니다.
SciPy의 minimize는 변수를 1열 벡터로 받습니다. 그래서, 3행 3열의 w.reshape(K, -1)로 행렬로 되돌린 후 계산하고, 기울기는 .ravel()로 1열에 나열하여 반환합니다.
5-3. 풀고, scikit-learn이나 정답과 비교하기
"""다항 로지스틱 회귀를 L-BFGS-B로 풀어, scikit-learn이나 정답과 비교하기"""
import numpy as np
from scipy.optimize import minimize
...
uv run python softmax_fit.py
상태별 건수: {'정상': 127, '주의': 137, '과부하': 36}
L-BFGS-B: 57회, 성공 = True
직접 만든 계수(행이 상태):
...
결과를 순서대로 보겠습니다.
확률 예측은 scikit-learn과 일치했습니다: 예측 확률의 차이는 최대 $ ext{4.4} imes 10^{-5}$입니다. scikit-learn에는 4.4 imes 10^{-5} C=np.inf
지정한 값을 지정하고, 정규화가 없음(최대우도 추정과 동일한 문제)으로 설정했습니다. - 정답에 가까운 값: '정상' 행과의 차이를 비교하면, '주의'는 (0.0917, 0.0364, -6.65)이고, '과부하'는 정답의 (0.2173, 0.1285, -23.65), (0.1, 0.05, -8)에 가까운 값입니다. '과부하' 데이터가 36건으로 적기 때문에 해당 행의 편차가 커졌습니다(0.2, 0.1, -20) -
모든 행에 같은 값을 더해도 음의 로그 우도는 변하지 않는다: 에를 더해도 159.1610 그대로였습니다. 제4장과 같이 최적해는 하나로 결정되지 않습니다 (1, -2, 30) -
2상태라면 로지스틱 회귀와 같다: '정상인지 아닌지'의 2상태로 풀면, 첫 번째 행 − 0행이 로지스틱 회귀의 계수 (0.0954, 0.0393, -6.9659)과 일치했습니다.
직접 구현한 방식과 scikit-learn을 사용한 방식 모두 계수 자체가 거의 같은 값이 되었습니다. 두 경우 모두 세 번째 행을 더하면 각 열이 거의 0이 됩니다.
직접 구현한 방식의 합계가 0이 되는 이유는 기울기(gradient) 형태에 있습니다. 기울기의 세 번째 행을 더하면, 각 데이터에 대해
5-4. 공식 그대로의 소프트맥스는 넘친다 (Overflow)
"""소프트맥스 함수를 공식대로 쓰면 값이 클 때 오버플로우가 발생한다"""
import numpy as np
from softmax_model import softmax
...
uv run python overflow.py
overflow.py:7: RuntimeWarning: overflow encountered in exp
print("공식대로 :", np.exp(z) / np.exp(z).sum())
overflow.py:7: RuntimeWarning: invalid value encountered in divide
...
경고의 맨 앞에는 실제 파일의 절대 경로가 표시됩니다.
공식대로 계산하면, nan (숫자가 아님)이 됩니다. inf (무한대)
)이 됩니다. 분모도 분자도 무한대가 되기 때문에, 무한대 ÷ 무한대는 결정되지 않아 nan입니다.
softmax_model.py의 softmax는 각 행의 최댓값(여기서는 1002)을 빼준 후에
막히면 확인할 항목 (Troubleshooting)
| 오류나 증상 | 원인 | 대처법 |
|---|---|
|RuntimeWarning: overflow encountered in exp가 뜨고 확률이 nan이 됨 | 소프트맥스 함수를 공식대로 계산하고, | 5-4와 같이 최댓값을 빼준 후 계산한다 |
|직접 구현과 라이브러리에서 계수 값이 다름 | 모든 행에 같은 벡터를 더해도 같은 예측이 되고, 답이 하나로 결정되지 않음 | 제4장과 같이 기준 상태의 행과의 차이로 비교한다. 예측 확률로 비교한다 |
|scikit-learn의 예측 확률과 직접 구현한 확률이 약간 다름 | LogisticRegression은 기본적으로 정규화(C=1.0)가 들어 있음 | C=np.inf를 지정하여, 정규화를 하지 않도록 한다 |
|Git Bash에서 출력을 파이프나 리다이렉트로 보내면 한글이 깨짐 | 출력지가 콘솔이 아닐 경우, Python이 Windows의 문자 코드(cp932)로 출력함 | PYTHONUTF8=1 uv run python softmax_fit.py와 같이 UTF-8 모드로 실행한다 |
요약 (Summary)
본 기사에서는 3단계 부하 상태를 예측하는 다항 로지스틱 회귀(polynomial logistic regression)를 목적 함수와 기울기(gradient)로부터 직접 만들어서 풀었습니다.
- 상태별로 1차식 점수(score)를 계산하고, 소프트맥스 함수를 이용해 양의 값으로 합이 1인 확률로 변환한 $z_k$ - 음의 로그 우도(negative log-likelihood)를 최소화했습니다. 기울기는 '예측한 확률 − 정답'에 $\sum_j ig( \log \sum_r e^{z_{jr}} - z_{j, y_j} \big)$을 곱한 합으로, 2지 선택의 로지스틱 회귀와 같은 형태가 되었습니다. $x_j$ - 2상태의 소프트맥스는 계수의 차이를 계수로 하는 로지스틱 회귀와 일치했습니다.
- 모든 행에 같은 벡터를 더해도 예측이 변하지 않기 때문에, 최적해는 하나로 결정되지 않았습니다. 계수는 기준 상태와의 차이로 비교했고, 예측 확률은 scikit-learn과 일치했습니다.
- 공식 그대로의 소프트맥스는 $\text{exp}(1000)$에서 넘쳐서
nan이 되기 쉬우며, 최댓값을 빼는 방식으로 이를 방지할 수 있었습니다.
모든 점수에 같은 값을 더해도 확률이 변하지 않는다는 하나의 성질이, 파라미터가 하나로 결정되지 않는 이유를 설명해 주기도 했고, 계산이 너무 많아지는 것을 방지하는 방법도 되어주었습니다.
참고 자료
논의