Spark版本:2.4.0
語言:Scala
任務:分類
這里對數據的處理步驟如下:
- 載入數據
- 歸一化
- PCA降維
- 劃分訓練/測試集
- 線性SVM分類
- 驗證精度
- 輸出cvs格式的結果
前言
從Spark 2.0開始,Spark機器學習API是基于DataFrame的spark.ml。而之前的基于RDD的API spark.mllib已進入維護模式。
也就是說,Spark ML是Spark MLlib的一種新的API,它主要有以下幾個優點:
- 面向DataFrame,在RDD基礎上進一步封裝,提供更強大更方便的API
- Pipeline功能,便于實現復雜的機器學習模型
- 性能提升
基于Pipeline的Spark ML中的幾個概念:
- DataFrame:從Spark SQL 的引用的概念,表示一個數據集,它可以容納多種數據類型。例如可以存儲文本,特征向量,標簽和預測值等
- Transformer:是可以將一個DataFrame變換成另一個DataFrame的算法。例如,一個訓練好的模型是一個Transformer,通過transform方法,將原始DataFrame轉化為一個包含預測值的DataFrame
- Estimator:是一個算法,接受一個DataFrame,產生一個Transformer。例如,一個學習算法(如PCA,SVM)是一個Estimator,通過fit方法,訓練DataFrame并產生模型Transformer
- Pipeline: Pipeline將多個Transformers和Estimators連接起來組合成一個機器學習工作流程
- Parameter:用于對Transformers和Estimators指定參數的統一接口
本次實驗使用的是Spark ML的API
首先要創建SparkSession
// 創建SparkSession
val spark = SparkSession
.builder
.appName("LinearSVCExample")
.master("local")
.getOrCreate()
數據處理步驟
1 載入數據
數據載入的方式有多種,這里使用libsvm格式的數據作為數據源,libsvm格式常被用來存儲稀疏的矩陣數據,它每一行的格式如下:
label index1:value1 index2:value2 ...
第一個值是標簽,后面是由“列號:值”組成鍵值對,只需要記錄非0項即可。
數據加載使用load方法完成:
// 加載訓練數據,生成DataFrame
val data = spark.read.format("libsvm").load("data/sample_libsvm_data.txt")
2 歸一化
作為數據預處理的第一步,需要對原始數據做歸一化處理,即把原始數據的每一維減去其平均值,再除以其標準差,使得數據總體分布為以0為中心,且標準差為1。
// 歸一化
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithMean(true)
.setWithStd(true)
.fit(data)
val scaleddata = scaler.transform(data).select("label", "scaledFeatures").toDF("label","features")
3 PCA降維
有時數據的維數可能很大,直接進行分類不僅計算量很大,而且對數據量的要求也很高,常常會出現過擬合。因此需要進行降維,常用的是主成分分析(PCA)算法。
// 創建PCA模型,生成Transformer
val pca = new PCA()
.setInputCol("features")
.setOutputCol("pcaFeatures")
.setK(5)
.fit(scaleddata)
// transform數據,生成主成分特征
val pcaResult = pca.transform(scaleddata).select("label","pcaFeatures").toDF("label","features")
4 劃分訓練/測試集
經過降維的數據就可以拿來訓練分類器了,但是在此之前要將數據劃分為訓練集和測試集,分類器只能在訓練集上進行訓練,在測試集上驗證其分類精度。Spark提供了很方便的接口,按給定的比例隨機劃分訓練/測試集。
// 將經過主成分分析的數據,按比例劃分為訓練數據和測試數據
val Array(trainingData, testData) = pcaResult.randomSplit(Array(0.7, 0.3), seed = 20)
5 線性SVM分類
這一步構建線性SVM模型,設置最大迭代次數和正則化項的系數,使用訓練集進行訓練。
// 創建SVC分類器(Estimator)
val lsvc = new LinearSVC()
.setMaxIter(10)
.setRegParam(0.1)
// 訓練分類器,生成模型(Transformer)
val lsvcModel = lsvc.fit(trainingData)
6 驗證精度
將訓練好的分類器作用于測試集上,獲得分類結果。
分類結果的好壞有很多種衡量的方法,如查準率、查全率等,這里我們使用最簡單的一種衡量標準——精度,即正確分類的樣本數占總樣本數的比值。
// 用訓練好的模型,驗證測試數據
val res = lsvcModel.transform(testData).select("prediction","label")
// 計算精度
val evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
.setMetricName("accuracy")
val accuracy = evaluator.evaluate(res)
println(s"Accuracy = ${accuracy}")
7 輸出cvs格式的結果
Spark的DataFrame類型支持導出多種格式,這里以常用的csv格式為例。
這里輸出的目的是為了使用Python進行可視化,在降維后進行,可以直觀的看出降維后的數據是否明顯可分。
使用VectorAssembler,將標簽與特征合并為一列,再進行輸出。
(這里是將合并后的列轉換為String再輸出的,因此輸出的csv文件是帶有引號和括號的,至于為什么要這樣輸出,請看第二部分)
// 將標簽與主成分合成為一列
val assembler = new VectorAssembler()
.setInputCols(Array("label","features"))
.setOutputCol("assemble")
val output = assembler.transform(pcaResult)
// 輸出csv格式的標簽和主成分,便于可視化
val ass = output.select(output("assemble").cast("string"))
ass.write.mode("overwrite").csv("output.csv")
當然也可以用同樣的方法輸出訓練/預測的結果,這里就不再詳細介紹。
遇到的問題
完成這個簡單的分類實驗,花了我兩天多的時間,從配置環境到熟悉API,再到遇見各種奇怪的問題……這里我都把他們記錄下來,供以后參考。
1 配置環境
起初,我想通過在本機編寫代碼,然后訪問安裝在虛擬機中的Spark節點(單節點)這種方式進行實驗的(不是提交jar包然后執行spark-submit),也就在是創建SparkSession時,指定虛擬機中的Spark:
val spark = SparkSession
.builder
.appName("LinearSVCExample")
.master("spark://192.168.1.128:7077") // 虛擬機IP
.getOrCreate()
然而,這樣并沒有成功。遇到的問題有:
- 拒絕連接
- Spark的worker里可以查看到提交的任務,但是一直處于等待狀態,沒有響應。并且提示:
WARN TaskSchedulerImpl: Initial job has not accepted any resources; check your cluster UI to ensure that workers are registered and have sufficient resources
(實際上,內存和CPU是夠的) - 報錯
RuntimeException: java.io.EOFException......
在嘗試過各種方案都沒有解決問題之后,我放棄了,最后還是在本機中安裝Spark,在local模式下運行。(如果有同學成功實現上面的訪問方法,歡迎留言告訴我~)
至于如何在本機(Windows)安裝Spark,百度搜索即可
2 導出CSV格式的數據
將DataFrame導出為cvs格式的時候,遇到了這個問題:
java.lang.UnsupportedOperationException: CSV data source does not support struct<type:tinyint,size:int,indices:array<int>,values:array<double>> data type.
而我要導出的DataFrame只是一個多行數組而已啊:
根據StackOverflow上面的提問,Spark的csv導出不支持復雜結構,array<double>都不行。
然后有人給了一種辦法,把數組轉化為String,就可以導出了。
但是導出的結果是這樣的:
需要進一步處理。
所以還不如手動實現導出csv文件,或者你有更好的辦法,歡迎留言告訴我,非常感謝~
3 PCA維數限制
當我想跑一個10萬維度的數據時,程序運行到PCA報錯:
java.lang.IllegalArgumentException: Argument with more than 65535 cols: 109600
原來,Spark ML的PCA不支持超過65535維的數據。參見源碼
4 SVM核
翻閱了Spark ML文檔,只找到Linear Support Vector Machine,即線性核的支持向量機。對于高斯核和其他非線性的核,Spark ML貌似還沒有實現。
5 withColumn操作
起初我認為對數據進行降維前,需要把DataFrame中的標簽label與特征feature分開,然后對feature進行降維,再使用withColumn方法,把label與降維后的feature組合成新的DataFrame。
發現這樣既不可行也沒有必要。
首先,withColumn只能添加當前DataFrame的數據(對DataFrame某一列進行一些操作,再添加到這個DataFrame本身),不能把來自于不同DataFrame的Column添加到當前DataFrame中。
其次,PCA降維時,只需指定InputCoulum作為特征列,指定OutputColumn作為輸出列,其他列的存在并不影響PCA的執行,PCA也不會改變它們,在新生成的DataFrame中依然會保留原來所有Column,并且添加上降維后的數據Column,后面再使用select方法選擇出所需的Column即可。
完整代碼(Pipeline版)
import org.apache.log4j.{Level, Logger}
import org.apache.spark.ml.Pipeline
import org.apache.spark.ml.feature._
import org.apache.spark.ml.evaluation.{BinaryClassificationEvaluator, MulticlassClassificationEvaluator}
import org.apache.spark.ml.feature.PCA
import org.apache.spark.ml.classification.LinearSVC
import org.apache.spark.sql.SparkSession
object Hello {
def main(args: Array[String]) {
System.setProperty("hadoop.home.dir", "D:\\hadoop-2.8.3")
// 屏蔽日志
Logger.getLogger("org.apache.spark").setLevel(Level.WARN)
Logger.getLogger("org.eclipse.jetty.server").setLevel(Level.OFF)
// 創建sparkSession
val spark = SparkSession
.builder
.appName("LinearSVCExample")
.master("local")
.getOrCreate()
// 加載訓練數據,生成DataFrame
val data = spark.read.format("libsvm").load("data/sample_libsvm_data.txt")
println(data.count())
// 歸一化
val scaler = new StandardScaler()
.setInputCol("features")
.setOutputCol("scaledFeatures")
.setWithMean(true)
.setWithStd(true)
.fit(data)
val scaleddata = scaler.transform(data).select("label", "scaledFeatures").toDF("label","features")
// 創建PCA模型,生成Transformer
val pca = new PCA()
.setInputCol("features")
.setOutputCol("pcaFeatures")
.setK(5)
.fit(scaleddata)
// transform 數據,生成主成分特征
val pcaResult = pca.transform(scaleddata).select("label","pcaFeatures").toDF("label","features")
// pcaResult.show(truncate=false)
// 將標簽與主成分合成為一列
val assembler = new VectorAssembler()
.setInputCols(Array("label","features"))
.setOutputCol("assemble")
val output = assembler.transform(pcaResult)
// 輸出csv格式的標簽和主成分,便于可視化
val ass = output.select(output("assemble").cast("string"))
ass.write.mode("overwrite").csv("output.csv")
// 將經過主成分分析的數據,按比例劃分為訓練數據和測試數據
val Array(trainingData, testData) = pcaResult.randomSplit(Array(0.7, 0.3), seed = 20)
// 創建SVC分類器(Estimator)
val lsvc = new LinearSVC()
.setMaxIter(10)
.setRegParam(0.1)
// 創建pipeline, 將上述步驟連接起來
val pipeline = new Pipeline()
.setStages(Array(scaler, pca, lsvc))
// 使用串聯好的模型在訓練集上訓練
val model = pipeline.fit(trainingData)
// 在測試集上測試
val predictions = model.transform(testData).select("prediction","label")
// 計算精度
val evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction")
.setMetricName("accuracy")
val accuracy = evaluator.evaluate(predictions)
println(s"Accuracy = ${accuracy}")
spark.stop()
}
}
最后的精度為1.0,這里使用的測試數據比較好分,從PCA后對前兩維的可視化結果可以看出:
參考資料
Spark ML文檔
DataFrame API
PCA列數限制-源碼
導出cvs文件方法-stackoverflow
無法導出csv文件-stackoverflow
示例數據