前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >LightGBM+Optuna 建模自动调参教程!

LightGBM+Optuna 建模自动调参教程!

作者头像
Python数据科学
发布于 2023-08-29 11:20:50
发布于 2023-08-29 11:20:50
1.5K00
代码可运行
举报
文章被收录于专栏:Python数据科学Python数据科学
运行总次数:0
代码可运行

kaggle机器学习竞赛赛中有一个调参神器组合非常热门,在很多个top方案中频频出现LightGBM+Optuna。知道很多小伙伴苦恼于漫长的调参时间里,这次结合一些自己的经验,给大家带来一个LGBM模型+OPTUNA调参的使用教程,这对可谓是非常实用且容易上分的神器组合了,实际工作中也可使用。

关于LightGBM不多说了,之前分享过很多文章,它是在XGBoost基础上对效率提升的优化版本,由微软发布的,运行效率极高,且准确度不降。目前是公认比较好,且广泛使用的机器学习模型了,分类回归均可满足。

关于调参,也就是模型的超参数调优,可能你会想到GridSearch。确实最开始我也在用GridSearch,暴力美学虽然好,但它的缺点很明显,运行太耗时,时间成本太高。相比之下,基于贝叶斯框架下的调参工具就舒服多了。这类开源工具也很多,常见的比如HyperOPT。当然今天主角不是它,而是另外一个更香的OPTUNA,轻量级且功能更强大,速度也是快到起飞!

因为需要用 LGBM 配合举例讲解,下面先从 LGBM 的几个主要超参数开始介绍,然后再根据这些超参设置 Optuna 进行调参。

LightGBM参数概述

通常,基于树的模型的超参数可以分为 4 类:

  1. 影响决策树结构和学习的参数
  2. 影响训练速度的参数
  3. 提高精度的参数
  4. 防止过拟合的参数

大多数时候,这些类别有很多重叠,提高一个类别的效率可能会降低另一个类别的效率。如果完全靠手动调参,那会比较痛苦。所以前期我们可以利用一些自动化调参工具给出一个大致的结果,而自动调参工具的核心在于如何给定适合的参数区间范围。 如果能给定合适的参数网格,Optuna 就可以自动找到这些类别之间最平衡的参数组合。

下面对LGBM的4类超参进行介绍。

1、控制树结构的超参数

max_depth 和 num_leaves

LGBM 中,控制树结构的最先要调的参数是max_depth(树深度) 和 num_leaves(叶子节点数)。这两个参数对于树结构的控制最直接了断,因为 LGBMleaf-wise 的,如果不控制树深度,会非常容易过拟合。max_depth一般设置可以尝试设置为3到8

这两个参数也存在一定的关系。由于是二叉树,num_leaves最大值应该是2^(max_depth)。所以,确定了max_depth也就意味着确定了num_leaves的取值范围。

min_data_in_leaf

树的另一个重要结构参数是min_data_in_leaf,它的大小也与是否过拟合有关。它指定了叶子节点向下分裂的的最小样本数,比如设置100,那么如果节点样本数量不够100就停止生长。当然,min_data_in_leaf的设定也取决于训练样本的数量和num_leaves。对于大数据集,一般会设置千级以上。

提高准确性的超参数

learning_rate 和 n_estimators

实现更高准确率的常见方法是使用更多棵子树并降低学习率。换句话说,就是要找到LGBMn_estimatorslearning_rate的最佳组合。

n_estimators控制决策树的数量,而learning_rate是梯度下降的步长参数。经验来说,LGBM 比较容易过拟合,learning_rate可以用来控制梯度提升学习的速度,一般值可设在 0.01 和 0.3 之间。一般做法是先用稍多一些的子树比如1000,并设一个较低的learning_rate,然后通过early_stopping找到最优迭代次数。

max_bin

除此外,也可以增加max_bin(默认值为255)来提高准确率。因为变量分箱的数量越多,信息保留越详细,相反,变量分箱数量越低,信息越损失,但更容易泛化。这个和特征工程的分箱是一个道理,只不过是通过内部的hist直方图算法处理了。如果max_bin过高,同样也存在过度拟合的风险。

更多超参数来控制过拟合

lambda_l1 和 lambda_l2

lambda_l1lambda_l2 对应着 L1L2 正则化,和 XGBoostreg_lambdareg_alpha 是一样的,对叶子节点数和叶子节点权重的惩罚,值越高惩罚越大。这些参数的最佳值更难调整,因为它们的大小与过拟合没有直接关系,但会有影响。一般的搜索范围可以在 (0, 100)

min_gain_to_split

这个参数定义着分裂的最小增益。这个参数也看出数据的质量如何,计算的增益不高,就无法向下分裂。如果你设置的深度很深,但又无法向下分裂,LGBM就会提示warning,无法找到可以分裂的了。参数含义和 XGBoostgamma 是一样,说明数据质量已经达到了极限了。比较保守的搜索范围是 (0, 20),它可以用作大型参数网格中的额外正则化。

bagging_fraction 和 feature_fraction

这两个参数取值范围都在(0,1)之间。

feature_fraction指定训练每棵树时要采样的特征百分比,它存在的意义也是为了避免过拟合。因为有些特征增益很高,可能造成每棵子树分裂的时候都会用到同一个特征,这样每个子树就同质化了。而如果通过较低概率的特征采样,可以避免每次都遇到一样的强特征,从而让子树的特征变得差异化,即泛化。

bagging_fraction指定用于训练每棵树的训练样本百分比。要使用这个参数,还需要设置 bagging_freq,道理和feature_fraction一样,也是让没棵子树都变得好而不同

在 Optuna 中创建搜索网格

Optuna 中的优化过程首先需要一个目标函数,该函数里面包括:

  • 字典形式的参数网格
  • 创建一个模型(可以配合交叉验证kfold)来尝试超参数组合集
  • 用于模型训练的数据集
  • 使用此模型生成预测
  • 根据用户定义的指标对预测进行评分并返回

下面给出一个常用的框架,模型是5折的Kfold,这样可以保证模型的稳定性。最后一行返回了需要优化的 CV 分数的平均值。目标函数可以自己设定,比如指标logloss最小,auc最大,ks最大,训练集和测试集的auc差距最小等等。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import optuna  # pip install optuna
from sklearn.metrics import log_loss
from sklearn.model_selection import StratifiedKFold

def objective(trial, X, y):
    # 后面填充
    param_grid = {}
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1121218)

    cv_scores = np.empty(5)
    for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
        X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
        y_train, y_test = y[train_idx], y[test_idx]

        model = lgbm.LGBMClassifier(objective="binary", **param_grid)
        model.fit(
            X_train,
            y_train,
            eval_set=[(X_test, y_test)],
            eval_metric="binary_logloss",
            early_stopping_rounds=100,
        )
        preds = model.predict_proba(X_test)
        cv_scores[idx] = preds

    return np.mean(cv_scores)

下面是参数的设置,Optuna比较常见的参数设置方式有suggest_categoricalsuggest_intsuggest_float。其中,suggest_intsuggest_float的设置方式为(参数,最小值,最大值,step=步长)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
def objective(trial, X, y):
    # 字典形式的参数网格
    param_grid = {
        "n_estimators": trial.suggest_categorical("n_estimators", [10000]),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3),
        "num_leaves": trial.suggest_int("num_leaves", 20, 3000, step=20),
        "max_depth": trial.suggest_int("max_depth", 3, 12),
        "min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 200, 10000, step=100),
        "max_bin": trial.suggest_int("max_bin", 200, 300),
        "lambda_l1": trial.suggest_int("lambda_l1", 0, 100, step=5),
        "lambda_l2": trial.suggest_int("lambda_l2", 0, 100, step=5),
        "min_gain_to_split": trial.suggest_float("min_gain_to_split", 0, 15),
        "bagging_fraction": trial.suggest_float(
            "bagging_fraction", 0.2, 0.95, step=0.1
        ),
        "bagging_freq": trial.suggest_categorical("bagging_freq", [1]),
        "feature_fraction": trial.suggest_float(
            "feature_fraction", 0.2, 0.95, step=0.1
        ),
    }

创建 Optuna 自动调起来

下面是完整的目标函数框架,供参考:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
from optuna.integration import LightGBMPruningCallback

def objective(trial, X, y):
    # 参数网格
    param_grid = {
        "n_estimators": trial.suggest_categorical("n_estimators", [10000]),
        "learning_rate": trial.suggest_float("learning_rate", 0.01, 0.3),
        "num_leaves": trial.suggest_int("num_leaves", 20, 3000, step=20),
        "max_depth": trial.suggest_int("max_depth", 3, 12),
        "min_data_in_leaf": trial.suggest_int("min_data_in_leaf", 200, 10000, step=100),
        "lambda_l1": trial.suggest_int("lambda_l1", 0, 100, step=5),
        "lambda_l2": trial.suggest_int("lambda_l2", 0, 100, step=5),
        "min_gain_to_split": trial.suggest_float("min_gain_to_split", 0, 15),
        "bagging_fraction": trial.suggest_float("bagging_fraction", 0.2, 0.95, step=0.1),
        "bagging_freq": trial.suggest_categorical("bagging_freq", [1]),
        "feature_fraction": trial.suggest_float("feature_fraction", 0.2, 0.95, step=0.1),
        "random_state": 2021,
    }
    # 5折交叉验证
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1121218)

    cv_scores = np.empty(5)
    for idx, (train_idx, test_idx) in enumerate(cv.split(X, y)):
        X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
        y_train, y_test = y[train_idx], y[test_idx]
        
        # LGBM建模
        model = lgbm.LGBMClassifier(objective="binary", **param_grid)
        model.fit(
            X_train,
            y_train,
            eval_set=[(X_test, y_test)],
            eval_metric="binary_logloss",
            early_stopping_rounds=100,
            callbacks=[
                LightGBMPruningCallback(trial, "binary_logloss")
            ],
        )
        # 模型预测
        preds = model.predict_proba(X_test)
        # 优化指标logloss最小
        cv_scores[idx] = log_loss(y_test, preds)

    return np.mean(cv_scores)

上面这个网格里,还添加了LightGBMPruningCallback,这个callback类很方便,它可以在对数据进行训练之前检测出不太好的超参数集,从而显着减少搜索时间。

设置完目标函数,现在让参数调起来!

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
study = optuna.create_study(direction="minimize", study_name="LGBM Classifier")
func = lambda trial: objective(trial, X, y)
study.optimize(func, n_trials=20)

direction可以是minimize,也可以是maximize,比如让auc最大化。然后可以设置trials来控制尝试的次数,理论上次数越多结果越优,但也要考虑下运行时间。

搜索完成后,调用best_valuebast_params属性,调参就出来了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
print(f"\tBest value (rmse): {study.best_value:.5f}")
print(f"\tBest params:")

for key, value in study.best_params.items():
    print(f"\t\t{key}: {value}")
    
-----------------------------------------------------
Best value (binary_logloss): 0.35738
 Best params:
  device: gpu
  lambda_l1: 7.71800699380605e-05
  lambda_l2: 4.17890272377219e-06
  bagging_fraction: 0.7000000000000001
  feature_fraction: 0.4
  bagging_freq: 5
  max_depth: 5
  num_leaves: 1007
  min_data_in_leaf: 45
  min_split_gain: 15.703519227860273
  learning_rate: 0.010784015325759629
  n_estimators: 10000

得到这个参数组合后,我们就可以拿去跑模型了,看结果再手动微调,这样就可以省很多时间了。

结语

本文给出了一个通过Optuna调参LGBM的代码框架,使用及其方便,参数区间范围需要根据数据情况自行调整,优化目标可以自定定义,不限于以上代码的logloss

推荐阅读

👉pandas进阶宝典

👉数据挖掘实战项目

👉机器学习入门

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python数据科学 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
猫狗队列
实现一种狗猫队列的结构,要求如下: 用户可以调用add方法将cat类或dog类的实例放入队列中; 用户可以调用pollAll方法,将队列中所有的实例按照进队列 的先后顺序依次弹出; 用户可以调用pollDog方法,将队列中dog类的实例按照 进队列的先后顺序依次弹出; 用户可以调用pollCat方法,将队列中cat类的实 例按照进队列的先后顺序依次弹出; 用户可以调用isEmpty方法,检查队列中是 否还有dog或cat的实例; 用户可以调用isDogEmpty方法,检查队列中是否有dog 类的实例; 用户可以调用isCatEmpty方法,检查队列中是否有cat类的实例。
名字是乱打的
2022/05/13
3340
左程云算法一星难度题目刷题(1)
一.栈 1.getmin栈 class MyStack{     public MyStack(Stack<Integer> stackData, Stack<Integer> stackMin) {         this.stackData = stackData;         this.stackMin = stackMin;     }     private Stack<Integer> stackData; //存所有值的     private Stack<Integer> stac
盒光曈辰
2021/11/27
3940
Java中队列(Queue)用法
队列(Queue)是一种特殊类型的集合,它遵循先进先出(FIFO - First In First Out)原则,这意味着第一个添加到队列的元素将是第一个被移除的元素。
王也518
2024/04/25
2410
栈和队列的相关问题
 队列可能稍微有点复杂,定义队列的时候需要定义三个变量,分别是end,start,size,先说说他们分别的作用,每次用户拿队中的元素,都从start下标位置取,每次进队都从s=end位置进,每次出队或者进队size都要++或--  假设数组长度是3,如果size没有到3,进队时就把元素放到end的位置上,这是end和size之间的约束关系;如果size不等于0,出队时就总出start位置,这是start和size之间的约束关系。end本身还有一个约束关系,end是控制进队的,所以每次进一个元素end就++,如果end==数组长度,那么end就回到开头也就是0位置
mathor
2018/08/17
7250
栈和队列的相关问题
基于数组的有界阻塞队列 —— ArrayBlockingQueue
" 在阅读完和 AQS 相关的锁以及同步辅助器之后,来一起阅读 JUC 下的和队列相关的源码。先从第一个开始:ArrayBlockingQueue。 "
程序员小航
2020/11/23
9380
基于数组的有界阻塞队列 —— ArrayBlockingQueue
两个栈实现队列以及两个队列实现栈
两个队列实现栈 思路:队列queue是专职进出栈的,队列help只是个中转站,起辅助作用。 入栈:直接入队列queue即可 出栈:把queue的除最后一个元素外全部转移到队help中,然后把刚才剩下queue中的那个元素出队列。之后把q2中的全部元素转移回q1中(或者两个队列互换) 入栈:
用户5513909
2023/04/25
2580
两个栈实现队列以及两个队列实现栈
由两个栈组成的队列
栈的特点是先进后出,队列的特点是先进先出,使用两个栈正好能把顺序反过来实现类似队列的操作。
HelloVass
2018/09/12
4680
栈和队列中的算法题
编写一个类,用两个栈实现队列,支持队列的基本操作(add, poll, peek)
归思君
2023/10/16
1900
数据结构与算法(2)——栈和队列栈队列LeetCode 相关题目整理其他题目整理
栈是一种用于存储数据的简单数据结构(与链表类似)。数据入栈的次序是栈的关键。可以把一桶桶装的薯片看作是一个栈的例子,当薯片做好之后,它们会依次被添加到桶里,每一片都会是当前的最上面一片,而每次我们取的时候也是取的最上面的那一片,规定你不能破坏桶也不能把底部捅穿,所以第一个放入桶的薯片只能最后一个从桶里取出;
我没有三颗心脏
2018/07/24
1.3K0
栈和队列深入浅出
1. 概念: 栈一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守先进后出的原则。
用户11305962
2024/10/09
1250
栈和队列深入浅出
基于链表的有界阻塞队列 —— LinkedBlockingQueue
" 上一节看了基于数据的有界阻塞队列 ArrayBlockingQueue 的源码,通过阅读源码了解到在 ArrayBlockingQueue 中入队列和出队列操作都是用了 ReentrantLock 来保证线程安全。下面咱们看另一种有界阻塞队列:LinkedBlockingQueue。 "
程序员小航
2020/11/23
6200
基于链表的有界阻塞队列 —— LinkedBlockingQueue
《Java初阶数据结构》----4.<线性表---Stack栈和Queue队列>
用户11288958
2024/09/24
950
《Java初阶数据结构》----4.<线性表---Stack栈和Queue队列>
【数据结构与算法-初学者指南】【附带力扣原题】队列
在计算机科学中,队列是一种常见的数据结构,它可以用于多种场景,例如任务调度、事件处理等。本篇博客将介绍队列的基本原理和常见操作,并探讨如何使用数组模拟队列的操作以及该方法的优缺点及性能影响。最后,我们将针对基于数组的队列算法题目提供解题思路和优化方法的讨论。
苏泽
2024/03/01
1720
数据结构-4.栈与队列
栈: 一种特殊的线性表, 只许在固定的一端进行插入和删除元素操作. 进行数据插入和删除操作的一端称为栈顶, 另一端称为栈底. 栈中的数据元素遵守后进先出的原则.
用户11369350
2024/11/19
610
数据结构-4.栈与队列
两个队列实现栈结构
实现思路: 一个存放我们数据的栈,每次我们添数据时候把数据放到我们这个data队列中 一个help队列,每次我们data队列出数据时候,将前面的数据都复制导入我们help队列,留最后一个数据弹出.最后交换引用,让help队列成为新的data队列,让空的data队列成为新的help队列
名字是乱打的
2022/05/13
3650
两个队列实现栈结构
队列及其经典面试题
由于出队操作只能在队列的头部进行,若采用数组的方案,每次出队一个元素就得搬移剩下的所有元素向前移动一个单位。
VIBE
2022/12/02
3110
【Java】栈和队列详解!!!
细心的同学观察图片和表格中的方法会发现,图片中并没有size方法,是因为Stack继承于Vector,他使用的size方法是Vector中的方法;
喜欢做梦
2024/11/25
6430
【Java】栈和队列详解!!!
Java对阻塞队列的实现ArrayBlockingQueueLinkedBlockingQueue
什么是阻塞队列? 阻塞队列与队列基本一致,额外的支持阻塞添加和阻塞删除方法. 阻塞添加: 当队列满时,线程不断尝试向其中添加,直到有其他线程取走元素,使添加操作成功,在此期间,线程阻塞. 阻塞删除:
呼延十
2019/07/01
7540
Java对阻塞队列的实现ArrayBlockingQueueLinkedBlockingQueue
【Java-数据结构篇】Java 中栈和队列:构建程序逻辑的关键数据结构基石
2.1 使用内置的 Stack 类 2.1.1 Stack 类的基本方法与操作示例
学无止尽5
2025/05/31
1600
【Java-数据结构篇】Java 中栈和队列:构建程序逻辑的关键数据结构基石
Java的栈与队列以及代码实现
栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶 栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等。 例如这把枪,第一发子弹是最后发射的,第一发子弹在栈底,而最新安装上去的子弹在栈的顶部,只有将上面的子弹打完(栈顶的数据走完),最后一发子弹才会射出
如烟花般绚烂却又稍纵即逝
2024/11/26
1650
Java的栈与队列以及代码实现
推荐阅读
相关推荐
猫狗队列
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档