MMSegmentation官方使用文档整理
按照代码目录下的文件夹,MMSegmentation 代码库主要可以包含四个部分:
(1)./tools
包括了调用 MMSegmentation 作为训练和测试入口的 ./tools/train.py
和 ./tools/test.py
,预训练模型和数据集准备的转换脚本,以及部署和可视化相关的脚本。
(2)./configs
包括了各个算法的配置文件、存放常用的数据集配置、基础模型以及训练策略的基配置文件 ./configs/_base_
。
(3)./mmseg
里面是 MMSegmentation 的算法库,包括核心组件、数据集处理、分割模型代码和面向用户的 API 接口
(4)./data
指的是存放数据集的路径,在原本的代码库中没有这个文件夹。用户只需指定正确的文件夹路径即可使用数据
下面是详细的 MMSegmentation 的算法库目录结构:
# MMSegmentation 算法库目录结构的主要部分
mmsegmentation
|
|- configs # 配置文件
| |- _base_ ## 基配置文件
| | |- datasets ### 数据集相关配置文件
| | |- models ### 模型相关配置文件
| | |- schedules ### 训练日程如优化器,学习率等相关配置文件
| | |- default_runtime.py ### 运行相关的默认的设置
| |- swin ## 各个分割模型的配置文件,会引用 _base_ 的配置并做修改
| |- ...
|- data # 原始及转换后的数据集文件
|- mmseg
| |- core ## 核心组件
| | |- evaluation ### 评估模型性能代码
| |- datasets ## 数据集相关代码
| | |- pipelines ### 数据预处理
| | |- samplers ### 数据集采样代码
| | |- ade.py ### 各个数据集准备需要的代码
| | |- ...
| |- models ## 分割模型具体实现代码
| | |- backbones ### 主干网络
| | |- decode_heads ### 解码头
| | |- losses ### 损失函数
| | |- necks ### 颈
| | |- segmentors ### 构建完整分割网络的代码
| | |- utils ### 构建模型时的辅助工具
| |- apis ## high level 用户接口,在这里调用 ./mmseg/ 内各个组件
| | |- train.py ### 训练接口
| | |- test.py ### 测试接口
| | |- ...
| |- ops ## cuda 算子(即将迁移到 mmcv 中)
| |- utils ## 辅助工具
|- tools
| |- model_converters ## 各个主干网络预训练模型转 key 脚本
| |- convert_datasets ## 各个数据集准备转换脚本
| |- train.py ## 训练脚本
| |- test.py ## 测试脚本
| |- ...
|- ...
MMSegmentation 的算法库有 3 个关键组件:
(1)./mmseg/apis/
,用于训练和测试的接口
(2)./mmseg/models/
,用于分割网络模型的具体实现
(3)./mmseg/datasets/
,用于数据集处理
本文我们主要介绍算法模型相关的代码,因此涉及内容主要在 ./mmseg/models
里面。
推荐用软链接,将数据集根目录链接到 $MMSEGMENTATION/data
里。如果您的文件夹结构是不同的,您也许可以试着修改配置文件里对应的路径。
├── data
│ ├── cityscapes
│ │ ├── leftImg8bit
│ │ │ ├── train
│ │ │ ├── val
│ │ ├── gtFine
│ │ │ ├── train
│ │ │ ├── val
│ ├── VOCdevkit
│ │ ├── VOC2012
│ │ │ ├── JPEGImages
│ │ │ ├── SegmentationClass
│ │ │ ├── ImageSets
│ │ │ │ ├── Segmentation
│ │ ├── VOC2010
│ │ │ ├── JPEGImages
│ │ │ ├── SegmentationClassContext
│ │ │ ├── ImageSets
│ │ │ │ ├── SegmentationContext
│ │ │ │ │ ├── train.txt
│ │ │ │ │ ├── val.txt
│ │ │ ├── trainval_merged.json
│ │ ├── VOCaug
│ │ │ ├── dataset
│ │ │ │ ├── cls
│ ├── ade
│ │ ├── ADEChallengeData2016
│ │ │ ├── annotations
│ │ │ │ ├── training
│ │ │ │ ├── validation
│ │ │ ├── images
│ │ │ │ ├── training
│ │ │ │ ├── validation
│ ├── CHASE_DB1
│ │ ├── images
│ │ │ ├── training
│ │ │ ├── validation
│ │ ├── annotations
│ │ │ ├── training
│ │ │ ├── validation
│ ├── DRIVE
│ │ ├── images
│ │ │ ├── training
│ │ │ ├── validation
│ │ ├── annotations
│ │ │ ├── training
│ │ │ ├── validation
│ ├── HRF
│ │ ├── images
│ │ │ ├── training
│ │ │ ├── validation
│ │ ├── annotations
│ │ │ ├── training
│ │ │ ├── validation
│ ├── STARE
│ │ ├── images
│ │ │ ├── training
│ │ │ ├── validation
│ │ ├── annotations
│ │ │ ├── training
│ │ │ ├── validation
| ├── dark_zurich
| │ ├── gps
| │ │ ├── val
| │ │ └── val_ref
| │ ├── gt
| │ │ └── val
| │ ├── LICENSE.txt
| │ ├── lists_file_names
| │ │ ├── val_filenames.txt
| │ │ └── val_ref_filenames.txt
| │ ├── README.md
| │ └── rgb_anon
| │ | ├── val
| │ | └── val_ref
| ├── NighttimeDrivingTest
| | ├── gtCoarse_daytime_trainvaltest
| | │ └── test
| | │ └── night
| | └── leftImg8bit
| | | └── test
| | | └── night
│ ├── loveDA
│ │ ├── img_dir
│ │ │ ├── train
│ │ │ ├── val
│ │ │ ├── test
│ │ ├── ann_dir
│ │ │ ├── train
│ │ │ ├── val
│ ├── potsdam
│ │ ├── img_dir
│ │ │ ├── train
│ │ │ ├── val
│ │ ├── ann_dir
│ │ │ ├── train
│ │ │ ├── val
│ ├── vaihingen
│ │ ├── img_dir
│ │ │ ├── train
│ │ │ ├── val
│ │ ├── ann_dir
│ │ │ ├── train
│ │ │ ├── val
│ ├── iSAID
│ │ ├── img_dir
│ │ │ ├── train
│ │ │ ├── val
│ │ │ ├── test
│ │ ├── ann_dir
│ │ │ ├── train
│ │ │ ├── val
在tools/convert_datasets有对应的数据转化脚本
使用单卡GPU训练
python tools/train.py ${CONFIG_FILE} [可选参数]
使用多卡GPU训练
sh tools/dist_train.sh ${CONFIG_FILE} ${GPUS} [可选参数]
可选参数:
--no-validate (不推荐): 训练时代码库默认会在每 k 轮迭代后在验证集上进行评估,如果不需评估使用命令 --no-validate
--work-dir ${工作路径}: 在配置文件里重写工作路径文件夹
--resume-from ${检查点文件}: 继续使用先前的检查点 (checkpoint) 文件(可以继续训练过程)
--load-from ${检查点文件}: 从一个检查点 (checkpoint) 文件里加载权重(对另一个任务进行精调)
--deterministic: 选择此模式会减慢训练速度,但结果易于复现
resume-from 和 load-from 的区别:
resume-from 加载出模型权重和优化器状态包括迭代轮数等
load-from 仅加载模型权重,从第0轮开始训练
示例:
# 模型的权重和日志将会存储在这个路径下: WORK_DIR=work_dirs/pspnet_r50-d8_512x512_80k_ade20k/
# 如果work_dir没有被设定,它将会被自动生成
sh tools/dist_train.sh configs/pspnet/pspnet_r50-d8_512x512_80k_ade20k.py 8 --work_dir work_dirs/pspnet_r50-d8_512x512_80k_ade20k/ --deterministic
注意: 在训练时,模型的和日志保存在“work_dirs/”下的配置文件的相同文件夹结构中。不建议使用自定义的“work_dirs/”,因为验证脚本可以从配置文件名中推断工作目录。如果你想在其他地方保存模型的权重,请使用符号链接,例如
ln -s ${YOUR_WORK_DIRS} ${MMSEG}/work_dirs
在单个机器上启动多个任务
如果您在单个机器上启动多个任务,例如在8卡 GPU 的一个机器上有2个4卡 GPU 的训练任务,您需要特别对每个任务指定不同的端口(默认为29500)来避免通讯冲突。否则,将会有报错信息 RuntimeError: Address already in use
。
如果您使用命令 dist_train.sh
来启动一个训练任务,您可以在命令行的用环境变量 PORT
设置端口:
CUDA_VISIBLE_DEVICES=0,1,2,3 PORT=29500 sh tools/dist_train.sh ${CONFIG_FILE} 4
CUDA_VISIBLE_DEVICES=4,5,6,7 PORT=29501 sh tools/dist_train.sh ${CONFIG_FILE} 4
可参考 https://mmsegmentation.readthedocs.io/zh_CN/latest/train.html
单卡GPU测试
python tools/test.py ${配置文件} ${检查点文件} [--out ${结果文件}] [--eval ${评估指标}] [--show]
多卡GPU测试
./tools/dist_test.sh ${配置文件} ${检查点文件} ${GPU数目} [--out ${结果文件}] [--eval ${评估指标}]
可选参数:
RESULT_FILE: pickle 格式的输出结果的文件名,如果不专门指定,结果将不会被专门保存成文件。(MMseg v0.17 之后,args.out 将只会保存评估时的中间结果或者是分割图的保存路径。)
EVAL_METRICS: 在结果里将被评估的指标。这主要取决于数据集, mIoU 对于所有数据集都可获得,像 Cityscapes 数据集可以通过 cityscapes 命令来专门评估,就像标准的 mIoU一样。
--show: 如果被指定,分割结果将会在一张图像里画出来并且在另一个窗口展示。它仅仅是用来调试与可视化,并且仅针对单卡 GPU 测试。请确认 GUI 在您的环境里可用,否则您也许会遇到报错 cannot connect to X server
--show-dir: 如果被指定,分割结果将会在一张图像里画出来并且保存在指定文件夹里。它仅仅是用来调试与可视化,并且仅针对单卡GPU测试。使用该参数时,您的环境不需要 GUI。
--eval-options: 评估时的可选参数,当设置 efficient_test=True 时,它将会保存中间结果至本地文件里以节约 CPU 内存。请确认您本地硬盘有足够的存储空间(大于20GB)。(MMseg v0.17 之后,efficient_test 不再生效,我们重构了 test api,通过使用一种渐近式的方式来提升评估和保存结果的效率。)
示例:检查点文件已经保存至checkpoints/
里
测试PSPNet并保存画出的图以便于之后的可视化
python tools/test.py configs/pspnet/pspnet_r50-d8_512x1024_40k_cityscapes.py \
checkpoints/pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth \
--show-dir psp_r50_512x1024_40ki_cityscapes_results
使用4卡GPU测试PSPNet,并且在标准mIou和cityscapes指标里评估模型
./tools/dist_test.sh configs/pspnet/pspnet_r50-d8_512x1024_40k_cityscapes.py \
checkpoints/pspnet_r50-d8_512x1024_40k_cityscapes_20200605_003338-2966598c.pth \
4 --out results.pkl --eval mIoU cityscapes
其它情况可参考 https://mmsegmentation.readthedocs.io/zh_CN/latest/inference.html
可以运行python/tools/print_config.py /PATH/TO/CONFIG
查看完整的配置文件
在config/_base_
文件夹下面有4种基本组件类型: 数据集(dataset),模型(model),训练策略(schedule)和运行时的默认设置(default runtime)。
{model}_{backbone}_[misc]_[gpu x batch_per_gpu]_{resolution}_{iterations}_{dataset}
{xxx}
是被要求的文件 [yyy]
是可选的
{model}: 模型种类,例如 psp, deeplabv3 等等
{backbone}: 主干网络种类,例如 r50 (ResNet-50), x101 (ResNeXt-101)
[misc]: 模型中各式各样的设置/插件,例如 dconv, gcb, attention, mstrain
[gpu x batch_per_gpu]: GPU数目 和每个 GPU 的样本数, 默认为 8x2
{iterations}: 训练迭代轮数,如160k
{dataset}: 数据集,如 cityscapes, voc12aug, ade
norm_cfg = dict(type='SyncBN', requires_grad=True) # 分割框架通常使用 SyncBN
model = dict(
type='EncoderDecoder', # 分割器(segmentor)的名字
pretrained='open-mmlab://resnet50_v1c', # 将被加载的 ImageNet 预训练主干网络
backbone=dict(
type='ResNetV1c', # 主干网络的类别。 可用选项请参考 mmseg/models/backbones/resnet.py
depth=50, # 主干网络的深度。通常为 50 和 101。
num_stages=4, # 主干网络状态(stages)的数目,这些状态产生的特征图作为后续的 head 的输入。
out_indices=(0, 1, 2, 3), # 每个状态产生的特征图输出的索引。
dilations=(1, 1, 2, 4), # 每一层(layer)的空心率(dilation rate)。
strides=(1, 2, 1, 1), # 每一层(layer)的步长(stride)。
norm_cfg=dict( # 归一化层(norm layer)的配置项。
type='SyncBN', # 归一化层的类别。通常是 SyncBN。
requires_grad=True), # 是否训练归一化里的 gamma 和 beta。
norm_eval=False, # 是否冻结 BN 里的统计项。
style='pytorch', # 主干网络的风格,'pytorch' 意思是步长为2的层为 3x3 卷积, 'caffe' 意思是步长为2的层为 1x1 卷积。
contract_dilation=True), # 当空洞 > 1, 是否压缩第一个空洞层。
decode_head=dict(
type='PSPHead', # 解码头(decode head)的类别。 可用选项请参考 mmseg/models/decode_heads。
in_channels=2048, # 解码头的输入通道数。
in_index=3, # 被选择的特征图(feature map)的索引。
channels=512, # 解码头中间态(intermediate)的通道数。
pool_scales=(1, 2, 3, 6), # PSPHead 平均池化(avg pooling)的规模(scales)。 细节请参考文章内容。
dropout_ratio=0.1, # 进入最后分类层(classification layer)之前的 dropout 比例。
num_classes=19, # 分割前景的种类数目。 通常情况下,cityscapes 为19,VOC为21,ADE20k 为150。
norm_cfg=dict(type='SyncBN', requires_grad=True), # 归一化层的配置项。
align_corners=False, # 解码里调整大小(resize)的 align_corners 参数。
loss_decode=dict( # 解码头(decode_head)里的损失函数的配置项。
type='CrossEntropyLoss', # 在分割里使用的损失函数的类别。
use_sigmoid=False, # 在分割里是否使用 sigmoid 激活。
loss_weight=1.0)), # 解码头里损失的权重。
auxiliary_head=dict(
type='FCNHead', # 辅助头(auxiliary head)的种类。可用选项请参考 mmseg/models/decode_heads。
in_channels=1024, # 辅助头的输入通道数。
in_index=2, # 被选择的特征图(feature map)的索引。
channels=256, # 辅助头中间态(intermediate)的通道数。
num_convs=1, # FCNHead 里卷积(convs)的数目. 辅助头里通常为1。
concat_input=False, # 在分类层(classification layer)之前是否连接(concat)输入和卷积的输出。
dropout_ratio=0.1, # 进入最后分类层(classification layer)之前的 dropout 比例。
num_classes=19, # 分割前景的种类数目。 通常情况下,cityscapes 为19,VOC为21,ADE20k 为150。
norm_cfg=dict(type='SyncBN', requires_grad=True), # 归一化层的配置项。
align_corners=False, # 解码里调整大小(resize)的 align_corners 参数。
loss_decode=dict( # 辅助头(auxiliary head)里的损失函数的配置项。
type='CrossEntropyLoss', # 在分割里使用的损失函数的类别。
use_sigmoid=False, # 在分割里是否使用 sigmoid 激活。
loss_weight=0.4))) # 辅助头里损失的权重。默认设置为0.4。
train_cfg = dict() # train_cfg 当前仅是一个占位符。
test_cfg = dict(mode='whole') # 测试模式, 选项是 'whole' 和 'sliding'. 'whole': 整张图像全卷积(fully-convolutional)测试。 'sliding': 图像上做滑动裁剪窗口(sliding crop window)。
dataset_type = 'CityscapesDataset' # 数据集类型,这将被用来定义数据集。
data_root = 'data/cityscapes/' # 数据的根路径。
img_norm_cfg = dict( # 图像归一化配置,用来归一化输入的图像。
mean=[123.675, 116.28, 103.53], # 预训练里用于预训练主干网络模型的平均值。
std=[58.395, 57.12, 57.375], # 预训练里用于预训练主干网络模型的标准差。
to_rgb=True) # 预训练里用于预训练主干网络的图像的通道顺序。
crop_size = (512, 1024) # 训练时的裁剪大小
train_pipeline = [ #训练流程
dict(type='LoadImageFromFile'), # 第1个流程,从文件路径里加载图像。
dict(type='LoadAnnotations'), # 第2个流程,对于当前图像,加载它的注释信息。
dict(type='Resize', # 变化图像和其注释大小的数据增广的流程。
img_scale=(2048, 1024), # 图像的最大规模。
ratio_range=(0.5, 2.0)), # 数据增广的比例范围。
dict(type='RandomCrop', # 随机裁剪当前图像和其注释大小的数据增广的流程。
crop_size=(512, 1024), # 随机裁剪图像生成 patch 的大小。
cat_max_ratio=0.75), # 单个类别可以填充的最大区域的比例。
dict(
type='RandomFlip', # 翻转图像和其注释大小的数据增广的流程。
flip_ratio=0.5), # 翻转图像的概率
dict(type='PhotoMetricDistortion'), # 光学上使用一些方法扭曲当前图像和其注释的数据增广的流程。
dict(
type='Normalize', # 归一化当前图像的数据增广的流程。
mean=[123.675, 116.28, 103.53], # 这些键与 img_norm_cfg 一致,因为 img_norm_cfg 被
std=[58.395, 57.12, 57.375], # 用作参数。
to_rgb=True),
dict(type='Pad', # 填充当前图像到指定大小的数据增广的流程。
size=(512, 1024), # 填充的图像大小。
pad_val=0, # 图像的填充值。
seg_pad_val=255), # 'gt_semantic_seg'的填充值。
dict(type='DefaultFormatBundle'), # 流程里收集数据的默认格式捆。
dict(type='Collect', # 决定数据里哪些键被传递到分割器里的流程。
keys=['img', 'gt_semantic_seg'])
]
test_pipeline = [
dict(type='LoadImageFromFile'), # 第1个流程,从文件路径里加载图像。
dict(
type='MultiScaleFlipAug', # 封装测试时数据增广(test time augmentations)。
img_scale=(2048, 1024), # 决定测试时可改变图像的最大规模。用于改变图像大小的流程。
flip=False, # 测试时是否翻转图像。
transforms=[
dict(type='Resize', # 使用改变图像大小的数据增广。
keep_ratio=True), # 是否保持宽和高的比例,这里的图像比例设置将覆盖上面的图像规模大小的设置。
dict(type='RandomFlip'), # 考虑到 RandomFlip 已经被添加到流程里,当 flip=False 时它将不被使用。
dict(
type='Normalize', # 归一化配置项,值来自 img_norm_cfg。
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', # 将图像转为张量
keys=['img']),
dict(type='Collect', # 收集测试时必须的键的收集流程。
keys=['img'])
])
]
data = dict(
samples_per_gpu=2, # 单个 GPU 的 Batch size
workers_per_gpu=2, # 单个 GPU 分配的数据加载线程数
train=dict( # 训练数据集配置
type='CityscapesDataset', # 数据集的类别, 细节参考自 mmseg/datasets/。
data_root='data/cityscapes/', # 数据集的根目录。
img_dir='leftImg8bit/train', # 数据集图像的文件夹。
ann_dir='gtFine/train', # 数据集注释的文件夹。
pipeline=[ # 流程, 由之前创建的 train_pipeline 传递进来。
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations'),
dict(
type='Resize', img_scale=(2048, 1024), ratio_range=(0.5, 2.0)),
dict(type='RandomCrop', crop_size=(512, 1024), cat_max_ratio=0.75),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='PhotoMetricDistortion'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='Pad', size=(512, 1024), pad_val=0, seg_pad_val=255),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_semantic_seg'])
]),
val=dict( # 验证数据集的配置
type='CityscapesDataset',
data_root='data/cityscapes/',
img_dir='leftImg8bit/val',
ann_dir='gtFine/val',
pipeline=[ # 由之前创建的 test_pipeline 传递的流程。
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(2048, 1024),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img'])
])
]),
test=dict(
type='CityscapesDataset',
data_root='data/cityscapes/',
img_dir='leftImg8bit/val',
ann_dir='gtFine/val',
pipeline=[
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(2048, 1024),
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(
type='Normalize',
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
to_rgb=True),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img'])
])
]))
log_config = dict( # 注册日志钩 (register logger hook) 的配置文件。
interval=50, # 打印日志的间隔
hooks=[
dict(type='TextLoggerHook', by_epoch=False),
dict(type='TensorboardLoggerHook', by_epoch=False),
dict(type='MMSegWandbHook', by_epoch=False, init_kwargs={'entity': entity, 'project': project, 'config': cfg_dict}), # 同样支持 Wandb 日志
])
dist_params = dict(backend='nccl') # 用于设置分布式训练的参数,端口也同样可被设置。
log_level = 'INFO' # 日志的级别。
load_from = None # 从一个给定路径里加载模型作为预训练模型,它并不会消耗训练时间。
resume_from = None # 从给定路径里恢复检查点(checkpoints),训练模式将从检查点保存的轮次开始恢复训练。
workflow = [('train', 1)] # runner 的工作流程。 [('train', 1)] 意思是只有一个工作流程而且工作流程 'train' 仅执行一次。根据 `runner.max_iters` 工作流程训练模型的迭代轮数为40000次。
cudnn_benchmark = True # 是否是使用 cudnn_benchmark 去加速,它对于固定输入大小的可以提高训练速度。
optimizer = dict( # 用于构建优化器的配置文件。支持 PyTorch 中的所有优化器,同时它们的参数与PyTorch里的优化器参数一致。
type='SGD', # 优化器种类,更多细节可参考 https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/optimizer/default_constructor.py#L13。
lr=0.01, # 优化器的学习率,参数的使用细节请参照对应的 PyTorch 文档。
momentum=0.9, # 动量 (Momentum)
weight_decay=0.0005) # SGD 的衰减权重 (weight decay)。
optimizer_config = dict() # 用于构建优化器钩 (optimizer hook) 的配置文件,执行细节请参考 https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/hooks/optimizer.py#L8。
lr_config = dict(
policy='poly', # 调度流程的策略,同样支持 Step, CosineAnnealing, Cyclic 等. 请从 https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/hooks/lr_updater.py#L9 参考 LrUpdater 的细节。
power=0.9, # 多项式衰减 (polynomial decay) 的幂。
min_lr=0.0001, # 用来稳定训练的最小学习率。
by_epoch=False) # 是否按照每个 epoch 去算学习率。
runner = dict(
type='IterBasedRunner', # 将使用的 runner 的类别 (例如 IterBasedRunner 或 EpochBasedRunner)。
max_iters=40000) # 全部迭代轮数大小,对于 EpochBasedRunner 使用 `max_epochs` 。
checkpoint_config = dict( # 设置检查点钩子 (checkpoint hook) 的配置文件。执行时请参考 https://github.com/open-mmlab/mmcv/blob/master/mmcv/runner/hooks/checkpoint.py。
by_epoch=False, # 是否按照每个 epoch 去算 runner。
interval=4000) # 保存的间隔
evaluation = dict( # 构建评估钩 (evaluation hook) 的配置文件。细节请参考 mmseg/core/evaluation/eval_hook.py。
interval=4000, # 评估的间歇点
metric='mIoU') # 评估的指标
使用_delete=True
去忽略基础配置文件里的一些域内容。
在 MMSegmentation 里,例如为了改变 PSPNet 的主干网络的某些内容:
# 修改前
norm_cfg = dict(type='SyncBN', requires_grad=True)
model = dict(
type='MaskRCNN',
pretrained='torchvision://resnet50',
backbone=dict(
type='ResNetV1c',
depth=50,
num_stages=4,
out_indices=(0, 1, 2, 3),
dilations=(1, 1, 2, 4),
strides=(1, 2, 1, 1),
norm_cfg=norm_cfg,
norm_eval=False,
style='pytorch',
contract_dilation=True),
decode_head=dict(...),
auxiliary_head=dict(...))
# 修改后
_base_ = '../pspnet/psp_r50_512x1024_40ki_cityscpaes.py'
norm_cfg = dict(type='SyncBN', requires_grad=True)
model = dict(
pretrained='open-mmlab://msra/hrnetv2_w32',
backbone=dict(
_delete_=True,
type='HRNet',
norm_cfg=norm_cfg,
extra=dict(
stage1=dict(
num_modules=1,
num_branches=1,
block='BOTTLENECK',
num_blocks=(4, ),
num_channels=(64, )),
stage2=dict(
num_modules=1,
num_branches=2,
block='BASIC',
num_blocks=(4, 4),
num_channels=(32, 64)),
stage3=dict(
num_modules=4,
num_branches=3,
block='BASIC',
num_blocks=(4, 4, 4),
num_channels=(32, 64, 128)),
stage4=dict(
num_modules=3,
num_branches=4,
block='BASIC',
num_blocks=(4, 4, 4, 4),
num_channels=(32, 64, 128, 256)))),
decode_head=dict(...),
auxiliary_head=dict(...))
_delete_=True
将用新的键去替换 backbone
域内所有老的键。
可参考1.2 官方数据集格式
如果给定 split
参数,只有部分在 img_dir/ann_dir 里的文件会被加载。 我们可以对被包括在 split 文本里的文件指定前缀。
一个 split 文本如下所示:
xxx
zzz
MMSegmentation 同样支持混合数据集去训练。 当前它支持拼接 (concat), 重复 (repeat) 和多图混合 (multi-image mix)数据集。
具体见 https://mmsegmentation.readthedocs.io/zh_CN/latest/tutorials/customize_datasets.html
不同于PyTorch官方使用Dataset和DataLoader做多线程的数据加载。而MMCV中使用DataContainer类别去帮助收集和分发不同大小的输入数据。
数据流程中的操作可分为数据加载 (data loading),预处理 (pre-processing),格式变化 (formatting) 和测试时数据增强 (test-time augmentation)。
img_norm_cfg = dict(
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
crop_size = (512, 1024)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations'),
dict(type='Resize', img_scale=(2048, 1024), ratio_range=(0.5, 2.0)),
dict(type='RandomCrop', crop_size=crop_size, cat_max_ratio=0.75),
dict(type='RandomFlip', flip_ratio=0.5),
dict(type='PhotoMetricDistortion'),
dict(type='Normalize', **img_norm_cfg),
dict(type='Pad', size=crop_size, pad_val=0, seg_pad_val=255),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_semantic_seg']),
]
test_pipeline = [
dict(type='LoadImageFromFile'),
dict(
type='MultiScaleFlipAug',
img_scale=(2048, 1024),
# img_ratios=[0.5, 0.75, 1.0, 1.25, 1.5, 1.75],
flip=False,
transforms=[
dict(type='Resize', keep_ratio=True),
dict(type='RandomFlip'),
dict(type='Normalize', **img_norm_cfg),
dict(type='ImageToTensor', keys=['img']),
dict(type='Collect', keys=['img']),
])
]
数据加载:LoadImageFromFile 、LoadAnnotations
预处理:Resize、RandomFlip、Pad、RandomCrop、Normalize、SegRescale、PhotoMetricDistortion
格式:ToTensor、ImageToTensor、Transpose、ToDataContainer
1、在任何一个文件里写一个新的流程,例如 my_pipeline.py
,它以一个字典作为输入并且输出一个字典
from mmseg.datasets import PIPELINES
@PIPELINES.register_module()
class MyTransform:
def __call__(self, results):
results['dummy'] = True
return results
2、导入一个新类
from .my_pipeline import MyTransform
3、在配置文件里使用它
img_norm_cfg = dict(
mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True)
crop_size = (512, 1024)
train_pipeline = [
dict(type='LoadImageFromFile'),
dict(type='LoadAnnotations'),
dict(type='MyTransform'),
dict(type='DefaultFormatBundle'),
dict(type='Collect', keys=['img', 'gt_semantic_seg']),
]
假设您想增加一个新的叫 MyOptimizer
的优化器,它的参数分别为 a
, b
, 和 c
。 您首先需要在一个文件里实现这个新的优化器,例如在 mmseg/core/optimizer/my_optimizer.py
里面:
from mmcv.runner import OPTIMIZERS
from torch.optim import Optimizer
@OPTIMIZERS.register_module
class MyOptimizer(Optimizer):
def __init__(self, a, b, c)
然后增加这个模块到 mmseg/core/optimizer/__init__.py
里面,这样注册器 (registry) 将会发现这个新的模块并添加它:
from .my_optimizer import MyOptimizer
之后您可以在配置文件的 optimizer
域里使用 MyOptimizer
, 如下所示,在配置文件里,优化器被 optimizer
域所定义:
optimizer = dict(type='MyOptimizer', a=a_value, b=b_value, c=c_value)
1、创建一个新的文件 mmseg/models/backbones/mobilenet.py
import torch.nn as nn
from ..builder import BACKBONES
@BACKBONES.register_module
class MobileNet(nn.Module):
def __init__(self, arg1, arg2):
pass
def forward(self, x): # should return a tuple
pass
def init_weights(self, pretrained=None):
pass
2、在 mmseg/models/backbones/__init__.py
里面导入模块
from .mobilenet import MobileNet
3、在配置文件里使用它
model = dict(
...
backbone=dict(
type='MobileNet',
arg1=xxx,
arg2=xxx),
...
在 MMSegmentation 里面,对于所有的分割头,我们提供一个基类解码头 BaseDecodeHead
首先,在 mmseg/models/decode_heads/psp_head.py
里添加一个新的解码头。 PSPNet 中实现了一个语义分割的解码头。为了实现一个解码头,我们只需要在新构造的解码头中实现如下的3个函数:
@HEADS.register_module()
class PSPHead(BaseDecodeHead):
def __init__(self, pool_scales=(1, 2, 3, 6), **kwargs):
super(PSPHead, self).__init__(**kwargs)
def init_weights(self):
def forward(self, inputs):
接着,使用者需要在 mmseg/models/decode_heads/__init__.py
里面添加这个模块,这样对应的注册器 (registry) 可以查找并加载它们。
norm_cfg = dict(type='SyncBN', requires_grad=True)
model = dict(
type='EncoderDecoder',
pretrained='pretrain_model/resnet50_v1c_trick-2cccc1ad.pth',
backbone=dict(
type='ResNetV1c',
depth=50,
num_stages=4,
out_indices=(0, 1, 2, 3),
dilations=(1, 1, 2, 4),
strides=(1, 2, 1, 1),
norm_cfg=norm_cfg,
norm_eval=False,
style='pytorch',
contract_dilation=True),
decode_head=dict(
type='PSPHead', # 这里添加新的decode_head
in_channels=2048,
in_index=3,
channels=512,
pool_scales=(1, 2, 3, 6),
dropout_ratio=0.1,
num_classes=19,
norm_cfg=norm_cfg,
align_corners=False,
loss_decode=dict(
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0)))
假设您想添加一个新的损失函数 MyLoss
到语义分割解码器里。 为了添加一个新的损失函数,使用者需要在 mmseg/models/losses/my_loss.py
里面去实现它。 weighted_loss
可以对计算损失时的每个样本做加权。
import torch
import torch.nn as nn
from ..builder import LOSSES
from .utils import weighted_loss
@weighted_loss
def my_loss(pred, target):
assert pred.size() == target.size() and target.numel() > 0
loss = torch.abs(pred - target)
return loss
@LOSSES.register_module
class MyLoss(nn.Module):
def __init__(self, reduction='mean', loss_weight=1.0):
super(MyLoss, self).__init__()
self.reduction = reduction
self.loss_weight = loss_weight
def forward(self,
pred,
target,
weight=None,
avg_factor=None,
reduction_override=None):
assert reduction_override in (None, 'none', 'mean', 'sum')
reduction = (
reduction_override if reduction_override else self.reduction)
loss = self.loss_weight * my_loss(
pred, target, weight, reduction=reduction, avg_factor=avg_factor)
return loss
然后使用者需要在 mmseg/models/losses/__init__.py
里面添加它:
from .my_loss import MyLoss, my_loss
为了使用它,修改 loss_xxx
域。之后您需要在解码头组件里修改 loss_decode
域。 loss_weight
可以被用来对不同的损失函数做加权。
loss_decode=dict(type='MyLoss', loss_weight=1.0))
在 MMSegmentation 里面,您也可以在配置文件里添加如下行来让解码头组件的学习率是主干组件的10倍。
optimizer=dict(
paramwise_cfg = dict(
custom_keys={
'head': dict(lr_mult=10.)}))
对于训练时采样,我们在 这里 做了像素采样器。 如下例子是使用 PSPNet 训练并采用 OHEM 策略的配置:
_base_ = './pspnet_r50-d8_512x1024_40k_cityscapes.py'
model=dict(
decode_head=dict(
sampler=dict(type='OHEMPixelSampler', thresh=0.7, min_kept=100000)) )
通过这种方式,只有置信分数在0.7以下的像素值点会被拿来训练。在训练时我们至少要保留100000个像素值点。如果 thresh
并未被指定,前 min_kept
个损失的像素值点才会被选择。
对于不平衡类别分布的数据集,您也许可以改变每个类别的损失权重。这里以 cityscapes 数据集为例
_base_ = './pspnet_r50-d8_512x1024_40k_cityscapes.py'
model=dict(
decode_head=dict(
loss_decode=dict(
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0,
# DeepLab 对 cityscapes 使用这种权重
class_weight=[0.8373, 0.9180, 0.8660, 1.0345, 1.0166, 0.9969, 0.9754,
1.0489, 0.8786, 1.0023, 0.9539, 0.9843, 1.1116, 0.9037,
1.0865, 1.0955, 1.0865, 1.1529, 1.0507])))
class_weight
将被作为 weight
参数,传递给 CrossEntropyLoss
。
对于训练时损失函数的计算,我们目前支持多个损失函数同时使用。 以 unet
使用 DRIVE
数据集训练为例, 使用 CrossEntropyLoss
和 DiceLoss
的 1:3
的加权和作为损失函数。配置文件写为:
_base_ = './fcn_unet_s5-d16_64x64_40k_drive.py'
model = dict(
decode_head=dict(loss_decode=[dict(type='CrossEntropyLoss', loss_name='loss_ce', loss_weight=1.0),
dict(type='DiceLoss', loss_name='loss_dice', loss_weight=3.0)]),
auxiliary_head=dict(loss_decode=[dict(type='CrossEntropyLoss', loss_name='loss_ce',loss_weight=1.0),
dict(type='DiceLoss', loss_name='loss_dice', loss_weight=3.0)]),
)
通过这种方式,确定训练过程中损失函数的权重 loss_weight
和在训练日志里的名字 loss_name
。
注意: loss_name
的名字必须带有 loss_
前缀,这样它才能被包括在反传的图里。
默认设置 avg_non_ignore=False
, 即每个像素都用来计算损失函数。尽管其中的一些像素属于需要被忽略的类别。
对于训练时损失函数的计算,我们目前支持使用 avg_non_ignore
和 ignore_index
来忽略 label 特定的类别。 这样损失函数将只在非忽略类别像素中求平均值,会获得更好的表现
以 unet
使用 Cityscapes
数据集训练为例, 在计算损失函数时,忽略 label 为0的背景,并且仅在不被忽略的像素上计算均值。配置文件写为:
_base_ = './fcn_unet_s5-d16_4x4_512x1024_160k_cityscapes.py'
model = dict(
decode_head=dict(
ignore_index=0,
loss_decode=dict(
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0, avg_non_ignore=True),
auxiliary_head=dict(
ignore_index=0,
loss_decode=dict(
type='CrossEntropyLoss', use_sigmoid=False, loss_weight=1.0, avg_non_ignore=True)),
))
通过这种方式,确定训练过程中损失函数的权重 loss_weight
和在训练日志里的名字 loss_name
。
注意: loss_name
的名字必须带有 loss_
前缀,这样它才能被包括在反传的图里。
使用梯度截断 (gradient clip) 去稳定训练
使用动量计划表 (momentum schedule) 去加速模型收敛:
自定义 learning schedule
自定义workflow
自定义钩hooks
原始官方文档见https://mmsegmentation.readthedocs.io/zh_CN/latest/tutorials/customize_runtime.html#