XGBoost的并行不是树粒度的而是特征粒度的,随机森林就是树粒度的并行。
寻找分裂点的时候,算法中先是遍历所有特征再遍历每个特征下的所有值。
遍历特征下所有值时要求值是排序好的,这样就可以使用差加速。
如果不排序,那么计算分类时候的损失函数减少量就没法达到O(1)的复杂度,因为二叉树的分裂是> x,分到a子树这样的形式。
在建树的过程中,最耗时是找最优的切分点,而这个过程中,最耗时的部分是将数据排序。为了减少排序的时间,提出Block结构存储数据。
- Block中的数据以稀疏格式CSC进行存储
- Block中的特征进行排序(不对缺失值排序,排序只有一次)
- Block 中特征还需存储指向样本的索引,这样才能根据特征的值来取梯度。
- 一个Block中存储一个或多个特征的值
个人理解是,这个block是原样本的一种映射,在这个block里,"样本"是按照列存储的,其实他存储的是列,而不是样本。因为样本是按照行来组织的,block中存储的是原样本的各个排序后的列。
所以就要有列中的每个元素与原样本之间的映射关系,因为在分裂节点的选择时,不仅要遍历某个特征(即列)中的所有元素,还要用到原样本的梯度(一阶导和二阶导), 所以就要通过列中的元素找到原样本。
按照block存储的好处就是,不同列之间可以并行查找,并且因为预排序了所以使得分裂节点查找时更快。坏处是空间大了一倍。
缓存优化
在分块并行中,block存储了排序的列,并建立和原来样本的一一映射,这样可以通过索来找到原始样本获得梯度,但是原始样本是存放是按照列值的原始顺序的(相邻内存的样本他们对应的列值可能不是连续的,而我们现在根据排序后的列值来找原样本,那么肯定会出现在内存上的跳跃式查找,就非连续内存访问,可能导致CPU cache命中率降低。
CPU cache命中率低对于加权分位数选择分裂点的方法没太大影响,因为其选择分裂点的时候本来就是跳跃着选的,但是对于精确贪心算法的效率影响就非常大了,因为其要遍历所有样本。
下图中,calculate 上下两个部分表示连续列值上计算G和H但是其对应的样本不连续。
红色字说明了连续列值对应的样本不连续性。
原论文中说
A naive implementation of split enumeration introduces immediate read/write dependency between the accumulation and the non-continuous memory fetch operation
通过降低读写的依赖性来解决cache miss的问题
1.对于精确贪心算法,对每个线程分配一个连续的缓存空间,预取接下来要读取的数据,这样就降低了直接从内存读取并且cache miss消耗的时间。
2. 对于近似分割算法,选取适当的block大小即可(2^16 * each_sample_size)
参考资料
https://www.hrwhisper.me/machine-learning-xgboost/
https://arxiv.org/pdf/1603.02754.pdf