본문 바로가기

AI/기계학습(Machine Learning)

[기계학습/ML]11. 검증 세트 - 교차 검증, 그리드 서치

728x90
반응형

Pupbani와 신팀장의 보고를 받은 이사님은 다음과 같은 질문을 던졌다.

  • max_depth를 3말고 다른 값으로 하면 성능이 달라지나요?"
  • "네" 라고 Pupbani가 대답했다.
  • "이런저런 값으로 모델을 많이 만들어서 테스트 세트로 평가하면 결국 테스트 세트에 잘 맞는 모델이 만들어지는게 아닌가요?"

Pupbani는 그 말을 듣고 기존에 했던 작업들을 돌아봤다.

  • 훈련 세트에서 모델을 훈련하고 테스트 세트에서 모델을 평가했다.
  • 이렇게 평가된 점수를 보고 일반화 성능을 가늠했다.
  • 그런데 이렇게 테스트 세트를 자꾸 사용해서 성능을 확인하다보면 결국 모델을 테스트 세트에 맞추게 되는 셈입니다.
  • 올바른 모델 개발을 위해 테스트 세트는 모델을 만들고 나서 마지막에 딱 한 번만 사용하는 것이 좋다.
  • 그렇다면 어떻게 해야할까요??

검증 세트

  • 검증 세트(Validation Set)는 테스트 세트를 사용하지 않고 모델이 과대적합인지 과소적합인지 확인할 수 있는 방법이다.
    • 가장 간단한 방법은 훈련 세트를 또 나누는 방법이다.

이전에 사용했던 데이터를 가져와서 검증세트를 만들어 모델 훈련해보기

  • 데이터를 가져와서 먼저 훈련 세트(80%)테스트 세트(20%)로 나눈다.
    • train_test_split() test_size 매개변수를 통해 테스트 세트의 비율을 정할 수 있다.
  • 훈련 세트를 훈련 세트(60%)검증 세트(20%)로 나눈다.
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine-date')
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

# 80% 20%
train_input, test_input, train_target, test_target = train_test_split(
	data, target,test_size=0.2, random_state=42)
    
# 60% 20%
sub_input, val_input, sub_target, val_target = train_test_split(
	train_input, train_target,test_size=0.2, random_state=42)
  • 이렇게 나온 훈련 세트와 검증 세트로 모델을 학습하고 평가한다.
from sklearn.tree import DecisionTreeClassifier

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

print(f"훈련 세트 정확도 : {dt.score(sub_input, sub_target)*100:.2f}%")
print(f"검증 세트 정확도 : {dt.score(val_input, val_target)*100:.2f}%")

  • 이 모델은 훈련 세트에 대해 과대적합 되어 있다.
  • 매개변수를 바꿔 더 좋은 모델을 찾아야한다.

교차 검증(Cross Validation)

  • 검증 세트를 만드느라 훈련 세트가 줄었다.
  • 보통 많은 데이터를 훈련에 사용할 수록 좋은 모델이 만들어 진다.
  • 그렇다고 검증 세트를 너무 적게하면 모델의 성능이 들쭉날쭉하고 불안정할 것 이다.
  • 이럴떄 사용하는 것이 교차검증(Cross Validation)이다.
    • 검증 세트를 떼어 내어 평가하는 과정을 여러번 반복한다.
    • K-폴드 교차 검증(K-fold cross validation)이라는 방법을 사용한다.
      • 훈련 세트를 K부분으로 나누어 검증을 진행 하는 방법이다.

3-폴드 교차 검증 과정

  • 보통 5 또는 10-폴드 교차검증을 많이 사용한다.
    • 이렇게 하면 데이터의 80~90%까지 훈련에 사용할 수 있다.
  • 사이킷런에는 cross_validate() 라는 교차 검증 함수가 존재한다.
    • 평가할 모델 객체를 첫번째 매개변수로 전달
    • 직접 검증 세트를 떼어 내지 않고 훈련 세트 전체를 전달한다.
from sklearn.model_selection import cross_validate

scores = cross_validate(dt, train_input, train_target)
print(scores)
# 출력 결과
{'fit_time': array([0.00744486, 0.0057323 , 0.00587416, 0.00581193, 0.0055747 ]), 
'score_time': array([0.0007453 , 0.00043416, 0.00042486, 0.00043082, 0.00043225]), 
'test_score': array([0.86923077, 0.84615385, 0.87680462, 0.84889317, 0.83541867])}
  • fit_time : 모델을 훈련한 시간
  • score_time : 모델을 검증한 시간
  • test_score : 검증 점수
  • test_score의 값들을 평균을 내어 최종 점수를 얻을 수 있다.
import numpy as np

print(f"최종 정확도 : {np.mean(scores['test_score'])*100:.2f}%")

  • 여기서 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다.
  • 이전에는 train_test_split으로 데이터를 섞어서 전달했기 때문에 따로 섞을 필요가 없었지만 만약 교차검증 시 훈련 세트를 섞으려면 분할기(Splitter)를 이용해야 한다. 
  • 분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정한다.
  • cross_validate()는 기본적으로 회귀 모델일 때는 KFold 분할기, 분류 모델일 때는 StratifiedKFold를 사용한다.
from sklearn.model_selection import StratifiedKFold

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

# or
# n_splits는 K의 값, 지금은 10-폴드
# splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
# scores = cross_validate(dt, train_input, train_target, cv=splitter)

print(f"최종 정확도 : {np.mean(scores['test_score'])*100:.2f}%")

 

하이퍼파라미터 튜닝

  • 머신러닝 모델이 학습하는 파라미터 : 모델 파라미터
  • 모델이 학습할 수 없어서 사용자가 지정해야하는 파라미터 : 하이퍼파라미터
    • 하이퍼파라미터는 모두 클래스나 메서드의 매개변수로 표현된다.
    • 사람의 개입 없이 하이퍼파라미터 튜닝을 자동으로 수행하는 기술을 "AutoML"이라고 한다.
  • 하이퍼파라미터 튜닝 과정
    1. 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련한다.
    2. 검증 세트의 점수나 교차 검증을 통해 매개변수를 조금씩 바꿔본다.
  • 결정 트리에서 하이퍼파라미터 튜닝
    • max_depth를 최적의 값으로 고정하고 min_samples_split을 바꿔가며 최적의 값을 찾는다.
    • 불행히도 max_depth의 최적값은 min_samples_split 매개변수의 값이 바뀌면 함께 달라진다.
    • 두 개의 매개변수를 동시에 바꿔가며 최적의 값을 찾아야 한다.
    • 이 경우 매개변수의 수가 많아질 경우 문제가 더 복잡해 진다.
  • 위의 문제를 해결하기 위해선 사이킷런에서 제공하는 그리드 서치(Grid Search)를 사용하면 된다.
    • GridSearchCV 클래스를 사용하면 하이퍼파라미터 탐색과 교차 검증을 한 번에 수행할 수 있다.
    • cross_validate() 함수를 따로 호출할 필요 없다.
    • 사용방법
      • 먼저 탐색할 매개변수와 탐색할 값의 리스트를 딕셔너리로 만든다.
      • GridSearchCV에 매개변수로 탐색대상 모델과 위에서 만든 딕셔너리를 전달한다.
      • n_jobs 매개변수는 병렬 실행에 사용할 CPU 코어 수를 지정할 수 있다.
        • 기본값은 1, -1로 지정하면 시스템에 있는 모든 코어를 사용한다.
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(train_input, train_target)
  • 그리드 서치는 이렇게 훈련이 끝나면 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다.
    • 이렇게 최적화되어 훈련된 모델은 GridSearchCV 클래스로 만들어진 객체의 best_estimator_ 속성에 저장되어 있다.
    • 최적의 매개변수는 best_params_ 속성에 저장되어 있다.
dt = gs.best_estimator_
print(f"그리드 서치를 통해 찾은 최적의 모델의 정확도 : {dt.score(train_input, train_target)*100:.2f}%")

print(f"최적의 매개변수 : {gs.best_params_}")

  • 0.0001이 가장 좋은 값으로 선택되었다.
  • 각 매개변수에서 수행한 교차 검증의 평균 점수는 cv_results_ 속성의 'mean_test_score'키에 저장되어 있다.
    • 5번의 교차 검증으로 얻은 점수를 출력해보자.
print(gs.cv_results_['mean_test_score'])

  • 이렇게 찾은  값들 중 가장큰 값의 인덱스를 찾는다.
    • Numpy의 argmax() 함수를 사용하면 쉽게 찾을 수 있다.
  • 이 인덱스를 사용해 params 키에 저장된 매개변수를 출력할 수 있다.
  • 이렇게 나온 매개변수가 최상의 검증 점수를 만든 매개변수 조합이다.
  • 앞에서 출력한 gs.best_params_와 동일한지 비교해보자.
best_index = np.argmax(gs.cv_results_['mean_test_score'])
print(f"최적의 매개변수 : {gs.cv_results_['params'][best_index]}")
print(f"최적의 매개변수(best_params_) : {gs.best_params_}")

  • 동일한 것을 확인할 수 있다.
  • 앞에서 했던 과정을 정리해보자
    1. 먼저 탐색할 매개변수를 지정한다.
    2. 훈련 세트에서 그리드 서치를 수행한다.
    3. 그리드 서치를 통해 나오는 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾는다.(이 조합은 그리드 서치 객체에 저장)
    4. 그리드 서치는 최상의 매개변수에서 (교차 검증에 사용한 훈련 세트 아님) 전체 훈련 세트를 사용해 최종 모델을 훈련한다.(이 모델도 그리드 서치 객체에 저장된다.)
  • GridSearchCV를 사용하니까 매개변수를 일일이 바꿔가며 교차 검증을 수행하지 않고 원하는 매개변수 값을 나열하면 자동으로 교차 검증을 통해 최상의 매개변수를 찾아준다.
  • 하지만 탐색할 매개변수의 간격을 미리 알아서 정해야하는 불편함이 있다.

랜덤서치(Random Search)

  • 매개변수의 값이 수치일 때 값의 범위나 간격을 미리 정하기 어려울 수 있다.
  • 너무 많은 변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있다.
  • 이러한 문제는 랜덤 서치(Random Search)를 사용하여 해결하면 된다.
    • 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달한다.

확률 분포 객체 만들기

  • Scipystats 서브 패키지에 있는 uniform randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다.
    • 이를 "균등 분포에서 샘플링 한다."라고 말한다.
    • randint : 정숫값을 뽑는다.
    • uniform : 실수값을 뽑는다.
from scipy.stats import uniform, randint
rgen = randint(0, 10)
rg = np.unique(rgen.rvs(1000), return_counts=True)
df = pd.DataFrame(rg[1].reshape(-1,10),columns = rg[0])
print(df)

  • 0 ~ 9까지의 숫자가 고르게 추출 되었다.
ugen = uniform(0, 1)
print(ugen.rvs(10))

  • 이제 이 두함수를 가지고 탐색할 매개변수의 딕셔너리를 만들어 보곘다.
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_decresase : 0.0001 ~ 0.001 사이의 실수
  • max_depth : 20 ~ 50 사이의 정수
  • min_samples_split : 2 ~ 25 사이의 정수
  • min_samples_leaf :  1~ 25 사이의 정수

랜덤서치 하기

  • RandomizedSearchCV 클래스로 랜덤 서치를 진행한다.
    • n_iter 매개변수로 샘플링 횟수를 설정한다.
    • 교차 검증할 모델, 매개변수 딕셔너리, 사용할 CPU 코어 개수, 데이터를 섞을 때 사용할 시드값을 매개변수로 전달한다.
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(f"최적의 매개변수 조합 : {gs.best_params_}")

  • 최고의 교차 검증 점수를 출력한다.
print(f"최고의 교차검증 점수 : {np.max(gs.cv_results_['mean_test_score']) * 100:.2f}%")

  • 최적의 모델은 best_estimator_에 저장되어 있다.
  • 이 모델로 테스트 세트의 성능을 확인해 본다.
dt = gs.best_estimator_
print(f"최종 테스트 세트에 대한 점수 : {dt.score(test_input, test_target) * 100 : .2f}%")

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

테스트 세트의 점수가 만족스럽지는 않지만 충분히 다양한 매개변수를 테스트해서 얻은 결과임을 말할 수 있을 것 같다.

Pupbani는 이제 앞으로 수동으로 매개변수를 바꾸는 것이 아니라 그리드 서치나 랜덤 서치를 사용하기로 했다.

728x90
반응형