Opencv提供了SVM的算法,在他的官方文档中有一个不错的介绍:https://docs.opencv.org/3.3.1/d1/d73/tutorial_introduction_to_svm.html,这里SVM原理进行一下整理,里面有我自己的理解。
SVM原理
SVM主要就是找出一个能够将某个值最大化的超平面,这个值就是超平面离所有训练样本的最小距离。
这个超平面可以用f(x)来定义:
f(x)=β0+βTx,
β 叫做 权重向量,β0叫做 偏置(bias) 。(所以就是他的权重向量的转置乘以x,x是离超平面最近的那些点)
最优超平面可以有无数种表达方式,即通过任意的缩放 β 和β0。 习惯上我们使用以下方式来表达最优超平面
|β0+ βTx| = 1
通过几何学的知识,我们知道点 x 到超平面(β,β0) 的距离为:
distance=|β0+βTx|/|β||. (我们的目的就是让这个distance最大化)
在distance最大化之前我们还要做一些变形,对于超平面, 表达式中的分子为1,因此支持向量到canonical 超平面的距离是:
distance support vectors=|β0+βTx|/|β||=1/|β||. (变成了一个只有β的表达式)
间隔(margin),这里表示为 M, 它的取值是最近距离的2倍:
M=2/|β||(最后最大化 M 转化为在附加限制条件下最小化函数 L(β) )
限制条件隐含超平面将所有训练样本 xi 正确分类的条件如下:
minβ,β0L(β)=12||β||2 subject to yi(βTxi+β0)≥1 ∀i,
式中 yi表示样本的类别标记。
这是一个拉格朗日优化问题,可以通过拉格朗日乘数法得到最优超平面的权重向量 β 和偏置 β0。
代码解析
以下是opencv官网提供的源码,我们对他进行一下解析:
#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;
int main(int, char**)
{
// Data for visual representation
int width = 512, height = 512;
Mat image = Mat::zeros(height, width, CV_8UC3);
// Set up training data
int labels[4] = {1, -1, -1, -1};//设置四个训练数据的标签
float trainingData[4][2] = { {501, 10}, {255, 10}, {501, 255}, {10, 501} };//设置四个训练数据的坐标
Mat trainingDataMat(4, 2, CV_32FC1, trainingData);//利用Mat矩阵存储训练坐标
Mat labelsMat(4, 1, CV_32SC1, labels);//利用Mat矩阵存储标签,4行1列
// Train the SVM
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);//SVM参数配置 后面会有介绍
svm->setKernel(SVM::LINEAR);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6));
svm->train(trainingDataMat, ROW_SAMPLE, labelsMat);对数据进行分类
// Show the decision regions given by the SVM
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) << j,i);
float response = svm->predict(sampleMat);//去识别Mat属于哪个标签
if (response == 1)//标签在1的范围内为绿色
image.at<Vec3b>(i,j) = green;
else if (response == -1)//标签是别为-1为蓝色
image.at<Vec3b>(i,j) = blue;
}
// Show the training data
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 );
// Show support vectors
thickness = 2;
lineType = 8;
Mat sv = svm->getUncompressedSupportVectors();
for (int i = 0; i < sv.rows; ++i)
{
const float* v = sv.ptr<float>(i);
circle( image, Point( (int) v[0], (int) v[1]), 6, Scalar(128, 128, 128), thickness, lineType);
}
imwrite("result.png", image); // save the image
imshow("SVM Simple Example", image); // show it to the user
waitKey(0);
}
SVM参数的设置:
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setKernel(SVM::LINEAR);
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 100, 1e-6));
SVM类型: 这里我们选择了 SVM::C_SVC 类型,该类型可以用于n-类分类问题 (n \geq 2)。该类型的重要特征是它可以处理非完美分类的问题 (及训练数据不可以完全的线性分割)。在本例中这一特征的意义并不大,因为我们的数据是可以线性分割的,我们这里选择它是因为它是最常被使用的SVM类型。
SVM 核类型: 核函数的目的是为了将训练样本映射到更有利于可线性分割的样本集。 映射的结果是增加了样本向量的维度,这一过程通过核函数完成。 此处我们选择的核函数类型是SVM::LINEAR 表示不需要进行映射。
算法终止条件: SVM训练的过程就是一个通过 迭代 方式解决约束条件下的二次优化问题,这里我们指定一个最大迭代次数和容许误差,以允许算法在适当的条件下停止计算。
以下是参数配置的参考链接:
https://docs.opencv.org/trunk/d1/d2d/classcv_1_1ml_1_1SVM.html#ab4b93a4c42bbe213ffd9fb3832c6c44f
大致对SVM的原理就梳理到这里,后续还会跟着自己的学习不断的去完善。