支持向量机(SVM)非线性数据切割
1.目标
本指导中你将学到:
l 当不可能线性切割训练数据时,如何定义SVM最优化问题。
l 在这样的问题上。如何配置CvSVMParams中的參数满足你的SVM;
2.动机
为什么我们有兴趣扩展SVM最优化问题来处理非线性切割训练数据?SVM在计算机视觉应用中须要一个比线性分类器更加强有力的工具。
原因在于,其实,在这样的问题上训练数据差点儿不能被一个超平面切割开。考虑一个这样的任务。比如,面部识别。
这样的情况下,训练数据由图像上的一组面部数据和非面部(不论什么除面部以外的其它东西)数据组成。这些训练数据很复杂,以至于能够对每个样本找到一种表述(特征向量),能把整个数据线性的从非面部中切割出来。
3.最优化问题扩展
记住,通过SVM我们得到一个切割超平面。
那么,由于如今训练数据是非线性可分的了。我们必须承认原来找到的超平面将不能正确分类当中的一些样本。
误分类是优化问题中须要考虑的一个新的变化。新的模型既要满足找超平面的老问题。给出最大边缘,而且还要做新工作,正确地生成新的训练数据,而使其不出现太多分类错误。我们先从寻找超平面这个优化问题的构想出发,由超平面给出最大边缘(边缘的概念在上篇中有解释):
有非常多种方法能够改动这个模型。所以它把误分类考虑在内了。
比方,你能够考虑最小化同一个量加上一个常数乘以训练数据误分类的错误数量,比如:
然而。这个并非最好的解决方式,在其它原因中(amongsome other reasons),我们对距离期望决策区域距离非常小的误分类样本和没有误分类的样本不作区分。因此,更好的解决方式将把误分类样本离他们的正确决策区域的距离考虑在内,比如:
对于每个训练数据样本,要定义一个新的參数ξi。每个此參数都包括了训练样本到他们的正确决策区域的距离。下图显示了两类非线性可分类的训练数据,一个分类超平面和误分类样本到正确决定区域的距离。
注意:图中值仅仅显示了误分类样本的距离。其它样本的距离为0。由于他们已经在正确的区域了。
图中红色和蓝色的线是每个决策区域的边界。每个ξi是从误分类点到期望区域的边界,明确这一点非常重要。最后。优化问题的新公式为:
怎么选择常数C呢?非常明显这个问题依赖于训练数据是怎么分布的。虽然没有统一的答案,考虑一下规则会非常有帮助:
l C的值越大,误分类的数量会越少,但边缘也会越小。这样的情况。是把误分类错误看的非常重,对误分类要求严格。
然而,优化的目标是最小化自变量,少量误分类错误是同意的。
l C值越小,边缘越大。误分类错误越大。这样的情况下最小化没有过多考虑求和项。因此其焦点在于寻找具有更大边缘的超平面。
4.代码
你可能也在OpenCV源码库中的此路径sample/cpp/tutorial_code/gpu/non_linear_svm/non_linear_svms目录中找到了源码,以及那些视频文件,也能够在此下载。
#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("SVM for Non-Linear Training Data", I); // show it to the user waitKey(0); }
译者注:因为凝视直接加入在代码中会导致排版问题。偷懒把凝视堆在此下了:
//训练数据矩阵。行数200。列2 //标记矩阵。行200,列1 //生成线性可切割的训练数据 //生成第1类的随机点 //取子矩阵。0~nLinearSamples行 //取上面子矩阵的子矩阵,第0列 //随机填充该列,其值范围从1到0.4 * WIDTH //取子矩阵的第1列 //随机填充该列。其值范围从1到HIGHT //此例中矩阵的第0列和第1列分别当做点的x,y坐标 //生成第2类的随机点 //取子矩阵。留出一个切割带直到最后 //取第0列 //随机填充该列,其值范围从0.6 * WIDTH到WIDTH //取第1列 //随机填充该列。其值范围从1到HIGHT //随机生成非线性可切割的训练数据 //取子矩阵。中间切割带部分 //取第0列 //随机填充,其值范围就是中间带的x坐标 //取第1列 //随机填充,值就为整个HIGHT范围 //Class 1//设置标识,1为第一类 // Class 2//标识2为第二类 //通过上一篇能够了解到,此參数是跟维度有关的。高维又是须要映射,这里LINEAR指不映射 //遍历整副图片,预測每一个像素点所属的类别。并作对应着色
5.代码解析
(1)创建训练数据
此处训练数据由一组标记了的二维点组成,共两类。
为了让这个练习更加有吸引力,训练数据通过均匀分布概率密度方程(a uniform probability density functions (PDFs))随机生成。我们已经把训练数据的生成分成了两个主要部分。第一部分,我们生成线性可分的两类数据。
// 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));
第二部分,我们为两类创建非线性可分的数据和重叠的数据。
// Generate random points for the classes 1and 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));
译者注:
void RNG::fill(InputOutputArray mat, int distType, InputArray a, InputArray b, bool saturateRange=false )
这个函数是对矩阵mat填充随机数,随机数的产生方式有參数2来决定,假设为參数2的类型为RNG::UNIFORM。则表示产生均一分布的随机数,假设为RNG::NORMAL则表示产生高斯分布的随机数。相应的參数3和參数4为上面两种随机数产生模型的參数。比方说假设随机数产生模型为均匀分布。则參数a表示均匀分布的下限,參数b表示上限。
假设随机数产生模型为高斯模型,则參数a表示均值。參数b表示方程。
參数5仅仅有当随机数产生方式为均匀分布时才有效,表示的是是否产生的数据要布满整个范围(没用过,所以也没细致去研究)。
另外。须要注意的是用来保存随机数的矩阵mat能够是多维的,也能够是多通道的,眼下最多仅仅能支持4个通道。
(2)创建SVM參数
可參见上一篇中关于CvSVMParams的解说。
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);
这里跟我们前一篇的配置仅有两处不同:
l CvSVM::C_SVC:我们这里选择了一个非常小的数值。目的是,在优化中不正确误分类错位做过多惩处。这么做到原因是我们希望获得最接近直观预期的解决方式。然而,我们推荐通过调整获得一个更能反映问题的參数。
注意:这里在两类之间重叠的点非常少,给FRAC_LINEAR_SEP一个更小的值。点的密度将添加。并且參数CvSVM::C_SVC的影响更深。
l Termination Criteria of thealgorithm,算法终止标准:为了通过非线性可分离的训练数据正确地解决这个问题,最大迭代次数必须添加非常多。特别的。我们对这一參数值添加了5个数量级。
(3)训练SVM
我们调用方法CvSVM::train来建立SVM模型。注意训练过程可能会花非常长时间。执行改程序是要有耐心。
CvSVM svm; svm.train(trainData, labels, Mat(), Mat(),params);
(4)显示决策区域
方法CvSVM::predict通过已训练的SVM来分类输入样本。在此例中,我们用这种方法根据SVM的预測来着色相关区域。也就是说,遍历一个图像。把它的像素当成笛卡尔平面的点。每个点根据SVM预測的类来着色;深绿色的是标记为1的类,深蓝色是标记为2的类。
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)显示训练数据
方法circle用来显示训练数据的样本点。标记为1的类的样本显示为亮绿色,标记为2的类的样本点显示为亮蓝色。
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)支持向量机
这里我么用来非常多方法来获取支持向量的信息。
方法CvSVM::get_support_vector_count输出该问题中支持向量的总数,方法CvSVM::get_support_vector能够依据索引获取到每个支持向量。我们用这些方法找到是支持向量的训练样本,并显示为高亮。
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); }
6结果
l 代码打开了一个图像,显示两类的训练数据。当中一类显示为亮绿色,还有一类显示为亮蓝色。
l 训练SVM。并用它来分类图像上全部的像素点。图像被分成蓝绿两块区域。
两个区域的边界就是切割超平面。由于训练数据是非线性可切割的,可以看到,一些样本被误分类了。一些绿色的点在蓝色区域中,另一些蓝色的点在绿色区域中。
l 最后,被灰色的圈包围的训练样本的点是支持向量。
原文:http://docs.opencv.org/doc/tutorials/ml/non_linear_svms/non_linear_svms.html#nonlinearsvms