[神经网络]-BP算法推导
目前基本上所有的神经网络都使用反向传播来完成人工神经网络的训练过程。本文首先从神经网络结构开始,其次介绍正向传播的过程,最后对反向传播(BP)的过程进行推导。
文章目录
1. 人工神经网络
人工神经的功能和生物神经元的功能是相似的:一个神经元具有许多输入和一个输出,神经元可以根据输入值决定输出值输出。神经元相互将输出作为输入,形成层次结构,便形成了神经网络。对于神经网络,在输入端输入信号后,信号经过不同层次的神经元不断传递,最终在输出端完成输出。
1.1 阶跃函数(激活函数)
人工神经网络需要模拟生物神经网络的工作原理,即在收到大于阈值的信号后发射信号。在人工神经网络中,“信号”即神经网络的输入值,是否发射信号则由阶跃函数计算发射。最常用的阶跃函数为sigmoid函数,其表达形式如下:
y
=
1
1
+
e
−
x
y =\frac{1}{1+e^{-x}}
y=1+e−x1
对sigmoid函数进行求导,有:
y
′
=
e
−
x
(
1
+
e
−
x
)
2
=
1
+
e
−
x
−
1
(
1
+
e
−
x
)
2
=
1
1
+
e
−
x
−
1
(
1
+
e
−
x
)
2
=
y
(
1
−
y
)
y'=\frac{e^{-x}}{(1+e^{-x})^{2}} =\frac{1+e^{-x}-1}{(1+e^{-x})^{2}} =\frac{1}{1+e^{-x}}-\frac{1}{(1+e^{-x})^{2}} =y(1-y)
y′=(1+e−x)2e−x=(1+e−x)21+e−x−1=1+e−x1−(1+e−x)21=y(1−y)
根据sigmoid函数的表达式,可以画出sigmoid函数的图像:
由图像可以看出,sigmoid函数具有性质:
-
s
i
g
m
o
i
d
(
x
)
∈
(
0
,
1
)
sigmoid(x)∈(0,1)
sigmoid(x)∈(0,1)sigmoid函数的输出值介于0~1之间,x<0时神经元不发射信号,x>0时神经元发射信号。
2)其导数较为简单,易于运算。
因此,sigmoid函数是神经网络中经典的阶跃函数。另外,还有tanh,relu等几个常用阶跃函数。
1.2 感知器
1943年,McCullch and Pitts 将神经元总结为以下模型:
神经元N可以分为三部分:(1)输入部分x1…xn,(2)权值部分w1…wn,(3)求和函数
s
u
m
sum
sum
,(4)阶跃函数f(x)(本文使用sigmoid函数) ,(5)输出部分$\hat{y} $。下面对5个部分进行分述:
(1) 输入部分:一个神经元可以有多个输入,输入可来自于用户输入或其他神经元的输出。
(2) 权值部分:对于每一个输入 ,都有一个权值 与之对应。
(3) 求和函数:当感知器接收到输入时,使用下式对输入进行加权求和:
s
u
m
=
∑
i
=
1
n
x
i
w
i
sum=\sum_{i=1}^{n}x_{i}w_{i}
sum=i=1∑nxiwi
(4) 阶跃函数:进行求和后,求sigmoid(sum)得到激活值。
(5) 输出部分:令$\hat{y}=sigmoid(sum)
,
输
出
,输出
,输出\hat{y}$。
1.3 人工神经网络
神经网络一般由2-3层感知机组成,最常用的神经网络大多使用3层构造,且已经证明,当感知机达到3层时就可以拟合任意精度的函数了。下图是一个典型三层网络结构:
在上图所示的网络结构中,输入层X有m个节点 x 1 , x 2 . . . x m x_{1},x_{2}...x_{m} x1,x2...xm,隐含层H有n个节点 h 1 , h 2 . . . h n h_{1},h_{2}...h_{n} h1,h2...hn,输出层Y有p个节点 y 1 , y 2 . . . y n y_{1},y_{2}...y_{n} y1,y2...yn。一般情况下,输入层节点数为特征数,隐含层节点一般取2m+1个(另有其他取值方法),输出层节点数一般为分类数,2分类可取1个或2个。相邻两层之间使用全连接的方式进行连接,每条边成为一个权重边。图中syn0有m×n个数,即节点 x i x_{i} xi与H层中每一个节点都相连,是一个m×n的矩阵。同理,syn1是一个n×p的矩阵。
2. 正向传播过程
正向传播过程简述如下:
特征从输入层输入,经过加权求和后在隐含层得到求和的值,然后加权和经过阶跃函数得到隐含层的值,隐含层的值再经过加权求和向前传递到输出层,输出层将得到的加权和经过阶跃函数得到输出值,至此结束。
用数学公式进行描述:
1)首先将特征输入输入层X:
x
i
=
a
i
,
i
=
(
1
,
2
,
.
.
.
,
m
)
x_{i}=a_{i},i=(1,2,...,m)
xi=ai,i=(1,2,...,m)
其中
a
i
a_{i}
ai为第
i
i
i个特征。
2)输入层通过权重边将值传递至隐含层得到加权和:
n
e
t
h
j
=
∑
i
=
1
m
x
i
⋅
s
y
n
i
j
(
j
=
1
,
2
,
.
.
.
,
n
)
net_{hj}=\sum_{i=1}^{m}x_{i}\cdot syn_{ij}(j=1,2,...,n)
nethj=i=1∑mxi⋅synij(j=1,2,...,n)
其中
s
y
n
i
j
syn_{ij}
synij为将
x
i
x_{i}
xi与
h
j
h_{j}
hj相连的权重边。
3)求隐含层节点值:
h
j
=
s
i
g
m
o
i
d
(
n
e
t
h
j
)
(
j
=
1
,
2
,
.
.
.
,
n
)
h_{j}=sigmoid(net_{hj})(j=1,2,...,n)
hj=sigmoid(nethj)(j=1,2,...,n)
4)隐含层的值继续向前传播:
n
e
t
y
k
=
∑
j
=
1
n
h
j
⋅
s
y
n
j
k
(
k
=
1
,
2
,
.
.
.
,
p
)
net_{yk}=\sum_{j=1}^{n}h_{j}\cdot syn_{jk}(k=1,2,...,p)
netyk=j=1∑nhj⋅synjk(k=1,2,...,p)
5)经过阶跃函数,得到预测值
y
^
\hat{y}
y^
y
^
k
=
s
i
g
m
o
i
d
(
n
e
t
y
k
)
(
k
=
1
,
2
,
.
.
.
,
p
)
\hat{y}_{k}=sigmoid(net_{yk})(k=1,2,...,p)
y^k=sigmoid(netyk)(k=1,2,...,p)
至此完成了正向传播。
如果从矩阵的角度看正向传播,则1)~5)几个过程用公式表达如下:
1)
X
=
A
X=A
X=A
2)
n
e
t
H
=
X
⋅
s
y
n
0
net_{H}=X\cdot syn_{0}
netH=X⋅syn0(此处为点乘,注意X与syn的顺序)
3)
H
=
s
i
g
m
o
i
d
(
n
e
t
H
)
H=sigmoid(net_{H})
H=sigmoid(netH)
4)
n
e
t
y
^
=
H
⋅
s
y
n
1
net_{\hat{y}}=H\cdot syn_{1}
nety^=H⋅syn1
5)
y
^
=
s
i
g
m
o
i
d
(
n
e
t
y
^
)
\hat{y}=sigmoid(net_{\hat{y}})
y^=sigmoid(nety^)
以上公式在使用numpy编写代码时用得到。
3. 反向传播
本节介绍BP反向传播的运行原理与数学解释。首先将从代价公式入手,介绍如何评价当前神经网络的错误率,其次介绍反向传播算法的工作原理以及详细的推导过程。
3.1 代价公式
所谓代价公式是指用来评估神经网络的公式,定义如下:
J
=
−
y
log
(
y
^
)
−
(
1
−
y
)
l
o
g
(
1
−
y
^
)
J=-y\log(\hat{y})-(1-y)log(1-\hat{y})
J=−ylog(y^)−(1−y)log(1−y^)
其中y为正确结果, y ^ \hat{y} y^为神经网络预测结果,且y, y ^ \hat{y} y^∈[0,1]。绘制此函数可以的到如下的图像:
图中x,y轴分别代表y和 y ^ \hat{y} y^。由图可见,当y和 y ^ \hat{y} y^趋于一致时,代价函数的值趋于0,当y和 y ^ \hat{y} y^相背时,J趋于无穷大。因此,若能一句代价公式进行最小化,就能使预测值尽量靠近实际值。
3.2 反向传播过程
本小节将对一个简单的神经网络反向求解过程进行分析。将第2节中的神经网络简化为下图中的简单模型:
反向传播过程实际上就是通过对代价公式求偏导,求得并神经网络中边权值的最优值来降低代价J的值。通过第2节中正向传播的过程可以了解到,神经元内部的值(输入层,隐含层以及输出层)都是由输入特征以及边权值计算得来的。想要最小化代价J,输入的特征值不能改变,只能修改边权值了。利用梯度下降方法,有:
s
y
n
1
′
=
s
y
n
1
−
∂
J
∂
s
y
n
1
(
1
)
syn'_{1}=syn_{1}-\frac{\partial J}{\partial syn_{1}} (1)
syn1′=syn1−∂syn1∂J(1)
以及
s
y
n
0
′
=
s
y
n
0
−
∂
J
∂
s
y
n
0
(
2
)
syn'_{0}=syn_{0}-\frac{\partial J}{\partial syn_{0}}(2)
syn0′=syn0−∂syn0∂J(2)
其中
s
y
n
1
′
syn'_{1}
syn1′,
s
y
n
0
′
syn'_{0}
syn0′为更新后的边权值。
对(1)中的分式进行链式展开:
∂
J
∂
s
y
n
0
=
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
∂
n
e
t
Y
^
s
y
n
1
\frac{\partial J}{\partial syn_{0}}=\frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}\frac{\partial net_{\hat{Y}}}{syn_{1}}
∂syn0∂J=∂y^∂J∂netY^∂y^syn1∂netY^
对其中分式进行进一步求偏导有:
∂
J
∂
y
^
=
−
y
y
^
+
1
−
y
1
−
y
^
\frac{\partial J}{\partial \hat{y}}=-\frac{y}{\hat{y}}+\frac{1-y}{1-\hat{y}}
∂y^∂J=−y^y+1−y^1−y
∂
y
^
∂
n
e
t
Y
^
=
y
^
(
1
−
y
^
)
\frac{\partial \hat{y}}{\partial net_{\hat{Y}}}=\hat{y}(1-\hat{y})
∂netY^∂y^=y^(1−y^)
∂
n
e
t
Y
^
s
y
n
1
=
h
\frac{\partial net_{\hat{Y}}}{syn_{1}}=h
syn1∂netY^=h
最终简化后:
∂
J
∂
s
y
n
0
=
(
y
^
−
y
)
h
\frac{\partial J}{\partial syn_{0}}=(\hat{y}-y)h
∂syn0∂J=(y^−y)h
在简化过程中,可以发现:
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
=
y
^
−
y
\frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}=\hat{y}-y
∂y^∂J∂netY^∂y^=y^−y
因此我们定义:
δ
n
e
t
Y
=
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
=
y
^
−
y
\delta _{netY}=\frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}=\hat{y}-y
δnetY=∂y^∂J∂netY^∂y^=y^−y
此处
δ
n
e
t
Y
\delta _{netY}
δnetY可视为神经网络的预测误差。
对于式(2),类似式(1)进行化简:
∂
J
∂
s
y
n
0
=
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
∂
n
e
t
Y
^
h
h
∂
n
e
t
n
e
t
h
∂
n
e
t
n
e
t
h
s
y
n
0
\frac{\partial J}{\partial syn_{0}}=\frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}\frac{\partial net_{\hat{Y}}}{h}\frac{h}{\partial net_{net_{h}}}\frac{\partial net_{net_{h}}}{syn_{0}}
∂syn0∂J=∂y^∂J∂netY^∂y^h∂netY^∂netnethhsyn0∂netneth
其中:
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
=
δ
n
e
t
Y
\frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}=\delta _{netY}
∂y^∂J∂netY^∂y^=δnetY
∂ n e t Y ^ h = s y n 1 \frac{\partial net_{\hat{Y}}}{h}=syn_{1} h∂netY^=syn1
h ∂ n e t n e t h = h ( 1 − h ) \frac{h}{\partial net_{net_{h}}}=h(1-h) ∂netnethh=h(1−h)
∂
n
e
t
n
e
t
h
s
y
n
0
=
x
\frac{\partial net_{net_{h}}}{syn_{0}}= x
syn0∂netneth=x
同理,我们定义
δ
n
e
t
H
=
∂
J
∂
y
^
∂
y
^
∂
n
e
t
Y
^
∂
n
e
t
Y
^
h
h
∂
n
e
t
n
e
t
h
=
δ
n
e
t
Y
s
y
n
1
h
(
1
−
h
)
\delta _{netH} = \frac{\partial J}{\partial \hat{y}} \frac{\partial \hat{y}}{\partial net_{\hat{Y}}}\frac{\partial net_{\hat{Y}}}{h}\frac{h}{\partial net_{net_{h}}}=\delta _{netY}syn_{1}h(1-h)
δnetH=∂y^∂J∂netY^∂y^h∂netY^∂netnethh=δnetYsyn1h(1−h)
最终,使用求得的偏导对权重边进行修正:
s
y
n
1
′
=
s
y
n
1
−
η
∂
J
∂
s
y
n
1
=
s
y
n
1
−
η
(
y
−
y
^
)
h
syn'_{1} = syn_{1}- \eta \frac{\partial J}{\partial syn_{1}}=syn_{1}-\eta(y-\hat{y})h
syn1′=syn1−η∂syn1∂J=syn1−η(y−y^)h
s
y
n
0
′
=
s
y
n
0
−
η
∂
J
∂
s
y
n
0
=
s
y
n
0
−
η
δ
n
e
t
Y
s
y
n
1
h
(
1
−
h
)
x
syn'_{0} = syn_{0}- \eta \frac{\partial J}{\partial syn_{0}}=syn_{0}-\eta\delta _{netY}syn_{1}h(1-h)x
syn0′=syn0−η∂syn0∂J=syn0−ηδnetYsyn1h(1−h)x
其中,
η
\eta
η为学习率,通常
η
=
0.005
\eta=0.005
η=0.005。过高的学习率会导致梯度下降跨度过大导致不能求得最优值,过低的学习率会导致训练速度过慢。
4 参考代码
以下给出了BP的参考python代码,训练和测试使用的数据为pima数据,正确率在80%左右。
import numpy as np
import math
import random
np.random.seed(1)
#sigmoid函数
def sigmoid(x):
return 1 / (1 + np.exp(-x))
#sigmoid求导函数
def deriveSigmoid(x):
return x * (1 - x)
#读取数据,lines中包含特征,label为标签
def inputData(addr):
file = open(addr)
allLine = []
for line in file:
allLine.append(line)
lines = []
label = []
for line in allLine:
words = line.strip().split(',')
words.insert(0,1)
for i in range(len(words)):
words[i] = float(words[i])
lines.append(words[0:len(words) - 1])
label.append([words[-1]])
return lines,label
def InitalizeNetwork(inputDim,hidDim,outputDim):
syn0 = np.random.random((inputDim,hidDim)) * 2 - 1
syn1 = np.random.random((hidDim,outputDim)) * 2 - 1
return syn0,syn1
def NNTrain(syn0,syn1,data,label):
#正向传播
X = np.array(data)
H = sigmoid(X.dot(syn0))
Y = sigmoid(H.dot(syn1))
#反向传播
l2error = Y - label
l2delta = l2error
syn1delta = np.dot(np.array([H]).T,np.array([l2delta]))
l1error = np.dot(l2delta,syn1.T)
l1delta = l1error * deriveSigmoid(H)
syn0delta = np.dot(np.array([X]).T,np.array([l1delta]))
#学习率为0.001
syn0 = syn0 - syn0delta * 0.0005
syn1 = syn1 - syn1delta * 0.0005
return syn0,syn1,int(np.round(Y[0]) == label)
def test(syn0,syn1,line):
l0 = np.array(line)
l1 = sigmoid(l0.dot(syn0))
l2 = sigmoid(l1.dot(syn1))
return l2[0]
#读取数据,这里用的是pima数据,是一个2分类数据,有8个特征。
lines,label = inputData(r"G:\DataSets\Balance.. 46\diabetes.txt")
#设置输入层维度(8)
inputDim = len(lines[0])
#设置隐含层节点数
hidDim = (inputDim) * 6
#设置输出层节点数
outputDim = 1
#初始化边权值
syn0,syn1 = InitalizeNetwork(inputDim,hidDim,outputDim)
#开始训练(1000次)
for i in range(3000):
rightCount = 0.0
for j in range(len(lines)):
syn0,syn1,right = NNTrain(syn0,syn1,lines[j],label[j])
rightCount+=right
if(i % 100 == 0):
print(str(i) + "times train correct:" + str(rightCount / len(lines) * 100) + '%')
#开始测试
lines,label = inputData(r"G:\DataSets\Balance.. 46\diabetes.txt")
right = 0.0
for i in range(len(lines)):
predict = test(syn0,syn1,lines[i])
predict = np.round(predict)
if predict == label[i][0]:
right+=1
#输出结果
print("test right:" + str(right / len(lines) * 100) + '%')
print("test complete.")