1. 程式人生 > 實用技巧 >基於Spark的SVM模型手寫數字識別

基於Spark的SVM模型手寫數字識別

資料集簡介

MNIST手寫數字資料集官網:THE MNIST DATABASE of handwritten digits

或者資料集下載網址: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
  }

  

測試程式碼

  1. 新增依賴

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

2.首先把標籤資料和圖片資料處理成 LabeledPoint 標籤向量資料格式

//處理成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)

  

結果