註釋OpenCV的支持向量機與範例

對於一些數據的分類算法中,在人工神經網絡以前所用的最多的還是支持向量機,支持向量機的好處是既可以支持離散的數據分類,也可以支持連續的數據分類。支持向量機本身是通過一個超平面來計算特定的點是否爲特定的分類。超平面可以是一個一維數軸上的點,也可以是一個二維平面上的直線,也可以三維空間中的一個平面,高於三維空間的N維空間,必定有一個N-1維的超平面可以對N維空間進行分隔,即便是在一個M維空間中不能線性可分的樣本集,那麼總可以映射到一個M+\epsilon(\epsilon >= 1, \epsilon = 1,2,3...)維空間使得樣本線性可分,這樣的空間有不可數個,我們可以通過核技巧來實現,由此我們會引入核函數

1、線性可分支持向量機

我們看得出來,由於在得知哪一個是合適的支持向量模型以前,我們需要通過一組樣本空間\mathfrak{D},其中的某一個子集D \in \mathfrak{D} \subseteq \mathbf{R}^n,並且爲這組數據設置一組一一對應的分類標籤,標籤的取值空間爲y \in \mathfrak{Y} = \{ +1 , -1\},爲提供給支持向量機的訓練依據。因此支持向量機是一種監督式學習, D有一組樣本數據是 D = [ \vec x _1, \vec x _2, \vec x_3, ... , \vec x_i, \vec y ], i = 1,2,3... 。當然,這種表示法無法直觀的表示單個樣本數據。因此用行向量表示法更好。

我們轉置一下\vec x_i,仍然用\vec x_i本身表示。行向量表示法爲將向量轉置以後用小括號表示。即行向量 (\vec x_1, y_1),(\vec x_2, y_2)..., 一個行向量表示一個樣本並對應一個樣本標籤(分類),即可以理解爲$$(\vec x_i, y_i) = (x_{i1}, x_{i2}, x_{i3},..., x_{ij}, y ), i=1,2,3....;j=1,2,3...$$

i爲第i行數據,或爲第i個樣本點。j爲樣本點中第j個屬性數據。

 

圖片來自OpenCV 官方文檔。

支持向量機的目的就是爲了找到一組向量對應的點,在這組向量中間找到一個超平面(2D平面上爲直線)

這個超平面就是可以看作一個簡單的線性函數:

f(\vec x)=\vec {w} ^{T} \vec {x} + b

但是由於在兩部分區域間線性可分的兩種不同分類樣本中,可以爲兩種分類樣本分離的超平面線性函數有無數個,我們選擇最優的那個條件有兩點:兩種分類之間分別有幾個樣本點之間歐式距離最近,那麼最優的直線肯定是通過這幾個互相距離對方分類最近的兩個樣本點之間距離的一半的點,我們能得出任意樣本點距離超平面的距離

d = \frac {|\vec w^T\vec x + b|}{||\vec w||} = \frac {\gamma}{||\vec w||},並且爲了計算簡單,可以使d = \frac {|\vec w^T\vec x + b|}{||\vec w||} = \frac {1}{||\vec w||} . 因爲我們最終要得到的是一個最小,一個最大,那麼這就牽扯到兩個條件優化問題. 我們要得到的是最小的兩個不同分類樣本點的距離,並且在這些樣本找到其中能夠與超平面距離最大的分隔,即帶符號的距離。

問題就成了求最優解問題:

max_{(\vec w, b)} \: \frac{\gamma}{||\vec w||}

subject \: to: y_i (\vec w^T \vec x_i + b) \geqslant \gamma, i= 1,2,3.... ,因此 \gamma = 1 或取其他值時不影響w和b。

最後線性可分支持向量機的學習算法就成了一個最優化求解問題:

min _{(\vec w, b)} \: \frac{1}{||\vec w||} \, or \, max _{(\vec w, b)} \: \frac{||\vec w||^2}{2} 這兩個最優化條件等價

subject \: to: y_i (\vec w^T \vec x_i + b) - 1 \geqslant 0, i= 1,2,3....

得到的\vec wb 可以得到超平面 \vec w ^T \vec x + b = 0,分類決策函數只需要使用sign函數獲取符號

f(x) = sign(\vec w ^T \vec x + b )

就可以得知爲二分類中的正類還是負類

 

2、非線性可分支持向量機

非線性可分支持向量機,情況就比較複雜一些。

損失優化方法

損失優化方法可以對少量線性不可分樣本進行容錯分類,線性不可分樣本集合如下圖

對於不可分樣本點,允許一定的誤分類誤差存在,誤分類點的到支持向量的比例因子爲\xi_i,i爲第i個誤分類樣本,引入懲罰因子C,將線性不可分問題轉化爲

min _{\vec w,b,\xi} \; \frac{1}{2}||\vec w||^2+C\sum_{i=1}^N\xi_i

subject \; to: \; y_i(\vec w\cdot \vec x_i+b)\geq\,1-\xi_i, \xi_i \geqslant 0

懲罰因子C越大時,對誤分類懲罰較大,會導致過擬合,C越小時對誤分類懲罰較小,會導致欠擬合。

核技巧

這種方法的思想是,將支持向量機轉化成對偶優化問題以後(拉格朗日乘子法優化方法),將\vec x_i 通過映射函數\phi(\vec x_i) 到新的線性可分空間中,並且定義核函數 K(\vec x, \vec z) = \phi (\vec x) \cdot \phi (\vec z),  在對偶方法中將 \vec x_i \cdot \vec x_j替換爲K(\vec x, \vec z) 以後,使得在更高維度空間中線性可分,最後再求解對偶問題即可。

關於SVM對偶問題最優化解法以及SMO算法暫不在此詳述。

 

3、OpenCV的SVM非線性可分支持向量機C++範例

 

以下附上OpenCV文檔中的訓練和預測非線性可分支持向量機的範例以及註釋詳解。

OpenCV中的非線性支持向量機封裝了大量SVM的內部實現,我們只需要瞭解其使用方法即可,OpenCV的範例是個極佳的示範:

#include <iostream>
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
using namespace cv;
using namespace cv::ml;
using namespace std;
static void help() {
	cout
			<< "\n--------------------------------------------------------------------------"
			<< endl
			<< "This program shows Support Vector Machines for Non-Linearly Separable Data. "
			<< endl
			<< "--------------------------------------------------------------------------"
			<< endl << endl;
}
/**
 * 訓練數據集格式爲(x,y,label),第一列和第二列是點的座標列,label爲標籤列
 */
int main() {
	help();
	const int NTRAINING_SAMPLES = 100;   // 每個類別訓練樣本數
	const float FRAC_LINEAR_SEP = 0.9f; // 組合線性可分樣本的比例
	// 數據可視化
	const int WIDTH = 512 /**列大小*/, HEIGHT = 512 /**行大小*/;
	Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3);	// 構建一個全零矩陣,形狀爲512x512
	//--------------------- 1. 隨機設置訓練數據 ---------------------------------------
	// 訓練數據矩陣
	Mat trainData(2 * NTRAINING_SAMPLES, 2, CV_32F);// 行數在本例子中是 2 x NTRAINING_SAMPLES = 200,列數:2
	Mat labels(2 * NTRAINING_SAMPLES, 1, CV_32S);// 標籤結果行數是 2 x NTRAINING_SAMPLES = 200,列數: 2
	RNG rng(100);// 生成類隨機值
	// 設置線性可分部分訓練數據
	int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES);//線性可分數據數量爲0.9 x 100 = 90
	// 生成類別 1隨機點
	Mat trainClass = trainData.rowRange(0, nLinearSamples);//訓練數據行[0, 90),一共90個點
	// 生成[0, 0.4)爲範圍的點 x 座標
	Mat c = trainClass.colRange(0, 1);//第1列,範圍[0,1),且爲整數,因此選取的列爲僅第1列
	rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(0.4 * WIDTH));//填充以[0, 0.4x512)之間的均勻分佈隨機數值
	// 生成[0, 1) 爲範圍的點 y 座標
	c = trainClass.colRange(1, 2);//第2列,範圍[1,2),且爲整數,因此選取的列爲僅第2列
	rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充以[0, 512)之間的均勻分佈隨機數值
	// 生成類別 2隨機點
	trainClass = trainData.rowRange(2 * NTRAINING_SAMPLES - nLinearSamples,
			2 * NTRAINING_SAMPLES);// 類別2點存放的範圍[2x 100 - 90, 2 x 100) = [110 ,200),一共90個點

	// 位於[0.6, 1]的點 x 座標
	c = trainClass.colRange(0, 1);//選定範圍[0,1),也就是第1列x座標列
	rng.fill(c, RNG::UNIFORM, Scalar(0.6 * WIDTH), Scalar(WIDTH));//填充點的 x 座標的範圍爲[0.6x512,512),均勻隨機分佈
	// 位於[0,1) 的點的y座標
	c = trainClass.colRange(1, 2);//選定列範圍[1,2),也就是第2列y座標列
	rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充點的 y 座標的範圍爲[0,512),均勻隨機分佈
	//------------------ 設置非線性可分部分的訓練數據 ---------------
	// 生成類別1和2的隨機點
	trainClass = trainData.rowRange(nLinearSamples,
			2 * NTRAINING_SAMPLES - nLinearSamples);//生成分屬1和2的混合隨機樣本點,在訓練集的範圍中是[90, 2x100-90) = [90, 110)

	//  生成[0.4, 0.6) 範圍的x座標
	c = trainClass.colRange(0, 1);
	rng.fill(c, RNG::UNIFORM, Scalar(0.4 * WIDTH), Scalar(0.6 * WIDTH));//填充隨機數範圍[0.4x512, 0.6x512),均勻隨機分佈
	// 生成[0, 1) 範圍的y座標
	c = trainClass.colRange(1, 2);
	rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充隨機數範圍[0, 512),均勻隨機分佈
	//------------------------- 設置類別標籤 ---------------------------------
	labels.rowRange(0, NTRAINING_SAMPLES).setTo(1);  // 在標籤列上爲範圍[0, 100)設置類別爲 1
	labels.rowRange(NTRAINING_SAMPLES, 2 * NTRAINING_SAMPLES).setTo(2); // 在標籤列上爲範圍[100, 200)設置類別爲 2
	//以上準備數據集準備完畢。
	//------------------------ 2. 設置支持向量機向量--------------------
	cout << "Starting training process" << endl;
	Ptr<SVM> svm = SVM::create();
	svm->setType(SVM::C_SVC);
	svm->setC(0.1);//設置允許錯誤分類閾值C
	svm->setKernel(SVM::LINEAR);//設置爲線性核函數
	svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int) 1e7, 1e-6));
	//------------------------ 3. 訓練支持向量機 ----------------------------------------------------
	svm->train(trainData, ROW_SAMPLE, labels);//開始訓練。告知訓練器爲行樣本和標籤,訓練過程是個耗時過程。
	cout << "Finished training process" << endl;
	//------------------------ 4. 展示分割區域 ----------------------------------------
	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++) {
			//爲了更好的展示分隔線,會將整張512x512的圖上所有點進行分類預測,如此以來整張背景圖都是分類區域圖
			//因此要對每個點進行分類預測
			Mat sampleMat = (Mat_<float>(1, 2) << j, i);
			//預測點的樣本分類
			float response = svm->predict(sampleMat);
			//針對分類進行顏色賦值,1爲綠色,2爲藍色
			if (response == 1)
				I.at<Vec3b>(i, j) = green;
			else if (response == 2)
				I.at<Vec3b>(i, j) = blue;
		}
	}
	//----------------------- 5. 展示訓練數據 --------------------------------------------
	//圓圈外邊緣
	int thick = -1;
	float px, py;
	// 類別 1
	// 針對類別1進行分類的樣本點展示
	for (int i = 0; i < NTRAINING_SAMPLES; i++) {//訓練數據的前100個點
		px = trainData.at<float>(i, 0);//第i個樣本的x座標
		py = trainData.at<float>(i, 1);//第i個樣本的y座標
		circle(I, Point((int) px, (int) py), 3, Scalar(0, 255, 0), thick);//在整張展示圖I中繪製點x,y
	}
	// 類別 2
	// 針對類別1進行分類的樣本點展示
	for (int i = NTRAINING_SAMPLES; i < 2 * NTRAINING_SAMPLES; i++) {//訓練數據的後100個點
		px = trainData.at<float>(i, 0);;//第i個樣本的x座標
		py = trainData.at<float>(i, 1);//第i個樣本的y座標
		circle(I, Point((int) px, (int) py), 3, Scalar(255, 0, 0), thick);//在整張展示圖I中繪製點x,y
	}
	//------------------------- 6. 展示支持向量 --------------------------------------------
	//圓點的外邊緣爲2,着重顯示支持向量的點
	thick = 2;
	//獲取支持向量
	Mat sv = svm->getUncompressedSupportVectors();
	for (int i = 0; i < sv.rows; i++) {
		//讀取第i個支持向量的點座標
		const float *v = sv.ptr<float>(i);
		//繪製支持向量的圓點,並且着重顯示這些點
		circle(I, Point((int) v[0], (int) v[1]), 6, Scalar(128, 128, 128),
				thick);
	}
	cout<<endl<<"Support Vectors:  "<<sv<<endl;
	imwrite("result.png", I);                      // 保存圖片
	imshow("SVM for Non-Linear Training Data", I); // 展示給用戶
	waitKey();
	return 0;
}

本例中使用的OpenCV版本號爲3.4.3,使用其他版本無法保證API仍然有效。

環境:

Ubuntu 18.04.3,附帶GTK+模塊編譯。

CMake 3.10

GCC 7.4.0

編譯通過。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章