【轉(zhuǎn)】 利用Open CV和SVM實(shí)現(xiàn)問題識(shí)別轉(zhuǎn)自:http://www.cnblogs.com/justany/archive/2012/11/23/2784125.html http://www.cnblogs.com/justany/archive/2012/11/26/2788509.html http://www.cnblogs.com/justany/archive/2012/11/27/2789767.html
一、SVM介紹 分類器 分類器是一種計(jì)算機(jī)程序。 他的設(shè)計(jì)目標(biāo)是在通過學(xué)習(xí)后,可自動(dòng)將數(shù)據(jù)分到已知類別。
平面線性分類器 一個(gè)簡(jiǎn)單的分類問題,如圖有一些圓圈和一些正方形,如何找一條最優(yōu)的直線將他們分開? 我們可以找到很多種方法畫出這條直線,但怎樣的直線才是最優(yōu)的呢?
怎么尋找距離最遠(yuǎn)的直線?枚舉所有直線,然后計(jì)算其樣本最小距離?這樣顯然不是一個(gè)好辦法,這將產(chǎn)生大量的計(jì)算開銷。 我們利用另一種方法,對(duì)直線的正負(fù)偏移量1,這樣就產(chǎn)生了一個(gè)區(qū)域(下圖的Maximum margin覆蓋的區(qū)域),區(qū)域邊界上的點(diǎn)到直線的距離是固定的,現(xiàn)在的問題是最近的點(diǎn)是否剛好在邊界上或者在邊界外。 還記得點(diǎn)到線的公式么?
那么區(qū)域邊緣到直線的距離: distance = (|Ax+By+C| + 1)/ (A2 + B2)1/2 = 1/ (A2 + B2)1/2。 并需要滿足對(duì)于所有樣本類別yi 滿足:yi (Ax+By+C) > = 1,也就是所有樣本都不在該區(qū)域以內(nèi)。 于是我們可以找到適當(dāng)?shù)腁、B、C,從而得到: Maximum margin = 2/ (A2 + B2)1/2。
超平面推廣 同理,我們將這一定理推廣到任意維度。其超平面表達(dá)式為:
其中 叫做 權(quán)重向量 , 叫做 偏置向量。 用這種表達(dá)式來表達(dá)線Ax+By+C = 0的話,可以這么表示: f(x) = (C, 0) + (A, B)T (x, y); 其中(C, 0) 是偏置向量 ,(A, B)是權(quán)重向量 。 由于最優(yōu)超平面可以有很多種表達(dá)方式,我們定義: β0 + βTx = 0, 為最優(yōu)超平面表達(dá)式。于是我們可以得到他的Maximum margin區(qū)域邊界表達(dá)式應(yīng)該為:
我們稱在這邊界上的點(diǎn)為:支持向量(Supper Vector)。 因?yàn)辄c(diǎn)到超平面距離公式為:
在邊界上,即支持向量到超平面距離:
所以Maximum margin為兩倍距離,即:
將M求倒數(shù)1/M 則可將求最大轉(zhuǎn)換成求最小。于是有:
其中 表示樣本的類別標(biāo)記。 這是一個(gè)拉格朗日優(yōu)化問題,可以通過拉格朗日乘數(shù)法得到最優(yōu)超平面的權(quán)重向量 和偏置 。
什么是SVM 支持向量機(jī) (SVM) 是一個(gè)類分類器,正式的定義是一個(gè)能夠?qū)⒉煌悩颖驹跇颖究臻g分隔的超平面。 換句話說,給定一些標(biāo)記好的訓(xùn)練樣本 (監(jiān)督式學(xué)習(xí)),SVM算法輸出一個(gè)最優(yōu)化的分隔超平面。 1995年Cortes和Vapnik于首先提出SVM,它在解決小樣本、非線性及高維模式識(shí)別中表現(xiàn)出許多特有的優(yōu)勢(shì),并能夠推廣應(yīng)用到函數(shù)擬合等其他機(jī)器學(xué)習(xí)問題中。
使用SVM #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/ml/ml.hpp> using namespace cv; int main() { // 用于保存可視化數(shù)據(jù)的矩陣 int width = 512, height = 512; Mat image = Mat::zeros(height, width, CV_8UC3); // 創(chuàng)建一些訓(xùn)練樣本 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 trainingDataMat(3, 2, CV_32FC1, trainingData); // 設(shè)置SVM參數(shù) CvSVMParams params; params.svm_type = CvSVM::C_SVC; params.kernel_type = CvSVM::LINEAR; params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6); // 對(duì)SVM進(jìn)行訓(xùn)練 CvSVM SVM; SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params); Vec3b green(0,255,0), blue (255,0,0); // 將SVM斷定的分劃區(qū)域繪制出來 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; } // 繪制訓(xùn)練數(shù)據(jù)點(diǎn) 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); // 繪制支持向量 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(128, 128, 128), thickness, lineType); } imwrite("result.png", image); imshow("簡(jiǎn)單SVM分類", image); waitKey(0); }
建立訓(xùn)練樣本 這里通過Mat構(gòu)造函數(shù),建立了一個(gè)簡(jiǎn)單的訓(xùn)練樣本。 //建立一個(gè)標(biāo)簽矩陣 float labels[4] = {1.0, -1.0, -1.0, -1.0}; Mat labelsMat(3, 1, CV_32FC1, labels); //建立一個(gè)訓(xùn)練樣本矩陣 float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} }; Mat trainingDataMat(3, 2, CV_32FC1, trainingData); 由于CvSVM::train 要求樣本數(shù)據(jù)存儲(chǔ)在float 類型的Mat中,所以建立了float類型的Mat樣本。
設(shè)置SVM參數(shù)
OpenCV的SVM
分割結(jié)果
OpenCV的SVM是基于臺(tái)灣大學(xué)林智仁開發(fā)的LIBSVM開發(fā)包的。如果你還不過癮可以看看下面林智仁的演示程序(需要JAVA支持): http://www.csie./~cjlin/libsvm/ 在這個(gè)實(shí)驗(yàn)中,我們成功讓機(jī)器找到了區(qū)分樣品的線性劃分,并將其支持向量顯示出來。
被山寨的原文 Introduction to Support Vector Machines . OpenCV.org Support Vector Machines API . OpenCV.org
二、SVM線性不可分問題 目的
原理 上一篇文章,我們推導(dǎo)出對(duì)于線性可分?jǐn)?shù)據(jù),最佳劃分超平面應(yīng)滿足:
現(xiàn)在我們想引入一些東西,來表示那些被錯(cuò)分的數(shù)據(jù)點(diǎn)(比如噪點(diǎn)),對(duì)劃分的影響。 如何來表示這些影響呢? 被錯(cuò)分的點(diǎn),離自己應(yīng)當(dāng)存在的區(qū)域越遠(yuǎn),就代表了,這個(gè)點(diǎn)“錯(cuò)”得越嚴(yán)重。 所以我們引入,為對(duì)應(yīng)樣本離同類區(qū)域的距離。 接下來的問題是,如何將這種錯(cuò)的程度,轉(zhuǎn)換為和原模型相同的度量呢? 我們?cè)僖胍粋€(gè)常量C,表示和原模型度量的轉(zhuǎn)換關(guān)系,用C對(duì)進(jìn)行加權(quán)和,來表征錯(cuò)分點(diǎn)對(duì)原模型的影響,這樣我們得到新的最優(yōu)化問題模型:
關(guān)于參數(shù)C的選擇, 明顯的取決于訓(xùn)練樣本的分布情況。 盡管并不存在一個(gè)普遍的答案,但是記住下面幾點(diǎn)規(guī)則還是有用的:
開始使用 #include <iostream> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/ml/ml.hpp> #define NTRAINING_SAMPLES 100 // 每類訓(xùn)練樣本的數(shù)量 #define FRAC_LINEAR_SEP 0.9f // 線性可分部分的樣本組成比例 using namespace cv; using namespace std; int main(){ // 用于顯示的數(shù)據(jù) const int WIDTH = 512, HEIGHT = 512; Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3); /* 1. 隨即產(chǎn)生訓(xùn)練數(shù)據(jù) */ Mat trainData(2*NTRAINING_SAMPLES, 2, CV_32FC1); Mat labels (2*NTRAINING_SAMPLES, 1, CV_32FC1); RNG rng(100); // 生成隨即數(shù) // 設(shè)置線性可分的訓(xùn)練數(shù)據(jù) int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES); // 生成分類1的隨機(jī)點(diǎn) Mat trainClass = trainData.rowRange(0, nLinearSamples); // 點(diǎn)的x坐標(biāo)在[0, 0.4)之間 Mat c = trainClass.colRange(0, 1); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(0.4 * WIDTH)); // 點(diǎn)的y坐標(biāo)在[0, 1)之間 c = trainClass.colRange(1,2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); // 生成分類2的隨機(jī)點(diǎn) trainClass = trainData.rowRange(2*NTRAINING_SAMPLES-nLinearSamples, 2*NTRAINING_SAMPLES); // 點(diǎn)的x坐標(biāo)在[0.6, 1]之間 c = trainClass.colRange(0 , 1); rng.fill(c, RNG::UNIFORM, Scalar(0.6*WIDTH), Scalar(WIDTH)); // 點(diǎn)的y坐標(biāo)在[0, 1)之間 c = trainClass.colRange(1,2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); /* 設(shè)置非線性可分的訓(xùn)練數(shù)據(jù) */ // 生成分類1和分類2的隨機(jī)點(diǎn) trainClass = trainData.rowRange( nLinearSamples, 2*NTRAINING_SAMPLES-nLinearSamples); // 點(diǎn)的x坐標(biāo)在[0.4, 0.6)之間 c = trainClass.colRange(0,1); rng.fill(c, RNG::UNIFORM, Scalar(0.4*WIDTH), Scalar(0.6*WIDTH)); // 點(diǎn)的y坐標(biāo)在[0, 1)之間 c = trainClass.colRange(1,2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); /* 設(shè)置分類標(biāo)簽 */ labels.rowRange( 0, NTRAINING_SAMPLES).setTo(1); // Class 1 labels.rowRange(NTRAINING_SAMPLES, 2*NTRAINING_SAMPLES).setTo(2); // Class 2 /* 設(shè)置支持向量機(jī)參數(shù) */ 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. 訓(xùn)練支持向量機(jī) */ cout << "Starting training process" << endl; CvSVM svm; svm.train(trainData, labels, Mat(), Mat(), params); cout << "Finished training process" << endl; /* 4. 顯示劃分區(qū)域 */ 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. 顯示訓(xùn)練數(shù)據(jù) */ int thick = -1; int lineType = 8; float px, py; // 分類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); } // 分類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. 顯示支持向量 */ 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); // 保存圖片 imshow("SVM線性不可分?jǐn)?shù)據(jù)劃分", I); // 顯示給用戶 waitKey(0); }
設(shè)置SVM參數(shù) 這里的參數(shù)設(shè)置可以參考一下上一篇文章的API。 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); 可以看到,這次使用的是C類支持向量分類機(jī)。其參數(shù)C的值為0.1。
結(jié)果
被山寨的原文 Support Vector Machines for Non-Linearly Separable Data . OpenCV.org
三、SVM文字識(shí)別 預(yù)備知識(shí) 下面兩個(gè)都不是必備知識(shí),但是如果你想了解更多內(nèi)容,可參考這兩篇文章。
SVM劃分的意義 到此,我們已經(jīng)對(duì)SVM有一定的了解了??墒沁@有什么用呢?回到上一篇文章結(jié)果圖: 這個(gè)結(jié)果圖的意義在于,他成功從二維劃分了分類的區(qū)域。于是如果以后,有一個(gè)新的樣本在綠色區(qū)域,那么我們就可以把他當(dāng)成是綠色的點(diǎn)。 由于這可以像更高維度推廣,所以如果我們能對(duì)樣品映射成高維度空間的點(diǎn),當(dāng)有足夠多的樣品時(shí),我們同樣可以找到一個(gè)高維度的超平面劃分,使得同一類樣品的映射點(diǎn)在同一區(qū)域,于是當(dāng)有新樣品落在這些區(qū)域是,我們可以把它當(dāng)成是這一類型的樣本。
通俗一點(diǎn) 可能我們能更加通俗一點(diǎn)。比如我們來識(shí)別男性和女性。 我們發(fā)現(xiàn)男性和女性可能頭發(fā)長(zhǎng)度不一樣,可能胸圍不一樣,于是我們對(duì)樣本個(gè)體產(chǎn)生這樣的一種映射: 人 —> (頭發(fā)長(zhǎng)度, 胸圍) 于是我們將每個(gè)樣品映射到二維平面,其中“頭發(fā)的長(zhǎng)度”和“胸圍的長(zhǎng)度”分別是x軸和y軸。我們把這些樣品丟給SVM學(xué)習(xí),則他會(huì)尋找出一個(gè)合理的x和y的區(qū)域來劃分男性和女性。 當(dāng)然,也有可能有些男的頭發(fā)比女的還長(zhǎng),有的男性的胸圍比女性還大,這些就是錯(cuò)分點(diǎn),它們也影響著劃分。 最后,當(dāng)我們把一個(gè)人映射到這個(gè)二維空間時(shí),SVM就可以根據(jù)以往的學(xué)習(xí),猜一猜這個(gè)人到底是什么性別。 我們學(xué)到了什么呢?
如果這是老板,你就可死翹翹了……
簡(jiǎn)單的文字識(shí)別 當(dāng)然計(jì)算機(jī)沒那么厲害能看出你的胸圍或者頭發(fā)長(zhǎng)短。他需要一些他能讀懂的東西,特別計(jì)算機(jī)通?!翱吹健钡氖窍旅娴倪@種東東…… 我們需要對(duì)文字找到他的特征,來映射到高維空間。 還記得小學(xué)時(shí)候練字的米字格么?這似乎暗示了我們,雖然每個(gè)人寫的字千差萬(wàn)別,但是他們卻具有一定的特點(diǎn)。 我們嘗試這樣做,取一個(gè)字,選取一個(gè)包含該字的正方形區(qū)域,將這個(gè)正方形區(qū)域分割成8*8個(gè)小格,統(tǒng)計(jì)每個(gè)小格中像素的數(shù)量,以這些數(shù)量為維度進(jìn)行映射。 OK,明白了原理讓我們開始吧。
樣本獲取 由于通常文字樣本都是白底黑字的,而手寫也可以直接獲取寫入的數(shù)據(jù)而無視背景,所以我們并不需要對(duì)樣本進(jìn)行提取,但我們需要對(duì)他定位,并弄成合適的大小。 比如,你沒法避免有人這么寫字…… 坑爹啊,好好的那么大地方你就躲在左上角……
開始定位 void getROI(Mat& src, Mat& dst){ int left, right, top, bottom; left = src.cols; right = 0; top = src.rows; bottom = 0; //得到區(qū)域 for(int i=0; i<src.rows; i++) { for(int j=0; j<src.cols; j++) { if(src.at<uchar>(i, j) > 0) { if(j<left) left = j; if(j>right) right = j; if(i<top) top = i; if(i>bottom) bottom = i; } } } int width = right - left; int height = bottom - top; //創(chuàng)建存儲(chǔ)矩陣 dst = Mat::zeros(width, height, CV_8UC1); Rect dstRect(left, top, width, height); dst(dstRect); } 這段代碼通過遍歷所有圖像矩陣的元素,來獲取該樣本的定位和大小。并把樣本提取出來。
重新縮放 Mat dst = Mat::zeros(8, 8, CV_8UC1); 進(jìn)行縮放,把所有樣本變成8*8的大小。為了簡(jiǎn)便,我們把像素多少變成了像素的灰度值。 resize的API:
準(zhǔn)備樣本數(shù)據(jù) Mat data = Mat::zeros(total, 64, CV_32FC1); //樣本數(shù)據(jù)矩陣 Mat res = Mat::zeros(total, 1, CV_32SC1); //樣本標(biāo)簽矩陣 res.at<double>(k, 1) = label; //對(duì)第k個(gè)樣本添加分類標(biāo)簽 //對(duì)第k個(gè)樣本添加數(shù)據(jù) for(int i = 0; i<8; i++) { for(int j = 0; j<8; j++) { res.at<double>(k, i * 8 + j) = dst.at<double>(i, j); } } 將剛剛的結(jié)果,輸入樣本,并加上標(biāo)簽。
訓(xùn)練 CvSVM svm = CvSVM(); CvSVMParams param; CvTermCriteria criteria; 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, res, Mat(), Mat(), param); svm.save( "SVM_DATA.xml" ); 開始訓(xùn)練并保存訓(xùn)練數(shù)據(jù)。
使用 CvSVM svm = CvSVM(); svm.load( "SVM_DATA.xml" ); svm.predict(m); //對(duì)樣本向量m檢測(cè)
參考資料 使用OPENCV訓(xùn)練手寫數(shù)字識(shí)別分類器 . firefight . 2011-05-28 |
|