본문 바로가기
스터디/데이터사이언스

Optuna & LightGBM

by 궁금한 준이 2023. 9. 19.
728x90
반응형

※ 원래 velog에 있던 내 글을 티스토리에 옮김

 

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

 

Optuna references

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

https://www.kaggle.com/code/corochann/optuna-tutorial-for-hyperparameter-optimization/notebook

 

Optuna tutorial for hyperparameter optimization

Explore and run machine learning code with Kaggle Notebooks | Using data from multiple data sources

www.kaggle.com

https://www.kaggle.com/code/bjoernholzhauer/lightgbm-tuning-with-optuna/notebook

 

LightGBM & tuning with optuna

Explore and run machine learning code with Kaggle Notebooks | Using data from Titanic - Machine Learning from Disaster

www.kaggle.com

반응형

 

예제 코드

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를 저장할 수 있도록 했다.

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

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

callback 도 구현하였다

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

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']

...(후략)...
728x90
반응형