인공지능 개발일지

[기계학습] 레이블의 분포도를 유지하여 데이터 분리와 교차검증 Stratified K-Fold,cross_val_score 본문

인공지능/머신러닝

[기계학습] 레이블의 분포도를 유지하여 데이터 분리와 교차검증 Stratified K-Fold,cross_val_score

Prcnsi 2022. 5. 22. 12:20
728x90

안녕하세요. 이번 시간에는 K-Fold/Stratified K-Fold를 이용해서 교차 검증과 데이터를 분리하는 방법 그리고 GridSearch CV를 이용해서 하이퍼 파라미터를 찾는 법에 대해 알아봅시다.

저는 요즘 머신러닝 사놓은 책들 정주행 중인데 보면서 배운 ML 전반에 대한 이론을 정리하는 중에 있습니다.

이런 이론들을 배워야하는 이유는 나중에 필요할 때 사용할 수 있어야 하기 때문입니다.

 

저의 경우는 다른 라벨에 개수에 민감한 데이터를 만지다가 데이터를 분리해야하는 상황에서 이 Stratified를 이용해서 분리한 경험이 있습니다 라벨의 분포도를 유지하여 데이터를 분리한다는 것의 의미는 원본 데이터의 패턴을 최대한 유지하는 것입니다. 그래서 부록으로 이 장의 마지막에 이의 활용한 코드를 한 번 가져왔습니다.

 

 


 

 

1. K-Fold와 Stratified K-Fold

기본적으로 K-Fold는 데이터를 나누어서 교차검증을 해서 과적합을 예방하는 것이고 Stratified K-Fold는 교차검증+레이블 분포를 고려하여 데이터를 분리해 검증을 하는 것입니다. 

 

2. K-Fold의 개념과 실습

우선 K-Fold는 교차 검증을 통해 과적합을 예방하는 것입니다. 그래서 하이퍼파라미터 K의 개수에 따라 K만큼 데이터를 분리해서 학습하고 검증해서 정확도를 구하는 과정을 k Set 반복한 뒤 이때 정확도의 평균으로  전체의 평균을 구하는 것으로 Train Set에 과적합 되는 것을 방지하기 위한 방법 중 하나입니다.  

 

그래서 아래 그림은 k=5인 예시입니다. 보시면 n_iter은 반복횟수입니다. 그래서 이때 위에서 말한 것과 같이 데이터를 5개로 나누고 1개는 Test Set 나머지 4개는 Train Set으로 하여 학습 후 정확도를 측정하여 이 정확도들의 평균으로 전체 Score를 측정합니다. 그래서 이 K-Fold의 핵심은 학습 데이터의 대상을 고루고루 나눠서 학습시킴으로써 과적합을 방지하는 것입니다.

코드는 아래와 같이 3가지 라인을 구조를 기반으로 동작합니다. 아래 첫 번째와 같이 Kfold를 import해주고 데이터를 나누는 개수를 n_splits로 입력받고 반복문에서 iter 인자로 Kfold.split을 줘서 데이터를 여러 번 나눈 채 학습하게 됩니다.

 

From sklearn.model_selection import Kfold
Kfold = Kfold(n_splits=n) #n개의 덩어리로 나눠줌
Kfold.split(features) #교차검증시 인덱스 설정해줌

 

그럼 이제 실습 코드를 봅시다. 데이터는 사이킷런의 iris 붓꽃 데이터를 이용하여 진행하였습니다.

우선 관련 모듈과 알고리즘을 불러오고 데이터를 불러와주고 KFold도 불러와서 객체에 할당해줍시다.

import pandas as np
from sklearn.model_selection import KFold
from sklearn.datasets import load_iris

# 붓꽃 데이터 세트와 DecisionTreeClassifier를 생성
iris = load_iris()
features = iris.data
label= iris.target
dt_clf = DecisionTreeClassifier(random_state = 156)

# 5개의 폴드 세트로 분리하는 KFold 객체와 폴드 세트별 정확도를 담을 리스트 객체 생성
kfold = KFold(n_splits=5)
cv_accuracy = []

# shape은 그 데이터의 행, 열을 반환하는데 인덱스 [0],[1]로 각각을 접근할 수 있다. 
print('붓꽃 데이터 세트 크기:',features.shape[0])
[Output]
붓꽃 데이터 세트 크기: 150

 

 

그리고 교차 검증 동작 과정을 이해하기 위한 절차로 Kfold.split을 했을 때 train_index, test_index를 한 번 확인해 봅시다. 그럼 아래와 같은 train, test의 인덱스를 반환합니다. 그러니까 위에서 행의 개수가 150개라고 확인했는데 이를 kfold를 이용하면 개수만 4/5, 1/5로 맞춘 채로 흔들어서 섞은 인덱스를 반환하는 것을 알 수 있습니다. 즉 위에서 본 n_splits() 그림처럼 5번의 반복 동안 학습 데이터와 테스트 데이터를 다르게 나눠줍니다. 

n_iter = 0 # 교차 검증 횟수

# KFold 객체의 split()을 호출하면 폴드별 학습용, 검증용 테스트의 로우 인덱스를 array로 반환
for train_index, test_index in kfold.split(features):
    print(train_index,test_index)
[Output]
[ 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  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  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
 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  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]
[  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  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  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
 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] [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]
[  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  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  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
 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] [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]
[  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  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  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
 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] [ 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]
[  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  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  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
  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] [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]

 

그리고 이제 전체 교차 검증을 해보면 아래와 같습니다. 우리가 아까 위에서 교차검증 split 사용시 train_index와 test_index를 잘 섞어서 반환하는 것을 확인했는데 다음은 간단합니다. 그 인덱스 그대로 데이터를 학습시키고 그때의 성능을 cv_accuracy 배열에 append 해서 마지막에 이들의 평균을 출력하면 우리가 원하는 작업이 끝납니다.

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))

 

그렇지만 그냥 K-Fold로 나누었을 때는 라벨의 분포를 고려하지 못해서 아래와 같이 train set에 0과 1의 라벨이 들어가고 test set에 2의 라벨이 들어간다면 모델은 절대 2를 예측할 수 없겠죠? 그래서 라벨의 분포를 고려하여 데이터를 분리해주는 작업이 필요합니다.

from sklearn.model_selection import StratifiedKFold
iris = load_iris()
iris_df = pd.DataFrame(data=iris.data, columns = iris.feature_names)
iris_df['label'] = iris.target
iris_df['label'].value_counts()

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("## 교차 검증 : {}".format(n_iter))
    print("학습 레이블 분포도 : \n", label_train.value_counts())
    print("테스트 레이블 분포도 : \n", label_test.value_counts())
    print("\n")
[Output]
## 교차 검증 : 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. Stratified K-Fold 실습

Stratified K-Fold에 대해 알아봅시다. Stratified K-Fold는 마찬가지로 교차검증에다 나눌 때 레이블의 분포를 유지한채 분리하는 것입니다. 

 

우선 기존 데이터의 라벨의 분포를 봅시다. 아래를 보면 라벨은 세 개인데 골고루 잘 나누어져 있네요. 그렇지만 만약 

iris_df['label'].value_counts()
[Output]
0    50
1    50
2    50
Name: label, dtype: int64

 

 

 

 

위에서는 나눈 인덱스를 봤다면 이제는 이게 라벨을 잘 나눠주는지 봅시다. 오 아래 결과를 보면 위와 다르게 잘 나눠주네요.

DTmodel = DecisionTreeClassifier(random_state=156)

skf = StratifiedKFold(n_splits=3)
n_iter = 0
cv_accuracy = []

for train_index, test_index in skf.split(iris_df, iris.target):
    n_iter += 1;
    x_test, x_train = iris.data[test_index], iris.data[train_index]
    y_test, y_train = iris.target[test_index], iris.target[train_index]
    y_train=pd.DataFrame(y_train)
    y_test=pd.DataFrame(y_test)
    print("## 교차 검증 : {}".format(n_iter))
    print("학습 레이블 분포도 : \n", y_train.value_counts())
    print("테스트 레이블 분포도 : \n", y_test.value_counts())
    print("\n")
[Output]
## 교차 검증 : 1
학습 레이블 분포도 : 
 2    34
0    33
1    33
dtype: int64
테스트 레이블 분포도 : 
 0    17
1    17
2    16
dtype: int64


## 교차 검증 : 2
학습 레이블 분포도 : 
 1    34
0    33
2    33
dtype: int64
테스트 레이블 분포도 : 
 0    17
2    17
1    16
dtype: int64


## 교차 검증 : 3
학습 레이블 분포도 : 
 0    34
1    33
2    33
dtype: int64
테스트 레이블 분포도 : 
 1    17
2    17
0    16
dtype: int64

 

그럼 이제 바로 학습을 시켜 줍시다.

DTmodel = DecisionTreeClassifier(random_state=156)

skf = StratifiedKFold(n_splits=3)
n_iter = 0
cv_accuracy = []

for train_index, test_index in skf.split(iris_df, iris.target):
    n_iter += 1;
    x_test, x_train = iris.data[test_index], iris.data[train_index]
    y_test, y_train = iris.target[test_index], iris.target[train_index]
    DTmodel.fit(x_train,y_train)
    pred = DTmodel.predict(x_test)
    accuracy = np.round(accuracy_score(pred, y_test), 4)
    train_size = x_train.shape[0]
    test_size = x_test.shape[0]
    print("\n# {} 교차 검증 정확도 : {}, 학습 데이터 크기 : {}, 검증 데이터 크기 : {}".format(n_iter, accuracy, train_size, test_size))
    print("\n# {} 교차 검증 인덱스 : {}".format(n_iter, test_index))
    cv_accuracy.append(accuracy)
    
print("\n 정확도 : ", np.mean(cv_accuracy))
[Output]
# 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.9666666666666667

정확도도 아주 잘 나왔네요.

 

마지막 정리

  • K-Fold:  교차검증(과적합 예방)
  • Stratified K-Fold: 교차검증 + 데이터 분리시 라벨 분포도 고려

 

 

4. 실제 활용 예시

아래와 같이 저는 라벨이 약 500개 정도 되는 데이터를 분리할 때 첫 번째 Outlier의 데이터에 과적합 될까봐 Stratified K-Fold를 이용해서 데이터를 분리해줬습니다. 

코드는 아래와 같이 n_splits=2로 주어서 나눈 데이터 둘 중 하나의 세트의 나눈 데이터와 그때의 원본 인덱스를 받아오는 형식으로 방식으로 진행하였습니다.

from sklearn.model_selection import StratifiedKFold

def seperate_train_test():    
  skf = StratifiedKFold(n_splits=2)
  n_iter = 1

  for train_index, test_index in skf.split(train2017,training_labels):
      print("train_index",train_index[2509])
      print(train_index, test_index)
      x_test, x_train = train2017.iloc[train_index,:], train2017.iloc[test_index,:]
      y_test, y_train = training_labels.iloc[train_index,:], training_labels.iloc[test_index,:]   
      print("train 레이블 분포도 : \n", y_test.value_counts())
      print("test 레이블 분포도 : \n", y_train.value_counts())
      
      label_distribute=y_train.value_counts().values
    
      plt.scatter(np.arange(538),label_distribute)
      plt.ylim(0,400)
      plt.title("Reseperate Data")
      plt.xlabel("sequential label values")
      plt.ylabel("Number of people by index")
      print('\n\n')
  return x_train, x_test, y_train, y_test,train_index,test_index
X_train,X_test,y_train,y_test,train_index,test_index=seperate_train_test()

그래서 아래 나눈 전후 라벨의 분포를 확인해 보면 아래와 같이 분포를 비슷하게 유지한채 나눠진 것을 확인할 수 있습니다.

나누기 전
나눈 후

 

그래서 이 전후를 시각화해서 비교하면 아래와 같습니다.

 


그럼 읽어주셔서 감사합니다!

728x90