Transformers documentation

Классификация видео

Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Классификация видео

Классификация видео — это задача присвоения метки или класса всему видео. Ожидается, что у каждого видео будет только один класс. Модели классификации видео принимают на вход видео и возвращают предсказание о том, к какому классу оно относится. Эти модели можно использовать для категоризации содержания видео. Примером практического применения классификации видео является распознавание действий/активности, что полезно для фитнес-приложений. Это также помогает людям с нарушениями зрения, особенно во время поездок.

В этом руководстве показано, как:

  1. Настройте VideoMAE на подмножестве набора данных UCF101.
  2. Используйте дообученную модель для инференса.

Чтобы увидеть все архитектуры и контрольные точки, совместимые с этой задачей, рекомендуем обратиться к странице задачи.

Прежде чем начать, убедитесь, что у вас установлены все необходимые библиотеки:

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]

Пути к видео (sorted) выглядят так:

...
'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 соответствующим образом. Подробнее можно узнать в документации к API data. Кроме того, если структура вашего набора данных похожа на показанную выше, то использование 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)
Person playing basketball

Обучение модели

Используйте Trainer из 🤗 Transformers для обучения модели. Для создания экземпляра 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_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, который вы загрузите сейчас. Единственная необходимая предобработка — взять 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, которая будет использоваться для объединения примеров в батчи. Каждый батч состоит из двух ключей, а именно 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,
...     processing_class=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))
Teams playing basketball

Самый простой способ проверить вашу дообученную модель для инференса — использовать её в VideoClassificationPipeline. Создайте экземпляр pipeline для классификации видео с вашей моделью и передайте в него видео:

>>> 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, если хотите.

>>> 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:

>>> 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
Update on GitHub