포스트

머신러닝 기초 (5) - Overfitting and Generalization: Train/Valid/Test and Bias-Variance

훈련 데이터에서 잘하는 것과 새 데이터에서 잘하는 것은 다릅니다. 과소적합, 과대적합의 정의, train/validation/test를 나누는 이유, 편향-분산 트레이드오프를 다항 회귀 과적합 시연으로 정리한 5편입니다.

머신러닝 기초 (5) - Overfitting and Generalization: Train/Valid/Test and Bias-Variance

머신러닝 기초 시리즈의 5편입니다. 4편의 “Gradient Descent: How to Reduce the Loss”에 이어집니다.

4편까지는 모델이 손실을 줄이도록 만드는 법을 봤습니다. 경사하강법은 주어진 데이터에서 손실을 작게 만드는 파라미터를 찾습니다. 그런데 여기에는 함정이 있습니다. 훈련 데이터의 손실을 0에 가깝게 만드는 것 자체는 목표가 아닙니다. 우리가 진짜로 원하는 것은 학습에 쓰지 않은 새 데이터에서도 잘 맞히는 것, 즉 일반화(generalization)입니다.

이 편은 그 간극을 다룹니다. 훈련 성능과 실제 성능이 왜 벌어지는지(과소적합, 과대적합), 그 간극을 재려면 데이터를 어떻게 나눠야 하는지(train/validation/test), 이 현상을 한 축으로 꿰는 편향-분산 트레이드오프를 차례로 봅니다. 마지막에는 다항 회귀의 차수를 올리며 과대적합이 실제로 어떻게 나타나는지 직접 확인합니다.

일반화: 우리가 진짜 원하는 것

모델을 학습시키는 이유는 훈련 데이터를 잘 맞히기 위해서가 아닙니다. 훈련 데이터의 정답은 이미 알고 있습니다. 목표는 아직 보지 못한 데이터, 즉 앞으로 들어올 실제 입력에서 잘 맞히는 것입니다. 이 능력을 일반화라고 부릅니다.

문제는 학습 중에 우리가 볼 수 있는 것이 훈련 데이터뿐이라는 점입니다. 훈련 손실은 얼마든지 줄일 수 있지만, 그것이 새 데이터에서의 성능을 보장하지는 않습니다. 훈련 손실과 일반화 성능이 벌어지는 두 가지 방식이 과소적합과 과대적합입니다.

과소적합과 과적합

과소적합(underfitting): 모델이 데이터의 구조를 담기에 너무 단순한 경우입니다. 실제 관계가 곡선인데 직선으로 맞추려는 상황이 여기 해당합니다. 훈련 데이터에서조차 잘 맞히지 못하므로, train 성능과 test 성능이 둘 다 낮습니다.

과대적합(overfitting): 모델이 너무 유연해서 훈련 데이터의 진짜 패턴뿐 아니라 우연히 섞인 노이즈까지 외워버린 경우입니다. 훈련 성능은 매우 높습니다. 그러나 그 노이즈는 새 데이터에는 없으므로 test 성능은 낮습니다. 즉 train은 높은데 test는 낮고, 둘의 간극이 큽니다.

상태train 성능test 성능진단
과소적합낮음낮음모델이 너무 단순함
적절높음높음(비슷)좋음
과대적합매우 높음낮음노이즈까지 외움

여기서 하나의 진단 신호가 나옵니다. train 성능과 test 성능의 간극입니다. 둘 다 낮으면 과소적합, 간극이 크게 벌어져 있으면 과대적합입니다. 그런데 이 신호를 읽으려면 애초에 train과 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도 선택에 개입한 데이터가 되어 오염됩니다. 이렇게 평가용 데이터의 정보가 모델로 새어드는 것을 데이터 누수라 부르며, 14편에서 데이터 누수와 파이프라인으로 따로 다룹니다.

데이터를 이렇게 한 번 잘라 validation을 쓰는 방식에는 약점이 있습니다. 어느 표본이 우연히 validation에 걸렸느냐에 따라 점수가 흔들립니다. 이 불안정을 줄이는 것이 6편의 교차검증입니다.

편향-분산 트레이드오프

과소적합과 과대적합을 하나의 축으로 설명하는 틀이 편향-분산(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²
1100.5200.522
2650.6280.360
32851.000-858.9
410001.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²가 음수라는 것은 그만큼 예측이 망가졌다는 신호입니다. 지표 자체는 9편 회귀 평가에서 자세히 다룹니다. 위 음수 값은 환경과 분할에 따라 정확한 크기가 다르지만, 깊은 음수라는 점은 같습니다.

여기서 핵심은 이것입니다. train 점수만 보면 차수를 올릴수록 모델이 좋아 보입니다. degree 3에서는 R²가 1.000이니 완벽해 보이기까지 합니다. 과대적합은 validation 점수를 함께 봐야 드러납니다. 훈련 성능은 오르는데 검증 성능이 떨어지기 시작하는 지점, 그 직전이 이 데이터가 감당할 수 있는 복잡도의 상한입니다.

이 데이터에서는 낮은 차수가 가장 나았지만, “항상 단순한 모델이 낫다”는 결론은 틀립니다. 데이터가 더 많거나 관계가 더 복잡하면 최적 차수는 올라갑니다. 요점은 특정 차수가 정답이라는 게 아니라, 최적 복잡도는 데이터가 정하며 그것을 validation으로 찾는다는 것입니다.

그래서 어떻게 하는가

이 편이 남긴 두 가지 문제는 각각 다음 편들로 이어집니다.

  • validation을 한 번만 잘라 쓰면 어느 표본이 걸렸느냐에 따라 점수가 흔들립니다. 데이터를 여러 번 나눠 평균 내는 교차검증으로 이 불안정을 줄이고, 차수 같은 하이퍼파라미터를 더 믿을 만하게 고릅니다. 6편 교차검증과 하이퍼파라미터 튜닝에서 코드까지 다룹니다.
  • 복잡한 모델을 쓰되 과대적합을 억누르는 다른 길도 있습니다. 계수가 지나치게 커지지 못하도록 손실에 벌점을 더하는 규제(Ridge, Lasso)입니다. degree 3에서 폭주한 계수를 눌러 검증 성능을 되살립니다. 7편 규제에서 다룹니다.

정리

개념한 줄 요약
일반화목표는 훈련 데이터가 아니라 새 데이터에서의 성능
과소적합모델이 너무 단순. train, test 모두 낮음 (편향 지배)
과대적합노이즈까지 외움. train 높고 test 낮음 (분산 지배)
train파라미터를 학습(fit)하는 몫
validation차수, 하이퍼파라미터, 모델을 고르는 몫
test최종 성능을 딱 한 번 재는 몫. 튜닝에 쓰지 않음
편향-분산복잡도↑ → 편향↓, 분산↑. 합이 최소인 지점이 목표
과적합 신호train은 오르는데 val이 벌어지며 떨어지는 순간

다음 글: 머신러닝 기초 (6) - Cross-Validation and Hyperparameter Tuning

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.