Spark-下一代机器学习教程-全-
Spark 下一代机器学习教程(全)
一、机器学习导论
我可以给你通常的论点。但事实是,发现的前景太甜了。
—杰弗里·辛顿 我
机器学习(ML)是人工智能的一个子领域,即制造智能机器的科学和工程。 ii 人工智能的先驱之一亚瑟·塞缪尔(Arthur Samuel)将机器学习定义为“无需明确编程就能赋予计算机学习能力的研究领域。” iii 图 1-1 展示了人工智能、机器学习、深度学习之间的关系。人工智能(AI)包含其他领域,这意味着尽管所有的机器学习都是 AI,但并非所有的 AI 都是机器学习。人工智能的另一个分支,符号人工智能,是二十世纪大部分时间里占主导地位的人工智能研究范式。 iv 符号人工智能实现被称为专家系统或知识图,它们本质上是使用 if-then 语句通过演绎推理得出逻辑结论的规则引擎。你可以想象,符号人工智能受到几个关键的限制;其中最主要的是在规则引擎中定义规则后修改规则的复杂性。添加更多的规则增加了规则引擎中的知识,但是它不能改变现有的知识。 v 机器学习模型另一方面更加灵活。他们可以根据新数据接受再培训,以学习新知识或修改现有知识。象征性人工智能也涉及重大的人类干预。它依赖于人类的知识,需要人类在规则引擎中硬编码规则。另一方面,机器学习更加动态,从输入数据中学习和识别模式,以产生所需的输出。
图 1-1
AI、机器学习、深度学习的关系 vi
深度学习在 2000 年代中期的复兴将人们的注意力重新集中到人工智能和机器学习的连接主义方法上。深度学习的复兴、高速图形处理单元(GPU)的可用性、大数据的出现以及谷歌、脸书、亚马逊、微软和 IBM 等公司的投资创造了推动人工智能复兴的完美风暴。
人工智能和机器学习用例
在过去的十年里,机器学习取得了一系列惊人的进步。这些突破正在扰乱我们的日常生活,并对你能想到的几乎每一个垂直领域产生影响。这绝不是机器学习用例的详尽列表,但它表明了每个行业正在发生的许多创新。
零售
零售业是最早受益于机器学习的行业之一。多年来,在线购物网站一直依赖协作和基于内容的过滤算法来个性化购物体验。在线推荐和高度针对性的营销活动为零售商创造了数百万甚至数十亿的收入。或许是 ML 驱动的在线推荐和个性化的典型代表,亚马逊是最受欢迎(也是最成功)的实现机器学习好处的在线零售商。根据麦肯锡的一项研究,亚马逊网站 35%的收入来自其推荐引擎。 vii 零售的附加机器学习应用包括货架空间规划、货架图优化、目标营销、客户细分和需求预测。
运输
几乎每个主要的汽车制造商都在研究由深度神经网络驱动的人工智能自动驾驶汽车。这些车辆配备了支持 GPU 的计算机,每秒可以处理超过 100 万亿次运算,用于实时人工智能感知、导航和路径规划。运输和物流公司,如 UPS 和 FedEx,使用机器学习进行路线和燃料优化、车队监控、预防性维护、旅行时间估计和智能地理围栏。
金融服务
预测客户终身价值(CLV)、信用风险预测和欺诈检测是金融服务的一些关键机器学习用例。对冲基金和投资银行使用机器学习来分析 Twitter Firehose 的数据,以检测可能推动市场的推文。金融服务的其他常见机器学习用例包括预测下一个最佳行动、流失预测、情感分析和多渠道营销归因。
医疗保健和生物技术
医疗保健是人工智能和机器学习研究和应用的一个重要领域。医院和医疗保健初创企业正在使用人工智能和机器学习来帮助准确诊断威胁生命的疾病,如心脏病、癌症和结核病。人工智能驱动的药物发现以及成像和诊断是人工智能最具代表性的领域 viii 。人工智能还彻底改变了生物技术和基因组学研究的方式,导致了路径分析、微阵列分析、基因预测和功能注释方面的新创新。IX
制造业
具有前瞻性思维的制造商正在使用深度学习进行质量检查,以检测硬件产品上的裂纹、边缘不平和划痕等缺陷。多年来,制造业和工业工程师一直使用生存分析来预测重型设备的故障时间。人工智能机器人正在实现制造过程的自动化,与人类机器人相比,操作速度更快,精度更高,从而提高了生产率,降低了产品缺陷。物联网(IoT)的到来和传感器数据的丰富正在扩大该行业机器学习应用的数量。
政府
机器学习在政府中有着广泛的应用。例如,公共事业公司一直在使用机器学习来监控公用事业管道。异常检测技术有助于检测泄漏和管道破裂,这些泄漏和管道破裂会导致全市范围的服务中断和数百万美元的财产损失。机器学习还被用于实时水质监测,防止污染疾病,并有可能挽救生命。为了节约能源,公有能源公司通过确定用电量的高峰和低谷,使用机器学习来相应地调整能源输出。人工智能支持的网络安全是另一个快速发展的领域,也是一个重要的政府用例,尤其是在当今时代。
机器学习和数据
机器学习模型是使用算法和数据的组合来构建的。使用强大的算法至关重要,但同样重要(有些人可能会说更重要)的是用大量高质量的数据进行训练。一般来说,数据越多,机器学习的表现越好。2001 年,微软的研究人员 Michele Banko 和 Eric Brill 在他们颇具影响力的论文“扩展到非常非常大的自然语言消歧语料库”中首次提出了这个概念。谷歌的研究主管彼得·诺维格(Peter Norvig)在他的论文《数据的不合理有效性》中进一步推广了这一概念。 x 然而,比数量更重要的是质量。每个高质量的模型都是从高质量的特性开始的。这就是特征工程进入画面的地方。特征工程是将原始数据转化为高质量特征的过程。这通常是整个机器学习过程中最艰巨的部分,但也是最重要的部分。我将在本章后面更详细地讨论特征工程。但与此同时,让我们来看看一个典型的机器学习数据集。图 1-2 显示了虹膜数据集的子集。我将在整本书的一些例子中使用这个数据集。
图 1-2
机器学习数据集
观察
单行数据被称为观察值或实例。
特征
特征是观察的属性。特征是用作模型输入的独立变量。在图 1-2 中,特征是花瓣长度、花瓣宽度、萼片长度和萼片宽度。
类别标签
类别标签是数据集中的因变量。这是我们试图预测的事情。它是输出。在我们的例子中,我们试图预测鸢尾花的类型:Virginica、Setosa 和 Versicolor。
模型
模型是一种具有预测能力的数学构造。它估计数据集中自变量和因变量之间的关系。Xi
机器学习方法
有不同类型的机器学习方法。决定使用哪一个很大程度上取决于你想要完成什么以及你所拥有的原始数据的种类。
监督学习
监督学习是一种使用训练数据集进行预测的机器学习任务。监督学习可以分为分类或回归。回归用于预测连续值,如“价格”、“温度”或“距离”,而分类用于预测类别,如“是”或“否”、“垃圾邮件”或“非垃圾邮件”,或者“恶性”或“良性”。
分类
分类可能是最常见的监督机器学习任务。您很可能已经遇到过在没有意识到的情况下利用分类的应用程序。常见的使用案例包括医疗诊断、定向营销、垃圾邮件检测、信用风险预测和情感分析等。有三种分类任务:二进制、多类和多标签。
二元分类
如果只有两个类别,则任务是二元或二项式分类。例如,当使用二进制分类算法进行垃圾邮件检测时,输出变量可以有两个类别,“垃圾邮件”或“非垃圾邮件”。对于检测癌症,类别可以是“恶性”或“良性”。对于有针对性的营销,预测某人购买诸如牛奶等物品的可能性,分类可以简单地是“是”或“否”。
多类分类
多类或多项分类任务有三个或更多类别。例如,要预测天气状况,您可能有五个类别:“多雨”、“多云”、“晴天”、“下雪”和“刮风”。为了扩展我们的目标营销示例,多类别分类可用于预测客户是否更有可能购买全脂牛奶、低脂牛奶、低脂牛奶或脱脂牛奶。
多标签分类
在多标签分类中,可以为每个观察值分配多个类别。相比之下,在多类别分类中,只能将一个类别分配给一个观察。使用我们的目标营销示例,多标签分类不仅用于预测客户是否更有可能购买牛奶,还用于预测其他商品,如饼干、黄油、热狗或面包。
分类和回归算法
多年来已经开发了各种分类和回归算法。它们在方法、特征、复杂性、易用性、训练性能和预测准确性方面各不相同。我在下面的文字中描述了最受欢迎的。我在第三章中详细介绍了它们。
支持向量机
支持向量机(SVM)是一种流行的算法,它通过寻找使两个类之间的间隔最大化的最佳超平面来工作,通过尽可能宽的间隙将数据点分成单独的类。最接近分类边界的数据点称为支持向量。
逻辑回归
逻辑回归是预测概率的线性分类器。它使用逻辑(sigmoid)函数将其输出转换为可以映射到两个(二元)类的概率值。通过多项式逻辑回归(softmax)支持多类分类。 xii 当您的数据有明确的决策边界时,线性分类器(如逻辑回归)是合适的。
奈伊夫拜厄斯
朴素贝叶斯是一种基于贝叶斯定理的简单多类线性分类算法。朴素贝叶斯之所以得名,是因为它天真地假设数据集中的要素是独立的,忽略了要素之间任何可能的相关性。现实世界的情况并非如此,朴素贝叶斯仍然表现良好,尤其是在小数据集或高维数据集上。像线性分类器一样,它在非线性分类问题上表现不佳。朴素贝叶斯是一种计算效率高且高度可伸缩的算法,只需要对数据集进行一次传递。对于使用大型数据集的分类任务,这是一个很好的基线模型。它的工作原理是在给定一组特征的情况下,找出一个点属于某个类的概率。
多层感知器
多层感知器是一个前馈人工网络,由几个完全连接的节点层组成。输入图层中的节点对应于输入数据集。中间层中的节点使用逻辑(sigmoid)函数,而最终输出层中的节点使用 softmax 函数来支持多类分类。输出层中的节点数量必须与类的数量相匹配。XIII
决策树
决策树通过学习从输入变量推断出的决策规则来预测输出变量的值。从视觉上看,决策树看起来就像一棵倒置的树,根节点在顶部。每个内部节点都代表对一个属性的测试。叶节点代表一个类标签,而单个分支代表一个测试的结果。决策树很容易解释。与像逻辑回归这样的线性模型相比,决策树不需要特征缩放。它能够处理缺失的特征,并处理连续和分类特征。 xiv 一次性编码分类特征 xv 在使用决策树和基于树的集成时不是必需的,事实上是不鼓励的。独热编码创建了不平衡的树,并且要求树生长得非常深以实现良好的预测性能。对于高基数分类特征来说尤其如此。不利的一面是,决策树对数据中的噪声很敏感,有过度拟合的倾向。由于这种限制,决策树本身很少在现实生产环境中使用。如今,决策树是更强大的集成算法的基础模型,如随机森林和梯度提升树。
随机森林
随机森林是一种集成算法,它使用一组决策树进行分类和回归。它使用一种叫做bagging(bootstrap aggregation)的方法来减少方差,同时保持低偏差。Bagging 从训练数据的子集训练单独的树。除了装袋,兰登森林还采用了另一种叫做的方法装袋。与 bagging(使用观测值的子集)相反,特征 bagging 使用特征(列)的子集。特征装袋旨在减少决策树之间的相关性。如果没有特征打包,单个树将会非常相似,尤其是在只有几个主要特征的情况下。对于分类,单个树的输出或模式的多数投票成为模型的最终预测。对于回归,单个树的输出的平均值成为最终输出(图 3-3 )。Spark 并行训练几棵树,因为每棵树都是在随机森林中独立训练的。
梯度提升树
梯度推进树(GBT)是另一种类似于随机森林的基于树的集成算法。gbt 使用一种称为 boosting to 的技术从弱学习者(浅树)中创建强学习者。GBTs 按顺序训练一组决策树 xvi ,每一棵后继的树减少前一棵的误差。这是通过使用前一个模型的残差来拟合下一个模型 xvii 来完成的。该残差校正过程 xviii 被执行设定的迭代次数,迭代次数由交叉验证确定,直到残差被完全最小化。
XGBoost
XGBoost(极限梯度提升)是目前可用的最好的梯度提升树实现之一。XGBoost 于 2014 年 3 月 27 日由陈天琦发布,作为一个研究项目,它已经成为分类和回归的主流机器学习算法。XGBoost 是使用梯度推进的一般原则设计的,将弱学习者组合成强学习者。但是,虽然梯度提升树是按顺序构建的——慢慢地从数据中学习,以在后续迭代中改进其预测,但 XGBoost 是并行构建树的。
XGBoost 通过其内置的正则化来控制模型复杂性和减少过拟合,从而产生更好的预测性能。为连续特征寻找最佳分割点时,XGBoost 使用近似算法来寻找分割点。 xix 近似分裂法使用离散箱来桶化连续特征,显著加快模型训练。XGBoost 包括另一种使用基于直方图的算法的树生长方法,该方法提供了一种将连续要素分入离散箱的更有效的方法。但是,虽然近似方法每次迭代都创建一组新的面元,但是基于直方图的方法在多次迭代中重复使用面元。这种方法允许使用近似方法无法实现的额外优化,例如缓存二进制文件以及父直方图和兄弟直方图相减 xx 的能力。为了优化排序操作,XGBoost 将排序后的数据存储在内存中的块单元中。排序块可以由并行 CPU 核心高效地分配和执行。XGBoost 可以通过其加权分位数草图算法有效地处理加权数据,可以有效地处理稀疏数据,支持缓存,并通过为大型数据集利用磁盘空间来支持核外计算,因此数据不必放在内存中。XGBoost 不是核心 Spark MLlib 库的一部分,但它作为一个外部包提供。
莱特格姆
多年来,XGBoost 一直是每个人最喜欢的分类和回归算法。最近,LightGBM 成为了王位的新挑战者。它是一个相对较新的基于树的梯度提升变体,类似于 XGBoost。LightGBM 于 2016 年 10 月 17 日发布,是微软分布式机器学习工具包(DMTK)项目的一部分。它被设计成快速和分布式的,导致更快的训练速度和更低的内存使用。它支持 GPU 和并行学习以及处理大型数据集的能力。
在几个基准测试和公共数据集上的实验中,LightGBM 显示出比 XGBoost 更快的速度和更好的准确性。它比 XGBoost 有几个优点。LightGBM 利用直方图将连续特征分成离散的箱。这为 LightGBM 提供了优于 XGBoost(默认情况下,XGBoost 使用基于预排序的算法进行树学习)的几个性能优势,例如减少了内存使用、减少了计算每次分割的增益的成本,以及减少了并行学习的通信成本。LightGBM 通过对其兄弟节点和父节点执行直方图减法来计算节点的直方图,从而实现了额外的性能提升。在线基准测试显示,在某些任务中,LightGBM 比 XGBoost(不含宁滨)快 11 到 15 倍。 xxi LightGBM 通过逐叶生长(最佳优先)的方式,一般在精度上优于 XGBoost。有两种主要的策略来训练决策树,级别方式和叶方式。对于大多数基于树的集成(包括 XGBoost),逐层树生长是生长决策树的传统方式。LightGBM 引入了逐叶增长策略。与水平方向生长相比,叶方向生长通常收敛更快 xxii 并实现更低的损失。 xxiii 和 XGBoost 一样,LightGBM 不是核心 Spark MLlib 库的一部分,但它作为一个外部包提供。LightGBM 的大部分特性最近都移植到了 XGBoost 上。我将在第三章详细讨论这两种算法。
回归
回归是一种用于预测连续数值的监督机器学习任务。举几个例子来说,流行的用例包括销售和需求预测、预测股票、房屋或商品价格以及天气预报。决策树、随机森林、梯度提升树、XGBoost 和 LightGBM 也可以用于回归。我将在第三章更详细地讨论回归。
线性回归
线性回归用于检查一个或多个自变量和因变量之间的线性关系。对单个自变量和单个连续因变量之间关系的分析称为简单线性回归。多元回归是简单线性回归的扩展,用于根据多个自变量预测因变量的值。
生存回归
生存回归,也称为死亡时间分析或失败时间分析,用于预测特定事件将要发生的时间。生存回归与线性回归的主要区别在于其处理删失的能力,删失是一种缺失数据问题,事件发生的时间未知。
无监督学习
无监督学习是一种机器学习任务,它在没有标记响应的帮助下发现数据集中隐藏的模式和结构。当您只能访问输入数据,而训练数据不可用或难以获得时,无监督学习是理想的选择。常见的方法包括聚类、主题建模、异常检测、推荐和主成分分析。
使聚集
聚类是一种无监督的机器学习任务,用于对具有某些相似性的未标记观察值进行分组。流行的聚类用例包括客户细分、欺诈分析和异常检测。在训练数据缺乏或不可用的情况下,聚类也经常用于为分类器生成训练数据。
k 均值
K-Means 是最流行的无监督聚类学习算法之一。K-Means 的工作原理是随机分配质心作为每个聚类的起点。该算法基于欧几里德距离迭代地将每个数据点分配到最近的质心。然后,它通过计算属于该聚类的所有点的平均值来计算每个聚类的新质心。当达到预定义的迭代次数,或者每个数据点都被分配到其最近的质心,并且不再有可以执行的重新分配时,该算法停止迭代。
主题建模
主题模型自动导出一组文档中的主题。这些主题可用于基于内容的推荐、文档分类、维度缩减和特征化。
潜在狄利克雷分配
潜在狄利克雷分配(LDA)是由戴维·布雷、吴恩达和迈克尔·乔丹在 2003 年提出的,尽管乔纳森·k·普里查德、马修·斯蒂芬斯和彼得·唐纳利在 2000 年也提出了一种用于群体遗传学的类似算法。应用于机器学习的 LDA 是基于图形模型的,并且是第一个包含基于 GraphX 构建的 Spark MLlib 的算法。潜在狄利克雷分配广泛用于主题建模。
异常检测
异常或异常值检测可识别出明显偏离大多数数据集的罕见观察值。它经常用于发现欺诈性金融交易、识别网络安全威胁或执行预测性维护,仅举几个使用案例。
隔离森林
隔离森林是一种基于树的集成算法,用于异常检测,该算法是由刘飞东尼、婷和开发的。 xxiv 与大多数异常检测技术不同,隔离森林试图显式检测实际异常值,而不是识别正常数据点。隔离林的运行基于这样一个事实,即数据集中通常存在少量异常值,因此易于进行隔离。 xxv 从正常数据点中分离异常值是有效的,因为它需要的条件较少。相比之下,隔离正常数据点通常会涉及更多条件。与其他基于树的集合类似,隔离林建立在一组称为隔离树的决策树上,每棵树都有整个数据集的一个子集。异常分数被计算为森林中树木的平均异常分数。异常分值来自分割数据点所需的条件数量。接近 1 的异常分数表示异常,而小于 0.5 的分数表示非异常观察。
单类支持向量机
支持向量机(SVM)通过使用最佳超平面,以尽可能宽的间隔将数据点划分为单独的类别,从而对数据进行分类。在一类 SVM 中,模型根据只有一个“正常”类的数据进行训练。与正常示例不同的数据点被视为异常。
降维
当数据集中有大量要素时,降维至关重要。例如,基因组学和工业分析领域的机器学习用例通常涉及数千甚至数百万个特征。高维数使得模型更加复杂,增加了过度拟合的机会。在某一点上添加更多的特征实际上会降低模型的性能。此外,对高维数据的训练需要大量的计算资源。这些被统称为维度的诅咒。降维技术旨在克服维数灾难。
主成分分析
主成分分析(PCA)是一种无监督的机器学习技术,用于降低特征空间的维度。它检测要素之间的相关性,并生成数量减少的线性不相关要素,同时保留原始数据集中的大部分方差。这些更紧凑、线性不相关的特征被称为主成分。主成分按其解释方差的降序排列。其他降维技术包括奇异值分解(SVD)和线性判别分析(LDA)。
推荐
提供个性化推荐是机器学习最流行的应用之一。几乎每个主要零售商,如亚马逊、阿里巴巴、沃尔玛和塔吉特,都根据顾客行为提供某种个性化推荐。网飞、Hulu 和 Spotify 等流媒体服务根据用户的口味和偏好提供电影或音乐推荐。协作过滤、基于内容的过滤和关联规则学习(用于购物篮分析)是构建推荐系统最流行的方法。Spark MLlib 支持用于协作过滤的交替最小二乘法(ALS ),以及用于购物篮分析的 FP-Growth 和 PrefixSpan。
半监督学习
在某些情况下,访问带标签的数据既费钱又费时。在标记响应稀少的情况下,半监督学习结合监督和非监督学习技术来进行预测。在半监督学习中,利用未标记数据来扩充标记数据以提高模型精度。
强化学习
强化学习试图通过试错来学习,以确定哪种行为提供了最大的回报。强化学习有三个组成部分:代理(决策者或学习者)、环境(代理与之交互的内容)和动作(代理可以执行的内容)。 xxvi 这种类型的学习经常用于游戏、导航和机器人。
深度学习
深度学习是机器学习和人工智能的一个子领域,使用深度、多层人工神经网络。它促成了人工智能领域最近的许多突破。虽然深度学习可以用于更平凡的分类任务,但当应用于更复杂的问题时,如医疗诊断、面部识别、自动驾驶汽车、欺诈分析和智能语音控制助理,它的真正力量会大放异彩。 xxvii 在某些领域,深度学习已经让计算机能够匹配甚至超越人类的能力。
神经网络
神经网络是一类算法,其操作类似于人脑的互连神经元。神经网络包含由互连节点组成的多层。通常有一个输入层、一个或多个隐藏层和一个输出层。数据经由输入层通过神经网络。隐藏层通过加权连接网络处理数据。隐藏层中的节点为输入分配权重,并将其与一组系数组合。数据通过一个节点的激活函数,该函数决定了层的输出。最后,数据到达输出层,输出层产生神经网络的最终输出。 xxviii 具有几个隐含层的神经网络称为“深度”神经网络。层次越多,网络就越深,通常网络越深,学习就变得越复杂,它能解决的问题也越复杂。
卷积神经网络
卷积神经网络(简称为 convnet 或 CNN)是一种特别擅长分析图像的神经网络(尽管它们也可以应用于音频和文本数据)。卷积神经网络层中的神经元以三维方式排列:高度、宽度和深度。CNN 使用卷积层来学习其输入特征空间(图像)中的局部模式,例如纹理和边缘。相反,全连接(密集)层学习全局模式。 xxix 卷积层中的神经元仅连接到其之前层的一小部分区域,而不是像密集层那样连接到所有神经元。密集层的完全连接结构会导致大量的参数,这是低效的,并可能很快导致过度拟合。 xxx 我在第七章更详细地介绍了深度学习和深度卷积神经网络。
特征工程
特征工程是转换数据以创建可用于训练机器学习模型的特征的过程。通常,原始数据需要通过多种数据准备和提取技术进行转换。例如,对于非常大的数据集,可能需要降维。可能需要从其他要素创建新要素。基于距离的算法需要特征缩放。当分类特征被一键编码时,一些算法执行得更好。文本数据通常需要标记化和特征矢量化。将原始数据转换为特征后,对其进行评估,并选择最具预测能力的特征。
特征工程是机器学习的一个重要方面。几乎在每一个机器学习的努力中,如果要成功,生成高度相关的特征是不言自明的。不幸的是,特征工程是一项复杂而耗时的任务,通常需要领域专业知识。这是一个反复的过程,包括集思广益的特征,创建它们并研究它们对模型准确性的影响。事实上,根据福布斯的一项调查,典型的数据科学家将大部分时间用于准备数据(图 1-3 )。XXXI
图 1-3
数据准备约占数据科学家工作的 80%
特征工程任务可以分为几类:特征选择、特征重要性、特征提取和特征构造。XXXII
特征选择
特征选择是识别重要特征和消除不相关或冗余特征的重要预处理步骤。它提高了预测性能和模型训练效率,并降低了维数。必须移除不相关的特征,因为它们会对模型的准确性产生负面影响,并降低模型训练的速度。某些特征可能没有任何预测能力,或者它们可能与其他特征是冗余的。但是我们如何确定这些特征是否相关呢?领域知识至关重要。例如,如果您正在建立一个模型来预测贷款违约的概率,它有助于了解在量化信用风险时需要考虑哪些因素。你可以从借款人的债务收入比开始。还有其他借款特定的因素要考虑,如借款人的信用评分,就业时间,就业职称和婚姻状况。经济增长等整个市场的考虑也可能很重要。人口统计学和心理学信息也应该被考虑。一旦你有了一个特性列表,有几种方法可以客观地确定它们的重要性。有多种特征选择方法可帮助您为模型选择正确的特征。XXXIII
过滤方法
过滤方法使用统计技术(如卡方检验、相关系数和信息增益)为每个特征分配等级。
包装方法
包装方法使用特征的子集来训练模型。然后,您可以根据模型的性能添加或删除特征。包装器方法的常见示例有递归特征消除、向后消除和向前选择。
嵌入式方法
嵌入式方法结合了过滤器和包装器方法使用的技术。流行的例子包括套索和岭回归、正则化树和随机多项式 logit。XXXIV
特征重要性
基于树的集成(如 Random Forest、XGBoost 和 LightGBM)提供了一种基于为每个特征计算的特征重要性分数的特征选择方法。分数越高,该特性对提高模型准确性越重要。随机森林中的特征重要性也称为基于基尼系数的重要性或杂质平均减少(MDI)。随机森林的一些实现利用不同的方法来计算特征重要性,这种方法被称为基于精度的重要性或平均精度下降(MDA)。 xxxv 基于准确度的重要性是基于特征被随机置换时预测准确度的降低来计算的。我将在第三章中详细讨论随机森林、XGBoost 和 LightGBM 中的特性重要性。
相关系数是特征选择方法的基本形式。相关系数代表两个变量之间线性关系的强度。对于线性问题,您可以使用相关性来选择相关要素(要素类相关性)和识别冗余要素(要素内相关性)。
特征抽出
当数据集中有大量要素时,要素提取至关重要。例如,基因组学和工业分析领域的机器学习用例通常涉及数千甚至数百万个特征。高维数使得模型更加复杂,增加了过度拟合的机会。此外,对高维数据的训练需要大量的计算资源。特征提取通常涉及使用降维技术。主成分分析(PCA)、线性判别分析(LDA)和奇异值分解(SVD)是一些最流行的降维算法,也用于特征提取。
特征构造
为了提高模型的准确性,有时需要根据现有要素构建新要素。有几种方法可以做到这一点。您可以组合或聚合要素。在某些情况下,您可能想要拆分它们。例如,您的模型可能受益于将大多数事务数据中常见的时间戳属性拆分成几个更细粒度的属性:秒、分钟、小时、日、月和年。然后,您可能希望使用这些属性来构造更多的特性,比如 dayofweek、weekofmonth、monthofyear 等等。特征构造既是艺术,又是科学,是特征工程中最困难和最耗时的方面之一。精通要素构造通常是经验丰富的数据科学家与新手之间的区别。
模型评估
在分类中,每个数据点都有一个已知的标签和一个模型生成的预测类。通过比较每个数据点的已知标注和预测类别,结果可分为四类:预测类别为正且标注为正的真阳性(TP );预测类别为负且标注为负的真阴性(TN );预测类别为正但标注为负的假阳性(FP );预测类别为负且标注为正的假阴性(FN)。这四个值构成了大多数分类任务评估指标的基础。它们经常出现在一个叫做混淆矩阵的表格中(表格 1-1 )。
表 1-1
混淆矩阵
准确
准确度是分类模型的评估标准。它被定义为正确预测数除以预测总数。
在数据集不平衡的情况下,精确度不是理想的衡量标准。为了举例说明,考虑具有 90 个阴性和 10 个阳性样本的假设分类任务;将所有分类为负面给出 0.90 的准确度分数。精度和召回率是评估用类不平衡数据训练的模型的更好的度量。
精确
精确度被定义为真阳性的数量除以真阳性的数量加上假阳性的数量。精度显示了当模型的预测为正时,模型正确的频率。例如,如果您的模型预测了 100 个癌症发生,但其中 10 个是不正确的预测,则您的模型的精确度为 90%。在误报成本很高的情况下,精确度是一个很好的度量标准。
回忆
召回是一个很好的衡量标准,可以用在假阴性成本很高的情况下。召回被定义为真阳性的数量除以真阳性的数量加上假阴性的数量。
F1 度量
F1 测量值或 F1 分数是精确度和召回率的调和平均值或加权平均值。这是评估多类分类器的一个常见性能指标。当存在不均匀的阶级分布时,这也是一个很好的衡量标准。F1 成绩最好的是 1,最差的是 0。一个好的 F1 测量意味着你有很低的假阴性和假阳性。F1 测度定义如下:
受试者工作特性下的面积(AUROC)
接受者操作特征下的面积(AUROC)是用于评估二元分类器的常见性能度量。受试者工作特性(ROC)是绘制真阳性率与假阳性率的图表。曲线下面积(AUC)是 ROC 曲线下的面积。AUC 可以解释为模型对随机正例的排序高于随机负例的概率。 xxxvi 曲线下面积越大(AUROC 越接近 1.0),模型表现越好。AUROC 为 0.5 的模型是无用的,因为它的预测准确性与随机猜测一样好。
过度拟合和欠拟合
模型的糟糕表现是由过度拟合或拟合不足造成的。过度拟合是指模型过于拟合训练数据。过度拟合的模型对训练数据表现良好,但对新的、看不见的数据表现不佳。过度拟合的反义词是拟合不足。对于欠拟合,模型过于简单,并且没有学习训练数据集中的相关模式,这是因为模型过于规则或者需要训练更长时间。模型很好地适应新的、看不见的数据的能力被称为泛化。这是每个模型调整练习的目标。防止过度拟合的几种既定方法包括使用更多数据或特征子集、交叉验证、放弃、修剪、提前停止和正则化。 xxxvii 对于深度学习来说,数据增强是一种常见的正则化形式。为了减少欠拟合,建议添加更多相关特征。对于深度学习,可以考虑在一层中增加更多的节点,或者在神经网络中增加更多的层,以增加模型的容量。xxxviii
型号选择
模型选择包括评估拟合的机器学习模型,并通过尝试用用户指定的超参数组合拟合底层估计器来输出最佳模型。使用 Spark MLlib,通过 CrossValidator 和 TrainValidationSplit 估计器执行模型选择。CrossValidator 为超参数调整和模型选择执行 k 重交叉验证和网格搜索。它将数据集分成一组随机的、不重叠的分区褶皱,用作训练和测试数据集。例如,如果 k=3 折,k 折交叉验证将生成 3 个训练和测试数据集对(每个折仅用作测试数据集一次),每个数据集对使用 2/3 用于训练数据,1/3 用于测试。XXXIXTrainValidationSplit 是超参数调优的另一个估计器。与 k-fold 交叉验证(这是一种昂贵的操作)相比,TrainValidationSplit 只对每个参数组合评估一次,而不是 k 次。
摘要
这一章是机器学习的快速介绍。为了更彻底的治疗,我建议统计学习的元素,第二版。、特雷弗·哈斯蒂等人的(施普林格,2016 年)和加雷斯·詹姆斯等人的统计学习导论(施普林格,2013 年)。关于深度学习的介绍,我推荐伊恩·古德菲勒等人的深度学习(麻省理工出版社,2016)。虽然机器学习已经存在很长时间了,但使用大数据来训练机器学习模型是最近才发展起来的。作为最受欢迎的大数据框架,Spark 的独特定位是成为构建大规模企业级机器学习应用的卓越平台。让我们在下一章深入探讨 Spark 和 Spark MLlib。
参考
-
raffi khatchadourian《DOOMSDAY 发明》,纽约人。com ,2015 年,
www.newyorker.com/magazine/2015/11/23/doomsday-invention-artificial-intelligence-nick-bostrom
-
约翰·麦卡锡;“什么是人工智能?”,stanford.edu,2007,
www-formal.stanford.edu/jmc/whatisai/node1.html
-
克里斯·尼科尔森;《人工智能(AI) vs 机器学习 vs 深度学习》,skimind.ai,2019,
https://skymind.ai/wiki/ai-vs-machine-learning-vs-deep-learning
-
玛尔塔·加内洛和默里·沙纳汉;《用符号化人工智能调和深度学习:表示对象和关系》,sciencedirect.com,2019,
www.sciencedirect.com/science/article/pii/S2352154618301943
-
克里斯·尼科尔森;“符号推理(Symbolic AI)和机器学习”;skymind.ai,2019,
https://skymind.ai/wiki/symbolic-reasoning
-
迈克尔·科普兰;“人工智能、机器学习、深度学习有什么区别?”,nvidia.com,2016,
https://blogs.nvidia.com/blog/2016/07/29/whats-difference-artificial-intelligence-machine-learning-deep-learning-ai/
-
伊恩·麦肯齐等人;“零售商如何才能跟上消费者”,mckinsey.com,2013,
www.mckinsey.com/industries/retail/our-insights/how-retailers-can-keep-up-with-consumers
-
CB 洞察;“从药物 R&D 到诊断学:医疗保健领域的 90+人工智能创业公司”,cbinsights.com,2019,
www.cbinsights.com/research/artificial-intelligence-startups-healthcare/
-
Ragothaman Yennamali《机器学习在生物学中的应用》,kolabtree.com,2019,
www.kolabtree.com/blog/applications-of-machine-learning-in-biology/
-
泽维尔·阿马特里安;《在机器学习中,什么更好:更多的数据还是更好的算法》,kdnuggets.com,2015,
www.kdnuggets.com/2015/06/machine-learning-more-data-better-algorithms.html
-
穆罕默德·古勒;“借助 Spark 进行大数据分析”,2015 年出版
-
阿帕奇 Spark《多项逻辑回归》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-classification-regression.html#multinomial-logistic-regression
-
阿帕奇 Spark《多层感知器分类器》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-classification-regression.html#multilayer-perceptron-classifier
-
分析 Vidhya 内容团队;《从零开始的基于树的建模完整教程(用 R & Python 编写)》,AnalyticsVidhya.com,2016,
www.analyticsvidhya.com/blog/2016/04/complete-tutorial-tree-based-modeling-scratch-in-python/#one
-
LightGBM《分类特征的最优分割》,lightgbm.readthedocs.io,2019,
https://lightgbm.readthedocs.io/en/latest/Features.html
-
约瑟夫·布拉德利和马尼什·阿姆德;《MLlib 中的随机森林与助推》,Databricks,2015,
https://databricks.com/blog/2015/01/21/random-forests-and-boosting-in-mllib.html
-
分析 Vidhya 内容团队;《理解 XGBoost 背后数学的端到端指南》,analyticsvidhya.com,2018,
www.analyticsvidhya.com/blog/2018/09/an-end-to-end-guide-to-understand-the-math-behind-xgboost/
-
本·戈尔曼;“一位 Kaggle 大师解释渐变增强,”Kaggle.com,2017,
http://blog.kaggle.com/2017/01/23/a-kaggle-master-explains-gradient-boosting/
-
莉娜·肖;《XGBoost:简明技术概述》,KDNuggets,2017,
www.kdnuggets.com/2017/10/xgboost-concise-technical-overview.html
-
Philip Hyunsu Cho“快速直方图优化生长器,8 到 10 倍加速”,DMLC,2017,
https://github.com/dmlc/xgboost/issues/1950
-
Laurae"基准测试 light GBM:light GBM 与 xgboost 相比有多快?",medium.com,2017 年
[`https://medium.com/implodinggradients/benchmarking-lightgbm-how-fast-is-lightgbm-vs-xgboost-15d224568031`](https://medium.com/implodinggradients/benchmarking-lightgbm-how-fast-is-lightgbm-vs-xgboost-15d224568031)
-
LightGBM《在速度和内存使用上的优化》,lightgbm.readthedocs.io,2019,
https://lightgbm.readthedocs.io/en/latest/Features.html
-
大卫·马克思;“决策树:逐叶(最佳优先)和逐级树遍历”,stackexchange.com,2018,
https://datascience.stackexchange.com/questions/26699/decision-trees-leaf-wise-best-first-and-level-wise-tree-traverse
-
费托尼刘,婷,-周华;《隔离森林》,acm.org,2008,
https://dl.acm.org/citation.cfm?id=1511387
-
亚历杭德罗·科雷亚·巴恩森;“利用隔离森林进行异常检测的好处”,easysol.net,2016,
https://blog.easysol.net/using-isolation-forests-anamoly-detection/
-
SAS《机器学习》,sas.com,2019,
www.sas.com/en_us/insights/analytics/machine-learning.html
-
英伟达;《深度学习》,developer.nvidia.com,2019,
https://developer.nvidia.com/deep-learning
-
SAS《神经网络如何工作》,sas.com,2019,
www.sas.com/en_us/insights/analytics/neural-networks.html
-
弗朗索瓦·乔莱;“计算机视觉的深度学习”,2018,用 Python 进行深度学习
-
安德烈·卡帕西;《卷积神经网络(CNN/conv nets)》,github.io,2019,
http://cs231n.github.io/convolutional-networks/
-
吉尔出版社;“清洗大数据:最耗时、最不愉快的数据科学任务”,调查称,“forbes.com,2016,
www.forbes.com/sites/gilpress/2016/03/23/data-preparation-most-time-consuming-least-enjoyable-data-science-task-survey-says/#680347536f63
-
杰森·布朗利;“发现特征工程,如何工程化特征,如何擅长”,machinelearningmastery.com,2014,
https://machinelearningmastery.com/discover-feature-engineering-how-to-engineer-features-and-how-to-get-good-at-it/
-
杰森·布朗利;《特征选择导论》,MachineLearningMastery.com,2014,
https://machinelearningmastery.com/an-introduction-to-feature-selection/
-
绍拉夫·考什克;《特征选择方法介绍及实例》,Analyticsvidhya.com,2016,
www.analyticsvidhya.com/blog/2016/12/introduction-to-feature-selection-methods-with-an-example-or-how-to-select-the-right-variables/
-
杰克·霍尔;《随机森林的可变重要性是如何计算的》,DisplayR,2018,
www.displayr.com/how-is-variable-importance-calculated-for-a-random-forest/
-
谷歌;“分类:ROC 曲线和 AUC”,developers.google.com,2019,
https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc
-
韦恩·汤普森;《机器学习最佳实践:理解泛化》,blogs.sas.com,2017,
https://blogs.sas.com/content/subconsciousmusings/2017/09/05/machine-learning-best-practices-understanding-generalization/
-
杰森·布朗利;《深度学习神经网络如何避免过拟合》,machinelearningmaster.com,2018,
https://machinelearningmastery.com/introduction-to-regularization-to-reduce-overfitting-and-improve-generalization-error/
-
火花;《CrossValidator》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/api/scala/index.html#org.apache.spark.ml.tuning.CrossValidator
二、Spark 和 Spark MLlib 简介
简单的模型和大量的数据胜过基于较少数据的更复杂的模型。
—彼得·诺维格 我
Spark 是一个统一的大数据处理框架,用于处理和分析大型数据集。Spark 提供 Scala、Python、Java 和 R 的高级 API,具有强大的库,包括用于机器学习的 MLlib、用于 SQL 支持的 Spark SQL、用于实时流的 Spark Streaming 和用于图形处理的 GraphX。 ii Spark 由马泰·扎哈里亚(Matei Zaharia)在加州大学伯克利分校的 AMPLab 创立,后捐赠给阿帕奇软件基金会,于 2014 年 2 月 24 日成为顶级项目。 iii 第一版于 2017 年 5 月 30 日发布。 iv
概观
Spark 的开发是为了解决 Hadoop 最初的数据处理框架 MapReduce 的局限性。Matei Zaharia 在加州大学伯克利分校和脸书分校(他在那里实习)看到了 MapReduce 的局限性,并试图创建一个更快、更通用、多用途的数据处理框架,以处理迭代和交互式应用程序。 v 它提供了一个统一的平台(图 2-1 ),支持流式、交互式、图形处理、机器学习、批处理等多种类型的工作负载。 vi Spark 作业的运行速度比同等的 MapReduce 作业快好几倍,这是因为它具有快速的内存功能和高级 DAG(定向非循环图)执行引擎。Spark 是用 Scala 编写的,因此它是 Spark 事实上的编程接口。我们将在整本书中使用 Scala。我们将在第七章中使用 PySpark,这是用于 Spark 的 Python API,用于分布式深度学习。本章是我上一本书下一代大数据(2018 年出版)中第五章的更新版本。
图 2-1
Apache Spark 生态系统
集群管理器
集群管理器管理和分配集群资源。Spark 支持 Spark(独立调度程序)、YARN、Mesos 和 Kubernetes 附带的独立集群管理器。
体系结构
在高层次上,Spark 将 Spark 应用程序任务的执行分布在集群节点上(图 2-2 )。每个 Spark 应用程序在其驱动程序中都有一个 SparkContext 对象。SparkContext 表示到集群管理器的连接,集群管理器为 Spark 应用程序提供计算资源。在连接到集群之后,Spark 在您的 worker 节点上获取执行器。然后 Spark 将您的应用程序代码发送给执行器。一个应用程序通常会运行一个或多个作业来响应一个 Spark 动作。然后,Spark 将每个作业划分为更小的阶段或任务的有向无环图(DAG)。然后,每个任务被分发并发送给工作节点上的执行器来执行。
图 2-2
Apache Spark 架构
每个 Spark 应用程序都有自己的一组执行器。因为来自不同应用程序的任务在不同的 JVM 中运行,所以一个 Spark 应用程序不会干扰另一个 Spark 应用程序。这也意味着,如果不使用外部数据源,比如 HDFS 或 S3,Spark 应用程序很难共享数据。使用 Tachyon(又名 Alluxio)等堆外内存存储可以使数据共享更快更容易。我将在这一章的后面更详细地讨论 Alluxio。
执行 Spark 应用程序
您可以使用交互式 shell (spark-shell 或 pyspark)或提交应用程序(spark-submit)来执行 spark 应用程序。一些人更喜欢使用基于网络的交互式笔记本,如 Apache Zeppelin 和 Jupyter,来与 Spark 进行交互。Databricks 和 Cloudera 等商业供应商也提供了他们自己的交互式笔记本环境。我将在整章中使用火花壳。在带有集群管理器(如 YARN)的环境中,启动 Spark 应用程序有两种部署模式。
集群模式
在集群模式下,驱动程序运行在由 YARN 管理的主应用程序中。客户端可以退出而不影响应用程序的执行。以集群模式启动应用程序或 spark-shell:
spark-shell --master yarn --deploy-mode cluster
spark-submit --class mypath.myClass --master yarn --deploy-mode cluster
客户端模式
在客户端模式下,驱动程序在客户端运行。应用程序主机仅用于向 YARN 请求资源。要在客户端模式下启动应用程序或 spark-shell:
spark-shell --master yarn --deploy-mode client
spark-submit --class mypath.myClass --master yarn --deploy-mode client
火花壳简介
您通常使用交互式 shell 进行特定的数据分析或探索。也是学习 Spark API 的好工具。Spark 的交互 shell 有 Spark 或者 Python 两种版本。在下面的示例中,我们将创建一个城市 RDD,并将它们全部转换为大写字母。当您启动 spark-shell 时,会自动创建一个名为“spark”的 SparkSession,如清单 2-1 所示。
spark-shell
Spark context Web UI available at http://10.0.2.15:4041
Spark context available as 'sc' (master = local[*], app id = local-1574144576837).
Spark session available as 'spark'.
Welcome to
____ __
/ __/__ ___ _____/ /__
_\ \/ _ \/ _ `/ __/ '_/
/___/ .__/\_,_/_/ /_/\_\ version 2.4.4
/_/
Using Scala version 2.11.12 (OpenJDK 64-Bit Server VM, Java 1.8.0_212)
Type in expressions to have them evaluated.
Type :help for more information.
scala>val myCities = sc.parallelize(List(
"tokyo",
"new york",
"sydney",
"san francisco"))
scala>val uCities = myCities.map {x =>x.toUpperCase}
scala>uCities.collect.foreach(println)
TOKYO
NEW YORK
SYDNEY
SAN FRANCISCO
Listing 2-1Introduction to spark-shell
火花会议
如图 2-2 所示,SparkContext 支持访问所有 Spark 特性和功能。驱动程序使用 SparkContext 来访问其他上下文,如 StreamingContext、SQLContext 和 HiveContext。从 Spark 2.0 开始,SparkSession 提供了与 Spark 交互的单一入口点。Spark 1.x 中通过 SparkContext、SQLContext、HiveContext 和 StreamingContext 提供的所有功能现在都可以通过 SparkSession 访问。 vii 你可能仍然会遇到用 Spark 1.x 编写的代码。
val sparkConf = new SparkConf().setAppName("MyApp").setMaster("local")
val sc = new SparkContext(sparkConf).set("spark.executor.cores", "4")
val sqlContext = new org.apache.spark.sql.SQLContext(sc)
在 Spark 2.x 中,您不必显式创建 SparkConf、SparkContext 或 SQLContext,因为它们的所有功能都已经包含在 SparkSession 中。
val spark = SparkSession.
builder().
appName("MyApp").
config("spark.executor.cores", "4").
getOrCreate()
弹性分布式数据集(RDD)
RDD 是一个有弹性的不可变分布式对象集合,跨集群中的一个或多个节点进行分区。rdd 可以通过两种类型的操作并行处理和操作:转换和操作。
Note
RDD 是 Spark 1.x 中 Spark 的主要编程接口。从 Spark 2.0 开始,数据集已经取代 RDD 成为主要的 API。由于更丰富的编程界面和更好的性能,建议用户从 RDD 切换到数据集/数据框架。我将在本章后面讨论数据集和数据帧。
创建 RDD
创建 RDD 非常简单。你可以从现有的 Scala 集合中创建一个 RDD,或者从存储在 HDFS 或 S3 的外部文件中读取。
平行放置
并行化从 Scala 集合创建一个 RDD。
val data = (1 to 5).toList
val rdd = sc.parallelize(data)
val cities = sc.parallelize(List("tokyo","new york","sydney","san francisco"))
文本文件
文本文件从储存在 HDFS 或 S3 的文本文件创建 RDD。
val rdd = sc.textFile("hdfs://master01:9000/files/mydirectory")
val rdd = sc.textFile("s3a://mybucket/files/mydata.csv")
请注意,RDD 是不可变的。数据转换会产生另一个 RDD,而不是修改当前的 RDD。RDD 操作可以分为两类:转换和行动。
转换
转换是创建新 RDD 的操作。我描述了一些最常见的转换。有关完整的列表,请参考在线 Spark 文档。
地图
Map 对 RDD 中的每个元素执行一个函数。它创建并返回结果的新 RDD。地图的返回类型不必与原始 RDD 的类型相同。
val cities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
val upperCaseCities = myCities.map {x =>x.toUpperCase}
upperCaseCities.collect.foreach(println)
TOKYO
NEW YORK
PARIS
SAN FRANCISCO
让我们展示另一个地图的例子。
val lines = sc.parallelize(List("Michael Jordan", "iPhone"))
val words = lines.map(line =>line.split(" "))
words.collect
res2: Array[Array[String]] = Array(Array(Michael, Jordan), Array(iPhone))
平面地图
FlatMap 对 RDD 中的每个元素执行一个函数,然后对结果进行拼合。
val lines = sc.parallelize(List("Michael Jordan", "iPhone"))
val words = lines.flatMap(line =>line.split(" "))
words.collect
res3: Array[String] = Array(Michael, Jordan, iPhone)
过滤器
Filter 返回一个仅包含与指定条件匹配的元素的 RDD。
val lines = sc.parallelize(List("Michael Jordan", "iPhone","Michael Corleone"))
val words = lines.map(line =>line.split(" "))
val results = words.filter(w =>w.contains("Michael"))
results.collect
res9: Array[Array[String]] = Array(Array(Michael, Jordan), Array(Michael, Corleone))
明显的
Distinct 只返回不同的值。
val cities1 = sc.parallelize(List("tokyo","tokyo","paris","sydney"))
val cities2 = sc.parallelize(List("perth","tokyo","canberra","sydney"))
val cities3 = cities1.union(cities2)
cities3.distinct.collect.foreach(println)
sydney
perth
canberra
tokyo
paris
ReduceByKey
ReduceByKey 使用指定的 reduce 函数将值与同一个键组合在一起。
val pairRDD = sc.parallelize(List(("a", 1), ("b",2), ("c",3), ("a", 30), ("b",25), ("a",20)))
val sumRDD = pairRDD.reduceByKey((x,y) =>x+y)
sumRDD.collect
res15: Array[(String, Int)] = Array((b,27), (a,51), (c,3))
键
Keys 返回只包含键的 RDD。
val rdd = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))
val keys = rdd.keys
keys.collect.foreach(println)
a
b
c
价值观念
值返回仅包含值的 RDD。
val rdd = sc.parallelize(List(("a", "Larry"), ("b", "Curly"), ("c", "Moe")))
val value = rdd.values
value.collect.foreach(println)
Larry
Curly
Moe
内部连接
内部连接基于连接谓词返回两个 RDD 中所有元素的 RDD。
val data = Array((100,"Jim Hernandez"), (101,"Shane King"))
val employees = sc.parallelize(data)
val data2 = Array((100,"Glendale"), (101,"Burbank"))
val cities = sc.parallelize(data2)
val data3 = Array((100,"CA"), (101,"CA"), (102,"NY"))
val states = sc.parallelize(data3)
val record = employees.join(cities).join(states)
record.collect.foreach(println)
(100,((Jim Hernandez,Glendale),CA))
(101,((Shane King,Burbank),CA))
RightOuterJoin 和 LeftOuterJoin
RightOuterJoin 返回右 RDD 中元素的 RDD,即使左 RDD 中没有匹配的行。LeftOuterJoin 等效于列顺序不同的 RightOuterJoin。
val record = employees.join(cities).rightOuterJoin(states)
record.collect.foreach(println)
(100,(Some((Jim Hernandez,Glendale)),CA))
(102,(None,NY))
(101,(Some((Shane King,Burbank)),CA))
联盟
Union 返回包含两个或更多 RDD 组合的 RDD。
val data = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","RollingHills","CA"))
val employees = sc.parallelize(data)
val data = Array((105,"Lester Cruz","VanNuys","CA"), (106,"John White","Inglewood","CA"))
val employees2 = sc.parallelize(data)
val rdd = sc.union([employees, employees2])
rdd.collect.foreach(println)
(103,MarkChoi,Torrance,CA)
(104,JanetReyes,RollingHills,CA)
(105,LesterCruz,VanNuys,CA)
(106,JohnWhite,Inglewood,CA)
减去
Subtract 返回仅包含第一个 RDD 中的元素的 RDD。
val data = Array((103,"Mark Choi","Torrance","CA"), (104,"Janet Reyes","Rolling Hills","CA"),(105,"Lester Cruz","Van Nuys","CA"))
val rdd = sc.parallelize(data)
val data2 = Array((103,"Mark Choi","Torrance","CA"))
val rdd2 = sc.parallelize(data2)
val employees = rdd.subtract(rdd2)
employees.collect.foreach(println)
(105,LesterCruz,Van Nuys,CA)
(104,JanetReyes,Rolling Hills,CA)
联合
联合减少了 RDD 中的分区数量。在大型 RDD 上执行过滤后,您可能需要使用合并。虽然过滤减少了新 RDD 消耗的数据量,但它继承了原始 RDD 的分区数量。如果新的 RDD 比原来的 RDD 小得多,它可能会有成百上千个小分区,这可能会导致性能问题。
当您想减少 Spark 在写入 HDFS 时生成的文件数量,防止可怕的“小文件”问题时,Coalesce 也很有用。每个分区作为单独的文件写入 HDFS。请注意,使用 coalesce 时,您可能会遇到性能问题,因为在写入 HDFS 时,您会有效地降低并行度。如果发生这种情况,请尝试增加分区的数量。在下面的例子中,我们只将一个拼花文件写入 HDFS。
df.coalesce(1).write.mode("append").parquet("/user/hive/warehouse/Mytable")
再分
重新分区可以减少或增加 RDD 中的分区数量。减少分区时通常会使用联合,因为它比重新分区更有效。增加分区数量有助于提高写入 HDFS 时的并行度。在下面的例子中,我们向 HDFS 写了六个拼花文件。
df.repartition(6).write.mode("append").parquet("/user/hive/warehouse/Mytable")
Note
合并通常比重新分区快。重新分区将执行完全洗牌,创建新分区并在工作节点之间平均分配数据。通过使用现有分区,联合最大限度地减少了数据移动并避免了完全洗牌。
行动
动作是向驱动程序返回值的 RDD 操作。我列出了一些最常见的动作。请参考在线 Spark 文档,了解完整的操作列表。
收集
Collect 将整个数据集作为数组返回给驱动程序。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.collect
res2: Array[String] = Array(tokyo, new york, paris, san francisco)
数数
Count 返回数据集中元素的数量。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.count
res3: Long = 4
拿
Take 将数据集的前 n 个元素作为数组返回。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.take(2)
res4: Array[String] = Array(tokyo, new york)
为每一个
Foreach 对数据集的每个元素执行一个函数。
val myCities = sc.parallelize(List("tokyo","new york","paris","san francisco"))
myCities.collect.foreach(println)
tokyo
newyork
paris
sanFrancisco
懒惰评估
Spark 支持惰性求值,这对于大数据处理至关重要。Spark 中的所有转换都是延迟计算的。Spark 不会立即执行转换。您可以继续定义更多的转换。当您最终想要最终结果时,您执行一个动作,这将导致转换被执行。
贮藏
默认情况下,每次运行操作时,都会重新执行每个转换。您可以使用 cache 或 persist 方法在内存中缓存 RDD,以避免多次重复执行转换。
蓄电池
累加器是只被“添加”的变量。它们通常用于实现计数器。在示例中,我使用累加器将数组的元素相加:
val accum = sc.longAccumulator("Accumulator 01")
sc.parallelize(Array(10, 20, 30, 40)).foreach(x =>accum.add(x))
accum.value
res2: Long = 100
广播变量
广播变量是存储在每个节点内存中的只读变量。Spark 使用高速广播算法来减少复制广播变量的网络延迟。使用广播变量在每个节点上存储数据集的副本是一种更快的方法,而不是将数据存储在 HDFS 或 S3 这样的慢速存储引擎中。
val broadcastVar = sc.broadcast(Array(10, 20, 30))
broadcastVar.value
res0: Array[Int] = Array(10, 20, 30)
Spark SQL、数据集和数据框架 API
开发 Spark SQL 是为了使处理和分析结构化数据变得更加容易。数据集类似于 RDD,因为它支持强类型,但在后台数据集有一个更有效的引擎。从 Spark 2.0 开始,数据集 API 现在是主要的编程接口。DataFrame 只是一个带有命名列的数据集,类似于关系表。Spark SQL 和 DataFrames 一起为处理和分析结构化数据提供了强大的编程接口。这里有一个关于如何使用 DataFrames API 的简单例子。
val jsonDF = spark.read.json("/jsondata/customers.json")
jsonDF.show
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
| 69| Plano| Kirsten Jung| TX| 202|75025|
| 52| Allen|Jessica Nguyen| TX| 203|75002|
+---+------+--------------+-----+------+-----+
jsonDF.select ("age","city").show
+---+------+
|age| city|
+---+------+
| 35|Frisco|
| 28|Dallas|
| 69| Plano|
| 52| Allen|
+---+------+
jsonDF.filter($"userid" < 202).show()
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
+---+------+--------------+-----+------+-----+
jsonDF.createOrReplaceTempView("jsonDF")
val df = spark.sql("SELECT userid, zip FROM jsonDF")
df.show
+------+-----+
|userid| zip|
+------+-----+
| 200|75034|
| 201|75001|
| 202|75025|
| 203|75002|
+------+-----+
Note
Spark 2.0 中统一了数据帧和数据集 API。DataFrame 现在只是行数据集的类型别名,其中行是通用的非类型化对象。相比之下,Dataset 是强类型对象 Dataset[T]的集合。Scala 支持强类型和非类型 API,而在 Java 中,Dataset[T]是主要的抽象。DataFrames 是 R 和 Python 的主要编程接口,因为它缺乏对编译时类型安全的支持。
Spark 数据源
读写不同的文件格式和数据源是最常见的数据处理任务之一。在我们的示例中,我们将同时使用 RDD 和 DataFrames API。
战斗支援车
Spark 为您提供了从 CSV 文件中读取数据的不同方法。您可以先将数据读入 RDD,然后将其转换为 DataFrame。
val dataRDD = sc.textFile("/sparkdata/customerdata.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)
val dataDF = parsedRDD.map{ a =>CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString) }.toDF
从 Spark 2.0 开始,CSV 连接器已经内置。
val dataDF = spark.read.format("csv")
.option("header", "true")
.load("/sparkdata/customerdata.csv")
可扩展置标语言
Databricks 有一个 Spark XML 包,可以轻松读取 XML 数据。
cat users.xml
<userid>100</userid><name>Wendell Ryan</name><city>San Diego</city><state>CA</state><zip>92102</zip>
<userid>101</userid><name>Alicia Thompson</name><city>Berkeley</city><state>CA</state><zip>94705</zip>
<userid>102</userid><name>Felipe Drummond</name><city>Palo Alto</city><state>CA</state><zip>94301</zip>
<userid>103</userid><name>Teresa Levine</name><city>Walnut Creek</city><state>CA</state><zip>94507</zip>
hadoop fs -mkdir /xmldata
hadoop fs -put users.xml /xmldata
spark-shell --packages com.databricks:spark-xml_2.10:0.4.1
使用 Spark XML 创建一个数据框架。在本例中,我们指定了行标记和 XML 文件所在的 HDFS 路径。
import com.databricks.spark.xml._
val xmlDF = spark.read
.option("rowTag", "user")
.xml("/xmldata/users.xml");
xmlDF: org.apache.spark.sql.DataFrame = [city: string, name: string, state: string, userid: bigint, zip: bigint]
我们也来看看数据。
xmlDF.show
+------------+---------------+-----+------+-----+
| city| name|state|userid| zip|
+------------+---------------+-----+------+-----+
| San Diego| Wendell Ryan| CA| 100|92102|
| Berkeley|Alicia Thompson| CA| 101|94705|
| Palo Alto|Felipe Drummond| CA| 102|94301|
|Walnut Creek| Teresa Levine| CA| 103|94507|
+------------+---------------+-----+------+-----+
数据
我们将创建一个 JSON 文件作为这个例子的样本数据。确保该文件位于 HDFS 名为/jsondata 的文件夹中。
cat users.json
{"userid": 200, "name": "Jonathan West", "city":"Frisco", "state":"TX", "zip": "75034", "age":35}
{"userid": 201, "name": "Andrea Foreman", "city":"Dallas", "state":"TX", "zip": "75001", "age":28}
{"userid": 202, "name": "Kirsten Jung", "city":"Plano", "state":"TX", "zip": "75025", "age":69}
{"userid": 203, "name": "Jessica Nguyen", "city":"Allen", "state":"TX", "zip": "75002", "age":52}
从 JSON 文件创建一个数据帧。
val jsonDF = spark.read.json("/jsondata/users.json")
jsonDF: org.apache.spark.sql.DataFrame = [age: bigint, city: string, name: string, state: string, userid: bigint, zip: string]
检查日期
jsonDF.show
+---+------+--------------+-----+------+-----+
|age| city| name|state|userid| zip|
+---+------+--------------+-----+------+-----+
| 35|Frisco| Jonathan West| TX| 200|75034|
| 28|Dallas|Andrea Foreman| TX| 201|75001|
| 69| Plano| Kirsten Jung| TX| 202|75025|
| 52| Allen|Jessica Nguyen| TX| 203|75002|
+---+------+--------------+-----+------+-----+
关系数据库和 MPP 数据库
我们在这个例子中使用 MySQL,但也支持其他关系数据库和 MPP 引擎,如 Oracle、Snowflake、Redshift、Impala、Presto 和 Azure DW。通常,只要关系数据库有 JDBC 驱动程序,就应该可以从 Spark 访问它。性能取决于您的 JDBC 驱动程序对批处理操作的支持。请查看您的 JDBC 驱动程序文档以了解更多详细信息。
mysql -u root -pmypassword
create databases salesdb;
use salesdb;
create table customers (
customerid INT,
name VARCHAR(100),
city VARCHAR(100),
state CHAR(3),
zip CHAR(5));
spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar
启动火花壳。
将 CSV 文件读入 RDD,并将其转换为数据帧。
val dataRDD = sc.textFile("/home/hadoop/test.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(customerid: Int, name: String, city: String, state: String, zip: String)
val dataDF = parsedRDD.map{ a =>CustomerData (a(0).toInt, a(1).toString, a(2).toString,a(3).toString,a(4).toString) }.toDF
将数据框注册为临时表,以便我们可以对其运行 SQL 查询。
dataDF.createOrReplaceTempView("dataDF")
让我们设置连接属性。
val jdbcUsername = "myuser"
val jdbcPassword = "mypass"
val jdbcHostname = "10.0.1.112"
val jdbcPort = 3306
val jdbcDatabase ="salesdb"
val jdbcrewriteBatchedStatements = "true"
val jdbcUrl = s"jdbc:mysql://${jdbcHostname}:${jdbcPort}/${jdbcDatabase}?user=${jdbcUsername}&password=${jdbcPassword}&rewriteBatchedStatements=${jdbcrewriteBatchedStatements}"
val connectionProperties = new java.util.Properties()
这将允许我们指定正确的保存模式–
追加、覆盖等等。
import org.apache.spark.sql.SaveMode
将 SELECT 语句返回的数据插入到 MySQL salesdb 数据库中存储的 customer 表中。
spark.sql("select * from dataDF")
.write
.mode(SaveMode.Append)
.jdbc(jdbcUrl, "customers", connectionProperties)
让我们用 JDBC 读一个表格。让我们用一些测试数据填充 MySQL 中的 users 表。确保 salesdb 数据库中存在 users 表。
mysql -u root -pmypassword
use salesdb;
describe users;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| userid | bigint(20) | YES | | NULL | |
| name | varchar(100) | YES | | NULL | |
| city | varchar(100) | YES | | NULL | |
| state | char(3) | YES | | NULL | |
| zip | char(5) | YES | | NULL | |
| age | tinyint(4) | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
select * from users;
Empty set (0.00 sec)
insert into users values (300,'Fred Stevens','Torrance','CA',90503,23);
insert into users values (301,'Nancy Gibbs','Valencia','CA',91354,49);
insert into users values (302,'Randy Park','Manhattan Beach','CA',90267,21);
insert into users values (303,'Victoria Loma','Rolling Hills','CA',90274,75);
select * from users;
+--------+---------------+-----------------+-------+-------+------+
| userid | name | city | state | zip | age |
+--------+---------------+-----------------+-------+-------+------+
| 300 | Fred Stevens | Torrance | CA | 90503 | 23 |
| 301 | Nancy Gibbs | Valencia | CA | 91354 | 49 |
| 302 | Randy Park | Manhattan Beach | CA | 90267 | 21 |
| 303 | Victoria Loma | Rolling Hills | CA | 90274 | 75 |
+--------+---------------+-----------------+-------+-------+------+
spark-shell --driver-class-path mysql-connector-java-5.1.40-bin.jar --jars mysql-connector-java-5.1.40-bin.jar
让我们设置 jdbcurl 和连接属性。
val jdbcURL = s"jdbc:mysql://10.0.1.101:3306/salesdb?user=myuser&password=mypass"
val connectionProperties = new java.util.Properties()
我们可以从整个表中创建一个数据帧。
val df = spark.read.jdbc(jdbcURL, "users", connectionProperties)
df.show
+------+-------------+---------------+-----+-----+---+
|userid| name| city|state| zip|age|
+------+-------------+---------------+-----+-----+---+
| 300| Fred Stevens| Torrance| CA|90503| 23|
| 301| Nancy Gibbs| Valencia| CA|91354| 49|
| 302| Randy Park|Manhattan Beach| CA|90267| 21|
| 303|Victoria Loma| Rolling Hills| CA|90274| 75|
+------+-------------+---------------+-----+-----+---+
镶木地板
在拼花地板上读写很简单。
val df = spark.read.load("/sparkdata/employees.parquet")
df.select("id","firstname","lastname","salary")
.write
.format("parquet")
.save("/sparkdata/myData.parquet")
You can run SELECT statements on Parquet files directly.
val df = spark.sql("SELECT * FROM parquet.`/sparkdata/myData.parquet`")
巴什
从 Spark 访问 HBase 有多种方式。例如,可以使用 SaveAsHadoopDataset 将数据写入 HBase。启动 HBase shell。
创建一个 HBase 表并用测试数据填充它。
hbase shell
create 'users', 'cf1'
启动火花壳。
spark-shell
val hconf = HBaseConfiguration.create()
val jobConf = new JobConf(hconf, this.getClass)
jobConf.setOutputFormat(classOf[TableOutputFormat])
jobConf.set(TableOutputFormat.OUTPUT_TABLE,"users")
val num = sc.parallelize(List(1,2,3,4,5,6))
val theRDD = num.filter.map(x=>{
val rowkey = "row" + x
val put = new Put(Bytes.toBytes(rowkey))
put.add(Bytes.toBytes("cf1"), Bytes.toBytes("fname"), Bytes.toBytes("my fname" + x))
(newImmutableBytesWritable, put)
})
theRDD.saveAsHadoopDataset(jobConf)
您还可以使用 Spark 中的 HBase 客户端 API 来读写 HBase 中的数据。如前所述,Scala 可以访问所有 Java 库。
启动 HBase shell。创建另一个 HBase 表,并用测试数据填充它。
hbase shell
create 'employees', 'cf1'
put 'employees','400','cf1:name', 'Patrick Montalban'
put 'employees','400','cf1:city', 'Los Angeles'
put 'employees','400','cf1:state', 'CA'
put 'employees','400','cf1:zip', '90010'
put 'employees','400','cf1:age', '71'
put 'employees','401','cf1:name', 'Jillian Collins'
put 'employees','401','cf1:city', 'Santa Monica'
put 'employees','401','cf1:state', 'CA'
put 'employees','401','cf1:zip', '90402'
put 'employees','401','cf1:age', '45'
put 'employees','402','cf1:name', 'Robert Sarkisian'
put 'employees','402','cf1:city', 'Glendale'
put 'employees','402','cf1:state', 'CA'
put 'employees','402','cf1:zip', '91204'
put 'employees','402','cf1:age', '29'
put 'employees','403','cf1:name', 'Warren Porcaro'
put 'employees','403','cf1:city', 'Burbank'
put 'employees','403','cf1:state', 'CA'
put 'employees','403','cf1:zip', '91523'
put 'employees','403','cf1:age', '62'
让我们验证数据是否成功地插入到我们的 HBase 表中。
scan 'employees'
ROW COLUMN+CELL
400 column=cf1:age, timestamp=1493105325812, value=71
400 column=cf1:city, timestamp=1493105325691, value=Los Angeles
400 column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
400 column=cf1:state, timestamp=1493105325738, value=CA
400 column=cf1:zip, timestamp=1493105325789, value=90010
401 column=cf1:age, timestamp=1493105334417, value=45
401 column=cf1:city, timestamp=1493105333126, value=Santa Monica
401 column=cf1:name, timestamp=1493105333050, value=Jillian Collins
401 column=cf1:state, timestamp=1493105333145, value=CA
401 column=cf1:zip, timestamp=1493105333165, value=90402
402 column=cf1:age, timestamp=1493105346254, value=29
402 column=cf1:city, timestamp=1493105345053, value=Glendale
402 column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
402 column=cf1:state, timestamp=1493105345074, value=CA
402 column=cf1:zip, timestamp=1493105345093, value=91204
403 column=cf1:age, timestamp=1493105353650, value=62
403 column=cf1:city, timestamp=1493105352467, value=Burbank
403 column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
403 column=cf1:state, timestamp=1493105352513, value=CA
403 column=cf1:zip, timestamp=1493105352549, value=91523
启动火花壳。
spark-shell
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.{HBaseConfiguration, HTableDescriptor}
import org.apache.hadoop.hbase.client.HBaseAdmin
import org.apache.hadoop.hbase.mapreduce.TableInputFormat
import org.apache.hadoop.hbase.HColumnDescriptor
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.util.Bytes;
import java.io.IOException;
val configuration = HBaseConfiguration.create()
指定 HBase 表和行键。
val table = new HTable(configuration, "employees");
val g = new Get(Bytes.toBytes("401"))
val result = table.get(g);
从表中提取值。
val val2 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("name"));
val val3 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("city"));
val val4 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("state"));
val val5 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("zip"));
val val6 = result.getValue(Bytes.toBytes("cf1"),Bytes.toBytes("age"));
将这些值转换为适当的数据类型。
val id = Bytes.toString(result.getRow())
val name = Bytes.toString(val2);
val city = Bytes.toString(val3);
val state = Bytes.toString(val4);
val zip = Bytes.toString(val5);
val age = Bytes.toShort(val6);
打印数值。
println(" employee id: " + id + " name: " + name + " city: " + city + " state: " + state + " zip: " + zip + " age: " + age);
employee id: 401 name: Jillian Collins city: Santa Monica state: CA zip: 90402 age: 13365
让我们使用 HBase API 写入 HBase。
val configuration = HBaseConfiguration.create()
val table = new HTable(configuration, "employees");
指定新的行键。
val p = new Put(new String("404").getBytes());
用新值填充单元格。
p.add("cf1".getBytes(), "name".getBytes(), new String("Denise Shulman").getBytes());
p.add("cf1".getBytes(), "city".getBytes(), new String("La Jolla").getBytes());
p.add("cf1".getBytes(), "state".getBytes(), new String("CA").getBytes());
p.add("cf1".getBytes(), "zip".getBytes(), new String("92093").getBytes());
p.add("cf1".getBytes(), "age".getBytes(), new String("56").getBytes());
写入 HBase 表。
table.put(p);
table.close();
确认值已成功插入 HBase 表中。
启动 HBase shell。
hbase shell
scan 'employees'
ROW COLUMN+CELL
400 column=cf1:age, timestamp=1493105325812, value=71
400 column=cf1:city, timestamp=1493105325691, value=Los Angeles
400 column=cf1:name, timestamp=1493105325644, value=Patrick Montalban
400 column=cf1:state, timestamp=1493105325738, value=CA
400 column=cf1:zip, timestamp=1493105325789, value=90010
401 column=cf1:age, timestamp=1493105334417, value=45
401 column=cf1:city, timestamp=1493105333126, value=Santa Monica
401 column=cf1:name, timestamp=1493105333050, value=Jillian Collins
401 column=cf1:state, timestamp=1493105333145, value=CA
401 column=cf1:zip, timestamp=1493105333165, value=90402
402 column=cf1:age, timestamp=1493105346254, value=29
402 column=cf1:city, timestamp=1493105345053, value=Glendale
402 column=cf1:name, timestamp=1493105344979, value=Robert Sarkisian
402 column=cf1:state, timestamp=1493105345074, value=CA
402 column=cf1:zip, timestamp=1493105345093, value=91204
403 column=cf1:age, timestamp=1493105353650, value=62
403 column=cf1:city, timestamp=1493105352467, value=Burbank
403 column=cf1:name, timestamp=1493105352445, value=Warren Porcaro
403 column=cf1:state, timestamp=1493105352513, value=CA
403 column=cf1:zip, timestamp=1493105352549, value=91523
404 column=cf1:age, timestamp=1493123890714, value=56
404 column=cf1:city, timestamp=1493123890714, value=La Jolla
404 column=cf1:name, timestamp=1493123890714, value=Denise Shulman
404 column=cf1:state, timestamp=1493123890714, value=CA
404 column=cf1:zip, timestamp=1493123890714, value=92093
虽然通常速度较慢,但也可以通过 SQL 查询引擎(如 Impala 或 Presto)访问 HBase。
亚马逊 S3
亚马逊 S3 是一个流行的对象存储,经常被用作临时集群的数据存储。它还是备份和冷数据的经济高效的存储方式。从 S3 读取数据就像从 HDFS 或任何其他文件系统读取数据一样。
阅读来自亚马逊 S3 的 CSV 文件。请确保您已经配置了 S3 凭据。
val myCSV = sc.textFile("s3a://mydata/customers.csv")
将 CSV 数据映射到 RDD。
import org.apache.spark.sql.Row
val myRDD = myCSV.map(_.split(',')).map(e ⇒ Row(r(0).trim.toInt, r(1), r(2).trim.toInt, r(3)))
创建一个模式。
import org.apache.spark.sql.types.{StructType, StructField, StringType, IntegerType};
val mySchema = StructType(Array(
StructField("customerid",IntegerType,false),
StructField("customername",StringType,false),
StructField("age",IntegerType,false),
StructField("city",StringType,false)))
val myDF = spark.createDataFrame(myRDD, mySchema)
使用
您可以使用 SolrJ 从 Spark 与 Solr 进行交互。 viii
import java.net.MalformedURLException;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrServer;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocumentList;
val solr = new HttpSolrServer("http://master02:8983/solr/mycollection");
val query = new SolrQuery();
query.setQuery("*:*");
query.addFilterQuery("userid:3");
query.setFields("userid","name","age","city");
query.setStart(0);
query.set("defType", "edismax");
val response = solr.query(query);
val results = response.getResults();
println(results);
从 Spark 访问 Solr 集合的一个更简单的方法是通过 spark-solr 包。Lucidworks 启动了 spark-solr 项目来提供 Spark-Solr 集成。与 solrJ 相比,使用 spark-solr 要简单和强大得多,它允许你从 Solr 集合中创建数据帧。
首先从 spark-shell 导入 JAR 文件。
spark-shell --jars spark-solr-3.0.1-shaded.jar
指定集合和连接信息。
val options = Map( "collection" -> "mycollection","zkhost" -> "{ master02:8983/solr}")
创建一个数据框架。
val solrDF = spark.read.format("solr")
.options(options)
.load
微软优越试算表
虽然我通常不推荐从 Spark 访问 Excel 电子表格,但是某些用例需要这种能力。一家名为 Crealytics 的公司开发了一个用于与 Excel 交互的 Spark 插件。该库需要 Spark 2.x。可以使用- packages 命令行选项添加该包。
spark-shell --packages com.crealytics:spark-excel_2.11:0.9.12
从 Excel 工作表创建数据框架。
val ExcelDF = spark.read
.format("com.crealytics.spark.excel")
.option("sheetName", "sheet1")
.option("useHeader", "true")
.option("inferSchema", "true")
.option("treatEmptyValuesAsNulls", "true")
.load("budget.xlsx")
将数据帧写入 Excel 工作表。
ExcelDF2.write
.format("com.crealytics.spark.excel")
.option("sheetName", "sheet1")
.option("useHeader", "true")
.mode("overwrite")
.save("budget2.xlsx")
你可以在他们的 GitHub 页面上找到更多的细节:github.com/crealytics.
安全 FTP
从 SFTP 服务器下载文件和向其写入数据帧也是一个流行的请求。SpringML 提供了一个 Spark SFTP 连接器库。该库需要 Spark 2.x 并利用 jsch,这是 SSH2 的一个 Java 实现。对 SFTP 服务器的读写将作为单个进程执行。
spark-shell --packages com.springml:spark-sftp_2.11:1.1.
从 SFTP 服务器中的文件创建一个数据帧。
val sftpDF = spark.read.
format("com.springml.spark.sftp").
option("host", "sftpserver.com").
option("username", "myusername").
option("password", "mypassword").
option("inferSchema", "true").
option("fileType", "csv").
option("delimiter", ",").
load("/myftp/myfile.csv")
将数据帧作为 CSV 文件写入 FTP 服务器。
sftpDF2.write.
format("com.springml.spark.sftp").
option("host", "sftpserver.com").
option("username", "myusername").
option("password", "mypassword").
option("fileType", "csv").
option("delimiter", ",").
save("/myftp/myfile.csv")
你可以在他们的 GitHub 页面上找到更多的细节:github.com/springml/spark-sftp.
Spark MLlib 简介
机器学习是 Spark 的主要应用之一。Spark MLlib 包括用于回归、分类、聚类、协作过滤和频繁模式挖掘的流行机器学习算法。它还为构建管线、模型选择和调整以及特征选择、提取和转换提供了广泛的功能。
Spark MLlib 算法
Spark MLlib 包括大量用于各种任务的机器学习算法。我们将在接下来的章节中介绍其中的大部分。
分类
-
逻辑回归(二项式和多项式)
-
决策图表
-
随机森林
-
梯度提升树
-
多层感知器
-
线性支持向量机
-
奈伊夫拜厄斯
-
一对一休息
回归
-
线性回归
-
决策图表
-
随机森林
-
梯度提升树
-
生存回归
-
保序回归
聚类
-
k 均值
-
平分 K-均值
-
高斯混合模型
-
潜在狄利克雷分配
协同过滤
- 交替最小二乘法
频繁模式挖掘
-
FP-增长
-
前缀 Span
ML 管道
Spark MLlib 的早期版本只包含一个基于 RDD 的 API。基于数据框架的 API 现在是 Spark 的主要 API。一旦基于数据帧的 API 达到特性对等,基于 RDD 的 API 在 Spark 2.3 中将被弃用。 x 基于 RDD 的 API 将在 Spark 3.0 中被移除。基于 DataFrames 的 API 通过提供更高级别的抽象来表示类似于关系数据库表的表格数据,使转换功能变得容易,这使它成为实现管道的自然选择。
Spark MLlib API 引入了几个创建机器学习管道的概念。图 2-3 显示了一个用于处理文本数据的简单 Spark MLlib 管道。记号赋予器将文本分解成一个单词包,将单词附加到输出数据帧上。词频–
逆文档频率(TF –
IDF)将数据帧作为输入,将单词包转换为特征向量,并将它们添加到第三个数据帧中。
图 2-3
一个简单的 Spark MLlib 流水线
管道
流水线是创建机器学习工作流的一系列相连的阶段。一个阶段可以是一个转换器或估计器。
变压器
转换器将一个数据帧作为输入,并输出一个新的数据帧,其中附加了附加列。新数据帧包括来自输入数据帧的列和附加列。
估计量
估计器是一种机器学习算法,可以根据训练数据拟合模型。估计器接受训练数据并产生机器学习模型。
ParamGridBuilder
ParamGridBuilder 用于构建参数网格。CrossValidator 执行网格搜索,并用参数网格中用户指定的超参数组合来训练模型。
交叉验证器
CrossValidator 交叉评估拟合的机器学习模型,并通过尝试用用户指定的超参数组合拟合底层估计器来输出最佳模型。使用 CrossValidator 或 TrainValidationSplit 估计器进行模型选择。
求值程序
评估者计算你的机器学习模型的性能。它输出精度和召回率等指标来衡量拟合模型的表现。赋值器的示例包括分别用于二进制和多类分类任务的 BinaryClassificationEvaluator 和 multiclasclassificationevaluator,以及用于回归任务的 RegressionEvaluator。
特征提取、变换和选择
大多数情况下,在使用原始数据拟合模型之前,需要进行额外的预处理。例如,基于距离的算法要求特征标准化。当分类数据被一键编码时,一些算法执行得更好。文本数据通常需要标记化和特征矢量化。对于非常大的数据集,可能需要降维。Spark MLlib 包含了一个针对这些任务类型的转换器和估算器的大集合。我将讨论 Spark MLlib 中一些最常用的变压器和估算器。
StringIndexer
大多数机器学习算法不能直接处理字符串,需要数据为数字格式。StringIndexer 是一个将标签的字符串列转换为索引的估计器。它支持四种不同的方法来生成索引:alphabetDesc、alphabetAsc、frequencyDesc 和 frequencyAsc。默认值设置为 frequencyDesc,最频繁的标签设置为 0,结果按标签频率降序排序。
import org.apache.spark.ml.feature.StringIndexer
val df = spark.createDataFrame(
Seq((0, "car"), (1, "car"), (2, "truck"), (3, "van"), (4, "van"), (5, "van"))
).toDF("id", "class")
df.show
+---+-----+
| id|class|
+---+-----+
| 0| car|
| 1| car|
| 2|truck|
| 3| van|
| 4| van|
| 5| van|
+---+-----+
val model = new StringIndexer()
.setInputCol("class")
.setOutputCol("classIndex")
val indexer = model.fit(df)
val indexed = indexer.transform(df)
indexed.show()
+---+-----+----------+
| id|class|classIndex|
+---+-----+----------+
| 0| car| 1.0|
| 1| car| 1.0|
| 2|truck| 2.0|
| 3| van| 0.0|
| 4| van| 0.0|
| 5| van| 0.0|
+---+-----+----------+
Tokenizer
当分析文本数据时,通常有必要将句子分成单独的术语或单词。记号赋予器正是这样做的。您可以使用 RegexTokenizer 的正则表达式执行更高级的标记化。标记化通常是机器学习 NLP 流水线的第一步。我将在第四章更详细地讨论自然语言处理(NLP)。
import org.apache.spark.ml.feature.Tokenizer
val df = spark.createDataFrame(Seq(
(0, "Mark gave a speech last night in Laguna Beach"),
(1, "Oranges are full of nutrients and low in calories"),
(2, "Eddie Van Halen is amazing")
)).toDF("id", "sentence")
df.show(false)
+---+-------------------------------------------------+
|id |sentence |
+---+-------------------------------------------------+
|0 |Mark gave a speech last night in Laguna Beach |
|1 |Oranges are full of nutrients and low in calories|
|2 |Eddie Van Halen is amazing |
+---+-------------------------------------------------+
val tokenizer = new Tokenizer().setInputCol("sentence").setOutputCol("words")
val tokenized = tokenizer.transform(df)
tokenized.show(false)
+---+-------------------------------------------------+
|id |sentence |
+---+-------------------------------------------------+
|0 |Mark gave a speech last night in Laguna Beach |
|1 |Oranges are full of nutrients and low in calories|
|2 |Eddie Van Halen is amazing |
+---+-------------------------------------------------+
+-----------------------------------------------------------+
|words |
+-----------------------------------------------------------+
|[mark, gave, a, speech, last, night, in, laguna, beach] |
|[oranges, are, full, of, nutrients, and, low, in, calories]|
|[eddie, van, halen, is, amazing] |
+-----------------------------------------------------------+
向量汇编器
Spark MLlib 算法要求将要素存储在单个向量列中。通常,训练数据会以表格格式出现,数据存储在单独的列中。VectorAssembler 是一个转换器,它将一组列合并成一个向量列。
import org.apache.spark.ml.feature.VectorAssembler
val df = spark.createDataFrame(
Seq((0, 50000, 7, 1))
).toDF("id", "income", "employment_length", "marital_status")
val assembler = new VectorAssembler()
.setInputCols(Array("income", "employment_length", "marital_status"))
.setOutputCol("features")
val df2 = assembler.transform(df)
df2.show(false)
+---+------+-----------------+--------------+-----------------+
|id |income|employment_length|marital_status|features |
+---+------+-----------------+--------------+-----------------+
|0 |50000 |7 |1 |[50000.0,7.0,1.0]|
+---+------+-----------------+--------------+-----------------+
标准鞋匠
正如在第一章中所讨论的,一些机器学习算法需要将特征规范化才能正常工作。StandardScaler 是一种将要素归一化为单位标准差和/或零均值的估计器。它接受两个参数:withst 和 withMean。用将特征缩放到单位标准偏差。默认情况下,该参数设置为 true。将【带平均值的 设置为“真”,则在缩放之前,数据以平均值为中心。默认情况下,该参数设置为 false。
import org.apache.spark.ml.feature.StandardScaler
import org.apache.spark.ml.feature.VectorAssembler
val df = spark.createDataFrame(
Seq((0, 186, 200, 56),(1, 170, 198, 42))
).toDF("id", "height", "weight", "age")
val assembler = new VectorAssembler()
.setInputCols(Array("height", "weight", "age"))
.setOutputCol("features")
val df2 = assembler.transform(df)
df2.show(false)
+---+------+------+---+------------------+
|id |height|weight|age|features |
+---+------+------+---+------------------+
|0 |186 |200 |56 |[186.0,200.0,56.0]|
|1 |170 |198 |42 |[170.0,198.0,42.0]|
+---+------+------+---+------------------+
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithStd(true)
.setWithMean(false)
val model = scaler.fit(df2)
val scaledData = model.transform(df2)
scaledData.select("features","scaledFeatures").show(false)
+------------------+------------------------------------------------------+
|features |scaledFeatures |
+------------------+------------------------------------------------------+
|[186.0,200.0,56.0]|[16.440232662587228,141.42135623730948,5.656854249492]|
|[170.0,198.0,42.0]|[15.026019100214134,140.0071426749364,4.2426406871192]|
+------------------+------------------------------------------------------+
用于重定数据比例的其他转换器包括 Normalizer、MinMaxScaler 和 MaxAbsScaler。请查看 Apache Spark 在线文档以了解更多详细信息。
停用词去除器
常用于文本分析,从字符串序列中删除停用词。停用词,如 I、the 和 a,对文档的意义没有太大贡献。
import org.apache.spark.ml.feature.StopWordsRemover
val remover = new StopWordsRemover().setInputCol("data").setOutputCol("output")
val dataSet = spark.createDataFrame(Seq(
(0, Seq("She", "is", "a", "cute", "baby")),
(1, Seq("Bob", "never", "went", "to", "Seattle"))
)).toDF("id", "data")
val df = remover.transform(dataSet)
df.show(false)
+---+-------------------------------+---------------------------+
|id |data |output |
+---+-------------------------------+---------------------------+
|0 |[She, is, a, cute, baby] |[cute, baby] |
|1 |[Bob, never, went, to, Seattle]|[Bob, never, went, Seattle]|
+---+-------------------------------+---------------------------+
n-克
当执行文本分析时,将术语组合成 n 元语法(文档中术语的组合)有时是有利的。创建 n 元语法有助于从文档中提取更有意义的信息。例如,“San”和“Diego”这两个词本身没有什么意义,但是将它们组合成一个词“San Diego”可以提供更多的上下文信息。我们将在第四章的后面使用 n-gram。
import org.apache.spark.ml.feature.NGram
val df = spark.createDataFrame(Seq(
(0, Array("Los", "Angeles", "Lobos", "San", "Francisco")),
(1, Array("Stand", "Book", "Case", "Phone", "Mobile", "Magazine")),
(2, Array("Deep", "Learning", "Machine", "Algorithm", "Pizza"))
)).toDF("id", "words")
val ngram = new NGram().setN(2).setInputCol("words").setOutputCol("ngrams")
val df2 = ngram.transform(df)
df2.select("ngrams").show(false)
+---------------------------------------------------------------------+
|ngrams |
+---------------------------------------------------------------------+
|[Los Angeles, Angeles Lobos, Lobos San, San Francisco] |
|[Stand Book, Book Case, Case Phone, Phone Mobile, Mobile Magazine] |
|[Deep Learning, Learning Machine, Machine Algorithm, Algorithm Pizza]|
+---------------------------------------------------------------------+
onehotencoderestomator 口腔癌
独热编码将分类特征转换成二进制向量,该向量最多具有单个一值,表示所有特征集合中特定特征值的存在。一键编码分类变量是逻辑回归、支持向量机等很多机器学习算法的要求。OneHotEncoderEstimator 可以转换多个列,为每个输入列生成一个独热编码的向量列。
import org.apache.spark.ml.feature.StringIndexer
val df = spark.createDataFrame(
Seq((0, "Male"), (1, "Male"), (2, "Female"), (3, "Female"), (4, "Female"), (5, "Male"))
).toDF("id", "gender")
df.show()
+---+------+
| id|gender|
+---+------+
| 0| Male|
| 1| Male|
| 2|Female|
| 3|Female|
| 4|Female|
| 5| Male|
+---+------+
val indexer = new StringIndexer()
.setInputCol("gender")
.setOutputCol("genderIndex")
val indexed = indexer.fit(df).transform(df)
indexed.show()
+---+------+-----------+
| id|gender|genderIndex|
+---+------+-----------+
| 0| Male| 1.0|
| 1| Male| 1.0|
| 2|Female| 0.0|
| 3|Female| 0.0|
| 4|Female| 0.0|
| 5| Male| 1.0|
+---+------+-----------+
import org.apache.spark.ml.feature.OneHotEncoderEstimator
val encoder = new OneHotEncoderEstimator()
.setInputCols(Array("genderIndex"))
.setOutputCols(Array("genderEnc"))
val encoded = encoder.fit(indexed).transform(indexed)
encoded.show()
+---+------+-----------+-------------+
| id|gender|genderIndex| genderEnc|
+---+------+-----------+-------------+
| 0| Male| 1.0| (1,[],[])|
| 1| Male| 1.0| (1,[],[])|
| 2|Female| 0.0|(1,[0],[1.0])|
| 3|Female| 0.0|(1,[0],[1.0])|
| 4|Female| 0.0|(1,[0],[1.0])|
| 5| Male| 1.0| (1,[],[])|
+---+------+-----------+-------------+
SQL 转换器
SQLTransformer 允许您使用 SQL 执行数据转换。虚拟表“THIS”对应于输入数据集。
import org.apache.spark.ml.feature.SQLTransformer
val df = spark.createDataFrame(
Seq((0, 5.2, 6.7), (2, 25.5, 8.9))).toDF("id", "col1", "col2")
val transformer = new SQLTransformer().setStatement("SELECT ABS(col1 - col2) as c1, MOD(col1, col2) as c2 FROM __THIS__")
val df2 = transformer.transform(df)
df2.show()
+----+-----------------+
| c1| c2|
+----+-----------------+
| 1.5| 5.2|
|16.6|7.699999999999999|
+----+-----------------+
术语频率–逆文档频率(TF–IDF)
TF –
IDF 或词频–
逆文档频率是文本分析中常用的一种特征矢量化方法。它经常被用来表示一个术语或单词对语料库中的文档的重要性。转换器 HashingTF 使用特征散列将术语转换成特征向量。估计器 IDF 对 HashingTF(或 CountVectorizer)生成的向量进行缩放。我将在第四章更详细地讨论 TF –
IDF。
import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
val df = spark.createDataFrame(Seq(
(0, "Kawhi Leonard is the league MVP"),
(1, "Caravaggio pioneered the Baroque technique"),
(2, "Using Apache Spark is cool")
)).toDF("label", "sentence")
df.show(false)
+-----+------------------------------------------+
|label|sentence |
+-----+------------------------------------------+
|0 |Kawhi Leonard is the league MVP |
|1 |Caravaggio pioneered the Baroque technique|
|2 |Using Apache Spark is cool |
+-----+------------------------------------------+
val tokenizer = new Tokenizer()
.setInputCol("sentence")
.setOutputCol("words")
val df2 = tokenizer.transform(df)
df2.select("label","words").show(false)
+-----+------------------------------------------------+
|label|words |
+-----+------------------------------------------------+
|0 |[kawhi, leonard, is, the, league, mvp] |
|1 |[caravaggio, pioneered, the, baroque, technique]|
|2 |[using, apache, spark, is, cool] |
+-----+------------------------------------------------+
val hashingTF = new HashingTF()
.setInputCol("words")
.setOutputCol("features")
.setNumFeatures(20)
val df3 = hashingTF.transform(df2)
df3.select("label","features").show(false)
+-----+-----------------------------------------------+
|label|features |
+-----+-----------------------------------------------+
|0 |(20,[1,4,6,10,11,18],[1.0,1.0,1.0,1.0,1.0,1.0])|
|1 |(20,[1,5,10,12],[1.0,1.0,2.0,1.0]) |
|2 |(20,[1,4,5,15],[1.0,1.0,1.0,2.0]) |
+-----+-----------------------------------------------+
val idf = new IDF()
.setInputCol("features")
.setOutputCol("scaledFeatures")
val idfModel = idf.fit(df3)
val df4 = idfModel.transform(df3)
df4.select("label", "scaledFeatures").show(3,50)
+-----+--------------------------------------------------+
|label| scaledFeatures|
+-----+--------------------------------------------------+
| 0|(20,[1,4,6,10,11,18],[0.0,0.28768207245178085,0...|
| 1|(20,[1,5,10,12],[0.0,0.28768207245178085,0.5753...|
| 2|(20,[1,4,5,15],0.0,0.28768207245178085,0.28768...|
+-----+--------------------------------------------------+
主成分分析
主成分分析(PCA)是一种降维技术,它将相关特征组合成一组较小的线性不相关特征,称为主成分。PCA 在图像识别和异常检测等多个领域都有应用。我将在第 [4 章更详细地讨论 PCA。
import org.apache.spark.ml.feature.PCA
import org.apache.spark.ml.linalg.Vectors
val data = Array(
Vectors.dense(4.2, 5.4, 8.9, 6.7, 9.1),
Vectors.dense(3.3, 8.2, 7.0, 9.0, 7.2),
Vectors.dense(6.1, 1.4, 2.2, 4.3, 2.9)
)
val df = spark.createDataFrame(data.map(Tuple1.apply)).toDF("features")
val pca = new PCA()
.setInputCol("features")
.setOutputCol("pcaFeatures")
.setK(2)
.fit(df)
val result = pca.transform(df).select("pcaFeatures")
result.show(false)
+---------------------------------------+
|pcaFeatures |
+---------------------------------------+
|[13.62324332562565,3.1399510055159445] |
|[14.130156836243236,-1.432033103462711]|
|[3.4900743524527704,0.6866090886347056]|
+---------------------------------------+
卡方选择器
ChiSqSelector 使用卡方独立性检验进行特征选择。卡方检验是一种检验两个分类变量之间关系的方法。 numTopFeatures 是默认的选择方法。它返回一组基于卡方检验的特征,或最具预测影响的特征。其他选择方法包括百分位数、fpr、fdr 和 fwe 。
import org.apache.spark.ml.feature.ChiSqSelector
import org.apache.spark.ml.linalg.Vectors
val data = Seq(
(0, Vectors.dense(5.1, 2.9, 5.6, 4.8), 0.0),
(1, Vectors.dense(7.3, 8.1, 45.2, 7.6), 1.0),
(2, Vectors.dense(8.2, 12.6, 19.5, 9.21), 1.0)
)
val df = spark.createDataset(data).toDF("id", "features", "class")
val selector = new ChiSqSelector()
.setNumTopFeatures(1)
.setFeaturesCol("features")
.setLabelCol("class")
.setOutputCol("selectedFeatures")
val df2 = selector.fit(df).transform(df)
df2.show()
+---+--------------------+-----+----------------+
| id| features|class|selectedFeatures|
+---+--------------------+-----+----------------+
| 0| [5.1,2.9,5.6,4.8]| 0.0| [5.1]|
| 1| [7.3,8.1,45.2,7.6]| 1.0| [7.3]|
| 2|[8.2,12.6,19.5,9.21]| 1.0| [8.2]|
+---+--------------------+-----+----------------+
相互关系
相关性评估两个变量之间线性关系的强度。对于线性问题,您可以使用相关性来选择相关要素(要素类相关性)和识别冗余要素(要素内相关性)。Spark MLlib 支持皮尔逊和斯皮尔曼的相关性。在下面的示例中,correlation 计算输入向量的相关矩阵。
import org.apache.spark.ml.linalg.{Matrix, Vectors}
import org.apache.spark.ml.stat.Correlation
import org.apache.spark.sql.Row
val data = Seq(
Vectors.dense(5.1, 7.0, 9.0, 6.0),
Vectors.dense(3.2, 1.1, 6.0, 9.0),
Vectors.dense(3.5, 4.2, 9.1, 3.0),
Vectors.dense(9.1, 2.6, 7.2, 1.8)
)
val df = data.map(Tuple1.apply).toDF("features")
+-----------------+
| features|
+-----------------+
|[5.1,7.0,9.0,6.0]|
|[3.2,1.1,6.0,9.0]|
|[3.5,4.2,9.1,3.0]|
|[9.1,2.6,7.2,1.8]|
+-----------------+
val Row(c1: Matrix) = Correlation.corr(df, "features").head
c1: org.apache.spark.ml.linalg.Matrix =
1.0 -0.01325851107237613 -0.08794286922175912 -0.6536434849076798
-0.01325851107237613 1.0 0.8773748081826724 -0.1872850762579899
-0.08794286922175912 0.8773748081826724 1.0 -0.46050932066780714
-0.6536434849076798 -0.1872850762579899 -0.46050932066780714 1.0
val Row(c2: Matrix) = Correlation.corr(df, "features", "spearman").head
c2: org.apache.spark.ml.linalg.Matrix =
1.0 0.399999999999999 0.19999999999999898 -0.8000000000000014
0.399999999999999 1.0 0.8000000000000035 -0.19999999999999743
0.19999999999999898 0.8000000000000035 1.0 -0.39999999999999486
-0.8000000000000014 -0.19999999999999743 -0.39999999999999486 1.0
还可以计算 DataFrame 列中存储的值的相关性,如下所示。
dataDF.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
dataDF.stat.corr("petal_length","label")
res48: Double = 0.9490425448523336
dataDF.stat.corr("petal_width","label")
res49: Double = 0.9564638238016178
dataDF.stat.corr("sepal_length","label")
res50: Double = 0.7825612318100821
dataDF.stat.corr("sepal_width","label")
res51: Double = -0.41944620026002677
评估指标
如第一章所述,精确度、召回率和准确度是评估模型性能的重要评估指标。然而,它们可能并不总是某些问题的最佳度量。
受试者工作特性下的面积(AUROC)
接受者操作特征下的面积(AUROC)是用于评估二元分类器的常见性能度量。受试者工作特性(ROC)是绘制真阳性率与假阳性率的图表。曲线下面积(AUC)是 ROC 曲线下的面积。AUC 可以解释为模型对随机正例的排序高于随机负例的概率。 xii 曲线下面积越大(AUROC 越接近 1.0),模型表现越好。AUROC 为 0.5 的模型是无用的,因为它的预测准确性与随机猜测一样好。
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator()
.setMetricName("areaUnderROC")
.setRawPredictionCol("rawPrediction")
.setLabelCol("label")
F1 度量
F1 测量值或 F1 分数是精确度和召回率的调和平均值或加权平均值。这是评估多类分类器的一个常见性能指标。当存在不均匀的阶级分布时,这也是一个很好的衡量标准。F1 成绩最好的是 1,最差的是 0。一个好的 F1 测量意味着你有很低的假阴性和假阳性。F1 度量的公式为:F1-Measure = 2∫(精度∫召回)/(精度+召回)。
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
val evaluator = new MulticlassClassificationEvaluator()
.setMetricName("f1")
.setLabelCol("label")
.setPredictionCol("prediction")
均方根误差(RMSE)
均方根误差(RMSE)是回归任务中最常见的指标。RMSE 就是均方误差(MSE)的平方根。MSE 表示回归线与一组数据点的接近程度,方法是将这些点到回归线的距离或“误差”取平方。XIIIMSE 越小,拟合越好。但是,MSE 与原始数据的单位不匹配,因为该值是平方的。RMSE 与输出的单位相同。
import org.apache.spark.ml.evaluation.RegressionEvaluator
val evaluator = new RegressionEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
.setMetricName("rmse")
我将在后续章节中介绍其他评估指标,如误差平方和(WSSSE)和轮廓系数。有关 Spark MLlib 支持的所有评估指标的完整列表,请参考 Spark 的在线文档。
模型持久性
Spark MLlib 允许您保存模型并在以后加载它们。如果您想要将您的模型与第三方应用程序集成,或者与团队的其他成员共享它们,这将特别有用。
保存单个随机森林模型
rf = RandomForestClassifier(numBin=10,numTrees=30)
model = rf.fit(training)
model.save("modelpath")
加载单个随机森林模型
val model2 = RandomForestClassificationModel.load("modelpath")
保存完整的管道
val pipeline = new Pipeline().setStages(Array(labelIndexer,vectorAssembler, rf))
val cv = new CrossValidator().setEstimator(pipeline)
val model = cv.fit(training)
model.save("modelpath")
加载完整的管道
val model2 = CrossValidatorModel.load("modelpath")
Spark MLlib 示例
让我们来看一个例子。我们将使用来自 UCI 机器学习知识库的心脏病数据集 xiv 来预测心脏病的存在。这些数据是由罗伯特·德特拉诺医学博士和他的团队在弗吉尼亚医学中心、长滩和克利夫兰诊所基金会收集的。历史上,克利夫兰数据集一直是众多研究的主题,因此我们将使用该数据集。原始数据集有 76 个属性,但其中只有 14 个用于 ML 研究(表 2-1 )。我们将进行二项式分类,确定患者是否患有心脏病(列表 2-2 )。
表 2-1
克利夫兰心脏病数据集属性信息
|属性
|
描述
|
| --- | --- |
| 年龄 | 年龄 |
| 性 | 性 |
| 丙酸纤维素 | 胸痛型 |
| treatbps | 静息血压 |
| 胆固醇 | 血清胆固醇(毫克/分升) |
| 前沿系统 | 空腹血糖> 120 毫克/分升 |
| 尊重 | 静息心电图结果 |
| 塔尔巴赫 | 达到最大心率 |
| 考试 | 运动诱发的心绞痛 |
| 旧峰 | 相对于静息运动诱发的 ST 段压低 |
| 倾斜 | 运动 ST 段峰值的斜率 |
| 大约 | 荧光镜染色的主要血管数量(0-3) |
| 塔尔 | 铊压力测试结果 |
| 数字 | 预测属性——心脏病的诊断 |
我们开始吧。下载文件,并将其复制到 HDFS。
wget http://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/cleveland.data
head -n 10 processed.cleveland.data
63.0,1.0,1.0,145.0,233.0,1.0,2.0,150.0,0.0,2.3,3.0,0.0,6.0,0
67.0,1.0,4.0,160.0,286.0,0.0,2.0,108.0,1.0,1.5,2.0,3.0,3.0,2
67.0,1.0,4.0,120.0,229.0,0.0,2.0,129.0,1.0,2.6,2.0,2.0,7.0,1
37.0,1.0,3.0,130.0,250.0,0.0,0.0,187.0,0.0,3.5,3.0,0.0,3.0,0
41.0,0.0,2.0,130.0,204.0,0.0,2.0,172.0,0.0,1.4,1.0,0.0,3.0,0
56.0,1.0,2.0,120.0,236.0,0.0,0.0,178.0,0.0,0.8,1.0,0.0,3.0,0
62.0,0.0,4.0,140.0,268.0,0.0,2.0,160.0,0.0,3.6,3.0,2.0,3.0,3
57.0,0.0,4.0,120.0,354.0,0.0,0.0,163.0,1.0,0.6,1.0,0.0,3.0,0
63.0,1.0,4.0,130.0,254.0,0.0,2.0,147.0,0.0,1.4,2.0,1.0,7.0,2
53.0,1.0,4.0,140.0,203.0,1.0,2.0,155.0,1.0,3.1,3.0,0.0,7.0,1
hadoop fs -put processed.cleveland.data /tmp/data
我们使用 spark-shell 来交互式地训练我们的模型。
spark-shell
val dataDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load(d("/tmp/data/processed.cleveland.data")
.toDF("id","age","sex","cp","trestbps","chol","fbs","restecg",
"thalach","exang","oldpeak","slope","ca","thal","num")
dataDF.printSchema
root
|-- id: string (nullable = false)
|-- age: float (nullable = true)
|-- sex: float (nullable = true)
|-- cp: float (nullable = true)
|-- trestbps: float (nullable = true)
|-- chol: float (nullable = true)
|-- fbs: float (nullable = true)
|-- restecg: float (nullable = true)
|-- thalach: float (nullable = true)
|-- exang: float (nullable = true)
|-- oldpeak: float (nullable = true)
|-- slope: float (nullable = true)
|-- ca: float (nullable = true)
|-- thal: float (nullable = true)
|-- num: float (nullable = true)
val myFeatures = Array("age", "sex", "cp", "trestbps", "chol", "fbs",
"restecg", "thalach", "exang", "oldpeak", "slope",
"ca", "thal", "num")
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(myFeatures)
.setOutputCol("features")
val dataDF2 = assembler.transform(dataDF)
import org.apache.spark.ml.feature.StringIndexer
val labelIndexer = new StringIndexer()
.setInputCol("num")
.setOutputCol("label")
val dataDF3 = labelIndexer.fit(dataDF2).transform(dataDF2)
val dataDF4 = dataDF3.where(dataDF3("ca").isNotNull)
.where(dataDF3("thal").isNotNull)
.where(dataDF3("num").isNotNull)
val Array(trainingData, testData) = dataDF4.randomSplit(Array(0.8, 0.2), 101)
import org.apache.spark.ml.classification.RandomForestClassifier
val rf = new RandomForestClassifier()
.setFeatureSubsetStrategy("auto")
.setSeed(101)
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator().setLabelCol("label")
import org.apache.spark.ml.tuning.ParamGridBuilder
val pgrid = new ParamGridBuilder()
.addGrid(rf.maxBins, Array(10, 20, 30))
.addGrid(rf.maxDepth, Array(5, 10, 15))
.addGrid(rf.numTrees, Array(20, 30, 40))
.addGrid(rf.impurity, Array("gini", "entropy"))
.build()
import org.apache.spark.ml.Pipeline
val pipeline = new Pipeline().setStages(Array(rf))
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(evaluator)
.setEstimatorParamMaps(pgrid)
.setNumFolds(3)
Listing 2-2Performing Binary Classification Using Random Forest
我们现在可以拟合模型了。
val model = cv.fit(trainingData)
对测试数据进行预测。
val prediction = model.transform(testData)
我们来评价一下模型。
import org.apache.spark.ml.param.ParamMap
val pm = ParamMap(evaluator.metricName -> "areaUnderROC")
val aucTestData = evaluator.evaluate(prediction, pm)
图形处理
Spark 包括一个名为 GraphX 的图形处理框架。有一个独立的包叫做 GraphFrames,它基于 DataFrames。GraphFrames 目前不是核心 Apache Spark 的一部分。在撰写本文时,GraphX 和 GraphFrames 仍在积极开发中。XV??【我盖 GraphX】第六章第六章。
超越 Spark MLlib:第三方机器学习集成
由于无数开源贡献者以及微软和谷歌等公司,Spark 可以访问第三方框架和库的丰富生态系统。虽然我涵盖了核心 Spark MLlib 算法,但这本书专注于更强大的下一代算法和框架,如 XGBoost、LightGBM、Isolation Forest、Spark NLP 和分布式深度学习。我将在接下来的章节中介绍它们。
使用 Alluxio 优化 Spark 和 Spark MLlib
Alluxio,原名 Tachyon,是加州大学伯克利分校 AMPLab 的一个开源项目。Alluxio 是一个以内存为中心的分布式存储系统,最初是由李皓原在 2012 年作为一个研究项目开发的,当时他是 AMPLab 的一名博士生和 Apache Spark 创始人。 xvi 该项目是 Berkeley 数据分析栈(BDAS)的存储层。2015 年,李创立了 Alluxio,Inc .以实现 Alluxio 的商业化,并获得了安德森·霍洛维茨(Andre essen Horowitz)750 万美元的现金注入。如今,Alluxio 拥有来自英特尔、IBM、雅虎和 Red Hat 等全球 50 个组织的 200 多名贡献者。几家知名公司目前正在生产中使用 Alluxio,如百度、阿里巴巴、Rackspace 和巴克莱。XVII
Alluxio 可用于优化 Spark 机器学习和深度学习工作负载,方法是将超快大数据存储到超大数据集。由 Alluxio 进行的深度学习基准测试显示,当从 Alluxio 而不是 S3 读取数据时,性能有显著提高。XVIII
体系结构
Alluxio 是一个以内存为中心的分布式存储系统,旨在成为大数据事实上的存储统一层。它提供了一个虚拟化层,统一了对不同存储引擎(如本地文件系统、HDFS、S3 和 NFS)和计算框架(如 Spark、MapReduce、Hive 和 Presto)的访问。图 2-4 给你一个 Alluxio 架构的概述。
图 2-4
Alluxio 架构概述
Alluxio 是协调数据共享和指导数据访问的中间层,同时为计算框架和大数据应用程序提供高性能低延迟的内存速度。Alluxio 与 Spark 和 Hadoop 无缝集成,只需要少量的配置更改。通过利用 Alluxio 的统一命名空间功能,应用程序只需连接到 Alluxio 即可访问存储在任何受支持的存储引擎中的数据。Alluxio 有自己的原生 API 以及 Hadoop 兼容的文件系统接口。便利类使用户能够执行最初为 Hadoop 编写的代码,而无需任何代码更改。REST API 提供了对其他语言的访问。我们将在本章的后面探讨 API。
Alluxio 的统一命名空间特性不支持关系数据库和 MPP 引擎,如 Redshift 或 Snowflake,也不支持文档数据库,如 MongoDB。当然,支持向 Alluxio 和上面提到的存储引擎写入数据。开发人员可以使用 Spark 等计算框架从红移表创建数据帧,并以 Parquet 或 CSV 格式存储在 Alluxio 文件系统中,反之亦然(图 2-5 )。
图 2-5
Alluxio 技术架构
为什么要用 Alluxio?
显著提高大数据处理性能和可扩展性
这些年来,内存变得越来越便宜,而其性能却变得越来越快。与此同时,硬盘驱动器的性能只是略有改善。毫无疑问,在内存中处理数据比在磁盘上处理数据快一个数量级。在几乎所有的编程范例中,我们都被建议在内存中缓存数据以提高性能。Apache Spark 优于 MapReduce 的一个主要优势是它能够缓存数据。Alluxio 将这一点提升到了一个新的水平,为大数据应用程序提供的不仅仅是一个缓存层,而是一个成熟的分布式高性能以内存为中心的存储系统。
百度正在运营世界上最大的 Alluxio 集群之一,1000 个工作节点处理超过 2PB 的数据。借助 Alluxio,百度在查询和处理时间方面的性能平均提高了 10 倍,最高可达 30 倍,显著提高了百度做出重要业务决策的能力。 xix 巴克莱发表文章描述了他们与 Alluxio 的经历。巴克莱数据科学家 Gianmario Spacagna 和高级分析主管 Harry Powell 能够使用 Alluxio 将他们的 Spark 工作从数小时调整到数秒。中国最大的旅游搜索引擎之一 Qunar.com 使用 Alluxio 后,性能提升了 15 到 300 倍。 xxi
多个框架和应用程序可以以内存速度共享数据
一个典型的大数据集群有多个会话运行不同的计算框架,如 Spark 和 MapReduce。对于 Spark,每个应用程序都有自己的执行器进程,执行器中的每个任务都运行在自己的 JVM 上,将 Spark 应用程序相互隔离。这意味着 Spark(和 MapReduce)应用程序无法共享数据,除了写入 HDFS 或 S3 等存储系统。如图 2-6 所示,Spark 作业和 MapReduce 作业使用存储在 HDFS 或 S3 的相同数据。在图 2-7 中,多个 Spark 作业使用相同的数据,每个作业在自己的堆空间中存储自己版本的数据。 xxii 不仅数据会重复,通过 HDFS 或 S3 共享数据也会很慢,尤其是当你共享大量数据时。
图 2-7
不同的工作通过 HDFS 或 S3 共享数据
图 2-6
不同的框架通过 HDFS 或 S3 共享数据
通过使用 Alluxio 作为堆外存储(图 2-8 ),多个框架和作业可以以内存速度共享数据,减少数据重复,提高吞吐量,减少延迟。
图 2-8
不同的作业和框架以内存速度共享数据
在应用程序终止或出现故障时提供高可用性和持久性
在 Spark 中,执行器进程和执行器内存驻留在同一个 JVM 中,所有缓存的数据都存储在 JVM 堆空间中(图 2-9 )。
图 2-9
Spark 作业有自己的堆内存
当作业完成或由于某种原因 JVM 由于运行时异常而崩溃时,所有缓存在堆空间中的数据都将丢失,如图 2-10 和 2-11 所示。
图 2-11
Spark 作业崩溃或完成。堆空间丢失
图 2-10
火花作业崩溃或完成
解决方案是使用 Alluxio 作为堆外存储(图 2-12 )。
图 2-12
Spark 使用 Alluxio 作为堆外存储
在这种情况下,即使 Spark JVM 崩溃,数据在 Alluxio 中仍然可用(图 2-13 和 2-14 )。
图 2-14
Spark 作业崩溃或完成。堆空间丢失。堆外内存仍然可用
图 2-13
火花作业崩溃或完成
优化整体内存使用并最大限度地减少垃圾收集
通过使用 Alluxio,内存使用效率大大提高,因为数据在作业和框架之间共享,并且因为数据存储在堆外,所以垃圾收集也被最小化,从而进一步提高了作业和应用程序的性能(图 2-15 )。
图 2-15
多个 Spark 和 MapReduce 作业可以访问存储在 Alluxio 中的相同数据
降低硬件要求
Alluxio 的大数据处理速度明显快于 HDFS 和 S3。IBM 的测试显示,在写入 io 方面,Alluxio 比 HDFS 快 110 倍。 xxiii 有了这样的性能,对额外硬件的需求就会减少,从而节省基础设施和许可成本。
阿帕奇 Spark 和 Alluxio
您在 Alluxio 中访问数据的方式类似于从 Spark 中访问存储在 HDFS 和 S3 的数据。
val dataRDD = sc.textFile("alluxio://localhost:19998/test01.csv")
val parsedRDD = dataRDD.map{_.split(",")}
case class CustomerData(userid: Long, city: String, state: String, age: Short)
val dataDF = parsedRDD.map{ a =>CustomerData(a(0).toLong, a(1).toString, a(2).toString, a(3).toShort) }.toDF
dataDF.show()
+------+---------------+-----+---+
|userid| city|state|age|
+------+---------------+-----+---+
| 300| Torrance| CA| 23|
| 302|Manhattan Beach| CA| 21|
+------+---------------+-----+---+
摘要
本章向您简要介绍了 Spark 和 Spark MLlib,足以让您掌握执行常见数据处理和机器学习任务所需的技能。我的目标是让你尽快熟悉情况。为了更彻底的治疗,比尔钱伯斯和马泰扎哈里亚(O'Reilly,2018)的《火花:权威指南》提供了对火花的全面介绍。Irfan Elahi (Apress,2019)的 Scala 编程用于大数据分析,Jason Swartz (O'Reilly,2014)的学习 Scala ,以及 Martin Odersky、Lex Spoon 和 Bill Venners (Artima,2016)的Scala 编程都是对 Scala 的很好介绍。我还介绍了 Alluxio,这是一个内存分布式计算平台,可用于优化大规模机器学习和深度学习工作负载。
参考
-
彼得·诺维格等人;《数据的不合理有效性》,googleuserconent.com,2009,
https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35179.pdf
-
火花;《星火总览》,spark.apache.org,2019,
https://spark.apache.org/docs/2.2.0/
-
阿帕奇软件基金会;“Apache 软件基金会宣布 Apache Spark 为顶级项目”,blogs.apache.org,2014,
https://blogs.apache.org/foundation/entry/the_apache_software_foundation_announces50
-
火花;《星火新闻》,spark.apache.org,2019,
https://spark.apache.org/news/
-
reddit“Matei Zaharia AMA,”reddit.com,2015 年,
-
数据块;《阿帕奇星火》,Databricks.com,2019,
https://databricks.com/spark/about
-
数据块;《如何在 Apache Spark 2.0 中使用 SparkSession》,Databricks.com,2016,
https://databricks.com/blog/2016/08/15/how-to-use-sparksession-in-apache-spark-2-0.html
-
Solr《利用索勒吉》,lucene.apache.org,2019,
https://lucene.apache.org/solr/guide/6_6/using-solrj.html
-
Lucidworks《Lucidworks Spark/Solr 集成》,github.com,2019,
https://github.com/lucidworks/spark-solr
-
火花;《机器学习库(MLlib)指南》,spark.apache.org,
http://spark.apache.org/docs/latest/ml-guide.html
-
火花;《OneHotEncoderEstimator》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-features#onehotencoderestimator
-
谷歌;“分类:ROC 曲线和 AUC”,developers.google.com,2019,
https://developers.google.com/machine-learning/crash-course/classification/roc-and-auc
-
斯蒂芬妮·格伦;《均方误差:定义与实例》,statisticshowto.datasciencecentral.com,2013,
www.statisticshowto.datasciencecentral.com/mean-squared-error/
-
安朵斯·雅诺西,威廉·施泰因布鲁恩,马蒂亚斯·普菲斯特勒,罗伯特·德特拉诺;《心脏病数据集》,archive.ics.uci.edu,1988,
http://archive.ics.uci.edu/ml/datasets/heart+Disease
-
火花;“GraphX”,spark.apache.org,2019,
https://spark.apache.org/graphx/
-
克里斯·马特曼;“孵化器的阿帕奇火花”,mail-archives.apache.org,2013 年,
[`http://mail-archives.apache.org/mod_mbox/incubator-general/201306.mbox/%3CCDD80F64.D5F9D%25chris.a.mattmann@jpl.nasa.gov%3E`](http://mail-archives.apache.org/mod_mbox/incubator-general/201306.mbox/%253CCDD80F64.D5F9D%2525chris.a.mattmann%2540jpl.nasa.gov%253E)
-
李皓原;“Alluxio,原名超光速粒子,随着 1.0 版本进入新时代,”alluxio.io,2016,
www.alluxio.com/blog/alluxio-formerly-tachyon-is-entering-a-new-era-with-10-release
-
傅;《用 Alluxio 实现深度学习的灵活快速存储》,alluxio.io,2018,
www.alluxio.io/blog/flexible-and-fast-storage-for-deep-learning-with-alluxio/
-
Alluxio“Alluxio 虚拟化分布式存储,以内存速度进行 Pb 级计算,”globenewswire.com,2016,
www.marketwired.com/press-release/alluxio-virtualizes-distributed-storage-petabyte-scale-computing-in-memory-speeds-2099053.html
-
亨利·鲍威尔和吉安马里奥·斯帕卡尼亚;“用超光速粒子让不可能成为可能:将火花工作从数小时加速到数秒,”dzone.com,2016,
https://dzone.com/articles/Accelerate-In-Memory-Processing-with-Spark-from-Hours-to-Seconds-With-Tachyon
-
李皓原;“Alluxio 在 Strata+Hadoop World Beijing 2016 上的主题演讲”,slideshare.net,2016,
www.slideshare.net/Alluxio/alluxio-keynote-at-stratahadoop-world-beijing-2016-65172341
-
费明·s。《用用例入门超光速粒子》,intel.com,2016,
https://software.intel.com/en-us/blogs/2016/02/04/getting-started-with-tachyon-by-use-cases
-
吉尔·韦尔尼克;“用于超快大数据处理的超光速粒子”,ibm.com,2015,
www.ibm.com/blogs/research/2015/08/tachyon-for-ultra-fast-big-data-processing/
三、监督学习
最可靠的知识是你自己构建的。
—朱迪亚珍珠 i
监督学习是一种使用训练数据集进行预测的机器学习任务。监督学习可以分为分类或回归。回归用于预测价格、温度或距离等连续值,而分类用于预测是或否、垃圾邮件或非垃圾邮件、恶性或良性等类别。
分类
分类可能是最常见的监督机器学习任务。您很可能已经遇到过在没有意识到的情况下利用分类的应用程序。常见的使用案例包括医疗诊断、定向营销、垃圾邮件检测、信用风险预测和情感分析等。有三种类型的分类任务。
二元分类
如果只有两个类别,则任务是二元或二项式分类。例如,当使用二进制分类算法进行垃圾邮件检测时,输出变量可以有两个类别:垃圾邮件或非垃圾邮件。为了检测癌症,分类可以是恶性的或良性的。对于有针对性的营销,预测某人购买诸如牛奶等物品的可能性,分类可以简单地是是或否。
多类分类
多类或多项分类任务有三个或更多类别。例如,要预测天气状况,您可能有五个类别:下雨、多云、晴天、下雪和刮风。为了扩展我们的目标营销示例,多类别分类可用于预测客户是否更有可能购买全脂牛奶、低脂牛奶、低脂牛奶或脱脂牛奶。
多标签分类
在多标签分类中,可以为每个观察值分配多个类别。相比之下,在多类别分类中,只能将一个类别分配给一个观察。使用我们的目标营销示例,多标签分类不仅用于预测客户是否更有可能购买牛奶,还用于预测其他商品,如饼干、黄油、热狗或面包。
Spark MLlib 分类算法
Spark MLlib 包括几个分类算法。我将讨论最流行的算法,并提供基于我们在第二章中所介绍的易于理解的代码示例。在本章的后面,我将讨论更高级的下一代算法,比如 XGBoost 和 LightGBM。
逻辑回归
逻辑回归是预测概率的线性分类器。它使用逻辑(sigmoid)函数将其输出转换为可以映射到两个(二元)类的概率值。通过多项式逻辑回归(softmax)支持多类分类。 ii 我们将在本章后面的一个例子中使用逻辑回归。
支持向量机
支持向量机是一种流行的算法,它通过寻找使两个类之间的间隔最大化的最佳超平面来工作,通过尽可能宽的间隙将数据点分成单独的类。最接近分类边界的数据点称为支持向量(图 3-1 )。
图 3-1
寻找最大化两个类之间的间隔的最优超平面【iii】
奈伊夫拜厄斯
朴素贝叶斯是一种基于贝叶斯定理的简单多类线性分类算法。朴素贝叶斯之所以得名,是因为它天真地假设数据集中的要素是独立的,忽略了要素之间任何可能的相关性。我们将在本章后面的情感分析示例中使用朴素贝叶斯。
多层感知器
多层感知器是一个前馈人工网络,由几个完全连接的节点层组成。输入图层中的节点对应于输入数据集。中间层中的节点使用逻辑(sigmoid)函数,而最终输出层中的节点使用 softmax 函数来支持多类分类。输出层中的节点数量必须与类的数量相匹配。 iv 我在第七章讨论多人感知机。
决策树
决策树通过学习从输入变量推断出的决策规则来预测输出变量的值。
从视觉上看,决策树看起来就像一棵倒置的树,根节点在顶部。每个内部节点都代表对一个属性的测试。叶节点代表一个类标签,而单个分支代表一个测试的结果。图 3-2 显示了一个预测信用风险的决策树。
图 3-2
预测信用风险的决策树
决策树执行特征空间的递归二元分裂。为了最大化信息增益,从一组可能的分裂中选择产生最大杂质减少的分裂。通过从父节点的杂质中减去子节点杂质的加权和来计算信息增益。子节点的杂质越低,信息增益越大。分裂继续进行,直到达到最大树深度(由 maxDepth 参数设置),不再获得大于 minInfoGain 的信息增益,或者 minInstancesPerNode 等于每个子节点产生的训练实例。
有两个用于分类的杂质度量(基尼杂质和熵)和一个用于回归的杂质度量(方差)。对于分类,Spark MLlib 中杂质的默认度量是基尼杂质。基尼系数是一个量化节点纯度的指标。如果基尼系数等于零(节点是纯的),则在一个节点内存在单个数据类别。如果基尼系数大于零,则意味着该节点包含属于不同类别的数据。
决策树很容易解释。与像逻辑回归这样的线性模型相比,决策树不需要特征缩放。它能够处理缺失的特征,并处理连续和分类特征。 v 独热编码分类特征 vi 在使用决策树和基于树的集成时不是必需的,事实上是不鼓励的。独热编码创建了不平衡的树,并且要求树生长得非常深以实现良好的预测性能。对于高基数分类特征来说尤其如此。
不利的一面是,决策树对数据中的噪声很敏感,有过度拟合的倾向。由于这种限制,决策树本身很少在现实生产环境中使用。如今,决策树是更强大的集成算法的基础模型,如随机森林和梯度提升树。
随机森林
随机森林是一种集成算法,它使用一组决策树进行分类和回归。它使用一种叫做 bagging (或 bootstrap aggregation)的方法来减少方差,同时保持低偏差。Bagging 从训练数据的子集训练单独的树。除了装袋,兰登森林还采用了另一种叫做的方法装袋。与 bagging(使用观测值的子集)相反,特征 bagging 使用特征(列)的子集。特征装袋旨在减少决策树之间的相关性。如果没有特征打包,单个树将会非常相似,尤其是在只有几个主要特征的情况下。
对于分类,单个树的输出或模式的多数投票成为模型的最终预测。对于回归,单棵树输出的平均值成为最终输出(图 3-3 )。Spark 并行训练几棵树,因为每棵树都是在随机森林中独立训练的。我将在本章后面更详细地讨论随机森林。
图 3-3
用于分类的随机森林
梯度提升树
梯度推进树(GBT)是另一种类似于随机森林的基于树的集成算法。gbt 使用一种称为 boosting to 的技术从弱学习者(浅树)中创建强学习者。GBTs 按顺序训练一组决策树 vii ,每一棵后继的树减少前一棵树的误差。这是通过使用前一个模型的残差来拟合下一个模型来完成的。 viii 该残差校正过程 ix 被执行设定的迭代次数,迭代次数由交叉验证确定,直到残差被完全最小化。
图 3-4
GBTs 中的决策树集成
图 3-4 显示了决策树集成在 GBTs 中是如何工作的。以我们的信用风险为例,个人根据其信用度被分为不同的类别。决策树中的每一片叶子都被分配了一个分数。将多个树的得分相加,得到最终的预测得分。例如,图 3-4 显示了第一个决策树给了这个女人 3 分。第二棵树给了她 2 分。把两个分数加在一起,这个女人的最终分数是 5 分。请注意,决策树是相互补充的。这是 GBTs 的主要原则之一。将分数与每个叶子相关联为 GBTs 提供了一种集成的优化方法。?? x
随机森林与梯度提升树
由于梯度提升树是按顺序训练的,因此通常认为它比随机森林慢,扩展性差,随机森林能够并行训练多棵树。然而,与随机森林相比,gbt 通常使用更浅的树,这意味着 gbt 可以更快地训练。
增加 GBTs 中的树的数量会增加过度拟合的机会(GBTs 通过利用更多的树来减少偏差),而增加随机森林中的树的数量会减少过度拟合的机会(随机森林通过利用更多的树来减少方差)。一般来说,在随机森林中添加更多的树可以提高性能,而当树的数量开始变得太大时,GBTs 的性能就会开始下降。正因为如此,GBTs 可能比随机森林更难调。
如果参数调整正确,梯度提升树通常被认为比随机森林更强大。GBTs 添加了新的决策树,补充了以前构建的决策树,与随机森林相比,使用更少的树可以获得更好的预测准确性。XII
近年来开发的大多数用于分类和回归的新算法,如 XGBoost 和 LightGBM,都是 GBTs 的改进版本。它们没有传统 gbt 的局限性。
第三方分类和回归算法
无数开源贡献者为 Spark 开发第三方机器学习算法投入了时间和精力。虽然它们不是核心 Spark MLlib 库的一部分,但 Databricks (XGBoost)和微软(LightGBM)等公司已经支持这些项目,并在世界各地广泛使用。XGBoost 和 LightGBM 目前被认为是用于分类和回归的下一代机器学习算法。在精度和速度至关重要的情况下,它们是首选算法。我将在本章后面讨论这两个问题。现在,让我们动手做一些事情,深入一些例子。
用逻辑回归进行多类分类
逻辑回归是预测概率的线性分类器。它因易于使用和训练速度快而受欢迎,经常用于二分类和多类分类。如图 3-5 的第一个图表所示,当您的数据具有清晰的决策边界时,逻辑回归等线性分类器是合适的。在类不是线性可分的情况下(如第二个图表所示),应该考虑非线性分类器,如基于树的集成。
图 3-5
线性与非线性分类问题
例子
我们将使用流行的 Iris 数据集(参见清单 3-1 )解决第一个例子中的多类分类问题。该数据集包含三个类,每个类 50 个实例,其中每个类涉及一种鸢尾植物(鸢尾、杂色鸢尾和海滨鸢尾)。从图 3-6 中可以看出,刚毛鸢尾与杂色鸢尾和海滨鸢尾是线性分离的,但是杂色鸢尾和海滨鸢尾彼此不是线性分离的。逻辑回归在数据集分类方面仍然做得不错。
图 3-6
虹膜数据集的主成分分析投影
我们的目标是在给定一组特征的情况下预测鸢尾植物的类型。数据集包含四个数字特征:萼片长度、萼片宽度、花瓣长度和花瓣宽度(均以厘米为单位)。
// Create a schema for our data.
import org.apache.spark.sql.types._
var irisSchema = StructType(Array (
StructField("sepal_length", DoubleType, true),
StructField("sepal_width", DoubleType, true),
StructField("petal_length", DoubleType, true),
StructField("petal_width", DoubleType, true),
StructField("class", StringType, true)
))
// Read the CSV file. Use the schema that we just defined.
val dataDF = spark.read.format("csv")
.option("header","false")
.schema(irisSchema)
.load("/files/iris.data")
// Check the schema.
dataDF.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
// Inspect the data to make sure they’re in the correct format.
dataDF.show
+------------+-----------+------------+-----------+-----------+
|sepal_length|sepal_width|petal_length|petal_width| class|
+------------+-----------+------------+-----------+-----------+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa|
+------------+-----------+------------+-----------+-----------+
only showing top 20 rows
// Calculate summary statistics for our data. This can
// be helpful in understanding the distribution of your data.
dataDF.describe().show(5,15)
+-------+---------------+---------------+---------------+---------------+
|summary| sepal_length| sepal_width| petal_length| petal_width|
+-------+---------------+---------------+---------------+---------------+
| count| 150| 150| 150| 150|
| mean|5.8433333333...|3.0540000000...|3.7586666666...|1.1986666666...|
| stddev|0.8280661279...|0.4335943113...|1.7644204199...|0.7631607417...|
| min| 4.3| 2.0| 1.0| 0.1|
| max| 7.9| 4.4| 6.9| 2.5|
+-------+---------------+---------------+---------------+---------------+
+--------------+
| class|
+--------------+
| 150|
| null|
| null|
| Iris-setosa|
|Iris-virginica|
+--------------+
// The input column class is currently a string. We'll use
// StringIndexer to encode it into a double. The new value
// will be stored in the new output column called label.
import org.apache.spark.ml.feature.StringIndexer
val labelIndexer = new StringIndexer()
.setInputCol("class")
.setOutputCol("label")
val dataDF2 = labelIndexer
.fit(dataDF)
.transform(dataDF)
// Check the schema of the new DataFrame.
dataDF2.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
// Inspect the new column added to the DataFrame.
dataDF2.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
only showing top 20 rows
// Combine the features into a single vector
// column using the VectorAssembler transformer.
import org.apache.spark.ml.feature.VectorAssembler
val features = Array("sepal_length","sepal_width","petal_length","petal_width")
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
val dataDF3 = assembler.transform(dataDF2)
// Inspect the new column added to the DataFrame.
dataDF3.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
// Inspect the new column added to the DataFrame.
dataDF3.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
+-----------------+
| features|
+-----------------+
|[5.1,3.5,1.4,0.2]|
|[4.9,3.0,1.4,0.2]|
|[4.7,3.2,1.3,0.2]|
|[4.6,3.1,1.5,0.2]|
|[5.0,3.6,1.4,0.2]|
|[5.4,3.9,1.7,0.4]|
|[4.6,3.4,1.4,0.3]|
|[5.0,3.4,1.5,0.2]|
|[4.4,2.9,1.4,0.2]|
|[4.9,3.1,1.5,0.1]|
|[5.4,3.7,1.5,0.2]|
|[4.8,3.4,1.6,0.2]|
|[4.8,3.0,1.4,0.1]|
|[4.3,3.0,1.1,0.1]|
|[5.8,4.0,1.2,0.2]|
|[5.7,4.4,1.5,0.4]|
|[5.4,3.9,1.3,0.4]|
|[5.1,3.5,1.4,0.3]|
|[5.7,3.8,1.7,0.3]|
|[5.1,3.8,1.5,0.3]|
+-----------------+
only showing top 20 rows
// Let's measure the statistical dependence between
// the features and the class using Pearson correlation.
dataDF3.stat.corr("petal_length","label")
res48: Double = 0.9490425448523336
dataDF3.stat.corr("petal_width","label")
res49: Double = 0.9564638238016178
dataDF3.stat.corr("sepal_length","label")
res50: Double = 0.7825612318100821
dataDF3.stat.corr("sepal_width","label")
res51: Double = -0.41944620026002677
// The petal_length and petal_width have extremely high class correlation, // while sepal_length and sepal_width have low class correlation.
// As discussed in Chapter 2, correlation evaluates how strong the linear // relationship between two variables. You can use correlation to select
// relevant features (feature-class correlation) and identify redundant
// features (intra-feature correlation).
// Divide our dataset into training and test datasets.
val seed = 1234
val Array(trainingData, testData) = dataDF3.randomSplit(Array(0.8, 0.2), seed)
// We can now fit a model on the training dataset
// using logistic regression.
import org.apache.spark.ml.classification.LogisticRegression
val lr = new LogisticRegression()
// Train a model using our training dataset.
val model = lr.fit(trainingData)
// Predict on our test dataset.
val predictions = model.transform(testData)
// Note the new columns added to the DataFrame:
// rawPrediction, probability, prediction.
predictions.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
|-- rawPrediction: vector (nullable = true)
|-- probability: vector (nullable = true)
|-- prediction: double (nullable = false)
// Inspect the predictions.
predictions.select("sepal_length","sepal_width",
"petal_length","petal_width","label","prediction").show
+------------+-----------+------------+-----------+-----+----------+
|sepal_length|sepal_width|petal_length|petal_width|label|prediction|
+------------+-----------+------------+-----------+-----+----------+
| 4.3| 3.0| 1.1| 0.1| 0.0| 0.0|
| 4.4| 2.9| 1.4| 0.2| 0.0| 0.0|
| 4.4| 3.0| 1.3| 0.2| 0.0| 0.0|
| 4.8| 3.1| 1.6| 0.2| 0.0| 0.0|
| 5.0| 3.3| 1.4| 0.2| 0.0| 0.0|
| 5.0| 3.4| 1.5| 0.2| 0.0| 0.0|
| 5.0| 3.6| 1.4| 0.2| 0.0| 0.0|
| 5.1| 3.4| 1.5| 0.2| 0.0| 0.0|
| 5.2| 2.7| 3.9| 1.4| 1.0| 1.0|
| 5.2| 4.1| 1.5| 0.1| 0.0| 0.0|
| 5.3| 3.7| 1.5| 0.2| 0.0| 0.0|
| 5.6| 2.9| 3.6| 1.3| 1.0| 1.0|
| 5.8| 2.8| 5.1| 2.4| 2.0| 2.0|
| 6.0| 2.2| 4.0| 1.0| 1.0| 1.0|
| 6.0| 2.9| 4.5| 1.5| 1.0| 1.0|
| 6.0| 3.4| 4.5| 1.6| 1.0| 1.0|
| 6.2| 2.8| 4.8| 1.8| 2.0| 2.0|
| 6.2| 2.9| 4.3| 1.3| 1.0| 1.0|
| 6.3| 2.8| 5.1| 1.5| 2.0| 1.0|
| 6.7| 3.1| 5.6| 2.4| 2.0| 2.0|
+------------+-----------+------------+-----------+-----+----------+
only showing top 20 rows
// Inspect the rawPrediction and probability columns.
predictions.select("rawPrediction","probability","prediction")
.show(false)
+------------------------------------------------------------+
|rawPrediction |
+------------------------------------------------------------+
|[-27765.164694901094,17727.78535517628,10037.379339724806] |
|[-24491.649758932126,13931.526474094646,10560.123284837473] |
|[20141.806983153703,1877.784589255676,-22019.591572409383] |
|[-46255.06332259462,20994.503038678085,25260.560283916537] |
|[25095.115980666546,110.99834659454791,-25206.114327261093] |
|[-41011.14350152455,17036.32945903473,23974.814042489823] |
|[20524.55747106708,1750.139974552606,-22274.697445619684] |
|[29601.783587714817,-1697.1845083924927,-27904.599079322325]|
|[38919.06696252647,-5453.963471106039,-33465.10349142042] |
|[-39965.27448934488,17725.41646382807,22239.85802551682] |
|[-18994.667253235268,12074.709651218403,6919.957602016859] |
|[-43236.84898013162,18023.80837865029,25213.040601481334] |
|[-31543.179893646557,16452.928101990834,15090.251791655724] |
|[-21666.087284218,13802.846783092147,7863.24050112584] |
|[-24107.97243292983,14585.93668397567,9522.035748954155] |
|[25629.52586174148,-192.40731255107312,-25437.11854919041] |
|[-14271.522512385294,11041.861803401871,3229.660708983418] |
|[-16548.06114507441,10139.917257827732,6408.143887246673] |
|[22598.60355651257,938.4220993796007,-23537.025655892172] |
|[-40984.78286289556,18297.704445848023,22687.078417047538] |
+------------------------------------------------------------+
+-------------+----------+
|probability |prediction|
+-------------+----------+
|[0.0,1.0,0.0]|1.0 |
|[0.0,1.0,0.0]|1.0 |
|[1.0,0.0,0.0]|0.0 |
|[0.0,0.0,1.0]|2.0 |
|[1.0,0.0,0.0]|0.0 |
|[0.0,0.0,1.0]|2.0 |
|[1.0,0.0,0.0]|0.0 |
|[1.0,0.0,0.0]|0.0 |
|[1.0,0.0,0.0]|0.0 |
|[0.0,0.0,1.0]|2.0 |
|[0.0,1.0,0.0]|1.0 |
|[0.0,1.0,0.0]|1.0 |
|[0.0,1.0,0.0]|1.0 |
|[1.0,0.0,0.0]|0.0 |
|[0.0,1.0,0.0]|1.0 |
|[0.0,1.0,0.0]|1.0 |
|[1.0,0.0,0.0]|0.0 |
|[0.0,0.0,1.0]|2.0 |
+-------------+----------+
only showing top 20 rows
// Evaluate the model. Several evaluation metrics are available
// for multiclass classification: f1 (default), accuracy,
// weightedPrecision, and weightedRecall.
// I discuss evaluation metrics in more detail in Chapter 2.
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
val evaluator = new MulticlassClassificationEvaluator().setMetricName("f1")
val f1 = evaluator.evaluate(predictions)
f1: Double = 0.958119658119658
val wp = evaluator.setMetricName("weightedPrecision").evaluate(predictions)
wp: Double = 0.9635416666666667
val wr = evaluator.setMetricName("weightedRecall").evaluate(predictions)
wr: Double = 0.9583333333333335
val accuracy = evaluator.setMetricName("accuracy").evaluate(predictions)
accuracy: Double = 0.9583333333333334
Listing 3-1Classification Using Logistic Regression
逻辑回归是一种流行的分类算法,由于其速度和简单性,经常被用作第一基线算法。对于生产使用,更高级的基于树的集成通常是首选,因为它们具有更高的准确性和捕捉数据集中复杂非线性关系的能力。
基于随机森林的流失预测
随机森林是一种强大的集成学习算法,建立在多个决策树作为基础模型上,每个决策树都在不同的数据自举子集上并行训练。如前所述,决策树容易过度拟合。随机森林通过使用一种称为bagging(bootstrap aggregation)的技术用随机选择的数据子集训练每个决策树来解决过度拟合问题。装袋减少了模型的方差,有助于避免过度拟合。随机森林在不增加偏差的情况下减少了模型的方差。它还执行特征打包,为每个决策树随机选择特征。特征打包的目标是减少单个树之间的相关性。
对于分类,最终类别通过多数投票决定。由各个决策树产生的类的模式(最频繁出现的)成为最终的类。对于回归,单个决策树输出的平均值成为模型的最终输出。
因为随机森林使用决策树作为它的基础模型,它继承了它的大部分特性。它能够处理连续和分类特征,并且不需要特征缩放和一次性编码。随机森林在不平衡数据上表现也很好,因为它的分层性质迫使它们同时处理这两类数据。最后,随机森林可以捕捉因变量和自变量之间的非线性关系。
由于其可解释性、准确性和灵活性,随机森林是最流行的基于树的分类和回归集成算法之一。然而,训练随机森林模型可能是计算密集型的(这使得它非常适合在 Hadoop 或 Spark 等多核和分布式环境中进行并行化)。与逻辑回归或朴素贝叶斯等线性模型相比,它需要更多的内存和计算资源。此外,随机森林往往在文本或基因组数据等高维数据上表现不佳。
Note
CSIRO 生物信息学团队开发了一个高度可扩展的随机森林实现,最初是为高维基因组数据设计的,称为 VariantSpark RF。XIIIVariantSpark RF 可以处理数百万个功能,并在基准测试中显示出 xiv 比 MLlib 的随机森林实现具有更高的可扩展性。关于 VariantSpark RF 的更多信息可以在 CSIRO 的生物信息学网站上找到。ReForeSt 是另一个高度可扩展的随机森林实现,由意大利热那亚大学 DIBRIS 的 SmartLab 研究实验室开发。 xv ReForeSt 可以处理数百万个特征,并支持随机森林旋转,这是一种新的集成方法,扩展了经典的随机森林算法。XVI
因素
随机森林相对容易调优。适当设置几个重要参数 xvii 往往就足以成功使用随机森林。
-
max_depth: 指定树的最大深度。为 max_depth 设置一个较高的值可以使模型更具表现力,但将其设置得太高可能会增加过度拟合的可能性,并使模型更复杂。
-
num_trees: 指定适合的树的数量。增加树的数量会减少方差,通常会提高准确性。增加树的数量会减慢训练的速度。在某个点之外添加更多的树可能不会提高准确性。
-
FeatureSubsetStrategy: 指定用于在每个节点进行分割的要素部分。设置该参数可以提高训练速度。
-
subsamplingRate: 指定将被选择用于训练每棵树的数据部分。设置此参数可以提高训练速度,并有助于防止过度拟合。将其设置得太低可能会导致拟合不足。
我提供了一些通用的指南,但是像往常一样,强烈建议执行参数网格搜索来确定这些参数的最佳值。有关随机森林参数的完整列表,请参考 Spark MLlib 的在线文档。
例子
客户流失预测是银行、保险公司、电信公司、有线电视运营商以及网飞、Hulu、Spotify 和 Apple Music 等流媒体服务的一个重要分类用例。能够预测更有可能取消订阅其服务的客户的公司可以实现更有效的客户保留策略。留住客户是有价值的。根据一家领先的客户参与分析公司的研究,客户流失每年给美国企业造成约 1360 亿美元的损失。贝恩公司所做的研究显示,客户保持率仅提高 5%,利润就会增加 25%到 95%。Lee Resource Inc .提供的另一项统计表明,吸引新客户的成本是留住现有客户的五倍。 xx
对于我们的例子,我们将使用来自加州大学欧文分校机器学习知识库的一个流行的电信客户流失数据集(参见清单 3-2 )。这是一个流行的 Kaggle 数据集 xxi ,在网上被广泛使用。 xxii
对于本书中的大多数示例,我将分别执行转换器和估算器(而不是在管道中指定它们),这样您就可以看到新列被添加到结果数据帧中。这将有助于您在研究示例时了解“幕后”发生了什么。
// Load the CSV file into a DataFrame.
val dataDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("churn_data.txt")
// Check the schema.
dataDF.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
// Select a few columns
.
dataDF
.select("state","phone_number","international_plan","total_day_minutes","churned").show
+-----+------------+------------------+-----------------+-------+
|state|phone_number|international_plan|total_day_minutes|churned|
+-----+------------+------------------+-----------------+-------+
| KS| 382-4657| no| 265.1| False|
| OH| 371-7191| no| 161.6| False|
| NJ| 358-1921| no| 243.4| False|
| OH| 375-9999| yes| 299.4| False|
| OK| 330-6626| yes| 166.7| False|
| AL| 391-8027| yes| 223.4| False|
| MA| 355-9993| no| 218.2| False|
| MO| 329-9001| yes| 157.0| False|
| LA| 335-4719| no| 184.5| False|
| WV| 330-8173| yes| 258.6| False|
| IN| 329-6603| no| 129.1| True|
| RI| 344-9403| no| 187.7| False|
| IA| 363-1107| no| 128.8| False|
| MT| 394-8006| no| 156.6| False|
| IA| 366-9238| no| 120.7| False|
| NY| 351-7269| no| 332.9| True|
| ID| 350-8884| no| 196.4| False|
| VT| 386-2923| no| 190.7| False|
| VA| 356-2992| no| 189.7| False|
| TX| 373-2782| no| 224.4| False|
+-----+------------+------------------+-----------------+-------+
only showing top 20 rows
import org.apache.spark.ml.feature.StringIndexer
// Convert the string column "churned" ("True", "False") to double (1,0).
val labelIndexer = new StringIndexer()
.setInputCol("churned")
.setOutputCol("label")
// Convert the string column "international_plan" ("yes", "no")
// to double 1,0.
val intPlanIndexer = new StringIndexer()
.setInputCol("international_plan")
.setOutputCol("int_plan")
// Let's select our features. Domain knowledge is essential in feature
// selection. I would think total_day_minutes and total_day_calls have
// some influence on customer churn. A significant drop in these two
// metrics might indicate that the customer does not need the service
// any longer and may be on the verge of cancelling their phone plan.
// However, I don't think phone_number, area_code, and state have any
// predictive qualities at all. We discuss feature selection later in
// this chapter.
val features = Array("number_customer_service_calls","total_day_minutes","total_eve_minutes","account_length","number_vmail_messages","total_day_calls","total_day_charge","total_eve_calls","total_eve_charge","total_night_calls","total_intl_calls","total_intl_charge","int_plan")
// Combine a given list of columns into a single vector column
// including all the features needed to train ML models.
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
// Add the label column to the DataFrame.
val dataDF2 = labelIndexer
.fit(dataDF)
.transform(dataDF)
dataDF2.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
|-- label: double (nullable = false)
// "True" was converted to 1 and "False" was converted to 0.
dataDF2.select("churned","label").show
+-------+-----+
|churned|label|
+-------+-----+
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| True| 1.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| True| 1.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
| False| 0.0|
+-------+-----+
only showing top 20 rows
// Add the int_plan column to the DataFrame.
val dataDF3 = intPlanIndexer.fit(dataDF2).transform(dataDF2)
dataDF3.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
|-- label: double (nullable = false)
|-- int_plan: double (nullable = false)
dataDF3.select("international_plan","int_plan").show
+------------------+--------+
|international_plan|int_plan|
+------------------+--------+
| no| 0.0|
| no| 0.0|
| no| 0.0|
| yes| 1.0|
| yes| 1.0|
| yes| 1.0|
| no| 0.0|
| yes| 1.0|
| no| 0.0|
| yes| 1.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
| no| 0.0|
+------------------+--------+
only showing top 20 rows
// Add the features vector column to the DataFrame.
val dataDF4 = assembler.transform(dataDF3)
dataDF4.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
|-- label: double (nullable = false)
|-- int_plan: double (nullable = false)
|-- features: vector (nullable = true)
// The features have been vectorized.
dataDF4.select("features").show(false)
+----------------------------------------------------------------------+
|features |
+----------------------------------------------------------------------+
|[1.0,265.1,197.4,128.0,25.0,110.0,45.07,99.0,16.78,91.0,3.0,2.7,0.0] |
|[1.0,161.6,195.5,107.0,26.0,123.0,27.47,103.0,16.62,103.0,3.0,3.7,0.0]|
|[0.0,243.4,121.2,137.0,0.0,114.0,41.38,110.0,10.3,104.0,5.0,3.29,0.0] |
|[2.0,299.4,61.9,84.0,0.0,71.0,50.9,88.0,5.26,89.0,7.0,1.78,1.0] |
|[3.0,166.7,148.3,75.0,0.0,113.0,28.34,122.0,12.61,121.0,3.0,2.73,1.0] |
|[0.0,223.4,220.6,118.0,0.0,98.0,37.98,101.0,18.75,118.0,6.0,1.7,1.0] |
|[3.0,218.2,348.5,121.0,24.0,88.0,37.09,108.0,29.62,118.0,7.0,2.03,0.0]|
|[0.0,157.0,103.1,147.0,0.0,79.0,26.69,94.0,8.76,96.0,6.0,1.92,1.0] |
|[1.0,184.5,351.6,117.0,0.0,97.0,31.37,80.0,29.89,90.0,4.0,2.35,0.0] |
|[0.0,258.6,222.0,141.0,37.0,84.0,43.96,111.0,18.87,97.0,5.0,3.02,1.0] |
|[4.0,129.1,228.5,65.0,0.0,137.0,21.95,83.0,19.42,111.0,6.0,3.43,0.0] |
|[0.0,187.7,163.4,74.0,0.0,127.0,31.91,148.0,13.89,94.0,5.0,2.46,0.0] |
|[1.0,128.8,104.9,168.0,0.0,96.0,21.9,71.0,8.92,128.0,2.0,3.02,0.0] |
|[3.0,156.6,247.6,95.0,0.0,88.0,26.62,75.0,21.05,115.0,5.0,3.32,0.0] |
|[4.0,120.7,307.2,62.0,0.0,70.0,20.52,76.0,26.11,99.0,6.0,3.54,0.0] |
|[4.0,332.9,317.8,161.0,0.0,67.0,56.59,97.0,27.01,128.0,9.0,1.46,0.0] |
|[1.0,196.4,280.9,85.0,27.0,139.0,33.39,90.0,23.88,75.0,4.0,3.73,0.0] |
|[3.0,190.7,218.2,93.0,0.0,114.0,32.42,111.0,18.55,121.0,3.0,2.19,0.0] |
|[1.0,189.7,212.8,76.0,33.0,66.0,32.25,65.0,18.09,108.0,5.0,2.7,0.0] |
|[1.0,224.4,159.5,73.0,0.0,90.0,38.15,88.0,13.56,74.0,2.0,3.51,0.0] |
+----------------------------------------------------------------------+
only showing top 20 rows
// Split the data into training and test data.
val seed = 1234
val Array(trainingData, testData) = dataDF4.randomSplit(Array(0.8, 0.2), seed)
trainingData.count
res13: Long = 4009
testData.count
res14: Long = 991
// Create a Random Forest classifier.
import org.apache.spark.ml.classification.RandomForestClassifier
val rf = new RandomForestClassifier()
.setFeatureSubsetStrategy("auto")
.setSeed(seed)
// Create a binary classification evaluator, and set label column to
// be used for evaluation.
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator().setLabelCol("label")
// Create a parameter grid.
import org.apache.spark.ml.tuning.ParamGridBuilder
val paramGrid = new ParamGridBuilder()
.addGrid(rf.maxBins, Array(10, 20,30))
.addGrid(rf.maxDepth, Array(5, 10, 15))
.addGrid(rf.numTrees, Array(3, 5, 100))
.addGrid(rf.impurity, Array("gini", "entropy"))
.build()
// Create a pipeline.
import org.apache.spark.ml.Pipeline
val pipeline = new Pipeline().setStages(Array(rf))
// Create a cross-validator.
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(3)
// We can now fit the model using the training dataset, choosing the
// best set of parameters for the model.
val model = cv.fit(trainingData)
// You can now make some predictions on our test data.
val predictions = model.transform(testData)
// Evaluate the model.
import org.apache.spark.ml.param.ParamMap
val pmap = ParamMap(evaluator.metricName -> "areaUnderROC")
val auc = evaluator.evaluate(predictions, pmap)
auc: Double = 0.9270599683335483
// Our Random Forest classifier has a high AUC score. The test
// data consists of 991 observations. 92 customers are predicted
// to leave the service.
predictions.count
res25: Long = 991
predictions.filter("prediction=1").count
res26: Long = 92
println(s"True Negative: ${predictions.select("*").where("prediction = 0 AND label = 0").count()} True Positive: ${predictions.select("*").where("prediction = 1 AND label = 1").count()}")
True Negative: 837 True Positive: 81
// Our test predicted 81 customers leaving who actually did leave and also
// predicted 837 customers not leaving who actually did not leave.
println(s"False Negative: ${predictions.select("*").where("prediction = 0 AND label = 1").count()} False Positive: ${predictions.select("*").where("prediction = 1 AND label = 0").count()}")
False Negative: 62 False Positive: 11
// Our test predicted 11 customers leaving who actually did not leave and
// also predicted 62 customers not leaving who actually did leave.
// You can sort the output by RawPrediction or Probability to target
// highest-probability customers. RawPrediction and Probability
// provide a measure of confidence for each prediction. The larger
// the value, the more confident the model is in its prediction.
predictions.select("phone_number","RawPrediction","prediction")
.orderBy($"RawPrediction".asc)
.show(false)
+------------+--------------------------------------+----------+
|phone_number|RawPrediction |prediction|
+------------+--------------------------------------+----------+
| 366-1084 |[15.038138063913935,84.96186193608602]|1.0 |
| 334-6519 |[15.072688486480072,84.9273115135199] |1.0 |
| 359-5574 |[15.276260309388752,84.72373969061123]|1.0 |
| 399-7865 |[15.429722388653014,84.57027761134698]|1.0 |
| 335-2967 |[16.465107279664032,83.53489272033593]|1.0 |
| 345-9140 |[16.53288465159445,83.46711534840551] |1.0 |
| 342-6864 |[16.694165016887318,83.30583498311265]|1.0 |
| 419-1863 |[17.594670105674677,82.4053298943253] |1.0 |
| 384-7176 |[17.92764148018115,82.07235851981882] |1.0 |
| 357-1938 |[18.8550074623437,81.1449925376563] |1.0 |
| 355-6837 |[19.556608109022648,80.44339189097732]|1.0 |
| 417-1488 |[20.13305147603522,79.86694852396475] |1.0 |
| 394-5489 |[21.05074084178182,78.94925915821818] |1.0 |
| 394-7447 |[21.376663858426735,78.62333614157326]|1.0 |
| 339-6477 |[21.549262081786424,78.45073791821355]|1.0 |
| 406-7844 |[21.92209788389343,78.07790211610656] |1.0 |
| 372-4073 |[22.098599119168263,77.90140088083176]|1.0 |
| 404-4809 |[22.515513847987147,77.48448615201283]|1.0 |
| 347-8659 |[22.66840460762997,77.33159539237005] |1.0 |
| 335-1874 |[23.336632598761128,76.66336740123884]|1.0 |
+------------+--------------------------------------+----------+
only showing top 20 rows
Listing 3-2Churn Prediction Using Random Forest
特征重要性
随机森林(和其他基于树的集合)具有内置的特征选择功能,可用于测量数据集中每个特征的重要性(参见清单 3-3 )。
随机森林将要素重要性计算为每次选择要素分割节点时聚合的每棵树上每个节点的节点杂质减少量之和除以森林中的树数。Spark MLlib 提供了一种方法,可以返回每个特性的重要性估计值。
import org.apache.spark.ml.classification.RandomForestClassificationModel
import org.apache.spark.ml.PipelineModel
val bestModel = model.bestModel
val model = bestModel
.asInstanceOf[PipelineModel]
.stages
.last
.asInstanceOf[RandomForestClassificationModel]
model.featureImportances
feature_importances: org.apache.spark.ml.linalg.Vector =
(13,[0,1,2,3,4,5,6,7,8,9,10,11,12],
[
0.20827010117447803,
0.1667170878866465,
0.06099491253318444,
0.008184141410796346,
0.06664053647245761,
0.0072108752126555,
0.21097011684691344,
0.006902059667276019,
0.06831916361401609,
0.00644772968425685,
0.04105403721675372,
0.056954219262186724,
0.09133501901837866])
Listing 3-3Showing Feature Importance with Random Forest
我们得到一个向量,包含特征的数量(在我们的例子中是 13)、特征的数组索引和相应的权重。表 3-1 以可读性更强的格式显示了输出,实际特征以相应的权重显示。如您所见,total_day_charge、total_day_minutes 和 number_customer_service_calls 是最重要的功能。有道理。大量客户服务呼叫可能表明服务多次中断或大量客户投诉。低 total_day_minutes 和 total_day_charge 可能表明客户不经常使用他的电话计划,这可能意味着他准备很快取消他的计划。
表 3-1
电信客户流失预测示例的功能重要性
|索引
|
特征
|
特征重要性
|
| --- | --- | --- |
| Zero | 数量 _ 客户 _ 服务 _ 呼叫 | 0.20827010117447803 |
| one | 总计 _ 天 _ 分钟 | 0.1667170878866465 |
| Two | 总计 _ eve _ 分钟 | 0.06099491253318444 |
| three | 帐户 _ 长度 | 0.008184141410796346 |
| four | 数字邮件消息 | 0.06664053647245761 |
| five | 总计 _ 天 _ 次呼叫 | 0.0072108752126555 |
| six | 总计 _ 天 _ 费用 | 0.21097011684691344 |
| seven | 总通话次数 | 0.006902059667276019 |
| eight | 总费用 | 0.06831916361401609 |
| nine | 夜间通话总数 | 0.00644772968425685 |
| Ten | 总呼叫次数 | 0.04105403721675372 |
| Eleven | total_intl_charge | 0.056954219262186724 |
| Twelve | int_plan | 0.09133501901837866 |
Note
Spark MLlib 在随机森林中实现的特征重要性也称为基于基尼的重要性或杂质平均减少(MDI)。随机森林的一些实现利用不同的方法来计算特征重要性,这种方法被称为基于精度的重要性或平均精度下降(MDA)。 xxiii 基于准确度的重要性是基于特征被随机置换时预测准确度的降低来计算的。虽然 Spark MLlib 的随机森林实现不直接支持这种方法,但通过评估模型同时一次置换每个特性的一列值,手动实现这种方法相当简单。
有时检查最佳模型使用的参数是有用的(参见清单 3-4 )。
import org.apache.spark.ml.classification.RandomForestClassificationModel
import org.apache.spark.ml.PipelineModel
val bestModel = model
.bestModel
.asInstanceOf[PipelineModel]
.stages
.last
.asInstanceOf[RandomForestClassificationModel]
print(bestModel.extractParamMap)
{
rfc_81c4d3786152-cacheNodeIds: false,
rfc_81c4d3786152-checkpointInterval: 10,
rfc_81c4d3786152-featureSubsetStrategy: auto,
rfc_81c4d3786152-featuresCol: features,
rfc_81c4d3786152-impurity: gini,
rfc_81c4d3786152-labelCol: label,
rfc_81c4d3786152-maxBins: 10,
rfc_81c4d3786152-maxDepth: 15,
rfc_81c4d3786152-maxMemoryInMB: 256,
rfc_81c4d3786152-minInfoGain: 0.0,
rfc_81c4d3786152-minInstancesPerNode: 1,
rfc_81c4d3786152-numTrees: 100,
rfc_81c4d3786152-predictionCol: prediction,
rfc_81c4d3786152-probabilityCol: probability,
rfc_81c4d3786152-rawPredictionCol: rawPrediction,
rfc_81c4d3786152-seed: 1234,
rfc_81c4d3786152-subsamplingRate: 1.0
}
Listing 3-4Extracting the Parameters of the Random Forest Model
XGBoost4J-Spark 的极限梯度升压
梯度推进算法是用于分类和回归的一些最强大的机器学习算法。目前有各种梯度提升算法的实现。流行的实现包括 AdaBoost 和 CatBoost(Yandex 最近开源的梯度提升库)。Spark MLlib 还包括自己的梯度提升树(GBT)实现。
XGBoost(极限梯度提升)是目前可用的最好的梯度提升树实现之一。XGBoost 于 2014 年 3 月 27 日由陈天琦发布,作为一个研究项目,它已经成为分类和回归的主流机器学习算法。为提高效率和可伸缩性而设计,其并行树提升能力使其比其他基于树的集成算法快得多。由于准确率高,XGBoost 通过赢得多个机器学习比赛而获得了知名度。2015 年,Kaggle 上的 29 个获奖解决方案中有 17 个使用了 XGBoost。2015 年 KDD 杯前 10 名的解决方案全部使用了 XGBoost。
XGBoost 是使用梯度推进的一般原则设计的,将弱学习者组合成强学习者。但是,虽然梯度提升树是按顺序构建的——慢慢地从数据中学习,以在后续迭代中改进其预测,但 XGBoost 是并行构建树的。XGBoost 通过其内置的正则化来控制模型复杂性和减少过拟合,从而产生更好的预测性能。当查找连续特征的最佳分割点时,它使用近似算法来查找分割点。XXV
近似分割方法使用离散箱来存储连续要素,从而显著加快模型训练速度。XGBoost 包括另一种使用基于直方图的算法的树生长方法,该方法提供了一种将连续要素分入离散箱的更有效的方法。但是,虽然近似方法每次迭代都创建一组新的面元,但是基于直方图的方法在多次迭代中重复使用面元。
这种方法允许使用近似方法无法实现的额外优化,例如缓存二进制文件以及父直方图和兄弟直方图相减的能力。 xxvi 为了优化排序操作,XGBoost 将排序后的数据存储在内存块单元中。排序块可以由并行 CPU 核心高效地分配和执行。XGBoost 可以通过其加权分位数草图算法有效地处理加权数据,可以有效地处理稀疏数据,支持缓存,并通过为大型数据集利用磁盘空间来支持核外计算,因此数据不必放在内存中。
XGBoost4J-Spark 项目于 2016 年末启动,将 XGBoost 移植到 Spark。XGBoost4J-Spark 利用了 Spark 高度可扩展的分布式处理引擎,并与 Spark MLlib 的数据帧/数据集抽象完全兼容。XGBoost4J-Spark 可以无缝嵌入到 Spark MLlib 管道中,并与 Spark MLlib 的变压器和估算器集成。
Note
XGBoost4J-Spark 需要 Apache Spark 2.4+。建议直接从 http://spark.apache.org
安装 Spark。XGBoost4J-Spark 不能保证与其他供应商的第三方 Spark 发行版(如 Cloudera、Hortonworks 或 MapR)一起使用。有关更多信息,请参考供应商的文档。 二十七
因素
XGBoost 比 Random Forest 有更多的参数,通常需要更多的调整。最初关注最重要的参数可以帮助您开始使用 XGBoost。随着你对算法越来越熟悉,你可以学习剩下的部分。
-
max_depth: 指定树的最大深度。为 max_depth 设置较高的值可能会增加过度拟合的可能性,并使模型更加复杂。
-
n _ estimates:指定要拟合的树的数量。一般来说,价值越大越好。将此参数设置得太高可能会影响训练速度。在某一点之外添加更多的树可能不会提高精度。默认值设置为 100。 二十八
-
sub_sample: 指定将为每棵树选择的数据部分。设置此参数可以提高训练速度,并有助于防止过度拟合。将其设置得太低可能会导致拟合不足。
-
colsample_bytree: 指定将为每棵树随机选择的列的分数。设置此参数可以提高训练速度,并有助于防止过度拟合。相关参数包括 colsample_bylevel 和 colsample_bynode。
-
目标:指定学习任务和学习目标。为该参数设置正确的值很重要,以避免不可预测的结果或不良的准确性。XGBClassifier 默认为二进制:逻辑进行二进制分类,而 XGBRegressor 默认为 reg:squarederror 。其他值包括用于多类分类的 multi:softmax 和multi:soft prob; rank:pairwise,rank:ndcg,rank:map 进行排名;和生存:cox 使用 cox 比例风险模型进行生存回归,仅举几例。
-
learning _ rate(eta):learning _ rate 作为收缩因子,在每一个 boosting 步骤后减少特征权重,目的是减缓学习速率。该参数用于控制过度拟合。较低的值需要更多的树。
-
n_jobs: 指定 XGBoost 使用的并行线程的数量(如果 n_thread 被弃用,则使用该参数)。
这些只是关于如何使用参数的一般准则。强烈建议执行参数网格搜索来确定这些参数的最佳值。有关 XGBoost 参数的完整列表,请参考 XGBoost 的在线文档。
Note
为了与 Scala 的变量命名约定保持一致,XGBoost4J-Spark 既支持默认的参数集,也支持这些参数的 camel case 变体(例如,max_depth 和 maxDepth)。
例子
我们将重用相同的电信客户流失数据集和前面随机森林示例中的大部分代码(参见清单 3-5 )。这一次,我们将使用管道将转换器和估算器连接在一起。
// XGBoost4J-Spark is available as an external package.
// Start spark-shell. Specify the XGBoost4J-Spark package.
spark-shell --packages ml.dmlc:xgboost4j-spark:0.81
// Load the CSV file into a DataFrame.
val dataDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("churn_data.txt")
// Check the schema.
dataDF.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
// Select a few columns.
dataDF.select("state","phone_number","international_plan","churned").show
+-----+------------+------------------+-------+
|state|phone_number|international_plan|churned|
+-----+------------+------------------+-------+
| KS| 382-4657| no| False|
| OH| 371-7191| no| False|
| NJ| 358-1921| no| False|
| OH| 375-9999| yes| False|
| OK| 330-6626| yes| False|
| AL| 391-8027| yes| False|
| MA| 355-9993| no| False|
| MO| 329-9001| yes| False|
| LA| 335-4719| no| False|
| WV| 330-8173| yes| False|
| IN| 329-6603| no| True|
| RI| 344-9403| no| False|
| IA| 363-1107| no| False|
| MT| 394-8006| no| False|
| IA| 366-9238| no| False|
| NY| 351-7269| no| True|
| ID| 350-8884| no| False|
| VT| 386-2923| no| False|
| VA| 356-2992| no| False|
| TX| 373-2782| no| False|
+-----+------------+------------------+-------+
only showing top 20 rows
import org.apache.spark.ml.feature.StringIndexer
// Convert the String "churned" column ("True", "False") to double(1,0).
val labelIndexer = new StringIndexer()
.setInputCol("churned")
.setOutputCol("label")
// Convert the String "international_plan" ("no", "yes") column to double(1,0).
val intPlanIndexer = new StringIndexer()
.setInputCol("international_plan")
.setOutputCol("int_plan")
// Specify features to be selected for model fitting.
val features = Array("number_customer_service_calls","total_day_minutes","total_eve_minutes","account_length","number_vmail_messages","total_day_calls","total_day_charge","total_eve_calls","total_eve_charge","total_night_calls","total_intl_calls","total_intl_charge","int_plan")
// Combines the features into a single vector column.
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
// Split the data into training and test data.
val seed = 1234
val Array(trainingData, testData) = dataDF.randomSplit(Array(0.8, 0.2), seed)
// Create an XGBoost classifier.
import ml.dmlc.xgboost4j.scala.spark.XGBoostClassifier
import ml.dmlc.xgboost4j.scala.spark.XGBoostClassificationModel
val xgb = new XGBoostClassifier()
.setFeaturesCol("features")
.setLabelCol("label")
// XGBClassifier's objective parameter defaults to binary:logistic which
// is the learning task and objective that we want for this example
// (binary classification). Depending on your task, remember to set the
// correct learning task and objective.
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
val evaluator = new MulticlassClassificationEvaluator().setLabelCol("label")
import org.apache.spark.ml.tuning.ParamGridBuilder
val paramGrid = new ParamGridBuilder()
.addGrid(xgb.maxDepth, Array(3, 8))
.addGrid(xgb.eta, Array(0.2, 0.6))
.build()
// This time we'll specify all the steps in the pipeline.
import org.apache.spark.ml.{ Pipeline, PipelineStage }
val pipeline = new Pipeline()
.setStages(Array(labelIndexer, intPlanIndexer, assembler, xgb))
// Create a cross-validator.
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(3)
// We can now fit the model using the training data. This will run
// cross-validation, choosing the best set of parameters.
val model = cv.fit(trainingData)
// You can now make some predictions on our test data.
val predictions = model.transform(testData)
predictions.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
|-- label: double (nullable = false)
|-- int_plan: double (nullable = false)
|-- features: vector (nullable = true)
|-- rawPrediction: vector (nullable = true)
|-- probability: vector (nullable = true)
|-- prediction: double (nullable = false)
// Let's evaluate the model.
val auc = evaluator.evaluate(predictions)
auc: Double = 0.9328044307445879
// The AUC score produced by XGBoost4J-Spark is slightly better compared
// to our previous Random Forest example. XGBoost4J-Spark was also
// faster than Random Forest in training this dataset.
// Like Random Forest, XGBoost lets you extract the feature importance.
import ml.dmlc.xgboost4j.scala.spark.XGBoostClassificationModel
import org.apache.spark.ml.PipelineModel
val bestModel = model.bestModel
val model = bestModel
.asInstanceOf[PipelineModel]
.stages
.last
.asInstanceOf[XGBoostClassificationModel]
// Execute the getFeatureScore method to extract the feature importance.
model.nativeBooster.getFeatureScore()
res9: scala.collection.mutable.Map[String,Integer] = Map(f7 -> 4, f9 -> 7, f10 -> 2, f12 -> 4, f11 -> 8, f0 -> 5, f1 -> 19, f2 -> 17, f3 -> 10, f4 -> 2, f5 -> 3)
// The method returns a map with the key mapping to the feature
// array index and the value corresponding to the feature importance score.
Listing 3-5Churn Prediction Using XGBoost4J-Spark
表 3-2
使用 XGBoost4J-Spark 的特性重要性
|索引
|
特征
|
特征重要性
|
| --- | --- | --- |
| Zero | 数量 _ 客户 _ 服务 _ 呼叫 | Two |
| one | 总计 _ 天 _ 分钟 | Fifteen |
| Two | 总计 _ eve _ 分钟 | Ten |
| three | 帐户 _ 长度 | three |
| four | 数字邮件消息 | Two |
| five | 总计 _ 天 _ 次呼叫 | three |
| six | 总计 _ 天 _ 费用 | 省略 |
| seven | 总通话次数 | Two |
| eight | 总费用 | 省略 |
| nine | 夜间通话总数 | Two |
| Ten | 总呼叫次数 | Two |
| Eleven | total_intl_charge | one |
| Twelve | int_plan | five |
请注意,输出缺少了几列,特别是 total_day_charge (f6)和 total_eve_charge (f8)。这些是 XGBoost 被认为在提高模型预测精度方面无效的特征(见表 3-2 )。只有在至少一次分割中使用的要素才能进入 XGBoost 要素重要性输出。有几种可能的解释。这可能意味着丢弃的要素具有非常低的方差或零方差。这也可能意味着这两个特征与其他特征高度相关。
将 XGBoost 的特性重要性输出与我们之前的随机森林示例进行比较时,有一些有趣的事情需要注意。请注意,虽然我们之前的随机森林模型认为 number_customer_service_calls 是最重要的特性之一,但 XGBoost 将其列为最不重要的特性之一。类似地,前面的随机森林模型认为 total_day_charge 是最重要的特性,但是 XGBoost 由于其缺乏重要性而完全将其从输出中忽略(参见清单 3-6 )。
val bestModel = model
.bestModel
.asInstanceOf[PipelineModel]
.stages
.last
.asInstanceOf[XGBoostClassificationModel]
print(bestModel.extractParamMap)
{
xgbc_9b95e70ab140-alpha: 0.0,
xgbc_9b95e70ab140-baseScore: 0.5,
xgbc_9b95e70ab140-checkpointInterval: -1,
xgbc_9b95e70ab140-checkpointPath: ,
xgbc_9b95e70ab140-colsampleBylevel: 1.0,
xgbc_9b95e70ab140-colsampleBytree: 1.0,
xgbc_9b95e70ab140-customEval: null,
xgbc_9b95e70ab140-customObj: null,
xgbc_9b95e70ab140-eta: 0.2,
xgbc_9b95e70ab140-evalMetric: error,
xgbc_9b95e70ab140-featuresCol: features,
xgbc_9b95e70ab140-gamma: 0.0,
xgbc_9b95e70ab140-growPolicy: depthwise,
xgbc_9b95e70ab140-labelCol: label,
xgbc_9b95e70ab140-lambda: 1.0,
xgbc_9b95e70ab140-lambdaBias: 0.0,
xgbc_9b95e70ab140-maxBin: 16,
xgbc_9b95e70ab140-maxDeltaStep: 0.0,
xgbc_9b95e70ab140-maxDepth: 8,
xgbc_9b95e70ab140-minChildWeight: 1.0,
xgbc_9b95e70ab140-missing: NaN,
xgbc_9b95e70ab140-normalizeType: tree,
xgbc_9b95e70ab140-nthread: 1,
xgbc_9b95e70ab140-numEarlyStoppingRounds: 0,
xgbc_9b95e70ab140-numRound: 1,
xgbc_9b95e70ab140-numWorkers: 1,
xgbc_9b95e70ab140-objective: reg:linear,
xgbc_9b95e70ab140-predictionCol: prediction,
xgbc_9b95e70ab140-probabilityCol: probability,
xgbc_9b95e70ab140-rateDrop: 0.0,
xgbc_9b95e70ab140-rawPredictionCol: rawPrediction,
xgbc_9b95e70ab140-sampleType: uniform,
xgbc_9b95e70ab140-scalePosWeight: 1.0,
xgbc_9b95e70ab140-seed: 0,
xgbc_9b95e70ab140-silent: 0,
xgbc_9b95e70ab140-sketchEps: 0.03,
xgbc_9b95e70ab140-skipDrop: 0.0,
xgbc_9b95e70ab140-subsample: 1.0,
xgbc_9b95e70ab140-timeoutRequestWorkers: 1800000,
xgbc_9b95e70ab140-trackerConf: TrackerConf(0,python),
xgbc_9b95e70ab140-trainTestRatio: 1.0,
xgbc_9b95e70ab140-treeLimit: 0,
xgbc_9b95e70ab140-treeMethod: auto,
xgbc_9b95e70ab140-useExternalMemory: false
}
Listing 3-6Extracting the Parameters of the XGBoost4J-Spark Model
LightGBM:微软的快速渐变提升
多年来,XGBoost 一直是每个人最喜欢的分类和回归算法。最近,LightGBM 成为了王位的新挑战者。它是一个相对较新的基于树的梯度提升变体,类似于 XGBoost。LightGBM 于 2016 年 10 月 17 日发布,是微软分布式机器学习工具包(DMTK)项目的一部分。它被设计成快速和分布式的,导致更快的训练速度和更低的内存使用。它支持 GPU 和并行学习以及处理大型数据集的能力。在几个基准测试和公共数据集上的实验中,LightGBM 显示出比 XGBoost 更快的速度和更好的准确性。
Note
LightGBM 作为微软 Apache Spark 机器学习(MMLSpark)生态系统的一部分被移植到 Spark。微软一直在积极开发与 Apache Spark 生态系统无缝集成的数据科学和深度学习工具,如微软认知工具包、OpenCV 和 LightGBM。MMLSpark 需要 Python 2.7 或 3.5+,Scala 2.11,Spark 2.3+。
与 XGBoost 相比,LightGBM 有几个优点。它利用直方图将连续特征分入离散的箱中。这为 LightGBM 提供了优于 XGBoost(默认情况下,XGBoost 使用基于预排序的算法进行树学习)的几个性能优势,例如减少了内存使用、减少了计算每次分割的增益的成本,以及减少了并行学习的通信成本。LightGBM 通过对其兄弟节点和父节点执行直方图减法来计算节点的直方图,从而实现了额外的性能提升。在线基准测试显示,在某些任务中,LightGBM 比 XGBoost(不含宁滨)快 11 到 15 倍。XXIX
LightGBM 通过逐叶生长树(最佳优先),在准确性方面通常优于 XGBoost。训练决策树有两种主要策略,层次式和叶式(如图 3-7 所示)。对于大多数基于树的集成(包括 XGBoost),逐层树生长是生长决策树的传统方式。LightGBM 引入了逐叶增长策略。与水平方向生长相比,叶方向生长通常收敛更快 xxx 并且损失更低。 xxxi
Note
逐叶增长往往会过度适应小数据集。建议在 LightGBM 中设置 max_depth 参数来限制树深度。请注意,即使设置了 max_depth,树仍然是逐叶生长的。 xxxii 我将在本章后面讨论 LightGBM 参数调优。
图 3-7
水平方向生长与叶方向生长
Note
此后,XGBoost 实现了许多由 LightGBM 首创的优化,包括逐叶树生长策略和使用直方图将连续特征存储到离散容器中。最新的性能指标评测显示,XGBoost 达到了与 LightGBM 相当的性能。XXXIII
因素
与其他算法(如随机森林)相比,调优 LightGBM 稍微复杂一些。LightGBM 使用逐叶(最佳优先)树生长算法,如果参数配置不正确,该算法很容易过度拟合。而且,LightGBM 有 100 多个参数。关注最重要的参数足以帮助您开始使用 LightGBM。随着你对算法越来越熟悉,你可以学习剩下的部分。
-
max_depth :设置该参数,防止树长得太深。浅树不太可能过度生长。如果数据集很小,设置此参数尤其重要。
-
num_leaves :控制树模型的复杂度。该值应小于 2^(max_depth)以防止过度拟合。将 num_leaves 设置为一个较大的值可以提高精度,但有较高的过度拟合风险。将 num_leaves 设置为较小的值有助于防止过度拟合。
-
min_data_in_leaf: 将该参数设置为较大的值可以防止树长得太深。这是另一个可以设置的参数,有助于控制过度拟合。将该值设置得太大会导致拟合不足。
-
max_bin: LightGBM 使用直方图将连续特征的值分组到离散桶中。设置 max_bin 以指定值将分组到的箱数。较小的值有助于控制过拟合并提高训练速度,而较大的值可提高精度。
-
feature_fraction: 该参数启用特征子采样。此参数指定在每次迭代中随机选择的要素比例。例如,将 feature_fraction 设置为 0.75 将在每次迭代中随机选择 75%的要素。设置此参数可以提高训练速度,并有助于防止过度拟合。
-
bagging_fraction: 指定在每次迭代中选择的数据的分数。例如,将 bagging_fraction 设置为 0.75 将在每次迭代中随机选择 75%的数据。设置此参数可以提高训练速度,并有助于防止过度拟合。
-
num_iteration: 设置增强迭代的次数。默认值为 100。对于多类分类,LightGBM 构建 num_class * num_iterations 树。设置该参数会影响训练速度。
-
目标:和 XGBoost 一样,LightGBM 支持多个目标。默认目标设置为回归。将该参数设置为 s 指定你的模型试图执行的任务类型。对于回归任务,选项有 regression_l2、regression_l1、poisson、quantile、mape、gamma、huber、fair 或 tweedie。对于分类任务,选项有二进制、多类或多类集。正确设置目标很重要,以避免不可预测的结果或不准确。
与往常一样,强烈建议执行参数网格搜索来确定这些参数的最佳值。有关 LightGBM 参数的详细列表,请参考 LightGBM 在线文档。
Note
在撰写本文时,LightGBM for Spark 还没有达到与 LightGBM for Python 同等的特性。虽然 LightGBM for Spark 包含了最重要的参数,但仍然缺少一些。您可以通过访问 https://bit.ly/2OqHl2M
获得 LightGBM for Spark 中所有可用参数的列表。可以和 https://bit.ly/30YGyaO
的 LightGBM 参数完整列表进行对比。
例子
我们将重用相同的电信客户流失数据集和前面的 Random Forest 和 XGBoost 示例中的大部分代码,如清单 3-7 所示。
spark-shell --packages Azure:mmlspark:0.15
// Load the CSV file into a DataFrame.
val dataDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema", "true")
.load("churn_data.txt")
// Check the schema.
dataDF.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
// Select a few columns.
dataDF.select("state","phone_number","international_plan","churned").show
+-----+------------+------------------+-------+
|state|phone_number|international_plan|churned|
+-----+------------+------------------+-------+
| KS| 382-4657| no| False|
| OH| 371-7191| no| False|
| NJ| 358-1921| no| False|
| OH| 375-9999| yes| False|
| OK| 330-6626| yes| False|
| AL| 391-8027| yes| False|
| MA| 355-9993| no| False|
| MO| 329-9001| yes| False|
| LA| 335-4719| no| False|
| WV| 330-8173| yes| False|
| IN| 329-6603| no| True|
| RI| 344-9403| no| False|
| IA| 363-1107| no| False|
| MT| 394-8006| no| False|
| IA| 366-9238| no| False|
| NY| 351-7269| no| True|
| ID| 350-8884| no| False|
| VT| 386-2923| no| False|
| VA| 356-2992| no| False|
| TX| 373-2782| no| False|
+-----+------------+------------------+-------+
only showing top 20 rows
import org.apache.spark.ml.feature.StringIndexer
val labelIndexer = new StringIndexer().setInputCol("churned").setOutputCol("label")
val intPlanIndexer = new StringIndexer().setInputCol("international_plan").setOutputCol("int_plan")
val features = Array("number_customer_service_calls","total_day_minutes","total_eve_minutes","account_length","number_vmail_messages","total_day_calls","total_day_charge","total_eve_calls","total_eve_charge","total_night_calls","total_intl_calls","total_intl_charge","int_plan")
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
val seed = 1234
val Array(trainingData, testData) = dataDF.randomSplit(Array(0.9, 0.1), seed)
// Create a LightGBM classifier.
import com.microsoft.ml.spark.LightGBMClassifier
val lightgbm = new LightGBMClassifier()
.setFeaturesCol("features")
.setLabelCol("label")
.setRawPredictionCol("rawPrediction")
.setObjective("binary")
// Remember to set the correct objective using the setObjective method.
// Specifying the incorrect objective can affect accuracy or produce
// unpredictable results. In LightGBM the default objective is set to
// regression. In this example, we are performing binary classification, so
// we set the objective to binary.
Listing 3-7Churn Prediction with LightGBM
Note
Spark 从 2.4 版本开始支持屏障执行模式。从 0.18 版开始,LightGBM 通过 setUseBarrierExecutionMode 方法支持屏障执行模式。
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator()
.setLabelCol("label")
.setMetricName("areaUnderROC")
import org.apache.spark.ml.tuning.ParamGridBuilder
val paramGrid = new ParamGridBuilder()
.addGrid(lightgbm.maxDepth, Array(2, 3, 4))
.addGrid(lightgbm.numLeaves, Array(4, 6, 8))
.addGrid(lightgbm.numIterations, Array(600))
.build()
import org.apache.spark.ml.{ Pipeline, PipelineStage }
val pipeline = new Pipeline()
.setStages(Array(labelIndexer, intPlanIndexer, assembler, lightgbm))
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(pipeline)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(3)
val model = cv.fit(trainingData)
// You can now make some predictions on our test data.
val predictions = model.transform(testData)
predictions.printSchema
root
|-- state: string (nullable = true)
|-- account_length: double (nullable = true)
|-- area_code: double (nullable = true)
|-- phone_number: string (nullable = true)
|-- international_plan: string (nullable = true)
|-- voice_mail_plan: string (nullable = true)
|-- number_vmail_messages: double (nullable = true)
|-- total_day_minutes: double (nullable = true)
|-- total_day_calls: double (nullable = true)
|-- total_day_charge: double (nullable = true)
|-- total_eve_minutes: double (nullable = true)
|-- total_eve_calls: double (nullable = true)
|-- total_eve_charge: double (nullable = true)
|-- total_night_minutes: double (nullable = true)
|-- total_night_calls: double (nullable = true)
|-- total_night_charge: double (nullable = true)
|-- total_intl_minutes: double (nullable = true)
|-- total_intl_calls: double (nullable = true)
|-- total_intl_charge: double (nullable = true)
|-- number_customer_service_calls: double (nullable = true)
|-- churned: string (nullable = true)
|-- label: double (nullable = false)
|-- int_plan: double (nullable = false)
|-- features: vector (nullable = true)
|-- rawPrediction: vector (nullable = true)
|-- probability: vector (nullable = true)
|-- prediction: double (nullable = false)
// Evaluate the model. The AUC score is higher than Random Forest
// and XGBoost from our previous examples.
val auc = evaluator.evaluate(predictions)
auc: Double = 0.940366124260358
//LightGBM also lets you extract the feature importance.
import com.microsoft.ml.spark.LightGBMClassificationModel
import org.apache.spark.ml.PipelineModel
val bestModel = model.bestModel
val model = bestModel.asInstanceOf[PipelineModel]
.stages
.last
.asInstanceOf[LightGBMClassificationModel]
LightGBM 中有两种类型的特征重要性,“分裂”(分裂总数)和“增益”(总信息增益)。通常建议使用“增益”,这与 Random Forest 计算特征重要性的方法大致相似,但在我们的二进制分类示例中,LightGBM 使用交叉熵(对数损失)而不是基尼杂质(见表 3-3 和 3-4 )。要最小化的损失取决于指定的目标。XXXIV
表 3-3
使用信息增益的 LightGBM 的特征重要性
|索引
|
特征
|
特征重要性
|
| --- | --- | --- |
| Zero | 数量 _ 客户 _ 服务 _ 呼叫 | 2648.0893859118223 |
| one | 总计 _ 天 _ 分钟 | 5339.0795262902975 |
| Two | 总计 _ eve _ 分钟 | 2191.832309693098 |
| three | 帐户 _ 长度 | 564.6461282968521 |
| four | 数字邮件消息 | 1180.4672759771347 |
| five | 总计 _ 天 _ 次呼叫 | 656.8244850635529 |
| six | 总计 _ 天 _ 费用 | Zero |
| seven | 总通话次数 | 533.6638155579567 |
| eight | 总费用 | 579.7435692846775 |
| nine | 夜间通话总数 | 651.5408382415771 |
| Ten | 总呼叫次数 | 1179.492751300335 |
| Eleven | total_intl_charge | 2186.5995585918427 |
| Twelve | int_plan | 1773.7864662855864 |
val gainFeatureImportances = model.getFeatureImportances("gain")
gainFeatureImportances: Array[Double] =
Array(2648.0893859118223, 5339.0795262902975, 2191.832309693098,564.6461282968521, 1180.4672759771347, 656.8244850635529, 0.0, 533.6638155579567, 579.7435692846775, 651.5408382415771, 1179.492751300335, 2186.5995585918427, 1773.7864662855864)
比较使用“split”时的输出。
val gainFeatureImportances = model.getFeatureImportances("split")
gainFeatureImportances: Array[Double] = Array(159.0, 583.0, 421.0, 259.0, 133.0, 264.0, 0.0, 214.0, 92.0, 279.0, 279.0, 366.0, 58.0)
表 3-4
使用拆分数量的 LightGBM 的特征重要性
|索引
|
特征
|
特征重要性
|
| --- | --- | --- |
| Zero | 数量 _ 客户 _ 服务 _ 呼叫 | One hundred and fifty-nine |
| one | 总计 _ 天 _ 分钟 | Five hundred and eighty-three |
| Two | 总计 _ eve _ 分钟 | Four hundred and twenty-one |
| three | 帐户 _ 长度 | Two hundred and fifty-nine |
| four | 数字邮件消息 | One hundred and thirty-three |
| five | 总计 _ 天 _ 次呼叫 | Two hundred and sixty-four |
| six | 总计 _ 天 _ 费用 | Zero |
| seven | 总通话次数 | Two hundred and fourteen |
| eight | 总费用 | Ninety-two |
| nine | 夜间通话总数 | Two hundred and seventy-nine |
| Ten | 总呼叫次数 | Two hundred and seventy-nine |
| Eleven | total_intl_charge | Three hundred and sixty-six |
| Twelve | int_plan | Fifty-eight |
println(s"True Negative: ${predictions.select("*").where("prediction = 0 AND label = 0").count()} True Positive: ${predictions.select("*").where("prediction = 1 AND label = 1").count()}")
True Negative: 407 True Positive: 58
println(s"False Negative: ${predictions.select("*").where("prediction = 0 AND label = 1").count()} False Positive: ${predictions.select("*").where("prediction = 1 AND label = 0").count()}")
False Negative: 20 False Positive: 9
基于朴素贝叶斯的情感分析
朴素贝叶斯是一种基于贝叶斯定理的简单多类线性分类算法。朴素贝叶斯之所以得名,是因为它天真地假设数据集中的要素是独立的,忽略了要素之间任何可能的相关性。现实世界的情况并非如此,朴素贝叶斯仍然表现良好,尤其是在小数据集或高维数据集上。像线性分类器一样,它在非线性分类问题上表现不佳。朴素贝叶斯是一种计算效率高且高度可伸缩的算法,只需要对数据集进行一次传递。对于使用大型数据集的分类任务,这是一个很好的基线模型。它的工作原理是在给定一组特征的情况下,找出一个点属于某个类的概率。贝叶斯定理方程可以表述为:
P(A|B)是后验概率可以解释为:“给定事件 B,事件 A 发生的概率是多少?”B 代表特征向量。分子代表条件概率乘以先验概率。分母代表证据。这个等式可以更精确地写成:
朴素贝叶斯常用于文本分类。文本分类的流行应用包括垃圾邮件检测和文档分类。另一个文本分类用例是情感分析。公司定期检查来自社交媒体的评论,以确定公众对产品或服务的意见是积极的还是消极的。对冲基金利用情绪分析来预测股市走势。
Spark MLlib 支持伯努利朴素贝叶斯和多项式朴素贝叶斯。伯努利朴素贝叶斯仅适用于布尔或二进制特征(例如,文档中存在或不存在单词),而多项式朴素贝叶斯是为离散特征(例如,单词计数)设计的。MLlib 的朴素贝叶斯实现的默认模型类型设置为多项式。可以为平滑设置另一个参数 lambda(默认值为 1.0)。
例子
让我们用一个例子来演示如何使用朴素贝叶斯进行情感分析。我们将使用来自加州大学欧文分校机器学习知识库的流行数据集。该数据集是为 Kotzias 等人的论文“使用深度特征从群体到个体标签”创建的。艾尔。,KDD 2015。数据集来自三家不同的公司:IMDB、亚马逊和 Yelp。每个公司有 500 个正面和 500 个负面评论。我们将使用来自亚马逊的数据集,根据亚马逊产品评论来确定特定产品的情绪是正面(1)还是负面(0)的概率。
我们需要将数据集中的每个句子转换成一个特征向量。Spark MLlib 为此提供了一个转换器。术语频率–
逆文档频率(TF –
IDF)通常用于从文本生成特征向量。TF –
IDF 用于通过计算单词在文档中出现的次数(TF)和单词在整个语料库中出现的频率(IDF)来确定单词与语料库中文档的相关性。在 Spark MLlib 中,TF 和 IDF 是分开实现的(HashingTF 和 IDF)。
在我们可以使用 TF –
IDF 将单词转换成特征向量之前,我们需要使用另一个转换器 tokenizer 将句子分割成单独的单词。这些步骤应该如图 3-8 所示,代码如清单 3-8 所示。
图 3-8
我们的情感分析示例的特征转换
// Start by creating a schema for our dataset.
import org.apache.spark.sql.types._
var reviewsSchema = StructType(Array (
StructField("text", StringType, true),
StructField("label", IntegerType, true)
))
// Create a DataFrame from the tab-delimited text file.
// Use the "csv" format regardless if its tab or comma delimited.
// The file does not have a header, so we’ll set the header
// option to false. We’ll set delimiter to tab and use the schema
// that we just built.
val reviewsDF = spark.read.format("csv")
.option("header", "false")
.option("delimiter","\t")
.schema(reviewsSchema)
.load("/files/amazon_cells_labelled.txt")
// Review the schema.
reviewsDF.printSchema
root
|-- text: string (nullable = true)
|-- label: integer (nullable = true)
// Check the data.
reviewsDF.show
+--------------------+-----+
| text|label|
+--------------------+-----+
|So there is no wa...| 0|
|Good case, Excell...| 1|
|Great for the jaw...| 1|
|Tied to charger f...| 0|
| The mic is great.| 1|
|I have to jiggle ...| 0|
|If you have sever...| 0|
|If you are Razr o...| 1|
|Needless to say, ...| 0|
|What a waste of m...| 0|
|And the sound qua...| 1|
|He was very impre...| 1|
|If the two were s...| 0|
|Very good quality...| 1|
|The design is ver...| 0|
|Highly recommend ...| 1|
|I advise EVERYONE...| 0|
| So Far So Good!.| 1|
| Works great!.| 1|
|It clicks into pl...| 0|
+--------------------+-----+
only showing top 20 rows
// Let's do some row counts.
reviewsDF.createOrReplaceTempView("reviews")
spark.sql("select label,count(*) from reviews group by label").show
+-----+--------+
|label|count(1)|
+-----+--------+
| 1| 500|
| 0| 500|
+-----+--------+
// Randomly divide the dataset into training and test datasets.
val seed = 1234
val Array(trainingData, testData) = reviewsDF.randomSplit(Array(0.8, 0.2), seed)
trainingData.count
res5: Long = 827
testData.count
res6: Long = 173
// Split the sentences into words.
import org.apache.spark.ml.feature.Tokenizer
val tokenizer = new Tokenizer().setInputCol("text")
.setOutputCol("words")
// Check the tokenized data.
val tokenizedDF = tokenizer.transform(trainingData)
tokenizedDF.show
+--------------------+-----+--------------------+
| text|label| words|
+--------------------+-----+--------------------+
| (It works!)| 1| [(it, works!)]|
|)Setup couldn't h...| 1|[)setup, couldn't...|
|* Comes with a st...| 1|[*, comes, with, ...|
|.... Item arrived...| 1|[...., item, arri...|
|1\. long lasting b...| 0|[1., long, lastin...|
|2 thumbs up to th...| 1|[2, thumbs, up, t...|
|:-)Oh, the charge...| 1|[:-)oh,, the, cha...|
| A Disappointment.| 0|[a, disappointment.]|
|A PIECE OF JUNK T...| 0|[a, piece, of, ju...|
|A good quality ba...| 1|[a, good, quality...|
|A must study for ...| 0|[a, must, study, ...|
|A pretty good pro...| 1|[a, pretty, good,...|
|A usable keyboard...| 1|[a, usable, keybo...|
|A week later afte...| 0|[a, week, later, ...|
|AFTER ARGUING WIT...| 0|[after, arguing, ...|
|AFter the first c...| 0|[after, the, firs...|
| AMAZON SUCKS.| 0| [amazon, sucks.]|
| Absolutel junk.| 0| [absolutel, junk.]|
| Absolutely great.| 1|[absolutely, great.]|
|Adapter does not ...| 0|[adapter, does, n...|
+--------------------+-----+--------------------+
only showing top 20 rows
// Next, we'll use HashingTF to convert the tokenized words
// into fixed-length feature vector.
import org.apache.spark.ml.feature.HashingTF
val htf = new HashingTF().setNumFeatures(1000)
.setInputCol("words")
.setOutputCol("features")
// Check the vectorized features.
val hashedDF = htf.transform(tokenizedDF)
hashedDF.show
+--------------------+-----+--------------------+--------------------+
| text|label| words| features|
+--------------------+-----+--------------------+--------------------+
| (It works!)| 1| [(it, works!)]|(1000,[369,504],[...|
|)Setup couldn't h...| 1|[)setup, couldn't...|(1000,[299,520,53...|
|* Comes with a st...| 1|[*, comes, with, ...|(1000,[34,51,67,1...|
|.... Item arrived...| 1|[...., item, arri...|(1000,[98,133,245...|
|1\. long lasting b...| 0|[1., long, lastin...|(1000,[138,258,29...|
|2 thumbs up to th...| 1|[2, thumbs, up, t...|(1000,[92,128,373...|
|:-)Oh, the charge...| 1|[:-)oh,, the, cha...|(1000,[388,497,52...|
| A Disappointment.| 0|[a, disappointment.]|(1000,[170,386],[...|
|A PIECE OF JUNK T...| 0|[a, piece, of, ju...|(1000,[34,36,47,7...|
|A good quality ba...| 1|[a, good, quality...|(1000,[77,82,168,...|
|A must study for ...| 0|[a, must, study, ...|(1000,[23,36,104,...|
|A pretty good pro...| 1|[a, pretty, good,...|(1000,[168,170,27...|
|A usable keyboard...| 1|[a, usable, keybo...|(1000,[2,116,170,...|
|A week later afte...| 0|[a, week, later, ...|(1000,[77,122,156...|
|AFTER ARGUING WIT...| 0|[after, arguing, ...|(1000,[77,166,202...|
|AFter the first c...| 0|[after, the, firs...|(1000,[63,77,183,...|
| AMAZON SUCKS.| 0| [amazon, sucks.]|(1000,[828,966],[...|
| Absolutel junk.| 0| [absolutel, junk.]|(1000,[607,888],[...|
| Absolutely great.| 1|[absolutely, great.]|(1000,[589,903],...|
|Adapter does not ...| 0|[adapter, does, n...|(1000,[0,18,51,28...|
+--------------------+-----+--------------------+--------------------+
only showing top 20 rows
// We will use the naïve Bayes classifier provided by MLlib.
import org.apache.spark.ml.classification.NaiveBayes
val nb = new NaiveBayes()
// We now have all the parts that we need to assemble
// a machine learning pipeline.
import org.apache.spark.ml.Pipeline
val pipeline = new Pipeline().setStages(Array(tokenizer, htf, nb))
// Train our model using the training dataset.
val model = pipeline.fit(trainingData)
// Predict using the test dataset.
val predictions = model.transform(testData)
// Display the predictions for each review.
predictions.select("text","prediction").show
+--------------------+----------+
| text|prediction|
+--------------------+----------+
|!I definitely reco...| 1.0|
|#1 It Works - #2 ...| 1.0|
| $50 Down the drain.| 0.0|
|A lot of websites...| 1.0|
|After charging ov...| 0.0|
|After my phone go...| 0.0|
|All in all I thin...| 1.0|
|All it took was o...| 0.0|
|Also, if your pho...| 0.0|
|And I just love t...| 1.0|
|And none of the t...| 1.0|
| Bad Choice.| 0.0|
|Best headset ever...| 1.0|
|Big Disappointmen...| 0.0|
|Bluetooth range i...| 0.0|
|But despite these...| 0.0|
|Buyer--Be Very Ca...| 1.0|
|Can't store anyth...| 0.0|
|Chinese Forgeries...| 0.0|
|Do NOT buy if you...| 0.0|
+--------------------+----------+
only showing top 20 rows
// Evaluate our model using a binary classifier evaluator.
import org.apache.spark.ml.evaluation.BinaryClassificationEvaluator
val evaluator = new BinaryClassificationEvaluator()
import org.apache.spark.ml.param.ParamMap
val paramMap = ParamMap(evaluator.metricName -> "areaUnderROC")
val auc = evaluator.evaluate(predictions, paramMap)
auc: Double = 0.5407085561497325
// Test on a positive example.
val predictions = model
.transform(sc.parallelize(Seq("This product is good")).toDF("text"))
predictions.select("text","prediction").show
+--------------------+----------+
| text|prediction|
+--------------------+----------+
|This product is good| 1.0|
+--------------------+----------+
// Test on a negative example.
val predictions = model
.transform(sc.parallelize(Seq("This product is bad")).toDF("text"))
predictions.select("text","prediction").show
+-------------------+----------+
| text|prediction|
+-------------------+----------+
|This product is bad| 0.0|
+-------------------+----------+
Listing 3-8Sentiment Analysis Using Naïve Bayes
可以做几件事来改进我们的模型。在大多数自然语言处理(NLP)任务中,执行额外的文本预处理(如 n 元语法、词汇化和去除停用词)是很常见的。我在第四章中介绍了斯坦福 CoreNLP 和 Spark NLP。
回归
回归是一种用于预测连续数值的监督机器学习任务。举几个例子来说,流行的用例包括销售和需求预测、预测股票、房屋或商品价格以及天气预报。我在第 [1 章更详细地讨论了回归。
简单线性回归
线性回归用于检查一个或多个自变量和因变量之间的线性关系。对单个自变量和单个连续因变量之间关系的分析称为简单线性回归。
正如您在图 3-9 中所看到的,该图显示了线性攻击试图绘制一条直线,以最好地减少观察到的响应和预测值之间的残差平方和。XXXV
图 3-9
简单的线性回归图
例子
对于我们的例子,我们将使用简单的线性回归来显示房价(因变量)如何根据该地区的平均家庭收入(自变量)而变化。清单 3-9 详细列出了代码。
import org.apache.spark.ml.regression.LinearRegression
import spark.implicits._
val dataDF = Seq(
(50000, 302200),
(75200, 550000),
(90000, 680000),
(32800, 225000),
(41000, 275000),
(54000, 300500),
(72000, 525000),
(105000, 700000),
(88500, 673100),
(92000, 695000),
(53000, 320900),
(85200, 652800),
(157000, 890000),
(128000, 735000),
(71500, 523000),
(114000, 720300),
(33400, 265900),
(143000, 846000),
(68700, 492000),
(46100, 285000)
).toDF("avg_area_income","price")
dataDF.show
+---------------+------+
|avg_area_income| price|
+---------------+------+
| 50000|302200|
| 75200|550000|
| 90000|680000|
| 32800|225000|
| 41000|275000|
| 54000|300500|
| 72000|525000|
| 105000|700000|
| 88500|673100|
| 92000|695000|
| 53000|320900|
| 85200|652800|
| 157000|890000|
| 128000|735000|
| 71500|523000|
| 114000|720300|
| 33400|265900|
| 143000|846000|
| 68700|492000|
| 46100|285000|
+---------------+------+
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(Array("avg_area_income"))
.setOutputCol("feature")
val dataDF2 = assembler.transform(dataDF)
dataDF2.show
+---------------+------+----------+
|avg_area_income| price| feature|
+---------------+------+----------+
| 50000|302200| [50000.0]|
| 75200|550000| [75200.0]|
| 90000|680000| [90000.0]|
| 32800|225000| [32800.0]|
| 41000|275000| [41000.0]|
| 54000|300500| [54000.0]|
| 72000|525000| [72000.0]|
| 105000|700000|[105000.0]|
| 88500|673100| [88500.0]|
| 92000|695000| [92000.0]|
| 53000|320900| [53000.0]|
| 85200|652800| [85200.0]|
| 157000|890000|[157000.0]|
| 128000|735000|[128000.0]|
| 71500|523000| [71500.0]|
| 114000|720300|[114000.0]|
| 33400|265900| [33400.0]|
| 143000|846000|[143000.0]|
| 68700|492000| [68700.0]|
| 46100|285000| [46100.0]|
+---------------+------+----------+
val lr = new LinearRegression()
.setMaxIter(10)
.setFeaturesCol("feature")
.setLabelCol("price")
val model = lr.fit(dataDF2)
import org.apache.spark.ml.linalg.Vectors
val testData = spark
.createDataFrame(Seq(Vectors.dense(75000))
.map(Tuple1.apply))
.toDF("feature")
val predictions = model.transform(testData)
predictions.show
+---------+------------------+
| feature| prediction|
+---------+------------------+
|[75000.0]|504090.35842779215|
+---------+------------------+
Listing 3-9Linear Regression Example
使用 XGBoost4J-Spark 进行多元回归
多元回归用于有两个或更多自变量和一个连续因变量的更现实的情况。在现实世界的用例中,同时具有线性和非线性特性是很常见的。XGBoost 等基于树的集成算法能够处理线性和非线性特征,这使其成为大多数生产环境的理想选择。在大多数情况下,使用基于树的集成(如 XGBoost)进行多元回归应该会显著提高预测精度。XXXVI
我们在本章前面使用 XGBoost 解决了一个分类问题。因为 XGBoost 同时支持分类和回归,所以使用 XGBoost 进行回归与分类非常相似。
例子
对于我们的多元回归示例,我们将使用稍微复杂一点的数据集,如清单 3-10 所示。数据集可以从 Kaggle 下载。 xxxvii 我们的目标是根据数据集中提供的属性预测房价。数据集包含七列:Avg。地区收入,平均。面积房屋年龄,平均。房间面积,平均。卧室面积数量,面积人口,价格和地址。为了简单起见,我们将不使用地址字段(有用的信息可以从家庭地址中获得,例如附近学校的位置)。价格是我们的因变量。
spark-shell --packages ml.dmlc:xgboost4j-spark:0.81
import org.apache.spark.sql.types._
// Define a schema for our dataset.
var pricesSchema = StructType(Array (
StructField("avg_area_income", DoubleType, true),
StructField("avg_area_house_age", DoubleType, true),
StructField("avg_area_num_rooms", DoubleType, true),
StructField("avg_area_num_bedrooms", DoubleType, true),
StructField("area_population", DoubleType, true),
StructField("price", DoubleType, true)
))
val dataDF = spark.read.format("csv")
.option("header","true")
.schema(pricesSchema)
.load("USA_Housing.csv").na.drop()
// Inspect the dataset.
dataDF.printSchema
root
|-- avg_area_income: double (nullable = true)
|-- avg_area_house_age: double (nullable = true)
|-- avg_area_num_rooms: double (nullable = true)
|-- avg_area_num_bedrooms: double (nullable = true)
|-- area_population: double (nullable = true)
|-- price: double (nullable = true)
dataDF.select("avg_area_income","avg_area_house_age","avg_area_num_rooms").show
+------------------+------------------+------------------+
| avg_area_income|avg_area_house_age|avg_area_num_rooms|
+------------------+------------------+------------------+
| 79545.45857431678| 5.682861321615587| 7.009188142792237|
| 79248.64245482568|6.0028998082752425| 6.730821019094919|
|61287.067178656784| 5.865889840310001| 8.512727430375099|
| 63345.24004622798|7.1882360945186425| 5.586728664827653|
|59982.197225708034| 5.040554523106283| 7.839387785120487|
| 80175.7541594853|4.9884077575337145| 6.104512439428879|
| 64698.46342788773| 6.025335906887153| 8.147759585023431|
| 78394.33927753085|6.9897797477182815| 6.620477995185026|
| 59927.66081334963| 5.36212556960358|6.3931209805509015|
| 81885.92718409566| 4.423671789897876| 8.167688003472351|
| 80527.47208292288| 8.09351268063935| 5.042746799645982|
| 50593.69549704281| 4.496512793097035| 7.467627404008019|
|39033.809236982364| 7.671755372854428| 7.250029317273495|
| 73163.6634410467| 6.919534825456555|5.9931879009455695|
| 69391.3801843616| 5.344776176735725| 8.406417714534253|
| 73091.86674582321| 5.443156466535474| 8.517512711137975|
| 79706.96305765743| 5.067889591058972| 8.219771123286257|
| 61929.07701808926| 4.788550241805888|5.0970095543775615|
| 63508.19429942997| 5.947165139552473| 7.187773835329727|
| 62085.27640340488| 5.739410843630574| 7.09180810424997|
+------------------+------------------+------------------+
only showing top 20 rows
dataDF.select("avg_area_num_bedrooms","area_population","price").show
+---------------------+------------------+------------------+
|avg_area_num_bedrooms| area_population| price|
+---------------------+------------------+------------------+
| 4.09|23086.800502686456|1059033.5578701235|
| 3.09| 40173.07217364482| 1505890.91484695|
| 5.13| 36882.15939970458|1058987.9878760849|
| 3.26| 34310.24283090706|1260616.8066294468|
| 4.23|26354.109472103148| 630943.4893385402|
| 4.04|26748.428424689715|1068138.0743935304|
| 3.41| 60828.24908540716|1502055.8173744078|
| 2.42|36516.358972493836|1573936.5644777215|
| 2.3| 29387.39600281585| 798869.5328331633|
| 6.1| 40149.96574921337|1545154.8126419624|
| 4.1| 47224.35984022191| 1707045.722158058|
| 4.49|34343.991885578806| 663732.3968963273|
| 3.1| 39220.36146737246|1042814.0978200927|
| 2.27|32326.123139488096|1291331.5184858206|
| 4.37|35521.294033173246|1402818.2101658515|
| 4.01|23929.524053267953|1306674.6599511993|
| 3.12| 39717.81357630952|1556786.6001947748|
| 4.3| 24595.90149782299| 528485.2467305964|
| 5.12|35719.653052030866|1019425.9367578316|
| 5.49|44922.106702293066|1030591.4292116085|
+---------------------+------------------+------------------+
only showing top 20 rows
val features = Array("avg_area_income","avg_area_house_age",
"avg_area_num_rooms","avg_area_num_bedrooms","area_population")
// Combine our features into a single feature vector.
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
val dataDF2 = assembler.transform(dataDF)
dataDF2.select("price","features").show(20,50)
+------------------+--------------------------------------------------+
| price| features|
+------------------+--------------------------------------------------+
|1059033.5578701235|[79545.45857431678,5.682861321615587,7.00918814...|
| 1505890.91484695|[79248.64245482568,6.0028998082752425,6.7308210...|
|1058987.9878760849|[61287.067178656784,5.865889840310001,8.5127274...|
|1260616.8066294468|[63345.24004622798,7.1882360945186425,5.5867286...|
| 630943.4893385402|[59982.197225708034,5.040554523106283,7.8393877...|
|1068138.0743935304|[80175.7541594853,4.9884077575337145,6.10451243...|
|1502055.8173744078|[64698.46342788773,6.025335906887153,8.14775958...|
|1573936.5644777215|[78394.33927753085,6.9897797477182815,6.6204779...|
| 798869.5328331633|[59927.66081334963,5.36212556960358,6.393120980...|
|1545154.8126419624|[81885.92718409566,4.423671789897876,8.16768800...|
| 1707045.722158058|[80527.47208292288,8.09351268063935,5.042746799...|
| 663732.3968963273|[50593.69549704281,4.496512793097035,7.46762740...|
|1042814.0978200927|[39033.809236982364,7.671755372854428,7.2500293...|
|1291331.5184858206|[73163.6634410467,6.919534825456555,5.993187900...|
|1402818.2101658515|[69391.3801843616,5.344776176735725,8.406417714...|
|1306674.6599511993|[73091.86674582321,5.443156466535474,8.51751271...|
|1556786.6001947748|[79706.96305765743,5.067889591058972,8.21977112...|
| 528485.2467305964|[61929.07701808926,4.788550241805888,5.09700955...|
|1019425.9367578316|[63508.19429942997,5.947165139552473,7.18777383...|
|1030591.4292116085|[62085.27640340488,5.739410843630574,7.09180810...|
+------------------+--------------------------------------------------+
only showing top 20 rows
// Divide our dataset into training and test data.
val seed = 1234
val Array(trainingData, testData) = dataDF2.randomSplit(Array(0.8, 0.2), seed)
// Use XGBoost for regression.
import ml.dmlc.xgboost4j.scala.spark.{XGBoostRegressionModel,XGBoostRegressor}
val xgb = new XGBoostRegressor()
.setFeaturesCol("features")
.setLabelCol("price")
// Create a parameter grid.
import org.apache.spark.ml.tuning.ParamGridBuilder
val paramGrid = new ParamGridBuilder()
.addGrid(xgb.maxDepth, Array(6, 9))
.addGrid(xgb.eta, Array(0.3, 0.7)).build()
paramGrid: Array[org.apache.spark.ml.param.ParamMap] =
Array({
xgbr_bacf108db722-eta: 0.3,
xgbr_bacf108db722-maxDepth: 6
}, {
xgbr_bacf108db722-eta: 0.3,
xgbr_bacf108db722-maxDepth: 9
}, {
xgbr_bacf108db722-eta: 0.7,
xgbr_bacf108db722-maxDepth: 6
}, {
xgbr_bacf108db722-eta: 0.7,
xgbr_bacf108db722-maxDepth: 9
})
// Create our evaluator.
import org.apache.spark.ml.evaluation.RegressionEvaluator
val evaluator = new RegressionEvaluator()
.setLabelCol("price")
.setPredictionCol("prediction")
.setMetricName("rmse")
// Create our cross-validator.
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(xgb)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(3)
val model = cv.fit(trainingData)
val predictions = model.transform(testData)
predictions.select("features","price","prediction").show
+--------------------+------------------+------------+
| features| price| prediction|
+--------------------+------------------+------------+
|17796.6311895433...|302355.83597895555| 591896.9375|
|[35454.7146594754...| 1077805.577726322| 440094.75|
|[35608.9862370775...| 449331.5835333807| 672114.75|
|[38868.2503114142...| 759044.6879907805| 672114.75|
|[40752.7142433209...| 560598.5384309639| 591896.9375|
|[41007.4586732745...| 494742.5435776913|421605.28125|
|[41533.0129597444...| 682200.3005599922|505685.96875|
|[42258.7745410484...| 852703.2636757497| 591896.9375|
|[42940.1389392421...| 680418.7240122693| 591896.9375|
|[43192.1144092488...|1054606.9845532854|505685.96875|
|[43241.9824225005...| 629657.6132544072|505685.96875|
|[44328.2562966742...| 601007.3511604669|141361.53125|
|[45347.1506816944...| 541953.9056802422|441908.40625|
|[45546.6434075757...| 923830.33486809| 591896.9375|
|[45610.9384142094...| 961354.287727855| 849175.75|
|[45685.2499205068...| 867714.3838490517|441908.40625|
|[45990.1237417814...|1043968.3994445396| 849175.75|
|[46062.7542664558...| 675919.6815570832|505685.96875|
|[46367.2058588838...|268050.81474351394| 379889.625|
|[47467.4239151893...| 762144.9261238109| 591896.9375|
+--------------------+------------------+------------+
only showing top 20 rows
Listing 3-10Multiple Regression Using XGBoost4J-Spark
让我们用均方根误差(RMSE)来评估这个模型。残差是数据点与回归线之间距离的度量。RMSE 是残差的标准差,用于测量预测误差。[xxxviii
val rmse = evaluator.evaluate(predictions)
rmse: Double = 438499.82356536255
// Extract the parameters.
model.bestModel.extractParamMap
res11: org.apache.spark.ml.param.ParamMap =
{
xgbr_8da6032c61a9-alpha: 0.0,
xgbr_8da6032c61a9-baseScore: 0.5,
xgbr_8da6032c61a9-checkpointInterval: -1,
xgbr_8da6032c61a9-checkpointPath: ,
xgbr_8da6032c61a9-colsampleBylevel: 1.0,
xgbr_8da6032c61a9-colsampleBytree: 1.0,
xgbr_8da6032c61a9-customEval: null,
xgbr_8da6032c61a9-customObj: null,
xgbr_8da6032c61a9-eta: 0.7,
xgbr_8da6032c61a9-evalMetric: rmse,
xgbr_8da6032c61a9-featuresCol: features,
xgbr_8da6032c61a9-gamma: 0.0,
xgbr_8da6032c61a9-growPolicy: depthwise,
xgbr_8da6032c61a9-labelCol: price,
xgbr_8da6032c61a9-lambda: 1.0,
xgbr_8da6032c61a9-lambdaBias: 0.0,
xgbr_8da6032c61a9-maxBin: 16,
xgbr_8da6032c61a9-maxDeltaStep: 0.0,
xgbr_8da6032c61a9-maxDepth: 9,
xgbr_8da6032c61a9-minChildWeight: 1.0,
xgbr_8da6032c61a9-missing: NaN,
xgbr_8da6032c61a9-normalizeType: tree,
xgbr_8da6032c61a9-nthread: 1,
xgbr_8da6032c61a9-numEarlyStoppingRounds: 0,
xgbr_8da6032c61a9-numRound: 1,
xgbr_8da6032c61a9-numWorkers: 1,
xgbr_8da6032c61a9-objective: reg:linear,
xgbr_8da6032c61a9-predictionCol: prediction,
xgbr_8da6032c61a9-rateDrop: 0.0,
xgbr_8da6032c61a9-sampleType: uniform,
xgbr_8da6032c61a9-scalePosWeight: 1.0,
xgbr_8da6032c61a9-seed: 0,
xgbr_8da6032c61a9-silent: 0,
xgbr_8da6032c61a9-sketchEps: 0.03,
xgbr_8da6032c61a9-skipDrop: 0.0,
xgbr_8da6032c61a9-subsample: 1.0,
xgbr_8da6032c61a9-timeoutRequestWorkers: 1800000,
xgbr_8da6032c61a9-trackerConf: TrackerConf(0,python),
xgbr_8da6032c61a9-trainTestRatio: 1.0,
xgbr_8da6032c61a9-treeLimit: 0,
xgbr_8da6032c61a9-treeMethod: auto,
xgbr_8da6032c61a9-useExternalMemory: false
}
LightGBM 多元回归
在清单 3-11 中,我们将使用 LightGBM。LightGBM 附带了专门用于回归任务的 LightGBMRegressor 类。我们将重用住房数据集和上一个 XGBoost 示例中的大部分代码。
spark-shell --packages Azure:mmlspark:0.15
var pricesSchema = StructType(Array (
StructField("avg_area_income", DoubleType, true),
StructField("avg_area_house_age", DoubleType, true),
StructField("avg_area_num_rooms", DoubleType, true),
StructField("avg_area_num_bedrooms", DoubleType, true),
StructField("area_population", DoubleType, true),
StructField("price", DoubleType, true)
))
val dataDF = spark.read.format("csv")
.option("header","true")
.schema(pricesSchema)
.load("USA_Housing.csv")
.na.drop()
dataDF.printSchema
root
|-- avg_area_income: double (nullable = true)
|-- avg_area_house_age: double (nullable = true)
|-- avg_area_num_rooms: double (nullable = true)
|-- avg_area_num_bedrooms: double (nullable = true)
|-- area_population: double (nullable = true)
|-- price: double (nullable = true)
dataDF.select("avg_area_income","avg_area_house_age",
"avg_area_num_rooms")
.show
+------------------+------------------+------------------+
| avg_area_income|avg_area_house_age|avg_area_num_rooms|
+------------------+------------------+------------------+
| 79545.45857431678| 5.682861321615587| 7.009188142792237|
| 79248.64245482568|6.0028998082752425| 6.730821019094919|
|61287.067178656784| 5.865889840310001| 8.512727430375099|
| 63345.24004622798|7.1882360945186425| 5.586728664827653|
|59982.197225708034| 5.040554523106283| 7.839387785120487|
| 80175.7541594853|4.9884077575337145| 6.104512439428879|
| 64698.46342788773| 6.025335906887153| 8.147759585023431|
| 78394.33927753085|6.9897797477182815| 6.620477995185026|
| 59927.66081334963| 5.36212556960358|6.3931209805509015|
| 81885.92718409566| 4.423671789897876| 8.167688003472351|
| 80527.47208292288| 8.09351268063935| 5.042746799645982|
| 50593.69549704281| 4.496512793097035| 7.467627404008019|
|39033.809236982364| 7.671755372854428| 7.250029317273495|
| 73163.6634410467| 6.919534825456555|5.9931879009455695|
| 69391.3801843616| 5.344776176735725| 8.406417714534253|
| 73091.86674582321| 5.443156466535474| 8.517512711137975|
| 79706.96305765743| 5.067889591058972| 8.219771123286257|
| 61929.07701808926| 4.788550241805888|5.0970095543775615|
| 63508.19429942997| 5.947165139552473| 7.187773835329727|
| 62085.27640340488| 5.739410843630574| 7.09180810424997|
+------------------+------------------+------------------+
dataDF.select("avg_area_num_bedrooms","area_population","price").show
+---------------------+------------------+------------------+
|avg_area_num_bedrooms| area_population| price|
+---------------------+------------------+------------------+
| 4.09|23086.800502686456|1059033.5578701235|
| 3.09| 40173.07217364482| 1505890.91484695|
| 5.13| 36882.15939970458|1058987.9878760849|
| 3.26| 34310.24283090706|1260616.8066294468|
| 4.23|26354.109472103148| 630943.4893385402|
| 4.04|26748.428424689715|1068138.0743935304|
| 3.41| 60828.24908540716|1502055.8173744078|
| 2.42|36516.358972493836|1573936.5644777215|
| 2.3| 29387.39600281585| 798869.5328331633|
| 6.1| 40149.96574921337|1545154.8126419624|
| 4.1| 47224.35984022191| 1707045.722158058|
| 4.49|34343.991885578806| 663732.3968963273|
| 3.1| 39220.36146737246|1042814.0978200927|
| 2.27|32326.123139488096|1291331.5184858206|
| 4.37|35521.294033173246|1402818.2101658515|
| 4.01|23929.524053267953|1306674.6599511993|
| 3.12| 39717.81357630952|1556786.6001947748|
| 4.3| 24595.90149782299| 528485.2467305964|
| 5.12|35719.653052030866|1019425.9367578316|
| 5.49|44922.106702293066|1030591.4292116085|
+---------------------+------------------+------------------+
only showing top 20 rows
val features = Array("avg_area_income","avg_area_house_age",
"avg_area_num_rooms","avg_area_num_bedrooms","area_population")
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
val dataDF2 = assembler.transform(dataDF)
dataDF2.select("price","features").show(20,50)
+------------------+--------------------------------------------------+
| price| features|
+------------------+--------------------------------------------------+
|1059033.5578701235|[79545.45857431678,5.682861321615587,7.00918814...|
| 1505890.91484695|[79248.64245482568,6.0028998082752425,6.7308210...|
|1058987.9878760849|[61287.067178656784,5.865889840310001,8.5127274...|
|1260616.8066294468|[63345.24004622798,7.1882360945186425,5.5867286...|
| 630943.4893385402|[59982.197225708034,5.040554523106283,7.8393877...|
|1068138.0743935304|[80175.7541594853,4.9884077575337145,6.10451243...|
|1502055.8173744078|[64698.46342788773,6.025335906887153,8.14775958...|
|1573936.5644777215|[78394.33927753085,6.9897797477182815,6.6204779...|
| 798869.5328331633|[59927.66081334963,5.36212556960358,6.393120980...|
|1545154.8126419624|[81885.92718409566,4.423671789897876,8.16768800...|
| 1707045.722158058|[80527.47208292288,8.09351268063935,5.042746799...|
| 663732.3968963273|[50593.69549704281,4.496512793097035,7.46762740...|
|1042814.0978200927|[39033.809236982364,7.671755372854428,7.2500293...|
|1291331.5184858206|[73163.6634410467,6.919534825456555,5.993187900...|
|1402818.2101658515|[69391.3801843616,5.344776176735725,8.406417714...|
|1306674.6599511993|[73091.86674582321,5.443156466535474,8.51751271...|
|1556786.6001947748|[79706.96305765743,5.067889591058972,8.21977112...|
| 528485.2467305964|[61929.07701808926,4.788550241805888,5.09700955...|
|1019425.9367578316|[63508.19429942997,5.947165139552473,7.18777383...|
|1030591.4292116085|[62085.27640340488,5.739410843630574,7.09180810...|
+------------------+--------------------------------------------------+
only showing top 20 rows
val seed = 1234
val Array(trainingData, testData) = dataDF2.randomSplit(Array(0.8, 0.2), seed)
import com.microsoft.ml.spark.{LightGBMRegressionModel,LightGBMRegressor}
val lightgbm = new LightGBMRegressor()
.setFeaturesCol("features")
.setLabelCol("price")
.setObjective("regression")
import org.apache.spark.ml.tuning.ParamGridBuilder
val paramGrid = new ParamGridBuilder()
.addGrid(lightgbm.numLeaves, Array(6, 9))
.addGrid(lightgbm.numIterations, Array(10, 15))
.addGrid(lightgbm.maxDepth, Array(2, 3, 4))
.build()
paramGrid: Array[org.apache.spark.ml.param.ParamMap] =
Array({
LightGBMRegressor_f969f7c475b5-maxDepth: 2,
LightGBMRegressor_f969f7c475b5-numIterations: 10,
LightGBMRegressor_f969f7c475b5-numLeaves: 6
}, {
LightGBMRegressor_f969f7c475b5-maxDepth: 3,
LightGBMRegressor_f969f7c475b5-numIterations: 10,
LightGBMRegressor_f969f7c475b5-numLeaves: 6
}, {
LightGBMRegressor_f969f7c475b5-maxDepth: 4,
LightGBMRegressor_f969f7c475b5-numIterations: 10,
LightGBMRegressor_f969f7c475b5-numLeaves: 6
}, {
LightGBMRegressor_f969f7c475b5-maxDepth: 2,
LightGBMRegressor_f969f7c475b5-numIterations: 10,
LightGBMRegressor_f969f7c475b5-numLeaves: 9
}, {
LightGBMRegressor_f969f7c475b5-maxDepth: 3,
LightGBMRegressor_f969f7c475b5-numIterations: 10,
LightGBMRegressor_f969f7c475b5-numLeaves: 9
}, {
Lig...
import org.apache.spark.ml.evaluation.RegressionEvaluator
val evaluator = new RegressionEvaluator()
.setLabelCol("price")
.setPredictionCol("prediction")
.setMetricName("rmse")
import org.apache.spark.ml.tuning.CrossValidator
val cv = new CrossValidator()
.setEstimator(lightgbm)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(3)
val model = cv.fit(trainingData)
val predictions = model.transform(testData)
predictions.select("features","price","prediction").show
+--------------------+------------------+------------------+
| features| price| prediction|
+--------------------+------------------+------------------+
|[17796.6311895433...|302355.83597895555| 965317.3181705693|
|[35454.7146594754...| 1077805.577726322|1093159.8506664087|
|[35608.9862370775...| 449331.5835333807|1061505.7131801855|
|[38868.2503114142...| 759044.6879907805|1061505.7131801855|
|[40752.7142433209...| 560598.5384309639| 974582.8481703462|
|[41007.4586732745...| 494742.5435776913| 881891.5646432829|
|[41533.0129597444...| 682200.3005599922| 966417.0064436384|
|[42258.7745410484...| 852703.2636757497|1070641.7611960804|
|[42940.1389392421...| 680418.7240122693|1028986.6314725328|
|[43192.1144092488...|1054606.9845532854|1087808.2361520242|
|[43241.9824225005...| 629657.6132544072| 889012.3734817103|
|[44328.2562966742...| 601007.3511604669| 828175.3829271109|
|[45347.1506816944...| 541953.9056802422| 860754.7467075661|
|[45546.6434075757...| 923830.33486809| 950407.7970842035|
|[45610.9384142094...| 961354.287727855|1175429.1179985087|
|[45685.2499205068...| 867714.3838490517| 828812.007346283|
|[45990.1237417814...|1043968.3994445396|1204501.1530193759|
|[46062.7542664558...| 675919.6815570832| 973273.6042265462|
|[46367.2058588838...|268050.81474351394| 761576.9192149616|
|[47467.4239151893...| 762144.9261238109| 951908.0117790927|
+--------------------+------------------+------------------+
only showing top 20 rows
val rmse = evaluator.evaluate(predictions)
rmse: Double = 198601.74726198777
Listing 3-11Multiple Regression Using LightGBM
让我们提取每个特性的特性重要性分数。
val model = lightgbm.fit(trainingData)
model.getFeatureImportances("gain")
res7: Array[Double] = Array(1.110789482705408E15, 5.69355224816896E14, 3.25231517467648E14, 1.16104381056E13, 4.84685311277056E14)
通过匹配列表中输出的顺序和特征向量中特征的顺序(avg_area_income,avg_area_house_age,avg_area_num_rooms,avg _ area _ num _ hydro ses,area_population),看起来 avg_area_income 是我们最重要的特征,其次是 avg_area_house_age,area_population 和 avg_area_num_rooms。最不重要的特征是 avg _ area _ num _ bedrooms。
摘要
我讨论了 Spark MLlib 中包含的一些最流行的监督学习算法,以及外部可用的新算法,如 XGBoost 和 LightGBM。虽然网上有大量关于 XGBoost 和 LightGBM for Python 的文档,但是关于 Spark 的信息和示例却很有限。本章旨在帮助弥合这一差距。
我建议您参考 https://xgboost.readthedocs.io/en/latest
来了解更多关于 XGBoost 的信息。对于 LightGBM, https://lightgbm.readthedocs.io/en/latest
有最新信息。有关 Spark MLlib 中包含的分类和回归算法背后的理论和数学的更深入的报道,我建议您参考 Gareth James、Daniela Witten、Trevor Hastie 和 Robert Tibshirani (Springer,2017 年)的统计学习介绍以及 Trevor Hastie、Robert Tibshirani 和 Jerome Friedman (Springer,2016 年)的统计学习要素。关于 Spark MLlib 的更多信息,请在线咨询 Apache Spark 的机器学习库(MLlib)指南:https://spark.apache.org/docs/latest/ml-guide.html
。
参考
-
朱迪亚珍珠;“E PUR SI MUOVE(但它会移动),”2018,原因之书:因果的新科学
-
阿帕奇 Spark《多项逻辑回归》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-classification-regression.html#multinomial-logistic-regression
-
圣乔治·德拉克斯;《支持向量机 vs 逻辑回归》,towardsdatascience.com,2018,
https://towardsdatascience.com/support-vector-machine-vs-logistic-regression-94cc2975433f
-
阿帕奇 Spark《多层感知器分类器》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-classification-regression.html#multilayer-perceptron-classifier
-
分析 Vidhya 内容团队;《从零开始的基于树的建模完整教程(用 R & Python 编写)》,AnalyticsVidhya.com,2016,
www.analyticsvidhya.com/blog/2016/04/complete-tutorial-tree-based-modeling-scratch-in-python/#one
-
LightGBM《分类特征的最优分割》,lightgbm.readthedocs.io,2019,
https://lightgbm.readthedocs.io/en/latest/Features.html
-
约瑟夫·布拉德利和马尼什·阿姆德;《MLlib 中的随机森林与助推》,Databricks,2015,
https://databricks.com/blog/2015/01/21/random-forests-and-boosting-in-mllib.html
-
分析 Vidhya 内容团队;《理解 XGBoost 背后数学的端到端指南》,analyticsvidhya.com,2018,
www.analyticsvidhya.com/blog/2018/09/an-end-to-end-guide-to-understand-the-math-behind-xgboost/
-
本·戈尔曼;“一位 Kaggle 大师解释渐变增强,”Kaggle.com,2017,
http://blog.kaggle.com/2017/01/23/a-kaggle-master-explains-gradient-boosting/
-
XGBoost《助推树木概论》xgboost.readthedocs.io,2019,
https://xgboost.readthedocs.io/en/latest/tutorials/model.html
-
阿帕奇 Spark“集合——基于 RDD 的 API”,spark.apache.org,2019,
https://spark.apache.org/docs/latest/mllib-ensembles.html#gradient-boosted-trees-gbts
-
陈天琦;“什么时候可以在梯度提升机器(GBM)上使用随机森林?,“quora.com,2015,
www.quora.com/When-would-one-use-Random-Forests-over-Gradient-Boosted-Machines-GBMs
-
艾登·奥布莱恩等人。艾尔。;“基因组变体的 VariantSpark 机器学习”,CSIRO,2018,
https://bioinformatics.csiro.au/variantspark
-
丹尼斯·c·鲍尔等人。艾尔。;“利用广泛的随机森林打破基因组学中的维数灾难”,Databricks,2017,
https://databricks.com/blog/2017/07/26/breaking-the-curse-of-dimensionality-in-genomics-using-wide-random-forests.html
-
亚历山大·露露利。艾尔!艾尔!;“重构”,github.com,2017 年,
-
重新造林;“如何用 ReForeSt 学习随机森林分类模型”,sites.google.com,2019,
https://sites.google.com/view/reforest/example?authuser=0
-
阿帕奇 Sparkspark.apache.org,2019 年,
https://spark.apache.org/docs/latest/mllib-ensembles.html#random-forests
-
CallMiner“新的研究发现不重视客户导致 1360 亿美元的转换流行病,”CallMiner,2018,
www.globenewswire.com/news-release/2018/09/27/1577343/0/en/New-research-finds-not-valuing-customers-leads-to-136-billion-switching-epidemic.html
-
红色 Reichheld《削减成本的药方》,贝恩公司,2016 年,
www2.bain.cimg/BB_Prescription_cutting_costs.pdf
-
亚历克斯·劳伦斯;《企业家留住客户的五个秘诀》,福布斯,2012,
www.forbes.com/sites/alexlawrence/2012/11/01/five-customer-retention-tips-for-entrepreneurs/
-
大卫·贝克汉姆;《电信数据集中的流失》,Kaggle,2017,
www.kaggle.com/becksddf/churn-in-telecoms-dataset
-
杰弗里·什曼;“如何使用 Apache Spark MLlib 预测电信客户流失”,DZone,2016,
https://dzone.com/articles/how-to-predict-telco-churn-with-apache-spark-mllib
-
杰克·霍尔;《随机森林的可变重要性是如何计算的》,DisplayR,2018,
www.displayr.com/how-is-variable-importance-calculated-for-a-random-forest/
-
迪德里克·尼尔森;《用 XGBoost 助推树》,挪威科技大学,2016,
https://brage.bibsys.no/xmlui/bitstream/handle/11250/2433761/16128_FULLTEXT.pdf
-
莉娜·肖;《XGBoost:简明技术概述》,KDNuggets,2017,
www.kdnuggets.com/2017/10/xgboost-concise-technical-overview.html
-
Philip Hyunsu Cho“快速直方图优化生长器,8 到 10 倍加速”,DMLC,2017,
https://github.com/dmlc/xgboost/issues/1950
-
XGBoost《用 XGBoost4J-Spark 构建一个 ML 应用》,xgboost.readthedocs.io,2019,
https://xgboost.readthedocs.io/en/latest/jvm/xgboost4j_spark_tutorial.html#pipeline-with-hyper-parameter-tunning
-
杰森·布朗利;《Python 中如何用 XGBoost 调优决策树的个数和大小》,machinelearningmastery.com,2016,
https://machinelearningmastery.com/tune-number-size-decision-trees-xgboost-python/
-
Laurae"基准测试 light GBM:light GBM 与 xgboost 相比有多快?",medium.com,2017 年
[`https://medium.com/implodinggradients/benchmarking-lightgbm-how-fast-is-lightgbm-vs-xgboost-15d224568031`](https://medium.com/implodinggradients/benchmarking-lightgbm-how-fast-is-lightgbm-vs-xgboost-15d224568031)
-
LightGBM《在速度和内存使用上的优化》,lightgbm.readthedocs.io,2019,
https://lightgbm.readthedocs.io/en/latest/Features.html
-
大卫·马克思;“决策树:逐叶(最佳优先)和逐级树遍历”,stackexchange.com,2018,
https://datascience.stackexchange.com/questions/26699/decision-trees-leaf-wise-best-first-and-level-wise-tree-traverse
-
LightGBM《LightGBM 特性》,lightgbm.readthedocs.io,2019,
https://lightgbm.readthedocs.io/en/latest/Features.html
-
西拉德·帕夫卡;“各种开源 GBM 实现的性能”,github.com,2019,
https://github.com/szilard/GBM-perf
-
胡利奥·安东尼奥·索托;"“增益”返回的特征重要性是什么?",github.com,2018,
https://github.com/Microsoft/LightGBM/issues/1842
-
sci kit-learn;《线性回归例题》,scikit-learn.org,2019,
https://scikit-learn.org/stable/auto_examples/linear_model/plot_ols.html
-
李宏建等。艾尔。;“用随机森林代替多元线性回归提高评分函数的结合亲和力预测:Cyscore 案例研究”,nih.gov,2014,
www.ncbi.nlm.nih.gov/pmc/articles/PMC4153907/
-
阿里扬·潘查尔;《USA Housing.csv》,Kaggle,2018,
www.kaggle.com/aariyan101/usa-housingcsv
-
数据科学中心;《RMSE:均方根误差》,Datasciencecentral.com,2016,
www.statisticshowto.datasciencecentral.com/rmse/
四、无监督学习
新知识是地球上最有价值的商品。我们掌握的真理越多,我们就越富有。
—库尔特·冯内古特 我
无监督学习是一种机器学习任务,它在没有标记响应的帮助下发现数据集中隐藏的模式和结构。当您只能访问输入数据,而训练数据不可用或难以获得时,无监督学习是理想的选择。常见的方法包括聚类、主题建模、异常检测和主成分分析。
K-均值聚类
聚类是一种无监督的机器学习任务,用于对具有某些相似性的未标记观察值进行分组。流行的聚类用例包括客户细分、欺诈分析和异常检测。在训练数据缺乏或不可用的情况下,聚类也经常用于为分类器生成训练数据。K-Means 是最流行的无监督聚类学习算法之一。Spark MLlib 包含一个更具可扩展性的 K-means 实现,称为 K-means||。图 4-1 显示了 K-means 将 Iris 数据集中的观察值分组为三个不同的聚类。
图 4-1
使用 K-means 对虹膜数据集进行聚类
图 4-2 显示了运行中的 K 均值算法。观察值显示为正方形,聚类质心显示为三角形。图 4-2 (a)显示了原始数据集。K-Means 的工作原理是随机分配质心作为每个聚类的起点(图 4-2 (b)和(c))。该算法基于欧几里德距离迭代地将每个数据点分配到最近的质心。然后,它通过计算属于该聚类的所有点的平均值来计算每个聚类的新质心(图 4-2 (d)和(e))。当达到预定义的迭代次数或每个数据点都被分配到其最近的质心,并且不再有可执行的重新分配时,算法停止迭代(图 4-2 (f))。
图 4-2
K-Means 算法在动作 ii
K-Means 要求用户向算法提供聚类数 k 。有多种方法可以找到数据集的最佳聚类数。我们将在本章的后面讨论肘和剪影方法。
例子
让我们来看一个简单的客户细分例子。我们将使用一个小型数据集,其中包含七个观察值以及三个分类要素和两个连续要素的组合。在开始之前,我们需要解决 K-means 的另一个限制。K-Means 不能直接处理分类特征,例如性别、【M】、【F】、、【M】、、、【CA】、【NY】、,并且要求所有特征都是连续的。然而,现实世界的数据集通常包含分类特征和连续特征的组合。幸运的是,我们仍然可以通过将分类特征转换成数字格式来使用 K-means。
这并不像听起来那么简单。例如,要将婚姻状况从其字符串表示形式、【M】、和【S】转换为一个数字,您可能会认为将 0 映射到、【M】、、【S】、对于 K-means 是合适的。正如你在第二章中了解到的,这被称为整数或标签编码。但这也带来了另一个问题。整数有一个自然的顺序 (0 < 1 < 2) 一些机器学习算法(如 K-means)可能会误解这一顺序,认为一个分类值比另一个分类值“更大”只是因为它被编码为整数,而实际上数据中并不存在这样的顺序关系。这可能会产生意想不到的结果。为了解决这个问题,我们将使用另一种类型的编码,称为一次性编码。 iii
在将分类特征转换为整数(使用 StringIndexer)之后,我们使用一键编码(使用 OneHotEncoderEstimator)将分类特征表示为二进制向量。例如,状态特征(“CA”、“NY”、“MA”、“AZ”)是表 4-1 中的一个热编码。
表 4-1
独热编码状态特征
|加利福尼亚
|
纽约州
|
马萨诸塞州
|
阿塞拜疆(Azerbaijan 的缩写)
|
| --- | --- | --- | --- |
| one | Zero | Zero | Zero |
| Zero | one | Zero | Zero |
| Zero | Zero | one | Zero |
| Zero | Zero | Zero | one |
特征缩放是 K-means 的另一个重要的预处理步骤。如第二章所述,特征缩放被认为是最佳实践,也是许多涉及距离计算的机器学习算法的要求。如果数据以不同的比例测量,则要素比例尤其重要。某些特性可能具有非常宽的值范围,导致它们支配其他特性。要素缩放可确保每个要素成比例地影响最终距离。在我们的示例中,我们将使用 StandardScaler 估计量来重新调整我们的特征,使其均值为 0,单位方差(标准差为 1)如清单 4-1 所示。
// Let's start with our example by creating some sample data.
val custDF = Seq(
(100, 29000,"M","F","CA",25),
(101, 36000,"M","M","CA",46),
(102, 5000,"S","F","NY",18),
(103, 68000,"S","M","AZ",39),
(104, 2000,"S","F","CA",16),
(105, 75000,"S","F","CA",41),
(106, 90000,"M","M","MA",47),
(107, 87000,"S","M","NY",38)
).toDF("customerid", "income","maritalstatus","gender","state","age")
// Perform some preprocessing steps.
import org.apache.spark.ml.feature.StringIndexer
val genderIndexer = new StringIndexer()
.setInputCol("gender")
.setOutputCol("gender_idx")
val stateIndexer = new StringIndexer()
.setInputCol("state")
.setOutputCol("state_idx")
val mstatusIndexer = new StringIndexer()
.setInputCol("maritalstatus")
.setOutputCol("maritalstatus_idx")
import org.apache.spark.ml.feature.OneHotEncoderEstimator
val encoder = new OneHotEncoderEstimator()
.setInputCols(Array("gender_idx","state_idx","maritalstatus_idx"))
.setOutputCols(Array("gender_enc","state_enc","maritalstatus_enc"))
val custDF2 = genderIndexer.fit(custDF).transform(custDF)
val custDF3 = stateIndexer.fit(custDF2).transform(custDF2)
val custDF4 = mstatusIndexer.fit(custDF3).transform(custDF3)
custDF4.select("gender_idx","state_idx","maritalstatus_idx").show
+----------+---------+-----------------+
|gender_idx|state_idx|maritalstatus_idx|
+----------+---------+-----------------+
| 0.0| 0.0| 1.0|
| 1.0| 0.0| 1.0|
| 0.0| 1.0| 0.0|
| 1.0| 3.0| 0.0|
| 0.0| 0.0| 0.0|
| 0.0| 0.0| 0.0|
| 1.0| 2.0| 1.0|
| 1.0| 1.0| 0.0|
+----------+---------+-----------------+
val custDF5 = encoder.fit(custDF4).transform(custDF4)
custDF5.printSchema
root
|-- customerid: integer (nullable = false)
|-- income: integer (nullable = false)
|-- maritalstatus: string (nullable = true)
|-- gender: string (nullable = true)
|-- state: string (nullable = true)
|-- age: integer (nullable = false)
|-- gender_idx: double (nullable = false)
|-- state_idx: double (nullable = false)
|-- maritalstatus_idx: double (nullable = false)
|-- gender_enc: vector (nullable = true)
|-- state_enc: vector (nullable = true)
|-- maritalstatus_enc: vector (nullable = true)
custDF5.select("gender_enc","state_enc","maritalstatus_enc").show
+-------------+-------------+-----------------+
| gender_enc| state_enc|maritalstatus_enc|
+-------------+-------------+-----------------+
|(1,[0],[1.0])|(3,[0],[1.0])| (1,[],[])|
| (1,[],[])|(3,[0],[1.0])| (1,[],[])|
|(1,[0],[1.0])|(3,[1],[1.0])| (1,[0],[1.0])|
| (1,[],[])| (3,[],[])| (1,[0],[1.0])|
|(1,[0],[1.0])|(3,[0],[1.0])| (1,[0],[1.0])|
|(1,[0],[1.0])|(3,[0],[1.0])| (1,[0],[1.0])|
| (1,[],[])|(3,[2],[1.0])| (1,[],[])|
| (1,[],[])|(3,[1],[1.0])| (1,[0],[1.0])|
+-------------+-------------+-----------------+
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(Array("income","gender_enc", "state_enc", "maritalstatus_enc", "age"))
.setOutputCol("features")
val custDF6 = assembler.transform(custDF5)
custDF6.printSchema
root
|-- customerid: integer (nullable = false)
|-- income: integer (nullable = false)
|-- maritalstatus: string (nullable = true)
|-- gender: string (nullable = true)
|-- state: string (nullable = true)
|-- age: integer (nullable = false)
|-- gender_idx: double (nullable = false)
|-- state_idx: double (nullable = false)
|-- maritalstatus_idx: double (nullable = false)
|-- gender_enc: vector (nullable = true)
|-- state_enc: vector (nullable = true)
|-- maritalstatus_enc: vector (nullable = true)
|-- features: vector (nullable = true)
custDF6.select("features").show(false)
+----------------------------------+
|features |
+----------------------------------+
|[29000.0,1.0,1.0,0.0,0.0,0.0,25.0]|
|(7,[0,2,6],[36000.0,1.0,46.0]) |
|[5000.0,1.0,0.0,1.0,0.0,1.0,18.0] |
|(7,[0,5,6],[68000.0,1.0,39.0]) |
|[2000.0,1.0,1.0,0.0,0.0,1.0,16.0] |
|[75000.0,1.0,1.0,0.0,0.0,1.0,41.0]|
|(7,[0,4,6],[90000.0,1.0,47.0]) |
|[87000.0,0.0,0.0,1.0,0.0,1.0,38.0]|
+----------------------------------+
import org.apache.spark.ml.feature.StandardScaler
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithStd(true)
.setWithMean(false)
val custDF7 = scaler.fit(custDF6).transform(custDF6)
custDF7.printSchema
root
|-- customerid: integer (nullable = false)
|-- income: integer (nullable = false)
|-- maritalstatus: string (nullable = true)
|-- gender: string (nullable = true)
|-- state: string (nullable = true)
|-- age: integer (nullable = false)
|-- gender_idx: double (nullable = false)
|-- state_idx: double (nullable = false)
|-- maritalstatus_idx: double (nullable = false)
|-- gender_enc: vector (nullable = true)
|-- state_enc: vector (nullable = true)
|-- maritalstatus_enc: vector (nullable = true)
|-- features: vector (nullable = true)
|-- scaledFeatures: vector (nullable = true)
custDF7.select("scaledFeatures").show(8,65)
+-----------------------------------------------------------------+
| scaledFeatures |
+-----------------------------------------------------------------+
|[0.8144011366375091,1.8708286933869707,1.8708286933869707,0.0,...|
|(7,[0,2,6],[1.0109807213431148,1.8708286933869707,3.7319696616...|
|[0.1404139890754326,1.8708286933869707,0.0,2.160246899469287,0...|
|(7,[0,5,6],[1.9096302514258834,1.9321835661585918,3.1640612348...|
|[0.05616559563017304,1.8708286933869707,1.8708286933869707,0.0...|
|[2.106209836131489,1.8708286933869707,1.8708286933869707,0.0,0...|
|(7,[0,4,6],[2.5274518033577866,2.82842712474619,3.813099436871...|
|[2.443203409912527,0.0,0.0,2.160246899469287,0.0,1.93218356615...|
+-----------------------------------------------------------------+
// We’ll create two clusters.
import org.apache.spark.ml.clustering.KMeans
val kmeans = new KMeans()
.setFeaturesCol("scaledFeatures")
.setPredictionCol("prediction")
.setK(2)
import org.apache.spark.ml.Pipeline
val pipeline = new Pipeline()
.setStages(Array(genderIndexer, stateIndexer,
mstatusIndexer, encoder, assembler, scaler, kmeans))
val model = pipeline.fit(custDF)
val clusters = model.transform(custDF)
clusters.select("customerid","income","maritalstatus",
"gender","state","age","prediction")
.show
+----------+------+-------------+------+-----+---+----------+
|customerid|income|maritalstatus|gender|state|age|prediction|
+----------+------+-------------+------+-----+---+----------+
| 100| 29000| M| F| CA| 25| 1|
| 101| 36000| M| M| CA| 46| 0|
| 102| 5000| S| F| NY| 18| 1|
| 103| 68000| S| M| AZ| 39| 0|
| 104| 2000| S| F| CA| 16| 1|
| 105| 75000| S| F| CA| 41| 0|
| 106| 90000| M| M| MA| 47| 0|
| 107| 87000| S| M| NY| 38| 0|
+----------+------+-------------+------+-----+---+----------+
import org.apache.spark.ml.clustering.KMeansModel
val model = pipeline.stages.last.asInstanceOf[KMeansModel]
model.clusterCenters.foreach(println)
[1.9994952044341603,0.37416573867739417,0.7483314773547883,0.4320493798938574,0.565685424949238,1.159310139695155,3.4236765156588613]
[0.3369935737810382,1.8708286933869707,1.247219128924647,0.7200822998230956,0.0,1.288122377439061,1.5955522466340666]
Listing 4-1A Customer Segmentation Example Using K-Means
我们通过计算误差平方和(WSSSE)来评估我们的聚类。使用“肘方法”检查 WSSSE 通常用于帮助确定最佳的集群数量。肘方法的工作原理是用一系列 k 值拟合模型,并将其绘制在 WSSSE 上。目视检查折线图,如果它类似于弯曲的手臂,它在曲线上弯曲的点(“肘”)指示 k 的最佳值。
val wssse = model.computeCost(custDF)
wssse: Double = 32.09801038868844
另一种评估聚类质量的方法是通过计算轮廓系数得分。轮廓分数提供了一个聚类中的每个点与其他聚类中的点的接近程度的度量。轮廓分数越大,聚类的质量越好。分数越接近 1 表示这些点越接近聚类的质心。分数越接近 0 表示这些点越接近其他聚类,负值表示这些点可能被指定到错误的聚类。
import org.apache.spark.ml.evaluation.ClusteringEvaluator
val evaluator = new ClusteringEvaluator()
val silhouette = evaluator.evaluate(clusters)
silhouette: Double = 0.6722088068201866
基于潜在狄利克雷分配的主题建模
潜在狄利克雷分配(LDA)是由戴维·布雷、吴恩达和迈克尔·乔丹在 2003 年提出的,尽管乔纳森·k·普里查德、马修·斯蒂芬斯和彼得·唐纳利在 2000 年也提出了一种用于群体遗传学的类似算法。应用于机器学习的 LDA 基于图形模型,是基于 GraphX 构建的 Spark MLlib 中包含的第一个算法。潜在狄利克雷分配广泛用于主题建模。主题模型自动导出一组文档中的主题(或话题)(图 4-3 )。这些主题可用于基于内容的推荐、文档分类、维度缩减和特征化。
图 4-3
使用潜在狄利克雷分配按主题对文档进行分组
尽管 Spark MLlib 具有广泛的文本挖掘和预处理功能,但它缺乏大多数企业级 NLP 库 iv 中的几个功能,例如词汇化、词干提取和情感分析等。在本章后面的主题建模示例中,我们将需要其中的一些特性。这是一个介绍斯坦福 CoreNLP for Spark 和约翰斯诺实验室的 Spark NLP 的好时机。
斯坦福 CoreNLP for Spark
斯坦福 CoreNLP 是由斯坦福大学的 NLP 研究小组开发的专业级 NLP 库。CoreNLP 支持多种语言,如阿拉伯语、中文、英语、法语和德语。 v 它提供了一个原生的 Java API,以及 web API 和命令行接口。也有针对主要编程语言(如 R、Python、Ruby 和 Lua)的第三方 API。来自 Databricks 的软件工程师孟祥瑞为 Spark 开发了斯坦福 CoreNLP 包装器(见清单 4-2 )。
spark-shell --packages databricks:spark-corenlp:0.4.0-spark2.4-scala2.11 --jars stanford-corenlp-3.9.1-models.jar
import spark.implicits._
import org.apache.spark.sql.types._
val dataDF = Seq(
(1, "Kevin Durant was the 2019 All-Star NBA Most Valuable Player."),
(2, "Stephen Curry is the best clutch three-point shooter in the NBA."),
(3, "My game is not as good as it was 20 years ago."),
(4, "Michael Jordan is the greatest NBA player of all time."),
(5, "The Lakers currently have one of the worst performances in the NBA."))
.toDF("id", "text")
dataDF.show(false)
+---+-------------------------------------------------------------------+
|id |text |
+---+-------------------------------------------------------------------+
|1 |Kevin Durant was the 2019 All-Star NBA Most Valuable Player. |
|2 |Stephen Curry is the best clutch three-point shooter in the NBA. |
|3 |My game is not as good as it was 20 years ago. |
|4 |Michael Jordan is the greatest NBA player of all time. |
|5 |The Lakers currently have one of the worst performances in the NBA.|
+---+-------------------------------------------------------------------+
// Stanford CoreNLP lets you chain text processing functions. Let's split
// the document into sentences and then tokenize the sentences into words.
import com.databricks.spark.corenlp.functions._
val dataDF2 = dataDF
.select(explode(ssplit('text)).as('sen))
.select('sen, tokenize('sen).as('words))
dataDF2.show(5,30)
+------------------------------+------------------------------+
| sen| words|
+------------------------------+------------------------------+
|Kevin Durant was the 2019 A...|Kevin, Durant, was, the, 2...|
|Stephen Curry is the best c...|[Stephen, Curry, is, the, b...|
|My game is not as good as i...|[My, game, is, not, as, goo...|
|Michael Jordan is the great...|[Michael, Jordan, is, the, ...|
|The Lakers currently have o...|[The, Lakers, currently, ha...|
+------------------------------+------------------------------+
// Perform sentiment analysis on the sentences. The scale
// ranges from 0 for strong negative to 4 for strong positive.
val dataDF3 = dataDF
.select(explode(ssplit('text)).as('sen))
.select('sen, tokenize('sen).as('words), sentiment('sen).as('sentiment))
dataDF3.show(5,30)
+------------------------------+------------------------------+---------+
| sen| words|sentiment|
+------------------------------+------------------------------+---------+
|Kevin Durant was the 2019 A...|[Kevin, Durant, was, the, 2...| 1|
|Stephen Curry is the best c...|[Stephen, Curry, is, the, b...| 3|
|My game is not as good as i...|[My, game, is, not, as, goo...| 1|
|Michael Jordan is the great...|[Michael, Jordan, is, the, ...| 3|
|The Lakers currently have o...|[The, Lakers, currently, ha...| 1|
+------------------------------+------------------------------+---------+
Listing 4-2A Brief Introduction to Stanford CoreNLP for Spark
请访问 Databricks 的 CoreNLP GitHub 页面,获取 Stanford CoreNLP for Spark 的完整功能列表。
约翰·斯诺实验室的 Spark NLP
John Snow Labs 的 Spark NLP 库本身支持 Spark ML 管道 API。它是用 Scala 编写的,包括 Scala 和 Python APIs。它包括几个高级功能,例如记号赋予器、词汇赋予器、词干分析器、实体和日期提取器、词性标记器、句子边界检测、拼写检查器和命名实体识别等等。
标注器在 Spark NLP 中提供了 NLP 功能。注释是 Spark NLP 操作的结果。有两种类型的标注器,标注器方法和标注器模型。注释器方法代表 Spark MLlib 估算器。它用数据拟合一个模型,以产生一个注释器模型或转换器。一个注释器模型是一个转换器,它接受一个数据集并添加一个包含注释结果的列。因为它们被表示为 Spark 估计器和转换器,所以注释器可以很容易地与 Spark 管道 API 集成。Spark NLP 为用户提供了几种访问其功能的方法。[VI
预训练管道
Spark NLP 包括用于快速文本注释的预训练管道。Spark NLP 提供了一个名为 explain_document_ml 的预训练管道,它接受文本作为输入(参见清单 4-3 )。预训练的管道包含流行的文本处理功能,并提供了一种快速和肮脏的方式来使用 Spark NLP,而没有太多的麻烦。
spark-shell --packages JohnSnowLabs:spark-nlp:2.1.0
import com.johnsnowlabs.nlp.pretrained.PretrainedPipeline
val annotations = PretrainedPipeline("explain_document_ml").annotate("I visited Greece last summer. It was a great trip. I went swimming in Mykonos.")
annotations("sentence")
res7: Seq[String] = List(I visited Greece last summer.,
It was a great trip., I went swimming in Mykonos.)
annotations("token")
res8: Seq[String] = List(I, visited, Greece, last, summer, .,
It, was, a, great, trip, ., I, went, swimming, in, Mykonos, .)
annotations("lemma")
res9: Seq[String] = List(I, visit, Greece, last, summer, .,
It, be, a, great, trip, ., I, go, swim, in, Mykonos, .)
Listing 4-3Spark NLP Pre-trained Pipeline Example
带有火花数据帧的预训练管道
预训练的流水线也可以处理 Spark 数据帧,如清单 4-4 所示。
val data = Seq("I visited Greece last summer. It was a great trip. I went swimming in Mykonos.").toDF("text")
val annotations = PretrainedPipeline("explain_document_ml").transform(data)
annotations.show()
+--------------------+--------------------+--------------------+
| text| document| sentence|
+--------------------+--------------------+--------------------+
|I visited Greece ...|[document, 0, 77...|[[document, 0, 28...|
+--------------------+--------------------+--------------------+
+--------------------+
| token|
+--------------------+
|[[token, 0, 0, I,...|
+--------------------+
+--------------------+--------------------+--------------------+
| checked| lemma| stem|
+--------------------+--------------------+--------------------+
|[[token, 0, 0, I,...|[[token, 0, 0, I,...|[[token, 0, 0, i,...|
+--------------------+--------------------+--------------------+
+--------------------+
| pos|
+--------------------+
|[[pos, 0, 0, PRP,...|
+--------------------+
Listing 4-4Spark NLP Pre-trained Pipelines with Spark DataFrames
带 Spark MLlib 管道的预训练管道
您可以将预训练的管道与 Spark MLlib 管道一起使用(参见清单 [4-5 )。请注意,需要一个名为 Finisher 的特殊转换器来以人类可读的格式显示令牌。
import com.johnsnowlabs.nlp.Finisher
import org.apache.spark.ml.Pipeline
val data = Seq("I visited Greece last summer. It was a great trip. I went swimming in Mykonos.").toDF("text")
val finisher = new Finisher()
.setInputCols("sentence", "token", "lemma")
val explainPipeline = PretrainedPipeline("explain_document_ml").model
val pipeline = new Pipeline()
.setStages(Array(explainPipeline,finisher))
pipeline.fit(data).transform(data).show(false)
+--------------------------------------------------+
|text |
+--------------------------------------------------+
|I visited Greece last summer. It was a great trip.|
+--------------------------------------------------+
+----------------------------+
| text |
+----------------------------+
|I went swimming in Mykonos. |
+----------------------------+
+-----------------------------------------------------+
|finished_sentence |
+-----------------------------------------------------+
|[I visited Greece last summer., It was a great trip. |
+-----------------------------------------------------+
+-----------------------------+
| finished_sentence |
+-----------------------------+
|,I went swimming in Mykonos.]|
+-----------------------------+
+-----------------------------------------------------------------+
|finished_token |
+-----------------------------------------------------------------+
|[I, visited, Greece, last, summer, ., It, was, a, great, trip, .,|
+-----------------------------------------------------------------+
+--------------------------------------------------------------+
|finished_lemma |
+--------------------------------------------------------------+
|[I, visit, Greece, last, summer, ., It, be, a, great, trip, .,|
+--------------------------------------------------------------+
+--------------------------------+
| finished_lemma |
+--------------------------------+
|, I, go, swim, in, Mykonos, .] |
+--------------------------------+
Listing 4-5Pre-trained Pipelines with Spark MLlib Pipeline Example
创建自己的 Spark MLlib 管道
您可以直接从自己的 Spark MLlib 管道中使用注释器,如清单 4-6 所示。
import com.johnsnowlabs.nlp.base._
import com.johnsnowlabs.nlp.annotator._
import org.apache.spark.ml.Pipeline
val data = Seq("I visited Greece last summer. It was a great trip. I went swimming in Mykonos.").toDF("text")
val documentAssembler = new DocumentAssembler()
.setInputCol("text")
.setOutputCol("document")
val sentenceDetector = new SentenceDetector()
.setInputCols(Array("document"))
.setOutputCol("sentence")
val regexTokenizer = new Tokenizer()
.setInputCols(Array("sentence"))
.setOutputCol("token")
val finisher = new Finisher()
.setInputCols("token")
.setCleanAnnotations(false)
val pipeline = new Pipeline()
.setStages(Array(documentAssembler,
sentenceDetector,regexTokenizer,finisher))
pipeline.fit(Seq.empty[String].toDF("text"))
.transform(data)
.show()
+--------------------+--------------------+--------------------+
| text| document| sentence|
+--------------------+--------------------+--------------------+
|I visited Greece ...|[document, 0, 77...|[[document, 0, 28...|
+--------------------+--------------------+--------------------+
+--------------------+--------------------+
| token| finished_token|
+--------------------+--------------------+
|[[token, 0, 0, I,...|[I, visited, Gree...|
+--------------------+--------------------+
Listing 4-6Creating Your Own Spark MLlib Pipeline Example
Spark NLP 光管道
Spark NLP 提供了另一类管道,称为 LightPipeline。它类似于 Spark MLlib 管道,但不是利用 Spark 的分布式处理能力,而是在本地执行。当处理少量数据并且需要低延迟执行时,LightPipeline 是合适的(参见清单 [4-7 )。
import com.johnsnowlabs.nlp.base._
val trainedModel = pipeline.fit(Seq.empty[String].toDF("text"))
val lightPipeline = new LightPipeline(trainedModel)
lightPipeline.annotate("I visited Greece last summer.")
Listing 4-7Spark NLP LightPipelines Example
Spark NLP OCR 模块
Spark NLP 包括一个 OCR 模块,允许用户从 PDF 文件创建 Spark 数据帧。OCR 模块不包含在核心 Spark NLP 库中。要使用它,您需要包含一个单独的包并指定一个额外的库,正如您在清单 4-8 中我的 spark-shell 命令中看到的。
spark-shell --packages JohnSnowLabs:spark-nlp:2.1.0,com.johnsnowlabs.nlp:spark-nlp-ocr_2.11:2.1.0,javax.media.jai:com.springsource.javax.media.jai.core:1.1.3
--repositories http://repo.spring.io/plugins-release
import com.johnsnowlabs.nlp.util.io.OcrHelper
val myOcrHelper = new OcrHelper
val data = myOcrHelper.createDataset(spark, "/my_pdf_files/")
val documentAssembler = new DocumentAssembler().setInputCol("text")
documentAssembler.transform(data).select("text","filename").show(1,45)
+------------------------------------------+
| text|
+------------------------------------------+
|this is a PDF document. Have a great day. |
+------------------------------------------+
+--------------------------------------------+
| filename |
+--------------------------------------------+
|file:/my_pdf_files/document.pdf |
+--------------------------------------------+
Listing 4-8Spark NLP OCR Module Example
Spark NLP 是一个强大的库,它包含了本简介中没有涉及的许多特性。要了解更多关于 Spark NLP 的信息,请访问 http://nlp.johnsnowlabs.com
。
例子
现在我们已经拥有了所有需要的东西,可以继续我们的主题建模例子了。我们将使用潜在的狄利克雷分配对超过 100 万条新闻标题进行分类,这些标题是在 15 年的时间内发布的。该数据集可从 Kaggle 下载,由澳大利亚广播公司提供,并由 Rohit Kulkarni 提供。
我们可以使用 John Snow Labs 的 Spark NLP 或 Stanford CoreNLP 软件包为我们提供额外的文本处理能力。对于这个例子,我们将使用斯坦福 CoreNLP 包(参见清单 4-9 )。
spark-shell --packages databricks:spark-corenlp:0.4.0-spark2.4-scala2.11 --jars stanford-corenlp-3.9.1-models.jar
import org.apache.spark.sql.functions._
import org.apache.spark.sql.types._
import org.apache.spark.sql._
// Define the schema.
var newsSchema = StructType(Array (
StructField("publish_date", IntegerType, true),
StructField("headline_text", StringType, true)
))
// Read the data.
val dataDF = spark.read.format("csv")
.option("header", "true")
.schema(newsSchema)
.load("abcnews-date-text.csv")
// Inspect the data.
dataDF.show(false)
+------------+--------------------------------------------------+
|publish_date|headline_text |
+------------+--------------------------------------------------+
|20030219 |aba decides against community broadcasting licence|
|20030219 |act fire witnesses must be aware of defamation |
|20030219 |a g calls for infrastructure protection summit |
|20030219 |air nz staff in aust strike for pay rise |
|20030219 |air nz strike to affect australian travellers |
|20030219 |ambitious olsson wins triple jump |
|20030219 |antic delighted with record breaking barca |
|20030219 |aussie qualifier stosur wastes four memphis match |
|20030219 |aust addresses un security council over iraq |
|20030219 |australia is locked into war timetable opp |
|20030219 |australia to contribute 10 million in aid to iraq |
|20030219 |barca take record as robson celebrates birthday in|
|20030219 |bathhouse plans move ahead |
|20030219 |big hopes for launceston cycling championship |
|20030219 |big plan to boost paroo water supplies |
|20030219 |blizzard buries united states in bills |
|20030219 |brigadier dismisses reports troops harassed in |
|20030219 |british combat troops arriving daily in kuwait |
|20030219 |bryant leads lakers to double overtime win |
|20030219 |bushfire victims urged to see centrelink |
+------------+--------------------------------------------------+
only showing top 20 rows
// Remove punctuations.
val dataDF2 = dataDF
.withColumn("headline_text",
regexp_replace((dataDF("headline_text")), "[^a-zA-Z0-9 ]", ""))
// We will use Stanford CoreNLP to perform lemmatization. As discussed
// earlier, lemmatization derives the root form of inflected words. For
// example, "camping", "camps", "camper", and "camped" are all inflected
// forms of "camp". Reducing inflected words to its root form helps reduce // the complexity of performing natural language processing. A similar
// process known as stemming also reduces inflected words to their root
// form, but it does so by crudely chopping off affixes, even though the
// root form may not be a valid word. In contrast, lemmatization ensures
// that the inflected words are reduced to a valid root word through the
// morphological analysis of words and the use of a vocabulary.vii
import com.databricks.spark.corenlp.functions._
val dataDF3 = dataDF2
.select(explode(ssplit('headline_text)).as('sen))
.select('sen, lemma('sen)
.as('words))
dataDF3.show
+--------------------+--------------------+
| sen| words|
+--------------------+--------------------+
|aba decides again...|[aba, decide, aga...|
|act fire witnesse...|[act, fire, witne...|
|a g calls for inf...|[a, g, call, for,...|
|air nz staff in a...|[air, nz, staff, ...|
|air nz strike to ...|[air, nz, strike,...|
|ambitious olsson ...|[ambitious, olsso...|
|antic delighted w...|[antic, delighted...|
|aussie qualifier ...|[aussie, qualifie...|
|aust addresses un...|[aust, address, u...|
|australia is lock...|[australia, be, l...|
|australia to cont...|[australia, to, c...|
|barca take record...|[barca, take, rec...|
|bathhouse plans m...|[bathhouse, plan,...|
|big hopes for lau...|[big, hope, for, ...|
|big plan to boost...|[big, plan, to, b...|
|blizzard buries u...|[blizzard, bury, ...|
|brigadier dismiss...|[brigadier, dismi...|
|british combat tr...|[british, combat,...|
|bryant leads lake...|[bryant, lead, la...|
|bushfire victims ...|[bushfire, victim...|
+--------------------+--------------------+
only showing top 20 rows
// We’ll remove stop words such as “a”, “be”, and “to”. Stop
// words have no contribution to the meaning of a document.
import org.apache.spark.ml.feature.StopWordsRemover
val remover = new StopWordsRemover()
.setInputCol("words")
.setOutputCol("filtered_stopwords")
val dataDF4 = remover.transform(dataDF3)
dataDF4.show
+--------------------+--------------------+--------------------+
| sen| words| filtered_stopwords|
+--------------------+--------------------+--------------------+
|aba decides again...|[aba, decide, aga...|[aba, decide, com...|
|act fire witnesse...|[act, fire, witne...|[act, fire, witne...|
|a g calls for inf...|[a, g, call, for,...|[g, call, infrast...|
|air nz staff in a...|[air, nz, staff, ...|[air, nz, staff, ...|
|air nz strike to ...|[air, nz, strike,...|[air, nz, strike,...|
|ambitious olsson ...|[ambitious, olsso...|[ambitious, olsso...|
|antic delighted w...|[antic, delighted...|[antic, delighted...|
|aussie qualifier ...|[aussie, qualifie...|[aussie, qualifie...|
|aust addresses un...|[aust, address, u...|[aust, address, u...|
|australia is lock...|[australia, be, l...|[australia, lock,...|
|australia to cont...|[australia, to, c...|[australia, contr...|
|barca take record...|[barca, take, rec...|[barca, take, rec...|
|bathhouse plans m...|[bathhouse, plan,...|[bathhouse, plan,...|
|big hopes for lau...|[big, hope, for, ...|[big, hope, launc...|
|big plan to boost...|[big, plan, to, b...|[big, plan, boost...|
|blizzard buries u...|[blizzard, bury, ...|[blizzard, bury, ...|
|brigadier dismiss...|[brigadier, dismi...|[brigadier, dismi...|
|british combat tr...|[british, combat,...|[british, combat,...|
|bryant leads lake...|[bryant, lead, la...|[bryant, lead, la...|
|bushfire victims ...|[bushfire, victim...|[bushfire, victim...|
+--------------------+--------------------+--------------------+
only showing top 20 rows
// Generate n-grams. n-grams are a sequence of “n” number of words often
// used to discover the relationship of words in a document. For example,
// “Los Angeles” is a bigram. “Los” and “Angeles” are unigrams. “Los” and // “Angeles” when considered as individual units may not mean much, but it // is more meaningful when combined as a single entity “Los Angeles”.
// Determining the optimal number of “n” is dependent on the use case
// and the language used in the document.viii For our example, we'll generate // a unigram, bigram, and trigram.
import org.apache.spark.ml.feature.NGram
val unigram = new NGram()
.setN(1)
.setInputCol("filtered_stopwords")
.setOutputCol("unigram_words")
val dataDF5 = unigram.transform(dataDF4)
dataDF5.printSchema
root
|-- sen: string (nullable = true)
|-- words: array (nullable = true)
| |-- element: string (containsNull = true)
|-- filtered_stopwords: array (nullable = true)
| |-- element: string (containsNull = true)
|-- unigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
val bigram = new NGram()
.setN(2)
.setInputCol("filtered_stopwords")
.setOutputCol("bigram_words")
val dataDF6 = bigram.transform(dataDF5)
dataDF6.printSchema
root
|-- sen: string (nullable = true)
|-- words: array (nullable = true)
| |-- element: string (containsNull = true)
|-- filtered_stopwords: array (nullable = true)
| |-- element: string (containsNull = true)
|-- unigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- bigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
val trigram = new NGram()
.setN(3)
.setInputCol("filtered_stopwords")
.setOutputCol("trigram_words")
val dataDF7 = trigram.transform(dataDF6)
dataDF7.printSchema
root
|-- sen: string (nullable = true)
|-- words: array (nullable = true)
| |-- element: string (containsNull = true)
|-- filtered_stopwords: array (nullable = true)
| |-- element: string (containsNull = true)
|-- unigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- bigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- trigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
// We combine the unigram, bigram, and trigram into a single vocabulary
.
// We will concatenate and store the words in a column “ngram_words”
// using Spark SQL.
dataDF7.createOrReplaceTempView("dataDF7")
val dataDF8 = spark.sql("select sen,words,filtered_stopwords,unigram_words,bigram_words,trigram_words,concat(concat(unigram_words,bigram_words),trigram_words) as ngram_words from dataDF7")
dataDF8.printSchema
root
|-- sen: string (nullable = true)
|-- words: array (nullable = true)
| |-- element: string (containsNull = true)
|-- filtered_stopwords: array (nullable = true)
| |-- element: string (containsNull = true)
|-- unigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- bigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- trigram_words: array (nullable = true)
| |-- element: string (containsNull = false)
|-- ngram_words: array (nullable = true)
| |-- element: string (containsNull = false)
dataDF8.select("ngram_words").show(20,65)
+-----------------------------------------------------------------+
|ngram_words |
+-----------------------------------------------------------------+
|[aba, decide, community, broadcasting, licence, aba decide, de...|
|[act, fire, witness, must, aware, defamation, act fire, fire w...|
|[g, call, infrastructure, protection, summit, g call, call inf...|
|[air, nz, staff, aust, strike, pay, rise, air nz, nz staff, st...|
|[air, nz, strike, affect, australian, traveller, air nz, nz st...|
|[ambitious, olsson, win, triple, jump, ambitious olsson, olsso...|
|[antic, delighted, record, break, barca, antic delighted, deli...|
|[aussie, qualifier, stosur, waste, four, memphis, match, aussi...|
|[aust, address, un, security, council, iraq, aust address, add...|
|[australia, lock, war, timetable, opp, australia lock, lock wa...|
|[australia, contribute, 10, million, aid, iraq, australia cont...|
|[barca, take, record, robson, celebrate, birthday, barca take,...|
|[bathhouse, plan, move, ahead, bathhouse plan, plan move, move...|
|[big, hope, launceston, cycling, championship, big hope, hope ...|
|[big, plan, boost, paroo, water, supplies, big plan, plan boos...|
|[blizzard, bury, united, state, bill, blizzard bury, bury unit...|
|[brigadier, dismiss, report, troops, harass, brigadier dismiss...|
|[british, combat, troops, arrive, daily, kuwait, british comba...|
|[bryant, lead, laker, double, overtime, win, bryant lead, lead...|
|[bushfire, victim, urge, see, centrelink, bushfire victim, vic...|
+-----------------------------------------------------------------+
only showing top 20 rows
// Use CountVectorizer to convert the text data to vectors of token counts
.
import org.apache.spark.ml.feature.{CountVectorizer, CountVectorizerModel}
val cv = new CountVectorizer()
.setInputCol("ngram_words")
.setOutputCol("features")
val cvModel = cv.fit(dataDF8)
val dataDF9 = cvModel.transform(dataDF8)
val vocab = cvModel.vocabulary
vocab: Array[String] = Array(police, man, new, say, plan, charge, call,
council, govt, fire, court, win, interview, back, kill, australia, find,
death, urge, face, crash, nsw, report, water, get, australian, qld, take,
woman, wa, attack, sydney, year, change, murder, hit, health, jail, claim,
day, child, miss, hospital, car, home, sa, help, open, rise, warn, school,
world, market, cut, set, accuse, die, seek, drug, make, boost, may, coast,
government, ban, job, group, fear, mp, two, talk, service, farmer, minister, election, fund, south, road, continue, lead, worker, first, national, test, arrest, work, rural, go, power, price, cup, final, concern, green, china, mine, fight, labor, trial, return, flood, deal, north, case, push, pm, melbourne, law, driver, one, nt, want, centre, record, ...
// We use IDF to scale the features generated by CountVectorizer.
// Scaling features generally improves performance.
import org.apache.spark.ml.feature.IDF
val idf = new IDF()
.setInputCol("features")
.setOutputCol("features2")
val idfModel = idf.fit(dataDF9)
val dataDF10 = idfModel.transform(dataDF9)
dataDF10.select("features2").show(20,65)
+-----------------------------------------------------------------+
| features2 |
+-----------------------------------------------------------------+
|(262144,[154,1054,1140,15338,19285],[5.276861439995834,6.84427...|
|(262144,[9,122,711,727,3141,5096,23449],[4.189486226673463,5.1...|
|(262144,[6,734,1165,1177,1324,43291,96869],[4.070620900306447,...|
|(262144,[48,121,176,208,321,376,424,2183,6231,12147,248053],[4...|
|(262144,[25,176,208,376,764,3849,12147,41079,94670,106284],[4....|
|(262144,[11,1008,1743,10833,128493,136885],[4.2101466208496285...|
|(262144,[113,221,3099,6140,9450,16643],[5.120230688038215,5.54...|
|(262144,[160,259,483,633,1618,4208,17750,187744],[5.3211036079...|
|(262144,[7,145,234,273,321,789,6163,10334,11101,32988],[4.0815...|
|(262144,[15,223,1510,5062,5556],[4.393970862600795,5.555011224...|
|(262144,[15,145,263,372,541,3896,15922,74174,197210],[4.393970...|
|(262144,[27,113,554,1519,3099,13499,41664,92259],[4.5216508634...|
|(262144,[4,131,232,5636,6840,11444,37265],[3.963488754657374,5...|
|(262144,[119,181,1288,1697,2114,49447,80829,139670],[5.1266204...|
|(262144,[4,23,60,181,2637,8975,9664,27571,27886],[3.9634887546...|
|(262144,[151,267,2349,3989,7631,11862],[5.2717309555002725,5.6...|
|(262144,[22,513,777,12670,33787,49626],[4.477068652869369,6.16...|
|(262144,[502,513,752,2211,5812,7154,30415,104812],[6.143079025...|
|(262144,[11,79,443,8222,8709,11447,194715],[4.2101466208496285...|
|(262144,[18,146,226,315,2877,5160,19389,42259],[4.414350240692...|
+-----------------------------------------------------------------+
only showing top 20 rows
// The scaled features could then be passed to LDA
.
import org.apache.spark.ml.clustering.LDA
val lda = new LDA()
.setK(30)
.setMaxIter(10)
val model = lda.fit(dataDF10)
val topics = model.describeTopics
topics.show(20,30)
+-----+------------------------------+------------------------------+
|topic| termIndices| termWeights|
+-----+------------------------------+------------------------------+
| 0|[2, 7, 16, 9482, 9348, 5, 1...|[1.817876125380732E-4, 1.09...|
| 1|[974, 2, 3, 5189, 5846, 541...|[1.949552388785536E-4, 1.89...|
| 2|[2253, 4886, 12, 6767, 3039...|[2.7922272919208327E-4, 2.4...|
| 3|[6218, 6313, 5762, 3387, 27...|[1.6618313204146235E-4, 1.6...|
| 4|[0, 1, 39, 14, 13, 11, 2, 1...|[1.981809243111437E-4, 1.22...|
| 5|[4, 7, 22, 11, 2, 3, 79, 92...|[2.49620962563534E-4, 2.032...|
| 6|[15, 32, 319, 45, 342, 121,...|[2.885684164769467E-5, 2.45...|
| 7|[2298, 239, 1202, 3867, 431...|[3.435238376348344E-4, 3.30...|
| 8|[0, 4, 110, 3, 175, 38, 8, ...|[1.0177738516279581E-4, 8.7...|
| 9|[1, 19, 10, 2, 7, 8, 5, 0, ...|[2.2854683602607976E-4, 1.4...|
| 10|[1951, 1964, 16, 33, 1, 5, ...|[1.959705576881449E-4, 1.92...|
| 11|[12, 89, 72, 3, 92, 63, 62,...|[4.167255720848278E-5, 3.19...|
| 12|[4, 23, 13, 22, 73, 18, 70,...|[1.1641833113477034E-4, 1.1...|
| 13|[12, 1, 5, 16, 185, 132, 24...|[0.008769073702733892, 0.00...|
| 14|[9151, 13237, 3140, 14, 166...|[8.201099412213086E-5, 7.85...|
| 15|[9, 1, 0, 11, 3, 15, 32, 52...|[0.0032039727688580703, 0.0...|
| 16|[1, 10, 5, 56, 27, 3, 16, 1...|[5.252120584885086E-5, 4.05...|
| 17|[12, 1437, 4119, 1230, 5303...|[5.532790361864421E-4, 2.97...|
| 18|[12, 2459, 7836, 8853, 7162...|[6.862552774818539E-4, 1.83...|
| 19|[21, 374, 532, 550, 72, 773...|[0.0024665346250921432, 0.0...|
+-----+------------------------------+------------------------------+
only showing top 20 rows
// Determine the max size of the vocabulary.
model.vocabSize
res27: Int = 262144
// Extract the topic words. The describeTopics method returns the
// dictionary indices from CountVectorizer's output. We will use a custom // user-defined function to map the words to the indices.ix
import scala.collection.mutable.WrappeddArray
import org.apache.spark.sql.functions.udf
val extractWords = udf( (x : WrappedArray[Int]) => { x.map(i => vocab(i)) })
val topics = model
.describeTopics
.withColumn("words", extractWords(col("termIndices")))
topics.select("topic","termIndices","words").show(20,30)
+-----+------------------------------+------------------------------+
|topic| termIndices| words|
+-----+------------------------------+------------------------------+
| 0|[2, 7, 16, 9482, 9348, 5, 1...|[new, council, find, abuse ...|
| 1|[974, 2, 3, 5189, 5846, 541...|[2016, new, say, china sea,...|
| 2|[2253, 4886, 12, 6767, 3039...|[nathan, interview nathan, ...|
| 3|[6218, 6313, 5762, 3387, 27...|[new guinea, papua new guin...|
| 4|[0, 1, 39, 14, 13, 11, 2, 1...|[police, man, day, kill, ba...|
| 5|[4, 7, 22, 11, 2, 3, 79, 92...|[plan, council, report, win...|
| 6|[15, 32, 319, 45, 342, 121,...|[australia, year, india, sa...|
| 7|[2298, 239, 1202, 3867, 431...|[sach, tour, de, tour de, d...|
| 8|[0, 4, 110, 3, 175, 38, 8, ...|[police, plan, nt, say, fun...|
| 9|[1, 19, 10, 2, 7, 8, 5, 0, ...|[man, face, court, new, cou...|
| 10|[1951, 1964, 16, 33, 1, 5, ...|[vic country, vic country h...|
| 11|[12, 89, 72, 3, 92, 63, 62,...|[interview, price, farmer, ...|
| 12|[4, 23, 13, 22, 73, 18, 70,...|[plan, water, back, report,...|
| 13|[12, 1, 5, 16, 185, 132, 24...|[interview, man, charge, fi...|
| 14|[9151, 13237, 3140, 14, 166...|[campese, interview terry, ...|
| 15|[9, 1, 0, 11, 3, 15, 32, 52...|[fire, man, police, win, sa...|
| 16|[1, 10, 5, 56, 27, 3, 16, 1...|[man, court, charge, die, t...|
| 17|[12, 1437, 4119, 1230, 5303...|[interview, redback, 666, s...|
| 18|[12, 2459, 7836, 8853, 7162...|[interview, simon, intervie...|
| 19|[21, 374, 532, 550, 72, 773...|[nsw, asylum, seeker, asylu...|
+-----+------------------------------+------------------------------+
only showing top 20 rows
// Extract the term weights from describeTopics.
val wordsWeight = udf( (x : WrappedArray[Int],
y : WrappedArray[Double]) =>
{ x.map(i => vocab(i)).zip(y)}
)
val topics2 = model
.describeTopics
.withColumn("words", wordsWeight(col("termIndices"), col("termWeights")))
val topics3 = topics2
.select("topic", "words")
.withColumn("words", explode(col("words")))
topics3.show(50,false)
+-----+------------------------------------------------+
|topic|words |
+-----+------------------------------------------------+
|0 |[new, 1.4723785654465323E-4] |
|0 |[council, 1.242876719889358E-4] |
|0 |[thursday, 1.1710009304019913E-4] |
|0 |[grandstand thursday, 1.0958369194828903E-4] |
|0 |[two, 8.119593156862581E-5] |
|0 |[charge, 7.321024120305904E-5] |
|0 |[find, 6.98723717903146E-5] |
|0 |[burley griffin, 6.474176573486395E-5] |
|0 |[claim, 6.448801852215021E-5] |
|0 |[burley, 6.390953777977556E-5] |
|1 |[say, 1.9595383103126804E-4] |
|1 |[new, 1.7986957579978078E-4] |
|1 |[murder, 1.7156446166835784E-4] |
|1 |[las, 1.6793241095301546E-4] |
|1 |[vegas, 1.6622904053495525E-4] |
|1 |[las vegas, 1.627321199362179E-4] |
|1 |[2016, 1.4906599207615762E-4] |
|1 |[man, 1.3653760511354596E-4] |
|1 |[call, 1.3277357539424398E-4] |
|1 |[trump, 1.250570735309821E-4] |
|2 |[ntch, 5.213678388314454E-4] |
|2 |[ntch podcast, 4.6907569870744537E-4] |
|2 |[podcast, 4.625754070258578E-4] |
|2 |[interview, 1.2297477650126824E-4] |
|2 |[trent, 9.319817855283612E-5] |
|2 |[interview trent, 8.967384560094343E-5] |
|2 |[trent robinson, 7.256857525120274E-5] |
|2 |[robinson, 6.888930961680287E-5] |
|2 |[interview trent robinson, 6.821800839623336E-5]|
|2 |[miss, 6.267572268770148E-5] |
|3 |[new, 8.244153432249302E-5] |
|3 |[health, 5.269269109549137E-5] |
|3 |[change, 5.1481361386635024E-5] |
|3 |[first, 3.474601129571304E-5] |
|3 |[south, 3.335342687995096E-5] |
|3 |[rise, 3.3245575277669534E-5] |
|3 |[country, 3.26422466284622E-5] |
|3 |[abuse, 3.25594250748893E-5] |
|3 |[start, 3.139959761950907E-5] |
|3 |[minister, 3.1327427652213426E-5] |
|4 |[police, 1.756612187665565E-4] |
|4 |[man, 1.2903801461819285E-4] |
|4 |[petero, 8.259870531430337E-5] |
|4 |[kill, 8.251557569137285E-5] |
|4 |[accuse grant, 8.187325944352362E-5] |
|4 |[accuse grant bail, 7.609807356711693E-5] |
|4 |[find, 7.219731162848223E-5] |
|4 |[attack, 6.804063612991027E-5] |
|4 |[day, 6.772554893634948E-5] |
|4 |[jail, 6.470525327671485E-5] |
+-----+------------------------------------------------+
only showing top 50 rows
// Finally, we split the word and the weight
into separate fields.
val topics4 = topics3
.select(col("topic"), col("words")
.getField("_1").as("word"), col("words")
.getField("_2").as("weight"))
topics4.show(50, false)
+-----+------------------------+---------------------+
|topic|word |weight |
+-----+------------------------+---------------------+
|0 |new |1.4723785654465323E-4|
|0 |council |1.242876719889358E-4 |
|0 |thursday |1.1710009304019913E-4|
|0 |grandstand thursday |1.0958369194828903E-4|
|0 |two |8.119593156862581E-5 |
|0 |charge |7.321024120305904E-5 |
|0 |find |6.98723717903146E-5 |
|0 |burley griffin |6.474176573486395E-5 |
|0 |claim |6.448801852215021E-5 |
|0 |burley |6.390953777977556E-5 |
|1 |say |1.9595383103126804E-4|
|1 |new |1.7986957579978078E-4|
|1 |murder |1.7156446166835784E-4|
|1 |las |1.6793241095301546E-4|
|1 |vegas |1.6622904053495525E-4|
|1 |las vegas |1.627321199362179E-4 |
|1 |2016 |1.4906599207615762E-4|
|1 |man |1.3653760511354596E-4|
|1 |call |1.3277357539424398E-4|
|1 |trump |1.250570735309821E-4 |
|2 |ntch |5.213678388314454E-4 |
|2 |ntch podcast |4.6907569870744537E-4|
|2 |podcast |4.625754070258578E-4 |
|2 |interview |1.2297477650126824E-4|
|2 |trent |9.319817855283612E-5 |
|2 |interview trent |8.967384560094343E-5 |
|2 |trent robinson |7.256857525120274E-5 |
|2 |robinson |6.888930961680287E-5 |
|2 |interview trent robinson|6.821800839623336E-5 |
|2 |miss |6.267572268770148E-5 |
|3 |new |8.244153432249302E-5 |
|3 |health |5.269269109549137E-5 |
|3 |change |5.1481361386635024E-5|
|3 |first |3.474601129571304E-5 |
|3 |south |3.335342687995096E-5 |
|3 |rise |3.3245575277669534E-5|
|3 |country |3.26422466284622E-5 |
|3 |abuse |3.25594250748893E-5 |
|3 |start |3.139959761950907E-5 |
|3 |minister |3.1327427652213426E-5|
|4 |police |1.756612187665565E-4 |
|4 |man |1.2903801461819285E-4|
|4 |petero |8.259870531430337E-5 |
|4 |kill |8.251557569137285E-5 |
|4 |accuse grant |8.187325944352362E-5 |
|4 |accuse grant bail |7.609807356711693E-5 |
|4 |find |7.219731162848223E-5 |
|4 |attack |6.804063612991027E-5 |
|4 |day |6.772554893634948E-5 |
|4 |jail |6.470525327671485E-5 |
+-----+------------------------+---------------------+
only showing top 50 rows
Listing 4-9Topic Modeling with LDA
为了简洁起见,我只显示了前 50 行,显示了 30 个主题中的 4 个。如果你仔细检查每个主题中的单词,你会发现重复的主题可以用来对标题进行分类。
隔离森林异常检测
异常或异常值检测可识别出明显偏离大多数数据集的罕见观察值。它经常用于发现欺诈性金融交易、识别网络安全威胁或执行预测性维护,仅举几个使用案例。异常检测是机器学习领域的一个热门研究领域。多年来,已经发明了几种异常检测技术,其效果各不相同。在这一章中,我将介绍一种最有效的异常检测技术,叫做隔离森林。隔离森林是一种基于树的集成算法,用于异常检测,该算法是由刘飞东尼、婷和开发的。 x
与大多数异常检测技术不同,隔离林试图明确检测实际的异常值,而不是识别正常的数据点。隔离林的运行基于这样一个事实,即数据集中通常存在少量异常值,因此易于进行隔离。【Xi】从正常数据点中分离异常值是有效的,因为它需要较少的条件。相比之下,隔离正常数据点通常会涉及更多条件。如图 4-4 (b)所示,异常数据点只用一个分区隔离,而正常数据点用了五个分区隔离。当数据被表示为树形结构时,异常更有可能在比正常数据点浅得多的深度上靠近根节点。如图 4-4 (a)所示,离群点(8,12)的树深度为 1,而正常数据点(9,15)的树深度为 5。
隔离林不需要要素缩放,因为用于检测异常值的距离阈值是基于树深度的。它适用于大型和小型数据集,并且不需要训练数据集,因为它是一种无监督的学习技术。XIII
图 4-4
用隔离林隔离异常和正常数据点 xii 所需的分区数
与其他基于树的集合类似,隔离林建立在称为隔离树的决策树集合上,每棵树都有整个数据集的子集。异常分数被计算为森林中树木的平均异常分数。异常分值来自分割数据点所需的条件数量。接近 1 的异常分数表示异常,而低于 0.5 的分数表示非异常观察(图 4-5 )。
图 4-5
用隔离林探测异常XIV
隔离林在准确性和性能方面都优于其他异常检测方法。图 4-6 和 4-7 显示了隔离森林与单类支持向量机(另一种众所周知的离群点检测算法)的性能比较。 xv 第一个测试针对属于单个组的正常观察值评估了两种算法(图 4-6 ),而第二个测试针对属于两个不均匀聚类的观察值评估了两种算法(图 4-7 )。在这两种情况下,隔离森林的表现优于单类支持向量机。
图 4-7
隔离森林与单类支持向量机——不均匀聚类(图片由 Alejandro Correa Bahnsen 提供)
图 4-6
隔离森林与单类支持向量机——正常观察,单组(图片由 Alejandro Correa Bahnsen 提供)
Spark-iForest 是杨在 Jie Fang 和几个贡献者的帮助下开发的 Spark 中隔离森林算法的一个实现。它作为外部第三方包提供,不包含在标准的 Apache Spark MLlib 库中。你可以通过访问 Spark-iForest GitHub 页面 https://github.com/titicaca/spark-iforest
找到更多关于 Spark-iForest 的信息以及最新的 JAR 文件。 十六世
因素
这是 Spark-iForest 支持的参数列表。如您所见,一些参数类似于其他基于树的集合,如随机森林。
-
maxFeatures :从数据中抽取的特征数,用来训练每棵树(> 0)。如果 maxFeatures < = 1,该算法将绘制 max features÷total features 要素。如果 maxFeatures >为 1,该算法将绘制 maxFeatures 特征。
-
maxDepth :构造一棵树的高度限制(> 0)。默认值大约为 log2(numSamples)。
-
numTrees:I forest 模型中的树的数量(> 0)。
-
maxSamples :从数据中抽取训练每棵树的样本数(> 0)。如果 maxSamples < = 1,该算法将绘制 max samples÷total sample 样本。如果 maxSamples >为 1,该算法将绘制 maxSamples 样本。总内存大约是 max samples÷num trees÷4+max samples÷8 字节。
-
污染:数据集中异常值的比例;该值应该在(0,1)中。它仅在预测阶段用于将异常分值转换为预测标签。为了提高性能,我们采用近似分位数来计算异常分值阈值。您可以将 param approxquantilerelativerror 设置为大于 0,以便计算大型数据集异常分值的近似分位数阈值。
-
approxquantilerrelativeerror:近似分位数计算的相对误差(0<= value<= 1);对于计算精确值,默认值为 0,这对于大型数据集来说代价很高。
-
bootstrap :如果为真,则个体树适合于用替换采样的训练数据的随机子集。如果为假,则执行无替换的采样。
-
种子:随机数生成器使用的种子。
-
Features coll:Features 列名,默认为“Features”。
-
Anomaly scoreCol :异常分值列名,默认为“anomalyScore”。
-
predictionCol :预测列名,默认为“预测”。XVII
例子
我们将使用 Spark-iForest 来预测乳腺癌的发生率(列表 4-10 ),使用威斯康星州乳腺癌数据集(表 4-2 ),可从 UCI 机器学习资源库获得。XVIII
表 4-2
威斯康星乳腺癌数据集
|索引
|
特征
|
领域
|
| --- | --- | --- |
| one | 样本代码编号 | 识别号 |
| Two | 团块厚度 | 1–10 |
| three | 细胞大小的均匀性 | 1–10 |
| four | 细胞形状的均匀性 | 1–10 |
| five | 边缘粘连 | 1–10 |
| six | 单一上皮细胞大小 | 1–10 |
| seven | 裸核 | 1–10 |
| eight | 平淡的染色质 | 1–10 |
| nine | 正常核仁 | 1–10 |
| Ten | 神话故事 | 1–10 |
| Eleven | 班级 | (良性 2 例,恶性 4 例) |
spark-shell --jars spark-iforest-1.0-SNAPSHOT.jar
import org.apache.spark.sql.types._
var dataSchema = StructType(Array(
StructField("id", IntegerType, true),
StructField("clump_thickness", IntegerType, true),
StructField("ucell_size", IntegerType, true),
StructField("ucell_shape", IntegerType, true),
StructField("marginal_ad", IntegerType, true),
StructField("se_cellsize", IntegerType, true),
StructField("bare_nuclei", IntegerType, true),
StructField("bland_chromatin", IntegerType, true),
StructField("normal_nucleoli", IntegerType, true),
StructField("mitosis", IntegerType, true),
StructField("class", IntegerType, true)
))
val dataDF = spark.read.option("inferSchema", "true")
.schema(dataSchema)
.csv("/files/breast-cancer-wisconsin.csv")
dataDF.printSchema
//The dataset contain 16 rows with missing attribute values.
//We'll remove them for this exercise.
val dataDF2 = dataDF.filter("bare_nuclei is not null")
val seed = 1234
val Array(trainingData, testData) = dataDF2.randomSplit(Array(0.8, 0.2), seed)
import org.apache.spark.ml.feature.StringIndexer
val labelIndexer = new StringIndexer().setInputCol("class").setOutputCol("label")
import org.apache.spark.ml.feature.VectorAssembler
val assembler = new VectorAssembler()
.setInputCols(Array("clump_thickness",
"ucell_size", "ucell_shape", "marginal_ad", "se_cellsize", "bare_nuclei", "bland_chromatin", "normal_nucleoli", "mitosis"))
.setOutputCol("features")
import org.apache.spark.ml.iforest._
val iForest = new IForest()
.setMaxSamples(150)
.setContamination(0.30)
.setBootstrap(false)
.setSeed(seed)
.setNumTrees(100)
.setMaxDepth(50)
val pipeline = new Pipeline()
.setStages(Array(labelIndexer, assembler, iForest))
val model = pipeline.fit(trainingData)
val predictions = model.transform(testData)
predictions.select("id","features","anomalyScore","prediction").show()
+------+--------------------+-------------------+----------+
| id| features| anomalyScore|prediction|
+------+--------------------+-------------------+----------+
| 63375|9.0,1.0,2.0,6.0,...| 0.6425205920636737| 1.0|
| 76389|[10.0,4.0,7.0,2.0...| 0.6475157383643779| 1.0|
| 95719|[6.0,10.0,10.0,10...| 0.6413247885878359| 1.0|
|242970|[5.0,7.0,7.0,1.0,...| 0.6156526231532693| 1.0|
|353098|[4.0,1.0,1.0,2.0,...|0.45686731187686386| 0.0|
|369565|[4.0,1.0,1.0,1.0,...|0.45957810648090186| 0.0|
|390840|[8.0,4.0,7.0,1.0,...| 0.6387497388682214| 1.0|
|412300|[10.0,4.0,5.0,4.0...| 0.6104797020175959| 1.0|
|466906|[1.0,1.0,1.0,1.0,...|0.41857428772927696| 0.0|
|476903|[10.0,5.0,7.0,3.0...| 0.6152957125696049| 1.0|
|486283|[3.0,1.0,1.0,1.0,...|0.47218763124223706| 0.0|
|557583|[5.0,10.0,10.0,10...| 0.6822227844447365| 1.0|
|636437|[1.0,1.0,1.0,1.0,...|0.41857428772927696| 0.0|
|654244|[1.0,1.0,1.0,1.0,...| 0.4163657637214968| 0.0|
|657753|[3.0,1.0,1.0,4.0,...|0.49314746153500594| 0.0|
|666090|[1.0,1.0,1.0,1.0,...|0.45842258207090547| 0.0|
|688033|[1.0,1.0,1.0,1.0,...|0.41857428772927696| 0.0|
|690557|[5.0,1.0,1.0,1.0,...| 0.4819098604217553| 0.0|
|704097|[1.0,1.0,1.0,1.0,...| 0.4163657637214968| 0.0|
|770066|[5.0,2.0,2.0,2.0,...| 0.5125093127301371| 0.0|
+------+--------------------+-------------------+----------+
only showing top 20 rows
Listing 4-10Anomaly Detection with Isolation Forest
我们不能使用 BinaryClassificationEvaluator 来评估隔离林模型,因为它要求原始预测字段出现在输出中。Spark-iForest 生成一个异常分数字段,而不是原始预测。我们将使用 BinaryClassificationMetrics 来评估模型。
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
val binaryMetrics = new BinaryClassificationMetrics(
predictions.select("prediction", "label").rdd.map {
case Row(prediction: Double, label: Double) => (prediction, label)
}
)
println(s"AUC: ${binaryMetrics.areaUnderROC()}")
AUC: 0.9532866199532866
主成分分析降维
主成分分析(PCA)是一种无监督的机器学习技术,用于降低特征空间的维度。它检测要素之间的相关性,并生成数量减少的线性不相关要素,同时保留原始数据集中的大部分方差。这些更紧凑、线性不相关的特征被称为主成分。主成分按其解释方差的降序排列。当数据集中有大量要素时,降维至关重要。例如,基因组学和工业分析领域的机器学习用例通常涉及数千甚至数百万个特征。高维数使得模型更加复杂,增加了过度拟合的机会。在某一点上添加更多的特征实际上会降低模型的性能。此外,对高维数据的训练需要大量的计算资源。这些被统称为维度诅咒。降维技术旨在克服维数灾难。
请注意,五氯苯甲醚产生的主要成分不可解释。在你需要理解为什么做出预测的情况下,这是一个交易破坏者。此外,在应用 PCA 之前对数据集进行标准化也很重要,这样可以防止最大比例的要素被认为比其他要素更重要。
例子
对于我们的例子,我们将在 Iris 数据集上使用 PCA 来将四维特征向量投影到二维主分量中(参见清单 [4-11 )。
import org.apache.spark.ml.feature.{PCA, VectorAssembler}
import org.apache.spark.ml.feature.StringIndexer
import org.apache.spark.sql.types._
val irisSchema = StructType(Array (
StructField("sepal_length", DoubleType, true),
StructField("sepal_width", DoubleType, true),
StructField("petal_length", DoubleType, true),
StructField("petal_width", DoubleType, true),
StructField("class", StringType, true)
))
val dataDF = spark.read.format("csv")
.option("header", "false")
.schema(irisSchema)
.load("/files/iris.data")
dataDF.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
dataDF.show
+------------+-----------+------------+-----------+-----------+
|sepal_length|sepal_width|petal_length|petal_width| class|
+------------+-----------+------------+-----------+-----------+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa|
+------------+-----------+------------+-----------+-----------+
only showing top 20 rows
dataDF.describe().show(5,15)
+-------+---------------+---------------+---------------+---------------+
|summary| sepal_length| sepal_width| petal_length| petal_width|
+-------+---------------+---------------+---------------+---------------+
| count| 150| 150| 150| 150|
| mean|5.8433333333...|3.0540000000...|3.7586666666...|1.1986666666...|
| stddev|0.8280661279...|0.4335943113...|1.7644204199...|0.7631607417...|
| min| 4.3| 2.0| 1.0| 0.1|
| max| 7.9| 4.4| 6.9| 2.5|
+-------+---------------+---------------+---------------+---------------+
+--------------+
| class|
+--------------+
| 150|
| null|
| null|
| Iris-setosa|
|Iris-virginica|
+--------------+
val labelIndexer = new StringIndexer()
.setInputCol("class")
.setOutputCol("label")
val dataDF2 = labelIndexer.fit(dataDF).transform(dataDF)
dataDF2.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
dataDF2.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
only showing top 20 rows
import org.apache.spark.ml.feature.VectorAssembler
val features = Array("sepal_length","sepal_width","petal_length","petal_width")
val assembler = new VectorAssembler()
.setInputCols(features)
.setOutputCol("features")
val dataDF3 = assembler.transform(dataDF2)
dataDF3.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
dataDF3.show
+------------+-----------+------------+-----------+-----------+-----+
|sepal_length|sepal_width|petal_length|petal_width| class|label|
+------------+-----------+------------+-----------+-----------+-----+
| 5.1| 3.5| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.0| 1.4| 0.2|Iris-setosa| 0.0|
| 4.7| 3.2| 1.3| 0.2|Iris-setosa| 0.0|
| 4.6| 3.1| 1.5| 0.2|Iris-setosa| 0.0|
| 5.0| 3.6| 1.4| 0.2|Iris-setosa| 0.0|
| 5.4| 3.9| 1.7| 0.4|Iris-setosa| 0.0|
| 4.6| 3.4| 1.4| 0.3|Iris-setosa| 0.0|
| 5.0| 3.4| 1.5| 0.2|Iris-setosa| 0.0|
| 4.4| 2.9| 1.4| 0.2|Iris-setosa| 0.0|
| 4.9| 3.1| 1.5| 0.1|Iris-setosa| 0.0|
| 5.4| 3.7| 1.5| 0.2|Iris-setosa| 0.0|
| 4.8| 3.4| 1.6| 0.2|Iris-setosa| 0.0|
| 4.8| 3.0| 1.4| 0.1|Iris-setosa| 0.0|
| 4.3| 3.0| 1.1| 0.1|Iris-setosa| 0.0|
| 5.8| 4.0| 1.2| 0.2|Iris-setosa| 0.0|
| 5.7| 4.4| 1.5| 0.4|Iris-setosa| 0.0|
| 5.4| 3.9| 1.3| 0.4|Iris-setosa| 0.0|
| 5.1| 3.5| 1.4| 0.3|Iris-setosa| 0.0|
| 5.7| 3.8| 1.7| 0.3|Iris-setosa| 0.0|
| 5.1| 3.8| 1.5| 0.3|Iris-setosa| 0.0|
+------------+-----------+------------+-----------+-----------+-----+
+-----------------+
| features|
+-----------------+
|[5.1,3.5,1.4,0.2]|
|[4.9,3.0,1.4,0.2]|
|[4.7,3.2,1.3,0.2]|
|[4.6,3.1,1.5,0.2]|
|[5.0,3.6,1.4,0.2]|
|[5.4,3.9,1.7,0.4]|
|[4.6,3.4,1.4,0.3]|
|[5.0,3.4,1.5,0.2]|
|[4.4,2.9,1.4,0.2]|
|[4.9,3.1,1.5,0.1]|
|[5.4,3.7,1.5,0.2]|
|[4.8,3.4,1.6,0.2]|
|[4.8,3.0,1.4,0.1]|
|[4.3,3.0,1.1,0.1]|
|[5.8,4.0,1.2,0.2]|
|[5.7,4.4,1.5,0.4]|
|[5.4,3.9,1.3,0.4]|
|[5.1,3.5,1.4,0.3]|
|[5.7,3.8,1.7,0.3]|
|[5.1,3.8,1.5,0.3]|
+-----------------+
// We will standardize the four attributes (sepal_length, sepal_width,
// petal_length, and petal_width) using StandardScaler even though they all // have the same scale and measure the same quantity. As discussed earlier, // standardization is considered the best practice and is a requirement for
// many algorithms such as PCA to execute optimally.
import org.apache.spark.ml.feature.StandardScaler
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithStd(true)
.setWithMean(false)
val dataDF4 = scaler.fit(dataDF3).transform(dataDF3)
dataDF4.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
|-- scaledFeatures: vector (nullable = true)
// Generate two principal components.
val pca = new PCA()
.setInputCol("scaledFeatures")
.setOutputCol("pcaFeatures")
.setK(2)
.fit(dataDF4)
val dataDF5 = pca.transform(dataDF4)
dataDF5.printSchema
root
|-- sepal_length: double (nullable = true)
|-- sepal_width: double (nullable = true)
|-- petal_length: double (nullable = true)
|-- petal_width: double (nullable = true)
|-- class: string (nullable = true)
|-- label: double (nullable = false)
|-- features: vector (nullable = true)
|-- scaledFeatures: vector (nullable = true)
|-- pcaFeatures: vector (nullable = true)
dataDF5.select("scaledFeatures","pcaFeatures").show(false)
+-------------------------------------------------------------------------+
|scaledFeatures |
+-------------------------------------------------------------------------+
|[6.158928408838787,8.072061621390857,0.7934616853039358,0.26206798787142]|
|[5.9174018045706,6.9189099611921625,0.7934616853039358,0.26206798787142] |
|[5.675875200302412,7.38017062527164,0.7367858506393691,0.26206798787142] |
|[5.555111898168318,7.149540293231902,0.8501375199685027,0.26206798787142]|
|[6.038165106704694,8.302691953430596,0.7934616853039358,0.26206798787142]|
|[6.52121831524107,8.99458294954981,0.9634891892976364,0.52413597574284] |
|[5.555111898168318,7.841431289351117,0.7934616853039358,0.39310198180713]|
|[6.038165106704694,7.841431289351117,0.8501375199685027,0.26206798787142]|
|[5.313585293900131,6.688279629152423,0.7934616853039358,0.26206798787142]|
|[5.9174018045706,7.149540293231902,0.8501375199685027,0.13103399393571] |
|[6.52121831524107,8.533322285470334,0.8501375199685027,0.26206798787142] |
|[5.7966385024365055,7.841431289351117,0.9068133546330697,0.262067987871] |
|[5.7966385024365055,6.9189099611921625,0.7934616853039358,0.131033993935]|
|[5.192821991766037,6.9189099611921625,0.6234341813102354,0.1310339939351]|
|[7.004271523777445,9.22521328158955,0.6801100159748021,0.26206798787142] |
|[6.883508221643351,10.147734609748506,0.8501375199685027,0.524135975742] |
|[6.52121831524107,8.99458294954981,0.7367858506393691,0.52413597574284] |
|[6.158928408838787,8.072061621390857,0.7934616853039358,0.39310198180713]|
|[6.883508221643351,8.763952617510071,0.9634891892976364,0.39310198180713]|
|[6.158928408838787,8.763952617510071,0.8501375199685027,0.39310198180713]|
+-------------------------------------------------------------------------+
+-----------------------------------------+
|pcaFeatures |
+-----------------------------------------+
|[-1.7008636408214346,-9.798112476165109] |
|[-1.8783851549940478,-8.640880678324866] |
|[-1.597800192305247,-8.976683127367169] |
|[-1.6613406138855684,-8.720650458966217] |
|[-1.5770426874367196,-9.96661148272853] |
|[-1.8942207975522354,-10.80757533867312] |
|[-1.5202989381570455,-9.368410789070643] |
|[-1.7314610064823877,-9.540884243679617] |
|[-1.6237061774493644,-8.202607301741613] |
|[-1.7764763044699745,-8.846965954487347] |
|[-1.8015813990792064,-10.361118028393015]|
|[-1.6382374187586244,-9.452155017757546] |
|[-1.741187558292187,-8.587346593832775] |
|[-1.3269417814262463,-8.358947926562632] |
|[-1.7728726239179156,-11.177765120852797]|
|[-1.7138964933624494,-12.00737840334759] |
|[-1.7624485738747564,-10.80279308233496] |
|[-1.7624485738747564,-10.80279308233496] |
|[-1.7624485738747564,-10.80279308233496] |
|[-1.6257080769316516,-10.44826393443861] |
+-----------------------------------------+
Listing 4-11Reducing Dimensions with PCA
如前所述,鸢尾数据集有三种花(刚毛鸢尾、杂色鸢尾和海滨鸢尾)。它有四个属性(萼片长度、萼片宽度、花瓣长度和花瓣宽度)。让我们在两个主成分上绘制样本。从图 4-8 中可以看出,刚毛鸢尾与其他两个纲分开的很好,而杂色鸢尾和海滨鸢尾略有重叠。
图 4-8
虹膜数据集的 PCA 投影
explainedVariance 方法返回一个向量,该向量包含由每个主成分解释的方差的比例。我们的目标是在新的主成分中保持尽可能多的差异。
pca.explainedVariance
res5: org.apache.spark.ml.linalg.DenseVector = [0.7277045209380264,0.23030523267679512]
基于该方法的输出,第一主成分解释了 72.77%的方差,而第二主成分解释了 23.03%的方差。累积起来,两个主成分解释了 95.8%的方差。如你所见,当我们降低维度时,我们丢失了一些信息。如果在保持良好的模型准确性的同时有实质性的训练性能改进,这通常是可接受的折衷。
摘要
我们讨论了几种无监督学习技术,并学习了如何将它们应用到现实世界的业务用例中。近年来,随着大数据的出现,无监督学习再次受到欢迎。聚类、异常检测和主成分分析等技术有助于理解移动和物联网设备、传感器、社交媒体等产生的大量非结构化数据。这是你机器学习武库中的一个强大工具。
参考
-
库尔特·冯内古特;"18.地球上最有价值的商品,“1998,猫的摇篮:一部小说
-
克里斯皮赫,吴恩达,迈克尔乔丹;”K 的意思是,“stanford.edu,2013,
https://stanford.edu/~cpiech/cs221/handouts/kmeans.html
-
杰森·布朗利;"为什么在机器学习中一次性编码数据?",machinelearningmastery.com,2017,
https://machinelearningmastery.com/why-one-hot-encode-data-in-machine-learning/
-
大卫·塔尔比;《面向 Apache Spark 的自然语言处理库介绍》,Databricks,2017,
https://databricks.com/blog/2017/10/19/introducing-natural-language-processing-library-apache-spark.html
-
克里斯托弗·d·曼宁等人;《斯坦福 CoreNLP 自然语言处理工具包》,斯坦福大学,
https://nlp.stanford.edu/pubs/StanfordCoreNlp2014.pdf
-
约翰·斯诺实验室;《快速入门》,约翰·斯诺实验室,2019,
https://nlp.johnsnowlabs.com/docs/en/quickstart
-
Shivam Bansal《理解和实现自然语言处理的终极指南》,Analytics Vidhya,2017,
www.analyticsvidhya.com/blog/2017/01/ultimate-guide-to-understand-implement-natural-language-processing-codes-in-python/
-
塞巴斯蒂安·拉什卡;《朴素贝叶斯与文本分类——导论与理论》,sebastiantraschka.com,2014,
https://sebastianraschka.com/Articles/2014_naive_bayes_1.html
-
齐格蒙特·扎瓦日基;《从 LDA 模型中获取话题词》,zstat.pl,2018,
www.zstat.pl/2018/02/07/scala-spark-get-topics-words-from-lda-model/
-
费托尼刘,婷,-周华;《隔离森林》,acm.org,2008,
https://dl.acm.org/citation.cfm?id=1511387
-
亚历杭德罗·科雷亚·巴恩森;“利用隔离森林进行异常检测的好处”,easysol.net,2016,
https://blog.easysol.net/using-isolation-forests-anamoly-detection/
-
李孙等。艾尔。;“使用扩展隔离森林算法检测异常用户行为:企业案例研究”,arxiv.org,2016,
https://arxiv.org/pdf/1609.06676.pdf
-
李孙等。艾尔。;“使用扩展隔离森林算法检测异常用户行为:企业案例研究”,arxiv.org,2016,
https://arxiv.org/pdf/1609.06676.pdf
-
张志敏;“通过隔离森林进行代表子集选择和离群点检测”,github.com,2016,
https://github.com/zmzhang/IOS
-
亚历杭德罗·科雷亚·巴恩森;“利用隔离森林进行异常检测的好处”,easysol.net,2016,
https://blog.easysol.net/using-isolation-forests-anamoly-detection/
-
杨与投稿人:【火花四射】github.com,2018 年
https://github.com/titicaca/spark-iforest
-
杨及供稿人:《spark-iforest》,github.com,2018,
https://github.com/titicaca/spark-iforest
-
威廉·h·沃尔伯格博士等人;“乳腺癌威斯康星州(诊断)数据集”,archive.isc.uci.edu,1995,
http://archive.ics.uci.edu/ml/datasets/breast+cancer+wisconsin+(diagnostic)
五、推荐
人是不知道渴望什么的生物,他求助于他人以下定决心。我们渴望别人渴望的东西,因为我们模仿他们的渴望。
—勒内·吉拉德 我
提供个性化推荐是机器学习最流行的应用之一。几乎每个主要零售商,如亚马逊、阿里巴巴、沃尔玛和塔吉特,都根据顾客行为提供某种个性化推荐。网飞、Hulu 和 Spotify 等流媒体服务根据用户的口味和偏好提供电影或音乐推荐。
推荐对于提高客户满意度和参与度至关重要,最终会增加销售额和收入。为了突出推荐的重要性,44%的亚马逊客户从他们在亚马逊上看到的产品推荐中购买。 ii 麦肯锡的一份报告发现,35%的客户销售直接来自亚马逊的推荐。同一份研究报告称,75%的观众在网飞上观看的内容来自个性化推荐。网飞的首席产品官在一次采访中宣称,网飞的个性化电影和电视节目推荐每年给公司带来 10 亿美元的收入。阿里巴巴的推荐引擎帮助推动了创纪录的销售,成为世界上最大的电子商务公司和零售业巨头之一,2013 年销售额达 2480 亿美元(超过亚马逊和易贝的总和)。 v
推荐不仅限于零售商和流媒体服务。银行将推荐引擎作为一种有针对性的营销工具,利用它向在线银行客户提供金融产品和服务,如基于其人口统计和心理特征的家庭或学生贷款。广告和营销机构使用推荐引擎来显示高针对性的在线广告。
推荐引擎的类型
有几种类型的推荐引擎。 vi 我们将讨论最流行的:协同过滤、基于内容的过滤和关联规则。
交替最小二乘协同过滤
协同过滤经常被用于在 Web 上提供个性化推荐。使用协同过滤的公司包括网飞、亚马逊、阿里巴巴、Spotify 和苹果等。协同过滤根据其他人的(协同)偏好或品味提供推荐(过滤)。它基于这样一种想法,即具有相同偏好的人将来更有可能拥有相同的兴趣。例如,劳拉喜欢泰坦尼克号、??、阿波罗 13 号和 ?? 高耸的地狱。汤姆喜欢阿波罗 13 号和高耸的地狱。如果安妮喜欢阿波罗 13 ,根据我们的计算,任何喜欢阿波罗 13 的人一定也喜欢高耸的地狱,那么高耸的地狱可能是安妮的潜在推荐。产品可以是任何项目,如电影、歌曲、视频或书籍。
图 5-1
作为评分矩阵【VII】
Spark MLlib 包括一种流行的协作过滤算法,称为交替最小二乘法(ALS)。ALS 将评分矩阵(图 5-1 )建模为用户与产品因素 viii (图 5-2 )的乘积。ALS 利用最小平方计算来最小化估计误差, ix 在固定客户因素和求解产品因素之间交替迭代,反之亦然,直到过程收敛。Spark MLlib 实现了 ALS 的分块版本,通过将两组因素(称为“用户”和“产品”)分组为块来利用 Spark 的分布式处理能力,并通过在每次迭代中仅向每个产品块发送每个用户向量的一个副本来减少通信,并且仅针对需要该用户的特征向量的产品块。 x
图 5-2
als 计算机如何推荐
Spark MLlib 的 ALS 实现支持显式和隐式评级。显式评级(默认)要求用户对产品的评级是一个分数(例如,1 –
5 个大拇指),而隐式评级表示用户将与产品交互的信心(例如,点击数或页面浏览量,或者视频流的次数)。隐性评分在现实生活中更为常见,因为并非每个公司都会为其产品收集显性评分。然而,隐式评级可以从公司数据中提取,如网络日志、观看习惯或销售交易。Spark MLlib 的 ALS 实现使用整数作为项目和用户 id,这意味着项目和用户 id 必须在整数的范围内,最大值为 2,147,483,647。
Note
在 Yehuda Koren 和 Robert M. Bell 的论文“具有联合导出的邻域插值权重的可伸缩协同过滤”中描述了交替最小二乘法(ALS)。Xi
因素
Spark MLlib 的 ALS 实现支持以下参数 xii :
-
alpha: 适用于 ALS 的隐式反馈版本,指导偏好观察的基线置信度
-
numBlocks: U sed 进行并行处理;项目和用户将被划分成的块数
-
非负:表示是否对最小二乘使用非负约束
-
implicitPrefs: 表示是使用显式反馈还是隐式反馈
-
k: 表示模型中潜在因素的数量
-
regParam :正则化参数
-
maxIter: 表示要执行的最大迭代次数。
例子
我们将使用 MovieLens 数据集来构建一个玩具电影推荐系统。数据集可以从 https://grouplens.org/datasets/movielens/
下载。数据集中包含多个文件,但我们主要对 ratings.csv 感兴趣。清单 5-1 中显示的文件中的每一行都包含用户对一部电影的明确评级(1 –
5 评级)。
val dataDF = spark.read.option("header", "true")
.option("inferSchema", "true")
.csv("ratings.csv")
dataDF.printSchema
root
|-- userId: integer (nullable = true)
|-- movieId: integer (nullable = true)
|-- rating: double (nullable = true)
|-- timestamp: integer (nullable = true)
dataDF.show
+------+-------+------+---------+
|userId|movieId|rating|timestamp|
+------+-------+------+---------+
| 1| 1| 4.0|964982703|
| 1| 3| 4.0|964981247|
| 1| 6| 4.0|964982224|
| 1| 47| 5.0|964983815|
| 1| 50| 5.0|964982931|
| 1| 70| 3.0|964982400|
| 1| 101| 5.0|964980868|
| 1| 110| 4.0|964982176|
| 1| 151| 5.0|964984041|
| 1| 157| 5.0|964984100|
| 1| 163| 5.0|964983650|
| 1| 216| 5.0|964981208|
| 1| 223| 3.0|964980985|
| 1| 231| 5.0|964981179|
| 1| 235| 4.0|964980908|
| 1| 260| 5.0|964981680|
| 1| 296| 3.0|964982967|
| 1| 316| 3.0|964982310|
| 1| 333| 5.0|964981179|
| 1| 349| 4.0|964982563|
+------+-------+------+---------+
only showing top 20 rows
val Array(trainingData, testData) = dataDF.randomSplit(Array(0.7, 0.3))
import org.apache.spark.ml.recommendation.ALS
val als = new ALS()
.setMaxIter(15)
.setRank(10)
.setSeed(1234)
.setRatingCol("rating")
.setUserCol("userId")
.setItemCol("movieId")
val model = als.fit(trainingData)
val predictions = model.transform(testData)
predictions.printSchema
root
|-- userId: integer (nullable = true)
|-- movieId: integer (nullable = true)
|-- rating: double (nullable = true)
|-- timestamp: integer (nullable = true)
|-- prediction: float (nullable = false)
predictions.show
+------+-------+------+----------+----------+
|userId|movieId|rating| timestamp|prediction|
+------+-------+------+----------+----------+
| 133| 471| 4.0| 843491793| 2.5253267|
| 602| 471| 4.0| 840876085| 3.2802277|
| 182| 471| 4.5|1054779644| 3.6534667|
| 500| 471| 1.0|1005528017| 3.5033386|
| 387| 471| 3.0|1139047519| 2.6689813|
| 610| 471| 4.0|1479544381| 3.006948|
| 136| 471| 4.0| 832450058| 3.1404104|
| 312| 471| 4.0|1043175564| 3.109232|
| 287| 471| 4.5|1110231536| 2.9776838|
| 32| 471| 3.0| 856737165| 3.5183017|
| 469| 471| 5.0| 965425364| 2.8298397|
| 608| 471| 1.5|1117161794| 3.007364|
| 373| 471| 5.0| 846830388| 3.9275675|
| 191| 496| 5.0| 829760898| NaN|
| 44| 833| 2.0| 869252237| 2.4776468|
| 609| 833| 3.0| 847221080| 1.9167987|
| 608| 833| 0.5|1117506344| 2.220617|
| 463| 1088| 3.5|1145460096| 3.0794377|
| 47| 1088| 4.0|1496205519| 2.4831696|
| 479| 1088| 4.0|1039362157| 3.5400867|
+------+-------+------+----------+----------+
import org.apache.spark.ml.evaluation.RegressionEvaluator
val evaluator = new RegressionEvaluator()
.setPredictionCol("prediction")
.setLabelCol("rating")
.setMetricName("rmse")
val rmse = evaluator.evaluate(predictions)
rmse: Double = NaN
Listing 5-1Movie Recommendations with ALS
看起来评估者不喜欢预测数据帧中的 NaN 值。现在让我们通过删除具有 NaN 值的行来解决这个问题。我们稍后将讨论如何使用 coldStartStrategy 参数来处理这个问题。
val predictions2 = predictions.na.drop
predictions2.show
+------+-------+------+----------+----------+
|userId|movieId|rating| timestamp|prediction|
+------+-------+------+----------+----------+
| 133| 471| 4.0| 843491793| 2.5253267|
| 602| 471| 4.0| 840876085| 3.2802277|
| 182| 471| 4.5|1054779644| 3.6534667|
| 500| 471| 1.0|1005528017| 3.5033386|
| 387| 471| 3.0|1139047519| 2.6689813|
| 610| 471| 4.0|1479544381| 3.006948|
| 136| 471| 4.0| 832450058| 3.1404104|
| 312| 471| 4.0|1043175564| 3.109232|
| 287| 471| 4.5|1110231536| 2.9776838|
| 32| 471| 3.0| 856737165| 3.5183017|
| 469| 471| 5.0| 965425364| 2.8298397|
| 608| 471| 1.5|1117161794| 3.007364|
| 373| 471| 5.0| 846830388| 3.9275675|
| 44| 833| 2.0| 869252237| 2.4776468|
| 609| 833| 3.0| 847221080| 1.9167987|
| 608| 833| 0.5|1117506344| 2.220617|
| 463| 1088| 3.5|1145460096| 3.0794377|
| 47| 1088| 4.0|1496205519| 2.4831696|
| 479| 1088| 4.0|1039362157| 3.5400867|
| 554| 1088| 5.0| 944900489| 3.3577442|
+------+-------+------+----------+----------+
only showing top 20 rows
val evaluator = new RegressionEvaluator()
.setPredictionCol("prediction")
.setLabelCol("rating")
.setMetricName("rmse")
val rmse = evaluator.evaluate(predictions2)
rmse: Double = 0.9006479893684061
Note
使用 ALS 时,您有时会遇到在模型定型时不存在的用户和/或测试数据集中的项目。新用户或项目可能没有任何评级,并且模型没有在其上被训练。这就是所谓的冷启动问题。当数据在评估数据集和训练数据集之间随机拆分时,也可能会遇到这种情况。当用户和/或项目不在模型中时,预测设置为 NaN。这就是我们之前在评估模型时遇到 NaN 结果的原因。为了解决这个问题,Spark 提供了一个 coldStartStrategy 参数,该参数可以设置为删除预测数据帧中包含 NaN 值的所有行。XIII
让我们提出一些建议。
为所有用户推荐前三部电影。
model.recommendForAllUsers(3).show(false)
+------+----------------------------------------------------------+
|userId|recommendations |
+------+----------------------------------------------------------+
|471 |[[7008, 4.8596725], [7767, 4.8047066], [26810, 4.7513227]]|
|463 |[[33649, 5.0881286], [3347, 4.7693057], [68945, 4.691733]]|
|496 |[[6380, 4.946864], [26171, 4.8910613], [7767, 4.868356]] |
|148 |[[183897, 4.972257], [6732, 4.561547], [33649, 4.5440807]]|
|540 |[[26133, 5.19643], [68945, 5.1259947], [3379, 5.1259947]] |
|392 |[[3030, 6.040107], [4794, 5.6566052], [55363, 5.4429026]] |
|243 |[[1223, 6.5019746], [68945, 6.353135], [3379, 6.353135]] |
|31 |[[4256, 5.3734074], [49347, 5.365612], [7071, 5.3175936]] |
|516 |[[4429, 4.8486495], [48322, 4.8443394], [28, 4.8082485]] |
|580 |[[86347, 5.20571], [4256, 5.0522637], [72171, 5.037114]] |
|251 |[[33649, 5.6993585], [68945, 5.613014], [3379, 5.613014]] |
|451 |[[68945, 5.392536], [3379, 5.392536], [905, 5.336588]] |
|85 |[[25771, 5.2532864], [8477, 5.186757], [99764, 5.1611686]]|
|137 |[[7008, 4.8952146], [26131, 4.8543305], [3200, 4.6918836]]|
|65 |[[33649, 4.695069], [3347, 4.5379376], [7071, 4.535537]] |
|458 |[[3404, 5.7415047], [7018, 5.390625], [42730, 5.343014]] |
|481 |[[232, 4.393473], [3473, 4.3804317], [26133, 4.357505]] |
|53 |[[3200, 6.5110188], [33649, 6.4942613], [3347, 6.452143]] |
|255 |[[86377, 5.9217377], [5047, 5.184309], [6625, 4.962062]] |
|588 |[[26133, 4.7600465], [6666, 4.65716], [39444, 4.613207]] |
+------+----------------------------------------------------------+
only showing top 20 rows
推荐所有电影前三用户。
model.recommendForAllItems(3).show(false)
+-------+------------------------------------------------------+
|movieId|recommendations |
+-------+------------------------------------------------------+
|1580 |[[53, 4.939177], [543, 4.8362885], [452, 4.5791063]] |
|4900 |[[147, 3.0081954], [375, 2.9420073], [377, 2.6285374]]|
|5300 |[[53, 4.29147], [171, 4.129584], [375, 4.1011653]] |
|6620 |[[392, 5.0614614], [191, 4.820595], [547, 4.7811346]] |
|7340 |[[413, 3.2256641], [578, 3.1126869], [90, 3.0790782]] |
|32460 |[[53, 5.642673], [12, 5.5260286], [371, 5.2030106]] |
|54190 |[[53, 5.544555], [243, 5.486003], [544, 5.243029]] |
|471 |[[51, 5.073474], [53, 4.8641024], [337, 4.656805]] |
|1591 |[[112, 4.250576], [335, 4.147236], [207, 4.05843]] |
|140541 |[[393, 4.4335465], [536, 4.1968756], [388, 4.0388694]]|
|1342 |[[375, 4.3189483], [313, 3.663758], [53, 3.5866988]] |
|2122 |[[375, 4.3286233], [147, 4.3245177], [112, 3.8350344]]|
|2142 |[[51, 3.9718416], [375, 3.8228302], [122, 3.8117828]] |
|7982 |[[191, 5.297085], [547, 5.020829], [187, 4.984965]] |
|44022 |[[12, 4.5919843], [53, 4.501897], [523, 4.301981]] |
|141422 |[[456, 2.7050805], [597, 2.6988854], [498, 2.6347125]]|
|833 |[[53, 3.8047972], [543, 3.740805], [12, 3.6920836]] |
|5803 |[[537, 3.8269677], [544, 3.8034997], [259, 3.76062]] |
|7993 |[[375, 2.93635], [53, 2.9159238], [191, 2.8663528]] |
|160563 |[[53, 4.048704], [243, 3.9232922], [337, 3.7616432]] |
+-------+------------------------------------------------------+
only showing top 20 rows
为一组指定的电影生成前三个用户推荐。
model.recommendForItemSubset(Seq((111), (202), (225), (347), (488)).toDF("movieId"), 3).show(false)
+-------+----------------------------------------------------+
|movieId|recommendations |
+-------+----------------------------------------------------+
|225 |[[53, 4.4893017], [147, 4.483344], [276, 4.2529426]]|
|111 |[[375, 5.113064], [53, 4.9947076], [236, 4.9493203]]|
|347 |[[191, 4.686208], [236, 4.51165], [40, 4.409832]] |
|202 |[[53, 3.349618], [578, 3.255436], [224, 3.245058]] |
|488 |[[558, 3.3870435], [99, 3.2978806], [12, 3.2749753]]|
+-------+----------------------------------------------------+
为指定的一组用户生成前三部电影推荐。
model.recommendForUserSubset(Seq((111), (100), (110), (120), (130)).toDF("userId"), 3).show(false)
+------+--------------------------------------------------------------+
|userId|recommendations |
+------+--------------------------------------------------------------+
|111 |[[106100, 4.956068], [128914, 4.9050474], [162344, 4.9050474]]|
|120 |[[26865, 4.979374], [3508, 4.6825113], [3200, 4.6406555]] |
|100 |[[42730, 5.2531567], [5867, 5.1075697], [3404, 5.0877166]] |
|130 |[[86377, 5.224841], [3525, 5.0586476], [92535, 4.9758487]] |
|110 |[[49932, 4.6330786], [7767, 4.600622], [26171, 4.5615706]] |
+------+--------------------------------------------------------------+
协同过滤可以非常有效地提供高度相关的推荐。它的扩展性很好,可以处理非常大的数据集。为了使协同过滤以最佳方式运行,它需要访问大量数据。数据越多越好。随着时间的推移,评分开始累积,推荐会变得越来越准确。在实现的早期阶段,访问大型数据集通常是一个问题。一种解决方案是将基于内容的过滤与协作过滤结合使用。由于基于内容的过滤不依赖于用户活动,它可以立即开始提供建议,随着时间的推移逐渐增加您的数据集。
具有 FP 增长的市场篮分析
购物篮分析是一种简单但重要的技术,通常被零售商用来提供产品推荐。它使用交易数据集来确定哪些产品经常一起购买。零售商可以利用这些建议进行个性化的交叉销售和追加销售,帮助提高转化率并最大化每个客户的价值。
在浏览 Amazon.com 时,你很可能已经看到了市场篮子分析在起作用。Amazon.com 产品页面通常会有一个名为“购买该商品的客户也购买了”的部分,为您提供一个经常与您当前浏览的产品一起购买的商品列表。该列表是通过购物篮分析生成的。实体店零售商也使用购物篮分析,通过告知货架图中的产品位置和相邻位置来优化商店。这样做的目的是通过把互补的物品放在一起来增加销量。
购物篮分析使用关联规则学习来进行推荐。关联规则使用大型事务数据集寻找项目之间的关系。 xiv 关联规则是由两个或两个以上称为项集的项计算出来的。关联规则由前因(if)和后果(then)组成。例如,如果有人买饼干(前因),那么这个人也更有可能买牛奶(后果)。流行的关联规则算法包括 Apriori、SETM、ECLAT 和 FP-Growth。Spark MLlib 为关联规则挖掘提供了一个高度可扩展的 FP-Growth 实现。 xv FP-Growth 使用频繁模式(“FP”代表频繁模式)树结构识别频繁项目并计算项目频率。XVI
Note
FP-Growth 在 Jiawei Han、Jian Pei 和 Iwen Yin 的论文“挖掘无候选生成的频繁模式”中有所描述。XVII
例子
对于使用 FP-Growth 的购物篮分析示例,我们将使用流行的 Instacart 在线杂货购物数据集。 xviii 该数据集包含来自 200,000 名 Instacart 客户的 50,000 种产品的 340 万份杂货订单。可以从 www.instacart.com/datasets/grocery-shopping-2017
下载数据集。对于 FP-Growth,我们只需要 products 和 order_products_train 表(参见清单 5-2 )。
val productsDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema","true")
.load("/instacart/products.csv")
ProductsDF.show(false)
+----------+-------------------------------------------------+
|product_id|product_name |
+----------+-------------------------------------------------+
|1 |Chocolate Sandwich Cookies |
|2 |All-Seasons Salt |
|3 |Robust Golden Unsweetened Oolong Tea |
|4 |Smart Ones Classic Favorites Mini Rigatoni With |
|5 |Green Chile Anytime Sauce |
|6 |Dry Nose Oil |
|7 |Pure Coconut Water With Orange |
|8 |Cut Russet Potatoes Steam N' Mash |
|9 |Light Strawberry Blueberry Yogurt |
|10 |Sparkling Orange Juice & Prickly Pear Beverage |
|11 |Peach Mango Juice |
|12 |Chocolate Fudge Layer Cake |
|13 |Saline Nasal Mist |
|14 |Fresh Scent Dishwasher Cleaner |
|15 |Overnight Diapers Size 6 |
|16 |Mint Chocolate Flavored Syrup |
|17 |Rendered Duck Fat |
|18 |Pizza for One Suprema Frozen Pizza |
|19 |Gluten Free Quinoa Three Cheese & Mushroom Blend |
|20 |Pomegranate Cranberry & Aloe Vera Enrich Drink |
+----------+-------------------------------------------------+
+--------+-------------+
|aisle_id|department_id|
+--------+-------------+
|61 |19 |
|104 |13 |
|94 |7 |
|38 |1 |
|5 |13 |
|11 |11 |
|98 |7 |
|116 |1 |
|120 |16 |
|115 |7 |
|31 |7 |
|119 |1 |
|11 |11 |
|74 |17 |
|56 |18 |
|103 |19 |
|35 |12 |
|79 |1 |
|63 |9 |
|98 |7 |
+--------+-------------+
only showing top 20 rows
val orderProductsDF = spark.read.format("csv")
.option("header", "true")
.option("inferSchema","true")
.load("/instacart/order_products__train.csv")
orderProductsDF.show()
+--------+----------+-----------------+---------+
|order_id|product_id|add_to_cart_order|reordered|
+--------+----------+-----------------+---------+
| 1| 49302| 1| 1|
| 1| 11109| 2| 1|
| 1| 10246| 3| 0|
| 1| 49683| 4| 0|
| 1| 43633| 5| 1|
| 1| 13176| 6| 0|
| 1| 47209| 7| 0|
| 1| 22035| 8| 1|
| 36| 39612| 1| 0|
| 36| 19660| 2| 1|
| 36| 49235| 3| 0|
| 36| 43086| 4| 1|
| 36| 46620| 5| 1|
| 36| 34497| 6| 1|
| 36| 48679| 7| 1|
| 36| 46979| 8| 1|
| 38| 11913| 1| 0|
| 38| 18159| 2| 0|
| 38| 4461| 3| 0|
| 38| 21616| 4| 1|
+--------+----------+-----------------+---------+
only showing top 20 rows
// Create temporary tables
.
orderProductsDF.createOrReplaceTempView("order_products_train")
productsDF.createOrReplaceTempView("products")
val joinedData = spark.sql("select p.product_name, o.order_id from order_products_train o inner join products p where p.product_id = o.product_id")
import org.apache.spark.sql.functions.max
import org.apache.spark.sql.functions.collect_set
val basketsDF = joinedData
.groupBy("order_id")
.agg(collect_set("product_name")
.alias("items"))
basketsDF.createOrReplaceTempView("baskets"
basketsDF.show(20,55)
+--------+-------------------------------------------------------+
|order_id| items|
+--------+-------------------------------------------------------+
| 1342|[Raw Shrimp, Seedless Cucumbers, Versatile Stain Rem...|
| 1591|[Cracked Wheat, Strawberry Rhubarb Yoghurt, Organic ...|
| 4519|[Beet Apple Carrot Lemon Ginger Organic Cold Pressed...|
| 4935| [Vodka]|
| 6357|[Globe Eggplant, Panko Bread Crumbs, Fresh Mozzarell...|
| 10362|[Organic Baby Spinach, Organic Spring Mix, Organic L...|
| 19204|[Reduced Fat Crackers, Dishwasher Cleaner, Peanut Po...|
| 29601|[Organic Red Onion, Small Batch Authentic Taqueria T...|
| 31035|[Organic Cripps Pink Apples, Organic Golden Deliciou...|
| 40011|[Organic Baby Spinach, Organic Blues Bread with Blue...|
| 46266|[Uncured Beef Hot Dog, Organic Baby Spinach, Smoked ...|
| 51607|[Donut House Chocolate Glazed Donut Coffee K Cup, Ma...|
| 58797|[Concentrated Butcher's Bone Broth, Chicken, Seedles...|
| 61793|[Raspberries, Green Seedless Grapes, Clementines, Na...|
| 67089|[Original Tofurky Deli Slices, Sharp Cheddar Cheese,...|
| 70863|[Extra Hold Non-Aerosol Hair Spray, Bathroom Tissue,...|
| 88674|[Organic Coconut Milk, Everything Bagels, Rosemary, ...|
| 91937|[No. 485 Gin, Monterey Jack Sliced Cheese, Tradition...|
| 92317|[Red Vine Tomato, Harvest Casserole Bowls, Organic B...|
| 99621|[Organic Baby Arugula, Organic Garlic, Fennel, Lemon...|
+--------+-------------------------------------------------------+
only showing top 20 rows
import org.apache.spark.ml.fpm.FPGrowth
// FPGrowth only needs a string containing the list of items.
val basketsDF = spark.sql("select items from baskets")
.as[Array[String]].toDF("items")
basketsDF.show(20,55)
+-------------------------------------------------------+
| items|
+-------------------------------------------------------+
|[Raw Shrimp, Seedless Cucumbers, Versatile Stain Rem...|
|[Cracked Wheat, Strawberry Rhubarb Yoghurt, Organic ...|
|[Beet Apple Carrot Lemon Ginger Organic Cold Pressed...|
| [Vodka]|
|[Globe Eggplant, Panko Bread Crumbs, Fresh Mozzarell...|
|[Organic Baby Spinach, Organic Spring Mix, Organic L...|
|[Reduced Fat Crackers, Dishwasher Cleaner, Peanut Po...|
|[Organic Red Onion, Small Batch Authentic Taqueria T...|
|[Organic Cripps Pink Apples, Organic Golden Deliciou...|
|[Organic Baby Spinach, Organic Blues Bread with Blue...|
|[Uncured Beef Hot Dog, Organic Baby Spinach, Smoked ...|
|[Donut House Chocolate Glazed Donut Coffee K Cup, Ma...|
|[Concentrated Butcher's Bone Broth, Chicken, Seedles...|
|[Raspberries, Green Seedless Grapes, Clementines, Na...|
|[Original Tofurky Deli Slices, Sharp Cheddar Cheese,...|
|[Extra Hold Non-Aerosol Hair Spray, Bathroom Tissue,...|
|[Organic Coconut Milk, Everything Bagels, Rosemary, ...|
|[No. 485 Gin, Monterey Jack Sliced Cheese, Tradition...|
|[Red Vine Tomato, Harvest Casserole Bowls, Organic B...|
|[Organic Baby Arugula, Organic Garlic, Fennel, Lemon...|
+-------------------------------------------------------+
only showing top 20 rows
val fpgrowth = new FPGrowth()
.setItemsCol("items")
.setMinSupport(0.002)
.setMinConfidence(0)
val model = fpgrowth.fit(basketsDF)
// Frequent itemsets.
val mostPopularItems = model.freqItemsets
mostPopularItems.createOrReplaceTempView("mostPopularItems")
// Verify results.
spark.sql("select ∗ from mostPopularItems wheresize(items) >= 2 order by freq desc")
.show(20,55)
+----------------------------------------------+----+
| items|freq|
+----------------------------------------------+----+
|[Organic Strawberries, Bag of Organic Bananas]|3074|
|[Organic Hass Avocado, Bag of Organic Bananas]|2420|
|[Organic Baby Spinach, Bag of Organic Bananas]|2236|
| [Organic Avocado, Banana]|2216|
| [Organic Strawberries, Banana]|2174|
| [Large Lemon, Banana]|2158|
| [Organic Baby Spinach, Banana]|2000|
| [Strawberries, Banana]|1948|
| [Organic Raspberries, Bag of Organic Bananas]|1780|
| [Organic Raspberries, Organic Strawberries]|1670|
| [Organic Baby Spinach, Organic Strawberries]|1639|
| [Limes, Large Lemon]|1595|
| [Organic Hass Avocado, Organic Strawberries]|1539|
| [Organic Avocado, Organic Baby Spinach]|1402|
| [Organic Avocado, Large Lemon]|1349|
| [Limes, Banana]|1331|
| [Organic Blueberries, Organic Strawberries]|1269|
| [Organic Cucumber, Bag of Organic Bananas]|1268|
| [Organic Hass Avocado, Organic Baby Spinach]|1252|
| [Large Lemon, Organic Baby Spinach]|1238|
+----------------------------------------------+----+
only showing top 20 rows
spark.sql("select ∗ from mostPopularItems where
size(items) > 2 order by freq desc")
.show(20,65)
+-----------------------------------------------------------------+----+
| items|freq|
+-----------------------------------------------------------------+----+
|[Organic Hass Avocado, Organic Strawberries, Bag of Organic Ba...| 710|
|[Organic Raspberries, Organic Strawberries, Bag of Organic Ban...| 649|
|[Organic Baby Spinach, Organic Strawberries, Bag of Organic Ba...| 587|
|[Organic Raspberries, Organic Hass Avocado, Bag of Organic Ban...| 531|
|[Organic Hass Avocado, Organic Baby Spinach, Bag of Organic Ba...| 497|
| [Organic Avocado, Organic Baby Spinach, Banana]| 484|
| [Organic Avocado, Large Lemon, Banana]| 477|
| [Limes, Large Lemon, Banana]| 452|
| [Organic Cucumber, Organic Strawberries, Bag of Organic Bananas]| 424|
| [Limes, Organic Avocado, Large Lemon]| 389|
|[Organic Raspberries, Organic Hass Avocado, Organic Strawberries]| 381|
| [Organic Avocado, Organic Strawberries, Banana]| 379|
| [Organic Baby Spinach, Organic Strawberries, Banana]| 376|
|[Organic Blueberries, Organic Strawberries, Bag of Organic Ban...| 374|
| [Large Lemon, Organic Baby Spinach, Banana]| 371|
| [Organic Cucumber, Organic Hass Avocado, Bag of Organic Bananas]| 366|
| [Organic Lemon, Organic Hass Avocado, Bag of Organic Bananas]| 353|
| [Limes, Organic Avocado, Banana]| 352|
|[Organic Whole Milk, Organic Strawberries, Bag of Organic Bana...| 339|
| [Organic Avocado, Large Lemon, Organic Baby Spinach]| 334|
+-----------------------------------------------------------------+----+
only showing top 20 rows
Listing 5-2Market Basket Analysis with FP-Growth
显示的是最有可能一起购买的商品。列表中最受欢迎的是有机鳄梨、有机草莓和一袋有机香蕉的组合。这种列表可以是“经常一起购买”-
类型推荐的基础。
// The FP-Growth model also generates association rules. The output includes
// the antecedent, consequent, and confidence (probability). The minimum
// confidence for generating association rule is determined by the
// minConfidence parameter.
val AssocRules = model.associationRules
AssocRules.createOrReplaceTempView("AssocRules")
spark.sql("select antecedent, consequent,
confidence from AssocRules order by confidence desc")
.show(20,55)
+-------------------------------------------------------+
| antecedent|
+-------------------------------------------------------+
| [Organic Raspberries, Organic Hass Avocado]|
| [Strawberries, Organic Avocado]|
| [Organic Hass Avocado, Organic Strawberries]|
| [Organic Lemon, Organic Hass Avocado]|
| [Organic Lemon, Organic Strawberries]|
| [Organic Cucumber, Organic Hass Avocado]|
|[Organic Large Extra Fancy Fuji Apple, Organic Straw...|
| [Organic Yellow Onion, Organic Hass Avocado]|
| [Strawberries, Large Lemon]|
| [Organic Blueberries, Organic Raspberries]|
| [Organic Cucumber, Organic Strawberries]|
| [Organic Zucchini, Organic Hass Avocado]|
| [Organic Raspberries, Organic Baby Spinach]|
| [Organic Hass Avocado, Organic Baby Spinach]|
| [Organic Zucchini, Organic Strawberries]|
| [Organic Raspberries, Organic Strawberries]|
| [Bartlett Pears]|
| [Gala Apples]|
| [Limes, Organic Avocado]|
| [Organic Raspberries, Organic Hass Avocado]|
+-------------------------------------------------------+
+------------------------+-------------------+
| consequent| confidence|
+------------------------+-------------------+
|[Bag of Organic Bananas]| 0.521099116781158|
| [Banana]| 0.4643478260869565|
|[Bag of Organic Bananas]| 0.4613385315139701|
|[Bag of Organic Bananas]| 0.4519846350832266|
|[Bag of Organic Bananas]| 0.4505169867060561|
|[Bag of Organic Bananas]| 0.4404332129963899|
|[Bag of Organic Bananas]| 0.4338461538461538|
|[Bag of Organic Bananas]|0.42270861833105333|
| [Banana]| 0.4187779433681073|
| [Organic Strawberries]| 0.414985590778098|
|[Bag of Organic Bananas]| 0.4108527131782946|
|[Bag of Organic Bananas]|0.40930232558139534|
|[Bag of Organic Bananas]|0.40706806282722513|
|[Bag of Organic Bananas]|0.39696485623003197|
|[Bag of Organic Bananas]| 0.3914780292942743|
|[Bag of Organic Bananas]|0.38862275449101796|
| [Banana]| 0.3860811930405965|
| [Banana]|0.38373305526590196|
| [Large Lemon]| 0.3751205400192864|
| [Organic Strawberries]|0.37389597644749756|
+------------------------+-------------------+
only showing top 20 rows
根据产量,购买有机树莓、有机鳄梨和有机草莓的顾客也更有可能购买有机香蕉。正如你所看到的,香蕉是非常受欢迎的商品。这种列表可以是“购买了该商品的顾客也购买了”-
类型推荐的基础。
Note
除了 FP-Growth 之外,Spark MLlib 还包括另一个用于频率模式匹配的算法实现,称为 PrefixSpan。虽然 FP-Growth 与项集的排序方式无关,但 PrefixSpan 使用序列或项集的有序列表来发现数据集中的顺序模式。PrefixSpan 属于称为序列模式挖掘的算法子组。PrefixSpan 在 Jian Pei 等人的论文“通过模式增长挖掘序列模式:PrefixSpan 方法”中有所描述。
基于内容的过滤
基于内容的过滤通过将项目信息(如项目名称、描述或类别)与用户配置文件进行比较来提供建议。让我们以一个基于内容的电影推荐系统为例。如果系统根据用户的个人资料确定用户偏爱加里·格兰特电影,它可能会开始推荐他的电影,如《西北偏北》、《?? 捉贼》和《?? 艳遇》。推荐者可以推荐相同类型演员的电影,例如吉米·斯图尔特、格里高里·派克或克拉克·盖博(经典电影)。经常与加里·格兰特合作的阿尔弗雷德·希区柯克和乔治·库克导演的电影也值得推荐。尽管简单,基于内容的推荐引擎通常提供相关的结果。也很容易实现。该系统可以立即提供推荐,而无需等待用户的显式或隐式反馈,这是一项艰巨的要求,困扰着其他方法,如协同过滤。
不利的一面是,基于内容的过滤在推荐方面缺乏多样性和新颖性。观众有时会想要更广泛的电影选择或更前卫的东西,这可能不是观众个人资料的完美匹配。可扩展性是困扰基于内容的推荐系统的另一个挑战。为了生成高度相关的推荐,基于内容的引擎需要大量关于它推荐的项目的相关领域特定信息。对于一个电影推荐者来说,仅仅根据标题、描述或类型来给出建议是不够的。内部数据可能需要用来自 IMDB 或烂番茄等第三方来源的数据进行扩充。诸如潜在狄利克雷分配(LDA)之类的无监督学习方法可用于从这些数据源提取的元数据中创建新主题。我在第四章中详细讨论了 LDA。
网飞在这一领域处于领先地位,它创建了数千个微引擎,提供高度有针对性的个性化推荐。比如,网飞不仅知道我老婆爱看韩国电影,还知道她喜欢浪漫音乐剧韩国电影,谋杀悬疑僵尸韩国电影,纪录片黑帮韩国僵尸电影,还有她个人最喜欢的法庭剧法律惊悚僵尸韩国电影。通过一个类似土耳其人的机械系统,网飞雇佣了兼职电影爱好者来为其图书馆中的数千部电影和电视节目手动分配描述和类别。最近一次统计显示,网飞有 76897 只独特的小企鹅。这是一种新的个性化水平,在网飞以外的任何地方都没有见过。
Spark MLlib 不包括基于内容的过滤算法,尽管 Spark 有必要的组件来帮助您开发自己的实现。首先,我建议您研究 Spark 中一种高度可扩展的相似性算法,称为“使用 MapReduce 的维度独立矩阵平方”,简称 DIMSUM。查看 https://bit.ly/2YV6qTr
了解更多关于 DIMSUM 的信息。 xx
摘要
每种方法都有自己的优点和缺点。在真实的场景中,通常的做法是构建一个混合推荐引擎,结合多种技术来增强结果。推荐者是研究的沃土。考虑到它为一些世界上最大的公司创造的收入,预计这一领域很快会有更多的发展。FP-Growth 的例子改编自 Bhavin Kukadia 和 Denny Lee 在 Databricks 的工作。XXI
参考
-
勒内·吉拉德;“rené girard and mimetic theory,”仿. org,2019 年,
-
迈克尔·奥斯本;“零售品牌如何利用亚马逊的战术竞争并取胜”,forbes.com,2017,
www.forbes.com/sites/forbesagencycouncil/2017/12/21/how-retail-brands-can-compete-and-win-using-amazons-tactics/#4f4e55bc5e18
-
伊恩·麦肯齐等人;“零售商如何才能跟上消费者”,mckinsey.com,2013,
www.mckinsey.com/industries/retail/our-insights/how-retailers-can-keep-up-with-consumers
-
内森·麦卡龙;《为什么网飞认为其个性化推荐引擎每年价值 10 亿美元》,businessinsider.com,2016,
www.businessinsider.com/netflix-recommendation-engine-worth-1-billion-per-year-2016-6
-
伯纳德·马尔;“中国科技巨头阿里巴巴使用人工智能和机器学习的惊人方式”,2018,
www.forbes.com/sites/bernardmarr/2018/07/23/the-amazing-ways-chinese-tech-giant-alibaba-uses-artificial-intelligence-and-machine-learning/#686ffa0117a9
-
威廉·沃希斯;《5 类推荐人》,datasciencecentral.com,2017,
www.datasciencecentral.com/m/blogpost?id=6448529%3ABlogPost%3A512183
-
卡罗尔·麦克唐纳;《用 Spark 打造推荐引擎》,mapr.com,2015,
https://mapr.com/ebooks/spark/08-recommendation-engine-spark.html
-
Burak Yavuz 等人;“使用 Apache Spark MLlib 的可扩展协同过滤”,Databricks.com,2014,
https://databricks.com/blog/2014/07/23/scalable-collaborative-filtering-with-spark-mllib.html
-
IBM《Apache Spark 实验室简介,第三部分:机器学习》,IBM.com,2017,
https://dataplatform.cloud.ibm.com/exchange/public/entry/view/5ad1c820f57809ddec9a040e37b4af08
-
火花;《类渐冻人症》,spark.apache.org,2019,
https://spark.apache.org/docs/2.0.0/api/java/org/apache/spark/mllib/recommendation/ALS.html
-
罗伯特·贝尔和耶胡达·科伦;“联合导出邻域插值权重的可扩展协同过滤”,acm.org,2007,
https://dl.acm.org/citation.cfm?id=1442050
-
阿帕奇 Spark《协同过滤》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-collaborative-filtering.html
-
阿帕奇 Spark《协同过滤》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/ml-collaborative-filtering.html
-
玛格丽特·劳斯;《关联规则(在数据挖掘中)》,techtarget.com,2018,
https://searchbusinessanalytics.techtarget.com/definition/association-rules-in-data-mining
-
阿帕奇 Spark《频繁模式挖掘》,spark.apache.org,2019,
https://spark.apache.org/docs/2.3.0/mllib-frequent-pattern-mining.html
-
韩佳伟等人。艾尔。;“挖掘无候选代的频繁模式”,acm.org,2000,
https://dl.acm.org/citation.cfm?doid=335191.335372
-
韩家伟,等;“挖掘无候选代的频繁模式”,acm.org,2000,
https://dl.acm.org/citation.cfm?id=335372%C3%DC
-
巴文·库卡迪亚和丹尼·李;“使用数据块上的 FP-growth 简化市场篮子分析”,Databricks.com,2018,
https://databricks.com/blog/2018/09/18/simplify-market-basket-analysis-using-fp-growth-on-databricks.html
-
泰勒·基南;"什么是基于内容的过滤?",upwork.com,2019,
www.upwork.com/hiring/data/what-is-content-based-filtering/
-
礼萨·扎德;“高效的相似性算法现在出现在 Apache Spark,这要归功于 Twitter,”Databricks.com,2014,
https://databricks.com/blog/2014/10/20/efficient-similarity-algorithm-now-in-spark-twitter.html
-
巴文·库卡迪亚和丹尼·李;“使用数据块上的 FP-growth 简化市场篮子分析”,Databricks.com,2018,
https://databricks.com/blog/2018/09/18/simplify-market-basket-analysis-using-fp-growth-on-databricks.html
六、图分析
告诉我我会死在哪里,这样我就不会去那里。
—芒格 i
图分析是一种数据分析技术,用于确定图中对象之间关系的强度和方向。图是用于建模对象之间的关系和过程的数学结构。 ii 它们可以用来表示数据中的复杂关系和依赖关系。图由代表系统中实体的顶点或节点组成。这些顶点由表示这些实体之间关系的边连接。 iii
图介绍
这可能不会立即显而易见,但图无处不在。LinkedIn、脸书和 Twitter 等社交网络就是图。互联网和万维网是图。计算机网络是图。自来水管道是图。道路网络是图。GraphX 等图处理框架有专门设计的图算法和操作符来处理面向图的数据。
无向图
无向图是边没有方向的图。无向图中的边可以在两个方向上遍历,并表示一种双向关系。图 6-1 显示了一个有三个节点和三条边的无向图。iv
图 6-1
无向图
有向图
有向图的边的方向表示单向关系。在有向图中,每条边只能在一个方向上遍历(图 6-2 )。
图 6-2
有向图
有向多重图
一个多重图在节点之间有多条边(图 6-3 )。
图 6-3
有向多重图
属性图
属性图是一种有向多图,其中顶点和边具有用户定义的属性 v (图 6-4 )。
图 6-4
属性图
图分析用例
图分析已经在许多行业蓬勃发展。任何利用大量关联数据的项目都是图分析的完美用例。这并不意味着是用例的全面列表,但是它应该给你一个关于图分析的可能性的想法。
欺诈检测和反洗钱(AML)
欺诈检测和反洗钱可能是图分析最广为人知的使用案例之一。手动筛选数以百万计的银行交易来识别欺诈和洗钱活动令人望而生畏,如果不是不可能的话。通过一系列复杂的相互关联、看似无害的交易来隐藏欺诈的复杂方法加剧了这一问题。检测和防止这些类型的攻击的传统技术在今天已经变得过时了。图分析是解决这一问题的完美方法,它可以轻松地将可疑交易与其他异常行为模式联系起来。像 GraphX 这样的高性能图处理 API 允许非常快速地从一个事务到另一个事务进行复杂的遍历。这方面最著名的例子可能是巴拿马文件丑闻。通过使用图分析识别数百万份泄露文件之间的联系,分析师能够发现知名外国领导人、政治家甚至女王拥有的离岸银行账户中的隐藏资产。 vi
数据治理和法规遵从性
典型的大型组织将数百万个数据文件存储在中央数据存储库中,例如数据湖。这需要适当的主数据管理,这意味着每次修改文件或创建新副本时都要跟踪数据沿袭。通过跟踪数据沿袭,组织可以跟踪数据从源到目标的移动,从而了解数据从一点到另一点的所有变化方式。 vii 一个完善的数据沿袭策略可以增强对数据的信心,并实现准确的业务决策。数据沿袭也是维护数据相关法规遵从性的一项要求,例如通用数据保护法规(GDPR)。违反 GDPR 教被抓可能意味着巨额罚款。图分析非常适合这些用例。
风险管理
为了管理风险,对冲基金和投资银行等金融机构使用图分析来识别可能引发“黑天鹅”事件的相互关联的风险和模式。以 2008 年金融危机为例,图分析可以揭示抵押贷款支持证券(MBS)、债务抵押债券(CDO)和 CDO 部分的信用违约互换(CDs)之间错综复杂的相互依赖关系,从而让人们了解复杂的衍生品证券化过程。
运输
空中交通网络是一个图。机场代表顶点,而路线代表边。商业飞行计划是使用图分析创建的。航空公司和物流公司使用图分析进行路线优化,以确定最安全或最快的路线。这些优化确保了安全性,并且可以节省时间和金钱。
网络社交
社交网络是图分析最直观的用例。如今,几乎每个人都有一张“社交图”,它代表了一个人在社交网络中与其他人或群体的社交关系。如果你在脸书、推特、Instagram 或 LinkedIn 上,你就有了社交图谱。一个很好的例子是脸书使用 Apache Giraph,另一个开源图框架,处理和分析一万亿条边进行内容排名和推荐。 viii
网络基础设施管理
互联网是互联路由器、服务器、台式机、物联网和移动设备的巨大图。政府和企业网络可以被视为互联网的子图。即使是小型家庭网络也是图。设备代表顶点,网络连接代表边。图分析为网络管理员提供了可视化复杂网络拓扑的能力,这对于监控和故障排除非常有用。这对于由数千台相连设备组成的大型企业网络来说尤为重要。
GraphX 简介
GraphX 是 Spark 基于 RDD 的图处理 API。除了图操作符和算法,GraphX 还提供了存储图数据的数据类型。 ix
图
属性图由 graph 类的一个实例表示。就像 rdd 一样,属性图被划分并分布在执行器之间,在崩溃时可以被重新创建。属性图是不可变的,这意味着创建一个新的图对于改变图的结构或值是必要的。 x
VertexRDD
属性图中的顶点由 VertexRDD 表示。每个顶点只有一个条目存储在 VertexRDD 中。
边缘
edge 类包含与源和目标顶点标识符相对应的源和目标 id,以及存储 Edge 属性的属性。Xi
边缘 d
属性图中的边由 EdgeRDD 表示。
边缘三联体
一条边和它所连接的两个顶点的组合由一个 EdgeTriplet 类的实例表示。它还包含边及其连接的顶点的属性。
边缘上下文
EdgeContext 类公开了三元组字段以及向源和目标顶点发送消息的函数。XII
GraphX 示例
对于这个例子(清单 6-1 ,我将使用 GraphX 来分析南加州不同城市之间的距离。我们将基于表 6-1 中的数据构建一个类似图 6-5 的属性图。
图 6-5
属性图
表 6-1
南加州不同城市之间的距离
|来源
|
目的地
|
距离
|
| --- | --- | --- |
| 加利福尼亚州圣莫尼卡 | 皇家海军,CA | 5 英里 |
| 加利福尼亚州圣莫尼卡 | 加利福尼亚州格伦代尔 | 24 英里 |
| 皇家海军,CA | 加利福尼亚州格伦代尔 | 28 英里 |
| 加利福尼亚州格伦代尔 | 加利福尼亚州帕萨迪纳市 | 9 英里 |
| 加利福尼亚州帕萨迪纳市 | 加利福尼亚州格伦代尔 | 9 英里 |
importorg.apache.spark.rdd.RDD
importorg.apache.spark.graphx._
// Define the vertices.
val vertices = Array((1L, ("Santa Monica","CA")),(2L,("Marina Del Rey","CA")),(3L, ("Glendale","CA")),(4L, ("Pasadena","CA")))
valvRDD = sc.parallelize(vertices)
// Define the edges.
val edges = Array(Edge(1L,2L,5),Edge(1L,3L,24),Edge(2L,3L,28),Edge(3L,4L,9),Edge(4L,3L,9))
val eRDD = sc.parallelize(edges)
// Create a property graph.
val graph = Graph(vRDD,eRDD)
graph.vertices.collect.foreach(println)
(3,(Glendale,CA))
(4,(Pasadena,CA))
(1,(Santa Monica,CA))
(2,(Marina Del Rey,CA))
graph.edges.collect.foreach(println)
Edge(1,2,5)
Edge(1,3,24)
Edge(2,3,28)
Edge(3,4,9)
Edge(4,3,9)
//Return the number of vertices.
val numCities = graph.numVertices
numCities: Long = 4
// Return the number of edges.
val numRoutes = graph.numEdges
numRoutes: Long = 5
// Return the number of ingoing edges for each vertex.
graph.inDegrees.collect.foreach(println)
(3,3)
(4,1)
(2,1)
// Return the number of outgoing edges for each vertex.
graph.outDegrees.collect.foreach(println)
(3,1)
(4,1)
(1,2)
(2,1)
// Return all routes that is less than 20 miles.
graph.edges.filter{ case Edge(src, dst, prop) => prop < 20 }.collect.foreach(println)
Edge(1,2,5)
Edge(3,4,9)
Edge(4,3,9)
// The EdgeTriplet class includes the source and destination attributes.
graph.triplets.collect.foreach(println)
((1,(Santa Monica,CA)),(2,(Marina Del Rey,CA)),5)
((1,(Santa Monica,CA)),(3,(Glendale,CA)),24)
((2,(Marina Del Rey,CA)),(3,(Glendale,CA)),28)
((3,(Glendale,CA)),(4,(Pasadena,CA)),9)
((4,(Pasadena,CA)),(3,(Glendale,CA)),9)
// Sort by farthest route.
graph.triplets.sortBy(_.attr, ascending=false).collect.foreach(println)
((2,(Marina Del Rey,CA)),(3,(Glendale,CA)),28)
((1,(Santa Monica,CA)),(3,(Glendale,CA)),24)
((3,(Glendale,CA)),(4,(Pasadena,CA)),9)
((4,(Pasadena,CA)),(3,(Glendale,CA)),9)
((1,(Santa Monica,CA)),(2,(Marina Del Rey,CA)),5)
// mapVertices applies a user-specified function to every vertex.
val newGraph = graph.mapVertices((vertexID,state) => "TX")
newGraph.vertices.collect.foreach(println)
(3,TX)
(4,TX)
(1,TX)
(2,TX)
// mapEdges applies a user-specified function to every edge.
val newGraph = graph.mapEdges((edge) => "500")
Edge(1,2,500)
Edge(1,3,500)
Edge(2,3,500)
Edge(3,4,500)
Edge(4,3,500)
Listing 6-1GraphX examplexiii
图算法
GraphX 提供了几种常见图算法的内置实现,如 PageRank、三角形计数和连通分量。
网页等级
PageRank 是一种算法,最初由 Google 开发,用于确定网页的重要性。具有较高 PageRank 的网页比具有较低 PageRank 的网页更相关。网页的排名取决于链接到它的网页的排名;因此,这是一个迭代算法。高质量链接的数量也有助于页面的 PageRank。GraphX 包含 PageRank 的内置实现。GraphX 带有静态和动态版本的 PageRank。
动态页面排名
动态 PageRank 执行,直到等级停止更新超过指定的容差(即,直到等级收敛)。
val dynamicPageRanks = graph.pageRank(0.001).vertices
val sortedRanks = dynamicPageRanks.sortBy(_._2,ascending=false)
sortedRanks.collect.foreach(println)
(3,1.8845795504535865)
(4,1.7507334787248419)
(2,0.21430059110133595)
(1,0.15038637972023575)
静态页面排名
静态 PageRank 执行一定次数。
val staticPageRanks = graph.staticPageRank(10)
val sortedRanks = staticPageRanks.vertices.sortBy(_._2,ascending=false)
sortedRanks.collect.foreach(println)
(4,1.8422463479403317)
(3,1.7940036520596683)
(2,0.21375000000000008)
(1,0.15000000000000005)
三角形计数
三角形由三个相连的顶点组成。三角形计数算法通过确定穿过每个顶点的三角形数量来提供聚类的度量。在图 6-5 中,圣莫尼卡、玛丽娜·德雷和格伦代尔都是三角形的一部分,而帕萨迪纳不是。
val triangleCount = graph.triangleCount()
triangleCount.vertices.collect.foreach(println)
(3,1)
(4,0)
(1,1)
(2,1)
连接组件
连通分量算法确定子图中每个顶点的成员。该算法返回子图中编号最低的顶点的顶点 id 作为该顶点的属性。图 6-6 显示了两个连接的部件。我在清单 6-2 中展示了一个例子。
图 6-6
连接的组件
val vertices = Array((1L, ("Santa Monica","CA")),(2L,("Marina Del Rey","CA")),(3L, ("Glendale","CA")),(4L, ("Pasadena","CA")),(5L, ("Anchorage","AK")),(6L, ("Fairbanks","AK")),(7L, ("Sterling","AK")),(8L, ("Wasilla","AK")))
val vRDD = sc.parallelize(vertices)
val edges = Array(Edge(1L,2L,5),Edge(1L,3L,24),Edge(2L,3L,28),Edge(3L,4L,9),Edge(4L,3L,9),Edge(5L,6L,32),Edge(6L,7L,28),Edge(5L,8L,17))
val eRDD = sc.parallelize(edges)
val graph = Graph(vRDD,eRDD)
graph.vertices.collect.foreach(println)
(6,(Fairbanks,AK))
(3,(Glendale,CA))
(4,(Pasadena,CA))
(1,(Santa Monica,CA))
(7,(Sterling,AK))
(8,(Wasilla,AK))
(5,(Anchorage,AK))
(2,(Marina Del Rey,CA))
graph.edges.collect.foreach(println)
Edge(1,2,5)
Edge(1,3,24)
Edge(2,3,28)
Edge(3,4,9)
Edge(4,3,9)
Edge(5,6,32)
Edge(5,8,17)
Edge(6,7,28)
val connectedComponents = graph.connectedComponents()
connectedComponents.vertices.collect.foreach(println)
(6,5)
(3,1)
(4,1)
(1,1)
(7,5)
(8,5)
(5,5)
(2,1)
Listing 6-2Determine the Connected Components
图框架
GraphFrames 是一个建立在 DataFrames 之上的图处理库。在撰写本文时,GraphFrames 仍在积极开发中,但它成为核心 Apache Spark 框架的一部分只是时间问题。有几件事让 GraphFrames 比 GraphX 更强大。所有 GraphFrames 算法在 Java、Python 和 Scala 中都可用。可以使用熟悉的 DataFrames API 和 Spark SQL 来访问 GraphFrames。它还完全支持 DataFrame 数据源,允许使用几种支持的格式和数据源(如关系数据源、CSV、JSON 和 Parquet)来读取和写入图。 xiv 清单 6-3 展示了一个如何使用 GraphFrames 的例子。 xv
spark-shell --packages graphframes:graphframes:0.7.0-spark2.4-s_2.11
import org.graphframes._
val vDF = spark.createDataFrame(Array((1L, "Santa Monica","CA"),(2L,"Marina Del Rey","CA"),(3L, "Glendale","CA"),(4L, "Pasadena","CA"),(5L, "Anchorage","AK"),(6L, "Fairbanks","AK"),(7L, "Sterling","AK"),(8L, "Wasilla","AK"))).toDF("id","city","state")
vDF.show
+---+--------------+-----+
| id| city|state|
+---+--------------+-----+
| 1| Santa Monica| CA|
| 2|Marina Del Rey| CA|
| 3| Glendale| CA|
| 4| Pasadena| CA|
| 5| Anchorage| AK|
| 6| Fairbanks| AK|
| 7| Sterling| AK|
| 8| Wasilla| AK|
+---+--------------+-----+
val eDF = spark.createDataFrame(Array((1L,2L,5),(1L,3L,24),(2L,3L,28),(3L,4L,9),(4L,3L,9),(5L,6L,32),(6L,7L,28),(5L,8L,17))).toDF("src","dst","distance")
eDF.show
+---+----+--------+
|src|dest|distance|
+---+----+--------+
| 1| 2| 5|
| 1| 3| 24|
| 2| 3| 28|
| 3| 4| 9|
| 4| 3| 9|
| 5| 6| 32|
| 6| 7| 28|
| 5| 8| 17|
+---+----+--------+
val graph = GraphFrame(vDF,eDF)
graph.vertices.show
+---+--------------+-----+
| id| city|state|
+---+--------------+-----+
| 1| Santa Monica| CA|
| 2|Marina Del Rey| CA|
| 3| Glendale| CA|
| 4| Pasadena| CA|
| 5| Anchorage| AK|
| 6| Fairbanks| AK|
| 7| Sterling| AK|
| 8| Wasilla| AK|
+---+--------------+-----+
graph.edges.show
+---+---+--------+
|src|dst|distance|
+---+---+--------+
| 1| 2| 5|
| 1| 3| 24|
| 2| 3| 28|
| 3| 4| 9|
| 4| 3| 9|
| 5| 6| 32|
| 6| 7| 28|
| 5| 8| 17|
+---+---+--------+
graph.triplets.show
+--------------------+----------+--------------------+
| src| edge| dst|
+--------------------+----------+--------------------+
|[1, Santa Monica,...|[1, 3, 24]| [3, Glendale, CA]|
|[1, Santa Monica,...| [1, 2, 5]|[2, Marina Del Re...|
|[2, Marina Del Re...|[2, 3, 28]| [3, Glendale, CA]|
| [3, Glendale, CA]| [3, 4, 9]| [4, Pasadena, CA]|
| [4, Pasadena, CA]| [4, 3, 9]| [3, Glendale, CA]|
| [5, Anchorage, AK]|[5, 8, 17]| [8, Wasilla, AK]|
| [5, Anchorage, AK]|[5, 6, 32]| [6, Fairbanks, AK]|
| [6, Fairbanks, AK]|[6, 7, 28]| [7, Sterling, AK]|
+--------------------+----------+--------------------+
graph.inDegrees.show
+---+--------+
| id|inDegree|
+---+--------+
| 7| 1|
| 6| 1|
| 3| 3|
| 8| 1|
| 2| 1|
| 4| 1|
+---+--------+
graph.outDegrees.show
+---+---------+
| id|outDegree|
+---+---------+
| 6| 1|
| 5| 2|
| 1| 2|
| 3| 1|
| 2| 1|
| 4| 1|
+---+---------+
sc.setCheckpointDir("/tmp")
val connectedComponents = graph.connectedComponents.run
connectedComponents.show
+---+--------------+-----+---------+
| id| city|state|component|
+---+--------------+-----+---------+
| 1| Santa Monica| CA| 1|
| 2|Marina Del Rey| CA| 1|
| 3| Glendale| CA| 1|
| 4| Pasadena| CA| 1|
| 5| Anchorage| AK| 5|
| 6| Fairbanks| AK| 5|
| 7| Sterling| AK| 5|
| 8| Wasilla| AK| 5|
+---+--------------+-----+---------+
Listing 6-3GraphFrames Example
摘要
本章介绍使用 GraphX 和 GraphFrames 进行图分析。图分析是一个令人兴奋的快速发展的研究领域,具有广泛而深远的应用。我们只是触及了 GraphX 和 GraphFrames 的皮毛。虽然它可能不适合每个用例,但图分析对于它的设计用途来说是很棒的,并且是您的分析工具集不可或缺的一部分。
参考
-
迈克尔·西蒙斯;《白手起家的亿万富翁查理·芒格有哪些与众不同的做法》,inc.com,2019,
www.inc.com/michael-simmons/what-self-made-billionaire-charlie-munger-does-differently.html
-
卡罗尔·麦克唐纳;《如何在 Scala 上使用 Apache Spark GraphX 入门》,mapr.com,2015,
https://mapr.com/blog/how-get-started-using-apache-spark-graphx-scala/
-
英伟达;“图分析”,开发者。英伟达。com ,2019,
https://developer.nvidia.com/discover/graph-analytics
-
MathWorks《有向和无向图》,mathworks.com,2019,
www.mathworks.com/help/matlab/math/directed-and-undirected-graphs.html
-
Rishi Yadav“第十一章,使用 GraphX 和 GraphFrames 进行图处理”,Packt Publishing,2017,Apache Spark 2.x Cookbook
-
沃克·罗;“图数据库的用例”,bmc.com,2019,
www.bmc.com/blogs/graph-database-use-cases/
-
罗布·佩里;“数据传承:GDPR 全面合规的关键”,insidebigdata.com,2018,
https://insidebigdata.com/2018/01/29/data-lineage-key-total-gdpr-compliance/
-
Avery Ching 等人;《一万亿条边:脸书尺度的图处理》,research.fb.com,2015,
https://research.fb.com/publications/one-trillion-edges-graph-processing-at-facebook-scale/
-
穆罕默德·古勒;“使用 Spark 进行图处理”,Apress,2015,使用 Spark 进行大数据分析
-
火花;《房产图》,spark.apache.org,2015,
https://spark.apache.org/docs/latest/graphx-programming-guide.html#the-property-graph
-
火花;《示例属性图》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/graphx-programming-guide.html#the-property-graph
-
火花;《地图减少三胞胎过渡指南》,spark.apache.org,2019,
https://spark.apache.org/docs/latest/graphx-programming-guide.html#the-property-graph
-
卡罗尔·麦克唐纳;《如何在 Scala 上使用 Apache Spark GraphX 入门》,mapr.com,2015,
https://mapr.com/blog/how-get-started-using-apache-spark-graphx-scala/
-
安库尔·戴夫等人;《GraphFrames 简介》,Databricks.com,2016,
https://databricks.com/blog/2016/03/03/introducing-graphframes.html
-
Rishi Yadav“第十一章,使用 GraphX 和 GraphFrames 进行图处理”,Packt Publishing,2017,Apache Spark 2.x Cookbook
七、深度学习
明斯基和帕佩特如果能找到解决这个问题的方法,而不是把感知机打死,他们会做出更大的贡献。
—弗朗西斯·克里克 一世
深度学习是机器学习和人工智能的一个子领域,它使用深度、多层人工神经网络(图 7-1 )。由于它能够非常精确地执行类似人类的任务,所以最近非常流行。深度学习领域的进步已经在计算机视觉、语音识别、游戏和异常检测等领域开辟了令人兴奋的可能性。GPU(图形处理器)的主流可用性几乎在一夜之间推动了人工智能在全球的采用。高性能计算已经变得足够强大,以至于仅仅几年前在计算上似乎不可能的任务现在都可以在多 GPU 云实例或廉价机器集群上例行执行。这使得人工智能领域的众多创新以过去不可能的速度发展。
图 7-1
AI、机器学习、深度学习的关系 ii
深度学习是人工智能领域最近许多突破的原因。虽然深度学习可以用于更平凡的分类任务,但当应用于更复杂的问题时,如医疗诊断、面部识别、自动驾驶汽车、欺诈分析和智能语音控制助理,它的真正力量会大放异彩。 iii 在某些领域,深度学习已经使计算机能够匹配甚至超越人类的能力。例如,通过深度学习,谷歌 DeepMind 的 AlphaGo 程序能够在围棋中击败韩国大师李世石,这是有史以来最复杂的棋盘游戏之一。围棋比国际象棋等其他棋类游戏复杂得多。它有 10 的 170 次方个可能的板配置,这比宇宙中的原子数量还要多。 iv 另一个很好的例子是特斯拉。特斯拉使用深度学习 v 来为其自动驾驶功能提供动力,处理来自其制造的每辆汽车中内置的几个环绕摄像头、超声波传感器和前向雷达的数据。为了向其深度神经网络提供高性能处理能力,特斯拉为其自动驾驶汽车配备了一台使用自己的人工智能芯片的计算机,配备了 GPU、CPU 和深度学习加速器,每秒可进行 144 万亿次运算(TOPS)。 vi
与其他机器学习算法类似,深度神经网络通常在更多的训练数据下表现更好。这就是火花出现的原因。但是,尽管 Spark MLlib 包括一系列广泛的机器学习算法,但在撰写本文时,其深度学习支持仍然有限(Spark MLlib 包括一个多层感知器分类器,一个完全连接的前馈神经网络,仅限于训练浅层网络和执行迁移学习)。然而,由于谷歌和脸书等开发商和公司的社区,有几个第三方分布式深度学习库和框架与 Spark 集成。我们将探索一些最受欢迎的。在这一章中,我将关注最流行的深度学习框架之一:Keras。对于分布式深度学习,我们将使用 Elephas 和分布式 Keras (Dist-Keras)。我们将在本章通篇使用 Python。我们将使用 PySpark 作为我们的分布式深度学习示例。
神经网络
神经网络是一类算法,其操作类似于人脑的互连神经元。神经网络包含由互连节点组成的多层。通常有一个输入层,一个或多个隐藏层,一个输出层(图 7-2 )。数据经由输入层通过神经网络。隐藏层通过加权连接网络处理数据。隐藏层中的节点为输入分配权重,并将其与一组系数组合。数据通过一个节点的激活函数,该函数决定了层的输出。最后,数据到达输出层,输出层产生神经网络的最终输出。VII
图 7-2
一个简单的神经网络
具有几个隐藏层的神经网络被称为“深度”神经网络。层次越多,网络就越深,通常网络越深,学习就变得越复杂,它能解决的问题也越复杂。这就是赋予深度神经网络达到最先进精度的能力。有几种类型的神经网络:前馈神经网络、循环神经网络和自动编码器神经网络,每种神经网络都有自己的功能,并为不同的目的而设计。前馈神经网络或多层感知器适用于结构化数据。对于音频、时间序列或文本等序列数据,循环神经网络是一个很好的选择。 viii 自动编码器是异常检测和降维的好选择。一种类型的神经网络,卷积神经网络,由于其在计算机视觉领域的革命性性能,最近在世界上掀起了风暴。
神经网络简史
第一个神经网络,麦卡洛克-皮茨神经元,是由沃伦·麦卡洛克和沃尔特·皮茨在 1943 年开发的,并在他们的开创性论文“神经活动内在思想的逻辑演算”中进行了描述 ix 麦卡洛克-皮茨神经元并不像现代的神经网络,但它是为我们今天所知的人工智能的诞生铺平道路的种子。弗兰克·罗森布拉特于 1958 年推出的感知机是神经网络领域的下一个革命性发展。Rosenblatt 感知器是一个简单的单层神经网络,用于二进制分类。它是在康奈尔航空实验室构思的,最初是用 IBM 704 用软件模拟的。罗森布拉特的工作最终创造了 Mark I 感知器,这是一种为图像识别而定制的机器。20 世纪 60 年代末,在马文·明斯基和西蒙·派珀特出版了《?? 感知器》之后,人们对感知器的兴趣开始减弱,这本极具影响力的书阐述了感知器的局限性,尤其是它无法学习非线性函数。明斯基和帕佩特对联结主义模型的过分热情和有时充满敌意(多年后他们承认了这一点)的批判导致了 20 世纪 80 年代人工智能研究的兴趣和资金减少。这种情况进一步加剧,部分原因是支持向量机(SVM)和图形模型的成功,以及符号人工智能(从 20 世纪 50 年代到 80 年代末流行的人工智能范式)的日益流行。尽管如此,许多科学家和研究人员,如吉姆·安德森、大卫·威尔肖和斯蒂芬·格罗斯伯格,仍然默默地继续着他们对神经网络的研究。1975 年,Kunihiko Fukushima 在日本东京 Segataya 的 NHK 科学技术研究实验室担任研究科学家,开发了第一个多层神经网络 neocognitron。 十三
神经网络在 20 世纪 80 年代中期再次兴起,在此期间,许多先驱发明了许多对现代深度学习至关重要的基本概念和技术。1986 年,Geoffrey Hinton(被许多人视为“人工智能教父”)推广了反向传播,这是神经网络学习的基本方法,也是几乎所有现代神经网络实现的核心。 xiv 同样重要的是,Hinton 和他的团队推广了使用多层神经网络学习非线性函数的想法,直接解决了 Minsky 和 Papert 在感知器中的批评。辛顿的工作有助于将神经网络从默默无闻中带回到聚光灯下。Yann LeCun 受福岛新认知神经的启发,在 20 世纪 80 年代末开发了卷积神经网络。1988 年在贝尔实验室时,Yann LeCun 使用卷积神经网络来识别手写字符。美国电话电报公司将该系统出售给银行,以读取和识别支票上的笔迹,并被认为是神经网络的第一个现实应用。XVIyo shua beng io 因其引入了单词嵌入的概念以及最近与 Ian Goodfellow 合作研究生成对抗网络(GANs)而闻名,生成对抗网络是一种深度神经网络架构,由两个彼此对立或“对抗”的神经网络组成。Yann LeCun 将对抗性训练描述为“过去 10 年机器学习中最有趣的想法。” xvii 三位先驱最近获得了 2018 年的图灵奖。
尽管如此,尽管有这些创新,但神经网络的实际应用仍然不是每个人都能达到的,而是最大的公司和资金充足的大学,因为需要大量的计算资源来训练它们。正是经济实惠的高性能 GPU 的广泛使用将神经网络推向了主流。以前被认为是不可扩展和不切实际的训练,神经网络突然开始在计算机视觉、语音识别、游戏和其他历史上困难的机器学习任务领域提供类似人类的性能,使用 GPU 具有非凡的准确性。当 Alex Krizhevsky、Ilya Sutskever 和 Geoff Hinton 开发的经过 GPU 训练的卷积神经网络 AlexNet 在 2012 年赢得 ImageNet 大规模视觉识别比赛时,世界受到了关注。 xviii 它以如此大的优势赢得了比赛,比亚军低了 10.8 个百分点,前五名的误差为 15.3%。 xix AlexNet 还普及了现在在大多数 CNN 架构中常见的大部分原则,如使用整流线性单元(ReLU)激活功能,使用辍学和数据增强来减少过拟合,重叠池,以及在多个 GPU 上训练。
最近,学术界和私营部门在人工智能领域都有前所未有的创新。谷歌、亚马逊、微软、脸书、IBM、特斯拉、英伟达等公司正在深度学习领域投入大量资源,进一步拓展人工智能的边界。斯坦福大学、卡内基梅隆大学和麻省理工学院等领先大学以及 OpenAI 和艾伦人工智能研究所等研究机构正在以惊人的速度发表突破性的研究。从全球来看,令人印象深刻的创新来自中国、加拿大、英国和澳大利亚等国家。
卷积神经网络
卷积神经网络(简称为 convnet 或 CNN)是一种特别擅长分析图像的神经网络(尽管它们也可以应用于音频和文本数据)。卷积神经网络层中的神经元以三维方式排列:高度、宽度和深度。CNN 使用卷积层来学习其输入特征空间(图像)中的局部模式,例如纹理和边缘。相反,全连接(密集)层学习全局模式。 xx 卷积层中的神经元仅连接到其之前层的一小部分区域,而不是像密集层那样连接到所有神经元。密集层的完全连接结构会导致大量的参数,这是低效的,并可能很快导致过度拟合。XXI
在我们继续讨论卷积神经网络之前,理解颜色模型的概念是很重要的。图像被表示为像素的集合。 xxii 像素是构成图像的最小信息单位。灰度图像的像素值范围从 0(黑色)到 255(白色),如图 7-3 (a)所示。灰度图像只有一个通道。RGB 是最传统的颜色模型。RGB 图像有三个通道:红色、绿色和蓝色。这意味着 RGB 图像中的每个像素由三个 8 位数字表示(图 7-3 (b)),每个数字的范围从 0 到 255。使用这些通道的不同组合会显示不同的颜色。
图 7-3
灰度和 RGB 图像
卷积神经网络接受形状的三维张量作为输入:图像的高度、重量和深度(图 7-4 )。图像的深度是通道的数量。对于灰度图像,深度为 1,而对于 RGB 图像,深度为 3。在图 7-4 中,图像的输入形状为(7,8,3)。
图 7-4
单个图像的输入形状
卷积神经网络体系结构
图 7-5 显示了典型的 CNN 架构的样子。卷积神经网络由几层组成,每层都试图识别和学习各种特征。层的主要类型是卷积层、汇集层和全连接层。这些层可以进一步分为两个主要类别:特征检测层和分类层。
图 7-5
一种用于分类动物图像的 CNN 架构XXIII
特征检测图层
卷积层
卷积层是卷积神经网络的主要构件。卷积层使输入图像通过一系列激活输入图像的某些特征的卷积核或滤波器。卷积是一种数学运算,当内核在输入特征地图上滑动(或跨越)时,在输入元素和内核元素重叠的每个位置执行逐元素乘法。将结果相加以产生当前位置的输出。对每一步重复该过程,生成称为输出特征地图的最终结果。XXIV图 7-6 展示了一个 3×3 的卷积核。图 7-7 显示了一个 2D 卷积,其中我们的 3 x 3 内核被应用于一个 5 x 5 输入特征映射,产生一个 3 x 3 输出特征映射。我们使用步长 1,这意味着内核从当前位置移动一个像素到下一个像素。
图 7-7
一种 2D 卷积,其中将 3 x 3 内核应用于 5 x 5 输入要素地图(25 个要素),从而生成一个包含 9 个输出要素的 3 x 3 输出要素地图
图 7-6
3×3 卷积核
对于 RGB 图像,3D 卷积涉及每个输入通道的一个核(总共 3 个核)。所有结果相加形成一个输出。添加用于帮助激活函数更好地拟合数据的偏差项,以创建最终的输出特征图。
整流线性单元(ReLU)激活功能
通常的做法是在每个卷积层之后包括一个激活层。激活函数也可以通过所有转发层支持的激活参数来指定。激活函数将节点的输入转换成输出信号,该输出信号用作下一层的输入。激活功能通过允许神经网络学习更复杂的数据,如图像、音频和文本,使神经网络更强大。它通过向我们的神经网络引入非线性特性来做到这一点。如果没有激活函数,神经网络将只是一个过于复杂的线性回归模型,只能处理简单的线性问题。 xxv Sigmoid 和 tanh 也是常见的激活层。整流线性单元(ReLU)是大多数 CNN 最流行和首选的激活功能。ReLU 返回它收到的任何实际输入的值,但如果收到负值,则返回 0。已经发现 ReLU 有助于加速模型训练,而不会显著影响准确性。
汇集层
池图层通过缩小输入影像的维度来降低计算复杂度和参数数量。通过减少维度,池层也有助于控制过度拟合。在每个卷积层之后插入一个汇集层也是很常见的。有两种主要的池:平均池和最大池。平均池使用来自每个池区域的所有值的平均值(图 7-8 (b)),而最大池使用最大值(图 7-8 (a))。在某些架构中,使用更大的跨度比使用池层更可取。
图 7-8
最大和平均池
分类层
展平图层
在将数据馈送到完全连接的密集图层之前,展平图层会将二维矩阵转换为一维向量。
全连接(密集)层
完全连接的图层(也称为密集图层)接收来自平整图层的数据,并输出包含每个类概率的矢量。
脱落层
在训练期间,一个脱落层随机地去激活网络中的一些神经元。丢弃是一种正则化形式,有助于降低模型复杂性并防止过度拟合。
Softmax 和 Sigmoid 函数
最终的密集层提供分类输出。它将 softmax 函数用于多类分类任务,或者将 sigmoid 函数用于二元分类任务。
深度学习框架
本节提供了当前可用的一些最流行的深度学习框架的快速概述。这绝不是一份详尽的清单。这些框架中的大多数或多或少都有相同的特性。它们在社区和行业采用程度、受欢迎程度和生态系统规模方面有所不同。
TensorFlow
TensorFlow 是目前最流行的深度学习框架。它是谷歌开发的,作为 Theano 的替代品。Theano 最初的一些开发者去了 Google,开发了 TensorFlow。TensorFlow 提供了一个运行在用 C/C++开发的引擎之上的 Python API。
提亚诺
Theano 是最早的开源深度学习框架之一。它最初由蒙特利尔大学的蒙特利尔学习算法研究所(MILA)于 2007 年发布。2017 年 9 月,Yoshua Bengio 正式宣布将结束对 Theano 的开发。XXVI
PyTorch
PyTorch 是由脸书人工智能研究(FAIR)小组开发的开源深度学习库。PyTorch 用户采用率最近一直在飙升,是第三大最受欢迎的框架,仅次于 TensorFlow 和 Keras。
深度学习 4J
DeepLearning4J 是一个用 Java 编写的深度学习库。它与 Scala 等基于 JVM 的语言兼容,并与 Spark 和 Hadoop 集成。它使用自己的开源科学计算库 ND4J,而不是 Breeze。对于更喜欢 Java 或 Scala 而不是 Python 进行深度学习的开发人员来说,DeepLearning4J 是一个很好的选择(尽管 DeepLearning4J 有一个使用 Keras 的 Python API)。
CNT(消歧义)
CNTK 又称微软认知工具包,是微软研究院开发的深度学习库,于 2015 年 4 月开源。它使用一系列计算步骤,使用有向图来描述神经网络。CNTK 支持跨多个 GPU 和服务器的分布式深度学习。
硬
Keras 是由 Francois Chollet 在谷歌开发的高级深度学习框架。它提供了一个简单的模块化 API,可以运行在 TensorFlow、Theano 和 CNTK 之上。它享有广泛的行业和社区采用和充满活力的生态系统。Keras 模型可以部署在 iOS 和 Android 设备上,通过 Keras.js 和 WebDNN 部署在浏览器中,部署在 Google Cloud 上,通过 Skymind 的 DL4J 导入功能部署在 JVM 上,甚至部署在 Raspberry Pi 上。Keras 开发主要由谷歌支持,但微软、亚马逊、英伟达、优步和苹果也是主要的项目贡献者。像其他框架一样,它具有出色的多 GPU 支持。对于最复杂的深度神经网络,Keras 通过 Horovod、Google Cloud 上的 GPU 集群以及 Dist-Keras 和 Elephas 上的 Spark 支持分布式训练。
使用 Keras 进行深度学习
我们将使用 Keras Python API 和 TensorFlow 作为所有深度学习示例的后端。我们将在本章后面的分布式深度学习示例中使用 PySpark 和 Elephas 以及分布式 Keras。
使用 Iris 数据集的多类分类
我们将使用一个熟悉的数据集进行我们的第一个神经网络分类 xxvii 任务(列表 7-1 )。如前所述,我们将在本章的所有例子中使用 Python。
import numpy as np
from keras.models import Sequential
from keras.optimizers import Adam
from keras.layers import Dense
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
from sklearn.datasets import load_iris
# Load the Iris dataset.
iris_data = load_iris()
x = iris_data.data
# Inspect the data. We'll truncate
# the output for brevity.
print(x)
[[5.1 3.5 1.4 0.2]
[4.9 3\. 1.4 0.2]
[4.7 3.2 1.3 0.2]
[4.6 3.1 1.5 0.2]
[5\. 3.6 1.4 0.2]
[5.4 3.9 1.7 0.4]
[4.6 3.4 1.4 0.3]
[5\. 3.4 1.5 0.2]
[4.4 2.9 1.4 0.2]
[4.9 3.1 1.5 0.1]]
# Convert the class label into a single column.
y = iris_data.target.reshape(-1, 1)
# Inspect the class labels. We'll truncate
# the output for brevity.
print(y)
[[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]
[0]]
# It's considered best practice to one-hot encode the class labels
# when using neural networks for multiclass classification tasks.
encoder = OneHotEncoder(sparse=False)
enc_y = encoder.fit_transform(y)
print(enc_y)
[[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]
[1\. 0\. 0.]]
# Split the data for training and testing.
# 70% for training dataset, 30% for test dataset.
train_x, test_x, train_y, test_y = train_test_split(x, enc_y, test_size=0.30)
# Define the model.
# We instantiate a Sequential model consisting of a linear stack of layers.
# Since we are dealing with a one-dimensional feature vector, we will use
# dense layers in our network. We use convolutional layers later in the
# chapter when working with images.
# We use ReLU as the activation function on our fully connected layers.
# We can name any layer by passing the name argument. We pass the number
# of features to the input_shape argument, which is 4 (petal_length,
# petal_width, sepal_length, sepal_width)
model = Sequential()
model.add(Dense(10, input_shape=(4,), activation="relu", name="fclayer1"))
model.add(Dense(10, activation="relu", name="fclayer2"))
# We use softmax activation function in our output layer. As discussed,
# using softmax activation layer allows the model to perform multiclass
# classification. For binary classification, use the sigmoid activation
# function instead. We specify the number of class, which in our case is 3.
model.add(Dense(3, activation="softmax", name="output"))
# Compile the model. We use categorical_crossentropy for multiclass
# classification. For binary classification, use binary_crossentropy.
model.compile(loss='categorical_crossentropy', optimizer="adam", metrics=['accuracy'])
# Display the model summary.
print(model.summary())
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
fclayer1 (Dense) (None, 10) 50
_________________________________________________________________
fclayer2 (Dense) (None, 10) 110
_________________________________________________________________
output (Dense) (None, 3) 33
=================================================================
Total params: 193
Trainable params: 193
Non-trainable params: 0
_________________________________________________________________
None
# Train the model on training dataset with epochs set to 100
# and batch size to 5\. The output is edited for brevity.
model.fit(train_x, train_y, verbose=2, batch_size=5, epochs=100)
Epoch 1/100
- 2s - loss: 1.3349 - acc: 0.3333
Epoch 2/100
- 0s - loss: 1.1220 - acc: 0.2952
Epoch 3/100
- 0s - loss: 1.0706 - acc: 0.3429
Epoch 4/100
- 0s - loss: 1.0511 - acc: 0.3810
Epoch 5/100
- 0s - loss: 1.0353 - acc: 0.3810
Epoch 6/100
- 0s - loss: 1.0175 - acc: 0.3810
Epoch 7/100
- 0s - loss: 1.0013 - acc: 0.4000
Epoch 8/100
- 0s - loss: 0.9807 - acc: 0.4857
Epoch 9/100
- 0s - loss: 0.9614 - acc: 0.6667
Epoch 10/100
- 0s - loss: 0.9322 - acc: 0.6857
...
Epoch 97/100
- 0s - loss: 0.1510 - acc: 0.9524
Epoch 98/100
- 0s - loss: 0.1461 - acc: 0.9810
Epoch 99/100
- 0s - loss: 0.1423 - acc: 0.9810
Epoch 100/100
- 0s - loss: 0.1447 - acc: 0.9810
<keras.callbacks.History object at 0x7fbb93a50510>
# Test on test dataset.
results = model.evaluate(test_x, test_y)
45/45 [==============================] - 0s 586us/step
print('Accuracy: {:4f}'.format(results[1]))
Accuracy: 0.933333
Listing 7-1Multiclass Classification with Keras Using the Iris Dataset
那很有趣。然而,当用于涉及图像等非结构化数据的更复杂问题时,神经网络确实大放异彩。对于我们的下一个例子,我们将使用卷积神经网络来执行手写数字识别。
用 MNIST 识别手写数字
MNIST 数据集是来自美国国家标准和技术研究所(NIST)的手写数字图像的数据库。该数据集包含 70,000 幅图像:60,000 幅图像用于训练,10,000 幅图像用于测试。都是 0 到 9 的 28 x 28 灰度图像(图 7-9 )。见清单 7-2 。
图 7-9
来自 MNIST 手写数字数据库的样本图像
import keras
import matplotlib.pyplot as plt
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D
# Download MNIST data.
(x_train, y_train), (x_test, y_test) = mnist.load_data()
image_idx = 400
print(y_train[image_idx])
# Inspect the data.
2
plt.imshow(x_train[image_idx], cmap="Greys")
plt.show()
Listing 7-2Handwritten Digit Recognition with MNIST Using Keras
# Check another number.
image_idx = 600
print(y_train[image_idx])
9
plt.imshow(x_train[image_idx], cmap="Greys")
plt.show()
# Get the "shape" of the dataset.
x_train.shape
(60000, 28, 28)
# Let’s reshape the array to 4-dims so that it can work with Keras.
# The images are 28 x 28 grayscale. The last parameter, 1, indicates
# that images are grayscale. For RGB, set the parameter to 3.
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)
# Convert the values to float before division.
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
# Normalize the RGB codes.
x_train /= 255
x_test /= 255
print'x_train shape: ', x_train.shape
x_train shape: 60000, 28, 28, 1
print'No. of images in training dataset: ', x_train.shape[0]
No. of images in training dataset: 60000
print'No. of images in test dataset: ', x_test.shape[0]
No. of images in test dataset: 10000
# Convert class vectors to binary class matrices. We also pass
# the number of classes (10).
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
# Build the CNN. Create a Sequential model and add the layers.
model = Sequential()
model.add(Conv2D(28, kernel_size=(3,3), input_shape= (28,28,1), name="convlayer1"))
# Reduce the dimensionality using max pooling.
model.add(MaxPooling2D(pool_size=(2, 2)))
# Next we need to flatten the two-dimensional arrays into a one-dimensional feature vector. This will allow us to perform classification.
model.add(Flatten())
model.add(Dense(128, activation="relu",name='fclayer1'))
# Before we classify the data, we use a dropout layer to randomly
# deactivate some of the neurons. Dropout is a form of regularization
# used to help reduce model complexity and prevent overfitting.
model.add(Dropout(0.2))
# We add the final layer. The softmax layer (multinomial logistic
# regression) should have the total number of classes as parameter.
# In this case the number of parameters is the number of digits (0–9)
# which is 10.
model.add(Dense(10,activation='softmax', name="output"))
# Compile the model.
model.compile(optimizer='adam', loss="categorical_crossentropy", metrics=['accuracy'])
# Train the model.
model.fit(x_train,y_train,batch_size=128, verbose=1, epochs=20)
Epoch 1/20
60000/60000 [==============================] - 11s 180us/step - loss: 0.2742 - acc: 0.9195
Epoch 2/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.1060 - acc: 0.9682
Epoch 3/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0731 - acc: 0.9781
Epoch 4/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0541 - acc: 0.9830
Epoch 5/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0409 - acc: 0.9877
Epoch 6/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0337 - acc: 0.9894
Epoch 7/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0279 - acc: 0.9910
Epoch 8/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0236 - acc: 0.9922
Epoch 9/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0200 - acc: 0.9935
Epoch 10/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0173 - acc: 0.9940
Epoch 11/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0163 - acc: 0.9945
Epoch 12/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0125 - acc: 0.9961
Epoch 13/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0129 - acc: 0.9956
Epoch 14/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0125 - acc: 0.9958
Epoch 15/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0102 - acc: 0.9968
Epoch 16/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0101 - acc: 0.9964
Epoch 17/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0096 - acc: 0.9969
Epoch 18/20
60000/60000 [==============================] - 9s 143us/step - loss: 0.0096 - acc: 0.9968
Epoch 19/20
60000/60000 [==============================] - 9s 142us/step - loss: 0.0090 - acc: 0.9972
Epoch 20/20
60000/60000 [==============================] - 9s 144us/step - loss: 0.0097 - acc: 0.9966
<keras.callbacks.History object at 0x7fc63d629850>
# Evaluate the model.
evalscore = model.evaluate(x_test, y_test, verbose=0)
print'Test accuracy: ', evalscore[1]
Test accuracy: 0.9851
print'Test loss: ', evalscore[0]
Test loss: 0.06053220131823819
# Let’s try recognizing some digits.
image_idx = 6700
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
pred = model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
4
image_idx = 8200
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
pred = model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
6
image_idx = 8735
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
pred = model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
8
恭喜你!我们的模型能够准确地识别数字。
Spark 分布式深度学习
训练像多目标探测器这样的复杂模型可能需要几个小时、几天甚至几周的时间。在大多数情况下,单个多 GPU 机器足以在合理的时间内训练大型模型。对于要求更高的工作负载,将计算分散到多台机器上可以大大减少训练时间,实现快速迭代实验并加速深度学习部署。
Spark 的并行计算和大数据能力使其成为分布式深度学习的理想平台。使用 Spark 进行分布式深度学习有额外的好处,特别是如果您已经有一个现有的 Spark 集群。分析存储在同一个集群上的大量数据是很方便的,比如 HDFS、Hive、Impala 或 HBase。您可能还希望与同一集群中运行的其他类型的工作负载共享结果,例如商业智能、机器学习、ETL 和功能工程。XXVIII
模型并行性与数据并行性
神经网络的分布式训练有两种主要方法:模型并行和数据并行。在数据并行中,分布式环境中的每个服务器都获得了模型的完整副本,但只是数据的一部分。训练在每个服务器中由完整数据集切片上的模型副本本地执行(图 7-10 (a))。在模型并行中,模型被分割在不同的服务器上(图 7-10 (b))。每个服务器被分配并负责处理单个神经网络的不同部分,例如层。 xxx 数据并行由于其简单性和易实现性,通常更受欢迎。但是,对于太大而无法在单台机器上安装的训练模型,模型并行是首选。Google 的大规模分布式深度学习框架 DistBelief 同时支持模型和数据并行。来自优步的分布式训练框架 Horovod 也支持模型和数据并行。
图 7-10
数据并行性与模型并行性 xxix
Spark 的分布式深度学习框架
感谢第三方贡献者,即使 Spark 的深度学习支持仍在开发中,也有几个外部分布式深度学习框架运行在 Spark 之上。我们将描述最受欢迎的。
深度学习管道
深度学习管道是来自 Databricks(由创建 Spark 的同一批人创建的公司)的第三方包,它提供了集成到 Spark ML 管道 API 中的深度学习功能。深度学习管道 API 使用 TensorFlow 和 Keras,TensorFlow 作为后端。它包括一个 ImageSchema,可用于将图像加载到 Spark 数据帧中。它支持迁移学习、分布式超参数调优以及将模型部署为 SQL 函数。在撰写本文时,深度学习管道仍在积极开发中。
BigDL
BigDL 是英特尔 Apache Spark 的分布式深度学习库。它与大多数深度学习框架的不同之处在于,它只支持 CPU。它使用多线程和英特尔的深度神经网络数学内核库(英特尔 MKL-DNN),这是一个开源库,用于加速英特尔架构上深度学习框架的性能。据说性能可以与传统的 GPU 相媲美。
咖啡馆
CaffeOnSpark 是雅虎开发的深度学习框架。它是 Caffe 的分布式扩展,旨在 Spark 集群之上运行。雅虎广泛使用 CaffeOnSpark 进行内容分类和图片搜索。
TensorFlowOnSpark
TensorFlowOnSpark 是雅虎开发的另一个深度学习框架。它支持使用 Spark 的分布式张量流推理和训练。它与 Spark ML 管道集成,支持模型和数据并行以及异步和同步训练。
张量框架
TensorFrames 是一个实验库,允许 TensorFlow 轻松处理 Spark 数据帧。它支持 Scala 和 Python,并提供了在 Spark 和 TensorFlow 之间传递数据的有效方式,反之亦然。
他们是
Elephas 是一个 Python 库,它扩展了 Keras,以支持使用 Spark 进行高度可扩展的分布式深度学习。Elephas 由 Max Pumperla 开发,利用数据并行实现分布式深度学习,以易用性和简单性著称。它还支持分布式超参数优化和集合模型的分布式训练。
分布式 Keras
分布式 Keras (Dist-Keras)是另一个运行在 Keras 和 Spark 之上的分布式深度学习框架。它是由欧洲粒子物理研究所的游里·赫尔曼斯开发的。它支持多种分布式优化算法,如 ADAG、动态 SGD、异步弹性平均 SGD (AEASGD)、异步弹性平均动量 SGD (AEAMSGD)和倾盆大雨 SGD。
如前所述,这一章将集中讨论大象和分布式 Keras。
Elephas:使用 Keras 和 Spark 的分布式深度学习
Elephas 是一个 Python 库,它扩展了 Keras,以支持使用 Spark 进行高度可扩展的分布式深度学习。已经超越单个多 GPU 机器的 Keras 用户正在寻找无需重写现有 Keras 程序即可扩展模型训练的方法。Elephas(和分布式 Keras)提供了一种简单的方法来实现这一点。
Elephas 使用数据并行性将 Keras 模型的训练分布在多个服务器上。使用 Elephas,Keras 模型、数据和参数在驱动程序中初始化后被序列化并复制到 worker 节点。Spark workers 对他们的数据部分进行训练,然后将他们的梯度发回,并使用优化器同步或异步地更新主模型。
Elephas 的主要抽象是 SparkModel。Elephas 传递一个编译好的 Keras 模型来初始化一个 SparkModel。然后,您可以调用 fit 方法,方法是将 RDD 作为定型数据和选项传递,如时期数、批量大小、验证拆分和详细程度,这与您使用 Keras 的方式类似。 xxxii 使用 spark-submit 或 pyspark 执行 Python 脚本。
from elephas.spark_model import SparkModel
from elephas.utils.rdd_utils import to_simple_rdd
rdd = to_simple_rdd(sc, x_train, y_train)
spark_model = SparkModel(model, frequency="epoch", mode="asynchronous")
spark_model.fit(rdd, epochs=10, batch_size=16, verbose=2, validation_split=0.2)
基于 MNIST 的手写数字识别
我们将使用 MNIST 数据集作为第一个大象的例子。 xxxiii 代码类似于我们之前使用 Keras 的示例,除了训练数据被转换为 Spark RDD,并且使用 Spark 训练模型。通过这种方式,你可以看到使用 Elephas 进行分布式深度学习是多么容易。我们将使用 pyspark 作为例子。见清单 7-3 。
# Download MNIST data.
import keras
import matplotlib.pyplot as plt
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Conv2D, Dropout, Flatten, MaxPooling2D
from elephas.spark_model import SparkModel
from elephas.utils.rdd_utils import to_simple_rdd
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train.shape
(60000, 28, 28)
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
x_test = x_test.reshape(x_test.shape[0], 28, 28, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
model = Sequential()
model.add(Conv2D(28, kernel_size=(3,3), input_shape= (28,28,1), name="convlayer1"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(128, activation="relu",name='fclayer1'))
model.add(Dropout(0.2))
model.add(Dense(10,activation='softmax', name="output"))
print(model.summary())
Model: "sequential_1"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
convlayer1 (Conv2D) (None, 26, 26, 28) 280
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 13, 13, 28) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 4732) 0
_________________________________________________________________
fclayer1 (Dense) (None, 128) 605824
_________________________________________________________________
dropout_1 (Dropout) (None, 128) 0
_________________________________________________________________
output (Dense) (None, 10) 1290
=================================================================
Total params: 607,394
Trainable params: 607,394
Non-trainable params: 0
_________________________________________________________________
None
# Compile the model.
model.compile(optimizer='adam', loss="categorical_crossentropy", metrics=['accuracy'])
# Build RDD from features and labels.
rdd = to_simple_rdd(sc, x_train, y_train)
# Initialize SparkModel from Keras model and Spark context.
spark_model = SparkModel(model, frequency="epoch", mode="asynchronous")
# Train the Spark model.
spark_model.fit(rdd, epochs=20, batch_size=128, verbose=1, validation_split=0.2)
# The output is edited for brevity.
15104/16051 [===========================>..] - ETA: 0s - loss: 0.0524 - acc: 0.9852
9088/16384 [===============>..............] - ETA: 2s - loss: 0.0687 - acc: 0.9770
9344/16384 [================>.............] - ETA: 2s - loss: 0.0675 - acc: 0.9774
15360/16051 [===========================>..] - ETA: 0s - loss: 0.0520 - acc: 0.9852
9600/16384 [================>.............] - ETA: 2s - loss: 0.0662 - acc: 0.9779
15616/16051 [============================>.] - ETA: 0s - loss: 0.0516 - acc: 0.9852
9856/16384 [=================>............] - ETA: 1s - loss: 0.0655 - acc: 0.9781
15872/16051 [============================>.] - ETA: 0s - loss: 0.0510 - acc: 0.9854
10112/16384 [=================>............] - ETA: 1s - loss: 0.0646 - acc: 0.9782
10368/16384 [=================>............] - ETA: 1s - loss: 0.0642 - acc: 0.9784
10624/16384 [==================>...........] - ETA: 1s - loss: 0.0645 - acc: 0.9784
10880/16384 [==================>...........] - ETA: 1s - loss: 0.0643 - acc: 0.9787
11136/16384 [===================>..........] - ETA: 1s - loss: 0.0633 - acc: 0.9790
11392/16384 [===================>..........] - ETA: 1s - loss: 0.0620 - acc: 0.9795
16051/16051 [==============================] - 6s 370us/step - loss: 0.0509 - acc: 0.9854 - val_loss: 0.0593 - val_acc: 0.9833
127.0.0.1 - - [01/Sep/2019 23:18:57] "POST /update HTTP/1.1" 200 -
11648/16384 [====================>.........] - ETA: 1s - loss: 0.0623 - acc: 0.9794
[Stage 0:=======================================> (2 + 1) / 3]794
12288/16384 [=====================>........] - ETA: 1s - loss: 0.0619 - acc: 0.9798
12672/16384 [======================>.......] - ETA: 1s - loss: 0.0615 - acc: 0.9799
13056/16384 [======================>.......] - ETA: 0s - loss: 0.0610 - acc: 0.9799
13440/16384 [=======================>......] - ETA: 0s - loss: 0.0598 - acc: 0.9803
13824/16384 [========================>.....] - ETA: 0s - loss: 0.0588 - acc: 0.9806
14208/16384 [=========================>....] - ETA: 0s - loss: 0.0581 - acc: 0.9808
14592/16384 [=========================>....] - ETA: 0s - loss: 0.0577 - acc: 0.9809
14976/16384 [==========================>...] - ETA: 0s - loss: 0.0565 - acc: 0.9812
15360/16384 [===========================>..] - ETA: 0s - loss: 0.0566 - acc: 0.9811
15744/16384 [===========================>..] - ETA: 0s - loss: 0.0564 - acc: 0.9813
16128/16384 [============================>.] - ETA: 0s - loss: 0.0557 - acc: 0.9815
16384/16384 [==============================] - 5s 277us/step - loss: 0.0556 - acc: 0.9815 - val_loss: 0.0906 - val_acc: 0.9758
127.0.0.1 - - [01/Sep/2019 23:18:58] "POST /update HTTP/1.1" 200 -
>>> Async training complete.
127.0.0.1 - - [01/Sep/2019 23:18:58] "GET /parameters HTTP/1.1" 200 -
# Evaluate the Spark model.
evalscore = spark_model.master_network.evaluate(x_test, y_test, verbose=2)
print'Test accuracy: ', evalscore[1]
Test accuracy: 0.9644
print'Test loss: ', evalscore[0]
Test loss: 0.12604748902269639
# Perform test digit recognition using our Spark model.
image_idx = 6700
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
Listing 7-3Distributed Deep Learning with Elephas, Keras, and Spark
pred = spark_model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
4
image_idx = 8200
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
pred = spark_model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
6
image_idx = 8735
plt.imshow(x_test[image_idx].reshape(28, 28),cmap='Greys')
plt.show()
pred = spark_model.predict(x_test[image_idx].reshape(1, 28, 28, 1))
print(pred.argmax())
8
在我们的例子中,我们使用 Python 从 numpy 数组生成 RDD。对于真正的大型数据集来说,这可能不可行。如果您的数据不适合内存,最好使用 Spark 创建 RDD,方法是直接从分布式存储引擎(如 HDFS 或 S3)读取数据,并使用 Spark MLlib 的转换器和估算器执行所有预处理。通过利用 Spark 的分布式处理能力来生成 RDD,您就拥有了一个完全分布式的深度学习平台。
Elephas 还支持使用 Spark MLlib 估算器和 Spark 数据帧进行模型训练。您可以将评估器作为更大的 Spark MLlib 管道的一部分来运行。参见清单 7-4 。
import keras
import matplotlib.pyplot as plt
from keras.datasets import mnist
from keras.models import Sequential
from keras.layers import Dense, Dropout
from keras import optimizers
from pyspark.sql.functions import rand
from pyspark.mllib.evaluation import MulticlassMetrics
from pyspark.ml import Pipeline
from elephas.ml_model import ElephasEstimator
from elephas.ml.adapter import to_data_frame
# Download MNIST data.
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train.shape
#(60000, 28, 28)
# We will be using only dense layers for our network. Let's flatten
# the grayscale 28x28 images to a 784 vector (28x28x1 = 784).
x_train = x_train.reshape(60000, 784)
x_test = x_test.reshape(10000, 784)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
# Since Spark DataFrames can't be created from three-dimensional
# data, we need to use dense layers when using Elephas and Keras
# with Spark DataFrames. Use RDDs if you need to use convolutional
# layers with Elephas.
model = Sequential()
model.add(Dense(128, input_dim=784, activation="relu", name="fclayer1"))
model.add(Dropout(0.2))
model.add(Dense(128, activation="relu",name='fclayer2'))
model.add(Dropout(0.2))
model.add(Dense(10,activation='softmax', name="output"))
# Build Spark DataFrames from features and labels.
df = to_data_frame(sc, x_train, y_train, categorical=True)
df.show(20,50)
+--------------------------------------------------+-----+
| features|label|
+--------------------------------------------------+-----+
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 5.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 0.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 4.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 2.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 3.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 4.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 3.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 5.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 3.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 6.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 7.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 2.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 8.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 6.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
+--------------------------------------------------+-----+
only showing top 20 rows
test_df = to_data_frame(sc, x_test, y_test, categorical=True)
test_df.show(20,50)
+--------------------------------------------------+-----+
| features|label|
+--------------------------------------------------+-----+
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 7.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 2.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 0.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 4.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 4.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 5.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 0.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 6.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 0.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 1.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 5.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 9.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 7.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 3.0|
|[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0....| 4.0|
+--------------------------------------------------+-----+
only showing top 20 rows
# Set and serialize optimizer.
sgd = optimizers.SGD(lr=0.01)
optimizer_conf = optimizers.serialize(sgd)
# Initialize Spark ML Estimator.
estimator = ElephasEstimator()
estimator.set_keras_model_config(model.to_yaml())
estimator.set_optimizer_config(optimizer_conf)
estimator.set_epochs(25)
estimator.set_batch_size(64)
estimator.set_categorical_labels(True)
estimator.set_validation_split(0.10)
estimator.set_nb_classes(10)
estimator.set_mode("synchronous")
estimator.set_loss("categorical_crossentropy")
estimator.set_metrics(['acc'])
# Fit a model.
pipeline = Pipeline(stages=[estimator])
pipeline_model = pipeline.fit(df)
# Evaluate the fitted pipeline model on test data.
prediction = pipeline_model.transform(test_df)
df2 = prediction.select("label", "prediction")
df2.show(20)
+-----+----------+
|label|prediction|
+-----+----------+
| 7.0| 7.0|
| 2.0| 2.0|
| 1.0| 1.0|
| 0.0| 0.0|
| 4.0| 4.0|
| 1.0| 1.0|
| 4.0| 4.0|
| 9.0| 9.0|
| 5.0| 6.0|
| 9.0| 9.0|
| 0.0| 0.0|
| 6.0| 6.0|
| 9.0| 9.0|
| 0.0| 0.0|
| 1.0| 1.0|
| 5.0| 5.0|
| 9.0| 9.0|
| 7.0| 7.0|
| 3.0| 2.0|
| 4.0| 4.0|
+-----+----------+
only showing top 20 rows
prediction_and_label= df2.rdd.map(lambda row: (row.label, row.prediction))
metrics = MulticlassMetrics(prediction_and_label)
print(metrics.precision())
0.757
Listing 7-4Training a Model with a Spark ML Estimator Using a DataFrame
分布式硬(Dist-Hard)
分布式 Keras (Dist-Keras)是另一个运行在 Keras 和 Spark 之上的分布式深度学习框架。它是由欧洲粒子物理研究所的游里·赫尔曼斯开发的。它支持多种分布式优化算法,如 ADAG、动态 SGD、异步弹性平均 SGD (AEASGD)、异步弹性平均动量 SGD (AEAMSGD)和倾盆大雨 SGD。Dist-Keras 包括自己的 Spark transformers,用于各种数据转换,例如 ReshapeTransformer、MinMaxTransformer、OneHotTransformer、DenseTransformer 和 LabelIndexTransformer 等等。与 Elephas 类似,Dist-Keras 利用数据并行性实现分布式深度学习。
基于 MNIST 的手写数字识别
为了保持一致,我们将使用 MNIST 数据集作为 Dist-Keras 示例。 xxxiv 在运行这个例子之前,确保你把 MNIST 数据集放在 HDFS 或者 S3。见清单 7-5 。
from distkeras.evaluators import *
from distkeras.predictors import *
from distkeras.trainers import *
from distkeras.transformers import *
from distkeras.utils import *
from keras.layers.convolutional import *
from keras.layers.core import *
from keras.models import Sequential
from keras.optimizers import *
from pyspark import SparkConf
from pyspark import SparkContext
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
from pyspark.ml.feature import OneHotEncoder
from pyspark.ml.feature import StandardScaler
from pyspark.ml.feature import StringIndexer
from pyspark.ml.feature import VectorAssembler
import pwd
import os
# First, set up the Spark variables. You can modify them to your needs.
application_name = "Distributed Keras MNIST Notebook"
using_spark_2 = False
local = False
path_train = "data/mnist_train.csv"
path_test = "data/mnist_test.csv"
if local:
# Tell master to use local resources.
master = "local[*]"
num_processes = 3
num_executors = 1
else:
# Tell master to use YARN.
master = "yarn-client"
num_executors = 20
num_processes = 1
# This variable is derived from the number of cores and executors and will be used to assign the number of model trainers.
num_workers = num_executors * num_processes
print("Number of desired executors: " + `num_executors`)
print("Number of desired processes / executor: " + `num_processes`)
print("Total number of workers: " + `num_workers`)
# Use the Databricks CSV reader; this has some nice functionality regarding invalid values.
os.environ['PYSPARK_SUBMIT_ARGS'] = '--packages com.databricks:spark-csv_2.10:1.4.0 pyspark-shell'
conf = SparkConf()
conf.set("spark.app.name", application_name)
conf.set("spark.master", master)
conf.set("spark.executor.cores", `num_processes`)
conf.set("spark.executor.instances", `num_executors`)
conf.set("spark.executor.memory", "4g")
conf.set("spark.locality.wait", "0")
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
conf.set("spark.local.dir", "/tmp/" + get_os_username() + "/dist-keras");
# Check if the user is running Spark 2.0 +
if using_spark_2:
sc = SparkSession.builder.config(conf=conf) \
.appName(application_name) \
.getOrCreate()
else:
# Create the Spark context.
sc = SparkContext(conf=conf)
# Add the missing imports.
from pyspark import SQLContext
sqlContext = SQLContext(sc)
# Check if we are using Spark 2.0.
if using_spark_2:
reader = sc
else:
reader = sqlContext
# Read the training dataset.
raw_dataset_train = reader.read.format('com.databricks.spark.csv') \
.options(header='true', inferSchema="true") \
.load(path_train)
# Read the testing dataset.
raw_dataset_test = reader.read.format('com.databricks.spark.csv') \
.options(header='true', inferSchema="true") \
.load(path_test)
# First, we would like to extract the desired features from the raw
# dataset. We do this by constructing a list with all desired columns.
# This is identical for the test set.
features = raw_dataset_train.columns
features.remove('label')
# Next, we use Spark's VectorAssembler to "assemble" (create) a vector of
# all desired features.
vector_assembler = VectorAssembler(inputCols=features, outputCol="features")
# This transformer will take all columns specified in features and create # an additional column "features" which will contain all the desired
# features aggregated into a single vector.
dataset_train = vector_assembler.transform(raw_dataset_train)
dataset_test = vector_assembler.transform(raw_dataset_test)
# Define the number of output classes.
nb_classes = 10
encoder = OneHotTransformer(nb_classes, input_col="label", output_col="label_encoded")
dataset_train = encoder.transform(dataset_train)
dataset_test = encoder.transform(dataset_test)
# Allocate a MinMaxTransformer from Distributed Keras to normalize
# the features.
# o_min -> original_minimum
# n_min -> new_minimum
transformer = MinMaxTransformer(n_min=0.0, n_max=1.0, \
o_min=0.0, o_max=250.0, \
input_col="features", \
output_col="features_normalized")
# Transform the dataset.
dataset_train = transformer.transform(dataset_train)
dataset_test = transformer.transform(dataset_test)
# Keras expects the vectors to be in a particular shape; we can reshape the
# vectors using Spark.
reshape_transformer = ReshapeTransformer("features_normalized", "matrix", (28, 28, 1))
dataset_train = reshape_transformer.transform(dataset_train)
dataset_test = reshape_transformer.transform(dataset_test)
# Now, create a Keras model.
# Taken from Keras MNIST example.
# Declare model parameters.
img_rows, img_cols = 28, 28
# Number of convolutional filters to use
nb_filters = 32
# Size of pooling area for max pooling
pool_size = (2, 2)
# Convolution kernel size
kernel_size = (3, 3)
input_shape = (img_rows, img_cols, 1)
# Construct the model.
convnet = Sequential()
convnet.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1],
border_mode='valid',
input_shape=input_shape))
convnet.add(Activation('relu'))
convnet.add(Convolution2D(nb_filters, kernel_size[0], kernel_size[1]))
convnet.add(Activation('relu'))
convnet.add(MaxPooling2D(pool_size=pool_size))
convnet.add(Flatten())
convnet.add(Dense(225))
convnet.add(Activation('relu'))
convnet.add(Dense(nb_classes))
convnet.add(Activation('softmax'))
# Define the optimizer and the loss.
optimizer_convnet = 'adam'
loss_convnet = 'categorical_crossentropy'
# Print the summary.
convnet.summary()
# We can also evaluate the dataset in a distributed manner.
# However, for this we need to specify a procedure on how to do this.
def evaluate_accuracy(model, test_set, features="matrix"):
evaluator = AccuracyEvaluator(prediction_col="prediction_index", label_col="label")
predictor = ModelPredictor(keras_model=model, features_col=features)
transformer = LabelIndexTransformer(output_dim=nb_classes)
test_set = test_set.select(features, "label")
test_set = predictor.predict(test_set)
test_set = transformer.transform(test_set)
score = evaluator.evaluate(test_set)
return score
# Select the desired columns; this will reduce network usage.
dataset_train = dataset_train.select("features_normalized", "matrix","label", "label_encoded")
dataset_test = dataset_test.select("features_normalized", "matrix","label", "label_encoded")
# Keras expects DenseVectors.
dense_transformer = DenseTransformer(input_col="features_normalized", output_col="features_normalized_dense")
dataset_train = dense_transformer.transform(dataset_train)
dataset_test = dense_transformer.transform(dataset_test)
dataset_train.repartition(num_workers)
dataset_test.repartition(num_workers)
# Assessing the training and test set.
training_set = dataset_train.repartition(num_workers)
test_set = dataset_test.repartition(num_workers)
# Cache them.
training_set.cache()
test_set.cache()
# Precache the training set on the nodes using a simple count.
print(training_set.count())
# Use the ADAG optimizer. You can also use a SingleWorker for testing
# purposes -> traditional nondistributed gradient descent.
trainer = ADAG(keras_model=convnet, worker_optimizer=optimizer_convnet, loss=loss_convnet, num_workers=num_workers, batch_size=16, communication_window=5, num_epoch=5, features_col="matrix", label_col="label_encoded")
trained_model = trainer.train(training_set)
print("Training time: " + str(trainer.get_training_time()))
print("Accuracy: " + str(evaluate_accuracy(trained_model, test_set)))
print("Number of parameter server updates: " + str(trainer.parameter_server.num_updates))
Listing 7-5Distributed Deep Learning with Dist-Keras, Keras, and Spark
将深度学习工作负载分布在多台机器上并不总是一个好方法。在分布式环境中运行作业是有开销的。更不用说建立和维护分布式 Spark 环境所花费的时间和精力了。高性能多 GPU 机器和云实例已经允许您在单台机器上以良好的训练速度训练相当大的模型。您可能根本不需要分布式环境。事实上,在 Keras 中利用 ImageDataGenerator 类加载数据和 fit_generator 函数训练模型在大多数情况下可能就足够了。让我们在下一个例子中探索这个选项。
猫狗图像分类
在这个例子中,我们将使用卷积神经网络来构建一个狗和猫的图像分类器。我们将使用一个流行的数据集,它是由 Francois Chollet 推广的,由微软研究院提供,可从 Kaggle 获得。 xxxv 和 MNIST 数据集一样,这个数据集在研究中被广泛使用。它包含 12,500 个猫图像和 12,500 个狗图像,尽管我们将只为每个类使用 2000 个图像(总共 4000 个图像)来加速训练。我们将使用 500 张猫图像和 500 张狗图像(总共 1000 张图像)进行测试。见图 7-11 。
图 7-11
来自数据集的样本狗和猫图像
我们将使用 fit_generator 来训练我们的 Keras 模型。我们还将利用 ImageDataGenerator 类来批量加载数据,从而允许我们处理大量数据。如果您的大型数据集无法容纳在内存中,并且您没有访问分布式环境的权限,这将非常有用。使用 ImageDataGenerator 的另一个好处是,它能够执行随机数据转换来扩充您的数据,帮助模型更好地进行概括,并帮助防止过度拟合。这个例子将展示如何在不使用 Spark 的情况下使用 Keras 的大型数据集。参见清单 7-6 。
import matplotlib.pyplot as plt
import numpy as np
import cv2
from keras.preprocessing.image import ImageDataGenerator
from keras.preprocessing import image
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
# The image dimension is 150x150\. RGB = 3.
if K.image_data_format() == 'channels_first':
input_shape = (3, 150, 150)
else:
input_shape = (150, 150, 3)
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=input_shape, activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(32, (3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(64, (3, 3), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(64, activation="relu"))
model.add(Dropout(0.5))
model.add(Dense(1, activation="sigmoid"))
# Compile the model.
model.compile(loss='binary_crossentropy',
optimizer='rmsprop',
metrics=['accuracy'])
print(model.summary())
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_1 (Conv2D) (None, 148, 148, 32) 896
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 74, 74, 32) 0
_________________________________________________________________
conv2d_2 (Conv2D) (None, 72, 72, 32) 9248
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 36, 36, 32) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 34, 34, 64) 18496
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 17, 17, 64) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 18496) 0
_________________________________________________________________
dense_1 (Dense) (None, 64) 1183808
_________________________________________________________________
dropout_1 (Dropout) (None, 64) 0
_________________________________________________________________
dense_2 (Dense) (None, 1) 65
=================================================================
Total params: 1,212,513
Trainable params: 1,212,513
Non-trainable params: 0
_________________________________________________________________
None
# We will use the following augmentation configuration for training.
train_datagen = ImageDataGenerator(
rescale=1\. / 255,
width_shift_range=0.2,
height_shift_range=0.2,
horizontal_flip=True)
# The only augmentation for test data is rescaling.
test_datagen = ImageDataGenerator(rescale=1\. / 255)
train_generator = train_datagen.flow_from_directory(
'/data/train',
target_size=(150, 150),
batch_size=16,
class_mode='binary')
Found 4000 images belonging to 2 classes.
validation_generator = test_datagen.flow_from_directory(
'/data/test',
target_size=(150, 150),
batch_size=16,
class_mode='binary')
Found 1000 images belonging to 2 classes.
# steps_per_epoch should be set to the total number of training sample,
# while validation_steps is set to the number of test samples. We set
# epoch to 15, steps_per_epoch and validation_steps to 100 to expedite
# model training.
model.fit_generator(
train_generator,
steps_per_epoch=100,
epochs=1=25,
validation_data=validation_generator,
validation_steps=100)
Epoch 1/25
100/100 [==============================] - 45s 451ms/step - loss: 0.6439 - acc: 0.6244 - val_loss: 0.5266 - val_acc: 0.7418
Epoch 2/25
100/100 [==============================] - 44s 437ms/step - loss: 0.6259 - acc: 0.6681 - val_loss: 0.5577 - val_acc: 0.7304
Epoch 3/25
100/100 [==============================] - 43s 432ms/step - loss: 0.6326 - acc: 0.6338 - val_loss: 0.5922 - val_acc: 0.7029
Epoch 4/25
100/100 [==============================] - 43s 434ms/step - loss: 0.6538 - acc: 0.6300 - val_loss: 0.5642 - val_acc: 0.7052
Epoch 5/25
100/100 [==============================] - 44s 436ms/step - loss: 0.6263 - acc: 0.6600 - val_loss: 0.6725 - val_acc: 0.6746
Epoch 6/25
100/100 [==============================] - 43s 427ms/step - loss: 0.6229 - acc: 0.6606 - val_loss: 0.5586 - val_acc: 0.7538
Epoch 7/25
100/100 [==============================] - 43s 426ms/step - loss: 0.6470 - acc: 0.6562 - val_loss: 0.5878 - val_acc: 0.7077
Epoch 8/25
100/100 [==============================] - 43s 429ms/step - loss: 0.6524 - acc: 0.6437 - val_loss: 0.6414 - val_acc: 0.6539
Epoch 9/25
100/100 [==============================] - 43s 427ms/step - loss: 0.6131 - acc: 0.6831 - val_loss: 0.5636 - val_acc: 0.7304
Epoch 10/25
100/100 [==============================] - 43s 429ms/step - loss: 0.6293 - acc: 0.6538 - val_loss: 0.5857 - val_acc: 0.7186
Epoch 11/25
100/100 [==============================] - 44s 437ms/step - loss: 0.6207 - acc: 0.6713 - val_loss: 0.5467 - val_acc: 0.7279
Epoch 12/25
100/100 [==============================] - 43s 430ms/step - loss: 0.6131 - acc: 0.6587 - val_loss: 0.5279 - val_acc: 0.7348
Epoch 13/25
100/100 [==============================] - 43s 428ms/step - loss: 0.6090 - acc: 0.6781 - val_loss: 0.6221 - val_acc: 0.7054
Epoch 14/25
100/100 [==============================] - 42s 421ms/step - loss: 0.6273 - acc: 0.6756 - val_loss: 0.5446 - val_acc: 0.7506
Epoch 15/25
100/100 [==============================] - 44s 442ms/step - loss: 0.6139 - acc: 0.6775 - val_loss: 0.6073 - val_acc: 0.6954
Epoch 16/25
100/100 [==============================] - 44s 441ms/step - loss: 0.6080 - acc: 0.6806 - val_loss: 0.5365 - val_acc: 0.7437
Epoch 17/25
100/100 [==============================] - 45s 448ms/step - loss: 0.6225 - acc: 0.6719 - val_loss: 0.5831 - val_acc: 0.6935
Epoch 18/25
100/100 [==============================] - 43s 428ms/step - loss: 0.6124 - acc: 0.6769 - val_loss: 0.5457 - val_acc: 0.7361
Epoch 19/25
100/100 [==============================] - 43s 430ms/step - loss: 0.6061 - acc: 0.6844 - val_loss: 0.5587 - val_acc: 0.7399
Epoch 20/25
100/100 [==============================] - 43s 429ms/step - loss: 0.6209 - acc: 0.6613 - val_loss: 0.5699 - val_acc: 0.7280
Epoch 21/25
100/100 [==============================] - 43s 428ms/step - loss: 0.6252 - acc: 0.6650 - val_loss: 0.5550 - val_acc: 0.7247
Epoch 22/25
100/100 [==============================] - 43s 429ms/step - loss: 0.6306 - acc: 0.6594 - val_loss: 0.5466 - val_acc: 0.7236
Epoch 23/25
100/100 [==============================] - 43s 427ms/step - loss: 0.6086 - acc: 0.6819 - val_loss: 0.5790 - val_acc: 0.6824
Epoch 24/25
100/100 [==============================] - 43s 425ms/step - loss: 0.6059 - acc: 0.7000 - val_loss: 0.5433 - val_acc: 0.7197
Epoch 25/25
100/100 [==============================] - 43s 426ms/step - loss: 0.6261 - acc: 0.6794 - val_loss: 0.5987 - val_acc: 0.7167
<keras.callbacks.History object at 0x7ff72c7c3890>
# We get a 71% validation accuracy. To increase model accuracy, you can try several things such as adding more training data and increasing the number of epochs.
model.save_weights('dogs_vs_cats.h5')
# Let's now use our model to classify a few images. dogs=1, cats=0
# Let's start with dogs.
img = cv2.imread(“/data/test/dogs/dog.148.jpg")
img = np.array(img).astype('float32')/255
img = cv2.resize(img, (150,150))
plt.imshow(img)
plt.show()
Listing 7-6Using ImageDataGenerator and Fit_Generator
img = img.reshape(1, 150, 150, 3)
print(model.predict(img))
[[0.813732]]
print(round(model.predict(img)))
1.0
# Another one
img = cv2.imread("/data/test/dogs/dog.235.jpg")
img = np.array(img).astype('float32')/255
img = cv2.resize(img, (150,150))
plt.imshow(img)
plt.show()
img = img.reshape(1, 150, 150, 3)
print(model.predict(img))
[[0.92639965]]
print(round(model.predict(img)))
1.0
# Let's try some cat photos.
img = cv2.imread("/data/test/cats/cat.355.jpg")
img = np.array(img).astype('float32')/255
img = cv2.resize(img, (150,150))
plt.imshow(img)
plt.show()
img = img.reshape(1, 150, 150, 3)
print(model.predict(img))
[[0.49332634]]
print(round(model.predict(img)))
0.0
# Another one
img = cv2.imread("/data/test/cats/cat.371.jpg")
img = np.array(img).astype('float32')/255
img = cv2.resize(img, (150,150))
plt.imshow(img)
plt.show()
img = img.reshape(1, 150, 150, 3)
print(model.predict(img))
[[0.16990553]]
print(round(model.predict(img)))
0.0
摘要
这一章为你提供了深度学习和 Spark 分布式深度学习的介绍。我选择 Keras 进行深度学习是因为它简单、易用、受欢迎。我选择 Elephas 和 Dist-Keras 进行分布式深度学习,以保持 Keras 的易用性,同时通过 Spark 实现高度可扩展的深度学习工作负载。除了 Elephas 和 Dist-Keras,我建议您探索其他非 Spark 分布式深度学习框架,如 Horovod。对于深度学习的更深入的处理,我推荐弗朗索瓦·乔莱(曼宁)的用 Python 深度学习和伊恩·古德菲勒、约舒阿·本吉奥和亚伦·库维尔(麻省理工出版社)的深度学习。
参考
-
弗朗西斯·克里克,《惊人的假设:对灵魂的科学探索》,斯克里布纳,1995 年
-
迈克尔·科普兰;“人工智能、机器学习、深度学习有什么区别?”,nvidia.com,2016,
https://blogs.nvidia.com/blog/2016/07/29/whats-difference-artificial-intelligence-machine-learning-deep-learning-ai/
-
英伟达;《深度学习》,developer.nvidia.com,2019,
https://developer.nvidia.com/deep-learning
-
心灵深处;“Alpha Go,”deepmind.com,2019 年,
-
特斯拉;《驾驶的未来》,tesla.com,2019,
www.tesla.com/autopilot
-
罗布·孔戈;“特斯拉提高了自动驾驶汽车制造商的门槛,”nvidia.com,2019 年,
https://blogs.nvidia.com/blog/2019/04/23/tesla-self-driving/
-
SAS《神经网络如何工作》,sas.com,2019,
www.sas.com/en_us/insights/analytics/neural-networks.html
-
H2O;《深度学习(神经网络)》,h2o.ai,2019,
http://docs.h2o.ai/h2o/latest-stable/h2o-docs/data-science/deep-learning.html
-
迈克尔·马尔萨利;《麦卡洛克-皮茨神经元》,ilstu.edu,2019,
www.mind.ilstu.edu/curriculum/modOverview.php?modGUI=212
-
弗朗西斯·克里克;《大脑损伤》第 181 页,西蒙&舒斯特,1995 年,《惊人的假设:对灵魂的科学探索》
-
大卫·b·福格尔;定义人工智能第 20 页,Wiley,2005,《进化计算:走向机器智能的新哲学》,第三版
-
巴纳巴斯·波佐斯;《机器学习导论(讲义)感知器》,cmu.edu,2017,
www.cs.cmu.edu/~10701/slides/Perceptron_Reading_Material.pdf
-
SAS《神经网络的历史》,sas.com,2019,
www.sas.com/en_us/insights/analytics/neural-networks.html
-
Skymind《神经网络反向传播初学者指南》,skymind.ai,2019,
https://skymind.ai/wiki/backpropagation
-
Cognilytica“人们是否过度迷恋深度学习,它真的能实现吗?”,cognilytica.com,2018,
www.cognilytica.com/2018/07/10/are-people-overly-infatuated-with-deep-learning-and-can-it-really-deliver/
-
Yann LeCun“不变量识别:卷积神经网络”,lecun.com,2004,
http://yann.lecun.com/ex/research/index.html
-
凯尔·威格斯;“Geoffrey Hinton、Yann LeCun、Yoshua Bengio 获图灵奖”,venturebeat.com,2019,
https://venturebeat.com/2019/03/27/geoffrey-hinton-yann-lecun-and-yoshua-bengio-honored-with-the-turing-award/
-
亚历克斯·克里热夫斯基、伊利亚·苏茨基弗、杰弗里·欣顿;“用深度卷积神经网络进行 ImageNet 分类”,toronto.edu,2012,
www.cs.toronto.edu/~fritz/absps/imagenet.pdf
-
皮托奇;“Alexnet,”pytorch.org,2019 年,
-
弗朗索瓦·乔莱;“计算机视觉的深度学习”,2018,用 Python 进行深度学习
-
安德烈·卡帕西;《卷积神经网络(CNN/conv nets)》,github.io,2019,
http://cs231n.github.io/convolutional-networks/
-
杰尼尔·达拉勒和索希尔·帕特尔;“图像基础”,Packt 出版社,2013 年,即时 OpenCV Starter
-
MATLAB《用 MATLAB 介绍深度学习》。mathworks.com,2019,
www.mathworks.com/content/dam/mathworks/tag-team/Objects/d/80879v00_Deep_Learning_ebook.pdf
-
文森特·杜穆林和弗朗切斯科·维辛;《深度卷积运算指南》
学,《axiv.org》2018, [`https://arxiv.org/pdf/1603.07285.pdf`](https://arxiv.org/pdf/1603.07285.pdf)
-
阿尼什·辛格·瓦利亚;"激活函数及其类型——哪个更好?",towardsdatascience.com,2017,
https://towardsdatascience.com/activation-functions-and-its-types-which-is-better-a9a5310cc8f
-
Skymind《AI 框架比较》,skymind.ai,2019,
https://skymind.ai/wiki/comparison-frameworks-dl4j-tensorflow-pytorch
-
尼哈尔·加贾雷;“Keras + TensorFlow 中的简单神经网络对虹膜数据集进行分类”,github.com,2017,
https://gist.github.com/NiharG15/cd8272c9639941cf8f481a7c4478d525
-
大 dl;“什么是重头戏,”github.com,2019 年,
-
Skymind《分布式深度学习,第一部分:神经网络分布式训练导论》,skymind.ai,2017,
https://blog.skymind.ai/distributed-deep-learning-part-1-an-introduction-to-distributed-training-of-neural-networks/
-
Skymind《分布式深度学习,第一部分:神经网络分布式训练导论》,skymind.ai,2017,
https://blog.skymind.ai/distributed-deep-learning-part-1-an-introduction-to-distributed-training-of-neural-networks/
-
数据块;“Apache Spark 的深度学习管道”,github.com,2019,
https://github.com/databricks/spark-deep-learning
-
Max Pumperla“Elephas:分布式深度学习与 Keras & Spark”,github.com,2019,
https://github.com/maxpumperla/elephas
-
max pumbaa 她;" mnist_lp_spark.py," github.com,2019 年,
-
游里·r·赫尔曼斯,欧洲粒子物理研究所信息技术数据库;《分布式 Keras:使用 Apache Spark 和 Keras 的分布式深度学习》,Github.com,2016,
https://github.com/JoeriHermans/dist-keras/
-
微软研究院;《狗与猫》,Kaggle.com,2013,
www.kaggle.com/c/dogs-vs-cats/data
-
弗朗索瓦·乔莱;“用极少的数据构建强大的图像分类模型”,keras.io,2016,
https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
2020-10-02 《线性代数》(同济版)——教科书中的耻辱柱