天天看點

空間金字塔池化(Spatial Pyramid Pooling, SPP)原理和代碼實作(Pytorch)

想直接看公式的可跳至第三節 3.公式修正

一、為什麼需要SPP

首先需要知道為什麼會需要SPP。

我們都知道卷積神經網絡(CNN)由卷積層和全連接配接層組成,其中卷積層對于輸入資料的大小并沒有要求,唯一對資料大小有要求的則是第一個全連接配接層,是以基本上所有的CNN都要求輸入資料固定大小,例如著名的VGG模型則要求輸入資料大小是 (224*224) 。

固定輸入資料大小有兩個問題:

1.很多場景所得到資料并不是固定大小的,例如街景文字基本上其高寬比是不固定的,如下圖示紅色框出的文字。

空間金字塔池化(Spatial Pyramid Pooling, SPP)原理和代碼實作(Pytorch)
空間金字塔池化(Spatial Pyramid Pooling, SPP)原理和代碼實作(Pytorch)

2.可能你會說可以對圖檔進行切割,但是切割的話很可能會丢失到重要資訊。

綜上,SPP的提出就是為了解決CNN輸入圖像大小必須固定的問題,進而可以使得輸入圖像高寬比和大小任意。

二、SPP原理

更加具體的原理可查閱原論文:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
空間金字塔池化(Spatial Pyramid Pooling, SPP)原理和代碼實作(Pytorch)

上圖是原文中給出的示意圖,需要從下往上看:

  • 首先是輸入層(input image),其大小可以是任意的
  • 進行卷積運算,到最後一個卷積層(圖中是\(conv_5\))輸出得到該層的特征映射(feature maps),其大小也是任意的
  • 下面進入SPP層
    • 我們先看最左邊有16個藍色小格子的圖,它的意思是将從\(conv_5\)得到的特征映射分成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):\((h+2p-f+1)*(w+2p-f+1)\)
  • 有步長(Stride):⌊\(\frac{h+2p-f}{s}\)+1⌋*⌊\(\frac{w+2p-f}{s}\)+1⌋

2.公式

假設

  • 輸入資料大小是\((c, h_{in}, w_{in})\),分别表示通道數,高度,寬度
  • 池化數量:\((n,n)\)

那麼則有

  • 核(Kernel)大小: \(⌈\frac{h_{in}}{n},\frac{w_{in}}{n}⌉=ceil(\frac{h_{in}}{n},\frac{w_{in}}{n})\)
  • 步長(Stride)大小: \(⌊\frac{h_{in}}{n},\frac{w_{in}}{n}⌋=floor(\frac{h_{in}}{n},\frac{w_{in}}{n})\)

我們可以驗證一下,假設輸入資料大小是\((10, 7, 11)\), 池化數量\((2, 2)\):

那麼核大小為\((4,6)\), 步長大小為\((3,5)\), 得到池化後的矩陣大小的确是\(2*2\)。

3.公式修正

是的,論文中給出的公式的确有些疏漏,我們還是以舉例子的方式來說明

假設輸入資料大小和上面一樣是\((10, 7, 11)\), 但是池化數量改為\((4,4)\):

此時核大小為\((2,3)\), 步長大小為\((1,2)\),得到池化後的矩陣大小的确是\(6*5\) ←[簡單的計算矩陣大小的方法:(7=2+1*5, 11=3+2*4)],而不是\(4*4\)。

那麼問題出在哪呢?

我們忽略了padding的存在(我在原論文中沒有看到關于padding的計算公式,如果有的話。。。那就是我看走眼了,麻煩提示我一下在哪個位置寫過,謝謝)。

仔細看前面的計算公式我們很容易發現并沒有給出padding的公式,在經過N次使用SPP計算得到的結果與預期不一樣以及查找各種網上資料(盡管少得可憐)後,現将加入padding後的計算公式總結如下。

\(K_h = ⌈\frac{h_{in}}{n}⌉=ceil(\frac{h_{in}}{n})\)

\(S_h = ⌈\frac{h_{in}}{n}⌉=ceil(\frac{h_{in}}{n})\)

\(p_h = ⌊\frac{k_h*n-h_{in}+1}{2}⌋=floor(\frac{k_h*n-h_{in}+1}{2})\)

\(h_{new} = 2*p_h +h_{in}\)

\(K_w = ⌈\frac{w_{in}}{n}⌉=ceil(\frac{w_{in}}{n})\)

\(S_w = ⌈\frac{w_{in}}{n}⌉=ceil(\frac{w_{in}}{n})\)

\(p_w = ⌊\frac{k_w*n-w_{in}+1}{2}⌋=floor(\frac{k_w*n-w_{in}+1}{2})\)

\(w_{new} = 2*p_w +w_{in}\)

  • \(k_h\): 表示核的高度
  • \(S_h\): 表示高度方向的步長
  • \(p_h\): 表示高度方向的填充數量,需要乘以2
注意核和步長的計算公式都使用的是ceil(),即向上取整,而padding使用的是floor(),即向下取整。

現在再來檢驗一下:

假設輸入資料大小和上面一樣是\((10, 7, 11)\), 池化數量為\((4,4)\):

Kernel大小為\((2,3)\),Stride大小為\((2,3)\),是以Padding為\((1,1)\)。

利用矩陣大小計算公式:⌊\(\frac{h+2p-f}{s}\)+1⌋*⌊\(\frac{w+2p-f}{s}\)+1⌋得到池化後的矩陣大小為:\(4*4\)。

四、代碼實作(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

微信公衆号:AutoML機器學習
空間金字塔池化(Spatial Pyramid Pooling, SPP)原理和代碼實作(Pytorch)
MARSGGBO ♥原創

如有意合作或學術讨論歡迎私戳聯系~

郵箱:[email protected]

2018-3-15