1.符号说明
首先先看到神经网络的模型例子:
接下来的推导过程我们会结合这个例子进行说明。
先介绍一下上图中一些符号的含义:
(1)给定训练集
D
=
{
(
x
1
,
y
1
)
,
(
x
2
,
y
2
)
,
.
.
.
,
(
x
m
,
y
m
)
}
D={(x_1,y_1),(x_2,y_2), ...,(x_m,y_m)}
D={(x1,y1),(x2,y2),...,(xm,ym)},m为训练集的大小。
(2)
l
l
l表示第
l
l
l层,
n
l
n_l
nl表示第
l
l
l层神经元的个数(不包括偏置单元),总的层数用
L
L
L表示,即输出层就是第
L
L
L层。
(3)
W
[
l
]
W^{[l]}
W[l]表示第
l
−
1
l-1
l−1层到
l
l
l层的权重矩阵,维度为
n
l
×
n
l
−
1
n_{l}\times n_{l-1}
nl×nl−1。
(4)
b
[
l
]
=
(
b
1
[
l
]
b
2
[
l
]
⋮
b
n
l
[
l
]
)
b^{[l]}=\begin{pmatrix}b_1^{[l]}\\b_2^{[l]}\\\vdots\\b_{n_l}^{[l]}\end{pmatrix}
b[l]=⎝⎜⎜⎜⎜⎛b1[l]b2[l]⋮bnl[l]⎠⎟⎟⎟⎟⎞表示第
l
−
1
l-1
l−1层到
l
l
l层的偏置,维度为
n
l
×
1
n_{l}\times 1
nl×1。
(4)
z
[
l
]
=
(
z
1
[
l
]
z
2
[
l
]
⋮
z
n
l
[
l
]
)
z^{[l]}=\begin{pmatrix}z_1^{[l]}\\z_2^{[l]}\\\vdots\\z_{n_l}^{[l]}\end{pmatrix}
z[l]=⎝⎜⎜⎜⎜⎛z1[l]z2[l]⋮znl[l]⎠⎟⎟⎟⎟⎞表示第
l
l
l层的输入,维度为
n
l
×
1
n_{l}\times 1
nl×1,
z
[
l
]
=
W
[
l
]
X
+
b
[
l
]
z^{[l]}=W^{[l]}X+b^{[l]}
z[l]=W[l]X+b[l]。
(5)
a
[
l
]
=
(
a
1
[
l
]
a
2
[
l
]
⋮
a
n
l
[
l
]
)
a^{[l]}=\begin{pmatrix}a_1^{[l]}\\a_2^{[l]}\\\vdots\\a_{n_l}^{[l]}\end{pmatrix}
a[l]=⎝⎜⎜⎜⎜⎛a1[l]a2[l]⋮anl[l]⎠⎟⎟⎟⎟⎞表示第
l
l
l层的激活项(输出),维度为
n
l
×
1
n_{l}\times 1
nl×1,
a
[
l
]
=
g
(
Z
[
l
]
)
a^{[l]}=g(Z^{[l]})
a[l]=g(Z[l]),g为激活函数(每层可以用不同的激活函数,例如隐藏层可以用
t
a
n
h
tanh
tanh,输出层可以用
s
i
g
m
o
i
d
sigmoid
sigmoid)。
特别说明:上面方括号[]内的内容表示第几层,上标圆括号()内的内容表示第几个样本。
2.误差反向传播
2.1代价函数
对于网络在样本
(
x
i
,
y
i
)
(x_i,y_i)
(xi,yi)上的均方误差为:
E
(
i
)
=
1
2
∑
k
=
1
n
L
(
y
^
k
(
i
)
−
y
k
(
i
)
)
2
E^{(i)}=\frac{1}{2}\sum_{k=1}^{n_L}(\hat y_k^{(i)}-y_k^{(i)})^2
E(i)=21k=1∑nL(y^k(i)−yk(i))2
其中,
y
^
k
(
i
)
\hat y_k^{(i)}
y^k(i)是预测值,
y
k
(
i
)
y_k^{(i)}
yk(i)表示的是真实值,
n
L
n_L
nL是输出层的维度,
1
2
\frac{1}{2}
21只是为了后续求导的便利。
因此总的代价可以表示为:
E
=
1
m
∑
i
=
1
m
E
(
i
)
E=\frac{1}{m}\sum_{i=1}^{m}E^{(i)}
E=m1i=1∑mE(i)
我们的目标就是找到权重w和b使总的代价最小,实现这一目的的方法之一就是梯度下降算法,使用梯度下降算法来最小化代价函数,其更新过程如下:
W
[
l
]
=
W
[
l
]
−
α
∂
E
∂
W
[
l
]
=
W
[
l
]
−
α
∑
i
=
1
m
∂
E
(
i
)
∂
W
[
l
]
W^{[l]}=W^{[l]}-\alpha \frac{∂ E}{∂ W^{[l]}}\\=W^{[l]}-\alpha\sum_{i=1}^m\frac{∂ E^{(i)}}{∂ W^{[l]}}
W[l]=W[l]−α∂W[l]∂E=W[l]−αi=1∑m∂W[l]∂E(i)
b
[
l
]
=
b
[
l
]
−
α
∂
E
∂
b
[
l
]
=
b
[
l
]
−
α
∑
i
=
1
m
∂
E
(
i
)
∂
b
[
l
]
b^{[l]}=b^{[l]}-\alpha \frac{∂ E}{∂ b^{[l]}}\\=b^{[l]}-\alpha\sum_{i=1}^m \frac{∂ E^{(i)}}{∂b^{[l]}}
b[l]=b[l]−α∂b[l]∂E=b[l]−αi=1∑m∂b[l]∂E(i)
其中,
α
\alpha
α为学习率。
不难看出,我们的目标就是要求出
δ
E
δ
W
[
l
]
\frac{\delta E}{\delta W^{[l]}}
δW[l]δE和
δ
E
δ
b
[
l
]
\frac{\delta E}{\delta b^{[l]}}
δb[l]δE,因为E是总样本的代价,所以有
∂
E
∂
W
[
l
]
=
∑
i
=
1
m
∂
E
(
i
)
∂
W
[
l
]
\frac{∂ E}{∂ W^{[l]}} =\sum_{i=1}^m\frac{∂ E^{(i)}}{∂ W^{[l]}}
∂W[l]∂E=∑i=1m∂W[l]∂E(i),
∂
E
∂
b
[
l
]
=
∑
i
=
1
m
∂
E
(
i
)
∂
b
[
l
]
\frac{∂ E}{∂ b^{[l]}} =\sum_{i=1}^m\frac{∂ E^{(i)}}{∂ b^{[l]}}
∂b[l]∂E=∑i=1m∂b[l]∂E(i),这里我们为了简便可以先求出一个样本的情况,用E代替
E
(
i
)
E^{(i)}
E(i)表示一个样本的代价,后面的E都是表示的一个样本。
2.2 引入误差 δ \delta δ
因为E是通过样本i逐层前向传播所求得的,对于上面2.1中的代价函数中的预测值
y
^
k
(
i
)
\hat y_k^{(i)}
y^k(i)就是
a
[
L
]
a^{[L]}
a[L],也就是输出层的激活项,而
a
[
L
]
a^{[L]}
a[L]通过激活函数
g
(
z
[
L
]
)
g(z^{[L]})
g(z[L])求得,
z
[
L
]
z^{[L]}
z[L]就是通过前一层的激活项
a
[
L
−
1
]
a^{[L-1]}
a[L−1]求得,因此通过利用链式求导法则,对于任一
l
l
l层权重的偏导可表示为如下形式:
∂
E
∂
w
j
k
[
l
]
=
∂
E
∂
z
j
[
l
]
∂
z
j
[
l
]
∂
w
j
k
[
l
]
(
1
)
\frac{∂ E}{∂ w^{[l]}_{jk}}=\frac{∂ E}{∂ z^{[l]}_j} \frac{∂ z^{[l]}_j}{∂ w^{[l]}_{jk}} \qquad (1)
∂wjk[l]∂E=∂zj[l]∂E∂wjk[l]∂zj[l](1)
其中,
w
j
k
[
l
]
w_{jk}^{[l]}
wjk[l]是权重矩阵
W
[
l
]
W^{[l]}
W[l]第j行k列的权重值。又因为(
W
[
l
]
W^{[l]}
W[l]维度为
n
l
×
n
l
−
1
n_{l}\times n_{l-1}
nl×nl−1),
z
j
[
l
]
=
w
j
1
[
l
]
a
1
[
l
−
1
]
+
w
j
2
[
l
]
a
2
[
l
−
1
]
+
.
.
.
w
j
k
[
l
]
a
k
[
l
−
1
]
.
.
.
+
w
j
n
l
−
1
[
l
]
a
n
l
−
1
[
l
−
1
]
+
b
j
[
l
]
z^{[l]}_j=w^{[l]}_{j1}a^{[l-1]}_1+w^{[l]}_{j2}a^{[l-1]}_2+...w^{[l]}_{jk}a^{[l-1]}_k...+w^{[l]}_{jn_{l-1}}a^{[l-1]}_{n_{l-1}}+b^{[l]}_j
zj[l]=wj1[l]a1[l−1]+wj2[l]a2[l−1]+...wjk[l]ak[l−1]...+wjnl−1[l]anl−1[l−1]+bj[l]
所以上式(1)进一步求得:
∂
E
∂
w
j
k
[
l
]
=
∂
E
∂
z
j
[
l
]
a
k
[
l
−
1
]
(
2
)
\frac{∂ E}{∂ w^{[l]}_{jk}}=\frac{∂ E}{∂ z^{[l]}_j} a^{[l-1]}_k \qquad (2)
∂wjk[l]∂E=∂zj[l]∂Eak[l−1](2)
这个时候我们引入一个中间量
δ
j
[
l
]
=
∂
E
∂
z
j
[
l
]
\delta ^{[l]}_j=\frac{∂ E}{∂ z^{[l]}_j}
δj[l]=∂zj[l]∂E,
δ
j
[
l
]
\delta ^{[l]}_j
δj[l]被称为第
l
l
l层第
j
j
j个神经元的误差。(这里个人认为直接把
δ
\delta
δ理解为一个中间量,我们引入它的目的就是为了反向传播逐层求导时方便求解,误差并不是很好理解)。
现在(2)式可以写成:
∂
E
∂
w
j
k
[
l
]
=
δ
j
[
l
]
a
k
[
l
−
1
]
(
3
)
\frac{∂ E}{∂ w^{[l]}_{jk}}=\delta ^{[l]}_j a^{[l-1]}_k \qquad (3)
∂wjk[l]∂E=δj[l]ak[l−1](3)
同理可得,
∂
E
∂
b
j
[
l
]
=
δ
j
[
l
]
(
4
)
\frac{∂ E}{∂ b^{[l]}_{j}}=\delta ^{[l]}_j \qquad (4)
∂bj[l]∂E=δj[l](4)
a
k
[
l
−
1
]
a^{[l-1]}_k
ak[l−1]通过前向传播求得,因此我们可以在前向传播时保存当前层的输入项也就是上一层的激活项,那么现在关于w和b求偏导的问题就变成了求解
δ
j
[
l
]
\delta ^{[l]}_j
δj[l]。(下面为了方便描述,还是将其称为误差。)
因为对于输出层的 δ j [ l ] \delta ^{[l]}_j δj[l]方便求解,所以我们将输出层的 δ j [ L ] \delta ^{[L]}_j δj[L]和隐藏层的 δ j [ l ] \delta ^{[l]}_j δj[l]分别求解。
2.3输出层误差
现在将2.1中的代价函数展开:
E
=
1
2
∑
k
=
1
n
L
(
y
^
k
−
y
k
)
2
=
1
2
(
(
a
1
[
L
]
−
y
1
)
2
+
(
a
2
[
L
]
−
y
2
)
2
+
.
.
.
+
(
a
j
[
L
]
−
y
j
)
2
+
.
.
.
+
(
a
n
L
[
L
]
−
y
n
L
)
2
)
=
1
2
(
(
g
(
z
1
[
L
]
)
−
y
1
)
2
+
(
g
(
z
2
[
L
]
)
−
y
2
)
2
+
.
.
.
+
(
g
(
z
j
[
L
]
)
−
y
j
)
2
+
.
.
.
+
(
g
(
z
n
L
[
L
]
)
−
y
n
L
)
2
)
E=\frac{1}{2}\sum_{k=1}^{n_L}(\hat y_k-y_k)^2\\=\frac{1}{2}((a^{[L]}_1-y_1)^2+(a^{[L]}_2-y_2)^2+...+(a^{[L]}_j-y_j)^2+...+(a^{[L]}_{n_{L}}-y_{n_{L}})^2)\\=\frac{1}{2}((g(z^{[L]}_1)-y_1)^2+(g(z^{[L]}_2)-y_2)^2+...+(g(z^{[L]}_j)-y_j)^2+...+(g(z^{[L]}_{n_L})-y_{n_{L}})^2)
E=21k=1∑nL(y^k−yk)2=21((a1[L]−y1)2+(a2[L]−y2)2+...+(aj[L]−yj)2+...+(anL[L]−ynL)2)=21((g(z1[L])−y1)2+(g(z2[L])−y2)2+...+(g(zj[L])−yj)2+...+(g(znL[L])−ynL)2)
其中,
y
^
k
\hat y_k
y^k是我们的预测值。
输出层误差:
δ
j
[
L
]
=
∂
E
∂
z
j
[
L
]
=
g
′
(
z
j
[
L
]
)
(
g
(
z
j
[
L
]
)
−
y
j
)
(
5
)
\delta ^{[L]}_j=\frac{∂ E}{∂ z^{[L]}_j} \\ =g^{\prime}(z^{[L]}_j)(g(z^{[L]}_j)-y_j) \quad (5)
δj[L]=∂zj[L]∂E=g′(zj[L])(g(zj[L])−yj)(5)
因为前面我们所求偏导的表达式是关于权重矩阵的某个元素
w
j
k
[
l
]
w^{[l]}_{jk}
wjk[l],现在我们需要将上面的式子(5)和(3)转换为向量的形式,更利于我们的编码实现以及运行效率:
{
δ
[
L
]
=
g
′
(
z
[
L
]
)
⊙
(
a
[
L
]
−
y
)
(
6
)
∂
E
∂
W
[
L
]
=
δ
[
L
]
(
a
[
L
−
1
]
)
T
(
7
)
\begin{cases}\delta ^{[L]}=g^{\prime}(z^{[L]})\odot(a^{[L]}-y)\quad (6)\\\\\frac{∂ E}{∂ W^{[L]}}=\delta ^{[L]}(a^{[L-1]})^T\quad\quad\quad\; (7)\end{cases}
⎩⎪⎨⎪⎧δ[L]=g′(z[L])⊙(a[L]−y)(6)∂W[L]∂E=δ[L](a[L−1])T(7)
这里的
⊙
\odot
⊙是对应元素相乘,不是内积。
例子. 用文章开头图片的网络例子说明一下为什么写成矩阵的形式是这样: ∂ E ∂ W [ L ] = δ [ L ] ( a [ L − 1 ] ) T = ( δ 1 [ 3 ] δ 2 [ 3 ] ) ( a 1 [ 2 ] a 2 [ 2 ] a 3 [ 2 ] ) = ( δ 1 [ 3 ] a 1 [ 2 ] δ 1 [ 3 ] a 2 [ 2 ] δ 1 [ 3 ] a 3 [ 2 ] δ 2 [ 3 ] a 1 [ 2 ] δ 2 [ 3 ] a 2 [ 2 ] δ 2 [ 3 ] a 3 [ 2 ] ) \frac{∂ E}{∂ W^{[L]}}=\delta ^{[L]}(a^{[L-1]})^T\\=\begin{pmatrix}\delta_1^{[3]}\\\delta_2^{[3]}\end{pmatrix}\begin{pmatrix}a_1^{[2]} &a_2^{[2]} &a_3^{[2]}\end{pmatrix}\\ =\begin{pmatrix}\delta_1^{[3]}a_1^{[2]} &\delta_1^{[3]}a_2^{[2]}&\delta_1^{[3]}a_3^{[2]}\\\delta_2^{[3]}a_1^{[2]} &\delta_2^{[3]}a_2^{[2]}&\delta_2^{[3]}a_3^{[2]}\end{pmatrix} ∂W[L]∂E=δ[L](a[L−1])T=(δ1[3]δ2[3])(a1[2]a2[2]a3[2])=(δ1[3]a1[2]δ2[3]a1[2]δ1[3]a2[2]δ2[3]a2[2]δ1[3]a3[2]δ2[3]a3[2])
可以看到所得矩阵的每个位置的解和我们上面分开求解 ∂ E ∂ w j k [ l ] \frac{∂ E}{∂ w^{[l]}_{jk}} ∂wjk[l]∂E结果是一样的。
2.4隐藏层误差
现在求隐藏层的
δ
j
[
l
]
\delta^{[l]}_{j}
δj[l],同样我们需要用到链式求导法则:
δ
j
[
l
]
=
∂
E
∂
z
j
[
l
]
=
∑
k
=
1
n
l
+
1
∂
E
∂
z
k
[
l
+
1
]
∂
z
k
[
l
+
1
]
∂
z
j
[
l
]
=
∑
k
=
1
n
l
+
1
δ
k
[
l
+
1
]
∂
z
k
[
l
+
1
]
∂
z
j
[
l
]
(
8
)
\delta^{[l]}_{j}=\frac{∂ E}{∂ z^{[l]}_{j}}\\=\sum_{k=1}^{n_{l+1}}\frac{∂ E}{∂ z^{[l+1]}_{k}} \frac{∂ z^{[l+1]}_{k}}{∂ z^{[l]}_{j}}\\=\sum_{k=1}^{n_{l+1}}\delta ^{[l+1]}_k \frac{∂ z^{[l+1]}_{k}}{∂ z^{[l]}_{j}}\quad(8)
δj[l]=∂zj[l]∂E=k=1∑nl+1∂zk[l+1]∂E∂zj[l]∂zk[l+1]=k=1∑nl+1δk[l+1]∂zj[l]∂zk[l+1](8)
这里说明一下为什么可以这么展开,为什么有求和的符号?
当代价函数像上面2.3那样继续向下展开时,当代价函数 E E E展开到 l + 1 l+1 l+1层时, E E E可以看做是 z [ l + 1 ] z^{[l+1]} z[l+1]的函数。 又因为 z [ l + 1 ] z^{[l+1]} z[l+1]又是关于 z [ l ] z^{[l]} z[l]的函数,如下:
z k [ l + 1 ] = w k 1 [ l + 1 ] g ( z 1 [ l ] ) + w k 2 [ l + 1 ] g ( z 2 [ l ] ) + . . . w k j [ l + 1 ] g ( z j [ l ] ) . . . + w k n l [ l + 1 ] a n l [ l ] + b k [ l ] ( 9 ) z^{[l+1]}_k=w^{[l+1]}_{k1}g(z^{[l]}_1)+w^{[l+1]}_{k2}g(z^{[l]}_2)+...w^{[l+1]}_{kj}g(z^{[l]}_j)...+w^{[l+1]}_{kn_{l}}a^{[l]}_{n_{l}}+b^{[l]}_k\quad (9) zk[l+1]=wk1[l+1]g(z1[l])+wk2[l+1]g(z2[l])+...wkj[l+1]g(zj[l])...+wknl[l+1]anl[l]+bk[l](9)
与此同时, z [ l + 1 ] = ( z 1 [ l + 1 ] z 2 [ l + 1 ] ⋮ z n l + 1 [ l + 1 ] ) z^{[l+1]}=\begin{pmatrix}z_1^{[l+1]}\\z_2^{[l+1]}\\\vdots\\z_{n_{l+1}}^{[l+1]}\end{pmatrix} z[l+1]=⎝⎜⎜⎜⎜⎛z1[l+1]z2[l+1]⋮znl+1[l+1]⎠⎟⎟⎟⎟⎞中的每一个 z k [ l + 1 ] z^{[l+1]}_k zk[l+1]都是关于 z [ l ] z^{[l]} z[l]的函数,所以上面的式子(8)在利用链式求导的时候就需要对每一个 z k [ l + 1 ] z^{[l+1]}_k zk[l+1]求导,然后 z k [ l + 1 ] z^{[l+1]}_k zk[l+1]再对 z j [ l ] z^{[l]}_j zj[l]求导再加起来。
现在对(9)式子进行求导,得:
∂
z
k
[
l
+
1
]
∂
z
j
[
l
]
=
w
k
j
[
l
+
1
]
g
′
(
z
j
[
l
]
)
(
10
)
\frac{∂ z^{[l+1]}_{k}}{∂ z^{[l]}_{j}}=w_{kj}^{{[l+1]}}g\prime(z^{[l]}_j)\quad (10)
∂zj[l]∂zk[l+1]=wkj[l+1]g′(zj[l])(10)
然后将(10)代入我们的式子(8)中,得:
δ
j
[
l
]
=
∑
k
=
1
n
l
+
1
δ
k
[
l
+
1
]
∂
z
k
[
l
+
1
]
∂
z
j
[
l
]
=
∑
k
=
1
n
l
+
1
δ
k
[
l
+
1
]
w
k
j
[
l
+
1
]
g
′
(
z
j
[
l
]
)
\delta^{[l]}_{j}=\sum_{k=1}^{n_{l+1}}\delta ^{[l+1]}_k \frac{∂ z^{[l+1]}_{k}}{∂ z^{[l]}_{j}}=\sum_{k=1}^{n_{l+1}}\delta ^{[l+1]}_kw_{kj}^{{[l+1]}}g\prime(z^{[l]}_j)
δj[l]=k=1∑nl+1δk[l+1]∂zj[l]∂zk[l+1]=k=1∑nl+1δk[l+1]wkj[l+1]g′(zj[l])
将上面用向量的形式表示(不明白下面如何得出的,可以随便举一个维度比较小的例子证明一下):
δ
[
l
]
=
(
(
W
[
l
+
1
]
)
T
δ
[
l
+
1
]
)
⊙
g
′
(
z
[
l
]
)
(
11
)
\delta^{[l]}=((W^{{[l+1]}})^T\delta ^{[l+1]})\odot g\prime(z^{[l]})\quad (11)
δ[l]=((W[l+1])Tδ[l+1])⊙g′(z[l])(11)
可以看到,
δ
[
l
]
\delta^{[l]}
δ[l] 可以通过
δ
[
l
+
1
]
\delta^{[l+1]}
δ[l+1]求得,因此隐藏的误差都可以通过最外面的输出层
δ
[
L
]
\delta^{[L]}
δ[L]求得,这也就是为什么设置这个中间量的原因。
2.5总结
现在我们将(4)(6)(7)(11)式子结合在一起可得:
δ
[
L
]
=
g
′
(
z
[
L
]
)
⊙
(
a
[
L
]
−
y
)
\delta ^{[L]}=g^{\prime}(z^{[L]})\odot(a^{[L]}-y)
δ[L]=g′(z[L])⊙(a[L]−y)
δ
[
l
]
=
(
(
W
[
l
+
1
]
)
T
δ
[
l
+
1
]
)
⊙
g
′
(
z
[
l
]
)
\delta^{[l]}=((W^{{[l+1]}})^T\delta ^{[l+1]})\odot g\prime(z^{[l]})
δ[l]=((W[l+1])Tδ[l+1])⊙g′(z[l])
∂
E
∂
W
[
l
]
=
δ
[
l
]
(
a
[
l
−
1
]
)
T
\frac{∂ E}{∂ W^{[l]}}=\delta ^{[l]}(a^{[l-1]})^T
∂W[l]∂E=δ[l](a[l−1])T
∂
E
∂
b
[
l
]
=
δ
[
l
]
\frac{∂ E}{∂ b^{[l]}}=\delta ^{[l]}
∂b[l]∂E=δ[l]
前面我们说过,是E作为一个样本的误差来计算的,但实际中我们通常都是多样本,需要将每一个求导的结果求和。
W [ l ] = W [ l ] − α ∑ i = 1 m ∂ E ( i ) ∂ W [ l ] W^{[l]}=W^{[l]}-\alpha\sum_{i=1}^m\frac{∂ E^{(i)}}{∂ W^{[l]}} W[l]=W[l]−αi=1∑m∂W[l]∂E(i) b [ l ] = b [ l ] − α ∑ i = 1 m ∂ E ( i ) ∂ b [ l ] b^{[l]}=b^{[l]}-\alpha\sum_{i=1}^m \frac{∂ E^{(i)}}{∂b^{[l]}} b[l]=b[l]−αi=1∑m∂b[l]∂E(i)
考虑多样本的情况,现在 a [ l ] a^{[l]} a[l]和 z [ l ] z^{[l]} z[l]的维度就变成 n l × m n_l\times m nl×m, y y y变成了 n L × m n_L\times m nL×m,相应的 δ [ l ] \delta^{[l]} δ[l]的维度也变成 n l × m n_l\times m nl×m,通过矩阵相乘维度的变化,我们可以发现上面的式子计算包含了我们的对每个样本求导结果求和的计算。
看到上面总结的式子,因为 δ [ l ] \delta^{[l]} δ[l]的维度是 n l × m n_l\times m nl×m,而 ( a [ l − 1 ] ) T (a^{[l-1]})^T (a[l−1])T是 m × n l − 1 m\times n_{l-1} m×nl−1,当 δ [ l ] \delta^{[l]} δ[l]中的某一行乘以 ( a [ l − 1 ] ) T (a^{[l-1]})^T (a[l−1])T中的一列时,就是m个样本求和。因此多样本的情况上式也是成立,唯一不同是 ∂ E ∂ b [ l ] = δ [ l ] \frac{∂ E}{∂ b^{[l]}}=\delta ^{[l]} ∂b[l]∂E=δ[l]需要把每一列的结果加起来,这在python中很容易实现:np.sum( δ [ l ] \delta^{[l]} δ[l], axis=1, keepdims=True)
补充.
上式中
g
′
(
z
[
L
]
)
g^{\prime}(z^{[L]})
g′(z[L])是我们激活函数的导数,当然我们每层可以使用不同的激活函数。
例如,我们在隐藏层使用
t
a
n
h
:
e
z
−
e
−
z
e
z
+
e
−
z
tanh:\frac{e^z-e^{-z}}{e^z+e^{-z}}
tanh:ez+e−zez−e−z,在输出层使用
s
i
g
m
o
i
d
:
1
1
+
e
−
z
sigmoid:\frac{1}{1+e^{-z}}
sigmoid:1+e−z1,那么对应的导数就是
t
a
n
h
:
1
−
(
e
z
−
e
−
z
e
z
+
e
−
z
)
2
tanh:1-(\frac{e^z-e^{-z}}{e^z+e^{-z}})^2
tanh:1−(ez+e−zez−e−z)2,
s
i
g
m
o
i
d
:
1
1
+
e
−
z
(
1
−
1
1
+
e
−
z
)
sigmoid:\frac{1}{1+e^{-z}}(1-\frac{1}{1+e^{-z}})
sigmoid:1+e−z1(1−1+e−z1),因为我们前向传播已经求出了激活项,所以求导的结果会用
a
a
a来表示,即
t
a
n
h
:
1
−
(
a
[
l
]
)
2
tanh:1-(a^{[l]})^2
tanh:1−(a[l])2,
s
i
g
m
o
i
d
:
a
[
l
]
(
1
−
a
[
l
]
)
sigmoid:a^{[l]}(1-a^{[l]})
sigmoid:a[l](1−a[l])。
3.python代码实现
为了更好的理解反向传播的整个过程参数传递变换,我们把2.5总结的内容稍作修改。
根据前面2.3中对
δ
\delta
δ的定义:
δ
j
[
l
]
=
∂
E
∂
z
j
[
l
]
\delta ^{[l]}_j=\frac{∂ E}{∂ z^{[l]}_j}
δj[l]=∂zj[l]∂E,不难发现
δ
\delta
δ其实就是z的偏导,我们可以简写成dz。
因为,
δ
[
l
]
=
d
E
d
z
[
l
]
=
d
E
d
a
[
l
]
d
a
[
l
]
d
z
[
l
]
=
(
(
w
[
l
+
1
]
)
T
δ
[
l
+
1
]
)
⊙
g
′
(
z
[
l
]
)
\delta^{[l]}=\frac{dE}{dz^{[l]}}=\frac{dE}{da^{[l]}}\frac{da^{[l]}}{dz^{[l]}}=((w^{{[l+1]}})^T\delta ^{[l+1]})\odot g\prime(z^{[l]})
δ[l]=dz[l]dE=da[l]dEdz[l]da[l]=((w[l+1])Tδ[l+1])⊙g′(z[l])
a
[
l
]
=
g
(
z
[
l
]
)
⟹
d
a
[
l
]
d
z
[
l
]
=
g
′
(
z
[
l
]
)
a^{[l]}=g(z^{[l]})\implies \frac{da^{[l]}}{dz^{[l]}}=g\prime(z^{[l]})
a[l]=g(z[l])⟹dz[l]da[l]=g′(z[l])
所以有如下等式:
d
E
d
a
[
l
]
=
(
(
w
[
l
+
1
]
)
T
δ
[
l
+
1
]
)
\frac{dE}{da^{[l]}}=((w^{{[l+1]}})^T\delta ^{[l+1]})
da[l]dE=((w[l+1])Tδ[l+1])
现在我们可以把2.5中的总结写成如下形式(下文和代码中为了方便,用
d
z
dz
dz表示
d
E
d
z
\frac{dE}{dz}
dzdE,
d
a
da
da表示
d
E
d
a
\frac{dE}{da}
dadE):
输
出
层
:
d
z
[
L
]
=
g
′
(
z
[
L
]
)
⊙
(
a
[
L
]
−
y
)
输出层:dz ^{[L]}=g^{\prime}(z^{[L]})\odot(a^{[L]}-y)
输出层:dz[L]=g′(z[L])⊙(a[L]−y)
隐
藏
层
:
{
d
a
[
l
]
=
(
(
w
[
l
+
1
]
)
T
d
z
[
l
+
1
]
)
d
z
[
l
]
=
d
a
[
l
]
⊙
g
′
(
z
[
l
]
)
隐藏层:\begin{cases}da^{[l]}=((w^{{[l+1]}})^Tdz ^{[l+1]})\\\\dz^{[l]}=da^{[l]}\odot g\prime(z^{[l]})\end{cases}
隐藏层:⎩⎪⎨⎪⎧da[l]=((w[l+1])Tdz[l+1])dz[l]=da[l]⊙g′(z[l])
∂
E
∂
W
[
l
]
=
δ
[
l
]
(
a
[
l
−
1
]
)
T
\frac{∂ E}{∂ W^{[l]}}=\delta ^{[l]}(a^{[l-1]})^T
∂W[l]∂E=δ[l](a[l−1])T
∂
E
∂
b
[
l
]
=
δ
[
l
]
\frac{∂ E}{∂ b^{[l]}}=\delta ^{[l]}
∂b[l]∂E=δ[l]
抛开输出层不讲,当我们在反向传播的过程中计算当前层的dW,db时,我们需要:
(1)前一层的输出值也是当前层的输入
a
[
l
−
1
]
a^{[l-1]}
a[l−1],可以在前项传播的过程缓存当前层的输入项,供反向传播时使用。
(2)当前层的da,而这个是通过后一层的W和dz计算的
(
(
w
[
l
+
1
]
)
T
d
z
[
l
+
1
]
)
((w^{{[l+1]}})^Tdz ^{[l+1]})
((w[l+1])Tdz[l+1]),因此我们在计算当前层的时候可以提前计算前一层的的
d
a
[
l
−
1
]
=
(
(
w
[
l
]
)
T
d
z
[
l
]
)
da^{[l-1]}=((w^{{[l]}})^Tdz ^{[l]})
da[l−1]=((w[l])Tdz[l])
(3)最后是
g
′
(
z
[
L
]
)
g^{\prime}(z^{[L]})
g′(z[L])的计算,通常的sigmoid和tanh函数都是通过当前层的输出(也就是激活项)来计算,可以看2.5中的补充,而当前层的输出也可以在前向传播的过程缓存。
因此,我们在前向传播每一层的时候需要缓存当前层的输出a(也可以是z,只是多一次计算),当前层的参数W,当层的输入,然后在反向传播每一层时中除了返回dW,db,还需要计算并返回 d a [ l − 1 ] da^{[l-1]} da[l−1]。
3.1初始化参数
def initialize_parameters_deep(layer_dims):
np.random.seed(3)
parameters = {}
L = len(layer_dims)
for l in range(1,L):
parameters["W"+str(l)] = np.random.randn(layer_dims[l],layer_dims[l-1])/np.sqrt(layer_dims[l-1])
parameters["b"+str(l)] = np.zeros((layer_dims[l],1))
assert(parameters["W"+str(l)].shape == (layer_dims[l],layer_dims[l-1]))
assert(parameters["b"+str(l)].shape == (layer_dims[l],1))
return parameters
3.2前项传播
def linear_forward(A,W,b):
Z = np.dot(W,A) + b
assert(Z.shape == (W.shape[0], A.shape[1]))
cache = (A,W,b) #A是当前层l的输入值,也是前一层l-1的输出,W,b是当前层的参数
return Z, cache
def linear_activation_forward(A_prev, W, b, activation):
if activation == "sigmoid":
Z, linear_cache = linear_forward(A_prev, W, b) #linear_cache是缓存的当前层的参数W,b 以及当前层的输入——(A_prev,W,b)
A, activation_cache = sigmoid(Z) #activation_cache缓存的是当前层的Z值,用于反向传播计算。
elif activation == "relu":
Z, linear_cache = linear_forward(A_prev, W, b)
A, activation_cache = relu(Z)
assert(A.shape == (W.shape[0], A_prev.shape[1]))
cache = (linear_cache, activation_cache)
return A,cache
def L_model_forward(X,parameters):
caches = []
A = X
L = len(parameters) // 2 #每一层有W和b两个参数
for l in range(1,L): #计算隐藏层
A_prev = A #A_prev是当前层的输入值
A,cache = linear_activation_forward(A_prev, parameters["W"+str(l)], parameters["b"+str(l)], "relu") #A是当前层的输出
caches.append(cache)
#计算输出层
AL, cache = linear_activation_forward(A, parameters["W"+str(L)], parameters["b"+str(L)],"sigmoid")
caches.append(cache)
assert(AL.shape == (1,X.shape[1]))
return AL, caches
3.3反向传播
def linear_backward(dZ, cache):
A_prev, W, b = cache
m = A_prev.shape[1]
dW = np.dot(dZ, A_prev.T) / m
db = np.sum(dZ, axis=1, keepdims=True) / m
dA_prev = np.dot(W.T, dZ)
assert(dA_prev.shape == A_prev.shape)
assert(dW.shape == W.shape)
assert(db.shape == b.shape)
return dA_prev, dW, db
def linear_activation_backward(dA,cache,activation="relu"): #dA和cache都是当前层
linear_cache, activation_cache = cache
if activation == "relu":
dZ = relu_backward(dA, activation_cache) #调用的库函数,看文末
dA_prev, dW, db = linear_backward(dZ, linear_cache)
elif activation == "sigmoid":
dZ = sigmoid_backward(dA, activation_cache) #调用的库函数,看文末
dA_prev, dW, db = linear_backward(dZ, linear_cache)
return dA_prev, dW, db
def L_model_backward(AL, Y, caches):
grads = {}
L = len(caches) #隐藏层数+1(输出层)
m = AL.shape[1] #训练样本数
Y = Y.reshape(AL.shape)
dAL = -(np.divide(Y, AL) - np.divide(1-Y, 1-AL))
current_cache = caches[L-1]
grads["dA"+str(L)], grads["dW"+str(L)],grads["db"+str(L)] = linear_activation_backward(dAL, current_cache,"sigmoid")
for l in reversed(range(L-1)): #l-2,l-3,...,0
cur_level = l+1 #表示当前是第几层,输入层是第0层
#因为caches中的第0位存放的是第一层的缓存,也就是第一个隐藏层,而第一层的cur_level=1,所以减一
current_cache = caches[cur_level-1] #当前层的缓存,包括当前层的参数W,b,z,以及输入项A_prev,W用于计算前一层的dA,z用于计算当前层的dZ,A_prev用于计算当前层的dW
dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads["dA"+str(cur_level+1)],current_cache,"relu")
grads["dA"+str(cur_level)] = dA_prev_temp
grads["dW"+str(cur_level)] = dW_temp
grads["db"+str(cur_level)] = db_temp
return grads
3.4参数更新
def update_parameters(parameters, grads, learning_rate):
L = len(parameters) // 2
for l in range(L):
parameters["W"+str(l+1)] = parameters["W"+str(l+1)] - learning_rate * grads["dW"+str(l+1)]
parameters["b"+str(l+1)] = parameters["b"+str(l+1)] - learning_rate * grads["db"+str(l+1)]
return parameters
3.5代码整合
def L_layer_model(X,Y, layer_dims, learning_rate = 0.0075,num_iterations=3000,print_cost=False,isPlot=True):
np.random.seed(1)
costs = []
parameters = initialize_parameters_deep(layer_dims)
for i in range(0,num_iterations):
AL, caches = L_model_forward(X, parameters)
cost = compute_cost(AL, Y)
grads = L_model_backward(AL,Y,caches)
parameters = update_parameters(parameters,grads,learning_rate)
if i%100 == 0:
costs.append(cost)
if print_cost:
print("第 ",i," 次迭代,成本值为:",np.squeeze(cost))
if isPlot:
plt.plot(np.squeeze(costs))
plt.ylabel("cost")
plt.xlabel("iterations (per tens)")
plt.title("learning rate ="+str(learning_rate))
plt.show()
return parameters
库函数
import numpy as np
def sigmoid(Z):
A = 1/(1+np.exp(-Z))
cache = Z
return A, cache
def sigmoid_backward(dA, cache):
Z = cache
s = 1/(1+np.exp(-Z))
dZ = dA * s * (1-s)
assert (dZ.shape == Z.shape)
return dZ
def relu(Z):
A = np.maximum(0,Z)
assert(A.shape == Z.shape)
cache = Z
return A, cache
def relu_backward(dA, cache):
Z = cache
dZ = np.array(dA, copy=True) # just converting dz to a correct object.
dZ[Z <= 0] = 0
assert (dZ.shape == Z.shape)
return dZ