上一篇文章:深度学习2—任意结点数的三层全连接神经网络
距离上篇文章过去了快四个月了,真是时光飞逝,之前因为要考博所以耽误了更新,谁知道考完博后之前落下的接近半个学期的工作是如此之多,以至于弄到现在才算基本填完坑,实在是疲惫至极。
另外在这段期间,发现了一本非常好的神经网络入门书籍,本篇的很多细节问题本人就是在这本书上找到的答案,强烈推荐一下:
上篇文章介绍了如何实现一个任意结点数的三层全连接神经网络。本篇,我们将利用已经写好的代码,搭建一个输入层、隐含层、输出层分别为784、100、10的三层全连接神经网络来训练闻名已久的mnist手写数字字符集,然后自己手写一个数字来看看网络是否能比较给力的工作。
在正式做之前,还是按照惯例讲几个会用到的知识点。
- mnist数字字符集的结构解析,这个我单独写了一篇文章来做介绍了,如有需要了解请先移步:深度学习3番外篇—mnist数据集格式及转换
- 我们之前都是直接放入几个数作为输入,然后给网络几个数作为目标来训练网络的,而mnist手写字符集给我们的是一堆手写的28*28像素的图片还有图片对应的手写数字标签,我们怎么对它进行转换?
转换是这样的,我们把图片的所有像素当做输入,也就是28*28=784个像素直接作为输入,然后用0~9总共十个数作为输出目标的指引(当标签是“5”,则目标输出为0.01、0.01、0.01、0.01、0.01、0.99、0.01、0.01、0.01、0.01,依次类推)。
这里有一点比较有意思,为什么要用0.01而不是0,用0.99而不是1?
答案是我们用的激活函数永远不能输出0,1这两个数,因此如果取了这两个数则网络永远无法达到预期,会有训练过度的可能。
另外,细心的你可能也想到了,我们之前输入的数都是在0~1的范围内的,而像素的灰度取值范围在0~255,因此我们需要先对灰度值做一个归一化处理然后再放入网络中。
这里归一化处理的方式也比较有意思,假设X为输入,我们的处理公式如下:
为什么要乘0.99再加0.01?
答案是我们不希望输入取到0值,因为0有个小学生都知道的特点,任何数乘以它都等于0,因此无论输入层到隐含层的权值是多少,在输入等于0的时候都是一样的,这会影响权值的更新。 - 我们前面只确定了输入和输出层的节点个数,隐含层的节点个数还不知道,那我们怎么选取呢?答案可能让人难过,没有绝对正确的公式,只有几个经验公式(似乎有优化算法可以确定隐含层节点个数,后面如有需要开一篇专门讨论):
其中, 是隐含层节点数, 是输入层节点数, 是输出层节点数, 是 ~ 之间的常数
本篇取第三个,最后因为比较接近100,就直接取了100(怎么感觉好随意。。。)。 - 因为这次的输入节点有784个,算是比较多的,要十分注意在初始化网络参数的时候要避免参数与输入节点的积之和过大的情况出现。因为我们用的是sigmod函数作为激活函数,它的波形如下图所示:
可以看到,如果输入的数值过大或过小,波形会趋于平缓,也就是通常所说的“梯度消失”,我们要避免这种情况的出现。当然这也是用sigmod函数作为激活函数的问题。
那么究竟权值取多少合适呢,一般的做法是取-1~1中间的随机数,而数学家得到的经验规则告诉我们,可以在一个节点传入链接数量平方根倒数的大致范围内随机采样,初始化权值。以我们隐含层到输出层权重为例,输出层节点有100条传入链接,则其权重范围在 至 之间。 - 最后,我们要判断输出的是否准确,则先做前向传播,得到10个输出之后,找到最大的一个跟标签对比,如果相同则网络预测正确。
那么,原理介绍完了,我们先对下图所示的第一、二张图像和相应的标签进行训练,主要代码如下(整个工程的代码会在最后面给出):
for (size_t count = 0; count < 3000; count++)
{
mnet.forwardPropagation(mNumImg[0].inputdata);//前向传播
mnet.backPropagation(mNumImg[0].outputdata);//反向传播
mnet.forwardPropagation(mNumImg[1].inputdata);//前向传播
mnet.backPropagation(mNumImg[1].outputdata);//反向传播
}
mnet.forwardPropagation(mNumImg[0].inputdata);
mnet.printresual(0);//输出结果
mnet.forwardPropagation(mNumImg[1].inputdata);
mnet.printresual(0);
运行结果如下:
可以看到训练了3000次之后该网络可以分类之前输入的两个数字了(10个数字中最大的为预测结果,为了方便后面的正确率统计,一般会写一个函数将最大的数选出来和标签进行对比,看看网络的判断是不是正确的)。
可以看到,该网络基本能达到96%的准确率,而且还有上升的趋势,因为训练时间感人,所以这里就不再接着训练了,据网上查询到的结果,该网络基本精确率基本到96%多一些,不到97%就到头了。
C++实现
因为网络结构都没有改,改的只是各层节点的个数,前面提到的一些注意点,因此如果对整套代码有不明白的地方可以移步前两篇文章或看看本篇前面提到的5个注意点。
因为代码量变得比较多了,因此为了方便管理将工程分成了三个文件。注意,这里的”stdafx.h”为VS2017自建工程自带的文件,如果你使用的环境有所差别,请对相应的部分做修改。
net.hpp
#pragma once
#include "stdafx.h"
//结点类,用以构成网络
class node
{
public:
double value; //数值,存储结点最后的状态
double *W=NULL; //结点到下一层的权值
void initNode(int num);//初始化函数,必须调用以初始化权值个数
~node(); //析构函数,释放掉权值占用内存
};
void node::initNode(int num)
{
W = new double[num];
srand((unsigned)time(NULL));
for (size_t i = 0; i < num; i++)//给权值赋一个随机值
{
W[i] = rand() % 100/double(100)*0.1;
if (rand()%2)
{
W[i] = -W[i];
}
//cout << W[i] << endl;
}
}
node::~node()
{
if (W!=NULL)
{
delete[]W;
}
}
//网络类,描述神经网络的结构并实现前向传播以及后向传播
class net
{
public:
node inlayer[IPNNUM]; //输入层
node hidlayer[HDNNUM];//隐含层
node outlayer[OPNNUM];//输出层
double yita = 0.1;//学习率η
double k1;//输入层偏置项权重
double k2;//隐含层偏置项权重
double Tg[OPNNUM];//训练目标
double O[OPNNUM];//网络实际输出
net();//构造函数,用于初始化各层和偏置项权重
double sigmoid(double z);//激活函数
double getLoss();//损失函数,输入为目标值
void forwardPropagation(double *input);//前向传播,输入为输入层节点的值
void backPropagation(double *T);//反向传播,输入为目标输出值
void printresual(int trainingTimes);//打印信息
};
net::net()
{
//初始化输入层和隐含层偏置项权值,给一个随机值
srand((unsigned)time(NULL));
k1 = rand() % 100 / double(100);
k2 = rand() % 100 / double(100);
//初始化输入层到隐含层节点权重
for (size_t i = 0; i < IPNNUM; i++)
{
inlayer[i].initNode(HDNNUM);
}
//初始化隐含层到输出层节点权重
for (size_t i = 0; i < HDNNUM; i++)
{
hidlayer[i].initNode(OPNNUM);
}
}
//激活函数
double net::sigmoid(double z)
{
return 1 / (1 + exp(-z));
}
//损失函数
double net::getLoss()
{
double mloss = 0;
for (size_t i = 0; i < OPNNUM; i++)
{
mloss += pow(O[i] - Tg[i], 2);
}
return mloss / OPNNUM;
}
//前向传播
void net::forwardPropagation(double *input)
{
for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)//输入层节点赋值
{
inlayer[iNNum].value = input[iNNum];
}
for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)//算出隐含层结点的值
{
double z = 0;
for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)
{
z += inlayer[iNNum].value*inlayer[iNNum].W[hNNum];
}
z += k1;//加上偏置项
hidlayer[hNNum].value = sigmoid(z);
}
for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)//算出输出层结点的值
{
double z = 0;
for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)
{
z += hidlayer[hNNum].value*hidlayer[hNNum].W[oNNum];
}
z += k2;//加上偏置项
O[oNNum] = outlayer[oNNum].value = sigmoid(z);
}
}
//反向传播,这里为了公式好看一点多写了一些变量作为中间值
//计算过程用到的公式在博文中已经推导过了,如果代码没看明白请看看博文
void net::backPropagation(double *T)
{
for (size_t i = 0; i < OPNNUM; i++)
{
Tg[i] = T[i];
}
for (size_t iNNum = 0; iNNum < IPNNUM; iNNum++)//更新输入层权重
{
for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)
{
double y = hidlayer[hNNum].value;
double loss = 0;
for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
{
loss += (O[oNNum] - Tg[oNNum])*O[oNNum] * (1 - O[oNNum])*hidlayer[hNNum].W[oNNum];
}
inlayer[iNNum].W[hNNum] -= yita * loss*y*(1 - y)*inlayer[iNNum].value;
}
}
for (size_t hNNum = 0; hNNum < HDNNUM; hNNum++)//更新隐含层权重
{
for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
{
hidlayer[hNNum].W[oNNum] -= yita * (O[oNNum] - Tg[oNNum])*
O[oNNum] * (1 - O[oNNum])*hidlayer[hNNum].value;
}
}
}
void net::printresual(int trainingTimes)
{
double loss = getLoss();
cout << "训练次数:" << trainingTimes << endl;
cout << "loss:" << loss << endl;
for (size_t oNNum = 0; oNNum < OPNNUM; oNNum++)
{
cout << "输出" << oNNum + 1 << ":" << O[oNNum] << endl;
}
}
numImg.hpp
#pragma once
#include "stdafx.h"
class numImg
{
public:
Mat img;
uchar tag;
double inputdata[IPNNUM];//输入数据
double outputdata[OPNNUM];//输出数据
numImg();
};
numImg::numImg()
{
img= Mat(IMGWIDTH, IMGHEIGHT, CV_8UC1);
}
Demo.cpp
#include "stdafx.h"
#include "numImg.hpp"
#include "net.hpp"
using namespace cv;
numImg* mNumImg;//图像对象指针
int sumOfImg;
net mnet;//神经网络对象
void imgTrainDataRead()
{
/**********************************/
/***********读取图片数据***********/
/**********************************/
char savepath[30];//图像存储路径
uchar readbuf[4];//信息数据读取空间
FILE *f;
fopen_s(&f, "train-images.idx3-ubyte", "rb");
fread_s(readbuf, 4, 1, 4, f);//读取魔数,即文件标志位
fread_s(readbuf, 4, 1, 4, f);//读取数据集图像个数
sumOfImg = (readbuf[0] << 24) + (readbuf[1] << 16) + (readbuf[2] << 8) + readbuf[3];//图像个数
fread_s(readbuf, 4, 1, 4, f);//读取数据集图像行数
int imgheight = (readbuf[0] << 24) + (readbuf[1] << 16) + (readbuf[2] << 8) + readbuf[3];//图像行数
fread_s(readbuf, 4, 1, 4, f);//读取数据集图像列数
int imgwidth = (readbuf[0] << 24) + (readbuf[1] << 16) + (readbuf[2] << 8) + readbuf[3];//图像列数
int imgdatalen = imgheight * imgwidth;//图像数据长度
mNumImg = new numImg[sumOfImg];
for (int i = 0; i < sumOfImg; i++)
{
mNumImg[i].img = Mat(imgheight, imgwidth, CV_8UC1);
fread_s(mNumImg[i].img.data, imgdatalen, 1, imgdatalen, f);//读取数据集图像列数
for (size_t px = 0; px < IPNNUM; px++)//图像数据归一化
{
//mNumImg[i].inputdata[px] = mNumImg[i].img.data[px]/(double)255*0.99+0.01;
mNumImg[i].inputdata[px] = mNumImg[i].img.data[px] / (double)255;
}
}
fclose(f);
/**********************************/
/***********读取标签数据***********/
/**********************************/
fopen_s(&f, "train-labels.idx1-ubyte", "rb");
fread_s(readbuf, 4, 1, 4, f);//读取魔数,即文件标志位
fread_s(readbuf, 4, 1, 4, f);//读取数据集图像个数
sumOfImg = (readbuf[0] << 24) + (readbuf[1] << 16) + (readbuf[2] << 8) + readbuf[3];//图像个数
for (int i = 0; i < sumOfImg; i++)
{
fread_s(&mNumImg[i].tag, 1, 1, 1, f);//读取数据集图像列数
for (size_t j = 0; j < 10; j++)
{
mNumImg[i].outputdata[j] = 0.01;
}
mNumImg[i].outputdata[mNumImg[i].tag] = 0.99;
}
fclose(f);
}
void AccuracyRate(int time)//精确率评估
{
double tagright = 0;//正确个数统计
for (size_t count = 50000; count < 60000; count++)
{
mnet.forwardPropagation(mNumImg[count].inputdata);//前向传播
double value = -100;
int gettag = -100;
for (size_t i = 0; i < 10; i++)
{
if (mnet.outlayer[i].value>value)
{
value = mnet.outlayer[i].value;
gettag = i;
}
}
if (mNumImg[count].tag== gettag)
{
tagright++;
}
}
//mnet.printresual(0);//信息打印
cout << "第" << time+1<< "轮: ";
cout << "正确率为:" << tagright / 10000 << endl;
}
void main()
{
imgTrainDataRead();//取训练数据集的图像数据,完成之后mNumImg中存着图像数据和对应的标签
for (size_t j = 0; j < 10; j++)
{
for (size_t i = 0; i < 50000; i++)
{
mnet.forwardPropagation(mNumImg[i].inputdata);//前向传播
mnet.backPropagation(mNumImg[i].outputdata);//反向传播
}
AccuracyRate(j);
}
}
因为最近换了电脑,python和pytorch用到的环境还没去配,因此下面等配了再补充吧,哈哈。
python实现
pytorch的CPU实现
pytorch的GPU实现
另外写文章累人,写代码掉头发,如果觉得文章有帮助,哈哈哈