文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

目标

在这篇教程中您将会学习如何:

  • 利用OpenCV中的CvSVM::train函数建立一个基于SVMs(基于向量机)的分类器和利用 CvSVM::predict函数测试分类器的性能。

什么是一个基于向量机的分类器?

支持向量机的官方定义是:一个分离超平面的具有识别力的分类器。换句话来说,对于给定的被标记的(译者按:即数据和其分类)训练数据(有监督学习),该算法将输出一个在上平面上最优的分类结果。

什么叫做在超平面上获得最佳分类?请考虑下边这个简单的问题:

对于一个线性可分的二维点集,里边所有的点属于两种类型。最好的分类方法即是只用一条直线将他们分开。

第 1 段(可获 2 积分)

笔记

在这个例子中我们将讨论在笛卡尔坐标系中的线和点,而非在一个高维度中的超平面和向量。这是对该问题的简化,因为我们的直觉能够更好的理解我们好理解的例子。但是,它的概念也能够运用于分类所在的平面维度是大于二的样例中。

在下边的图片中你可以看出,为了解决这个例子使用了较多的线条。在这些线中是否有一些解好于其他解?我们可以直观地定义一个标准用以评估这些线的价值:

第 2 段(可获 2 积分)

距离样本太近的直线不是最优的,因为这样的直线对噪声敏感度高,泛化性较差。 因此我们的目标是找到一条直线,离所有点的距离最远。

由此, SVM算法的实质是找出一个能够将某个值最大化的超平面,这个值就是超平面离所有训练样本的最小距离。这个最小距离用SVM术语来说叫做 间隔(margin) 。 因此,最优分割超平面 最大化 训练数据的间隔。

如何计算最优超平面?

第 3 段(可获 2 积分)

下面的公式定义了超平面的表达式:

\beta 为权重向量,偏置

See also

关于超平面的更加详细的说明可以参考T. Hastie, R. Tibshirani 和 J. H. Friedman的书籍 Elements of Statistical Learning , section 4.5 (Seperating Hyperplanes)。

最优超平面可以有无数种表达方式,即通过任意的缩放 \beta 和 \beta_{0} 。 习惯上我们使用以下方式来表达最优超平面

|\beta_{0} + \beta^{T} x| = 1

式中 x 表示离超平面最近的那些点。 这些点被称为支持向量。该超平面也称为 canonical 超平面.

第 4 段(可获 2 积分)

通过几何学的知识,我们知道点 x 到超平面 (\beta, \beta_{0}) 的距离为:

\mathrm{distance} = \frac{|\beta_{0} + \beta^{T} x|}{||\beta||}.

特别的,对于 canonical 超平面, 表达式中的分子为1,因此支持向量到canonical 超平面的距离是

\mathrm{distance}_{\text{ support vectors}} = \frac{|\beta_{0} + \beta^{T} x|}{||\beta||} = \frac{1}{||\beta||}.

刚才我们介绍了间隔(margin),这里表示为 M, 它的取值是最近距离的2倍:

M = \frac{2}{||\beta||}

最后最大化 M 转化为在附加限制条件下最小化函数 L(\beta) 。 限制条件隐含超平面将所有训练样本 x_{i} 正确分类的条件,

第 5 段(可获 2 积分)

式中 y_{i} 表示样本的类别标记。

这是一个拉格朗日优化问题,可以通过拉格朗日乘数法得到最优超平面的权重向量 \beta 和偏置 \beta_{0} 。

源代码

#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/ml/ml.hpp>

using namespace cv;

int main()
{
    // 可视化数据
    int width = 512, height = 512;
    Mat image = Mat::zeros(height, width, CV_8UC3);

    // 设置训练数据
    float labels[4] = {1.0, -1.0, -1.0, -1.0};
    Mat labelsMat(4, 1, CV_32FC1, labels);

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

    // 设置SVM's参数
    CvSVMParams params;
    params.svm_type    = CvSVM::C_SVC;
    params.kernel_type = CvSVM::LINEAR;
    params.term_crit   = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);

    // 训练SVM
    CvSVM SVM;
    SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);

    Vec3b green(0,255,0), blue (255,0,0);
    // 展示SVM得出的决策区域
    for (int i = 0; i < image.rows; ++i)
        for (int j = 0; j < image.cols; ++j)
        {
            Mat sampleMat = (Mat_<float>(1,2) << j,i);
            float response = SVM.predict(sampleMat);

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

    // 显示训练数据
    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("SVM Simple Example", image); // 向用户展示结果
    waitKey(0);

}
第 6 段(可获 2 积分)

解释

  1. 建立训练样本

本例中的训练样本由分属于两个类别的2维点组成, 其中一类包含一个样本点,另一类包含三个点。

float labels[4] = {1.0, -1.0, -1.0, -1.0};
float trainingData[4][2] = {{501, 10}, {255, 10}, {501, 255}, {10, 501}};

 函数 CvSVM::train 要求训练数据储存于float类型的 Mat 结构中, 因此我们定义了以下矩阵:

Mat trainingDataMat(4, 2, CV_32FC1, trainingData);
Mat labelsMat      (4, 1, CV_32FC1, labels);
第 7 段(可获 2 积分)

    2.设置SVM参数

此教程中,我们以可线性分割的两类的训练样本简单讲解了SVM的基本原理。 但是,SVM的实际应用情形可能复杂得多 (比如非线性分割数据问题,SVM核函数的选择问题等等)。 总而言之,我们需要在训练之前对SVM做一些参数设定。 这些参数保存在类 CvSVMParams 中。

CvSVMParams params;
params.svm_type    = CvSVM::C_SVC;
params.kernel_type = CvSVM::LINEAR;
params.term_crit   = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);
  • SVM类型. 这里我们选择了 CvSVM::C_SVC 类型,该类型可以用于n-类分类问题 (n \geq 2)。 这个参数定义在 CvSVMParams.svm_type 属性中。
  • 注意:CvSVM::C_SVC 类型的重要特征是它可以处理非完美分类的问题 (及训练数据不可以完全的线性分割)。在本例中这一特征的意义并不大,因为我们的数据是可以线性分割的,我们这里选择它是因为它是最常被使用的SVM类型。
  • SVM 核类型. 我们没有讨论核函数,因为对于本例的样本,核函数的讨论没有必要。然而,有必要简单说一下核函数背后的主要思想, 核函数的目的是为了将训练样本映射到更有利于可线性分割的样本集。 映射的结果是增加了样本向量的维度,这一过程通过核函数完成。 此处我们选择的核函数类型是 CvSVM::LINEAR 表示不需要进行映射。 该参数由 CvSVMParams.kernel_type 属性定义。

  • 算法终止条件. SVM训练的过程就是一个通过 迭代 方式解决约束条件下的二次优化问题,这里我们指定一个最大迭代次数和容许误差,以允许算法在适当的条件下停止计算。 该参数定义在 cvTermCriteria 结构中。

    3.训练SVM

调用函数 CvSVM::train 来建立SVM模型。

CvSVM SVM;
SVM.train(trainingDataMat, labelsMat, Mat(), Mat(), params);

   4.SVM区域分割

第 8 段(可获 2 积分)

函数 CvSVM::predict 通过训练好的SVM来将输入的样本分类。 本例中我们通过该函数利用SVM的预测结果给向量空间着色, 即遍历图像中的每个像素当作卡迪尔平面上的一点,每一点的着色取决于SVM对该点的分类类别:绿色表示标记为1的点,蓝色表示标记为-1的点。

Vec3b green(0,255,0), blue (255,0,0);

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;
    }

 

第 9 段(可获 2 积分)

     5.支持向量

这里用了几个函数来获取支持向量的信息。 函数 CvSVM::get_support_vector_count 输出支持向量的数量,函数 CvSVM::get_support_vector 根据输入支持向量的索引来获取指定位置的支持向量。 通过这一方法我们找到训练样本的支持向量并突出显示它们。

int c     = SVM.get_support_vector_count();

for (int i = 0; i < c; ++i)
{
const float* v = SVM.get_support_vector(i); // get and then highlight with grayscale
circle(   image,  Point( (int) v[0], (int) v[1]),   6,  Scalar(128, 128, 128), thickness, lineType);
}

结果

  • 程序打开了一张图像,在其中显示了训练样本的所有分类。其中一个类显示为白色圆圈,另一个类显示为黑色圆圈。
  • 训练得到SVM并将图像的每一个像素分类。 分类的结果将图像分为蓝绿两部分,中间的边界线就是最优分割超平面。
  • 最后支持向量在训练样例中用灰色环标表示(译者按:就是白圈和黑圈外边的一圈灰色的环)。

The seperated planes

第 10 段(可获 2 积分)

文章评论