Post

[혼공머신] 4주차 05-2 교차검증과 그리드 서치

[혼공머신] 4주차 05-2 교차검증과 그리드 서치

본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.

검증 세트

우리는 이제까지 학습을 할 때, training set과 test set을 활용해서 학습을 하고 테스트를 하는 식으로 진행을 했었다. 그런데, 이렇게 할 경우 overfitting의 우려를 버릴 수 없다. 이를 방지하기 위해서 training set을 또 나눠서 검증 세트(Validation Set)을 추가로 만들어 활용하는 것이다. 즉, training set에서 훈련한 다음 validation set으로 평가를 하고 매개변수를 수정해가면서 가장 좋은 모델을 고르고, 이후 또 test set에서 최종적으로 평가를 하는 것이다. 이것도 코드를 작성해서 볼 수는 있긴 하지만, 사실 이 방법은 너무 기초적인 수준이라서 실제 사용하기는 어렵다. 우리는 이것 말고 뒤에 나올 교차검증이라는 것을 더 많이 사용할 것이다.

교차검증

교차 검증(Cross Validation)은 아까 살펴보았던 validation set을 조금 더 발전시킨 것이다. 우리는 안 그래도 적은 training set을 또 나누어 validation set을 만들어서 사용했었다. 이러면 training이 제대로 안 되지 않을까? 이러한 우려를 해결하기 위해서 우리는 새로운 방법을 생각해냈다. 바로 모든 trianing set을 validation set으로 활용하는 것이다. 우리의 교재에서는 k-폴드 교차 검증(k-Fold Cross Validation)을 활용한다. 이를 간단히 말해서 training set을 k개의 set으로 나눈 다음 각 set을 한번씩 validation set으로 사용해 학습하는 것이다. 그리고 이렇게 학습한 k개의 결과를 평균을 내서 점수를 얻는 방법이다. 이를 그림으로 표현하면 아래와 같이 표현할 수 있다.

image

이렇게 cross validation을 활용하면 특정 training set에 편향되지 않고, 만약 training set이 적더라도 여러번 학습을 해 더 나은 score를 얻을 수 있게 된다. 결과를 보면 처음 두 개는 소요되는 시간이고 마지막이 score이다. 따라서 최종 score는 이 마지막 score들을 평균을 내서 구할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.tree import DecisionTreeClassifier

wine = pd.read_csv("https://bit.ly/wine_csv_data")

data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)

scores = cross_validate(dt, X_train, y_train)
scores

{‘fit_time’: array([0.004951 , 0.00413013, 0.00400686, 0.00574923, 0.00399709]), ‘score_time’: array([0.00150466, 0. , 0.00100374, 0.00100231, 0.00060654]), ‘test_score’: array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}

1
np.mean(scores['test_score'])

np.mean(scores[‘test_score’])

그런데, 여기서 한 가지 유의할 점은 그냥 cross_validate()는 training set을 섞지 않는다. 그냥 처음 입력한 데이터를 가지고 그대로 validation set을 만들어 학습을 하게 된다. 이러한 경우 만약 처음 입력한 데이터가 크기 순으로 정렬되어 있다거나 하면 각 validation set에 대한 학습 결과가 많이 차이가 날 수도 있을 것이다. 따라서 우리는 training set을 섞은 다음 set을 만들어 학습하는 것이 좋을 것이다. 이를 위해 우리는 StratifiedKFold를 사용해볼 것이다. 우리가 방금 했던 cross validation을 조금 더 자세하게 작성하면 아래와 같이 작성할 수 있다.

1
2
3
from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, X_train, y_train, cv=StratifiedKFold())
np.mean(scores['test_score'])

0.855300214703487

이제 세부적으로 파라미터 설정을 해서 training set을 섞어보도록 하자. 그리고 k를 10으로도 지정해 보았다. 이렇게 변경한 결과 아까보다 조금 더 성능이 올라간 것을 확인할 수 있다.

1
2
3
splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, X_train, y_train, cv=splitter)
np.mean(scores['test_score'])

0.8574181117533719

그리드 서치

그런데 한 가지 문제가 있다. 모델을 살펴보다 보면 수많은 하이퍼파라미터들이 존재한다. 그런데 이걸 직접 하나하나 조절하면서 어떤 모델이 가장 score이 좋은지 비교하는 건 거의 불가능할 것이다. 물론 for 문을 사용해서 할 수도 있겠지만 조금 많이 번거롭다.

이를 위해서 그리드 서치(Grid Search)라는 것이 존재한다. 이것은 하이퍼파라미터 탐색과 cross validation을 동시에 수행해서 최적의 결과를 도출해준다. 이를 바탕으로 방금 살펴본 모델의 최적값을 찾아보기로 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
from sklearn.model_selection import GridSearchCV
params = {
    'criterion': ['gini', 'entropy'],
    'max_depth': [None, 10, 20, 30],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'max_features': [None, 'sqrt', 'log2'],
    'splitter': ['best', 'random']
}
grid_search = GridSearchCV(DecisionTreeClassifier(random_state=42), param_grid=params, n_jobs=-1)
grid_search.fit(X_train, y_train)
best_model = grid_search.best_estimator_
best_model.score(X_train, y_train)

0.996921300750433

1
best_model.score(X_test, y_test)

0.8653846153846154

그리고 최적의 하이퍼파라미터도 다음과 같이 구할 수 있다.

1
grid_search.best_params_

{‘criterion’: ‘entropy’, ‘max_depth’: None, ‘max_features’: ‘sqrt’, ‘min_samples_leaf’: 1, ‘min_samples_split’: 2, ‘splitter’: ‘best’}

마지막으로 최적의 교차 검증 점수는 다음과 같다.

1
np.max(grid_search.cv_results_['mean_test_score'])

0.8626145702228474

### 랜덤 서치 그런데 grid search에서 한 가지 문제가 있다. 무식하게 모든 경우의 수를 다 때려 넣어 가장 좋은 결과가 나오는 조합을 찾는 것이기에 만약 모든 하이퍼파라미터를 다 사용하고, 데이터의 크기가 매우 크다면 학습을 하는 데에 소요되는 시간이 너무나 오래 걸린다. 물론 많은 돈이 있다면 조금 해결될 수 있겠지만 우리는 4천만원짜리 그래픽 카드를 사기에는 돈이 없다. 따라서 조금이나마 학습을 하는 것을 줄이기 위해 모든 경우의 수를 때려넣는 것 대신 랜덤으로 sampling해서 최적의 하이퍼파라미터를 찾는 방법을 사용할 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from scipy.stats import uniform, randint
from sklearn.model_selection import RandomizedSearchCV

params = {
    'criterion': ['gini', 'entropy'],
    'max_depth': randint(20, 50),
    'min_samples_split': randint(2, 25),
    'min_samples_leaf': randint(1, 25),
    'max_features': [None, 'sqrt', 'log2'],
    'splitter': ['best', 'random']
}

grid_search = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), n_iter=100, param_distributions=params, n_jobs=-1)
grid_search.fit(X_train, y_train)
best_model = grid_search.best_estimator_
best_model.score(X_train, y_train)

0.8870502212815086

1
grid_search.best_params_

{‘criterion’: ‘gini’, ‘max_depth’: 22, ‘max_features’: None, ‘min_samples_leaf’: 24, ‘min_samples_split’: 8, ‘splitter’: ‘best’}

1
np.max(grid_search.cv_results_['mean_test_score'])

0.8680002961427409

1
best_model.score(X_test, y_test)

0.8576923076923076

결과를 봤을 때, grid search보다 random search의 score이 조금 낮아 보여 조금이라도 score를 높이기 위해 항상 grid search를 사용하는 것이 낫지 않느냐고 물어볼 수 있을 것이다. 그러나 나중에 학습을 하다 보면 학습을 12시간 이상 돌리게 될 때, 아 이건 도저히 안 되겠다는 생각이 들면서 random search를 사용하게 될 때가 빈번하게 있었다. 따라서 grid search는 시간과 여유가 있다면 하면 좋으나 그걸 기다릴 수 없다면 random search를 사용하는 것도 좋은 선택이 될 수 있다.

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