将 ML 投入生产 II:日志记录和监控
在我们之前的文章中,我们展示了如何使用 Apache Kafka 的 Python API ( Kafka-Python )来实时生成算法。在这篇文章中,我们将更多地关注 ML 方面,更具体地说,关注如何在(重新)训练过程中记录信息,并监控实验的结果。为此,我们将使用 MLflow 和hyperpt或 HyperparameterHunter 。
场景和解决方案
场景和解决方案的详细描述可以在前面提到的帖子中找到。
总之,我们希望实时运行算法**,,并且需要根据算法的输出(或预测)立即采取一些行动。此外,在 N 次交互(或观察)后,算法需要重新训练而不停止**预测 服务。
我们的解决方案主要依赖于 Kafka-Python 在流程的不同组件之间分发信息(更多细节请参见我们第一篇文章中的图 1):
- 服务/应用程序生成一条消息(JSON ),其中包含算法所需的信息(即功能)。
- “预测器组件接收消息,处理信息并运行算法,将预测发送回服务/应用。
- 在 N 个已处理的消息(或观察)之后,预测器向“训练器”组件发送消息,开始新的训练实验。这个新的实验将包括原始数据集加上所有新收集的观察数据。正如我们在第一篇文章中所描述的,在现实世界中,在重新训练算法之前,人们必须等到它接收到与观察一样多的真实结果(即真实标签或数字结果)。
- 一旦算法被重新训练,训练器发送相应的消息,预测器将加载新的模型而不停止服务。
ML 工具
为了简洁起见,我们更喜欢在这里关注过程而不是算法本身。但是,如果你想进一步探索,至少让我给你一些指导。
我们使用的核心算法是 LightGBM 。LightGBM 已经成为我几乎每个涉及分类或回归的项目的首选算法(它还可以进行排名)。网上有大量关于这个包的信息,人们可以在 github repo 的示例区学习如何在各种场景中使用它。但是,对于给定的算法,我总是推荐阅读相应的论文。在这种情况下,柯等人 2017 做得太棒了。这篇论文写得很好,总的来说,很容易理解。
使用的优化包将是 HyperparameterHunter(以下简称 HH)和 Hyperopt ,两者都采用贝叶斯优化方法。HH 使用 Skopt 作为后端,其BayesianOptimization
方法基于高斯过程。另一方面,据我所知,Hyperopt 是唯一实现 TPE(Parzen estimators 树)算法的 Python 包。我发现了一些使用该算法的其他库,但都依赖于 Hyperopt(例如 Optunity 或 Project Ray 的 tune )。
如果你想学习贝叶斯优化方法,我建议你做以下事情。首先阅读 Skopt 站点中的贝叶斯优化部分。在那里你可以找到问题陈述和贝叶斯过程(或循环)的描述,我认为这对于贝叶斯优化方法来说是相当常见的。然后去超视论文( Bergstra 等人,2011 )。同样,如果你想熟悉贝叶斯方法,这是一篇“必读”的论文。特别是,在那里你将学习高斯过程(GP)和基于顺序模型的全局优化(SMBO)算法(第 2-4 节)背景下的 TPE。
剩下的 ML“成分”是 MLflow,这里将使用它来帮助跟踪和监控训练过程(尽管您会看到 HH 已经很好地保存了所有重要的数据)。
跟踪 ML 流程
按照我们第一篇文章中使用的类似方法,我们将使用代码作为评论最重要部分的指南。本节中的所有代码都可以在我们的 repo 的train
模块中找到。我们将从远视开始,然后转移到 HH,在那里我们将说明是什么使后者独一无二。
远视
本节中的代码可以在train
模块的脚本[train_hyperopt_mlflow](https://github.com/jrzaurin/ml_pipelines/blob/master/train/train_hyperopt_mlflow.py).py
中找到。
记住,目标是最小化一个目标函数。我们的远视目标函数看起来像这样:
Snippet 1
其中params
可能是,例如:
Snippet 2
让我们讨论函数中的代码。该功能必须仅依赖于params
。在函数中,我们使用交叉验证,并输出最佳指标,在本例中为binary_logloss
。请注意,我们使用 LightGBM(作为lgb
导入)方法(第 14 行,片段 1 中的lgb.cv
),而不是相应的sklearn
总结。这是因为根据我的经验,LightGBM 自己的方法通常要快一些。还要注意的是,LightGBM 并没有实现像f1_score
这样的指标。尽管如此,我们还是在train_hyperopt.py
和train_hyperopt_mlfow.py
脚本中包含了一个 LightGBM f1
定制的度量函数,以防万一。
在代码片段 1 中的第 22行处停下来一秒钟是值得的,这里我们记录了用于特定迭代的提升轮次的数量。这是因为当使用 Hyperopt(或 Skopt 或 HH)时,算法将根据参数的输入值进行优化,其中一个参数是num_boost_round
。在目标函数中,我们通过提前停止进行交叉验证,以避免过度拟合。这意味着最终的提升轮数可能与输入值不同。该信息将在优化过程中“丢失”。为了克服这个限制,我们简单地将最后的num_boost_round
保存到字典early_stop_dict
中。然而,并不总是清楚这是最好的解决方案。关于这个和其他关于 GBMs 优化的问题的完整讨论,请在我的 github 中查看这个笔记本。
最后,记住我们需要最小化输出值。因此,如果输出是一个分数,objective
函数必须输出它的负值,而如果是一个错误(rmse
)或损失(binary_logloss
),函数必须输出值本身。
运行优化过程的代码很简单:
Snippet 3
每一组被尝试的参数将被记录在trials
对象中。一旦优化完成,我们可以编写自己的功能来记录结果。或者,我们可以使用 MLflow 等工具来帮助我们完成这项任务,并直观地监控不同的实验。你可以想象,一个人只能在 MLflow 上写一些帖子。在这里,我们将简单地说明我们如何使用它来记录最佳性能参数、模型和指标,并监控算法的性能。
MLflow
对于 Hyperopt 和 HH,跟踪每个实验结果的 MLflow 块几乎保持不变,如下面的代码片段所述。
Snippet 4
第 1–8 行:我们在最新的 MLflow 版本(0.8.2
)中发现的一个"恼人的"行为是,当你第一次实例化类MLflowClient()
或创建一个实验(mlflow.create_experiment('test')
)时,它会创建两个目录,mlruns/0
和mlruns/1
。前者被称为Default
,当你运行实验时,它将保持为空。这里我们在一个名为test_mlflow
的空目录中展示了这种行为:
infinito:test_mlflow javier$ ls
infinito:test_mlflow javier$ ipython
Python 3.6.5 (default, Apr 15 2018, 21:22:22)
Type ‘copyright’, ‘credits’ or ‘license’ for more information
IPython 7.2.0 — An enhanced Interactive Python. Type ‘?’ for help.In [1]: import mlflowIn [2]: mlflow.__version__
Out[2]: ‘0.8.2’In [3]: mlflow.create_experiment(‘test’)
Out[3]: 1In [4]: ls mlruns/
0/ 1/
因此,当您打开 UI 时,第一个屏幕将是一个空屏幕,上面有一个名为Default
的实验。如果你能接受这一点(我不能),那么有更简单的方法来编写 MLflow 块中第 1–8 行的代码,例如:
Snippet 5
在当前设置中(片段 4),我们的第一次流程初始化(python initialize.py
)将被称为Default
,并存储在目录mlruns/0
中。
另一方面,定义每次运行的experiment_id
的一种更优雅的方式是列出现有的实验并获得最后一个元素的 id:
experiments = client.list_experiments()
with mlflow.start_run(experiment_id=experiments[-1].experiment_id):
然而,我发现的另一个*【不方便】*行为是client.list_experiments()
不维护秩序。这就是为什么我们使用“不太优雅”的解决方案n_experiments
。
提前第 9 行:我们只是运行实验,并记录所有参数、指标和模型作为 MLflow 工件。
在此阶段,值得一提的是,我们完全意识到我们“未充分利用”MLflow。除了跟踪算法的结果,MLflow 还可以打包和部署项目。换句话说,使用 MLflow 可以管理几乎整个机器学习周期。然而,我的印象是,要做这样一件事,需要在头脑中开始一个项目。此外,对我来说,如何在不增加不必要的复杂性的情况下使用 MLflow 打包本文和之前的文章中描述的整个项目并不简单。尽管如此,我在这个库中看到了很多潜力,我清楚地看到自己在未来的项目中使用它。
超参数猎人(HH)
看了代码片段 1-4 后,有人可能会想:“如果我想记录每一次超优化运行并在排行榜中保持跟踪,该怎么办?”。嗯,有两种可能性: i) 可以简单地将 MLflow 块移到目标函数的主体,并相应地修改代码,或者 ii) 简单地使用 HH。
在我写这篇文章的时候,我看到了使用 HH 的两个缺点。首先,你不需要编写自己的目标函数。虽然这在很大程度上是一个积极的方面,但也意味着它不太灵活。然而,我使用 HH 已经有一段时间了,除非你需要设计一个复杂的目标函数(例如,目标中一些不寻常的数据操作,或者参数的内部更新),HH 会完成这项工作。如果你确实需要设计一个高度定制的目标函数,你可以使用sklearn
的语法编写一个,作为model_initializer
传递给 HH 的optimizer
对象。
第二个缺点,也许更重要,与 HH 没有直接关系,而是 Skopt。HH 是在 Skopt 的基础上构建的,Skopt 明显比 Hyperopt 慢。然而,我知道目前有人在努力添加 Hyperopt 作为一个替代后端(以及其他即将推出的功能,如功能工程,敬请关注)。
总之,如果你不需要设计一个特别复杂的目标函数,并且你能负担得起“目标速度”,HH 提供了许多使它独一无二的功能。首先,HH 为你记录和组织所有的实验。此外,当你运行额外的测试时,它会学习,因为过去的测试不会浪费。换句话说:
“super parameter hunter 已经知道了你所做的一切,这就是 super parameter hunter 做一些了不起的事情的时候。它不像其他库那样从头开始优化。它从您已经运行过的所有实验和之前的优化回合开始*。”亨特·麦古森。*
让我们看看代码。以下 3 个片段是使用 HH 时需要的全部*(更多细节参见文档)。本节的完整代码可以在train
模块的脚本train_hyperparameterhunter_mlflow.py
中找到。*
正如你将看到的,语法非常简洁。我们首先设置一个Environment
,它只是一个简单的类,用来组织允许实验被公平比较的参数。
Snippet 6
然后我们进行实验
Snippet 7
其中model_init_params
和model_extra_params
为:
Snippet 8
当仔细观察代码片段 7 时,我们可以发现 HH 和 Hyperopt 之间的进一步差异,同样纯粹与 Skopt 后端有关。您将会看到,当使用 Hyperopt 时,可以使用分位数均匀分布(hp.quniform(low, high, step)
)。在 Skopt 中没有这样的选项。这意味着对于像num_boost_rounds
或num_leaves
这样的参数,搜索效率较低。例如,先验地,人们不会期望 100 轮和 101 轮助推的两个实验产生不同的结果。例如,这就是为什么在使用 Hyperopt 时,我们将num_boost_rounds
设置为hp.quniform(50, 500, 10)
。
一种可能的方法是使用 Skopt 的分类变量:
num_leaves=Categorical(np.arange(31, 256, 4)),
然而,这并不是最佳解决方案,因为在现实中,num_boost_rounds
或num_leaves
并不是分类变量,而是将被如此对待。例如,默认情况下,Skopt 将为分类特征构建输入空间的一键编码表示。由于类别没有内在的顺序,如果cat_a != cat_b
在那个空间中两点之间的距离是 1,否则是 0。在搜索像num_leaves
这样的参数的过程中,这种行为不是我们想要的,因为 32 片叶子与 31 片叶子的距离可能是 255 片。Skopt 提供了不改造空间的可能性,虽然仍不理想,但更好:
num_leaves=Categorical(np.arange(31, 256, 4), transform=’identity’),
但是由于这样或那样的原因,每次我试图使用它时,它都会抛出一个错误。然而,我们将使用Integer
,并在 HH 实现 Hyperopt 作为后端时使用它。
运行示例
在本节中,我们将运行一个最小的示例来说明我们如何使用 HH 和 MLflow 来跟踪培训过程的每个细节。这里我们将保持简单,但是可以使用 HH 中的Callbacks
功能来无缝集成这两个包。
最小的例子包括处理 200 条消息,并且每处理 50 条消息就重新训练该算法。在每个再训练实验中,HH 将只运行 10 次迭代。在现实世界中,对传入的消息没有限制(即 Kafka 消费者总是在收听),在处理了数千个新的观察结果之后,或者在某个时间步长(即每周)之后,可能会发生重新训练,并且 HH 应该运行数百次迭代。
此外,为了便于说明,这里使用我们自己的日志记录方法(主要是pickle
)、HH 和 MLflow 对进程进行了“过量日志记录”。在生产中,我建议使用结合 MLflow 和 HH 的定制解决方案。
让我们看一看
Figure 1. Screen shot after minimal example has run
图 1 显示了处理 200 条消息并重新训练模型 4 次(每 50 条消息一次)后的屏幕截图。在左上角的终端中,我们运行预测器(predictor.py
),中间的终端运行训练器(trainer.py)
,在这里我们可以看到上次 HH 运行的输出,而下方的终端运行应用/服务(sample_app.py
),在这里我们可以看到收到的请求和输出预测。
读者可能会注意到左上角终端加载的新模型的变化(NEW MODEL RELOADED 0
->-NEW MODEL RELOADED 1
->-NEW MODEL RELOADED 0
->-NEW MODEL RELOADED 1
)。这是因为当使用我们自己的日志方法时,我们使用一个名为EXTRA_MODELS_TO_KEEP
的参数来设置我们保留了多少过去的模型。它当前被设置为 1,当前的加载过程指向我们的输出目录。这可以很容易地在代码中改变,以保存过去的模型,或者指向 HH 或 MLflow 对应的输出目录,其中存储了过去所有性能最好的模型。
图 1 中右上方的终端启动 MLflow 跟踪服务器。图 2 显示了 MLflow UI 的一个屏幕截图。
Figure 2. Screen shot of the MLflow monitoring tool
该图显示了 MLflow 为我们称之为*“实验 _ 2”*的特定情况保存的信息(即,用 100 个累积的新观察/消息重新训练模型)。为了与 HH 保持一致,我们将每个再训练过程保存为不同的实验。如果您希望将所有的再训练过程保存为一个实验,只需转到train_hyperparameter_hunter.py
的LGBOptimizer
类中的optimize
方法,并将reuse_experiment
参数更改为True.
根据当前的设置,在mlruns
目录中,每个实验将有一个子目录。例如,mlruns/1
目录的结构是:
Snippet 8. Summary of the mlruns directory’s file structure
如你所见,所有你需要的信息都在那里,组织得非常好。让我坚持一下,我们 MLflow-code 的当前结构只保存了性能最好的一组参数和模型。如前所述,可以将代码片段 4 中的 MLflow 块移到目标函数中,并修改代码,以便 MLflow 记录每一次迭代。或者,也可以使用 HH。
HH 记录了所有的事情。让我们看看HyperparameterHunterAssets
的目录结构。每个子目录中内容的详细解释可以在这里找到。可以看到,保存了关于每个迭代的大量信息,包括每个实验的训练数据集(包括原始/默认数据集的 5 个数据集,加上每个再训练周期包括 50 个额外观察的 4 个新数据集)和当前获胜迭代的排行榜。
Snippet 9. Brief Summary of the HyperparameterHunterAssets directory’s file structure
希望在这个阶段,读者对如何结合 MLflow 和 HH 来跟踪和监控算法在生产中的性能有一个好的想法。
然而,尽管在生产 ML 时需要考虑这里描述的所有因素,但这里并没有讨论所有需要考虑的因素。让我至少在下一节提到缺失的部分。
缺失的部分
单元测试
我们在这篇和上一篇文章中(有意)忽略的一个方面是单元测试。这是因为单元测试通常依赖于算法的应用。例如,在您的公司,可能有一些管道部分在过去已经被广泛测试过,而一些新的代码可能需要彻底的单元测试。如果我有时间,我会在回购中包括一些与此任务相关的代码。
概念漂移
生产中经常被忽略的是 【概念漂移】 。概念漂移是指数据集的统计属性会随着时间的推移而发生变化,这会对预测的质量产生显著影响。例如,假设你的公司有一个应用程序,主要面向 30 岁以下的人。一周前,你开展了一场扩大用户年龄范围的广告活动。有可能你现有的模型在新观众面前表现不佳。
有许多选择来检测和解决概念漂移。人们可以编写一个定制的类,以确保训练和测试数据集中的特征分布在一定范围内保持稳定。或者,可以使用像 MLBox 这样的库。MLBox 适合自动化 ML 库的新浪潮,并带有一系列不错的功能,包括一种优化方法,它依赖于,猜猜看,什么…Hyperopt。包中的另一个功能是Drift_thresholder()
类。这个类会自动为你处理概念漂移。
MLBox 实现使用一个分类器(默认情况下是一个随机森林),并尝试预测一个观察值是属于训练数据集还是测试数据集。那么估计的漂移被测量为:
drift = (max(np.mean(S), 1-np.mean(S))-0.5)*2
其中 S 是包含与过程中使用的 n 折叠相对应的roc_auc_score
的列表。如果没有概念漂移,算法应该不能区分来自两个数据集的观察值,每折叠的roc_auc_score
应该接近随机猜测(0.5),数据集之间的漂移应该接近零。或者,如果一个或多个特征随时间发生了变化,那么算法将很容易区分训练和测试数据集,每折叠的roc_auc_score
将接近 1,因此,漂移也将接近 1。请注意,如果数据集高度不平衡和/或非常稀疏,您可能希望使用更合适的度量和算法(已知随机森林在非常稀疏的数据集中表现不佳)。
总的来说,如果你想了解更多关于包和概念漂移的概念,你可以在这里找到更多信息。他们DriftEstimator()
的源代码是这里是。同样,如果我有时间,我会在我们的回购协议中包含一些与概念漂移相关的代码。
最后,到目前为止,我们已经在本地使用了 Kafka,我们不需要扩展或维护它。然而,在现实世界中,人们必须根据流量进行调整,并维护算法的组件。我们最初的想法是让 Sagemaker 来完成这个任务,然后写第三篇文章。然而,在深入研究这个工具(你可以在回购的分支sagemaker
中找到)之后,我们发现使用 Sagemaker 带来了很多不必要的复杂性。换句话说,使用它比简单地将代码转移到云中,使用 AWS 工具(主要是 ec2 和 S3)并使用简单的定制脚本自动运行它要复杂得多。
总结和结论
让我们总结一下将 ML 投入生产时必须考虑的概念和组件。
- 写得很好的代码和结构合理的项目(非常感谢 Jordi
- 随着时间的推移记录和监控算法的性能
- 单元测试
- 概念漂移
- 算法/服务扩展和维护。
在这篇文章和我们之前的文章中,我们使用了 Kafka-Python,MLflow 和 HyperparameterHunter 或 Hyperopt 来说明第一点和第二点。第 3 点、第 4 点和第 5 点将不在此讨论。关于第 3 点和第 4 点,我认为在这篇或另一篇文章中写进一步的详细解释没有多大意义(尽管如果我有时间,我会在回购协议中添加一些代码)。关于第 5 点,正如我们之前提到的,我们认为简单地将这里描述的代码和结构迁移到云中,并使其适应那里可用的工具(例如 EC2s、S3……)就足够了。
一如既往,评论/建议:jrzaurin@gmail.com
参考资料:
[1]柯,,Thomas Finley,王泰峰,,,叶启伟,,LightGBM: 一种高效的梯度推进决策树,神经信息处理系统进展,3149–3157,2017 .
[2]伯格斯特拉,詹姆斯;巴登内特,雷米;本吉奥,约书亚;Kegl,Balazs (2011),“超参数优化算法”,神经信息处理系统进展。
将“科学”放在数据科学中
大多数企业数据科学工作与实际科学几乎没有关系…
正确的科学研究始于某个模糊的特定问题或普遍的奇迹。什么过程控制着某些特定的过程?为什么我们会看到某种效果?或者说,我们如何解释一种行为?这些普遍的奇迹需要在推测或假设中被形式化,而这些推测或假设又可以被检验。这是科学探究的真正起点。另一方面,企业数据科学家太经常“看数据说什么”从概念上讲,他们的起点是方法论框架的中点。
由于许多原因,这是有问题的。没有先验知识表明数据代表当前问题、样本大小具有统计相关性、数据实际上可以回答问题或系统的行为遍历性。最重要的是,数据是可塑的;令人惊讶的是,数据很容易遵循一个给定的叙述。
Photo by Adrien Converse on Unsplash
数据科学方法
数据科学与其最接近的科学同行有很大的不同。因为与研究不同,数据科学通常是一种反应式的操作。从某种意义上说,它建立在数据捕获的基础上,并在过去的某个时间点指定。适当的研究以其演绎的法理方法为标志,通过基于概念世界观和严格规则的精心制作的假设,数据被捕获,零假设被证伪。所以数据跟着问题走,而不是反过来。
数据科学建立在这样的假设之上,即数据在正确建模后可以揭示关于世界的新信息。由于这种积极的世界观,一些“数据驱动方法”的陷阱出现了。
- 数据可用性偏差:可用的数据被认为是相关的、全面的、决定性的和最终的。数据拥有正确的变量来推导或建模任何需要的东西。没有隐藏实际预测值的数据丢失。
- 数据质量和意义 : 数据科学团队外部来源收集的数据通常不会考虑最终目的。数据不明确,时间标记错误或者有质量问题。问任何一个数据科学家,他们都会同意:他们更喜欢质量而不是数量。
- 不隐含因果关系:特别是在处理客户数据时,很容易发现变量之间的高度相关性,通过适当的想象,这些变量之间可能存在因果关系。然而,与客户“要做的工作”相关的数据不太可能被完美捕获。
- 数据没有给出规范性的指导:数据不会告诉你什么是正确的做法。它不会神奇地揭示前进的道路。它要么是基于过去事件的预测,要么是历时的时间片。
科学方法
当在结构上应用时,集成的科学方法可能会打开集成数据科学的全新领域。例如,考虑将定性导向的社会科学或行为经济学与一个更加定量导向的数据科学家团队相结合。前者可以根据最新的学术研究确定假设,后者可以确定数据要求,并用数字补充定性研究。协力推动业务获得新的洞察力。
例如,把学术行为经济学中关于激励和推动的想法带到数字世界。测试假设并迭代。不只是从你现有的数据中提取“真相”,而是重新想象在线和离线信息收集的可能性。这意味着从被动、反应式的分析转向主动的实验。从理查德·费曼所说的货物崇拜研究转向一砖一瓦建造房屋的复合研究。
虽然我必须看到第一个实现,但我不是唯一一个将这些想法概念化的人。麻省理工学院最近发表了一篇论文,强调了数据科学在客户体验方面的缺点,并提出了类似的数据科学团队扩展,例如,客户体验专家。
这种方法的改变也将极大地改变数据科学团队在组织中的位置。它将数据科学放在驾驶员座位上,推动组织变革,开发客户知识,进行试验并迫使网站变革,所有这些都基于研究和坚实的基础。最后,将科学放入数据科学。
PVANET:用于实时对象检测的深度但轻量级的神经网络
论文摘要
Fig 2. PVANET Entire Model Vizualization
一篇论文论文摘要 PVANET:深度但轻量级的神经网络用于实时物体检测 作者:、丛瑶、何文、、周树昌、何、梁家军
论文链接: 、
概观
本文介绍了我们的轻量级特征提取网络结构,命名为 PVANET,它实现了实时目标检测性能而不损失准确性。
- 计算成本:1065 x640 输入的特征提取 7.9GMAC
- 运行时性能 :英特尔 i7 上 750 毫秒/图像(1.3FPS),NVIDIA Titan X GPU 上 42 毫秒/图像(21.7FPS)
- 精度:VOC-2007 上 83.8% mAP;VOC-2012 的 82.5% mAP
关键的设计原则是“少通道多层次”。
此外,该网络还采用了其他一些构件:
- 级联校正线性单元(C.ReLU)应用于我们的细胞神经网络的早期阶段,以在不损失精度的情况下将计算量减少一半。
- 初始应用于我们特征生成子网络的剩余部分
- 采用了多尺度表示的概念,它结合了几个中间输出,以便可以同时考虑多层次的细节和非线性。
方法
Fig 2. Model Architecture
级联整流线性单元
Fig 3. Concatenated Rectified Linear Unit (C.ReLU)
C.ReLU 的动机是观察到,在早期阶段,输出节点往往是成对的,这样一个节点的激活是另一个的相反侧。C.ReLU 将输出通道的数量减少了一半,通过简单地将相同的输出与否定连接起来,使其增加了一倍,从而使早期阶段的速度提高了 2 倍。
开始
Fig4. The Inception Module
对于捕获大对象和小对象来说,Inception 可能是最具成本效益的构建块之一。他们用 2 个 3x3 取代了普通先启块中的 5x5 卷积。
超级网络
多尺度表示及其组合在许多深度学习任务中被证明是有效的。在特征提取层将细粒度的细节和高度抽象的信息结合起来,有助于后续的区域建议网络和分类网络检测不同尺度的目标。
它们组合了
1)最后一层
2)两个中间层,其规模分别是最后一层的 2x 和 4x。
深度网络培训
为了更好的训练,他们采用了剩余的结构。他们将剩余连接添加到初始层,以稳定深层网络的后期部分。
在所有 ReLU 激活层之前添加批处理规范化层。
他们使用的学习率策略是基于平台检测,他们根据损失的移动平均值检测平台,如果低于某个阈值,他们会以某个系数降低学习率。
使用 PVANET 实现更快的 R-CNN
conv3_4、conv4_4 和 conv5_4 的三个中间输出合并成 512 通道多电平输出特性,并馈入更快的 RCNN 模块
结果
- 使用 ILSVRC2012 训练图像对 PVANET 进行预处理,用于 1000 类图像分类。
- 所有图像的尺寸都调整为 256×256,192×192 的小块被随机裁剪并用作网络输入。
- 学习率最初设置为 0.1,然后每当检测到平稳状态时,以 1/sqrt(10) ~ 0.3165 的因子减少。
- 如果学习率下降到 1e-4 以下(这通常需要大约 2M 迭代),则预训练终止
- 然后用 MS-COCO trainval、VOC2007 trainval、VOC2012 trainval 的联合集对 PVANET 进行训练。之后还需要对 VOC2007 trainval 和 VOC2012 trainval 进行微调,因为 MS-COCO 和 VOC 的类定义略有不同。
- 随机调整训练图像的大小,使图像的短边介于 416 和 864 之间。
- 对于 PASCAL VOC 评估,调整每个输入图像的大小,使其短边为 640。
- 除了在非最大抑制(NMS) (=12000)和 NMS 阈值(=0.4)之前的建议框的数量之外,与更快的 R-CNN 相关的所有参数都被设置为原始工作中的参数
- 所有评估均在单核英特尔 i7 和 NVIDIA Titan X GPU 上完成。
Fig 5. Performance with VOC2007
Fig 6.Performance with VOC2012
PVANET+在 PASCAL VOC 2012 挑战赛中获得第二名。第一个是比 PVANET 重得多的更快的 RCNN + ResNet101。
参考
- 金敬姬、S. Hong、B. Roh、Y. Cheon 和 M. Park。PVANET:用于实时对象检测的深度但轻量级的神经网络。arXiv 预印本 arXiv:1608.08021,2016。
- 克里斯蒂安·塞格迪、贾、皮埃尔·塞尔马内、斯科特·里德、德拉戈米尔·安盖洛夫、杜米特鲁·埃汉、文森特·万霍克和安德鲁·拉宾诺维奇。用回旋越走越深。IEEE 计算机视觉和模式识别国际会议(CVPR)论文集,2015 年。
- 何、、、任、。用于图像识别的深度残差学习。IEEE 计算机视觉与模式识别国际会议(CVPR)论文集,2016。
- 孔涛,姚安邦,,孙富春。面向精确区域提议生成和联合目标检测的超网络。IEEE 计算机视觉和模式识别国际会议(CVPR)论文集,2016 年。
感谢阅读!一定要看报纸。如果我发现更多有趣的见解,我会更新。
py chubby——自动面部扭曲
介绍
我们都知道这个故事。你在街上随便找一个人给你和你的朋友拍照。过了一会儿,你高兴地感谢他们,继续你的生活。过了一段时间,你终于坐下来喝杯咖啡,检查你的照片。“我的上帝,为什么我们都没有笑?!"
首先,不要惊慌。今天是你的幸运日。如果你知道如何安装东西,也许会有希望。
pip install pychubby
安装完成后,只需编写如下代码:
Minimal code example
什么是 PyChubby
pychubby
是一个自动化的面部扭曲工具。它的主要目标是作为深度学习人脸相关任务的专用增强界面。但也不妨作为一个傻乎乎的面部扭曲工具(见简介)。
你可能会想,当一个人可以用 Photoshop 和其他软件做类似的事情时,为什么还要费心呢?答案很简单— 自动化。你不必定位任何标志,移动它们,然后在每张照片的每张脸上重复这个过程。
像imgaug
这样流行的图像增强包是通用的(任何类型的图像),并且在涉及到几何变换时不提供许多选项。pychubby
专门研究人脸,允许创建几何变换:
- 局部(在脸上)
- 平滑(无伪影)
- (在一定程度上)现实
换句话说,人们不是增强整个图像,而是仅仅增强面部。增强是现实的,没有文物。
积木
pychubby
的逻辑可以概括为三块
- **地标检测:**给定一张照片,预训练的地标检测模型预测每张脸上的 68 个地标。
- **参考空间映射:**标志被映射到所谓的参考空间。该映射校正输入图像中可能的旋转、平移和缩放。
- **手动动作定义:**大多数
pychubby
动作都是在参考空间中定义的,因此应该在不同的面上产生一致的扭曲。去画廊看看预定义的或者随意定义新的。
想了解更多?
如果您有兴趣尝试一下pychubby
,或者只是想了解更多,请查看以下几个有用的链接:
所有潜在的贡献者都非常欢迎,任何反馈都非常感谢。
原载于 2019 年 9 月 16 日https://jank repl . github . io。
口袋里的 PyData 堆栈。字面上。
在手机上运行 Jupyter 笔记本
我最近将我的经典 iPhone SE 升级到了三星 Note 9,其中一个原因是惊人的摄像头和巨大的屏幕,“它可以运行 Ubuntu”。我知道,有点怪怪的原因。
但是没错,它确实可以通过一个叫做“Linux on Dex”的容器化层来运行 Ubuntu。这是一个允许你为 ARM64 架构运行纯 Ubuntu 16.04 桌面的应用程序。你需要的只是任何高质量的 USB-C HDMI 适配器、蓝牙键盘/鼠标套件(或者如果你使用 USB-C hub,就只有 USB 套件)和一个备用屏幕。要在 Dex 上试用 Linux,你只需要一根 USB-C 屏幕电缆,因为手机可以充当触摸板和键盘。
That is how it looks. I’ve used my wife’s Apple USB-C adapter to connect screen and keyboard.
但是为什么呢?!
我的爱好之一是尝试在不同的奇怪设备上运行 Python,如 Raspberry Pi、VoCore 和 Lichee Pi Nano
Why not spend your weekend bringing upstream Linux on some cheap SBC or running Ubuntu with Anaconda on Android Tablet?
这些小而便宜的设备运行普通 Linux 和 Python 的能力让我吃惊,还记得启动我的 486DX2 66Mhz。
如果这些微小的东西可以运行常规的 Python,那么它绝对值得尝试 8 核 CPU/6GB RAM beast(是的,我有一个“廉价”版本,不是 8GB 的那个)。
根据我处理基于 ARM 的 Python 发行版的经验,这条路不会是直的,但这并不意味着它不值得一试。我已经为树莓派开发 ARM conda 包做出了贡献,所以大部分步骤我都知道。
获取 Linux on Dex 应用程序
要获得 Dex 应用上的 Linux,你必须注册测试程序。你可以在这里做:
编辑描述
play.google.com](https://play.google.com/apps/testing/com.samsung.android.lxd)
有关支持的手机和功能的更多信息,请访问网站:
安装并运行容器
Download an image after app installation and unzip the file. Then create a container and launch it
Create a nice recognizable name for your container. It will help you later.
下载和解压图像需要一些时间,所以请耐心等待。
如果下载有问题,试试直接链接。
修复 apt 包管理器
不幸的是,“Linux on Dex”中的 Ubuntu 带有坏掉的 aptitude manager。但是幸运的是社区找到了修复它的方法。
在终端中运行以下命令。在 apt-get 升级的过程中,可能会发生这样的情况,你必须通过多次按“Ctrl+C”来终止挂起过程。
sudo apt-get update
sudo apt-get upgrade
sudo purge-old-kernels
sudo apt auto remove
sudo apt autoclean
安装依赖项
Archiconda 最初是为了支持较新版本的 Ubuntu 而构建的,因此 Python 需要更新的 libc 库和其他依赖项。我们会从 Ubuntu 18.04 ARM64 发行版抓取。
[wget http://launchpadlibrarian.net/365857916/libc6_2.27-3ubuntu1_arm64.deb](http://launchpadlibrarian.net/365857916/libc6_2.27-3ubuntu1_arm64.deb) -O libc6_2.27.deb[wget http://launchpadlibrarian.net/365857921/libc-bin_2.27-3ubuntu1_arm64.deb](http://launchpadlibrarian.net/365857921/libc-bin_2.27-3ubuntu1_arm64.deb) -O libc-bin_2.27.debsudo dpkg -i libc6_2.27.deb
sudo dpkg -i libc-bin_2.27.debwget [http://launchpadlibrarian.net/365856924/locales_2.27-3ubuntu1_all.deb](http://launchpadlibrarian.net/365856924/locales_2.27-3ubuntu1_all.deb) -O locales_2.27.debsudo dpkg -i locales_2.27.debwget [https://mirror.yandex.ru/ubuntu-ports/pool/main/z/zlib/zlib1g-dbg_1.2.11.dfsg-1ubuntu2_arm64.deb](https://mirror.yandex.ru/ubuntu-ports/pool/main/z/zlib/zlib1g-dbg_1.2.11.dfsg-1ubuntu2_arm64.deb) -O zlib1g-dbg_1.2.11.deb
wget [https://mirror.yandex.ru/ubuntu-ports/pool/main/z/zlib/zlib1g-dev_1.2.11.dfsg-1ubuntu2_arm64.deb](https://mirror.yandex.ru/ubuntu-ports/pool/main/z/zlib/zlib1g-dev_1.2.11.dfsg-1ubuntu2_arm64.deb) -O zlib1g-dev_1.2.11.debsudo dpkg -i zlib1g-dbg_1.2.11.deb
sudo dpkg -i zlib1g-dev_1.2.11.deb
创建一个conda
用户
由于默认dextop
用户权限管理上的一些问题,conda 包管理器无法写入自己的文件夹。当然,这是可以修复的,但是创建专门的 conda 用户和管理使用它的环境更容易。通过adduser condauser
创建一个用户,然后通过运行sudousermod -aG sudo condauser
将该用户添加到 sudoers 中。Linux on Dex 的默认 root 密码是secret
。字面上。
提供权限
需要更多的修复(测试版软件的常见情况)。如果你现在尝试 ping 任何远程主机,你会发现你没有网络权限。不知何故,在“Linux on Dex”下新创建用户没有被分配给“网络”组。
要解决这个问题,您需要编辑\etc\group
文件,并通过添加condauser
来修复组inet
和net_raw
,如下所示
inet:x:3003:root, condauser, dextop
net_raw:x:3004:root, dextop, condauser
现在,您可以通过运行su — condauser
将当前用户更改为condauser
安装 Archiconda
下载并安装 ArchiConda 。我们使用的不是最新的版本,因为我们需要一些来自 archiarm 频道的包,这在 0.2.3 版本中是没有的。
wget [https://github.com/Archiconda/build-tools/releases/download/0.2.2/Archiconda3-0.2.2-Linux-aarch64.sh](https://github.com/Archiconda/build-tools/releases/download/0.2.2/Archiconda3-0.2.2-Linux-aarch64.sh)chmod +x Archiconda3-0.2.2-Linux-aarch64.sh./Archiconda3-0.2.2-Linux-aarch64.sh
现在让我们通过运行来添加更新 conda
conda update -n base --all
并添加我的频道,其中包含 Jupyter 笔记本等丢失的包
conda config — add channel gaiar
conda install -c gaiar jupyter
安装其他软件包
由于安装了 jupyter,并且 conda 可以访问包含aarch64
软件包发行版的通道,我们可以按照您习惯的方式安装任何所需的软件包,例如
conda install seaborn -y
在手机上运行 Jupyter
现在,您已经为在手机上运行 Jupyter 做好了一切准备,让我们来运行 Jupyter 笔记本吧
jupyter notebook
如果前面的步骤一切顺利,您将看到类似这样的内容
Jupyter notebook server running on Note 9
下载笔记本
为测试克隆回购与 Seaborn 的例子https://github.com/PatWalters/plotting_distributions
启动笔记本并运行所有单元。然后你会看到类似的东西
Seaborn with Jupyter Notebook running fine
Pokedex(一些乐趣)
如果你觉得无聊,你甚至可以在手机上运行 jupyter 笔记本服务器,通过笔记本电脑连接。为此,您需要在“终端模式”下启动“Linux on Dex”。然后真正的乐趣开始了。尝试在电话键盘上输入前面提到的所有命令。
Luckily, Note 9 quite big
要在无头模式下运行笔记本,您需要更改几个命令。首先,确保您连接到 WiFi 网络,并获得您的本地 IP。运行ip a
并搜索wlan0
。记住并复制地址。
然后用命令运行 Jupyter notebook
jupyter notebook --ip YOU_IP --NotebookApp.token="token" --no-browser
1. Get your IP address. 2. Start Jupyter Notebook. 3. See progress
如果你做的一切都正确,那么你就可以在同一网络的另一台电脑上运行浏览器,并通过类似[http://YOUR_IP:8888](http://YOUR_IP:8888)
的网址访问你的“服务器”。输入您的代币,享受:)
Access notebook running our the phone
附言
当前文章的大部分是直接在手机上写的
Who does need laptops these days?
PySpark 调试— 6 个常见问题
调试 spark 应用程序可以是有趣的,也可以是非常(我是说非常)令人沮丧的经历。
我已经开始收集我不时遇到的问题,列出最常见的问题及其解决方案。
这是这个列表的第一部分。我希望它对你有用,并能为你节省一些时间。大多数问题都很容易解决,但是它们的堆栈跟踪可能很神秘,没有什么帮助。
1.从 udf 返回的列为空
当您使用 udf 向数据帧添加一列,但结果为 Null 时:udf 返回的数据类型与定义的不同
例如,如果您定义一个 udf 函数,将两个数字a
和b
作为输入,并返回a / b
,这个 udf 函数将返回一个 float(在 Python 3 中)。如果 udf 定义为:
udf_ratio_calculation = F.udf(calculate_a_b_ratio, T.BooleanType())
# or
udf_ratio_calculation = F.udf(calculate_a_b_ratio, T.DoubleType())
而不是:
udf_ratio_calculation = F.udf(calculate_a_b_ratio, T.FloatType())
那么使用 udf 的结果将是这样的:
df = df.withColumn('a_b_ratio', udf_ratio_calculation('a', 'b'))
df.show()
+---+---+---------+
| a| b|a_b_ratio|
+---+---+---------+
| 1| 0| null|
| 10| 3| null|
+---+---+---------+
完整示例:
Example of wrongly defined udf return datatype
2.ClassNotFoundException
当您试图将应用程序连接到外部系统(如数据库)时,通常会发生这种异常。下面的 stacktrace 来自于在 Postgres 中保存数据帧的尝试。
这意味着 spark 无法找到连接数据库所需的 jar 驱动程序。在实例化会话时,我们需要在 spark 配置中为应用程序提供正确的 jar
**from** pyspark **import** SparkConf
**from** pyspark.sql **import** SparkSession
conf = SparkConf()
conf.set('**spark.jars**', '**/full/path/to/postgres.jar,/full/path/to/other/jar**') spark_session = SparkSession.builder \
.config(conf=conf) \
.appName(**'**test**'**) \
.getOrCreate()
或者作为命令行参数——取决于我们如何运行我们的应用程序。
spark-submit --jars /full/path/to/postgres.jar,/full/path/to/other/jar ...
注 1:jar 对于所有节点 都是 可访问的,而不是驱动程序本地的,这一点非常重要。
注 2 :该错误也可能意味着集群组件之间的 spark 版本不匹配。还有其他更常见的指示器,如AttributeError
。更多关于这个这里。
注 3 :确保罐子列表中的逗号之间没有空格。
3.低内存消耗——为什么 spark 没有用完我的所有资源?
火花驱动器存储器和火花执行器存储器默认设置为1g
。一般来说,查看许多配置参数及其默认值是非常有用的,因为有许多因素会影响您的 spark 应用程序。
火花提供了三个位置来配置系统:火花属性控制大多数应用参数,可以…
spark.apache.org](https://spark.apache.org/docs/latest/configuration.html)
当 spark 在本地运行时,您应该将spark.driver.memory
调整为对您的系统合理的值,例如8g
,当在集群上运行时,您可能还想调整spark.executor.memory
,尽管这取决于您的集群类型及其配置。
4.文件不存在:Spark 在本地模式下运行正常,但在 YARN 中运行时找不到文件
和第二步一样,所有必要的文件/jar 应该放在集群中所有组件都可以访问的地方,例如 FTP 服务器或一个普通的挂载驱动器。
spark-submit --master yarn --deploy-mode cluster http://somewhere/accessible/to/master/and/workers/test.py
5.尝试连接到数据库:Java . SQL . SQL 异常:没有合适的驱动程序
或者,如果在试图保存到数据库时发生错误,您将得到一个java.lang.NullPointerException
:
这通常意味着我们忘记了设置**driver**
,例如 Postgres 的**org.postgresql.Driver**
:
df = spark.read.format(**'jdbc'**).options(
url= **'db_url'**,
driver='**org.postgresql.Driver**', # <-- here
dbtable=**'table_name'**,
user=**'user'**,
password=**'password'**
).load()
另外,请确保检查#2,以便正确设置驱动程序 jar。
6.NoneType ‘对象没有属性’_jvm'
出于各种原因,您可能会得到以下可怕的堆栈跟踪。
最常见的两种是:
- 您在没有激活 spark 会话的情况下使用 pyspark 函数
**from** pyspark.sql **import** SparkSession, functions **as** Fclass A(object):
def __init__(self):
self.calculations = F.col('a') / F.col('b')...a = A() # instantiating A without an active spark session will give you this error
- 或者在 udf 中使用 pyspark 函数:
**from** pyspark **import** SparkConf
**from** pyspark.sql **import** SparkSession, functions **as** F, types **as** T conf = SparkConf()
spark_session = SparkSession.builder \
.config(conf=conf) \
.appName(**'test'**) \
.getOrCreate()
*# create a dataframe* data = [{**'a'**: 1, **'b'**: 0}, {**'a'**: 10, **'b'**: 3}]
df = spark_session.createDataFrame(data)
df.show()
*# +---+---+
# | a| b|
# +---+---+
# | 1| 0|
# | 10| 3|
# +---+---+
# define a simple function that returns a / b
# we *cannot* use pyspark functions inside a udf
# udfs operate on a row per row basis while pyspark functions on a column basis* **def** calculate_a_b_max(a, b):
**return** F.max([a, b])
*# and a udf for this function - notice the return datatype* udf_max_calculation = F.udf(calculate_a_b_ratio, T.FloatType())
df = df.withColumn(**'a_b_max'**, udf_max_calculation(**'a'**, **'b'**))
df.show()
两者都会给你这个错误。
在最后一个例子中,F.max
需要一个列作为输入,而不是一个列表,所以正确的用法应该是:
df = df.withColumn(**'a_max'**, F.max(**'a'**))
这将为我们提供列 *a*
— 的最大值,而不是 udf 试图做的事情。
设置 udf 来计算每行两列之间的最大值的正确方法是:
def calculate_a_b_max(a, b):
return max([a, b])
假设a
和b
是数字。
(当然没有 udf 也有其他的方法。)
我希望这有所帮助。我计划继续这个列表,并及时处理更复杂的问题,比如调试 pyspark 应用程序中的内存泄漏。非常欢迎任何想法、问题、更正和建议:)
如果您想了解 Spark 的工作原理,请查看:
[## 用非技术性的方式解释技术性的东西——Apache Spark
什么是 Spark 和 PySpark,我可以用它做什么?
towardsdatascience.com](/explaining-technical-stuff-in-a-non-techincal-way-apache-spark-274d6c9f70e9)
py spark——需求预测数据科学项目
从预处理到建模的完整指南
https://upload.wikimedia.org/wikipedia/commons/f/f3/Apache_Spark_logo.svg
在本文中,我们将使用 Pyspark 构建一个逐步需求预测项目。这里,任务列表:
- 导入数据
- 过滤数据
- 功能工程(功能创建)
- 输入数据
- 功能工程(功能转换)
- 应用梯度增强树回归器
- 用 Kfold 和 GridSearch 方法优化模型
- 一次性
I)导入数据
首先,我们将使用预定义的模式导入数据。我在谷歌云平台的虚拟机上工作,数据来自云存储的一个桶。还是导入吧。
from pyspark.sql.types import *schema = StructType([
StructField("DATE", DateType()),
StructField("STORE", IntegerType()),
StructField("NUMBERS_OF_TICKETS", IntegerType()),
StructField("QTY", IntegerType()),
StructField("CA", DoubleType()),
StructField("FORMAT", StringType())])df = spark.read.csv("gs://my_bucket/my_table_in_csv_format", header = 'true', schema=schema)
II)过滤数据
然后,我们将应用一些过滤器,我们将只在大型超市工作,并确保数据中没有负数量或缺少日期。
df = df.filter(
(F.col("QTY") > 0)
&(F.col("DATE").notNull())
& (F.col("DATE").between("2015-01-01", "2019-01-01"))
& (~F.col("FORMAT").isin(["PRO", "SUP"]))
)
III)特征工程(特征创建)
然后,我们将定义一些对建模有用的变量,比如日期导数。
df = (df
.withColumn('yearday', F.dayofyear(F.col("DATE")))
.withColumn('Month', F.Month(F.col('DATE')))
.withColumn('dayofweek', F.dayofweek(F.col('DATE')))
.withColumn('YEAR', F.year(F.col('DATE')))
.withColumn('QUARTER', F.q(F.col('DATE')))
.withColumn('Month', F.Month(F.col('DATE')))
.withColumn('WeekOfYear', F.weekofyear(F.col('DATE')))
.withColumn('Week', F.date_trunc('week',F.col('DATE')))
.withColumn('MonthQuarter', F.when((df[DATE] <= 8), 0) .otherwise(F.when((df['DATE'] <= 16), 1) .otherwise(F.when((df['DATE'] <= 24), 2) .otherwise(3))))
)
现在,我们将计算每个商店的参考数量,即 1 年前同一天的销售量。然而,这一天可能没有销售,所以我们将在这个参考日期前后平均 7 天。这个函数会更复杂,需要 numpy。确实有可能把 numpy 和 spark 说清楚,让我们看看怎么做。
首先,必须定义一个用户定义的函数,以矢量化和稀疏的方式从每个存储中提取时间序列。我们将定义一个函数来创建一个稀疏向量,该向量以一年中的天数和相关量的值为索引
好吧,让我们休息一下,他有一些事情要解释。
- 输入:我们要求一个日期索引和相关数量值的列表
- 输出:我们返回一个按日索引的稀疏向量,它允许我们找到与他的日索引相关的数量
整个过程通过一个 UDF,并期望成为一个[VectorUDT](https://spark.apache.org/docs/2.1.2/api/java/org/apache/spark/mllib/linalg/VectorUDT.html)
简洁地说,这是一种可以被 UDF 操纵的向量。
然后,为了响应函数请求的输入,我们将每年创建一个聚合数据帧,存储并应用一个 collect_list 到日期和数量。正如您在下面看到的,我们恢复了商店当年的所有日数量值。这两个列表进入我们的 UDF 来创建想要的向量,现在我们可以在 numpy 中处理这个向量了!
现在我们可以定义一个作用于这个向量的函数,并再次把它放到 UDF 中。
并应用它:
df= (df
.join(self_join
, ([self_join.p_id_store == df.STORE, self_join.year_join == df.year]),
how = "left"
)
.withColumn("qty_reference", getReference(F.col("yearday"), F.col("qties_vectorized")))
)
让我们详细描述一下这个函数,首先它试图找到准确的参考日,我们已经将数据帧和它的前一年连在一起,所以我们希望当前日等于向量中的索引日并得到值。如果无法检索该值,我们将围绕该关键日期定义一个窗口,并对该窗口的值进行平均。
最后的结果是:
但是我们不会让它们像那样被硬编码,而是将所有东西都打包到一个 Spark 管道中。为此,我们将创建一个继承自 Spark Transformer 对象的类模板,如下所示,我们将为每个变量重复这个模板。我不打算在本文中写完整的功能管道,但是你可以在 github 的代码中找到它。我们将在本文的结尾看到如何将这个特性的管道放在一起,并将其插入到流程中。
关于这个类模板特性的更多细节,请参阅我的文章…:https://towardsdatascience . com/py spark-wrap-your-feature-engineering-in-a-a-pipeline-ee 63 BDB 913
IV)输入数据
让我们检查是否有一些丢失的值。
df.select([F.count(F.when(F.isnan(c) | F.col(c).isNull(), c)).alias(c) for c in df.columns]).show()
可能不会有更多,但在插入新变量后可能会有一些,因此我们将定义一个将在变量创建管道后出现的估算器。
from pyspark.ml.feature import Imputer
imputer = Imputer(
inputCols=df.columns,
outputCols=["{}_imputed".format(c) for c in df.columns]
)#imputer.fit(df).transform(df)
v)特征工程(特征转换)
从下面的表格摘录中,我将向您展示在插入预测算法之前,我们将如何进行其余的数据转换。
我们将在最后一次性完成整个过程。
# needed importfrom pyspark.ml import Pipeline
from pyspark.ml.feature import PCA
from pyspark.ml.feature import StringIndexer, OneHotEncoder, VectorAssembler
索引
Spark [String Indexer](https://spark.apache.org/docs/latest/ml-features#stringindexer)
将一列标签编码成一列标签索引。索引在[0,numLabels 中。映射首先由最高频率完成。
indexers = [ StringIndexer(inputCol=c, outputCol="{0}_indexedd".format(c), handleInvalid = 'error') for c in categorical_col]pip = Pipeline(stages = indexers)
fitted_df =pip.fit(df)
df = fitted_df.transform(df)
OneHotEncoding
Spark 的[OneHotEncoder](https://spark.apache.org/docs/latest/ml-features#onehotencoder)
One-hot 编码将表示为标签索引的分类特征映射到一个二进制向量,该向量最多具有一个单值,指示所有特征值集合中特定特征值的存在。对于字符串类型的输入数据,通常首先使用 StringIndexer 对分类特征进行编码。
indexers = [ StringIndexer(inputCol=c, outputCol="{0}_indexedd".format(c), handleInvalid = 'error') for c in categorical_col]encoders = [OneHotEncoder(dropLast=True,inputCol=indexer.getOutputCol(),
outputCol="{0}_encodedd".format(indexer.getOutputCol())) for indexer in indexers]pip = Pipeline(stages = indexers + encoders)
fitted_df =pip.fit(df)
df = fitted_df.transform(df)
PCA 简化和向量
在执行我们的管道之前,让我们检查一下在我们的流程中是否有任何令人讨厌的丢失值。在运行降维算法之前,我们必须将所有变量传递到一个向量汇编器中,该汇编器将返回所有数据的稀疏表示。然后,PCA 算法将采用这个向量来“简化”它,并在 dataframe 列中返回另一个稀疏表示。
我们看到了如何索引、一键编码我们的数据、应用主成分分析并将所有内容放入准备建模的向量中。显然还有其他的可能性来标准化、规范化…等等。然而,全球原则保持不变。现在我们的特征已经有了很好的形状,可以通过 Pyspark 算法进行建模。
- 分割数据集
X_train = final_dataset.filter(F.col('DATE').between("2015-01-02", "2018-06-01"))X_test = final_dataset.filter(F.col('DATE') > "2018-06-01")X_train = X_train.withColumn(target, F.log1p(F.col(target)))X_test = X_test.withColumn(target, F.log1p(F.col(target)))
VI)应用梯度增强树回归器
我们将训练一个梯度提升树模型,对商店每天销售的总数量进行回归。
target = 'QTY'gbt = GBTRegressor(featuresCol = 'Features', labelCol=target)fitted = gbt.fit(X_train)yhat = (fitted.transform(X_test)
.withColumn("prediction", F.expm1(F.col("prediction")))
.withColumn(target, F.expm1(F.col(target)))
).select(F.col("prediction"), F.col("STORE").alias('SID_STORE'), F.col("DATE").alias("ID_DAY")).show()
计算 KPI
我们将定义一个 python 对象,以此为基础在不同的指标上评估我们的模型。
eval_ = RegressionEvaluator(labelCol= target, predictionCol= "prediction", metricName="rmse")rmse = eval_.evaluate(yhat)
print('rmse is %.2f', %rmse)mae = eval_.evaluate(yhat, {eval_.metricName: "mae"})
print('mae is %.2f', %mae)r2 = eval_.evaluate(yhat, {eval_.metricName: "r2"})
print('R² is %.2f', %r2)
- r 是 0.84
- 均方根误差为 20081.54
- mae 是 13289.10
模型看起来不错,我们会努力改进它。
VII)使用 Kfold 和 GridSearch 方法优化模型
我们将尝试使用不同的参数来优化我们的 GBDT,并制作一个 kfold 以确保其鲁棒性。
from pyspark.ml.tuning import CrossValidator, ParamGridBuilderparamGrid = (ParamGridBuilder()
.addGrid(gbt.maxDepth, [5, 8, 10, 12])
.addGrid(gbt.maxBins, [32, 64])
.build())cv = CrossValidator(estimator=gbt,
estimatorParamMaps=paramGrid,
evaluator=eval_,
numFolds=3) cvModel = cv.fit(X_train)yhat = (cvModel.transform(X_test)
.withColumn("prediction", F.expm1(F.col("prediction")))
.withColumn(target, F.expm1(F.col(target)))
)
我只多了 0.3 分,但这已经足够了!😃
附加:特性重要性:)
fi = fitted.featureImportances.toArray()import pandas as pdfeatures = [encoder.getOutputCol() for encoder in encoders] + \
[x +'_imputed' for x in numeric_col] + ['day', 'month', 'weekday', 'weekend', 'monthend', 'monthbegin', 'monthquarter', 'yearquarter']feat_imp = (pd.DataFrame(dict(zip(features, fi)), range(1))
.T.rename(columns={0:'Score'})
.sort_values("Score", ascending =False)
)
VIII)一次性
名词(noun 的缩写)b:在 features_utils 包中有所有与特性管道相关的类。
最后
本文总结了我对一个数据科学项目的 Pyspark 各种砖块的简要介绍。我希望它们能帮助哪怕是一个人的工作。Pyspark 是一个非常强大的大容量工具。他不具备像 sklearn 那样的一系列算法,但他拥有主要的算法和许多资源。
如果您想让我为 Pyspark 做些什么,请随时告诉我,谢谢!
【Y】You 可以在这里找到代码*😗https://github.com/AlexWarembourg/Medium
py spark–特征工程(P1)
计算、聚合、转换任何数据
https://upload.wikimedia.org/wikipedia/commons/f/f3/Apache_Spark_logo.svg
在本文中,我们将看到如何通过连接、窗口函数、UDF 和向量操作来计算新变量。
提醒一下,下面是我们使用的表格:
。使用列和 sql 函数
要在 Spark 上创建新的列,只需传递函数。withColumn 并添加 sql 函数
df = (df
.withColumn('dayofyear', F.dayofyear(F.col("ID_DAY")))
.withColumn('Month', F.Month(F.col('ID_DAY')))
.withColumn('ratio_ticket_qty', F.col('F_TOTAL_QTY')/F.col('NB_TICKET'))
)
更多请看这里: SQL 函数
。条件为 f 的列
与 sql 一样,可以使用 when 框进行条件计算。
df = df.withColumn("day_to_xmas", F.when((F.col("ID_DAY").between("2018-12-01", "2018-12-31")) | (F.col('ID_DAY').between("2019-12-01", "2019-12-31")),
F.lit('xmas_is_coming')).otherwise(F.datediff(F.col("ID_DAY"), F.lit('2018-12-01').cast(DateType())))
)
。带列和窗口功能
窗口函数对于计算时间轴上的值或保存用户连接非常有用
grouped_windows = Window.partitionBy(F.col('SID_STORE'), F.col('Month'))rolling_windows = (Window.orderBy(F.col("dayofyear").cast(IntegerType())).rangeBetween(-7, 0))df = (df
.withColumn('rolling_average', F.avg("F_TOTAL_QTY").over(rolling_windows))
.withColumn('monthly_qty', F.avg('F_TOTAL_QTY').over(grouped_windows))
)
只是一个连接
我们可以通过连接对 monthly_qty 进行与 windows 函数完全相同的计算。
month_qty = df.groupBy('SID_STORE', 'Month').agg(F.avg('F_TOTAL_QTY').alias('monthly_qty_by_join'))
df = df.join(month_qty, how = "left", on = ["SID_STORE", "Month"])
用户定义的函数(UDF)
我们可以像在 python 上一样在 pyspark 上定义函数,但它不会(直接)与我们的 spark 数据框架兼容。为此,我们需要定义一个 UDF(用户定义的函数),它将允许我们在 Spark 数据帧上应用我们的函数。缺点是 UDF 可能很长,因为它们是逐行应用的。要应用一个 UDF,只需将它作为函数的修饰者,用一种与其输出相关联的数据类型来添加即可。
**from** pyspark.sql.functions **import** udf@udf("long")
**def** squared_udf(s):
**return** s * sdf = df.withColumn('squared_col', squared_udf(F.col('my_columns')))
仅此而已。
向量 UDT 和 numpy 处理复杂数组操作
现在,我们将计算每个商店的参考数量,即 1 年前同一天的销售量。然而,这一天可能没有销售,所以我们将在这个参考日期前后平均 7 天。这个功能会更复杂,需要 numpy。T t 确实有可能把 numpy 和 spark 说清楚,我们来看看怎么做。
首先,必须定义一个用户定义的函数,以矢量化和稀疏的方式从每个存储中提取时间序列。我们之前已经了解了什么是 UDF,我们将定义一个稀疏向量,它用一年中的天数和相关量的值来索引
好吧,让我们休息一下,他有一些事情要解释。
- 输入:我们要求一个日期索引和相关数量值的列表
- 输出:我们返回一个按日索引的稀疏向量,这允许我们找到与他的日索引相关的数量
整个过程通过一个 UDF,并期望成为一个[VectorUDT](https://spark.apache.org/docs/2.1.2/api/java/org/apache/spark/mllib/linalg/VectorUDT.html)
简洁地说,这是一种可以被 UDF 操纵的向量。
然后,为了响应函数请求的输入,我们将每年创建一个聚合数据帧,存储并应用一个 collect_list 到日期和数量。正如您在下面看到的,我们恢复了商店当年的所有日数量值。这两个列表进入我们的 UDF 来创建想要的向量,现在我们可以在 numpy 中处理这个向量了!
现在我们可以定义一个作用于这个向量的函数,并再次把它放到 UDF 中。
并应用它:
df= (df
.join(self_join
, ([self_join.p_id_store == df.SID_STORE, self_join.year_join == df.year]),
how = "left"
)
.withColumn("qty_reference", getReference(F.col("yearday"), F.col("qties_vectorized")))
)
让我们详细描述一下这个函数,首先它试图找到准确的参考日,我们已经将数据帧和它的前一年连接在一起,所以我们希望当前日等于向量中的索引日并得到值。如果无法检索该值,我们将围绕该关键日期定义一个窗口,并对该窗口的值进行平均。
最后的结果是:
更快的…更强壮的…再进一步!将您的特征工程包装在管道中!
在这里,我写的另一篇文章介绍了如何创建一个定制管道来一次性集成所有这些特性:
最后
现在,您已经构建了令人惊叹的特征,您需要索引您的分类特征,或者对它们进行一次性编码,并且可能应用一些特征缩减,然后将所有内容打包到一个向量组装器中进行建模!不介意这是下一篇:)
【Y】You 可以在这里找到代码*😗https://github.com/AlexWarembourg/Medium
面向数据科学工作流的 PySpark
生产中的数据科学第 6 章
在 PySpark 中展示的经验是雇主在建立数据科学团队时最需要的能力之一,因为它使这些团队能够拥有实时数据产品。虽然我之前已经写过关于【py spark】*并行化 和UDF的博客,但我想作为本书第 章提供一个关于这个主题的适当概述。我分享这完整的一章,因为我想鼓励数据科学家采用 PySpark 作为工具。这篇文章中的所有代码示例都可以在 这里 找到,所有先决条件都包含在示例章节 这里 中。你可能想在潜水前吃点零食!*
6 PySpark 用于批量管道
Spark 是一个通用计算框架,可以扩展到海量数据。它建立在 Hadoop 和 MapReduce 等先前的大数据工具之上,同时在其支持的语言的表达能力方面提供了显著的改进。Spark 的核心组件之一是弹性分布式数据集(RDD),它使机器集群能够在一个协调、容错的过程中执行工作负载。在 Spark 的最新版本中,Dataframe API 在 rdd 之上提供了一个抽象,类似于 R 和 Pandas 中的相同数据结构。PySpark 是 Spark 的 Python 接口,它提供了在分布式计算环境中处理大规模数据集的 API。
PySpark 对于数据科学家来说是一个非常有价值的工具,因为它可以简化将原型模型转化为生产级模型工作流的过程。在 Zynga,我们的数据科学团队拥有许多生产级系统,为我们的游戏和营销团队提供有用的信号。通过使用 PySpark,我们已经能够减少工程团队从概念到生产扩大模型所需的支持量。
到目前为止,在本书中,我们构建和部署的所有模型都是针对单台机器的。虽然我们能够使用 Lambda、ECS 和 GKS 将模型服务扩展到多台机器,但是这些容器是孤立工作的,并且在这些环境中的节点之间没有协调。使用 PySpark,我们可以构建模型工作流,这些工作流设计为在集群环境中运行,用于模型训练和模型服务。结果是,数据科学家现在可以解决比以前使用 Python 工具可能解决的大得多的问题。PySpark 在一种富于表现力的编程语言和 API 之间提供了一个很好的折衷,Spark 与 MapReduce 等更传统的选项相比。总的趋势是,随着更多的数据科学和工程团队转向 Spark 生态系统,Hadoop 的使用正在下降。在第 7 章中,我们将探索另一个用于数据科学的分布式计算生态系统,称为云数据流,但目前 Spark 是该领域的开源领导者。PySpark 是我在数据科学工作流中从 R 转向 Python 的主要动机之一。
本章的目的是向 Python 程序员介绍 PySpark,展示如何为批量评分应用程序构建大规模模型管道,其中可能有数十亿条记录和数百万用户需要评分。虽然生产级系统通常会将结果推送到应用程序数据库,但在本章中,我们将重点关注从数据湖中提取数据并将结果推回到数据湖以供其他系统使用的批处理过程。我们将探索为 AWS 和 GCP 执行模型应用程序的管道。虽然本章中使用的数据集依赖于 AWS 和 GCP 进行存储,但 Spark 环境不必运行在这些平台上,而是可以运行在 Azure、其他云或 on-pem Spark 集群上。
我们将在本章中讨论各种不同的主题,展示 PySpark 在可伸缩模型管道中的不同用例。在展示了如何在 S3 上为 Spark 提供数据之后,我们将重点介绍 PySpark 的一些基础知识,重点是数据帧操作。接下来,我们将构建一个预测模型管道,从 S3 读入数据,执行批量模型预测,然后将结果写入 S3。接下来,我们将展示一个名为 Pandas UDFs 的新功能如何与 PySpark 一起使用,以执行分布式深度学习和功能工程。最后,我们将使用 GCP 构建另一个批处理模型管道,然后讨论如何在 Spark 生态系统中实现工作流的产品化。
6.1 火花环境
配置 Spark 集群和向集群提交命令以供执行的方法有很多种。当作为一名数据科学家开始使用 PySpark 时,我的建议是使用一个免费的笔记本环境,以便尽快使用 Spark。虽然对于大规模工作流,PySpark 的性能可能不如 Java 或 Scala,但在交互式编程环境中开发的便利性是值得权衡的。
根据您的组织,您可能会从零开始使用 Spark,或者使用现有的解决方案。以下是我在实践中见过的 Spark 部署类型:
- ***自托管:*一个工程团队管理一组集群,并提供控制台和笔记本访问。
- 云解决方案: AWS 提供了一个名为 EMR 的托管 Spark 选项,GCP 有 Cloud DataProc。
- 供应商解决方案: Databricks、Cloudera 和其他供应商提供完全托管的 Spark 环境。
在选择 Spark 生态系统时,有许多不同的因素需要考虑,包括成本、可扩展性和功能集。当您使用 Spark 扩展团队规模时,还需要考虑生态系统是否支持多租户,即多个作业可以在同一个集群上并发运行,以及隔离,即一个作业失败不会影响其他作业。自托管解决方案需要大量的工程工作来支持这些额外的考虑,因此许多组织为 Spark 使用云或供应商解决方案。在本书中,我们将使用 Databricks 社区版,它提供了在协作笔记本环境中学习 Spark 所需的所有基本功能。
Spark 是一个快速发展的生态系统,随着平台的发展,很难写出关于这个主题的书籍不会很快过时。另一个问题是,对于大多数编码示例,许多书籍都将 Scala 作为目标,而不是 Python。我给想要更深入挖掘 Spark 生态系统的读者的建议是,探索基于更广泛的 Spark 生态系统的书籍,如(Karau et al. 2015 )。您可能需要通读 Scala 或 Java 代码示例,但是所涵盖的大部分内容都与 PySpark 相关。
火花簇
Spark 环境是一个机器集群,有一个驱动节点和一个或多个工作节点。驱动程序机器是集群中的主节点,负责协调要执行的工作负载。通常,在 Spark 数据帧上执行操作时,工作负载将分布在工作节点上。但是,当使用本机 Python 对象(如列表或字典)时,对象将在驱动程序节点上实例化。
理想情况下,您希望所有的工作负载都在 worker 节点上运行,以便要执行的步骤分布在整个集群中,而不会受到驱动程序节点的限制。然而,在 PySpark 中有一些类型的操作,驱动程序必须执行所有的工作。最常见的情况是在工作负载中使用 Pandas 数据帧。当您使用toPandas
或其他命令将数据集转换为 Pandas 对象时,所有数据都被加载到驱动程序节点的内存中,这可能会在处理大型数据集时使驱动程序节点崩溃。
在 PySpark 中,大多数命令都是延迟执行的,这意味着直到明确需要输出时才执行操作。例如,两个 Spark 数据帧之间的join
操作不会立即导致执行 join 操作,这就是 Pandas 的工作方式。相反,一旦将输出添加到要执行的操作链中,就执行连接,例如显示结果数据帧的样本。Pandas 操作之间的一个关键区别是,PySpark 操作是延迟执行的,直到需要时才被拉入内存。这种方法的一个好处是,要执行的操作图可以在发送到集群执行之前进行优化。
一般来说,Spark 集群中的节点应该被认为是短暂的,因为集群可以在执行过程中调整大小。此外,一些供应商在安排作业运行时可能会启动新的集群。这意味着 Python 中的常见操作,比如将文件保存到磁盘,不会直接映射到 PySpark。相反,使用分布式计算环境意味着您在保存数据时需要使用持久性文件存储,如 S3。这对于日志记录很重要,因为工作节点可能会崩溃,并且可能无法ssh
进入该节点进行调试。大多数 Spark 部署都有一个日志系统来帮助解决这个问题,但是将工作流状态记录到持久存储中是一个很好的做法。
6.1.2 Databricks 社区版
启动并运行 PySpark 的最快方法之一是使用托管笔记本环境。Databricks 是最大的 Spark 供应商,并提供了一个名为 Community Edition [ 13 的免费入门版本。我们将使用这个环境开始使用 Spark,并构建 AWS 和 GCP 模型管道。
第一步是在 Databricks 网站上为 community edition 创建一个登录名。接下来,在登录后执行以下步骤来启动测试集群:
- 单击左侧导航栏上的“Clusters”
- 单击“创建集群”
- 分配一个名称“DSP”
- 选择最新的运行时(非测试版)
- 单击“创建集群”
几分钟后,我们将建立一个集群,我们可以使用它来提交 Spark 命令。在将笔记本连接到集群之前,我们将首先设置将在本章中使用的库。我们不使用pip
来安装库,而是使用 Databricks UI,它确保集群中的每个节点都安装了相同的库集。我们将使用 Maven 和 PyPI 在集群上安装库。要安装 BigQuery 连接器,请执行以下步骤:
- 单击左侧导航栏上的“Clusters”
- 选择“DSP”集群
- 单击“库”选项卡
- 选择“安装新的”
- 点击“Maven”选项卡。
- 将坐标设置为
com.spotify:spark-bigquery_2.11:0.2.2
- 单击安装
然后,UI 会将状态显示为“正在解决”,然后是“正在安装”,然后是“已安装”。我们还需要附加一些 Python 库,这些库没有预安装在新的 Databricks 集群上。安装了 Pandas 等标准库,但您可能需要升级到更新的版本,因为 Databricks 预安装的库可能会明显滞后。
要在 Databricks 上安装 Python 库,请执行与前面步骤 5 相同的步骤。接下来,不选择“Maven ”,而是选择“PyPI”。在Package
下,指定要安装的软件包,然后点击“安装”。要遵循本章中的所有章节,您需要安装以下 Python 包:
- 考拉 —用于数据帧转换
- 特征工具——用于特征生成
- tensorflow —用于深度学习后端
- keras —深度学习模型
现在,您将拥有一个能够执行分布式特征工程和深度学习的集群设置。我们将从基本的 Spark 命令开始,展示新的功能,如Koalas
库,然后深入探讨这些更高级的主题。设置完成后,集群库的设置应该如图 6.1 所示。为了确保一切设置成功,请重新启动集群并检查已安装库的状态。
FIGURE 6.1: Libraries attached to a Databricks cluster.
现在我们已经配置了一个集群并设置了所需的库,我们可以创建一个笔记本来开始向集群提交命令。要创建新笔记本,请执行以下步骤:
- 点击左侧导航栏上的“数据块”
- 在“常见任务”下,选择“新建笔记本”
- 指定一个名称“CH6”
- 选择“Python”作为语言
- 选择“DSP”作为集群
- 点击“创建”
结果将是一个笔记本环境,您可以开始运行 Python 和 PySpark 命令,比如print("Hello World!")
。运行该命令的笔记本示例如图 6.2 所示。我们现在有了一个 PySpark 环境,可以用来构建分布式模型管道。
FIGURE 6.2: Running a Python command in Databricks.
6.2 暂存数据
数据对于 PySpark 工作流至关重要。Spark 支持各种读入数据集的方法,包括连接到数据湖和数据仓库,以及从库中加载样本数据集,比如波士顿住房数据集。由于本书的主题是构建可伸缩的管道,我们将重点关注使用与分布式工作流一起工作的数据层。为了开始使用 PySpark,我们将为 S3 上的建模管道准备输入数据,然后将数据集作为 Spark 数据帧读入。
本节将展示如何将数据转移到 S3,如何设置从 Spark 访问数据的凭证,以及如何将数据从 S3 提取到 Spark 数据帧中。第一步是在 S3 上设置一个存储桶,用于存储我们想要加载的数据集。要执行此步骤,请在命令行上运行以下操作。
*aws s3api create-bucket --bucket dsp-ch6 --region us-east-1
aws s3 ls*
在运行命令创建一个新的 bucket 之后,我们使用ls
命令来验证 bucket 是否已经成功创建。接下来,我们将游戏数据集下载到 EC2 实例,然后使用cp
命令将文件移动到 S3,如下面的代码片段所示。
*wget https:**//**github.com/bgweber/Twitch/raw/master/
Recommendations/games-expand.csv
aws s3 cp games-expand.csv s3:**//**dsp-ch6/csv/games-expand.csv*
除了将游戏数据集转移到 S3,我们还将从 Kaggle NHL 数据集复制 CSV 文件的子集,这是我们在第 1.5.2 节中设置的。运行以下命令,将来自 NHL 数据集的 plays 和 stats CSV 文件转移到 S3。
*aws s3 cp game_plays.csv s3:**//**dsp-ch6/csv/game_plays.csv
aws s3 cp game_skater_stats.csv
s3:**//**dsp-ch6/csv/game_skater_stats.csv
aws s3 ls s3:**//**dsp-ch6/csv/*
我们现在有了本章中代码示例所需的所有数据集。为了从 Spark 读入这些数据集,我们需要设置 S3 凭证,以便与 Spark 集群中的 S3 进行交互。
S3 全权证书
对于生产环境,最好使用 IAM 角色来管理访问,而不是使用访问键。然而,Databricks 的社区版限制了允许多少配置,所以我们将使用访问键来启动和运行本章中的示例。我们已经设置了一个从 EC2 实例访问 S3 的用户。要创建一组以编程方式访问 S3 的凭据,请从 AWS 控制台执行以下步骤:
- 搜索并选择“IAM”
- 点击“用户”
- 选择在第 3.3.2 节“S3 _ 拉姆达”中创建的用户
- 单击“安全凭证”
- 单击“创建访问密钥”
结果将是允许访问 S3 的访问密钥和秘密密钥。请将这些值保存在安全的位置,因为我们将在笔记本中使用它们来连接到 S3 上的数据集。完成本章后,建议您撤销这些凭据。
现在我们已经为访问设置了凭证,我们可以返回到 Databricks 笔记本来读取数据集。要启用对 S3 的访问,我们需要在集群的 Hadoop 配置中设置访问密钥和秘密密钥。要设置这些键,运行下面代码片段中显示的 PySpark 命令。您需要用我们刚刚为S3_Lambda
角色创建的凭证替换访问和秘密密钥。
*AWS_ACCESS_KEY = "AK..."
AWS_SECRET_KEY = "dC..."**sc._jsc.hadoopConfiguration**()**.set**(
"fs.s3n.awsAccessKeyId", AWS_ACCESS_KEY)
**sc._jsc.hadoopConfiguration**()**.set**(
"fs.s3n.awsSecretAccessKey", AWS_SECRET_KEY)*
我们现在可以使用read
命令将数据集读入 Spark 数据帧,如下所示。该命令使用spark
上下文发布一个读取命令,并使用 CSV 输入读取器读取数据集。我们还指定 CSV 文件包含一个标题行,并希望 Spark 推断列的数据类型。当读入 CSV 文件时,Spark 急切地将数据集提取到内存中,这可能会导致较大数据集的问题。在处理大型 CSV 文件时,最佳做法是将大型数据集分割成多个文件,然后在输入路径中使用通配符读入文件。当使用其他文件格式时,比如 Parquet 或 AVRO,Spark 会缓慢地获取数据集。
*games_df = **spark.read.csv**("s3://dsp-ch6/csv/games-expand.csv",
header=True, inferSchema = True)
**display**(games_df)*
上面代码片段中的display
命令是 Databricks 提供的一个实用函数,它对输入数据帧进行采样,并显示该帧的表格表示,如图 6.3 所示。它类似于 Pandas 中的head
功能,但是提供了额外的功能,例如将采样数据帧转换为绘图。我们将在 6.3.3 节探索绘图功能。
FIGURE 6.3: Displaying the Dataframe in Databricks.
既然我们已经将数据加载到 Spark 数据框架中,我们就可以开始探索 PySpark 语言了,它使数据科学家能够构建生产级的模型管道。
6.3 py spark 底漆
PySpark 是一种用于探索性分析和构建机器学习管道的强大语言。PySpark 中的核心数据类型是 Spark 数据帧,它类似于 Pandas 数据帧,但被设计为在分布式环境中执行。虽然 Spark Dataframe API 确实为 Python 程序员提供了一个熟悉的接口,但是在向这些对象发出的命令的执行方式上有很大的不同。一个关键的区别是 Spark 命令是延迟执行的,这意味着像iloc
这样的命令在这些对象上是不可用的。虽然使用 Spark 数据框架可能看起来有局限性,但好处是 PySpark 可以扩展到比 Pandas 大得多的数据集。
本节将介绍 Spark 数据帧的常见操作,包括持久化数据、在不同数据帧类型之间转换、转换数据帧以及使用用户定义的函数。我们将使用 NHL stats 数据集,它提供了每场比赛的玩家表现的用户级摘要。要将该数据集作为 Spark 数据帧加载,请运行下面代码片段中的命令。
*stats_df = **spark.read.csv**("s3://dsp-ch6/csv/game_skater_stats.csv",
header=True, inferSchema = True)
**display**(stats_df)*
持久化数据帧
PySpark 中的一个常见操作是将数据帧保存到持久存储中,或者从存储层读入数据集。虽然 PySpark 可以处理 Redshift 等数据库,但在使用 S3 或 GCS 等分布式文件存储时,它的性能要好得多。在本章中,我们将使用这些类型的存储层作为模型管道的输出,但是将数据转移到 S3 作为工作流中的中间步骤也很有用。例如,在 Zynga 的 AutoModel [ 14 ]系统中,我们在使用 MLlib 训练和应用模型预测之前,将特征生成步骤的输出暂存到 S3。
要使用的数据存储层取决于您的云平台。对于 AWS,S3 与 Spark 在分布式数据读写方面配合得很好。当使用 S3 或其他数据湖时,Spark 支持各种不同的文件格式来保存数据。在使用 Spark 时,Parquet 是典型的行业标准,但是除了 CSV 之外,我们还将探讨 Avro 和 ORC。Avro 是流数据管道的更好格式,ORC 在处理传统数据管道时很有用。
为了展示 Spark 支持的数据格式范围,我们将把 stats 数据集写入 AVRO,然后是 Parquet,然后是 ORC,最后是 CSV。在执行了数据 IO 的往返之后,我们将最终得到我们的初始 Spark 数据帧。首先,我们将使用下面的代码片段以 Avro 格式保存 stats 数据帧。这段代码使用 Databricks Avro writer 将数据帧以 Avro 格式写入 S3,然后使用同一个库读入结果。执行这些步骤的结果是,我们现在有一个指向 S3 上的 Avro 文件的 Spark 数据帧。由于 PySpark 延迟评估操作,Avro 文件不会被拉至 Spark 集群,直到需要从该数据集创建输出。
**# AVRO write*
avro_path = "s3://dsp-ch6/avro/game_skater_stats/"
**stats_df.write.mode**('overwrite')**.format**(
"com.databricks.spark.avro")**.save**(avro_path)*# AVRO read*
avro_df = **sqlContext.read.format**(
"com.databricks.spark.avro")**.load**(avro_path)*
Avro 是基于记录的分布式文件格式,而 Parquet 和 OR 格式是基于列的。它对我们将在第 9 章探讨的流工作流很有用,因为它压缩了分布式数据处理的记录。将 stats 数据帧保存为 Avro 格式的输出如下面的代码片段所示,其中显示了将数据帧作为 Avro 保存到 S3 时生成的状态文件和数据文件的子集。像大多数可扩展的数据格式一样,Avro 将根据指定的分区将记录写入几个文件,以便实现高效的读写操作。
*aws s3 ls s3:**//**dsp-ch6/avro/game_skater_stats/
2019-11-27 23:02:43 1455 _committed_1588617578250853157
2019-11-27 22:36:31 1455 _committed_1600779730937880795
2019-11-27 23:02:40 0 _started_1588617578250853157
2019-11-27 23:31:42 0 _started_6942074136190838586
2019-11-27 23:31:47 1486327 part-00000-tid-6942074136190838586-
c6806d0e-9e3d-40fc-b212-61c3d45c1bc3-15-1-c000.avro
2019-11-27 23:31:43 44514 part-00007-tid-6942074136190838586-
c6806d0e-9e3d-40fc-b212-61c3d45c1bc3-22-1-c000.avro*
S3 上的 Parquet 目前是在 AWS 上构建数据湖的标准方法,Delta Lake 等工具正在利用这种格式来提供高度可伸缩的数据平台。Parquet 是一种面向列的文件格式,当一个操作只访问列的一个子集时,例如使用 Spark SQL 时,这种文件格式可以提高读取效率。Parquet 是 Spark 的原生格式,这意味着 PySpark 具有用于读写这种格式的文件的内置函数。
下面的代码片段展示了一个将 stats 数据帧写成 Parquet 文件,并将结果读入一个新的数据帧的例子。在本例中,我们没有设置分区键,但是与 Avro 一样,数据帧将被分割成多个文件,以便支持高性能的读写操作。当处理大规模数据集时,使用repartition
函数为文件导出设置分区键是很有用的。在本节之后,我们将使用 Parquet 作为使用 Spark 时的主要文件格式。
**# parquet out*
parquet_path = "s3a://dsp-ch6/games-parquet/"
**avro_df.write.mode**('overwrite')**.parquet**(parquet_path)*# parquet in*
parquet_df = **sqlContext.read.parquet**(parquet_path)*
ORC 是另一种与 Spark 配合良好的列格式。与 Parquet 相比,它的主要优势是可以支持更高的压缩率,但代价是增加了计算成本。我将它包含在本章中,因为一些遗留系统仍然使用这种格式。将 stats 数据帧写入 ORC 并将结果读回 Spark 数据帧的示例如下面的代码片段所示。与 Avro 格式一样,ORC write 命令会根据大小将数据帧分配给多个文件。
**# orc out*
orc_path = "s3a://dsp-ch6/games-orc/"
**parquet_df.write.mode**('overwrite')**.orc**(orc_path)*# orc in*
orc_df = **sqlContext.read.orc**(orc_path)*
为了完成文件格式的往返,我们将以 CSV 格式将结果写回 S3。为了确保我们编写的是单个文件而不是一批文件,我们将使用 coalesce 命令将数据收集到单个节点,然后再导出。这是一个在处理大型数据集时会失败的命令,通常在使用 Spark 时最好避免使用 CSV 格式。然而,CSV 文件仍然是共享数据的常用格式,因此了解如何导出到这种格式是很有用的。
**# CSV out*
csv_path = "s3a://dsp-ch6/games-csv-out/"
**orc_df.coalesce**(1)**.write.mode**('overwrite')**.format**(
"com.databricks.spark.csv")**.option**("header","true")**.save**(csv_path)
*# and CSV to finish the round trip*
csv_df = **spark.read.csv**(csv_path, header=True, inferSchema = True)*
产生的数据帧与我们第一次从 S3 读入的数据帧相同,但是如果数据类型不是很容易推断,那么 CSV 格式可能会导致问题。使用 PySpark 持久化数据时,最好使用描述持久化数据模式的文件格式。
转换数据帧
虽然在创作 PySpark 工作负载时最好使用 Spark 数据帧,但通常有必要根据您的用例在不同格式之间进行转换。例如,您可能需要执行 Pandas 操作,比如从 dataframe 中选择特定的元素。需要时,您可以使用toPandas
功能将火花数据帧拉入驱动节点的存储器中。下面的代码片段显示了如何执行这个任务,显示结果,然后将 Pandas 数据帧转换回 Spark 数据帧。一般来说,在编写 PySpark 工作流时,最好避免使用 Pandas,因为它会阻止分发和扩展,但这通常是表达要执行的命令的最佳方式。
*stats_pd = **stats_df.toPandas**()stats_df = **sqlContext.createDataFrame**(stats_pd)*
为了弥合 Pandas 和 Spark 数据帧之间的差距,Databricks 引入了一个名为 Koalas 的新库,它类似于 Pandas 用于 Spark 支持的数据帧的 API。结果是,您可以编写与 Pandas 命令一起使用的 Python 代码,这些命令可以扩展到 Spark 级别的数据集。下面的代码片段展示了一个将 Spark 数据帧转换成考拉数据帧再转换回 Spark 数据帧的示例。将 stats 数据帧转换为考拉数据帧后,该代码片段显示了如何计算在冰上的平均时间以及考拉数据帧的索引。考拉的目的是为 Spark 数据帧提供一个 Pandas 接口,随着考拉库的成熟,更多的 Python 模块可能会利用 Spark。代码片段的输出显示,每场比赛在冰上的平均时间是 993 秒。
*import databricks.koalas as ksstats_ks = **stats_df.to_koalas**()
stats_df = **stats_ks.to_spark**()**print**(stats_ks['timeOnIce']**.mean**())
**print**(stats_ks.iloc[:1, 1:2])*
在本书的开发过程中,考拉仍然是初步的,只是部分实现,但它看起来为 Python 编码人员提供了一个熟悉的接口。熊猫和 Spark 数据帧都可以与考拉一起工作,下面的片段显示了如何从 Spark 到考拉再到熊猫再到 Spark,以及 Spark 到熊猫再到考拉再到 Spark。
**# spark -> koalas -> pandas -> spark*
df = **sqlContext.createDataFrame**(**stats_df.to_koalas**()**.toPandas**())*# spark -> pandas -> koalas -> spark*
df = **ks.from_pandas**(**stats_df.toPandas**())**.to_spark**()*
一般来说,在 PySpark 环境中编写代码时,您将使用 Spark 数据帧。但是,能够根据需要使用不同的对象类型来构建模型工作流是非常有用的。考拉和熊猫 UDF 为将工作负载移植到大规模数据生态系统提供了强大的工具。
转换数据
PySpark Dataframe API 为聚合、过滤、透视和汇总数据提供了各种有用的函数。虽然其中一些功能可以很好地映射到 Pandas 操作,但是我建议在 PySpark 中快速启动和运行 munging 数据的方法是使用名为 Spark SQL 的 SQL 接口来处理 Spark 中的数据帧。如果您已经在使用pandasql
或framequery
库,那么 Spark SQL 应该会提供一个熟悉的接口。如果您不熟悉这些库,那么 SQL 接口仍然提供了一种使用 Spark 生态系统的简单方法。我们将在本节稍后讨论 Dataframe API,但是首先从 SQL 接口开始启动和运行。
探索性数据分析(EDA)是数据科学工作流中理解数据集形状的关键步骤之一。为了在 PySpark 中完成这个过程,我们将把 stats 数据集加载到 dataframe 中,将其作为视图公开,然后计算汇总统计数据。下面的代码片段展示了如何加载 NHL stats 数据集,将其作为 Spark 的视图公开,然后对 dataframe 运行查询。然后使用 Databricks 中的display
命令可视化聚合的数据帧。
*stats_df = **spark.read.csv**("s3://dsp-ch6/csv/game_skater_stats.csv",
header=True, inferSchema = True)
**stats_df.createOrReplaceTempView**("stats")new_df = **spark.sql**("""
select player_id, sum(1) as games, sum(goals) as goals
from stats
group by 1
order by 3 desc
limit 5
""")**display**(new_df)*
该代码块的输出如图 6.4 所示。它通过根据进球总数对结果进行排名,显示了 NHL 数据集中得分最高的球员。Spark 的一个强大特性是,在需要结果集之前,SQL 查询不会对数据帧进行操作。这意味着笔记本中的命令可以为 Spark 数据帧设置多个数据转换步骤,直到后面的步骤需要执行代码块中定义的操作图时才执行这些步骤。
FIGURE 6.4: Summarizing player activity.
Spark SQL 表达能力强、速度快,是我在 Spark 环境中处理大数据集的首选方法。虽然以前的 Spark 版本在使用 Dataframe API 时比 Spark SQL 表现得更好,但性能上的差异现在已经微不足道了,您应该使用能够为处理大型数据集提供最佳迭代速度的转换工具。使用 Spark SQL,您可以连接数据帧、运行嵌套查询、设置临时表,以及混合使用 Spark 操作和 SQL 操作。例如,如果您想要查看 NHL stats 数据中的进球与射门的分布,您可以在 dataframe 上运行以下命令。
***display**(**spark.sql**("""
select cast(goals/shots * 50 as int)/50.0 as Goals_per_shot
,sum(1) as Players
from (
select player_id, sum(shots) as shots, sum(goals) as goals
from stats
group by 1
having goals >= 5
)
group by 1
order by 1
"""))*
该查询将进球数与射门数的比率限制为超过 5 个进球的球员,以防止出现异常值,如守门员在强攻中得分。我们将使用display
命令将结果集输出为表格,然后使用 Databricks 将输出显示为图形。许多 Spark 生态系统都有可视化结果的方法,Databricks 环境通过display
命令提供了这种能力,它可以很好地处理表格和透视表数据。运行上述命令后,您可以点击图表图标并选择显示进球与射门分布的尺寸和度量,如图 6.5 所示。
FIGURE 6.5: Distribution of goals per shot.
虽然我提倡使用 SQL 来转换数据,因为它可以扩展到不同的编程环境,但是熟悉 PySpark 中的一些基本数据帧操作是很有用的。下面的代码片段显示了如何执行常见操作,包括删除列、选择列的子集以及向 Spark 数据帧添加新列。像前面的命令一样,所有这些操作都是延迟执行的。与 Pandas 有一些语法差异,但是用于转换数据集的一般命令应该是熟悉的。
*from pyspark.sql.functions import lit*# dropping columns*
copy_df = **stats_df.drop**('game_id', 'player_id')*# selection columns*
copy_df = **copy_df.select**('assists', 'goals', 'shots')*# adding columns*
copy_df = **copy_df.withColumn**("league", **lit**('NHL'))
**display**(copy_df)*
在数据帧之间执行的一个常见操作是连接。这很容易在 Spark SQL 查询中表达,但有时最好用 Dataframe API 以编程方式来实现。下面的代码片段显示了当在game_id
和player_id
字段上连接时,如何将两个数据帧连接在一起。作为文字的league
列将与 stats 数据帧的其余部分连接在一起。这是一个简单的例子,我们在一个小数据帧上添加了一个新列,但是 Dataframe API 的连接操作可以扩展到大规模数据集。
*copy_df = **stats_df.select**('game_id', 'player_id').
**withColumn**("league", **lit**('NHL'))
df = **copy_df.join**(stats_df, ['game_id', 'player_id'])
**display**(df)*
上述连接操作的结果集如图 6.6 所示。Spark 支持各种不同的连接类型,在本例中,我们使用了一个内部连接将 league 列附加到 stats 数据帧中。
FIGURE 6.6: The dataframe resulting from the join.
还可以在数据帧上执行聚合操作,例如计算列的总和和平均值。下面的代码片段显示了一个在统计数据集中计算球员平均上场时间和总进球数的例子。groupBy
命令使用player_id
作为折叠数据集的列,而agg
命令指定要执行的聚合。
*summary_df = **stats_df.groupBy**("player_id")**.agg**(
{'timeOnIce':'avg', 'goals':'sum'})
**display**(summary_df)*
该代码片段创建了一个包含player_id
、timeOnIce
和goals
列的 dataframe。我们将再次使用 Databricks 中的绘图功能来可视化结果,但这次选择散点图选项。图 6.7 显示了冰上目标与时间的关系。
FIGURE 6.7: Time on ice and goal scoring plots.
我们已经通过介绍性示例在 PySpark 中启动和运行数据框架,重点关注在训练机器学习模型之前对数据进行管理的有用操作。这些类型的操作与读取和写入数据帧相结合,为在海量数据集上执行探索性分析提供了一组有用的技能。
6.3.4 熊猫 UDF
虽然 PySpark 提供了大量处理数据帧的功能,但它通常缺少 Python 库中提供的核心功能,例如 SciPy 中的曲线拟合功能。虽然可以使用toPandas
函数将数据帧转换成 Python 库的 Pandas 格式,但这种方法在使用大型数据集时会失效。Pandas UDFs 是 PySpark 中的一个新特性,通过将 Pandas 转换分布在 Spark 集群中的工作节点上,帮助数据科学家解决这个问题。使用 Pandas UDF,您可以通过操作定义一个 group by,将数据集划分为足够小的数据帧,以适合工作节点的内存,然后创建一个函数,将 Pandas 数据帧作为输入参数,并返回转换后的 Pandas 数据帧作为结果。在幕后,PySpark 使用 PyArrow 库高效地将数据帧从 Spark 翻译到 Pandas,并从 Pandas 翻译回 Spark。这种方法使 Python 库(如 Keras)能够扩展到大型机器集群。
本节将通过一个示例问题,我们需要使用现有的 Python 库,并展示如何使用 Pandas UDFs 将工作流转换为可伸缩的解决方案。我们希望回答的问题是理解统计数据集中的shots
和hits
属性之间是正相关还是负相关。为了计算这个关系,我们可以使用 SciPy 中的leastsq
函数,如下面的代码片段所示。该示例为单个 player_id 创建了一个 Pandas 数据帧,然后在这些属性之间拟合了一个简单的线性回归。输出是用于拟合最小二乘运算的系数,在这种情况下,击球次数与击球次数没有很强的相关性。
*sample_pd = **spark.sql**("""
select * from stats
where player_id = 8471214
""")**.toPandas**()*# Import python libraries*
from scipy.optimize import leastsq
import numpy as np*# Define a function to fit*
def **fit**(params, x, y):
**return** (y - (params[0] + x * params[1] ))
*# Fit the curve and show the results*
result = **leastsq**(fit, [1,0], args=(sample_pd.shots,sample_pd.hits))
**print**(result)*
现在我们想对 stats 数据集中的每个球员执行这个操作。为了扩展到这个卷,我们将首先通过player_id
进行分区,如下面代码片段中的groupBy
操作所示。接下来,我们将使用 apply 命令为每个分区数据集运行analyze_player
函数。用作该操作输入的stats_df
数据帧和返回的players_df
数据帧是火花数据帧,而由分析播放器功能返回的sampled_pd
数据帧和数据帧是熊猫。Pandas UDF 注释为 PySpark 提供了一个关于如何分配工作负载的提示,这样它就可以跨工作节点集群扩展操作,而不是急切地将所有数据拉至驱动节点。像大多数 Spark 操作一样,Pandas UDFs 是延迟求值的,直到需要输出值时才会执行。
我们最初的例子现在翻译成使用熊猫 UDF 如下所示。在定义了要包含的附加模块之后,我们指定将从操作中返回的数据帧的模式。schema
对象定义了应用分析播放器函数返回的 Spark 数据帧的结构。代码块中的下一步列出了一个注释,该注释将此函数定义为分组映射操作,这意味着它作用于数据帧而不是标量值。和以前一样,我们将使用leastsq
函数来拟合镜头和点击属性。在计算了曲线拟合的系数之后,我们用玩家 id 和回归系数创建了一个新的熊猫数据帧。该代码块末尾的display
命令将强制熊猫 UDF 执行,这将为数据集中的每个玩家创建一个分区,应用最小二乘运算,并将结果合并回一个大的 Spark 数据帧中。
**# Load necessary libraries*
from pyspark.sql.functions import pandas_udf, PandasUDFType
from pyspark.sql.types import *
import pandas as pd*# Create the schema for the resulting data frame*
schema = **StructType**([**StructField**('ID', **LongType**(), True),
**StructField**('p0', **DoubleType**(), True),
**StructField**('p1', **DoubleType**(), True)])*# Define the UDF, input and outputs are Pandas DFs*
@**pandas_udf**(schema, PandasUDFType.GROUPED_MAP)
def **analize_player**(sample_pd):
*# return empty params in not enough data*
**if** (**len**(sample_pd.shots) <= 1):
return **pd.DataFrame**({'ID': [sample_pd.player_id[0]],
'p0': [ 0 ], 'p1': [ 0 ]})
*# Perform curve fitting*
result = **leastsq**(fit, [1, 0], args=(sample_pd.shots,
sample_pd.hits))
*# Return the parameters as a Pandas DF*
return **pd.DataFrame**({'ID': [sample_pd.player_id[0]],
'p0': [result[0][0]], 'p1': [result[0][1]]})
*# perform the UDF and show the results*
player_df = **stats_df.groupby**('player_id')**.apply**(analyze_player)
**display**(player_df)*
Pandas UDFs 提供的关键功能是,只要您有好的数据分区方法,它们就能使 Python 库在分布式环境中使用。这意味着像 Featuretools 这样最初并不是为在分布式环境中工作而设计的库,可以扩展到大型集群。图 6.8 显示了在统计数据集上应用上述熊猫 UDF 的结果。这个特性支持不同数据帧格式之间的无缝转换。
FIGURE 6.8: The output dataFrame from the Pandas UDF.
为了进一步展示熊猫 UDF 的价值,我们将把它们应用于分布特征生成管道和深度学习管道。然而,在工作流中使用 Pandas UDFs 时存在一些问题,因为它们会使调试变得更加困难,有时会由于 Spark 和 Pandas 之间的数据类型不匹配而失败。
最佳实践
虽然 PySpark 为 Python 程序员提供了一个熟悉的环境,但是最好遵循一些最佳实践来确保您高效地使用 Spark。以下是我根据自己将一些投影从 Python 移植到 PySpark 的经验编写的一组建议:
- ***避免使用字典:*使用字典这样的 Python 数据类型意味着代码可能无法在分布式模式下执行。不要使用键来索引字典中的值,可以考虑向 dataframe 中添加另一列来用作过滤器。该建议适用于其他 Python 类型,包括不可在 PySpark 中分发的列表。
- ***限制熊猫使用:*调用
toPandas
将导致所有数据被加载到驱动程序节点的内存中,并阻止操作以分布式模式执行。当数据已经聚合并且您想要使用熟悉的 Python 绘图工具时,使用此函数是很好的,但是它不应该用于大型数据帧。 - ***避免循环:*除了使用 for 循环,通常还可以使用 group by 和 apply 等函数方法来达到相同的结果。使用这种模式意味着代码可以被支持的执行环境并行化。我注意到,专注于在 Python 中使用这种模式也导致了更容易翻译成 PySpark 的代码的清理。
- ***最小化急切操作:*为了让您的管道尽可能地可伸缩,最好避免将完整数据帧拉入内存的急切操作。例如,在 CSV 中读取是一个急切的操作,我的工作是将数据帧存放到 S3 作为拼花,然后在后面的管道步骤中使用它。
- ***使用 SQL:*Python 和 PySpark 中都有针对数据帧提供 SQL 操作的库。如果你正在用别人的 Python 代码工作,破译熊猫的一些操作正在实现什么可能是棘手的。如果您计划将代码从 Python 移植到 PySpark,那么使用 Pandas 的 SQL 库可以使这种转换更容易。
通过在编写 PySpark 代码时遵循这些最佳实践,我已经能够改进我的 Python 和 PySpark 数据科学工作流。
6.4 MLlib 批处理管道
既然我们已经介绍了使用 PySpark 加载和转换数据,现在我们可以使用 PySpark 中的机器学习库来构建预测模型。PySpark 中构建预测模型的核心库称为 MLlib。这个库提供了一套监督和非监督算法。虽然该库没有完全涵盖 sklearn 中的所有算法,但它为数据科学工作流所需的大多数类型的操作提供了功能。在本章中,我们将展示如何将 MLlib 应用于分类问题,并将模型应用程序的输出保存到数据湖中。
*games_df = **spark.read.csv**("s3://dsp-ch6/csv/games-expand.csv",
header=True, inferSchema = True)
**games_df.createOrReplaceTempView**("games_df")games_df = **spark.sql**("""
select *, row_number() over (order by rand()) as user_id
,case when rand() > 0.7 then 1 else 0 end as test
from games_df
""")*
管道中的第一步是加载我们想要用于模型训练的数据集。上面的代码片段展示了如何加载游戏数据集,并使用 Spark SQL 将两个附加属性添加到加载的数据帧中。运行该查询的结果是,大约 30%的用户将被分配一个测试标签,我们将使用该标签进行模型应用,并且每个记录都被分配一个唯一的用户 ID,我们将在保存模型预测时使用该 ID。
下一步是将数据集分成训练和测试数据帧。对于这个管道,我们将使用测试数据帧作为模型应用的数据集,在这里我们预测用户行为。下面的代码片段显示了一个使用测试列拆分数据帧的示例。这应该会产生大约 16.1k 的培训用户和 6.8k 的测试用户。
*trainDF = **games_df.filter**("test == 0")
testDF = **games_df.filter**("test == 1")
**print**("Train " + **str**(**trainDF.count**()))
**print**("Test " + **str**(**testDF.count**()))*
向量列
MLlib 要求使用 Spark 中的矢量数据类型格式化输入数据。要将 dataframe 转换成这种格式,我们可以使用VectorAssembler
类将一系列列组合成一个向量列。下面的代码片段展示了如何使用这个类将 dataframe 中的前 10 列合并到一个名为features
的新向量列中。使用transform
函数将该命令应用于训练数据帧后,我们使用选择函数仅从数据帧中检索模型训练和应用所需的值。对于训练数据帧,我们只需要标签和特征,对于测试数据帧,我们还需要选择用户 ID。
*from pyspark.ml.feature import VectorAssembler*# create a vector representation*
assembler = **VectorAssembler**(
inputCols= trainDF.schema.names[0:10],
outputCol="features" )trainVec = **assembler.transform**(trainDF)**.select**('label', 'features')
testVec = **assembler.transform**(testDF)**.select**(
'label', 'features', 'user_id')
**display**(testVec)*
display 命令显示了将我们的测试数据集转换成 MLlib 可用的向量类型的结果。输出数据帧如图 6.9 所示。
FIGURE 6.9: The features in the sparse vector format.
模型应用
现在我们已经准备好了训练和测试数据集,我们可以使用 MLlib 提供的逻辑回归算法来拟合训练数据帧。我们首先创建一个逻辑回归对象,并定义用作标签和特征的列。接下来,我们使用fit
函数在训练数据集上训练模型。在下面代码片段的最后一步,我们使用transform
函数将模型应用到我们的测试数据集。
*from pyspark.ml.classification import LogisticRegression*# specify the columns for the model*
lr = **LogisticRegression**(featuresCol='features', labelCol='label')*# fit on training data*
model = **lr.fit**(trainVec)*# predict on test data*
predDF = **model.transform**(testVec)*
产生的 dataframe 现在有一个probability
列,如图 6.10 所示。该列是一个 2 元素数组,包含类 0 和 1 的概率。为了在测试数据集上测试逻辑回归模型的准确性,我们可以使用 Mlib 中的二元分类评估器来计算 ROC 指标,如下面的代码片段所示。对于我运行的模型,ROC 指标的值是 0.761。
*from pyspark.ml.evaluation import BinaryClassificationEvaluatorroc = **BinaryClassificationEvaluator**()**.evaluate**(predDF)
**print**(roc)*
在生产管道中,没有需要预测的用户标签,这意味着您需要执行交叉验证来选择最佳模型进行预测。第 6.7 节介绍了这种方法的一个例子。在这种情况下,我们使用单个数据集来保持代码示例的简短,但是在生产工作流中可以使用类似的管道。
既然我们有了测试用户的模型预测,我们需要检索预测的标签,以便创建一个数据帧来持久化数据湖。由于 MLlib 创建的概率列是一个数组,我们需要定义一个 UDF 来检索第二个元素作为我们的倾向列,如下面的代码片段所示。
*from pyspark.sql.functions import udf
from pyspark.sql.types import FloatType*# split out the array into a column*
secondElement = **udf**(lambda v:**float**(v[1]),**FloatType**())
predDF = **predDF.select**("*",
**secondElement**("probability")**.alias**("propensity"))
**display**(predDF)*
FIGURE 6.10: The dataframe with propensity scores.
运行该代码块后,dataframe 将有一个名为propensity
的附加列,如图 6.10 所示。这个批量预测管道的最后一步是将结果保存到 S3。我们将使用select
函数从预测数据帧中检索相关的列,然后在数据帧上使用write
函数将结果持久化为 S3 上的拼花。
**# save results to S3*
results_df = **predDF.select**("user_id", "propensity")
results_path = "s3a://dsp-ch6/game-predictions/"
**results_df.write.mode**('overwrite')**.parquet**(results_path)*
我们现在已经拥有了创建 PySpark 管道所需的所有构件,该管道可以从存储层获取数据,训练预测模型,并将结果写入持久存储。我们将在第 6.8 节讨论如何安排这种类型的管道。
开发模型时,检查输出以查看模型预测的分布是否与预期匹配是很有用的。我们可以使用 Spark SQL 对模型输出执行聚合,然后使用 display 命令直接在 Databricks 中执行这个过程,如下面的代码片段所示。对模型预测执行这些步骤的结果如图 6.11 所示。
**# plot the predictions*
predDF **.createOrReplaceTempView**("predDF ")plotDF = **spark.sql**("""
select cast(propensity*100 as int)/100 as propensity,
label, sum(1) as users
from predDF
group by 1, 2
order by 1, 2
""")*# table output*
**display**(plotDF)*
FIGURE 6.11: The distribution of propensity scores.
MLlib 可以使用大量算法应用于各种各样的问题。虽然我们在本节中探讨了逻辑回归,但是库提供了许多不同的分类方法,并且还支持其他类型的操作,包括回归和聚类。
6.5 分布式深度学习
虽然 MLlib 为经典的机器学习算法提供了可扩展的实现,但它本身并不支持深度学习库,如 Tensorflow 和 PyTorch。有一些库可以在 Spark 上并行化深度学习模型的训练,但数据集需要能够适应每个工作节点上的内存,这些方法最适合用于中等规模数据集的分布式超参数调优。
对于模型应用阶段,我们已经有了一个经过训练的深度学习模型,但需要将结果模型应用到一个大型用户群,我们可以使用 Pandas UDFs。使用 Pandas UDFs,我们可以划分和分布我们的数据集,根据 Keras 模型运行结果数据帧,然后将结果编译回一个大 Spark 数据帧。本节将展示如何使用我们在 1.6.3 节中构建的 Keras 模型,并使用 PySpark 和 Pandas UDFs 将其扩展到更大的数据集。但是,我们仍然要求用于训练模型的数据能够适合驱动程序节点上的内存。
我们将使用上一节中的相同数据集,其中我们将游戏数据集分为用户的训练集和测试集。这是一个相对较小的数据集,因此我们可以使用toPandas
操作将 dataframe 加载到驱动程序节点上,如下面的代码片段所示。结果是一个数据框架和列表,我们可以将其作为输入来训练 Keras 深度学习模型。
**# build model on the driver node*
train_pd = **trainDF.toPandas**()
x_train = train_pd.iloc[:,0:10]
y_train = train_pd['label']*
当使用 PyPI 在 Spark 集群上安装 TensorFlow 时,安装的库版本应该是 2.0 或更高版本。这不同于我们在前面章节中使用的 TensorFlow 的版本 1。就代码片段而言,主要影响是 Tensorflow 2 现在具有内置的 AUC 功能,不再需要我们以前应用的工作流。
模型培训
我们将使用与之前相同的方法来训练一个 Keras 模型。下面的代码片段显示了如何建立一个具有输入层、后丢弃层、单个隐藏层和输出层的网络,并使用rmsprop
和交叉熵损失进行优化。在模型应用阶段,我们将在 Pandas UDFs 中重用model
对象来分配工作负载。
*import tensorflow as tf
import keras
from keras import models, layersmodel = **models.Sequential**()
**model.add**(**layers.Dense**(64, activation='relu', input_shape=(10,)))
**model.add**(**layers.Dropout**(0.1))
**model.add**(**layers.Dense**(64, activation='relu'))
**model.add**(**layers.Dense**(1, activation='sigmoid'))**model.compile**(optimizer='rmsprop', loss='binary_crossentropy')
history = **model.fit**(x_train, y_train, epochs=100, batch_size=100,
validation_split = .2, verbose=0)*
为了测试过度拟合,我们可以绘制训练和验证数据集的结果,如图 6.12 所示。下面的代码片段展示了如何使用 matplotlib 来显示这些数据集随时间的损失。虽然训练损失在额外的时期内继续减少,但验证损失在 20 个时期后停止改善,但随着时间的推移没有显著增加。
*import matplotlib.pyplot as pltloss = history.history['loss']
val_loss = history.history['val_loss']
epochs = **range**(1, **len**(loss) + 1)fig = **plt.figure**(figsize=(10,6) )
**plt.plot**(epochs, loss, 'bo', label='Training Loss')
**plt.plot**(epochs, val_loss, 'b', label='Validation Loss')
**plt.legend**()
**plt.show**()
**display**(fig)*
FIGURE 6.12: Training a Keras model on a subset of data.
模型应用
现在我们有了一个经过训练的深度学习模型,我们可以使用 PySpark 将其应用到可扩展的管道中。第一步是确定如何划分需要评分的用户集合。对于这个数据集,我们可以将用户群分配到 100 个不同的桶中,如下面的代码片段所示。这将每个用户随机分配到 100 个桶中的 1 个,这意味着在逐步应用组后,每个转换为 Pandas 的数据帧的大小大约是原始数据帧的 1%。如果您有一个大型数据集,您可能需要使用数千个存储桶来分发数据集,甚至更多。
**# set up partitioning for the train data frame*
**testDF.createOrReplaceTempView**("testDF ")partitionedDF = **spark.sql**("""
select *, cast(rand()*100 as int) as partition_id
from testDF
""")*
下一步是定义将应用 Keras 模型的熊猫 UDF。我们将定义一个用户 ID 和倾向得分的输出模式,如下所示。UDF 在我们之前训练的模型对象上使用predict
函数,在传入的数据帧上创建一个prediction
列。return 命令选择我们为 schema 对象定义的两个相关列。group by 命令使用我们的分桶方法对数据集进行分区,apply 命令跨工作节点集群执行 Keras 模型应用程序。结果是显示命令可视化的火花数据帧,如图 6.13 所示。
*from pyspark.sql.functions import pandas_udf, PandasUDFType
from pyspark.sql.types import *schema = **StructType**([**StructField**('user_id', **LongType**(), True),
**StructField**('propensity', **DoubleType**(),True)])@**pandas_udf**(schema, PandasUDFType.GROUPED_MAP)
def **apply_keras**(pd):
pd['propensity'] = **model.predict**(pd.iloc[:,0:10])
return pd[['user_id', 'propensity']]results_df=**partitionedDF.groupby**('partition_id')**.apply**(apply_keras)
**display**(results_df)*
FIGURE 6.13: The resulting dataframe for distributed Keras.
需要注意的一点是,在 Pandas UDFs 中可以引用的对象类型是有限制的。在这个例子中,我们引用了model
对象,它是在训练模型时在驱动程序节点上创建的。当 PySpark 中的变量从 driver 节点转移到 workers 节点以进行分布式操作时,会制作一个变量的副本,因为在集群中同步变量是低效的。这意味着对熊猫 UDF 中的变量所做的任何更改都不会应用到原始对象。这也是为什么在使用 UDF 时应该避免使用 Python 列表和字典这样的数据类型。函数以类似的方式工作,在 6.3.4 节中,我们在 Pandas UDF 中使用了fit
函数,该函数最初是在驱动程序节点上定义的。Spark 还提供了用于在集群中共享变量的广播变量,但是理想的分布式代码段应该尽可能避免通过变量共享状态。
6.6 分布式特征工程
要素工程是数据科学工作流中的关键步骤,有时有必要使用 Python 库来实现此功能。例如,Zynga 的 AutoModel 系统使用 Featuretools 库从原始跟踪事件中生成数百个特征,然后用作分类模型的输入。为了扩大我们在 1.7 节中首次探索的自动化特征工程方法,我们可以使用 Pandas UDFs 来分发特征应用过程。与前一节一样,在确定要执行的转换时,我们需要对数据进行采样,但是在应用转换时,我们可以扩展到大规模数据集。
在本节中,我们将使用 NHL Kaggle 示例中的游戏比赛数据集,其中包括每场比赛中发生的事件的详细描述。我们的目标是将深而窄的数据帧转换成浅而宽的数据帧,该数据帧将每场比赛总结为具有数百列的单个记录。在 PySpark 中加载这些数据并选择相关列的示例如下面的代码片段所示。在调用toPandas
之前,我们使用 filter 函数对 0.3%的记录进行采样,然后将结果投射到一个熊猫框架中,该框架的形状为 10717 行 16 列。
*plays_df = **spark.read.csv**("s3://dsp-ch6/csv/game_plays.csv",
header=True, inferSchema = True)**.drop**(
'secondaryType', 'periodType', 'dateTime', 'rink_side')
plays_pd = **plays_df.filter**("rand() < 0.003")**.toPandas**()
plays_pd.shape*
特征生成
我们将使用 1.7 节中介绍的相同的两步过程,首先对数据帧中的分类特征进行一次性编码,然后对数据集应用深度特征合成。下面的代码片段显示了如何使用 Featuretools 库执行编码过程。输出是初始数据帧的转换,现在有 20 个虚拟变量,而不是事件和描述变量。
*import featuretools as ft
from featuretools import Featurees = **ft.EntitySet**(id="plays")
es = **es.entity_from_dataframe**(entity_id="plays",dataframe=plays_pd,
index="play_id", variable_types = {
"event": ft.variable_types.Categorical,
"description": ft.variable_types.Categorical })f1 = **Feature**(es["plays"]["event"])
f2 = **Feature**(es["plays"]["description"])encoded, defs = **ft.encode_features**(plays_pd, [f1, f2], top_n=10)
**encoded.reset_index**(inplace=True)*
下一步是使用dfs
函数对我们的编码数据帧进行深度特征合成。输入数据帧将有每场比赛的记录,而输出数据帧在使用各种不同的聚合将详细事件折叠成宽列表示后,将有每场比赛的单个记录。
*es = **ft.EntitySet**(id="plays")
es = **es.entity_from_dataframe**(entity_id="plays",
dataframe=encoded, index="play_id")es = **es.normalize_entity**(base_entity_id="plays",
new_entity_id="games", index="game_id")
features, transform=**ft.dfs**(entityset=es,
target_entity="games",max_depth=2)
**features.reset_index**(inplace=True)*
与之前的方法相比,我们需要执行的一个新步骤是,我们需要确定所生成要素的方案,因为这需要作为熊猫 UDF 注记的输入。为了弄清楚为生成的数据帧生成的模式是什么,我们可以创建一个 Spark 数据帧,然后从 Spark 数据帧中检索模式。在转换 Pandas 数据帧之前,我们需要修改生成的数据帧中的列名,以删除特殊字符,如下面的代码片段所示。特性应用步骤的 Spark 模式如图 6.14 所示。
*features.columns = **features.columns.str.replace**("[(). =]", "")
schema = **sqlContext.createDataFrame**(features).schema
features.columns*
FIGURE 6.14: The schema for the generated features.
我们现在有了定义熊猫 UDF 所需的模式。与我们过去定义的 UDF 不同,模式可能会根据 Featuretools 选择的特征转换聚合在不同运行之间发生变化。在这些步骤中,我们还创建了一个定义用于编码的特性转换的defs
对象和一个定义执行深度特性合成的转换的transform
对象。与上一节中的模型对象一样,这些对象的副本将被传递给在 worker 节点上执行的熊猫 UDF。
特征应用
为了使我们的方法能够跨工作节点集群扩展,我们需要定义一个用于分区的列。像前面的部分一样,我们可以将事件分成不同的数据集,以确保 UDF 过程可以伸缩。与以前的一个不同之处是,我们需要将一个特定游戏中的所有游戏分组到同一个分区中。为了达到这个结果,我们可以通过game_id
而不是player_id
进行分区。下面的代码片段显示了这种方法的一个例子。此外,我们可以使用游戏 ID 上的散列函数来随机化该值,从而产生更平衡的桶。
**# bucket IDs*
**plays_df.createOrReplaceTempView**("plays_df")
plays_df = **spark.sql**("""
select *, abs(hash(game_id))%1000 as partition_id
from plays_df
""")*
现在,我们可以使用下面定义的熊猫 UDF 将特征变换应用于整个数据集。在被传递到生成特征函数之前,播放数据帧由桶进行分区。该函数使用先前生成的特征变换来确保相同的变换应用于所有工作节点。输入的熊猫数据帧是游戏数据的窄而深的表示,而返回的数据帧是游戏概要的浅而宽的表示。
*from pyspark.sql.functions import pandas_udf, PandasUDFType@**pandas_udf**(schema, PandasUDFType.GROUPED_MAP)
def **gen_features**(plays_pd): es = **ft.EntitySet**(id="plays")
es = **es.entity_from_dataframe**(entity_id="plays",
dataframe=plays_pd, index="play_id", variable_types = {
"event": ft.variable_types.Categorical,
"description": ft.variable_types.Categorical })
encoded_features = **ft.calculate_feature_matrix**(defs, es)
**encoded_features.reset_index**(inplace=True)
es = **ft.EntitySet**(id="plays")
es = **es.entity_from_dataframe**(entity_id="plays",
dataframe=encoded, index="play_id")
es = **es.normalize_entity**(base_entity_id="plays",
new_entity_id="games", index="game_id")
generated = **ft.calculate_feature_matrix**(transform,es)**.fillna**(0)
**generated.reset_index**(inplace=True)
generated.columns = **generated.columns.str.replace**("[(). =]","")
return generated
features_df = **plays_df.groupby**('partition_id')**.apply**(gen_features)
**display**(features_df)*
显示命令的输出如图 6.15 所示。我们现在已经完成了可扩展模型管道中的特征生成和深度学习。现在我们有了一个转换后的数据集,我们可以将结果与额外的特征结合起来,例如我们希望预测的标签,并开发一个完整的模型管道。
FIGURE 6.15: Generated features in the Spark dataframe.
6.7 GCP 模型管道
批处理模型管道的常见工作流是从湖中读取输入数据,应用机器学习模型,然后将结果写入应用数据库。在 GCP,BigQuery 充当数据湖,云 Bigtable 可以充当应用数据库。我们将在下一章使用这些组件构建端到端的管道,但是现在我们将直接在 Spark 中使用 GCP 组件的子集。
虽然 BigQuery 有一个 Spark 连接器[ 15 ],可以直接使用 BigQuery 构建大规模的 PySpark 管道,但是这个库有一些问题,使得为我们的 Databricks 环境进行设置非常复杂。例如,我们需要重新构建一些 JAR 文件并隐藏依赖关系。一种替代方法是使用我们在 5.1 节中探索的 Python BigQuey 连接器,但是这种方法不是分布式的,并且会急切地将查询结果作为 Pandas 数据帧拉至驱动程序节点。在本章中,我们将探索一个工作流,在这个工作流中,我们将查询结果卸载到云存储中,然后从 GCS 中读入数据集,作为管道中的第一步。类似地,对于模型输出,我们将结果保存到 GCS,在 GCS 中,输出可用于推送到 Bigtable。为了将这种类型的工作流程产品化,可以使用气流将这些不同的动作链接在一起。
大查询导出
我们要执行的第一步是将 BigQuery 查询的结果导出到 GCS,这可以使用 BigQuery UI 手动执行。这可以直接在 Spark 中执行,但是正如我提到的,使用当前版本的连接器库进行配置是非常复杂的。我们将为这个管道使用natality
数据集,它列出了关于分娩的属性,比如出生体重。
*create table dsp_demo.natality **as** (
select *
from `bigquery-public-data.samples.natality`
order by **rand**()
limit 10000
)*
为了创建数据集,我们将从 BigQuery 中的 natality 公共数据集中抽取 10k 条记录。要将这个结果集导出到 GCS,我们需要在 BigQuery 上创建一个包含要导出的数据的表。创建这个数据样本的 SQL 如上面的代码片段所示。要将此数据导出到 GCS,请执行以下步骤:
- 浏览到 GCP 控制台
- 搜索“大查询”
- 将上面代码片段中的查询粘贴到查询编辑器中
- 单击运行
- 在左侧导航窗格中,选择新表“dsp_demo.natality”
- 单击“导出”,然后单击“导出到 GCS”
- 设置位置,“/dsp_model_store/natality/avro”
- 使用“Avro”作为导出格式
- 点击“导出”
执行这些步骤后,抽样出生率数据将以 Avro 格式保存到 GCS。导出数据集的确认对话框如图 6.16 所示。现在,我们已经将数据保存到 GCS 中,格式与 Spark 兼容。
FIGURE 6.16: Confirming the Avro export on GCS.
GCP 全权证书
我们现在有了一个数据集,可以用作 PySpark 管道的输入,但是我们还不能从我们的 Spark 环境访问 GCS 上的 bucket。借助 AWS,我们能够使用访问密钥和秘密密钥设置对 S3 的编程访问。对于 GCP,这个过程稍微复杂一些,因为我们需要将 json 凭证文件移动到集群的驱动程序节点,以便在 GCS 上读写文件。使用 Spark 的一个挑战是,您可能无法通过 SSH 访问驱动程序节点,这意味着我们需要使用持久存储将文件移动到驱动程序机器上。不建议在生产环境中使用,而是作为概念验证展示。在生产环境中管理凭证的最佳实践是使用 IAM 角色。
*aws s3 cp dsdemo.json s3:**//**dsp-ch6/secrets/dsdemo.json
aws s3 ls s3:**//**dsp-ch6/secrets/*
要将 json 文件移动到驱动程序节点,我们可以首先将凭证文件复制到 S3,如上面的代码片段所示。现在我们可以切换回 Databricks 并创建模型管道。要将文件复制到驱动节点,我们可以使用sc
Spark 上下文逐行读取文件。这不同于我们之前的所有操作,在这些操作中,我们将数据集作为数据帧读入。读取文件后,我们使用 Python open
和write
函数在驱动节点上创建一个文件。同样,这是在 Spark 中执行的一个不寻常的操作,因为您通常希望写入持久存储,而不是本地存储。执行这些步骤的结果是,凭据文件现在将在集群中的驱动程序节点上本地可用。
*creds_file = '/databricks/creds.json'
creds = **sc.textFile**('s3://dsp-ch6/secrets/dsdemo.json')with **open**(creds_file, 'w') as file:
**for** line **in** **creds.take**(100):
**file.write**(line + "\n")*
现在我们已经将 json 凭证文件移动到了驱动程序本地存储,我们可以设置访问 GCS 上的数据所需的 Hadoop 配置。下面的代码片段显示了如何配置项目 ID、文件系统实现和凭证文件位置。运行这些命令后,我们现在可以在 GCS 上读写文件了。
***sc._jsc.hadoopConfiguration**()**.set**("fs.gs.impl",
"com.google.cloud.hadoop.fs.gcs.GoogleHadoopFileSystem")
**sc._jsc.hadoopConfiguration**()**.set**("fs.gs.project.id",
"your_project_id")
**sc._jsc.hadoopConfiguration**()**.set**(
"mapred.bq.auth.service.account.json.keyfile", creds_file)
**sc._jsc.hadoopConfiguration**()**.set**(
"fs.gs.auth.service.account.json.keyfile", creds_file)*
模型管道
为了读入出生数据集,我们可以使用带有 Avro 设置的 read 函数来获取数据集。由于我们使用的是 Avro 格式,dataframe 将被延迟加载,并且直到使用 display 命令对数据集进行采样时才会检索数据,如下面的代码片段所示。
*natality_path = "gs://dsp_model_store/natality/avro"
natality_df = **spark.read.format**("avro")**.load**(natality_path)
**display**(natality_df)*
在使用 MLlib 构建回归模型之前,我们需要对数据集执行一些转换,以选择一个功能子集,转换数据类型,并将记录分成训练组和测试组。我们还将使用如下所示的fillna
函数,将数据帧中的任何空值替换为零。在这个建模练习中,我们将构建一个回归模型,使用一些不同的特征(包括母亲的婚姻状况和父母的年龄)来预测婴儿的出生体重。准备好的数据帧如图 6.17 所示。
***natality_df.createOrReplaceTempView**("natality_df")natality_df = **spark.sql**("""
SELECT year, plurality, apgar_5min,
mother_age, father_age,
gestation_weeks, ever_born
,case when mother_married = true
then 1 else 0 end as mother_married
,weight_pounds as weight
,case when rand() < 0.5 then 1 else 0 end as test
from natality_df
""")**.fillna**(0)trainDF = **natality_df.filter**("test == 0")
testDF = **natality_df.filter**("test == 1")
**display**(natality_df)*
FIGURE 6.17: The prepared Natality dataframe.
接下来,我们将把数据帧转换成 MLlib 需要输入的向量数据类型。转换出生率数据集的过程如下面的代码片段所示。在执行了transform
函数之后,我们现在有了可以用作回归模型输入的训练和测试数据集。我们正在构建一个模型来预测的标签是weight
列。
*from pyspark.ml.feature import VectorAssembler*# create a vector representation*
assembler = **VectorAssembler**(inputCols= trainDF.schema.names[0:8],
outputCol="features" )trainVec = **assembler.transform**(trainDF)**.select**('weight','features')
testVec = **assembler.transform**(testDF)**.select**('weight', 'features')*
MLlib 提供了一组用于在模型工作流中执行交叉验证和超参数调整的实用程序。下面的代码片段显示了如何对随机森林回归模型执行此过程。我们没有直接在模型对象上调用fit
,而是用一个交叉验证器对象来包装模型对象,该验证器对象探索不同的参数设置,比如树的深度和数量。这个工作流程类似于 sklearn 中的网格搜索功能。在搜索整个参数空间并使用基于折叠数的交叉验证之后,在应用于对测试数据集进行预测之前,在完整的训练数据集上重新训练随机森林模型。结果是一个实际体重和预测出生体重的数据框架。
*from pyspark.ml.tuning import ParamGridBuilder
from pyspark.ml.regression import RandomForestRegressor
from pyspark.ml.tuning import CrossValidator
from pyspark.ml.evaluation import RegressionEvaluatorfolds = 3
rf_trees = [ 50, 100 ]
rf_depth = [ 4, 5 ] rf= **RandomForestRegressor**(featuresCol='features',labelCol='weight')paramGrid = **ParamGridBuilder**()**.addGrid**(rf.numTrees, rf_trees).
**ddGrid**(rf.maxDepth, rf_depth)**.build**()
crossval = **CrossValidator**(estimator=rf, estimatorParamMaps =
paramGrid, evaluator=**RegressionEvaluator**(
labelCol='weight'), numFolds = folds)
rfModel = **crossval.fit**(trainVec)
predsDF = **rfModel.transform**(testVec)**.select**("weight", "prediction")*
在 GCP 模型管道的最后一步,我们将把结果保存到 GCS,以便工作流中的其他应用程序或流程可以利用这些预测。下面的代码片段显示了如何以 Avro 格式将数据帧写入 GCS。为了确保管道的不同运行不会覆盖过去的预测,我们在导出路径上附加了一个时间戳。
*import timeout_path = "gs://dsp_model_store/natality/preds-{time}/".
**format**(time = **int**(**time.time**()*1000))
**predsDF.write.mode**('overwrite')**.format**("avro")**.save**(out_path)
**print**(out_path)*
将 GCP 组件与 PySpark 一起使用需要花费一些精力来配置,但是在本例中,我们在不同的云提供商中运行 Spark,而不是在我们读取和写入数据的地方。在生产环境中,您最有可能在与处理数据集相同的云中运行 Spark,这意味着您可以利用 IAM 角色来正确管理对不同服务的访问。
6.8 生产 PySpark
一旦您在笔记本环境中测试了批处理模型管道,就有几种不同的方法来安排管道定期运行。例如,您可能希望移动游戏的客户流失预测模型每天早上运行,并将分数发布到应用程序数据库。类似于我们在第 5 章中介绍的工作流工具,PySpark 管道应该对任何可能发生的故障进行监控。调度 PySpark 作业有几种不同的方法:
- 工作流工具: Airflow、Azkaban 和 Luigi 都支持在工作流中运行 spark 作业。
- ***云工具:*AWS 上的 EMR 和 GCP 上的 Dataproc 支持预定的 Spark 作业。
- 供应商工具: Databricks 支持通过 web 用户界面设置带有监控的作业计划。
- Spark Submit: 如果已经配置了集群,可以使用 crontab 等工具发出
spark-submit
命令。
供应商和云工具通常更容易启动和运行,因为它们在工作流中提供了配置集群的选项。例如,使用 Databricks,您可以定义要启动的集群类型,以便按计划运行笔记本电脑。使用工作流工具(如 Airflow)时,您需要向工作流添加额外的步骤,以便启动和终止集群。大多数工作流工具都提供了到 EMR 的连接器,用于将集群作为工作流的一部分进行管理。Spark 提交选项在第一次开始调度 Spark 作业时很有用,但是它不支持将集群作为工作流的一部分来管理。
Spark 作业可以在短暂或持久的集群上运行。临时集群是一个 Spark 集群,它被提供来执行一组任务,然后被终止,比如运行一个 churn 模型管道。持久集群是长期运行的集群,可能支持交互式笔记本,例如我们在本章开始时设置的 Databricks 集群。持久集群对于开发很有用,但是如果为集群而启动的硬件没有得到充分利用,那么它的成本会很高。一些供应商支持集群的自动伸缩,以降低长期运行的持久集群的成本。临时集群是有用的,因为旋转一个新的集群来执行一个任务能够隔离跨任务的失败,并且这意味着不同的模型管道可以使用不同的库版本。
除了为调度作业和作业失败警报设置工具之外,为 Spark 模型管道设置额外的数据和模型质量检查也很有用。例如,我已经设置了执行审计任务的 Spark 作业,比如确保应用程序数据库有当天的预测,如果预测数据过时就触发警报。作为 Spark 管道的一部分,记录度量标准也是一个很好的实践,比如交叉验证模型的 ROC。我们将在第 11 章更详细地讨论这一点。
6.9 结论
PySpark 是数据科学家构建可扩展分析和建模管道的强大工具。这是公司非常需要的技能,因为它使数据科学团队能够拥有更多构建和拥有数据产品的过程。为 PySpark 设置环境的方法有很多种,在这一章中,我们探索了一个流行的 Spark 供应商提供的免费笔记本环境。
本章主要关注批处理模型管道,其目标是定期为大量用户创建一组预测。我们探索了 AWS 和 GCP 部署的管道,其中数据源和数据输出是数据湖。这些类型的管道的一个问题是,在使用预测时,预测可能已经过时。在第 9 章中,我们将探索 PySpark 的流管道,其中模型预测的延迟被最小化。
PySpark 是一种用于创作模型管道的高度表达性语言,因为它支持所有 Python 功能,但需要一些变通方法来让代码在一个 workers 节点集群上执行。在下一章中,我们将探索 Dataflow,它是 Apache Beam 库的一个运行时,也支持大规模分布式 Python 管道,但是在可以执行的操作类型方面受到更多限制。
参考
卡劳、霍尔登、安迪·孔温斯基、帕特里克·温德尔和马泰·扎哈里亚。2015.学习火花:快如闪电的大数据分析。第一版。奥莱利媒体。
13.https://community.cloud.databricks.com/↩︎14。https://www.gamasutra.com/blogs/BenWeber/20190426/340293/↩︎t11】15。https://github.com/spotify/spark-bigquery/t14】↩︎
本·韦伯是 Zynga 的一名杰出的数据科学家。我们正在招聘!
py spark–导入任何数据
使用 Spark 导入数据的简要指南
https://upload.wikimedia.org/wikipedia/commons/f/f3/Apache_Spark_logo.svg
通过这篇文章,我将开始一系列关于 Pyspark 的简短教程,从数据预处理到建模。第一个将处理任何类型的数据的导入和导出,CSV,文本文件,Avro,Json…等等。我在谷歌云平台上的一台虚拟机上工作,数据来自云存储上的一个桶。让我们导入它们。
导入 CSV
Spark 具有读取 csv 的集成功能,非常简单:
csv_2_df = spark.read.csv("gs://my_buckets/poland_ks")#print it
csv_2_df.show()
数据以正确的列数加载,数据中似乎没有任何问题,但是标题不固定。我们需要设置 header = True 参数。
csv_2_df = spark.read.csv("gs://my_buckets/poland_ks", header = "true")
还有其他可能的语法。
csv_2_df= spark.read.load("gs://my_buckets/poland_ks", format="csv", header="true")
以及 sep 这样的参数来指定分隔符或者 inferSchema 来推断数据的类型,我们顺便来看看 Schema。
csv_2_df.printSchema()
我们的 dataframe 在 string 中有所有类型的数据集,让我们尝试推断模式。
csv_2_df = spark.read.csv("gs://my_buckets/poland_ks", header =True, inferSchema=True)csv_2_df.printSchema()
我们可以手动指定我们的模式
from pyspark.sql.types import *schema = StructType([
StructField("ID_DAY", DateType()),
StructField("SID_STORE", IntegerType()),
StructField("NB_TICKET", IntegerType()),
StructField("F_TOTAL_QTY", IntegerType()),
StructField("F_VAL_SALES_AMT_TCK", DoubleType()),
StructField("SITE_FORMAT", StringType())])csv_2_df = spark.read.csv("gs://alex_precopro/poland_ks", header = 'true', schema=schema)
导入 JSON
json_to_df = spark.read.json("gs://my_bucket/poland_ks_json")
进口拼花地板
parquet_to_df = spark.read.parquet("gs://my_bucket/poland_ks_parquet")
导入 AVRO
在 Avro 的情况下,我们需要调用外部数据块包来读取它们。
df = spark.read.format("com.databricks.spark.avro").load("gs://alex_precopro/poland_ks_avro", header = 'true')
导入文本文件
同样,spark 也有一个内置功能
textFile = spark.read.text('path/file.txt')
您也可以将文本文件读取为 rdd
# read input text file to RDD
lines = sc.textFile('path/file.txt')
# collect the RDD to a list
list = lines.collect()
出口任何东西
要导出数据,你必须适应你想要的输出,如果你写在 parquet,avro 或任何分区文件没有问题。如果我们想以 CSV 格式编写,我们必须将分散在不同工作器上的分区分组,以编写我们的 CSV 文件
#partitioned file
output.write.parquet(“gs://my_bucket/my_output")#csv partitioned_output.coalesce(1).write.mode("overwrite")\
.format("com.databricks.spark.csv")\
.option("header", "true")\
.option("sep", "|")\
.save('gs://my_bucket/my_output_csv')
通过使用 coalesce(1)或 repartition(1 ),数据帧的所有分区被组合到一个块中。
最后
我们看到了如何导入我们的文件并编写它。让我们来看我的下一篇文章,学习如何过滤我们的数据框架。感谢。【Y】You 可以在这里找到代码*😗https://github.com/AlexWarembourg/Medium
谷歌可乐中的 PySpark
在 Colab 中使用 PySpark 创建简单的线性回归模型
Photo by Ashim D’Silva on Unsplash
随着数据池来源的拓宽,大数据主题在过去几年中受到了越来越多的关注。除了处理各种类型和形状的巨大数据之外,大数据的分析部分的目标周转时间也大大减少了。这种速度和效率不仅有助于大数据的即时分析,也有助于识别新机会。这反过来又导致采取更明智的商业行动,更高效的运营,更高的利润和更满意的客户。
Apache Spark 旨在以更快的速度分析大数据。Apache Spark 提供的一个重要特性是能够在内存中运行计算。对于在磁盘上运行的复杂应用程序,它也被认为比 MapReduce 更高效。
Spark 被设计成高度可访问的,提供了 Python、Java、Scala 和 SQL 的简单 API,以及丰富的内置库。它还与其他大数据工具紧密集成。特别是 Spark 可以在 Hadoop 集群中运行,可以访问任何 Hadoop 数据源,包括 Cassandra。
PySpark 是使用 Python 编程语言访问 Spark 的接口。PySpark 是用 python 开发的 API,用于 Spark 编程和用 Python 风格编写 spark 应用程序,尽管底层执行模型对于所有 API 语言都是相同的。
在本教程中,我们将主要处理 PySpark 机器学习库 Mllib,该库可用于导入线性回归模型或其他机器学习模型。
是的,但是为什么是 Google Colab 呢?
Google 的 Colab 基于 Jupyter Notebook,这是一个非常强大的工具,利用了 google docs 的功能。由于它运行在谷歌服务器上,我们不需要在我们的系统中本地安装任何东西,无论是 Spark 还是深度学习模型。Colab 最吸引人的特性是免费的 GPU 和 TPU 支持!由于 GPU 支持运行在谷歌自己的服务器上,事实上,它比一些商用 GPU(如 Nvidia 1050Ti)更快。分配给用户的一条常规系统信息如下所示:
Gen RAM Free: 11.6 GB | Proc size: 666.0 MB
GPU RAM Free: 11439MB | Used: 0MB | Util 0% | Total 11439MB
如果你有兴趣了解更多关于 Colab 的信息,Anna Bonner的这篇文章指出了使用 Colab 的一些显著好处。
闲聊到此为止。让我们用 Google Colab 中的 PySpark 创建一个简单的线性回归模型。
要打开 Colab Jupyter 笔记本,请点击此链接。
在 Colab 运行 Pyspark
要在 Colab 中运行 spark,首先我们需要在 Colab 环境中安装所有依赖项,如 Apache Spark 2.3.2 with hadoop 2.7、Java 8 和 Findspark,以便在系统中定位 Spark。工具安装可以在 Colab 的 Jupyter 笔记本内进行。
按照以下步骤安装依赖项:
!apt-get install openjdk-8-jdk-headless -qq > /dev/null
!wget -q [https://www-us.apache.org/dist/spark/spark-2.4.1/spark-2.4.1-bin-hadoop2.7.tgz](https://www-us.apache.org/dist/spark/spark-2.4.1/spark-2.4.1-bin-hadoop2.7.tgz)
!tar xf spark-2.4.1-bin-hadoop2.7.tgz
!pip install -q findspark
既然我们已经在 Colab 中安装了 Spark 和 Java,现在是时候设置环境路径,使我们能够在我们的 Colab 环境中运行 PySpark。通过运行以下代码设置 Java 和 Spark 的位置:
import os
os.environ["JAVA_HOME"] = "/usr/lib/jvm/java-8-openjdk-amd64"
os.environ["SPARK_HOME"] = "/content/spark-2.3.2-bin-hadoop2.7"
我们可以运行一个本地 spark 会话来测试我们的安装:
import findspark
findspark.init()
from pyspark.sql import SparkSession
spark = SparkSession.builder.master("local[*]").getOrCreate()
我们的实验室可以运行 PySpark 了。让我们建立一个简单的线性回归模型。
线性回归模型
线性回归模型是一种最古老和广泛使用的机器学习方法,它假设因变量和自变量之间存在关系。例如,建模者可能希望根据湿度比来预测降雨。线性回归由穿过图上分散点的最佳拟合线组成,最佳拟合线称为回归线。关于线性回归的详细内容可以在这里找到。
为了从 Colab 中的 Pyspark 开始并保持简单,我们将使用著名的波士顿住房数据集。这个数据集的完整描述可以在这个链接中找到。
本练习的目标是根据给定的特征预测房价。让我们通过将 MEDV 作为目标变量,将所有其他变量作为输入要素来预测 Boston Housing 数据集的价格。
我们可以从这个链接下载数据集,并将它保存在本地驱动器中的某个可访问的地方。可以在同一驱动器上使用以下命令将数据集加载到 Colab 目录中。
from google.colab import files
files.upload()
我们现在可以检查 Colab 的目录内容
!ls
我们应该看到一个名为 BostonHousing.csv 的文件被保存。现在我们已经成功上传了数据集,我们可以开始分析了。
对于我们的线性回归模型,我们需要从 PySpark API 导入向量汇编器和线性回归模块。Vector Assembler 是一个转换工具,它将包含 type double 的多个列中的所有特征组合成一个向量。我们应该使用 ( 必须使用 ) StringIndexer 如果我们的任何列包含字符串值,就将其转换为数值。幸运的是,BostonHousing 数据集只包含 double 类型,所以我们现在可以跳过 StringIndexer 。
from pyspark.ml.feature import VectorAssembler
from pyspark.ml.regression import LinearRegressiondataset = spark.read.csv('BostonHousing.csv',inferSchema=True, header =True)
注意,我们在 read.csv()中使用了 InferSchema。InferSchema 自动为每一列推断不同的数据类型。
让我们查看数据集,看看每一列的数据类型:
dataset.printSchema()
它应该打印如下数据类型:
在下一步中,我们将把不同列中的所有特征转换成一个单独的列,我们可以在 outputCol 中将这个新的向量列称为“属性”。
#Input all the features in one vector column
assembler = VectorAssembler(inputCols=['crim', 'zn', 'indus', 'chas', 'nox', 'rm', 'age', 'dis', 'rad', 'tax', 'ptratio', 'b', 'lstat'], outputCol = 'Attributes')output = assembler.transform(dataset)#Input vs Output
finalized_data = output.select("Attributes","medv")finalized_data.show()
这里,“属性”是所有列的输入特征,“medv”是目标列。
接下来,我们应该根据我们的数据集拆分训练和测试数据(在本例中为 0.8 和 0.2)。
#Split training and testing data
train_data,test_data = finalized_data.randomSplit([0.8,0.2])regressor = LinearRegression(featuresCol = 'Attributes', labelCol = 'medv')#Learn to fit the model from training set
regressor = regressor.fit(train_data)#To predict the prices on testing set
pred = regressor.evaluate(test_data)#Predict the model
pred.predictions.show()
预测列中的预测得分作为输出:
我们还可以使用以下命令打印回归模型的系数和截距:
#coefficient of the regression model
coeff = regressor.coefficients#X and Y intercept
intr = regressor.interceptprint ("The coefficient of the model is : %a" %coeff)
print ("The Intercept of the model is : %f" %intr)
完成基本的线性回归操作后,我们可以进一步从 Pyspark 导入 RegressionEvaluator 模块,对我们的模型进行统计分析。
from pyspark.ml.evaluation import RegressionEvaluator
eval = RegressionEvaluator(labelCol="medv", predictionCol="prediction", metricName="rmse")# Root Mean Square Error
rmse = eval.evaluate(pred.predictions)
print("RMSE: %.3f" % rmse)# Mean Square Error
mse = eval.evaluate(pred.predictions, {eval.metricName: "mse"})
print("MSE: %.3f" % mse)# Mean Absolute Error
mae = eval.evaluate(pred.predictions, {eval.metricName: "mae"})
print("MAE: %.3f" % mae)# r2 - coefficient of determination
r2 = eval.evaluate(pred.predictions, {eval.metricName: "r2"})
print("r2: %.3f" %r2)
就是这样。你已经在 Google Colab 中使用 Pyspark 创建了你的第一个机器学习模型。
你可以从 github 的这里获得完整的代码。
请让我知道,如果你遇到任何其他的新手问题,我也许可以帮助你。如果可以的话,我很乐意帮助你!
Pyspark —将您的特征工程包装在管道中
将变量创建集成到 spark 管道的指南
https://upload.wikimedia.org/wikipedia/commons/f/f3/Apache_Spark_logo.svg
为了有一个更干净和更工业化的代码,创建一个处理特征工程的管道对象可能是有用的。假设我们有这种类型的数据帧:
df.show()+----------+-----+
| date|sales|
+----------+-----+
|2018-12-22| 17|
|2017-01-08| 22|
|2015-08-25| 48|
|2015-03-12| 150|
+----------+-----+
然后我们想创建从日期派生的变量。大多数时候,我们会做这样的事情:
现在,我们希望将这些变量的创建集成到 spark 的管道中,此外,在它们的计算之前采取一些保护措施。为此,我们将创建一个继承 spark 管道的[Transformer](https://spark.apache.org/docs/latest/ml-pipeline.html#transformers)
方法的的类,并且我们将添加一个在计算前检查输入的函数。
- 我们的类继承了 Spark
[Transforme](https://spark.apache.org/docs/latest/ml-pipeline.html#transformers)r
的属性,这允许我们将其插入到管道中。 [this](https://spark.apache.org/docs/latest/api/java/org/apache/spark/ml/util/Identifiable.html)
函数允许我们通过给对象分配一个唯一的 ID 来使我们的对象在管道中可识别和不可变[defaultCopy](https://spark.apache.org/docs/1.5.1/api/java/org/apache/spark/ml/param/Params.html#defaultCopy(org.apache.spark.ml.param.ParamMap))
尝试创建一个具有相同 UID 的新实例。然后,它复制嵌入的和额外的参数,并返回新的实例。- 然后使用 check_input_type 函数检查输入字段的格式是否正确,最后我们简单地实现
[SQL Functions](https://spark.apache.org/docs/2.3.0/api/sql/index.html)
F.day of month 来返回我们的列。
我们将重用这个框架来创建我们需要的其他变量,他唯一要做的改变就是 ID 和 _transform 函数
我们已经定义了具有相同框架的 MonthQuarterExtractor,与[withColumn](https://spark.apache.org/docs/1.6.1/api/java/org/apache/spark/sql/DataFrame.html#withColumn(java.lang.String,%20org.apache.spark.sql.Column))
方法相比,它可能看起来有点冗长,但是要干净得多!
最后,我们对年份变量做同样的处理。
现在让我们将它们集成到我们的管道中。
df.show()+----------+-----+----------+----+------------+
| date|sales|dayofmonth|year|monthquarter|
+----------+-----+----------+----+------------+
|2018-12-22| 17| 22|2018| 2|
|2017-01-08| 22| 8|2017| 0|
|2015-08-25| 48| 25|2015| 3|
|2015-03-12| 150| 12|2015| 1|
+----------+-----+----------+----+------------+
诀窍是,我们创建了一个“海关”变压器,并将其插入火花管道。也可以将它们与其他对象、向量汇编器、字符串索引器或其他对象一起插入管道。
最后
Spark 管道是一个非常强大的工具,我们可以在一个管道中管理几乎整个数据科学项目,同时保持每个对象的可追溯性,并允许更简单的代码工业化,不要犹豫滥用它!
感谢您阅读我,如果您对 Pyspark 的更多技巧|教程感兴趣,请不要犹豫,留下您的评论。 (Y ou 可以在这里找到代码*😗)
为什么 Pyspider 可能是初学者最好的刮擦仪表板之一
py spider——竞争对手监控指标的实际应用
知己知彼,百战不殆——孙子
最近,我为公司构建了多个爬虫,我开始发现很难关注爬虫的性能。因此,我在网上搜索,看看是否存在一个 python 包,它不仅可以更简单地构建一个爬虫,还可以内置一个仪表板来跟踪爬虫的执行。
然后,我发现了这个 python 包——py spider,它真的很有用,尤其是在下面这几点:
- 更容易调试——它有一个用户界面让你知道哪个部分出错了。
- 内置仪表盘 —用于监控目的。
- 兼容 javascript —不像 Scrapy 需要安装 scrapy-splash 来渲染 Javascript 网站,但是 Pyspider 提供了 Puppeteer ,这是 Google 用 Javascript 开发的一个非常著名和强大的库,用于网页抓取。
- 数据库支持 — MySQL、MongoDB 和 PostgreSQL。
- 可扩展性 —分布式架构。
监控竞争对手指标
现在,我将向您展示一个关于 Pyspider 的用例,并告诉您 Pyspider 的酷功能,但如果您希望我介绍 Pyspider,请在下面留下评论,我将写一篇帖子向您介绍该框架及其关键概念。
我将更多地关注 Pyspider,而不是进入网络抓取部分的每个细节。如果你对学习网页抓取更感兴趣,请访问我的系列文章 使用 Python 来一步一步的解释。
我们开始吧!
Similarweb —一个在线竞争情报工具,为任何网站提供流量和营销见解。我将向你展示如何利用木偶师的力量来清理这个网站。
Snapshot of the Similar website
安装 pyspider
pip install pyspider
启动 pyspider
因为我们将抓取 javascript 渲染的网页,所以我们将使用pyspider all
而不是pyspider
。在命令提示符下运行它。
pyspider all
如果您看到上面的消息,这意味着 pyspider 在您的本地主机端口 5000 上成功启动。
浏览 pyspider
打开你的浏览器,浏览到地址 localhost:5000(即在你的地址栏输入 http://localhost:5000/ ),你会看到类似下图的东西。
然后我们将点击创建按钮(在左下角)来创建我们的刮刀。
然后,将项目名称填写为 Similarweb,并再次点击创建按钮。
之后你就可以看到上面的截图了,让我们开始吧。
脚本
这是我创建的一个简单的脚本。为了解释这是如何工作的,我将把它分成 3 个部分。
脚本—第 1 部分
如上面要点的第 8–15 行所示,初始化请求的标题。
[@every](http://twitter.com/every)(minutes=24 * 60)
def on_start(self):
self.crawl('[https://www.similarweb.com/'](https://www.similarweb.com/'),
fetch_type = 'chrome',
validate_cert = False,
headers = self.headers,
callback=self.index_page)
默认情况下,该功能将执行 on_start 功能。有五个变量要填入抓取函数。您会注意到 每 装饰,这意味着该功能将每 24*60 分钟执行一次,即 1 天。
- https://www.similarweb.com/:你想要抓取的网址,所以在这个例子中,我们将首先抓取主页。
- fetch_type:参数设置为 chrome,表示使用木偶师渲染 javascript 网站。
- validate_cert:参数为 False,因此 Pyspider 将跳过服务器证书的验证。
- 标题:请求网页时使用我们之前定义的标题。
- 回调:调用
index_page
函数作为下一个要解析的函数。
脚本—第 2 部分
[@config](http://twitter.com/config)(age=10 * 24 * 60 * 60)
def index_page(self, response):
print(response.cookies)
self.crawl('[https://www.similarweb.com/website/google.com'](https://www.similarweb.com/website/google.com'),
fetch_type = 'chrome',
validate_cert = False,
headers = self.headers,
cookies = response.cookies,
callback=self.index_page_1)
在我们的情况下,我们感兴趣的竞争对手是 google.com。这就是为什么网址是https://www.similarweb.com/website/google.com的原因。
你会注意到还有另一个装饰器叫做 config ,这个装饰器是用来表示这个函数是否每 10 天只运行一次。 抓取 功能的参数与上一个类似,只有一点不同:
- cookie:在请求数据时,我们从上一个会话获取 cookie 作为这个会话的输入。
脚本—第 3 部分
[@config](http://twitter.com/config)(age=10 * 24 * 60 * 60)
def index_page_1(self, response):
return {
response.doc('span.engagementInfo-param.engagementInfo-param--large.u-text-ellipsis').text(): response.doc('span.engagementInfo-valueNumber.js-countValue').text().split()[0]
}
这个函数只是返回总访问量作为字典。Pyspider 使用 Pyquery css 作为主路径选择器。
结果
所以复制要点代码并粘贴到右边的面板,如图所示。然后单击右上角的 save 按钮(在带有紫色边缘的框中高亮显示)保存脚本。之后,单击 run 按钮(在带有蓝色边缘的框中突出显示)运行代码。
单击“跟随”按钮,跟随刮擦过程的流程。
然后点击箭头按钮(紫色边框内高亮显示)继续下一个刮程。
再次单击箭头按钮。
看紫色的方框区域,这是刮削的输出。总共有 604.9 亿人次访问 google.com。
仪表盘
这是铲运机的总览仪表板。您可以单击紫色框(运行按钮)来执行 crawler。除此之外,您还可以通过单击红框(结果按钮)将结果保存到 CSV 或 JSON 文件中。
看右上角的紫色框,你会发现我之前说过的要下载的文件选项。点击按钮,你会得到你需要的格式(即 JSON/CSV)的结果。
最终想法
Pyspider 是一个非常有用的工具,它可以刮得非常快,但是如果你正在处理实现反爬行机制的网站,我会建议你使用 Scrapy 来代替。
谢谢你读到帖子的最后部分,真的很感激。
我将每周发布内容,所以请随时在下面留下您可能感兴趣的话题的评论,我将努力为您创建内容。
关于作者
Low 魏宏是 Shopee 的数据科学家。他的经验更多地涉及抓取网站,创建数据管道,以及实施机器学习模型来解决业务问题。
他提供爬行服务,能够为你提供你所需要的准确和干净的数据。你可以访问 这个网站 查看他的作品集,也可以联系他获取抓取服务。
在媒体上阅读低纬鸿的作品。Shopee 的数据科学家。每天,低伟鸿和其他成千上万的…
medium.com](https://medium.com/@lowweihong?source=post_page---------------------------)