Каждый инженер, работающий в области компьютерного зрения, сталкивается с задачами детекции, сегментации и “сто бед — YOLO ответ”. Однако приходит момент, когда на горизонте появляется новая сложная задача — анализ и классификация видео. Одни предпочитают обходить её стороной, другие пытаются решать её с помощью традиционных методов, но мы пойдем дальше и научимся решать с помощью трансформеров. В целях ознакомления рассмотрим наиболее популярные и эффективные подходы. Погнали!
ViViT (Video Vision Transformer)
ViViT (Video Vision Transformer) – одна из первых моделей, использующих архитектуру трансформеров для анализа видеоданных.
Работа ViViT аналогична Vision Transformers (ViT), но ключевое отличие – разбиение входных данных на трехмерную последовательность изображений. Это позволяет учитывать временную информацию в видеоряде.
Структура модели
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:
-
snowboarding: 90.55%
-
skiing (not slalom or crosscountry): 7.75%
-
ski jumping: 0.98%
-
faceplanting: 0.28%
-
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 может быть более эффективен для обработки длинных видео, а также в вычислительном плане так как нет необходимости обрабатывать все патчи одновременно.
Перейдем к коду:
Код
Код абсолютно идентичен приведенному в разделе 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:
-
snowboarding: 53.77%
-
skiing (not slalom or crosscountry): 40.71%
-
somersaulting: 2.59%
-
snowkiting: 0.90%
-
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 Autoencoders
Основной идеей является восстановление исходных данных из их частичной или искаженной версии. Для этого модель обучается понимать контекст и структуру данных, скрывая часть входных данных с помощью маскирования.
Процесс можно разделить на три этапа:
1) Маскирование:
-
Входное изображение разбивается на патчи (маленькие блоки).
-
Применяется случайная или фиксированная маска, чтобы “спрятать” определенные части данных
-
Оригинальные и замаскированные патчи передаются энкодеру.
2) Энкодер:
-
Преобразует видимые части данных в сжатое скрытое представление. Видимые патчи кодируются в скрытые представления через несколько слоев трансформера.
Важная часть — энкодер не видит замаскированные части данных, он тренируется распознавать структуру и контекст только на основе доступных ему данных.
3) Декодер:
-
Декодер получает скрытые представления из энкодера, которые содержат видимые патчи и токены маски. Его цель – восстановить полные данные, включая замаскированные части.
-
Оценивается насколько восстановленные данные отличаются от оригинальных замаскированных данных. Чаще используется среднеквадратичная ошибка (MSE).
VideoMAE
Мы уже выяснили, что MAE — модель, восстанавливающая маскированные части данных. В контексте видео это означает работу с временными аспектами видео, выборочные скрытия некоторых фрагментов на кадрах или даже целые кадры, которые модель затем должна восстановить. Такой подход заставляет модель учиться на локальных и глобальных особенностях каждого кадра, улучшая общее понимание видеоконтента. Все это позволяет модели самостоятельно выявлять паттерны и структуры в видео, не полагаясь на заранее размеченные данные.
Таким образом, VideoMAE — автоэнкодер, который выступает как data-efficient инструмент для самообучения (self-supervised learning). Данная технология была разработана для повышения эффективности обучения моделей на видео, минимизируя большие затраты на данные и вычисления.
Архитектура VideoMAE аналогична MAE, если коротко:
-
Маскирование.
-
Энкодер.
-
Декодер с маскированными патчами.
-
Реконструкция видео.
Теперь перейдем непосредственно к коду:
Код
Код также аналогичен приведенным ранее, но с небольшими изменениями:
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 предсказаний:
-
snowboarding: 41.77%
-
skiing (not slalom or crosscountry): 13.58%
-
tobogganing: 2.46%
-
biking through snow: 1.07%
-
motorcycling: 0.33%
Время работы модели на CPU: 0.77 секунды
Время работы модели на GPU: 0.17 секунды
VideoMAE показывает хуже результаты на данном видео, но отличные результаты по скорости обработки.
Обучение
Давайте теперь поговорим про работу с видеоклассификатором, но уже на наших данных.
Коротко расскажу о двух подходах:
-
Непосредственно обучение всей модели на новые классы (end-to-end): сработает если достаточно вычислительных ресурсов и данных для обучения.
-
Использование предобученных моделей только в качестве 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:
-
Если важна производительность и экономия вычислительных ресурсов.
-
Если большое количество видео содержат шум или искажения.
-
При необходимости устойчивости к неполным данным.
-
Когда цель – сократить вычислительные затраты без значительной потери точности.
Вывод
Надеюсь, статья была полезной и станет отправной точкой в изучении видеоаналитики и видеоклассификации! Спасибо за внимание!
Ссылки на статьи:
ссылка на оригинал статьи https://habr.com/ru/articles/827474/
Добавить комментарий