空间金字塔池化网络SPPNet详解

参考:https://cloud.tencent.com/developer/article/1441559

前言
SPP-Net是出自2015年发表在IEEE上的论文-《Spatial Pyramid Pooling in Deep ConvolutionalNetworks for Visual Recognition》,这篇论文解决之前深度神经网络的一个大难题,即输入数据的维度一定要固定。
一、什么是空间金字塔池化网络——SPPNet
所谓空间金字塔池化网络,英文全称为Spatial Pyramid Pooling Networks ,简称SPP-Net。它也是由何凯明大神与2015年首先发表的。
二、为什么要用SPP-Net
2.1、传统卷积神经网络的限制
之前的深度卷积神经网络(CNNs)都需要输入的图像尺寸固定(比如224×224)。由于输入的图像大小固定,即数据维度固定,但是现实样本中往往很多样本是大小不一的,为了产生固定输入大小的样本,有两种主要的预处理措施(会降低识别的精度):
(1)crop(裁剪)
 空间金字塔池化网络SPPNet详解_第1张图片
从上面可以看出,对原始图像进行裁剪之后,必然会有相关的特征被剔除掉了,肯定会影响到特征的提取;
(2)wrap(缩放)
 空间金字塔池化网络SPPNet详解_第2张图片
从上面可以看出,原始图像经过缩放之后,变得很畸形失真,这也会影响到特征提取的过程。
2.2、CNN为什么需要固定的输入
CNN主要由两部分组成,卷积部分和其后的全连接部分。卷积部分通过滑窗进行计算,并输出代表激活的空间排布的特征图(feature map)。事实上,卷积并不需要固定的图像尺寸,他可以产生任意尺寸的特征图。而另一方面,根据定义,全连接层则需要固定的尺寸输入。因此固定尺寸的问题来源于全连接层,也是网络的最后阶段。
找到了问题的症结所在,现在就可以来说明解决方案了。
三、什么是SPP-Net
3.1 SPP-Net与经典CNN的架构对比
首先看一下传统CNN网络与SPP-Net网络的一个对比。
 空间金字塔池化网络SPPNet详解_第3张图片
 空间金字塔池化网络SPPNet详解_第4张图片
从上面的架构中可以看出,SPP-Net与经典CNN最主要的区别在于两点:
第一点:不再需要对图像进行crop/wrap这样的预处理;
第二点:在卷积层和全连接层交接的地方添加所谓的空间金字塔池化层,即(spatial pyramid pooling),使用这种方式,可以让网络输入任意的图片,而且还会生成固定大小的输出。
3.2 金字塔的具体工作过程
假设我们以一个三层金字塔作为例子来说明,将3.1中的图二中红色字体标注出来部分堆叠层展开,如下所示:
 空间金字塔池化网络SPPNet详解_第5张图片
抽象出来为下图:
 空间金字塔池化网络SPPNet详解_第6张图片
图3.1中有一个特别重要的标注信息,:
fix bin numbers,do not fix bin size(固定块的数量而非块的大小)
如何理解这句话?
实际上 fix bin size 正是我们经典CNN所采取的方式,即固定一个池化层的大小(size)和步幅(stride),比如池化层的大小为5*5,步幅为3,那么针对不同的输入,池化层输出之后的特征图大小当然不是不定的,自然也没有办法起到固定特征大小的作用了。
那什么又是fix bin numbers?他的意思就是我最终的的那个池化层产生的结果是固定的,即针对一个特征图,经过某一个池化层之后,我的目的就是要产生一个固定大小的特征图,比如上面的三层金字塔:
第一层:为4*4,即要保证我前面的特征图经过池化之后能够总能够产生4*4的输出,即16个特征;
第二层:为2*2,即要保证我前面的特征图经过池化之后能够总能够产生2*2的输出,及4个特征;
第三层:为1*1,即要保证我前面的特征图经过池化之后能够总能够产生1*1的输出,即1个特征;
这样一共就得到了16+4+1=21个特征了。我们将整个这三层包装成一个“金字塔层(这个名字是我自己起的,其实就相当于一个卷积核的意思)”,那么有N个“金字塔层”的时候,最后得到的输出特征为 21*N个,这是固定大小的。 
了解池化层过程的小伙伴应该能够体会到这里的含义了,既然要保证对于不同的特征图输入,都能够产生相同的输出,每一个池化过程的池化核肯定是不一样的。
总结:现在可以用一句话来概括fix bin numbers,do not fix bin size.这句话的含义了。即经典的CNN中的4*4指的是一个4*4的池化核;而SPP-Net中的4*4指的是要产生固定的4*4的特征输出。
那具体我要怎么样才能保证针对不同的输入特征图,输出具有相同尺寸的输出特征图呢?这实际上就是由两个参数决定的:
第一个:a*a,指的是最后一个卷积层之后得到的输出,也即是我的金字塔池化层的输入维度;
第二个:n*n,指的是金字塔池化层的期望输出,比如上面的4*4,2*2,1*1.
那到底是怎么决定的呢?在下面的训练过程再说明。
3.3 金字塔池化层的训练过程
SPP-Net的训练过程是分为两个过程的
(1)单一尺寸训练——single-size
所谓单一尺寸训练指的是先只对一种固定输入图像进行训练,比如224*224,在conv5之后的特征图为:13x13这就是我们的(a*a)而我要得到的输出为4*4,2*2,1*1,怎么办呢?这里金字塔层bins即为 n*n,也就是4*4,2*2,1*1,我们要做的就是如何根据a和n设计一个池化层,使得a*a的输入能够得到n*n的输出。实际上这个池化层很好设计,我们称这个大小和步幅会变化的池化层为sliding window pooling。
它的大小为:windows_size=[a/n] 向上取整 , stride_size=[a/n]向下取整。数据实验如下:
当a*a为13*13时,要得到4*4的输出,池化层的大小为4,移动步幅为3;
当a*a为13*13时,要得到2*2的输出,池化层的大小为7,移动步幅为6;
当a*a为13*13时,要得到1*1的输出,池化层的大小为13,移动步幅为13;
有的小伙伴一定发现,那如果我的输入a*a变化为10*10呢,此时再用上面的三个池化核好像得不到固定的理想输出啊,事实上的确如此,这是训练的第二个过程要讲的,因为此过程称之为“单一尺度训练”,针对的就是某一个固定的输入尺度而言的。
(2)多尺寸训练——multi-size(以两种尺度为例)
虽然带有SPP(空间金字塔)的网络可以应用于任意尺寸,为了解决不同图像尺寸的训练问题,我们往往还是会考虑一些预设好的尺寸,而不是一些尺寸种类太多,毫无章法的输入尺寸。现在考虑这两个尺寸:180×180,224×224,此处只考虑这两个哦。
我们使用缩放而不是裁剪,将前述的224的区域图像变成180大小。这样,不同尺度的区域仅仅是分辨率上的不同,而不是内容和布局上的不同。
那么对于接受180输入的网络,我们实现另一个固定尺寸的网络。在论文中,conv5输出的特征图尺寸是axa=10×10。我们仍然使用windows_size=[a/n] 向上取整 , stride_size=[a/n]向下取整,实现每个金字塔池化层。这个180网络的空间金字塔层的输出的大小就和224网络的一样了。
当a*a为10*10时,要得到4*4的输出,池化层的大小为3,移动步幅为2(注意:此处根据这样的一个池化层,10*10的输入好像并得不到4*4的输出,9*9或者是11*11的倒可以得到4*4的)这个地方我也还不是特别清楚这个点,后面我会说出我的个人理解。
当a*a为10*10时,要得到2*2的输出,池化层的大小为5,移动步幅为5;
当a*a为10*10时,要得到1*1的输出,池化层的大小为10,移动步幅为10;
(3)原始论文中的两个训练过程
上面的红色字体表明了在多尺度训练过程的一个漏洞,这其实不是错误,因为我们期望得到的是4*4,2*2,1*1的特征,但是180*180的输入图却并得不到4*4的,这说明用它作为输入是不行的,那到底该怎么搞呢?后面会给出解释,我们先来看一下原始论文中的期望输出是
3*3,2*2,1*1,即期望得到特征是9+4+1=14个。
在single-size过程:
当a*a为13*13时,要得到3*3的输出,池化层的大小为5,移动步幅为4;
当a*a为13*13时,要得到2*2的输出,池化层的大小为7,移动步幅为6;
当a*a为13*13时,要得到1*1的输出,池化层的大小为13,移动步幅为13;
这没有问题:
在multi-size过程:
当a*a为10*10时,要得到3*3的输出,池化层的大小为4,移动步幅为3;
当a*a为10*10时,要得到2*2的输出,池化层的大小为5,移动步幅为5;
当a*a为10*10时,要得到1*1的输出,池化层的大小为10,移动步幅为10;
这也没有问题。

3.4 金字塔池化网络SPP-Net的结构设计
我们知道,在设计卷积神经网络的时候,每一个卷积层、池化层的size和stride需要很好的设计,他决定了说每一次操作之后的输出特征图的大小。虽然SPP-Net名义上称之为可以处理不同尺度的输入尺寸,但是这个尺寸也没有那么的随意,因为就像上面的例子所示,不是所有的尺寸最后都可以完美的得到理想的期望输出的,那怎么办呢?注意几个点即可:
(1)至少使用一个大的尺寸和一个小的尺寸。因为从大尺寸到小尺寸,不同尺度的区域仅仅是分辨率上的不同,而不是内容和布局上的不同;
(2)不同的尺寸之间要能够较好的“兼容(我自己起的名字)”。指的是这个大小也不是随便乱规定的,我们需要根据最后一层卷积之后的尺寸,即a*a,以及我们期望得到的尺寸 n*n,去计算好到底哪些不同的尺寸可以“兼容”。
四、SPP-Net的应用与案例
SPP-Net从诞生开始,在图像识别、目标检测方面都有着很好的应用。
4.1 在object classify方面的应用
这里可以参考相关的论文,这里不再详细说明了。
4.2 在object detect方面的应用
SPP网络,这个方法的思想在R-CNN、Fast RCNN, Faster RCNN上都起了举足轻重的作用,对于检测算法,论文中是这样做到:使用ss生成~2k个候选框,缩放图像min(w,h)=s之后提取特征,每个候选框使用一个4层的空间金字塔池化特征,网络使用的是ZF-5的SPPNet形式。之后将12800d的特征输入全连接层,SVM的输入为全连接层的输出。这个算法可以应用到多尺度的特征提取:先将图片resize到五个尺度:480,576,688,864,1200,加自己6个。然后在map window to feature map一步中,选择ROI框尺度在{6个尺度}中大小最接近224x224的那个尺度下的feature maps中提取对应的roi feature。这样做可以提高系统的准确率。
这里有一张图来完整的描述SPP-Net

空间金字塔池化网络SPPNet详解_第7张图片

五、代码

5.1 spp_layer.py

import math
def spatial_pyramid_pool(self,previous_conv, num_sample, previous_conv_size, out_pool_size):
    '''
    previous_conv: a tensor vector of previous convolution layer
    num_sample: an int number of image in the batch
    previous_conv_size: an int vector [height, width] of the matrix features size of previous convolution layer
    out_pool_size: a int vector of expected output size of max pooling layer
    
    returns: a tensor vector with shape [1 x n] is the concentration of multi-level pooling
    '''    
    # print(previous_conv.size())
    for i in range(len(out_pool_size)):
        # print(previous_conv_size)
        h_wid = int(math.ceil(previous_conv_size[0] / out_pool_size[i]))
        w_wid = int(math.ceil(previous_conv_size[1] / out_pool_size[i]))
        h_pad = (h_wid*out_pool_size[i] - previous_conv_size[0] + 1)/2
        w_pad = (w_wid*out_pool_size[i] - previous_conv_size[1] + 1)/2
        maxpool = nn.MaxPool2d((h_wid, w_wid), stride=(h_wid, w_wid), padding=(h_pad, w_pad))
        x = maxpool(previous_conv)
        if(i == 0):
            spp = x.view(num_sample,-1)
            # print("spp size:",spp.size())
        else:
            # print("size:",spp.size())
            spp = torch.cat((spp,x.view(num_sample,-1)), 1)
    return spp

5.2 cnn_with_spp.py

import torch
import torch.nn as nn
from torch.nn import init
import functools
from torch.autograd import Variable
import numpy as np
import torch.nn.functional as F
from spp_layer import spatial_pyramid_pool
class SPP_NET(nn.Module):
    '''
    A CNN model which adds spp layer so that we can input multi-size tensor
    '''
    def __init__(self, opt, input_nc, ndf=64,  gpu_ids=[]):
        super(SPP_NET, self).__init__()
        self.gpu_ids = gpu_ids
        self.output_num = [4,2,1]
        
        self.conv1 = nn.Conv2d(input_nc, ndf, 4, 2, 1, bias=False)
        
        self.conv2 = nn.Conv2d(ndf, ndf * 2, 4, 1, 1, bias=False)
        self.BN1 = nn.BatchNorm2d(ndf * 2)

        self.conv3 = nn.Conv2d(ndf * 2, ndf * 4, 4, 1, 1, bias=False)
        self.BN2 = nn.BatchNorm2d(ndf * 4)

        self.conv4 = nn.Conv2d(ndf * 4, ndf * 8, 4, 1, 1, bias=False)
        self.BN3 = nn.BatchNorm2d(ndf * 8)

        self.conv5 = nn.Conv2d(ndf * 8, 64, 4, 1, 0, bias=False)
        self.fc1 = nn.Linear(10752,4096)
        self.fc2 = nn.Linear(4096,1000)

    def forward(self,x):
        x = self.conv1(x)
        x = self.LReLU1(x)

        x = self.conv2(x)
        x = F.leaky_relu(self.BN1(x))

        x = self.conv3(x)
        x = F.leaky_relu(self.BN2(x))
        
        x = self.conv4(x)
        # x = F.leaky_relu(self.BN3(x))
        # x = self.conv5(x)
        spp = spatial_pyramid_pool(x,1,[int(x.size(2)),int(x.size(3))],self.output_num)
        # print(spp.size())
        fc1 = self.fc1(spp)
        fc2 = self.fc2(fc1)
        s = nn.Sigmoid()
        output = s(fc2)
        return output

 

你可能感兴趣的:(空间金字塔池化网络SPPNet详解)