前言

本文就是大名鼎鼎的focalloss中提出的网络,其基本结构backbone+fpn+head也是目前目标检测算法的标准结构。RetinaNet凭借结构精简,清晰明了、可扩展性强、效果优秀,成为了很多算法的baseline。本文不去过多从理论分析focalloss的机制,从代码角度解析RetinaNet的实现过程,尤其是anchor生成与匹配、loss计算过程。

论文链接:

https://arxiv.org/abs/1708.02002

参考代码链接:

https://github.com/yhenon/pytorch-retinanet

网络结构

网络结构非常清晰明了,使用的组件都是标准公认的,并且容易替换掉的。在这里,你不会看到SSD没有特征融合的多尺度,你也不会看到只有yolo才用的darknet。预测输出就是类别+位置,也是目标检测任务面临的本质。

FPN

这部分无需过多介绍,就是融合不同尺度的特征,融合的方式一般是element-wise相加。当遇到尺度不一致时,利用卷积+上采样操作来处理。为了清晰理解,给出实例:

一般backbone会提取4层特征,尺度分别是,假设batch为1:

c2:1*64*W/4*H/4
c3:1*128*W/8*H/8
c4:1*256*W/16*H/16
c5:1*512*W/32*H/32:

这里只需要后三层特征;假设输入数据为[1,3,320,320],FPN输出的特征维度分别为:

torch.Size([1, 256, 40, 40])
torch.Size([1, 256, 20, 20])
torch.Size([1, 256, 10, 10])
torch.Size([1, 256, 5, 5])
torch.Size([1, 256, 3, 3])

当然FPN是非常容易定制的组件,当你的场景不需要太多尺度的话,可以删减输出分支。

Head

Fpn输出的分支,每一个都会进行分类和回归操作

分类输出

每层特征经过4次卷积+relu操作,然后再通过head 卷积

self.output = nn.Conv2d(feature_size, num_anchors * num_classes, kernel_size=3, padding=1)
self.output_act = nn.Sigmoid()

输出最终预测输出,尺度是

torch.Size([1, 14400, 80])
torch.Size([1, 3600, 80])
torch.Size([1, 900, 80])
torch.Size([1, 225, 80])
torch.Size([1, 81, 80])

其中14400 = 40*40*9,9为anchor个数,最后在把所有结果拼接在一起[1,19206,80]的tensor。可以理解为每一个特征图位置预测9个anchor,每个anchor具有80个类别。拼接操作为了和anchor的形式统一起来,方便计算loss和前向预测。注意,这里的激活函数使用的是sigmoid(),如果你想使用softmax()输出,那么就需要增加一个类别。不过论文证明了Sigmoid()效果要优于softmax().

回归输出

和分类头类似,同样是4层卷积+relu()操作,最后是输出卷积。由于是回归问题,所以没有进行激活操作。

self.output = nn.Conv2d(feature_size, num_anchors * 4, kernel_size=3, padding=1)

尺度变化为:

torch.Size([1, 14400, 4])
torch.Size([1, 3600, 4])
torch.Size([1, 900, 4])
torch.Size([1, 225, 4])
torch.Size([1, 81, 4])

最后在把所有结果拼接在一起[1,19206,4],4代表预测box的中心点+宽高。

Anchor生成

大的特征图预测小的物体,小的特征图预测大的物体,fpn有5个输出,所以会有5中尺度的anchor,每种尺度又分为9中宽高比。

首先定义特征图的level:

self.pyramid_levels = [3, 4, 5, 6, 7]

获取对应stride为:

self.strides = [2 ** x for x in self.pyramid_levels]
# [8,16,32,64,128]

获取每一层上的base size:

self.sizes = [2 ** (x + 2) for x in self.pyramid_levels]
# [32,64,128,256,512]

将3种框高比和3个scale进行搭配,获取9个anchor:

ratios = np.array([0.5, 1, 2])
scales = np.array([2 ** 0, 2 ** (1.0 / 3.0), 2 ** (2.0 / 3.0)])=[1,1.26,1.587]

首先计算大小:

anchors[:, 2:] = base_size * np.tile(scales, (2, len(ratios))).T

获取初步的anchor的宽高 (举例,最小的输出层):

[[ 0.          0.         32.         32.        ][ 0.          0.         40.3174736  40.3174736 ][ 0.          0.         50.79683366 50.79683366][ 0.          0.         32.         32.        ][ 0.          0.         40.3174736  40.3174736 ][ 0.          0.         50.79683366 50.79683366][ 0.          0.         32.         32.        ][ 0.          0.         40.3174736  40.3174736 ][ 0.          0.         50.79683366 50.79683366]]

获取每一种尺度的面积:

[1024. 1625. 2580. 1024. 1625. 2580. 1024. 1625. 2580.]

然后按照宽高比生成anchor:

[[ 0.          0.         45.254834   22.627417  ][ 0.          0.         57.01751796 28.50875898][ 0.          0.         71.83757109 35.91878555][ 0.          0.         32.         32.        ][ 0.          0.         40.3174736  40.3174736 ][ 0.          0.         50.79683366 50.79683366][ 0.          0.         22.627417   45.254834  ][ 0.          0.         28.50875898 57.01751796][ 0.          0.         35.91878555 71.83757109]]

最后转化为xyxy的形式:

[[-22.627417   -11.3137085   22.627417    11.3137085 ][-28.50875898 -14.25437949  28.50875898  14.25437949][-35.91878555 -17.95939277  35.91878555  17.95939277][-16.         -16.          16.          16.        ][-20.1587368  -20.1587368   20.1587368   20.1587368 ][-25.39841683 -25.39841683  25.39841683  25.39841683][-11.3137085  -22.627417    11.3137085   22.627417  ][-14.25437949 -28.50875898  14.25437949  28.50875898][-17.95939277 -35.91878555  17.95939277  35.91878555]]

因此获取了其中一层的base anchor,这组anchor是特征图上位置(0,0)的特征图片,只需要复制+平移到其他位置,就可以获取整张特征图上所有的anchor。其他尺度的特征图做法类似最后将所有特征图上的anchor拼接起来,size同样为为[1,19206,4]

anchor编码

代码没有将anchor编码拆分成一个独立的模块,

首先gt box转化成中心点和宽高的形式:

gt_widths  = assigned_annotations[:, 2] - assigned_annotations[:, 0]
gt_heights = assigned_annotations[:, 3] - assigned_annotations[:, 1]
gt_ctr_x   = assigned_annotations[:, 0] + 0.5 * gt_widths
gt_ctr_y   = assigned_annotations[:, 1] + 0.5 * gt_heights

同理anchor也转换成中心点和宽高的形式:

anchor_widths  = anchor[:, 2] - anchor[:, 0]
anchor_heights = anchor[:, 3] - anchor[:, 1]
anchor_ctr_x   = anchor[:, 0] + 0.5 * anchor_widths
anchor_ctr_y   = anchor[:, 1] + 0.5 * anchor_heights

计算二者的相对值

targets_dx = (gt_ctr_x - anchor_ctr_x_pi) / anchor_widths_pi
targets_dy = (gt_ctr_y - anchor_ctr_y_pi) / anchor_heights_pi
targets_dw = torch.log(gt_widths / anchor_widths_pi)
targets_dh = torch.log(gt_heights / anchor_heights_pi)

当然我们的目标就是网络预测值和这四个相对值相等。

anchor分配

这部分主要是根据iou的大小划分正负样本,既挑出那些负责预测gt的anchor。分配的策略非常简单,就是iou策略。

需要求iou:

IoU_max, IoU_argmax = torch.max(IoU, dim=1) # num_anchors x 1
  1. 正样本:和gt的iou大于0.5的ancho样本

  2. 负样本:和gt的iou小于0.4的anchor

  3. 忽略样本:其他anchor

问题:没有像yolo系列一样,如果没有大于0.5的anchor预测,至少会分配一个iou最大的anchor。因为retinanet认为coco数据集按照此策略,匹配不到的情况非常少。

loss计算

focal loss 请参考:

皮特潘:Focal loss的简单实现(二分类+多分类)zhuanlan.zhihu.com

当图片没有目标时,只计算分类loss,不计算box位置loss,所有anchor都是负样本:

alpha_factor = torch.ones(classification.shape) * alphaalpha_factor = 1. - alpha_factor
focal_weight = classification
focal_weight = alpha_factor * torch.pow(focal_weight, gamma)bce = -(torch.log(1.0 - classification))cls_loss = focal_weight * bce
classification_losses.append(cls_loss.sum())
# 回归loss为0
regression_losses.append(torch.tensor(0).float())

分类loss:

# 注意,这里是利用sigmoid输出,可以直接使用alpha和1-alpha。每一个分支都在做目标和背景的二分类
alpha_factor = torch.where(torch.eq(targets, 1.), alpha_factor, 1. - alpha_factor)
focal_weight = torch.where(torch.eq(targets, 1.), 1. - classification, classification)
focal_weight = alpha_factor * torch.pow(focal_weight, gamma)
bce = -(targets * torch.log(classification) + (1.0 - targets) * torch.log(1.0 - classification))
cls_loss = focal_weight * bce

回归loss:

# 只在正样本的anchor上计算,abs就是f1 loss
regression_diff = torch.abs(targets - regression[positive_indices, :])
# 进行smooth一下,就是smooth l1 loss
regression_loss = torch.where(torch.le(regression_diff, 1.0 / 9.0),0.5 * 9.0 * torch.pow(regression_diff, 2),regression_diff - 0.5 / 9.0)

测试推理

因为测试推理过程一般比较简单,部分代码如下:

def forward(self, boxes, deltas):widths  = boxes[:, :, 2] - boxes[:, :, 0]heights = boxes[:, :, 3] - boxes[:, :, 1]ctr_x   = boxes[:, :, 0] + 0.5 * widthsctr_y   = boxes[:, :, 1] + 0.5 * heightsdx = deltas[:, :, 0] * self.std[0] + self.mean[0]dy = deltas[:, :, 1] * self.std[1] + self.mean[1]dw = deltas[:, :, 2] * self.std[2] + self.mean[2]dh = deltas[:, :, 3] * self.std[3] + self.mean[3]
'''其中boxes为anchor,deltas为网络回归的box分支。
注意这里的self.std[0] + self.mean[0]是对输出的标准化逆向操作,
因为网络输出时的监督有标准化操作。使用的均值和方差是固定数值。
目的是对相对数值进行放大,帮助网络回归'''pred_ctr_x = ctr_x + dx * widthspred_ctr_y = ctr_y + dy * heightspred_w     = torch.exp(dw) * widthspred_h     = torch.exp(dh) * heightspred_boxes_x1 = pred_ctr_x - 0.5 * pred_wpred_boxes_y1 = pred_ctr_y - 0.5 * pred_hpred_boxes_x2 = pred_ctr_x + 0.5 * pred_wpred_boxes_y2 = pred_ctr_y + 0.5 * pred_hpred_boxes = torch.stack([pred_boxes_x1, pred_boxes_y1, pred_boxes_x2, pred_boxes_y2], dim=2)return pred_boxes

解码完成后,获得真实预测的box,还要经过clipBoxes操作,就是保证所有数不会超过图片的尺度范围。然后对每一个类别进行遍历,获取类别的score,提取大于一定阈的box,再进行nms就可以了。没啥。

结语

RetinaNet是一个结构非常清晰的目标检测框架,backbone以及neck的FPN非常容易更换掉,head的定义也非常简单。又有focal loss的加成,成为了很多算法baseline,例如任意角度的目标检测。本文从代码层面进行剖析,希望和大家一起学习。

往期精彩回顾适合初学者入门人工智能的路线及资料下载机器学习及深度学习笔记等资料打印机器学习在线手册深度学习笔记专辑《统计学习方法》的代码复现专辑
AI基础下载机器学习的数学基础专辑
获取本站知识星球优惠券,复制链接直接打开:
https://t.zsxq.com/qFiUFMV
本站qq群704220115。加入微信群请扫码:

【深度学习】RetinaNet 代码完全解析相关推荐

  1. NVIDIA深度学习Tensor Core性能解析(下)

    NVIDIA深度学习Tensor Core性能解析(下) DeepBench推理测试之RNN和Sparse GEMM DeepBench的最后一项推理测试是RNN和Sparse GEMM,虽然测试中可 ...

  2. 【深度学习】VGGNet原理解析及实现

    [深度学习]VGGNet原理解析及实现 VGGNet由牛津大学的视觉几何组(Visual Geometry Group)和Google DeepMind公司的研究员共同提出,是ILSVRC-2014中 ...

  3. NVIDIA深度学习Tensor Core性能解析(上)

    NVIDIA深度学习Tensor Core性能解析(上) 本篇将通过多项测试来考验Volta架构,利用各种深度学习框架来了解Tensor Core的性能. 很多时候,深度学习这样的新领域会让人难以理解 ...

  4. 深度学习目标检测详细解析以及Mask R-CNN示例

    深度学习目标检测详细解析以及Mask R-CNN示例 本文详细介绍了R-CNN走到端到端模型的Faster R-CNN的进化流程,以及典型的示例算法Mask R-CNN模型.算法如何变得更快,更强! ...

  5. 如何阅读一份深度学习项目代码?

    犹豫很久要不要把读代码这个事情专门挑出来写成一篇推文.毕竟读代码嘛,大家可能都会读.而且笔者个人读的和写的代码量也并不足以到指导大家读代码的程度.但笔者还是决定大胆地写一点:就当是给自己设立今后读代码 ...

  6. 深度学习项目代码阅读建议

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达本文转自|机器学习实验室 犹豫很久要不要把读代码这个事情专门挑出来写 ...

  7. 怎样高效阅读一份深度学习项目代码?

    犹豫很久要不要把读代码这个事情专门挑出来写成一篇推文.毕竟读代码嘛,大家可能都会读.而且笔者个人读的和写的代码量也并不足以到指导大家读代码的程度.但笔者还是决定大胆地写一点:就当是给自己设立今后读代码 ...

  8. (一)深度学习项目代码结构

    1.代码结构 参考链接:李宏毅2021年机器学习HW2 Phoneme Classification 2.代码细节 获得运行设备 这两种写法的返回值都是字符串 #check device def ge ...

  9. 深度学习论文 代码复现 环境配置操作

    ***深度学习论文代码复现 前置工作 安装Ubuntu18.04 安装Nvidia显卡驱动 安装anaconda 安装CUDA与cuDNN 通过软链接的修改实现多版本CUDA间的切换 将~/.bash ...

最新文章

  1. 请指出document load和document ready的区别?
  2. Java EE之RMI
  3. 天天沉迷于皇上本宫的都是sb
  4. 【数据结构】顺序线性表的构造和存储数据
  5. uniapp图标_uniapp扩展自定义uniIcon组件图标
  6. input 底线_社区建设如何帮助组织的底线
  7. 使用RestTemplate遇到的问题
  8. IC卡读写器开发说明
  9. 5600高流明更清晰 NEC CF6600U投影试用
  10. 苹果退款_苹果退款有什么影响吗
  11. 游戏开发的专业术语整理
  12. 【屏幕灯】MI电脑显示器灯条用户手册
  13. [Noip2003] 侦探推理
  14. 优秀课程案例:使用Scratch制作打弹球游戏2-得分过关
  15. 【防火墙配置QOS之最小带宽保证】
  16. startactivity后App出现闪退问题情况分析
  17. 'Bullet' object has no attribute 'draw_bullet'
  18. Zimbra黑白名单的配置
  19. 18-12-19 美国7-11连锁店
  20. DO、DTO、BO、AO、VO、POJO

热门文章

  1. 一个时间日期转换格式的小功能(Oracle)
  2. SPHINX 文档写作工具安装简要指南 - windows 版 - 基于python
  3. 51nod1307(暴力树剖/二分dfs/并查集)
  4. [ An Ac a Day ^_^ ] CodeForces 468A 24 Game 构造
  5. Delphi 2009 超前预知!
  6. WEB页面多语言支持解决方案(转自CSDN)
  7. 转载:VMware Workstation 无法连接到虚拟机。
  8. oracle for循环_浅谈Oracle的执行计划
  9. 导出真实表格显示列数不能超过256_平均月薪真有6万5?说说我所知道的金融人真实薪酬...
  10. 光流 | OpenCV中的Lucas-Kanade光流与稠密光流:基于Opencv+Python(附代码)