[C1W3] Neural Networks and Deep Learning - Shallow neural networks

第三周:浅层神经网络(Shallow neural networks)

神经网络概述(Neural Network Overview)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

神经网络的表示(Neural Network Representation)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

神经网络中,我们将使用上标方括号的形式表示这些值来自于哪一层,有趣的是在约定俗成的符号传统中,在这里你所看到的这个例子,只能叫做一个两层的神经网络。原因是输入层是不算入总层数内,所以隐藏层是第一层,输出层是第二层。第二个惯例是我们将输入层称为第零层,所以在技术上,这仍然是一个三层的神经网络,因为这里有输入层、隐藏层,还有输出层。但是在传统的符号使用中,如果你阅读研究论文或者在这门课中,你会看到人们将这个神经网络称为一个两层的神经网络,因为我们不将输入层看作一个标准的层。

计算一个神经网络的输出(Computing a Neural Network's output)

单个样本时,向量化图示

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

单个样本时,运用四行代码计算出一个简单神经网络的输出(预测)。

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

多样本向量化(Vectorizing across multiple examples)

上一节使用的是单个样本,如果把单个样本的向量横向堆叠成矩阵,就可以计算全部样本的神经网络输出(预测)。

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

向量化实现的解释(Justification for vectorized implementation)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

激活函数(Activation functions)

Pros and cons of activation functions

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

结果表明,如果在隐藏层上使用 tanh (双曲正切)函数效果总是优于 sigmoid 函数。因为函数值域在 -1 和 +1 的激活函数,其均值是更接近 0 均值的,而不是0.5,这会使下一层学习简单一点。
在讨论优化算法时,有一点要说明:我基本已经不用 sigmoid 激活函数了,tanh 函数在所有场合都优于 sigmoid 函数。
但有一个例外:在二分类的问题中,对于输出层,想让的数值介于 0 和 1 之间,而不是在 -1 和 +1 之间,所以需要使用 sigmoid 激活函数。

sigmoid 函数和 tanh 函数两者共同的缺点是,在 \(Z\) 特别大或者特别小的情况下,导数的梯度或者函数的斜率会变得特别小,最后就会接近于0,导致降低梯度下降的速度。

在机器学习另一个很流行的函数是:修正线性单元的函数(ReLu)。只要 \(Z\) 是正值的情况下,导数恒等于1,当 \(Z\) 是负值的时候,导数恒等于 0。从实际上来说,当使用 \(Z\) 的导数时,\(Z=0\) 的导数是没有定义的。但是当编程实现的时候,\(Z\) 的取值刚好是 0.0000000000 的概率很低,所以不用担心这个。你也可以在 \(Z=0\) 时 ,给它的导数赋值 0 或 1 都可以。

这有一些选择激活函数的经验法则:

如果输出是0、1值(二分类问题),则输出层选择 sigmoid 函数,然后其它的所有单元都选择 Relu 函数。这是很多激活函数的默认选择,如果在隐藏层上不确定使用哪个激活函数,那么通常会使用 Relu 激活函数。有时,也会使用 tanh 激活函数,Relu 的一个缺点是:当 \(Z\) 是负值的时候,导数等于0,在实践中这没什么问题。

但 Relu 还有另外一个版本叫 Leaky Relu。当 \(Z\) 是负值时,这个函数的导数不是为 0,而是有一个平缓的斜率。这个函数通常比 Relu 激活函数效果要好,尽管在实际中 Leaky ReLu 使用的并不多。这些选一个就好了,我通常只用 Relu。

Relu 和 Leaky Relu 的好处在于,for a lot of the space of \(Z\),激活函数的导数和 0 差很远,所以在实践中使用 Relu 激活函数,你的神经网络学习速度通常会快很多。原因是 Relu 没有这种函数斜率接近 0 时,减慢学习速度的效应。我知道对于 \(Z\) 的一半范围来说,Relu 的斜率为 0,但在实践中,有足够多的隐藏单元令 \(Z\) 大于 0,所以对于大多数训练样本来说还是很快的。

在选择自己神经网络的激活函数时,有一定的直观感受,在深度学习中的经常遇到一个问题:在编写神经网络的时候,会有很多选择:隐藏层单元的个数、激活函数的选择、初始化权值……这些选择想得到一个对比较好的指导原则是挺困难的。

鉴于以上三个原因,以及在工业界的见闻,提供一种直观的感受,哪一种工业界用的多,哪一种用的少。但是,自己的神经网络的应用,以及其特殊性,是很难提前知道选择哪些效果更好。所以通常的建议是:如果不确定哪一个激活函数效果更好,可以把它们都试试,然后在交叉验证集或者在我们稍后会讲到的开发集上跑跑,看看哪一个表现好,就用哪个。在你的应用中自己多尝试下不同的选择,很可能你会搭建出具有前瞻性的神经网络架构,可以对你的问题的特质更有针对性,让你的算法迭代更流畅。这里不会告诉你一定要使用 Relu 激活函数,而不用其他的,这对你现在或未来要处理的问题而言,可能管用,也可能不管用。

为什么需要非线性激活函数?(why need a nonlinear activation function?)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

如果你使用线性激活函数或者叫恒等激励函数,那么神经网络只是把输入线性组合再输出。

如果你使用线性激活函数或者没有使用一个激活函数,那么无论你的神经网络有多少层一直在做的只是计算线性函数,所以不如直接去掉全部隐藏层。

事实证明如果你在隐藏层用线性激活函数,在输出层用 sigmoid 函数,那么这个模型的复杂度和没有任何隐藏层的标准Logistic回归是一样的。

除非你引入非线性,否则你无法计算更有趣的函数,即使你的网络层数再多也不行;只有一个地方可以使用线性激活函数,就是你在做机器学习中的回归问题。举个例子,\(y\) 是一个实数,比如你想预测房地产价格, 就不是二分类任务 0 或 1,而是一个实数,从 0 到正无穷。如果 \(y\) 是个实数,那么在输出层用线性激活函数也许可行,你的输出 \(y\) 也是一个实数,从负无穷到正无穷。而且,此时隐藏层也不能用线性激活函数,可以用 ReLU 或者 tanh 或者 leaky ReLU 或者其他的非线性激活函数,唯一可以用线性激活函数的通常就是输出层;除了这种情况,会在隐层用线性函数的,还有一些特殊情况,比如与压缩有关的,不在这里深入讨论。另外,因为房价都是非负数,所以我们也可以在输出层使用 ReLU 函数,这样你的 \(\hat{y}\) 都大于等于 0。

激活函数的导数(Derivatives of activation functions)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

sigmoid 导数为 \(a(1-a)\),当 a 已知的时候 ,可以很方便计算出其导数。

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

tanh 导数为 \(1-a^2\),当 a 已知的时候 ,可以很方便计算出其导数。

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

注:

  • ReLu 导数为 0 或 1,通常在 \(Z = 0\) 的时候导数 undefined,直接将导数赋值 0 或 1;当然 \(Z = 0.000000000\) 的概率很低。
  • Leaky ReLu 导数为 0.01 或 1,通常在 \(Z = 0\) 的时候导数 undefined,直接将导数赋值 0.01 或 1;当然 \(Z = 0.000000000\) 的概率很低。

神经网络的梯度下降(Gradient descent for neural networks)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

这里 np.sum 是 python 的 numpy 命令,axis=1 表示水平相加求和,keepdims = True 是防止 python 输出秩为 1 的数组,如果不使用 keepdims,可以可以使用 reshape 显性指明数组的维度。

图中唯一的一个乘号是元素相乘。

以上就是正向传播的 4 个方程和反向传播的 6 个方程,这里我是直接给出的,在下一节,我会讲如何导出反向传播的这 6 个式子的。如果你要实现这些算法,你必须正确执行正向和反向传播运算,你必须能计算所有需要的导数,用梯度下降来学习神经网络的参数;

(选修)直观理解反向传播(Backpropagation intuition)

[C1W3] Neural Networks and Deep Learning - Shallow neural networks

  • \(J\) 中第一个求和,是最后一层所有激活项所有元素整体求和,最后是一个常数。第二个求和是所有层中,所有 \(W\) 中的所有元素求和,最后也是一个常数,所以 \(J\) 最后也是一个常数,shape 为 (1, 1)。

  • 紫色为正则化项,因为 \(J\) 中正则化项包含 \(W\),所以使用链式求导法则求到 \(W\) 时,即需要对 \(W\) 求全导数时,还需要把 \(J\) 中的正则化项(因为包含 \(W\))带过来。求导参考连接,其他时候该项导数为 0,可以忽略。

  • \(L\) 表示为神经网络的层数,在计算神经网络层数时,\(X\)(即:\(A^{[0]}\)),即:输入层,不被算入在内,或者也可以把它称作神经网络的第 0 层。所以图中演示的神经网络有 2 层,所以 \(L = 2\).

  • \(n^{[l]}\) 表示第 \(l\) 层的激活单元数,本例中 \(n^{[0]} = 2\)(输入层样本有两个特征), \(n^{[1]} = 4\)(隐藏层有 4 个激活单元), \(n^{[2]} = 1\)(最后一层,即输出层有 1 个激活单元)。因为 \(n^{[2]}\) 是最后一层,所以 \(n^{[L]}=n^{[2]}\)。

  • \(m\) 表示样本数量,图中示例有 200 个样本,所以 \(m = 200\),图中涉及 \(m\) 的地方均用红色字体高亮显示,方便比对查看。另外,在计算 \(dW\) 和 \(db\) 时,最后需要除以一个 \(m\),这是因为 \(J\) 中也带有一个 \(\frac{1}{m}\) 项,所以当链式求导求导终点的时候,需要把它带过来。此处求导终点的意思是 \(dW\) 和 \(db\) 里面没有复合函数存在了,所以是求导的重点。对比理解当求导求到 \(dA\) 时,它仍是一个复合函数,里面还包含其他函数,所以不是求导终点。它继续求导,可以求出下一层的 \(dZ\),再继续可以求出下一层的 \(dW\) 和 \(db\),到此又是一个求导的终点。

  • 关于绿色的 \(dW\) 和 \(db\),也即梯度。\(dW\) 中存在矩阵乘法,内含求和操作,向量化的方式计算高效,代码简洁。但是 \(db\) 在计算时没有矩阵乘法,也就是没有求和操作,所以需要用其他方法来完成,图中在计算 \(db^{[1]}\) 和 \(db^{[2]}\) 时分别记录了两种方式来对 \(db\) 按行求和。np.sum(\(dZ^{[1]}\), axis=1, keepdims=True) 是调用 numpy 中的 sum 方法来求和,axis = 1 表示对每行进行求和,keepdims 表示求和后保留其维度特性。另外一种求和方式是初始化一个全部为 1 的矩阵,然后让 \(dZ\) 与其进行矩阵相乘,言外之意就是做求和的操作,异曲同工。

  • 关于褐红色 \(dZ\),可参考多元复合函数的链式求导法则,其间均为点号相连,表示相乘。式中的第二个等号是其向量化的表示,可以看到其中既有矩阵乘,也有元素乘(*),因为是向量化表示,所以需要合理的组织和选择向量的前后位置,是否需要转置,使用矩阵乘还是元素乘等等,使之达到正确计算第一个等号列出的链式求导公式的目的。可以看到图中反向传播求导时都是直接求到 \(dZ\),跨过了 \(dA\),因为每一层的激活单元可能不同,所以 \(dA\) 的导数都是使用 \({g^{[l]}}^{'}(Z^{[l]})\) 来代替,以不变应万变。另外,\(dZ^{[l]}\) 可以推导出它上一层的 \(dZ^{[l-1]}\),而每一层的 \(dZ\) 又可以直接推到出该层的 \(dW\) 和 \(db\)。所以在编写程序时,直接计算出并缓存各个层的 \(dZ^{[l]}\) 是一个非常好的设计,既可以计算本层的 \(dW\) 和 \(db\),也可以计算上一层的 \(dZ\)。它就相当于一个计算梯度的中介和桥梁。

  • 关于图中垂直的红色虚线,被它贯穿的向量维度均一致。如此方便比对神经网络在计算时各个步骤间向量呈现出的维度特性和变化规律。比如:
    \(A^{[l]}.shape=(n^{[l]}, m)\)
    \(Z^{[l]}.shape=dZ^{[l]}.shape=(n^{[l]}, m)\)
    \(W^{[l]}.shape=dW^{[l]}.shape=(n^{[l]}, n^{[l-1]})\)
    \(b^{[l]}.shape=db^{[l]}.shape=(n^{[l]}, 1)\)
    \(J.shape=(1, 1)\)
    如此可以更合理和高效的使用 Python 的 assert :
    assert (\(A^{[l]}\).shape == (\(n^{[l]}\), m));
    assert (\(Z^{[l]}\).shape == \(dZ^{[l]}\).shape == (\(n^{[l]}\), m));
    assert (\(W^{[l]}\).shape == \(dW^{[l]}\).shape == (\(n^{[l]}\), \(n^{[l-1]}\)));
    assert (\(b^{[l]}\).shape == \(db^{[l]}\).shape == (\(n^{[l]}\), 1));
    assert (J.shape == (1, 1));

随机初始化(Random+Initialization)

如果把 \(W\) 全部初始化为 0,意味着每个隐藏层中所有激活单元的权重全部相同,输出也全部相同,当梯度下降算法计算出更新 \(W\) 的梯度之后,梯度也完全相同。如此的隐藏层毫无意义,无法自动的捕捉到一些有意思的特征,所以在初始化 \(W\) 的时候需要打破这种对称性(symmetry breaking problem),令外 \(b\) 在初始化时可以全部初始化为 0,因为 \(W\) 已经打破对称,整个激活单元数出也就打破对称了。

如何初始化?

  1. 在吴恩达老师机器学习课程中,初始化神经网络参数的方法是让 \(W\) 为一个在 \([-\epsilon, \epsilon]\) 之间的随机数。
    epsilon_init = 0.12;
    np.random.rand(\(s_l\), \(s_{l-1}\)) * 2 * epsilon_init - epsilon_init

  2. 在吴恩达老师本门课程(深度学习)中,使用如下方法:
    \(W^{[1]}\)=np.random.randn(\(s_l\), \(s_{l-1})\) * 0.01 (生成高斯分布)
    \(b^{[1]}\) = np.zeros((\(s_l\), 1))
    之所以 * 0.01,而不是 100 或 1000 ...是因为通常我们倾向于将 \(W\) 初始化为一个很小的随机数。试想,如果 \(W\) 很大, 导致 \(Z\) 过大或过小都会令 tanh 或者 sigmoid 激活函数的梯度处于平坦接近 0 的地方,从而导致龟速梯度下降。如果没有 sigmoid / tanh 激活函数在整个的神经网络里,就不成问题。但如果你做二分类并且你的输出单元是 Sigmoid 函数,那么你是不会希望初始参数太大的,所以乘上 0.01 或者其他一些很小的数都是合理的尝试。事实上有时候也有比 0.01 更好的常数,当你训练一个只有一层隐藏层的网络时(这是相对浅的神经网络,没有太多的隐藏层),设为 0.01 或许还可以。但当你训练一个非常非常深的神经网络时,可能需要试试 0.01 以外的常数。下面课程中我们会讨论 how & when 去选择一个不同于 0.01 的常数,但是无论如何它通常都会是个相对很小的数。

上一篇:常用激活函数sigmoid,Tanh,ReLU


下一篇:常用激活函数总结