Learning to be Giant.

Kaffe: 从零开始写一个神经网络(一)

Backpropagation, SGD和Deep Forward Networks

|

神经网络(Neural Network)从被提出到现在已经三十多年,经历了兴盛与衰败,如今随着计算能力和数据量的增大又重返大家的视野。三十年间,无数的模型被提出:Convolutional Neural Network, Deep Belief Network, Restricted Boltzmann Machine, LSTM等等。模型种类纷繁众多,但最基本的构建模块却从来没有变过,包括Back Propagation和Stochastic Gradient Descent。本系列目录见这里

Deep Forward Networks

开始接触神经网络时遇到的第一个模型往往都是Forward Networks。通常这种网络有一个输入层,一个输出层,以及一些中间的Hidden Layers。Deep Forward Networks可以认为是这种最简单的网络的一个扩展,唯一的区别就是中间的Hidden Layer数量稍微多一些。至于多多少,这个就比较难以界定了。接下来,我们来介绍一些一个基本的Forward Neural Network的组成。

一个简单的Neural Network

首先,上面这幅图当中由所有的$x$构成的这一层是输入层,所有的数据通过这一层传入Network。输入可以是各种形式的数据,比如对于计算机视觉(Computer Vision)的应用来说,最常见的输入就是图片,那么相应的这些$x$就表示图片当中的每一个像素。

接下来我们看到,这些输入$x$通过一些$w$们连接到了$p$。这里的连接所表示的意义是: \[p=\sum_iw_ix_i\] 用矩阵表示就是:$\mathbf{p}=\mathbf{w}^T\mathbf{x}$。$p$有一个名字,叫做Pre-activation。有的人会把$p$叫做一个Neuron,这种叫法曾经对我自己产生过误导。我认为,把所有与某个$p$相连接的所有$w$的集合称为Neuron更加容易理解。我个人认为,一个Neuron应该代表一种与前一层连接的方式,而不应仅仅是一个值。

由$p$到$h$的变化叫做Activate。常见的Activation function有Sigmoid (logistic function), tanh和近些年非常流行的ReLU。本文中所选的activation function是sigmoid,所以$h$与$p$的关系:\[h=sigm(p)=\frac{1}{1+\exp(-p)}\]为什么需要activation呢?因为我们生活在一个非线性的世界里,比如我们从看到图片到识别出图片中的物体,这整个过程并不是一个线性的过程,而如果希望能够表示这种非线性的关系,我们自然要在模型中引入非线性。倘若我们不使用activation function,而是直接将$p$连接到输出$y$上面的话,我们发现我们所做的只不过是一个线性变化而已。

由$h$到输出$y$的变化与$x$到$p$类似,只是一个简单的向量乘法。一组$p$和$h$构成一个Hidden Layer。

Loss Function(损失函数)

Loss Function是我们训练模型的代理人,通过它我们可以告诉模型它的预测是不是正确。比如常见的Loss Function有Cross Entropy和Euclidean Distance,两者分别常用于Classification和Regression当中。我们就拿比较简单的Euclidean Distance(有时候也叫做$L_2$ Loss)来举例。 \[L_2(x,\hat{x}) = (x-\hat{x})^2\] 比如,我有一些学生们身高、饭量的数据,想以此预测一个学生的体重。假设这个学生的体重是60kg,我的预测是60kg。由于我的预测是准确的,所以那么Euclidean Distance就会告诉我我预测差距是0,那么我的模型就会很开心,因为它什么都不用做;而如果我的预测是50kg,那么Euclidean Distance就会告诉我我错误的预测的差距是$(60-50)^2=100$,那么我的模型就需要利用上面提到过的梯度下降的方法来更新自己,使得下一次遇到类似情况的时候能错的少一点。

损失函数的存在使得我们有办法量化地监督模型的学习过程。通过它,我们可以量化地告诉模型什么样的预测更接近真实结果。比如在上面的例子当中,我们的预测如果是59.9kg,由于已经足够接近,所以Euclidean Distance就认为不必再苛求,错误差距就只有0.01。然而如果我们的预测是50kg,实在差的太远,我们的Loss Function就会认为应当好好惩罚一下模型,给它一个很大的损失(100)让它好好长长记性。

Gradient Descent(梯度下降法)

梯度下降是优化中的一种基本方法,原理就是使得自变量$x$以循环迭代的方法不断的向着梯度下降的方向移动,最终移动到函数$f(x)$的最小值。

Gradient Descent

我们可以想象梯度下降就如同上图中的小球一样,沿着碗的边沿滚到碗底。图中蓝色箭头指示的方向就是梯度方向。这张图跟网上很多其他的图片不同,其他图片大多数会将梯度方向标为沿着斜坡向下的方向,这其实是不准确的。梯度作为一个向量,在图中这个一元函数就是一个一维向量,指示自变量$x$变化最快的方向(向正方向或者负方向),它的数值其实就是斜率,或者函数在这一点的导数。另外我们发现,虽然我们知道小球下一步是会向右滚动的,但是梯度方向指向左边(因为函数数值在下降,所以斜率是负数),所以当计算下一步的$x$时,正确的算法应该是\[x_{t+1} = x_t - \nabla f(x)\]其中$x_{t+1}$是下一步的小球在$x$轴的位置,$x_t$是小球当前的位置。

Learning Rate

Learning rate

提到梯度下降必须提Learning rate,它控制着学习的速度。这里我们稍微修正一下上面提到的更新自变量的方法\[x_{t+1} = x_t - t\nabla f(x)\] 其中$t$就是learning rate(代码示例)。从上面的图片当中我们可以看出,如果Learning rate太小,那么我们需要花很长的时间才能找到最优值;而如果太大,我们可能就会因为步子太大错过最优值。

Stochastic Gradient Descent (SGD)

SGD是梯度下降的一种变体,在训练深度神经网络的时候使用SGD的优势主要在于两点:

  1. 减小对内存使用量的要求,每次可以只针对训练集中一小部分样本进行训练
  2. 帮助模型收敛到一个稳定的局部最优解 (Local minima)

具体的操作方法是这样的。对于梯度下降来说,我们需要一次性将所有的训练样本作为输入,然后通过优化网络的weights使得最终的Loss Function取得最小值。往往来说,深度学习需要的数据量都非常大,比如ImageNet数据集就包含百万计的图片数据,由此可见,梯度下降对于运算量和内存的要求是非常大的。那么SGD其实就是每次每次输入一张图片,计算整个网络对于这一张图片的Loss,通过这一张图片来求出更新weights的方向,从而更新weights。下一次迭代的时候再使用下一张图片。理论上可以证明,对于Convex Function,从期望上来说SGD和梯度下降能够收敛到同样的最优解。

GD vs SGD

上面这张图大致说明了SGD的第二点优势。Deep Learning的问题普遍都是highly nonconvex的问题,想要找到一个全局最优解非常难。幸运的是,任何一个局部最优解对于deep learning来说都足够好,都可以产生很好的结果。可是这些局部最优解当中也有个好坏之分。如同SVM当中我们希望Large margin一样,对于deep neural network,我们也希望尽可能的能够找到一个flat local minima,也就是说在我们找到的minima周围普遍比较的小或者说整个区域比较平。SGD由于它随机的特性,优化的过程回到处“跳”,它不会完全根据整个函数的梯度方向衍化,而是会有一些随机性,这些随机性导致SGD能够从一个局部最优解当中跳出来。只有当收敛到的局部最优解是我们期待的flat local minima的时候,SGD才会陷入其中跳不出来。

Back Propagation

Chain Rule(链式法则)

大学微积分前几节课就会讲的链式法则在神经网络的训练当中起到举足轻重的作用。这里来回顾一下:

  • Chain Rule: $\frac{\partial f(g(x))}{\partial x} = \frac{\partial f}{\partial g}\frac{\partial g}{\partial x}$
  • Multivariable Chain Rule: $\frac{\partial f(g(x), t(x))}{\partial x} = \frac{\partial f(g, t)}{\partial g}\frac{\partial g}{\partial x} + \frac{\partial f(g, t)}{\partial t}\frac{\partial t}{\partial x}$

Back Propagation

Back Propagation其实就是给链式法则起了一个新的名字而已。

我们看到上面使用梯度下降法优化函数的时候需要对函数进行求导从而得到梯度,那么究竟要怎么求导呢?其实我们很容易想象,一个神经网络的Loss其实就是一个关于输入($x$)、中间weights($w$)的函数。对于本文最初的神经网络的例子来说,就是: \[C(\mathbf{w’},\mathbf{w},\mathbf{x})=\mathbf{y}=\mathbf{w’}^Tsigm\left(\mathbf{w}^T\mathbf{x}\right)\]

而我们的目标函数其实就是: \[\mathop{\mathrm{argmax}}\limits_{\mathbf{w}, \mathbf{w’}}C(\mathbf{w’},\mathbf{w},\mathbf{x})\]

了解了这一切之后,其实关于$w$求导很容易,我们就$C$对$\mathbf{w’},\mathbf{w}$求偏导就好了。可是要知道,我们这里举的例子非常简单,只有几层而已,如果十几层或者变量数量特别多,这么把整个网络展开成一个函数就非常复杂了。而且,要将整个网络展开需要非常大的内存资源。幸运的是,Chain Rule使得我们可以一层一层的求导,使得整个求导过程非常的模块化,并最后完成对整个网络中所有变量的求导。

我们不难发现一个神经网络中$L_{k+1}$层的输入就是$L_k$层的输出,并且$L_{k+1}$只与$L_k$有关,与其他的层都没有关系。所以,如果我们用层的名字来表示这一层的输出的话,那么: \[L_{k+1}=f\left(L_k\right)\] 同样的,对于后续层来说$L_{k+2}=g\left(L_{k+1}\right)$等等。最终的Loss Function可以看作是最关于最后一层输出的函数,也即: \[Loss = C(L_n)\]

在优化的时候,比如我们需要求的是$\frac{\partial C}{\partial L_k}$,那么其实我们通过Chain Rule可以发现,这就是 \[\frac{\partial C}{\partial L_k}=\frac{\partial C}{\partial L_{k+1}}\frac{\partial L_{k+1}}{\partial L_k}\] 同样的道理,$\frac{\partial C}{\partial L_{k+1}}$可以继续展开。这样,我们从最后一层不断地向前计算,最终就可以完成对于网络中所有变量的求导。这就是Back Propagation。

开始动手实现吧!

我们现在知道了关于Deep Learning最基本的概念,包括基本的网络结构(Deep Forward Network)、优化的方法(梯度下降和SGD)以及求导的方法(Back Propagation)。这些基本上涵盖了深度学习最为基础的内容。这些理论储备已经足够我们实现一个简单的神经网络了。

UML

我发现Caffe以层为单位的架构非常容易理解,所以我也沿用了这种方式。整体架构如上面的UML图所示。一个Network是由多个Layer组成的。而一个Optimizer当中必须有一个需要被Optimize的Network,这两者一对一的关系。Network也可以离开Optimizer存在,比如在测试和部署的时候我们是不需要Optimizer的。

Optimizer通过运行Network获得输出和gradient,从来依照相应的算法来优化网络。Network包含了一大堆Layer,并且保存了每个layer的输出以及求导所得的信息,供Optimizer使用。Network提供forwardbackward方法以运行整个网络。Layer类需要实现forwardbackward两个方法,分别用来运行当前层和计算当前层的gradient,这一点与caffe的设计一致

这一节代码见:https://github.com/codinfox/kaffe/tree/master/hw1.

demo.py会在MNIST数据集上训练一个简单的网络并显示学习过程和测试结果。gradient_check.py的相关内容将会在后面的内容中讲到。

几个Layer

所谓的Fully Connected Layer就是我们到现在为止一直拿来举例子的最基本的layer,它实现的功能就是: \[L_{k+1} = \mathbf{W}^TL_k+\mathbf{b}\] 其中$\mathbf{W}$是以matrix形式表示的这一层的weights,$\mathbf{b}$是相对应的bias。从这个形式当中我们应该可以看出,$L_k$和$L_{k+1}$都可能是多维的,每一维对应这一层的一个neuron。

Softmax是Logistic function的自然延伸,用来进行多类的分类。 在这里的实现当中有一步是我们减去了最大值,原因是$\exp$这个函数增长太快,非常容易溢出,减去最大值之后就不会溢出了。

Cross Entropy是非常常见的用于classification的loss function,其作用在于比较两个distribution是不是相同。

具体求导这里就不做了,大家可以从代码当中看到。

小结

在这篇日志当中我们介绍了最基本的一些深度学习需要的基础知识,包括SGD、BP等等。掌握了这些基础就已经掌握了理解深度学习的基石。

Disclaimer: This is a personal weblog. The opinions expressed here represent my own and not those of any entity with which I have been, am now, or will be affiliated.

Comments