You Only Look One-level Feature
摘要 :
本文重新审视了用于一级检测器的特征金字塔网络 (FPN),并指出 FPN 的成功在于其对目标检测中的优化问题的分而治之的解决方案,而不是多尺度特征融合。从优化的角度来看,我们引入了一种替代方法来解决问题,而不是采用复杂的特征金字塔——仅利用一层特征进行检测。基于简单高效的解决方案,我们提出了 You Only Look One-level Feature (YOLOF)。在我们的方法中,提出了两个关键组件,扩张编码器和均匀匹配,并带来了相当大的改进。在 COCO 基准上的大量实验证明了所提出模型的有效性。我们的 YOLOF 实现了与其特征金字塔对应的 RetinaNet 相当的结果,同时速度提高了 2.5 倍。在没有变换器层的情况下,YOLOF 可以以单级特征方式与 7×less 训练时期的 DETR 性能相匹配。在608×608的图像尺寸下,YOLOF在2080Ti上以60 fps运行达到44.3 mAP,比YOLOv4快13%
特征金字塔网络(FPN)的好处:
- 多尺度特征融合:融合多个低分辨率和高分辨率特征输入以获得更好的表示;
- 分而治之:根据对象的尺度检测不同级别的对象
NAS-FPN
SSD [26] 首先利用多尺度特征并在每个尺度上对不同尺度的对象进行对象检测。 FPN [22] 遵循 SSD [26] 和 UNet [33],通过结合浅层特征和深层特征构建语义丰富的特征金字塔。
MIMO效果最好,其次SISO,MISO,SISO.
SISO编码的两个问题:
第一个问题是与 C5 特征的感受野匹配的尺度范围是有限的,这阻碍了对不同尺度的物体的检测性能。
第二个是单层特征中稀疏anchor提出的positive anchors的不平衡问题。
class Bottleneck(nn.Module):
def __init__(self,
in_channels: int = 512,
mid_channels: int = 128,
dilation: int = 1,
norm_type: str = 'BN',
act_type: str = 'ReLU'):
super(Bottleneck, self).__init__()
self.conv1 = nn.Sequential(
nn.Conv2d(in_channels, mid_channels, kernel_size=1, padding=0),
get_norm(norm_type, mid_channels),
get_activation(act_type)
)
self.conv2 = nn.Sequential(
nn.Conv2d(mid_channels, mid_channels,
kernel_size=3, padding=dilation, dilation=dilation),
get_norm(norm_type, mid_channels),
get_activation(act_type)
)
self.conv3 = nn.Sequential(
nn.Conv2d(mid_channels, in_channels, kernel_size=1, padding=0),
get_norm(norm_type, in_channels),
get_activation(act_type)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x
out = self.conv1(x)
out = self.conv2(out)
out = self.conv3(out)
out = out + identity
return out
Dilated Encoder。它包含两个主要组件:Projector 和 Residual Blocks。投影层首先应用一个 1×1 卷积层来降低通道维度,然后添加一个 3×3 卷积层来细化语义上下文,这与 FPN [22] 中的相同。之后,我们在 3×3 卷积层中堆叠四个具有不同扩张率的连续扩张残差块,以生成具有多个感受野的输出特征,覆盖所有对象的尺度。
扩张卷积是一种在目标检测中扩大特征感受野的常用策略.
Residual Blocks目的是在主干之外生成具有多个感受野的特征。
YOLOF
FFN层
前馈神经网络没法输入 8 个矩阵呀,这该怎么办呢?所以我们需要一种方式,把 8 个矩阵降为 1 个,首先,我们把 8 个矩阵连在一起,这样会得到一个大的矩阵,再随机初始化一个矩阵和这个组合好的矩阵相乘,最后得到一个最终的矩阵。
Group Normalization(GN)
批量归一化BN是计算机视觉发展中很重要的组成部分,BN是在一个batch中计算均值和方差,BN可以简化并优化使得非常深的网络能够收敛。但是BN却很受batch大小的影响,通过实验证明:BN需要一个足够大的批量,小的批量大小会导致对批统计数据的不准确率提高,显著增加模型的错误率。比如在检测、分割、视频识别等任务中,比如在faster R-cnn或mask R-cnn框架中使用一个batchsize为1或2的图像,因为分辨率更高,其中BN被“冻结”转换为线性层;在3D卷积的视频分类中,空间时间特征的存在引入了时间长度和批处理大小之间的权衡。BN的使用经常会使这些系统在模型符号和批量大小之间做出妥协。
在很宽的批处理大小中,GN表现很稳定,在一个2个样本的批量大小情况下。ImageNet的ResNet-50中,GN的错误比BN要低10.6%。使用常规的批处理大小,GN性能yuBN相当,并优于其他标准化的变体。
上图为四种归一化方法,其中N为批量,C为通道,(H,W)表示feature map,蓝色像素代表相同的均值和方差归一化。
如果我们将组号设置为G = C(即每组一个通道),则GN变为IN。 但是IN只能依靠空间维度来计算均值和方差,并且错过了利用信道依赖的机会。
注:四种归一化的理解
-
BatchNorm:batch方向做归一化,计算NHW的均值
-
LayerNorm:channel方向做归一化,计算CHW的均值
-
InstanceNorm:一个channel内做归一化,计算H*W的均值
-
GroupNorm:先将channel方向分group,然后每个group内做归一化,计算(C//G)HW的均值
-
GN与LN和IN有关,这两种标准化方法在训练循环(RNN / LSTM)或生成(GAN)模型方面特别成功。
DETR [4] 是最近提出的检测器,它将变压器 [39] 引入对象检测。它在 COCO 基准测试 [24] 上取得了令人惊讶的结果,并证明仅采用单个 C5 特征,就可以与多级特征检测器(Faster R-CNN w/ FPN [22])取得可比的结果。
DETR在定位上的误差比YOLOF大,这可能与其回归机制有关。 DETR 以完全无锚的方式回归对象并在图像中全局预测位置,这导致定位困难。相比之下,YOLOF 依赖于预定义的锚点,这在预测中导致比 DETR [4] 更高的丢失错误。根据4.2节的分析,YOLOF的anchors稀疏且在推理阶段不够灵活。
anchor-free 机制引入 YOLOF 可能有助于缓解这个问题,
在这项工作中,我们发现 FPN 的成功归功于它对密集目标检测中优化问题的分而治之的解决方案。
encoder
class DilatedEncoder(nn.Module):
"""
Dilated Encoder for YOLOF.
This module contains two types of components:
- the original FPN lateral convolution layer and fpn convolution layer,
which are 1x1 conv + 3x3 conv
- the dilated residual block
"""
def __init__(self, cfg, input_shape: List[ShapeSpec]):
super(DilatedEncoder, self).__init__()
# fmt: off
self.backbone_level = cfg.MODEL.YOLOF.ENCODER.BACKBONE_LEVEL
self.in_channels = cfg.MODEL.YOLOF.ENCODER.IN_CHANNELS
self.encoder_channels = cfg.MODEL.YOLOF.ENCODER.NUM_CHANNELS
self.block_mid_channels = cfg.MODEL.YOLOF.ENCODER.BLOCK_MID_CHANNELS
self.num_residual_blocks = cfg.MODEL.YOLOF.ENCODER.NUM_RESIDUAL_BLOCKS
self.block_dilations = cfg.MODEL.YOLOF.ENCODER.BLOCK_DILATIONS
self.norm_type = cfg.MODEL.YOLOF.ENCODER.NORM
self.act_type = cfg.MODEL.YOLOF.ENCODER.ACTIVATION
# fmt: on
assert input_shape[self.backbone_level].channels == self.in_channels
assert len(self.block_dilations) == self.num_residual_blocks
# init
self._init_layers()
self._init_weight()
def _init_layers(self):
self.lateral_conv = nn.Conv2d(self.in_channels,
self.encoder_channels,
kernel_size=1)
self.lateral_norm = get_norm(self.norm_type, self.encoder_channels)
self.fpn_conv = nn.Conv2d(self.encoder_channels,
self.encoder_channels,
kernel_size=3,
padding=1)
self.fpn_norm = get_norm(self.norm_type, self.encoder_channels)
encoder_blocks = []
for i in range(self.num_residual_blocks):
dilation = self.block_dilations[i]
encoder_blocks.append(
Bottleneck(
self.encoder_channels,
self.block_mid_channels,
dilation=dilation,
norm_type=self.norm_type,
act_type=self.act_type
)
)
self.dilated_encoder_blocks = nn.Sequential(*encoder_blocks)
def _init_weight(self):
c2_xavier_fill(self.lateral_conv)
c2_xavier_fill(self.fpn_conv)
for m in [self.lateral_norm, self.fpn_norm]:
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
for m in self.dilated_encoder_blocks.modules():
if isinstance(m, nn.Conv2d):
nn.init.normal_(m.weight, mean=0, std=0.01)
if hasattr(m, 'bias') and m.bias is not None:
nn.init.constant_(m.bias, 0)
if isinstance(m, (nn.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
def forward(self, feature: torch.Tensor) -> torch.Tensor:
out = self.lateral_norm(self.lateral_conv(feature))
out = self.fpn_norm(self.fpn_conv(out))
return self.dilated_encoder_blocks(out)
decoder
class Decoder(nn.Module):
"""
Head Decoder for YOLOF.
This module contains two types of components:
- A classification head with two 3x3 convolutions and one
classification 3x3 convolution
- A regression head with four 3x3 convolutions, one regression 3x3
convolution, and one implicit objectness 3x3 convolution
"""
def __init__(self, cfg):
super(Decoder, self).__init__()
# fmt: off
self.in_channels = cfg.MODEL.YOLOF.DECODER.IN_CHANNELS
self.num_classes = cfg.MODEL.YOLOF.DECODER.NUM_CLASSES
self.num_anchors = cfg.MODEL.YOLOF.DECODER.NUM_ANCHORS
self.cls_num_convs = cfg.MODEL.YOLOF.DECODER.CLS_NUM_CONVS
self.reg_num_convs = cfg.MODEL.YOLOF.DECODER.REG_NUM_CONVS
self.norm_type = cfg.MODEL.YOLOF.DECODER.NORM
self.act_type = cfg.MODEL.YOLOF.DECODER.ACTIVATION
self.prior_prob = cfg.MODEL.YOLOF.DECODER.PRIOR_PROB
# fmt: on
self.INF = 1e8
# init
self._init_layers()
self._init_weight()
def _init_layers(self):
cls_subnet = []
bbox_subnet = []
for i in range(self.cls_num_convs):
cls_subnet.append(
nn.Conv2d(self.in_channels,
self.in_channels,
kernel_size=3,
stride=1,
padding=1))
cls_subnet.append(get_norm(self.norm_type, self.in_channels))
cls_subnet.append(get_activation(self.act_type))
for i in range(self.reg_num_convs):
bbox_subnet.append(
nn.Conv2d(self.in_channels,
self.in_channels,
kernel_size=3,
stride=1,
padding=1))
bbox_subnet.append(get_norm(self.norm_type, self.in_channels))
bbox_subnet.append(get_activation(self.act_type))
self.cls_subnet = nn.Sequential(*cls_subnet)
self.bbox_subnet = nn.Sequential(*bbox_subnet)
self.cls_score = nn.Conv2d(self.in_channels,
self.num_anchors * self.num_classes,
kernel_size=3,
stride=1,
padding=1)
self.bbox_pred = nn.Conv2d(self.in_channels,
self.num_anchors * 4,
kernel_size=3,
stride=1,
padding=1)
self.object_pred = nn.Conv2d(self.in_channels,
self.num_anchors,
kernel_size=3,
stride=1,
padding=1)
def _init_weight(self):
for m in self.modules():
if isinstance(m, nn.Conv2d):
nn.init.normal_(m.weight, mean=0, std=0.01)
if hasattr(m, 'bias') and m.bias is not None:
nn.init.constant_(m.bias, 0)
if isinstance(m, (nn.GroupNorm, nn.BatchNorm2d, nn.SyncBatchNorm)):
nn.init.constant_(m.weight, 1)
nn.init.constant_(m.bias, 0)
# Use prior in model initialization to improve stability
bias_value = -math.log((1 - self.prior_prob) / self.prior_prob)
torch.nn.init.constant_(self.cls_score.bias, bias_value)
def forward(self,
feature: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
cls_score = self.cls_score(self.cls_subnet(feature))
N, _, H, W = cls_score.shape
cls_score = cls_score.view(N, -1, self.num_classes, H, W)
reg_feat = self.bbox_subnet(feature)
bbox_reg = self.bbox_pred(reg_feat)
objectness = self.object_pred(reg_feat)
# implicit objectness
objectness = objectness.view(N, -1, 1, H, W)
normalized_cls_score = cls_score + objectness - torch.log(
1. + torch.clamp(cls_score.exp(), max=self.INF) + torch.clamp(
objectness.exp(), max=self.INF))
normalized_cls_score = normalized_cls_score.view(N, -1, H, W)
return normalized_cls_score, bbox_reg