0 摘要比来 YOLOX 火爆全网,速度和精度相比 YOLOv3、v4 都有了大幅提升,而且提出了很多通用性的 trick,同时供给了摆设相关剧本,适用性极强。 MMDetection 开源团队成员也构造停止了相关复现。 在本次复现进程中,有5位社区成员介入进献:
首先很是感激几位社区成员的进献! 经过协同开辟,不但让复现进程加倍高效,而且社区成员在介入进程中可以不竭熟悉算法,熟悉 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 的首要特征可以归纳为:
算法细节将在后续小结中会停止具体说明。 2 YOLOX 复现流程全剖析我们简单将 YOLOX 复现进程拆分为 3 个步调,别离是:
2.1 推理精度对齐 为了方便将官方开源权重迁移到 MMDetection 中,在推理精度对齐进程中,我们没有点窜任何模子代码,而且简单的复制开源代码,别离插入 MMDetection 的 backbone 和 head 文件夹下,这样就只需要简单的替换模子 key 即可。 一个出格需要留意的点:BN 层参数不是默许值 解除了模子方面的题目(后处置战略我们临时也没有改),对齐推理精度焦点就是分析图片前处置代码。其处置流程很是简单 # 前处置焦点操纵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 的开辟团队和社区用户配合完成,我们将整体的对齐分化成以下多少模块,每个模块都有社区用户介入:
斟酌到 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 相关逻辑高,否则会出题目。由于全部练习进程流程是:
大师可以发现,此时保存的模子虽然同时保存了 ema model 和自己 model 参数,可是现实上是反的,也就是说保存的 model 参数现实上是 ema model 参数值,而 ema model 参数是 model 参数值。如此设想的缘由是为了可以完全正确的 resume。在是 resume 阶段,以下步调会依次履行:
需要夸大:由于这类特别的 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) 马赛克增强
2) 多少变更增强 random_perspective 包括平移、扭转、缩放、错切等增强,而且会将输入图片复原为 (640, 640),同时对增强后的标注停止处置,过滤法则是
3) MixUp Mixup 实现方式有多种,常见的做法是:要末 label 间接拼接起来,要末 label 也采用 alpha 夹杂,作者的做法很是简单,对 label 间接拼接即可,而图片也是采用牢固的 0.5:0.5 夹杂方式。 其处置流程是:
4) 图片后处置 图片后处置操纵也包括众大都据增强操纵,以下所示:
(2) MMDetection 实现 Dataset 部分触及到的代码比力多,首要包括:
针对第一个题目,以 Mosaic 为例实现方式有多种,下面列一下临时想到的计划:
参考: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 工具,例如
其焦点就是:在内部保持一个牢固巨细的缓冲池,实时更新缓和存已经读取过的 index,只要缓存池充足大,那末理论上 Mosaic 结果应当很是接近。 这类做法可以避免第一种做法缺点,优点也很是明显:即插即用,无其他要求,可是其结果能否和标准的 mosaic 结果能否分歧,需要调研下。由于时候比力赶,我们没有去调研终极结果能否分歧。
在 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,其首要计较进程为:
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) )
上述计较出的价格值充实斟酌了各个分支猜测值,也斟酌了中心先验,有益于练习稳定和收敛,同时也为后续的静态婚配供给了全局信息。 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
6) 计较 loss 分类分支和 objectness 分支采用 bce loss,bbox 猜测分支采用 IoU Loss。
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) 封闭 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 焦点就是 (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 焦点是 (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 阈值便可以。
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
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]
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 微信社群】:微信扫码关注公众号!增加小助手,进群挑选自己感爱好的研讨偏向!
|
阅读前请点击右上角“关注”,每天免费获取职场文化及管理知识。职场千里马文化,只做
先考考大家,SOP里的P到底是procedure 还是process?或许你会想SOP不就是「标准作业流
为大家精选了10片优秀的流程图文章~画了多年的流程图,你真的画规范了吗?如何画逻辑
最近有一个朋友找我抱怨,说他们公司的太规范了。我一听,这肯定不是表扬,问他怎么了
引言最近恶补计网,HTTPS涉及到的知识比较多,整理一下。HTTPS实际上就是HTTP穿上了SS
最近后台私信收到比较多关于报名相关的提问,尤其是往届生问得多一些,关于报考点、档
优秀的流程图需要遵循一定的规范,包括符号规范、结构规范、路径规范等。只要熟练掌握
最近在做项目和复习的时候,用了不少流程图软件给我帮了大忙,所以今天就来分享分享你
自从切换到mac之后,我一直在寻找一款趁手的流程图工具。遇到http://draw.io之后,我
文@0000070 摘要轻松掌握 MMDetection 训练测试流程(二)对整个目标检测框架的训练以及
0 摘要最近 YOLOX 火爆全网,速度和精度相比 YOLOv3、v4 都有了大幅提升,并且提出了
鱼羊 发自 凹非寺量子位 报道 | 公众号 QbitAI流程图/思维导图让工作变得高效。但是,
以下25张图总结了工程建设项目的一般流程,建议收藏,随手查阅!一、工程建设项目前期
1 概述本文内容篇幅比较长,为方便阅读,先放一张本文的框架图,便于理解。IPD,Inte
现在很多企业在搞流程管理,都会安排各个部门、各个岗位将自己做的事情画成流程图。等
编辑导语:你了解供应链5大流程吗?它们分别为计划、采购、生产制造、交付和退货,本
不知道大家有没有被流程图困扰过,在阐述生信分析流程时有它的身影,在规划科研基金
在工作中,画流程图是产品经理的基本技能之一,对于业务流程图、功能流程图和页面流程
前言最近很多朋友的Aspen都过期了。借此机会,介绍一下两款开源的流程模拟软件,DWSIM
夏天到了,天气晴朗,又是装修的好季节!最近也有很多业主私信飞墨君,询问关于装修的
声明:本站内容由网友分享或转载自互联网公开发布的内容,如有侵权请反馈到邮箱 1415941@qq.com,我们会在3个工作日内删除,加急删除请添加站长微信:15924191378
Copyright @ 2022-2024 私域运营网 https://www.yunliebian.com/siyu/ Powered by Discuz! 浙ICP备19021937号-4