视频分类
视频分类是指为整个视频分配标签或类别的任务。视频预期每个视频只有一个类别。视频分类模型以视频作为输入,并返回关于视频属于哪个类别的预测。这些模型可用于对视频的全部内容进行分类。视频分类的一个现实应用是动作/活动识别,这对于健身应用很有用。对于视障人士,尤其是在通勤时,它也很有帮助。
本指南将向您展示如何
要查看与此任务兼容的所有架构和检查点,我们建议您查看 任务页面。
在开始之前,请确保已安装所有必要的库
pip install -q pytorchvideo transformers evaluate
您将使用 PyTorchVideo(称为 pytorchvideo
)来处理和准备视频。
我们鼓励您登录您的 Hugging Face 帐户,以便您可以上传模型并与社区共享。出现提示时,输入您的令牌以登录
>>> from huggingface_hub import notebook_login
>>> notebook_login()
加载 UCF101 数据集
首先加载 UCF-101 数据集 的一个子集。这将使您有机会进行实验并确保一切正常,然后再花费更多时间在完整数据集上进行训练。
>>> 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
video_2.mp4
...
...
val/
BandMarching/
video_1.mp4
video_2.mp4
...
Archery
video_1.mp4
video_2.mp4
...
...
test/
BandMarching/
video_1.mp4
video_2.mp4
...
Archery
video_1.mp4
video_2.mp4
...
...
然后,您可以计算视频的总数。
>>> import pathlib
>>> dataset_root_path = "UCF101_subset"
>>> dataset_root_path = pathlib.Path(dataset_root_path)
>>> video_count_train = len(list(dataset_root_path.glob("train/*/*.avi")))
>>> video_count_val = len(list(dataset_root_path.glob("val/*/*.avi")))
>>> video_count_test = len(list(dataset_root_path.glob("test/*/*.avi")))
>>> video_total = video_count_train + video_count_val + video_count_test
>>> print(f"Total videos: {video_total}")
>>> all_video_file_paths = (
... list(dataset_root_path.glob("train/*/*.avi"))
... + list(dataset_root_path.glob("val/*/*.avi"))
... + list(dataset_root_path.glob("test/*/*.avi"))
... )
>>> all_video_file_paths[:5]
(排序
)视频路径如下所示
...
'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
表示。例如,v_ApplyEyeMakeup_g07_c04.avi
和 v_ApplyEyeMakeup_g07_c06.avi
。
对于验证和评估拆分,您不希望具有来自同一组/场景的视频剪辑,以防止 数据泄漏。本教程中使用的子集考虑了此信息。
接下来,您将派生数据集中存在的标签集。此外,创建两个在初始化模型时有帮助的字典
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 个视频。
加载要微调的模型
从预训练的检查点及其关联的图像处理器实例化一个视频分类模型。模型的编码器带有预训练的参数,分类头是随机初始化的。图像处理器在为我们的数据集编写预处理管道时会派上用场。
>>> 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, # provide this in case you're planning to fine-tune an already fine-tuned checkpoint
... )
在加载模型时,您可能会注意到以下警告
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', 'decoder.decoder_layers.2.attention.attention.key.weight']
- This IS expected if you are initializing VideoMAEForVideoClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
- This IS NOT expected if you are initializing VideoMAEForVideoClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
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
层的权重和偏差)并随机初始化其他一些权重(新 classifier
层的权重和偏差)。在这种情况下,这是预期的,因为我们正在添加一个我们没有预训练权重的新头,因此库警告我们应该在将此模型用于推理之前对其进行微调,这正是我们将要做的。
**注意**,此检查点 导致在此任务上获得更好的性能,因为该检查点是通过在具有相当领域重叠的类似下游任务上进行微调而获得的。您可以查看 此检查点,它是通过微调 MCG-NJU/videomae-base-finetuned-kinetics
获得的。
准备用于训练的数据集
为了预处理视频,您将利用 PyTorchVideo 库。首先导入我们需要的依赖项。
>>> import pytorchvideo.data
>>> from pytorchvideo.transforms import (
... ApplyTransformToKey,
... Normalize,
... RandomShortSideScale,
... RemoveKey,
... ShortSideScale,
... UniformTemporalSubsample,
... )
>>> from torchvision.transforms import (
... Compose,
... Lambda,
... RandomCrop,
... RandomHorizontalFlip,
... Resize,
... )
对于训练数据集转换,使用均匀时间子采样、像素归一化、随机裁剪和随机水平翻转的组合。对于验证和评估数据集转换,保留相同的转换链,除了随机裁剪和水平翻转。要了解有关这些转换详细信息的更多信息,请查看 PyTorchVideo 的官方文档。
使用与预训练模型关联的 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
现在,分别定义数据集特定的转换和数据集。从训练集开始
>>> 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,
... )
相同的流程序列可以应用于验证和评估集
>>> 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,
... )
**注意**:以上数据集管道取自 PyTorchVideo 官方示例。我们使用 pytorchvideo.data.Ucf101()
函数,因为它适合 UCF-101 数据集。在后台,它返回一个 pytorchvideo.data.labeled_video_dataset.LabeledVideoDataset
对象。LabeledVideoDataset
类是 PyTorchVideo 数据集中所有视频的基础类。因此,如果您想使用 PyTorchVideo 未直接支持的自定义数据集,则可以相应地扩展 LabeledVideoDataset
类。参考 data
API 文档以了解更多信息。此外,如果您的数据集遵循类似的结构(如上所示),则使用 pytorchvideo.data.Ucf101()
应该可以正常工作。
您可以访问 num_videos
参数以了解数据集中视频的数量。
>>> print(train_dataset.num_videos, val_dataset.num_videos, test_dataset.num_videos)
# (300, 30, 75)
可视化预处理后的视频以进行更好的调试
>>> 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)
训练模型
利用 🤗 Transformers 中的 Trainer
来训练模型。要实例化一个 Trainer
,您需要定义训练配置和评估指标。最重要的部分是 TrainingArguments
,这是一个包含所有配置训练属性的类。它需要一个输出文件夹名称,该名称将用于保存模型的检查点。它还有助于在 🤗 Hub 上的模型存储库中同步所有信息。
大多数训练参数是不言自明的,但这里一个非常重要的参数是 remove_unused_columns=False
。此参数将删除模型调用函数未使用任何特征。默认情况下,它是 True
,因为通常删除未使用的特征列是理想的,这使得将输入解包到模型的调用函数中变得更容易。但是,在这种情况下,您需要未使用的特征(特别是“video”),以便创建 pixel_values
(这是模型在其输入中期望的必填键)。
>>> 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_ratio=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,
... )
pytorchvideo.data.Ucf101()
返回的数据集没有实现 __len__
方法。因此,在实例化 TrainingArguments
时,我们必须定义 max_steps
。
接下来,您需要定义一个函数来根据预测计算指标,该函数将使用您现在要加载的 metric
。您需要做的唯一预处理是获取预测 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
,它将用于将示例批处理在一起。每个批次包含 2 个键,即 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}
然后,您只需将所有这些内容与数据集一起传递给 Trainer
>>> trainer = Trainer(
... model,
... args,
... train_dataset=train_dataset,
... eval_dataset=val_dataset,
... tokenizer=image_processor,
... compute_metrics=compute_metrics,
... data_collator=collate_fn,
... )
您可能想知道为什么在已经预处理数据的情况下,您将 image_processor
作为标记器传递。这仅仅是为了确保图像处理程序配置文件(存储为 JSON)也将上传到 Hub 上的存储库。
现在通过调用 train
方法来微调我们的模型
>>> train_results = trainer.train()
训练完成后,使用 push_to_hub() 方法将您的模型共享到 Hub,以便每个人都可以使用您的模型
>>> trainer.push_to_hub()
推理
太好了,现在您已经微调了一个模型,您可以将其用于推理!
加载视频进行推理
>>> sample_test_video = next(iter(test_dataset))
尝试使用微调后的模型进行推理的最简单方法是在 pipeline
中使用它。使用您的模型实例化一个用于视频分类的 pipeline
,并将您的视频传递给它
>>> 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'}]
如果您愿意,也可以手动复制 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"]]
... ), # this can be skipped if you don't have labels available.
... }
... 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"])
解码 logits
,我们得到
>>> predicted_class_idx = logits.argmax(-1).item()
>>> print("Predicted class:", model.config.id2label[predicted_class_idx])
# Predicted class: BasketballDunk