티스토리 뷰
05-2 교차 검증과 그리드 서치¶
- 검증 세트¶
테스트 세트를 사용하지 않으면 모델이 과대적합인지 과소적합인지 판단하기 어렵다. 테스트 세트를 사용하지 않고 이를 측정하는 간단한 방법은 훈련 세트를 또 나누는 것이다. 이 데이터를 검증 세트라고 부른다.
앞서 전체 와인 데이터중 20%를 테스트 세트로 만들고 80%를 훈련 세트로 만들었다. 이 훈련 세트중에서 다시 20%를 떼어 내어 검증 세트로 만든다.
훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가한다. 이런 식으로 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고른다. 그다음 이 매개변수를 사용해 훈련 세트와 검증 세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련한다. 그리고 마지막에 테스트 세트에서 최종 점수를 평가한다.
데이터를 다시 불러오자.
import pandas as pd
wine = pd.read_csv('/home/jaeyoon89/hg-mldl/wine.csv')
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)
이 과정 다음에 train_input과 train_target을 다시 train_test_split() 함수에 넣어 훈련 세트 sub_input, sub_target과 검증세트 val_input,val_target을 만든다. 여기에서도 test_size 매개변수를 0.2로 지정하여 train_input의 약 20%를 val_input으로 만든다.
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)
원래 5197개 였던 훈련 세트가 4157개로 줄고 검증 세트는 1040개가 되었다. 이제 모델을 만들고 평가해 보자.
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
이 모델은 훈련 세트에 과대적합되어 있다. 매개변수를 바꿔서 더 좋은 모델을 찾아야 한다. 그 전에 검증 세트에 대해 더 알아보자.
- 교차 검증¶
검증 세트를 만드느라 훈련 세트가 줄었다. 보통 많은 데이터를 훈련에 사용할수록 좋은 모델이 만들어진다. 그렇다고 검증 세트를 너무 조금 떼어 놓으면 검증 점수가 들쭉날쭉하고 불안정할 것이다. 이럴 때 교차 검증을 이용하면 안정적인 검증 점수를 얻고 훈련에 더 많은 데이터를 사용할 수 있다.
교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복한다. 그다음 이 점수를 평균하여 최종 검증 점수를 얻는다. 보통 5-폴드 교차 검증이나 10-폴드 교차 검증을 많이 사용한다. 이렇게 하면 데이터의 80 ~ 90% 까지 훈련에 사용할 수 있다. 검증 세트가 줄어들지만 각 폴드에서 계산한 검증 점수를 평균하기 때문에 안정된 점수로 생각할 수 있다.
사이킷런에는 cross_validate() 라는 교차 검증 함수가 있다. 먼저 평가할 모델 객체를 첫 번째 매개변수로 전달한다. 그다음 앞에서처럼 직접 검증 세트를 떼어내지 않고 훈련 세트 전체를 cross_validat() 함수에 전달한다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
{'fit_time': array([0.00835681, 0.00643492, 0.00545597, 0.00457048, 0.00498867]), 'score_time': array([0.00071645, 0.00080824, 0.0004096 , 0.00037885, 0.00034475]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
이 함수는 fit_time,score_time,test_score 키를 가진 딕셔너리를 반환한다. 처음 2개의 키는 각각 모델을 훈련하는 시간과 검증하는 시간을 의미한다. 각 키마다 5개의 숫자가 담겨있다. cross_validate() 함수는 기본적으로 5-폴드 교차 검증을 수행한다. cv 매개변수에서 폴드 수를 바꿀 수도 있다.
교차 검증의 최종 점수는 test_score 키에 담긴 5개의 점수를 평균하여 얻을 수 있다.
import numpy as np
print(np.mean(scores['test_score']))
0.855300214703487
교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있다. 한 가지 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다. 앞서 우리는 train_test_split() 함수로 전체 데이터를 섞은 후 훈련 세트를 준비했기 때문에 따로 섞을 필요가 없다. 하지만 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기를 지정해야 한다.
사이킷런의 분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정해 준다. 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-폴드 교차 검증을 수행하려면 다음과 같이 작성한다.
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
- 하이퍼파라미터 튜닝¶
하이퍼파라미터는 모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터이다. 사이킷런과 같은 머신러닝 라이브러리를 사용할 때 이런 하이퍼파라미터는 모두 클래스나 메서드의 매개변수로 표현된다. 그럼 이런 하이퍼파라미터를 튜닝하는 작업은 어떻게 할까? 먼저 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련한다. 그 다음 검증 세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꿔본다. 모델마다 적게는 1 ~ 2개에서, 만게는 5 ~ 6개의 매개변수를 제공한다.
매개변수가 많아지면 문제는 더 복잡해진다. 사이킷런에서 제공하는 그리드서치를 사용해보자. 사이킷런의 GridSearchCV 클래스는 친절하게도 하이퍼파라미터 탐색과 교차 검증을 한 번에 수행한다. 별도로 cross_validate() 함수를 호출할 필요가 없다. 기존 매개변수를 사용한 결정 트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아보자.
from sklearn.model_selection import GridSearchCV
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이다. min_impurity_decrease 값마다 5-폴드 교차 검증을 수행한다. 결국 5x5=25 개의 모델을 훈련하게 된다. GridSearchCV 클래스의 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]})
사이킷런의 그리드 서치는 훈련이 끝나면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다. 이 모델은 gs 객체의 bestestimator 속성에 저장 되어 있다.
dt =gs.best_estimator_
print(dt.score(train_input, train_target))
0.9615162593804117
그리드 서치로 찾은 최적의 매개변수는 bestparams 속성에 저장되어 있다.
print(gs.best_params_)
{'min_impurity_decrease': 0.0001}
0.0001이 가장 좋은 값으로 선택되었다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 cvresults 속성의 'mean_test_score' 키에 저장되어 있다. 5번의 교차 검증으로 얻은 점수를 출력해보자.
print(gs.cv_results_['mean_test_score'])
[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]
위처럼 수동으로 고르는 것보다 넘파이의 argmax() 함수를 사용하면 가장 큰 값의 인덱스를 추출할 수 있다. 그 다음 이 인덱스를 사용해 params 키에 저장된 매개변수를 출력할 수 있다.
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
{'min_impurity_decrease': 0.0001}
조금 더 복잡한 매개 변수 조합을 탐색해보자. 결정 트리에서 min_impurity_decreas는 노드를 분할하기 위한 불순도 감소 최소량을 지정한다. 여기에 max_depth로 트리의 깊이를 제한하고 min_samples_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)
}
넘파이arang() 함수는 첫 번째 매개변수 값에서 시작하여 두 번쨰 매개변수에 도달할 때까지 세 번째 매개변수를 계속 더한 배열을 만든다. 배열의 총 원소는 9개이다. 파이썬 range 함수도 비슷하다. 하지만 이 함수는 정수만 사용할 수 있다. max_depth는 15개의 값을 만들고, min_sample_split은 10개의 값을 만든다. 이 매개변수로 수행할 교차 검증 횟수는 9x15x10 = 1350개이다. 기본 5-폴드 교차검증을 수행하므로 만들어지는 6750개이다. 다음 그리드 서치를 실행해 보자.
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
- 랜덤 서치¶
매개변수의 값이 수치일 때 값으 범위나 간격을 미리 정하기 어려울 수 있다. 또 너무 많은 매개변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있다. 이럴 때 랜덤 서치를 사용하면 좋다.
랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다. 먼저 싸이파이에서 2개의 확률 분포 클래스를 임포트하자.
from scipy.stats import uniform, randint
uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다. 이를 균등 분포에서 샘플링한다 라고 한다. randint는 정숫값을 뽑고 uniform은 실수값을 뽑는다. 아래 코드를 따라해 보자.
rgen = randint(0,10)
rgen.rvs(10)
array([9, 0, 1, 3, 4, 8, 8, 2, 1, 3])
이번엔 1000개를 샘플링해서 각 숫자의 개수를 세어보자.
np.unique(rgen.rvs(1000), return_counts=True)
(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]), array([ 95, 98, 108, 92, 104, 102, 88, 120, 106, 87]))
개수가 늘어나니 0에서 9까지의 숫자가 어느 정도 고르게 추출된 것을 볼 수 있다. uniform 클래스의 사용법도 동일하다. 0 ~ 1 사이에서 10개의 실수를 추출해 보자.
ugen = uniform(0,1)
ugen.rvs(10)
array([0.00585314, 0.20742187, 0.75448161, 0.67021122, 0.00958848, 0.13415657, 0.32193048, 0.08565726, 0.18960181, 0.07269254])
이제 탐색할 매개변수의 딕셔너리를 만들어 보자. 여기에서는 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),
}
샘플링 횟수는 사이킷런의 랜덤 서치 클래스인 RandomizedSearchCV의 n_iter 매개변수에 지정한다.
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 0x7fc7fe22c828>, 'min_impurity_decrease': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c400>, 'min_samples_leaf': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c7b8>, 'min_samples_split': <scipy.stats._distn_infrastructure.rv_frozen object at 0x7fc7fe22c160>}, random_state=42)
위 params에 정의된 매개변수 범위에서 총 100번을 샘플링하여 교차 검증을 수행하고 최적의 매개변수 조합을 찾는다. 앞서 그리드 서치보다 훨씬 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색할 수 있다. 먼저 최적의 매개변수 조합을 출력하자.
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
출처 : 혼자 공부하는 머신러닝 + 딥러닝
'혼자공부하는 머신러닝+딥러닝' 카테고리의 다른 글
혼자 공부하는 머신러닝+딥러닝(ch6-1 군집 알고리즘) (0) | 2021.05.03 |
---|---|
혼자 공부하는 머신러닝+딥러닝(ch5-3 트리의 앙상블) (0) | 2021.05.03 |
혼자 공부하는 머신러닝+딥러닝(ch5-1 결정 트리) (0) | 2021.05.02 |
혼자 공부하는 머신러닝+딥러닝(ch4-2 확률적 경사 하강법) (0) | 2021.05.01 |
혼자 공부하는 머신러닝+딥러닝(ch4-1 로지스틱 회귀) (0) | 2021.04.30 |