포스트

머신러닝 기초 (4) - 과대적합과 일반화: train/valid/test와 편향-분산

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

머신러닝 기초 (4) - 과대적합과 일반화: train/valid/test와 편향-분산

머신러닝 기초 시리즈의 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²
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²가 음수라는 것은 그만큼 예측이 망가졌다는 신호입니다. 지표 자체는 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이 벌어지며 떨어지는 순간

다음 글: 머신러닝 기초 (5) - 교차검증과 하이퍼파라미터 튜닝

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