카테고리 보관물: scikit-learn

“핸즈온 머신러닝” 주피터 노트북 업데이트 안내

핸즈온 머신러닝“의 3장, 11장 주피터 노트북이 조금 업데이트되었습니다.

  • 3장: 연습문제 3번의 답에 서포트 벡터 머신과 랜덤 포레스트의 결과를 박스 플롯(boxplot)으로 비교하는 셀이 추가되었습니다. 다음 스샷을 참고해 주세요.

  • 11장: SELU 활성화 함수에 대한 설명이 보강되었습니다. 다음 스샷을 참고해 주세요.


감사합니다! 🙂

[핸즈온 머신러닝] 업데이트 안내해 드립니다.

핸즈온 머신러닝” 도서의 수정 사항에 대해 안내해 드립니다. 최근 원서 깃허브에 여러가지 내용이 추가 되어서 이를 번역서 깃허브에 반영하였습니다. 본문 내용에 영향이 있는 것은 에러타로 등록하였습니다. 도서를 구매하셨다면 꼭 확인 부탁드립니다. 주요한 변경 사항은 다음과 같습니다.

  • 7장의 연습문제 8, 9번의 답이 추가되었습니다.
  • 16장의 연습문제 8번의 답이 추가되었습니다.
  • 2장 3장에 범주형 변수의 인코딩을 위해 OrdinalEncoder와 OneHotEncoder를 사용하는 예가 추가되었습니다(사이킷런 0.20에 추가될 예정인 CategoricalEncoder가 두 개의 클래스로 리팩토링되었습니다). 그리고 두 클래스를 future_encoders.py 로 분리했습니다.
  • 2장에 신뢰 구간을 구하는 예가 추가되었습니다.
  • 7장에 XGBoost 예가 추가되었습니다.
  • 10장에 tf.contrib.learn 외에 tf.estimator를 사용한 예가 추가되었습니다.
  • 12장에 tf.data 를 사용한 예가 추가되었습니다.
  • 16장에 넘파이 배열을 사용한 재현 메모리 구현 예가 추가되었습니다.
  • tf.examples.tutorials.mnist 가 deprecated 될 예정이므로 Keras에 포함된 MNIST 함수를 사용하도록 바꾸었습니다.
  • 텐서보드를 주피터 노트북에 표현하기 위해 추가했던 코드를 tensorflow_graph_in_jupyter.py 파일로 분리했습니다.
  • 9장에 있던 자동 미분 예를 extra_autodiff.ipynb로 분리했습니다. 이 노트북의 번역을 완료하여 업데이트했습니다.

아직 번역하지 못한 extra 노트북이 여러개 남아 있습니다. 꾸준히 작업하고 있으니 조금만 기다려 주세요. 감사합니다!

‘핸즈온 머신러닝’이 출간되었습니다.

b9267655530_lHands-On Machine Learning with Scikit-Learn & TensorFlow의 번역서 ‘핸즈온 머신러닝‘이 출간되었습니다!

그 동안 이 작업을 하면서 많은 것을 배웠습니다. 모쪼록 다른 누군가에게도 도움이 된다면 다행입니다.

원래 번역서 제목이 ‘사이킷런과 텐서플로를 활용한 머신러닝, 딥러닝 실무’ 정도로 예상되었는데 출간 직전에 ‘핸즈온 머신러닝’으로 변경되었습니다. 개인적으로는 단순하고 평소에도 부르는 이름이라 만족입니다. 🙂 혹시 이 책에 관해 궁금한 점 있으면 언제든지 댓글이나 메일 주세요.

마지막으로 책을 만드는 데 도움을 주신 많은 분들께 다시 한번 감사드립니다.

(저자 오렐리앙도 아주 기뻐하네요. ㅎ)

결정 트리와 불순도에 대한 궁금증

이 글은 세바스찬 라쉬카의 머신러닝 FAQ 글(Why are implementations of decision tree algorithms usually binary and what are the advantages of the different impurity metrics?)을 번역한 것입니다.

결정 트리 알고리즘은 왜 이진 트리로 구현되어 있을까요? 각 불순도 지표들의 장점은 무엇인가요?

현실적인 이유로(조합의 폭발적인 증가combinatorial explosion) 대부분의 라이브러리들은 결정 트리를 이진 트리로 구현합니다. 좋은 점은 이진 트리가 NP-완전 문제NP-complete라는 것입니다(Hyafil, Laurent, and Ronald L. Rivest. “Constructing optimal binary decision trees is NP-complete.” Information Processing Letters 5.1 (1976): 15-17.).

(CARTClassification And Regression Tree 알고리즘의) 목적 함수는 각 분할에서 정보 이득information gain(IG)을 최대화하는 것입니다:

IG(D_p, f) = I(D_p) - \sum_{j=1}^{m}\dfrac{N_j}{N}I(D_j)

는 분할에 사용되는 특성이고 D와 D는 각각 부모 노드와 번째 자식 노드의 데이터셋입니다. 는 불순도 지표입니다. N 은 샘플의 전체 개수이고 Njj 번째 자식 노드의 샘플 개수입니다. 이제 분류에서 가장 널리 사용되는 (CART 알고리즘의) 분할 기준을 살펴 보겠습니다. 간단하게 이진 분할로 식을 표현하지만 당연히 다중 분할로 일반화될 수 있습니다. 이진 분할에 대한 IG 는 다음과 같이 계산합니다:

IG(D_p, a) = I(D_p) - \dfrac{N_{left}}{N}I(D_{left}) - \dfrac{N_{right}}{N}I(D_{right})

이진 결정 트리에서 가장 널리 사용되는 불순도 지표 또는 분할 기준은 지니 불순도Gini impurity(IG)와 엔트로피Entropy(IH)와 분류 오차Classification Error(IE)입니다. 엔트로피의 정의부터 살펴보죠. 다음과 같습니다.

I_H(t) = - \sum_{i=1}^c p(i|t)\,log_2\,p(i|t)

클래스에 샘플이 하나라도 있다면

p(i|t) \ne 0

이고 p(i|t) 는 특정 노드 t 에서 클래스 c 에 속한 샘플의 비율입니다. 그러므로 한 노드의 샘플이 모두 동일한 클래스에 속한다면 엔트로피는 0입니다. 클래스 별로 균등하게 분포되어 있다면 당연하게 엔트로피는 최대가 됩니다. 지니 불순도는 잘 못 분류될 확률을 최소화시키려는 기준으로 생각할 수 있습니다.

I_G(t) = \sum_{i=1}^c p(i|t)(1-p(i|t)) = 1 - \sum_{i=1}^c p(i|t)^2

엔트로피와 비슷하게 지니 불순도는 클래스가 균일하게 섞여 있을 때 최대가 됩니다. 그러나 실제로 지니 불순도와 엔트로피는 매우 비슷한 결과를 만들며 불순도 기준을 사용하여 트리를 평가하는데 시간을 들이는 것보다 가지치기pruning 방식을 바꿔보는 것이 더 낫습니다. 또 다른 불순도 지표는 분류 오차입니다.

I_E = 1 - \text{max}\{ p(i|t) \}

가지치기에는 좋은 지표지만 노드의 클래스 확률 변화에 덜 민감하기 때문에 결정 트리를 구성하는데는 적합하지 않습니다.

overview-plot

분류 오차가 클래스 확률 변화에 왜 덜 민감한지 보기 위해 다음 그림에서 두 개의 분할 사례를 살펴 보겠습니다.

split

클래스 1에 40개의 샘플, 클래스 2에 40개의 샘플을 가진 부모 노드의 데이터셋 Dp 를 각각 두 개의 데이터셋 Dleft 와 Dright 로 나누었습니다. 분류 오차를 분할 기준으로 사용했을 때 정보 이득은 AB 의 경우 모두 같습니다(IGE = 0.25):

I_E(D_p) = 1 - \text{max}(0.5 + 0.5) = 0.5

A:

I_E(D_{left}) = 1 - \text{max}(0.75, 0.25) = 0.25

I_E(D_{right}) = 1 - \text{max}(0.25, 0.75) = 0.25

IG_E = 0.5 - \dfrac{40}{80}\times0.25-\dfrac{40}{80}\times0.25 = 0.25

B:

I_E(D_{left}) = 1 - \text{max}(\dfrac{20}{60},\dfrac{40}{60}) = \dfrac{1}{3}

I_E(D_{right}) = 1 - \text{max}(1, 0) = 0

IG_E = 0.5 - \dfrac{60}{80}\times\dfrac{1}{3}-\dfrac{20}{80}\times0 = 0.25

하지만 지니 불순도 기준의 IG는 A(0.125) 보다 B(0.1666) 가 더 순수하기 때문에 높습니다.

I_G(D_p) = 1 - (0.5^2 + 0.5^2) = 0.5

A:

I_G(D_{left}) = 1 - \left( \left(\dfrac{3}{4}\right)^2 + \left(\dfrac{1}{4}\right)^2 \right) = \dfrac{3}{8} = 0.375

I_G(D_{right}) = 1 - \left( \left(\dfrac{1}{4}\right)^2 + \left(\dfrac{3}{4}\right)^2 \right) = \dfrac{3}{8} = 0.375

IG_G = 0.5 - \dfrac{40}{80}\times0.375 - \dfrac{40}{80}\times0.375 = 0.125

B:

I_G(D_{left}) = 1 - \left( \left(\dfrac{2}{6}\right)^2 + \left(\dfrac{4}{6}\right)^2 \right) = \dfrac{4}{9}

I_G(D_{right}) = 1 - \left( \left(\dfrac{2}{2}\right)^2 + \left(\dfrac{0}{2}\right)^2 \right) = 1 - 1 = 0

IG_G = 0.5 - \dfrac{60}{80}\times\dfrac{4}{9} - \dfrac{40}{80}\times0 = 0.1666

비슷하게 엔트로피 기준은 A(0.19) 보다 B(0.31)를 선호합니다.

I_H(D_p) = - (0.5\,log_2\,(0.5) + 0.5\,log_2(0.5)) = 1

A:

I_H(D_{left}) = - \left( \dfrac{3}{4}\,log_2\left(\dfrac{3}{4}\right) + \dfrac{1}{4}\,log_2\left(\dfrac{1}{4}\right) \right) = 0.81

I_H(D_{right}) = - \left( \dfrac{1}{4}\,log_2\left(\dfrac{1}{4}\right) + \dfrac{3}{4}\,log_2\left(\dfrac{3}{4}\right) \right) = 0.81

IG_H = 1 - \dfrac{40}{80}\times0.81 - \dfrac{40}{80}\times0.81 = 0.19

B:

I_H(D_{left}) = - \left( \dfrac{2}{6}\,log_2\left(\dfrac{2}{6}\right) + \dfrac{4}{6}\,log_2\left(\dfrac{4}{6}\right) \right) = 0.92

I_H(D_{right}) = - \left( \dfrac{2}{2}\,log_2\left(\dfrac{2}{2}\right) + \dfrac{0}{2}\,log_2\left(\dfrac{0}{2}\right) \right) = 0

IG_G = 1 - \dfrac{60}{80}\times0.92 - \dfrac{40}{80}\times0 = 0.31

지니 불순도와 엔트로피에 대해 덧붙이자면 위에 언급한 것처럼 만들어진 트리는 실제 매우 비슷합니다. 지니 불순도의 장점은 로그를 계산할 필요가 없어서 구현 성능이 조금 더 낫다는 것입니다.

옮긴이: 위 그림을 보면 엔트로피보다 지니 불순도 방식이 불순도 값을 줄이기 위해 더 클래스 확률을 낮추어야 함을 알 수 있습니다. 즉 엔트로피 방식이 조금 더 균형잡힌 트리를 만들 가능성이 높습니다. scikit-learn의 DecisionTreeClassifier와 DecisionTreeRegressor 및 이들을 사용하는 랜덤 포레스트와 그래디언트 부스팅에서 특성 중요도 속성은(feature_importances_) 트리가 분할에 사용한 특성별로 발생되는 모든 정보 이득을 더하고 전체 특성 중요도의 합이 1이 되도록 정규화한 것입니다.

다중 평가 지표: 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()를 사용합니다.

from sklearn.svm import SVC
cross_val_score(SVC(), X_train, y_train)
array([0.90200445, 0.90200445, 0.90200445])

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

cross_val_score(SVC(), X_train, y_train, scoring='accuracy')

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

from sklearn.model_selection import cross_validate
cross_validate(SVC(), 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(), X_train, y_train, 
               scoring=['accuracy'], 
               return_train_score=False)['test_accuracy']

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

cross_validate(SVC(), X_train, y_train, 
               scoring={'acc':'accuracy', 'ra':'roc_auc'}, 
               return_train_score=False)
{'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)
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)
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)에서 확인할 수 있습니다.

MLPClassifier의 다중 레이블 분류

이 포스트는 “나이브”님이 메일로 문의 주신 내용을 바탕으로 작성되었습니다. “파이썬 라이브러리를 활용한 머신러닝” 2.3.8절의 신경망에 소개된 MLPClassifier 모델이 다중 분류Multi-class Classification, 다중 레이블 분류Multi-label Classification가 가능한지에 대해 문의를 주셨습니다. 책을 보니 공교롭게도 예제가 모두 이진 분류로 나와 있네요. 🙂

MLPClassifier는 다중 분류, 다중 레이블 분류를 지원합니다. 일반적으로 신경망에서 다중 분류를 구현하려면 출력층의 뉴런을 2개 이상 놓아야 합니다. MLPClassifier 클래스는 겉으로 드러나 있지는 않지만 타깃 배열 y의 차원을 보고 출력 뉴런의 개수를 자동으로 결정합니다. 간단한 예를 만들어 확인해 보겠습니다. 먼저 책에서 사용한 moons 데이터셋을 억지로 다중 분류를 위한 데이터셋으로 변경해 보겠습니다. 즉1차원 배열인 타깃값 y를 (100, 2) 2차원 배열로 만들어 사용합니다.

print(Y_train[:10])
array([[ 0.,  1.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 0.,  1.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 0.,  1.],
        [ 1.,  0.],
        [ 1.,  0.],
        [ 0.,  1.]])

그런 다음 책의 예제와 동일한 옵션으로 신경망을 학습시켜 보겠습니다. 타깃을 2차원 배열로 변형시켰기 때문에 y_train이 아니라 Y_train 처럼 대문자를 사용했습니다.

mlp_multi = MLPClassifier(solver='lbfgs', random_state=0).fit(X_train, Y_train)

mlp_multi

이 그래프를 아래 이진 분류의 경우와 비교해 보면 결정 경계가 조금 다른 것을 확인할 수 있습니다.

mlp_binary

MLPClassifier의 기본값은 100개의 뉴런을 가진 은닉층 하나를 사용합니다. 그림 2-47과 같은 신경망 구조를 상상해 보면, 마지막 출력층과 은닉층 사이의 연결(가중치)이 출력층의 뉴런의 개수가 하나일때와 두 개일때 달라질 것이라는 것을 눈치챌 수 있습니다. 이런 차이 때문에 결정 경계가 조금 달라졌습니다. 하지만 우리가 사용한 샘플 데이터는 그렇게 조밀하지 않으므로 변화된 결정 경계에 영향을 받지 않아 테스트 점수가 동일합니다.

mlp_multi.score(X_test, Y_test)
0.88

이번에는 다중 레이블 분류를 위해 ClassifierChain 예제에서 사용했던 Yeast 데이터셋을 이용해 보겠습니다. 이 데이터의 타깃값은 확실히 다중 레이블입니다.

Y_train[:10]
array([[ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,
          0.],
        [ 0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 0.,  1.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  1.,  1.,  1.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 0.,  1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,
          0.],
        [ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,  0.,  0.,  0.,  0.,
          0.],
        [ 1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.],
        [ 1.,  1.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  1.,  1.,
          0.]])

입력의 특성이 103개이므로 뉴런과 은닉층의 개수를 조금 늘려 보겠습니다(300, 100). 그리고 기본 solver인 Adam 알고리즘을 사용하므로 최대 반복횟수(max_iter)를 기본값인 200에서 크게 증가시켜 주었습니다.

mlp_multilabel = MLPClassifier(hidden_layer_sizes=(300,100), max_iter=10000, 
                               random_state=42).fit(X_train, Y_train)
mlp_multilabel.score(X_test, Y_test)
0.16115702479338842

분류 모델의 score 메서드는 다중 레이블 분류를 지원하지 않습니다. 즉 행의 전체 원소가 모두 정확히 맞았을 때를 카운트합니다. ClassifierChain에서 처럼 자카드 유사도를 사용할 수 있지만 여기서는 하나 원소라도 맞았을 때를 수동으로 확인해 보겠습니다.

Y_pred = mlp_multilabel.predict(X_test)
np.sum(np.sum(Y_test.astype(int) & Y_pred, axis=1) > 0)/Y_test.shape[0]
0.85330578512396693

이 코드는 예측(Y_pred)을 만들어 테스트 데이터(Y_test)의 각 원소에 대해 논리 곱(AND) 연산을 합니다. 즉 두 행렬의 같은 위치의 원소가 모두 True일 때만 True가 됩니다. 그리고 난 후 True의 개수가 0 보다 큰 행의 개수를 카운트했습니다. 테스트 세트의 85%는 최소한 하나의 레이블 이상 맞았네요. 🙂

 

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

분류기 체인: ClassifierChain

Scikit-Learn 0.19 버전에 추가된 기능으로 이번에 소개할 모델은 ClassifierChain입니다.

다중 레이블(Multi-Label) 문제를 직접 다룰 수 있는 모델도 있지만(가령, 랜덤 포레스트), 이진 분류기를 사용하여 다중 레이블 분류기를 구현하는 간단한 방법은 One-Vs-All 방식을 사용하는 것입니다. 이 예에서 사용할 데이터는 Yeast 데이터셋으로 2,417개의 샘플과 103개의 특성, 14개의 타깃 레이블(즉, 다중 레이블)을 가지고 있습니다.

import numpy as np
from sklearn.datasets import fetch_mldata

yeast = fetch_mldata('yeast')

X = yeast['data']
Y = yeast['target']

Y = Y.transpose().toarray()

X와 행의 차원을 맞추기 위해 희소 행렬 Y를 전치하고 밀집 행렬로 변환합니다. 그러면 X는 (2417×103)인 행렬이고 Y는 (2417×14)인 행렬이 됩니다. 그 다음 훈련 데이터와 테스트 데이터로 나눕니다.

from sklearn.model_selection import train_test_split
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.2, 
                                                    random_state=42)

먼저 로지스틱 회귀을 사용해 OneVsRestClassifier 모델을 훈련시켜 보겠습니다.

from sklearn.linear_model import LogisticRegression
from sklearn.multiclass import OneVsRestClassifier

ovr = OneVsRestClassifier(LogisticRegression())
ovr.fit(X_train, Y_train)
pred_ovr = ovr.predict(X_test)

OneVsRestClassifier는 타깃 레이블의 개수 만큼 로지스틱 회귀 모델을 만들어 각 레이블을 타깃으로 하는 분류기를 학습시킵니다. 따라서 pred_ovr의 차원은 (2417×14)이 됩니다. 다중 레이블 분류에 사용되는 대표적인 측정 방법은 자카드 유사도(jaccard_similarity_score)입니다. 이 함수에 진짜 타깃 레이블과 예측 결과를 전달하면 0~1 사이의 값을 반환합니다. 1에 가까울수록 두 값이 비슷한 것입니다.

from sklearn.metrics import jaccard_similarity_score
ovr_score = jaccard_similarity_score(Y_test, pred_ovr)
ovr_score
0.50828086055358779

이번에는 ClassifierChain을 사용해 보겠습니다. 이 (메타) 분류기는 OneVsRestClassifier처럼 레이블마다 하나의 모델을 학습시키지만 타깃 레이블을 특성으로 사용합니다. 다중 레이블의 문제에서 종종 타깃은 서로에게 상관관계를 가집니다. 이런 경우 분류기 체인 방식을 사용하면 One-Vs-All 전략 보다 더 좋은 결과를 얻을 수 있습니다.

ClassifierChain은 먼저 첫 번째 레이블에 대해 이진 분류기를 학습시킵니다. 그 다음 두 번째 분류기를 학습시킬 때 첫 번째 레이블을 특성으로 포함시킵니다. 그 다음엔 첫 번째와 두 번째 레이블을 특성으로 포함시켜 세 번째 분류기를 학습시키는 식입니다. 이렇다 보니 레이블의 순서에 따라 결과가 달라질 수 있습니다. ClassifierChain의 order 매개변수 기본값은 Y 타깃값을 순서대로 사용하므로 이보다는 ‘random’으로 주는 것이 일반적입니다.

from sklearn.multioutput import ClassifierChain

cc = ClassifierChain(LogisticRegression(), order='random', random_state=42)
cc.fit(X_train, Y_train)
pred_cc = cc.predict(X_test)
cc_score = jaccard_similarity_score(Y_test, pred_cc)
cc_score
0.52203774760592925

예측을 만들 때는 체인 순서대로 첫 번째 분류기의 예측을 만들어 다음 분류기에서 특성으로 사용하고, 두 번째 분류기의 예측을 만들어 첫 번째 예측 결과와 함께 세 번째 분류기의 특성으로 사용하는 식으로 진행됩니다.

이번에는 random_state를 바꾸어 가면서 여러개의 ClassifierChain을 만들어 order=’random’일 때 미치는 영향을 살펴 보겠습니다. 다음 코드는 리스트 내포(list comprehension)를 사용하여 random_state 값을 바꾸면서 10개의 ClassifierChain을 만들고 자카드 점수를 계산하지만 기본적으로 위의 코드와 동일합니다.

chains = [ClassifierChain(LogisticRegression(), order='random', random_state=42+i)
          for i in range(10)]
for chain in chains:
    chain.fit(X_train, Y_train)

pred_chains = np.array([chain.predict(X_test) for chain in chains])
chain_scores = [jaccard_similarity_score(Y_test, pred_chain)
                    for pred_chain in pred_chains]
[0.52203774760592925,
 0.50759953430407978,
 0.54071149809786168,
 0.51879427390791022,
 0.51900088547815826,
 0.51148445792040831,
 0.52014626787354057,
 0.50362964056145876,
 0.50333366128820667,
 0.47443673750491933]

확실히 random_state에 따라 타깃값을 사용하는 순서가 바뀌기 때문에 성능이 들쭉 날쭉합니다. 다음 그래프에서 첫 번째 막대 그래프는 ovr_score이고 그 다음 10개의 그래프는 chain_scores입니다.

jaccard_bar1

어떤 순서로 레이블을 사용할지 사전에 알 수 없으므로 ClassifierChain의 앙상블을 만들어 사용하는 것이 좋습니다. 앙상블을 만들 때는 각 분류기 체인의 값을 평균내어 사용합니다. 분류기 체인의 예측 결과를 앙상블하는 것보다 예측 확률을 앙상블하는 것이 조금 더 안정적인 결과를 제공할 것으로 기대할 수 있습니다. 로지스틱 회귀는 predict_proba() 메서드를 제공하므로 간단하게 체인의 확률값을 얻을 수 있습니다. 앙상블의 평균을 낸 후에 이를 예측값으로 바꾸어 Y_test와 유사도를 측정하려면 앙상블의 평균 확률이 0.5 보다 큰지를 판단하면 됩니다.

proba_chains = np.array([chain.predict_proba(X_test) for chain in chains])
proba_ensemble = proba_chains.mean(axis=0)
ensemble_score = jaccard_similarity_score(Y_test, proba_ensemble >= 0.5)
0.51958792470156101

앙상블의 결과는 0.5196로 첫 번째 체인보다는 점수가 낮지만 가장 나쁜 체인의 점수에 비하면 비교적 높은 점수를 얻었습니다. 또 OneVsRestClassifier 보다 높은 점수를 얻었습니다. 다음 그래프의 첫 번째 막대 그래프는 ovr_score이고 그 다음 10개의 그래프는 chain_scores이며 마지막 막대 그래프가 앙상블의 결과입니다.

jaccard_bar2

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

QuantileTransformer

Scikit-Learn 0.19 버전에 추가된 기능 중 오늘은 QuantileTransformer() 변환기를 살펴 보겠습니다. 먼저 ‘파이썬 라이브러리를 활용한 머신러닝‘ 3장에 있는 스케일 조정의 예제와 비슷한 데이터 셋을 make_blobs 함수로 만들겠습니다. 여기서는 샘플 개수가 어느 정도 되어야 눈으로 확인하기 좋으므로 500개를 만들겠습니다.

X, y = make_blobs(n_samples=500, centers=2, random_state=4)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='black')

quantile-1

QuantileTransformer 는 지정된 분위수에 맞게 데이터를 변환합니다. 기본 분위수는 1,000개이며 n_quantiles 매개변수에서 변경할 수 있습니다. 여기서는 100개 정도로 지정해 보겠습니다.

quan = QuantileTransformer(n_quantiles=100)

fit() 메서드에서 입력 데이터의 범위를 분위수에 맞게 나누어 quantiles_ 속성에 저장합니다. 이를 위해 넘파이 percentile() 함수를 사용하여 분위에 해당하는 값을 손쉽게 구할 수 있습니다. QuantileTransformer 에는 기본값이 100,000인 subsample 매개변수가 있습니다.  만약 입력 데이터 개수가 subsample 보다 크면 계산량을 줄이기 위해 subsample 개수만큼 데이터를 샘플링하여 percentile() 함수를 적용합니다. percentile() 함수는 특성마다 각각 적용되므로 quantiles_ 속성의 크기는 [n_quantiles, X.shape[1]] 이 됩니다.

quan.fit(X)
print(quan.quantiles_.shape)
(100, 2)

transform() 메서드에서는 데이터를 quantiles_를 기준으로 하여 0~1 사이로 매핑합니다. 이를 위해 넘파이 interp() 함수를 사용합니다. 두 개의 특성이 모두 0~1 사이로 균등하게 나뉘어진 것을 그래프로 확인할 수 있습니다.

X_quan = quan.transform(X)
plt.scatter(X_quan[:, 0], X_quan[:, 1], c=y, edgecolors='black')

quantile-2

이런 변환은 RobustScaler와 비슷하게 이상치에 민감하지 않게 됩니다. 하지만 균등 분포라서 무조건 [0, 1] 사이로 클리핑합니다.

사실 transform() 메서드에서는 scipy.stats.uniform.ppf() 함수를 사용하여 균등 분포로 변환합니다. 하지만 interp() 함수에서 동일한 변환을 이미 하고 있기 때문에 효과가 없습니다. QuantileTransformer 에서 output_distribution=’normal’ 로 지정하면 scipy.stats.norm.ppf() 함수를 사용하여 정규 분포로 변환합니다.

quan = QuantileTransformer(output_distribution='normal', n_quantiles=100)
X_quan = quan.fit_transform(X)
plt.scatter(X_quan[:, 0], X_quan[:, 1], c=y, edgecolors='black')

quantile-3

변환된 데이터는 평균이 0, 표준편차가 1인 정규 분포임을 확인할 수 있습니다.

X_quan.mean(axis=0), X_quan.std(axis=0)
(array([-0.00172502, -0.00134149]), array([ 1.0412595 ,  1.03818794]))

StandardScaler 와 비교해 보면 평균 과 표준편차는 같지만 정규 분포를 따르지 않는 분포에서 차이를 확인할 수 있습니다. 🙂

from sklearn.preprocessing import StandardScaler
X_std = StandardScaler().fit_transform(X)
plt.scatter(X_std[:, 0], X_std[:, 1], c=y, edgecolors='black')

quantile-4

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

[Review] Hands-On Machine Learning with Scikit-Learn & TensorFlow

lrg 이 책의 번역을 시작한지 벌써 6개월이 훌쩍 넘어가 버렸습니다. 개인적인 핑계가 없는 것은 아니지만, 그럼에도 불구하고 이렇게 오랜 시간이 걸린 것은 번역 작업이 어떤 병렬화도 불가능한 노동 집약적인 일이기 때문일 것입니다 🙂 작업이 거의 마무리되어 가는 시점에 원서 전체에 대한 리뷰와 작업에 대한 기록을 위해 글을 남깁니다.

이 책은 500페이지가 넘어 두툼한 편이고, Scikit-Learn 라이브러리를 사용한 머신러닝Machine Learning텐서플로TensorFlow를 사용한 딥러닝Deep Learning 파트 두 부분으로 크게 나누어 집니다(실제 최근 프랑스어 판은 두 권의 책으로 출판되었다고 합니다). 분량만으로 비교했을 때에도 약간 딥러닝 쪽에 더 무게를 두고 있습니다. 간간히 Scikit-Learn과 텐서플로를 함께 다루고 있는 책들이 있는데, 아마도 딥러닝만을 설명하기엔 머신러닝 개념이 꼭 필요하기 때문인 것 같습니다. 물론 텐서플로만 다루는 책이더라도 도입부에 회귀와 분류 모델을 간단히 텐서플로로 만들어 봄으로써 머신러닝에 대한 개념을 다루고 진행하는 것이 일반적입니다. 이 책은 두 라이브러리를 모두 다루고 있으며 유사한 책 중에서는 가장 포괄적이고 세세한 내용을 담고 있습니다. 또 이론서와 실용서의 중간 형태라고 볼 수 있습니다. 수식없이 코드만 있지 않고, 코드는 없고 이론만 늘어 놓지도 않습니다.

작업을 시작하고 나서 7월, 10월에 원서의 내용이 크게 개정되었습니다. 주로 딥러닝 파트였으며, 원서의 여러가지 오류 때문이기도 하지만 아마도 이 분야의 변화가 빠른 것도 한 몫 했을 것입니다. 바뀐 부분을 PDF로 받았는데 책으로 옮겨 적기에는 너무 양이 많아 출력해서 책에 오려 붙이고 작업을 진행했습니다. 그 외에도 괴장히 많은 에러타가 있고, 아직 컨펌되지 않은 에러타도 남아 있습니다(에러타가 많이 발견되었다는 것은 그 만큼 높은 인기를 반증합니다). 최근에는 에러타와 개정판에도 언급되지 않은 변경사항이 사파리온라인에서 발견되어 저의 멘탈을 붕괴 직전까지 몰고 가기도 했습니다. 😦

이 책이 다른 책과 구별되는 한 장을 고르라고 한다면 저는 2장을 선택할 것 같습니다. 2장은 회귀 모델을 사용해 머신러닝 프로젝트의 준비에서부터 런칭까지 모든 단계를 짚어가고 있습니다. 이 책에는 특성 공학feature engineering에 대한 장을 따로 할당하지 않았는데 2장에서 이런 부분이 다소 해소됩니다. 다른 책에서도 프로젝트 운영에 대한 가이드라인이 다소 포함되어 있기도 하지만, 여기에서처럼 실제 프로젝트를 진행하듯이 자세히 기술되어 있지는 않습니다. 이런 부분은 저자의 프로젝트 경험을 엿볼 수 있는 좋은 사례인 것 같습니다.

이 책은 구성상 독특한 점이 두 가지가 있습니다. 첫째는 다른 책들과는 달리 샘플 코드 전체를 싣지 않고 있는 것입니다. 비교적 많은 주제에 대해 상세히 논하고 있기 때문에 만약 코드를 모두 썼다면 이보다 훨씬 더 두꺼운 책이 되었을 것입니다. 설명을 위해 꼭 필요한 코드 부분만 실었으며 전체 코드는 깃허브github.com의 주피터 노트북Jupyter Notebook을 참고하도록 안내하고 있습니다. 둘째, 각 장의 끝에 연습문제를 포함하고 있습니다. 그렇다고 이 책이 대학 교재스러운 편집 구성을 가지는 것은 아닙니다. 일반 컴퓨터 과학 도서에서 연습문제를 가진 경우는 드물기 때문에 신선하기도 하고 저자의 노력을 느낄 수 있습니다. 연습문제는 크게 본문의 내용을 질문하는 것과 실습을 하는 것, 두 종류로 나누어 집니다. 내용에 대한 질문의 답은 책의 부록에 실려 있습니다. 실습 문제는 깃허브의 장별 주피터 노트북 말미에 포함되어 있습니다. 다만 아직 모든 연습문제의 답이 올려져 있지 않고 점진적으로 추가되고 있습니다. 번역서가 출간될 쯤에는 깃허브의 주피터 노트북의 내용도 모두 한글로 번역되어 제공됩니다. 추후 업데이트되는 연습문제도 함께 번역이 될 것입니다.

Scikit-Learn을 이용해서는 회귀, 분류에 대한 기본 내용으로 시작해서 서포트 벡터 머신Support Vector Machine, 결정 트리Decision Tree, 앙상블emsemble, 차원 축소Dimensionality Reduction까지 다룹니다. 아무래도 ‘파이썬 라이브러리를 활용한 머신러닝‘처럼 머신러닝 파이프라인의 모든 부분을 다루기엔 지면이 부족합니다. 하지만 다루는 모델의 이론(수식)을 충분히 설명하고 예제 코드를 병행하고 있습니다. 텐서플로를 사용해서는 기본 인공 신경망과 텐서플로에 대한 소개를 먼저 시작합니다. 그리고 심층 신경망을 학습하기 위해 당면한 문제들을 해결하는 여러가지 방법을 소개합니다. 모델 재사용, 여러가지 최적화 알고리즘, 초기화, 규제 등입니다. 보통 GPU나 분산처리 부분은 딥러닝 책의 말미에 나오는 경우가 많은데 이 책에서는 합성곱Convolution 신경망 보다도 먼저 나오고 비중이 비교적 큽니다. 그 다음 합성곱 신경망Convolution Neural Networks에서 합성곱, 패딩, 스트라이딩에 대한 설명 뿐만 아니라 LeNet-5, AlexNet, GooLeNet, ResNet 구조를 상세히 설명하고 있습니다. 순환 신경망Recurrent Neural Networks에서는 직접 간단한 RNN 연산을 만들어 보면서 순환 신경망에 대한 이해를 돕고 있으며, LSTM, GRU와 기본적인 기계번역 RNN 구조까지 설명하고 있습니다. 그 다음 오토인코더Autoencoder, 강화학습Reinforcement Learning을 다루는 장으로 이어집니다.

아래 장별 제목을 옮겨 놓았습니다. 책에 관련하여 궁금한 점이나 리뷰, 추천사에 의향이 있으시면 언제든지 댓글이나 메일 주세요. 앗 그리고 새해 복 많이 받으세요! 🙂

  1. 한눈에 보는 머신러닝
  2. 머신러닝 프로젝트 시작부터 끝까지
  3. 분류
  4. 모델 훈련
  5. 서포트 벡터 머신
  6. 결정 트리
  7. 앙상블 학습과 랜덤 포레스트
  8. 차원 축소
  9. 텐서플로 시작하기
  10. 인공 신경망 소개
  11. 심층 신경망 훈련
  12. 다중 머신과 장치를 위한 분산 텐서플로
  13. 합성곱 신경망
  14. 순환 신경망
  15. 오토인코더
  16. 강화학습
  • 연습문제 정답
  • 머신러닝 프로젝트 체크 리스트
  • SVM 쌍대 문제
  • 자동 미분
  • 인기 있는 다른 인공 신경망 구조

반복 교차 검증

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

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

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

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

먼저 가장 기본이 되는 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를 그리드서치에 적용해 보겠습니다.

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)
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)에서 확인할 수 있습니다.