首页 网站首页 商业资讯 流程 查看内容

YOLOX 在 MMDetection 中复现全流程解析

共生网络 流程 2023-1-7 12:29 9097人围观

0 摘要

比来 YOLOX 火爆全网,速度和精度相比 YOLOv3、v4 都有了大幅提升,而且提出了很多通用性的 trick,同时供给了摆设相关剧本,适用性极强。 MMDetection 开源团队成员也构造停止了相关复现。

在本次复现进程中,有5位社区成员介入进献:

  • HAOCHENYE :https://github.com/HAOCHENYE
  • xiaohu2015 :https://github.com/xiaohu2015
  • HsLOL :https://github.com/HsLOL
  • shinya7y :https://github.com/shinya7y
  • zhiqwang :https://github.com/zhiqwang

首先很是感激几位社区成员的进献!

经过协同开辟,不但让复现进程加倍高效,而且社区成员在介入进程中可以不竭熟悉算法,熟悉 MMDetection 开辟形式。后续我们也会再次构造相关复现活动,让社区成员积极介入,配合长大,配合打造加倍优异的方针检测框架

本文先简要先容 YOLOX 算法,然后重点描写复现流程

1 YOLOX 算法简介

官方开源地址:https://github.com/Megvii-BaseDetection/YOLOX

MMDetection 开源地址:GitHub - open-mmlab/mmdetection: OpenMMLab Detection Toolbox and Benchmark,接待 star

复现相关 projects:Support of YOLOX · open-mmlab/mmdetection

YOLOX 网上的解读很是多,详情可见官方解读:https://www.zhihu.com/question/473350307/answer/2021031747

YOLOX 的首要特征可以归纳为:

  1. Anchor-free,无需设想 anchor,更少先验,削减复杂超参,推理较高效
  2. 提出了 Decoupled Head,参考 FCOS 算法设想解耦了分类和回归分支,同时新增 objectness 回归分支
  3. 为了加速收敛以及进步性能,引入了 SimOTA 标签分派战略,其包括两部分 Multi positives 和 SimOTA,Multi positives 可以简单的增加每个 gt 所需的正样本数,SimOTA 基于 CVPR2021 最新的 OTA 算法,采用最优传输理论全局分派每个 gt 的正样本,斟酌到 OTA 带来的额外练习价格,作者提出了简化版本的 OTA ,在长 epoch 练习使命中 SimOTA 和 OTA 性能相当,且可以极大的缩小练习价格
  4. 参考 YOLOV4 的数据增强战略,引入了 Mosaic 和 MixUp,而且在最初 15 个 epoch 时辰封闭这两个数据增强操纵,尝试表白可以极大地提升性能
  5. 基于上述理论,参考 YOLOV5 收集设想思绪,提出了 YOLOX-Nano、YOLOX-Tiny、YOLOX-S、YOLOX-M、YOLOX-L 和 YOLOX-X 分歧参数目的模子

算法细节将在后续小结中会停止具体说明。

2 YOLOX 复现流程全剖析

我们简单将 YOLOX 复现进程拆分为 3 个步调,别离是:

  1. 推理精度对齐
  2. 练习精度对齐
  3. 重构

2.1 推理精度对齐

为了方便将官方开源权重迁移到 MMDetection 中,在推理精度对齐进程中,我们没有点窜任何模子代码,而且简单的复制开源代码,别离插入 MMDetection 的 backbone 和 head 文件夹下,这样就只需要简单的替换模子 key 即可。

一个出格需要留意的点:BN 层参数不是默许值 eps=1e-5, momentum=0.1,而是 eps=1e-3, momentum=0.03,这个应当是间接参考 YOLOV5。

解除了模子方面的题目(后处置战略我们临时也没有改),对齐推理精度焦点就是分析图片前处置代码。其处置流程很是简单

# 前处置焦点操纵
def preproc(image, input_size, mean, std, swap=(2, 0, 1)):
# 预界说输出图片巨细
padded_img = np.ones((input_size[0], input_size[1], 3)) * 114.0

# 连结宽高比的 resize
img = np.array(image)
r = min(input_size[0] / img.shape[0], input_size[1] / img.shape[1])
resized_img = cv2.resize(
img,
(int(img.shape[1] * r), int(img.shape[0] * r)),
interpolation=cv2.INTER_LINEAR,
).astype(np.float32)

# 右下 padding
padded_img[: int(img.shape[0] * r), : int(img.shape[1] * r)] = resized_img

# bgr -> rgb
padded_img = padded_img[:, :, ::-1]

# 减均值,除方差
padded_img /= 255.0
if mean is not None:
padded_img -= mean
if std is not None:
padded_img /= std
padded_img = padded_img.transpose(swap)
return padded_img, r

可以发现,其前处置流程比力简单:先采用连结宽高比的 resize,然后右下 padding 成指定巨细输出,最初是归一化。

在 MMDetection 中可以间接经过点窜设置文件来支持上述功用:

test_pipeline = [
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=img_scale,
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(type='Pad', size=(640, 640), pad_val=114.0),
dict(type='Normalize', **img_norm_cfg),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img'])
])
]

相关的后处置阈值为:

test_cfg=dict(
score_thr=0.001,
nms=dict(type='nms', iou_threshold=0.65)))

别离对 YOLOX-S 和 YOLOX-Tiny 模子权重在官方源码和 MMDetection 中停止评价,考证能否对齐,成果以下:



留意:由于官方开源代码一向处于更新中,现鄙人载的最新权重,能够 mAP 不是上表中的值。

2.2 练习精度对齐

练习精度对齐相对来说复杂很多,初步观察源码,发现练习 trick 还是蛮多的,而 MMDetection 中临时没有间接能复用的模块。练习精度对齐由 MMDetection 的开辟团队和社区用户配合完成,我们将整体的对齐分化成以下多少模块,每个模块都有社区用户介入:

  1. 优化器和进修率调剂器
  2. EMA 战略
  3. Dataset
  4. Loss
  5. 其他练习 trick

斟酌到 YOLOX 练习需要 300 epoch,练习时长比力长,我们采用 YOLOX-Tiny 模子停止练习精度对齐,经过对照源码和复现版本的 log 停止判定。相比标准的 YOLOX-S 模子,其不同仅仅是没有益用 MixUp 以及额外的两个超参纷歧样而已。

2.2.1 优化器和进修率调剂器

(1) 优化器

优化器部分相对来说轻易实现,其流程是:将优化参数设备为3组,卷积 bias、BN 和卷积权重,其中只对卷积的权重停止 decay,优化器是 SGD+ nesterov+ momentum。在 MMDetection 中可经过点窜设置间接实现上述功用:

optimizer = dict(
type='SGD',
lr=0.01,
momentum=0.9,
weight_decay=5e-4,
nesterov=True,
paramwise_cfg=dict(norm_decay_mult=0., bias_decay_mult=0.))

焦点是依靠 paramwise_cfg 参数,封闭 norm 和 bias 的weight decay。

留意: MMDetection 中的 lr 设备是针对总 batch 的,上面写的 lr =0.01 是指的 8 卡 x 8 bs 的情况,而不算单卡的,假如你的总 bs 不是 64,那末你需要手动停止线性缩放

(2) 进修率调剂器

YOLOX 的进修率调剂器是带有 warmup 战略的余弦调剂战略,而且为了配合数据增强,在最初 15 个 epoch 会采用牢固的最小进修率

MMDetection 中导入的 MMCV 已经实现了带有 warmup 战略的余弦调剂战略,可是比力麻烦的是指数 warmup 战略公式不太一样,而且不具有在最初 15 个 epoch 采用牢固最小进修率的功用。为领会决上述题目,且不变动依靠 MMCV 版本,我们是在 MMDetection 中继续了本来的 CosineAnnealingLrUpdaterHook,并重写了相关方式。

class YOLOXLrUpdaterHook(CosineAnnealingLrUpdaterHook):
def get_warmup_lr(self, cur_iters):

def _get_warmup_lr(cur_iters, regular_lr):
# 重写 warmup 战略
k = self.warmup_ratio * pow(
(cur_iters + 1) / float(self.warmup_iters), 2)
warmup_lr = [_lr * k for _lr in regular_lr]
return warmup_lr

...

def get_lr(self, runner, base_lr):
last_iter = len(runner.data_loader) * self.num_last_epochs

progress = runner.iter
max_progress = runner.max_iters

progress += 1

target_lr = base_lr * self.min_lr_ratio

if progress >= max_progress - last_iter:
# 牢固进修率战略
return target_lr
else
return annealing_cos(
base_lr, target_lr, (progress - self.warmup_iters) /
(max_progress - self.warmup_iters - last_iter))

为了保证代码的正确性,我们零丁写了剧本,运转官方源码和 MMDetection 复现代码,比力双方的 lr 曲线能否完全分歧。

2.2.2 EMA 战略

模子的指数移动均匀可以提升模子鲁棒性和性能,属于一个常用的 trick。其道理是:对模子额外保护一份指数移动均匀模子 ema_model,然后在每次迭代模子参数更新后,操纵 model 中的参数和 ema_model 计较更新后的 ema_model,评价阶段利用的是 ema_model

def update(self, model):
# Update EMA parameters
with torch.no_grad():
self.updates += 1
# self.decay = lambda x: decay * (1 - math.exp(-x / 2000))
d = self.decay(self.updates)

msd = (
model.module.state_dict() if is_parallel(model) else model.state_dict()
)
for k, v in self.ema.state_dict().items():
if v.dtype.is_floating_point:
v *= d
v += (1.0 - d) * msd[k].detach()

MMCV 中已经经过 Hook 实现了 EMA 功用,可是没有斟酌 BN buffer 的 EMA 操纵,而且 decay 公式也纷歧样,MMCV 中是线性 decay 战略,而 YOLOX 中是指数 decay。为此,我们在兼容 MMCV EMA 功用的条件下,重新设想了 EMA Hook,今朝临时放在 MMDetection 中,会在后续版本迁移到 MMCV 中。

先分析下 MMCV 中 EMA 战略实现方式,然后再说明若何斟酌兼容。

EMA 的实现是采用 Hook 实现的,下面贴焦点代码:

@HOOKS.register_module()
class EMAHook(Hook):
# 在模子运转前,将模子参数重新 copy 一份,然后重新作为 buffer 插入到模子中
# 也就是说此时 Model 里面有两份不异参数的模子了
def before_run(self, runner):
model = runner.model
if is_module_wrAPPer(model):
model = model.module
self.param_ema_buffer = {}
self.model_parameters = dict(model.named_parameters(recurse=True))
for name, value in self.model_parameters.items():
# "." is not allowed in module's buffer name
buffer_name = f"ema_{name.replace('.', '_')}"
self.param_ema_buffer[name] = buffer_name
model.register_buffer(buffer_name, value.data.clone())
self.model_buffers = dict(model.named_buffers(recurse=True))
# 这个很关键,后续会说明
if self.checkpoint is not None:
runner.resume(self.checkpoint)

# 实时计较更新 ema 模子参数
def after_train_iter(self, runner):
curr_step = runner.iter
# We warm up the momentum considering the instability at beginning
momentum = min(self.momentum,
(1 + curr_step) / (self.warm_up + curr_step))
if curr_step % self.interval != 0:
return
for name, parameter in self.model_parameters.items():
buffer_name = self.param_ema_buffer[name]
buffer_parameter = self.model_buffers[buffer_name]
buffer_parameter.mul_(1 - momentum).add_(momentum, parameter.data)
# 很是关键,后续说明
def after_train_epoch(self, runner):
"""We load parameter values from ema backup to model before the
EvalHook."""
self._swap_ema_parameters()

def before_train_epoch(self, runner):
"""We recover model's parameter from ema backup after last epoch's
EvalHook."""
self._swap_ema_parameters()

def _swap_ema_parameters(self):
"""Swap the parameter of model with parameter in ema_buffer."""
for name, value in self.model_parameters.items():
temp = value.data.clone()
ema_buffer = self.model_buffers[self.param_ema_buffer[name]]
value.data.copy_(ema_buffer.data)
ema_buffer.data.copy_(temp)

为了后续方便保存和规复模子,我们没有零丁保护一个新的 ema_model,而是将参数重新 copy 一份,然后作为 buffer 插入到本来模子中,酿成两份。然后在每次迭代练习后,操纵 momentum 和更新后的模子参数来更新 ema 模子。

由于在评价时辰采用的是 ema 模子,为了不影响前面的 evalhook 和 save checkpoint 相关逻辑,我们在每次开启 epoch 练习前和练习后城市交换一次模子参数,这样在评价进程就会自动利用 ema 模子,这是一个比力奇妙的 trick,需要特地夸大的是 EMAHook 优先级一定要确保比 evalhook 和 save checkpoint 相关逻辑高,否则会出题目。由于全部练习进程流程是:

  1. 首先在里面构建模子参数和初始化模子
  2. 在 before_run 中新建一份 ema 模子参数的 buffer,而且插入到本来模子中
  3. 在开启 epoch 练习前 before_train_epoch 交换一次权重,即 model 的参数值是 ema 参数值,而 ema 参数值是 model,而且由于此时两者完全相称,所以可以以为没有交换
  4. 迭代的更新 model 参数,而且在 after_train_iter 中实时更新 ema 模子参数
  5. 由于 EMAHook 优先级较高,故会优先于 evalhook 和 save checkpoint 相关逻辑,其会先运转 after_train_epoch 交换一次参数,此时 model 的参数值是 ema 参数值,而 ema 参数值是 model,完成了一次实在的参数交换
  6. 运转 evalhook 和 save checkpoint 相关逻辑,其面临的 model 是 ema model,合适预期
  7. 鄙人一次循环迭代时辰,反复 3-6 进程,不竭的交换参数

大师可以发现,此时保存的模子虽然同时保存了 ema model 和自己 model 参数,可是现实上是反的,也就是说保存的 model 参数现实上是 ema model 参数值,而 ema model 参数是 model 参数值。如此设想的缘由是为了可以完全正确的 resume。在是 resume 阶段,以下步调会依次履行:

  1. 首先在里面构建模子参数和初始化模子
  2. resume 模子时辰,由于此时 ema 模子还没有构建,所以只能加载权重字典中的 model 字段,但现实上该 model 字段是 ema
  3. 在 before_run 中新建一份 ema 模子参数的 buffer,而且插入到本来模子中,此时就构建出了 ema 模子
  4. 假如传入了待 resume 的 checkpoint,此时会重新加载一遍,由于 ema 模子已经构建,所以 ema 模子和 model 城市被 resume,同时 ema 模子参数是 model,而 model 参数是 ema
  5. 在开启 epoch 练习前 before_train_epoch 交换一次权重,此时就正确了,也就是说到这一步就完全规复了
  6. 继续一般的练习

需要夸大:由于这类特别的 resume 技能,当你需要对模子停止 resume 时辰,临时不成以经过内部指定 resume 参数实现,必必要点窜设置中的 resume_from 字段,否则 resume 进程是不正确的

上述逻辑能够比力绕,大师需要仔细思考。这么设想的缘由是:1. 方便后续评价 2. 可以正确 resume。

斟酌到 YOLOX 中的 ema 是需要同时平滑 buffer 的,为此我们重新停止了设想,在兼容的同时有更好的扩大性。

(1) 斟酌要可以平滑 BN 的 buffer 参数,我们增加了 skip_buffers 参数

if self.skip_buffers:
# 假如跳过,那就间接用参数即可
self.model_parameters = dict(model.named_parameters())
else:
# 假如不跳过,则间接利用状态字典
self.model_parameters = model.state_dict()

(2) 斟酌会能够存在多种平滑曲线,我们设想了 BaseEMAHook,然后继续这个类停止扩大分歧的平滑曲线

@HOOKS.register_module()
class ExpMomentumEMAHook(BaseEMAHook):

def __init__(self, total_iter=2000, **kwargs):
super(ExpMomentumEMAHook, self).__init__(**kwargs)
self.momentum_fun = lambda x: (1 - self.momentum) * math.exp(-(
1 + x) / total_iter) + self.momentum


@HOOKS.register_module()
class LinearMomentumEMAHook(BaseEMAHook):

def __init__(self, warm_up=100, **kwargs):
super(LinearMomentumEMAHook, self).__init__(**kwargs)
self.momentum_fun = lambda x: min(self.momentum**self.interval,
(1 + x) / (warm_up + x))

后续我们会将该 EMA hook 从 MMDetection 移动到 MMCV 中酿成根本模块。

2.2.3 Dataset

dataset 部分最复杂,其包括 Mosaic 、MixUp、ColorJit 和静态 resize 等等操纵。由于 MMDetection 中临时都没有实现上述组件,而且超参很多,为此在我们第一版中现实上是间接复制了源码的 dataset,只对 dataset 输出停止包装,使其可以接入 MMDetection 练习进程,在练习精度对齐落后行重新设想。本小结先简要分析源码,然后再描写 MMDetection 实现进程。

(1) 源码诠释

dataset = COCODataset(...)

dataset = MosaicDetection(
dataset,
mosaic=not no_aug,
img_size=self.input_size,
preproc=TrainTransform(
rgb_means=(0.485, 0.456, 0.406),
std=(0.229, 0.224, 0.225),
max_labels=120,
),
degrees=self.degrees,
translate=self.translate,
scale=self.scale,
shear=self.shear,
perspective=self.perspective,
enable_mixup=self.enable_mixup,
)

为了实现 Mosaic 和 Mixup,作者引入了 MosaicDetection 来包裹 COCODataset。其 dataset 流程为:

def __getitem__(self, idx):
# 能否进入 mosaic,默许前 285 个 epoch 城市进入
if self.enable_mosaic:
1 马赛克增强
2 多少变更增强
# 能否进入 mixup,nano 和 tiny 版本默许是封闭的
if self.enable_mixup and not len(mosaic_labels) == 0:
3 mixup 增强
4 图片后处置
else:
4 图片后处置

总共分红上述 4 个步调,整体流程以下图所示(前两步是马赛克增强,第三步是多少变更增强,第 4 步是 MixUp 增强)



上图由社区的小伙伴 HAOCHENYE 供给,戴德!

1) 马赛克增强

  1. 随机出 4 张图片在待输出图片中交接的中心点坐标
  2. 随机出别的 3 张图片的索引以及读取对应的标注
  3. 对每张图片采用连结宽高比的 resize 操纵缩放到指定巨细
  4. 依照高低左右法则,计较每张图片在待输出图片中应当放置的位置,由于图片能够出界故还需要计较裁剪坐标
  5. 操纵裁剪坐标将缩放后的图片裁剪,然后贴到前面计较出的位置,其他位置全数补 114 像素值
  6. 对每张图片的标注也停止响应处置
  7. 由于拼接了 4 张图,所以输出图片巨细会扩大 4 倍

2) 多少变更增强

random_perspective 包括平移、扭转、缩放、错切等增强,而且会将输入图片复原为 (640, 640),同时对增强后的标注停止处置,过滤法则是

  1. 增强后的 gt bbox 宽高要大于 wh_thr
  2. 增强后的 gt bbox 面积和增强前的 gt bbox 面积要大于 ar_thr,避免增强太严重
  3. 最大宽高比要小于 area_thr,避免宽高比改变太多

3) MixUp

Mixup 实现方式有多种,常见的做法是:要末 label 间接拼接起来,要末 label 也采用 alpha 夹杂,作者的做法很是简单,对 label 间接拼接即可,而图片也是采用牢固的 0.5:0.5 夹杂方式。

其处置流程是:

  1. 随机出一张图片,必必要保证该图片不是空标注
  2. 对随机出的图片采用连结宽高比的 resize 操纵缩放到指定巨细
  3. 然后左上 padding 成指定巨细,padding 值也是 114
  4. 对 padding 后的图片停止随机发抖增强
  5. 随机采用 flip 增强
  6. 假如处置后的图片比原图大,则还需要停止随机裁剪增强
  7. 对标签停止对应处置,而且采用和马赛克增强一样的过滤法则
  8. 假如过滤后还存在 gt bbox,则采用 0.5:0.5 的比例夹杂原图和处置后的图片,标签则间接拼接即可

4) 图片后处置

图片后处置操纵也包括众大都据增强操纵,以下所示:

  1. 随机 ColorJit,包括众多色彩相关增强
  2. 随机翻转增强
  3. 对随机后的图片采用连结宽高比的 resize 操纵缩放到指定巨细
  4. 对于宽高小于 8 像素的 gt bbox 间接删掉,由于收集输出的最小 stride 是 8
  5. Padding 成正方形图片输出

(2) MMDetection 实现

Dataset 部分触及到的代码比力多,首要包括:

  1. 之前框架中还没有实现过类似 Mosaic 等需要再次操纵 dataset 相关信息的代码
  2. 之前框架中还没有实现过类似在某一阶段封闭某些数据增强的操纵

针对第一个题目,以 Mosaic 为例实现方式有多种,下面列一下临时想到的计划:

  • 类似 RandomFlip 等,作为 pipeline 实现

参考:https://github.com/open-mmlab/mmdetection/blob/e41dc0cb26ea43302c6444f504c99a688fc93ff4/mmdet/datasets/pipelines/transforms.py#L1815

作为 pipeline 实现的时辰,需要额外插入 dataset 工具大概相关信息,否则内部获得不到 dataset。这类做法对应的设置写法是:

mosaic_pipeline = [
dict(type='LoadImageFromFile', to_float32=True),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='PhotoMetricDistortion'),
dict(
type='Expand',
mean=img_norm_cfg['mean'],
to_rgb=img_norm_cfg['to_rgb'],
ratio_range=(1, 2)),
]

mosaic_data = dict(
type=dataset_type,
ann_file=data_root + 'annotations/instances_train2017.json',
img_prefix=data_root + 'train2017/',
pipeline=mosaic_pipeline)

train_pipeline = [
dict(type='LoadImageFromFile', to_float32=True),
dict(type='LoadAnnotations', with_bbox=True),
dict(type='PhotoMetricDistortion'),
dict(
type='Expand',
mean=img_norm_cfg['mean'],
to_rgb=img_norm_cfg['to_rgb'],
ratio_range=(1, 2)),
dict(type='Mosaic', size=(416, 416), dataset=mosaic_data, min_offset=0.2),
dict(type='Resize', img_scale=[(320, 320), (416, 416)], keep_ratio=True),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size_divisor=32),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]

这类写法的益处是可以无缝插入任何 MMDetection 已经实现的算法中,设置修改最小。可是其有一个很是致命的弱点:dataset 需要在内部重新 build,这个开销比力大,出格是当你的数据集很是大的时辰。而且倘使有多个类似的夹杂数据增强,那末就需要 build 屡次,为此这类写法现实上很难接管。

固然有其他折衷法子,例如在 dataset 基类中传入 self 工具,例如 results['dataset']=self,由于 dataset 工具可以贯串全部 pipeline 生命周期,可是其风险比力大,例如能够出现循环挪用,同时由于 self 工具贯串全部生命周期,一旦你在某个时辰不谨慎点窜了 dataset,那末发生的 bug 将会难以估量和排查,为此我们也不筹算采用这类做法。

  • 类似 RandomFlip 等,作为 pipeline 实现,可是内部缓存 index

其焦点就是:在内部保持一个牢固巨细的缓冲池,实时更新缓和存已经读取过的 index,只要缓存池充足大,那末理论上 Mosaic 结果应当很是接近。

这类做法可以避免第一种做法缺点,优点也很是明显:即插即用,无其他要求,可是其结果能否和标准的 mosaic 结果能否分歧,需要调研下。由于时候比力赶,我们没有去调研终极结果能否分歧。

  • 类似 RepeatDataset 等,作为 dataset wrapper 实现

在 mmdet/datasets/dataset_wrappers.py 中实现 MultiImageMixDataset,其会对 dataset 停止包装,避免了屡次 build 的性能开销题目。

其焦点代码是:

def __getitem__(self, idx):
# 获得当前 idx 的图片信息
results = self.dataset[idx]
# 遍历 transorm,其中可以包括 mosaic 、mixup、flip 等各类 transform
for (transform, transform_type) in zip(self.pipeline,
self.pipeline_types):
# 斟酌到某些练习阶段需要静态封闭掉部分数据增强,故引入 _skip_type_keys
if self._skip_type_keys is not None and \
transform_type in self._skip_type_keys:
continue
if hasattr(transform, 'get_indexes'):
# transform 假如额外供给了 get_indexes 方式,则暗示需要停止夹杂数据增强
# 返回索引
indexes = transform.get_indexes(self.dataset)
if not isinstance(indexes, collections.abc.Sequence):
indexes = [indexes]
# 获得夹杂图片信息
mix_results = [
copy.deepcopy(self.dataset[index]) for index in indexes
]
results['mix_results'] = mix_results
# 静态标准 resize
if self._dynamic_scale is not None:
# Used for subsequent pipeline to automatically change
# the output image size. E.g MixUp, Resize.
results['scale'] = self._dynamic_scale
# 数据增强
results = transform(results)

if 'mix_results' in results:
results.pop('mix_results')
if 'img_scale' in results:
results.pop('img_scale')
return results

其焦点是对于 Mosaic 大概 MixUp 等需要夹杂数据的增强操纵,对应的 transform 需要额外供给 get_indexes 方式,内部返回 indexes 信息;然后 MultiImageMixDataset 会自动完成相关获得数据操纵。

以 Mosaic 和 MixUp 为例,其作为 pipeline 的写法以下所示:

@PIPELINES.register_module()
class Mosaic:
def __init__(self,
img_scale=(640, 640),
center_ratio_range=(0.5, 1.5),
pad_val=114):
assert isinstance(img_scale, tuple)
self.img_scale = img_scale
self.center_ratio_range = center_ratio_range
self.pad_val = pad_val

def __call__(self, results):
results = self._mosaic_transform(results)
return results

def get_indexes(self, dataset):
indexs = [random.randint(0, len(dataset)) for _ in range(3)]
return indexs


@PIPELINES.register_module()
class MixUp:
def __init__(self,
img_scale=(640, 640),
ratio_range=(0.5, 1.5),
flip_ratio=0.5,
pad_value=114,
max_iters=15,
min_bbox_size=5,
min_area_ratio=0.2,
max_aspect_ratio=20):
assert isinstance(img_scale, tuple)
self.dynamic_scale = img_scale
self.ratio_range = ratio_range
self.flip_ratio = flip_ratio
self.pad_value = pad_value
self.max_iters = max_iters
self.min_bbox_size = min_bbox_size
self.min_area_ratio = min_area_ratio
self.max_aspect_ratio = max_aspect_ratio

def __call__(self, results):
results = self._mixup_transform(results)
return results


# 必必要返回非空 gt bbox 数据索引
def get_indexes(self, dataset):
for i in range(self.max_iters):
index = random.randint(0, len(dataset))
gt_bboxes_i = dataset.get_ann_info(index)['bboxes']
if len(gt_bboxes_i) != 0:
break

return index

对应的完整设置写法以下:

train_pipeline = [
dict(type='Mosaic', img_scale=img_scale, pad_val=114.0),
dict(
type='RandomAffine',
scaling_ratio_range=(0.1, 2),
border=(-img_scale[0] // 2, -img_scale[1] // 2)),
dict(type='MixUp', img_scale=img_scale, ratio_range=(0.8, 1.6)),
# PhotoMetricDistortion 和 YOLOX 中实现的色彩增强纷歧样,PhotoMetricDistortion 增强更强一些,可是斟酌随机操纵能够对终极性能没有很大影响,故我们并没有对其停止点窜
dict(
type='PhotoMetricDistortion',
brightness_delta=32,
contrast_range=(0.5, 1.5),
saturation_range=(0.5, 1.5),
hue_delta=18),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='Resize', keep_ratio=True),
dict(type='Pad', pad_to_square=True, pad_val=114.0),
dict(type='Normalize', **img_norm_cfg),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
]

train_dataset = dict(
type='MultiImageMixDataset',
dataset=dict(
type=dataset_type,
ann_file=data_root + 'annotations/instances_train2017.json',
img_prefix=data_root + 'train2017/',
pipeline=[
dict(type='LoadImageFromFile', to_float32=True),
dict(type='LoadAnnotations', with_bbox=True)
],
filter_empty_gt=False,
),
pipeline=train_pipeline,
dynamic_scale=img_scale)

这类写法虽然会对今朝已经实现的算法设置写法有较大修改,可是可扩大强,不存在性能开销题目。

对于 MultiImageMixDataset 中实现的静态 scale 和封闭数据增强操纵在 2.2.5 小结分析。需要说明的是: 由于 YOLOX 算法开辟进度比力快,时候比力赶,上述计划能够不是最好的,我们后续也会渐渐改良的,使其在满足可扩大性条件下,易用性进步,出错率可以下降。

2.2.4 Loss

关于 Loss 部分,在第一版中我们是间接复制了源码。斟酌到 loss 是 YOLOX 的焦点,故先简要分析源码,然后再描写 MMDetection 重构版本。

(1) 源码诠释

其收集输出包括 3 个 标准, stride 别离是 8、16 和 32,每个输出标准上又包括 3 个输出,别离是 bbox 输出分支、objectness 输出分支和 cls 种别输出分支。其 loss 计较流程为:

1) 计较 3 个输出层所需要的特征图标准的坐标,用于 bbox 解码

yv, xv = torch.meshgrid([torch.arange(hsize), torch.arange(wsize)])
grid = torch.stack((xv, yv), 2).view(1, 1, hsize, wsize, 2).type(dtype)

2) 对输出 bbox 停止解码复原到原图标准

output = output.view(batch_size, 1, n_ch, hsize, wsize)
output = output.permute(0, 1, 3, 4, 2).reshape(
batch_size, 1 * hsize * wsize, -1
)
grid = grid.view(1, -1, 2)
# 复原
output[..., :2] = (output[..., :2] + grid) * stride
output[..., 2:4] = torch.exp(output[..., 2:4]) * stride

经过上述解码公式,可以晓得 bbox 输出猜测值 cxcywh,别离代表 gt bbox 中心和当前网格左上标偏移以及 wh 的指数变更值,而且都基于当前 stride 停止了缩放。

3) 对每张图片零丁计较婚配的正样本和对应的 target

由于后续婚配法则需要斟酌中心地区,故提早计较每个 gt bbox 在指定范围内的中心地区。作者引入了超参 center_radius =2.5,其首要计较进程为:

  1. 基于 grid 和 stride 计较 anchor 点的中心坐标,其中 anchor 是个数为 1 的正方形 anchor
  2. 计较一切 gt bbox 的中心坐标
  3. 计较一切在 gt bbox 内部的 anchor 点的掩码 is_in_boxes_all
  4. 操纵 center_radius 阈值重新计较在 gt bbox 中心 center_radius 范围内的 anchor 点的掩码 is_in_centers_all
  5. 两个掩码取并集获得在 gt bbox 内部或处于 center_radius 范围内的 anchor 点的掩码 is_in_boxes_anchor,同时可以取交集获得每个 gt bbox 和哪些 anchor 点合适 gt bbox 内部和处于 center_radius 范围内的 anchor is_in_boxes_and_center

4) 计较每张图片中 gt bbox 和候选猜测框的婚配价格

# fg_mask : (n,) 假如某个位置是 True 代表该 anchor 点是远景即
# 落在 gt bbox 内部大概在间隔 gt bbox 中心 center_radius 半径范围内
# is_in_boxes_and_center:(num_gt,n), 假如某个位置是 True 代表
# 该 anchor 点落在 gt bbox 内部而且在间隔 gt bbox 中心 center_radius 半径范围内
# 提取对应值
bboxes_preds_per_image = bboxes_preds_per_image[fg_mask]
cls_preds_ = cls_preds[batch_idx][fg_mask]
obj_preds_ = obj_preds[batch_idx][fg_mask]
num_in_boxes_anchor = bboxes_preds_per_image.shape[0]

# 计较猜测框和 gt bbox 的配对 iou
pair_wise_ious = bboxes_iou(gt_bboxes_per_image, bboxes_preds_per_image, False)
gt_cls_per_image = (
F.one_hot(gt_classes.to(torch.int64), self.num_classes)
.float()
.unsqueeze(1)
.repeat(1, num_in_boxes_anchor, 1)
)
# iou 越大,婚配度越高,所以需要取负号
pair_wise_ious_loss = -torch.log(pair_wise_ious + 1e-8)

cls_preds_ = (
cls_preds_.float().unsqueeze(0).repeat(num_gt, 1, 1).sigmoid_()
* obj_preds_.unsqueeze(0).repeat(num_gt, 1, 1).sigmoid_()
)
# 配对的分类 Loss,包括了 iou 分支猜测值
pair_wise_cls_loss = F.binary_cross_entropy(
cls_preds_.sqrt_(), gt_cls_per_image, reduction="none"
).sum(-1)
del cls_preds_

# 计较每个 gt bbox 和挑选出来的候选猜测框的分类 loss + 坐标 loss + 中心点和半径约束
# 值越小,暗示婚配度越高
# (num_gt,n)
cost = (
pair_wise_cls_loss
+ 3.0 * pair_wise_ious_loss
+ 100000.0 * (~is_in_boxes_and_center)
)
  1. fg_mask 就是前面计较出的 is_in_boxes_anchor,假如某个位置是 True 代表该 anchor 点是远景即落在 gt bbox 内部大概在间隔 gt bbox 中心 center_radius 半径范围内,这些 True 位置就是正样本候选点
  2. 操纵 fg_mask 提取对应的猜测信息,假定 num_gt 是 3,一共提取了 800 个候选猜测位置,则每个 gt bbox 城市提取出 800 个候选位置
  3. 计较候选猜测框和 gt bbox 的配对 iou,然后加 log 和负数,酿成 iou 的价格函数
  4. 计较候选猜测框和 gt bbox 的配对分类价格值,同时斟酌了 objectness 猜测分支,而且其分类 cost 在 binary_cross_entropy 前有开根号的练习 trick
  5. is_in_boxes_and_center shape 是 (3, 800), 假如某个位置是 True 暗示该 anchor 点落在 gt bbox 内部而且在间隔 gt bbox 中心 center_radius 半径范围内。在计较价格函数时辰,假如该猜测点是 False,暗示不再交集内部,那末应当不太能够是候选点,所以赐与一个很是大的价格权重 100000.0,该操纵可以保证每个 gt bbox 终极挑选的候选点不会在交集内部

上述计较出的价格值充实斟酌了各个分支猜测值,也斟酌了中心先验,有益于练习稳定和收敛,同时也为后续的静态婚配供给了全局信息。

5) 为每个 gt bbox 静态挑选 k 个 候选猜测值,作为婚配正样本

def dynamic_k_matching(self, cost, pair_wise_ious, gt_classes, num_gt, fg_mask):
# 婚配矩阵初始化为 0
matching_matrix = torch.zeros_like(cost)
ious_in_boxes_matrix = pair_wise_ious
# 每个 gt bbox 挑选的候选猜测点不跨越 10 个
n_candidate_k = min(10, ious_in_boxes_matrix.size(1))
topk_ious, _ = torch.topk(ious_in_boxes_matrix, n_candidate_k, dim=1)
# 每个 gt bbox 的静态 k
dynamic_ks = torch.clamp(topk_ious.sum(1).int(), min=1)
for gt_idx in range(num_gt):
_, pos_idx = torch.topk(
cost[gt_idx], k=dynamic_ks[gt_idx].item(), largest=False
)
# 婚配上位置设备为 1
matching_matrix[gt_idx][pos_idx] = 1.0
del topk_ious, dynamic_ks, pos_idx
# n, 暗示该候选点有没有婚配到 gt bbox
anchor_matching_gt = matching_matrix.sum(0)
if (anchor_matching_gt > 1).sum() > 0:
# 假如某个候选点婚配了多个 gt bbox,则挑选价格最小的,保证每个候选点只能婚配一个 gt bbox
_, cost_argmin = torch.min(cost[:, anchor_matching_gt > 1], dim=0)
matching_matrix[:, anchor_matching_gt > 1] *= 0.0
matching_matrix[cost_argmin, anchor_matching_gt > 1] = 1.0

# 每个候选点的婚配情况
fg_mask_inboxes = matching_matrix.sum(0) > 0.0
# 总共有几多候选点
num_fg = fg_mask_inboxes.sum().item()
# 更新远景掩码,在前面中心先验的条件下进一步挑选正样本
fg_mask[fg_mask.clone()] = fg_mask_inboxes

# 该候选框婚配到哪个 gt bbox
matched_gt_inds = matching_matrix[:, fg_mask_inboxes].argmax(0)
gt_matched_classes = gt_classes[matched_gt_inds]

# 提取对应的猜测点和gt bbox 的 iou
pred_ious_this_matching = (matching_matrix * pair_wise_ious).sum(0)[
fg_mask_inboxes
]
return num_fg, gt_matched_classes, pred_ious_this_matching, matched_gt_inds
  1. 初始化 gt bbox 和候选点的婚配矩阵为全 0,暗示全数不婚配
  2. 每个 gt bbox 挑选的候选猜测点不跨越 10 个
  3. 操纵前面的婚配价格,给每个 gt bbox 计较静态 k
  4. 遍历每个 gt bbox,提取价格为前静态 k 个位置,暗示婚配上
  5. 假如某个候选点婚配了多个 gt bbox,则挑选价格最小的,保证每个候选点只能婚配一个 gt bbox
  6. 返回总共有几多候选点 num_fg、每个候选点婚配上的 gt bbox 信息 gt_matched_classes、每个候选点婚配上的 和 gt bbox 计较的 IoU 值、婚配上的 gt bbox 索引、更新后的远景掩码 fg_mask,其长度和猜测点个数不异,其中 1 暗示正样本点,0 暗示负样本点。

6) 计较 loss

分类分支和 objectness 分支采用 bce loss,bbox 猜测分支采用 IoU Loss。

  1. 分类分支仅仅斟酌正样本即 fg_mask 为 1 的位置,其 label 是同时斟酌了猜测值和 gt bbox 的 IoU 值,用于增强各分支间的分歧性
  2. objectness 分支需要同时斟酌正负样本,其起到抑制布景的感化,其 label 就是上述 fg_mask,非 0 即 1,即一切候选点
  3. bbox 分支也仅仅斟酌正样本,其 label 就是正样本候选点所对应的解码后的猜测值

7) 附加 L1 Loss

在最初 15 个 epoch 后,作者加入了额外的 L1 Loss。其感化的工具是原始没有解码的正样本 bbox 猜测值,和对应的 gt bbox。

从以上分析可知: 分类分支不斟酌布景,布景猜测功用由 objectness 分支供给,而 bbox 分支结合采用了 IoU Loss 和 L1 Loss,其最大改良在于静态婚配。

(2) MMDetection 重构版本

Loss 写法的重构,首要修改是采用 MMDetection 中默许的构建方式即 prior_generator + bbox assign + bbox encode decode + loss。

YOLOX 是 anchor-free 算法,可是仍然需要特征图上每个猜测点的 point 信息,为此我们间接采用 RepPoints 中的 MlvlPointGenerator 类用于天生 anchor-point 坐标;bbox assign 部分则重写并新建了 SimOTAAssigner 类,用于对猜测点停止分派正负样本;其他地方则完善了命名标准,和简化了部分写法,使其加倍轻易了解,整体逻辑没有修改。

2.2.5 其他练习 trick

除了上述焦点部件,作者还引入了其他练习 trick,以下所示:

  1. 在最初 15 个 epoch 封闭 Mosaic 和 MixUp 增强,而且增加额外的 L1 loss
  2. 每隔一定间隔,改变输出图片尺寸,而且保证多卡之间的图片尺寸不异
  3. 每隔一定间隔,对 BN 参数停止多卡同步,保证评价时辰分歧卡的权重性能分歧

(1) 封闭 Mosaic 和 MixUp 增强,增加额外的 L1 loss

def before_epoch(self):

if self.epoch + 1 == self.max_epoch - self.exp.no_aug_epochs or self.no_aug:
logger.info("--->No mosaic aug now!")
self.train_loader.close_mosaic()
logger.info("--->Add additional L1 loss now!")
self.model.head.use_l1 = True

MMDetection 是经过 hook 实现上述功用的

@HOOKS.register_module()
class YOLOXModeSwitchHook(Hook):

def __init__(self, num_last_epochs=15):
self.num_last_epochs = num_last_epochs

def before_train_epoch(self, runner):
"""Close mosaic and mixup augmentation and switches to use L1 loss."""
epoch = runner.epoch
train_loader = runner.data_loader
model = runner.model
if is_parallel(model):
model = model.module
if (epoch + 1) == runner.max_epochs - self.num_last_epochs:
runner.logger.info('No mosaic and mixup aug now!')
train_loader.dataset.update_skip_type_keys(
['Mosaic', 'RandomAffine', 'MixUp'])
runner.logger.info('Add additional L1 loss now!')
model.bbox_head.use_l1 = True

焦点就是 train_loader.dataset.update_skip_type_keys,将需要解除的 pipeline 对应的类名写入即可。 Loss 切换也是同理。

(2) 改变输出图片尺寸

# 在每次练习迭代后,判定
if self.exp.random_size is not None and (self.progress_in_iter + 1) % 10 == 0:
self.input_size = self.exp.random_resize(
self.train_loader, self.epoch, self.rank, self.is_distributed
)


def random_resize(self, data_loader, epoch, rank, is_distributed):
tensor = torch.LongTensor(2).cuda()

if rank == 0:
# 随机采样一个新的 size
size_factor = self.input_size[1] * 1.0 / self.input_size[0]
size = random.randint(*self.random_size)
size = (int(32 * size), 32 * int(size * size_factor))
tensor[0] = size[0]
tensor[1] = size[1]

if is_distributed:
dist.barrier()
dist.broadcast(tensor, 0) # 广播到其他卡

# 改变图片 size
input_size = data_loader.change_input_dim(
multiple=(tensor[0].item(), tensor[1].item()), random_range=None
)
return input_size

而且操纵 broadcast 将 tensor 广播给其他卡,从而实现分歧卡间的输入图片尺寸不异功用。

MMDetection 是经过 hook 实现上述功用的

@HOOKS.register_module()
class SyncRandomSizeHook(Hook):
"""Change and synchronize the random image size across ranks, currently
used in YOLOX.

Args:
ratio_range (tuple[int]): Random ratio range. It will be multiplied
by 32, and then change the dataset output image size.
Default: (14, 26).
img_scale (tuple[int]): Size of input image. Default: (640, 640).
interval (int): The interval of change image size. Default: 10.
"""

def __init__(self,
ratio_range=(14, 26),
img_scale=(640, 640),
interval=10):
self.rank, world_size = get_dist_info()
self.is_distributed = world_size > 1
self.ratio_range = ratio_range
self.img_scale = img_scale
self.interval = interval

def after_train_iter(self, runner):
"""Change the dataset output image size."""
if self.ratio_range is not None and (runner.iter +
1) % self.interval == 0:
# 同步静态 resize

焦点是 runner.data_loader.dataset.update_dynamic_scale,该操纵会实时改变 MultiImageMixDataset 中的 _dynamic_scale 属性,进而改变对应 pipeline 的 scale 字段值例如 Resize 和 MixUp,实现每个几个 iter 就改变 size 的多标准练习功用。

(3) BN 参数停止多卡同步

在每次评价前,会同步下分歧卡的 BN 参数,保证分歧卡间参数的分歧性,其中字典工具的同步进程参考了 detectron2 中的代码实现。

def all_reduce_norm(module):
"""
All reduce norm statistics in different devices.
"""
states = get_async_norm_states(module)
states = all_reduce(states, op="mean")
module.load_state_dict(states, strict=False)

MMDetection 是经过 hook 实现上述功用的,而且斟酌多卡同步字典操纵是通用的,为此将其封装到 get_norm_states(module) 中。

@HOOKS.register_module()
class SyncNormHook(Hook):

def __init__(self, interval=1):
self.interval = interval

def after_train_epoch(self, runner):
"""Synchronizing norm."""
epoch = runner.epoch
module = runner.model
if (epoch + 1) % self.interval == 0:
_, world_size = get_dist_info()
if world_size == 1:
return
norm_states = get_norm_states(module)
norm_states = all_reduce_dict(norm_states, op='mean')
module.load_state_dict(norm_states, strict=False)

(4) Hook 优先级留意事项

经过前面分析,可以发现 YOLOX 重构插入了 5 个 hook,此时就需要出格斟酌下 hook 优先级,出格需要留意的是 EMAHook 优先级一定要高于 evalhook 和保存权重操纵,其他几个 hook 优先级只要比 EMAHook 高就行,其设置以下所示:

custom_hooks = [
dict(type='YOLOXModeSwitchHook', num_last_epochs=15, priority=48),
dict(
type='SyncRandomSizeHook',
ratio_range=(14, 26),
img_scale=img_scale,
interval=interval,
priority=48),
dict(type='SyncNormHook', interval=interval, priority=48),
dict(type='ExpMomentumEMAHook', resume_from=resume_from, priority=49)
]

evalhook 和保存权重操纵的优先级是 50,优先级值越小优先级越高,为此我们将 ExpMomentumEMAHook 优先级调剂为 49,其他三个 hook,优先级设备为 48 就行,这三个 hook 履行顺序没有出格要求,优先级不异则会根据插入顺序履行。

2.3 重构

在练习精度对齐后需要将上述代码重构,使其在兼容 MMDetection 标准条件下代码可读性提升。重构部分也是并行停止,社区职员和保护者负责分歧的部分,而且经过 review 后合并。

重构部分的焦点是模子、后处置、loss、dataset 四个大模块,其中 loss 和 dataset 以及其他模块重构已经在 2.2 小结已经分析过了,故本小结只分析模子和后处置自己的重构。

(1) 模子相关重构

YOLOX 系列模子典型结构是 CSPDarknet+SPPBottleneck+PAFPN+Head,收集设想参考了 YOLOV5,Head 部分停止了改良:



采用领会耦 Head,其完整结构以下所示:



由于 YOLOX 模子是参考 YOLOV5 的,我们在重构前期会商了两种模子构建方式,别离是:

1) 参考 ResNet 写法,arch_settings 按 stage 写,逐一 stage build

arch_settings = {
'P5': [[64, 128, 3, True, False],
[128, 256, 9, True, False],
[256, 512, 9, True, False],
[512, 1024, 3, False, True]],

'P6': [[64, 128, 3, True, False],
[128, 256, 9, True, False],
[256, 512, 9, True, False],
[512, 768, 3, True, False],
[768, 1024, 3, False, True]]
}

2) 参考 YOLOV5 写法,arch_settings 按 YOLOv5 config 的气概写,逐一 layer build

backbone:
# [from, number, module, args]
[ [ -1, 1, Focus, [ 64, 3 ] ], # 0-P1/2
[ -1, 1, Conv, [ 128, 3, 2 ] ], # 1-P2/4
[ -1, 3, C3, [ 128 ] ],
[ -1, 1, Conv, [ 256, 3, 2 ] ], # 3-P3/8
[ -1, 9, C3, [ 256 ] ],
[ -1, 1, Conv, [ 512, 3, 2 ] ], # 5-P4/16
[ -1, 9, C3, [ 512 ] ],
[ -1, 1, Conv, [ 768, 3, 2 ] ], # 7-P5/32
[ -1, 3, C3, [ 768 ] ],
[ -1, 1, Conv, [ 1024, 3, 2 ] ], # 9-P6/64
[ -1, 1, SPP, [ 1024, [ 3, 5, 7 ] ] ],
[ -1, 3, C3, [ 1024, False ] ], # 11
]

第一种写法优点是清楚易懂,弱点是灵活度较低,第二种写法恰好相反,灵活度较高,可是代码难以了解。权衡之下,并连系 MMDetection 气概,我们终极采用了第一种计划。

如上图所示,SPPBottleneck 包括在了 CSPDarknet 中了,backbone 输出 3 个分支,stride 别离是 [8, 16, 32],然后接一个标准的 PAFPN 模块,也是输出 3 个分支,通道数都是 256,最初接 3 个不同享权重的 Head 模块,别离输出种别、bbox 和 objectness 猜测信息。

有个细节:在重构模子结构后重新练习后发现,初始时辰 Loss 相比原版代码有较大差异,经过排查发现是由于卷积参数初始化致使的。MMDetection 在构建模子时辰城市采用 ConvModule 来搭建 Conv+BN+Act 结构,可是该模块内部会点窜默许的卷积参数初始化进程。为了可以和源码完全对齐,我们需要在初始化后重置一切卷积层的参数初始化进程。

幸亏有 init_cfg,这个参数可以经过设置文件来点窜肆意位置的参数初始化进程,而不需要修改任何代码。例如要实现上述功用,则只需要在构建模子时辰,传入特定的 init_cfg 就行,例如 backbone 中要重置一切卷积层的参数初始化

@BACKBONES.register_module()
class CSPDarknet(BaseModule):

def __init__(self,
arch='P5',
...
init_cfg=dict(
type='Kaiming',
layer='Conv2d',
a=math.sqrt(5),
distribution='uniform',
mode='fan_in',
nonlinearity='leaky_relu')):

由于 init_cfg 功用很是强大,后续我们会推出关于 init_cfg 的专题解读和用法。

(2) 后处置相关重构

YOLOX 的后处置战略很是简单,只需要 conf_thr 和 nms 阈值便可以。

  • 基于输出特征图宽高和 stride,天生 grid,然后基于 grid 对猜测框停止解码复原到原图标准
def decode_outputs(self, outputs, dtype):
grids = []
strides = []
for (hsize, wsize), stride in zip(self.hw, self.strides):
yv, xv = torch.meshgrid([torch.arange(hsize), torch.arange(wsize)])
grid = torch.stack((xv, yv), 2).view(1, -1, 2)
grids.append(grid)
shape = grid.shape[:2]
strides.append(torch.full((*shape, 1), stride))
grids = torch.cat(grids, dim=1).type(dtype)
strides = torch.cat(strides, dim=1).type(dtype)
# 解码复原
outputs[..., :2] = (outputs[..., :2] + grids) * strides
outputs[..., 2:4] = torch.exp(outputs[..., 2:4]) * strides

return outputs
  • 操纵 conf_thre 过滤 bbox
# 提取最大分值对应的种别和猜测分值
class_conf, class_pred = torch.max(
image_pred[:, 5 : 5 + num_classes], 1, keepdim=True
)
# obj 分值和 class_conf 相乘,然后操纵 conf_thre 停止过滤
conf_mask = (image_pred[:, 4] * class_conf.squeeze() >= conf_thre).squeeze()

# 拼接
detections = torch.cat((image_pred[:, :5], class_conf, class_pred.float()), 1)
detections = detections[conf_mask]
  • NMS 后处置
nms_out_index = torchvision.ops.batched_nms(
detections[:, :4],
detections[:, 4] * detections[:, 5],
detections[:, 6],
nms_thre,
)
detections = detections[nms_out_index]

可以发现后处置战略很是简单,没有 nms_pre 参数、没有两次过滤原则、没有 max_num_pre_img 参数

MMDetection 后处置重构整体没有改变(由于已经很简单了),只不外我们同一用 prior_generator 来天生 grid,其他地方同一了参数命名方式和代码气概。

3 总结

本文具体分析了若何重头复现一个新算法,高效的复现进程离不开 MMDetection 开辟团队和社区小伙伴们的不懈尽力,再次暗示感激!我们也希望在这样的合作开辟形式中,大师在快速了解算法自己的同时,社区小伙伴们也可以进一步的了解了 MMDetection 设想理念、代码标准和开辟要求等等,配合长大和进步。

我们希望后续可以进一步推行这类开辟形式,让更多的社区用户介入进来,配合打造最好用的方针检测框架,打造更好用的 OpenMMLab 开源库。

最初接待大师加入 MMDetection QQ社群:810800355 !!!

领会更多OpenMMLab 可加入社区:

【OpenMMLab QQ社群】:1群:144762544(行将满员) 2群:920178331

【OpenMMLab 微信社群】:微信扫码关注公众号!增加小助手,进群挑选自己感爱好的研讨偏向!


高端人脉微信群

高端人脉微信群

人脉=钱脉,我们相信天下没有聚不拢的人脉,扫码进群找到你所需的人脉,对接你所需的资源。

商业合作微信

商业合作微信

本站创始人微信,13年互联网营销经验,擅长引流裂变、商业模式、私域流量,高端人脉资源丰富。

我有话说......

查看全部评论>>

相关推荐

非常详细的企业管理流程模板,整理了很久,管理者日常工作必备!

非常详细的企业管理流程模板,整理了很久,管理者日常工作必备!

阅读前请点击右上角“关注”,每天免费获取职场文化及管理知识。职场千里马文化,只做

外企英语之公司流程是procedure还是process?

外企英语之公司流程是procedure还是process?

先考考大家,SOP里的P到底是procedure 还是process?或许你会想SOP不就是「标准作业流

关于流程图,你想知道的都在这里

关于流程图,你想知道的都在这里

为大家精选了10片优秀的流程图文章~画了多年的流程图,你真的画规范了吗?如何画逻辑

亲身经历谈流程管理(共8篇)——第一篇,什么是流程?

亲身经历谈流程管理(共8篇)——第一篇,什么是流程?

最近有一个朋友找我抱怨,说他们公司的太规范了。我一听,这肯定不是表扬,问他怎么了

让面试官膜拜你的HTTPS运行流程(超详细)

让面试官膜拜你的HTTPS运行流程(超详细)

引言最近恶补计网,HTTPS涉及到的知识比较多,整理一下。HTTPS实际上就是HTTP穿上了SS

22考研快要报名了!时间表已出炉,提前看报名流程及注意 ... ...

22考研快要报名了!时间表已出炉,提前看报名流程及注意 ... ...

最近后台私信收到比较多关于报名相关的提问,尤其是往届生问得多一些,关于报考点、档

优秀的流程图都这样画(附三大绘制规范)

优秀的流程图都这样画(附三大绘制规范)

优秀的流程图需要遵循一定的规范,包括符号规范、结构规范、路径规范等。只要熟练掌握

评测了10款画流程图软件,这4款最好用!(完全免费)

评测了10款画流程图软件,这4款最好用!(完全免费)

最近在做项目和复习的时候,用了不少流程图软件给我帮了大忙,所以今天就来分享分享你

强强联手!VS Code让它成为最强流程图工具

强强联手!VS Code让它成为最强流程图工具

自从切换到mac之后,我一直在寻找一款趁手的流程图工具。遇到http://draw.io之后,我

轻松掌握 MMDetection 中 Head 流程

轻松掌握 MMDetection 中 Head 流程

文@0000070 摘要轻松掌握 MMDetection 训练测试流程(二)对整个目标检测框架的训练以及

YOLOX 在 MMDetection 中复现全流程解析

YOLOX 在 MMDetection 中复现全流程解析

0 摘要最近 YOLOX 火爆全网,速度和精度相比 YOLOv3、v4 都有了大幅提升,并且提出了

输入文本直接生成流程图,这个极简工具火了,在线可玩

输入文本直接生成流程图,这个极简工具火了,在线可玩

鱼羊 发自 凹非寺量子位 报道 | 公众号 QbitAI流程图/思维导图让工作变得高效。但是,

史上最全的工程建设项目流程

史上最全的工程建设项目流程

以下25张图总结了工程建设项目的一般流程,建议收藏,随手查阅!一、工程建设项目前期

大名鼎鼎的IPD开发流程为什么这么厉害?一篇文章轻松读懂它 ... ...

大名鼎鼎的IPD开发流程为什么这么厉害?一篇文章轻松读懂它 ...

1 概述本文内容篇幅比较长,为方便阅读,先放一张本文的框架图,便于理解。IPD,Inte

流程的三大概念:分级、分类、分层

流程的三大概念:分级、分类、分层

现在很多企业在搞流程管理,都会安排各个部门、各个岗位将自己做的事情画成流程图。等

供应链的5大流程,从老王的初恋说起

供应链的5大流程,从老王的初恋说起

编辑导语:你了解供应链5大流程吗?它们分别为计划、采购、生产制造、交付和退货,本

科研大佬用什么工具画流程图?看完这篇就知道了。

科研大佬用什么工具画流程图?看完这篇就知道了。

​不知道大家有没有被流程图困扰过,在阐述生信分析流程时有它的身影,在规划科研基金

产品经理必会的3大流程:业务流程、功能流程、页面流程

产品经理必会的3大流程:业务流程、功能流程、页面流程

在工作中,画流程图是产品经理的基本技能之一,对于业务流程图、功能流程图和页面流程

开源的流程模拟软件

开源的流程模拟软件

前言最近很多朋友的Aspen都过期了。借此机会,介绍一下两款开源的流程模拟软件,DWSIM

装修不要当“甩手掌柜”,这份装修流程汇总,说得很详细!请参考

装修不要当“甩手掌柜”,这份装修流程汇总,说得很详细!请参考

夏天到了,天气晴朗,又是装修的好季节!最近也有很多业主私信飞墨君,询问关于装修的

电话咨询: 15924191378
添加微信