Opencv中SVM樣本訓練、歸類流程及實現
支援向量機(SVM)中最核心的是什麼?個人理解就是前4個字——“支援向量”,一旦在兩類或多累樣本集中定位到某些特定的點作為支援向量,就可以依據這些支援向量計算出來分類超平面,再依據超平面對類別進行歸類劃分就是水到渠成的事了。有必要回顧一下什麼是支援向量機中的支援向量。
上圖中需要對紅色和藍色的兩類訓練樣本進行區分,實現綠線是決策面(超平面),最靠近決策面的2個實心紅色樣本和1個實心藍色樣本分別是兩類訓練樣本的支援向量,決策面所在的位置是使得兩類支援向量與決策面之間的間隔都達到最大時決策面所處的位置。一般情況下,訓練樣本都會存在噪聲,這就導致其中一類樣本的一個或多個樣本跑到了決策面的另一邊,摻雜到另一類樣本中。針對這種情況,SVM加入了鬆弛變數(懲罰變數)來應對,確保這些噪聲樣本不會被作為支援向量,而不管它們離超平面的距離有多近。包括SVM中的另一個重要概念“核函式”,也是為訓練樣本支援向量的確定提供支援的。
在OpenCV中,SVM的訓練、歸類流程如下:
1. 獲取訓練樣本
SVM是一種有監督的學習分類方法,所以對於給出的訓練樣本,要明確每個樣本的歸類是0還是1,即每個樣本都需要標註一個確切的類別標籤,提供給SVM訓練使用。對於樣本的特徵,以及特徵的維度,SVM並沒有限定,可以使用如Haar、角點、Sift、Surf、直方圖等各種特徵作為訓練樣本的表述參與SVM的訓練。Opencv要求訓練資料儲存在float型別的Mat結構中。最簡單的情況下,假定有兩類訓練樣本,樣本的維度是二維,每類包含3個樣本,可以定義如下:
float labels[4] = {1.0, -1.0, -1.0, -1.0}; //樣本資料 Mat labelsMat(3, 1, CV_32FC1, labels); //樣本標籤 float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} }; //Mat結構特徵資料 Mat trainingDataMat(3, 2, CV_32FC1, trainingData); //Mat結構標籤
2. 設定SVM引數
Opencv中SVM的引數設定在CvSVMParams類中,常用的設定包括SVM的型別,核函式型別和演算法的終止條件,鬆弛變數等。可以按如下設定:
CvSVMParams params;
params.svm_type = CvSVM::C_SVC;
params.kernel_type = CvSVM::LINEAR;
params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);
CVSVM::C_SVC型別是SVM中最常被用到的型別,它的重要特徵是可以處理非完美分類的問題(即訓練資料不可以被線性分割),包括線性可分割和不可分割。
核函式的目的是為了將訓練樣本對映到更有利於線性分割的樣本集中。對映的結果是增加了樣本向量的維度。這一過程通過核函式來完成。若設定核函式的型別是CvSVM::LINEAR表示不需要進行高維空間對映。
演算法終止條件,SVM訓練的過程是一個通過迭代方式解決約束條件下的二次優化問題,可以設定一個最大迭代次數和容許誤差的組合,以允許演算法在適當的條件下停止計算。
3. 訓練支援向量
通過Opencv中的CvSVM::train函式對訓練樣本進行訓練,並建立SVM模型。
CvSVM SVM;
SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);
4. 不同訓練樣本區域分割
函式CvSVM::predict可以運用之前建立的支援向量機的模型來判斷測試樣本空間中樣本所屬的類別,一種比較直觀的方式是對不同類別樣本所在的空間進行塗色,如下程式碼把兩類樣本空間分別圖上綠色和藍色,以示區分:
Vec3b green(0,255,0), blue (255,0,0);
// Show the decision regions given by the SVM
for (int i = 0; i < image.rows; ++i)
for (int j = 0; j < image.cols; ++j)
{
Mat sampleMat = (Mat_<float>(1,2) << i,j);
float response = SVM.predict(sampleMat);
if (response == 1)
image.at<Vec3b>(j, i) = green;
else if (response == -1)
image.at<Vec3b>(j, i) = blue;
}
樣本區域分割效果,藍色和綠色區域代表不同的樣本空間,兩者交界處就是分類超平面位置:
5. 繪製支援向量
函式SVM.get_support_vector_count 可以獲取到支援向量的數量資訊,函式SVM.get_support_vector根據輸入支援向量的索引號來獲取指定位置的支援向量,如下程式碼獲取支援向量的數量,並在樣本空間中用紅色小圓圈逐個標識出來:
thickness = 2;
lineType = 8;
int c = SVM.get_support_vector_count();
for (int i = 0; i < c; ++i)
{
const float* v = SVM.get_support_vector(i);
circle( image, Point( (int) v[0], (int) v[1]), 6, Scalar(0, 0, 255), thickness, lineType);
}
以上就是OpenCV中SVM樣本訓練以及歸類的詳細流程,無論是線性可分割還是線性不可分割都可以按照這5個基本步驟進行。
以下分別是SVM中線性可分二分類、線性不可分二分類和線性不可分多(三)分類問題的Opencv程式碼實現。
一、線性可分二分類問題
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
using namespace cv;
int main()
{
// Data for visual representation
int width = 512, height = 512;
Mat image = Mat::zeros(height, width, CV_8UC3);
// Set up training data
float labels[5] = {1.0, -1.0, -1.0, -1.0,1.0}; //樣本資料
Mat labelsMat(4, 1, CV_32FC1, labels); //樣本標籤
float trainingData[5][2] = { {501, 300}, {255, 10}, {501, 255}, {10, 501},{450,500} }; //Mat結構特徵資料
Mat trainingDataMat(4, 2, CV_32FC1, trainingData); //Mat結構標籤
// Set up SVM's parameters
CvSVMParams params;
params.svm_type = CvSVM::C_SVC;
params.kernel_type = CvSVM::LINEAR;
params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);
// Train the SVM
CvSVM SVM;
SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);
Vec3b green(0,255,0), blue (255,0,0);
// Show the decision regions given by the SVM
for (int i = 0; i < image.rows; ++i)
for (int j = 0; j < image.cols; ++j)
{
Mat sampleMat = (Mat_<float>(1,2) << i,j);
float response = SVM.predict(sampleMat);
if (response == 1)
image.at<Vec3b>(j, i) = green;
else if (response == -1)
image.at<Vec3b>(j, i) = blue;
}
imshow("樣本區域分割",image);
waitKey();
// Show the training data
int thickness = -1;
int lineType = 8;
circle( image, Point(501, 300), 5, Scalar( 0, 0, 0), thickness, lineType);
circle( image, Point(255, 10), 5, Scalar(255, 255, 255), thickness, lineType);
circle( image, Point(501, 255), 5, Scalar(255, 255, 255), thickness, lineType);
circle( image, Point( 10, 501), 5, Scalar(255, 255, 255), thickness, lineType);
circle( image, Point( 450, 500), 5, Scalar(0, 0, 0), thickness, lineType);
// Show support vectors
thickness = 2;
lineType = 8;
int c = SVM.get_support_vector_count();
for (int i = 0; i < c; ++i)
{
const float* v = SVM.get_support_vector(i);
circle( image, Point( (int) v[0], (int) v[1]), 6, Scalar(0, 0, 255), thickness, lineType);
}
imwrite("result.png", image); // save the image
imshow("SVM Simple Example", image); // show it to the user
waitKey(0);
}
線性可分二分類問題的分類效果:
圖中右上角紅色的圓圈代表支援向量,位置明顯是錯的,據不可靠訊息,這個版本OpenCV的獲取支援向量的函式get_support_vector_count是有問題,具體各位可以自己驗證一下。
二、 線性不可分割問題
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
#define NTRAINING_SAMPLES 100 // Number of training samples per class
#define FRAC_LINEAR_SEP 0.9f // Fraction of samples which compose the linear separable part
using namespace cv;
using namespace std;
int main()
{
// Data for visual representation
const int WIDTH = 512, HEIGHT = 512;
Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3);
//--------------------- 1. Set up training data randomly ---------------------------------------
Mat trainData(2*NTRAINING_SAMPLES, 2, CV_32FC1);
Mat labels (2*NTRAINING_SAMPLES, 1, CV_32FC1);
RNG rng(100); // Random value generation class
// Set up the linearly separable part of the training data
int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES);
// Generate random points for the class 1
Mat trainClass = trainData.rowRange(0, nLinearSamples);
// The x coordinate of the points is in [0, 0.4)
Mat c = trainClass.colRange(0, 1);
rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(0.4 * WIDTH));
// The y coordinate of the points is in [0, 1)
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT));
// Generate random points for the class 2
trainClass = trainData.rowRange(2*NTRAINING_SAMPLES-nLinearSamples, 2*NTRAINING_SAMPLES);
// The x coordinate of the points is in [0.6, 1]
c = trainClass.colRange(0 , 1);
rng.fill(c, RNG::UNIFORM, Scalar(0.6*WIDTH), Scalar(WIDTH));
// The y coordinate of the points is in [0, 1)
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT));
//------------------ Set up the non-linearly separable part of the training data ---------------
// Generate random points for the classes 1 and 2
trainClass = trainData.rowRange( nLinearSamples, 2*NTRAINING_SAMPLES-nLinearSamples);
// The x coordinate of the points is in [0.4, 0.6)
c = trainClass.colRange(0,1);
rng.fill(c, RNG::UNIFORM, Scalar(0.4*WIDTH), Scalar(0.6*WIDTH));
// The y coordinate of the points is in [0, 1)
c = trainClass.colRange(1,2);
rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT));
//------------------------- Set up the labels for the classes ---------------------------------
labels.rowRange( 0, NTRAINING_SAMPLES).setTo(1); // Class 1
labels.rowRange(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES).setTo(2); // Class 2
//------------------------ 2. Set up the support vector machines parameters --------------------
CvSVMParams params;
params.svm_type = SVM::C_SVC;
params.C = 0.1;
params.kernel_type = SVM::LINEAR;
params.term_crit = TermCriteria(CV_TERMCRIT_ITER, (int)1e7, 1e-6);
//------------------------ 3. Train the svm ----------------------------------------------------
cout << "Starting training process" << endl;
CvSVM svm;
svm.train(trainData, labels, Mat(), Mat(), params);
cout << "Finished training process" << endl;
//------------------------ 4. Show the decision regions ----------------------------------------
Vec3b green(0,100,0), blue (100,0,0);
for (int i = 0; i < I.rows; ++i)
for (int j = 0; j < I.cols; ++j)
{
Mat sampleMat = (Mat_<float>(1,2) << i, j);
float response = svm.predict(sampleMat);
if (response == 1) I.at<Vec3b>(j, i) = green;
else if (response == 2) I.at<Vec3b>(j, i) = blue;
}
//----------------------- 5. Show the training data --------------------------------------------
int thick = -1;
int lineType = 8;
float px, py;
// Class 1
for (int i = 0; i < NTRAINING_SAMPLES; ++i)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(0, 255, 0), thick, lineType);
}
// Class 2
for (int i = NTRAINING_SAMPLES; i <2*NTRAINING_SAMPLES; ++i)
{
px = trainData.at<float>(i,0);
py = trainData.at<float>(i,1);
circle(I, Point( (int) px, (int) py ), 3, Scalar(255, 0, 0), thick, lineType);
}
//------------------------- 6. Show support vectors --------------------------------------------
thick = 2;
lineType = 8;
int x = svm.get_support_vector_count();
for (int i = 0; i < x; ++i)
{
const float* v = svm.get_support_vector(i);
circle( I, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thick, lineType);
}
imwrite("result.png", I); // save the Image
imshow("線性不可分二類問題", I); // show it to the user
waitKey(0);
}
訓練樣本空間最優分割面和訓練樣本分佈如下所示,在最優分割面附近有4個樣本點被當做噪點:
三、線性不可分多(三)分類問題
#include <iostream>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>
#include <CTYPE.H>
#define NTRAINING_SAMPLES 100 // Number of training samples per class
#define FRAC_LINEAR_SEP 0.9f // Fraction of samples which compose the linear separable part
using namespace cv;
using namespace std;
int main(int argc, char* argv[])
{
int size = 400; // height and widht of image
const int s = 1000; // number of data
int i, j,sv_num;
IplImage* img;
CvSVM svm ;
CvSVMParams param;
CvTermCriteria criteria; // 停止迭代標準
CvRNG rng = cvRNG();
CvPoint pts[s]; // 定義1000個點
float data[s*2]; // 點的座標
int res[s]; // 點的類別
CvMat data_mat, res_mat;
CvScalar rcolor;
const float* support;
// 影象區域的初始化
img = cvCreateImage(cvSize(size,size),IPL_DEPTH_8U,3);
cvZero(img);
// 學習資料的生成
for (i=0; i<s;++i)
{
pts[i].x = cvRandInt(&rng)%size;
pts[i].y = cvRandInt(&rng)%size;
if (pts[i].y>50*cos(pts[i].x*CV_PI/100)+200)
{
cvLine(img,cvPoint(pts[i].x-2,pts[i].y-2),cvPoint(pts[i].x+2,pts[i].y+2),CV_RGB(255,0,0));
cvLine(img,cvPoint(pts[i].x+2,pts[i].y-2),cvPoint(pts[i].x-2,pts[i].y+2),CV_RGB(255,0,0));
res[i]=1;
}
else
{
if (pts[i].x>200)
{
cvLine(img,cvPoint(pts[i].x-2,pts[i].y-2),cvPoint(pts[i].x+2,pts[i].y+2),CV_RGB(0,255,0));
cvLine(img,cvPoint(pts[i].x+2,pts[i].y-2),cvPoint(pts[i].x-2,pts[i].y+2),CV_RGB(0,255,0));
res[i]=2;
}
else
{
cvLine(img,cvPoint(pts[i].x-2,pts[i].y-2),cvPoint(pts[i].x+2,pts[i].y+2),CV_RGB(0,0,255));
cvLine(img,cvPoint(pts[i].x+2,pts[i].y-2),cvPoint(pts[i].x-2,pts[i].y+2),CV_RGB(0,0,255));
res[i]=3;
}
}
}
// 學習資料的現實
cvNamedWindow("SVM訓練樣本空間及分類",CV_WINDOW_AUTOSIZE);
cvShowImage("SVM訓練樣本空間及分類",img);
cvWaitKey(0);
// 學習引數的生成
for (i=0;i<s;++i)
{
data[i*2] = float(pts[i].x)/size;
data[i*2+1] = float(pts[i].y)/size;
}
cvInitMatHeader(&data_mat,s,2,CV_32FC1,data);
cvInitMatHeader(&res_mat,s,1,CV_32SC1,res);
criteria = cvTermCriteria(CV_TERMCRIT_EPS,1000,FLT_EPSILON);
param = CvSVMParams(CvSVM::C_SVC,CvSVM::RBF,10.0,8.0,1.0,10.0,0.5,0.1,NULL,criteria);
svm.train(&data_mat,&res_mat,NULL,NULL,param);
// 學習結果繪圖
for (i=0;i<size;i++)
{
for (j=0;j<size;j++)
{
CvMat m;
float ret = 0.0;
float a[] = {float(j)/size,float(i)/size};
cvInitMatHeader(&m,1,2,CV_32FC1,a);
ret = svm.predict(&m);
switch((int)ret)
{
case 1:
rcolor = CV_RGB(100,0,0);
break;
case 2:
rcolor = CV_RGB(0,100,0);
break;
case 3:
rcolor = CV_RGB(0,0,100);
break;
}
cvSet2D(img,i,j,rcolor);
}
}
// 為了顯示學習結果,通過對輸入影象區域的所有畫素(特徵向量)進行分類,然後對輸入的畫素用所屬顏色等級的顏色繪圖
for(i=0;i<s;++i)
{
CvScalar rcolor;
switch(res[i])
{
case 1:
rcolor = CV_RGB(255,0,0);
break;
case 2:
rcolor = CV_RGB(0,255,0);
break;
case 3:
rcolor = CV_RGB(0,0,255);
break;
}
cvLine(img,cvPoint(pts[i].x-2,pts[i].y-2),cvPoint(pts[i].x+2,pts[i].y+2),rcolor);
cvLine(img,cvPoint(pts[i].x+2,pts[i].y-2),cvPoint(pts[i].x-2,pts[i].y+2),rcolor);
}
// 支援向量的繪製
sv_num = svm.get_support_vector_count();
for (i=0; i<sv_num;++i)
{
support = svm.get_support_vector(i);
cvCircle(img,cvPoint((int)(support[0]*size),(int)(support[i]*size)),5,CV_RGB(200,200,200));
}
cvNamedWindow("SVM",CV_WINDOW_AUTOSIZE);
cvShowImage("SVM分類結果及支援向量",img);
cvWaitKey(0);
cvDestroyWindow("SVM");
cvReleaseImage(&img);
return 0;
}
訓練樣本空間及分類:
SVM分類效果及支援向量: