Skip to main content
Summary

이 장에서 배울 것

앞에서는 데이터셋을 읽고, 무엇을 예측할지 정의하는 방법을 배웠다. 이제 다음 질문으로 넘어가야 한다.

"이 모델이 처음 보는 데이터에서도 잘 맞을까?"

머신러닝 모델은 훈련에 사용한 데이터에서는 좋은 점수를 받을 수 있다. 하지만 우리가 진짜 알고 싶은 것은 이미 본 데이터를 얼마나 잘 외웠는지가 아니라, 새로운 데이터에 얼마나 잘 일반화되는지다. 그래서 데이터를 훈련용과 테스트용으로 나누어 평가한다.

이 장을 끝내면 다음을 할 수 있어야 한다.

  • 훈련 데이터와 테스트 데이터를 나누는 이유를 설명한다.

  • 일반화, 과적합, 과소적합의 의미를 구분한다.

  • train_test_split으로 재현 가능한 데이터 분할을 수행한다.

  • 분류 문제에서 stratify가 왜 중요한지 이해한다.

  • 검증 데이터와 테스트 데이터의 역할 차이를 설명한다.

  • 데이터 분할 과정에서 발생할 수 있는 데이터 누수를 피한다.

훈련 데이터와 테스트 데이터

왜 이 주제가 중요한가

초보자가 가장 자주 하는 실수 중 하나는 같은 데이터로 모델을 학습하고, 같은 데이터로 성능을 확인하는 것이다. 이렇게 하면 점수는 높게 나올 수 있다. 하지만 그 점수는 모델이 실전에서 잘한다는 뜻이 아닐 수 있다.

예를 들어 시험 문제와 정답을 모두 외운 학생이 있다고 하자. 그 학생은 이미 본 문제에서는 거의 만점을 받을 수 있다. 하지만 숫자나 문장이 조금 바뀐 새 문제가 나오면 크게 흔들릴 수 있다. 이 학생이 진짜 개념을 이해했는지 보려면 처음 보는 문제를 풀게 해야 한다.

머신러닝 모델도 마찬가지다. 훈련 데이터는 공부용 문제집이고, 테스트 데이터는 처음 보는 시험 문제에 가깝다. 모델이 훈련 데이터만 잘 맞히고 테스트 데이터에서 못한다면, 우리는 그 모델을 믿고 사용할 수 없다.

따라서 데이터 분할은 단순한 코드 한 줄이 아니다. 모델 평가의 출발점이다. 좋은 평가 없이는 좋은 모델 선택도 어렵다.

핵심 개념 설명

훈련 데이터와 테스트 데이터

훈련 데이터(train data)는 모델이 패턴을 배우는 데 사용하는 데이터다. 모델은 훈련 데이터의 특징 X_train과 정답 y_train을 보고 내부 기준을 조정한다.

테스트 데이터(test data)는 학습이 끝난 뒤 성능을 확인하는 데 사용하는 데이터다. 테스트 데이터는 모델 학습 과정에서 보지 않아야 한다. 그래야 모델이 새로운 데이터에 얼마나 잘 작동하는지 비교적 정직하게 확인할 수 있다.

흐름은 다음과 같다.

전체 데이터
  ├─ 훈련 데이터: 모델 학습에 사용
  └─ 테스트 데이터: 마지막 성능 확인에 사용

중요한 원칙은 단순하다.

테스트 데이터는 시험지다. 공부할 때 미리 보면 안 된다.

테스트 데이터를 보고 계속 모델을 고치면, 모델은 테스트 데이터에 맞춰진다. 그러면 진짜 새 데이터에서의 성능을 알기 어려워진다.

일반화

일반화(generalization)는 모델이 처음 보는 데이터에도 잘 작동하는 능력이다.

머신러닝의 목표는 훈련 데이터를 완벽히 맞히는 것이 아니다. 우리가 원하는 것은 앞으로 들어올 새 고객, 새 이메일, 새 거래, 새 측정값에 대해 쓸 만한 예측을 하는 것이다.

그래서 모델을 평가할 때는 항상 이런 질문을 해야 한다.

  • 훈련 데이터에서만 좋은가?

  • 테스트 데이터에서도 비슷하게 좋은가?

  • 데이터가 조금 달라져도 안정적인가?

훈련 점수와 테스트 점수의 차이는 모델의 일반화 상태를 보는 중요한 단서다.

과적합과 과소적합

과적합(overfitting)은 모델이 훈련 데이터를 지나치게 자세히 외운 상태다. 훈련 데이터에서는 점수가 매우 높지만, 테스트 데이터에서는 성능이 떨어진다.

예를 들어 결정 트리가 너무 깊어지면 훈련 데이터의 작은 예외까지 모두 규칙으로 만들 수 있다. "나이가 31세이고, 화요일에 가입했고, 구매 횟수가 7회인 고객"처럼 지나치게 세부적인 규칙은 새 데이터에 잘 맞지 않을 수 있다.

반대로 과소적합(underfitting)은 모델이 데이터의 중요한 패턴을 충분히 배우지 못한 상태다. 이 경우 훈련 데이터에서도 점수가 낮고, 테스트 데이터에서도 점수가 낮다.

간단히 정리하면 다음과 같다.

상태훈련 성능테스트 성능해석
과소적합낮음낮음모델이 너무 단순하거나 특징이 부족함
적절한 학습높음높음훈련 데이터의 패턴이 새 데이터에도 잘 이어짐
과적합매우 높음낮음훈련 데이터를 지나치게 외움

좋은 모델은 훈련 데이터와 테스트 데이터 모두에서 안정적인 성능을 보인다.

검증 데이터는 언제 필요한가

프로젝트가 조금만 복잡해져도 모델을 한 번만 만들고 끝내지 않는다. 여러 모델을 비교하고, 하이퍼파라미터를 바꾸고, 전처리 방법도 실험한다.

이때 테스트 데이터를 계속 들여다보면 문제가 생긴다. 테스트 성능을 보고 모델을 고치는 순간, 테스트 데이터가 모델 선택 과정에 영향을 주기 때문이다.

그래서 필요한 것이 검증 데이터(validation data)다.

전체 데이터
  ├─ 훈련 데이터: 모델 학습
  ├─ 검증 데이터: 모델 선택과 설정 조정
  └─ 테스트 데이터: 마지막 성능 확인

데이터가 충분하다면 훈련/검증/테스트로 나눌 수 있다. 데이터가 많지 않다면 뒤에서 배울 교차검증을 사용해 훈련 데이터를 여러 방식으로 나누어 평가할 수 있다.

핵심은 이것이다.

  • 검증 데이터: 실험 중에 참고해도 되는 평가용 데이터

  • 테스트 데이터: 최종 확인 전까지 아껴 두는 데이터

분류 문제에서는 클래스 비율을 확인한다

분류 문제에서는 데이터를 나눌 때 클래스 비율이 크게 달라지지 않도록 조심해야 한다.

예를 들어 전체 데이터에서 사기 거래가 5%뿐이라고 하자. 아무 생각 없이 데이터를 나누면 운 나쁘게 테스트 데이터에 사기 거래가 거의 들어가지 않을 수 있다. 그러면 모델이 사기 거래를 잘 잡는지 평가하기 어렵다.

이때 stratify 옵션을 사용하면 훈련 데이터와 테스트 데이터의 클래스 비율을 전체 데이터와 비슷하게 유지할 수 있다.

train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)

stratify=y는 "정답 y의 비율을 고려해서 나누라"는 뜻이다. 특히 클래스 불균형이 있는 분류 문제에서 중요하다.

시간 순서가 있는 데이터는 무작위로 섞으면 안 될 수 있다

모든 데이터가 무작위 분할에 적합한 것은 아니다. 시간 순서가 중요한 데이터는 조심해야 한다.

예를 들어 다음 달 매출을 예측하는 모델을 만든다고 하자. 2025년 데이터를 훈련에 쓰고, 2024년 데이터를 테스트에 쓰면 이상하다. 실제 상황에서는 과거 데이터로 미래를 예측해야 한다. 미래 데이터를 학습에 섞어 버리면 현실보다 쉬운 평가가 된다.

시계열 데이터나 로그 데이터처럼 시간 흐름이 중요한 경우에는 보통 과거를 훈련 데이터로, 이후 기간을 테스트 데이터로 둔다.

과거 데이터 ─────────────── 최근 데이터
[훈련 데이터] [검증 데이터] [테스트 데이터]

처음 배우는 단계에서는 train_test_split으로 무작위 분할을 많이 사용하지만, 실무에서는 데이터가 만들어진 시간과 예측 시점을 반드시 확인해야 한다.

쉬운 비유 또는 사례

데이터 분할은 요리 연습과 비슷하다.

요리사가 레시피를 연습할 때 매번 같은 재료, 같은 접시, 같은 심사위원 앞에서만 평가받는다면 실력이 좋아 보일 수 있다. 하지만 다른 재료가 들어오거나, 손님 취향이 조금 달라지면 결과가 흔들릴 수 있다.

그래서 일부 재료는 연습에 쓰고, 일부 재료는 마지막 평가용으로 남겨 두어야 한다. 연습 중에는 검증용으로 맛을 보며 조정할 수 있지만, 최종 심사용 접시는 미리 건드리지 않는 편이 공정하다.

머신러닝에서도 훈련 데이터는 연습용 재료이고, 검증 데이터는 중간 점검용이며, 테스트 데이터는 마지막 심사에 가깝다.

직접 해보는 실습

이번 실습에서는 scikit-learn의 유방암 예제 데이터를 사용해 훈련 데이터와 테스트 데이터를 나누고, 간단한 모델을 학습한 뒤 성능을 확인한다. 이 데이터는 학습용 예제이며 실제 의료 판단에 사용하기 위한 것이 아니다.

1단계: 데이터 불러오기

from sklearn.datasets import load_breast_cancer
import pandas as pd

raw = load_breast_cancer()
X = pd.DataFrame(raw.data, columns=raw.feature_names)
y = pd.Series(raw.target, name="target")

print(X.shape)
print(y.value_counts())
print(raw.target_names)

scikit-learn의 이 데이터에서는 원래 라벨이 0=malignant, 1=benign이다. 즉 0은 악성, 1은 양성 종양을 뜻한다. 숫자의 크기보다 각 숫자가 어떤 범주를 뜻하는지가 중요하다.

2단계: 훈련 데이터와 테스트 데이터 나누기

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

print("훈련 데이터:", X_train.shape, y_train.shape)
print("테스트 데이터:", X_test.shape, y_test.shape)
print("훈련 클래스 비율")
print(y_train.value_counts(normalize=True))
print("테스트 클래스 비율")
print(y_test.value_counts(normalize=True))

여기서 중요한 옵션은 세 가지다.

  • test_size=0.2: 전체 데이터의 20%를 테스트 데이터로 사용한다.

  • random_state=42: 같은 결과가 다시 나오도록 난수 기준을 고정한다.

  • stratify=y: 훈련 데이터와 테스트 데이터의 클래스 비율을 비슷하게 유지한다.

random_state는 모델 성능을 높이는 마법 숫자가 아니다. 실습 결과를 재현하기 위한 장치다.

3단계: 모델 학습과 평가

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report

model = make_pipeline(
    StandardScaler(),
    LogisticRegression(max_iter=1000)
)

model.fit(X_train, y_train)
pred = model.predict(X_test)

print("정확도:", accuracy_score(y_test, pred))
print("혼동 행렬:n", confusion_matrix(y_test, pred))
print(classification_report(y_test, pred, target_names=raw.target_names))

make_pipeline은 전처리와 모델을 하나로 묶는다. 위 코드에서는 훈련 데이터로만 StandardScaler의 기준을 학습하고, 그 기준을 테스트 데이터에 적용한다. 이렇게 해야 테스트 데이터의 정보가 훈련 과정에 섞이는 일을 줄일 수 있다.

4단계: 같은 데이터로 평가했을 때와 비교하기

이번에는 일부러 좋지 않은 평가 방법을 확인해 보자. 모델을 훈련 데이터에 맞춘 뒤 훈련 데이터에서 점수를 재면 어떤 일이 생길까?

train_pred = model.predict(X_train)
print("훈련 정확도:", accuracy_score(y_train, train_pred))
print("테스트 정확도:", accuracy_score(y_test, pred))

훈련 정확도가 테스트 정확도보다 높게 나올 수 있다. 차이가 조금 나는 것은 자연스럽다. 하지만 훈련 점수는 매우 높은데 테스트 점수가 크게 낮다면 과적합을 의심해야 한다.

5단계: 검증 데이터까지 나누어 보기

데이터를 훈련/검증/테스트로 나누는 연습도 해 보자.

X_temp, X_test, y_temp, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

X_train, X_valid, y_train, y_valid = train_test_split(
    X_temp, y_temp,
    test_size=0.25,
    random_state=42,
    stratify=y_temp
)

print("훈련:", X_train.shape)
print("검증:", X_valid.shape)
print("테스트:", X_test.shape)

첫 번째 분할에서 20%를 테스트로 떼어 두었다. 남은 80% 중 25%를 검증 데이터로 만들면 전체 기준으로는 60% 훈련, 20% 검증, 20% 테스트가 된다.

검증 데이터는 모델을 고르는 과정에서 사용하고, 테스트 데이터는 마지막에 한 번 확인하는 용도로 남겨 둔다.

흔한 실수와 오류 분석

1. 훈련 데이터로 성능을 판단한다

훈련 데이터 성능은 모델이 공부한 문제집 점수다. 참고는 할 수 있지만 최종 실력으로 보면 안 된다. 테스트 데이터에서의 성능을 함께 확인해야 한다.

2. 테스트 데이터를 계속 보며 모델을 고친다

테스트 점수를 보고 모델을 여러 번 바꾸면 테스트 데이터가 사실상 검증 데이터가 된다. 그러면 최종 성능을 정직하게 추정하기 어렵다. 모델 선택에는 검증 데이터나 교차검증을 사용하고, 테스트 데이터는 마지막 확인용으로 남겨 두자.

3. 클래스 비율을 확인하지 않는다

분류 문제에서 훈련 데이터와 테스트 데이터의 클래스 비율이 크게 달라지면 평가가 흔들린다. 특히 양성 클래스가 적은 문제에서는 stratify=y를 사용하는 습관이 좋다.

4. 전처리를 분할 전에 전체 데이터에 적용한다

스케일 조정, 결측치 대체, 특징 선택을 전체 데이터에 먼저 적용한 뒤 나누면 테스트 데이터의 정보가 훈련 과정에 들어갈 수 있다. 이를 데이터 누수라고 한다.

안전한 흐름은 다음과 같다.

데이터 분할 → 훈련 데이터로 전처리 기준 학습 → 테스트 데이터에는 같은 기준만 적용

Pipeline을 사용하면 이 흐름을 지키기 쉬워진다.

5. 시간 순서를 무시한다

미래를 예측하는 문제에서 미래 데이터를 훈련에 섞으면 안 된다. 고객 이탈, 매출, 재고, 센서 로그처럼 시간 흐름이 있는 문제는 무작위 분할보다 시간 기준 분할이 더 적절할 수 있다.

요약

훈련 데이터는 모델이 학습하는 데 사용하고, 테스트 데이터는 학습이 끝난 뒤 성능을 확인하는 데 사용한다. 머신러닝에서 중요한 것은 훈련 데이터를 외우는 능력이 아니라 처음 보는 데이터에 잘 작동하는 일반화 능력이다.

과적합은 훈련 데이터에 지나치게 맞춰져 테스트 데이터에서 성능이 낮아지는 상태다. 과소적합은 훈련 데이터의 패턴도 충분히 배우지 못한 상태다. 훈련 성능과 테스트 성능을 함께 보면 두 문제를 의심할 수 있다.

모델 선택과 설정 조정에는 검증 데이터나 교차검증을 사용하고, 테스트 데이터는 마지막 확인용으로 남겨 두어야 한다. 분류 문제에서는 stratify로 클래스 비율을 유지하고, 전처리는 데이터 분할 이후 훈련 데이터 기준으로 적용해야 데이터 누수를 줄일 수 있다.

퀴즈

  1. 훈련 데이터와 테스트 데이터를 나누는 이유는 무엇인가?

  2. 일반화란 무엇인가?

  3. 과적합과 과소적합의 차이를 설명해 보자.

  4. 테스트 데이터를 계속 보면서 모델을 고치면 왜 문제가 되는가?

  5. 분류 문제에서 stratify=y를 사용하는 이유는 무엇인가?

  6. random_state는 어떤 역할을 하는가?

  7. 전처리를 전체 데이터에 먼저 적용하면 어떤 데이터 누수가 생길 수 있는가?

  8. 시간 순서가 있는 데이터에서 무작위 분할이 위험할 수 있는 이유는 무엇인가?

체크리스트

  • 훈련 데이터와 테스트 데이터의 역할을 구분할 수 있다.

  • 일반화, 과적합, 과소적합을 설명할 수 있다.

  • train_test_split으로 데이터를 재현 가능하게 나눌 수 있다.

  • 분류 문제에서 클래스 비율을 확인할 수 있다.

  • stratify와 random_state의 의미를 이해했다.

  • 검증 데이터와 테스트 데이터의 차이를 말할 수 있다.

  • 전처리를 분할 이후에 적용해야 하는 이유를 설명할 수 있다.

  • 시간 순서가 있는 데이터에서는 분할 방식이 달라질 수 있음을 안다.

다음 장 연결

이제 우리는 데이터를 문제에 맞게 정의하고, 훈련 데이터와 테스트 데이터로 나누어 평가할 준비를 할 수 있다. 다음에는 실제 모델을 만들어 본다.

다음 장에서는 가장 기본적인 지도학습 모델인 선형 회귀와 로지스틱 회귀를 배운다. 선형 회귀는 숫자를 예측하고, 로지스틱 회귀는 주로 분류 문제에 사용된다. 이번 장에서 배운 데이터 분할 원칙은 다음 모델 실습에서도 계속 사용된다.

책 목록

AI 교육 2권 : 머신러닝 기초: 데이터로 배우고 검증하는 첫 모델

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기