
2편에서는 사이킷런을 이용하여 데이터 분할, 모델 학습, 예측, 평가까지의 기본적인 머신러닝 Workflow를 실습했습니다.
하지만 단 한 번의 학습/테스트 분할만으로 모델의 성능을 평가하면 데이터 분포가 치우쳐 있을 경우 올바른 평가가 어려울 수 있습니다.
이 문제를 해결하는 대표적인 기법이 **교차검증(Cross Validation)**입니다.
이번 3편에서는 다음 내용을 다룹니다.
- 교차검증의 개념
- K-Fold 검증 방식
- 레이블 분포 불균형을 고려한 Stratified K-Fold 검증
- 사이킷런 코드로 직접 실습
- 4편에서 사용할 cross_val_score로 이어지는 흐름 정리
1. 교차검증(Cross Validation)이란?
교차검증은 기존 학습 데이터를 다시 여러 조각으로 나누어 반복적으로 학습과 평가를 수행하는 방법입니다.
즉, 학습 데이터를 단 한 번만 학습/테스트로 나누는 것이 아니라, 여러 번 “모의고사”를 보듯 반복 평가함으로써 모델 성능을 더 객관적으로 측정할 수 있습니다.
교차검증이 필요한 이유
- 하나의 테스트 데이터 분할 결과에 성능이 좌우되는 것을 방지
- 데이터 편향을 최소화하여 신뢰할 수 있는 평균 성능 확보
- 과적합(overfitting) 발생 여부를 더 정확하게 판단 가능
2. K-Fold 교차검증
K-Fold는 가장 기본적인 교차검증 방식입니다.
K-Fold 방식
예: K=5일 경우
- 데이터를 5개의 ‘폴드(fold)’로 나눕니다.
- 5번 반복 실행
- 매번 1개의 폴드를 검증용(Test)으로 사용
- 나머지 4개 폴드는 학습용(Train)으로 사용
- 5번의 평가 점수 평균을 최종 점수로 활용
이 방식은 데이터가 고르게 분포되어 있을 때 효과적입니다.
K-Fold 실습 코드
# 교차검증: K-Fold 적용 실습
from sklearn.tree import DecisionTreeClassifier # 결정트리(Decision Tree) 분류 모델 클래스 임포트
from sklearn.metrics import accuracy_score # 정확도(accuracy) 평가 함수 임포트
from sklearn.model_selection import KFold # K-Fold 교차 검증을 위한 클래스 임포트
from sklearn.datasets import load_iris # 예제용 Iris(붓꽃) 데이터셋 로더 임포트
import numpy as np # 수치 계산을 위한 NumPy 임포트
# -----------------------------
# 1. 데이터 로딩 및 분리
# -----------------------------
# load_iris() : 사이킷런에서 제공하는 대표적인 다중 분류 예제 데이터셋
iris = load_iris()
# 특징(feature) 데이터
# - 꽃받침 길이/너비, 꽃잎 길이/너비 4개의 연속형 숫자 변수로 구성
features = iris.data
# 레이블(label) 데이터
# - 품종 정보 (0: setosa, 1: versicolor, 2: virginica)
label = iris.target
# -----------------------------
# 2. 모델 생성
# -----------------------------
# DecisionTreeClassifier : 결정트리를 이용한 분류 모델
# random_state=11 : 트리 생성 시 사용되는 난수 시드를 고정하여, 실행할 때마다 같은 결과가 나오도록 설정
dt_clf = DecisionTreeClassifier(random_state=11)
# -----------------------------
# 3. K-Fold 설정
# -----------------------------
# KFold(n_splits=5)
# - 전체 데이터를 5개의 폴드(fold)로 나눔
# - 총 5번의 반복:
# 매 반복마다 4개의 폴드는 학습용, 나머지 1개의 폴드는 검증용으로 사용
kfold = KFold(n_splits=5)
# 각 폴드별 정확도(accuracy)를 저장할 리스트
cv_accuracy = []
# 교차 검증 반복 횟수를 기록할 변수 (현재 몇 번째 폴드인지 확인용)
n_iter = 0
# -----------------------------
# 4. K-Fold 교차 검증 수행
# -----------------------------
# kfold.split(features)
# - 전체 feature 배열에 대해,
# 각 반복마다 학습용 인덱스(train_index)와 검증용 인덱스(test_index)를 생성해서 반환
for train_index, test_index in kfold.split(features):
# train_index, test_index 는 1차원 배열 형태의 정수 인덱스 리스트입니다.
# 인덱스를 이용해 학습용/검증용 데이터 분리
# X_train : 학습에 사용할 입력 특징 데이터
# X_test : 검증에 사용할 입력 특징 데이터
X_train, X_test = features[train_index], features[test_index]
# y_train : 학습에 사용할 레이블(정답) 데이터
# y_test : 검증에 사용할 레이블(정답) 데이터
y_train, y_test = label[train_index], label[test_index]
# -----------------------------
# (1) 모델 학습
# -----------------------------
# fit(입력데이터, 레이블) : 주어진 학습 데이터를 이용해 결정트리 모델을 학습시킵니다.
dt_clf.fit(X_train, y_train)
# -----------------------------
# (2) 검증 데이터 예측
# -----------------------------
# predict(입력데이터) : 학습된 모델로부터 예측 결과(클래스 레이블)를 반환
pred = dt_clf.predict(X_test)
# 현재가 교차 검증의 몇 번째 반복인지 카운트
n_iter += 1
# -----------------------------
# (3) 정확도 계산
# -----------------------------
# accuracy_score(실제값, 예측값)
# - 전체 검증 샘플 중에서, 예측이 정답과 일치한 비율을 계산
# np.round(..., 4) : 소수점 넷째 자리까지 반올림하여 보기 좋게 출력
accuracy = np.round(accuracy_score(y_test, pred), 4)
# 학습에 사용된 데이터 개수
# X_train.shape[0] : 행(row)의 개수 = 샘플 개수
train_size = X_train.shape[0]
# 검증에 사용된 데이터 개수
test_size = X_test.shape[0]
# 반복 번호, 정확도, 학습 데이터 크기, 검증 데이터 크기 출력
# 예) "1 0.9667 120 30" 형태로 출력
print(n_iter, accuracy, train_size, test_size)
# 현재 반복에서 검증에 사용된 인덱스(행 번호)들을 출력
# - 어떤 샘플들이 검증용으로 사용되었는지 확인할 수 있음
print("검증 인덱스:", test_index)
# 각 폴드에서 계산된 정확도를 리스트에 저장
cv_accuracy.append(accuracy)
# -----------------------------
# 5. 전체 폴드에 대한 평균 정확도 계산
# -----------------------------
# np.mean(cv_accuracy)
# - 5번의 교차 검증에서 얻은 정확도들의 평균값
# - 모델이 데이터 전반에 대해 어느 정도의 일반화 성능을 가지는지 판단할 수 있는 지표
print(f"평균 검증 정확도: {np.mean(cv_accuracy)}")
K-Fold 결과 해석
- 각 Fold마다 정확도가 출력됩니다.
- 최종적으로 5개의 정확도를 평균내어 모델의 성능을 평가할 수 있습니다.
- 단점: 레이블 분포가 불균형할 경우 문제가 발생할 수 있음 → Stratified K-Fold 필요
3. Stratified K-Fold 교차검증
일반 K-Fold는 단순히 데이터를 폴드 수만큼 균등하게 나누기 때문에
레이블 분포가 불균형한 경우 학습/검증 데이터의 비율이 서로 다르게 구성되는 문제가 있습니다.
Stratified K-Fold의 특징
- 학습 데이터와 검증 데이터에서 레이블 비율을 동일하게 유지
- 분류(Classification) 문제에서는 필수적인 교차검증 방식
- 과적합을 방지하는 데 매우 효과적
(1) 레이블 분포 확인 및 일반 K-Fold 문제점
from sklearn.model_selection import KFold # (추후 교차검증에 사용할 예정인) KFold 클래스 임포트
import pandas as pd # 판다스 라이브러리 임포트 (데이터프레임 생성/처리용)
from sklearn.datasets import load_iris # Iris 예제 데이터셋 로딩을 위한 함수 임포트
# ---------------------------------------------------------
# 1. Iris 데이터셋 로딩
# ---------------------------------------------------------
# load_iris()
# - 사이킷런에서 제공하는 대표적인 붓꽃(Iris) 데이터셋을 메모리로 불러옵니다.
# - 반환되는 객체는 Bunch 타입으로, 딕셔너리와 비슷한 구조를 가집니다.
iris = load_iris()
# ---------------------------------------------------------
# 2. 특징(feature) 데이터프레임 생성
# ---------------------------------------------------------
# iris.data
# - 넘파이 배열 형태의 2차원 데이터 (행: 샘플, 열: 특징)
# - 각 열은 꽃받침 길이, 꽃받침 너비, 꽃잎 길이, 꽃잎 너비를 의미합니다.
#
# iris.feature_names
# - 각 열(특징)에 대한 이름이 문자열 리스트로 들어 있습니다.
# 예) ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
#
# pd.DataFrame(data=..., columns=...)
# - 넘파이 배열을 판다스 DataFrame 형태로 변환하면서
# 열 이름(columns)을 feature_names로 지정합니다.
iris_df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
# ---------------------------------------------------------
# 3. 레이블(label) 컬럼 추가
# ---------------------------------------------------------
# iris.target
# - 각 샘플(행)에 대한 품종 정보가 정수 형태로 들어 있습니다.
# 0: setosa, 1: versicolor, 2: virginica
#
# 새로운 컬럼 "label"을 추가하여, 각 행의 정답(클래스)을 저장합니다.
iris_df["label"] = iris.target
# ---------------------------------------------------------
# 4. 데이터 확인: 상위 3개 행 출력
# ---------------------------------------------------------
# head(3)
# - 데이터프레임의 첫 3개 행만 미리보기 형태로 출력합니다.
# - 각 특징 값과 label 값이 어떻게 구성되어 있는지 구조를 빠르게 파악할 수 있습니다.
print(iris_df.head(3))
# ---------------------------------------------------------
# 5. 레이블 분포 확인
# ---------------------------------------------------------
# iris_df["label"]
# - DataFrame에서 "label" 컬럼(시리즈)을 선택합니다.
#
# value_counts()
# - 해당 시리즈 안에 각 값(클래스)이 몇 개씩 있는지 개수를 집계하여 반환합니다.
# - Iris 데이터셋은 각 품종(setosa, versicolor, virginica)마다 50개 샘플로 구성되어 있기 때문에
# 0, 1, 2 각각 50개씩 출력되는지 확인할 수 있습니다.
print(iris_df["label"].value_counts()) # 각 클래스 50개씩
일반 K-Fold로 분리 시 문제점 확인
from sklearn.model_selection import KFold # K-Fold 교차 검증을 위한 클래스 임포트
# ---------------------------------------------------------
# 1. KFold 객체 생성
# ---------------------------------------------------------
# KFold(n_splits=3)
# - 전체 데이터를 3개의 폴드(fold)로 나누어 교차 검증을 수행하기 위한 설정입니다.
# - 각 반복(iteration)마다 서로 다른 1개 폴드는 검증용, 나머지 2개 폴드는 학습용으로 사용됩니다.
# - 여기서는 단순 KFold 이므로, 레이블 비율을 고려하지 않고 순서대로 데이터를 분할합니다.
kfold = KFold(n_splits=3)
# 교차 검증이 몇 번째 반복인지 기록하는 변수입니다.
n_iter = 0
# ---------------------------------------------------------
# 2. KFold를 이용한 학습/검증 인덱스 생성 및 레이블 분포 확인
# ---------------------------------------------------------
# kfold.split(iris_df)
# - iris_df의 행 인덱스를 기준으로 학습용/검증용 인덱스를 생성합니다.
# - 반환 값:
# train_index : 이번 반복에서 학습에 사용할 행 인덱스 배열
# test_index : 이번 반복에서 검증에 사용할 행 인덱스 배열
for train_index, test_index in kfold.split(iris_df):
# 반복 횟수(현재 몇 번째 폴드인지) 1 증가
n_iter += 1
# -----------------------------------------------------
# 학습용 레이블(label) 추출
# -----------------------------------------------------
# iris_df["label"] : 전체 데이터프레임에서 레이블 컬럼(정답 클래스)만 선택합니다.
# iloc[train_index] : train_index에 해당하는 행의 레이블만 선택합니다.
# → 이번 반복에서 학습에 사용되는 샘플들의 레이블 시리즈가 됩니다.
label_train = iris_df["label"].iloc[train_index]
# -----------------------------------------------------
# 검증용 레이블(label) 추출
# -----------------------------------------------------
# iloc[test_index] : test_index에 해당하는 행의 레이블만 선택합니다.
# → 이번 반복에서 검증에 사용되는 샘플들의 레이블 시리즈가 됩니다.
label_test = iris_df["label"].iloc[test_index]
# 현재가 몇 번째 폴드인지 출력
print("n_iter:", n_iter)
# -----------------------------------------------------
# 학습 데이터의 레이블 분포 출력
# -----------------------------------------------------
# value_counts()
# - 시리즈 안에서 각 클래스(0, 1, 2)가 몇 개씩 존재하는지 개수를 세어줍니다.
# - KFold는 단순 분할이기 때문에, 각 폴드마다 레이블 비율이 균등하지 않을 수 있습니다.
print("학습 데이터 레이블 분포:\n", label_train.value_counts())
# -----------------------------------------------------
# 검증 데이터의 레이블 분포 출력
# -----------------------------------------------------
# 마찬가지로, 검증 데이터에 포함된 각 클래스의 개수를 확인합니다.
# - StratifiedKFold와 비교할 때, 레이블 불균형이 생길 수 있는지 확인하는 용도로 많이 사용합니다.
print("검증 데이터 레이블 분포:\n", label_test.value_counts())
위 출력 결과를 보면
학습/검증 데이터의 레이블 분포가 동일하지 않은 폴드가 있다는 것을 확인할 수 있습니다.
→ 분류 문제에서는 매우 좋지 않은 구조

(2) Stratified K-Fold 적용
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import accuracy_score
import numpy as np
from sklearn.tree import DecisionTreeClassifier # 의사결정나무 분류기 클래스 임포트
# StratifiedKFold 객체 생성
# n_splits=3 : 전체 데이터를 3개의 폴드(fold)로 나눠서 3번 교차 검증을 수행하겠다는 의미입니다.
# StratifiedKFold는 각 폴드에서 타깃(label)의 비율이 원래 데이터셋과 최대한 비슷하도록 나눠줍니다.
skf = StratifiedKFold(n_splits=3)
# 각 폴드별 정확도(accuracy)를 저장할 리스트
cv_accuracy = []
# 교차 검증이 몇 번째 반복인지를 기록할 변수
n_iter = 0
# 결정트리(Decision Tree) 분류기 객체 생성
# random_state=11 : 결과 재현성을 위해 난수 시드를 고정합니다.
dt_clf = DecisionTreeClassifier(random_state=11)
# skf.split(특징데이터, 레이블데이터)
# - iris_df: 전체 데이터프레임이라고 가정
# - iris_df["label"]: 타깃(레이블) 컬럼
# 이 메서드는 각 폴드마다 학습용 인덱스(train_index)와 검증용 인덱스(test_index)를 반환합니다.
for train_index, test_index in skf.split(iris_df, iris_df["label"]):
# 반복 횟수 증가 (현재 몇 번째 폴드인지 기록)
n_iter += 1
# iloc을 사용하여 인덱스로 행을 선택
# X_train: 학습용 특징 데이터 (마지막 열은 label이라고 가정하고 제외)
# X_test : 검증용 특징 데이터
X_train, X_test = iris_df.iloc[train_index, :-1], iris_df.iloc[test_index, :-1]
# y_train: 학습용 레이블 (라벨 컬럼만 선택)
# y_test : 검증용 레이블
y_train, y_test = iris_df["label"].iloc[train_index], iris_df["label"].iloc[test_index]
# -----------------------------
# 1. 모델 학습 단계
# -----------------------------
# 학습 데이터(X_train, y_train)를 사용하여 결정트리 분류기(dt_clf)를 학습시킵니다.
dt_clf.fit(X_train, y_train)
# -----------------------------
# 2. 예측 단계
# -----------------------------
# 학습된 모델을 사용하여 검증 데이터(X_test)에 대한 예측 결과를 얻습니다.
pred = dt_clf.predict(X_test)
# -----------------------------
# 3. 평가 단계 (정확도 계산)
# -----------------------------
# accuracy_score(실제값, 예측값)을 사용하여 정확도를 계산합니다.
# np.round(..., 4) : 소수점 넷째 자리까지 반올림하여 표현합니다.
accuracy = np.round(accuracy_score(y_test, pred), 4)
# 각 폴드에서의 정확도를 리스트에 추가하여 나중에 평균을 계산할 수 있도록 합니다.
cv_accuracy.append(accuracy)
# 현재 몇 번째 폴드인지 출력
print(f"n_iter: {n_iter}")
# 학습 데이터에 사용된 레이블 분포 출력
# value_counts() : 각 클래스(레이블)가 몇 개씩 포함되어 있는지 개수를 보여줍니다.
print("학습 레이블 분포:\n", y_train.value_counts())
# 검증 데이터에 사용된 레이블 분포 출력
# StratifiedKFold를 사용했기 때문에, 학습/검증 데이터 모두에서
# 레이블 분포가 원본 데이터와 최대한 비슷하게 유지됩니다.
print("검증 레이블 분포:\n", y_test.value_counts())
# 모든 폴드에서의 정확도 리스트 출력
print(f"교차 정확도: {cv_accuracy}")
# np.mean(cv_accuracy) : 폴드별 정확도들의 평균값을 계산
# 교차 검증을 통해 얻은 모델의 평균 일반화 성능을 나타냅니다.
print(f"평균 검증 정확도: {np.mean(cv_accuracy)}")
Stratified K-Fold 결과
- 모든 폴드에서 레이블 분포가 동일
- 데이터 편향이 제거되어 평가의 신뢰도가 높아짐
- 분류 문제에서는 반드시 Stratified K-Fold를 사용하는 것이 권장됨

이번 3편에서는 머신러닝 모델의 성능을 더욱 정확하고 안정적으로 평가하기 위한 **교차검증 기법(K-Fold, Stratified K-Fold)**을 알아보고 직접 실습해보았습니다.
K-Fold는 기본적인 교차검증 방식이지만, 분류 문제에서는 레이블 비율을 유지하는 Stratified K-Fold가 훨씬 더 적합함을 실습을 통해 확인했습니다.
다음 4편에서는 이러한 교차검증을 더 효율적으로 수행하는 방식인 **cross_val_score와 하이퍼파라미터 튜닝을 위한 GridSearchCV**를 학습합니다.
교차검증 자동화와 모델 최적화의 핵심 내용이므로 반드시 알고 넘어가야 할 부분입니다.
'Programming' 카테고리의 다른 글
| [5편] 머신러닝 데이터 전처리 기본기: 인코딩·스케일링 실습 정리개요 (0) | 2025.12.10 |
|---|---|
| [4편] cross_val_score와 GridSearchCV: 교차검증 자동화 및 하이퍼파라미터 튜닝 (0) | 2025.12.09 |
| [2편] 사이킷런으로 머신러닝 모델 실습하기: 데이터 분할, 학습, 예측, 평가까지 (0) | 2025.12.09 |
| [1편] 사이킷런(scikit-learn) 이해하기: 머신러닝 기본 개념과 예제 아이리스(Iris) 소개 (0) | 2025.12.09 |
| 딥러닝 기초 개념과 학습 프로세스 완벽 정리 (0) | 2025.12.09 |