Одно из направлений работы нашей команды компьютерного зрения Vision RnD в SberDevices — распознавание жестового языка. Об этой задаче и о том, как мы ее решаем, мы уже писали на Хабре тут и тут (а еще тут и тут). Некоторое время назад перед нами встал вопрос выбора архитектуры нейросети для быстрой и качественной обработки изображений (видео-энкодера). Хотя сама задача распознавания жестового языка предполагает обработку видео, в качестве первого этапа нужна нейросеть, обрабатывающая изображения на отдельных кадрах. Причем делающая это достаточно быстро, чтобы обеспечить работу всей конструкции в реальном времени. Безусловно, за последний десяток лет человечеству стало известно немало архитектур нейросетей для обработки изображений. Однако, сопоставить их по критерию цена-качество точность-производительность и выбрать лидера не так просто. Мы решили собрать несколько популярных решений-претендентов на звание чемпиона и провести состязание в славном городе Гамбурге тестирование в идентичных условиях. Результатами этого исследования делимся под катом.
О том, как мы работаем над распознаванием жестового языка, мы уже писали, и не раз. В частности, мы
— собрали и опубликовали датасет русского жестового языка Slovo, о чем рассказывали в статье «Slovo и русский жестовый язык»,
— получили SOTA результаты на датасете американского жестового языка WLASL-2000, результатами делились в статье «Русский жестовый язык: первое место в американском бенчмарке»,
— в статье «GigaChat и русский жестовый язык» рассказали о реализации прототипа общения с генеративной языковой моделью GigaChat,
— в статье «Распознавание и перевод жестовых языков: обзор подходов» рассказали о путях решения задачи распознавания жестовых языков в целом.
Итак, при распознавании жестового языка на видео в качестве первого и, часто, самого ресурсоемкого этапа стоит обработка поступающих кадров нейросетью, которая кодирует каждый кадр в виде вектора признаков, — видео-энкодером. Для того, чтобы обеспечить работу всей системы в реальном времени, нам понадобилось выбрать архитектуру видео-энкодера, оптимальную с точки зрения баланса скорости работы и качества.
Почему это вообще оказалось необходимым? Разве авторы не публикуют показатели качества и производительности в сравнении с другими моделями?
Да, в соответствующих статьях приводятся сравнения предлагаемых архитектур с предшественниками. Однако:
часто авторы ограничиваются указанием очередного SOTA-результата по точности, и лишь косвенно указывают производительность;
там, где все же приводятся графики, показывающие положение предлагаемых архитектур на диаграмме точность-производительность, в качестве конкурентов приводятся существенно более старые архитектуры. По понятным причинам там нет сравнения с архитектурами, вышедшими почти одновременно с публикуемой работой;
данные по быстродействию из разных статей сравнивать проблематично, т. к. производительность в них оценена в разных условиях;
открытым остается вопрос, насколько опубликованным данным можно верить.
Дисклеймер: список претендентов составлен закрыто, недемократично, субъективно и предвзято. И он такой, какой есть. Комментарии вида «Ну как можно было не включить такую замечательную BlaBlaBla Net!» с благодарностью принимаются (а вдруг мы и правда забыли про BlaBlaBla Net или вообще не слышали про такую, позор!), но ответить можем только «потому что так сложилось».
Итак, для состязания на ринг вызываются:
представители молодого (на самом деле, уже не очень) племени видео-трансформеров:
MViT-v2: статья (2021), реализация
EfficientVit: статья (2022), реализация
Next-ViT: статья (2022), реализация
EfficientFormer: статья (2022), реализация
EfficientFormerV2: статья (2022), реализация
FasterVit: статья (2023), реализация
и продолжатели дела хорошо зарекомендовавшей себя классики свёрточных сетей:
ConvNeXt: статья (2022), реализация
ConvNeXt-v2: статья (2023), реализация
FasterNet: статья (2023), реализация
В качестве задачи использовали классику жанра: классификацию изображений ImageNet (он же ILSVRC2012). Метрика качества — top-1 и top-5 accuracy. Как оказалось, относительные положения результатов на графиках top-1 и top-5 качественно почти совпадают, отличаются только значения. Поэтому картинки приводим только для top-1.
В качестве платформы использовался виртуальный облачный сервер, в который из физического сервера NVIDIA DGX-2 было выделено 3 ядра от процессора Intel Xeon Platinum 8168 2.7 GHz, 1 GPU Tesla V100 32 ГБ и 92 ГБ оперативной памяти. В основном опыты ставили на CUDA 11.8 и PyTorch 2.0.0. Также для сравнения попробовали некоторые проекты запустить на CUDA 11.1 + PyTorch 1.9.1, об этом ниже.
Оценка делалась для 4 вариантов запуска:
CPU, batch size = 1
1 карта GPU, batch size = 1
1 карта GPU, batch size = 128
1 карта GPU, batch size = 128, c AMP (Automatic Mixed Precision, см. ниже)
По поводу выбора платформы должен сделать такой же дисклеймер, как и в отношении выбора моделей для тестирования. Мне очень жаль, что мы не сделали тесты на другом железе: мобильные устройства, Mac и прочее. Но человеческая жизнь коротка. Ограничились тем, что требовалось в конкретный момент.
В статьях с описанием большинства тестируемых проектов приводятся данные о скорости работы на том или ином «железе». Но, за редким исключением, в исходном коде проектов нет готового скрипта, где бы эта производительность оценивалась. Поэтому данный код пришлось писать самим. Хочу тут привести небольшой ликбез. Прошу прощения, если для кого-то скажу очевидные вещи. Стандартный шаблон кода для оценки метрик качества упрощенно выглядит так:
for images, target in data_loader:
output = model(images)
metrics.update(output, target)
Поскольку нам нужно время работы собственно модели, естественным кажется замерить время выполнения соответствующей строки кода:
tic1 = time.time()
output = model(images)
execution_time = time.time() - tic1
metrics.update(output, target)
В результате такой оценки получается просто-таки замечательное время работы модели. Которое превосходит ожидаемое в несколько раз.
Дело в том, что вычисления на CUDA происходят асинхронно. Данные передаются на видеокарту, запускается расчет, а программа на python на CPU продолжает свою работу. Этому не препятствует даже то, что мы, вроде как, получили результат — находящийся на CUDA тензор output
. Фактически, эта переменная просто хранит информацию, что где-то на CUDA будет результат вычислений, и позволяет его при необходимости оттуда получить. Но пока такая необходимость не настала, вычисления на GPU — сами по себе, на CPU — сами по себе. И только где-то внутри metrics.update(output, target)
, когда мы обратимся к output
и попытаемся считать лежащие там значения, код будет вынужден дождаться завершения расчетов на GPU. Но это случится уже сильно после того, как мы замерили якобы потраченное моделью время.
Чтобы справиться с этой проблемой, в PyTorch существует функция torch.cuda.synchronize(), которая заставляет код дождаться завершения расчетов на CUDA, даже тех, которые еще не требуются. Если поместить ее вызов после вызова model(images)
, перед вычислением времени выполнения, замеренное время увеличивается в 2-3 раза. Это огорчает, зато честно!
Любопытно, что в двух исследованных проектах, в которых код для замера производительности присутствует (FasterNet и NextVit), этот код выглядит примерно так:
tic1 = time.time()
for i in range(iter):
model(images)
torch.cuda.synchronize()
execution_time = (time.time() - tic1)/iter
Как видим, torch.cuda.synchronize()
здесь используется, но не на каждой итерации, а для всех итераций вместе. Впрочем, в отличие от примера выше, здесь это не дает заметной ошибки, т. к. в этом цикле просто нет моментов, когда CUDA могла бы продолжать свои вычисления, а затраченное на это время не учитывалось бы. И все же можно предположить, что для каких-то моделей результат может оказаться не вполне корректным, т. к. такой код позволяет оптимизировать работу CUDA в пределах обработки нескольких батчей. Что отличается от честного хронометража обработки каждого отдельного батча заданного размера.
В наших расчетах мы использовали во всех проектах шаблон кода:
execution_time = 0
for n in range(num_repeats):
t0 = time.time()
output = model(inputs)
torch.cuda.synchronize()
execution_time += time.time() - t0
throughput = num_repeats * batch_size / execution_time # кадров/сек.
Еще немного комментариев от Капитана Очевидность! Как известно, на современных графических ускорителях, которые поддерживают вычисления с половинной точностью, уменьшение разрядности вычислений с float32 на float16 дает заметную прибавку к производительности. При этом в некоторых из исследованных моделей работа с половинной точностью была реализована, в некоторых — нет, и для корректного сравнения пришлось ее добавить. Благо, PyTorch включает технологию Automatic Mixed Precision (AMP), которая позволяет это сделать автоматически добавлением одной строчки кода:
with torch.cuda.amp.autocast(use_amp):
...
Мы оценили, насколько заметную прибавку к скорости дает использование AMP. Оказалось — более чем, достигается ускорение до 3.5 раз. Отметим, что прибавка к скорости более существенна для более сложных моделей. Однако эффект есть только при достаточно большом batch size. При batch size = 1 никакого ускорения использование AMP не дает, иногда даже приводит к небольшой деградации скорости.
Подробнее результаты для batch size = 128 приведены в таблице 1 и диаграмме.
Таблица 1. Ускорение за счет использования AMP (PyTorch 2.0.0, batch size = 128)
Модель | top-1 accuracy | Params (M) | скорость без AMP, | скорость с AMP, | ускорение при использовании AMP |
Next-ViT small | 82.5 | 31.7 | 800 | 1820 | x 2.3 |
Next-ViT large | 83.6 | 57.8 | 450 | 1060 | x 2.4 |
FasterVit v0 | 82.1 | 31.4 | 1540 | 3150 | x 2.0 |
FasterVit v5 | 85.6 | 975.5 | 73 | 255 | x 3.5 |
FasterNet t0 | 71.9 | 3.9 | 5500 | 11200 | x 2.0 |
FasterNet L | 83.5 | 93.4 | 400 | 1100 | x 2.8 |
MViT-v2 T | 82.3 | 24 | 640 | 1050 | x 1.6 |
MViT-v2 L | 85.3 | 218 | 93 | 167 | x 1.8 |
EfficientFormerV2 s0 | 76.15 | 3.6 | 900 | 1140 | x 1.3 |
EfficientFormerV2 L | 83.51 | 26.1 | 310 | 375 | x 1.2 |
EfficientVit B1 | 79.05 | 9.1 | 2250 | 2860 | x 1.3 |
EfficientVit L3 | 85.80 | 246 | 230 | 600 | x 2.6 |
ConvNeXt-V2 A | 76.65 | 3.7 | 2230 | 2400 | x 1.1 |
ConvNeXt-V2 L | 85.76 | 198 | 130 | 250 | x 1.9 |
Также мы оценили для 2 моделей (FasterVit, NextVit) изменение производительности при замене версии PyTorch с 1.9.1 на 2.0.0. Разработчики PyTorch обещали, что версия 2.0 «is faster», но было интересно узнать, насколько «faster». Оказалось, что без AMP эффект если и есть, то незначительный (см. Таблицу 2). Но при работе с AMP во второй версии PyTorch скорость может быть выше почти в 1.5 раза (Таблица 3).
Таблица 2. Прирост производительности при переходе с PyTorch 1.9.1 на PyTorch 2.0.0, batch size = 128, без AMP
Модель | скорость v1.9.1, | скорость v2.0.0, | ускорение при обновлении PyTorch, % |
Next-ViT small | 800 | 800 | 0 |
Next-ViT large | 450 | 450 | 0 |
FasterVit v0 | 1400 | 1540 | 10% |
Таблица 3. Прирост производительности при переходе с PyTorch 1.9.1 на PyTorch 2.0.0, batch size = 128, с использованием AMP
Модель | скорость v1.9.1, | скорость v2.0.0, | ускорение при обновлении PyTorch, % |
Next-ViT small | 1440 | 1820 | 26% |
Next-ViT large | 840 | 1060 | 26% |
FasterVit v0 | 2200 | 3150 | 43% |
Ниже приводим получившиеся графики «точность (top-1 accuracy) — производительность (изображений/сек)» для каждой из трех заявленных дисциплин. Для каждой архитектуры тестировалось несколько конфигураций, для которых авторами были выложены предобученные на ImageNet веса.
Таблица 4. Конфигурации моделей, участвовавшие в тестировании
Модель | Конфигурации |
MViT-v2 | T, S, B, L |
EfficientVit | B1, B2, B3, L1, L2, L3 |
Next-ViT | small, base, large |
EfficientFormer | v1_L7 |
EfficientFormerV2 | s0, s1, s2, L |
FasterVit | v0, v1, v2, v3, v4, v5 |
ConvNeXt | T, S, B, L |
ConvNeXt-v2 | A, P, N, B, L |
FasterNet | t0, t1, t2, S, M, L |
Часть результатов приведена выше в Таблице 1. Полную таблицу с результатами для экономии места не приводим, представим только графики.
Конфигурации каждая модели представлены на графиках точками определенного вида. Для каждой модели конфигурации упорядочены в порядке увеличения сложности и, соответственно, точности. Поэтому сопоставить точки на графиках с конфигурациями не представляет труда. Отмечу, что в большинстве случаев увеличению точности и сложности соответствует уменьшение производительности, но в некоторых тестах это правило может немного нарушаться.
В этом режиме большинство рассмотренных архитектур показывает весьма синхронную зависимость точности и быстродействия. Впрочем, на левом фланге (среди относительно тяжёлых сетей) можно в качестве лидеров выделить EfficientViT и ConvNeXt-v2, а на правом, среди быстрых сетей с качеством пониже — NextViT и FasterViT. Еще один трансформер, MViT-v2, работает неплохо и там, и там. Нишу совсем быстрых сетей занимают легкие модификации свёрточных FasterNet и ConvNeXt-v2, но качество работы у них падает очень существенно.
В этом режиме заметно, что сетки на основе трансформеров работают хорошо в вариантах с большим количеством параметров (медленно, но качественно), но для более простых моделей скорость работы растет медленнее, а качество падает намного быстрее, чем для свёрточных архитектур, поэтому в зачете «скорость важнее качества» FasterNet и ConvNeXt уверенно оставляют трансформеры позади.
Самый типичный случай применения для обработки больших массивов данных. Но не всегда применимый для решения практических задач. Здесь в качестве лидеров можно отметить FasterVit и, для более «тяжёлых» архитектур, EfficientVit. Есть еще самая легкая модификация FasterNet с совсем большой скоростью работы, но низким качеством, просто ни одна другая архитектура не претендует на работу в этой области баланса скорости и точности.
Приведу в сжатом виде наблюдения, которые мы сделали в ходе этого небольшого исследования.
Технологические выводы:
1.1. Важно не забывать корректно использовать torch.cuda.synchronize() при хронометрировании работы модели.
1.2. Использование AMP (Automatic Mixed Precision) не дает эффекта или даже немного ухудшает производительность при batch size=1.
1.3. При большом batch size (равном 128) AMP дает очень существенный выигрыш производительности — до 3.5 раз.
1.4. Выигрыш от использования AMP больше для сложных моделей и меньше для простых
1.5. Более новая версия PyTorch (2.0.0 против 1.9.1) дает заметный выигрыш в производительности при использовании AMP и большого batch size (ускорение до 1.5 раз). В остальных случаях (CPU, GPU без AMP или с batch size = 1) улучшение если есть, то в пределах 10%.
Выводы относительно скорости работы исследованных моделей:
2.1. При тестировании на CPU все модели показывают достаточно синхронную зависимость производительности и точности. Т. е. разброс точности работы для конфигураций, работающих с одинаковой скоростью, существенно меньше, чем в других условиях тестирования.
2.2. При работе на GPU с batch size = 1 в номинации «медленно, но качественно» наилучшие результаты показывают сети на основе трансформеров. Но при уменьшении сложности (и, соответственно, точности) скорость работы таких архитектур оказывается хуже, чем у дающих ту же точность свёрточных сетей. Поэтому в случаях, когда скорость работы важнее качества, в фаворитах оказывается старая добрая классика CNN.
2.3. При работе на GPU с большим batch size сети на основе CNN уступают трансформерам, причем в случаях, когда скорость важнее качества, отставание становится особенно заметным. Т. е. соотношение противоположно тому, которое наблюдается при batch size = 1.
2.4. При тестах в разных режимах и для разных положений на шкале качество-скорость лидирующее положение занимают различные архитектуры. Поэтому выбор оптимальной архитектуры и ее конфигурации надо делать, исходя из требуемого режима работы. В этом могут помочь приведенные в статье графики.
В завершение хочется сказать банальное «нельзя объять необъятное». Жаль, что не хватило времени протестировать некоторые другие архитектуры. Еще более жаль, что мы не сделали тесты на других платформах. Но, надеюсь, представленные результаты принесут читателям пользу.
Также хочу поблагодарить коллег из команды Vision RnD SberDevices Карину Кванчиани и Александра Капитанова за помощь в подготовке статьи.