参考链接: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)
对于两个区域 R R R和 R ′ R^{'} R′,则两个区域的重叠程度overlap计算如下: O ( R , R ′ ) = R ∩ R ′ R ∪ R ′ O(R,R^{'})=\frac{R\cap R^{'}}{R\cup R^{'}} O(R,R′)=R∪R′R∩R′
非极大值抑制(Non-Maximum Suppression又称NMS)
非极大值抑制(NMS)可以看做是局部最大值的搜索问题,把不是极大值的抑制掉,在物体检测上,就是对一个目标有多个标定框,使用极大值抑制算法滤掉多余的标定框。
主流的检测框架:
主要分为两阶段检测器,单阶段检测器。
如上图所示,R-CNN这个物体检查系统可以大致分为四步进行:
高度非线性的深度网络具有很强的建模能力,计算复杂度高仅生成少量Region Proposal;训练需要大量标注数据有监督预训练 +领域特定微调。比较常用的是selective search方法,具有如下特性:
有如下两种方式:
对图像进行分割,每个分割区域生成一个对应的外接矩形框
基于相似度进行层次化地区域合并
将不同大小的Region Proposal缩放到相同大小:227x227, 进行些许扩大以包含少量上下文信息。
缩放分为两大类:
1)各向同性缩放,长宽放缩相同的倍数
2)各向异性缩放, 长宽放缩的倍数不同
不管图片是否扭曲,长宽缩放的比例可能不一样,直接将长宽缩放到227*227,如下图(D)所示
将所有窗口送入Backbone,如预训练的 AlexNet,ResNet等提取特征。一般与训练的backbone需要进行微调finetune,
以最后一个全连接层FC的输出作为特征表示。
分类算法:
边框校准:
使用回归器精细修正候选框位置:对于每一个类,训练一个线性回归模型去判定这个框是否框得完美。
所以我们要学习的目标即为: d x ( P ) d_{x}(P) dx(P), d y ( P ) d_{y}(P) dy(P), d w ( P ) d_{w}(P) dw(P)和 d h ( P ) d_{h}(P) dh(P),统一为 d ∗ ( P ) d_{*}(P) d∗(P),可写为:
d ∗ ( P ) = w ∗ T Φ G A P ( P ) d_{*}(P)=w^{T}_{*}\Phi_{GAP}(P) d∗(P)=w∗TΦGAP(P)
其中 Φ G A P ( P ) \Phi_{GAP}(P) ΦGAP(P)表示proposal P P P经Backbone(AlexNet,VGGNet,ResNet,etc) Global Avg Pool之后的特征向量。
d x ( P ) d_{x}(P) dx(P), d y ( P ) d_{y}(P) dy(P), d w ( P ) d_{w}(P) dw(P)和 d h ( P ) d_{h}(P) dh(P)对应的groud truth为 t ∗ = { t x , t y , t w , t h } t_{*}=\{t_{x},t_{y},t_{w},t_{h}\} t∗={tx,ty,tw,th},那么误差函数写为:
w ∗ = arg min w ∗ 1 N ( t ∗ − w ∗ T Φ G A P ( P ) ) 2 + λ ∣ ∣ w ∗ ∣ ∣ 2 w_{*}=\arg\min_{w_{*}}\frac{1}{N}(t_{*}-w_{*}^{T}\Phi_{GAP}(P))^{2}+\lambda||w_{*}||^{2} w∗=argw∗minN1(t∗−w∗TΦGAP(P))2+λ∣∣w∗∣∣2
目标框groud truth为 G ( G x , G y , G w , G h ) G(G_{x},G_{y},G_{w},G_{h}) G(Gx,Gy,Gw,Gh),所以 { t x , t y , t w , t h } \{t_{x},t_{y},t_{w},t_{h}\} {tx,ty,tw,th}确定值为:
t x = ( G x − P x ) / P w t y = ( G y − P y ) / P h t w = log ( G w / P w ) t h = log ( G h / P h ) t_{x}=(G_{x}-P_{x})/P_{w}\\ t_{y}=(G_{y}-P_{y})/P_{h}\\ t_{w}=\log(G_{w}/P_{w})\\ t_{h}=\log(G_{h}/P_{h}) tx=(Gx−Px)/Pwty=(Gy−Py)/Phtw=log(Gw/Pw)th=log(Gh/Ph)
Note that 只有当Proposal样本和Ground Truth比较接近时(这里取IoU>0.6),才能将其作为训练样本训练我们的线性回归模型,否则会导致训练的回归模型不work。(当Proposal跟G离得较远,就是复杂的非线性问题了,此时用线性回归建模显然不合理)
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之后的每一个像素点对应的局部感受域的中心,如下给定一个例子:
通常情况下,设当前特征图下某位置为 x i + 1 x_{i+1} xi+1,对应于上一个特征图的卷积核中心的位置为 x i x_{i} xi,则有对应关系:
x i = s i ∗ x i + 1 + ⌈ F i − 1 2 ⌉ − P i x_{i}=s_{i}*x_{i+1}+\left \lceil \frac{F_{i}-1}{2} \right \rceil-P_{i} xi=si∗xi+1+⌈2Fi−1⌉−Pi
其中 s i s_{i} si是stride, F i F_{i} Fi是卷积核的尺寸, P i P_{i} Pi是卷积核的padding。一般情况下,可以取 P i = ⌊ F i / 2 ⌋ P_{i}=\left \lfloor F_{i}/{2} \right \rfloor Pi=⌊Fi/2⌋,所以可以化简为: x i = s i ∗ x i + 1 x_{i}=s_{i}*x_{i+1} xi=si∗xi+1
对公式进行级联可以得到: x 0 = ∏ i = 0 L s i ∗ x L + 1 x_{0}=\prod _{i=0}^{L}s_{i*}x_{L+1} x0=i=0∏Lsi∗xL+1
加入了的ROI(Region Of Interest) Pooling层,对每个region都提取一个固定维度的特征表示。相当于特殊的SPP层,RoI层是使用单个尺度的SPP层(不用多个尺度的原因是多个尺度准确率提升不高,但是计算量开销显著)。
RoI Pooling原理
RoI层将每一个候选区域都分为提前定义的 H × W H\times W H×W块。对每个小块做max-pooling,此时每一个将候选区的局部特征映射转变为大小统一的数据,送入下一层。
梯度反向传播:
设 x i x_{i} xi为输入层结点, y i y_{i} yi为输出层的节点.
∂ L ∂ x i = { 0 , i f δ ( i , j ) = F a l s e ∂ L ∂ y j , i f δ ( i , j ) = T r u e \frac{\partial L }{\partial x_{i}}=\left\{\begin{matrix} 0, if\ \delta(i,j)=False \\ \frac{\partial L }{\partial y_{j}},if\ \delta(i,j)=True \end{matrix}\right. ∂xi∂L={0,if δ(i,j)=False∂yj∂L,if δ(i,j)=True
中判决函数 δ ( i , j ) \delta(i,j) δ(i,j)表示 i i i节点是否被 j j j节点选为最大值输出。不被选中有两种可能: x i x_{i} xi不在 y j y_{j} yj范围内,或者 x i x_{i} xi不是最大值.
一个输入节点可能和多个输出节点相连。设 x i x_{i} xi为输入层的节点, y r j y_{rj} yrj为第 r r r个候选区域的第 j j j个输出节点。
∂ L ∂ x i = ∑ r , j δ ( i , r , j ) ∂ L ∂ y r j \frac{\partial L }{\partial x_{i}}=\sum_{r,j}\delta(i,r,j)\frac{\partial L }{\partial y_{rj}} ∂xi∂L=r,j∑δ(i,r,j)∂yrj∂L
多任务
另外,之前RCNN的处理流程是先提proposal,然后CNN提取特征,之后用SVM分类器,最后再做bbox regression,而在Fast-RCNN中,作者巧妙的把bbox regression放进了神经网络内部,与region分类和并成为了一个multi-task模型,实际实验也证明,这两个任务能够共享卷积特征。
边框校准误差:smooth L1 Loss s m o o t h L 1 ( x ) = { 0.5 x x , ∣ x ∣ < 1 ∣ x ∣ − 0.5 , o t h e r w i s e smooth_{L_{1}}(x)=\left\{\begin{matrix} 0.5x^{x},|x|<1\\ |x|-0.5,otherwise \end{matrix}\right. smoothL1(x)={0.5xx,∣x∣<1∣x∣−0.5,otherwise
论文地址:https://arxiv.org/pdf/1703.06870.pdf
实例分割(instance segmentation):对于检测到的每个物体(实例),精确地标记出其每个像素
在Faster R-CNN中增加实例分割模块:RoIPool
→RoIAlign
ROIAlign:https://www.cnblogs.com/wangyong/p/8523814.html
对一张 800 × 800 800\times 800 800×800原图,经过VGG16的处理后,一共stride=32,图片缩小为 25 × 25 25\times 25 25×25。设定原图中有一 665 × 665 665\times 665 665×665的proposal,映射到特征图中的大小:665/32=20.78,即20.78×20.78:
在Faster R-CNN
上增加了Instance Segmentation Head:
首先简单介绍一下全卷积 (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倍到最终结果,结果越来越精细:
具体的,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。
目标检测和分割可使用mmdetection库来实现,代码参考(CUHK,MM Lab):https://github.com/open-mmlab/mmdetection。
需要首先创建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
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文件的细节如下所示:
nn.Conv2d
变为build_conv_layer
,并且第一个参数是conv_cfg
nn.BatchNorm2d
变为build_norm_layer
,并且都要使用self.add_module(self.norm_name, norm)
和@property
来进行索引。_freeze_stages
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