MLlib标准化了机器学习算法的API,使得将多个算法组合成单一的管道或工作流程变得更加容易。本节介绍了Pipelines API引入的关键概念,其中管道的概念主要受到scikit-learn项目的启发。
DataFrame:这个机器学习API使用来自Spark SQL的DataFrame作为机器学习数据集,它可以包含多种数据类型。例如,一个DataFrame可以有不同的列存储文本、特征向量、真实标签和预测值。
Transformer:Transformer是一种算法,可以将一个DataFrame转换成另一个DataFrame。例如,一个机器学习模型是一个Transformer,它将带有特征的DataFrame转换成带有预测的DataFrame。
Estimator:Estimator是一种算法,可以在DataFrame上进行拟合以产生一个Transformer。例如,一个学习算法是一个Estimator,它在DataFrame上训练并产生一个模型。
Pipeline:Pipeline将多个Transformers和Estimators链接在一起,以指定一个机器学习工作流程。
Parameter:所有的Transformers和Estimators现在共享一个用于指定参数的通用API。
机器学习可以应用于多种数据类型,如向量、文本、图像和结构化数据。这个API采用了Spark SQL中的DataFrame以支持多种数据类型。
DataFrame支持许多基本和结构类型;有关支持类型的列表,请参阅Spark SQL数据类型参考。除了Spark SQL指南中列出的类型,DataFrame还可以使用ML Vector类型。
DataFrame可以从常规RDD隐式或显式创建。请参阅下面的代码示例和Spark SQL编程指南中的示例。
DataFrame中的列是有名称的。下面的代码示例使用了如“text”、“features”和“label”这样的名称。
Transformer是一个包括特征转换器和学习模型的抽象概念。技术上,Transformer实现了一个transform()方法,它可以将一个DataFrame转换成另一个DataFrame,通常是通过添加一个或多个列。例如:
一个特征转换器可能会取一个DataFrame,读取一个列(例如,文本),将其映射到一个新列(例如,特征向量),并输出一个附加了映射列的新DataFrame。
一个学习模型可能会取一个DataFrame,读取包含特征向量的列,预测每个特征向量的标签,并输出一个附加了预测标签列的新DataFrame。
Estimator抽象了学习算法或任何在数据上拟合或训练的算法的概念。技术上,Estimator实现了一个fit()方法,该方法接受一个DataFrame并产生一个模型,而模型是一个Transformer。例如,一个学习算法如LogisticRegression是一个Estimator,调用fit()训练了一个LogisticRegressionModel,这是一个模型,因此也是一个Transformer。
Transformer.transform()和Estimator.fit()都是无状态的。将来,有状态的算法可能通过替代概念得到支持。
Transformer或Estimator的每个实例都有一个唯一的ID,这在指定参数时非常有用(下面将讨论)。
在机器学习中,通常会运行一系列算法来处理和学习数据。例如,一个简单的文本文档处理工作流可能包括几个阶段:
Pipeline 被指定为一系列的阶段,每个阶段要么是 Transformer,要么是 Estimator。这些阶段按顺序运行,输入的 DataFrame 在通过每个阶段时被转换。对于 Transformer 阶段,会在 DataFrame 上调用 transform() 方法。对于 Estimator 阶段,则调用 fit() 方法来产生一个 Transformer(它成为 PipelineModel 或已拟合 Pipeline 的一部分),然后在 DataFrame 上调用该 Transformer 的 transform() 方法。
我们以简单的文本文档工作流为例来说明这一点。下面的图是在训练时使用 Pipeline 的情况。
上面,顶部的行代表一个有三个阶段的 Pipeline。前两个(Tokenizer 和 HashingTF)是 Transformers(蓝色),第三个(LogisticRegression)是一个 Estimator(红色)。底部的行代表数据流经管道,其中圆柱体表示 DataFrames。Pipeline.fit() 方法被调用在原始 DataFrame 上,该 DataFrame 包含原始文本文档和标签。Tokenizer.transform() 方法将原始文本文档分割成单词,向 DataFrame 添加一个包含单词的新列。HashingTF.transform() 方法将单词列转换成特征向量,向 DataFrame 添加一个包含这些向量的新列。现在,由于 LogisticRegression 是一个 Estimator,Pipeline 首先调用 LogisticRegression.fit() 来产生一个 LogisticRegressionModel。如果 Pipeline 有更多的 Estimators,它会在将 DataFrame 传递到下一个阶段之前,调用 LogisticRegressionModel 的 transform() 方法在 DataFrame 上。
Pipeline 是一个 Estimator。因此,在 Pipeline 的 fit() 方法运行之后,它产生一个 PipelineModel,这是一个 Transformer。这个 PipelineModel 在测试时被使用;下面的图说明了这种用法。
在上图中,PipelineModel 与原始 Pipeline 拥有相同数量的阶段,但原始 Pipeline 中的所有 Estimators 都已变成 Transformers。当在测试数据集上调用 PipelineModel 的 transform() 方法时,数据按顺序通过已拟合的 pipeline。每个阶段的 transform() 方法更新数据集并将其传递到下一个阶段。
Pipelines 和 PipelineModels 帮助确保训练数据和测试数据经过相同的特征处理步骤。
DAG管道:管道的阶段被指定为一个有序数组。这里给出的例子都是线性管道,即,每个阶段使用前一个阶段产生的数据的管道。只要数据流图形成一个有向无环图(DAG),就有可能创建非线性管道。这个图目前是基于每个阶段的输入和输出列名隐式指定的(通常作为参数指定)。如果管道形成了一个DAG,那么阶段必须按拓扑顺序指定。
运行时检查:由于管道可以操作具有不同类型的DataFrames,它们不能使用编译时类型检查。管道和PipelineModels相反,在实际运行管道之前进行运行时检查。这种类型检查是使用DataFrame模式完成的,DataFrame模式是对DataFrame中列的数据类型的描述。
独特的管道阶段:管道的阶段应该是独特的实例。例如,相同的实例myHashingTF不应该被插入到管道中两次,因为管道阶段必须有唯一的ID。然而,不同的实例myHashingTF1和myHashingTF2(都是HashingTF类型的)可以放入同一个管道,因为不同的实例将会创建具有不同ID的。
MLlib中的估计器(Estimators)和转换器(Transformers)使用统一的API来指定参数。
Param是一个具有自包含文档的命名参数。ParamMap是一组(参数,值)对。
有两种主要的方法来向算法传递参数:
为实例设置参数。例如,如果lr是LogisticRegression的一个实例,可以调用lr.setMaxIter(10)使lr.fit()使用最多10次迭代。这个API类似于在spark.mllib包中使用的API。
向fit()或transform()传递一个ParamMap。ParamMap中的任何参数将覆盖之前通过设置方法指定的参数。
参数属于估计器和转换器的特定实例。例如,如果我们有两个LogisticRegression实例lr1和lr2,那么我们可以构建一个ParamMap,其中指定了两个maxIter参数:ParamMap(lr1.maxIter -> 10, lr2.maxIter -> 20)。如果在一个Pipeline中有两个算法都有maxIter参数,这就非常有用。
ML持久性:保存和加载管道
很多时候,将模型或管道保存到磁盘以供以后使用是值得的。在Spark 1.6中,模型导入/导出功能被添加到Pipeline API中。截至Spark 2.3,基于DataFrame的API在spark.ml和pyspark.ml中已经完全覆盖。
ML持久性在Scala、Java和Python之间是通用的。然而,R目前使用了一种修改过的格式,所以在R中保存的模型只能在R中加载;这个问题应该在将来得到解决,并在SPARK-15572中跟踪。
ML持久性的向后兼容性
一般来说,MLlib保持了对ML持久性的向后兼容性。也就是说,如果你在一个版本的Spark中保存了一个ML模型或管道,那么你应该能够在未来的Spark版本中加载并使用它。然而,有少数例外
###Code examples(代码示例)
本节提供了代码示例,说明了上述功能。更多信息,请参考API文档。
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.sql.{Row, SparkSession}
/**
* @description Pipeline 示例
* @date 2024/1/31 17:48
*/
object Pipeline {
def main(args: Array[String]): Unit = {
val spark = SparkSession
.builder
.master("local[*]")
.appName("Pipeline")
.getOrCreate()
// 训练文档,一个三元组
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")
// 定义一个ML Pipeline,包含三个步骤: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.01)
val pipeline = new Pipeline().setStages(Array(tokenizer, hashingTF, lr))
// 用测试数据训练模型
val model:PipelineModel = pipeline.fit(training)
//将训练好的模型保存在磁盘上
model.write.overwrite().save("tmp/spark-logistic-regression-model")
// 也可以将未训练的模型保存
pipeline.write.overwrite().save("tmp/unfit-lr-model")
// 加载已经训练好的模型
val sameModel:PipelineModel = PipelineModel.load("tmp/spark-logistic-regression-model")
//准备未打标签的测试文档
val test = spark.createDataFrame(Seq(
(4L, "spark i j k"),
(5L, "l m n"),
(6L, "spark hadoop spark"),
(7L, "apache hadoop")
)).toDF("id", "text")
sameModel.transform(test).select("id", "text", "probability", "prediction").show()
}
}