태그 보관물: cross_val_score

다중 평가 지표: cross_validate()

Scikit-Learn 0.19 버전에 추가된 새로운 기능 시리즈 마지막으로 다중 평가 지표에 대해 알아보겠습니다. 다중 평가 지표란 말 그대로 모델을 평가할 때 여러개의 지표를 이용할 수 있다는 뜻입니다. 0.19 버전에 새롭게 추가된 cross_validate() 함수가 이 기능을 제공합니다.

5.3.5절과 유사한 예제를 만들어 보겠습니다. 먼저 숫자 데이터셋을 불러들여 타깃을 9로 하는 이진 분류 문제로 데이터셋을 훈련 세트와 테스트 세트로 나눕니다.

digits = load_digits()
X_train, X_test, y_train, y_test = train_test_split(
    digits.data, digits.target == 9, random_state=42)

지금까지 교차 검증에 사용했던 cross_val_score() 함수를 훈련 세트에 적용해 보겠습니다. 분류 모델은 SVC()를 사용합니다.

사이킷런 0.20 버전에서 SVC ​클래스의 gamma 매개변수 옵션에 auto외에 scale이 추가되었습니다. auto는 1/n_features, 즉 특성 개수의 역수입니다. scale은 1/(n_features * X.std())로 스케일 조정이 되지 않은 특성에서 더 좋은 결과를 만듭니다. 사이킷런 0.22 버전부터는 gamma 매개변수의 기본값이 auto에서 scale로 변경됩니다. 서포트 벡터 머신을 사용하기 전에 특성을 표준화 전처리하면 scale과 auto는 차이가 없습니다. 경고를 피하기 위해 명시적으로 auto 옵션을 지정합니다.

사이킷런 0.22 버전에서 cross_val_score 함수와 GridSearchCV 클래스의 cv 매개변수 기본값이 3에서 5로 바뀔 예정입니다. 0.20 버전에서 cv 매개변수를 지정하지 않는 경우 이에 관한 경고 메세지가 출력됩니다. 경고 메세지를 피하기 위해 cv 매개변수 값을 명시적으로 3으로 지정합니다.

from sklearn.svm import SVC
cross_val_score(SVC(gamma='auto'), X_train, y_train, cv=3)
array([0.90200445, 0.90200445, 0.90200445])

cross_val_score() 함수는 scoring 매개변수에 원하는 평가 지표를 지정할 수 있습니다. 분류 문제일 경우 기본은 정확도를 의미하는 ‘accuracy’입니다. 따라서 다음 코드는 위와 동일한 결과를 출력합니다.

cross_val_score(SVC(gamma='auto'), X_train, y_train, scoring='accuracy' cv=3)

여러개의 평가 지표를 사용하려면 새롭게 추가된 cross_validate() 함수를 사용합니다. cross_val_score()와 마찬가지로 scoring 매개변수에서 평가 지표를 지정할 수 있습니다. 여러개의 평가 지표를 지정하려면 리스트로 만들어 전달하면 됩니다. 이 함수는 디폴트 설정에서 테스트 폴드에 대한 점수 뿐만 아니라 훈련 폴드에 대한 점수도 반환합니다. 향후 버전에서 기본으로 훈련 폴드 점수가 반환되지 않는다는 경고 메세지가 출력되므로 return_train_score 매개변수에서 명시적으로 훈련 폴드의 점수를 받을지 여부를 설정하는 것이 좋습니다.

from sklearn.model_selection import cross_validate
cross_validate(SVC(gamma='auto'), X_train, y_train, 
               scoring=['accuracy', 'roc_auc'], 
               return_train_score=True)
{'fit_time': array([0.07761502, 0.07732582, 0.07719207]),
 'score_time': array([0.06746364, 0.06803942, 0.06800795]),
 'test_accuracy': array([0.90200445, 0.90200445, 0.90200445]),
 'test_roc_auc': array([0.99657688, 0.99814815, 0.99943883]),
 'train_accuracy': array([1., 1., 1.]),
 'train_roc_auc': array([1., 1., 1.])}

이 함수는 각 폴드에서 훈련과 테스트에 걸린 시간을 반환하고 scoring 매개변수에 지정한 평가 지표마다 훈련 점수와 테스트 점수를 반환합니다. 훈련 점수와 테스트 점수를 반환된 딕셔너리에서 추출하려면 ‘train_XXXX’, ‘test_XXXX’와 같은 스타일의 키를 사용하면 됩니다.

사실 0.19 버전부터는 cross_val_score() 함수도 cross_validate()를 사용합니다. 그래서 다음 코드는 cross_val_score() 함수와 동일한 결과를 반환합니다.

cross_validate(SVC(gamma='auto'), X_train, y_train, 
               scoring=['accuracy'], cv=3,
               return_train_score=False)['test_accuracy']

cross_validate() 함수의 scoring 매개변수에 리스트 대신 딕셔너리로 평가 지표를 전달할 수 있습니다. 딕셔너리의 키는 임의의 문자열이 가능합니다. 이렇게 하면 결과 딕셔너리의 키 이름을 간략하게 나타낼 수 있습니다. 다음과 같이 씁니다.

cross_validate(SVC(gamma='auto'), X_train, y_train, 
               scoring={'acc':'accuracy', 'ra':'roc_auc'}, 
               return_train_score=False, cv=3)
{'fit_time': array([0.07760668, 0.07740569, 0.07696486]),
 'score_time': array([0.06791329, 0.06786489, 0.06783247]),
 'test_acc': array([0.90200445, 0.90200445, 0.90200445]),
 'test_ra': array([0.99657688, 0.99814815, 0.99943883])}

평가 지표 mean_squared_error 같이 긴 이름을 자주 참조해야 한다면 이름을 간소하게 나타내는 것이 좋을 것 같습니다.

다중 평가 지표는 cross_validate()는 물론 그리드서치에서도 사용할 수 있습니다. GridSearchCV()의 scoring 매개변수도 마찬가지로 리스트 또는 딕셔너리로 설정할 수 있습니다. 다만 평가 지표를 리스트나 딕셔너리로 설정하려면 refit 매개변수에서 어떤 평가 지표로 선택한 최종 모델을 학습할 것인지 지정해야 합니다.

param_grid = {'gamma': [0.0001, 0.01, 0.1, 1, 10]}
grid = GridSearchCV(SVC(), param_grid=param_grid, 
                    scoring=['accuracy'], refit='accuracy',
                    return_train_score=True, cv=3)
grid.fit(X_train, y_train)

최적의 파라미터와 교차 검증 점수를 출력해 보겠습니다.

grid.best_params_
{'gamma': 0.0001}
grid.best_score_
0.9651076466221232

gamma가 0.0001이 선택되었습니다. 전체 교차 검증 결과를 출력해 보기 위해서 판다스의 DataFrame으로 만든 다음 넘파이로 행과 열을 바꾸겠습니다(컬럼 제목을 좌측에 놓기 위해서입니다).

np.transpose(pd.DataFrame(grid.cv_results_))

스크린샷 2018-03-13 오후 4.34.44.png

확실히 gamma가 0.0001일 때 mean_test_accuracy가 가장 높습니다. 이제 roc_auc를 추가해 그리드서치를 실행해 보겠습니다.

grid = GridSearchCV(SVC(), param_grid=param_grid, 
                    scoring={'acc':'accuracy', 'ra':'roc_auc'}, refit='ra',
                    return_train_score=True, cv=3)
grid.fit(X_train, y_train)

최적의 파라미터와 교차 검증 점수를 출력합니다.

grid.best_params_
{'gamma': 0.01}
grid.best_score_
0.9983352038907595

앞에서와 같이 교차 검증 결과를 출력해 보겠습니다. accuracy와 roc_auc에 대해 모두 점수가 출력되므로 꽤 긴 출력이 됩니다. 위에서처럼 gamma가 0.0001일 때는 mean_test_acc가 가장 높지만 gamma가 0.01일 때는 mean_test_ra가 제일 높습니다. refit 매개변수를 ‘ra’로 주었기 때문에 최적 파라미터와 최종 모델은 gamma가 0.01입니다.

스크린샷 2018-03-13 오후 4.37.26

grid.best_estimator_
SVC(C=1.0, cache_size=200, class_weight=None, coef0=0.0,
  decision_function_shape='ovr', degree=3, gamma=0.01, kernel='rbf',
  max_iter=-1, probability=False, random_state=None, shrinking=True,
  tol=0.001, verbose=False)

이 글의 샘플 코드는 ‘파이썬 라이브러리를 활용한 머신러닝‘ 깃허브(https://github.com/rickiepark/introduction_to_ml_with_python/blob/master/cross_validate.ipynb)에서 확인할 수 있습니다.

반복 교차 검증

Scikit-Learn 0.19 버전에서 추가된 기능 중 이번에는 모델 선택 패키지 하위에 있는 RepeatedKFold와 RepeatedStratifiedKFold를 알아 보겠습니다. 이 두 클래스는 각각 KFold와 StratifiedKFold를 감싸고 있는 래퍼 클래스처럼 보아도 무방합니다. 데이터셋의 크기가 크지 않을 경우, LOOCV를 사용하기에는 결과가 불안정해서 교차 검증을 반복적으로 여러번 수행하곤 합니다. 0.19 버전에서는 이런 반복적인 교차 검증을 수행할 수 있는 두 개의 클래스를 추가하였습니다.

RepeatedKFold와 RepeatedStratifiedKFold를 사용하는 방법은 다른 분할 클래스들과 매우 비슷합니다. 이 클래스들은 폴드를 한번만 나누는 것이 아니고 지정한 횟수(n_repeats)만큼 반복해서 나누게 되고 교차 검증 점수도 반복한 만큼 얻을 수 있습니다. 이때 기본적으로 랜덤하게 나누므로 분할기에 자동으로 Shuffle=True 옵션이 적용됩니다. n_repeats의 기본값은 10입니다.

붓꽃 데이터셋으로 이 클래스들을 사용해서 교차 검증을 해 보겠습니다.

사이킷런 0.19 버전 이하에서는 LinearSVCliblinear를 사용하는 LogisticRegressionverbose 매개변수가 0이 아니고 max_iter 반복 안에 수렴하지 않을 경우 반복 횟수를 증가하라는 경고 메세지가 나옵니다. 사이킷런 0.20 버전부터는 verbose 매개변수에 상관없이 max_iter 반복 안에 수렴하지 않을 경우 반복 횟수 증가 경고가 나옵니다. 경고 메세지를 피하기 위해 max_iter 매개변수의 기본값을 증가시킵니다.

향후 사이킷런 0.22 버전에서 LogisticRegression 클래스의 solver 매개변수 기본값이 liblinear에서 lbfgs로 변경될 예정입니다. 사이킷런 0.20 버전에서 solver 매개변수를 지정하지 않는 경우 이에 대한 경고 메세지를 출력합니다. 경고 메세지를 피하고 향후 변경될 내용을 적용하기 위하여 solver 매개변수를 lbfgs로 설정합니다.

사이킷런 0.20 버전에서 LogisticRegressionmulti_class 매개변수 옵션에 auto가 추가되었습니다. auto로 설정하면 이진 분류이거나 solverliblinear일 경우에는 ovr을 선택하고 그 외에는 multinomial을 선택합니다. 사이킷런 0.22 버전부터는 multi_class 매개변수의 기본값이 ovr에서 auto​로 변경됩니다. 경고 메세지를 피하기 위해 명시적으로 ovr 옵션을 지정합니다.

from sklearn.datasets import load_iris
from sklearn.model_selection import cross_val_score, KFold, StratifiedKFold
from sklearn.linear_model import LogisticRegression

iris = load_iris()
logreg = LogisticRegression(solver='liblinear', multi_class='auto', max_iter=1000)

먼저 가장 기본이 되는 KFold를 사용한 교차 검증입니다.

kfold = KFold(n_splits=5)
scores = cross_val_score(logreg, iris.data, iris.target, cv=kfold)
scores, scores.mean()
(array([ 1.        ,  0.93333333,  0.43333333,  0.96666667,  0.43333333]),
 0.7533333333333333)

iris 데이터셋은 타깃값의 순서대로 훈련 데이터가 정렬되어 있기 때문에 기본 KFold에서는 교차 검증 점수가 매우 들쭉 날쭉 합니다.

이번엔 RepeatedKFold를 사용해 보겠습니다. 사용법은 KFold와 같지만 반복 횟수(n_repeats)를 설정해 주고 결과를 일정하게 고정하기 위해 random_state를 지정했습니다.

from sklearn.model_selection import RepeatedKFold
rkfold = RepeatedKFold(n_splits=5, n_repeats=5, random_state=42)
scores = cross_val_score(logreg, iris.data, iris.target, cv=rkfold)
scores, scores.mean()
(array([ 1.        ,  0.93333333,  0.9       ,  0.96666667,  0.96666667,
         0.96666667,  0.93333333,  1.        ,  1.        ,  0.83333333,
         0.93333333,  0.9       ,  0.96666667,  0.9       ,  0.93333333,
         0.96666667,  1.        ,  0.96666667,  0.93333333,  0.93333333,
         0.96666667,  0.9       ,  1.        ,  0.93333333,  0.93333333]),
 0.94666666666666677)

교차 검증 점수가 반복 횟수만큼 5개 행으로 출력되었습니다. 이전 보다 훨씬 고른 점수 분포를 보여 주며 전체 점수의 평균은 0.95 정도입니다. 교차 검증 점수의 범위를 박스플롯으로 가늠해 볼 수 있습니다.

boxplot1

다음엔 계층별 교차 검증인 StratifiedKFold를 적용해 보겠습니다.

skfold = StratifiedKFold(n_splits=5)
scores = cross_val_score(logreg, iris.data, iris.target, cv=skfold)
scores, scores.mean()
(array([ 1.        ,  0.96666667,  0.93333333,  0.9       ,  1.        ]),
 0.96000000000000019)

StratifiedKFold는 타깃값 클래스 비율을 고려하므로 전체적으로 고르게 폴드를 나누어서 교차 검증 점수가 안정되었습니다. 그럼 RepeatedStratifiedKFold를 사용해서 비교해 보겠습니다.

from sklearn.model_selection import RepeatedStratifiedKFold
rskfold = RepeatedStratifiedKFold(n_splits=5, n_repeats=5, random_state=42)
scores = cross_val_score(logreg, iris.data, iris.target, cv=rskfold)
scores, scores.mean()
(array([ 0.96666667,  0.96666667,  0.96666667,  0.93333333,  0.96666667,
         0.86666667,  0.96666667,  0.96666667,  0.93333333,  0.96666667,
         1.        ,  1.        ,  0.93333333,  0.93333333,  0.93333333,
         1.        ,  0.96666667,  0.96666667,  0.9       ,  0.96666667,
         0.96666667,  0.96666667,  0.96666667,  0.9       ,  0.96666667]),
 0.95466666666666655)

평균 교차 검증 점수는 0.955 정도로 RepeatedKFold와 StratifiedKFold의 중간에 해당하는 값입니다. 박스 플롯 끼리 비교해 보면 거의 비슷하지만 RepeatedStratifiedKFold가 약간 더 집중되어 있음을 알 수 있습니다.

boxplot2.png

이 두 클래스가 더욱 유용해질 때는 그리드서치에 사용할 때 입니다. cross_val_score와 마찬가지로 GridSearchCV의 분할은 회귀일 때는 KFold, 분류일 때는 StratifiedKFold가 기본입니다. 자원이 허락된다면 RepeatedKFold와 RepeatedStratifiedKFold로 바꾸지 않을 이유가 없습니다.

앞에서는 책의 예제와 비슷한 모양을 갖추기 위해 데이터셋을 나누지 않았습니다. 여기서는 훈련 데이터와 테스트 데이터로 나누고 RepeatedStratifiedKFold를 그리드서치에 적용해 보겠습니다.

사이킷런 0.22 버전부터는 GridSearchCV와 RandomizedSearchCV의 iid 매개변수 기본값이 True에서 False로 바뀝니다. 0.24 버전에서는 이 매개변수가 아예 삭제될 예정입니다. iid 매개변수가 True이면 독립 동일 분포라고 가정하고 테스트 세트의 샘플 수로 폴드의 점수를 가중 평균합니다. False로 지정하면 단순한 폴드 점수의 평균입니다. False일 때 기본 교차 검증과 동작 방식이 같습니다. 사이킷런 0.20 버전에서 iid 매개변수가 기본값일 때 가중 평균과 단순 평균의 차이가 10^-4 이상이면 경고 메세지가 발생합니다. 경고 메세지를 피하고 향후 변경될 방식을 사용하기 위해 GridSearchCV의 iid 매개변수를 False로 설정합니다

from sklearn.model_selection import GridSearchCV, train_test_split
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target, random_state=42)
param_grid = {'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000]}
grid = GridSearchCV(logreg, param_grid, cv=rskfold, return_train_score=True, iid=False)
grid.fit(X_train, y_train)
grid.score(X_test, y_test), grid.best_params_, grid.best_score_

테스트 점수는 완벽하고 규제 매개 변수 C의 최적값은 100이 선택되었습니다. 교차 검증 최고 점수는 0.96 입니다.

(1.0, {'C': 100}, 0.96250000000000002)

cv_results_에 있는 교차 검증 결과를 출력해 보면 훈련과 테스트에 대해 모두 5번 반복해서 각각 25개의 점수가 있는 것을 확인할 수 있습니다.

for k in grid.cv_results_:
    if 'split' in k:
    print(k, grid.cv_results_[k])

내용이 장황하므로 일부만 추렸습니다. 전체 출력 내용은 깃허브를 참고해 주세요.

split21_test_score [ 0.34782609  0.65217391  0.82608696  0.91304348  0.86956522  0.91304348
  0.86956522]
split6_test_score [ 0.34782609  0.65217391  0.7826087   0.91304348  0.95652174  0.95652174
  0.95652174]
split24_train_score [ 0.34065934  0.64835165  0.83516484  0.94505495  0.96703297  0.96703297
  0.96703297]
split3_train_score [ 0.34444444  0.65555556  0.82222222  0.95555556  0.96666667  0.96666667
  0.96666667]
...
split16_test_score [ 0.34782609  0.65217391  0.91304348  0.95652174  0.95652174  1.
  0.95652174]
split23_test_score [ 0.31818182  0.63636364  0.77272727  0.95454545  0.95454545  0.95454545
  0.95454545]
split1_test_score [ 0.34782609  0.65217391  0.86956522  0.82608696  0.91304348  0.95652174
  1.        ]
split18_train_score [ 0.34444444  0.65555556  0.82222222  0.95555556  0.96666667  0.96666667
  0.96666667]

폴드 분할을 여러번 반복하여 그리드서치의 결과를 조금 더 안정적으로 만들 수 있을 것 같습니다. 반복 횟수가 클수록 훈련 시간은 느려지게 되므로 파이프라인 캐싱을 활용하고 가용한 자원내에서 반복을 수행하는 것이 좋을 것 같습니다.

이 샘플 코드는 ‘파이썬 라이브러리를 활용한 머신러닝‘ 깃허브(https://github.com/rickiepark/introduction_to_ml_with_python/blob/master/RepeatedKFold.ipynb)에서 확인할 수 있습니다.