Scala 机器学习项目(一)

原文:annas-archive.org/md5/4e1b7010caf1fbe3188c9c515fe244d4

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习通过将数据转化为可操作的智能,已经对学术界和工业界产生了巨大的影响。另一方面,Scala 在过去几年中在数据科学和分析领域的应用稳步增长。本书是为那些具备复杂数值计算背景并希望学习更多实践机器学习应用开发的资料科学家、数据工程师和深度学习爱好者编写的。

所以,如果您精通机器学习概念,并希望通过深入实际应用,利用 Scala 的强大功能扩展您的知识,那么这本书正是您所需要的!通过 11 个完整的项目,您将熟悉如 Spark ML、H2O、Zeppelin、DeepLearning4j 和 MXNet 等流行的机器学习库。

阅读完本书并实践所有项目后,您将能够掌握数值计算、深度学习和函数式编程,执行复杂的数值任务。因此,您可以在生产环境中开发、构建并部署研究和商业项目。

本书并不需要从头到尾阅读。您可以翻阅到您正在尝试实现的目标相关的章节,或者那些激发您兴趣的章节。任何改进的反馈都是欢迎的。

祝阅读愉快!

本书的适用人群

如果您想利用 Scala 和开源库(如 Spark ML、Deeplearning4j、H2O、MXNet 和 Zeppelin)的强大功能来理解大数据,那么本书适合您。建议对 Scala 和 Scala Play 框架有较强的理解,基本的机器学习技术知识将是额外的优势。

本书涵盖的内容

第一章,分析保险严重性索赔,展示了如何使用一些广泛使用的回归技术开发预测模型来分析保险严重性索赔。我们将演示如何将此模型部署到生产环境中。

第二章,分析与预测电信流失,使用 Orange Telecoms 流失数据集,其中包含清洗后的客户活动和流失标签,指定客户是否取消了订阅,来开发一个实际的预测模型。

第三章,基于历史数据和实时数据的高频比特币价格预测,展示了如何开发一个收集历史数据和实时数据的实际项目。我们预测未来几周、几个月等的比特币价格。此外,我们演示了如何为比特币在线交易生成简单的信号。最后,本章将整个应用程序作为 Web 应用程序,使用 Scala Play 框架进行包装。

第四章,大规模聚类与种族预测,使用来自 1000 基因组计划的基因组变异数据,应用 K-means 聚类方法对可扩展的基因组数据进行分析。目的是对种群规模的基因型变异进行聚类。最后,我们训练深度神经网络和随机森林模型来预测种族。

第五章,自然语言处理中的主题建模——对大规模文本的更好洞察,展示了如何利用基于 Spark 的 LDA 算法和斯坦福 NLP 开发主题建模应用,处理大规模原始文本。

第六章,开发基于模型的电影推荐引擎,展示了如何通过奇异值分解、ALS 和矩阵分解的相互操作,开发一个可扩展的电影推荐引擎。本章将使用电影镜头数据集进行端到端项目。

第七章,使用 Q 学习和 Scala Play 框架进行期权交易,在现实的 IBM 股票数据集上应用强化 Q 学习算法,并设计一个由反馈和奖励驱动的机器学习系统。目标是开发一个名为期权交易的实际应用。最后,本章将整个应用作为 Web 应用封装,使用 Scala Play 框架。

第八章,使用深度神经网络进行银行电话营销的客户订阅评估,是一个端到端项目,展示了如何解决一个名为客户订阅评估的现实问题。将使用银行电话营销数据集训练一个 H2O 深度神经网络。最后,本章评估该预测模型的性能。

第九章,使用自编码器和异常检测进行欺诈分析,使用自编码器和异常检测技术进行欺诈分析。所用数据集是由 Worldline 与ULB布鲁塞尔自由大学)机器学习小组在研究合作期间收集和分析的欺诈检测数据集。

第十章,使用递归神经网络进行人体活动识别,包括另一个端到端项目,展示了如何使用名为 LSTM 的 RNN 实现进行人体活动识别,使用智能手机传感器数据集。

第十一章,使用卷积神经网络进行图像分类,展示了如何开发预测分析应用,如图像分类,使用卷积神经网络对名为 Yelp 的真实图像数据集进行处理。

为了最大限度地利用本书

本书面向开发人员、数据分析师和深度学习爱好者,适合那些对复杂数值计算没有太多背景知识,但希望了解深度学习是什么的人。建议具备扎实的 Scala 编程基础及其函数式编程概念。对 Spark ML、H2O、Zeppelin、DeepLearning4j 和 MXNet 的基本了解及高层次知识将有助于理解本书。此外,假设读者具备基本的构建工具(如 Maven 和 SBT)知识。

所有示例都使用 Scala 在 Ubuntu 16.04 LTS 64 位和 Windows 10 64 位系统上实现。你还需要以下内容(最好是最新版本):

  • Apache Spark 2.0.0(或更高版本)

  • MXNet、Zeppelin、DeepLearning4j 和 H2O(请参见章节和提供的pom.xml文件中的详细信息)

  • Hadoop 2.7(或更高版本)

  • Java(JDK 和 JRE)1.7+/1.8+

  • Scala 2.11.x(或更高版本)

  • Eclipse Mars 或 Luna(最新版本),带有 Maven 插件(2.9+)、Maven 编译插件(2.3.2+)和 Maven 组装插件(2.4.1+)

  • IntelliJ IDE

  • 安装 SBT 插件和 Scala Play 框架

需要一台至少配备 Core i3 处理器的计算机,建议使用 Core i5,或者使用 Core i7 以获得最佳效果。然而,多核处理将提供更快的数据处理和可扩展性。对于独立模式,建议至少有 8GB RAM;对于单个虚拟机,使用至少 32GB RAM,对于集群则需要更高配置。你应该有足够的存储空间来运行大型作业(具体取决于你将处理的数据集大小);最好有至少 50GB 的空闲硬盘存储空间(独立模式和 SQL 数据仓库均适用)。

推荐使用 Linux 发行版(包括 Debian、Ubuntu、Fedora、RHEL、CentOS 等)。更具体地说,例如,对于 Ubuntu,建议使用 14.04(LTS)64 位(或更高版本)的完整安装,VMWare Player 12 或 VirtualBox。你可以在 Windows(XP/7/8/10)或 Mac OS X(10.4.7+)上运行 Spark 作业。

下载示例代码文件

你可以从www.packtpub.com的账户中下载本书的示例代码文件。如果你从其他地方购买了本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。

你可以通过以下步骤下载代码文件:

  1. www.packtpub.com登录或注册。

  2. 选择 SUPPORT 标签。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用以下最新版本的工具解压或提取文件夹:

  • Windows 的 WinRAR/7-Zip

  • Zipeg/iZip/UnRarX(适用于 Mac)

  • Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,地址是 github.com/PacktPublishing/Scala-Machine-Learning-Projects。我们还提供了来自我们丰富书籍和视频目录的其他代码包,地址是 github.com/PacktPublishing/。快来看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,里面包含本书中使用的截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/ScalaMachineLearningProjects_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“将下载的 WebStorm-10*.dmg 磁盘镜像文件挂载为系统中的另一个磁盘。”

代码块的设置如下:

val cv = new CrossValidator()
      .setEstimator(pipeline)
      .setEvaluator(new RegressionEvaluator)
      .setEstimatorParamMaps(paramGrid)
      .setNumFolds(numFolds)

Scala 功能代码块如下所示:

 def variantId(genotype: Genotype): String = {
      val name = genotype.getVariant.getContigName
      val start = genotype.getVariant.getStart
      val end = genotype.getVariant.getEnd
      s"$name:$start:$end"
  }

当我们希望特别提醒你注意某个代码块的部分内容时,相关的行或项会以粗体显示:

var paramGrid = new ParamGridBuilder()
      .addGrid(dTree.impurity, "gini" :: "entropy" :: Nil)
      .addGrid(dTree.maxBins, 3 :: 5 :: 9 :: 15 :: 23 :: 31 :: Nil)
      .addGrid(dTree.maxDepth, 5 :: 10 :: 15 :: 20 :: 25 :: 30 :: Nil)
      .build()

任何命令行输入或输出都按如下方式书写:

$ sudo mkdir Bitcoin
$ cd Bitcoin

粗体:表示新术语、重要词汇或屏幕上显示的词汇。例如,菜单或对话框中的单词会像这样显示在文本中。示例:“在管理面板中选择系统信息。”

警告或重要提示会像这样显示。

提示和技巧会像这样显示。

与我们联系

我们始终欢迎读者的反馈。

一般反馈:通过电子邮件 feedback@packtpub.com 联系我们,并在邮件主题中注明书名。如果你对本书的任何部分有疑问,请通过 questions@packtpub.com 联系我们。

勘误:尽管我们已尽力确保内容的准确性,但难免会有错误。如果你发现本书中的错误,请向我们报告。请访问 www.packtpub.com/submit-errata,选择你的书籍,点击“勘误提交表单”链接,输入详细信息。

盗版:如果你在互联网上发现任何非法复制的我们的作品,请提供该位置地址或网站名称,我们将不胜感激。请通过 copyright@packtpub.com 联系我们,并附上相关材料的链接。

如果你有兴趣成为一名作者:如果你对某个领域有专长,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com

评价

请留下评论。在阅读并使用本书后,何不在您购买书籍的网站上留下评论?潜在读者可以看到并参考您的客观意见做出购买决定,我们在 Packt 也能了解您对我们产品的看法,而我们的作者也能看到您对他们书籍的反馈。谢谢!

欲了解更多关于 Packt 的信息,请访问packtpub.com

第一章:分析保险赔付严重度

预测保险公司理赔的成本,从而预测赔付严重度,是一个需要准确解决的现实问题。在本章中,我们将向您展示如何使用一些最广泛使用的回归算法,开发一个用于分析保险赔付严重度的预测模型。

我们将从简单的线性回归LR)开始,看看如何通过一些集成技术(如梯度提升树GBT)回归器)提高性能。接着我们将研究如何使用随机森林回归器提升性能。最后,我们将展示如何选择最佳模型并将其部署到生产环境中。此外,我们还将提供一些关于机器学习工作流程、超参数调优和交叉验证的背景知识。

对于实现,我们将使用Spark ML API,以实现更快的计算和大规模扩展。简而言之,在整个端到端项目中,我们将学习以下主题:

  • 机器学习与学习工作流程

  • 机器学习模型的超参数调优与交叉验证

  • 用于分析保险赔付严重度的线性回归(LR)

  • 使用梯度提升回归器提高性能

  • 使用随机森林回归器提升性能

  • 模型部署

机器学习与学习工作流程

机器学习ML)是使用一组统计学和数学算法来执行任务,如概念学习、预测建模、聚类和挖掘有用模式。最终目标是以一种自动化的方式改进学习,使得不再需要人工干预,或者尽可能减少人工干预的程度。

我们现在引用Tom M. Mitchell在《机器学习》一书中的著名定义(Tom Mitchell, McGraw Hill, 1997*),他从计算机科学的角度解释了什么是学习:

“计算机程序被称为从经验 E 中学习,针对某类任务 T 和性能度量 P,如果它在 T 类任务中的表现,通过 P 衡量,在经验 E 下有所提高。”

根据前面的定义,我们可以得出结论:计算机程序或机器可以执行以下操作:

  • 从数据和历史中学习

  • 通过经验得到改善

  • 交互式地增强一个可以用来预测结果的模型

一个典型的机器学习(ML)函数可以被表示为一个凸优化问题,目的是找到一个凸函数f的最小值,该函数依赖于一个变量向量w(权重),并且包含d条记录。形式上,我们可以将其写为以下优化问题:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/87f40bc8-35a2-4bab-ae37-9de1886ef0d4.png

在这里,目标函数的形式为:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/5050b364-5c61-4106-b65d-5594a34ca97f.png

在这里,向量 https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/1bc24f84-8db5-4cbd-a0c3-546b033d7e06.png1≤i≤n 的训练数据点,它们是我们最终想要预测的相应标签。如果 L(w;x,y) 可以表示为 wTxy 的函数,我们称该方法为线性

目标函数 f 有两个组成部分:

  • 控制模型复杂度的正则化器

  • 测量模型在训练数据上误差的损失函数

损失函数 L(w;) 通常是 w 的凸函数。固定的正则化参数 λ≥0 定义了训练误差最小化和模型复杂度最小化之间的权衡,以避免过拟合。在各章节中,我们将详细学习不同的学习类型和算法。

另一方面,深度神经网络DNN)是深度学习DL)的核心,它通过提供建模复杂和高级数据抽象的算法,能够更好地利用大规模数据集来构建复杂的模型。

有一些广泛使用的基于人工神经网络的深度学习架构:DNN、胶囊网络、限制玻尔兹曼机、深度信念网络、矩阵分解机和递归神经网络。

这些架构已广泛应用于计算机视觉、语音识别、自然语言处理、音频识别、社交网络过滤、机器翻译、生物信息学和药物设计等领域。在各章节中,我们将看到多个使用这些架构的实际案例,以实现最先进的预测精度。

典型的机器学习工作流程

典型的机器学习应用涉及多个处理步骤,从输入到输出,形成一个科学工作流程,如图 1,机器学习工作流程所示。一个典型的机器学习应用包括以下步骤:

  1. 加载数据

  2. 将数据解析成算法所需的输入格式

  3. 对数据进行预处理并处理缺失值

  4. 将数据分为三个集合,分别用于训练、测试和验证(训练集和验证集),以及一个用于测试模型(测试数据集)

  5. 运行算法来构建和训练你的机器学习模型

  6. 使用训练数据进行预测并观察结果

  7. 使用测试数据测试并评估模型,或者使用交叉验证技术通过第三个数据集(称为验证数据集)来验证模型。

  8. 调整模型以提高性能和准确性

  9. 扩展模型,使其能够处理未来的大规模数据集

  10. 在生产环境中部署机器学习模型:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/a91968fa-0d1b-4233-a91f-81b0c24a6953.png

图 1:机器学习工作流程

上述工作流程表示了解决机器学习问题的几个步骤,其中,机器学习任务可以大致分为监督学习、无监督学习、半监督学习、强化学习和推荐系统。下面的图 2,监督学习的应用显示了监督学习的示意图。当算法找到了所需的模式后,这些模式可以用于对未标记的测试数据进行预测:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/893ef2af-4c84-4f42-af07-2f3eb3cabd97.png

图 2:监督学习的应用

示例包括用于解决监督学习问题的分类和回归,从而可以基于这些问题构建预测分析的预测模型。在接下来的章节中,我们将提供多个监督学习的示例,如 LR、逻辑回归、随机森林、决策树、朴素贝叶斯、多层感知机等。

回归算法旨在产生连续的输出。输入可以是离散的也可以是连续的:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/6b7d7c39-737e-4c4a-8774-faf7797f90cc.png

图 3:回归算法旨在产生连续输出

而分类算法则旨在从一组离散或连续的输入值中产生离散的输出。这一区别很重要,因为离散值的输出更适合由分类处理,这将在后续章节中讨论:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/99801fcd-6736-48e4-b691-e2af528dfe29.png

图 4:分类算法旨在产生离散输出

在本章中,我们将主要关注监督回归算法。我们将从描述问题陈述开始,然后介绍非常简单的 LR 算法。通常,这些机器学习模型的性能通过超参数调整和交叉验证技术进行优化。因此,简要了解它们是必要的,这样我们才能在后续章节中轻松使用它们。

超参数调整和交叉验证

调整算法简单来说是一个过程,通过这个过程可以使算法在运行时间和内存使用方面达到最佳表现。在贝叶斯统计学中,超参数是先验分布的一个参数。在机器学习中,超参数指的是那些无法通过常规训练过程直接学习到的参数。

超参数通常在实际训练过程开始之前就已固定。通过为这些超参数设置不同的值,训练不同的模型,然后通过测试它们来决定哪些效果最好。以下是一些典型的此类参数示例:

  • 树的叶子数、箱数或深度

  • 迭代次数

  • 矩阵分解中的潜在因子数量

  • 学习率

  • 深度神经网络中的隐藏层数量

  • k-means 聚类中的簇数量等等

简而言之,超参数调优是一种根据所呈现数据的表现选择合适的超参数组合的技术。它是从机器学习算法中获取有意义和准确结果的基本要求之一。下图展示了模型调优过程、需要考虑的事项以及工作流程:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/91d93133-43aa-4cc8-9d2f-45e076ca4033.png

图 5:模型调优过程

交叉验证(也称为旋转估计)是一种用于评估统计分析和结果质量的模型验证技术。其目标是使模型对独立的测试集具有较强的泛化能力。如果你希望估计预测模型在实践中部署为机器学习应用时的表现,交叉验证会有所帮助。在交叉验证过程中,通常会使用已知类型的数据集训练模型。

相反,它是使用一个未知类型的数据集进行测试。在这方面,交叉验证有助于通过使用验证集在训练阶段描述数据集,以测试模型。有两种类型的交叉验证,具体如下:

  • 穷尽性交叉验证:包括留 p 交叉验证和留一交叉验证

  • 非穷尽性交叉验证:包括 K 折交叉验证和重复随机子抽样交叉验证

在大多数情况下,研究人员/数据科学家/数据工程师使用 10 折交叉验证,而不是在验证集上进行测试(见 图 610 折交叉验证技术)。正如下图所示,这种交叉验证技术是所有使用案例和问题类型中最广泛使用的。

基本上,使用该技术时,您的完整训练数据会被分割成若干个折叠。这个参数是可以指定的。然后,整个流程会针对每个折叠运行一次,并为每个折叠训练一个机器学习模型。最后,通过分类器的投票机制或回归的平均值将获得的不同机器学习模型结合起来:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/5b0e92fa-300e-4609-be4d-3a658d7e9779.png

图 6:10 折交叉验证技术

此外,为了减少变异性,交叉验证会进行多次迭代,使用不同的数据分割;最后,验证结果会在各轮次中进行平均。

分析和预测保险索赔的严重性

预测保险公司索赔的费用,从而推测其严重性,是一个需要以更精确和自动化的方式解决的现实问题。在本示例中,我们将做类似的事情。

我们将从简单的逻辑回归开始,并学习如何使用一些集成技术(如随机森林回归器)来提高性能。接着,我们将看看如何使用梯度提升回归器来进一步提升性能。最后,我们将展示如何选择最佳模型并将其部署到生产环境中。

动机

当一个人遭遇严重的车祸时,他的关注点在于自己的生命、家人、孩子、朋友和亲人。然而,一旦提交了保险索赔文件,计算索赔严重程度的整个纸质流程就成了一项繁琐的任务。

这就是为什么保险公司不断寻求创新思路,自动化改进客户索赔服务的原因。因此,预测分析是预测索赔费用,从而预测其严重程度的可行解决方案,基于现有和历史数据。

数据集描述

将使用来自Allstate 保险公司的数据集,该数据集包含超过 30 万个示例,数据是经过掩码处理和匿名化的,并包含超过 100 个分类和数值属性,符合保密性约束,足够用于构建和评估各种机器学习技术。

数据集是从 Kaggle 网站下载的,网址是 www.kaggle.com/c/allstate-claims-severity/data。数据集中的每一行代表一次保险索赔。现在,任务是预测loss列的值。以cat开头的变量是分类变量,而以cont开头的变量是连续变量。

需要注意的是,Allstate 公司是美国第二大保险公司,成立于 1931 年。我们正在努力使整个过程自动化,预测事故和损坏索赔的费用,从而预测其严重程度。

数据集的探索性分析

让我们看看一些数据属性(可以使用EDA.scala文件)。首先,我们需要读取训练集,以查看可用的属性。首先,将你的训练集放在项目目录或其他位置,并相应地指向它:

val train = "data/insurance_train.csv"

我希望你已经在机器上安装并配置了 Java、Scala 和 Spark。如果没有,请先完成安装。无论如何,我假设它们已经安装好了。那么,让我们创建一个活跃的 Spark 会话,这是任何 Spark 应用程序的入口:

val spark = SparkSessionCreate.createSession()
import spark.implicits._

Scala REPL 中的 Spark 会话别名

如果你在 Scala REPL 中,Spark 会话别名spark已经定义好了,所以可以直接开始。

在这里,我有一个名为createSession()的方法,它位于SparkSessionCreate类中,代码如下:

import org.apache.spark.sql.SparkSession 

object SparkSessionCreate { 
  def createSession(): SparkSession = { 
    val spark = SparkSession 
      .builder 
      .master("local[*]") // adjust accordingly 
      .config("spark.sql.warehouse.dir", "E:/Exp/") //change accordingly 
      .appName("MySparkSession") //change accordingly 
      .getOrCreate() 
    return spark 
    }
} 

由于在本书中会频繁使用此功能,我决定创建一个专门的方法。因此,我们使用read.csv方法加载、解析并创建 DataFrame,但使用 Databricks .csv 格式(也称为com.databricks.spark.csv),因为我们的数据集是以.csv格式提供的。

此时,我必须打断一下,告诉你一个非常有用的信息。由于我们将在接下来的章节中使用 Spark MLlib 和 ML API,因此,提前解决一些问题是值得的。如果你是 Windows 用户,那么我得告诉你一个很奇怪的问题,你在使用 Spark 时可能会遇到。

好的,事情是这样的,Spark 可以在WindowsMac OSLinux上运行。当你在 Windows 上使用EclipseIntelliJ IDEA开发 Spark 应用程序(或者通过 Spark 本地作业提交)时,你可能会遇到 I/O 异常错误,导致应用程序无法成功编译或被中断。

原因在于 Spark 期望在 Windows 上有一个Hadoop的运行环境。不幸的是,Spark二进制发布版(例如v2.2.0)不包含一些 Windows 本地组件(例如,winutils.exehadoop.dll等)。然而,这些是运行Hadoop在 Windows 上所必需的(而不是可选的)。因此,如果你无法确保运行环境,就会出现类似以下的 I/O 异常:

24/01/2018 11:11:10 
ERROR util.Shell: Failed to locate the winutils binary in the hadoop binary path
java.io.IOException: Could not locate executable null\bin\winutils.exe in the Hadoop binaries.

现在有两种方法来解决这个问题,针对 Windows 系统:

  1. 来自 IDE,如 Eclipse 和 IntelliJ IDEA:从github.com/steveloughran/winutils/tree/master/hadoop-2.7.1/bin/下载winutils.exe。然后下载并将其复制到 Spark 分发版中的bin文件夹——例如,spark-2.2.0-bin-hadoop2.7/bin/。然后选择项目 | 运行配置… | 环境 | 新建 | 创建一个名为HADOOP_HOME的变量,并在值字段中填入路径——例如,c:/spark-2.2.0-bin-hadoop2.7/bin/ | 确定 | 应用 | 运行。这样就完成了!

  2. 使用本地 Spark 作业提交:将winutils.exe文件路径添加到 hadoop 主目录,使用 System 设置属性——例如,在 Spark 代码中System.setProperty("hadoop.home.dir", "c:\\\spark-2.2.0-bin-hadoop2.7\\\bin\winutils.exe")

好的,让我们回到你原始的讨论。如果你看到上面的代码块,我们设置了读取 CSV 文件的头部,它直接应用于创建的 DataFrame 的列名,并且inferSchema属性被设置为true。如果你没有明确指定inferSchema配置,浮动值将被视为strings. 这可能导致VectorAssembler抛出像java.lang.IllegalArgumentException: Data type StringType is not supported的异常:

 val trainInput = spark.read 
    .option("header", "true") 
    .option("inferSchema", "true") 
    .format("com.databricks.spark.csv") 
    .load(train) 
    .cache 

现在让我们打印一下我们刚才创建的 DataFrame 的 schema。我已经简化了输出,只显示了几个列:

Println(trainInput.printSchema()) 
root 
 |-- id: integer (nullable = true) 
 |-- cat1: string (nullable = true) 
 |-- cat2: string (nullable = true) 
 |-- cat3: string (nullable = true) 
  ... 
 |-- cat115: string (nullable = true) 
 |-- cat116: string (nullable = true)
  ... 
 |-- cont14: double (nullable = true) 
 |-- loss: double (nullable = true) 

你可以看到有 116 个分类列用于分类特征。还有 14 个数值特征列。现在让我们使用count()方法看看数据集中有多少行:

println(df.count())
>>>
 188318 

上述的数字对于训练 ML 模型来说相当高。好的,现在让我们通过show()方法查看数据集的快照,但只选取一些列,以便更有意义。你可以使用df.show()来查看所有列:

df.select("id", "cat1", "cat2", "cat3", "cont1", "cont2", "cont3", "loss").show() 
>>> 

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/dc972f3b-871f-4080-8a6c-e85e23ab79e8.png

然而,如果你使用df.show()查看所有行,你会看到一些分类列包含了过多的类别。更具体地说,cat109cat116这些分类列包含了过多的类别,具体如下:

df.select("cat109", "cat110", "cat112", "cat113", "cat116").show() 
>>> 

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/55f4b79f-1af8-438a-8f99-4baedbed98cf.png

在后续阶段,值得删除这些列,以去除数据集中的偏斜性。需要注意的是,在统计学中,偏斜度是衡量一个实值随机变量的概率分布相对于均值的非对称性的一种度量。

现在我们已经看到了数据集的快照,接下来值得查看一些其他统计信息,比如平均索赔或损失、最小值、最大损失等等,使用 Spark SQL 来进行计算。但在此之前,我们先将最后一列的loss重命名为label,因为 ML 模型会对此产生警告。即使在回归模型中使用setLabelCol,它仍然会查找名为label的列。这会导致一个令人烦恼的错误,提示org.apache.spark.sql.AnalysisException: cannot resolve 'label' given input columns

val newDF = df.withColumnRenamed("loss", "label") 

现在,由于我们想要执行 SQL 查询,我们需要创建一个临时视图,以便操作可以在内存中执行:

newDF.createOrReplaceTempView("insurance") 

现在让我们计算客户声明的平均损失:

spark.sql("SELECT avg(insurance.label) as AVG_LOSS FROM insurance").show()
>>>
+------------------+
| AVG_LOSS |
+------------------+
|3037.3376856699924|
+------------------+

类似地,让我们看看到目前为止的最低索赔:

spark.sql("SELECT min(insurance.label) as MIN_LOSS FROM insurance").show() 
>>>  
+--------+
|MIN_LOSS|
+--------+
| 0.67|
+--------+

让我们看看到目前为止的最高索赔:

spark.sql("SELECT max(insurance.label) as MAX_LOSS FROM insurance").show() 
>>> 
+---------+
| MAX_LOSS|
+---------+
|121012.25|
+---------+

由于 Scala 或 Java 没有自带便捷的可视化库,我暂时无法做其他处理,但现在我们集中精力在数据预处理上,准备训练集之前进行清理。

数据预处理

既然我们已经查看了一些数据属性,接下来的任务是进行一些预处理,如清理数据,然后再准备训练集。对于这一部分,请使用Preprocessing.scala文件。对于这部分,需要以下导入:

import org.apache.spark.ml.feature.{ StringIndexer, StringIndexerModel}
import org.apache.spark.ml.feature.VectorAssembler

然后我们加载训练集和测试集,如以下代码所示:

var trainSample = 1.0 
var testSample = 1.0 
val train = "data/insurance_train.csv" 
val test = "data/insurance_test.csv" 
val spark = SparkSessionCreate.createSession() 
import spark.implicits._ 
println("Reading data from " + train + " file") 

 val trainInput = spark.read 
        .option("header", "true") 
        .option("inferSchema", "true") 
        .format("com.databricks.spark.csv") 
        .load(train) 
        .cache 

    val testInput = spark.read 
        .option("header", "true") 
        .option("inferSchema", "true") 
        .format("com.databricks.spark.csv") 
        .load(test) 
        .cache 

下一步任务是为我们的 ML 模型准备训练集和测试集。在之前的训练数据框中,我们将loss重命名为label。接着,将train.csv的内容分割为训练数据和(交叉)验证数据,分别为 75%和 25%。

test.csv的内容用于评估 ML 模型。两个原始数据框也进行了采样,这对在本地机器上运行快速执行非常有用:

println("Preparing data for training model") 
var data = trainInput.withColumnRenamed("loss", "label").sample(false, trainSample) 

我们还应该进行空值检查。这里,我采用了一种简单的方法。因为如果训练数据框架中包含任何空值,我们就会完全删除这些行。这是有意义的,因为在 188,318 行数据中,删除少数几行并不会造成太大问题。不过,你也可以采取其他方法,如空值插补:

var DF = data.na.drop() 
if (data == DF) 
  println("No null values in the DataFrame")     
else{ 
  println("Null values exist in the DataFrame") 
  data = DF 
} 
val seed = 12345L 
val splits = data.randomSplit(Array(0.75, 0.25), seed) 
val (trainingData, validationData) = (splits(0), splits(1)) 

接着我们缓存这两个数据集,以便更快速地进行内存访问:

trainingData.cache 
validationData.cache 

此外,我们还应该对测试集进行采样,这是评估步骤中所需要的:

val testData = testInput.sample(false, testSample).cache 

由于训练集包含了数值型和分类值,我们需要分别识别并处理它们。首先,让我们只识别分类列:

def isCateg(c: String): Boolean = c.startsWith("cat") 
def categNewCol(c: String): String = if (isCateg(c)) s"idx_${c}" else c 

接下来,使用以下方法删除类别过多的列,这是我们在前一节中已经讨论过的:

def removeTooManyCategs(c: String): Boolean = !(c matches "cat(109$|110$|112$|113$|116$)")

接下来使用以下方法只选择特征列。所以本质上,我们应该删除 ID 列(因为 ID 只是客户的识别号码,不包含任何非平凡的信息)和标签列:

def onlyFeatureCols(c: String): Boolean = !(c matches "id|label") 

好的,到目前为止,我们已经处理了一些无关或不需要的列。现在下一步任务是构建最终的特征列集:

val featureCols = trainingData.columns 
    .filter(removeTooManyCategs) 
    .filter(onlyFeatureCols) 
    .map(categNewCol) 

StringIndexer将给定的字符串标签列编码为标签索引列。如果输入列是数值类型的,我们使用StringIndexer将其转换为字符串,并对字符串值进行索引。当下游管道组件(如 Estimator 或 Transformer)使用这个字符串索引标签时,必须将该组件的输入列设置为该字符串索引列名。在许多情况下,你可以通过setInputCol来设置输入列。

现在,我们需要使用StringIndexer()来处理类别列:

val stringIndexerStages = trainingData.columns.filter(isCateg) 
      .map(c => new StringIndexer() 
      .setInputCol(c) 
      .setOutputCol(categNewCol(c)) 
      .fit(trainInput.select(c).union(testInput.select(c)))) 

请注意,这不是一种高效的方法。另一种替代方法是使用 OneHotEncoder 估算器。

OneHotEncoder 将标签索引列映射到二进制向量列,每个向量最多只有一个值为 1。该编码允许期望连续特征的算法(如逻辑回归)利用类别特征。

现在让我们使用VectorAssembler()将给定的列列表转换为单一的向量列:

val assembler = new VectorAssembler() 
    .setInputCols(featureCols) 
    .setOutputCol("features")

VectorAssembler是一个转换器。它将给定的列列表合并为单一的向量列。它对于将原始特征和由不同特征转换器生成的特征合并为一个特征向量非常有用,以便训练机器学习模型,如逻辑回归和决策树。

在开始训练回归模型之前,这就是我们需要做的全部。首先,我们开始训练 LR 模型并评估其性能。

用 LR 预测保险赔偿的严重程度

正如你已经看到的,预测的损失包含连续值,也就是说,这是一个回归任务。因此,在此使用回归分析时,目标是预测一个连续的目标变量,而另一个领域——分类,则预测从有限集合中选择一个标签。

逻辑回归LR)属于回归算法家族。回归的目标是寻找变量之间的关系和依赖性。它通过线性函数建模连续标量因变量y(即标签或目标)与一个或多个(D 维向量)解释变量(也称为自变量、输入变量、特征、观察数据、观测值、属性、维度和数据点)x之间的关系:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/6e405005-952c-4f9c-92c8-cbf4e839e297.png

图 9:回归图将数据点(红色圆点)分开,蓝线为回归线

LR 模型描述了因变量 y 与一组相互依赖的自变量 x[i] 之间的关系。字母 AB 分别表示描述 y 轴截距和回归线斜率的常数:

y = A+Bx

图 9回归图将数据点(红色点)与回归线(蓝色线)分开,显示了一个简单的 LR 示例,只有一个自变量——即一组数据点和一个最佳拟合线,这是回归分析的结果。可以观察到,这条线并不完全通过所有数据点。

任何数据点(实际测量值)与回归线(预测值)之间的距离称为回归误差。较小的误差有助于更准确地预测未知值。当误差被减少到最小水平时,最终的回归误差会生成最佳拟合线。请注意,在回归误差方面没有单一的度量标准,以下是几种常见的度量:

  • 均方误差MSE):它是衡量拟合线与数据点接近程度的指标。MSE 越小,拟合程度越接近数据。

  • 均方根误差RMSE):它是均方误差(MSE)的平方根,但可能是最容易解释的统计量,因为它与纵轴上绘制的量具有相同的单位。

  • R 平方:R 平方是衡量数据与拟合回归线之间接近程度的统计量。R 平方总是介于 0 和 100%之间。R 平方越高,模型越能拟合数据。

  • 平均绝对误差MAE):MAE 衡量一组预测中误差的平均幅度,而不考虑其方向。它是测试样本中预测值与实际观察值之间的绝对差异的平均值,其中所有个体差异具有相同的权重。

  • 解释方差:在统计学中,解释方差衡量数学模型在多大程度上能够解释给定数据集的变化。

使用 LR 开发保险赔偿严重性预测模型

在本小节中,我们将开发一个预测分析模型,用于预测客户在事故损失中的赔偿严重性。我们从导入所需的库开始:

import org.apache.spark.ml.regression.{LinearRegression, LinearRegressionModel} 
import org.apache.spark.ml.{ Pipeline, PipelineModel } 
import org.apache.spark.ml.evaluation.RegressionEvaluator 
import org.apache.spark.ml.tuning.ParamGridBuilder 
import org.apache.spark.ml.tuning.CrossValidator 
import org.apache.spark.sql._ 
import org.apache.spark.sql.functions._ 
import org.apache.spark.mllib.evaluation.RegressionMetrics 

然后,我们创建一个活动的 Spark 会话,作为应用程序的入口点。此外,导入 implicits__,这是隐式转换所需的,如将 RDD 转换为 DataFrame。

val spark = SparkSessionCreate.createSession() 
import spark.implicits._ 

然后,我们定义一些超参数,如交叉验证的折数、最大迭代次数、回归参数的值、容差值以及弹性网络参数,如下所示:

val numFolds = 10 
val MaxIter: Seq[Int] = Seq(1000) 
val RegParam: Seq[Double] = Seq(0.001) 
val Tol: Seq[Double] = Seq(1e-6) 
val ElasticNetParam: Seq[Double] = Seq(0.001) 

现在,我们创建一个 LR 估计器:

val model = new LinearRegression()
        .setFeaturesCol("features")
        .setLabelCol("label") 

现在,让我们通过连接变换器和 LR 估计器来构建一个管道估计器:

println("Building ML pipeline") 
val pipeline = new Pipeline()
         .setStages((Preproessing.stringIndexerStages  
         :+ Preproessing.assembler) :+ model)

Spark ML 管道包含以下组件:

  • 数据框:用作中央数据存储,所有原始数据和中间结果都存储在这里。

  • 转换器:转换器通过添加额外的特征列将一个 DataFrame 转换成另一个 DataFrame。转换器是无状态的,意味着它们没有内部记忆,每次使用时的行为都完全相同。

  • 估算器:估算器是一种机器学习模型。与转换器不同,估算器包含内部状态表示,并且高度依赖于它已经见过的数据历史。

  • 管道:将前面的组件、DataFrame、转换器和估算器连接在一起。

  • 参数:机器学习算法有许多可调整的参数。这些称为超参数,而机器学习算法通过学习数据来拟合模型的值称为参数

在开始执行交叉验证之前,我们需要有一个参数网格(paramgrid)。所以让我们通过指定最大迭代次数、回归参数值、公差值和弹性网络参数来创建参数网格,如下所示:

val paramGrid = new ParamGridBuilder() 
      .addGrid(model.maxIter, MaxIter) 
      .addGrid(model.regParam, RegParam) 
      .addGrid(model.tol, Tol) 
      .addGrid(model.elasticNetParam, ElasticNetParam) 
      .build() 

现在,为了获得更好且稳定的性能,让我们准备 K 折交叉验证和网格搜索作为模型调优的一部分。正如你们可能猜到的,我将进行 10 折交叉验证。根据你的设置和数据集,可以自由调整折数:

println("Preparing K-fold Cross Validation and Grid Search: Model tuning") 
val cv = new CrossValidator() 
      .setEstimator(pipeline) 
      .setEvaluator(new RegressionEvaluator) 
      .setEstimatorParamMaps(paramGrid) 
      .setNumFolds(numFolds) 

太棒了——我们已经创建了交叉验证估算器。现在是训练 LR 模型的时候了:

println("Training model with Linear Regression algorithm") 
val cvModel = cv.fit(Preproessing.trainingData) 

现在,我们已经有了拟合的模型,这意味着它现在能够进行预测。所以,让我们开始在训练集和验证集上评估模型,并计算 RMSE、MSE、MAE、R 平方等指标:

println("Evaluating model on train and validation set and calculating RMSE") 
val trainPredictionsAndLabels = cvModel.transform(Preproessing.trainingData)
                .select("label", "prediction")
                .map { case Row(label: Double, prediction: Double) 
                => (label, prediction) }.rdd 

val validPredictionsAndLabels = cvModel.transform(Preproessing.validationData)
                                .select("label", "prediction")
                                .map { case Row(label: Double, prediction: Double) 
                                => (label, prediction) }.rdd 

val trainRegressionMetrics = new RegressionMetrics(trainPredictionsAndLabels) 
val validRegressionMetrics = new RegressionMetrics(validPredictionsAndLabels) 

太棒了!我们已经成功计算了训练集和测试集的原始预测结果。接下来,让我们寻找最佳模型:

val bestModel = cvModel.bestModel.asInstanceOf[PipelineModel] 

一旦我们有了最佳拟合并且通过交叉验证的模型,我们可以期望得到良好的预测准确性。现在,让我们观察训练集和验证集上的结果:

val results = "n=====================================================================n" + s"Param trainSample: ${Preproessing.trainSample}n" + 
      s"Param testSample: ${Preproessing.testSample}n" + 
      s"TrainingData count: ${Preproessing.trainingData.count}n" + 
      s"ValidationData count: ${Preproessing.validationData.count}n" + 
      s"TestData count: ${Preproessing.testData.count}n" +      "=====================================================================n" +   s"Param maxIter = ${MaxIter.mkString(",")}n" + 
      s"Param numFolds = ${numFolds}n" +      "=====================================================================n" +   s"Training data MSE = ${trainRegressionMetrics.meanSquaredError}n" + 
      s"Training data RMSE = ${trainRegressionMetrics.rootMeanSquaredError}n" + 
      s"Training data R-squared = ${trainRegressionMetrics.r2}n" + 
      s"Training data MAE = ${trainRegressionMetrics.meanAbsoluteError}n" + 
      s"Training data Explained variance = ${trainRegressionMetrics.explainedVariance}n" +      "=====================================================================n" +   s"Validation data MSE = ${validRegressionMetrics.meanSquaredError}n" + 
      s"Validation data RMSE = ${validRegressionMetrics.rootMeanSquaredError}n" + 
      s"Validation data R-squared = ${validRegressionMetrics.r2}n" + 
      s"Validation data MAE = ${validRegressionMetrics.meanAbsoluteError}n" + 
      s"Validation data Explained variance = ${validRegressionMetrics.explainedVariance}n" + 
      s"CV params explained: ${cvModel.explainParams}n" + 
      s"LR params explained: ${bestModel.stages.last.asInstanceOf[LinearRegressionModel].explainParams}n" +      "=====================================================================n" 

现在,我们将打印前面的结果,如下所示:

println(results)
>>> 
Building Machine Learning pipeline 
Reading data from data/insurance_train.csv file 
Null values exist in the DataFrame 
Training model with Linear Regression algorithm
===================================================================== 
Param trainSample: 1.0 
Param testSample: 1.0 
TrainingData count: 141194 
ValidationData count: 47124 
TestData count: 125546 
===================================================================== 
Param maxIter = 1000 
Param numFolds = 10 
===================================================================== 
Training data MSE = 4460667.3666198505 
Training data RMSE = 2112.0292059107164 
Training data R-squared = -0.1514435541595276 
Training data MAE = 1356.9375609756164 
Training data Explained variance = 8336528.638733305 
===================================================================== 
Validation data MSE = 4839128.978963534 
Validation data RMSE = 2199.802031766389 
Validation data R-squared = -0.24922962724089603 
Validation data MAE = 1356.419484419514 
Validation data Explained variance = 8724661.329105612 
CV params explained: estimator: estimator for selection (current: pipeline_d5024480c670) 
estimatorParamMaps: param maps for the estimator (current: [Lorg.apache.spark.ml.param.ParamMap;@2f0c9855) 
evaluator: evaluator used to select hyper-parameters that maximize the validated metric (current: regEval_00c707fcaa06) 
numFolds: number of folds for cross validation (>= 2) (default: 3, current: 10) 
seed: random seed (default: -1191137437) 
LR params explained: aggregationDepth: suggested depth for treeAggregate (>= 2) (default: 2) 
elasticNetParam: the ElasticNet mixing parameter, in range [0, 1]. For alpha = 0, the penalty is an L2 penalty. For alpha = 1, it is an L1 penalty (default: 0.0, current: 0.001) 
featuresCol: features column name (default: features, current: features) 
fitIntercept: whether to fit an intercept term (default: true) 
labelCol: label column name (default: label, current: label) 
maxIter: maximum number of iterations (>= 0) (default: 100, current: 1000) 
predictionCol: prediction column name (default: prediction) 
regParam: regularization parameter (>= 0) (default: 0.0, current: 0.001) 
solver: the solver algorithm for optimization. If this is not set or empty, default value is 'auto' (default: auto) 
standardization: whether to standardize the training features before fitting the model (default: true) 
tol: the convergence tolerance for iterative algorithms (>= 0) (default: 1.0E-6, current: 1.0E-6) 
weightCol: weight column name. If this is not set or empty, we treat all instance weights as 1.0 (undefined) 
===================================================================== 

所以,我们的预测模型在训练集和测试集上的 MAE 约为1356.419484419514。然而,在 Kaggle 的公共和私人排行榜上,MAE 要低得多(请访问:www.kaggle.com/c/allstate-claims-severity/leaderboard),公共和私人的 MAE 分别为 1096.92532 和 1109.70772。

等等!我们还没有完成。我们仍然需要在测试集上进行预测:

println("Run prediction on the test set") 
cvModel.transform(Preproessing.testData) 
      .select("id", "prediction") 
      .withColumnRenamed("prediction", "loss") 
      .coalesce(1) // to get all the predictions in a single csv file 
      .write.format("com.databricks.spark.csv")
      .option("header", "true") 
      .save("output/result_LR.csv")

前面的代码应生成一个名为result_LR.csv的 CSV 文件。如果我们打开文件,我们应该能够看到每个 ID(即索赔)对应的损失。我们将在本章结束时查看 LR、RF 和 GBT 的内容。尽管如此,结束 Spark 会话时,调用spark.stop()方法总是个好主意。

集成方法是一种学习算法,它创建一个由其他基础模型组成的模型。Spark ML 支持两种主要的集成算法,分别是基于决策树的 GBT 和随机森林。接下来,我们将看看是否可以通过显著减少 MAE 误差来提高预测准确度,使用 GBT。

用于预测保险赔付严重性的 GBT 回归器

为了最小化loss函数,梯度提升树GBT)通过迭代训练多棵决策树。在每次迭代中,算法使用当前的集成模型来预测每个训练实例的标签。

然后,原始预测与真实标签进行比较。因此,在下一次迭代中,决策树将帮助纠正之前的错误,如果数据集被重新标记以强调对预测不准确的训练实例进行训练。

既然我们讨论的是回归,那么讨论 GBT 的回归能力及其损失计算会更有意义。假设我们有以下设置:

  • N 数据实例

  • y[i] = 实例i的标签

  • x[i] = 实例i的特征

然后,*F(x[i])*函数是模型的预测标签;例如,它试图最小化误差,即损失:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/a116bede-a15a-490c-b5a4-cc9b15c285a1.png

现在,与决策树类似,GBT 也会:

  • 处理类别特征(当然也包括数值特征)

  • 扩展到多类分类设置

  • 执行二分类和回归(目前尚不支持多类分类)

  • 不需要特征缩放

  • 捕捉线性模型中(如线性回归)严重缺失的非线性和特征交互

训练过程中的验证:梯度提升可能会过拟合,尤其是在你使用更多树训练模型时。为了防止这个问题,训练过程中进行验证是非常有用的。

既然我们已经准备好了数据集,我们可以直接进入实现基于 GBT 的预测模型来预测保险赔付严重性。让我们从导入必要的包和库开始:

import org.apache.spark.ml.regression.{GBTRegressor, GBTRegressionModel} 
import org.apache.spark.ml.{Pipeline, PipelineModel} 
import org.apache.spark.ml.evaluation.RegressionEvaluator 
import org.apache.spark.ml.tuning.ParamGridBuilder 
import org.apache.spark.ml.tuning.CrossValidator 
import org.apache.spark.sql._ 
import org.apache.spark.sql.functions._ 
import org.apache.spark.mllib.evaluation.RegressionMetrics 

现在让我们定义并初始化训练 GBT 所需的超参数,例如树的数量、最大分箱数、交叉验证中使用的折数、训练的最大迭代次数,最后是最大树深度:

val NumTrees = Seq(5, 10, 15) 
val MaxBins = Seq(5, 7, 9) 
val numFolds = 10 
val MaxIter: Seq[Int] = Seq(10) 
val MaxDepth: Seq[Int] = Seq(10) 

然后,我们再次实例化一个 Spark 会话并启用隐式转换,如下所示:

val spark = SparkSessionCreate.createSession() 
import spark.implicits._ 

既然我们关心的是一个估算器算法,即 GBT:

val model = new GBTRegressor()
                .setFeaturesCol("features")
                .setLabelCol("label") 

现在,我们通过将变换和预测器串联在一起构建管道,如下所示:

val pipeline = new Pipeline().setStages((Preproessing.stringIndexerStages :+ Preproessing.assembler) :+ model) 

在开始执行交叉验证之前,我们需要一个参数网格。接下来,我们通过指定最大迭代次数、最大树深度和最大分箱数来开始创建参数网格:

val paramGrid = new ParamGridBuilder() 
      .addGrid(model.maxIter, MaxIter) 
      .addGrid(model.maxDepth, MaxDepth) 
      .addGrid(model.maxBins, MaxBins) 
      .build() 

现在,为了获得更好且稳定的性能,让我们准备 K-fold 交叉验证和网格搜索作为模型调优的一部分。如你所料,我将进行 10-fold 交叉验证。根据你的设置和数据集,你可以自由调整折数:

println("Preparing K-fold Cross Validation and Grid Search") 
val cv = new CrossValidator() 
      .setEstimator(pipeline) 
      .setEvaluator(new RegressionEvaluator) 
      .setEstimatorParamMaps(paramGrid) 
      .setNumFolds(numFolds) 

很棒,我们已经创建了交叉验证估算器。现在是时候训练 GBT 模型了:

println("Training model with GradientBoostedTrees algorithm ") 
val cvModel = cv.fit(Preproessing.trainingData) 

现在我们已经得到了拟合的模型,这意味着它现在能够进行预测。所以让我们开始在训练集和验证集上评估模型,并计算 RMSE、MSE、MAE、R-squared 等指标:

println("Evaluating model on train and test data and calculating RMSE") 
val trainPredictionsAndLabels = cvModel.transform(Preproessing.trainingData).select("label", "prediction").map { case Row(label: Double, prediction: Double) => (label, prediction) }.rdd 

val validPredictionsAndLabels = cvModel.transform(Preproessing.validationData).select("label", "prediction").map { case Row(label: Double, prediction: Double) => (label, prediction) }.rdd 

val trainRegressionMetrics = new RegressionMetrics(trainPredictionsAndLabels) 
val validRegressionMetrics = new RegressionMetrics(validPredictionsAndLabels) 

很好!我们已经成功计算了训练集和测试集的原始预测值。让我们开始寻找最佳模型:

val bestModel = cvModel.bestModel.asInstanceOf[PipelineModel] 

如前所述,使用 GBT 可以衡量特征重要性,这样在后续阶段我们可以决定哪些特征要使用,哪些要从 DataFrame 中删除。让我们找到之前创建的最佳模型的特征重要性,并按升序列出所有特征,如下所示:

val featureImportances = bestModel.stages.last.asInstanceOf[GBTRegressionModel].featureImportances.toArray 
val FI_to_List_sorted = featureImportances.toList.sorted.toArray  

一旦我们有了最佳拟合且交叉验证的模型,就可以期待良好的预测精度。现在让我们观察训练集和验证集上的结果:

val output = "n=====================================================================n" + s"Param trainSample: ${Preproessing.trainSample}n" + 
      s"Param testSample: ${Preproessing.testSample}n" + 
      s"TrainingData count: ${Preproessing.trainingData.count}n" + 
      s"ValidationData count: ${Preproessing.validationData.count}n" + 
      s"TestData count: ${Preproessing.testData.count}n" +      "=====================================================================n" +   s"Param maxIter = ${MaxIter.mkString(",")}n" + 
      s"Param maxDepth = ${MaxDepth.mkString(",")}n" + 
      s"Param numFolds = ${numFolds}n" +      "=====================================================================n" +   s"Training data MSE = ${trainRegressionMetrics.meanSquaredError}n" + 
      s"Training data RMSE = ${trainRegressionMetrics.rootMeanSquaredError}n" + 
      s"Training data R-squared = ${trainRegressionMetrics.r2}n" + 
      s"Training data MAE = ${trainRegressionMetrics.meanAbsoluteError}n" + 
      s"Training data Explained variance = ${trainRegressionMetrics.explainedVariance}n" +      "=====================================================================n" +    s"Validation data MSE = ${validRegressionMetrics.meanSquaredError}n" + 
      s"Validation data RMSE = ${validRegressionMetrics.rootMeanSquaredError}n" + 
      s"Validation data R-squared = ${validRegressionMetrics.r2}n" + 
      s"Validation data MAE = ${validRegressionMetrics.meanAbsoluteError}n" + 
      s"Validation data Explained variance = ${validRegressionMetrics.explainedVariance}n" +      "=====================================================================n" +   s"CV params explained: ${cvModel.explainParams}n" + 
      s"GBT params explained: ${bestModel.stages.last.asInstanceOf[GBTRegressionModel].explainParams}n" + s"GBT features importances:n ${Preproessing.featureCols.zip(FI_to_List_sorted).map(t => s"t${t._1} = ${t._2}").mkString("n")}n" +      "=====================================================================n" 

现在,我们按如下方式打印之前的结果:

println(results)
 >>> 
===================================================================== 
Param trainSample: 1.0 
Param testSample: 1.0 
TrainingData count: 141194 
ValidationData count: 47124 
TestData count: 125546 
===================================================================== 
Param maxIter = 10 
Param maxDepth = 10 
Param numFolds = 10 
===================================================================== 
Training data MSE = 2711134.460296872 
Training data RMSE = 1646.5522950385973 
Training data R-squared = 0.4979619968485668 
Training data MAE = 1126.582534126603 
Training data Explained variance = 8336528.638733303 
===================================================================== 
Validation data MSE = 4796065.983773314 
Validation data RMSE = 2189.9922337244293 
Validation data R-squared = 0.13708582379658474 
Validation data MAE = 1289.9808960385383 
Validation data Explained variance = 8724866.468978886 
===================================================================== 
CV params explained: estimator: estimator for selection (current: pipeline_9889176c6eda) 
estimatorParamMaps: param maps for the estimator (current: [Lorg.apache.spark.ml.param.ParamMap;@87dc030) 
evaluator: evaluator used to select hyper-parameters that maximize the validated metric (current: regEval_ceb3437b3ac7) 
numFolds: number of folds for cross validation (>= 2) (default: 3, current: 10) 
seed: random seed (default: -1191137437) 
GBT params explained: cacheNodeIds: If false, the algorithm will pass trees to executors to match instances with nodes. If true, the algorithm will cache node IDs for each instance. Caching can speed up training of deeper trees. (default: false) 
checkpointInterval: set checkpoint interval (>= 1) or disable checkpoint (-1). E.g. 10 means that the cache will get checkpointed every 10 iterations (default: 10) 
featuresCol: features column name (default: features, current: features) 
impurity: Criterion used for information gain calculation (case-insensitive). Supported options: variance (default: variance) 
labelCol: label column name (default: label, current: label) 
lossType: Loss function which GBT tries to minimize (case-insensitive). Supported options: squared, absolute (default: squared) 
maxBins: Max number of bins for discretizing continuous features. Must be >=2 and >= number of categories for any categorical feature. (default: 32) 
maxDepth: Maximum depth of the tree. (>= 0) E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes. (default: 5, current: 10) 
maxIter: maximum number of iterations (>= 0) (default: 20, current: 10) 
maxMemoryInMB: Maximum memory in MB allocated to histogram aggregation. (default: 256) 
minInfoGain: Minimum information gain for a split to be considered at a tree node. (default: 0.0) 
minInstancesPerNode: Minimum number of instances each child must have after split. If a split causes the left or right child to have fewer than minInstancesPerNode, the split will be discarded as invalid. Should be >= 1\. (default: 1) 
predictionCol: prediction column name (default: prediction) 
seed: random seed (default: -131597770) 
stepSize: Step size (a.k.a. learning rate) in interval (0, 1] for shrinking the contribution of each estimator. (default: 0.1) 
subsamplingRate: Fraction of the training data used for learning each decision tree, in range (0, 1]. (default: 1.0) 
GBT features importance: 
   idx_cat1 = 0.0 
   idx_cat2 = 0.0 
   idx_cat3 = 0.0 
   idx_cat4 = 3.167169394850417E-5 
   idx_cat5 = 4.745749854188828E-5 
... 
   idx_cat111 = 0.018960701085054904 
   idx_cat114 = 0.020609596772820878 
   idx_cat115 = 0.02281267960792931 
   cont1 = 0.023943087007850663 
   cont2 = 0.028078353534251005 
   ... 
   cont13 = 0.06921704925937068 
   cont14 = 0.07609111789104464 
===================================================================== 

所以我们的预测模型显示训练集和测试集的 MAE 分别为 1126.5825341266031289.9808960385383。最后一个结果对理解特征重要性至关重要(前面的列表已经简化以节省空间,但你应该收到完整的列表)。特别是,我们可以看到前三个特征完全不重要,因此我们可以安全地将它们从 DataFrame 中删除。在下一节中我们会提供更多的见解。

最后,让我们在测试集上运行预测,并为每个客户的理赔生成预测损失:

println("Run prediction over test dataset") 
cvModel.transform(Preproessing.testData) 
      .select("id", "prediction") 
      .withColumnRenamed("prediction", "loss") 
      .coalesce(1) 
      .write.format("com.databricks.spark.csv") 
      .option("header", "true") 
      .save("output/result_GBT.csv") 

上述代码应该生成一个名为 result_GBT.csv 的 CSV 文件。如果我们打开文件,我们应该能看到每个 ID 对应的损失,也就是理赔。我们将在本章末尾查看 LR、RF 和 GBT 的内容。不过,结束时调用 spark.stop() 方法停止 Spark 会话总是一个好主意。

使用随机森林回归器提升性能

在之前的章节中,尽管我们对每个实例的损失严重性做出了预测,但并没有得到预期的 MAE 值。在本节中,我们将开发一个更稳健的预测分析模型,目的是相同的,但使用随机森林回归器。不过,在正式实现之前,我们需要简要了解一下随机森林算法。

随机森林用于分类和回归

随机森林是一种集成学习技术,用于解决监督学习任务,如分类和回归。随机森林的一个优势特性是它能够克服训练数据集上的过拟合问题。随机森林中的一片“森林”通常由数百到数千棵树组成。这些树实际上是在同一训练集的不同部分上训练的。

更技术性地说,单棵树如果长得很深,往往会从高度不可预测的模式中学习。这会导致训练集上的过拟合问题。此外,较低的偏差会使分类器表现较差,即使你的数据集在特征呈现方面质量很好。另一方面,随机森林通过将多棵决策树进行平均,目的是减少方差,确保一致性,通过计算案例对之间的接近度来实现。

GBT还是随机森林?虽然 GBT 和随机森林都是树的集成方法,但它们的训练过程不同。两者之间存在一些实际的权衡,这常常会带来选择困难。然而,在大多数情况下,随机森林是更优的选择。以下是一些理由:

  • GBT 每次训练一棵树,而随机森林则可以并行训练多棵树。所以随机森林的训练时间较短。然而,在某些特殊情况下,使用较少数量的树进行 GBT 训练更简单且速度更快。

  • 在大多数情况下,随机森林不易过拟合,因此降低了过拟合的可能性。换句话说,随机森林通过增加树的数量来减少方差,而 GBT 通过增加树的数量来减少偏差。

  • 最后,随机森林相对更容易调优,因为性能会随着树的数量单调提升,但 GBT 随着树的数量增加表现较差。

然而,这会略微增加偏差,并使得结果更难以解释。但最终,最终模型的性能会显著提高。在使用随机森林作为分类器时,有一些参数设置:

  • 如果树的数量是 1,则完全不使用自助抽样;但如果树的数量大于 1,则需要使用自助抽样。支持的值有autoallsqrtlog2onethird

  • 支持的数值范围是*(0.0-1.0)[1-n]*。但是,如果选择featureSubsetStrategyauto,算法将自动选择最佳的特征子集策略。

  • 如果numTrees == 1,则featureSubsetStrategy设置为all。但是,如果numTrees > 1(即森林),则featureSubsetStrategy将设置为sqrt用于分类。

  • 此外,如果设置了一个实数值n,并且n的范围在*(0, 1.0)*之间,则将使用n*number_of_features。但是,如果设置了一个整数值n,并且n的范围在(1,特征数)之间,则仅交替使用n个特征。

  • 参数 categoricalFeaturesInfo 是一个映射,用于存储任意或分类特征。一个条目 (n -> k) 表示特征 n 是分类的,有 I 个类别,索引从 0: (0, 1,…,k-1)

  • 纯度标准用于信息增益计算。支持的值分别为分类和回归中的 ginivariance

  • maxDepth 是树的最大深度(例如,深度为 0 表示一个叶节点,深度为 1 表示一个内部节点加上两个叶节点)。

  • maxBins 表示用于拆分特征的最大桶数,建议的值是 100,以获得更好的结果。

  • 最后,随机种子用于自助抽样和选择特征子集,以避免结果的随机性。

正如前面提到的,由于随机森林足够快速且可扩展,适合处理大规模数据集,因此 Spark 是实现 RF 的合适技术,并实现这种大规模的可扩展性。然而,如果计算了邻近性,存储需求也会呈指数增长。

好的,关于 RF 就讲到这里。现在是时候动手实践了,开始吧。我们从导入所需的库开始:

import org.apache.spark.ml.regression.{RandomForestRegressor, RandomForestRegressionModel} 
import org.apache.spark.ml.{ Pipeline, PipelineModel } 
import org.apache.spark.ml.evaluation.RegressionEvaluator 
import org.apache.spark.ml.tuning.ParamGridBuilder 
import org.apache.spark.ml.tuning.CrossValidator 
import org.apache.spark.sql._ 
import org.apache.spark.sql.functions._ 
import org.apache.spark.mllib.evaluation.RegressionMetrics 

然后,我们创建一个活动的 Spark 会话并导入隐式转换:

val spark = SparkSessionCreate.createSession() 
import spark.implicits._ 

然后我们定义一些超参数,比如交叉验证的折数、最大迭代次数、回归参数的值、公差值以及弹性网络参数,如下所示:

val NumTrees = Seq(5,10,15)  
val MaxBins = Seq(23,27,30)  
val numFolds = 10  
val MaxIter: Seq[Int] = Seq(20) 
val MaxDepth: Seq[Int] = Seq(20) 

请注意,对于基于决策树的随机森林,我们要求 maxBins 至少与每个分类特征中的值的数量一样大。在我们的数据集中,我们有 110 个分类特征,其中包含 23 个不同的值。因此,我们必须将 MaxBins 设置为至少 23。然而,还是可以根据需要调整之前的参数。好了,现在是时候创建 LR 估计器了:

val model = new RandomForestRegressor().setFeaturesCol("features").setLabelCol("label")

现在,让我们通过将变换器和 LR 估计器连接起来,构建一个管道估计器:

println("Building ML pipeline") 
val pipeline = new Pipeline().setStages((Preproessing.stringIndexerStages :+ Preproessing.assembler) :+ model) 

在我们开始执行交叉验证之前,需要有一个参数网格。所以让我们通过指定树的数量、最大树深度的数字和最大桶数参数来创建参数网格,如下所示:

val paramGrid = new ParamGridBuilder() 
      .addGrid(model.numTrees, NumTrees) 
      .addGrid(model.maxDepth, MaxDepth) 
      .addGrid(model.maxBins, MaxBins) 
      .build() 

现在,为了获得更好且稳定的性能,让我们准备 K 折交叉验证和网格搜索作为模型调优的一部分。正如你可能猜到的,我将执行 10 折交叉验证。根据你的设置和数据集,随时调整折数:

println("Preparing K-fold Cross Validation and Grid Search: Model tuning") 
val cv = new CrossValidator() 
      .setEstimator(pipeline) 
      .setEvaluator(new RegressionEvaluator) 
      .setEstimatorParamMaps(paramGrid) 
      .setNumFolds(numFolds) 

太棒了,我们已经创建了交叉验证估计器。现在是训练 LR 模型的时候了:

println("Training model with Random Forest algorithm")  
val cvModel = cv.fit(Preproessing.trainingData) 

现在我们已经有了拟合的模型,这意味着它现在能够进行预测。那么让我们开始在训练集和验证集上评估模型,并计算 RMSE、MSE、MAE、R-squared 等指标:

println("Evaluating model on train and validation set and calculating RMSE") 
val trainPredictionsAndLabels = cvModel.transform(Preproessing.trainingData).select("label", "prediction").map { case Row(label: Double, prediction: Double) => (label, prediction) }.rdd 

val validPredictionsAndLabels = cvModel.transform(Preproessing.validationData).select("label", "prediction").map { case Row(label: Double, prediction: Double) => (label, prediction) }.rdd 

val trainRegressionMetrics = new RegressionMetrics(trainPredictionsAndLabels) 
val validRegressionMetrics = new RegressionMetrics(validPredictionsAndLabels) 

很棒!我们已经成功地计算了训练集和测试集的原始预测结果。接下来,让我们寻找最佳模型:

val bestModel = cvModel.bestModel.asInstanceOf[PipelineModel]

如前所述,通过使用 RF(随机森林),可以衡量特征的重要性,以便在后续阶段决定哪些特征应该保留,哪些特征应从 DataFrame 中删除。接下来,让我们按升序查找刚刚为所有特征创建的最佳模型的特征重要性,如下所示:

val featureImportances = bestModel.stages.last.asInstanceOf[RandomForestRegressionModel].featureImportances.toArray 
val FI_to_List_sorted = featureImportances.toList.sorted.toArray  

一旦我们得到了最佳拟合并经过交叉验证的模型,就可以期待较好的预测准确性。现在,让我们观察训练集和验证集的结果:

val output = "n=====================================================================n" + s"Param trainSample: ${Preproessing.trainSample}n" + 
      s"Param testSample: ${Preproessing.testSample}n" + 
      s"TrainingData count: ${Preproessing.trainingData.count}n" + 
      s"ValidationData count: ${Preproessing.validationData.count}n" + 
      s"TestData count: ${Preproessing.testData.count}n" +      "=====================================================================n" +   s"Param maxIter = ${MaxIter.mkString(",")}n" + 
      s"Param maxDepth = ${MaxDepth.mkString(",")}n" + 
      s"Param numFolds = ${numFolds}n" +      "=====================================================================n" +   s"Training data MSE = ${trainRegressionMetrics.meanSquaredError}n" + 
      s"Training data RMSE = ${trainRegressionMetrics.rootMeanSquaredError}n" + 
      s"Training data R-squared = ${trainRegressionMetrics.r2}n" + 
      s"Training data MAE = ${trainRegressionMetrics.meanAbsoluteError}n" + 
      s"Training data Explained variance = ${trainRegressionMetrics.explainedVariance}n" +      "=====================================================================n" +   s"Validation data MSE = ${validRegressionMetrics.meanSquaredError}n" + 
      s"Validation data RMSE = ${validRegressionMetrics.rootMeanSquaredError}n" + 
      s"Validation data R-squared = ${validRegressionMetrics.r2}n" + 
      s"Validation data MAE = ${validRegressionMetrics.meanAbsoluteError}n" + 
      s"Validation data Explained variance =
${validRegressionMetrics.explainedVariance}n" +      "=====================================================================n" +   s"CV params explained: ${cvModel.explainParams}n" + 
      s"RF params explained: ${bestModel.stages.last.asInstanceOf[RandomForestRegressionModel].explainParams}n" + 
      s"RF features importances:n ${Preproessing.featureCols.zip(FI_to_List_sorted).map(t => s"t${t._1} = ${t._2}").mkString("n")}n" +      "=====================================================================n" 

现在,我们按如下方式打印前面的结果:

println(results)
>>>Param trainSample: 1.0
 Param testSample: 1.0
 TrainingData count: 141194
 ValidationData count: 47124
 TestData count: 125546
 Param maxIter = 20
 Param maxDepth = 20
 Param numFolds = 10
 Training data MSE = 1340574.3409399686
 Training data RMSE = 1157.8317412042081
 Training data R-squared = 0.7642745310548124
 Training data MAE = 809.5917285994619
 Training data Explained variance = 8337897.224852404
 Validation data MSE = 4312608.024875177
 Validation data RMSE = 2076.6819749001475
 Validation data R-squared = 0.1369507149716651"
 Validation data MAE = 1273.0714382935894
 Validation data Explained variance = 8737233.110450774

因此,我们的预测模型在训练集和测试集上分别显示出 MAE(平均绝对误差)为809.59172859946191273.0714382935894。最后的结果对于理解特征重要性非常重要(前面的列表已简化以节省空间,但您应该会收到完整的列表)。

我已经在 Python 中绘制了类别特征和连续特征及其相应的重要性,因此这里不再展示代码,只展示图表。让我们看看类别特征的特征重要性,以及对应的特征编号:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/ca636833-3cff-4307-a496-220d051b6786.png

图 11:随机森林类别特征重要性

从前面的图表中可以清楚地看出,类别特征cat20cat64cat47cat69的重要性较低。因此,删除这些特征并重新训练随机森林模型,以观察更好的表现是有意义的。

现在,让我们看看连续特征与损失列的相关性及其贡献。从下图中可以看到,所有连续特征与损失列之间都有正相关关系。这也意味着,这些连续特征与我们在前面图中看到的类别特征相比并不那么重要:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/3e81f309-b2f2-4737-9047-cf674a9bf9d8.png

图 12:连续特征与标签之间的相关性

从这两个分析中我们可以得出结论:我们可以简单地删除一些不重要的列,并训练随机森林模型,观察训练集和验证集的 MAE 值是否有所减少。最后,让我们对测试集进行预测:

println("Run prediction on the test set") 
cvModel.transform(Preproessing.testData) 
      .select("id", "prediction") 
      .withColumnRenamed("prediction", "loss") 
      .coalesce(1) // to get all the predictions in a single csv file                 
      .write.format("com.databricks.spark.csv") 
      .option("header", "true") 
      .save("output/result_RF.csv") 

此外,与 LR(逻辑回归)类似,您可以通过调用stop()方法停止 Spark 会话。现在生成的result_RF.csv文件应该包含每个 ID(即索赔)的损失。

比较分析与模型部署

你已经看到,LR 模型对于小型训练数据集来说训练起来要容易得多。然而,与 GBT(梯度提升树)和随机森林模型相比,我们并没有看到更好的准确性。然而,LR 模型的简洁性是一个非常好的起点。另一方面,我们已经讨论过,随机森林在许多方面都会胜过 GBT。让我们在表格中查看结果:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/2c90a1fe-89d1-4300-8946-1e2c1e877daf.png

现在,让我们看看每个模型对于 20 起事故或损害索赔的预测情况:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/f13ba518-733c-4670-aea7-a02cf25050d7.png

图 13:i) 线性回归(LR)、ii) 梯度提升树(GBT)和 iii) 随机森林模型的损失预测

因此,根据表 2,我们可以清楚地看到,我们应该选择随机森林回归模型(Random Forest regressor)来预测保险理赔损失以及其生产情况。现在我们将简要概述如何将我们最好的模型,即随机森林回归模型投入生产。这个想法是,作为数据科学家,你可能已经训练了一个机器学习模型,并将其交给公司中的工程团队进行部署,以便在生产环境中使用。

在这里,我提供了一种简单的方法,尽管 IT 公司肯定有自己的模型部署方式。尽管如此,本文的最后会有专门的章节。通过使用模型持久化功能——即 Spark 提供的保存和加载模型的能力,这种场景完全可以变为现实。通过 Spark,你可以选择:

  • 保存和加载单一模型

  • 保存并加载整个工作流

单一模型相对简单,但效果较差,主要适用于基于 Spark MLlib 的模型持久化。由于我们更关心保存最好的模型,也就是随机森林回归模型,我们首先将使用 Scala 拟合一个随机森林回归模型,保存它,然后再使用 Scala 加载相同的模型:

// Estimator algorithm 
val model = new RandomForestRegressor() 
                    .setFeaturesCol("features") 
                    .setLabelCol("label") 
                    .setImpurity("gini") 
                    .setMaxBins(20) 
                    .setMaxDepth(20) 
                    .setNumTrees(50) 
fittedModel = rf.fit(trainingData) 

现在我们可以简单地调用write.overwrite().save()方法将该模型保存到本地存储、HDFS 或 S3,并使用加载方法将其重新加载以便将来使用:

fittedModel.write.overwrite().save("model/RF_model")  
val sameModel = CrossValidatorModel.load("model/RF_model") 

现在我们需要知道的是如何使用恢复后的模型进行预测。答案如下:

sameModel.transform(Preproessing.testData) 
    .select("id", "prediction") 
    .withColumnRenamed("prediction", "loss") 
    .coalesce(1) 
    .write.format("com.databricks.spark.csv") 
    .option("header", "true") 
    .save("output/result_RF_reuse.csv") 

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/149cecc1-17f7-4375-b90d-263bf5166ed9.png

图 14:Spark 模型在生产中的部署

到目前为止,我们只看过如何保存和加载单一的机器学习模型,但没有涉及调优或稳定的模型。这个模型可能甚至会给你很多错误的预测。因此,现在第二种方法可能更有效。

现实情况是,在实际操作中,机器学习工作流包括多个阶段,从特征提取和转换到模型拟合和调优。Spark ML 提供了工作流帮助工具,以帮助用户构建这些工作流。类似地,带有交叉验证模型的工作流也可以像我们在第一种方法中做的那样保存和恢复。

我们用训练集对交叉验证后的模型进行拟合:

val cvModel = cv.fit(Preproessing.trainingData)   

然后我们保存工作流/流水线:

cvModel.write.overwrite().save("model/RF_model") 

请注意,前面的代码行将把模型保存在你选择的位置,并具有以下目录结构:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/18125bde-d6a1-45e6-92f1-18fa933f1365.png

图 15:保存的模型目录结构

//Then we restore the same model back:
val sameCV = CrossValidatorModel.load("model/RF_model") 
Now when you try to restore the same model, Spark will automatically pick the best one. Finally, we reuse this model for making a prediction as follows:
sameCV.transform(Preproessing.testData) 
      .select("id", "prediction") 
      .withColumnRenamed("prediction", "loss") 
      .coalesce(1) 
      .write.format("com.databricks.spark.csv") 
      .option("header", "true") 
      .save("output/result_RF_reuse.csv")  

基于 Spark 的模型部署用于大规模数据集

在生产环境中,我们常常需要以规模化的方式部署预训练模型。尤其是当我们需要处理大量数据时,我们的 ML 模型必须解决这个可扩展性问题,以便持续执行并提供更快速的响应。为了克服这个问题,Spark 为我们带来的一大大数据范式就是引入了内存计算(尽管它支持磁盘操作),以及缓存抽象。

这使得 Spark 非常适合大规模数据处理,并使得计算节点能够通过访问多个计算节点上的相同输入数据,执行多个操作,无论是在计算集群还是云计算基础设施中(例如,Amazon AWS、DigitalOcean、Microsoft Azure 或 Google Cloud)。为此,Spark 支持四种集群管理器(不过最后一个仍然处于实验阶段):

  • Standalone: Spark 附带的简单集群管理器,使得设置集群变得更加容易。

  • Apache Mesos: 一个通用的集群管理器,也可以运行 Hadoop MapReduce 和服务应用程序。

  • Hadoop YARN: Hadoop 2 中的资源管理器。

  • Kubernetes(实验性): 除了上述内容外,还支持 Kubernetes 的实验性功能。Kubernetes 是一个开源平台,用于提供容器为中心的基础设施。更多信息请见 spark.apache.org/docs/latest/cluster-overview.html

你可以将输入数据集上传到 Hadoop 分布式文件系统HDFS)或 S3 存储中,以实现高效计算和低成本存储大数据。然后,Spark 的 bin 目录中的 spark-submit 脚本将用于在任意集群模式下启动应用程序。它可以通过统一的接口使用所有集群管理器,因此你不需要为每个集群专门配置应用程序。

然而,如果你的代码依赖于其他项目,那么你需要将它们与应用程序一起打包,以便将代码分发到 Spark 集群中。为此,创建一个包含你的代码及其依赖项的 assembly jar 文件(也称为 fatuber jar)。然后将代码分发到数据所在的地方,并执行 Spark 作业。SBTMaven 都有 assembly 插件,可以帮助你准备这些 jar 文件。

在创建 assembly jar 文件时,也需要将 Spark 和 Hadoop 列为依赖项。这些依赖项不需要打包,因为它们会在运行时由集群管理器提供。创建了合并的 jar 文件后,可以通过以下方式传递 jar 来调用脚本:

  ./bin/spark-submit \
      --class <main-class> \
      --master <master-url> \
      --deploy-mode <deploy-mode> \
      --conf <key>=<value> \
       ... # other options
       <application-jar> \
       [application-arguments]

在上面的命令中,列出了以下一些常用的选项:

  • --class: 应用程序的入口点(例如,org.apache.spark.examples.SparkPi)。

  • --master: 集群的主 URL(例如,spark://23.195.26.187:7077)。

  • --deploy-mode: 是否将驱动程序部署在工作节点(集群)上,还是作为外部客户端在本地部署。

  • --conf: 任意的 Spark 配置属性,采用 key=value 格式。

  • application-jar:包含你的应用程序和所有依赖项的捆绑 jar 文件的路径。URL 必须在你的集群中全局可见,例如,hdfs:// 路径或在所有节点上都存在的 file:// 路径。

  • application-arguments:传递给主类主方法的参数(如果有的话)。

例如,你可以在客户端部署模式下,在 Spark 独立集群上运行 AllstateClaimsSeverityRandomForestRegressor 脚本,如下所示:

./bin/spark-submit \
   --class com.packt.ScalaML.InsuranceSeverityClaim.AllstateClaimsSeverityRandomForestRegressor\
   --master spark://207.184.161.138:7077 \
   --executor-memory 20G \
   --total-executor-cores 100 \
   /path/to/examples.jar

如需更多信息,请参见 Spark 网站:spark.apache.org/docs/latest/submitting-applications.html。不过,你也可以从在线博客或书籍中找到有用的信息。顺便提一下,我在我最近出版的一本书中详细讨论了这个话题:Md. Rezaul Karim, Sridhar Alla, Scala 和 Spark 在大数据分析中的应用,Packt Publishing Ltd. 2017。更多信息请见:www.packtpub.com/big-data-and-business-intelligence/scala-and-spark-big-data-analytics

无论如何,我们将在接下来的章节中学习更多关于如何在生产环境中部署 ML 模型的内容。因此,这一章就写到这里。

总结

在本章中,我们已经学习了如何使用一些最广泛使用的回归算法开发用于分析保险严重性索赔的预测模型。我们从简单的线性回归(LR)开始。然后我们看到如何通过使用 GBT 回归器来提升性能。接着,我们体验了使用集成技术(如随机森林回归器)来提高性能。最后,我们进行了这些模型之间的性能对比分析,并选择了最佳模型来部署到生产环境中。

在下一章中,我们将介绍一个新的端到端项目,名为 电信客户流失分析与预测。流失预测对于企业至关重要,因为它可以帮助你发现那些可能取消订阅、产品或服务的客户。它还可以最大限度地减少客户流失。通过预测哪些客户更有可能取消服务订阅,达到了这一目的。

第二章:分析与预测电信行业流失

在这一章中,我们将开发一个机器学习ML)项目,用于分析和预测客户是否可能取消其电信合同的订阅。此外,我们还将对数据进行一些初步分析,仔细查看哪些客户特征通常与这种流失相关。

广泛使用的分类算法,如决策树、随机森林、逻辑回归和支持向量机SVM),将用于分析和做出预测。最终,读者将能够选择最适合生产环境的最佳模型。

简而言之,在这个端到端的项目中,我们将学习以下主题:

  • 为什么以及如何进行流失预测?

  • 基于逻辑回归的流失预测

  • 基于 SVM 的流失预测

  • 基于决策树的流失预测

  • 基于随机森林的流失预测

  • 选择最佳模型进行部署

为什么我们要进行流失分析,如何进行流失分析?

客户流失是指客户或顾客的流失(也称为客户流失率、客户流动率或客户弃用)。这一概念最初用于电信行业,当时许多用户转向其他服务提供商。然而,这已成为其他业务领域的重要问题,如银行、互联网服务提供商、保险公司等。嗯,流失的主要原因之一是客户不满,以及竞争对手提供更便宜或更好的优惠。

如你在图 1中所见,商业行业中与客户可能签订的合同有四种类型:契约性合同、非契约性合同、自愿性合同和非自愿性合同。客户流失的全部成本包括失去的收入以及与用新客户替代这些流失客户所涉及的(电)营销成本。然而,这种类型的损失可能会给企业带来巨大的损失。想想十年前,当诺基亚是手机市场的霸主时,突然,苹果发布了 iPhone 3G,这标志着智能手机时代的革命。接着,大约 10%到 12%的客户停止使用诺基亚,转而选择了 iPhone。虽然后来诺基亚也尝试推出智能手机,但最终,它们无法与苹果竞争:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/cc26a83d-61b4-4cf0-b673-1e91438ec329.png

图 1:与客户可能签订的四种合同类型

流失预测对企业至关重要,因为它能帮助企业检测出可能取消订阅、产品或服务的客户。它还可以最大限度地减少客户流失。通过预测哪些客户可能取消订阅服务,企业可以为这些客户(可能取消订阅的客户)提供特别优惠或计划。这样,企业就可以减少流失率。这应该是每个在线业务的关键目标。

在员工流失预测方面,典型的任务是确定哪些因素预测员工离职。这类预测过程依赖于大量数据,通常需要利用先进的机器学习技术。然而,在本章中,我们将主要关注客户流失的预测和分析。为此,应该分析多个因素,以便理解客户行为,包括但不限于:

  • 客户的基本信息数据,如年龄、婚姻状况等

  • 客户的社交媒体情感分析

  • 从点击流日志中获取的浏览行为

  • 显示行为模式的历史数据,提示可能的客户流失

  • 客户的使用模式和地理位置使用趋势

  • 通话圈数据和支持呼叫中心统计信息

开发一个流失分析管道

在机器学习中,我们将算法的表现分为两个阶段:学习和推理。学习阶段的最终目标是准备和描述可用数据,也称为特征向量,它用于训练模型。

学习阶段是最重要的阶段之一,但也是极其耗时的。它包括从经过转换的训练数据中准备特征向量(也称为特征向量,表示每个特征值的数字向量),以便我们可以将其输入到学习算法中。另一方面,训练数据有时也包含一些不纯净的信息,需要一些预处理,例如清理。

一旦我们拥有特征向量,接下来的步骤是准备(或编写/重用)学习算法。下一个重要步骤是训练算法,以准备预测模型。通常,(当然根据数据大小),运行一个算法可能需要几个小时(甚至几天),以使特征收敛为有用的模型,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/f458d39c-885d-4161-b766-251ea56c1efb.png

图 2:学习和训练预测模型 - 展示了如何从训练数据中生成特征向量,进而训练学习算法,最终产生预测模型

第二个最重要的阶段是推理,它用于智能地利用模型,例如对从未见过的数据进行预测、提供推荐、推断未来规则等。通常,与学习阶段相比,推理所需的时间较短,有时甚至是实时的。因此,推理的核心是通过新的(即未观察过的)数据测试模型,并评估模型本身的表现,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/7e917a27-ffcf-43d6-8794-a16d48bd0fed.png

图 3:从现有模型进行推理以进行预测分析(特征向量由未知数据生成,用于做出预测)

然而,在整个过程中,为了使预测模型成功,数据在所有机器学习任务中都是至关重要的。考虑到这一点,下面的图表展示了电信公司可以使用的分析管道:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/9abacbdc-9ed4-4ab4-938f-54ee24efcc24.png

图 4:流失分析管道

通过这种分析,电信公司可以辨别如何预测并改善客户体验,从而防止客户流失并量身定制营销活动。在实际操作中,这类商业评估通常用于留住最可能流失的客户,而非那些可能留下的客户。

因此,我们需要开发一个预测模型,确保我们的模型对 Churn = True 样本具有敏感性——这是一个二分类问题。我们将在接下来的章节中详细探讨。

数据集描述

Orange Telecom 的客户流失数据集,包含了清理后的客户活动数据(特征),以及一个流失标签,指示客户是否取消了订阅。我们将使用该数据集来开发我们的预测模型。可以通过以下链接分别下载 churn-80 和 churn-20 数据集:

然而,由于更多的数据对于开发机器学习模型通常是有利的,因此我们将使用较大的数据集(即 churn-80)进行训练和交叉验证,使用较小的数据集(即 churn-20)进行最终测试和模型性能评估。

请注意,后者数据集仅用于评估模型(即用于演示目的)。在生产环境中,电信公司可以使用自己的数据集,经过必要的预处理和特征工程。该数据集的结构如下:

  • : String

  • 账户时长: Integer

  • 区号: Integer

  • 国际计划: String

  • 语音邮件计划: String

  • 电子邮件消息数量: Integer

  • 总白天分钟数: Double

  • 总白天电话数量: Integer

  • 总白天费用: Double

  • 总傍晚分钟数: Double

  • 总傍晚电话数量: Integer

  • 总傍晚费用: Double

  • 总夜间通话分钟数: Double

  • 总夜间电话数量: Integer

  • 总夜间费用: Double

  • 总国际分钟数: Double

  • 总国际电话数量: Integer

  • 总国际费用: Double

  • 客户服务电话: Integer

探索性分析与特征工程

在这一小节中,我们将在开始预处理和特征工程之前,对数据集进行一些探索性数据分析(EDA)。只有在此之后,创建分析管道才有意义。首先,让我们导入必要的软件包和库,代码如下:

import org.apache.spark._
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.sql.Dataset

然后,让我们指定数据集的来源和模式。当将数据加载到 DataFrame 时,我们可以指定模式。这一指定相比于 Spark 2.x 之前的模式推断提供了优化的性能。

首先,我们创建一个包含所有字段的 Scala 案例类。变量名一目了然:

case class CustomerAccount(state_code: String, 
    account_length: Integer, 
    area_code: String, 
    international_plan: String, 
    voice_mail_plan: String, 
    num_voice_mail: Double, 
    total_day_mins: Double, 
    total_day_calls: Double, 
    total_day_charge: Double,
    total_evening_mins: Double, 
    total_evening_calls: Double, 
    total_evening_charge: Double,
    total_night_mins: Double, 
    total_night_calls: Double, 
    total_night_charge: Double,
    total_international_mins: Double, 
    total_international_calls: Double, 
    total_international_charge: Double,
    total_international_num_calls: Double, 
    churn: String)

现在,让我们创建一个自定义模式,结构与我们已创建的数据源相似,如下所示:

val schema = StructType(Array(
    StructField("state_code", StringType, true),
    StructField("account_length", IntegerType, true),
    StructField("area_code", StringType, true),
    StructField("international_plan", StringType, true),
    StructField("voice_mail_plan", StringType, true),
    StructField("num_voice_mail", DoubleType, true),
    StructField("total_day_mins", DoubleType, true),
    StructField("total_day_calls", DoubleType, true),
    StructField("total_day_charge", DoubleType, true),
    StructField("total_evening_mins", DoubleType, true),
    StructField("total_evening_calls", DoubleType, true),
    StructField("total_evening_charge", DoubleType, true),
    StructField("total_night_mins", DoubleType, true),
    StructField("total_night_calls", DoubleType, true),
    StructField("total_night_charge", DoubleType, true),
    StructField("total_international_mins", DoubleType, true),
    StructField("total_international_calls", DoubleType, true),
    StructField("total_international_charge", DoubleType, true),
    StructField("total_international_num_calls", DoubleType, true),
    StructField("churn", StringType, true)
))

让我们创建一个 Spark 会话并导入implicit._,以便我们指定 DataFrame 操作,如下所示:

val spark: SparkSession = SparkSessionCreate.createSession("preprocessing")
import spark.implicits._

现在,让我们创建训练集。我们使用 Spark 推荐的格式com.databricks.spark.csv读取 CSV 文件。我们不需要显式的模式推断,因此将推断模式设置为 false,而是需要我们之前创建的自定义模式。接着,我们从所需位置加载数据文件,最后指定数据源,确保我们的 DataFrame 与我们指定的结构完全一致:

val trainSet: Dataset[CustomerAccount] = spark.read.
        option("inferSchema", "false")
        .format("com.databricks.spark.csv")
        .schema(schema)
        .load("data/churn-bigml-80.csv")
        .as[CustomerAccount]

现在,让我们看看模式是什么样的:

trainSet.printSchema()
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/489ed265-308b-4a22-82c2-127c473bd023.png

太棒了!它看起来与数据结构完全相同。现在让我们使用show()方法查看一些示例数据,如下所示:

trainSet.show()
>>>

在下图中,列名已被缩短,以便在图中显示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/601a7523-4f1b-45d2-9481-d0f868bca26d.png

我们还可以使用 Spark 的describe()方法查看训练集的相关统计数据:

describe()方法是 Spark DataFrame 的内置方法,用于统计处理。它对所有数值列应用汇总统计计算,最后将计算值作为单个 DataFrame 返回。

val statsDF = trainSet.describe()
statsDF.show()
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/fe307ce5-7904-41fd-b30b-63a24fee94e5.png

如果这个数据集可以装入内存,我们可以使用 Spark 的cache()方法将其缓存,以便快速和重复地访问:

trainSet.cache()

让我们查看一些有用的属性,比如与流失(churn)的变量相关性。例如,看看流失与国际通话总数之间的关系:

trainSet.groupBy("churn").sum("total_international_num_calls").show()
>>>
+-----+----------------------------------+
churn|sum(total_international_num_calls)|
+-----+----------------------------------+
|False| 3310.0|
| True| 856.0|
+-----+----------------------------------+

让我们看看流失与国际通话费用总额之间的关系:

trainSet.groupBy("churn").sum("total_international_charge").show()
 >>>
+-----+-------------------------------+
|churn|sum(total_international_charge)|
+-----+-------------------------------+
|False| 6236.499999999996|
| True| 1133.63|
+-----+-------------------------------+

既然我们还需要准备测试集来评估模型,让我们准备与训练集相似的测试集,如下所示:

val testSet: Dataset[CustomerAccount] = 
    spark.read.
    option("inferSchema", "false")
    .format("com.databricks.spark.csv")
    .schema(schema)
    .load("data/churn-bigml-20.csv")
    .as[CustomerAccount]

现在,让我们将它们缓存,以便更快地进行进一步操作:

testSet.cache()

现在,让我们查看一些训练集的相关属性,以了解它是否适合我们的目的。首先,我们为当前会话创建一个临时视图以用于持久化。我们可以创建一个目录作为接口,用于创建、删除、修改或查询底层数据库、表、函数等:

trainSet.createOrReplaceTempView("UserAccount")
spark.catalog.cacheTable("UserAccount")

按照流失标签对数据进行分组并计算每个组中的实例数量,显示出假流失样本大约是实际流失样本的六倍。让我们使用以下代码验证这一说法:

trainSet.groupBy("churn").count.show()
>>>
+-----+-----+
|churn|count|
+-----+-----+
|False| 2278|
| True| 388 |
+-----+-----+

我们还可以看到前面的语句,使用 Apache Zeppelin 验证过的(有关如何配置和入门的更多细节,请参见第八章,在银行营销中使用深度信念网络),如下所示:

spark.sqlContext.sql("SELECT churn,SUM(international_num_calls) as Total_intl_call FROM UserAccount GROUP BY churn").show()
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/b933288d-c96b-4772-888f-b1930c1c0593.png

如我们所述,在大多数情况下,目标是保留那些最有可能流失的客户,而不是那些可能会留下或已经留下的客户。这也意味着我们应该准备我们的训练集,确保我们的 ML 模型能够敏感地识别真正的流失样本——即,标记为流失(True)的样本。

我们还可以观察到,前面的训练集高度不平衡。因此,使用分层抽样将两种样本类型放在同等基础上是可行的。当提供每种样本类型的返回比例时,可以使用 sampleBy() 方法来实现。

在这里,我们保留了所有 True 流失类的实例,但将 False 流失类下采样至 388/2278,约为 0.1675

val fractions = Map("False" -> 0.1675, "True" -> 1.0)

这样,我们也仅映射了 True 流失样本。现在,让我们为仅包含下采样样本的训练集创建一个新的 DataFrame:

val churnDF = trainSet.stat.sampleBy("churn", fractions, 12345L)

第三个参数是用于可重复性目的的种子值。现在让我们来看一下:

churnDF.groupBy("churn").count.show()
>>>
+-----+-----+
|churn|count|
+-----+-----+
|False| 390|
| True| 388|
+-----+-----+

现在让我们看看变量之间的关系。让我们查看白天、夜晚、傍晚和国际语音通话如何影响 churn 类别。只需执行以下代码:

spark.sqlContext.sql("SELECT churn, SUM(total_day_charge) as TDC, SUM(total_evening_charge) as TEC,    
                      SUM(total_night_charge) as TNC, SUM(total_international_charge) as TIC,  
                      SUM(total_day_charge) + SUM(total_evening_charge) + SUM(total_night_charge) + 
                      SUM(total_international_charge) as Total_charge FROM UserAccount GROUP BY churn 
                      ORDER BY Total_charge DESC")
.show()
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/5a0af826-28e0-463e-b042-50b3498f37b0.png

在 Apache Zeppelin 上,可以看到如下的前置结果:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/a764a0a5-e3c5-4867-b495-2d5952c8407d.png

现在,让我们看看白天、夜晚、傍晚和国际语音通话分别对 churn 类别的前置总费用贡献了多少。只需执行以下代码:

spark.sqlContext.sql("SELECT churn, SUM(total_day_mins) 
                      + SUM(total_evening_mins) + SUM(total_night_mins) 
                      + SUM(total_international_mins) as Total_minutes 
                    FROM UserAccount GROUP BY churn").show()
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/548bee1c-eb4e-40dc-b02c-e58b1729a683.png

在 Apache Zeppelin 上,可以看到如下的前置结果:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/aa00e9d2-c314-4459-8e51-135748fb243d.png

从前面的两张图表和表格可以清楚地看出,总白天通话分钟数和总白天费用是这个训练集中的高度相关特征,这对我们的 ML 模型训练并不有利。因此,最好将它们完全去除。此外,以下图表展示了所有可能的相关性(虽然是用 PySpark 绘制的):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/7aea33d2-92d3-4c01-8b74-aba1b5cb883e.jpg

图 5:包含所有特征的相关矩阵

让我们丢弃每对相关字段中的一列,同时也丢弃StateArea code列,因为这些列也不会使用:

val trainDF = churnDF
    .drop("state_code")
    .drop("area_code")
    .drop("voice_mail_plan")
    .drop("total_day_charge")
    .drop("total_evening_charge")

很好。最后,我们得到了可以用于更好的预测建模的训练 DataFrame。让我们看一下结果 DataFrame 的一些列:

trainDF.select("account_length", "international_plan", "num_voice_mail",         
               "total_day_calls","total_international_num_calls", "churn")
.show(10)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/a90814fb-81d6-440c-bc2a-226ee24a1bd0.png

然而,我们还没有完成;当前的 DataFrame 不能作为估算器输入给模型。如我们所描述的,Spark ML API 要求我们的数据必须转换为 Spark DataFrame 格式,包含标签(Double 类型)和特征(Vector 类型)。

现在,我们需要创建一个管道来传递数据,并将多个变换器和估算器连接起来。这个管道随后作为特征提取器工作。更具体地说,我们已经准备好了两个StringIndexer变换器和一个VectorAssembler

StringIndexer将一个分类标签列编码为标签索引列(即数字)。如果输入列是数字类型,我们必须将其转换为字符串并对字符串值进行索引。其他 Spark 管道组件,如估算器或变换器,都会利用这个字符串索引标签。为了做到这一点,组件的输入列必须设置为这个字符串索引列的名称。在许多情况下,你可以使用setInputCol来设置输入列。有兴趣的读者可以参考这个spark.apache.org/docs/latest/ml-features.html以获取更多详情。

第一个StringIndexer将分类特征international_plan和标签转换为数字索引。第二个StringIndexer将分类标签(即churn)转换为数字。通过这种方式,索引化的分类特征使得决策树和随机森林等分类器能够适当处理分类特征,从而提高性能。

现在,添加以下代码行,对标签列进行索引标签和元数据处理。在整个数据集上进行拟合,以确保所有标签都包括在索引中:

val ipindexer = new StringIndexer()
    .setInputCol("international_plan")
    .setOutputCol("iplanIndex")

val labelindexer = new StringIndexer()
    .setInputCol("churn")
    .setOutputCol("label")

现在我们需要提取对分类最有贡献的最重要特征。由于我们已经删除了一些列,结果列集包含以下字段:

* Label → churn: True or False
* Features → {("account_length", "iplanIndex", "num_voice_mail", "total_day_mins", "total_day_calls", "total_evening_mins", "total_evening_calls", "total_night_mins", "total_night_calls", "total_international_mins", "total_international_calls", "total_international_num_calls"}

由于我们已经使用StringIndexer将分类标签转换为数字,接下来的任务是提取特征:

val featureCols = Array("account_length", "iplanIndex", 
                        "num_voice_mail", "total_day_mins", 
                        "total_day_calls", "total_evening_mins", 
                        "total_evening_calls", "total_night_mins", 
                        "total_night_calls", "total_international_mins", 
                        "total_international_calls", "total_international_num_calls")

现在,让我们将特征转换为特征向量,特征向量是表示每个特征值的数字向量。在我们的例子中,我们将使用VectorAssembler。它将所有的featureCols合并/转换成一个名为features的单列:

val assembler = new VectorAssembler()
    .setInputCols(featureCols)
    .setOutputCol("features")

现在我们已经准备好了包含标签和特征向量的真实训练集,接下来的任务是创建一个估算器——管道的第三个元素。我们从一个非常简单但强大的逻辑回归分类器开始。

用于流失预测的 LR

LR 是预测二元响应最常用的分类器之一。它是一种线性机器学习方法,正如在第一章中所描述的,分析保险严重性理赔loss函数是由逻辑损失给出的公式:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/58d1cdb4-4e46-48ce-8051-bf44e2fbf31c.png

对于 LR 模型,loss 函数是逻辑损失函数。对于二分类问题,该算法输出一个二元 LR 模型,对于给定的新数据点,记为 x,该模型通过应用逻辑函数进行预测:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/a34028da-b03e-4cb1-a247-c08dd7e7d7ce.png

在上述方程中,z = W^TX,如果 f(W^TX)>0.5,则结果为正;否则为负。

请注意,LR 模型的原始输出 f(z) 具有概率解释。

请注意,与线性回归相比,逻辑回归为你提供了更高的分类精度。此外,它是一种灵活的方式来对模型进行正则化,以进行自定义调整,总体而言,模型的响应是概率的度量。

最重要的是,尽管线性回归只能预测连续值,线性回归仍然足够通用,可以预测离散值:

import org.apache.spark._
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.ml.classification.{BinaryLogisticRegressionSummary, LogisticRegression, LogisticRegressionModel}
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics 
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator

既然我们已经知道线性回归的工作原理,让我们开始使用基于 Spark 的线性回归实现。首先,让我们导入所需的包和库。

现在,让我们创建一个 Spark 会话并导入隐式转换:

val spark: SparkSession = SparkSessionCreate.createSession("ChurnPredictionLogisticRegression")
import spark.implicits._

我们现在需要定义一些超参数来训练基于线性回归的管道:

val numFolds = 10
val MaxIter: Seq[Int] = Seq(100)
val RegParam: Seq[Double] = Seq(1.0) // L2 regularization param, set 1.0 with L1 regularization
val Tol: Seq[Double] = Seq(1e-8)// for convergence tolerance for iterative algorithms
val ElasticNetParam: Seq[Double] = Seq(0.0001) //Combination of L1 & L2

RegParam 是一个标量,用于调整约束的强度:较小的值表示软边界,因此,自然地,较大的值表示硬边界,而无限大则是最硬的边界。

默认情况下,LR 执行 L2 正则化,正则化参数设置为 1.0。相同的模型执行 L1 正则化变种的 LR,正则化参数(即 RegParam)设置为 0.10。弹性网络是 L1 和 L2 正则化的组合。

另一方面,Tol 参数用于迭代算法(如逻辑回归或线性支持向量机)的收敛容忍度。现在,一旦我们定义并初始化了超参数,接下来的任务是实例化一个线性回归估算器,如下所示:

val lr = new LogisticRegression()
    .setLabelCol("label")
    .setFeaturesCol("features")

现在,我们已经有了三个变换器和一个估算器,接下来的任务是将它们串联成一个单一的管道——即,它们每一个都作为一个阶段:

val pipeline = new Pipeline()
    .setStages(Array(PipelineConstruction.ipindexer,
    PipelineConstruction.labelindexer,
    PipelineConstruction.assembler, lr))

为了在超参数空间上执行这样的网格搜索,我们需要先定义它。在这里,Scala 的函数式编程特性非常方便,因为我们只需将函数指针和要评估的相应参数添加到参数网格中,在该网格中你设置要测试的参数,并使用交叉验证评估器来构建一个模型选择工作流。这将搜索线性回归的最大迭代次数、正则化参数、容忍度和弹性网络,以找到最佳模型:

val paramGrid = new ParamGridBuilder()
    .addGrid(lr.maxIter, MaxIter)
    .addGrid(lr.regParam, RegParam)
    .addGrid(lr.tol, Tol)
    .addGrid(lr.elasticNetParam, ElasticNetParam)
    .build()

请注意,超参数形成了一个 n 维空间,其中n是超参数的数量。这个空间中的每个点是一个特定的超参数配置,即超参数向量。当然,我们无法探索该空间中的每个点,因此我们基本上做的是在该空间中对(希望均匀分布的)子集进行网格搜索。

然后我们需要定义一个BinaryClassificationEvaluator评估器,因为这是一个二分类问题。使用该评估器,模型将通过比较测试标签列与测试预测列,根据精度指标进行评估。默认的度量标准是精度-召回曲线下面积和接收者操作特征ROC)曲线下面积:

val evaluator = new BinaryClassificationEvaluator()
    .setLabelCol("label")
    .setRawPredictionCol("prediction")

我们使用CrossValidator进行最佳模型选择。CrossValidator使用估算器管道、参数网格和分类评估器。CrossValidator使用ParamGridBuilder来遍历线性回归的最大迭代次数、回归参数、容差和弹性网参数,然后评估模型,对于每个参数值重复 10 次以获得可靠结果——即进行 10 折交叉验证:

val crossval = new CrossValidator()
    .setEstimator(pipeline)
    .setEvaluator(evaluator)
    .setEstimatorParamMaps(paramGrid)
    .setNumFolds(numFolds)

前面的代码旨在执行交叉验证。验证器本身使用BinaryClassificationEvaluator评估器来评估每一折中的训练,确保没有过拟合发生。

尽管后台有很多复杂操作,CrossValidator对象的接口依然简洁且熟悉,因为CrossValidator也继承自估算器,并支持 fit 方法。这意味着,在调用 fit 后,完整的预定义管道,包括所有特征预处理和 LR 分类器,将被多次执行——每次使用不同的超参数向量:

val cvModel = crossval.fit(Preprocessing.trainDF)

现在是时候使用测试数据集评估我们创建的 LR 模型的预测能力了,该测试数据集此前未用于任何训练或交叉验证——也就是说,对模型来说是未见过的数据。第一步,我们需要将测试集转换为模型管道,这将根据我们在前述特征工程步骤中描述的相同机制映射特征:

val predictions = cvModel.transform(Preprocessing.testSet)
al result = predictions.select("label", "prediction", "probability")
val resutDF = result.withColumnRenamed("prediction", "Predicted_label")
resutDF.show(10)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/b2101733-4336-453c-9d7e-2bf2fb7c7f1a.png

预测概率在根据客户的缺陷可能性进行排名时也非常有用。通过这种方式,电信业务可以利用有限的资源进行保留,并集中于最有价值的客户。

然而,看到之前的预测数据框,实际上很难猜测分类准确率。在第二步中,评估器通过BinaryClassificationEvaluator自行进行评估,如下所示:

val accuracy = evaluator.evaluate(predictions)
println("Classification accuracy: " + accuracy)
>>>
Classification accuracy: 0.7670592565329408

所以,我们的二分类模型的分类准确率大约为 77%。现在,使用准确率来评估二分类器并没有太大意义。

因此,研究人员经常推荐其他性能指标,如精确度-召回率曲线下的面积和 ROC 曲线下的面积。然而,为此我们需要构建一个包含测试集原始得分的 RDD:

val predictionAndLabels = predictions
    .select("prediction", "label")
    .rdd.map(x => (x(0).asInstanceOf[Double], x(1)
    .asInstanceOf[Double]))

现在,可以使用前述 RDD 来计算之前提到的两个性能指标:

val metrics = new BinaryClassificationMetrics(predictionAndLabels)
println("Area under the precision-recall curve: " + metrics.areaUnderPR)
println("Area under the receiver operating characteristic (ROC) curve : " + metrics.areaUnderROC)
>>>
Area under the precision-recall curve: 0.5761887477313975
Area under the receiver operating characteristic (ROC) curve: 0.7670592565329408

在这种情况下,评估结果为 77% 的准确率,但只有 58% 的精确度。接下来,我们计算一些其他的性能指标;例如,假阳性、真阳性和假阴性预测对评估模型的性能也非常有用:

  • 真阳性:模型正确预测订阅取消的频率

  • 假阳性:模型错误预测订阅取消的频率

  • 真阴性:模型正确预测没有取消的频率

  • 假阴性:模型错误预测没有取消的频率

val lp = predictions.select("label", "prediction")
val counttotal = predictions.count()
val correct = lp.filter($"label" === $"prediction").count()

val wrong = lp.filter(not($"label" === $"prediction")).count()
val ratioWrong = wrong.toDouble / counttotal.toDouble
val ratioCorrect = correct.toDouble / counttotal.toDouble

val truep = lp.filter($"prediction" === 0.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val truen = lp.filter($"prediction" === 1.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val falsep = lp.filter($"prediction" === 1.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

val falsen = lp.filter($"prediction" === 0.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

println("Total Count : " + counttotal)
println("Correct : " + correct)
println("Wrong: " + wrong)
println("Ratio wrong: " + ratioWrong)
println("Ratio correct: " + ratioCorrect)
println("Ratio true positive : " + truep)
println("Ratio false positive : " + falsep)
println("Ratio true negative : " + truen)
println("Ratio false negative : " + falsen)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/16371f9d-cf39-4676-98fc-4407d4fba749.png

然而,我们还没有得到良好的准确性,因此让我们继续尝试其他分类器,例如 SMV。这次,我们将使用来自 Apache Spark ML 包的线性 SVM 实现。

SVM 用于流失预测

SVM 也广泛用于大规模分类任务(即二分类以及多项分类)。此外,它也是一种线性机器学习方法,如第一章《分析保险赔偿严重性》中所描述。线性 SVM 算法输出一个 SVM 模型,其中 SVM 使用的损失函数可以通过铰链损失来定义,如下所示:

L(w;x,y):=max{0,1−yw^Tx}

Spark 中的线性 SVM 默认使用 L2 正则化进行训练。然而,它也支持 L1 正则化,通过这种方式,问题本身变成了一个线性规划问题。

现在,假设我们有一组新的数据点 x;模型根据 w^Tx** 的值做出预测。默认情况下,如果 w*^T***x**≥0,则结果为正,否则为负。

现在我们已经了解了 SVM 的工作原理,让我们开始使用基于 Spark 的 SVM 实现。我们从导入所需的包和库开始:

import org.apache.spark._
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.ml.classification.{LinearSVC, LinearSVCModel}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions.max
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator

现在,让我们创建一个 Spark 会话并导入隐式转换:

val spark: SparkSession = SparkSessionCreate.createSession("ChurnPredictionLogisticRegression")
import spark.implicits._

我们现在需要定义一些超参数来训练基于 LR 的管道:

val numFolds = 10
val MaxIter: Seq[Int] = Seq(100)
val RegParam: Seq[Double] = Seq(1.0) // L2 regularization param, set 0.10 with L1 reguarization
val Tol: Seq[Double] = Seq(1e-8)
val ElasticNetParam: Seq[Double] = Seq(1.0) // Combination of L1 and L2

现在,一旦我们定义并初始化了超参数,下一步是实例化一个 LR 估计器,如下所示:

val svm = new LinearSVC()

现在我们已经准备好三个转换器和一个估计器,下一步是将它们串联成一个管道——也就是说,每个都充当一个阶段:

val pipeline = new Pipeline()
     .setStages(Array(PipelineConstruction.ipindexer,
                      PipelineConstruction.labelindexer,
                      PipelineConstruction.assembler,svm)
                      )

让我们定义 paramGrid,以便在超参数空间上执行网格搜索。这个搜索将遍历 SVM 的最大迭代次数、正则化参数、容差和弹性网,以寻找最佳模型:

val paramGrid = new ParamGridBuilder()
    .addGrid(svm.maxIter, MaxIter)
    .addGrid(svm.regParam, RegParam)
    .addGrid(svm.tol, Tol)
    .addGrid(svm.elasticNetParam, ElasticNetParam)
    .build()

让我们定义一个 BinaryClassificationEvaluator 评估器来评估模型:

val evaluator = new BinaryClassificationEvaluator()
    .setLabelCol("label")
    .setRawPredictionCol("prediction")

我们使用 CrossValidator 执行 10 次交叉验证,以选择最佳模型:

val crossval = new CrossValidator()
    .setEstimator(pipeline)
    .setEvaluator(evaluator)
    .setEstimatorParamMaps(paramGrid)
    .setNumFolds(numFolds)

现在我们调用fit方法,以便完整的预定义流水线,包括所有特征预处理和 LR 分类器,将被执行多次——每次使用不同的超参数向量:

val cvModel = crossval.fit(Preprocessing.trainDF)

现在是时候评估 SVM 模型在测试数据集上的预测能力了。第一步,我们需要使用模型流水线转换测试集,这将根据我们在前面特征工程步骤中描述的机制来映射特征:

val predictions = cvModel.transform(Preprocessing.testSet)
prediction.show(10)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/ad85dba1-6541-44bf-bf6c-560e0d867804.png

然而,从之前的预测数据框中,确实很难猜测分类准确率。在第二步中,评估器使用BinaryClassificationEvaluator进行自我评估,如下所示:

val accuracy = evaluator.evaluate(predictions)
println("Classification accuracy: " + accuracy)
>>>
Classification accuracy: 0.7530180345969819

所以我们从我们的二分类模型中得到了大约 75%的分类准确率。现在,单单使用二分类器的准确率并没有太大意义。

因此,研究人员通常推荐其他性能指标,比如精确度-召回曲线下面积和 ROC 曲线下面积。然而,为此我们需要构建一个包含测试集原始分数的 RDD:

val predictionAndLabels = predictions
    .select("prediction", "label")
    .rdd.map(x => (x(0).asInstanceOf[Double], x(1)
    .asInstanceOf[Double]))

现在,可以使用前面的 RDD 来计算两个之前提到的性能指标:

val metrics = new BinaryClassificationMetrics(predictionAndLabels)
println("Area under the precision-recall curve: " + metrics.areaUnderPR)
println("Area under the receiver operating characteristic (ROC) curve : " + metrics.areaUnderROC)
>>>
Area under the precision-recall curve: 0.5595712265324828
Area under the receiver operating characteristic (ROC) curve: 0.7530180345969819

在这种情况下,评估返回了 75%的准确率,但仅有 55%的精确度。接下来,我们再次计算一些其他指标;例如,假阳性、真阳性、假阴性和真阴性预测也有助于评估模型的性能:

val lp = predictions.select("label", "prediction")
val counttotal = predictions.count()

val correct = lp.filter($"label" === $"prediction").count()

val wrong = lp.filter(not($"label" === $"prediction")).count()
val ratioWrong = wrong.toDouble / counttotal.toDouble

val ratioCorrect = correct.toDouble / counttotal.toDouble

val truep = lp.filter($"prediction" === 0.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val truen = lp.filter($"prediction" === 1.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val falsep = lp.filter($"prediction" === 1.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

val falsen = lp.filter($"prediction" === 0.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

println("Total Count : " + counttotal)
println("Correct : " + correct)
println("Wrong: " + wrong)
println("Ratio wrong: " + ratioWrong)
println("Ratio correct: " + ratioCorrect)
println("Ratio true positive : " + truep)
println("Ratio false positive : " + falsep)
println("Ratio true negative : " + truen)
println("Ratio false negative : " + falsen)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/6d30c141-8fcc-4bda-8273-b5fc3c4f9a3d.png

然而,我们使用 SVM 时并没有获得好的准确率。而且,无法选择最合适的特征,这会帮助我们用最合适的特征训练模型。这一次,我们将再次使用一个更强大的分类器,比如 Apache Spark ML 包中的决策树DTs)实现。

用于流失预测的决策树

决策树通常被认为是一种监督学习技术,用于解决分类和回归任务。

更技术性地讲,决策树中的每个分支代表一个可能的决策、事件或反应,基于统计概率。与朴素贝叶斯相比,决策树是一种更强健的分类技术。原因在于,首先,决策树将特征分为训练集和测试集。然后,它通过良好的泛化能力来推断预测标签或类别。最有趣的是,决策树算法可以处理二分类和多分类问题。

例如,在下面的示例图中,DT 从入学数据中学习,通过一组if…else决策规则来近似正弦曲线。数据集包含每个申请入学学生的记录,例如,申请进入美国大学的学生。每条记录包含研究生入学考试成绩、CGPA 成绩和排名。现在,我们需要根据这三个特征(变量)预测谁是合格的。DTs 可以在训练 DT 模型并剪枝不需要的树枝后,用于解决这种问题。通常来说,更深的树表示更复杂的决策规则和更好的拟合模型:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/fa48fbd9-4b1f-4e97-abd1-fe85ca6be6d4.png

图 6:大学入学数据的决策树

因此,树越深,决策规则越复杂,模型拟合度越高。现在,让我们看一下 DT 的优缺点:

优点缺点更擅长
决策树 (DTs)-简单实现、训练和解释-可以可视化树-数据准备要求少-模型构建和预测时间较短-可以处理数值和分类数据-通过统计测试验证模型的可能性-对噪声和缺失值具有鲁棒性-高精度-大而复杂的树难以解释-同一子树中可能出现重复-可能存在对角决策边界的问题-决策树学习者可能会创建过于复杂的树,无法很好地泛化数据-有时 DTs 可能因为数据中的微小变化而不稳定-学习 DT 本身是一个 NP 完全问题-如果某些类占主导地位,DT 学习者会创建偏倚的树-目标是实现高准确度的分类-医学诊断和预后-信用风险分析

现在,我们已经了解了 DT 的工作原理,接下来让我们开始使用基于 Spark 的 DT 实现。首先,导入所需的包和库:

import org.apache.spark._
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.{DecisionTreeClassifier, DecisionTreeClassificationModel}
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}

现在让我们创建一个 Spark 会话并导入隐式转换:

val spark: SparkSession = SparkSessionCreate.createSession("ChurnPredictionDecisionTrees")
import spark.implicits._

现在,一旦我们定义并初始化了超参数,下一步是实例化一个DecisionTreeClassifier估算器,如下所示:

val dTree = new DecisionTreeClassifier()
                .setLabelCol("label")
                .setFeaturesCol("features")
                .setSeed(1234567L)

现在,我们有三个转换器和一个估算器准备好,接下来的任务是将它们串联成一个单一的管道——即它们每个都作为一个阶段:

val pipeline = new Pipeline()
                .setStages(Array(PipelineConstruction.ipindexer,
                PipelineConstruction.labelindexer,
                PipelineConstruction.assembler,dTree))

让我们定义参数网格,在超参数空间上执行这样的网格搜索。这个搜索通过 DT 的杂质、最大分箱数和最大深度来寻找最佳模型。树的最大深度:深度 0 表示 1 个叶节点;深度 1 表示 1 个内部节点+2 个叶节点。

另一方面,最大分箱数用于分离连续特征并选择每个节点上如何分裂特征。更多的分箱提供更高的粒度。简而言之,我们通过决策树的maxDepthmaxBins参数来搜索最佳模型:

var paramGrid = new ParamGridBuilder()
    .addGrid(dTree.impurity, "gini" :: "entropy" :: Nil)
    .addGrid(dTree.maxBins, 2 :: 5 :: 10 :: 15 :: 20 :: 25 :: 30 :: Nil)
    .addGrid(dTree.maxDepth, 5 :: 10 :: 15 :: 20 :: 25 :: 30 :: 30 :: Nil)
    .build()

在前面的代码段中,我们通过序列格式创建了一个逐步的参数网格。这意味着我们正在创建一个包含不同超参数组合的网格空间。这将帮助我们提供由最优超参数组成的最佳模型。

让我们定义一个BinaryClassificationEvaluator评估器来评估模型:

val evaluator = new BinaryClassificationEvaluator()
    .setLabelCol("label")
    .setRawPredictionCol("prediction")

我们使用CrossValidator进行 10 折交叉验证,以选择最佳模型:

val crossval = new CrossValidator()
    .setEstimator(pipeline)
    .setEvaluator(evaluator)
    .setEstimatorParamMaps(paramGrid)
    .setNumFolds(numFolds)

现在让我们调用fit方法,这样完整的预定义管道,包括所有特征预处理和决策树分类器,将被多次执行——每次使用不同的超参数向量:

val cvModel = crossval.fit(Preprocessing.trainDF)

现在是时候评估决策树模型在测试数据集上的预测能力了。第一步,我们需要使用模型管道转换测试集,这将按照我们在前面的特征工程步骤中描述的相同机制映射特征:

val predictions = cvModel.transform(Preprocessing.testSet)
prediction.show(10)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/7ecce996-69f6-4412-b021-7588816d89d6.png

然而,看到前面的预测 DataFrame,真的很难猜测分类准确率。在第二步中,评估是通过使用BinaryClassificationEvaluator进行评估,如下所示:

val accuracy = evaluator.evaluate(predictions)
println("Classification accuracy: " + accuracy)
>>>
Accuracy: 0.870334928229665

所以,我们从我们的二元分类模型中得到了大约 87%的分类准确率。现在,类似于 SVM 和 LR,我们将基于以下包含测试集原始分数的 RDD,观察精确度-召回曲线下的面积和 ROC 曲线下的面积:

val predictionAndLabels = predictions
    .select("prediction", "label")
    .rdd.map(x => (x(0).asInstanceOf[Double], x(1)
    .asInstanceOf[Double]))

现在,前面的 RDD 可以用于计算之前提到的两个性能指标:

val metrics = new BinaryClassificationMetrics(predictionAndLabels)
println("Area under the precision-recall curve: " + metrics.areaUnderPR)
println("Area under the receiver operating characteristic (ROC) curve : " + metrics.areaUnderROC)
>>>
Area under the precision-recall curve: 0.7293101942399631
Area under the receiver operating characteristic (ROC) curve: 0.870334928229665

在这种情况下,评估结果返回 87%的准确率,但只有 73%的精确度,这比 SVM 和 LR 要好得多。接下来,我们将再次计算一些其他指标;例如,假阳性和真阳性及假阴性预测也有助于评估模型的性能:

val lp = predictions.select("label", "prediction")
val counttotal = predictions.count()

val correct = lp.filter($"label" === $"prediction").count()

val wrong = lp.filter(not($"label" === $"prediction")).count()

val ratioWrong = wrong.toDouble / counttotal.toDouble

val ratioCorrect = correct.toDouble / counttotal.toDouble

val truep = lp.filter($"prediction" === 0.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val truen = lp.filter($"prediction" === 1.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val falsep = lp.filter($"prediction" === 1.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

val falsen = lp.filter($"prediction" === 0.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

println("Total Count : " + counttotal)
println("Correct : " + correct)
println("Wrong: " + wrong)
println("Ratio wrong: " + ratioWrong)
println("Ratio correct: " + ratioCorrect)
println("Ratio true positive : " + truep)
println("Ratio false positive : " + falsep)
println("Ratio true negative : " + truen)
println("Ratio false negative : " + falsen)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/e919b818-4fc3-4bd9-8bde-c1e4974f3518.png

太棒了;我们达到了 87%的准确率,但是什么因素导致的呢?嗯,可以通过调试来获得分类过程中构建的决策树。但首先,让我们看看在交叉验证后我们在什么层次上达到了最佳模型:

val bestModel = cvModel.bestModel
println("The Best Model and Parameters:n--------------------")
println(bestModel.asInstanceOf[org.apache.spark.ml.PipelineModel].stages(3))
>>>

最佳模型和参数:

DecisionTreeClassificationModel (uid=dtc_1fb45416b18b) of depth 5 with 53 nodes.

这意味着我们在深度为 5,节点数为 53 的决策树模型上达到了最佳效果。现在,让我们通过展示树来提取树构建过程中做出的决策。这棵树帮助我们找出数据集中最有价值的特征:

bestModel.asInstanceOf[org.apache.spark.ml.PipelineModel]
    .stages(3)
    .extractParamMap

val treeModel = bestModel.asInstanceOf[org.apache.spark.ml.PipelineModel]
    .stages(3)
    .asInstanceOf[DecisionTreeClassificationModel]
println("Learned classification tree model:n" + treeModel.toDebugString)
>>>

学到的分类树模型:

If (feature 3 <= 245.2)
    If (feature 11 <= 3.0)
        If (feature 1 in {1.0})
            If (feature 10 <= 2.0)
                Predict: 1.0
            Else (feature 10 > 2.0)
            If (feature 9 <= 12.9)
                Predict: 0.0
            Else (feature 9 > 12.9)
                Predict: 1.0
        ...
    Else (feature 7 > 198.0)
        If (feature 2 <= 28.0)
            Predict: 1.0
        Else (feature 2 > 28.0)
            If (feature 0 <= 60.0)
                Predict: 0.0
            Else (feature 0 > 60.0)
                Predict: 1.0

在前面的输出中,toDebugString()函数打印了决策树的决策节点,最终预测结果出现在叶子节点上。我们也可以清楚地看到特征 11 和 3 被用来做决策;它们是客户可能流失的两个最重要因素。那么这两个特征是什么呢?我们来看一下:

println("Feature 11:" + Preprocessing.trainDF.filter(PipelineConstruction.featureCols(11)))
println("Feature 3:" + Preprocessing.trainDF.filter(PipelineConstruction.featureCols(3)))
>>>
Feature 11: [total_international_num_calls: double]
Feature 3: [total_day_mins: double]

因此,客户服务电话和总通话时长被决策树选中,因为它提供了一种自动化机制来确定最重要的特征。

等等!我们还没完成。最后但同样重要的是,我们将使用一种集成技术——随机森林(RF),它被认为比决策树(DTs)更强大的分类器。同样,让我们使用 Apache Spark ML 包中的随机森林实现。

随机森林用于流失预测

正如在第一章中所述,分析保险严重性索赔,随机森林是一种集成技术,它通过构建决策树集成来进行预测——即,多个决策树的集成。更技术地讲,它构建多个决策树,并将它们集成在一起,以获得更准确和更稳定的预测。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/5cc25b68-25d8-404b-8419-01ce7bd71ce4.png

图 7:随机森林及其集成技术的解释

这是一个直接的结果,因为通过独立评审团的最大投票,我们得到了比最佳评审团更好的最终预测(见前图)。现在我们已经知道了随机森林(RF)的工作原理,让我们开始使用基于 Spark 的 RF 实现。首先,导入所需的包和库:

import org.apache.spark._
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.{RandomForestClassifier, RandomForestClassificationModel}
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
import org.apache.spark.ml.tuning.{ParamGridBuilder, CrossValidator}

现在让我们创建 Spark 会话并导入隐式库:

val spark: SparkSession = SparkSessionCreate.createSession("ChurnPredictionRandomForest")
import spark.implicits._

现在,一旦我们定义并初始化了超参数,接下来的任务是实例化一个DecisionTreeClassifier估计器,如下所示:

val rf = new RandomForestClassifier()
    .setLabelCol("label")
    .setFeaturesCol("features")
    .setSeed(1234567L)// for reproducibility

现在我们已经准备好了三个变换器和一个估计器,接下来的任务是将它们串联成一个单一的管道——也就是说,每个变换器作为一个阶段:

val pipeline = new Pipeline()
    .setStages(Array(PipelineConstruction.ipindexer,
    PipelineConstruction.labelindexer,
    PipelineConstruction.assembler,rf))

让我们定义 paramgrid,以便在超参数空间上执行网格搜索:

val paramGrid = new ParamGridBuilder()
    .addGrid(rf.maxDepth, 3 :: 5 :: 15 :: 20 :: 50 :: Nil)
    .addGrid(rf.featureSubsetStrategy, "auto" :: "all" :: Nil)
    .addGrid(rf.impurity, "gini" :: "entropy" :: Nil)
    .addGrid(rf.maxBins, 2 :: 5 :: 10 :: Nil)
    .addGrid(rf.numTrees, 10 :: 50 :: 100 :: Nil)
    .build()

让我们定义一个BinaryClassificationEvaluator评估器来评估模型:

val evaluator = new BinaryClassificationEvaluator()
    .setLabelCol("label")
    .setRawPredictionCol("prediction")

我们使用CrossValidator执行 10 折交叉验证,以选择最佳模型:

val crossval = new CrossValidator()
    .setEstimator(pipeline)
    .setEvaluator(evaluator)
    .setEstimatorParamMaps(paramGrid)
    .setNumFolds(numFolds)

现在,让我们调用fit方法,以便执行完整的预定义管道,其中包括所有的特征预处理和决策树分类器,每次都会用不同的超参数向量执行:

val cvModel = crossval.fit(Preprocessing.trainDF)

现在是时候评估决策树模型在测试数据集上的预测能力了。第一步,我们需要将测试集转换为模型管道,这将按照我们在之前的特征工程步骤中描述的相同机制映射特征:

val predictions = cvModel.transform(Preprocessing.testSet)
prediction.show(10)
>>>

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/3c3565fc-44f6-4bf0-a838-72fae88c09e3.png

然而,通过查看前述的预测数据框,确实很难猜测分类准确性。在第二步中,评估是通过使用BinaryClassificationEvaluator来进行的,如下所示:

val accuracy = evaluator.evaluate(predictions)
println("Classification accuracy: " + accuracy)
>>>
Accuracy: 0.870334928229665

因此,我们的二分类模型得到了约 87%的分类准确率。现在,类似于 SVM 和 LR,我们将根据以下包含测试集原始分数的 RDD,观察精度-召回曲线下的面积以及 ROC 曲线下的面积:

val predictionAndLabels = predictions
    .select("prediction", "label")
    .rdd.map(x => (x(0).asInstanceOf[Double], x(1)
    .asInstanceOf[Double]))

现在,前述的 RDD 可以用来计算之前提到的两个性能指标:

val metrics = new BinaryClassificationMetrics(predictionAndLabels)

println("Area under the precision-recall curve: " + metrics.areaUnderPR)
println("Area under the receiver operating characteristic (ROC) curve : " + metrics.areaUnderROC)
>>>
Area under the precision-recall curve: 0.7293101942399631
Area under the receiver operating characteristic (ROC) curve: 0.870334928229665

在这种情况下,评估返回了 87%的准确性,但仅有 73%的精度,这比 SVM 和 LR 要好得多。接下来,我们将再次计算一些更多的指标;例如,假阳性和真阳性、假阴性和真阴性预测也有助于评估模型的性能:

val lp = predictions.select("label", "prediction")
val counttotal = predictions.count()

val correct = lp.filter($"label" === $"prediction").count()

val wrong = lp.filter(not($"label" === $"prediction")).count()

val ratioWrong = wrong.toDouble / counttotal.toDouble

val ratioCorrect = correct.toDouble / counttotal.toDouble

val truep = lp.filter($"prediction" === 0.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val truen = lp.filter($"prediction" === 1.0).filter($"label" ===
$"prediction").count() / counttotal.toDouble

val falsep = lp.filter($"prediction" === 1.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

val falsen = lp.filter($"prediction" === 0.0).filter(not($"label" ===
$"prediction")).count() / counttotal.toDouble

println("Total Count : " + counttotal)
println("Correct : " + correct)
println("Wrong: " + wrong)
println("Ratio wrong: " + ratioWrong)
println("Ratio correct: " + ratioCorrect)
println("Ratio true positive : " + truep)
println("Ratio false positive : " + falsep)
println("Ratio true negative : " + truen)
println("Ratio false negative : " + falsen)
>>>

我们将得到以下结果:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/9c1a146a-2390-4b0e-b71e-af399d8e61b3.png

太棒了;我们达到了 91%的准确率,但是什么因素导致的呢?嗯,类似于决策树,随机森林也可以调试,获取分类过程中构建的决策树。为了打印树并选择最重要的特征,尝试 DT 的最后几行代码,您就完成了。

现在,你能猜到训练了多少个不同的模型吗?嗯,我们在交叉验证上有 10 折,超参数空间的基数为 2 到 7 的五个维度。现在来做一些简单的数学计算:10 * 7 * 5 * 2 * 3 * 6 = 12600 个模型!

请注意,我们仍然将超参数空间限制在numTreesmaxBinsmaxDepth的范围内,最大为 7。此外,请记住,较大的树通常会表现得更好。因此,欢迎在此代码上进行尝试,添加更多特性,并使用更大的超参数空间,例如,更大的树。

选择最佳模型进行部署

从前述结果可以看出,LR 和 SVM 模型的假阳性率与随机森林和决策树相同,但较高。因此,我们可以说,在真正的阳性计数方面,决策树和随机森林的准确性整体上更好。让我们通过每个模型的饼图预测分布来验证前述说法的有效性:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/e0108be2-f86e-40ab-9f3f-00e341dda8df.png

现在,值得一提的是,使用随机森林时,我们实际上能获得较高的准确性,但这是一个非常耗费资源和时间的工作;特别是训练,与 LR 和 SVM 相比,训练时间显著较长。

因此,如果您的内存或计算能力较低,建议在运行此代码之前增加 Java 堆空间,以避免 OOM 错误。

最后,如果您想部署最佳模型(在我们的案例中是随机森林),建议在fit()方法调用后立即保存交叉验证模型:

// Save the workflow
cvModel.write.overwrite().save("model/RF_model_churn")

训练好的模型将保存在该位置。该目录将包含:

  • 最佳模型

  • 估计器

  • 评估器

  • 训练本身的元数据

现在,接下来的任务是恢复相同的模型,如下所示:

// Load the workflow back
val cvModel = CrossValidatorModel.load("model/ RF_model_churn/")

最后,我们需要将测试集转换为模型管道,以便根据我们在前述特征工程步骤中描述的相同机制映射特征:

val predictions = cvModel.transform(Preprocessing.testSet)

最后,我们评估恢复的模型:

val evaluator = new BinaryClassificationEvaluator()
    .setLabelCol("label")
    .setRawPredictionCol("prediction")

val accuracy = evaluator.evaluate(predictions)
    println("Accuracy: " + accuracy)
    evaluator.explainParams()

val predictionAndLabels = predictions
    .select("prediction", "label")
    .rdd.map(x => (x(0).asInstanceOf[Double], x(1)
    .asInstanceOf[Double]))

val metrics = new BinaryClassificationMetrics(predictionAndLabels)
val areaUnderPR = metrics.areaUnderPR
println("Area under the precision-recall curve: " + areaUnderPR)
val areaUnderROC = metrics.areaUnderROC
println("Area under the receiver operating characteristic (ROC) curve: " + areaUnderROC)
>>>

您将收到以下输出:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/scala-ml-proj/img/5cb2c2c5-1984-4919-a0c1-8d433a22c386.png

好了,完成了!我们成功地重用了模型并进行了相同的预测。但是,由于数据的随机性,我们观察到略微不同的预测结果。

总结

在本章中,我们已经学习了如何开发一个机器学习项目来预测客户是否有可能取消订阅,并通过此方法开发了一个真实的预测模型。我们使用了逻辑回归(LR)、支持向量机(SVM)、决策树(DT)和随机森林(Random Forest)来构建预测模型。我们还分析了通常用来进行数据初步分析的客户数据类型。最后,我们了解了如何选择适合生产环境的模型。

在下一章中,我们将学习如何开发一个真实项目,该项目收集历史和实时的比特币数据,并预测未来一周、一个月等的价格。此外,我们还将学习如何为在线加密货币交易生成一个简单的信号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值