[혼공머신] Ch.5 트리 알고리즘_2. 교차 검증과 그리드 서치
<혼자 공부하는 머신러닝 + 딥러닝>의 'Ch.5 트리 알고리즘_2. 교차 검증과 그리드 서치'의 내용을 요약 및 정리한 내용입니다.
https://product.kyobobook.co.kr/detail/S000001810330
혼자 공부하는 머신러닝+딥러닝 | 박해선 - 교보문고
혼자 공부하는 머신러닝+딥러닝 | 혼자 해도 충분하다! 1:1 과외하듯 배우는 인공지능 자습서이 책은 수식과 이론으로 중무장한 머신러닝, 딥러닝 책에 지친 ‘독학하는 입문자’가 ‘꼭 필요한
product.kyobobook.co.kr
1. 검증 세트가 필요한 이유를 이해하고 교차 검증에 대해 배웁니다.
2. 그리드 서치와 랜덤 서치를 이용해 최적의 성능을 내는 하이퍼파리미터를 찾습니다.
0. 시작하기 전에
지금까지는 훈련 세트에서 모델을 훈련하고 테스트 세트에서 모델을 평가했습니다. 테스트 세트에서 얻은 점수를 보고 " 아, 이 모델을 실전에 투입하면 이 정도 성능을 기대할 수 있겠군"이라고 생각을 합니다. 즉 일반화 성능을 가늠해 볼 수 있는 거죠. 그런데 테스트 세트를 사용해 자꾸 성능을 확인하다 보면 점점 테스트 세트에 맞추게 되는 셈입니다.
이전까지는 문제를 간단히 하려고 테스트 세트를 사용했습니다. 하지만 테스트 세트로 일반화 성능을 올바르게 예측하려면 가능한 한 테스트 세트를 사용하지 말아야 합니다. 모델을 만들고 나서 마지막에 딱 한 번만 사용하는 것이 좋습니다. 그렇다면 max_depth 매개변수를 사용한 하이퍼파라미터 튜닝을 어떻게 할 수 있을까요? 게다가 결정 트리는 테스트해 볼 매개변수가 많습니다. 이에 대해 알아보겠습니다.
1. 검증 세트
테스트 세트를 사용하지 않으면 모델이 과대적합인지 과소적합인지 판단하기 어렵습니다. 테스트 세트를 사용하지 않고고 이를 측정하는 간단한 방법은 훈련 세트를 또 나누는 것입니다. 이 데이터를 검증 세트라고 부릅니다.
해당 방법이 너무 단순해서 이상하게 들릴 수도 있겠지만, 실제로 많이 사용하는 방법입니다. 앞에서는 전체 데이터 중 20%를 테스트 세트로 만들고 나머지 80%를 훈련 세트로 만들었습니다. 이 훈련 세트 중에서 다시 20%를 떼어 내어 검증 세트로 만듭니다.
print("""
훈련 세트 | 검증 세트 | 테스트세트
=====================================================
|| 약 60 % || 약 20 % || 약 20 % ||
=====================================================
""")
훈련 세트에서 모델을 훈련하고 검증 세트로 모델을 평가합니다. 이런 식으로 테스트하고 싶은 매개변수를 바꿔가며 가장 좋은 모델을 고릅니다. 그 다음 이 매개변수를 사용해 훈련 세트와 검증 세트를 합쳐 전체 훈련 데이터에서 모델을 다시 훈련합니다. 그리고 마지막에 테스트 세트에서 최종 점수를 평가합니다. 이런 과정을 거치게되면 실전에 투입했을 때 테스트 세틍의 점수와 비슷한 성능을 기대할 수 있을 것입니다.
그럼 다시 데이터를 불러와서 검증 세트를 만들어 보겠습니다. 먼저 판다스로 CSV 데이터를 읽습니다.
import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
그다음 class 열을 타깃으로 사용하고 나머지 열은 특성 배열에 저장합니다.
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=42)
그 다음 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)
단순히 train_test_split() 함수를 2번 적용하는 것만으로 훈련 세트와 검증 세트로 나눴습니다. 훈련세트와 검증 세트의 크기를 확인해보겠습니다.
원래 5,197개였던 훈련 세트가 4,157개로 줄고, 검증 세트는 1,040개가 되었습니다. 이제 sub_input, sub_target과 val_input, val_target을 사용해 모델을 만들고 평가해보겠습니다.
이렇게 val_input, val_target 을 사용해서 모델을 평가하면 됩니다. 보이는 것처럼 이 모델은 확실히 훈련 세트에 과대적합되어 있습니다. 즉 매개 변수를 바꿔서 더 좋은 모델을 찾아야 합니다. 그 전의 검증 세트에 대해 조금 더 알아보겠습니다.
2. 교차 검증
검증 세트를 만드느라 훈련 세트가 줄었습니다. 보통 많은 데이터를 훈련해 사용할수록 좋은 모델이 만들어집니다. 그렇다고 검증 세트를 너무 조금 떼어 놓으면 검증 점수가 들쭉날쭉하고 불안정 할 것입니다. 이럴 때 교차 검증을 이용하면 안정적인 검증 점수를 얻고 훈련에 더 많은 데이터를 사용할 수 있습니다.
교차 검증은 검증 세트를 떼어 내어 평가하는 과정을 여러 번 반복합니다. 그다음 이 점수를 평균하여 최종 검증 점수를 얻습니다. 이 과정을 그림으로 나타내면 아래와 같습니다.
보통 5-폴드 교차 검증이나 10-폴드 교차 검증을 많이 사용합니다. 이렇게 하면 데이터의 80~90%까지 훈련에 사용할 수 있습니다. 검증 세트가 줄어들지만 각 폴드에서 계산한 검증 점수를 평균하기 때문에 안정된 점수로 생각할 수 있습ㄴ디ㅏ.
사이킷런에는 cross_validate() 라는 교차 점증 함수가 있습니다. 사용법은 먼저 평가할 모델 객체를 첫 번째, 매개변수로 전달하고 그 다음에 앞에서철머 직접 검증 세트를 떼어 내지 않고 훈련 세트 전체를 cross_validate()함수에 전달하기만 하면 됩니다.
이 함수는 fi_time, score_time, test_score 키를 가진 딕셔너니를 반환합니다. 처음 2개의 키는 각각 모델을 훈련하는데 시간과 검증하는 시간을 의미합니다. 그리고 각 키마다 5개의 숫자가 담겨있는 것을 알 수 있습니다. 이는 cross_validate() 함수가 기본적으로 5-폴드 교차 검증을 수행하는 것을 의미합니다. 이는 cv 매개변수에서 폴드 수를 바꿀 수도 있습니다.
교차 검증의 최종 점수는 test_score 키에 담긴 5개의 점수를 평균하여 얻을 수 있스비다. 이름은 test_score 지만 검증 폴드의 점수입니다. 혼동해서는 안됩니다.
교차 검증을 수행하면 입력한 모델에서 얻을 수 있는 최상의 검증 점수를 가늠해 볼 수 있습니다.
다만 한 가지 주의할 점은 cross_validate()는 훈련 세트를 섞어 폴드를 나누지 않습니다. 이번 경우는 train_test_split()함수로 전체 데이터를 섞은 후 훈련 세트를 준비했기 때문에 따로 섞을 필요가 없었습니다. 하지만 만약 교차 검증을 할 때 훈련 세트를 섞으려면 분할기를 지정해야 합니다.
사이킷런의 분할기는 교차 검증에서 폴드를 어떻게 나눌지 결정해줍니다. cross_validate()함수는 기본적으로 회귀 모델일 경우 KFold 분할기를 사용하고 분류 모델일 경우 타깃 클래스를 골고루 나누기 위해 StratifiedKFold를 사용합니다. 즉 앞서 수행한 교차 검증은 아래의 코드와 동일합니다.
예를 들어, 만약 훈련 세트를 섞을 후 10-폴드 교차 검증을 수행하려면 코드는 아래와 같습니다.
KFold 클래스도 동일한 방식으로 사용할 수 있습니다. 이제 교차 검증에 대해 이해했습니다. 이어서 결정 트리의 매개변수 값을 바꿔가며 가장 좋은 성능이 나오는 모델을 찾아보겠습니다. 이때 테스트 세트를 사용하지 않고 교차 검증을 통해서 좋은 모델을 고르면 됩니다.
3. 하이퍼파라미터 튜닝
머신러닝 모델이 학습하는 파라미터를 모델 파라미터라고 부릅니다. 반면 모델이 학습할 수 없어서 사용자가 지정해야만 하는 파라미터를 하이퍼파라미터라고 합니다. 사이킷런과 같은 머신러닝 라이브러리를 사용할 때 이런 하이퍼파라미터는 모두 클래스나 메서드의 매개변수로 표현됩니다.
하이퍼파라이머를 튜닝하는 작업은 어떻게 진행될까요? 먼저 라이브러리가 제공하는 기본값을 그대로 사용해 모델을 훈련합니다. 그다음 검증 세트의 점수가 교차 검증을 통해서 매개변수를 조금씩 바꿔 봅니다. 모델마다 적게는 1~2개에서 많게는 5~6개의 매개변수를 제공합니다. 이 매개변수를 바꿔가면서 모델을 훈련하고 교차 검증을 수행해야 합니다.
그런데 아주 중요한 점이 있습니다. 가령 결정 트리 모델에서 최적의 max_depth 값을 찾았다고 가정해보겠습니다. 그 다음 max_depth를 최적의 값으로 고정하고 min_samples_split을 바꿔가며 최적의 값을 찾습니다. 이렇게 한 매개변수의 최적값을 찾고 다른 매개 변수의 최적값을 찾아도 될까요? 아니요. 틀렸습니다. max_depth의 최적값은 min_samples_split 매개변수의 값이 바뀌면 함께 달라집니다. 즉 이 두 매개변수를 동시에 바꿔가며 최적의 값을 찾아야 합니다.
게다가 매개 변수가 많아지면 문제는 더 복잡해 집니다. 이때 사이킷런에서 제공하는 그리드 서치를 사용합니다.
사이킷런의 GridSearchCV 클래스는 하이퍼파라미터 탐색과 교차검증을 한 번에 수행하는 장점도 갖고 있습니다. 즉 별도로 cross_validate() 함수를 호출할 필요가 없습니다.
그럼 어떻게 사용하지는 간단한 예를 만들어보겠습니다.
기본 매개변수를 사용한 결정 트리 모델에서 min_impurity_decrease 매개변수의 최적값을 찾아보겠습니다. 먼저 GridSearchCV 클래스를 임포트하고 탐색할 매개변수와 탐색할 값의 리스트를 딕셔너리로 만듭니다.
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번 실행합니다.
GridSearchCV의 cv 매개변수 기본값은 5입니다. 따라 min_impurity_decrease 값마다 5-폴드 교차 검증을 수행합니다. 즉 25개의 모델을 훈련한다는 의미입니다. 많은 모델을 훈련하기 때문에 GridSearchCV 클래스의 n_jobs 매개변수에서 병렬 실행에 사용할 CPU 코어 수를 지정하는 것이 좋습니다. 이 매개변수의 기본값은 1입니다. 이때 -1로 지정하면 시스템에 있는 모든 코어를 사용한다는 의미입니다. 그럼 그리드 서치를 수행해 보겠습니다.
교차 검증에서 최적의 하이퍼파라미터를 찾으면 전체 훈련 세트로 모델을 다시 만들어야 한다고 했습니다. 그리드 서치는 편리하게도 훈련이 끝나면 25개의 모델 중에서 검증 점수가 가장 높은 모델의 매개변수 조합으로 전체 훈련 세트에서 자동으로 다시 모델을 훈련합니다. 이 모델은 gs 객체의 best_estimator_ 속성에 저장되어 있습니다. 이 모델을 일반 결정 트리처럼 똑같이 사용할 수 있습니다.
그리드 서치로 찾은 최적의 매개변수는 best_params_ 속성에 저장되어 있습니다.
여기서는 0.0001이 가장 좋은 값으로 선택되었습니다. 각 매개변수에서 수행한 교차 검증의 평균 점수는 cv_results 속성의 'mean_test_score'키에 저장되어 있습니다. 5번의 교차 검증으로 얻은 점수를 출력해 보겠습니다.
첫 번째 값이 가장 큰 것 같습니다. 수동으로 고르는 것보다 넘파이 argmax() 함수를 사용함녀 가장 큰 값의 인덱스를 추출할 수 있습니다. 그다음 이 인덱스를 사용해 parmas 키에 저장된 매개변수를 출력할 수 있습니다. 이 값이 최상의 검증 점수로 만들 매개변수 조합입니다.
이 과정을 정리하면 아래와 같습니다.
1. 먼저 탐색할 매개변수를 지정합니다.
2. 그 다음 훈련 세트에서 그리드 서치를 수행하여 최상의 평균 검증 점수가 나오는 매개변수 조합을 찾습니다. 이 조합은 그리드 서치 객체에 저장됩니다.
3. 그리드 서치는 최상의 매개변수에서 전체 훈련 세트를 사용해 최종 모델을 훈련합니다. 이 모델도 그리드 서치 객체에 저장됩니다.
그럼 조금 더 복잡한 매개변수 조합도 탐색해 보겠습니다. 결정 트리에서 min_impurity_decrease는 노드를 분할하기 위한 불순도 감소 최소량을 지정합니다. 여기에다가 max_depth로 트리의 깊이를 제한하고 min_samples_split 으로 노드를 나누기 위한 최소 샘플 수도 골라 보겠습니다.
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)
}
넘파이 arrange()함수는 첫 번째 매개 변수 값에서 시작하여 두 번째 매개변수에 도달할 때까지 세 번째 매개변수를 계속 더한 배열을 만듭니다. 코드에서는 0.0001에서 시작하여 0.001이 될 때까지 0.0001을 계속 더한 배열입니다. 두 번째 매개변수는 포함되지 않으므로 배열의 원소는 총 9개 입니다.
파이썬 range() 함수도 비슷하지만 정수만 사용할 수 있습니다. 이 경우 max_depth를 5에서 20까지 1씩 증가시하면서 15개의 값을 만듭니다. min_samples_split 은 2에서 100까지 10씩 증가하면서 10개의 값을 만듭니다.
다라서 이 매개변수로 수행할 교차 검증 횟수는 1,350개(9*15*10) 입니다. 기본 5-폴드 교차 검증을 수행하므로 만들어지는 모델의 수는 6,750개가 됩니다. n_jobs 매개변수를 -1로 설정하고 그리드 서치를 실행해 보겠습니다.
최상의 매개변수 조합과 최상의 교차 검증 점수도 확인해 보겠습니다.
GridSearchCV 클래스를 사용하니 매개변수를 일일이 바꿔가며 교차 검증을 수행하지 않고 원하는 매개변수 값을 나열하면 자동으로 교차 검증을 수행해서 최상의 매개변수를 찾을 수 있습니다.
아직 조금 아쉬운 점이 있습니다. 앞에서 탐색할 매개변수의 간격을 0.0001 혹은 1로 설정했습니다. 하지만 이런 간격을 둔 것에 대해 근거가 없습니다. 이보다 더 좋거나 넓은 간격으로 시도해볼수도 있는데 말입니다!
랜덤 서치
매개변수의 값이 수치일 때 값의 범위나 간격을 미리 정하기 어려울 수 있습니다. 또 너무 많은 매개 변수 조건이 있어 그리드 서치 수행 시간이 오래 걸릴 수 있습니다. 이럴 때 랜덤 서치를 사용하면 좋습니다. 랜덤 서치에는 매개변수 값의 목록을 전달하는 것이 아니라 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달합니다.
한 번 해보겠습니다. 먼저 싸이파이에서 2개의 확률 분포 클래스를 임포트합니다.
from scipy.stats import uniform, randint
싸이파이의 stats 서브 패키지에 있는 uniform과 randint 클래스는 모두 주어진 범위에서 고르게 값을 뽑습니다. 이를 '균등 분포에서 샘플링한다'라고 말합니다. randint는 정숫값을 뽑고, uniform은 실숫값을 뽑습니다. 사용하는 방법은 동일합니다.
0 에서 1000 개를 샘플링해서 각 숫자의 개수를 세어 보겠습니다.
0에서 9까지의 숫자가 보이는것 처럼 어느정도 고르게 추출된 것을 알 수 있습니다. uniform의 클래스의 사용법도 동일합니다. 0~1 사이에서 10개의 실수를 추출해보겠습니다.
이를 난수 발생기와 유사하게 생각하면 됩니다. 랜덤 서치에 randint 와 unform 클래스 객체를 넘겨주고 총 몇번을 샘플링해서 최적의 매개변수를 찾으라고 명령할 수 있습니다. 샘플링 횟수는 시스템 자원이 허락하는 범위 내에서 최대한 크게 하는 것이 좋습니다.
탐색할 매개변수의 딕셔너리를 만들어 보겠습니다. 여기에서는 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의 n_iter 매개변수에 지정합니다.
위 parmas 에 정의된 매개변수 범위에서 총 100을 샘플링하여 교차 검증을 수행하고 최적의 매개변수 조합을 찾습니다. 앞서 그리드 서치보다 훨씬 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색할 수 있습니다. 결과를 확인해 보겠습니다. 먼저 최적의 매개변수 조합을 출력하겠습니다.
최고의 교차 검증 점수도 확인해 보겠습니다.
최적의 모델은 이미 전체 훈련 세트로 훈련되어 best_estimator_ 속성에 저장되어 있습니다. 이 모델을 최종 모델로 결정하고 테스트 세트의성능을 확인해보겠습니다.
테스트 세트 점수는 검증 세트에 대한 점수보다 조금 작은 것이 일반적입니다. 테스트 세트 점수가 아주 만족스럽지는 않지만 충분히 다양한 매개변수를 테스트해서 얻은 결과로 좀 더 믿을만한 점수입니다.
4. 최적의 모델을 위한 하이퍼파라미터 탐색
작업의 성능을 끌어올리기 위해 결정 트리의 다양한 하이퍼파라미터를 시도해봐야 합니다. 이런 과정에서 테스트 세트를 사용하면 결국 테스트 세트에 맞춰 모델을 훈련하는 효과를 만듭니다.
테스트 세트는 최종 모델을 선택할 때까지 사용하지 말아야 합니다. 테스트 세트를 사용하지 않고 모델을 평가하려면 또 다른 세트가 필요합니다. 이를 검증 세트라고 부릅니다. 혹은 개발 세트라고도 불브니다. 검증 세트는 훈련 세트 중 일부를 다시 덜어 내어 만듭니다.
검증 세트가 크지 않다면 어떻게 데이터를 나누었는지에 따라 검증 점수가 들쭉날쭉할 것입니다. 훈련한 모델의 성능을 안정적으로 평가하기 위해 검증 세트를 한 번 나누어 모델을 평가하는 것에 그치지 않고 여러 번 반복할 수 있습니다. 이를 교차 검증이라고 합니다.
보통 훈련 세트를 5등분 혹은 10등분 합니다. 나누어진 한 덩어리를 폴드라고 부르며 한 폴드씩 돌아가면서 검증 세트의 역할을 합니다. 따라서 전체적으로 5개 혹은 10개의 모델을 만듭니다. 최종 검증 점수는 모든 폴드의 검증 점수를 평균하여 계산합니다.
교차 검증을 사용해 다양한 하이퍼파라미터를 탐색합니다. 머신러닝 라이브러리에서는 클래스와 메서드의 매개변수를 바꾸어 모델을 훈련하고 평가해 보는 작업입니다. 이런 과정을 때론 지루하고 반복적입니다. 테스트하고 싶은 매개변수 리스트를 만들어 이 과정을 자동화하는 그리드 서치를 사용하면 편리합니다.
매개변수 값이 수치형이고 특히 연속적인 실숫값이라면 싸이파이의 확률 분포 객체를 전달하여 특정 범위 내에서 지정된 횟수만큼 매개변수 후보 값을 샘플링하여 교차 검증을 시도할 수 있습니다. 이는 한정된 자원을 최대한 활용하여 효율적으로 하이퍼파리미터 공간을 탐색할 수 있는 아주 좋은 도구 입니다.