首先要引入概念 机器学习管道(ML pipelines),ML pipelines 提供基于DataFrames的高级API , 此API可以帮忙开发者创建和实践ML 管道。
Table of Contents
一 ML pipelines 基本概念
Spark MLlib API 可以很方便的将多种算法串联成一个管道或工作流。 本章节覆盖ML 管道 API中关键概念 , 这引起概念是受scikit-learn 项目启发。
1 DataFrame : ML API 使用Spark SQL 的DataFrame 作为ML 的数据集,这样可以接受任意数据集。 例如, DataFrame 可以接受各种各样的例,如文本,特征向量,真值标签和预测输出 。
2 Transformer: 变换是ML的算法 ,它将原始的DataFrame 变换为更一个DataFrame数据 , 例如, ML 模型将原始特征向量的DataFrame 变换为预测结果的DataFrame
3 Estimator: 变换的估计实际也是一个变换,它将DataFrame做变换后,得出一个对变换的接受度值 ,如 , 学习算法是一个估计算法,它训练DataFrame 后得到模型。
4 Pipeline: 管道是将多个变换和估计串联在一起,形成一个ML 流。
5 Parameter: 对于特定的参数, 所有变换和估计使用相同的接口
1.1 DataFrame
机器学习可以应用到各种数据类型,如向量、文本、图片和结构数据。 Spark SQL 中的DataFrame 同样也适用于各种数据类型。
DataFrame支持多种基本数据类型,同样也支持结构化数据类型;详见: Spark SQL datatype reference(
http://spark.apache.org/docs/latest/sql-programming-guide.html#data-types)
中支持的数据类型。除了Spark SQL 指南中列出来的数据类型外, Dataframe 也支持ML向量类型。
DataFrame 可以从RDD 数据中隐式,或显示创建为DataFrame。见Spark SQL programming guide 中示例代码。
DataFrame的列数据是有名称的,后面的代码使用名字 text , feature 和label 。
1.2 Pipeline components 管道的组成
1.2.1 Transformers
变换包含特征变换和模型学习,从编程来说,变换需要实现transform() 方法,此方法需要将DataFrame 转化为另一种数据DataFrame . 例如,
1> 特征变换会读取DataFrame 数据的每一列, 映射出新的列数据(如特征向量) , 输出带此输出列的新的DataFrame
2> 模型学习读取DataFrame 中包含特征向量的列,对每个特征向量预测一个类标签, 将有类标签的新的DataFrame输出
1.2.2 Estimators
估计函数抽象了学习模型, 或者评估哪些数据适用于哪些算法,从编程上来说, Estimator 需要实现 fit() 方法, 此方法需要一个参数 DataFrame, 并返回一个Model, 也就是一个Transformer。 例如,
学习算法,如线性回归 LogisticRegression 也是一个估计函数, 并且运行git() 方法生成一个LogisticRegressionModel , 即可以看作一个Model , 也是一个Transformer。
1.2.3 管道组成的属性
Transformer.transformer() 和 Estimator.fit() 都是无状态的, 未来会支持更多有状态的算法
每个Transformer和 Estimator 有一个唯一的ID , 在确认参数时这个ID 会有用处。
1.3 Pipeline
在机器学习中,通常会使用一系列算法来处理和学习数据,例如, 一个简单的文本文档的处理会有很多流程:
1 > 将文档中每个单词拆分出来
2 > 将每个单词转化为数据型特征向量
3 > 使用特征向量和类标签学习预测模型
MLib中一个workflow 称为一个pipeline, 每个Pipeline是由PipelineStages( 管道的处理段组成,可以是transformers 或estimators ) 按一定顺序排列。下一节会以一个简单的workflow为例讲解。
1.3.1 工作原理
Pipeline 实际是一序列处理段,每一个处理段或者是Transformer 或 Estimator。 这些处理段是顺序执行,这样对输入数据DataFrame 逐步进行处理。 对于Transformer 处理段, 对DataFrame 调用
transform() 方法, 而到了Estimator 处理段,fit() 方法调用并输出给Transformer (
which becomes part of the
PipelineModel
, or fitted
Pipeline
), 即对DataFrame 调用Transformer 的transform()方法。
下图展现一个文档的workflow , 图中说明pipeline的训练。
图中第一行展现Pipeline 表示三个处理段,前两个处理段分别是(Tokenizer 和 HashingTF) 分别 是Transformers , 第三个处理段是(LogisticRegression) 回归, 也就是Estimator 。
第二行展现数据流,其中圆柱形表示DataFrames, Pipeline.fit() 方法处理原始DataFrame ,这个数据是原始文本和类标签的二元组 。Tokenizer.transform() 方法将原始文本拆分成单词,
HashingTF.transform() 方法将单词向量转化为特征向量,并添加一个类标签到特征向量。 因为LogisticRegress 是一种Estimator,所以 Pipeline 首先调用LogisticRegression.fit() 方法输出
一个LogisticRegressionModel 模型。 假使此pipeline 再有多一些的处理段,可以调用LogisticRegressionModel 的 transform() 方法对DataFrame做处理后传给下一个处理段。
pipeline 也可以只有一个estimator , 这样一旦pipeline运行完fit()方法后,就可以输出 PipelineModel模型, 这个模型就是Transformer. 以下图就是验证PilelineModel 模型。
在上图中, PipelineModel 模型和前述模型有相同的处理段, 但前述模型中的Estimator ,在这个模型中变成Transformer 。 当PipelineModle 的transform() 方法在验证数据集时, 数据集会输送给待验证的模型workfllow, 每个处理段 transform()方法更新数据集,并将数据传到下一个处理段。
Pipelines 和 PipelineModel 的训练 和验证过程,数据都会流转到特征处理段来实现。
1.3.2 细节
DAG Pipeline : Pipeline 的处理段是一个有序地队列。 本节中例子给出的是线性Pipeline, 如, 每一步的Pipeline处理段输出结果传给下一个处理段 ,同样也可以构造一个非线性Pipeline , 只需要将数据处理段
排列成一个有向无环图(DAG) 。 用每一个处理段的输入和输出名称(column name)可以确定隐式一个图, 如果Pipeline 是一个DAG , 那么这些处理段需要按拓扑顺序排除。
runtime checking : 因为pipeline 可以对Dataframe进行各种操作,这样就没法使用编译期检查, Pipeline 和PipelineModel 只能在运行前进行运行期检查,这种检查是通过检查DataFrame的schema 实现 ,也就是检查DataFrame 每个属性的类型是否和描述匹配。
Unique Pipeline Stages : Pipeline的处理段应该有唯一的初阶 , 例如: 每一个pipeline 中不能出现myHashingTF 实例两次及两次以上, 因为每个处理段有唯一一个ID 标识。 尽管这样, 不同的初阶myHashingTF1和myHashingTF2 可以在同一个pipeline中出现, 因为不同的实例有不同地ID标识。
1.4 参数
MLlib Estimator 和Transformer 针对某些参数使用一致的API 。
Param 是参数名, ParamMap 是 (参数, 值)对。
有两种主要方式将参数传递给算法 :
1 对实例设置参数 。 例如, lr 是LogisticRegression 的实例,可以调用lr.setMaxIter(1)) 来设置 lr.fit() 至多使用10次迭代。这种方式多见sark.mllib包。
2 将ParamMap 传递给fit() 或transfor() 方法, 这种情况下,传递的ParamMap 参数会覆盖之前的 setter方法设置的参数 。
对 于Estimator 和Transformer 都有的参数如何修改,如 , LogisticRegression 实例 lr1, lr2 , 可以用两个实例的maxIter参数构造一个ParamMap : ParamMap(lr1.maxIter -> 10 , lr2.maxIter -> 20) ,此时Pipeline中使用的两个算法都有maxIter 参数时,这样很方便。
1.5 保存和加载pipeline
很多时候 ,我们需要保存一个模型,或Pipeline 用于下次使用。 在Spark 1.6中, pipeline API 中添加了模型的导入和导出功能。 很多基本的变换,以及基本的ML模型都支持这个功能, 详见算法API 文档查询是否支持保存和加载 pipeline
2 代码示例
本节给出演示的代码示例, 详见scala API 文本。
2.1 例: Estimator , Transformer , 和Param
下例展示Estimator , Transformer 和param 的使用
Estimator 详见 scala 文档, 详见:
api/scala/index.html#org.apache.spark.ml.Estimator
Transformer Scala文档, 详见:
api/scala/index.html#org.apache.spark.ml.Transformer
Param scala文档,详见:
api/scala/index.html#org.apache.spark.ml.param.Params
import
org.apache.spark.ml.classification.LogisticRegression
import
org.apache.spark.ml.linalg.
{
Vector
,
Vectors
}
import
org.apache.spark.ml.param.ParamMap
import
org.apache.spark.sql.Row
// Prepare training data from a list of (label, features) tuples.
val training
= spark
.createDataFrame
(
Seq
(
(
1.0
,
Vectors
.dense
(
0.0
,
1.1
,
0.1
)),
( 0.0 , Vectors .dense ( 2.0 , 1.0 , - 1.0 )),
( 0.0 , Vectors .dense ( 2.0 , 1.3 , 1.0 )),
( 1.0 , Vectors .dense ( 0.0 , 1.2 , - 0.5 )) )).toDF ( "label" , "features" )
// Create a LogisticRegression instance. This instance is an Estimator.
val lr
=
new
LogisticRegression
()
// Print out the parameters, documentation, and any default values.
println
(
"LogisticRegression parameters:\n"
+ lr
.explainParams
()
+
"\n"
)
// We may set parameters using setter methods.
lr
.setMaxIter
(
10
)
.setRegParam
(
0.01
)
// Learn a LogisticRegression model. This uses the parameters stored in lr.
val model1
= lr
.fit
(training
)
// Since model1 is a Model (i.e., a Transformer produced by an Estimator),
// we can view the parameters it used during fit().
// This prints the parameter (name: value) pairs, where names are unique IDs for this
// LogisticRegression instance.
println
(
"Model 1 was fit using parameters: "
+ model1
.parent
.extractParamMap
)
// We may alternatively specify parameters using a ParamMap,
// which supports several methods for specifying parameters.
val paramMap
=
ParamMap
(lr
.maxIter
->
20
)
.put
(lr
.maxIter
,
30
)
// Specify 1 Param. This overwrites the original maxIter.
.put (lr .regParam -> 0.1 , lr .threshold -> 0.55 ) // Specify multiple Params.
// One can also combine ParamMaps.
val paramMap2
=
ParamMap
(lr
.probabilityCol
->
"myProbability"
)
// Change output column name.
val paramMapCombined
= paramMap
++ paramMap2
// Now learn a new model using the paramMapCombined parameters.
// paramMapCombined overrides all parameters set earlier via lr.set* methods.
val model2
= lr
.fit
(training
, paramMapCombined
)
println
(
"Model 2 was fit using parameters: "
+ model2
.parent
.extractParamMap
)
// Prepare test data.
val test
= spark
.createDataFrame
(
Seq
(
(
1.0
,
Vectors
.dense
(-
1.0
,
1.5
,
1.3
)),
( 0.0 , Vectors .dense ( 3.0 , 2.0 , - 0.1 )),
( 1.0 , Vectors .dense ( 0.0 , 2.2 , - 1.5 )) )).toDF ( "label" , "features" )
// Make predictions on test data using the Transformer.transform() method.
// LogisticRegression.transform will only use the 'features' column.
// Note that model2.transform() outputs a 'myProbability' column instead of the usual
// 'probability' column since we renamed the lr.probabilityCol parameter previously.
model2
.transform
(test
)
.select
(
"features"
,
"label"
,
"myProbability"
,
"prediction"
)
.collect ()
.foreach { case Row (features : Vector , label : Double , prob : Vector , prediction : Double ) =>
println ( s"( $features , $label ) -> prob= $prob , prediction= $prediction " )
}
完整例子见:
examples/src/main/scala/org/apache/spark/examples/ml/EstimatorTransformerParamExample.scala
2.2 例子: Pipeline
下例给出pipeline的使用
Pipeline scala 文档详见:
api/scala/index.html#org.apache.spark.ml.Pipeline
import
org.apache.spark.ml.
{
Pipeline
,
PipelineModel
}
import
org.apache.spark.ml.classification.LogisticRegression
import
org.apache.spark.ml.feature.
{
HashingTF
,
Tokenizer
}
import
org.apache.spark.ml.linalg.Vector
import
org.apache.spark.sql.Row
// Prepare training documents from a list of (id, text, label) tuples.
val training
= spark
.createDataFrame
(
Seq
(
(
0L
,
"a b c d e spark"
,
1.0
),
( 1L , "b d" , 0.0 ),
( 2L , "spark f g h" , 1.0 ),
( 3L , "hadoop mapreduce" , 0.0 ) )).toDF ( "id" , "text" , "label" )
// Configure an ML pipeline, which consists of three stages: tokenizer, hashingTF, and lr.
val tokenizer
=
new
Tokenizer
()
.setInputCol
(
"text"
)
.setOutputCol
(
"words"
)
val hashingTF
=
new
HashingTF
()
.setNumFeatures
(
1000
)
.setInputCol (tokenizer .getOutputCol )
.setOutputCol
(
"features"
)
val lr
=
new
LogisticRegression
()
.setMaxIter
(
10
)
.setRegParam
(
0.001
)
val pipeline
=
new
Pipeline
()
.setStages
(
Array
(tokenizer
, hashingTF
, lr
))
// Fit the pipeline to training documents.
val model
= pipeline
.fit
(training
)
// Now we can optionally save the fitted pipeline to disk
model
.write
.overwrite
().save
(
"/tmp/spark-logistic-regression-model"
)
// We can also save this unfit pipeline to disk
pipeline
.write
.overwrite
().save
(
"/tmp/unfit-lr-model"
)
// And load it back in during production
val sameModel
=
PipelineModel
.load
(
"/tmp/spark-logistic-regression-model"
)
// Prepare test documents, which are unlabeled (id, text) tuples.
val test
= spark
.createDataFrame
(
Seq
(
(
4L
,
"spark i j k"
),
( 5L , "l m n" ),
( 6L , "spark hadoop spark" ),
( 7L , "apache hadoop" ) )).toDF ( "id" , "text" )
// Make predictions on test documents.
model
.transform
(test
)
.select
(
"id"
,
"text"
,
"probability"
,
"prediction"
)
.collect ()
.foreach { case Row (id : Long , text : String , prob : Vector , prediction : Double ) =>
println ( s"( $id , $text ) --> prob= $prob , prediction= $prediction " )
}
详见:
examples/src/main/scala/org/apache/spark/examples/ml/PipelineExample.scala
2.3 模型选择 (hyperparameter 超参数优化)
使用ML Pipeline的好处是实现超参数优化, 详见:ML Tuning Guide :
http://spark.apache.org/docs/latest/ml-tuning.html 查阅更多关于自动模型选择的信息