对于一些数据的分类算法中,在人工神经网络以前所用的最多的还是支持向量机,支持向量机的好处是既可以支持离散的数据分类,也可以支持连续的数据分类。支持向量机本身是通过一个超平面来计算特定的点是否为特定的分类。超平面可以是一个一维数轴上的点,也可以是一个二维平面上的直线,也可以三维空间中的一个平面,高于三维空间的N维空间,必定有一个N-1维的超平面可以对N维空间进行分隔,即便是在一个M维空间中不能线性可分的样本集,那么总可以映射到一个维空间使得样本线性可分,这样的空间有不可数个,我们可以通过核技巧来实现,由此我们会引入核函数。
1、线性可分支持向量机
我们看得出来,由于在得知哪一个是合适的支持向量模型以前,我们需要通过一组样本空间,其中的某一个子集
,并且为这组数据设置一组一一对应的分类标签,标签的取值空间为
,为提供给支持向量机的训练依据。因此支持向量机是一种监督式学习,
有一组样本数据是
。当然,这种表示法无法直观的表示单个样本数据。因此用行向量表示法更好。
我们转置一下,仍然用
本身表示。行向量表示法为将向量转置以后用小括号表示。即行向量
, 一个行向量表示一个样本并对应一个样本标签(分类),即可以理解为
为第
行数据,或为第
个样本点。
为样本点中第
个属性数据。
图片来自OpenCV 官方文档。
支持向量机的目的就是为了找到一组向量对应的点,在这组向量中间找到一个超平面(2D平面上为直线)
这个超平面就是可以看作一个简单的线性函数:
但是由于在两部分区域间线性可分的两种不同分类样本中,可以为两种分类样本分离的超平面线性函数有无数个,我们选择最优的那个条件有两点:两种分类之间分别有几个样本点之间欧式距离最近,那么最优的直线肯定是通过这几个互相距离对方分类最近的两个样本点之间距离的一半的点,我们能得出任意样本点距离超平面的距离
,并且为了计算简单,可以使
. 因为我们最终要得到的是一个最小,一个最大,那么这就牵扯到两个条件优化问题. 我们要得到的是最小的两个不同分类样本点的距离,并且在这些样本找到其中能够与超平面距离最大的分隔,即带符号的距离。
问题就成了求最优解问题:
,因此
或取其他值时不影响w和b。
最后线性可分支持向量机的学习算法就成了一个最优化求解问题:
这两个最优化条件等价
得到的和
可以得到超平面
,分类决策函数只需要使用sign函数获取符号
就可以得知为二分类中的正类还是负类
2、非线性可分支持向量机
非线性可分支持向量机,情况就比较复杂一些。
损失优化方法
损失优化方法可以对少量线性不可分样本进行容错分类,线性不可分样本集合如下图
对于不可分样本点,允许一定的误分类误差存在,误分类点的到支持向量的比例因子为,i为第i个误分类样本,引入惩罚因子C,将线性不可分问题转化为
惩罚因子C越大时,对误分类惩罚较大,会导致过拟合,C越小时对误分类惩罚较小,会导致欠拟合。
核技巧
这种方法的思想是,将支持向量机转化成对偶优化问题以后(拉格朗日乘子法优化方法),将 通过映射函数
到新的线性可分空间中,并且定义核函数
, 在对偶方法中将
替换为
以后,使得在更高维度空间中线性可分,最后再求解对偶问题即可。
关于SVM对偶问题最优化解法以及SMO算法暂不在此详述。
3、OpenCV的SVM非线性可分支持向量机C++范例
以下附上OpenCV文档中的训练和预测非线性可分支持向量机的范例以及注释详解。
OpenCV中的非线性支持向量机封装了大量SVM的内部实现,我们只需要了解其使用方法即可,OpenCV的范例是个极佳的示范:
#include <iostream>
#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;
using namespace std;
static void help() {
cout
<< "\n--------------------------------------------------------------------------"
<< endl
<< "This program shows Support Vector Machines for Non-Linearly Separable Data. "
<< endl
<< "--------------------------------------------------------------------------"
<< endl << endl;
}
/**
* 训练数据集格式为(x,y,label),第一列和第二列是点的坐标列,label为标签列
*/
int main() {
help();
const int NTRAINING_SAMPLES = 100; // 每个类别训练样本数
const float FRAC_LINEAR_SEP = 0.9f; // 组合线性可分样本的比例
// 数据可视化
const int WIDTH = 512 /**列大小*/, HEIGHT = 512 /**行大小*/;
Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3); // 构建一个全零矩阵,形状为512x512
//--------------------- 1. 随机设置训练数据 ---------------------------------------
// 训练数据矩阵
Mat trainData(2 * NTRAINING_SAMPLES, 2, CV_32F);// 行数在本例子中是 2 x NTRAINING_SAMPLES = 200,列数:2
Mat labels(2 * NTRAINING_SAMPLES, 1, CV_32S);// 标签结果行数是 2 x NTRAINING_SAMPLES = 200,列数: 2
RNG rng(100);// 生成类随机值
// 设置线性可分部分训练数据
int nLinearSamples = (int) (FRAC_LINEAR_SEP * NTRAINING_SAMPLES);//线性可分数据数量为0.9 x 100 = 90
// 生成类别 1随机点
Mat trainClass = trainData.rowRange(0, nLinearSamples);//训练数据行[0, 90),一共90个点
// 生成[0, 0.4)为范围的点 x 坐标
Mat c = trainClass.colRange(0, 1);//第1列,范围[0,1),且为整数,因此选取的列为仅第1列
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(0.4 * WIDTH));//填充以[0, 0.4x512)之间的均匀分布随机数值
// 生成[0, 1) 为范围的点 y 坐标
c = trainClass.colRange(1, 2);//第2列,范围[1,2),且为整数,因此选取的列为仅第2列
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充以[0, 512)之间的均匀分布随机数值
// 生成类别 2随机点
trainClass = trainData.rowRange(2 * NTRAINING_SAMPLES - nLinearSamples,
2 * NTRAINING_SAMPLES);// 类别2点存放的范围[2x 100 - 90, 2 x 100) = [110 ,200),一共90个点
// 位于[0.6, 1]的点 x 坐标
c = trainClass.colRange(0, 1);//选定范围[0,1),也就是第1列x坐标列
rng.fill(c, RNG::UNIFORM, Scalar(0.6 * WIDTH), Scalar(WIDTH));//填充点的 x 坐标的范围为[0.6x512,512),均匀随机分布
// 位于[0,1) 的点的y坐标
c = trainClass.colRange(1, 2);//选定列范围[1,2),也就是第2列y坐标列
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充点的 y 坐标的范围为[0,512),均匀随机分布
//------------------ 设置非线性可分部分的训练数据 ---------------
// 生成类别1和2的随机点
trainClass = trainData.rowRange(nLinearSamples,
2 * NTRAINING_SAMPLES - nLinearSamples);//生成分属1和2的混合随机样本点,在训练集的范围中是[90, 2x100-90) = [90, 110)
// 生成[0.4, 0.6) 范围的x坐标
c = trainClass.colRange(0, 1);
rng.fill(c, RNG::UNIFORM, Scalar(0.4 * WIDTH), Scalar(0.6 * WIDTH));//填充随机数范围[0.4x512, 0.6x512),均匀随机分布
// 生成[0, 1) 范围的y坐标
c = trainClass.colRange(1, 2);
rng.fill(c, RNG::UNIFORM, Scalar(0), Scalar(HEIGHT));//填充随机数范围[0, 512),均匀随机分布
//------------------------- 设置类别标签 ---------------------------------
labels.rowRange(0, NTRAINING_SAMPLES).setTo(1); // 在标签列上为范围[0, 100)设置类别为 1
labels.rowRange(NTRAINING_SAMPLES, 2 * NTRAINING_SAMPLES).setTo(2); // 在标签列上为范围[100, 200)设置类别为 2
//以上准备数据集准备完毕。
//------------------------ 2. 设置支持向量机向量--------------------
cout << "Starting training process" << endl;
Ptr<SVM> svm = SVM::create();
svm->setType(SVM::C_SVC);
svm->setC(0.1);//设置允许错误分类阈值C
svm->setKernel(SVM::LINEAR);//设置为线性核函数
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int) 1e7, 1e-6));
//------------------------ 3. 训练支持向量机 ----------------------------------------------------
svm->train(trainData, ROW_SAMPLE, labels);//开始训练。告知训练器为行样本和标签,训练过程是个耗时过程。
cout << "Finished training process" << endl;
//------------------------ 4. 展示分割区域 ----------------------------------------
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++) {
//为了更好的展示分隔线,会将整张512x512的图上所有点进行分类预测,如此以来整张背景图都是分类区域图
//因此要对每个点进行分类预测
Mat sampleMat = (Mat_<float>(1, 2) << j, i);
//预测点的样本分类
float response = svm->predict(sampleMat);
//针对分类进行颜色赋值,1为绿色,2为蓝色
if (response == 1)
I.at<Vec3b>(i, j) = green;
else if (response == 2)
I.at<Vec3b>(i, j) = blue;
}
}
//----------------------- 5. 展示训练数据 --------------------------------------------
//圆圈外边缘
int thick = -1;
float px, py;
// 类别 1
// 针对类别1进行分类的样本点展示
for (int i = 0; i < NTRAINING_SAMPLES; i++) {//训练数据的前100个点
px = trainData.at<float>(i, 0);//第i个样本的x坐标
py = trainData.at<float>(i, 1);//第i个样本的y坐标
circle(I, Point((int) px, (int) py), 3, Scalar(0, 255, 0), thick);//在整张展示图I中绘制点x,y
}
// 类别 2
// 针对类别1进行分类的样本点展示
for (int i = NTRAINING_SAMPLES; i < 2 * NTRAINING_SAMPLES; i++) {//训练数据的后100个点
px = trainData.at<float>(i, 0);;//第i个样本的x坐标
py = trainData.at<float>(i, 1);//第i个样本的y坐标
circle(I, Point((int) px, (int) py), 3, Scalar(255, 0, 0), thick);//在整张展示图I中绘制点x,y
}
//------------------------- 6. 展示支持向量 --------------------------------------------
//圆点的外边缘为2,着重显示支持向量的点
thick = 2;
//获取支持向量
Mat sv = svm->getUncompressedSupportVectors();
for (int i = 0; i < sv.rows; i++) {
//读取第i个支持向量的点坐标
const float *v = sv.ptr<float>(i);
//绘制支持向量的圆点,并且着重显示这些点
circle(I, Point((int) v[0], (int) v[1]), 6, Scalar(128, 128, 128),
thick);
}
cout<<endl<<"Support Vectors: "<<sv<<endl;
imwrite("result.png", I); // 保存图片
imshow("SVM for Non-Linear Training Data", I); // 展示给用户
waitKey();
return 0;
}
本例中使用的OpenCV版本号为3.4.3,使用其他版本无法保证API仍然有效。
环境:
Ubuntu 18.04.3,附带GTK+模块编译。
CMake 3.10
GCC 7.4.0
编译通过。