Нормализация текста в задачах распознавания речи

При решении задач, связанных с распознаванием (Speech-To-Text) и генерацией (Text-To-Speech) речи важно, чтобы транскрипт соответствовал тому, что произнёс говорящий — то есть реально устной речи. Это означает, что прежде чем письменная речь станет нашим транскриптом, её нужно нормализовать.


Другими словами, текст нужно провести через несколько этапов:


  • Замена числа прописью: 1984 год -> тысяча девятьсот восемьдесят четвёртый год;
  • Расшифровка сокращений: 2 мин. ненависти -> две минуты ненависти;
  • Транскрипция латиницы: Orwell -> Оруэлл и т.д.

Normalization


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


Как вишенка на торте, мы решили выложить наш нормализатор на базе seq2seq в открытый доступ: ссылка на github. Он максимально прост в использовании и вызывается одним методом:


norm = Normalizer()
result = norm.norm_text('С 9 до 11 котики кушали whiskas')

>>> 'С девяти до одиннадцати котики кушали уискас'

Подробнее про задачу


Так в чём же, собственно, проблема? Сокращения на то и сокращения, что их можно легко привести к начальной форме. На самом деле, тут всё не так однозначно и без интуиции носителя языка во всех нюансах тяжело разобраться.


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


  • второе(ые), но д. 2едва е;
  • 2 частидве части, но нет 2 части — нет второй части;
  • длиной до 2 км — длиной до двух километров, но едем до 2 км — едем до второго километра;
  • = 2/5 — равно две пятых, но д. 2/5 — дом два дробь пять или даже — два пять.

Не меньше проблем с расшифровкой сокращений: одна и та же аббревиатура может читаться по-разному в зависимости от контекста(ггород или год) и человека(БЦб ц или бизнес центр?). Думаю, что творится с транскрипцией других языков, вы уже и сами догадались. Особенно остро проблема стоит с обработкой разговорной речи.


Статистическая модель


Во всем этом многоообразии легко потеряться и уйти в бесконечный цикл с поиском и обработкой всё новых и новых кейсов. В какой-то момент лучше остановиться и вспомнить о принципе Парето. Вместо того, чтобы решать задачу в общем виде, мы можем обработать ~20% самых частотных случаев, но покрыть ~80% языка.


В первых релизах Open_STT мы подошли к решению ещё брутальней: видим число — заменяем его на дефолтное количественное числительное. С точки зрения STT такое решение было даже оправданным, всё-таки 2020 год лучше превратить в две тысячи двадцать год и ошибиться только в одном символе, чем проигнорировать целых три слова.


Постепенно мы подкрутили ещё контекстозависимость. Теперь числительные перед словом год становились порядковыми и 2020 год наконец превратилось в две тысячи двадцатый. Так появился наш "ручной" статистический пайплайн — находим наиболее популярные комбинации и добавляем их в набор правил.


Sequence to Sequence модели


В какой-то момент стало понятно, что архитектура sequence-to-sequence (seq2seq) идеально подходит под описание нашей задачи. Действительно, seq2seq умеет делать всё то же самое, что и "ручной" пайплайн, и даже больше:


  • Преобразует одну последовательность в другую — чек;
  • Учитывает контекст — чек;
  • Сам статистически определяет правила, по которым последовательность преобразуется — чек;

Attention


График attention весов для готовой модели на последовательности "5 января". Можно заметить, что для генерации окончания "пятОГО" модель учитывает не только "5", но и последующие символы из слова "января".

В качестве основы мы взяли реализацию модели seq2seq на PyTorch отсюда. Не будем подробно останавливаться на архитектуре — лучше прочитайте исходный пост. На вход подавалась последовательность символов из словаря русских букв + латиница + пунктуация + спец токены, на выходе — только русские буквы + пунктуация.


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


Так, для обучающей выборки мы замиксовали:


  • Нормализованные данные из открытых источников — Russian Text Normalization;
  • Отфильтрованные данные со случайных веб-сайтов, прогнанные через наш ручной пайплайн.;
  • Парочку аугментаций, чтобы сделать сетку более устойчивой к выбросам — пунктуация и пробелы в рандомных местах, капитализация, длинные числа и т.д.

Тестим TorchScript


Нашей целью было не только решение задачи, но и тест разных интересных вещей, одним из которых стал Torchscript.


TorchScript — это замечательный инструмент из PyTorch, с помощью которого можно выкатить свою модель далеко за рамки Python и даже встроить в C++.


Если коротко, PyTorch даёт нам на выбор два пути:


  1. Хардкорный, с полным погружением в пучину TorchScript языка, со всеми вытекающими последствиями;
  2. Готовый инструмент torch.jit.scripttorch.jit.trace), работающий из коробки.

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


Примеры работы


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


  • norm.norm_string("Вторая по численности группа — фарсиваны — от 27 до 38 %.")

'Вторая по численности группа — фарсиваны — от двадцати семи до тридцати восьми процентов.'


  • norm.norm_string("Висенте Каньяс родился 22 октября 1939 года")

'Висенте Каньяс родился двадцать второго октября тысяча девятьсот тридцать девятого года'


  • norm.norm_string("играет песня «The Crying Game»")

'играет песня «зэ краинг гейм»'


  • norm.norm_string("к началу XVIII века")

'к началу восемнадцатого века'


  • norm.norm_string("и в 2012 году составляла 6,6 шекеля")
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 8

    0
    Оффтоп. Кто-то знает, почему гугл-переводчик иногда «переводит»: 1984 -> тысяча девятьсот восемьдесят четвёртый год?
      0
      Потому что там теперь нейросети и порой вместо перевода получаются какие-то его фантазии.
      +2
      Похоже на строчке «norm.norm_string(»и в 2012 году составляла 6,6 шекеля")"
      сработал Fatal Exception, т.к. на ней статья обрывается :)
        +2
        Возможно на этой строчке у автора наступила суббота. И таки да, статья обрезанная получилась.
        –2
        Я, конечно, не знаток, но нас учили, что числа до 10 лучше записывать строкой, а все, что выше, желательно в виде цифр.
        5,45% в виде строки довольно неудобно читать :)
          +3
          Читать разумеется удобней, но тут предполагается текста для обучения систем распознавания речи, а там как раз предпочтительней, чтобы текст в точности соответствовал тому, что было произнесено, без сокращение и прочего
          0
          Очень интересно, спасибо!
          А где Вы используете Ваши наработки более конкретно?
            +1

            Используем как один из этапов предобработки текста в открытом датасете русской речи Open_STT. Про сам датасет можно подробнее почитать в статье на Хабре.

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

          Самое читаемое