Мы решили задачу омографов и ударений в русском языке
- среда, 15 октября 2025 г. в 00:00:19
Мы наконец решили задачу омографов. Конечно, с рядом оговорок, куда без них. Получилось пресловутое приключение на 20 минут.
Несмотря на кажущуюся простоту (задача по сути является бинарной классификацией, число кейсов с тремя валидными вариантами ничтожно мало), задача является просто кладезем различных "мин замедленного действия" и типичных граблей в сфере машинного обучения. Да, задачу "ёфикации" (расстановка буквы ё там, где люди её поленились поставить) мы считаем частным случаем задачи простановки ударений и омографов.
Также мы опубликовали наше продуктовое решение для простановки ударений (в омографах в том числе) в рамках репозитория silero-stress и также напрямую через pypi
. В ближайшее время добавим эту модель и обновим наши публичные модели синтеза и раскатим более мощную "большую" (тоже маленькую по современным меркам) версию модели в приватные сервисы и для клиентов. Также мы опубликовали бенчмарки качества и скорости публичных академических решений… и там всё очень неоднозначно.
Наливайте себе чай, садитесь поудобнее. Мы постараемся описать наш путь длиной в вечность без лишних подробностей.
Омографы (графические омонимы) — слова, совпадающие по написанию, но различные по звучанию и значению, например, «за́мок» и «замо́к». Если коротко, омографы это вот такие слова:
То есть простыми словами - на письме слова пишутся одинаково (а у нас довольно "фонетический" алфавит, если знать ударение), но ударение меняет смысл слова. Зачастую особенно старые говорилки грешат "упоротыми" ударениями в стиле "кожаных ублюдков". На момент, когда последний раз это проверяли, к примеру Яндекс хорошо разбирал омографы, а Сбер - практически не разбирал, так что проблема всё ещё актуальна, даже для компаний без каких-либо ограничений на бюджеты.
Также существуют ещё и омофоны, омоформы и полные омонимы, но с точки зрения синтеза речи интерес представляют только омографы.
Видя, какая задача предстоит, мы не торопились с решением и раза 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. Если вы хотите, чтобы мы инкорпорировали ваши словари ударений и / или датасеты в наш инструмент - вы можете создать тикет в репозитории.