二、xgboost

  1. xgboost 也是使用与提升树相同的前向分步算法。其区别在于:xgboost 通过结构风险极小化来确定下一个决策树的参数 二、xgboost - 图1

    二、xgboost - 图2

    其中:

    • 二、xgboost - 图3 为第 二、xgboost - 图4 个决策树的正则化项。这是xgboostGBT的一个重要区别。
    • 二、xgboost - 图5 为目标函数。
  2. 定义:

    二、xgboost - 图6

    即:

    • 二、xgboost - 图7二、xgboost - 图8二、xgboost - 图9 的一阶导数。
    • 二、xgboost - 图10二、xgboost - 图11二、xgboost - 图12 的二阶导数。

    对目标函数 二、xgboost - 图13 执行二阶泰勒展开:

    二、xgboost - 图14

    提升树模型只采用一阶泰勒展开。这也是xgboostGBT的另一个重要区别。

  3. 对一个决策树 二、xgboost - 图15 ,假设不考虑复杂的推导过程,仅考虑决策树的效果:

    • 给定输入 二、xgboost - 图16,该决策树将该输入经过不断的划分,最终划分到某个叶结点上去。
    • 给定一个叶结点,该叶结点有一个输出值。

    因此将决策树拆分成结构部分 二、xgboost - 图17,和叶结点权重部分 二、xgboost - 图18 ,其中 二、xgboost - 图19 为叶结点的数量。

    • 结构部分 二、xgboost - 图20 的输出是叶结点编号 二、xgboost - 图21 。它的作用是将输入 二、xgboost - 图22 映射到编号为 二、xgboost - 图23 的叶结点。
    • 叶结点权重部分就是每个叶结点的值。它的作用是输出编号为 二、xgboost - 图24 的叶结点的值 二、xgboost - 图25

    因此决策树改写为: 二、xgboost - 图26

2.1 结构分

  1. 定义一个决策树的复杂度为:二、xgboost - 图27

    其中:二、xgboost - 图28 为叶结点的个数;二、xgboost - 图29 为每个叶结点的输出值;二、xgboost - 图30 为系数,控制这两个部分的比重。

    • 叶结点越多,则决策树越复杂。
    • 每个叶结点输出值的绝对值越大,则决策树越复杂。

    该复杂度是一个经验公式。事实上还有很多其他的定义复杂度的方式,只是这个公式效果还不错。

  2. 将树的拆分、树的复杂度代入 二、xgboost - 图31 的二阶泰勒展开,有:

    二、xgboost - 图32

    对于每个样本 二、xgboost - 图33,它必然被划分到树 二、xgboost - 图34 的某个叶结点。定义划分到叶结点 二、xgboost - 图35 的样本的集合为:二、xgboost - 图36 。则有:

    二、xgboost - 图37

  3. 定义 :二、xgboost - 图38

    • 二、xgboost - 图39 刻画了隶属于叶结点 二、xgboost - 图40 的那些样本的一阶偏导数之和。
    • 二、xgboost - 图41 刻画了隶属于叶结点 二、xgboost - 图42 的那些样本的二阶偏导数之和。

    偏导数是损失函数 二、xgboost - 图43 关于当前模型的输出 二、xgboost - 图44 的偏导数。

    则上式化简为:二、xgboost - 图45

    假设 二、xgboost - 图46 与 与 二、xgboost - 图47 无关,对 二、xgboost - 图48 求导等于0,则得到:二、xgboost - 图49

    忽略常数项,于是定义目标函数为:

    二、xgboost - 图50

  4. 在推导过程中假设 二、xgboost - 图51 与 与 二、xgboost - 图52 无关,这其实假设已知树的结构。

    事实上 二、xgboost - 图53 是与 二、xgboost - 图54 相关的,甚至与树的结构相关,因此定义 二、xgboost - 图55 为结构分。

    结构分刻画了:当已知树的结构时目标函数的最小值。

2.2 分解结点

  1. 现在的问题是:如何得到最佳的树的结构,从而使得目标函数全局最小。

2.2.1 贪心算法

  1. 第一种方法是对现有的叶结点加入一个分裂,然后考虑分裂之后目标函数降低多少。

    • 如果目标函数下降,则说明可以分裂。
    • 如果目标函数不下降,则说明该叶结点不宜分裂。
  2. 对于一个叶结点,假如给定其分裂点,定义划分到左子结点的样本的集合为:二、xgboost - 图56 ;定义划分到右子结点的样本的集合为:二、xgboost - 图57 。则有:

    二、xgboost - 图58

  3. 定义叶结点的分裂增益为:

    二、xgboost - 图59

    其中:

    • 二、xgboost - 图60 表示:该叶结点的左子树的结构分。
    • 二、xgboost - 图61 表示:该叶结点的右子树的结构分。
    • 二、xgboost - 图62 表示:如果不分裂,则该叶结点本身的结构分。
    • 二、xgboost - 图63 表示:因为分裂导致叶结点数量增大1,从而导致增益的下降。

    每次分裂只一个叶结点,因此其它叶结点不会发生变化。因此:

    • 二、xgboost - 图64 ,则该叶结点应该分裂。
    • 二、xgboost - 图65,则该叶结点不宜分裂。
  4. 现在的问题是:不知道分裂点。对于每个叶结点,存在很多个分裂点,且可能很多分裂点都能带来增益。

    解决的办法是:对于叶结点中的所有可能的分裂点进行一次扫描。然后计算每个分裂点的增益,选取增益最大的分裂点作为本叶结点的最优分裂点。

  5. 最优分裂点贪心算法:

    • 输入:

      • 数据集 二、xgboost - 图66 ,其中样本 二、xgboost - 图67
      • 属于当前叶结点的样本集的下标集合 二、xgboost - 图68
    • 输出:当前叶结点最佳分裂点。

    • 算法:

      • 初始化:二、xgboost - 图69

      • 遍历各维度:二、xgboost - 图70

        • 初始化: 二、xgboost - 图71

        • 遍历各拆分点:沿着第 二、xgboost - 图72 维 :

          • 如果第 二、xgboost - 图73 维特征为连续值,则将当前叶结点中的样本从小到大排序。然后用 二、xgboost - 图74 顺序遍历排序后的样本下标:

            二、xgboost - 图75

          • 如果第 二、xgboost - 图76 维特征为离散值 二、xgboost - 图77 ,设当前叶结点中第 二、xgboost - 图78 维取值 二、xgboost - 图79 样本的下标集合为 二、xgboost - 图80 ,则遍历 二、xgboost - 图81

            二、xgboost - 图82

      • 选取最大的 二、xgboost - 图83 对应的维度和拆分点作为最优拆分点。

  6. 分裂点贪心算法尝试所有特征和所有分裂位置,从而求得最优分裂点。

    当样本太大且特征为连续值时,这种暴力做法的计算量太大。

2.2.2 近似算法

  1. 近似算法寻找最优分裂点时不会枚举所有的特征值,而是对特征值进行聚合统计,然后形成若干个桶。

    然后仅仅将桶边界上的特征的值作为分裂点的候选,从而获取计算性能的提升。

  2. 假设数据集 二、xgboost - 图84 ,样本 二、xgboost - 图85

    对第 二、xgboost - 图86 个特征进行分桶:

    • 如果第 二、xgboost - 图87 个特征为连续特征,则执行百分位分桶,得到分桶的区间为:二、xgboost - 图88 ,其中 二、xgboost - 图89

      分桶的数量、分桶的区间都是超参数,需要仔细挑选。

    • 如果第 二、xgboost - 图90 个特征为离散特征,则执行按离散值分桶,得到的分桶为:二、xgboost - 图91 ,其中 二、xgboost - 图92 为第 二、xgboost - 图93 个特征的所有可能的离散值。

      分桶的数量 二、xgboost - 图94 就是所有样本在第 二、xgboost - 图95 个特征上的取值的数量。

  3. 最优分裂点近似算法:

    • 输入:

      • 数据集 二、xgboost - 图96 ,其中样本 二、xgboost - 图97
      • 属于当前叶结点的样本集的下标集合 二、xgboost - 图98
    • 输出:当前叶结点最佳分裂点。

    • 算法:

      • 对每个特征进行分桶。 假设对第 二、xgboost - 图99 个特征上的值进行分桶为:二、xgboost - 图100

        如果第 二、xgboost - 图101 个特征为连续特征,则要求满足 二、xgboost - 图102

      • 初始化:二、xgboost - 图103

      • 遍历各维度:二、xgboost - 图104

        • 初始化: 二、xgboost - 图105

        • 遍历各拆分点,即遍历 二、xgboost - 图106

          • 如果是连续特征,则设叶结点的样本中,第 二、xgboost - 图107 个特征取值在区间 二、xgboost - 图108 的样本的下标集合为 二、xgboost - 图109,则:

            二、xgboost - 图110

          • 如果是离散特征,则设叶结点的样本中,第 二、xgboost - 图111 个特征取值等于 二、xgboost - 图112 的样本的下标集合为 二、xgboost - 图113 ,则:

            二、xgboost - 图114

        • 选取最大的 二、xgboost - 图115 对应的维度和拆分点作为最优拆分点。

  4. 分桶有两种模式:

    • 全局模式:在算法开始时,对每个维度分桶一次,后续的分裂都依赖于该分桶并不再更新。

      • 优点是:只需要计算一次,不需要重复计算。
      • 缺点是:在经过多次分裂之后,叶结点的样本有可能在很多全局桶中是空的。
    • 局部模式:除了在算法开始时进行分桶,每次拆分之后再重新分桶。

      • 优点是:每次分桶都能保证各桶中的样本数量都是均匀的。
      • 缺点是:计算量较大。

    全局模式会构造更多的候选拆分点。而局部模式会更适合构建更深的树。

  5. 分桶时的桶区间间隔大小是个重要的参数。

    区间间隔越小,则桶越多,则划分的越精细,候选的拆分点就越多。

2.3 加权分桶

  1. 假设候选样本的第 二、xgboost - 图116 维特征,及候选样本的损失函数的二阶偏导数为:

    二、xgboost - 图117

    定义排序函数:

    二、xgboost - 图118

    它刻画的是:第 二、xgboost - 图119 维小于 二、xgboost - 图120 的样本的 二、xgboost - 图121 之和,占总的 二、xgboost - 图122 之和的比例。

  2. xgboost 的作者提出了一种带权重的桶划分算法。定义候选样本的下标集合为 二、xgboost - 图123 ,拆分点 二、xgboost - 图124 定义为:

    二、xgboost - 图125

    其中 二、xgboost - 图126 表示样本 二、xgboost - 图127 的第 二、xgboost - 图128 个特征。即:

    • 最小的拆分点是所有样本第 二、xgboost - 图129 维的最小值。

    • 最大的拆分点是所有样本第 二、xgboost - 图130 维的最大值。

    • 中间的拆分点:选取拆分点,使得相邻拆分点的排序函数值小于 二、xgboost - 图131 (分桶的桶宽)。

      • 其意义为:第 二、xgboost - 图132 维大于等于 二、xgboost - 图133 ,小于 二、xgboost - 图134 的样本的 二、xgboost - 图135 之和,占总的 二、xgboost - 图136 之和的比例小于 二、xgboost - 图137
      • 这种拆分点使得每个桶内的以 二、xgboost - 图138 为权重的样本数量比较均匀,而不是样本个数比较均匀。
  3. 上述拆分的一个理由是:根据损失函数的二阶泰勒展开有:

    二、xgboost - 图139

    对于第 二、xgboost - 图140 个决策树,它等价于样本 二、xgboost - 图141 的真实标记为 二、xgboost - 图142、权重为 二、xgboost - 图143、损失函数为平方损失函数。因此分桶时每个桶的权重为 二、xgboost - 图144

2.4 缺失值

  1. 真实场景中,有很多可能导致产生稀疏。如:数据缺失、某个特征上出现很多 0 项、人工进行 one-hot 编码导致的大量的 0。

    • 理论上,数据缺失和数值0的含义是不同的,数值 0 是有效的。

    • 实际上,数值0的处理方式类似缺失值的处理方式,都视为稀疏特征。

      xgboost 中,数值0的处理方式和缺失值的处理方式是统一的。这只是一个计算上的优化,用于加速对稀疏特征的处理速度。

    • 对于稀疏特征,只需要对有效值进行处理,无效值则采用默认的分裂方向。

      注意:每个结点的默认分裂方向可能不同。

  2. xgboost 算法的实现中,允许对数值0进行不同的处理。可以将数值0视作缺失值,也可以将其视作有效值。

    如果数值0是有真实意义的,则建议将其视作有效值。

  3. 缺失值处理算法:

    • 输入:

      • 数据集 二、xgboost - 图145 ,其中样本 二、xgboost - 图146
      • 属于当前叶结点的样本的下标集合 二、xgboost - 图147
      • 属于当前叶结点,且第 二、xgboost - 图148 维特征有效的样本的下标集合 二、xgboost - 图149
    • 输出:当前叶结点最佳分裂点。

    • 算法:

      • 初始化:二、xgboost - 图150

      • 遍历各维度:二、xgboost - 图151

        • 先从左边开始遍历:

          • 初始化: 二、xgboost - 图152

          • 遍历各拆分点:沿着第 二、xgboost - 图153 维,将当前有效的叶结点的样本从小到大排序。

            这相当于所有无效特征值的样本放在最右侧,因此可以保证无效的特征值都在右子树。

            然后用 二、xgboost - 图154 顺序遍历排序后的样本下标:

          二、xgboost - 图155

        • 再从右边开始遍历:

          • 初始化: 二、xgboost - 图156

          • 遍历各拆分点:沿着 二、xgboost - 图157 维,将当前叶结点的样本从大到小排序。

            这相当于所有无效特征值的样本放在最左侧,因此可以保证无效的特征值都在左子树。

            然后用 二、xgboost - 图158 逆序遍历排序后的样本下标:

          二、xgboost - 图159

      • 选取最大的 二、xgboost - 图160 对应的维度和拆分点作为最优拆分点。

  4. 缺失值处理算法中,通过两轮遍历可以确保稀疏值位于左子树和右子树的情形。

2.5 其他优化

2.5.1 正则化

  1. xgboost 在学习过程中使用了如下的正则化策略来缓解过拟合:

    • 通过学习率 二、xgboost - 图161 来更新模型:二、xgboost - 图162
    • 类似于随机森林,采取随机属性选择。

2.5.2 计算速度提升

  1. xgboost 在以下方面提出改进来提升计算速度:

    • 预排序pre-sorted
    • cache-aware 预取。
    • Out-of-Core 大数据集。
2.5.2.1 预排序
  1. xgboost 提出column block 数据结构来降低排序时间。

    • 每一个block 代表一个属性,样本在该block 中按照它在该属性的值排好序。
    • 这些block 只需要在程序开始的时候计算一次,后续排序只需要线性扫描这些block 即可。
    • 由于属性之间是独立的,因此在每个维度寻找划分点可以并行计算。
  2. block 可以仅存放样本的索引,而不是样本本身,这样节省了大量的存储空间。

    如:block_1 代表所有样本在feature_1 上的从小到大排序:sample_no1,sample_no2,....

    其中样本编号出现的位置代表了该样本的排序。

2.5.2.2 预取
  1. 由于在column block 中,样本的顺序会被打乱,这会使得从导数数组中获取 二、xgboost - 图163 时的缓存命中率较低。

    因此xgboost 提出了cache-aware 预取算法,用于提升缓存命中率。

  2. xgboost 会以minibatch 的方式累加数据,然后在后台开启一个线程来加载需要用到的导数 二、xgboost - 图164

    这里有个折中:minibatch 太大,则会引起cache miss ;太小,则并行程度较低。

2.5.2.3 Out-of-Core
  1. xgboost 利用硬盘来处理超过内存容量的大数据集。其中使用了下列技术:

    • 使用block 压缩技术来缓解内存和硬盘的数据交换IO : 数据按列压缩,并且在硬盘到内存的传输过程中被自动解压缩。
    • 数据随机分片到多个硬盘,每个硬盘对应一个预取线程,从而加大”内存-硬盘”交换数据的吞吐量。