在上节课中,我们学习了逻辑回归——一种经典的学习算法。我兴致勃勃地用它训练了一个猫狗分类模型,结果只得到了57%这么个惨淡的准确率。正好,这周开始学习如何实现更复杂的模型了。这次,我一定要一雪前耻!

开始学这周的课之前,先回忆一下上周我们学习了什么。

对于一个神经网络,我们要定义它的网络结构(一个数学公式),定义损失函数。根据损失函数和网络结构,我们可以对网络的参数求导,并用梯度下降法最小化损失函数。

也就是说,不管是什么神经网络,都由以下几部分组成:

  • 网络结构
  • 损失函数
  • 优化策略

而在编程实现神经网络时,我们不仅要用计算机语言定义上面这几项内容,还需要收集数据预处理数据

在这堂课中,我们要学一个更复杂的模型,其知识点逃不出上面这些范围。在之后的学习中我们还会看到,浅层神经网络的损失函数优化策略和上节课的逻辑回归几乎是一模一样的。我们要关心的,主要是网络结构上的变化。

在学习之前,我们可以先有一个心理准备,知道大概要学到哪些东西。

前排提示:本文篇幅较长。如果想看本文的精简版,欢迎移步我在其他地方发的文章.

课堂笔记

神经网络概述与符号标记

上节课我们使用的逻辑回归过于简单,它只能被视为只有一个神经元(计算单元)的神经网络。如上图第一行所示。

一般情况下,神经网络都是由许多神经元组成的。我们把一次性计算的神经元都算作“一层”。比如上图第二行的网络有两层,第一层有3个神经元,第二层有1个神经元。

上节课中,对于一个样本xxx,一层的神经网络是用下面的公式计算的:

y^=a=σ(wTx+b)\hat{y}=a=\sigma(w^Tx+b) y^​=a=σ(wTx+b)

而这节课将使用的两层神经网络,也使用类似的公式计算:

a[1]=σ(W[1]x+b[1])y^=a[2]=σ(W[2]a[1]+b[2])\begin{aligned} a^{[1]} & = \sigma(W^{[1]}x+b^{[1]}) \\ \hat{y}=a^{[2]} & = \sigma(W^{[2]}a^{[1]}+b^{[2]}) \end{aligned} a[1]y^​=a[2]​=σ(W[1]x+b[1])=σ(W[2]a[1]+b[2])​

上节课中,参数www是一个列向量。这节课的参数WWW是一个矩阵。我们稍后会见到WWW的全貌。

这里的方括号上标[l][l][l]表示第lll层相关的变量。总结一下,ai[j](k)a_i^{[j](k)}ai[j](k)​表示第kkk个样本在网络第jjj层中向量的第iii个分量。

事实上,输入xxx可以看成a[0]a^{[0]}a[0]。

这里的a是activation(激活)意思,每个aaa都是激活函数的输出。

为了方便称呼,我们给神经网络的层取了些名字:

其中,输入叫做“输入层”,最后一个计算层叫做“输出层”,中间其余的层都叫做“隐藏层”。事实上,由于第一个输入层不参与计算,它不会计入网络的总层数,只是为了方便称呼才这么叫。因此,上面这个网络看上去有3层,但叫做“双层神经网络”,或“单隐藏层神经网络”。

单样本多神经元的计算

让我们先看一下,对于一个输入样本x(1)x^{(1)}x(1),神经网络是怎么计算输出的。

如图,输入 xxx 是一个形状为3×13 \times 13×1的列向量。第一层有三个神经元,第一个神经元的参数是w1[1],b1[1]w_1^{[1]}, b_1^{[1]}w1[1]​,b1[1]​,第二个是w2[1],b2[1]w_2^{[1]}, b_2^{[1]}w2[1]​,b2[1]​,第三个是w3[1],b3[1]w_3^{[1]}, b_3^{[1]}w3[1]​,b3[1]​。

wi[1]w_i^{[1]}wi[1]​的形状是1×31 \times 31×3,bi[1]b_i^{[1]}bi[1]​是常数。

每个神经元的计算公式和上节课的逻辑回归相同,都是zi[1]=wi[1]x+bi[1]z_i^{[1]}=w_i^{[1]}x+b_i^{[1]}zi[1]​=wi[1]​x+bi[1]​,ai[1]=σ(zi[1])a_i^{[1]}=\sigma(z_i^{[1]})ai[1]​=σ(zi[1]​)(i∈[1,2,3]i \in [1, 2, 3]i∈[1,2,3])。

回忆一下,上一节课里www的形状是nx×1n_x \times1nx​×1,即一个长度为nxn_xnx​的列向量,其中nxn_xnx​是输入向量的长度(此处为3)。bbb是一个常数。计算结果时,我们要把www转置,计算wTx+bw^Tx+bwTx+b。这里的wi[1]w_i^{[1]}wi[1]​是一个行向量,其形状是1×nx1 \times n_x1×nx​,计算时不用转置。计算时直接wi[1]x+bi[1]w_i^{[1]}x+b_i^{[1]}wi[1]​x+bi[1]​就行。

因为有三个神经元,我们得到三个计算结果a1[1],a2[1],a3[1]a_1^{[1]}, a_2^{[1]}, a_3^{[1]}a1[1]​,a2[1]​,a3[1]​。我们可以把它们合起来当成一个3×13 \times 13×1的列向量a[1]a^{[1]}a[1],就像输入xxx一样。

之后,这三个输出作为输入传入第二层的神经元,计算z[2]=W1[2]a[1]+b[2]z^{[2]}=W_1^{[2]}a^{[1]}+b^{[2]}z[2]=W1[2]​a[1]+b[2], y^=a[2]=σ(z[2])\hat{y}=a^{[2]}=\sigma(z^{[2]})y^​=a[2]=σ(z[2])。这个算式和上周的逻辑回归一模一样。

总结一下,如果某一层有nnn个神经元,那么这一层的输出就是一个长度为nnn的列向量。这个输出会被当作下一层的输入。神经网络的每一层都按同样的方式计算着。

对于单隐层神经网络,隐藏层的参数W[1]W^{[1]}W[1]的形状是n1×nxn_1 \times n_xn1​×nx​,其中n1n_1n1​是隐藏层神经元个数,nxn_xnx​是每个输入样本的向量长度。参数b[1]b^{[1]}b[1]的形状是n1×1n_1 \times 1n1​×1。输出层参数W[2]W^{[2]}W[2]的形状是1×n11 \times n_11×n1​,b[2]b^{[2]}b[2]的形状是1×11 \times 11×1。

多样本多神经元的计算

和上一节课一样,让我们把一个输入样本拓展到多个样本,看看整个计算公式该怎么写。

对于第iii个输入样本x(i)x^{(i)}x(i),我们要计算:
a[1](i)=σ(W[1]x(i)+b[1])a[2](i)=σ(W[2]a[1](i)+b[2])\begin{aligned} a^{[1](i)} & =\sigma(W^{[1]}x^{(i)}+b^{[1]}) \\ a^{[2](i)} & =\sigma(W^{[2]}a^{[1](i)}+b^{[2]}) \end{aligned} a[1](i)a[2](i)​=σ(W[1]x(i)+b[1])=σ(W[2]a[1](i)+b[2])​
直接写的话,我们要写个for循环,把iii从000遍历到m−1m-1m−1。

回忆一下,mmm是样本总数。

但是,如果把输入打包在一起,形成一个nx×mn_x \times mnx​×m的矩阵XXX,那么整个计算过程可以用十分相似的向量化计算公式表示:

A[1]=σ(W[1]X+b[1])A[2]=σ(W[2]A[1]+b[2])\begin{aligned} A^{[1]} & =\sigma(W^{[1]}X+b^{[1]}) \\ A^{[2]} & =\sigma(W^{[2]}A^{[1]}+b^{[2]}) \end{aligned} A[1]A[2]​=σ(W[1]X+b[1])=σ(W[2]A[1]+b[2])​

这里的XXX,AAA相当于横向“拉长了”:
X=[∣∣∣x(1)x(2)...x(m)∣∣∣]A[l]=[∣∣∣a[l](1)a[l](2)...a[l](m)∣∣∣]X=\left[ \begin{matrix} | & | & & | \\ x^{(1)} & x^{(2)} & ... & x^{(m)} \\ | & | & & | \end{matrix} \right] \\ \ \\ A^{[l]}=\left[ \begin{matrix} | & | & & | \\ a^{[l](1)} & a^{[l](2)} & ... & a^{[l](m)} \\ | & | & & | \end{matrix} \right] X=⎣⎡​∣x(1)∣​∣x(2)∣​...​∣x(m)∣​⎦⎤​ A[l]=⎣⎡​∣a[l](1)∣​∣a[l](2)∣​...​∣a[l](m)∣​⎦⎤​

激活函数

在神经网络中,我们每做完一个线性运算Z=WX+bZ=WX+bZ=WX+b后,都会做一个σ(Z)\sigma(Z)σ(Z)的操作。上周我们讲这个σ\sigmaσ(sigmoid函数)是为了把实数的输入映射到[0,1][0, 1][0,1]。这是它在逻辑回归的作用。而在普通的神经网络中,σ\sigmaσ就有别的作用了——激活线性输出。σ\sigmaσ其实只是激活输出的激活函数的一员,还有很多其他函数都可以用作为激活函数。我们现在暂时不管这个“激活”是什么意思,先认识一下常见的激活函数。

画这些函数的代码见后文。

它们的数学公式如下:

sigmoid(x)=11+e−xtanh(x)=ex−e−xex+e−xrelu(x)={x(x≥0)0(x<0)leaky_relu(x)={x(x≥0)kx(x<0,k<1)sigmoid(x)=\frac{1}{1+e^{-x}} \\ tanh(x)=\frac{e^x - e^{-x}}{e^x + e^{-x}} \\ relu(x) = \left\{ \begin{aligned} x & \ (x \geq 0) \\ 0 & \ (x < 0) \end{aligned} \right. \\ leaky\_relu(x) = \left\{ \begin{aligned} x & \ (x \geq 0) \\ kx & \ (x < 0, k < 1) \end{aligned} \right. sigmoid(x)=1+e−x1​tanh(x)=ex+e−xex−e−x​relu(x)={x0​ (x≥0) (x<0)​leaky_relu(x)={xkx​ (x≥0) (x<0,k<1)​

其中leaky_relu里的kkk是一个常数,这个常数要小于1。图中的leaky_relu的kkk取了0.1。

现在来介绍一下这些激活函数。

sigmiod,老熟人了,这个函数可以把实数上的输入映射到(0,1)(0, 1)(0,1)。tanh其实是sigmoid的一个“位移版”(二者的核心都是exe^xex),它可以把实数的输入映射到(−1,1)(-1, 1)(−1,1)。

这两个函数有一个问题:当x极大或者极小的时候,函数的梯度几乎为0。从图像上来看,也就是越靠近左边或者右边,函数曲线就越平。梯度过小,会导致梯度下降法每次更新的幅度较小,从而使网络训练速度变慢。

为了解决梯度变小的问题,研究者们又提出了relu函数(rectified linear unit, 线性整流单元)。别看这个名字很高大上,relu函数本身其实很简单:你是正数,就取原来的值;你是负数,就取0。非常的简单直接。把这个函数用作激活函数,梯度总是不会太小,可以有效加快训练速度。

有人觉得relu对负数太“一刀切”了,把relu在负数上的值改成了一个随输入xxx变化的,十分接近0的值。这样一个新的relu函数就叫做leaky relu。(大家应该知道为什么leaky_relu的kkk要小于1了吧)

写在博客里的题外话:浅谈文章的统一性。为什么这里relu用的是小写呢?按照英文的写法,应该是ReLU才对啊?这里是不是写文章的时候不够严谨啊?其实不是。我们这里其实统一用的是代码写法,即全部单词小写。我们首次介绍relu时,是在上文的图片和公式里。那里面用的是小写的relu。后文其实是对这种描述的一个统一,表示“前文用到的relu”,而不是一般用语中的ReLU。在后面的文章中,我会使用ReLU这个称呼。

如果有严谨的文字工作者,还会质疑道:“你这篇文章里有些单词应该用公式框起来,有些应该用代码框起来,怎么直接用文本表示啊?”这是因为微信公众号对公式的支持很烂,我编辑得累死了,不想动脑去思考到底用公式还是用代码了。要把一个东西写得天衣无缝,需要耗费大量的时间。为了权衡,我抛弃了部分严谨性,换来了写文章的效率。

如何选择激活函数

tanh由于其值域比sigmoid大,原理又一模一样,所以tanh在数学上严格优于sigmoid。除非是输出恰好处于(0,1)(0, 1)(0,1)(比如逻辑回归的输出),不然宁可用tanh也不要用sigmoid。

现在大家都默认使用relu作为激活函数,偶尔也有使用leaky_relu的。吴恩达老师鼓励大家多多尝试不同的激活函数。

在之前介绍的公式中,我们所有激活函数ggg都默认用的是g=σg=\sigmag=σ。准确来说,单隐层神经网络公式应该写成下面这种形式:

A[1]=g[1](W[1]X+b[1])A[2]=g[2](W[2]A[1]+b[2])\begin{aligned} A^{[1]} & =g^{[1]}(W^{[1]}X+b^{[1]}) \\ A^{[2]} & =g^{[2]}(W^{[2]}A^{[1]}+b^{[2]}) \end{aligned} A[1]A[2]​=g[1](W[1]X+b[1])=g[2](W[2]A[1]+b[2])​

由于第二层网络的输出落在[0, 1],我们第二个激活函数还是可以用sigmoid,即g[2]=σg^{[2]}=\sigmag[2]=σ。

激活函数的作用

假设我们有一个两层神经网络:

y^=g(W2⋅g(W1x+b1)+b2)\hat{y} = g(W_2 \cdot g(W_1x+b_1) + b_2) y^​=g(W2​⋅g(W1​x+b1​)+b2​)

其中激活函数用ggg表示。

假如我们不使用激活函数,即令g(x)=xg(x)=xg(x)=x的话,这个神经网络就变成了:

y^=W2⋅(W1x+b1)+b2=(W2W1)x+(b1+b2)\begin{aligned} \hat{y} &= W_2 \cdot (W_1x+b_1) + b_2 \\ &= (W_2W_1)x+(b_1+b_2) \end{aligned} y^​​=W2​⋅(W1​x+b1​)+b2​=(W2​W1​)x+(b1​+b2​)​

我们把W2W1W_2W_1W2​W1​看成一个新的“WWW”,(b1+b2)(b_1+b_2)(b1​+b2​)看成一个新的"bbb",那么这其实是一个单层神经网络。

也就是说,如果我们不用激活函数,那么无论神经网络有多少层,这个神经网络都等价于只有一层。这种神经网络永远只能拟合一个线性函数。

为了让神经网络取拟合一个非线性的,超级复杂的函数,我们必须要使用激活函数。

激活函数的导数(选读)

为了让大家重新体验一下高中学数学的感觉,这里求导的步骤推得十分详细。

sigmoid

f(x)=11+e−xf′(x)=−(11+e−x)2(e−x)′=−(11+e−x)2(−e−x)=e−x(1+e−x)2=f(x)(1−f(x))\begin{aligned} f(x) &= \frac{1}{1+e^{-x}} \\ f'(x) &= -(\frac{1}{1+e^{-x}})^2(e^{-x})' \\ &= -(\frac{1}{1+e^{-x}})^2(-e^{-x}) \\ &= \frac{e^{-x}}{(1+e^{-x})^2} \\ &= f(x)(1-f(x)) \end{aligned} f(x)f′(x)​=1+e−x1​=−(1+e−x1​)2(e−x)′=−(1+e−x1​)2(−e−x)=(1+e−x)2e−x​=f(x)(1−f(x))​

上篇笔记也吐槽过了,想写出最后一步,需要发动数学家的固有技能:「注意到」。这不怎么学数学的人谁能注意到最后这一步啊。

tanh

f(x)=ex−e−xex+e−x=e2x−1e2x+1(x−1x+1)′=(x+1)−(x−1)(x+1)2=2(x+1)2f′(x)=(x−1x+1)′(e2x)⋅(e2x)′=2(e2x+1)2(2e2x)=4e2x(e2x+1)2=(1+f(x))(1−f(x))\begin{aligned} f(x) &= \frac{e^x - e^{-x}}{e^x + e^{-x}} = \frac{e^{2x}-1}{e^{2x}+1}\\ \\ (\frac{x-1}{x+1})' &=\frac{(x+1)-(x-1)}{(x+1)^2}\\ &= \frac{2}{(x+1)^2}\\ \\ f'(x) &= (\frac{x-1}{x+1})'(e^{2x}) \cdot (e^{2x})' \\ &= \frac{2}{(e^{2x}+1)^2}(2e^{2x}) \\ &= \frac{4e^{2x}}{(e^{2x}+1)^2} \\ &= (1+ f(x))(1-f(x)) \end{aligned} f(x)(x+1x−1​)′f′(x)​=ex+e−xex−e−x​=e2x+1e2x−1​=(x+1)2(x+1)−(x−1)​=(x+1)22​=(x+1x−1​)′(e2x)⋅(e2x)′=(e2x+1)22​(2e2x)=(e2x+1)24e2x​=(1+f(x))(1−f(x))​

回忆一下,(uv)′=(u′v−uv′v2)(\frac{u}{v})'=(\frac{u'v-uv'}{v^2})(vu​)′=(v2u′v−uv′​)。

最后这步我依然注意不到。我猜原函数f(x)f(x)f(x)是用f′(x)=(1+f(x))(1−f(x))f'(x)=(1+ f(x))(1-f(x))f′(x)=(1+f(x))(1−f(x))这个微分方程构造出来的,而不是反过来恰好发现导数能够写得这么简单。

relu

f(x)={x(x≥0)0(x<0)f′(x)={1(x>0)0(x<0)f(x) = \left\{ \begin{aligned} x & \ (x \geq 0) \\ 0 & \ (x < 0) \end{aligned} \right. \\ f'(x) = \left\{ \begin{aligned} 1 & \ (x > 0) \\ 0 & \ (x < 0) \end{aligned} \right. \\ f(x)={x0​ (x≥0) (x<0)​f′(x)={10​ (x>0) (x<0)​

这个导求得神清气爽。

leaky relu

f(x)={x(x≥0)kx(x<0)f′(x)={1(x>0)k(x<0)f(x) = \left\{ \begin{aligned} x & \ (x \geq 0) \\ kx & \ (x < 0) \end{aligned} \right. \\ f'(x) = \left\{ \begin{aligned} 1 & \ (x > 0) \\ k & \ (x < 0) \end{aligned} \right. \\ f(x)={xkx​ (x≥0) (x<0)​f′(x)={1k​ (x>0) (x<0)​

学数学的人可能会很在意:relu和leaky relu在0处没有导数啊!碰到0你怎么梯度下降啊?实际上,我们编程的时候,不用管那么多,直接也令0处的导数为1就行(即导数在0处的右极限)。

对神经网络做梯度下降

回顾一下,如果只有两个参数w,bw, bw,b,应该用下式做梯度下降:

w←w−αdJdwb←b−αdJdb\begin{aligned} w & \gets w - \alpha \frac{dJ}{dw} \\ b & \gets b - \alpha \frac{dJ}{db} \end{aligned} wb​←w−αdwdJ​←b−αdbdJ​​

回忆一下,α\alphaα是学习率,表示梯度更新的速度,一般取0.00010.00010.0001这种很小的值。

现在,我们有4个参数:W[1],W[2],b[1],b[2]W^{[1]},W^{[2]}, b^{[1]},b^{[2]}W[1],W[2],b[1],b[2],它们也应该按照同样的规则执行梯度下降:

W[1]←W[1]−αdJdW[1]W[2]←W[2]−αdJdW[2]b[1]←b[1]−αdJdb[1]b[2]←b[2]−αdJdb[2]\begin{aligned} W^{[1]} & \gets W^{[1]} - \alpha \frac{dJ}{dW^{[1]}} \\ W^{[2]} & \gets W^{[2]} - \alpha \frac{dJ}{dW^{[2]}} \\ b^{[1]} & \gets b^{[1]} - \alpha \frac{dJ}{db^{[1]}} \\ b^{[2]} & \gets b^{[2]} - \alpha \frac{dJ}{db^{[2]}} \\ \end{aligned} W[1]W[2]b[1]b[2]​←W[1]−αdW[1]dJ​←W[2]−αdW[2]dJ​←b[1]−αdb[1]dJ​←b[2]−αdb[2]dJ​​

剩下的问题就是怎么求导了。让我们再看一遍神经网络正向传播的公式:

Z[1]=W[1]X+b[1]A[1]=g[1](Z[1])Z[2]=W[2]X+b[2]A[2]=g[2](Z[2])\begin{aligned} Z^{[1]} &= W^{[1]}X+b^{[1]} \\ A^{[1]} &= g^{[1]}(Z^{[1]}) \\ Z^{[2]} &= W^{[2]}X+b^{[2]} \\ A^{[2]} &= g^{[2]}(Z^{[2]}) \end{aligned} Z[1]A[1]Z[2]A[2]​=W[1]X+b[1]=g[1](Z[1])=W[2]X+b[2]=g[2](Z[2])​

由于我们令g[2]=σg^{[2]}=\sigmag[2]=σ,所以神经网络第二层(输出层)的导数可以直接套用上周的导数公式:

dZ[2]=A[2]−YdW[2]=1mdZ[2]A[1]Tdb[2]=1mΣi=1mdZ[2](i)\begin{aligned} dZ^{[2]} &= A^{[2]}-Y \\ dW^{[2]} &= \frac{1}{m}dZ^{[2]}A^{[1]T} \\ db^{[2]} &= \frac{1}{m} \Sigma_{i=1}^m dZ^{[2](i)} \end{aligned} dZ[2]dW[2]db[2]​=A[2]−Y=m1​dZ[2]A[1]T=m1​Σi=1m​dZ[2](i)​

注意! 上周我们算的是AdZTAdZ^TAdZT,这周是dZ[2]A[1]TdZ^{[2]}A^{[1]T}dZ[2]A[1]T。这是因为参数WWW转置了一下。上周的www是列向量,这周每个神经元的权重WiW_iWi​是行向量。

之后,我们来看第一层。首先求dZ[1]dZ^{[1]}dZ[1]:
dZ[1]=dA[1]dA[1]dZ[1]=W[2]TdZ[2]∗g[1]′(Z[1])\begin{aligned} dZ^{[1]} &= dA^{[1]}\frac{dA^{[1]}}{dZ^{[1]}} \\ &=W^{[2]T}dZ^{[2]} \ast g^{[1]'} (Z^{[1]}) \end{aligned} dZ[1]​=dA[1]dZ[1]dA[1]​=W[2]TdZ[2]∗g[1]′(Z[1])​

注意,上式中右边第一项dA[1]dA^{[1]}dA[1]是dJdA[1]\frac{dJ}{dA^{[1]}}dA[1]dJ​的简写,第二项dA[1]dZ[1]\frac{dA^{[1]}}{dZ^{[1]}}dZ[1]dA[1]​是实实在在的求导。

这里dA[1]dA^{[1]}dA[1]和dW[2]dW^{[2]}dW[2]的计算是对称的哟。

之后的dW[1],db[1]dW^{[1]}, db^{[1]}dW[1],db[1]的公式和前面dW[2],db[2]dW^{[2]}, db^{[2]}dW[2],db[2]的相同:
dW[1]=1mdZ[1]XTdb[1]=1mΣi=1mdZ[1](i)\begin{aligned} dW^{[1]} &= \frac{1}{m}dZ^{[1]}X^{T} \\ db^{[1]} &= \frac{1}{m} \Sigma_{i=1}^m dZ^{[1](i)} \end{aligned} dW[1]db[1]​=m1​dZ[1]XT=m1​Σi=1m​dZ[1](i)​

别忘了,X=A[0]X=A^{[0]}X=A[0]。

这些求导的步骤写成代码如下:

dZ2=A2-Y
dW2=np.dot(dZ2, A1.T) / m
db2=np.sum(dZ2, axis=1, keepdims=True) / m
dZ1=np.dot(W2.T, dZ2) * g1_backward(Z1)
dW1=np.dot(dZ1, X.T) / m
db1=np.sum(dZ1, axis=1, keepdims=True) / m

再次温馨提示,搞不清楚数学公式的细节没关系,直接拿来用就好了。要学会的是算法的整体思路。

这段代码有一点需要注意:

db2=np.sum(dZ2, axis=1, keepdims=True)
db1=np.sum(dZ1, axis=1, keepdims=True)

这个keepdims=True是必不可少的。使用np.sum, np.mean这种会导致维度变少的计算时,如果加了keepdims=True,会让变少的那一个维度保持长度1.比如一个[4, 3]的矩阵,我们对第二维做求和,理论上得到的是一个[4]的向量。但如果用了keepdims=True,就会得到一个[4, 1]的矩阵。

保持向量的维度,可以让某些广播运算正确进行。比如我要用[4, 3]的矩阵减去[4]的矩阵就会报错,而减去[4, 1]的矩阵就不会报错。

参数随机初始化

再次回顾下,梯度下降算法的结构如下:

初始化参数
迭代 k 步:算参数的梯度用梯度更新参数

对于这节课新学的单隐层神经网络,求导、更新参数的过程我们已经学完了。我们还有一个东西没有详细探究:参数的初始化方式。现在,我们来详细研究一下参数初始化。

在上节课中,我们用一句话就带过了参数初始化方法:令参数全为0就行了。这种初始化方法在这节课还有用吗?让我们来看课堂里提到的一个示例:

如上图,对于输入长度为2,第一层有2个神经元的网络,其第一层参数W[1]W^{[1]}W[1]为[[0, 0], [0, 0]]。这样算出来的神经元输出a1[1],a2[1]a^{[1]}_1,a^{[1]}_2a1[1]​,a2[1]​是一样的。而更新梯度时,每一个神经元的参数W1[1],W2[1]W^{[1]}_1, W^{[1]}_2W1[1]​,W2[1]​的梯度都只和该神经元的输出有关。这样,每个神经元参数的导数dw都是一模一样的。导数一样,初始化的值也一样,那么每个神经元的参数的值会一直保持相同。这样,不论我们在某一层使用了多少个神经元,都等价于只使用一个神经元。

为了不发生这样的情况,我们需要让每一个神经元的参数www都取不同的值。这可以通过随机初始化实现。只需要使用下面的代码就可以随机初始化www:

w = np.random.randn((h, w)) * 0.01

注意,这里我们给随机出的数乘了个0.01。这是因为出于经验,人们更倾向于使用更小的参数,以计算出更小的结果,防止激活函数(如tanh)在绝对值过大时梯度过小的问题。

后面的课会详细介绍该如何初始化这些参数,以及初始化参数可以解决哪些问题。

而bbb和之前一样,直接用0初始化就行了。

知识总结

在这堂课中,我们正式认识了神经网络的定义。原来,上周的逻辑回归只是一个特殊的神经网络。它只有一个输出层,并且使用sigmoid作激活函数。而这周,我们学习了如何定义一个两层(一个隐藏层、一个输出层)的神经网络,并且知道如何在网络中使用不同的激活函数。

让我们来看一下这节课的知识点:

  • 神经网络的定义

    • 输入层、隐藏层、输出层
    • 每一层每一个神经元相关的参数该怎么表示
  • 神经网络的计算方式
    • 单样本 -> 多样本
    • 正向传播与反向传播
  • 激活函数
    • 直观认识激活函数——激活函数属于神经网络计算中的哪一部分?
    • 常见的四种激活函数:sigmoid, tanh, relu, leaky_relu
    • 如何选择激活函数
    • 为什么要使用激活函数
  • 神经网络与逻辑回归的区别——参数初始化问题
    • 为什么不能用0初始化WWW
    • 随机初始化WWW
    • 可以用0初始化bbb

代码实战

这节课的编程作业是搞一个点集分类器。此任务的数据集如下图所示:

在平面上,已知有一堆红色的点和绿色的点。我们希望任意给定一个点,程序能够判断这个点是红点还是绿点。

让我们人类来分类的话,肯定会认为左边一片花瓣和右上角两片花瓣是绿色的,剩下三片花瓣是红色的(有部分点不满足这个规律,可以认为这些点是噪声,即不正确的数据)。让神经网络来做这个任务,会得到怎样的结果呢?

现在,让我们用这周学的单隐层神经网络,来实现这个分类器。

虽然前面说这周要继续挑战猫狗分类任务,但我估摸着这周的模型可能还是简单了一点。等下周学了再强大一点的模型,我再来复仇。

项目链接:https://github.com/SingleZombie/DL-Demos/tree/master/dldemos/ShallowNetwork

通用分类器类

在上节课的编程实战中,我们很暴力地写了“一摊”代码。说实话,有编程洁癖的我是不能接受那种潦草的代码的。如果代码写得太乱,就根本不能复用,根本不可读,根本不能体现编程的逻辑之美。

这周,我将解除封印,释放我30%的编程水平,展示一个比较优雅的通用分类器类该怎么写。我们会先把上周的逻辑回归用继承基类的方式实现一遍,再实现一遍这周的浅层神经网络。

分类器基类的代码如下:

import abc
import numpy as npclass BaseRegressionModel(metaclass=abc.ABCMeta):# Use Cross Entropy as the cost functiondef __init__(self):pass@abc.abstractmethoddef forward(self, X, train_mode=True):# if self.train_mode:#   forward_train()# else:#   forward_test()pass@abc.abstractmethoddef backward(self, Y):pass@abc.abstractmethoddef gradient_descent(self, learning_rate=0.001):passdef loss(self, Y_hat, Y):return np.mean(-(Y * np.log(Y_hat) + (1 - Y) * np.log(1 - Y_hat)))def evaluate(self, X, Y):Y_hat = self.forward(X, train_mode=False)predicts = np.where(Y_hat > 0.5, 1, 0)score = np.mean(np.where(predicts == Y, 1, 0))print(f'Accuracy: {score}')

为了简化代码,我们用BaseRegressionModel表示一个使用交叉熵为损失函数的二分类模型。这样,我们所有的模型都可以共用一套损失函数loss、一套评估方法evaluate。这里损失函数和评估方法的实现都是从上周的代码里复制过来的。

让我们分别看一下其他几个类方法的描述:

  • __init__: 模型的参数应该在__init__方法里初始化。
  • forward:正向传播函数。这个函数即可以用于测试,也可以用于训练。如果是用于训练,就要令参数train_mode=True。为什么要区分训练和测试呢?这是因为,正向传播在训练的时候需要额外保存一些数据(缓存),保存数据是存在开销的。在测试的时候,我们可以不做缓存,以降低程序运行开销。
  • backward:反向传播函数。这个函数用于forward之后的梯度计算。算出来的梯度会缓存起来,供反向传播使用。
  • gradient_descent:用梯度下降更新模型的参数。(一般框架会把优化器和模型分开写。由于我们现在只学了梯度下降这一种优化策略,所以直接把梯度下降当成了模型类的方法)

有了这样一个分类器基类后,我们可以用统一的方式训练模型:

def train_model(model: BaseRegressionModel,X_train,Y_train,X_test,Y_test,steps=1000,learning_rate=0.001,print_interval=100):for step in range(steps):Y_hat = model.forward(X_train)model.backward(Y_train)model.gradient_descent(learning_rate)if step % print_interval == 0:train_loss = model.loss(Y_hat, Y_train)print(f'Step {step}')print(f'Train loss: {train_loss}')model.evaluate(X_test, Y_test)

有了一个初始化好的模型model后,我们在训练函数train_model里可以直接开始循环训练模型。每次我们先调用model.forward做正向传播,缓存一些数据,再调用model.backward反向传播算梯度,最后调用model.gradient_descent更新模型的参数。每训练一定的步数,我们监控一次模型的训练情况,输出模型的训练loss和测试精度。

看吧,是不是使用了类来实现神经网络后,整个代码清爽而整洁?

工具函数

import numpy as npdef sigmoid(x):return 1 / (1 + np.exp(-x))def relu(x):return np.maximum(x, 0)def relu_de(x):return np.where(x > 0, 1, 0)

同样,为了让代码更整洁,我把一些工具函数单独放到了一个文件里。现在,如上面的代码所示,我们的工具函数只有几个损失函数及它们的导数。(用于sigmoid只用于最后一层,我们可以直接用dZ=A-Y跳一个导数计算步骤,所以这里没有写sigmoid的导数)。

复现逻辑回归

class LogisticRegression(BaseRegressionModel):def __init__(self, n_x):super().__init__()self.n_x = n_xself.w = np.zeros((n_x, 1))self.b = 0def forward(self, X, train_mode=True):Z = np.dot(self.w.T, X) + self.bA = sigmoid(Z)  # hat_Y = Aif train_mode:self.m_cache = X.shape[1]self.X_cache = Xself.A_cache = Areturn Adef backward(self, Y):d_Z = self.A_cache - Yd_w = np.dot(self.X_cache, d_Z.T) / self.m_cached_b = np.mean(d_Z)self.d_w_cache = d_wself.d_b_cache = d_bdef gradient_descent(self, learning_rate=0.001):self.w -= learning_rate * self.d_w_cacheself.b -= learning_rate * self.d_b_cache

逻辑回归是上节课的内容,这里就不讲解,直接贴代码了。大家可以通过这个例子看一看BaseRegressionModel的子类应该怎么写。

实现单隐层神经网络

有了基类后,我们更加明确代码中哪些地方是要重新写,不能复用以前的代码了。在实现浅层神经网络时,我们要重写模型初始化正向传播反向传播梯度下降这几个步骤。

模型初始化

我们要在__init__里初始化模型的参数。回忆一下这周的单隐层神经网络推理公式:
A[1]=g[1](W[1]X+b[1])A[2]=g[2](W[2]A[1]+b[2])\begin{aligned} A^{[1]} & =g^{[1]}(W^{[1]}X+b^{[1]}) \\ A^{[2]} & =g^{[2]}(W^{[2]}A^{[1]}+b^{[2]}) \end{aligned} A[1]A[2]​=g[1](W[1]X+b[1])=g[2](W[2]A[1]+b[2])​

其中,有四个参数W[1],W[2],b[1],b[2]W^{[1]}, W^{[2]}, b^{[1]}, b^{[2]}W[1],W[2],b[1],b[2],它们的形状分别是n1×nxn_1 \times n_xn1​×nx​, 1×n11 \times n_11×n1​, n1×1n_1 \times 1n1​×1, 1×11 \times 11×1。我们需要在这里决定nx,n1n_x, n_1nx​,n1​这两个数。

nxn_xnx​由输入向量的长度决定。由于我们是做2维平面点集分类,每一个输入数据就是一个二维的点。因此,在稍后初始化模型时,我们会令nx=2n_x=2nx​=2。

n1n_1n1​属于网络的超参数,我们可以调整这个参数的值。

计划好了初始化函数的输入参数后,我们来看看初始化函数的代码:

def __init__(self, n_x, n_1):super().__init__()self.n_x = n_xself.n_1 = n_1self.W1 = np.random.randn(n_1, n_x) * 0.01self.b1 = np.zeros((n_1, 1))self.W2 = np.random.randn(1, n_1) * 0.01self.b2 = np.zeros((1, 1))

别忘了,前面我们学过,初始化W时要使用随机初始化,且让初始化出来的值比较小。

正向传播

我们打算神经网络令第一层的激活函数为relu,第二层的激活函数为sigmoid。因此,模型的正向传播公式如下:
A[1]=relu(W[1]X+b[1])A[2]=sigmoid(W[2]A[1]+b[2])\begin{aligned} A^{[1]} & =relu(W^{[1]}X+b^{[1]}) \\ A^{[2]} & =sigmoid(W^{[2]}A^{[1]}+b^{[2]}) \end{aligned} A[1]A[2]​=relu(W[1]X+b[1])=sigmoid(W[2]A[1]+b[2])​
用代码表示如下:

def forward(self, X, train_mode=True):Z1 = np.dot(self.W1, X) + self.b1A1 = relu(Z1)Z2 = np.dot(self.W2, A1) + self.b2A2 = sigmoid(Z2)if train_mode:self.m_cache = X.shape[1]self.X_cache = Xself.Z1_cache = Z1self.A1_cache = A1self.A2_cache = A2return A2

其中train_mode里的内容是我们待会儿要在反向传播用到的数据,这里需要先缓存起来。

事实上,我是边写反向传播函数,边写这里if train_mode:里面的缓存数据的。编程不一定要按照顺序写。

反向传播

翻译一下这些公式:

$$
\begin{aligned}
dZ^{[2]} &= A^{[2]}-Y \
dW^{[2]} &= \frac{1}{m}dZ{[2]}A{[1]T} \
db^{[2]} &= \frac{1}{m} \Sigma_{i=1}^m dZ^{2} \
dZ^{[1]} &=W{[2]T}dZ{[2]} \ast g^{[1]'} (Z^{[1]}) \
dW^{[1]} &= \frac{1}{m}dZ{[1]}X{T} \
db^{[1]} &= \frac{1}{m} \Sigma_{i=1}^m dZ^{1}
\end{aligned}

$$

用代码写就是这样:

def backward(self, Y):dZ2 = self.A2_cache - YdW2 = np.dot(dZ2, self.A1_cache.T) / self.m_cachedb2 = np.sum(dZ2, axis=1, keepdims=True) / self.m_cachedA1 = np.dot(self.W2.T, dZ2)dZ1 = dA1 * relu_de(self.Z1_cache)dW1 = np.dot(dZ1, self.X_cache.T) / self.m_cachedb1 = np.sum(dZ1, axis=1, keepdims=True) / self.m_cacheself.dW2_cache = dW2self.dW1_cache = dW1self.db2_cache = db2self.db1_cache = db1

算完梯度后,我们要把它们缓存起来,用于之后的梯度下降。

梯度下降

def gradient_descent(self, learning_rate=0.001):self.W1 -= learning_rate * self.dW1_cacheself.b1 -= learning_rate * self.db1_cacheself.W2 -= learning_rate * self.dW2_cacheself.b2 -= learning_rate * self.db2_cache

梯度已经算好了,梯度下降就没什么好讲的了。

挑战点集分类问题

数据收集

这里我已经提前实现好了生成数据集的函数。本文的附录里会介绍这些函数的细节。

使用项目里的 generate_point_set 函数可以生成一个平面点集分类数据集:

x, y, label = generate_point_set()
# x: [240]
# y: [240]
# label: [240]

其中,x[i]是第i个点的横坐标,y[i]是第i个点的纵坐标,label[i]是第i个点的标签。标签为0表示是红色的点,标签为1表示是绿色的点。

数据预处理

得到了原始数据后,我们要把数据处理成矩阵X和Y,其中X的形状是[2, m],Y的形状是[1, m],其中m是样本大小。之后,我们还需要把原始数据拆分成训练集和测试集。

第一步生成矩阵的代码如下:

X = np.stack((x, y), axis=1)
Y = np.expand_dims(label, axis=1)
# X: [240, 2]
# Y: [240, 1]

大家应该能猜出stackexpand_dims是什么意思。stack能把两个张量堆起来,比如这里把表示x,y坐标的一维向量合成起来,变成一个向量(长度为2)的向量(长度为240)。expand_dims就是凭空给张量加一个长度为1的维度,比如这里给Y添加了axis=1上的维度。

第二步划分数据集的方法如下:

indices = np.random.permutation(X.shape[0])
X_train = X[indices[0:200], :].T
Y_train = Y[indices[0:200], :].T
X_test = X[indices[200:], :].T
Y_test = Y[indices[200:], :].T
# X_train: [2, 200]
# Y_train: [1, 200]
# X_test: [2, 40]
# Y_test: [1, 40]

注意,我们划分数据集的时候最好要随机划分。我这里使用np.random.permutation生成了一个排列,把这个排列作为下标来打乱数据集。

大家看不懂这段代码的话,可以想象这样一个例子:老师想抽10个人去值日,于是,他把班上同学的学号打乱,在打乱后的学号列表中,把前10个学号的同学叫了出来。代码里indices就是用随机排列生成的一个“打乱过的学号”,根据这个随机索引值,我们把前200个索引的数据当成训练集,200号索引之后的数据当成测试集。

经过这些处理,数据就符合课堂上讲过的形状要求了。

使用模型

n_x = 2model1 = LogisticRegression(n_x)
model2 = ShallowNetwork(n_x, 2)
model3 = ShallowNetwork(n_x, 4)
model4 = ShallowNetwork(n_x, 10)
train_model(model1, X_train, Y_train, X_test, Y_test, 500, 0.0001, 50)
train_model(model2, X_train, Y_train, X_test, Y_test, 2000, 0.01, 100)
train_model(model3, X_train, Y_train, X_test, Y_test, 2000, 0.01, 100)
train_model(model4, X_train, Y_train, X_test, Y_test, 2000, 0.01, 100)

由于我们前面已经定义好了模型,使用模型的过程就很惬意了。这里直接初始化我们自己编写的类,再用训练函数训练模型即可。

为了比较不同的模型,从感性上认识不同模型间的区别,在示例代码中我训练了4个模型。第一个模型是逻辑回归,后三个模型分别是隐藏层有2、4、10个神经元的单隐藏层神经网络。

模仿这堂课的编程作业,我也贴心地实现了模型可视化函数:

visualize_X = generate_plot_set()
plot_result = model4.forward(visualize_X, train_mode=False)
visualize(X, Y, plot_result)

只要运行上面这些代码,大家就可以看到模型具体是怎么分类2维平面上所有点的。让我们在下一节里看看这些函数的运行效果。

实验报告

好了,最好玩的地方来了。让我们有请四位选手,看看他们在二维点分类任务上表现如何。

首先是逻辑回归:

逻辑回归选手也太菜了吧!他只能模拟一条直线。这条直线虽然把下面两片红色花瓣包进去了,但忽略了左上角的花瓣。太弱了,太弱了!

隐藏层只有2个神经元的选手也菜得不行,和逻辑回归一起可谓是“卧龙凤雏”啊!

4-神经元选手似乎在尝试做出一些改变!好像有一次的运行结果还挺不错!但怎么我感觉他的发挥不是很稳定啊?他是在瞎蒙吧?

好,那我们最后上场的是4号选手10-神经元网络。4号选手可谓是受到万众的期待啊。据说,他有着“二维点分类小丸子”的称号,让我们来看一看他的表现:


只见4号网络手起刀落,刀刀见血。不论是怎么运行程序,他都能精准无误地把点集正确分类。我宣布,他就是本届点集分类大赛的冠军!让我们祝贺他!

程序里很多超参数是可调的,数据集也是可以随意修改的。欢迎大家去使用本课的代码,比较一下不同的神经网络。

总结

通过这节课的编程练习后,大家应该掌握以下编程技能:

  • 编写单隐层神经网络的正向传播
  • 编写单隐层神经网络的反向传播
  • 正确初始化神经网络的参数
  • 常见激活函数及其导数的实现

此外,通过浏览我的项目,大家应该能够提前学到以下技能:

  • 在神经网络中使用缓存的方法保存数据

当然,我相信我的项目里还展示了许多编程技术。这些技能严格来说不在本课程的要求范围内,大家可以自行体悟。

附赠内容

如何画激活函数

import matplotlib.pyplot as plt
import numpy as np

先导入第三方库。

def sigmoid(x):return 1 / (1 + np.exp(-x))def tanh(x):return (np.exp(x) - np.exp(-x)) / (np.exp(x) + np.exp(-x))def relu(x):return np.maximum(x, 0)def leaky_relu(x):return np.maximum(x, 0.1 * x)

再定义好激活函数的公式。

x = np.linspace(-3, 3, 100)
y1 = sigmoid(x)
y2 = tanh(x)
y3 = relu(x)
y4 = leaky_relu(x)

画函数,其实就是生成函数上的一堆点,再把相邻的点用直线两两连接起来。为了生成函数上的点,我们先用np.linspace(-3, 3, 100)生成100个位于[-3, 3]上的x坐标值,用这些x坐标值算出每个函数的y坐标值。

plt.subplot(2, 2, 1)
plt.axvline(x=0, color='k')
plt.axhline(y=0, color='k')
plt.plot(x, y1)
plt.title('sigmoid')

之后就是调用API了。这里只展示一下sigmoid函数是怎么画出来的。plt.subplot(a, b, c)表示你要在一个a x b的网格里的第c个格子里画图。 plt.axvline(x=0, color='k') plt.axhline(y=0, color='k')用于生成x,y轴,plt.plot(x, y1)用于画函数曲线,plt.title('sigmoid')用于给图像写标题。

plt.show()

用类似的方法画完所有函数后,调用plt.show()把图片显示出来就大功告成了。

这段代码的链接:https://github.com/SingleZombie/DL-Demos/blob/master/dldemos/ShallowNetwork/plot_activation_func.py

学API本身没有任何技术含量,知道API能做什么,有需求的时候去查API用法即可。

画花

看完上面的内容,有些人肯定会想:“诶,你数据集里那朵花画得挺不错啊,你是不是学过美术的啊?”嘿嘿,你们能这么想,我很荣幸。其实那朵花是用程序生成出来的。作为笔记的赠品,我打算顺手介绍一下该怎么用高中知识画出前面的那朵花。

代码文件:dldemos/ShallowNetwork/genereate_points.py

流程一览

这幅图足以概括花朵绘制的流程。

  1. 生成半个椭圆。
  2. 合成完整的椭圆。
  3. 把椭圆移到x正半轴。
  4. 复制、旋转椭圆。

有人会说:“这前三步可以用一步就完成吧?你直接生成一个在x正半轴上的椭圆就好了,干嘛要拆开来?”别急,看了后文你就知道了。我这么做,完全是为了多展示一点知识,可谓是用心良苦啊。

画半个椭圆

椭圆的公式是x2a2+y2b2=1\frac{x^2}{a^2}+\frac{y^2}{b^2}=1a2x2​+b2y2​=1,其中aaa是椭圆在x轴上的轴长,bbb是在y轴上的轴长。我画的椭圆的长轴为20,短轴为10,其形状和公式如图所示。

但程序可不认得这个公式。为了生成椭圆上的点,我们可以遍历横坐标x,用公式y=b1−x2a2y=b\sqrt{1-\frac{x^2}{a^2}}y=b1−a2x2​​算出对应的y坐标。

这段生成半椭圆的代码如下所示:

def half_oval(cnt, h=10, w=20):x = np.linspace(-w, w, cnt)y = np.sqrt(h * h * (1 - x * x / w / w))return np.stack((x, y), 1)petal1 = half_oval(20)

hafl_ovel的参数分别表示椭圆上点的数量、y轴上轴长、x轴上轴长。根据刚刚的理论分析,我们在第二、三行算出所有点的x, y坐标。第四行用np.stack((x, y), 1)把坐标合并起来。

这里要介绍一下stack函数的用法。stack用于把多个张量(第一个参数)按某一维(第二个参数)堆叠起来。第一个参数很好理解,而第二个参数“堆叠的维度”就不是那么好理解了。让我们针对这份代码,看两个取不同维度的例子。

在我们这份代码中,执行完第二、三行后,x[x1, x2 ..., xn]这样一个形状为[n]的向量,y也是[y1, y2 ..., yn]这样一个形状为[n]的向量。

当堆叠维度取0时,x会变成[[x1, x2, ..., xn]]([1, n])的矩阵,y会变成[[y1, y2, ..., yn]]([1, n])的矩阵。之后,两个矩阵的第一维会拼起来,变成[[x1, x2, ..., xn], [y1, y2, ..., yn]]这样一个形状为[2, n]的矩阵。

当堆叠维度取1时,x会变成[[x1], [x2], ..., [xn]]([n, 1])的矩阵,y会变成[[y1], [y2], ..., [yn]]([n, 1])的矩阵。之后,两个矩阵的第二维会拼起来,变成[[x1, y1], [x2, y2]..., [xn, yn]]这样一个形状为[n, 2]的矩阵。

我们希望生成一个坐标的数组,即形状为[n 2]的矩阵。因此,我们会堆叠维度1(第二个维度),即使用如下代码:

np.stack((x, y), 1)

总之,经过以上操作,half_oval会返回一个形状为[n, 2]的坐标数组,表示半个花瓣上每个点的坐标。

翻转合并椭圆

要把半椭圆垂直翻转,实际上只要令半椭圆上所有点的y坐标取反即可:

{x←xy←−y\left\{ \begin{aligned} x &\gets x \\ y &\gets -y \end{aligned} \right. {xy​←x←−y​

但是,这种写法不够高级。我们可以写成矩阵乘法的形式:

[xy]←[100−1]×[xy]⇒[xy]←[x−y]\begin{aligned} &\left[ \begin{aligned} &x \\ &y \end{aligned} \right] \gets \left[ \begin{aligned} &1 &0 \\ &0 &-1 \end{aligned} \right] \times \left[ \begin{aligned} &x \\ &y \end{aligned} \right] \\ \Rightarrow&\left[ \begin{aligned} &x \\ &y \end{aligned} \right] \gets \left[ \begin{aligned} x \\ -y \end{aligned} \right] \end{aligned} ⇒​[​xy​]←[​10​0−1​]×[​xy​][​xy​]←[x−y​]​

如果你对矩阵乘法不熟,只需要知道
[abcd]×[xy]=[ax+bycx+dy]\left[ \begin{aligned} &a &b \\ &c &d \end{aligned} \right] \times \left[ \begin{aligned} &x \\ &y \end{aligned} \right]= \left[ \begin{aligned} &ax+by \\ &cx+dy \end{aligned} \right] [​ac​bd​]×[​xy​]=[​ax+bycx+dy​]

设翻转矩阵为FFF,坐标向量为ppp,则翻转后的向量p′p'p′可以写成:
p′=Fpp'=Fp p′=Fp
这里我们默认ppp和p′p'p′都是列向量。但是,刚刚我们生成点的坐标时,每个坐标都是一个行向量。也就是说,ppp和p′p'p′其实都是行向量。因此,上式应该改成:
p′T=FpTp'^T=Fp^T p′T=FpT
最后我们要算的是p′p'p′,因此可以对上式两边再取转置:
p′=(FpT)T=pFT\begin{aligned} p'&=(Fp^T)^T \\ &=pF^T \end{aligned} p′​=(FpT)T=pFT​

有了这些数学上的分析,我们可以写代码了。

首先是生成翻转矩阵:

def vertical_flip():return np.array([[1, 0], [0, -1]])

之后生成翻转后的花瓣:

petal2 = np.dot(half_oval(20), vertical_flip().T)

现在,我们有开始得到的petal1和翻转后的petal2,它们的形状都是[n, 2]。我们希望把这两个坐标数组合并起来。这可以通过下面这行代码实现:

petal = np.concatenate((petal1, petal2), 0)

concatenate用于按某一维(第二个参数)拼接张量(第一个参数)。回顾一下,刚刚的半椭圆张量的形状[n, 2]表示有n2维坐标。合并两个半椭圆后,我们应该得到2n个点,即得到一个形状为[2n, 2]的张量。因此,这里我们要把两个半椭圆数组按第一维(0号维度)拼接。

concatenate和刚刚提到的stack有点像。其实,stack就是新建了一个维度,再做concatenate操作。stack一般由于把单独计算出来的x, y, z这样的坐标堆叠成一个坐标数组/坐标张量,concatenate一般用于合并多个性质一样的张量,比如这里的合并两个坐标数组。

移动椭圆

移动椭圆很简单,只要给所有坐标加同一个向量就行了:

petal += [25, 0]

注意,这里的petal是一个形状为[2n, 2]的张量,而[25, 0]是一个形状为[2]的张量。这一个逐元素的加法操作之所以能够被程序正常解读,是因为上周提到的“广播”操作。通过使用广播,[25, 0]这个向量被加到了坐标数组中的每一个坐标里。

旋转花瓣,生成花朵

如上图1所示,一个坐标(x,y)(x, y)(x,y)可以用它到原点的距离rrr和与x正半轴夹角θ\thetaθ表示:

{x=r⋅cosθy=r⋅sinθ\left\{ \begin{aligned} x = r \cdot cos\theta \\ y = r \cdot sin\theta \end{aligned} \right. {x=r⋅cosθy=r⋅sinθ​

那么,如上图2所示,假设现在把一个夹角为θ\thetaθ的(x1,y1)(x_1, y_1)(x1​,y1​)旋转α\alphaα后得到了(x2,y2)(x_2, y_2)(x2​,y2​),(x2,y2)(x_2, y_2)(x2​,y2​)可以表示为:

{x2=r⋅cos(θ+α)y2=r⋅sin(θ+α)\left\{ \begin{aligned} x_2 = r \cdot cos(\theta+\alpha) \\ y_2 = r \cdot sin(\theta+\alpha) \end{aligned} \right. {x2​=r⋅cos(θ+α)y2​=r⋅sin(θ+α)​

但是,我们现在只知道(x1,y1)(x_1, y_1)(x1​,y1​)这个坐标。给定x1,y1,αx_1, y_1, \alphax1​,y1​,α,该怎么计算出x2,y2x_2, y_2x2​,y2​呢?

这里,我们可以用高中学过的三角函数两角和公式,把刚才那个三角函数“拆开”:

{x2=r⋅cos(θ+α)=r(cosθcosα−sinθsinα)y2=r⋅sin(θ+α)=r(sinθcosα+cosθsinα)\left\{ \begin{aligned} x_2 &= r \cdot cos(\theta+\alpha) \\ &= r(cos \theta cos\alpha - sin \theta sin\alpha)\\ y_2 &= r \cdot sin(\theta+\alpha) \\ &= r(sin \theta cos\alpha + cos \theta sin\alpha) \end{aligned} \right. ⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧​x2​y2​​=r⋅cos(θ+α)=r(cosθcosα−sinθsinα)=r⋅sin(θ+α)=r(sinθcosα+cosθsinα)​

我们又已知:

{x1=r⋅cosθy1=r⋅sinθ\left\{ \begin{aligned} x_1 = r \cdot cos\theta \\ y_1 = r \cdot sin\theta \end{aligned} \right. {x1​=r⋅cosθy1​=r⋅sinθ​

因此,x2,y2x_2, y_2x2​,y2​可以用下面的式子表示:

{x2=r(cosθcosα−sinθsinα)=cosα⋅x1−sinα⋅y1y2=r(sinθcosα+cosθsinα)=cosα⋅y1+sinα⋅x1\left\{ \begin{aligned} x_2 &= r(cos \theta cos\alpha - sin \theta sin\alpha)\\ &= cos\alpha \cdot x_1 - sin\alpha \cdot y_1\\ y_2 &= r(sin \theta cos\alpha + cos \theta sin\alpha) \\ &= cos\alpha \cdot y_1 + sin\alpha \cdot x_1 \end{aligned} \right. ⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧​x2​y2​​=r(cosθcosα−sinθsinα)=cosα⋅x1​−sinα⋅y1​=r(sinθcosα+cosθsinα)=cosα⋅y1​+sinα⋅x1​​

这个式子用矩阵乘法表达如下:

[x2y2]=[cosα−sinαsinαcosα]×[x1y2]\left[ \begin{aligned} &x_2 \\ &y_2 \end{aligned} \right]= \left[ \begin{aligned} &cos\alpha &-sin\alpha \\ &sin\alpha &cos\alpha \end{aligned} \right] \times \left[ \begin{aligned} &x_1 \\ &y_2 \end{aligned} \right] [​x2​y2​​]=[​cosαsinα​−sinαcosα​]×[​x1​y2​​]

也就是说,旋转操作也可以用一个矩阵表示。我们可以用和刚刚做翻转操作相同的办法,对坐标数组做旋转。以下是代码实现:

def rotate(theta):return np.array([[np.cos(theta), -np.sin(theta)],[np.sin(theta), np.cos(theta)]])

这个函数可以生成一个让坐标旋转theta弧度的矩阵。

这样,我们如果想让一个坐标数组旋转60度,可以写下面的代码:

new_petal = np.dot(petal, rotate(np.radians(60)).T)

在生成花朵时,我们除了生成第一片花瓣外,还要通过旋转生成另外5朵花瓣,并把花瓣合并起来。这整个流程的代码如下:

petal1 = half_oval(20)
petal2 = np.dot(half_oval(20), vertical_flip().T)
petal = np.concatenate((petal1, petal2), 0)
petal += [25, 0]
flower = petal.copy()
for i in range(5):new_petal = np.dot(petal.copy(), rotate(np.radians(60) * (i + 1)).T)flower = np.concatenate((flower, new_petal), 0)

我们可以给每个坐标打上0或1的标签,0表示点是红色,1表示点是绿色。然后,我们把各个花瓣染成不同的颜色:

label = np.zeros([40 * 6])
label[0:40] = 1
label[40:80] = 1
label[120:160] = 1

再做一些操作就可以用matplotlib画出花朵了:

x = flower[:, 0]
y = flower[:, 1]c = np.where(label == 0, 'r', 'g')import matplotlib.pyplot as plt
plt.scatter(x, y, c=c)plt.xlim(-50, 50)
plt.ylim(-50, 50)plt.show()

在数据中加入噪声

大家可以发现,我生成的花朵数据中,有几个点的颜色“不太对劲”。这是为了模拟训练数据中的噪声数据。让我们看看这些噪声是怎么添加的。

为了让部分数据的标签出错,我们只需要随机挑选出一些数据,然后令它们的标签取反(0变1,1变0)即可。这里涉及一个问题:该怎样从n个数据中随机挑选出若干个数据呢?

在我项目中,我使用的方法如下:

from numpy.random import default_rngrng = default_rng()
noise_indice1 = rng.choice(40 * 6, 10, replace=False)
label[noise_indice1] = 1 - label[noise_indice1]

生成随机数需要一个随机数生成器。这里我用rng = default_rng()生成了一个默认的随机数生成器,它从均匀分布生成随机数。

noise_indice1 = rng.choice(40 * 6, 10, replace=False)用于生成多个不重复的随机数。rng.choice的第一个参数40*6表示生成出来的随机数位于区间[1, 40*6]。第二个参数10表示生成10个随机数。replace=False表示生成的随机数不重复。

最后,我们用label[noise_indice1] = 1 - label[noise_indice1]把随机选中的标签取反。

吴恩达《深度学习专项》笔记+代码实战(三):“浅度”神经网络相关推荐

  1. [转载]《吴恩达深度学习核心笔记》发布,黄海广博士整理!

    红色石头 深度学习专栏 深度学习入门首推课程就是吴恩达的深度学习专项课程系列的 5 门课.该专项课程最大的特色就是内容全面.通俗易懂并配备了丰富的实战项目.今天,给大家推荐一份关于该专项课程的核心笔记 ...

  2. 737 页《吴恩达深度学习核心笔记》发布,黄海广博士整理!

    点击上方"AI有道",选择"置顶"公众号 重磅干货,第一时间送达 深度学习入门首推课程就是吴恩达的深度学习专项课程系列的 5 门课.该专项课程最大的特色就是内容 ...

  3. 深度学习入门首推资料--吴恩达深度学习全程笔记分享

    本文首发于微信公众号"StrongerTang",可打开微信搜一搜,或扫描文末二维码,关注查看更多文章. 原文链接:(https://mp.weixin.qq.com/s?__bi ...

  4. 吴恩达深度学习课程笔记(四):卷积神经网络2 实例探究

    吴恩达深度学习课程笔记(四):卷积神经网络2 实例探究 吴恩达深度学习课程笔记(四):卷积神经网络2 实例探究 2.1 为什么要进行实例探究 2.2 经典网络 LeNet-5 AlexNet VGG- ...

  5. 免费分享全套吴恩达深度学习课程笔记以及编程作业集合

    分享吴恩达深度学习全套 笔记 笔记来源于吴恩达老师课程中口述翻译,并包含板书.可以取代看视频,做到更快速学习. (部分目录) (部分目录) (板书) 编程作业 扫描二维码后台回复"0&quo ...

  6. 吴恩达深度学习课程笔记(初步认识神经网络)

    吴恩达深度学习课程笔记1 课程主要内容 1.神经网络与深度学习介绍 2.Improving Deep Neural Networks:超参数调整,正则化,优化方法 3.结构化机器学习工程:比如如何分割 ...

  7. 吴恩达深度学习课程笔记-3

    吴恩达深度学习课程笔记-3 src="http://nbviewer.jupyter.org/github/HuaGuo1992/Blog/blob/master/%E5%90%B4%E6% ...

  8. 吴恩达深度学习第四课第一周 卷积神经网络

    文章目录 前言 一.计算机视觉(引言) 二.边缘检测示例(过滤器) 三.更多边缘检测内容(由亮到暗还是由暗到亮?) 四.Padding(Valid.Same.p) 五.卷积步长(s) 六.三维卷积(通 ...

  9. 吴恩达深度学习课程笔记之卷积神经网络(2nd week)

    0 参考资料 [1]  大大鹏/Bilibili资料 - Gitee.com [2] [中英字幕]吴恩达深度学习课程第四课 - 卷积神经网络_哔哩哔哩_bilibili [3]  深度学习笔记-目录 ...

  10. Andrew Ng吴恩达深度学习Course_1笔记

    基于吴恩达深度学习课程所记的相关笔记 目录 术语概念 第一周 深度学习概念 第二周 神经网络基础 Notation logistic回归函数 Loss function损失函数和Cost functi ...

最新文章

  1. k8s aws 部署_如何在短短30分钟内使用CircleCI设置到AWS S3的持续部署
  2. (0040) iOS 开发之10.3新特性:程序内评价
  3. Could not find a version that satisfies the requirement pox=0.2.6 (from pathos)
  4. AI目前的根本问题——缺乏 自由意志,无法分辨真正的善恶
  5. zabbix mysql.status_Zabbix 监控 Mysql 状态
  6. Loadrunner 入门连载教程
  7. NOIP模拟测试17「入阵曲·将军令·星空」
  8. Mac SecureCRT解决中文乱码
  9. mysql查看系统可用字符集_MySQL查看所有可用的字符集
  10. ES6——class和继承,保护对象
  11. 【LGP5161】WD与数列
  12. 【iOS】iOS开发之使用Mac自动操作制作@1x@2x@3x图片(切图)
  13. 【操作系统/OS笔记03】启动、中断、异常和系统调用
  14. 计算机应用基础试题及答案数据库,数据库原理试题及答案.doc
  15. Mysql数据库delete删除后数据恢复成功案例报告
  16. 从校园到职场 - 什么是职场经验
  17. 正大国际期货:国际期货中的八条看盘技巧!
  18. 程序员的国庆大阅兵,太好好好好好好看了吧 ……
  19. 想做网上线上引流怎么做?如何通过网络获取流量?
  20. ML笔记1——什么是ML;回归LossFunction(LF)推导;LF与凸函数关系;梯度下降推导;范数与正则化。

热门文章

  1. Matlab如何提取论文插图中的渐变色?一招轻松搞定
  2. 微信公众号开发--微信消息可以直接点击,然后请求中控服务器获取数据
  3. MySQL中文参考手册--1.MySQL的一般信息
  4. UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0xbd in position 23368: illegal multibyte sequence
  5. 高德地图2016清明出行交通预测报告(完整版)
  6. 如何使用Python tkinter 设计软件登录界面
  7. 编程中保护眼睛的颜色
  8. ImportError: TensorBoard logging requires TensorBoard version 1.15 or above 问题解决
  9. 36. 有效的数独(javascript)36. Valid Sudoku
  10. 从0开始教你三天完成毕业设计-项目设计