머신러닝 기초 (8) - 회귀 평가: RMSE, R², 잔차
회귀 예측을 평가하는 MAE, MSE, RMSE, R²가 각각 무엇을 재는지, 이상치에 어떻게 반응하는지, 그리고 잔차가 남긴 구조를 어떻게 읽는지를 load_diabetes로 확인하는 8편입니다.
머신러닝 기초 시리즈의 8편입니다. 7편의 “분류 평가: 혼동행렬과 ROC-AUC”에 이어집니다.
회귀에는 “맞다/틀리다”가 없다
7편의 분류에서는 예측을 맞았는지 틀렸는지 셀 수 있었습니다. 정답이 스팸인데 스팸으로 예측했으면 맞은 것이고, 아니면 틀린 것입니다. 혼동행렬의 네 칸은 결국 이 맞고 틀림을 분류한 표였습니다.
회귀는 다릅니다. 예측이 연속값이라 “맞았다”는 개념이 성립하지 않습니다. 실제 집값이 3억 2천만 원인데 3억 1천 9백만 원으로 예측했다면, 그건 틀린 걸까요 맞은 걸까요? 회귀에서 묻는 것은 맞고 틀림이 아니라 얼마나 떨어졌는가, 즉 거리입니다.
그래서 회귀 평가의 출발점은 관측치 하나하나의 오차이고, 어려움은 그 다음에 있습니다. 데이터가 수백 개면 오차도 수백 개입니다. 이 수백 개를 하나의 수로 요약하는 방법이 여럿이고, 방법마다 강조하는 것이 다릅니다. 이 편은 그 요약 방법들, 즉 MAE, MSE, RMSE, R²가 각각 무엇을 재는지, 그리고 하나의 수가 감추는 것을 잔차로 어떻게 되살리는지를 봅니다.
오차는 그냥 평균 내면 안 된다
관측치 하나의 오차, 즉 잔차(residual)는 실제값에서 예측값을 뺀 값입니다.
1
2
3
# 잔차 = 실제값 - 예측값
# e = y - y_hat
# 양수면 과소예측(실제보다 낮게 봄), 음수면 과대예측
수백 개의 오차를 요약하는 가장 순진한 방법은 그냥 평균 내는 것입니다. 하지만 이건 작동하지 않습니다. 오차에는 양수(과소예측)와 음수(과대예측)가 섞여 있어서, 그대로 더하면 서로 상쇄되어 0에 가까운 값이 나옵니다. 크게 빗나간 모델도 위아래로 골고루 틀리면 평균 오차는 0에 가까워집니다.
그래서 요약 전에 부호를 없애야 합니다. 부호를 없애는 방법이 둘이고, 여기서 두 갈래가 갈립니다. 절댓값을 쓰면 MAE, 제곱을 쓰면 MSE입니다.
MAE, MSE, RMSE
세 지표는 모두 “오차를 요약한 하나의 수”지만, 부호를 없애는 방식과 단위가 다릅니다.
MAE(Mean Absolute Error, 평균 절대 오차): 오차에 절댓값을 씌워 평균 낸 값입니다.
1
# MAE = mean(abs(y - y_hat))
단위가 타깃 y와 같습니다. 집값을 원 단위로 예측했다면 MAE도 원 단위이고, “평균적으로 이만큼 빗나간다”로 바로 읽힙니다. 해석이 가장 직관적입니다.
MSE(Mean Squared Error, 평균 제곱 오차): 오차를 제곱해 평균 낸 값입니다.
1
# MSE = mean((y - y_hat) ** 2)
제곱하면 단위도 제곱됩니다. 집값이 원이면 MSE는 “원의 제곱”이라 그 자체로는 해석되지 않습니다. 대신 제곱은 미분이 매끄러워 최적화에 쓰기 좋습니다. 2편에서 손실 함수로 최소화하던 그 식이 바로 MSE입니다. 학습할 때 줄이던 값을 평가할 때 다시 꺼내 쓰는 셈입니다.
RMSE(Root Mean Squared Error, 평균 제곱근 오차): MSE에 제곱근을 씌운 값입니다.
1
# RMSE = sqrt(MSE) = sqrt(mean((y - y_hat) ** 2))
제곱근으로 단위를 다시 y로 되돌립니다. 그래서 MAE처럼 타깃과 같은 단위로 읽히면서, MSE의 성질(제곱으로 큰 오차를 강조하는 것)은 그대로 유지합니다. 회귀에서 가장 널리 보고되는 지표입니다.
| 지표 | 정의 | 단위 | 성격 |
|---|---|---|---|
| MAE | 절대 오차의 평균 | y와 같음 | 모든 오차를 동등하게 |
| MSE | 제곱 오차의 평균 | y의 제곱 | 큰 오차를 강조, 최적화용 |
| RMSE | MSE의 제곱근 | y와 같음 | 큰 오차 강조 + 읽기 쉬움 |
한 가지 항상 성립하는 관계가 있습니다. RMSE는 언제나 MAE보다 크거나 같습니다. 그리고 둘의 차이는 오차 크기가 제각각일수록 벌어집니다. 모든 오차가 똑같은 크기면 RMSE와 MAE는 같아지고, 몇몇 오차가 유독 크면 RMSE만 위로 벌어집니다. 이 성질이 다음 절의 핵심입니다.
제곱은 큰 오차를 훨씬 크게 벌한다
MAE와 MSE가 이상치를 다르게 취급하는 이유는 제곱 하나에 있습니다. 오차가 10이면 절댓값에서는 그냥 10이지만, 제곱에서는 100입니다. 오차가 2배가 되면 MAE 기여는 2배, MSE 기여는 4배가 됩니다. 큰 오차일수록 제곱이 불균형하게 크게 벌합니다.
오차 다섯 개로 확인합니다.
1
2
3
4
5
6
7
8
9
10
11
import numpy as np
base = np.array([1.0, -1.0, 1.0, -1.0, 1.0]) # 모두 크기 1
out = np.array([1.0, -1.0, 1.0, -1.0, 10.0]) # 하나만 10으로 튐
for e in (base, out):
mae = np.abs(e).mean()
rmse = np.sqrt((e ** 2).mean())
print(round(mae, 2), round(rmse, 2))
# 1.0 1.0 ← 오차가 모두 같으면 MAE = RMSE
# 2.8 4.56 ← 하나가 튀자 RMSE가 훨씬 크게 반응
오차 하나가 1에서 10으로 커졌을 뿐인데 MAE는 1.0에서 2.8로, RMSE는 1.0에서 4.56으로 뜁니다. RMSE(와 MSE)는 이상치 하나에 크게 흔들리고, MAE는 상대적으로 견고합니다(robust).
어느 쪽이 옳은 게 아니라 문제에 따라 고릅니다. 크게 빗나간 예측이 특히 위험한 문제, 예를 들어 수요를 크게 잘못 예측하면 손실이 급증하는 경우라면 큰 오차를 강하게 벌하는 RMSE가 맞습니다. 반대로 이상치가 데이터 품질 문제에서 온 것이라 거기에 휘둘리고 싶지 않다면 MAE가 안정적입니다.
RMSE와 MAE를 함께 보고하고, 둘의 격차가 크면 소수의 큰 오차가 존재한다는 신호로 읽으세요. 한 지표만 보면 이 정보가 사라집니다.
R²는 오차를 기준선과 비교한다
MAE와 RMSE는 절대적인 값입니다. RMSE가 50이라는 것만으로는 좋은지 나쁜지 알 수 없습니다. 타깃이 0~10 범위면 50은 재앙이고, 0~10000 범위면 훌륭합니다. 좋고 나쁨을 판단하려면 문제의 스케일을 알아야 합니다.
R²(결정계수, R-squared)는 이 스케일 문제를 없앤 상대 지표입니다. 단위가 없고, 기준선과 비교한 값을 냅니다. 기준선은 “아무것도 학습하지 않고 항상 타깃의 평균으로만 예측하는 모델”입니다.
1
2
3
# SS_res = sum((y - y_hat) ** 2) ← 내 모델이 남긴 오차 제곱합
# SS_tot = sum((y - y.mean()) ** 2) ← 평균으로만 예측했을 때의 오차 제곱합
# R2 = 1 - SS_res / SS_tot
읽는 법은 이렇습니다.
- R² = 1: 오차가 전혀 없는 완벽한 예측.
- R² = 0: 평균으로 찍는 것과 똑같은 수준. 모델이 아무것도 보태지 못함.
- R² < 0: 평균으로 찍느니만 못함. 테스트셋에서 충분히 나올 수 있습니다.
흔히 “타깃 분산의 몇 %를 설명하는가”로 해석합니다. R²가 0.45면 타깃이 가진 변동의 약 45%를 모델이 설명하고 나머지는 못 잡았다는 뜻입니다.
R²를 “상관계수의 제곱”으로 기억하는 경우가 많은데, 그 등식은 단순선형회귀의 학습 데이터에서만 우연히 성립합니다. 일반적으로는 위의 정의(1 - SS_res/SS_tot)가 R²이고, 그래서 테스트셋에서는 음수도 나옵니다. 상관계수의 제곱은 결코 음수가 될 수 없다는 점에서도 둘은 다릅니다.
잔차 분석: 하나의 수가 감춘 것을 되살린다
MAE, RMSE, R²는 모두 수백 개의 오차를 하나의 수로 압축합니다. 편하지만, 압축은 정보를 버립니다. 이 지표들은 “얼마나 틀렸나”는 알려줘도 “어디서, 왜 틀렸나”는 감춥니다.
그 감춰진 정보를 되살리는 것이 잔차 분석입니다. 방법은 단순합니다. 잔차(실제값 - 예측값)를 예측값에 대해 산점도로 그립니다. 잘 맞은 모델의 잔차는 0을 중심으로 아무 패턴 없이 무작위로 흩어져야 합니다. 모델이 데이터의 구조를 다 뽑아냈다면, 남은 것은 설명할 수 없는 잡음뿐이어야 하기 때문입니다.
반대로 잔차에 패턴이 보인다면, 그건 모델이 아직 뽑아내지 못한 구조가 남아 있다는 신호입니다.
- 곡선 모양(U자나 역U자): 실제 관계가 휘어 있는데 직선으로 맞췄다는 뜻입니다. 비선형 관계를 선형 모델이 놓친 경우입니다.
- 깔때기 모양(예측값이 커질수록 잔차가 넓게 퍼짐): 오차의 크기가 예측값에 따라 달라지는 이분산(heteroscedasticity)입니다.
- 한쪽으로 치우침(대부분 양수이거나 대부분 음수): 모델이 전반적으로 낮거나 높게 예측하는 편향입니다.
핵심은, 같은 RMSE를 내는 두 모델이라도 잔차 그림은 전혀 다를 수 있다는 점입니다. 하나는 잔차가 깨끗한 잡음이고 다른 하나는 뚜렷한 곡선을 남긴다면, 후자는 손볼 여지가 남아 있다는 뜻입니다. 단일 지표는 이 차이를 보여주지 못합니다.
실습: load_diabetes로 전부 계산하기
회귀용 내장 데이터셋 load_diabetes로 앞의 지표를 모두 계산합니다. 당뇨 환자 442명의 열 가지 지표(나이, BMI, 혈압 등)로 1년 뒤 질병 진행도를 예측하는 문제이고, 타깃은 대략 25~346 사이입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
X, y = load_diabetes(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
model = LinearRegression().fit(X_train, y_train)
pred = model.predict(X_test)
mae = mean_absolute_error(y_test, pred)
mse = mean_squared_error(y_test, pred)
rmse = np.sqrt(mse) # 버전 무관하게 안전한 RMSE 계산
r2 = r2_score(y_test, pred)
print(round(mae, 2), round(rmse, 2), round(r2, 4))
# 42.79 53.85 0.4526
세 지표를 함께 읽습니다. MAE 42.79는 평균 152 규모의 타깃에서 평균 약 43만큼 빗나간다는 뜻입니다. RMSE 53.85는 그보다 큰데, 이 격차(약 11)가 크게 빗나간 예측이 일부 있다는 신호입니다. R² 0.4526은 타깃 변동의 약 45%만 설명했다는 뜻으로, 이 데이터에서 단순 선형회귀의 한계를 보여줍니다.
r2_score 대신 회귀 모델의 .score()를 써도 같은 값이 나옵니다. scikit-learn 회귀 모델의 기본 점수가 R²이기 때문입니다.
1
model.score(X_test, y_test) # 0.4526, r2_score와 동일
기준선과 비교하면 R²의 의미가 분명해집니다. 학습 데이터의 평균으로만 예측하면 어떻게 되는지 봅니다.
1
2
baseline = np.full_like(y_test, y_train.mean(), dtype=float)
print(round(r2_score(y_test, baseline), 4)) # -0.012, 0보다 약간 작음
평균으로만 찍은 기준선의 R²는 약 -0.012로, 0을 살짝 밑돕니다. 학습셋 평균이 테스트셋 평균과 정확히 같지 않아서 0에 못 미친 것입니다. 우리 모델의 0.45는 이 기준선 위에서 얻은 값입니다.
이제 잔차를 계산하고 예측값에 대해 그립니다.
1
2
3
4
5
6
7
8
9
10
11
import matplotlib.pyplot as plt
resid = y_test - pred # 잔차 = 실제값 - 예측값
plt.scatter(pred, resid, alpha=0.6)
plt.axhline(0, color="red", linestyle="--") # 잔차 0 기준선
plt.xlabel("predicted")
plt.ylabel("residual")
plt.show()
print(round(resid.mean(), 2)) # 3.91, 0 근처
잔차 평균은 3.91로 0 근처에 있고, 산점도에서 점들이 빨간 기준선을 중심으로 뚜렷한 곡선이나 깔때기 없이 흩어져 있으면 선형 모델이 이 데이터에서 잡을 수 있는 구조는 대체로 잡았다고 볼 수 있습니다. R²가 0.45에 그치는 것은 모델이 구조를 놓쳐서라기보다, 이 열 개 특성만으로는 설명되지 않는 변동이 데이터에 크게 남아 있기 때문입니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 잔차 | 실제값 - 예측값. 그냥 평균 내면 부호가 상쇄돼 0이 되므로 절댓값이나 제곱으로 요약 |
| MAE | 절대 오차의 평균. 타깃과 같은 단위, 모든 오차를 동등하게 취급, 이상치에 견고 |
| MSE | 제곱 오차의 평균. 단위는 타깃의 제곱, 최적화에 쓰는 손실 함수와 같은 식 |
| RMSE | MSE의 제곱근. 타깃 단위로 읽히면서 큰 오차를 강조. 항상 MAE 이상 |
| MAE vs RMSE | 둘의 격차가 크면 소수의 큰 오차가 있다는 신호. 함께 보고한다 |
| R² | 평균 예측 기준선과 비교한 상대 지표. 1은 완벽, 0은 평균 수준, 음수도 가능 |
| 잔차 분석 | 잔차에 남은 패턴(곡선, 깔때기, 치우침)은 모델이 놓친 구조. 단일 지표가 감춘 것을 되살린다 |
다음 편에서는 지금까지 평가만 해온 그 모델의 안을 엽니다. 선형 회귀가 실제로 무엇을 학습하는지, 같은 골격이 어떻게 로지스틱 회귀로 분류까지 확장되는지를 봅니다.