想直接看公式的可跳至第三節 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),其大小可以是任意的
- 進行卷積運算,到最后一個卷積層(圖中是\(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