上一篇文章:深度学习1—最简单的全连接神经网络 
  我们完成了一个三层(输入+隐含+输出)且每层都具有两个节点的全连接神经网络的原理分析和代码编写。本篇文章将进一步探讨如何把每层固定的两个节点变成任意个节点,以方便我们下一篇文章用本篇文章完成的网络来训练手写字符集“mnist”。
  对于前向传播,基本上没有什么变化,就不用说了。主要看看后向传播的梯度下降公式。先放上上篇文章的网络图。
在这里插入图片描述
  上篇文章我们推知,含有两个节点的隐含层到输出层的权值对误差的偏导数,公式如下:
 
$$
 \frac{\partial E_{总}}{\partial W_{11}^{(2)}}=(O_{1}-T_{1})*O_{1}(1-O_{1})*Y_{1}
$$
  而含有两个节点的输入层到隐含层的权值对于误差梯度的偏导数公式如下:
  
$$
\frac{\partial E_{总}}{\partial W_{11}^{(1)}}=((O_{1}-T_{1})*O_{1}(1-O_{1})*W_{11}^{(2)}+(O_{2}-T_{2})*O_{2}(1-O_{2})*W_{21}^{(2)})*Y_{1}(1-Y_{1})*X_{1}
$$
  现在我们来总结规律,先看第一道公式:
  
$$
W_{11}^{(2)}
$$
位于输出层第一个结点的后方,与隐含层第一个结点相连接,其对总误差求偏导的式子结果也只与这两个节点的相关参数有关,与输入层、隐含层的第二个节点、输出层的第二个节点均无关。因此,也可以说,无论各层的节点数怎么样变化,隐含层到输出层的权值只与它连接的两个节点的参数相关。上式可以写成:
  
$$
\frac{\partial E_{总}}{\partial W_{xy}^{(2)}}=(O_{x}-T_{x})*O_{x}(1-O_{x})*Y_{y}
$$
  再看第二道公式:
  
$$
W_{11}^{(1)}
$$
位于隐含层第一个节点的后方,与输入层第一个节点相连接,第一个隐含层节点又与输出层所有节点相连接。公式中出现的项与上面描述到的节点相关,因此如果输出层节点增加,则表达式需要增加对增加节点的计算。而隐含层和输入层增加则无所谓。因此表达式可以写成:
  
$$
\frac{\partial E_{总}}{\partial W_{xy}^{(1)}}=[\sum_{k=1}^{n}(O_{k}-T_{k})*O_{k}(1-O_{k})*W_{kx}^{(2)}]*Y_{x}(1-Y_{x})*X_{y}
$$
  大功告成!规律总结完了,下面就只剩下写代码了。不得不承认,这一篇原理部分蛮少的,应该只能算是上一篇文章的补充。但从固定到任意还是挺重要的,因此独立出来写。
  下面我们用代码来实现一个任意节点数(代码中取的输入层、隐含层、输出层分别为5、10、5个节点,当然你高兴的话可以随意改动)的三层全连接神经网络。

C++实现


直接上代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#include <iostream>
#include <cmath>
#include<ctime>
using namespace std;

#define IPNNUM 5 //输入层节点数
#define HDNNUM 10 //隐含层节点数
#define OPNNUM 5 //输出层节点数

//结点类,用以构成网络
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;
}
}

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;
}
}

void main()
{
net mnet;
double minput[IPNNUM] = { 0.1, 0.2, 0.3, 0.4, 0.5 };
double mtarget[IPNNUM] = { 0.2, 0.4, 0.6, 0.8, 1 };
for (size_t i = 0; i < 10000; i++)
{
mnet.forwardPropagation(minput);//前向传播
mnet.backPropagation(mtarget);//反向传播
if (i%1000==0)
{
mnet.printresual(i);//信息打印
}
}
}

python实现


直接上代码

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import math
import random
import numpy as np

IPNNUM=5 #输入层节点数
HDNNUM=10 #隐含层节点数
OPNNUM=3 #输出层节点数

class node:
#结点类,用以构成网络
def __init__(self,connectNum=0):
self.value=0 #数值,存储结点最后的状态,对应到文章示例为X1,Y1等值
self.W = (2*np.random.random_sample(connectNum)-1)*0.01

class net:
#网络类,描述神经网络的结构并实现前向传播以及后向传播
def __init__(self):
#初始化函数,用于初始化各层间节点和偏置项权重
#输入层结点
self.inlayer=[node(HDNNUM)];
for obj in range(1, IPNNUM):
self.inlayer.append(node(HDNNUM))
#隐含层结点
self.hidlayer=[node(OPNNUM)];
for obj in range(1, HDNNUM):
self.hidlayer.append(node(OPNNUM))
#输出层结点
self.outlayer=[node(0)];
for obj in range(1, OPNNUM):
self.outlayer=[node(0)]

self.yita = 0.1 #学习率η
self.k1=random.random() #输入层偏置项权重
self.k2=random.random() #隐含层偏置项权重
self.Tg=np.zeros(OPNNUM) #训练目标
self.O=np.zeros(OPNNUM) #网络实际输出

def sigmoid(self,z):
#激活函数
return 1 / (1 + math.exp(-z))

def getLoss(self):
#损失函数
loss=0
for num in range(0, OPNNUM):
loss+=pow(self.O[num] -self.Tg[num],2)
return loss/OPNNUM

def forwardPropagation(self,input):
#前向传播
for i in range(0, IPNNUM):
#输入层节点赋值
self.inlayer[i].value = input[i]
for hNNum in range(0,HDNNUM):
#算出隐含层结点的值
z = 0
for iNNum in range(0,IPNNUM):
z+=self.inlayer[iNNum].value*self.inlayer[iNNum].W[hNNum]
#加上偏置项
z+= self.k1
self.hidlayer[hNNum].value = self.sigmoid(z)
for oNNum in range(0,OPNNUM):
#算出输出层结点的值
z = 0
for hNNum in range(0,HDNNUM):
z += self.hidlayer[hNNum].value* self.hidlayer[hNNum].W[oNNum]
z += self.k2
self.O[oNNum] = self.sigmoid(z)

def backPropagation(self,T):
#反向传播,这里为了公式好看一点多写了一些变量作为中间值
for num in range(0, OPNNUM):
self.Tg[num] = T[num]
for iNNum in range(0,IPNNUM):
#更新输入层权重
for hNNum in range(0,HDNNUM):
y = self.hidlayer[hNNum].value
loss = 0
for oNNum in range(0, OPNNUM):
loss+=(self.O[oNNum] - self.Tg[oNNum])*self.O[oNNum] * (1 - self.O[oNNum])*self.hidlayer[hNNum].W[oNNum]
self.inlayer[iNNum].W[hNNum] -= self.yita*loss*y*(1- y)*self.inlayer[iNNum].value
for hNNum in range(0,HDNNUM):
#更新隐含层权重
for oNNum in range(0,OPNNUM):
self.hidlayer[hNNum].W[oNNum]-= self.yita*(self.O[oNNum] - self.Tg[oNNum])*self.O[oNNum]*\
(1- self.O[oNNum])*self.hidlayer[hNNum].value

def printresual(self,trainingTimes):
#信息打印
loss = self.getLoss()
print("训练次数:", trainingTimes)
print("loss",loss)
for oNNum in range(0,OPNNUM):
print("输出",oNNum,":",self.O[oNNum])

#主程序
mnet=net()
input=np.array([0.1,0.2,0.3,0.4,0.5])
target=np.array([0.1,0.4,0.5])

for n in range(0,1000):
mnet.forwardPropagation(input)
mnet.backPropagation(target)
if (n%200==0):
mnet.printresual(n)

pytorch的CPU实现


其实pytorch本身就已经支持任意节点数,所以这里只是随意的改了下上一篇文章中代码的参数部分。

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
# coding=UTF-8
import time
import torch
import torch.nn as nn
from torch.autograd import Variable

class Net(nn.Module):
def __init__(self):
#定义Net的初始化函数,这个函数定义了该神经网络的基本结构
super(Net, self).__init__() #复制并使用Net的父类的初始化方法,即先运行nn.Module的初始化函数
self.intohid_layer = nn.Linear(5, 10) #定义输入层到隐含层的连结关系函数
self.hidtoout_layer = nn.Linear(10, 5)#定义隐含层到输出层的连结关系函数

def forward(self, input):
#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成
x = torch.nn.functional.sigmoid(self.intohid_layer(input)) #输入input在输入层经过经过加权和与激活函数后到达隐含层
x = torch.nn.functional.sigmoid(self.hidtoout_layer(x)) #类似上面
return x

mnet = Net()
target=Variable(torch.FloatTensor([0.2, 0.4, 0.6, 0.8, 1])) #目标输出
input=Variable(torch.FloatTensor([0.1, 0.2, 0.3, 0.4, 0.5])) #输入

loss_fn = torch.nn.MSELoss() #损失函数定义,可修改
optimizer = torch.optim.SGD(mnet.parameters(), lr=0.5, momentum=0.9);

start = time.time()

for t in range(0,5000):
optimizer.zero_grad() #清空节点值
out=mnet(input) #前向传播
loss = loss_fn(out,target) #损失计算
loss.backward() #后向传播
optimizer.step() #更新权值
if (t%1000==0):
print(out)

end = time.time()
print(end - start)

pytorch的GPU实现


直接上代码

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
# coding=UTF-8
import time
import torch
import torch.nn as nn
from torch.autograd import Variable

class Net(nn.Module):
def __init__(self):
#定义Net的初始化函数,这个函数定义了该神经网络的基本结构
super(Net, self).__init__() #复制并使用Net的父类的初始化方法,即先运行nn.Module的初始化函数
self.intohid_layer = nn.Linear(5, 10) #定义输入层到隐含层的连结关系函数
self.hidtoout_layer = nn.Linear(10, 5)#定义隐含层到输出层的连结关系函数

def forward(self, input):
#定义该神经网络的向前传播函数,该函数必须定义,一旦定义成功,向后传播函数也会自动生成
x = torch.nn.functional.sigmoid(self.intohid_layer(input)) #输入input在输入层经过经过加权和与激活函数后到达隐含层
x = torch.nn.functional.sigmoid(self.hidtoout_layer(x)) #类似上面
return x

mnet = Net().cuda()
target=Variable(torch.cuda.FloatTensor([0.2, 0.4, 0.6, 0.8, 1])) #目标输出
input=Variable(torch.cuda.FloatTensor([0.1, 0.2, 0.3, 0.4, 0.5])) #输入

loss_fn = torch.nn.MSELoss() #损失函数定义,可修改
optimizer = torch.optim.SGD(mnet.parameters(), lr=0.5, momentum=0.9);

start = time.time()

for t in range(0,5000):
optimizer.zero_grad() #清空节点值
out=mnet(input) #前向传播
loss = loss_fn(out,target) #损失计算
loss.backward() #后向传播
optimizer.step() #更新权值
if (t%1000==0):
print(out)

end = time.time()
print(end - start)

到此就用代码实现了任意结点数的三层全连接神经网络,代码运行的结果都是对的,但相对来说pytorch收敛的快一些,可能跟其默认初始化的参数有关(自己写的代码都是用随机数初始化的)。
  下一篇文章我们将用一个输入层、隐含层、输出层分别为784、100、10的三层全连接神经网络来训练闻名已久的MNIST手写数字字符集,然后自己手写一个数字来看看网络是否能比较给力的工作。
  最后再预告下篇文章,深度学习3—用三层全连接神经网络训练MNIST手写数字字符集
  另外写文章累人,写代码掉头发,如果觉得文章有帮助,哈哈哈