• Spark MLlib框架详解


    1. 概述

    1.1 功能

      MLlib是Spark的机器学习(machine learing)库,其目标是使得机器学习的使用更加方便和简单,其具有如下功能:

    • ML算法:常用的学习算法,包括分类、回归、聚类和过滤;
    • 特征:特征萃取、转换、降维和选取;
    • Pipelines:其是一个工具,目标是用于构建、测量和调节;
    • 使用工具:包括线性代数、统计学习和数据操作等等。

    1.2 API架包

    MLlib有两个API架包:

      1) Spark.mllib:基于RDDAPI包,在spark 2.0时已经进入维护模型
      2) Spark.ml:基于DataFrameAPI包,目前Spark官方首推使用该包。

    2. Pipelines Components

    MLlib标准化机器学习算法的API,使得更容易将多个算法组合成到单个管道(工作流)。其设计思想是受到Scikit-learn项目的启发。

    • DataFrameMLlib的数据使用Spark SQL中的DataFrame结构来存储,即用户的数据集和模型的输出标签都是以此结构存储,包括Pipeline内部数据的传输都是以此结构存储;
    • TransformerMLlib将算法模型用Transformer结构来表示,其以一个DataFrame数据作为输入,通过模型计算后转换为一个DataFrame数据;
    • EstimatorEstimator结构也表示一种算法,但其以一个DataFrame数据作为输入,通过模型计算后转换为一个Transformer对象,而不是DataFrame数据;
    • PipelineMLlib使用Pipeline来组织多个ML模型,即其内部有多个TransformerEstimator对象,从而组成一个算法工作流;
    • ParameterMLlib使用Parameter结构来存储参数,用户通过这些参数来配置和调节模型。即在一个Pipeline对象内的所有TransformerEstimator对象都共享一个Parameter对象。

    2.1 DataFrame

      机器学习中数据集是由一个个样本组成,而每个样本其实是一条有多个特征组成的记录,从而数据集其实是一个矩阵结构。而Spark SQL中的DataFrame结构也拥有类似的结构,DataFrame内部有一行行的数据Row组成,每个Row对象内部也可以由多个属性组成。从而MLlib使用DataFrame来描述机器学习中的数据集正好不过了。

      Spark SQL的DataFrame其实一种Dataset类型,只是存储的是Row元素,如下Spark源码所示:

    Package object sql{

    ……

    type DataFrame = Dataset[Row]

    }

    2.2 Pipeline

      MLlib使用Pipeline来组织多个ML模型,其内部有多个Transformer和Estimator对象,从而组成一个算法工作流。在Spark ML中与Pipeline相关联的类如图 1所示。从图中可明显看出Transformer和Estimator都是PipelineStage抽象类的子类;并且Pipeline类内部有一个stages数组来存储PipelineStage对象,即存放Transformer和Estimator对象;当用户调用Pipeline的fit()方法时,将产生一个PipelineModel对象;PipelineModel类有一个transform()方法能返回一个DataFrame对象。

    图 1  

    3. 工作机制

      Pipeline是由一系列stage组成,这些stage有两种类型:Transformer和Estimator。Stage在Pipeline的运行是有序的,而且输入的DataFrame会在stage中被转换和传递。若stage是Transformer类型,则对条用Transformer对象的transform()方法将输入的DataFrame转换为另一种DataFrame;若stage是Estimator类型,则会调用Estimator对象的fit()方法产生Transformer对象,调用该Transformer对象的transform()方法一样会产生一个DataFrame。

    可以将上述这一段,详细解释为两个过程:模型训练和模型预测,如下所示:

    3.1 模型训练

      Pipeline对象内部有一个stages容器,存放多个Transformer对象和一个Estimator对象。当用户调用Pipeline对象的fit()方法时,会接收输入的DataFrame,然后在这些stage中被转换和传递。当传递到最后一个stage(Estimator对象)时,将生成一个PipelineModel对象(Transformer子类),如图 2所示。

     

    图 2

      用户调用上图中Pipeline的fit()时,会将stages容器存放的所有Transformer对象和Estimator对象生成的Transformer对象都添加到PipelineModel对象中,该对象有一个stages容器(Array[Transformer]类型),其能够存放Transformer对象。

        通过Spark源码,可以查看Pipeline类中的fit()内容如下所示:

    override def fit(dataset: Dataset[_]): PipelineModel = {

    transformSchema(dataset.schema, logging = true)

    val theStages = $(stages)

    var curDataset = dataset

    val transformers = ListBuffer.empty[Transformer]

    theStages.view.zipWithIndex.foreach { case (stage, index) =>

    if (index <= indexOfLastEstimator) {

    val transformer = stage match {

    case estimator: Estimator[_] =>//若是Estimator对象,则调用fit()方法生成一个Transformer

    estimator.fit(curDataset)

    case t: Transformer =>//若是Transformer对象,则直接返回

    t

    case _ =>

    throw new IllegalArgumentException(

    s"Does not support stage $stage of type ${stage.getClass}")

    }

    if (index < indexOfLastEstimator) {

    curDataset = transformer.transform(curDataset)//如果不是最后的对象,则调用transformer对象的transform方法,生成一个DataFrame

    }

    transformers += transformer //将生成的所有Transformer对象都添加到一个list

    } else {

    transformers += stage.asInstanceOf[Transformer]

    }

    }

     new PipelineModel(uid, transformers.toArray).setParent(this) //最后创建PipelineModel对象,并传递上述的Transformer列表。

    }

    3.2 模型预测

      在模型训练阶段会通过向Pipeline的fit()方法传递DataFrame数据来训练模型,从而生成一个PipelineModel对象(Transformer子类),该对象内部有一个stages容器,存放了所有Transformer对象。

      当进行模型预测时,即通过向PipelineModel对象的transform传递一个DataFrame数据来预测时,会依序调用其stages容器中的Transformer对象,每个Transformer对象都有一个DataFrame输入和一个DataFrame的输出,最后生成一个DataFrame作为用户的输出,如图 3所示。

     

    图 3

    类似,可以查看PipelineModel对象的transform()方法,如下所示:

    override def transform(dataset: Dataset[_]): DataFrame = {

    transformSchema(dataset.schema, logging = true)

    stages.foldLeft(dataset.toDF)((cur, transformer) => transformer.transform(cur))

    }

      stages.foldLeft(dataset.toDF)((cur, transformer) => transformer.transform(cur))语句正是图 3的实现,即第一次输入数据是dataset.toDF,然后每次调用transformer.transform(cur))方法,产生的DataFrame输出作为下一次的输入。

    3.3 关系总结

      通过上述Pipeline工作机制的分析,现在从机器学习的角度总结一下Pipeline、Transformer和Estimator三者之间的关系,如图 4所示。

     

    图 4

    1. Transformer:是对数据进行预处理,如特征向量萃取、向量转换或降维;
    2. Estimator:机器学习的某种算法,如线性回归、贝叶斯或支持向量机;
    3. Pipeline:是一种算法组织者,将TransformerEstimator组织成有序的执行过程。

    4. Examples

    4.1 Estimator、Transformer和Param

      本节以Estimator类为例,没有使用Pipeline结构来组织Estimator和Transformer对象。Estimator类可以单独使用,不需要Pipeline结构也能工作,此时Estimator类似Scikit-learn框架。首先,用户直接调用Estimator对象的fit()方法来训练数据;然后,根据fit()方法返回的Transformer对象,用户接着调用Transformer的transform()方法来预测或测试;

    如下所示的完整程序:

    // scalastyle:off println

    package org.apache.spark.examples.ml

    // $example on$

    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

    // $example off$

    import org.apache.spark.sql.SparkSession

    object EstimatorTransformerParamExample {

    def main(args: Array[String]): Unit = {

    val spark = SparkSession

    .builder

    .appName("EstimatorTransformerParamExample")

    .getOrCreate()

    // $example on$

    // 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: " + lr.explainParams() + " ")

    // 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")

    }

    // $example off$

    spark.stop()

    }

    }

      其实Estimator类的单独使用,也可以理解为Pipeline对象只有一个Estimator对象。上述的程序来自:srcmainscalaorgapachesparkexamplesml ElementwiseProductExample.scala

    4.2 Pipeline

      输入的Dataframe经过PipelineStage对象处理后悔输出新的DataFrame,此时输出的DataFrame会增加一些列,即增加了一些特征,而具体增加什么列,需要看具体是什么PipelineStage对象。

    如下所示,输入DataFrame只有三列"id"、"text"、"label",但输出DataFrame不仅保存了输入列,同时增加了一些列。

    package org.apache.spark.examples.ml

    // $example on$

    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

    // $example off$

    import org.apache.spark.sql.SparkSession

    object PipelineExample {

    def main(args: Array[String]): Unit = {

    val spark = SparkSession

    .builder

    .appName("PipelineExample")

    .getOrCreate()

    // $example on$

    // 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.

    //Tokenizer功能是对输入的DataFrame某一列进行分割,分割后将数据添加到DataFrame的新列种

    val tokenizer = new Tokenizer()

    .setInputCol("text") //设置输入DataFrame中要处理的列名字

    .setOutputCol("words") //设置输出的DataFrame中增加列的名字

    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")

    }

    // $example off$

    spark.stop()

    }

    }

    5. 参考文献

    [1]. Spark MLlib
  • 相关阅读:
    android 开机启动
    android 获取lanucher 列表
    原创高端影楼人像专业磨皮法教程详解 附PSD源码
    [转]在SQLPLUS启动和停止Oracle数据库
    挑印刷时间最新的地图!
    Eclipse3.2下进行ArcGIS Server 9.2 Java WebADF开发手记 Eclipse使用技巧
    [藏]常用的匹配正则表达式和实例
    [藏]C# 中的常用正则表达式总结
    [转]使用uDig制作geoserver中需要的style
    [转]geoserver与OpenLayers配置入门
  • 原文地址:https://www.cnblogs.com/huliangwen/p/7491797.html
Copyright © 2020-2023  润新知