Apache Spark Gradient Boosted Tree培训运行速度慢

时间:2015-09-21 19:22:21

标签: amazon-web-services machine-learning apache-spark elastic-map-reduce

我正在使用Spark 1.4的ML库中的Gradient Boosted Trees学习算法进行实验。我正在解决二进制分类问题,我的输入是大约50,000个样本和~500,000个特征。我的目标是以人类可读的格式输出生成的GBT集合的定义。到目前为止,我的经验是,对于我的问题大小,向群集添加更多资源似乎对运行的长度没有影响。 10次​​迭代训练似乎大约需要13个小时。这是不可接受的,因为我希望进行100-300次迭代运行,并且执行时间似乎随着迭代次数而爆炸。

我的Spark应用程序

这不是确切的代码,但可以简化为:

SparkConf sc = new SparkConf().setAppName("GBT Trainer")
            // unlimited max result size for intermediate Map-Reduce ops.
            // Having no limit is probably bad, but I've not had time to find
            // a tighter upper bound and the default value wasn't sufficient.
            .set("spark.driver.maxResultSize", "0");
JavaSparkContext jsc = new JavaSparkContext(sc)

// The input file is encoded in plain-text LIBSVM format ~59GB in size
<LabeledPoint> data = MLUtils.loadLibSVMFile(jsc.sc(), "s3://somebucket/somekey/plaintext_libsvm_file").toJavaRDD();

BoostingStrategy boostingStrategy = BoostingStrategy.defaultParams("Classification");
boostingStrategy.setNumIterations(10);
boostingStrategy.getTreeStrategy().setNumClasses(2);
boostingStrategy.getTreeStrategy().setMaxDepth(1);
Map<Integer, Integer> categoricalFeaturesInfo = new HashMap<Integer, Integer>();
boostingStrategy.treeStrategy().setCategoricalFeaturesInfo(categoricalFeaturesInfo);

GradientBoostedTreesModel model = GradientBoostedTrees.train(data, boostingStrategy);

// Somewhat-convoluted code below reads in Parquete-formatted output
// of the GBT model and writes it back out as json.
// There might be cleaner ways of achieving the same, but since output
// size is only a few KB I feel little guilt leaving it as is.

// serialize and output the GBT classifier model the only way that the library allows
String outputPath = "s3://somebucket/somekeyprefex";
model.save(jsc.sc(), outputPath + "/parquet");
// read in the parquet-formatted classifier output as a generic DataFrame object
SQLContext sqlContext = new SQLContext(jsc);
DataFrame outputDataFrame = sqlContext.read().parquet(outputPath + "/parquet"));    
// output DataFrame-formatted classifier model as json           
outputDataFrame.write().format("json").save(outputPath + "/json");

问题

我的Spark应用程序(或GBT学习算法本身)对该大小的输入有什么性能瓶颈,如何实现更高的执行并行性?

我仍然是新手Spark dev,我很欣赏有关群集配置和执行分析的任何提示。

有关群集设置的更多详细信息

我在r3.8xlarge实例(32个内核,每个244GB RAM)的AWS EMR集群(emr-4.0.0,YARN集群模式)上运行此应用程序。我正在使用如此大的实例,以最大限度地提高资源分配的灵活性。到目前为止,我已尝试在驱动程序和工作程序之间使用1-3个r3.8xlarge实例和各种资源分配方案。例如,对于1个r3.8xlarge实例的集群,我按如下方式提交应用程序:

aws emr add-steps --cluster-id $1 --steps Name=$2,\
Jar=s3://us-east-1.elasticmapreduce/libs/script-runner/script-runner.jar,\
Args=[/usr/lib/spark/bin/spark-submit,--verbose,\
--deploy-mode,cluster,--master,yarn,\
--driver-memory,60G,\
--executor-memory,30G,\
--executor-cores,5,\
--num-executors,6,\
--class,GbtTrainer,\
"s3://somebucket/somekey/spark.jar"],\
ActionOnFailure=CONTINUE

对于3个r3.8xlarge实例的集群,我调整了资源分配:

--driver-memory,80G,\
--executor-memory,35G,\
--executor-cores,5,\
--num-executors,18,\

我并不清楚每个执行者有多少记忆是有用的,但我觉得在任何一种情况下我都很慷慨。通过Spark UI,我没有看到输入大小超过几GB的任务。在为驱动程序进程提供如此多的内存时,我正在谨慎行事,以确保它不会因任何中间结果聚合操作而缺乏内存。

我正在尝试根据Cloudera's How To Tune Your Spark Jobs series中的建议将每个执行程序的核心数减少到5(根据他们的说法,5个核心往往会引入HDFS IO瓶颈)。我还确保为主机操作系统和Hadoop服务留下足够的备用RAM和CPU。

到目前为止我的发现

我唯一的线索是Spark UI在执行的尾部为许多任务显示非常长的调度延迟。我也感觉到Spark UI显示的阶段/任务时间线并不能解释作业完成所有时间。我怀疑驱动程序应用程序在每次训练迭代结束时或整个训练运行结束时都会执行某种冗长的操作。

我已经对调整Spark应用程序做了大量研究。大多数文章都会提供有关使用RDD操作的很好建议,这些操作可以减少中间输入大小或避免阶段之间的数据混乱。在我的情况下,我基本上使用的是一种“开箱即用”算法,该算法由ML专家编写, 已经在这方面进行了很好的调整。我自己的代码将GBT模型输出到S3应该花费很少的时间来运行。

2 个答案:

答案 0 :(得分:0)

我没有使用MLLibs GBT实现,但是我都使用了

LightGBMXGBoost成功。我强烈建议您看看这些其他库。

通常,GBM实现需要迭代训练模型,因为它们在构建下一棵树时会考虑整个集合的损失。这使得GBM训练固有地成为瓶颈,并且不容易并行化(不同于随机并行化的随机森林)。我希望它以更少的任务执行得更好,但这可能不是您的全部问题。由于您拥有500K的众多功能,因此在训练过程中计算直方图和分割点时将有非常高的开销。您应该减少拥有的特征的数量,尤其是因为它们的数量远远超过导致特征过度拟合的样本数量。

关于调整集群: 您希望最大程度地减少数据移动,以便更少的执行程序和更多的内存。每个ec2实例1个执行程序,其内核数设置为该实例提供的值。

您的数据足够小,足以容纳大约2个该大小的EC2。假设您使用的是双精度(8字节),则为8 * 500000 * 50000 = 200 GB尝试通过在数据帧上使用<a-box position="-1 0.5 -3" rotation="0 45 0" material="color: blue" wireframe></a-box> 将其全部加载到ram中。如果对所有行(例如求和)执行操作,则应强制加载该操作,然后可以测量IO花费的时间。一旦将其放入ram并对其进行缓存,其他任何操作都会更快。

答案 1 :(得分:0)

使用如此大小的数据集,最好将整个数据集加载到内存中,并直接使用XGBoost,而不是Spark实现。

如果您要坚持使用Spark来提供更大的可扩展性,建议您仔细研究一下您的分区策略。如上所述,如果无法有效地对数据进行分区,则添加计算机将不会改善运行时间,并且超负荷工作程序的子集仍然是您的瓶颈。确保您具有有效的分区密钥,并在开始训练阶段之前重新分区RDD。