본문 바로가기
Machine-Learning/Basic

[ML] Cross Validation

by AteN 2023. 1. 11.

Cross Validation (교차 검증)

 

모델을 학습시키는 학습 데이터와 이에 대한 예측 성능을 평가하기 위한 별도의 테스트 데이터가 필요하다. 하지만 이 방법은 과적합(Overfitting)에 취약한 약점을 가질 수 있다. 

과적합은 모델이 학습 데이터에만 과도하게 최적화되어, 실제 예측을 다른 데이터로 수행할 경우 예측 성능이 과도하게 떨어지는 것을 말한다. 그런데 고정된 학습 데이터와 테스트 데이터로 평가를 하다보면 테스트 데이터에만 최적의 성능을 발휘할 수 있도록 편향되게 모델을 유도하는 경향이 생기게 된다. 결국은 해당 테스트에만 과적합되는 학습 모델이 만들어져 다른 테스트 데이터가 들어올 경우에는 성능이 저하된다. 이러한 문제점을 개선하기 위해 교차 검증을 이용해 더 다양한 학습과 평가를 수행한다. 

 

머신러닝에서 데이터는 이상치, 분포도, 다양한 속성값, 피처 중요도 등 여러 가지 머신러닝에 영향을 미치는 요소를 가지고 있다. 특정 머신러닝 알고리즘에서 최적으로 동작할 수 있도록 데이터를 선별해 학습한다면 실제 데이터 양식과는 많은 차이가 있을 것이고 결국 성능 저하로 이어질 것이다. 

교차 검증은 이러한 데이터 편증을 막기 위해서 별도의 여러 세트로 구성된 학습 데이터 세트와 검증 데이터 세트에서 학습과 평가를 수행하는 것이다. 그리고 각 세트에서 수행한 평가 결과에 따라 하이퍼 파라미터 튜닝 등의 모델 최적화를 더욱 손쉽게 할 수 있다. 

대부분의 머신러닝 모델의 성능 평가는 교차 검증을 기반으로 1차 평가를 한 뒤에 최종적으로 테스트 데이터 세트에 적용해 평가하는 프로세스이다. 머신러닝에 사용되는 데이터 세트를 세분화해서 학습, 검증, 테스트 데이터 세트로 나눌 수 있다. 테스트 데이터 세트 외에 별도의 검증 데이터 세트를 둬서 최종 평가 이전에 학습된 모델을 다양하게 평가하는 데 사용한다 

 

K 폴드 교차 검증 

K 폴드 교차 검증은 가장 보편적으로 사용되는 교차 검증 기법이다. 먼저 K개의 데이터 폴드 세트를 만들어서 K 번 만큼 각 폴드 세트에 학습과 검증 평가를 반복적으로 수행하는 방법이다.

 

만약 5 폴드 교차 검증을 수행한다고 하면 (K 가 5) 5개의 폴드된 데이터 세트를 학스보가 검증을 위한 데이터 세트로 변경하면서 5번 평가를 수행한뒤, 이 5개의 평가를 평균한 결과를 가지고 예측 성능을 평가한다. 

먼저 데이터 세트를 K등분 (5등분) 한다. 그리고 첫 번째 반복에서는 처음부터 4개 등분을 학습 데이터 세트, 마지막 5번째 등분 하나를 검증 데이터 세트로 설정하고 학습 데이터 세트에서 학습 수행, 검증 데이터 세트에서 평가를 수행한다. 

첫번째 평가를 수행하고 나면 이제는 두번째 반복에서 다시 비슷한 학습과 평가 작업을 수행한다. 단, 이번에는 학습 데이터와 검증 데이터를 변경한다 (처음부터 3개 등분까지, 그리고 마지막 5번째 등분을 학습 데이터 세트로, 4번째 등분 하나를 검증 데이터로 세트로 설정)

이렇게 학습 데이터 세트와 검증 데이터 세트를 점진적으로 변경하면서 마지막 5번째 (k번째)까지 학습과 검증을 수행하는 것이 바로 K 폴드 교차 검증이다. 

 

Kfold 클래스를 이용해 불꽃 데이터 세트를 교차 검증하고 예측 정확도를 알아보자. 

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

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

# 5개의 폴드 세트로 분리하는 KFold 객체와 폴드 세트별 정확도를 담을 리스트 객체 생성.
kfold = KFold(n_splits=5)
cv_accuaracy = []
print('붓꽃 데이터 세트 크기:', features.shape[0])
붓꽃 데이터 세트 크기: 150

Kflod(n_split=5)을 통해서 전체 붓꽃 데이터를 5개의 폴드 데이터로 분리하여 전체 붓꽃 데이터는 모두 150개이다. 

학습용 데이터 세트는 이중 4/5인 120개, 검증 테스트 데이터 세트는 1/5인 30개로 분할된다. 

 

어떤 값을 실제로 반환되는지도 확인해 보기 위해 검증 데이터 세트의 인덱스를 추출하면

n_iter = 0

# Kflod 객체의 split()를 호출하면 폴드 별 학습용, 검증용 테스트의 로우 인덱스를 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(f'\n#{n_iter} 교차 검증 정확도 : {accuracy}, 학습 데이터 크기 {train_size}, 검증 데이터 크기 {test_size}')
    print(f'#{n_iter} 검증 세트 인덱스{test_index}')
    
    cv_accuaracy.append(accuracy)
    
# 개별 iteration별 정확도를 합하여 평균 정확도 계산
print('\n## 평균 검증 정확도 : ', np.mean(cv_accuaracy))
#1 교차 검증 정확도 : 1.0, 학습 데이터 크기 120, 검증 데이터 크기 30
#1 검증 세트 인덱스[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 24 25 26 27 28 29]

#2 교차 검증 정확도 : 0.9667, 학습 데이터 크기 120, 검증 데이터 크기 30
#2 검증 세트 인덱스[30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
 54 55 56 57 58 59]

#3 교차 검증 정확도 : 0.8667, 학습 데이터 크기 120, 검증 데이터 크기 30
#3 검증 세트 인덱스[60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
 84 85 86 87 88 89]

#4 교차 검증 정확도 : 0.9333, 학습 데이터 크기 120, 검증 데이터 크기 30
#4 검증 세트 인덱스[ 90  91  92  93  94  95  96  97  98  99 100 101 102 103 104 105 106 107
 108 109 110 111 112 113 114 115 116 117 118 119]

#5 교차 검증 정확도 : 0.7333, 학습 데이터 크기 120, 검증 데이터 크기 30
#5 검증 세트 인덱스[120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
 138 139 140 141 142 143 144 145 146 147 148 149]

## 평균 검증 정확도 :  0.9
5번 교차 검증 겨로가 평균 검증 정확도는 0.9이다. 그리고 교차 검증 시마다 검증 세트의 인덱스가 달라짐을 알 수 있다. 
 

Stratified K 폴드

Stratified K 폴드는 불균형한 (imbalanced) 분포도를 가진 레이블(결정 클래스) 데이터 집합을 위한 K폴드 방식이다

불균형한 분포도를 가진 레이블 데이터 집합은 특정 레이블 값이 특이하게 많거나 매우 적어서 값의 분포가 한쪽으로 치우치는 것을 말한다 

Stratified K 폴드는 K 폴드가 레이블 데이터 집합이 원본 데이터 집합의 레이블 분포를 학습 및 테스트 세트에 제대로 분해하지 못하는 경우의 문제를 해결해 준다. 이를 위해 Stratified K 폴드는 원본 데이터의 레이블 분포를 먼저 고려한 뒤 이 분포와 동일하게 학습과 검증 데이터 세트를 분배한다. 

붓꽃 데이터 세트를 예시를 통해 Dataframe으로 생성하고 레이블 값의 분포도를 확인하면

 

import pandas as pd
import numpy as np

from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import KFold, StratifiedKFold
from sklearn.datasets import load_iris

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

Setosa 품종, Versicolor 품종, Viginica 품종 모두 50개로 동일하게 분포 되어 있다. 

이것을 Kfold와 Stratified Kfold의 3개의 폴드 세트로 교차 검증 시마다 생성되는 학습/ 검증 레이블 데이터 값의 분포도를 확인하면

kfold = KFold(n_splits=3)
K_n_iter = 0

for train_index, test_index in kfold.split(iris_df):
    K_n_iter +=1
    label_train = iris_df['label'].iloc[train_index]
    label_test = iris_df['label'].iloc[test_index]
    print(f'## 교차검증 : {K_n_iter}')
    print('학습 label 데이터 분포 : \n', label_train.value_counts())
    print('검증 label 데이터 분포 : \n', label_test.value_counts())
## 교차검증 : 1
학습 label 데이터 분포 : 
 1    50
2    50
검증 label 데이터 분포 : 
 0    50
## 교차검증 : 2
학습 label 데이터 분포 : 
 0    50
2    50
검증 label 데이터 분포 : 
 1    50
## 교차검증 : 3
학습 label 데이터 분포 : 
 0    50
1    50
검증 label 데이터 분포 : 
 2    50
skf = StratifiedKFold(n_splits=3)
s_n_iter = 0

for train_index, test_index in skf.split(iris_df, iris_df['label']):
    s_n_iter +=1
    label_train = iris_df['label'].iloc[train_index]
    label_test = iris_df['label'].iloc[test_index]
    print(f'## 교차검증 : {s_n_iter}')
    print('학습 label 데이터 분포 : \n', label_train.value_counts())
    print('검증 label 데이터 분포 : \n', label_test.value_counts())
 
## 교차검증 : 1
학습 label 데이터 분포 : 
 2    34
0    33
1    33
검증 label 데이터 분포 : 
 0    17
1    17
2    16
## 교차검증 : 2
학습 label 데이터 분포 : 
 1    34
0    33
2    33
검증 label 데이터 분포 : 
 0    17
2    17
1    16
## 교차검증 : 3
학습 label 데이터 분포 : 
 0    34
1    33
2    33
검증 label 데이터 분포 : 
 1    17
2    17
0    16

Kfold와 Stratified Kfold 를 비교하면 Stratified Kfold에서 학습 레이브로가 검증 레이블 데이터 값의 분포도가 비교적 동일하게 분포 되어 있는 것을 확인할 수 있다

features = iris.data
label = iris.target

dt_clf = DecisionTreeClassifier(random_state = 156)
skf = StratifiedKFold(n_splits=3)
cv_accuaracy = []
n_iter = 0

# StratifiedKFold의 split() 호출시 반드시 레이블 데이터 세트도 추가 입력이 필요
for train_index, test_index in skf.split(features, label):
    # 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(f'\n#{n_iter} 교차 검증 정확도 : {accuracy}, 학습 데이터 크기 {train_size}, 검증 데이터 크기 {test_size}')
    print(f'#{n_iter} 검증 세트 인덱스{test_index}')
    
    cv_accuaracy.append(accuracy)
    
# 교차 검증별 정확도 및 평균 정확도 계산
print('\n## 교차 검증별 정확도 : ', np.round(cv_accuaracy, 4))
print('## 평균 검증 정확도 : ', np.mean(cv_accuaracy))
#1 교차 검증 정확도 : 0.98, 학습 데이터 크기 100, 검증 데이터 크기 50
#1 검증 세트 인덱스[  0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  50
  51  52  53  54  55  56  57  58  59  60  61  62  63  64  65  66 100 101
 102 103 104 105 106 107 108 109 110 111 112 113 114 115]

#2 교차 검증 정확도 : 0.94, 학습 데이터 크기 100, 검증 데이터 크기 50
#2 검증 세트 인덱스[ 17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  67
  68  69  70  71  72  73  74  75  76  77  78  79  80  81  82 116 117 118
 119 120 121 122 123 124 125 126 127 128 129 130 131 132]

#3 교차 검증 정확도 : 0.98, 학습 데이터 크기 100, 검증 데이터 크기 50
#3 검증 세트 인덱스[ 34  35  36  37  38  39  40  41  42  43  44  45  46  47  48  49  83  84
  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 133 134 135
 136 137 138 139 140 141 142 143 144 145 146 147 148 149]

## 교차 검증별 정확도 :  [0.98 0.94 0.98]
## 평균 검증 정확도 :  0.9666666666666667

3개의 Stratified Kfold 로 교차 검증한 결과 평균 검증 정확도가 0.96으로 측정되었다. Stratified Kfold의 경우 원본 데이터의 레이블 분포도 특성을 반영한 학습 및 검증 데이터 세트를 만들 수 있으므로 왜곡된 레이블 데이터 세트에서는 반드시 Stratified Kfold를 이용해 교차 검증해야 한다. 

 

교차 검증을 간편하게 - cross_val_score(), cross_validate()

사이킷런은 교차 검증을 편리하게 수행할 수 있는 cross_val_score()를 제공한다 

 

cross_val_score()

(estimator, X, y=None, *, groups=None, scoring=None, cv=None, n_jobs=None, verbose=0, fit_params=None, pre_dispatch='2*n_jobs', error_score=nan)

이중에 estimator, X, y, scoring, cv가 주요 파라미터 이며, 각각의 의미는 다음과 같다 

  • estimator : 분류 ( classifier) 또는 회귀(regressor) 의미
  • X : 피처 데이터 세트
  • y : 레이블 데이터 세트
  • scoring : 예측 성능 평가 지표 
  • cv : 교차 검증 폴드의 수 
import numpy as np

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))
교차 검증별 정확도 :  [0.98 0.94 0.98]
평균 검증 정확도  :  0.9667

cross_val_score()는 cv로 지정된 횟수만큰 scoring 파라미터로 지정된 평가 지표로 평가 결괏값을 배열로 반환한다. 

cross_validate()

from sklearn import datasets, linear_model
from sklearn.model_selection import cross_validate
from sklearn.metrics import make_scorer
from sklearn.metrics import confusion_matrix
from sklearn.svm import LinearSVC

diabetes = datasets.load_diabetes()
X = diabetes.data[:150]
y = diabetes.target[:150]
lasso = linear_model.Lasso()

cv_results = cross_validate(lasso, X, y, cv=3)
print(sorted(cv_results.keys()))
print(cv_results['test_score'])
['fit_time', 'score_time', 'test_score']
[0.33150734 0.08022311 0.03531764]
scores = cross_validate(lasso, X, y, cv=3,
                        scoring=('r2', 'neg_mean_squared_error'),
                        return_train_score=True)
print(scores['test_neg_mean_squared_error'])
print(scores['train_r2'])
[-3635.51152303 -3573.34242148 -6114.78229547]
[0.28010158 0.39088426 0.22784852]

비슷한 cross_validate()는 cross_val_score()는 단 하나의 평가 지표만 가능하지만 cross_validate()는 여러 개의 평가 지표를 반환할 수 있으며, 또한 학습 데이터에 대한 성능 평가 지표와 수행 시간도 같이 제공한다. 

 
 
Reference 
- 머신러닝 완벽 가이드 

댓글