Home [혼공머신러닝 5–2] 교차 검증과 그리드 서치, 랜덤 서치
Post
Cancel

[혼공머신러닝 5–2] 교차 검증과 그리드 서치, 랜덤 서치

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에 작성해 두었습니다.

https://github.com/ynkim0/study/blob/main/5-2.ipynb

This post is licensed under CC BY 4.0 by the author.