【youcans动手学模型】目标检测之 SPPNet 模型

欢迎关注『youcans动手学模型』系列
本专栏内容和资源同步到 GitHub/youcans



本文用 PyTorch 实现 SPPNet 网络模型。


1. SPPNet 卷积神经网络模型

何恺明、张祥雨、任少卿、孙剑在 ECCV 2014 发表的论文【用于视觉识别的深度卷积网络的空间金字塔池化】,提出了 SPPNet 模型。

Kaiming He, Xiangyu Zhang, Shaoqing Ren, Jian Sun, Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition

【下载地址】: https://arxiv.org/abs/1406.4729
【GitHub地址】: [OverFeat_pytorch] https://github.com/BeopGyu/OverFeat_pytorch

http://research.microsoft.com/en-us/um/people/kahe/

在这里插入图片描述


1.1 论文摘要

现有的深度卷积神经网络(CNNs)需要固定大小的输入图像。这一人为的要求可能会降低任意大小/比例的图像或子图像的识别精度。

我们提出一种 SPP Net 网络结构,采用另一种池化策略 “空间金字塔池化”,对任意大小/比例的输入图像,都可以生成固定长度的表示。金字塔池化对对象变形也具有鲁棒性。基于这些优势,SPP Net 通常可以改进所有基于卷积神经网络的图像分类方法。

在 ImageNet 2012 数据集上,使用 SPP Net 可以提高了各种卷积神经网络架构的准确性。在 Pascal VOC 2007 和 Caltech101 数据集上,SPP Net 使用单个完整图像表示,无需微调即可实现最先进的分类结果。

对于目标检测任务,使用 SPP Net 只需要从整个图像中计算一次特征图,就可以以任意区域(子图像)为输入,生成固定大小的输出特征图表示,用于训练检测器。由于 SPP Net 可以避免重复计算卷积特征,它比 R-CNN 方法快 24~102倍,并在 Pascal VOC 2007 上实现了更好的或相当的精度。

在2014 年 ImageNet 视觉识别挑战赛(ILSVRC2014)中,SPP Net 在目标检测任务获得了第二名,在图像分类任务获得了第三名。


1.2 技术背景

最近,基于深度网络的方法在图像分类、对象检测、许多其他识别任务甚至非识别任务方面取得很大进展。然而,在卷积神经网络的训练和测试中存在一个问题:卷积神经网络需要固定的输入图像大小(如224*224),这限制了输入图像的纵横比和规模。

固定大小的输入图像带来两个问题:(1)很多场景所得到图像并不是固定大小的,(2)如果对图片进行切割,可能会丢失到重要信息。

当应用于任意大小的图像时,当前的方法大多通过裁剪或缩放将输入图像拟合为固定大小。但是裁剪的区域可能不包含整个对象,而缩放的结果可能会导致几何失真,从而影响识别的准确性。此外,当对象比例变化时,预定义的比例可能不合适。总之,要求固定大小的输入图像,忽略了实际图像的尺度问题。

为什么卷积神经网络需要固定的输入大小呢?卷积神经网络由卷积层和全连接层两部分组成。卷积层以滑动窗口的方式操作,并不需要输入图像具有固定大小,并且可以生成任何大小的输出特征图。但是,全连接层要求具有固定尺寸的输入向量。因此,卷积神经网络固定尺寸的约束是来自全连接层,这些层存在于网络模型的末段。

本文引入了一个空间金字塔池化层(Spatial Pyramid Pooling, SPP)来处理卷积神经网络的固定大小约束。具体来说,我们在最后一个卷积层的顶部增加了一个 SPP 层。SPP 层生成固定大小的输出,然后将其输出到全连接层(或其它分类器)。换句话说,我们在卷积层和全连接层之间通过 SPP 执行信息“聚合”,而不是在开始就对图像进行裁剪或缩放。

在这里插入图片描述

作为词袋模型(BoW)的扩展,空间金字塔池化(通常称为 空间金字塔匹配或 SPM),是计算机视觉中最成功的方法之一。它将图像划分为从精细到粗糙的层次,并在其中聚合局部特征。在卷积神经网络流行之前,SPP 一直是分类和检测系统中的关键组成部分,但是在卷积神经网络中尚未得到应用。

SPP Net 对于深度卷积神经网络具有几个显著特性:

(1)无论输入图像的大小如何,SPP Net 都能够生成固定长度的输出,这是以前的卷积和池化层所不能实现的。

(2) SPP 使用多级空间容器,而滑动窗口池化仅使用单个窗口大小。多级池化对于目标变形具有更好的鲁棒性。

(3) 由于输入尺度的灵活性,SPP Net 可以汇集在可变尺度上提取的特征。

SPP Net 不仅可以从任意大小的图像/窗口中生成用于测试的表示,还允许我们在训练过程中提供不同大小或比例的图像。用可变大小的图像进行训练增加了尺度不变性并减少了过度拟合。

我们开发了一种简单的多尺寸训练方法。在每个epoch中,我们用给定的输入大小训练网络,在下一个epoch切换到另一个输入大小。实验表明,这种多尺度训练可以收敛,而且测试精度更好。

SPP网络在目标检测方面也显示出强大的能力。R-CNN 通过深度卷积网络提取候选窗口的特征,在目标检测任务中的性能领先。但 RCNN 对每张图像需要重复计算数千个候选区域的特征图,计算非常耗时。

很多研究都尝试了在特征图(而不是图像区域)上训练/运行检测器在本文中,我们只对整个图像进行一次卷积处理(无论窗口的数量如何),然后通过 SPP 在特征图上提取特征。SPP Net 通过 SPP 在任意窗口大小的灵活性,获得了卓越的准确性和效率。基于 R-CNN 的处理流程,SPP Net 计算特征比 R-CNN 快 24~102 倍,同时具有更好的精度。

总结:

与无 SPP 的卷积神经网络相比,SPP Net 可以增强更深、更大的各种网络。

通过不同大小/定位窗口的特征图,进行多视图测试,可以提高分类精度。


1.3 空间金字塔池化

卷积层可以接受任意大小的输入。卷积层使用滑动滤波器,其输出与输入具有大致相同的纵横比。这些输出被称为特征图,它们不仅涉及响应的强度,还涉及空间位置。

卷积层可以接受任意的输入大小,产生的输出特征图的大小也是可变的。而分类器(SVM/softmax)或全连接层需要固定长度的输入向量。

空间金字塔池化方法,通过局部的空间窗口池化来生成固定长度的输出向量,并且可以保留空间信息。只要池化的空间窗口的大小与图像大小成比例,则无论图像大小如何,都可以得到固定大小或长度的输出向量。

在这里插入图片描述

因此,为了对任意大小的图像采用卷积神经网络,我们将原有网络中最后一个池化层替换为空间金字塔池化层。

(1)通过空间金字塔池化,输入图像可以是任何大小。这不仅允许任意的纵横比,而且允许任意的比例。

通过选择不同尺寸的卷积核(w,h)和不同的步长(stride),我们可以将任意尺寸大小和比例的输入图像,调整为任何比例的输出图像,或调整为固定长度的矢量 输出,以便应用于分类器。

(2)尺度在传统图像处理方法中起着重要作用,尺度对深度网络的准确性也很重要。通过不同尺度和参数的卷积核,网络可以从图像中提取不同尺度的特征。

选择不同尺寸的卷积核(kw,kh)和不同的步长(stride),构造 3 级(3*3, 2*2, 1*1)金字塔池化层。

如图所示,网络的最后一个卷积层的输出特征维度为 (h,w,ch),作为 SPP 层的输入。金字塔的最底层,设置卷积核大小等于图像的高宽,池化输出维度为 1*1,这实际上是“全局池化”操作;金字塔的上一层,卷积核大小是图像高宽的 1/2,池化输出维度为 2*2;金字塔的顶层,卷积核大小是图像高宽的 1/3 或1/4,池化输出维度为 3*3 或 4*4。将这 3级池化输出都展平为一维后,拼接后得到长度为 1 + 2 ∗ 2 + 4 ∗ 4 = 21 1+2*2+4*4=21 1+22+44=21 的一维向量。对于 ch=256,拼接后一维向量的总长度为 21 ∗ 256 = 5376 21*256=5376 21256=5376。(注:第3层输出维度,论文图中为 4*4,示例中为 3*3,都是由用户选择确定的)


1.4 目标检测

在目标检测任务中,R-CNN 首先使用选择性搜索从图像中选出 2000 个候选窗口,然后将每个窗口中的图像区域变形到固定大小 227×227 作为输入图像,使用卷积神经网络提取每个窗口的特征,最后用 SVM 分类器进行特征检测。R-CNN 在一张图像的 2000个窗口上反复运行卷积网络,耗时很大,而在测试阶段的特征提取是主要瓶颈。

我们对整张图像运行卷积网络获得特征图,然后对图像中的每个候选窗口,在特征图的对应窗口上应用空间金字塔池化,形成这个窗口的一个固定长度表示 。 因为只应用一次卷积网络,我们的方法快得多。

在这里插入图片描述


例如,在每个候选窗口中,我们使用 4 级空间金字塔(1*1,2*2,3*3,6*6)来汇集特征,将这 4级输出都展平为一维后,拼接后得到长度为 1 + 2 ∗ 2 + 3 ∗ 3 + 6 ∗ 6 = 50 1+2*2+3*3+6*6=50 1+22+33+66=50 的一维向量。对于 ch=256,得到一维向量的总长度为 256 ∗ 50 = 12800 256*50=12800 25650=12800


1.5 总结

(1)SPP-Net 允许不同大小的输入图片,可以获得相同尺寸的特征向量。

(2)SPP-Net 在特征检测任务中只对整张图像运行一次卷积网络,速度比 RCNN 快得多。

(3)仍然使用选择性搜索(SS)来产生候选区域,这个过程的速度比较慢。


2. 在 PyTorch 中定义 SPP 模型类

2.1 SPP 层的参数计算

k h = ⌈ h i n / n ⌉ = c e i l ( h i n / n ) s h = ⌈ h i n / n ⌉ = c e i l ( h i n / n ) p h = ⌊ ( k h ∗ n − h i n + 1 ) / 2 ⌋ = f l o o r ( ( k h ∗ n − h i n + 1 ) / 2 ) h n e w = h i n + 2 ∗ p h k w = ⌈ w i n / n ⌉ = c e i l ( w i n / n ) s w = ⌈ w i n / n ⌉ = c e i l ( w i n / n ) p w = ⌊ ( k w ∗ n − w i n + 1 ) / 2 ⌋ = f l o o r ( ( k w ∗ n − w i n + 1 ) / 2 ) w n e w = w i n + 2 ∗ p w \begin{matrix} k_h &= &\lceil {h_{in}}/{n} \rceil &= &ceil( {h_{in}}/{n})\\ s_h &= &\lceil {h_{in}}/{n} \rceil &= &ceil( {h_{in}}/{n})\\ p_h &= &\lfloor (k_h*n-h_{in}+1)/2 \rfloor &= &floor( (k_h*n-h_{in}+1)/2)\\ h_{new} &= &h_{in} + 2*p_h\\ \\ k_w &= &\lceil {w_{in}}/{n} \rceil &= &ceil( {w_{in}}/{n})\\ s_w &= &\lceil {w_{in}}/{n} \rceil &= &ceil( {w_{in}}/{n})\\ p_w &= &\lfloor (k_w*n-w_{in}+1)/2 \rfloor &= &floor( (k_w*n-w_{in}+1)/2)\\ w_{new} &= &w_{in} + 2*p_w \end{matrix} khshphhnewkwswpwwnew========hin/nhin/n⌊(khnhin+1)/2hin+2phwin/nwin/n⌊(kwnwin+1)/2win+2pw======ceil(hin/n)ceil(hin/n)floor((khnhin+1)/2)ceil(win/n)ceil(win/n)floor((kwnwin+1)/2)

其中, k h , k w k_h, k_w kh,kw 为卷积核的高度、宽度, s h , s w s_h, s_w sh,sw 为高度、宽度方向的步长, p h , p w p_h,p_w ph,pw 为边界填充值(两侧乘2)。

注意卷积核和步长的计算公式都使用 ceil() 向上取整,而 padding 使用 floor() 向下取整。


2.2 定义 SPP Layer

构建 SPP 层(空间金字塔池化层)的例程如下。

# 构建 SPP 层(空间金字塔池化层)
class SPPlayer(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):
        b, c, h, w = x.size()
        xSPP = torch.zeros([b, 0]).cuda()
        for i in range(self.num_levels):
            level = i + 1
            kh, kw = math.ceil(h/level), math.ceil(w/level)
            sh, sw = math.ceil(h/level), math.ceil(w/level)
            ph, pw = math.floor((kh*level-h+1)/2), math.floor((kw*level-w+1)/2)

            # 最大池化
            tensor = nn.functional.max_pool2d(x, kernel_size=(kh,kw), stride=(sh,sw), padding=(ph,pw))
            # 平均池化
            # tensor = nn.functional.avg_pool2d(x, kernel_size=(kh,kw), stride=(sh,sw), padding=(ph,pw))
            # 展平为一维后拼接
            xSPP = torch.cat((xSPP, tensor.view(b, -1)), 1)  # (1+2*2+3*3)*c = 14*c
        return xSPP

2.3 SPPnet 模型类

参考 AlexNet 模型,使用 SPP Layer 构造 SPPnet 模型如下。

class SPPnet(nn.Module):
    # Expected input size is 64x64
    def __init__(self, spp_level=3, num_classes=10):
        super(SPPnet, self).__init__()
        self.spp_level = spp_level
        self.num_grids = 0
        for i in range(spp_level):
            self.num_grids += (i+1)**2  # 1*1, 2*2, 3*3

        self.features = nn.Sequential(OrderedDict([
            ('conv1', nn.Conv2d(3, 128, 3)),
            ('relu1', nn.ReLU()),
            ('pool1', nn.MaxPool2d(2)),
            ('conv2', nn.Conv2d(128, 128, 3)),
            ('relu2', nn.ReLU()),
            ('pool2', nn.MaxPool2d(2)),
            ('conv3', nn.Conv2d(128, 128, 3)),
            ('relu3', nn.ReLU()),
            ('pool3', nn.MaxPool2d(2)),
            ('conv4', nn.Conv2d(128, 128, 3)),
            ('relu4', nn.ReLU())
        ]))

        self.spp_layer = SPPlayer(spp_level, 'max_pool')

        self.classifier = nn.Sequential(OrderedDict([
            # ('dropout', nn.Dropout(p=0.2, inplace=True)),
            ('fc1', nn.Linear(self.num_grids*128, 1024)),
            ('fc1_relu', nn.ReLU()),
            ('fc2', nn.Linear(1024, num_classes)),
        ]))

    def forward(self, x):
        x = self.features(x)
        x = self.spp_layer(x)
        x = self.classifier(x)
        return x

模型的结构如下。

----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1          [-1, 128, 62, 62]           3,584
              ReLU-2          [-1, 128, 62, 62]               0
         MaxPool2d-3          [-1, 128, 31, 31]               0
            Conv2d-4          [-1, 128, 29, 29]         147,584
              ReLU-5          [-1, 128, 29, 29]               0
         MaxPool2d-6          [-1, 128, 14, 14]               0
            Conv2d-7          [-1, 128, 12, 12]         147,584
              ReLU-8          [-1, 128, 12, 12]               0
         MaxPool2d-9            [-1, 128, 6, 6]               0
           Conv2d-10            [-1, 128, 4, 4]         147,584
             ReLU-11            [-1, 128, 4, 4]               0
         SPPlayer-12                 [-1, 1792]               0
           Linear-13                 [-1, 1024]       1,836,032
             ReLU-14                 [-1, 1024]               0
           Linear-15                   [-1, 10]          10,250
================================================================
Total params: 2,292,618
Trainable params: 2,292,618
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.05
Forward/backward pass size (MB): 10.66
Params size (MB): 8.75
Estimated Total Size (MB): 19.45
----------------------------------------------------------------

3. SPPnet 模型的训练

[SPP-net_pytorch/ GitHub]
https://github.com/zjZSTU/SPP-net/tree/master/py

import torch
import torch.nn as nn


class ZFNet(nn.Module):

    def __init__(self, num_classes=1000):
        super(ZFNet, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(64, 192, kernel_size=5, stride=2, padding=2),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
            nn.Conv2d(192, 384, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(384, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),
        )
        self.avgpool = nn.AdaptiveAvgPool2d((6, 6))
        self.classifier = nn.Sequential(
            nn.Dropout(),
            nn.Linear(256 * 6 * 6, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Linear(4096, num_classes),
        )

    def forward(self, x):
        x = self.features(x)
        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.classifier(x)
        return x


【本节完】


版权声明:
欢迎关注『youcans动手学模型』系列
转发请注明原文链接:
【youcans动手学模型】目标检测之 SPPNet 模型
Copyright 2023 youcans, XUPT
Crated:2023-07-25


猜你喜欢

转载自blog.csdn.net/youcans/article/details/131910101