集成算法终极模型之《神器LightGBM》—最后的高山
全文共 6481 字,阅读全文需 16 分钟
写在前面的话
大家好,我是小一
2021 年第一篇文章,还是决定用 6000 多字的技术文来开篇。
今天的文章是机器学习算法里面比较重要的一篇,也是目前常规比赛较为流行的一种建模方式。
还是那句话,建议先收藏,一遍看不懂就看三遍,集成学习最后的高山,就在眼前了!
ok,直接开始 LightGBM 算法
LightGBM 是对 XgBoost 算法的强化,并且对 XgBoost 算法中的问题进行了相应的改进和优化。
回顾一下 xgb 算法:
xgb 算法是属于 boosting 算法中的一种,是 GBDT 算法的一个工程实现。xgb 在训练的时候关注的是残差,通过在目标函数中使用二阶泰勒展开和加入正则化的方式提高了模型的精度的同时降低模型的过拟合。
xgb 在决策树的生成过程中采用精确贪心的思路,寻找最佳分裂点的时候使用预排序算法,对所有特征按照特征的数值进行预排序,然后遍历每个特征上的所有分裂点,计算该分裂点分裂后的全部样本的目标函数增益,找到最大增益对应的特征和候选分裂点位,从而进行分裂,一层一层的完成建树过程。
xgb 训练的时候,是通过加法的方式进行训练,每一次通过减少残差训练一棵树,最后的预测结果是所有树的加和表示。
对应的,xgb 因为每次分裂之前先根据特征对样本进行预排序,之后会计算所有特征对应的所有分裂点的目标函数增益,所以导致不但需要额外空间存储预排序的结果,还会造成时间上很大的开销,每遍历一个分割点都需要计算分裂增益,并且在选择最大增益的时候需要对比所有样本的增益情况。
因此,基于 xgb 寻找最优分裂点的复杂度,可以总结如下:
复杂度 = 特征数量 * 分裂点数量 * 样本数量
具体 xgb 的推导可以参考上篇文章:集成算法终极模型之《手撕 xgboost》—附详细手推公式
说完了 xgb 再来看 lgb, 既然 lgb 是对 xgb 的优化,那应该也是从上面三个维度进行优化。例如:减少特诊数量、优化分裂点的个数、减少样本数量等。
Lightgbm 算法确实是这样做的,首先 lgb 通过直方图算法进一步减少分裂点的数量【相比 xgb 的近似算法,直方图算法更优】,通过单边梯度抽样算法减少样本数量,通过互斥特征捆绑算法减少特征的数量。
下面详细看看这三个算法的细节优化
直方图算法
直方图算法是把连续的浮点特征离散化为 k 个整数,也是采用了分箱的思想,不同的是直方图算法根据特征所在的 bin 对其进行梯度累加和个数统计。
在遍历数据的时候,根据离散化的后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分裂点。
具体可以参考下图:
预排序算法中,根据特征的不同取值进行分割,在直方图算法不考虑特征的取值,直接根据样本的在每个 bin 中的数量,并且在新的 bin 中存储的是特征取值的个数。
这样在遍历该特征的时候,只需要根据直方图的离散值,遍历寻找最优的分割点即可。并且由于 bins 的数量是远远小于特征不同取值的数量,所有分桶之后要遍历的分裂点个数会少很多,进一步减少计算量。
另外,xgb 在进行预排序的时候只考虑非零值进行加速【稀疏感知】,lgb 也采用非零特征去构建直方图。
由于 xgb 需要用 32 位的浮点数去存储特征值,并且用32位的整型去存储预排序的索引,而在 lgb 中只需要保存特征离散化的值,这个值一般用 8 位的整型存储即可,在内存消耗上也降为原来的 1/8
直方图做差加速
在直方图算法中一个技巧是直方图做差加速。当节点分裂成两个时,右子节点直方图等于父节点的直方图减去左子节点的直方图。
画图描述一下是这样的:
利用这个方法,Lightgbm 可以在构造一个叶子(含有较少数据)的直方图后,可以用非常微小的代价得到它兄弟叶子(含有较多数据)的直方图。
注意:原来在做每个子节点的直方图需要将该节点上的样本都计算一遍,现在只需要计算较少子节点的直方图,然后用父节点-子节点=另一个子节点
通过直方图算法找到的分割点并不算是最优的分割点,毕竟相比 xgb 算法来说通过 bins 分箱之后分裂点少了很多。但是正因为这个导致加上了一个正则化的效果,使得生成的弱分类器的有效的防止过拟合。
直方图算法可以起到的作用就是减少分割点的数量,加快计算。
xgb 中的近似算法其实也类似 lgb 的直方图算法,为什么速度比 lgb 慢很多?
从原理上来说,xgb 的近似算法是根据对 loss 的影响权值划分,用每个样本的 hi 进行分裂点划分。在分裂之前会先根据当前特征对整体的样本都进行预排序,也就是说针对不同的特征预排序结果不一样,这也将会导致二阶导的分布发生变化。
因此,xgb 的直方图算法并不是针对某个特征的 feature,所以在每一层都需要动态的构建直方图。而 lgb 针对每个特征都有一个直方图,构建一次就可以,并且在分裂的时候还可以通过直方图进行做差加速。
所以,xgb 的近似算法是不如 lgb 的直方图算法快的
单边梯度抽样算法 GOSS
单边梯度算法:Gradient-based One-Side Sampling,简称 GOSS
GOSS 是从减少样本的角度出发,排除大部分权重小的样本,仅用剩下的样本计算信息增益。
稍微解释一下上一句话:我们知道 GBDT 算法关注的是残差,并且算法的梯度大小可以反映样本的权重,这就说明梯度越小模型拟合的越好,而残差正是样本梯度的一个相反数。
可参考:xgb 中对于一阶梯度的推导:集成算法终极模型之《手撕 xgboost》—附详细手推公式
总结一下:如果要想模型降低残差的效果好,那么样本的梯度就应该越大越好,反之梯度小的样本对于降低残差的效果不大。
那,直接筛选所有梯度大的样本岂不是效果会很好?
道理没错,但是,如果直接去掉梯度小的数据,会改变数据的分布,所以 lgb 提出了单边梯度抽样算法,根据样本的权重信息进行抽样,减少了大量梯度小的样本,但是还不会过多的改变数据集的分布。
主要是因为 GOSS 算法保留了梯度大的样本,并对梯度小的样本进行随机抽取,在计算增益的时候为梯度小的样本引入了一个常数进行平衡。
首先将要进行分裂的特征的所有取值按照绝对值大小降序排序,然后取前面 a% 的梯度大的样本和剩下样本的 b%,在计算增益的时候,后面的 b% 乘以 来放大梯度小的样本的权重。
这样做的好处是将更多的在注意力放在训练不足的样本上,也可以通过权重来防止采样对原始数据分布造成太大的影响
看个例子
假设目前有 8 个训练样本,a=b=25%,样本分为 3 个 bins,对应的一阶导和二阶导如下:
首先根据样本的一阶导数【gi】对样本进行排序,结果如下:
利用 GOSS 算法进行采样,选出 8*a=2 个梯度大的样本,选出 8 *b=2 个梯度小的样本。
选中 2 个梯度大的样本为 6号 和 7号,从剩下的里面随机选中两个梯度小的样本 2号 和 4号
梯度小的样本需要乘以权重:,也就是随机选出的 2号 和 4号。根据 GOSS 算法采样后的样本重新构建直方图如下:
黑色的是梯度大的样本,不需要乘以权重,红色的是梯度小的样本,需要乘以 (1-a)/b
梯度小的样本乘以相应的权重以后,可以发现最终的样本总个数依然是 8个,lgb 正是通过 GOSS 采样的方式,在不降低太多精度的同时,减少了样本数量,使得训练速度加快。
互斥特征捆绑算法 EFB
互斥特征捆绑算法:Exclusive Feature Bundling,EFB
前面说过 EFB 算法是通过减少特征数量使用 lgb 算法更快,它指出如果将一些特征进行融合捆绑,则可以降低特征数量。
类似于 one-hot 独热编码的原理,对于高维度的稀疏数据,可以通过捆绑的方式减少特征的维度。通常被捆绑的特征之间是互斥的【特征不会同时为非零值】,这样才不会丢失信息。但是当特征之间并不是互斥的时候,就需要用一个指标对特征不互斥程度进行衡量【称之为冲突比率】,当冲突比率比较小的时候可以把两个不完全互斥的特征进行捆绑,从而不影响最后的精度
极端情况下,x1、x2、x3、x4之间完全互斥,则可以进行如下捆绑:
如何判定哪些特征可以进行捆绑?
Greedy bundle 算法利用特征和特征间的关系构造一个加权无向图,并将其转换为图着色问题,通过贪心策略得到近似解,以此来生成 bundle。
图着色问题:给定无向连通图 G 和 m 种不同的颜色。用这些颜色为图 G 的各顶点着色,每个顶点着一种颜色。是否有一种着色法使 G 中每条边的 2 个顶点着不同颜色?
看一个图着色的例子:
Greedy bundle 算法在处理的时候的具体步骤如下:
构造一个加权无向图,顶点是特征,边的权重是两个特征的总冲突值【即两个特征上同时不为0的样本个数】 根据节点的度进行降序排序,度越大,表示与其他特征的冲突越大 对于每一个特征,通过遍历已有的特征簇【没有则新建一个】,如果该特征加入到特征簇中的冲突值不超过某个阈值,则将该特征加入到该簇中。如果该特征不能加入任何一个已有的特征簇,则新建一个簇,并加入该特征。
EFB 算法允许特征并不完全互斥来增加特征捆绑的数量,在具体计算的时候需要设置一个最大互斥率指标,一般情况下这个指标设置会相对较小,此时算法的精度和效率之间会有一个最优值
像上面极端情况下,特征之间都是相互独立的点,度都为0,所以排序之后发现与其他特征的冲突为0,直接放到一个簇里面并不影响。
但是大多数的特征之间并不是相互独立的,比如说下图中的特征:
针对上图的训练集,EFB 算法在处理的时候:① 首先会先建图,计算每个顶点的总冲突值,② 其次根据节点的度进行排序,③ 最后针对每个特征进行遍历,根据特征的冲突值对其进行簇的划分
具体的操作流程是这样的:
最后,可以发现是将特征 x1、x4 进行了特征捆绑,最后的捆绑之后特征如下:
上面的过程存在一个缺点:在特征数量特征多的时候,第一步建立加权无向图会影响效率,此时可以直接统计特征之间非零样本的个数,并将其进行排序。
这样做的原因是因为更多的非零值的特征会导致更多的冲突,同样可以达到特征加权的目的
Greedy bundle 【贪婪捆绑】算法的伪代码如下:
特征如何捆绑?捆绑之后的特征值如何计算?
Merge Exclusive Features 算法将 bundle 中的特征合并为新的特征,合并的关键是原有的不同特征值在构建后的 bundle 中仍能够识别。
由于基于直方图的方法存储的是离散的 bin 而不是连续的数值,因此可以通过添加偏移的方法将不同的 bin 值设定为不同的区间。
举个例子:特征 A 和特征 B 可以实现特征捆绑,A 特征的原始取值区间是[0,10) ,B 特征的原始取值是[0,20)。如果直接进行特征值的相加,那么 5 这个值我们不能分清它是出自特征 A 还是特征 B。此时可以给特征 B 加一个偏移量 10 将取值区间转换成 [10,30),这时进行特征捆绑就可以分清来源。
MEF 特征合并算法的伪代码如下:
通过 EFB 算法,许多排他的特征会被捆绑成更少的密集特征,大大减少了特征的数量,对训练速度带来很大的提升。
对于类别型特征,如果在特征处理的时候有进行过 onehot 编码,那么 EFB 算法会再次将这些特征进行捆绑,毕竟这些特征属于互斥特征,符合特征捆绑的要求。
所以,对于类别型的特征,lgb 可以直接将每个特征取值和一个 bin 关联,从而自动的进行处理,而不需要在特征工程中进行 onehot 编码。
LightGBM 的生长策略 Leaf-wise
稍微总结一下,目前我们已经知道 lgb 在分裂点数量、特征数量和样本数量上进行优化,其中直方图算法减少了分裂点的数量、GOSS 算法减少了样本的数量,EFB 算法减少了特征的数量。
其中在创建树的过程中, lgb 除了在寻找最优分裂点的过程中进行了优化,在树的生成过程中也进行了优化,它抛弃了 xgb 中的按层生成【Level-wise】,而使用了带有深度限制的按叶子生长【Leaf-wise】。
xgb 在树的生成过程中采用了 level-wise 的增长策略,该策略遍历一次数据可以同时分裂同一层的所有叶子,容易进行多线程的优化,并且不会因为某个分支过深造成过拟合现象。
Level-wise 算法不加区分的对待同一层的叶子,实际上部分叶子的分裂增益较低,没必要进行后续的分裂操作,因为带来了很多没有必要的计算开销。
Leaf-wise 算法则是从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,一直到达到要求为止。所以在分裂次数相同的情况下,Leaf-wise 生成的树可能会较深,所以相比而言它可以降低更多的误差,得到更好的精度。
同时因为 Leaf-wise 生成的树比较深,容易产生过拟合现象,所以 lgb 在 Leaf-wise 之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。
总的来说,Level-wise 会产生一些低信息增益的节点,浪费运算资源的同时防止过拟合;Leaf-wise 会寻找更多高信息增益的节点,追求精度降低误差的同时会带来过拟合的问题。
但是过拟合问题可以通过设置 max_depth 来控制树的深度,并且 lgb 在直方图、GOSS 算法上都有正则化的体现,所以综合考虑 Leaf-wise 是一种更优的选择。
类别特征最优分割
lgb 的这个特点前面也有说到,在处理类别特征的时候,不需要通过 one-hot 编码将类别特征转换成多维的0/1特征,并且在决策树模型中也并不推荐使用 one-hot 编码,会存在以下问题:
会产生样本切分不平衡问题,导致切分增益非常小
使用 one-hot 编码意味着在每一个决策节点上只能使用 one vs rest 的决策方式【即 1 vs n-1】。
当对某一特征进行切分后,这一特征上只有少量样本为 1,大量样本为 0 【或者相反】,这时因为较小的切分样本集占总样本的比例太小,所以无论它的增益多大,乘以该比例后几乎可以忽略;同样的较大的拆分样本集,因为几乎是原始的样本集,所以增益几乎为 0。
直观上的理解就是不平衡的切分相当于没有切分。
会影响决策树的学习
决策树依赖的是数据的统计信息,而独热编码会把数据切分到零散的小空间上。零散的小空间上的统计信息是不准确的,学习效果会变差。如下图左
相反,在下图右的切分方法,数据会被切分到两个比较大的空间,统计信息会更准确,下一步的学习也会更好些。
下图右叶子节点的含义是 x=A 或者 X=C 时放在左孩子,其余放到右孩子。
基于上面问题—解决 one-hot 编码处理类别特征的不足,lgb 优化了对类别特征的支持,可以直接输入类别特征,而不需要额外的 0/1 展开。
lgb 采用 many-vs-many 的切分方式将类别特征分为两个子集,实现了类别特征的最优切分【即 m vs n】。
工程上的优化
和 xgb 一样,lgb 不但有算法上的优化,还存在工程上的优化。工程优化主要涉及到两点:
高效并行
特征并行
思想:不同机器在不同的特征集合上分别寻找最优的分割点,然后在机器间同步最优的分割点。
注意 lgb 是在每台机器上保存全部训练数据,在得到最佳划分方案后可在本地执行划分而减少了不惜要的通信。
数据并行
lgb 通过分散规约把直方图合并的任务分摊到不同的机器,降低通信和计算,并利用直方图做差进一步减少了一半的通信量。
投票并行
找出本地 Top K 的特征,并基于投票筛选出可能是最优分割点的特征,在合并时只合并每个机器筛选出来的特征。
可以参考随机森林算法中的投票策略。
Cache 命中率优化
lgb 所使用的直方图算法对 Cache 天生友好,首先是所有的特征都采用相同的方式计算梯度(区别于 xgb 不同的特征通过不同的索引获得梯度),只需要对梯度进行排序即可实现连续访问,大大提高了缓存的命中率。
其次,在内存上不需要存储叶子索引的数组,所以也就不存在 Cache Miss 的问题。
总结
lgb 针对 xgb 做了很多的优化,从而导致它的速度更快,效率更优。基于 xgb 的优化策略,lgb 主要从减少分裂点数量、减少样本数量、减少特征数量上进行优化。
在寻找最优分裂点上,我们说了直方图算法算法原理,这个可以降低分裂点的数量,然后又通过 GOSS 技术通过单边梯度抽样降低了样本的数量,最后通过 EFB 算法从贪心捆绑和特征整合上减少了特征的数量。
除此之外,lgb 在树的生长策略上进行了优化,不再使用 xgb 的Level-wise 的生长策略,而使用基于叶子节点的 Leaf-wise 的生长策略,从而提高了模型的精度。
最后是在类别特征上,lgb 支持类别特征,并且采用 many-vs-many 的切分方式将类别特征分为两个子集,实现类别特诊的最优切分。
另外,lgb 还进行了工程上的优化,通过特征并行、数据并行和投票并行的方式实现高效并行,通过直方图算法对 Cache 的命中率进行优化。
以上就是 lgb 的技术总结,lgb 正是通过上述的方式提高了训练速度,所以相比于 xgb 会更快
lgb 相比 xgb 的优点
lgb 相比 xgb 有内存和速度上的优化,这些优点一般会在面试的时候被提到,总结如下:
速度更快
lgb 采用直方图算法将遍历样本转变为遍历直方图,极大的降低了时间复杂度; lgb 在训练过程中采用 GOSS 单边梯度抽样算法过滤掉梯度小的样本,减少了大量的计算; lgb 采用 Leaf-wise 算法的策略构建树,减少了叶子节点的分裂,提高了模型的精度; lgb 采用优化后的特征并行、数据并行方法加速计算,在数据量非常大的时候还可以采用投票并行策略加速计算; lgb 对缓存进行优化,增加了缓存命中率。
内存更小
xgb 使用预排序后需要记录特征值及其对应样本的统计值的索引,而 lgb 使用了直方图算法不需要记录特征到样本的索引,极大的减少了内存消耗; lgb 采用了直方图算法将存储特征值转变成存储 bin 值,降低了内存消耗; lgb 在训练过程中采用互斥特征捆绑算法减少了特征数量,降低了内存消耗。
参考文献
我是小一,坚持向暮光所走的人,终将成为耀眼的存在!
期待你的 三连!我们下节见