티스토리 뷰

지금까지 실습을 진행하면서 테스트 세트에서 얻어진 점수를 보고 해당 모델을 실전에 투입하면 이 정도 성능을 기대할 수 있겠다. 하는 일반화 성능을 기대할 수 있다고 생각했다. 

 

그럼, 결정 트리의 max_depth와 같은 매개변수를 이런 저런 값으로 넣어 최적의 값을 찾아내려면, 테스트 세트의 점수를 반복적으로 확인해야 한다.

 

그러나, 테스트 세트를 자꾸 사용하여 성능을 확인하다보면 점점 해당 테스트 세트에 모델이 맞춰지는 셈이다. 

 

그렇기 때문에 테스트 세트로 일반화 성능을 올바르게 예측하기 위해서는 가능한 한 테스트 세트를 사용하지 않아야 한다.

 

그러면 어떻게 해야 max_depth와 같은 매개변수를 사용한 하이퍼파라미터(사람이 정해야 하는 변수들)을 튜닝할 수 있을까? 게다가 결정 트리는 이외에도 테스트해 볼 매개변수가 많다.

 

이를 위해 훈련 세트를 한번 더 나눠 사용하는 검증 세트가 필요하다.


검증 세트

직전 포스팅에서 데이터의 20%를 테스트 데이터로 만든 것과 같이 80%의 훈련 세트 중 다시 20%를 떼어서 검증 세트를 만드는 것이다. 

 

훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가한다. 이런 식으로 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고른다. 

 

그 다음 이 매개변수를 사용하여 훈련 세트와 검증 세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련시킨다. 

 

이렇게 하면 마지막 테스트 세트에서 최종 점수를 평가하고 실전에 투입했을 때, 테스트 세트의 점수와 비슷한 성능을 기대할 수 있다.

 

바로 한번 확인해보자.

 

데이터는 직전 포스팅과 동일한 와인 데이터로 진행한다.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression


from sklearn.tree import DecisionTreeClassifier

# 데이터 준비
wine = pd.read_csv('https://bit.ly/wine_csv_data')

# 맨 위 5개 데이터 확인
print(wine.head())
#  alcohol  sugar    pH  class
# 0      9.4    1.9  3.51    0.0
# 1      9.8    2.6  3.20    0.0
# 2      9.8    2.3  3.26    0.0
# 3      9.8    1.9  3.16    0.0
# 4      9.4    1.9  3.51    0.0

# 데이터프레임의 각 열의 데이터 타입과 누락된 데이터가 있는지 확인
wine.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 6497 entries, 0 to 6496
# Data columns (total 4 columns):
#  #   Column   Non-Null Count  Dtype
# ---  ------   --------------  -----
#  0   alcohol  6497 non-null   float64
#  1   sugar    6497 non-null   float64
#  2   pH       6497 non-null   float64
#  3   class    6497 non-null   float64
# dtypes: float64(4)
# memory usage: 203.2 KB

# 데이터 열에 대한 간단한 통계 값 출력
print(wine.describe())
#            alcohol        sugar           pH        class
# count  6497.000000  6497.000000  6497.000000  6497.000000
# mean     10.491801     5.443235     3.218501     0.753886
# std       1.192712     4.757804     0.160787     0.430779
# min       8.000000     0.600000     2.720000     0.000000
# 25%       9.500000     1.800000     3.110000     1.000000
# 50%      10.300000     3.000000     3.210000     1.000000
# 75%      11.300000     8.100000     3.320000     1.000000
# max      14.900000    65.800000     4.010000     1.000000

# input 과 target 데이터로 나눔
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

# test_size = 0.2 -> 샘플 갯수가 충분히 많으므로, 20% 정도만 테스트 세트로 나눈다.
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size = 0.2, random_state=42)

 

이제 나눈 데이터를 한번 더 나눠 검증 세트를 만든다.

# 다시 훈련 세트를 train_test_split 에 넣어 훈련 세트와 검증 세트를 나눈다.
sub_input, val_input, sub_target, val_target =  train_test_split(train_input, train_target, test_size = 0.2, random_state=42)

 

이제 해당 훈련 세트와 검증 세트의 결정 트리 모델을 평가해보자.

dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)

print(dt.score(sub_input, sub_target))
# 0.9971133028626413

print(dt.score(val_input, val_target))
# 0.864423076923077

아무런 매개변수를 지정해주지 않았기 때문에 확실히 데이터가 과대적합되었다.

 

검증을 거치며 매개변수를 바꿔 더 좋은 모델을 찾아야 한다.


교차 검증

검증 세트를 만드느라 훈련 세트가 20% 가량 줄었다. 보통 많은 데이터를 훈련에 사용할수록 좋은 모델이 만들어진다. 그렇다고 검증 세트를 너무 조금 떼어 놓으면 검증 점수가 들쭉날쭉하고 불안정할 것이다. 

 

이럴 때 교차 검증을 이용하면 안정적인 검증 점수를 얻고 훈련에 더 많은 데이터를 사용할 수 있다.

 

교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복한다. 그 다음 이 점수를 평균하여 최종 검증 점수를 얻는다.

 

이를 그림으로 나타내면 아래와 같다.

출처:&nbsp;https://gilbertlim.github.io/machine%20learning/ml_cross_validation/

훈련 세트를 몇개의 부분으로 나누냐에 따라 k의 값이 달라진다.

 

보통의 경우 5-폴드 교차 검증이나 10-폴드 교차 검증을 많이 사용하는데, 이렇게 하면 데이터의 80% ~ 90%까지 훈련에 사용할 수 있다.

 

사이킷런에는 cross_validate()라는 교차 검증 함수가 있다. 

 

아래 코드와 같이 평가할 모델 객체를 첫번째 매개변수로 전달한 뒤, 위 코드처럼 직접 검증 세트를 떼어내는 것이 아닌 훈련 세트 전체를 매개변수로 넣어주면 된다.

from sklearn.model_selection import cross_validate

# 5-폴드 교차 검증
scores = cross_validate(dt, train_input, train_target)

print(scores)
# {
#   'fit_time': array([0.00600505, 0.00700665, 0.00600553, 0.006006  , 0.00696492]),
#   'score_time': array([0.00100136, 0.00100017, 0.00100088, 0.        , 0.00100112]),
#   'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])
#   }

# 검증 폴드의 평균 점수
print(np.mean(scores['test_score']))
# 0.855300214703487

교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있다.

 

한 가지 중요한 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다.  위에서 train_test_split() 함수로 전체 데이터를 섞은 후 훈련 세트를 준비했기 때문에 따로 섞을 필요가 없지만, 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기(splitter)를 지정해줘야 한다.

 

cross_validate() 함수는 기본적으로 회귀 모델의 경우 KFold 분할기를 사용하고 분류 모델의 경우 타깃 클래스를 골고루 나누기 위해 StratifiedKFold를 사용한다. 

 

만약 훈련 세트를 섞은 뒤, 10-폴드 교차 검증을 수행하려면 아래와 같이 코드를 짜야한다.

# 훈련 세트를 섞은 뒤, 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

하이파라미터 튜닝

 머신러닝이 학습하는 파라미터를 모델 파라미터라고 부르고, 모델이 학습할 수 없어서 사용자가 지정해야 하는 파라미터를 하이퍼파라미터라고 한다고 했다. 

 

이제 교차 검증이 무엇인지 알았으니, 이 하이퍼 파라미터를 조금씩 바꿔가면서 모델을 훈련하고 최적의 하이퍼파라미터 값을 찾는 과정을 알아보자.

 

최적의 하이퍼파라미터를 찾기 위해서는 반복하여 값을 수정하며 교차검증을 돌려야 하는데, 반복문을 직접 구성하여 해도 되지만 사이킷런에서 제공하는 도구를 사용하는 편이 훨씬 간편하게 할 수 있다.

 

사이킷런에서는 기본적으로 이런 반복적인 교차 검증을 위한 그리드 서치를 제공한다. 

 

GridSearchCV 클래스는 친절하게 하이퍼파리미터 탐색과 교차 검증을 한번에 수행한다. 따라서 별도로 cross_validate()를 호출할 필요도 없다.

 

코드로 한번 확인해보자.

from sklearn.model_selection import GridSearchCV
params = {'min_impurity_decrease': [0.0001, 0.0002, 0.0003, 0.0004, 0.0005]}

# min_impurity_decrease 값을 바꿔가며, 5번 실행한다.
# 또한 GridSearchCV의 cv 매개변수 기본값이 5이기 때문에 min_impurity_decrease 마다 5 - 폴드 교차 검증을 실행한다.
# n_jobs -> 병렬 실행에 사용할 CPU 코어 수를 지정한다. 기본값은 1이지만, -1로 지정하면 시스템에 있는 모든 코어를 사용한다.
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)

# 25개의 모델의 검증이 끝나면, 모델 중 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다.
# 해당 모델은 gs 객체의 best_estimator_ 속성에 저장되어 있다.
gs.fit(train_input, train_target)

dt = gs.best_estimator_
print(dt.score(train_input, train_target))
# 0.9615162593804117

위에서는 하이퍼파라미터인 min_impurity_decrease를 0.0001 부터 0.0005 까지 리스트를 만들어 GridSearchCV를 돌렸다.

 

또한 아래와 같이 그리드 서치로 찾은 최적의 매개변수 값이나 교차 검증의 평균 점수, 최고점 등을 확인할 수 있다.

# 그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어 있다.
print(gs.best_params_)
# {'min_impurity_decrease': 0.0001}

# 각 매개변수에서 수행한 교차 검증의 평균 점수는 mean_test_score 키에 저장된다.
print(gs.cv_results_['mean_test_score'])
# [0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]

# 넘파이의 argmax() 를 사용하여가장 큰 값의 index를 추출할 수도 있다.
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])
# {'min_impurity_decrease': 0.0001}

위 코드와 하이퍼 파라미터를 그리드 서치로 찾아내는 과정을 다시 정리해보면 아래와 같다.

 

 1. 탐색할 매개변수를 지정한다.

 

 2. 그 다음 훈련 세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다. 이 조합은 그리드 서치 객체에 자동으로 저장된다.

 

 3. 그리드 서치는 최상의 매개변수에서 (교차 검증에서 사용한 훈련 세트가 아닌) 전체 훈련 세트를 사용하여 최종 모델을 훈련한다. 이 모델 또한 자동으로 그리드 서치 객체에 저장된다.


이제, 조금 더 복잡한 매개변수 조합을 사용해보자.

 

결정 트리에서 min_impurity_decrease는 노드를 분할하기 위한 불순도 감소 최소량을 지정한다. 여기에 max_depth로 트리의 깊이를 제한하고, min_samples_split으로 노드를 나누기 위한 최소 샘플 수도 골라보자.

# 조금 더 복잡한 매개변수 조합을 탐색해보자
# 넘파이의 arange, range를 사용하여 좀 더 편하게 배열을 만들 수 있다.
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)
}
# 따라서 이 매개변수 조합으로 수행할 교차 검증 횟수는 9 x 15 x 10 = 1350회이다.
# 기본 5-폴드 교차 검증을 수행하기 때문에 총 횟수는 6750회
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

print(gs.best_params_)
#b {'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}

print(np.max(gs.cv_results_['mean_test_score']))
# 0.8683865773302731

위와 같이 3개 종류의 하이퍼 파라미터를 셋팅하고 돌려도, GridSearchCV가 알아서 최적의 파라미터 값을 찾아준다.

(매ㅡㅡㅡㅡ우 편리)


 

랜덤 서치

위 GridSearchCV 클래스를 사용하니 매개변수를 일일이 바꿔가며 교차 검증을 수행하지 않고 원하는 매개변수 값을 나열하면 자동으로 교차 검증을 수행하니 매우 편리하게 하이퍼 파라미터의 최적 값을 찾아서 사용할 수 있다. 

 

하지만 위 하이퍼파라미터 조합을 0.0001이나 1 간격으로 지정한 데에는 어떤 근거도 없다. 매개변수 조합을 임의로 선택하지 않고 확률분포에 근거하여 랜덤하게 뽑아서 사용하는 방식이 바로 랜덤 서치이다.

 

랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아닌, 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다.

파이썬의 scipy.stats 에 있는 uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑으며, 이를 '균등 분포에서 샘플링한다' 라고 말한다.

 

from scipy.stats import uniform, randint

# uniform: start ~ end 까지의 랜덤 실수(균등 분포)
# randint: start ~ end 까지의 랜덤 정수(균등 분포)
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 객체를 불러와서 사용하면 된다. 

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)

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

 

테스트 세트의 점수는 검증 세트에 대한 점수보다 조금 작은 것이 일반적이며, 테스트 세트의 점수가 아주 만족스럽지는 않지만, 충분히 다양한 매개변수를 테스트하여 얻은 결과이니 실전에서 활용했을 때 거의 차이가 안날 것이다.

 

모델을 교차검증하여 최적의 매개변수를 찾는 과정은 머신 러닝에 있어 매우 중요한 과정이니, 반복 숙달하자.

 

 

끝!!

Comments