Привет, Хабр! Меня зовут Антон Рябых, работаю в Doubletapp и в этой статье расскажу про технические детали применения машинного обучения в проекте HitFactor.
Что такое hit factor? На соревнованиях по практической стрельбе спортсмены быстро перемещаются, меняют магазин и стреляют по разным, в том числе и подвижным мишеням. Hit factor — это результат соревнования, то есть количество набранных очков, деленное на время прохождения.
Нам рассказали об этом чемпионы мира по практической стрельбе Алена Карелина и Роман Халитов, которым нужно было мобильное приложение для помощи в тренировках. Двигаться экономнее, стрелять быстрее — анализ записи тренировки поможет понять, как сократить время на прохождение упражнения и повысить эффективность.
В проекте требовалось очень точно определять время начала выстрела и время стартового сигнала. Каких-то готовых решений на момент разработки продукта (2019 год) не было. В статье расскажу:
Описание конечного продукта
Клиенты попросили нас разработать приложение для практической стрельбы, которое будет параллельно показывать 2 видео, синхронизировать их по стартовому сигналу, отмечать на таймлайне выстрелы из каждого видео. Это позволит спортсменам видеть, где они совершают лишние движения между выстрелами и тратят время. Также возле видео отображается время совершения каждого выстрела. Интерфейсно в приложении это выглядело так:
Подробнее именно с продуктовой точки зрения можно почитать (и посмотреть видео) тут.
Требования к распознаванию: должно работать офлайн на iOS-девайсе, требуется очень высокая точность распознавания времени выстрела (не более 50 ms ошибки).
Работа над решением
Задача решалась только через обработку звука. Решить ее через обработку видео не представлялось возможным, т.к. стрелок может быть вообще не виден (находиться за препятствием). Кроме того, на видео никак не видно стартовый сигнал, т.е. обрабатывать звук потребовалось бы в любом случае.
Для начала поймем, что есть выстрел и что есть стартовый сигнал.
Стартовый сигнал — это равномерный звуковой сигнал на частоте примерно 2 kHz, но приборы, дающие сигнал, бывают разные, поэтому и частота может меняться. Продолжительность сигнала — примерно секунда. На площадке звуков, похожих на него, почти нет.
Выстрел — громкий звук с некоторой продолжительностью, может быть секунда, может быть дольше, постепенно затухающий. Выстрелов может быть несколько подряд. Нам важно находить именно начало звука выстрела, т.е. момент, когда выстрел был совершен. В том числе, если было несколько выстрелов подряд, нужно найти начало каждого выстрела. Похожих на выстрел звуков на площадке может быть много (любой стук, лязг оружия).
Готовые решения
Как я уже говорил, в 2019 году, когда мы работали над проектом, готовых решений не нашлось — и для задачи распознавания выстрелов, и в целом чтобы находить точный момент начала какого-то конкретного звука на длинной аудиодорожке.
Данные, pt. 1
В начале работы данных было мало — несколько роликов мы получили от клиентов и скачали с YouTube, а также нашли датасеты со звуками выстрелов, но там их было немного и звуки были не изолированы, т.е. в одной дорожке могла быть тишина, потом звук выстрела, снова тишина, и иногда снова выстрел, иногда очередь.
Baseline, решение без ML
В качестве базового решения мы просто попробовали находить выстрелы по громкости и делать отсечку по затуханию. Решение показало себя очень ненадежным:
Непросто подобрать порог: иногда, если человек уходит подальше от камеры, звуки становятся сильно тише; разное оружие стреляет с разной громкостью, плюс по-разному звучит в помещении и на улице — единый порог заранее не подобрать.
Часто есть много посторонних громких звуков: речь человека с камерой, лязг оружия, выбрасывание обоймы — много ложных срабатываний.
Это не помогало искать стартовый сигнал.
Решение с ML
Почти все решения с распознаванием аудио работают поверх спектрограммы, так что и тут обработка аудио быстро перешла в формат работы через спектрограмму. Для справки: спектрограмма — это по сути 2д массив, показывающий, какая была громкость на какой частоте в каждый момент времени.
Спектрограмма получалась через библиотеку librosa.
Видео с выстрелами было немного, но в них самих выстрелов было немало. Плюс были аудиодорожки, где были выстрелы среди тишины.
Решили нарезать звуков выстрелов через программу Audacity, сделать синтетические дорожки, где есть выстрелы поверх, например, говорящего или бегущего человека или поверх музыки, а также есть стартовые сигналы, и на этом обучить сеть.
Первая версия сети
Изначально была опробована следующая архитектура: сеть получала на вход 2д массив размера 9х1025, где 9 — это количество бинов времени (т.е. горизонтальных элементов на графике спектрограммы; один бин — это, например, 0,2 секунды в зависимости от параметров преобразования звука в спектрограмму), а 1025 — количество бинов частоты (вертикальные на графике). Задача сети была предсказать, является ли центральный бин началом выстрела, стартовым сигналом или фоном.
Архитектура сети была сверточной, с постепенным снижением вертикальной размерности за счет MaxPooling (размерность по времени не снижалась).
В конце была классификация на три класса: фон, начало выстрела, стартовый сигнал.
Метрики
Для первой архитектуры метрики считались как для задачи классификации конкретного момента времени (т.е. является ли центр началом выстрела) и по цифрам выглядели неплохо: f-мера для выстрелов была примерно 92%.
Результат первой архитектуры
Она была внедрена в мобильное приложение на iOS и работала в офлайне через CoreML. Каких-то сложностей с внедрением этой архитектуры не было.
Проблемы
Много false positives на реальных роликах: обучение было на синтетических данных, и на них же считались метрики; разметки настоящих видео со стрельб не было. И тут возникли проблемы: звуки, похожие на выстрелы, вроде лязга оружия, воспринимались за ложноположительные результаты.
Низкая точность нахождения начала выстрела: хотя сами выстрелы находились неплохо, время старта не определялось точно. Причина этого, скорее всего, была в том, что, вырезая выстрелы из Audacity, мы не суперточно обрезали их начало — просто потому что инструмент для этого неудобен. В результате сеть научилась детектировать в качестве старта выстрела не совсем старт, а что-то рядом.
Долгое время работы на устройстве: так как для каждого момента времени нужно было прогнать сетку, обработка была долгой: около минуты на 20-секундный ролик.
Данные, pt. 2: инструмент для разметки
Чтобы разобраться с первыми двумя проблемами, было решено собрать больше роликов от клиента и разметить именно их, при этом разметить очень точно. Т.е. в реальной звуковой дорожке знать, где находятся выстрелы, где — стартовый сигнал. Это должно было снизить количество false positives за счет того, что реальные похожие на выстрелы звуки будут присутствовать на аудио и будут размечены как фон, а точность вырастет за счет точной разметки начала выстрела. Также решено было размечать не только начало выстрела, но и последующее затухание.
Клиент в этот момент передал жесткий диск, где была примерно тысяча роликов с тренировок — достаточно много.
Чтобы сделать такую разметку, потребовалось написать свой инструмент разметки. Он работал в браузере, сервер был написан на Flask.
В инструменте одно видео было в двух плеерах, был таймлайн с размеченными выстрелами, звуковая волна, тоже с размеченными выстрелами, и спектрограмма. На спектрограмме или звуковой волне можно было мышкой обвести область и нажать на нужную кнопку на клавиатуре — и тогда она сохранялась как выстрел, стартовый сигнал или похожий на выстрел звук. Из видеоплееров один просто показывал видео, и оно синхронизировалось с таймлайнами, а второй показывал только выделенные области — это позволяло не сбрасывать основной таймлайн при просмотре области.
Результатом разметки был как файл для аудиодорожки с отметками конкретных мест (тут выстрел, тут стартовый сигнал), так и отдельные нарезанные звуки, чтобы впоследствии их можно было использовать для генерации дорожек.
Инструмент был удобен и позволил быстро разметить множество видео.
Разметили мы 30 видео и придумали, как модифицировать архитектуру так, чтобы работа сети была больше похожа на ручную работу по разметке. То есть идея архитектуры возникла, когда мы размечали области на спектрограммах.
Вторая версия сети
Новая архитектура состояла из 1д сверток, за которыми следовала BiLSTM. Один из вариантов архитектуры показан на скриншоте:
1д свертки понижали частотную размерность, учитывая соседние по времени столбики. Временная размерность не менялась до самого конца.
В конце была классификация на 4 класса:
фон;
стартовый сигнал;
начало выстрела;
остальная часть выстрела (затухание).
Начало выстрела помечалось не одним, а пятью подряд идущими элементами. Это должно было чуть упростить для сети сильную несбалансированность классов, не сбивая при этом точность нахождения начала, плюс помочь понимать, что если что-то похоже на начало, то и соседние элементы должны быть началом.
Остальная часть выстрела функционально была не нужна, но логика за ней была в том, что сети проще определить, где начало выстрела, когда она явно умеет находить его продолжение. При этом продолжение выстрела было долгим и такого сильного дисбаланса классов с ним не было.
Первая версия такой сети была обучена на небольшой части размеченных данных — около 30 роликов. После этого она была интегрирована в инструмент для разметки и стала подсказывать и показывать, что считает выстрелом. Это позволило:
понимать, насколько хорошо работает сеть на глаз — видеть, что она считает выстрелом, началом выстрела, стартовым сигналом;
ускорить разметку — если сеть правильно находила выстрел, достаточно было нажать на него и после нажать на клавишу, сохранявшую выстрел. Если сеть ошибалась, то ее ответ можно было удалить.
Таким образом было в итоге размечено примерно 120 роликов.
Еще столько же было сгенерировано: брались нарезанные звуки выстрелов и стартовых сигналов, вставлялись поверх фоновой дорожки (бег, музыка, подкасты).
На таком датасете была обучена итоговая сеть.
С таким подходом мы решили проблемы предыдущей версии:
стало мало false positives, так как сложные звуки из оригинальных дорожек были сохранены и показаны сети;
точность детекции начала выстрела стала высокой, так как инструмент позволил очень точно размечать начало выстрела на спектрограмме — его легко увидеть;
после портирования на девайс эта сеть работала намного быстрее.
Итоговые метрики
Метрики распознавания выстрелов считались с учётом того, что начало выстрела, определённое сетью, не должно отличаться от реального начала выстрела больше чем на 50 миллисекунд.
Точность распознавания составила 99,1%.
Полнота распознавания 97,8%.
Точность — вероятность того, что найденный выстрел — действительно выстрел.
Полнота — вероятность того, что существующий выстрел будет найден.
Портирование на iOS
Сеть была портирована на iOS и работала в офлайне. В отличие от предыдущей версии, эта версия работала гораздо быстрее и точнее. Была сложность с тем, что в CoreML было ограничение на длину последовательности в LSTM — если запись была примерно с минуту, появлялась ошибка. Ее решили простым разбиением аудиозаписи на n частей и склейкой ответа.
Также после портирования выяснилось, что модель плохо работает на данных, полученных с отличным от тех, на которых она обучена, sample rate (44 kHz vs 22 kHz), и это несмотря на то, что после получения спектрограммы разницы по идее быть не должно. Поэтому итоговая модель была обучена на разных вариантах sample rate (22 kHz, 44 kHz, 11 kHz). Это позволило ей обобщиться и нормально воспринимать данные с девайса.
Итог
Мы сделали приложение, которое очень точно находит стартовые сигналы и время выстрелов, и попутно разработали удобный инструмент для разметки звуков в сочетании с видео.