[혼공머신러닝] Ch 5. 트리 알고리즘(2)
교차 검증과 그리드 서치
테스트 세트 성능을 올바르게 판단하기 위해서는 모델을 전부 만든 뒤에 마지막에 한 번만 사용하는 것이 좋습니다. 그렇다면 테스트 세트를 사용하지 않고 어떻게 하이퍼 파라미터 튜닝을 할 수 있을지 알아보도록 하겠습니다.
검증 세트
테스트 세트를 사용하지 않고 모델이 과대적합인지 과소적합인지 알아보기 위해서는 훈련 세트를 또 나누는 방법을 취할 수 있습니다. 이 데이터를 검증세트라고 합니다. 훈련 세트에서 모델을 훈련한 후 검증세트로 평가합니다. 이런 식으로 매개변수들을 바꿔가며 가장 좋은 모델을 고르면 됩니다.
그럼 데이터를 불러와서 train 세트와 test 세트로 나눈 후에 검증 세트로도 나눠보겠습니다.
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
와인 데이터를 data에는 alcohol, sugar, pH 를 지정하고, target에는 class 를 넘파이 배열로 지정해줍니다.
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
train_test_split를 사용하여 train data와 test data로 나눕니다. test_size를 0.2로 지정하여 20%만 test 데이터로 가져옵니다. test size를 지정하지 않을 경우 train_test_split는 디폴트로 0.25를 테스트 세트로 지정합니다.
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)
그리고 train data를 sub data와 val data로 한번 더 나누어 줍니다.
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)
데이터를 각각 훈련데이터, 검증데이터, 테스트 데이터로 나누어줬습니다. 다음으로 훈련데이터와 검증데이터를 사용하여 모델을 만들고 평가합니다.
from sklearn.tree import DecisionTreeClassifier
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
모델이 훈련 세트에 과대적합 되어있으니 매개변수를 바꾸어 더 좋은 모델을 찾아보겠습니다.
교차 검증
많은 데이터를 훈련세트에 사용할 수록 모델의 정확도가 좋아집니다. 그렇다고 검증세트를 적게 사용하면 검증 점수가 불안정하게 나올 수 있습니다. 이를 해결하기 위해 교차검증을 이용합니다.
검증세트를 떼어놓는 과정을 여러 번 반복하고 이 점수들을 평균내어 검증 점수를 얻어냅니다.
이런 방법을 사용하면 데이터의 8,90% 정도를 훈련에 사용할 수 있습니다.
사이킷런에 cross_validate() 함수를 사용하여 교차검증을 해보도록 하겠습니다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
{‘fit_time’: array([0.00482202, 0.00468516, 0.00489712, 0.00469923, 0.0045011 ]), ‘score_time’: array([0.0005331 , 0.00038004, 0.00038218, 0.00042176, 0.00033784]), ‘test_score’: array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
이 함수는 fit_time, score_time, test_score 키를 갖는 딕셔너리를 반환합니다. cross_validate 함수는 기본적으로 5-폴드 교차검증을 수행하기 때문에 각 키마다 5개의 숫자가 담겨있습니다. 교차 검증의 점수들을 평균내어 최종 점수를 얻습니다.
import numpy as np
print(np.mean(scores['test_score']))
0.855300214703487
다만 cross_validate 함수는 훈련 세트를 섞어서 폴드를 나누지 않기 때문에 만약 훈련세트를 섞어서 나누고 싶다면 splitter을 지정해줘야합니다. cross_validate 함수는 기본적으로 회귀모델일 경우 KFold 분할기를 사용하고 분류모델일 경우 StratifiedKFold를 사용합니다. 다음 코드와 앞서 수행한 교차 검증은 동일합니다.
from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))
0.855300214703487
10-교차검증을 해보고 싶다면 다음과 같이 stratifiedKFold에 splits를 10으로 지정하고 splitter에 할당하여 사용하면 됩니다.
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv=splitter)
print(np.mean(scores['test_score']))
0.8574181117533719
하이퍼파라미터 튜닝
결정트리 모델에서는 여러 매개변수를 동시에 바꿔가며 최적의 값을 찾아야하기 때문에 그리드 서치를 사용합니다. GridSearchCV 클래스는 하이퍼파라미터 탐색과 교차 검증을 한번에 수행해줍니다. 한 예시로 기본 매개변수를 사용한 결정트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아보겠습니다.
from sklearn.model_selection import GridSearchCV
params = {'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}
여기서는 5개의 값을 시도합니다.
결정트리 클래스의 객체를 생성하고 바로 전달합니다.
gs 객체에 fit 메서드를 호출합니다. 메서드를 호출하면 그리드 서치 객체는 min_impurity_decrease 값을 바꿔가며 5번 실행합니다.
많은 모델을 훈련하기 때문에 n_jobs 매개변수를 사용하여 미리 사용할 CPU 코어수를 지정해줄 수 있습니다.
이 매개변수의 기본 값은 1로, -1로 지정하면 시스템에 있는 모든 코어를 사용합니다.
사이킷런의 그리드 서치는 훈련이 끝나면 best_estimator_속성에 가장 좋은 모델을 저장합니다.
그리고 그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어있습니다.
확인해보겠습니다.
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={‘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
print(gs.best_params_)
{‘min_impurity_decrease’: 0.0001}
0.0001이 가장 좋은 값으로 선택되었습니다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 mean_test_score에 저장되어있습니다. 그리고 어떤 값이 큰지 확인하기 위해 넘파이의 argmax() 함수를사용합니다. 그다음 그 인덱스를 이용하여 params키에 저장된 매개변수를 출력합니다.
print(gs.cv_results_['mean_test_score'])
[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
{‘min_impurity_decrease’: 0.0001}
이제 더 복잡한 매개변수 조합들에서도 사용해보겠습니다. params에 다음과 같이 여러 매개변수의 범위를 설정하고 그리드 서치를 실행해봅니다.
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)
}
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
랜덤서치
매개변수의 값의 범위를 미리 정하기 어려울 때, 그리드 서치 수행시간이 너무 오래걸릴 때 랜덤서치를 사용할 수 있습니다. 랜덤 서치에는 매개변수를 샘플링할 수 있는 확률분포를 전달합니다. 확률 분포 클래스를 임포트하겠습니다.
from scipy.stats import uniform, randint
임포트한 두 클래스 모두 주어진 범위 내에서 고르게 값을 뽑습니다. 차이점은 randint 값은 정숫값을, uniform은 실수값을 뽑습니다. 0과 10 사이의 범위를 갖는 randint 객체를 만들어 샘플링을 해보겠습니다.
rgen = randint(0, 10)
rgen.rvs(10)
array([3, 6, 1, 2, 3, 1, 7, 6, 5, 1])
고르게 샘플링이 되는지 확인하기 어려우니, 1000개를 샘플링해서 각 갯수를 세어봅니다.
np.unique(rgen.rvs(1000), return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([ 79, 89, 110, 112, 106, 104, 95, 99, 104, 102]))
꽤 고르게 샘플링이 된 것을 확인할 수 있습니다. 마찬가지로 uniform도 사용해보겠습니다.
ugen = uniform(0, 1)
ugen.rvs(10)
array([0.38360187, 0.85618662, 0.41423051, 0.18153867, 0.36998781, 0.93449236, 0.36294108, 0.2842492 , 0.26135179, 0.12996384])
난수 발생기와 유사한 형태로 출력되었습니다.
이제 탐색할 매개변수의 딕셔너리를 만들어줍니다. 탐색할 매개변수의 범위를 다음과 같이 지정합니다. 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),
}
from sklearn.model_selection import RandomizedSearchCV
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 0x7f8f08156b80>, ‘min_impurity_decrease’: <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f8f510ecfd0>, ‘min_samples_leaf’: <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f8f081568b0>, ‘min_samples_split’: <scipy.stats._distn_infrastructure.rv_frozen object at 0x7f8f202cc2b0>}, random_state=42)
params에서 정의된 범위에서 총 100번을 샘플링하여 교차검증을 수행하여 최적의 값을 찾습니다. 샘플링 횟수는 n_iter 매개변수에 지정해주었습니다. 이제 최적의 매개변수의 값을 출력하고 최고의 교차 검증 점수도 확인해보겠습니다.
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
best_estimator_에 저장되어있는 최적의 모델을 사용하여 테스트의 성능을 확인합니다.
dt = gs.best_estimator_
print(dt.score(test_input, test_target))
0.86
오늘은 수동으로 매개변수를 바꾸는 대신 그리드 서치나 랜덤 서치를 이용하여 최적의 매개변수를 찾는 방법을 알아보았습니다. 다음에는 결정 트리를 확장한 앙상블 모델에 대해 알아보도록 하겠습니다.
Comments