今天终于可以开始讲神经网络啦~~
1.一个简单的介绍
我们先把那些关于人脑、神经什么的东西抛到一边,来简单地理解下神经网络。在linear classification那一章,我们是通过计算输入图片属于不同类别的score来判断它到底属于哪个类的,即,其中W是参数矩阵,x是由输入图像的所有pixel组成的一个特征列向量。比如以CIFAR-10为例的话x就是[3072*1]的列向量,W是[10*3072]的矩阵,因此输出是10个类别得分。
那么在神经网络中,计算score的方法有所不同:。其中也是一个参数矩阵,他可以把原始图像特征x转变成比如说100维的中间层向量特征 (则此时W1应为[1003072]维)。函数max(0,~)是一个非线性函数,对矩阵中小于0的元素置零而保留大于0的元素值不变。这个非线性函数我们有多种选择(后面会讲),不过这一个是比较常用的,简单地通过0阈值来激活元素值。最后,则应该是一个[10100]的矩阵,这样我们最后还是得到了10个类别得分。其中,就是通过之前所说的随机梯度下降法学习到的,其中用到反向传播算法来计算每一步更新的梯度哦(不了解这部分内容要去戳一下我前面的课程笔记了啊)。注意非线性函数部分(也称为激活函数),是神经网络中很重要的一个部分,但是即使我们把非线性函数部分去掉,把直接和相乘我们也可以得到10个类别得分,但是这样就少了一些扰动(wiggle),影响泛化性能等。
同样的,3层的神经网络会长这个样子:,就是这么简单!其中,,都是学习得到的参数。中间层特征向量的维度是hyperparameter,我们在后面会讲怎么设置他们。
接下来让我们再用人脑神经元之类的东西来解释上面所说的神经网络吧。
如果你觉得这篇文章看起来稍微还有些吃力,或者想要系统地学习人工智能,那么推荐你去看床长人工智能教程。非常棒的大神之作,教程不仅通俗易懂,而且很风趣幽默。点击这里可以查看教程。
2. 建模神经元
神经网络这一领域最早来源于对生物神经系统的建模,而后慢慢演变成一种工程的方法,并且在机器学习中取得了很好的效果。我们这边只简短地介绍和我们相关的生物学系统的知识。
2.1 神经元的激活和连接
人脑中基本的计算单元叫做神经元(neuron).人的神经系统中大约包含860亿个这样的神经元,并且他们之间通过大约10^14~10^15这么多的突触(synapses)连接。下图就显示了一个神经元和它抽象出的数学模型。每个神经元会从它们的树突(dendrites)获得输入信号,然后再将输出信号传给它唯一的轴突(axon)。轴突再通过突触和其他神经元的树突相连。
在神经元的数学模型中,轴突所携带的信号(例如:)通过突触进行传递,由于突触的强弱不一,假设我们以表示,那么我们传到下一个神经元的树突处的信号就变成了。其中突触强弱(参数w)是可学的,它控制了一个神经元对另一个神经元影响的大小和方向(正负)。然后树突接收到信号后传递到神经元内部(cell body),与其他树突传递过来的信号一起进行加和,如果这个和的值大于某一个固定的阈值的话,神经元就会被激活,然后传递冲激信号给树突。在数学模型中我们假设传递冲激信号的时间长短并不重要,只有神经元被激活的频率用于传递信息。我们将是否激活神经元的函数称为激活函数(activation function f),它代表了轴突接收到冲激信号的频率。以前我们比较常用的一个激活信号是sigmoid function ,因为它接收一个实值的信号(即上面所说的加和的值)然后将它压缩到0-1的范围内。我们在后面会介绍更多的激活函数。
关于上述传播过程(前向传播),代码如下:
<code class="language-python hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Neuron</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(object)</span>:</span> <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># ... </span> <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> <span class="hljs-title" style="box-sizing: border-box;">forward</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(inputs)</span>:</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""" assume inputs and weights are 1-D numpy arrays and bias is a number """</span> cell_body_sum = np.sum(inputs * self.weights) + self.bias firing_rate = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> / (<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> + math.exp(-cell_body_sum)) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># sigmoid activation function</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> firing_rate</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>
2.2 一个神经元就是一个线性分类器
神经元的上述前向传播过程从形式上看着很熟悉。我们之前在线性分类器中看到,分类器具有判断score好坏的能力,在神经元中也是一样,我们通过激活与否来得到神经元的输出,再通过一个恰当的损失函数就能将一个神经元转化成线性分类器了。
Binary Softmax classifier. 比如说,我们可以把看成是某类的概率,那么另一类的概率则是,因为对于二值分类器而言两类的概率相加应为1。然后我们再通过在线性分类器那一章见过的交叉熵loss对score的好坏进行量化,这就是一个二值softmax分类器了(也叫逻辑回归)。因为sigmoid function会把只限定于0-1之间,分类器可以通过判断上述概率是否大于0.5来进行分类。
Binary SVM classifier. 我们还可以选择边界最大化的hinge loss来衡量神经元输出的好坏,那么此时就变成了一个二值SVM分类器了。
Regularization.不管是在SoftmaxLoss还是在SVM中,loss的正则项regularization loss在生物学角度上都可以理解成是渐进遗忘(gradual forgetting),因为它的作用在于使得所有突触的权重w在权重更新的过程中都趋向于0。
2.3 几种常见的激活函数
Sigmoid. Sigmoid 非线性激活函数的形式是,其图形如上图左所示。之前我们说过,sigmoid函数输入一个实值的数,然后将其压缩到0~1的范围内。特别地,大的负数被映射成0,大的正数被映射成1。sigmoid function在历史上流行过一段时间因为它能够很好的表达“激活”的意思,未激活就是0,完全饱和的激活则是1。而现在sigmoid已经不怎么常用了,主要是因为它有两个缺点:
- Sigmoids saturate and kill gradients. Sigmoid容易饱和,并且当输入非常大或者非常小的时候,神经元的梯度就接近于0了,从图中可以看出梯度的趋势。这就使得我们在反向传播算法中反向传播接近于0的梯度,导致最终权重基本没什么更新,我们就无法递归地学习到输入数据了。另外,你需要尤其注意参数的初始值来尽量避免saturation的情况。如果你的初始值很大的话,大部分神经元可能都会处在saturation的状态而把gradient kill掉,这会导致网络变的很难学习。
- Sigmoid outputs are not zero-centered. Sigmoid 的输出不是0均值的,这是我们不希望的,因为这会导致后层的神经元的输入是非0均值的信号,这会对梯度产生影响:假设后层神经元的输入都为正(e.g. x>0 elementwise in ),那么对w求局部梯度则都为正,这样在反向传播的过程中w要么都往正方向更新,要么都往负方向更新,导致有一种捆绑的效果,使得收敛缓慢。
当然了,如果你是按batch去训练,那么每个batch可能得到不同的符号(正或负),那么相加一下这个问题还是可以缓解。因此,非0均值这个问题虽然会产生一些不好的影响,不过跟上面提到的 kill gradients 问题相比还是要好很多的。
Tanh. Tanh和Sigmoid是有异曲同工之妙的,它的图形如上图右所示,不同的是它把实值得输入压缩到-1~1的范围,因此它基本是0均值的,也就解决了上述Sigmoid缺点中的第二个,所以实际中tanh会比sigmoid更常用。但是它还是存在梯度饱和的问题。Tanh是sigmoid的变形:。
ReLU. 近年来,ReLU 变的越来越受欢迎。它的数学表达式是: f(x)=max(0,x)。很显然,从上图左可以看出,输入信号
<0时,输出为0,>0时,输出等于输入。ReLU的优缺点如下:
- 优点1:Krizhevsky et al. 发现使用 ReLU 得到的SGD的收敛速度会比 sigmoid/tanh 快很多(如上图右)。有人说这是因为它是linear,而且梯度不会饱和
- 优点2:相比于 sigmoid/tanh需要计算指数等,计算复杂度高,ReLU 只需要一个阈值就可以得到激活值。
- 缺点1: ReLU在训练的时候很”脆弱”,一不小心有可能导致神经元”坏死”。举个例子:由于ReLU在x<0时梯度为0,这样就导致负的梯度在这个ReLU被置零,而且这个神经元有可能再也不会被任何数据激活。如果这个情况发生了,那么这个神经元之后的梯度就永远是0了,也就是ReLU神经元坏死了,不再对任何数据有所响应。实际操作中,如果你的learning rate 很大,那么很有可能你网络中的40%的神经元都坏死了。 当然,如果你设置了一个合适的较小的learning rate,这个问题发生的情况其实也不会太频繁。
Leaky ReLU. Leaky ReLUs 就是用来解决ReLU坏死的问题的。和ReLU不同,当x<0时,它的值不再是0,而是一个较小斜率(如0.01等)的函数。也就是说f(x)=1(x<0)(ax)+1(x>=0)(x),其中a是一个很小的常数。这样,既修正了数据分布,又保留了一些负轴的值,使得负轴信息不会全部丢失。关于Leaky ReLU 的效果,众说纷纭,没有清晰的定论。有些人做了实验发现 Leaky ReLU 表现的很好;有些实验则证明并不是这样。
- PReLU. 对于 Leaky ReLU 中的a,通常都是通过先验知识人工赋值的。然而可以观察到,损失函数对a的导数我们是可以求得的,可不可以将它作为一个参数进行训练呢? Kaiming He 2015的论文《Delving Deep into Rectifiers: Surpassing Human-Level Performance on ImageNet Classification》指出,不仅可以训练,而且效果更好。原文说使用了Parametric ReLU后,最终效果比不用提高了1.03%.
-Randomized Leaky ReLU. Randomized Leaky ReLU 是 leaky ReLU 的random 版本, 其核心思想就是,在训练过程中,a是从一个高斯分布中随机出来的,然后再在测试过程中进行修正。
Maxout. Maxout的形式是f(x)=max(w_1^Tx+b_1,w_2^Tx+b_2),它最早出现在ICML2013上,作者Goodfellow将maxout和dropout结合后,号称在MNIST, CIFAR-10, CIFAR-100, SVHN这4个数据上都取得了start-of-art的识别率。可以看出ReLU 和 Leaky ReLU 都是Maxout的一个变形,所以Maxout 具有 ReLU 的优点(如:计算简单,不会 saturation),同时又没有 ReLU 的一些缺点 (如:容易饱和)。不过呢Maxout相当于把每个神经元的参数都double了,造成参数增多。
Maxout的拟合能力非常强,它可以拟合任意的的凸函数。作者从数学的角度上也证明了这个结论,即只需2个maxout节点就可以拟合任意的凸函数了(相减),前提是”隐含层”节点的个数可以任意多。
How to choose a activation function? 怎么选择激活函数呢?
我觉得这种问题不可能有定论的吧,只能说是个人建议。
如果你使用 ReLU,那么一定要小心设置 learning rate,而且要注意不要让你的网络出现很多坏死的 神经元,如果这个问题不好解决,那么可以试试 Leaky ReLU、PReLU 或者 Maxout.
友情提醒:最好不要用 sigmoid,你可以试试 tanh,不过可以预期它的效果会比不上 ReLU 和 Maxout.
还有,通常来说,很少会把各种激活函数串起来在一个网络中使用的。
- 关于激活函数的部分内容参考:
http://blog.csdn.net/cyh_24/article/details/50593400
3. 神经网络结构
3.1 逐层构建
神经网络是神经元构成的图(此图是指graph,而不是指image)。神经网络是神经元互相连接构成的一个非循环的图,也就是说一些神经元的输出会作为其他神经元的输入,另外环路是不允许的因为这会使得神经网络的前向传播陷入无止尽的循环中。当然,神经元之间的排列是有规律的,通常情况下被构建成层层连接的形式,每一层中又有多个神经元。比如说常见的一种层叫做全连接层(fully-connected layer),表示的是相邻两层之间的神经元两两连接,同层的神经元则互不连接。下图是两个全连接的例子:
命名习惯。注意我们平时说N层神经网络,是不把输入层计算在内的。也就是说一个单层神经网络表示的是输入层接输出层,没有隐含层的网络结构。所以有时候你可能会注意到逻辑回归或SVM被看成是单层神经网络,或者叫人工神经网络(Artificial Neural Networks,ANN)或多层感知机(Multi-Layer Perceptrons,MLP)。还有许多人并不喜欢“神经网络”这个称呼,容易联想成生物学的神经元什么的,所以倾向于把neurons称为units。
输出层。和神经网络其他的层不同,最后的输出层通常情况下没有activation function,这是因为最后一层的输出通常用来表示类别的score(特别是分类),score嘛通常是实值的数。
神经网络的大小。衡量某个神经网络有多大,通常有两种方法,1是神经元的数目,2是参数的数目,相比之下第二种更常用。比如说以上图为例:
- 上图左的网络共包含(4+2=6)个神经元(不包括输入层),参数则有[3*4]+[4*2]=20个weights还有[4+2=6]个bias,也就是总共26个可学习的参数。
- 上图右的网络共包含(4+4+1=9)个神经元,参数则有[3*4]+[4*4]+[4*1]=12+16+4=32个weights还有[4+4+1=9]个bias,也就是总共41个可学习的参数。
实际上,现在所有的卷积网络通常都包含亿级的参数,并且由10-20层网络组成(因此说是deep learning)。
3.2 例子:前向传播的计算
在神经网路中,矩阵运算是非常常用和有效的。神经网络之所以被构建成层层连接的形式,一个重要的原因就是通过矩阵运算可以很方便快捷地计算不同层的输入输出。比如以上图3层的神经网络为例,输入就是一个[3*1]维的向量,每一层的所有权重也同样是矩阵形式,例如第一个隐层的weights 就是一个[4*3]维的矩阵,同时该层所有units的bias 维度则是[4*1]。也就是说每个神经元的weights在中都是一列,那么简单的矩阵乘法就可以计算出这一层所有神经元的激活值。同样的,是[4*4]维的矩阵,存储着第二个隐含层的所有连接参数,则是输出层的weights,是[1*4]维的矩阵。那么这整个3层神经网络的前向传播过程就是3个简单的矩阵乘法,然后再接激活函数:
<code class="language-python hljs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># forward-pass of a 3-layer neural network:</span>f = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">lambda</span> x: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span>/(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1.0</span> + np.exp(-x)) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># activation function (use sigmoid)</span>x = np.random.randn(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># random input vector of three numbers (3x1)</span>h1 = f(np.dot(W1, x) + b1) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># calculate first hidden layer activations (4x1)</span>h2 = f(np.dot(W2, h1) + b2) <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># calculate second hidden layer activations (4x1)</span>out = np.dot(W3, h2) + b3 <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;"># output neuron (1x1)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>
在上述代码中,是网络的可学习参数。注意最后一层输出层是不用加激活函数的。其实,输入数据x也可以不是一个列向量,而是包含整个训练数据batch的一个矩阵,矩阵中的每一列还是代表一个训练数据样本。这样所有的样本就可以同时进行计算了,非常高效。
The forward pass of a fully-connected layer corresponds to one matrix multiplication followed by a bias offset and an activation function.
3.3 神经网络对数据的表达力
我们常常会把包含全连接层的神经网络看成是一系列由网络参数构成的函数的组合。那么问题来了,这些函数的组合对数据的表达力怎么样?是不是任何函数都能通过神经网络进行建模?
有人证明过包含一层隐含层的神经网络是一个万能逼近器(见Michael Nielsen的证明),也就是说它可以估计任何连续的f(x)。那么既然两层的神经网络就可以估计任何函数了,为什么我们还需要更多的层,需要Go deeper?这是因为虽然两层神经网络从数学上来看是能逼近任何连续函数,但是从实际来看,更深的网络通常都比两层网络效果好,也就是说这是一种经验论,虽然它们从数学上来看对数据的表达力是一样的。
顺便说一下,在实际中3层的神经网络通常比2层的效果更好,但是4,5,6层却不一定能提升更多,这和卷积网络有着鲜明的对比,在卷积网络中通常深度是一个好的识别系统的非常关键的因素。有人说这是因为图像本身就是一个分层的结构(比如人脸是由眼睛构成的,眼睛由由边缘构成等),因此分多层来学习数据能够使网络获得更加语义化的理解。当然了,这整个领域还在不断的研究中,下面是一些可参考的读物:
- Deep Learning book in press by Bengio, Goodfellow, Courville, in practicular Chapter 6.4.
- Do Deep Nets Really Need to be Deep?
- FitNets: Hints for Thin Deep Nets
3.4 如何设置神经网络的层数和神经元数
面对一个实际问题,我们到底该使用什么样的网络呢?我们是应该用没有隐含层的呢,还是1个,2个隐含层呢?每一层所包含的神经元数目又该怎么设置?
首先我们需要注意到当我们增加层数和神经元数目的时候,神经网络的性能也会有所提升。因为神经元可以协同作用表达出不同的函数来对数据进行表示。比如说假设我们在二维空间中有一个二值的分类问题,我们可以训练3个不同的神经网络,每个神经网络都包含一个隐含层,但是隐含层中包含的神经元数目不一样,我们来看一下分类器的分类效果:
在上图中,我们看到包含更多神经元的神经网络能够表达更加复杂的函数。但是这既好又不好,好是因为可以对更复杂的数据进行分类,不好是因为它容易对训练数据过拟合(overfitting)。比如说隐层包含20个神经元的那个图,它虽然把所有的数据都分对了但是把整个平面分成了红绿相间、相互脱节的小区域,看上去很不平滑;而3个神经元的图,它能够从大方向上去分类数据,而把一些被绿色点包围的红色点看成是异常值、噪声(outliers)。实际中这能够使得模型在测试数据上有更好的泛化能力。
基于上述讨论,貌似当数据不太复杂的时候,我们可以选用小一点的神经网络来预防overfitting?不不不,这是不对滴,我们可以选择其他更好的方法来避免过拟合(比如L2正则化,dropout,增加噪声等,我们后面再讨论)。实际中用这些方法避免过拟合比减少神经元的个数要更好。比如说我们可以看看不同的正则强度是如何控制20个隐含神经元的过拟合的:
总的来说就是你不能因为害怕过拟合就使用小一点的神经网络,相反如果你的计算机性能允许,你应该用大的神经网络,丙炔通过一些正则的方法来控制overfitting.
4. 总结
在这章中,我们介绍了如下内容:
- 我们粗略地介绍了生物的神经元。
- 我们讨论了几种实际使用的激活函数,其中ReLU是最常用的选择
- 我们介绍了神经网络,其中神经元是通过全连接层来组织的,全连接层中相邻层的神经元两两连接,同一层的神经元互相不连接
- 我们发现这种层级的结构使得神经网络只要进行矩阵乘法和激活函数就可以计算了
- 我们发现神经网络是万能逼急器,但是我们也说了这种特性并不能表示我们不需要go deeper。
- 我们讨论了大型的神经网络总是会比小型神经网络效果要好,但是这也导致它们有可能会过拟合,因此我们需要选用恰当的正则方法来弥补这一缺点,在后面我们会见到更多形式的正则(如dropout等)。