[关闭]
@water 2015-11-09T01:23:40.000000Z 字数 15895 阅读 3750

用Theano进行LeNet实验

Deeplearning Theano 字符识别


1 LeNet简介

传统的神经网络,神经元之间全连接,用sigmoid,tanh等非线性函数将输入数据的线性组合wx+b进行多层的非线性变换,把输入数据变换到一个可以用传统的分类器(如SVM, Logistic Regression),较好区分的空间,进行类别的划分。LeNet保留了传统神经网络的主要思想,并受生物视觉皮层处理图像的启发,对传统的神经网络进行了改进。

1.1 传统视觉皮层的两点启发

视觉皮层处理中,每个神经元,只处理一定范围(它所在的接收域)内的输入数据,是一个层次化的处理过程。处于底层的简单神经元,对在其接收域内,特定的边有最大的反馈,如图1所示;位于高层的复杂神经元,与底层神经元相比,对最底层的输入有更宽的接收域,并且对某一模式的识别,和其在局部区域的具体位置无关。
图1 底层的神经元对最左侧图的反应对应右侧的两张图
图1 底层的神经元对最左侧图的反馈类似右侧的两张图,右侧的图通过单层LeNet卷积层实现,见图4。这表明单层LeNet卷积层实现了视觉神经元底层对特定边反馈的工作。

1.2 LeNet卷积层的稀疏连接和权值共享特性

基于上述的特性,LeNet卷积层在传统的全连接神经网络基础上做了改进,主要表现在稀疏连接和权值共享两方面。这也使得LeNet的参数更少,对于图像这类高维度数据的处理有更好的适应性。

1.2.1 LeNet卷积层的稀疏连接

LeNet的稀疏连接
图2 LeNet卷积层的稀疏连接。每个神经元对应一定范围的接收域,图例中神经元对于相邻的底层输入的接收域为3。m层对m-1层的接收域为3,m+1层对m-1层的接收域为5。

1.2.2 LeNet卷积层的权值共享

LeNet的权值共享
图3 LeNet卷积层的权值共享。图中相同颜色的边权值相同,即第m层的神经元,对m-1层的各接收域之间,采用相同权值的边连接。所有采用相同权值的m层神经元构成了一个特征映射(feature map),即相同的权值边构成的特征过滤器(filter)提取了m-1层输入的相同特征。

1.2.3 单层LeNet卷积层

单层LeNet卷积层
图4 单层LeNet卷积层。
结合前面两点特性,将输入层和输出层变成二维图像矩阵就构成了单层LeNet卷积层。图例中,输入层m-1层有4个feature map,即左侧四个大的矩形。隐含层 m中有2个feature map:h_0h_1W^kl_ij表示,连接m层第k个feature map与m-1层的第l个feature map的filter中,坐标(i,j)对应的权值。图示中最左侧的的4个写着不同W^kl_ij的小方块,表示4个filter,代表了h_0与m-1层的4个feature map间的卷积filter,也是二者之间的连接权值。每一对(m-1层的feature map,m层的feature map)对应着一个卷积filter,它们间连接以卷积filter的大小为单位。在图4中对应2x2的卷积filter,m-1层的第一个feature map 中每个卷积filter大小的区域,以灰色的卷积filter的值为权值,连接h_0中的某个神经元。h_0中所有的神经元与m-1层第一个feature map 的所有2x2区域的连接,对应的权值都为灰色卷积filter的值,从而实现了权值共享。m-1层的feature map中每个2x2的卷积filter大小的区域,在LeNet中,是紧邻而不相交的关系,如图5所示。m层中的每个神经元/像素(图示中的红色和蓝色的方块)由卷积filter与卷积filter对应的m-1层的feature map做卷积运算后,加上bias,并进行非线性变换后得到。每个像素只与一块卷积filter大小的输入连接,实现了稀疏连接。这些filter也叫做convolutional filters,得到的feature map也称为convolved feature map,或者卷积层。

1.2.3.1 filter与feature map的卷积运算

卷积运算实例
图5 卷积运算过程。
输入的feature map大小为5x5,卷积filter大小为3x3的矩阵101010101,与输入的feature map进行卷积运算,卷积层的第一个元素计算过程如下:
100110111101010101=1×1+1×0+1×1+0×0+1×1+1×0+0×1+0×0+1×1=4.
得到的卷积层的大小为(5-3+1)x(5-3+1),即3x3的矩阵。
注: 真实的隐含层单元的值,还需要将convolved feature的值加上bias后,再进行非线性变换才算得到。非线性变换后的隐含单元值相当于隐含单元上的一个分数。

1.2.3.2 单层LeNet卷积层的Theano代码实现

以下是对图1所示的示例的代码实现。对theano.shared的解释见2.1数据准备

  1. #use ssh -X username@servername
  2. #the -X will get rid of the no display name and no $DISPLAY environment variable error
  3. # $THEANO_FLAGS=mode=FAST_RUN,device=cpu,floatX=float32 python
  4. import theano
  5. from theano import tensor as T
  6. from theano.tensor.nnet import conv
  7. import numpy
  8. import pylab
  9. from PIL import Image
  10. """begin processing procedure definition"""
  11. # param rng: To randomelize weight filter w
  12. # 23455 random seed, to ensure the generated rng stability
  13. rng = numpy.random.RandomState(23455)
  14. # instantiate 4D tensor for input
  15. # [mini-batch size, number of input feature maps, image height, image width]
  16. input = T.tensor4(name='input')
  17. # initialize shared variable for weights.
  18. # [ number of feature maps at layer m,number of feature maps at layer m-1, filter height, filter width]
  19. # input image consists of 3 feature maps (RGB), use 2 9*9 convolutional filters
  20. w_shp = (2, 3, 9, 9)
  21. w_bound = numpy.sqrt(3 * 9 * 9)
  22. W = theano.shared( numpy.asarray(
  23. rng.uniform(
  24. low=-1.0 / w_bound,
  25. high=1.0 / w_bound,
  26. size=w_shp),
  27. dtype=input.dtype), name ='W')
  28. # initialize shared variable for bias (1D tensor) with random values. IMPORTANT: biases are usually initialized to zero. initialize them to random values to "simulate" learning.
  29. b_shp = (2,)
  30. b = theano.shared(numpy.asarray(
  31. rng.uniform(low=-.5,high=.5,size=b_shp)
  32. ,dtype=input.dtype), name='b')
  33. # build symbolic expression that computes the convolution of input with filter in w
  34. conv_out = conv.conv2d(input,W)
  35. # build symbolic expression to add bias and apply activation function
  36. output = T.nnet.sigmoid(conv_out + b.dimshuffle ('x',0,'x','x') )
  37. #create theano function to compute filtered images
  38. f = theano.function([input],output)
  39. """end processing procedure definition"""
  40. """begin specific image processing"""
  41. # open random image of dimensions 639x516
  42. img = Image.open(open('3wolfmoon.jpg'))
  43. # dimensions are (height, width, channel), after /256 the input image scales to the range [0,1]
  44. img = numpy.asarray(img, dtype='float32')/256.
  45. # put image in 4D tensor of shape (1, 3, height, width), 1 mini-batch, 3 feature maps
  46. # why should we use transpose(2,0,1) here???
  47. img_ = img.transpose(2, 0, 1).reshape(1, 3, 639, 516)
  48. filtered_img = f(img_)
  49. # plot original image and first and second components of output
  50. pylab.subplot(1, 3, 1); pylab.axis('off'); pylab.imshow(img)
  51. pylab.gray();
  52. # recall that the convOp output (filtered image) is actually a "minibatch" of size 1 here, so we take index 0 in the first dimension:
  53. # [mini-batch size, number of input feature maps, image height, image width]
  54. pylab.subplot(1, 3, 2); pylab.axis('off'); pylab.imshow(filtered_img[0,0,:,:])
  55. pylab.subplot(1, 3, 3); pylab.axis('off'); pylab.imshow(filtered_img[0,1,:,:])
  56. pylab.show()
  57. """end specific image processing"""

图1中第二幅和第三幅图分别代表了由一层LeNet卷积层对图像进行提取后得到的特征信息,它们与视觉皮层的底层神经元提取的边界信息非常近似。

1.2.4 Maxpooling最大池化

为了更进一步地让高层的神经元与最底层输入的模式的位置无关,并进一步减少模型参数,在卷积层后引入了最大池化[1],即将卷积层划分为互不重叠的几个区域,用各区域的最大值作为各区域的表示,形成最大池化层。如图6所示。
池化过程
图6 池化过程。

最大池化函数定义的Theano代码如下:

  1. from theano.tensor.signal import downsample
  2. input = T.dtensor4('input')
  3. maxpool_shape = (2, 2)
  4. pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True)
  5. f = theano.function([input],pool_out)

1.3 LeNet 5的结构

通过卷积和最大池化层的不断交替连接,与最后1-2层中的神经元间采用传统的全连接神经网络一起组成了如图7所示的LeNet5神经网络。

Todo: 在代码中将此结构反应出来。

LeNet 5
图7: LeNet 5。连接信息见图示。来源LeNet5原文,LeNet5原文2
简要的解释:输入图像32x32的灰度图像,第一层卷积层C1由6个feature map组成,卷积fitler的大小为5x5,共有六个卷积filter,这样卷积层feature map的大小为(32-5+1)x(32-5+1)=28x28。第一个池化层S2有6个feature map,与6个C1中的feature map 一一对应。池化的区域为2x2,池化层的大小为(28/2)x(28/2)=14x14。
S2和C3间的连接,对应的卷积filter的大小仍为5x5,但它们并没有采用所有feature maps 互连的方式,而是只将双方一部分的feature map 连接了起来,一是可以减少参数个数,二是保证不同的feature map 提取出的图像特征是不同的。S2与C3之间的连接用一个connection map来表示,connection map表示如下:
connection map
图8 S2和C3之间的connection map,行表示S2的feature map,列表示C3的feature map。画x的表示feature map之间有连接。C3前面6个feature map分别与S2中的3个连接,7-15个与S2中的4个连接,第16个与S2中全部的feature map连接。
C3中的第0个feature map计算如下:
C30=S20W0,0+S21W1,0+S22W2,0, 其中*代表卷积运算,Wk,l表示C3的第k个feature map和S2的第l个卷积filter。
依次往后,S4与C5之间,通过5x5的卷积filter刚好实现了全连接。
F6和C5之间是全连接的方式。F6和output的连接,原文中采用了输入与权值间的距离的平方和表示,即yi=j(xjwij)2. 在实际应用中,我们采用logistic regression来计算F6的输入和10个output label之间的关系。

在Theano的实例代码中,各层的feature map是全连接的,神经元的值的计算方法依然同上。全联接最重要的原因在于目前硬件设备计算性能上的提升。

1.4 反向传播算法

1.4.1 概述

构建好模型后,就需要对模型的参数根据数据进行推断了。推断的结果,即参数的值,由最优化训练数据的某一标准得到。对于监督的神经网络,需要调整整个模型的参数,将输出层的误差最小化。调整参数得到最小化误差时,我们这里利用了梯度下降的方法,将权重参数按误差函数对权重的梯度的方向和倍数进行递减。在使用梯度下降法,进行最优化的过程中,需要得到误差函数分别对各权重的偏导数CostfunctionW,这就是反向传播算法的目的所在。由链导法则可得,误差对各权重的偏导数可以由输出层起,反向传递计算,一直计算到误差函数对输入层连接边权值的偏导。这一反向传递计算误差对权重偏导对方法,也被称为反向传播方法。
理解反向传播算法有重要意义,首先,它的应用广泛。反向传播计算误差函数对各权重对偏导数,只需一次反向传播即可得到,而若从输入连接的权重开始正向递推到输出,每次只能计算一个输出误差对连接权重的偏导。反向传播极大地减少了计算众多参数对应的偏导数的计算时间。其次,反向传播算法能让我们更加清楚,权重和bias的改变,如何改变整个神经网络的效果。

对于反向传播方法个人觉得最好的博客介绍来自一位Googler的博客,他的每篇博客都会花上很多时间来写,非常清晰易懂,内容循序渐进,加入了自身对技术对看法,很有启发意义[2]。请移步http://colah.github.io/posts/2015-08-Backprop/更详细的教程参见http://neuralnetworksanddeeplearning.com/chap2.html#warm_up_a_fast_matrix-based_approach_to_computing_the_output_from_a_neural_network

同时,为了时模型具有较好的扩展性,在应用梯度下降方法更新权重时,我们引入了测试集,使得模型参数对不在训练集中的数据也能达到最优。具体见2.2.1 验证数据集对模型训练的考量。

在用代码计算各偏导时,用到了向量化来计算,能极大地提高计算效率。
简要介绍:
在神经网络第l层的激活值组成的向量为alwl为第l层的神经元个数x第l1层的神经元个数大小的矩阵,bll层的各神经元的偏差组成的向量,σ为element-wise函数,即对向量中的每个元素进行计算。这样就有:alσ(wlal1+bl).也可以写成alσ(zl),其中zlwlal1+bl,表示第l层的带权重的输入。
反向传播算法的目的在于计算误差函数分别对各权重和偏置的偏导数CostfunctionWCostfunctionb。在计算之前,先引入误差δlj,表示第l层,第j个神经元的误差。反向传播算法提供了计算δlj的方法,然后将δljCostfunctionwljkCostfunctionblj相关联。

1.4.2与单个神经元误差有关的四大公式

定义误差δljCostaljσ(zlj)Czlj。定义为对zlj,而非对alj求偏导,是因为在计算时可以更方便。与误差相关的四个公式,在这篇文章中,是先提出公式,后证明的,在阅读过程中对于公式变化的推倒和理解,按链导法则来即可。

注:对误差定义http://neuralnetworksanddeeplearning.com/chap2.html#the_four_fundamental_equations_behind_backpropagation 难理解的地方:
1)将权重输入zlj改变Δzlj,最终cost funtion的改变量为CostzljΔzlj理解
2)https://www.evernote.com/shard/s661/sh/8828d79b-4eda-4479-ae66-45c180a42a1e/84a24dfc9798083fa247172596a3dcfc

四大公式如下,各公式在反向传递偏导时都有自身的意义:
1 δLj=CaLjσ(zLj),写成矩阵或向量的形式为 δL=aCσ(zL),其中aC是一个向量,它对元素为CaLj为元素间相乘。
2 δlj=((wl+1)Tδl+1)σ(zlj)
3 Cblj=δlj
4 Cwljk=al1kδlj
四大公式的直观意义:
1.最后一层的误差怎么由最后一层的数值计算。
2.误差怎么往前递推。
3.cost funtion 对偏置的偏导怎么计算。
4.怎么由误差计算得到cost funtion 对权重的偏导。
从这四个式子还可以看出偏导的大小与激活函数σ之间的关系。
下面推导一下传说中的四大公式,更好地掌握反向传播方法。
1.δLj=CzLj=kCaLkaLkzLj=CaLjaLjzLj=CaLjσ(zLj)
2.δlj=Czlj=kCzl+1kzl+1kzlj=kδl+1kzl+1kzlj
zl+1k=jwl+1kjalj+bl+1k=jwl+1kjσ(zlj)+bl+1k
所以有zl+1kzlj=wl+1kjσ(zlj)
所以δlj=kδl+1kzl+1kzlj=kδl+1kwl+1kjσ(zlj)
δl+1=(δl1,...,δlJ)T
wl+1=wl+111wl+121...wl+1K1wl+112wl+122...wl+1K2.............wl+11Jwl+12J...wl+1KJ
δlj=kδl+1kwl+1kjσ(zlj)=((wl+1)Tδl+1)σ(zlj)
3.Cblj=kCzlkzlkblj=Czljzljblj=δlj
4.Cwljk=Czljzljwljk
zlj=kwljkal1k+blj
所以zljwljk=al1k
所以Cwljk=Czljzljwljk=al1kδlj.

1.4.3反向传播算法过程

http://neuralnetworksanddeeplearning.com/chap2.html#the_backpropagation_algorithm
Loss function:1||(θ={W,b},)=1||||i=0log(P(Y=y(i)|x(i),W,b))(θ={W,b},)
再求cost对参数的导数,将导数乘学习率,用随机梯度下降法进行权值更新。

2 实验过程

2.1 数据准备

输入数据格式的定义:a tuple of 3 lists : the training set, the validation set and the testing set. Each of the three lists is a pair formed from a list of images and a list of class labels for each of the images. An image is represented as numpy 1-dimensional array of 784 (28 x 28) float values between 0 and 1 (0 stands for black, 1 for white). The labels are numbers between 0 and 9 indicating which digit the image represents.
training set: 是一个pair,这个pair的格式为(N个(N为训练集个数)784列vetor组成的list,N个label组成的list) 。
归一化图像vector时,直接除以255。

2.1.1 数据预处理

对任何数据只要用脚本转换为上述的输入数据格式即可。
原始数据格式:
数据包含在一个大的文件夹下,运行转换格式的脚本时,需要输入该文件夹的位置。大文件夹内包含以数字命名的文件夹,代表各类类标号对应的类别。见:https://github.com/waterofphysics/LearningTheano/tree/master/data/t10k-images-bmp/t10k-images

训练集与验证集来源于同一个数据源,按训练集90%与验证集10%对原数据进行了划分。
测试集自成一个数据源,格式与上面一样。

转换数据结构
写的这个脚本最大的瓶颈在于没有考虑大型数据集,存成pkl时,需要先在内存中存一个三元组组成的tuple;
第二个瓶颈在彩色图像的处理。后续解决了新图像分类的问题后,会回过头来解决这两个问题的.
正确性的验证一方面是代码的逻辑,不过我不确定都考虑全了。
另一方面,把MNIST数据集转成了图像,放在tk10-images中,再用自己的脚本转化成了testMnist.pkl,实验的结果在code下的test.txt中,如果误差率在0.92%左右,则是正确的。

训练数据和验证数据的生成:
https://github.com/waterofphysics/LearningTheano/blob/master/LeNet/imagestopkl.py

测试数据的生成:
https://github.com/waterofphysics/LearningTheano/blob/master/LeNet/imagestopklFTestset.py

2.1.2 考虑GPU时数据处理的注意项

2.1.2.1 theano.shared的使用

""" Function that loads the dataset into shared variables

The reason we store our dataset in shared variables is to allow
Theano to copy it into the GPU memory (when code is run on GPU).
Since copying data into the GPU is slow, copying a minibatch everytime is needed (the default behaviour if the data is not in a shared variable) would lead to a large decrease in performance.
"""

一句话:直接将数据立马拷贝到GPU内存中,避免后续训练过程中每次都要将minibatch的数据从内存拷贝到GPU内存。
反应在代码中就是下列函数:

  1. def shared_dataset(data_xy, borrow=True):
  2. """ Function that loads the dataset into shared variables
  3. The reason we store our dataset in shared variables is to allow
  4. Theano to copy it into the GPU memory (when code is run on GPU).
  5. Since copying data into the GPU is slow, copying a minibatch everytime
  6. is needed (the default behaviour if the data is not in a shared
  7. variable) would lead to a large decrease in performance.
  8. """
  9. data_x, data_y = data_xy
  10. shared_x = theano.shared(numpy.asarray(data_x,
  11. dtype=theano.config.floatX),
  12. borrow=borrow)
  13. shared_y = theano.shared(numpy.asarray(data_y,
  14. dtype=theano.config.floatX),
  15. borrow=borrow)
  16. # When storing data on the GPU it has to be stored as floats
  17. # therefore we will store the labels as ``floatX`` as well
  18. # (``shared_y`` does exactly that). But during our computations
  19. # we need them as ints (we use labels as index, and if they are
  20. # floats it doesn't make sense) therefore instead of returning
  21. # ``shared_y`` we will have to cast it to int. This little hack
  22. # lets ous get around this issue
  23. return shared_x, T.cast(shared_y, 'int32')

2.1.2.2 GPU的内存大小限制

GPU的内存大小是有限制的,若训练,验证和测试加起来超过了GPU内存的大小,则需要对数据的输入代码进行改变。
You can however store a sufficiently small chunk of your data (several minibatches) in a shared variable and use that during training. Once you got through the chunk, update the values it stores. This way you minimize the number of data transfers between CPU memory and GPU memory.

To Do: 将上述思想转为代码

2.1.2.3 floatX的使用

When storing data on the GPU it has to be stored as floats therefore we will store the labels as floatX as well (shared_y does exactly that). But during our computations we need them as ints (we use labels as index, and if they are floats it doesn't make sense) therefore instead of returning shared_y we will have to cast it to int. This little hack lets us get around this issue.
在运行代码时定义floatX的类型

$ THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python check1.py

$ python imagestopkl.py

$ python imagestopklFTest.py

$(THEANO_FLAGS=mode=FAST_RUN,device=gpu,floatX=float32 python convolutional_mlp.py > test.txt&)

2.1.2.4

2.2 代码分析

建立LeNet

论文: Object recognition with Gradient-Based learning

1.actual training and early-stopping需要参考建立MLP,更细化和全面地理解,以便按需写出自己的代码。2.参数的存储,和对新数据的分类参考LeNet trainded model for prediction

代码参见:
https://github.com/waterofphysics/LearningTheano/blob/master/LeNet/convolutional_mlp.py

2.2.1 验证数据集对模型的优化

1epoch表示将所有的训练数据都过1遍。

3 扩展

3.1 多块GPU并行加速

目前的数据量还不必使用,不过有Theano的改进版本了。用到了多线程编程和网络通信编程的知识。不太懂。但这是训练平台搭建后期需要解决的事情。
举例:https://github.com/Theano/Theano/wiki/Using-Multiple-GPUs

3.2 对不在训练集中数据的识别

是否真的需要把所有的字符数据都放到训练集中?可否用非参模型的方法,让模型自动地为新类别数据生成新类别?
http://rinuboney.github.io/2015/10/18/theoretical-motivations-deep-learning.html
Why not classical non-parametric algorithms?
唯一想创新的点都走不通。
非参本来就难。


[1] 原模型中,并没有采用如1.2.4所述的最大池化的方法,而是每次将输入层中在池化范围内的4个值相加,乘以一个权值再加上bias,并对结果进行非线性变换后得到池化层某个神经元的值。 后续的理论和实验都表明max pooling的效果更好。
[2] 看过Nat and Lo对作者对采访http://googleresearch.blogspot.com/2015/09/a-beginners-guide-to-deep-neural.html,非常nice的一个人,榜样。
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注