写在前面:
最近在完成一个项目时,提取出的样本数据较大,自己也没有良好的处理数据的能力,就想着利用现成的 SVM对数据进行分类。
查了很多资料,对SVM的大致原理有点了解,网络上也有开源的LibSvm,可是在没有完全理解SVM原理前,阅读和修改源码有些难度。而opencv恰好有一个Machine Learning模块,囊括了大多数机器学习的算法,而SVM也在其中。
这篇笔记不包含SVM算法的具体原理,只是对opencv中的SVM使用方法做一点拙劣的笔记。下面开始:
SVM是什么? SVM:Support Vector Machine,即支持向量机。通过Wiki,有个大体的认识支持向量机
源码阅读 源代码来自Introduction to Support Vector Machines,opencv3.1.0的官方网站。自己做了一些注释。
#include <iostream> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include "opencv2/imgcodecs.hpp" #include <opencv2/highgui.hpp> #include <opencv2/ml.hpp> //ml模块 #define NTRAINING_SAMPLES 100 // 每类的训练样本 #define FRAC_LINEAR_SEP 0.9f // Fraction of samples which compose the linear separable part using namespace cv; using namespace cv::ml; //ml命名空间 using namespace std; int main() { const int WIDTH = 512, HEIGHT = 512; Mat I = Mat::zeros(HEIGHT, WIDTH, CV_8UC3); //---------- 1. 随机生成训练数据(二维数据,超平面为二维) --- //训练200个样本,每个样本为2维向量 Mat trainData(2 * NTRAINING_SAMPLES, 2, CV_32FC1); //200*1,用以标注样本类别 Mat labels(2 * NTRAINING_SAMPLES, 1, CV_32SC1); RNG rng(100); // 随机数生成类 // 设置数据中线性可分部分 int nLinearSamples = (int)(FRAC_LINEAR_SEP * NTRAINING_SAMPLES); // 为类别1生成随机数 Mat trainClass = trainData.rowRange(0, nLinearSamples); // x坐标范围为[0,0.4*WIDTH] Mat c = trainClass.colRange(0, 1); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(0.4 * WIDTH)); // y坐标范围为[0,WIDTH) c = trainClass.colRange(1, 2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); // 为类别2生成随机数 trainClass = trainData.rowRange(2 * NTRAINING_SAMPLES - nLinearSamples, 2 * NTRAINING_SAMPLES); // x坐标范围为(0.4*WIDTH,1*WIDTH) c = trainClass.colRange(0, 1); rng.fill(c, RNG::UNIFORM, Scalar(0.6*WIDTH), Scalar(WIDTH)); // y坐标范围为[0,1*HEIGHT] c = trainClass.colRange(1, 2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); //------------------ 设置训练数据中线性不可分部分 --------------- // 为类别1,2生成数据 trainClass = trainData.rowRange(nLinearSamples, 2 * NTRAINING_SAMPLES - nLinearSamples); // x坐标范围是 [0.4*WIDTH, 0.6*WIDTH) c = trainClass.colRange(0, 1); rng.fill(c, RNG::UNIFORM, Scalar(0.4*WIDTH), Scalar(0.6*WIDTH)); // y坐标范围是 [0, 1*HEIGHT) c = trainClass.colRange(1, 2); rng.fill(c, RNG::UNIFORM, Scalar(1), Scalar(HEIGHT)); //-------- 分类(为类别1,2设置标志)---------- labels.rowRange(0, NTRAINING_SAMPLES).setTo(1); // 类1 labels.rowRange(NTRAINING_SAMPLES, 2 * NTRAINING_SAMPLES).setTo(2); // 类2 //------------------ 2. 设置支持向量机范围 ------------- //------------------- 3. 训练SVM ------------------------- cout << "Starting training process" << endl; Ptr<SVM> svm = SVM::create(); //声明SVM对象 svm->setType(SVM::C_SVC); //SVM模型选择 svm->setC(0.2); //惩罚因子设置(原始0.1) svm->setKernel(SVM::LINEAR); //核函数类型:线性 svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int)1e7, 1e-6)); //迭代要求 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) { //Mat_<float>为Mat1f,一维向量v=(i,j) Mat = (Mat1f(1, 2) << i, j); //Mat sampleMat = (Mat_<float>(1, 2) << i, j); //判别是哪一类 if (response == 1) I.at<Vec3b>(j, i) = green; else if (response == 2) I.at<Vec3b>(j, i) = blue; } //----------------------- 5. 显示训练数据------------------------------- int thick = -1; int lineType = 8; float px, py; // 类 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); } // 类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. 画出支持向量 ---------------------------- thick = 2; lineType = 8; Mat sv = svm->getUncompressedSupportVectors(); for (int i = 0; i < sv.rows; ++i) { const float* v = sv.ptr<float>(i); circle(I, Point((int)v[0], (int)v[1]), 6, Scalar(0, 0, 0), thick, lineType); } imwrite("result.png", I); // save the Image imshow("SVM for Non-Linear Training Data", I); // show it to the user waitKey(0); }运行环境是VS2015+opencv3.1.0。
思路阐述
以上代码通过core.hpp中的RNG随机数生成类,生成了200个二维向量作为训练数据,数值大小在[0,512)中,以便可以用一张图片表示,存储在一个200*2的Mat结构里。同时生成一个Mat的label,将x坐标在一定范围内点标识为类1,其余为类2。生成的训练数据二维可分,故超平面为二维平面内的直线。 下面通过创建一个SVM类,调用几个函数,设定参数,并训练数据。
Ptr<SVM> svm = SVM::create(); //声明SVM对象 svm->setType(SVM::C_SVC); //SVM模型选择 svm->setC(0.1); //惩罚因子设置 svm->setKernel(SVM::LINEAR); //核函数类型:线性 svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, (int)1e7, 1e-6)); //迭代要求 svm->train(trainData, ROW_SAMPLE, labels); //训练 cout << "Finished training process" << endl;几个地方需要注意一下:
1.SVM类型选择:setType(SVM::C_SVC) SVM模型类型枚举: enum { C_SVC, NU_SVC, ONE_CLASS, EPSILON_SVR, NU_SVR }; 如果只是用ml::SVM,完成简单的数据分类,则可以这个参数设置为C_SVC. 标志作用C_SVCC表示惩罚因子,C越大表示对错误分类的惩罚越大NU_SVC和C_SVC类似(参数C的范围不同,C_SVC采用的是0到正无穷,NU_SVC是[0,1]。)ONE_CLASS需要类标号,用于支持向量的密度估计和聚类.EPSILON_SVR敏感损失函数,对样本点来说,存在着一个不为目标函数提供任何损失值的区域,即-带。NU_SVR由于EPSILON_SVR需要事先确定参数,然而在某些情况下选择合适的参数却不是一件容易的事情。而NU_SVR能够自动计算参数。2.惩罚因子设置: svm->setC(0.1) ①大的C(惩罚因子)使得解决方案更小的误分类率和更小的间隔。考虑到在这种情况下,分类错误的代价是昂贵的。因为优化的目的是减小不同,更小的误分类率是被接受的 ②小的C(惩罚因子)使得解决方案有更大的分类间隔和更大的误分类率。这种情况下,最小化没有考虑太多总和,因此它更注重于发现有更大间隔的高维空间。
3.选择核函数类型 简要说一下,核函数在SVM的作用。 SVM是一个二分类器,目的是找到一个平面,将已知的数据分为两类。以上代码已经实现了对二维空间中数据的分类,二维空间中的平面即为分类的直线(又叫做支持向量)。设想数据复杂一点,虽然维度还是三维,可是已经不能找到简单的平面来对数据进行分类。见图
从人眼来看,很明显可以用一个圆来对两类数据进行分类,而SVM所定义的最佳分类平面在二维空间中是直线。所以核函数出现了,它能将二维空间变化到高维空间。现在将两个圆坐标变换到三维空间,经过简单的旋转变换,可以用平面对数据进行分类,可见下图(来自pluskid:下面的gif 动画,先用 Matlab 画出一张张图片,再用 Imagemagick 拼贴成)
好了,回到opencv中的SVM核函数: enum {LINEAR, POLY, RBF,SIGMOID, PRECOMPUTED }; LINEAR:线性核函数(linear kernel) POLY:多项式核函数(ploynomial kernel) RBF:径向机核函数(radical basis function) SIGMOID:神经元的非线性作用函数核函数(Sigmoid tanh)
根据数据的特性选择不同核函数,进行SVM分类。
4.训练
svm->train(trainData, ROW_SAMPLE, labels);trainData:训练数据 labels:数据类型几率
关于ROW_SAMPLE,见ml.hpp中的定义(添加注释)
enum SampleTypes { //每一行为一个样本数据 ROW_SAMPLE = 0, //!< each training sample is a row of samples //每一列为一个样本数据 COL_SAMPLE = 1 //!< each training sample occupies a column of samples };知道以上内容后,我们就可以利用opencv中的SVM进行简单的数据分类。 ————–2017.3.8