原文:
annas-archive.org/md5/8cd899a961e3fea998473427a1ba1c82
译者:飞龙
第六章 机器学习
我们每天都在使用机器学习,无论我们是否注意到。像 Google 这样的电子邮件服务提供商会自动将一些来信推送到垃圾邮件
文件夹,像 Amazon 这样的在线购物网站或 Facebook 这样的社交网络网站会推荐一些意外有用的商品或信息。那么,是什么让这些软件产品能够重新连接久未联系的朋友呢?这些只是机器学习应用的几个例子。
从形式上来说,机器学习是人工智能(AI)的一部分,它涉及一类能够从数据中学习并进行预测的算法。其技术和基本概念来自统计学领域。机器学习位于计算机科学和统计学的交叉点,被认为是数据科学最重要的组成部分之一。虽然它已经存在一段时间,但随着数据量和可扩展性要求的增加,其复杂性也不断提升。机器学习算法往往是资源密集型和迭代的,这使得它们不太适合 MapReduce 范式。MapReduce 非常适合单次运行的算法,但对于多次运行的算法并不太适用。正是为了应对这一挑战,Spark 研究项目应运而生。Apache Spark 在其 MLlib 库中配备了高效的算法,旨在即使在迭代计算要求下也能表现良好。
上一章概述了数据分析的生命周期及其各种组成部分,如数据清洗、数据转换、采样技术和用于可视化数据的图形技术,以及描述性统计和推断统计的概念。我们还看了一些可以在 Spark 平台上执行的统计测试。在上一章所建立的基础上,本章将介绍大多数机器学习算法及如何在 Spark 上使用它们构建模型。
本章的前提是对机器学习算法和计算机科学基础知识有一定的了解。尽管如此,我们已经通过一些理论基础和适当的实际案例讲解了这些算法,使其更加易于理解和实现。本章涵盖的主题包括:
-
机器学习简介
-
演化
-
有监督学习
-
无监督学习
-
-
MLlib 和管道 API
-
MLlib
-
机器学习管道
-
-
机器学习简介
-
参数化方法
-
非参数化方法
-
-
回归方法
-
线性回归
-
回归的正则化
-
-
分类方法
-
逻辑回归
-
线性支持向量机(SVM)
-
-
决策树
-
不纯度度量
-
停止规则
-
分割候选
-
决策树的优点
-
示例
-
-
集成方法
-
随机森林
-
梯度提升树
-
-
多层感知机分类器
-
聚类技术
- K 均值聚类
-
总结
介绍
机器学习完全是通过示例数据来学习;这些示例为特定输入产生特定输出。机器学习在商业中有多种应用案例。让我们通过几个例子来了解它究竟是什么:
-
一个推荐引擎,向用户推荐他们可能感兴趣的购买项目
-
客户细分(将具有相似特征的客户分组)用于市场营销活动
-
癌症的疾病分类——恶性/良性
-
预测建模,例如,销售预测,天气预测
-
绘制商业推断,例如,理解改变产品价格对销售的影响
演变
统计学习的概念在第一个计算机系统被引入之前就已存在。在十九世纪,最小二乘法(现在称为线性回归)已经被开发出来。对于分类问题,费舍尔提出了线性判别分析(LDA)。大约在 1940 年代,一种 LDA 的替代方法——逻辑回归被提出,所有这些方法不仅随着时间的推移得到了改进,还激发了其他新算法的发展。
在那个时期,计算是一个大问题,因为它是通过笔和纸来完成的。因此,拟合非线性方程并不十分可行,因为它需要大量的计算。1980 年代以后,随着技术的进步和计算机系统的引入,分类/回归树被提出。随着技术和计算系统的进一步发展,统计学习在某种程度上与现在所称的机器学习融合在一起。
监督学习
正如上一节所讨论的,机器学习完全是通过示例数据来学习。根据算法如何理解数据并在其上进行训练,机器学习大致可以分为两类:监督学习和无监督学习。
监督统计学习涉及构建基于一个或多个输入的模型,以产生特定的输出。这意味着我们得到的输出可以根据我们提供的输入来监督我们的分析。换句话说,对于每个预测变量的观察(例如,年龄、教育和支出变量),都有一个与之相关的响应变量的测量(例如,薪水)。请参考下表了解我们尝试基于年龄、教育和支出变量预测薪水的示例数据集:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_001.jpg
监督算法可以用于预测、估算、分类以及其他类似的需求,我们将在接下来的部分中进行介绍。
无监督学习
无监督统计学习是通过一个或多个输入构建模型,但没有预期产生特定的输出。这意味着没有明确需要预测的响应/输出变量;但输出通常是具有一些相似特征的数据点分组。与监督学习不同,你并不知道将数据点分类到哪些组/标签,而是将这一决策交给算法自己去决定。
在这里,并没有一个 训练
数据集来通过构建模型将结果变量与 预测
变量关联起来,并随后使用 测试
数据集验证模型。无监督算法的输出无法根据你提供的输入来监督你的分析。这类算法可以从数据中学习关系和结构。聚类 和 关联规则学习 是无监督学习技术的例子。
以下图片展示了如何使用聚类将具有相似特征的数据项分组:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_002.jpg
MLlib 和 Pipeline API
让我们首先了解一些 Spark 的基础知识,以便能够在其上执行机器学习操作。本节将讨论 MLlib 和 Pipeline API。
MLlib
MLlib 是建立在 Apache Spark 之上的机器学习库,包含了大部分可以大规模实现的算法。MLlib 与 GraphX、SQL 和 Streaming 等其他组件的无缝集成为开发者提供了相对容易组装复杂、可扩展和高效工作流的机会。MLlib 库包含常用的学习算法和工具,包括分类、回归、聚类、协同过滤和降维等。
MLlib 与 spark.ml
包协同工作,后者提供了一个高级的 Pipeline API。这两个包之间的根本区别在于,MLlib(spark.mllib
)在 RDD 之上工作,而 ML(spark.ml
)包在 DataFrame 之上工作,并支持 ML Pipeline。目前,Spark 支持这两个包,但建议使用 spark.ml
包。
该库中的基本数据类型是向量和矩阵。向量是局部的,可以是密集的或稀疏的。密集向量以值数组的形式存储。稀疏向量则存储为两个数组;第一个数组存储非零值的索引,第二个数组存储实际的值。所有元素值都以双精度浮点数形式存储,索引以从零开始的整数形式存储。理解这些基本结构有助于高效使用库,并帮助从零开始编写任何新的算法。让我们看一些示例代码,帮助更好地理解这两种向量表示方式:
Scala
//Create vectors
scala> import org.apache.spark.ml.linalg.{Vector, Vectors}
import org.apache.spark.ml.linalg.{Vector, Vectors}
//Create dense vector
scala> val dense_v: Vector = Vectors.dense(10.0,0.0,20.0,30.0,0.0)
dense_v: org.apache.spark.ml.linalg.Vector = [10.0,0.0,20.0,30.0,0.0]
scala>
//Create sparse vector: pass size, position index array and value array
scala> val sparse_v1: Vector = Vectors.sparse(5,Array(0,2,3),
Array(10.0,20.0,30.0))
sparse_v1: org.apache.spark.ml.linalg.Vector = (5,[0,2,3],[10.0,20.0,30.0])
scala>
//Another way to create sparse vector with position, value tuples
scala> val sparse_v2: Vector = Vectors.sparse(5,
Seq((0,10.0),(2,20.0),(3,30.0)))
sparse_v2: org.apache.spark.ml.linalg.Vector = (5,[0,2,3],[10.0,20.0,30.0])
scala>
Compare vectors
--------------- cala> sparse_v1 == sparse_v2
res0: Boolean = true
scala> sparse_v1 == dense_v
res1: Boolean = true //All three objects are equal but...
scala> dense_v.toString()
res2: String = [10.0,0.0,20.0,30.0,0.0]
scala> sparse_v2.toString()
res3: String = (5,[0,2,3],[10.0,20.0,30.0]) //..internal representation
differs
scala> sparse_v2.toArray
res4: Array[Double] = Array(10.0, 0.0, 20.0, 30.0, 0.0)
Interchangeable ---------------
scala> dense_v.toSparse
res5: org.apache.spark.mllib.linalg.SparseVector = (5,[0,2,3]
[10.0,20.0,30.0])
scala> sparse_v1.toDense
res6: org.apache.spark.mllib.linalg.DenseVector = [10.0,0.0,20.0,30.0,0.0]
scala>
A common operation ------------------
scala> Vectors.sqdist(sparse_v1,
Vectors.dense(1.0,2.0,3.0,4.0,5.0))
res7: Double = 1075.0
Python:
//Create vectors
>>> from pyspark.ml.linalg import Vector, Vectors
//Create vectors
>>> dense_v = Vectors.dense(10.0,0.0,20.0,30.0,0.0)
//Pass size, position index array and value array
>>> sparse_v1 = Vectors.sparse(5,[0,2,3],
[10.0,20.0,30.0])
>>>
//Another way to create sparse vector with position, value tuples
>>> sparse_v2 = Vectors.sparse(5,
[[0,10.0],[2,20.0],[3,30.0]])
>>>
Compare vectors
--------------- >>> sparse_v1 == sparse_v2
True
>>> sparse_v1 == dense_v
True //All three objects are equal but...
>>> dense_v
DenseVector([10.0, 0.0, 20.0, 30.0, 0.0])
>>> sparse_v1
SparseVector(5, {0: 10.0, 2: 20.0, 3: 30.0}) //..internal representation
differs
>>> sparse_v2
SparseVector(5, {0: 10.0, 2: 20.0, 3: 30.0})
Interchangeable
---------------- //Note: as of Spark 2.0.0, toDense and toSparse are not available in pyspark
A common operation
------------------- >>> Vectors.squared_distance(sparse_v1,
Vectors.dense(1.0,2.0,3.0,4.0,5.0))
1075.0
矩阵可以是局部的或分布式的,可以是稠密的或稀疏的。局部矩阵存储在单台机器上,作为一维数组。稠密的局部矩阵按照列主序存储(列成员是连续的),而稀疏矩阵的值则以**压缩稀疏列(CSC)**格式以列主序存储。在这种格式中,矩阵以三个数组的形式存储。第一个数组包含非零值的行索引,第二个数组包含每列第一个非零值的起始位置索引,第三个数组包含所有非零值。索引的类型是整数,从零开始。第一个数组包含从零到行数减一的值。第三个数组的元素类型为双精度。第二个数组需要一些解释。该数组中的每一项对应每一列第一个非零元素的索引。例如,假设在一个 3×3 的矩阵中,每列只有一个非零元素。那么第二个数组将包含 0、1、2 作为其元素。第一个数组包含行位置,第三个数组包含三个值。如果某一列中没有非零元素,你会注意到第二个数组中的相同索引会重复。让我们看一些示例代码:
Scala:
scala> import org.apache.spark.ml.linalg.{Matrix,Matrices}
import org.apache.spark.ml.linalg.{Matrix, Matrices}
Create dense matrix
------------------- //Values in column major order
Matrices.dense(3,2,Array(9.0,0,0,0,8.0,6))
res38: org.apache.spark.mllib.linalg.Matrix =
9.0 0.0
0.0 8.0
0.0 6.0
Create sparse matrix
-------------------- //1.0 0.0 4.0
0.0 3.0 5.0
2.0 0.0 6.0//
val sm: Matrix = Matrices.sparse(3,3,
Array(0,2,3,6), Array(0,2,1,0,1,2),
Array(1.0,2.0,3.0,4.0,5.0,6.0))
sm: org.apache.spark.mllib.linalg.Matrix =
3 x 3 CSCMatrix
(0,0) 1.0
(2,0) 2.0
(1,1) 3.0
(0,2) 4.0
(1,2) 5.0
(2,2) 6.0
Sparse matrix, a column of all zeros
------------------------------------ //third column all zeros
Matrices.sparse(3,4,Array(0,2,3,3,6),
Array(0,2,1,0,1,2),values).toArray
res85: Array[Double] = Array(1.0, 0.0, 2.0, 0.0, 3.0, 0.0, 0.0, 0.0, 0.0,
4.0, 5.0, 6.0)
Python:
//Create dense matrix
>>> from pyspark.ml.linalg import Matrix, Matrices
//Values in column major order
>>> Matrices.dense(3,2,[9.0,0,0,0,8.0,6])
DenseMatrix(3, 2, [9.0, 0.0, 0.0, 0.0, 8.0, 6.0], False)
>>>
//Create sparse matrix
//1.0 0.0 4.0
0.0 3.0 5.0
2.0 0.0 6.0//
>>> sm = Matrices.sparse(3,3,
[0,2,3,6], [0,2,1,0,1,2],
[1.0,2.0,3.0,4.0,5.0,6.0])
>>>
//Sparse matrix, a column of all zeros
//third column all zeros
>>> Matrices.sparse(3,4,[0,2,3,3,6],
[0,2,1,0,1,2],
values=[1.0,2.0,3.0,4.0,5.0,6.0]).toArray()
array([[ 1., 0., 0., 4.],
[ 0., 3., 0., 5.],
[ 2., 0., 0., 6.]])
>>>
分布式矩阵是最复杂的矩阵,选择合适的分布式矩阵类型非常重要。分布式矩阵由一个或多个 RDDs 支持。行和列的索引是long
类型,以支持非常大的矩阵。分布式矩阵的基本类型是RowMatrix
,它仅由其行的 RDD 支持。
每一行依次是一个局部向量。当列数非常低时,这种方式很适用。记住,我们需要传递 RDDs 来创建分布式矩阵,而不像局部矩阵那样。让我们来看一个例子:
Scala:
scala> import org.apache.spark.mllib.linalg.{Vector,Vectors}
import org.apache.spark.mllib.linalg.{Vector, Vectors}
scala> import org.apache.spark.mllib.linalg.distributed.RowMatrix
import org.apache.spark.mllib.linalg.distributed.RowMatrix
scala>val dense_vlist: Array[Vector] = Array(
Vectors.dense(11.0,12,13,14),
Vectors.dense(21.0,22,23,24),
Vectors.dense(31.0,32,33,34))
dense_vlist: Array[org.apache.spark.mllib.linalg.Vector] =
Array([11.0,12.0,13.0,14.0], [21.0,22.0,23.0,24.0], [31.0,32.0,33.0,34.0])
scala>
//Distribute the vector list
scala> val rows = sc.parallelize(dense_vlist)
rows: org.apache.spark.rdd.RDD[org.apache.spark.mllib.linalg.Vector] =
ParallelCollectionRDD[0] at parallelize at <console>:29
scala> val m: RowMatrix = new RowMatrix(rows)
m: org.apache.spark.mllib.linalg.distributed.RowMatrix =
org.apache.spark.mllib.linalg.distributed.RowMatrix@5c5043fe
scala> print("Matrix size is " + m.numRows()+"X"+m.numCols())
Matrix size is 3X4
scala>
Python:
>>> from pyspark.mllib.linalg import Vector,Vectors
>>> from pyspark.mllib.linalg.distributed import RowMatrix
>>> dense_vlist = [Vectors.dense(11.0,12,13,14),
Vectors.dense(21.0,22,23,24), Vectors.dense(31.0,32,33,34)]
>>> rows = sc.parallelize(dense_vlist)
>>> m = RowMatrix(rows)
>>> "Matrix size is {0} X {1}".format(m.numRows(), m.numCols())
'Matrix size is 3 X 4'
IndexedRowMatrix
将行索引前缀添加到行条目。这在执行连接操作时很有用。你需要传递IndexedRow
对象来创建一个IndexedRowMatrix
。IndexedRow
对象是一个包含long
类型Index
和行元素的Vector
的封装器。
CoordinatedMatrix
将数据存储为行列索引和元素值的元组。BlockMatrix
将分布式矩阵表示为局部矩阵的块。提供了将矩阵从一种类型转换为另一种类型的方法,但这些操作非常昂贵,使用时应谨慎。
ML 流水线
现实中的机器学习工作流程是一个迭代循环,包含数据提取、数据清洗、预处理、探索、特征提取、模型拟合和评估。Spark 上的 ML 流水线是一个简单的 API,供用户设置复杂的机器学习工作流程。它的设计旨在解决一些常见问题,如参数调整、基于不同数据划分(交叉验证)或不同参数集训练多个模型等。编写脚本来自动化整个过程不再是必需的,所有这些都可以在 Pipeline API 中处理。
Pipeline API 由一系列流水线阶段组成(作为 转换器 和 估算器 等抽象的实现),这些阶段将按预定顺序执行。
在 ML 流水线中,您可以调用上一章讨论过的数据清洗/转换函数,并调用 MLlib 中可用的机器学习算法。这可以通过迭代的方式进行,直到您获得模型的理想性能。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_003.jpg
转换器
转换器是一个抽象,实现了 transform()
方法,将一个 DataFrame 转换成另一个 DataFrame。如果该方法是一个特征转换器,结果 DataFrame 可能包含一些基于您执行的操作的额外转换列。然而,如果该方法是一个学习模型,那么结果 DataFrame 将包含一个包含预测结果的额外列。
估算器
估算器是一个抽象,它可以是任何实现了 fit()
方法的学习算法,用来在 DataFrame 上训练以生成模型。从技术上讲,这个模型是给定 DataFrame 的转换器。
示例:逻辑回归是一种学习算法,因此是一个估算器。调用 fit()
会训练一个逻辑回归模型,生成的模型是一个转换器,可以生成一个包含预测列的 DataFrame。
以下示例演示了一个简单的单阶段流水线。
Scala:
//Pipeline example with single stage to illustrate syntax
scala> import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._
//Create source data frame
scala> val df = spark.createDataFrame(Seq(
("Oliver Twist","Charles Dickens"),
("Adventures of Tom Sawyer","Mark Twain"))).toDF(
"Title","Author")
//Split the Title to tokens
scala> val tok = new Tokenizer().setInputCol("Title").
setOutputCol("words")
tok: org.apache.spark.ml.feature.Tokenizer = tok_2b2757a3aa5f
//Define a pipeline with a single stage
scala> val p = new Pipeline().setStages(Array(tok))
p: org.apache.spark.ml.Pipeline = pipeline_f5e0de400666
//Run an Estimator (fit) using the pipeline
scala> val model = p.fit(df)
model: org.apache.spark.ml.PipelineModel = pipeline_d00989625bb2
//Examine stages
scala> p.getStages //Returns a list of stage objects
res1: Array[org.apache.spark.ml.PipelineStage] = Array(tok_55af0061af6d)
// Examine the results
scala> val m = model.transform(df).select("Title","words")
m: org.apache.spark.sql.DataFrame = [Title: string, words: array<string>]
scala> m.select("words").collect().foreach(println)
[WrappedArray(oliver, twist)]
[WrappedArray(adventures, of, tom, sawyer)]
Python:
//Pipeline example with single stage to illustrate syntax
//Create source data frame
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Tokenizer
>>> df = sqlContext.createDataFrame([
("Oliver Twist","Charles Dickens"),
("Adventures of Tom Sawyer","Mark Twain")]).toDF("Title","Author")
>>>
//Split the Title to tokens
>>> tok = Tokenizer(inputCol="Title",outputCol="words")
//Define a pipeline with a single stage
>>> p = Pipeline(stages=[tok])
//Run an Estimator (fit) using the pipeline
>>> model = p.fit(df)
//Examine stages
>>> p.getStages() //Returns a list of stage objects
[Tokenizer_4f35909c4c504637a263]
// Examine the results
>>> m = model.transform(df).select("Title","words")
>>> [x[0] for x in m.select("words").collect()]
[[u'oliver', u'twist'], [u'adventures', u'of', u'tom', u'sawyer']]
>>>
上面的示例展示了流水线的创建和执行,尽管这里只有一个阶段,在此上下文中是一个 Tokenizer。Spark 提供了若干个“特征转换器”,这些特征转换器在数据清洗和数据准备阶段非常有用。
以下示例展示了将原始文本转换为特征向量的实际案例。如果您不熟悉 TF-IDF,可以阅读这个来自 www.tfidf.com
的简短教程。
Scala:
scala> import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._
scala>
//Create a dataframe
scala> val df2 = spark.createDataset(Array(
(1,"Here is some text to illustrate pipeline"),
(2, "and tfidf, which stands for term frequency inverse document
frequency"
))).toDF("LineNo","Text")
//Define feature transformations, which are the pipeline stages
// Tokenizer splits text into tokens
scala> val tok = new Tokenizer().setInputCol("Text").
setOutputCol("Words")
tok: org.apache.spark.ml.feature.Tokenizer = tok_399dbfe012f8
// HashingTF maps a sequence of words to their term frequencies using hashing
// Larger value of numFeatures reduces hashing collision possibility
scala> val tf = new HashingTF().setInputCol("Words").setOutputCol("tf").setNumFeatures(100)
tf: org.apache.spark.ml.feature.HashingTF = hashingTF_e6ad936536ea
// IDF, Inverse Docuemnt Frequency is a statistical weight that reduces weightage of commonly occuring words
scala> val idf = new IDF().setInputCol("tf").setOutputCol("tf_idf")
idf: org.apache.spark.ml.feature.IDF = idf_8af1fecad60a
// VectorAssembler merges multiple columns into a single vector column
scala> val va = new VectorAssembler().setInputCols(Array("tf_idf")).setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_23205c3f92c8
//Define pipeline
scala> val tfidf_pipeline = new Pipeline().setStages(Array(tok,tf,idf,va))
val tfidf_pipeline = new Pipeline().setStages(Array(tok,tf,idf,va))
scala> tfidf_pipeline.getStages
res2: Array[org.apache.spark.ml.PipelineStage] = Array(tok_399dbfe012f8, hashingTF_e6ad936536ea, idf_8af1fecad60a, vecAssembler_23205c3f92c8)
scala>
//Now execute the pipeline
scala> val result = tfidf_pipeline.fit(df2).transform(df2).select("words","features").first()
result: org.apache.spark.sql.Row = [WrappedArray(here, is, some, text, to, illustrate, pipeline),(100,[0,3,35,37,69,81],[0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644,0.4054651081081644])]
Python:
//A realistic, multi-step pipeline that converts text to TF_ID
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Tokenizer, HashingTF, IDF, VectorAssembler, \
StringIndexer, VectorIndexer
//Create a dataframe
>>> df2 = sqlContext.createDataFrame([
[1,"Here is some text to illustrate pipeline"],
[2,"and tfidf, which stands for term frequency inverse document
frequency"
]]).toDF("LineNo","Text")
//Define feature transformations, which are the pipeline stages
//Tokenizer splits text into tokens
>>> tok = Tokenizer(inputCol="Text",outputCol="words")
// HashingTF maps a sequence of words to their term frequencies using
hashing
// Larger the numFeatures, lower the hashing collision possibility
>>> tf = HashingTF(inputCol="words", outputCol="tf",numFeatures=1000)
// IDF, Inverse Docuemnt Frequency is a statistical weight that reduces
weightage of commonly occuring words
>>> idf = IDF(inputCol = "tf",outputCol="tf_idf")
// VectorAssembler merges multiple columns into a single vector column
>>> va = VectorAssembler(inputCols=["tf_idf"],outputCol="features")
//Define pipeline
>>> tfidf_pipeline = Pipeline(stages=[tok,tf,idf,va])
>>> tfidf_pipeline.getStages()
[Tokenizer_4f5fbfb6c2a9cf5725d6, HashingTF_4088a47d38e72b70464f, IDF_41ddb3891541821c6613, VectorAssembler_49ae83b800679ac2fa0e]
>>>
//Now execute the pipeline
>>> result = tfidf_pipeline.fit(df2).transform(df2).select("words","features").collect()
>>> [(x[0],x[1]) for x in result]
[([u'here', u'is', u'some', u'text', u'to', u'illustrate', u'pipeline'], SparseVector(1000, {135: 0.4055, 169: 0.4055, 281: 0.4055, 388: 0.4055, 400: 0.4055, 603: 0.4055, 937: 0.4055})), ([u'and', u'tfidf,', u'which', u'stands', u'for', u'term', u'frequency', u'inverse', u'document', u'frequency'], SparseVector(1000, {36: 0.4055, 188: 0.4055, 333: 0.4055, 378: 0.4055, 538: 0.4055, 597: 0.4055, 727: 0.4055, 820: 0.4055, 960: 0.8109}))]
>>>
本示例创建并执行了一个多阶段流水线,将文本转换为可以被机器学习算法处理的特征向量。在继续之前,我们再看几个其他特性。
Scala:
scala> import org.apache.spark.ml.feature._
import org.apache.spark.ml.feature._
scala>
//Basic examples illustrating features usage
//Look at model examples for more feature examples
//Binarizer converts continuous value variable to two discrete values based on given threshold
scala> import scala.util.Random
import scala.util.Random
scala> val nums = Seq.fill(10)(Random.nextDouble*100)
...
scala> val numdf = spark.createDataFrame(nums.map(Tuple1.apply)).toDF("raw_nums")
numdf: org.apache.spark.sql.DataFrame = [raw_nums: double]
scala> val binarizer = new Binarizer().setInputCol("raw_nums").
setOutputCol("binary_vals").setThreshold(50.0)
binarizer: org.apache.spark.ml.feature.Binarizer = binarizer_538e392f56db
scala> binarizer.transform(numdf).select("raw_nums","binary_vals").show(2)
+------------------+-----------+
| raw_nums|binary_vals|
+------------------+-----------+
|55.209245003482884| 1.0|
| 33.46202184060426| 0.0|
+------------------+-----------+
scala>
//Bucketizer to convert continuous value variables to desired set of discrete values
scala> val split_vals:Array[Double] = Array(0,20,50,80,100) //define intervals
split_vals: Array[Double] = Array(0.0, 20.0, 50.0, 80.0, 100.0)
scala> val b = new Bucketizer().
setInputCol("raw_nums").
setOutputCol("binned_nums").
setSplits(split_vals)
b: org.apache.spark.ml.feature.Bucketizer = bucketizer_a4dd599e5977
scala> b.transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
| raw_nums|binned_nums|
+------------------+-----------+
|55.209245003482884| 2.0|
| 33.46202184060426| 1.0|
+------------------+-----------+
scala>
//Bucketizer is effectively equal to binarizer if only two intervals are
given
scala> new Bucketizer().setInputCol("raw_nums").
setOutputCol("binned_nums").setSplits(Array(0,50.0,100.0)).
transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
| raw_nums|binned_nums|
+------------------+-----------+
|55.209245003482884| 1.0|
| 33.46202184060426| 0.0|
+------------------+-----------+
scala>
Python:
//Some more features
>>> from pyspark.ml import feature, pipeline
>>>
//Basic examples illustrating features usage
//Look at model examples for more examples
//Binarizer converts continuous value variable to two discrete values based on given threshold
>>> import random
>>> nums = [random.random()*100 for x in range(1,11)]
>>> numdf = sqlContext.createDataFrame(
[[x] for x in nums]).toDF("raw_nums")
>>> binarizer = feature.Binarizer(threshold= 50,
inputCol="raw_nums", outputCol="binary_vals")
>>> binarizer.transform(numdf).select("raw_nums","binary_vals").show(2)
+------------------+-----------+
| raw_nums|binary_vals|
+------------------+-----------+
| 95.41304359504672| 1.0|
|41.906045589243405| 0.0|
+------------------+-----------+
>>>
//Bucketizer to convert continuous value variables to desired set of discrete values
>>> split_vals = [0,20,50,80,100] //define intervals
>>> b =
feature.Bucketizer(inputCol="raw_nums",outputCol="binned_nums",splits=split
vals)
>>> b.transform(numdf).select("raw_nums","binned_nums").show(2)
+------------------+-----------+
| raw_nums|binned_nums|
+------------------+-----------+
| 95.41304359504672| 3.0|
|41.906045589243405| 1.0|
+------------------+-----------+
//Bucketizer is effectively equal to binarizer if only two intervals are
given
>>> feature.Bucketizer(inputCol="raw_nums",outputCol="binned_nums",
splits=[0,50.0,100.0]).transform(numdf).select(
"raw_nums","binned_nums").show(2)
+------------------+-----------+
| raw_nums|binned_nums|
+------------------+-----------+
| 95.41304359504672| 1.0|
|41.906045589243405| 0.0|
+------------------+-----------+
>>>
机器学习简介
在本书的前几节中,我们学习了响应/结果变量如何与预测变量相关,通常是在监督学习的背景下。如今,人们通常用不同的名称来表示这两类变量。让我们看看它们的一些同义词,并在本书中交替使用:
-
输入变量 (X): 特征,预测变量,解释变量,自变量
-
输出变量 (Y): 响应变量,因变量
如果存在一个 Y 与 X 之间的关系,其中 X=X[1], X[2], X[3],…, X[n] (n 个不同的预测变量),那么可以写成如下形式:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_004.jpg
这里 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_005.jpg 是一个函数,表示 X 如何描述 Y,并且是未知的!这就是我们利用手头的观测数据点来找出的内容。这个术语
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_006.jpg
是一个均值为零并且与 X 独立的随机误差项。
基本上,这样的方程式会涉及两种类型的误差——可约误差和不可约误差。顾名思义,可约误差与函数相关,并且可以通过提高精度来最小化。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_007.jpg
通过使用更好的学习算法或调整相同的算法来提高。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_008.jpg
, 如果与 X 无关,仍然会存在一些无法解决的误差。这被称为不可约误差 (
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_009.jpg
)。总是存在一些影响结果变量的因素,但在建立模型时未考虑这些因素(因为它们大多数时候是未知的),并且这些因素会贡献到不可约误差项。因此,本书中讨论的方法将专注于最小化可约误差。
我们构建的大多数机器学习模型可以用于预测、推断或两者的组合。对于某些算法,函数
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_010.jpg
可以表示为一个方程,告诉我们因变量 Y 如何与自变量 (X1, X2,…, Xn) 相关联。在这种情况下,我们可以进行推断和预测。然而,某些算法是“黑盒”算法,我们只能进行预测,无法进行推断,因为 Y 如何与 X 相关是未知的。
请注意,线性机器学习模型可能更适用于推断场景,因为它们对于业务用户来说更具可解释性。然而,在预测场景中,可能会有更好的算法提供更准确的预测,但它们的可解释性较差。当推断是目标时,我们应优先选择具有更好可解释性的限制性模型,如线性回归;而当只有预测是目标时,我们可以选择使用高度灵活的模型,如支持向量机(SVM),这些模型可解释性较差但准确性更高(然而,这在所有情况下并不成立)。您需要根据业务需求仔细选择算法,权衡可解释性和准确性之间的利弊。让我们更深入地理解这些概念背后的基本原理。
基本上,我们需要一组数据点(训练数据)来构建模型以估算
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_011.jpg
(X),从而Y =
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_012.jpg
(X)。一般来说,这些学习方法可以是参数化的,也可以是非参数化的。
参数方法
参数方法遵循一个两步过程。在第一步中,您假设X的形状
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_013.jpg
()。例如,X与Y呈线性关系,因此X的函数是
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_014.jpg
*(X),*可以用以下线性方程表示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Beta1.jpg
选择模型后,第二步是使用手头的数据点来训练模型,估算参数β0、β1、…、βn,从而:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Beta-2.jpg
这种参数化方法的一个缺点是我们假设的线性关系对于https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_016.jpg*()*可能在实际生活中并不成立。
非参数方法
我们不对Y和X之间的线性关系以及变量的数据分布做任何假设,因此X的形式是
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_017.jpg
*()*在非参数方法中。由于它不假设任何形式的
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_018.jpg
(),它可以通过与数据点良好拟合来产生更好的结果,这可能是一个优势。
因此,非参数方法需要比参数方法更多的数据点来估算
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_019.jpg
*()*准确。不过请注意,如果没有得到妥善处理,它可能会导致过拟合问题。我们将在进一步讨论中详细探讨这个问题。
回归方法
回归方法是一种监督学习方法。如果响应变量是定量/连续的(如年龄、薪水、身高等数值),那么这个问题可以被称为回归问题,而不管解释变量的类型。针对回归问题,有多种建模技术。本节的重点将是线性回归技术及其一些不同的变种。
回归方法可以用来预测任何实际数值的结果。以下是一些例子:
-
根据员工的教育水平、位置、工作类型等预测薪资
-
预测股价
-
预测客户的购买潜力
-
预测机器故障前的运行时间
线性回归
在我们前一节参数方法讨论的基础上,在线性假设成立后,
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_020.jpg
(X),我们需要训练数据来拟合一个模型,该模型描述解释变量(记作 X)和响应变量(记作 Y)之间的关系。当只有一个解释变量时,称为简单线性回归;当有多个解释变量时,称为多元线性回归。简单线性回归是将一条直线拟合到二维空间中,当有两个预测变量时,它将拟合一个三维空间中的平面,依此类推,在维度更高的设置中,当变量超过两个时,也是如此。
线性回归方程的常见形式可以表示为:
Y’ =
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_021.jpg
(X) +
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_022.jpg
这里 Y’ 代表预测的结果变量。
只有一个预测变量的线性回归方程可以表示为:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Beta11.jpg
具有多个预测变量的线性回归方程可以表示为:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Beta22.jpg
这里 https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_025.jpg 是与 X 无关的无法简化的误差项,且其均值为零。我们无法控制它,但可以朝着优化的方向努力。
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_026.jpg
(X)。由于没有任何模型能够达到 100%的准确率,因此总会有一些与之相关的误差,这些误差源自无法简化的误差成分(
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_027.jpg
).
最常见的线性回归拟合方法叫做最小二乘法,也称为普通最小二乘法(OLS)方法。该方法通过最小化每个数据点到回归线的垂直偏差的平方和,找到最适合观察数据点的回归线。为了更好地理解线性回归的工作原理,让我们现在看一个简单线性回归的例子:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Beta33.jpg
其中,β0 是回归线的 Y 截距,β1 定义了回归线的斜率。意思是,β1 是 X 变化一个单位时 Y 的平均变化。我们以 X 和 Y 为例:
X | Y |
---|---|
1 | 12 |
2 | 20 |
3 | 13 |
4 | 38 |
5 | 27 |
如果我们通过数据点拟合一条线性回归线,如上表所示,那么它将呈现如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_028.jpg
在上图中,红色的垂直线表示预测误差,可以定义为实际的 Y 值和预测的 Y’ 值之间的差异。如果你将这些差异平方并求和,就得到了 平方误差和 (SSE),这是用来找到最优拟合线的最常见度量。下表显示了如何计算 SSE:
X | Y | Y’ | Y-Y’ | (Y-Y’) ² |
---|---|---|---|---|
1 | 12 | 12.4 | 0.4 | 0.16 |
2 | 20 | 17.2 | 2.8 | 7.84 |
3 | 13 | 22 | -9 | 81 |
4 | 38 | 26.8 | 11.2 | 125.44 |
5 | 27 | 31.6 | -4.6 | 21.16 |
总和 | 235.6 |
在上述表格中,(Y-Y’) 被称为残差。残差平方和 (RSS) 可以表示为:
RSS = residual[1]² + residual[2]² + residual[3]² + …+ residual[n]²
请注意,回归对异常值非常敏感,如果不在应用回归之前处理异常值,可能会引入巨大的 RSS 误差。
在回归线拟合到观察数据点之后,你应该通过将残差绘制在 Y 轴上,对应于 X 轴上的解释变量来检查残差。如果图像接近直线,那么你关于线性关系的假设是有效的,否则可能表明存在某种非线性关系。如果存在非线性关系,你可能需要考虑非线性。一个技术是通过向方程中加入高阶多项式来解决。
我们看到,RSS 是拟合回归线时的重要特征(在建立模型时)。现在,为了评估回归拟合的好坏(在模型建立后),你需要另外两个统计量——残差标准误差 (RSE) 和 R² 统计量。
我们讨论了不可减少的误差成分 ε,由于这个原因,即使你的方程完全拟合数据点并且正确估计了系数,也总会存在某种程度的误差。RSE 是 ε 标准差的估计,可以定义如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_029.jpg
这意味着实际值会平均偏离真实回归线一个 RSE 因子。
由于 RSE 实际上是以 Y 的单位来衡量的(参考我们在上一节如何计算 RSS),所以很难说它是模型精度的唯一最佳统计量。
因此,引入了一种替代方法,称为 R²统计量(也叫做决定系数)。计算 R²的公式如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_030.jpg
总平方和 (TSS) 可以通过以下方式计算:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_031.jpg
这里需要注意的是,TSS 衡量的是Y中固有的总变异性,即使在进行回归预测Y之前也包含在内。可以观察到其中没有Y’。相反,RSS 表示回归后Y中未解释的变异性。这意味着(TSS - RSS)能够解释回归后响应变量中的变异性。
R²统计量通常介于 0 到 1 之间,但如果拟合结果比拟合一个水平线还要差,它可能为负值,但这种情况很少发生。接近 1 的值表示回归方程可以解释响应变量中大部分的变异性,并且拟合效果良好。相反,接近 0 的值表示回归方程几乎没有解释响应变量中的变异性,拟合效果不好。例如,R² 为 0.25 意味着Y的 25%变异性由X解释,表示需要调优模型以改善效果。
现在让我们讨论如何通过回归来处理数据集中的非线性。正如前面所讨论的,当你发现非线性关系时,需要适当处理。为了使用相同的线性回归方法建模非线性方程,必须创建高阶特征,回归方法会将其视为另一个变量。例如,如果薪水是一个预测购买潜力的特征/变量,而我们发现它们之间存在非线性关系,那么我们可能会创建一个特征叫做(薪水³),具体取决于需要解决多少非线性问题。请注意,在创建这种高阶特征时,你还需要保留基础特征。在这个例子中,你必须在回归方程中同时使用(薪水)和(薪水³)。
到目前为止,我们假设所有预测变量都是连续的。如果存在类别型预测变量怎么办?在这种情况下,我们必须将这些变量进行虚拟编码(例如,将男性编码为 1,女性编码为 0),这样回归方法就会生成两个方程,一个用于性别=男性(该方程包含性别变量),另一个用于性别=女性(该方程不包含性别变量,因为它会被作为 0 值丢弃)。有时,当类别变量较少时,可以考虑根据类别变量的水平将数据集划分,并为每个部分构建单独的模型。
最小二乘线性回归的一个主要优点是它能解释结果变量如何与预测变量相关。这使得它非常易于解释,并且可以用来做推断和预测。
损失函数
许多机器学习问题可以被表述为凸优化问题。这个问题的目标是找到使平方损失最小的系数值。这个目标函数基本上包含两个部分——正则化项和损失函数。正则化项用于控制模型的复杂度(防止过拟合),而损失函数用于估计回归函数的系数,使得平方损失(RSS)最小。
用于最小二乘法的损失函数称为平方损失,如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_032.jpg
这里的Y是响应变量(实值),W是权重向量(系数值),X是特征向量。所以
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Capture-1.jpg
给出预测值,我们将其与实际值Y进行比较,以求得需要最小化的平方损失。
用于估计系数的算法称为梯度下降。不同类型的损失函数和优化算法适用于不同种类的机器学习算法,我们将在需要时介绍。
优化
最终,线性方法必须优化损失函数。在底层,线性方法使用凸优化方法来优化目标函数。MLlib 支持随机梯度下降(SGD)和有限内存 - Broyden-Fletcher-Goldfarb-Shanno(L-BFGS)算法。目前,大多数算法 API 支持 SGD,少数支持 L-BFGS。
SGD 是一种一阶优化技术,最适合大规模数据和分布式计算环境。目标函数(损失函数)可以写作求和形式的优化问题最适合使用 SGD 来解决。
L-BFGS 是一种优化算法,属于拟牛顿法家族,用于解决优化问题。与其他一阶优化技术如 SGD 相比,L-BFGS 通常能实现更快的收敛。
MLlib 中的一些线性方法同时支持 SGD 和 L-BFGS。你应该根据考虑的目标函数选择其中之一。一般来说,L-BFGS 相对于 SGD 更推荐,因为它收敛更快,但你需要根据需求仔细评估。
回归的正则化
在权重(系数值)较大的情况下,更容易导致模型过拟合。正则化是一种主要用于通过控制模型复杂度来消除过拟合问题的技术。通常当你发现训练数据和测试数据上的模型表现存在差异时,便可采用正则化。如果训练性能高于测试数据的性能,可能是过拟合的情况(高方差问题)。
为了解决这个问题,提出了一种正则化技术,通过对损失函数进行惩罚来改进模型。特别是在训练数据样本较少时,建议使用任何一种正则化技术。
在进一步讨论正则化技术之前,我们需要理解在有监督学习环境下,偏差和方差的含义,以及为什么它们总是存在某种权衡。虽然两者都与误差有关,有偏的模型意味着它倾向于某种错误的假设,并且可能在某种程度上忽视了预测变量与响应变量之间的关系。这是欠拟合的情况!另一方面,高方差模型意味着它试图拟合每一个数据点,最终却在建模数据集中的随机噪声。这就是过拟合的情况。
带 L2 惩罚的线性回归(L2 正则化)称为岭回归,带 L1 惩罚的线性回归(L1 正则化)称为Lasso 回归。当同时使用 L1 和 L2 惩罚时,称为弹性网回归。我们将在接下来的部分中逐一讨论它们。
相较于 L1 正则化问题,L2 正则化问题通常更容易解决,因为 L2 正则化具有平滑性,但 L1 正则化问题会导致权重的稀疏性,从而产生更小且更具可解释性的模型。因此,Lasso 有时被用来进行特征选择。
岭回归
当我们将 L2 惩罚(也称为收缩惩罚)添加到最小二乘法的损失函数中时,它变成了岭回归,如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_034.jpg
这里的λ(大于 0)是一个调节参数,需要单独确定。前面公式中的第二项被称为收缩惩罚,只有当系数(β0、β1…等)较小并接近 0 时,收缩惩罚才能变得较小。当λ = 0时,岭回归就变成了最小二乘法。随着 lambda 趋向无穷大,回归系数趋近于零(但永远不会是零)。
岭回归为每个λ值生成不同的系数值集合。因此,需要通过交叉验证仔细选择 lambda 值。随着 lambda 值的增加,回归线的灵活性减少,从而降低方差并增加偏差。
注意,收缩惩罚应用于所有解释变量,除截距项β0外。
当训练数据较少时,或者当预测变量或特征的数量大于观察值的数量时,岭回归表现得非常好。此外,岭回归所需的计算与最小二乘法几乎相同。
由于岭回归不会将任何系数值缩减为零,所有变量都会出现在模型中,这可能使得模型在变量数目较多时变得不易解释。
Lasso 回归
Lasso 是在岭回归之后引入的。当我们将 L1 惩罚添加到最小二乘的损失函数中时,它变成了 Lasso 回归,如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_035.jpg
这里的区别在于,它不是取平方系数,而是取系数的模。与岭回归不同,Lasso 回归可以将一些系数强制设为零,这可能导致一些变量被消除。因此,Lasso 回归也可以用于变量选择!
Lasso 为每个 lambda 值生成不同的系数值。因此,需要通过交叉验证仔细选择 lambda 值。像岭回归一样,随着 lambda 的增加,方差减小,偏差增大。
与岭回归相比,Lasso 产生的模型更容易解释,因为它通常只有部分变量。若有许多类别型变量,建议选择 Lasso 而不是岭回归。
实际上,岭回归和 Lasso 回归并不是总是互相优劣。Lasso 通常在少量预测变量且它们的系数较大,而其余系数非常小的情况下表现较好。岭回归通常在有大量预测变量且几乎所有的变量系数都很大且相似时表现较好。
岭回归适用于分组选择,并且能解决多重共线性问题。而 Lasso 则无法进行分组选择,通常只会选择一个预测变量。如果一组预测变量之间高度相关,Lasso 倾向于只选择其中一个,并将其他变量的系数缩小为零。
弹性网回归
当我们将 L1 和 L2 惩罚都添加到最小二乘损失函数中时,它就变成了弹性网回归,如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_036.jpg
以下是弹性网回归的优点:
-
强制稀疏性并有助于移除最不有效的变量
-
鼓励分组效应
-
结合了岭回归和 Lasso 回归的优点
弹性网回归的朴素版本会产生双重收缩问题,这会导致偏差增大和预测精度下降。为了解决这个问题,一种方法是通过将(1 + λ2)与估计的系数相乘来重新缩放它们:
Scala
import org.apache.spark.mllib.linalg.Vectors
import org.apache.spark.mllib.regression.LabeledPoint
import org.apache.spark.mllib.regression.LinearRegressionModel
import org.apache.spark.mllib.regression.LinearRegressionWithSGD
scala> import org.apache.spark.ml.regression.{LinearRegression,LinearRegressionModel}
import org.apache.spark.ml.regression.{LinearRegression,LinearRegressionModel}
// Load the data
scala> val data = spark.read.format("libsvm").load("data/mllib/sample_linear_regression_data.txt")
data: org.apache.spark.sql.DataFrame = [label: double, features: vector]
// Build the model
scala> val lrModel = new LinearRegression().fit(data)
//Note: You can change ElasticNetParam, MaxIter and RegParam
// Defaults are 0.0, 100 and 0.0
lrModel: org.apache.spark.ml.regression.LinearRegressionModel = linReg_aa788bcebc42
//Check Root Mean Squared Error
scala> println("Root Mean Squared Error = " + lrModel.summary.rootMeanSquaredError)
Root Mean Squared Error = 10.16309157133015
Python:
>>> from pyspark.ml.regression import LinearRegression, LinearRegressionModel
>>>
// Load the data
>>> data = spark.read.format("libsvm").load("data/mllib/sample_linear_regression_data.txt")
>>>
// Build the model
>>> lrModel = LinearRegression().fit(data)
//Note: You can change ElasticNetParam, MaxIter and RegParam
// Defaults are 0.0, 100 and 0.0
//Check Root Mean Squared Error
>>> print "Root Mean Squared Error = ", lrModel.summary.rootMeanSquaredError
Root Mean Squared Error = 10.16309157133015
>>>
分类方法
如果响应变量是定性/类别型的(例如性别、贷款违约、婚姻状况等),那么无论解释变量的类型如何,这个问题都可以称为分类问题。分类方法有很多种,但在本节中我们将重点讨论逻辑回归和支持向量机。
以下是一些分类方法的应用实例:
-
一个顾客购买某个产品或不购买
-
一个人是否患有糖尿病
-
一个申请贷款的个人会违约或不会违约
-
一个电子邮件接收者会读邮件或不读
逻辑回归
逻辑回归衡量解释变量与分类响应变量之间的关系。我们不会对分类响应变量使用线性回归,因为响应变量不是连续的,因此误差项不是正态分布的。
所以逻辑回归是一个分类算法。逻辑回归不是直接建模响应变量Y,而是建模P(Y|X)的概率分布,即Y属于某一特定类别的概率。条件分布(Y|X)是伯努利分布,而不是高斯分布。逻辑回归方程可以表示为:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_037.jpg
对于二分类问题,模型的输出应该仅限于两个类别之一(例如 0 或 1)。由于逻辑回归预测的是概率而不是直接预测类别,我们使用一个逻辑函数(也称为sigmoid 函数)来将输出限制为一个单一的类别:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_038.jpg
解前面的方程会得到以下结果:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Capture-2.jpg
它可以进一步简化为:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_040.jpg
左边的量 P(X)/1-P(X) 被称为赔率。赔率的值范围从 0 到无穷大。接近 0 的值表示概率很小,而数值较大的则表示概率很高。在某些情况下,赔率会直接代替概率使用,这取决于具体情况。
如果我们取赔率的对数,它就变成了对数赔率或 logit,可以表示如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_041.jpg
从前面的方程中可以看出,logit 与 X 是线性相关的。
在有两个类别 1 和 0 的情况下,我们当p >= 0.5时预测Y = 1,当p < 0.5时预测Y = 0。因此,逻辑回归实际上是一个线性分类器,决策边界为p = 0.5。在某些商业场景中,p可能默认并不设置为 0.5,您可能需要使用一些数学技巧来确定合适的值。
一种称为最大似然法的方法被用来通过计算回归系数来拟合模型,该算法可以像线性回归一样使用梯度下降。
在逻辑回归中,损失函数应关注错误分类率。因此,逻辑回归使用的损失函数称为逻辑损失,如下所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_042.jpg
注意
请注意,当您使用更高阶的多项式来更好地拟合模型时,逻辑回归也容易过拟合。为了解决这个问题,您可以像在线性回归中一样使用正则化项。到目前为止,Spark 不支持正则化逻辑回归,因此我们暂时跳过这一部分。
线性支持向量机(SVM)
支持向量机(SVM)是一种监督学习算法,可以用于分类和回归。然而,它在解决分类问题时更为流行,且由于 Spark 将其作为 SVM 分类器提供,我们将仅限于讨论分类设置。当作为分类器使用时,与逻辑回归不同,它是一个非概率性分类器。
SVM(支持向量机)起源于一种简单的分类器,称为最大间隔分类器。由于最大间隔分类器要求类别通过线性边界可分,因此它无法应用于许多数据集。因此,它被扩展为一种改进版本,称为支持向量分类器,能够处理类别重叠且类别之间没有明显分隔的情况。支持向量分类器进一步扩展为我们所称的 SVM,以适应非线性类别边界。让我们一步步讨论 SVM 的演变,帮助我们更清楚地理解它的工作原理。
如果数据集有p维度(特征),那么我们将在这个 p 维空间中拟合一个超平面,其方程可以定义如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_043.jpg
这个超平面被称为分隔超平面,形成决策边界。结果将根据结果进行分类;如果大于 0,则位于一侧;如果小于 0,则位于另一侧,如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_044.jpg
在前面的图中观察到,可以有多个超平面(它们可以是无限的)。应该有一种合理的方法来选择最优的超平面。这就是我们选择最大间隔超平面的地方。如果你计算所有数据点到分隔超平面的垂直距离,那么最小的距离被称为间隔。因此,对于最大间隔分类器,超平面应该具有最大的间隔。
与分隔超平面距离近且等距的训练观测值被称为支持向量。对于支持向量的任何轻微变化,超平面也会重新定向。这些支持向量实际上定义了间隔。那么,如果考虑的两个类别不可分怎么办?我们可能希望有一个分类器,它不完美地将两个类别分开,并且有一个较软的边界,允许一定程度的误分类。这一需求促使了支持向量分类器(也称为软间隔分类器)的引入。
从数学上讲,它是方程中的松弛变量,允许出现误分类。此外,支持向量分类器中还有一个调节参数,应该通过交叉验证来选择。这个调节参数是在偏差和方差之间进行权衡的,需要小心处理。当它较大时,边界较宽,包含许多支持向量,具有较低的方差和较高的偏差。如果它较小,边界中的支持向量较少,分类器将具有较低的偏差但较高的方差。
SVM 的损失函数可以表示如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_045.jpg
截至本文写作时,Spark 只支持线性 SVM。默认情况下,线性 SVM 会使用 L2 正则化进行训练。Spark 还支持替代的 L1 正则化。
到目前为止都很好!但是,当类别之间存在非线性边界时,支持向量分类器如何工作呢?如下图所示:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_046.jpg
任何线性分类器,例如支持向量分类器,在前述情况下都会表现得非常差。如果它通过数据点画一条直线,那么类别将无法正确分离。这就是非线性类别边界的一个例子。解决这个问题的方法是使用 SVM。换句话说,当支持向量分类器与非线性核函数结合时,它就变成了 SVM。
类似于我们在回归方程中引入高阶多项式项以处理非线性一样,SVM 中也可以做类似的处理。SVM 使用被称为核函数的东西来处理数据集中的不同类型的非线性;不同的核函数适用于不同类型的非线性。核方法将数据映射到更高维的空间中,因为如果这样做,数据可能会被很好地分离开来。此外,它还使区分不同类别变得更加容易。我们来讨论几个重要的核函数,以便能够选择正确的核函数。
线性核函数
这是最基本的一种核函数类型,它只允许我们挑选出直线或超平面。它相当于一个支持向量分类器。如果数据集中存在非线性,它无法处理。
多项式核函数
这使我们能够在多项式阶数的范围内处理一定程度的非线性。当训练数据已经规范化时,这种方法表现得很好。这个核函数通常有更多的超参数,因此会增加模型的复杂度。
径向基函数核
当你不确定使用哪个核函数时,径向基函数 (RBF) 是一个很好的默认选择。它可以让你挑选出圆形或超球体。虽然它通常比线性核函数或多项式核函数表现得更好,但当特征数非常大时,它的表现可能不佳。
Sigmoid 核函数
Sigmoid 核函数源自神经网络。因此,具有 Sigmoid 核的 SVM 等同于具有双层感知机的神经网络。
训练一个 SVM
在训练 SVM 时,建模者需要做出一些决策:
-
如何预处理数据(转换与缩放)。分类变量应通过虚拟化转换为数值型变量。同时,需要对数值进行缩放(将其归一化到 0 到 1 或 -1 到 +1)。
-
选择哪个核函数(如果你无法可视化数据或得出结论,可以通过交叉验证检查)。
-
SVM 的参数设置:惩罚参数和核函数参数(通过交叉验证或网格搜索找到)。
如有需要,您可以使用基于熵的特征选择方法,只包含模型中的重要特征。
Scala:
scala> import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD}
import org.apache.spark.mllib.classification.{SVMModel, SVMWithSGD}
scala> import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
import org.apache.spark.mllib.evaluation.BinaryClassificationMetrics
scala> import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.mllib.util.MLUtils
scala>
// Load training data in LIBSVM format.
scala> val data = MLUtils.loadLibSVMFile(sc, "data/mllib/sample_libsvm_data.txt")
data: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[6] at map at MLUtils.scala:84
scala>
// Split data into training (60%) and test (40%).
scala> val splits = data.randomSplit(Array(0.6, 0.4), seed = 11L)
splits: Array[org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint]] = Array(MapPartitionsRDD[7] at randomSplit at <console>:29, MapPartitionsRDD[8] at randomSplit at <console>:29)
scala> val training = splits(0).cache()
training: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[7] at randomSplit at <console>:29
scala> val test = splits(1)
test: org.apache.spark.rdd.RDD[org.apache.spark.mllib.regression.LabeledPoint] = MapPartitionsRDD[8] at randomSplit at <console>:29
scala>
// Run training algorithm to build the model
scala> val model = SVMWithSGD.train(training, numIterations=100)
model: org.apache.spark.mllib.classification.SVMModel = org.apache.spark.mllib.classification.SVMModel: intercept = 0.0, numFeatures = 692, numClasses = 2, threshold = 0.0
scala>
// Clear the default threshold.
scala> model.clearThreshold()
res1: model.type = org.apache.spark.mllib.classification.SVMModel: intercept =
0.0, numFeatures = 692, numClasses = 2, threshold = None
scala>
// Compute raw scores on the test set.
scala> val scoreAndLabels = test.map { point =>
val score = model.predict(point.features)
(score, point.label)
}
scoreAndLabels: org.apache.spark.rdd.RDD[(Double, Double)] =
MapPartitionsRDD[213] at map at <console>:37
scala>
// Get evaluation metrics.
scala> val metrics = new BinaryClassificationMetrics(scoreAndLabels)
metrics: org.apache.spark.mllib.evaluation.BinaryClassificationMetrics = org.apache.spark.mllib.evaluation.BinaryClassificationMetrics@3106aebb
scala> println("Area under ROC = " + metrics.areaUnderROC())
Area under ROC = 1.0
scala>
注意
mllib
已经进入维护模式,SVM 仍未在 ml 模块下提供,因此这里只提供了 Scala 代码示例。
决策树
决策树是一种非参数化的监督学习算法,既可以用于分类问题,也可以用于回归问题。决策树像倒立的树,根节点位于顶部,叶节点向下延展。存在不同的算法用于将数据集划分为分支状的段。每个叶节点被分配到一个类别,表示最合适的目标值。
决策树不需要对数据集进行任何缩放或转换,直接处理原始数据。它们既能处理分类特征,又能处理连续特征,并且能够解决数据集中的非线性问题。决策树本质上是一种贪心算法(它只考虑当前最佳分裂,而不考虑未来的情况),通过递归二元划分特征空间进行操作。划分是基于每个节点的信息增益,因为信息增益衡量了给定特征在目标类别或值上的区分效果。第一次分裂发生在产生最大信息增益的特征上,成为根节点。
节点的信息增益是父节点的不纯度与两个子节点的不纯度加权和之间的差值。为了估算信息增益,Spark 目前针对分类问题提供了两种 impurity 度量方法,针对回归问题提供了一种度量方法,具体如下。
不纯度度量
不纯度是衡量同质性的一种方法,也是递归划分的最佳标准。通过计算不纯度,可以决定最佳的划分候选。大多数不纯度度量方法是基于概率的:
某一类别的概率 = 该类别的观察次数 / 总观察次数
让我们花点时间来探讨 Spark 支持的几种重要的 impurity(不纯度)度量方法。
基尼指数
基尼指数主要用于数据集中连续的属性或特征。如果不是这样,它将假设所有属性和特征都是连续的。该分裂使得子节点比父节点更加纯净。基尼倾向于找到最大类别——即响应变量中观察数最多的类别。它可以定义如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_047.jpg
如果所有响应的观察值都属于同一类别,那么该类别P的概率j,即(Pj),将为 1,因为只有一个类别,而*(Pj)2*也将为 1。这使得基尼指数为零。
熵
熵主要用于数据集中的分类属性或特征。它可以定义如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_048.jpg
如果所有响应的观察值都属于同一类别,那么该类别的概率(Pj)将为 1,*log§*将为零。这样熵将为零。
下图显示了公平掷硬币的概率:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/Capture-3.jpg
为了说明前面的图,如果你掷一枚公平的硬币,正面或反面的概率为 0.5,因此在概率为 0.5 时将有最多的观察值。
如果数据样本完全同质,那么熵为零;如果样本可以平分为两个部分,那么熵为一。
它的计算速度比基尼指数稍慢,因为它还需要计算对数。
方差
与基尼指数和熵不同,方差用于计算回归问题的信息增益。方差可以定义为:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_050.jpg
停止规则
当满足以下条件之一时,递归树构建将在某个节点停止:
-
节点深度等于
maxDepth
训练参数 -
没有分裂候选导致的信息增益大于
minInfoGain
-
没有分裂候选产生的子节点,每个子节点至少有
minInstancesPerNode
个训练实例
分裂候选
数据集通常包含分类特征和连续特征的混合。我们需要了解特征如何进一步分裂成分裂候选,因为有时我们需要对它们进行一定程度的控制,以建立更好的模型。
分类特征
对于具有M个可能值(类别)的分类特征,可以提出2(M-ˆ’1)-ˆ’1个分裂候选。无论是二分类还是回归,分裂候选的数量可以通过按平均标签对分类特征值排序减少到M-ˆ’1。
例如,考虑一个二元分类(0/1)问题,其中有一个具有三个类别 A、B 和 C 的分类特征,它们对应的标签-1 响应变量的比例分别为 0.2、0.6 和 0.4。在这种情况下,分类特征可以按 A、C、B 排列。所以,两个分裂候选项(M-1 = 3-1 = 2)可以是 A | (C, B) 和 A, (C | B),其中 ‘|’ 表示分裂。
连续特征
对于一个连续特征变量,可能没有两个值是相同的(至少我们可以假设如此)。如果有 n 个观测值,那么 n 个分裂候选项可能不是一个好主意,尤其是在大数据环境下。
在 Spark 中,通过对数据样本进行分位数计算,并相应地对数据进行分箱来完成此操作。你仍然可以通过 maxBins
参数控制最大分箱数。maxBins
的最大默认值是 32
。
决策树的优点
-
它们容易理解和解释,因此也很容易向业务用户解释
-
它们适用于分类和回归
-
在构建决策树时,定性和定量数据都可以得到处理
决策树中的信息增益偏向于具有更多层次的属性。
决策树的缺点
-
它们对于连续结果变量的效果不是特别好
-
当类别很多且数据集很小时,性能较差
-
轴平行切分会降低精度
-
它们容易受到高方差的影响,因为它们尝试拟合几乎所有的数据点
示例
在实现方面,分类树和回归树之间没有太大区别。让我们看看在 Spark 上的实际实现。
Scala:
//Assuming ml.Pipeline and ml.features are already imported
scala> import org.apache.spark.ml.classification.{
DecisionTreeClassifier, DecisionTreeClassificationModel}
import org.apache.spark.ml.classification.{DecisionTreeClassifier,
DecisionTreeClassificationModel}
scala>
/prepare train data
scala> val f:String = "<Your path>/simple_file1.csv"
f: String = <your path>/simple_file1.csv
scala> val trainDF = spark.read.options(Map("header"->"true",
"inferSchema"->"true")).csv(f)
trainDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int]
scala>
//define DecisionTree pipeline
//StringIndexer maps labels(String or numeric) to label indices
//Maximum occurrence label becomes 0 and so on
scala> val lblIdx = new StringIndexer().
setInputCol("Label").
setOutputCol("indexedLabel")
lblIdx: org.apache.spark.ml.feature.StringIndexer = strIdx_3a7bc9c1ed0d
scala>
// Create labels list to decode predictions
scala> val labels = lblIdx.fit(trainDF).labels
labels: Array[String] = Array(2, 1, 3)
scala>
//Define Text column indexing stage
scala> val fIdx = new StringIndexer().
setInputCol("Text").
setOutputCol("indexedText")
fIdx: org.apache.spark.ml.feature.StringIndexer = strIdx_49253a83c717
// VectorAssembler
scala> val va = new VectorAssembler().
setInputCols(Array("indexedText")).
setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_764720c39a85
//Define Decision Tree classifier. Set label and features vector
scala> val dt = new DecisionTreeClassifier().
setLabelCol("indexedLabel").
setFeaturesCol("features")
dt: org.apache.spark.ml.classification.DecisionTreeClassifier = dtc_84d87d778792
//Define label converter to convert prediction index back to string
scala> val lc = new IndexToString().
setInputCol("prediction").
setOutputCol("predictedLabel").
setLabels(labels)
lc: org.apache.spark.ml.feature.IndexToString = idxToStr_e2f4fa023665
scala>
//String the stages together to form a pipeline
scala> val dt_pipeline = new Pipeline().setStages(
Array(lblIdx,fIdx,va,dt,lc))
dt_pipeline: org.apache.spark.ml.Pipeline = pipeline_d4b0e884dcbf
scala>
//Apply pipeline to the train data
scala> val resultDF = dt_pipeline.fit(trainDF).transform(trainDF)
//Check results. Watch Label and predictedLabel column values match
resultDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int ... 6 more
fields]
scala>
resultDF.select("Text","Label","features","prediction","predictedLabel").show()
+----+-----+--------+----------+--------------+
|Text|Label|features|prediction|predictedLabel|
+----+-----+--------+----------+--------------+
| A| 1| [1.0]| 1.0| 1|
| B| 2| [0.0]| 0.0| 2|
| C| 3| [2.0]| 2.0| 3|
| A| 1| [1.0]| 1.0| 1|
| B| 2| [0.0]| 0.0| 2|
+----+-----+--------+----------+--------------+
scala>
//Prepare evaluation data
scala> val eval:String = "€œ<Your path>/simple_file2.csv"
eval: String = <Your path>/simple_file2.csv
scala> val evalDF = spark.read.options(Map("header"->"true",
"inferSchema"->"true")).csv(eval)
evalDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int]
scala>
//Apply the same pipeline to the evaluation data
scala> val eval_resultDF = dt_pipeline.fit(evalDF).transform(evalDF)
eval_resultDF: org.apache.spark.sql.DataFrame = [Text: string, Label: int ... 7
more fields]
//Check evaluation results
scala>
eval_resultDF.select("Text","Label","features","prediction","predictedLabel").sh
w()
+----+-----+--------+----------+--------------+
|Text|Label|features|prediction|predictedLabel|
+----+-----+--------+----------+--------------+
| A| 1| [0.0]| 1.0| 1|
| A| 1| [0.0]| 1.0| 1|
| A| 2| [0.0]| 1.0| 1|
| B| 2| [1.0]| 0.0| 2|
| C| 3| [2.0]| 2.0| 3|
+----+-----+--------+----------+--------------+
//Note that predicted label for the third row is 1 as against Label(2) as
expected
Python:
//Model training example
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import StringIndexer, VectorIndexer, VectorAssembler,
IndexToString
>>> from pyspark.ml.classification import DecisionTreeClassifier,
DecisionTreeClassificationModel
>>>
//prepare train data
>>> file_location = "../work/simple_file1.csv"
>>> trainDF = spark.read.csv(file_location,header=True,inferSchema=True)
//Read file
>>>
//define DecisionTree pipeline
//StringIndexer maps labels(String or numeric) to label indices
//Maximum occurrence label becomes 0 and so on
>>> lblIdx = StringIndexer(inputCol = "Label",outputCol = "indexedLabel")
// Create labels list to decode predictions
>>> labels = lblIdx.fit(trainDF).labels
>>> labels
[u'2', u'1', u'3']
>>>
//Define Text column indexing stage
>>> fidx = StringIndexer(inputCol="Text",outputCol="indexedText")
// Vector assembler
>>> va = VectorAssembler(inputCols=["indexedText"],outputCol="features")
//Define Decision Tree classifier. Set label and features vector
>>> dt = DecisionTreeClassifier(labelCol="indexedLabel",featuresCol="features")
//Define label converter to convert prediction index back to string
>>> lc = IndexToString(inputCol="prediction",outputCol="predictedLabel",
labels=labels)
//String the stages together to form a pipeline
>>> dt_pipeline = Pipeline(stages=[lblIdx,fidx,va,dt,lc])
>>>
>>>
//Apply decision tree pipeline
>>> dtModel = dt_pipeline.fit(trainDF)
>>> dtDF = dtModel.transform(trainDF)
>>> dtDF.columns
['Text', 'Label', 'indexedLabel', 'indexedText', 'features', 'rawPrediction',
'probability', 'prediction', 'predictedLabel']
>>> dtDF.select("Text","Label","indexedLabel","prediction",
"predictedLabel").show()
+----+-----+------------+----------+--------------+
|Text|Label|indexedLabel|prediction|predictedLabel|
+----+-----+------------+----------+--------------+
| A| 1| 1.0| 1.0| 1|
| B| 2| 0.0| 0.0| 2|
| C| 3| 2.0| 2.0| 3|
| A| 1| 1.0| 1.0| 1|
| B| 2| 0.0| 0.0| 2|
+----+-----+------------+----------+--------------+
>>>
>>> //prepare evaluation dataframe
>>> eval_file_path = "../work/simple_file2.csv"
>>> evalDF = spark.read.csv(eval_file_path,header=True, inferSchema=True)
//Read eval file
>>> eval_resultDF = dt_pipeline.fit(evalDF).transform(evalDF)
>>> eval_resultDF.columns
['Text', 'Label', 'indexedLabel', 'indexedText', 'features', 'rawPrediction', 'probability', 'prediction', 'predictedLabel']
>>> eval_resultDF.select("Text","Label","indexedLabel","prediction",
"predictedLabel").show()
+----+-----+------------+----------+--------------+
|Text|Label|indexedLabel|prediction|predictedLabel|
+----+-----+------------+----------+--------------+
| A| 1| 1.0| 1.0| 1|
| A| 1| 1.0| 1.0| 1|
| A| 2| 0.0| 1.0| 1|
| B| 2| 0.0| 0.0| 2|
| C| 3| 2.0| 2.0| 3|
+----+-----+------------+----------+--------------+
>>>
Accompanying data files:
simple_file1.csv Text,Label
A,1
B,2
C,3
A,1
B,2simple_file2.csv Text,Label
A,1
A,1
A,2
B,2
C,3
集成方法
顾名思义,集成方法通过使用多个学习算法来获得在预测准确性方面更精确的模型。通常,这些技术需要更多的计算能力,并使得模型更加复杂,进而增加了解释的难度。我们来讨论 Spark 上可用的各种集成技术。
随机森林
随机森林是决策树的一种集成技术。在介绍随机森林之前,我们先来看看它是如何发展的。我们知道,决策树通常存在较高的方差问题,容易导致过拟合。为了解决这个问题,引入了一个叫做 袋装(也叫做自助聚合)的概念。对于决策树来说,方法是从数据集中获取多个训练集(自助训练集),用这些训练集分别构建决策树,然后对回归树进行平均。对于分类树,我们可以取所有树的多数投票或最常见的类别。这些树生长得很深,且没有任何修剪。虽然每棵树可能会有较高的方差,但这显著降低了方差。
使用传统的袋装方法时,存在一个问题,即对于大多数自助法训练集,强预测变量位于顶部分裂位置,几乎使得袋装树看起来相似。这意味着预测结果也相似,如果你对它们进行平均,方差并没有达到预期的减少效果。为了解决这个问题,需要一种技术,它采用与袋装树类似的方法,但消除了树之间的相关性,从而形成了随机森林。
在这种方法中,你构建自助法训练样本来创建决策树,但唯一的区别是每次进行分裂时,会从总共的 K 个预测变量中随机选择 P 个预测变量。这就是随机森林向这种方法注入随机性的方式。作为经验法则,我们可以将 P 设置为 Q 的平方根。
和袋装方法一样,在这种方法中,如果目标是回归,则对预测结果进行平均,如果目标是分类,则进行多数投票。Spark 提供了一些调优参数来调整该模型,具体如下:
-
numTrees
:你可以指定在随机森林中考虑的树的数量。如果树的数量较多,则预测的方差较小,但所需的时间会更长。 -
maxDepth
:你可以指定每棵树的最大深度。增加深度可以提高树的预测准确性。尽管它们容易过拟合单棵树,但由于我们最终会对结果进行平均,因此整体输出仍然不错,从而减少了方差。 -
subsamplingRate
:此参数主要用于加速训练。它用于设置自助法训练样本的大小。值小于 1.0 会加速性能。 -
featureSubsetStrategy
:此参数也有助于加速执行。它用于设置每个节点用于分裂的特征数。需要谨慎设置此值,因为过低或过高的值可能会影响模型的准确性。
随机森林的优点
-
它们的运行速度较快,因为执行过程是并行的。
-
它们不易过拟合。
-
它们易于调优。
-
与树或袋装树相比,预测准确性更高。
-
即使预测变量是类别特征和连续特征的混合,它们也能很好地工作,并且不需要缩放。
梯度提升树
与随机森林类似,梯度提升树 (GBTs)也是一种树的集成方法。它们既可以应用于分类问题,也可以应用于回归问题。与袋装树或随机森林不同,后者是基于独立数据集并行构建的树,彼此独立,GBT 是按顺序构建的。每棵树都是基于之前已构建的树的结果来生成的。需要注意的是,GBT 不适用于自助法样本。
在每次迭代中,GBT 会使用当前的集成模型来预测训练实例的标签,并将其与真实标签进行比较,估算误差。预测精度较差的训练实例会被重新标记,以便决策树在下一次迭代中根据上一次的误差率进行修正。
找到误差率并重新标记实例的机制是基于损失函数的。GBT 的设计旨在在每次迭代中减少这个损失函数。Spark 支持以下类型的损失函数:
-
对数损失:这用于分类问题。
-
平方误差(L2 损失):这用于回归问题,并且是默认设置。它是所有观察值的实际输出与预测输出之间平方差的总和。对于这种损失函数,应该对异常值进行良好的处理。
-
绝对误差(L1 损失):这也用于回归问题。它是所有观察值的实际输出与预测输出之间绝对差的总和。与平方误差相比,它对异常值更为稳健。
Spark 提供了一些调参参数来调整此模型,具体如下:
-
loss
:你可以传递一个损失函数,如前节所讨论的,具体取决于你处理的数据集以及你是要进行分类还是回归。 -
numIterations
:每次迭代只产生一棵树!如果你设置得很高,那么执行所需的时间也会很长,因为操作将是顺序进行的,这也可能导致过拟合。为了更好的性能和准确性,应该谨慎设置。 -
learningRate
:这其实并不是一个调参参数。如果算法的行为不稳定,降低学习率可以帮助稳定模型。 -
algo
:分类或回归,根据你需要的类型来设置。
GBT 可能会因为树的数量过多而导致过拟合,因此 Spark 提供了runWithValidation
方法来防止过拟合。
提示
截至目前,Spark 上的 GBT 尚不支持多类分类。
让我们通过一个例子来说明 GBT 的实际应用。这个示例数据集包含了二十名学生的平均分数和出勤情况。数据中还包含了通过或未通过的结果,这些结果遵循一组标准。然而,几个学生(ID 为 1009 和 1020)尽管未符合标准,但却被“授予”了通过状态。现在我们的任务是检查模型是否会把这两名学生排除在外。
通过标准如下:
-
分数应该至少为 40,出勤率应该至少为“足够”。
-
如果分数在 40 到 60 之间,那么出勤率应该是“完整”才能通过。
以下示例还强调了在多个模型中重用管道阶段。因此,我们首先构建一个决策树分类器,然后构建 GBT。我们构建了两个共享阶段的不同管道。
输入:
// Marks < 40 = Fail
// Attendence == Poor => Fail
// Marks >40 and attendence Full => Pass
// Marks > 60 and attendence Enough or Full => Pass
// Two exceptions were studentId 1009 and 1020 who were granted Pass
//This example also emphasizes the reuse of pipeline stages
// Initially the code trains a DecisionTreeClassifier
// Then, same stages are reused to train a GBT classifier
Scala:
scala> import org.apache.spark.ml.feature._
scala> import org.apache.spark.ml.Pipeline
scala> import org.apache.spark.ml.classification.{DecisionTreeClassifier,
DecisionTreeClassificationModel}
scala> case class StResult(StudentId:String, Avg_Marks:Double,
Attendance:String, Result:String)
scala> val file_path = "../work/StudentsPassFail.csv"
scala> val source_ds = spark.read.options(Map("header"->"true",
"inferSchema"->"true")).csv(file_path).as[StResult]
source_ds: org.apache.spark.sql.Dataset[StResult] = [StudentId: int, Avg_Marks:
double ... 2 more fields]
scala>
//Examine source data
scala> source_ds.show(4)
+---------+---------+----------+------+
|StudentId|Avg_Marks|Attendance|Result|
+---------+---------+----------+------+
| 1001| 48.0| Full| Pass|
| 1002| 21.0| Enough| Fail|
| 1003| 24.0| Enough| Fail|
| 1004| 4.0| Poor| Fail|
+---------+---------+----------+------+
scala>
//Define preparation pipeline
scala> val marks_bkt = new Bucketizer().setInputCol("Avg_Marks").
setOutputCol("Mark_bins").setSplits(Array(0,40.0,60.0,100.0))
marks_bkt: org.apache.spark.ml.feature.Bucketizer = bucketizer_5299d2fbd1b2
scala> val att_idx = new StringIndexer().setInputCol("Attendance").
setOutputCol("Att_idx")
att_idx: org.apache.spark.ml.feature.StringIndexer = strIdx_2db54ba5200a
scala> val label_idx = new StringIndexer().setInputCol("Result").
setOutputCol("Label")
label_idx: org.apache.spark.ml.feature.StringIndexer = strIdx_20f4316d6232
scala>
//Create labels list to decode predictions
scala> val resultLabels = label_idx.fit(source_ds).labels
resultLabels: Array[String] = Array(Fail, Pass)
scala> val va = new VectorAssembler().setInputCols(Array("Mark_bins","Att_idx")).
setOutputCol("features")
va: org.apache.spark.ml.feature.VectorAssembler = vecAssembler_5dc2dbbef48c
scala> val dt = new DecisionTreeClassifier().setLabelCol("Label").
setFeaturesCol("features")
dt: org.apache.spark.ml.classification.DecisionTreeClassifier = dtc_e8343ae1a9eb
scala> val lc = new IndexToString().setInputCol("prediction").
setOutputCol("predictedLabel").setLabels(resultLabels)
lc: org.apache.spark.ml.feature.IndexToString = idxToStr_90b6693d4313
scala>
//Define pipeline
scala>val dt_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,dt,lc))
dt_pipeline: org.apache.spark.ml.Pipeline = pipeline_95876bb6c969
scala> val dtModel = dt_pipeline.fit(source_ds)
dtModel: org.apache.spark.ml.PipelineModel = pipeline_95876bb6c969
scala> val resultDF = dtModel.transform(source_ds)
resultDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ...
10 more fields]
scala> resultDF.filter("Label != prediction").select("StudentId","Label","prediction","Result","predictedLabel").show()
+---------+-----+----------+------+--------------+
|StudentId|Label|prediction|Result|predictedLabel|
+---------+-----+----------+------+--------------+\
| 1009| 1.0| 0.0| Pass| Fail|
| 1020| 1.0| 0.0| Pass| Fail|
+---------+-----+----------+------+--------------+
//Note that the difference is in the student ids that were granted pass
//Same example using Gradient boosted tree classifier, reusing the pipeline stages
scala> import org.apache.spark.ml.classification.GBTClassifier
import org.apache.spark.ml.classification.GBTClassifier
scala> val gbt = new GBTClassifier().setLabelCol("Label").
setFeaturesCol("features").setMaxIter(10)
gbt: org.apache.spark.ml.classification.GBTClassifier = gbtc_cb55ae2174a1
scala> val gbt_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,gbt,lc))
gbt_pipeline: org.apache.spark.ml.Pipeline = pipeline_dfd42cd89403
scala> val gbtResultDF = gbt_pipeline.fit(source_ds).transform(source_ds)
gbtResultDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ... 8 more fields]
scala> gbtResultDF.filter("Label !=
prediction").select("StudentId","Label","Result","prediction","predictedLabel").show()
+---------+-----+------+----------+--------------+
|StudentId|Label|Result|prediction|predictedLabel|
+---------+-----+------+----------+--------------+
| 1009| 1.0| Pass| 0.0| Fail|
| 1020| 1.0| Pass| 0.0| Fail|
+---------+-----+------+----------+--------------+
Python:
>>> from pyspark.ml.pipeline import Pipeline
>>> from pyspark.ml.feature import Bucketizer, StringIndexer, VectorAssembler, IndexToString
>>> from pyspark.ml.classification import DecisionTreeClassifier,
DecisionTreeClassificationModel
>>>
//Get source file
>>> file_path = "../work/StudentsPassFail.csv"
>>> source_df = spark.read.csv(file_path,header=True,inferSchema=True)
>>>
//Examine source data
>>> source_df.show(4)
+---------+---------+----------+------+
|StudentId|Avg_Marks|Attendance|Result|
+---------+---------+----------+------+
| 1001| 48.0| Full| Pass|
| 1002| 21.0| Enough| Fail|
| 1003| 24.0| Enough| Fail|
| 1004| 4.0| Poor| Fail|
+---------+---------+----------+------+
//Define preparation pipeline
>>> marks_bkt = Bucketizer(inputCol="Avg_Marks",
outputCol="Mark_bins", splits=[0,40.0,60.0,100.0])
>>> att_idx = StringIndexer(inputCol = "Attendance",
outputCol="Att_idx")
>>> label_idx = StringIndexer(inputCol="Result",
outputCol="Label")
>>>
//Create labels list to decode predictions
>>> resultLabels = label_idx.fit(source_df).labels
>>> resultLabels
[u'Fail', u'Pass']
>>>
>>> va = VectorAssembler(inputCols=["Mark_bins","Att_idx"],
outputCol="features")
>>> dt = DecisionTreeClassifier(labelCol="Label", featuresCol="features")
>>> lc = IndexToString(inputCol="prediction",outputCol="predictedLabel",
labels=resultLabels)
>>> dt_pipeline = Pipeline(stages=[marks_bkt, att_idx, label_idx,va,dt,lc])
>>> dtModel = dt_pipeline.fit(source_df)
>>> resultDF = dtModel.transform(source_df)
>>>
//Look for obervatiuons where prediction did not match
>>> resultDF.filter("Label != prediction").select(
"StudentId","Label","prediction","Result","predictedLabel").show()
+---------+-----+----------+------+--------------+
|StudentId|Label|prediction|Result|predictedLabel|
+---------+-----+----------+------+--------------+
| 1009| 1.0| 0.0| Pass| Fail|
| 1020| 1.0| 0.0| Pass| Fail|
+---------+-----+----------+------+--------------+
//Note that the difference is in the student ids that were granted pass
>>>
//Same example using Gradient boosted tree classifier, reusing the pipeline
stages
>>> from pyspark.ml.classification import GBTClassifier
>>> gbt = GBTClassifier(labelCol="Label", featuresCol="features",maxIter=10)
>>> gbt_pipeline = Pipeline(stages=[marks_bkt,att_idx,label_idx,va,gbt,lc])
>>> gbtResultDF = gbt_pipeline.fit(source_df).transform(source_df)
>>> gbtResultDF.columns
['StudentId', 'Avg_Marks', 'Attendance', 'Result', 'Mark_bins', 'Att_idx',
'Label', 'features', 'prediction', 'predictedLabel']
>>> gbtResultDF.filter("Label !=
prediction").select("StudentId","Label","Result","prediction","predictedLabel").show()
+---------+-----+------+----------+--------------+
|StudentId|Label|Result|prediction|predictedLabel|
+---------+-----+------+----------+--------------+
| 1009| 1.0| Pass| 0.0| Fail|
| 1020| 1.0| Pass| 0.0| Fail|
+---------+-----+------+----------+--------------+
多层感知机分类器
多层感知器分类器(MLPC)是一个前馈人工神经网络,具有多个层次的节点,节点之间以有向方式相互连接。它使用一种名为反向传播的有监督学习技术来训练网络。
中间层的节点使用 sigmoid 函数将输出限制在 0 和 1 之间,输出层的节点使用softmax
函数,它是 sigmoid 函数的广义版本。
Scala:
scala> import org.apache.spark.ml.classification.MultilayerPerceptronClassifier
import org.apache.spark.ml.classification.MultilayerPerceptronClassifier
scala> import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
import org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator
scala> import org.apache.spark.mllib.util.MLUtils
import org.apache.spark.mllib.util.MLUtils
// Load training data
scala> val data = MLUtils.loadLibSVMFile(sc,
"data/mllib/sample_multiclass_classification_data.txt").toDF()
data: org.apache.spark.sql.DataFrame = [label: double, features: vector]
//Convert mllib vectors to ml Vectors for spark 2.0+. Retain data for previous versions
scala> val data2 = MLUtils.convertVectorColumnsToML(data)
data2: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
// Split the data into train and test
scala> val splits = data2.randomSplit(Array(0.6, 0.4), seed = 1234L)
splits: Array[org.apache.spark.sql.Dataset[org.apache.spark.sql.Row]] = Array([label: double, features: vector], [label: double, features: vector])
scala> val train = splits(0)
train: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
scala> val test = splits(1)
test: org.apache.spark.sql.Dataset[org.apache.spark.sql.Row] = [label: double, features: vector]
// specify layers for the neural network:
// input layer of size 4 (features), two intermediate of size 5 and 4 and output of size 3 (classes)
scala> val layers = ArrayInt
layers: Array[Int] = Array(4, 5, 4, 3)
// create the trainer and set its parameters
scala> val trainer = new MultilayerPerceptronClassifier().
setLayers(layers).setBlockSize(128).
setSeed(1234L).setMaxIter(100)
trainer: org.apache.spark.ml.classification.MultilayerPerceptronClassifier = mlpc_edfa49fbae3c
// train the model
scala> val model = trainer.fit(train)
model: org.apache.spark.ml.classification.MultilayerPerceptronClassificationModel = mlpc_edfa49fbae3c
// compute accuracy on the test set
scala> val result = model.transform(test)
result: org.apache.spark.sql.DataFrame = [label: double, features: vector ... 1 more field]
scala> val predictionAndLabels = result.select("prediction", "label")
predictionAndLabels: org.apache.spark.sql.DataFrame = [prediction: double, label: double]
scala> val evaluator = new MulticlassClassificationEvaluator().setMetricName("accuracy")
evaluator: org.apache.spark.ml.evaluation.MulticlassClassificationEvaluator = mcEval_a4f43d85f261
scala> println("Accuracy:" + evaluator.evaluate(predictionAndLabels))
Accuracy:0.9444444444444444
Python: >>> from pyspark.ml.classification import MultilayerPerceptronClassifier
>>> from pyspark.ml.evaluation import MulticlassClassificationEvaluator
>>> from pyspark.mllib.util import MLUtils
>>>
//Load training data
>>> data = spark.read.format("libsvm").load( "data/mllib/sample_multiclass_classification_data.txt")
//Convert mllib vectors to ml Vectors for spark 2.0+. Retain data for previous versions
>>> data2 = MLUtils.convertVectorColumnsToML(data)
>>>
// Split the data into train and test
>>> splits = data2.randomSplit([0.6, 0.4], seed = 1234L)
>>> train, test = splits[0], splits[1]
>>>
// specify layers for the neural network:
// input layer of size 4 (features), two intermediate of size 5 and 4 and output of size 3 (classes)
>>> layers = [4,5,4,3]
// create the trainer and set its parameters
>>> trainer = MultilayerPerceptronClassifier(layers=layers, blockSize=128,
seed=1234L, maxIter=100)
// train the model
>>> model = trainer.fit(train)
>>>
// compute accuracy on the test set
>>> result = model.transform(test)
>>> predictionAndLabels = result.select("prediction", "label")
>>> evaluator = MulticlassClassificationEvaluator().setMetricName("accuracy")
>>> print "Accuracy:",evaluator.evaluate(predictionAndLabels)
Accuracy: 0.901960784314
>>>
聚类技术
聚类是一种无监督学习技术,其中没有响应变量来监督模型。其思想是将具有一定相似度的数据点进行聚类。除了探索性数据分析外,它还作为有监督管道的一部分,分类器或回归器可以在不同的聚类上构建。聚类技术有很多种可用的。让我们来看看一些 Spark 支持的重要方法。
K-means 聚类
K-means 是最常见的聚类技术之一。k-means 问题是找到聚类中心,以最小化类内方差,即每个数据点与其聚类中心(与其最近的中心)之间的平方距离之和。你必须提前指定数据集中所需的聚类数目。
由于它使用欧几里得距离度量来找到数据点之间的差异,因此在使用 k-means 之前,需要将特征缩放到一个可比单位。欧几里得距离可以通过图形的方式更好地解释如下:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_051.jpg
给定一组数据点(x1,x2,…,xn),这些数据点的维度与变量的数量相同,k-means 聚类的目标是将这 n 个观测值划分为 k 个(小于n)集合,记作S = {S1,S2,…,Sk},以最小化类内平方和(WCSS)。换句话说,它的目标是找到:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_06_052.jpg
Spark 要求传递以下参数给这个算法:
-
k
:这是所需的聚类数目。 -
maxIterations
:这是要执行的最大迭代次数。 -
initializationMode
:该参数指定随机初始化或通过 k-means 初始化||。 -
runs
:这是运行 k-means 算法的次数(k-means 不保证找到全局最优解,当对给定数据集运行多次时,算法会返回最好的聚类结果)。 -
initializationSteps
:这个参数确定 k-means||算法的步骤数。 -
epsilon
:这个参数确定了我们认为 k-means 已收敛的距离阈值。 -
initialModel
:这是一个可选的聚类中心集合,用于初始化。如果提供了此参数,则只执行一次运行。
k-means 的缺点
-
它仅适用于数值特征
-
在实现算法之前,需要进行缩放
-
它容易受到局部最优解的影响(解决方法是 k-means++)
示例
让我们在相同的学生数据上运行 k-means 聚类。
scala> import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
import org.apache.spark.ml.clustering.{KMeans, KMeansModel}
scala> import org.apache.spark.ml.linalg.Vectors
import org.apache.spark.ml.linalg.Vectors
scala>
//Define pipeline for kmeans. Reuse the previous stages in ENSEMBLES
scala> val km = new KMeans()
km: org.apache.spark.ml.clustering.KMeans = kmeans_b34da02bd7c8
scala> val kmeans_pipeline = new
Pipeline().setStages(Array(marks_bkt,att_idx,label_idx,va,km,lc))
kmeans_pipeline: org.apache.spark.ml.Pipeline = pipeline_0cd64aa93a88
//Train and transform
scala> val kmeansDF = kmeans_pipeline.fit(source_ds).transform(source_ds)
kmeansDF: org.apache.spark.sql.DataFrame = [StudentId: int, Avg_Marks: double ... 8 more fields]
//Examine results
scala> kmeansDF.filter("Label != prediction").count()
res17: Long = 13
Python:
>>> from pyspark.ml.clustering import KMeans, KMeansModel
>>> from pyspark.ml.linalg import Vectors
>>>
//Define pipeline for kmeans. Reuse the previous stages in ENSEMBLES
>>> km = KMeans()
>>> kmeans_pipeline = Pipeline(stages = [marks_bkt, att_idx, label_idx,va,km,lc])
//Train and transform
>>> kmeansDF = kmeans_pipeline.fit(source_df).transform(source_df)
>>> kmeansDF.columns
['StudentId', 'Avg_Marks', 'Attendance', 'Result', 'Mark_bins', 'Att_idx', 'Label', 'features', 'prediction', 'predictedLabel']
>>> kmeansDF.filter("Label != prediction").count()
4
摘要
在本章中,我们解释了各种机器学习算法,如何在 MLlib 库中实现它们,以及如何使用 pipeline API 进行简化的执行。概念通过 Python 和 Scala 代码示例进行了讲解,方便参考。
在下一章中,我们将讨论 Spark 如何支持 R 编程语言,重点介绍一些算法及其执行方式,类似于我们在本章中讲解的内容。
参考文献
MLlib 中支持的算法:
Spark ML 编程指南:
来自 2015 年 6 月峰会幻灯片的高级数据科学文档:
-
databricks.com/blog/2015/07/29/new-features-in-machine-learning-pipelines-in-spark-1-4.html
-
databricks.com/blog/2015/06/02/statistical-and-mathematical-functions-with-dataframes-in-spark.html
-
databricks.com/blog/2015/01/07/ml-pipelines-a-new-high-level-api-for-mllib.html
第七章:使用 SparkR 扩展 Spark
统计学家和数据科学家一直在使用 R 来解决几乎所有领域的挑战性问题,从生物信息学到选举活动。他们偏爱 R 是因为它强大的可视化功能、强大的社区以及丰富的统计学和机器学习包生态系统。全球许多学术机构都使用 R 语言教授数据科学和统计学。
R 最初是在 1990 年代中期,由统计学家为统计学家创建的,目的是提供一种更好、更用户友好的方式来进行数据分析。R 最初用于学术和研究领域。随着企业越来越意识到数据科学在业务增长中的作用,使用 R 进行数据分析的企业数据分析师数量也开始增长。经过二十年的发展,R 语言的用户基础已被认为超过两百万。
这一切成功背后的驱动力之一是 R 的设计目的是让分析师的工作更轻松,而不是让计算机的工作更轻松。R 本质上是单线程的,它只能处理完全适合单台机器内存的数据集。然而,现如今,R 用户正在处理越来越大的数据集。将现代分布式处理能力无缝集成到这个已经建立的 R 语言下,使数据科学家能够兼得二者的优势。这样,他们可以跟上日益增长的业务需求,同时继续享受自己喜爱的 R 语言的灵活性。
本章介绍了 SparkR,这是一个为 R 程序员设计的 Spark API,使他们能够利用 Spark 的强大功能,而无需学习新的语言。由于假设读者已具备 R、R Studio 及数据分析技能,本章不再介绍 R 基础知识。提供了一个非常简短的 Spark 计算引擎概述作为快速回顾。读者应阅读本书的前三章,以深入理解 Spark 编程模型和 DataFrame。这个知识非常重要,因为开发人员需要理解他编写的代码中哪些部分是在本地 R 环境中执行的,哪些部分是由 Spark 计算引擎处理的。本章涉及的主题如下:
-
SparkR 基础知识
-
R 与 Spark 的优势及其局限性
-
使用 SparkR 编程
-
SparkR DataFrame
-
机器学习
SparkR 基础知识
R 是一种用于统计计算和图形的语言和环境。SparkR 是一个 R 包,它提供了一个轻量级的前端,使得可以从 R 访问 Apache Spark。SparkR 的目标是将 R 环境提供的灵活性和易用性与 Spark 计算引擎提供的可扩展性和容错性相结合。在讨论 SparkR 如何实现其目标之前,我们先回顾一下 Spark 的架构。
Apache Spark 是一个快速的、通用的、容错的框架,用于对大规模分布式数据集进行交互式和迭代计算。它支持多种数据源和存储层。它提供了统一的数据访问,能够结合不同的数据格式、流数据,并使用高级可组合操作符定义复杂的操作。你可以使用 Scala、Python 或 R shell(或者 Java,无需 shell)交互式地开发应用。你可以在家用桌面上部署它,或者将它部署在大规模集群中,处理数 PB 的数据。
注意
SparkR 起源于 AMPLab (amplab.cs.berkeley.edu/
),旨在探索将 R 的可用性与 Spark 的可扩展性相结合的不同技术。它作为 Apache Spark 1.4 的一个 alpha 组件发布,该版本于 2015 年 6 月发布。Spark 1.5 版本提高了 R 的可用性,并引入了带有广义线性模型(GLMs)的 MLlib 机器学习包。2016 年 1 月发布的 Spark 1.6 版本增加了一些新特性,如模型摘要和特征交互。2016 年 7 月发布的 Spark 2.0 版本带来了多个重要特性,如 UDF、改进的模型覆盖、DataFrames 窗口函数 API 等等。
从 R 环境访问 SparkR
你可以从 R shell 或 R Studio 启动 SparkR。SparkR 的入口点是 SparkSession 对象,它代表了与 Spark 集群的连接。R 所运行的节点成为驱动程序。由 R 程序创建的任何对象都驻留在此驱动程序上。通过 SparkSession 创建的任何对象都在集群中的工作节点上创建。下图展示了 R 与在集群上运行的 Spark 交互的运行时视图。请注意,R 解释器存在于集群中的每个工作节点上。下图没有显示集群管理器,也没有显示存储层。你可以使用任何集群管理器(例如 Yarn 或 Mesos)和任何存储选项,如 HDFS、Cassandra 或 Amazon S3:
https://github.com/OpenDocCN/freelearn-ds-pt5-zh/raw/master/docs/spk-ds/img/image_07_001.jpg
来源:http://www.slideshare.net/Hadoop_Summit/w-145p210-avenkataraman.
SparkSession 对象通过传递应用名称、内存、核心数和要连接的集群管理器等信息来创建。所有与 Spark 引擎的交互都通过这个 SparkSession 对象发起。如果使用 SparkR shell,则会自动为你创建一个 SparkSession 对象;否则,你需要显式创建它。这个对象取代了 Spark 1.x 版本中的 SparkContext 和 SQLContext 对象。为了向后兼容,这些对象仍然存在。即使前面的图展示的是 SparkContext,你也应当将其视为 Spark 2.0 后的 SparkSession。
现在我们已经理解了如何从 R 环境访问 Spark,接下来让我们来看看 Spark 引擎提供的核心数据抽象。
RDD 和 DataFrame
Spark 引擎的核心是其主要的数据抽象,称为弹性分布式数据集(Resilient Distributed Dataset,简称 RDD)。一个 RDD 由一个或多个数据源组成,并由用户定义为一系列在一个或多个稳定(具体的)数据源上的转换(也叫血统)。每个 RDD 或 RDD 分区都知道如何使用血统图在失败时重新创建自己,从而提供容错功能。RDD 是不可变的数据结构,这意味着它可以在不需要同步开销的情况下在线程间共享,因此适合并行化。对 RDD 的操作要么是转换,要么是动作。转换是血统中的单独步骤。换句话说,转换是创建 RDD 的操作,因为每个转换都是从一个稳定的数据源获取数据或转换一个不可变的 RDD,进而创建另一个 RDD。转换只是声明;它们在对 RDD 执行动作操作之前不会被评估。动作是利用 RDD 的操作。
Spark 会根据当前的操作优化 RDD 的计算。例如,如果操作是读取第一行,则只计算一个分区,跳过其余部分。当内存不足时,它会自动执行内存计算,并优雅地退化(当内存不足时,溢出到磁盘),并将处理分布到所有核心。你可以缓存一个 RDD,如果它在你的程序逻辑中频繁被访问,从而避免重新计算的开销。
R 语言提供了一种二维数据结构,叫做数据框(DataFrame),使得数据操作变得方便。Apache Spark 有自己的 DataFrame,这些 DataFrame 灵感来自于 R 和 Python(通过 Pandas)中的 DataFrame。Spark 的 DataFrame 是建立在 RDD 数据结构抽象之上的一种专用数据结构。它提供了分布式的 DataFrame 实现,从开发者的角度看,它与 R DataFrame 非常相似,同时还能支持非常大的数据集。Spark 数据集 API 为 DataFrame 增加了结构,而这一结构为底层优化提供了信息。
开始使用
现在我们已经理解了底层数据结构和运行时视图,是时候运行一些命令了。在这一部分,我们假设你已经成功安装了 R 和 Spark,并将其添加到了路径中。我们还假设已经设置了 SPARK_HOME
环境变量。接下来我们看看如何通过 R shell 或 R Studio 访问 SparkR:
> R // Start R shell
> Sys.getenv("SPARK_HOME") //Confirm SPARK_HOME is set
<Your SPARK_HOME path>
> library(SparkR, lib.loc =
c(file.path(Sys.getenv("SPARK_HOME"), "R", "lib")))
Attaching package: 'SparkR'
The following objects are masked from 'package:stats':
cov, filter, lag, na.omit, predict, sd, var, window
The following objects are masked from 'package:base':
as.data.frame, colnames, colnames<-, drop, endsWith, intersect,
rank, rbind, sample, startsWith, subset, summary, transform, union
>
> //Try help(package=SparkR) if you want to more information
//initialize SparkSession object
> sparkR.session()
Java ref type org.apache.spark.sql.SparkSession id 1
>
Alternatively, you may launch sparkR shell which comes with predefined SparkSession.
> bin/sparkR // Start SparkR shell
> // For simplicity sake, no Log messages are shown here
> //Try help(package=SparkR) if you want to more information
>
这就是你在 R 环境中访问 Spark DataFrame 的所有操作。
优势与限制
R 语言长期以来一直是数据科学家的通用语言。其易于理解的数据框架抽象、富有表现力的 API 和充满活力的包生态系统正是分析师所需要的。主要的挑战在于可扩展性。SparkR 通过提供分布式内存中的数据框架,同时保持在 R 生态系统内,弥补了这一缺陷。这样的共生关系使得用户能够获得以下好处:
-
分析师无需学习新语言
-
SparkR 的 API 与 R 的 API 类似
-
你可以通过 R Studio 访问 SparkR,并且可以使用自动补全功能。
-
执行对非常大数据集的交互式、探索性分析不再受到内存限制或长时间等待的困扰。
-
从不同类型的数据源访问数据变得更加容易。大多数之前必须显式操作的任务,现在已经转变为声明式任务。请参阅第四章,统一数据访问,了解更多信息。
-
你可以自由混合使用如 Spark 函数、SQL 和仍未在 Spark 中可用的 R 库等 dplyr。
尽管结合两者优点带来了许多令人兴奋的优势,但这种结合仍然存在一些局限性。这些局限性可能不会影响每个用例,但我们还是需要意识到它们:
-
R 的固有动态特性限制了 Catalyst 优化器能够获取的信息。与静态类型语言(如 Scala)相比,我们可能无法充分利用像谓词下推等优化。
-
SparkR 不支持 Scala API 等其他 API 中已经提供的所有机器学习算法。
总结来说,使用 Spark 进行数据预处理,使用 R 进行分析和可视化似乎是未来的最佳方案。
使用 SparkR 编程
到目前为止,我们已经理解了 SparkR 的运行时模型以及提供容错性和可扩展性的基本数据抽象。我们也了解了如何从 R shell 或 R Studio 访问 Spark API。现在是时候尝试一些基本且熟悉的操作了:
>
> //Open the shell
>
> //Try help(package=SparkR) if you want to more information
>
> df <- createDataFrame(iris) //Create a Spark DataFrame
> df //Check the type. Notice the column renaming using underscore
SparkDataFrame[Sepal_Length:double, Sepal_Width:double, Petal_Length:double, Petal_Width:double, Species:string]
>
> showDF(df,4) //Print the contents of the Spark DataFrame
+------------+-----------+------------+-----------+-------+
|Sepal_Length|Sepal_Width|Petal_Length|Petal_Width|Species|
+------------+-----------+------------+-----------+-------+
| 5.1| 3.5| 1.4| 0.2| setosa|
| 4.9| 3.0| 1.4| 0.2| setosa|
| 4.7| 3.2| 1.3| 0.2| setosa|
| 4.6| 3.1| 1.5| 0.2| setosa|
+------------+-----------+------------+-----------+-------+
>
> head(df,2) //Returns an R data.frame. Default 6 rows
Sepal_Length Sepal_Width Petal_Length Petal_Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
> //You can use take(df,2) to get the same results
//Check the dimensions
> nrow(df) [1] 150 > ncol(df) [1] 5
这些操作看起来与 R DataFrame 函数非常相似,因为 Spark DataFrame 的模型是基于 R DataFrame 和 Python(Pandas)DataFrame 的。但这种相似性可能会造成混淆,如果不小心,你可能会误以为负载会被分配,从而在 R data.frame
上运行计算密集型函数,导致本地计算机崩溃。例如,intersect 函数在两个包中的签名相同。你需要注意对象是否是 SparkDataFrame
(Spark DataFrame)类,还是 data.frame
(R DataFrame)类。你还需要尽量减少本地 R data.frame
对象与 Spark DataFrame 对象之间的反复转换。让我们通过尝试一些例子来感受这种区别:
>
> //Open the SparkR shell
> df <- createDataFrame(iris) //Create a Spark DataFrame
> class(df) [1] "SparkDataFrame" attr(,"package") [1] "SparkR"
> df2 <- head(df,2) //Create an R data frame
> class(df2)
[1] "data.frame"
> //Now try running some R command on both data frames
> unique(df2$Species) //Works fine as expected [1] "setosa" > unique(df$Species) //Should fail Error in unique.default(df$Species) : unique() applies only to vectors > class(df$Species) //Each column is a Spark's Column class [1] "Column" attr(,"package") [1] "SparkR" > class(df2$Species) [1] "character"
函数名称遮蔽
现在我们已经尝试了一些基本操作,接下来让我们稍微跑题一下。我们必须了解当加载的库与基础包或已加载的其他包存在函数名重叠时会发生什么。这有时被称为函数名重叠、函数屏蔽或命名冲突。你可能注意到当加载 SparkR 包时,消息中提到了被屏蔽的对象。这对于任何加载到 R 环境中的包都是常见的,并不仅仅是 SparkR 特有的。如果 R 环境中已经存在一个与正在加载的包中函数同名的函数,那么随后的对该函数的调用将表现出最新加载的包中函数的行为。如果你希望访问以前的函数而不是 SparkR
函数,你需要显式地在该函数名前加上包名,如下所示:
//First try in R environment, without loading sparkR
//Try sampling from a column in an R data.frame
>sample(iris$Sepal.Length,6,FALSE) //Returns any n elements [1] 5.1 4.9 4.7 4.6 5.0 5.4 >sample(head(iris),3,FALSE) //Returns any 3 columns
//Try sampling from an R data.frame
//The Boolean argument is for with_replacement
> sample(head
> head(sample(iris,3,TRUE)) //Returns any 3 columns
Species Species.1 Petal.Width
1 setosa setosa 0.2
2 setosa setosa 0.2
3 setosa setosa 0.2
4 setosa setosa 0.2
5 setosa setosa 0.2
6 setosa setosa 0.4
//Load sparkR, initialize sparkSession and then execute this
> df <- createDataFrame(iris) //Create a Spark DataFrame
> sample_df <- sample(df,TRUE,0.3) //Different signature
> dim(sample_df) //Different behavior [1] 44 5
> //Returned 30% of the original data frame and all columns
> //Try with base prefix
> head(base::sample(iris),3,FALSE) //Call base package's sample
Species Petal.Width Petal.Length
1 setosa 0.2 1.4
2 setosa 0.2 1.4
3 setosa 0.2 1.3
4 setosa 0.2 1.5
5 setosa 0.2 1.4
6 setosa 0.4 1.7
子集数据
对 R 数据框的子集操作非常灵活,SparkR 尝试保留这些操作,并提供相同或类似的等效功能。我们已经在前面的示例中看到了一些操作,但本节将这些操作按顺序呈现:
//Subsetting data examples
> b1 <- createDataFrame(beaver1)
//Get one column
> b1$temp
Column temp //Column class and not a vector
> //Select some columns. You may use positions too
> select(b1, c("day","temp"))
SparkDataFrame[day:double, temp:double]
>//Row subset based on conditions
> head(subset(b1,b1$temp>37,select= c(2,3)))
time temp
1 1730 37.07
2 1740 37.05
3 1940 37.01
4 1950 37.10
5 2000 37.09
6 2010 37.02
> //Multiple conditions with AND and OR
> head(subset(b1, between(b1$temp,c(36.0,37.0)) |
b1$time %in% 900 & b1$activ == 1,c(2:4)),2)
time temp activ
1 840 36.33 0
2 850 36.34 0
提示
在撰写本书时(Apache Spark 2.o 发布版),基于行索引的切片操作尚不可用。你将无法使用 df[n,]
或 df[m:n,]
语法获取特定的行或行范围。
//For example, try on a normal R data.frame
> beaver1[2:4,]
day time temp activ
2 346 850 36.34 0
3 346 900 36.35 0
4 346 910 36.42 0
//Now, try on Spark Data frame
> b1[2:4,] //Throws error
Expressions other than filtering predicates are not supported in the first parameter of extract operator [ or subset() method.
>
列函数
你应该已经注意到在子集数据部分有列函数 between
。这些函数作用于 Column
类。如其名称所示,这些函数一次处理单个列,通常用于子集化 DataFrame。还有其他一些常用的列函数,用于排序、类型转换和格式化等常见操作。除了处理列中的值外,你还可以向 DataFrame 添加列或删除一个或多个列。可以使用负的列下标来省略列,类似于 R。以下示例展示了在子集操作中使用 Column
类函数,随后进行添加和删除列的操作:
> //subset using Column operation using airquality dataset as df
> head(subset(df,isNull(df$Ozone)),2)
Ozone Solar_R Wind Temp Month Day
1 NA NA 14.3 56 5 5
2 NA 194 8.6 69 5 10
>
> //Add column and drop column examples
> b1 <- createDataFrame(beaver1)
//Add new column
> b1$inRetreat <- otherwise(when(b1$activ == 0,"No"),"Yes")
head(b1,2)
day time temp activ inRetreat
1 346 840 36.33 0 No
2 346 850 36.34 0 No
>
//Drop a column.
> b1$day <- NULL
> b1 // Example assumes b1$inRetreat does not exist
SparkDataFrame[time:double, temp:double, activ:double]
> //Drop columns using negative subscripts
> b2 <- b1[,-c(1,4)] > head(b2)
time temp
1 840 36.33
2 850 36.34
3 900 36.35
4 910 36.42
5 920 36.55
6 930 36.69
>
分组数据
可以使用类似 SQL 的 group_by
函数对 DataFrame 数据进行子分组。有多种方式可以执行这样的操作。本节介绍了一个稍微复杂的示例。此外,我们使用了由 magrittr
库提供的 %>%
,即前向管道操作符,它提供了一个链式命令的机制:
> //GroupedData example using iris data as df
> //Open SparkR shell and create df using iris dataset
> groupBy(df,"Species")
GroupedData //Returns GroupedData object
> library(magrittr) //Load the required library
//Get group wise average sepal length
//Report results sorted by species name
>df2 <- df %>% groupBy("Species") %>%
avg("Sepal_Length") %>%
withColumnRenamed("avg(Sepal_Length)","avg_sepal_len") %>%
orderBy ("Species")
//Format the computed double column
df2$avg_sepal_len <- format_number(df2$avg_sepal_len,2)
showDF(df2)
+----------+-------------+
| Species|avg_sepal_len|
+----------+-------------+
| setosa| 5.01|
|versicolor| 5.94|
| virginica| 6.59|
+----------+-------------+
你可以继续使用前向管道操作符来链式调用操作。仔细观察代码中的列重命名部分。列名参数是先前操作的输出,这些操作在此操作开始之前已经完成,因此你可以放心假设 avg(sepal_len)
列已经存在。format_number
按预期工作,这又是一个便捷的 Column
操作。
下一节有另一个类似的示例,使用 GroupedData
及其等效的 dplyr
实现。
SparkR DataFrame
在本节中,我们尝试了一些有用的、常用的操作。首先,我们尝试了传统的 R/dplyr
操作,然后展示了使用 SparkR API 的等效操作:
> //Open the R shell and NOT SparkR shell
> library(dplyr,warn.conflicts=FALSE) //Load dplyr first
//Perform a common, useful operation
> iris %>%
+ group_by(Species) %>% + summarise(avg_length = mean(Sepal.Length),
+ avg_width = mean(Sepal.Width)) %>% + arrange(desc(avg_length))
Source: local data frame [3 x 3]
Species avg_length avg_width
(fctr) (dbl) (dbl)
1 virginica 6.588 2.974
2 versicolor 5.936 2.770
3 setosa 5.006 3.428
//Remove from R environment
> detach("package:dplyr",unload=TRUE)
这个操作与 SQL 中的 GROUP BY
很相似,后面跟着排序。它在 SparkR 中的等效实现也与 dplyr
示例非常相似。请看下面的示例。注意方法名称,并将它们的位置与前面的 dplyr
示例进行比较:
> //Open SparkR shell and create df using iris dataset
> collect(arrange(summarize(groupBy(df,df$Species), + avg_sepal_length = avg(df$Sepal_Length), + avg_sepal_width = avg(df$Sepal_Width)), + "avg_sepal_length", decreasing = TRUE))
Species avg_sepal_length avg_sepal_width
1 setosa 5.006 3.428
2 versicolor 5.936 2.770
3 virginica 6.588 2.974
SparkR 旨在尽可能接近现有的 R API。因此,方法名称与 dplyr
方法非常相似。例如,查看这个示例,其中使用了 groupBy
,而 dplyr
使用的是 group_by
。SparkR 支持冗余的函数名称。例如,它同时提供 group_by
和 groupBy
,以适应来自不同编程环境的开发者。dplyr
和 SparkR 中的方法名称再次与 SQL 关键字 GROUP BY
非常接近。但这些方法调用的顺序并不相同。示例还展示了一个额外的步骤:使用 collect
将 Spark DataFrame 转换为 R 的 data.frame
。这些方法的顺序是由内而外的,意味着首先对数据进行分组,然后进行汇总,最后进行排序。这是可以理解的,因为在 SparkR 中,最内层方法中创建的 DataFrame 成为其直接前驱方法的参数,以此类推。
SQL 操作
如果你对前面的示例中的语法不太满意,你可以尝试如下编写 SQL 字符串,它和前面的操作完全相同,但使用了老式的 SQL 语法:
> //Register the Spark DataFrame as a table/View
> createOrReplaceTempView(df,"iris_vw")
//Look at the table structure and some rows
> collect(sql(sqlContext, "SELECT * FROM iris_tbl LIMIT 5"))
Sepal_Length Sepal_Width Petal_Length Petal_Width Species
1 5.1 3.5 1.4 0.2 setosa
2 4.9 3.0 1.4 0.2 setosa
3 4.7 3.2 1.3 0.2 setosa
4 4.6 3.1 1.5 0.2 setosa
5 5.0 3.6 1.4 0.2 setosa
> //Try out the above example using SQL syntax
> collect(sql(sqlContext, "SELECT Species, avg(Sepal_Length) avg_sepal_length, avg(Sepal_Width) avg_sepal_width FROM iris_tbl GROUP BY Species ORDER BY avg_sepal_length desc"))
Species avg_sepal_length avg_sepal_width
1 virginica 6.588 2.974
2 versicolor 5.936 2.770
3 setosa 5.006 3.428
前面的示例看起来是实现当前操作最自然的方式,特别是如果你习惯从 RDBMS 表中提取数据的话。但我们是怎么做的呢?第一条语句告诉 Spark 注册一个临时表(或者,顾名思义,它是一个视图,是表的逻辑抽象)。这与数据库表并不完全相同。它是临时的,因为它会在 SparkSession 对象被销毁时被销毁。你并没有显式地将数据写入任何 RDBMS 数据存储(如果要这样做,你必须使用 SaveAsTable
)。但是,一旦你将 Spark DataFrame 注册为临时表,你就可以自由使用 SQL 语法对该 DataFrame 进行操作。接下来的语句是一个基本的 SELECT
语句,显示了列名,后面跟着 LIMIT
关键字指定的五行数据。接下来的 SQL 语句创建了一个包含 Species 列的 Spark DataFrame,后面跟着两个平均值列,并按照平均花萼长度排序。这个 DataFrame 又通过 collect
被收集为 R 的 data.frame
。最终结果与前面的示例完全相同。你可以自由选择使用任何一种语法。欲了解更多信息和示例,请查看第四章,统一数据访问。
集合操作
SparkR 中提供了常用的集合操作,例如union
、intersection
和minus
,这些操作开箱即用。事实上,当加载 SparkR 时,警告信息显示intersect
是被屏蔽的函数之一。以下示例基于beaver
数据集:
> //Create b1 and b2 DataFrames using beaver1 and beaver2 datasets
> b1 <- createDataFrame(beaver1)
> b2 <- createDataFrame(beaver2)
//Get individual and total counts
> > c(nrow(b1), nrow(b2), nrow(b1) + nrow(b2))
[1] 114 100 214
//Try adding both data frames using union operation
> nrow(unionAll(b1,b2))
[1] 214 //Sum of two datsets
> //intersect example
//Remove the first column (day) and find intersection
showDF(intersect(b1[,-c(1)],b2[,-c(1)]))
+------+-----+-----+
| time| temp|activ|
+------+-----+-----+
|1100.0|36.89| 0.0|
+------+-----+-----+
> //except (minus or A-B) is covered in machine learning examples
合并 DataFrames
下一个示例演示了如何使用merge
命令连接两个 DataFrame。示例的第一部分展示了 R 实现,接下来的部分展示了 SparkR 实现:
> //Example illustrating data frames merging using R (Not SparkR)
> //Create two data frames with a matching column
//Products df with two rows and two columns
> products_df <- data.frame(rbind(c(101,"Product 1"),
c(102,"Product 2")))
> names(products_df) <- c("Prod_Id","Product")
> products_df
Prod_Id Product
1 101 Product 1
2 102 Product 2
//Sales df with sales for each product and month 24x3
> sales_df <- data.frame(cbind(rep(101:102,each=12), month.abb,
sample(1:10,24,replace=T)*10))
> names(sales_df) <- c("Prod_Id","Month","Sales")
//Look at first 2 and last 2 rows in the sales_df
> sales_df[c(1,2,23,24),]
Prod_Id Month Sales
1 101 Jan 60
2 101 Feb 40
23 102 Nov 20
24 102 Dec 100
> //merge the data frames and examine the data
> total_df <- merge(products_df,sales_df)
//Look at the column names
> colnames(total_df)
> [1] "Prod_Id" "Product" "Month" "Sales"
//Look at first 2 and last 2 rows in the total_df
> total_df[c(1,2,23,24),]
Prod_Id Product Month Sales
1 101 Product 1 Jan 10
2 101 Product 1 Feb 20
23 102 Product 2 Nov 60
24 102 Product 2 Dec 10
上述代码完全依赖于 R 的基础包。为了简便起见,我们在两个 DataFrame 中使用了相同的连接列名称。接下来的代码演示了使用 SparkR 的相同示例。它看起来与前面的代码相似,因此需要仔细观察其中的差异:
> //Example illustrating data frames merging using SparkR
> //Create an R data frame first and then pass it on to Spark
> //Watch out the base prefix for masked rbind function
> products_df <- createDataFrame(data.frame(
base::rbind(c(101,"Product 1"),
c(102,"Product 2"))))
> names(products_df) <- c("Prod_Id","Product")
>showDF(products_df)
+-------+---------+
|Prod_Id| Product|
+-------+---------+
| 101|Product 1|
| 102|Product 2|
+-------+---------+
> //Create Sales data frame
> //Notice the as.data.frame similar to other R functions
> //No cbind in SparkR so no need for base:: prefix
> sales_df <- as.DataFrame(data.frame(cbind(
"Prod_Id" = rep(101:102,each=12),
"Month" = month.abb,
"Sales" = base::sample(1:10,24,replace=T)*10)))
> //Check sales dataframe dimensions and some random rows
> dim(sales_df)
[1] 24 3
> collect(sample(sales_df,FALSE,0.20))
Prod_Id Month Sales
1 101 Sep 50
2 101 Nov 80
3 102 Jan 90
4 102 Jul 100
5 102 Nov 20
6 102 Dec 50
> //Merge the data frames. The following merge is from SparkR library
> total_df <- merge(products_df,sales_df)
// You may try join function for the same purpose
//Look at the columns in total_df
> total_df
SparkDataFrame[Prod_Id_x:string, Product:string, Prod_Id_y:string, Month:string, Sales:string]
//Drop duplicate column
> total_df$Prod_Id_y <- NULL
> head(total_df)
Prod_Id_x Product Month Sales
1 101 Product 1 Jan 40
2 101 Product 1 Feb 10
3 101 Product 1 Mar 90
4 101 Product 1 Apr 10
5 101 Product 1 May 50
6 101 Product 1 Jun 70
> //Note: As of Spark 2.0 version, SparkR does not support
row sub-setting
你可能想尝试不同类型的连接,如左外连接和右外连接,或使用不同的列名称,以更好地理解这个函数。
机器学习
SparkR 提供了对现有 MLLib 函数的封装。R 公式被实现为 MLLib 特征转换器。转换器是 ML 管道(spark.ml
)的一个阶段,它接受 DataFrame 作为输入,并产生另一个 DataFrame 作为输出,通常包含一些附加列。特征转换器是一种转换器,将输入列转换为特征向量,这些特征向量会附加到源 DataFrame 上。例如,在线性回归中,字符串输入列被独热编码,数值被转换为双精度。一个标签列将会被附加(如果在数据框中尚未存在的话),作为响应变量的副本。
本节我们提供了朴素贝叶斯和高斯 GLM 模型的示例代码。我们不会详细解释模型本身或它们所产生的总结,而是直接演示如何使用 SparkR 来实现。
朴素贝叶斯模型
朴素贝叶斯模型是一个直观简单的模型,适用于分类数据。我们将使用朴素贝叶斯模型训练一个样本数据集。我们不会解释模型的工作原理,而是直接使用 SparkR 来训练模型。如果你想了解更多信息,请参考第六章,机器学习。
本示例使用一个包含二十名学生的平均成绩和出勤数据的数据集。实际上,这个数据集已经在第六章中介绍过,机器学习,用于训练集成方法。然而,让我们重新回顾一下它的内容。
学生们根据一组明确定义的规则被授予Pass
或Fail
。两个学生,ID 为1009
和1020
,尽管本应不及格,但被授予了Pass
。即使我们没有为模型提供实际的规则,我们仍期望模型预测这两名学生的结果为Fail
。以下是Pass
/ Fail
标准:
-
Marks < 40 => Fail
-
出勤不佳 => Fail
-
成绩高于 40 且出勤为全勤 => Pass
-
成绩 > 60 且出勤至少足够 => 通过。以下是训练朴素贝叶斯模型的示例:
//Example to train Naïve Bayes model
//Read file
> myFile <- read.csv("../work/StudentsPassFail.csv") //R data.frame
> df <- createDataFrame(myFile) //sparkDataFrame
//Look at the data
> showDF(df,4)
+---------+---------+----------+------+
|StudentId|Avg_Marks|Attendance|Result|
+---------+---------+----------+------+
| 1001| 48.0| Full| Pass|
| 1002| 21.0| Enough| Fail|
| 1003| 24.0| Enough| Fail|
| 1004| 4.0| Poor| Fail|
+---------+---------+----------+------+
//Make three buckets out of Avg_marks
// A >60; 40 < B < 60; C > 60
> df$marks_bkt <- otherwise(when(df$Avg_marks < 40, "C"),
when(df$Avg_marks > 60, "A"))
> df$marks_bkt <- otherwise(when(df$Avg_marks < 40, "C"),
when(df$Avg_marks > 60, "A"))
> df <- fillna(df,"B",cols="marks_bkt")
//Split train and test
> trainDF <- sample(df,TRUE,0.7)
> testDF <- except(df, trainDF)
//Build model by supplying RFormula, training data
> model <- spark.naiveBayes(Result ~ Attendance + marks_bkt, data = trainDF)
> summary(model)
$apriori
Fail Pass
[1,] 0.6956522 0.3043478
$tables
Attendance_Poor Attendance_Full marks_bkt_C marks_bkt_B
Fail 0.5882353 0.1764706 0.5882353 0.2941176
Pass 0.125 0.875 0.125 0.625
//Run predictions on test data
> predictions <- predict(model, newData= testDF)
//Examine results
> showDF(predictions[predictions$Result != predictions$prediction,
c("StudentId","Attendance","Avg_Marks","marks_bkt", "Result","prediction")])
+---------+----------+---------+---------+------+----------+
|StudentId|Attendance|Avg_Marks|marks_bkt|Result|prediction|
+---------+----------+---------+---------+------+----------+
| 1010| Full| 19.0| C| Fail| Pass|
| 1019| Enough| 45.0| B| Fail| Pass|
| 1014| Full| 12.0| C| Fail| Pass|
+---------+----------+---------+---------+------+----------+
//Note that the predictions are not exactly what we anticipate but models are usually not 100% accurate
高斯 GLM 模型
在这个示例中,我们尝试基于臭氧、太阳辐射和风速的值预测温度:
> //Example illustrating Gaussian GLM model using SparkR
> a <- createDataFrame(airquality)
//Remove rows with missing values
> b <- na.omit(a)
> //Inspect the dropped rows with missing values
> head(except(a,b),2) //MINUS set operation
Ozone Solar_R Wind Temp Month Day
1 NA 186 9.2 84 6 4
2 NA 291 14.9 91 7 14
> //Prepare train data and test data
traindata <- sample(b,FALSE,0.8) //Not base::sample
testdata <- except(b,traindata)
> //Build model
> model <- glm(Temp ~ Ozone + Solar_R + Wind,
data = traindata, family = "gaussian")
> // Get predictions
> predictions <- predict(model, newData = testdata)
> head(predictions[,c(predictions$Temp, predictions$prediction)],
5)
Temp prediction
1 90 81.84338
2 79 80.99255
3 88 85.25601
4 87 76.99957
5 76 71.75683
总结
截至目前,SparkR 尚未支持 Spark 中的所有算法,但正在积极开发中以弥补这一差距。Spark 2.0 版本已经改善了算法覆盖,包括朴素贝叶斯、k-均值聚类和生存回归等。请查看最新文档了解支持的算法。更多工作正在进行,旨在推出 SparkR 的 CRAN 版本,并与 R 包和 Spark 包更好地集成,同时提供更好的 RFormula 支持。
参考文献
-
SparkR:过去、现在与未来,作者:Shivaram Venkataraman:
shivaram.org/talks/sparkr-summit-2015.pdf
-
通过 Spark 和 R 实现探索性数据科学,作者:Shivaram Venkataraman 和 Hossein Falaki:
www.slideshare.net/databricks/enabling-exploratory-data-science-with-spark-and-r
-
SparkR:使用 Spark 扩展 R 程序,作者:Shivaram Venkataraman 等:
shivaram.org/publications/sparkr-sigmod.pdf
-
SparkR 中的最新进展:面向高级分析,作者:Xiangrui Meng:
files.meetup.com/4439192/Recent%20Development%20in%20SparkR%20for%20Advanced%20Analytics.pdf
-
要理解 RFormula,请尝试以下链接: