前言
目前,深度学习有TensorFlow、Torch 、Caffe、Theano、Deeplearning4j等诸多开源框架,我们可以十分方便的用其实现一个深度神经网络,但是某些框架封装得太完善以至于让人忘记几个简单的调用语句背后程序做了多少工作。
因此,本博客的第一个目标是,在不使用库的情况下,使用Fortran语言完成一个多层神经网络,并用一些经典算例对其进行测试。鉴于并行计算的复杂性(PS:其实主要是我不会cuda,MPI虽然会,但是对于全连接问题,CPU间传输数据量会比较大,OpenMP可能更合适这种情况,这部分在之后探讨),这里不考虑并行性。另外,我们知道像Tensorflow、Theano等都集成了自动梯度工具,这里我们不会实现自动梯度功能,而是通过推导出公式之后再代码实现。
MNIST是一个手写数字的数据集,包含了60000个训练集以及10000个测试集数据,MNIST数据集是NIST数据集的子集,并且统一了图片的尺寸并做了居中化处理。根据作者的介绍,MNIST测试集的前5000个样本来自于NIST数据集的训练集,后5000个样本来自于NIST数据集的测试集,前者比后者干净、容易识别。
一、MNIST数据集格式
根据MNIST官网介绍,其数据包解压之后得到的二进制文件格式如下:
训练数据的标签数据集格式:
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.
训练数据的图片数据集格式:
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
测试数据的标签数据集格式:
TEST SET LABEL FILE (t10k-labels-idx1-ubyte):
[offset] [type] [value] [description]
0000 32 bit integer 0x00000801(2049) magic number (MSB first)
0004 32 bit integer 10000 number of items
0008 unsigned byte ?? label
0009 unsigned byte ?? label
........
xxxx unsigned byte ?? label
The labels values are 0 to 9.
测试数据的图片数据集格式:
TEST SET IMAGE FILE (t10k-images-idx3-ubyte):
[offset] [type] [value] [description]
0000 32 bit integer 0x00000803(2051) magic number
0004 32 bit integer 10000 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
训练集和测试集的图片数据格式是类似的,开始都是4个4字节的整数,分别表示:magic number、样本数量、每个样本行数28、每个样本的列数28。接下来就是图片的信息,由于图片是灰度图,使用的是1字节的整型表示其灰度值,范围是0~255。
训练集和测试集的标签数据格式也是类似的,开始是2个4字节的整数,分别表示:magic number、样本数量。接下来就是图片代表的哪个数字,用1字节的整型表示,范围是0~9。
二、Fortran读取
2.1 Fortran读取二进制文件
先来看如何使用Fortran读取二进制文件。使用Fortran读取二进制文件有两种方式:直接访问和流访问。
使用二进制阅读器查看train-images.idx3-ubyte文件可以看到前16字节写成八进制形式为:
00 00 08 03
00 00 EA 60
00 00 00 1C
00 00 00 1C
2.1.1 直接访问:
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col
integer :: int_len
inquire(iolength=int_len) magic_number
write(*, *) "int_len = ", int_len
open(UNIT=30, FILE='train-images.idx3-ubyte', ACCESS='direct', FORM='unformatted', RECL=int_len)
read(30, rec=1) magic_number
read(30, rec=2) sample_count
read(30, rec=3) row
read(30, rec=4) col
write(*, *) magic_number, sample_count, row, col
close(30)
END PROGRAM
输出结果:
int_len = 1
50855936 1625948160 469762048 469762048
数字转为十六进制为:
03 08 00 00
60 EA 00 00
1C 00 00 00
1C 00 00 00
2.1.2 流访问模式
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col
open(UNIT=30, FILE='train-images.idx3-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count, row, col
write(*, *) magic_number, sample_count, row, col
close(30)
END PROGRAM
如果使用的是Intel编译器的话,还可以直接使用binary格式。
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col
open(UNIT=30, FILE='train-images.idx3-ubyte', FORM='binary')
read(30) magic_number, sample_count, row, col
write(*, *) magic_number, sample_count, row, col
close(30)
END PROGRAM
2.1.3 结果的探讨
程序读取得到的结果是:50855936 1625948160 469762048 469762048。而不是:2051 60000 28 28 呢?转换成十六进制就可以看出,Fortran将2051十六进制00 00 08 03读进去之后,变成了03 08 00 00,即50855936。也就是说反向了,因而得到了不正确的结果。
2.2 使用Fortran读取数据集文件
将训练集图片信息读取输出到文本文件。这里需要注意的是这行代码:if (pixel_4int < 0) pixel_4int = pixel_4int + 256
,这是因为Fortran从文本中读取1个字节赋给1个字节的整型时,有时候会得到赋值,看起来像溢出一样,不知道什么缘故。
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col, point_count
integer(kind=1) :: pixel
integer(kind=4) :: pixel_4int
integer :: i
open(UNIT=33, FILE='train.txt', FORM='formatted', STATUS='replace')
open(UNIT=30, FILE='train-images.idx3-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count, row, col
sample_count = 60000
row = 28
col = 28
point_count = row*col*sample_count
do i=1, point_count
read(30) pixel
pixel_4int = pixel
if (pixel_4int < 0) pixel_4int = pixel_4int + 256
if (mod(i,28)== 1 .and. i > 1) write(33, *)
write(33, '(I5\)') pixel_4int
end do
close(30)
close(33)
END PROGRAM
将得到的文本文件打开可以看到前四个数字分别是:5 0 4 1。
三、转换为Fortran读取的二进制
为了以后更加方便的使用该数据集,使用Fortran将其按照4字节二进制输出到文件。这些文件已上传到Github上,这里是下载地址。
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col, point_count
integer(kind=1) :: pixel
integer(kind=4) :: pixel_4int
integer :: i
open(UNIT=33, FILE='train-images.fortran', &
ACCESS='stream', FORM='unformatted', STATUS='replace')
open(UNIT=30, FILE='train-images.idx3-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count, row, col
magic_number = 2051
sample_count = 60000
row = 28
col = 28
point_count = row*col*sample_count
write(33) magic_number, sample_count, row, col
do i=1, point_count
read(30) pixel
pixel_4int = pixel
if (pixel_4int < 0) pixel_4int = pixel_4int + 256
write(33) pixel_4int
end do
close(30)
close(33)
END PROGRAM
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, row, col, point_count
integer(kind=1) :: pixel
integer(kind=4) :: pixel_4int
integer :: i
open(UNIT=33, FILE='t10k-images.fortran', &
ACCESS='stream', FORM='unformatted', STATUS='replace')
open(UNIT=30, FILE='t10k-images.idx3-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count, row, col
magic_number = 2051
sample_count = 10000
row = 28
col = 28
point_count = row*col*sample_count
write(33) magic_number, sample_count, row, col
do i=1, point_count
read(30) pixel
pixel_4int = pixel
if (pixel_4int < 0) pixel_4int = pixel_4int + 256
write(33) pixel_4int
end do
close(30)
close(33)
END PROGRAM
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, point_count
integer(kind=1) :: label
integer(kind=4) :: label_4int
integer :: i
open(UNIT=33, FILE='train-labels.fortran', &
ACCESS='stream', FORM='unformatted', STATUS='replace')
open(UNIT=30, FILE='train-labels.idx1-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count
magic_number = 2049
sample_count = 60000
point_count = sample_count
write(33) magic_number, sample_count
do i=1, point_count
read(30) label
label_4int = label
if (label_4int < 0) label_4int = label_4int + 256
write(33) label_4int
end do
close(30)
close(33)
END PROGRAM
PROGRAM MAIN
implicit none
integer(kind=4) :: magic_number, sample_count, point_count
integer(kind=1) :: label
integer(kind=4) :: label_4int
integer :: i
open(UNIT=33, FILE='t10k-labels.fortran', &
ACCESS='stream', FORM='unformatted', STATUS='replace')
open(UNIT=30, FILE='t10k-labels.idx1-ubyte', ACCESS='stream', FORM='unformatted')
read(30) magic_number, sample_count
magic_number = 2049
sample_count = 10000
point_count = sample_count
write(33) magic_number, sample_count
do i=1, point_count
read(30) label
label_4int = label
if (label_4int < 0) label_4int = label_4int + 256
write(33) label_4int
end do
close(30)
close(33)
END PROGRAM
附录
多层神经网络,从零开始——(一)、Fortran读取MNIST数据集
多层神经网络,从零开始——(二)、Fortran随机生成“双月”分类问题数据
多层神经网络,从零开始——(三)、BP神经网络公式的详细推导
多层神经网络,从零开始——(四)、多层BP神经网络的矩阵形式
多层神经网络,从零开始——(五)、定义数据结构
多层神经网络,从零开始——(六)、激活函数
多层神经网络,从零开始——(七)、损失函数
多层神经网络,从零开始——(八)、分类问题中为什么使用交叉熵作为损失函数
多层神经网络,从零开始——(九)、优化函数
多层神经网络,从零开始——(十)、参数初始化
多层神经网络,从零开始——(十一)、实现训练类
多层神经网络,从零开始——(十二)、实现算例类
多层神经网络,从零开始——(十三)、关于并行计算的简单探讨