# 视频分类

视频分类是指为整个视频分配一个标签或类别的任务。每个视频预期仅对应一个类别。视频分类模型以视频作为输入，并返回该视频所属类别的预测结果。这些模型可用于对视频内容进行归类。视频分类的一个实际应用是动作/活动识别，这在健身应用中非常有用。它对视障人士也很有帮助，尤其是在他们通勤时。

本指南将向您展示如何：

1. 在 [UCF101](https://www.crcv.ucf.edu/data/UCF101.php) 数据集的子集上微调 [VideoMAE](../model_doc/videomae)。
2. 使用您微调后的模型进行推理。

要查看与此任务兼容的所有架构和检查点，我们建议查阅[任务页面](https://huggingface.co/tasks/video-classification)。

在开始之前，请确保已安装所有必要的库：

```bash
pip install -q pytorchvideo transformers evaluate
```

您将使用 [PyTorchVideo](https://pytorchvideo.org/)（即 `pytorchvideo`）来处理并准备视频。

我们建议您登录您的 Hugging Face 账户，以便上传并与社区共享您的模型。当系统提示时，请输入您的令牌以完成登录：

```py
>>> from huggingface_hub import notebook_login

>>> notebook_login()
```

## 加载 UCF101 数据集

首先加载 [UCF-101 数据集](https://www.crcv.ucf.edu/data/UCF101.php) 的一个子集。这样你可以先进行实验，确保一切正常运行，然后再花更多时间在完整数据集上进行训练。

```py
>>> 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")
```

子集下载完成后，您需要解压压缩文件：

```py
>>> import tarfile

>>> with tarfile.open(file_path) as t:
...      t.extractall(".")
```

从宏观层面来看，数据集的组织结构如下：

```bash
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
            ...
        ...
```

然后，您可以统计视频的总数。

```py
>>> import pathlib
>>> dataset_root_path = "UCF101_subset"
>>> dataset_root_path = pathlib.Path(dataset_root_path)
```

```py
>>> 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}")
```

```py
>>> 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]
```

The (`sorted`) 视频路径如下所示：

```bash
...
'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`。

对于验证集和评估集，你不应包含来自同一组/场景的视频片段，以防止[数据泄露](https://www.kaggle.com/code/alexisbcook/data-leakage)。本教程中使用的子集已考虑了这些信息。

接下来，您将推导出数据集中存在的标签集合。此外，创建两个字典，以便在初始化模型时提供帮助：

* `label2id`：将类别名称映射为整数。
* `id2label`：将整数映射到类别名称。

```py
>>> 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 个视频。

## 加载用于微调的模型

从预训练检查点及其关联的图像处理器实例化一个视频分类模型。该模型的编码器带有预训练参数，而分类头则随机初始化。在编写数据集的预处理流水线时，图像处理器将派上用场。

```py
>>> 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
... )
```

在模型加载期间，您可能会注意到以下警告：

```bash
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` 层的权重和偏置）。在这种情况下这是预期行为，因为我们正在添加一个新的输出头，而该输出头没有预训练权重，因此库会提醒我们，在将该模型用于推理之前应先对其进行微调，而这正是我们接下来要做的事情。

**注意**，[此检查点](https://huggingface.co/MCG-NJU/videomae-base-finetuned-kinetics)在该任务上表现更佳，因为该检查点是通过在具有相当领域重叠的类似下游任务上进行微调获得的。您可以查看通过微调 `MCG-NJU/videomae-base-finetuned-kinetics` 获得的 [此检查点](https://huggingface.co/sayakpaul/videomae-base-finetuned-kinetics-finetuned-ucf101-subset)。

## 准备用于训练的数据集

在预处理视频时，你将使用 [PyTorchVideo 库](https://pytorchvideo.org/)。首先，导入所需的依赖项。

```py
>>> import pytorchvideo.data

>>> from pytorchvideo.transforms import (
...     ApplyTransformToKey,
...     Normalize,
...     RandomShortSideScale,
...     RemoveKey,
...     ShortSideScale,
...     UniformTemporalSubsample,
... )

>>> from torchvision.transforms import (
...     Compose,
...     Lambda,
...     RandomCrop,
...     RandomHorizontalFlip,
...     Resize,
... )
```

对于训练数据集的转换，请使用均匀时间子采样、像素归一化、随机裁剪和随机水平翻转的组合。对于验证和评估数据集的转换，保留相同的转换链，但不包括随机裁剪和水平翻转。要了解这些转换的更多细节，请参阅 [PyTorchVideo 的官方文档](https://pytorchvideo.org)。

使用与预训练模型关联的 `image_processor` 获取以下信息：

* 用于对视频帧像素进行归一化的图像均值和标准差。
* 视频帧将被调整到的空间分辨率。

首先定义一些常量。

```py
>>> 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
```

现在，分别定义数据集特定的转换和数据集。从训练集开始：

```py
>>> 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,
... )
```

相同的流程可以应用于验证集和评估集：

```py
>>> 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 官方示例](https://pytorchvideo.org/docs/tutorial_classification#dataset)。我们使用 [`pytorchvideo.data.Ucf101()`](https://pytorchvideo.readthedocs.io/en/latest/api/data/data.html#pytorchvideo.data.Ucf101) 函数，因为它专为 UCF-101 数据集设计。在底层，它返回一个 [`pytorchvideo.data.labeled_video_dataset.LabeledVideoDataset`](https://pytorchvideo.readthedocs.io/en/latest/api/data/data.html#pytorchvideo.data.LabeledVideoDataset) 对象。`LabeledVideoDataset` 类是 PyTorchVideo 数据集中所有视频相关内容的基类。因此，如果你想使用 PyTorchVideo 未开箱即用的自定义数据集，可以相应地扩展 `LabeledVideoDataset` 类。请参阅 `data` API [文档](https://pytorchvideo.readthedocs.io/en/latest/api/data/data.html) 以了解更多信息。此外，如果你的数据集遵循类似的结构（如上所示），那么使用 `pytorchvideo.data.Ucf101()` 应该也能正常工作。

您可以通过访问 `num_videos` 参数来了解数据集中的视频数量。

```py
>>> print(train_dataset.num_videos, val_dataset.num_videos, test_dataset.num_videos)
# (300, 30, 75)
```

## 可视化预处理后的视频，以便更好地进行调试

```py
>>> 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](/docs/transformers/v5.23.0/zh/main_classes/trainer#transformers.Trainer) 来训练模型。要实例化 `Trainer`，你需要定义训练配置和评估指标。其中最重要的是 [TrainingArguments](/docs/transformers/v5.23.0/zh/main_classes/trainer#transformers.TrainingArguments)，它是一个包含所有用于配置训练的属性的类。它需要一个输出文件夹名称，该名称将用于保存模型的检查点。它还有助于同步 🤗 Hub 上模型仓库中的所有信息。

大多数训练参数都一目了然，但这里有一个相当重要的参数：`remove_unused_columns=False`。该参数会丢弃模型 call 函数未使用的任何特征。默认值为 `True`，因为通常丢弃未使用的特征列是理想的，这样更容易将输入解包到模型的 call 函数中。但在这种情况下，你需要保留未使用的特征（特别是 'video'），以便创建 `pixel_values`（这是我们模型输入中必需的键）。

```py
>>> 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,
... )
```

`pytorchvideo.data.Ucf101()` 返回的数据集未实现 `__len__` 方法。因此，在实例化 `TrainingArguments` 时，我们必须定义 `max_steps`。

接下来，你需要定义一个函数，根据预测结果计算指标，该函数将使用你即将加载的 `metric`。你需要做的唯一预处理是取预测 logits 的 argmax：

```py
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 论文](https://huggingface.co/papers/2203.12602) 中，作者采用了以下评估策略。他们在测试视频的多个片段上评估模型，并对这些片段应用不同的裁剪，然后报告汇总得分。但为了简洁起见，本教程中不考虑这一点。

此外，定义一个 `collate_fn`，用于将示例组合成批次。每个批次包含 2 个键，即 `pixel_values` 和 `labels`。

```py
>>> 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`：

```py
>>> 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` 作为分词器传入。这样做只是为了确保图像处理器配置文件（以 JSON 格式存储）也会被上传到 Hub 上的仓库中。

现在通过调用 `train` 方法来微调我们的模型：

```py
>>> train_results = trainer.train()
```

训练完成后，使用 [push_to_hub()](/docs/transformers/v5.23.0/zh/main_classes/trainer#transformers.Trainer.push_to_hub) 方法将您的模型共享到 Hub，以便所有人都能使用您的模型：

```py
>>> trainer.push_to_hub()
```

## 推理

很好，现在你已经完成了模型的微调，可以使用它进行推理了！

加载视频用于推理：

```py
>>> sample_test_video = next(iter(test_dataset))
```

    

尝试使用微调后的模型进行推理的最简单方法是将其用于 [VideoClassificationPipeline](/docs/transformers/v5.23.0/zh/main_classes/pipelines#transformers.VideoClassificationPipeline)。使用你的模型实例化一个用于视频分类的 `pipeline`，并将你的视频传入其中：

```py
>>> from transformers import pipeline
from accelerate import Accelerator

>>> 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` 的结果。

```py
>>> def run_inference(model, video):
...     # (num_frames, num_channels, height, width)
...     permuted_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 = Accelerator().device
...     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`：

```py
>>> logits = run_inference(trained_model, sample_test_video["video"])
```

解码 `logits` 后，我们得到：

```py
>>> predicted_class_idx = logits.argmax(-1).item()
>>> print("Predicted class:", model.config.id2label[predicted_class_idx])
# Predicted class: BasketballDunk
```

