參考鏈接:https://www.jianshu.com/p/5056e6143ed5
目標檢測技術的演進:RCNN->SppNET->Fast-RCNN->Faster-RCNN
不同於分類問題,物體檢測可能會存在多個檢測目標,這不僅需要我們判別出各個物體的類別,而且還要準確定位出物體的位置。
首先講解幾個常用的概念:Bbox,IoU,非極大值抑制。
Bounding Box(bbox)
bbox是包含物體的最小矩形,該物體應在最小矩形內部,如上圖紅色框藍色框和綠色框。
物體檢測中關於物體位置的信息輸出是一組(x,y,w,h)數據,其中x,y代表着bbox的左上角(或者其他固定點,可自定義),對應的w,h表示bbox的寬和高.一組(x,y,w,h)可以唯一的確定一個定位框。
Intersection over Union(IoU)
對於兩個區域和,則兩個區域的重疊程度overlap計算如下:
非極大值抑制(Non-Maximum Suppression又稱NMS)
非極大值抑制(NMS)可以看做是局部最大值的搜索問題,把不是極大值的抑制掉,在物體檢測上,就是對一個目標有多個標定框,使用極大值抑制算法濾掉多餘的標定框。
主流的檢測框架:
主要分爲兩階段檢測器,單階段檢測器。
R-CNN(Regions with CNN features)
如上圖所示,R-CNN這個物體檢查系統可以大致分爲四步進行:
- 獲取輸入圖像
- 提取約2000個候選區域(Region Proposal)
- 將候選區域分別輸入CNN網絡(這裏需要將候選圖片進行縮放)
- 將CNN的輸出輸入到SVM中進行類別的判定
第一步:生成Region Proposal
高度非線性的深度網絡具有很強的建模能力,計算複雜度高僅生成少量Region Proposal;訓練需要大量標註數據有監督預訓練 +領域特定微調。比較常用的是selective search方法,具有如下特性:
- 無監督:沒有訓練過程,不需要帶標註的數據
- 數據驅動:根據圖像特徵生成候選窗
- 基於圖像分割任務
有如下兩種方式:
對圖像進行分割,每個分割區域生成一個對應的外接矩形框
基於相似度進行層次化地區域合併
第二步:用CNN提取Region Proposal特徵
將不同大小的Region Proposal縮放到相同大小:227x227, 進行些許擴大以包含少量上下文信息。
縮放分爲兩大類:
1)各向同性縮放,長寬放縮相同的倍數
- tightest square with context: 把region proposal的邊界進行擴展延伸成正方形,灰色部分用原始圖片中的相應像素填補,如下圖(B)所示
- tightest square without context: 把region proposal的邊界進行擴展延伸成正方形,灰色部分不填補,如下圖 (C ) 所示
2)各向異性縮放, 長寬放縮的倍數不同
不管圖片是否扭曲,長寬縮放的比例可能不一樣,直接將長寬縮放到227*227,如下圖(D)所示
將所有窗口送入Backbone,如預訓練的 AlexNet,ResNet等提取特徵。一般與訓練的backbone需要進行微調finetune,
以最後一個全連接層FC的輸出作爲特徵表示。
第三步:對Region Proposal進行分類+邊框校準
分類算法:
- SVM:對CNN輸出的特徵用SVM進行分類,針對每個類別單獨訓練。二分類問題,判斷是不是屬於這個類別,是就是positive,反之negative。
- Softmax:和整個CNN一起端到端訓練,所有類別一起訓練,多類分類
邊框校準:
使用迴歸器精細修正候選框位置:對於每一個類,訓練一個線性迴歸模型去判定這個框是否框得完美。
- 讓檢測框的位置更加準確,同時更加緊緻(包含更少的背景區域)
- 線性迴歸模型
對於檢測框修正到預測框,首先平移中心點座標:
其中和是平移量,和是迴歸的目標。
對寬和高進行縮放:
其中和是伸縮因子,和是迴歸的目標。
所以我們要學習的目標即爲:,,和,統一爲,可寫爲:
其中表示proposal 經Backbone(AlexNet,VGGNet,ResNet,etc) Global Avg Pool之後的特徵向量。
,,和對應的groud truth爲,那麼誤差函數寫爲:
目標框groud truth爲,所以確定值爲:
Note that 只有當Proposal樣本和Ground Truth比較接近時(這裏取IoU>0.6),才能將其作爲訓練樣本訓練我們的線性迴歸模型,否則會導致訓練的迴歸模型不work。(當Proposal跟G離得較遠,就是複雜的非線性問題了,此時用線性迴歸建模顯然不合理)
SPPNet (Spatial Pyramid Pooling)
R-CNN要求輸入圖像的尺寸相同,不同尺度和長寬比的區域被變換到相同大小。但是裁剪會使信息丟失(或引入過多背景),縮放會使物體變形:
卷積允許任意大小的圖像輸入網絡。原始圖像通過卷積層之後,Spatial Pyramid Pooling(SPP) layer負責將不同size的檢測框進行歸一化地pooling,每一個pooling的filter會根據輸入調整大小,而SPP的輸出尺度始終是固定的。
具體做法是,在conv5層得到的特徵圖是256個channel的,先把每個特徵圖分割成多個不同尺寸的網格,比如網格分別爲4×4、2×2、1×1,然後每個網格做max pooling,這樣256層特徵圖就形成了16×256,4×256,1×256維特徵
一般來說檢測框很多都是重疊的,對檢測框進行卷積操作會帶來大量的重複操作,所有SPPNet對原始圖像進行卷積操作去除了各個區域的重複計算。此外,對於一個proposal,需要弄清楚SPP之後的每一個像素點對應的局部感受域的中心,如下給定一個例子:
通常情況下,設當前特徵圖下某位置爲,對應於上一個特徵圖的卷積核中心的位置爲,則有對應關係:
其中是stride,是卷積核的尺寸,是卷積核的padding。一般情況下,可以取,所以可以化簡爲:
對公式進行級聯可以得到:
Fast R-CNN
加入了的ROI(Region Of Interest) Pooling層,對每個region都提取一個固定維度的特徵表示。相當於特殊的SPP層,RoI層是使用單個尺度的SPP層(不用多個尺度的原因是多個尺度準確率提升不高,但是計算量開銷顯著)。
RoI Pooling原理
RoI層將每一個候選區域都分爲提前定義的塊。對每個小塊做max-pooling,此時每一個將候選區的局部特徵映射轉變爲大小統一的數據,送入下一層。
梯度反向傳播:
設爲輸入層結點,爲輸出層的節點.
中判決函數表示節點是否被節點選爲最大值輸出。不被選中有兩種可能:不在範圍內,或者不是最大值.
一個輸入節點可能和多個輸出節點相連。設爲輸入層的節點,爲第個候選區域的第個輸出節點。
多任務
另外,之前RCNN的處理流程是先提proposal,然後CNN提取特徵,之後用SVM分類器,最後再做bbox regression,而在Fast-RCNN中,作者巧妙的把bbox regression放進了神經網絡內部,與region分類和併成爲了一個multi-task模型,實際實驗也證明,這兩個任務能夠共享卷積特徵。
邊框校準誤差:smooth L1 Loss
Mask R-CNN
論文地址:https://arxiv.org/pdf/1703.06870.pdf
實例分割(instance segmentation):對於檢測到的每個物體(實例),精確地標記出其每個像素
RoIAlign
在Faster R-CNN中增加實例分割模塊:RoIPool
→RoIAlign
ROIAlign:https://www.cnblogs.com/wangyong/p/8523814.html
對一張原圖,經過VGG16的處理後,一共stride=32,圖片縮小爲。設定原圖中有一的proposal,映射到特徵圖中的大小:665/32=20.78,即20.78×20.78:
- 對於RoIPool:在計算的時候會進行取整操作,於是,進行所謂的第一次量化,即映射的特徵圖大小爲20×20。設歸一化的尺寸爲7×7,則每一個小區域的尺寸爲:20/7=2.86,即2.86×2.86。此時,進行第二次量化,故小區域大小變成2×2。每個2×2的小區域裏,取出其中最大的像素值,作爲這一個區域的‘代表’,這樣,49個小區域就輸出49個像素值,組成2.97×2.97大小的feature map
總結: 經過兩次量化,即將浮點數取整,原本在特徵圖上映射的20×20大小的region proposal,偏差成大小爲14×14的,這樣的像素偏差勢必會對後層的迴歸定位產生影響。所以,產生了更精細的替代方案,RoiAlign。 - 對於RoIAlign:沒有像RoiPooling那樣就行取整操作,保留浮點數特徵圖大小20.78×20.78,之後劃分每個小區域:20.78/7=2.97,即2.97×2.97。假定採樣點數爲4,即對於每個2.97×2.97的小區域,平分4份,每一份取其中心點位置,而中心點位置的像素,採用雙線性插值法進行計算,這樣,就會得到四個點的像素值,如下圖
上圖中,四個紅色叉叉‘×’的像素值是通過雙線性插值算法計算得到的。最後,取四個像素值中最大值作爲這個小區域(即:2.97×2.97大小的區域)的像素值,如此類推,同樣是49個小區域得到49個像素值,組成7×7大小的feature map
在Faster R-CNN
上增加了Instance Segmentation Head:
FCN
首先簡單介紹一下全卷積 (FCN,fully-connected networks) ,FCN將傳統CNN後面的全連接層替換爲卷積,這樣就可以獲得2維的feature map,後接softmax獲得每一個像素點的分類信息,從而解決分割問題。
論文:https://arxiv.org/pdf/1411.4038.pdf
衆所周知,每一次卷積都是對圖像的一次縮小,每一次縮小帶來的是分辨率越低,圖像越模糊,而在第一部分我們知道FCN是通過像素點進行圖像分割,那FCN是怎麼解決的這一個問題?答案是上採樣,比如我們在3次卷積後,圖像分別縮小了2 4 8倍,因此在最後的輸出層,我們需要進行8倍的上採樣,從而得到原來的圖像大小.而上採樣本身就是一個反捲積實現的。
從論文中得到的結果來看,從32倍,16倍,8倍到最終結果,結果越來越精細:
Instance Segmentation Head
具體的,Head Architecture如下所示:
圖中,箭頭表明卷積
,反捲積
或FC
層(根據context可以推斷,conv保護spatial信息,deconv升採樣,FC作用於一維向量)。所有的conv是3×3,除了輸出conv是1×1,deconvs是2×2(stride=2)。Left:‘res5’表明ResNet的第5個階段;Right:‘×4’表明4個連續的convs。
Pytorch實現
目標檢測和分割可使用mmdetection庫來實現,代碼參考(CUHK,MM Lab):https://github.com/open-mmlab/mmdetection。
1.安裝
需要首先創建anaconda的虛擬環境,在虛擬環境中進行mmdection的安裝:
(1)創建虛擬環境並激活:
conda create -n open-mmlab python=3.7 -y
conda activate open-mmlab
(2)安裝pytorch,torchvision以及依賴的mmcv庫,版本可以隨機更改:
pip install torch==1.1.0
pip install torchvision==0.3.0
pip install mmcv
(3)定位到mmdection文件夾,運行如下命令編譯安裝:
python setup.py develop
2.更換backbone爲自己的net
open-mmlab/mmdetection/tree/master/mmdet/models/backbones
裏放入backbone文件,這裏以ResNetSE爲例,創建resnet_se.py
文件:
import logging
import torch.nn as nn
from mmcv.cnn import constant_init, kaiming_init
from mmcv.runner import load_checkpoint
from torch.nn.modules.batchnorm import _BatchNorm
from ..registry import BACKBONES
from ..utils import build_conv_layer, build_norm_layer
class SELayer(nn.Module):
def __init__(self, channel, reduction = 16):
super(SELayer, self).__init__()
self.avg_pool = nn.AdaptiveAvgPool2d(1)
self.fc = nn.Sequential(
nn.Linear(channel, channel // reduction),
nn.ReLU(inplace = True),
nn.Linear(channel // reduction, channel),
nn.Sigmoid()
)
print('add one SELayer!')
def forward(self, x):
b, c, _, _ = x.size()
y = self.avg_pool(x).view(b, c)
y = self.fc(y).view(b, c, 1, 1)
return x * y
class Bottleneck(nn.Module):
expansion = 4
def __init__(self,
inplanes,
planes,
stride=1,
dilation=1,
downsample=None,
conv_cfg=None,
norm_cfg=dict(type='BN')):
super(Bottleneck, self).__init__()
self.inplanes = inplanes
self.planes = planes
self.stride = stride
self.dilation = dilation
self.conv_cfg = conv_cfg
self.norm_cfg = norm_cfg
self.conv1_stride = 1
self.conv2_stride = stride
self.norm1_name, norm1 = build_norm_layer(norm_cfg, planes, postfix=1)
self.norm2_name, norm2 = build_norm_layer(norm_cfg, planes, postfix=2)
self.norm3_name, norm3 = build_norm_layer(
norm_cfg, planes * self.expansion, postfix=3)
self.conv1 = build_conv_layer(
conv_cfg,
inplanes,
planes,
kernel_size=1,
stride=self.conv1_stride,
bias=False)
self.add_module(self.norm1_name, norm1)
self.conv2 = build_conv_layer(
conv_cfg,
planes,
planes,
kernel_size=3,
stride=self.conv2_stride,
padding=dilation,
dilation=dilation,
bias=False)
self.add_module(self.norm2_name, norm2)
self.conv3 = build_conv_layer(
conv_cfg,
planes,
planes * self.expansion,
kernel_size=1,
bias=False)
self.add_module(self.norm3_name, norm3)
self.relu = nn.ReLU(inplace=True)
self.downsample = downsample
self.se = SELayer(planes * self.expansion)
@property
def norm1(self):
return getattr(self, self.norm1_name)
@property
def norm2(self):
return getattr(self, self.norm2_name)
@property
def norm3(self):
return getattr(self, self.norm3_name)
def forward(self, x):
identity = x
out = self.conv1(x)
out = self.norm1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.norm2(out)
out = self.relu(out)
out = self.conv3(out)
out = self.norm3(out)
out = self.se(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
def make_res_layer(block,
inplanes,
planes,
blocks,
stride=1,
dilation=1,
conv_cfg=None,
norm_cfg=dict(type='BN')):
downsample = None
if stride != 1 or inplanes != planes * block.expansion:
downsample = nn.Sequential(
build_conv_layer(
conv_cfg,
inplanes,
planes * block.expansion,
kernel_size=1,
stride=stride,
bias=False),
build_norm_layer(norm_cfg, planes * block.expansion)[1],
)
layers = []
layers.append(
block(
inplanes=inplanes,
planes=planes,
stride=stride,
dilation=dilation,
downsample=downsample,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg))
inplanes = planes * block.expansion
for i in range(1, blocks):
layers.append(
block(
inplanes=inplanes,
planes=planes,
stride=1,
dilation=dilation,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg))
return nn.Sequential(*layers)
@BACKBONES.register_module
class ResNetSE(nn.Module):
arch_settings = {
50: (Bottleneck, (3, 4, 6, 3)),
101: (Bottleneck, (3, 4, 23, 3)),
152: (Bottleneck, (3, 8, 36, 3))
}
def __init__(self,
depth,
in_channels=3,
num_stages=4,
strides=(1, 2, 2, 2),
dilations=(1, 1, 1, 1),
out_indices=(0, 1, 2, 3),
frozen_stages=-1,
conv_cfg=None,
norm_cfg=dict(type='BN', requires_grad=True),
norm_eval=True,
zero_init_residual=True):
super(ResNetSE, self).__init__()
self.depth = depth
self.strides = strides
self.dilations = dilations
self.out_indices = out_indices
self.frozen_stages = frozen_stages
self.conv_cfg = conv_cfg
self.norm_cfg = norm_cfg
self.norm_eval = norm_eval
self.zero_init_residual = zero_init_residual
self.block, stage_blocks = self.arch_settings[depth]
self.stage_blocks = stage_blocks[:num_stages]
self.inplanes = 64
self._make_stem_layer(in_channels)
self.res_layers = []
for i, num_blocks in enumerate(self.stage_blocks):
stride = strides[i]
dilation = dilations[i]
planes = 64 * 2**i
res_layer = make_res_layer(
self.block,
self.inplanes,
planes,
num_blocks,
stride=stride,
dilation=dilation,
conv_cfg=conv_cfg,
norm_cfg=norm_cfg)
self.inplanes = planes * self.block.expansion
layer_name = 'layer{}'.format(i + 1)
self.add_module(layer_name, res_layer)
self.res_layers.append(layer_name)
self._freeze_stages()
self.feat_dim = self.block.expansion * 64 * 2**(
len(self.stage_blocks) - 1)
@property
def norm1(self):
return getattr(self, self.norm1_name)
def _make_stem_layer(self, in_channels):
self.conv1 = build_conv_layer(
self.conv_cfg,
in_channels,
64,
kernel_size=7,
stride=2,
padding=3,
bias=False)
self.norm1_name, norm1 = build_norm_layer(self.norm_cfg, 64, postfix=1)
self.add_module(self.norm1_name, norm1)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
def _freeze_stages(self):
if self.frozen_stages >= 0:
self.norm1.eval()
for m in [self.conv1, self.norm1]:
for param in m.parameters():
param.requires_grad = False
for i in range(1, self.frozen_stages + 1):
m = getattr(self, 'layer{}'.format(i))
m.eval()
for param in m.parameters():
param.requires_grad = False
def init_weights(self, pretrained=None):
if isinstance(pretrained, str):
checkpoint = torch.load(pretrained)
param_dict = {}
for k, v in zip(self.state_dict().keys(), checkpoint['state_dict'].keys()):
param_dict[k] = checkpoint['state_dict'][v]
self.load_state_dict(param_dict)
elif pretrained is None:
for m in self.modules():
if isinstance(m, nn.Conv2d):
kaiming_init(m)
elif isinstance(m, (_BatchNorm, nn.GroupNorm)):
constant_init(m, 1)
if self.zero_init_residual:
for m in self.modules():
if isinstance(m, Bottleneck):
constant_init(m.norm3, 0)
else:
raise TypeError('pretrained must be a str or None')
def forward(self, x):
x = self.conv1(x)
x = self.norm1(x)
x = self.relu(x)
x = self.maxpool(x)
outs = []
for i, layer_name in enumerate(self.res_layers):
res_layer = getattr(self, layer_name)
x = res_layer(x)
if i in self.out_indices:
outs.append(x)
return tuple(outs)
def train(self, mode=True):
super(ResNetSE, self).train(mode)
self._freeze_stages()
if mode and self.norm_eval:
for m in self.modules():
# trick: eval have effect on BatchNorm only
if isinstance(m, _BatchNorm):
m.eval()
Note that backbone文件會與pytorch的model文件略有不同,因爲目標檢測和實例分割還需要在數據集上finetune等等,結合mmdetection庫,修改model文件的細節如下所示:
- 所有創建conv的操作都由
nn.Conv2d
變爲build_conv_layer
,並且第一個參數是conv_cfg
- 所有創建batch_norm的操作都由
nn.BatchNorm2d
變爲build_norm_layer
,並且都要使用self.add_module(self.norm_name, norm)
和@property
來進行索引。 - 加入參數凍結函數:
_freeze_stages
3.修改配置文件
open-mmlab/mmdetection/tree/master/mmdet/models/backbones
裏放入配置文件,以mask_rcnn_r50_fpn_1x.py
爲模板,注意修改如下部分的內容:
model = dict(
type='MaskRCNN',
pretrained='torchvision://resnet50', # 預訓練的模型文件
backbone=dict(
type='ResNetSE', # 模型名字,下面的參數與model文件參數一致
depth=50, #
num_stages=4,
out_indices=(0, 1, 2, 3),
frozen_stages=1), # frozen_stages表示凍結的階段編號
neck=dict(
type='FPN',
in_channels=[256, 512, 1024, 2048], # 這裏需要修改對應out_indices每個階段輸出的channel
out_channels=256,
num_outs=5),
...
訓練細節部分:
(1)對於8GPUS* 2imgs=16imgs/batch設置,初始學習率爲 0.02。若每個batch處理的img數量不同,則需要調整,比如2GPUS*2imgs=4imgs/batch,則初始學習率爲0.005.
(2)finetune一共有2種epoch數量設置,12epochs和24epochs。兩種方法需要調整lr_config
中的step參數,分別爲[8,11]
和[16,22]
。
optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)
optimizer_config = dict(grad_clip=dict(max_norm=35, norm_type=2))
# learning policy
lr_config = dict(
policy='step',
warmup='linear',
warmup_iters=500,
warmup_ratio=1.0 / 3,
step=[8, 11])
...
total_epochs = 12