EUAdvancer

CS231n-卷积神经网络分类Cifar10

bg
学习了这么久的课程,终于是到了CNN了,它在图片分类上的效果确实出类拔群,而其中的卷积层,池化层也在这里揭开了面纱

卷积神经网络结构

卷积神经网络主要由3种类型的层构成:卷积层,池化层,全连接层,这些层的重叠组合就可以构成一个卷积神经网络,比如常见的卷积神经网络构成如下:INPUT -> [[CONV -> RELU] x N -> POOL?] x M -> [FC -> RELU] x K -> FC, POOL?代表池化层是可选的,通常N<=3,M>=0,K>=0,通常K<3,如INPUT -> [CONV -> RELU -> POOL] x 2 -> FC -> RELU -> FC

卷积层

卷积层是由一些可学习的滤波器(卷积核)构成的,每一个滤波器的空间大小(长宽)是比较小的,而它的深度是和输入数据相同的,前向传播过程中,每个滤波器会在输入数据上沿着长宽不断滑动(卷积),并计算它与当前窗口的内积从而生成二维激活图(特征图)。因此卷积核相当于一个特征模板,而生成的激活图则代表输入数据在不同位置该特征的激活效果。卷积层有两个比较特别的特点,分别为局部连接和权值共享

局部连接

不同于以往的全连接层,为了在高维输入数据中减小参数,卷积层的神经元只和输入数据的局部区域相连,因为我们认为距离较远的像素相关性则较弱,而人的认知一般是从局部到全局,因此我们可以在更高层的卷积层中将局部信息综合起来从而得到全局信息。因此卷积层的神经元有一个感受野,它的大小是和卷积核一样的。因此卷积层的神经元的参数就是卷积核的大小。

输出数据神经元

它涉及到了几个超参数,分别为步长(S),零填充(P)和深度(K),步长代表卷积核每次移动s的像素滑动窗口,零填充是用0在输入数据周围填充,用于控制输出数据的空间大小,深度则是表示有k个卷积核。因为我们知道卷积层的输出神经元个数其实就是K个卷积核在输入数据窗口移动的个数,因此我们得到以下公式:

  • 假设输入数据为:H x W x C, 卷积核为K x F x F x C,,则 H_new = (H - F + 2P) / S + 1,W_new = (W - F + 2P) / S + 1, D = K,即输出尺寸为(H_new, W_new, D),其中每个神经元的参数为F x F x C

权值共享

假如输入图像为 227 x 227 x 3,我们用11 x 11 x 3 x 32的卷积核,步长为4, 零填充为0,则共有55 x 55 x 32 = 96800个输出神经元,而每个神经元的参数为 11 x 11 x 3 = 363个,所以总参数为96800 x 363 = 35138400个参数,而这只是一层的参数,好像仍然太多了些吧。所以这时候我们做一个合理的假设,如果一个特征在计算某个位置有用,那么它计算不同的位置也是可用的,即图像的一部分的统计特性与其他部分是一样的,也就是说,对于同一深度的神经元(由同一卷积得到的神经元),它们使用同一个参数,这样就大大减小了参数,这时候总的参数为32 * 363 = 11616个参数

下面演示一个卷积操作的过程(动画效果可以去官网看

这里输入为5 x 5 x 3,有两个卷积核,大小为3 x 3 x 3,零填充为1,步长为2, 最终输出有两个激活图(因为有两个卷积核)。激活图的每个元素都是先通过蓝色的输入数据和红色的滤波器逐元素相乘,然后求其总和,最后加上偏差得来。由于三维难以展示,所以将三个通道分别展示出来。代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def conv_forward_naive(x, w, b, conv_params):
"""
input:
x: (N, C, H, W)
w: (D, C, HH, WW)
b: (D, )
conv_params:
{
stride: a value
pad: a value
}

H' = (H - HH + 2 * pad) / stride + 1
W' = (W - HH + 2 * pad) / stride + 1

return:
out: (N, D, H', W')
cache: (x, w, b, conv_params)
"""

stride = conv_params['stride']
pad = conv_params['pad']
N, C, H, W = x.shape
D, C, HH, WW = w.shape

X_pad = np.lib.pad(x, ((0, 0), (0, 0), (pad, pad), (pad, pad)), mode='constant')

H_new = int((H - HH + 2 * pad) / stride + 1)
W_new = int((W - WW + 2 * pad) / stride + 1)

out = np.zeros((N, D, H_new, W_new))
for n in range(N):
for d in range(D):
for hn in range(H_new):
for wn in range(W_new):
hn_index = int(hn * stride)
wn_index = int(wn * stride)
window = X_pad[n, :, hn_index:hn_index + HH, wn_index:wn_index + WW] # 这里的通道(C)是一起加的,因为卷积核的深度和输出数据是一样的
out[n, d, hn, wn] = np.sum(w[d] * window) + b[d]

cache = (x, w, b, conv_params)
return out, cache

池化层

池化层可以有效减少输入数据的空间尺寸(长宽),因此可以减小计算消耗同时防止过拟合。而它的操作也很简单,就是使用MAX操作。最常见的形式是池化层使用尺寸2x2的滤波器,以步长为2来对每个深度切片进行降采样,将其中75%的激活信息都丢掉。每个MAX操作是从4个数字中取最大值(也就是在深度切片中某个2x2的区域)。深度保持不变。

  • 假设输入数据为: H x W x C,滤波器为 F x F ,则 H_new = (H - F) / S + 1, W_new = (W - F) / S + 1, D = C, 即输出为(H_new, W_new, D),池化层没有引入参数。

以下是池化的演示

代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def max_pool_forward_naive(x, pool_params):
"""
input:
x: (N, C, H, W)
pool_params:
{
pool_width: a value
pool_height: a value
stride: a value
}

H' = (H - pool_height) / stride + 1
W' = (W - pool_width) / stride + 1

return:
out: (N, C, H', W')
cahce: (x, pool_params)
"""

N, C, H, W = x.shape
pool_width = pool_params['pool_width']
pool_height = pool_params['pool_height']
stride = pool_params['stride']

H_new = int((H - pool_height) / stride + 1)
W_new = int((W - pool_width) / stride + 1)

out = np.zeros((N, C, H_new, W_new))
for n in range(N):
for c in range(C):
for hn in range(H_new):
for wn in range(W_new):
hn_index = int(hn * stride)
wn_index = int(wn * stride)
# 这里的通道C是分开池化的,因为输出深度和输入深度相同,max操作针对每一个通道的窗口
window = x[n, c, hn_index:hn_index + pool_height, wn_index:wn_index + pool_width]
out[n, c, hn, wn] = np.max(window)

cache = (x, pool_params)
return out, cache

卷积神经网络的尺寸设计规律

卷积层的卷积核应该使用小尺寸(比如3x3或最多5x5,如果一定要用大尺寸,一般只在第一层卷积),步长为1,然后使用零填充来保证在卷积层不改变输入数据的空间尺寸(长宽),使得卷积层都能保持其输入数据的空间尺寸,池化层只负责对数据体从空间维度进行降采样。池化层一般使用2 x 2的感受野,步长为2

几个小滤波器卷积层的组合比一个大滤波器卷积层好:假设你一层一层地重叠了3个3x3的卷积层(层与层之间有非线性激活函数)。在这个排列下,第一个卷积层中的每个神经元都对输入数据体有一个3x3的视野。第二个卷积层上的神经元对第一个卷积层有一个3x3的视野,也就是对输入数据体有5x5的视野。同样,在第三个卷积层上的神经元对第二个卷积层有3x3的视野,也就是对输入数据体有7x7的视野。假设不采用这3个3x3的卷积层,二是使用一个单独的有7x7的感受野的卷积层,那么所有神经元的感受野也是7x7,但是就有一些缺点。首先,多个卷积层与非线性的激活层交替的结构,比单一卷积层的结构更能提取出深层的更好的特征。其次,假设所有的数据有C个通道,那么每个的7x7卷积核得到的输出将会包含49C个参数,而3个3x3的卷积层的组合仅有27C个参数。直观说来,最好选择带有小滤波器的卷积层组合,而不是用一个带有大的滤波器的卷积层。前者可以表达出输入数据中更多个强力特征,使用的参数也更少。唯一的不足是,在进行反向传播时,中间的卷积层可能会导致占用更多的内存。

CNN分类Cifar10模型设计

在本次分类中,我选择的模型如下:

  • Input -> ((conv->relu) x 2 ->pool->batchnorm) x 2 -> affline->relu->affline(softmax),卷积核为3 x 3,learning_rate=1e-3, weight_scales=1e-3, reg=1e-3, lr_decay=0.97, iters_per_ann=400
1
2
3
4
5
6
7
8
9
10
11
Input: [3 x 32 x 32]         memory: 3 x 32 x 32=3kb      weights=0
conv-32: [32 x 32 x 32] memory: 32 x 32 x 32=32kb weights=3 x 3 x 3 x 32=0.85kb
conv-64: [64 x 32 x 32] memory: 64 x 32 x 32=64kb weights=3 x 3 x 32 x 64 = 18kb
pool-2: [64 x 16 x 16] memory: 64 x 16 x 16=16kb weights=0
conv-128: [128 x 16 x 16] memory: 128 x 16 x 16=32kb weights=3 x 3 x 64 x 128=72kb
conv-256: [256 x 16 x 16] memory: 256 x 16 x 16=64kb weights=3 x 3 x 128 x 256=288kb
pool-2: [256 x 8 x 8] memory: 256 x 8 x 8=16kb weights=0
fc-384: [1 x 1 x 384] memory: 1 x 1 x 384=0.375kb weight=8 x 8 x 256 x 384=6M
fc-10: [1 x 1 x 10] memory: 1 x 1 x 10=0.001kb weight=10 x 384=3.75kb
TOTAL memory: 226kb * 4 bytes ~= 912kb / image (only forward! ~*2 for bwd)
TOTAL params: 6M parameters

加入归一化层可以加快训练速度,以下是我测试有和没有batchnorm的区别

将上面的模型训练10个epochs得到以下结果

在测试集上的正确率为0.7775,这次其实我没怎么调参,cnn的训练速度比较慢,每种参数的测试也是很漫长的过程,而最后定好参数训练需要大概10小时才可以完成10个epochs的训练,不过后面我还会测试几组参数对比一下效果

完整代码

代码见 github/cs231n

结语

这次总算写完了cnn,接下来我应该会停一阵子了,因为我的project的模型就是cnn,现在刚学完cnn是时候重构下代码了。