使用OpenCV3.4的SVM实现mnist手写体的训练和预测

最近在用C++做手写体识别,踩了许多坑。。网上使用SVM的教程遇到的都比较坑,看了半天没怎么涉及原理,而代码又比较乱,没怎么介绍,害我搞了一下午,所以就很烦,所幸最后终于找到了方法,所以想把这段比较痛苦的经历记录下来,造福后人。如果是想从本文弄懂原理的话,那比较抱歉。

说明 

实验环境是:VS2017 + OpenCV3.4.0+win10;

关于配置OpenCV.3.4.0, 整个过程的步骤比较简单,结合代码,从读取数据,训练和预测三个方面来展开。

 

读取数据

先从官网下载好四个数据集,链接可以看这里

在读取的时候,我对mnist数据集进行了二值化,将大于0的数据置为255。

关于读取步骤不多讲,直接上代码。

  • 传入文件名,读取标签集,将标签数据转为Mat矩阵。读取数据的格式需要指定为CV_32SC1。这一步很重要!!其他版本可能不同,但是如果是其他类型的需要convertTo来转换,不然在训练数据的时候会报错。
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<float>(i, 0) = label;
			//cout << "Label: " << labels[i] << endl;
		}

	}
}

 

  • 传入文件名,读取训练数据和测试数据集,将数据转为Mat矩阵。读取数据的格式需要指定为CV_32F,浮点型。这一步很重要!!其他版本可能不同,不然的话训练会报莫名的错误。

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<float>(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。

主要有以下几个步骤。

  1. 声明SVM分类器
  2. 设置SVM参数
  3. 提取数据后开始训练
  4. 保存训练结果
  • 创建SVM分类器,然后设置参数。
// 创建分类器并设置参数
Ptr<SVM> 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<TrainData> 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<float>(0, j) = testImage->at<float>(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 <stdio.h>  
#include <time.h>  
#include <opencv2/opencv.hpp>  
#include <opencv/cv.h>  
#include <iostream> 
#include <opencv2/core/core.hpp>  
#include <opencv2/highgui/highgui.hpp>  
#include <opencv2/ml/ml.hpp>  



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<float>(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<float>(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> 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<TrainData> 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<float>(0, j) = testImage->at<float>(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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章