文章目录[隐藏]
提出了一种对比表征嵌入的方法来来实现小样本目标检测,观察到使用不同的 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 学习易于对比的表征嵌入。这个对比分支可以作为二阶段网络的一个即插即用模块。
采用基于余弦相似度的 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 的重叠 IOU
overlaps = 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] = 1
labels[max_overlaps >= cfg.FLAGS.rpn_positive_overlap] = 1
# 如果需要抑制 positive 的 anchor,就将背景 anchor 后赋值,# 在这里将最大 IoU 仍然小于阈值(0.3)的某些 anchor 置0
if 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=128
fg_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,其余的都设为-1
labels[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 delta
outputs.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: off
self.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
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 list
argument 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 list
argument 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 list
argument 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 list
argument 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.NAME
backbone = 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 = value
def __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_loader
data 文件夹下的 __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 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),
)
--------------------------------------------------------------------------------------
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 dicts
DatasetCatalog.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 logging
MetadataCatalog.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_id
ret["novel_classes"] = novel_classes
ret["base_dataset_id_to_contiguous_id"] = base_dataset_id_to_contiguous_id
ret["base_classes"] = base_classes
return ret
def 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.prob
if 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 / 255
noise = np.random.normal(self.mean, self.sigma, img.shape)
img = img + noise
img = 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 variables
self._dataset_name = dataset_name
def reset(self): # reset predictions
self._predictions = []
def process(self, inputs, outputs): # prepare predictions for evaluation
for 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 predictions
results = 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 += 1
with _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 = epoch
if 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 = data
param_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 scheduler
raise 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_iter
self.max_iter = max_iter
with 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 state
for p, momentum_buffer in zip(params_with_grad, momentum_buffer_list):
state = self.state[p]
state['momentum_buffer'] = momentum_buffer
return loss
之后在 after_step() 中调用 LRScheduler 这个 hook 的 after_step() 函数,在这个函数中执行了 scheduler.step()。
'这个 hook 中主要关注 after_step() 这个函数'
class LRScheduler(HookBase):
def __init__(self, optimizer, scheduler):
self._optimizer = optimizer
self._scheduler = scheduler
def 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 按训练流程不停循环至结束。
版权声明:本文为CSDN博主「不说话装高手H」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/AmbitionalH/article/details/118896507
暂无评论