15分钟快速上手OpenMM框架——从自定义Config到模型微调
参考资料:
mmpretrain-学习配置文件
mmengine-mm架构说明
1. 什么是Config?——Config概述
在软件开发中,Config(或Configuration)指的是配置文件或配置项,用于存储和管理应用程序的参数、设置和选项。配置文件通常是一个文本文件,其中包含一系列键值对(或其他格式),用于指定应用程序的行为和属性。
配置文件的作用是将应用程序的参数和设置与代码分离,使得应用程序的行为可以根据配置文件的内容进行灵活的调整和定制,而无需修改代码。通过修改配置文件,可以改变应用程序的行为、适应不同的环境或需求,而无需重新编译或重新部署代码。
配置文件通常包含应用程序的各种配置选项,例如数据库连接信息、日志级别、文件路径、网络端口等。在运行时,应用程序可以读取配置文件的内容,并根据配置文件中指定的参数来初始化和配置自身。
配置文件的格式可以是各种形式,如INI格式、JSON格式、YAML格式等,具体取决于开发者的选择和应用程序的要求。
通过使用配置文件,开发者可以实现灵活、可配置的应用程序,使得应用程序能够适应不同的环境和需求,提高代码的可维护性和可扩展性。
在OpenMMLab中,同样使用了"Config"的概念,配置文件以py格式储存,它是用于配置模型、训练和推理等任务的重要组成部分,用于指定各种参数、超参数、路径和其他设置,主要作用是将各种参数和设置与代码分离,使其更具可配置性和可重复性。通过修改配置文件中的参数,可以轻松地进行不同实验设置的尝试和比较。
OpenMMLab的配置文件通常包含以下内容:
-
模型配置:指定模型的架构、层次结构、特征维度等相关信息。
-
数据配置:指定数据集的路径、文件格式、数据增强等数据处理相关的设置。
-
训练配置:指定训练过程中的优化器、学习率策略、损失函数等超参数。
-
推理配置:指定推理阶段的批处理大小、推理模式、后处理等相关设置。
-
路径配置:指定保存模型、日志文件、结果输出等路径和文件名。
-
其他参数:可能包含一些其他的任务特定参数或自定义参数。
在OpenMMLab中,配置文件通常作为命令行参数传递给训练脚本或推理脚本,以便在运行时加载和使用配置。这样的设计使得模型的训练和推理过程更加灵活和可定制。
例如以下代码中的第2-4行(–work-dir是通过命令行形式对Config的一个更新):
CUDA_VISIBLE_DEVICES=2 PORT=8083 nohup \
python </Absolute/Path/of/tools/train.py> \
</Absolute/Path/of/config.py> \
--work-dir </Absolute/Path/of/work/result/mmpose> \
> output.log 2>&1 &
以下代码是调用config文件进行模型训练的最原始形式:
python </Absolute/Path/of/tools/train.py> \
</Absolute/Path/of/config.py> \
总而言之,OpenMMLab中的配置文件用于定义模型、训练和推理等任务的参数和设置,提供了一种方便的方式来管理和调整实验设置。
2. 如何理解Config?——Config示例与使用
目录结构
your_directory
|---main_config.py
|---resnet18.py
|---imagenet_bs32.py
|---imagenet_bs256.py
|---default_runtime.py
main_config.py(被命令行调用的Config)
当OpenMMLab的配置系统解析配置文件时,它首先会加载
_base_
指向的配置文件,然后将当前配置文件中的配置加载进去,如果有相同的键(比如model
或optimizer
),当前配置文件中的配置将会覆盖_base_
中的配置,所以,在你的例子中,即使resnet18.py
中并没有定义optimizer
字段,它也会从_base_
中的optimizer_cfg.py
文件中继承optimizer
的配置。这就是为什么在
_base_= ['optimizer_cfg.py']
之后,配置文件会获得optimizer_cfg.py
中的所有字段的原因。——OpenMMEngine说明文档
以下代码中,我们使用相对路径为_base_
添加导入的配置文件(使用绝对路径也可以)
_base_ = [
'./resnet18.py',
'./imagenet_bs32.py',
'./imagenet_bs256.py',
'./default_runtime.py'
]
resnet18.py
# 定义模型类在初始化时的参数
model = dict(
type='ImageClassifier',
backbone=dict(
type='ResNet',
depth=18,
num_stages=4,
out_indices=(3,),
style='pytorch'
), # 主干网络,用于提取特征;这里使用的是resnet18
neck=dict(
type='GlobalAveragePooling'
), # neck用于将主干网络提取的特征进行处理,这里使用的是全局平均池化,将特征变成一维向量
head=dict(
type='LinearClsHead',
num_classes=1000,
in_channels=512,
loss=dict(type='CrossEntropyLoss', loss_weight=1.0),
topk=(1, 5),
) # head用于将向量映射到类别空间,这里使用的是线性分类器
)
imagenet_bs32.py
# 数据集配置文件,用于配置数据集的相关参数,包括数据集类型、数据集路径、数据集预处理方式、数据集加载方式等等
dataset_type = 'ImageNet'
data_preprocessor = dict(
# num_classes是类别数,需要和模型的head中的num_classes一致
num_classes=1000,
# mean和std是归一化的参数
mean=[123.675, 116.28, 103.53],
std=[58.395, 57.12, 57.375],
# to_rgb表示是否将图片从BGR格式转换为RGB格式
to_rgb=True,
) # 定义了模型拿到数据后的预处理方式,这里使用的是ImageNet的预处理方式
# pipeline用于数据集加载,需要写在dataloader之前以便调用
train_pipeline = [
dict(type='LoadImageFromFile'), # 从文件中读取图片
dict(type='RandomResizedCrop', scale=224), # 随机裁剪图片
dict(type='RandomFlip', prob=0.5, direction='horizontal'), # 随机翻转图片
dict(type='PackInputs'), # 将图片和标签打包
]
test_pipeline = [
dict(type='LoadImageFromFile'), # 从文件中读取图片
dict(type='Resize', scale=256, edge='short'), # 将图片的短边缩放到256
dict(type='CenterCrop', crop_size=224), # 中心裁剪图片
dict(type='PackInputs'), # 将图片和标签打包
]
# 数据集加载部分
train_dataloader = dict(
batch_size=32, # batch_size是每个batch的样本数
num_workers=5, # num_workers是用于数据加载的子进程数
dataset=dict(
type=dataset_type, # 利用python语法调用开头定义的数据集类型
data_root='data/imagenet', # 数据集的根目录
ann_file='meta/train.txt', # 数据集的标注文件
data_prefix='train', # 数据集的图片文件夹
pipeline=train_pipeline # 利用python语法调用上方定义的数据集加载方式
), # 数据集的相关配置,新数据集需要在这里进行配置
sampler=dict(
type='DefaultSampler', # 采样器类型
shuffle=True # 是否打乱数据集
), # 数据集采样器配置
) # 训练集加载器
val_dataloader = dict(
batch_size=32, # batch_size是每个batch的样本数
num_workers=5, # num_workers是用于数据加载的子进程数
dataset=dict(
type=dataset_type, # 利用python语法调用开头定义的数据集类型
data_root='data/imagenet', # 数据集的根目录
ann_file='meta/val.txt', # 数据集的标注文件
data_prefix='val', # 数据集的图片文件夹
pipeline=test_pipeline # 利用python语法调用上方定义的数据集加载方式
), # 数据集的相关配置,新数据集需要在这里进行配置
sampler=dict(
type='DefaultSampler', # 采样器类型
shuffle=False # 是否打乱数据集
), # 数据集采样器配置
) # 验证集加载器
val_evaluator = dict(
type='Accuracy', # 评估器类型
topk=(1, 5) # topk表示计算top-k准确率
) # 验证集评估器
# 利用python语法调用上方已经定义好的加载器和评估器,此处val_dataloader和val_evaluator用作test_dataloader和test_evaluator
test_dataloader = val_dataloader
test_evaluator = val_evaluator
imagenet_bs256.py
# 规划配置文件,用于规划训练过程如何进行以及具体的参数设置
# 优化器,用于优化网络参数
optim_wrapper = dict(
optimizer=dict(
type='SGD', # 优化器类型
lr=0.1, # 学习率
momentum=0.9, # 动量
weight_decay=0.0001 # 权重衰减
)
)
# 参数规划器,用于规划学习率如何变化
param_sheduler = dict(
type='MultiStepLR', # 学习率衰减器类型
by_epoch=True, # 是否按照epoch数进行衰减
milestones=[30, 60, 90], # 发生衰减的epoch数
gamma=0.1 # 衰减倍数
)
# 训练配置,用于规划训练过程如何进行
train_cfg = dict(
by_epoch=True, # 是否按照epoch数进行训练
max_epochs=100, # 最大训练epoch数
val_freq=1 # 验证集评估频率,此处1表示每个epoch都进行验证(即每隔1个epoch进行一次验证)
)
val_cfg = dict() # 空字典,表示使用默认配置
test_cfg = dict() # 空字典,表示使用默认配置
auto_scale_lr = dict(
base_batch_size=256, # 此处base_batch_size表示原始配置文件中的batch_size
) # 自动调整学习率的配置,举例:如果分布式八卡训练,每张卡的batch_size为32,则单卡学习率会自动调整1/8
default_runtime.py
# 运行参数配置
default_scope = 'mmpretrain'
default_hooks = dict(
time=dict(
type='TimeHook'
), # 计时器
logger=dict(
type='LoggerHook',
interval=10 # 日志打印频率,此处10表示每10个iteration打印一次日志
), # 日志打印器
param_scheduler=dict(
type='ParamSchedulerHook'
), # 参数规划器
checkpoint=dict(
type='CheckpointHook',
interval=1, # 模型保存频率,此处1表示每个epoch保存一次模型
max_keep_ckpts=5, # 最大保存模型数
save_best='auto' # 根据验证集指标自动保存精度最高的模型
), # 模型保存器
sampler_seed=dict(
type='DistSamplerSeedHook'
), # 此处为分布式采样器种子设置器
visualization=dict(
type='VisualizationHook',
enabled=False # 是否开启可视化
), # 可视化器
)
env_cfg = dict(
cudnn_benchmark=False, # 是否开启cudnn加速
mp_cfg=dict(
mp_start_method='fork', # 分布式训练启动方式
opencv_num_threads=0 # opencv线程数
),
dist_cfg=dict(
backend='nccl' # 分布式训练后端
),
)
vis_backends = [
dict(
type='LocalVisBackend', # 可视化后端类型
)
]
visualizer = dict(
type='UniversalVisualizer', # 可视化器类型
vis_backends=vis_backends # 可视化后端
)
log_level = 'INFO' # 日志打印等级
load_from = None # 模型加载路径
resume = None # 模型恢复路径
randomness = dict(
seed=None, # 随机种子
deterministic=False # cuda是否使用确定性算法
) # 是否开启随机性
3. 为什么要Config?——Config的设计哲学
Config的设计哲学主要包括以下几个方面:
-
模块化设计:config.py将模型的各个组件(backbone、neck、head等)以及数据处理、数据加载器、评估器、优化器等功能模块进行了模块化设计。每个组件都有独立的配置选项,使得用户能够根据需求自由选择、组合和配置各个模块,从而构建出符合自己任务需求的完整模型。
-
可扩展性:config.py提供了丰富的配置选项和参数,用户可以根据自己的需求进行定制和扩展。例如,可以选择不同的模型类型、主干网络、头部结构,配置不同的数据处理方式和数据加载器,以及根据需求选择不同的优化器和参数调度器等。这种可扩展性使得用户能够灵活地进行模型训练,并适应各种任务和数据集的需求。
-
易用性:config.py采用了简洁明了的配置方式,使用字典的形式表示各个组件的配置选项。这种配置方式使得用户可以直观地了解和修改各个模块的配置,同时也方便了代码的维护和扩展。此外,config.py还提供了默认的配置选项,用户只需修改特定的配置项,即可快速配置自己的训练任务,降低了使用的门槛。
-
可读性和可维护性:config.py采用了清晰的变量命名和注释,使得代码具有良好的可读性和可维护性。每个配置选项都有相应的注释说明,指导用户进行配置。此外,config.py还按照功能模块对配置进行了组织,使得代码结构清晰,易于理解和维护。
总的来说,config.py在代码层面上的设计哲学主要体现在模块化、可扩展性、易用性、可读性和可维护性方面。通过合理的配置选项和灵活的组件选择,用户可以根据自己的需求定制和配置模型训练任务,从而实现高效、灵活和可扩展的图像预训练。
4. FAQ
是否可能把config都写进一个dict()中?
在config.py中,type
字段起到了标识和区分不同组件的作用。每个组件(如模型、优化器、数据加载器等)都有一个type
字段,用于指定该组件的类型或名称。这样做的好处是可以根据类型来选择相应的实现或组件,实现了组件的灵活替换和定制。
合并所有的dict
为一个大的dict
是理论上可行的,但这样做会导致代码的可读性和可维护性降低。将不同的组件分别存储在不同的dict
中,有助于代码的结构清晰和组件之间的逻辑关系明确。这样设计的好处包括:
-
可读性:通过将每个组件的配置放在单独的
dict
中,可以使得代码更易于阅读和理解。每个组件都有自己的配置段落,不会混在一起,使得代码结构更清晰明了。 -
可维护性:将组件的配置分开存储,使得对某个组件进行修改或调整时更加方便。修改一个组件的配置不会影响其他组件,代码修改的范围更小,维护起来更加方便。
-
可扩展性:将组件配置分散在不同的
dict
中,方便添加、替换或删除特定的组件。如果将所有组件合并为一个大的dict
,则每次修改都需要涉及整个大dict
,不利于灵活的组件定制和扩展。
因此,将不同的组件分别存储在不同的dict
中是一种更合理的设计选择,可以提高代码的可读性、可维护性和可扩展性。