GIL's LAB

모델 성능 향상을 위한 하이퍼 파라미터 튜닝 본문

데이터사이언스/머신러닝

모델 성능 향상을 위한 하이퍼 파라미터 튜닝

GIL~ 2021. 9. 4. 18:25

이번 포스팅에서는 지도 학습 모델을 만들때 필수적인 단계인 하이퍼 파라미터 튜닝에 대해 소개한다.

 

하이퍼 파라미터와 하이퍼 파라미터 튜닝이란?

머신러닝에서 하이퍼 파라미터란 쉽게 생각해서 사용자의 입력값, 혹은 설정 가능한 옵션이라고 볼 수 있다. 모든 데이터와 문제에 대해 가장 좋은 하이퍼 파라미터 값이 있으면 좋겠지만, 아래 그림과 같이 데이터에 따라 좋은 하이퍼 파라미터라는 것이 다르다. 

 

데이터에 따라 좋은 모델과 하이퍼파라미터가 다르다.

그래서 데이터마다 좋은 입력값을 설정해주는 노가다 작업이 필요한데, 이를 하이퍼 파라미터 튜닝이라고 한다. 예를 들어서, k-최근접 이웃에서 k를 3으로도 해보고, 5로도 해보고, 10으로도 해 본 다음 그 가운데 가장 좋은 k를 찾는 과정이다. 하이퍼 파라미터 튜닝을 노가다라고 표현한 이유는 해보기 전까진 3이 좋을지, 5가 좋을지, 10이 좋을지 알 수 없기 때문이다 (실제로 2000년대 초반에는 최적의 k를 찾는 연구가 수행되었지만, 결론은 data by data라고 났다). 

 

그리드 서치(grid search)

하이퍼 파라미터 그리드는 한 모델의 하이퍼 파라미터 조합을 나타내며, 그리드 서치란 하이퍼 파라미터 그리드에 속한 모든 파라미터 조합을 비교 평가하는 방법을 의미한다. 

예를 들어, 아래 그림에서는 k-최근접 이웃의 하이퍼파라미터인 이웃수(n_neighbors)와 거리 척도 (metric)에 대한 하이퍼 파라미터 그리드를 보여준다. 이 그리드에서 이웃 수는 {3, 5, 7}의 범위에서, 거리 척도는 {Manhattan, Euclidean}의 범위에서 튜닝되며, 그 결과로 총 3 x  2 = 6개의 파라미터 조합이 생성되었다. 이제 이 모든 조합을 비교하여 최적의 파라미터를 찾으면 된다. 물론 전역 최적이 아니라, 지역 최적일 가능성이 매우매우 높겠지만 말이다. 

그리드 서치 구현 

sklearn에는 GridSearchCV라는 함수가 있지만, 전처리 과정 등을 효과적으로 반영하기 어렵기에 필자는 잘 사용하지 않는다 (물론 사용은 매우 간편하긴 하다). 그 대신에 여기에서는 sklearn.model_selection.ParameterGrid이라는 함수를 소개한다.

 

이 함수는 이름 그대로 파라미터 그리드를 만들어주는 함수이며, 입력으로는 아래 구조와 같은 사전을 받는다.

위 예시에서는 n_neighbors라는 파라미터는 {3, 5, 7}이라는 범위에서, metric이라는 파라미터는 {"Manhattan", "Euclidean"}이라는 범위에서 튜닝할 것이라는 뜻을 내포한 파라미터 그리드를 정의한 것이다. 물론, n_neighbors와 metric은 어떤 함수의 키워드이며, {3, 5, 7}과 {"Manhattan", "Euclidean"}는 해당 키워드에 대해 입력 가능한 값이다. 그렇지 않다면 당연히 정상적으로 작동할 수 없다.

 

이제 파라미터 그리드 함수가 어떻게 작동하는지 보자. 아래 사진에서 보듯이, 사전을 파라미터 그리드 함수의 입력으로 넣게 되면, 파라미터 그리드에 있는 각 셀 (하나의 파라미터 조합)을 리턴하는 제너레이터를 반환한다. 

이 함수의 출력값을 효과적으로 사용하기 위해서는 아래 두 가지 개념을 알고 있어야 한다.

먼저 어떤 함수의 입력으로 사전을 사용하고 싶다면, 사전 앞에 **를 붙여야 한다. 

그렇게 되면, 사전의 key와 일치하는 함수의 키워드에 value가 아래 그림과 같이 입력되게 된다. 

두 번째 필요한 개념은 최소값 혹은 최대값 찾기 알고리즘이다. 알고리즘이라고 해서 거창해보이지만, 사실 누구나 이해할 수 있는 매우 간단한 개념이다. 물론 max 함수를 사용하면 손쉽겠지만, 각 값을 순회하면서 최대값을 업데이트해야 하는 상황에서는 max 함수를 사용하기 어렵다 (빈 리스트에 값들을 추가해서 max 함수를 쓰면 되지 않느냐라고 반문할 수 있겠지만, 하이퍼 파리미터 튜닝에서는 메모리 낭비로 이어질 가능성이 매우 높다).

[10, 20, 30, 10, 20]이라는 리스트 L이 있고, 이 리스트에서 최대값을 찾는다고 하자.

그러면 아래와 같은 절차로 탐색할 수 있다.

  • (0번째 이터레이션) 우리가 찾을 최대값인 Max_value를 매우 작은 값으로 설정
  • (1번째 이터레이션) 0번째 요소와 Max_value를 비교 ==> 0번째 요소가 더 크므로 Max_value를 0번째 요소로 업데이트
  • (2번째 이터레이션) 1번째 요소와 Max_value를 비교 ==> 1번째 요소가 더 크므로 1번째 요소로 Max_value를 업데이트
  • (3번째 이터레이션) 2번째 요소와 Max_value를 비교 ==> 2번째 요소가 더 크므로 2번째 요소로 Max_value를 업데이트
  • (4번째 이터레이션) 3번째 요소와 Max_value를 비교 ==> Max_value가 더 크므로 pass~
  • (5번째 이터레이션) 2번째 요소와 Max_value를 비교 ==> Max_value가 더 크므로 pass~

 

실습 코드

이제 실제 코드를 가지고 어떻게 튜닝을 할 수 있는지 더 자세히 살펴보자.

먼저, sklearn.datasets에 있는 iris 데이터를 가져온다.

# 예제 데이터 불러오기
from sklearn.datasets import load_iris
X = load_iris()['data'] # feature
Y = load_iris()['target'] # label

그리고 학습 데이터와 평가 데이터로 분리한다.

# 학습 데이터와 평가 데이터 분할
from sklearn.model_selection import train_test_split
Train_X, Test_X, Train_Y, Test_Y = train_test_split(X, Y)

모델은 KNN과 SVM을 사용하자. 

# 모델 불러오기
from sklearn.neighbors import KNeighborsClassifier as KNN
from sklearn.svm import SVC

이제 모델별 파라미터 그리드를 설계한다.

param_grid_for_knn에는 KNN의 파라미터 그리드를, param_grid_for_svm에는 SVM의 파라미터 그리드를 정의한다. 

그리고 이 값들을 param_grid라는 사전에 추가한다. 이는 모델이 여러 개 있을 때 파라미터 튜닝을 쉽게 하는 팁이다.

# 파라미터 그리드 생성
param_grid = dict() 
# 입력: 모델 함수, 출력: 모델의 하이퍼 파라미터 그리드

# 모델별 파라미터 그리드 생성
param_grid_for_knn = ParameterGrid({"n_neighbors": [3, 5, 7],
                           "metric":['euclidean', 'manhattan']})

param_grid_for_svm = ParameterGrid({"C": [0.1, 1, 10],
                           "kernel":['rbf', 'linear']})

# 모델 - 하이퍼 파라미터 그리드를 param_grid에 추가
param_grid[KNN] = param_grid_for_knn
param_grid[SVC] = param_grid_for_svm

이제 튜닝을 시작하자.

먼저, 현재까지 찾은 가장 높은 f1_score를 초기화한다. 

f1_score가 가장 큰 모델과 파라미터를 찾는 것이기에, 매우 작은 값으로 설정해야 하지만, 이 값은 0보다 작을 수 없기에 0이하로 설정해도 무방하다.

그리고 model_func으로 KNN과 SVC 함수를 순회하고, 앞서 정의한 param_grid 사전을 이용하여 각 모델의 파라미터를 param으로 받는다. 모델별 파라미터 그리드를 포함하는 param_grid를 정의한 것이 이렇게 라인 수를 줄여주는데 효과적이다. 이제 param으로 각 파라미터를 돌면서, model_func에 param을 입력하여 모델을 학습하고 f1_score를 계산한다. 이 score가 best_score보다 크다면, 최고 점수와 모델, 파라미터를 업데이트한다. 이 과정에서 best_score를 업데이트하지않게 되면, 튜닝하는 효과가 전혀 없으므로 주의해야 한다. 

# 하이퍼 파라미터 튜닝 
best_score = -1 # 현재까지 찾은 가장 높은 f1_score (f1 score는 절대 0보다 작을수 없기에, -1로 설정해도 무방)

from sklearn.metrics import f1_score

for model_func in [KNN, SVC]:
    for param in param_grid[model_func]:
        model = model_func(**param).fit(Train_X, Train_Y)
        pred_Y = model.predict(Test_X)        
        score = f1_score(Test_Y, pred_Y, average = 'micro')
        
        if score > best_score: 
            # 현재 점수가 지금까지 찾은 최고 점수보다 좋으면, 최고 모델, 파라미터, 점수 업데이트
            best_model_func = model_func
            best_score = score
            best_param = param
            
            # best_model = model

이제 튜닝된 결과를 확인하자. 

print("모델:", best_model_func)
print("점수:", best_score)
print("파라미터:", best_param)

 

(Tip) 튜닝 코드에서 기껏 모델을 학습해놓고 best_model을 주석 처리한 이유는 최종 모델을 학습할 때는 아래와 같이 전체 데이터를 사용하는 경우가 종종 있기 때문에 그렇다.

# 최종 모델 학습: 전체 X와 전체 Y에 대해.
final_model = best_model_func(**best_param).fit(X, Y)

 

Comments