Spark ML Pipelines
ML管道
管道的主要概念
MLlib對用於機器學習算法的API進行了標準化,從而使將多種算法組合到單個管道或工作流中變得更加容易。
-DataFrame
:此ML API使用DataFrameSpark SQL作爲ML數據集,可以保存各種數據類型。例如,一個DataFrame可能有不同的列,用於存儲文本,特徵向量,真實標籤和預測。
-
Transformer
:一個Transformer是一種算法,其可以將一個DataFrame到另一個DataFrame。例如,ML模型是一種Transformer將DataFrame具有特徵的a轉換爲DataFrame具有預測的a的模型。 -
Estimator
:An Estimator是一種算法,可以適合DataFrame產生Transformer。例如,學習算法是在上Estimator進行訓練DataFrame並生成模型的算法。 -
Pipeline
:將Pipeline多個Transformer和鏈接Estimator在一起以指定ML工作流程。 -
Parameter
:所有Transformer和Estimator現在共享一個用於指定參數的通用API。
DataFrame
Machine learning可以應用於多種數據類型,例如矢量,文本,圖像和結構化數據。該API採用Spark SQL中的DataFrame,以支持多種數據類型。
DataFrame支持許多基本類型和結構化類型。請參考Spark SQL數據類型參。除了Spark SQL指南中列出的類型之外,DataFrame還可使用ML Vector類型。
可以從常規RDD隱式或顯式創建DataFrame。
命名DataFrame中的列。下面的代碼示例使用諸如“文本”,“功能”和“標籤”之類的名稱。
Pipeline components(管道組件)
Transformers(轉換器)
Transformers
是一種抽象,其中包括特徵轉換器和學習的模型。從技術上講,Transformer實現了transform()
方法,該方法通常通過附加一個或多個列將一個DataFrame轉換爲另一個DataFrame。例如:
-
特徵轉換器可以獲取一個DataFrame,讀取一列(例如:文本),將其映射到一個新列(例如:特徵向量),然後輸出一個新的DataFrame並附加映射的列。
-
學習模型可能需要一個DataFrame,讀取包含特徵向量的列,預測每個特徵向量的標籤,然後輸出帶有預測標籤的新DataFrame作爲列添加。
Estimators(估算器)
一個Estimator抽象學習算法的概念或算法適合或數據串。從技術上講,Estimator實現是一種方法fit(),該方法接收一個DataFrame併產生一個 Model,即一個Transformer。例如,學習算法(例如爲LogisticRegression)Estimator和調用 fit()訓練一個LogisticRegressionModel,即爲Model,因此爲Transformer。
Properties of pipeline components(管道組件屬性)
Transformer.transform()和Estimator.fit()都是無狀態的。將來,可通過替代概念來支持有狀態算法。
每個Transformer或Estimator實例都有一個唯一的ID,該ID在指定參數中很有用。
Pipeline(管道)
在machine learning中,通常需要運行一系列算法來處理數據並從中學習。例如,簡單的文本文檔處理工作流程可能包括幾個階段:
- 將每個文檔的文本拆分爲單詞。
- 將每個文檔的單詞轉換成數字特徵向量。
- 使用特徵向量和標籤學習預測模型。
MLlib將這樣的工作流表示爲“Pipeline”,它由要按特定順序運行的一系列PipelineStages
(Transformer和Estimator
)組成。
工作流程
Pipeline被指定爲階段序列,每個階段可以是一個Transformer
或Estimator
。這些階段按順序運行,並且輸入DataFrame在通過每個階段時都會進行轉換。對於Transformer
階段,在DataFrame上調用transform()方法
。對於Estimator
階段,調用fit()
方法以生成一個Transformer(它將成爲PipelineModel
或已擬合Pipeline
的一部分),並且在DataFrame上調用該Transformer的transform()
方法。
下圖爲簡單的文本文檔工作流程管道的培訓時間使用情況。
上圖的第一行代表Pipeline的三個階段。前面兩個藍色區域(Tokenizer
和HashingTF
)爲Transformers
,第三個(LogisticRegression
)是Estimator
。第二行表示流經管道的數據,其中第一個表示DataFrames。Pipeline.fit()
在原始DataFrame文件上調用此方法,原始文件包含原始文本文檔和標籤。該Tokenizer.transform()
方法將原始文本文檔拆分爲單詞,然後向添加帶有單詞的新列DataFrame。該HashingTF.transform()
方法將words列轉換爲特徵向量,並將帶有這些向量的新列添加到DataFrame。現在,由於LogisticRegression爲Estimator,因此Pipeline第一個調用LogisticRegression.fit()
產生一個LogisticRegressionModel
。如果管道中有更多Estimator,則在將DataFrame傳遞到下一階段之前,將在DataFrame上調用LogisticRegressionModel的transform()
方法。
當Pipeline只有Estimator,因此,運行Pipeline的fit()方法後,它會生成PipelineModel,它是一個Transformer。該PipelineModel在測試時使用,用法如下圖。
在上圖中,PipelineModel具有與原始Pipeline相同的階段數,但是原始Pipeline中的所有Estimator都已變爲Transformers。在測試數據集上調用PipelineModel的transform()
方法時,數據將按順序通過擬合的管道。每個階段的transform()
方法都會更新數據集,並將其傳遞到下一個階段。
Pipelines 和 PipelineModels有助於確保訓練和測試數據經過相同的特徵處理步驟。
詳細
-
DAG Pipeline
:Pipeline的階段被指定爲有序數組。此處給出的所有示例均適用於線性管道,即每個階段使用前一階段產生的數據的管道。只要數據流圖形成有向無環圖
(DAG),就可以創建非線性管道。當前基於每個階段的輸入和輸出列名稱(通常指定爲參數)隱式指定該圖。如果管道形成DAG,則必須按拓撲順序指定階段。 -
運行時檢查
:由於管道可以在具有各種類型的DataFrame上運行,因此它們不能使用編譯時類型檢查。Pipelines和PipelineModels會在實際運行Pipeline之前進行運行時檢查。此類型檢查使用DataFrame架構完成,該架構是對DataFrame中列的數據類型的描述。 -
唯一的Pipeline階段
:管道的階段應該是唯一的實例。例如,同一實例myHashingTF不應兩次插入到管道中,因爲管道階段必須具有唯一的ID。但是,可以將不同的實例myHashingTF1和myHashingTF2(均爲HashingTF類型)放置到同一管道中,因爲將使用不同的ID創建不同的實例。
參數
MLlib Estimators和Transformers使用統一的API來指定參數。
參數是具有獨立文件的命名參數。 ParamMap是一組(參數, 值)對。
將參數傳遞給算法的主要方法有兩種:
- 設置實例的參數。例如,如果lr是LogisticRegression的實例,則可以調用
lr.setMaxIter(10)
以使lr.fit()
最多使用10次迭代。該API與spark.mllib
軟件包中使用的API相似。 - 將ParamMap傳遞給
fit()
或transform()
。 ParamMap中的任何參數都將覆蓋以前通過setter
方法指定的參數。
參數屬於Estimators和Transformers的特定實例。例如,如果我們有兩個LogisticRegression實例lr1和lr2,則可以使用指定的兩個maxIter參數構建ParamMap:ParamMap(lr1.maxIter-> 10, lr2.maxIter-> 20)
。如果Pipeline中有兩個算法的maxIter
參數,這種方法就很適用。
ML持久性:Saving and Loading Pipelines
ML 通常將模型或管道保存到磁盤以供以後使用。在Spark 1.6
中,模型導入/導出功能已添加到管道API。從Spark 2.3
開始,spark.ml和pyspark.ml中基於DataFrame的API已有完整介紹。
ML持久性適用於Scala,Java和Python。但是,R當前使用修改後的格式,因此保存在R中的模型只能重新加載到R中。R語言用戶可等待後續官方修復。
持久性的向後兼容
通常,MLlib爲ML持久性保持向後兼容性。也就是說,如果您將ML模型或管道保存在一個版本的Spark中,則應該能夠將其重新加載並在以後的Spark版本中使用。但是,有極少數例外,如下所述。
模型持久性
:是否可以通過Y版本的Spark加載在Apache X版本中使用Apache Spark ML持久性保存的模型或管道?
主要版本
:無保證,但盡力而爲。次要版本和補丁程序版本
:是,這些是向後兼容的。關於格式的注意事項
:不能保證穩定的持久性格式,但是模型加載本身被設計爲向後兼容。
模型行爲
:Spark版本X中的模型或管道在Spark版本Y中的行爲是否相同?
主要版本
:無保證,但盡力而爲。次要版本和修補程序版本
:除錯誤修復外,行爲相同。
對於模型持久性和模型行爲,Spark版本發行說明中都會報告次要版本或修補程序版本中的所有重大更改。如果發行說明中未報告損壞,則應將其視爲要修復的錯誤。
代碼示例
Estimator, Transformer, and Param
def main(args: Array[String]): Unit = {
// 屏蔽日誌
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.jetty.server").setLevel(Level.OFF)
val spark = SparkSession
.builder()
.master("local[*]")
.appName(Demo02.getClass.getName)
.getOrCreate()
// 從(標籤, 特徵)元組列表準備訓練數據
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")
// 創建一個LogisticRegression實例。該實例是一個Estimator.
val lr = new LogisticRegression()
// 打印出參數,文檔和任何默認值
println(s"LogisticRegression parameters:\n ${lr.explainParams()}\n")
// 可以使用setter方法設置參數
lr.setMaxIter(10)
.setRegParam(0.01)
// 瞭解LogisticRegression模型。這使用存儲在lr中的參數
val model1 = lr.fit(training)
// 由於model1是模型(即Estimator生產的Transformer)
// 我們可以查看它在fit()中使用的參數。
// 打印參數(名稱:值)對,其中名稱是爲此的唯一ID
// LogisticRegression 實例.
println(s"Model 1 was fit using parameters: ${model1.parent.extractParamMap}")
// 使用ParamMap指定參數,
// 支持幾種制定參數的方法.
val paramMap = ParamMap(lr.maxIter -> 20)
.put(lr.maxIter, 30) //指定一個參數, 這樣就會覆蓋之前的maxIter.
.put(lr.regParam -> 0.1, lr.threshold -> 0.55) // Specify multiple Params.
// 也可以綜合使用 ParamMaps.
val paramMap2 = ParamMap(lr.probabilityCol -> "myProbability") // Change output column name.
val paramMapCombined = paramMap ++ paramMap2
// paramMapCombined 參數的新模型
// paramMapCombined 會覆蓋之前的所有參數.
val model2 = lr.fit(training, paramMapCombined)
println(s"Model 2 was fit using parameters: ${model2.parent.extractParamMap}")
// 測試數據.
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")
// 使用Transformer.transform() 方法對數據進行預測.
// LogisticRegression.transform 應用到'features'列.
// model2.transform() 輸出 'myProbability'
// 因爲我們之前已重命名了lr.probabilityCol參數,所以使用了'probability'列
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")
}
}
Pipeline
def main(args: Array[String]): Unit = {
// 屏蔽日誌
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.apache.jetty.server").setLevel(Level.OFF)
val spark = SparkSession
.builder()
.master("local[*]")
.appName(Demo02.getClass.getName)
.getOrCreate()
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
// 從 (id, text, label) 元組列表準備培訓文檔.
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管道,該管道包括三個階段: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))
// 使pipeline適合培訓文檔
val model = pipeline.fit(training)
// 選擇將已擬合的管道保存到磁盤
model.write.overwrite().save("/Users/mashikang/IdeaProjects/spark-mllib/src/main/resources/spark-logistic-regression-model")
// 將不合適的管道保存到磁盤
pipeline.write.overwrite().save("/Users/mashikang/IdeaProjects/spark-mllib/src/main/resources/unfit-lr-model")
// 在生產期間將其加載回
val sameModel = PipelineModel.load("/Users/mashikang/IdeaProjects/spark-mllib/src/main/resources/spark-logistic-regression-model")
// 準備未標記(id,text)元組的測試文檔
val test = spark.createDataFrame(Seq(
(4L, "spark i j k"),
(5L, "l m n"),
(6L, "spark hadoop spark"),
(7L, "apache hadoop")
)).toDF("id", "text")
// 對測試文件進行預測
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")
}
}