本文所用的MNIST 数据集来自美国国家标准与技术研究所, National Institute of Standards and Technology (NIST). 训练集 (training set) 由来自 250 个不同人手写的数字构成, 其中 50% 是高中学生, 50% 来自人口普查局 (the Census Bureau) 的工作人员。 测试集(test set) 也是同样比例的手写数字数据。数据库可在 http://yann.lecun.com/exdb/mnist/ 获取, 它包含了四个部分:
train-images-idx3-ubyte: training set images
train-labels-idx1-ubyte: training set labels
t10k-images-idx3-ubyte: test set images
t10k-labels-idx1-ubyte: test set labels
其中,训练数据集包含60000幅图片,测试集包含10000幅图片。
1.读取数据集的数据
//mnist.h #ifndef MNIST_H #define MNIST_H #include <iostream> #include <string> #include <fstream> #include <ctime> #include <opencv2/opencv.hpp> using namespace cv; using namespace std; //小端存储转换 int reverseInt(int i); //读取image数据集信息 Mat read_mnist_image(const string fileName); //读取label数据集信息 Mat read_mnist_label(const string fileName); #endif大端模式:高位字节放在内存低地址处,低位字节放在内存高地址处;
小端模式:低位字节放在内存低地址处,高位字节放在内存高地址处;Intel处理器一般为小端模式。
MNIST使用了大端存储模式,因此第一步我们要做的就是大端转小端。
//mnist.cpp #include "mnist.h" //计时器 double cost_time; clock_t start_time; clock_t end_time; //测试item个数 int testNum = 10000; int reverseInt(int i) { unsigned char c1, c2, c3, c4; c1 = i & 255; c2 = (i >> 8) & 255; c3 = (i >> 16) & 255; c4 = (i >> 24) & 255; return ((int)c1 << 24) + ((int)c2 << 16) + ((int)c3 << 8) + c4; } Mat read_mnist_image(const string fileName) { int magic_number = 0; int number_of_images = 0; int n_rows = 0; int n_cols = 0; Mat DataMat; ifstream file(fileName, ios::binary); if (file.is_open()) { cout << "成功打开图像集 ... \n"; 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)); //cout << magic_number << " " << number_of_images << " " << n_rows << " " << n_cols << endl; 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 << " ;NUMBER OF IMAGES = " << number_of_images << " ; NUMBER OF ROWS = " << n_rows << " ; NUMBER OF COLS = " << n_cols << endl; //-test- //number_of_images = testNum; //输出第一张和最后一张图,检测读取数据无误 Mat s = Mat::zeros(n_rows, n_rows * n_cols, CV_32FC1); Mat e = Mat::zeros(n_rows, n_rows * n_cols, CV_32FC1); cout << "开始读取Image数据......\n"; start_time = clock(); DataMat = Mat::zeros(number_of_images, n_rows * n_cols, CV_32FC1); for (int i = 0; i < number_of_images; i++) { for (int j = 0; j < n_rows * n_cols; j++) { unsigned char temp = 0; file.read((char*)&temp, sizeof(temp)); float pixel_value = float((temp + 0.0) / 255.0); DataMat.at<float>(i, j) = pixel_value; //打印第一张和最后一张图像数据 if (i == 0) { s.at<float>(j / n_cols, j % n_cols) = pixel_value; } else if (i == number_of_images - 1) { e.at<float>(j / n_cols, j % n_cols) = pixel_value; } } } end_time = clock(); cost_time = (end_time - start_time) / CLOCKS_PER_SEC; cout << "读取Image数据完毕......" << cost_time << "s\n"; imshow("first image", s); imshow("last image", e); waitKey(0); } file.close(); return DataMat; } Mat read_mnist_label(const string fileName) { int magic_number; int number_of_items; Mat LabelMat; ifstream file(fileName, ios::binary); if (file.is_open()) { cout << "成功打开Label集 ... \n"; file.read((char*)&magic_number, sizeof(magic_number)); file.read((char*)&number_of_items, sizeof(number_of_items)); magic_number = reverseInt(magic_number); number_of_items = reverseInt(number_of_items); cout << "MAGIC NUMBER = " << magic_number << " ; NUMBER OF ITEMS = " << number_of_items << endl; //-test- //number_of_items = testNum; //记录第一个label和最后一个label unsigned int s = 0, e = 0; cout << "开始读取Label数据......\n"; start_time = clock(); LabelMat = Mat::zeros(number_of_items, 1, CV_32SC1); for (int i = 0; i < number_of_items; i++) { unsigned char temp = 0; file.read((char*)&temp, sizeof(temp)); LabelMat.at<unsigned int>(i, 0) = (unsigned int)temp; //打印第一个和最后一个label if (i == 0) s = (unsigned int)temp; else if (i == number_of_items - 1) e = (unsigned int)temp; } end_time = clock(); cost_time = (end_time - start_time) / CLOCKS_PER_SEC; cout << "读取Label数据完毕......" << cost_time << "s\n"; cout << "first label = " << s << endl; cout << "last label = " << e << endl; } file.close(); return LabelMat; }
2、opencv---svm识别
在成功读取MNIST数据集后,我们就准备好了训练样本和测试样本,接下去要做的就是调用opencv(本人所用的是opencv2.4.11)中的SVM进行识别,首先用训练数据进行训练得到model,然后用训练所得的model在测试数据集上进行测试。
开始训练前,我们要确定训练参数。在SVM中,参数对最终性能的影响非常大,在opencv中有两种函数用于训练,一种是固定参数的训练函数train,一种是可以寻找最优参数的训练函数train_auto,原型如下:
CvSVM::train(const CvMat* trainData, const CvMat* responses, const CvMat* varIdx=0, const CvMat* sampleIdx=0, CvSVMParams params=CvSVMParams() )1、trainData: 练习数据,必须是CV_32FC1 (32位浮点类型,单通道)。数据必须是CV_ROW_SAMPLE的,即特点向量以行来存储。
2、responses: 响应数据,凡是是1D向量存储在CV_32SC1 (仅仅用在分类题目上)或者CV_32FC1格局。
3、varIdx: 指定感爱好的特点。可所以整数(32sC1)向量,例如以0为开端的索引,或者8位(8uC1)的应用的特点或者样本的掩码。用户也可以传入NULL指针,用来默示练习中应用所有变量/样本。
4、sampleIdx: 指定感爱好的样本。描述同上。
5、params: SVM参数。
C++: bool CvSVM::train_auto(const Mat& trainData, const Mat& responses, const Mat& varIdx, const Mat& sampleIdx, CvSVMParams params, int k_fold=10, CvParamGrid Cgrid=CvSVM::get_default_grid(CvSVM::C), CvParamGrid gammaGrid=CvSVM::get_default_grid(CvSVM::GAMMA), CvParamGrid pGrid=CvSVM::get_default_grid(CvSVM::P), CvParamGrid nuGrid=CvSVM::get_default_grid(CvSVM::NU), CvParamGrid coeffGrid=CvSVM::get_default_grid(CvSVM::COEF), CvParamGrid degreeGrid=CvSVM::get_default_grid(CvSVM::DEGREE), bool balanced=false )自动训练函数的参数注释(13个)
1、前5个参数参考构造函数的参数注释。
2、k_fold: 交叉验证参数。训练集被分成k_fold的自子集。其中一个子集是用来测试模型,其他子集则成为训练集。所以,SVM算法复杂度是执行k_fold的次数。
3、*Grid: (6个)对应的SVM迭代网格参数。
4、balanced: 如果是true则这是一个2类分类问题。这将会创建更多的平衡交叉验证子集。
自动训练函数的使用说明
这个方法根据CvSVMParams中的最佳参数C, gamma, p, nu, coef0, degree自动训练SVM模型。参数被认为是最佳的交叉验证,其测试集预估错误最小。如果没有需要优化的参数,相应的网格步骤应该被设置为小于或等于1的值。例如,为了避免gamma的优化,设置gamma_grid.step = 0,gamma_grid.min_val, gamma_grid.max_val 为任意数值。所以params.gamma 由gamma得出。
最后,如果参数优化是必需的,但是相应的网格却不确定,你可能需要调用函数CvSVM::get_default_grid(),创建一个网格。例如,对于gamma,调用CvSVM::get_default_grid(CvSVM::GAMMA)。
该函数为分类运行 (params.svm_type=CvSVM::C_SVC 或者 params.svm_type=CvSVM::NU_SVC) 和为回归运行 (params.svm_type=CvSVM::EPS_SVR 或者 params.svm_type=CvSVM::NU_SVR)效果一样好。如果params.svm_type=CvSVM::ONE_CLASS,没有优化,并指定执行一般的SVM。
这里需要注意的是,对于需要的优化的参数虽然train_auto可以自动选择最优值,但在代码中也要先赋初始值,要不然编译能通过,但运行时会报错。
最终的main.cpp如下:
/*//main.cpp svm_type – 指定SVM的类型,下面是可能的取值: CvSVM::C_SVC C类支持向量分类机。 n类分组 (n \geq 2),允许用异常值惩罚因子C进行不完全分类。 CvSVM::NU_SVC \nu类支持向量分类机。n类似然不完全分类的分类器。参数为 \nu 取代C(其值在区间【0,1】中,nu越大,决策边界越平滑)。 CvSVM::ONE_CLASS 单分类器,所有的训练数据提取自同一个类里,然后SVM建立了一个分界线以分割该类在特征空间中所占区域和其它类在特征空间中所占区域。 CvSVM::EPS_SVR \epsilon类支持向量回归机。训练集中的特征向量和拟合出来的超平面的距离需要小于p。异常值惩罚因子C被采用。 CvSVM::NU_SVR \nu类支持向量回归机。 \nu 代替了 p。 可从 [LibSVM] 获取更多细节。 kernel_type – SVM的内核类型,下面是可能的取值: CvSVM::LINEAR 线性内核。没有任何向映射至高维空间,线性区分(或回归)在原始特征空间中被完成,这是最快的选择。K(x_i, x_j) = x_i^T x_j. CvSVM::POLY 多项式内核: K(x_i, x_j) = (\gamma x_i^T x_j + coef0)^{degree}, \gamma > 0. CvSVM::RBF 基于径向的函数,对于大多数情况都是一个较好的选择: K(x_i, x_j) = e^{-\gamma ||x_i - x_j||^2}, \gamma > 0. CvSVM::SIGMOID Sigmoid函数内核:K(x_i, x_j) = \tanh(\gamma x_i^T x_j + coef0). degree – 内核函数(POLY)的参数degree。 gamma – 内核函数(POLY/ RBF/ SIGMOID)的参数\gamma。 coef0 – 内核函数(POLY/ SIGMOID)的参数coef0。 Cvalue – SVM类型(C_SVC/ EPS_SVR/ NU_SVR)的参数C。 nu – SVM类型(NU_SVC/ ONE_CLASS/ NU_SVR)的参数 \nu。 p – SVM类型(EPS_SVR)的参数 \epsilon。 class_weights – C_SVC中的可选权重,赋给指定的类,乘以C以后变成 class\_weights_i * C。所以这些权重影响不同类别的错误分类惩罚项。权重越大,某一类别的误分类数据的惩罚项就越大。 term_crit – SVM的迭代训练过程的中止条件,解决部分受约束二次最优问题。您可以指定的公差和/或最大迭代次数。 */ #include "mnist.h" #include <opencv2/core/core.hpp> #include <opencv2/imgproc/imgproc.hpp> //#include "opencv2/imgcodecs.hpp" #include <opencv2/highgui/highgui.hpp> #include <opencv2/ml/ml.hpp> #include <string> #include <iostream> using namespace std; using namespace cv; //using namespace cv::ml; string trainImage = "D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\train-images.idx3-ubyte"; string trainLabel = "D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\train-labels.idx1-ubyte"; string testImage = "D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\t10k-images.idx3-ubyte"; string testLabel = "D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\t10k-labels.idx1-ubyte"; //计时器 double cost_time_; clock_t start_time_; clock_t end_time_; int main() { //--------------------- 1. Set up training data --------------------------------------- Mat trainData; Mat labels; trainData = read_mnist_image(trainImage); labels = read_mnist_label(trainLabel); cout << trainData.rows << " " << trainData.cols << endl; cout << labels.rows << " " << labels.cols << endl; //------------------------ 2. Set up the support vector machines parameters -------------------- CvSVMParams params; params.svm_type = CvSVM::C_SVC;//c类支持向量机 params.kernel_type = CvSVM::RBF;//内核函数选择 LINEAR/RBF/SIGMOID params.C = 1; params.p = 5e-3; params.gamma = 0.01; params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 100, 1e-6);//迭代终止条件 //-------------------------------------训练最佳参数该训练函数train为train_auto------------------------------------------------------------ //对不用的参数step设为0 CvParamGrid nuGrid = CvParamGrid(1,1,0.0); CvParamGrid coeffGrid = CvParamGrid(1,1,0.0); CvParamGrid degreeGrid = CvParamGrid(1,1,0.0); //------------------------ 3. Train the svm ---------------------------------------------------- cout << "Starting training process" << endl; start_time_ = clock(); CvSVM SVM; //SVM.train(trainData, labels, Mat(), Mat(), params); SVM.train_auto(trainData, labels, Mat(), Mat(), params, 10, SVM.get_default_grid(CvSVM::C), SVM.get_default_grid(CvSVM::GAMMA), SVM.get_default_grid(CvSVM::P), nuGrid, coeffGrid, degreeGrid); end_time_ = clock(); cost_time_ = (end_time_ - start_time_) / CLOCKS_PER_SEC; cout << "Finished training process...cost " << cost_time_ << " seconds..." << endl; CvSVMParams params_re = SVM.get_params(); float C = params_re.C; float gamma = params_re.gamma; cout << "the best c and the best gamma is " << C << " and " <<gamma << endl; //------------------------ 4. save the svm ---------------------------------------------------- SVM.save("D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\mnist_svm.xml"); cout << "save as /D:/C++_file/SVM_DEAL/MNIST DATABASE/mnist_svm.xml" << endl; //------------------------ 5. load the svm ---------------------------------------------------- cout << "开始导入SVM文件...\n"; CvSVM svm1; svm1.load("D:\\C++_file\\SVM_DEAL\\MNIST DATABASE\\mnist_svm.xml"); cout << "成功导入SVM文件...\n"; //------------------------ 6. read the test dataset ------------------------------------------- cout << "开始导入测试数据...\n"; Mat testData; Mat tLabel; testData = read_mnist_image(testImage); tLabel = read_mnist_label(testLabel); cout << "成功导入测试数据!!!\n"; float count = 0; for (int i = 0; i < testData.rows; i++) { Mat sample = testData.row(i); float res = svm1.predict(sample); res = std::abs(res - tLabel.at<unsigned int>(i, 0)) <= FLT_EPSILON ? 1.f : 0.f; count += res; } cout << "正确的识别个数 count = " << count << endl; cout << "错误率为..." << (10000 - count + 0.0) / 10000 * 100.0 << "%....\n"; system("pause"); return 0; }
最终得到的测试结果不太理想,错误率极高,猜想是不是跟我原始的参数设置有关系,看了很多人家的博客,在train_auto函数中我的参数设置如下:
SVM.train_auto(trainData, labels, Mat(), Mat(), params, 10, SVM.get_default_grid(CvSVM::C), SVM.get_default_grid(CvSVM::GAMMA), SVM.get_default_grid(CvSVM::P), nuGrid, coeffGrid, degreeGrid);
而人家的参数设置为:
SVM.train_auto(trainData, labels, NULL, NULL, params, 10, SVM.get_default_grid(CvSVM::C), SVM.get_default_grid(CvSVM::GAMMA), SVM.get_default_grid(CvSVM::P), nuGrid, coeffGrid, degreeGrid);
由于第三个和第四个参数我设置为NULL时一直报错,设置为Mat()可正常运行,这个问题一直没有解决,希望哪位路过的大神不小心看到本文的话提点一下,万分感谢!