您的位置:首页 > 编程语言

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

2018-12-05 23:49 330 查看

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)输出值,最大者对应类别为输入的类别。

图片里面有三类,因此训练三个分类器,三类中的一个类作为正样本,则其它两类都划分为负样本,进行训练,得到三个分类器。

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

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个叶结点,每个节点对应一个二类分类器。

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::loa
1b5d8
d("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%。

第二部分:再重复两次此过程得到三个分类器。通过调用分类器实现一对一法。

#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法,消去误判、拒分的情况。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐