基於Spark的SVM模型手寫數字識別
資料集簡介
MNIST手寫數字資料集官網:
或者資料集下載網址:http://yann.lecun.com/exdb/mnist/
共有4個數據集,下載之後是4個gz壓縮包,把它們儲存在磁碟中: train-images-idx3-ubyte.gz: training set images (9912422 bytes) train-labels-idx1-ubyte.gz: training set labels (28881 bytes) t10k-images-idx3-ubyte.gz: test set images (1648877 bytes) t10k-labels-idx1-ubyte.gz: test set labels (4542 bytes)
解壓之後得到的是一個二進位制檔案。由60000個訓練樣本集和10000個測試樣本集構成,每個樣本的尺寸為28x28,以二進位制格式儲存。標籤是0到9的數字,影象則是0到255的畫素值。
檔案的格式如下:訓練集和測試集格式一樣,這裡以訓練集為例:
train-labels-idx1-ubyte
[offset] | [type] | [value] | [description] |
---|---|---|---|
0000 | 32 bit integer | 0x00000801(2049) | magic number (MSB first) (檔案頭魔數) |
0004 | 32 bit integer | 60000 | number of items (標籤個數) |
0008 | unsigned byte | ?? |
label (影象標籤) |
0009 | unsigned byte | ?? | label |
…….. | unsigned byte | ?? | label |
The labels values are 0 to 9.
train-images-idx3-ubyte
[offset] | [type] | [value] | [description] |
---|---|---|---|
0000 | 32 bit integer | 0x00000803(2051) | magic number(檔案頭魔數) |
0004 | 32 bit integer | 60000 | number of images(影象個數) |
0008 | 32 bit integer | 28 | number of rows(影象寬度) |
0012 |
32 bit integer | 28 | number of columns(影象高度) |
0016 | unsigned byte | ?? | pixel (影象畫素值) |
0017 | unsigned byte | ?? | pixel |
…….. | unsigned byte | ?? | pixel |
這裡的行和列,其實就是一張圖片的畫素矩陣,也就是每個圖片都有28×28=784
個畫素。從第16個位元組開始,就是圖片的每一個畫素點的值了。
SVM模型簡介
svm通常用於解決監督式機器學習中的二元分類問題,其基本模型定義為特徵空間上間隔最大的線性分類器,即支援向量機的學習策略便是間隔最大化,最終可轉化為一個凸二次規劃問題的求解。如圖示
Spark MLlib通過SVMWithSGD類和其伴生物件實現了線性SVM二元分類模型,並採用隨機梯度下降演算法來優化目標函式
資料集處理
資料集讀取
spark MLlib的資料格式是 LabeledPoint 標籤向量資料格式,所以要先對資料進行讀取和處理。
/** * 安全開啟檔案流方法 */ def using[A <: { def close(): Unit }, B](resource: A)(f: A => B): B = try { f(resource) } finally { resource.close() }
讀取圖片資訊如下:返回有每一張圖片畫素資料組成的陣列
/** * 讀取影象檔案 * * @param imagesPath * @return Array[Array[Byte]] * TRAINING SET IMAGE FILE (train-images-idx3-ubyte): * [offset] [type] [value] [description] * 0000 32 bit integer 0x00000803(2051) magic number * 0004 32 bit integer 60000 number of images * 0008 32 bit integer 28 number of rows * 0012 32 bit integer 28 number of columns * 0016 unsigned byte ?? pixel * 0017 unsigned byte ?? pixel * ........ * xxxx unsigned byte ?? pixel */ def loadImages(imagesPath: String): Array[Array[Byte]] ={ val file = new File(imagesPath) val in = new FileInputStream(file) var trainingDS = new Array[Byte](file.length.toInt) using(new FileInputStream(file)) { source => { in.read(trainingDS) } } //32 bit integer 0x00000803(2051) magic number val magicNum = ByteBuffer.wrap(trainingDS.take(4)).getInt println(s"magicNum=$magicNum") //32 bit integer 60000 number of items val numOfItems = ByteBuffer.wrap(trainingDS.slice(4, 8)).getInt println(s"numOfItems=$numOfItems") //32 bit integer 28 number of rows val numOfRows = ByteBuffer.wrap(trainingDS.slice(8, 12)).getInt println(s"numOfRows=$numOfRows") //32 bit integer 28 number of columns val numOfCols = ByteBuffer.wrap(trainingDS.slice(12, 16)).getInt println(s"numOfCols=$numOfCols") trainingDS = trainingDS.drop(16) val itemsBuffer = new ArrayBuffer[Array[Byte]] for(i <- 0 until numOfItems){ //使用slice方法從規定的索引處提取陣列中的元素 itemsBuffer += trainingDS.slice( i * numOfCols * numOfRows , (i+1) * numOfCols * numOfRows) } itemsBuffer.toArray }
讀取標籤程式碼如下:
/** * 讀取標籤 * * @param labelPath * @return Array[Byte] * TRAINING SET LABEL FILE (train-labels-idx1-ubyte): * [offset] [type] [value] [description] * 0000 32 bit integer 0x00000801(2049) magic number (MSB first) * 0004 32 bit integer 60000 number of items * 0008 unsigned byte ?? label * 0009 unsigned byte ?? label * ........ * xxxx unsigned byte ?? label * The labels values are 0 to 9. */ def loadLabel(labelPath: String): Array[Byte] ={ val file = new File(labelPath) //根據檔案路徑獲取讀檔案的物件in val in = new FileInputStream(file) var labelDS = new Array[Byte](file.length.toInt) //定義一個Array[Byte]型別的labelDS,把讀取到的檔案放進去 using(new FileInputStream(file)) { source => { in.read(labelDS) } } /** * Wraps a byte array into a buffer.(將位元組陣列包裝到緩衝區中。) * * 輸入引數:array * The array that will back this buffer * * @return The new byte buffer */ //32 bit integer 0x00000801(2049) magic number (MSB first--high endian) val magicLabelNum = ByteBuffer.wrap(labelDS.take(4)).getInt println(s"magicLabelNum=$magicLabelNum") //32 bit integer 60000 number of items val numOfLabelItems = ByteBuffer.wrap(labelDS.slice(4, 8)).getInt println(s"numOfLabelItems=$numOfLabelItems") //刪掉前面的檔案描述 labelDS = labelDS.drop(8) labelDS }
-
新增依賴
<dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.11</artifactId> <version>2.4.7</version> </dependency> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-mllib_2.11</artifactId> <version>2.4.7</version> </dependency>
//處理成mlLib能用的基本型別 LabeledPoint if(trainLabel.length == trainImages.length) { /** * zip函式將傳進來的兩個引數中相應位置上的元素組成一個pair陣列。如果其中一個引數元素比較長,那麼多餘的引數會被刪掉。 * 標籤數量和影象數量能對上則合併陣列 Array[(label,images)] * 把畫素值由二進位制byte型別轉換成double型別 使用p & 0xFF * 只能對0/1進行識別 * data: Array[LabeledPoint] * LabeledPoint:[label:0/1 features:各個影象的畫素值] */ val data = trainLabel.zip(trainImages).filter(d => d._1.toInt == 0 || d._1.toInt == 1).map( d => LabeledPoint(d._1.toInt, Vectors.dense(d._2.map(p => (p & 0xFF).toDouble)))) //建立data的RDD //trainRdd: RDD[LabeledPoint] val trainRdd = sc.makeRDD(data)
3.呼叫MLlib的api,輸入資料進行訓練,得到分類模型
/** * 同樣使用zip函式將將test資料集中的testLabel,testImages一一對應組成Array[(labe,images)] * testData: Array[(Int:0/1, Vector:各個影象的畫素值)] */ val testData = testLabel.zip(testImages).filter(d => d._1.toInt == 0 || d._1.toInt == 1) .map(d =>(d._1.toInt,Vectors.dense(d._2.map(p => (p & 0xFF).toDouble )) )) //testRDD: RDD[Vector] 其中的元素都是各個元素的畫素值 val testRDD = sc.makeRDD(testData.map(_._2)) // res:Array[Int] 其中的元素都是使用model模型預測的測試集資料的標籤 val res = model.predict(testRDD).map(l => l.toInt).collect() //res.foreach(println(_)) //把測試的結果label和資料集本身的label組成一個pair陣列 val tr = res.zip(testData.map(_._1)) //統計測試結果和資料集本身的label一樣的個數 val sum = tr.map( f =>{ if(f._1 == f._2.toInt) 1 else 0 }).sum println("準確率為:"+ sum.toDouble /tr.length)