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

ViViT (Video Vision Transformer)

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

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

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

1. Patch Embedding:

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

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

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

Важное дополнение: В 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"

Комментарии (0)