数据挖掘实践(金融风控-贷款违约预测)(四):建模与调参
目录
一、引言
构建模型过程包括构建模型的预处理,选择算法与模型优化,而模型优化涉及评估验证和调参。
目标
- 学习在金融分控领域常用的机器学习模型
- 学习机器学习模型的建模过程与调参流程
项目地址:https://github.com/datawhalechina/team-learning-data-mining/tree/master/FinancialRiskControl
比赛地址:https://tianchi.aliyun.com/competition/entrance/531830/introduction
二、模型相关原理介绍
2.1 逻辑回归模型
1. logistic回归模型的含义
我们把分类模型分成两个阶段,推断阶段和决策阶段,推断阶段对联合概率分布建模,然后归一化,得到后验概率。决策阶段确定每个新输入x的类别。
我们用推断阶段的方法来推导logistic回归模型,首先对类条件概率密度
和类先验概率分布
建模,然后通过贝叶斯定理计算后验概率密度。
考虑二分类的情形,类别C1的后验概率密度:
结论:logistic回归值表示所属类的后验概率,无论是二分类还是多分类,分类结果都是后验概率最大所对应的类。
2.logistic的决策边界函数分析
决策边界函数,简而言之,就是函数的两侧是不同的分类结果,首先介绍多元变量高斯分布的概念,然后讨论logistic的决策边界函数。
(1)多元变量高斯分布的协方差解析
多元变量的高斯分布公式:
因此,可定性的分析协方差的三种情况与分布图的关系,(a)图表示正常的协方差矩阵的高斯分布图:(b)图表示协方差矩阵是对角矩阵的高斯分布图;(c)图表示协方差矩阵是对角矩阵且对角元素都相等的高斯分布图。
(2)logistic的决策边界函数分析
logistic曲线如下图,红色直线(a=0)表示决策边界函数:
假设类条件概率密度是高斯分布,即P(x|Ck),然后求解后验概率的表达式,即P(Ck|x)。由第一节可知logistic回归值就是所求的后验概率。
假设类条件概率密度的协方差相同,类条件概率密度为:
由第一节的推导公式可得后验概率为:
其中:
由后验概率
的表达式可知可知,当类条件的协方差矩阵相等时,决策边界函数是随x线性变化的直线。
结论:如下图,若两类的条件概率密度的协方差相同时(如C1和C2的协方差相同),则决策边界函数是直线;若两类的条件概率密度的协方差不相同时(如C1和C3,C2和C3),则决策边界函数是曲线。判断协方差矩阵是否相同可以根据分布图形形状是否相同来判断,如C1和C2的协方差相同,C3和C1、C2的协方差不相同。
假设类条件概率密度符合高斯分布且具有相同的协方差矩阵,则决策边界函数是一条直线;若类条件概率密度符合更一般的指数分布且缩放参数s相同,决策边界函数仍是一条直线。
3.logistic模型的参数最优化
(1)logistic模型损失函数
logistic回归模型的含义是后验概率分布,因此可以从概率的角度去设计损失函数。
(2)logistic模型的参数最优化
损失函数最小化等价于模型参数的最优化,如下图:
具体求法在本文不展开,只给出算法思想。
为了避免过拟合问题,则在原来的损失函数增加正则项,然后利用梯度下降法求最优解,这里也不展开。
4.logistic模型与感知机模型的比较
(1)logistic模型与感知机模型的相同点
由第二节分析可知,假设类条件概率分布的协方差相同,则logistic模型的决策边界函数是随x线性变化的直线,因此,感知机模型与logistic模型的分类策略一样,即决策边界函数是一样的。如下图。
感知机模型:当点落在直线上方,y>0,则分类结果C1;反之为C2。
logistic模型:当点落在直线上方,y>0,则后验概率P(C1|X)>0.5,分类结果C1;反之为C2。
考虑到对输入变量x进行非线性变换
,感知机和logistic模型的分类策略仍一样,决策边界函数相同,如下图:
感知机模型:当点落在圆外,y>0,则分类结果C1;反之为C2。
logistic模型:当点落在圆外,y>0,则后验概率P(C1|X)>0.5,分类结果C1;反之为C2。
(2)logistic模型与感知机模型的异同点
1)logistic回归模型限制值的范围在0~1,感知机模型对值范围没有限制,因此logistic模型相比感知机模型,对异常点有更强的鲁棒性。如下图,当有异常数据时,logistic模型要好于感知机模型。
2)感知机模型用误分类点到超平面的距离衡量损失函数,而logistic模型则从概率角度去衡量损失函数。
5.总结
logistic回归的含义是后验概率分布,用概率的角度去设计似然函数,logistic模型相比于感知机模型对异常数据具有更好的鲁棒性。
2.2 决策树模型
详情见:监督学习(二):决策树模型
1.决策树不确定性的度量方法
(1)不确定性的理解
下图为事件A是否发生的概率分布,事件发生记为1,讨论事件A的不确定性。
1)我们考虑一种极端的情况,若 p=1或 p=0,表示为事件A必定发生或事件A不可能发生,即不确定性为0。
2) 若 p>1/2,即事件A发生的概率大于事件A不发生的概率,我们倾向于预测事件A是发生的;若p<1/2,即事件A不发生的概率小于事件A发生的概率,我们倾向于预测事件A是不发生的。若p=1/2,即事件A发生的概率等于事件A不发生的概率,我们无法作出预测,即事件A的不确定性达到最大,以致于我们无从预测,或者可以理解成事件A太复杂了,复杂的我们只能靠运气去猜测事件A是否发生。
(2)决策树不确定性的度量方法
本文用熵和基尼指数去衡量数据集的不确定性,假设数据集包含了K类,每个类的大小和比例分别为Di 和 pi,i = 1,2,…K。
1)熵的不确定性度量方法
在信息论和概率论统计中,熵是表示随机变量不确定性的度量,令熵为H§,则:
熵越大,数据集的不确定性就越大。
2)基尼指数的不确定度量方法
数据集的基尼指数定义为:
基尼指数越大,数据集的不确定性越大。
2. 决策树的特征筛选准则
假设数据集A共有K个特征,记为xi,i=1,2,…K。数据集A的不确定性越大,则数据集A包含的信息也越多。假设数据集A的信息为H(A),经过特征xi筛选后的信息为H(A|xi),定义信息增益g(A,xi)为两者的差值,即:
g(A,xi) = H(A) - H(A|xi)
选择使数据集A信息增益最大的特征作为筛选特征,数学表示为:
x = max( g(A,xi) ) = max( H(A) - H(A|xi) )
3. 决策函数的损失函数评估
令决策树的叶节点数为T,损失函数为:
其中C(T)为决策树的训练误差,决策树模型用不确定性表示,不确定性越大,则训练误差亦越大。T表示决策树的复杂度惩罚,α参数权衡训练数据的训练误差与模型复杂度的关系,意义相当于正则化参数。
考虑极端情况:当α趋于0的时候,最优决策树模型的训练误差接近 0,模型处于过拟合;当α趋于无穷大的时候,最优决策树模型是由根节点组成的单节点树。
4. 决策树最优模型的构建步骤
将数据集A通过一定的比例划分为训练集和测试集。
决策树的损失函数:
决策树最优模型的构建步骤包括训练阶段和测试阶段:
训练阶段
(1)最小化决策树的不确定性值得到的生成模型,即 决策树生成;
(2)通过决策树剪枝,得到不同的正则化参数α下的最优决策树模型,即 决策树剪枝。
下面重点讨论训练阶段的决策树生成步骤和决策树剪枝步骤。
决策树生成步骤:
(1) 根据决策树的特征筛选准则,选择数据集信息增益最大的特征;
(2) 重复第一个步骤,直到所有叶节点的不确定性为0 。
决策树剪枝步骤:
(1) 将正则化参数α从小到大分成不同的区间
,对决策树的非叶节点进行剪枝,令该节点为T,以该节点为根节点的子树为Tt。
(2) 当α满足如下条件时:
即节点为单节点树的损失函数与子树Tt的损失函数相等,而剪枝后的复杂度降低了,泛化性能较好,因此,对该节点进行剪枝。
(3)遍历所有非叶节点,得到每个剪枝后的最优子树与对应的α参数 。
备注:决策树生成和剪枝步骤只给出大致框架,具体请参考李航《统计学习方法》。
测试阶段:
通过测试集评估不同α参数下的最优决策树模型,选择测试误差最小的最优决策树模型和相应的正则化参数α。
5. 决策树的优缺点分析
优点:
算法简单,模型具有很强的解释性
可以用于分类和回归问题
缺点:
决策树模型很容易出现过拟合现象,即训练数据集的训练误差很小,测试数据集的测试误差很大,且不同的训练数据集构建的模型相差也很大。实际项目中,我们往往不是单独使用决策树模型,为了避免决策树的过拟合,需对决策树结合集成算法使用,如bagging算法和boosting算法。
2.3 GBDT模型
1. 单决策树与提升树算法的不同
决策树是单一学习器,提升树是以CART决策树为基本学习器的提升方法。本节从结果评价角度和损失函数角度去描述两者的不同。假设决策树的学习器模型是 f(x),提升树共有K个弱学习器
,其中 i=1,2,…,K。
(1)结果评价方法:
对某一个输入数据xi
决策树模型的输出结果 yi:
提升树模型的输出结果 yi:
若是回归:
若是分类:
其中,i 表示某一个弱学习器,T代表分类模型(如逻辑线性回归,或sign函数)
(2)模型构建方法
1)决策树模型构建方法
决策树运用基尼指数来计算模型的损失函数,决策树生成阶段是最小化模型的损失函数,最大化决策树的深度;决策树剪枝阶段是采用正则化的损失函数来完成,最后通过交叉验证法选择最佳子树;
2)提升树模型构建方法
提升树是多个决策树组合为强学习器的提升方法,每个决策树的复杂度比单一决策树低得多,因此不能像单一决策树那样最大化决策树深度来最小化损失函数。决策树是boosting族的成员,弱学习器是串行迭代生成的,通过最小化每一步的弱学习器损失函数来构建提升树模型。
2. 提升树算法
提升树算法的基本学习器是决策树,且提升树算法是加法模型,因此,提升树模型可以表示为:
其中,T(x;
θ
\theta
θm)表示决策树,
θ
\theta
θm表示决策树的参数,M为树的个数。
提升树算法:
提升树算法采用前向分布算法,假设初始提升树 fo(x)=0,第m步的模型是:
其中,fm-1(x) 为上一个模型,通过经验风险极小化确定当前模型的参数
θ
\theta
θm ,
其中L表示损失函数,fm-1(x) 和 yi为常数,由(1)式可知,我们只要知道损失函数L,我们就能得到每一轮的模型参数
θ
\theta
θm ,也可以理解为用当前模型 T(Xi;
θ
\theta
θm) 去拟合上一轮模型的残差。因此,提升树算法原理是用当前的决策树去拟合上一轮的模型残差,使当前的损失函数值最小。
【例】假如有个人30岁,我们首先用20岁去拟合发现损失有10岁,这时我们用6岁去拟合,这样迭代下去,直到损失函数达到我们的要求,这就是提升树的思想。
大家有没有想过,为什么我们不直接去用30岁去拟合,这么做的步骤相当于最大化该决策树的深度,得到的模型不是一个弱分类器,导致过拟合问题的出现。
3. GBDT算法
我们再次回顾上一节例题,如果我们首先用10岁去拟合发现损失有20岁,然后再用8岁去拟合损失有12岁,这样迭代下去,与上节达到相同的损失函数值则需要更多的迭代次数,这就是GBDT算法思想:用损失函数负梯度近似残差,回归树拟合负梯度得到本轮的最小损失函数。因为提升树是直接拟合残差,所以达到相同的损失函数值时,提升树需要的迭代步骤要小得多。
步骤:
(1)用损失函数的负梯度近似残差,表示为:
(2) 回归树拟合负梯度,得到叶节点区域Rmj,j=1,2,…,J。其中J为叶节点个数,m为第m颗回归树。
(3) 针对每一个叶子节点里的样本,我们求出使损失函数最小,得到拟合叶子节点最好的输出值Cmj,如下:
(4)更新回归树模型:
(5)得到最终的回归树模型:
可能最难理解的是第三步,我们可以理解为在负梯度方向上去求模型最小化。负梯度方向理解成损失函数负梯度的回归树划分规则,用该规则去求损失函数最小化。
假设损失函数曲线如下图:
其中,m表示迭代次数,fm(xi) 表示共迭代m轮后的学习模型,L(yi,fm(xi)) 表示模型的损失函数,由提升树的原理可知,模型的损失函数随着迭代次数的增加而降低。如上图,梯度和负梯度分别为绿色线和红色线,当我们用当前的学习模型 fm(xi) 沿着负梯度方向增加时,损失函数是下降最快的,因此,GBDT算法思想是可行的。
PS:李航老师《统计学习方法》P152的GBDT算法表达式:
个人觉得下式的模型表示法可能会更好:
4. GBDT常用损失函数
由上面两节可知,提升树是用决策树去拟合上一轮的损失函数,GBDT是用决策树去拟合上一轮损失函数的梯度。因此,只要知道模型的损失函数,就能通过前向分布算法计算每轮弱学习器模型的参数,本节总结了GBDT常用的损失函数。
(1)分类算法
a) 指数损失函数:
对应负梯度:
b) 对数损失函数:
对应负梯度:
(2)回归算法
这里不讨论最常用的损失函数,如均方差和绝对损失函数。本节介绍对异常点有较好鲁棒性的Huber损失和分位数损失。
a) Huber损失函数:它是均方差和绝对损失的折衷产物,对于远离中心的异常点,采用绝对损失,而中心附近的点采用均方差。这个界限一般用分位数点度量。损失函数如下:
对应的负梯度:
b) 分位数损失。它对应的是分位数回归的损失函数,表达式为
其中, 为分位数,需要在回归前设置,对应的负梯度:
5. GBDT的正则化
正则化是为了防止模型处于过拟合,GBDT的正则化主要有三种方式:
(1)GBDT是加法模型,因此,模型可表示为:
加上正则化项v,则有:
v的取值范围为:0< v ≤ 1。达到同样的训练集学习效果,(2)式需要更多的迭代次数,即(2)式模型的复杂度较小。
(2) 对训练集进行无放回抽样,抽样比例为 v(0 < v ≤ 1)。取部分样本去做GBDT决策树的拟合可以降低方差,但是会增加偏差。
(3) 对每轮拟合的损失函数梯度的回归树进行剪枝,剪枝算法参考CART剪枝过程。
6. GBDT与AdaBoost的模型比较
假设初始化
AdaBoost模型表示为:
GBDT模型表示为:
比较这两式,你会发现GBDT模型没有权值 αi,原因是:AdaBoost每次都是对不同分布(权值)的原始数据集进行训练,得到一系列弱学习器,然后结合权值得到最终模型;GBDT模型没有权值是因为弱学习器是拟合上一轮的残差,随着回归树个数的增加,残差越来越小,因此弱学习器不需要权值。
7. 总结
本文介绍了boosting族的提升树算法和GBDT算法,提升树算法的每轮弱学习器是拟合上一轮的残差生成的,GBDT算法的每轮弱学习器是拟合上一轮损失函数的负梯度生成的。提升树算法和GBDT算法都是用CART回归树作为弱学习器,只要确定模型的损失函数,提升树和GBDT就可以通过前向分布算法进行构建。
2.4 XGBoost模型
1. 最优模型的构建方法
构建最优模型的一般方法是最小化训练数据的损失函数,我们用字母 L表示,如下式:
式(1)称为经验风险最小化,训练得到的模型复杂度较高。当训练数据较小时,模型很容易出现过拟合问题。
因此,为了降低模型的复杂度,常采用下式:
其中J(f)为模型的复杂度,式(2)称为结构风险最小化,结构风险最小化的模型往往对训练数据以及未知的测试数据都有较好的预测 。
应用:决策树的生成和剪枝分别对应了经验风险最小化和结构风险最小化,XGBoost的决策树生成是结构风险最小化的结果,后续会详细介绍。
2. Boosting的回归思想
Boosting法是结合多个弱学习器给出最终的学习结果,不管任务是分类或回归,我们都用回归任务的思想来构建最优Boosting模型 。
回归思想:把每个弱学习器的输出结果当成连续值,这样做的目的是可以对每个弱学习器的结果进行累加处理,且能更好的利用损失函数来优化模型。
假设 ft(xi) 是第 t 轮弱学习器的输出结果,yit 是模型的输出结果,yi 是实际输出结果,表达式如下:
上面两式就是加法模型,都默认弱学习器的输出结果是连续值。
分类任务的回归思想:
根据2.1式的结果,得到最终的分类器:
分类的损失函数一般选择指数函数或对数函数,这里假设损失函数为对数函数,学习器的损失函数是:
若实际输出结果yi=1,则:
求(2.5)式对 yi(t)的梯度,得:
负梯度方向是损失函数下降最快的方向,(2.6)式取反的值大于0,因此弱学习器是往增大 yi(t) 的方向迭代的,图形表示为:
如上图,当样本的实际标记 yi 是 1 时,模型输出结果 yi(t) 随着迭代次数的增加而增加(红线箭头),模型的损失函数相应的减小;当样本的实际标记 yi 是 -1时,模型输出结果 yi(t) 随着迭代次数的增加而减小(红线箭头),模型的损失函数相应的减小 。这就是加法模型的原理所在,通过多次的迭代达到减小损失函数的目的。
小结:Boosting方法把每个弱学习器的输出看成是连续值,使得损失函数是个连续值,因此可以通过弱学习器的迭代达到优化模型的目的,这也是集成学习法加法模型的原理所在 。
3. XGBoost的目标函数推导
目标函数,即损失函数,通过最小化损失函数来构建最优模型,由第一节可知, 损失函数应加上表示模型复杂度的正则项,且XGBoost对应的模型包含了多个CART树,因此,模型的目标函数为:
(3.1)式是正则化的损失函数,等式右边第一部分是模型的训练误差,第二部分是正则化项,这里的正则化项是K棵树的正则化项相加而来的。
CART树的介绍
上图为第K棵CART树,确定一棵CART树需要确定两部分,第一部分就是树的结构,这个结构将输入样本映射到一个确定的叶子节点上,记为 fk(x) 。第二部分就是各个叶子节点的值,q(x)表示输出的叶子节点序号,Wq(x) 表示对应叶子节点序号的值。由定义得:
树的复杂度定义
XGBoost法对应的模型包含了多棵cart树,定义每棵树的复杂度:
其中T为叶子节点的个数,||w||为叶子节点向量的模 。γ表示节点切分的难度,λ表示L2正则化系数。
如下例树的复杂度表示:
目标函数推导
根据(3.1)式,共进行t次迭代的学习模型的目标函数为:
泰勒公式的二阶导近似表示:
令 ft(xi) 为Δx,则(3.5)式的二阶近似展开:
其中:
l(yi,y(t-1) 表示前t-1棵树组成的学习模型的预测误差,gi和hi分别表示预测误差对当前模型的一阶导和二阶导 ,当前模型往预测误差减小的方向进行迭代。
忽略(3.8)式常数项,并结合(3.4)式,得:
通过(3.2)式简化(3.9)式:
(3.10)式第一部分是对所有训练样本集进行累加,因为所有样本都是映射为树的叶子节点,我们换种思维,从叶子节点出发,对所有的叶子节点进行累加,得:
令
Gj表示映射为叶子节点 j 的所有输入样本的一阶导之和,同理,Hj表示二阶导之和。
得:
对于第 t 棵CART树的某一个确定结构(可用q(x)表示),其叶子节点是相互独立的,Gj和Hj是确定量,因此,(3.12)可以看成是关于叶子节点的一元二次函数 。最小化(3.12)式,得:
得到最终的目标函数:
(3.14)也称为打分函数(scoring function),它是衡量树结构好坏的标准,值越小,代表这样的结构越好 。我们用打分函数选择最佳切分点,从而构建CART树。
4. XGBoost的回归树构建方法
上节推导得到的打分函数是衡量树结构好坏的标准,因此,可用打分函数来选择最佳切分点。首先确定样本特征的所有切分点,对每一个确定的切分点进行切分,切分好坏的标准如下:
Gain表示单节点obj与切分后的两个节点的树obj之差,遍历所有特征的切分点,找到最大Gain的切分点即是最佳分裂点,根据这种方法继续切分节点,得到CART树。若 γ 值设置的过大,则Gain为负,表示不切分该节点,因为切分后的树结构变差了。γ值越大,表示对切分后obj下降幅度要求越严,这个值可以在XGBoost中设定。
5. XGBoost与GDBT的区别
1)XGBoost生成CART树考虑了树的复杂度,GDBT未考虑,GDBT在树的剪枝步骤中考虑了树的复杂度。
2)XGBoost是拟合上一轮损失函数的二阶导展开,GDBT是拟合上一轮损失函数的一阶导展开,因此,XGBoost的准确性更高,且满足相同的训练效果,需要的迭代次数更少。
3)XGBoost与GDBT都是逐次迭代来提高模型性能,但是XGBoost在选取最佳切分点时可以开启多线程进行,大大提高了运行速度。
PS:本节只选取了与本文内容相关的几个区别。
2.5 LightGBM模型
1. 基于直方图的切分点算法
算法流程图如下:
流程解释:LightGBM通过直方图算法把连续的特征值离散化成对应的bin(可以理解成桶),然后累加每个bin对应特征的梯度值并计数,最后遍历所有特征和数据,寻找最优切分点 。下面详细解释这一过程 。
1) 直方图算法:
直方图通过分段函数把连续值离散化成对应的bin。
如分段函数为:
那么,对于连续值≥0的特征分成四个桶,特征的所有可能切分点个数降为4,即bin的个数 ,因此大大加快了训练速度。
直方图每个bin包含了两类信息,分别是每个bin样本中的梯度之和与每个bin中的样本数量,对应于流程图的表达式:
每个bin累加的梯度包含了 一阶梯度与二阶梯度 ,如下代码(LightGBM/src/io/sparse_bin.hpp):
void ConstructHistogram(int leaf, const score_t* gradient, const score_t* hessian,
HistogramBinEntry* out) const override {
// get current leaf boundary
const data_size_t start = leaf_start_[leaf];
const data_size_t end = start + leaf_cnt_[leaf];
const int rest = (end - start) % 4;
data_size_t i = start;
// use data on current leaf to construct histogram
for (; i < end - rest; i += 4) {
const VAL_T bin0 = ordered_pair_[i].bin;
const VAL_T bin1 = ordered_pair_[i + 1].bin;
const VAL_T bin2 = ordered_pair_[i + 2].bin;
const VAL_T bin3 = ordered_pair_[i + 3].bin;
const auto g0 = gradient[ordered_pair_[i].ridx];
const auto h0 = hessian[ordered_pair_[i].ridx];
const auto g1 = gradient[ordered_pair_[i + 1].ridx];
const auto h1 = hessian[ordered_pair_[i + 1].ridx];
const auto g2 = gradient[ordered_pair_[i + 2].ridx];
const auto h2 = hessian[ordered_pair_[i + 2].ridx];
const auto g3 = gradient[ordered_pair_[i + 3].ridx];
const auto h3 = hessian[ordered_pair_[i + 3].ridx];
out[bin0].sum_gradients += g0; // 累加一阶梯度
out[bin1].sum_gradients += g1;
out[bin2].sum_gradients += g2;
out[bin3].sum_gradients += g3;
out[bin0].sum_hessians += h0; // 累加二阶梯度
out[bin1].sum_hessians += h1;
out[bin2].sum_hessians += h2;
out[bin3].sum_hessians += h3;
++out[bin0].cnt; // 累加个数
++out[bin1].cnt;
++out[bin2].cnt;
++out[bin3].cnt;
}
for (; i < end; ++i) {
const VAL_T bin0 = ordered_pair_[i].bin;
const auto g0 = gradient[ordered_pair_[i].ridx];
const auto h0 = hessian[ordered_pair_[i].ridx];
out[bin0].sum_gradients += g0;
out[bin0].sum_hessians += h0;
++out[bin0].cnt;
}
}
2)切分点算法:
LightGBM和XGBoost的切分点算法思想是一样的,比较切分后的增益与设定的最小增益的大小,若大于 , 则 切 分 ; 反 之 , 则 不 切 分 该 节 点 。 下 面 详 细 叙 述 这 一 过 程 ( 该 节 代 码 实 现 均 在LightGBM/src/treelearner/feature_histogram.hpp):
(1)计算每个叶子节点的输出wj;
static double CalculateSplittedLeafOutput(double sum_gradients, double sum_hessians, double l1,
double ret = -ThresholdL1(sum_gradients, l1) / (sum_hessians + l2); // 计算叶子节点输出
if (max_delta_step <= 0.0f || std::fabs(ret) <= max_delta_step) {
return ret;
} else {
return Common::Sign(ret) * max_delta_step;
}
}
其中,thresholdL1函数含义为如下代码:
static double ThresholdL1(double s, double l1) {
const double reg_s = std::max(0.0, std::fabs(s) - l1); // 比较s与l1的大小
return Common::Sign(s) * reg_s;
}
根据上面两图的代码,可知lightGBM的叶子节点输出与XGBoost类似,即:
其中,Gj为叶子节点包含所有样本的一阶梯度之和,Hj为二阶梯度之和 。
(2)计算节点切分后的分数
static double GetSplitGains(double sum_left_gradients, double sum_left_hessians,
double sum_right_gradients, double sum_right_hessians,
double l1, double l2, double max_delta_step,
double min_constraint, double max_constraint, int8_t monotone_cons
// 计算左叶子节点的输出,
double left_output = CalculateSplittedLeafOutput(sum_left_gradients, sum_left_hessians, l
// 计算右叶子节点的输出
double right_output = CalculateSplittedLeafOutput(sum_right_gradients, sum_right_hessians
if (((monotone_constraint > 0) && (left_output > right_output)) ||
((monotone_constraint < 0) && (left_output < right_output))) {
return 0;
}
// 节点切分后的分数等于左叶子节点分数与右叶子节点分数之和
return GetLeafSplitGainGivenOutput(sum_left_gradients, sum_left_hessians, l1, l2, left_outpu
+ GetLeafSplitGainGivenOutput(sum_right_gradients, sum_right_hessians, l1, l2, right_outpu
}
叶子节点分数为GetLeafSplitGainGivenOutput函数:
static double GetLeafSplitGainGivenOutput(double sum_gradients, double sum_hessians, double l1,
const double sg_l1 = ThresholdL1(sum_gradients, l1);
return -(2.0 * sg_l1 * output + (sum_hessians + l2) * output * output); // 叶子节点分数计算
}
其中output代表叶子节点输出Wj 。考虑一般情况,假如叶子节点的样本一阶梯度和大于l1,那么,叶子节点分数的表达式为:
节点切分前的分数:
double gain_shift = GetLeafSplitGain(sum_gradient, sum_hessian,
meta_->config->lambda_l1, meta_->config->lambda_l2,
meta_->config->max_delta_step);
因此,计算节点切分增益:
节点切分增益= 节点分数 - (左叶子节点分数 + 右叶子节点分数)
比较切分增益与设置的最小增益大小,记为Gain:
Gain = 节点切分增益 — 最小切分增益
最后得到与XGBoost类似的形式,XGBoost为如下形式:
LightGBM的最小切分增益对应于XGBoost的γ。
2. 直方图算法改进
直方图算法通过分段函数把连续值离散化成对应的bin的复杂度为O(#feature×#data),若降低feature或data的大小,那么直方图算法的复杂度也相应的降低,微软开源的LightGBM提供了两个算法分别降低feature和data的大小:
1) GOSS(减少样本角度):保留梯度较大的样本数,减少梯度较小的样本个数 。样本训练误差小,表示该样本得到了很好的训练,对应的梯度亦越小 。一种直接的想法是抛弃这些梯度较小的样本,但是这种处理方法会改变样本集的分布,降低了训练模型的准确率(因为丢弃的都是损失误差较小的样本) 。因此,我们建议采用一种名为GOSS的方法,即GOSS保留具有大梯度的样本数,对梯度较小的样本进行随机采样,为了弥补数据集分布改变的影响,GOSS对小梯度的样本数增加了权重常数 。
GOSS伪代码如下:
伪 代 码 中 损 失 函 数 的 梯 度 g 代 表 的 含 义 是 一 阶 梯 度 和 二 阶 梯 度 的 乘 积 , 见 Github 的 实 现 (LightGBM/src/boosting/goss.hpp)。
for (int cur_tree_id = 0; cur_tree_id < num_tree_per_iteration_; ++cur_tree_id) {
size_t idx = static_cast<size_t>(cur_tree_id) * num_data_ + start + i;
tmp_gradients[i] += std::fabs(gradients_[idx] * hessians_[idx]); // 一阶梯度和二阶梯度乘
}
2) EFB(从减少特征角度):捆绑(bundling) 互斥特征,用一个特征代替多个互斥特征,达到减少特征个数的目的 。EFB算法需要解决两个问题,(1)捆绑互斥特征,(2)合并互斥特征 。这里就不详细介绍了。
3. 直方图做差优化
一个叶子节点的直方图可以由父亲节点的直方图与它兄弟的直方图做差得到 。利用这个方法,LightGBM可以用父亲节点的直方图减去数据量比较小的叶子节点直方图,得到数据量比较大的叶子节点直方图,因为该直方图是做差得到的,时间复杂度仅为O(#bins),比起不做差得到的兄弟节点的直方图,速度上可以提升一倍。
直方图做差示意图如下:
4. 树的生长策略
XGBoost是按层生长(level-wise)的方式展开节点,优点是不容易过拟合,缺点是它对每一层叶子节点不加区分的进行展开,实际上某些叶子节点的分裂增益较低,没必要进行搜索和分裂。如下图所示:
LightGBM是按最大增益的节点(叶子明智,Leaf-wise)进行展开,这样做的好处是找到分裂增益最大的叶子节点进行分裂,如此循环 。优点是效率高,在分裂次数相同的情况下,Leaf-wise可以得到更高的准确率 。缺点是可能会产生过拟合,通过设置树的最大生长深度避免 。如下图所示
5. 支持类别特征
类别特征在实际项目中比较常见,XGBoost通过one-hot编码把类别特征转化为多维特征,降低了算法运行效率,LightGBM优化了类别特征的支持,不需要对类别特征进行one-hot编码 。
6. 支持高效并行
LightGBM支持特征并行和数据并行。
特征并行:在不同机器选择局部不同特征对应的最优切分点,然后同步各机器结果,选择最优切分点 。如下图:
LightGBM对特征并行进行了优化,通过在本地保存全部数据避免对数据切分结果的通信 。
数据并行:不同机器在本地构造直方图,然后进行全局的合并,最后在合并的直方图上寻找最优分割点。如下图:
LightGBM有下面两种优化数据并行的方法:
(1)LightGBM通过"Reduce Scatter" 将把直方图合并的任务分摊到不同的机器,降低通信和计算,并利用直方图做差,进一步减少了一半的通信量 。
(2)LightGBM通过“PV-Tree”的算法进行投票并行(Voting Parallel),使通信开销变成常数级别,在数据量很大的时候,使用投票并行可以得到非常好的加速效果。如下图:
7. 与XGBoost的对比
LightGBM相对于XGboost具有更快的训练效率和更低的内存使用,如下对比图:
LightGBM内存开销降低到原来的1/8倍 ,在Expo数据集上速度快了8倍和在Higg数据集上加速了40%。
2.6 Catboost模型
后期补充(O(∩_∩)O哈哈~,不想补充了)
2.7 时间序列模型(选学)
后期补充(O(∩_∩)O哈哈~,不想补充了)
推荐教材
三、模型对比与性能评估
3.1 逻辑回归
- 优点
o 训练速度较快,分类的时候,计算量仅仅只和特征的数目相关;
o 简单易理解,模型的可解释性非常好,从特征的权重可以看到不同的特征对最后结果的影响;
o 适合二分类问题,不需要缩放输入特征;
o 内存资源占用小,只需要存储各个维度的特征值;
- 缺点
o 逻辑回归需要预先处理缺失值和异常值【可参考(三)特征工程】;
o 不能用Logistic回归去解决非线性问题,因为Logistic的决策面是线性的;
o 对多重共线性数据较为敏感,且很难处理数据不平衡的问题;
o 准确率并不是很高,因为形式非常简单,很难去拟合数据的真实分布;
3.2 决策树模型
- 优点
o 简单直观,生成的决策树可以可视化展示
o 数据不需要预处理,不需要归一化,不需要处理缺失数据
o 既可以处理离散值,也可以处理连续值
- 缺点
o 决策树算法非常容易过拟合,导致泛化能力不强(可进行适当的剪枝)
o 采用的是贪心算法,容易得到局部最优解
3.3 集成模型集成方法(ensemble method)
通过组合多个学习器来完成学习任务,通过集成方法,可以将多个弱学习器组合成一个强分类器,因此集成学习的泛化能力一般比单一分类器要好。
集成方法主要包括Bagging和Boosting,Bagging和Boosting都是将已有的分类或回归算法通过一定方式组合起来,形成一个更加强大的分类。两种方法都是把若干个分类器整合为一个分类器的方法,只是整合的方式不一样,最终得到不一样的效果。常见的基于Baggin思想的集成模型有:随机森林,基于Boosting思想的集成模型有:Adaboost、GBDT、XgBoost、LightGBM等。
Baggin和Boosting的区别总结如下:
- 样本选择上: Bagging方法的训练集是从原始集中有放回的选取,所以从原始集中选出的各轮训练集之间是独立的;而Boosting方法需要每一轮的训练集不变,只是训练集中每个样本在分类器中的权重发生变化。而权值是根据上一轮的分类结果进行调整
- 样例权重上: Bagging方法使用均匀取样,所以每个样本的权重相等;而Boosting方法根据错误率不断调整样本的权值,错误率越大则权重越大
- 预测函数上: Bagging方法中所有预测函数的权重相等;而Boosting方法中每个弱分类器都有相应的权重,对于分类误差小的分类器会有更大的权重
- 并行计算上: Bagging方法中各个预测函数可以并行生成;而Boosting方法各个预测函数只能顺序生成,因为后一个模型参数需要前一轮模型的结果。
3.4 模型评估方法
对于模型来说,其在训练集上面的误差我们称之为训练误差或者经验误差,而在测试集上的误差称之为测试误差。
对于我们来说,我们更关心的是模型对于新样本的学习能力,即我们希望通过对已有样本的学习,尽可能的将所有潜在样本的普遍规律学到手,而如果模型对训练样本学的太好,则有可能把训练样本自身所具有的一些特点当做所有潜在样本的普遍特点,这时候我们就会出现过拟合的问题。
因此我们通常将已有的数据集划分为训练集和测试集两部分,其中训练集用来训练模型,而测试集则是用来评估模型对于新样本的判别能力。
对于数据集的划分,我们通常要保证满足以下两个条件:
- 训练集和测试集的分布要与样本真实分布一致,即训练集和测试集都要保证是从样本真实分布中独立同分布采样而得;
- 训练集和测试集要互斥
对于数据集的划分有三种方法:留出法,交叉验证法和自助法,介绍如下:
①留出法
留出法是直接将数据集D划分为两个互斥的集合,其中一个集合作为训练集S,另一个作为测试集T。需要注意的是在划分的时候要尽可能保证数据分布的一致性,即避免因数据划分过程引入额外的偏差而对最终结果产生影响。为了保证数据分布的一致性,通常我们采用分层采样的方式来对数据进行采样。
Tips: 通常,会将数据集D中大约2/3~4/5的样本作为训练集,其余的作为测试集。
②交叉验证法
k折交叉验证通常将数据集D分为k份,其中k-1份作为训练集,剩余的一份作为测试集,这样就可以获得k组训练/测试集,可以进行k次训练与测试,最终返回的是k个测试结果的均值。交叉验证中数据集的划分依然是依据分层采样的方式来进行。
对于交叉验证法,其k值的选取往往决定了评估结果的稳定性和保真性,通常k值选取10。
当k=1的时候,我们称之为留一法。
③自助法
我们每次从数据集D中取一个样本作为训练集中的元素,然后把该样本放回,重复该行为m次,这样我们就可以得到大小为m的训练集,在这里面有的样本重复出现,有的样本则没有出现过,我们把那些没有出现过的样本作为测试集。
进行这样采样的原因是因为在D中约有36.8%的数据没有在训练集中出现过。留出法与交叉验证法都是使用分层采样的方式进行数据采样与划分,而自助法则是使用有放回重复采样的方式进行数据采样
数据集划分总结
- 对于数据量充足的时候,通常采用留出法或者k折交叉验证法来进行训练/测试集的划分;
- 对于数据集小且难以有效划分训练/测试集时使用自助法;
- 对于数据集小且可有效划分的时候最好使用留一法来进行划分,因为这种方法最为准确。
3.5 模型评价标准
对于本次比赛,我们选用auc作为模型评价标准,类似的评价标准还有ks、f1-score等,具体介绍与实现大家可以回顾下task1中的内容。
一起来看一下auc到底是什么?
在逻辑回归里面,对于正负例的界定,通常会设一个阈值,大于阈值的为正类,小于阈值为负类。如果我们减小这个阀值,更多的样本会被识别为正类,提高正类的识别率,但同时也会使得更多的负类被错误识别为正类。为了直观表示这一现象,引入ROC。
根据分类结果计算得到ROC空间中相应的点,连接这些点就形成ROC curve,横坐标为False Positive Rate(FPR:假正率),纵坐标为True Positive Rate(TPR:真正率)。 一般情况下,这个曲线都应该处于(0,0)和(1,1)连线的上方,如图:
ROC曲线中的四个点:
- 点(0,1):即FPR=0, TPR=1,意味着FN=0且FP=0,将所有的样本都正确分类;
- 点(1,0):即FPR=1,TPR=0,最差分类器,避开了所有正确答案;
- 点(0,0):即FPR=TPR=0,FP=TP=0,分类器把每个实例都预测为负类;
- 点(1,1):分类器把每个实例都预测为正类
总之:ROC曲线越接近左上角,该分类器的性能越好,其泛化性能就越好。而且一般来说,如果ROC是光滑的,那么基本可以判断没有太大的overfitting。
但是对于两个模型,我们如何判断哪个模型的泛化性能更优呢? 这里我们有主要以下两种方法:
如果模型A的ROC曲线完全包住了模型B的ROC曲线,那么我们就认为模型A要优于模型B;
如果两条曲线有交叉的话,我们就通过比较ROC与X,Y轴所围得曲线的面积来判断,面积越大,模型的性能就越优,这个面积我们称之为AUC(area under ROC curve)。
四、实战
# 导入相关关和相关设置
import pandas as pd
import numpy as np
import warnings
import os
import seaborn as sns
import matplotlib.pyplot as plt
"""
sns 相关设置
@return:
"""
# 声明使用 Seaborn 样式
sns.set()
# 有五种seaborn的绘图风格,它们分别是:darkgrid, whitegrid, dark, white, ticks。默认的主题是darkgrid。
sns.set_style("whitegrid")
# 有四个预置的环境,按大小从小到大排列分别为:paper, notebook, talk, poster。其中,notebook是默认的。
sns.set_context('talk')
# 中文字体设置-黑体
plt.rcParams['font.sans-serif'] = ['SimHei']
# 解决保存图像是负号'-'显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
# 解决Seaborn中文显示问题并调整字体大小
sns.set(font='SimHei')
# 读取文件
data_train = pd.read_csv('./train.csv')
data_test_a = pd.read_csv('./testA.csv')
data_train_sample = pd.read_csv("./train.csv", nrows=5)
# """合并数据集合"""
# df_train['sample'] = 'train'
# df_testA[ 'sample' ] = 'test'
# df_base = pd.concat([df_train,df_testA], axis=0)
# 读取数据
# reduce_mem_usage 函数通过调整数据类型,帮助我们减少数据在内存中占用的空间
def reduce_mem_usage(df):
start_mem = df.memory_usage().sum()
print('Memory usage of dataframe is {:.2f} MB'.format(start_mem))
for col in df.columns:
col_type = df[col].dtype
if col_type != object:
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
df[col] = df[col].astype(np.int64)
else:
if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
df[col] = df[col].astype(np.float16)
elif c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
df[col] = df[col].astype(np.float64)
else:
df[col] = df[col].astype('category')
end_mem = df.memory_usage().sum()
print('Memory usage after optimization is: {:.2f} MB'.format(end_mem))
print('Decreased by {:.1f}%'.format(100 * (start_mem - end_mem) / start_mem))
return df
# 读取数据
data = pd.read_csv('data_for_model.csv')
data = reduce_mem_usage(data)
# 建模
from sklearn.model_selection import KFold
# 分离数据集,方便进行交叉验证
X_train = data.loc[data['sample'] == 'train', :].drop(['id', 'issueDate', 'isDefault', 'sample'], axis=1)
X_test = data.loc[data['sample'] == 'test', :].drop(['id', 'issueDate', 'isDefault', 'sample'], axis=1)
y_train = data.loc[data['sample'] == 'train', 'isDefault']
# 5折交叉验证
folds = 5
seed = 2020
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)
# 使用Lightgbm进行建模
"""对训练集数据进行划分,分成训练集和验证集,并进行相应的操作"""
from sklearn.model_selection import train_test_split
import lightgbm as lgb
# 数据集划分
X_train_split, X_val, y_train_split, y_val = train_test_split(X_train, y_train, test_size=0.2)
train_matrix = lgb.Dataset(X_train_split, label=y_train_split)
valid_matrix = lgb.Dataset(X_val, label=y_val)
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'learning_rate': 0.1,
'metric': 'auc',
'min_child_weight': 1e-3,
'num_leaves': 31,
'max_depth': -1,
'reg_lambda': 0,
'reg_alpha': 0,
'feature_fraction': 1,
'bagging_fraction': 1,
'bagging_freq': 0,
'seed': 2020,
'nthread': 8,
'silent': True,
'verbose': -1,
}
"""使用训练集数据进行模型训练"""
model = lgb.train(params, train_set=train_matrix, valid_sets=valid_matrix, num_boost_round=20000, verbose_eval=1000, early_stopping_rounds=200)
# 对验证集进行预测
from sklearn import metrics
from sklearn.metrics import roc_auc_score
"""预测并计算roc的相关指标"""
val_pre_lgb = model.predict(X_val, num_iteration=model.best_iteration)
fpr, tpr, threshold = metrics.roc_curve(y_val, val_pre_lgb)
roc_auc = metrics.auc(fpr, tpr)
print('未调参前lightgbm单模型在验证集上的AUC:{}'.format(roc_auc))
"""画出roc曲线图"""
plt.figure(figsize=(8, 8))
plt.title('Validation ROC')
plt.plot(fpr, tpr, 'b', label = 'Val AUC = %0.4f' % roc_auc)
plt.ylim(0,1)
plt.xlim(0,1)
plt.legend(loc='best')
plt.title('ROC')
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
# 画出对角线
plt.plot([0, 1], [0, 1], 'r--')
plt.show()
# 更进一步的,使用5折交叉验证进行模型性能评估
import lightgbm as lgb
"""使用lightgbm 5折交叉验证进行建模预测"""
cv_scores = []
for i, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):
print('************************************ {} ************************************'.format(str(i + 1)))
X_train_split, y_train_split, X_val, y_val = X_train.iloc[train_index], y_train[train_index], X_train.iloc[
valid_index], y_train[valid_index]
train_matrix = lgb.Dataset(X_train_split, label=y_train_split)
valid_matrix = lgb.Dataset(X_val, label=y_val)
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'learning_rate': 0.1,
'metric': 'auc',
'min_child_weight': 1e-3,
'num_leaves': 31,
'max_depth': -1,
'reg_lambda': 0,
'reg_alpha': 0,
'feature_fraction': 1,
'bagging_fraction': 1,
'bagging_freq': 0,
'seed': 2020,
'nthread': 8,
'silent': True,
'verbose': -1,
}
model = lgb.train(params, train_set=train_matrix, num_boost_round=20000, valid_sets=valid_matrix, verbose_eval=1000,
early_stopping_rounds=200)
val_pred = model.predict(X_val, num_iteration=model.best_iteration)
cv_scores.append(roc_auc_score(y_val, val_pred))
print(cv_scores)
print("lgb_scotrainre_list:{}".format(cv_scores))
print("lgb_score_mean:{}".format(np.mean(cv_scores)))
print("lgb_score_std:{}".format(np.std(cv_scores)))
# 模型调参
# 1. 贪心调参
from sklearn.model_selection import cross_val_score
# 调objective
best_obj = dict()
for obj in objective:
model = LGBMRegressor(objective=obj)
"""预测并计算roc的相关指标"""
score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
best_obj[obj] = score
# num_leaves
best_leaves = dict()
for leaves in num_leaves:
model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x: x[1])[0], num_leaves=leaves)
"""预测并计算roc的相关指标"""
score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
best_leaves[leaves] = score
# max_depth
best_depth = dict()
for depth in max_depth:
model = LGBMRegressor(objective=min(best_obj.items(), key=lambda x: x[1])[0],
num_leaves=min(best_leaves.items(), key=lambda x: x[1])[0],
max_depth=depth)
"""预测并计算roc的相关指标"""
score = cross_val_score(model, X_train, y_train, cv=5, scoring='roc_auc').mean()
best_depth[depth] = score
"""
可依次将模型的参数通过上面的方式进行调整优化,并且通过可视化观察在每一个最优参数下模型的得分情况
"""
# 2. 网格搜索
"""通过网格搜索确定最优参数"""
from sklearn.model_selection import GridSearchCV
def get_best_cv_params(learning_rate=0.1, n_estimators=581, num_leaves=31, max_depth=-1, bagging_fraction=1.0,
feature_fraction=1.0, bagging_freq=0, min_data_in_leaf=20, min_child_weight=0.001,
min_split_gain=0, reg_lambda=0, reg_alpha=0, param_grid=None):
# 设置5折交叉验证
cv_fold = StratifiedKFold(n_splits=5, random_state=0, shuffle=True, )
model_lgb = lgb.LGBMClassifier(learning_rate=learning_rate,
n_estimators=n_estimators,
num_leaves=num_leaves,
max_depth=max_depth,
bagging_fraction=bagging_fraction,
feature_fraction=feature_fraction,
bagging_freq=bagging_freq,
min_data_in_leaf=min_data_in_leaf,
min_child_weight=min_child_weight,
min_split_gain=min_split_gain,
reg_lambda=reg_lambda,
reg_alpha=reg_alpha,
n_jobs=8
)
grid_search = GridSearchCV(estimator=model_lgb,
cv=cv_fold,
param_grid=param_grid,
scoring='roc_auc'
)
grid_search.fit(X_train, y_train)
print('模型当前最优参数为:{}'.format(grid_search.best_params_))
print('模型当前最优得分为:{}'.format(grid_search.best_score_))
"""以下代码未运行,耗时较长,请谨慎运行,且每一步的最优参数需要在下一步进行手动更新,请注意"""
"""
需要注意一下的是,除了获取上面的获取num_boost_round时候用的是原生的lightgbm(因为要用自带的cv)
下面配合GridSearchCV时必须使用sklearn接口的lightgbm。
"""
"""设置n_estimators 为581,调整num_leaves和max_depth,这里选择先粗调再细调"""
lgb_params = {'num_leaves': range(10, 80, 5), 'max_depth': range(3,10,2)}
get_best_cv_params(learning_rate=0.1, n_estimators=581, num_leaves=None, max_depth=None, min_data_in_leaf=20,
min_child_weight=0.001,bagging_fraction=1.0, feature_fraction=1.0, bagging_freq=0,
min_split_gain=0, reg_lambda=0, reg_alpha=0, param_grid=lgb_params)
"""num_leaves为30,max_depth为7,进一步细调num_leaves和max_depth"""
lgb_params = {'num_leaves': range(25, 35, 1), 'max_depth': range(5,9,1)}
get_best_cv_params(learning_rate=0.1, n_estimators=85, num_leaves=None, max_depth=None, min_data_in_leaf=20,
min_child_weight=0.001,bagging_fraction=1.0, feature_fraction=1.0, bagging_freq=0,
min_split_gain=0, reg_lambda=0, reg_alpha=0, param_grid=lgb_params)
"""
确定min_data_in_leaf为45,min_child_weight为0.001 ,下面进行bagging_fraction、feature_fraction和bagging_freq的调参
"""
lgb_params = {'bagging_fraction': [i/10 for i in range(5,10,1)],
'feature_fraction': [i/10 for i in range(5,10,1)],
'bagging_freq': range(0,81,10)
}
get_best_cv_params(learning_rate=0.1, n_estimators=85, num_leaves=29, max_depth=7, min_data_in_leaf=45,
min_child_weight=0.001,bagging_fraction=None, feature_fraction=None, bagging_freq=None,
min_split_gain=0, reg_lambda=0, reg_alpha=0, param_grid=lgb_params)
"""
确定bagging_fraction为0.4、feature_fraction为0.6、bagging_freq为 ,下面进行reg_lambda、reg_alpha的调参
"""
lgb_params = {'reg_lambda': [0,0.001,0.01,0.03,0.08,0.3,0.5], 'reg_alpha': [0,0.001,0.01,0.03,0.08,0.3,0.5]}
get_best_cv_params(learning_rate=0.1, n_estimators=85, num_leaves=29, max_depth=7, min_data_in_leaf=45,
min_child_weight=0.001,bagging_fraction=0.9, feature_fraction=0.9, bagging_freq=40,
min_split_gain=0, reg_lambda=None, reg_alpha=None, param_grid=lgb_params)
"""
确定reg_lambda、reg_alpha都为0,下面进行min_split_gain的调参
"""
lgb_params = {'min_split_gain': [i/10 for i in range(0,11,1)]}
get_best_cv_params(learning_rate=0.1, n_estimators=85, num_leaves=29, max_depth=7, min_data_in_leaf=45,
min_child_weight=0.001,bagging_fraction=0.9, feature_fraction=0.9, bagging_freq=40,
min_split_gain=None, reg_lambda=0, reg_alpha=0, param_grid=lgb_params)
"""
参数确定好了以后,我们设置一个比较小的learning_rate 0.005,来确定最终的num_boost_round
"""
# 设置5折交叉验证
# cv_fold = StratifiedKFold(n_splits=5, random_state=0, shuffle=True, )
final_params = {
'boosting_type': 'gbdt',
'learning_rate': 0.01,
'num_leaves': 29,
'max_depth': 7,
'min_data_in_leaf':45,
'min_child_weight':0.001,
'bagging_fraction': 0.9,
'feature_fraction': 0.9,
'bagging_freq': 40,
'min_split_gain': 0,
'reg_lambda':0,
'reg_alpha':0,
'nthread': 6
}
cv_result = lgb.cv(train_set=lgb_train,
early_stopping_rounds=20,
num_boost_round=5000,
nfold=5,
stratified=True,
shuffle=True,
params=final_params,
metrics='auc',
seed=0,
)
print('迭代次数{}'.format(len(cv_result['auc-mean'])))
print('交叉验证的AUC为{}'.format(max(cv_result['auc-mean'])))
# 3.贝叶斯调参
from sklearn.model_selection import cross_val_score
"""定义优化函数"""
def rf_cv_lgb(num_leaves, max_depth, bagging_fraction, feature_fraction, bagging_freq, min_data_in_leaf,
min_child_weight, min_split_gain, reg_lambda, reg_alpha):
# 建立模型
model_lgb = lgb.LGBMClassifier(boosting_type='gbdt', bjective='binary', metric='auc',
learning_rate=0.1, n_estimators=5000,
num_leaves=int(num_leaves), max_depth=int(max_depth),
bagging_fraction=round(bagging_fraction, 2),
feature_fraction=round(feature_fraction, 2),
bagging_freq=int(bagging_freq), min_data_in_leaf=int(min_data_in_leaf),
min_child_weight=min_child_weight, min_split_gain=min_split_gain,
reg_lambda=reg_lambda, reg_alpha=reg_alpha,
n_jobs=8
)
val = cross_val_score(model_lgb, X_train_split, y_train_split, cv=5, scoring='roc_auc').mean()
return val
from bayes_opt import BayesianOptimization
"""定义优化参数"""
bayes_lgb = BayesianOptimization(
rf_cv_lgb,
{
'num_leaves':(10, 200),
'max_depth':(3, 20),
'bagging_fraction':(0.5, 1.0),
'feature_fraction':(0.5, 1.0),
'bagging_freq':(0, 100),
'min_data_in_leaf':(10,100),
'min_child_weight':(0, 10),
'min_split_gain':(0.0, 1.0),
'reg_alpha':(0.0, 10),
'reg_lambda':(0.0, 10),
}
)
"""开始优化"""
bayes_lgb.maximize(n_iter=10)
"""显示优化结果"""
bayes_lgb.max
"""调整一个较小的学习率,并通过cv函数确定当前最优的迭代次数"""
base_params_lgb = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': 'auc',
'learning_rate': 0.01,
'num_leaves': 14,
'max_depth': 19,
'min_data_in_leaf': 37,
'min_child_weight':1.6,
'bagging_fraction': 0.98,
'feature_fraction': 0.69,
'bagging_freq': 96,
'reg_lambda': 9,
'reg_alpha': 7,
'min_split_gain': 0.4,
'nthread': 8,
'seed': 2020,
'silent': True,
'verbose': -1,
}
cv_result_lgb = lgb.cv(
train_set=train_matrix,
early_stopping_rounds=1000,
num_boost_round=20000,
nfold=5,
stratified=True,
shuffle=True,
params=base_params_lgb,
metrics='auc',
seed=0
)
print('迭代次数{}'.format(len(cv_result_lgb['auc-mean'])))
print('最终模型的AUC为{}'.format(max(cv_result_lgb['auc-mean'])))
# 模型参数已经确定,建立最终模型并对验证集进行验证
import lightgbm as lgb
"""使用lightgbm 5折交叉验证进行建模预测"""
cv_scores = []
for i, (train_index, valid_index) in enumerate(kf.split(X_train, y_train)):
print('************************************ {} ************************************'.format(str(i + 1)))
X_train_split, y_train_split, X_val, y_val = X_train.iloc[train_index], y_train[train_index], X_train.iloc[
valid_index], y_train[valid_index]
train_matrix = lgb.Dataset(X_train_split, label=y_train_split)
valid_matrix = lgb.Dataset(X_val, label=y_val)
params = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': 'auc',
'learning_rate': 0.01,
'num_leaves': 14,
'max_depth': 19,
'min_data_in_leaf': 37,
'min_child_weight': 1.6,
'bagging_fraction': 0.98,
'feature_fraction': 0.69,
'bagging_freq': 96,
'reg_lambda': 9,
'reg_alpha': 7,
'min_split_gain': 0.4,
'nthread': 8,
'seed': 2020,
'silent': True,
}
model = lgb.train(params, train_set=train_matrix, num_boost_round=14269, valid_sets=valid_matrix, verbose_eval=1000,
early_stopping_rounds=200)
val_pred = model.predict(X_val, num_iteration=model.best_iteration)
cv_scores.append(roc_auc_score(y_val, val_pred))
print(cv_scores)
print("lgb_scotrainre_list:{}".format(cv_scores))
print("lgb_score_mean:{}".format(np.mean(cv_scores)))
print("lgb_score_std:{}".format(np.std(cv_scores)))
""""""
base_params_lgb = {
'boosting_type': 'gbdt',
'objective': 'binary',
'metric': 'auc',
'learning_rate': 0.01,
'num_leaves': 14,
'max_depth': 19,
'min_data_in_leaf': 37,
'min_child_weight':1.6,
'bagging_fraction': 0.98,
'feature_fraction': 0.69,
'bagging_freq': 96,
'reg_lambda': 9,
'reg_alpha': 7,
'min_split_gain': 0.4,
'nthread': 8,
'seed': 2020,
'silent': True,
}
"""使用训练集数据进行模型训练"""
final_model_lgb = lgb.train(base_params_lgb, train_set=train_matrix, valid_sets=valid_matrix, num_boost_round=13000, verbose_eval=1000, early_stopping_rounds=200)
"""预测并计算roc的相关指标"""
val_pre_lgb = final_model_lgb.predict(X_val)
fpr, tpr, threshold = metrics.roc_curve(y_val, val_pre_lgb)
roc_auc = metrics.auc(fpr, tpr)
print('调参后lightgbm单模型在验证集上的AUC:{}'.format(roc_auc))
"""画出roc曲线图"""
plt.figure(figsize=(8, 8))
plt.title('Validation ROC')
plt.plot(fpr, tpr, 'b', label = 'Val AUC = %0.4f' % roc_auc)
plt.ylim(0,1)
plt.xlim(0,1)
plt.legend(loc='best')
plt.title('ROC')
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
# 画出对角线
plt.plot([0, 1], [0, 1], 'r--')
plt.show()
"""保存模型到本地"""
# 保存模型
import pickle
pickle.dump(final_model_lgb, open('model_lgb_best.pkl', 'wb'))
结果:
未调参前lightgbm单模型在验证集上的AUC:0.7249469360631181
调参后lightgbm单模型在验证集上的AUC:0.7317405262608612
五、经验总结
在本节中,我们主要完成了建模与调参的工作,首先在建模的过程中通过划分数据集、交叉验证等方式对模型的性能进行评估验证,并通过可视化方式绘制模型ROC曲线。
最后我们对模型进行调参,这部分介绍了贪心调参、网格搜索调参、贝叶斯调参共三种调参手段,重点使用贝叶斯调参对本次项目进行简单优化。