이전 글

이 전에 학습용 데이터셋을 기준으로 잘 맞게 학습시켜 만든 모델을 과대적합된 모델이라 하였다. 하지만 테스트용 데이터셋으로만 평가하면 결국 테스트용 데이터셋에 잘 맞는 모델이 만들어지는 것이 아닌가?

테스트용 데이터셋은 모델을 구현 후 마지막에 딱 한 번만 사용하는 것이 좋다. 그렇다면 max_depth를 이용한 하이퍼파라미터 튜닝은 어떻게 해야하는가?

여기에서 하이퍼파라미터란(Hyper Parameter)? 모델 학습 과정에 반영되는 값, 학습 시작 전에 미리 조정

파라미터(Parameter): 모델 내부에서 결정되는 변수, 데이터로부터 학습 또는 예측되는 값

테스트용 데이터셋을 사용하지 않고,이를 측정하는 간단한 방법은 바로 학습용 데이터셋을 또 나누는 것이다. 이 데이터를 검증 세트(validation set)이라 한다.

우선 이 전에 전체 데이터셋 중 20%는 테스트용 데이터셋으로, 80%는 학습용 데이터셋으로 나누었다. 이 학습용 데이터셋에서 20%를 검증 세트로 만든다.

학습용 데이터셋에서 모델을 학습시키고, 검증 데이터셋으로 모델을 평가한다. 그 다음 가장 적합한 매개변수를 찾은 후에 학습용 데이터셋과 검증 데이터셋을 합친다.

합친 전체 학습용 데이터셋을 이용하여 적합한 매개변수를 이용해 다시 모델을 학습시킨다. 그리고 마지막에 테스트용 데이터셋에서 최총적으로 평가한다.

데이터 준비하기

#6497개의 와인 샘플 데이터
#피처: 알코올 도수, 당도, PH값
#class: 0은 레드와인, class:1은 화이트 와인
import pandas as pd

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

데이터를 종속변수 X와 독립변수 y로 나누기

data_X = wine[['alcohol', 'sugar', 'pH']].to_numpy()
data_y = wine['class'].to_numpy()

학습용 데이터셋과 테스트용 데이터셋으로 분리

from sklearn.model_selection import train_test_split

train_X, test_X, train_y, test_y = train_test_split(data_X, data_y, test_size=0.2, random_state=42)

학습용 데이터셋에서 다시 8:2 비율로 학습용 데이터셋과 검증용 데이터셋을 나눈다.

sub_X, val_X, sub_y, val_y = train_test_split(train_X, train_y, test_size=0.2, random_state=42)

이제 검증용 데이터셋과 서브 학습용 데이터셋을 이용하여 모델을 구현 후 평가한다.

from sklearn.tree import DecisionTreeClassifier

dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_X, sub_y)

print(dt.score(sub_X, sub_y))
print(dt.score(val_X, val_y))
#출력값: 0.9971133028626413
#출력값: 0.864423076923077

해당 모델은 학습용 데이터셋에 과대적합되어 있다. 그 전에 검증 세트에 관해 알아야 할 것이 있다.

보통 학습용 데이터셋에 많은 데이터를 사용할수록 좋은 모델이 만들어진다. 하지만 검증 셋을 너무 조금 떼어놓으면 검증 점수가 불안정해진다.

이럴 때 교차 검증(cross validation) 을 이용하여 안정적인 검증 점수를 얻고, 학습용 데이터셋에 더 많은 데이터를 사용할 수 있다.

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


다음은 3-폴드 교차 검증의 그림이다.

보통은 5-폴드 교차 검증이나, 10-폴드 교차 검증을 많이 사용한다. 그렇게 하면 데이터의 80~90%까지 학습용 데이터셋에 사용할 수 있다.

검증 셋은 줄어들지만, 각 폴드에서 계산된 검증 점수를 평균하기 때문에 안정적인 점수가 도출됨.


사이킷런에는 cross_validate() 메서드가 있다. 이 메서드는 교차 검증 함수이다. 먼저 평가할 모델 객체를 첫 번째 매개변수로, 그 다음은 학습용 데이터셋을 매개변수로 전달한다.

from sklearn.model_selection import cross_validate

scores = cross_validate(dt, train_X, train_y)

이 함수는 fit_time, score_time, test_score 키를 가진 딕셔너리를 반환한다.

fit_time, score_time은 각각 모델을 학습하는 시간과 검증하는 시간을 의미한다. 각 키마다 5개의 숫자가 담겨져 있는데 cross_validate()는 기본적으로 5-폴드 교차 검증을 수행하기 때문이다.

test_score의 5개의 점수를 평균하여 최종적인 교차 검증의 점수를 도출할 수 있다.

import numpy as np

print(np.mean(scores['test_score']))
#출력값: 0.855300214703487

cross_validate()는 학습용 데이터셋을 섞어 폴드를 나누지 않는다. 따라서 학습용 데이터셋을 섞고싶다면 분할기(splitter)을 지정해야 한다.

분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정한다. cross_validate() 함수는 회귀 모델일 경우 기본적으로 kFold 분할기를 사용하며, 분류 모델일 경우에는 StratifiedKFold를 사용한다.

from sklearn.model_selection import StratifiedKFold

scores = cross_validate(dt, train_X, train_y, cv=StratifiedKFold())
print(np.mean(scores['test_score']))
#출력값: 0.855300214703487

만약 기본값 5-폴드 교차 검증이 아닌 10-폴드 교차 검증을 사용하고 싶다면 다음과 같이 작성한다.

splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_X, train_y, cv=splitter)
print(np.mean(scores['test_score']))
#출력값: 0.8574181117533719

하이퍼파라미터(Hyper parameter)

하이퍼파라미터는 모델이 학습할 수 없어, 사용자가 지정해야만 하는 파라미터이다.

하이퍼파라미터 튜닝법
1. 해당 라이브러리의 기본값으로 모델을 학습시킨다.
2. 검증 셋의 점수나 교차 검증을 통해 매개변수를 조금씩 변경한다.

하지만 결정 트리 모델에서 최적의 max_depth를 찾았다고 가정하자. max_depth를 최적의 값으로 고정 후 min_samples_split을 바꾸면서 min_samples_split의 최적값을 찾아야할까? 아니다.

max_depth의 최적값이 바뀌면 min_samples_split의 값도 함께 달라진다. 즉 두 매개변수를 동시에 바꿔가며 최적의 값을 찾아야 한다. 게다가 매개변수가 많아질수록 문제는 더 복잡해진다. 사이킷런에서는 그리드 서치(Grid Search)를 제공한다.

그리드 서치(GridSearch)

사이킷런의 GridSearchCV는 하이퍼파라미터 탐색과 교차 검증을 한 번에 수행한다. 별도로 cross_validate()를 호출할 필요가 없다.

사용할 결정 트리 모델에서 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]}
gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
  • gs 객체에 fit() 메서드를 호출하면 그리드 서치 객체는 결정 트리 모델이 min_impurity_decrease 값을 총 5번 바꿔가며 실행한다.
  • GridSearchCV의 cv(cross_validate)의 기본값이 5이다. 즉 5-폴드 교차 검증을 수행하는데 총 5개의 min_impurity_decrease이 있으므로, 결국 5*5=25개의 모델을 학습한다.
  • 한 번에 많은 모델을 학습시키기 때문에 n_jobs = -1 을 사용하여 cpu에 있는 모든 코어를 사용하도록 한다. (기본값은 1)
gs.fit(train_X, train_y)

아까 교차 검증에서는 최적의 하이퍼파라미터를 찾으면 검증 셋과 서브 학습 셋을 합쳐 전체 학습용 데이터셋으로 모델을 다시 만들어야 한다고 했다.

하지만 사이킷런의 그리드 서치는 학습이 끝나면 25개의 모델 중 검증 점수가 가장 높은 모델의 매개변수의 조합으로 자동으로 전체 학습용 데이터셋을 다시 모델에 학습시킨다.

이 gs 모델을 일반적인 결정 트리처럼 똑같이 사용할 수 있다.

dt = gs.best_estimator_
print(dt.score(train_X, train_y))
#출력값: 0.9615162593804117

최적의 하이퍼파라미터 출력(min_impurity_decrease)

print(gs.best_params_)

요약.

  1. 먼저 탐색할 매개변수를 지정한다.
  2. 학습용 데이터셋에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다. 이 조합은 그리드 서치 객체에 저장됨.
  3. 그리드 서치는 최상의 매개변수에서 전체 학습용 데이터셋을 사용해 다시 최종 모델을 학습시킨다. 해당 모델도 그리드 서치 객체에 저장됨.

방금은 하나의 하이퍼파라미터 튜닝에 대하여 진행하였다. 이번에는 총 3개의 하이퍼파라미터의 최적의 조합을 찾아보자.

  1. min_impurity_decrease: 최소 불순도
  2. max_depth: 트리의 깊이
  3. min_samples_split: 노드를 나누기 위한 최소 샘플 수

이번 교차 검증 횟수는 첫 번째 하이퍼파라미터 갯수: 9개, 두 번째 하이퍼파라미터 갯수: 15개, 세 번째 하이퍼파라미터 갯수: 10개,

총 9 15 10 = 1350개이다. 기본적으로 5-폴드 교차 검증이 이루어지므로 1350 * 5 = 6750 => 총 6750개의 모델이 생성된다.

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)}

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_X, train_y)
#최적의 하이퍼파라미터 값
print(gs.best_estimator_)
#출력값: DecisionTreeClassifier(max_depth=14, min_impurity_decrease=0.0004,
                       min_samples_split=12, random_state=42)
#최상의 교차 검증 점수
print(gs.best_score_)
#출력값: 0.8683865773302731

하지만 아쉬운 점이 있다면, 앞에서 탐색할 매개변수의 범위를 0.0001~0.001의 범위 이런 식으로 우리가 설정을 하여 진행하였다. 하지만 이런 범위를 설정한 특별한 근거가 없다.

랜덤 서치

매개변수의 값의 범위를 정하기 어려울 때 사용하는 것이 랜덤 서치이다. 랜덤 서치에서는 매개변수 값의 목록을 전달하는 것이 아니라, 매개변수를 샘플링할 수 있는 확률 분포 객체를 전딜한다.

우선 싸이파이(scipy) 라이브러리를 이용하여 임포트하자.

from scipy.stats import uniform, randint

uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다. 이를 균등 분포에서 샘플링한다.라고 말한다.

randint는 정숫값을 뽑고, uniform은 실수값을 뽑는다.

다음은 0에서 10사이의 범위를 갖는 randint 객체를 만든 후 10개의 숫자를 샘플링한다.

rgen = randint(0, 10)
ugen = uniform(0, 10)

print(rgen.rvs(10))  #rvs: random value sampling
print(ugen.rvs(10))  #rvs: random value sampling
#출력값: [1 1 0 3 3 3 5 2 1 3]
#출력값: [0.06826999 0.55100353 2.61135429 0.5098182  3.32309369 4.06117668
 3.00453773 4.58504145 4.12705333 7.20115658]

이제 탐색할 매개변수에 랜덤값을 넣어 최적의 매개변수를 찾는다.

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 객체를 생성 후 학습용 데이터를 학습시킨다.

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_X, train_y)

위 params에 정의된 매개변수 범위에서 총 100번(n_iter=100)을 샘플링 후 교차검증을 수행하고 최적의 매개변수 조합을 찾는다.

앞선 그리드 서치보다 훨씬 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색이 가능하다.

#최적의 파라미터
print(gs.best_params_)
#출력값: {'max_depth': 39, 'min_impurity_decrease': 0.00034102546602601173, 'min_samples_leaf': 7, 'min_samples_split': 13}
#최적의 교차 검증 점수
print(gs.best_score_)
#출력값: 0.8695428296438884
profile
노력하는 개발자

0개의 댓글