ML管道提供基于DataFrame的统一高级API集,可帮助用户创建和调整实用的机器学习管道。
MLlib对用于机器学习算法的API进行了标准化,从而使将多种算法组合到单个管道或工作流中变得更加容易。
三.管道组件
Transformers
Transformer是一个包含特征转换器和学习模型的抽象。从技术上讲,一种Transformer工具实现了一种transform()方法,DataFrame通常通过附加一个或多个列将其转换为另一列的方法。例如:
估算器
一个Estimator抽象学习算法的概念或算法。从技术上讲,Estimator实现一种fit()方法,该方法接受一个DataFrame并产生一个Model,即一个Transformer。例如,学习算法(例如LogisticRegression)Estimator和调用 fit()训练一个LogisticRegressionModel。
管道组件的属性
Transformer.transform()和Estimator.fit()都是无状态的。将来,可通过替代概念来支持有状态算法。
Transformer或Estimator的每个实例都有一个唯一的ID,该ID在指定参数中很有用。
管道
在机器学习中,通常需要运行一系列算法来处理数据并从中学习。例如,一个简单的文本文档处理工作流程可能包括几个阶段:
MLlib将这样的工作流程表示为Pipeline,其中包含要按特定顺序运行的一系列 PipelineStages(Transformers和Estimators)。
这个怎么运作
A Pipeline被指定为一个阶段序列,每个阶段是a Transformer或an Estimator。这些阶段按顺序运行,并且输入DataFrame在通过每个阶段时都会进行转换。对于Transformer阶段,该transform()方法在DataFrame上调用。对于Estimator阶段,将调用fit()方法来生成Transformer(成为PipelineModel或一部分Pipeline),Transformer在DataFrame上调用transform方法。
下图是的训练时间用法Pipeline。
上方的第一行代表Pipeline具有三个阶段。前两个(Tokenizer和HashingTF)是Transformers(蓝色),第三个(LogisticRegression)是Estimator(红色)。最下面的行表示流经管道的数据,其中圆柱体表示DataFrames。Pipeline.fit()在原始DataFrame文件上调用此方法,原始文件包含原始文本文档和标签。该Tokenizer.transform()方法将原始文本文档拆分为单词,然后向添加带有单词的新列DataFrame。该HashingTF.transform()方法将words列转换为特征向量,并将带有这些向量的新列添加到DataFrame。由于LogisticRegression为Estimator,因此Pipeline第一个调用LogisticRegression.fit()产生一个LogisticRegressionModel。如果Pipeline有更多Estimator,它将调用LogisticRegressionModel的transform() 方法,DataFrame然后再传递DataFrame到下一个阶段。
A Pipeline是一个Estimator。因此,运行Pipeline的fit()方法后,它会产生一个PipelineModel,即一个 Transformer。这PipelineModel是在使用测试时间 ; 下图说明了这种用法。
在上图中,PipelineModel具有与原始相同的阶段数Pipeline,但是原始中的所有Estimators Pipeline已变为Transformer。当测试数据集在PipelineModel的transform()方法上被使用时,该数据是为了通过拟合管道传递。每个阶段的transform()方法都会更新数据集,并将其传递到下一个阶段。
Pipeline和PipelineModel有助于确保训练和测试数据经过相同的特征处理步骤。
DAG Pipeline:Pipeline的级被指定为一个有序阵列。线性Pipelines,即Pipeline每个阶段使用前一阶段产生的数据。只要数据流图形成有向无环图(DAG),就可以创建非线性Pipeline。基于当前每个阶段的输入和输出列名称(通常指定为参数)隐式指定该图。如果Pipeline形成DAG,则必须按拓扑顺序指定阶段。
运行时检查:由于Pipelines可以对DataFrames进行各种类型的操作,因此它们不能使用编译时类型检查。 Pipelines和PipelineModels会在实际运行之前进行运行时检查Pipeline。使用DataFrame 模式完成类型检查,模式是DataFrame中列的数据类型描述。
唯一的管道阶段:A Pipeline的阶段应该是唯一的实例。例如,同一实例 myHashingTF不应插入Pipeline两次,因为Pipeline阶段必须具有唯一的ID。但是,由于将使用不同的ID创建不同的实例,因此可以将不同的实例myHashingTF1和myHashingTF2(类型均为HashingTF)放入同一Pipeline实例。
MLlib Estimator和Transformers使用统一的API来指定参数。
A Param是具有独立文件的命名参数。A ParamMap是一组(参数,值)对。
将参数传递给算法的主要方法有两种:
通常,将模型或管道保存到磁盘以供以后使用是值得的。在Spark 1.6中,模型导入/导出功能已添加到管道API。从Spark 2.3开始,基于DataFrame的API spark.ml增加pyspark.ml。
ML持久性可跨Scala,Java和Python使用。但是,R当前使用修改后的格式,因此保存在R中的模型只能重新加载到R中。以后应该修复此问题,并在SPARK-15572中进行跟踪。
通常,MLlib为ML持久性保持向后兼容性。即,如果在一个版本的Spark中保存ML模型或管道,则应该能够将其加载回去并在以后的Spark版本中使用。但是,有极少数例外,如下所述。
模型的持久性:是否可以通过Y版本的Spark加载使用Spark ML在Spark版本X中的持久性保存的模型或管道?
模型行为:Spark版本X中的模型或管道在Spark版本Y中的行为是否相同?
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")
}
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(s"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(s"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(s"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: Long, prob: Vector, prediction: Double) =>
println(s"($features, $label) -> prob=$prob, prediction=$prediction")
}