지금까지는 훈련 Set으로 모델을 훈련시킨 후, 테스트 Set으로 모델의 성능을 평가하였다. 이 때, 테스트 Set을 이용해 모델의 성능을 평가하는 이유는 모델의 일반화 성능을 가늠해보기 위함이다. 그러나, 계속해서 동일한 테스트 Set으로 성능을 평가하다보면, 모델은 점점 테스트 Set에 맞추어질 것이다. 즉, 모델의 일반화 성능이 보장되지 않을 수도 있다는 것이다.
정확한 일반화 성능을 평가하기 위해선, 테스트 Set을 마지막에 한 번만 사용해야 한다. 하지만, 테스트 Set을 처음에 사용하지 않으면, 모델의 과대/과소 적합 여부를 확인할 수 없게 된다. 그러므로 이러한 문제를 해결하기 위해, 앞으로는 훈련 Set의 일부를 검증 Set으로 사용할 것이다.
검증 Set은 쉽게 말해 모델의 성능을 중간 평가하기 위한 데이터 Set으로, 과대/과소 적합 방지, 모델 성능 예측, 하이퍼파라미터 튜닝 등의 목적으로 사용될 수 있다.
지난 포스팅에서 전체 데이터의 20%를 테스트 Set으로, 나머지 80%를 훈련 Set으로 사용하였다. 이번에도 비슷하게 훈련 Set에서 다시 20%를 떼어내어, 검증 Set을 만들어 볼 것이다.
① Pandas 라이브러리를 이용하여 CSV 데이터를 불러온다.
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
② 'class' 열은 타깃 데이터로, 나머지 열은 입력 데이터로 사용한다.
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()
③ 훈련 Set과 테스트 Set을 구분한다.
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)
④ 훈련 Set의 일부를 떼어내어 검증 Set을 만든다.
sub_input, val_input, sub_target, val_target = train_test_split(train_input, train_target, test_size=0.2, random_state=42)
⑤ 훈련 Set을 이용하여 모델을 훈련시킨 후, 훈련 Set과 검증 Set을 이용해 모델의 성능을 평가한다.
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))
지난 포스팅에서와 비슷하게, 이번 모델 역시 과대 적합된 양상을 보이고 있다.
검증 Set을 만들면, 훈련 Set의 데이터가 줄어들기 때문에, 모델의 성능이 낮아질 가능성이 있다. 따라서, 검증 Set을 사용하면서도, 모델이 기존의 훈련 Set을 모두 학습할 수 있도록 Cross Validation(교차 검증)을 수행한다.
교차 검증은 훈련 Set에서 검증 Set을 떼어 내어 모델을 훈련하는 과정을 K번 반복하고, 각각의 경우에서 얻어진 점수의 평균을 검증 점수로 사용한다. 우리는 이러한 방법을 K-Fold Cross Validation이라 부른다.
위 그림에서는 3-Fold 교차 검증을 나타내었지만, 일반적으로는 5-Fold 교차 검증이나 10-Fold 교차 검증이 자주 사용된다. 특히, 10-Fold 교차 검증은 전체 데이터의 10% 정도만 검증 Set으로 사용하면서도, 모든 데이터를 검증 Set에 활용함으로써, 안정된 검증 점수를 제공한다.
이제 실제로 교차 검증을 수행하는 방법에 대해 알아보자. 교차 검증을 수행해야 할 때에는 사이킷런의 cross_validate()
메서드를 사용할 수 있다. 이 때, cross_validate()
메서드에는 검증 Set을 떼어내기 전의 전체 훈련 Set을 전달해야 한다.
from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)
cross_validate()
메서드는 기본적으로 5-Fold 교차 검증을 수행한다. (K 값을 변경하고 싶다면, cv 매개변수를 사용하면 된다.) 여기서 fit_time과 score_time은 모델을 훈련 및 검증하는 데에 걸린 시간을 의미하며, test_score는 각각의 Fold에서 측정된 성능을 의미한다. 즉, test_score의 평균이 곧, 최종 검증 점수가 되는 것이다.
import numpy as np
print(np.mean(scores['test_score'])) # 0.855300214703487 출력
최적의 하이퍼파라미터를 찾는 일은 모델의 성능 향상에 있어 매우 중요하다. 실제로 지난 시간에 만든 결정 트리 모델도, max_depth의 값에 따라 성능이 변화하였음을 기억할 것이다.
그러나 하이퍼파라미터를 조절하여 최적의 모델을 만드는 작업은 생각만큼 간단하지 않다. 예를 들어, 결정 트리 모델에서 최적의 max_depth 값을 찾는 과정을 생각해보자. 최적의 max_depth 값은 min_samples_split(노드 분할을 위해 필요한 Sample의 최소 개수)과 min_impurity_decrease(노드 분할을 위해 필요한 최소 불순도 감소량)에 영향을 받기 때문에, 이 두 값을 함께 조절해가면서 최적의 max_depth 값을 찾아야 한다.
무려 3개의 변수 값을 조절해야하기 때문에 직접 구현해야 했다면, 3중 for문을 사용했어야 할 것이다. (매개 변수의 개수가 더 많아진다면, 이에 따라 for문의 중첩 횟수도 점점 늘어날 것이다.)
다행히 하이퍼파라미터 튜닝을 보다 간편하게 수행할 수 있도록, 사이킷런에선 GridSearchCV 클래스를 제공하고 있다. 참고로, 클래스 이름에 CV(Cross Validation)가 붙은 이유는 GridSearchCV 클래스에서 교차 검증도 함께 수행해주기 때문이다. GridSearchCV 클래스가 교차 검증을 수행하는 이유는, 당연히 하이퍼파라미터의 변화에 따른 성능을 비교하여 최적의 값을 알아내기 위함이다.
먼저 GridSearchCV 클래스의 사용 방법에 대해 알아보기 위해, 기본 결정 트리 모델에서 최적의 min_impurity_decrease 값을 찾아보기로 하자.
① 매개변수의 이름을 Key로, 값 리스트를 Value로 갖는 딕셔너리를 생성한다.
from sklearn.model_selection import GridSearchCV
params = {'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}
② GridSearchCV 객체에 탐색의 대상이 되는 모델과 딕셔너리를 전달한다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
③ 모델 훈련이 완료되면, GridSearchCV 클래스는 검증 점수가 가장 높은 모델의 매개변수 조합을 이용하여, 자동으로 모델을 다시 훈련시킨다.
dt = gs.best_estimator_
print(dt.score(train_input, train_target)) # 0.9615162593804117 출력
④ Grid Search로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어 있다.
print(gs.best_params_) # {'min_impurity_decrease': 0.0001} 출력
⑤ 각각의 min_impurity_decrease 값에 대한 검증 점수는 cv_result_ 속성의 mean_test_score 키를 통해 확인할 수 있다.
print(gs.cv_results_['mean_test_score'])
GridSearchCV의 사용법에 대해 충분히 알아보았으니, 배운 내용을 바탕으로 min_samples_split과 min_impurity_decrease, max_depth의 최적 값을 찾아보자.
① params 딕셔너리를 생성한다.
params = {
'min_impurity_decrease': np.arange(0.0001, 0.001, 0.0001),
'max_depth': range(5, 20),
'min_samples_split': range(2, 100, 10)
}
② GridSearchCV 모델을 생성 및 훈련한다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)
③ best_params_ 속성을 이용하여, Grid Search로 찾은 매개변수의 최적 값을 확인해보자.
print(gs.best_params_)
④ cv_result_ 속성을 이용하여 최상의 검증 점수도 확인해보자.
print(np.max(gs.cv_results_['mean_test_score']))
확실히 GridSearchCV 클래스를 활용하니, 최적의 매개변수를 찾는 일이 매우 수월해졌다. 그러나 아직도 문제가 하나 있는데, 바로 탐색할 매개변수의 간격 및 범위를 설정할 마땅한 기준이 없다는 것이다. 앞서 설정한 매개변수의 간격이나 범위 역시, 특별한 근거 없이 임의로 설정된 값일 뿐이다.
기준이 없다보니 다양한 값을 고려해야 하는데, Grid Search의 실행 시간은 매우 긴 편이기 때문에, 무작정 범위를 넓히거나 간격을 좁히기도 어렵다. 실제로 위에서의 계산만 보더라도, 'min_impurity_decrease'의 값 9개, 'max_depth'의 값 15개, 'min_samples_split'의 값 10개에 대해 5-Fold 교차 검증을 수행하므로, 연산 횟수가 무려 9 * 15 * 10 * 5 = 6750
회나 된다. 아무래도 매개변수의 범위를 넓히면서도, 효율적으로 최적 값을 탐색할 수 있는 새로운 방법이 필요할 것 같다.
이러한 상황에서 사용할 수 있는 방법이 바로 Random Search이다. Random Search는 매개변수의 값 리스트가 아닌, 매개변수를 Sampling 할 수 있는 확률 분포 함수를 입력받는다. 이 때, 전달해야 할 확률 분포 함수는 Scipy 라이브러리의 uniform()
메서드와 randint()
메서드를 사용하여 쉽게 생성할 수 있다.
여기서 uniform()
메서드와 randint()
메서드는 각각 정해진 범위 내의 실수 또는 정수를 랜덤하게 선택하는 역할을 수행한다. 이제 Random Search를 활용하여 보다 넓은 범위에서 최적 값을 탐색해보자.
① params 딕셔너리의 Value에 Sampling 범위(확률 분포)를 전달한다.
from scipy.stats import uniform, randint
params = {
'min_impurity_decrease': uniform(0.0001, 0.001),
'max_depth': range(20, 50),
'min_samples_split': randint(2, 25),
'min_samples_leaf': randint(1, 25)
}
② 이번에는 GridSearchCV 모델이 아닌, RandomizedSearchCV 모델을 생성 및 훈련한다.
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)
③ best_params_ 속성을 이용하여, Random Search로 찾은 매개변수의 최적 값을 확인해보자.
print(gs.best_params_)
④ cv_result_ 속성을 이용하여 최상의 검증 점수도 확인해보자.
print(np.max(gs.cv_results_['mean_test_score']))
⑤ 검증이 완료되면, 전체 훈련 Set으로 훈련된 모델이 best_estimator_ 속성에 저장된다.
dt = gs.best_estimator_
print(dt.score(test_input, test_target)) # 0.86 출력
이처럼 검증 Set을 활용하면, 모델의 일반화 성능이 보장되며, 하이퍼파라미터 튜닝이 보다 간편해진다.