想直接看公式的可跳至第三节 3. 公式修正
一为什么需要 SPP
首先需要知道为什么会需要 SPP
我们都知道卷积神经网络 (CNN) 由卷积层和全连接层组成, 其中卷积层对于输入数据的大小并没有要求, 唯一对数据大小有要求的则是第一个全连接层, 因此基本上所有的 CNN 都要求输入数据固定大小, 例如著名的 VGG 模型则要求输入数据大小是 (224*224)
固定输入数据大小有两个问题:
1. 很多场景所得到数据并不是固定大小的, 例如街景文字基本上其高宽比是不固定的, 如下图示红色框出的文字
2. 可能你会说可以对图片进行切割, 但是切割的话很可能会丢失到重要信息
综上, SPP 的提出就是为了解决 CNN 输入图像大小必须固定的问题, 从而可以使得输入图像高宽比和大小任意
二 SPP 原理
更加具体的原理可查阅原论文: Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
上图是原文中给出的示意图, 需要从下往上看:
首先是输入层(input image), 其大小可以是任意的
进行卷积运算, 到最后一个卷积层 (图中是 ) 输出得到该层的特征映射(feature maps), 其大小也是任意的
下面进入 SPP 层
我们先看最左边有 16 个蓝色小格子的图, 它的意思是将从 得到的特征映射分成 16 份, 另外 16X256 中的 256 表示的是 channel, 即 SPP 对每一层都分成 16 份(不一定是等比分, 原因看后面的内容就能理解了)
中间的 4 个绿色小格子和右边 1 个紫色大格子也同理, 即将特征映射分别分成 4X256 和 1X256 份
那么将特征映射分成若干等分是做什么用的呢? 我们看 SPP 的名字就是到了, 是做池化操作, 一般选择 MAX Pooling, 即对每一份进行最大池化
我们看上图, 通过 SPP 层, 特征映射被转化成了 16X256+4X256+1X256 = 21X256 的矩阵, 在送入全连接时可以扩展成一维矩阵, 即 1X10752, 所以第一个全连接层的参数就可以设置成 10752 了, 这样也就解决了输入数据大小任意的问题了
注意上面划分成多少份是可以自己是情况设置的, 例如我们也可以设置成 3X3 等, 但一般建议还是按照论文中说的的进行划分
三 SPP 公式
理论应该理解了, 那么如何实现呢? 下面将介绍论文中给出的计算公式, 但是在这之前先要介绍两种计算符号以及池化后矩阵大小的计算公式:
1. 预先知识
取整符号:
: 向下取整符号 59/60=0, 有时也用 floor() 表示
: 向上取整符号 59/60=1, 有时也用 ceil() 表示
池化后矩阵大小计算公式:
没有步长(Stride):
有步长(Stride):+1*+1
2. 公式
假设
输入数据大小是, 分别表示通道数, 高度, 宽度
池化数量:
那么则有
核 (Kernel) 大小:
步长 (Stride) 大小:
我们可以验证一下, 假设输入数据大小是, 池化数量:
那么核大小为, 步长大小为 , 得到池化后的矩阵大小的确是
3. 公式修正
是的, 论文中给出的公式的确有些疏漏, 我们还是以举例子的方式来说明
假设输入数据大小和上面一样是, 但是池化数量改为:
此时核大小为, 步长大小为 , 得到池化后的矩阵大小的确是 [简单的计算矩阵大小的方法:(7=2+1*5, 11=3+2*4)], 而不是
那么问题出在哪呢?
我们忽略了 padding 的存在(我在原论文中没有看到关于 padding 的计算公式, 如果有的话那就是我看走眼了, 麻烦提示我一下在哪个位置写过, 谢谢)
仔细看前面的计算公式我们很容易发现并没有给出 padding 的公式, 在经过 N 次使用 SPP 计算得到的结果与预期不一样以及查找各种网上资料 (尽管少得可怜) 后, 现将加入 padding 后的计算公式总结如下
: 表示核的高度
: 表示高度方向的步长
: 表示高度方向的填充数量, 需要乘以 2
注意核和步长的计算公式都使用的是 ceil(), 即向上取整, 而 padding 使用的是 floor(), 即向下取整
现在再来检验一下:
假设输入数据大小和上面一样是 , 池化数量为:
Kernel 大小为,Stride 大小为 , 所以 Padding 为
利用矩阵大小计算公式:+1*+1 得到池化后的矩阵大小为:
四代码实现(Python)
这里我使用的是 PyTorch 深度学习框架, 构建了一个 SPP 层, 代码如下:
- #coding=utf-8
- import math
- import torch
- import torch.nn.functional as F
- # 构建 SPP 层(空间金字塔池化层)
- class SPPLayer(torch.nn.Module):
- def __init__(self, num_levels, pool_type='max_pool'):
- super(SPPLayer, self).__init__()
- self.num_levels = num_levels
- self.pool_type = pool_type
- def forward(self, x):
- num, c, h, w = x.size() # num: 样本数量 c: 通道数 h: 高 w: 宽
- for i in range(self.num_levels):
- level = i+1
- kernel_size = (math.ceil(h / level), math.ceil(w / level))
- stride = (math.ceil(h / level), math.ceil(w / level))
- pooling = (math.floor((kernel_size[0]*level-h+1)/2), math.floor((kernel_size[1]*level-w+1)/2))
- # 选择池化方式
- if self.pool_type == 'max_pool':
- tensor = F.max_pool2d(x, kernel_size=kernel_size, stride=stride, padding=pooling).view(num, -1)
- else:
- tensor = F.avg_pool2d(x, kernel_size=kernel_size, stride=stride, padding=pooling).view(num, -1)
- # 展开拼接
- if (i == 0):
- x_flatten = tensor.view(num, -1)
- else:
- x_flatten = torch.cat((x_flatten, tensor.view(num, -1)), 1)
- return x_flatten
上述代码参考: sppnet-pytorch
为防止原作者将代码删除, 我已经 Fork 了, 也可以通过如下地址访问代码:
marsggbo/sppnet-pytorch
MARSGGBO 原创
2018-3-15
来源: https://juejin.im/entry/5aaa12cb6fb9a028c42ded13