前言
看了众多大佬写的文章、书籍,发现多数都是讲公式不讲算法,后来看了《深度学习》发现里面是纯讲算法,不讲公式,后来发现如果两者结合一起来理解,效果会更好。于是写此博文,加深自己理解的同时,希望能帮助有需要的人
前置知识
如果想要弄明白反向传播算法
,那么你得有一些微积分
的基础,以及知道什么Chain rule
,即链式法则
。链式法则主要用于对复合函数求导,比如有一个函数
z
=
f
(
x
)
z=f(x)
z=f(x)
和一个复合函数
u
=
h
(
z
)
u=h(z)
u=h(z)
那么根据链式法则,就有如下等式
d
u
d
x
=
d
u
d
z
d
z
d
x
\frac{du}{dx}=\frac{du}{dz}\frac{dz}{dx}
dxdu=dzdudxdz
如下图
如果我们想求
d
p
d
u
\frac{dp}{du}
dudp,那么直接从下面标识的路径逐一相乘即可,为
d
p
d
t
d
t
d
u
\frac{dp}{dt}\frac{dt}{du}
dtdpdudt
如果我们想要求
d
p
d
y
\frac{dp}{dy}
dydp,如下图,但有两条路径,我们需要对不同路径进行相加,即
d
p
d
y
=
d
p
d
t
(
d
t
d
u
d
u
d
y
+
d
t
d
v
d
v
d
y
)
\frac{dp}{dy}=\frac{dp}{dt}(\frac{dt}{du}\frac{du}{dy}+\frac{dt}{dv}\frac{dv}{dy})
dydp=dtdp(dudtdydu+dvdtdydv)
现在请记住上述规则,后续算法会用到。
反向传播
基本定义
对于一个人工神经网络,一共有 L L L层
a ( n ) a^{(n)} a(n)表示第 n n n层的输入
h ( n ) h^{(n)} h(n)表示第 n n n层的输出。
w ( n ) w^{(n)} w(n)表示第 n n n层的权重参数。
b ( n ) b^{(n)} b(n)表示第 n n n层神经元的偏置
算法解析
第一步,前向传播
先上前向传播
的伪代码(参考《深度学习》Ian Goodfellow / Yoshua Bengio所著)
下面来解析一下算法。所谓前向传播
,其实就是计算整个网络的输出值,现在假设有如下网络
因为对于输入层来说,输入层的输入等于输入层的输出,所以算法的一开始要置 h ( 0 ) = a ( 0 ) = x h^{(0)}=a^{(0)}=x h(0)=a(0)=x。
在整个循环中,逐步计算了网络中每层的输入和每次层的输出。
为了更好的理解,举个简单的例子。
隐藏层中的蓝色部分,为该层的输入,红色部分为该层的输出,为了节省时间,只画了一个神经元的连接,实际上这个网络的全连接的
假设输入数据为 X X X,是一个 4 × 100 4 \times100 4×100的矩阵,也就是说有100个数据,每个数据有4个元素。
对于隐藏层1来说,该层的权重为 W ( 1 ) W^{(1)} W(1),是一个 3 × 4 3\times4 3×4的矩阵,也就是说有3个神经单元,每个神经单元有4个参数。
对于隐藏层1来说,该层的偏置为 b ( 1 ) b^{(1)} b(1),是一个 3 × 100 3\times100 3×100的向量(向量的每个元素的初始值为1),也就是说有三个神经元,每个神经元的初始偏置为1。因为有100个数据,所以我们要对100个加权值进行偏置(注意,这里讲b设置为矩阵是为了让公式成立,实际上用python实现的话保存为向量就可以了)
所以,隐藏层1的输入为:
a
(
1
)
=
W
(
k
)
X
+
b
(
1
)
a^{(1)}=W^{(k)}X+b^{(1)}
a(1)=W(k)X+b(1)
所以
a
(
1
)
a^{(1)}
a(1)就是一个
3
×
100
3\times100
3×100的矩阵,这也就是隐含层1的输入
在假设隐含层1选择的激活函数为f(x),那么隐含层1的输出为
h
(
1
)
=
f
(
a
(
1
)
)
h^{(1)}=f(a^{(1)})
h(1)=f(a(1))
其输出也是一个
3
×
100
3\times100
3×100的矩阵。
然后我们再把隐含层1的输出当作隐含层2的输入,再进行上述过程计算,最终就会得到一个 2 × 100 2\times100 2×100的矩阵,这也就是整个网络的输出。
前向传播非常简单,除了矩阵知识,没有数学可言。算法的数学部分主要集中在反向传播
(
←
\leftarrow
←表示赋值 )
在这部分算法的最开始我们要计算的是损失函数关于整个网络输出的梯度,也就是损失函数关于第L层输出的梯度
▽
y
^
J
=
∂
J
∂
y
^
【
式
1
】
\bigtriangledown_{\hat{y}}J=\frac{\partial J}{\partial \hat{y}} 【式1】
▽y^J=∂y^∂J【式1】
这里你要清楚,在整个网络中,最容易求出来的就是1式了,后面我们要根据它去导出各层的权重参数的梯度。
完成上述工作之后,我们会进入一个循环,这个循环是从最后一层遍历到第一层,这也就是反向传播名字的由来了。
假如我们刚进入循环,此时K=L。
我们碰到的一个计算是如下公式,这个公式求的是损失函数关于输出层的输入的梯度,请反复度黑体字数遍。
g
←
▽
a
(
L
)
J
=
g
⨀
f
′
(
a
(
L
)
)
g\leftarrow \bigtriangledown_{a^{(L)}}J=g\bigodot f'(a^{(L)})
g←▽a(L)J=g⨀f′(a(L))
(
⨀
\bigodot
⨀ 表示矩阵对应元素相乘)
这个公式的推导很简单,如下(打公式太累,只能手写,但字难看…)
因为激活函数
f
f
f是我们实现设置的,所以它的导数我们是知道的,
g
g
g在算法的最开始的我们也已经求出来了,所以
▽
a
(
L
)
J
\bigtriangledown_{a^{(L)}}J
▽a(L)J也就自然而然的得到了。
上面步骤完成之后
g
g
g的值发生改变
g
=
▽
a
(
L
)
J
g=\bigtriangledown_{a^{(L)}}J
g=▽a(L)J
然后算法就要开始计算损失函数关于 b ( L ) b^{(L)} b(L)的梯度,也就是说要求损失函数关于最后一层神经元偏置的梯度。
非常巧妙的是
▽
b
(
L
)
J
=
g
\bigtriangledown_{b^{(L)}}J=g
▽b(L)J=g
推导过程如下
(解释一下
a
(
L
)
=
w
(
L
)
h
(
L
−
1
)
+
b
(
L
)
a^{(L)}=w^{(L)}h^{(L-1)}+b^{(L)}
a(L)=w(L)h(L−1)+b(L),这个公式在前向传播中已经求出来了)。
所以这里我们就得出一个结论:损失函数关于第x层的偏置的梯度等于损失函数关于第x层的输入的梯度
上述步骤完成之后,就是要计算损失函数关于第L层的权重参数的梯度,其计算公式如下:
▽
w
(
L
)
J
=
g
(
h
(
L
−
1
)
)
T
\bigtriangledown_{w^{(L)}}J=g(h^{(L-1)})^T
▽w(L)J=g(h(L−1))T
其推导过程如下:
(解释:这里之所以对
h
(
L
−
1
)
h^{(L-1)}
h(L−1)取转置是遵循了矩阵求导的公式,有兴趣的可以去了解一下矩阵求导)
所以,我们得出结论:损失函数
关于第x层的权重参数
的梯度等于损失函数
关于第x层输入
的梯度乘以第x-1层的输出
算法执行到这里,我们就已经求出了最后一层的权重参数的梯度和偏置的梯度
在这个循环体中,最后一行代码是为关键的,因为它关系着整个算法的迭代
g
=
(
w
(
L
)
)
T
g
=
▽
h
(
L
−
1
)
J
g=(w^{(L)})^Tg=\bigtriangledown_{h^{(L-1)}}J
g=(w(L))Tg=▽h(L−1)J
我们之所以要对g的值进行这样的迭代,是因为这样可以求出损失函数关于第
L
−
1
L-1
L−1层输出的梯度,求出了损失函数关于第
L
−
1
L-1
L−1层输出的梯度就可以求出损失函数关于第
L
−
1
L-1
L−1层输入的梯度,求出了损失函数关于第
L
−
1
L-1
L−1层输入的梯度就可以利用上面的两个结论求出第L-1层的权重参数的梯度和偏置的梯度。
反复进行上述迭代就可以求出整个网络的权重参数和偏置的梯度
至此还有最后一个公式没有推导,推导如下
(这里为什么
h
(
L
−
1
)
T
跑
到
g
的
前
面
去
了
,
这
里
也
是
因
为
我
们
求
导
是
对
矩
阵
求
导
,
也
就
是
求
某
个
函
数
的
J
a
c
o
b
i
矩
阵
h^{(L-1)^T}跑到g的前面去了,这里也是因为我们求导是对矩阵求导,也就是求某个函数的Jacobi矩阵
h(L−1)T跑到g的前面去了,这里也是因为我们求导是对矩阵求导,也就是求某个函数的Jacobi矩阵,想具体知道为什么的去学习一下矩阵求导即可)。
代码实现
def __backprop__(self,x_t,y_t):
# 首先进行前向传播,并且记录下每层的输出值和输入值
x_temp = x_t
# 输入数据
a = [x_temp]
# 输出数据
h = [x_temp]
# 对神经网络进行前向传播
for weight,b ,i in zip(self.w,self.biases,np.array([i for i in range(0,self.deep-1)])): # 这里的self.deep指的是网络深度
a.append(np.dot(weight,h[i-1])+b)
h.append(self.ACF(a)) # 这里的ACF指的是激活函数
net_out = h[-1]
g = self.DcostFunc(net_out,y_t) # 这里的self.DcsotFunc指的是损失函数的梯度
cur = self.deep #
res_db = [] # 待返回的结果
res_dw = [] # 待返回的结果
while cur >= 1:
g = g*self.ACF(a[cur])
res_db.insert(0,g)
res_dw.insert(0,np.dot(g,h[cur-1]))
cur=cur-1
return res_dw,res_db
注意,上面所有过程都没有加入正则项,如果加入正则项的话,整个推导过程如下:
关于正则化项
假设没有加入正则项的损失函数为
L
(
w
)
L(w)
L(w),那么加入了正则项的损失函数为(这里为2范数正则项):
μ
(
w
)
=
L
(
w
)
+
λ
2
∑
n
=
0
L
∑
i
=
0
∑
j
=
0
(
w
i
j
(
n
)
)
2
\mu(w)=L(w)+\frac{\lambda }{2}\sum_{n=0}^{L}\sum_{i=0} \sum_{j=0} (w^{(n)}_{ij})^2
μ(w)=L(w)+2λn=0∑Li=0∑j=0∑(wij(n))2
那么
∂
μ
w
m
n
(
n
)
=
∂
L
w
m
n
(
n
)
+
∂
(
λ
2
∑
n
=
0
L
∑
i
=
0
∑
j
=
0
(
w
i
j
(
n
)
)
2
)
)
∂
w
m
n
(
n
)
\frac{\partial \mu}{w^{(n)}_{mn}}=\frac{\partial L}{w^{(n)}_{mn}}+\frac{\partial(\frac{\lambda }{2}\sum_{n=0}^{L}\sum_{i=0} \sum_{j=0} (w^{(n)}_{ij})^2))}{\partial w^{(n)}_{mn}}
wmn(n)∂μ=wmn(n)∂L+∂wmn(n)∂(2λ∑n=0L∑i=0∑j=0(wij(n))2))
化简之后
∂
μ
w
m
n
(
n
)
=
∂
L
w
m
n
(
n
)
+
λ
w
m
n
(
n
)
\frac{\partial \mu}{w^{(n)}_{mn}}=\frac{\partial L}{w^{(n)}_{mn}}+\lambda w^{(n)}_{mn}
wmn(n)∂μ=wmn(n)∂L+λwmn(n)
所以,如果我要对某一层进行求梯度的话,那么就是
∂
μ
w
(
n
)
=
∂
L
w
(
n
)
+
λ
w
(
n
)
\frac{\partial \mu}{w^{(n)}}=\frac{\partial L}{w^{(n)}}+\lambda w^{(n)}
w(n)∂μ=w(n)∂L+λw(n)
同理,对偏置也是如此
∂
μ
w
(
n
)
=
∂
L
b
(
n
)
+
λ
b
(
n
)
\frac{\partial \mu}{w^{(n)}}=\frac{\partial L}{b^{(n)}}+\lambda b^{(n)}
w(n)∂μ=b(n)∂L+λb(n)
至此,关于反向传播算法就全部解析完毕了。