머신러닝 기초 (2) - 손실 함수: 모델이 최소화하는 것
학습이란 결국 손실 함수를 최소화하는 일입니다. 손실이 왜 필요한지, 회귀의 MSE와 이진분류의 로그손실이 무엇을 재는지를 numpy와 sklearn으로 직접 확인한 2편입니다.
머신러닝 기초 시리즈의 2편입니다. 1편의 “지도학습이란: 회귀와 분류”에 이어집니다.
1편에서 지도학습을 (X, y) 쌍에서 X를 y로 보내는 함수를 찾는 일로 정의했습니다. 그런데 “함수를 찾는다”는 말은 아직 절차가 아닙니다. 후보 함수가 여럿일 때 무엇이 더 나은지 가릴 기준이 없으면, 모델은 어느 방향으로도 나아갈 수 없습니다.
그 기준이 손실 함수입니다. 이 편은 손실이 무엇을 재는지, 왜 하나의 수여야 하는지, 그리고 회귀와 이진분류에서 표준으로 쓰는 두 손실이 각각 무엇을 벌하는지를 봅니다. 이 손실을 실제로 줄이는 절차(경사하강)는 3편에서 다루고, 여기서는 “무엇을 줄이려 하는가”까지만 정합니다.
손실 함수란 무엇인가
손실 함수(loss function)는 예측값과 실제값의 차이를 하나의 수로 요약하는 함수입니다. 이 수가 클수록 모델이 많이 틀렸다는 뜻이고, 작을수록 잘 맞혔다는 뜻입니다. 학습의 목표는 단순합니다. 이 수를 가능한 한 작게 만드는 것입니다.
여기서 두 가지를 구분해야 합니다.
- 예측 하나의 오차: 개별 샘플에서 예측이 실제와 얼마나 어긋났는가.
- 손실: 그 오차들을 데이터 전체에 걸쳐 하나의 수로 합친 값. 보통 평균을 씁니다.
모델을 학습시킨다는 것은 손실이라는 이 하나의 수를 최소화하도록 모델의 파라미터를 조정하는 일입니다. 그래서 손실 함수를 목적 함수(objective function) 라고도 부릅니다. 모델이 겨냥하는 목표라는 뜻입니다.
왜 하나의 수가 필요한가
“예측이 얼마나 틀렸나”를 굳이 하나의 수로 눌러 담는 데는 두 가지 이유가 있습니다.
비교하기 위해서입니다. 모델 A와 모델 B 중 무엇이 나은지 판단하려면 같은 잣대로 잰 값이 있어야 합니다. “A는 여기서 잘 맞히고 저기서 못 맞힌다”는 서술로는 순위를 매길 수 없습니다. 손실이 3.1과 2.4라면, 낮은 쪽이 낫다고 곧바로 말할 수 있습니다.
개선하기 위해서입니다. 손실은 모델 파라미터의 함수입니다. 파라미터를 조금 바꾸면 손실도 따라 바뀝니다. 이 대응이 있으면 “어느 방향으로 파라미터를 움직여야 손실이 줄어드는가”를 물을 수 있습니다. 학습이 방향을 갖는 절차가 되는 것은 손실이 하나의 수이기 때문입니다.
손실 함수는 데이터를 잘 설명하려고 만드는 것이 아니라, 모델을 개선할 방향을 만들려고 정의하는 것이다. 그래서 뒤에 나오듯 분류에서는 “정확도”가 아니라 미분 가능한 손실을 따로 쓴다.
문제 유형에 따라 어떤 손실이 자연스러운지가 달라집니다. 회귀와 이진분류의 표준 손실을 차례로 봅니다.
회귀의 손실: MSE
회귀는 연속값을 예측합니다. 예측이 실제에서 얼마나 벗어났는지는 두 값의 차이, 즉 오차 y - 예측으로 잽니다. 문제는 이 오차가 양수일 수도 음수일 수도 있어서, 그냥 평균 내면 서로 상쇄되어 버린다는 점입니다.
MSE(Mean Squared Error, 평균제곱오차) 는 오차를 제곱해 이 문제를 없앱니다. 말로 풀면 “(실제값 - 예측값)을 제곱해 전부 평균한 값”입니다. numpy로 그대로 옮기면 정의가 곧 코드입니다.
1
2
3
4
5
6
7
8
import numpy as np
y_true = np.array([3.0, 5.0, 2.5, 7.0]) # 실제값
y_pred = np.array([2.5, 5.5, 2.0, 8.0]) # 모델 예측
error = y_true - y_pred # 오차, 부호가 섞임: [ 0.5, -0.5, 0.5, -1.0]
mse = np.mean(error ** 2) # 제곱해서 평균
mse # 0.4375
제곱이 하는 일은 두 가지입니다. 첫째, 부호를 없애 오차가 상쇄되지 않게 합니다. 둘째, 큰 오차에 더 큰 벌점을 줍니다. 오차 1.0은 제곱하면 1.0이지만 오차 0.5는 0.25입니다. 두 배 어긋난 예측이 손실에는 네 배로 반영됩니다. 그래서 MSE는 크게 빗나간 소수의 샘플(이상치)에 민감합니다.
같은 값을 sklearn으로도 확인할 수 있습니다. 직접 구현한 식과 라이브러리가 같은 수를 내는지 보는 습관은 개념을 코드로 붙잡는 데 도움이 됩니다.
1
2
3
from sklearn.metrics import mean_squared_error
mean_squared_error(y_true, y_pred) # 0.4375, 위 numpy 결과와 동일
제곱 대신 절댓값을 쓰면 MAE(Mean Absolute Error) 가 됩니다. np.mean(np.abs(y_true - y_pred))로, 위 예에서는 0.625입니다. MAE는 오차를 제곱하지 않으니 이상치에 덜 민감합니다. 손실로 무엇을 고르느냐는 “큰 오차를 얼마나 심각하게 볼 것인가”에 대한 선택이기도 합니다. 회귀 평가지표를 본격적으로 비교하는 것은 8편에서 다룹니다.
분류의 손실: 로그손실
이진분류는 두 범주 중 하나를 맞히는 문제입니다. 여기서 “정확도(맞힌 비율)를 손실로 쓰면 되지 않나”라는 생각이 자연스럽게 듭니다. 그런데 정확도는 학습의 손실로 쓰기에 두 가지 결함이 있습니다.
- 확신의 정도를 버립니다. 정답 확률을 0.51로 본 예측과 0.99로 본 예측은 둘 다 “정답 1건”으로 똑같이 셉니다. 얼마나 확신했는지가 사라집니다.
- 개선 방향을 주지 못합니다. 파라미터를 조금 바꿔도 맞힘/틀림이 뒤집히기 전까지 정확도는 그대로입니다. 그러다 어느 순간 계단처럼 툭 바뀝니다. 이런 계단 모양 함수는 “어느 쪽으로 움직여야 나아지는가”를 알려주지 못합니다.
그래서 분류 모델은 0 또는 1을 곧장 내지 않고 양성일 확률(0과 1 사이의 값)을 냅니다. 그리고 이 확률이 실제 정답에 얼마나 가까운지를 손실로 잽니다. 그 표준이 로그손실(log loss), 다른 이름으로 크로스엔트로피(cross-entropy) 입니다.
정의를 말로 풀면 이렇습니다. 실제 정답 y가 1이면 손실은 -log(예측확률), 0이면 -log(1 - 예측확률)입니다. 즉 정답인 쪽에 부여한 확률에 로그를 씌워 음수를 취하고, 전체 샘플에 대해 평균합니다. log는 자연로그입니다.
핵심은 이 식이 확신에 찬 오답에 매우 큰 벌점을 준다는 데 있습니다. 정답일 확률을 0.99로 잘 맞히면 손실은 -log(0.99)로 약 0.01에 불과하지만, 정답을 0.01로 잘못 확신하면 -log(0.01)로 약 4.6까지 치솟습니다. 확률이 0에 가까워질수록 로그는 음의 무한대로 발산합니다. 자신 있게 틀릴수록 가혹하게 벌하는 구조입니다.
numpy로 정의를 그대로 확인한 뒤, 실무에서는 sklearn의 log_loss를 씁니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import numpy as np
from sklearn.metrics import log_loss
y_true = [1, 0, 1, 0]
p_good = [0.9, 0.1, 0.8, 0.2] # 정답 쪽에 높은 확률을 준 예측
p_bad = [0.1, 0.9, 0.2, 0.8] # 정답 쪽에 낮은 확률을 준 예측 (확신에 찬 오답)
# 정의를 numpy로 직접: y=1이면 -log(p), y=0이면 -log(1-p)
y = np.array(y_true); p = np.array(p_good)
manual = -np.mean(y * np.log(p) + (1 - y) * np.log(1 - p))
manual # 0.1643
log_loss(y_true, p_good) # 0.1643, 잘 맞힌 예측: 손실이 작다
log_loss(y_true, p_bad) # 1.9560, 확신에 찬 오답: 손실이 크다
같은 4개 샘플인데 손실이 0.16과 1.96으로 갈립니다. 로그손실은 “몇 개를 맞혔나”가 아니라 “정답에 얼마나 높은 확률을 걸었나”를 재기 때문입니다.
예측 확률이 정확히 0이나 1이면
-log(0)이 무한대가 되어 손실이 발산한다. sklearn의log_loss는 확률을 0과 1에서 아주 조금 떨어뜨리는 클리핑으로 이를 막지만, 직접np.log를 쓸 때는log(0)경고나inf를 만날 수 있다.
내장 데이터로도 확인해 봅니다. load_breast_cancer(양성/악성 이진분류)에 분류 모델을 학습시키고, predict_proba로 얻은 확률을 log_loss에 넣습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import log_loss
X, y = load_breast_cancer(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
model = LogisticRegression(max_iter=10000).fit(X_train, y_train)
proba = model.predict_proba(X_test)[:, 1] # 양성 클래스일 확률
log_loss(y_test, proba) # 0.0825, 확률이 실제 정답에 가까울수록 작아진다
model.score(X_test, y_test) # 0.965, 참고용 정확도(맞힌 비율)
정확도 0.965는 “얼마나 맞혔나”를, 로그손실 0.0825는 “얼마나 확신 있게 맞혔나”를 각각 요약합니다. 모델을 학습시킬 때 실제로 줄이는 대상은 후자입니다. 분류 성능을 여러 각도에서 평가하는 지표들(혼동행렬, ROC-AUC 등)은 7편에서 따로 다룹니다.
손실을 실제로 줄이는 법은 3편에서
여기까지 정한 것은 “무엇을 최소화할 것인가”입니다. 회귀는 MSE, 이진분류는 로그손실이라는 목표를 세웠습니다. 남은 질문은 하나입니다. 그 목표에 실제로 어떻게 도달하는가. 즉 손실을 가장 작게 만드는 파라미터를 어떻게 찾는가.
손실은 파라미터의 함수이므로, “손실이 줄어드는 방향으로 파라미터를 조금씩 움직인다”는 전략이 가능합니다. 이 방향을 기울기(gradient)로 잡아 반복적으로 내려가는 절차가 경사하강법입니다. 다음 편에서 다룹니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 손실 함수 | 예측과 실제의 차이를 하나의 수로 요약. 학습이 최소화하는 목적 함수 |
| 왜 하나의 수인가 | 모델을 비교하고 개선 방향을 잡으려면 파라미터의 함수인 척도가 필요 |
| MSE (회귀) | (실제 - 예측)을 제곱해 평균. 큰 오차에 큰 벌점, 이상치에 민감 |
| MAE (회귀) | 제곱 대신 절댓값. 이상치에 덜 민감 |
| 로그손실 (이진분류) | 정답 쪽에 건 확률에 -log. 확신에 찬 오답에 매우 큰 벌점 |
| 정확도를 손실로 안 쓰는 이유 | 확신을 버리고 계단형이라, 개선 방향을 주지 못한다 |