교차 검증(Cross Validation)
알고리즘을 학습시키기 위해선 학습 데이터와 이에 대한 예측 성능을 평가하기 위한 별도의 테스트용 데이터가 필요하다. 하지만 이러한 방법은 과적합(Overfitting)
의 위험이 있다. 과적합이란, 모델이 학습 데이터에만 과도하게 최적화되어 실제 예측을 다른 데이터로 수행할 경우 예측 성능이 매우 떨어지는 것을 말한다. 또한 고정된 학습 데이터와 테스트 데이터를 통해 모델을 평가하면, 테스트 데이터에만 최적의 성능을 발휘할 수 있도록 편향되게 모델을 유도하게 된다.
이러한 문제점을 개선하는 방법은 교차 검증(Cross Validation)
을 수행하는 것이다. 교차 검증이란, 별도의 여러 세트로 구성된 학습 데이터 세트와 검증 데이터 세트를 통해 학습과 평가를 수행하는 것이다. 테스트 데이터 세트에 대해 평가하는 것을 수능이라고 비유한다면, 학습 데이터를 통해 학습을 하고, 이후 검증 데이터 세트를 통해 알고리즘 학습과 평가를 수행하는 것을 모의고사에 비유할 수 있다. 이러한 교차 검증 과정에서 각 세트마다 수행한 평가 결과에 따라 하이퍼 파라미터 튜닝 등의 모델 최적화를 더욱 손쉽게 할 수 있다.
대부분의 머신러닝 모델의 성능 평가는 교차 검증 기반으로 1차 평가를 한 뒤, 최종적으로 테스트 데이터 세트에 적용해 평가하는 프로세스이다. 머신러닝에 사용되는 데이터 세트를 세분화해서 학습 데이터 + 검정 데이터 + 테스트 데이터 세트로 나눌 수 있다.
K-Fold Cross Validation
K-Fold Cross Validation
은 가장 보편적으로 사용되는 교차 검증 기법으로, K개의 데이터 폴드 세트를 만들어서 K번만큼 각 Fold set에 학습과 검증 평가를 반복적으로 수행하는 방법이다. K-Fold Cross Validation의 수행 과정은 그림을 보면 이해가 쉽다.
해당 이미지는 K=5로 지정하여 교차 검증을 수행한다고 가정했다. 전체 데이터를 5개의 Fold로 나눈 후, 각 iteration 마다 다른 Fold를 Test Set로 지정하고, 남은 4개의 Fold를 통해 학습을 진행한다. 이렇게 총 5번의 학습과 평가 과정을 거친 후 5개의 예측 평가를 평균내서 K-Fold 평가 결과 값으로 반영한다. 붓꽃 데이터를 통해 간단하게 K-Fold Cross Validation을 진행해봤다.
# 붓꽃 데이터 세트와 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 = []
간단하게 붓꽃 데이터 세트를 불러온 후에 DecisionTree 객체를 불러왔고, Fold 세트별 정확도를 나타낸 리스트 객체를 생성했다.
n_iter = 0 # 교차 검증 횟수
# KFold 객체의 Fold별 학습, 검증 데이터의 인덱스 반환
for train_index, test_index in kfold.split(features):
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))
출력 결과를 보면, 각 Iteration마다 학습용 데이터와 검증용 데이터의 크기는 동일하지만, 다른 인덱스의 데이터들이 검증용 데이터로 포함된 것을 확인할 수 있다. 이를 통해 위에서 확인한 Iteration별로 다른 Fold가 검증용 데이터로 활용된다는 것을 확인할 수 있다. 그리고 마지막으로 각 Fold별 학습 정확도를 평균해 전체적인 평균 정확도를 계산한 결과 0.9의 accuracy가 나왔다.
Stratified K-Fold Cross Validation
Stratified K-Fold
는 불균형(imbalanced)한 분포를 가진 레이블 데이터 집합을 위한 교차 검증 방식이다. Stratified K-Fold Cross Validation은 원본 데이터의 레이블 분포를 먼저 고려한 뒤 이 분포와 동일하게 학습과 검증 데이터 세트를 분배한다.
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('## 교차 검증: {0}'.format(n_iter))
print('학습 레이블 데이터 분포:\n', label_train.value_counts())
print('검증 레이블 데이터 분포:\n', label_test.value_counts())
붓꽃 데이터 세트의 본래 클래스 비율은 Setosa 품종, Versicolor 품종, Virginica 품종 모두 50개씩 동일하다. 이를 그냥 교차 검증을 시행하면 각각 학습/검증 레이블이 완전히 다르게 나온다. 예를 들어 첫 번째 교차 검증에서는 1과 2에 해당하는 클래스가 50개씩 학습 데이터를 구성하지만, 정작 검증 데이터에서는 0에 해당하는 데이터만 50개가 있기 때문에 정확도는 당연히 0이 나올 수 밖에 없다.
skf = StratifiedKFold(n_splits=3)
n_iter=0
# Stratified K Fold 수행
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())
Stratified K-Fold Cross Validation을 수행하면, 기존에 K-Fold Cross Validation과는 다르게 학습/검증 데이터 내 레이블의 분포가 동일하게 유지되는 것을 확인할 수 있다.
dt_clf = DecisionTreeClassifier(random_state=156)
skfold = StratifiedKFold(n_splits=3)
n_iter=0
cv_accuracy=[]
for train_index, test_index in skfold.split(features, label):
# 학습용, 검증용 테스트 데이터 추출
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.mean(cv_accuracy))
Stratified K Fold Cross Validation을 수행한 결과, 기존에 K-Fold Cross Validation에 비해 정확도가 상승한 것을 확인할 수 있다.(90% -> 96.67%) 이처럼 레이블 분포가 왜곡된 데이터 세트에 대한 분류(classification)
의 경우, 반드시 Stratified K-Fold Cross Validation를 진행해야 한다. 하지만 회귀(Regression)
문제에선 Stratified K-Fold Cross Validation을 수행할 수 없다. 이는 회귀의 레이블 값이 이산값이 아닌 연속된 형태의 값이기 때문에 결정값별 분포를 고려하는 것이 의미가 없기 때문이다.
cross_val_score()
K-Fold Cross Validation의 코드 구성은 Fold Set를 정하고, for loop에서 반복적으로 학습 및 검증 데이터의 인덱스를 추출한 뒤, 반복적으로 학습과 예측을 수행하고 성능을 반환한다. cross_val_score()
는 이러한 과정을 한번에 수행할 수 있도록 도와주는 API이다. cross_val_score()의 파라미터는 다음과 같다.
- estimator : regressor or classifier
- x : 피처 데이터 세트
- y : 레이블 데이터 세트
- scoring : 성능 평가 지표
- cv : cross validation fold 횟수
cross_val_score()의 반환값은 scoring 파라미터로 지정한 성능 지표 값이다. 또한 classifier가 입력되면 자동으로 Stratified K-Fold 방식으로 학습/검증 데이터를 분할하며, Regressor의 경우 K-Fold Cross Validation 방식을 사용한다. Scoring 파라미터의 성능 지표의 종류는 매우 많은데, 이는 하단의 링크에서 확인 가능하다. 여기서 특이한 점은, cross_val_score() 함수의 성능 지표들은 모두 큰 값이 좋은 성능을 나타낸다는 점이다. 따라서 MAE 같은 평가 지표들도 큰 값이 더 좋은 성능을 나타내기 위해 Negative MAE를 사용한다.
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score , cross_validate
from sklearn.datasets import load_iris
import numpy as np
# 붓꽃 데이터 로드 및 DecisionTreeClassifier 모델 생성
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))
앞서 본 예제처럼 붓꽃 데이터 세트를 로드하고, DecisionTree 객체를 생성한 후 3개의 Fold로 교차 검증을 수행한다. 이처럼 cross_val_score()는 내부에서 Estimator에 대한 fit, predict, evaluation을 한꺼번에 수행해 간편하게 교차 검증을 수행할 수 있다.
cross_val_score()와 비슷한 API로 cross_validate()
가 있다. 이는 cross_val_score()가 하나의 평가 지표만 반환하는 반면, 여러 개의 평가 지표를 한꺼번에 반환할 수 있다. 또한 학습 데이터에 대한 성능 평가 지표와 수행 시간도 같이 제공된다.
GridSearchCV()
하이퍼 파라미터는 머신러닝 알고리즘의 주요 구성 요소이며, 이 값을 조정해 해당 모델의 예측 성능을 개선할 수 있다. 사이킷런은 GridSearchCV()
를 통해 Cross Validation을 위한 학습/검증 데이터 세트를 자동으로 분할한 뒤에, param_grid에 지정한 하이퍼 파라미터들을 순차적으로 적용시켜 최적의 파라미터를 찾게 해준다. 그러나 순차적으로 파라미터를 적용시키기 때문에 수행 시간이 오래 걸릴 수 있다. GridSearchCV()의 주요 파라미터는 다음과 같다.
- estimator : classifier, regressor, pipeline 등
- param_grid : '파라미터명 + 값' 조합의 딕셔너리
- scoring : 평가 지표
- cv : Cross Validation Fold 횟수
- refit : 최적의 파라미터를 찾은 후 estimator를 해당 파라미터로 재학습할지에 대한 여부(Default=True)
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.metrics import accuracy_score
# 데이터를 로드 및 학습/테스트 데이터 분리
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(iris_data.data, iris_data.target,
test_size=0.2, random_state=121)
dtree = DecisionTreeClassifier()
# 파라미터들을 딕셔너리 형태로 저장
parameters = {'max_depth' : [1,2,3], 'min_samples_split' : [2,3]}
# GridSearchCV 수행
grid_dtree = GridSearchCV(dtree, param_grid = parameters, cv = 3, refit = True, return_train_score = True)
grid_dtree.fit(X_train, y_train)
scores_df = pd.DataFrame(grid_dtree.cv_results_)
scores_df[['params', 'mean_test_score', 'rank_test_score', 'split0_test_score',\
'split1_test_score', 'split2_test_score']]
붓꽃 데이터를 로드하고, 학습/테스트 데이터를 8:2의 비율로 분리했다. 이후 결정 트리 알고리즘에 사용되는 주요 파라미터인 max_depth
와 min_samples_split
에 대한 파라미터 값을 파라미터로 지정한 뒤, GridSearchCV()를 진행했다. GridSearchCV()의 모델은 결정 트리 모델을, Fold 수는 3으로 지정했고, 최적 파라미터에 대한 재학습을 진행했다. 이후 GridSearchCV()의 결과가 저장되는 cv_results_ 딕셔너리를 데이터프레임 형태로 반환했다. 해당 데이터프레임을 확인하면 각 파라미터가 순차적으로 테스트된 것을 확인할 수 있으며, 각 Fold별 테스트 결과의 평균을 반환한다. 최종적으로 max_depth 파라미터의 값은 3, min_samples_split 파라미터의 값은 2일 때 모델의 예측 성능이 가장 높은 것을 확인할 수 있다.
print('GridSearchCV 최적 파라미터:', grid_dtree.best_params_)
print('GridSearchCV 최고 정확도: {0:.4f}'.format(grid_dtree.best_score_))
GridSearchCV()는 최적의 파라미터 및 최고의 정확도를 각각 best_params_와 best_score_ 속성을 통해 확인할 수 있다. 앞서 확인했듯이 max_depth 파라미터 값은 3, min_samples_split 파라미터의 값은 2일 때 0.975로 예측 성능이 가장 높았다.
# GridSearchCV의 refit으로 이미 학습이 된 estimator 반환
estimator = grid_dtree.best_estimator_
# GridSearchCV의 best_estimator_는 이미 최적 하이퍼 파라미터로 학습이 되어 별도 학습이 필요 x
pred = estimator.predict(X_test)
print('테스트 데이터 세트 정확도: {0:.4f}'.format(accuracy_score(y_test,pred)))
앞서 GridSearchCV()의 refit() 파라미터를 True로 지정했기 때문에, 최적 성능을 나타내는 파라미터로 Estimator를 학습해 저장했다. 이미 최적의 하이퍼 파라미터를 통해 학습이 되어있기 때문에 따로 fit()을 수행할 필요는 없으며, 바로 그 전에 미리 떼어놓은 테스트 데이터 세트를 통해 예측을 진행했을 때 약 0.9667의 정확도가 나왔다. 이렇듯, 학습 데이터를 GridSearchCV를 통해 최적 하이퍼 파라미터를 튜닝한 후, 별도의 테스트 데이터 세트를 통해 예측 성능을 확인하는 것이 일반적인 머신러닝 모델 적용 방법이다.
Reference
https://towardsdatascience.com/cross-validation-k-fold-vs-monte-carlo-e54df2fc179b
https://blog.naver.com/sjy5448/222427780700
https://scikit-learn.org/stable/modules/model_evaluation.html
https://m.blog.naver.com/hongjg3229/221793868091