并行化实现基于BP神经网络的手写体数字识别
手写体数字识别可以堪称是神经网络学习的“Hello World” ,我今天要说的是如何实现BP神经网络算法的并行化,我们仍然是以手写体数字识别为例,会给出实现原理与不同参数的实例分析。
并行的实现是基于MPICH与OpenMP两种,运行环境是Linux 。
环境搭建
MPI
MPI的环境搭建说简单也简单,说难也难,优快云上有各种教程,大部分都是对的,我只提一下我在搭建环境的时候遇到的问题以及要注意的地方。
1.如果只在一台机器上跑代码的话,就相对简单一些,只需要下载压缩包,解压,编译安装就好了,这里要注意的是安装的时候,MPI依赖于gcc、g++、Fortran等编译工具,我们大多数人的机器上应该只有C/C++的编译工具,如果我们不打算在Fortran上使用MPI的话,可以选择在安装的时候禁用掉Fortran,好像是在安装命令的末尾加上–disable Fortran就可以了。
2.如果是想搭集群,首先要把MPI安装在相同的目录例如:/usr/local/mpich,然后还要注意要使用相同的用户名进行ssh免密登录的配置。这些东西都有很完整的教程,我在这里就不进行详细说了。
OpenMP
如果你的编译链工具版本够新的话,编译OpenMP只需要加上一句-fopenmp即可
BP神经网络算法基础
1. 算法框架
BP神经网络的过程主要分为两个阶段,第一阶段是信号的前向传播,从输入层经过隐含层,最后到达输出层;第二阶段是误差的反向传播,从输出层到隐含层,最后到输入层,依次调节隐含层到输出层的权重和偏置,输入层到隐含层的权重和偏置。
2.样本训练
正向传播
对每一层遍历每一个神经细胞,做如下操作:
- 获取第n个神经细胞的输入权重数组
- 遍历输入权重数组每一个输入权重,累加该权重和相应输入的乘积
- 将累加后的值通过激活函数,得到当前神经细胞的最终输出
- 该输出作为下一层的输入,对下一层重复上述操作,直到输出层输出为止
激活函数: sigmoid(S型)函数
反向训练
1.首先输入期望输出,同输出层的输出进行计算得到输出误差数组
2.然后对包括输出层的每一层: 遍历当前层的神经细胞,得到该神经细胞的输出,同时利用反向传播激活函数计算反向传播回来的误差, 进行调整权重矩阵。
激活函数: sigmoid(S型)函数的导函数
算法参考博客:
https://blog.youkuaiyun.com/xuanwolanxue/article/details/71565934
https://blog.youkuaiyun.com/qq_41645895/article/details/85265148
https://blog.youkuaiyun.com/u014303046/article/details/7820001
3. 数据处理
1. 训练数据集
简介
样本来源于美国提供的MNIST数据集一共包含7万个样本
数据集包含四个二进制文件:
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个样本
文件的格式如下
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
数据来源:http://yann.lecun.com/exdb/mnist/
读取数据
数据集的读取单独设置一个类,每次读数据传进一个index参数,index表示要读取文件中第几张图片,这就需要用到C++文件流的seekg()函数来索引到正确的位置,seekg()函数有两种形式的重载,我们采用的单参数重载,参数是相对于文件初始的偏移量。
偏移量 = 文件头大小 + (index - 1) * 图片大小
bool dataLoader::readIndex(int* label, int pos) {
if (mLabelFile.is_open() && !mLabelFile.eof()) {
mLabelFile.seekg(mLableStartPos + pos*mLabelLen);
mLabelFile.read((char*)label, mLabelLen);
return mLabelFile.gcount() == mLabelLen;
}
return false;
}
bool dataLoader::readImage(char imageBuf[], int pos) {
if (mImageFile.is_open() && !mImageFile.eof()) {
mImageFile.seekg(mImageStartPos + pos*mImageLen);
mImageFile.read(imageBuf, mImageLen);
return mImageFile.gcount() == mImageLen;
}
return false;
}
//label 用于保存标签 imagebuf保存图片像素 pos表示需要读取数据集中第几张图片
bool dataLoader::read(int* label, char imageBuf[], int pos) {
if (readIndex(label, pos)) {
return readImage(imageBuf, pos);
}
return false;
}
2. 算法相关参数声明
样本参数:
每个图片样本从二进制文件中读取的格式是unsigned char [28 * 28] (图片大小为28 * 28)
然后对样本的像素矩阵进行归一化处理,转成double数组:像素值大于128置1否则置0
inline void preProcessInputData(const unsigned char src[], double out[], int size) {
for (int i = 0; i < size; i++) {
out[i] = (src[i] >= 128) ? 1.0 : 0.0;
}
}
权重参数:
神经网络的每一层都有一个二维的权重数组,在隐藏层每一个神经细胞都会有28*28个输入,每个细胞对应一个输出,所以输出层的每个神经细胞对应有隐藏层细胞总数个的输入。权重的初始化是由随机数生成。
// 随机整数数[x, y]
inline int RandInt(int x, int y)
{
return rand() % (y - x + 1) + x;
}
// 随机浮点数(0, 1)
inline double RandFloat()
{
return (rand()) / (RAND_MAX + 1.0);
}
// 随机布尔值
inline bool RandBool()
{
return RandInt(0