一步一步学习Deep Learning (1) — C++ MNIST loading

MNIST字库介绍

MNIST是一个标准的手写字符测试集,收集者是人工智能领域著名的科学家,现在最火的深度学习网络Convulution Nueral Networks的创始人,现任Facebook AI实验室的主任 -- Yann LeCun (见下图).

Yann LeCun

该数据库具体介绍可以参看我挂在MNIST的超链接,这里仅仅简单的介绍一下,方便后续的学习。该数据库由四部分组成,其中训练样本包含60,000个集合以及其对应的标注信息,测试集合包含10,000个集合以及其对应的标注信息。这里标注信息其实就是Supervised Learning中的label。在90年代,这个数据库应该算是Big data,Yann LeCun利用自己设计的CNNs在此数据库上取得了当时最好的结果,在MNIST管网上有一些实验结果的对比。多的不闲扯了,主要来看看怎么读取这些字符已进一步的学习Deep Learning。


C++读取MNIST数据库

先从MNIST下载4份文件: "train-images-idx3-ubyte.gz", "train-labels-idx1-ubyte.gz", "t10k-images-idx3-ubyte.gz", "t10k-labels-idx1-ubyte.gz",并解压。解压后我们看到,MNIST的数据是用二进制文件存储,个人觉得用二进制文件存储数据有有两点优势:(1) 就是存储的数据占用的空间比较小; (2)读取起来速度比较快。这两点都是个人经验性的总结。但相对的,二进制文件不方便进行查看,需要事先知道其存储的格式或结构。在C++中,读取这类文件一般用到<fstream>,这里我选择使用<cstdio>中的纯C函数 -- fread来进行读取数据的操作。

刚才提到了,二进制文件的读取必须事先知道其内部的存储结构,所以在读取文件之前,先来简单分析一下文件中的存储结构,如不:

images files:

magic number
number of images
rows
cols
a very very long vector contains all digits

labels files:

magic number
number of images
a long vector contains all labels

首先,每份文件都有"magic number"以及"number of images",在图像文件中,紧接着还有每一张图片的大小,最后便是一个很长的向量,用于存储数据。分析清楚之后,读取的过程就相对简单了,只需要一步一步读取适当的数据即可。例如需要读取images里面的文件,首先读取"magic number",然后依次读取"number of images", "rows", "cols",然后利用这些读取到的参数分配内存空间进而读取字符数据。这里需要注意的有两点:(1) header(magic, number of images, rows, cols) 的读取;以及(2) digits的读取。

Header loading (magic, number, rows, cols)

关于"magic", "number", "rows"以及"cols"数据的读取主要跟初始Yann存储的相关,需要读取后做一个简单的转换,这里我借用了别人的代码进行数据的转换,如下:

int reverseInt4MNIST(const int i)
{
    uchar 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;
}

此代码将上面提到的几个数据进行转换,可以得到正确的数据,例如"magic"在images中是2051,labels中2049。读取指令如下(仅仅贴出读取图片数量的代码,完整代码会挂在github上):

fread(&numImages, sizeof(int), 1, file);

Images loading

从个人角度来说,我一般会用C++以及OpenCV来理解机器学习以及计算机视觉领域一些有意思算法的细节,这里也不例外。不过,在进行images数据的读取的时候,需要弄清楚两个问题:

- cv::Mat 的数据存储格式
- MNIST 图片的存储方式

cv::Mat数据存储格式

关于cv::Mat的数据存储格式可以参考两篇比较优秀的blog[1, 2],这里简单的解释一下。在OpenCV中,cv::Mat这一容器会将读取到的图像排列成如下模式:

B G R | B G R | B G R ...
B G R | B G R | B G R ...
...

这里以多通道为例(单通道的访问比较简单,同时也可以看成是特殊的多通道模式)。比较简单的访问这些元素的模式是利用cv::Mat自带的at函数,例如需要访问(r = 10, c = 11)的三通道元素,又假设此时通道存储的数据类型是CV_8UC3,则访问如下:

// CV_8UC3 --> Vec3b (完整对应请参看OpenCV手册或直接看代码)
image.at<Vec3b>(10, 11)[0] // B
image.at<Vec3b>(10, 11)[1] // G
image.at<Vec3b>(10, 11)[2] // R

不过这样的访问效率不是很高,在这篇博客中做了几种访问数据的速度对比。这里我们选择使用指针访问,同时利用cv::Mat的特殊结构 -- 即将数据存储在一个长向量中,则访问的模式可以通过以下代码实现:

// access [r, c, ch] data, CV_8UC3 type
// @r row index
// @c col index
// @ch channel index
uchar *ptr = (uchar *)image.data; // convert to CV_8U
uchar val = ptr[r * cols * channels + c * channels + ch];

上面rows, cols以及channels是图像行,列以及数据通道(RGB是3通道,而gray则是单通道)。此访问模式相对复杂,但其效率可以在所以进行图像的pixel操作中最优。为了能够更好的理解此指针操作,请结合前面关于cv::Mat数据存储的情况。

MNIST 图片的存储方式

MNIST所有图片存储在一个单一的很长很长的向量中,其实仔细剖析之后,发现其存储的方式为扫描方式,具体来说就是每一张图片的第一行填满前面D维数据,第二行填满接下来的D维数据,而第二张图片则接着第一张图片的存储,依次类推。在写这篇blog的时候发现,cv::Mat与MNIST里面数据的存储方式一致,而fread可以同时读取多位数据,所以这更加方便了我们读取MNIST数据集合,读取代码简单如下:

// @file source file
// @digits vector<mat> type for all digits 
// @rows rows per digit
// @cols cols per digit
// @i current digit index
fread(digits[i].data, sizeof(uchar), rows * cols, file)

前面提到的pixel的访问模式也可以进行读取,可能效率会慢一点,当然为了进一步提高读取的效率,可能可以通过OpenCV中的MatND数据结构,不过本人没有尝试过。好了基本的读取操作结束,这里简单的给出读取后Digits照片:

random digits


后记

MNIST是众多机器学习库中一个非常popular的库,最近几年依然有人在刷这个库的错误率。这里以这个数据库作为初期学习样本,一步一步深入理解最近比较火的Deep Learning模型,然后准备在Deep Learning上做一些非调参数的工作。以后会陆续记录自己学习Deep Learning的过程以及分享自己的代码。 More code here

Reference

[1] http://blog.csdn.net/xiaowei_cqu/article/details/7771760

[2] http://blog.csdn.net/yang_xian521/article/details/7161335

Comments