theano學習指南4(翻譯)- 卷積神經網絡

原文鏈接 http://www.cnblogs.com/xueliangliu/archive/2013/06/09/3127197.html

動機

卷積神經網絡是一種特殊的MLP,這個概念是從生物裏面演化過來的. 根據Hubel和Wiesel早期在貓的視覺皮層上的工作 [Hubel68], 我們知道在視覺皮層上面存在一種細胞的複雜分佈,這些細胞對一些局部輸入是很敏感的,它們被成爲感知野, 並通過這種特殊的組合方式來覆蓋整個視野. 這些過濾器對輸入空間是局部敏感的,因此能夠更好得發覺自然圖像中不同物體的空間相關性.

進一步講, 視覺皮層存在兩類不同的細胞,簡單細胞S和複雜細胞C. 簡單細胞儘可能得可視野中特殊的類似邊緣這種結構進行相應.複雜細胞具有更大的感知範圍,它們可以對刺激的空間位置進行精確的定位.

作爲已知的最強大的視覺系統,視覺皮層也成爲了科學研究的對象. 很多神經科學中提出的模型,都是基於對其進行的研究,比如, NeoCognitron [Fukushima], HMAX [Serre07] 以及本文討論的重點 LeNet-5 [LeCun98]

稀疏連接性

CNN通過增強相鄰兩層中神經元的局部的連接來發掘局部空間相關性. m層的隱輸入單元和m-1層的一部分空間相鄰,並具有連續可視野的神經元相連接. 它們的關係如下圖所示:

 _images/sparse_1D_nn.png
我們可以假設m-1層爲輸入視網膜, 在它之上,m層的視覺神經元具有寬度爲3的可視野,因此一個單元可以連接視網膜層的三個相鄰的神經元. m層的神經元和m-1層具有類似的連接屬性. 因此m+1層的神經元對於m層,仍具有寬度爲3的可視野,但是相對於m-1層,可視野的寬度更大(結果爲5). 這種結構把訓練好的過濾器構建成一種局部空間模式. 如上圖所示, 過濾器由多個感知層堆積而成,它變得更加地全局. 比如,m+1層的一個神經元可以對m-1層的寬度爲5的特徵進行編碼.

共享權重

在CNN中,每一個稀疏的過濾器hihi在整個可視野上是疊加的重複的. 這些重複的單元形成了一種特徵圖,它可以共享相同的參數,比如共同的權向量和偏差.

_images/conv_1D_nn.png

上圖中, 屬於同一個的特徵圖的三個隱單元,因爲需要共享相同顏色的權重, 他們的被限制成相同的. 梯度下降算法,在進行了一個輕微的改動之後, 仍然可以用來學習這些共享的參數.  共享權重的梯度可以對共享參數的梯度進行簡單的求和得到.

爲什麼要對共享權重感興趣呢? 在這種方式中,重複單元可以檢測特徵,無論他們在可視野中的位置在什麼地方. 而權重的共享爲此提供了一種非常有效的方法, 因爲這樣做可以在很大程度上減少需要學習的參數. 通過控制模型的容量,CNN在視覺問題上達到了更好的泛化.

具體細節

從概念上講,特徵圖通過對輸入圖像在一個線性濾波器上的卷積運算,增加一個便宜量,在結果上作用一個非線性函數得到.如果我們把某層的第k個的特徵圖記爲hkhk,其過濾器由權重WW和偏移量bkbk決定, 那麼,特徵圖可以通過下面的函數得到:

hkij=tanh((Wkx)ij+bk).hijk=tanh⁡((Wk∗x)ij+bk).

爲了更好的表達數據, 隱層由一系列的多個特徵圖構成h(k),k=0..Kh(k),k=0..K. 其權重WW由四個參數決定: 目標特徵圖的索引,源特徵圖的索引,源水平位置索引和源垂直位置索引. 偏移量爲一個向量,其中每一個元素對應目標特徵圖的一個索引. 其邏輯關係通過下圖表示: 

_images/cnn_explained.png

Figure 1: 卷積層實例 (這個圖和下面的說明有點衝突,下面的特徵權重表示成了W0W0,W1W1,圖中是 W1W1,W2W2)

這裏是一個兩層的CNN,它有 m-1層的四個特徵圖和m層的兩個特徵圖(h0,h1h0,h1)構成. 神經元在h0h0h1h1的輸出(藍色和紅色的框所示)是由m-1層落入其相應的2*2的可視野的像素計算得到, 這裏需要注意可視野如何地跨四個特徵圖.其權重爲3D張量,分別表示了輸入特徵圖的索引,以及像素的座標.

整合以上概念, WklijWijkl表示了連接m層第k個特徵圖的特徵圖上每一個像素的權重, 像素爲m-1層的第l個特徵圖,其位置爲 (i,j)(i,j)

ConvOp

Convop是Theano中實現卷積的函數, 它主要重複了scipy工具包中signal.convolve2d的函數功能. 總的來講,ConvOp包含兩個參數:

  • 對應輸入圖像的mini-batch的4D張量. 其每個張量的大小爲:[mini-batch的大小, 輸入的特徵圖的數量, 圖像的高度,圖像的寬度]
  • 對應權重矩陣WW的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()
複製代碼

 

運行代碼,可以得到如下結果:

_images/3wolfmoon_output.png

我們可以注意到,隨機初始化的濾波器能夠產生邊緣檢測算子的作用。另外,我們用和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), 和一個縮放因子來對這個張量進行共用最大化的變換。下面的例子說明了這個過程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from theano.tensor.signal import downsample
 
input = T.dtensor4('input')
maxpool_shape = (22)
pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=True)
= theano.function([input],pool_out)
 
invals = numpy.random.RandomState(1).rand(3255)
print 'With ignore_border set to True:'
print 'invals[0, 0, :, :] =\n', invals[00, :, :]
print 'output[0, 0, :, :] =\n', f(invals)[00, :, :]
 
pool_out = downsample.max_pool_2d(input, maxpool_shape, ignore_border=False)
= theano.function([input],pool_out)
print 'With ignore_border set to False:'
print 'invals[1, 0, :, :] =\n ', invals[10, :, :]
print 'output[1, 0, :, :] =\n ', f(invals)[10, :, :]

  這段代碼的輸出爲類似下面的內容:

複製代碼
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的模型。

_images/mylenet.png

模型的低層由卷積和共有最大化層組成,高層是全連接的一個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

結果中的細小差別來自於不同硬件下不同的圓整機制,這些差別可以忽


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章