首先是输入层(input image),其大小可以是任意的
进行卷积运算,到最后一个卷积层(图中是conv5)输出得到该层的特征映射(feature maps),其大小也是任意的
下面进入SPP层
我们先看最左边有16个蓝色小格子的图,它的意思是将从conv5得到的特征映射分成16份,另外16X256中的256表示的是channel,即SPP对每一层都分成16份(不一定是等比分,原因看后面的内容就能理解了)。
中间的4个绿色小格子和右边1个紫色大格子也同理,即将特征映射分别分成4X256和1X256份
那么将特征映射分成若干等分是做什么用的呢? 我们看SPP的名字就是到了,是做池化操作,一般选择MAX Pooling,即对每一份进行最大池化。
我们看上图,通过SPP层,特征映射被转化成了16X256+4X256+1X256 = 21X256的矩阵,在送入全连接时可以扩展成一维矩阵,即1X10752,所以第一个全连接层的参数就可以设置成10752了,这样也就解决了输入数据大小任意的问题了。
注意上面划分成多少份是可以自己是情况设置的,例如我们也可以设置成3X3等,但一般建议还是按照论文中说的的进行划分。
from math import floor, ceil
import torch
import torch.nn as nn
import torch.nn.functional as F
class SpatialPyramidPooling2d(nn.Module):
def __init__(self, num_level, pool_type='max_pool'):
super(SpatialPyramidPooling2d, self).__init__()
self.num_level = num_level
self.pool_type = pool_type
def forward(self, x):
N, C, H, W = x.size()
print('多尺度提取信息,并进行特征融合...')
print()
for i in range(self.num_level):
level = i + 1
print('第',level,'次计算池化核:')
kernel_size = (ceil(H / level), ceil(W / level))
print('kernel_size: ',kernel_size)
stride = (ceil(H / level), ceil(W / level))
print('stride: ',stride)
padding = (floor((kernel_size[0] * level - H + 1) / 2), floor((kernel_size[1] * level - W + 1) / 2))
print('padding: ',padding)
print()
print('进行最大池化并将提取特征展开:')
if self.pool_type == 'max_pool':
tensor = (F.max_pool2d(x, kernel_size=kernel_size, stride=stride, padding=padding)).view(N, -1)
else:
tensor = (F.avg_pool2d(x, kernel_size=kernel_size, stride=stride, padding=padding)).view(N, -1)
if i == 0:
res = tensor
print('展开大小为: ',res.size()) #64x1
print()
else:
res = torch.cat((res, tensor), 1) # i=1 64x(1+4) i=2 64x(1+4+9)
print('合并为: ',res.size())
print()
return res
class SPPNet(nn.Module):
def __init__(self, num_level=3, pool_type='max_pool'):
super(SPPNet, self).__init__()
self.num_level = num_level
self.pool_type = pool_type
self.feature = nn.Sequential(nn.Conv2d(3, 64, 3),
nn.ReLU(),
nn.MaxPool2d(2),
nn.Conv2d(64, 64, 3),
nn.ReLU())
# num_grid = 1 + 4 + 9 = 14
self.num_grid = self._cal_num_grids(num_level)
self.spp_layer = SpatialPyramidPooling2d(num_level)
self.linear = nn.Sequential(nn.Linear(self.num_grid * 64, 512),
nn.Linear(512, 10))
def _cal_num_grids(self, level):
count = 0
for i in range(level):
count += (i + 1) * (i + 1)
return count
def forward(self, x):
print('x初始大小为:')
N, C, H, W = x.size()
print('N:', N, ' C:', C, ' H', H, ' W:', W)
print()
x = self.feature(x)
print('x经过卷积、激活、最大池化、卷积、激活变成:')
N, C, H, W = x.size()
print('64(conv)->62(maxpool)->31(conv)->29')
print('N:', N, ' C:', C, ' H', H, ' W:', W)
print()
print('x进行空间金字塔池化:')
x = self.spp_layer(x)
print('空间金字塔池化后,x进入全连接层:')
x = self.linear(x)
return x
if __name__ == '__main__':
a = torch.rand((1, 3, 64, 64))
net = SPPNet()
output = net(a)
print(output)
x初始大小为:
N: 1 C: 3 H 64 W: 64
x经过卷积、激活、最大池化、卷积、激活变成:
64(conv)->62(maxpool)->31(conv)->29
N: 1 C: 64 H 29 W: 29
x进行空间金字塔池化:
多尺度提取信息,并进行特征融合...
第 1 次计算池化核:
kernel_size: (29, 29)
stride: (29, 29)
padding: (0, 0)
进行最大池化并将提取特征展开:
展开大小为: torch.Size([1, 64])
第 2 次计算池化核:
kernel_size: (15, 15)
stride: (15, 15)
padding: (1, 1)
进行最大池化并将提取特征展开:
合并为: torch.Size([1, 320])
第 3 次计算池化核:
kernel_size: (10, 10)
stride: (10, 10)
padding: (1, 1)
进行最大池化并将提取特征展开:
合并为: torch.Size([1, 896])
空间金字塔池化后,x进入全连接层:
tensor([[-0.0894, -0.1091, -0.1104, 0.0846, -0.0732, 0.0539, 0.0072, -0.0244,
-0.0082, 0.0929]], grad_fn=<AddmmBackward>)
Process finished with exit code 0