浅层神经网络
神经网络概述
之前讨论了逻辑回归,模型如下图。
公式为:
xwb⎭⎪⎬⎪⎫⟹z=wTx+b
如图,首先输入特征x,参数w,b,然后计算出z,公式为:
xwb⎭⎪⎬⎪⎫⟹z=wTx+b⟹a=σ(z)⟹L(a,y)
神经网络看起来是下面这个样子:
注:字母的上标[i]代表神经网络中的第i层(layer)
使用括号(i)作为上标,代表第i个训练样本,不要弄混。
可以把许多sigmoid
单元堆叠起来,形成一个神经网路。
在这个神经网络对应的三个节点中,首先计算第一层网络中各个节点的z[1],接着计算a[1],计算下一层网络同理。
在第一层中,计算公式为:
xW[1]b[1]⎭⎪⎬⎪⎫⟹z[1]=W[1]x+b[1]⟹a[1]=σ(z[1])
在第二层中,计算公式为:
使用第一层中的输出a[1]作为第二层的输入。
a[1]W[2]b[2]⎭⎪⎬⎪⎫⟹z[2]=W[2]a[1]+b[2]⟹a[2]=σ(z[2])⟹L(a[2],y)
此时a[2]就是整个神经网络最终的输出。
神经网络的表示
上图是一个神经网络的例子。
有输入特征x1,x2,x3,竖直地堆叠起来,叫做神经网络的输入层(input layer)。输入层右边有另外一层,称为隐藏层(hidden layer),最后只有一个结点的一层称为输出层(output layer)。
其中,隐藏层的含义为:在一个神经网络中,当使用监督学习训练它的时候,训练集包含了输入x和目标输出y,所以隐藏层表示在训练集中,这些中间节点的准确值是不知道的。可以看到输入的值,也能看到输出的值,但是隐藏层中的数据,在训练集中是无法看到的。
再引入几个符号。
就像之前使用向量x表示输入特征,这里有个可代替的记号a[0]可表示输入特征,a表示激活(activate)的意思。它意味着网络中不同层的值会传递到它们后面的层中,下一层隐藏同样会产生一些激活值,记为a[1],以此类推。
所以隐藏层的激活值可以写为一个四维向量或维度为4x1
的矩阵。
a[1]=⎣⎢⎢⎢⎢⎡a1[1]a2[1]a3[1]a4[1]⎦⎥⎥⎥⎥⎤
最后输出层产生某个数值a[2],是一个单独的实数。
上图的是一个二层的神经网络,原因是当计算网络层数时,**输入层不算入总层数。**所以隐藏层是第一次层,输出层是第二层。
(无论在阅读研究论文还是在这门课中,都遵循此惯例。)
最后,隐藏层和最后的输出层是带有参数的,这里的隐藏层拥有两个参数W[1]和b[1],表示这些参数和第一层输入层有关系。
这里的W[1]是一个4x3
的矩阵(隐藏层有四个神经元,每一个神经元都对应了输入x1,x2,x3,因此每个神经元有三个参数),而b[1]是一个4x1
的向量。
类似的,输入层也有相关联的参数W[2],b[2]。它们的维数分别为:1x4
和1x1
。
计算一个神经网络的输出
关于神经网络是如何计算的,首先从之前学到的逻辑回归开始。
如图。
逻辑回归计算有两个步骤,首先根据特征输入向量和参数计算z,然后以sigmoid
函数为激活函数计算a。
一个神经网络只是做了很多次这种重复计算。
回到刚才提到的两层神经网络。
从隐藏层的一个神经元开始计算。
和逻辑回归运算一样分为两步。
- 计算z1[1], z1[1]=w1[1]Tx+b1[1]
- 通过激活函数
sigmoid
,计算a1[1]=σ(z1[1])
隐藏层的其他几个神经元也是这样计算,只是表示符号不同。
详细结果为:
z1[1]=w1[1]Tx+b1[1],a1[1]=σ(z1[1])z2[1]=w2[1]Tx+b2[1],a2[1]=σ(z2[1])z3[1]=w3[1]Tx+b3[1],a3[1]=σ(z3[1])z4[1]=w4[1]Tx+b4[1],a4[1]=σ(z4[1])
向量化计算
将上述公式向量化。
说先将隐藏层中的参数w堆积起来,变成一个(4,3)
的矩阵,使用符号W[1]表示,偏置b变成一个(4,1)
的矩阵,用符号b[1]表示。
那么公式为:z[n]=w[n]x+b[n], a[n]=σ(z[n])。
详细公式为:
z[1]=⎣⎢⎢⎢⎢⎡z1[1]z2[1]z3[1]z4[1]⎦⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎡⋯⋯⋯⋯W1[1]TW2[1]TW3[1]TW4[1]T⋯⋯⋯⋯⎦⎥⎥⎥⎥⎤W[1]4×3的矩阵⎣⎢⎡x1x2x3⎦⎥⎤+⎣⎢⎢⎢⎢⎡b1[1]b2[1]b3[1]b4[1]⎦⎥⎥⎥⎥⎤b[1]
a[1]=⎣⎢⎢⎢⎢⎡a1[1]a2[1]a3[1]a4[1]⎦⎥⎥⎥⎥⎤=σ(z[1])
多样本向量化(Vectorizing across multiple examples)
在上个视频中,了解了针对单一的训练样本,在神经网络上计算出预测值。
接下来,将说明如何向量化多个训练样本,并计算结果。该过程与逻辑回归中类似。
同样以上面给出的两层神经网络的隐藏层为例。
首先将m个训练样本的特征输入向量写成一个矩阵。
X=⎣⎢⎢⎢⎡⋮x(1)⋮⋮x(2)⋮⋮⋯⋮⋮x(m)⋮⎦⎥⎥⎥⎤
隐藏层的两个参数w,b为:
W[1]=⎣⎢⎢⎢⎢⎡⋯⋯⋯⋯W1[1]TW2[1]TW3[1]TW4[1]T⋯⋯⋯⋯⎦⎥⎥⎥⎥⎤b[1]=⎣⎢⎢⎢⎢⎡b1[1]b2[1]b3[1]b4[1]⋯⋯⋯⋯⎦⎥⎥⎥⎥⎤
第一个式子:每一行代表一个神经元,每行各分量对应一个输入特征。
第二个式子:首列各分量代表四个神经元的偏置b,后面所有列与第一列相同。
矩阵b[1]每一列分别对应一个训练样本,不同训练样本对于同一个神经元的偏置是相同的,因此矩阵后面各列与首列相同。
函数输出z写成矩阵:
Z[1]=⎣⎢⎢⎢⎡⋮z[1](1)⋮⋮z[1](2)⋮⋮⋯⋮⋮z[1](m)⋮⎦⎥⎥⎥⎤
注意区分[]
和()
。
预测值a写成矩阵:
A[1]=⎣⎢⎢⎢⎡⋮a[1](1)⋮⋮a[1](2)⋮⋮⋯⋮⋮a[1](m)⋮⎦⎥⎥⎥⎤
分析矩阵A[1]每一个元素的含义。
例如矩阵A中第一行第一列的元素,代表隐藏层中,第一个神经元对第一个训练样本的预测输出值。
例如矩阵A中第二行第一列的元素,代表隐藏层中,第二个神经元对第一个训练样本的预测数值。
总的来说,从水平方向(或横向)看,矩阵A代表了各个训练样本,从竖直方向看,矩阵A的不同索引对应不同的神经元。
至此,完成了一个神经网络的前向传播。
激活函数
使用神经网络时,需要决定哪种激活函数用在隐藏层上,哪种用在输出层。到目前为止,只是用过sigmoid
激活函数。但是有时其他的激活函数效果会更好。
在神经网络的前向传播中,在a[1]=σ(z[1]),a[2]=σ(z[2])这两步使用到了sigmoid
函数,在这里被称为激活函数。
更通常的情况下,使用不同的激活函数g(z[1]),g可以是除了sigmoid
函数以外的其他非线性函数。例如双曲正切函数:
tanh(z)=ez+e−zez−e−z
该函数的值域是(-1, 1)
。
tanh
函数在总体上要优于sigmoid
函数。
事实上,tanh
函数是sigmoid
函数向下平移和伸缩的结果。
对它变形后,穿过了(0,0)
点。
函数图像为:
结果表明,如果在隐藏层中使用tanh
双曲正切函数,效果总是优于sigmoid
函数。并且因为值域为(-1, 1)
,因此均值更接近0。
有一点要说明:现在几乎不会使用到sigmoid
激活函数,tanh
函数在所有场合都优于sigmoid
函数。但是一个例外是在二分类问题中,因为输出层y的值为0或1,可以对输出层使用sigmoid
函数。
所以,在不同的神经网络层中,激活函数可以不同。
在不同层中,使用不同的上标来标明激活函数。如g[1]表示第一层使用的激活函数。
sigmoid
函数和tanh
函数共同的缺点是:在z特别大或特别小的情况下,激活函数导数梯度会变得非常小,最后接近于0,这会降低梯度下降的速度。
另外一个很流行的函数是:ReLu
函数,图像如下图。
公式为:f(x)=max(0,x)。
有一些选择激活函数的经验法则:
如果是二分类问题(输出是0,1),则输出层选择sigmoid
函数,其他所有层都选择Relu
函数。
这是很多激活函数的默认选择,即 如果在隐藏层中不确定使用哪个激活函数,那么通常会使用Relu
函数。
该函数的一个优点是:当z是负值是,导数为0。
也有另外一个版本的Relu
被称为Leaky Relu
。
当z是负值时,函数值不是0,而是微微向y轴负方向倾斜。
公式可以为:f(x)=max(0.1x,x)。这里的0.1
可以为其他值。
这两个Relu
函数的优点是:
- 在
z
的区间变动很大的情况下,激活函数的导数或斜率会远大于0。并且在程序中可以很容易使用if-else
语句实现。而sigmoid
需要浮点四则运算。在实践中,使用Relu
的神经网络通常会比使用sigmoid
或tanh
学习的更快。 sigmoid
和tanh
函数的导数在正负饱和区梯度都接近于0,这会导致梯度弥散。而Relu
和Leaky Relu
函数大于0的部分都为常数,不会产生梯度弥散的现象。(当Relu进入负半区时,梯度为0,神经元不会训练,产生所谓稀疏性,Leaky Relu
不会有这个问题)。
概括一下不同激活函数。
sigmoid
函数:除了输入层是一个二分类问题,基本不会使用。tanh
函数:tanh
很优秀,几乎适合所有场合。Relu
函数:最常用的默认函数。如果不确定用哪个激活函数,就是用Relu
或Leaky Relu
。其中Leaky Relu
函数f(x)=max(ax,x),可以为不同算法选择不同的参数a(0 < a < 1)
在深度学习中经常遇到的问题时,在编写神经网络时,会有很多选择:隐藏层单元的个数,激活函数的选择,初始化权重。这些选择想得到一个比较好的指导原则比较困难。
因此通常的建议是,如果不确定哪一个激活函数效果更好,可以都试试,然后在验证集上评价。
为什么要使用非线性激活函数
假如使用线性激活函数,例如令a[1]=z[1],也就是令激活函数g(z)=z,也被称为线性激活函数(更学术的名字是恒等激活函数,因为只是把输入值输出)。同样,为了说明问题把a[2]=z[2],那么这个模型的输出y仅仅只是输入特征的一个线性组合,可以由z=wx+b来计算。这里不再给出计算过程(很简单,把第一层的变线性函数带入第二层的a[2]=z[2])。
事实证明,如果使用线性激活函数或者没有使用激活函数,那么无论神经网络有多少层,一直在做的只是计算线性函数,所以不如直接去掉所有隐藏层。
激活函数的导数
在神经网络中使用反向传播时,需要计算激活函数的导数或斜率。针对上面讲到的四种激活函数,分别计算导数(就是个求导的事儿)。
sigmoid
函数
g(z)=1+e−z1g(z)′=dzdg(z)=(1+e−z)2e−z=g(z)(1−g(z))
tanh
函数
g(z)=tanh(z)=ez+e−zez−e−zg(z)′=dzdg(z)=(ez+e−z)24=1−(tanh(z))2
ReLU
g(z)=max(0,x)g(z)′=⎩⎪⎪⎨⎪⎪⎧0z<01z>0undefinedz=0
通常该函数在z=0处不可导,但是通常在z=0时给定其导数1
或0
,当然z=0的情况很少。
Leaky ReLU
g(z)=max(0.01z,z)g(z)′=⎩⎪⎪⎨⎪⎪⎧0.01z<01z>0undefinedz=0
同样,可在z=0处给定导数0.01
或1
。
神经网络的梯度下降
在这部分,说明神经网络的反向传播。
首先,nx或n[0]表示输入特征个数,即输入层特征变量个数;n[1]表示隐藏单元个数(隐藏层神经元个数),n[2]表示输出单元个数(输出层神经元个数)。
有参数:W[1](维度(n[1],n[0])),b[1](维度(n[1],1)),W[2](维度(n[2],n[1])),b[2](维度(n[2],1))
神经网络成本函数,假设在做二分类问题,那么代价函数为:
J(W[1],b[1],W[2],b[2])=m1i=1∑mL(a,y)
损失函数L和逻辑回归中完全一样。
L(a,y)=−ylog(a)−(1−y)log(1−a)
每次梯度下降,需要循环计算下列值:
dW[1]=dW[1]dJdb[1]=db[1]dJdW[2]=dW[2]dJdb[2]=db[2]dJ
然后更新参数:
W[1]=W[1]−αdW[1]b[1]=b[1]−αdb[1]W[2]=W[2]−αdW[2]b[2]=b[2]−αdb[2]
总结
正向传播相关公式如下:
z[1]=W[1]x+b[1]a[1]=σ(z[1])z[2]=W[2]a[1]+b[2]a[2]=σ(z[2])
反向传播相关公式如下:
dz[2]=A[2]−YdW[2]=m1dz[2]A[1]Tdb[2]=m1np.sum(dz[2],axis=1,keepdims=True)dz[1]=(n[1],m)W[2]Tdz[2]∗(n[1],m)g[1]′(z[1])dW[1]=m1dz[1]xTdb[1]=m1np.sum(dz[1],axis=1,keepdism=True)
(axis=1
参数表示水平相加,keepdims
参数防止python
输出奇怪秩数)。
*
是矩阵点乘,即维数相同矩阵对应位置相乘,满足交换律。
其中dz[1]比较难算。
因为z[1]是隐藏层的输出,而输出层的输出z[2]与代价函数有直接联系,因此需要链式求导才可算出dz[1]。
首先有:
dz[1]=dz[1]dJ=dz[2]dJdz[1]dz[2]z[2]=W[2]a[1]+b[2]=W[2]g[1](z[1])+b[2]
其中g[1]表示隐藏层使用的激活函数。
因此可得到z[2]对z[1]的导数。
注意:这里的乘法是数值意义的乘法,不是矩阵乘法,因此写成点乘。
dz[1]dz[2]=W[2]∗g[1]′(z[1])
代入上面链式求导公式得到:
dz[1]=W[2]Tdz[2]∗g[1]′(z[1])
也可根据下图,来更好理解上面公式中的链式求导。
随机初始化
当训练神经网络时,权重参数的初始化是很重要的。对于逻辑回归,把权重全初始化为0是可以的。但是对于神经网络,如果把权重参数全部初始化为0,则梯度下降不会起作用。
举一个例子,有两个输入特征,则n[0]=2,2两个隐藏层单元,n[1]=2。那么隐藏层的权重参数矩阵W[1]是一个2x2的矩阵,如果把该矩阵所有参数初始化为0,偏置b[1]也初始化为0,[0,0]T。
把偏置项置为0是可以的,但是把权重参数w置为0就有问题了
那么这样计算的话,会发现隐藏层的两个单元的输出a1[1],a2[1]相等。因为这两个隐含单元计算同样的函数,参数也想通。并且做反向传播时,dz1[1],dz2[1]也会相等。
因此如果把权重都初始化为0,那么所有隐含单元的计算都是相同的。无论运行梯度下降多少次,都会一直计算相同的函数。
那么如何初始化呢。
解决方法就是随机初始参数。可以使用np.random.randn(2,2)
(生成高斯分布),通常会再乘上一个较小的数,例如0.01
。
例如上面例子:
W[1]=np.random.randn(2,2)∗0.01,b[1]=np.zeros((2,1))。
那么为什么要乘上这个常数0.01
。
这是因为如果使用激活函数tanh
或sigmoid
,当参数值很大时,求出的z值就会很大或很小,这种情况下,z就会落在sigmoid
或tanh
很平坦的地方,这些位置梯度很小,从而造成梯度消失。
而梯度很小,梯度下降就会很慢,从而造成参数收敛很慢。
事实上,有比0.01
更好的参数。当训练只有一个隐藏层的网络时,可以使用0.01
。
但当训练一个非常非常深的网络时,可能要试试其他常数。