提出了一种对比表征嵌入的方法来来实现小样本目标检测,观察到使用不同的 IoU 来检测物体与对比学习方法中对比不同“正对”和“负对”来实现分类有异曲同工之妙以及好的特征嵌入是提升小样本学习性能的关键。动机是观察到模型的错误更有可能是误分类而不是定位,本文解决这一问题的方法是对“正对”和“负对”施加了对比嵌入损失(CPE loss),使“正对”的得分远大于“负对”的得分,在当时的 PASCAL VOC 和 COCO 数据集上均达到了 SOTA。

“正对”、"负对"示例

在有监督对比学习的图像分类任务中,使用数据扩增来丰富“正对”。而在检测任务中对同一物体不同 IoU 值的 proposal,也可以看作是一种对“正对”的补充。

常见的二阶段检测模型 Faster Rcnn 的 RPN 模块可以很好的找出前景区域,最后的回归层也可以很好地定位出新颖类别的物体。在大数据集上两类相似物体的余弦相似度可以达到0.39,物体和背景的余弦相似度则为-0.21。而在小样本学习的设定下,相似物体的相似度可达到0.59,更容易出错。因此得出结论模型的错误更有可能是误分类而不是定位,文中也做了一个统计,如果能纠正分类错误的话新颖物体的平均检测精度可以涨20个点。

度量学习算法更关注对区分不同物体更有效的高层表征,而不是聚焦在像素级别的细节上。

传统的迁移学习方法违反直觉的地方是:FPN 和 RPN 学习到的提取 base instance 特征的能力可以直接迁移到提取 novel instance 的特征上。但是如果迁移后不冻结 FPN 和 RPN 的话,又会影响精度。但文中提出如果采用适当的训练策略,可以提高传统迁移学习的精度,文中称为 Strong Baseline。

Strong Baseline

大家一直都认同的一个观点是越多的模型组件被 fine tune,在 novel instance 上的精度就会越低。但是文中发现在 base 数据集和 novel 数据集上从 positive anchor 中挑出的 proposal 的数量差距很大,后者只有前者的四分之一,前景的 proposal 数量的差距同样也很大。

主要问题在于 RPN 使得 positive anchor 的得分太低,在经过 NMS 后剩下的 proposal 太少以及 proposal 的数量太少,导致背景的 proposal 主导了梯度下降。为了解决这两个问题提出了两个 Trick:

  • 将 RPN 中 NMS 之后留下来的 proposal 的最大数量增加一倍;
  • 将 Head 中用来计算损失的 RoI 的数量减少一半。

取得了不错的效果,对比的 baseline 是《Frustratingly simple few-shot object detection》,这里可以看作是两个 trick。

FSCE

在模型的 Head 增加了一个对比分支,这个分支度量了 proposal 的相似性。这个分支附带了一个损失函数,contrastive proposal encoding (CPE),用来将同一类别的实例凑的更“近”,而不同类别尽可能“远”的分离。这个对比分支使用一个 MLP 来实现,将 RoI 编码成一个128维的向量,即一个对比表征嵌入。使用这个嵌入来计算相似度得分以及将这个部分的损失函数 CPE 附加到总的损失函数里,这个 Contrastive Head 引导 RoI 学习易于对比的表征嵌入。这个对比分支可以作为二阶段网络的一个即插即用模块。

FSCE Overview

采用基于余弦相似度的 Box Classifier,计算 RoI 与各个类别的相似性度量,下式表示第 i 个 RoI 与第 j 类物体的相似性度量。

α 是超参数,用来放大梯度,文中采用20。

在余弦相似投影的超空间内,对比表征嵌入可以使得簇内距离更小,簇间距离更大。

训练过程分为两个阶段:首先在通用的大数据集上训练 Faster Rcnn。之后将其迁移到小数据集上,这个小数据集包括 novel instance 以及从大数据集里随机选取的 base instance。第二个阶段的训练冻结 backbone 的参数,更新 Neck 和新加上对比分支的 Head 的参数及损失函数。

 取0.5。

CPE Loss

对于一个小批量 N 的 RoI features,有 ,其中 zi 是第 i 个 proposal 的对比表征嵌入,ui 是 IoU 的得分,yi 是 ground truth 的标签。CPE Loss 定义为

Nyi 是标签为 yi 的 proposal 的数量,zi*zj 代表了余弦相似度。 是正则项,这里对对比表征嵌入和正则项有一个消融实验。

and τ is the hyper-parameter temperature as in InfoNCE [48].

《Representation learning with contrastive predictive coding》

f(ui) 是为了防止 IoU 得分过低使得 proposal 中包含干扰的背景信息而定义的,包含阈值项和一个权重函数。

g() 为不同的 IoU 分配不同的权重参数,而  取0.7,这里有一个消融实验。

t-SNE 证明了该损失函数的有效性。

Experiment

PASCAL VOC

可以看到迁移后的模型在 base 数据上精度依然有很大提升。

COCO

Conclusion

FSCE 对实例进行建模而不是对类别进行建模,通过 CPE Loss 来建模同一类别实例的相似性,指导 Contrastive Head 来学习易于对比的嵌入表征。

附加

  • 两个 Trick 的具体实现

将 NMS 之后留下来的 proposal 的最大数量增加一倍 (以 深度解析Faster RCNN (3)---从loss到全局解析 - 知乎 中的代码为例):

def anchor_target_layer(rpn_cls_score, gt_boxes, im_info, _feat_stride, all_anchors, num_anchors):...# 求所有的 anchor(共 h*w*9 个,这里的 h、w 是特征图的高和宽)与 gt_boxes 的重叠 IOUoverlaps = bbox_overlaps(np.ascontiguousarray(anchors, dtype=np.float),np.ascontiguousarray(gt_boxes, dtype=np.float))# 求各 anchor 与 gt 重叠面积最大的 gt 序号argmax_overlaps = overlaps.argmax(axis=1)
# 求最大重叠面积的大小  max_overlaps = overlaps[np.arange(len(inds_inside)), argmax_overlaps]# 求各个 gt 最大重叠面积的 anchor 序号gt_argmax_overlaps = overlaps.argmax(axis=0)
# 求最大重叠面积的大小 gt_max_overlaps = overlaps[gt_argmax_overlaps,np.arange(overlaps.shape[1])]
#获取与每个gt最大重叠面积的anchor序号的行gt_argmax_overlaps = np.where(overlaps == gt_max_overlaps)[0]# 如果不需要抑制 positive 的 anchor,就先给背景 anchor 赋值,这样在赋前景值的时候可以覆盖。if not cfg.FLAGS.rpn_clobber_positives:  labels[max_overlaps < cfg.FLAGS.rpn_negative_overlap] = 0
# 标记前景区域labels[gt_argmax_overlaps] = 1labels[max_overlaps >= cfg.FLAGS.rpn_positive_overlap] = 1
# 如果需要抑制 positive 的 anchor,就将背景 anchor 后赋值,# 在这里将最大 IoU 仍然小于阈值(0.3)的某些 anchor 置0if cfg.FLAGS.rpn_clobber_positives: labels[max_overlaps < cfg.FLAGS.rpn_negative_overlap] = 0'''
第一个 Trick 实现在这里,超参数中将 rpn_batchsize 设置为512就行
'''num_fg = int(cfg.FLAGS.rpn_fg_fraction * cfg.FLAGS.rpn_batchsize)  #0.5*256=128fg_inds = np.where(labels == 1)[0]if len(fg_inds) > num_fg: # 大于则进行随机采样disable_inds = npr.choice(fg_inds, size=(len(fg_inds) - num_fg), replace=False)  #从fg_inds中选择128个fg,其余的都设为-1labels[disable_inds] = -1
'''
背景的 proposal 也加倍
'''num_bg = cfg.FLAGS.rpn_batchsize - np.sum(labels == 1)  bg_inds = np.where(labels == 0)[0]if len(bg_inds) > num_bg: # 随机采样disable_inds = npr.choice(bg_inds, size=(len(bg_inds) - num_bg), replace=False)labels[disable_inds] = -1 #从bg_inds中选择128个bg,其余的都设为-1...return rpn_labels, rpn_bbox_targets, rpn_bbox_inside_weights, rpn_bbox_outside_weights

以 FSCE 中的代码为例,将超参数中的 RPN.POST_NMS_TOPK_TRAIN 设置为4000(baseline 的 Faster Rcnn 中的 RPN 每层输出的是2000个 proposal):

def __init__(self, cfg, input_shape: Dict[str, ShapeSpec]):super().__init__()...self.post_nms_topk = {
'''True: cfg.MODEL.RPN.POST_NMS_TOPK_TRAIN,
'''False: cfg.MODEL.RPN.POST_NMS_TOPK_TEST,}...proposals = find_top_rpn_proposals(outputs.predict_proposals(),  # transform anchors to proposals by applying deltaoutputs.predict_objectness_logits(),images,self.nms_thresh,self.pre_nms_topk[self.training],
'''self.post_nms_topk[self.training],
'''self.min_box_side_len,self.training,
)

将 Head 中用来计算损失的 RoI 的数量减少一半:

def __init__(self, cfg, input_shape: Dict[str, ShapeSpec]):super(ROIHeads, self).__init__()# fmt: offself.batch_size_per_image     = cfg.MODEL.ROI_HEADS.BATCH_SIZE_PER_IMAGE...sampled_fg_idxs, sampled_bg_idxs = subsample_labels(gt_classes, self.batch_size_per_image, self.positive_sample_fraction,self.num_classes)

也是超参数中的 ROI_HEADS.BATCH_SIZE_PER_IMAGE,默认是512,将其设置为256即可。

  • 在 Windows 上 bulid 时必须是在一个没有 build Detectron2 的环境,原因还没有找到,会报 RuntimeError: Not compiled with GPU support 的错。

Detectron2 安装填坑通过-2020.05.14 | 码农家园

​​​​​https://github.com/facebookresearch/detectron2/blob/main/INSTALL.md#common-installation-issues
​​​​​https://github.com/facebookresearch/detectron2/issues/55

  • build 时还可能报错
D:/actnn/actnn/actnn/cpp_extension/quantization_cuda_kernel.cu(126): error: no instance of overloaded function "std::min" matches the argument listargument types are: (long long, long)D:/actnn/actnn/actnn/cpp_extension/quantization_cuda_kernel.cu(190): error: no instance of overloaded function "std::min" matches the argument listargument types are: (long long, long)D:/actnn/actnn/actnn/cpp_extension/quantization_cuda_kernel.cu(302): error: no instance of overloaded function "std::min" matches the argument listargument types are: (long long, long)D:/actnn/actnn/actnn/cpp_extension/quantization_cuda_kernel.cu(379): error: no instance of overloaded function "std::min" matches the argument listargument types are: (long long, long)

Fix windows build (#953) · pytorch/vision@f516753 · GitHub

/home/anshul/es3cap/my_codes/Pedestron/mmdet/ops/roi_align/src/roi_align_cuda.cpp:20:23: note: suggested alternative: ‘DCHECK’#define CHECK_CUDA(x) AT_CHECK(x.type().is_cuda(), #x, " must be a CUDAtensor ")^
/home/anshul/es3cap/my_codes/Pedestron/mmdet/ops/roi_align/src/roi_align_cuda.cpp:20:23: note: in definition of macro ‘CHECK_CUDA’#define CHECK_CUDA(x) AT_CHECK(x.type().is_cuda(), #x, " must be a CUDAtensor ")

需将 AT_CHECK 改为 TORCH_CHECK。

  • 训练时报错
BrokenPipeError: [Errno 32] Broken pipe

Windows 使用 DataLoader 时设置 num_workers 的问题,将 num_workers 设置为0即可。

  • 注册机制 Registry

detectron2 中常使用注册机制来调用不同的模块,如不同的 backbone、RPN 等。以 backbone 为例:

BACKBONE_REGISTRY = Registry("BACKBONE")def build_backbone(cfg, input_shape=None):"""Build a backbone from `cfg.MODEL.BACKBONE.NAME`.Returns:an instance of :class:`Backbone`"""if input_shape is None:input_shape = ShapeSpec(channels=len(cfg.MODEL.PIXEL_MEAN))backbone_name = cfg.MODEL.BACKBONE.NAMEbackbone = BACKBONE_REGISTRY.get(backbone_name)(cfg, input_shape)assert isinstance(backbone, Backbone)return backbone

如果创建了一个 Registry 的对象,并在方法/类定义的时候用这个装饰器装饰该方法/类定义,则可以通过 registry_machine.get(方法名) 的办法来间接的调用被注册的函数,比如上面代码里的 BACKBONE_REGISTRY.get(backbone_name)(cfg, input_shape)。比如在 resnet.py 内:

@BACKBONE_REGISTRY.register()
def build_resnet_backbone(cfg, input_shape):

这样的话 BACKBONE_REGISTRY.get(backbone_name) 返回的就是这个 build_resnet_backbone 函数,再将 (cfg, input_shape) 传入调用这个函数。对于 detectron2 这种需要支持许多不同的模型的大型框架,理想情况下所有的模型的参数都希望写在配置文件中,比如通过配置文件决定是使用 VGG 还是 ResNet,用注册机制来实现可扩展性就非常好。

  • python中@classmethod @staticmethod区别 - Eliefly - 博客园
  • __setattr__ 方法
class A(object):def __init__(self, value):self.value = valuedef __setattr__(self, name, value):self.name = value     # 会报错# 应该是:object.__setattr__(self, name, value)# 或者:self.__dict__[name] = value

这是个死循环。当我们实例化这个类的时候,会进入 __init__,然后对 value 进行设置值,设置值会进入 __setattr__ 方法,而 __setattr__ 方法里面又有一个 self.name=value 设置值的操作,会再次调用自身 __setattr__,造成死循环。

  • 数据预处理过程

python import导入时,发生了什么?_python小工具的博客-CSDN博客

以 COCO 数据集为例,运行 train_net.py 文件后

train_net.py:
from fsdet.data import MetadataCatalog, build_detection_train_loaderdata 文件夹下的 __init__.py:
from . import datasets, samplers  datasets 文件夹下的 __init__.py:
from . import builtin  

通过上述过程链接到 builtin.py:

...
_PREDEFINED_SPLITS_COCO = {}
_PREDEFINED_SPLITS_COCO["coco"] = {"coco_2014_train": ("coco/train2014", "coco/annotations/instances_train2014.json"),"coco_2014_val": ("coco/val2014", "coco/annotations/instances_val2014.json"),"coco_2014_minival": ("coco/val2014", "coco/annotations/instances_minival2014.json"),"coco_2014_minival_100": ("coco/val2014", "coco/annotations/instances_minival2014_100.json"),"coco_2014_valminusminival": ("coco/val2014","coco/annotations/instances_valminusminival2014.json",),"coco_2017_train": ("coco/train2017", "coco/annotations/instances_train2017.json"),"coco_2017_val": ("coco/val2017", "coco/annotations/instances_val2017.json"),"coco_2017_test": ("coco/test2017", "coco/annotations/image_info_test2017.json"),"coco_2017_test-dev": ("coco/test2017", "coco/annotations/image_info_test-dev2017.json"),"coco_2017_val_100": ("coco/val2017", "coco/annotations/instances_val2017_100.json"),
}
...register_all_coco()
register_all_lvis()
register_all_pascal_voc()

_PREDEFINED_SPLITS_COCO 内存放了 COCO 数据集的划分以及对应划分的图像位置和标注位置,运行了几个数据集的 register。以 register_all_coco() 为例:

builtin.py:
def register_all_coco(root="datasets"):for dataset_name, splits_per_dataset in _PREDEFINED_SPLITS_COCO.items():for key, (image_root, json_file) in splits_per_dataset.items():# Assume pre-defined datasets live in `./datasets`.register_coco_instances(key,_get_builtin_metadata(dataset_name),os.path.join(root, json_file) if "://" not in json_file else json_file,os.path.join(root, image_root),)# register meta datasetsMETASPLITS = [("coco_trainval_all", "coco/trainval2014", "cocosplit/datasplit/trainvalno5k.json"),("coco_trainval_base", "coco/trainval2014", "cocosplit/datasplit/trainvalno5k.json"),("coco_test_all", "coco/val2014", "cocosplit/datasplit/5k.json"),("coco_test_base", "coco/val2014", "cocosplit/datasplit/5k.json"),("coco_test_novel", "coco/val2014", "cocosplit/datasplit/5k.json"),]# register small meta datasets for fine-tuning stagefor prefix in ["all", "novel"]:for shot in [1, 2, 3, 5, 10, 30]:for seed in range(10):seed = "" if seed == 0 else "_seed{}".format(seed)name = "coco_trainval_{}_{}shot{}".format(prefix, shot, seed)METASPLITS.append((name, "coco/trainval2014", ""))for name, imgdir, annofile in METASPLITS:register_meta_coco(name,_get_builtin_metadata("coco_fewshot"),os.path.join(root, imgdir),os.path.join(root, annofile),)
--------------------------------------------------------------------------------------
builtin_meta.py:
def _get_builtin_metadata(dataset_name):if dataset_name == "coco":return _get_coco_instances_meta()elif dataset_name == "coco_fewshot":return _get_coco_fewshot_instances_meta()elif dataset_name == "lvis_v0.5":return _get_lvis_instances_meta_v0_5()elif dataset_name == "lvis_v0.5_fewshot":return _get_lvis_fewshot_instances_meta_v0_5()elif dataset_name == "pascal_voc_fewshot":return _get_pascal_voc_fewshot_instances_meta()raise KeyError("No built-in metadata for dataset {}".format(dataset_name))

把代码拆分成几个部分,这个 for 循环内首先运行 _get_builtin_metadata(dataset_name),找到 COCO 对应的 _get_coco_instances_meta(),这个函数的作用就是将 COCO 原本91类的 stuff 映射成连续80类的 thing 的对应信息,比如 id、name 以及每类别物体检测框的颜色 color。

_get_builtin_metadata(dataset_name) 的结果:

{'thing_dataset_id_to_contiguous_id': {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7, 9: 8, 10: 9, 11: 10, 13: 11, 14: 12, 15: 13, 16: 14, 17: 15, 18: 16, 19: 17, 20: 18, 21: 19, 22: 20, 23: 21, 24: 22, 25: 23, 27: 24, 28: 25, 31: 26, 32: 27, 33: 28, 34: 29, 35: 30, 36: 31, 37: 32, 38: 33, 39: 34, 40: 35, 41: 36, 42: 37, 43: 38, 44: 39, 46: 40, 47: 41, 48: 42, 49: 43, 50: 44, 51: 45, 52: 46, 53: 47, 54: 48, 55: 49, 56: 50, 57: 51, 58: 52, 59: 53, 60: 54, 61: 55, 62: 56, 63: 57, 64: 58, 65: 59, 67: 60, 70: 61, 72: 62, 73: 63, 74: 64, 75: 65, 76: 66, 77: 67, 78: 68, 79: 69, 80: 70, 81: 71, 82: 72, 84: 73, 85: 74, 86: 75, 87: 76, 88: 77, 89: 78, 90: 79}, 'thing_classes': ['person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus', 'train', 'truck', 'boat', 'traffic light', 'fire hydrant', 'stop sign', 'parking meter', 'bench', 'bird', 'cat', 'dog', 'horse', 'sheep', 'cow', 'elephant', 'bear', 'zebra', 'giraffe', 'backpack', 'umbrella', 'handbag', 'tie', 'suitcase', 'frisbee', 'skis', 'snowboard', 'sports ball', 'kite', 'baseball bat', 'baseball glove', 'skateboard', 'surfboard', 'tennis racket', 'bottle', 'wine glass', 'cup', 'fork', 'knife', 'spoon', 'bowl', 'banana', 'apple', 'sandwich', 'orange', 'broccoli', 'carrot', 'hot dog', 'pizza', 'donut', 'cake', 'chair', 'couch', 'potted plant', 'bed', 'dining table', 'toilet', 'tv', 'laptop', 'mouse', 'remote', 'keyboard', 'cell phone', 'microwave', 'oven', 'toaster', 'sink', 'refrigerator', 'book', 'clock', 'vase', 'scissors', 'teddy bear', 'hair drier', 'toothbrush'], 'thing_colors': [[220, 20, 60], [119, 11, 32], [0, 0, 142], [0, 0, 230], [106, 0, 228], [0, 60, 100], [0, 80, 100], [0, 0, 70], [0, 0, 192], [250, 170, 30], [100, 170, 30], [220, 220, 0], [175, 116, 175], [250, 0, 30], [165, 42, 42], [255, 77, 255], [0, 226, 252], [182, 182, 255], [0, 82, 0], [120, 166, 157], [110, 76, 0], [174, 57, 255], [199, 100, 0], [72, 0, 118], [255, 179, 240], [0, 125, 92], [209, 0, 151], [188, 208, 182], [0, 220, 176], [255, 99, 164], [92, 0, 73], [133, 129, 255], [78, 180, 255], [0, 228, 0], [174, 255, 243], [45, 89, 255], [134, 134, 103], [145, 148, 174], [255, 208, 186], [197, 226, 255], [171, 134, 1], [109, 63, 54], [207, 138, 255], [151, 0, 95], [9, 80, 61], [84, 105, 51], [74, 65, 105], [166, 196, 102], [208, 195, 210], [255, 109, 65], [0, 143, 149], [179, 0, 194], [209, 99, 106], [5, 121, 0], [227, 255, 205], [147, 186, 208], [153, 69, 1], [3, 95, 161], [163, 255, 0], [119, 0, 170], [0, 182, 199], [0, 165, 120], [183, 130, 88], [95, 32, 0], [130, 114, 135], [110, 129, 133], [166, 74, 118], [219, 142, 185], [79, 210, 114], [178, 90, 62], [65, 70, 15], [127, 167, 115], [59, 105, 106], [142, 108, 45], [196, 172, 0], [95, 54, 80], [128, 76, 255], [201, 57, 1], [246, 0, 122], [191, 162, 208]]}

通常的检测任务是指检测包含80个类别的 thing,stuff 就是天空这样不规则的物体,thing 就是可数的物体如 person。

builtin.py:
for dataset_name, splits_per_dataset in _PREDEFINED_SPLITS_COCO.items():for key, (image_root, json_file) in splits_per_dataset.items():# Assume pre-defined datasets live in `./datasets`.register_coco_instances(key,_get_builtin_metadata(dataset_name),os.path.join(root, json_file) if "://" not in json_file else json_file,os.path.join(root, image_root),)

之后调用 register_coco_instances(name, metadata, json_file, image_root) 这个函数:

def register_coco_instances(name, metadata, json_file, image_root):# 1. register a function which returns dictsDatasetCatalog.register(name, lambda: load_coco_json(json_file, image_root, name))# 2. Optionally, add metadata about this dataset,since they might be useful in evaluation, visualization or loggingMetadataCatalog.get(name).set(json_file=json_file, image_root=image_root, evaluator_type="coco", **metadata)

这里 lambda 函数又调用了 load_coco_json(json_file, image_root, name),这里的 name 指的是 COCO 数据集中对应划分的名称如 coco_2014_train、coco_2014_val 等。这个函数完成对标注数据的处理,处理成特定的格式(这里是 detectron 的格式,比如包含:file_name、height、width、image_id 以及图片内每一个物体的 iscrowd、bbox、category_id、bbox_mode,其中 category_id 是 COCO 数据集的 thing 的 id)

_REGISTERED = {}@staticmethod
def register(name, func):"""Args:name (str): the name that identifies a dataset, e.g. "coco_2014_train".func (callable): a callable which takes no arguments and returns a list of dicts."""assert callable(func), "You must register a function with `DatasetCatalog.register`!"assert name not in DatasetCatalog._REGISTERED, "Dataset '{}' is already registered!".format(name)DatasetCatalog._REGISTERED[name] = func

之后调用 register(name, lambda: load_coco_json(json_file, image_root, name) 函数将数据集划分的名称及其标注文件放入名为 _REGISTERED 的字典中,到这一步即完成了数据集的注册。第二步 MetadataCatalog.get(name).set(json_file=json_file, image_root=image_root, evaluator_type="coco", **metadata) 的代码没看太懂,手册的解释是:

Each dataset is associated with some metadata, accessible through MetadataCatalog.get(dataset_name).some_metadata. Metadata is a key-value mapping that contains information that’s shared among the entire dataset, and usually is used to interpret what’s in the dataset, e.g., names of classes, colors of classes, root of files, etc.

以及可以通过下面的方法来添加 metadata:

MetadataCatalog.get(dataset_name).some_key = some_value

所以 MetadataCatalog.get(name).set(json_file=json_file, image_root=image_root, evaluator_type="coco", **metadata) 的作用应该跟上面这个语句的作用相同,只不过是同时添加了多个 metadata。

接着继续执行 builtin.py:

# register meta datasets
METASPLITS = [("coco_trainval_all", "coco/trainval2014", "cocosplit/datasplit/trainvalno5k.json"),("coco_trainval_base", "coco/trainval2014", "cocosplit/datasplit/trainvalno5k.json"),("coco_test_all", "coco/val2014", "cocosplit/datasplit/5k.json"),("coco_test_base", "coco/val2014", "cocosplit/datasplit/5k.json"),("coco_test_novel", "coco/val2014", "cocosplit/datasplit/5k.json"),
]# register small meta datasets for fine-tuning stage
for prefix in ["all", "novel"]:for shot in [1, 2, 3, 5, 10, 30]:for seed in range(10):seed = "" if seed == 0 else "_seed{}".format(seed)name = "coco_trainval_{}_{}shot{}".format(prefix, shot, seed)METASPLITS.append((name, "coco/trainval2014", ""))for name, imgdir, annofile in METASPLITS:register_meta_coco(name,_get_builtin_metadata("coco_fewshot"),os.path.join(root, imgdir),os.path.join(root, annofile),)

跟之前是一样的操作,这里操作的是进行小样本任务的数据集。之前一直以为 5k.json 和 instances_minival2014.json 重复了只是名字不同,但是按代码的意思这应该是两个不同的 json 文件,应该是作者自己在 COCO 数据集里划分的。再往后用 _get_builtin_metadata("coco_fewshot") 记录标注中通用的部分,之后调用register_meta_coco(name, _get_builtin_metadata("coco_fewshot"), os.path.join(root, imgdir), os.path.join(root, annofile),) 来完成注册。

def _get_builtin_metadata(dataset_name):if dataset_name == "coco":return _get_coco_instances_meta()elif dataset_name == "coco_fewshot":return _get_coco_fewshot_instances_meta()...def _get_coco_fewshot_instances_meta():ret = _get_coco_instances_meta()novel_ids = [k["id"] for k in COCO_NOVEL_CATEGORIES if k["isthing"] == 1]novel_dataset_id_to_contiguous_id = {k: i for i, k in enumerate(novel_ids)}novel_classes = [k["name"] for k in COCO_NOVEL_CATEGORIES if k["isthing"] == 1]base_categories = [k for k in COCO_CATEGORIES \if k["isthing"] == 1 and k["name"] not in novel_classes]base_ids = [k["id"] for k in base_categories]base_dataset_id_to_contiguous_id = {k: i for i, k in enumerate(base_ids)}base_classes = [k["name"] for k in base_categories]ret["novel_dataset_id_to_contiguous_id"] = novel_dataset_id_to_contiguous_idret["novel_classes"] = novel_classesret["base_dataset_id_to_contiguous_id"] = base_dataset_id_to_contiguous_idret["base_classes"] = base_classesreturn retdef register_meta_coco(name, metadata, imgdir, annofile):DatasetCatalog.register(name, lambda: load_coco_json(annofile, "/usr/FSFS/datasets/coco/val2014", metadata, name),)if '_base' in name or '_novel' in name:split = 'base' if '_base' in name else 'novel'metadata['thing_dataset_id_to_contiguous_id'] = \metadata['{}_dataset_id_to_contiguous_id'.format(split)]metadata['thing_classes'] = metadata['{}_classes'.format(split)]MetadataCatalog.get(name).set(json_file=annofile, image_root=imgdir, evaluator_type="coco",dirname="datasets/coco", **metadata,)
  • detectron2 代码流程 detectron2详解(faster-rcnn) - 知乎

首先在 setup() 里

train.py 里:进入到 build_model(cfg) 方法内开始搭建模型。

def main(args):...if args.eval_only:model = Trainer.build_model(cfg)...

进入 defaults.py 内:

def build_model(cls, cfg):...model = build_model(cfg)...
  • Mosaic 增强

OpenCV里imshow()处理不同数据类型的numpy.ndarray分析_温知故新的博客-CSDN博客

  • Detectron2 数据增强实现

上面介绍的数据预处理的过程是将原始数据处理成 Detectron2 的格式,在 build_detection_train_loader 函数中 DatasetMapper 和 MapDataset 的作用是完成对数据的增强,包括对图像和标注的操作。前者完成增强的定义,后者完成增强的操作。DatasetMapper 类初始化时主要是执行了 utils.build_transform_gen(cfg, is_train),这里主要  transform_gen.py 和 transform.py 在发挥作用。前者提供了一种数据增强的手段,而后者则是具体的图像/坐标变换操作。

举个例子,“图像旋转”是一种 transform,指定一个旋转角度,即可以对输入图像进行旋转并返回处理后图像(输入坐标也可);而“随机图像旋转”是一种 transform_gen,它依赖于“图像旋转”,对于输入图像,它生成一个随机数,构造“图像旋转”的 transform 实例,并对图像进行处理,返回该 transform 实例(顺带一提,如果随机角度为0,会返回 NoOpTransform 实例,也就是啥都不干。这种行为在那种要么做要么不做的随机增强中经常会出现,如随机镜像)。

比如新增加一个随机的高斯扩增,在 transform_gen.py 内:

class  RandomGaussian(TransformGen):def __init__(self, mean=0, sigma=0.006):super().__init__()self._init(locals())def get_transform(self, img):do = self._rand_range() < self.probif do:return Gaussian(self.mean, self.sigma)else:return NoOpTransform()

其中 Gaussian 则在 transform.py 内实现:

class Gaussian(Transform):def __init__(self, mean, sigma):super().__init__()self._set_attributes(locals())def apply_image(self, img, interp=None):        img = img / 255noise = np.random.normal(self.mean, self.sigma, img.shape)img = img + noiseimg = np.clip(img, 0, 1)img = np.uint8(img * 255)return np.asarray(img)def apply_coords(self, coords):return coords

理解Detectron2中数据读取以及在线数据增强流程_hjxu2016的博客-CSDN博客_detectron2数据增强

detectron2中的DatasetMapper类——detectron2如何做数据增强_B1151937289的博客-CSDN博客

这个增强目前还有点问题。。

  • 训练 custom few-shot 数据集 few-shot-object-detection/CUSTOM.md at master · ucbdrive/few-shot-object-detection · GitHub

Step3 和 Step4 中的 evaluator 需要特别注意,其余的几步都是关于 dataset 的处理。

from fsdet.evaluation.evaluator import DatasetEvaluator
class NewDatasetEvaluator(DatasetEvaluator):def __init__(self, dataset_name): # initial needed variablesself._dataset_name = dataset_namedef reset(self): # reset predictionsself._predictions = []def process(self, inputs, outputs): # prepare predictions for evaluationfor input, output in zip(inputs, outputs):prediction = {"image_id": input["image_id"]}if "instances" in output:prediction["instances"] = output["instances"]self._predictions.append(prediction)def evaluate(self): # evaluate predictionsresults = evaluate_predictions(self._predictions)return {"AP": results["AP"],"AP50": results["AP50"],"AP75": results["AP75"],}def build_evaluator(cls, cfg, dataset_name, output_folder=None):...if evaluator_type == "new_dataset":return NewDatasetEvaluator(dataset_name)...

这个 NewDatasetEvaluator 中设置了 base 类和 novel 类的类别 id:

class NewDatasetEvaluator(DatasetEvaluator):def __init__(self, dataset_name, cfg, distributed, output_dir=None):   ...self._base_classes = [1, 2, 3, 4, 5]self._novel_classes = [1, 2, 3, 4, 5]...

这是 custom few shot 数据集与 COCO 数据集不一致的地方,否则类别数对不上的话会报错。而之所以在 base 类数据训练的时候没有报错,是因为在 _eval_predictions 函数中进行了判断:

def _eval_predictions(self):...if self._is_splits:...for split, classes, names in [("all", None, self._metadata.get("thing_classes")),("base", self._base_classes, self._metadata.get("base_classes")),("novel", self._novel_classes, self._metadata.get("novel_classes"))]:...res_ = self._derive_coco_results(coco_eval, "bbox", class_names=names,)...else:...res = self._derive_coco_results(coco_eval, "bbox",class_names=self._metadata.get("thing_classes"))...

即如果不是 few shot 的情况下,class_names 是从数据集的元数据中获得的,也就是 custom 的。而在 few shot 情况下初始化时的 base 和 novel 还按照 COCO 设置的话,这里就会报错。

  • MS COCO 评价指标:AP,AP50,AP70,mAP,AP[.50:.05:.95]

以 COCO AP70,一个类别的检测任务为例。一张图片内目标物体的标注(bbox),将图片送到网络的输出为 n 个预测(score + bbox,score 是网络预测这个 bbox 是目标物体的概率)。TP、FN 和 FP 可以计算,TN 不可计算:

若某个标注 bbox 与某个预测 bbox 的 IoU 大于 IoU_T(一个阈值,这里以 AP70 为例,所以为 0.7),则 TP+=1。

如果没有预测的 bbox 与某个标注 bbox 的 IoU 大于 IoU_T,说明这个标注 bbox(正例)未被预测出,则 FN+=1。

如果没有标注的 bbox 与某个预测 bbox 的 IoU 大于 IoU_T,说明这个预测是错误的即假正例,则 FP+=1。

在此基础上加入预测的 score 信息算出 PR 曲线,再算出 AP。

假如这张图片的标注,就2个 bbox:

id bbox
0 [480, 457, 515, 529]
1 [637, 435, 676, 536]

网络输出的预测,有6个 bbox 和6个 score,按照 score 排序:

假设 IoU > 0.7 的只有标注内 id = 0 的 [480, 457, 515, 529] 和预测内 id = 1 的 [484, 455, 514, 519]。

按照 AP 的计算流程,先把 score 的分界线画在第一个:

把 score 的分界线画在第二个:

把 score 的分界线画在第三个:

把 score 的分界线画在第四个:

把 score 的分界线画在第五个:

把 score 的分界线画在第六个:

然后求11个点的 AP70:

recall 取11个点 = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],对应的 precision 的11个点 = [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0, 0, 0, 0, 0]。

则 AP70 = 0.5+0.5+0.5+0.5+0.5+0.5+0+0+0+0+0+0/11 = 0.27。

COCO 中说的 AP 是 AP[.50:.05:.95],也就是 IoU_T 设置为 0.5, 0.55, 0.60, 0.65, …, 0.95,算十个 APxx,然后再求平均得到的就是 AP。因为 COCO 还是多类别,所以再对类别求平均就是 mAP,也即 COCO 中的 AP。

  • RPN 中正负 anchor 的界定
MODEL.RPN.IOU_THRESHOLDS = [0.3, 0.7]
MODEL.RPN.IOU_LABELS = [0, -1, 1]

anchor 与 gt 的 IoU 在[-∞, 0.3]之间的为负样本,label 为0;在[0.3, 0.7]之间的为弃用的 anchor,label 为-1;而在[0.7, +∞]之间的 anchor,label 为1。

  • Detectron2 中调整学习率代码梳理

在最开始训练时,在 DefaultTrainer 中构造 optimizer、scheduler 和 register_hooks,并完成整个模型的构造。

class DefaultTrainer(SimpleTrainer):optimizer = self.build_optimizer(cfg, model)self.scheduler = self.build_lr_scheduler(cfg, optimizer)    self.register_hooks(self.build_hooks())'1在 build_optimizer() 中构建优化器:'torch.optim.SGD(params, lr, momentum=cfg.SOLVER.MOMENTUM)    '1在 build_lr_scheduler() 中设置按设定的间隔调整学习率:'WarmupMultiStepLR(optimizer,cfg.SOLVER.STEPS,cfg.SOLVER.GAMMA,warmup_factor=cfg.SOLVER.WARMUP_FACTOR,warmup_iters=cfg.SOLVER.WARMUP_ITERS,warmup_method=cfg.SOLVER.WARMUP_METHOD,)'1在 register_hooks 中构建一个 hooks.LRScheduler(self.optimizer, self.scheduler) 的 hook'hooks.LRScheduler(self.optimizer, self.scheduler)

这里的 WarmupMultiStepLR() 继承了 torch.optim.lr_scheduler._LRScheduler,构造如下:

class WarmupMultiStepLR(torch.optim.lr_scheduler._LRScheduler):def __init__(last_epoch: int = -1,...                                     '其余参数省略'super().__init__(optimizer, last_epoch) '2'def get_lr(self) -> List[float]:            '5'warmup_factor = _get_warmup_factor_at_iter(                  '这里是获取 warmup 的方法,先按下不表'self.warmup_method, self.last_epoch, self.warmup_iters, self.warmup_factor)'对于模型中各个部分的学习率,返回一个列表。base_lr 是各参数的初始学习率,self.gamma 是学习率单次下降的倍数,这里为0.1。''self.milestones 是 cfg 中的 STEPS: (60000, 85000, 110000),即需要调整学习率的这个时刻的 iter,bisect_right 是二分法,''返回的是 self.last_epoch 在 self.milestones 这个列表中按大小排列的下标'return [                        base_lr * warmup_factor * self.gamma ** bisect_right(self.milestones, self.last_epoch)for base_lr in self.base_lrs]

可以看到类初始化函数中 last_epoch 初始化为-1,之后调用父类初始化函数。

class _LRScheduler(object):def __init__(self, optimizer, last_epoch=-1, verbose=False):# Initialize epoch and base learning rates           if last_epoch == -1:                                 '不是恢复断点训练时'for group in optimizer.param_groups:group.setdefault('initial_lr', group['lr'])else:                                                '恢复断点训练时'for i, group in enumerate(optimizer.param_groups):if 'initial_lr' not in group:raise KeyError("param 'initial_lr' is not specified ""in param_groups[{}] when resuming an optimizer".format(i))'这里将模型中各部分参数的初始学习率写入 self.base_lrs,后面调用子类 get_lr() 时有用'self.base_lrs = [group['initial_lr'] for group in optimizer.param_groups]    self.last_epoch = last_epoch...self.step()    '3调用 step() 函数'def step(self, epoch=None):...self._step_count += 1with _enable_get_lr_call(self):if epoch is None:           ''self.last_epoch += 1    '4到这里 self.last_epoch 变为0,并调用 get_lr() 函数,调用回子类的 get_lr(),返回的 values 是个列表'values = self.get_lr()    else:warnings.warn(EPOCH_DEPRECATION_WARNING, UserWarning)self.last_epoch = epochif hasattr(self, "_get_closed_form_lr"):values = self._get_closed_form_lr()else:values = self.get_lr()for i, data in enumerate(zip(self.optimizer.param_groups, values)):'6这里是将 values(即 self.get_lr() ),放入到 self.optimizer.param 中,''self.get_lr() 可以计算出当前的学习率,在这里将其放入 optimizer 中为下一个 iter 做准备'param_group, lr = dataparam_group['lr'] = lr                    self.print_lr(self.verbose, i, lr, epoch)self._last_lr = [group['lr'] for group in self.optimizer.param_groups]def get_lr(self):            '父类中的 get_lr() 需要重写'# Compute learning rate using chainable form of the schedulerraise NotImplementedError

至此完成了 optimizer 和 scheduler 的初始化,完成模型构造开始训练。

trainer = Trainer(cfg)
trainer.train()

与平时多见的训练流程不同,Detectron2 中是按照 iter 来进行处理,通常的训练流程中多按照 epoch 来处理:

class TrainerBase:def train(self, start_iter: int, max_iter: int):"""Args:start_iter, max_iter (int): See docs above"""logger = logging.getLogger(__name__)logger.info("Starting training from iteration {}".format(start_iter))self.iter = self.start_iter = start_iterself.max_iter = max_iterwith EventStorage(start_iter) as self.storage:try:self.before_train()for self.iter in range(start_iter, max_iter):self.before_step()self.run_step()self.after_step()finally:self.after_train()

在 for self.iter in range(start_iter, max_iter) 这个循环中,before_step() 和 after_step() 这两个函数完成各个 hook 在这两个时期对应的操作,run_step() 完成一次 iteration 中模型的前向计算和梯度的反向传播。scheduler.step() 要放在 optimizer.step() 之后,后者在 run_step() 中执行,前者在前面提到的 LRScheduler 这个 hook 中执行。

optimizer.step() 中将这个 Optimizer 中的 param_groups 取出来得到学习率、参数、权重衰减等优化器的参数,并进行参数的更新。

class SGD(Optimizer):@torch.no_grad()def step(self, closure=None):for group in self.param_groups:params_with_grad = []d_p_list = []momentum_buffer_list = []weight_decay = group['weight_decay']momentum = group['momentum']dampening = group['dampening']nesterov = group['nesterov']lr = group['lr']                 '在 optimizer 中取出更新需要的参数'for p in group['params']:        '完成参数更新'if p.grad is not None:params_with_grad.append(p)d_p_list.append(p.grad)state = self.state[p]if 'momentum_buffer' not in state:momentum_buffer_list.append(None)else:momentum_buffer_list.append(state['momentum_buffer'])F.sgd(params_with_grad,d_p_list,momentum_buffer_list,weight_decay=weight_decay,momentum=momentum,lr=lr,dampening=dampening,nesterov=nesterov)# update momentum_buffers in statefor p, momentum_buffer in zip(params_with_grad, momentum_buffer_list):state = self.state[p]state['momentum_buffer'] = momentum_bufferreturn loss

之后在 after_step() 中调用 LRScheduler 这个 hook 的 after_step() 函数,在这个函数中执行了 scheduler.step()。

'这个 hook 中主要关注 after_step() 这个函数'
class LRScheduler(HookBase):def __init__(self, optimizer, scheduler):self._optimizer = optimizerself._scheduler = schedulerdef after_step(self):lr = self._optimizer.param_groups[self._best_param_group_id]["lr"]self.trainer.storage.put_scalar("lr", lr, smoothing_hint=False)self._scheduler.step()    '这里执行了 scheduler.step()'

做个总结,构造 scheduler 时,会调用 scheduler.step(),将 get_lr() 返回的学习率、权重衰减等参数传到 optimizer 中。之后开始训练每个 iter 调用 optimizer.step() 更新参数,更新完参数再调用 scheduler.step() 获取当前 iter 的学习率,传入到 optimizer 中,至此完成这个 iter 的训练。之后每一个 iter 按训练流程不停循环至结束。

论文阅读《FSCE: Few-Shot Object Detection via Contrastive Proposal Encoding》相关推荐

  1. 《基于卷积神经网络的深度迁移学习,用于燃气轮机燃烧室的故障检测》论文阅读

    目录 突出 抽象 引言 1.1动机 1.2文献综述获得的结论 1.3贡献 1.4组织 2方法 2.1燃汽轮机组故障知识共享 2.2迁移学习 2.3 基于卷积神经网络的深度迁移学习 2.4用于燃气轮机燃 ...

  2. 基于卷积神经网络和投票机制的三维模型分类与检索 2019 论文笔记

    作者:白静 计算机辅助设计与图形学学报 1.解决的问题 由于三维模型投影得到的视图是由不同视点得到,具有相对独立性,这种像素级的融合运算并没有直接的物理或者几何意义,更有可能造成图像有益信息淹没和混淆 ...

  3. TextCNN——基于卷积神经网络的文本分类学习

    1.CNN基础内容 CNN的全称是Convolutional Neural Network,是一种前馈神经网络.由一个或多个卷积层.池化层以及顶部的全连接层组成,在图像处理领域表现出色. 本文主要学习 ...

  4. 读懂深度迁移学习,看这文就够了 | 赠书

    百度前首席科学家.斯坦福大学副教授吴恩达(Andrew Ng)曾经说过:迁移学习将是继监督学习之后的下一个促使机器学习成功商业化的驱动力. 本文选自<深度学习500问:AI工程师面试宝典> ...

  5. 一种基于卷积神经网络的图像去雾研究-含matlab代码

    目录 一.绪论 二.去雾卷积网络 2.1 特征提取 2.2 多尺度映射 2.3 局部均值 2.4 非线性回归 三.实验与分析 四.Matlab代码获取 一.绪论 雾是一种常见的大气现象,空气中悬浮的水 ...

  6. 机械臂论文笔记(一)【基于卷积神经网络的二指机械手 抓取姿态生成研究 】

    基于卷积神经网络的二指机械手 抓取姿态生成研究 论文下载 摘要 第1章 绪论 1.1 抓取生成国内外研究现状 1.1.1已知物体抓取生成 1.1.2相似物体抓取生成 1.1.3 未知物体抓取生成 1. ...

  7. 毕业设计 - 基于卷积神经网络的乳腺癌分类 深度学习 医学图像

    文章目录 1 前言 2 前言 3 数据集 3.1 良性样本 3.2 病变样本 4 开发环境 5 代码实现 5.1 实现流程 5.2 部分代码实现 5.2.1 导入库 5.2.2 图像加载 5.2.3 ...

  8. 基于卷积神经网络与迁移学习的油茶病害图像识别

    基于卷积神经网络与迁移学习的油茶病害图像识别 1.研究思路 利用深度卷积神经网络强大的特征学习和特征表达能力来自动学习油茶病害特征,并借助迁移学习方法将AlexNet模型在ImageNet图像数据集上 ...

  9. Python深度学习实例--基于卷积神经网络的小型数据处理(猫狗分类)

    Python深度学习实例--基于卷积神经网络的小型数据处理(猫狗分类) 1.卷积神经网络 1.1卷积神经网络简介 1.2卷积运算 1.3 深度学习与小数据问题的相关性 2.下载数据 2.1下载原始数据 ...

  10. 基于卷积神经网络实现图片风格的迁移 1

    卷积神经网络详解 一.实验介绍 1.1 实验内容 Prisma 是最近很火的一款APP,它能够将一张普通的图像转换成各种艺术风格的图像.本课程基于卷积神经网络,使用Caffe框架,探讨图片风格迁移背后 ...

最新文章

  1. 字节流和字符流复制文件内容实例
  2. Eclipse上安装GIT插件EGit及使用
  3. 修改Chrome的UserAgent
  4. mysql 日期查询今天_Mysql 日期查询今天、昨天、近7天、近30天、本月、上一月、本季...
  5. 大众eagit_试驾大众全新高尔夫GTI
  6. Leetcode-探索 | 买股票的最佳时机II
  7. (PPT素材)扁平图标、PNG免抠图小图片
  8. Python 电子书下载列表
  9. LifecycleBeanPostProcessor的作用
  10. poj2287田忌赛马
  11. 【数学建模】regress()函数进行回归分析| 美国人口预测
  12. 分享嵌入式开发使用过程中遇到的几个问题(MQX4.2,IAR,Kinetis K66)
  13. 数据科学与大数据技术专业毕业设计选题
  14. Dockers的安装卸载
  15. 3D游戏人物角色建模入门第一步:了解人体的构造
  16. SpiderMonkey相关学习资料
  17. 【Android -- 性能优化】耗电优化
  18. 计算机文化基础专科,计算机文化基础(专科)复习资料
  19. 硬盘测试软件黑屏,电脑接上硬盘就黑屏,是什么原因啊?
  20. 【源码】智能微电网的Simulink仿真

热门文章

  1. C++如何判断一个程序是 死锁 还是 死循环,如何进行问题定位与分析
  2. Arcgis用矢量文件裁剪栅格图像
  3. 立体几何——球缺问题
  4. bit.ly 短地址转换_使用PHP创建Bit.ly短URL:API版本3
  5. cpu超线程优缺点_CPU有无超线程重要吗?i7 10700K与9700K对比测试
  6. html图片加标题加链接,手机移动网页制作:插入图片、标题、文字链接
  7. C语言用函数max求两个数的最大值
  8. win10 休眠唤醒 电源_一劳永逸解决WIN10所有睡眠问题
  9. html5 怎么插指南针,HTML5 App实战(5):指南针
  10. php trying to get,php 做微信认证登陆 返回错误 Trying to get property of non-object