来源:http://deeplearning.net/tutorial/lenet.html#lenet
Convolutional Neural Networks (LeNet)
note:这部分假设读者已经看过()和()。另外,这里是用新的theano函数和概念: , , , , , , , .如果你想要在GPU上跑,记得看看 。为了在GPU上跑这个例子,你需要一个好的GPU,至少需要1GB的显存。如果你的显示器也是连接着这个GPU,那么就需要注意一些事情了。因为GPU连接着显示器的时候,在每个GPU的函数调用的时候都会有几秒的限制。这是因为在当显示器需要请求GPU的时候,是无法进行计算的。没有这个限制的时候,你的屏幕看起来就像是死机一样,而且还会保持很长时间。(该例子就是在中端GPU下遇到的这个问题,这句话是原作者遇到的)当GPU没有连接到显示器的时候,就没有时间限制了,你可以降低batch size来解决这个时间延迟的问题。
note:这部分的可用代码可以从这里 和这里 下到。
一、动机
卷积神经网络(Convolutional Neural Networks ,CNN)是MLPs的生物启发的变种。从Hubel和Wiesel的早期在猫的视觉皮层上的工作上来看,我们知道视觉皮层包含着许多复杂排列的细胞。这些细胞对于视觉区域中小的子区域是非常敏感的,叫做感受野。这些子区域可以平铺从而覆盖整个视觉区域。这些细胞扮演着基于输入空间的局部过滤器而且很适合用来探索自然图像中的空间局部的强相关性。
另外,两类基本的细胞类型已经被发现:简单细胞当检测到它们感受野的类边缘模式的时候的响应是最大的。复杂的细胞有着更大的感受野,而且对于陌生的提取的位置具有局部不变性。
动物的视觉皮层是现今最好的视觉处理系统,所以我们很自然的去效仿它的原理。因此,能够在文献中找到许多神经启发的模型。比如:NeoCogitron , HMAX and LeNet-5 ,本教程关注的是第三个模型。
二、稀疏链接
CNN是通过在毗连的层的神经元之间建立局部连接模式来达到空间局部化的相关性。换句话说,第 m 层的隐藏单元的输入就是来自于第 m -1 层单元的子集,这些单元有着空间连续感受野。我们可以以下图示意:
想象下第m -1 层是视网膜作为输入。在上面部分,第 m 层的单元在基于视网膜输入的基础上有着width 为3的感受野,所以在视网膜层就只有3个毗连的神经元与上层的一个单元相连。第 m +1 层的单元与下层有着相似的连接,这里关于下层的感受野也同样是3,不过它们关于视网膜输入层的感受野是5(比3大)。每个单元对于视网膜上感受野外部区域的响应是无变化的(没反应也就是)。这个结构因此就能确保学到的“过滤器”能够对空间局部输入模式生成最强的响应。
然而,正如上面介绍的,堆叠许多这样的(非线性)“过滤器”层就能够增加“全局”性(即,能够对更大的像素空间进行响应)。例如,第m+1的隐藏层中的单元能够编码一个有着width为5(在像素空间中的单位)的非线性特征。
三、共享权重
另外,在CNN中,每个过滤器 在整个视觉区域上是交叉重复的。在一个特征图中这些重复的单元共享相同的参数(权重向量和偏置):
在上面的图中,我们展示的是属于同一个特征图的3个隐藏单元。具有相同颜色的权重的值是相同的。梯度下降仍然可以用来学习这样的共享的参数,只是需要稍微改动下原来的算法。共享权重的梯度是简单的共享的参数的梯度的和。
以这种形式来重复的单元允许检测到的特征在视觉区域中无视它们所处的位置。另外权重共享通过大量的减少了所需要学习的自由参数的数量而提升了学习的效率。这个模型上的约束条件能够保证CNN在视觉问题上得到更好的泛化。
四、细节和符号介绍
一个特征图可以通过重复的将同一个函数交叉的应用在整个图像的子区域上,换句话说,通过使用一个线性分类器来对输入图像进行卷积,并增加一个偏置项,然后使用一个非线性函数来计算。假设我们在给定的层上第 k 个特征图为,该特征图上的过滤器是由权重 和偏置 决定的 ,然后这个特征图可以如下形式获得:
note:回顾下1D信号的卷积的定义 。这可以扩展到2D的形式: 。
为了形成数据的更丰富的表征,每个隐藏层都是由多特征图组成的, 。一个隐藏层的权重 可以被表示成4D张量的形式,其中包含了每个元素都是由目的特征图、源特征图、源垂直位置、源水平位置的组合而成的。偏置 可以被表示成一个向量,其中包含着的每个元素都是对应着每个不同的目标特征图。图示的形式如下:
图1:一个卷积层的例子
该图显示的是一个CNN的两层。第m-1层包含着4个特征图。隐藏层m 包含着2个特征图( 和 )。在 和 (以蓝色和红色标出的方形区域)中的像素(神经元输出)是从第m-1层中的像素计算出来的,而第m-1层中的感受野为2×2(有色的矩形框)。注意这里感受野是如何跨越所有的四个输入特征图的。 、 的权重 、 所以是3D权重张量,其中第一个维度是用来索引输入特征图的,同时其他两个用来表示像素的坐标。
将它们放在一起,那么, 用来表示在第m 层的第k 个特征图上每个像素与第m-1层的第 I 个特征图的(i,j)位置上的像素相连的权重。
五、卷积操作
在Theano中ConvOp是实现一个卷积层的主力。ConvOp通过theano.tensor.singal.conv2d来使用,这里需要两个符号输入:
- 一个 4D 张量对应着输入图像的一个mini-batch。 张量的原型为: [mini-batch size, 输入特征图的个数, 图像的高度, 图像的宽度].
- 一个 4D 张量对应着权重矩阵. 该张量的原型为: [第m层特征图的个数, 第m-1层特征图的格式,过滤器的高度,过滤器的宽度]。
import theanofrom theano import tensor as Tfrom theano.tensor.nnet import convimport numpyrng = numpy.random.RandomState(23455)# instantiate 4D tensor for inputinput = T.tensor4(name='input')# initialize shared variable for weights.w_shp = (2, 3, 9, 9)w_bound = numpy.sqrt(3 * 9 * 9)W = theano.shared( numpy.asarray( rng.uniform( low=-1.0 / w_bound, high=1.0 / w_bound, size=w_shp), dtype=input.dtype), name ='W')# initialize shared variable for bias (1D tensor) with random values# IMPORTANT: biases are usually initialized to zero. However in this# particular application, we simply apply the convolutional layer to# an image without learning the parameters. We therefore initialize# them to random values to "simulate" learning.b_shp = (2,)b = theano.shared(numpy.asarray( rng.uniform(low=-.5, high=.5, size=b_shp), dtype=input.dtype), name ='b')# build symbolic expression that computes the convolution of input with filters in wconv_out = conv.conv2d(input, W)# build symbolic expression to add bias and apply activation function, i.e. produce neural net layer output# A few words on ``dimshuffle`` :# ``dimshuffle`` is a powerful tool in reshaping a tensor;# what it allows you to do is to shuffle dimension around# but also to insert new ones along which the tensor will be# broadcastable;# dimshuffle('x', 2, 'x', 0, 1)# This will work on 3d tensors with no broadcastable# dimensions. The first dimension will be broadcastable,# then we will have the third dimension of the input tensor as# the second of the resulting tensor, etc. If the tensor has# shape (20, 30, 40), the resulting tensor will have dimensions# (1, 40, 1, 20, 30). (AxBxC tensor is mapped to 1xCx1xAxB tensor)# More examples:# dimshuffle('x') -> make a 0d (scalar) into a 1d vector# dimshuffle(0, 1) -> identity# dimshuffle(1, 0) -> inverts the first and second dimensions# dimshuffle('x', 0) -> make a row out of a 1d vector (N to 1xN)# dimshuffle(0, 'x') -> make a column out of a 1d vector (N to Nx1)# dimshuffle(2, 0, 1) -> AxBxC to CxAxB# dimshuffle(0, 'x', 1) -> AxB to Ax1xB# dimshuffle(1, 'x', 0) -> AxB to Bx1xAoutput = T.nnet.sigmoid(conv_out + b.dimshuffle('x', 0, 'x', 'x'))# create theano function to compute filtered imagesf = theano.function([input], output)用这个来做点有趣的事情:
import numpyimport pylabfrom PIL import Image# open random image of dimensions 639x516img = Image.open(open('doc/images/3wolfmoon.jpg'))# dimensions are (height, width, channel)img = numpy.asarray(img, dtype='float64') / 256.# put image in 4D tensor of shape (1, 3, height, width)img_ = img.transpose(2, 0, 1).reshape(1, 3, 639, 516)filtered_img = f(img_)# plot original image and first and second components of outputpylab.subplot(1, 3, 1); pylab.axis('off'); pylab.imshow(img)pylab.gray();# recall that the convOp output (filtered image) is actually a "minibatch",# of size 1 here, so we take index 0 in the first dimension:pylab.subplot(1, 3, 2); pylab.axis('off'); pylab.imshow(filtered_img[0, 0, :, :])pylab.subplot(1, 3, 3); pylab.axis('off'); pylab.imshow(filtered_img[0, 1, :, :])pylab.show()应该会生成这样的输出:
注意到一个随机初始化的过滤器就像是一个边缘检测器!
注意到我们使用了和MLP中一样的权重初始化公式。权重是从一个均匀分布 [-1/fan-in, 1/fan-in]中随机采样得到的。这里fan-in就是输入到一个隐藏单元的数量。对于MLPs来说,这是下层的单元的数量。对于CNN来说,需要考虑到输入特征图的数量和感受野的size。
六、最大池化
另一个CNN的重要概念就是最大池化,这是非线性下采样的一种形式。最大池化是将输入图像划分成一个非重叠矩阵集合,然后对于每个子区域,输出他们的最大值。
最大池化在视觉中很有用是基于以下两个原因:
-
通过消除非最大值, 减少了上层的计算量.
-
提供了一种平移不变性的形式. 想象下一个卷积层级联着一层最大池化层。对于一个单一的像素来说它有8个方向可以平移,如果是在一个2×2区域上使用最大池化,那么这8个可能的组合中的3个将会在卷积层上生成一样的输出,对于基于3×3的窗口上的最大池化来说,它达到了5/8(就是原来是3/8)。因为它提供额外的位置上的鲁棒性,最大池化是一种“明智”的方式来减少中间表征的维度。
一个例子胜过千言万语:
from theano.tensor.signal import downsampleinput = T.dtensor4('input')maxpool_shape = (2, 2)pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True)f = theano.function([input],pool_out)invals = numpy.random.RandomState(1).rand(3, 2, 5, 5)print 'With ignore_border set to True:'print 'invals[0, 0, :, :] =\n', invals[0, 0, :, :]print 'output[0, 0, :, :] =\n', f(invals)[0, 0, :, :]pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=False)f = theano.function([input],pool_out)print 'With ignore_border set to False:'print 'invals[1, 0, :, :] =\n ', invals[1, 0, :, :]print 'output[1, 0, :, :] =\n ', f(invals)[1, 0, :, :]This should generate the following output:With ignore_border set to True: invals[0, 0, :, :] = [[ 4.17022005e-01 7.20324493e-01 1.14374817e-04 3.02332573e-01 1.46755891e-01] [ 9.23385948e-02 1.86260211e-01 3.45560727e-01 3.96767474e-01 5.38816734e-01] [ 4.19194514e-01 6.85219500e-01 2.04452250e-01 8.78117436e-01 2.73875932e-02] [ 6.70467510e-01 4.17304802e-01 5.58689828e-01 1.40386939e-01 1.98101489e-01] [ 8.00744569e-01 9.68261576e-01 3.13424178e-01 6.92322616e-01 8.76389152e-01]] output[0, 0, :, :] = [[ 0.72032449 0.39676747] [ 0.6852195 0.87811744]]With ignore_border set to False: invals[1, 0, :, :] = [[ 0.01936696 0.67883553 0.21162812 0.26554666 0.49157316] [ 0.05336255 0.57411761 0.14672857 0.58930554 0.69975836] [ 0.10233443 0.41405599 0.69440016 0.41417927 0.04995346] [ 0.53589641 0.66379465 0.51488911 0.94459476 0.58655504] [ 0.90340192 0.1374747 0.13927635 0.80739129 0.39767684]] output[1, 0, :, :] = [[ 0.67883553 0.58930554 0.69975836] [ 0.66379465 0.94459476 0.58655504] [ 0.90340192 0.80739129 0.39767684]]注意到和大多数theano代码相比,max_pool_2d操作有一点特别。他需要一个缩小因子 ds (长度为2的元组,包含图像宽度和高度的缩小因子)在graph建立的时候需要知道的。这在将来也许会改变(也就是10年的这个theano和今年15年的这个函数有可能不一样,要注意)。
七、完整的模型:LeNet
稀疏、卷积层和最大池化是LeNet模型家族的核心。不过这些模型的详细细节还是变化很大的,下图显示了一个LeNet模型的示意图:
低层都是有交替的卷积和最大池化层构成的。高层是全连接层,对应着一个传统的MLP(隐藏层+逻辑回归)。输入到第一个全连接层的是低层的所有特征图的集合。
从一个实现的角度来看,这意味着低层是在4D张量上操作的,然后平铺成一个2D矩阵栅格特征图,用来兼容之前的MLP实现。
八、把上面的合并到一起
我们现在有了所有需要的。先来构建一个LeNetConvPoolLayer 类,用来实现{卷积+最大池化}层:
class LeNetConvPoolLayer(object): """Pool Layer of a convolutional network """ def __init__(self, rng, input, filter_shape, image_shape, poolsize=(2, 2)): """ Allocate a LeNetConvPoolLayer with shared variable internal parameters. :type rng: numpy.random.RandomState :param rng: a random number generator used to initialize weights :type input: theano.tensor.dtensor4 :param input: symbolic image tensor, of shape image_shape :type filter_shape: tuple or list of length 4 :param filter_shape: (number of filters, num input feature maps, filter height, filter width) :type image_shape: tuple or list of length 4 :param image_shape: (batch size, num input feature maps, image height, image width) :type poolsize: tuple or list of length 2 :param poolsize: the downsampling (pooling) factor (#rows, #cols) """ assert image_shape[1] == filter_shape[1] self.input = input # there are "num input feature maps * filter height * filter width" # inputs to each hidden unit fan_in = numpy.prod(filter_shape[1:]) # each unit in the lower layer receives a gradient from: # "num output feature maps * filter height * filter width" / # pooling size fan_out = (filter_shape[0] * numpy.prod(filter_shape[2:]) / numpy.prod(poolsize)) # initialize weights with random weights W_bound = numpy.sqrt(6. / (fan_in + fan_out)) self.W = theano.shared( numpy.asarray( rng.uniform(low=-W_bound, high=W_bound, size=filter_shape), dtype=theano.config.floatX ), borrow=True ) # the bias is a 1D tensor -- one bias per output feature map b_values = numpy.zeros((filter_shape[0],), dtype=theano.config.floatX) self.b = theano.shared(value=b_values, borrow=True) # convolve input feature maps with filters conv_out = conv.conv2d( input=input, filters=self.W, filter_shape=filter_shape, image_shape=image_shape ) # downsample each feature map individually, using maxpooling pooled_out = downsample.max_pool_2d( input=conv_out, ds=poolsize, ignore_border=True ) # add the bias term. Since the bias is a vector (1D array), we first # reshape it to a tensor of shape (1, n_filters, 1, 1). Each bias will # thus be broadcasted across mini-batches and feature map # width & height self.output = T.tanh(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x')) # store parameters of this layer self.params = [self.W, self.b]注意到当初始化权重值的时候,fan-in是由感受野的size和输入特征图的个数决定的。
最后,使用在()中的LogisticRegression 类和()中定义的HiddenLayer 类,我们可以如下来实例化:
x = T.matrix('x') # the data is presented as rasterized images y = T.ivector('y') # the labels are presented as 1D vector of # [int] labels ###################### # BUILD ACTUAL MODEL # ###################### print '... building the model' # Reshape matrix of rasterized images of shape (batch_size, 28 * 28) # to a 4D tensor, compatible with our LeNetConvPoolLayer # (28, 28) is the size of MNIST images. layer0_input = x.reshape((batch_size, 1, 28, 28)) # Construct the first convolutional pooling layer: # filtering reduces the image size to (28-5+1 , 28-5+1) = (24, 24) # maxpooling reduces this further to (24/2, 24/2) = (12, 12) # 4D output tensor is thus of shape (batch_size, nkerns[0], 12, 12) layer0 = LeNetConvPoolLayer( rng, input=layer0_input, image_shape=(batch_size, 1, 28, 28), filter_shape=(nkerns[0], 1, 5, 5), poolsize=(2, 2) ) # Construct the second convolutional pooling layer # filtering reduces the image size to (12-5+1, 12-5+1) = (8, 8) # maxpooling reduces this further to (8/2, 8/2) = (4, 4) # 4D output tensor is thus of shape (batch_size, nkerns[1], 4, 4) layer1 = LeNetConvPoolLayer( rng, input=layer0.output, image_shape=(batch_size, nkerns[0], 12, 12), filter_shape=(nkerns[1], nkerns[0], 5, 5), poolsize=(2, 2) ) # the HiddenLayer being fully-connected, it operates on 2D matrices of # shape (batch_size, num_pixels) (i.e matrix of rasterized images). # This will generate a matrix of shape (batch_size, nkerns[1] * 4 * 4), # or (500, 50 * 4 * 4) = (500, 800) with the default values. layer2_input = layer1.output.flatten(2) # construct a fully-connected sigmoidal layer layer2 = HiddenLayer( rng, input=layer2_input, n_in=nkerns[1] * 4 * 4, n_out=500, activation=T.tanh ) # classify the values of the fully-connected sigmoidal layer layer3 = LogisticRegression(input=layer2.output, n_in=500, n_out=10) # the cost we minimize during training is the NLL of the model cost = layer3.negative_log_likelihood(y) # create a function to compute the mistakes that are made by the model test_model = theano.function( [index], layer3.errors(y), givens={ x: test_set_x[index * batch_size: (index + 1) * batch_size], y: test_set_y[index * batch_size: (index + 1) * batch_size] } ) validate_model = theano.function( [index], layer3.errors(y), givens={ x: valid_set_x[index * batch_size: (index + 1) * batch_size], y: valid_set_y[index * batch_size: (index + 1) * batch_size] } ) # create a list of all model parameters to be fit by gradient descent params = layer3.params + layer2.params + layer1.params + layer0.params # create a list of gradients for all model parameters grads = T.grad(cost, params) # train_model is a function that updates the model parameters by # SGD Since this model has many parameters, it would be tedious to # manually create an update rule for each model parameter. We thus # create the updates list by automatically looping over all # (params[i], grads[i]) pairs. updates = [ (param_i, param_i - learning_rate * grad_i) for param_i, grad_i in zip(params, grads) ] train_model = theano.function( [index], cost, updates=updates, givens={ x: train_set_x[index * batch_size: (index + 1) * batch_size], y: train_set_y[index * batch_size: (index + 1) * batch_size] } )这里没有实际训练和早期停止的代码,因为它实际上和MLP的一样。感兴趣的读者可以访问DeepLearningTutorials中“code”这个文件夹。
九、运行该代码
用户可以如下形式运行该代码:
python code/convolutional_mlp.py接下来的输出可以在 Core i7-2600K CPU clocked at 3.40GHz上使用默认参数和 flags ‘floatX=float32’:来得到:
Optimization complete.Best validation score of 0.910000 % obtained at iteration 17800,with testperformance 0.920000 %The code for file convolutional_mlp.py ran for 380.28m使用GeForce GTX 285,得到如下结果:
Optimization complete.Best validation score of 0.910000 % obtained at iteration 15500,with testperformance 0.930000 %The code for file convolutional_mlp.py ran for 46.76m使用GeForce GTX 480的结果:
Optimization complete.Best validation score of 0.910000 % obtained at iteration 16400,with testperformance 0.930000 %The code for file convolutional_mlp.py ran for 32.52m
注意到在验证的时候和测试时候误差的差异(迭代的次数),这是因为硬件中舍入机制的不同实现造成的。这可以被忽略掉,不用管。
在win7_64bit+cuda6.5_64bit+anaconda2.1.0_64bit+gtx 780ti,结果:
(不知道为什么时间反而多了,原因待分析)。
十、提示和技巧
选择超参数
CNN训练的时候特别需要技巧,因为它们相比一个标准的MLP来说有着更多的超参数。不过通常的学习率和正则化约束的经验规则还是适用的,接下来就是在优化CNN的时候需要记住的。
过滤器的数量
当选择每一层的过滤器的个数的时候,记得一个单一的卷积过滤器的激活值的计算比传统的MLPs代价更高昂。
假设层包含着个特征图和个像素位置(即,位置个数乘以特征图个数),在形状为的层 上有 个过滤器。然后计算一个特征图(在所有过滤器能够使用的 个像素位置上使用一个 的过滤器)的代价为 。总的cost是 乘以这个值。如果在同一层的所有的特征不是连接到之前一层的所有特征,那么事情就会变得更加的复杂。
对于一个标准的MLP,cost将会只是 ,这里 在层上有个不同的神经元。同样的,在CNNs中使用的过滤器的个数通常要小于在MLPs中隐藏单元的个数,并且依赖于特征图的size(它本身的一个输入图像的size和过滤器形状的函数)
因为特征图的size随着深度的增加而下降,靠近输入层的层会有着更少的过滤器而更高层就会有更多的过滤器。事实上,为了在每一层中平衡下计算量,这些特征个数和像素位置个数的乘积通常在层之间是差不多保持稳定的。为了保留有关输入的信息,将会需要维持激活的总数(特征图的数量乘以像素位置数量)来使得从这一层到下一层的时候没有减少(当然,我们希望在做有监督训练的时候没有变得更少)。特征图的个数直接控制着能力(capacity),同样依赖于可利用的样本的个数和任务的复杂程度。
过滤器的形状
一般过滤器形状在文献中变化万千,通常是基于特定的数据集的。在MNIST-sized 图像(28×28)这样的上,最好的结果通常是第一层上有着5×5的过滤器大小,同时对于自然图像数据集(通常在每一维上有着上百个像素)倾向于第一层使用更大的过滤器,例如12×12或者15×15。
所以这里的技巧就是在给定的数据集的基础上,去找到正确的“粒度”(即,过滤器形状),从而能够在合适的尺寸下生成好的抽象表征。
最大池化的形状
通常来说值为2×2或者没有最大池化操作。非常大的输入图像也许在低层上会有着4×4的池化。不过记得,这会以因子为16来减少信号的维度,同时也许会导致丢失过多的信息。
脚注
[1] 更清晰的说,我们使用“unit”或者“neuron”来表示人工神经元,“cell”来表示生物神经元。
提示
如果你想在一个新的数据集上使用这个模型,这里有一些提示也许有助于你生成更好的结果:
- 对数据进行白化 (e.g. with PCA)
- 在每个epoch上衰减学习率
参考资料:
[1] 官网:http://deeplearning.net/tutorial/lenet.html#lenet
[2] Deep learning with Theano 官方中文教程(翻译)(四) 卷积神经网络(CNN):http://www.cnblogs.com/charleshuang/p/3651843.html