浅层神经网络
神经网络概览
最为常见的由输入层、隐藏层和输出层三层构成的神经网络通常被称为双层神经网络,其中输入层为第零层(因为并不把输入层看作是一个标准的层),隐藏层和输出层分别为第一、二层。
隐藏层和输出层是带有参数的,隐藏层有两个相关参数w[1]和b[1],上标1表示是和第一层隐藏层有关。
接下来的例子里W[1]是一个4x3的矩阵(其中4表示有4个节点,或四个隐藏单元。3表示有三个输入特征),b[1]是一个4x1d的向量。
类似的第二层输出层也有相关参数W[2](1x4,表示又四个隐藏单元)和b[2](1x1,一个隐藏单元)。
计算神经网络的输出
之前讲到的逻辑回归模型,模型内的圆圈代表了逻辑回归计算的两个步骤。先计算出z,之后计算激活函数。
神经网络实际上就是重复计算这些步骤很多次。
这里就是对隐藏层的每一个节点(圆圈)进行计算:
例如第一节点:
z
[
1
]
=
w
T
x
+
b
z^{[1]}=w^Tx+b
z[1]=wTx+b
a
1
[
1
]
=
σ
(
z
1
[
1
]
)
a_1^{[1]}=\sigma(z_1^{[1]})
a1[1]=σ(z1[1])
所以对于z和a按符号约定写成
a
i
[
l
]
a^{[l]}_i
ai[l] l表示层数,i表示层中的第几个节点
将w堆起来构成一个(4x3)的矩阵
[
.
.
.
w
1
[
1
]
T
.
.
.
.
.
.
w
2
[
1
]
T
.
.
.
.
.
.
w
3
[
1
]
T
.
.
.
.
.
.
w
4
[
1
]
T
.
.
.
]
\begin{bmatrix} ...&w_1^{[1]T} &... \\ ...&w_2^{[1]T} &... \\ ...&w_3^{[1]T} &... \\ ...&w_4^{[1]T} &... \\ \end{bmatrix}
⎣⎢⎢⎢⎡............w1[1]Tw2[1]Tw3[1]Tw4[1]T............⎦⎥⎥⎥⎤
也可以看做是我们拥有4个逻辑回归单元,并且每一个逻辑回归单元都有对应的参数向量w
W[1]的维度是(4,3),b[1]的维度是(4,1)
a[1]的维度是(4,1)
也可以说x等于a[0],就像
y
^
{\hat y}
y^等于a[2]
输出层的参数W[2]的维度是(1,4),b[2]的维度是(1,1),也就得到z[2]是一个实数
z[1] = W[1]a[0] + b[1]
a[1] = σ(z[1])
z[2] = W[2]a[1] + b[2]
a[2] = σ(z[2])
最后的输出单元就像是逻辑回归模型的执行过程
当有一个单隐层神经网络,在代码中实现的过程就是计算以上四个等式,前两个式子是计算隐藏层的四个逻辑回归单元,后两个式子是计算输出层的逻辑回归单元。
多样本向量化实现
上一节说的对于输入的特征向量x单个训练样本,可以用它们生成一个
a
[
2
]
=
y
^
a^{[2]}=\hat y
a[2]=y^
现在如果有m个训练样本,就需要重复以上计算
例:用第一个训练样本x(1)来计算
y
^
(
1
)
\hat y^{(1)}
y^(1),即对第一训练样本的预测
同理,用x(2)来计算
y
^
(
2
)
\hat y^{(2)}
y^(2),x(m)来计算
y
^
(
m
)
\hat y^{(m)}
y^(m)
用激活函数来表示这些式子
a
[
2
]
(
1
)
=
y
^
(
1
)
a^{[2](1)}=\hat y^{(1)}
a[2](1)=y^(1)
a
[
2
]
(
2
)
=
y
^
(
2
)
a^{[2](2)}=\hat y^{(2)}
a[2](2)=y^(2)
a
[
2
]
(
m
)
=
y
^
(
m
)
a^{[2](m)}=\hat y^{(m)}
a[2](m)=y^(m)
这里a[2](i),i指训练样本i,2指第二层
代码表示
for i = 1 to m,
z[1](i) = W[1]x(1) + b[1]
a[1](i) = σ(z[1](i))
z[2](i) = W[2]a[1](i) + b[2]
a[2](i) = σ(z[2](i))
将训练样本x堆叠到矩阵各列,构成(
n
x
n_x
nx,m)维X矩阵
得到计算式子
Z[1] = W[1]X + b[1]
A[1] = σ(Z[1])
Z[2] = W[2]A[1] + b[2]
A[2] = σ(Z[2])
同理将向量z和a横向堆叠出矩阵Z和A
横向即训练集中的所有训练样本,竖向表示对应神经网络中的不同节点,例如矩阵A[1]中的左上角就表示第一个训练样本的第一个隐藏单元的激活函数;矩阵X的竖向对应不同的输入特征,即神经网络输入层的不同节点。
激活函数
之前式子里面一直提到的σ(·)就是所谓的激活函数
在更一般的情况下,可以使用不同的函数g(·),其中g可以是非线性函数,不一定是σ函数。
σ函数是介于0和1之间的,然而tanh函数(双曲正切函数)较σ表现更好
tanh函数介于-1和1之间
tanh
(
z
)
=
e
z
−
e
−
z
e
z
+
e
−
z
\tanh (z) = \frac{{{e^z} - {e^{ - z}}}}{{{e^z} + {e^{ - z}}}}
tanh(z)=ez+e−zez−e−z
从数学角度讲tanh函数实际上是sigmoid函数变换得到的,即
将sigmoid函数向下平移1/2个单位长度
1
1
+
e
−
z
−
1
2
=
1
2
⋅
1
−
e
−
z
1
+
e
−
z
\frac{1}{{1 + {e^{ - z}}}} - \frac{1}{2} = \frac{1}{2} \cdot \frac{{1 - {e^{ - z}}}}{{1 + {e^{ - z}}}}
1+e−z1−21=21⋅1+e−z1−e−z
再将变换后的函数横纵向拉伸2倍
(
1
2
⋅
1
−
e
−
z
×
2
1
+
e
−
z
×
2
)
×
2
=
1
−
e
−
2
z
1
+
e
−
2
z
=
e
z
−
e
−
z
e
z
+
e
−
z
(\frac{1}{2} \cdot \frac{{1 - {e^{ - z \times 2}}}}{{1 + {e^{ - z \times 2}}}}) \times 2 = \frac{{1 - {e^{ - 2z}}}}{{1 + {e^{ - 2z}}}} = \frac{{{e^z} - {e^{ - z}}}}{{{e^z} + {e^{ - z}}}}
(21⋅1+e−z×21−e−z×2)×2=1+e−2z1−e−2z=ez+e−zez−e−z
如果使用tanh当激活函数,通常总是比σ函数效果更好,因为这时函数的输出介于-1和1之间,激活函数的平均值就更接近0。使用tanh函数代替sigmoid函数可以中心化你的数据,使得是数据的平均值接近0,而不是0.5。这样让下一层的学习更加方便,tanh函数几乎在所有场合都更优越。
不过有一个例外,那就是输出层, 因为如果y是0或1,那么得到的
y
^
\hat y
y^介于0到1之间更合理,而不是-1和1之间。
使用σ激活函数的一个例外场合就是使用二元分类的时候,这种情况下,可以使用σ激活函数作为输出层。
在之前介绍的神经网络例子中,可以在隐藏层中使用tanh函数,在输出层使用σ函数。即不同层的激活函数可以不一样。
为了方便表示不同的激活函数g(·)可以使用上标进行区分,例如g[1]和g[2],数字表示第几层。
σ函数和tanh函数有一个共同的缺点,对于激活函数g(z),如果z非常大或非常小,那么导数的梯度,或者说这个函数的斜率可能就很小,接近于0,从而拖慢梯度下降算法。
在机器学习中,最受欢迎的一个选择是ReLU函数(修正线性单元)
只要z为正,导数就是1;当z为负时,导数或斜率为0。如果你实际使用这个函数,导数在z刚好为0的时候不是良定义的(not well-defined即有歧义的),但如果你编程实现,那么你得到的z恰好等于000000…的概率很低,所以实际中不用担心这点。你可以在z=0时,给导数赋值成1或0,尽管事实上这个函数不可微。
【选择激活函数的经验法则】
如果你的输出值是0和1,如果你在做二分类
那么sigmoid函数适合作为输出层的激活函数,然后其他所有单元都用ReLU。ReLU是激活函数的默认选择, 如果不确定隐藏层该用哪个,那么就用ReLU。
ReLU还有另一个版本leaky ReLU,这个函数在z为负时,不再为0。它通常比ReLU激活函数更好,不过实际使用的频率没那么高。
ReLU和leaky ReLU的优点在于对于大量z空间,激活函数的导数、激活函数的斜率和0差的很远。所以在实践中使用ReLU激活函数,通常可以使神经网络的学习速度加快很多,比使用tanh或σ激活函数快得多。
主要原因在于ReLU在函数斜率接近0时,几乎不出现减慢学习速度的效应。
对于z的另一半范围来说,ReLU的斜率为0,但在实践中,有足够多的的隐藏单元令z大于0,所以对大多数训练样本来说还是很快的。
【为什么神经网络需要非线性激活函数】
之前提到的神经网络计算公式,用g(z)代指激活函数
z[1] = W[1]x + b[1]
a[1] = g[1](z[1])
z[2] = W[2]a[1] + b[2]
a[2] = g[2](z[2])
为什么不能让g(z)=z,这时g(z)叫做线性激活函数或恒等激活函数,这是激活直接将输入值输出
为了说明问题,假设让a[2]=z[2],事实证明如果这样做,那么这个模型的输出y或
y
^
\hat y
y^不过是你输入特征x的线性组合
如果 a[1] = z[1],
也就是 a[1] = W[1]x + b[1]
a[2] = z[2],
也就是 a[2] = W[2]a[1] + b[2]
即
a[2] = W[2](W[1]x + b[1]) + b[2]
=(W[2]W[1])x + (W[2]b[1] + b[2])
令W[2]W[1] = W’,(W[2]b[1] + b[2]) = b’
即上式为 W’x + b’
那么神经网络只是把输入线性组合再输出
如果你的神经网络有很多隐藏层,并且你使用线性激活函数或是没有激活函数。那么无论你的神经网络有多少层,只是一直在计算线性激活函数,那还不如直接去掉全部隐藏层。
对于一个双层神经网络,如果在第一层使用线性激活函数,在第二层使用sigmoid激活函数,那么这个模型的复杂度和没有任何隐藏层的标准逻辑回归是一样的。线性隐藏层一点用都没有,因为两个线性函数的组合本身就是线性函数,所以除非引入非线性,再多的网络层数也计算不出有趣的函数。
只有一个地方可以使用线性激活函数g(z)=z,也就是机器学习的回归问题。除了一些与压缩有关的非常特殊的情况,会在隐藏层用线性激活函数。除此之外使用线性激活函数非常少见。
【激活函数的导数】
当对神经网络使用反向传播的时候,需要计算激活函数的斜率或导数。
(1)sigmoid
对σ激活函数求导
d
d
z
g
(
z
)
=
1
1
+
e
−
z
(
1
−
1
1
+
e
−
z
)
=
g
(
z
)
(
1
−
g
(
z
)
)
\frac{d}{{dz}}g(z) = \frac{1}{{1 + {e^{ - z}}}}(1 - \frac{1}{{1 + {e^{ - z}}}}) = g(z)(1 - g(z))
dzdg(z)=1+e−z1(1−1+e−z1)=g(z)(1−g(z))
在这个式子中,如果z非常大,假设z=10
那么g(z)就接近1
d
d
z
g
(
z
)
\frac{d}{{dz}}g(z)
dzdg(z)就接近1·(1-1)=0
这实际上是对的,因为当z很大的时候,斜率接近0。
相反如果很小,例如z等于-10
那么g(z)接近0
d
d
z
g
(
z
)
\frac{d}{{dz}}g(z)
dzdg(z)就接近0·(1-0)=0
也是很接近于0,所以也是正确的。
如果z=0,那么g(z)=1/2
这就是sigmoid函数
这时导数
d
d
z
g
(
z
)
=
1
2
⋅
(
1
−
1
2
)
=
1
4
\frac{d}{{dz}}g(z) = \frac{1}{2} \cdot (1 - \frac{1}{2})=\frac{1}{4}
dzdg(z)=21⋅(1−21)=41
可以证明这是正确的导数值,或者说是z=0时正确的函数斜率。
(2)tanh
可以得到导数
g
′
(
z
)
=
d
d
z
g
(
z
)
=
1
−
(
tanh
(
z
)
)
2
g'(z)=\frac{d}{{dz}}g(z) = 1 - {(\tanh (z))^2}
g′(z)=dzdg(z)=1−(tanh(z))2
当z=10时,tanh(z)≈1,g’(z)≈0
所以当z很大的时候,斜率接近0
当z=-10,tanh(z)≈-1,g’(z)≈0
所以当z很小的时候,斜率接近0
当z=0,g’(z)=1
(3)ReLU、leaky ReLU
RELU:
g(z)=max(0,z)
当z<0,g’(z)=0
当z>0,g’(z)=1
当z=0,g’(z)没有定义
但是在软件中实现这个算法时,并不能百分百保证在数学上z为0,但是也有可能出现。如果z为0那么可以操作令导数为1或是0。
在优化技术上,g’变成了所谓的激活函数g(z)的次梯度,这就是梯度下降依然有效的原因。
但是z精确为0的概率非常小,所以将z=0处的导数设置成哪个值实际上无关紧要。所以在实践中,人们一般都这么做。
leaky ReLU:
g(z)=max(0.01z,z)
当z<0,g’(z)= 0.01
当z>0,g’(z)=1
z=0的梯度严格意义上是没有定义的,但是可以写一段代码来定义这个梯度,使z=0处g’(z)为0.01或1。
神经网络的梯度下降算法
【单层神经网络的参数(括号内为参数对应的维度)】
W[1] (n[1], n[0]),b[1] (n[1], 1),W[2] (n[2], n[1]),b[2] (n[2], 1)
输入特征数:
n
x
=
n
[
0
]
n_x=n^{[0]}
nx=n[0]
隐藏单元数:
n
[
1
]
n^{[1]}
n[1]
输出单元数:
n
[
2
]
n^{[2]}
n[2](在之前的例子中,值介绍过
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\limits_{i = 1}^n {L(\hat y,y)}
J(W[1],b[1],W[2],b[2])=m1i=1∑nL(y^,y)
L表示当神经网络预测出
y
^
\hat y
y^时的损失函数
y
^
\hat y
y^相当于之前算式中的a[2]
正确的标注等于y(ground truth labels)
如果做的是二分类,那么损失函数就和之前做逻辑回归完全一样,所以需要使用梯度下降法为算法训练参数。
在训练神经网络时,随机初始化参数很重要,不能将参数全都初始化为0。
当把参数初始化成某些值后,每个梯度下降循环都会计算预测值,所以需要计算预测值
y
^
(
i
)
,
i
=
1
,
.
.
.
,
m
\hat y^{(i)},i=1,...,m
y^(i),i=1,...,m,之后需要计算导数
d
W
[
1
]
=
∂
J
∂
W
[
1
]
,
d
b
[
1
]
=
∂
J
∂
b
[
1
]
,
.
.
.
d{W^{[1]}} = \frac{{\partial J}}{{\partial {W^{[1]}}}},d{b^{[1]}} = \frac{{\partial J}}{{\partial {b^{[1]}}}},...
dW[1]=∂W[1]∂J,db[1]=∂b[1]∂J,...
最终梯度下降更新会更新W[1],b[1],W[2],b[2]
W[1] := W[1] - α·dW[1]
b[1] := b[1] - α·db[1]
dW[1],db[1]为学习率,W[2]和b[2]同理
这就表示了梯度下降的一次迭代循环,训练过程中需要多次循环,直到得到的参数看起来收敛。
【计算导数方程】
正向传播:
Z[1] = W[1]X + b[1]
A[1] = g[1](Z[1])
Z[2] = W[2]A[1] + b[2]
A[2] = g[2](Z[2])=σ(Z[2])
反向传播(计算导数,针对所有样本的向量化):
dZ[2] = A[2] - Y
矩阵Y是(1×m)维矩阵
d
W
[
2
]
=
1
m
d
Z
[
2
]
A
[
1
]
T
d{W^{[2]}} = \frac{1}{m}d{Z^{[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
)
d{b^{[2]}} = \frac{1}{m}np.sum(d{Z^{[2]}},axis = 1,keepdims = True)
db[2]=m1np.sum(dZ[2],axis=1,keepdims=True)
这三个式子跟逻辑的梯度下降非常相似
np.sum是Python的numpy命令,用于对矩阵的一个维度求和,水平相加求和;keepdims为了防止Python直接输出这些古怪的秩为1的数组,其维度为(n,…)。加上keepdims=True来确保Python输出的是矩阵,使db[2]这个向量的输出维度为(n,1),准确的来讲应该是(n[2],1)
到这位置,所做的和逻辑回归非常相似,但是当开始计算反向传播时,需要计算
d
Z
[
1
]
=
W
[
2
]
T
d
Z
[
2
]
∗
g
[
1
]
′
(
Z
[
1
]
)
d{Z^{[1]}} = {W^{[2]T}}d{Z^{[2]}} * {g^{[1]}}'({Z^{[1]}})
dZ[1]=W[2]TdZ[2]∗g[1]′(Z[1])
g[1]'是在隐藏层使用的激活函数的导数
反向传播中的第一个式子是基于使用σ函数进行二分类为前提的,所以已经包含在dz[2]的式子里了
*指逐个元素乘积,所以(W[2]TdZ[2])是一(n[1],m)维矩阵,后半部分就是一个(n[1],m)维矩阵
d
W
[
1
]
=
1
m
d
Z
[
1
]
X
T
d{W^{[1]}} = \frac{1}{m}d{Z^{[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
)
d{b^{[1]}} = \frac{1}{m}np.sum(d{Z^{[1]}},axis = 1,keepdims = True)
db[1]=m1np.sum(dZ[1],axis=1,keepdims=True)
如果前面的n[2]=1,那么keepdims可能没那么重要,然而这里的db[1]维度为(n[1],1),为了不输出难以进行运算的古怪数组,除了使用keepdims参数外,还有另一种方法。但是要显式的调动reshape把np.sum输出结果写成矩阵形式,这样就得到了希望出现的矩阵形式。
直观理解反向传播
前一篇文章中讨论的逻辑回归中
正向传播先是得到z,之后是a,最后计算损失函数
之后反向传播,先计算da,然后是dz,最后计算dw和db
损失函数的定义是
L
(
a
,
y
)
=
−
y
log
a
−
(
1
−
y
)
log
(
1
−
a
)
L(a,y) = - y\log a - (1 - y)\log (1 - a)
L(a,y)=−yloga−(1−y)log(1−a)
对L求a的导得到了da
d
a
=
−
y
a
+
1
−
y
1
−
a
da = - \frac{y}{a} + \frac{{1 - y}}{{1 - a}}
da=−ay+1−a1−y
再往反向走一步,计算dz时
之前计算出了
d
z
=
a
−
y
dz = a - y
dz=a−y
事实上,使用微积分的链式法则
d
z
=
d
a
⋅
g
′
(
z
)
dz = da \cdot g'(z)
dz=da⋅g′(z)
其中
g
(
z
)
=
σ
(
z
)
g(z) = \sigma (z)
g(z)=σ(z)
即对L求z的导数
d
L
d
z
=
d
L
d
a
⋅
d
a
d
z
\frac{{dL}}{{dz}} = \frac{{dL}}{{da}} \cdot \frac{{da}}{{dz}}
dzdL=dadL⋅dzda
这里仍然是逻辑回归,有
x
1
,
x
2
,
x
3
x_1, x_2, x_3
x1,x2,x3,并且只有一个sigmoid单元可以得到
y
^
\hat y
y^和a,所以这里的激活函数是个sigmoid函数
当只有一个训练样本的时候,计算出dW和db
d
w
=
d
z
⋅
x
d
b
=
d
z
\begin{array}{l} dw = dz \cdot x\\ db = dz \end{array}
dw=dz⋅xdb=dz
这就是逻辑回归的反向传播。
当计算神经网络反向传播时,会做类似这样的计算。不过并非一次计算,而是进行了两次计算,因为在神经网络中,x并非直接连着输出单元,x首先进入了隐藏层,之后才连接到了输出单元。
所以和逻辑回归的一步计算不同,这里进行两步计算就像是神经网络内的两层。
双层神经网络中的反向传播做法:
向后推算出da[2],之后是dz[2],dW[2]和db[2],然后反向计算da[1],dz[1],dW[1]、db[1]。并不需要对输入x求导,因为监督学习的输入x是固定的,而我们并不想优化x,所以不用求导(至少在监督学习中,不会对x求导)。
da[2]和dz[2]的运算过程在这里同之前逻辑回归一样,所以可以得到
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
d
b
[
2
]
=
d
z
[
2
]
\begin{array}{l} dW^{[2]} = dz^{[2]} \cdot a^{[1]T}\\ db^{[2]} = dz^{[2]} \end{array}
dW[2]=dz[2]⋅a[1]Tdb[2]=dz[2]
(在这里a[2]为(n[2],1)维,dz[2]为(n[2],1)维,a[1]为(n[1],1),dW[2]为(n[2],n[1])维,db[2]为(n[2],1)维)
到这里就完成了反向传播的一半,接下来可以继续计算da[1],虽然在实践中,da[1]和dz[1]的计算通常合并成一步,所以实际编程时用的是
d
z
[
1
]
=
W
[
2
]
T
d
z
[
2
]
∗
g
[
1
]
′
(
z
[
1
]
)
d{z^{[1]}} = {W^{[2]T}}d{z^{[2]}} * {g^{[1]}}'({z^{[1]}})
dz[1]=W[2]Tdz[2]∗g[1]′(z[1])
这里由于使用的是编程符号,导致有点乱,过程参考以下公式
(在这里dz[1]的维度是(n[1],1),dz[2]的维度是(n[2],1),W[2]的维度是(n[2],n[1]),g[1]’(z[1])的维度是(n[1],1))
在这里需要知道任意变量foo和dfoo,它们的维度相同。为了维度匹配,需要使用矩阵的转置。
(探究矩阵求导为什么会出现转置,参考博文)
实现反向传播,必须确保矩阵的维度相互匹配。
继续计算得到
d
W
[
1
]
=
d
z
[
1
]
⋅
x
T
d
b
[
1
]
=
d
z
[
1
]
\begin{array}{l} d{W^{[1]}} = d{z^{[1]}} \cdot {x^T}\\ d{b^{[1]}} = d{z^{[1]}} \end{array}
dW[1]=dz[1]⋅xTdb[1]=dz[1]
可以发现和第二层的反向传播非常相似,因为x扮演了a[0]的角色,实际上x的转置就是a[0]的转置 。
至此对于双层神经网络的反向传播,得到了6个关键式子
在每次训练单个训练样本时,必须写出反向传播。但是实际上并不会一个个样本进行计算,所以要把所有训练样本向量化。
得到反向传播的向量化方程:
d
Z
[
2
]
=
A
[
2
]
−
Y
d{Z^{[2]}} = {A^{[2]}} - Y
dZ[2]=A[2]−Y
d
W
[
2
]
=
1
m
d
Z
[
2
]
A
[
1
]
T
d{W^{[2]}} = \frac{1}{m}d{Z^{[2]}}{A^{[1]T}}
dW[2]=m1dZ[2]A[1]T
这里的
1
m
\frac{1}{{\rm{m}}}
m1是因为成本函数
J
(
.
.
.
)
=
1
m
∑
i
=
1
n
(
y
^
,
y
)
{\rm{J(}}...{\rm{) = }}\frac{1}{m}\sum\limits_{i = 1}^n {(\hat y,y)}
J(...)=m1i=1∑n(y^,y)
所以求导后会有额外项
1
m
\frac{1}{{\rm{m}}}
m1
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
)
d{b^{[2]}} = \frac{1}{m}np.sum(d{Z^{[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
]
′
(
Z
[
1
]
)
d{Z^{[1]}} = {W^{[2]T}}d{Z^{[2]}}*{g^{[1]}}'\left( {{Z^{[1]}}} \right)
dZ[1]=W[2]TdZ[2]∗g[1]′(Z[1])
(在这里dZ[1]的维度是(n[1],m),W[2]TdZ[2]的维度是(n[1],m),g[1]’(Z[1])的维度是(n[1],m))
d
W
[
1
]
=
1
m
d
Z
[
1
]
X
T
d{W^{[1]}} = \frac{1}{m}d{Z^{[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
)
d{b^{[1]}} = \frac{1}{m}np.sum(d{Z^{[1]}},axis = 1,keepdims = True)
db[1]=m1np.sum(dZ[1],axis=1,keepdims=True)
至此得到了双层神经网络反向传播的向量化公式。
最后要提一点,事实证明在初始化参数的时候,使用随机初始化,不是将参数初始化为全零,将对神经网络的训练有非常重要的影响。
随机初始化
对于逻辑回归,可以将权重初始化为零。但如果将神经网络的个参数数组全部初始化为0,再使用梯度下降算法,那会完全无效。
下面我们来看看如果初始化权重为零,会出现什么情况
在这里,输入层n[0]=2,隐藏层n[1]=2,那么W[1]是一个2×2的矩阵
如果令
W
[
1
]
=
[
0
0
0
0
]
W^{[1]}=\begin{bmatrix} 0&0 \\ 0& 0 \end{bmatrix}
W[1]=[0000]
b
[
1
]
=
[
0
0
]
b^{[1]}=\begin{bmatrix} 0 \\ 0 \end{bmatrix}
b[1]=[00]
将偏置项b初始化为零实际上是可行的,但是将W初始化为零会出现问题
这种初始化形式的问题在于,在给网络输入任何样本时,
a
1
[
1
]
=
a
2
[
1
]
a^{[1]}_1=a^{[1]}_2
a1[1]=a2[1],它们经过激活函数计算得到的值完全一样,因为两个隐藏单元都在做完全一样的计算(w,b还有激活函数都相同)。
当进行反向传播时,事实证明,出于对称性
d
z
1
[
1
]
=
d
z
2
[
1
]
dz^{[1]}_1=dz^{[1]}_2
dz1[1]=dz2[1],两个隐藏单元会以同样的方式初始化。确切的讲,就是假设输出的权重也是一样的,即
W
[
2
]
=
[
0
0
]
W^{[2]}=\begin{bmatrix} 0 & 0\end{bmatrix}
W[2]=[00]
如果以这种方式初始化神经网络,那么隐藏单元
a
1
[
1
]
a^{[1]}_1
a1[1]和
a
2
[
1
]
a^{[1]}_2
a2[1]就完全一样了,也就是所谓的完全对称,意味着得出了完全一样的函数。
可以通过归纳法证明,每次进行迭代训练后,两个隐藏单元仍然在计算完全相同的函数,因为dW是这样一个矩阵
d
W
=
[
u
v
u
v
]
dW=\begin{bmatrix} u & v\\ u & v\end{bmatrix}
dW=[uuvv]
各行相同,然后执行一次权重更新
W
[
1
]
=
W
[
1
]
−
α
⋅
d
W
W^{[1]}=W^{[1]}-α·dW
W[1]=W[1]−α⋅dW
你会发现每次迭代后,W[1]的第一行和第二行都是完全一样的。由此可以通过归纳法来证明,如果将W的所有值都初始化为0,因为两个隐藏单元一开始就是在做同样的计算,所以两个隐藏单元对输出单元的影响也一样大。那么在一次迭代后,同样的对称性依然存在,两个隐藏单元仍然是对称的。之后不论迭代多少层,两个隐藏单元仍然在计算完全一样的函数。所以在这种情况下,多隐藏单元并没有实际意义。
同理隐藏层包含多个隐藏单元,其权重均初始化为零时,所有隐藏单元依然都是对称的。
【解决方案】
这个问题的解决方案就是随机初始化所有参数
可以令 W[1]=np.random.randn((2,2))*0.01
将权重初始化为很小的随机数
b并没有对称性问题,将b初始化为零是可以
b[1]=np.zeros((2,1))
因为只要W随机初始化,那么就可以使用不同的隐藏单元计算不同函数。
同理可以这样初始化W[2]和b[2]
W[2]=np.random.randn((1,2))*0.01
b[2]=0
在这里使用0.01,主要是因为通常偏向将权重矩阵初始化为非常小的随机值。由于如果使用的是tanh或sigmoid激活函数,或者在输出层有一个sigmoid函数,这里权重太大会导致经过tanh和sigmoid函数运算得到的值,掉在了函数的平缓部分,梯度的斜率非常小,最终结果意味着梯度下降法会非常慢,减慢学习速度。
实际上,有比0.01更好用的常数。不过在这里训练的是一个单隐藏层的神经网络,设为0.01还是可以的。当训练一个很的神经网络时,可能就需要尝试0.01之外的常数了。
课后作业
【测验】
存在问题
题(1)
a
4
[
2
]
a^{[2]}_4
a4[2]指第二层中的第四个结点的激活函数值
a
[
2
]
(
12
)
a^{[2](12)}
a[2](12)指第12个训练样本的第二层得到的激活函数值
题(7)
逻辑回归没有隐藏层。如果将权重初始化为零,那么逻辑回归中第一个样本x将输出零。但是逻辑回归的导数依赖于非零的输入x,所以在第二次迭代中,权重遵循了x的分布并且当x不是常向量时彼此之间分布不同。
【编程作业】
【Logistic回归部分】
(1) numpy.squeeze(a, axis = None)
从数组的shape中,去掉维度为1的条目。
a表示输入的数组
axis:用于删除数组shape中指定的一维项,指定项必须是一维的,否则会报错
举例说明:
# shape
import numpy as np
a = np.array([1,2,3])
b = np.array([[1],[2],[3]])
c = np.array([[1,2,3],[4,5,6]])
d = np.array([[1],[2,3],[4,5,6]])
print ("a.shape=", a.shape)
print ("b.shape=", b.shape)
print ("c.shape=", c.shape)
print ("c.shape[0]=", c.shape[0])
print ("c.shape[1]=", c.shape[1])
print ("d.shape=", d.shape)
print ("d.shape[0]=", d.shape[0])
# output
a.shape= (3,)
b.shape= (3, 1)
c.shape= (2, 3)
c.shape[0]= 2
c.shape[1]= 3
d.shape= (3,)
d.shape[0]= 3
# squeeze
import numpy as np
a = np.array([[[1,2,3],[4,5,6],[7,8,9]]])
b = np.array([[[1],[2],[3]]])
print("a.shape=", a.shape)
print("a=", a)
print("np.squeeze(a)=", np.squeeze(a))
print("np.squeeze(a,axis=0)=", np.squeeze(a,axis=0))
print("b.shape=", b.shape)
print("b=", b)
print("np.squeeze(b)=", np.squeeze(b))
print("np.squeeze(b,axis=0)=", np.squeeze(b,axis=0))
print("np.squeeze(b,axis=2)=", np.squeeze(b,axis=2))
# output
a.shape= (1, 3, 3)
a= [[[1 2 3]
[4 5 6]
[7 8 9]]]
np.squeeze(a)= [[1 2 3]
[4 5 6]
[7 8 9]]
np.squeeze(a,axis=0)= [[1 2 3]
[4 5 6]
[7 8 9]]
b.shape= (1, 3, 1)
b= [[[1]
[2]
[3]]]
np.squeeze(b)= [1 2 3]
np.squeeze(b,axis=0)= [[1]
[2]
[3]]
np.squeeze(b,axis=2)= [[1 2 3]]
从得出的结果可以看出,squeeze操作实际上就是将数据中一维的括号去掉了
(2)matplotlib.pyplot.scatter()
plt.scatter(X[0, :], X[1, :], c=np.squeeze(Y), s=40, cmap=plt.cm.Spectral)
X[0, :], X[1, :]:取出数据集X中的两类数据,描点表示
c:标记颜色
用于将两类数据的颜色区分开
下图为去掉 c=np.squeeze(Y)得到的结果
s:标记大小
下图是s为20的情况
cmap:色彩盘,colormap是MATLAB里面用来设定和获取当前色图的函数
(3)DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
在预计一维数组的情况下,传入了列向量y。请使用例如ravel()的函数改变y的维度。
(4)numpy.ravel()、numpy.flatten()、numpy.squeeze()
ndarray.flatten(order=‘C’):
返回一份数组拷贝,对拷贝所做的修改不会影响原始数组。
order:‘C’ – 按行,‘F’ – 按列,‘A’ – 原顺序,‘K’ – 元素在内存中的出现顺序。
numpy.ravel(a, order=‘C’):
展平的数组元素,顺序通常是"C风格",返回的是数组视图,修改会影响原始数组。
numpy.squeeze 函数从给定数组的形状中删除一维的条目。
(5)在已给的代码包下,文件planar_utils.py里的plot_decision_boundary函数,修改该函数最后一句plt.scatter(X[0, :], X[1, :], c=y, cmap=plt.cm.Spectral),c=y改为c=np.squeeze(y)或者c=y.ravel()
(6)numpy.meshgrid()从坐标向量中返回坐标矩阵,在实例 np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))中,其中h=0.01。表示将x和y的范围按0.01精度进行分割,并将分割出的横纵坐标分别存在xx和yy矩阵中.
(7)numpy.c_:按行转换成矩阵;numpy.r_:按列转换成矩阵:
import numpy as np
a = np.array([[1,2,3],[4,5,6]])
b = np.array([[7,8,9],[10,11,12]])
c = np.c_[a,b]
r = np.r_[a,b]
print("c=",c)
print("r=",r)
# output
c= [[ 1 2 3 7 8 9]
[ 4 5 6 10 11 12]]
r= [[ 1 2 3]
[ 4 5 6]
[ 7 8 9]
[10 11 12]]
(8)lambda表达式,用于定义一个匿名函数,匿名函数的内容简单,例如
in: add = lambda x, y : x+y
in: add(1,2)
out: 3
详情及应用
plot_decision_boundary(lambda x: clf.predict(x), X, Y)
def plot_decision_boundary(model, X, y):
Z = model(np.c_[xx.ravel(), yy.ravel()])
这三条语句配合起来就是
model = lambda x: clf.predict(x)
clf.predict(np.c_[xx.ravel(), yy.ravel()])
clf.predict()是用训练得到的模型对按0.01精度分割的坐标点进行预测,返回预测结果。此时得到结果Z,Z.shape= (1038240,)。
对Z进行变形,Z.reshape(xx.shape),xx.shape=(1008, 1030),即将Z变形为1008×1030阶矩阵
(9)matplotlib.pyplot contourf()
用于绘制等高线,contour和contourf都是画三维等高线图的,不同点在于contour() 是绘制轮廓线,contourf()会填充轮廓。
下图分别为使用contour和contourf函数所绘制的图片
(10)numpy.dot()返回两数组的点积
处理一维数组,返回两数组的内积,即对应位置相乘再相加。
处理二维数组,返回矩阵积,即完成矩阵乘法。
float((np.dot(Y, LR_predictions) + np.dot(1 - Y,1 - LR_predictions))
在这里是将真正的标记和预测出的标记做内积,可以的到预测准确的数量,前半部分是预测是真实标记均为1的数量,后半部分是预测和真实标记均为0的数量。
(11)使用logistic回归算法建立的模型测试的准确性只有47%,原因是logistic回归的决策边界是线性的,而从图里明显可以看出数据的分布是非线性的。【注意线性回归方程并非线性方程,一个线性回归模型不需要是自变量的线性函数——线性回归方程】
【神经网络部分】
(1)numpy.multiply():数组和矩阵对应位置相乘,输出与相乘数组/矩阵的大小一致。
(2)numpy.sum(a, axis=1, keepdims=True):
axis 可选参数,默认None,可以是整数或者整数元组,用于某个轴或多个轴方向上进行求和运算。
keepdims 布尔类型的可选参数(keep dimensions),默认False,被删去的维度在结果矩阵中就被设置为一。函数详解
详解的例子中axis=0的情况下是从第二维层面,对应相加再组合。即
[
0
+
12
1
+
13
2
+
14
3
+
15
4
+
16
.
.
.
.
.
.
.
.
.
8
+
20
.
.
.
.
.
.
.
.
.
]
\begin{bmatrix} 0+12&1+13&2+14&3+15 \\ 4+16& ...& ...& ...\\ 8+20&...& ...& ... \end{bmatrix}
⎣⎡0+124+168+201+13......2+14......3+15......⎦⎤
维度:1×3×4(axis=-3结果同axis=0)
axis=1的情况下是从第三维层面,对应位置相加再组合。即
[
0
+
4
+
8
1
+
5
+
9
2
+
6
+
10
3
+
7
+
11
12
+
16
+
20
.
.
.
.
.
.
.
.
.
]
\begin{bmatrix} 0+4+8&1+5+9&2+6+10&3+7+11 \\ 12+16+20&...&...&... \end{bmatrix}
[0+4+812+16+201+5+9...2+6+10...3+7+11...]
维度:2×4(axis=-2结果同axis=1)
axis=2的情况下是从第四维层面,对应位置相加再组合。即
[
0
+
1
+
2
+
3
4
+
5
+
6
+
7
8
+
9
+
10
+
11
12
+
13
+
14
+
15
.
.
.
.
.
.
]
\begin{bmatrix} 0+1+2+3&4+5+6+7&8+9+10+11 \\ 12+13+14+15&...&... \end{bmatrix}
[0+1+2+312+13+14+154+5+6+7...8+9+10+11...]
维度:2×3(axis=-1结果同axis=2)
import numpy as np
a = np.arange(24).reshape(2,3,4)
print("a:\n", a)
print("np.sum(a, axis=0):\n", np.sum(a, axis=0))
print("np.sum(a, axis=1):\n", np.sum(a, axis=1))
print("np.sum(a, axis=2):\n", np.sum(a, axis=2))
print("np.sum(a, axis=-1):\n", np.sum(a, axis=-1))
print("np.sum(a, axis=-2):\n", np.sum(a, axis=-2))
print("np.sum(a, axis=-3):\n", np.sum(a, axis=-3))
# 输出结果
a:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
np.sum(a, axis=0):
[[[12 14 16 18]
[20 22 24 26]
[28 30 32 34]]]
np.sum(a, axis=1):
[[12 15 18 21]
[48 51 54 57]]
np.sum(a, axis=2):
[[ 6 22 38]
[54 70 86]]
np.sum(a, axis=-1):
[[ 6 22 38]
[54 70 86]]
np.sum(a, axis=-2):
[[12 15 18 21]
[48 51 54 57]]
np.sum(a, axis=-3):
[[12 14 16 18]
[20 22 24 26]
[28 30 32 34]]
# keepdims = True 时
# 输出结果
a:
[[[ 0 1 2 3]
[ 4 5 6 7]
[ 8 9 10 11]]
[[12 13 14 15]
[16 17 18 19]
[20 21 22 23]]]
np.sum(a, axis=0,keepdims=True):
[[[12 14 16 18]
[20 22 24 26]
[28 30 32 34]]]
np.sum(a, axis=1,keepdims=True):
[[[12 15 18 21]]
[[48 51 54 57]]]
np.sum(a, axis=2,keepdims=True):
[[[ 6]
[22]
[38]]
[[54]
[70]
[86]]]
np.sum(a, axis=-1,keepdims=True):
[[[ 6]
[22]
[38]]
[[54]
[70]
[86]]]
np.sum(a, axis=-2,keepdims=True):
[[[12 15 18 21]]
[[48 51 54 57]]]
np.sum(a, axis=-3,keepdims=True):
[[[12 14 16 18]
[20 22 24 26]
[28 30 32 34]]]
axis | 0 | 1 | 2 |
---|---|---|---|
维度 | 1×3×4 | 2×1×4 | 2×3×1 |
(3)numpy.power(x1, x2):
x1求x2对应位置该值的次方,x2可以是数字或数组,当x2是数组时,要求x1和x2的列数相同
import numpy as np
x1 = np.arange(8).reshape(1,8)
x2 = np.array([[1,2,0,3,2,2,3,0]])
print("x1:\n",x1)
print("x2:\n",x2)
print("np.power(x1,2)\n",np.power(x1,2))
print("np.power(x1,x2)\n",np.power(x1,x2))
# output
x1:
[[0 1 2 3 4 5 6 7]]
x2:
[[1 2 0 3 2 2 3 0]]
np.power(x1,2):
[[ 0 1 4 9 16 25 36 49]]
np.power(x1,x2):
[[ 0 1 1 27 16 25 216 1]]
(4)(tanh(x))’=1-tanh2(x)
(5)nn_model()函数下的parameters = update_parameters(parameters,grads,learning_rate = 0.5)里的学习率应改为learning_rate = 1.2,来同update_parameters()函数相匹配,anaconda3下的Python3.7并不会报错。
(6)plt.figure():在plt中绘制一张图片
plt.subplot:创建单个子图
(7)在隐藏层结点为4的情况下,改变学习率
# ************ grads,learning_rate = 0.1 ************
# ********************** output *********************
第 0 次循环,成本为:0.6930480201239823
第 1000 次循环,成本为:0.6082072899772629
第 2000 次循环,成本为:0.36073810801711903
第 3000 次循环,成本为:0.3290211712344614
第 4000 次循环,成本为:0.3169254361704178
第 5000 次循环,成本为:0.30973342826059314
第 6000 次循环,成本为:0.3046867806714765
第 7000 次循环,成本为:0.30080373411176864
第 8000 次循环,成本为:0.2976318125152893
第 9000 次循环,成本为:0.29493076176193456
准确率: 89%
# ************ grads,learning_rate = 0.5 ************
# ********************** output *********************
第 0 次循环,成本为:0.6930480201239823
第 1000 次循环,成本为:0.3098018601352803
第 2000 次循环,成本为:0.2924326333792646
第 3000 次循环,成本为:0.2833492852647412
第 4000 次循环,成本为:0.27678077562979253
第 5000 次循环,成本为:0.2634715508859308
第 6000 次循环,成本为:0.24204413129940755
第 7000 次循环,成本为:0.23552486626608762
第 8000 次循环,成本为:0.2314096450985427
第 9000 次循环,成本为:0.22846408048352362
准确率: 90%
# ************ grads,learning_rate = 1.0 ************
# ********************** output *********************
第 0 次循环,成本为:0.6930480201239823
第 1000 次循环,成本为:0.29229023562502554
第 2000 次循环,成本为:0.276357149207868
第 3000 次循环,成本为:0.24083501866130597
第 4000 次循环,成本为:0.23106003194293667
第 5000 次循环,成本为:0.2260242509095667
第 6000 次循环,成本为:0.22267730382455533
第 7000 次循环,成本为:0.22018493619014953
第 8000 次循环,成本为:0.21820806978190718
第 9000 次循环,成本为:0.2165878755163115
准确率: 90%
# ************ grads,learning_rate = 10 ************
# ********************** output *********************
第 0 次循环,成本为:0.6930480201239823
第 1000 次循环,成本为:0.2810720970389547
第 2000 次循环,成本为:0.26576363650532914
第 3000 次循环,成本为:0.2580216345974393
第 4000 次循环,成本为:0.2533743499376164
第 5000 次循环,成本为:0.2503057082048681
第 6000 次循环,成本为:0.2481490876972756
第 7000 次循环,成本为:0.24654832535042956
第 8000 次循环,成本为:0.24530008619469737
第 9000 次循环,成本为:0.24434823473941694
准确率: 90%
(8)以下改变第一次激活的激活函数方法仅供参考,由于得出的结果个人认为很奇怪,如果有大佬发现错误请指正,非常感谢。
修改第一次激活的激活函数为sigmoid时,不同学习率下得到的结果
# planar_utils.py中
# 修改函数forward_propagation中
A1 = sigmoid(Z1)
# 修改函数backward_propagation中
dZ1 = np.multiply(np.dot(W2.T, dZ2), dsigmoid(A1))
修改第一次激活的激活函数为ReLU时,不同学习率下得到的结果
# 修改函数forward_propagation中
A1 = np.maximum(Z1, 0)
# 修改函数backward_propagation中
dZ1 = np.multiply(np.dot(W2.T, dZ2), np.where(A1>0, 1, 0))