- 人工智能
- 机器学习
- 分布式训练
- 图计算
- 后端
【免费下载链接】angel
A Flexible and Powerful Parameter Server for large-scale machine learning
本文以 Angel 开源仓库的官方算法文档 gbdt_on_angel.md 为核心骨架,结合 GBDT 源码实现 与单元测试,系统讲解 GBDT(Gradient Boosting Decision Tree,梯度提升决策树)在 Angel 参数服务器架构下的分布式训练原理、梯度直方图存储方案、两阶段分裂算法,以及完整的训练/预测提交命令与调参指南。读完本文,你将掌握如何在 Angel 上提交并调优一个分布式 GBDT 训练任务,并理解其相对 Spark 版 GBDT、MPI 版 XGBoost 的性能优势来源。
1. GBDT 算法核心思想
GBDT 是一种集成学习算法,通过串行地训练多棵决策树(弱分类器),将每棵树的预测结果累加起来,从而不断提升整体模型的分类或回归精度。它在大量分类和回归场景中都有不错的效果。
如下图所示,这是一个对一群消费者的消费力进行预测的例子,其处理流程为:
- 第一棵树(根节点分裂):根节点选取的特征是「年龄」,年龄小于 30 的被分到左子节点,年龄大于 30 的被分到右叶子节点,右叶子节点的预测值为 1;
- 第一棵树(左子节点继续分裂):左一节点继续分裂,分裂特征是「月薪」,月薪小于 10K 划为左叶子节点,预测值为 5;月薪大于 10K 划为右叶子节点,预测值为 10;
- 更新预测值:建立完第一棵树之后,C、D 和 E 的预测值被更新为 1,A 为 5,B 为 10;
- 建立第二棵树:根据新的预测值(残差)开始建立第二棵树,第二棵树的根节点分裂特征是「性别」,女性预测值为 0.5,男性预测值为 1.5;
- 累加预测值:建立完第二棵树之后,将第二棵树的预测值加到每个消费者已有的预测值上。例如 A 的预测值为两棵树预测值之和:
5 + 0.5 = 5.5; - 迭代优化:通过这种方式逐棵树地拟合残差,不断地优化预测准确率。
从源码看,这种"逐棵建树、累加预测"的过程由 GBDTController.java 中的状态机驱动,每个 Worker 维护一棵RegTree森林(RegTree[] forest),建树完成后通过updateInsPreds()将叶子权重乘以学习率累加到每条样本的预测值上(对应源码trainDataStore.preds[insIdx] += this.param.learningRate * weight),与上述算法流程完全一致。
2. GBDT 在 Angel 上的分布式实现
2.1 参数服务器中的参数存储
为了优化算法性能,Angel 将 GBDT 训练过程中反复更新和传递的全部参数以矩阵形式存储在参数服务器(PS)上,主要包括:
- 每个树节点的分裂特征 ID(feature ID)
- 每个树节点的分裂特征值(feature Value)
- 叶子节点的预测值(leaf-prediction)
- 全局一阶梯度直方图(grad histogram)
- 全局二阶梯度直方图(hess histogram)
这些参数矩阵在整个 GBDT 的计算过程中会被反复更新和传递。
在仓库源码 GBDTModel.scala 中,这些参数被落实为 10 个 PS 矩阵(每个矩阵均按 PS 节点数做了列切分,以解决高维模型汇总的单点瓶颈):
| 矩阵名 | 源码常量 | 维度(行 × 列) | 行类型 | 用途 |
|---|---|---|---|---|
| Quantile sketch | gbdt.sketch | 1 × 特征数 × 分裂数 | T_DOUBLE_DENSE | 存储每个特征的候选分裂值(候选分裂点) |
| Sampled feature | gbdt.feature.sample | 树数 × 采样特征数 | T_INT_DENSE | 记录每棵树采样出的特征子集 |
| Grad/Heiss histogram | gbdt.grad.histogram.node{i} | 1 × 2×分裂数×采样特征数 | T_DOUBLE_DENSE | 每个树节点一张,存储全局一阶/二阶梯度直方图 |
| Active tree nodes | gbdt.active.nodes | 1 × 最大节点数 | T_INT_DENSE | 标记哪些树节点处于"待分裂"活跃状态 |
| Split feature | gbdt.split.feature | 树数 × 最大节点数 | T_INT_DENSE | 每个树节点的分裂特征 ID |
| Split value | gbdt.split.value | 树数 × 最大节点数 | T_DOUBLE_DENSE | 每个树节点的分裂特征值 |
| Split gain | gbdt.split.gain | 树数 × 最大节点数 | T_DOUBLE_DENSE | 每个树节点分裂的目标函数增益 |
| Node grad stats | gbdt.node.grad.stats | 树数 × 2×最大节点数 | T_DOUBLE_DENSE | 每个节点的梯度统计(一阶和、二阶和) |
| Node predict | gbdt.node.predict | 树数 × 最大节点数 | T_DOUBLE_DENSE | 叶子节点的预测值 |
| Categorical feature | gbdt.feature.category | Worker 数 × 类别特征数×分裂数 | T_DOUBLE_DENSE | 类别特征的分裂点 |
其中分裂直方图、梯度直方图、活跃节点等中间矩阵均通过.setNeedSave(false)标记为不落盘,仅保留分裂特征、分裂值、节点预测值等最终模型参数。模型定义中还根据indexRange % psNumber对特征数做了向上取整调整,保证参数矩阵能被均匀切分到各个 PS 节点上。
2.2 整体训练流程
GBDT 的训练包含几大步骤:
- 计算候选分裂点(Create sketch):扫描训练数据,对每种特征计算候选分裂特征值,从而得到候选分裂点(分裂特征 + 分裂特征值),常用的方法有 Quantile sketch。Angel 实现中由 TYahooSketchSplit.java 完成,并由领导 Worker(
taskIndex == 0)将全局 sketch 推送到 PS 的gbdt.sketch矩阵上; - 创建决策树(New tree):Worker 创建新的树,进行初始化工作,包括初始化树结构、计算训练数据的一阶和二阶梯度、初始化一个待处理树节点的队列、将树的根节点加入队列。对应源码
createNewTree():新建RegTree、重置活跃节点、把根节点(nid=0)设为活跃、调用calGradPairs()计算梯度; - 寻找最佳分裂点 & 分裂树节点:这是最关键的一步,下文单独展开;
- 计算合并叶子节点的预测值(Finish tree):Worker 计算出叶子节点的预测值并推送给 PS(
updateLeafPreds()写入gbdt.node.predict); - 完成一颗决策树,重新开始第 2 步:直到训练完所有决策树,计算并输出性能指标(准确率、误差等),输出训练模型。
在 GBDTController.java 中,上述步骤被建模为一个状态机:CREATE_SKETCH → GET_SKETCH → SAMPLE_FEATURE → NEW_TREE → RUN_ACTIVE → FIND_SPLIT → AFTER_SPLIT → FINISH_TREE → FINISHED,每一轮通过updatePhase()推进,直到currentTree >= treeNum完成全部树的训练。
2.3 最佳分裂点搜索与两阶段分裂:GBDT 的精髓
如何寻找最佳分裂点并进行分裂,是 GBDT 的精髓和难点,也是参数服务器对它产生重要价值的所在。整体流程为:
- 计算梯度直方图(Run active node):从待处理树节点的队列中取出待处理的树节点,在 Worker 上根据本节点的训练数据计算局部梯度直方图(包括一阶和二阶)。源码中通过
HistCalThread按batchSize分批并行计算,并支持HistSubThread用"父节点直方图 - 兄弟节点直方图"的直方图减法来快速得到另一子节点的直方图(ml.gbdt.hist.subtraction默认开启),大幅减少重复计算; - 同步 & 合并直方图:Worker 通过 PS 接口将局部梯度直方图推送到参数服务器。在发送之前,每个局部梯度直方图被切分为 P 个分块(P 为参数服务器节点个数),每个分块分别发送到对应的参数服务器节点;PS 节点接收到 Worker 发送的局部梯度直方图后,确定处理的树节点,将其累加到对应的全局梯度直方图上。源码中每个节点直方图独立存放在
gbdt.grad.histogram.node{nid}矩阵中,按2 * splitNum * sampleFeatNum / psNumber做列切分; - 寻找最佳分裂点(Find split):Worker 使用参数服务器提供的计算最佳分裂点的接口,从参数服务器获取每个参数服务器节点上的最佳分裂点,然后比较 P 个分裂点的目标函数增益,选取增益最大的分裂点作为全局最佳分裂点。源码中,多个活跃树节点在各 Task 间采用 Round-Robin 分配(
taskContext.getTaskIndex() == activeTNodeNum),保证分裂计算并行均衡; - 分裂树节点(After split):Worker 根据计算得到的最佳分裂点创建叶子节点,将本节点的训练数据切分到两个叶子节点上(
updateTrainInsPos()使用快速划分原地重排样本区间);如果树的高度没有达到最大限制,则将两个叶子节点加入到待处理树节点的队列。
其中两阶段分裂算法(two-phase tree splitting,ml.gbdt.server.split)是关键优化:当开关开启时,Worker 不再把整棵直方图拉回本地计算,而是通过 PS 上的 PSF(Parameter Server Function,参数服务器函数)GBDTGradHistGetRowFunc直接在 PS 端并行扫描直方图、找到每个分块内的局部最佳分裂点,再把 P 个局部最佳分裂点(而不是庞大的直方图)返回给 Worker,Worker 只需比较增益即可选出全局最佳分裂点。对应源码位于 GBDTGradHistGetRowFunc.java 与findSplit()中isServerSplit分支。
从上面的算法逻辑剖析可以看出,GBDT 算法存在大量的模型更新和同步操作,非常适合参数服务器的系统架构,具体体现在:
- 超大模型:GBDT 用到的梯度直方图的大小与特征数量成正比,对于高维大数据集,梯度直方图会非常大。Angel 将梯度直方图切分到多个 PS 节点上存储,有效解决了高维度模型在汇总参数时的单点瓶颈问题;
- 两阶段树分裂算法:在寻找最佳分裂点时,在多个 PS 节点上并行处理,只需要将局部最佳分裂点返回给 Worker,通信开销几乎可以忽略不计。
整体来看,Angel 的 PS 优势使得它在该算法上的性能远超 Spark 版本的 GBDT 实现,也显著优于 MPI 版本的 XGBoost。
3. 运行 GBDT:输入格式与参数详解
3.1 输入格式
ml.feature.index.range:特征向量的维度;ml.data.type:支持dummy、libsvm两种数据格式,具体参考 Angel 数据格式说明。仓库内置的 GBDT 单元测试 GBDTTest.java 使用agaricus_127d_train.libsvm(127 维特征)作为训练数据、agaricus_127d_test.libsvm作为预测数据,该数据已随仓库提供在 data/agaricus 目录下,可直接用于本地复现。
3.2 算法参数
| 参数 | 含义 | 默认值(源码 MLConf) | 说明 |
|---|---|---|---|
ml.gbdt.tree.num | 树的数量 | 10 | 默认 10,增大可提升精度但增加训练时间 |
ml.gbdt.tree.depth | 树的最大高度 | 5 | 默认 5,深度越大模型越复杂 |
ml.gbdt.split.num | 每个特征的分裂点的数量 | 10 | 决定候选分裂点个数,也决定直方图长度 |
ml.learn.rate | 学习速率 | 0.5 | 每棵树预测值的缩放系数,一般取 0.01~0.2 |
ml.data.validate.ratio | 每次 validation 的样本比率,设为 0 时不做 validation | 0.05 | 训练过程中按比例切分验证集 |
ml.gbdt.sample.ratio | 特征下采样的比率 | 1 | 默认 1 表示使用全部特征 |
ml.gbdt.server.split | 两阶段分裂算法开关 | false | 开启后在 PS 端并行寻找最佳分裂点 |
ml.gbdt.batch.size | 并行训练时一个批量的数量 | 10000 | 控制直方图计算时分批粒度 |
angel.compress.bytes | 低精度压缩,每个浮点数的大小 | 8 | 可设为 [1,8],如设为 2 即用 2 字节量化梯度再传输 |
补充说明(来自源码 MLConf.scala 与 GBDTParam.java):ml.gbdt.task.type可设为classification(默认,使用二元逻辑损失binary:logistic)或regression(使用平方损失),回归任务对应的评估指标为 RMSE;ml.gbdt.cate.feat用于声明类别特征;另外还有ml.gbdt.reg.alpha/ml.gbdt.reg.lambda(分裂增益的正则项)、ml.gbdt.min.child.weight(子节点最小二阶梯度权重和)、ml.gbdt.max.node.num(最大节点数)、ml.gbdt.feature.sample.ratio等扩展参数可供精细调优。当angel.compress.bytes < 1 || > 8时,源码会判定为非法配置并回退到默认值 8。
3.3 输入输出参数
| 参数 | 含义 |
|---|---|
angel.train.data.path | 训练数据的输入路径 |
angel.predict.data.path | 预测数据的输入路径 |
ml.gbdt.cate.feat | 类别特征,"特征id:特征范围"的格式,以逗号分隔,例如"0:2,1:3"。设为none表示没有离散特征,设为all表示全部为离散特征 |
ml.model.type | 模型类型,默认为T_FLOAT_DENSE |
angel.save.model.path | 训练完成后模型的保存路径 |
angel.predict.out.path | 预测结果的保存路径 |
angel.log.path | 日志文件的保存路径 |
3.4 资源参数
| 参数 | 含义 | 默认值 |
|---|---|---|
angel.workergroup.number | Worker 个数 | - |
angel.worker.memory.gb | Worker 申请内存大小(单位 GB) | - |
angel.worker.task.number | 每个 Worker 上的 task 的个数 | 1 |
angel.ps.number | PS 个数 | 1 |
angel.ps.memory.gb | PS 申请内存大小(单位 GB) | - |
3.5 训练任务启动命令示例
angel-submit \ -Dangel.am.log.level=INFO \ -Dangel.ps.log.level=INFO \ -Dangel.worker.log.level=INFO \ -Dangel.app.submit.class=com.tencent.angel.ml.GBDT.GBDTRunner \ -Daction.type=train \ -Dml.data.type=libsvm \ -Dml.model.type=T_FLOAT_DENSE \ -Dml.data.validate.ratio=0.1 \ -Dml.feature.index.range=10000 \ -Dml.gbdt.cate.feat=none \ -Dml.gbdt.tree.num=20 \ -Dml.gbdt.tree.depth=7 \ -Dml.gbdt.split.num=10 \ -Dml.gbdt.sample.ratio=1.0 \ -Dml.learn.rate=0.01 \ -Dml.gbdt.server.split=true \ -Dangel.compress.bytes=2 \ -Dangel.train.data.path=$input_path \ -Dangel.save.model.path=$model_path \ -Dangel.workergroup.number=50 \ -Dangel.worker.memory.gb=10 \ -Dangel.task.data.storage.level=memory \ -Dangel.worker.task.number=1 \ -Dangel.ps.number=50 \ -Dangel.ps.memory.gb=103.6 预测任务启动命令示例
angel-submit \ -Dangel.am.log.level=INFO \ -Dangel.ps.log.level=INFO \ -Dangel.worker.log.level=INFO \ -Dangel.app.submit.class=com.tencent.angel.ml.GBDT.GBDTRunner \ -Daction.type=predict \ -Dml.data.type=libsvm \ -Dml.model.type=T_FLOAT_DENSE \ -Dml.data.validate.ratio=0.1 \ -Dml.feature.index.range=10000 \ -Dml.gbdt.tree.num=20 \ -Dml.gbdt.tree.depth=7 \ -Dml.gbdt.sample.ratio=1.0 \ -Dml.learn.rate=0.01 \ -Dangel.predict.data.path=$input_path \ -Dangel.save.model.path=$model_path \ -Dangel.predict.out.path=$predict_path \ -Dangel.workergroup.number=50 \ -Dangel.worker.memory.gb=10 \ -Dangel.task.data.storage.level=memory \ -Dangel.worker.task.number=1 \ -Dangel.ps.number=50 \ -Dangel.ps.memory.gb=103.7 任务执行链路(源码视角)
训练与预测任务的入口为 GBDTRunner.scala。训练模式下,Runner 依次执行:创建 AngelClient →startPSServer()启动参数服务器 →loadModel()加载(或新建)GBDT 模型 →runTask(classOf[GBDTTrainTask])在 Worker 上拉起训练 Task →waitForCompletion()等待训练完成 →saveModel()保存模型 →stop()释放集群资源。预测模式则额外将矩阵传输超时(angel.worker.matrix.transfer.request.timeout.ms)调大至 60000ms 后执行runTask(classOf[GBDTPredictTask])。预测时每个样本遍历全部树,从根节点按x.get(splitFeat) <= splitValue走向左右子树,最终把所有叶节点预测值乘以学习率累加(见GBDTModel.predict())。
对于希望快速本地验证的读者,可直接运行仓库中的 GBDT 单元测试 GBDTTest.java,它以 LOCAL 模式在单机单 Worker 单 PS 上完成训练与预测的端到端验证,数据使用仓库自带的 agaricus 蘑菇数据集。
4. 性能评测:Angel vs XGBoost
以下评测数据来自原文档(基于腾讯内部数据集,在腾讯线上 Gaia 集群(YARN)上完成对比)。
训练数据
| 数据集 | 数据集大小 | 数据数量 | 特征数量 | 任务 |
|---|---|---|---|---|
| UserGender1 | 24GB | 1250 万 | 2570 | 二分类 |
| UserGender2 | 145GB | 1.2 亿 | 33 万 | 二分类 |
实验目的是预测用户的性别。数据集 UserGender1 大小为 24GB,包含 1250 万个训练数据,每个训练数据的特征维度为 2570;数据集 UserGender2 大小为 145GB,包含 1.2 亿个训练数据,每个训练数据的特征维度为 33 万。两个数据集都是高维稀疏数据集。
实验环境
实验所使用的集群是腾讯的线上 Gaia 集群(YARN),单台机器的配置为:
- CPU:2680 × 2
- 内存:256 GB
- 网络:10G × 2
- 磁盘:4T × 12(SATA)
参数配置
Angel 和 XGBoost 使用如下的参数配置:
- 树的数量:20
- 树的最大高度:7
- 梯度直方图大小:10
- 学习速度:0.1(XGBoost)、0.2(Angel)
- 工作节点数量:50
- 参数服务器数量:10
- 每个工作节点内存:2GB(UserGender1)、10GB(UserGender2)
实验结果
| 系统 | 数据集 | 训练总时间 | 每棵树时间 | 测试集误差 |
|---|---|---|---|---|
| XGBoost | UserGender1 | 36min 48s | 110s | 0.155008 |
| Angel | UserGender1 | 25min 22s | 76s | 0.154160 |
| XGBoost | UserGender2 | 2h 25min | 435s | 0.232039 |
| Angel | UserGender2 | 58min 39s | 175s | 0.243316 |
需要说明的是:上述结果来自该算法文档记录的特定评测实验(数据集、集群与参数均如上),用于说明 Angel 参数服务器架构在高维大数据集上的扩展性优势,读者在自己的环境复现时应以实测为准。
5. 总结与进一步阅读
GBDT 的训练过程天然包含"大模型存储 + 高频梯度同步 + 并行分裂搜索",这与 Angel 参数服务器架构高度契合。通过将梯度直方图按 PS 节点切分存储、在两阶段分裂模式下让 PS 端并行计算局部最佳分裂点,Angel 有效消除了高维特征下直方图聚合与分裂搜索的瓶颈,实现了对 Spark 版 GBDT 与 MPI 版 XGBoost 的性能超越。
如需进一步深入,建议阅读仓库内的以下资源:
- 算法官方文档:gbdt_on_angel.md、gbdt_on_angel_en.md
- 分布式实现核心:GBDTController.java、GBDTModel.scala
- 任务入口与 Runner:GBDTRunner.scala、GBDTTrainTask.scala、GBDTPredictTask.scala
- 配置定义:MLConf.scala、GBDTParam.java
- 单元测试与本地复现:GBDTTest.java、GBDTLocalExample.java
- 数据格式:data_format.md
参考文献
- Jiawei Jiang, Bin Cui, Ce Zhang and Fangcheng Fu. DimBoost: Boosting Gradient Boosting Decision Tree to Higher Dimensions. SIGMOD, 2018.
- Tianqi Chen and Carlos Guestrin. XGBoost: A Scalable Tree Boosting System. KDD, 2016.
- Michael Greenwald and Sanjeev Khanna. Space-efficient Online Computation of Quantile Summaries. SIGMOD, 2001.
- 人工智能
- 机器学习
- 分布式训练
- 图计算
- 后端
【免费下载链接】angel
A Flexible and Powerful Parameter Server for large-scale machine learning
相关推荐
MMPose全身姿态估计133关键点:一次讲清怎么用
MMPose全身姿态估计133关键点:一次讲清怎么用 标准人体姿态估计只给 17 个关节,指尖、五官、脚趾一概看不到。MMPose 的 WholeBody 全身
计算机视觉人工智能深度学习3分钟掌握cargo-zigbuild:Rust跨平台编译的完整指南
3分钟掌握cargo zigbuild:Rust跨平台编译的完整指南 cargo zigbuild是一个革命性的Rust构建工具,它通过集成Zig编译器作为链接
开发工具TensorFlow-Examples中的梯度提升决策树(GBDT)实现解析
TensorFlow Examples中的梯度提升决策树 GBDT 实现解析 什么是梯度提升决策树 GBDT 梯度提升决策树 Gradient Boosted
示例工程机器学习
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考