Detectron2(到底是怎么用的?)

Detectron2(到底是怎么用的?)

在这里插入图片描述

目录

引言:Detectron2是一个用于计算机视觉的PyTorch库,主要用于目标检测、实例分割和关键点检测等任务。Detectron2是2020年三大计算机视觉框架之一。目前很多研究基于Detectron2的基础上进行,因此,有必要熟悉下Detetron2的一些基本知识。

官网:

https://github.com/facebookresearch/detectron2

https://detectron2.readthedocs.io/en/latest/

官方代码中有很多Python的其他知识,阅读本文后可以阅读下以下文章,深度了解Detectron2:
detectron2中的configurable函数——Python装饰器实例
detectron2中的DatasetMapper类——detectron2如何做数据增强


安装

需求

  • Linux or macOS with Python ≥ 3.7(这里好像没得windows)
  • PyTorch ≥ 1.8 and torchvision that matches the PyTorch installation. Install them together at pytorch.org to make sure of this
  • OpenCV is optional but needed by demo and visualization

具体参考:Installation


Getting Started with Detectron2

官方给出了一个Colab Notebook的教程:地址。主要介绍了如何用现有模型运行推理,以及如何在自定义数据集上训练一个内置模型。

预训练模型的推理演示

克隆整个项目,进入demo,运行下列命令:

cd demo/
python demo.py --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml \
  --input input1.jpg input2.jpg \
  [--other-options]
  --opts MODEL.WEIGHTS detectron2://COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x/137849600/model_final_f10217.pkl

--config-file:训练参数配置

扫描二维码关注公众号,回复: 16175058 查看本文章

--input:输入图片

--opts:指定其他参数,如模型

关于命令行参数的细节,请参见demo.py -h或查看其源代码以了解其作用。一些常见的参数是:

  • To run on your webcam, replace --input files with --webcam.
  • To run on a video, replace --input files with --video-input video.mp4.
  • To run on cpu, add MODEL.DEVICE cpu after --opts.
  • To save outputs to a directory (for images) or a file (for webcam or video), use --output.

在命令行中进行训练和评估

官方提供了两个脚本进行训练“tools/plain_train_net.py” 和“tools/train_net.py”

与 "train_net.py "相比,"plain_train_net.py "支持较少的默认特征。它也包括较少的抽象,因此更容易添加自定义逻辑。

要用 "train_net.py "训练一个模型,首先按照datasets/README.md设置相应的数据集,然后运行:

cd tools/
# 8GPU训练
./train_net.py --num-gpus 8 \
  --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml
# 1GPU训练
./train_net.py \
  --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml \
  --num-gpus 1 SOLVER.IMS_PER_BATCH 2 SOLVER.BASE_LR 0.0025
# 评估
./train_net.py \
  --config-file ../configs/COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_1x.yaml \
  --eval-only MODEL.WEIGHTS /path/to/checkpoint_file

更多指令运行 ./train_net.py -h查看。


Use Builtin Datasets

Detectron2对一些数据集有内置的支持。这些数据集被假定存在于由环境变量DETECTRON2_DATASETS指定的目录中。在该目录下,Detectron2将寻找以下结构的数据集:

$DETECTRON2_DATASETS/
  coco/
  lvis/
  cityscapes/
  VOC20{07,12}/

每个数据集有不同的结构,如果Pascal VOC数据集格式:

VOC20{07,12}/
  Annotations/
  ImageSets/
    Main/
      trainval.txt
      test.txt
      # train.txt or val.txt, if you use these splits
  JPEGImages/

Extend Detectron2’s Defaults

在detectron2中提供了两种类型的接口帮助用户方便使用,官方介绍:

  1. 接受从yaml文件中创建的配置(cfg)参数的函数和类(有时有几个额外参数)。
    这样的函数和类实现了 "标准的默认 "行为:它将从一个给定的配置中读取它所需要的东西并做 "标准 "的事情。用户只需要加载一个专家制作的配置并将其传递出去,而不必担心哪些参数被使用以及它们都意味着什么。
    详情查看 Yacs Configs
  2. 具有明确定义的显式参数的函数和类。
    每一个都是整个系统的一个小构件。它们需要用户的专业知识来理解每个论点应该是什么,并且需要更多的努力来拼接成一个更大的系统。但它们可以用更灵活的方式拼接起来。
    当你需要实现detectron2中包含的 "标准默认值 "不支持的东西时,这些定义良好的组件可以被重复使用。
    LazyConfig系统依赖于这样的函数和类。
  3. 一些函数和类是用@configurable装饰器实现的–它们既可以用配置来调用,也可以用显式参数来调用,或者两者的混合。它们的显式参数接口目前是实验性的。
    作为一个例子,一个Mask R-CNN模型可以通过以下方式建立:
    • 点击展开
      # Config-only:
      # load proper yaml config file, then
      model = build_model(cfg)
      
      # Mixture of config and additional argument overrides:
      model = GeneralizedRCNN(
        cfg,
        roi_heads=StandardROIHeads(cfg, batch_size_per_image=666),
        pixel_std=[57.0, 57.0, 57.0])
        
      # Full explicit arguments:
      model = GeneralizedRCNN(
          backbone=FPN(
              ResNet(
                  BasicStem(3, 64, norm="FrozenBN"),
                  ResNet.make_default_stages(50, stride_in_1x1=True, norm="FrozenBN"),
                  out_features=["res2", "res3", "res4", "res5"],
              ).freeze(2),
              ["res2", "res3", "res4", "res5"],
              256,
              top_block=LastLevelMaxPool(),
          ),
          proposal_generator=RPN(
              in_features=["p2", "p3", "p4", "p5", "p6"],
              head=StandardRPNHead(in_channels=256, num_anchors=3),
              anchor_generator=DefaultAnchorGenerator(
                  sizes=[[32], [64], [128], [256], [512]],
                  aspect_ratios=[0.5, 1.0, 2.0],
                  strides=[4, 8, 16, 32, 64],
                  offset=0.0,
              ),
              anchor_matcher=Matcher([0.3, 0.7], [0, -1, 1], allow_low_quality_matches=True),
              box2box_transform=Box2BoxTransform([1.0, 1.0, 1.0, 1.0]),
              batch_size_per_image=256,
              positive_fraction=0.5,
              pre_nms_topk=(2000, 1000),
              post_nms_topk=(1000, 1000),
              nms_thresh=0.7,
          ),
          roi_heads=StandardROIHeads(
              num_classes=80,
              batch_size_per_image=512,
              positive_fraction=0.25,
              proposal_matcher=Matcher([0.5], [0, 1], allow_low_quality_matches=False),
              box_in_features=["p2", "p3", "p4", "p5"],
              box_pooler=ROIPooler(7, (1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32), 0, "ROIAlignV2"),
              box_head=FastRCNNConvFCHead(
                  ShapeSpec(channels=256, height=7, width=7), conv_dims=[], fc_dims=[1024, 1024]
              ),
              box_predictor=FastRCNNOutputLayers(
                  ShapeSpec(channels=1024),
                  test_score_thresh=0.05,
                  box2box_transform=Box2BoxTransform((10, 10, 5, 5)),
                  num_classes=80,
              ),
              mask_in_features=["p2", "p3", "p4", "p5"],
              mask_pooler=ROIPooler(14, (1.0 / 4, 1.0 / 8, 1.0 / 16, 1.0 / 32), 0, "ROIAlignV2"),
              mask_head=MaskRCNNConvUpsampleHead(
                  ShapeSpec(channels=256, width=14, height=14),
                  num_classes=80,
                  conv_dims=[256, 256, 256, 256, 256],
              ),
          ),
          pixel_mean=[103.530, 116.280, 123.675],
          pixel_std=[1.0, 1.0, 1.0],
          input_format="BGR",
      )
      
      

上面这段话啥意思呢?看个例子

#vscode中运行下面代码
# %%
from detectron2.config import get_cfg
cfg = get_cfg()    # obtain detectron2's default config
print(cfg.dump())
# %%
cfg.merge_from_file("/home/server/cv/pzq/detectron2/configs/PascalVOC-Detection/faster_rcnn_R_50_C4.yaml")
print(cfg.dump())
# %%
cfg.merge_from_list(["MODEL.WEIGHTS", "weights.pth"])   # can also load values from a list of str
print(cfg.dump())  # print formatted configs
with open("output.yaml", "w") as f:
  f.write(cfg.dump())   # save config to file

运行第一段代码时,打印了这些
运行第一段代码时,打印了这些
第二段代码加载了faster_rcnn_R-50_C4.yaml后再次打印
第二段代码加载了faster_rcnn_R-50_C4.yaml后再次打印
可以看到相关参数已经被修改
可以看到相关参数已经被修改
运行第三段代码会得到yaml文件,可以看到通过代码修改的参数也存在
运行第三段代码会得到yaml文件,可以看到通过代码修改的参数也存在

这样子很方便用户对参数进行修改,增删操作。

如果要根据自己的需求扩展 detectron2,请参阅以下教程了解更多详细信息:

用户自定义方法:

  • Detectron2 includes a few standard datasets. To use custom ones, see Use Custom Datasets.
  • Detectron2 contains the standard logic that creates a data loader for training/testing from a dataset, but you can write your own as well. See Use Custom Data Loaders.
  • Detectron2 implements many standard detection models, and provide ways for you to overwrite their behaviors. See Use Models and Write Models.
  • Detectron2 provides a default training loop that is good for common training tasks. You can customize it with hooks, or write your own loop instead. See training.

Use Custom Datasets

detectron2 中的内置支持的数据集被列在builtin datasets

如果您想在使用自定义数据集的同时重用 detectron2 的数据加载器,则需要:

  1. Register your dataset (i.e., tell detectron2 how to obtain your dataset).
  2. Optionally, register metadata for your dataset.

Colab 教程有一个实例,说明如何在自定义格式的数据集上注册和训练。这里简单叙述下方法:

如何注册和训练自己的数据集

首先准备数据集:这里用官方提供的数据集,其目录如下

├── train
│   ├── 10464445726_6f1e3bbe6a_k.jpg
│   ├── ……
│   └── via_region_data.json
└── val
    ├── 14898532020_ba6199dd22_k.jpg
    ├── ……
    └── via_region_data.json

数据标签:这是一个包含注释信息的JSON格式数据,其中包括文件名、文件大小、对象位置和类别等信息。
这是一个包含注释信息的JSON格式数据,其中包括文件名、文件大小、对象位置和类别等信息。

# if your dataset is in COCO format, this cell can be replaced by the following three lines:
# from detectron2.data.datasets import register_coco_instances
# register_coco_instances("my_dataset_train", {}, "json_annotation_train.json", "path/to/image/dir")
# register_coco_instances("my_dataset_val", {}, "json_annotation_val.json", "path/to/image/dir")
import os, json, cv2, random
import numpy as np
from detectron2.data import MetadataCatalog, DatasetCatalog
from detectron2.structures import BoxMode

def get_balloon_dicts(img_dir):
    json_file = os.path.join(img_dir, "via_region_data.json")
    with open(json_file) as f:
        imgs_anns = json.load(f)

    dataset_dicts = []
    for idx, v in enumerate(imgs_anns.values()):
        record = {
     
     }
        
        filename = os.path.join(img_dir, v["filename"])
        height, width = cv2.imread(filename).shape[:2]
        
        record["file_name"] = filename
        record["image_id"] = idx
        record["height"] = height
        record["width"] = width
      
        annos = v["regions"]
        objs = []
        for _, anno in annos.items():
            assert not anno["region_attributes"]
            anno = anno["shape_attributes"]
            px = anno["all_points_x"]
            py = anno["all_points_y"]
            poly = [(x + 0.5, y + 0.5) for x, y in zip(px, py)]
            poly = [p for x in poly for p in x]

            obj = {
     
     
                "bbox": [np.min(px), np.min(py), np.max(px), np.max(py)],
                "bbox_mode": BoxMode.XYXY_ABS,
                "segmentation": [poly],
                "category_id": 0,
            }
            objs.append(obj)
        record["annotations"] = objs
        dataset_dicts.append(record)
    return dataset_dicts

for d in ["train", "val"]:
    DatasetCatalog.register("balloon_" + d, lambda d=d: get_balloon_dicts("balloon/" + d))  # 第一步注册,(balloon_train,balloon/train)
    MetadataCatalog.get("balloon_" + d).set(thing_classes=["balloon"])                      # 第二部get
balloon_metadata = MetadataCatalog.get("balloon_train")

# %%
import matplotlib.pyplot as plt
from detectron2.utils.visualizer import Visualizer

dataset_dicts = get_balloon_dicts("datasets/balloon/train")
for d in random.sample(dataset_dicts, 3):
    img = cv2.imread(d["file_name"])
    visualizer = Visualizer(img[:, :, ::-1], metadata=balloon_metadata, scale=0.5)
    out = visualizer.draw_dataset_dict(d)
    plt.imshow(out.get_image())

展示下图片
在这里插入图片描述

Register a Dataset

为了让 detectron2 知道如何获得名为 "my_dataset "的数据集,用户需要实现一个函数来返回你的数据集中的items ,然后告诉 detectron2 这个函数:

def my_dataset_function():
  ...
  return list[dict] in the following format

from detectron2.data import DatasetCatalog
DatasetCatalog.register("my_dataset", my_dataset_function)
# later, to access the data:
data: List[Dict] = DatasetCatalog.get("my_dataset")
# 这就是对应的案例中的balloon数据集

这里,该片段将一个名为 "my_dataset "的数据集与一个返回数据的函数联系起来。如果多次调用,该函数必须返回相同的数据(以相同的顺序)。该注册在进程退出前一直有效。

该函数可以做任意的事情,应该返回list[dict]中的数据,每个dict都是以下格式中的一种:

  1. Detectron2的标准数据集dict,如下所述。这将使其与detectron2中的许多其他内置功能一起工作,所以建议在足够的时候使用它。
  2. 任何自定义格式。你也可以用你自己的格式返回任意的dict,比如为新任务添加额外的键。那么你也需要在下游正确处理它们。更多细节见下文。

Standard Dataset Dicts(这里说的是数据集格式,就不多讲了)

Task Fields
Common file_name, height, width, image_id
Instance detection/segmentation annotations
Semantic segmentation sem_seg_file_name
Panoptic segmentation pan_seg_file_name, segments_info
  • file_name: the full path to the image file.
  • height, width: integer. The shape of the image.
  • image_id (str or int): a unique id that identifies this image. Required by many evaluators to identify the images, but a dataset may use it for different purposes.
  • annotations (list[dict]): Required by instance detection/segmentation or keypoint detection tasks. Each dict corresponds to annotations of one instance in this image, and may contain the following keys:
    • bbox (list[float], required): list of 4 numbers representing the bounding box of the instance.

    • bbox_mode (int, required): the format of bbox. It must be a member of structures.BoxMode. Currently supports: BoxMode.XYXY_ABS, BoxMode.XYWH_ABS.

    • category_id (int, required): an integer in the range [0, num_categories-1] representing the category label. The value num_categories is reserved to represent the “background” category, if applicable.

    • segmentation (list[list[float]] or dict): the segmentation mask of the instance.

      • If list[list[float]], it represents a list of polygons, one for each connected component of the object. Each list[float] is one simple polygon in the format of [x1, y1, ..., xn, yn] (n≥3). The Xs and Ys are absolute coordinates in unit of pixels.
      • If dict, it represents the per-pixel segmentation mask in COCO’s compressed RLE format. The dict should have keys “size” and “counts”. You can convert a uint8 segmentation mask of 0s and 1s into such dict by pycocotools.mask.encode(np.asarray(mask, order="F")). cfg.INPUT.MASK_FORMAT must be set to bitmask if using the default data loader with such format.
    • keypoints (list[float]): in the format of [x1, y1, v1,…, xn, yn, vn]. v[i] means the visibility of this keypoint. n must be equal to the number of keypoint categories. The Xs and Ys are absolute real-value coordinates in range [0, W or H].

      (Note that the keypoint coordinates in COCO format are integers in range [0, W-1 or H-1], which is different from our standard format. Detectron2 adds 0.5 to COCO keypoint coordinates to convert them from discrete pixel indices to floating point coordinates.)

    • iscrowd: 0 (default) or 1. Whether this instance is labeled as COCO’s “crowd region”. Don’t include this field if you don’t know what it means.
      If annotations is an empty list, it means the image is labeled to have no objects. Such images will by default be removed from training, but can be included using DATALOADER.FILTER_EMPTY_ANNOTATIONS.

  • sem_seg_file_name (str): The full path to the semantic segmentation ground truth file. It should be a grayscale image whose pixel values are integer labels.
  • pan_seg_file_name (str): The full path to panoptic segmentation ground truth file. It should be an RGB image whose pixel values are integer ids encoded using the panopticapi.utils.id2rgb function. The ids are defined by segments_info. If an id does not appear in segments_info, the pixel is considered unlabeled and is usually ignored in training & evaluation.
  • segments_info (list[dict]): defines the meaning of each id in panoptic segmentation ground truth. Each dict has the following keys:
    • id (int): integer that appears in the ground truth image.
    • category_id (int): an integer in the range [0, num_categories-1] representing the category label.
    • iscrowd: 0 (default) or 1. Whether this instance is labeled as COCO’s “crowd region”.

Custom Dataset Dicts for New Tasks

在你的数据集函数返回的 list[dict] 中,字典也可以有任意的自定义数据。这对于一个需要标准数据集字典没有涵盖的额外信息的新任务来说将是非常有用的。在这种情况下,你需要确保下游的代码能够正确地处理你的数据。通常这需要为数据转换器编写一个新的映射器(见使用自定义数据转换器)。

在设计自定义格式时,要注意所有的dict都存储在内存中(有时是序列化的,而且有多个副本)。为了节省内存,每个dict都要包含关于每个样本的少量但足够的信息,比如文件名和注释。加载完整的样本通常发生在数据加载器中。

对于整个数据集共享的属性,使用Metadata(见下文)。为了避免额外的内存,不要在每个样本内保存此类信息。

“Metadata” for Datasets

每个数据集都与一些元数据相关联,可以通过MetadataCatalog.get(dataset_name).some_metadata访问。元数据是一个键值映射,包含整个数据集共享的信息,通常用于解释数据集中的内容,例如,类的名称、类的颜色、文件的根等。这些信息对于增强、评估、可视化、记录等都很有用。元数据的结构取决于相应的下游代码需要什么。

Register a COCO Format Dataset

如果你的实例级(检测、分割、关键点)数据集已经是COCO格式的json文件,那么该数据集及其相关的元数据可以很容易地被注册:

from detectron2.data.datasets import register_coco_instances
register_coco_instances("my_dataset", {
    
    }, "json_annotation.json", "path/to/image/dir")

如果你的数据集是COCO格式,但需要进一步处理,或者有额外的自定义的每个实例注释,load_coco_json函数可能是有用的。

Update the Config for New Datasets

一旦你注册了数据集,你就可以在cfg.DATASETS.{TRAIN,TEST}中使用数据集的名称(例如,上面例子中的 “my_dataset”)。为了在新的数据集上进行训练或评估,你可能还需要改变其他配置:

  • MODEL.ROI_HEADS.NUM_CLASSESMODEL.RETINANET.NUM_CLASSES 分别是R-CNN和RetinaNet模型的类别数量。
  • MODEL.ROI_KEYPOINT_HEAD.NUM_KEYPOINTS 设置关键点R-CNN的关键点数量。 你还需要用 TEST.KEYPOINT_OKS_SIGMAS 设置 Keypoint OKS 以便评估。
  • MODEL.SEM_SEG_HEAD.NUM_CLASSES 设置FPN & Panoptic FPN 的 stuff classes 的数量。
  • TEST.DETECTIONS_PER_IMAGE 控制要检测的物体的最大数量。如果测试图像可能包含>100个物体,则将其设置为一个较大的数字。
  • If you’re training Fast R-CNN (with precomputed proposals), DATASETS.PROPOSAL_FILES_{TRAIN,TEST} need to match the datasets. The format of proposal files are documented here.

New models (e.g. TensorMask, PointRend) often have similar configs of their own that need to be changed as well.


Dataloader(数据集加载)

Dataloader是为模型提供数据的组件。数据加载器通常(但不一定)从数据集中获取原始信息,并将其处理成模型所需的格式。

How the Existing Dataloader Works

Detectron2包含一个内置的数据加载流程。最好了解它的工作原理,以备你需要编写一个自定义的数据加载流程。

detectron2提供了两个函数build_detection_ {train,test} _ loader,它们从给定的配置创建默认数据加载程序。这是build_detection _ {train,test} _ loader的工作:

  1. 它以注册数据集的名称(例如“coco_2017_train”)的名称加载了一个以轻量级格式代表数据集成项的list[dict]。这些数据集的items尚未准备好由模型使用(例如,图像未加载到内存中,尚未进行随机增强等)。有关数据集格式和数据集注册的详细信息可以在数据集中找到。
  2. 此列表中的每个字典都由一个函数(“mapper”)映射:
    • 用户可以通过在build_detection_{train,test}_loader中指定 "mapper "参数来定制这个映射函数。默认的映射器是DatasetMapper
    • 映射器的输出格式可以是任意的,只要它能被这个数据加载器的消费者(通常是模型)接受。默认映射器的输出,在批处理后,遵循 Use Models 中记录的默认模型输入格式。
    • 映射器的作用是将数据集项目的轻量级表示转换为模型可以消费的格式(包括,例如,读取图像、执行随机数据增强和转换为Torch Tensors)。如果你想对数据进行自定义转换,你往往需要一个自定义映射器。
  3. 映射器的输出是分批进行的(简单来说就是进入一个列表)。
  4. 这个分批的数据是数据加载器的输出。通常情况下,它也是model.forward()的输入。

Write a Custom Dataloader

build_detection_{train,test}_loader(mapper=)使用不同的 "映射器 "对大多数自定义数据加载的使用情况都有效。例如,如果你想把所有图片的大小调整为训练时的固定大小,可以使用:

import detectron2.data.transforms as T
from detectron2.data import DatasetMapper   # the default mapper
dataloader = build_detection_train_loader(cfg,
   mapper=DatasetMapper(cfg, is_train=True, augmentations=[
      T.Resize((800, 800))
   ]))
# use this dataloader instead of the default

如果默认的DatasetMapper的参数不能提供你所需要的,你可以写一个自定义的映射函数来代替它,例如:

from detectron2.data import detection_utils as utils
 # Show how to implement a minimal mapper, similar to the default DatasetMapper
def mapper(dataset_dict):
    dataset_dict = copy.deepcopy(dataset_dict)  # it will be modified by code below
    # can use other ways to read image
    image = utils.read_image(dataset_dict["file_name"], format="BGR")
    # See "Data Augmentation" tutorial for details usage
    auginput = T.AugInput(image)
    transform = T.Resize((800, 800))(auginput)
    image = torch.from_numpy(auginput.image.transpose(2, 0, 1))
    annos = [
        utils.transform_instance_annotations(annotation, [transform], image.shape[1:])
        for annotation in dataset_dict.pop("annotations")
    ]
    return {
    
    
       # create the format that the model expects
       "image": image,
       "instances": utils.annotations_to_instances(annos, image.shape[1:])
    }
dataloader = build_detection_train_loader(cfg, mapper=mapper)
’‘’
  这段代码实现了一个 minimal mapper,用于将数据集里的图像和其对应的注释转换成模型需要的格式。
  具体来说,它通过 deep copy 的方式复制了数据集的字典,读取了图像文件并进行了 resize 处理,
  然后使用 transform_instance_annotations 函数将注释进行了转换,并最终返回 image 和 instances 两个字段的字典。
  这个 minimal mapper 可以用在 Detectron2 的训练数据加载器 build_detection_train_loader 函数中,
  同时可以传入其他参数来进行更复杂的数据增强等操作。
  自定义操作可以参考上述代码来实现。
‘’‘


如果你不仅想改变映射器(例如,为了实现不同的采样或批处理逻辑),build_detection_train_loader将无法工作,你将需要编写一个不同的数据加载器。数据加载器只是一个Python迭代器,产生模型所接受的格式。你可以使用任何你喜欢的工具来实现它。

不管实现什么,建议查看detectron2.data的API文档,了解这些函数的API。

Use a Custom Dataloader

如果你使用DefaultTrainer,你可以覆盖它的build_{train,test}_loader方法来使用你自己的dataloader。请看deeplab数据加载器的例子。

如果你写自己的训练循环,你可以很容易地插入你的数据加载器。


Data Augmentation(数据增强)

数据增强是训练的一个重要部分。Detectron2的数据增强系统旨在解决以下目标:

  1. 允许将多种数据类型放在一起增强(例如,图像与它们的边界框和遮罩放在一起)
  2. 允许应用静态声明的增强序列
  3. 允许添加自定义的新数据类型来增强(旋转的边界框,视频剪辑等)
  4. 处理和操作由增强物应用的操作

前两个功能涵盖了大多数常见的使用情况,并且在其他库中也可以使用,如 albumentations。支持其他功能会给detectron2的增强API增加一些开销,我们将在本教程中解释。

本教程主要介绍在编写新的数据加载器时如何使用增强器,以及如何编写新的增强器。如果你使用detectron2中的默认数据加载器,它已经支持接受用户提供的自定义增强器列表,这在Dataloader教程中有所说明。

Basic Usage

特征(1)和(2)的基本用法如下:

from detectron2.data import transforms as T
# Define a sequence of augmentations:
augs = T.AugmentationList([
    T.RandomBrightness(0.9, 1.1),
    T.RandomFlip(prob=0.5),
    T.RandomCrop("absolute", (640, 640))
])  # type: T.Augmentation

# Define the augmentation input ("image" required, others optional):
input = T.AugInput(image, boxes=boxes, sem_seg=sem_seg)
# Apply the augmentation:
transform = augs(input)  # type: T.Transform
image_transformed = input.image  # new image
sem_seg_transformed = input.sem_seg  # new semantic segmentation

# For any extra data that needs to be augmented together, use transform, e.g.:
image2_transformed = transform.apply_image(image2)
polygons_transformed = transform.apply_polygons(polygons)
```
  这段代码对图像数据进行了增强处理。
  首先定义了一系列的数据增强操作,包括随机亮度调整、随机翻转和随机裁剪。
  然后通过定义输入数据,包括图像、物体边界框和语义分割标签(可选),
  并应用之前定义的增强操作,将输入数据进行增强。
  最后,对于需要一起进行增强的其他数据(例如另一个图像或多边形数据),
  使用transform进行增强。最终输出增强后的图像和语义分割标签。
```

这里涉及三个基本概念。它们是:

  • T.Augmentation定义了修改输入的 “策略”。
    • 它的__call__(AugInput) -> Transform方法对输入进行就地增强,并返回被应用的操作。
  • T.Transform 实现了转换数据的实际操作
    • 它有诸如apply_image、apply_coords等方法来定义如何转换每种数据类型。
  • T.AugInput存储T.Augmentation所需的输入,以及它们应该如何被转换。这个概念在一些高级用法中是需要的。直接使用这个类应该足以满足所有常见的使用情况,因为不在T.AugInput中的额外数据可以使用返回的变换进行增强,如上面的例子所示。

Write New Augmentations

大多数二维增强只需要知道输入图像的情况。这样的增强可以像这样轻松实现:

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)

除了图像之外,只要是函数签名的一部分,就可以使用给定的AugInput的任何属性,例如:

class MyCustomCrop(T.Augmentation):
    def get_transform(self, image, sem_seg):
        # decide where to crop using both image and sem_seg
        return T.CropTransform(...)

augs = MyCustomCrop()
assert hasattr(input, "image") and hasattr(input, "sem_seg")
transform = augs(input)

也可以通过子类化T.Transform添加新的变换操作。

Use Models(使用模型)

Build Models from Yacs Config

模型(和它们的子模型)可以由build_model、build_bone、build_roi_heads等函数从yacs配置对象中建立:

from detectron2.modeling import build_model
model = build_model(cfg)  # returns a torch.nn.Module

build_model只建立模型结构,并将随机参数填入其中。关于如何将现有的checkpoint 加载到模型中,以及如何使用model对象,请见下文。

Load/Save a Checkpoint

from detectron2.checkpoint import DetectionCheckpointer
DetectionCheckpointer(model).load(file_path_or_url)  # load a file, usually from cfg.MODEL.WEIGHTS

checkpointer = DetectionCheckpointer(model, save_dir="output")
checkpointer.save("model_999")  # save to output/model_999.pth

Detectron2的检查指针可以识别pytorch的.pth格式的模型,以及我们model zoo中的.pkl文件。有关其使用的更多细节,请参见API文档。

对于.pth文件,可以使用torch.{load,save},对于.pkl文件,可以使用pickle.{dump,load}任意操作模型文件。

Use a Model

一个模型可以通过output = model(inputs)调用,其中inputs是一个list[dict]。每个dict对应一个图像,所需的键取决于模型的类型,以及模型是处于训练还是评估模式。例如,为了进行推理,所有现有的模型都需要 "image "键,以及可选的 "height "和 “width”。现有模型的输入和输出的详细格式解释如下。

训练: 当处于训练模式时,所有模型都需要在一个EventStorage下使用。训练的统计数据将被放入存储中:

from detectron2.utils.events import EventStorage
with EventStorage() as storage:
  losses = model(inputs)

推理: 如果你只想使用现有的模型做简单的推理,DefaultPredictor是一个围绕模型的包装,提供这样的基本功能。它包括默认行为,包括模型加载、预处理,以及对单个图像而非批次进行操作。使用方法见其文档。

你也可以像这样直接运行推理:

model.eval()
with torch.no_grad():
  outputs = model(inputs)

Model Input Format

用户可以实现支持任何任意输入格式的自定义模型。这里我们描述了 detectron2 中所有内置模型支持的标准输入格式。它们都接受一个list[dict]作为输入。每个dict对应于一个图像的信息。

该字典可能包含以下键:

  • “image”: (C, H, W)格式的tensor。通道的含义由cfg.INPUT.FORMAT定义。如果有的话,图像标准化将在模型内部使用cfg.MODEL.PIXEL_{MEAN,STD}进行。
  • “height”、“width”:推理中希望输出的高度和宽度,不一定与image字段的高度或宽度相同。例如,如果调整大小被用作预处理步骤,image字段包含调整后的图像。但你可能希望输出是原始分辨率的。如果提供,模型将以这个分辨率产生输出,而不是以输入到模型的图像的分辨率。这是更有效和更准确的。
  • “instances”:一个用于训练的Instances对象,有以下字段:
    • “gt_boxes”:一个Boxes对象,存储N个盒子,每个实例一个。
    • “gt_classes”: 长类型的张量,N个标签的向量,范围为[0, num_categories]。
    • “gt_masks”:一个PolygonMasks或BitMasks对象,存储了N个掩码,每个实例一个。
    • “gt_keypoints”:一个存储N个关键点集的Keypoints对象,每个实例一个。
  • “sem_seg”: Tensor[int],(H,W)格式。用于训练的语义分割基础真理。值代表从0开始的类别标签。
  • “proposs”:仅在快速R-CNN风格的模型中使用的Instances对象,有以下字段:
    • “proposal_boxes”:一个存储P个proposal box的Boxes对象。
    • “objectness_logits”:Tensor,一个P分数的向量,每个提议都有一个。

对于内置模型的推理,只需要“image”键,“width/height”是可选的。

我们目前没有为全景分割训练定义标准的输入格式,因为现在的模型使用由自定义数据加载器产生的自定义格式。

How it connects to data loader

默认DatasetMapper的输出是一个遵循上述格式的dict。在数据加载器执行批处理后,它变成了内置模型支持的list[dict]。

Model Output Format

当处于训练模式时,内置模型会输出一个带有所有损失的dict[str->ScalarTensor]

当处于推理模式时,内置模型会输出一个list[dict],每张图片有一个dict。基于模型正在做的任务,每个dict可能包含以下字段:

  • “instances”: Instances object with the following fields:
    • “pred_boxes”: Boxes object storing N boxes, one for each detected instance.
    • “scores”: Tensor, a vector of N confidence scores.
    • “pred_classes”: Tensor, a vector of N labels in range [0, num_categories).
    • “pred_masks”: a Tensor of shape (N, H, W), masks for each detected instance.
    • “pred_keypoints”: a Tensor of shape (N, num_keypoint, 3). Each row in the last dimension is (x, y, score). Confidence scores are larger than 0.
  • “sem_seg”: Tensor of (num_categories, H, W), the semantic segmentation prediction.
  • “proposals”: Instances object with the following fields:
    • “proposal_boxes”: Boxes object storing N boxes.
    • “objectness_logits”: a torch vector of N confidence scores.
  • “panoptic_seg”: A tuple of (pred: Tensor, segments_info: Optional[list[dict]]). The pred tensor has shape (H, W), containing the segment id of each pixel.
    • If segments_info exists, each dict describes one segment id in pred and has the following fields:
      • “id”: the segment id
      • “isthing”: whether the segment is a thing or stuff
      • “category_id”: the category id of this segment.
        If a pixel’s id does not exist in segments_info, it is considered to be void label defined in Panoptic Segmentation.
    • If segments_info is None, all pixel values in pred must be ≥ -1. Pixels with value -1 are assigned void labels. Otherwise, the category id of each pixel is obtained by category_id = pixel // metadata.label_divisor.

Partially execute a model

有时你可能想获得一个模型内部的中间张量,比如某个层的输入,后处理前的输出。由于通常有数百个中间张量,所以没有一个API可以提供你所需要的中间结果。你有以下选择:

  1. 写一个(子)模型。按照教程,你可以重写一个模型组件(例如一个模型的头部),使其与现有组件做同样的事情,但返回你需要的输出。
  2. 部分地执行一个模型。你可以像往常一样创建模型,但使用自定义代码来执行它,而不是其forward()。例如,下面的代码在mask head之前获得mask features。
images = ImageList.from_tensors(...)  # preprocessed input tensor
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])
  1. 使用forward hooks。forward hooks可以帮助你获得某个模块的输入或输出。如果它们不是你想要的,至少可以和部分执行一起使用,以获得其他张量。

所有的选项都需要你阅读文档,有时还需要阅读现有模型的代码,以了解内部逻辑,从而编写代码来获得内部张量。


Write Models(实现模型)

如果你想做一些全新的事情,你可能希望完全从头开始实现一个模型。然而,在许多情况下,你可能对修改或扩展一个现有模型的某些组件感兴趣。因此,我们也提供了一些机制,让用户覆盖标准模型的某些内部组件的行为。

Register New Components

对于用户经常想要自定义的常见概念,例如“backbone feature extractor”、“box head”,我们提供了一种注册机制,供用户注入自定义实现,这些实现将立即在配置文件中使用。

例如,要添加一个新的骨干,在你的代码中导入这段代码:

from detectron2.modeling import BACKBONE_REGISTRY, Backbone, ShapeSpec

@BACKBONE_REGISTRY.register()
class ToyBackbone(Backbone):
  def __init__(self, cfg, input_shape):
    super().__init__()
    # create your own backbone
    self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=16, padding=3)

  def forward(self, image):
    return {
    
    "conv1": self.conv1(image)}

  def output_shape(self):
    return {
    
    "conv1": ShapeSpec(channels=64, stride=16)}

在这段代码中,我们按照 Backbone 类的接口实现了一个新的 backbone,并将其注册到需要 Backbone子类的 BACKBONE_REGISTRY 中。导入此代码后,detectron2 可以将类的名称链接到它的实现。因此可以编写如下代码:

cfg = ...   # read a config
cfg.MODEL.BACKBONE.NAME = 'ToyBackbone'   # or set it in the config file
model = build_model(cfg)  # it will find `ToyBackbone` defined above

再举一个例子,要在通用 R-CNN 元架构中为 ROI 头添加新功能,您可以实现一个新的 ROIHeads 子类并将其放在 ROI_HEADS_REGISTRY 中。 DensePoseMeshRCNN 是实现新 ROIHeads 以执行新任务的两个示例。并且 projects/ 包含更多实现不同架构的示例。

可以在 API 文档中找到完整的注册表列表。您可以在这些注册表中注册组件以自定义模型的不同部分或整个模型。

Construct Models with Explicit Arguments

注册表是连接配置文件中的名称和实际代码的桥梁。它们是为了涵盖用户经常需要更换的几个主要组件。然而,基于文本的配置文件的能力有时是有限的,一些更深入的定制可能只能通过编写代码来实现。

detectron2中的大多数模型组件都有一个清晰的__init__接口,记录了它需要哪些输入参数。用自定义参数调用它们将给你一个自定义的模型变体。

例如,要在 Faster R-CNN 的 box head 中使用自定义损失函数,我们可以执行以下操作:

  1. 目前损失是在FastRCNNOutLayers中计算的。我们需要实现它的一个变体或子类,带有自定义的损失函数,名为MyRCNNOutput
  2. box_predictor=MyRCNNOut()参数调用StandardROIHeads,而不是内置的FastRCNNOutputLayers。如果所有其他参数应该保持不变,这可以通过使用可配置的__init__机制轻松实现:
    roi_heads = StandardROIHeads(
      cfg, backbone.output_shape(),
      box_predictor=MyRCNNOutput(...)
    )
    
  3. (可选)如果我们想从配置文件中启用这个新模型,需要注册:
@ROI_HEADS_REGISTRY.register()
class MyStandardROIHeads(StandardROIHeads):
  def __init__(self, cfg, input_shape):
    super().__init__(cfg, input_shape,
                     box_predictor=MyRCNNOutput(...))

Training(训练)

从前面的教程来看,你现在可能有一个自定义模型和一个数据加载器。为了运行训练,用户通常有以下两种风格之一的偏好:

Custom Training Loop

准备好模型和数据加载器后,编写训练循环所需的一切都可以在 PyTorch 中找到,您可以自由地自己编写训练循环。这种风格让研究人员可以更清晰地管理整个训练逻辑并拥有完全的控制权。 tools/plain_train_net.py 中提供了一个这样的例子。

#!/usr/bin/env python
# Copyright (c) Facebook, Inc. and its affiliates.
"""
Detectron2 training script with a plain training loop.

This script reads a given config file and runs the training or evaluation.
It is an entry point that is able to train standard models in detectron2.

In order to let one script support training of many models,
this script contains logic that are specific to these built-in models and therefore
may not be suitable for your own project.
For example, your research project perhaps only needs a single "evaluator".

Therefore, we recommend you to use detectron2 as a library and take
this file as an example of how to use the library.
You may want to write your own script with your datasets and other customizations.

Compared to "train_net.py", this script supports fewer default features.
It also includes fewer abstraction, therefore is easier to add custom logic.
"""

import logging
import os
from collections import OrderedDict
import torch
from torch.nn.parallel import DistributedDataParallel

import detectron2.utils.comm as comm
from detectron2.checkpoint import DetectionCheckpointer, PeriodicCheckpointer
from detectron2.config import get_cfg
from detectron2.data import (
    MetadataCatalog,
    build_detection_test_loader,
    build_detection_train_loader,
)
from detectron2.engine import default_argument_parser, default_setup, default_writers, launch
from detectron2.evaluation import (
    CityscapesInstanceEvaluator,
    CityscapesSemSegEvaluator,
    COCOEvaluator,
    COCOPanopticEvaluator,
    DatasetEvaluators,
    LVISEvaluator,
    PascalVOCDetectionEvaluator,
    SemSegEvaluator,
    inference_on_dataset,
    print_csv_format,
)
from detectron2.modeling import build_model
from detectron2.solver import build_lr_scheduler, build_optimizer
from detectron2.utils.events import EventStorage

logger = logging.getLogger("detectron2")


def get_evaluator(cfg, dataset_name, output_folder=None):
    """
    Create evaluator(s) for a given dataset.
    This uses the special metadata "evaluator_type" associated with each builtin dataset.
    For your own dataset, you can simply create an evaluator manually in your
    script and do not have to worry about the hacky if-else logic here.
    """
    if output_folder is None:
        output_folder = os.path.join(cfg.OUTPUT_DIR, "inference")
    evaluator_list = []
    evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type
    if evaluator_type in ["sem_seg", "coco_panoptic_seg"]:
        evaluator_list.append(
            SemSegEvaluator(
                dataset_name,
                distributed=True,
                output_dir=output_folder,
            )
        )
    if evaluator_type in ["coco", "coco_panoptic_seg"]:
        evaluator_list.append(COCOEvaluator(dataset_name, output_dir=output_folder))
    if evaluator_type == "coco_panoptic_seg":
        evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder))
    if evaluator_type == "cityscapes_instance":
        return CityscapesInstanceEvaluator(dataset_name)
    if evaluator_type == "cityscapes_sem_seg":
        return CityscapesSemSegEvaluator(dataset_name)
    if evaluator_type == "pascal_voc":
        return PascalVOCDetectionEvaluator(dataset_name)
    if evaluator_type == "lvis":
        return LVISEvaluator(dataset_name, cfg, True, output_folder)
    if len(evaluator_list) == 0:
        raise NotImplementedError(
            "no Evaluator for the dataset {} with the type {}".format(dataset_name, evaluator_type)
        )
    if len(evaluator_list) == 1:
        return evaluator_list[0]
    return DatasetEvaluators(evaluator_list)


def do_test(cfg, model):
    results = OrderedDict()
    for dataset_name in cfg.DATASETS.TEST:
        data_loader = build_detection_test_loader(cfg, dataset_name)
        evaluator = get_evaluator(
            cfg, dataset_name, os.path.join(cfg.OUTPUT_DIR, "inference", dataset_name)
        )
        results_i = inference_on_dataset(model, data_loader, evaluator)
        results[dataset_name] = results_i
        if comm.is_main_process():
            logger.info("Evaluation results for {} in csv format:".format(dataset_name))
            print_csv_format(results_i)
    if len(results) == 1:
        results = list(results.values())[0]
    return results

# 定义训练函数do_train,参数包括配置 cfg、模型 model,以及布尔值 resume 表示是否从已保存的检查点中重新开始训练。
def do_train(cfg, model, resume=False):
    model.train()
    optimizer = build_optimizer(cfg, model) # 构建优化器 optimizer,并构建学习率调度器 scheduler。
    scheduler = build_lr_scheduler(cfg, optimizer)
    
    # 定义 DetectionCheckpointer,用于保存和恢复检查点。
    checkpointer = DetectionCheckpointer(
        model, cfg.OUTPUT_DIR, optimizer=optimizer, scheduler=scheduler
    )
    # 获取训练迭代次数的起点 start_iter,如果 resume=True,则从已保存的检查点中恢复,否则为 0。
    start_iter = (
        checkpointer.resume_or_load(cfg.MODEL.WEIGHTS, resume=resume).get("iteration", -1) + 1
    )
    # 定义训练总迭代次数 max_iter。
    max_iter = cfg.SOLVER.MAX_ITER
    # 构建 PeriodicCheckpointer,定期调用 DetectionCheckpointer,保存检查点。
    periodic_checkpointer = PeriodicCheckpointer(
        checkpointer, cfg.SOLVER.CHECKPOINT_PERIOD, max_iter=max_iter
    )
    # 定义日志输出方式 writers,主要包括使用 Tensorboard 进行可视化输出。
    writers = default_writers(cfg.OUTPUT_DIR, max_iter) if comm.is_main_process() else []

    # compared to "train_net.py", we do not support accurate timing and
    # precise BN here, because they are not trivial to implement in a small training loop
    data_loader = build_detection_train_loader(cfg) # 构建数据加载器 data_loader,用于从数据集中加载训练数据。
    logger.info("Starting training from iteration {}".format(start_iter))
    with EventStorage(start_iter) as storage: # 开始训练循环,并使用 EventStorage 记录训练过程中的数据和日志。
        for data, iteration in zip(data_loader, range(start_iter, max_iter)):
            storage.iter = iteration

            loss_dict = model(data)
            losses = sum(loss_dict.values())
            assert torch.isfinite(losses).all(), loss_dict

            loss_dict_reduced = {
    
    k: v.item() for k, v in comm.reduce_dict(loss_dict).items()}
            losses_reduced = sum(loss for loss in loss_dict_reduced.values())
            if comm.is_main_process():
                storage.put_scalars(total_loss=losses_reduced, **loss_dict_reduced)

            optimizer.zero_grad()
            losses.backward()
            optimizer.step()
            storage.put_scalar("lr", optimizer.param_groups[0]["lr"], smoothing_hint=False)
            scheduler.step()
            # 在每个迭代中,使用模型进行前向传播,得到损失函数 loss_dict,并计算总损失 losses。
            # 对损失进行反向传播,并使用优化器进行模型参数更新。
            # 定期进行模型测试,并在测试结果上调用 comm.synchronize() 同步不同进程中的测试结果。
            # 定期输出训练日志,包括损失值和学习率等。
            # 定期保存训练检点。
            if (
                cfg.TEST.EVAL_PERIOD > 0
                and (iteration + 1) % cfg.TEST.EVAL_PERIOD == 0
                and iteration != max_iter - 1
            ):
                do_test(cfg, model)
                # Compared to "train_net.py", the test results are not dumped to EventStorage
                comm.synchronize()

            if iteration - start_iter > 5 and (
                (iteration + 1) % 20 == 0 or iteration == max_iter - 1
            ):
                for writer in writers:
                    writer.write()
            periodic_checkpointer.step(iteration)

# 定义一个函数来进行基本设置
def setup(args):
    """
    Create configs and perform basic setups.
    创建配置并进行基本设置。
    """
    # 获取设定
    cfg = get_cfg()
    # 从配置文件中合并设置
    cfg.merge_from_file(args.config_file)
    # 从命令行参数中合并设置
    cfg.merge_from_list(args.opts)
    # 冻结配置,防止在以后的代码中更改
    cfg.freeze()
    # 使用 default_setup 函数进行默认设置
    default_setup(
        cfg, args
    )  # 如果您不喜欢任何默认设置,请编写您自己的设置代码
    # 返回配置
    return cfg


def main(args):
    cfg = setup(args)

    model = build_model(cfg)
    logger.info("Model:\n{}".format(model))
    if args.eval_only:
        DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load(
            cfg.MODEL.WEIGHTS, resume=args.resume
        )
        return do_test(cfg, model)

    distributed = comm.get_world_size() > 1
    if distributed:
        model = DistributedDataParallel(
            model, device_ids=[comm.get_local_rank()], broadcast_buffers=False
        )

    do_train(cfg, model, resume=args.resume)
    return do_test(cfg, model)


if __name__ == "__main__":
    args = default_argument_parser().parse_args()
    print("Command Line Args:", args)
    launch(
        main,
        args.num_gpus,
        num_machines=args.num_machines,
        machine_rank=args.machine_rank,
        dist_url=args.dist_url,
        args=(args,),
    )

然后,用户可以很容易地控制对训练逻辑的任何定制。

Trainer Abstraction

我们还提供了一个标准化的 "训练者 "抽象,其挂钩系统有助于简化标准训练行为。它包括以下两个实例:

  • SimpleTrainer为单成本单优化器单数据源的训练提供了一个最小的训练循环,其他什么都没有。其他任务(检查点、记录等)可以使用hook system来实现。
  • DefaultTrainer是一个由yacs config初始化的SimpleTrainer,被tools/train_net.py和许多脚本使用。它包括更多标准的默认行为,人们可能想要选择,包括优化器的默认配置、学习率计划、日志、评估、检查点等。
    #!/usr/bin/env python
    # Copyright (c) Facebook, Inc. and its affiliates.
    """
    A main training script.
    
    This scripts reads a given config file and runs the training or evaluation.
    It is an entry point that is made to train standard models in detectron2.
    
    In order to let one script support training of many models,
    this script contains logic that are specific to these built-in models and therefore
    may not be suitable for your own project.
    For example, your research project perhaps only needs a single "evaluator".
    
    Therefore, we recommend you to use detectron2 as an library and take
    this file as an example of how to use the library.
    You may want to write your own script with your datasets and other customizations.
    """
    
    import logging
    import os
    from collections import OrderedDict
    
    import detectron2.utils.comm as comm
    from detectron2.checkpoint import DetectionCheckpointer
    from detectron2.config import get_cfg
    from detectron2.data import MetadataCatalog
    from detectron2.engine import DefaultTrainer, default_argument_parser, default_setup, hooks, launch
    from detectron2.evaluation import (
        CityscapesInstanceEvaluator,
        CityscapesSemSegEvaluator,
        COCOEvaluator,
        COCOPanopticEvaluator,
        DatasetEvaluators,
        LVISEvaluator,
        PascalVOCDetectionEvaluator,
        SemSegEvaluator,
        verify_results,
    )
    from detectron2.modeling import GeneralizedRCNNWithTTA
    
    
    def build_evaluator(cfg, dataset_name, output_folder=None):
        """
        Create evaluator(s) for a given dataset.
        This uses the special metadata "evaluator_type" associated with each builtin dataset.
        For your own dataset, you can simply create an evaluator manually in your
        script and do not have to worry about the hacky if-else logic here.
        """
        # 如果输出文件夹output_folder未定义,则将其设置为cfg.OUTPUT_DIR下的"inference"文件夹
        if output_folder is None:
            output_folder = os.path.join(cfg.OUTPUT_DIR, "inference")
        evaluator_list = []
        evaluator_type = MetadataCatalog.get(dataset_name).evaluator_type
        if evaluator_type in ["sem_seg", "coco_panoptic_seg"]:
            evaluator_list.append(
                SemSegEvaluator(
                    dataset_name,
                    distributed=True,
                    output_dir=output_folder,
                )
            )
        if evaluator_type in ["coco", "coco_panoptic_seg"]:
            evaluator_list.append(COCOEvaluator(dataset_name, output_dir=output_folder))
        if evaluator_type == "coco_panoptic_seg":
            evaluator_list.append(COCOPanopticEvaluator(dataset_name, output_folder))
        if evaluator_type == "cityscapes_instance":
            return CityscapesInstanceEvaluator(dataset_name)
        if evaluator_type == "cityscapes_sem_seg":
            return CityscapesSemSegEvaluator(dataset_name)
        elif evaluator_type == "pascal_voc":
            return PascalVOCDetectionEvaluator(dataset_name)
        elif evaluator_type == "lvis":
            return LVISEvaluator(dataset_name, output_dir=output_folder)
        if len(evaluator_list) == 0:
            raise NotImplementedError(
                "no Evaluator for the dataset {} with the type {}".format(dataset_name, evaluator_type)
            )
        elif len(evaluator_list) == 1:
            return evaluator_list[0]
        return DatasetEvaluators(evaluator_list)
    
    
    class Trainer(DefaultTrainer):
        """
        We use the "DefaultTrainer" which contains pre-defined default logic for
        standard training workflow. They may not work for you, especially if you
        are working on a new research project. In that case you can write your
        own training loop. You can use "tools/plain_train_net.py" as an example.
        """
        # 定义一个名为build_evaluator的类方法,该方法接收三个参数,
        # 包括cfg、dataset_name和output_folder,返回一个评估器对象
        @classmethod
        def build_evaluator(cls, cfg, dataset_name, output_folder=None):
            return build_evaluator(cfg, dataset_name, output_folder)
    
        @classmethod
        def test_with_TTA(cls, cfg, model):
            logger = logging.getLogger("detectron2.trainer")
            # In the end of training, run an evaluation with TTA
            # Only support some R-CNN models.
            logger.info("Running inference with test-time augmentation ...")
            model = GeneralizedRCNNWithTTA(cfg, model)
            evaluators = [
                cls.build_evaluator(
                    cfg, name, output_folder=os.path.join(cfg.OUTPUT_DIR, "inference_TTA")
                )
                for name in cfg.DATASETS.TEST
            ]
            res = cls.test(cfg, model, evaluators)
            res = OrderedDict({
          
          k + "_TTA": v for k, v in res.items()})
            return res
    
    
    def setup(args):
        """
        Create configs and perform basic setups.
        """
        cfg = get_cfg()
        cfg.merge_from_file(args.config_file)
        cfg.merge_from_list(args.opts)
        cfg.freeze()
        default_setup(cfg, args)
        return cfg
    
    
    def main(args):
        cfg = setup(args)
    
        if args.eval_only:
            model = Trainer.build_model(cfg)
            DetectionCheckpointer(model, save_dir=cfg.OUTPUT_DIR).resume_or_load(
                cfg.MODEL.WEIGHTS, resume=args.resume
            )
            res = Trainer.test(cfg, model)
            if cfg.TEST.AUG.ENABLED:
                res.update(Trainer.test_with_TTA(cfg, model))
            if comm.is_main_process():
                verify_results(cfg, res)
            return res
    
        """
        If you'd like to do anything fancier than the standard training logic,
        consider writing your own training loop (see plain_train_net.py) or
        subclassing the trainer.
        """
        trainer = Trainer(cfg)
        trainer.resume_or_load(resume=args.resume)
        if cfg.TEST.AUG.ENABLED:
            trainer.register_hooks(
                [hooks.EvalHook(0, lambda: trainer.test_with_TTA(cfg, trainer.model))]
            )
        return trainer.train()
    
    
    if __name__ == "__main__":
        args = default_argument_parser().parse_args()
        print("Command Line Args:", args)
        launch(
            main,
            args.num_gpus,
            num_machines=args.num_machines,
            machine_rank=args.machine_rank,
            dist_url=args.dist_url,
            args=(args,),
        )
    

要自定义 DefaultTrainer:

  1. 对于简单的定制(例如改变optimizer、evaluator、LRscheduler、数据加载器等),在子类中重写其方法,就像tools/train_net.py一样。
  2. 对于训练期间的额外任务,请检查hook system,看看是否支持。

例如,在训练期间打印 hello:

class HelloHook(HookBase):
  def after_step(self):
    if self.trainer.iter % 100 == 0:
      print(f"Hello at iteration {
      
      self.trainer.iter}!")
  1. 使用 trainer+hook system意味着总会有一些不规范的行为无法得到支持,尤其是在研究中。出于这个原因,我们有意保持培训师和挂钩系统最小化,而不是强大。如果这样的系统无法实现任何目标,那么从 tools/plain_train_net.py 开始手动实现自定义训练逻辑会更容易。

Logging of Metrics

在训练期间,detectron2 模型和训练器将指标放入集中式 EventStorage。您可以使用以下代码访问它并向其记录指标:

from detectron2.utils.events import get_event_storage

# inside the model:
if self.training:
  value = # compute the value from inputs
  storage = get_event_storage()
  storage.put_scalar("some_accuracy", value)

有关详细信息,请参阅其文档。

然后使用 EventWriter 将指标写入各种目的地。 DefaultTrainer 启用一些具有默认配置的 EventWriter。有关如何自定义它们的信息,请参见上文。


Evaluation

评估是一个采用多个输入/输出对并将它们聚合的过程。您始终可以直接使用该模型,只需手动解析其输入/输出即可执行评估。或者,评估是使用 DatasetEvaluator 接口在 detectron2 中实现的。

Detectron2 包括一些 DatasetEvaluator,它们使用标准数据集特定的 API(例如,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):
    # save self.count somewhere, or print it, or return it.
    return {
    
    "count": self.count}

Use evaluators

手动使用评估者的方法进行评估:

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()

评估器也可以与 inference_on_dataset 一起使用。例如,

eval_results = inference_on_dataset(
    model,
    data_loader,
    DatasetEvaluators([COCOEvaluator(...), Counter()]))

这将对来自 data_loader 的所有输入执行model,并调用评估器来处理它们。

与使用模型手动运行评估相比,此功能的好处是可以使用 DatasetEvaluators 将评估器合并在一起,并且所有评估都可以在对数据集的一次正向传递中完成。此功能还为给定的模型和数据集提供准确的速度基准。

Evaluators for custom dataset

detectron2 中的许多评估器都是针对特定数据集制作的,以便使用每个数据集的官方 API 获得分数。除此之外,两个评估器能够评估任何遵循 detectron2 标准数据集格式的通用数据集,因此它们可用于评估自定义数据集:

  • COCOEvaluator 能够在任何自定义数据集上评估框检测、实例分割、关键点检测的 AP(平均精度)。
  • emSegEvaluator 能够评估任何自定义数据集上的语义分割指标。

总结

本文结合官方的入门教程,加上了自己的理解介绍了Detectron2框架,内容有限,如有错误的地方,请批评指正。

猜你喜欢

转载自blog.csdn.net/p3116002589/article/details/129884320
今日推荐