
Мы наконец решили задачу омографов. Конечно, с рядом оговорок, куда без них. Получилось пресловутое приключение на 20 минут.
Несмотря на кажущуюся простоту (задача по сути является бинарной классификацией, число кейсов с тремя валидными вариантами ничтожно мало), задача является просто кладезем различных "мин замедленного действия" и типичных граблей в сфере машинного обучения. Да, задачу "ёфикации" (расстановка буквы ё там, где люди её поленились поставить) мы считаем частным случаем задачи простановки ударений и омографов.
Также мы опубликовали наше продуктовое решение для простановки ударений (в омографах в том числе) в рамках репозитория silero-stress и также напрямую через pypi
. В ближайшее время добавим эту модель и обновим наши публичные модели синтеза и раскатим более мощную "большую" (тоже маленькую по современным меркам) версию модели в приватные сервисы и для клиентов. Также мы опубликовали бенчмарки качества и скорости публичных академических решений … и там всё очень неоднозначно.
Наливайте себе чай, садитесь поудобнее. Мы постараемся описать наш путь длиной в вечность без лишних подробностей.
Тут оглавление статьи c якорями
Что такое омографы?
Омографы (графические омонимы) — слова, совпадающие по написанию, но различные по звучанию и значению, например, «за́мок» и «замо́к». Если коротко, омографы это вот такие слова:
Что такое омографы?

То есть простыми словами - на письме слова пишутся одинаково (а у нас довольно "фонетический" алфавит, если знать ударение), но ударение меняет смысл слова. Зачастую особенно старые говорилки грешат "упоротыми" ударениями в стиле "кожаных ублюдков". На момент, когда последний раз это проверяли, к примеру Яндекс хорошо разбирал омографы, а Сбер - практически не разбирал, так что проблема всё ещё актуальна, даже для компаний без каких-либо ограничений на бюджеты.
Также существуют ещё и омофоны, омоформы и полные омонимы, но с точки зрения синтеза речи интерес представляют только омографы.
Почему по-настоящему решить задачу омографов сложно

Видя, какая задача предстоит, мы не торопились с решением и раза 3 или 4 заходили на повторный круг, это даже стало у нас своеобразным мемом, Габен не даст соврать.
Почему же эта задача сложная? Есть пять основных причин:
Сами омографы с первого взгляда кажутся задачей бинарной классификации. Есть условно два класса - «за́мок» и «замо́к». Но это только лишь кажется. На самом деле это задача, где классов N * 2, где N это число рассматриваемых слов. И тут легко наступить на типичные грабли мало понимающего в ML человека - рассматривать задачу просто как классификацию на 2 класса и целевой метрикой считать точность на корпусе в среднем. Почему так - ниже в отдельном разделе;
Сами омографы как правило (за минусом десятка сверхчастотных) довольно редкие слова ... и второй вариант омографа как правило ещё на порядок реже встречается в тексте. Всего омографов где-то около 15 тысяч (30 тысяч вариантов), но хоть сколько-то частотных всего около 2.5 тысяч (5 тысяч вариантов);
В эпоху LLM и терабайтных публичных корпусов ... не существует качественных публичных данных с омографами и решений для их простановки (почему текущие публичные решения ими не являются - будет в отдельной главе). Единственный хоть как-то подходящий корпус это подкорпуса НКРЯ, но он по сути как бы "приватизирован" Яндексом, доступ к нему получить проблематично, да и качество данных там очень посредственное на самом деле именно для задачи решения омографов (лучше вообще не использовать их для обучения);
Сама суть задачи идёт вразрез с развитием языка. Носителям языка и так всё "понятно" (как и с буквой ё), язык как правило развивается по принципу достаточности и языковой экономии. Наше морфологическое письмо достаточно оптимально решает задачу написать понятно для носителя языка ... если конечно знать ударение;
Во многих популярных языках проблема омографов стоит гораздо менее остро. Например в английском они существуют (например слово accent - в зависимости от ударения или существительное или глагол), но их количество и встречаемость их на порядок ниже. Не будем погружаться в политику, но если проблемы "нет" в английском (там есть другие проблемы), значит внимания на неё будет на порядок меньше;
В сухом остатке получаем вроде бы простую задачу (с учетом современных инструментов), но с таким количеством нюансов, что тут легко пройти по полю граблей и не дойдя даже до середины объявить, что "получена точность 96% и задача решена". Но это не наш путь {тут обязательная ремарка про отношение шума к сигналу на Хабре}.
Сколько всего омографов и описание нашего датасета
Всего омографов около 15 тысяч (30 тысяч вариантов). Но если сделать банальную отсечку по частотности (оставить только омографы, использование которых позволяет покрыть 99% всех предложений), то останется порядка 5.6 тысяч омографов (~11 тысяч вариантов). Всего омографы можно разбить на такие категории:
Частотные омографы. Примеры:
+уже
/уж+е
,вс+е
/вс+ё
;Слова, которые формально являются омографами, но на практике допустимы и равнозначны оба варианта. Примеры:
тв+орог
/твор+ог
,щ+авель
/щав+ель
,зв+онишь
/звон+ишь
(да простят нас учителя русского языка, но с точки зрения реальности, в которой мы живём, это допустимые варианты);Слова, которые фигурировали в публичных списках омографов, но по факту омографами не являются или скорее всего вам никогда не встретятся. Примеры:
+еды
/ед+ы
,жел+езами
/желез+ами
;Слова, которые являются омографами, но они настолько редкие, что собирать качественные данные с ними проблематично и имеет мало смысла. Пример:
т+акая
(от слова "т+акать" - говорить "так-так-так") /так+ая
;
В итоге мы выделили список из:
5,630 слов омографов (~11.3 тыс. вариантов);
3,473 слов, которые мы не считаем омографами и просто ставим в них одно ударение (которое нам больше нравится);
2,157 слов, которые мы считаем омографами, и будем решать.
Тем не менее, процесс сбора данных для классификатора омографов с нуля - крайне трудозатратный и времязатратный. На данный момент из заявленных 2,157 слов мы сейчас поддерживаем 1,924 слова. Остальные вероятно получится добавить в следующих обновлениях. Всего сейчас в пайплайнах обработки данных находится суммарно где-то 3,500 слов на разных этапах.
Тут нужно сделать важную оговорку, что существуют "настоящие" омографы (например, з+ападу
/ запад+у
, к+урим
/ кур+им
, т+акая
/ так+ая
), у которых настолько большая разница в частотности и настолько редкие "контрпримеры", что precision должен быть выше 95%, а иногда и выше 99%, чтобы оправдать использование модели. На практике такие слова проще переводить в категорию "слов, которые мы не считаем омографами".
Такое исключение имеет под собой и другое практическое основание. Учитывая, что в продакшене будет практически незаметна (если не вредна) классификация слов наподобие такая
, нет никакого смысла "замедлять" свой алгоритм, увеличивая размер инпута для классификатора (скорее тут повышается "хрупкость" на пустом месте).
Из большого числа источников (включая ручную разметку с тройным покрытием), мы собрали следующий датасет:
Всего 122M предложений;
В среднем 60 тыс. предложений на омограф, медиана 11 тыс. предложений на омограф;
В среднем 30 тыс. предложений на вариант, медиана 3.2 тыс. предложений на вариант;
Гистограмма количества слов на один омограф:
Гистограмма количества слов на один омограф

Гистограмма количества слов на один вариант:
Гистограмма количества слов на один вариант

Нетрудно догадаться, что датасет является очень несбалансированным, как по количеству примеров на один омограф, так и по распределению вариантов внутри одного омографа. Отсюда сразу довольно очевидны первые "детские" грабли, на которые можно наступить при решении задачи. В качестве целевой метрики рассматривать только точность на всём датасете. Если максимизировать только эту метрику, можно получить решение … которое на большом количестве доменов бьётся … тупо выбором самого частотного варианта. Отчасти это чем-то похоже на предсказание того, что у человека никогда нет рака. Точность будет 99%, но вот пользы от такого классификатора будет мало.
Наглядный пример дисбаланса

Метрики качества
model |
pr |
re |
f1 |
word_acc |
total_acc |
---|---|---|---|---|---|
silero-stress |
0.84 |
0.9 |
0.85 |
0.92 |
0.93 |
silero-stress-private |
0.9 |
0.96 |
0.91 |
0.96 |
0.96 |
RUAccent-tiny2.1 |
0.65 |
0.61 |
0.56 |
0.69 |
0.78 |
RUAccent-turbo3.1 |
0.7 |
0.7 |
0.64 |
0.76 |
0.84 |
omogre |
0.47 |
0.56 |
0.46 |
0.67 |
0.73 |
baseline |
0.39 |
0.5 |
0.43 |
0.77 |
0.85 |
baseline
- наивный "классификатор" омографов по принципу "всегда ставь самый частый вариант".
silero-stress-private
- закрытая модель с более высоким качеством.
Методология подробно описана тут.
В целом тут нужно понимать только, что F1
независима от частотности слов и вариантов, и показывает точность классификации "в вакууме", а total_acc
сильно зависит от точности на самых частых вариантах самых частых слов, и показывает точность в "реальных" продовых сценариях. В предельном случае, если дисбаланс составляет 100 к одному, можно получить точность 99% просто предсказывая всегда один вариант.
Гистограммы метрик по отдельным омографам

Метрики скорости работы
Методология и замечания
Замеры проводились на AMD Ryzen Threadripper 3960X, RTX 3090;
Замеры проводились на "среднестатистическом" тексте в ~400 символов, в котором есть два омографа. По сути, это один небольшой абзац произвольного текста. Десять омографов - уже скорее синтетический тест краевого случая. В реальных сценариях такой текст крайне редко будет попадаться;
silero-stress
иomogre
замерялись на одном потоке CPU;RUAccent
(какtiny2.1
, так иturbo3.1
) "из коробки" забивают целиком ВЕСЬ процессор в CPU-режиме, и ~6 потоков процессора в GPU-режиме.
Два омографа
Старинный каменный замок на вершине утёса молчаливо взирал на долину, храня вековые тайны. Мы поднимались по извилистой тропе, с каждым шагом погружаясь в прошлое. Воздух был напоён ароматом хвои и влажного камня. Наконец, мы достигли массивных врат. Дубовая дверь, окованная железом, оказалась заперта на ржавый замок. Он висел там, вероятно, не одно столетие, и у нас не было ни ключа, ни сил его сорвать.

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

Общее впечатление от публичных решений
Публичные продуктовые решения отсутствуют как класс, публичные академические решения имеют очень много проблем, которые скорее делают их относительно малополезными в сравнении с наивными альтернативами (словарь, простановка более частотного варианта);
На CPU академические решения имеют скорее отрицательную полезность:
omogre
в силу очень низкой точности,ruaccent
в силу очень низкой скорости (ударения + омографы на CPU могут работать дольше продуктового синтеза речи, и библиотека поедает ВСЕ доступные потоки процессора);На GPU
ruaccent
уже не является полностью бесполезным, но тут роль начинает играит низкое качество самой архитектуры / самой библиотеки: она удаляет некоторую пунктуацию / пробелы без какой-либо очевидной логики (в документации на эту тему пусто), даже при запуске на GPU занимает до 6-8 потоков CPU … и даже по общей точности на датасете она проигрывает по точности наивному алгоритму (ставим более частотный вариант). Судя по публикациям в социальных сетях разработчиков - они фокусировались только на средней точности на всём датасете, и никак не работали с классовыми дисбалансом, точностью и полнотой;Ещё когда-то давно был
rustress
, но он не поддерживается, и пару-тройку лет назад мы снимали метрики, и они были печальными.
Получается, что единственная библиотека с нетривиальными метриками выделяет непропорционально большие вычислительные ресурсы на вспомогательную задачу к синтезу речи. Такими темпами скоро будем решать задачи бинарной классификации с помощью нейросетей на 100 миллиардов параметров (шутка).
Запуск нашего решения
Наше решение запускается в 1 строчку путём установки из pypi
или напрямую из репозитория через torch.hub
(просто пулится с GitHub под капотом). Решение продуктовое, то есть:
Размер пакета ~50 мегабайт (архив весит около 30 мегабайт);
Решение работает на 1 потоке процессора с поддержкой AVX2 инструкций;
Выброшены все лишние зависимости, по сути есть зависимость только от PyTorch и стандартной библиотеки питона. То есть подойдут PyTorch +/- любой свежей версии, поддерживаются все версии питона начиная с
3.10
;Наше решение максимально пытается "сохранить" пунктуацию оригинального текста, не применяя к нему произвольные преобразования;
Простановка ударения в 1 слове занимает примерно 0.5 миллисекунд, простановка ударения в 1 абзаце (400 символов) с 2 омографами - порядка 30 миллисекунд;
Запуск через pypi
:
!pip install -q silero-stress
from silero_stress import load_accentor
# the number of threads is set automatically to 1
accentor = load_accentor()
sample_sent = "Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!"
print(accentor(sample_sent))
# Мен+я зов+ут Л+ёва Корол+ёв. +Я +из г+отов. +И +я уж+е гот+ов откр+ыть вс+е в+аши замк+и люб+ой сл+ожности!
Запуск через torch.hub
(по сути скачивание репозитория):
import torch
torch.set_num_threads(1)
accentor = torch.hub.load(repo_or_dir='snakers4/silero-stress', model='silero_stress')
sample_sent = "Меня зовут Лева Королев. Я из готов. И я уже готов открыть все ваши замки любой сложности!"
print(accentor(sample_sent))
# Мен+я зов+ут Л+ёва Корол+ёв. +Я +из г+отов. +И +я уж+е гот+ов откр+ыть вс+е в+аши замк+и люб+ой сл+ожности!
Наше решение проставляет ударения, ударения в омографах, расставляет букву ё и также решает омографы … с буквой ё.
Пара слов про простановку ударений
Наша библиотека может не только расставлять ударения в омографах (и решать омографы с буквой ё), ставить букву ё, но и также она проставляет ударения в обычных словах.
При этом немаловажно, что точность расстановки ударений в обычных словах составляет 100%, с точностью до имеющегося у нас словаря. Размер словаря составляет порядка 4.1М слов.
С момента прошлого публичного релиза, мы вручную разметили ещё дополнительно 60к слов словаря, и добавили ~70k новых слов, включая некоторые имена собственные, неологизмы, и прочие, пропущенные нами ранее слова. На данный момент наш словарь составляет ~4.1M словоформ, из которых 130k были перепроверены вручную.
Также немаловажно, что по обычными словам есть небольшая генерализация, то есть точность простановки ударений в неизвестных словах и "придуманных" словах и топонимах составляет порядка 60-70%.
Вывод
Мы опубликовали библиотеку silero-stress
для расстановки ударений в обычных словах и омографах, которая:
Расставляет ударения, решает омографы, ставит букву ё;
"Знает" порядка 4М русских слов и словоформ и порядка 2K омографов;
Простановка ударения в обычном 1 слове занимает где-то 0.5 ms, а в предложении на 400 символов с 2 омографами - порядка 30 ms;
Общий размер библиотеки составляет порядка 50 мегабайт (архив весит порядка 30 мегабайт), что является сжатием словарей и всех датасетов примерно в 400 раз;
Опубликована под популярной и простой лицензией (MIT);
Не содержит раздутого кода, лишних библиотек, гигабайтов академических артефактов;
Зависит только от стандартной библиотеки питона и работает на всех последних версиях PyTorch (используется как движок для ускорения нейросетей);
Ссылки
Репозиторий проекта - https://github.com/snakers4/silero-stress
Библиотека в
pypi
- https://pypi.org/project/silero-stress/
P.S. Если вы хотите, чтобы мы инкорпорировали ваши словари ударений и / или датасеты в наш инструмент - вы можете создать тикет в репозитории.
Ktilis
В ссылку тихо закралась точка с запятой...
snakers4 Автор
Поправил