首页 > 代码库 > 支持向量机(SVM)非线性数据分割

支持向量机(SVM)非线性数据分割

支持向量机(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);

这里跟我们前一篇的配置仅有两处不同:

CvSVM::C_SVC:我们这里选择了一个很小的数值,目的是,在优化中不对误分类错位做过多惩罚。这么做到原因是我们希望获得最接近直观预期的解决方案。然而,我们推荐通过调整获得一个更能反映问题的参数。

注意:这里在两类之间重叠的点很少,给FRAC_LINEAR_SEP一个更小的值,点的密度将增加,而且参数CvSVM::C_SVC的影响更深。

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

支持向量机(SVM)非线性数据分割