第三章 浅层神经网络
自学Andrew Ng老师的神经网络公开课,作一些笔记,老师讲的非常详细,通俗易懂
文章目录
3.1 神经网络概览
本节讲了logistics回归和神经网络的关系,神经网络中可以使用多个logistics回归的例子,每个logistics回归是神经网络的一个节点,这些节点分布在不同的层,输入就是上一层节点的输出,输出是下一层节点的输入,每一个节点都满足logistics回归的运算方法,最后整个网络依然要计算最后一层的输出和输入标签y的差距,也就是成本函数
L
(
a
[
m
]
,
y
)
L(a^{[m]},y)
L(a[m],y)
m
m
m是指神经网络一共
m
m
m层。
同理,对于反向传播算法也同样适用于神经网络,只需要反向去计算各个参数的导数就行。
使用中括号的上标来表示神经网络的层序号。
3.2 神经网络表示
本节使用一个简单的神经网络来介绍神经网络中的一些参数符号和关系。以一个两层神经网络为例,第0层是输入层,一般写作
a
[
0
]
a^{[0]}
a[0] = X,第1层是隐藏层,之所以是隐藏层,是指相对于训练样本来说,没有输入或输出的数据直接与该层有关,写作
a
[
1
]
a^{[1]}
a[1],第2层是输出层,输出层有一个节点,因为要输出结果,写作
y
^
=
a
[
2
]
\hat y = a^{[2]}
y^=a[2],第1层和第2层含有参数
w
w
w和
b
b
b,每个节点都含有,每个层的
w
w
w和
b
b
b维度不一定相同,取决于该层的节点数和输入层的节点数。
虽然有第0层,但一般称做两层神经网络。
3.3 计算神经网络的输出
这节讲得比较细,如果上一节看懂了,这节的内容很容易懂,其实就是把神经网络如何计算的用公式写了出来。
对于每个节点,都有一个输入
z
=
w
∗
x
+
b
z=w*x + b
z=w∗x+b
和一个输出计算
a
=
δ
(
z
)
a=\delta(z)
a=δ(z)
所以对于任何一个节点,都存在这两个公式。将一层的节点写作一个向量,于是就能够得到整个层的计算公式是
Z
[
m
]
=
w
[
m
]
.
T
∗
x
+
b
[
m
]
Z^{[m]}=w^{[m].T}*x+b^{[m]}
Z[m]=w[m].T∗x+b[m]
a
[
m
]
=
δ
(
Z
[
m
]
)
a^{[m]}=\delta(Z^{[m]})
a[m]=δ(Z[m])
然后将每一层都按这么写,由于w和b的维度不同,最后输出的a就是
y
^
\hat{y}
y^,一个实数,这是因为最后一层是一个节点。
也就是说,每个节点都在进行logistic运算。
3.4 多个训练样本的向量化
上一节讲的是对于一个训练样本穿过整个神经网络的例子,也就是小x,这节讲如果有m个训练样本,怎么向量化。
如果不向量化的话,就是把神经网络循环m次,但这样效率不高。
注意每个样本的特征和样本数不同,每个样本的特征一般计做n,样本数计做m,在矩阵中,同一个样本的多个特征按列排列,不同样本的同一个特征按行排列。
对于输入层,也就是训练样本层,
X
=
M
a
t
r
i
x
(
n
∗
m
)
X=Matrix(n*m)
X=Matrix(n∗m)每列是一个训练样本,每行是所有训练样本的同一个特征;对于隐藏层,定义
Z
[
1
]
=
M
a
t
r
i
x
(
n
∗
m
)
Z^{[1]}=Matrix(n*m)
Z[1]=Matrix(n∗m)
A
[
1
]
=
M
a
t
r
i
x
(
n
∗
m
)
A^{[1]}=Matrix(n*m)
A[1]=Matrix(n∗m)输出层同理。
故而:
Z
[
1
]
=
w
[
1
]
∗
X
+
b
[
1
]
=
M
a
t
r
i
x
(
n
∗
m
)
Z^{[1]}=w^{[1]}*X + b^{[1]}=Matrix(n*m)
Z[1]=w[1]∗X+b[1]=Matrix(n∗m)
A [ 1 ] = δ ( Z [ 1 ] ) = M a t r i x ( n ∗ m ) A^{[1]}=\delta (Z^{[1]})=Matrix(n * m) A[1]=δ(Z[1])=Matrix(n∗m)
Z [ 2 ] = w [ 2 ] ∗ A [ 1 ] + b [ 2 ] = M a t r i x ( 1 ∗ m ) Z^{[2]}=w^{[2]}*A^{[1]}+b^{[2]}=Matrix(1*m) Z[2]=w[2]∗A[1]+b[2]=Matrix(1∗m)
A [ 2 ] = y ^ = δ ( Z [ 2 ] ) = M a t r i x ( 1 ∗ m ) A^{[2]}=\hat y = \delta (Z^{[2]})=Matrix(1*m) A[2]=y^=δ(Z[2])=Matrix(1∗m)
注意一个符号定义:对于第l层的第i个样本,写作:
Z
[
l
]
(
i
)
Z^{[l](i)}
Z[l](i)
A [ l ] ( i ) A^{[l](i)} A[l](i)
注意理解,对于除输出层的其他层,Z和A的每一列的每一项都是一个节点,比如 Z [ l ] ( i ) Z^{[l](i)} Z[l](i)的第一个项是指隐藏层中的第一个训练样本的第一个节点,所以对于Z和A来说,横着看每一行,就是每个节点在不同输入样本下的表现。竖着看是每个样本在当前层的所有节点的表现。
3.5 训练样本向量化的解释
这一节讲的内容就是上一节的解释,其实可以不看,没有进一步的内容。
其中一点是,对于第一层
Z
[
1
]
=
w
[
1
]
∗
X
+
b
1
Z^{[1]} = w^{[1]}*X + b^{1}
Z[1]=w[1]∗X+b1
其中的X其实就是训练样本
X
=
A
[
0
]
X=A^{[0]}
X=A[0]
比如,第1个训练样本就是
X
(
1
)
=
A
[
0
]
(
1
)
X^{(1)}=A^{[0](1)}
X(1)=A[0](1)
以此类推。
向量化可以允许我们一次性对所有输入训练样本进行处理。
3.6 激活函数
这节内容比较多,围绕激活函数展开。之前的教程里激活函数都是使用sigmoid函数
δ
=
1
(
1
+
e
−
z
)
\delta=\frac {1}{(1+e^{-z})}
δ=(1+e−z)1
之所以用这个函数,是因为这个函数的
z
z
z
趋向于正负无穷大时,
δ
\delta
δ 趋近于1或0,用于二元分类很实用。然而,在神经网络中,很少给隐藏节点使用这个激活函数,而是用tanh函数来替代
g
(
z
)
=
t
a
n
h
(
z
)
=
e
z
−
e
−
z
e
z
+
e
−
z
g(z)=tanh(z)=\frac{e^z-e^{-z}}{e^z+e^{-z}}
g(z)=tanh(z)=ez+e−zez−e−z
这个函数其实是sigmoid函数的平移,但由于tanh函数的输入状态平均值为0,所以更有利于下一层的学习。不过,当神经网络作为二元分类时,也就是估计状态为0或1时,输出层的激活函数依然是sigmoid函数。
每一层的激活函数都可以不相同,有时候用g的上标来区分不同层的激活函数
g
[
l
]
(
z
)
g^{[l]}(z)
g[l](z)
表示第
l
l
l层的激活函数。
sigmoid函数和tanh函数作为激活函数有个共同的缺点,就是当z趋近于正负无穷大时,函数的导数都趋近于0,这样会拖慢梯度下降算法。
还可以使用一种叫做ReLU函数的激活函数
R
e
L
U
(
z
)
=
m
a
x
(
0
,
z
)
ReLU(z)=max(0,z)
ReLU(z)=max(0,z)
该函数当z大于等于0时,是斜率为1的线性函数,当z小于0时,是0。虽然函数不可微,但只有当z=0一点不可微,所以还好,概率很低。另外也会有人用泄漏ReLU
L
e
a
k
y
R
e
L
U
(
z
)
=
m
a
x
(
0.01
z
,
z
)
LeakyReLU(z)=max(0.01z,z)
LeakyReLU(z)=max(0.01z,z)
0.01是一个小常数,泄漏ReLU的特点是当z小于0时,函数的斜率不为0,这样也可以避免拖慢梯度下降算法的问题。但泄漏ReLU函数用的并不多。
这几种激活函数都能够选用,虽然大多数情况下都是使用ReLU,但并不意味着ReLU一定好,所以有些时候需要把几种激活函数都在验证集数据中试一下,看看哪种更好。
3.7 为什么需要非线性函数作为激活函数
这节解释了上一节中为什么我们总是选择非线性函数作为激活函数,为什么不能去掉
δ
\delta
δ,直接令
a
[
l
]
=
z
[
l
]
=
w
[
l
]
∗
x
+
b
[
l
]
a^{[l]}=z^{[l]}=w^{[l]}*x +b^{[l]}
a[l]=z[l]=w[l]∗x+b[l]
原因是,线性函数的叠加依然是线性函数,如果神经网络中的激活函数使用线性函数,那么隐藏层将没有意义,重复性的计算和直接使用logistics回归计算的结果是一样的。
有一种情况下,输出层可以使用线性函数作为激活函数,就是当输出状态不是二元估计,而是实数时,可以用线性函数输出,不过这种情况下去使用ReLU其实更好。对于隐藏层来说,除了在一些压缩的算法应用场景下,使用线性函数作为激活函数和直接去掉隐藏层是一样的效果。
3.8 激活函数的导数
讲解了几个常见的激活函数的导数是什么。
sigmoid函数的导数是
g
′
(
z
)
=
∂
∂
z
g
(
z
)
=
g
(
z
)
∗
(
1
−
g
(
z
)
)
g^{'}(z)=\frac{\partial}{\partial{z}}g(z)=g(z)*(1-g(z))
g′(z)=∂z∂g(z)=g(z)∗(1−g(z))
tanh函数的导数是
g
′
(
z
)
=
∂
∂
z
g
(
z
)
=
1
−
g
(
z
)
2
g^{'}(z)=\frac{\partial}{\partial{z}}g(z)=1-g(z)^2
g′(z)=∂z∂g(z)=1−g(z)2
ReLU函数的导数是
g
′
(
z
)
=
∂
∂
z
g
(
z
)
=
{
0
z<0
1
z>0
u
n
d
e
f
i
n
e
d
z=0
g^{'}(z)=\frac{\partial}{\partial{z}}g(z)={ \begin{cases} 0& \text{z<0} \\ 1& \text{z>0} \\ undefined& \text{z=0} \\ \end{cases} }
g′(z)=∂z∂g(z)=⎩⎪⎨⎪⎧01undefinedz<0z>0z=0
在程序中可以让当z=0时的情况导数为0或1,都没关系,应该z=0的概率很低。
泄漏的ReLU函数的导数是,对于
g
(
z
)
=
m
a
x
(
0.01
z
,
z
)
g(z)=max(0.01z, z)
g(z)=max(0.01z,z)
g
′
(
z
)
=
∂
∂
z
g
(
z
)
=
{
0.01
z<0
1
z>0
u
n
d
e
f
i
n
e
d
z=0
g^{'}(z)=\frac{\partial}{\partial{z}}g(z)={ \begin{cases} 0.01& \text{z<0} \\ 1& \text{z>0} \\ undefined& \text{z=0} \\ \end{cases} }
g′(z)=∂z∂g(z)=⎩⎪⎨⎪⎧0.011undefinedz<0z>0z=0
3.9 神经网络的梯度下降算法
这节我们开始接触神经网络中的反向传播算法,本节仅仅将反向传播的公式写了出来。
首先,回顾神经网络模型,我们的两层神经网络中,有四个参数:
w
[
1
]
:
(
n
[
1
]
,
n
[
0
]
)
w^{[1]} :(n^{[1]},n^{[0]})
w[1]:(n[1],n[0])
b [ 1 ] : ( n [ 1 ] , 1 ) b^{[1]}:(n^{[1]},1) b[1]:(n[1],1)
w [ 2 ] : ( n [ 2 ] , n [ 1 ] ) w^{[2]}:(n^{[2]},n^{[1]}) w[2]:(n[2],n[1])
b [ 2 ] : ( n [ 2 ] , 1 ) b^{[2]}:(n^{[2]},1) b[2]:(n[2],1)
其中,冒号后边的是这个变量的矩阵维度,在我们的例子中, n [ 2 ] = 1 n^{[2]} = 1 n[2]=1
代价函数是:
J
(
W
[
1
]
,
b
[
1
]
,
w
[
2
]
,
b
[
2
]
)
=
1
m
∑
i
=
1
n
L
(
y
^
,
y
)
J(W^{[1]}, b^{[1]}, w^{[2]}, b^{[2]})=\frac{1}{m}\sum_{i=1}^nL(\hat y, y)
J(W[1],b[1],w[2],b[2])=m1i=1∑nL(y^,y)
梯度下降算法就是通过计算输出的预测值,以及各个参数的导数,进而更新各个参数,最后使得整个系统收敛:
forloop{
compute predict (yhat)
dw^[1]=dJ/dw^[1]
db^[1]=dJ/db^[1]
dw^[2]=dJ/dw^[2]
db^[2]=dJ/db^[2]
w^[1]=w^[1]-a*dw^[1]
b^[1]=b^[1]-a*db^[1]
w^[2]=w^[2]-a*dw^[2]
b^[2]=b^[2]-a*db^[2]
}
本节只是把计算微分的几个公式写出来:
d
z
[
2
]
=
A
[
2
]
−
Y
dz^{[2]}=A^{[2]}-Y
dz[2]=A[2]−Y
其中
Y
=
[
y
(
1
)
,
y
(
2
)
,
.
.
.
,
y
[
m
]
]
Y=[y^{(1)}, y^{(2)}, ..., y^{[m]}]
Y=[y(1),y(2),...,y[m]]
输出层参数的导数是:
d
w
[
2
]
=
1
m
d
z
[
2
]
A
[
1
]
.
T
dw^{[2]}=\frac{1}{m}dz^{[2]}A^{[1].T}
dw[2]=m1dz[2]A[1].T
d b [ 2 ] = 1 m n p . s u m ( d z [ 2 ] , a x i s = 1 , k e e p d i m s = T r u e ) db^{[2]}=\frac{1}{m}np.sum(dz^{[2]}, axis=1, keepdims=True) db[2]=m1np.sum(dz[2],axis=1,keepdims=True)
这里用Python代码表示,axis表示按行或按列来累加,keepdims=True表示将一维的秩表示为矩阵,避免错误计算,就是前边讲过的reshape类似的功能。 d b [ 2 ] db^{[2]} db[2]是一个实数,但 d b [ 1 ] db^{[1]} db[1]不是实数。
隐藏层参数的导数是:
d
z
[
1
]
=
w
[
2
]
.
T
d
z
[
2
]
∗
g
[
1
]
′
(
z
[
1
]
)
dz^{[1]}=w^{[2].T}dz^{[2]}*g^{[1]'}(z^{[1]})
dz[1]=w[2].Tdz[2]∗g[1]′(z[1])
d w [ 1 ] = 1 m d z [ 1 ] X T dw^{[1]}=\frac{1}{m}dz^{[1]}X^{T} dw[1]=m1dz[1]XT
d b [ 1 ] = 1 m n p . s u m ( d z [ 1 ] , a x i s = 1 , k e e p d i m s = T r u e ) db^{[1]}=\frac{1}{m}np.sum(dz^{[1]}, axis=1, keepdims=True) db[1]=m1np.sum(dz[1],axis=1,keepdims=True)
3.10 直观理解反向传播的导数公式的推导
这一节的内容是选修内容。
老师从logistics回归的反向传播算法讲起,回顾logistics回归的反向传播公式,然后引入神经网络的公式,神经网络的反向传播算法仅仅是logistics回归多了一层的结果,不过反向传播到第1层的公式要更加复杂,然后可以推导出单个样本的反向传播公式:
d
z
[
2
]
=
a
[
2
]
−
y
dz^{[2]}=a^{[2]}-y
dz[2]=a[2]−y
d w [ 2 ] = d z [ 2 ] a [ 1 ] . T dw^{[2]}=dz^{[2]}a^{[1].T} dw[2]=dz[2]a[1].T
d b [ 2 ] = d z [ 2 ] db^{[2]}=dz^{[2]} db[2]=dz[2]
d z [ 1 ] = w [ 2 ] . T d z [ 2 ] ∗ g [ 1 ] ′ ( z [ 1 ] ) dz^{[1]}=w^{[2].T}dz^{[2]}*g^{[1]'}(z^{[1]}) dz[1]=w[2].Tdz[2]∗g[1]′(z[1])
d w [ 1 ] = d z [ 1 ] x T dw^{[1]}=dz^{[1]}x^{T} dw[1]=dz[1]xT
d b [ 1 ] = d z [ 1 ] db^{[1]}=dz^{[1]} db[1]=dz[1]
我们可以通过检查每个项的维度来判断是否会出错。 d z [ 1 ] dz^{[1]} dz[1]是 ( n [ 1 ] , 1 ) (n^{[1]}, 1) (n[1],1)维度, d z [ 2 ] dz^{[2]} dz[2]是 ( n [ 2 ] , 1 ) (n^{[2]}, 1) (n[2],1)维度。
将整个样本集按照向量法来写出公式为:
d
Z
[
2
]
=
A
[
2
]
−
Y
dZ^{[2]} = A^{[2]}-Y
dZ[2]=A[2]−Y
d W [ 2 ] = 1 m d Z [ 2 ] A [ 1 ] . T dW^{[2]}=\frac{1}{m}dZ^{[2]}A^{[1].T} dW[2]=m1dZ[2]A[1].T
d B [ 2 ] = 1 m n p . s u m ( d Z [ 2 ] , a x i s = 1 , k e e p d i m s = T r u e ) dB^{[2]}=\frac{1}{m}np.sum(dZ^{[2]}, axis=1, keepdims=True) dB[2]=m1np.sum(dZ[2],axis=1,keepdims=True)
d Z [ 1 ] = W [ 2 ] . T d Z [ 2 ] ∗ g [ 1 ] ′ ( X T ) dZ^{[1]}=W^{[2].T}dZ^{[2]}*g^{[1]'}(X^{T}) dZ[1]=W[2].TdZ[2]∗g[1]′(XT)
d W [ 1 ] = 1 m d Z [ 1 ] X T dW^{[1]}=\frac{1}{m}dZ^{[1]}X^{T} dW[1]=m1dZ[1]XT
d B [ 1 ] = 1 m n p . s u m ( d Z [ 1 ] , a x i s = 1 , k e e p d i m s = T r u e ) dB^{[1]}=\frac{1}{m}np.sum(dZ^{[1]}, axis=1, keepdims=True) dB[1]=m1np.sum(dZ[1],axis=1,keepdims=True)
3.11 随机初始化
如果将神经网络中的各参数初始化为0,则会导致神经网络无效,本节讲解为什么会出现问题。
对于w是存在这个问题的,b没有问题,可以为0。
原因是,由于 z = w x + b z=wx+b z=wx+b当w为0时 a [ 1 ] ( 1 ) = a [ 1 ] ( 2 ) a^{[1](1)}=a^{[1](2)} a[1](1)=a[1](2)通过推理,我们知道 d z [ 1 ] ( 1 ) = d z [ 1 ] ( 2 ) dz^{[1](1)}=dz^{[1](2)} dz[1](1)=dz[1](2)进而导致dw的值都是相同的,这样,无论经过多少次计算,所有同一层的节点都在做同样的计算,从而不管有再多的节点,都等效于一个节点。
解决这个问题的办法就是随机初始化:
w
[
1
]
=
n
p
.
r
a
n
d
o
m
.
r
a
n
d
n
(
(
n
[
1
]
,
n
[
0
]
)
)
∗
0.01
w^{[1]}=np.random.randn((n^{[1]},n^{[0]}))*0.01
w[1]=np.random.randn((n[1],n[0]))∗0.01
b [ 1 ] = n p . z e r o s ( ( n [ 1 ] , 1 ) ) b^{[1]}=np.zeros((n^{[1]},1)) b[1]=np.zeros((n[1],1))
w [ 2 ] = n p . r a n d o m . r a n d n ( ( n [ 2 ] , n [ 1 ] ) ) ∗ 0.01 w^{[2]}=np.random.randn((n^{[2]},n^{[1]}))*0.01 w[2]=np.random.randn((n[2],n[1]))∗0.01
b [ 2 ] = n p . z e r o s ( ( n [ 2 ] , 1 ) ) b^{[2]}=np.zeros((n^{[2]},1)) b[2]=np.zeros((n[2],1))
其中0.01是一个小数,之所以不选择一个大数,是因为大数很容易让训练时的神经网络趋向饱和,收敛更慢,如果神经网络的输出是二元状态,这个值应该要很小。