SVM的原理

SVM也叫支持向量机,最大间隔分类器。在分类、回归方面普遍出现。
我们在这里考虑使用二维空间来表示一系列数据,每个数据都有它自身的(x,y),然后用一条直线将其进行分类。这应该就是最简单的线性可分。
一般来说,当两个类别距离这条直线越远,那么分类的置信度就越高。

函数间隔与几何间隔

上述我们提到二维空间中的样本数据,但是一般来说,我们的数据是处于高维空间的。那么这里我们就是用公式 wTx+b=0 将其扩展到高维,那么进行分类的就不再是直线了,而是超平面。

在超平面wTx+b=0确定的情况下,|wTx+b|能够相对地表示点x距离超平面的远近,而且如果分类正确,则y(i)与wTx(i)+b的符号一致,即y(i)(wTx(i)+b)>0,同时表示分类的正确性以及确信度。
函数间隔:超平面(w,b)关于样本点(x(i),y(i))的函数间隔为γ^(i)=y(i)(wTx(i)+b)

但是,观察到函数间隔实际上并不能表示点到超平面的距离:因为当超平面(w,b)参数扩大相同的倍数后,如(2w,2b),而超平面的位置并没有改变,但是函数间隔也变大了相同的倍数2γ^(i)。

因此就引入了几何间隔的概念:

我们设样本点A坐标为x(i),点A到超平面的垂直距离记为γ(i),分离超平面wTx(i)+b=0的单位法向量为w||w||,因此点B的坐标为x(i)−γ(i)w||w||,且点B在直线上,带入直线公式有:

opencv 分类器训练识别沙发 opencv分类器原理_支持向量机

松弛因子与惩罚参数

以上我们都是讨论能够使用一条直线或者一个超平面就将训练数据完美切分的情况,但是一般情况下不会那么理想。这里我们就可以引入松弛因子。也可以理解为包容一些函数间隔大于1的样本。

C称为惩罚参数(C>0),C值越大对误分类的惩罚越大;因为当C为无穷大时,即成为了线性可分问题。

所以,最后函数就变为:

opencv 分类器训练识别沙发 opencv分类器原理_svm_02

核函数

因为可能存在数据量十分庞大的情况,那么在计算的时候计算量就十分可怕了,会造成维数灾害,所以我们使用一个函数,将原来的数据映射到另外一个分布,或者空间中,以减少数据的运算。这个函数就叫做核函数。
常见的核函数有:线性核函数,高斯核函数,多项式核函数,双曲正切核函数。

损失函数

对于SVM来说,我们的目的就最小化我们的目标函数,也就是

opencv 分类器训练识别沙发 opencv分类器原理_支持向量机_03


我们一般称SVM的损失函数为Hinge loss,也就是合页损失函数

处理过程

获取数据

首先当然是获取数据,遍历文件夹,来获取各个类别的图片,并计算个数。我们可以标记正样本为1,负样本为0,当然还可以有更多的类别。

void getFiles(const std::string path, std::vector<std::string>& files){
	intptr_t hFile = 0;
	struct _finddata_t fileinfo;
	std::string p;
	if ((hFile = _findfirst(p.assign(path).append("\\*").c_str(), &fileinfo)) != -1){
		do{
			if ((fileinfo.attrib &  _A_SUBDIR)){
				if (strcmp(fileinfo.name, ".") != 0 && strcmp(fileinfo.name, "..") != 0)
					getFiles(p.assign(path).append("\\").append(fileinfo.name), files);
			}
			else{
				if (strstr(fileinfo.name, ".jpg") || strstr(fileinfo.name, ".bmp") ||
					strstr(fileinfo.name, ".png") || strstr(fileinfo.name, ".JPG"))
					files.push_back(path + "\\" + fileinfo.name);
			}
		} while (_findnext(hFile, &fileinfo) == 0);
		_findclose(hFile);
	}
}

数据标注

因为SVM是监督学习,所以我们获得数据后就要进行标注了。这里的标注比较简单,就是放在一个文件夹里,每个文件夹都为一类,也就是0或者1,又或者2、3等等这些数字。

void getNeutral(Mat& trainingImages, vector<int>& trainingLabels, string FilePath){
	vector<string> files;
	getFiles(FilePath, files);
	int number = files.size();
	cout << "neutral samples has : " << number << endl;
	for (int i = 0; i < number; i++){
		Mat  image = imread(files[i], 0);
		equalizeHist(image, image);
		resize(image, image, Size(128, 128));
		Mat  hogFeactureMat = detector_HOG(image);
		if (!hogFeactureMat.empty()) {
			trainingImages.push_back(hogFeactureMat);
			trainingLabels.push_back(2);
		}
	}
}

void getPositive(Mat& trainingImages, vector<int>& trainingLabels, string FilePath){
	vector<string> files;
	getFiles(FilePath, files);
	int number = files.size();
	cout << "positive samples has : " << number << endl;
	for (int i = 0; i < number; i++){
		Mat  image = imread(files[i], 0);
		equalizeHist(image, image);
		resize(image, image, Size(128, 128));
		Mat  hogFeactureMat = detector_HOG(image);
		if (!hogFeactureMat.empty()) {
			trainingImages.push_back(hogFeactureMat);
			trainingLabels.push_back(1);
		}
	}
}

void getNegative(Mat& trainingImages, vector<int>& trainingLabels, string FilePath){
	vector<string> files;
	getFiles(FilePath, files);
	int number = files.size();
	cout << "negative samples has : " << number << endl;
	for (int i = 0; i < number; i++){
		Mat  image = imread(files[i], 0);
		equalizeHist(image, image);
		resize(image, image, Size(128, 128));
		Mat  hogFeactureMat = detector_HOG(image);
		if (!hogFeactureMat.empty()) {
			trainingImages.push_back(hogFeactureMat);
			trainingLabels.push_back(0);
		}
	}
}

特征提取

接下来就是对这些样本进行特征提取,这里使用HOG函数作特征提取。将图片变为一行又一行的行向量,最后再送进到SVM分类器中进行训练。

首先对给定的特定图像,设置一个覆盖图像中整个对象的检测窗口(感兴趣区域,大小一般为图片的尺寸),然后,计算检测窗口中每个像素的梯度大小和方向。

怎么计算呢?下面就会介绍一下。当计算完之后就将这些特征串联起来,组成一行又一行的特征向量,放进SVM中进行训练。

其实在特征提取中,经常使用到HOG,因为其的优点有:
1.HOG表示的是边缘(梯度)的结构特征,因此可以描述局部的形状信息;
2.位置和方向空间的量化一定程度上可以抑制平移和旋转带来的影响;
3.采取在局部区域归一化直方图,可以部分抵消光照变化带来的影响;
4.由于一定程度忽略了光照颜色对图像造成的影响,使得图像所需要的表征数据的维度降低了;
5.重要的是,由于它这种分块分单元的处理方法,也使得图像局部像素点之间的关系可以很好得到的表征;

但是它也有它自身的的缺点:
1.生成过程复杂,所以导致速度慢,实时性差;
2.很难处理遮挡问题,鲁棒性不强;
3.由于梯度的性质,所以HOG对噪点相当敏感;

HOG的参数调节

在HOGDescriptor()中有几个参数说明 : winSize(64,128), blockSize(16,16), blockStride(8,8),cellSize(8,8), nbins(9), derivAperture(1), winSigma(-1)

Mat detector_HOG(Mat image) {
	//查看参数介绍,第一个Size和图片尺寸一样
	HOGDescriptor hog(Size(128, 128), Size(16, 16), Size(8, 8), Size(8, 8), 3);  
	Mat featureMat;
	vector<float> descriptors;
	hog.compute(image, descriptors);
	featureMat = Mat::zeros(1, descriptors.size(), CV_32FC1);
	for (int j = 0; j < descriptors.size(); j++){
		featureMat.at<float>(0, j) = descriptors[j];
	}
	return featureMat;
}

首先,这里就需要用将检测窗口分成若干像素连接而成的单元格,所有单元格的大小相同,这里单元格的大小由你来确定。

我是将图片resize成64128大小,然后在其中划分若干个block,block大小为1616,每个block由8*8个cell组成,这些cell大小相同但是互不重叠。

然后在进行窗口滑动的时候,步长为8*8。之后会计算不同block之中,这些cell的梯度大小及方向。将其平均分为9个bins,每个cell内的像素用幅值来表示权值,为其所在的梯度直方图进行加权投票。最后,在提取完特征后就可以做一些Gamma归一化操作。

训练

训练主要需要注意的是,一是训练的数据,二是训练的参数。

首先我们需要保证数据的质量,比如清晰度,分布场景,类别的比重

其实就是训练的参数,主要修改的参数:

SVM可以用作分类与回归,我们主要用于分类,所以设置的时候设置为SVC即可

SVM默认的核函数是线性核函数,但是一般来说效果是不太理想的。

这里我使用的核函数是Rbf,主要需要调整的参数是gamma,惩罚因子C。

如果使用Poly核函数的话,主要需要调整惩罚因子C,阶数degree,gamma,偏移量coef,

另外,倘若样本不均衡的话,可能还需更改class_weight参数

在训练的最后有setTermCriteria中终止条件,其中参数有最大迭代次数,以及损失的阈值
void trainSVM(string path) {
	Mat classes;
	Mat trainingData;
	Mat trainingImages;
	vector<int> trainingLabels;
	getPositive(trainingImages, trainingLabels, PosFilePath);
	getNegative(trainingImages, trainingLabels, NegFilePath);
	getNeutral(trainingImages, trainingLabels, NeuFilePath);

	Mat(trainingImages).copyTo(trainingData);
	trainingData.convertTo(trainingData, CV_32FC1);  //int转为浮点型
	Mat(trainingLabels).copyTo(classes);

	Ptr<SVM> SVM_params = SVM::create();
	SVM_params->setType(SVM::C_SVC);
	SVM_params->setKernel(SVM::RBF);
	SVM_params->setDegree(0);
	SVM_params->setGamma(0.01);
	SVM_params->setCoef0(0);  //针对多项式Poly
	SVM_params->setC(1.0);
	SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
	
	cout << "start training!!!" << endl;
	Ptr<TrainData> tData = TrainData::create(trainingData, ROW_SAMPLE, classes);
	SVM_params->train(tData);
	SVM_params->save(path); 
	cout << "train done!!" << endl;
	valSVM(path, NeuFilePath, 2);
	valSVM(path, PosFilePath, 1);
	valSVM(path, NegFilePath, 0);
}

评估

训练后之后会保存下一个XML文件,这就是我们训练后的结果。我们接下来就要对其进行评估,看模型能否使用,因为训练的时候并没有打印多余的信息。

所以我在这里加入了对训练集的评估,实际上评估集需要另外提供。我这里计算了训练的时间,以及评估模型的准确率(评估训练集)

void valSVM(string Modelpath, string FilePath, int label) {
	Ptr<ml::SVM>svm = ml::SVM::load(Modelpath);
	vector<string> files;
	getFiles(FilePath, files);
	int number = files.size();
	int count = 0;
	clock_t start, end;
	start = clock();
	for (int i = 0; i < number; i++){
		Mat image = imread(files[i], 0);
		equalizeHist(image, image);
		resize(image, image, Size(128, 128));
		int result;
		image = detector_HOG(image);
		image.convertTo(image, CV_32F);
		result = svm->predict(image);
		
		if (result == label)
			count++;
	}
	end = clock();
	double totalCost = (double)(end - start);
	cout << "every sample cost time is : " << static_cast<float>(totalCost) / static_cast<float>(number)
		<< setprecision(3) << " ms" << endl;
	cout << FilePath << " 's  accuracy is : " << static_cast<float>(count) / static_cast<float>(number) * 100 
		<<setprecision(3) << " %"<< endl;
}

预测

当训练完毕后,我们在后期进行预测的时候,只需要载入训练后的XML模型,就可以查看识别的效果。

这里对进行预测的图片一样要经过预处理,比如灰度化,归一化,HOG特征提取,最后再进行预测,输出识别的结果。

我们可以像上面一样,在函数体内载入模型,也可以定义一个全局变量,来进行模型的载入。

Ptr<ml::SVM> svm;

bool svmInit(const char * SVMpath) {
	svm = ml::SVM::load(SVMpath);
	if (!svm.empty()) {
		cout << "SVM model load successfully!!!" << endl;
		return true;
	}
	cout << "SVM model load failed, please check model path!!!" << endl;
	return false;
}

int predict(Ptr<ml::SVM>svm, Mat img){
	cvtColor(img, img, CV_BGR2GRAY);
	equalizeHist(img, img);
	resize(img, img, Size(128, 128));
	int result;
	img = detector_HOG(img);
	img.convertTo(img, CV_32F);
	result = svm->predict(img);	
	cout << result << endl;
	return result;
}