LightGBM 算法理论及scala实现

算法理论

算法的设计理念

LightGBM 采用分布式的GBDT,选择了基于直方图的决策树算法
LightGBM 的动机
​ 常用的机器学习算法,例如神经网络等算法,都可以以 mini-batch 的方式训练,训练数据的大小不会受到内存限制。

​ 而 GBDT 在每一次迭代的时候,都需要遍历整个训练数据多次。如果把整个训练数据装进内存则会限制训练数据的大小;如果不装进内存,反复地读写训练数据又会消耗非常大的时间。尤其面对工业级海量的数据,普通的 GBDT 算法是不能满足其需求的。

LightGBM 提出的主要原因就是为了解决 GBDT 在海量数据遇到的问题,让 GBDT 可以更好更快地用于工业实践。
1
Xgboost 原理
​ 目前已有的 GBDT 工具基本都是基于预排序的方法(pre-sorted)的决策树算法(如 xgboost)。这种构建决策树的算法基本思想是:

首先,对所有特征都按照特征的数值进行预排序。

其次,在遍历分割点的时候用O(#data)的代价找到一个特征上的最好分割点。

最后,找到一个特征的分割点后,将数据分裂成左右子节点。

这样的预排序算法的优点是:能精确地找到分割点。

缺点也很明显:

首先,空间消耗大。这样的算法需要保存数据的特征值,还保存了特征排序的结果(例如排序后的索引,为了后续快速的计算分割点),这里需要消耗训练数据两倍的内存。

其次,时间上也有较大的开销,在遍历每一个分割点的时候,都需要进行分裂增益的计算,消耗的代价大。

最后,对 cache 优化不友好。在预排序后,特征对梯度的访问是一种随机访问,并且不同的特征访问的顺序不一样,无法对 cache 进行优化。同时,在每一层长树的时候,需要随机访问一个行索引到叶子索引的数组,并且不同特征访问的顺序也不一样,也会造成较大的 cache miss。

LightGBM 优化
LightGBM 优化部分包含以下:

  • 基于 Histogram 的决策树算法
  • 带深度限制的 Leaf-wise 的叶子生长策略
  • 直方图做差加速
  • 直接支持类别特征(Categorical Feature)
  • Cache 命中率优化
  • 基于直方图的稀疏特征优化
  • 多线程优化。

lightgbm数学原理

关系:

lightgbm=xgboost+histogram+goss+efb

histogram算法:直方图算法,减少候选分位点数量
goss算法:基于梯度的单边采样算法,减少样本数量
efb:互斥特征捆绑算法,减少特征数量

通过这三个算法的引入,lightgbm生成一片叶子需要的复杂度大大降低了,从而极大节约了计算时间
同时histogram算法还将特征由浮点数转换成0~255位的整数进行存储,从而极大节约了内存存储

1.Histogram 算法

​ 直方图算法的基本思想是先对特征值进行装箱处理,把连续的浮点特征值离散化成k个整数,形成一个个的箱体,同时构造一个宽度为k的直方图。在遍历数据的时候,根据离散化后的值作为索引在直方图中累积统计量(即频数直方图),当遍历一次数据后,直方图累积了需要的统计量,然后根据直方图的离散值,遍历寻找最优的分割点。

对于连续特征来说,装箱就是特征工程的离散化:如,[0,10)区间的值可以赋值为0,[10,20)赋值1等,这样可以把众多的数值划分到有限的分箱中,默认分箱数是256,也可以设置。

对于分类特征,则是每种取值放入一个分箱,取值个数大于最大分箱数时会忽略那些很少出现的分类值。比如国家名称,少于256直接分箱,大于256会忽略出现最少的。

然后在计算上的代价也大幅降低,预排序算法每遍历一个特征值就需要计算一次分裂的增益,而直方图算法只需要计算k次(k可以认为是常数),时间复杂度从O(#data*#feature)优化到O(k*#features)。

此外,直方图算法还能做减法直方图加速。当节点分裂成两个时,右边节点的直方图等于父节点直方图减左边叶子节点直方图。

​ 当然,Histogram 算法并不是完美的。由于特征被离散化后,找到的并不是很精确的分割点,所以会对结果产生影响。但在不同的数据集上的结果表明,离散化的分割点对最终的精度影响并不是很大,甚至有时候会更好一点。

原因是决策树本来就是弱模型,分割点是不是精确并不是太重要;较粗的分割点也有正则化的效果,可以有效地防止过拟合;即使单棵树的训练误差比精确分割的算法稍大,但在梯度提升(Gradient Boosting)的框架下没有太大的影响。

lightGBM采用基于直方图的决策树算法,该算法将遍历样本转化为遍历直方图。既降低了内存使用率又可以利用直方图做差的方式降低计算复杂度;而XGBoost采用的是基于预排序(pre-sorted)的决策树算法,不仅需要额外的空间保存特征排序结果,而且需要遍历每一个分割点计算分裂增益,计算代价大(候选分裂点数和样本数成正比)。

2.goss算法

单边梯度采样算法,gradient-based one-side sampling

在训练过程中采用单边梯度算法(GOSS)过滤掉梯度小的样本,减少了大量的计算。梯度更大的样本点在计算信息增益时会占有更重要的作用,当我们对样本进行下采样时保留这些梯度较大的样本点,并随机去掉梯度小的样本点。GOSS先将要进行分裂的特征的所有梯度绝对值大小进行降序排序,选取梯度绝对值最大的 a × 100 % a \times 100\% a×100%个数据,然后在剩下的较小梯度数据中随机选择 b × 100 % b \times 100\% b×100%个数据。接着将这 b × 100 % b \times100\% b×100%个数据的信息增益乘以一个常数 1 − a b \frac{1-a} {b} b1a,这样算法既可以更关注训练不足的样本,又不用担心会改变原数据集的分布。(? 1 − a b \frac{1-a} {b} b1a的理论推导)

goss算法描述

输入:训练数据,迭代步数d,大梯度数据的采样率a,小梯度数据的采样率b,损失函数和若学习器的类型(一般为决策树);

输出:训练好的强学习器;

(1)根据样本点的梯度的绝对值对它们进行降序排序;

(2)对排序后的结果选取前a*100%的样本生成一个大梯度样本点的子集;

(3)对剩下的样本集合(1-a)100%的样本,随机的选取b(1-a)*100%个样本点,生成一个小梯度样本点的集合;

(4)将大梯度样本和采样的小梯度样本合并;

(5)将小梯度样本乘上一个权重系数;

(6)使用上述的采样的样本,学习一个新的弱学习器;

(7)不断地重复(1)~(6)步骤直到达到规定的迭代次数或者收敛为止。

通过上面的算法可以在不改变数据分布的前提下不损失学习器精度的同时大大的减少模型学习的速率。

从上面的描述可知,当a=0时,GOSS算法退化为随机采样算法;当a=1时,GOSS算法变为采取整个样本的算法。在许多情况下,GOSS算法训练出的模型精确度要高于随机采样算法。另一方面,采样也将会增加若学习器的多样性,从而潜在的提升了训练出的模型泛化能力。

3.efb算法

exclusive feature bundling,互斥特征捆绑算法

在训练过程可以将两两互斥或冲突率低的特征捆绑成一个特征进行处理,减少了特征数量,尤其是特征中包含大量稀疏特征的时候,降低了内存消耗(使用图着色原理选择要捆绑在一起的特征)。可以直接将每个类别取值和一个bin关联,从而自动处理他们,而无需预处理成one-hot编码。(因为对于类别特征,如果转换成onehot编码,则这些onehot编码后的多个特征相互之间是互斥的,从而可以被捆绑成为一个特征。因此,对于指定为类别特征的特征,LightGBM可以直接将每个类别取值和一个bin关联,从而自动地处理它们,而无需预处理成onehot编码多此一举。)

EFB算法描述
输入:特征F,最大冲突数K,图G;

输出:特征捆绑集合bundles;

(1)构造一个边带有权重的图,其权值对应于特征之间的总冲突;

(2)通过特征在图中的度来降序排序特征;

(3)检查有序列表中的每个特征,并将其分配给具有小冲突的现有bundling(由控制),或创建新bundling。

上述算法的时间复杂度为并且在模型训练之前仅仅被处理一次即可。在特征维度不是很大时,这样的复杂度是可以接受的。但是当样本维度较高时,这种方法就会特别的低效。所以对于此,作者又提出的另外一种更加高效的算法:按非零值计数排序,这类似于按度数排序,因为更多的非零值通常会导致更高的冲突概率。 这仅仅改变了上述算法的排序策略,所以只是针对上述算法将按度数排序改为按非0值数量排序,其他不变。

4.带深度限制的 Leaf-wise 的叶子生长策略

​ 在 Histogram 算法之上,LightGBM 进行进一步的优化。首先它抛弃了大多数 GBDT 工具使用的按层生长 (level-wise) 的决策树生长策略,而使用了带有深度限制的按叶子生长 (leaf-wise) 算法。Level-wise 过一次数据可以同时分裂同一层的叶子,容易进行多线程优化,也好控制模型复杂度,不容易过拟合。但实际上 Level-wise 是一种低效的算法,因为它不加区分的对待同一层的叶子,带来了很多没必要的开销,因为实际上很多叶子的分裂增益较低,没必要进行搜索和分裂。
Leaf-wise 则是一种更为高效的策略,每次从当前所有叶子中,找到分裂增益最大的一个叶子,然后分裂,如此循环。因此同 Level-wise 相比,在分裂次数相同的情况下,Leaf-wise 可以降低更多的误差,得到更好的精度。Leaf-wise 的缺点是可能会长出比较深的决策树,产生过拟合。因此 LightGBM 在 Leaf-wise 之上增加了一个最大深度的限制,在保证高效率的同时防止过拟合。

lightGBM采用了leaf-wise算法的增长策略构建树,减少了很多不必要的计算。XGBoost采用按层(level-wise)生长策略,该策略遍历一次数据可以同时分裂同一层的叶子,虽然这样方便进行多线程优化,控制模型复杂度并且不容易过拟合,但这种策略不加区分低对待同一层的叶子,非常低效。而lightGBM采用的leaf-wise算法每次只对当前分裂增益最大的叶子结点进行分裂。这种方法很容易长出比较深的树从而导致过拟合,所以一般会在树的最大深度上加一个限制。

(简单而言,leaf-wise只分裂一个节点,level分裂一层。另外从名字看,leaf是叶子,表示分裂一个叶节点;level是层,表示分裂一层)

5.直接支持类别特征

lightGBM可以直接处理类别型特征。

6.并行学习

采用优化后的特征并行、数据并行方法加速计算,当数据量非常大的时候还可以采用投票并行的策略。
传统的特征并行主要思想是在并行化决策树中寻找最佳切分点,在数据量大时难以加速,同时需要对切分结果通信整合。
lightgbm使用分散规约(reduce scatter),将直方图合并的任务分给不同机器,降低通信和计算的开销,并利用直方图做加速训练,进一步减少开销。

7.cache 命中率优化

lightGBM对缓存进行了优化,增加了缓存命中率。

总结而言就是

  • 更快的训练速度
  • 更低的内存消耗
  • 准确率相当
  • 支持类别特征,不需要one-hot
  • 都可以自动处理特征缺失值

缺点

  • 可能会长比深的决策树,产生过拟合。
  • lightGBM是基于偏差的算法,所以会对异常值比较敏感。
  • 在寻找最优解的时候,没有将最优解是全部特征的综合这一理念考虑进去。

参数含义

  • boosting_type:‘gbdt’(传统的GBDT模型)、‘dart’、‘goss’、‘rf’(随机森林)
    dart: Dropouts meet Multiple Additive Regression Trees. 在每棵树的迭代过程中不再单单去拟合前一棵树的残差,而是从前面已有的树中采样一部分树,组合成一个新的树,然后去拟合这部分的残差,从而使后面的树贡献变大一些。
    goss:Gradient-based One-Side Sampling,单边梯度采样。目的是丢弃一些对计算增益没有帮助的样本留下有帮助的,降低计算复杂度。

  • num_leaves:基学习器的最大叶子节点数。

  • max_depth: 基学习器的最大树深度。若 ≤ 0 ≤ 0 0则意味着对树深度不加限制,当模型过拟合时,可以考虑首先降低 max_depth

  • learning_rate: 学习率。

  • n_estimators:学习器的个数。

  • subsample_for_bin: 构成bins的样本数。
    lightGBM中使用基于直方图算法的决策树算法。对每个特征进行直方图统计,然后根据直方图的离散值,遍历寻找最优的分割点。

  • objective: ‘binary’、‘multiclass’

  • class_weight: 该参数主要用于多分类任务中;在二分类任务中,可以使用is_unbalance或者scale_pos_weight。

  • colsample_bytree:构造每棵树时特征的抽样比率。

  • reg_alpha: L1正则化项。

  • reg_lambda: L2正则化项。

  • silent: 是否打印每次的运行结果。

  • eval_metric:评价指标。分类任务默认使用’logloss‘。

  • categorical_feature: 类别特征。lightGBM是目前唯一能直接处理类别特征的算法。在lightGBM中类别特征不需要在用one-hot进行转换了。

  • min_data_in_leaf 叶子可能具有的最小记录数 默认20,过拟合时用

  • feature_fraction 例如 为0.8时,意味着在每次迭代中随机选择80%的参数来建树 boosting 为 random forest 时用bagging_fraction 每次迭代时用的数据比例 用于加快训练速度和减小过拟合

  • early_stopping_round 如果一次验证数据的一个度量在最近的early_stopping_round 回合中没有提高,模型将停止训练 加速分析,减少过多迭代

  • lambda 指定正则化 0~1

  • min_gain_to_split 描述分裂的最小 gain 控制树的有用的分裂

  • max_cat_group 在 group 边界上找到分割点 当类别数量很多时,找分割点很容易过拟合时

  • application 模型的用途 选择 regression: 回归时,binary: 二分类时,multiclass: 多分类时
    boosting 要用的算法 gbdt, rf: random forest, dart: Dropouts meet Multiple Additive Regression Trees, goss: Gradient-based One-Side Sampling

  • num_boost_round 迭代次数 通常 100+

  • learning_rate 如果一次验证数据的一个度量在最近的 early_stopping_round 回合中没有提高,模型将停止训练 常用 0.1, 0.001, 0.003…

  • num_leaves 默认 31

  • device cpu 或者 gpu

  • metric mae: mean absolute error , mse: mean squared error , binary_logloss: loss for binary classification , multi_logloss: loss for multi classification
    3.3 IO参数
    IO parameter 含义
    max_bin 表示 feature 将存入的 bin 的最大数量
    categorical_feature 如果 categorical_features = 0,1,2, 则列 0,1,2是 categorical 变量
    ignore_column 与 categorical_features 类似,只不过不是将特定的列视为categorical,而是完全忽略
    save_binary 这个参数为 true 时,则数据集被保存为二进制文件,下次读数据时速度会变快

  • num_leaves 取值应 <= 2 ^(max_depth), 超过此值会导致过拟合

  • min_data_in_leaf 将它设置为较大的值可以避免生长太深的树,但可能会导致 underfitting,在大型数据集时就设置为数百或数千

  • max_depth 这个也是可以限制树的深度
    下表对应了 Faster Speed ,better accuracy ,over-fitting 三种目的时,可以调的参数

Faster Speedbetter accuracyover-fitting
将 max_bin 设置小一些用较大的 max_binmax_bin 小一些
num_leaves 大一些num_leaves 小一些
用 feature_fraction 来做 sub-sampling用 feature_fraction
用 bagging_fraction 和 bagging_freq设定 bagging_fraction 和 bagging_freq
training data 多一些training data 多一些
用 save_binary 来加速数据加载直接用 categorical feature用 gmin_data_in_leaf 和 min_sum_hessian_in_leaf
用 parallel learning用 dart用 lambda_l1, lambda_l2 ,min_gain_to_split 做正则化
num_iterations 大一些,learning_rate 小一些用 max_depth 控制树的深度

LightGBM中的主要调节的参数

针对 Leaf-wise(Best-first)树的参数优化

(1)num_leaves这是控制树模型复杂度的主要参数。理论上, 借鉴 depth-wise 树, 我们可以设置 num_leaves= 但是, 这种简单的转化在实际应用中表现不佳. 这是因为, 当叶子数目相同时, leaf-wise 树要比 depth-wise 树深得多, 这就有可能导致过拟合. 因此, 当我们试着调整 num_leaves 的取值时, 应该让其小于 . 举个例子, 当 max_depth=7时,depth-wise 树可以达到较高的准确率.但是如果设置 num_leaves 为 128 时, 有可能会导致过拟合, 而将其设置为 70 或 80 时可能会得到比 depth-wise 树更高的准确率. 其实, depth 的概念在 leaf-wise 树中并没有多大作用, 因为并不存在一个从 leaves 到 depth 的合理映射。

(2)min_data_in_leaf. 这是处理 leaf-wise 树的过拟合问题中一个非常重要的参数. 它的值取决于训练数据的样本个树和 num_leaves. 将其设置的较大可以避免生成一个过深的树, 但有可能导致欠拟合. 实际应用中, 对于大数据集, 设置其为几百或几千就足够了。

(3)max_depth(默认不限制,一般设置该值为5—10即可) 你也可以利用 max_depth 来显式地限制树的深度。

针对更快的训练速度

(1)通过设置 bagging_fraction 和 bagging_freq 参数来使用 bagging 方法;

(2)通过设置 feature_fraction 参数来使用特征的子抽样;

(3)使用较小的 max_bin;

(4)使用 save_binary 在以后的学习过程对数据进行加速加载。

针对更好的准确率

(1)使用较大的 max_bin (学习速度可能变慢);

(2)使用较小的 learning_rate 和较大的 num_iterations;

(3)使用较大的 num_leaves (可能导致过拟合);

(4)使用更大的训练数据;

(5)尝试 dart(一种在多元Additive回归树种使用dropouts的算法).

处理过拟合

(1)使用较小的 max_bin(默认为255)

(2)使用较小的 num_leaves(默认为31)

(3)使用 min_data_in_leaf(默认为20) 和 min_sum_hessian_in_leaf(默认为)

(4)通过设置 bagging_fraction (默认为1.0)和 bagging_freq (默认为0,意味着禁用bagging,k表示每k次迭代执行一个bagging)来使用 bagging

(5)通过设置 feature_fraction(默认为1.0) 来使用特征子抽样

(6)使用更大的训练数据

(7)使用 lambda_l1(默认为0), lambda_l2 (默认为0)和 min_split_gain(默认为0,表示执行切分的最小增益) 来使用正则

(8)尝试 max_depth 来避免生成过深的树

代码实现

import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.DataFrame
import org.apache.spark.sql.types.{DoubleType, StringType, StructField, StructType, IntegerType}
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.ml.linalg.Vector
import org.apache.spark.ml.feature.VectorAssembler
import org.apache.spark.ml.attribute.Attribute
import org.apache.spark.ml.feature.{IndexToString, StringIndexer}
import com.microsoft.ml.spark.{lightgbm=>lgb}
import com.google.gson.{JsonObject, JsonParser}
import scala.collection.JavaConverters._
 
object LgbDemo extends Serializable {
    
    def printlog(info:String): Unit ={
        val dt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date)
        println("=========="*8+dt)
        println(info+"\n")
    }
    
    def main(args:Array[String]):Unit= {
 
 
    /*================================================================================*/
    //  一,加载数据
    /*================================================================================*/
    printlog("step1: preparing data ...")
 
    //加载数据
    val spark = SparkSession.builder().getOrCreate()
    val dfdata_raw = spark.read.option("header","true")
        .option("delimiter", "\t")
        .option("inferschema", "true")
        .option("nullValue","")
        .csv("data/breast_cancer.csv")
 
    dfdata_raw.sample(false,0.1,1).printSchema 
 
    //将特征组合成features向量
    val feature_cols = dfdata_raw.columns.filter(!Array("label").contains(_)) 
    val cate_cols = Array("mean_radius","mean_texture") 
 
 
    val vectorAssembler = new VectorAssembler().
      setInputCols(feature_cols).
      setOutputCol("features")
 
    val dfdata = vectorAssembler.transform(dfdata_raw).select("features", "label")
    val Array(dftrain,dfval)  = dfdata.randomSplit(Array(0.7, .3), 666)
 
    //各个特征的名字存储在了schema 的 metadata中了, 所以可以用特征名指定类别特征 
    println(dfdata.schema("features").metadata)
    dfdata.show(10) 
 
    /*================================================================================*/
    //  二,定义模型
    /*================================================================================*/
    printlog("step2: defining model ...")
 
    val lgbclassifier = new lgb.LightGBMClassifier()
      .setNumIterations(100)
      .setLearningRate(0.1)
      .setNumLeaves(31)
      .setMinSumHessianInLeaf(0.001)
      .setMaxDepth(-1)
      .setBoostFromAverage(false)
      .setFeatureFraction(1.0)
      .setMaxBin(255)
      .setLambdaL1(0.0)
      .setLambdaL2(0.0)
      .setBaggingFraction(1.0)
      .setBaggingFreq(0)
      .setBaggingSeed(1)
      .setBoostingType("gbdt") //rf、dart、goss
      .setCategoricalSlotNames(cate_cols)
      .setObjective("binary") //binary, multiclass
      .setFeaturesCol("features") 
      .setLabelCol("label")
 
    println(lgbclassifier.explainParams) 
 
 
    /*================================================================================*/
    //  三,训练模型
    /*================================================================================*/
    printlog("step3: training model ...")
 
    val lgbmodel = lgbclassifier.fit(dftrain)
 
    val feature_importances = lgbmodel.getFeatureImportances("gain")
    val arr = feature_cols.zip(feature_importances).sortBy[Double](t=> -t._2)
    val dfimportance = spark.createDataFrame(arr).toDF("feature_name","feature_importance(gain)")
 
    dfimportance.show(100)
 
 
    /*================================================================================*/
    //  四,评估模型
    /*================================================================================*/
    printlog("step4: evaluating model ...")
 
    val evaluator = new BinaryClassificationEvaluator()
      .setLabelCol("label")
      .setRawPredictionCol("rawPrediction")
      .setMetricName("areaUnderROC")
 
    val dftrain_result = lgbmodel.transform(dftrain)
    val dfval_result = lgbmodel.transform(dfval)
 
    val train_auc  = evaluator.evaluate(dftrain_result)
    val val_auc = evaluator.evaluate(dfval_result)
    println(s"train_auc = ${train_auc}")
    println(s"val_auc = ${val_auc}")
 
 
    /*================================================================================*/
    //  五,使用模型
    /*================================================================================*/
    printlog("step5: using model ...")
 
    //批量预测
    val dfpredict = lgbmodel.transform(dfval)
    dfpredict.sample(false,0.1,1).show(20)
 
    //对单个样本进行预测
    val features = dfval.head().getAs[Vector]("features")
    val single_result = lgbmodel.predict(features)
 
    println(single_result)
 
 
    /*================================================================================*/
    //  六,保存模型
    /*================================================================================*/
    printlog("step6: saving model ...")
 
    //保存到集群,多文件
    lgbmodel.write.overwrite().save("lgbmodel.model")
    //加载集群模型
    println("load model ...")
    val lgbmodel_loaded = lgb.LightGBMClassificationModel.load("lgbmodel.model")
    val dfresult = lgbmodel_loaded.transform(dfval)
    dfresult.show() 
 
    //保存到本地,单文件,和Python接口兼容
    //lgbmodel.saveNativeModel("lgb_model",true)
    //加载本地模型
    //val lgbmodel_loaded = LightGBMClassificationModel.loadNativeModelFromFile("lgb_model")
    
    }
    
}
 
 

注意 println(lgbclassifier.explainParams)可以获取LightGBM模型各个参数的含义以及默认值。

参考资料:

1
2
3
4.LightGBM: A Highly Efficient Gradient Boosting
Decision Tree

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值