LightGBM 由微软提出,主要用于解决 GDBT 在海量数据中遇到的问题,以便其可以更好更快地用于工业实践中。其相对 XGBoost 具有训练速度快、内存占用低的特点。主要有以下一些优化点:

  1. 单边梯度抽样算法;
  2. 直方图算法;
  3. 互斥特征捆绑算法;
  4. 基于最大深度的 Leaf-wise 的垂直生长算法;
  5. 类别特征最优分割;
  6. 特征并行和数据并行;
  7. 缓存优化

  接下来分数学原理和工程实现两个角度,分别介绍这些优化点。

1. 数学原理

1.1 单边梯度抽样算法(Gradient-based One-Side Sampling, GOSS)

  GOSS 算法从减少样本的角度出发,排除大部分小梯度的样本,仅用剩下的样本计算信息增益,它是一种在减少数据量和保证精度上平衡的算法。

  AdaBoost 中,样本权重是数据重要性的指标。然而在 GBDT 中没有原始样本权重,不能应用权重采样。幸运的是,我们观察到 GBDT 中每个数据都有不同的梯度值,对采样十分有用。即梯度小的样本,训练误差也比较小,说明数据已经被模型学习得很好了,直接想法就是丢掉这部分梯度小的数据。然而这样做会改变数据的分布,将会影响训练模型的精确度,为了不改变样本的数据分布,在计算增益时为梯度小的样本引入一个常数进行平衡。

-w643

  我们可以看到 GOSS 事先基于梯度的绝对值对样本进行排序(无需保存排序后结果),然后拿到前 a% 的梯度大的样本,和剩下样本的 b%,在计算增益时,通过乘上 [公式] 来放大梯度小的样本的权重。一方面算法将更多的注意力放在训练不足的样本上,另一方面通过乘上权重来防止采样对原始数据分布造成太大的影响。

1.2 直方图算法(Histogram-based Algorithm)

 1)直方图算法思想

  直方图算法的基本思想是将连续的特征离散化为 k 个离散特征,同时构造一个宽度为 k 的直方图用于统计信息(含有 k 个 bin)。利用直方图算法我们无需遍历数据,只需要遍历 k 个 bin 即可找到最佳分裂点。

  我们知道特征离散化的具有很多优点,如存储方便、运算更快、鲁棒性强、模型更加稳定等等。对于直方图算法来说最直接的有以下两个优点(以 k=256 为例):

  • 内存占用更小:XGBoost 需要用 32 位的浮点数去存储特征值,并用 32 位的整形去存储索引,而 LightGBM 只需要用 8 位去存储直方图,相当于减少了 1/8;
  • 计算代价更小:计算特征分裂增益时,XGBoost 需要遍历一次数据找到最佳分裂点,而 LightGBM 只需要遍历一次 k 次,直接将时间复杂度从 $O(# \text { data } * # \text { feature })$ 降低到 $O(k * # \text { feature })$,而我们知道 $# \text { data } » k$。

  虽然将特征离散化后无法找到精确的分割点,可能会对模型的精度产生一定的影响,但较粗的分割也起到了正则化的效果,一定程度上降低了模型的方差。

-w620

  直方图算法的基本思想:先把连续的浮点特征值离散化成k个整数,同时构造一个宽度为k的直方图。遍历数据时,根据离散化后的值作为索引在直方图中累积统计量,当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。

  从算法中可以看到:直方图优化算法需要在训练前预先把特征值转化为bin value,也就是对每个特征的取值做个分段函数,将所有样本在该特征上的取值划分到某一段(bin)中。最终把特征取值从连续值转化成了离散值。需要注意得是:feature value对应的bin value在整个训练过程中是不会改变的。

 2)直方图加速

  在构建叶节点的直方图时,我们还可以通过父节点的直方图与相邻叶节点的直方图相减的方式构建,从而减少了一半的计算量。在实际操作过程中,我们还可以先计算直方图小的叶子节点,然后利用直方图作差来获得直方图大的叶子节点。

 3)稀疏特征优化

  XGBoost 在进行预排序时只考虑非零值进行加速,而 LightGBM 也采用类似策略:只用非零特征构建直方图。

1.3 互斥特征捆绑算法(Exclusive Feature Bundling, EFB)

  高维特征往往是稀疏的,而且特征间可能是相互排斥的(如两个特征不同时取非零值),如果两个特征并不完全互斥(如只有一部分情况下是不同时取非零值),可以用互斥率表示互斥程度。EFB 算法指出如果将一些特征进行融合绑定,则可以降低特征数量。

  针对这种想法,我们会遇到两个问题:

  1. 哪些特征可以一起绑定?
  2. 特征绑定后,特征值如何确定?

  对于问题一:EFB 算法利用特征和特征间的关系构造一个加权无向图,并将其转换为图着色算法。我们知道图着色是个 NP-Hard 问题,故采用贪婪算法得到近似解,具体步骤如下:

  1. 构造一个加权无向图,顶点是特征,边是两个特征间互斥程度;
  2. 根据节点的度进行降序排序,度越大,与其他特征的冲突越大;
  3. 遍历每个特征,将它分配给现有特征包,或者新建一个特征包,是的总体冲突最小。

  算法允许两两特征并不完全互斥来增加特征捆绑的数量,通过设置最大互斥率 $\gamma$ 来平衡算法的精度和效率。EFB 算法的伪代码如下所示:

我们看到时间复杂度为 $O(# \text { feature}^2)$,在特征不多的情况下可以应付,但如果特征维度达到百万级别,计算量则会非常大,为了改善效率,我们提出了一个更快的解决方案:将 EFB 算法中通过构建图,根据节点度来排序的策略改成了根据非零值的技术排序,因为非零值越多,互斥的概率会越大。

  对于问题二:论文给出特征合并算法,其关键在于原始特征能从合并的特征中分离出来。假设 Bundle 中有两个特征值,A 取值为 [0, 10]、B 取值为 [0, 20],为了保证特征 A、B 的互斥性,我们可以给特征 B 添加一个偏移量转换为 [10, 30],Bundle 后的特征其取值为 [0, 30],这样便实现了特征合并。具体算法如下所示:

1.4 带深度限制的 Leaf-wise 算法

  在建树的过程中有两种策略:

  1. Level-wise:基于层进行生长,直到达到停止条件;
  2. Leaf-wise:每次分裂增益最大的叶子节点,直到达到停止条件。

  XGBoost 采用 Level-wise 的增长策略,方便并行计算每一层的分裂节点,提高了训练速度,但同时也因为节点增益过小增加了很多不必要的分裂,降低了计算量;LightGBM 采用 Leaf-wise 的增长策略减少了计算量,配合最大深度的限制防止过拟合,由于每次都需要计算增益最大的节点,所以无法并行分裂。

1.5 类别特征最优分割

  大部分的机器学习算法都不能直接支持类别特征,一般都会对类别特征进行编码,然后再输入到模型中。常见的处理类别特征的方法为 one-hot 编码,但我们知道对于决策树来说并不推荐使用 one-hot 编码:

  1. 会产生样本切分不平衡问题,切分增益会非常小。如,国籍切分后,会产生是否中国,是否美国等一系列特征,这一系列特征上只有少量样本为 1,大量样本为 0。这种划分的增益非常小:较小的那个拆分样本集,它占总样本的比例太小。无论增益多大,乘以该比例之后几乎可以忽略;较大的那个拆分样本集,它几乎就是原始的样本集,增益几乎为零;
  2. 影响决策树学习:决策树依赖的是数据的统计信息,而独热码编码会把数据切分到零散的小空间上。在这些零散的小空间上统计信息不准确的,学习效果变差。本质是因为独热码编码之后的特征的表达能力较差的,特征的预测能力被人为的拆分成多份,每一份与其他特征竞争最优划分点都失败,最终该特征得到的重要性会比实际值低。

  LightGBM 原生支持类别特征,采用 many-vs-many 的切分方式将类别特征分为两个子集,实现类别特征的最优切分。假设有某维特征有 $k$ 个类别,则有 $2^{k-1}-1$ 中可能,时间复杂度为 $O(2^k)$,LightGBM 基于 Fisher 大佬的 《On Grouping For Maximum Homogeneity》实现了 $O(klogk)$ 的时间复杂度。

  上图为左边为基于 one-hot 编码进行分裂,后图为 LightGBM 基于 many-vs-many 进行分裂,在给定深度情况下,后者能学出更好的模型。

  其基本思想在于每次分组时都会根据训练目标对类别特征进行分类,根据其累积值 $\frac{\sum gradient}{\sum hessian}$ 对直方图进行排序,然后在排序的直方图上找到最佳分割。此外,LightGBM 还加了约束条件正则化,防止过拟合。

2. 工程实现

2.1 特征并行

  传统的特征并行算法在于对数据进行垂直划分,然后使用不同机器找到不同特征的最优分裂点,基于通信整合得到最佳划分点,然后基于通信告知其他机器划分结果。

  传统的特征并行方法有个很大的缺点:需要告知每台机器最终划分结果,增加了额外的复杂度(因为对数据进行垂直划分,每台机器所含数据不同,划分结果需要通过通信告知)。

  LightGBM 则不进行数据垂直划分,每台机器都有训练集完整数据,在得到最佳划分方案后可在本地执行划分而减少了不必要的通信。

2.2 数据并行

  传统的数据并行策略主要为水平划分数据,然后本地构建直方图并整合成全局直方图,最后在全局直方图中找出最佳划分点。

  这种数据划分有一个很大的缺点:通讯开销过大。如果使用点对点通信,一台机器的通讯开销大约为 $O(# \text { machine } * # \text { feature } * # \text { bin })$;如果使用集成的通信,则通讯开销为 $O(2 * # \text { feature } * # \text { bin })$。

  LightGBM 采用分散规约(Reduce scatter)的方式将直方图整合的任务分摊到不同机器上,从而降低通信代价,并通过直方图做差进一步降低不同机器间的通信。

2.3 投票并行

  针对数据量特别大特征也特别多的情况下,可以采用投票并行。投票并行主要针对数据并行时数据合并的通信代价比较大的瓶颈进行优化,其通过投票的方式只合并部分特征的直方图从而达到降低通信量的目的。

大致步骤为两步:

  1. 本地找出 Top K 特征,并基于投票筛选出可能是最优分割点的特征;
  2. 合并时只合并每个机器选出来的特征。

2.4 缓存优化

  上边说到 XGBoost 的预排序后的特征是通过索引给出的样本梯度的统计值,因其索引访问的结果并不连续,XGBoost 提出缓存访问优化算法进行改进。

  而 LightGBM 所使用直方图算法对 Cache 天生友好:

  1. 首先,所有的特征都采用相同的方法获得梯度(区别于不同特征通过不同的索引获得梯度),只需要对梯度进行排序并可实现连续访问,大大提高了缓存命中;
  2. 其次,因为不需要存储特征到样本的索引,降低了存储消耗,而且也不存在 Cache Miss的问题。

总结

  LightGBM 在很多方面会比 XGBoost 表现的更为优秀。它有以下优势:

  • 基于Histogram的决策树算法
  • 带深度限制的 Leaf-wise 的叶子生长策略
  • 更快的训练效率
  • 低内存使用
  • 更高的准确率
  • 多线程优化,支持并行化学习
  • 可处理大规模数据
  • 直方图做差加速
  • 基于直方图的稀疏特征优化
  • 直接支持类别特征(Categorical Feature)
  • Cache 命中率优化

  总结下 LightGBM 相对于 XGBoost 的优点,从内存和速度两方面进行介绍。

  • 内存更小
    • XGBoost 使用预排序后需要记录特征值及其对应样本的统计值的索引,而 LightGBM 使用了直方图算法将特征值转变为 bin 值,且不需要记录特征到样本的索引,将空间复杂度从 $O(2 * # \text { data })$ 降低为 $O(# \text { bin})$,极大的减少了内存消耗;
    • LightGBM 采用了直方图算法将存储特征值转变为存储 bin 值,降低了内存消耗;
    • LightGBM 在训练过程中采用互斥特征捆绑算法减少了特征数量,降低了内存消耗。
  • 速度更快
    • LightGBM 采用了直方图算法将遍历样本转变为遍历直方图,极大的降低了时间复杂度;
    • LightGBM 在训练过程中采用单边梯度算法过滤掉梯度小的样本,减少了大量的计算;
    • LightGBM 采用了基于 Leaf-wise 算法的增长策略构建树,减少了很多不必要的计算量;
    • LightGBM 采用优化后的特征并行、数据并行方法加速计算,当数据量非常大的时候还可以采用投票并行的策略;
    • LightGBM 对缓存也进行了优化,增加了 Cache hit 的命中率。

lightGBM调参

(1)num_leaves

LightGBM使用的是leaf-wise的算法,因此在调节树的复杂程度时,使用的是num_leaves而不是max_depth。

大致换算关系:num_leaves = 2^(max_depth)

(2)样本分布非平衡数据集:可以param[‘is_unbalance’]=’true’

(3)Bagging参数:bagging_fraction+bagging_freq(必须同时设置)、feature_fraction

(4)min_data_in_leaf、min_sum_hessian_in_leaf

# 01. train set and test set
train_data = lgb.Dataset(dtrain[predictors],label=dtrain[target],feature_name=list(dtrain[predictors].columns), categorical_feature=dummies)
test_data = lgb.Dataset(dtest[predictors],label=dtest[target],feature_name=list(dtest[predictors].columns), categorical_feature=dummies)

# 02. parameters
param = {
    'max_depth':6,
    'num_leaves':64,
    'learning_rate':0.03,
    'scale_pos_weight':1,
    'num_threads':40,
    'objective':'binary',
    'bagging_fraction':0.7,
    'bagging_freq':1,
    'min_sum_hessian_in_leaf':100
}

param['is_unbalance']='true'
param['metric'] = 'auc'

# 03. cv and train
bst=lgb.cv(param,train_data, num_boost_round=1000, nfold=3, early_stopping_rounds=30)
estimators = lgb.train(param,train_data,num_boost_round=len(bst['auc-mean']))

# 04. test predict
ypred = estimators.predict(dtest[predictors])

References

  1. 机器学习算法之 LightGBM
  2. 『 论文阅读』LightGBM原理-LightGBM: A Highly Efficient Gradient Boosting Decision Tree
  3. LightGBM.pdf
  4. 『我爱机器学习』集成学习(四)LightGBM
  5. LightGBM
  6. lightgbm做二分类,多分类以及回归任务
  7. 决策树(下)——XGBoost、LightGBM(非常详细)
  8. RF,GBDT,XGBOOST, LightGBM的对比和分析