본문 바로가기

머신러닝 + 딥러닝

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

728x90

검증 세트(validation set): 테스트세트를 사용하지 않고 모델이 과대적합인지 과소적합인지 측정하기 위해 훈련세트를 또 나누어 만든 데이터

 

- 전체 데이터 중에서 60%를 훈련세트로, 20%를 테스트세트로, 20%를 검증세트로 만들어 사용한다.

 

1. 훈련세트에서 모델을 훈련하고 검증세트로 모델을 평가한다.

2. 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 골라 이 매개변수를 사용해 훈련세트와 검증세트를 합쳐 전체 훈련데이터에서 모델을 다시 훈련한다. 

3. 테스트세트에서 최종점수를 평가한다

-> 실전에 투입했을 때 테스트세트의 점수와 비슷한 성능을 기대할 수 있을 것

 

판다스로 CSV데이터를 읽고 class열을 타깃으로 사용하고 나머지 alcohol, sugar, pH열은 특성배열에 저장한다.

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

 

훈련세트와 테스트세트를 나누고 훈련세트의 입력데이터와 타깃데이터를 train_input과 train_target배열에 저장한다.

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

 

다시 train_input과 train_target을 train_test_split()함수에 넣어 훈련세트 sub_input, sub_target과 검증세트 val_input, val_target을 만든다.

test_size 매개변수를 0.2로 지정해 train_input의 약 20%를 val_input으로 만든다

sub_input, val_input, sub_target, val_target = train_test_split(train_input, train_target, test_size=0.2, random_state=42)

 

print(sub_input.shape, val_input.shape)

훈련세트와 검증세트의 크기를 확인해보니

(4157, 3) (1040, 3)

원래 5197개였던 훈련세트가 4157개의 훈련세트로, 1040개의 검증세트로 나뉜 것을 확인할 수 있다.

 

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

이렇게 만든 훈련세트와 검증세트로 모델을 만들고 평가해보았다.

0.9971133028626413
0.864423076923077

훈련세트의 점수보다 검증세트의 점수가 낮은 것으로 보아 훈련세트에 과대적합 되어있음을 볼 수 있다.

 

 

교차 검증(cross validation): 훈련세트를 훈련세트와 검증세트로 분리한 뒤, 검증세트를 사용해 검증하는 방식

- 안정적인 검증 점수를 얻고 훈련에 더 많은 데이터를 사용할 수 있다.

- 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복

 

장점 단점
○ 모든 데이터셋을 훈련과 평가에 활용할 수 있다
○ 정확도를 향상시킬 수 있다
○ 데이터 부족으로 인한 과대적합을 방지할 수 있다
○ 평가에 사용되는 데이터 편중을 막을 수 있다
○ 평가 결과에 따라 좀 더 일반화된 모델을 만들 수 있다
△  반복 횟수가 많아 모델을 훈련하고 평가하는 시간이 오래 걸린다

 

3-폴드 교차 검증(k-폴드 교차 검증, k-겹 교차 검증)

 

데이터의 80~90%까지 훈련에 사용할 수 있다. 검증 세트가 줄어들지만 각 폴드에서 계산한 검증 점수를 평균하여 안정된 점수로 생각할 수 있다.

 

사이킷런의 cross_validate()라는 교차 검증 함수를 사용해보자

이는 평가할 모델 객체를 첫 번째 매개변수로 전달하고 직접 검증 세트를 떼어내지 않고 훈련세트 전체를 이 함수에 전달한다.

from sklearn.model_selection import cross_validate
scores = cross_validate(dt, train_input, train_target)
print(scores)

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

{'fit_time': array([0.0076046 , 0.00728416, 0.00738478, 0.00718737, 0.006953  ]), 'score_time': array([0.00066686, 0.00066161, 0.00060678, 0.00061059, 0.00063467]), 'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}

fit_time: 모델을 훈련하는 시간

score_time: 모델을 검증하는 시간

 

각 키마다 5개의 숫자가 담겨 있는데 기본적으로 cross_validate() 함수는 5-폴드 교차 검증을 수행한다. cv 매개변수에서 폴드 수를 바꿀 수도 있다.

scores = cross_validate(dt , train_input , train_target ,cv=3)

↑이런 식으로!

파라미터는 (모델, 훈련데이터의 특성, 훈련데이터의 타깃, 폴드 수)이다

 

import numpy as np
print(np.mean(scores['test_score']))

교차검증의 최종점수는 test_score키에 담긴 5개의 점수를 평균하여 얻을 수 있다. -> 검증 폴드의 점수

0.855300214703487

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

 

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

 

 

사이킷런의 분할기는 교차검증에서 폴드를 어떻게 나눌지 결정해준다.

- cross_validate() 함수는 기본적으로 회귀 모델일 경우 KFold분할기를 사용

- 분류 모델일 경우 타깃 클래스를 골고루 나누기 위해 StratifiedFold를 사용한다.

from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv=StratifiedKFold())
print(np.mean(scores['test_score']))

앞서 수행한 코드와 같다

0.855300214703487

 

훈련세트를 섞은 후 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']))

n_splits 매개변수: 몇(k) 폴드 교차 검증을 할지 결정

0.8574181117533719

KFold클래스도 동일한 방식으로 사용할 수 있다.

 

결정트리의 매개변수 값을 바꿔가며 가장 좋은 성능이 나오는 모델을 찾아보자

테스트세트를 사용하지 않고 교차검증을 통해 좋은 모델을 고르면 된다!

 


하이퍼파라미터 튜닝: 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련하고, 검증세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꿔본다.

이 매개변수를 바꿔가며 모델을 훈련하고 교차검증을 수행한다.

 * 하이퍼파라미터 : 사용자 지정 파라미터

 

그리드 서치(Grid Search): 탐색할 매개변수를 나열하면 교차검증을 수행해 가장 좋은 검증 점수의 매개변수 조합을 선택하고 이 매개변수 조합으로 최종 모델을 훈련한다. 하이퍼파라미터 탐색을 자동으로 해주는 도구

- max_depth의 최적값은 min_samples_split 매개변수의 값이 바뀌면 함께 달라진다. 이 두 매개변수를 동시에 바꿔가며 최적의 값을 찾기 위한 도구

 

GridSearchCV 클래스로 하이퍼파라미터 탐색과 교차검증을 한 번에 수행할 수 있다 -> cross_validate()함수를 호출할 필요가 없다

딕셔너리 형태로 min_impurity_decrease 매개변수의 최적값을 찾아본다

 * min_impurity_decrease : 최소 불순도(default = 0.0)

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

0.0001부터 0.0005까지 0.0001씩 증가하는 5개의 값을 시도

GridSearchCV클래스에 탐색 대상 모델과 params 변수를 전달하여 그리드 서치 객체를 생성

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)

결정 트리 클래스 객체 gs에 fit() 메서드를 호출한다.

그리드 서치 객체는 결정 트리 모델 min_impurity_decrease 값을 바꿔가며 총 5번 실행한다.

n_jobs 매개변수: 병렬 실행에 사용할 CPU 코어 수를 지정, 기본값은 1, -1로 지정하면 시스템에 있는 모든 코어 사용

 

GridSearchCV의 cv매개변수 기본값은 5이다. min_impurity_decrease 값마다 5-폴드 교차 검증을 수행한다. 

gs.fit(train_input, train_target)

min_impurity_decrease 값마다 5-폴드 교차 검증을 수행 -> 5 x 5 = 25개의 모델을 훈련

 

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

best_estimator_ 속성: 훈련이 끝나면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련세트에서 다시 모델을 훈련한다, 최고 점수의 모델을 찾아준다

0.9615162593804117

 

print(gs.best_params_)

best_params_ 속성: 최적의 매개변수, 최고 점수를 낸 파라미터

{'min_impurity_decrease': 0.0001}

여기에서는 0.0001이 가장 좋은 값으로 선택되었다.

 

print(gs.cv_results_['mean_test_score'])

cv_results_ 속성: 각 매개변수에서 수행한 교차검증의 평균 점수

[0.86819297 0.86453617 0.86492226 0.86780891 0.86761605]

첫 번째 값(0.0001)이 가장 큰 것을 확인할 수 있다

-> 수동으로 고르지 않고 argmax() 함수를 사용해 가장 큰 값의 인덱스 추출 가능, 이 인덱스를 사용해 params 키에 저장된 매개변수 추출 가능, 이 값이 최상의 검증 점수를 만든 매개변수 조합이다

best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(gs.cv_results_['params'][best_index])

앞에서 출력한 gs.best_params_와 동일

{'min_impurity_decrease': 0.0001}

 

조금 더 복잡한 매개변수 조합을 탐색해보자

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) arange() 함수: 첫 번째 매개변수 값에서 시작해 두 번째 매개변수 값에 도달할 때까지 세 번째 매개변수 값만큼 증가 시킨다.

 -> 0.0001에서 시작해 0.0001만큼씩 증가시켜 0.001까지 더한다.

 

2) range() 함수: 정수만 사용 가능, max_depth를 5에서 20까지 1씩 증가하면서 15개의 값을 만든다. min_samples_split은 2부터 100까지 10씩 증가시켜 10개의 값을 만든다

 - min_samples_split : 나무 성장을 멈추기 위한 임계치

 

즉, 이 매개변수로 수행할 교차 검증 횟수는 9 * 15 * 10 = 1350개이다. 기본 5-폴드 교차 검증을 수행하므로 만들어지는 모델의 수가 6750개나 된다.

 

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

print(gs.best_params_)

그리드 서치를 수행하고 최상의 매개변수 조합을 확인해보면

{'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}

 

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

최상의 교차 검증 점수 확인

0.8683865773302731

GridSearchCV 클래스를 사용하니 매개변수를 일일이 바꿔가며 교차 검증을 수행하지 않고 원하는 매개변수 값을 나열하면 자동으로 교차 검증을 수행해서 최상의 매개변수를 찾을 수 있다.

 

 

랜덤 서치(Random Search) : 하이퍼파라미터의 값을 랜덤하게 넣어보고 그 중 우수한 값을 보인 하이퍼파라미터를 활용해 모델을 생성한다

http://www.jmlr.org/papers/volume13/bergstra12a/bergstra12a.pdf

- 그리드서치는 딕셔너리에 지정한 값을 일일이 다 탐색해야한다. 임의로 정한 값이라 어떤 값이 효과적인지 알 수 없고, 입력이 되었으니 한 번씩 모델이 생성되어야한다.

- 랜덤서치는 랜덤하게 숫자를 넣은 뒤, 정해진 간격 사이에 위치한 값들에 대해서도 확률적으로 탐색이 가능하다. 최적 하이퍼파라미터를 빨리 찾을 수 있다.

 

from scipy.stats import uniform, randint

rgen = randint(0, 10)
rgen.rvs(10)

싸이파이 stats 서브 패키지에 있는 uniform과 randint 클래스는 주어진 범위에서 고르게 값을 뽑는다. -> 균등 분포에서 샘플링한다.

 

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

rgen = randint(0, 10) # 0부터 10까지의 범위
rgen.rvs(10)

rvs는 랜덤 샘플을 생성해준다. 무작위로 표본을 만들 때 사용한다.

rvs(size, random_state)

 - size: 표본 생성 시 생성될 표본의 크기

 - random_state: 표본 생성 시 사용되는 시드값

array([5, 0, 7, 0, 2, 9, 5, 1, 1, 3])

 

ugen = uniform(0,1) # 0부터 1까지의 범위
ugen.rvs(10)

uniform 클래스도 동일한 사용법으로, 0부터 1 사이에 10개의 실숫값을 추출해본다.

array([0.80014573, 0.01414502, 0.61798293, 0.83862562, 0.62798304,
       0.82742719, 0.98974063, 0.01515137, 0.25313545, 0.27991184])

 

min_samples_leaf 매개변수 : 리프노드가 되기 위한 최소 샘플의 개수

 * 어떤 노드가 분할하여 만들어질 자식 노드의 샘플 수가 이 값보다 작을 경우 분할하지 않는다

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

min_impurity_decrease : 0.0001에서 0.001 사이의 실숫값을 샘플링

max_depth : 20부터 50까지의 정수를 샘플링

min_samples_split : 2부터 25까지 정수를 샘플링

min_samples_leaf : 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_input, train_target)

n_iter 매개변수 : 샘플링 횟수를 저장, 여기에서는 100번을 샘플링하여 교차 검증을 수행했다.

 

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

최적의 모델은 전체 훈련세트로 훈련되어 best_estimator_ 속성에 저장되어 있다.

이 모델을 최종 모델로 결정하고 테스트세트의 성능을 확인해본다.

0.86

 

 

 

∴ 테스트 세트 점수는 검증 세트에 대한 점수보다 조금 작은 것이 일반적이다.

수동으로 매개변수를 바꾸지 않고 그리드 서치나 랜덤 서치를 이용하면 된다.

728x90