В задачах распознаваниях речи при переводе аудио в текст есть дополнительные этапы, делающие этот текст более человекочитаемым. Например, предложение "привет хабр сегодня мы сделаем двадцать шесть моделей по распознаванию голоса" будет выглядеть лучше в таком виде: "Привет, хабр. Сегодня мы сделаем 26 моделей по распознаванию голоса". Другими словами, сегодня мы поговорим про то, как автоматически восстановить пунктуацию и капитализацию (сделать нужные буквы заглавными). Также упомянем денормализацию текста (при этом числа обретут свою цифровую форму обратно, эту задачу еще называют inverse text normalization).


Пунктуация и капитализация


После непродолжительного поиска выяснится, что пара решений для русского языка в этом направлении уже есть (например, модели от vosk и silero). Мы же копнем чуть глубже и разберемся как самому натренировать такую модель. Это даст нам возможность выбора знаков препинания, нужного языка (например, башкирского или чувашского) и подбора соответствующего нашему домену корпуса текстов.


Важно понимать, что так как мы работаем исключительно с текстами, то предложения типа "казнить нельзя помиловать" модель будет разрешать на основании данных, на которых она обучалась. Эти данные никак не связаны с аудио (я встречал идеи о том, как можно использовать паузы из аудио в виде признаков, но это не очень надежно, особенно в разговорной речи) и неточности обязательно будут. Нашей задачей здесь является лишь сделать текст более легким для восприятия.


Данные


Первым делом нам нужно раздобыть тренировочный корпус. Это должен быть текстовый документ с одним предложением на строку. В предложениях должны быть знаки препинания, которые мы хотим восстанавливать. Подготовить такой файл, имея на руках обычные тексты, довольно просто.


Если вы делаете модель для малоресурсного языка, то можно воспользоваться проектом Lingtrain, который я делаю для создания параллельных книг (проект открытый и идеи приветствуются). На первом этапе в нем идет разбитие всего текстового документа на предложения, после чего такой файл можно скачать.


Если же вы делаете модель для какого-то популярного языка, то можно воспользоваться готовыми датасетами типа Tatoeba. Для удобства я оформил скрипты для подготовки и обучения в репозиторий multipunct, поэтому дальше я буду обращаться к нему. Качаем его и начинаем подготовку данных.


Чтобы скачать датасет (около 500Mb) и извлечь из него предложения на русском языке нужно выполнить следующую команду:


python ./get_tatoeba.py --data_dir ./dataset --lang rus

Предложения сохранятся в файл sentences.txt. Этот файл — часть большого скачанного датасета sentences.csv, в котором находятся предложения на многочисленных языках. Параметр lang задает русский язык. Теперь нам нужно каким-то образом разметить данные, чтобы модель могла на них обучаться. Про модель мы пока не говорили, поэтому следующим шагом перейдем к ней.


Модель


Есть очень популярный фреймворк для задач ASR (automatic speech recognition или же распознавание речи) от Nvidia, который называется NeMo. В нем можно найти много полезного, нас же сейчас интересует Punctuation and Capitalization Model. Эта модель на основе BERT'а будет определять для каждого кусочка предложения (токена) и его класс — заглавная у него буква или нет и какой знак препинания после него ставить, если это нужно. Затем токены собираются воедино с учетом классов и мы получим наше готовое предложение.


Для тех, кто только интересуется машинным обучением и NLP в частности, рекомендую ознакомиться с замечательными статьями Джея Аламмара, переводы которых есть на хабре (один, два).

Важным для нас тут является то, данные мы размечаем сами (тем самым контролируя набор знаков препинания), а при обучении можно передать предтренированную модель с huggingface, что очень сильно упрощает задачу.


Разметим данные. При помощи второго скрипта из multipunct разметим наш корпус, а также разобьем его на тренировочные и валидационные части.


python ./prepare_data.py --data_dir ./dataset --num_samples 50000 --percent_dev 0.2

По умолчанию, размечаться будут знаки ".,!?", остальная пунктуация будет удалена. Изменить этот набор можно в скрипте prepare_data.py. Также в этом скрипте пунктуация немного стандартизируется, обрабатываются случаи с unicode'ными тире и т.д.


Важный момент — в скрипте есть параметр lines_to_combine. Если установить его равным двум, то на одну строку будет по два предложения. Эффектом от этого станет то, что модель будет учиться разбивать текст на несколько предложений. Выбирайте параметр в зависимости от ваших потребностей.


Разметка будет представлять из себя что-то типа такого: "OU OO ?O". Здесь символ "O" говорит, что ничего делать не нужно, "U" — первая буква должна быть заглавной, а знак препинания говорит сам за себя.


Обучение


Обучать будем в Colab'e. Так мы используем предтренированную модель (в ноутбуке это DeepPavlov/distilrubert-tiny-cased-conversational-v1), то бесплатной версии будет вполне достаточно. Не забываем выбрать GPU в меню среды выполнения. Константа PRETRAINED_BERT_MODEL задает путь к модели на huggingface, здесь можно попробовать другую модель. Перед началом тренировки загрузите размеченные выше данные в папку /data.


Далее запускаем обучение и ждём. Состояние модели будет сохраняться в checkpoint'ах, которые можно будет загружать позже. Весь дополнительный код по токенизации и постобработке выхода модели заключен внутри самой модели. Поэтому при простом ее использовании потребуется только вызвать метод add_punctuation_capitalization().


Если понадобится экспортировать модель для инференса, например, в TorchScript, то для этого есть метод export(). Обратите внимание, что экспортируется при этом только модель, выдающая логиты (числа, по которым определяются вероятности принадлежности к классам). Весь инфраструктурный код придется вытащить дополнительно.


Итак, модель натренирована, можно передавать в нее тексты в нижнем регистре без пунктуации.


queries = [
        'меня зовут сергей а как тебя',
        'закрой за мной дверь я ухожу'
    ]

results = model.add_punctuation_capitalization(queries)

for query, result in zip(queries, results):
    print(f'TEXT  : {query}')
    print(f'RESULT: {result.strip()}\n')

TEXT  : привет меня зовут сергей а как тебя
RESULT: Привет, меня зовут Сергей. А как тебя?

TEXT  : закрой за мной дверь я ухожу
RESULT: Закрой за мной дверь. Я ухожу.

Для улучшения качества попробуйте поиграться с разными моделями и сбалансируйте датасет в соответствии с вашими знаками препинания.


Текст стал выглядеть лучше, давайте теперь попробуем разобраться с переводом чисел из текстового вида в исходный.


Денормализация


Опять же, есть статьи на тему нейронных сетей и денормализации. Мы же для этой задачи воспользуемся неплохой библиотекой Word-to-Number-Russian от Kouki_RUS. Она основана на другой известной библиотеке natasha от alexanderkuk, — обязательно посмотрите, если интересуетесь обработкой текстов.


Однако сразу использовать эти скрипты для задач ASR (где текст без пунктуации) будет не очень удобно, — случаи типа "шестьсот одиннадцать два два три" будут схлапываться в "618" (парсинг работает жадно). Но так как у нас на руках есть код парсинга, а не нейронная сеть, то можно поправить логику, проделав некоторые алгоритмические упражнения. Я добавил пару проверок на разрядность чисел, чтобы понимать нужно ли прибавлять следующее число к предыдущему, а также обработку нулей. После этого имеем такой результат:


>> мой телефон девятьсот десять ноль девяносто пять пятьдесят шесть десять
>> мой телефон 910 0 95 56 10

О временных метках


Еще один важный момент — если после транскрибирования аудио у нас есть временные метки на уровне слов (по таким меткам можно извлечь отдельное слово из аудио), то после денормализации они станут неактуальными (количество слов поменяется). Нам необходимо понять, какие слова были схлопнуты в одно. Для случая, когда в тексте только одно составное число, это сделать довольно тривиально, но как быть с таким:


>> в этом предложении есть числа тридцать три двадцать пять и семь
>> в этом предложении есть числа 33 25 и 7

В исходном предложении 11 слов и 11 пар меток, в денормализованном — по 9, и граница между 33 и 25 неясна. Нужен некоторый маппинг, например, массив индексов, который бы показывал сколько слов из исходного предложения соответствуют каждому выходному. Давайте еще немного доработаем логику в этом направлении.


В результате получаем что-то типа такого:


>> в этом предложении есть числа тридцать три двадцать пять и семь
>> в этом предложении есть числа 33 25 и 7
>> [1, 1, 1, 1, 1, 2, 2, 1, 1]

>> одна тысяча восемьсот тридцать первый и тысяча девятьсот пятьдесят четвертый
>> 1831 и 1954
>> [5, 1, 4]

Чтобы попробовать самостоятельно и внести свои коррективы, предлагаю воспользоваться этим репозиторием, коммиты и баги приветствуются.


Заключение


Мы посмотрели на то как можно улучшать читаемость текста в задачах распознавания речи, но можно найти этим решениям и другие применения, — например, расстановка знаков препинания для сообщений из чатов. Подходы же в дообучении нейросетевых моделей (transfer learning) вообще являются очень общими и используются повсеместно (от генерации контента до классификации чего-бы то ни было).


P.S. Как многие увлекающиеся машинным обучением люди я тоже завел свой телеграм канал для заметок и новостей из мира ML, куда вас и приглашаю.


Ссылки


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


  1. AigizK
    11.04.2022 11:25
    +1

    Спасибо большое. А то @Christina29 и @snakers4 не делятся кодом, как именно надо тренировать ????


    1. averkij Автор
      11.04.2022 12:04
      +2

      Возможно, у коллег что-то более хитрое и оптимизированное.

      Я вижу ценность в том, что это можно попробовать для других языков, для которых таких моделей пока нет. Так что ждем результатов по башкирскому языку ????.


      1. snakers4
        13.04.2022 12:00
        +1

        @snakers4не делится кодом тренировки, потому, что ему нужно откуда-то платить зарплату сотрудникам. Сбер же прекрасно профинансировался из нашего с вами Фонда Национального Благосостояния, у них уже все хорошо.

        Если опустить очевидные риски использования "черных коробок" и зависимости от аттракционов неслыханной щедрости (к которым также можно отнести использование фреймворков, которые неизбежное зло), то могу заметить только то, что доведение моделей до нужного качества и свойств занимает обычно нетривиальное количество времени.


  1. quaer
    12.04.2022 14:36
    +1

    Детей в школе учат правилам, почему нельзя использовать чёткий алгоритм?

    Можно потом скорректировать результат, если в где-то одинаково неправильно?


    1. averkij Автор
      12.04.2022 15:34

      Детей, например, учат, что имя собственное должно быть с большой буквы. Дети понимают это и дальше сами решают, что к этому относятся. Для модели такое правило сформулировать трудно. Поэтому мы обучаем модель на корпусе текстов и она сама "учится", как писать правильно.

      С пунктуацией можно составить модель на правилах, но это будет очень сложно, -- понадобятся силы лингвистов, которые должны будут над этим работать. А если понадобится сделать модель, скажем, для венгерского, то придется искать венгерских лингвистов ???? и т.д. Гораздо проще найти корпус венгерской литературы и обучиться на нем.

      Кроме того, если у вас просто бесконечный поток речи, то тут четких правил не сделать. Опять же, лучше обучить модель на примерах и пусть она сама делит на предложения.

      Для коррекции, думаю, можно накидать каких-то эвристик, чтобы проверять что-то конкретное (первое слово с большой буквы, перед но стоит запятая и т.д.).


      1. quaer
        12.04.2022 22:32

        Данные для модели ведь всё равно надо готовить, так? То есть всё равно нужны обученные люди. Как исправляются ошибки, которые сеть будет делать?

        Сколько вообще языков в мире с различающимися правилами?


        1. averkij Автор
          12.04.2022 22:57

          Данные берутся сырые (просто тексты), из того же домена, на котором планируется использовать модель. Например, берете дамп википедии, учите на нем. Все.

          Ошибки будут, но мы в этой задаче лишь улучшаем читаемость текста, а не делаем его литературным.

          Про языки вопрос интересный. Я бы сказал, что если язык естественный, то правила и нормы у него со временем меняются, поэтому применять одни и те же законы к двум разным языкам все равно будет не надежно. Самих языков (и это смотря что под этим понимать) около 7000, думаю и правил столько же.


        1. averkij Автор
          12.04.2022 22:58

          Исключением в этом плане был бы эсперанто (и другие плановые языки), в котором нет исключений.


          1. quaer
            13.04.2022 11:54

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


            1. averkij Автор
              13.04.2022 12:07

              Я бы лично с удовольствием использовал труд лингвистов, считаю их работу очень интересной. Но тут речь о том, чтобы получить модель, имея на руках данные, которые можно быстро найти в интернете.

              А лингвистов ещё пойди найди в нужном количестве.


              1. quaer
                13.04.2022 21:12

                А как это работает в Word, предлагая корректировку при вводе текста?