Convolution Neural Networks(LeNet)
开学了,就少了时间去看一些杂七杂八的东西。
能全心的学一些东西果然整个人都好起来的,熬夜都写下去。
希望自己以后再多点时间来学学这学学那,
毕竟开学了课程还是蛮重的。 T - T
Motivation 动机
卷积神经网络(CNN)是MLPS的一个仿生学变种。在Hubel和Wiesel对猫神经的早期工作中,我们知道视觉大脑皮城包含一系列复杂的神经细胞。这些细胞是视野中敏感的小区域,叫做感受野(receptive field)。子区域被拼接以覆盖整个视野。这些细胞就像是输入的局部过滤器而且很恰当地利用自然图像中的强空间局部相关性。
额外地,有两种基础的细胞类型:简单细胞在他们的感受野最大地接受具体的边缘模式;复杂的细胞有更大的感受野,是局部不变的模式确切位置。
动物的视觉大脑皮层是最厉害的视觉处理系统,模仿它的行为是很自然的想法。因此,很多仿生学模型可以在论文中被找到。下面列出其中一些:the NeoCognitron,HMAX和LeNet-5,这是我们本次教程所关注的。
Sparse Connectivity 稀疏链接
CNNs通过使用一个相邻层的局部链接模式开拓了局部稀疏链接。换句话说,第m层隐藏层单元的输入是第m-1层神经元的子集,神经元在空间上有相邻的感受野。我们可以用下面的图片来表示:
想象一下第m-1层是输入的视网膜。在上面的图片中,第m层神经元在视网膜中有宽度为3的感受野,因此它只和视网膜中的3个相邻神经元连接。第m+1层的神经元也有在它下面的层有同样的链接。我们可以说他们对于下层的感受野是3,但是他们对于输入的感受野更大(5)。每个神经元响应于它的感受野以外的视网膜变化。这样的设计因此保证学习到的过滤器(filters)能对一个最强的局部稀疏的输入模式响应。
就如所说的那样,堆叠许多这样的层导致一个(非线性的)的增长式全局(“global”)过滤器(filters)(例如,响应一个大区域的像素空间)。例如第m+1层可以解码一个非线性的宽度为5的特征。
Shared Weights 共享权值
在CNNs,每个过滤器
在整个视野中是被复制的。这些复制的神经元共享相同的参数(权值向量和偏置项)并且组成了一个特征图(feature map)。
在上图中,我们展示了3个属于相同特征图的隐藏神经元。相同颜色的权值是共享的-约束是相同的。梯度下降仍然可以被使用来学习这样的共享参数,只需要一些在原来的算法进行小小的改动。共享权值的梯度是简单的共享权值梯度的求和。
这种方法下复制的神经元允许特征被检测而不用管它在视野中的位置。另外,权值共享通过大幅较少需要学习的参数数量来增加学习效率。这些结构使得CNNs在视觉问题上有很大的泛化能力。
Detials and Notation 细节和注意
- 一个特征图通过把一个函数重复在整幅图的的子区域间应用来获得,换句话说,通过对输入图像用一个线性过滤器进行卷积,加上一个偏置项然后使用一个非线性的函数。如果我们表示给定层的第k个特征图为
,它的权值
和
,然后特征图
公式如下(tanh非线性):
注意:一维信号的卷积定义:。扩展到二维
为了更丰富地表达数据,每个隐藏层有多个特征图组成,
.一个隐藏层权重
W
可以在一个由每个目标特征图、源特征图、源垂直分量和源水平分量组合的4维张量里表示。偏置项b
可以被表示为一个包括每个特征图的一个元素的一维向量。我们用下面的图来说明:
这幅图表示了一个CNN中的两个层。m-1层包括四个特征图。隐藏层包括两个特征图(和
)。在h0和h1(用红色和蓝色强调)的像素(神经元输出)从m-1层的2*2的感受野(染色的矩阵)像素计算得到。注意感受野是怎样接受所有四个输入特征图的。权重h0和h1的权重W0和W1因此是3维权重张量。最先的维度代表输入的特征图,另外两个代表像素关系。
把所有放在一起,
表示在m层每个k特征图相连的权重,像素是m-1层l特征图的坐标(i, j)。
The Convolution Operator 卷积运算
ConvOp是在Theano中实现一个卷积层的主力。ConvOp要传进两个输入给
theano.tensor.signal.conv2d
:1. 一个和小批量输入图像相关的四维张量。张量的模型如下:[小批量形状,输入特征图的数量,图片的高,图片的宽]。 2. 一个权重矩阵W相关的四维张量。张量的形状如下:[m层特征图的数量,m-1层特征图的数量,过滤器高,过滤器宽]下面是Theano的代码实验一个图1的卷积层。输入是3个特征图(一个RGB图),大小是120x160.我们使用两个过滤器9x9感受野。
import theano
from theano import tensor as T
from theano.tensor.nnet import conv2d
import numpy
# 初始化4维输入张量
input = T.tensor4(name='input')
# 初始化共享权值
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')
# 随机初始化共享变量偏置项(1维张量)
# 重要:偏置项通常初始化为0。但是在本次练习中,我们简单
# 应用卷积层到一张图片而不去学习它的参数。因此我们初始化
# 他们模拟学习
b_shp =(2,)
b = theano.shared(numpy.asarray(
rng.uniform(low=-.5, high=.5, size=b_shp),
dtype=input.dtype), name='b')
# 建立符号表达式计算输入和W过滤器的卷积
conv_out = conv2d(input, W)
# 建立符号表达式增加偏置项和应用激活函数,例如,生成神经网络层输出
# 原文说dimshuffle是很强大的reshape工具
output = T.nnet.sigmoid(conv_out + b.dimshuffle('x', 0, 'x', 'x'))
# 建立函数编译theano函数
f = theano.function([input], output)
- 让我们来玩点东西
import numpy
import pylab
from PIL import Image
# 打开一个随机维度为639*516的图像
img = Image.open(open('doc/images/3wolfmoon.jpg))
# 维度是(高, 宽, 通道)
img = numpy.asarray(img, dtype='float64') / 256.
# 使图像变成4维张量(1, 3, 高, 宽)
img_ = img.transpose(2, 0, 1).reshape(1, 3, 639, 516)
# 绘制原始图像和输出的第一第二组成
pylab.subplot(1, 3, 1);pylab.axis('off');pylab.imshow(img);pylab.gray()
# 卷积运算的输出(过滤后的照片)其实就是一个小批量
# 这里size为1,所以我们在第一维使用索引0
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,这是下一层的单元数。但是对于CNNs,我们要考虑到输入特征图的数量和感受野的大小。
MaxPooling 最大池化
CNNs另外一个重要的概念就是最大池化,是一种非线性降采样的形式。最大池化把输入图像划分成一系列非重叠的正方形,对于每一个小区域,输出他的最大值。
最大池化在视觉上有用的两个原因:
- 通过消除非极大值,减少到上一层的计算量
- 这提供了一个平移不变性的形式。想象一个卷积层的最大池化层。一个像素点有8个移动的方向,如果使用2*2的最大池化,8个可能方向中会有3个方向产生相同的输出。一个3*3的最大池化,机率就是5/8.(不懂)
这种机制提供了定位的鲁棒性,最大池化是一个“聪明”的方法去减少中间层的维度。
最大池化在theano使用
theano.tensor.signal.pool.pook_2d
函数实现。这个函数输入一个N维的张量(N>=2),一个降阶因子,并且在一个2尾随维度的张量(2 trailing dimensions)实现池化。一个例子:
from theano.tensor.signal import pool
input = T.dtensor('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[0, 0, :, :] =\n', invals[1, 0, :, :])
print('output[0, 0, :, :] =\n', f(invals)[1, 0, :, :])
- 注意到和大多数的Theano代码相比,
max_pool_2d
操作有一点特殊。它需要一个降阶因子ds
(长度为2的元组包括图像宽度和高度的降阶因子)去知道图像构建时间。这在未来可能会改变。
The Full Model: LeNet 整个模型
稀疏,卷积层和最大池化是LeNet模型家族的核心。或许一些细节会有很大的不同,下面的图片是对LeNet模型的描绘:
较低层由卷积层和最大池化层组成。但是较高层是全链接并且和传统的MLP(隐藏层+逻辑斯特层)类似。第一个全链接层的输入是前一层所有特征图的集合。
从以上的观点来看,这意味着较底层在一个4维张量上操作。然后flattened(变平)成一个过滤器过滤后的2维矩阵,然后再与之前的MLP模型相容。
注意:
卷积有可能是不同的数学运算:
1. theano.tensor.nnet.conv_2d
,最常用于所有当下的卷积模型。这个运算中,每个输出特征图与每个输入特征图链接通过一个不同的2维过滤器,并且他们的值是所有相关过滤输入的和。
2. 在传统LeNet模型使用的卷积:每个输出特征图只和输入特征图的子集相连。
3. 在信号处理的卷积:theano.tensor.signal.conv.conv_2d
只应用于单通道输入。
这里我们使用第一个运算,所以这个模型还是与传统LeNet模型有点不同的。一个使用2的理由是,减少所需要的计算量,但是现代硬件可以使它和全链接模式一样快。另外一个原因就是在一定程度上减少自由参数的数量,但是我们有另外的正则化技术来处理。
Putting it All Together 放在一起煮
- 现在我们在Theano里有我们实现LeNet的所有部件了。让我们从LeNetPoolingLayer开始,实现一个{卷积 + 最大池化}的层
class LeNetConvPoolLayer(object):
"""卷积网络里的池化层"""
def __init__ (self, rng, inputs, filter_shape, image_shape, poolsize=(2, 2)):
"""
用共享内部权值分配一个LeNetConvPoolLayer
:type rng: numpy.random.RandomState
:param rng: 一个随机数生成器初始化权值
:type inputs: theano.tensor.dtensor4
:param inputs: 形状是image_shape的符号化图像张量
:type filter_shape: 元组或者长度为4的列表
:param filter_shape: (过滤器数量,输入特征图的数量,过滤器高度,过滤器宽度)
:type image_shape: 元组或者长度为4的列表
:param image_shape: (批量大小,输入特征图的数量,图像高度,图像宽度)
:type poolsize: 元组或者长度为2的列表
:param poolsize: 降阶(池化)因子(#行数,#列数)
"""
assert image_shape[1] == filter_shape[1]
self.inputs = inputs
# 这里有“特征图数量*过滤器高度*过滤器宽度”的输入到每一个输入层
# .prod 返回里面元素的乘积
fan_in = numpy.prod(filter_shape[1:])
# 每一个上一层的单元接受一个梯度形式:
# “输出特征图的数量 * 过滤器高度 * 过滤器宽度 / 池化形状
fan_out = (filter_shape[0] * numpy.prod(filter_shape[2:]) // numpy.prod(poolsize))
# 初始化随机权重
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)
# 一维的偏置项,一个偏置项对应一个输出特征图
b_values = numpy.zeros((filter_shape[0],), dtype=theano.config.floatX)
self.b = theano.shared(value=b_values, borrow=True)
# 用过滤器卷积输入的特征图
conv_out = conv2d(
input = inputs,
filters=self.W,
filter_shape=filter_shape,
input_shape=image_shape)
# 使用最大池化对每个特征图进行池化
pooled_out = pool.pool_2d(
input=conv_out,
ds=poolsize,
ignore_border=True)
# 添加偏置项。当偏置项是一个一维向量,我们首先把他扩展成
# (1, n_filters, 1, 1)。每个偏置项会广播到小批量和特征图的宽高
self.output = T.tanh(pooled_out + self.b.dimshuffle('x', 0, 'x', 'x'))
# 储存参数
self.params = [self.W, self.b]
# 更新输入
self.inputs = inputs
注意到当初始化权值的时候,fan-in被感受野的大小和输入特征图的数量决定
最后,使用LogisticRegression类和HiddenLayer类,我们可以实例化网络如下
x = T.matrix('x') # 数据以光栅化图片的形式表达
y = T.ivector('y') # 图片的标签
################
# 建立实际模型 #
################
print('...building the model')
# 重塑输入图像的形状,从(批量大小,28*28)
# 到一个符合LeNetConvPoolLayer的四维张量
# (28, 28)是MNIST图像的大小
layer0_input = x.reshape((batch_size, 1, 28, 28))
# 构建第一个卷积池化层:
# 过滤器使得图像变成一个(28-5+1, 28-6+1) = (24, 24)
# 最大池化使得图像减少成一个(24/2, 24/2) = (12, 12)
# 4维输出张量是一个(批量大小,nkerns[0], 12, 12)
layer0 = LeNetConvPoolLayer(
rng,
inputs=layer0_input,
image_shape=(batch_size, 1, 28, 28),
filter_shape=(nkerns[0], 1, 5, 5),
poolsize=(2, 2))
# 构建第二个卷积池化层
# 过滤器使图像减少为一个(12-5+1, 12-5+1) = (8, 8)
# 最大池化使图像减少为一个(8/2, 8/2) = (4, 4)
# 4维输出张量是一个(批量大小,nkern[1], 4, 4)
layer1 = LeNetConvPoolLayer(
rng,
inputs=layer0.output,
image_shape=(batch_size, nkerns[0], 12, 12),
filter_shape=(nkerns[1], nkerns[0], 5, 5),
poolsize=(2, 2))
# 全链接的隐藏层,作用在一个2维德矩阵,形状为
# (批量大小, 像素数目)(例如 光栅化图像矩阵)
# 这会生成一个大小为(批量大熊啊,nkern[1]*4*4)的矩阵
# 或者一个默认值为(500, 50*4*4) = (500, 800)
layer2_input = layer1.output.flatten(2)
layer2 = HiddenLayer(
rng,
inputs=layer2_input,
n_in=nkerns[1] * 4 * 4,
n_out=500,
activation=T.tanh)
# 对全链接sigmoidal层进行分类
layer3 = LogisticRegression(inputs=layer2.output, n_in=500, n_out=10)
# 我们需要最小化的损失
cost = layer3.negative_log_likelihood(y)
# 新建一个函数计算错误
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]
})
# 新建一个列表储存所有参数
params = layer3.params + layer2.params + layer1.params + layer0+params
# 新建一个列表对所有的参数求导
grads = T.grad(cost, params)
# train_model是一个函数用来通过SGD来更新模型,
# 一个个写更新会很乏味,所以我们使用一些技巧实现自动化
updates = [
(param_i, param_i - learning_rate * grad_i)
for param_i, grad_i in zip(params, grads)]
train_model = theano.function(
[index],
cost,
upates=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基本相同。
Tips and Tricks 提示和技巧
选择超参数
CNNs对训练很狡猾,他们比标准MLP加入了更多的超参数。通常的学习率和正则的技巧仍然有效,但是下面的在优化的时候也需要注意
过滤器的数量
当选择每一层过滤器的数量的时候,记住计算一个单卷积过滤器的激活项比传统的MLP的成本要大!
假设(l-1)层包括的特征图和 M x N的像素位置,并且在 m x n 的l层有
个过滤器。然后计算特征图花费(M - m) x (N - n) x m x n *
。如果不是所有同个特征跟前一个所有特征全链接,事情会更复杂。
对于传统的MLP,会花费当在l层油kl个不同的神经元。这样,CNNs中过滤器的使用往往会比MLPs中的隐藏层要小,并且依赖于特征图的大小(自身是一个输入图像和过滤器形状的函数)。
当特征图的大小随深度的增加而减少,靠近输出的层会比前面的层有更小的过滤器。实际上,为了均衡每一层的计算,特征数和像素位置数量的乘积会被设置成在每一层大致相同。为了上层的信息,输入要求保持激活项的数量(特征图数量次的像素位置数量)在一层到下一层不减少(当然我们在做监督学习时希望丢弃更小)。特征图的数量直接控制网络的能力,这当然也取决于有效的样本和任务的复杂度。过滤器的大小
在研究中过滤器的大小各有不同,通常取决于数据集。MNIST(28x28)最好的结果通常是第一层5x5,但是自然图像数据集(通常是每维上百的像素)趋向于选用12x12或15x15在第一层。
这里的技巧通常是寻找最好的“粒度”,来对正确的规模进行抽象。最大池化的大小
通常的池化是2x2或者没有池化。非常大的输入图像保证在较低层有4x4的池化。但是记住,通过使用一个16的因子会减少信号的维度,最后导致丢弃大量的信息。技巧
如果你想在新的数据集使用这个模型,这里有几个技巧可以是模型表现得更好:
白化数据(例如,PCA)
每一代减少学习率