Spark-MLlib学习日记2:MNIST手写数字的读取使用

前言

近来广州阴雨连绵,实在是让人提不起精神来,文档什么的也看不进去,脑袋昏昏沉沉的,所以拖拖拉拉到今天才出更新=。=

重申一遍,我是沿着spark官网中MLlib的api往下去学习的,提到的相关算法都会去学习下。文档上有的东西呢,我就不赘述了,只是把自己学习过程中一些重要的,别的博客没有提到的,记录一下,以后自己复习也方便。在这篇东西之前,我已经自己看完了spark MLlib的数据类型和基本统计,没了解的可以先去看一下文档,后续会有用到。

好了,言归正传,今天要讲的是被称为机器学习界的“Hello world”,几乎每个入门系列都会用到的一个知名数据集——MNIST手写数字图像数据集,该系列的后续文章中也经常会用到,所以今天独立出来讲一下好了。

MNIST手写数字数据集

首先给出官网:THE MNIST DATABASE of handwritten digits

MNIST是一个经典的手写数字数据集,来自美国国家标准与技术研究所,由不同人手写的0至9的数字构成,由60000个训练样本集和10000个测试样本集构成,每个样本的尺寸为28x28,以二进制格式存储,如下图所示:

这里有个比较坑的细节,就是数据采集都是外国人,他们的手写数字风格跟我们亚洲人的略有不同,这也导致识别我们自己手写的数字时候准确率下降了不少=。=

下载下来之后是4个gz压缩包:

解压之后得到的是一个二进制文件,文件的格式官网有给出,这里我顺便贴一下:

TEST 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
…….. unsigned byte ?? label

The labels values are 0 to 9.

TEST 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
…….. unsigned byte ?? pixel

Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black).

训练集和测试集的结构都是一样的,这里我就只贴出训练集的结构,标签是0到9的数字,图像则是0到255的像素值。

其实一开始看到二进制文件我也是有点懵,因为以前工作也没接触过。不过看多两眼就明白了。
以images数据集来说吧,这个train-images-idx3-ubyte文件:

  • 0000字节到第0003字节,是一个32位int类型数字,值为2051的魔数
  • 0004字节到第0007字节也是一个32位int类型数字,转换过来值为60000,表示一共有60000个训练样本。
  • 0008字节到第00011字节和第0012字节到第0015字节同理,代表着有28行28列

这里的行和列,其实就是一张图片的像素矩阵,也就是每个图片都有28×28=784个像素。所以说,从第16个字节开始,就是图片的每一个像素点的值了。

说到这里可能会有些朋友担心,会不会每一张图片都是这么排下来呢?为此我去算了下这个二进制文件的长度,刚好就是16+28*28*60000,所以文件描述信息呢就之在文件开头出现了,后面的就是每张图片的像素连在一起了,图片的规格统一被处理成28*28的图片了。

因此,读取图片信息有了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 读取图像文件
* @param imagesPath
* @return
*/
def loadImages(imagesPath: String): Array[Array[Byte]] ={

val file = new File(imagesPath)
val in = new FileInputStream(file)
var trainingDS = new Array[Byte](file.length.toInt)
using(new FileInputStream(file)) { source =>
{
in.read(trainingDS)
}
}

//32 bit integer 0x00000803(2051) magic number
val magicNum = ByteBuffer.wrap(trainingDS.take(4)).getInt
println(s"magicNum=$magicNum")
//32 bit integer 60000 number of items
val numOfItems = ByteBuffer.wrap(trainingDS.slice(4, 8)).getInt
println(s"numOfItems=$numOfItems")
//32 bit integer 28 number of rows
val numOfRows = ByteBuffer.wrap(trainingDS.slice(8, 12)).getInt
println(s"numOfRows=$numOfRows")
//32 bit integer 28 number of columns
val numOfCols = ByteBuffer.wrap(trainingDS.slice(12, 16)).getInt
println(s"numOfCols=$numOfCols")

trainingDS = trainingDS.drop(16)


val itemsBuffer = new ArrayBuffer[Array[Byte]]
for(i <- 0 until numOfItems){
itemsBuffer += trainingDS.slice( i * numOfCols * numOfRows , (i+1) * numOfCols * numOfRows)
}

itemsBuffer.toArray
}

代码里面提取了文件描述信息,并返回有每一张图片像素数据组成的数组。完整代码我放在github上了: 完整代码

读取标签数据也是同理,在下一篇文章中,我将会详细讲述如何用标签和图片数据去做训练,敬请期待=。=

其实数据集到这里已经是完成读取了,后续只要转换成spark MLlib的数据格式就能用了,但是!我在把像素值由二进制byte类型转换成double类型的时候,居然出现了负数。这是怎么回事呢,看官网说的,值应该是在0到255之间才对啊。再仔细看了一遍文件架构,发现标明的像素类型为unsigned byte,即无符号byte类型,在我学习java的时候,隐约记得有学过说java类型都是有符号的,第一位即是正负号。
于是去搜索了一下,找到了这篇文章:《Java中对于unsigned byte类型的转换处理》

把二进制像素值转换为Double类型数字

由于Java中没有unsigned byte类型,所以一个字节只能表示(-128,127),而想要表示(0,255),也很简单,只要跟0xFF&操作即可。

原理其实就是正数的反码和补码都是其本身,负数的反码是对原码除了符号位之外作取反运算,补码则为反码+1。

更多细节可以参考:《byte为什么要与上0xff?》《Java中对于unsigned byte类型的转换处理》
下面贴出我处理逻辑的部分代码:

1
2
3
val data = trainLabe.zip(trainImages).map( d =>
LabeledPoint(d._1.toInt, Vectors.dense(d._2.map(p => (p & 0xFF).toDouble)))
)

下期预告

下一期我将会记录我用spark MLlib中提供的朴树贝叶斯算法,对该数据集进行训练,并用测试集测试。关于该数据集呢,其实还有一点要补充的,比如把自己的手写图片转换成MNIST标准格式,还有替代MNIST手写数字集的图像数据集Fashion MNIST,这些后续都会补充进该文章。持续更新ing…

参考链接

0%