新手入门
安装
python | torch | torchaudio | torchvision | cuda | mmcv |
---|---|---|---|---|---|
3.8.10 | 2.0.0 | 2.0.1 | 0.15.1 | 12.1.1 | 2.1.0 |
- mmaction2.1.2要求mmcv<2.2.0
# conda create -n openmmlab python=3.8.10# python -m pip install --upgrade pip# pip install torch==2.0.0 torchaudio==2.0.1 torchvision==0.15.1pip install -U openmimmim install mmengine# linux版本pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.0/index.html# win版本pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cpu/torch2.0/index.htmlgit clone https://github.com/open-mmlab/mmaction2.gitcd mmaction2pip install -v -e .# "-v" 表示输出更多安装相关的信息# "-e" 表示以可编辑形式安装,这样可以在不重新安装的情况下,让本地修改直接生效。
快速运行
推理
python demo/demo_inferencer.py demo/demo.mp4 \ --rec tsn --print-result \ --label-file tools/data/kinetics/label_map_k400.txt# 推理结果# {'predictions': [{'rec_labels': [[6]], 'rec_scores': [[...]]}]}
准备数据集
# 下载官方预先准备好的 kinetics400_tiny.zip ,并将其解压到 MMAction2 根目录下的 data/ 目录wget https://download.openmmlab.com/mmaction/kinetics400_tiny.zipmkdir -p data/unzip kinetics400_tiny.zip -d data/
修改配置
使用 resnet50 作为主干网络来训练 TSN。由于 MMAction2 已经有了完整的 Kinetics400 数据集的配置文件 (configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py),只需要在其基础上进行一些修改。
# 修改数据集# 打开 configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py ,按如下替换关键字:data_root = 'data/kinetics400_tiny/train'data_root_val = 'data/kinetics400_tiny/val'ann_file_train = 'data/kinetics400_tiny/kinetics_tiny_train_video.txt'ann_file_val = 'data/kinetics400_tiny/kinetics_tiny_val_video.txt'# 修改运行配置# 由于数据集的大小减少,我们建议将训练批大小减少到4个,训练epoch的数量相应减少到10个。此外,我们建议将验证和权值存储间隔缩短为1轮,并修改学习率衰减策略。修改 configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py 中对应的关键字,如下所示生效。# 设置训练批大小为 4train_dataloader['batch_size'] = 4# 每轮都保存权重,并且只保留最新的权重default_hooks = dict( checkpoint=dict(type='CheckpointHook', interval=1, max_keep_ckpts=1))# 将最大 epoch 数设置为 10,并每 1 个 epoch验证模型train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=10, val_interval=1)#根据 10 个 epoch调整学习率调度param_scheduler = [ dict( type='MultiStepLR', begin=0, end=10, by_epoch=True, milestones=[4, 8], gamma=0.1)# 修改模型配置# 由于精简版 Kinetics 数据集规模较小,建议加载原始 Kinetics 数据集上的预训练模型。此外,模型需要根据实际类别数进行修改。请直接将以下代码添加到 configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py 中。model = dict( cls_head=dict(num_classes=2))load_from = 'https://download.openmmlab.com/mmaction/v1.0/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb_20220906-cd10898e.pth'# 在这里,我们直接通过继承 ({external+mmengine:doc} MMEngine: Config <advanced_tutorials/ Config>) 机制重写了基本配置中的相应参数。原始字段分布在 configs/_base_/models/tsn_r50.py、configs/_base_/schedules/sgd_100e.py 和 configs/_base_/default_runtime.py中。
训练
python tools/train.py configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py# 在没有额外配置的情况下,模型权重将被保存到 work_dirs/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb/,而日志将被存储到 work_dirs/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb/。
测试
20分钟了解 MMAction2 框架设计
步骤3:构建一个识别器
# 修改此处 predictions[0].pred_score -> predictions[0].pred_scores.itemprint('Scores of Sample[0]', predictions[0].pred_scores.item)
步骤4:构建一个评估指标
# 修改此处 data_sample['pred_score'].cpu().numpy() -> data_sample['pred_scores']['item']scores = data_sample['pred_scores']['item']# 并在下方额外添加 scores = np.array(scores)scores = np.array(scores)
步骤5:使用本地 PyTorch 训练和测试
'''修改此处 for data_batch in track_iter_progress(val_data_loader): ->task_num = len(val_data_loader)for data_batch in track_iter_progress((val_data_loader,task_num)):'''task_num = len(val_data_loader)for data_batch in track_iter_progress((val_data_loader,task_num)):
修改完的完整代码
from mmaction.utils import register_all_modulesregister_all_modules(init_default_scope=True)print('**************************步骤0:准备数据*****************************')print('**************************步骤1:构建一个数据流水线*****************************')import mmcvimport decordimport numpy as npfrom mmcv.transforms import TRANSFORMS, BaseTransform, to_tensorfrom mmaction.structures import ActionDataSample@TRANSFORMS.register_module()class VideoInit(BaseTransform): def transform(self, results): container = decord.VideoReader(results['filename']) results['total_frames'] = len(container) results['video_reader'] = container return results@TRANSFORMS.register_module()class VideoSample(BaseTransform): def __init__(self, clip_len, num_clips, test_mode=False): self.clip_len = clip_len self.num_clips = num_clips self.test_mode = test_mode def transform(self, results): total_frames = results['total_frames'] interval = total_frames // self.clip_len if self.test_mode: # 使测试期间的采样具有确定性 np.random.seed(42) inds_of_all_clips = [] for i in range(self.num_clips): bids = np.arange(self.clip_len) * interval offset = np.random.randint(interval, size=bids.shape) inds = bids + offset inds_of_all_clips.append(inds) results['frame_inds'] = np.concatenate(inds_of_all_clips) results['clip_len'] = self.clip_len results['num_clips'] = self.num_clips return results@TRANSFORMS.register_module()class VideoDecode(BaseTransform): def transform(self, results): frame_inds = results['frame_inds'] container = results['video_reader'] imgs = container.get_batch(frame_inds).asnumpy() imgs = list(imgs) results['video_reader'] = None del container results['imgs'] = imgs results['img_shape'] = imgs[0].shape[:2] return results@TRANSFORMS.register_module()class VideoResize(BaseTransform): def __init__(self, r_size): self.r_size = (np.inf, r_size) def transform(self, results): img_h, img_w = results['img_shape'] new_w, new_h = mmcv.rescale_size((img_w, img_h), self.r_size) imgs = [mmcv.imresize(img, (new_w, new_h)) for img in results['imgs']] results['imgs'] = imgs results['img_shape'] = imgs[0].shape[:2] return results@TRANSFORMS.register_module()class VideoCrop(BaseTransform): def __init__(self, c_size): self.c_size = c_size def transform(self, results): img_h, img_w = results['img_shape'] center_x, center_y = img_w // 2, img_h // 2 x1, x2 = center_x - self.c_size // 2, center_x + self.c_size // 2 y1, y2 = center_y - self.c_size // 2, center_y + self.c_size // 2 imgs = [img[y1:y2, x1:x2] for img in results['imgs']] results['imgs'] = imgs results['img_shape'] = imgs[0].shape[:2] return results@TRANSFORMS.register_module()class VideoFormat(BaseTransform): def transform(self, results): num_clips = results['num_clips'] clip_len = results['clip_len'] imgs = results['imgs'] # [num_clips*clip_len, H, W, C] imgs = np.array(imgs) # [num_clips, clip_len, H, W, C] imgs = imgs.reshape((num_clips, clip_len) + imgs.shape[1:]) # [num_clips, C, clip_len, H, W] imgs = imgs.transpose(0, 4, 1, 2, 3) results['imgs'] = imgs return results@TRANSFORMS.register_module()class VideoPack(BaseTransform): def __init__(self, meta_keys=('img_shape', 'num_clips', 'clip_len')): self.meta_keys = meta_keys def transform(self, results): packed_results = dict() inputs = to_tensor(results['imgs']) data_sample = ActionDataSample().set_gt_label(results['label']) metainfo = {k: results[k] for k in self.meta_keys if k in results} data_sample.set_metainfo(metainfo) packed_results['inputs'] = inputs packed_results['data_samples'] = data_sample return packed_resultsimport os.path as ospfrom mmengine.dataset import Composepipeline_cfg = [ dict(type='VideoInit'), dict(type='VideoSample', clip_len=16, num_clips=1, test_mode=False), dict(type='VideoDecode'), dict(type='VideoResize', r_size=256), dict(type='VideoCrop', c_size=224), dict(type='VideoFormat'), dict(type='VideoPack')]pipeline = Compose(pipeline_cfg)data_prefix = 'data/kinetics400_tiny/train'results = dict(filename=osp.join(data_prefix, 'D32_1gwq35E.mp4'), label=0)packed_results = pipeline(results)inputs = packed_results['inputs']data_sample = packed_results['data_samples']print('shape of the inputs: ', inputs.shape)# 获取输入的信息print('image_shape: ', data_sample.img_shape)print('num_clips: ', data_sample.num_clips)print('clip_len: ', data_sample.clip_len)# 获取输入的标签print('label: ', data_sample.gt_label)print('**************************步骤2:构建一个数据集和数据加载器*****************************')import os.path as ospfrom mmengine.fileio import list_from_filefrom mmengine.dataset import BaseDatasetfrom mmaction.registry import DATASETS@DATASETS.register_module()class DatasetZelda(BaseDataset): def __init__(self, ann_file, pipeline, data_root, data_prefix=dict(video=''), test_mode=False, modality='RGB', **kwargs): self.modality = modality super(DatasetZelda, self).__init__(ann_file=ann_file, pipeline=pipeline, data_root=data_root, data_prefix=data_prefix, test_mode=test_mode, **kwargs) def load_data_list(self): data_list = [] fin = list_from_file(self.ann_file) for line in fin: line_split = line.strip().split() filename, label = line_split label = int(label) filename = osp.join(self.data_prefix['video'], filename) data_list.append(dict(filename=filename, label=label)) return data_list def get_data_info(self, idx: int) -> dict: data_info = super().get_data_info(idx) data_info['modality'] = self.modality return data_infofrom mmaction.registry import DATASETStrain_pipeline_cfg = [ dict(type='VideoInit'), dict(type='VideoSample', clip_len=16, num_clips=1, test_mode=False), dict(type='VideoDecode'), dict(type='VideoResize', r_size=256), dict(type='VideoCrop', c_size=224), dict(type='VideoFormat'), dict(type='VideoPack')]val_pipeline_cfg = [ dict(type='VideoInit'), dict(type='VideoSample', clip_len=16, num_clips=5, test_mode=True), dict(type='VideoDecode'), dict(type='VideoResize', r_size=256), dict(type='VideoCrop', c_size=224), dict(type='VideoFormat'), dict(type='VideoPack')]train_dataset_cfg = dict( type='DatasetZelda', ann_file='kinetics_tiny_train_video.txt', pipeline=train_pipeline_cfg, data_root='data/kinetics400_tiny/', data_prefix=dict(video='train'))val_dataset_cfg = dict( type='DatasetZelda', ann_file='kinetics_tiny_val_video.txt', pipeline=val_pipeline_cfg, data_root='data/kinetics400_tiny/', data_prefix=dict(video='val'))train_dataset = DATASETS.build(train_dataset_cfg)packed_results = train_dataset[0]inputs = packed_results['inputs']data_sample = packed_results['data_samples']print('shape of the inputs: ', inputs.shape)# 获取输入的信息print('image_shape: ', data_sample.img_shape)print('num_clips: ', data_sample.num_clips)print('clip_len: ', data_sample.clip_len)# 获取输入的标签print('label: ', data_sample.gt_label)from mmengine.runner import RunnerBATCH_SIZE = 2train_dataloader_cfg = dict( batch_size=BATCH_SIZE, num_workers=0, persistent_workers=False, sampler=dict(type='DefaultSampler', shuffle=True), dataset=train_dataset_cfg)val_dataloader_cfg = dict( batch_size=BATCH_SIZE, num_workers=0, persistent_workers=False, sampler=dict(type='DefaultSampler', shuffle=False), dataset=val_dataset_cfg)train_data_loader = Runner.build_dataloader(dataloader=train_dataloader_cfg)val_data_loader = Runner.build_dataloader(dataloader=val_dataloader_cfg)batched_packed_results = next(iter(train_data_loader))batched_inputs = batched_packed_results['inputs']batched_data_sample = batched_packed_results['data_samples']assert len(batched_inputs) == BATCH_SIZEassert len(batched_data_sample) == BATCH_SIZEprint('**************************步骤3:构建一个识别器*****************************')import torchfrom mmengine.model import BaseDataPreprocessor, stack_batchfrom mmaction.registry import MODELS@MODELS.register_module()class DataPreprocessorZelda(BaseDataPreprocessor): def __init__(self, mean, std): super().__init__() self.register_buffer( 'mean', torch.tensor(mean, dtype=torch.float32).view(-1, 1, 1, 1), False) self.register_buffer( 'std', torch.tensor(std, dtype=torch.float32).view(-1, 1, 1, 1), False) def forward(self, data, training=False): data = self.cast_data(data) inputs = data['inputs'] batch_inputs = stack_batch(inputs) # 批处理 batch_inputs = (batch_inputs - self.mean) / self.std # 归一化 data['inputs'] = batch_inputs return datafrom mmaction.registry import MODELSdata_preprocessor_cfg = dict( type='DataPreprocessorZelda', mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375])data_preprocessor = MODELS.build(data_preprocessor_cfg)preprocessed_inputs = data_preprocessor(batched_packed_results)print(preprocessed_inputs['inputs'].shape)import torchimport torch.nn as nnimport torch.nn.functional as Ffrom mmengine.model import BaseModel, BaseModule, Sequentialfrom mmengine.structures import LabelDatafrom mmaction.registry import MODELS@MODELS.register_module()class BackBoneZelda(BaseModule): def __init__(self, init_cfg=None): if init_cfg is None: init_cfg = [dict(type='Kaiming', layer='Conv3d', mode='fan_out', nonlinearity="relu"), dict(type='Constant', layer='BatchNorm3d', val=1, bias=0)] super(BackBoneZelda, self).__init__(init_cfg=init_cfg) self.conv1 = Sequential(nn.Conv3d(3, 64, kernel_size=(3, 7, 7), stride=(1, 2, 2), padding=(1, 3, 3)), nn.BatchNorm3d(64), nn.ReLU()) self.maxpool = nn.MaxPool3d(kernel_size=(1, 3, 3), stride=(1, 2, 2), padding=(0, 1, 1)) self.conv = Sequential(nn.Conv3d(64, 128, kernel_size=3, stride=2, padding=1), nn.BatchNorm3d(128), nn.ReLU()) def forward(self, imgs): # imgs: [batch_size*num_views, 3, T, H, W] # features: [batch_size*num_views, 128, T/2, H//8, W//8] features = self.conv(self.maxpool(self.conv1(imgs))) return features@MODELS.register_module()class ClsHeadZelda(BaseModule): def __init__(self, num_classes, in_channels, dropout=0.5, average_clips='prob', init_cfg=None): if init_cfg is None: init_cfg = dict(type='Normal', layer='Linear', std=0.01) super(ClsHeadZelda, self).__init__(init_cfg=init_cfg) self.num_classes = num_classes self.in_channels = in_channels self.average_clips = average_clips if dropout != 0: self.dropout = nn.Dropout(dropout) else: self.dropout = None self.fc = nn.Linear(self.in_channels, self.num_classes) self.pool = nn.AdaptiveAvgPool3d(1) self.loss_fn = nn.CrossEntropyLoss() def forward(self, x): N, C, T, H, W = x.shape x = self.pool(x) x = x.view(N, C) assert x.shape[1] == self.in_channels if self.dropout is not None: x = self.dropout(x) cls_scores = self.fc(x) return cls_scores def loss(self, feats, data_samples): cls_scores = self(feats) labels = torch.stack([x.gt_label for x in data_samples]) labels = labels.squeeze() if labels.shape == torch.Size([]): labels = labels.unsqueeze(0) loss_cls = self.loss_fn(cls_scores, labels) return dict(loss_cls=loss_cls) def predict(self, feats, data_samples): cls_scores = self(feats) num_views = cls_scores.shape[0] // len(data_samples) # assert num_views == data_samples[0].num_clips cls_scores = self.average_clip(cls_scores, num_views) for ds, sc in zip(data_samples, cls_scores): pred = LabelData(item=sc) ds.pred_scores = pred return data_samples def average_clip(self, cls_scores, num_views): if self.average_clips not in ['score', 'prob', None]: raise ValueError(f'{self.average_clips} is not supported. ' f'Currently supported ones are ' f'["score", "prob", None]') total_views = cls_scores.shape[0] cls_scores = cls_scores.view(total_views // num_views, num_views, -1) if self.average_clips is None: return cls_scores elif self.average_clips == 'prob': cls_scores = F.softmax(cls_scores, dim=2).mean(dim=1) elif self.average_clips == 'score': cls_scores = cls_scores.mean(dim=1) return cls_scores@MODELS.register_module()class RecognizerZelda(BaseModel): def __init__(self, backbone, cls_head, data_preprocessor): super().__init__(data_preprocessor=data_preprocessor) self.backbone = MODELS.build(backbone) self.cls_head = MODELS.build(cls_head) def extract_feat(self, inputs): inputs = inputs.view((-1, ) + inputs.shape[2:]) return self.backbone(inputs) def loss(self, inputs, data_samples): feats = self.extract_feat(inputs) loss = self.cls_head.loss(feats, data_samples) return loss def predict(self, inputs, data_samples): feats = self.extract_feat(inputs) predictions = self.cls_head.predict(feats, data_samples) return predictions def forward(self, inputs, data_samples=None, mode='tensor'): if mode == 'tensor': return self.extract_feat(inputs) elif mode == 'loss': return self.loss(inputs, data_samples) elif mode == 'predict': return self.predict(inputs, data_samples) else: raise RuntimeError(f'Invalid mode: {mode}')import torchimport copyfrom mmaction.registry import MODELSmodel_cfg = dict( type='RecognizerZelda', backbone=dict(type='BackBoneZelda'), cls_head=dict( type='ClsHeadZelda', num_classes=2, in_channels=128, average_clips='prob'), data_preprocessor = dict( type='DataPreprocessorZelda', mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375]))model = MODELS.build(model_cfg)# 训练model.train()model.init_weights()data_batch_train = copy.deepcopy(batched_packed_results)data = model.data_preprocessor(data_batch_train, training=True)loss = model(**data, mode='loss')print('loss dict: ', loss)# 验证with torch.no_grad(): model.eval() data_batch_test = copy.deepcopy(batched_packed_results) data = model.data_preprocessor(data_batch_test, training=False) predictions = model(**data, mode='predict')here = (predictions)print('Label of Sample[0]', predictions[0].gt_label)print('----------------------------------------------------')print('Label of Sample[0]', predictions[0].gt_label)print('Scores of Sample[0]', predictions[0].pred_scores.item)print('**************************步骤4:构建一个评估指标*****************************')import copyfrom collections import OrderedDictfrom mmengine.evaluator import BaseMetricfrom mmaction.evaluation import top_k_accuracyfrom mmaction.registry import METRICS@METRICS.register_module()class AccuracyMetric(BaseMetric): def __init__(self, topk=(1, 5), collect_device='cpu', prefix='acc'): super().__init__(collect_device=collect_device, prefix=prefix) self.topk = topk def process(self, data_batch, data_samples): data_samples = copy.deepcopy(data_samples) for data_sample in data_samples: result = dict() scores = data_sample['pred_scores']['item'] scores = np.array(scores) label = data_sample['gt_label'].item() result['scores'] = scores result['label'] = label self.results.append(result) def compute_metrics(self, results: list) -> dict: eval_results = OrderedDict() labels = [res['label'] for res in results] scores = [res['scores'] for res in results] topk_acc = top_k_accuracy(scores, labels, self.topk) for k, acc in zip(self.topk, topk_acc): eval_results[f'topk{k}'] = acc return eval_resultsfrom mmaction.registry import METRICSmetric_cfg = dict(type='AccuracyMetric', topk=(1, 5))metric = METRICS.build(metric_cfg)data_samples = [d.to_dict() for d in predictions]metric.process(batched_packed_results, data_samples)acc = metric.compute_metrics(metric.results)print(acc)print('**************************步骤5:使用本地 PyTorch 训练和测试*****************************')import torch.optim as optimfrom mmengine import track_iter_progressfrom tqdm import tqdmdevice = 'cuda' # or 'cpu'max_epochs = 10optimizer = optim.Adam(model.parameters(), lr=0.01)for epoch in range(max_epochs): model.train() losses = [] task_num = len(train_data_loader) for data_batch in track_iter_progress((train_data_loader, task_num)): data = model.data_preprocessor(data_batch, training=True) loss_dict = model(**data, mode='loss') loss = loss_dict['loss_cls'] optimizer.zero_grad() loss.backward() optimizer.step() losses.append(loss.item()) print(f'Epoch[{epoch}]: loss ', sum(losses) / len(train_data_loader)) with torch.no_grad(): model.eval() task_num = len(val_data_loader) for data_batch in track_iter_progress((val_data_loader,task_num)): data = model.data_preprocessor(data_batch, training=False) predictions = model(**data, mode='predict') data_samples = [d.to_dict() for d in predictions] metric.process(data_batch, data_samples) acc = metric.acc = metric.compute_metrics(metric.results) for name, topk in acc.items(): print(f'{name}: ', topk)print('**************************步骤6:使用 MMEngine 训练和测试(推荐)*****************************')# from mmengine.runner import Runner## train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=10, val_interval=1)# val_cfg = dict(type='ValLoop')## optim_wrapper = dict(optimizer=dict(type='Adam', lr=0.01))## runner = Runner(model=model_cfg, work_dir='./work_dirs/guide',# train_dataloader=train_dataloader_cfg,# train_cfg=train_cfg,# val_dataloader=val_dataloader_cfg,# val_cfg=val_cfg,# optim_wrapper=optim_wrapper,# val_evaluator=[metric_cfg],# default_scope='mmaction')# runner.train()
用户指南
使用现有模型进行推理
MMAction2 提供了用于对给定视频进行推理的高级 Python API:
init_recognizer: 使用配置文件和权重文件初始化一个识别器
inference_recognizer: 对给定视频进行推理
下面是一个使用 Kinitics-400 预训练权重构建模型并对给定视频进行推理的示例
from mmaction.apis import inference_recognizer, init_recognizerconfig_path = 'configs/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x8-100e_kinetics400-rgb.py'checkpoint_path = 'https://download.openmmlab.com/mmaction/v1.0/recognition/tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x8-100e_kinetics400-rgb/tsn_imagenet-pretrained-r50_8xb32-1x1x8-100e_kinetics400-rgb_20220906-2692d16c.pth' # 可以是本地路径img_path = 'demo/demo.mp4' # 您可以指定自己的图片路径# 从配置文件和权重文件中构建模型model = init_recognizer(config_path, checkpoint_path, device="cpu") # device 可以是 'cuda:0'# 对单个视频进行测试result = inference_recognizer(model, img_path)# result 是一个包含 pred_scores 的字典,概率最高的在pred_scores中索引为6print(result)
学习配置文件
使用 Python 文件作为配置文件,可以在 $MMAction2/configs 目录下找到所有提供的配置文件,可以运行 python tools/analysis_tools/print_config.py /PATH/TO/CONFIG 来查看完整的配置文件。
通过脚本参数修改配置
在使用 tools/train.py 或 tools/test.py 提交作业时,您可以通过指定 --cfg-options 来原地修改配置。
- 更新字典的配置键。
可以按照原始配置中字典键的顺序来指定配置选项。 例如,--cfg-options model.backbone.norm_eval=False 将模型骨干中的所有 BN 模块更改为 train 模式。
- 更新配置列表中的键。
一些配置字典在配置文件中以列表形式组成。例如,训练流程 train_pipeline 通常是一个列表, 例如 [dict(type='SampleFrames'), ...]。如果您想要在流程中将 'SampleFrames' 更改为 'DenseSampleFrames', 您可以指定 --cfg-options train_pipeline.0.type=DenseSampleFrames。
- 更新列表/元组的值。
如果要更新的值是列表或元组。例如,配置文件通常设置 model.data_preprocessor.mean=[123.675, 116.28, 103.53]。如果您想要 更改此键,您可以指定 --cfg-options model.data_preprocessor.mean="[128,128,128]"。请注意,引号 ” 是支持列表/元组数据类型的必需内容。
配置文件结构
configs/base 下有 3 种基本组件类型,即 models、schedules 和 default_runtime。 许多方法只需要一个模型、一个训练计划和一个默认运行时组件就可以轻松构建,如 TSN、I3D、SlowOnly 等。 由 base 组件组成的配置文件被称为 primitive。
对于同一文件夹下的所有配置文件,建议只有一个 primitive 配置文件。其他所有配置文件都应该继承自 primitive 配置文件。这样,继承级别的最大值为 3。
为了方便理解,我们建议贡献者继承现有方法。 例如,如果基于 TSN 进行了一些修改,用户可以首先通过指定 base = ../tsn/tsn_imagenet-pretrained-r50_8xb32-1x1x3-100e_kinetics400-rgb.py 来继承基本的 TSN 结构,然后在配置文件中修改必要的字段。
如果您正在构建一个与任何现有方法的结构不共享的全新方法,可以在 configs/TASK 下创建一个文件夹
配置文件命名约定
配置文件名分为几个部分,不同部分逻辑上用下划线 '' 连接,同一部分的设置用破折号 '-' 连接{算法信息}{模块信息}{训练信息}{数据信息}.py{xxx} 是必填字段,[yyy] 是可选字段
{算法信息}:{模型}: 模型类型,例如 tsn、i3d、swin、vit 等。[模型设置]: 某些模型的特定设置,例如 base、p16、w877 等。
{模块信息}:[预训练信息]: 预训练信息,例如 kinetics400-pretrained、in1k-pre 等。{骨干网络}: 骨干网络类型,例如 r50(ResNet-50)等。[骨干网络设置]: 某些骨干网络的特定设置,例如 nl-dot-product、bnfrozen、nopool 等。
{训练信息}:{gpu x batch_per_gpu]}: GPU 和每个 GPU 上的样本数。{pipeline设置}: 帧采样设置,例如 dense、{clip_len}x{frame_interval}x{num_clips}、u48 等。{schedule}: 训练计划,例如 coslr-20e。
{数据信息}:{数据集}: 数据集名称,例如 kinetics400、mmit 等。{模态}: 数据模态,例如 rgb、flow、keypoint-2d 等。
# 模型设置model = dict( # 模型的配置 type='Recognizer2D', # 识别器的类名 backbone=dict( # 骨干网络的配置 type='ResNet', # 骨干网络的名称 pretrained='torchvision://resnet50', # 预训练模型的 URL/网站 depth=50, # ResNet 模型的深度 norm_eval=False), # 是否在训练时将 BN 层设置为评估模式 cls_head=dict( # 分类头的配置 type='TSNHead', # 分类头的名称 num_classes=400, # 要分类的类别数量。 in_channels=2048, # 分类头的输入通道数。 spatial_type='avg', # 空间维度池化的类型 consensus=dict(type='AvgConsensus', dim=1), # 一致性模块的配置 dropout_ratio=0.4, # dropout 层中的概率 init_std=0.01, # 线性层初始化的标准差值 average_clips='prob'), # 平均多个剪辑结果的方法 data_preprocessor=dict( # 数据预处理器的配置 type='ActionDataPreprocessor', # 数据预处理器的名称 mean=[123.675, 116.28, 103.53], # 不同通道的均值用于归一化 std=[58.395, 57.12, 57.375], # 不同通道的标准差用于归一化 format_shape='NCHW'), # 最终图像形状的格式 # 模型训练和测试设置 train_cfg=None, # TSN 的训练超参数的配置 test_cfg=None) # TSN 的测试超参数的配置# 数据集设置dataset_type = 'RawframeDataset' # 用于训练、验证和测试的数据集类型data_root = 'data/kinetics400/rawframes_train/' # 用于训练的数据的根路径data_root_val = 'data/kinetics400/rawframes_val/' # 用于验证和测试的数据的根路径ann_file_train = 'data/kinetics400/kinetics400_train_list_rawframes.txt' # 用于训练的注释文件的路径ann_file_val = 'data/kinetics400/kinetics400_val_list_rawframes.txt' # 用于验证的注释文件的路径ann_file_test = 'data/kinetics400/kinetics400_val_list_rawframes.txt' # 用于测试的注释文件的路径train_pipeline = [ # 训练数据处理流程 dict( # SampleFrames 的配置 type='SampleFrames', # 采样帧的流程,从视频中采样帧 clip_len=1, # 每个采样输出剪辑的帧数 frame_interval=1, # 相邻采样帧的时间间隔 num_clips=3), # 要采样的剪辑数 dict( # RawFrameDecode 的配置 type='RawFrameDecode'), # 加载和解码帧的流程,选择给定索引的原始帧 dict( # Resize 的配置 type='Resize', # 调整大小的流程 scale=(-1, 256)), # 要调整图像的比例 dict( # MultiScaleCrop 的配置 type='MultiScaleCrop', # 多尺度裁剪的流程,根据随机选择的尺度列表裁剪图像 input_size=224, # 网络的输入大小 scales=(1, 0.875, 0.75, 0.66), # 要选择的宽度和高度的尺度 random_crop=False, # 是否随机采样裁剪框 max_wh_scale_gap=1), # 宽度和高度尺度级别的最大差距 dict( # Resize 的配置 type='Resize', # 调整大小的流程 scale=(224, 224), # 要调整图像的比例 keep_ratio=False), # 是否保持纵横比进行调整大小 dict( # Flip 的配置 type='Flip', # 翻转的流程 flip_ratio=0.5), # 实施翻转的概率 dict( # FormatShape 的配置 type='FormatShape', # 格式化形状的流程,将最终图像形状格式化为给定的 input_format input_format='NCHW'), # 最终图像形状的格式 dict(type='PackActionInputs') # PackActionInputs 的配置]val_pipeline = [ # 验证数据处理流程 dict( # SampleFrames 的配置 type='SampleFrames', # 采样帧的流程,从视频中采样帧 clip_len=1, # 每个采样输出剪辑的帧数 frame_interval=1, # 相邻采样帧的时间间隔 num_clips=3, # 要采样的剪辑数 test_mode=True), # 是否在采样时设置为测试模式 dict( # RawFrameDecode 的配置 type='RawFrameDecode'), # 加载和解码帧的流程,选择给定索引的原始帧 dict( # Resize 的配置 type='Resize', # 调整大小的流程 scale=(-1, 256)), # 要调整图像的比例 dict( # CenterCrop 的配置 type='CenterCrop', # 中心裁剪的流程,从图像中裁剪中心区域 crop_size=224), # 要裁剪的图像大小 dict( # Flip 的配置 type='Flip', # 翻转的流程 flip_ratio=0), # 实施翻转的概率 dict( # FormatShape 的配置 type='FormatShape', # 格式化形状的流程,将最终图像形状格式化为给定的 input_format input_format='NCHW'), # 最终图像形状的格式 dict(type='PackActionInputs') # PackActionInputs 的配置]test_pipeline = [ # 测试数据处理流程 dict( # SampleFrames 的配置 type='SampleFrames', # 采样帧的流程,从视频中采样帧 clip_len=1, # 每个采样输出剪辑的帧数 frame_interval=1, # 相邻采样帧的时间间隔 num_clips=25, # 要采样的剪辑数 test_mode=True), # 是否在采样时设置为测试模式 dict( # RawFrameDecode 的配置 type='RawFrameDecode'), # 加载和解码帧的流程,选择给定索引的原始帧 dict( # Resize 的配置 type='Resize', # 调整大小的流程 scale=(-1, 256)), # 要调整图像的比例 dict( # TenCrop 的配置 type='TenCrop', # 十次裁剪的流程,从图像中裁剪十个区域 crop_size=224), # 要裁剪的图像大小 dict( # Flip 的配置 type='Flip', # 翻转的流程 flip_ratio=0), # 实施翻转的概率 dict( # FormatShape 的配置 type='FormatShape', # 格式化形状的流程,将最终图像形状格式化为给定的 input_format input_format='NCHW'), # 最终图像形状的格式 dict(type='PackActionInputs') # PackActionInputs 的配置]train_dataloader = dict( # 训练数据加载器的配置 batch_size=32, # 训练时每个单个 GPU 的批量大小 num_workers=8, # 训练时每个单个 GPU 的数据预取进程数 persistent_workers=True, # 如果为 `True`,则数据加载器在一个 epoch 结束后不会关闭工作进程,这可以加速训练速度 sampler=dict( type='DefaultSampler', # 支持分布式和非分布式训练的 DefaultSampler。参考 https://github.com/open-mmlab/mmengine/blob/main/mmengine/dataset/sampler.py shuffle=True), # 每个 epoch 随机打乱训练数据 dataset=dict( # 训练数据集的配置 type=dataset_type, ann_file=ann_file_train, # 注释文件的路径 data_prefix=dict(img=data_root), # 帧路径的前缀 pipeline=train_pipeline))val_dataloader = dict( # 验证数据加载器的配置 batch_size=1, # 验证时每个单个 GPU 的批量大小 num_workers=8, # 验证时每个单个 GPU 的数据预取进程数 persistent_workers=True, # 如果为 `True`,则数据加载器在一个 epoch 结束后不会关闭工作进程 sampler=dict( type='DefaultSampler', shuffle=False), # 验证和测试时不进行随机打乱 dataset=dict( # 验证数据集的配置 type=dataset_type, ann_file=ann_file_val, # 注释文件的路径 data_prefix=dict(img=data_root_val), # 帧路径的前缀 pipeline=val_pipeline, test_mode=True))test_dataloader = dict( # 测试数据加载器的配置 batch_size=32, # 测试时每个单个 GPU 的批量大小 num_workers=8, # 测试时每个单个 GPU 的数据预取进程数 persistent_workers=True, # 如果为 `True`,则数据加载器在一个 epoch 结束后不会关闭工作进程 sampler=dict( type='DefaultSampler', shuffle=False), # 验证和测试时不进行随机打乱 dataset=dict( # 测试数据集的配置 type=dataset_type, ann_file=ann_file_val, # 注释文件的路径 data_prefix=dict(img=data_root_val), # 帧路径的前缀 pipeline=test_pipeline, test_mode=True))# 评估设置val_evaluator = dict(type='AccMetric') # 验证评估器的配置test_evaluator = val_evaluator # 测试评估器的配置train_cfg = dict( # 训练循环的配置 type='EpochBasedTrainLoop', # 训练循环的名称 max_epochs=100, # 总的训练周期数 val_begin=1, # 开始验证的训练周期 val_interval=1) # 验证间隔val_cfg = dict( # 验证循环的配置 type='ValLoop') # 验证循环的名称test_cfg = dict( # 测试循环的配置 type='TestLoop') # 测试循环的名称# 学习策略param_scheduler = [ # 更新优化器参数的学习率测率,支持字典或列表 dict(type='MultiStepLR', # 达到一个里程碑时衰减学习率 begin=0, # 开始更新学习率的步骤 end=100, # 结束更新学习率的步骤 by_epoch=True, # 是否按 epoch 更新学习率 milestones=[40, 80], # 衰减学习率的步骤 gamma=0.1)] # 学习率衰减的乘法因子# 优化器optim_wrapper = dict( # 优化器包装器的配置 type='OptimWrapper', # 优化器包装器的名称,切换到 AmpOptimWrapper 可以启用混合精度训练 optimizer=dict( # 优化器的配置。支持 PyTorch 中的各种优化器。参考 https://pytorch.org/docs/stable/optim.html#algorithms type='SGD', # 优化器的名称 lr=0.01, # 学习率 momentum=0.9, # 动量因子 weight_decay=0.0001), # 权重衰减 clip_grad=dict(max_norm=40, norm_type=2)) # 梯度裁剪的配置# 运行时设置default_scope = 'mmaction' # 用于查找模块的默认注册表作用域。参考 https://mmengine.readthedocs.io/en/latest/tutorials/registry.htmldefault_hooks = dict( # 执行默认操作的钩子,如更新模型参数和保存权重。 runtime_info=dict(type='RuntimeInfoHook'), # 将运行时信息更新到消息中心的钩子 timer=dict(type='IterTimerHook'), # 用于记录迭代过程中花费的时间的日志记录器 logger=dict( type='LoggerHook', # 用于记录训练/验证/测试阶段的日志记录器 interval=20, # 打印日志的间隔 ignore_last=False), # 忽略每个 epoch 中最后几个迭代的日志 param_scheduler=dict(type='ParamSchedulerHook'), # 更新优化器中某些超参数的钩子 checkpoint=dict( type='CheckpointHook', # 定期保存权重的钩子 interval=3, # 保存的周期 save_best='auto', # 用于评估最佳权重的指标 max_keep_ckpts=3), # 保留的最大权重文件数量 sampler_seed=dict(type='DistSamplerSeedHook'), # 用于分布式训练的数据加载采样器 sync_buffers=dict(type='SyncBuffersHook')) # 在每个 epoch 结束时同步模型缓冲区env_cfg = dict( # 设置环境的字典 cudnn_benchmark=False, # 是否启用 cudnn benchmark mp_cfg=dict(mp_start_method='fork', opencv_num_threads=0), # 设置多进程的参数 dist_cfg=dict(backend='nccl')) # 设置分布式环境的参数,也可以设置端口号log_processor = dict( type='LogProcessor', # 用于格式化日志信息的日志处理器 window_size=20, # 默认的平滑间隔 by_epoch=True) # 是否使用 epoch 类型格式化日志vis_backends = [ # 可视化后端的列表 dict(type='LocalVisBackend')] # 本地可视化后端visualizer = dict( # 可视化器的配置 type='ActionVisualizer', # 可视化器的名称 vis_backends=vis_backends)log_level = 'INFO' # 日志记录的级别load_from = None # 从给定路径加载模型权重作为预训练模型。这不会恢复训练。resume = False # 是否从 `load_from` 中定义的权重恢复。如果 `load_from` 为 None,则会从 `work_dir` 中恢复最新的权重。
数据集支持
使用CLI下载
使用CLI下载
如果openxlab login出错,就使用openxlab config,.openxlab\config中键入ak和sk
下载整个数据集
openxlab dataset get --dataset-repo username/repo-name --target-path /path/to/local/folderopenxlab dataset get -r username/repo-name -t /path/to/local/folder
下载数据集的某一个文件
openxlab dataset download --dataset-repo username/repo-name --source-path /train/file --target-path /path/to/local/folderopenxlab dataset download -r username/repo-name -s /train/file -t /path/to/local/folder
其中:
参数 | 缩写 | 是否必填 | 参数类型 | 参数说明 | 示例 |
---|---|---|---|---|---|
dataset-repo | -r | 是 | String | 数据集仓库的地址,由 username/repo_name 组成 | zhangsan/repo-name |
source-path | -s | 是 | String | 对应数据集仓库下文件的相对路径 | -s /train/file |
target-path | -t | 否 | String | 下载仓库指定的本地路径 | --target-path /path/to/local/folder |
使用SDK下载
下载整个数据集
from openxlab.dataset import getget(dataset_repo='username/repo_name', target_path='/path/to/local/folder')
下载数据集的某一个文件
from openxlab.dataset import downloaddownload(dataset_repo='username/repo_name', source_path='/train/file', target_path='/path/to/local/folder')
其中:
参数 | 缩写 | 是否必填 | 参数类型 | 参数说明 | 示例 |
---|---|---|---|---|---|
dataset | _repo-r | 是 | String | 数据集仓库的地址,由 username/repo_name 组成 | zhangsan/repo-name |
source_path | -s | 是 | String | 对应数据集仓库下文件的相对路径 | -s /path/to/local/folder |
target_path | -t | 否 | String | 下载仓库指定的本地路径 | --target-path /path/to/local/folder |