“本笔记参考了 吴恩达深度学习笔记 - 知乎

第一课 深度学习入门与神经网络

1. 深度学习入门

1.1 什么是神经网络

我们把一个有输入和输出的计算单元叫做“神经元”。最简单的神经元就是一个线性函数。比如预测房价和房屋面积的关系时,我们会用一个线性函数去拟合。这个线性函数就是一个神经元。

事实上,一个神经元不仅包含一个线性函数,还包括一个激活函数。这里提到了激活函数 ReLU 的概念,其具体内容应该会在后面的课程里介绍。

如图所示举例,不同的X变量如房子大小/房间数量/邮编地段 最后会得到一个房价的预测,省去了中间的自己完成推算的过程。

在用一个神经元来表示房价和房屋面积的关系时,神经元的输入是房屋面积,输出是房价。而用多层神经元时,每个神经元的意义可能都不一样。比如上图第一列神经元可能会根据输入的邮政编码、地址特征、周围财富状况( x_{1}...x_{4} ),输出一个表示房屋地段的中间特征。中间特征经过第二列的神经元,输出房价y  。在神经网络中,这些中间特征都是自动生成的(意味着我们只需要管理神经网络的输入和输出,而不用指定中间的特征,也不用理解它们究竟有没有实际意义)。

以前的一些机器学习要手动设置特征。而神经网络这种自动生成特征的性质,是其成功的原因之一。

1.2用神经网络做监督学习

常见的神经网络有三类:

  1. 标准神经网络(即全连接网络)可以用于房屋分类、广告分类问题。(这些问题一般输入是结构化的)
  2. 卷积神经网络(CNN)一般用于图像相关的问题,比如图片猫狗分类,自动驾驶中识别其他车辆的位置。
  3. 循环神经网络(RNN)一般用于处理有时序的序列数据,比如和音频、文字有关的应用都需要RNN。

所谓结构化数据,就是所有数据项都是人能理解的数据(比如房子的面积、价格)下图左侧。

与之相对,无结构化数据的具体含义是无法直接解释的,比如图像每一个像素值、音频某时刻的频率和响度、某一个文字/单词。 

 1.3深度学习崛起的背后力量

在数据量比较小的阶段,几种算法排名不明显,更取决于数据特征函数以及算法的细节。

但是数据量比较大的时候,谁的性能上限更高就可以看出来了。

这张图足以解释深度学习腾飞的原因。随着数据量的增加,所有AI方法都有性能的上限。而对于神经网络来说,增加神经网络的复杂度,可以提升它的性能上限。复杂的神经网络(深度学习方法)在海量数据不断产生的今天上限更高,更具优势。

光有大量的数据,没有使用数据的方法是不够的。总结来看,深度学习在近几年得到发展的原因有下:

  • 互联网的发展使得数字数据大量增长。
  • GPU等计算设备使得处理数据的硬件变强。
  • 深度学习的算法不断更新迭代,从软件层面上加快了数据处理。(比如激活函数的改进,从sigmoid到ReLU)

深度学习本质上还是以实验为主。计算能力上来了,研究人员做实验做得快了,各种各样的深度学习的应用也就出来了。各种应用又鼓舞着更多人参与深度学习研究。

2. 简单的神经网络-逻辑回归

在计算机中,颜色主要是通过RGB(红绿蓝) 三种颜色通道表示。每一种通道一般用长度8位的整数表示,即用一个0~255的数表示某颜色在红绿蓝上的深浅程度。这样,一个颜色就可以用一个长度为3的向量表示。一副图像,其实就是许多颜色的集合,即许多长度为3的向量的集合。

颜色通道再算上某颜色所在像素的位置(x,y),图像就可以堪称一个三维张量I\in R^{H*W*3},其中H是图像高度,W是图像宽度,3是图像通道数,在把图像输入逻辑回归时,我们会把图像“拉直”成一个一维向量。这个向量就是特征向量。如上图所示X时12288行的一维列向量。

实际上,我们有很多个训练样本。样本总数记为 m 。第i个训练样本叫做\left ( x^{(i)} ,y^{(i)}\right )  。在后面使用其他标记时,也会使用上标\left ( i \right )  表示第i个训练样本得到的计算结果。 

为了后续神经网络计算方便,我们把特征向量写成下面的形式:

X=\begin{bmatrix} | & |& & |\\ x^{\left ( 1 \right )}& x ^{\left ( 2 \right )}& ...& x^{\left ( m \right )}\\ |& |& & | \end{bmatrix} ,X\in R^{n_{x}\times m}

那么Y值可以写成向量为:

Y=\left [ y^{\left ( 1 \right )} , y^{\left ( 2\right )}...y^{\left ( m \right )} \right ] Y\in R^{m}

2.1逻辑回归的公式描述

逻辑回归是一个学习算法,用于对真值只有0或者1的"逻辑"问题进行建模。给定输入x,逻辑回归输出一个\hat{y}.这个\hat{y}是对真值y的一个估计,准确来说,他描述的y=1的概率,即\hat{y}=P(y=1|x)

关于拟合函数,最容易想到的是线性函数w^{T}x+b(即做点乘再加b: w^{T}x +b = (\sum_{i=1}^{n_{x}}w_{I}x_{i}+b))。但线性函数的值域是\left (- \infty,+ \infty \right ),,概率的取值是[0,1]  。我们还需要一个定义域为R  ,值域为 [0,1] ,把线性函数映射到[0,1]  上的一个函数。

逻辑回归中,使用的映射函数是sigmoid函数\sigma,它的定义为\sigma \left ( z \right )=\frac{1}{1+e^{-z}}

这个函数可以有效地完成映射,它的函数图像长这个样子:

只需要知道这个函数的趋势: z 越小,  \sigma \left ( z \right )越靠近0;x越大,  \sigma \left ( z \right )越靠近1。 

最终的逻辑回归公式\hat{y}=\sigma (w^{T}x+b)

2.2逻辑回归的损失函数(cost Function)

所有的机器学习问题本质上是优化问题,一般我们会定义一个损失函数,再通过优化参数来最小化这个损失函数。

回顾一下我们的任务目标:我们定义了逻辑回归公式\hat{y}=\sigma (w^{T}x+b),我们希望\hat{y}尽可能和y相近。这里的“相近”就是优化目标,损失函数,可以堪称是y, \hat{y}间的距离。

逻辑回归中,定义了每个输出和真值的误差函数,这个误差函数叫交叉熵L(\hat{y},y)=-\left ( log{\hat{y}} +(1-y)log(1-\hat{y}))\right )

 不使用另外一种常见的误差函数均方误差的原因是,交叉熵较均方误差梯度更加平滑,更容易在之后的优化中找到全局最优解。

误差函数是定义在每个样本上的,而损失函数是定义在整个样本上的,表示所有样本误差的“总和”的平均值。

J\left ( w,b \right )=\frac{1}{m}\sum_{i=1}^{m}-\left ( y^{\left ( i \right )} log\left ( \hat{y}^{\left ( i \right )} \right )+(1-y^{\left ( i \right )}log\left ( 1-\hat{y} ^{\left ( i \right )}\right )))\right )

2.3优化算法--梯度下降(Gradient Descent)

梯度下降的思想很符合直觉:如果要让函数值更小,就应该让函数的输入沿着函数值下降最快的方向(梯度的方向)移动。

一元函数的梯度值就是导数值,方向只有正和负两个方向。我们要根据每个点的导数,让每个点向左或向右“运动”,以使函数值更小。

为了让优化能顺利进行,梯度下降法使用学习率(Learning Rate) 来控制参数优化的“步伐”,即用如下方法更新损失函数  参数:

w\leftarrow w-\alpha \frac{dJ}{dw}

这里的  就是学习率,它控制了每次梯度更新的幅度。

其实这里还有两个问题:参数 w,b 该如何初始化;该执行梯度下降多少次。在这个问题中初始化对结果影响不大,可以简单地令w=0  。而优化的次数没有硬性的需求,先执行若干次,根据误差是否收敛再决定是否继续优化即可。优化的内容就是使得J(w,b)近可能小。

2.4 计算图

其实,所有复杂的数学运算都可以拆成计算图表示法。

比如上图中,哪怕是简单的运算  ,也可以拆成两步:先算  ,再算  。

这里的“步”指原子运算,即最简单的运算。原子运算可以是加减乘除,也可以是求指数、求对数。复杂的运算,只是对简单运算的组合、嵌套。

明明简简单单可以用一行公式表示的事,要费很大的功夫画一张计算图呢?这是因为,对函数求导满足链式法则。借助计算图,可以更方便地用链式法则算出所有参数的导数。比如在上图中要求f  对  a的导数,使用链式法则的话,可以通过先求 f 对  c的导数,再求c  对  a的导数得到。

 2.5利用计算图对逻辑回归求导

z=w_{1}x_{1}+w_{2}x_{2}+b 

a=\frac{1}{1+e^{-z}}

L=-(yloga+(1-y)log(1-a))

\frac{dL}{da}=-(\frac{y}{a}-\frac{1-y}{1-a})

\frac{da}{dz}=\frac{e^{-z}}{\left ( 1+e^{-z} \right )^{2}}=a(1-a)

\frac{dL}{dz}=\frac{dL}{da}\frac{da}{dz}=...=a-y

\frac{dL}{dw_{i}}=\frac{dL}{dz}\frac{dz}{dw_{i}}=(a-y)x_{i}

\frac{dL}{db}=\frac{dL}{dz}\frac{dz}{db}=(a-y)

\frac{dL}{dw_{i}}, \frac{dL}{db} 就是我们要的梯度了,用它们去更新原来的参数即可 

2.6Python向量化计算

import numpy as np
import time
a = np.random.rand(1000000)
b = np.random.rand(1000000)
print(a)
tic = time.time()
c = np.dot(a,b)
toc = time.time()
print("Vectorized version:" +str(1000*(toc-tic))+"ms")

c = 0
tic = time.time()
for i in range(1000000):
    c += a[i]*b[i]
toc = time.time()
print("for Loop version:" +str(1000*(toc-tic))+"ms")

[0.7673079 0.70045255 0.91818371 ... 0.84624675 0.50542048 0.375106 ]

Vectorized version:5.6400299072265625ms

for Loop version:1155.3120613098145ms 

note: np.dot(a,b)是a^{T}b

上述代码表示 直接拿 Python 写这些 for 循环,程序会跑得很慢的。这里最好使用向量化计算。

import numpy np

u = np.exp(v) 对每个元素做指数

np.log(v) 对每个对数求log

np.abs(v) 对每个元素

np.maxmum(v,0)对每个元素求最大值

v**2 //对每个元素求平方

1/v 对每个元素求倒数

简单来说如上图所示向量化的逻辑回归是

A=[a^{(1) },a^{2},....a^{(m)}]=\sigma \left ( z \right )

z=[z^{(1)},z^{^{(2)}}...z^{(m)}]=w^{T}X+[b,b...b] = np.dot(w^{T},X)+b 

2.7向量化logistic回归的梯度下降

 db = \frac{1}{m}\sum_{i=1}^{m}dz^{(i)}=\frac{1}{m}np.sum(dz)

dw=\frac{1}{m}Xdz^{T}=X=\begin{bmatrix} | & |& & |\\ x^{\left ( 1 \right )}& x ^{\left ( 2 \right )}& ...& x^{\left ( m \right )}\\ |& |& & | \end{bmatrix} \begin{bmatrix} dz^{(1))}\\ ...\\ dz^{(m)} \end{bmatrix}

=\frac{1}{m}\left [ x^{(1)}dz^{(1)}+...x^{(m)} dz^{(m))}\right ]

如下图所示,是完整的逻辑回归梯度下降的一次迭代。

用代码描述多样本前向传播和反向传播就是:

Z = np.dot(w.T, x)+b
A = sigmoid(Z)
dZ = A-Y
dw = np.dot(X, dZ.T) / m
db = np.mean(dZ)
# db=np.sum(dZ) / m
np.dot实现了求向量内积或 矩阵乘法np.sum实现了求和, np.mean实现了求均值。

Python中的广播

numpy 允许一种叫做“广播”的操作,这种操作能够完成不同形状数据间的运算。

a = np.ones(10) # a的形状:[10]
k = np.array([3]) # 用列表[3]新建张量,k的形状:[1]
a = k * a # 广播

这里k的shape为[1] ,a的shape为[10] 。用k乘a,实际上就是令a[i] = k[0] * a[i] 。也就是说,k[0] “广播”到了a 的每一个元素上。

有一种快速理解广播的方法:可以认为k的形状从[1] 变成了[10] ,再让k和a逐个元素做乘法。

同理,如果用一个a[x, y] 的矩阵加一个b[x, 1] 的矩阵,实际上是做了下面的运算:

for i in range(x):
  for j in range(y):
    a[i, j] = a[i, j] + b[i, 0] 

用刚刚介绍的方法来理解,可以认为b 从[x, 1] 扩充成了[x, y] ,再和a 做逐个元素的加法运算

Python numpy的使用技巧

 a = np.random.randn(5) 所以这只是秩为1的数组,行为和行向量和列向量完全不一致

如果想创建向量和矩阵需要下面操作

总结

这堂课的主要知识点有:

  • 什么是二分类问题。
  • 如何对建立逻辑回归模型。
    • Sigmoid 函数 
  • 误差函数与损失函数
    • 逻辑回归的误差函数: 
  • 梯度下降算法优化损失函数
  • 计算图的概念及如何利用计算图算梯度

学完这堂课后,应该掌握的编程技能有:

  • 了解numpy基本知识
    • resize
    • .T
    • exp
    • dot
    • mean, sum
  • 用numpy做向量化计算
  • 实现逻辑回归
    • 对输入数据做reshape的预处理
    • 用向量化计算算  及参数的梯度
    • 迭代优化损失函数

3.“浅度”神经网络

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

3.1单样本多神经元的计算

如下图所示是两层的神经网络结构: 

详细来看的话如下图:

 总结一下就是:

输入x是一个形状为3\times 1的特征向量,第一层有四个神经元,神经元的

参数分别是 (w_{1}^{[1]},b_{1}^{[1]}) ,(w_{2}^{[1]},b_{2}^{[1]}), (w_{3}^{[1]},b_{3}^{[1]}),(w_{4}^{[1]},b_{4}^{[1]})w_{i}^{[1]}的形状是1\times 3b_{i}^{(1)}是常数

每个神经元的计算公式和上节课的逻辑回归相同,都是z_{i}^{(i)}=w_{i}^{(1)}x+b_{i}^{(1)}a_{i}^{(1)}=\sigma (z_{i}^{(i)})

因为有四个神经元,我们得到四个计算结果a_{1}^{(1)},a_{2}^{(1)},a_{3}^{(1)},a_{4}^{(1)},可以当作4\times 1的列向量a^{(1)},             

就像输入x 一样。

上图中w_{1},w_{2},w_{3},w_{4}都是第一层的3\times 1的矩阵,统称为w_{i}^{(1)}

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

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

3.2多样本向量化

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

对于第i个输入样本x^{(i)} 我们要计算:

a^{[1](i)}=\sigma (W^{[1]}x^{(i)}+b^{[1]})

a^{[2](i)}=\sigma (W^{[2]}a^{[1](i)}+b^{[2]})

直接写的话,我们要写个for循环,把输入打包在一起形成一个n_{x}\times m的矩阵X(m是样本总数)

把i从0遍历到m的矩阵X,那么整个计算过程可以用向量化计算公式:

A^{[1]}=\sigma (W^{[1]}X+b^{[1]})

A^{[2]}=\sigma (W^{[2]}A^{[1]}+b^{[2]})

X和A被横向堆叠了

 其中A^{[l]}代表第l层的特征矩阵,  a^{[l](i)} 表示第l层第i列的新的特征矩阵(也可以说第i个样本列)

3.3激活函数

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

他们的数学公式如下:

sigmoid(x)=\frac{1}{1+e^{-x}} 

tanh(x) = \frac{e^{x}-e^{x}}{e^{x}+e^{x}}

relu(x)=\left\{\begin{matrix} x & (x\geq 0)\\ 0& (x< 0) \end{matrix}\right.

leaky-relu(x)=\left\{\begin{matrix} x& (x\geq 0)\\ kx& (x< 0) \end{matrix}\right.

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

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

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

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

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

3.4 如何选择激活函数

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

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

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

A^{\left ( 1 \right )}=g^{\left ( 1 \right )}\left ( W^{\left ( 1\right )}X+b^{\left ( 1 \right )} \right )

A^{\left ( 2\right )}=g^{\left ( 2 \right )}\left ( W^{\left ( 2\right )}A^{(1)}+b^{\left ( 2 \right )} \right )

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

3.5 激活函数为什么不选线性的

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

\hat{y}=g(W_{2} \cdot g(W_{1}x+b_{1})+b_{2})

其中激活函数用g  表示。

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

\hat{y}=g(W_{2} \cdot g(W_{1}x+b_{1})+b_{2})=(W_{2}W_{1})x+(b_{1}+b_{2})

我们把 (W_{2}W_{1}) 看成一个新的“W ”,  b1+b2看成一个新的" b ",那么这其实是一个单层神经网络。

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

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

激活函数的导数

3.6 对神经网络做梯度下降

 

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

w\leftarrow w-\alpha \frac{dJ}{dw}

b\leftarrow b-\alpha \frac{dJ}{db}

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

如今四个参数:W^{[1]},W^{[2]},b^{[1]},b^{[2]},他们也应该按照同样的规则执行梯度下降:

W^{[1]}\leftarrow W^{[1]}-\alpha \frac{dJ}{dW^{[1]}}

W^{[2]}\leftarrow W^{[2]}-\alpha \frac{dJ}{dW^{[2]}}

b^{[1]}\leftarrow b^{[1]}-\alpha \frac{dJ}{db^{[1]}}

b^{[2]}\leftarrow b^{[2]}-\alpha \frac{dJ}{db^{[2]}}

正向传播:

Z^{[1]}=W^{[1]}X+b^{[1]}

A^{[1]}=g^{[1]}(Z^{[1]})

Z^{[2]}=A^{[2]}-Y

A^{[2]}=g^{[2]}(Z^{[2]})

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

dZ^{[2]}=A^{[2]}-Y

dW^{[2]}=\frac{1}{m}dZ^{[2]}A^{[1]T}

db^{[2]}=\frac{1}{m}\sum_{i=1}^{m}dZ^{[2](i)}

注意! 上周我们算的是 AdZ^{T} ,这周是dZ^{[2]}A^{[1]T}  。这是因为参数W  转置了一下。上周的w  是列向量,这周每个神经元的权重 W_{i} 是行向量。

之后,我们来看第一层。首先求dZ^{[1]}:

dZ^{[1]}=\frac{dA^{[1]}}{dZ^{[1]}}

dZ^{[1]}=W^{[2]T}dZ^{[2]}*g^{[1]'}(Z^{[1]})

之后的dW^{[1]}=\frac{1}{m}dZ^{[1]}X^{T}

db^{[1]}=\frac{1}{m}\sum_{i=1}^{m}dZ^{[1](i)}

 其中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

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

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

3.7 随机初始化

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

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

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

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

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

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

知识总结

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

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

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

        一些求导过程:

画本文的激活函数图
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 * 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()把图片显示出来就大功告成了。

4. 深度神经网络

4.1深层神经网络概述与符号标记

所谓深度神经网络,只是神经网络的隐藏层数量比较多而已,它的本质结构和前两课中的神经网络是一样的,让我们再复习一下神经网络的标记:

L表示网络的层数:

在下图中L=4,(注意:输入层冰不计入层数,但可以用“0”层称呼输入层)

l\in [0,L],比如,n^{[l]}是神经网络第l层的神经元数,如下图:

n^{[1]}=5, n^{[3]}=3,以此类推。值得注意的是n^{[0]}=n_{x}=3,  。回想第二课的知识,  n_{x}是输入向量的长度。 

a^{[l]}l层的输出向量,a^{[l]}=g^{[l]}\left ( z^{[l]} \right ),其中g^{[l]}是第l层的激活函数,

z^{[l]} 是第 l 层的中间运算结果。  W^{[l]},b^{[l]}是第l  层的参数。

和上节课的单隐层神经网络类似,对于 L 层的网络,我们如下方法对单样本做前向传播(推理):

a^{[l]}\leftarrow g^{[l]}(W^{[l]}a^{[l-1]}+b^{[l]})

for l\in [1,2,3...L]

其中,输入输出为:x=a^{[0]},\hat{y}=a^{[L]}

当我们考虑全体样本X,Y,

A^{[l]}\leftarrow g^{[l]}(W^{[l]}A^{[l-1]}+b^{[l]})

for l\in [1,2....L]

其中,输入输出分别为:X=A^{[0]},\hat{Y}=A^{[L]}

4.2向前和反向传播


4.3深层网络中的向前传播

从公式上看,使用向量化计算全体样本只是把小写字母换成了大写字母而已。用代码实现时,我们甚至也只需要照搬上述公式就行。但我们要记住,全体样本是把每个样本以列向量的形式横向堆叠起来,堆成了一个矩阵。我们心中对  X,Y的矩阵形状要有数。

在实现深度神经网络时,我们不可避免地引入了一个新的for循环:循环遍历网络的每一层。这个for循环是无法消除的。要记住,我们要消除的for循环,只有向量化计算中的for循环。它们之所以能被消除,是因为向量化计算可以使用并行加速,而不是for循环本身有问题。我们甚至可以把“向量化加法”、“向量化乘法”这些运算视为最小的运算单元。而在写其他代码时,不用刻意去规避for循环。 

4.4核对矩阵的维数

参数矩阵的形状是: 

W^{[l]}:(n^{[l]},n^{[l-1]}) 

b^{[l]}:\left ( n^{[l]},1 \right )

每一层的输入输出矩阵形状是: A^{[l]}:\left ( n^{[l]},m \right )

如果忘了 W 的形状,就拿 矩阵乘法形状规则 (a,b)\cdot (b,c)(a,c)  推一下。

某张量的梯度张量与其形状相同。比如  dW的形状也是  (n^{[l]},n^{[l-1]})

 4.5为什么使用深层表示

 上图所示的人脸检测网络,网络的第一层是别的可能是图片的边缘,第二层识别的是人的五官,第三层是别的事整个人脸,更深的网络有助于捕捉更高层次更全面的特征。

另外,从计算复杂度的角度来看,用更深的网络,而不是在同一层网络里叠加更多神经元,往往更轻易的拟合出一个函数,比如要拟合a+b+c+d,我们可以先计算(a+b)(c+d).再算(a+b)+(c+d)。这只需要2“层”计算。如果把上面4个数相加变成n  个数相加,我们只需要构造 long 层网络。但如果用单层网络拟合  个数相加,网络可能要尝试a,b,a+b,c,a+b+c...一共2^{n}种公式,需要在1层里放O(2^{n})个神经元。

深度神经网络好不好用,究竟用多少层的网络,这些决定都取决于实际的问题。只不过大多数任务用深度神经网络实现都能生效。

 4.6搭建设层神经网络块

前两节课,我们的网络只有1层或2层。我们或许可以直接写出它们的训练步骤。现在,对于 层的网络,我们必须系统化地写出它们的训练流程。

上面的公式里,默认损失函数  

 从算法的角度来看,梯度下降法只需要用反向传播算梯度。我们这里之所以做一遍正向传播,是因为反向传播要用到正向传播的中间运算结果。从逻辑关系来看,是反向传播函数调用了正向传播函数,而不是“先正向传播,再反向传播”的并列关系,虽然编程时是用后者来表达。

4.7参数与超参数 

之前,我们在不经意间就已经接触了“超参数”这个词,但一直没有对它下一个定义。现在,我们来正式介绍超参数这个概念,以及它和参数的关系。

对于我们之前的神经网络,参数包括:

  • W
  • b

这些参数和数学里的参数意义一样,表示函数的参数。

而超参数则包括:

  • 学习率 \alpha
  • 训练迭代次数
  • 网络层数 L

我们直接从超参数的作用来给超参数下定义。超参数的取值会决定参数W,b  的取值,它们往往只参与训练,而不参与最后的推理计算。可以说,除了网络中要学习的参数外,网络中剩下的可以变动的数值,都是超参数。

一个简单区别超参数的方法是:超参数一般是我们手动调的。我们常说“调参”,说的是超参数。

吴恩达老师鼓励我们多尝试调参,对于不同的问题可以尝试不同的超参数。

4.8神经网络与大脑

生物的神经由“树突”,“轴突”等部分组成。生物信号会通过这些部分在神经里传播。神经网络的工作原理和生物神经的原理有那么一点类似

但迄今为止,生物神经的原理还没有被破解。我们把神经网络当成一个  的映射就好了。

总结

这一课主要是介绍编程实现的思路,没有过多的知识点:

  • 实现任意层数的神经网络
  • 前向传播
  • 反向传播
  • 缓存信息
  • 为什么用更深的网络
  • 分辨超参数与参数
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐