10. 교차검증과 그리드 서치

  • 지금까지 우리는 훈련 세트에서 모델을 훈련하고 테스트 세트에서 모델을 평가했다.
  • 테스트 세트의 결과를 토대로 모델이 제대로 성능을 내는지 판단했다. 즉, 일반화 성능을 평가했다. 이는 테스트 세트를 사용해 성능을 테스트 세트에 맞추는 결과를 가져왔다.
  • 모델을 만들고 마지막에만 테스트 세트를 사용하는것이 좋다. 이를 어떻게 수행해야 할까?

1. 들어가며

# 필수 요소들
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_validate
from sklearn.model_selection import StratifiedKFold
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform, randint

2. 검증세트

  • 테스트 세트를 사용하지 않으면 모델이 과대적합인지 과소적합인지 판단하기 어렵다. 테스트 세트를 이용하지 않고 이를 측정하기 위해서는 훈련세트를 다시 나누는 방법이있다. 이런 데이터 세트를 검증 세트라고 한다.
  • 훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가한다. 이처럼 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고른다. 그다음 매개변수를 사용해 훈련세트와 검증세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련한다. 이후 마지막에 테스트 세트에서 최종점수를 평가하는 방법을 사용하는 것이다.
wine = pd.read_csv('https://bit.ly/wine_csv_data')
wine.columns
Index(['alcohol', 'sugar', 'pH', 'class'], dtype='object')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
  • 데이터를 불러왔다. 이를 이용해 테스트 세트와 훈련 세트로 나눠준다.
  • 이후 훈련 세트에서 검증세트를 빼서 만들어준다.
  • 만들어진 검증세트를 이용해 모델을 만들고 평가해보자.
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size=0.2, random_state=42)
sub_input, val_input, sub_target, val_target = train_test_split(train_input, train_target, test_size = 0.2, random_state=42)
print(sub_input.shape, val_input.shape)
(4157, 3) (1040, 3)
dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)
print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target))
0.9971133028626413
0.864423076923077
  • 모델을 확인해보면 이 모델은 훈련 세트에 과대적합된 모습을 볼 수 있다.
  • 매개변수를 바꿔 더 좋은 모델을 찾아야한다.

1. 교차검증

  • 기존의 데이터보다 검증 세트를 만드느라 데이터의 양이 줄어들었다. 통상적으로 데이터의 양이 많아야 더 좋은 모델이 나온다.
  • 그렇다고 검증 세트가 너무 작으면 점수가 불안정하게 나올 가능성이 크다. 이런 경우 교차검증을 사용한다.
  • 교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러번 반복한다. 그 후 이 점수들의 평균을 통해 최종 검증 점수를 얻는다.

2. k-폴드 교차 검증

  • 훈련세트를 임의의 부분으로 나누어 여러번 검증을 하는 것을 말한다.
  • 주로 5-폴드, 10-폴드를 많이 사용한다.
  • 이렇게 사용하면 데이터의 80~90%정도를 훈련에 사용할 수 있다. 검증 세트가 줄어들지만 각 폴드에서 계산한 검증 점수를 평균하기 때문에 보다 안정적이다.
scores = cross_validate(dt, train_input, train_target)
print(scores)
{'fit_time': array([0.00499868, 0.00499868, 0.00500035, 0.0049994 , 0.00400233]), 'score_time': array([0.00100088, 0.        , 0.        , 0.00100017, 0.00099754]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
  • 해당 함수는 fit_time, score_time, test_time키를 가진 딕셔너리를 반환한다. 처음 2개의 키는 각 모델을 훈련하는 시간과 검증하는 시간을 의미하며, 각 키마다 5개의 숫자가 담겨있다.
  • cross_validate()함수는 기본적으로 5-폴드 교차검증을 실시한다. cv매개변수에서 폴드 수를 바꿀수도 있다.
  • test_score키의 5개 점수를 평균하여 최종 점수를 얻을 수 있다.
print(np.mean(scores['test_score']))
0.855300214703487
  • 이를 통해 교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해볼 수 있다.
  • cross_vaildate()는 훈련 세트를 섞어 폴드를 나누지 않기때문에 교차 검증을 할 때 훈련 세트를 섞으려면 분할기를 지정해야 한다.
  • cross_vaildate()함수는 기본적으로 회귀 모델일 경우 KFold 분할기를 사용하고 분류 모델일 경우 타깃 클래스를 골고루 나누기 위해 StratifiedKFold를 사용한다. 앞서 사용한 교차 검증은 cv매개변수가 StratifiedKFold와 동일하다.
  • 10-폴드 교차검증을 수행하려면 아래와 같다.
splitter = StratifiedKFold(n_splits = 10, shuffle=True, random_state = 42) # n_splits는 몇 번 폴드 교차 검증을 할지 정한다.
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))
0.8574181117533719

3. 하이퍼파라미터 튜닝

  • 머신러닝 모델이 학습하는 파라미터를 모델 파라미터라고 부른다. 반면 모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 한다.
  • 사이킷런과 같은 머신러닝 라이브러리를 사용할 때 이런 하이퍼파라미터는 모두 클래스나 메서드의 매개변수로 표현된다.
  • 이를 튜닝하는 방법은 먼저 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련한 다음 검증 세트의 점수나 교차 검증을 통해 매개변수를 조금씩 바꿔본다. 이처럼 조금씩 매개변수를 바꿔가면서 모델을 훈련하고 교차검증을 수행해야 한다.
  • 하나의 최적값을 찾고 다른 최적값을 찾는것이 아닌 동시에 바꿔가며 최적의 파라미터값을 찾아야한다.
  • 매개변수가 많아지면 찾는 문제가 더 복잡해진다. 이를 그리드 서치를 이용하면 더 편하게 사용이 가능하다.
  • 사이킷런의 GridSearchCV 클래스는 하이퍼파라미터 탐색과 교차 검증을 한번에 수행해준다.
  • 예시로 기본 매개변수를 사용한 결정 트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아보자.
params = {'min_impurity_decrease' : [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}
  • 여기서는 0.0001부터 0.0005까지 0.0001씩 증가하는 5개의 값을 시도해보도록 하겠다. GridSearchCV클래스의 탐색 대상 모델과 params 변수를 전달해 그리드 서치 객체를 만든다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params,n_jobs = -1)
  • 그다음 일반 모델을 훈련하는 것처럼 gs객체에 fit() 메서드를 호출한다. 이를 호출하면 min_impurity_decrease를 값을 바꿔가며 5회를 실행한다.
  • GridSearchCV의 cv매개변수 기본값은 5이다. 많은 모델을 훈련하기 때문에 n_jobs 매개변수에서 병렬 실행에 사용할 cpu코어 수를 지정하는 것이 좋다. 기본값은 1이지만 -1로 지정하면 시스템의 모든 코어를 사용한다.
gs.fit(train_input, train_target)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1,
             param_grid={'min_impurity_decrease': [0.0001, 0.0002, 0.0003,
                                                   0.0004, 0.0005]})
  • 교차 검증에서 최적의 파라미터를 찾으면 전체 훈련 세트로 모델을 다시 만들어야 하지만 사이킷런은 그리드 서치 훈련이 끝나면 가장 점수가 좋은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다.
dt = gs.best_estimator_
print(dt.score(train_input, train_target))
0.9615162593804117
  • 그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어있다.
print(gs.best_params_)
{'min_impurity_decrease': 0.0001}
  • 여기서는 0.0001이 가장 좋은 값으로 선택되었다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 cv_results_ 속성의 ‘mean_test_score’키에 저장되어있다.
print(gs.cv_results_['mean_test_score'])
[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]
  • 첫 번째 값이 가장 큰 것 같다. 수동으로 고르기 보단 numpy의 argmax()함수를 이용하면 가장 큰 값의 인덱스를 추출할 수 있다. 이 인덱스를 사용해 params 키에 저장된 매개변수를 출력할 수 있다. 이 값이 최상의 점수를 만든 매개변수의 조합이다.
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
{'min_impurity_decrease': 0.0001}
  • 과정을 정리하면 다음과 같다.
    1. 먼저 탐색할 매개변수를 지정한다.
    2. 훈련 세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다.
    3. 그리드 서치는 최상의 매개변수에서 전체 훈련 세트를 사용해 최종 모델을 훈련한다.
  • 조금 더 복잡한 매개변수 조합을 탐색해 보자. 결정 트리에서 min_impurity_decrease는 노드를 분할하기 위한 불순도 감소 최소량을 지정한다. 여기에다가 max_depth로 트리의 깊이를 제한하고 min_sample_split으로 노드를 나누기 위한 최소 샘플 수도 골라보자.
params = {'min_impurity_decrease' : np.arange(0.0001, 0.001, 0.0001),
         'max_depth' : range(5, 20, 1),
         'min_samples_split' : range(2, 100, 10)
         }
  • 위의 매개변수로 수행한 교차검증의 횟수는 1350개이며, 5-폴드 교차 검증 수행시 6750개나 된다. n_jobs를 -1로 설정하고 그리드 서치를 수행해보자
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params,n_jobs=-1)
gs.fit(train_input, train_target)
GridSearchCV(estimator=DecisionTreeClassifier(random_state=42), n_jobs=-1,
             param_grid={'max_depth': range(5, 20),
                         'min_impurity_decrease': array([0.0001, 0.0002, 0.0003, 0.0004, 0.0005, 0.0006, 0.0007, 0.0008,
       0.0009]),
                         'min_samples_split': range(2, 100, 10)})
print(gs.best_params_)
{'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}
  • 점수도 확인해보자
print(np.max(gs.cv_results_['mean_test_score']))
0.8683865773302731
  • 이처럼 그리드 서치를 활용하면 하나씩 매개변수를 바꿔가며 교차 검증을 수행하지 않고도 최상의 매개변수를 찾을 수 있다.
  • 하지만 아쉬운점이 존재하는데 매개변수의 간격의 근거가 없다. 이를 정하기 어려운 경우도 존재한다.

4. 랜덤서치

  • 매개변수의 값이 수치일 때 값의 범위나 간격을 미리 정하기가 어려울 수 있다. 또는 너무 많은 매개변수 조건이 있어 그리드 서치 수행시간이 오래 걸릴 수 있다. 이럴 때 랜덤 서치를 이용하면 좋다.
  • 랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아닌 매개변수를 샘플링 할 수 있는 확률 분포객체를 전달한다.
  • scipy를 이용해 2개의 확률분포 클래스를 임포트 한뒤 0에서 10범위를 갖는 randint객체를 만들고 10개의 숫자를 샘플링 해보자.
rgen = randint(0,10)
rgen.rvs(10)
array([2, 6, 2, 5, 3, 7, 0, 7, 5, 0])
np.unique(rgen.rvs(1000), return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
 array([103,  98, 109, 102,  99, 115,  81,  83, 101, 109], dtype=int64))
  • uniform 클래스 사용법도 동일하다.
ugen = uniform(0,1)
ugen.rvs(10)
array([0.32777925, 0.8078062 , 0.51967578, 0.80336748, 0.3876953 ,
       0.93972201, 0.43193232, 0.10679947, 0.22862636, 0.4850421 ])
  • 이는 난수발생기랑 유사하다고 생각하면된다. 이를 이용하기 앞서, 매개변수의 딕셔너리를 먼저 만들어 보자.
  • min_samples_leaf 매개변수를 탐색 대상에 추가하겠다. 해당 매개변수는 리프 노드가 되기 위한 최소 샘플의 개수를 말한다.
params = {'min_impurity_decrease' : uniform(0.0001, 0.001),
         'max_depth' : randint(20, 50),
         'min_samples_split' : randint(2, 25),
         'min_samples_leaf' : randint(1, 25)
         }
gs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42),params, n_iter = 100, n_jobs=-1, random_state=42)
gs.fit(train_input, train_target)
RandomizedSearchCV(estimator=DecisionTreeClassifier(random_state=42),
                   n_iter=100, n_jobs=-1,
                   param_distributions={'max_depth': <scipy.stats._distn_infrastructure.rv_frozen object at 0x00000275BDD2F610>,
                                        'min_impurity_decrease': <scipy.stats._distn_infrastructure.rv_frozen object at 0x00000275BDD2F340>,
                                        'min_samples_leaf': <scipy.stats._distn_infrastructure.rv_frozen object at 0x00000275BDC7FEE0>,
                                        'min_samples_split': <scipy.stats._distn_infrastructure.rv_frozen object at 0x00000275BDD4A700>},
                   random_state=42)
print(gs.best_params_)
{'max_depth': 39, 'min_impurity_decrease': 0.00034102546602601173, 'min_samples_leaf': 7, 'min_samples_split': 13}
print(np.max(gs.cv_results_['mean_test_score']))
0.8695428296438884
  • 최적의 모델은 이미 전체 훈련 세트로 훈련되어 저장되어있다. 이 모델을 최종으로 테스트 세트의 성능을 확인해보자.
dt = gs.best_estimator_
print(dt.score(test_input, test_target))
0.86
  • 테스트 세트 점수는 검증 세트에 대한 점수보다 조금 작은 것이 일반적이다.
  • 점수가 만족스럽진 못하지만 오늘은 여기까지 해야겠다.

5. 마치며

  • 오늘은 검증에 관한 내용과 그리드 서치, 랜덤서치 등을 배웠다.
  • 엄청 긴 내용이진 않았지만 내용자체가 중요하다고 생각되어 다시 한번 더 공부를 해봐야 겠다고 생각했다.
  • 그리드 서치와 랜덤 서치를 이용해 최적의 매개변수값을 찾고 이를 이용해 모델을 최적화하고 좋은 점수를 내기위해 사용할 수 있다는 것을 다시한번 배울 수 있는 기회였다. 또한 다양한 매개변수를 이용해 테스트하고 내린 결과로 검증된 결과를 만들기 위해서라도 필요하다는 것을 알게 되었다.

Categories:

Updated:

Leave a comment