머신러닝

교차검증(K 폴드 교차 검증, Stratified K 폴드)

Y0un9Ki 2024. 2. 21. 18:59

우리가 머신러닝 알고리즘을 학습시키기 위해선 학습 데이터(X_train)와 이에 대한 예측 성능을 평가하기 위한 별도의 테스트용 데이터(X_test)가 필요하다고 했다.

그리고 학습을 위한 데이터의 양을 일정 수준 이상으로 보장해주는 것 그리고 학습된 모델에 대해 다양한 데이터를 기반으로 예측 성능을 평가하는 것도 중요하다.

왜 그러할까? 생각을 해보면 우리는 과적합이라는 머신러닝에 취약한 약점이 있다는 것을 알고 있다. 우리는 계속해서 분류모델의 의사결정나무(DecisionTreeClassifier)을 사용하는데 이 분류기는 특히 과적합에 취약하다.

 

그렇다면 과적합은 무엇인가? 

 

과적합(Overfitting)이란? : 모델이 학습 데이터에만 과도하게 최적화되어, 실제 예측을 다른 데이터로 수행할 경우에 예측 성능이 과도하게 떨어지는 것을 의미한다. 

 

그런데 우리는 계속해서 고정된 학습 데이터(X_train)와 별도의 테스트 데이터(X_test)로 평가를 하다 보니 테스트 데이터에만 최적의 성능을 발휘 하도록 편향되게 모델을 유도하는 경향이 생기며 이에 따라 해당 테스트 데이터에만 과적합되는 학습 모델이 만들어지고 다른 테스트용 데이터가 들어올 경우에 성능이 저하되는 경우가 생긴다.

 

그렇기에 우리는 이러한 문제점을 개선하기 위해 교차 검증을 이용해 더 다양한 학습과 평가를 진행해야한다. 교차검증의 필요성을 제시하는 것이다.

여기서 궁금증이 나온다 도대체 교차검증을 왜 하는가???  이것의 답변은 머신러닝의 성능을 올려주기 위함이다!!!

머신러닝의 성능을 올려주는 몇몇 가지의 방법이 있는데 밑에 적어보겠다.

  1. 샘플링을 잘해서 성능을 올림 ( ex) 층화추출... )
  2. 교차검증을 통해서 성능을 올림 
  3. 하이퍼파라미터 튜닝을 통해서 성능을 올림
  4. 데이터의 재가공을 통해서 성능을 올림
  5. 모델의 종류(클래스)를 변경하면서 성능을 올림

 

교차검증

내가 공부하는 책에서 교차검증을 쉽게 설명을 해놓은 예시가 있다.

교차검증은 본고사를 치르기 전에 모의고사를 여러 번 보는 것이다.

본 고사가 테스트 데이터 세트에 대해 평가하는 거라면 모의고사는 교차검증에서 많은 학습과 검증 세트에서 알고리즘 학습과 평가를 수행하는 것 이라고 표현되어있다.

 

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

 

교차 검증은 이러한 데이터 편중을 막기 위해서 별도의 여러 세트로 구성된 학습(=훈련) 데이터 세트와 검증 데이터 세트에서 학습과 평가를 수행하는 것이다. 

대분분의 머신러닝 모델의 성능 평가는 교차 검증 기반으로 1차 평가를 한 뒤에 최종적으로테스트 데이터 세트에 적용해 평가하는 프로세스이다.

ML에 사용되는 데이터 세트를 세분화해서 학습(=훈련), 검증, 테스트 데이터 세트로 나눌 수 있다. 테스트 데이터 세트 외에 별도의 검증 데이터 세트를 둬서 최종평가 이전에 학습된 모델을 다양하게 평가를 한다.

 

밑에 그림에서 학습 데이터 세트X_train 이며 테스트 데이터 세트는 X_test 가 된다.

거기서 학습 데이터 세트(X_train)에서는 교차검증을 위해 학습 데이터 세트와 검증 데이터 세트로 다시 나뉜다.

 

학습데이터 세트에서 분할된 데이터는 학습(=훈련) 데이터 세트와 검증 데이터 세트로도 다시 나뉜다.!!!

교차검증을 진행하기 위해서!!!!

 

학습데이터가 다른 학습 세트로 나뉘는 이미지

 

K 폴드 교차 검증

K 폴드 교차 검증은 가장 보편적으로 사용되는 교차 검증 기법이다. 먼저 학습 데이터 세트(X_train)에서 학습(=훈련) 데이터 세트 검증 데이터 세트를 다시 나누기 전에 K개의 데이터 폴드 세트를 만들어서 랜덤하게 학습 데이터 및 테스트 데이터 세트의 인덱스를 뽑아 K번만큼 각 폴드 세트에 학습과 검증 평가를 반복적으로 수행하는 방법이다.

K Fold가 돌아가는 방식                 (꼭 기억하자!!!)

5개의 폴드로 폴딩된 학습 데이터 세트(X_train)를 학습 데이터 4개와 검증 데이터 1개로 나눠서 할당을 하고 이 순서를 번갈아 가면서 5번의 평가를 수행한뒤,  5개의 평가를 평균한 결과를 가지고 예측 성능을  테스트 데이터 세트(X_test)로 평가한다.

매우 중요!!!!!

 

다음 그림과 같은 K 폴드 교차 검증은 5 폴드 교차 검증이다.

 

이제 이것을 코드로 구현 해 보자

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)

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

# 결과값 붓꽃 데이터 세트 크기: 150

5번의 K 폴드 교차 검정을 하겠다는 의미이다.

결과값으로는 피처 데이터 세트 크기가 150개가  나왔다.

여기서 이제 5 폴드 교차 검증을 하면 150을 5개의 폴드 데이터로 나눈다.

그러면 각각 30개의 데이터가 담기고, 1개의 폴드 데이터는 검증 데이터로 4개의 폴드 데이터 세트는 학습(훈련) 데이터로 서로 서로 바뀌면서 5번의 예측 평가를 한후 5번의 결과의 평균값으로 K 폴드 평가 결과로 반영된다.

n_iter = 0
# KFold객체의 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('\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 폴드 교차 검증을 하면 150을 5개의 폴드 데이터로 나눈다.

그러면 각각 30개의 데이터가 담기고, 1개의 폴드 데이터는 검증 데이터로 4개의 폴드 데이터 세트는 학습(훈련) 데이터로 서로 서로 바뀌면서 5번의 예측 평가를 한후 5번의 결과의 평균값으로 K 폴드 평가 결과로 반영된다.

우리가 예상했던 결과대로 결과가 나온 것을 볼 수 있다.

 

Stratified K 폴드

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

불균형한 분포도를 가진 레이블 데이터 집합은 특정 레이블 값이 특이하게 많거나 매우 적어서 값의 분포가 한쪽으로 치우는 경우 그것의 비율을 고려해서 본래의 레이블 데이터의 비율에 맞게 학습 레이블/검증 레이블 데이터를 뽑는 것을 말한다.

이것을 층화추출(stratified sampling)이라고도 한다.

 

이해를 돕기 위해 예시를 들어보겠다.

대출 사기 데이터를 예측한다고 가정해 보겠다.

이 데이터 세트는 1억건이며, 수십개의 피처와 대출 사기 여부를 뜻하는 레이블(대출 사기: 1, 정상대출: 0)로 수치형 데이터로 되어있다. 하지만 1억건의 데이터 중에서는 대부분이 정상 대출일 것이며 대출 사기 건은 거의 없을 것이다. 여기서 대출 사기 건을 약 1000건이 있다고 가정한다면 전체 데이터 중에 0.0001%의 아주 작은 확률로 대출 사기 레이블이 존재 한다는것이다.

이렇게 작은 비율로 1 레이블 값이 있다면 K 폴드와 같이 랜덤하게 학습 데이터 및 테스트 데이터 세트의 인덱스를 고르더라도 레이블 값인 0과 1의 비율을 제대로 반영하지 못할 것이다.  어쩔때는 1 레이블이 많이 들어있다가도 어쩔때는 아예 1 레이블이 뽑히지 않을 수도 있다는 것이다.

하지만 우리가 대출 사기를 예측할 때 레이블이 1인 레코드는 비록 건수는 적지만 알고리즘에서 중요한 피처 값을 가지고 있기 때문에 매우 중요한 데이터 세트이다. 그렇기 때문에 원본 데이터와 유사한 대출 사기 레이블 값의 분포를 학습 / 테스트 세트에도 유지하는게 매우 중요하다.

Stratified K 폴드는 이처럼 K 폴드가 레이블 데이터 집합이 원본 데이터 집합의 레이블 분포를 학습 및 테스트 세트에 제대로 분배하지 못하는 경우의 문제를 해결해준다. 

 Stratified K  폴드는 원본 데이터의 레이블 분포를 먼저 고려한 뒤 이 분포와 동일하게 학습(훈련) 데이터 세트와  검증 데이터 세트를 분배 한다.

 

간단하게 붓꽃 데이터 세트를 보고 분포도를 확인해보자

# K 폴드 교차 검증
import pandas as pd
iris = load_iris()
iris_df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
iris_df['label']=iris.target   #iris.target은 0,1,2 로 이루어진 배열이다. 품종을 수치데이터로 바꿔놓은것이다.
iris_df['label'].value_counts()

 

label
0    50
1    50
2    50
Name: count, dtype: int64

결과값이 나온 것을 볼 수 있다. label 값의 분포가 모두 일정한 것을 볼 수 있다.

우리가 K 폴드를 사용했을 때 문제가 되는 문제를 보기위해 3개의 폴드 세트를 KFOLD로 생성하고 교차 검증시마다 생성되는 학습 레이블/검증 레이블 데이터 값의 분포도를 보자

kfold = KFold(n_splits=3)
# kfold.split(X)는 폴드 세트를 3번 반복할 때마다 달라지는 학습(=훈련)/검증 데이터 세트의 row(행) 인덱스 번호를 반환. 
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
학습 레이블 데이터 분포:
 1    50
2    50
Name: label, dtype: int64
검증 레이블 데이터 분포:
 0    50
Name: label, dtype: int64
## 교차 검증: 2
학습 레이블 데이터 분포:
 0    50
2    50
Name: label, dtype: int64
검증 레이블 데이터 분포:
 1    50
Name: label, dtype: int64
## 교차 검증: 3
학습 레이블 데이터 분포:
 0    50
1    50
Name: label, dtype: int64
검증 레이블 데이터 분포:
 2    50
Name: label, dtype: int64

 

교차 검증 시마다 3개의 폴드 세트로 만들어지는 학습 레이블과 검증 레이블이 완전히 다른 값으로 추출되었다.

예를 들어서 첫 번째 교차검증시에는 학습 레이블의 1, 2 값이 각각 50개가 추출되었고 검증 레이블의 0값이 50개가 추출되었다.

이러면 학습 레이블이 1,2 밖에 없기에 0의 경우 전혀 학습을 하지 못하고 그렇기에 0을 절대 예측하지 못하는 상황이 발생된다.

이러한 경우는 교차 검증 데이터 세트를 분할하면 검증 예측 정확도는 0이 될 수밖에 없다.

  • 레이블 0의 값을 학습하지도 않고 예측을 하지 못하기 때문에

그렇기 때문에 레이블의 분포도 비율을 먼저 고려한뒤 고려한 분포도를 반영하는 stratifiedKFold를 사용해야 하는 것이다. 

 

stratifiedKFold는  KFold와 사용법이 비슷하지만 단 하나의 큰 차이가 존재하는데 그것은 stratifiedKFold는 레이블의 분포도를 알아야 학습(=훈련)/검증 데이터를 나누기 때문에 split() 메서드 안에 인자로 피처 데이터 세트 뿐만 아니라 레이블 데이터 세트도 반드시 필요하다는 것이다.

 

코드를 살펴보자!

 

# stratified K 폴드 방식 (층화추출 방식으로 샘플링)
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())
## 교차 검증: 1
학습 레이블 데이터 분포:
 2    34
0    33
1    33
Name: label, dtype: int64
검증 레이블 데이터 분포:
 0    17
1    17
2    16
Name: label, dtype: int64
## 교차 검증: 2
학습 레이블 데이터 분포:
 1    34
0    33
2    33
Name: label, dtype: int64
검증 레이블 데이터 분포:
 0    17
2    17
1    16
Name: label, dtype: int64
## 교차 검증: 3
학습 레이블 데이터 분포:
 0    34
1    33
2    33
Name: label, dtype: int64
검증 레이블 데이터 분포:
 1    17
2    17
0    16
Name: label, dtype: int64

 결과를 보면 학습 레이블과 검증 레이블 데이터 값의 분포도가 거의 동일하게 할당됐음을 알 수 있다. 레이블 전체 데이터의 분포가 0은 50개, 1은 50개, 2는 50개로 서로 동일한 분포를 보였기 때문에 stratifiedKFold를 했을 때 동일한 분포로 결과값이 나온 것을 볼 수 있다.

 

이결과를 가지고 붓꽃 데이터에 StratifiedKFold를 이용해 데이터를 분리하고 예측을 해보자!!

 

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):
    # 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))
#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.9667

 

3개의 StratifiedKFold로 교차 검증한 결과 평균 검증 정확도가 약 96.7%로 측정 되었다. KFold로 교차 검증을 했을 때 0.9보다 더 높은 예측결과를 보여주는 것을 볼 수 있다.

 

Stratified K Fold 요약 : Stratified K Fold의 경우 원본 데이터의 레이블 분포도 특성을 반영한 학습(=훈련) 및 검증 데이터 세트를 만들 수 있으므로 왜곡된 레이블 데이터 세트에서는 반드시 Stratified K Fold를 이용해서 교차 검증을 해야 한다. 일반적으로 분류에서의 교차 검증은 K 폴드가 아닌 Stratified K 폴드로 분할해야 한다.  그리고 이러한 행위를 하는 이유는 모두 머신러닝의 성능을 올려주기 위함이다!!! 매우 중요!!!!

 

예외의 내용으로 회귀에서는 Stratified K 폴드가 지원되지 않는다. 그 이유는 간단하게 회귀의 결정값은 이산값 형태의 레이블이 아니라 연속된 숫자값이기 때문에 결정값별로 분포를 정하는 의미가 없기 때문이다.

++--++