米开朗基罗优步的机器学习(ML)平台,支持在优步的各种用例中进行机器学习模型训练,例如预测乘客需求,欺诈检测,优步外卖的食物发现和推荐,并不断改进估计到达时间的准确性.
随着米开朗基罗越来越深的树模型创建更大的数据集,分布式的高效训练梯度增加(GBD)算法变得越来越具有挑战性。为了防止优步的培训延迟,我们利用Apache Spark MLlib和分布式XGBoost的有效的基于全减少的实现促进更高效和可扩展的并行树构建和大数据集的外核计算。
在本文中,我们将分享在生产和扩展XGBoost以使用Uber数据存储中的数十亿条记录来训练深度(深度16+)模型树时遇到的一些技术挑战和经验教训。通过使用XGBoost对大型训练数据集进行深度树抽样分层,我们在平台上的多个用例(包括ETA估计)上的模型性能上取得了显著的进展,从而改善了整体的用户体验。
在Uber使用Apache Spark和XGBoost for ML
典型的XGBoost训练工作流包括两个阶段:清理和处理原始数据,以便在ML模型评估期间使用。上面的图1显示了一个典型的XGBoost训练工作流,分为特征转换和交叉验证拆分两个阶段。
在特征转换阶段,我们通常执行在一个炎热的编码或者使用我们内部的batch实现StringIndexer在一系列特征转换和imputation阶段之前,将分类列转换为索引列。然后将转换后的特征传递给XGBoost教练.评估框架加载生成的XGBoost模型和特性,以计算用户指定的特性重要性和自定义模型性能指标。
米开朗基罗, Uber的ML平台,使用Spark ML管道表示训练和持久化模型在生产培训和服务方面。这种表示包括对XGBoost的估计器和转换器的支持。Michelangelo支持使用XGBoost的不同版本(包括0.6到0.9版本)训练的模型。
我们调优Spark MLlib SerDe用于低延迟模型加载,扩展Spark transformer用于在线服务api,并创建自定义Estimators和transformer以每秒高查询(QPS)在线加载和服务XGBoost模型。这种低延迟-高QPS支持的结合满足了生产XGBoost模型的核心需求,并导致更快的模型评估。开源社区可以通过Apache Spark JIRA请求获得这些更改,无spark MLLib在线服务的SPIP - ML模型扩展.
在为大型系统生产XGBoost时,有几个因素需要考虑。通过我们自己使用米开朗基罗的经验,我们确定了一些其他团队在大规模使用XGBoost时可能会发现有用的一般最佳实践:
确保SparseVector和DenseVector使用一致
在Apache Spark中,有两种向量存储结构:
- SparseVector是一种稀疏表示,它只保留非零元素而忽略所有零元素。SparseVector适用于许多ML用例,其中数据集包含高比例的零元素。
- DenseVector表示将每个值按顺序存储在vector中,如下图2所示:
SparseVectors中的零值被Apache Spark上的XGBoost视为缺失值(默认为Float.NaN),而DenseVectors中的零则被简单地视为零。向量存储在Apache Spark ML是隐式优化,因此矢量数组根据空间效率存储为SparseVector或DenseVector。如果ML从业者试图在推理时向在SparseVector上训练的模型提供DenseVector,反之亦然,XGBoost不提供任何警告由于零的存储方式,预测输入可能会进入意想不到的分支,从而导致不一致的预测。因此,在服务时间和培训时间之间,存储结构输入保持一致是至关重要的。
度量模型特性的重要性
基于Java虚拟机(Java Virtual Machine, JVM)的XGBoost 0.81版本和更老版本的软件迭代计算特定特性的重要性(换句话说,相对影响),该特性影响总体模型性能的频率与所有树的分割总数在相应的特征上。如果有明显的强和弱特征,这种计算是不可靠的。该模型将在前几个增强步骤中利用较强的特征,然后利用较弱的特征来改善残差,因此夸大了一些较弱特征的重要性。
防止膨胀较弱特征的解决方法是序列化模型并使用Python或基于r的XGBoost包重新加载它,从而允许用户利用其他特征重要性计算方法,如信息增益(使用一个特征进行分割时杂质的平均减少量)和覆盖率(在所有树中,一个特征上的分割影响的样本的平均数量)。注意,这些方法基于信息获取和覆盖也可以在基于jvm的XGBoost 0.82及更高版本中使用。然而,这些方法仍然有偏差,因为它们有高估连续变量或高基数分类变量的重要性的倾向
基于这些方法,纯随机或白噪声变量可以经验地击败更重要的变量,因为它们更有可能产生分歧,即使它们是虚假的。此外,这些方法只提供了对所有样本的平均效应的估计,而没有考虑个别情况下的条件效应。其他最近的与模型无关的技术,例如沙普利加性解释³,可以探索,以帮助解决这些限制。
满足自定义目标函数和评估的要求
杠杆化是有要求的自定义目标和评估在XGBoost。为了对大型数据集的每个候选分割进行贪婪地搜索和有效地重新计算损失,XGBoost采用牛顿提振该方法依赖于执行二阶泰勒展开来近似损失函数,并选择在优化近似损失的特征上进行分割。牛顿推进对目标的限制为2倍而后可微的为了保证损失的二阶近似是合理的,即损失的局部曲率包含关于其最优值位置的一些信息。对于损失函数,其中的元素黑森矩阵都是在非零集的特征空间上支持零,牛顿提振将不准确。
一个函数的例子,其中黑森等于零是分位数回归损失方程,如下所示:
在哪里
是目标分位数。它几乎到处都有定义良好的导数:
如果我们计算拆分一组样本的增益一个分成两个单独的分区B和C通过使用二阶近似来近似损失,我们会注意到它纯粹是损失的点梯度和黑森函数:

在哪里
是一些样本D和的近似损失
是基本学习器的和。
在某些情况下,例如确定分位数回归损失的分割,计算近似增益可能会有问题。这是因为当估计的分位数值远离分区内的观测值时,梯度和Hessian对于较大的残差都是恒定的,通过分割到单独的分区所获得的增益很难高到足以发生任何分割。考虑到XGBoost的特征重要性计算依赖于特定特征的分割频率,由于增益低而没有分割的常见症状是所有特征的特征重要性得分为零。
一些可能的解决方法包括使用目标损失函数的近似值(如果可用的话),例如:pseudo-Huber或log-cosh近似损失Huber损失或平均绝对误差(MAE),或将黑森的元素设置为固定常数,以便它将进行一阶逼近,这是一个类似于梯度下降而不需要二阶逼近的函数。请注意,如果梯度是分段常数,它也有在大残差下低增益的风险。在这些情况下,为Hessian术语添加一个缩放因子以鼓励拆分通常是有用的。
解耦堆和堆外内存需求
XGBoost的重分区阶段主要依赖于堆内存,即执行程序内存,但是使用Java本机接口触发的分布式训练需要堆外内存。属性可以调整堆外内存spark.executor.memoryOverhead在Apache Spark中设置。因此,与其余的训练工作流相比,XGBoost训练阶段有不同的Apache Spark需求。将XGBoost训练和其他阶段与调优Apache Spark设置的能力相分离,为各种训练用例提供了所需的灵活性。
XGBoost在米开朗基罗上的结果
在针对米开朗基罗中的大量数据进行训练时,Uber的许多团队都选择XGBoost作为树算法。
最佳实践
我们利用米开朗基罗扩展XGBoost来训练更大的模型的经验显示了几个与有效生产分布式XGBoost相关的最佳实践,我们打算将这些实践应用到这项工作的未来迭代中:
利用黄金数据集和基线模型来衡量模型性能
拥有它很重要黄金数据集(它很好地覆盖了不同的数据配置,例如高基数、宽特征、不常见的分类特征)和基线模型,可以用作跨工作流各个阶段的性能基准。在每次更改被推入生产之前的集成测试中使用,以及升级Apache Spark和XGBoost版本时使用,以确保培训和服务性能都不会出现倒退。在测量实时预测服务的有效性时,我们比较了使用样本输入数据生成的预测分数,以确保任何给定模型对每个样本数据点生成的预测值与Apache Spark和XGBoost的旧版本和新版本相同。我们还测量总体模型加载时间和延迟,以确保在版本之间没有引入回归。
XGBoost培训分开训练前和训练后阶段
在调用XGBoost训练器之前,米开朗基罗训练工作流要经历几个阶段,包括使用从系统的FeatureStore获取的特征进行数据连接、训练-验证-测试数据分割以及特征分布计算。每个阶段都有不同的需求,所以我们发现将这些阶段解耦为任务(称为Apache Spark应用程序),并能够注入自己的自定义Apache Spark设置,如下图4所示:
米开朗基罗的内部工作流编排服务生成一个优化的物理执行计划,根据不同的需求将各个阶段分解为单独的Apache Spark应用程序,并使用自己的设置。为了简化ML模型开发过程,我们设计Michelangelo为Apache Spark和XGBoost提供默认调优设置。然而,在某些情况下,我们需要更改Apache Spark或XGBoost设置,以适应更大数据集和/或更复杂模型的调优需求。
作为米开朗基罗工作流系统的一部分,我们可以选择将每个工作流阶段产生的元数据和中间数据检查到HDFS。我们发现始终将转换后的特性序列化到HDFS是可取的,可以简化可调试性和重试,而不需要经过预训练转换步骤,与XGBoost分布式训练阶段相比,这些步骤往往占用更多的资源和时间。
保持对新特性和现有bug的了解
我们在为Michelangelo构建XGBoost解决方案时遇到的一个问题是,在训练作业结束时,Apache Spark会话从未被清理。在XGBoost 0.8及更高版本中,一旦我们进入XGBoost,就会有一个保守的逻辑,这样任何失败的任务都会注册一个SparkListener关闭SparkContext。
然而,Apache Spark 2.3.2版本不能正确处理SparkListener的异常,导致SparkContext被锁定。因此,当整个作业完成时,一个SparkContext。stop被调用,作业不能正确地保护和停止SparkContext,导致Apache Spark Session无法正确关闭。
幸运的是,这个问题在XGBoost 0.91及更高版本中得到了修复,并提供了以下特性:
-
- SparkContext是停止明确而不是依赖SparkListener出现的异常。
- 重新分区阶段的保守检查被移除,这样它就可以容忍失败的任务重试。您可以通过通过版本更新来了解新特性和现有错误,从而减少从各个基础架构层中梳理日志以确定根本原因的时间。
在模型训练阶段考虑内存需求
内存不足和分割错误是我们在使用XGBoost进行模型训练时遇到的两个最常见的问题。由于训练工作流的每个步骤都有不同的内存资源需求,因此确保用户的体系结构考虑到这些需求是明智的。例如,在XGBoost训练工作流的FeatureStore连接步骤中,会发生大量数据变换,从而导致数据溢出到磁盘。在实际的XGBoost训练阶段,溢出率会有所不同。我们发现在模型拟合阶段,我们可以通过提供足够数量的具有大容器内存的执行器和禁用来消除XGBoost中的分割错误问题外部内存使用情况.
推动平台限制并提供指导方针
在GBD树的深度和数量与可以在其上可靠训练的数据量之间,将您的ML平台推向极限是很重要的。为了优化我们的模型以获得最可能的用例,我们跟踪了它们在各种资源约束下的准确性和总体训练时间,比如执行程序的数量和它们的容器内存限制。此外,这种健壮的测试有助于为平台用户设定适当的期望,并使我们能够准确地处理容量规划。
自动化模型再训练
对于许多问题,我们有特定于城市/地区的模型和全局模型。定期重新训练这些模型需要平台支持动态数据范围和过滤逻辑和工具,以自动化数据验证、训练、模型性能检查和模型部署。在我们的例子中,我们利用了Michelangelo ML Explorer (MLE),这是一个用于ML工作流创建的内部工具Uber的自动化数据工作流管理系统。如果没有工具,管理成千上万用例的模型生命周期将是一个挑战。
下一个步骤
在米开朗基罗的XGBoost训练工作流的未来迭代中,我们打算加速训练,减少内存占用,提高模型可解释性,并进一步调优Apache Spark设置,以考虑更高级的ML训练用例。反过来,这些更新将进一步使米开朗基罗为大规模用户提供有价值的见解。
如果使用XGBoost, Apache Spark和其他大数据技术对大规模开发ML模型训练工作流程感兴趣的您,可以考虑申请一个角色在我们队!
确认
如果没有优步工程师和数据科学家团队的帮助,我们不可能完成本文中概述的技术工作。特别感谢Felix张,Nan Zhu, Jeffrey Zhong, Tracy Liu, Min Cai, Mayank Bansal, Olcay Cirit, Cen Guo, Armelle Patault, Bozhena Bidyuk, Anurag Gupta, Michael Mallory, Jyothi Narayana, Sally Lee,感谢Anupriya Mouleesha, Smitha Shyam,以及整个米开朗基罗团队的支持。
Apache Spark是Apache软件基金会在美国和/或其他国家的注册商标。使用此标记并不意味着Apache软件基金会的认可。
参考文献
-
- 陈,田琦,卡洛斯·格斯特林。”XGBoost:一个可扩展的树增强系统。第22届ACM SIGKDD知识发现和数据挖掘国际会议论文集- KDD ' 16 (2016)
- Strobl, C., Boulesteix, AL., Zeileis, A.等人。”随机森林变量重要性测量中的偏差BMC生物信息学(2007)
- 伦德伯格,斯科特,李秀仁。”解释模型预测的统一方法。第30届神经信息处理系统论文集- NIPS ' 17 (2017)
- XGBoost文档






