Transformers 视频分类实战:用 VideoMAE 在 UCF101 子集上微调与推理
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
导读
视频分类(Video Classification)是给整段视频分配一个类别标签的多模态视觉任务,其典型落地场景包括健身应用中的动作识别、通勤场景下的行为理解等。本指南以 docs/source/ja/tasks/video_classification.md 为核心骨架,结合 🤗 Transformers 仓库中的 VideoMAE 模型源码、视频分类 Pipeline 与测试用例,系统讲解如何基于pytorchvideo完成视频的采样与增强、加载 UCF101 子集、用Trainer微调VideoMAEForVideoClassification,以及用pipeline和手动前向传播两条路径做推理。读完本文,你将掌握一条从原始.avi视频到可复用的视频分类模型的完整实战链路。
1. 任务背景:视频分类是什么
视频分类的目标是:输入一段视频,模型预测它属于预定义类别集合中的哪一类。与逐帧图像分类不同,视频分类必须同时建模空间信息(画面内容)与时间信息(帧间运动)。视频分类模型在 🤗 Transformers 中被建模为一个标准的序列分类结构:预训练的视频编码器(如 VideoMAE)负责抽取时空特征,顶端接一个随机初始化的分类头。
从仓库源码看,这一任务在框架内部有完整支持:
- 模型侧,
VideoMAEForVideoClassification定义于 modeling_videomae.py,由VideoMAEModel编码器与线性分类头组成; - 推理侧,
VideoClassificationPipeline定义于 video_classification.py,支持通过任务标识符"video-classification"加载,注册于 pipelines/init.py; - 测试侧,test_modeling_videomae.py 提供了
test_inference_for_video_classification慢测试,验证了 Kinetics-400 微调检查点输出 logits 形状为(1, 400)且数值与期望一致,可作为本项目视频分类功能的实证锚点。
本文采用的模型是VideoMAE,论文与仓库实现均以MCG-NJU/videomae-base为基准检查点(见 configuration_videomae.py 的@auto_docstring(checkpoint="MCG-NJU/videomae-base")装饰器)。其配置默认值:image_size=224、patch_size=16、num_frames=16、tubelet_size=2、hidden_size=768、12 层 Transformer、use_mean_pooling=True(参见 configuration_videomae.py)。
2. 环境准备
开始前先安装依赖:
pip install -q pytorchvideo transformers evaluatepytorchvideo:负责视频的读取、帧采样、裁剪与增强等预处理工作;transformers:提供模型架构、Trainer训练框架与推理pipeline;evaluate:加载精度评估指标。
此外,由于VideoClassificationPipeline底层依赖 PyAV 进行视频解码(见 video_classification.py 中的requires_backends(self, "av")),执行推理前建议一并安装:
pip install av若计划把模型上传到 Hub 与社区共享,建议先登录 Hugging Face 账号:
>>> from huggingface_hub import notebook_login >>> notebook_login()3. 加载 UCF101 子集数据集
3.1 下载并解压
先下载 UCF-101 数据集的一个子集(10 类、每类训练 30 个视频),以便在投入完整数据集训练前快速验证流程可行性:
>>> from huggingface_hub import hf_hub_download >>> hf_dataset_identifier = "sayakpaul/ucf101-subset" >>> filename = "UCF101_subset.tar.gz" >>> file_path = hf_hub_download(repo_id=hf_dataset_identifier, filename=filename, repo_type="dataset")解压压缩包:
>>> import tarfile >>> with tarfile.open(file_path) as t: ... t.extractall(".")解压后数据集目录结构如下:
UCF101_subset/ train/ BandMarching/ video_1.mp4 video_2.mp4 ... Archery/ video_1.mp4 ... val/ BandMarching/ ... Archery/ ... test/ BandMarching/ ... Archery/ ...3.2 理解数据集结构:分组与数据泄漏
排序后的视频路径形如:
... 'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g07_c04.avi', 'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g07_c06.avi', 'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g08_c01.avi', 'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g09_c02.avi', 'UCF101_subset/train/ApplyEyeMakeup/v_ApplyEyeMakeup_g09_c06.avi' ...注意文件命名中的g表示分组(group)/场景:同一组内的视频剪辑属于同一拍摄场景。为预防数据泄漏(同场景剪辑同时出现在训练与验证集会导致指标虚高),验证与测试划分不应包含同组的剪辑。本教程所用的sayakpaul/ucf101-subset已经考虑了这一点。
3.3 构造标签映射
从视频路径中提取全部类别名,并构建两份字典:
label2id:类别名 → 整数;id2label:整数 → 类别名。
>>> class_labels = sorted({str(path).split("/")[2] for path in all_video_file_paths}) >>> label2id = {label: i for i, label in enumerate(class_labels)} >>> id2label = {i: label for label, i in label2id.items()} >>> print(f"Unique classes: {list(label2id.keys())}.") # Unique classes: ['ApplyEyeMakeup', 'ApplyLipstick', 'Archery', 'BabyCrawling', 'BalanceBeam', 'BandMarching', 'BaseballPitch', 'Basketball', 'BasketballDunk', 'BenchPress'].该子集共 10 个类别,每类训练视频 30 个。
4. 加载待微调模型
从预训练检查点实例化视频分类模型及其配套的图像处理器。编码器保留预训练参数,分类头随机初始化:
>>> from transformers import VideoMAEImageProcessor, VideoMAEForVideoClassification >>> model_ckpt = "MCG-NJU/videomae-base" >>> image_processor = VideoMAEImageProcessor.from_pretrained(model_ckpt) >>> model = VideoMAEForVideoClassification.from_pretrained( ... model_ckpt, ... label2id=label2id, ... id2label=id2label, ... ignore_mismatched_sizes=True, # 若计划微调一个已微调过的检查点,请加上该参数 ... )加载时可能出现如下警告:
Some weights of the model checkpoint at MCG-NJU/videomae-base were not used when initializing VideoMAEForVideoClassification: [..., 'decoder.decoder_layers.1.attention.output.dense.bias', ...] - This IS expected if you are initializing VideoMAEForVideoClassification from the checkpoint of a model trained on another task or with another architecture (...) Some weights of VideoMAEForVideoClassification were not initialized from the model checkpoint at MCG-NJU/videomae-base and are newly initialized: ['classifier.bias', 'classifier.weight'] You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.这属于预期行为:原检查点中的解码器(用于掩码自编码预训练)权重被丢弃,新的classifier层随机初始化,因此必须在推理前微调。从源码看,这正是VideoMAEForVideoClassification.__init__中self.classifier = nn.Linear(config.hidden_size, config.num_labels)新建线性层所致(modeling_videomae.py)。
提示:若追求更优的下游精度,可改用MCG-NJU/videomae-base-finetuned-kinetics(已在 Kinetics-400 上微调、与下游任务域高度重合)作为起点,sayakpaul/videomae-base-finetuned-kinetics-finetuned-ucf101-subset即是这样得到的二次微调检查点。
5. 构建数据集预处理管线
视频预处理完全依赖 PyTorchVideo。先导入依赖:
>>> import pytorchvideo.data >>> from pytorchvideo.transforms import ( ... ApplyTransformToKey, ... Normalize, ... RandomShortSideScale, ... RemoveKey, ... ShortSideScale, ... UniformTemporalSubsample, ... ) >>> from torchvision.transforms import ( ... Compose, ... Lambda, ... RandomCrop, ... RandomHorizontalFlip, ... Resize, ... )5.1 从图像处理器推导预处理常量
利用与预训练模型关联的image_processor获取两项关键信息:
- 归一化均值/标准差:帧像素归一化所用;
- 空间分辨率:帧缩放的目标尺寸。
>>> mean = image_processor.image_mean >>> std = image_processor.image_std >>> if "shortest_edge" in image_processor.size: ... height = width = image_processor.size["shortest_edge"] >>> else: ... height = image_processor.size["height"] ... width = image_processor.size["width"] >>> resize_to = (height, width) >>> num_frames_to_sample = model.config.num_frames >>> sample_rate = 4 >>> fps = 30 >>> clip_duration = num_frames_to_sample * sample_rate / fps这里image_processor来自VideoMAEImageProcessor(定义于 image_processing_videomae.py),其类级默认值为image_mean = IMAGENET_STANDARD_MEAN、image_std = IMAGENET_STANDARD_STD、size = {"shortest_edge": 224}、crop_size = {"height": 224, "width": 224}、do_resize = True、do_center_crop = True、do_normalize = True。因此本教程中resize_to = (224, 224)。
num_frames从model.config.num_frames读取(VideoMAE 默认 16,见 configuration_videomae.py)。按sample_rate=4、fps=30计算,clip_duration = 16 * 4 / 30 ≈ 2.13秒。
5.2 训练集变换与数据集
训练变换组合了均匀时间子采样、像素归一化、随机短边缩放、随机裁剪、随机水平翻转:
>>> train_transform = Compose( ... [ ... ApplyTransformToKey( ... key="video", ... transform=Compose( ... [ ... UniformTemporalSubsample(num_frames_to_sample), ... Lambda(lambda x: x / 255.0), ... Normalize(mean, std), ... RandomShortSideScale(min_size=256, max_size=320), ... RandomCrop(resize_to), ... RandomHorizontalFlip(p=0.5), ... ] ... ), ... ), ... ] ... ) >>> train_dataset = pytorchvideo.data.Ucf101( ... data_path=os.path.join(dataset_root_path, "train"), ... clip_sampler=pytorchvideo.data.make_clip_sampler("random", clip_duration), ... decode_audio=False, ... transform=train_transform, ... )5.3 验证集与测试集变换
验证/测试变换与训练保持一致,但去掉随机裁剪与水平翻转(保证评估确定性):
>>> val_transform = Compose( ... [ ... ApplyTransformToKey( ... key="video", ... transform=Compose( ... [ ... UniformTemporalSubsample(num_frames_to_sample), ... Lambda(lambda x: x / 255.0), ... Normalize(mean, std), ... Resize(resize_to), ... ] ... ), ... ), ... ] ... ) >>> val_dataset = pytorchvideo.data.Ucf101( ... data_path=os.path.join(dataset_root_path, "val"), ... clip_sampler=pytorchvideo.data.make_clip_sampler("uniform", clip_duration), ... decode_audio=False, ... transform=val_transform, ... ) >>> test_dataset = pytorchvideo.data.Ucf101( ... data_path=os.path.join(dataset_root_path, "test"), ... clip_sampler=pytorchvideo.data.make_clip_sampler("uniform", clip_duration), ... decode_audio=False, ... transform=val_transform, ... )训练集使用"random"剪辑采样器,验证/测试集使用"uniform"采样器,这是常见的训练-评估策略差异。
关于自定义数据集:pytorchvideo.data.Ucf101()内部返回LabeledVideoDataset对象,它是 PyTorchVideo 中所有视频数据集的基类。如果你的数据目录结构与上面展示的一致,直接使用Ucf101()即可;若要接入其他格式的数据,可以继承并扩展LabeledVideoDataset。
统计视频数量:
>>> print(train_dataset.num_videos, val_dataset.num_videos, test_dataset.num_videos) # (300, 30, 75)6. 可视化预处理后的视频
调试时可将预处理后的视频张量转成 GIF 直观检查采样与增强效果:
>>> import imageio >>> import numpy as np >>> from IPython.display import Image >>> def unnormalize_img(img): ... """Un-normalizes the image pixels.""" ... img = (img * std) + mean ... img = (img * 255).astype("uint8") ... return img.clip(0, 255) >>> def create_gif(video_tensor, filename="sample.gif"): ... """Prepares a GIF from a video tensor. ... ... The video tensor is expected to have the following shape: ... (num_frames, num_channels, height, width). ... """ ... frames = [] ... for video_frame in video_tensor: ... frame_unnormalized = unnormalize_img(video_frame.permute(1, 2, 0).numpy()) ... frames.append(frame_unnormalized) ... kargs = {"duration": 0.25} ... imageio.mimsave(filename, frames, "GIF", **kargs) ... return filename >>> def display_gif(video_tensor, gif_name="sample.gif"): ... """Prepares and displays a GIF from a video tensor.""" ... video_tensor = video_tensor.permute(1, 0, 2, 3) ... gif_filename = create_gif(video_tensor, gif_name) ... return Image(filename=gif_filename) >>> sample_video = next(iter(train_dataset)) >>> video_tensor = sample_video["video"] >>> display_gif(video_tensor)注意张量维度约定:数据集输出的video张量形状为(通道数, 帧数, 高, 宽),而可视化与后续批处理需要(帧数, 通道数, 高, 宽),因此都要先permute(1, 0, 2, 3)。
7. 用 Trainer 微调模型
7.1 配置 TrainingArguments
使用 🤗 Transformers 的Trainer进行微调。TrainingArguments中最关键的一个参数是remove_unused_columns=False:
- 默认
True时,Trainer 会删除模型调用函数未使用的特征列,便于解包输入; - 但本任务中,
pixel_values(模型输入的必需键)正是由未使用的video特征在collate_fn中构造的,因此必须显式关闭该默认行为。
>>> from transformers import TrainingArguments, Trainer >>> model_name = model_ckpt.split("/")[-1] >>> new_model_name = f"{model_name}-finetuned-ucf101-subset" >>> num_epochs = 4 >>> args = TrainingArguments( ... new_model_name, ... remove_unused_columns=False, ... eval_strategy="epoch", ... save_strategy="epoch", ... learning_rate=5e-5, ... per_device_train_batch_size=batch_size, ... per_device_eval_batch_size=batch_size, ... warmup_steps=0.1, ... logging_steps=10, ... load_best_model_at_end=True, ... metric_for_best_model="accuracy", ... push_to_hub=True, ... max_steps=(train_dataset.num_videos // batch_size) * num_epochs, ... )为什么必须设置max_steps:pytorchvideo.data.Ucf101()返回的数据集没有实现__len__方法,Trainer 无法通过len(dataset)推算训练步数,因此需手动给出max_steps = (训练视频数 // batch_size) * 轮数。
7.2 定义评估指标与 collate_fn
用evaluate加载 accuracy 指标,只需对预测 logits 取 argmax 即可:
import evaluate metric = evaluate.load("accuracy") def compute_metrics(eval_pred): predictions = np.argmax(eval_pred.predictions, axis=1) return metric.compute(predictions=predictions, references=eval_pred.label_ids)评估策略说明:VideoMAE 原论文采用的是“多剪辑 + 多裁剪”的测试时增强评估,即从测试视频采样多个剪辑并施加不同裁剪,汇总报告最终分数。本教程为保持简洁,不采用该策略,直接对单剪辑做评估。
随后定义批处理用的collate_fn,每个 batch 由pixel_values与labels两个键组成:
>>> def collate_fn(examples): ... # permute to (num_frames, num_channels, height, width) ... pixel_values = torch.stack( ... [example["video"].permute(1, 0, 2, 3) for example in examples] ... ) ... labels = torch.tensor([example["label"] for example in examples]) ... return {"pixel_values": pixel_values, "labels": labels}7.3 实例化 Trainer 并训练
>>> trainer = Trainer( ... model, ... args, ... train_dataset=train_dataset, ... eval_dataset=val_dataset, ... processing_class=image_processor, ... compute_metrics=compute_metrics, ... data_collator=collate_fn, ... )为什么传image_processor给processing_class:数据已经手工预处理完毕,传入处理器并非为了再次预处理,而是让图像处理器的配置文件(JSON)随模型一并上传到 Hub 仓库,方便他人直接复现推理。从 Trainer 源码看,processing_class同时用于在未提供data_collator时自动构造DataCollatorWithPadding(见 trainer.py),本教程显式传入data_collator后,处理器的职责即以上传配置为主。
启动微调:
>>> train_results = trainer.train()训练完成后将模型推送到 Hub 共享:
>>> trainer.push_to_hub()8. 推理:两种使用方式
8.1 方式一:pipeline(推荐)
从测试集取一个样本视频,然后用微调后的模型实例化VideoClassificationPipeline:
>>> sample_test_video = next(iter(test_dataset))>>> from transformers import pipeline >>> video_cls = pipeline(model="my_awesome_video_cls_model") >>> video_cls("https://huggingface.co/datasets/sayakpaul/ucf101-subset/resolve/main/v_BasketballDunk_g14_c06.avi") [{'score': 0.9272987842559814, 'label': 'BasketballDunk'}, {'score': 0.017777055501937866, 'label': 'BabyCrawling'}, {'score': 0.01663011871278286, 'label': 'BalanceBeam'}, {'score': 0.009560945443809032, 'label': 'BandMarching'}, {'score': 0.0068979403004050255, 'label': 'BaseballPitch'}]从源码看,VideoClassificationPipeline支持以下关键参数(video_classification.py):
| 参数 | 默认值 | 说明 |
|---|---|---|
top_k | 5 | 返回得分最高的标签个数,超过模型标签总数时自动取num_labels |
num_frames | model.config.num_frames | 从视频中采样的帧数 |
frame_sampling_rate | 1 | 帧采样间隔,如 2 表示每两帧取一帧 |
function_to_apply | "softmax" | 对输出施加的变换,可选["softmax", "sigmoid", "none"] |
输入支持单个/批量视频,形式可以是 HTTP 链接或本地路径,但同一批输入必须格式一致。Pipeline 内部使用 PyAV 解码视频,并按np.linspace均匀采样num_frames帧,再交给图像处理器或视频处理器生成pixel_values(video_classification.py)。输出为按分数降序排列的{label, score}字典列表,分数默认经 softmax 归一化。
8.2 方式二:手动前向传播
如需更细粒度的控制(例如批量推理、嵌入特征导出),可手动复现 pipeline 流程:
>>> def run_inference(model, video): ... # (num_frames, num_channels, height, width) ... perumuted_sample_test_video = video.permute(1, 0, 2, 3) ... inputs = { ... "pixel_values": perumuted_sample_test_video.unsqueeze(0), ... "labels": torch.tensor( ... [sample_test_video["label"]] ... ), # 没有标签时可省略该键 ... } ... device = torch.device("cuda" if torch.cuda.is_available() else "cpu") ... inputs = {k: v.to(device) for k, v in inputs.items()} ... model = model.to(device) ... # forward pass ... with torch.no_grad(): ... outputs = model(**inputs) ... logits = outputs.logits ... return logits传入视频并得到 logits:
>>> logits = run_inference(trained_model, sample_test_video["video"])解码预测类别:
>>> predicted_class_idx = logits.argmax(-1).item() >>> print("Predicted class:", model.config.id2label[predicted_class_idx]) # Predicted class: BasketballDunk注意:这里手动推理的输入是已经过预处理(归一化、裁剪)的视频张量;若要从原始视频文件直接推理,应先走 5.2/5.3 节的数据变换管线。模型前向内部流程可参考VideoMAEForVideoClassification.forward(modeling_videomae.py):编码器输出last_hidden_state后,若use_mean_pooling=True则对序列做均值池化并经fc_norm(LayerNorm),否则取[CLS]位置向量,最后过classifier线性层得到 logits。
9. 关键要点回顾
- 任务本质:视频分类是同时建模空间与时间信息的序列分类任务,VideoMAE 在 🤗 Transformers 中由 VideoMAEForVideoClassification 实现,配置默认
num_frames=16、tubelet_size=2、use_mean_pooling=True(configuration_videomae.py)。 - 数据防泄漏:UCF101 按场景分组(文件名的
g标记),训练/验证/测试划分必须隔离同组剪辑。 - 预处理:训练用“均匀时间子采样 + 归一化 + 随机短边缩放 + 随机裁剪 + 随机水平翻转”,验证/测试去掉随机裁剪与翻转;分辨率与归一化统计量统一取自
VideoMAEImageProcessor(shortest_edge=224、ImageNet 均值/方差,见 image_processing_videomae.py)。 - Trainer 关键参数:必须设
remove_unused_columns=False(pixel_values依赖原始video特征),且因数据集无__len__必须手动指定max_steps;processing_class传image_processor仅为了随模型上传处理器配置。 - 推理双路径:
pipeline支持 URL/本地路径输入与top_k、num_frames、frame_sampling_rate、function_to_apply等参数;手动前向则需自行完成帧采样与预处理。 - 验证锚点:仓库慢测试 test_inference_for_video_classification 确认
MCG-NJU/videomae-base-finetuned-kinetics对单视频输出 logits 形状为(1, 400)且与期望数值一致,可作为自训模型输出结构的对照参考。
【免费下载链接】transformers🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.项目地址: https://gitcode.com/GitHub_Trending/tra/transformers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考