1. 程式人生 > >機器學習之路(一)---支援向量機SVM

機器學習之路(一)---支援向量機SVM

##什麼是支援向量機(SVM)?
支援向量機 (SVM) 是一個類分類器,正式的定義是一個能夠將不同類樣本在樣本空間分隔的超平面。 換句話說,給定一些標記(label)好的訓練樣本 (監督式學習), SVM演算法輸出一個最優化的分隔超平面。

如何來界定一個超平面是不是最優的呢? 考慮如下問題:

假設給定一些分屬於兩類的2維點,這些點可以通過直線分割, 我們要找到一條最優的分割線.
![此處輸入圖片的描述][1]
在上面的圖中, 你可以直覺的觀察到有多種可能的直線可以將樣本分開。 那是不是某條直線比其他的更加合適呢? 我們可以憑直覺來定義一條評價直線好壞的標準:

距離樣本太近的直線不是最優的,因為這樣的直線對噪聲敏感度高,泛化性較差。 因此我們的目標是找到一條直線,離所有點的距離最遠。
由此, SVM演算法的實質是找出一個能夠將某個值最大化的超平面,這個值就是超平面離所有訓練樣本的最小距離。這個最小距離用SVM術語來說叫做 間隔(margin) 。 概括一下,最優分割超平面 最大化 訓練資料的間隔。
![此處輸入圖片的描述][2]

##如何計算最優超平面?
下面的公式定義了超平面的表示式:

![此處輸入圖片的描述][3]

![此處輸入圖片的描述][4] 叫做 權重向量 , ![此處輸入圖片的描述][5] 叫做 偏置(bias) 。

最優超平面可以有無數種表達方式,即通過任意的縮放 ![此處輸入圖片的描述][6] 和 ![此處輸入圖片的描述][7] 。 習慣上我們使用以下方式來表達最優超平面:

![此處輸入圖片的描述][8]

式中 x 表示離超平面最近的那些點。 這些點被稱為**支援向量****。 **該超平面也稱為 canonical 超平面.

通過幾何學的知識,我們知道點 x 到超平面 (![此處輸入圖片的描述][9] , ![此處輸入圖片的描述][10] ) 的距離為:

![此處輸入圖片的描述][11]

特別的,對於 canonical 超平面, 表示式中的分子為1,因此支援向量到canonical 超平面的距離是:

![此處輸入圖片的描述][12]

剛才我們介紹了間隔(margin),這裡表示為 M, 它的取值是最近距離的2倍:

![此處輸入圖片的描述][13]

最後最大化![此處輸入圖片的描述][14]轉化為在附加限制條件下最小化函式 L(![此處輸入圖片的描述][15] ) 。 限制條件隱含超平面將所有訓練樣本 ![此處輸入圖片的描述][16]正確分類的條件:

![此處輸入圖片的描述][17]

式中![此處輸入圖片的描述][18]表示樣本的類別標記。

這是一個拉格朗日優化問題,可以通過拉格朗日乘數法得到最優超平面的權重向量 ![此處輸入圖片的描述][19] 和偏置![此處輸入圖片的描述][20] 。

##程式碼實現

// TestProj.cpp : 定義控制檯應用程式的入口點。
//

#include "stdafx.h"
#include <iostream>  
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/imgcodecs/imgcodecs.hpp>
#include <opencv2/ml/ml.hpp>

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

int main()
{
	// Data for visual representation
	int width = 512, height = 512;
	Mat image = Mat::zeros(height, width, CV_8UC3);

	// Set up training data
	int labels[4] = { 1, -1, -1, -1 };
	Mat labelsMat(4, 1, CV_32SC1, labels); //將labels轉換成4行1列的32位單通道字元型陣列

	float trainingData[4][2] = { { 501, 10 }, { 255, 10 }, { 501, 255 }, { 10, 501 } };
	Mat trainingDataMat(4, 2, CV_32FC1, trainingData);

	// Set up SVM's parameters
	Ptr<SVM> mySvm;
	mySvm = SVM::create();//建立一個空的svm檔案
	mySvm->setType(SVM::Types::C_SVC);
	mySvm->setKernel(SVM::KernelTypes::LINEAR);
	mySvm->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6));//SVM的迭代訓練過程的終止條件

	// Train the SVM
	Ptr<TrainData> tData = TrainData::create(trainingDataMat, ROW_SAMPLE, labelsMat);
	mySvm->train(tData);

	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);//將每個i, j按照順序輸入進Mat
		float response = mySvm->predict(sampleMat);

		if (response == 1)
			image.at<Vec3b>(j, i) = green;
		else if (response == -1)
			image.at<Vec3b>(j, i) = blue;
	}

	// Show the training data
	int thickness = -1;
	int lineType = 8;
	circle(image, Point(501, 10), 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);

	// Show support vectors
	thickness = 2;
	lineType = 8;
	Mat sv = mySvm->getUncompressedSupportVectors();
	for (int i = 0; i < sv.rows; ++i)
	{
		const float* v = sv.ptr<float>(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);
	cout << "done" << endl;
}

##程式碼解釋
###版本差異說明
此程式碼主要參考中文官網文件,並作修改。因為官網程式碼是2.3版本,本次執行的是3.2版本,許多函式已經廢棄了。
###建立訓練樣本
本例中的訓練樣本由分屬於兩個類別的2維點組成, 其中一類包含一個樣本點,另一類包含三個點。

	// Set up training data
	int labels[4] = { 1, -1, -1, -1 };
	Mat labelsMat(4, 1, CV_32SC1, labels); //將labels轉換成4行1列的32位單通道字元型陣列

	float trainingData[4][2] = { { 501, 10 }, { 255, 10 }, { 501, 255 }, { 10, 501 } };
	Mat trainingDataMat(4, 2, CV_32FC1, trainingData);

###設定SVM引數
我們以可線性分割的分屬兩類的訓練樣本簡單講解了SVM的基本原理。 然而,SVM的實際應用情形可能複雜得多 (比如非線性分割資料問題,SVM核函式的選擇問題等等)。 總而言之,我們需要在訓練之前對SVM做一些引數設定。

	// Set up SVM's parameters
	Ptr<SVM> mySvm;
	mySvm = SVM::create();//建立一個空的svm檔案
	mySvm->setType(SVM::Types::C_SVC);
	mySvm->setKernel(SVM::KernelTypes::LINEAR);
	mySvm->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6));//SVM的迭代訓練過程的終止條件

####SVM型別

  • 這裡我們選擇了 SVM::C_SVC 型別,該型別可以用於n-類分類問題 (n >= 2)。

SVM::C_SVC 型別的重要特徵是它可以處理非完美分類的問題 (及訓練資料不可以完全的線性分割)。在本例中這一特徵的意義並不大,因為我們的資料是可以線性分割的,我們這裡選擇它是因為它是最常被使用的SVM型別。
SVM::NU_SVC : 類支援向量分類機。n類似然不完全分類的分類器。引數為取代C(其值在區間【0,1】中,nu越大,決策邊界越平滑)。
SVM::ONE_CLASS : 單分類器,所有的訓練資料提取自同一個類裡,然後SVM建立了一個分界線以分割該類在特徵空間中所佔區域和其它類在特徵空間中所佔區域。
SVM::EPS_SVR : 類支援向量迴歸機。訓練集中的特徵向量和擬合出來的超平面的距離需要小於p。異常值懲罰因子C被採用。
SVM::NU_SVR : 類支援向量迴歸機。 代替了 p。

####SVM 核函式(4種)
SVM 核型別. 我們沒有討論核函式,因為對於本例的樣本,核函式的討論沒有必要。然而,有必要簡單說一下核函式背後的主要思想, 核函式的目的是為了將訓練樣本對映到更有利於可線性分割的樣本集。 對映的結果是增加了樣本向量的維度,這一過程通過核函式完成。 此處我們選擇的核函式型別是 CvSVM::LINEAR 表示不需要進行對映。

SVM::LINEAR : 線性核心,沒有任何向對映至高維空間,線性區分(或迴歸)在原始特徵空間中被完成,這是最快的選擇。
這裡寫圖片描述

SVM::POLY : 多項式核心:
這裡寫圖片描述

SVM::RBF : 基於徑向的函式,對於大多數情況都是一個較好的選擇:
這裡寫圖片描述

SVM::SIGMOID : Sigmoid函式核心:
這裡寫圖片描述

####核心引數

  • degree:核心函式(POLY)的引數degree。
  • gamma:核心函式(POLY/ RBF/ SIGMOID)的引數。
  • coef0:核心函式(POLY/ SIGMOID)的引數coef0。
  • Cvalue:SVM型別(C_SVC/ EPS_SVR/ NU_SVR)的引數C。
  • nu:SVM型別(NU_SVC/ ONE_CLASS/ NU_SVR)的引數 。
  • p:SVM型別(EPS_SVR)的引數。
  • class_weights:C_SVC中的可選權重,賦給指定的類,乘以C以後變成 。所以這些權重影響不同類別的錯誤分類懲罰項。權重越大,某一類別的誤分類資料的懲罰項就越大。
  • term_crit:SVM的迭代訓練過程的中止條件,解決部分受約束二次最優問題。您可以指定的公差和/或最大迭代次數。

當我們選擇LINEAR為核心的時候,上述核心引數都是不需要的,另外三個核心的時候,需要對其分別進行設定,設定後可以進行直接訓練:

svm_ = cv::ml::SVM::create();
    svm_->setType(cv::ml::SVM::C_SVC);
    svm_->setKernel(cv::ml::SVM::RBF);
    svm_->setDegree(0.1);
    // 1.4 bug fix: old 1.4 ver gamma is 1  //1.4版本bug修復
    svm_->setGamma(1);
    svm_->setCoef0(0.1);
    svm_->setC(1);
    svm_->setNu(0.1);
    svm_->setP(0.1);
    svm_->setTermCriteria(cvTermCriteria(CV_TERMCRIT_ITER, 20000, 0.0001));
  svm_->train(train_data);

演算法終止條件.

SVM訓練的過程就是一個通過 迭代 方式解決約束條件下的二次優化問題,這裡我們指定一個最大迭代次數和容許誤差,以允許演算法在適當的條件下停止計算。 該引數定義在 cvTermCriteria 結構中。

#define CV_TERMCRIT_ITER    1  
#define CV_TERMCRIT_NUMBER  CV_TERMCRIT_ITER  
#define CV_TERMCRIT_EPS     2  
typedef struct CvTermCriteria  {  
 int    type;  /* CV_TERMCRIT_ITER 和CV_TERMCRIT_EPS二值之一,或者二者的組合 */  
 int    max_iter; /* 最大迭代次數 */
 double epsilon; /* 結果的精確性 */  
}  
 CvTermCriteria;  
/* 建構函式 */  
inline  CvTermCriteria  cvTermCriteria( int type, int max_iter, double epsilon );  
//訓練完成後儲存txt或者xml檔案
svm->save("svm_image.xml");

// 載入模型的語句:
Ptr<SVM> svmp =  SVM::load<SVM>("svm_image.xml");

/*載入之後就可以進行預測了*/
//返回的是預測資料距離決策面(超平面)的幾何距離
float response = svmp->predict(sampleMat, noArray(), StatModel::Flags::RAW_OUTPUT);

//返回的是標籤分類
float response = svmp->predict(sampleMat, noArray(), 0);
float response = svmp->predict(sampleMat);

###SVM區域分割

函式 CvSVM::predict 通過重建訓練完畢的支援向量機來將輸入的樣本分類。 本例中我們通過該函式給向量空間著色, 及將影象中的每個畫素當作卡迪爾平面上的一點,每一點的著色取決於SVM對該點的分類類別:綠色表示標記為1的點,藍色表示標記為-1的點。

###支援向量

這裡用了幾個函式來獲取支援向量的資訊。 函式 CvSVM::get_support_vector_count 輸出支援向量的數量,函式 CvSVM::get_support_vector 根據輸入支援向量的索引來獲取指定位置的支援向量。 通過這一方法我們找到訓練樣本的支援向量並突出顯示它們。

	// Show support vectors
	thickness = 2;
	lineType = 8;
	Mat sv = mySvm->getUncompressedSupportVectors();
	for (int i = 0; i < sv.rows; ++i)
	{
		const float* v = sv.ptr<float>(i);
		circle(image, Point((int)v[0], (int)v[1]), 6, Scalar(0, 0, 255), thickness, lineType);
	}

##執行結果

  • 程式建立了一張影象,在其中顯示了訓練樣本,其中一個類顯示為白色圓圈,另一個類顯示為黑色圓圈。
  • 訓練得到SVM,並將影象的每一個畫素分類。 分類的結果將影象分為藍綠兩部分,中間線就是最優分割超平面。
  • 最後支援向量通過紅色邊框加重顯示。

這裡寫圖片描述