LightGBM parameter tuning-(2) (with optuna)

joon_1592·2021년 7월 18일
0


LightGBM 하이퍼파라미터를 튜닝하기는 사실 어렵다. 워낙 LGBM이 파라미터 튜닝에 민감하기도 하고, 신경쓸게 너무 많다. 인턴을 하면서 알게된건 3가지 방법 순서대로 파라미터 튜닝을 했다.

파라미터 튜닝 마음가짐 순서

1. 그럼에도 불구하고, 끝까지 노가다 한다

어쩔 수 없다. 학습이 빠르고 정확한 결과를 얻기 위해서 lightgbm을 선택했으니, 파라미터 튜닝은 온전히 내가 해낸다는 마음으로 하나씩 값을 바꿔본다. 아니면 XGBoost 쓰던가

2. 다른 모델 사용하기


CatBoost는 categorical feature에 특화된 결정트리모델을 제공한다. 그리고 자체 알고리즘으로 학습하면서 파라미터를 조절한다!! 심지어 learning_rate도 알아서 해준다! 처음엔 매우 편했는데, 수치형 데이터가 카테고리 데이터보다 훨씬 많으면 오히려 underfitting이 되는 문제가 있었다.

3. optuna 사용하기


optuna를 이용하면 파라미터 튜닝을 알아서 해준다. 정확히 말하자면 각 파라미터마다 튜닝할 숫자의 범위를 정해주면, 각 trial마다 random하게 숫자를 골라준다. 단순히 random selection이 아니라 grid search도 적용할 수 있어서 효율적으로 파라미터를 튜닝한다.

OPTUNA

참고 자료

나는 아래 링크에 있는 글에서 도움을 많이 받았다. (특히 첫번째 글)
좀 더 명확한 feature 분석을 위해서는 추가 작업도 해야하지만 이 글에서는 그냥 파라미터튜닝 자체에만 설명한다.
Optuna tutorial for hyperparameter optimization
LightGBM & tuning with optuna

예제 코드

optuna에는 Trial이라는 객체가 있는데, 이 Trial마다 파라미터를 조절하면 된다.
나는 필수적으로 고정시킬 파라미터는 main()에서 세팅하고 objective()에서 optuna가 파라미터 튜닝할 수 있게 하였다. 그리고 objective()는 반드시 score를 리턴해야하는데, study에서 이 score가 작아야/커야 좋은 것인지 판단할 수 있기 때문이다.

def objective(trial, ...):
    # calculate score...
    return score

이제 파라미터 튜닝을 시켜보자

study = optuna.create_study()
study.optimize(objective, n_trials=10)

만약 objective()Trial말고도 여러 argument가 필요하다면 다음과 같이 작성할 수 있다.

study.optimize(lambda trial: objective(trial, arg0=1, arg1=2), n_trials=100)

실제 작성한 코드

아래는 실제 인턴 생활을 하면서 내가 직접 작성한 코드이다.
참고로, 함수에 한번에 lightgbm.Dataset객체를 넘기려 했으나, 어떤 이유에선지 max_bin값을 바꿀수 없다고 warning이 떠서(실제 값을 출력하면 max_bin의 값이 바뀌긴 했지만) 불안한 마음에 그냥 rough하게 dataframe, feature, target, dataset 을 넘겨서 objective()안에서 데이터셋을 세팅했다.

그리고 서버가 불안해서 중간에 멈출까봐 매 trial마다 params, model, loss를 저장하기 위해서 trial.set_user_attr()를 이용했다. 그리고 best_model, best_loss도 call_back함수에서 정의하여 study가 항상 best_model, best_loss를 저장할 수 있도록 했다.

아직 인턴이기도 했고, 마감에 쫓겨 코드를 작성하다보니 코드가 간결하지 못하고 지저분한건 아쉬웠다. (거의 매일 야근, 주말까지 갈아넣은 내 인턴생활 ㅎ)

1. objective() 정의하기

import lightgbm as lgb
import optuna
from optuna import Trial

def objective(trial: Trial, params, evals_result, path, **dataset):
    '''
    optuna objective function
    
    Args
        trial: trial object
        params (dict): base parameters of lightGBM model
        evals_result (dict): contains loss dictionary while training
        path (str): path or directory to save model, parameters, loss
        dataset (dict): kwargs related datasets
            X_train (pandas.DataFrame): train set
            y_train (array-like): labels of train set
            X_valid (pandas.DataFrame): valid set
            y_valid (array-like): labels of valid set
            cat_features (arrray-like): categorical columns of df_train

        
    Returns
        score (float): metric score of current model
    '''
   
    # randomly select the range of hyperparameters to tune 
    params['lambda_l1'] = trial.suggest_loguniform('lambda_l1', 1e-8, 1e-1)
    params['lambda_l2'] = trial.suggest_loguniform('lambda_l2', 1e-8, 1e-1)
    params['path_smooth'] = trial.suggest_loguniform('path_smooth', 1e-8, 1e-3)
    params['num_leaves'] = trial.suggest_int('num_leaves', 30, 200)
    params['min_data_in_leaf'] = trial.suggest_int('min_data_in_leaf', 10, 100)
    params['max_bin'] = trial.suggest_int('max_bin', 100, 255)
    params['feature_fraction'] = trial.suggest_uniform('feature_fraction', 0.5, 0.9)
    params['bagging_fraction'] = trial.suggest_uniform('bagging_fraction', 0.5, 0.9)
    
    
    # set train, valid set
    train_data = lgb.Dataset(
        dataset['X_train'], 
        label=dataset['y_train'],
        categorical_feature=dataset['CAT_FEATURES'],
        free_raw_data=False
    )
    
    valid_data = lgb.Dataset(
        dataset['X_valid'], 
        dataset['y_valid'],
        categorical_feature=dataset['CAT_FEATURES'],
        free_raw_data=False
    )
    
    # train the model
    model = lgb.train(params,
                      train_set=train_data,
                      valid_sets = [train_data, valid_data],
                      valid_names = ['train', 'valid'],
                      categorical_feature=dataset['CAT_FEATURES'],
                      evals_result=evals_result,
                      verbose_eval=100,
                     )  
    
    # ----- save current trial model, parameters, loss
    make_single_directory(f'{path}trials')
    save_model(model, f'{path}trials/', f'model_{trial.number}')
    save_object(params, f'{path}trials/', f'params_{trial.number}')
    
    
    # make a train-valid loss as a dataframe
    df_loss = pd.DataFrame({
            key: evals_result[key][params['metric']]
            for key in evals_result.keys()
    })
    df_loss.to_csv(f'{path}trials/loss_{trial.number}.csv', index=False)
    
    
    # ----- set user attributes: model, loss
    trial.set_user_attr(key='model', value=model)
    trial.set_user_attr(key='loss', value=df_loss)
    
    # p_valid: predicted value of valid set
    # y_valid: true value of valid set
    p_valid = model.predict(valid_data.get_data(), 
                            num_iteration=model.best_iteration)
    y_valid = valid_data.get_label()
    
    # ----- get score
    score = MAE(y_valid, p_valid)
    print(f'bset score: {model.best_score}')
    
    return score
    
    
def callback_study(study, trial) -> None:
    '''
    save best trial's model, loss if current trial is a best trial
    
    Args:
        study (optuna.study)
        trial (optuna.trial): a certain trial of STUDY
    
    Returns: None
    '''
    
    if study.best_trial.number == trial.number:
        study.set_user_attr(key='best_model', value=trial.user_attrs['model'])
        study.set_user_attr(key='best_loss', value=trial.user_attrs['loss'])

2. main()


#-- set lightgbm parameters
params = get_parameters(config)
#-- train the model
evals_result = {}

... (중략)...

dataset = {
	'X_train': X_train,
	'y_train': y_train,
	'X_valid': X_valid,
	'y_valid': y_valid,
	'CAT_FEATURES': CAT_FEATURES
}

# metric이 accuracy라면 direction='maximize'로 한다.
study = optuna.create_study(direction='minimize')
study.optimize(lambda trial: objective(trial, params, evals_result=evals_result, path=RESULT_PATH, **dataset),
                      n_trials=N_TRIALS, 
                      callbacks=[callback_study])
        
#-- get best parameters, model and loss
params.update(study.best_trial.params)
model = study.user_attrs['best_model']
df_loss = study.user_attrs['best_loss']

...(후략)...
profile
공부용 벨로그

2개의 댓글

comment-user-thumbnail
2023년 3월 26일

안녕하세요 면접 준비하던 취준생입니다..
포스트 잘 읽었습니다 도움이 많이 되었어요ㅠㅠ 감사합니다..!!
실례가 안된다면 혹시 어느 직무로 계신지 알 수 있을까요..??

1개의 답글