OpenCV机器学习:SVM分类器实现MNIST手写数字识别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wblgers1234/article/details/73477860

0. 开发环境

最近机器学习随着AI人工智能的兴起越来越火,博主想找一些ML的库来练手。突然想起之前在看Opencv的doc时发现有ML的component,于是心血来潮就开始写代码试试。话不多说,直接进正题。

以下我的开发环境配置:
-Windows7
-Visual Studio2015
-OpenCV3.2

1. MNIST手写数据库

我们选用鼎鼎大名的MNIST手写库作为数据集,MNIST是由深度学习三大神之一Yann LeCun带头创建的,可以在下面的链接进行下载:

http://yann.lecun.com/exdb/mnist/

MNIST数据集分为以下四部分:
(1)
train-images-idx3-ubyte
训练图像的集合,共有60000张,大小是28×28
(2)
train-labels-idx1-ubyte
对应于训练图像的标签集,为0~9
(3)
t10k-images-idx3-ubyte
测试图像的集合,共有10000张,大小是28×28
(4)
t10k-labels-idx1-ubyte
对应于测试图像的标签集,为0~9

2. 代码分析

2.1 读取MNIST数据集

在MNIST数据库的主页上对整个MNIST的结构进行了介绍,四个文件都是binary二进制文件,并且数据的存储有一定的格式,在读取时需要小心。

对于标签集train-labels-idx1-ubyte(t10k-labels-idx1-ubyte也是类似)来说,数据存储的描述如下:

TRAINING SET LABEL FILE (train-labels-idx1-ubyte):
[offset] [type]          [value]          [description] 
0000     32 bit integer  0x00000801(2049) magic number (MSB first) 
0004     32 bit integer  60000            number of items 
0008     unsigned byte   ??               label 
0009     unsigned byte   ??               label 
........ 
xxxx     unsigned byte   ??               label
The labels values are 0 to 9.

可以看到,我们需要读取的第一个数是32bit的magic number,第二个数据是图像标签的个数,也是32bit的整数,接下来才是每个图像对应的标签值,每个按照unsigned byte进行存储。

对于训练/测试图像集train-images-idx3-ubyte(t10k-images-idx3-ubyte也是类似)来说,数据存储的描述如下:

TRAINING SET IMAGE FILE (train-images-idx3-ubyte):

[offset] [type]          [value]          [description] 
0000     32 bit integer  0x00000803(2051) magic number 
0004     32 bit integer  60000            number of images 
0008     32 bit integer  28               number of rows 
0012     32 bit integer  28               number of columns 
0016     unsigned byte   ??               pixel 
0017     unsigned byte   ??               pixel 
........ 
xxxx     unsigned byte   ??               pixel
Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black).

可以看到,我们需要读取的第一个数是32bit的magic number,第二个数据是图像的个数,也是32bit的整数,接下来分别是图像的行数和列数,均是32bit的整数,最后是所有图像的像素点(8bit的unsigned byte)按照行优先的格式进行存储。

PS:注意MNIST的描述中有这样一句话,所有32bit的整数是按照MSB在前(大端模式)进行存储的。在Intel及其他小端处理器上,需要对这些整数进行大小端翻转。

All the integers in the files are stored in the MSB first (high endian) format used by most non-Intel processors. Users of Intel processors and other low-endian machines must flip the bytes of the header.
2.1.1

下面的代码段通过位运算实现32bit整数的大端-小端的转换:

//大端转小端
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;
}
2.1.2

我们利用C++中fstream的子类ifstream进行二进制文件的读取,请注意在打开文件时需要设置ios::binary

//读取训练样本集
ifstream if_trainImags("train-images-idx3-ubyte", ios::binary);
//读取失败
if (true == if_trainImags.fail())
{
    cout << "Please check the path of file train-images-idx3-ubyte" << endl;
    return;
}

调用函数ifstream.read()对数据进行按byte读取

int magic_num, trainImgsNum, nrows, ncols;
//读取magic number
if_trainImags.read((char*)&magic_num, sizeof(magic_num));
magic_num = reverseInt(magic_num);

在读取训练图像时,需要注意的一点是需要将CV_8UC1格式的数据转换为CV_32FC1。在for循环里,我们每一次读取一张图片的所有像素点到Mat矩阵temp,然后调用Opencv内置的convertTo函数实现unsigned char到32bit float转换,最后拷贝到trainFeatures中占满一行,注意Mat数据结构是行优先的。

//读取训练图像
int imgVectorLen = nrows * ncols;
Mat trainFeatures = Mat::zeros(trainImgsNum, imgVectorLen, CV_32FC1);
Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
for (int i = 0; i < trainImgsNum; i++)
{
    if_trainImags.read((char*)temp.data, imgVectorLen);
    Mat tempFloat;
    //由于SVM需要的训练数据格式是CV_32FC1,在这里进行转换
    temp.convertTo(tempFloat, CV_32FC1);
    memcpy(trainFeatures.data+i*imgVectorLen *sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
}

在完成训练样本的读取后,需要对其进行归一化,像素点是0~255的,归一化至0~1。原因在于我们在SVM分类时采用的是RBF的kernal,这一点我们在后面会细讲。

//归一化
trainFeatures = trainFeatures / 255;

按照同样的读取方式,测试样本集以及训练&测试标签集都可以读取成功。

2.2 SVM训练&预测

按照下面的代码创建一个SVM的分类器,并初始化参数
(1)type
这里我们选择了 SVM::C_SVC 类型,该类型可以用于n-类分类问题 (n>2)。
(2)kernal
CvSVM::RBF : 基于径向的函数,对于大多数情况都是一个较好的选择。
(3)Gamma&C
经验值选择
在训练结束之后我们把SVM分类模型保存在xml文件里。

// 训练SVM分类器
//初始化
Ptr<SVM> svm = SVM::create();
//多分类
svm->setType(SVM::C_SVC);
//kernal选用RBF
svm->setKernel(SVM::RBF);
//设置经验值 
svm->setGamma(0.01);
svm->setC(10.0);
//设置终止条件,在这里选择迭代200次
svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 200, FLT_EPSILON));
//训练开始
svm->train(trainFeatures, ROW_SAMPLE, trainLabels);

cout << "训练结束,正写入xml:" << endl;
//保存模型
svm->save("mnist.xml");

接下来导入训练好的SVM模型对测试数据集进行预测并计算准确率

//载入训练好的SVM模型
Ptr<SVM> svm = SVM::load("mnist.xml");
int sum = 0;
//对每一个测试图像进行SVM分类预测
for (int i = 0; i < testLblsNum; i++)
{
    Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
    memcpy(predict_mat.data, testFeatures.data + i*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
    //预测
    float predict_label = svm->predict(predict_mat);
    //真实的样本标签
    float truth_label = testLabels.at<int>(i);
    //比较判定是否预测正确
    if ((int)predict_label == (int)truth_label)
    {
        sum++;
    }
}

cout << "预测准确率为:"<<(double)sum / (double)testLblsNum << endl;

2.3 结果

MNIST手写数据集的训练过程:
这里写图片描述

MNIST手写数据集的测试验证:
这里写图片描述

从结果可以看到,训练样本集为60000,测试样本集为10000时,SVM分类准确率可以到96.98%,而且SVM的最大迭代次数为200。

单个样本的随机测试:
这里写图片描述

在自己的测试图片上进行预测:(可以用windows自带的画图板进行黑底白字的数字绘制)
这里写图片描述

2.4 全部代码

2.4.1 SVM训练MNIST过程:
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
#include <fstream>

using namespace cv;
using namespace cv::ml;
using namespace std;

//大端转小端
int reverseInt(int i);

void main()
{
    //读取训练样本集
    ifstream if_trainImags("train-images-idx3-ubyte", ios::binary);
    //读取失败
    if (true == if_trainImags.fail())
    {
        cout << "Please check the path of file train-images-idx3-ubyte" << endl;
        return;
    }
    int magic_num, trainImgsNum, nrows, ncols;
    //读取magic number
    if_trainImags.read((char*)&magic_num, sizeof(magic_num));
    magic_num = reverseInt(magic_num);
    cout << "训练图像数据库train-images-idx3-ubyte的magic number为:" << magic_num << endl;
    //读取训练图像总数
    if_trainImags.read((char*)&trainImgsNum, sizeof(trainImgsNum));
    trainImgsNum = reverseInt(trainImgsNum);
    cout << "训练图像数据库train-images-idx3-ubyte的图像总数为:" << trainImgsNum << endl;
    //读取图像的行大小
    if_trainImags.read((char*)&nrows, sizeof(nrows));
    nrows = reverseInt(nrows);
    cout << "训练图像数据库train-images-idx3-ubyte的图像维度row为:" << nrows << endl;
    //读取图像的列大小
    if_trainImags.read((char*)&ncols, sizeof(ncols));
    ncols = reverseInt(ncols);
    cout << "训练图像数据库train-images-idx3-ubyte的图像维度col为:" << ncols << endl;

    //读取训练图像
    int imgVectorLen = nrows * ncols;
    Mat trainFeatures = Mat::zeros(trainImgsNum, imgVectorLen, CV_32FC1);
    Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
    for (int i = 0; i < trainImgsNum; i++)
    {
        if_trainImags.read((char*)temp.data, imgVectorLen);
        Mat tempFloat;
        //由于SVM需要的训练数据格式是CV_32FC1,在这里进行转换
        temp.convertTo(tempFloat, CV_32FC1);
        memcpy(trainFeatures.data+i*imgVectorLen *sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
    }
    //归一化
    trainFeatures = trainFeatures / 255;
    //读取训练图像对应的分类标签
    ifstream if_trainLabels("train-labels-idx1-ubyte", ios::binary);
    //读取失败
    if (true == if_trainLabels.fail())
    {
        cout << "Please check the path of file train-labels-idx1-ubyte" << endl;
        return;
    }
    int magic_num_2, trainLblsNum;
    //读取magic number
    if_trainLabels.read((char*)&magic_num_2, sizeof(magic_num_2));
    magic_num_2 = reverseInt(magic_num_2);
    cout << "训练图像标签数据库train-labels-idx1-ubyte的magic number为:" << magic_num_2 << endl;
    //读取训练图像的分类标签的数量
    if_trainLabels.read((char*)&trainLblsNum, sizeof(trainLblsNum));
    trainLblsNum = reverseInt(trainLblsNum);
    cout << "训练图像标签数据库train-labels-idx1-ubyte的标签总数为:" << trainLblsNum << endl;

    //由于SVM需要输入的标签类型是CV_32SC1,在这里进行转换
    Mat trainLabels = Mat::zeros(trainLblsNum, 1, CV_32SC1);
    Mat readLabels  = Mat::zeros(trainLblsNum, 1, CV_8UC1);
    if_trainLabels.read((char*)readLabels.data, trainLblsNum*sizeof(char));
    readLabels.convertTo(trainLabels, CV_32SC1);

    /*
    //Add some random test code
    while (1)
    {
    int index;
    cout << "请输入要查看的训练图像下标" << endl;
    cin >> index;
    if (-1 == index)
    {
    break;
    }
    Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);
    memcpy(show_mat.data, trainFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
    Mat show_char;
    show_mat.convertTo(show_char, CV_8UC1);
    imshow("test", show_mat);
    cout << "标签值为" << trainLabels.at<int>(index);
    }
    waitKey(0);
    */


    // 训练SVM分类器
    //初始化
    Ptr<SVM> svm = SVM::create();
    //多分类
    svm->setType(SVM::C_SVC);
    //kernal选用RBF
    svm->setKernel(SVM::RBF);
    //设置经验值 
    svm->setGamma(0.01);
    svm->setC(10.0);
    //设置终止条件,在这里选择迭代200次
    svm->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER, 200, FLT_EPSILON));
    //训练开始
    svm->train(trainFeatures, ROW_SAMPLE, trainLabels);

    cout << "训练结束,正写入xml:" << endl;
    //保存模型
    svm->save("mnist.xml");



}

//大端转小端
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;
}
2.4.2 SVM测试MNIST过程:

鉴于在评论区里有很多同学在问怎么把自己的图片丢到代码里面去进行测试,所以我把代码进行了修改。详见while循环中的改动。

#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include "opencv2/imgcodecs.hpp"
#include <opencv2/highgui.hpp>
#include <opencv2/ml.hpp>
#include <fstream>

using namespace cv;
using namespace cv::ml;
using namespace std;

//大端转小端
int reverseInt(int i);

void main()
{
    //读取测试样本集
    ifstream if_testImags("t10k-images-idx3-ubyte", ios::binary);
    //读取失败
    if (true == if_testImags.fail())
    {
        cout << "Please check the path of file t10k-images-idx3-ubyte" << endl;
        return;
    }
    int magic_num, testImgsNum, nrows, ncols;
    //读取magic number
    if_testImags.read((char*)&magic_num, sizeof(magic_num));
    magic_num = reverseInt(magic_num);
    cout << "测试图像数据库t10k-images-idx3-ubyte的magic number为:" << magic_num << endl;
    //读取测试图像总数
    if_testImags.read((char*)&testImgsNum, sizeof(testImgsNum));
    testImgsNum = reverseInt(testImgsNum);
    cout << "测试图像数据库t10k-images-idx3-ubyte的图像总数为:" << testImgsNum << endl;
    //读取图像的行大小
    if_testImags.read((char*)&nrows, sizeof(nrows));
    nrows = reverseInt(nrows);
    cout << "测试图像数据库t10k-images-idx3-ubyte的图像维度row为:" << nrows << endl;
    //读取图像的列大小
    if_testImags.read((char*)&ncols, sizeof(ncols));
    ncols = reverseInt(ncols);
    cout << "测试图像数据库t10k-images-idx3-ubyte的图像维度col为:" << ncols << endl;

    //读取测试图像
    int imgVectorLen = nrows * ncols;
    Mat testFeatures = Mat::zeros(testImgsNum, imgVectorLen, CV_32FC1);
    Mat temp = Mat::zeros(nrows, ncols, CV_8UC1);
    for (int i = 0; i < testImgsNum; i++)
    {
        if_testImags.read((char*)temp.data, imgVectorLen);
        Mat tempFloat;
        //由于SVM需要的测试数据格式是CV_32FC1,在这里进行转换
        temp.convertTo(tempFloat, CV_32FC1);
        memcpy(testFeatures.data + i*imgVectorLen * sizeof(float), tempFloat.data, imgVectorLen * sizeof(float));
    }
    //归一化
    testFeatures = testFeatures / 255;
    //读取测试图像对应的分类标签
    ifstream if_testLabels("t10k-labels-idx1-ubyte", ios::binary);
    //读取失败
    if (true == if_testLabels.fail())
    {
        cout << "Please check the path of file t10k-labels-idx1-ubyte" << endl;
        return;
    }
    int magic_num_2, testLblsNum;
    //读取magic number
    if_testLabels.read((char*)&magic_num_2, sizeof(magic_num_2));
    magic_num_2 = reverseInt(magic_num_2);
    cout << "测试图像标签数据库t10k-labels-idx1-ubyte的magic number为:" << magic_num_2 << endl;
    //读取测试图像的分类标签的数量
    if_testLabels.read((char*)&testLblsNum, sizeof(testLblsNum));
    testLblsNum = reverseInt(testLblsNum);
    cout << "测试图像标签数据库t10k-labels-idx1-ubyte的标签总数为:" << testLblsNum << endl;

    //由于SVM需要输入的标签类型是CV_32SC1,在这里进行转换
    Mat testLabels = Mat::zeros(testLblsNum, 1, CV_32SC1);
    Mat readLabels = Mat::zeros(testLblsNum, 1, CV_8UC1);
    if_testLabels.read((char*)readLabels.data, testLblsNum * sizeof(char));
    readLabels.convertTo(testLabels, CV_32SC1);

    //载入训练好的SVM模型
    Ptr<SVM> svm = SVM::load("mnist.xml");
    int sum = 0;
    //对每一个测试图像进行SVM分类预测
    for (int i = 0; i < testLblsNum; i++)
    {
        Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
        memcpy(predict_mat.data, testFeatures.data + i*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
        //预测
        float predict_label = svm->predict(predict_mat);
        //真实的样本标签
        float truth_label = testLabels.at<int>(i);
        //比较判定是否预测正确
        if ((int)predict_label == (int)truth_label)
        {
            sum++;
        }
    }

    cout << "预测准确率为:"<<(double)sum / (double)testLblsNum << endl;


    //随机测试某一个图像看效果,输入为-2时退出,输入-1时则测试本地图片“2.jpg”,注意路径要放到源代码同级目录
    while (1)
    {
        int index;
        cout << "请输入要查看的测试图像下标" << endl;
        cin >> index;
        if (-1 == index)
        {
            Mat imgRead = imread("2.jpg", 0);
            Mat imgReadScal = Mat::zeros(nrows, ncols, CV_8UC1);
            Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);

            resize(imgRead, imgReadScal, imgReadScal.size());

            imgReadScal.convertTo(show_mat, CV_32FC1);

            show_mat = show_mat / 255;

            //
            Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
            //memcpy(show_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            memcpy(predict_mat.data, show_mat.data, imgVectorLen * sizeof(float));
            float response = svm->predict(predict_mat);

            imshow("test", show_mat);
            cout << "标签值为" << response << endl;

            waitKey(0);

        }
        else if (-2 == index)
        {
            break;
        }
        else
        {
            Mat show_mat = Mat::zeros(nrows, ncols, CV_32FC1);
            Mat predict_mat = Mat::zeros(1, imgVectorLen, CV_32FC1);
            memcpy(show_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            memcpy(predict_mat.data, testFeatures.data + index*imgVectorLen * sizeof(float), imgVectorLen * sizeof(float));
            float response = svm->predict(predict_mat);

            imshow("test", show_mat);
            cout << "标签值为" << response << endl;

            waitKey(0);
        }

    }

}

//大端转小端
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;
}

喜欢的话可以拿去试试,但务必请注明转载地址~

3. 参考资料

http://yann.lecun.com/exdb/mnist/
http://docs.opencv.org/3.2.0/d1/d73/tutorial_introduction_to_svm.html

猜你喜欢

转载自blog.csdn.net/wblgers1234/article/details/73477860