# 動画分類

Video classificationは、動画全体にラベルまたはクラスを割り当てるタスクです。各動画には1つのクラスのみが割り当てられることが前提とされます。Video classificationモデルは動画をインプットとして受け取り、その動画がどのクラスに属するかについての予測を返します。これらのモデルは、動画の内容を分類するために使用できます。Video classificationの実世界での応用例として、フィットネスアプリケーションに有用なアクション/アクティビティ認識があります。また、視覚障害のある方々、特に通勤時にも役立ちます。

このガイドでは、次の方法について説明します：

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)を防ぐため、同じグループ/シーンからの動画クリップが含まれないようにする必要があります。このチュートリアルで使用しているサブセットは、この情報を考慮しています。

次に、データセットに存在するラベルの集合を導出します。また、モデルの初期化時に役立つ2つの辞書を作成します。

* `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 library](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)から引用したものです。UCF-101データセットに特化しているため、[`pytorchvideo.data.Ucf101()`](https://pytorchvideo.readthedocs.io/en/latest/api/data/data.html#pytorchvideo.data.Ucf101)関数を使用しています。内部では、[`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/main/ja/main_classes/trainer#transformers.Trainer) を活用してモデルを学習します。`Trainer` をインスタンス化するには、学習設定と評価指標を定義する必要があります。最も重要なのは [TrainingArguments](/docs/transformers/main/ja/main_classes/trainer#transformers.TrainingArguments) で、これは学習を設定するためのすべての属性を含むクラスです。モデルのチェックポイントを保存するために使用される出力フォルダ名を指定する必要があります。また、🤗 Hub 上のモデルリポジトリ内のすべての情報を同期するのにも役立ちます。

ほとんどの学習引数はその名前から意味が推測できますが、ここでは`remove_unused_columns=False`が特に重要です。この引数は、モデルのcall関数で使用されない特徴量を削除します。通常、未使用の特徴量カラムを削除するのが望ましいため、デフォルトでは`True`になっています。これにより、モデルのcall関数への入力のアンパッキングが容易になります。しかし、このケースでは、`pixel_values`（モデルの入力に必須のキー）を作成するために、未使用の特徴量（特に'video'）が必要です。

```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`を定義します。これは、サンプルをバッチにまとめるために使用されます。各バッチは`pixel_values`と`labels`という2つのキーで構成されます。

```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/main/ja/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/main/ja/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
```

