卷积神经网络(LeNet)

注意:当GPU和显示器相连时,对每个GPU函数的运行有几秒的限制,因为当GPU进行计算时无法与显示器相连。如果不做限制,显示冻结较长时间而使人误以为是电脑死机。可以通过降低批次规模来解决超时问题。

诱因

卷积神经网络(CNN)是MLP的仿生变异。从Hubel and Wiesel早期关于猫的视觉皮质研究,我们知道视觉皮质由一系列细胞通过复杂的排列组成。这些细胞对于视觉域的小块亚区域敏感,称为接收域。这些亚区域通过排列以覆盖整个视觉域。这些细胞像作用于输入空间的本地过滤器,擅长处理自然图像的本地空间相关性。

此外,我们已经识别了两类基本细胞:对于接收域中的特定边缘图形反应最强的一般细胞和有更大接收域并对图形具体位置具有本地抗干扰性的复杂细胞。

动物的视觉皮质是现存最强大的视觉处理系统,自然的我们会模仿它的机理。也因此在文献中能找到很多仿生模型的阐述。例如 theNeoCognitron , HMAX[Serre07] and LeNet-5[LeCun98],这里我们关注最后一个模型。

稀疏连接

CNN通过执行神经元和相邻层的本地连接来探寻本地空间的相关性。换句话说m层隐藏单元的输入,是来自于m-1层的子集单元,这些单元有共同的空间接收域。我们用下图示例:


将m-1层想象成输入虹膜,在上图中,m层的单元是输入虹膜中宽度为3的接收域,因此只和虹膜中3个相邻神经元连接。m+1层中的单元和下层有相似的连接,它们与下层的接收域依然是3,但相对于输入虹膜的接收域是5。每个单元对于虹膜接收域之外的变动无反应。因此保证了学习到的过滤器对本地空间输入模式有最强的反应。

但如上图所述,通过层堆叠使得过滤器具有非线性并全局化(对更大范围的像素空间有反应)。例如m+1隐藏层中的单元能够为宽度为5(从像素空间的角度)的非线性特征编码。

共享权重

此外CNN中,每个过滤器

在整个视觉域中重复。这些重复的单元分享相同的参数(权重向量和偏差)并组成一个特征图。

上图中,3个隐藏单元同属一个特征图。相同颜色的权重被共享并且一致。只要对原算法稍作改变就可以使用梯度下降来学习共享参数。共享权重的梯度是被共享参数梯度之和。

以这种方式重复单元使得特征不论其在视觉域中的位置都可以被检测到。此外,共享权重通过大量减少学习到的自由参数的数量来提高学习效率。对于模型的约束使得CNN对于视觉问题具有更好的泛化性。

细节和标识

特征图可以通过重复应用一个遍览整体图像不同区域的函数得到,换句话说,通过使用一个线性过滤器卷积输入图像,加上一个偏差项后使用非线性函数。如果一个给定层的第k个特征图记

,其权重

偏差

,那么特征图

可以通过以下公式得到:

注意:对于1D信号的卷积可以定义为:

对于2D可以扩展为:

为更好的体现数据,每一隐藏层由多个特征图构成

隐藏层的权重W可以一个4D张量来表示,包括目标特征图,原特征图,原纵向位置,原横向位置。偏差b可以向量表示,每个原色代表一个目标特征图,示意图如下:

该图展示了两层CNN。m-1层包含4个特征图,隐藏层m包含2个特征图。像素(神经元输出)


(图中蓝色和红色方框)由m-1层中像素计算得出,在下层2*2接收域中(彩色方框)。注意接收域跨越所有4个输入特征图。


的权重


是3D权重张量。第一个纬度索引代表输入特征,另两个指代像素坐标。放在一起

代表了连接m层k个特征图每个像素的权重,该像素在m-1层的第I个特征图的坐标(i, j)。

卷积操作

ConvOp是Theano中实现卷积层的主要负荷器,被theano.tensor.signal.conv2d使用,接受两个象征输入:

一个与输入图像微批次对应的4D张量,包括[微批次大小,输入特征图数量,图像高度,图像宽度]。

一个与权重矩阵W对应的4D张量,包括[m层特征图的数量,m-1层特征图的数量,过滤器高度,过滤器宽度]。

以下是实现卷积层的代码,输入包括了3个120*160的特征图(RGB彩色图像)。我们使用2个接收域为9*9的卷积过滤器。


import theano
from theano import tensor as T
from theano.tensor.nnet import conv2d

import numpy

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 = 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)


我们让它变得更有意思


import numpy
import pylab
from PIL import Image

# open random image of dimensions 639x516
img = 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 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来说是下一层的单元数量。而对CNN来说我们要考虑输入特征图的数量和接收域的大小。

最大池化

CNN另一个重要概念是最大池化,这是一种非线性下取样的形式。最大池化把输入图分割成一组互相不重叠的四边形,并对每一个次区域取最大值。

最大池化的有效性基于以下两点原因:

1、通过去除非最大值,降低了上层的计算。

2、 提供了转化恒定性的一种方式。想象一下将一个卷积层和一个最大池化层串联起来。通过一个像素转化输入图像有8个方向。如果在2*2的区域进行最大池化,8个里有3个设置会在卷积层产生相同的输出,如果在3*3窗口最大池化,则8个里有5个会如此。考虑到它为位置提供了额外的抗干扰性,最大池化是一种减少中间体现维度的聪明方式。

最大池化在Theano中通过theano.tensor.signal.pool.pool_2d来完成。这个函数将输入作为一个N维张量(N>=2)和一个下降因此,并对张量后2个维度进行最大池化操作。

举例如下:


from theano.tensor.signal import pool

input = T.dtensor4('input')
maxpool_shape = (2, 2)
pool_out = pool.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 = pool.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]]


注意和大多数Theano代码相比,max_pool_2d操作略有不同。它要求下降因子ds(长度为2的图像宽度和高度的下降因子元组) 在构建图时应已知。这点可能在将来改变。

完整模型:LeNet

稀疏性、卷积层和最大池化是LeNet家族模型的核心。尽快具体模型细节相差万千,下图显示了LeNet的基本结构。


下面的层由卷积和最大池化交替构成,上面的层则全连接,对应传统的MLP(隐藏层+ 逻辑回归)。第一个全连接层的输入是下面层的特征图组。

从执行的角度,这意味着下面的层在4D张量上操作,然后通过点阵化特征图平铺为2D矩阵,以和此前MLP执行相适应。

完整代码

我们现在已经掌握了使用Theano实现一个LeNet模型的全部知识。我们从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 = conv2d(
            input=input,
            filters=self.W,
            filter_shape=filter_shape,
            input_shape=image_shape
        )

        # pool each feature map individually, using maxpooling
        pooled_out = pool.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]

        # keep track of model input
        self.input = input


注意我们初始化权重值时,fan-in由接收域的大小和输入特征图的数量决定。

最后我们使用此前学到的LogisticRregresson类和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中的完全一致,我们这里不再重复。

执行代码

可以通过执行如下代码运行程序


python code/convolutional_mlp.py


在Core i7-2600KCPU clocked at 3.40GHz and using flags ‘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


在GeForce GTX 480


Optimization complete.
Best validation score of 0.910000 % obtained at iteration 16400,with test
performance 0.930000 %
The code for file convolutional_mlp.py ran for 32.52m


技巧

选择超参数

CNN相比标准MLP加入了更多的超参数,因此训练更为困难。虽然对于学习速率和正则化的黄金法则依然成立,在优化CNN时要记住以下几点。

过滤器的数量

选择每层过滤器数量时,记住计算单个卷积过滤器的激活要比传统MLP更昂贵。

假设

层包含

特征图和

像素位置(位置数量乘以特征图),并且在

层有


过滤器。那么计算一个特征图(将m*n过滤器执行于

像素位置)成本为

。总成本再乘以

。如果一层所有特征不是都与前一层所有特征连接,那么就会更为复杂。 对于标准MLP,对于

层的

个神经元成本为

。因此在CNN中使用过滤器的数量一般小于MLP中使用隐藏单元的数量并依赖于特征图(输入图象大小和过滤器形状的函数)的大小。

考虑到特征图的大小随深度减小,接近输入层的层过滤器较少而越往上越多。实际上,为均衡每层的计算,特征数量和像素位置的乘积一般力求每层相同。保存信息要求使激活的总数量(特征图的数量乘以像素位置)在层间流转时不减少。特征图数量直接控制理解能力因此依赖于样本的数量和任务的复杂度。

过滤器形状

文献中过滤器形状差别万千,一般依赖于数据。对于MNIST大小的图像(28*28)最好的结果是第一层用5*5,而对于自然图像集(一般每个维度有数百个像素)则倾向于使用更大的12*12或15*15作为第一层过滤器。

最大池化形状

典型值为2*2或不使用最大池化。非常大的输入图象可考虑在低层使用4*4。但记住这可能以16倍减少信号纬度从而导致损失太多信息。

技巧

如果要在新的数据集上使用该模型,可考虑使用以下技巧取得更好的效果。

*白化数据(例如使用PCA)

*每次训练降低学习速率

注:我们使用单元或神经元来指代人工神经元,使用细胞来指代生物神经元。