数据, 术→技巧

使用Optuna优化LightGBM超参数

钱魏Way · · 4 次浏览

在先前的文章,已经很详细的介绍了LightGBM的原理及使用示例。模型的安装与调用本身不会遇到很大的问题,实际使用过程中遇到的最大难题是如何优化超参数。由于没有进行很好的超参数优化导致产生的模型性能存在欠缺。先前也简单的介绍过使用Optuna进行超参数优化,同样存在的问题是介绍的不够详细,与实际应用还有一点差距。所以每次自己在使用的时候也在不断完善。今天的这篇文章就是基于最近的一些优化实践总结出来的。

LightGBM的核心超参数

对于LightGBM模型,以下是一些最常用的超参数:

  • ‘num_leaves’: 这是控制模型复杂度的主要参数。理想情况下,num_leaves的值应小于或等于2^(max_depth)
  • ‘learning_rate’: 通常在05-0.3之间。学习率过大可能会导致过拟合,过小则会使训练过程变得非常缓慢。
  • ‘min_data_in_leaf’: 这是一个防止过拟合的参数,它的值取决于训练数据的数量和num_leaves。设置得太大会导致模型欠拟合。
  • ‘max_depth’: 控制树的最大深度,可以用来防止过拟合。在LightGBM中,max_depth被设置为-1,表示没有限制。
  • ‘objective’: 对于二分类问题,设置为’binary’,对于多分类问题,设置为’multiclass’,’objective’: 对于回归问题,通常设置为’regression’
  • ‘metric’: 对于二分类问题,常使用’binary_logloss’和’auc’,对于多分类问题,常使用’multi_logloss’,’metric’: 对于回归问题,常使用’rmse’

以上只是一些常规的启动设置,实际上,这些参数应根据问题的具体情况进行精细调整,以获得最佳的模型性能。此外,还有一些其他参数,如:

  • ‘feature_fraction’: 取值范围为[0,1],用于指定在每次迭代(每棵树)中随机选择的特征的比例。较小的feature_fraction可以防止过拟合,同时可以大大减少训练时间。
  • ‘bagging_fraction’: 取值范围为[0,1],类似于随机森林中的子采样,这是在不进行重采样的情况下随机选择部分数据。当设置为5意味着LightGBM将在每棵树训练之前,随机选择一半的数据。如果设置为1,则表示使用所有数据进行训练。
  • ‘bagging_freq’: 如果设置的bagging_fraction小于1,那么可以通过设置bagging_freq来指定bagging的频率,在每k次迭代执行bagging,0表示禁用bagging。
  • ‘min_sum_hessian_in_leaf’: 也称为’min_child_weight’,它的值取决于样本权重,用于控制叶子节点中最小的海森矩阵之和。较大的值可以防止模型过拟合,但如果设置得过大,可能会导致欠拟合。
  • ‘lambda_l1’: L1正则化系数。添加L1正则化可以使模型更加稀疏(即使更多的权重变为0),这可以帮助防止过拟合。
  • ‘lambda_l2’: L2正则化系数。L2正则化可以使模型中不重要的权重接近0而不是完全是0,这可以防止过拟合。

使用Optuna优化LightGBM

分类示例

import optuna
import lightgbm as lgb
from sklearn.model_selection import train_test_split

categorical_features =   ['C1','C2', 'C3', 'C4']
X = train_data.iloc[:, :-1]
y = train_data.iloc[:, -1]

train_x, valid_x, train_y, valid_y = train_test_split(X, y, test_size=0.25)

def objective(trial):
    dtrain = lgb.Dataset(train_x, label=train_y, categorical_feature=categorical_features)

    param = {
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'learning_rate': trial.suggest_float('learning_rate', 1e-8, 1.0, log=True),
        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 2, 256),
        'max_depth': trial.suggest_int('max_depth', 2, 256),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_sum_hessian_in_leaf': trial.suggest_int('min_sum_hessian_in_leaf', 0, 100),
        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
        'objective': 'binary',
        'metric': 'binary_logloss',
        'verbosity': -1,
        "is_unbalance":True
    }

    cv_result = lgb.cv(param, dtrain, nfold=5, stratified=True, shuffle=True, callbacks=[lgb.early_stopping(stopping_rounds=10),])
    
    return cv_result['valid binary_logloss-mean'][-1]

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)

print('Number of finished trials: ', len(study.trials))
print('Best trial: score {}, params {}'.format(study.best_trial.value, study.best_trial.params))

这里主要使用了lgb.cv(),lgb.cv() 是该框架中的一个函数,用于执行交叉验证。该函数的主要参数包括:

  • params(dict):包含训练参数的字典。
  • train_set(Dataset):训练数据集。
  • num_boost_round(int):提升迭代的数量。
  • folds(generator 或 iterator):可选。如果指定了,CV 将使用这些自定义的折叠。如果没有指定,CV 将使用 nfold 参数创建随机折叠。
  • nfold(int):用于交叉验证的折叠数。如果没有指定 folds 参数,这个参数将会被使用。
  • stratified(bool):是否进行分层抽样。
  • shuffle(bool):是否在创建折叠时打乱数据。
  • metrics(string, list of strings or None):评估指标。如果设置为 None,将使用在 params 中指定的指标。
  • fobj(function):自定义的目标函数。
  • feval(function):定义的评估函数。
  • init_model(string, Booster or None):初始模型。如果是字符串,那么它是一个模型文件的名字。如果是 Booster,那么它是一个模型。
  • feature_name(list of strings or ‘auto’):特征名称。
  • categorical_feature(list of strings or ‘auto’):指定分类特征的名称。
  • early_stopping_rounds(int):如果一个验证集的评估指标在 early_stopping_rounds 轮迭代中没有改善,训练将停止。当提供了多个评估指标时,最后一个指标将被用来做早停。
  • fpreproc(function):预处理函数,它将在每一轮迭代之前对数据进行处理。
  • verbose_eval(bool, int, or None):是否显示训练的进度。如果是 None,将使用 verbose 参数。如果是 True,则对每一轮的结果进行打印。如果是 False,则不打印任何东西。如果是 int,则每 verbose_eval 轮打印一次进度。
  • show_stdv(bool):是否显示标准差。
  • seed(int):随机数种子。
  • callbacks(list of callback functions):用于自定义训练过程的回调函数列表。

LightGBM的lgb.cv()函数和sklearn的StratifiedKFold()函数

LightGBM的lgb.cv()函数和sklearn的StratifiedKFold()函数都是用于交叉验证的。交叉验证是一种统计学方法,其目的是评估机器学习模型在独立的数据集上的表现。这两者的主要区别在于应用和灵活性:

  • cv():这是特定于LightGBM模型的交叉验证函数。它直接在模型训练中实现交叉验证,简化了流程。当使用lgb.cv()时,你可以直接得到每次迭代的训练和验证分数,以及标准偏差等信息。此外,它还提供了early_stopping_rounds参数,可以用于提前停止训练以防止过拟合。
  • StratifiedKFold():这是sklearn中的一个函数,用于实现分层k折交叉验证。这是一种更通用的交叉验证技术,可以用于任何模型。它保证在每个折中的每个类别的比例与整个数据集中的比例相同。这个函数提供了更多的灵活性,因为你可以在交叉验证期间使用不同的模型和评估策略。

我们将使用 StratifiedKFold() 替代 lgb.cv()。因为 StratifiedKFold() 可以确保在每个折中的每个类别的比例与整个数据集中的比例相同。这对于解决分类问题很有帮助。以下是如何改写上面的代码:

import optuna
import lightgbm as lgb
from sklearn.datasets import load_breast_cancer
from sklearn.metrics import log_loss
from sklearn.model_selection import train_test_split, StratifiedKFold

data, target = load_breast_cancer(return_X_y=True)
train_x, valid_x, train_y, valid_y = train_test_split(data, target, test_size=0.25)

def objective(trial):
    param = {
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'learning_rate': trial.suggest_float('learning_rate', 1e-8, 1.0, log=True),
        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 2, 256),
        'max_depth': trial.suggest_int('max_depth', 2, 256),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_sum_hessian_in_leaf': trial.suggest_int('min_sum_hessian_in_leaf', 0, 100),
        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
        'objective': 'binary',
        'verbosity': -1
    }
    
    # StratifiedKFold cross validation
    skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)

    logloss = []
    for train_idx, valid_idx in skf.split(train_x, train_y):
        dtrain = lgb.Dataset(train_x[train_idx], label=train_y[train_idx])
        dvalid = lgb.Dataset(train_x[valid_idx], label=train_y[valid_idx])
        model = lgb.train(param, dtrain)
        pred = model.predict(train_x[valid_idx])
        loss = log_loss(train_y[valid_idx], pred)
        logloss.append(loss)

    return sum(logloss) / len(logloss)

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)

print('Number of finished trials: ', len(study.trials))
print('Best trial: score {}, params {}'.format(study.best_trial.value, study.best_trial.params))

回归示例

import optuna
import lightgbm as lgb
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split

data, target = load_boston(return_X_y=True)
train_x, valid_x, train_y, valid_y = train_test_split(data, target, test_size=0.25)

def objective(trial):
    dtrain = lgb.Dataset(train_x, label=train_y)

    param = {
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'learning_rate': trial.suggest_float('learning_rate', 1e-8, 1.0, log=True),
        'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 2, 256),
        'max_depth': trial.suggest_int('max_depth', 2, 256),
        'feature_fraction': trial.suggest_float('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_float('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_sum_hessian_in_leaf': trial.suggest_int('min_sum_hessian_in_leaf', 0, 100),
        'lambda_l1': trial.suggest_float('lambda_l1', 1e-8, 10.0, log=True),
        'lambda_l2': trial.suggest_float('lambda_l2', 1e-8, 10.0, log=True),
        'objective': 'regression',
        'metric': 'rmse',
        'verbosity': -1
    }

    cv_result = lgb.cv(param, dtrain, nfold=5, stratified=False, shuffle=True, early_stopping_rounds=10)
    return cv_result['rmse-mean'][-1]

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=100)

print('Number of finished trials: ', len(study.trials))
print('Best trial: score {}, params {}'.format(study.best_trial.value, study.best_trial.params))

LightGBM 的 lgb.cv() 函数执行交叉验证时,会输出一个包含多个指标的字典,这些指标反映了模型在不同轮次的表现。cv_result 中包含的具体指标取决于你设置的参数,尤其是 metric 参数。下面是一些常见的指标:

  1. 损失函数的平均值和标准差:对于每个指定的评估指标(如 binary_loglossl2auc 等),cv_result 会包含该指标的平均值和标准差,格式通常为 [指标名]-mean[指标名]-stdv。例如,如果使用二分类对数损失(binary_logloss),则会有 binary_logloss-meanbinary_logloss-stdv
  2. 训练损失(如果设置):如果在 lgb.cv() 中设置了 return_train_score=True,则还会包含训练集上的指标,格式为 [指标名]-train-mean[指标名]-train-stdv
  3. 早停轮次(如果使用早停):如果在 lgb.cv() 中使用了早停(early_stopping_rounds 参数),则 cv_result 会包含模型在哪个轮次达到最佳性能,例如 best_iteration 或类似的键。
  4. 其他自定义指标:如果你使用了自定义的评估函数,那么这些自定义指标也会出现在 cv_result 中。

需要注意的是,具体的指标取决于你在 lgb.cv() 调用中设置的 metric 参数以及其他相关参数。始终建议检查 cv_result 的输出,以了解在你的特定设置下哪些指标可用。

理解代码中的 cv_result[‘rmse-mean’][-1]:在这段代码中,cv_result[‘rmse-mean’]是一个列表,每个元素对应一轮迭代后的平均对数损失。当你访问这个列表的 [-1] 元素时,它返回的是列表中的最后一个元素,即最终一轮迭代后的平均对数损失。在交叉验证中,通常关心的是最后一轮迭代的结果,因为它代表了模型在经过所有训练后的性能。因此,cv_result[‘rmse-mean’][-1][-1] 表示的是在所有交叉验证轮次完成后,模型的最终平均对数损失,通常用这个值作为模型性能的指标。

optuna.integration.XGBoostPruningCallback()的使用

optuna.integration.LightGBMPruningCallback() 是Optuna库内置的一个类,其目的是为了对LightGBM训练过程进行早期停止(也被称为pruning)。

这是一种防止过拟合的技术。在训练模型时,我们通常会在某个验证集上评估模型的性能。如果发现模型在验证集上的性能停止提升(或者开始下降),我们就可以提前终止训练,防止模型在训练集上过度拟合。

在LightGBM中,可以使用LightGBMPruningCallback来进行早期停止。它会在每一轮训练之后,检查模型在验证集上的性能是否有提升。如果在一定的连续轮数(即patience)内,模型的性能没有提升,那么就会提前终止训练。

LightGBMPruningCallback的使用非常简单。在调用lightgbm.train函数的时候,只需要将其实例添加到callbacks参数中就可以了。

下面是一个使用的例子:

import optuna
import lightgbm as lgb

def objective(trial):
    train_data = lgb.Dataset(X_train, label=y_train)
    valid_data = lgb.Dataset(X_valid, label=y_valid, reference=train_data)

    params = {
        'objective': 'binary',
        'metric': 'auc',
        'verbosity': -1,
        'boosting_type': 'gbdt',
        'lambda_l1': trial.suggest_loguniform('lambda_l1', 1e-8, 10.0),
        'lambda_l2': trial.suggest_loguniform('lambda_l2', 1e-8, 10.0),
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'feature_fraction': trial.suggest_uniform('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_uniform('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
    }

    pruning_callback = optuna.integration.LightGBMPruningCallback(trial, 'auc')
    gbm = lgb.train(params, train_data, valid_sets=[valid_data], callbacks=[pruning_callback])

    preds = gbm.predict(X_valid)
    pred_labels = np.rint(preds)
    accuracy = sklearn.metrics.accuracy_score(y_valid, pred_labels)
    return accuracy

study = optuna.create_study(pruner=optuna.pruners.MedianPruner(n_warmup_steps=10), direction='maximize')
study.optimize(objective, n_trials=100)

在这个例子中,LightGBMPruningCallback将trial(Optuna的试验对象)和’auc’(需要监控的指标)作为输入参数。在每一轮训练后,LightGBM都会调用我们的回调函数,如果发现性能没有提升,那么Optuna就会抛出一个TrialPruned异常,从而提前终止训练。

你可以按照以下方式将代码修改为使用lgb.cv():

import optuna
import lightgbm as lgb

def objective(trial):
    train_data = lgb.Dataset(X, label=y)

    params = {
        'objective': 'binary',
        'metric': 'auc',
        'verbosity': -1,
        'boosting_type': 'gbdt',
        'lambda_l1': trial.suggest_loguniform('lambda_l1', 1e-8, 10.0),
        'lambda_l2': trial.suggest_loguniform('lambda_l2', 1e-8, 10.0),
        'num_leaves': trial.suggest_int('num_leaves', 2, 256),
        'feature_fraction': trial.suggest_uniform('feature_fraction', 0.4, 1.0),
        'bagging_fraction': trial.suggest_uniform('bagging_fraction', 0.4, 1.0),
        'bagging_freq': trial.suggest_int('bagging_freq', 1, 7),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 100),
    }

    # Add a callback for pruning.
    pruning_callback = optuna.integration.LightGBMPruningCallback(trial, 'auc-mean')
    cv_results = lgb.cv(params, train_data, nfold=5, callbacks=[pruning_callback], num_boost_round=100)
    return cv_results['auc-mean'][-1]

study = optuna.create_study(pruner=optuna.pruners.MedianPruner(n_warmup_steps=10), direction='maximize')
study.optimize(objective, n_trials=100)

这个例子中的LightGBMPruningCallback监控指标为”auc-mean”,它是lgb.cv()在交叉验证中返回的性能指标。在objective()函数的返回值,我们返回了最大的测试AUC,所以我们需要把optuna.create_study()中的direction参数设为’maximize’。

参考链接:

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注