본문 바로가기
Python/Machine Learning

[ML]_머신러닝에서의 교차 검증 - K 폴드 vs. Stratified K 폴드

by ssolLEE 2023. 8. 22.
반응형

  • 이 공부는 다음 멋진 책과 함께 합니다. 위키북스와 저자님 너무나도 감사합니다. 

교차 검증

  • 교차 검증은 데이터 편중을 막기 위해 별도의 여러 세트로 구성된 학습 데이터 세트와 검증 데이터 세트에서 학습과 평가를 수행하는 것입니다.
  • 머신러닝에서 사용되는 데이터 세트를 학습 데이터와 테스트 데이터로 나눕니다.
  • 학습 데이터 세트를 다시 분할하여, 학습 데이터 세트와 학습된 모델의 성능을 일차 평가하는 검증 데이터 세트로 나눕니다.
  • 모든 학습/검증 과정이 완료된 후 테스트 데이터 세트로 최종적으로 성능을 평가합니다.
  • 데이터가 적을 때 사용합니다. 

 

K 폴드 교차 검증

  • 데이터 샘플링에서 생길 수 있는 이슈를 줄이고자 학습과 검증 평가를 반복적으로 수행하는 것입니다. 
  • 사이킷런에서는 K 폴드 교차 검증 프로세스를 구현하기 위해 KFold와 StratifiedKFold 클레스를 제공합니다. 
  • KFold 클래스를 이용해 붓꽃 데이터 세트를 교차 검증하고 예측 정확도를 알아보겠습니다.

붓꽃 데이터 세트로 실습하기

#1. 데이터 불러오기

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold
import numpy as np

iris = load_iris()
features = iris.data
label = iris.target
dt_clf = DecisionTreeClassifier(random_state=156)

print("붓꽃 데이터 세트 크기: ", features.shape[0])
  • 붓꽃 데이터 세트 크기는 150으로 출력됩니다. 

 

#2. KFold 코드 작성

  • KFold 객체 생성
# 5개의 폴드 세트로 분리하는 KFold 객체와 폴드 세트별 정확도를 담을 리스트 객체 생성
kfold = KFold(n_splits = 5)
cv_accuracy = []
  • split()을 호출해 전체 붓꽃 데이터를 5개의 폴드 데이터 세트로 분리하겠습니다. 
  • 전체 150개 데이터 중에서 학습용 데이터 세트는 4/5에 해당하는 120개, 검증 데이터 세트는 나머지 1/5인 30개로 분할됩니다. 
n_iter = 0    # 학습 데이터 반복 횟수. 반복하며 1씩 증가됨

# KFold 객체의 split()을 호출하면 폴드별 학습용, 검증용 테스트의 row index를 array로 반환
for train_index, test_index in kfold.split(features):
    # kfold.split()으로 반환된 인덱스를 이용해 학습용, 검증용 테스트 데이터 추출
    X_train, X_test = features[train_index], features[test_index]  # 독립변수
    y_train, y_test = label[train_index], label[test_index]        # 종속변수 

    # 학습 및 예측
    dt_clf.fit(X_train, y_train)
    pred = dt_clf.predict(X_test)
    n_iter += 1

    # 반복 시마다 정확도 측정
    accuracy = np.round(accuracy_score(y_test, pred), 4)
    train_size = X_train.shape[0]
    test_size = X_test.shape[0]
    print('\n#{0} 교차 검증 정확도 :{1}, 학습 데이터 크기: {2}, 검증 데이터 크기: {3}'
          .format(n_iter, accuracy, train_size, test_size))
    print('#{0} 검증 세트 인덱스:{1}'.format(n_iter,test_index))
    cv_accuracy.append(accuracy)

# 개별 iteration별 정확도를 합하여 평균 정확도 계산
print("\n## 평균 검증 정확도", np.mean(cv_accuracy))

  • 5번 교차 검증 결과 평균 검증 정확도는 0.9가 나왔습니다. 
  • 교차 검증을 할 때마다 검증 세트의 인덱스가 달라진 것도 확인할 수 있습니다. 

 

Stratified K 폴드

  • 특정 레이블 값이 특이하게 많거나 매우 적어서 값의 분포가 한쪽으로 치우치는 것을 불균형한 분포도를 가졌다고 말합니다. 
  • 예 : 대출 사기 데이터 세트 1억 건 중 대출 사기가 1000건(0.0001%로 아주 작은 확률로 대출 사기 레이블이 존재, 거의 대부분은 정상 대출)
  • 불균형한 분포도를 가진 레이블 데이터 집합을 위한 K 폴드 방식입니다.
  • 층화 추출에서 나온 개념입니다.
  • 분류 모델에서는 Stratified K 폴드를 꼭 써야 합니다.
  • 원본 데이터의 레이블 분포를 먼저 고려한 뒤, 이 분포와 동일하게 학습과 검증 데이터 세트를 분배합니다. 

#1. 데이터 불러오기

import pandas as pd

iris = load_iris()
iris_df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
iris_df['label'] = iris.target
iris_df['label'].value_counts()

  • 붓꽃의 품종(setosa, versicolor, virginica)의 레이블 값이 모두 동일하게 50개입니다. 
  • 이슈가 발생하는 상황을 도출하기 위해 3개의 폴드 세트를 KFold로 생성하고, 각 교차 검증 시마다 생성되는 학습/검증 레이블 데이터 값의 분포도를 확인하겠습니다. 

#2. K 폴드의 단점 확인하기

kfold = KFold(n_splits=3)
n_iter = 0

for train_index, test_index in kfold.split(iris_df):
    n_iter += 1
    label_train = iris_df['label'].iloc[train_index]
    label_test = iris_df['label'].iloc[test_index]
    print('## 교차 검증: {0}'.format(n_iter))
    print('학습 레이블 데이터 분포:\n', label_train.value_counts())
    print('검증 레이블 데이터 분포:\n', label_test.value_counts())

  • 출력값을 보면, 학습 레이블과 검증 레이블이 완전히 다른 값으로 추출된 것을 확인할 수 있습니다. 
  • 첫 번째 교차 검증을 보면, 학습 레이블은 1, 2만 있으므로 0의 경우는 전혀 학습하지 못합니다. 
  • 검증 레이블은 0밖에 없으므로 학습 모델은 절대 0을 예측하지 못합니다. 
  • 이런 유형으로 교차 검증 데이터 세트를 분할하면 검증 예측 정확도는 0이 될 수밖에 없습니다. 
  • 이러한 점을 보완하는 것이 바로 StratifiedKFold입니다. 

#3. StrartifiedKFold 코드 작성하기

from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits = 3)
n_iter = 0

for train_index, test_index in skf.split(iris_df, iris_df['label']):
    n_iter += 1
    label_train = iris_df['label'].iloc[train_index]
    label_test = iris_df['label'].iloc[test_index]
    print('## 교차 검증: {0}'.format(n_iter))
    print('학습 레이블 데이터 분포:\n', label_train.value_counts())
    print('검증 레이블 데이터 분포:\n', label_test.value_counts())

  • 출력 결과를 보니, 학습 레이블과 검증 레이블 데이터 값의 분포도가 거의 동일하게 할당되었음을 확인할 수 있습니다.

 

#4. 모형 학습 및 예측 평가

from pandas._libs.tslibs.dtypes import npy_unit_to_abbrev

dt_clf = DecisionTreeClassifier(random_state = 156)
skfold = StratifiedKFold(n_splits=3)
n_iter = 0
cv_accuracy = []

# StratifiedKFold의 split() 호출 시 반드시 레이블 데이터 세트도 추가 입력 필요
for train_index, test_index in skfold.split(features, label):
    # print(train_index, test_index) ## 적절하게 데이터가 섞여 있는지 확인!(정밀한 검증을 할 수 있겠군!!)
    
    # split()으로 반환된 인덱스를 이용해 학습용, 검증용 테스트 데이터 추출
    X_train, X_test = features[train_index], features[test_index]
    y_train, y_test = label[train_index], label[test_index]

    # 학습 및 예측
    dt_clf.fit(X_train, y_train)
    pred = dt_clf.predict(X_test)

    # 반복 시마다 정확도 측정
    n_iter += 1
    accuracy = np.round(accuracy_score(y_test, pred), 4)
    train_size = X_train.shape[0]
    test_size = X_test.shape[0]
    print('\n#{0} 교차 검증 정확도 :{1}, 학습 데이터 크기: {2}, 검증 데이터 크기: {3}'
          .format(n_iter, accuracy, train_size, test_size))
    print('#{0} 검증 세트 인덱스:{1}'.format(n_iter,test_index))
    cv_accuracy.append(accuracy)

# 교차 검증별 정확도 및 평균 정확도 계산
print('\n## 교차 검증별 정확도:', np.round(cv_accuracy, 4))
print('## 평균 검증 정확도:', np.round(np.mean(cv_accuracy), 4))

  • 위에서 진행한 K 폴드 교차 검증에 비해 평균 검증 정확도가 약 96.67%로 증가하였습니다. 
  • Stratified K 폴드의 경우 원본 데이터의 레이블 분포도 특성을 반영한 학습 및 검증 데이터 세트를 만들 수 있으므로 왜곡된 레이블 데이터 세트에서는 반드시 Stratified K 폴드를 이용해 교차 검증을 해야 합니다. 
  • 일반적으로 분류에서의 교차 검증은 Stratified K 폴드로 분할되어야 합니다.
  • 회귀에서는 Stratified K 폴드가 지원되지 않습니다. 
    • 회귀의 결정값은 이산값이 아닌, 연속된 숫자값이기 때문에 결정값별로 분포를 정하는 의미가 없습니다. 

 

교차 검증을 보다 간편하게 : cross_val_score()

  • KFold로 데이터를 학습하고 예측하는 코드를 다시 살펴보면 다음의 과정이 될 것입니다. 
    • 폴드 세트를  설정
    • for 루프에서 반복으로 학습 및 테스트 데이터의 인덱스 추출
    • 반복적으로 학습과 예측을 수행하고 예측 성능 반환
  • 위와 같은 일련의 과정을 한꺼번에 수행해주는 API가 cross_val_score()입니다. 다시 말하면 반복문 코드를 줄여주는 것입니다. 
  • 다음은 cross_val_score() API의 선언 형태입니다.
    • estimator : 머신 러닝 모델을 의미합니다. Classifier, Regressor 등이 있습니다. 이 매개변수에는 모델을 초기화한 객체를 넣어주어야 합니다. 
    • X : 피처 데이터 세트. # 입력
    • y : 레이블 데이터 세트. 타겟(지도학습에서 예측을 수행할 타겟 변수) # 출력
    • scoring : 예측 성능 평가 지표(기본값: Classifier일 경우에는 accuracy, Regressor의 경우에는 r2)
    • cv : 교차 검증 폴드 수(None = 기본적으로 5-fold cross validation)
    • n_jobs : 병렬로 교차 검증을 수행할 때 사용할 CPU 코어의 갯수 지정. 기본값 1, -1은 가능한 모든 코어 사용
    • verbose : 교차 검증 과정을 얼마나 자세하게 출력할 지 제어
    • fit_params : estimator를 fit할 때 사용할 추가적인 매개변수를 딕셔너리 형태로 지정
    • pro_dispatch : 병렬로 실행 중에 전달되는 작업 수를 제어
cross_val_score(estimator, X, y=None, scoring=None, cv=None, 
				n_jobs=1, verbose=0, fit_params=None, pro_dispatch='2*n_jobs')

 

  • 다음과 같이 코드를 입력합니다. 
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score, cross_validate
from sklearn.datasets import load_iris

iris_data = load_iris()
dt_clf = DecisionTreeClassifier(random_state = 156)

data = iris_data.data
label = iris_data.target

# 성능 지표는 정확도(accuracy), 교차 검증 세트는 3개
scores = cross_val_score(dt_clf, data, label, scoring='accuracy', cv = 3)
print("교차 검증별 정확도: ", np.round(scores, 4))
print("평균 검증 정확도: ", np.round(np.mean(scores), 4))

  • cross_val_score() API는 내부에서 estimator를 학습(fit), 예측(predict), 평가(evaluation)를 시켜주므로 간단하게 교차 검증을 수행할 수 있습니다. 
  • 출력결과는 우리가 위에서 StratifiedKFold 수행 결과와 일치합니다. 
    • cross_val_score() 는 내부적으로 StratifiedKFold을 이용하기 때문입니다. 

 

 

지금까지 교차 검증에 대해 학습해보았습니다. 

우리는 모형의 정확성을 높이기 위해 교차 검증을 하지만 그 중에서도 분류에서는 꼭 Stratified K 폴드를 써야겠다는 결론을 얻었네요. 

다음 포스팅에서는 이러한 교차 검증을 보다 간편하게 제공해주는 사이킷런의 API를 살펴보겠습니다.

오늘도 감사합니다!