На заре машинного обучения большинство решений выглядели очень странно, обособленно и необычно. Сегодня множество ML алгоритмов уже выстраиваются в привычный для программиста набор фреймворков и тулкитов, с которыми можно работать, не вдаваясь в детали их реализации.
К слову, я противник такого поверхностного подхода, но для своих коллег хотел бы показать, что эта отрасль движется семимильными шагами и нет ничего сложного, чтобы применять ее наработки в продакшен проектах.
Для примера я покажу, как можно помочь пользователю найти нужный видеоматериал среди сотен других в нашем сервисе документооборота.
В моем проекте пользователи создают и обмениваются сотнями различных материалов: текстом, картинками, видеороликами, статьями, документами в различных форматах.
Поиск по документам представляется достаточно просто. Но что делать с поиском по мультимедиа контенту? Для полноценного сервиса пользователя надо обязать заполнить описание, дать название видеоролику или картинке, не помешает несколько тегов. К сожалению, далеко не все хотят тратить время на подобные улучшения контента. Обычно пользователь загружает ссылку на youtube, сообщает что это новое видео и нажимает сохранить. Что же делать сервису с таким “серым” контентом. Первая идея — спросить у YouTube? Но YouTube тоже наполняют пользователи (часто это один и тот же пользователь). Часто видеоматериал может быть и не с Youtube сервиса.
Так мне пришла идея научить наш сервис “слушать” видеоролик и самостоятельно “понимать”, о чем он.
Признаюсь, эта идея не нова, но сегодня для ее реализации не надо иметь штат из десяти Data Scientist-ов, достаточно два дня и немного аппаратных ресурсов.
Постановка задачи
Наш микросервис, назовем его Summarizer, должен:
- Скачать видеоролик с медиа сервиса;
- Извлечь аудио дорожку;
- Прослушать аудио дорожу, собственно Speech to text;
- Найти 20 ключевых слов;
- Выделить одно предложение из текста, которое максимально могло бы раскрыть суть ролика;
- Все результаты отправить на сервис контента;
Реализацию доверим Python, так не придется заниматься интеграцией с готовыми ML решениями.
Шаг первый: аудио в текст.
Для начала установим все необходимые компоненты.
pip3 install wave numpy tensorflow youtube_dl ffmpeg-python deepspeech nltk networkx
brew install ffmpeg wget
Далее скачаем и распакуем натренированную модель для Speech to text решения от Mozilla — Deepspeech.
mkdir /Users/Volodymyr/Projects/deepspeech/
cd /Users/Volodymyr/Projects/deepspeech/
wget https://github.com/mozilla/DeepSpeech/releases/download/v0.3.0/deepspeech-0.3.0-models.tar.gz
tar zxvf deepspeech-0.3.0-models.tar.gz
Команда из Mozilla создала и натренировала достаточно хорошее решение, которое, используя TensorFlow, может превращать длинные аудиоролики в большие куски текста с высоким качеством. Также TensorFlow позволяет вам «из коробки» работать как на CPU, так и на GPU.
Наш код будет начинать со скачивания контента. В этом ему поможет замечательная библиотека youtube-dl, которая имеет встроенный постпроцессор, способный конвертировать видео в необходимый формат. К сожалению, код постпроцессора немного ограничен, он не умеет делать ресемплинг, так что мы ему поможем.
На вход Deepspeech необходимо подать аудиофайл с монодорожнкой и семплрейтом 16K. Для этого надо повторно обработать наш полученный файл.
_ = ffmpeg.input(youtube_id + '.wav').output(output_file_name, ac=1, t=crop_time, ar='16k').overwrite_output().run(capture_stdout=False)
В этой же операции мы можем и ограничить продолжительность нашего файла, передав дополнительный параметр “t”.
Загружаем deepspeech модель.
deepspeech = Model(args.model, N_FEATURES, N_CONTEXT, args.alphabet, BEAM_WIDTH)
При помощи библиотеки wave извлекаем фреймы в формате np.array и передаем их на вход deepspeech библиотеки.
fin = wave.open(file_name, 'rb')
framerate_sample = fin.getframerate()
if framerate_sample != 16000:
print('Warning: original sample rate ({}) is different than 16kHz. Resampling might produce erratic speech recognition.'.format(framerate_sample), file=sys.stderr)
fin.close()
return
else:
audio = np.frombuffer(fin.readframes(fin.getnframes()), np.int16)
audio_length = fin.getnframes() * (1/16000)
fin.close()
print('Running inference.', file=sys.stderr)
inference_start = timer()
result = deepspeech.stt(audio, framerate_sample)
Через некоторое время, пропорциональное вашим аппаратным ресурсам, вы получите текст.
Шаг второй: поиск “смысла”
Для поиска слов, описывающих полученный текст я буду использовать метод графов. Данный метод основан на расстоянии между последовательностями. Сперва мы находим все “уникальные” слова, представим, что это вершины нашего графа. После, проходясь по тексту “окном” заданной длины, находим расстояние между словами, они-то и будут ребрами нашего гафа. Добавленные в граф вершины могут быть ограничены синтаксическими фильтрами, которые выбирают только лексические единицы определенной части речи. Например, можно рассматривать только существительные и глаголы для добавления в граф. Следовательно, мы будем строить потенциальные ребра, основываясь только на отношениях, которые могут быть установлены между существительными и глаголами.
Оценка, связанная с каждой вершиной, устанавливается на начальное значение 1, и запускается алгоритм ранжирования. Алгоритм ранжирования заключается в «голосовании» или «рекомендации». Когда одна вершина связана с другой, она“голосует” за эту (связанную) вершину. Чем выше число голосов, поданных за вершину, тем выше важность этой вершины. Более того, важность вершины для голосования определяет, насколько важным является само голосование, и эта информация также учитывается моделью ранжирования. Следовательно, оценка, связанная с вершиной, определяется на основе голосов, поданных за нее, и оценки вершин, подающих эти голоса.
Предположим, у нас есть граф G=(V, E), описанный вершинами V и ребрами E. Для данной вершины V пусть будет множество вершин E, которые связаны с ней. Для каждой вершины Vi существует In(Vi) вершин, связанных с ней, и Out(Vi) вершин, с которыми связана вершина Vi. Таким образом, вес вершины Vi можно представить по формуле.
Где d — фактор затухания / подавления, принимающий значение от 1 до 0.
Алгоритм несколько раз должен проитерировать граф для получения приблизительных оценок.
После получения приблизительной оценки для каждой вершины в графе вершины сортируются в порядке уменьшения оценки. Вершины, которые окажутся в начале списка, и будут нашими искомыми ключевыми словами.
Максимально релевантное предложение в тексте находится путем нахождения среднего от сложения оценок всех слов в предложении. То есть складываем все оценки и делим на количество слов в предложении.
iMac:YoutubeSummarizer $ cd /Users/Volodymyr/Projects/YoutubeSummarizer ; env "PYTHONIOENCODING=UTF-8" "PYTHONUNBUFFERED=1" /usr/local/bin/python3 /Users/Volodymyr/.vscode/extensions/ms-python.python-2018.11.0/pythonFiles/experimental/ptvsd_launcher.py --default --client --host localhost --port 53730 /Users/Volodymyr/Projects/YoutubeSummarizer/summarizer.py --youtube-id yA-FCxFQNHg --model /Users/Volodymyr/Projects/deepspeech/models/output_graph.pb --alphabet /Users/Volodymyr/Projects/deepspeech/models/alphabet.txt --lm /Users/Volodymyr/Projects/deepspeech/models/lm.binary --trie /Users/Volodymyr/Projects/deepspeech/models/trie --crop-time 900
Done downloading, now converting ...
ffmpeg version 4.1 Copyright (c) 2000-2018 the FFmpeg developers
built with Apple LLVM version 10.0.0 (clang-1000.11.45.5)
configuration: --prefix=/usr/local/Cellar/ffmpeg/4.1 --enable-shared --enable-pthreads --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gpl --enable-libmp3lame --enable-libopus --enable-libsnappy --enable-libtheora --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-libxvid --enable-lzma --enable-opencl --enable-videotoolbox
libavutil 56. 22.100 / 56. 22.100
libavcodec 58. 35.100 / 58. 35.100
libavformat 58. 20.100 / 58. 20.100
libavdevice 58. 5.100 / 58. 5.100
libavfilter 7. 40.101 / 7. 40.101
libavresample 4. 0. 0 / 4. 0. 0
libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100
Guessed Channel Layout for Input Stream #0.0 : stereo
Input #0, wav, from 'yA-FCxFQNHg.wav':
Metadata:
encoder : Lavf58.20.100
Duration: 00:17:27.06, bitrate: 1536 kb/s
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 48000 Hz, stereo, s16, 1536 kb/s
Stream mapping:
Stream #0:0 -> #0:0 (pcm_s16le (native) -> pcm_s16le (native))
Press [q] to stop, [?] for help
Output #0, wav, to 'result-yA-FCxFQNHg.wav':
Metadata:
ISFT : Lavf58.20.100
Stream #0:0: Audio: pcm_s16le ([1][0][0][0] / 0x0001), 16000 Hz, mono, s16, 256 kb/s
Metadata:
encoder : Lavc58.35.100 pcm_s16le
size= 28125kB time=00:15:00.00 bitrate= 256.0kbits/s speed=1.02e+03x
video:0kB audio:28125kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000271%
Loading model from file /Users/Volodymyr/Projects/deepspeech/models/output_graph.pb
TensorFlow: v1.11.0-9-g97d851f04e
DeepSpeech: unknown
Warning: reading entire model file into memory. Transform model file into an mmapped graph to reduce heap usage.
2018-12-14 17:42:03.121170: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX2 FMA
Loaded model in 0.5s.
Loading language model from files /Users/Volodymyr/Projects/deepspeech/models/lm.binary /Users/Volodymyr/Projects/deepspeech/models/trie
Loaded language model in 3.17s.
Running inference.
Building top 20 keywords...
{'communicate', 'government', 'repetition', 'terrorism', 'technology', 'thinteeneighty', 'incentive', 'ponsibility', 'experience', 'upsetting', 'democracy', 'infection', 'difference', 'evidesrisia', 'legislature', 'metriamatrei', 'believing', 'administration', 'antagethetruth', 'information', 'conspiracy'}
Building summary sentence...
intellectually antagethetruth administration thinteeneighty understanding metriamatrei shareholders evidesrisia recognizing ponsibility communicate information legislature abaddoryis technology difference conspiracy repetition experience government protecting categories mankyuses democracy campaigns primarily attackers terrorism believing happening infection seriously incentive upsetting testified fortunate questions president companies prominent actually platform massacre powerful building poblanas thinking supposed accounts murdered function unsolved perverse recently fighting opposite motional election children watching traction speaking measured nineteen repeated coverage imagined positive designed together countess greatest fourteen attacks publish brought through explain russian opinion winking somehow welcome trithis problem looking college gaining feoryhe talking ighting believe happens connect further working ational mistake diverse between ferring
Inference took 76.729s for 900.000s audio file.
Итоги
Идеи, которые приходят вам в голову, сегодня можно реализовать намного быстрее, чем еще три-четыре года назад. Пробуйте, экспериментируйте! Я думаю, что artificial intelligence — это в первую очередь intelligence инженеров, которые с ним работают.
Код доступен в моем Github репозитории.
Автор: Roaming