YOLOF:速度和效果均超過YOLOv4的檢測(cè)模型
文末粉絲福利:價(jià)值千元《圖像分類與圖像搜索》特訓(xùn),限時(shí)0.01元拼團(tuán),僅限60人!
1、設(shè)計(jì)了多組實(shí)驗(yàn),深入探討了 FPN 模塊成功的主要因素
2、基于實(shí)驗(yàn)結(jié)論,設(shè)計(jì)了無需 FPN 模塊,單尺度簡(jiǎn)單高效的 Neck 模塊 Dilated Encoder
3、基于 FPN 分治處理多尺度問題,配合 Neck 模塊提出 Uniform Matching 正負(fù)樣本匹配策略
4、由于不存在復(fù)雜且耗內(nèi)存極多的 FPN 模塊,YOLOF 可以在保存高精度的前提下,推理速度快,消耗內(nèi)存也相對(duì)更小
項(xiàng)目地址:github.com/open-mmlab/mmdetection,歡迎 star~
1 FPN 模塊分析

首先目標(biāo)檢測(cè)算法可以簡(jiǎn)單按照上述結(jié)構(gòu)進(jìn)行劃分,網(wǎng)絡(luò)部分主要分為 Backbone、Encoder 和 Decoder,或者按照我們前系列解讀文章劃分方法分為 Backbone、Neck 和 Head。對(duì)于單階段算法來說,常見的 Backbone 是 ResNet,Encoder 或者 Neck 是 FPN,而 Head 就是對(duì)應(yīng)的輸出層結(jié)構(gòu)。
一般我們都認(rèn)為 FPN 層作用非常大,不可或缺,其通過特征多尺度融合,可以有效解決尺度變換預(yù)測(cè)問題。而本文認(rèn)為 FPN 至少有兩個(gè)主要作用:
多尺度特征融合 分治策略,可以將不同大小的物體分配到不同大小的的輸出層上,克服尺度預(yù)測(cè)問題

對(duì) FPN 模塊進(jìn)一步抽象,如上圖所示,可以分成 4 種結(jié)構(gòu) MiMo、SiMo、MiSo 和 SiSo,其中 MiMo 即為標(biāo)準(zhǔn)的 FPN結(jié)構(gòu),輸入和輸出都包括多尺度特征圖。
將 FPN 替換為上述 4 個(gè)模塊,然后基于 RetinaNet 重新訓(xùn)練,計(jì)算 mAP 、 GFLOPs 和 FPS 指標(biāo)
從 mAP 角度分析,SiMo 結(jié)果和 MiMo 差距不大,說明 C5 (Backbone 輸出)包含了足夠的檢測(cè)不同尺度目標(biāo)的上下文信息;而 MiSo 和 SiSo 則和 MiMo 差距較大,說明 FPN 分治優(yōu)化作用遠(yuǎn)遠(yuǎn)大于 多尺度特征融合
從下表 GFLOPs 和 FPS 可以看出,MiMo 結(jié)構(gòu)由于存在高分辨率特征圖 C3 會(huì)帶來較大的計(jì)算量,并且拖慢速度
FPN 模塊的主要增益來自于其分治優(yōu)化手段,而不是多尺度特征融合
FPN 模塊中存在高分辨率特征融合過程,導(dǎo)致消耗內(nèi)存比較多,訓(xùn)練和推理速度也比較慢,對(duì)部署不太優(yōu)化
如果想在拋棄 FPN 模塊的前提下精度不丟失,那么主要問題是提供分治優(yōu)化替代手段
2 YOLOF 原理簡(jiǎn)析

如果僅僅使用 C5 特征,會(huì)出現(xiàn)圖(a)所示的情況
若使用空洞卷積操作來增大 C5 特征圖的感受野,則會(huì)出現(xiàn)圖(b)所示的情況,感受野變大,能夠有效地表達(dá)尺寸較大的目標(biāo),但是對(duì)小目標(biāo)表達(dá)能力會(huì)變差
如果采用不同空洞率的疊加,則可以有效避免上述問題


前面說過 FPN 的核心功能是分治手段,但是我們知道雖然其輸出多個(gè)尺度特征圖,但是要想發(fā)揮分治功能則主要依靠 bbox 正負(fù)樣本分配策略,也就是說 FPN 和優(yōu)異的 bbox 正負(fù)樣本分配策略結(jié)合才能最大程度發(fā)揮功效,大部分最新的單階段目標(biāo)檢測(cè)算法都在 bbox 分配策略上面做文章,可以借用 AutoAssign 論文中的圖說明:

一般來說,由于自然場(chǎng)景中,大小物體分布本身就不均勻,并且大物體在圖片中所占區(qū)域較大,如果不設(shè)計(jì)好,會(huì)導(dǎo)致大物體的正樣本數(shù)遠(yuǎn)遠(yuǎn)多于小物體,最終性能就會(huì)偏向大物體,導(dǎo)致整體性能較差。YOLOF 算法采用單尺度特征圖輸出,錨點(diǎn)的數(shù)量會(huì)大量的減少(比如從 100K 減少到 5K),導(dǎo)致了稀疏錨點(diǎn),如果不進(jìn)行重新設(shè)計(jì),會(huì)加劇上述現(xiàn)象。為此作者提出了新的均勻匹配策略,核心思想就是不同大小物體都盡量有相同數(shù)目的正樣本。

所提兩個(gè)模塊的作用如下所示:

Uniform Matching 作用非常大,說明該模塊其實(shí)發(fā)揮了 FPN 的分治作用
Dilated Encoder 配合 Uniform Matching 可以提供額外的變感受野功能,有助于多尺度物體預(yù)測(cè)
3 YOLOF 源碼解析
3.1 BackboneBackbone
pretrained='open-mmlab://detectron/resnet50_caffe',
backbone=dict(
type='ResNet',
depth=50,
num_stages=4,
out_indices=(3, ),
frozen_stages=1,
norm_cfg=dict(type='BN', requires_grad=False),
norm_eval=True,
style='caffe'),neck=dict(
type='DilatedEncoder',
in_channels=2048,
out_channels=512,
block_mid_channels=128,
num_residual_blocks=4),3.3 Head

def forward_single(self, feature):
# 分類分支
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=INF) +
torch.clamp(objectness.exp(), max=INF))
normalized_cls_score = normalized_cls_score.view(N, -1, H, W)
return normalized_cls_score, bbox_regimport torch
if __name__ == '__main__':
INF = 1e8
N = 1
num_classes = 2
H = W = 3
cls_score = torch.rand((N, 1, num_classes, H, W))
objectness = torch.rand(N, 1, 1, H, W)
normalized_cls_score = cls_score + objectness - torch.log(
1. + torch.clamp(cls_score.exp(), max=INF) +
torch.clamp(objectness.exp(), max=INF))
cls_score_s = torch.sigmoid(cls_score) * torch.sigmoid(objectness)
assert torch.allclose(cls_score_s, torch.sigmoid(normalized_cls_score))3.4 Bbox
anchor_generator=dict(
type='AnchorGenerator',
ratios=[1.0],
scales=[1, 2, 4, 8, 16],
strides=[32]),
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[.0, .0, .0, .0],
target_stds=[1., 1., 1., 1.],
add_ctr_clamp=True,
ctr_clamp=32),
3.5 Bbox Assigner
這個(gè)部分是 YOLOF 的核心,需要重點(diǎn)分析。首先分析論文中描述,然后再基于代碼說明代碼和論文的差異。論文中描述的非常簡(jiǎn)單,核心目的是保證不同尺度物體都盡可能有相同數(shù)目的正樣本
遍歷每個(gè) gt bbox,然后選擇 topk 個(gè)距離最近的 anchor 作為其匹配的正樣本
由于存在極端比例物體和小物體,上述強(qiáng)制 topk 操作可能出現(xiàn) anchor 和 gt bbox 的不匹配現(xiàn)象,為了防止噪聲樣本影響,在所有正樣本點(diǎn)中,將 anchor 和 gt bbox 的 iou 低于 0.15 的正樣本(因?yàn)椴还芷ヅ淝闆r,topk 都會(huì)選擇出指定數(shù)目的正樣本)強(qiáng)制認(rèn)為是忽略樣本,在所有負(fù)樣本點(diǎn)中,將 anchor 和 gt bbox 的 iou 高于 0.75 的負(fù)樣本(可能該物體比較大,導(dǎo)致很多 anchor 都能夠和該 gt bbox 很好的匹配,這些樣本就不適合作為負(fù)樣本了)強(qiáng)制認(rèn)為是忽略樣本
實(shí)際上作者代碼的寫法如下所示
遍歷每個(gè) gt bbox,然后選擇 topk 個(gè)距離最近的 anchor 作為其匹配的正樣本
遍歷每個(gè) gt bbox,然后選擇 topk 個(gè)距離最近的預(yù)測(cè)框作為補(bǔ)充的匹配正樣本
計(jì)算 gt bbox 和預(yù)測(cè)框的 iou,在所有負(fù)樣本點(diǎn)中,將 iou 高于 0.75 的負(fù)樣本強(qiáng)制認(rèn)為是忽略樣本
計(jì)算 gt bbox 和 anchor 的 iou,在所有正樣本點(diǎn)中,將 iou 低于 0.15 的正樣本強(qiáng)制認(rèn)為是忽略樣本
可以發(fā)現(xiàn)相比于論文描述,實(shí)際上代碼額外動(dòng)態(tài)補(bǔ)充了一定量的正樣本,同時(shí)也額外考慮了一些忽略樣本。相比于純粹采用 anchor 和 gt bbox 進(jìn)行匹配,額外引入預(yù)測(cè)框,可以動(dòng)態(tài)調(diào)整正負(fù)樣本,理論上會(huì)更好。
# 全部任務(wù)是負(fù)樣本
assigned_gt_inds = bbox_pred.new_full((num_bboxes, ),
0,
dtype=torch.long)
# 計(jì)算兩兩直接的距離,包括 預(yù)測(cè)框和 gt bbox,以及 anchor 和 gt bbox
cost_bbox = torch.cdist(
bbox_xyxy_to_cxcywh(bbox_pred),
bbox_xyxy_to_cxcywh(gt_bboxes),
p=1)
cost_bbox_anchors = torch.cdist(
bbox_xyxy_to_cxcywh(anchor), bbox_xyxy_to_cxcywh(gt_bboxes), p=1)
# 分別提取 topk 個(gè)樣本點(diǎn)作為正樣本,此時(shí)正樣本數(shù)會(huì)加倍
index = torch.topk(
C,
k=self.match_times,
dim=0,
largest=False)[1]
# self.match_times x n
index1 = torch.topk(C1, k=self.match_times, dim=0, largest=False)[1]
# (self.match_times*2) x n
indexes = torch.cat((index, index1),
dim=1).reshape(-1).to(bbox_pred.device)
# 計(jì)算 iou 矩陣
pred_overlaps = self.iou_calculator(bbox_pred, gt_bboxes)
anchor_overlaps = self.iou_calculator(anchor, gt_bboxes)
pred_max_overlaps, _ = pred_overlaps.max(dim=1)
anchor_max_overlaps, _ = anchor_overlaps.max(dim=0)
# 計(jì)算 gt bbox 和預(yù)測(cè)框的 iou,在所有負(fù)樣本點(diǎn)中,將 iou 高于 0.75 的負(fù)樣本強(qiáng)制認(rèn)為是忽略樣本
ignore_idx = pred_max_overlaps > self.neg_ignore_thr
assigned_gt_inds[ignore_idx] = -1
# 計(jì)算 gt bbox 和 anchor 的 iou,在所有正樣本點(diǎn)中,將 iou 低于 0.15 的正樣本強(qiáng)制認(rèn)為是忽略樣本
pos_gt_index = torch.arange(
0, C1.size(1),
device=bbox_pred.device).repeat(self.match_times * 2)
pos_ious = anchor_overlaps[indexes, pos_gt_index]
pos_ignore_idx = pos_ious < self.pos_ignore_thr
pos_gt_index_with_ignore = pos_gt_index + 1
pos_gt_index_with_ignore[pos_ignore_idx] = -1
assigned_gt_inds[indexes] = pos_gt_index_with_ignore
3.6 Loss
在確定了每個(gè)特征點(diǎn)位置哪些是正樣本和負(fù)樣本后,就可以計(jì)算 loss 了,分類采用 focal loss,回歸采用 giou loss,都是常規(guī)操作。
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.25,
loss_weight=1.0),
loss_bbox=dict(type='GIoULoss', loss_weight=1.0))上述就是整個(gè) YOLOF 核心實(shí)現(xiàn)過程。至于推理過程和 RetinaNet 算法完全相同。
4 YOLOF 復(fù)現(xiàn)心得和體會(huì)
如果不仔細(xì)思考,可能看不出上述代碼有啥問題,實(shí)際上在 Bbox Assigner 環(huán)節(jié)會(huì)存在重復(fù)索引分配問題,這個(gè)問題會(huì)帶來幾個(gè)影響。具體代碼是:
# 對(duì)應(yīng) 3.5 小節(jié)的源碼分析第 44 行
assigned_gt_inds[indexes] = pos_gt_index_with_ignore舉個(gè)簡(jiǎn)單例子,當(dāng)前圖片中僅僅有一個(gè) gt bbox,且預(yù)測(cè)輸出特征圖大小是 10x10,設(shè)置 anchor 個(gè)數(shù)是 1,那么說明輸出特征圖上只有 10x10 個(gè)anchor,并且對(duì)應(yīng)了 10x10 個(gè)預(yù)測(cè)框,topk 設(shè)置為 4
計(jì)算該 gt bbox 和 100 個(gè) anchor 的距離,然后選擇最近的前 4 個(gè)位置作為正樣本
計(jì)算該 gt bbox 和 100 個(gè)預(yù)測(cè)框的距離,然后選擇最近的前 4 個(gè)位置作為正樣本,注意這里選擇的 4個(gè)位置很可能和前面選擇的 4 個(gè)位置有重復(fù)
計(jì)算該 gt bbox 和預(yù)測(cè)框的 iou,在所有負(fù)樣本點(diǎn)中,將 iou 高于 0.75 的負(fù)樣本強(qiáng)制認(rèn)為是忽略樣本
計(jì)算該 gt bbox 和 anchor 的 iou,在所有正樣本點(diǎn)中,將 iou 低于 0.15 的正樣本強(qiáng)制認(rèn)為是忽略樣本,注意和上一步的區(qū)別,由于 iou 計(jì)算的輸入是不一樣的,可能導(dǎo)致某個(gè)被重復(fù)計(jì)算的正樣本位置出現(xiàn) 2 種情況:1. 兩個(gè)步驟都認(rèn)為是忽略樣本;2. 一個(gè)認(rèn)為是忽略樣本,一個(gè)認(rèn)為是正樣本,而一旦出現(xiàn)第二種情況則在 CUDA 并行計(jì)算中出現(xiàn)不確定輸出
如果兩個(gè)重復(fù)索引處對(duì)應(yīng)的 gt bbox 是同一個(gè),那么相當(dāng)于該 gt bbox 對(duì)應(yīng)的正樣本 loss 權(quán)重加倍
如果兩個(gè)重復(fù)索引處對(duì)應(yīng)的 gt bbox 不是同一個(gè),那么就會(huì)出現(xiàn)歧義,因?yàn)樘卣鲌D上同一個(gè)預(yù)測(cè)點(diǎn),被同時(shí)分配給了兩個(gè)不同的 gt bbox
讀者理解代碼運(yùn)行流程會(huì)比較困惑
同一個(gè)程序跑多次,可能輸出結(jié)果不一致
訓(xùn)練過程不穩(wěn)定
當(dāng)重復(fù)索引出現(xiàn)時(shí)候,回歸分支 loss 計(jì)算過程非常奇怪,難以理解
低版本 CUDA 上會(huì)出現(xiàn)非法內(nèi)存越界錯(cuò)誤, 實(shí)驗(yàn)發(fā)現(xiàn) CUDA9.0 會(huì)出現(xiàn)非法內(nèi)存越界錯(cuò)誤,但是 CUDA10.1 則正常,其余版本沒有進(jìn)行測(cè)試
上述這個(gè)寫法,給代碼復(fù)現(xiàn)帶來了些問題,并且由于 YOLOF 學(xué)習(xí)率非常高 lr=0.12,訓(xùn)練過程偶爾會(huì)出現(xiàn) Nan 現(xiàn)象,訓(xùn)練不太穩(wěn)定,可能對(duì)參數(shù)設(shè)置例如 warmup 比較敏感。
最后還是要感謝作者 Qiang Chen,在復(fù)現(xiàn)過程中解答了一些疑問。通過針對(duì)訓(xùn)練不穩(wěn)定的問題,也指明了可能解決的方案。
(粉絲福利來啦)
七月在線 【圖像分類與圖像搜索】特訓(xùn) 0.01元秒殺
僅限 60 人!
課程詳情如下:
本課程是CV高級(jí)小班的前期預(yù)習(xí)課之一,主要內(nèi)容包含卷積神經(jīng)網(wǎng)絡(luò)基礎(chǔ)知識(shí)、卷積網(wǎng)絡(luò)結(jié)構(gòu)、反向傳播、圖像特征提取、三元組損失等理論,以及目標(biāo)檢測(cè)和圖像搜索實(shí)戰(zhàn)項(xiàng)目,理論和實(shí)戰(zhàn)結(jié)合,打好計(jì)算機(jī)視覺基礎(chǔ)。
戳↓↓“閱讀原文”0.01元秒殺!





