基于LightGBM的集成学习算法
目录
- 一、LightGBM基本原理
- 1.1 基于直方图的决策树算法
- 1.1.1 连续变量分箱
- 1.2 互斥特征捆绑
- 1.2.1 互斥特征捆绑计算流程
- 1.2.2 互斥特征捆绑算法基本原理
- 1.2.2.1 冲突比例(conflict_rate)
- 1.2.2.2 图着色
- 1.2.2.3 特征捆绑
- 1.3 基于梯度的单边采样(Gradient-based One-Side Sampling,GOSS)
- 1.3.1 GOSS计算基本原理
- 1.3.1.1 GOSS 的核心思想
- 1.3.1.2 GOSS计算实例
- 二、LightGBM的Sklearn实现
- 2.1 LGBMClassifier超参数概览
- 2.1.1 决策树剪枝超参数
- 2.1.2 Boosting过程控制超参数
- 2.1.2.1 借助验证曲线进行验证
- 2.1.3 特征和数据处理类超参数
- 2.1.4 其他超参数
- 2.2 LGBMRegressor超参数概览
- 三、LightGBM超参数优化
- 3.1 基于贝叶斯优化器的超参数优化
- 3.1.1 核心思想
- 3.1.2 优势
- 3.1.3 超参数优化案例
- 3.1.4 优化案例
- 3.1.4.1 1.HyperOPT搜索基本流程
- 3.2 基于网格搜索的超参数优化
- 3.2.1 网格搜索注意事项
- 3.2.2 优化案例
常见的机器学习算法:
机器学习算法(一)—决策树
机器学习算法(二)—支持向量机SVM
机器学习算法(三)—K近邻
机器学习算法(四)—集成算法
基于XGBoost的集成学习算法
基于LightGBM的集成学习算法
机器学习算法(五)—聚类
机器学习算法(六)—逻辑回归
机器学习算法(七)—Apriori 关联分析
机器学习算法(八)—朴素贝叶斯
九种降维方法汇总
一、LightGBM基本原理
LightGBM 是一种高效的梯度提升(Gradient Boosting)算法。如果说 XGBoost 在提升 GBDT 类算法的计算精度上实现了里程碑式的突破,那么 LightGBM 则在计算效率和内存优化方面提出了开创性的解决方案。通过引入基于直方图的决策树算法、 互斥特征捆绑和单边梯度采样 等技术,LightGBM 显著提升了计算效率,相比传统方法提速近 20 倍,同时将内存占用减少了 80%。这一突破使得 GBDT 类算法(作为机器学习领域精度最高的预测算法之一)能够真正应用于海量数据的建模与预测任务,极大地拓展了其在实际场景中的应用范围。
尽管 LightGBM 在计算精度上存在“选择性的牺牲”,但其实际建模效果仍能接近 XGBoost 的水平。更重要的是,这种“选择性的牺牲”从另一个角度来看,实际上起到了抑制模型过拟合的作用。因此,在许多实际场景中,LightGBM 的算法表现甚至优于 XGBoost,尤其是在数据规模较大或特征维度较高的场景下,LightGBM 的高效性和鲁棒性使其成为更优的选择。
1.1 基于直方图的决策树算法
传统的GBDT算法在训练过程中需要对每个特征进行遍历和排序,以便找到最佳的分裂点。这种方式在数据集非常大时,计算开销和内存占用都非常高。LightGBM 引入了直方图方法,在构建决策树时,将连续的特征值离散化为多个区间(连续变量分箱)。这样,算法不再遍历每个原始的特征值,而是只需要考虑几个离散化后的区间,极大提高了计算效率,尤其是在数据量非常大的时候。
1.1.1 连续变量分箱
LightGBM 采用的连续变量分箱方法是一种基于等宽分箱的简单策略,这与特征工程中常见的等宽分箱方法类似。具体来说,首先计算连续变量的取值范围,然后根据用户设定的超参数 max_bin
,将取值范围划分为数量为 max_bin
的等宽区间,并将连续变量的值映射到对应的箱体中。例如,假设某连续变量的取值范围为 [0, 10]
,且 max_bin=2
,则会将区间划分为 bin0=[0, 5)
和 bin1=[5, 10]
。此时,若某连续变量的取值为 1
,则会被映射到 bin0
(或标记为 0
);若取值为 10
,则会被映射到 bin1
(或标记为 1
)。通过这种方式,连续变量被转化为离散变量,从而简化了模型的计算复杂度,同时保留了数据的主要分布特征。
这种分箱方法虽然简单,但在实际应用中非常高效,尤其是在处理大规模数据时,能够显著降低计算开销。此外,等宽分箱的离散化过程也为后续的直方图加速算法奠定了基础,进一步提升了 LightGBM 的训练效率。(XGB也会对连续变量进行分箱,但XGB的分箱是分位数分箱,而不是等宽分箱。)
import numpy as np
import pandas as pd
np.random.seed(22)
x1 = np.array([1.2, 2.9, 2.6, 3.3, 2.0, 2.5, 1.4, 2.1, 1.7, 3.0])
x2 = np.array([4.7, 5.5, 3.9, 6.2, 3.5, 4.5, 5.1, 2.7, 4.1, 3.8])
x3 = np.random.randint(0, 2, 10)
x4 = np.random.randint(0, 2, 10)
y = np.array([1, 0, 1, 0, 1, 1, 0, 0, 1, 1])
data = pd.DataFrame({'x1':x1, 'x2':x2, 'x3':x3, 'x4':x4, 'y':y})
data
数据集中x1和x2是连续特征,x3和x4是离散特征,y是标签,对连续变量进行分箱,设置max_bin=2,即进行两个箱体的等宽分箱。
from sklearn.preprocessing import KBinsDiscretizer # 初始化 KBinsDiscretizer
n_bins = 2
strategy = 'uniform'
kbins = KBinsDiscretizer(n_bins=n_bins, encode='ordinal', strategy=strategy)
# 对 x1 和 x2 进行分箱处理
data['x1_binned'] = kbins.fit_transform(data[['x1']])
data['x2_binned'] = kbins.fit_transform(data[['x2']])
# 输出结果
print(data)
1.2 互斥特征捆绑
原始的 GBDT 在每棵树的训练过程中,需要遍历全部数据来计算信息增益,从而确定决策树的最佳切分点。尽管这种方法能够保证切分点的精确性,但其计算复杂度非常高(与特征数量和样本数量成正比),在面对海量数据时,会消耗大量的计算资源和时间。因此,为了高效处理大规模数据的模型训练,样本采样和特征降维成为了必要的优化手段。然而,传统方法在这方面往往存在明显缺陷:例如,简单的欠采样(如随机抽样)可能导致模型训练过程不稳定,而 PCA 降维仅适用于处理冗余特征,当每个特征都包含重要信息时,强行降维会导致大量信息丢失。
为了解决这些问题,LightGBM 提出了两项开创性的技术:基于梯度的单边采样方法(GOSS)和互斥特征捆绑方法(EFB)。GOSS 通过对样本进行智能采样,优先保留梯度较大的样本(即对模型训练贡献更大的样本),同时随机抽取部分梯度较小的样本,从而在减少数据量的同时,最大限度地保留信息增益的准确性。EFB 则通过将互斥特征(即很少同时取非零值的特征)捆绑在一起,有效减少特征数量,从而降低计算复杂度。与传统的采样和降维方法不同,GOSS 和 EFB 在显著提升计算效率的同时,能够很好地兼顾模型的预测精度,为海量数据的建模提供了高效且可靠的解决方案。
这两项技术的引入,使得 LightGBM 在处理大规模数据时,不仅能够大幅减少计算时间和内存占用,还能保持甚至提升模型的性能,从而成为工业界和学术界广泛应用的强大工具。
1.2.1 互斥特征捆绑计算流程
互斥特征捆绑算法,其实是受到独热编码启发,设计的类似于独热编码逆向过程的一种算法。例如一组数据情况如下,独热编码是从左往右的计算过程,把一列展开为多列,而互斥特征捆绑则是从右往左进行计算,将多列压缩为一列:
在实际计算过程中,互斥特征捆绑是找到那些原始状态下就存在类似上图中x1和x2这种关系的特征,来将其合成为一个特征。上图中x1和x2两个特征存在一种这样的关系——任何一条样本都不会同时在x1和x2上取值非0,这种关系的特征被称作互斥特征,而互斥特征其实是可以合成一个特征的,比如上图中的x,这个合成的过程并不会有任何的信息损失,而合成的过程又被称作特征捆绑。这也就是所谓的互斥特征捆绑算法。
1.2.2 互斥特征捆绑算法基本原理
1.2.2.1 冲突比例(conflict_rate)
在实际应用中,互斥特征捆绑的计算过程相对复杂,尤其是在定义特征之间的“互斥”关系时。互斥特征捆绑并不局限于完全互斥的特征,而是通过引入一个灵活的冲突比例(也称为非互斥比例)来衡量特征之间的互斥程度。冲突比例表示两个特征在数据集中同时取非零值的比例,冲突比例越高,说明特征的互斥程度越低;反之,冲突比例越低,说明特征的互斥程度越高。
例如,假设有以下数据集,共包含四条数据:
特征A | 特征B |
---|---|
0 | 1 |
1 | 0 |
0 | 0 |
1 | 1 |
只有第四条数据中特征A和特征B同时取非零值(即发生了冲突),而其他三条数据都是互斥的。因此,冲突比例为 1/4 = 0.25。这个比例可以用来量化特征A和特征B之间的互斥程度。而如果数据集如下,此时两个特征非同时为0的(非全零样本)数量为3,则冲突比例为1/3:
样本情况 | 特征A | 特征B |
---|---|---|
非全零样本 | 0 | 1 |
非全零样本 | 1 | 0 |
全零样本 | 0 | 0 |
非全零样本 | 1 | 1 |
LightGBM 提供了一个名为 max_conflict_rate 的超参数,用于控制特征捆绑的灵活性。该参数表示允许的最大冲突比例,即两个特征在数据集中同时取非零值的比例。如果两个特征的冲突比例小于设定的 max_conflict_rate,则认为它们是互斥的,可以进行捆绑;反之,则认为它们是冲突的,不能进行捆绑。例如,假设我们设置 max_conflict_rate=0.3,特征A和特征B的冲突比例为 1/4 = 0.25,小于 max_conflict_rate=0.3,因此它们被认为是互斥的,可以进行捆绑。然而,如果我们将 max_conflict_rate 设置为 0.2,由于冲突比例 0.25 超过了阈值,这两个特征将被视为冲突的,无法进行捆绑。
1.2.2.2 图着色
通过引入 最大冲突比例(max_conflict_rate) 的概念,LightGBM 放宽了对特征互斥性的严格限制,为是否互斥提供了一个可量化的阈值。这一机制使得特征捆绑更加灵活,能够适应更多实际场景的需求。然而,真正的特征捆绑过程并不仅仅是简单的阈值判断,而是将问题转化为一个经典的组合优化问题——图着色问题。
图着色问题是一种经典的组合优化问题,其核心目标是:给定一个无向图,用尽可能少的颜色对图中的每个顶点进行着色,并确保相邻的顶点颜色不同。这里的“颜色”可以是任意符号或编号,只要满足相邻顶点颜色不同的条件即可。在 LightGBM 的 互斥特征捆绑过程中,特征捆绑问题被巧妙地转化为图着色问题。
(1)计算冲突比例
首先,计算每对特征之间的冲突比例(即同时取非零值的比例)。如果冲突比例小于 max_conflict_rate,则认为这两个特征是互斥的;否则,认为它们是冲突的。
import numpy as np
import pandas as pd
def conflict_ratio_matrix(data):
"""
计算特征之间的冲突比例矩阵
:param data: 输入数据,可以是 pandas.DataFrame 或 numpy.ndarray,形状为 (n_samples, n_features)
:return: 冲突比例矩阵,形状为 (n_features, n_features)
"""
# 检查输入类型,确保为 numpy.ndarray
if isinstance(data, pd.DataFrame):
data = data.values
elif not isinstance(data, np.ndarray):
raise ValueError("输入数据必须是 pandas.DataFrame 或 numpy.ndarray 类型")
num_features = data.shape[1]
# 初始化冲突比例矩阵
conflict_matrix = np.zeros((num_features, num_features))
# 遍历所有特征对
for i in range(num_features):
for j in range(i + 1, num_features):
# 计算同时非零的样本数(冲突数)
conflict_count = np.sum(np.logical_and(data[:, i] != 0, data[:, j] != 0))
# 计算至少一个特征非零的样本数
total_count = np.sum(np.logical_or(data[:, i] != 0, data[:, j] != 0))
# 计算冲突比例
conflict_ratio = conflict_count / total_count if total_count != 0 else 0
# 填充冲突比例矩阵
conflict_matrix[i, j] = conflict_ratio
conflict_matrix[j, i] = conflict_ratio
return conflict_matrix
conflict_ratio_matrix(data[['x1_binned', 'x2_binned', 'x3', 'x4']])
(2)图的构建
将每个特征视为图中的一个顶点。如果两个特征之间存在冲突(即冲突比例大于 0),则用一条无向边连接这两个顶点,边的权重即为冲突比例。如果两个特征互斥(即冲突比例为 0),则它们之间没有边相连。
(3)图着色
在图着色过程中,目标是用尽可能少的颜色对图中的顶点进行着色,并确保相邻顶点颜色不同。由于相互冲突的特征之间有边相连,因此它们会被赋予不同的颜色。相同颜色的顶点则代表互斥的特征,因为它们之间没有边相连。
1.2.2.3 特征捆绑
完成图着色后,相同颜色的特征会被合并为一个捆绑特征。这些捆绑特征在后续的模型训练中被视为一个整体,从而显著减少特征数量,降低计算复杂度。
arr = np.array(data['x4'])
arr[arr != 0] += 1 #非零加1
data['x2_binned&x4'] = arr + data['x2_binned']
data
经过连续变量分箱和特征捆绑,实际上接下来带入进行模型训练的特征就只有x1_binned、x2_binned&x4和x3这三个特征。
1.3 基于梯度的单边采样(Gradient-based One-Side Sampling,GOSS)
1.3.1 GOSS计算基本原理
GOSS是一种基于梯度分布的智能抽样方法,与简单的随机抽样不同,它能够更高效地利用样本信息,从而在保证模型精度的同时显著提升计算效率。
1.3.1.1 GOSS 的核心思想
在梯度提升算法中,每个样本都有一个对应的梯度值,梯度绝对值的大小反映了样本对模型训练的贡献程度:
- 梯度绝对值较小的样本:这些样本通常已经被模型正确分类或预测结果接近真实值,因此在后续的参数更新过程中,它们对模型改进的贡献较小。如果每次迭代都重新计算这些样本的梯度,会造成一定的计算资源浪费。
- 梯度绝对值较大的样本:这些样本的预测误差较大,对模型训练有更大的贡献,因此在训练过程中需要重点关注。
基于这一观察,GOSS 提出了一种高效的采样策略:
1.样本排序:将所有样本按照梯度绝对值大小进行降序排序。
2.抽取大梯度样本:从排序后的样本中抽取梯度绝对值最大的前 a % a\% a%的样本,这里的 a % a\% a%可以换成更专业的超参数名称:top_rate。
3.随机抽取小梯度样本:从剩余的小梯度样本中随机抽取 b % b\% b% 的样本, b % b\% b%更专业的名称则是other_rate。
4.构建训练集:将抽取的大梯度样本和随机抽取的小梯度样本合并,构成新的训练集。
注意:尽管带入训练的数据是GOSS抽样后数据,但在后续决策树生长的过程中,小梯度样本的梯度(和损失函数二阶导数)会再乘以一个大于1的膨胀系数,再和大梯度样本的梯度(和损失函数二阶导数)进行相加,构成一个数据集的梯度(和损失函数二阶导数),来指导后续的迭代进行。让小梯度样本进一步膨胀再加入到样本数据梯度中,也是为了尽可能还原原始真实的数据集梯度。即GOSS抽样并不是想要改变数据集梯度,而是希望通过更小的计算量,来尽可能还原原始数据集完整梯度,以此来提升建模的精确度。
膨胀系数就是 1 − a b \frac{1-a}{b} b1−a,或者说是 1 − t o p _ r a t e o t h e r _ r a t e \frac{1-top\_rate}{other\_rate} other_rate1−top_rate。例如当top_rate=0.1,other_rate=0.2时,小样本梯度膨胀系数为: 1 − t o p _ r a t e o t h e r _ r a t e = 1 − 0.1 0.2 = 4.5 \frac{1-top\_rate}{other\_rate}=\frac{1-0.1}{0.2}=4.5 other_rate1−top_rate=0.21−0.1=4.5。最终样本梯度=大样本梯度+小样本梯度*4.5算得。
GOSS 的特点:
- 单边采样:GOSS 只对小梯度样本进行随机抽样,而保留全部大梯度样本,这也是“单边采样”名称的由来。
- 高效性:通过减少对小梯度样本的计算,GOSS 能够显著降低计算开销,同时保留对模型训练贡献较大的样本。
- 精度保障:由于大梯度样本被全部保留,且小梯度样本通过随机抽样部分保留,GOSS 能够在保证模型精度的前提下提升计算效率。
示例:假设有一个包含 1000 个样本的数据集:
排序:将所有样本按照梯度绝对值从大到小排序。
抽取大梯度样本:抽取梯度绝对值最大的前 10% 的样本(即 100 个样本)。
随机抽取小梯度样本:从剩余的 900 个小梯度样本中随机抽取 20% 的样本(即 180 个样本,对抽取的 180 个小梯度样本的梯度乘以膨胀系数)。
构建训练集:将 100 个大梯度样本和 180 4.5个小梯度样本合并,构成新的训练集(共 100+1804.5=910个样本)。
1.3.1.2 GOSS计算实例
- 损失函数、梯度与Hessian
由于GOSS采样计算过程会涉及样本梯度计算,我们知道样本梯度 是 损失函数在各参数方向上求导得到的结果,因此梯度实际上和损失函数相关。data数据集是二分类数据集,损失函数是二分类交叉熵损失函数:
L
=
−
1
n
∑
i
=
1
n
[
y
i
log
p
i
+
(
1
−
y
i
)
log
(
1
−
p
i
)
]
\mathcal{L}=-\frac{1}{n} \sum_{i=1}^{n}\left[y_{i} \log p_{i}+\left(1-y_{i}\right) \log \left(1-p_{i}\right)\right]
L=−n1i=1∑n[yilogpi+(1−yi)log(1−pi)]
第
i
i
i条样本的梯度就是损失函数对预测样本的一阶偏导数:
∂
L
∂
p
i
=
1
−
y
i
1
−
p
i
−
y
i
p
i
\frac{\partial \mathcal{L}}{\partial p_{i}}=\frac{1-y_{i}}{1-p_{i}}-\frac{y_{i}}{p_{i}}
∂pi∂L=1−pi1−yi−piyi
二阶偏导数,也就是所谓的Hessian矩阵(值),也被称作黑塞矩阵、海森矩阵等。对于第
i
i
i条样本,损失函数的二阶偏导数:
∂
2
L
∂
p
i
2
=
y
i
p
i
2
+
1
−
y
i
(
1
−
p
i
)
2
\frac{\partial^{2} \mathcal{L}}{\partial p_{i}^{2}}=\frac{y_{i}}{p_{i}^{2}}+\frac{1-y_{i}}{\left(1-p_{i}\right)^{2}}
∂pi2∂2L=pi2yi+(1−pi)21−yi
def binary_cross_entropy(y_true, y_pred):
"""
计算二分类交叉熵损失函数
:param y_true: 真实标签,一个 Numpy 数组,形状为 (n_samples,)
:param y_pred: 预测标签,一个 Numpy 数组,形状为 (n_samples,)
:return:二分类交叉熵损失值,一个标量
"""
# 确保输入的标签为 0 或 1
y_true = np.clip(y_true, 1e-7, 1-1e-7) #np.clip函数是用于将输出结果限制在一个范围内,避免出现零值相除的情况
y_pred = np.clip(y_pred, 1e-7, 1-1e-7)
# 计算二分类交叉熵损失
loss = -(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)).mean()
return loss
def binary_cross_entropy_grad(y_true, y_pred):
"""
计算二分类交叉熵损失函数的一阶导数
:param y_true: 真实标签,一个 Numpy 数组,形状为 (n_samples,)
:param y_pred: 预测标签,一个 Numpy 数组,形状为 (n_samples,)
:return:二分类交叉熵损失函数的一阶导数,一个 Numpy 数组,形状为 (n_samples,)
"""
# 确保输入的标签为 0 或 1
y_true = np.clip(y_true, 1e-7, 1-1e-7)
y_pred = np.clip(y_pred, 1e-7, 1-1e-7)
# 计算二分类交叉熵损失函数的一阶导数
grad = (1 - y_true) / (1 - y_pred) - y_true / y_pred
return grad
def binary_cross_entropy_hess(y_true, y_pred):
"""
计算二分类交叉熵损失函数的二阶导数
:param y_true: 真实标签,一个 Numpy 数组,形状为 (n_samples,)
:param y_pred: 预测标签,一个 Numpy 数组,形状为 (n_samples,)
:return: 生成的特征张和标签张量:二分类交叉熵损失函数的二阶导数,一个 Numpy 数组,形状为 (n_samples,)
"""
# 确保输入的标签为 0 或 1
y_true = np.clip(y_true, 1e-7, 1 - 1e-7)
y_pred = np.clip(y_pred, 1e-7, 1 - 1e-7)
# 计算二分类交叉熵损失函数的二阶导数
hess = y_true / (y_pred ** 2) + (1 - y_true) / ((1 - y_pred) ** 2)
return hess
- LightGBM初始预测值
首次进行GOSS抽样时梯度计算依据是LightGBM算法的初始预测值,如果是交叉熵损失函数,则初始预测值为1类样本占比(或者1类样本总数)的对数几率(log odds)计算结果,并且每条样本初始预测值都相同,计算公式如下:
y
^
=
l
n
p
1
−
p
\hat y = ln\frac{p}{1-p}
y^=ln1−pp
其中
p
p
p为1类的占比(或者1类总数,计算结果都相同)。例如对于data数据集来说,初始预测值可以按照如下方式进行计算:
# 计算每个类别的频率
class_freq = data['y'].value_counts(normalize=True)
# 计算初始预测值
initial_prediction = np.log(class_freq[1] / class_freq[0])
print("初始预测值:", initial_prediction) #初始预测值: 0.4054651081081642
因此所有样本的初始预测结果为0.4,将其添加到data的最后一列中:
y_pred = np.array([0.4054]*10)
data['y_pred'] = y_pred
接下来我们即可依据y_pred来计算每条样本的梯度。
data['grad'] = binary_cross_entropy_grad(data['y'], data['y_pred'])
data
然后再计算样本的hes值:
data['hess'] = binary_cross_entropy_hess(data['y'], data['y_pred'])
data
- GOSS抽样
假设top_rate=0.2,other_rate=0.5,由于我们样本量较少,因此这里设置的比例偏大。此时GOSS抽样过程如下,首先计算样本梯度绝对值:
abs_gradients = np.abs(data['grad'])
abs_gradients
然后进行从大到小的索引排序:
sorted_index = np.argsort(-abs_gradients)
sorted_index
此时top_rate=0.2,other_rate=0.5,即从10条数据中挑选出梯度最大的2条数据,然后从剩下的8条数据中抽取50%,即抽取4条数据,共同构成本次GOSS抽样得到的训练数据集。借助布尔索引先挑选出梯度最大的2条数据:
topn_index = sorted_index[:2]
topn_data = data.iloc[topn_index.values, :]
topn_data
然后再从剩下的数据集中随机抽取4条数据,首先是找到剩余数据集索引,进行无放回抽样:
raten_index = np.random.choice(sorted_index[2:].values, size=4, replace=False)
raten_data = data.iloc[raten_index, :]
raten_data
至此,我们就完成了一次GOSS抽样全过程,接下来我们就将带入我们抽样得到的topn_data和raten_data带入进行模型训练。
二、LightGBM的Sklearn实现
#!pip install lightgbm -i https://pypi.tuna.tsinghua.edu.cn/simple #cpu版本安装
# 科学计算模块
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler,PolynomialFeatures
from sklearn.model_selection import train_test_split,GridSearchCV
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
from lightgbm import LGBMClassifier
# 读取数据
iris = load_iris()
data = pd.DataFrame(data= np.c_[iris['data'], iris['target']], columns= iris['feature_names'] + ['target'])
# 划分数据集
X_train, X_test, y_train, y_test = train_test_split(data.drop("target", axis=1), data["target"], test_size=0.2)
#模型实例化
gbm = LGBMClassifier()
# 训练模型
gbm.fit(X_train, y_train)
gbm.predict(X_test)
#gbm.predict_proba(X_test)
#np.argmax(gbm.predict_proba(X_test), 1) #将概率预测结果转化为类别
accuracy_score(y_train, gbm.predict(X_train)), accuracy_score(y_test, gbm.predict(X_test)) #输出(1.0, 0.96)
2.1 LGBMClassifier超参数概览
LGBMClassifier的超参数分为决策树剪枝超参数、Boosting过程控制超参数、特征和数据处理类超参数和其他超参数四类。其中决策树剪枝超参数包括max_depth(树的最大深度)、num_leaves(叶子节点数)、min_child_samples(子节点的最小样本数)等;Boosting过程控制超参数和XGB的booster类似,包含boosting_type(提升类型,如gbdt、dart、goss、rf),n_estimators(迭代次数)、learning_rate(学习率)和reg_alpha(L1正则化系数)、reg_lambda(L2正则化系数);而特征和数据处理类超参数则主要与数据采样有关,包含subsample(样本子集的比例)、subsample_freq(进行子采样的频率)、colsample_bytree(列采样的比例)等;而其他参数则是一些对建模效果并没有重要影响的超参数,例如random_state(随机数种子)、n_jobs(并行计算所用的CPU核心数)、class_weight(类别权重)等。
2.1.1 决策树剪枝超参数
Name | Description |
---|---|
num_leaves | 一棵树上的叶子节点数,默认为 31 |
max_depth | 树的最大深度,默认值为 -1,表示无限制 |
min_split_gain | 相当于min_impurity_decrease,再分裂所需最小增益。默认值为 0,表示无限制 |
min_child_weight | 子节点的最小权重和。默认值为 1e-3。较大的 min_child_weight 可以防止过拟合 |
min_child_samples | 相当于min_samples_leaf,单个叶子节点上的最小样本数量。默认值为 20。较大的 min_child_samples 可以防止过拟合 |
2.1.2 Boosting过程控制超参数
boosting_type 是 LightGBM 中用于指定梯度提升算法类型的参数,默认值为 ‘gbdt’。以下是各选项的详细说明及其适用场景:
- gbdt(Gradient Boosting Decision Tree)
描述:gbdt 是传统的梯度提升决策树算法,也是 LightGBM 的默认选项。它通过迭代地构建决策树来最小化损失函数,每次迭代都会基于前一次迭代的残差构建新的树。
特点:
性能稳定,适用于大多数场景。
计算效率较高,精度通常较好。
适用场景:
通用场景,尤其是对模型精度要求较高的任务。
中小规模数据集或计算资源充足的情况。 - dart(Dropouts meet Multiple Additive Regression Trees)
描述:dart 是一种结合了 Dropout 和多重加性回归树的算法。它在每次迭代中随机丢弃一部分已经构建的树(类似于神经网络中的 Dropout 技术),从而增加模型的随机性。
特点:
通过随机丢弃树,减少过拟合风险。
训练过程较慢,因为需要重新构建被丢弃的树。
适用场景:
数据集中存在较多噪声,需要减少过拟合风险。
数据集相对简单,模型容易过拟合的情况。 - goss(Gradient-based One-Side Sampling)
描述:goss 是一种基于梯度的单边采样方法。它在每次迭代中优先选择梯度绝对值较大的样本进行训练,同时对梯度较小的样本进行随机采样,从而减少计算量。
特点:
显著提升训练速度,尤其适用于大规模数据集。
在保持较高精度的同时,减少计算复杂度。
有时精度可能略低于 gbdt,但计算效率更高。
适用场景:
大规模数据集,计算资源有限的情况。
对训练速度要求较高的任务。 - rf(Random Forest)
描述:rf 使用随机森林算法替代了传统的 Boosting 过程。它通过 Bagging 的方式构建多棵独立的决策树,并对结果进行平均或投票。
特点:
不再是 Boosting,而是 Bagging 方法。
每棵树独立构建,增加了模型的多样性。
训练速度较快,但精度通常不如 Boosting 方法。
适用场景:
需要快速训练模型的场景。
对模型精度要求不高,但需要稳定性和抗过拟合能力的任务。
参数选择建议
默认选择:如果没有特殊需求,建议使用默认的 gbdt,它在大多数场景下表现稳定且精度较高。
大规模数据:如果数据集较大且对训练速度有较高要求,可以选择 goss。
抗过拟合:如果数据集噪声较多或模型容易过拟合,可以尝试 dart。
快速训练:如果需要快速训练模型且对精度要求不高,可以选择 rf。
subsample_for_bin 是 LightGBM 中用于控制连续变量分箱时抽样样本数量的参数。它的默认值为 200000,表示在对连续变量进行分箱(直方图优化过程)时,最多会从训练数据中抽取 200,000 个样本用于计算分箱点。如果实际数据量小于 subsample_for_bin 的值,则会使用全部样本进行计算。
与 boosting_type 的关系:当 boosting_type 设置为 ‘goss’ 时,subsample_for_bin 参数会失效。这是因为 goss 在直方图优化过程中会自动完成抽样,其抽样策略如下:
保留所有小梯度样本:这些样本已经被模型较好地拟合,保留它们可以有效防止过拟合。
对大梯度样本进行采样:这些样本对模型训练贡献较大,采样后可以加速训练过程。
因此,goss 的抽样策略已经兼顾了训练效率和防止过拟合的需求,subsample_for_bin 的设置不再影响最终结果。
如果需要控制goss过程,则需要借助top_rate 和 other_rate 这两个参数,但是这两个参数只存在于LGBM原生API中,在sklearn中并没有,因此在使用 LightGBM 的 sklearn API 时,GOSS 采样方法会自动进行调整。
其他参数和GBDT及XGBoost类似,具体解释如下:
Name | Description |
---|---|
learning_rate | 学习率,即每次迭代中梯度提升的步长,默认值为 0.1 |
n_estimators | 迭代次数,即生成的基学习器的数量,默认值为 100 |
reg_alpha | L1 正则化系数,默认值为 0 |
reg_lambda | L2 正则化系数。默认值为 0 |
2.1.2.1 借助验证曲线进行验证
- 评估器个数
import numpy as np
import matplotlib.pyplot as plt
from sklearn.model_selection import validation_curve
from lightgbm import LGBMClassifier
from sklearn.datasets import make_classification
def plot_validation_curve(model, param_name, param_range, X, y, cv=5, scoring="accuracy", n_jobs=-1):
"""
绘制验证曲线
:param model: 机器学习模型
:param param_name: 参数名称
:param param_range: 参数范围
:param X: 特征数据
:param y: 标签数据
:param cv: 交叉验证折数
:param scoring: 评估指标
:param n_jobs: 并行计算数
"""
# 计算验证曲线
train_scores, val_scores = validation_curve(
model, X, y, param_name=param_name, param_range=param_range,
cv=cv, scoring=scoring, n_jobs=n_jobs
)
# 计算训练和验证分数的均值与标准差
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
val_mean = np.mean(val_scores, axis=1)
val_std = np.std(val_scores, axis=1)
# 绘制验证曲线
plt.figure(figsize=(10, 6))
plt.title(f"Validation Curve with {model.__class__.__name__} ({param_name})", fontsize=14)
plt.xlabel(param_name, fontsize=12)
plt.ylabel("Score", fontsize=12)
plt.ylim(0.8, 1.02) # 设置 y 轴范围
plt.grid(True, linestyle='--', alpha=0.6) # 添加网格线
# 绘制训练分数曲线
plt.plot(param_range, train_mean, label="Training Score", color="darkorange", lw=2)
plt.fill_between(param_range, train_mean - train_std, train_mean + train_std, alpha=0.2, color="darkorange")
# 绘制验证分数曲线
plt.plot(param_range, val_mean, label="Cross-Validation Score", color="navy", lw=2)
plt.fill_between(param_range, val_mean - val_std, val_mean + val_std, alpha=0.2, color="navy")
# 添加图例
plt.legend(loc="best", fontsize=12)
# 显示图形
plt.show()
# 生成分类数据集
X, y = make_classification(n_samples=1000, n_features=20, n_informative=2, n_redundant=10, random_state=42)
# 初始化 LightGBM 分类器
model = LGBMClassifier(random_state=22)
# 定义参数范围:树的数量 (n_estimators)
param_range = np.arange(1, 100, 2)
# 调用函数绘制验证曲线
plot_validation_curve(model, "n_estimators", param_range, X, y)
- 学习率
# 创建一个LightGBM分类器
model = LGBMClassifier()
# 要计算验证曲线的参数范围
param_range = np.linspace(0.01, 0.1, 100)
# 调用函数
plot_validation_curve(model, "learning_rate", param_range, X, y)
- max_depth最大树深度
# 创建一个LightGBM分类器
model = LGBMClassifier()
# 要计算验证曲线的参数范围
param_range = np.arange(1, 20, 1)
# 调用函数
plot_validation_curve(model, "max_depth", param_range, X, y)
- reg_alpha:L1正则化项
# 创建一个LightGBM分类器
model = LGBMClassifier(n_estimators=45)
# 要计算验证曲线的参数范围
param_range = np.linspace(0.00, 0.5, 100)
# 调用函数
plot_validation_curve(model, "reg_alpha", param_range, X, y)
- reg_lambda:L2正则化项
# 创建一个LightGBM分类器
model = LGBMClassifier(n_estimators=45)
# 要计算验证曲线的参数范围
param_range = np.linspace(0.00, 0.5, 100)
# 调用函数
plot_validation_curve(model, "reg_lambda", param_range, X, y)
2.1.3 特征和数据处理类超参数
Name | Description |
---|---|
subsample | 模型训练时抽取的样本数量,取值范围为 (0, 1],表示抽样比例,默认为1.0 |
subsample_freq | 抽样频率,表示每隔几轮进行一次抽样,默认取值为0,表示不进行随机抽样 |
colsample_bytree | 在每次迭代(树的构建)时,随机选择特征的比例,取值范围为 (0, 1],默认为1.0 |
-
subsample和subsample_for_bin之间的关系
这两个参数尽管从字面理解都是抽样比例,但实际上这两个参数是完全独立的,彼此之间并不影响。其中subsample_for_bin抽样结果用于直方图构建,而subsample抽样结果则是用于模型训练,这两个抽样过程彼此独立,互不影响; -
subsample和subsample_freq之间的关系
更加关键的是subsample_freq参数,如果subsample_freq=0,则无论subsample取值为多少,模型训练时都不会进行随机抽样;换而言之,只有subsample_freq不等于0,且subsample不等于1.0时,才会进行抽样; -
subsample_freq和colsample_bytree之间的关系
不同于subsample是样本抽样,colsample_bytree是每次迭代(每次构建一颗树时)进行的特征抽样,并且colsample_bytree不受subsample_freq影响,即无论subsample_freq取值为多少,每次建树时都会根据colsample_bytree设置的比例进行列抽样。
LightGBM和RF的不同特征抽样规则:随机森林是每棵树的每次分裂时都随机分配特征,而LightGBM是每次构建一颗树时随机分配一个特征子集,这颗树在成长过程中每次分裂都是依据这个特征子集进行生长。
2.1.4 其他超参数
Name | Description |
---|---|
objective | 指定目标函数,默认为None,会自动判断是二分类还是多分类问题,这里我们也可以手动设置 ‘binary’(用于二分类问题)或’multiclass’(用于多分类问题) |
class_weight | 样本权重设置参数 |
importance_type | 特征重要性计算方式,默认为 ‘split’,表示使用特征在模型中被选中作为分裂特征的次数,可选 ‘gain’,表示使用特征在模型中的分裂收益之和作为重要性评估指标 |
random_state | 随机数种子 |
n_jobs | 并行的线程数,默认为-1,调用全部可用线程 |
silent | 是否沉默(不输出日志),默认为’warn’,仅显示警告和报错,可选’info’,用于打印全部信息 |
关于目标函数的设置,对于LGBM的sklearn API来说,objective参数较为简单,只有’binary’和’multiclass’两种,其中’binary’表示逻辑回归损失,也就是二分类交叉熵损失,而’multiclass’则代表多分类交叉熵损失。
2.2 LGBMRegressor超参数概览
LGBMRegressor和LGBMClassifier只有两点不同,其一是LGBMRegressor没有class_weight参数,其二则是LGBMRegressor的损失函数和LGBMClassifier完全不同。接下来我们就LGBMRegressor的损失函数选取进行解释。
均方误差(MSE, Mean Squared Error):最常用的的损失函数,此时objective='regression' 或 objective='regression_l2';
平均绝对误差(MAE, Mean Absolute Error):通常用于标签存在异常值情况,此时objective='regression_l1';
Huber损失(Huber Loss):适用于目标值存在大量异常值或者噪声时。Huber损失在预测误差较小时表现为均方误差,在预测误差较大时表现为平均绝对误差,这使得它对异常值具有更好的鲁棒性。此时objective='quantile';
Quantile损失(Quantile Loss):用于分位数回归,最小化预测值与真实值之间的分位数损失,适用于需要对预测分布进行精细控制的场景(例如围绕某种分布进行预测)。此时objective='quantile';
Poisson损失(Poisson Loss): 适用于计数问题,即目标值是非负整数且服从泊松分布。此时objective='poisson'
Gamma损失(Gamma Loss):适用于预测非负实数且服从伽马分布的目标值。此时objective='gamma'
Tweedie损失(Tweedie Loss):适用于广义线性模型(Generalized Linear Models, GLMs)中的 Tweedie 分布(非对称分布)的数据集。此时objective='tweedie'。
三、LightGBM超参数优化
3.1 基于贝叶斯优化器的超参数优化
贝叶斯优化是当前超参数优化领域的 SOTA(State of the Art) 方法,被广泛认为是最先进的优化框架。它不仅适用于超参数优化,还可以应用于神经网络架构搜索、元学习 等 AutoML 的核心领域。现代几乎所有在效率和效果上取得显著成果的超参数优化方法,都是基于贝叶斯优化的基本理念发展而来的。
3.1.1 核心思想
贝叶斯优化通过构建一个 代理模型(Surrogate Model) 来近似目标函数(如模型性能),并利用 采集函数(Acquisition Function) 指导下一步的搜索方向。其核心步骤包括:
代理模型:通常使用高斯过程(Gaussian Process)来建模目标函数,预测未知点的性能。
采集函数:基于代理模型的预测结果,选择下一个最有潜力的超参数组合进行评估。
迭代优化:通过不断迭代,逐步逼近最优解。
3.1.2 优势
1.高效性:
通过代理模型和采集函数,贝叶斯优化能够在较少的评估次数内找到接近最优的超参数组合。特别适合目标函数计算成本高(如深度学习模型训练)的场景。
2.自适应性:
贝叶斯优化能够根据历史评估结果动态调整搜索策略,适应不同的目标函数。
3.广泛适用性:
不仅适用于超参数优化,还可用于神经网络架构搜索、元学习等复杂任务。
4.理论基础强:
基于贝叶斯统计理论,具有坚实的数学基础。
数据集 通过网盘分享的文件:WA_Fn-UseC_-Telco-Customer-Churn.csv
链接: https://pan.baidu.com/s/18Osmhz-dQ9ILsMAEX85prQ 提取码: xypm
3.1.3 超参数优化案例
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, roc_auc_score
from sklearn.model_selection import train_test_split, GridSearchCV,cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.base import BaseEstimator, TransformerMixin
from tqdm import tqdm
from lightgbm import LGBMClassifier
import warnings
warnings.filterwarnings('ignore') # 忽略警告
# 读取数据
tcc = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
# 标注连续/离散字段
# 离散字段
category_cols = ['gender', 'SeniorCitizen', 'Partner', 'Dependents',
'PhoneService', 'MultipleLines', 'InternetService', 'OnlineSecurity', 'OnlineBackup',
'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies', 'Contract', 'PaperlessBilling',
'PaymentMethod']
# 连续字段
numeric_cols = ['tenure', 'MonthlyCharges', 'TotalCharges']
# 标签
target = 'Churn'
# ID列
ID_col = 'customerID'
# 验证是否划分能完全
assert len(category_cols) + len(numeric_cols) + 2 == tcc.shape[1]
# 连续字段转化
tcc['TotalCharges'] = tcc['TotalCharges'].apply(lambda x: x if x != ' ' else np.nan).astype(float)
tcc['MonthlyCharges'] = tcc['MonthlyCharges'].astype(float)
# 缺失值填补
tcc['TotalCharges'] = tcc['TotalCharges'].fillna(0)
# 标签值手动转化
tcc['Churn'].replace(to_replace='Yes', value=1, inplace=True)
tcc['Churn'].replace(to_replace='No', value=0, inplace=True)
# 划分训练集和测试集
train, test = train_test_split(tcc, random_state=22)
X_train = train.drop(columns=[ID_col, target]).copy()
X_test = test.drop(columns=[ID_col, target]).copy()
y_train = train['Churn'].copy()
y_test = test['Churn'].copy()
# 特征工程:时间衍生
def derive_time_features(df, tenure_col='tenure'):
"""
从tenure列衍生时间特征
:param df: 输入数据框
:param tenure_col: tenure列名
:return: 衍生后的数据框
"""
df_seq = pd.DataFrame()
df_seq['tenure_year'] = ((72 - df[tenure_col]) // 12) + 2014
df_seq['tenure_month'] = (72 - df[tenure_col]) % 12 + 1
df_seq['tenure_quarter'] = ((df_seq['tenure_month'] - 1) // 3) + 1
return df_seq
# 生成时间衍生特征
X_train_seq = derive_time_features(X_train)
X_test_seq = derive_time_features(X_test)
# 独热编码
def one_hot_encode(df, cols, drop=None):
"""
对指定列进行独热编码
:param df: 输入数据框
:param cols: 需要编码的列
:param drop: 是否删除某一类别(避免多重共线性)
:return: 编码后的数据框
"""
enc = OneHotEncoder(drop=drop)
enc.fit(df[cols])
encoded_df = pd.DataFrame(enc.transform(df[cols]).toarray(),
columns=enc.get_feature_names_out(cols))
encoded_df.index = df.index
return encoded_df
# 对时间衍生特征进行独热编码
X_train_seq = one_hot_encode(X_train_seq, X_train_seq.columns)
X_test_seq = one_hot_encode(X_test_seq, X_test_seq.columns)
# 对离散特征进行Ordinal编码
def ordinal_encode(df, cols):
"""
对指定列进行Ordinal编码
:param df: 输入数据框
:param cols: 需要编码的列
:return: 编码后的数据框
"""
enc = OrdinalEncoder()
enc.fit(df[cols])
encoded_df = pd.DataFrame(enc.transform(df[cols]), columns=cols)
encoded_df.index = df.index
return encoded_df
# 对离散特征进行Ordinal编码
X_train_OE = ordinal_encode(X_train, category_cols)
X_test_OE = ordinal_encode(X_test, category_cols)
# 合并编码后的离散特征和连续特征
X_train_OE = pd.concat([X_train_OE, X_train[numeric_cols]], axis=1)
X_test_OE = pd.concat([X_test_OE, X_test[numeric_cols]], axis=1)
# 重置索引
X_train_OE = X_train_OE.reset_index(drop=True)
y_train = y_train.reset_index(drop=True)
X_test_OE = X_test_OE.reset_index(drop=True)
y_test = y_test.reset_index(drop=True)
gbm = LGBMClassifier() #实例化
# 训练模型
gbm.fit(X_train_OE, y_train)
gbm.predict(X_test_OE)
gbm.score(X_train_OE, y_train),gbm.score(X_test_OE, y_test) #不进行超参数优化时模型输出测试集:0.8906, 验证集:0.7871
- 优化参数
Name | Description | 经验最优范围 |
---|---|---|
num_leaves | 一棵树上的叶子节点数,默认为 31 | range(20, 51, 2) |
max_depth | 树的最大深度,默认值为 -1,表示无限制 | range(5, 15, 2) |
min_split_gain | 相当于min_impurity_decrease,再分裂所需最小增益。默认值为 0,表示无限制 | - |
min_child_weight | 子节点的最小权重和。默认值为 1e-3。较大的 min_child_weight 可以防止过拟合 | - |
min_child_samples | 相当于min_samples_leaf,单个叶子节点上的最小样本数量。默认值为 20。较大的 min_child_samples 可以防止过拟合 | - |
boosting_type | 使用的梯度提升算法类型,默认为GBDT | [‘gbdt’, ‘goss’] |
subsample_for_bin | 该参数表示对连续变量进行分箱时(直方图优化过程)抽取样本的个数,默认取值为200000 | - |
learning_rate | 学习率,即每次迭代中梯度提升的步长,默认值为 0.1 | np.linspace(0.01, 0.2, 5) |
n_estimators | 迭代次数,即生成的基学习器的数量,默认值为 100 | range(10, 200, 50) |
reg_alpha | L1 正则化系数,默认值为 0 | np.linspace(0.01, 0.1, 2) |
reg_lambda | L2 正则化系数。默认值为 0 | np.linspace(0.01, 0.1, 2) |
subsample | 模型训练时抽取的样本数量,取值范围为 (0, 1],表示抽样比例,默认为1.0 | - |
subsample_freq | 抽样频率,表示每隔几轮进行一次抽样,默认取值为0,表示不进行随机抽样 | - |
colsample_bytree | 在每次迭代(树的构建)时,随机选择特征的比例,取值范围为 (0, 1],默认为1.0 | [0.6, 0.8, 1.0] |
3.1.4 优化案例
3.1.4.1 1.HyperOPT搜索基本流程
- 1、定义超参数搜索空间
在hyperopt中,我们使用特殊的字典形式来定义参数空间,其中键值对上的键可以任意设置,只要与目标函数中索引参数的键一致即可,键值对的值则是hyperopt独有的hp函数,包括了:
hp.quniform(“参数名称”, 下界, 上界, 步长) - 适用于均匀分布的浮点数
hp.uniform(“参数名称”,下界, 上界) - 适用于随机分布的浮点数
hp.randint(“参数名称”,上界) - 适用于[0,上界)的整数,区间为前闭后开
hp.choice(“参数名称”,[“字符串1”,“字符串2”,…]) - 适用于字符串类型,最优参数由索引表示
hp.choice(“参数名称”,[*range(下界,上界,步长)]) - 适用于整数型,最优参数由索引表示
hp.choice(“参数名称”,[整数1,整数2,整数3,…]) - 适用于整数型,最优参数由索引表示
hp.choice(“参数名称”,[“字符串1”,整数1,…]) - 适用于字符与整数混合,最优参数由索引表示
- 2、定义目标函数
- 目标函数的输入必须是符合hyperopt规定的字典,不能是类似于sklearn的参数空间字典、不能是参数本身,更不能是数据、算法等超参数以外的元素。因此在自定义目标函数时,我们需要让超参数空间字典作为目标函数的输入。
- Hyperopt只支持寻找 f ( x ) f(x) f(x)的最小值,不支持寻找最大值,因此当我们定义的目标函数是某种正面的评估指标时(如准确率auc),我们需要对该评估指标取负。如果我们定义的目标函数是负损失,也需要对负损失取绝对值。当且仅当我们定义的目标函数是普通损失时,我们才不需要改变输出。
import numpy as np
from hyperopt import hp, fmin, tpe, Trials
from hyperopt.early_stop import no_progress_loss
from lightgbm import LGBMClassifier
from sklearn.model_selection import cross_val_score
# 1、定义超参数空间
LGBM_params_space = {
'max_depth': hp.choice('max_depth', np.arange(3, 30).tolist()),
'num_leaves': hp.choice('num_leaves', np.arange(10, 100).tolist()),
'n_estimators': hp.choice('n_estimators', np.arange(50, 200).tolist()),
'boosting_type': hp.choice('boosting_type', ['gbdt', 'goss']),
'colsample_bytree': hp.uniform('colsample_bytree', 0.2, 1.0),
'learning_rate': hp.uniform('learning_rate', 0.01, 0.1),
'reg_alpha': hp.uniform('reg_alpha', 0.01, 0.5),
'reg_lambda': hp.uniform('reg_lambda', 0.01, 0.5)
}
# 2、定义目标函数
def hyperopt_lgbm(params):
lgbm = LGBMClassifier(random_state=22, **params, verbose=-1) # 禁用LightGBM输出
res = -cross_val_score(lgbm, X_train_OE, y_train, scoring='roc_auc', cv=5).mean()
return res
# 3、调用贝叶斯优化
def param_hyperopt_lgbm(max_evals):
trials = Trials()
early_stop_fn = no_progress_loss(10)
params_best = fmin(
fn=hyperopt_lgbm,
space=LGBM_params_space,
algo=tpe.suggest,
max_evals=max_evals,
trials=trials,
early_stop_fn=early_stop_fn,
verbose=0 # 禁用Hyperopt输出
)
return params_best
# 执行超参数优化
lgbm_params_best = param_hyperopt_lgbm(100)
# 打印最佳结果
print("Best Parameters:", lgbm_params_best)
对于离散变量,搜索结果实际上是索引值,而非真实值。
- 带入原模型进行测试:
lgbm = LGBMClassifier(random_state = 22,
max_depth = np.arange(3, 30).tolist()[11],
num_leaves = np.arange(10, 100).tolist()[43],
n_estimators = np.arange(50, 200).tolist()[99],
boosting_type = ['gbdt', 'goss'][1],
colsample_bytree = 0.4296491494550363,
learning_rate = 0.013732013321709684,
reg_alpha = 0.42344265953077664,
reg_lambda = 0.43195063354217994).fit(X_train_OE, y_train)
lgbm.score(X_train_OE, y_train), lgbm.score(X_test_OE, y_test) #输出:测试集:0.8459, 验证集:0.7831
准确率 | 训练集 | 测试集 |
---|---|---|
LGBM原始模型 | 0.8906 | 0.7871 |
LGBM+TPE+100 | 0.8459 | 0.7831 |
- 修改目标函数
在定义目标函数的时候,可以考虑区分目标函数的训练状态和测试状态,训练状态(train=True)是默认状态,作为超参数搜索时的目标函数时使用,而train=Fasle时则为测试状态,此时函数用于带入搜索出来的超参数,来直接输出最终的最优模型:
def hyperopt_lgbm(params, train=True):
# 读取参数
if train == True:
max_depth = params['max_depth']
num_leaves = params['num_leaves']
n_estimators = params['n_estimators']
boosting_type = params['boosting_type']
colsample_bytree = params['colsample_bytree']
learning_rate = params['learning_rate']
reg_alpha = params['reg_alpha']
reg_lambda = params['reg_lambda']
else:
max_depth = params['max_depth'] + 3
num_leaves = params['num_leaves'] + 10
n_estimators = params['n_estimators'] + 50
boosting_type = ['gbdt', 'goss'][params['boosting_type']]
colsample_bytree = params['colsample_bytree']
learning_rate = params['learning_rate']
reg_alpha = params['reg_alpha']
reg_lambda = params['reg_lambda']
# 实例化模型
lgbm = LGBMClassifier(random_state = 22,
max_depth = max_depth,
num_leaves = num_leaves,
n_estimators = n_estimators,
boosting_type = boosting_type,
colsample_bytree = colsample_bytree,
learning_rate = learning_rate,
reg_alpha = reg_alpha,
reg_lambda = reg_lambda)
if train == True:
res = -cross_val_score(lgbm, X_train_OE, y_train).mean()
else:
res = lgbm.fit(X_train_OE, y_train)
return res
lgbm_params_best = param_hyperopt_lgbm(200)
hyperopt_lgbm(lgbm_params_best, train=False)
clf = hyperopt_lgbm(lgbm_params_best, train=False)
clf.score(X_test_OE, y_test)
准确率 | 训练集 | 测试集 |
---|---|---|
LGBM原始模型 | 0.8906 | 0.7871 |
LGBM+TPE+100 | 0.8459 | 0.7831 |
LGBM+TPE+200 | 0.8406 | 0.7882 |
- 测试更多次数迭代
lgbm_params_best = param_hyperopt_lgbm(1000)
clf = hyperopt_lgbm(lgbm_params_best, train=False)
clf.score(X_train_OE, y_train), clf.score(X_test_OE, y_test)
准确率 | 训练集 | 测试集 |
---|---|---|
LGBM原始模型 | 0.8906 | 0.7871 |
LGBM+TPE+100 | 0.8459 | 0.7831 |
LGBM+TPE+200 | 0.8406 | 0.7882 |
LGBM+TPE+1000 | 0.8531 | 0.7797 |
3.2 基于网格搜索的超参数优化
在 sklearn 中,超参数优化器主要有四种:GridSearchCV(网格搜索)、RandomizedSearchCV(随机网格搜索)、HalvingSearchCV(对半网格搜索) 和 HalvingRandomizedSearchCV(对半随机网格搜索)。这些方法各有特点,适用于不同的场景。
方法 | 搜索策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
GridSearchCV | 枚举所有参数组合 | 精度最高 | 计算量大 | 参数空间小,计算资源充足 |
RandomizedSearchCV | 随机抽取参数子空间 | 计算量较小,灵活性高 | 精度较低 | 参数空间大,计算效率要求高 |
HalvingSearchCV | 多轮筛选,淘汰一半参数组合 | 计算效率高,容错性强 | 精度有限 | 数据量大,计算效率要求高 |
HalvingRandomizedSearchCV | 随机子空间 + 多轮筛选 | 计算效率最高 | 精度最低 | 数据量极大,计算效率要求极高 |
3.2.1 网格搜索注意事项
- 小步前进,快速调整
每次需要设置一个相对较小的参数搜索空间,然后快速执行一次超参数搜索,并根据超参数搜索结果来调整参数空间,并进行更进一步的超参数搜索,如此往复,直到参数空间内包含了全部参数的最优解为止。
- 分批训练策略
一般来说如果超参数个数较多,则可以分两批、甚至是分三批进行搜索,例如有A、B、C、D、E五个超参数时,可以先搜索A、B、C,在搜索出一组最优值后,再以此为中心创建搜索空间并加入新的D、E两个参数,设置各自对应的搜索空间,并进行第二批搜索。
params | 经验最优范围 |
---|---|
num_leaves | range(20, 51, 2) |
max_depth | range(5, 15, 2)) |
learning_rate | np.linspace(0.01, 0.2, 5) |
n_estimators | range(10, 160, 70) |
boosting_type | [‘gbdt’, ‘goss’] |
colsample_bytree | [0.6, 0.8, 1.0] |
(第一阶段)reg_alpha | np.linspace(0.01, 0.1, 2) |
(第二阶段)reg_lambda | np.linspace(0.01, 0.1, 2) |
3.2.2 优化案例
import time
import numpy as np
from sklearn.model_selection import GridSearchCV
from lightgbm import LGBMClassifier
start = time.time()
# 设置超参数空间
parameter_space = {
"num_leaves": range(20, 51, 2),
"max_depth": range(5, 15, 2),
"learning_rate": list(np.linspace(0.01, 0.2, 5)),
"n_estimators": range(10, 160, 70),
"boosting_type": ['gbdt', 'goss'],
"colsample_bytree": [0.6, 0.8, 1.0]
}
# 实例化模型与评估器
LGBM_0 = LGBMClassifier(random_state=22, verbose=-1) # 禁用输出
# 设置网格搜索
grid_LGBM_0 = GridSearchCV(LGBM_0, parameter_space, n_jobs=-1, cv=5, scoring='roc_auc')
# 模型训练
grid_LGBM_0.fit(X_train_OE, y_train)
print("Best Parameters:", grid_LGBM_0.best_params_)
print("Execution Time:", time.time() - start)
grid_LGBM_0.best_score_ #0.84869
grid_LGBM_0.score(X_train_OE, y_train), grid_LGBM_0.score(X_test_OE, y_test) # 输出测试集:0.8768, 验证集:0.8371
grid_LGBM_0.best_params_
准确率 | 训练集 | 测试集 |
---|---|---|
LGBM原始模型 | 0.8906 | 0.7871 |
LGBM+TPE+100 | 0.8459 | 0.7831 |
LGBM+TPE+200 | 0.8406 | 0.7882 |
LGBM+TPE+1000 | 0.8531 | 0.7797 |
LGBM+grid | 0.8768 | 0.8371 |