Detectron2解读全部文章链接:
7. 数据增强
链接:https://detectron2.readthedocs.io/en/latest/tutorials/getting_started.html
数据增强是训练中很重要的一个环节,Detectron2 的数据增强需要达成一下目标:
- 可以同时对很多种数据进行数据增强(比如图片,gt框,gt边缘等等)。
- 可以同时进行很多种数据增强。
- 可以允许自定义数据增强类型。
- 可以在中途对数据增强进行控制。
前两种基本满足了大部分需求。这一部分介绍如何根据自己想用的数据增强来创建新的 Dataloader,以及如何创建新的数据增强类型。不过现在默认的 dataloader 已经允许用户自定义数据增强,在前面 Dataloader 部分有所提及。
基本操作
(1),(2) 可以这样基本地使用:
from detectron2.data import transforms as T
# 定义一堆数据增强:
augs = T.AugmentationList([
T.RandomBrightness(0.9, 1.1),
T.RandomFlip(prob=0.5),
T.RandomCrop("absolute", (640, 640))
])
# 定义数据增强的输入(必须输入图片,其它输入可选)。=
input = T.AugInput(image, boxes=boxes, sem_seg=sem_seg)
# 实施数据增强
transform = augs(input) # 类型:T.Transform
image_transformed = input.image # 新的图像
sem_seg_transformed = input.sem_Seg # 新的语义分割
# 除了input这些图片之外,如果你想进行额外的操作:
image2_transformed = transform.apply_image(image2)
polygons_transformed = transform.apply_polygons(polygons)
基础概念:
- T.Augmentation 定义了如何去改变输入数据。
- 它的 __ call __(AugInput) -> Transform 方法 in-place 地增强图片,并且返回它进行的操作。
- T.Transform 对数据应用具体操作。
- 比如它的 apply_image, apply_coords 方法,定义了怎么转换各种类型的数据。
- T.AugInput 存储了 T.Augmentation 需要的输入数据和它们需要怎样被转换。这个概念可能有一些更高级的应用。一般情况下,使用这个类即可满足任何需求,因为额外的数据,即使没在 T.AugInput 中,也可以使用返回的 transform 来进行增强。
定义新的数据增强
绝大多数的 2D 数据增强仅仅需要图片的信息,这种类型可以轻松实现:
class MyColorAugmentation(T.Augmentation):
def get_transform(self, image):
r = np.random.rand(2)
return T.ColorTransform(lambda x: x * r[0] + r[1] * 10)
class MyCustomResize(T.Augmentation):
def get_transform(self, image):
old_h, old_w = image.shape[:2]
new_h, new_w = int(old_h * np.random.rand()), int(old_w * 1.5)
return T.ResizeTransform(old_h, old_w, new_h, new_w)
augs = MyCustomResize()
transform = augs(input)
如果增强的时候还需要一些额外的信息,你只需要在 get_transform 方法中多加一些参数:
class MyCustomCrop(T.Augmentation):
def get_transform(self, image, sem_seg):
...
return T.CropTransform(...)
augs = MyCustomCrop()
assert hasattr(input, "image") and hasattr(input, "sem_seg")
transform = augs(input)
如果你需要更多的转换操作,你还可以给 T.Transform 定义更多子类。
高级操作
我们提供一些高级操作的例子,以供研究者参考。
自定义transform策略
你如果不想仅仅返回增强后的数据,Augmentation类会把数据增强的操作返回成 T.Transform。这可以允许用户自定义 Transform 策略,比如,在关键点检测任务中:
关键点是(x,y)坐标,在数据增强中我们是需要这些关键点的因为这些关键点携带语义信息,所以,在进行数据增强时我们要同时考虑这些关键点,所以我们要考虑返回的 transform。比如,如果一个图片被水平翻转了,那么 ”左眼“ 和 ”右眼“ 的关键点需要被交换,我们可以这么做:
# augs 和 inputs 的定义和上个例子一样
transform = augs(input) # 类型 T.Transform
keypoints_xy = transform.apply_coords(keypoints_xy) # 对坐标进行transform
# 获得所有增强策略的 List:
transforms = T.TransformList([transform]).transforms
# 看看是不是被翻转了奇数次
do_hflip = sum(isinstance(t, T.HFlipTransform) for t in transforms) % 2 == 1
if do_hflip:
keypoints_xy = keypoints_xy[flip_indices_mapping]
另外一个例子,关键点检测任务中,观测点都有一个“能见度”。一组增强策略可能会首先把一个关键点变得不可见(比如裁剪,会导致关键点在图片外),随后再把对应位置补零,又导致关键点在图片内。此时用户可能会希望把这个关键点标记为“不可见”,那么每一次增强操作我们都要检查一下“能见度”:
transform = augs(input) # type:T.TransformList
assert isinstance(transform, T.TransformList)
for t in transform.transforms:
keypoints_xy = t.apply_coords(keypoints_xy)
visibility &= (keypoints_xy >= [0,0] & keypoints_xy <= [W,H]).all(axis=1)
# 注意,Detectron2内置的 transform_keypoint_annotations 在这种情况下会选择将该点标记为 Visible
# keypoints_xy = transform.apply_coords(keypoints_xy)
# visibility &= (keypoints_xy >= [0,0] & keypoints_xy <= [W,H]).all(axis=1)
transform的逆操作
如果在推理之前,一张图片被transform了,那么我们需要对预测的结果进行该transform的逆操作去取得正确的结果。我们可以使用 inverse() 方法:
transform = augs(input)
pred_mask = make_prediction(input.image)
inv_transform = transform.inverse()
pred_mask_orig = inv_transform.apply_segmentation(pred_mask)
新的数据类型
T.Transform 支持一些常见的数据类型的数据增强,比如 图片,mask,坐标,多边形。如果需要对新的数据类型进行 transform,我们需要注册该类型:
@T.HFlipTransform.register_type("rotated_boxes")
def func(flip_transform: T.HFlipTransform, rotated_box: Any):
...
return flipped_rotated_boxes
t = HFlipTransform(width=800)
transformed_rotated_boxes = t.apply_rotated_boxes(rotated_boxes) # 函数会被调用
T.AugInput 的扩展
一种数据增强只能访问固定的成员,包括 图片,检测框,分割图形,等等,这些可以满足绝大部分数据增强的情况。如果你需要数据增强可以访问其他内容,你需要自定义一个 T.AugInput。
你需要重新实现 AugInput 的 transform() 方法,你可以以一种相互关联的方法来进行数据增强,这种情况不常见(比如,你需要根据增强后的 mask 去处理检测框),但是是可以实现的。
8. 使用模型
Detectron2 中的模型是被 build_model, build_backbone, build_roi_heads 等函数创建的:
from detectron2.modeling import build_model
model = build_model(cfg) # 返回一个 torch.nn.Module
build_model 只会返回模型结构(随机初始化的参数)。之后我们研究怎么加载模型以及对模型对象进行操作。
加载/保存模型
from detectron2.checkpoint import DetectionCheckpointer
# 加载模型,通常情况下是从 cfg.MODEL.WEIGHTS 的路径加载
DetectionCheckpointer(model).load(file_path_or_url)
checkpointer = DetectionCheckpointer(model, save_dir="output")
checkpointer.save("model_999") # 保存 output/model_999.pth
Checkpointer 对象可以识别 .pth 和 .pkl 格式的模型,具体参考API。
模型文件可以被任意处理,比如 torch.{load,save} (针对.pth),或者 pickle.{dump,load} (针对.pkl)
使用模型
我们可以使用 outputs = model(inputs)来调用模型,这里 inputs 是一个 list[dict],每一个字典对应一张图片,这个字典包含推理所需要的参数,同时这也取决与 model 是在 training mode 还是 evaluation mode。比如,进行单张推理时,我们希望 dict 包含 图片,或者可选的 height 和 width。具体的输入和输出格式如下:
训练: 训练时,所有模型需要在 EventStorage 中被使用,训练的所有统计信息会被记录:
from detectron2.utils.events import EventStorage
with EventStorage() as storage:
losses = model(inputs)
推理: 如果你希望进行单张图片的推理,你可以使用 DefaultPredictor,DefaultPredict 包含了基础的加载模型,预处理,以及单张推理的功能。具体使用方法可以参考 API。或者,你可以使用如下方法:
model.eval()
with torch.no_grad():
outputs = model(inputs)
模型的输入格式
用户在使用自定义的模型时可以采用任何形式的输入。这里我们介绍内置模型标准的输入格式 - 所有输入都是 list[dict] 形式的。每一个 dict 包含了一张图片的信息,它可能包含这些键:
- “image” (Tensor): 是 (C,H,W) 格式的。C 的定义在 cfg.INPUT.FORMAT中被定义。图片的归一化会根据 cfg.MODEL.PIXEL_{mean,STD} 中进行操作。
- “height”, “width”:我们希望的输出的高和宽,这意味着它不需要和 “image” 的输入保持一致。比如说 “image” 在预处理中被缩放,但是你希望输出是原本的分辨率,你可以在这里设定,模型会自动输出你设定的分辨率的图片。
- “instances”:训练中需要用到的物体标注,它包含了这些键:
- “gt_boxes": 一个 Boxes 对象,存储了 N 个 gt 框,每一个对应了一个物体。
- “gt_classes”: Long 类型的 Tensor,长度为 N,值在 [0, 类别数] 中间。
- “gt_masks”: 一个 PolygonMasks 或者 BitMasks 对象,存储了 N 个物体的mask。
- “gt_keypoints”: 一个 Keypoints 对象,存储了 N 个关键点集合,每个对应了一个物体。
- “sem_seg”: Tensor[int], (H,W) 格式,语义分割中的 gt,单通道图,值对应类别。
- ”proposals": 只有 Fast R-CNN 类型的模型才需要,需要包含:
- “proposal_boxes”: 一个 Boxes 对象包含了 P 个先验框。
- “objectness_logits”: Tensor,包含了 P 个评分,对应 P 个先验框。
推理模型时,只需要 “image” 即可, “width/height” 是可选的。
目前对于全景分割我们没有标准输入格式,因为全景分割的 Dataloader 是自定义的。
**模型如何链接到 dataloader: **
默认 DatasetMapper 的输出是一个如上格式的字典,进行 batching 操作之后即会变为 list[dict],之后便可以输入给模型。
模型的输出格式
训练模式下,模型输出 dict[str->ScalarTensor],包含了所有 loss。推理时,模型输出一个 list[dict],每一个 dict 对应一张图,可能会包含如下内容:
- “instances”: 每一个对应了一个物体,包含了如下内容:
- “pred_boxes”:Boxes对象,包含了 N 个检测框,每个包含了一个物体。
- “scores”:Tensor,包含了 N 个 confidence 值。
- “pred_classes”:Tensor,包含了 N 个物体的类别。
- “pred_masks”: Tensor,(N,H,W) 格式,每一个对应一个物体的 mask。
- “pred_keypoints”:Tensor,(N, 关键点数量,3)。每一行的最后一个维度是 (x, y, score)。
- “sem_seg”: Tensor,(类别数, H, W) 格式,每一个类别的语义分割。
- “proposals”: Instances 对象,包含了如下内容:
- “proposal_boxes”:Boxes 对象,包含了 N 个先验框。
- “objectness_logits”:Torch vector,包含了 N 个先验框的 confidence。
- “panoptic_seg”: Tuple,(pred: Tensor, segments_info: Optional[list[dict]]),pred tensor 是一个 (H,W) 格式的tensor,包含了每个像素的 segment id。
- segments_info 具体查看官方文档
只运行部分模型
有时候你需要查看过程中的 Tensor,比如某一层而得输出,或者后处理之前的输出。因为可能存在几百个中间输出,模型没有提供相关的 API,如果你需要这个功能,你可以:
- 重写模型的某个内容,返回你需要的部分。
- 像正常一样创建模型,但是不使用 forward(),而是仅仅运行部分组件,比如:
images = ImageList.from_tensors(...) # 预处理后的 input
model = build_model(cfg)
model.eval()
features = model.backbone(images.tensor)
proposals, _ = model.proposal_generator(images,features)
instances, _ = model.roi_heads(images, features, proposals)
mask_features = [features[f] for f in model.roi_heads.in_features]
mask_features = model.roi_heads.mask_pooler(mask_features, [x.pred_boxes for x in instances])
- 使用 forward hooks,具体请查看pytorch官方文档。
你确实需要充分理解代码和内部逻辑,才能获取中间层的输出。
9. 写自己的模型
如果你试图写自己的模型,你可以试图更改现有的模型,或者,我们提供了方法使你可以覆写模型的组件:
注册新的组件
如果你想使用你自己的 “backbone feature extractor” 或者 “box head”,我们为你提供了一种注册机制,用户可以通过这种机制把自定义的组建交给框架使用,以及直接在 config 中调用。
比如,如果你想写一个自己的 Backbone:
from detectron2.modeling import BACKBONE_REGISTRY, Backbone, ShapeSpec
@BACKBONE_REGISTRY.register()
class ToyBackbone(Backbone):
def __init__(self, cfg, input_shape):
super().__init__()
# 创建你自己的 backbone:
self.conv1 = nn.Conv2d(...)
def forward(self, image):
return {"conv1":self.conv1(image)}
def output_shape(self):
return {"conv1":ShapeSpec(channels=64,stride=16)}
在这段代码中,我们的类继承了 Backbone 类,并且把它注册到了 BACKBONE_REGISTRY 中。如果你 import 了这段代码,你就可以:
cfg = ... # 读取 config
cfg.MODEL.BACKBONE.NAME = 'ToyBackbone' # 或者你可以在 config file 中更改
model = build_model(cfg)
举个其它例子,如果你想在 Generalized R-CNN 中定义一个自己的 ROI heads,你需要创建一个 ROIHeads 的子类,并且注册到 ROI_HEADS_REGISTRY 中。在 projects/ 文件夹中有一些自定义的模型的例子可以参考。
所有可以注册的组建可以在这个 API 中查询。
显式地创建模型
Registry 的作用实际上是连接 config files 和具体的代码,它仅仅可以允许用户在常用的组件中进行切换,如果你需要进行更深层次的自定义,最好的方法还是写代码。
大部分模型的组建都包含一个 __ init __ 方法来记录它需要的参数,你可以修改这些参数来调用它们。
比如,如果你需要在 Faster R-CNN 的 box-head 中使用自定义的损失函数,你可以:
- Losses 在 FastRCNNOutputLayers 中被计算,你可以实现一个它的子类,比如叫 MyRCNNOutput.
- 调用 StandardROIHeads 时使用 box_predictor=MyRCNNOutput() 参数。如果其它部分不变,我们可以简单地使用可调节的 __ init __ 机制:
roi_heads = StandardROIHeads(
cfg, backbone.output_shape(),
box_predictor=MyRCNNOutput(...)
)
- (可选)如果你需要从 config 中使用这个修改过的模型,你需要注册它
@ROI_HEADS_REGISTRY.register()
class MyStandardROIHeads(StandardROIHeads):
def __init__(self, cfg, input_shape):
super().__init__(cfg, input_shape, box_predictor=MyRCNNOutput(...))
10. 训练
如果你实践了前面部分的内容,你应该已经有了一个自定义的模型和一个 data loader,为了训练这个网络,你可以使用如下两种方法:
自定义训练循环
只要你有了模型和data loader,剩下要做的就是用 pytorch 的常规训练流程就行了,你完全可以自己写一个训练循环。这样的话用户便可以有对整个训练流程的完全控制。具体可以参考 tools/plain_train_net.py。
训练器抽象
同样的,我们提供了几种标准的带hook的训练器抽象,使用它们可以简化传统的训练模式,它们包含了如下两个实例化:
- SimpleTrainer 提供了最简单的 单损失,单优化器,单数据集的 训练循环,没有其它的任何功能(包括保存,记录等),这些功能可以通过 hook 来实现。
- DefaultTrainer 继承自 SimpleTrainer, 在 tools/train_net.py 和很多脚本中被使用。DefaultTrainer 从 config 初始化,包含了一些允许用户自定义的更加标准化的操作,比如优化器的选择、学习率的规划、记录日志、保存模型、评测模型等。
如果你需要自定义一个 DefaultTrainer 的话。
- 如果你仅仅需要一些简单的自定义操作(改变优化器,学习率规划等),你只需要创建一个 DefaultTrainer 的子类,然后重写对应的方法即可。
- 如果你需要实现更加复杂的功能,可以使用 hook。
举个例子,比如你需要在每一步迭代时打印 “Hello”:
class HelloHook(HookBase):
def after_step(self):
if self.trainer.iter % 100 == 0:
print(f"Hello at iteration {self.trainer.iter}!")
- 使用一个 trainer + hook 的系统意味着你需要实现一些 Detectron2 不支持的操作,尤其是在进行研究项目的时候。因为这个原因,我们有意将 trainer + hook 系统设计为最简单的形式,而不是功能最强大的形式。如果这样还支持你的操作,你仅仅需要自己设计一个训练脚本。
记录日志
训练过程中,Detectron2 会把日志记录到 EventStorage 中。你可以这样去添加其它东西:
from detectron2.utils.events import get_event_storage
# 在模型内部:
if self.training:
value = # 从input计算value
storage = get_event_storage()
storage.put_scalar("some_accuracy", value)
更多细节查看 EventStorage 的文档。
11. 评测模型
你总是可以直接使用模型进行逐图片推理来完成模型的评测。或者,你可以使用我们内置的评测器 DatasetEvaluator。
Detectron2 包含了一些 DatasetEvaluator 来计算模型在常用数据集上的表现 (COCO, LVIS等)。你可以实现你自己的 DatasetEvaluator 来完成一些其他任务。比如,如果你需要计数模型在验证集上面检测到了多少物体:
class Counter(DatasetEvaluator):
def reset(self):
self.count = 0
def process(self, inputs, outputs):
for output in outputs:
self.count += len(output['instances'])
def evaluate(self):
# 保存 self.count
return {"count": self.count}
使用evaluators
你可以使用evaluator自带的方法来评测模型:
def get_all_inputs_outputs():
for data in data_loader:
yield data, model(data)
evaluator.reset()
for inputs, outputs in get_all_inputs_outputs():
evaluator.process(inputs, outputs)
eval_results = evaluator.evaluate()
自定义数据集上的评测器
实际上 Detectron2 内置的评测器大多都是为了一些特定的数据集设计的,目的是测试模型在通用数据集上的官方表现。同时,以下两个评测器可以评测所有 Detectron2 支持的数据集格式:
- COCOEvaluator 可以在任何自定义数据集(COCO格式)上评测目标检测、实例分割、关键点检测的AP。
- SemSegEvaluator 可以评测任何自定义数据集上语义分割的表现。
12. Configs
Detectron2 提供了一组 键-值 对应的系统去拼接一些标准模型。
Detectron2 的 config 文件都使用 YAML 或者 yacs 格式。此外,我们提供了一些基础操作来访问或者更新这些 config。
- Config 可以有 _ BASE _: base.yaml 的键,这样会使 config 首先加载 base config, 然后子 config 会重写 base config 中的条目。对于一些常见的模型,我们提供了 base configs.
- 为了提供反向的兼容性,我们提供了 config 的版本机制。如果你的 config 加入了版本信息,比如 VERSION: 2,Detectron2 可以把它识别出来,哪怕以后一些键值被改动了。
总的来说,config 所能提供的功能非常有限,我们事实上也不期望 config 可以实现所有功能。如果你需要改动的内容在 config 中没有提供,请查阅 API 并重写代码。
Config 的基础操作
这里列出了一些 CfgNode 对象的基础操作,更多请查看文档。
from detectron2.config import get_cfg
cfg = get_cfg() # 获得 detectron2 的默认 config
cfg.xxx = yyy # 加入新的 config 条目
cfg.merge_from_file("my_cfg.yaml") # 从文件中读取 config
cfg.merge_from_list(["MODEL.WEIGHTS", "weights.pth"]) # 同时可以从list或者str中读取config
print(cfg.dump()) # 打印config
大部分 Detectron2 提供的工具允许在命令行重写 config,你只需要在命令行中提供 键-值 对即可,比如你可以这样使用 demo.py
./demo.py --config-file config.yaml [--other-options] \
--opts MODEL.WEIGHTS /path/to/weights INPUT.MIN_SIZE_TEST 1000
对于 Detectron2 内置的 config 的所有内容请查阅 这里 (后续会写文介绍)
Projects中的Config
一个非内置模型可以定义它自己的 configs,这仅仅需要简单的操作让它生效:
from detectron2.projects.point_rend import add_pointrend_config
cfg = get_cfg()
add_pointrend_config(cfg)
如何最好地使用 Config
- 把 configs 视作代码,避免经常复制,如果有很多重复的部分,使用 __ BASE __。
- 保证你写的 configs 尽可能简单,不要加入一些不会影响整体实验的条目。
- 记录你自己 config 的 version.
13. 部署
(略)如果有部署需求的小伙伴可以自行查看官方文档。