使用Spark ML進行數據分析

Spark版本:2.4.0
語言:Scala
任務:分類

這里對數據的處理步驟如下:

  1. 載入數據
  2. 歸一化
  3. PCA降維
  4. 劃分訓練/測試集
  5. 線性SVM分類
  6. 驗證精度
  7. 輸出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只是一個多行數組而已啊:

image.png

根據StackOverflow上面的提問,Spark的csv導出不支持復雜結構,array<double>都不行。

然后有人給了一種辦法,把數組轉化為String,就可以導出了。

但是導出的結果是這樣的:

image.png

需要進一步處理。

所以還不如手動實現導出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貌似還沒有實現。

image.png

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后對前兩維的可視化結果可以看出:

image.png

參考資料

Spark ML文檔
DataFrame API
PCA列數限制-源碼
導出cvs文件方法-stackoverflow
無法導出csv文件-stackoverflow
示例數據

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380