动机
卷积神经网络是一种特殊的MLP,这个概念是从生物里面演化过来的. 根据Hubel和Wiesel早期在猫的视觉皮层上的工作 [Hubel68], 我们知道在视觉皮层上面存在一种细胞的复杂分布,这些细胞对一些局部输入是很敏感的,它们被成为感知野, 并通过这种特殊的组合方式来覆盖整个视野. 这些过滤器对输入空间是局部敏感的,因此能够更好得发觉自然图像中不同物体的空间相关性.
进一步讲, 视觉皮层存在两类不同的细胞,简单细胞S和复杂细胞C. 简单细胞尽可能得可视野中特殊的类似边缘这种结构进行相应.复杂细胞具有更大的感知范围,它们可以对刺激的空间位置进行精确的定位.
作为已知的最强大的视觉系统,视觉皮层也成为了科学研究的对象. 很多神经科学中提出的模型,都是基于对其进行的研究,比如, NeoCognitron [LeCun98]
稀疏连接性
CNN通过增强相邻两层中神经元的局部的连接来发掘局部空间相关性. m层的隐输入单元和m-1层的一部分空间相邻,并具有连续可视野的神经元相连接. 它们的关系如下图所示:
共享权重
在CNN中,每一个稀疏的过滤器$h_i$在整个可视野上是叠加的重复的. 这些重复的单元形成了一种特征图,它可以共享相同的参数,比如共同的权向量和偏差.
上图中, 属于同一个的特征图的三个隐单元,因为需要共享相同颜色的权重, 他们的被限制成相同的. 梯度下降算法,在进行了一个轻微的改动之后, 仍然可以用来学习这些共享的参数. 共享权重的梯度可以对共享参数的梯度进行简单的求和得到.
为什么要对共享权重感兴趣呢? 在这种方式中,重复单元可以检测特征,无论他们在可视野中的位置在什么地方. 而权重的共享为此提供了一种非常有效的方法, 因为这样做可以在很大程度上减少需要学习的参数. 通过控制模型的容量,CNN在视觉问题上达到了更好的泛化.
具体细节
从概念上讲,特征图通过对输入图像在一个线性滤波器上的卷积运算,增加一个便宜量,在结果上作用一个非线性函数得到.如果我们把某层的第k个的特征图记为$h^k$,其过滤器由权重$W$和偏移量$b_k$决定, 那么,特征图可以通过下面的函数得到:
$$h^k_{ij} = \tanh ( (W^k * x)_{ij} + b_k ).$$
为了更好的表达数据, 隐层由一系列的多个特征图构成${h^{(k)}, k= 0 .. K}$. 其权重$W$由四个参数决定: 目标特征图的索引,源特征图的索引,源水平位置索引和源垂直位置索引. 偏移量为一个向量,其中每一个元素对应目标特征图的一个索引. 其逻辑关系通过下图表示:
ConvOp
Convop是Theano中实现卷积的函数, 它主要重复了scipy工具包中signal.convolve2d的函数功能. 总的来讲,ConvOp包含两个参数:
- 对应输入图像的mini-batch的4D张量. 其每个张量的大小为:[mini-batch的大小, 输入的特征图的数量, 图像的高度,图像的宽度]
- 对应权重矩阵$W$的4D张量,其每个张量的大小为:[m层的特征图的数量,m-1层的特征图的数量,过滤器的高度,过滤器的宽度].
下面的代码实现了一个类似图1里面的卷积层. 输入图像包括大小为120*160的三个特征图(对应RGB). 我们可以用两个具有9*9的可视野的卷积过滤器.
from theano.tensor.nnet import conv rng = numpy.random.RandomState(23455) # instantiate 4D tensor for input input = 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 w conv_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 Bx1xA output = T.nnet.sigmoid(conv_out + b.dimshuffle('x', 0, 'x', 'x')) # create theano function to compute filtered images f = theano.function([input], output)
首先我们用得到的函数f做点有意思的事情.
import pylab from PIL import Image # open random image of dimensions 639x516 img = Image.open(open('images/3wolfmoon.jpg')) img = numpy.asarray(img, dtype='float64') / 256. # put image in 4D tensor of shape (1, 3, height, width) img_ = img.swapaxes(0, 2).swapaxes(1, 2).reshape(1, 3, 639, 516) filtered_img = f(img_) # plot original image and first and second components of output pylab.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是输入层到隐层单元的数量。对于MLP来说,这正是下一层的单元的数目。而对于CNNs,我们需要考虑到输入特征图的数量,以及可视野的大小。
共用最大化
CNN的另外一个重要特征是共用最大化,这其实是一种非线性向下采样的方法。共用最大化把输入图像分割成不重叠的矩形,然后对于每个矩形区域,输出最大化的结果。
这个技术在视觉上的好处主要有两个方面 (1)它降低了上层的计算复杂度 (2)它提供了一种变换不变量的。对于第二种益处,我们可以假设把一个共用最大化层和一个卷积层组合起来,对于单个像素,输入图像可以有8个方向的变换。如果共有最大层在2*2的窗口上面实现,这8个可能的配置中,有3个可以准确的产生和卷积层相同的结果。如果窗口变成3*3,则产生精确结果的概率变成了5/8.
可见,共有最大化对位置信息提供了附加的鲁棒性,它以一种非常聪明的方式减少了中间表示的维度。
在Theano中,这种技术通过函数 theano.tensor.signal.downsample.max_pool_2d 实现,这个函数的输入是一个N维张量(N>2), 和一个缩放因子来对这个张量进行共用最大化的变换。下面的例子说明了这个过程:
from theano.tensor.signal import downsample input = 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, :, :]
这段代码的输出为类似下面的内容:
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]]
注意到和大部分代码不同的是,这个函数max_pool_2d 在创建Theano图的时候,需要一个向下采样的因子ds (长度为2的tuple变量,表示了图像的宽和高的缩放. 这个可能在以后的版本中升级。
LeNet模型
稀疏,卷积层和共有最大化是LeNet的核心概念。因为模型的细节会有很大的变换,我们用下面的图来诠释LeNet的模型。
模型的低层由卷积和共有最大化层组成,高层是全连接的一个MLP 神经网络,它包含了隐层和对数回归。高层的输入是下层特征图的结合。
从实现的角度讲,这意味着低层操作了4D的张量,这个张量被压缩到了一个2D矩阵表示的光栅化的特征图上,以便于和前面的MLP的实现兼容。
综合所有
现在我们有了实现LeNet模型的所有细节,我们创建一个LeNetConvPoolLayer类,用了表示一个卷积和共有最大化层:
class LeNetConvPoolLayer(object): 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 # initialize weight values: the fan-in of each hidden neuron is # restricted by the size of the receptive fields. fan_in = numpy.prod(filter_shape[1:]) W_values = numpy.asarray(rng.uniform( low=-numpy.sqrt(3./fan_in), high=numpy.sqrt(3./fan_in), size=filter_shape), dtype=theano.config.floatX) self.W = theano.shared(value=W_values, name='W') # 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, name='b') # convolve input feature maps with filters conv_out = conv.conv2d(input, self.W, filter_shape=filter_shape, image_shape=image_shape) # downsample each feature map individually, using maxpooling pooled_out = downsample.max_pool_2d(conv_out, 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是由感知野的大小和输入特征图的数目决定的。
最后,采用前面章节定义的LogisticRegression和HiddenLayer类,LeNet就可以工作了。
class LeNetConvPoolLayer(object): 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 # initialize weight values: the fan-in of each hidden neuron is # restricted by the size of the receptive fields. fan_in = numpy.prod(filter_shape[1:]) W_values = numpy.asarray(rng.uniform( low=-numpy.sqrt(3./fan_in), high=numpy.sqrt(3./fan_in), size=filter_shape), dtype=theano.config.floatX) self.W = theano.shared(value=W_values, name='W') # 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, name='b') # convolve input feature maps with filters conv_out = conv.conv2d(input, self.W, filter_shape=filter_shape, image_shape=image_shape) # downsample each feature map individually, using maxpooling pooled_out = downsample.max_pool_2d(conv_out, 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]
这里我们忽略了具体的训练和提前结束的代码,这些代码和前面MLP里面的是完全一样的。感兴趣的读者可以查阅DeeplearningTutoirals下面code目录的代码。
运行算法
算法运行很简单,通过一个命令:
python code/convolutional_mlp.py
下面的结果为在i7-2600K CPU的机器上面,采用默认参数和‘floatX=float32’的输出
Optimization complete. Best validation score of 0.910000 % obtained at iteration 17800,with test performance 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 test performance 0.930000 % The code for file convolutional_mlp.py ran for 46.76m
结果中的细小差别来自于不同硬件下不同的圆整机制,这些差别可以忽略。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:theano学习指南4(翻译)- 卷积神经网络 - Python技术站