Transformers documentation

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

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
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