1. 검증 세트
우리는 그간 테스트 세트의 평가 값을 모델의 일반적인 성능으로 생각하고 사용해 왔는데, 모델의 하이퍼파라미터를 평가 값이 최대가 되는 곳에 맞췄을 때에는 모델이 테스트 세트에 잘 맞춰져 있는 것이지 일반적인 성능이 높다고 장담할 수가 없습니다. 이 문제를 해결하기 위해 검증 세트를 만들어야 합니다.
검증 세트를 만드는 법은 매우 간단한데, 테스트 세트와 훈련 세트를 분리한 상태에서 훈련 세트의 20%를 다시 떼어내 검증 세트로 만드는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
from sklearn.model_selection import train_test_split
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) # 훈련 세트 중 20%를 다시 떼어 검증 세트로 만듦
sub_input, sub_target은 훈련 세트, val_input, val_target은 검증 세트입니다. 이제 훈련 세트로 훈련을 한 뒤 검증 세트로 모델을 평가하여 최선의 결과를 찾은 후, 테스트 세트로 마지막에 평가한다면 테스트 세트는 모델의 일반적인 성능을 대표할 수 있게 됩니다.
그런데, 이 방법에도 문제가 있는데, 테스트 세트 20%, 검증 세트 20%를 떼어내면 훈련 세트는 전체 데이터의 60%밖에 남지 않는다는 것입니다. 이를 해결하기 위해 교차 검증을 도입합니다.
2. 교차 검증
교차 검증은 모델에서 검증 세트를 조금씩 떼어 평가하는 과정을 여러 번 반복하는 것입니다. 최종 검증 점수는 검증 점수의 평균을 사용하는데, 이렇게 하면 검증 세트로 사용하는 양을 줄이고, 여러 번 시도하기 때문에 안정적인 평가 점수를 얻을 수 있게 됩니다.
1
2
3
4
5
6
7
8
9
from sklearn.model_selection import cross_validate # 교차 검증
scores = cross_validate(dt, train_input, train_target) # 기본값으로 5-폴드 교차 검증을 함
print(scores)
=>{'fit_time': array([0.01302743, 0.01005721, 0.01093102, 0.01039124, 0.00997639]), 'score_time': array([0.00123739, 0.00105524, 0.00107336, 0.00102425, 0.00110364]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
import numpy as np
print(np.mean(scores['test_score'])) # 교차 검증의 평균
=>0.855300214703487
cross_validate를 사용하면 쉽게 교차 검증을 할 수 있으며, 검증을 몇 번 반복하느냐에 따라 k-폴드 교차 검증이라 부르는데, 기본 값은 5-폴드 교차 검증입니다.
이렇게 하면 이제 완벽한 검증을 하는 것 같지만, 사실 더 할 일이 남았습니다. 검정 세트를 떼어내고, 모델을 훈련하고, 평가하는 모든 사이클에서 훈련 세트는 섞이지 않고 그대로 사용됩니다. 이것을 섞기 위해서는 분할기를 사용해야 합니다. 분할기는 StratifiedKFold(분류 문제), KFold(회귀 문제)를 사용하는데, 이 경우는 분류 문제이기때문에 StratifiedKFold를 사용하겠습니다.
1
2
3
4
5
from sklearn.model_selection import StratifiedKFold
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42) # n_splits : 몇 폴드 교차 검증을 할지, shuffle : 교차 검증 시 훈련 세트를 섞음
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))
=>0.8574181117533719
shuffle = True로 설정해주면 기존보다 더 높은 평가 점수를 얻음을 확인하였습니다.
3. 그리드 서치
검증하는 법을 배웠으니, 직접 하이퍼파라미터를 설정하여 가장 나은 것을 찾는 대신 자동으로 그것을 계산해주는 그리드 서치에 대해 알아보겠습니다. 먼저, params 딕셔너리에 각 하이퍼파라미터의 매개변수 이름을 키 값으로 할당하고, value에는 테스트해보고자 하는 값들을 list로 만들어 넣습니다.
1
2
3
4
5
from sklearn.model_selection import GridSearchCV # 그리드 서치
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) # 노드를 나누기 위한 최소 샘플 수
}
이제 그리드 서치를 사용하여 최적의 하이퍼파라미터를 찾은 뒤 전체 훈련 세트로 훈련시킵시다.
1
2
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
이렇게 마지막 훈련까지 끝난 모델은 best_estimator_에 저장되어 있으며, best_params_에는 해당 모델의 하이퍼파라미터가 저장되어 있습니다.
1
2
3
dt = gs.best_estimator_ # 앞서 찾은 최적의 하이퍼파라미터를 가진 모델이 저장되어 있음
print(dt.score(train_input, train_target))
print(dt.score(test_input, test_target))
이제 항상 하던 것과 같이 훈련 세트의 평가 점수와 테스트 세트의 평가 점수를 확인해보면 됩니다.
4. 랜덤 서치
그런데, params에 하이퍼파라미터들의 값 리스트를 넣을 때, 이 값이 연속적인 실수라면 얼마 차이로 값을 할당해야 할지 판단하기가 어려울 수 있습니다. 이제부터 소개하는 랜덤 서치는 그럴 때 유용하게 사용할 수 있는 방법입니다.
랜덤 서치는 주어진 범위 내에서 고르게 값을 뽑는 uniform(실수), randint(정수)를 이용합니다.
1
from scipy.stats import uniform, randint
랜덤 서치에서는 params에 각 값을 실수라면 uniform으로, 정수라면 randint로 랜덤하게 값을 뽑을 수 있는 영역을 설정해줍니다.
1
2
3
4
5
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),
}
이렇게 설정한 뒤에는 n_iter에 수를 할당해 이런 랜덤한 하이퍼파라미터를 이용한 검증 사이클을 몇 번 반복할 것인지를 정하면 되는데, uniform과 randint는 너무 반복을 적게 할 때는 수를 고르게 뽑기 어려우니 적당히 큰 수를 할당해야 할 것 같습니다. n_iter를 제외하고는 나머지 사용 방법은 그리드 서치와 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.model_selection import RandomizedSearchCV # 연속적인 실수 값에서 최적의 값을 찾고자 할때 유용
gs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), params,
n_iter=100, n_jobs=-1, random_state=42) # n_iter : n번 반복
gs.fit(train_input, train_target)
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
공부한 내용의 전체 코드는 github에 작성해 두었습니다.