最近在用C++做手写体识别,踩了许多坑。。网上使用SVM的教程遇到的都比较坑,看了半天没怎么涉及原理,而代码又比较乱,没怎么介绍,害我搞了一下午,所以就很烦,所幸最后终于找到了方法,所以想把这段比较痛苦的经历记录下来,造福后人。如果是想从本文弄懂原理的话,那比较抱歉。
实验环境是:VS2017 + OpenCV3.4.0+win10;
关于配置OpenCV.3.4.0, 整个过程的步骤比较简单,结合代码,从读取数据,训练和预测三个方面来展开。
先从官网下载好四个数据集,链接可以看这里。
在读取的时候,我对mnist数据集进行了二值化,将大于0的数据置为255。
关于读取步骤不多讲,直接上代码。
void read_Mnist_Label(string filename, Mat* &trainLabel)
{
ifstream file(filename, ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
trainLabel = new Mat(number_of_images, 1, CV_32SC1);
for (int i = 0; i < number_of_images; i++)
{
unsigned char label = 0;
file.read((char*)&label, sizeof(label));
if (label > 0) label = 255;
trainLabel->at(i, 0) = label;
//cout << "Label: " << labels[i] << endl;
}
}
}
void read_Mnist_Images(string filename, Mat* &trainImages)
{
ifstream file(filename, ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
file.read((char*)&n_rows, sizeof(n_rows));
file.read((char*)&n_cols, sizeof(n_cols));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
n_rows = ReverseInt(n_rows);
n_cols = ReverseInt(n_cols);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
cout << "rows = " << n_rows << endl;
cout << "cols = " << n_cols << endl;
trainImages = new Mat(number_of_images, n_rows * n_cols, CV_32F);
for (int i = 0; i < number_of_images; i++)
{
for (int r = 0; r < n_rows; r++)
{
for (int c = 0; c < n_cols; c++)
{
unsigned char image = 0;
file.read((char*)&image, sizeof(image));
if (image > 0) image = 255;
trainImages->at(i, r * n_cols + c) = image;
//if (i == 9999) cout << "IMAGE: " << i << " " << r * n_cols + c << " " << images[i][r * n_cols + c ] << endl;
//cout << images[i][r * n_cols + c] << endl;
}
}
}
}
}
其中数据需要从小端转为大端模式。
int ReverseInt(int i)
{
unsigned char ch1, ch2, ch3, ch4;
ch1 = i & 255;
ch2 = (i >> 8) & 255;
ch3 = (i >> 16) & 255;
ch4 = (i >> 24) & 255;
return((int)ch1 << 24) + ((int)ch2 << 16) + ((int)ch3 << 8) + ch4;
}
使用方法如下:
// 训练 加载模型
// 读取训练样本的数据
Mat* trainingDataMat = nullptr;
read_Mnist_Images("mnist_dataset/train-images.idx3-ubyte", trainingDataMat);
//训练样本的响应值
Mat* responsesMat = nullptr;
read_Mnist_Label("mnist_dataset/train-labels.idx1-ubyte", responsesMat);
Mat* testImage = nullptr;
Mat* testLabel = nullptr;
read_Mnist_Images("mnist_dataSet/t10k-images.idx3-ubyte", testImage);
read_Mnist_Label("mnist_dataSet/t10k-labels.idx1-ubyte", testLabel);
我是直接使用opencv内置的函数,所以整个过程关于训练部分代码量比较少。我CPU为 i5,大概训练时间为2min。
主要有以下几个步骤。
// 创建分类器并设置参数
Ptr SVM_params = SVM::create();
SVM_params->setType(SVM::C_SVC); //C_SVC用于分类,C_SVR用于回归
SVM_params->setKernel(SVM::RBF);
// 注释掉部分对本项目不影响,影响因子只有两个
//SVM_params->setDegree(0); //核函数中的参数degree,针对多项式核函数;
SVM_params->setGamma(0.50625); //核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数;
//SVM_params->setCoef0(0); //核函数中的参数,针对多项式/SIGMOID核函数;
SVM_params->setC(12.5); //SVM最优问题参数,设置C-SVC,EPS_SVR和NU_SVR的参数;
//SVM_params->setNu(0); //SVM最优问题参数,设置NU_SVC, ONE_CLASS 和NU_SVR的参数;
//SVM_params->setP(0); //SVM最优问题参数,设置EPS_SVR 中损失函数p的值.
//结束条件,即训练1000次或者误差小于0.01结束
SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
Ptr tData = TrainData::create(*trainingDataMat, ROW_SAMPLE, *responsesMat);
SVM_params->train(tData);//训练
保存模型
SVM_params->save("svm.xml");
当训练过后,可直接加载分类器模型,然后提取数据进行预测。
SVM_params = SVM::load("svm.xml");
int count = 0; // 统计正确个数
Mat* testImage = nullptr;
Mat* testLabel = nullptr;
read_Mnist_Images("mnist_dataSet/t10k-images.idx3-ubyte", testImage);
read_Mnist_Label("mnist_dataSet/t10k-labels.idx1-ubyte", testLabel);
int height = testImage->size().height; // 测试图片的数量
int width = testImage->size().width; // 图片的维度
for (int i = 0; i < height; i++) { // 遍历所有测试图片
Mat image(1, width, CV_32F); // 单张图片
for (int j = 0; j < width; j++) { //
image.at(0, j) = testImage->at(i, j);
}
//cout << image.size().height << " " << image.size().width << " " << endl;
//cout << image.cols << " " << image.rows << " " << endl;
//cout << SVM_params->getVarCount() << " " << endl;
if (SVM_params->predict(image)) {
count++;
}
}
cout << "训练预测的准确率为:" << (double)count / height << endl;
system("pause");
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
using namespace cv;
using namespace ml;
Mat dealimage;
Mat src;
Mat yangben_gray;
Mat yangben_thresh;
int ReverseInt(int i)
{
unsigned char ch1, ch2, ch3, ch4;
ch1 = i & 255;
ch2 = (i >> 8) & 255;
ch3 = (i >> 16) & 255;
ch4 = (i >> 24) & 255;
return((int)ch1 << 24) + ((int)ch2 << 16) + ((int)ch3 << 8) + ch4;
}
void read_Mnist_Label(string filename, Mat* &trainLabel)
{
ifstream file(filename, ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
trainLabel = new Mat(number_of_images, 1, CV_32SC1);
for (int i = 0; i < number_of_images; i++)
{
unsigned char label = 0;
file.read((char*)&label, sizeof(label));
if (label > 0) label = 255;
trainLabel->at(i, 0) = label;
//cout << "Label: " << labels[i] << endl;
}
}
}
void read_Mnist_Images(string filename, Mat* &trainImages)
{
ifstream file(filename, ios::binary);
if (file.is_open())
{
int magic_number = 0;
int number_of_images = 0;
int n_rows = 0;
int n_cols = 0;
file.read((char*)&magic_number, sizeof(magic_number));
file.read((char*)&number_of_images, sizeof(number_of_images));
file.read((char*)&n_rows, sizeof(n_rows));
file.read((char*)&n_cols, sizeof(n_cols));
magic_number = ReverseInt(magic_number);
number_of_images = ReverseInt(number_of_images);
n_rows = ReverseInt(n_rows);
n_cols = ReverseInt(n_cols);
cout << "magic number = " << magic_number << endl;
cout << "number of images = " << number_of_images << endl;
cout << "rows = " << n_rows << endl;
cout << "cols = " << n_cols << endl;
trainImages = new Mat(number_of_images, n_rows * n_cols, CV_32F);
for (int i = 0; i < number_of_images; i++)
{
for (int r = 0; r < n_rows; r++)
{
for (int c = 0; c < n_cols; c++)
{
unsigned char image = 0;
file.read((char*)&image, sizeof(image));
if (image > 0) image = 255;
trainImages->at(i, r * n_cols + c) = image;
//if (i == 9999) cout << "IMAGE: " << i << " " << r * n_cols + c << " " << images[i][r * n_cols + c ] << endl;
//cout << images[i][r * n_cols + c] << endl;
}
}
}
}
}
int main()
{
cout << "训练数据请输入 1, 直接使用训练模型预测输入2" << endl;
string flag = "";
while (1) {
cin >> flag;
if (flag == "1" || flag == "2")
break;
else {
cout << "输入1,2" << endl;
}
}
// 创建分类器并设置参数
Ptr SVM_params = SVM::create();
if (flag == "1") {
// 训练 加载模型
// 读取训练样本的数据
Mat* trainingDataMat = nullptr;
read_Mnist_Images("mnist_dataset/train-images.idx3-ubyte", trainingDataMat);
//训练样本的响应值
Mat* responsesMat = nullptr;
read_Mnist_Label("mnist_dataset/train-labels.idx1-ubyte", responsesMat);
////===============================创建SVM模型===============================////
cout << SVM_params->getVarCount() << " " << endl;
SVM_params->setType(SVM::C_SVC); //C_SVC用于分类,C_SVR用于回归
SVM_params->setKernel(SVM::RBF); //LINEAR线性核函数。SIGMOID为高斯核函数
// 注释掉部分对本项目不影响,影响因子只有两个
//SVM_params->setDegree(0); //核函数中的参数degree,针对多项式核函数;
SVM_params->setGamma(0.50625); //核函数中的参数gamma,针对多项式/RBF/SIGMOID核函数;
//SVM_params->setCoef0(0); //核函数中的参数,针对多项式/SIGMOID核函数;
SVM_params->setC(12.5); //SVM最优问题参数,设置C-SVC,EPS_SVR和NU_SVR的参数;
//SVM_params->setNu(0); //SVM最优问题参数,设置NU_SVC, ONE_CLASS 和NU_SVR的参数;
//SVM_params->setP(0); //SVM最优问题参数,设置EPS_SVR 中损失函数p的值.
//结束条件,即训练1000次或者误差小于0.01结束
SVM_params->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER + TermCriteria::EPS, 1000, 0.01));
//Mat* responsesTransfer = new Mat(responsesMat->size().height, 1, CV_32FC1);
//responsesMat->convertTo(*responsesMat, CV_32SC1); 类型为CV_32SC1,此处省略是因为读取的时候已指明该格式了。
//trainingDataMat->convertTo(*trainingDataMat, CV_32F); 此处需要注意训练数据类型为 CV_32F
//训练数据和标签的结合
cout << "开始训练" << endl;
cout << "训练数据长度" << trainingDataMat->size().width << " 高度 " << trainingDataMat->size().height << endl;
cout << "标签数据长度" << responsesMat->size().width << " 高度 " << responsesMat->size().height << endl;
Ptr tData = TrainData::create(*trainingDataMat, ROW_SAMPLE, *responsesMat);
// 训练分类器
SVM_params->train(tData);//训练
SVM_params->save("svm.xml");
cout << SVM_params->getVarCount() << " " << endl;
//保存模型
SVM_params->save("svm.xml");
cout << "训练好了!!!" << endl;
delete trainingDataMat;
delete responsesMat;
trainingDataMat = NULL;
responsesMat = NULL;
}
else if (flag == "2") {
cout << "训练模型参数加载" << endl;
SVM_params = SVM::load("svm.xml");
//cout << SVM_params.empty() << endl;
}
cout << "-------SVM 开始预测-------------------------------" << endl;
int count = 0; // 统计正确率
Mat* testImage = nullptr;
Mat* testLabel = nullptr;
read_Mnist_Images("mnist_dataSet/t10k-images.idx3-ubyte", testImage);
read_Mnist_Label("mnist_dataSet/t10k-labels.idx1-ubyte", testLabel);
int height = testImage->size().height; // 测试图片的数量
int width = testImage->size().width; // 图片的维度
for (int i = 0; i < height; i++) { // 遍历所有测试图片
Mat image(1, width, CV_32F); // 单张图片
for (int j = 0; j < width; j++) { //
image.at(0, j) = testImage->at(i, j);
}
//cout << image.size().height << " " << image.size().width << " " << endl;
//cout << image.cols << " " << image.rows << " " << endl;
//cout << SVM_params->getVarCount() << " " << endl;
if (SVM_params->predict(image) == testLabel[i]) {
count++;
}
}
cout << "训练预测的准确率为:" << (double)count / height << endl;
system("pause");
//waitKey(0);
return 0;
}