Neural Networks and Deep Learning(神经网络与深度学习) - 学习笔记

一、神经网络简介

0x1: 神经网络的分层神经元意味着什么

为了解释这个问题,我们先从一个我们熟悉的场景开始说起,电子电路的设计,

如上图所示,在实践中,在解决线路设计问题(或者大多数其他算法问题)时,我们通常先考虑如何解决子问题,然后逐步地集成这些子问题的解。换句话说,我们通过多层的抽象来获得最终的解答,回到上图的电路,我们可以看到,不论多么复杂的电路功能,在最底层的底层,都是由最简单的"与、或、非"门通过一定的逻辑关系组成。

这就很自然地让我么联想到深度神经网络的一张脍炙人口的架构图

深度神经网络中间的隐层可以理解为是一种逐层抽象封装的思想,这么说可能并没有严格的理论依据,但是却十分符合我自己直觉上的理解,例如,如果我们在进行视觉模式识别:

  1. 第一层的神经元可能学会识别边
  2. 第二层的神经元可以在边的基础上学会识别更加复杂的形状(例如三角形或者矩形)
  3. 第三层能够识别更加复杂的形状

以此类推,这些多层的抽象看起来能够赋予深度网络一种学习解决复杂模式识别问题的能力。

借着这个话题,我们引申出一个很有趣的论点:

用4个参数可以描绘出一个大象,如果给我5个参数,我甚至可以让它卷鼻子

通过线性方程组来拟合一个数据集,本质是说世界上的所有的事物都可以用线性方程组来描绘

0x2: 神经网络的普遍性(神经网络可以计算任何函数)

神经网络的一个最显著的事实就是它可以计算任何的函数,不管目标数据集对应的函数是什么样的,总会确保有一个神经网络能够对任何可能的输入x,其值f(x)或者某个足够准确的近似是网络的输出。这表明神经网络拥有一种"普遍性",普遍性是指,在原理上,神经网络可以做所有的事情

1、两个预先声明

在讨论普遍性定理成立之前,我们先定义下两个预定声明,即"神经网络可以计算任何函数"

  1. 这句话不是说一个网络可以被用来准确地计算任何函数,而是说,我们可以获得尽可能好的一个"近似"(即拟合),同时通过增加隐藏元的数量,我们可以提升近似的精度。
  2. 只有连续函数才可以被神经网络按照上面的方式去近似。

总的来说,关于普遍性定理的表述应该是:

包含一个及以上隐藏层的神经网络可以被用来按照任意给定的精度来近似任何连续函数

2、激活函数输出值在各个区间的累加和抵消

这样写标题可能有些奇怪,但这是我个人这个现象的理解,这个情况在我们高中数学中并不少见,即每个函数都有自己的定义域和值区间,当把两个函数相加时需要同时考虑它们的定义域区间,最终得到"累加和抵消"后的函数结果,而我们网络中每个神经元对应的激活函数输出值都可以看作是一个函数,我们回到我们的sigmoid S型函数,把它的w权重设置为一个较大的值,想象一下它的函数曲线会是下面这个样子(接近阶跃函数)。

可以看到,S型函数的阶跃点为 s = -b / w: 和b成正比,和w成反比,在这种前提下,隐藏层的加权输出(w1a1 + w2a2)就可以近似看成是一组阶跃函数的输出,那问题就好办了

这些事情就变得很有趣的,由于b不同,导致阶跃点不同,每个神经元的激活值输出函数的阶跃点是错开的,这就允许我们通过调整b(当然阶跃点也受w的影响)来构造任意的突起。再注意第二点,这个突起的高度是谁决定的?答案也很明显,它是由这一层的激活值输出乘上下一层的权重的累加值决定的。

以上2点达成后,我们具备了一个能力: 可以在一个隐藏层中通过有效地调整w和b,来构造任意宽、任意高的"塔形凸起"。

当继续增加神经元的组合时,我们可以构造出更复杂的塔形突起

总的来说,通过改变权重和偏置,我们实际上是在设计这个拟合函数

Relevant Link:

http://neuralnetworksanddeeplearning.com/
http://neuralnetworksanddeeplearning.com/chap4.html
https://github.com/ty4z2008/Qix/blob/master/dl.md
http://neuralnetworksanddeeplearning.com/chap1.html
http://neuralnetworksanddeeplearning.com/

 

二、感知器及激活函数

为了更好的理解神经网络的"决策过程",我么需要先了解激活函数以及它的理论前身: 感知器,请注意我这里用词,决策过程,不管是多少层的神经网络,每一层/每一层上的每一个神经元都不不断进行"决策",在深度神经网络中这通过激活函数来支持(我们稍后再详细讨论激活函数,现在只要知道激活函数为这个决策提供了输入)。

0x1: 感知器

我们刚才提到决策这个概念,一个感知器接受几个二进制输入x1、x2、x3...,并产生一个二进制输出,

同时,对每个二进制输入(对应图上左边的3根箭头)都定义了权重w1、w2、w3,表示相应输入对于输出(决策)重要性的实数

可以看到,这里就包含了一个最简单朴素的决策器思想,并且随着权重w和阈值threshold的变化,你可以得到不同的"决策模型",我们把这些感知器组成一个层状网络

这个感知器网络能做出一些很"微秒"的决策

  1. 第一层感知器通过权衡输入依据做出3个非常简单的决定
  2. 第二层感知器可以比第一层做出更复杂和抽象的决策
  3. 第三层中的感知器甚至能进行更复杂的决策

以这种方式,一个多层的感知器网络可以从事复杂巧妙的决策。

再观察一个细节,每一层的感知器都和上一层的所有感知器的输入进行了"全连接",这意味着从第二层开始每一个层的所有感知器都是一个独立的决策者。

我们把每一层的权重累加改写为,这里的w和x对应权重和输入的向量,然后把阈值threshold移到不等式的另一边,并用b = -threshold代替,用偏置而不是阈值,那么感知器的决策规则可以重写为

我么可以把偏置看作一种表示让感知器输出1(或者用生物学的术语即"激活感知器"),即输入和权重和乘积累加加上这个偏置到达甚至超过这个感知器的"激活点",让它达到"激活态"

0x2: S型神经元(sigmoid激活函数)

感知器很好,它体现了一个多路输入综合决策的思想,但是我们仔细看一下感知器的函数图,它是一个阶跃函数,它最大的一个问题就是阶跃点附近会产生巨大的翻转,体现在感知器网络上就是单个感知器的权限或者偏置的微小改动有时候会引起那个感知器的输出的完全翻转。这也很容易想象,因为在阶跃点附近,感知器的输出是瞬间从0->1或者1->0的,这本质是感知输入决策是一种阶跃函数,它不具备函数连续性,不具备连续性的函数自然也无法对输入的微小改变做出相应的微小改变。

为了解决非连续性的问题,我么可以引入一个称为S型神经元的人工神经元,S型神经元最大的特点就是输入权重和偏置的微小改动只会引入输出的微小变化,这对让神经网络学习起来是很关键的

上面被称为S型函数,这实际上也就是sigmoid激活函数的单个形式,权重和输入的乘积累加,和偏置的和的输出是:

可以看到,S型函数是一个连续函数

这个函数可以看成是感知器阶跃函数平滑后的版本,Sigmoid激活函数的平滑意味着权重和偏置的微小变化,会从神经元产生一个微小的输出变化

0x3: tanh激活函数(双曲正切  hyperbolic tangent函数)

除了S型激活函数之外,还有很多其他类型的激活函数,tanch函数的输入为,通过简单的代数运算,我们可以得到: 。可以看出,tanh是S型函数的按比例变化版本

tanh在特征相差明显时的效果会很好,在循环过程中会不断扩大特征效果。与 sigmoid 的区别是,tanh 是 0 均值的,因此实际应用中 tanh 会比 sigmoid 更好。

0x4: ReLu激活函数(修正线性神经元 rectified linear neuron)

输入为x,权重向量为w,偏置为b的ReLU神经元的输出是: ,函数的形态是这样的

ReLU 得到的 SGD 的收敛速度会比 sigmoid/tanh 快很多,因为它不存在在"在接近0或1时学习速率大幅下降"的问题。

0x5: softmax激活函数(柔性最大值)

softmax 的想法其实就是为神经网络定义一种新式的输出层。开始时和 sigmoid 层一样的,首先计算带权输入: ,不过,这里我们不会使用 sigmoid 函数来获得输出。而是,会应用一种叫做 softmax 函数

分母是对所有的输出神经元进行求和。该方程同样保证输出激活值都是正数,因为指数函数是正的。将这两点结合起来,我们看到 softmax 层的输出是一些相加为 <span id="MathJax-Span-4843" class="mrow"><span id="MathJax-Span-4844" class="mn">1 正数的集合。换言之,softmax 层的输出可以被看做是一个概率分布。

这样的效果很令人满意。在很多问题中,将这些激活值作为网络对于某个输出正确的概率的估计非常方便。所以,比如在 MNIST 分类问题中,我们可以将 <span id="MathJax-Span-4846" class="mrow"><span id="MathJax-Span-4847" class="msubsup"><span id="MathJax-Span-4848" class="mi">输出值<span id="MathJax-Span-4849" class="mi"><span id="MathJax-Span-4850" class="mi">解释成网络估计正确数字分类为 <span id="MathJax-Span-4852" class="mrow"><span id="MathJax-Span-4853" class="mi">j 的概率

Relevant Link:

http://www.jianshu.com/p/22d9720dbf1a 

 

2. 代价函数(loss function)

我们已经了解了组成神经网络的基本单元S型神经单元,并且了解了它的基本组成架构

现在我们将注意力从单个神经元扩展到整个网络整体,从整体的角度来看待所有神经元对最后输入"最终决策"的影响,网络中每一层的所有的权重和偏置在输入的作用下,最终在输出层得到一个输出向量,这个时候问题来了,网络预测的输出结果和我们预期的结果(label)一致吗?它们差距了多少?以及是哪一层的哪一个神经元(或者多个神经元)的参数没调整好导致了这种偏差(这个问题在之后的梯度下降会详细解释),为了量化这些偏差,我们需要为神经网络定义一个代价函数,代价函数有很多形式

下面只会简单的介绍并给出对应代价函数的数学表示,而不是详细讨论,因为代价函数本身没啥可以讨论的,它的真正用途在于对各层神经元的w/b进行偏微分求导,从而得到该如何调整修正各个神经元w/b的最优化指导,这是一种被称为梯度下降的技术

0x1: 二次代价函数(均方误差 MSE)

y(x) - a是目标值和实际输出值的差,可以看到,当对于所有的训练输入x,y(x)都接近于输出a时(即都预测正确时),代价函数C(w, b)的值相当小,换句话说,如果我们的学习算法能找到合适的权重和偏置,使得C(w, b) = 0,则该网络就是一个很好的网络,因此这就表明,我们训练的目的,是最小化权重和偏置的代价函数,我们后面会说道将使用梯度下降来达到这个目的

1. 神经元饱和问题(仅限S型神经元这类激活函数)

在开始探讨这个问题前,先来解释下什么是神经元饱和,当然这里依然需要下面将要讲到的梯度下降的相关知识

1. 我们知道,循环迭代训练神经网络的根本目的是寻找到一组w和b的向量,让当前网络能尽量准确地近似我们的目标数据集
2. 而要达到这个目的,其中最关键的一个"反馈",最后一层输出层的结果和目标值能够计算出一个代价函数,根据这个代价函数对权重和偏置计算偏微分(求导),得到一个最佳下降方向
3. 知道第二步之前都没问题,问题在于"二次代价函数+Sigmoid激活函数"的组合,得到的偏导数和激活函数本身的导致有关,这样,激活函数的曲线缓急就直接影响了代价函数偏导数的缓急,这样是很不高效的,常常会遇到"神经元饱和"问题,即如果一个S型神经元的值接近1或者0,它认为自己已经接近优化完毕了,它会降低自己的激活导致,让自己的w和b趋于稳定,但是如果恰好这个w和b是随机初始的错误值或者不小心进入了一个错误的调整,则很难再纠正这个错误,有点撞了南墙不回头的意思

这么说可能会很抽象,我们通过数学公式推导和可视化函数图像来说明这点,首先,我们的二次代价函数方程如下

我们有,其中。使用链式法则来求权重和偏置的偏导数有

我们发现,公式中包含了这一项,仔细回忆下的函数图像

从这幅图可以看出,当神经元的输出接近1的时候(或者0),曲线变得相当平,所以就很小了,带回上面的方程,则w和b的偏导数方程也就很小了,这就导致了神经元饱和时学习速率缓慢的原因。如果正确调整了倒还好,如果是因为初始化或者错误的调整导致进入了一个错误的方向,则要调整回来就变得很缓慢很困难

对着这个这题,我们再延伸出去思考一下,导致学习速率缓慢的罪魁祸首是S型神经元的这种曲线特性导致的是吧?那如果不用S型呢,用ReLU呢,是不是就不存在这种情况呢?答案是肯定的,至少不存在学习速率缓慢的问题(虽然ReLU也有自己的缺点)

所以严格来说,并不是二次代价函数MSE导致的神经元饱和,是二次代价函数和S型的组合存在神经元饱和的问题

2. 输出层使用线性神经元时使用二次代价函数不存在学习速率慢的问题

如果我们输出层的神经元都是线性神经元,而不再是S型函数的话,即使我么继续使用二次代价函数,最终关于权重和偏置的偏导数为

这表明如果输出神经元是线性的那么二次代价函数不再会导致学习速率下降的问题,在此情形下,二次代价函数就是一种合适的选择

0x2: 交叉熵代价函数

有句话说得好: "失败是成功之母",如果我们能及时定义自己犯得错误并及时改正,那么我们的学习速度会变得很快,同样的道理也适用在神经网络中,我们希望神经网络可以从错误中快速地学习。我们定义如下的代价函数

通过数学分析我们同样可以看出

1. C > 0: 非负
2. 在实际输出和目标输出之间的差距越小,最终的交叉熵的值就越低

这其实就是我们想要的代价函数的特性

1. 交叉熵代价函数解决神经元饱和问题

同时它也避免了学习速度下降的问题,我们来看看它的偏导数情况,我们将代入前式的链式偏导数中

这个公式很有趣也很优美,它告诉我们权重学习的速度受到,也就是输出中的误差的控制,更大的误差,更快的学习速率,这是一种非常符合我们直觉认识的一种现象,类似的,我们也可以计算出关于偏置的偏导数

这正是我们期待的当神经元开始出现严重错误时能以最快速度学习。事实上,如果在输出神经元是S型神经元时,交叉熵一般都是更好的选择

2. 交叉熵究竟表示什么

交叉熵是一种源自信息论的解释,它是"不确定性"的一种度量,交叉熵衡量我们学习到目标值y的正确值的平均起来的不确定性

1. 如果输出我们期望的结果,不确定性就会小一些
2. 反之,不确定性就会大一些 

0x3: multiclass svm loss(hinge loss)

http://vision.stanford.edu/teaching/cs231n-demos/linear-classify/

Relevant Link:

https://hit-scir.gitbooks.io/neural-networks-and-deep-learning-zh_cn/content/chap3/c3s1.html

 

3. 用梯度下降法来学习-Learning with gradient descent

在上面讨论清楚了代价函数的相关概念之后,我们接下来可以毫无障碍地继续讨论梯度下降的这个知识了。我们再次重申一下我们的目标,我们需要不断调整w和b向量组,找到一组最佳的向量组,使之最终计算得到的代价函数C最小(C最小也就意味着最大化的拟合)

我们先从一个最简单的情况切入话题,我们设计一个神经网络,它只有一个输入、一个输出,权重w = 1,偏置b = 2,代价函数为 C = || (a - y) ||

我们的输入x  = 1,可以看到,神经元输出的值为3,代价函数为3,我们的目标是让C降为0(x是不变的),为此我们需要调整w和b的值,当让这个例子太简单了,我们知道怎么做,但是如果这里的神经元有多个呢,层数有多层呢,更重要的是,我们需要让计算机自动完成这个过程。为此回答这个问题,我们需要引入梯度这个概念

1. 我们需要根据这里的代价函数计算出对w和b的偏导数,这里都是-1
2. 引入一个学习速率的概念,它表示一个学习调整的步长,设置为1
3. 接着我们每次把w和b分别进行: w = w - 1 * -1,b = b - 1 * -1
4. 很容易想象,在进行了2次学习后,此时w = -1,b = 1,此时代价函数C = 0,学习完成

这里根据代价函数C求权重和偏置的偏导数,并根据学习速率,逐轮调整w和b的思想,就是梯度下降。同样的问题推广到二次函数也是类似的

0x1: 使用梯度下降来寻找C的全局最小点

再次重申,我们训练神经网络的目的是找到能最小化二次代价函数C(w, b)的权重和偏置,抛开所有细节不谈,现在让我们想象我们只要最小化一个给定的多元函数(各个神经元上的w和b就是它的变元): C(v),它可以是任意的多元实值函数,想象C是一个只有两个变量v1和v2的函数

我们想要找到C的全局最小值,一种解决这个问题的方式是用微积分来解析最小值,我们可以计算导数(甚至二阶导数)去寻找C的极值点,这里引入一个额外的话题: 随机梯度下降(SGD)

1. 对于一次训练来说,输入的x是固定值,代表了所有样本的输入值
2. 梯度下降要做的是计算所有输入值的"累加平均梯度"(这里会有一个累加符号),并根据最终总的累加平均梯度来反馈到各层的w和b上,让其作出相应调整
3. 问题就来了,当输入样本量十分巨大的时候,计算所有这些累加平均梯度是一件十分耗时的工作,所以为了改进这个问题,使用了随机mini_batch技术,即从这批样本中随机选出一定数目的样本作为代表,代表这批样本进行计算累加平均梯度,计算得到的梯度再反馈给所有整体样本,用这种方式提高运算效率
4. 假设我么总共样本是100,我们选的mini-batch = 10,则我们每次随机选取10个样本计算梯度,然后继续重复10次,把所有样本都轮一边(当然随机抽样不能保证一定所有样本都覆盖到),这被称为完成了一个"训练迭代(epoch)"

我们把我们的代价函数C想象成一个山谷,我们当前的w/b想象成在山谷中的某个点,我们的梯度下降就是当前小球沿重力作用要滚落的方向,学习率可以看成是速度v,而每次w/b调整的的大小可以看成是位移

微积分告诉我们C将会有如下变化: ,进一步重写为: 。这个表达式解释了为什么被称为梯度向量,因为它把v的变化关联为C的变化,整个方程让我们看到了如何选取才能让C下降: ,这里的是个很小的正数(称为学习速率)。至此,C的变量的变化形式我们得到了: 。然后我们用它再次更新规则计算下一次移动,如果我们反复持续这样做,我们将持续减小C直到获得一个全局最小值。总结一下

梯度下降算法工作的方式就是重复计算梯度然后沿着相反的方向移动,沿着山谷"滚落"

从举例中回到权重w和偏置的情况也是一样的,我们用w和b代替变量v

0x2: 基于momentum的梯度下降

为了理解momentum技术,想想我么关于梯度下降的原始图片,其中我们研究了一个球滚向山谷的场景,我们称之为梯度下降,momentum技术修改了梯度下降的两处使之更加贴近于真实物理场景

1. 为我们想要优化的参数引入了一个称为速度(velocity)的概念,梯度的作用就是改变速度(物理中力的作用是改变速度,而不是位移)
2. momentum方法引入了一种摩擦力的项,用来逐步减少速度

我们将梯度下降更新规则: 改为

我们来仔细看一看上面这个方程,它真正变化的量是v,每一轮计算新的v之前都要乘上一个因子u,然后减去学习率乘以代价函数差值(这个和普通梯度是一样的),唯一的区别就在那个因子u

在上面方程中是用来控制阻碍或者摩擦力的量的超参数,为了理解这个参数的意义,我么可以考虑一下

1. u = 1的时候,对应于没有任何摩擦力,此时我们看到代价函数差值直接改变速度v,速度v随后再控制w的变化率。直觉上想象一下,我们朝着梯度的方向不断下降,由于v的关系,当我们到达谷底的时候,还会继续越过去,这个时候如果梯度本该快速改变而没有改变,我们会发现我们在错误的方向上移动太多了,虽然此时代价函数已经翻转了,但是不能完全抵消v的影响
2. 前面提到,u可以控制系统中摩擦力的大小,更加准确的说,我们应该将"1 - u"看作是摩擦力的量
  1) 当u = 1没有摩擦,速度完全由梯度导数决定
  2) 当u = 0就存在很大摩擦,速度无法叠加,上诉公式就退化成了普通的梯度下降公式

0x3: rmsprop

SGD的问题在于如果我们把learning rate设置的很小,则SGD会花费一个相当长的过程,为此,我们有很多对SGD的改进梯度下降方法,接下来逐一研究

The basic idea behind rmsprop is to adjust the learning rate per-parameteraccording to the a (smoothed) sum of the previous gradients. Intuitively this means that

1. frequently occurring features get a smaller learning rate (because the sum of their gradients is larger): 梯度越大,一次参数调整的size就要越小,这相当于速度很快了,时间s就要动态调整小,防止一次移动的距离s过大(步子迈的太大扯着蛋)
2. and rare features get a larger learning rate: 梯度越小,一个参数调整的size就要越大

The implementation of rmsprop is quite simple. For each parameter we keep a cache variable and during gradient descent we update the parameter and the cache as follows (example for W):

cacheW = decay * cacheW + (1 - decay) * dW ** 2
W = W - learning_rate * dW / np.sqrt(cacheW + 1e-6)

The decay is typically set to 0.9 or 0.95 and the 1e-6 term is added to avoid division by 0.

可以看到,RMSPROP的做法和momentum的梯度下降核心思想是一样的,这是一种对不同的梯度进行动态补偿调整的机制,目的是让网络在大梯度情况下慢慢收敛,而在平缓梯度的时候尽快前进

0x4: (Nesterov) Momentum Method

0x5: AdaGrad

AdaGrad是RMSPROP的"old version",RMSPROP是在AdaGrad的基础上改进而来的,我们来看看AdaGrad的定义

historical_grad += g^2
adjusted_grad = grad / (fudge_factor + sqrt(historical_grad))
w = w - master_stepsize*adjusted_grad

缺点是因为公式中分母上会累加梯度平方,这样在训练中持续增大的话,会使学习率非常小,甚至趋近无穷小

0x6: AdaDelta

0x7: Adam

Adam可以理解为momutum SGD和RMSPROP的综合改进版本,同时引入了动态调整特性以及动量V特性。Adam(Adaptive Moment Estimation)本质上是带有动量项的RMSprop,它利用梯度的一阶矩估计和二阶矩估计动态调整每个参数的学习率。Adam的优点主要在于经过偏置校正后,每一次迭代学习率都有个确定范围,使得参数比较平稳。公式如下

Relevant Link:

http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/
http://blog.csdn.net/yc461515457/article/details/50498266
https://tigerneil.gitbooks.io/neural-networks-and-deep-learning-zh/content/chapter3b.html
http://blog.csdn.net/u012759136/article/details/52302426

 

4. 用反向传播调整神经网络中逐层所有神经元的超参数 

这个话题是针对整个神经网络的所有神经元而言的,是一个整体优化的话题,在开始讨论这个话题前,我们先看一张多层神经网络的架构图

我们设想一下,我们在当前超参数的前提下,根据一组输入值x,得到了一个代价函数,接下来我们要怎么把这种误差传递给每一层的所有神经元呢,答案就是对所有神经元反向计算梯度,即要对每一层每一个神经元的w/b计算偏导数

这张图实际上为了说明接下来要讨论的"消失的梯度"的问题的,但是我认为它同样表达出了反向传播的核心思想,仔细看这个方程,我们会发现几点

1. 越前面层的神经元相对于C的偏导数,可以通过后面层的神经元的偏导数推演而得,直观上就像从最后一层反向传播到了第一层一样,故名反向传播
2. C对逐层神经元的w/b的偏导数,随着越往前,偏导数乘的因子越多

0x2: 反向传播算法标准化流程

仔细看这个算法,我么可以看到为何它被称为反向传播,我们从最后一层开始向后计算误差向量,这种反向移动其实是代价函数是网络输出的函数的结果。说动这里引入一个题外话

1. 前馈网络: 输入x通过一层一层的w/b逐步把影响传递到最后一层输出层
2. 反向传播: 这个时候输入可以看作是代价函数C,通过链式求导反向逐层把C的误差值传递给前面每一层的每一个神经元

这2点让我影响深刻,充满了哲学思想

0x3: 不稳定的梯度问题(梯度激增/消失)

我们从一个现象引出这个话题: 多层神经网络并不能显著提高整体的精确度,在一个网络框架的基础上,再增加新的一层神经元,网络的整体精确度并没有显著提升?这是为什么呢?新增加的隐层没有增加网络对抽象问题的决策能力吗?

我们来看一个多层神经网络各个层的学习速率的变化曲线对比图

我们可以发现,每层的神经元的学习速率都差了一个数量级,越前面层的神经元,获得的学习速率越小,这种现象也被称为"消失的梯度(vanishing gradient problem)",同时值得注意的是,这种情况也存在反例,即前面层的神经元学习速率比后面的大,即"激增的梯度问题(exploiding gradient problem)"。更一般的说,在深度神经网络中的梯度是不稳定的,在前面的层中会消失或激增

这种现象背后的数学原理是啥呢?我么再次来看一下前面那张代价函数对各层神经元的偏导数

我们知道,越前面的神经元,代价函数C对w/b的偏导数的公式中,乘积因子越多,所以接下来问题就是这些多出来的乘积因子对结果产生什么影响了呢?为了理解每个项的行为,先看看下面的sigmoid函数导数的图像

该导数在时达到最高值。现在,如果我们使用标准方法来初始化网络中的权重,那么会使用一个均值为0标准差为1的高斯分布,因此所有的权重会满足 <span id="MathJax-Span-953" class="mrow"><span id="MathJax-Span-954" class="texatom"><span id="MathJax-Span-955" class="mrow"><span id="MathJax-Span-956" class="mo">|<span id="MathJax-Span-957" class="msubsup"><span id="MathJax-Span-958" class="mi">w<span id="MathJax-Span-959" class="mi">j<span id="MathJax-Span-960" class="texatom"><span id="MathJax-Span-961" class="mrow"><span id="MathJax-Span-962" class="mo">|<span id="MathJax-Span-963" class="mo"><<span id="MathJax-Span-964" class="mn">1。有了这些信息,我们发现会有 <span id="MathJax-Span-966" class="mrow"><span id="MathJax-Span-967" class="msubsup"><span id="MathJax-Span-968" class="mi">w<span id="MathJax-Span-969" class="mi">j<span id="MathJax-Span-970" class="msup"><span id="MathJax-Span-971" class="mi">σ<span id="MathJax-Span-972" class="mo">′<span id="MathJax-Span-973" class="mo">(<span id="MathJax-Span-974" class="msubsup"><span id="MathJax-Span-975" class="mi">z<span id="MathJax-Span-976" class="mi">j<span id="MathJax-Span-977" class="mo">)<span id="MathJax-Span-978" class="mo"><<span id="MathJax-Span-979" class="mn">1<span id="MathJax-Span-980" class="texatom"><span id="MathJax-Span-981" class="mrow"><span id="MathJax-Span-982" class="mo">/<span id="MathJax-Span-983" class="mn">4。并且在我们进行了所有这些项的乘积时,最终结果肯定会指数级下降:项越多,乘积的下降的越快。这就是消失的梯度问题的合理解释。

Relevant Link:

https://tigerneil.gitbooks.io/neural-networks-and-deep-learning-zh/content/chapter5.html
http://blog.csdn.net/yc461515457/article/details/50499515

 

5. 过拟合问题

在科学领域有一种说法,一个拥有大量参数的模型能够描述特别神奇的现象,但即使这样的模型能够很好地拟合已有的数据,但并不表示它就是一个好的模型,因为这可能只是模型中足够的自由度使得它可以描述几乎所有给定大小的数据集,而不需要真正洞察现象背后的本质。所以发生这种情形时,模型对已有的数据会表现的很好,但是对新的数据很难泛化。事实上,对一个模型真正的检验就是它对没有见过的场景的"预测能力"

0x1: 过拟合(overfitting)/过度训练(overtrainning)在神经网络训练中表现出的几种现象

1. 训练集和测试集的准确率曲线没有一致收敛

网络几乎是在单纯记忆训练集合,而没有对数字本质进行理解能够泛化到测试数据集上

2. 训练数据集准确度曲线在到达一定迭代次数后维持在一个数值周围上线剧烈浮动

0x2: 如何规避过拟合问题

1. 在train_set/test_set的基础上,增加validate_set验证数据集判断是否需要提前停止

我们使用validate_set来防止过度拟合,在每个迭代周期的最后都计算在validate_set上的分类准确度,一旦分类准确度已经饱和,就停止训练,这个策略被称为"提前停止"

在网络根据训练数据集进行训练的过程中,我们借助validate_set来不断验证各种超参数,然后一旦获得了想要的超参数,最终我们就使用test_set进行准确率测量,这给了我们在test_set上的结果是一个网络泛化能力真正的度量。换言之,你可以将验证集看成一种特殊的训练数据集能够帮助我们学习好的超参数。这种寻找好的超参数的方法有时被称为"hold out"方法(因为validate_set是从training_set中留出或者拿出一部分)

2. 增加训练样本的数量

思考一个最简单的问题,我们有1000个超参数,但是训练样本只有100个,那么平均下来就是10个超参数去拟合一个样本,这给参数拟合带来了很大的自由度,也就很容易造成过拟合。一般来说,最好的降低过拟合度的方式之一就是增加训练样本的量。有了足够的训练数据,就算是一个规模非常大的网络也不大容易过度拟合

3. 规范化

规范化能够帮助我们解决过度拟合的问题,规范化有很多种方式,例如L1规范化、L2规范化,本小节我们重点讨论L2规范化,他在实际的Tensorflow或者thoeno中用的最多

L2规范化,也叫权重衰减(weight decay),它的思想是增加一个额外的项到代价函数上,这个项叫做规范化项,下面就是规范化的交叉熵

注意到第二个项是新加入的所有权重的平方的和。然后使用一个因子 <span id="MathJax-Span-12357" class="mrow"><span id="MathJax-Span-12358" class="mi">λ<span id="MathJax-Span-12359" class="texatom"><span id="MathJax-Span-12360" class="mrow"><span id="MathJax-Span-12361" class="mo">/<span id="MathJax-Span-12362" class="mn">2<span id="MathJax-Span-12363" class="mi">n 进行量化调整,其中 <span id="MathJax-Span-12365" class="mrow"><span id="MathJax-Span-12366" class="mi">λ<span id="MathJax-Span-12367" class="mo">><span id="MathJax-Span-12368" class="mn">0 可以成为 规范化参数,而 <span id="MathJax-Span-12370" class="mrow"><span id="MathJax-Span-12371" class="mi">n 就是训练集合的大小。当然,对其他的代价函数也可以进行规范化,例如二次代价函数。类似的规范化的形式如下

两者都可以写成这样

直觉上看,规范化的效果是让网络倾向于学习小一点的权重,换言之,规范化可以当作一种寻找小的权重和最小化原始代价函数之间的折中,这两部分之前相对的重要性就由 <span id="MathJax-Span-12381" class="mrow"><span id="MathJax-Span-12382" class="mi">λ 的值来控制了:<span id="MathJax-Span-12384" class="mrow"><span id="MathJax-Span-12385" class="mi">λ 越小,就偏向于最小化原始代价函数,反之,倾向于小的权重。

思考一个问题,为什么规范化能够帮助减轻过度拟合?我们要明白,规范化只是一种术语说法,它的本质是通过改变原始代价函数的方程式,让代价函数有的新的属性,即诱导网络学习尽量小的权重,这背后的道理可以这么理解:

小的权重在某种程度上,意味着更低的复杂性,也就对数据给出了一种更简单却更强大的解释,因此应该优先选择

让我们从抗噪音干扰的角度来思考这个问题,假设神经网络的大多数参数有很小的权重,这最可能出现在规范化的网络中。更小的权重意味着网络的行为不会因为我们随便改变一个输入而改变太大。这会让规范化网络学习局部噪声的影响更加困难。将它看作是一种让单个的证据不会影响整个网络输出太多的方式。相对的,规范化网络学习去对整个训练集中经常出现的证据进行反应,对比看,大权重的网络可能会因为输入的微小改变而产生比较大的行为改变

4. 弃权(Dropout)技术

弃权是一种相当激进的技术,和L1/L2规范化不同,弃权技术并不依赖对代价函数的修改,而是在弃权中,我们改变了网络本身,假设我们尝试训练一个网络

特别地,假设我们有一个训练数据 <span id="MathJax-Span-12763" class="mrow"><span id="MathJax-Span-12764" class="mi">x 和 对应的目标输出 <span id="MathJax-Span-12766" class="mrow"><span id="MathJax-Span-12767" class="mi">y。通常我们会通过在网络中前向传播 <span id="MathJax-Span-12769" class="mrow"><span id="MathJax-Span-12770" class="mi">x ,然后进行反向传播来确定对梯度的共现。使用 dropout,这个过程就改了。我们会从随机(临时)地删除网络中的一半的神经元开始,让输入层和输出层的神经元保持不变。在此之后,我们会得到最终的网络。注意那些被 dropout 的神经元,即那些临时性删除的神经元,用虚圈表示在途中

我们前向传播输入,通过修改后的网络,然后反向传播结果,同样通过这个修改后的网络。在 minibatch 的若干样本上进行这些步骤后,我们对那些权重和偏差进行更新。然后重复这个过程,首先重置 dropout 的神经元,然后选择新的随机隐藏元的子集进行删除,估计对一个不同的minibatch的梯度,然后更新权重和偏差

为了解释所发生的事,我希望你停下来想一下没有 dropout 的训练方式。特别地,想象一下我们训练几个不同的神经网络,使用的同一个训练数据。当然,网络可能不是从同一初始状态开始的,最终的结果也会有一些差异。出现这种情况时,我们可以使用一些平均或者投票的方式来确定接受哪个输出。例如,如果我们训练了五个网络,其中三个被分类当做是 <span id="MathJax-Span-12772" class="mrow"><span id="MathJax-Span-12773" class="mn">3,那很可能它就是 <span id="MathJax-Span-12775" class="mrow"><span id="MathJax-Span-12776" class="mn">3。另外两个可能就犯了错误。这种平均的方式通常是一种强大(尽管代价昂贵)的方式来减轻过匹配。原因在于不同的网络可能会以不同的方式过匹配,平均法可能会帮助我们消除那样的过匹配。

那么这和 dropout 有什么关系呢?启发式地看,当我们丢掉不同的神经元集合时,有点像我们在训练不同的神经网络。所以,dropout 过程就如同大量不同网络的效果的平均那样。不同的网络以不同的方式过匹配了,所以,dropout 的网络会减轻过匹配。

一个相关的启发式解释在早期使用这项技术的论文中曾经给出

因为神经元不能依赖其他神经元特定的存在,这个技术其实减少了复杂的互适应的神经元。所以,强制要学习那些在神经元的不同随机子集中更加健壮的特征

换言之,如果我们就爱那个神经网络看做一个进行预测的模型的话,我们就可以将 dropout 看做是一种确保模型对于证据丢失健壮的方式。这样看来,dropout 和 L1、L2 规范化也是有相似之处的,这也倾向于更小的权重,最后让网络对丢失个体连接的场景更加健壮

5. 人为扩展训练数据

我们前面说过,减少过拟合的一个最好的手段就是增加训练样本量,但这在很多场景是很难做到的,在图像识别领域,为了弥补样本量不够的问题,人们想出了一种扩大训练样本量的方法,即数据扩展,例如

1. 图像翻转
2. 图像平移
3. 模拟人手肌肉的图像线条抖动
..

Relevant Link:

https://tigerneil.gitbooks.io/neural-networks-and-deep-learning-zh/content/chapter3a.html

 

6. IMPLEMENTING A NEURAL NETWORK FROM SCRATCH IN PYTHON – AN INTRODUCTION

0x1: LOGISTIC REGRESSION(使用逻辑回归分类器分类两类圆环点集)

import numpy as np
from sklearn import datasets, linear_model
import matplotlib.pyplot as plt


def generate_data():
    np.random.seed(0)
    X, y = datasets.make_moons(200, noise=0.20)
    return X, y


def visualize(X, y, clf):
    # plt.scatter(X[:, 0], X[:, 1], s=40, c=y, cmap=plt.cm.Spectral)
    # plt.show()
    plot_decision_boundary(lambda x: clf.predict(x), X, y)
    plt.title("Logistic Regression")


def plot_decision_boundary(pred_func, X, y):
    # Set min and max values and give it some padding
    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.01
    # Generate a grid of points with distance h between them
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    # Predict the function value for the whole gid
    Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    # Plot the contour and training examples
    plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
    plt.show()


def classify(X, y):
    clf = linear_model.LogisticRegressionCV()
    clf.fit(X, y)
    return clf


def main():
    X, y = generate_data()
    # visualize(X, y)
    clf = classify(X, y)
    visualize(X, y, clf)


if __name__ == "__main__":
    main()

可以看到,逻辑回归尽可能地忽略噪音(从直线拟合的角度看的噪音),用一条直线对数据集进行了分类,但是很明显,逻辑回归没有"理解"数据背后真正的"含义",没有把圆环给分类出来

0x2: TRAINING A NEURAL NETWORK

Let’s now build a 3-layer neural network with one input layer, one hidden layer, and one output layer. The number of nodes in the input layer is determined by the dimensionality of our data, 2. Similarly, the number of nodes in the output layer is determined by the number of classes we have, also 2. (Because we only have 2 classes we could actually get away with only one output node predicting 0 or 1, but having 2 makes it easier to extend the network to more classes later on). The input to the network will be x- and y- coordinates and its output will be two probabilities, one for class 0 (“female”) and one for class 1 (“male”). It looks something like this:

1. HOW OUR NETWORK MAKES PREDICTIONS

Our network makes predictions using forward propagation, which is just a bunch of matrix multiplications and the application of the activation function(s) we defined above. If x is the 2-dimensional input to our network then we calculate our prediction \hat{y} (also two-dimensional) as follows:

2. LEARNING THE PARAMETERS

Learning the parameters for our network means finding parameters (W_1, b_1, W_2, b_2) that minimize the error on our training data. But how do we define the error? We call the function that measures our error the loss function. A common choice with the softmax output is the categorical cross-entropy loss (also known as negative log likelihood). If we have N training examples and C classes then the loss for our prediction \hat{y} with respect to the true labels y is given by:

We can use gradient descent to find the minimum and I will implement the most vanilla version of gradient descent, also called batch gradient descent with a fixed learning rate. Variations such as SGD (stochastic gradient descent) or minibatch gradient descent typically perform better in practice. So if you are serious you’ll want to use one of these, and ideally you would also decay the learning rate over time.

3. IMPLEMENTATION

import numpy as np
from sklearn import datasets, linear_model
import matplotlib.pyplot as plt


class Config:
    nn_input_dim = 2  # input layer dimensionality
    nn_output_dim = 2  # output layer dimensionality
    # Gradient descent parameters (I picked these by hand)
    epsilon = 0.01  # learning rate for gradient descent
    reg_lambda = 0.01  # regularization strength


def generate_data():
    np.random.seed(0)
    X, y = datasets.make_moons(200, noise=0.20)
    return X, y


def visualize(X, y, model):
    # plt.scatter(X[:, 0], X[:, 1], s=40, c=y, cmap=plt.cm.Spectral)
    # plt.show()
    plot_decision_boundary(lambda x:predict(model,x), X, y)
    plt.title("Logistic Regression")


def plot_decision_boundary(pred_func, X, y):
    # Set min and max values and give it some padding
    x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
    y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
    h = 0.01
    # Generate a grid of points with distance h between them
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
    # Predict the function value for the whole gid
    Z = pred_func(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    # Plot the contour and training examples
    plt.contourf(xx, yy, Z, cmap=plt.cm.Spectral)
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=plt.cm.Spectral)
    plt.show()


# Helper function to evaluate the total loss on the dataset
def calculate_loss(model, X, y):
    num_examples = len(X)  # training set size
    W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
    # Forward propagation to calculate our predictions
    z1 = X.dot(W1) + b1
    a1 = np.tanh(z1)
    z2 = a1.dot(W2) + b2
    exp_scores = np.exp(z2)
    # 得到实际预测值,用于和目标值计算代价函数
    probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
    # Calculating the loss(交叉熵)
    corect_logprobs = -np.log(probs[range(num_examples), y])
    # 针对每一个输入样本都要计算一个代价函数,C = 总的代价累加结果的平均值
    data_loss = np.sum(corect_logprobs)
    # Add regulatization term to loss (optional)
    data_loss += Config.reg_lambda / 2 * (np.sum(np.square(W1)) + np.sum(np.square(W2)))
    # 除以样本数,得到平均代价函数值
    return 1. / num_examples * data_loss


def predict(model, x):
    W1, b1, W2, b2 = model['W1'], model['b1'], model['W2'], model['b2']
    # Forward propagation
    z1 = x.dot(W1) + b1
    a1 = np.tanh(z1)
    z2 = a1.dot(W2) + b2
    exp_scores = np.exp(z2)
    # 根据当前网络的w/b向量组,根据激活函数softmax得到一组预测值输出向量
    probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)
    # 因为softmax输出中各项和为1,所以其中值最大的那个代表了该网络的预测项
    return np.argmax(probs, axis=1)


# This function learns parameters for the neural network and returns the model.
# - nn_hdim: Number of nodes in the hidden layer
# - num_passes: Number of passes through the training data for gradient descent
# - print_loss: If True, print the loss every 1000 iterations
def build_model(X, y, nn_hdim, num_passes=20000, print_loss=False):
    # Initialize the parameters to random values. We need to learn these.
    num_examples = len(X)
    np.random.seed(0)
    W1 = np.random.randn(Config.nn_input_dim, nn_hdim) / np.sqrt(Config.nn_input_dim)
    b1 = np.zeros((1, nn_hdim))
    W2 = np.random.randn(nn_hdim, Config.nn_output_dim) / np.sqrt(nn_hdim)
    b2 = np.zeros((1, Config.nn_output_dim))

    # This is what we return at the end
    model = {}

    # Gradient descent. For each batch...
    for i in range(0, num_passes):

        # Forward propagation
        z1 = X.dot(W1) + b1
        a1 = np.tanh(z1)
        z2 = a1.dot(W2) + b2
        exp_scores = np.exp(z2)
        # 计算softmax
        probs = exp_scores / np.sum(exp_scores, axis=1, keepdims=True)

        # Backpropagation
        delta3 = probs
        delta3[range(num_examples), y] -= 1
        dW2 = (a1.T).dot(delta3)
        db2 = np.sum(delta3, axis=0, keepdims=True)
        delta2 = delta3.dot(W2.T) * (1 - np.power(a1, 2))
        dW1 = np.dot(X.T, delta2)
        db1 = np.sum(delta2, axis=0)

        # Add regularization terms (b1 and b2 don't have regularization terms)
        dW2 += Config.reg_lambda * W2
        dW1 += Config.reg_lambda * W1

        # Gradient descent parameter update
        W1 += -Config.epsilon * dW1
        b1 += -Config.epsilon * db1
        W2 += -Config.epsilon * dW2
        b2 += -Config.epsilon * db2

        # Assign new parameters to the model
        model = {'W1': W1, 'b1': b1, 'W2': W2, 'b2': b2}

        # Optionally print the loss.
        # This is expensive because it uses the whole dataset, so we don't want to do it too often.
        if print_loss and i % 1000 == 0:
            print("Loss after iteration %i: %f" % (i, calculate_loss(model, X, y)))

    return model


def classify(X, y):
    # clf = linear_model.LogisticRegressionCV()
    # clf.fit(X, y)
    # return clf

    pass


def main():
    X, y = generate_data()
    model = build_model(X, y, 3, print_loss=True)
    visualize(X, y, model)


if __name__ == "__main__":
    main()

Relevant Link:

http://www.wildml.com/2015/09/implementing-a-neural-network-from-scratch/
https://github.com/dennybritz/nn-from-scratch

Copyright (c) 2017 LittleHann All rights reserved

posted @ 2017-04-23 21:21  郑瀚Andrew  阅读(6208)  评论(1编辑  收藏  举报