利用 Spark 和 scikit-learn 将你的模型训练加快 100 倍

文章发布于公号【数智物语】 (ID:decision_engine),关注公号不错过每一篇干货。

来源 | AI开发者(okweiwu)

作者 | skura

在 Ibotta,我们训练了许多机器学习模型。这些模型为我们的推荐系统、搜索引擎、定价优化引擎、数据质量等提供动力。它们在与我们的移动应用程序交互时为数百万用户做出预测。

当我们使用 Spark 进行数据处理时,我们首选的机器学习框架是 scikit-learn。随着计算机变得越来越便宜,机器学习解决方案的上市时间变得越来越关键,我们探索了加快模型训练的各种方法。其中一个解决方案是将 Spark 和 scikit-learn 中的元素组合到我们自己的混合解决方案中。

01sk-dist 的介绍

我们很高兴地宣布我们的开源项目 sk-dist 的启动。该项目的目标是为使用 Spark 分发 scikit 学习元估计器提供一个通用框架。元估计器的例子有决策树集合(随机林和额外随机树)、超参数调解器(网格搜索和随机搜索)和多分类技术(一对多和多对一)。

我们的主要动机是填补传统机器学习模型空间的空白。在神经网络和深度学习的空间之外,我们发现我们的训练模型的大部分计算时间并没有花在训练单个数据集的单个模型上。相反,大部分时间都花在使用元估计器在数据集上训练模型的多次迭代上。

02例子

让我们谈谈手写数字数据集。在这里,我们对手写数字的图像进行了适当的编码、分类。我们可以很快在一台机器上训练 1797 条记录的支持向量机,花费的时间不到一秒钟。但超参数调整需要在训练数据的不同子集上进行大量的训练。

如下图所示,我们已经构建了一个总计需要 1050 个训练的参数网格。在拥有 100 多个核的 Spark 上使用 sk dist 只需 3.4 秒。这项工作的总时间是 7.2 分钟,意思是在没有并行化的单机上训练要花这么长时间。

1. import timefrom sklearn import datasets, svm
2. from skdist.distribute.search import DistGridSearchCV
3. from pyspark.sql import SparkSession # instantiate spark session
4. spark = (
5.      SparkSession
6.      .builder
7.      .getOrCreate()
8.      )
9. sc = spark.sparkContext # the digits dataset
10. digits = datasets.load_digits()
11. X = digits["data"]
12. y = digits["target"] # create a classifier: a support vector classifier
13. classifier = svm.SVC()
14. param_grid = {
15.   "C": [0.01, 0.01, 0.1, 1.0, 10.0, 20.0, 50.0],
16.   "gamma": ["scale", "auto", 0.001, 0.01, 0.1],
17.   "kernel": ["rbf", "poly", "sigmoid"]
18.   }
19. scoring = "f1_weighted"
20. cv = 10# hyperparameter optimization
21. start = time.time()
22. model = DistGridSearchCV(
23.        classifier, param_grid,
24.        sc=sc, cv=cv, scoring=scoring,
25.        verbose=True
26.        )
27. model.fit(X,y)
28. print("Train time: {0}".format(time.time() - start))
29. print("Best score: {0}".format(model.best_score_))------------------------------
30. Spark context found; running with spark
31. Fitting 10 folds for each of 105 candidates, totalling 1050 fits
32. Train time: 3.380601406097412
33. Best score: 0.981450024203508

这个例子演示了一个常见的场景,在这个场景中,将数据拟合到内存中并训练单个分类器是很简单的,但是适合超参数优化所需的匹配数量会迅速增加。下面是一个运行网格搜索问题的例子,和上面的 sk dist 示例类似:

带sk-dist的网格搜索

对于 ibotta 传统机器学习的实际应用,我们经常发现自己处于类似这样的情况中:中小型数据(10k 到 1M 的记录)和许多简单分类器迭代以适应超参数调整、集成和多分类解决方案。

03 现有解决方案

传统的机器学习元估计器训练方法已经存在。第一个是最简单的:scikit-learn 使用 joblib 内置的元估计器并行化。这与 sk-dist 的操作非常相似,但是它有一个主要的限制:性能受限于任何机器的资源。即使与理论上拥有数百个内核的单机相比,Spark 仍然具有一些优势,如执行器的微调内存规范、容错,以及成本控制选项,如对工作节点使用 spot 实例。

另一个现有的解决方案是 Spark ML,它是 Spark 的一个本地机器学习库,支持许多与 scikit-learn 相同的算法来解决分类和回归问题。它还具有诸如树集合和网格搜索之类的元估计器,以及对多分类问题的支持。

分布在不同的维度上

如上所示,Spark ML 将针对分布在多个执行器上的数据来训练单个模型。当数据量很大,以至于无法存入一台机器上的内存时,这种方法可以很好地工作。然而,当数据量很小时,在单台机器上这可能会比 scikit-learn 的学习效果差。此外,例如,当训练一个随机森林时,Spark ML 按顺序训练每个决策树。此项工作的时间将与决策树的数量成线性比例,和分配给该任务的资源无关。

对于网格搜索,Spark ML 实现了一个并行参数,该参数将并行地训练各个模型。然而,每个单独的模型仍在对分布在执行器之间的数据进行训练。这项任务的总并行度只是纯粹按照模型维度来的,而不是数据分布的维度。

最后,我们希望将我们的训练分布在与 Spark ML 不同的维度上。当使用中小型数据时,将数据拟合到内存中不是问题。对于随机森林的例子,我们希望将训练数据完整地广播给每个执行器,在每个执行者身上拟合一个独立的决策树,并将这些拟合的决策树带回给驱动器,以集合成一个随机森林。这个维度比串行分布数据和训练决策树快几个数量级。

04特征

考虑到这些现有解决方案在我们的问题空间中的局限性,我们内部决定开发 sk-dist。归根结底,我们希望发布的是模型,而不是数据。

虽然 sk-dist 主要关注元估计器的分布式训练,但它也包括很多其它模块,如 Spark 的 scikit-learn 模型的分布式预测模块等。

1. 分布式训练 ——使用 Spark 进行分布式元估计训练,支持以下算法:带网格搜索和随机搜索的超参数优化、带随机林的树集合、额外树和随机树嵌入,以及一对一和一对多的多分类策略。

2. 分布预测 ——具有 Spark 数据帧的拟合 scikit-learn 估计器的预测方法。这使得带有 scikit-learn 的大规模分布式预测可以在没有 Spark 的情况下进行。

3. 特征编码 ——分布特征编码使用被称为编码器的灵活特征变换器来完成。不管有没有 Spark,它都可以起作用。它将推断数据类型,自动应用默认的特征变换器作为标准特征编码技术的最佳实现。它还可以作为一个完全可定制的功能联合,如编码器,它的附加优势是与 Spark 匹配的分布式 transformer。

05用例

以下是判断 sk-dist 是否适合解决你的机器学习问题的一些准则:

1. 传统的机器学习方法,如广义线性模型、随机梯度下降、最近邻、决策树和朴素贝叶斯等,都能很好地应用于 sk-dist,这些方法都可以在 scikit-learn 中实现,并且可以直接应用于 sk-dist 元估计。

2. 中小型数据、大数据不能很好地在 sk-dist 中起作用。记住,分布式训练的维度是沿着模型的轴,而不是数据。数据不仅需要放在每个执行器的内存中,而且要小到可以传播。根据 Spark 配置,最大传播大小可能会受到限制。

3. Spark 定向和访问——sk-dist 的核心功能需要运行 Spark。对于个人或小型数据科学团队来说,这并不总是可行的。

这里一个重要的注意事项是,虽然神经网络和深度学习在技术上可以用于 sk-dist,但这些技术需要大量的训练数据,有时需要专门的基础设施才能有效。深度学习不是 sk-dist 的最佳用例,因为它违反了上面的(1)和(2)。

06安装

要开始使用 sk-dist,请查看安装指南。代码库还包含一个示例库,用于说明 sk-dist 的一些用例。欢迎所有人提交问题并为项目做出贡献。

星标我,每天多一点智慧