AI/혼자 공부하는 머신러닝+딥러닝 책

챕터 05 - 2

codehunter 2023. 7. 28. 22:13

앞에서 전체 데이터를 훈련세트와 테스트세트로 나눠서 훈련세트로 훈련한 값으로 테스트 세트를 평가하는 과정이 하나의  루틴처럼 굳어졌다.

 

그동안 평가 점수가 높게 나왔기 때문에 별문제가 없었지만 사실 하이퍼 파라미터를 이리저리 조정해서 테스트 세트의 점수가 높게 나오게 조정하면서 계속 학습한다면 테스트 세트로 테스트하는 의미가 약해진다. 즉 테스트 세트는 말그대로 테스트 할때 한번만 사용해야 한다.

 

검증세트

 

그럼 어떻게 해야 할까? 훈련세트를 또 나누는 방법이 있다. 전체 데이터중에서 20%를 테스트 세트로 나머지 80%를 훈련세트로 사용했는데 여기서 훈련세트에서 다시 20%를 떼어 내어 검증세트로 만든다.

 

위의 방법대로 와인문제를 다시 접근해보자.

훈련세트와 검증세트로 훈련하고 점수를 확인해보니 확실히 훈련세트에 과대적합인 상태이다.

 

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

 

교차검증

교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복한다. 훈련세트를 3등분해서 쓰는 3-폴드 교차검증이라 하는데 훈련 세트를 몇 부분을 나누냐에 따라 k-겹 교차 검증이라고 부르기도 함. 보통은 5-폴드, 10-폴드 교차 검증을 많이 사용한다고 함.

 

사이킷런에는 cross_validate() 교차 검증 함수가 있는데 사용법은 다음과 같다.

이 함수는 fit_time, score_time, test_score 키를 가진 딕셔너리를 반환하는데 처음 2개의 키는 각각 모델을 훈련하는 시간과 검증하는 시간을 의미한다. 이 함수는 기본적으로 5-폴드 교차 검증을 수행하기 때문에 각 키마다 5개의 숫자가 담겨있다. 교차 검증의 최종 점수는 test_score 키에 담긴 5개의 점수를 평균하여 얻을수 있다.

한 가지 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않는다. 만약 교차 검증을 할 때 훈련 세트를 섞으려면분할기를 지정해야 한다. 기본적으로 회귀 모델일 경우 KFold 분할기를 사용하고 분류모델일 경우 타깃 클래스를 골고루 나누기 위해 StratifiedKFold를 사용한다.

교차검증을 그림으로 나타내면 다음과 같다. 그림에서는 기본 훈련세트를 3등분한 3-폴드교차검증이다.

 

하이퍼파라미터 튜닝

머신러닝 모델이 학습하는 파라미터를 모델 파라미터라고 부르고 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 하는데 이런 하이퍼파라미터 튜닝은 다음 순서로 진행된다.

 

1. 라이브러리가 제공하는 기본값으로 모델을 훈련

2. 검증 세트의 점수나 교차 검증을 통해서 매개변수를 조금씩 바꿔보기

3. 바꾼 매개변수를 적용한 라이브러리로 다시 훈련

 

여기서 중요한건 예를 들면 결정 트리 모델에서 최적의 max_depth 값을 찾았다고 가정하면 그 값을 고정하고 min_samples_split 값을 바꿔가며 최적의 값을 찾는건 틀렸다. 즉 이 두 매개변수의 최적의 값을 찾는다는건 동시에 바꿔가며 찾아야 하는 것이다. 그런데 매개 변수가 많아지면 문제가 복잡해지는데 이럴때 사용하라고 그리드 서치라는 기능이 있다.

 

사이킷런의 GridSearchCV 클래스는 친절하게 하이퍼라라미터 탐색과 교차 검증을 한 번에 수행한다. 별로로 cross_validate() 함수를 호출할 필요가 없다. 그럼 어떻게 사용하는지 예를 보자.

 

기본 매개변수를 사용한 결정 트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아보자.

 

먼저 GridSearchCV 클래스를 임포트하고 탐색할 매개변수와 탐색할 값의 리스트를 딕셔너리로 만든다.

 

여기서는 0.0001 ~ 0.0005까지 0.0001씩 증가하는 5개의 값을 시도해보자 GridSearchCV 클래스에 탐색 대상 모델과 params 변수를 전달하여 그리드 서치 객체를 만든다.

 

그 다음 일반 모델을 훈련하는 것처럼 gs 객체에 .fit() 메서드를 호출한다. 이 메서드를 호출하면 그리드 서치 객체는 결정 트리 모델 min_impurity_decrease 값을 바꿔가며 총 5번을 실행한다.

 

GridSearchCV의 cv 매개변수 기본값은 5이다. 따라서 min_impurity_decrease 값마다 5-폴드 교차 검증을 수행한다. 결국 5x5=25개의 모델을 훈련하게 된다. 많은 모델을 훈련하기 때문에 GridSearchCV 클래스의 n_jobs 매개변수에서 병렬 실행에 사용할 CPU 코어 수를 지정하는게 좋다. 이 매개변수의 기본값은 1이다. -1로 지정하면 시스템에 있는 모든 코어를 사용한다. 

 

그리드 서치의 훈련이 끝나면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련한다. 

그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어 있다.

여기서는 0.0001이 가장 좋은 값으로 선택되었다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 cv_results_ 속성의 'mean_test_score' 키에 저장되어 있다.

첫 번째 값이 가장 큰 데 수동으로 고르는 것보다 넘파이 argmax() 함수를 사용하면 가장 큰 값의 인덱스를 추출할 수 있다.

그다음 이 인덱스를 사용해 params  키에 저장된 매개변수르 출력할 수 있다. 그 다음 이 인덱스를 사용해서 params 키에 저장된 매개변수를 출력할 수 있다. 이 값이 최상의 검증 점수를 만든 매개변수 조합이다. 앞에서 출력한 gs.best_params_와 동일한지 확인해보자

여기까지는 그리드 서치를 사용하는 방법을 알아본것이고 이제 본격적으로 와인 찾기에 이 방법을 적용해보자.

 

먼저 위 방법을 다시정리하면

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

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

3. 그리드 서치는 최상의 매개변수에서 (교차 검증에 사용한 훈련 세트가 아니라) 전체 훈련 세트를 사용해 최종 모델을 훈련한다. 이 모델도 그리드 서치 객체에 저장된다.

 

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

min_impurity_decrease 는 0.0001 부터 0.001 까지 0.0001을 계속 더한 배열 9개를 만들고

max_depth 는 5 에서 20 까지 1씩 증가하면서 15개의 값을 만들고

min_samples_split 는 2 에서 100 까지 10씩 증가하면서 10개의 값을 만든다.

따라서 이 매개변수로 수행할 교차 검증 횟수는 9 x 15 x 10 = 1,350개이다. 기본 5-폴드 교차검증을 수행하므로 만들어지는 모델의 수는 6,750개이다. 

이 파라메터로 그리드 서치를 실행해보자.

최상의 교차 검증 점수도 확인해보자

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

 

그런데 위에서 세팅한 값의 범위는 특별한 근거가 없이 정했는데 값의 범위나 간격을 미리 정하기 어려울 수 있다. 또 너무 많은 매개 변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있는데 이럴때는 랜덤 서치라는 것을 사용하면 좋다.

 

랜덤 서치

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

확률 분포 객체를 사용해서 랜덤 서치를 사용해보자.

 

먼저 싸이파이에서 2개의 확률 분포 클래스를 임포트하자.

싸이파이는 파이썬의 과학 라이브러리 중 하나이다. 싸이파이의 stats 서브 패키지에 있는 uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑는다. 이를 '균등 분포에서 샘플링한다'라고 한다.

randint는 정숫값을 뽑고

uniform은 실숫값을 뽑는다.

0에서 10사이의 범위를 갖는 randint 객체를 만들고 10개의 숫자를 샘플링해 보자. 10개밖에 되지 않기 때문에 고르게 샘플링되는 것 같지 않지만 샘플링 숫자를 늘리면 쉽게 확인할 수 있다. 1000개를 샘플링해서 각 숫자의 개수를 세어보자.

개수가 늘어나니 0에서 9까지의 숫자가 어느 정도 고르게 추출된 것을 볼 수 있다. uniform도 사용해보자.

랜덤 서치에 randint와 uniform 클래스 객체를 넘겨주고 총 몇 번을 샘플링해서 최적의 매개변수를 찾으라고 명령할 수 있다.

 

탐색할 매개변수의 딕셔너리를 만들어 보자. 여기에서는 min_samples_leaf 매개변수를 탐색 대상에 추가하자. 이 매개변수는 리프 노드가 되기 위한 최소 샘플의 개수입니다. 탐색할 매개변수 범위는 다음과 같다.

min_impurity_decrease : 0.0001 에서 0.001 사이의 실숫값,
max_depth : 20 에서 50 사이의 정수,
min_samples_split : 2 에서 25사이의 정수,
min_samples_leaf : 1 에서 25 사이의 정수를 샘플링한다


샘플링 횟수는 사이킷런의 랜덤 서치 클래스인 RandomizedSearchCV 의 n_iter 매개변수에 지정한다.

 

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

최적의 모델은 이미 전체 훈련 세틍로 훈련되어 best_estimator_ 속성에 저장되어 있다. 이 모델을 최종 모델로 결정하고 테스트 세트의 성능을 확인해보자.

테스트 세트 점수가 아주 만족스럽지는 않지만 다양한 매개변수를 테스트해서 얻은 결과를 확인할수 있었다.

 

전체코드

## 검증 세트

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

### 훈련세트와 테스트세트

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 )

### 훈련세트와 검증세트

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)

## 모델 만들고 평가하기

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

### 교차 검증

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

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

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

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

## 하이퍼파라미터 튜닝

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)

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

print(gs.best_params_)

print(gs.cv_results_['mean_test_score'])

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

param = {'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), param, n_jobs=-1)
gs.fit(train_input, train_target)

최상의 매개변수 조합을 확인해 보자

print(gs.best_params_)

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

### 랜덤 서치

from scipy.stats import uniform, randint

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

np.unique(rgen.rvs(1000), return_counts=True)

ugen = uniform(0, 1)
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),
          }

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

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

dt = gs.best_estimator_
print(dt.score(test_input, test_target))

'AI > 혼자 공부하는 머신러닝+딥러닝 책' 카테고리의 다른 글

챕터 06 - 1  (0) 2023.07.31
챕터 05 - 3  (0) 2023.07.30
챕터 05 - 1  (0) 2023.07.24
챕터 04 - 2  (0) 2023.07.21
챕터 04 - 1  (0) 2023.07.17