學習SVM(一) SVM模型訓練與分類的OpenCV實現

chaibubble發表於2017-03-29

簡介

學習SVM(一) SVM模型訓練與分類的OpenCV實現
學習SVM(二) 如何理解支援向量機的最大分類間隔
學習SVM(三)理解SVM中的對偶問題
學習SVM(四) 理解SVM中的支援向量(Support Vector)
學習SVM(五)理解線性SVM的鬆弛因子

Andrew Ng 在史丹佛大學的機器學習公開課上這樣評價支援向量機:
support vector machines is the supervised learning algorithm that many people consider the most effective off-the-shelf supervised learning algorithm.That point of view is debatable,but there are many people that hold that point of view.

可見,在監督學習演算法中支援向量機有著非常廣泛的應用,而且在解決影像分類問題時有著優異的效果。

OpenCV整合了這種學習演算法,它被包含在ml模組下的CvSVM類中,下面我們用OpenCV實現SVM的資料準備模型訓練載入模型實現分類,為了理解起來更加直觀,我們用三個工程來實現。

資料準備

在OpenCV的安裝路徑下,搜尋digits,可以得到一張圖片,圖片大小為1000*2000,有0-9的10個數字,每5行為一個數字,總共50行,共有5000個手寫數字,每個數字塊大小為20*20。 下面將把這些數字中的0和1作為二分類的準備資料。其中0有500張,1有500張。
用下面的程式碼將圖片準備好,在寫入路徑提前建立好資料夾:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace std;
using namespace cv;

int main()
{
    char ad[128]={0};
    int  filename = 0,filenum=0; 
    Mat img = imread("digits.png");
    Mat gray;
    cvtColor(img, gray, CV_BGR2GRAY);
    int b = 20;
    int m = gray.rows / b;   //原圖為1000*2000
    int n = gray.cols / b;   //裁剪為5000個20*20的小圖塊

    for (int i = 0; i < m; i++)
    {
        int offsetRow = i*b;  //行上的偏移量
        if(i%5==0&&i!=0)
        {
            filename++;
            filenum=0;
        }
        for (int j = 0; j < n; j++)
        {
            int offsetCol = j*b; //列上的偏移量
            sprintf_s(ad, "D:\\data\\%d\\%d.jpg",filename,filenum++);
            //擷取20*20的小塊
            Mat tmp;
            gray(Range(offsetRow, offsetRow + b), Range(offsetCol, offsetCol + b)).copyTo(tmp);
            imwrite(ad,tmp);
        }
    }
    return 0;
}

最後可以得到這樣的結果:
這裡寫圖片描述
組織的二分類資料形式為:

--D  --data
    --train_image
      --0400張)
      --1400張)
    --test_image
      --0100張)
      --1100張)

這裡寫圖片描述
訓練資料800張,0,1各400張;測試資料200張,0,1各100張

模型訓練

資料準備完成之後,就可以用下面的程式碼訓練了:

#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <opencv/cv.h>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files);
void get_1(Mat& trainingImages, vector<int>& trainingLabels);
void get_0(Mat& trainingImages, vector<int>& trainingLabels);

int main()
{
    //獲取訓練資料
    Mat classes;
    Mat trainingData;
    Mat trainingImages;
    vector<int> trainingLabels;
    get_1(trainingImages, trainingLabels);
    get_0(trainingImages, trainingLabels);
    Mat(trainingImages).copyTo(trainingData);
    trainingData.convertTo(trainingData, CV_32FC1);
    Mat(trainingLabels).copyTo(classes);
    //配置SVM訓練器引數
    CvSVMParams SVM_params;
    SVM_params.svm_type = CvSVM::C_SVC;
    SVM_params.kernel_type = CvSVM::LINEAR; 
    SVM_params.degree = 0;
    SVM_params.gamma = 1;
    SVM_params.coef0 = 0;
    SVM_params.C = 1;
    SVM_params.nu = 0;
    SVM_params.p = 0;
    SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);
    //訓練
    CvSVM svm;
    svm.train(trainingData, classes, Mat(), Mat(), SVM_params);
    //儲存模型
    svm.save("svm.xml");
    cout<<"訓練好了!!!"<<endl;
    getchar();
    return 0;
}
void getFiles( string path, vector<string>& files )  
{  
    long   hFile   =   0;  
    struct _finddata_t fileinfo;  
    string p;  
    if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1)  
    {  
        do  
        {  
            if((fileinfo.attrib &  _A_SUBDIR))  
            {  
                if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0)  
                    getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
            }  
            else  
            {  
                files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
            }  
        }while(_findnext(hFile, &fileinfo)  == 0);  

        _findclose(hFile);  
    }  
} 
void get_1(Mat& trainingImages, vector<int>& trainingLabels)
{
    char * filePath = "D:\\data\\train_image\\1"; 
    vector<string> files;  
    getFiles(filePath, files );  
    int number = files.size();  
    for (int i = 0;i < number;i++)  
    {  
        Mat  SrcImage=imread(files[i].c_str());
        SrcImage= SrcImage.reshape(1, 1);
        trainingImages.push_back(SrcImage);
        trainingLabels.push_back(1);
    }  
}
void get_0(Mat& trainingImages, vector<int>& trainingLabels)
{
    char * filePath = "D:\\data\\train_image\\0"; 
    vector<string> files;  
    getFiles(filePath, files );  
    int number = files.size();  
    for (int i = 0;i < number;i++)  
    {  
        Mat  SrcImage=imread(files[i].c_str());
        SrcImage= SrcImage.reshape(1, 1);
        trainingImages.push_back(SrcImage);
        trainingLabels.push_back(0);
    }  
}

整個訓練過程可以分為一下幾個部分:
資料準備:
該例程中一個定義了三個子程式用來實現資料準備工作:
getFiles()用來遍歷資料夾下所有檔案,可以參考:
http://blog.csdn.net/chaipp0607/article/details/53914954
getBubble()用來獲取有氣泡的圖片和與其對應的Labels,該例程將Labels定為1。
getNoBubble()用來獲取沒有氣泡的圖片與其對應的Labels,該例程將Labels定為0。
getBubble()與getNoBubble()將獲取一張圖片後會將圖片(特徵)寫入到容器中,緊接著會將標籤寫入另一個容器中,這樣就保證了特徵和標籤是一一對應的關係push_back(0)或者push_back(1)其實就是我們貼標籤的過程。

trainingImages.push_back(SrcImage);
trainingLabels.push_back(0);

在主函式中,將getBubble()與getNoBubble()寫好的包含特徵的矩陣拷貝給trainingData,將包含標籤的vector容器進行型別轉換後拷貝到trainingLabels裡,至此,資料準備工作完成,trainingData與trainingLabels就是我們要訓練的資料。

    Mat classes;
    Mat trainingData;
    Mat trainingImages;
    vector<int> trainingLabels;
    getBubble(trainingImages, trainingLabels);
    getNoBubble(trainingImages, trainingLabels);
    Mat(trainingImages).copyTo(trainingData);
    trainingData.convertTo(trainingData, CV_32FC1);
    Mat(trainingLabels).copyTo(classes);

特徵選取
其實特徵提取和資料的準備是同步完成的,我們最後要訓練的也是正負樣本的特徵。本例程中同樣在getBubble()與getNoBubble()函式中完成特徵提取工作,只是我們簡單粗暴將整個圖的所有畫素作為了特徵,因為我們關注更多的是整個的訓練過程,所以選擇了最簡單的方式完成特徵提取工作,除此中外,特徵提取的方式有很多,比如LBP,HOG等等。

  SrcImage= SrcImage.reshape(1, 1);

我們利用reshape()函式完成特徵提取,原型如下:

 Mat reshape(int cn, int rows=0) const;

可以看到該函式的引數非常簡單,cn為新的通道數,如果cn = 0,表示通道數不會改變。引數rows為新的行數,如果rows = 0,表示行數不會改變。我們將引數定義為reshape(1, 1)的結果就是原影像對應的矩陣將被拉伸成一個一行的向量,作為特徵向量。
引數配置
引數配置是SVM的核心部分,在Opencv中它被定義成一個結構體型別,如下:

struct CV_EXPORTS_W_MAP CvSVMParams
{
    CvSVMParams();
    CvSVMParams(  
    int svm_type, 
    int kernel_type,
    double degree, 
    double coef0,
    double Cvalue, 
    double p,
    CvMat* class_weights, 
    CvTermCriteria term_crit );
    CV_PROP_RW int         svm_type;
    CV_PROP_RW int         kernel_type;
    CV_PROP_RW double      degree; // for poly
    CV_PROP_RW double      gamma;  // for poly/rbf/sigmoid
    CV_PROP_RW double      coef0;  // for poly/sigmoid
    CV_PROP_RW double      C;  // for CV_SVM_C_SVC,       CV_SVM_EPS_SVR and CV_SVM_NU_SVR
    CV_PROP_RW double      nu; // for CV_SVM_NU_SVC, CV_SVM_ONE_CLASS, and CV_SVM_NU_SVR
    CV_PROP_RW double      p; // for CV_SVM_EPS_SVR
    CvMat*      class_weights; // for CV_SVM_C_SVC
    CV_PROP_RW CvTermCriteria term_crit; // termination criteria
};

所以在例程中我們定義了一個結構體變數用來配置這些引數,而這個變數也就是CVSVM類中train函式的第五個引數,下面對引數進行說明。
SVM_params.svm_type :SVM的型別:
C_SVC表示SVM分類器,C_SVR表示SVM迴歸
SVM_params.kernel_type:核函式型別
線性核LINEAR:
d(x,y)=(x,y)
多項式核POLY:
d(x,y)=(gamma*(x’y)+coef0)degree
徑向基核RBF:
d(x,y)=exp(-gamma*|x-y|^2)
sigmoid核SIGMOID:
d(x,y)= tanh(gamma*(x’y)+ coef0)

SVM_params.degree:核函式中的引數degree,針對多項式核函式;
SVM_params.gama:核函式中的引數gamma,針對多項式/RBF/SIGMOID核函式;
SVM_params.coef0:核函式中的引數,針對多項式/SIGMOID核函式;
SVM_params.c:SVM最優問題引數,設定C-SVCEPS_SVRNU_SVR的引數;
SVM_params.nu:SVM最優問題引數,設定NU_SVCONE_CLASSNU_SVR的引數;
SVM_params.p:SVM最優問題引數,設定EPS_SVR 中損失函式p的值.
訓練模型

CvSVM svm;
svm.train(trainingData, classes, Mat(), Mat(), SVM_params);

通過上面的過程,我們準備好了待訓練的資料和訓練需要的引數,其實可以理解為這個準備工作就是在為svm.train()函式準備實參的過程。來看一下svm.train()函式,Opencv將SVM封裝成CvSVM庫,這個庫是基於臺灣大學林智仁(Lin Chih-Jen)教授等人開發的LIBSVM封裝的,由於篇幅限制,不再全部貼上庫的定義,所以一下程式碼只是CvSVM庫中的一部分資料和函式:

class CV_EXPORTS_W CvSVM : public CvStatModel
{
public:
virtual bool train( 
  const CvMat* trainData, 
  const CvMat* responses,
  const CvMat* varIdx=0, 
  const CvMat* sampleIdx=0,
  CvSVMParams params=CvSVMParams() );
virtual float predict( 
  const CvMat* sample, 
  bool returnDFVal=false ) const;

我們就是應用類中定義的train函式完成模型訓練工作。
儲存模型

svm.save("svm.xml");

儲存模型只有一行程式碼,利用save()函式,我們看下它的定義:

    CV_WRAP virtual void save( const char* filename, const char* name=0 ) const;

該函式被定義在CvStatModel類中,CvStatModel是ML庫中的統計模型基類,其他 ML 類都是從這個類中繼承。

總結:到這裡我們就完成了模型訓練工作,可以看到真正用於訓練的程式碼其實很少,OpenCV最支援向量機的封裝極大地降低了我們的程式設計工作。

載入模型實現分類

#include <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <opencv/cv.h>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  
#include <io.h>

using namespace std;
using namespace cv;

void getFiles( string path, vector<string>& files );

int main()
{
    int result = 0;
    char * filePath = "D:\\data\\test_image\\0"; 
    vector<string> files;  
    getFiles(filePath, files );  
    int number = files.size();  
    cout<<number<<endl;
    CvSVM svm;
    svm.clear();
    string modelpath = "svm.xml";
    FileStorage svm_fs(modelpath,FileStorage::READ);
    if(svm_fs.isOpened())
    {
        svm.load(modelpath.c_str());
    }
    for (int i = 0;i < number;i++)  
    {  
        Mat inMat = imread(files[i].c_str());
        Mat p = inMat.reshape(1, 1);
        p.convertTo(p, CV_32FC1);
        int response = (int)svm.predict(p);
        if (response == 0)
        {
            result++;
        }
    }
    cout<<result<<endl;
    getchar();
    return  0;
}
void getFiles( string path, vector<string>& files )  
{  
    long   hFile   =   0;  
    struct _finddata_t fileinfo;  
    string p;  
    if((hFile = _findfirst(p.assign(path).append("\\*").c_str(),&fileinfo)) !=  -1)  
    {  
        do  
        {  
            if((fileinfo.attrib &  _A_SUBDIR))  
            {  
                if(strcmp(fileinfo.name,".") != 0  &&  strcmp(fileinfo.name,"..") != 0)  
                    getFiles( p.assign(path).append("\\").append(fileinfo.name), files );  
            }  
            else  
            {       files.push_back(p.assign(path).append("\\").append(fileinfo.name) );  
            }  
        }while(_findnext(hFile, &fileinfo)  == 0);  
        _findclose(hFile);  
    }  
} 

在上面我們把該介紹的都說的差不多了,這個例程中只是用到了load()函式用於模型載入,載入的就是上面例子中生成的模型,load()被定義在CvStatModel這個基類中:

    svm.load(modelpath.c_str());

load的路徑是string modelpath = "svm.xml",這意味著svm.mxl檔案應該在測試工程的根目錄下面,但是因為訓練和預測是兩個獨立的工程,所以必須要拷貝一下這個檔案。最後用到predict()函式用來預測分類結果,predict()被定義在CVSVM類中。

注意:

1.為什麼要建立三個獨立的工程呢?
主要是考慮寫在一起話,程式碼量會比較大,邏輯沒有分開清晰,當跑通上面的程式碼之後,就可以隨意的改了。
2.為什麼加上資料準備?
之前有評論說道資料的問題,提供資料後實驗能更順利一些,因為本身程式碼沒有什麼含金量,這樣可以更順利的執行起來工程,並修改它。
3.一些容易引起異常的情況:
(1):注意生成的.xml記得拷貝到預測工程下;
(2):注意準備好資料路徑和程式碼是不是一致;
(3):注意訓練的特徵要和測試的特徵一致;

相關文章