【Spark MLlib】(一)Spark ML Pipelines

Spark ML Pipeline 的引入,是受到 scikit-learn 的启发,虽然 MLlib 已经足够简单实用,但如果目标数据集结构复杂,需要多次处理,或是在学习过程中,要使用多个转化器 (Transformer) 和预测器 (Estimator),这种情况下使用 MLlib 将会让程序结构极其复杂。

所以,一个可用于构建复杂机器学习工作流应用的新库已经出现了,它就是 Spark 1.2 版本之后引入的 ML Pipeline。在本节中,我们将介绍ML Pipelines的概念。 ML Pipelines提供了一组基于DataFrame构建的统一的高级API,可帮助用户创建和调整实用的机器学习流程。

一、Pipelines的主要概念

MLlib标准化用于机器学习算法的API,以便更轻松地将多个算法组合到单个管道或工作流程中。本节介绍Pipelines API引入的关键概念,其中管道概念主要受到scikit-learn项目的启发。

  • DataFrame:此ML API使用Spark SQL中的DataFrame作为ML数据集,它可以包含各种数据类型。例如,DataFrame可以具有存储文本,特征向量,标签(true labels)和预测的不同列。

  • Transformer:Transformer是一种可以将一个DataFrame转换为另一个DataFrame的算法。例如,ML模型是变换器,其将具有特征的DataFrame转换为具有预测的DataFrame。

  • Estimator:Estimator是一种算法,可以适应DataFrame以生成Transformer。例如,学习算法是Estimator,其在DataFrame上训练并产生模型。

  • Pipeline:管道将多个Transformers和Estimators链接在一起以指定ML工作流程。

  • 参数:所有Transformers和Estimators现在共享一个用于指定参数的通用API。

1.1 DataFrame

ML API 使用 Spark SQL 中的 DataFrame 作为机器学习数据集,可以容纳很多种类型的数据:文本、向量、图像和结构化数据等等。Spark DataFrame 以 RDD(Resilient Distributed Datasets) 为基础,但是带有 Schema(数据库中对象的集合)信息,类似于传统数据库中的二维表格。

扫描二维码关注公众号,回复: 9822881 查看本文章

1.2 Pipeline组件

1.2.1 Transformers

中文可译作转换器,继承自 PipelineStage,它是一个算法,可以将一个 DataFrame 转换成另一个 DataFrame。如:

  • 一个特征转换器对 DataFrame 的操作可能为,读取一个文本列,将其映射为一个新的特征向量列,然后输出一个带有特征向量的新的 DataFrame。
  • 一个机器学习模型对 DataFrame 的操作可能为,读取一个带有特征向量的列,对每一个特征向量进行预测,然后输出一个带有预测数据的新的 DataFrame。

1.2.2 Estimators

中文可译作预测器,它也是一个算法。预测器通过 fit() 方法,接收一个 DataFrame 并产出一个模型。例如,逻辑回归 (LogisticRegression) 算法就是一种预测器,通过调用 fit() 方法来训练得到一个逻辑回归模型。

⚠️ 注:Transformer.transform() 和 Estimator.fit() 都是无状态的。未来可能会被有状态的算法替代。每个转换器和预测器都有唯一 ID,这在调参过程中很有用。

1.3 Pipeline

在机器学习中,通常运行一系列算法来处理和学习数据。 例如,简单的文本文档处理工作流程可能包括几个阶段:

  • 将每个文档的文本拆分为单词。
  • 将每个文档的单词转换为数字特征向量。
  • 使用特征向量和标签学习预测模型。

MLlib将此类工作流表示为管道,其由一系列以特定顺序运行的PipelineStages(变换器和估算器)组成。我们将在本节中将此简单工作流用作运行示例。

1.4 Parameter

所有的转换器和预测器使用同一个 API 来指定参数。 一个 Param 是被定义好的已命名参数。一个 ParamMap 是一组“参数-值” (parameter, value) 对。

向一个算法传参的方法主要有两种:

  1. 为实例设置参数。例如,lr 是 LogisticRegression 的一个实例,我们可以通过调用 lr.setMaxIter(10) 方法来设定 lr.fit() 最多进行10次迭代。这个 API 和 spark.mllib 包中的 API 相似。
  2. 通过 ParamMap 给 fit() 和 transform() 传参。ParamMap 中的参数将会覆盖之前通过 setter 方法设置过的参数。

如果我们有两个 LogisticRegression 实例 lr1 和 lr2,可以创建一个包含两个 maxIter 的 ParamMap:ParamMap(lr1.maxIter -> 10, lr2.maxIter -> 20)。如果一个 Pipeline 里有两个带有 maxIter 的算法,这种方法比较实用。

1.5 工作原理

Pipeline 由一系列 stage 组成,每个 stage 为一个转换器 (Transformer) 或预测器 (Estimator)。这些 stage 的执行是按一定顺序的,输入的 DataFrame 在通过每个 stage 时被改变。在转换器阶段,transform() 方法作用在 DataFrame 上。预测器阶段,调用 fit() 方法来产生一个转换器(成为 PipelneModel 的一部分),然后该转换器的 transform() 方法作用在 DataFrame 上。

我们通过一个简单的文档工作流来解释其工作原理,下图展示了训练过程中 Pipeline 的工作流程:

在这里插入图片描述

Pipeline 在训练过程中的流程

上图中,上面一行表示 Pipeline 的三个 stage。前两个 Tokenizer 和 HashingTF 是转换器,第三个 LogisticRegression 是一个预测器。下面一行表示通过 pipeline 的数据流,圆柱体代表 DataFrame。Pipeline.fit() 方法作用于原始 DataFrame,其中包含原始的文档和标签。Tokenizer.transform() 方法将原始文档分成单个词,将其作为新的列加入 DataFrame。HashingTF.transform() 方法将文本列转化成特征向量,将这些向量作为新的列加入 DataFrame。因为 LogisticRegression 是一个预测器,Pipeline 首先调用 LogisticRegression.fit() 来产生一个 LogisticRegressionModel。如果 Pipeline 还有其他的预测器,在将 DataFrame 传入下一个 stage 前,它将先调用 LogisticRegressionModel 的 transform() 方法。

整个 Pipeline 可以看作一个预测器。因此,在一个 Pipeline 的 fit() 方法执行完毕后,它会产生一个 PipelineModel,它是一个转换器。这个 PipelineModel 可在测试阶段调用,下图展示了具体的工作流程:
在这里插入图片描述
PipelineModel 在测试过程中的流程

上图中,PipelineModel 和原始 Pipeline 有相同数量的 stage,但原始 Pipeline 中的预测器都变成了转换器。当 PipelineModel 的 transform() 方法被作用于测试数据集时,数据会按顺序穿过 pipeline 的各个阶段。每个 stage 的 transform() 方法都对数据集作了改变,然后再将其送至下一个 stage。

Pipeline 和 PipelineModel 有助于确保训练数据和测试数据的特征处理流程保持一致。

1.6 详细信息

DAG Pipeline: Pipeline 的 stage 是一个有序数组。这里引用的例子都是线性 Pipeline,其中的每个 stage 所用的数据都是由上一个 stage 产生的。但如果数据的流向为一个有向无环图 (DAG, Directed Acyclic Graph),那么 Pipeline 就可以是非线性的。这种图需要指定每个 stage 中输入和输出列的名字(通常通过参数来指定)。如果 Pipeline 的形式为 DAG,那么每个 stage 都必须为拓扑排序。

Runtime checking: 因为 Pipeline 可以运行在各种类型的 DataFrame 上,所以不能在编译时检查出错误类型。Pipeline 和 PipelineModel 会在 Pipeline 实际运行前进行检查。这种类型检查是基于 DataFrame schema 的,schema 包含了 DataFrame 中每一列数据的类型。

Unique Pipeline stages: Pipeline 的每一个 stage 都应该是唯一的实例。例如,同一个 myHashingTF 实例不应该进入 Pipeline 两次,因为 Pipeline stage 必须有唯一 ID。然而,不同的实例 myHashingTF1 和 myHashingTF2 可以进入同一个 Pipeline,因为两个实例是通过不同 ID 创建的。

1.7 ML持久性:保存和加载管道

通常,将模型或管道保存到磁盘以供以后使用是值得的。 在Spark 1.6中,模型导入/导出功能已添加到Pipeline API中。 从Spark 2.3开始,spark.ml和pyspark.ml中基于DataFrame的API具有完整的覆盖范围。

ML持久性适用于Scala,Java和Python。 但是,R当前使用的是修改后的格式,因此保存在R中的模型只能加载回R; 这应该在将来修复,并在SPARK-15572中进行跟踪。

1.7.1 ML持久性的向后兼容性

通常,MLlib保持ML持久性的向后兼容性。即,如果您在一个版本的Spark中保存ML模型或Pipeline,那么您应该能够将其加载回来并在将来的Spark版本中使用它。但是,极少数例外情况如下所述。

模型持久性:Spark版本Y可以加载Spark版本X中使用Apache Spark ML持久性保存模型或管道吗?

  • 主要版本:没有保证,但是尽力而为。
  • 次要和补丁版本:是的;这些是向后兼容的。
  • 关于格式的注意事项:不保证稳定的持久性格式,但模型加载本身设计为向后兼容。

模型行为:Spark版本X中的模型或管道在Spark版本Y中的行为是否相同?

  • 主要版本:没有保证,但是尽力而为。
  • 次要和补丁版本:相同的行为,除了错误修复。

对于模型持久性和模型行为,在Spark版本发行说明中报告了次要版本或修补程序版本的任何重大更改。如果发行说明中未报告破损,则应将其视为要修复的错误。

二、Pipelines 实例

一个pipeline由多个步骤组成,每一个步骤都是一个Transformer或者Estimator。这些步骤按顺序执行,首先输入DataFrame,然后通过每个阶段进行转换。在Transformer步骤中,DataFrame会调用transform()方法。在Estimator步骤中,fit()方法被调用并产生一个transformer,并且DataFrame会调用这个Transformer的transform()方法。

1、实例:Estimator, Transformer, Param

package sparkml
 
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.linalg.{Vectors, Vector}
import org.apache.spark.ml.param.ParamMap
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.Row
 
/**
  * 实例:Estimator, Transformer, Param
  */
 
object Pipeline {
  def main(args: Array[String]): Unit = {
    //设置日志输出格式
    Logger.getLogger("org").setLevel(Level.WARN)
 
    //定义SparkSession
    val spark = SparkSession.builder()
      .appName("pipeline")
      .master("local[*]")
      .getOrCreate()
 
    import spark.implicits._
 
    //1、训练样本
    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")
 
    //2、创建逻辑回归Estimator
    val lr = new LogisticRegression()
 
    //3、通过setter方法设置模型参数
    lr.setMaxIter(10)  //设置最大迭代次数
      .setRegParam(0.01)  //设置正则迭代因子
 
    //4、训练模型
    val model1 = lr.fit(training)
 
    //5、通过ParamMap设置参数方法
    val paramMap = ParamMap(lr.maxIter -> 20)
      .put(lr.maxIter, 30)
      .put(lr.regParam -> 0.1, lr.threshold -> 0.55)
    //ParamMap合并
    /* val paramMap2 = paramMap(lr.probabilityCol -> "myProbability")
     val paramMapCombined = paramMap ++ paramMap2*/
 
    //6、训练模型,采用ParamMap参数
    //paramMapCombined会覆盖所有lr.set设置的参数
    //val model2 = lr.fit(training, paramMapCombined)
    val model2 = lr.fit(training, paramMap)
 
    //7、测试样本
    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")
 
    //8、对模型进行测试
    model2.transform(test)
      .select("features", "label", "prediction")
      .collect()
      .foreach{
        case Row(features: Vector, label: Double, prediction: Double) =>
          println(s"($features. $label) -> prediction=$prediction")
      }
  }
   
}

结果如下所示:

([-1.0,1.5,1.3]. 1.0) -> prediction=1.0
([3.0,2.0,-0.1]. 0.0) -> prediction=0.0
([0.0,2.2,-1.5]. 1.0) -> prediction=1.0

2、实例:pipeline

package sparkml
 
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.Row
import org.apache.spark.ml.linalg.Vector
 
/**
  * 实例:Pipeline
  */
 
object Pipeline2 {
  def main(args: Array[String]): Unit = {
    val spark = SparkSession.builder()
      .appName("pipeline2")
      .master("local[*]")
      .getOrCreate()
 
    import spark.implicits._
 
    //1.训练样本
    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")
 
    //2.ML pipeline参数设置,包括三个过程:首先是tokenizer,然后是hashingTF,最后是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))
 
    //3.训练pipeline模型
    val model = pipeline.fit(training)
 
    //4.保存pipeline模型
    // model.write.overwrite().save("E://temp//one")
 
    //5.保存pipeline
    // pipeline.write.overwrite().save("E://temp//two")
 
    //6.加载pipeline模型
    //val sameModel = PipelineModel.load("E://temp//one")
 
    //7.测试样本
    val test = spark.createDataFrame(Seq(
      (4L, "spark i j k"),
      (5L, "l m n"),
      (6L, "spark hadoop spark"),
      (7L, "apache hadoop")
    )).toDF("id", "text")
 
    //8.模型测试
    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")
      }
  }
 
}

结果如下所示:

(4. spark i j k) --> prob=[0.15964077387874118,0.8403592261212589], prediction=1.0
(5. l m n) --> prob=[0.8378325685476612,0.16216743145233875], prediction=0.0
(6. spark hadoop spark) --> prob=[0.06926633132976273,0.9307336686702373], prediction=1.0
(7. apache hadoop) --> prob=[0.9821575333444208,0.01784246665557917], prediction=0.0

3、实例:处理文本数据

文本内容如下所示:
在这里插入图片描述
测试代码如下:

package sparkml
 
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.classification.LogisticRegression
import org.apache.spark.ml.feature.{HashingTF, Tokenizer}
import org.apache.spark.sql.SparkSession
import org.apache.spark.sql.functions._
import org.apache.spark.sql.Row
import org.apache.spark.ml.linalg.Vector
 
/**
  * 实例:处理文本数据
  */
 
object Pipeline3 {
  def main(args: Array[String]): Unit = {
    //设置日志输出格式
    Logger.getLogger("org").setLevel(Level.WARN)
 
    //1.定义SparkSession
    val spark = SparkSession.builder()
      .appName("textdata")
      .master("local[*]")
      .getOrCreate()
 
    import spark.implicits._
 
    //2.读取数据
    val data = spark.read.format("csv")
      .option("delimiter", "|")
      .option("header", "true")
      .load("C://Users//Machenike//Desktop//xzw//doc_class.dat")
      .toDF("myapp_id", "typenameid", "typename", "myapp_word", "myapp_word_all")
 
    val toDouble = udf[Double, String](_.toDouble)
    val clearData = udf[String, String](_.replace(" ", ""))
    val data2 = data.withColumn("label", toDouble(data("typenameid")))
      .withColumn("myapp_word_all", clearData(data("myapp_word_all")))
      .withColumnRenamed("myapp_word_all", "text")
      .withColumnRenamed("myapp_id", "id")
      .select("id", "text", "label")
 
    //3.将数据分为训练数据和测试数据
    val splitData = data2.randomSplit(Array(0.8, 0.2))
    val training = splitData(0)  //训练数据
    val testing = splitData(1)  //测试数据
 
    //4.tokenizer、hashingTF、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)
    //对tokenizer、hashingTF、lr进行封装
    val pipeline = new Pipeline()
      .setStages(Array(tokenizer, hashingTF, lr))
 
    //5.训练模型
    val model = pipeline.fit(training)
 
    //6.保存pipeline模型
    //model.write.overwrite().save("E://temp//one")
 
    //7.保存pipeline
    //pipeline.write.overwrite().save("E://temp//two")
 
    //8.加载pipeline模型
    //val sameModel = PipelineModel.load("E://temp//one")
 
    //9.测试
    val testing2 = testing.select("id", "text")
    model.transform(testing2)
      .select("id", "text", "probability", "prediction")
      .collect()
      .take(5)
      .foreach{
        case Row(id: String, text: String, prob: Vector, prediction: Double) =>
          println(s"($id, $text) --> prob = $prob, prediction = $prediction")
      }
  }
 
}

结果如下图所示:(结果数据较多,此处只截取部分结果)
在这里插入图片描述

发布了334 篇原创文章 · 获赞 227 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/BeiisBei/article/details/104839095