【CV知识学习】神经网络梯度与归一化问题总结+highway network、ResNet的思考

这是一篇水货写的笔记,但愿路过的大牛能够指出其中的错误,带蒟蒻飞啊~git

 

1、    梯度消失/梯度爆炸的问题github

首先来讲说梯度消失问题产生的缘由吧,虽然是已经被各大牛说烂的东西。不如先看一个简单的网络结构,算法

 

能够看到,若是输出层的值仅是输入层的值与权值矩阵W的线性组合,那么最终网络最终的输出会变成输入数据的线性组合。这样很明显没有办法模拟出非线性的状况。记得神经网络是能够拟合任意函数的。好了,既然须要非线性函数,那干脆加上非线性变换就行了。通常会使用sigmoid函数,获得,这个函数会把数据压缩到开区间(0,1),函数的图像以下api

 

能够看到,函数的两侧很是平滑,并且无限的接近0和1,仅仅是中间部分函数接近一条直线,顺便说一下,这个函数的导数最值居然真的是1啊,也就是x=0的位置,于是称这个函数是双端饱和的,并且它是到处可导。那么,为何要选用它呢?看到有资料说是模拟神经学科的,但我不太懂这个,我的认为是由于(1)能够引入非线性(2)容易求导(3)能够把数据压缩,这样数据不容易发散。网络

另外,有一个函数与sigmoid函数很相似,就是tanh()函数,它能够把数据压缩到(-1,1)之间。app

可是,咱们要讲的是梯度的消失问题哦,要知道,神经网络训练的方法是BP算法(不知道还有没有其余的训练方法。。。)。BP算法的基础其实就是导数的链式法则,这个估计不须要细说了,就是有不少乘法会链接在一块儿。在看看sigmoid函数的图像就知道了,导数最大是1,并且大多数值都被推向两侧饱和的区域,这些区域的导数但是很小的呀~~~~能够预见到,随着网络的加深,梯度后向传播到浅层网络时,就呵呵了,基本不能引发数值的扰动,这样浅层的网络就学习不到新的特征了。dom

那么怎么办?我暂时看到了四种解决问题的办法,仅仅是根据我本身看的论文总结的,并不是权威的说法。第一种很明显,能够经过使用别的激活函数;第二种可使用层归一化;第三种是在权重的初始化上下功夫,第四种是构建新的网络结构~。但暂时不写,我还想记录一下看到的梯度消失/爆炸问题在另外一个经典网络的出现。ide

======================函数

噔噔噔噔,对的,就是RNN。学习

RNN网络简单来讲,就是把上层的hidden state与输入数据一同输入到神经元中进行处理(如左图),它是与序列相关的。若是把网络按照时间序列展开,能够获得右图

                

假如要求偏导数,能够看到一个连乘的式子,元素是,假如大于1,通过k个乘法后会变得异常巨大,毕竟是指数级的,若是小于1,又会变得十分小。这就是RNN中梯度爆炸与消失的问题了。

 贴一个RNN的代码,有注释,很容易看明白,来自这里

  1 import copy, numpy as np
  2 np.random.seed(0)
  3 
  4 # compute sigmoid nonlinearity
  5 def sigmoid(x):
  6     output = 1/(1+np.exp(-x))
  7     return output
  8 
  9 # convert output of sigmoid function to its derivative
 10 def sigmoid_output_to_derivative(output):
 11     return output*(1-output)
 12 
 13 
 14 # training dataset generation
 15 int2binary = {}
 16 binary_dim = 8
 17 
 18 largest_number = pow(2,binary_dim)
 19 binary = np.unpackbits(
 20     np.array([range(largest_number)],dtype=np.uint8).T,axis=1)
 21 for i in range(largest_number):
 22     int2binary[i] = binary[i]
 23 
 24 
 25 # input variables
 26 alpha = 0.1
 27 input_dim = 2
 28 hidden_dim = 16
 29 output_dim = 1
 30 
 31 
 32 # initialize neural network weights
 33 synapse_0 = 2*np.random.random((input_dim,hidden_dim)) - 1
 34 synapse_1 = 2*np.random.random((hidden_dim,output_dim)) - 1
 35 synapse_h = 2*np.random.random((hidden_dim,hidden_dim)) - 1
 36 
 37 synapse_0_update = np.zeros_like(synapse_0)
 38 synapse_1_update = np.zeros_like(synapse_1)
 39 synapse_h_update = np.zeros_like(synapse_h)
 40 
 41 # training logic
 42 for j in range(10000):
 43     
 44     # generate a simple addition problem (a + b = c)
 45     a_int = np.random.randint(largest_number/2) # int version
 46     a = int2binary[a_int] # binary encoding
 47 
 48     b_int = np.random.randint(largest_number/2) # int version
 49     b = int2binary[b_int] # binary encoding
 50 
 51     # true answer
 52     c_int = a_int + b_int
 53     c = int2binary[c_int]
 54     
 55     # where we'll store our best guess (binary encoded)
 56     d = np.zeros_like(c)
 57 
 58     overallError = 0
 59     
 60     layer_2_deltas = list()
 61     layer_1_values = list()
 62     layer_1_values.append(np.zeros(hidden_dim))
 63     
 64     # moving along the positions in the binary encoding
 65     for position in range(binary_dim):
 66         
 67         # generate input and output
 68         X = np.array([[a[binary_dim - position - 1],b[binary_dim - position - 1]]])
 69         y = np.array([[c[binary_dim - position - 1]]]).T
 70 
 71         # hidden layer (input ~+ prev_hidden)
 72         layer_1 = sigmoid(np.dot(X,synapse_0) + np.dot(layer_1_values[-1],synapse_h))
 73 
 74         # output layer (new binary representation)
 75         layer_2 = sigmoid(np.dot(layer_1,synapse_1))
 76 
 77         # did we miss?... if so, by how much?
 78         layer_2_error = y - layer_2
 79         layer_2_deltas.append((layer_2_error)*sigmoid_output_to_derivative(layer_2))
 80         overallError += np.abs(layer_2_error[0])
 81     
 82         # decode estimate so we can print it out
 83         d[binary_dim - position - 1] = np.round(layer_2[0][0])
 84         
 85         # store hidden layer so we can use it in the next timestep
 86         layer_1_values.append(copy.deepcopy(layer_1))
 87     
 88     future_layer_1_delta = np.zeros(hidden_dim)
 89     
 90     for position in range(binary_dim):
 91         
 92         X = np.array([[a[position],b[position]]])
 93         layer_1 = layer_1_values[-position-1]
 94         prev_layer_1 = layer_1_values[-position-2]
 95         
 96         # error at output layer
 97         layer_2_delta = layer_2_deltas[-position-1]
 98         # error at hidden layer
 99         layer_1_delta = (future_layer_1_delta.dot(synapse_h.T) + layer_2_delta.dot(synapse_1.T)) * sigmoid_output_to_derivative(layer_1)
100 
101         # let's update all our weights so we can try again
102         synapse_1_update += np.atleast_2d(layer_1).T.dot(layer_2_delta)
103         synapse_h_update += np.atleast_2d(prev_layer_1).T.dot(layer_1_delta)
104         synapse_0_update += X.T.dot(layer_1_delta)
105         
106         future_layer_1_delta = layer_1_delta
107     
108 
109     synapse_0 += synapse_0_update * alpha
110     synapse_1 += synapse_1_update * alpha
111     synapse_h += synapse_h_update * alpha    
112 
113     synapse_0_update *= 0
114     synapse_1_update *= 0
115     synapse_h_update *= 0
116     
117     # print out progress
118     if(j % 1000 == 0):
119         print "Error:" + str(overallError)
120         print "Pred:" + str(d)
121         print "True:" + str(c)
122         out = 0
123         for index,x in enumerate(reversed(d)):
124             out += x*pow(2,index)
125         print str(a_int) + " + " + str(b_int) + " = " + str(out)
126         print "------------"
127 
128         
RNN

 

2、    选择其余激活函数

 激活函数有不少其余的选择,常看见的有ReLU、Leaky ReLU函数。下面就说一说这两个函数,对于ReLU,函数为,观察一下它的函数图像

能够看到,它在负数的一段永远是0啊。为何要使用这个函数呢?听说它是与神经学科有关的,这是由于稀疏激活的,这表如今负数端是抑制状态,正数兴奋激活。并且有理论也代表,稀疏的网络更准确,在googLeNet的实现中就是利用了神经网络的稀疏性。并且,在正数端导数永远为1,这就很好地解决了梯度消失的问题了。但是,它没有把数据压缩,这会使得数据的范围可能很大。

此外还有Leaky ReLU函数,这个我是在YOLO看到的,其实和ReLU差很少,就是在负数端不彻底抑制了。图像以下:

 

3、  层归一化

 这里记录的是Batch Normalization。主要参考(1)(2)(3)写的总结,可怜我只是个搬运工啊。

 先说一说BN解决的问题,论文说要解决 Internal covariate shift 的问题,covariate shift 是指源空间与目标空间中条件几率一致,可是边缘几率不一样。在深度网络中,越深的网络对特征的扭曲就越厉害(应该是这样说吧……),可是特征自己对于类别的标记是不变的,因此符合这样的定义。BN经过把输出层的数据归一化到mean = 0, var = 1的分布中,可让边缘几率大体相同吧(知乎魏大牛说不能够彻底解决,由于均值方差相同不表明分布相同~~他应该是对的),因此题目说是reducing。

那么BN是怎么实现的呢?它是经过计算min batch 的均值与方差,而后使用公式归一化。例如激活函数是sigmoid,那么输出归一化后的图像就是

中间就是接近线性了,这样,导数几乎为常数1,这样不就能够解决梯度消失的问题了吗?

可是,对于ReLU函数,这个是否起做用呢?好像未必吧,不过我以为这个归一化能够解决ReLU不能把数据压缩的问题,这样可使得每层的数据的规模基本一致了。上述(3)中写到一个BN的优势,我以为和个人想法是一致的,就是可使用更高的学习率。若是每层的scale不一致,实际上每层须要的学习率是不同的,同一层不一样维度的scale每每也须要不一样大小的学习率,一般须要使用最小的那个学习率才能保证损失函数有效降低,Batch Normalization将每层、每维的scale保持一致,那么咱们就能够直接使用较高的学习率进行优化。这样就能够加快收敛了。我以为仍是主要用来减小covariate shift 的。

可是,上述归一化会带来一个问题,就是破坏本来学习的特征的分布。那怎么办?论文加入了两个参数,来恢复它原本的分布这个带入归一化的式子看一下就能够知道恢复原来分布的条件了。可是,若是恢复了原来的分布,那还须要归一化?我开始也没想明白这个问题,后来看看别人的解释,注意到新添加的两个参数,其实是经过训练学习的,就是说,最后可能恢复,也可能没有恢复。这样能够增长网络的capicity,网络中就存在多种不一样的分布了。最后抄一下BP的公式:

 

 那么在哪里可使用这个BN?很明显,它应该使用在激活函数以前。而在此处还提到一个优势就是加入BN能够不使用dropout,为何呢?dropout它是用来正则化加强网络的泛化能力的,减小过拟合,而BN是用来提高精度的,之因此说有这样的做用,可能有两方面的缘由(1)过拟合通常发生在数据边缘的噪声位置,而BN把它归一化了(2)归一化的数据引入了噪声,这在训练时必定程度有正则化的效果。对于大的数据集,BN的提高精度会显得更重要,这二者是能够结合起来使用的。

最后贴一个算法的流程,以及结构图,结构图是来自   http://yeephycho.github.io/2016/08/03/Normalizations-in-neural-networks/   

       

 

4、   权值初始化

 为了让信息能够更好的在网络中流动(不必定是梯度消失的问题),可使用xavier的初始化方法。主要能够看知乎专栏。为了避免重复别人的工做,我简单总结一下算了。注意一个问题,xavier的初始化方法的前提假设是,激活函数是线性的(其实归一化后,可能把数据集中在了一处,就好像将BN的那张图同样)。

若是输入数据x和权值w都知足均值为0,标准差为,(x能够经过归一化、白化实现)并且各数据是独立同分布的。这样,输出为,根据几率公式,z 的均值依然为0,方差为

经过递推公式能够获得,则。因而,方差的计算公式为。这里又出现了连乘,仍是按照以前与1比较的讨论,那么,最好是可让方差保持一致啦,这样数值的幅度就不会相差太大,就好像上面BN说的那样,能够收敛的更快。那么就是让连乘内的每一项都为1了,则能够推出权值的初始化为

上面说的是前向的,那么后向呢?后向传播时,若是可让方差保持一致,一样地会有前向传播的效果,梯度能够更好地在网络中流动。因为假设是线性的,那么回流的梯度公式是,能够写成。令回流的方差不变,那么权值又能够初始化成。注意一个是前向,一个是后向的,二者的n 是不一样的。取平均

最后,是使用均匀分布来初始化权值的,获得初始化的范围

另一种MSRA的初始化的方法,能够学习http://blog.csdn.net/shuzfan/article/details/51347572,实验效果表现要好一些,但貌似xavier用的要多一些。

 

5、   调整网络的结构

 解决RNN的问题,提出了一种LSTM的结构,但我对LSTM还不是太熟悉,就不装逼了。主要是总结最近看的两篇文章《Training Very Deep Networks》和《Deep Residual Learning for Image Recognition》。

 

Highway Network

 Highway Network主要解决的问题是,网络深度加深,梯度信息回流受阻形成网络训练困难的问题。先看下面的一张对比图片,分别是没有highway 和有highway的。

能够看到,当网络加深,训练的偏差反而上升了,而加入了highway以后,这个问题获得了缓解。通常来讲,深度网络训练困难是因为梯度回流受阻的问题,可能浅层网络没有办法获得调整,或者我本身YY的一个缘由是(回流的信息通过网络以后已经变形了,极可能就出现了internal covariate shift相似的问题了)。Highway Network 受LSTM启发,增长了一个门函数,让网络的输出由两部分组成,分别是网络的直接输入以及输入变形后的部分。

假设定义一个非线性变换为,定义门函数,携带函数。对于门函数取极端的状况0/1会有,而对应的门函数使用sigmoid函数,则极端的状况不会出现。

一个网络的输出最终变为,注意这里的乘法是element-wise multiplication。

注意,门函数,转换的维度应该是相同的。若是不足,能够用0补或者用一个卷积层去变化。

在初始化的时候,论文是把偏置 b 初始化为负数,这样可让携带函数 C 偏大,这样作的好处是什么呢?可让更多的信息直接回流到输入,而不须要通过一个非线性转化。个人理解是,在BP算法时,这必定程度上增大了梯度的回流,而不会被阻隔;在前向流动的时候,把容许原始的信息直接流过,增长了容量,就好像LSTM那样,能够有long - term temporal dependencies。

 

 Residual Network

 ResNet的结构与Highway很相似,若是把Highway的网络变一下形会获得,而在ResNet中,直接把门函数T(x)去掉,就获得一个残差函数,并且会获得一个恒等的映射 x ,对的,这叫残差网络,它解决的问题与Highway同样,都是网络加深致使的训练困难且精度降低的问题。残差网络的一个block以下:

是的,就是这么简单,可是,网络很强大呀。并且实验证实,在网络加深的时候,依然很强大。那为何这么强大呢?我以为是由于identity map是的梯度能够直接回流到了输入层。至因而否去掉门函数会更好呢,这个并不知道。在做者的另外一篇论文《Identity Mappings in Deep Residual Networks》中,实验证实了使用identity map会比加入卷积更优。并且经过调整激活函数和归一化层的位置到weight layer以前,称为 pre-activation,会获得更优的结果。

对于网络中的一些虚线层,他们的shortcut就链接了两个维度不一样的feature,这时,有两种解决办法(1)在维度减小的部分直接使用 identity 映射,同时对于feature map增长部分用0补齐。(2)经过1*1的卷积变形获得。对于这个1*1的投影是怎么作的,能够参考VGG-16。我开始也很纳闷,例如上面的虚线,输入有64个Feature,输出是128个Feature,若是是用128个kernel作卷积,应该有64*128个feature啊。纠结好久,看了看VGG的参数个数就明白了,以下图

 

 

 

 

例如第1、二行,输入3个Feature,有64个卷积核但却有64个输出,是怎么作到的呢?看它的权值的个数的计算时(3*3*3)*64,也就是说,实际上这64个卷积核实际上是有3维的通道的。对应于ResNet的64个输入,一样卷积核也是有64个channel的。

 

结语

还请大牛能够指正不足以及思考不周的地方,指点学习的方向啊!!!!!!