MLlib对用于机器学习算法的API进行了标准化,从而使将多种算法组合到单个管道或工作流中变得更加容易。本节介绍了Pipelines API引入的关键概念,其中,管道概念主要受scikit-learn项目的启发。
1、DataFrame
机器学习可以应用于多种数据类型,例如矢量,文本,图像和结构化数据。该API采用Spark SQL中的DataFrame,以支持各种数据类型。
DataFrame支持许多基本类型和结构化类型。请参阅Spark SQL数据类型参考以获取受支持类型的列表。除了Spark SQL指南中列出的类型之外,DataFrame还可以使用ML Vector类型。
可以从常规RDD隐式或显式创建DataFrame。有关示例,请参见下面的代码示例和Spark SQL编程指南
命名DataFrame中的列。下面的代码示例使用诸如“文本”,“功能”和“标签”之类的名称。
2、Pipeline components
1)转换器
变形器是一种抽象,其中包括特征转换器和学习的模型。从技术上讲,Transformer实现了transform()方法,该方法通常通过附加一个或多个列将一个DataFrame转换为另一个。例如:
2)估计器
估计器抽象学习算法或适合或训练数据的任何算法的概念。从技术上讲,估算器实现了fit()方法,该方法接受DataFrame并生成一个Model(即Transformer)。例如,诸如LogisticRegression之类的学习算法是Estimator,调用fit()会训练LogisticRegressionModel,后者是Model,因此是Transformer。
3)管道组件的属性
Transformer.transform()和Estimator.fit()都是无状态的。将来,可以通过替代概念来支持有状态算法。
每个Transformer或Estimator实例都有一个唯一的ID,该ID对指定参数(在下面讨论)很有用。
3、管道
在机器学习中,通常需要运行一系列算法来处理数据并从中学习。例如,简单的文本文档处理工作流程可能包括几个阶段:
MLlib将这样的工作流表示为“管道”,它由要按特定顺序运行的一系列管道阶段(变形器和估计器)组成。在本节中,我们将使用此简单的工作流作为运行示例。
1)How it works
管线被指定为阶段序列,每个阶段可以是一个Transformer或Estimator。这些阶段按顺序运行,并且输入DataFrame在通过每个阶段时都会进行转换。对于Transformer阶段,在DataFrame上调用transform()方法。对于Estimator阶段,调用fit()方法以生成一个Transformer(它将成为PipelineModel或已拟合Pipeline的一部分),并且在DataFrame上调用该Transformer的transform()方法。
我们通过简单的文本文档工作流程对此进行说明。下图是管道的培训时间使用情况。
上方的第一行代表三个阶段的管道。前两个(令牌生成器和HashingTF)是“变形金刚”(蓝色),第三个(LogisticRegression)是“估计”(红色)。最下面的行表示流经管道的数据,圆柱体表示DataFrame。 在原始DataFrame上调用Pipeline.fit()方法,该DataFrame具有原始文本文档和标签。 Tokenizer.transform()方法将原始文本文档拆分为单词,并向DataFrame添加带有单词的新列。HashingTF.transform()方法将word列转换为特征向量,并将带有这些向量的新列添加到DataFrame。现在,由于LogisticRegression是Estimator,因此管道首先调用LogisticRegression.fit()来生成LogisticRegressionModel。如果管道中有更多估算器,则在将DataFrame传递到下一阶段之前,将在DataFrame上调用LogisticRegressionModel的transform()方法。
管道是估算器。因此,在运行Pipeline的fit()方法之后,它将生成PipelineModel,它是一个Transformer。该PipelineModel在测试时使用;下图说明了这种用法。
在上图中,PipelineModel具有与原始Pipeline相同的阶段数,但是原始Pipeline中的所有Estimator都已变为Transformers。在测试数据集上调用PipelineModel的transform()方法时,数据将按顺序通过拟合的管道。每个阶段的transform()方法都会更新数据集,并将其传递到下一个阶段。
管道和管道模型有助于确保训练和测试数据经过相同的特征处理步骤。
2)细节
DAG Pipelines(DAG管道):管道的阶段被指定为有序数组。此处给出的所有示例均适用于线性管道,即每个阶段使用前一阶段产生的数据的管道。只要数据流图形成有向非循环图(DAG),就可以创建非线性管道。当前根据每个阶段的输入和输出列名称(通常指定为参数)隐式指定该图。 If the Pipeline forms a DAG, then the stages must be specified in topological order.
Runtime checking: 由于管道可以对具有各种类型的DataFrame进行操作,因此它们不能使用编译时类型检查。相反,Pipelines和PipelineModels在实际运行Pipeline之前会进行运行时检查。此类型检查使用DataFrame架构完成,该架构是对DataFrame中列的数据类型的描述。
Unique Pipeline stages: 管道的阶段应该是唯一的实例。例如,同一实例myHashingTF不应两次插入到管道中,因为管道阶段必须具有唯一的ID。但是,由于将使用不同的ID创建不同的实例,因此可以将不同的实例myHashingTF1和myHashingTF2(均为HashingTF类型)放入同一管道中。
4、Parameters(参数)
MLlib估计器和变形器使用统一的API来指定参数。
参数是具有独立文件的命名参数。 ParamMap是一组(参数,值)对。
将参数传递给算法的主要方法有两种:
参数属于估计器和变形器的特定实例。例如,如果我们有两个LogisticRegression实例lr1和lr2,则可以使用指定的两个maxIter参数来构建ParamMap:ParamMap(lr1.maxIter-> 10,lr2.maxIter-> 20)。如果管道中有两个带有maxIter参数的算法,这将很有用。
5、ML persistence : saving and loading pipelines
通常,将模型或管道保存到磁盘以供以后使用是值得的。在Spark 1.6中,模型导入/导出功能已添加到管道API。从Spark 2.3开始,spark.ml和pyspark.ml中基于DataFrame的API已有完整介绍。
ML持久性可跨Scala,Java和Python使用。但是,R当前使用修改后的格式,因此保存在R中的模型只能重新加载到R中。以后应该修复此问题,并在SPARK-15572中进行跟踪。
1)ML持久性的向后兼容性
通常,MLlib为ML持久性保持向后兼容性。也就是说,如果您将ML模型或管道保存在一个版本的Spark中,则应该能够将其重新加载并在以后的Spark版本中使用。但是,有极少数例外,如下所述。
模型持久性:是否可以通过Y版本的Spark加载使用X版本X中的Apache Spark ML持久性保存的模型或管道?
模型行为:Spark版本X中的模型或管道在Spark版本Y中的行为是否相同?
对于模型持久性和模型行为,在次要版本或补丁版本中的所有重大更改都将在Spark版本发行说明中报告。如果发行说明中未报告损坏,则应将其视为要修复的错误。
本节提供了说明上述功能的代码示例。有关更多信息,请参阅API文档(Scala,Java和Python)。
1)Example : 估计器,变压器和参数
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: Double, prob: Vector, prediction: Double) =>
println(s"($features, $label) -> prob=$prob, prediction=$prediction")
}
2)Example :管道
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")
}
协作过滤通常用于推荐系统。这些技术旨在填充用户项关联矩阵的缺失条目。 spark.ml当前支持基于模型的协作过滤,其中通过一小部分潜在因素来描述用户和产品,这些潜在因素可用于预测缺少的条目。 spark.ml使用交替最小二乘(ALS)算法来学习这些潜在因素。 spark.ml中的实现具有以下参数:
注意:用于ALS的基于DataFrame的API当前仅支持用户和项目ID的整数。用户和项目ID列支持其他数字类型,但是ID必须在整数值范围内。
基于矩阵分解的协作过滤的标准方法将用户项矩阵中的条目视为用户对项目(例如,给电影评分的用户)赋予的明确偏好。
在许多实际用例中,通常只能访问隐式反馈(例如,视图,点击,购买,喜欢,分享等)。 spark.ml中用于处理此类数据的方法来自隐式反馈数据集的协作过滤。从本质上讲,此方法不是尝试直接对评分矩阵建模,而是将数据视为代表用户操作观察力的数字(例如,点击次数或某人观看电影的累积时间)。然后,这些数字与观察到的用户偏好的置信度有关,而不是与对商品的明确评分有关。然后,该模型尝试查找可用于预测用户对某项商品的期望偏好的潜在因素。
在解决每个最小二乘问题时,我们根据用户在更新用户因子时生成的评分数量或在更新产品因数中获得的产品评分数量来缩放正则化参数regParam。这种方法称为“ ALS-WR”,并在论文“ Netflix奖的大规模并行协作过滤”中进行了讨论。
它使regParam减少了对数据集规模的依赖,因此我们可以将从采样子集中学习的最佳参数应用于整个数据集,并期望获得类似的性能。
使用ALSModel进行预测时,通常会遇到训练模型期间不存在的用户和/或测试数据集中的项目。这通常在两种情况下发生:
默认情况下,当模型中不存在用户和/或项目因子时,Spark在ALSModel.transform期间分配NaN预测。这在生产系统中可能很有用,因为它表明有新用户或新物品,因此系统可以做出一些后备决策以用作预测。
但是,这在交叉验证期间是不可取的,因为任何NaN预测值都将导致评估指标的NaN结果(例如,使用RegressionEvaluator时)。这使得无法选择模型。
Spark允许用户将coldStartStrategy参数设置为“ drop”,以便删除包含NaN值的预测数据框中的任何行。然后,将根据非NaN数据计算评估指标并将其有效。下例说明了此参数的用法。
注意:当前支持的冷启动策略是“ nan”(上述默认行为)和“ drop”。将来可能会支持其他策略。
示例代码
在以下示例中,我们从MovieLens数据集中加载收视率数据,每一行包括用户,电影,收视率和时间戳。然后,我们训练一个ALS模型,该模型默认情况下假设等级为显式(implicitPrefs为false)。我们通过测量评分预测的均方根误差来评估推荐模型。
有关该API的更多详细信息,请参阅ALS Scala文档。
import org.apache.spark.ml.evaluation.RegressionEvaluator
import org.apache.spark.ml.recommendation.ALS
case class Rating(userId: Int, movieId: Int, rating: Float, timestamp: Long)
def parseRating(str: String): Rating = {
val fields = str.split("::")
assert(fields.size == 4)
Rating(fields(0).toInt, fields(1).toInt, fields(2).toFloat, fields(3).toLong)
}
val ratings = spark.read.textFile("data/mllib/als/sample_movielens_ratings.txt")
.map(parseRating)
.toDF()
val Array(training, test) = ratings.randomSplit(Array(0.8, 0.2))
// Build the recommendation model using ALS on the training data
val als = new ALS()
.setMaxIter(5)
.setRegParam(0.01)
.setUserCol("userId")
.setItemCol("movieId")
.setRatingCol("rating")
val model = als.fit(training)
// Evaluate the model by computing the RMSE on the test data
// Note we set cold start strategy to 'drop' to ensure we don't get NaN evaluation metrics
model.setColdStartStrategy("drop")
val predictions = model.transform(test)
val evaluator = new RegressionEvaluator()
.setMetricName("rmse")
.setLabelCol("rating")
.setPredictionCol("prediction")
val rmse = evaluator.evaluate(predictions)
println(s"Root-mean-square error = $rmse")
// Generate top 10 movie recommendations for each user
val userRecs = model.recommendForAllUsers(10)
// Generate top 10 user recommendations for each movie
val movieRecs = model.recommendForAllItems(10)
// Generate top 10 movie recommendations for a specified set of users
val users = ratings.select(als.getUserCol).distinct().limit(3)
val userSubsetRecs = model.recommendForUserSubset(users, 10)
// Generate top 10 user recommendations for a specified set of movies
val movies = ratings.select(als.getItemCol).distinct().limit(3)
val movieSubSetRecs = model.recommendForItemSubset(movies, 10)
如果评级矩阵是从其他信息源中得出的(即是从其他信号推断得出的),则可以将implicitPrefs设置为true以获得更好的结果:
val als = new ALS()
.setMaxIter(5)
.setRegParam(0.01)
.setImplicitPrefs(true)
.setUserCol("userId")
.setItemCol("movieId")
.setRatingCol("rating")
挖掘频繁项,项集,子序列或其他子结构通常是分析大规模数据集的第一步,而这是多年来数据挖掘中的活跃研究主题。我们建议用户参考Wikipedia的关联规则学习以获取更多信息。
FP增长算法在Han等人的论文中进行了描述,该算法在不生成候选者的情况下挖掘频繁模式,其中“ FP”代表频繁模式。 给定交易数据集,FP增长的第一步是计算项目频率并识别频繁项目。与为相同目的设计的类似Apriori的算法不同,FP-growth的第二步使用后缀树(FP-tree)结构对交易进行编码,而无需显式生成候选集,这通常成本较高。第二步之后,可以从FP树中提取频繁项集。在spark.mllib中,我们实现了称为PFP的FP-growth的并行版本,如Li et al。,PFP:并行FP-growth用于查询推荐中所述。 PFP基于事务的后缀来分配增长的FP树的工作,因此比单机实现更具可伸缩性。我们请用户参考这些文件以获取更多详细信息。
spark.ml的FP-growth实现采用以下(超)参数:
FPGrowthModel提供:
import org.apache.spark.ml.fpm.FPGrowth
val dataset = spark.createDataset(Seq(
"1 2 5",
"1 2 3 5",
"1 2")
).map(t => t.split(" ")).toDF("items")
val fpgrowth = new FPGrowth().setItemsCol("items").setMinSupport(0.5).setMinConfidence(0.6)
val model = fpgrowth.fit(dataset)
// Display frequent itemsets.
model.freqItemsets.show()
// Display generated association rules.
model.associationRules.show()
// transform examines the input items against all the association rules and summarize the
// consequents as prediction
model.transform(dataset).show()
PrefixSpan是在Pei等人的《通过模式增长来挖掘顺序模式:PrefixSpan方法》中描述的顺序模式挖掘算法。我们为读者提供参考文献,以规范化顺序模式挖掘问题。
spark.ml的PrefixSpan实现采用以下参数:
import org.apache.spark.ml.fpm.PrefixSpan
val smallTestData = Seq(
Seq(Seq(1, 2), Seq(3)),
Seq(Seq(1), Seq(3, 2), Seq(1, 2)),
Seq(Seq(1, 2), Seq(5)),
Seq(Seq(6)))
val df = smallTestData.toDF("sequence")
val result = new PrefixSpan()
.setMinSupport(0.5)
.setMaxPatternLength(5)
.setMaxLocalProjDBSize(32000000)
.findFrequentSequentialPatterns(df)
.show()