Видеоаналитика: Разбор VideoMAE, ViViT и TimeSFormer

от автора

Каждый инженер, работающий в области компьютерного зрения, сталкивается с задачами детекции, сегментации и “сто бед — YOLO ответ”. Однако приходит момент, когда на горизонте появляется новая сложная задача — анализ и классификация видео. Одни предпочитают обходить её стороной, другие пытаются решать её с помощью традиционных методов, но мы пойдем дальше и научимся решать с помощью трансформеров. В целях ознакомления рассмотрим наиболее популярные и эффективные подходы. Погнали!

ViViT (Video Vision Transformer)

ViViT (Video Vision Transformer) – одна из первых моделей, использующих архитектуру трансформеров для анализа видеоданных.

Работа ViViT аналогична Vision Transformers (ViT), но ключевое отличие – разбиение входных данных на трехмерную последовательность изображений. Это позволяет учитывать временную информацию в видеоряде.

Общая архитектура ViViT

Общая архитектура ViViT
Структура модели

1. Patch Embedding:

  • Видео разбивается на небольшие трехмерные патчи (например, размером 16×16 пикселей с временной глубиной в несколько кадров).

  • Каждый патч затем преобразуется в эмбеддинг, используя линейную проекцию, представляющий собой векторное пространство.

  • Для учета последовательности патчей добавляются позиционные эмбеддинги. Позиционные эмбеддинги – это способ «запомнить» порядок патчей для нейронной сети. Соответственно, мы получаем информацию о позиции патча в исходном изображении.

Важное дополнение: В ViViT позиционные эмбеддинги являются  обучаемыми параметрами.  Это означает, что сеть сама «учится»  оптимальному способу кодирования информации о положении патчей.

2. Transformer Encoder:

  • Каждый патч (эмбеддинг и позиционный эмбеддинг) подаются на вход трансформера

  • Трансформер состоит из нескольких слоев с механизмом внимания (attention), который помогает модели выучить зависимости между патчами.

3. Classification Head:

  • После прохождения через энкодер модель выводит вектор признаков для классификации.

  • Этот вектор подается в классификационный слой, который выдает вероятности каждого класса.

Теперь давайте перейдем к коду:

Код

Для начала необходимо установить все необходимые библиотеки:

pip install transformers torch pillow opencv-python

Спасибо библиотеке transformers, что наши тесты будут максимально простыми и с минимальным количеством кода:

# Импортируем необходимые библиотеки from transformers import VivitForVideoClassification, VivitImageProcessor import torch import cv2  # Загружаем модель и процессинг кадров model_name = "google/vivit-b-16x2-kinetics400" processor = VivitImageProcessor.from_pretrained(model_name) model = VivitForVideoClassification.from_pretrained(model_name)  # Функция для загрузки и обработки видео def load_video(video_path, num_frames=32, frame_height=480, frame_width=480):     cap = cv2.VideoCapture(video_path)     frames = []     while len(frames) < num_frames:         ret, frame = cap.read()         if not ret:             break         frame = cv2.resize(frame, (frame_width, frame_height))         frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)         frames.append(frame)     cap.release()     if len(frames) < num_frames:         raise ValueError(f"Video is too short. Needed: {num_frames}, Found: {len(frames)}")     return frames  # Функция для загрузки меток def load_labels(label_path):     with open(label_path, 'r') as f:         labels = f.read().splitlines()     return labels   # Функция для получения предсказаний def predict_label(video_path, label_map_path):     frames = load_video(video_path)     inputs = processor(images=frames, return_tensors="pt")     outputs = model(**inputs)     # Загрузка меток     kinetics_labels = load_labels(label_map_path)     # Добавим вывод вероятностей     logits = outputs.logits     probabilities = torch.nn.functional.softmax(logits, dim=-1)     top_5_probs, top_5_indices = torch.topk(probabilities[0], 5)     top_5_predictions = [kinetics_labels[idx.item()] for idx in top_5_indices]      print("Топ 5 предсказаний:")     for i, (label, prob) in enumerate(zip(top_5_predictions, top_5_probs), 1):         print(f"{i}. {label}: {prob.item() * 100:.2f}%")  # Путь к видеофайлу video_path = 'snow.mp4'  # Загрузка label_map.txt происходит по след. ссылке: https://github.com/google-deepmind/kinetics-i3d/blob/master/data/label_map.txt # Получение предсказаний predict_label(video_path, "label_map.txt")

В качестве примера был взят видеофрагмент сноубординга:

Топ 5 предсказаний ViViT:

  1. snowboarding: 90.55%

  2. skiing (not slalom or crosscountry): 7.75%

  3. ski jumping: 0.98%

  4. faceplanting: 0.28%

  5. tobogganing: 0.25%

Неплохо, не правда ли?

Время работы модели на CPU: 5.85 секунды

Время работы модели на GPU: 0.84 секунды

TimeSFormer

В целом, подход схож с ViViT, но основное различие между ними заключается в способе обработке патчей и организационной структуре временных и пространственных зависимостей.

Структура модели

Давайте также коротко пробежимся по структуре:

1. Patch Embedding:

  • Каждый фрейм делится на небольшие патчи (по аналогии с Vision Transformer, ViT).

  • Патчи из каждого фрейма выравниваются по временной оси, образуя многослойную последовательность патчей.

  • Патчи преобразуются в векторы фиксированной размерности через линейную проекцию.

  • К этим векторным представлениям добавляется позиционное кодирование, чтобы сохранить информацию о позиции патчей в пространстве и времени.

2. Transformer Encoder:

  • Энкодер состоит из нескольких слоев attention-механизмов и feed-forward сетей.

  • В TimeSformer используется раздельное внимание (attention) к пространственным и временным аспектам.

  • Space Attention (пространственное внимание) применяется ко всем патчам в каждом временном кадре независимо.

  • Time Attention (временное внимание) применяется ко всем патчам из всех временных кадров для каждого пространственного патча.

3. Classification Head:

  • После прохождения через энкодер модель выводит вектор признаков для классификации.

  • Этот вектор подается в классификационный слой, который выдает вероятности каждого класса.

TimeSformer чередует слои для обработки пространственных зависимостей (внутри одного кадра) и временных зависимостей (между разными кадрами), в итоге, в отличии от ViViT, обрабатывает пространственные и временные attention отдельно.

За счет раздельного вычисления временного и пространственного self-attention TimeSformer может быть более эффективен для обработки длинных видео, а также в вычислительном плане так как нет необходимости обрабатывать все патчи одновременно.

Пространственно-временное внимание TimeSFormer

Пространственно-временное внимание TimeSFormer

Перейдем к коду:

Код

Код абсолютно идентичен приведенному в разделе ViViT, разница только в модели:

from transformers import AutoImageProcessor, TimesformerForVideoClassification  model_name = "facebook/timesformer-base-finetuned-k400" processor = AutoImageProcessor.from_pretrained(model_name) model = TimesformerForVideoClassification.from_pretrained(model_name)

Топ 5 предсказаний TimeSformer:

  1. snowboarding: 53.77%

  2. skiing (not slalom or crosscountry): 40.71%

  3. somersaulting: 2.59%

  4. snowkiting: 0.90%

  5. ski jumping: 0.74%

Время работы модели на CPU: 1.25 секунды

Время работы модели на GPU: 0.63 секунды

Здесь уже есть большие сомнения между сноубордом и катанием на лыжах, но моя задача как раз показать различия в технике, скорости и результате между моделями.

Video Masked Autoencoders

Для начала давайте разберемся что такое autoencoders. Если очень коротко, то Autoencoders – это тип нейронных сетей, который используется для обучения эффективного кодирования данных. Состоит из двух частей: 

1. Энкодер: сжимает входные данные в компактное представление (код).

2. Декодер: восстанавливает исходные данные из этого кода.

Основная цель — научиться полезному представлению данных, минимизируя разницу между оригинальными и восстановленными данными.

На хабр есть подробная статья про autoencoders: https://habr.com/ru/companies/skillfactory/articles/671864/

Masked Autoencoders

Работа Masked Autoencoder

Работа Masked Autoencoder
Masked Autoencoders

Основной идеей является восстановление исходных данных из их частичной или искаженной версии. Для этого модель обучается понимать контекст и структуру данных, скрывая часть входных данных с помощью маскирования.

Процесс можно разделить на три этапа:
1) Маскирование: 

  • Входное изображение разбивается на патчи (маленькие блоки).

  • Применяется случайная или фиксированная маска, чтобы “спрятать” определенные части данных

  • Оригинальные и замаскированные патчи передаются энкодеру.

2) Энкодер: 

  • Преобразует видимые части данных в сжатое скрытое представление. Видимые патчи кодируются в скрытые представления через несколько слоев трансформера.

Важная часть — энкодер не видит замаскированные части данных, он тренируется распознавать структуру и контекст только на основе доступных ему данных. 

3) Декодер: 

  • Декодер получает скрытые представления из энкодера, которые содержат видимые патчи и токены маски. Его цель – восстановить полные данные, включая замаскированные части.

  • Оценивается насколько восстановленные данные отличаются от оригинальных замаскированных данных. Чаще используется среднеквадратичная ошибка (MSE).

VideoMAE

Мы уже выяснили, что MAE — модель, восстанавливающая маскированные части данных. В контексте видео это означает работу с временными аспектами видео, выборочные скрытия некоторых фрагментов на кадрах или даже целые кадры, которые модель затем должна восстановить. Такой подход заставляет модель учиться на локальных и глобальных особенностях каждого кадра, улучшая общее понимание видеоконтента. Все это позволяет модели самостоятельно выявлять паттерны и структуры в видео, не полагаясь на заранее размеченные данные.

Таким образом, VideoMAE — автоэнкодер, который выступает как data-efficient инструмент для самообучения (self-supervised learning). Данная технология была разработана для повышения эффективности обучения моделей на видео, минимизируя большие затраты на данные и вычисления.

Архитектура VideoMAE аналогична MAE, если коротко:

  1. Маскирование.

  2. Энкодер.

  3. Декодер с маскированными патчами.

  4. Реконструкция видео.

Архитектура VideoMAE

Архитектура VideoMAE

Теперь перейдем непосредственно к коду:

Код

Код также аналогичен приведенным ранее, но с небольшими изменениями:

from transformers import VideoMAEForVideoClassification, VideoMAEImageProcessor   model_name = "MCG-NJU/videomae-base-finetuned-kinetics" model = VideoMAEForVideoClassification.from_pretrained(model_name) feature_extractor = VideoMAEImageProcessor.from_pretrained(model_name)  # Изменения также касаются num_frames def load_video(video_path, num_frames=16, frame_height=480, frame_width=480):     cap = cv2.VideoCapture(video_path)     frames = []     try:         while len(frames) < num_frames:             ret, frame = cap.read()             if not ret:                 break             frame = cv2.resize(frame, (frame_width, frame_height))             frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)             frames.append(frame)     except Exception as e:         print(f"Error reading video: {e}")      cap.release()      if len(frames) < num_frames:         raise ValueError(f"Video is too short. Needed: {num_frames}, Found: {len(frames)}")      return frames

Топ 5 предсказаний:

  1. snowboarding: 41.77%

  2. skiing (not slalom or crosscountry): 13.58%

  3. tobogganing: 2.46%

  4. biking through snow: 1.07%

  5. motorcycling: 0.33%

Время работы модели на CPU: 0.77 секунды

Время работы модели на GPU: 0.17 секунды

VideoMAE показывает хуже результаты на данном видео, но отличные результаты по скорости обработки.

Обучение

Давайте теперь поговорим про работу с видеоклассификатором, но уже на наших данных.

Коротко расскажу о двух подходах:

  1. Непосредственно обучение всей модели на новые классы (end-to-end): сработает если достаточно вычислительных ресурсов и данных для обучения.

  2. Использование предобученных моделей только в качестве feature-extractor в комбинации с классификатором: если у вас часто обновляются классы.

Подход номер 1

Здесь всё абсолютно идентично стандартному transfer learning:

# Замораживаем все слои, кроме последнего for param in model.parameters():     param.requires_grad = False  model.classifier.requires_grad = True  # Создадим класс для загрузки данных class VideoDataset(torch.utils.data.Dataset):     def __init__(self, video_paths, labels, num_frames=16):         self.video_paths = video_paths         self.labels = labels         self.num_frames = num_frames         self.processor = VideoMAEImageProcessor.from_pretrained(model_name)      def __len__(self):         return len(self.video_paths)      def __getitem__(self, idx):         video_path = self.video_paths[idx]         label = self.labels[idx]         frames = load_video(video_path, num_frames=self.num_frames)         inputs = self.processor(frames, return_tensors="pt")         return inputs['pixel_values'].squeeze(0), label         ''' Далее настраиваем пути до наших данных Выбираем подходящую loss функцию и выполняем стандартный цикл обучения  ''' dataset = VideoDataset(video_paths, labels) dataloader = torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=True)  for epoch in range(num_epochs):   ...   

На выходе получаем веса, которые мы можем использовать для инференса на новых видео данных.

Подход номер 2

Основная идея заключается в том, чтобы использовать трансформер в качестве фиче-экстрактора, сохранении полученных фичей и дальнейшем обучении модели классификатора.

Преимущество данного подхода в том, что нет необходимости постоянно переобучать модели трансформера: можно просто извлечь фичи для новых данных и обучить новый легковесный классификатор, соответственно, снижаются вычислительные и временные затраты.

Недостаток, очевидно, в том, что модель фиче-экстрактора должна соответствовать вашим ожиданиям, т.е. уметь доставать требуемые признаки для дальнейшего анализа.

Давайте очень приблизительно изобразим подход в коде:

# Изменим получение предсказаний на получение признаков def extract_features(video_path):     frames = load_video(video_path)     inputs = processor(frames, return_tensors="pt").to(device)     with torch.no_grad():         outputs = model(**inputs, output_hidden_states=True)     features = outputs.hidden_states[-1].squeeze(0).mean(dim=1)     return features.cpu()  # Добавим сохранение признаков def save_features(features, save_path):     torch.save(features, save_path)  # Добавим пути до наших видео (только для примера в таком формате) video_paths = {     'class1': ['video1.mp4'],     'class2': ['video2.mp4'], }  # Извлечем и сохраним наши признаки в отдельные файлы (опять же, только для примера) for class_name, paths in video_paths.items():     class_save_dir = os.path.join(save_dir, class_name)     os.makedirs(class_save_dir, exist_ok=True)     for video_path in paths:         features = extract_features(video_path)         video_name = os.path.basename(video_path).split('.')[0]         save_path = os.path.join(class_save_dir, f'{video_name}_features.pt')         save_features(features, save_path)

Дальше можем делать с полученными признаками все, что угодно. Один из вариантов — обучить классификатор с triplet loss:

from pytorch_metric_learning import losses from pytorch_metric_learning.miners import TripletMarginMiner from torch.utils.data import Dataset  # Создаем класс для загрузки полученных ранее фичей class FeaturesLoad(Dataset):     def __init__(self, features_dir):       ...     def __getitem__(self, idx):       ...       return feature, label  # Описываем модель классификатора или используем готовый class SimpleCls(nn.Module):   ...  model = SimpleСls(...).to(device) criterion = losses.TripletMarginLoss(margin=0.1) triplet_miner = TripletMarginMiner(margin=0.2, type_of_triplets="hard")  # Обучение for epoch in range(num_epochs):     for features, labels in dataloader:         features, labels = features.to(device), labels.to(device)                  optimizer.zero_grad()         embeds = model(features)         triplets = triplet_miner(embeds, labels)         loss = criterion(embeds, labels, triplets)         loss.backward()         optimizer.step() 

Сравнение моделей

Давайте коротко про то, какую модель выбрать в каких ситуациях:

Когда выбрать ViViT:

  • Когда требуется высокая точность.

  • При наличии больших вычислительных ресурсов (GPU/TPU).

  • Для работы с крупными наборами видеоданных, где критична хорошая масштабируемость модели.

  • Не критична скорость инференса и обучения.

Когда выбрать TimeSFormer:

  • Необходима работа с длинными видеофрагментами.

  • Если присутствуют классы видео, содержащие различную длину фрагментов.

  • Если задачи требуют учета и временных, и пространственных зависимостей (например, задачи отслеживания объектов и понимания сцены).

Когда выбрать VideoMAE:

  • Если важна производительность и экономия вычислительных ресурсов.

  • Если большое количество видео содержат шум или искажения.

  • При необходимости устойчивости к неполным данным.

  • Когда цель – сократить вычислительные затраты без значительной потери точности.

Вывод

Надеюсь, статья была полезной и станет отправной точкой в изучении видеоаналитики и видеоклассификации! Спасибо за внимание!

Ссылки на статьи:

  1. «ViViT: A Video Vision Transformer»

  2. «Is Space-Time Attention All You Need for Video Understanding?»

  3. «VideoMAE: Masked Autoencoders for Self-Supervised Video Representation Learning»


ссылка на оригинал статьи https://habr.com/ru/articles/827474/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *