1. 程式人生 > >OpenCV機器學習:SVM分類器實現MNIST手寫數字識別

OpenCV機器學習:SVM分類器實現MNIST手寫數字識別

0. 開發環境

最近機器學習隨著AI人工智慧的興起越來越火,博主想找一些ML的庫來練手。突然想起之前在看Opencv的doc時發現有ML的component,於是心血來潮就開始寫程式碼試試。話不多說,直接進正題。

以下我的開發環境配置:
-Windows7
-Visual Studio2015
-OpenCV3.2

1. MNIST手寫資料庫

我們選用鼎鼎大名的MNIST手寫庫作為資料集,MNIST是由深度學習三大神之一Yann LeCun帶頭建立的,可以在下面的連結進行下載:

http://yann.lecun.com/exdb/mnist/

MNIST資料集分為以下四部分:
(1)
train-images-idx3-ubyte
訓練影象的集合,共有60000張,大小是28×28
(2)
train-labels-idx1-ubyte
對應於訓練影象的標籤集,為0~9
(3)
t10k-images-idx3-ubyte
測試影象的集合,共有10000張,大小是28×28
(4)
t10k-labels-idx1-ubyte
對應於測試影象的標籤集,為0~9

2. 程式碼分析

2.1 讀取MNIST資料集

在MNIST資料庫的主頁上對整個MNIST的結構進行了介紹,四個檔案都是binary二進位制檔案,並且資料的儲存有一定的格式,在讀取時需要小心。

對於標籤集train-labels-idx1-ubyte(t10k-labels-idx1-ubyte也是類似)來說,資料儲存的描述如下:

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.

可以看到,我們需要讀取的第一個數是32bit的magic number,第二個資料是影象標籤的個數,也是32bit的整數,接下來才是每個影象對應的標籤值,每個按照unsigned byte進行儲存。

對於訓練/測試影象集train-images-idx3-ubyte(t10k-images-idx3-ubyte也是類似)來說,資料儲存的描述如下:

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
Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black).

可以看到,我們需要讀取的第一個數是32bit的magic number,第二個資料是影象的個數,也是32bit的整數,接下來分別是影象的行數和列數,均是32bit的整數,最後是所有影象的畫素點(8bit的unsigned byte)按照行優先的格式進行儲存。

PS:注意MNIST的描述中有這樣一句話,所有32bit的整數是按照MSB在前(大端模式)進行儲存的。在Intel及其他小端處理器上,需要對這些整數進行大小端翻轉。

All the integers in the files are stored in the MSB first (high endian) format used by most non-Intel processors. Users of Intel processors and other low-endian machines must flip the bytes of the header.
2.1.1

下面的程式碼段通過位運算實現32bit整數的大端-小端的轉換:

//大端轉小端
int reverseInt(int i)
{
    unsigned char c1, c2, c3, c4;

    c1 = i & 255;
    c2 = (i >> 8) & 255;
    c3 = (i >> 16) & 255;
    c4 = (i >> 24) & 255;

    return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}
2.1.2

我們利用C++中fstream的子類ifstream進行二進位制檔案的讀取,請注意在開啟檔案時需要設定ios::binary

//讀取訓練樣本集
ifstream if_trainImags("train-images-idx3-ubyte", ios::binary);
//讀取失敗
if (true == if_trainImags.fail())
{
    cout << "Please check the path of file train-images-idx3-ubyte" << endl;
    return;
}

呼叫函式ifstream.read()對資料進行按byte讀取

int magic_num, trainImgsNum, nrows, ncols;
//讀取magic number
if_trainImags.read((char*)&magic_num, sizeof(magic_num));
magic_num = reverseInt(magic_num);

在讀取訓練影象時,需要注意的一點是需要將CV_8UC1格式的資料轉換為CV_32FC1。在for迴圈裡,我們每一次讀取一張圖片的所有畫素點到Mat矩陣temp,然後呼叫Opencv內建的convertTo函式實現unsigned char到32bit float轉換,最後拷貝到trainFeatures中佔滿一行,注意Mat資料結構是行優先的。

//讀取訓練影象
int imgVectorLen = nrows * ncols;
Mat trainFeatures = Mat::zeros(trainImgsNum, imgVectorLen, CV_32FC1);
Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
for (int i = 0; i < trainImgsNum; i++)
{
    if_trainImags.read((char*)temp.data, imgVectorLen);
    Mat tempFloat;
    //由於SVM需要的訓練資料格式是CV_32FC1,在這裡進行轉換
    temp.convertTo(tempFloat, CV_32FC1);
    memcpy(trainFeatures.data+i*imgVectorLen *sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
}

在完成訓練樣本的讀取後,需要對其進行歸一化,畫素點是0~255的,歸一化至0~1。原因在於我們在SVM分類時採用的是RBF的kernal,這一點我們在後面會細講。

//歸一化
trainFeatures = trainFeatures / 255;

按照同樣的讀取方式,測試樣本集以及訓練&測試標籤集都可以讀取成功。

2.2 SVM訓練&預測

按照下面的程式碼建立一個SVM的分類器,並初始化引數
(1)type
這裡我們選擇了 SVM::C_SVC 型別,該型別可以用於n-類分類問題 (n>2)。
(2)kernal
CvSVM::RBF : 基於徑向的函式,對於大多數情況都是一個較好的選擇。
(3)Gamma&C
經驗值選擇
在訓練結束之後我們把SVM分類模型儲存在xml檔案裡。

// 訓練SVM分類器
//初始化
Ptr<SVM> svm = SVM::create();
//多分類
svm->setType(SVM::C_SVC);
//kernal選用RBF
svm->setKernel(SVM::RBF);
//設定經驗值 
svm->setGamma(0.01);
svm->setC(10.0);
//設定終止條件,在這裡選擇迭代200次
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 200, FLT_EPSILON));
//訓練開始
svm->train(trainFeatures, ROW_SAMPLE, trainLabels);

cout << "訓練結束,正寫入xml:" << endl;
//儲存模型
svm->save("mnist.xml");

接下來匯入訓練好的SVM模型對測試資料集進行預測並計算準確率

//載入訓練好的SVM模型
Ptr<SVM> svm = SVM::load("mnist.xml");
int sum = 0;
//對每一個測試影象進行SVM分類預測
for (int i = 0; i < testLblsNum; i++)
{
    Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
    memcpy(predict_mat.data, testFeatures.data + i*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
    //預測
    float predict_label = svm->predict(predict_mat);
    //真實的樣本標籤
    float truth_label = testLabels.at<int>(i);
    //比較判定是否預測正確
    if ((int)predict_label == (int)truth_label)
    {
        sum++;
    }
}

cout << "預測準確率為:"<<(double)sum / (double)testLblsNum << endl;

2.3 結果

MNIST手寫資料集的訓練過程:
這裡寫圖片描述

MNIST手寫資料集的測試驗證:
這裡寫圖片描述

從結果可以看到,訓練樣本集為60000,測試樣本集為10000時,SVM分類準確率可以到96.98%,而且SVM的最大迭代次數為200。

單個樣本的隨機測試:
這裡寫圖片描述

在自己的測試圖片上進行預測:(可以用windows自帶的畫圖板進行黑底白字的數字繪製)
這裡寫圖片描述

2.4 全部程式碼

2.4.1 SVM訓練MNIST過程:
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
#include <fstream>

using namespace cv;
using namespace cv::ml;
using namespace std;

//大端轉小端
int reverseInt(int i);

void main()
{
    //讀取訓練樣本集
    ifstream if_trainImags("train-images-idx3-ubyte", ios::binary);
    //讀取失敗
    if (true == if_trainImags.fail())
    {
        cout << "Please check the path of file train-images-idx3-ubyte" << endl;
        return;
    }
    int magic_num, trainImgsNum, nrows, ncols;
    //讀取magic number
    if_trainImags.read((char*)&magic_num, sizeof(magic_num));
    magic_num = reverseInt(magic_num);
    cout << "訓練影象資料庫train-images-idx3-ubyte的magic number為:" << magic_num << endl;
    //讀取訓練影象總數
    if_trainImags.read((char*)&trainImgsNum, sizeof(trainImgsNum));
    trainImgsNum = reverseInt(trainImgsNum);
    cout << "訓練影象資料庫train-images-idx3-ubyte的影象總數為:" << trainImgsNum << endl;
    //讀取影象的行大小
    if_trainImags.read((char*)&nrows, sizeof(nrows));
    nrows = reverseInt(nrows);
    cout << "訓練影象資料庫train-images-idx3-ubyte的影象維度row為:" << nrows << endl;
    //讀取影象的列大小
    if_trainImags.read((char*)&ncols, sizeof(ncols));
    ncols = reverseInt(ncols);
    cout << "訓練影象資料庫train-images-idx3-ubyte的影象維度col為:" << ncols << endl;

    //讀取訓練影象
    int imgVectorLen = nrows * ncols;
    Mat trainFeatures = Mat::zeros(trainImgsNum, imgVectorLen, CV_32FC1);
    Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
    for (int i = 0; i < trainImgsNum; i++)
    {
        if_trainImags.read((char*)temp.data, imgVectorLen);
        Mat tempFloat;
        //由於SVM需要的訓練資料格式是CV_32FC1,在這裡進行轉換
        temp.convertTo(tempFloat, CV_32FC1);
        memcpy(trainFeatures.data+i*imgVectorLen *sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
    }
    //歸一化
    trainFeatures = trainFeatures / 255;
    //讀取訓練影象對應的分類標籤
    ifstream if_trainLabels("train-labels-idx1-ubyte", ios::binary);
    //讀取失敗
    if (true == if_trainLabels.fail())
    {
        cout << "Please check the path of file train-labels-idx1-ubyte" << endl;
        return;
    }
    int magic_num_2, trainLblsNum;
    //讀取magic number
    if_trainLabels.read((char*)&magic_num_2, sizeof(magic_num_2));
    magic_num_2 = reverseInt(magic_num_2);
    cout << "訓練影象標籤資料庫train-labels-idx1-ubyte的magic number為:" << magic_num_2 << endl;
    //讀取訓練影象的分類標籤的數量
    if_trainLabels.read((char*)&trainLblsNum, sizeof(trainLblsNum));
    trainLblsNum = reverseInt(trainLblsNum);
    cout << "訓練影象標籤資料庫train-labels-idx1-ubyte的標籤總數為:" << trainLblsNum << endl;

    //由於SVM需要輸入的標籤型別是CV_32SC1,在這裡進行轉換
    Mat trainLabels = Mat::zeros(trainLblsNum, 1, CV_32SC1);
    Mat readLabels  = Mat::zeros(trainLblsNum, 1, CV_8UC1);
    if_trainLabels.read((char*)readLabels.data, trainLblsNum*sizeof(char));
    readLabels.convertTo(trainLabels, CV_32SC1);

    /*
    //Add some random test code
    while (1)
    {
    int index;
    cout << "請輸入要檢視的訓練影象下標" << endl;
    cin >> index;
    if (-1 == index)
    {
    break;
    }
    Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);
    memcpy(show_mat.data, trainFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
    Mat show_char;
    show_mat.convertTo(show_char, CV_8UC1);
    imshow("test", show_mat);
    cout << "標籤值為" << trainLabels.at<int>(index);
    }
    waitKey(0);
    */


    // 訓練SVM分類器
    //初始化
    Ptr<SVM> svm = SVM::create();
    //多分類
    svm->setType(SVM::C_SVC);
    //kernal選用RBF
    svm->setKernel(SVM::RBF);
    //設定經驗值 
    svm->setGamma(0.01);
    svm->setC(10.0);
    //設定終止條件,在這裡選擇迭代200次
    svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 200, FLT_EPSILON));
    //訓練開始
    svm->train(trainFeatures, ROW_SAMPLE, trainLabels);

    cout << "訓練結束,正寫入xml:" << endl;
    //儲存模型
    svm->save("mnist.xml");



}

//大端轉小端
int reverseInt(int i)
{
    unsigned char c1, c2, c3, c4;

    c1 = i & 255;
    c2 = (i >> 8) & 255;
    c3 = (i >> 16) & 255;
    c4 = (i >> 24) & 255;

    return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}
2.4.2 SVM測試MNIST過程:

鑑於在評論區裡有很多同學在問怎麼把自己的圖片丟到程式碼裡面去進行測試,所以我把程式碼進行了修改。詳見while迴圈中的改動。

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
#include <fstream>

using namespace cv;
using namespace cv::ml;
using namespace std;

//大端轉小端
int reverseInt(int i);

void main()
{
    //讀取測試樣本集
    ifstream if_testImags("t10k-images-idx3-ubyte", ios::binary);
    //讀取失敗
    if (true == if_testImags.fail())
    {
        cout << "Please check the path of file t10k-images-idx3-ubyte" << endl;
        return;
    }
    int magic_num, testImgsNum, nrows, ncols;
    //讀取magic number
    if_testImags.read((char*)&magic_num, sizeof(magic_num));
    magic_num = reverseInt(magic_num);
    cout << "測試影象資料庫t10k-images-idx3-ubyte的magic number為:" << magic_num << endl;
    //讀取測試影象總數
    if_testImags.read((char*)&testImgsNum, sizeof(testImgsNum));
    testImgsNum = reverseInt(testImgsNum);
    cout << "測試影象資料庫t10k-images-idx3-ubyte的影象總數為:" << testImgsNum << endl;
    //讀取影象的行大小
    if_testImags.read((char*)&nrows, sizeof(nrows));
    nrows = reverseInt(nrows);
    cout << "測試影象資料庫t10k-images-idx3-ubyte的影象維度row為:" << nrows << endl;
    //讀取影象的列大小
    if_testImags.read((char*)&ncols, sizeof(ncols));
    ncols = reverseInt(ncols);
    cout << "測試影象資料庫t10k-images-idx3-ubyte的影象維度col為:" << ncols << endl;

    //讀取測試影象
    int imgVectorLen = nrows * ncols;
    Mat testFeatures = Mat::zeros(testImgsNum, imgVectorLen, CV_32FC1);
    Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
    for (int i = 0; i < testImgsNum; i++)
    {
        if_testImags.read((char*)temp.data, imgVectorLen);
        Mat tempFloat;
        //由於SVM需要的測試資料格式是CV_32FC1,在這裡進行轉換
        temp.convertTo(tempFloat, CV_32FC1);
        memcpy(testFeatures.data + i*imgVectorLen * sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
    }
    //歸一化
    testFeatures = testFeatures / 255;
    //讀取測試影象對應的分類標籤
    ifstream if_testLabels("t10k-labels-idx1-ubyte", ios::binary);
    //讀取失敗
    if (true == if_testLabels.fail())
    {
        cout << "Please check the path of file t10k-labels-idx1-ubyte" << endl;
        return;
    }
    int magic_num_2, testLblsNum;
    //讀取magic number
    if_testLabels.read((char*)&magic_num_2, sizeof(magic_num_2));
    magic_num_2 = reverseInt(magic_num_2);
    cout << "測試影象標籤資料庫t10k-labels-idx1-ubyte的magic number為:" << magic_num_2 << endl;
    //讀取測試影象的分類標籤的數量
    if_testLabels.read((char*)&testLblsNum, sizeof(testLblsNum));
    testLblsNum = reverseInt(testLblsNum);
    cout << "測試影象標籤資料庫t10k-labels-idx1-ubyte的標籤總數為:" << testLblsNum << endl;

    //由於SVM需要輸入的標籤型別是CV_32SC1,在這裡進行轉換
    Mat testLabels = Mat::zeros(testLblsNum, 1, CV_32SC1);
    Mat readLabels = Mat::zeros(testLblsNum, 1, CV_8UC1);
    if_testLabels.read((char*)readLabels.data, testLblsNum * sizeof(char));
    readLabels.convertTo(testLabels, CV_32SC1);

    //載入訓練好的SVM模型
    Ptr<SVM> svm = SVM::load("mnist.xml");
    int sum = 0;
    //對每一個測試影象進行SVM分類預測
    for (int i = 0; i < testLblsNum; i++)
    {
        Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
        memcpy(predict_mat.data, testFeatures.data + i*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
        //預測
        float predict_label = svm->predict(predict_mat);
        //真實的樣本標籤
        float truth_label = testLabels.at<int>(i);
        //比較判定是否預測正確
        if ((int)predict_label == (int)truth_label)
        {
            sum++;
        }
    }

    cout << "預測準確率為:"<<(double)sum / (double)testLblsNum << endl;


    //隨機測試某一個影象看效果,輸入為-2時退出,輸入-1時則測試本地圖片“2.jpg”,注意路徑要放到原始碼同級目錄
    while (1)
    {
        int index;
        cout << "請輸入要檢視的測試影象下標" << endl;
        cin >> index;
        if (-1 == index)
        {
            Mat imgRead = imread("2.jpg", 0);
            Mat imgReadScal = Mat::zeros(nrows, ncols, CV_8UC1);
            Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);

            resize(imgRead, imgReadScal, imgReadScal.size());

            imgReadScal.convertTo(show_mat, CV_32FC1);

            show_mat = show_mat / 255;

            //
            Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
            //memcpy(show_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            memcpy(predict_mat.data, show_mat.data, imgVectorLen * sizeof(float));
            float response = svm->predict(predict_mat);

            imshow("test", show_mat);
            cout << "標籤值為" << response << endl;

            waitKey(0);

        }
        else if (-2 == index)
        {
            break;
        }
        else
        {
            Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);
            Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
            memcpy(show_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            memcpy(predict_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            float response = svm->predict(predict_mat);

            imshow("test", show_mat);
            cout << "標籤值為" << response << endl;

            waitKey(0);
        }

    }

}

//大端轉小端
int reverseInt(int i)
{
    unsigned char c1, c2, c3, c4;

    c1 = i & 255;
    c2 = (i >> 8) & 255;
    c3 = (i >> 16) & 255;
    c4 = (i >> 24) & 255;

    return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4;
}

喜歡的話可以拿去試試,但務必請註明轉載地址~

3. 參考資料