본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로AI › 머신러닝 › 기초

훈련세트와 테스트 세트

목차

훈련 세트와 테스트 세트

물고기의 길이와 무게로 어종을 맞히는 모델을 만든다고 해보자. 가지고 있는 물고기 100마리의 정답을 모두 보여주고 같은 100마리로 정확도를 계산하면, 모델이 처음 보는 물고기도 구별하는지는 알 수 없다. 학습에 사용하지 않은 데이터를 따로 남겨두어야 한다.

flowchart LR
    A[수집한 원본 데이터] --> B{목적에 맞게 분할}
    B --> C[훈련 세트<br/>모델 학습]
    B --> D[검증 세트<br/>모델·설정 선택]
    B --> E[테스트 세트<br/>최종 성능 확인]
    C --> F[학습된 모델]
    F --> D
    D --> G[설정 확정]
    G --> E

위의 세 묶음은 역할이 다르다. 검증과 테스트를 모두 평가 데이터라고 부르기도 하지만, 검증 세트로 모델을 여러 번 고르면 그 선택에도 검증 데이터의 정보가 반영된다. 최종 성능을 말할 때는 선택 과정에서 사용하지 않은 테스트 세트가 필요하다.

같은 정확도 90%라도 해석은 다르다. 훈련 세트에서 90%면 이미 본 문제를 얼마나 맞혔는지 보여 준다. 검증 세트에서 90%면 모델 후보를 고르는 근거가 된다. 한 번만 확인한 독립 테스트 세트에서 90%라면, 그 테스트 세트와 비슷한 새 데이터에 대해 어느 정도 기대할지 판단하는 단서가 된다. 실제 운영 대상이 다르면 그 수치도 옮겨지지 않을 수 있다.

세트사용하는 시점하는 일
훈련(train)학습 중모델의 매개변수를 조정한다.
검증(validation)학습 중간 또는 실험 비교 때모델 종류, 학습 횟수, 임계값 등을 고른다.
테스트(test)선택을 마친 뒤처음 보는 데이터에 대한 성능을 한 번 확인한다.

작은 데이터에서는 별도 검증 세트 대신 훈련 부분 안에서 교차 검증을 할 수도 있다. 이때도 최종 테스트 부분은 따로 남긴다.

분할 단위를 먼저 정하기

행마다 독립적인 샘플이라면 무작위 분할이 출발점이 된다. 그런데 물고기 한 마리를 여러 각도에서 찍은 사진 10장은 독립적인 10마리가 아니다. 같은 물고기의 사진을 훈련과 테스트에 나누면 모델이 어종의 일반적인 특징보다 배경·상처 같은 개체 특성을 알아볼 수 있다. 이때는 물고기 ID 단위로 나누어 같은 개체가 한 세트에만 들어가야 한다.

또한 이번 달에 측정한 물고기로 다음 달의 어종을 예측하려는 경우, 과거와 미래를 섞는 무작위 분할은 실제 사용 상황을 재현하지 못한다. 훈련은 과거, 테스트는 미래에서 뽑는다. 반대로 앞으로도 같은 수조에서 같은 방식으로 측정한 물고기를 예측한다면 무작위 분할이 더 가까운 평가일 수 있다. “정답인 분할 비율”을 외우기보다 언제, 어디서, 누구의 데이터를 예측할지를 먼저 정한다.

먼저 분할하고, 그다음 전처리한다

표준화할 때 전체 데이터의 평균을 먼저 계산하면 테스트 데이터의 값이 훈련 과정에 새어 들어간다. 텍스트 분류에서도 전체 문서로 어휘집이나 특징을 만든 뒤 분할하면 비슷한 문제가 생긴다. 순서는 분할 → 훈련 세트에서 전처리 규칙 학습 → 같은 규칙을 다른 세트에 적용이다.

숫자로 보면 차이가 분명하다. 훈련 길이가 10, 12이고 테스트 길이가 100이라면, 훈련 평균은 11이다. 테스트까지 섞어서 먼저 평균을 계산하면 (10+12+100)/3 ≈ 40.7이 된다. 테스트 샘플의 값 100이 훈련 샘플을 변환하는 규칙에 영향을 준 것이다. 최종 모델은 테스트를 보기 전에는 그 값을 알 수 없어야 한다. 평균·표준편차뿐 아니라 결측값 대체값, 특성 선택, 차원 축소도 같은 원칙을 적용한다.

flowchart TD
    A[원본 샘플] --> B[중복·그룹·시간 확인]
    B --> C[훈련 / 검증 / 테스트 분할]
    C --> D[훈련 세트로 전처리 규칙 학습]
    D --> E[훈련 세트 변환 및 모델 학습]
    D --> F[검증·테스트 세트에 같은 규칙 적용]
    E --> G[검증으로 설정 선택]
    F --> G
    G --> H[테스트로 최종 평가]

예를 들어 고객 A의 비슷한 문의 10건이 훈련 세트와 테스트 세트에 섞이면, 모델은 새로운 고객의 문의를 처리하는 능력보다 이미 본 표현을 기억하는 능력을 평가받는다. 같은 고객, 같은 문서에서 잘라낸 조각, 같은 환자의 기록처럼 서로 연결된 샘플은 그룹 단위로 분리한다. 미래 데이터를 예측한다면 무작위로 섞기보다 시간 순서에 맞춰 과거로 학습하고 미래로 평가한다.

데이터 누수는 전처리에서만 생기지 않는다. 결과가 나온 뒤에야 알 수 있는 특성(예: 배송 완료 여부로 배송 지연을 예측)을 넣거나, 같은 원본의 복제 행이 두 세트에 걸쳐 있어도 점수가 부풀려진다. 모델에 넣는 각 특성에 대해 실제 예측 시점에도 그 값을 알 수 있는가를 확인한다.

간단한 분할 예제

아래는 각 어종의 비율을 비슷하게 유지하며 훈련·검증·테스트를 나누는 예다. X는 길이와 무게, y는 어종 레이블이다. 비율 60:20:20은 설명용 선택이며, 정답이 적거나 시계열인 문제에서는 분할 방식이 달라져야 한다.

from sklearn.model_selection import train_test_split

X = [[8, 5], [9, 6], [10, 7], [11, 8], [12, 9],
     [25, 70], [26, 75], [27, 80], [28, 85], [29, 90]]
y = ["A"] * 5 + ["B"] * 5

X_train, X_holdout, y_train, y_holdout = train_test_split(
    X, y, test_size=0.4, stratify=y, random_state=42
)
X_valid, X_test, y_valid, y_test = train_test_split(
    X_holdout, y_holdout, test_size=0.5,
    stratify=y_holdout, random_state=42
)

실제 데이터가 위처럼 클래스당 5개뿐이면 검증과 테스트에 남는 샘플이 너무 적어 점수가 크게 흔들린다. 이 코드는 분할 순서를 보여주는 예제이며, 믿을 만한 성능 추정에는 더 많은 독립 샘플이 필요하다. 데이터가 작을 때는 학습 부분에서 교차 검증을 하고 테스트는 마지막에 확인하는 방법을 검토한다.

X_train과 y_train은 같은 인덱스의 길이·무게와 어종 정답이 짝을 이룬다. 첫 분할에서 40%를 임시 묶음(holdout)으로 남기고, 두 번째 분할에서 그 묶음을 반으로 나누므로 전체적으로 약 60:20:20이 된다. stratify=y는 두 어종의 비율이 크게 달라지지 않게 돕는다. random_state=42는 같은 입력으로 예제를 다시 실행할 때 같은 분할을 얻기 위한 값이지 성능을 높이는 설정이 아니다.

분할 뒤의 모델 학습과 결과 읽기

전처리와 모델을 Pipeline으로 묶으면 fit을 호출할 때 훈련 부분에서만 표준화 기준을 학습하고, score를 호출할 때는 그 기준을 재사용한다. 다음 코드는 위의 분할 결과를 이어서 사용하는 예시다.

from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import confusion_matrix

model = make_pipeline(StandardScaler(), KNeighborsClassifier(n_neighbors=3))
model.fit(X_train, y_train)

valid_predictions = model.predict(X_valid)
print('검증 정확도:', model.score(X_valid, y_valid))
print('검증 혼동 행렬:', confusion_matrix(
    y_valid, valid_predictions, labels=['A', 'B']
))

# 모델 구조와 설정을 확정한 뒤, 테스트는 마지막에 확인한다.
print('테스트 정확도:', model.score(X_test, y_test))

이 예제에서는 A와 B가 검증 세트에 한 개씩만 남는다. 그러면 한 마리의 결과가 정확도를 50%포인트씩 바꾼다. 예를 들어 둘 다 맞히면 100%, 한 마리만 맞히면 50%다. 이 수치는 코드 실행 결과가 아니라 분모가 2일 때의 계산이다. 작은 검증 세트의 우연을 근거로 k나 특성을 여러 번 바꾸면 쉽게 과적합한다.

혼동 행렬의 행은 실제 클래스, 열은 예측 클래스다. labels=['A', 'B']로 순서를 고정했으므로 A를 B로 잘못 분류한 수는 첫 행 둘째 열에 놓인다. 전체 정확도만 보고서는 어느 어종을 놓쳤는지 알 수 없기 때문에 이런 표가 필요하다.

실제 프로젝트에서는 검증으로 설정을 고른 뒤, 확정된 설정으로 훈련과 검증 데이터를 다시 학습하는 경우가 많다. 그 과정에서도 테스트 세트는 전처리 기준 계산이나 모델 선택에 사용하지 않는다. 교차 검증을 쓴다면 각 폴드 안에서 전처리를 다시 학습하도록 Pipeline 전체를 교차 검증에 넣는다. 사이킷런의 데이터 누수 설명에 이 순서가 정리되어 있다.

결과를 읽을 때 확인할 것

  • 훈련 점수만 높고 검증 점수가 낮다면 과적합을 의심한다.
  • 전체 정확도만 높아도 드문 클래스는 거의 맞히지 못할 수 있다. 클래스별 정밀도·재현율과 혼동 행렬을 함께 본다.
  • 검증 결과를 보고 데이터 정제 규칙이나 모델을 바꿨다면 검증 세트는 이미 선택에 쓰였다. 테스트 결과를 보고 다시 수정했다면 새로운 독립 테스트가 필요하다.
  • 운영 데이터의 수집 시기와 대상이 실험 데이터와 다르면 테스트 점수가 높아도 실제 성능은 달라질 수 있다.

관련 개념: 학습 유형

지도 학습(Supervised Learning)

  • 입력(데이터)과 타깃(정답)이 짝을 이룬 데이터가 필요하다.
  • 예를 들어 [길이, 무게] → 어종 또는 리뷰 문장 → 긍정/부정을 학습한다.
  • 정확도나 재현율을 계산하려면 평가 데이터에도 정답이 있어야 한다. 다만 그 정답은 학습에 사용하지 않는다.

비지도 학습(Unsupervised Learning)

  • 타깃 없이 입력 데이터의 구조를 찾는다. 비슷한 샘플을 묶는 클러스터링이 한 예다.
  • 학습에 정답이 없다는 뜻이지, 결과를 평가할 방법이 전혀 없다는 뜻은 아니다. 목적에 맞는 별도 평가 기준이 필요하다.

강화학습(Reinforcement Learning)

  • 환경에서 행동하고 그 결과로 받은 보상을 사용해 학습한다.
  • 지도 학습의 정답 레이블과 달리, 보상은 여러 행동의 결과를 뒤늦게 반영할 수 있다.

정리하면, 분할의 핵심은 정해진 비율이 아니라 실제로 처음 만날 데이터를 재현하는 것이다. 그 기준을 먼저 정해야 어떤 샘플을 함께 묶고 어느 시점에서 나눌지 결정할 수 있다.

참고: scikit-learn의 데이터 누수 안내, 교차 검증 설명, train_test_split API

같은 카테고리의 글