데이터사이언스 이론 공부

하이퍼 파라미터 튜닝 구현하기 - Random Forest(랜덤포레스트)

soopy 2023. 3. 15. 18:40
728x90

Hyper-Parameter optimization

  • 랜덤포레스트 모델의 하이퍼 파라미터 튜닝(optimization)에 대해서 알아본다.

 

로그 데이터 저장

  • 로그데이터를 log파일로 남기는 함수를 작성했다. 이를 활용하면 편리하게 결과를 하나의 로그파일에 적재할 수 있다.
import logging

def get_logger(name, dir_, stream=False):
    """log 데이터 파일 저장
    
    Args:
        name(str): 로그 이름 지정
        dir_(str): 로그 파일을 저장할 경로 지정
        stream(bool): 콘솔에 로그를 남길지에 대한 유무
    
    Returns: logging.RootLogger
        
    """
    logging.basicConfig(level=logging.INFO)
    logger = logging.getLogger(name)
    logger.setLevel(logging.INFO)  # logging all levels
    logger.handlers.clear() # 중복 입력 방지
    
    formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(message)s')
    stream_handler = logging.StreamHandler()
    file_handler = logging.FileHandler(os.path.join(dir_, f'{name}.log'))

    stream_handler.setFormatter(formatter)
    file_handler.setFormatter(formatter)

    if stream:
        logger.addHandler(stream_handler)
    logger.addHandler(file_handler)

    return logger

 

GridSearch CV 구현

개념만 가지고 직접 구현해보자

  • 파라미터 적용 후보값을 지정해주면 해당 값의 모든 조합을 나열한다.
  • 나열된 조합을 모델에 적용 후 K-Fold CV를 통해 학습 및 평가한다.
  • 모든 조합 중 성능이 가장 좋은 조합을 확인한다.

 

먼저 Product 함수를 활용하면 각 리스트 간 조합을 편리하게 적용할 수 있다.

param_grid = {
    'max_depth': [50, 80, 100],
    'n_estimators': [65, 70, 75]
}

from itertools import product
# product 함수는 여러 튜플 입력 시 튜플 내 모든 조합을 인풋 순서에 따라 순차적으로 출력해준다.
# ex) (1, 2, 3), (4, 5, 6) -> (1, 4), (1, 5), (1, 6), (2, 4), ...

names = param_grid.keys()
value_cand = param_grid.values()

for value in product(*value_cand):
    params = {key:value for key, value in zip(names, value)}
    
    print(params)

>>>
{'max_depth': 50, 'n_estimators': 65}
{'max_depth': 50, 'n_estimators': 70}
{'max_depth': 50, 'n_estimators': 75}
{'max_depth': 80, 'n_estimators': 65}
{'max_depth': 80, 'n_estimators': 70}
{'max_depth': 80, 'n_estimators': 75}
{'max_depth': 100, 'n_estimators': 65}
{'max_depth': 100, 'n_estimators': 70}
{'max_depth': 100, 'n_estimators': 75}

 

  • 위 함수를 기반으로 기능 구현하기
from sklearn.model_selection import KFold
from sklearn.metrics import f1_score
from sklearn.ensemble import RandomForestClassifier
from datetime import datetime, timezone, timedelta 
from tqdm.notebook import tqdm
from itertools import product
import os

# grid search 함수에 필요한 파라미터 설정
config = {
    'recorder_dir': './',
    'model': RandomForestClassifier(),
    'param_grid': {
        'max_depth': [50, 80, 100],
        'n_estimators': [65, 70, 75]
    },
    'metric_info': [f1_score, {'average': 'macro'}],
    'cv': 5
}

def grid_search_cv(X, y, model, param_grid, cv, metric_info, recorder_dir):
    """Grid Search CV를 실행합니다.
        
    Args:
        X(array): X data
        y(array): label data
        model(func): 모델 선택
        param_grid(dict): GridSearch에 적용할 파라미터 범위
            {'파라미터명': [값1, 값2, ...]}
        cv(int): cross_validation 개수
        metric_info(list): 평가지표 및 평가지표의 파라미터 설정
            ['평가지표함수', {'파라미터1': '값'...}]
        recorder_dir(str): log파일 저장 위치
    
    Returns:
        best_score(float)
        best_params(dict)
    
    """
    
    # train serial (튜닝 시작 시간을 따로 기록해둔다.)
    kst = timezone(timedelta(hours=9)) # 우리나라는 UTC 기준으로 9시간 빠름
    train_serial = datetime.now(tz=kst).strftime("%Y%m%d_%H%M%S")
    
    # get logger
    logger = get_logger(name=f'train_GSCV', dir_=recorder_dir, stream=False)
    logger.info(f"-----GRID SEARCH START-----")
    logger.info(f"Start Date: {train_serial}")
    logger.info(f"Configure {config}\n")
    
    score_list = []
    params_list = []
    
    names = param_grid.keys() # parameter names
    value_cand = param_grid.values() # value candidate

    # itertools의 product 함수는 여러 튜플 입력 시 튜플 내 모든 조합을 인풋 순서에 따라 순차적으로 출력해준다.
    # ex) (1, 2, 3), (4, 5, 6) -> (1, 4), (1, 5), (1, 6), (2, 4), ...
    
    # 모든 조합의 수
    candidates = 1
    for cand in value_cand:
        candidates *= len(cand)
    
    # log information
    logger.info(f"Fitting {cv} folds for each of {candidates} candidates, totalling {candidates*cv} fits")
    
    for i, value in zip(tqdm(range(candidates)), product(*value_cand)): # asterisk를 붙여야 리스트를 인식한다.
        params = {key:value for key, value in zip(names, value)}

        # k-fold CV
        avg_score = 0

        k_fold = KFold(n_splits=cv, shuffle=False)
        for train_idx, test_idx in k_fold.split(X):
            train_x, train_y = X[train_idx, :], y[train_idx]
            test_x, test_y = X[test_idx, :], y[test_idx]

            # train, predict
            model.set_params(**params).fit(train_x, train_y)
            pred = model.predict(test_x)

            # metric
            metric, metric_params = metric_info
            score = metric(test_y, pred, **metric_params)

            # update avg_score
            avg_score += score / cv
        
        # collect
        score_list.append(avg_score)
        params_list.append(params)

        logger.info(f"*****CANDIDATE_NO.{str(i+1).zfill(3)} RESULT*****")
        logger.info(f"Parameters: {params}")
        logger.info(f"{metric.__name__}: {avg_score}")
    
    # Final Result
    best_score = max(score_list)
    best_params = params_list[score_list.index(best_score)]
    logger.info(f"*****ITERATION END*****")
    logger.info(f"Best Score: {best_score}  Best Params: {params_list[score_list.index(best_score)]}")
    logger.info(f"-----GRID SEARCH END-----\n")
    
    return best_score, best_params

# Run
best_score, best_params = grid_search_cv(train_x, train_y, **config)

 

RandomSearch CV 구현

개념만 가지고 직접 구현해보자

  • 수치형 데이터를 요구하는 각 파라미터의 최소, 최대값을 정해주면 해당 범위 내에서 랜덤한 값을 샘플링하여 사용하는 방식이다. string 타입은 목록 중에서 랜덤선정한다.
  • 파라미터값을 선정 후 K-Fold CV로 한 번의 iteration에 K번 만큼 평가한 후 평균 score를 채택한다.
  • 랜덤 서치는 지정한 횟수(iteration)만큼 반복한다.

 

from sklearn.model_selection import KFold
from sklearn.metrics import f1_score
from sklearn.ensemble import RandomForestClassifier
from datetime import datetime, timezone, timedelta 
from tqdm.notebook import tqdm
import os


# random search 함수에 필요한 파라미터 설정
config = {
    'recorder_dir': './',
    'model': RandomForestClassifier(),
    'param_grid': {
        'max_depth': ([100, 105], 'int'),
        'n_estimators': ([70, 75], 'int'),
        'min_samples_split': ([5, 7], 'int'),
        'min_samples_leaf': ([1, 2], 'int'),
        'criterion': (['gini', 'entropy'], 'str')
    },
    'metric_info': [f1_score, {'average': 'macro'}],
    'num_iter': 10, 
    'cv': 5
}


def random_search_cv(X, y, num_iter, model, param_grid, cv, metric_info, recorder_dir):
    """Random Search CV를 실행합니다.
        
    Args:
        X(array): X data
        y(array): label data
        num_iter(int): iteration 횟수 지정
        model(func): 모델 선택
        param_grid(dict): RandomSearch에 적용할 파라미터 범위
            {'파라미터명': ([최소값: 최대값], 'type')...}
        cv(int): cross_validation 개수
        metric_info(list): 평가지표 및 평가지표의 파라미터 설정
            ['평가지표함수', {'파라미터1': '값'...}]
        recorder_dir(str): log파일 저장 위치
    
    Returns:
        best_score(float)
        best_params(defaultdict)
    
    """
    
    # train serial (튜닝 시작 시간을 따로 기록해둔다.)
    kst = timezone(timedelta(hours=9)) # 우리나라는 UTC 기준으로 9시간 빠름
    train_serial = datetime.now(tz=kst).strftime("%Y%m%d_%H%M%S")
    
    # get logger
    logger = get_logger(name=f'train', dir_=recorder_dir, stream=False)
    logger.info(f"-----RANDOM SEARCH START-----")
    logger.info(f"Start Date: {train_serial}")
    logger.info(f"Configure {config}\n")
    
    score_list = []
    params_list = []
    
    for i in tqdm(range(num_iter), leave=False):
        # hyper-parameter random sampling
        params = dict()
        for name, value_info in param_grid.items():
            if len(value_info[0]) == 1:
                params[name] = value_info[0][0]
            elif value_info[1] == 'int':
                params[name] = np.random.randint(min(value_info[0]), max(value_info[0])+1)
            elif value_info[1] == 'float':
                params[name] = np.random.uniform(min(value_info[0]), max(value_info[0]))
            elif value_info[1] == 'str':
                params[name] = np.random.choice(value_info[0])

        # k-fold CV
        avg_score = 0

        k_fold = KFold(n_splits=cv, shuffle=False)
        for train_idx, test_idx in k_fold.split(X):
            train_x, train_y = X[train_idx, :], y[train_idx]
            test_x, test_y = X[test_idx, :], y[test_idx]


            # train, predict
            model.set_params(**params).fit(train_x, train_y)
            pred = model.predict(test_x)

            # metric
            metric, metric_params = metric_info
            score = metric(test_y, pred, **metric_params)

            # update avg_score
            avg_score += score / 5
        
        # collect
        score_list.append(avg_score)
        params_list.append(params)

        logger.info(f"*****ITERATION_{str(i+1).zfill(3)} RESULT*****")
        logger.info(f"Parameters: {params}")
        logger.info(f"{metric.__name__}: {avg_score}")
    
    # Final Result
    best_score = max(score_list)
    best_params = params_list[score_list.index(best_score)]
    logger.info(f"*****ITERATION END*****")
    logger.info(f"Best Score: {best_score}  Best Params: {params_list[score_list.index(best_score)]}")
    logger.info(f"-----RANDOM SEARCH END-----\n")
    
    return best_score, best_params

# Run
random_search_cv(X, y, **config)

  • 위와같이 Iteration이 도는 동안 기록을 눈으로 확인함과 동시에 기록을 남길 수 있다.
  • 하이퍼 파라미터 범위를 수정 후 재가동 한다해도 ‘train.log’ 파일에 지속적으로 적재된다.

 

# read log file
with open("train.log", "r", encoding='utf-8') as f:
    for line in f:
        print(line, end="")

>>>
2023-03-15 18:17:06,828 | train | INFO | -----RANDOM SEARCH START-----
2023-03-15 18:17:06,865 | train | INFO | Start Date: 20230315_181706
2023-03-15 18:17:06,877 | train | INFO | Configure {'recorder_dir': './', 'model': RandomForestClassifier(), 'param_grid': {'max_depth': ([100, 105], 'int'), 'n_estimators': ([70, 75], 'int'), 'min_samples_split': ([5, 7], 'int'), 'min_samples_leaf': ([1, 2], 'int'), 'criterion': (['gini', 'entropy'], 'str')}, 'metric_info': [<function f1_score at 0x7fd1e30489d0>, {'average': 'macro'}], 'num_iter': 10, 'cv': 5}

2023-03-15 18:17:12,309 | train | INFO | *****ITERATION_001 RESULT*****
2023-03-15 18:17:12,311 | train | INFO | Parameters: defaultdict(None, {'max_depth': 100, 'n_estimators': 72, 'min_samples_split': 7, 'min_samples_leaf': 1, 'criterion': 'entropy'})
2023-03-15 18:17:12,311 | train | INFO | f1_score: 0.7537978944002951
2023-03-15 18:17:17,137 | train | INFO | *****ITERATION_002 RESULT*****
2023-03-15 18:17:17,138 | train | INFO | Parameters: defaultdict(None, {'max_depth': 100, 'n_estimators': 73, 'min_samples_split': 5, 'min_samples_leaf': 2, 'criterion': 'entropy'})
2023-03-15 18:17:17,139 | train | INFO | f1_score: 0.7512725097026567
...
  • 당연하겠지만 with open으로도 log를 확인할 수 있다.
728x90
728x90