머신러닝 기초 (4) - 과대적합과 일반화: train/valid/test와 편향-분산
훈련 데이터에서 잘하는 것과 새 데이터에서 잘하는 것은 다릅니다. 과소적합, 과대적합의 정의, train/validation/test를 나누는 이유, 편향-분산 트레이드오프를 다항 회귀 과적합 시연으로 정리한 4편입니다.
머신러닝 기초 시리즈의 4편입니다. 3편의 “경사하강법: 손실을 줄이는 법”에 이어집니다.
3편까지는 모델이 손실을 줄이도록 만드는 법을 봤습니다. 경사하강법은 주어진 데이터에서 손실을 작게 만드는 파라미터를 찾습니다. 그런데 여기에는 함정이 있습니다. 훈련 데이터의 손실을 0에 가깝게 만드는 것 자체는 목표가 아닙니다. 우리가 진짜로 원하는 것은 학습에 쓰지 않은 새 데이터에서도 잘 맞히는 것, 즉 일반화(generalization)입니다.
이 편은 그 간극을 다룹니다. 훈련 성능과 실제 성능이 왜 벌어지는지(과소적합, 과대적합), 그 간극을 재려면 데이터를 어떻게 나눠야 하는지(train/validation/test), 이 현상을 한 축으로 꿰는 편향-분산 트레이드오프를 차례로 봅니다. 마지막에는 다항 회귀의 차수를 올리며 과대적합이 실제로 어떻게 나타나는지 직접 확인합니다.
일반화: 실제로 원하는 것
모델을 학습시키는 이유는 훈련 데이터를 잘 맞히기 위해서가 아닙니다. 훈련 데이터의 정답은 이미 알고 있습니다. 목표는 아직 보지 못한 데이터, 즉 앞으로 들어올 실제 입력에서 잘 맞히는 것입니다. 이 능력을 일반화라고 부릅니다.
문제는 학습 중에 우리가 볼 수 있는 것이 훈련 데이터뿐이라는 점입니다. 훈련 손실은 얼마든지 줄일 수 있지만, 그것이 새 데이터에서의 성능을 보장하지는 않습니다. 훈련 손실과 일반화 성능이 벌어지는 두 가지 방식이 과소적합과 과대적합입니다.
과소적합과 과대적합
과소적합(underfitting): 모델이 데이터의 구조를 담기에 너무 단순한 경우입니다. 실제 관계가 곡선인데 직선으로 맞추려는 상황이 여기 해당합니다. 훈련 데이터에서조차 잘 맞히지 못하므로, train 성능과 test 성능이 둘 다 낮습니다.
과대적합(overfitting): 모델이 너무 유연해서 훈련 데이터의 진짜 패턴뿐 아니라 우연히 섞인 노이즈까지 외워버린 경우입니다. 훈련 성능은 매우 높습니다. 그러나 그 노이즈는 새 데이터에는 없으므로 test 성능은 낮습니다. 즉 train은 높은데 test는 낮고, 둘의 간극이 큽니다.
| 상태 | train 성능 | test 성능 | 진단 |
|---|---|---|---|
| 과소적합 | 낮음 | 낮음 | 모델이 너무 단순함 |
| 적절 | 높음 | 높음(비슷) | 좋음 |
| 과대적합 | 매우 높음 | 낮음 | 노이즈까지 외움 |
여기서 하나의 진단 신호가 나옵니다. train 성능과 test 성능의 간극입니다. 둘 다 낮으면 과소적합, 간극이 크게 벌어져 있으면 과대적합입니다. 그런데 이 신호를 읽으려면 애초에 train과 test를 나눠 두어야 합니다.
train / validation / test 분할
test 성능을 알려면 test 데이터가 필요합니다. 그런데 학습에 쓴 데이터로 성능을 재면 안 됩니다. 이미 외운 답을 다시 묻는 셈이라 성능이 부풀려집니다. 그래서 데이터를 나눠, 학습에 쓰지 않은 부분으로 성능을 추정합니다. 보통 세 몫으로 나눕니다.
- train: 모델 파라미터를 학습(
fit)하는 데 쓰는 몫. - validation: 여러 후보 중 하나를 고르는 몫. 다항 차수, 하이퍼파라미터, 모델 종류를 이 점수로 비교합니다.
- test: 최종 선택된 모델을 딱 한 번 평가하는 몫. 실제 성능의 정직한 추정치입니다.
validation과 test를 굳이 분리하는 이유가 중요합니다. validation 점수를 보고 모델을 고르는 순간, validation은 선택 과정에 개입한 데이터가 됩니다. 여러 후보 중 validation에서 가장 잘 나온 것을 뽑았으니, 그 점수는 더 이상 “본 적 없는 데이터”의 성능이 아니라 조금 낙관적으로 치우친 값입니다. 그래서 아무 선택에도 쓰지 않은 test를 따로 남겨, 최종 성능만 여기서 잽니다.
train_test_split은 한 번에 두 덩이로만 나누므로, 세 몫은 두 번에 나눠 만듭니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
X, y = load_diabetes(return_X_y=True) # 회귀용 내장 데이터: 표본 442, 특성 10
# 1차 분할: 전체에서 test 20%를 떼어 둔다
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=42)
# 2차 분할: 남은 80%를 train 60% / validation 20%로 (0.25 × 0.8 = 0.2)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=42)
X_train.shape[0], X_val.shape[0], X_test.shape[0] # (264, 89, 89)
random_state=42로 난수를 고정해, 다시 실행해도 같은 분할이 나오게 합니다. 재현 가능성을 위한 관례입니다.
test는 최종 평가 때 딱 한 번만 봅니다. test 점수를 확인한 뒤 그 결과를 근거로 모델을 다시 고치면, test도 선택에 개입한 데이터가 되어 오염됩니다. 이렇게 평가용 데이터의 정보가 모델로 새어드는 것을 데이터 누수라 부르며, 13편에서 데이터 누수와 파이프라인으로 따로 다룹니다.
데이터를 이렇게 한 번 잘라 validation을 쓰는 방식에는 약점이 있습니다. 어느 표본이 우연히 validation에 걸렸느냐에 따라 점수가 흔들립니다. 이 불안정을 줄이는 것이 5편의 교차검증입니다.
편향-분산 트레이드오프
과소적합과 과대적합을 하나의 축으로 설명하는 틀이 편향-분산(bias-variance) 분해입니다. 새 데이터에서의 평균 오차는 대략 세 조각으로 나눠 볼 수 있습니다. 편향의 제곱 + 분산 + 데이터 자체의 줄일 수 없는 노이즈입니다. 마지막 항은 우리가 손댈 수 없으니, 통제 대상은 앞의 둘입니다.
- 편향(bias): 모델이 상정한 형태가 실제 관계를 못 맞추어 생기는 오차입니다. 곡선 관계를 직선으로 근사하려는 것처럼 가정이 지나치게 단순하면 편향이 큽니다. 편향이 크면 train에서조차 잘 못 맞히니, 이것이 과소적합입니다.
- 분산(variance): 훈련 데이터가 조금 바뀌면 학습된 모델이 얼마나 크게 달라지는가입니다. 모델이 지나치게 유연하면 표본의 우연한 출렁임까지 그대로 따라가므로 분산이 큽니다. 분산이 크면 새 데이터에서 무너지니, 이것이 과대적합입니다.
둘 사이에는 맞바꿈(트레이드오프)이 있습니다. 모델 복잡도를 올리면 편향은 줄지만 분산은 늘고, 낮추면 반대가 됩니다. 새 데이터에서의 오차는 이 둘의 합이므로, 합이 최소가 되는 중간 지점에서 일반화 성능이 가장 좋습니다. 한쪽 끝(너무 단순)은 편향이 지배해 과소적합, 다른 끝(너무 복잡)은 분산이 지배해 과대적합입니다.
이 “복잡도”를 직접 조절하는 값 하나를 바꿔 보면, 과소적합에서 과대적합으로 넘어가는 과정을 눈으로 볼 수 있습니다. 다항 회귀의 차수가 그런 값입니다.
차수를 올리며 과적합 지켜보기
PolynomialFeatures는 원래 특성들의 거듭제곱과 서로 간의 곱을 새 특성으로 추가합니다. degree를 올릴수록 특성 수가 급격히 늘고, 그만큼 모델이 표현할 수 있는 곡선이 복잡해집니다. 즉 degree가 복잡도를 조절하는 값입니다. 앞에서 나눈 X_train/X_val을 그대로 쓰고, 차수만 1에서 4까지 올리며 train R²와 validation R²를 비교합니다.
1
2
3
4
5
6
7
8
9
10
11
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
for degree in [1, 2, 3, 4]:
poly = PolynomialFeatures(degree=degree, include_bias=False)
X_train_poly = poly.fit_transform(X_train) # 변환 기준은 train으로만 학습
X_val_poly = poly.transform(X_val) # 같은 변환을 val에 그대로 적용
model = LinearRegression().fit(X_train_poly, y_train)
train_r2 = model.score(X_train_poly, y_train) # score는 회귀에서 R²
val_r2 = model.score(X_val_poly, y_val)
print(degree, X_train_poly.shape[1], round(train_r2, 3), round(val_r2, 1))
출력을 표로 정리하면 이렇습니다.
| degree | 특성 수 | train R² | val R² |
|---|---|---|---|
| 1 | 10 | 0.520 | 0.522 |
| 2 | 65 | 0.628 | 0.360 |
| 3 | 285 | 1.000 | -858.9 |
| 4 | 1000 | 1.000 | -27.4 |
한 줄씩 읽어 봅니다.
- degree 1: train과 val이 0.52로 거의 같습니다. 간극이 없으니 과대적합은 아닙니다. 다만 둘 다 그리 높지 않아, 편향이 다소 큰, 더 짜낼 여지가 있는 쪽입니다.
- degree 2: train은 0.63으로 오르는데 val은 0.36으로 떨어집니다. 간극이 벌어지기 시작합니다. 과대적합의 초입입니다.
- degree 3: train R²가 1.000입니다. 특성 수(285)가 훈련 표본 수(264)를 넘어서면서, 모델이 훈련 데이터를 통째로 외워 버렸습니다. 그런데 val R²는 깊은 음수로 폭락합니다. 외운 것이 새 데이터에는 전혀 맞지 않는다는 뜻입니다.
- degree 4: 여전히 train은 완벽하지만 val은 음수입니다.
model.score가 회귀에서 돌려주는 값은 결정계수 R²입니다. 1이면 완벽, 0이면 “그냥 평균값으로 찍는 것과 같음”을 뜻하고, 음수는 평균으로 찍느니만 못하다는 뜻입니다. degree 3과 4의 val R²가 음수라는 것은 그만큼 예측이 망가졌다는 신호입니다. 지표 자체는 8편 회귀 평가에서 자세히 다룹니다. 위 음수 값은 환경과 분할에 따라 정확한 크기가 다르지만, 깊은 음수라는 점은 같습니다.
여기서 핵심은 이것입니다. train 점수만 보면 차수를 올릴수록 모델이 좋아 보입니다. degree 3에서는 R²가 1.000이니 완벽해 보이기까지 합니다. 과대적합은 validation 점수를 함께 봐야 드러납니다. 훈련 성능은 오르는데 검증 성능이 떨어지기 시작하는 지점, 그 직전이 이 데이터가 감당할 수 있는 복잡도의 상한입니다.
이 데이터에서는 낮은 차수가 가장 나았지만, “항상 단순한 모델이 낫다”는 결론은 틀립니다. 데이터가 더 많거나 관계가 더 복잡하면 최적 차수는 올라갑니다. 요점은 특정 차수가 정답이라는 게 아니라, 최적 복잡도는 데이터가 정하며 그것을 validation으로 찾는다는 것입니다.
그래서 무엇을 하나
이 편이 남긴 두 가지 문제는 각각 다음 편들로 이어집니다.
- validation을 한 번만 잘라 쓰면 어느 표본이 걸렸느냐에 따라 점수가 흔들립니다. 데이터를 여러 번 나눠 평균 내는 교차검증으로 이 불안정을 줄이고, 차수 같은 하이퍼파라미터를 더 믿을 만하게 고릅니다. 5편 교차검증과 하이퍼파라미터 튜닝에서 코드까지 다룹니다.
- 복잡한 모델을 쓰되 과대적합을 억누르는 다른 길도 있습니다. 계수가 지나치게 커지지 못하도록 손실에 벌점을 더하는 규제(Ridge, Lasso)입니다. degree 3에서 폭주한 계수를 눌러 검증 성능을 되살립니다. 6편 규제에서 다룹니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 일반화 | 목표는 훈련 데이터가 아니라 새 데이터에서의 성능 |
| 과소적합 | 모델이 너무 단순. train, test 모두 낮음 (편향 지배) |
| 과대적합 | 노이즈까지 외움. train 높고 test 낮음 (분산 지배) |
| train | 파라미터를 학습(fit)하는 몫 |
| validation | 차수, 하이퍼파라미터, 모델을 고르는 몫 |
| test | 최종 성능을 딱 한 번 재는 몫. 튜닝에 쓰지 않음 |
| 편향-분산 | 복잡도↑ → 편향↓, 분산↑. 합이 최소인 지점이 목표 |
| 과적합 신호 | train은 오르는데 val이 벌어지며 떨어지는 순간 |