SVM实现多分类常用的两种方法以及一对一法的代码(VS13+opencv3.4)

SVM实现多分类常用的两种方法以及一对一法的代码(VS13+opencv3.4)

SVM是一个二值分类器,处理多分类问题的时候需要构造合适的多类分类器。
(1)直接法,直接在目标函数上进行修改,将多个分类面的参数求解合并到一个最优化问题中,通过求解该最优化问题“一次性”实现多类分类。这种方法看似简单,但其计算复杂度比较高,实现起来比较困难,所以没有被广泛应用。
(2)间接法,将多类问题分解为一系列SVM可直接求解的二值分类问题,再根据一系列SVM求解结果得到最终判别结果。基于此种思想的多分类方法有一对余类法,一对一法,DAG法,决策树方法,纠错输出编码法,共五种,下面将说明比较常用的一对余类法,一对一法。

1.一类对余类法(One versus rest,OVR)是最早出现也是目前应用最为广泛的方法之一,其步骤是构造k个两类分类机(假设共有K个类别),训练时第i个分类机取训练集中第i类为正样本,其余类都划分为负样本,进行训练,实例可见下图。判别时,输入信号分别经过k个分类机共得到k个输出值fi(x)=sgn(gi(x)),若只有一个+1出现,则其对应类别为输入信号类别,此为理想情况 ;实际情况下构造的决策函数总是有误差的,若输出不只一个+1(不只一类声称它属于自己),或者没有一个输出为+1(即没有一个类声称它属于自己),则比较g(x)输出值,最大者对应类别为输入的类别。
《SVM实现多分类常用的两种方法以及一对一法的代码(VS13+opencv3.4)》
图片里面有三类,因此训练三个分类器,三类中的一个类作为正样本,则其它两类都划分为负样本,进行训练,得到三个分类器。

评价:这种方法的优点是,对k类(K较大)问题,只需要训练k个两类分类支持向量机,故其所得到的分类函数的个数(k个)较少,其分类速度相对较快。缺点:样本不平衡问题:一对多方法在训练每个分类器时,其训练样本是1类对k-1类,正负样本的规模相差较大,如1个正样本和99个负样本,则在训练此分类器时很有可能最终的分类器是D(x)=-1,即不论输入什么都输出-1,这样他的错误率也很小只有0.01,达不到训练的效果。解决这个问题可以用一对一方法。详细见博客链接:https://blog.csdn.net/henghane/article/details/54970544

2.一对一法
 分类器个数:假设有K类样本,其做法是在任意两类样本之间设计一个SVM,因此k个类别的样本就需要设计k(k-1)/2个SVM。
结果判定:训练好全部分类器后,当对一个测试样本进行分类时,此样本经过全部分类器,得到k(k-1)/2个分类结果,最后得票最多的类别即为该测试样本的类别。本次将实现基于这个方法的人,车,背景三分类,因为只有三类,所以分类器个数只需要三个。

评价:这种方法虽然好,但是当类别k很多的时候,分类器的个数是k*(k-1)/2,需要多次投票;此外还会存在误判(例如人在车与背景分类器中被划分为背景),拒分(票数相同不知道如何划分类),解决误判,拒分问题可采用DAG法。

3.DAG法
DAG-SvMS是由PIatt提出的决策导向的循环图DAG导出的,是针对“一对一”SvMS存在误分,拒分现象提出的。这种方法的训练过程类似于“一对一”方法,k类别问题需要求解k(k-1)/2个支持向量机分类器,这些分类器构成一个有向无环图。该有向无环图中含有k(k-1)/2个内部节点和k个叶结点,每个节点对应一个二类分类器。
《SVM实现多分类常用的两种方法以及一对一法的代码(VS13+opencv3.4)》

DAG-SVMS简单易行,只需要使用k一1个决策函数即可得出结果,较“一对一”方法提高了测试速度,而且不存在误分、拒分区域;另外,由于其特殊的结构,故有一定的容错性,分类精度较一般的二叉树方法高。然而,由于存在自上而下的“误差积累”现象是层次结构固有弊端,故DAG-SVMS也逃脱不掉。即如果在某个结点上发生了分类错误,则会把分类错误延续到该结点的后续结点上.详见http://blog.sina.com.cn/s/blog_5eef0840010147pa.html

下面展示基于一对一法的三分类代码实现的两个部分

第一部分:首先我们需要分别训练出三个分类器,并且要测试每一个分类器的分类准确率,确保每一个SVM都有较高的正确分类率。下面展示人车分类器的训练以及测试分类代码,其余两个分类器也可同理求得。

#include <stdio.h>
#include <iostream>  
#include <fstream>  
#include <opencv2/opencv.hpp>
#include <string>
#include <io.h>
#include<stdlib.h>

using namespace std;
using namespace cv::ml;

#define PosSamNO   1152   //正样本个数                                                    
#define NegSamNO   1152  //负样本个数    
#define HardExampleNO 0  //难例个数
#define TestSamNO  300   //测试个数                                                    

/*-------------------------------------将正样本的图片名循环写入到txt文件内--------------------------------------------*/

void Pos_List()
{
	cout << "Writingcar..." << endl;
	_finddata_t file;	//_finddata_t是用来存储文件各种信息的结构体
	ofstream outf; //声明输出流
	outf.open("E:\\交通异常检测\\样本库\\人样本库-训练\\PositiveImageList.txt");//通过构造函数打开文件
	int k;
	long long HANDLE;
	k = HANDLE = _findfirst("E:\\交通异常检测\\样本库\\人样本库-训练\\*.png", &file);//找到所有png文件  
	while (k != -1)
	{
		outf << file.name << endl;	//输出文件的名字
		k = _findnext(HANDLE, &file);
	}
	_findclose(HANDLE);
	outf.close();
	cout << "Finish!" << endl;
}

/*-------------------------------------将负样本的图片名循环写入到txt文件内--------------------------------------*/

void Neg_List()
{
	cout << "Writingcar..." << endl;
	_finddata_t file;	//_finddata_t是用来存储文件各种信息的结构体
	ofstream outf;
	outf.open("E:\\交通异常检测\\样本库\\车样本库-训练\\NegativeImageList.txt");
	int k;
	long long HANDLE;
	k = HANDLE = _findfirst("E:\\交通异常检测\\样本库\\车样本库-训练\\*.png", &file);
	while (k != -1)
	{
		outf << file.name << endl;	//输出文件的文件名
		k = _findnext(HANDLE, &file);
	}
	_findclose(HANDLE);
	outf.close();
	cout << "Finish!" << endl;
	//system("pause");
}


/*-------------------------------------把测试文件夹里面的图片文件名循环写入到txt文件内--------------------------------------*/
void test_List()
{
	cout << "Writingcar..." << endl;
	_finddata_t file;	//_finddata_t是用来存储文件各种信息的结构体
	ofstream outf; //声明输出流
	outf.open("E:\\交通异常检测\\样本库\\人样本库-测试\\testList.txt");//通过构造函数打开文件
	int k;
	long long HANDLE;
	k = HANDLE = _findfirst("E:\\交通异常检测\\样本库\\人样本库-测试\\*.png", &file);//找到所有png文件  
	while (k != -1)
	{
		outf << file.name << endl;	//输出文件的名字
		k = _findnext(HANDLE, &file);
	}
	_findclose(HANDLE);
	outf.close();
}
/*-------------------------------------把测试文件夹的图片文件名循环写入到另一个txt文件内--------------------------------------*/

void train_svm()
{

	//HOG检测器,用来计算HOG描述子的
	//检测窗口(48,48),块尺寸(16,16),块步长(8,8),cell尺寸(8,8),直方图bin个数9 
	cv::HOGDescriptor hog(cv::Size(48, 48), cv::Size(16, 16), cv::Size(8, 8), cv::Size(8, 8), 9);

	int DescriptorDim;//HOG描述子的维数,由图片大小、检测窗口大小、块大小、细胞单元中直方图bin个数决定  

	//设置SVM参数	
	cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
	svm->setType(cv::ml::SVM::Types::C_SVC);
	svm->setKernel(cv::ml::SVM::KernelTypes::LINEAR);
	svm->setTermCriteria(cv::TermCriteria(cv::TermCriteria::MAX_ITER, 100, 1e-6));
	std::string ImgName;

	//正样本图片的文件列表
	std::ifstream finPos("E:\\交通异常检测\\样本库\\人样本库-训练\\PositiveImageList.txt");
	//负样本图片的文件列表
	std::ifstream finNeg("E:\\交通异常检测\\样本库\\车样本库-训练\\NegativeImageList.txt");

	//所有训练样本的特征向量组成的特征矩阵,行数等于所有样本的个数(PosSamNO + NegSamNO),列数等于HOG描述子维数
	cv::Mat sampleFeatureMat;
	//训练样本的类别向量,行数等于所有样本的个数,列数等于1;1表示正样本,-1表示负样本
	cv::Mat sampleLabelMat;

	//依次读取txt中PosSamNO张正样本图片,并生成HOG描述子  
	for (int num = 0; num < PosSamNO && getline(finPos, ImgName); num++)
	{
		//给出图片绝对路径,将图片存入image矩阵
		ImgName = "E:\\交通异常检测\\样本库\\人样本库-训练\\" + ImgName;
		cv::Mat image = cv::imread(ImgName);

		//灰度化
		cv::Mat srcGray;
		cv::cvtColor(image, srcGray, CV_RGB2GRAY);
		std::cout << "Processing:" << ImgName << std::endl;
		//调整图片大小,令图片大小等于hog检测器窗口大小,运算速度将会很快
		cv::resize(srcGray, srcGray, cv::Size(48, 48));

		//HOG描述子向量
		std::vector<float> descriptors;
		//计算HOG描述子,检测窗口移动步长(8,8)
		hog.compute(srcGray, descriptors, cv::Size(8, 8));
		//处理第一个样本时初始化特征向量矩阵和类别矩阵,因为只有知道了特征向量的维数才能初始化特征向量矩阵 
		if (0 == num)
		{
			//HOG描述子的维数
			DescriptorDim = descriptors.size();
			//初始化所有训练样本的特征向量组成的矩阵,行数等于所有样本的个数,列数等于HOG描述子维数sampleFeatureMat 
			sampleFeatureMat = cv::Mat::zeros(PosSamNO + NegSamNO, DescriptorDim, CV_32FC1);
			//初始化训练样本的类别向量,行数等于所有样本的个数,列数等于1
			sampleLabelMat = cv::Mat::zeros(PosSamNO + NegSamNO, 1, CV_32SC1);
		}
		//将计算好的HOG描述子复制到样本特征矩阵sampleFeatureMat  
		for (int i = 0; i < DescriptorDim; i++)
		{
			//第num个样本的特征向量中的第i个元素 
			sampleFeatureMat.at<float>(num, i) = descriptors[i];
		}		//正样本类别为1
		sampleLabelMat.at<float>(num, 0) = 1;
	}


	//依次读取负样本图片,生成HOG描述子  
	for (int num = 0; num < NegSamNO && getline(finNeg, ImgName); num++)
	{

		ImgName = "E:\\交通异常检测\\样本库\\车样本库-训练\\" + ImgName;
		cv::Mat image = cv::imread(ImgName);
		cv::Mat srcGray;
		cv::cvtColor(image, srcGray, CV_RGB2GRAY);
		std::cout << "Processing:" << ImgName << std::endl;
		cv::resize(srcGray, srcGray, cv::Size(48, 48));
		//HOG描述子向量		
		std::vector<float> descriptors;
		//计算HOG描述子,检测窗口移动步长(8,8) 
		hog.compute(srcGray, descriptors, cv::Size(8, 8));

		//将计算好的HOG描述子复制到样本特征矩阵sampleFeatureMat
		for (int i = 0; i < DescriptorDim; i++)
		{
			//第PosSamNO+num个样本的特征向量中的第i个元素
			sampleFeatureMat.at<float>(num + PosSamNO, i) = descriptors[i];
		}
		//负样本类别为-1
		sampleLabelMat.at<float>(num + PosSamNO, 0) = -1;
	}


	//训练SVM分类器  
	std::cout << "开始训练SVM分类器" << std::endl;
	cv::Ptr<cv::ml::TrainData> td = cv::ml::TrainData::create(sampleFeatureMat, cv::ml::SampleTypes::ROW_SAMPLE, sampleLabelMat);

	svm->train(td);
	std::cout << "SVM分类器训练完成" << std::endl;

	//将训练好的SVM模型保存为xml文件
	svm->save("E:\\交通异常检测\\样本库\\人车SVM_HOG.xml");
	return;
}

void svm_hog_classification()
{
	//HOG检测器,用来计算HOG描述子的
	//检测窗口(48,48),块尺寸(16,16),块步长(8,8),cell尺寸(8,8),直方图bin个数9  
	cv::HOGDescriptor hog(cv::Size(48, 48), cv::Size(16, 16), cv::Size(8, 8), cv::Size(8, 8), 9);
	//HOG描述子的维数,由图片大小、检测窗口大小、块大小、细胞单元中直方图bin个数决定 
	int DescriptorDim;

	//测试样本图片的文件列表
	std::ifstream finTest("E:\\交通异常检测\\样本库\\人样本库-测试\\testList.txt");
	std::string ImgName;
	//分类错误的样本总和,有一个错误就加一
	float n = 0;
	for (int num = 0; num < TestSamNO && getline(finTest, ImgName); num++)
	{
		//从XML文件读取训练好的SVM模型
		cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::load("E:\\交通异常检测\\样本库\\人车SVM_HOG.xml");
		if (svm->empty())
		{
			std::cout << "load svm detector failed!!!" << std::endl;
			return;
		}
		//针对测试集进行识别
		std::cout << "开始识别..." << std::endl;
		std::cout << "Processing:" << ImgName << std::endl;
		ImgName = "E:\\交通异常检测\\样本库\\人样本库-测试\\" + ImgName;
		//ImgName = "D:\\1.png";
		cv::Mat test = cv::imread(ImgName);
		//也对测试图片进行灰度化
		cv::Mat srcGray;
		cv::cvtColor(test, test, CV_RGB2GRAY);
		cv::resize(test, test, cv::Size(48, 48));
		std::vector<float> descriptors;
		hog.compute(test, descriptors, cv::Size(8, 8));
		cv::Mat testDescriptor = cv::Mat::zeros(1, descriptors.size(), CV_32FC1);
		for (size_t i = 0; i < descriptors.size(); i++)
		{
			testDescriptor.at<float>(0, i) = descriptors[i];
		}
		float label = svm->predict(testDescriptor);
		std::cout << "这张图属于:" << label << std::endl;

		if (label < 0)
		{
			std::cout << "这张图属于:" << "background" << std::endl;
			n++;
		}
	}
	std::cout << "分类错误的图片数量:" << n << std::endl;
	std::cout << "测试图片总数量:" << TestSamNO << std::endl;
	float  x = (TestSamNO - n) / TestSamNO;
	std::cout << "正确率:" << x << std::endl;
	return;
}

int main(int argc, char** argv)
{
	//create txt
	Pos_List();
	Neg_List();
	test_List();
  
	train_svm();
	svm_hog_classification();

	system("pause");
	return 0;

}

此SVM分类准确率经过调整输入正负样本的个数后,准确率有94%。
《SVM实现多分类常用的两种方法以及一对一法的代码(VS13+opencv3.4)》
第二部分:再重复两次此过程得到三个分类器。通过调用分类器实现一对一法。

#include <stdio.h>
#include <iostream>  
#include <fstream>  
#include <opencv2/opencv.hpp>
#include <string>
#include <io.h>
#include<stdlib.h>

using namespace std;
using namespace cv::ml;
                                                 
int main(int argc, char** argv)
{
	//HOG检测器,用来计算HOG描述子的
	//检测窗口(48,48),块尺寸(16,16),块步长(8,8),cell尺寸(8,8),直方图bin个数9  
	cv::HOGDescriptor hog(cv::Size(48, 48), cv::Size(16, 16), cv::Size(8, 8), cv::Size(8, 8), 9);
	//HOG描述子的维数,由图片大小、检测窗口大小、块大小、细胞单元中直方图bin个数决定 
	int DescriptorDim;


	//从XML文件读取训练好的SVM模型
	cv::Ptr<cv::ml::SVM> svm1 = cv::ml::SVM::load("E:\\交通异常检测\\样本库\\人车SVM_HOG.xml");
	cv::Ptr<cv::ml::SVM> svm2 = cv::ml::SVM::load("E:\\交通异常检测\\样本库\\人背景SVM_HOG.xml");
	cv::Ptr<cv::ml::SVM> svm3 = cv::ml::SVM::load("E:\\交通异常检测\\样本库\\车背景SVM_HOG.xml");
	if (svm1->empty())
	{
		std::cout << "load svm1 detector failed!!!" << std::endl;
		//return 0;
	}
	if (svm2->empty())
	{
		std::cout << "load svm2 detector failed!!!" << std::endl;
		//return 0;
	}
	if (svm3->empty())
	{
		std::cout << "load svm3 detector failed!!!" << std::endl;
		//return 0;
	}
	//针对测试集进行识别
	std::cout << "开始识别..." << std::endl;
	string	ImgName = "E:\\交通异常检测\\样本库\\车样本库-测试\\1.png";
	cv::Mat test = cv::imread(ImgName);
	//也对测试图片进行灰度化
	cv::cvtColor(test, test, CV_RGB2GRAY);
	cv::resize(test, test, cv::Size(48, 48));
	std::vector<float> descriptors;
	hog.compute(test, descriptors, cv::Size(8, 8));
	cv::Mat testDescriptor = cv::Mat::zeros(1, descriptors.size(), CV_32FC1);
	for (int i = 0; i < descriptors.size(); i++)
	{
		testDescriptor.at<float>(0, i) = descriptors[i];
	}
	//计算投票结果
	int people = 0;
	int	car = 0;
	int background = 0;
	float label1 = svm1->predict(testDescriptor);
	if (label1 < 0)
	{
		car++;
	}
	else
	{
		people++;
	}

	float label2 = svm2->predict(testDescriptor);
	if (label2 < 0)
	{
		people++;
	}
	else
	{
		background++;
	}


	float label3 = svm3->predict(testDescriptor);
	if (label3 < 0)
	{
		car++;
	}
	else
	{
		background++;
	}



	if (car == people && car == background)
	{
		std::cout << "胡歌0" << std::endl;
		system("pause");
		return 0;
	}
	else if (car >= 2)
	{
		std::cout << "胡歌1" << std::endl;
		system("pause");
		return 1;
	}
	else if (people >= 2)
	{
		std::cout << "胡歌2" << std::endl;
		system("pause");
		return 2;
	}
	else if (background >=2 )
	{
		std::cout << "胡歌3" << std::endl;
		system("pause");
		return 3;
	}


}

但是此种方法存在一个问题,如果分类器效果不好的话,会存在多次平票结果,难以判断出属于哪一个类,所以下一次我将结合DAG法,消去误判、拒分的情况。

    原文作者:306247770
    原文地址: https://blog.csdn.net/qq_42178056/article/details/84845673
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞