Представьте себя на месте изучающего русский язык иностранца. Ударение станет одним из ваших самых страшных ночных кошмаров. Во-первых, оно не описывается каким-то простым набором правил, и чаще всего правильное произношение приходится просто запоминать. Во-вторых, оно обычно не обозначается в текстах, что практически сводит на нет относительную близость русской орфографии к произношению - без ударений правильно прочитать текст с незнакомыми словами иностранец все равно не сможет. В-третьих, неправильное ударение сильно меняет фонетический образ слова для русского человека, и из-за одной ошибки вас могут просто не понять.
Мой опыт
Я знаком с одной девушкой, которая изучает русский язык в университете Лейдена. Однажды я написал приложение для изучения русского, одной из функций которого была расстановка ударений. Я просто проверял каждое слово по словарю. Однако, это часто приводило к ситуации неоднозначности, когда ударение зависит от контекста. Например: "два сло́ва", но "длинные слова́". В подобных ситуациях мне ничего не оставалось делать, кроме как оставлять слово без ударения.
Идея
Но ведь неоднозначность возникает из-за того, что ударение зависит от того, в какой форме употреблено слово, от его морфологических показателей. Если мы сможем найти словарь ударений в зависимости от формы слова + научимся определять форму слова по контексту, то мы сможем разрешить неоднозначность. Морфологический анализ - одна из стандартных задач NLP, для ее решения воспользуемся библиотекой Spacy.
Данные
Где же нам найти подходящий словарь ударений? Интересующая нас информация есть в wiktionary.
Для парсинга wiktionary существует либа wiktionaryparser. Впрочем, для наших целей подойдёт уже готовый результат её работы. Json-файл содержит набор словоформ, для каждой из которых доступно ее написание с ударением, набор определений, а также маркер части речи. Кроме того, я распарсил словарь Зализняка и добавил те формы оттуда, которых не нашлось в wiktionary.
Для одного токена может быть несколько объектов-словоформ, которые отличаются частью речи (военный как прилагательное и военный как существительное) или смыслом (писа́ть и пи́сать).
Я перевел словарь в новый формат: каждому токену соответствует массив словоформ, для каждой словоформы определены поля accentuated (вид словоформы с ударением), form (морфологические показатели) и lemma (ссылка на лемму). Словарь сериализован с помощью pickle в wordforms.dat.
Кроме этого, я создал словарь лемм. Леммы содержат массив возможных частей речи + частотный ранг. Словарь лемм также сериализован с помощью pickle, в lemmas.dat.
Spacy
Spacy - это библиотека для NLP. Она умеет делать такие вещи как токенизация, морфологический анализ, синтаксический анализ, Named Entity Recognition. Нас интересуют первые две функции.
Мы будем использовать natasha-spacy, реализацию русского языка для spacy на основе natasha, поскольку она поддерживает более сложный морфологический анализ (падежи, времена), чем стандартная версия. Инструкция по настройке находится здесь.
Инициализация
import spacy
import pickle
ru_nlp = spacy.load('ru_core_news_md')
def load():
with open(file="lemmas.dat", mode='rb') as f:
lemmas = pickle.loads(f.read())
with open(file="wordforms.dat", mode='rb') as f:
wordforms = pickle.loads(f.read())
return lemmas, wordforms
def introduce_special_cases_from_dictionary(dictionary):
for word in dictionary:
if (" " in word) or ("-" in word):
if len(dictionary[word]) == 1:
ru_nlp.tokenizer.add_special_case(word, [{"ORTH": dictionary[word][0]["accentuated"]}])
ru_nlp.tokenizer.add_special_case(word.capitalize(), [{"ORTH": dictionary[word][0]["accentuated"].capitalize()}])
lemmas, wordforms = load()
introduce_special_cases_from_dictionary(wordforms)
f = open("in.txt", mode='r', encoding='utf-8')
text = f.read()
f.close()
res = accentuate(text, wordforms, lemmas)
f = open("out.txt", mode='w', encoding='utf-8')
f.write(res)
f.close()
Сначала загрузим наши словари, затем модифицируем токенизатор: добавим нестандартные (содержащие дефис или пробел) токены из нашего словаря. Это нужно из-за случаев наподобие "по-моему": иначе слово будет разделено на "по", "-" и "моему", с неправильным ударением в "моему".
Токенизация и морфологический анализ
def tokenize(text, wordforms):
res = []
doc = ru_nlp(text)
for token in doc:
if token.pos_ != 'PUNCT':
word = {"token": token.text, "tag": token.tag_}
if word["token"] in wordforms:
word["interpretations"] = wordforms[word["token"]]
if word["token"].lower() in wordforms:
word["interpretations"] = wordforms[word["token"].lower()]
word["lemma"] = token.lemma_
word["is_punctuation"] = False
word["uppercase"] = word["token"].upper() == word["token"]
word["starts_with_a_capital_letter"] = word["token"][0].upper() == word["token"][0]
else:
word = {"token": token.text, "is_punctuation": True}
word["whitespace"] = token.whitespace_
res.append(word)
return res
Делаем одновременно токенизацию и морфологический анализ.
Каждое слово представляем в виде словаря со значениями:
token — собственно запись словоформы (в нижнем регистре)
tag — набор морфологических показателей. Строка tag может выглядеть, например, так: "NOUN__Animacy=Inan|Case=Gen|Gender=Masc|Number=Plur" или "'VERB__Aspect=Perf|Mood=Imp|Number=Plur|Person=Second|VerbForm=Fin|Voice=Act'"
interpretations — набор объектов из словаря (возможно, с различными ударениями), которыми может быть наш токен
lemma — исходная лемма токена с точки зрения spacy
is_punctuation — признак принимает значение True не только для знаков пунктуации, но и для "специальных случаев" (см. предыдущий раздел про инициализацию spacy).
starts_with_a_capital_letter — мы переводим все токены в нижний регистр, и отдельно запоминаем, а было ли слово изначально написано с большой буквы
uppercase — написано ли слово полностью в верхнем регистре
whitespace — содержит пробел, если после токена идёт пробел. Нужно для восстановления результата
Совместимость морфологических показателей
Для каждой возможной "интерпретации" токена мы будем проверять, совместима ли она с морфологическими тегами, которые выдал spacy.
def compatible(interpretation, lemma, tag, lemmas):
if lemma in lemmas:
pos_exists = False
possible_poses = lemmas[lemma]["pos"]
for i in range(len(possible_poses)):
if possible_poses[i] in tag:
pos_exists = True
break
if not (pos_exists):
return False
if interpretation == "canonical":
return True
if "plural" in interpretation and not ("Number=Plur" in tag):
return False
if "singular" in interpretation and not ("Number=Sing" in tag):
return False
if not ("nominative" in interpretation) and ("Case=Nom" in tag):
return False
if not ("genitive" in interpretation) and ("Case=Gen" in tag):
return False
if not ("dative" in interpretation) and ("Case=Dat" in tag):
return False
if not ("accusative" in interpretation) and ("Case=Acc" in tag):
adj = False
if "ADJ" in tag and "Animacy=Inan" in tag:
adj = True
if not adj:
return False
if not ("instrumental" in interpretation) and ("Case=Ins" in tag):
return False
if not ("prepositional" in interpretation) and not ("locative" in interpretation) and ("Case=Loc" in tag):
return False
if (("present" in interpretation) or ("future" in interpretation)) and ("Tense=Past" in tag):
return False
if (("past" in interpretation) or ("future" in interpretation)) and ("Tense=Pres" in tag):
return False
if (("past" in interpretation) or ("present" in interpretation)) and ("Tense=Fut" in tag):
return False
return True
Аргументы:
interpretation — строка с морфологическими показателями из словаря wordforms. Пример: "genitive plural"
lemma — лемма токена по версии natasha-spacy
tag — морфологический тег от spacy. Пример: "NOUN__Animacy=Inan|Case=Gen|Gender=Masc|Number=Plur"
lemmas — словарь лемм
Сначала проверим, что лемма lemma вообще может быть частью речи, указанной в tag. Это позволяет отфильтровать случаи вроде "потом" как наречие, чтобы не интерпретировать его как форму слова "пот".
Далее, проверяем различные несовместимые условия (только если interpretation не 'canonical'):
В interpretation написано и в tag явно указано разное грамматическое число
В tag указан падеж, а в interpretation соответствующего падежа нет
Явное противоречие во времени глагола
Обрабатываем все токены
def accentuate(text, wordforms, lemmas):
res = ""
words = tokenize(text, wordforms)
for i in range(len(words)):
accentuated = accentuate_word(words[i], lemmas)
if "starts_with_a_capital_letter" in words[i] and words[i]["starts_with_a_capital_letter"]:
accentuated = accentuated.capitalize()
if "uppercase" in words[i] and words[i]["uppercase"]:
accentuated = accentuated.upper()
res += accentuated
res += words[i]["whitespace"]
return res
def accentuate_word(word, lemmas):
if ("tag" in word) and ("PROPN" in word["tag"]):
return word["token"]
if word["is_punctuation"] or (not "interpretations" in word):
return word["token"]
else:
res = derive_single_accentuation(word["interpretations"])
if not (res is None):
return res
else:
compatible_interpretations = []
for i in range(len(word["interpretations"])):
if compatible(word["interpretations"][i]["form"], word["interpretations"][i]["lemma"], word["tag"], lemmas):
compatible_interpretations.append(word["interpretations"][i])
res = derive_single_accentuation(compatible_interpretations)
if not (res is None):
return res
else:
new_compatible_interpretations = []
for i in range(len(compatible_interpretations)):
if compatible_interpretations[i]["lemma"] == word["lemma"]:
new_compatible_interpretations.append(compatible_interpretations[i])
res = derive_single_accentuation(new_compatible_interpretations)
if not (res is None):
return res
else:
return word["token"]
def derive_single_accentuation(interpretations):
if len(interpretations) == 0:
return None
res = interpretations[0]["accentuated"]
for i in range(1, len(interpretations)):
if interpretations[i]["accentuated"] != res:
return None
return res
В accentuate_word сначала проверяем, не является ли слово именем собственным. Если является, ничего с ним не делаем. Если этого не делать, могут возникнуть случаи вроде интерпретации "Же́не" как "жене́".
derive_single_interpretation проверяет, существует ли единственный способ постановки ударения. Если да, она возвращает этот способ, иначе возвращается None.
Постановка ударения происходит в 3 этапа
Если у нас сразу получилось так, что можно однозначно поставить ударение, то мы ничего дальше не делаем. Большинство слов в реальных текстах будут попадать в эту категорию.
Если есть различные варианты ударения, отфильтруем interpretations, оставив только те, которые проходят процедуру compatible. После данного этапа снова проверяем, остался ли у нас лишь один вариант ударения.
Если даже это не помогло, оставим только те interpretations, у которых лемма совпадает с той, которую дает natasha-spacy (в определении леммы spacy нередко ошибается).
Возвращаем просто сырой токен без ударения, если даже после этого у нас не образовалось единственного варианта.
Результаты
Код и данные: https://github.com/einhornus/russian_accentuation
Замечание: алгоритм не делает ёфикацию, поэтому текст следует предварительно пропустить через ёфикатор.
Литературный текст
Жил на опу́шке дрему́чего ле́са бе́дный дровосе́к со свое́й жено́й и двумя́ детьми́; ма́льчика зва́ли Гензель, а де́вочку - Гретель. Жил дровосе́к впроголодь; вот наступи́ла одна́жды в той земле́ така́я дорогови́зна, что не на что бы́ло ему́ купи́ть да́же хлеба на пропита́ние. И вот, под ве́чер, лёжа в посте́ли, стал он разду́мывать, и всё одолева́ли его́ ра́зные мы́сли и забо́ты; повздыхал он и говори́т жене́:
— Что же тепе́рь бу́дет с на́ми? Как нам прокорми́ть бе́дных дете́й, нам-то ведь и сами́м есть не́чего!
— А зна́ешь что, — отвеча́ла жена́, — дава́й-ка пораньше у́тром, то́лько начнёт света́ть, заведём дете́й в лес, в са́мую глуху́ю ча́щу; разведём им костёр, дади́м ка́ждому по куску́ хлеба, а са́ми уйдём на рабо́ту и оста́вим их одни́х. Доро́ги домо́й они́ не найду́т, вот мы от них и изба́вимся.
Ошибок нет. Однако, слова "впроголодь", "повздыхал" и "пораньше" не были найдены в словаре, и для них ударения не проставлены. Кроме того, не поставлено ударение в слове "хлеба": в словаре не записано, что "хлеба́" - множественное число, а "хле́ба" - родительный падеж (парсер немного запутался из-за того, что нормальная форма множественного числа - "хлебы"). Проблемы подобного рода со словарем есть, но они довольно редки.
Некоторые сложные случаи
Я стою у окна́. В до́ме больши́е о́кна.
Ну, "стоить у окна" в теории тоже возможно. Слово "большие" иногда получает ударение на и (работа spacy стохастическая), иногда остается без него.
Я куплю́ немно́жко земли́. Не смей претендова́ть на мои́ зе́мли.
Без ошибок
Нам нужны́ учителя́. Я процити́ровал своего́ учи́теля.
Без ошибок
Мы зале́зли на строи́тельные леса́.
Без ошибок
Самолёт жда́ли два дире́ктора. Дире́ктора бы́ли пожило́го во́зраста.
Увы, ошибка natasha-spacy со словом "директора"
Ключ снача́ла находи́лся в двери, а пото́м лежа́л на полу́.
Не разрешена неоднозначность со словом "двери"
Солда́ты вы́строились в ка́ре.
К сожалению, слово "каре" не нашлось в словаре.
По-мо́ему, Маше стоит купи́ть маши́ну.
Не разрешена неоднозначность со словом "стоит"
По ле́су броди́л медве́дь. Мы наткну́лись на него́, когда́ гуля́ли в лесу́.
Без ошибок.
Комментарии (33)
sterr
28.08.2021 23:07-2Дире́ктора бы́ли пожило́го во́зраста.
Директоры же!Нам нужны́ учителя́.
Учители же!
То есть вы пытаетесь научить правильному ударению в словах с ошибками?einhorn Автор
28.08.2021 23:15https://en.wiktionary.org/wiki/директор#Russian
https://ru.wiktionary.org/wiki/директор
Везде "директора́"
https://en.wiktionary.org/wiki/учитель#Russian
Вариант "учи́тели" помечен как "dated"
https://ru.wiktionary.org/wiki/учитель
В знач. основоположник учения встречается также вариант склонения по схеме 2a с ударением в формах мн. ч.: учи́тели, учи́телей, учи́телям, учи́телями, учи́телях.
Но основной вариант - "учителя́"
MacIn
29.08.2021 01:51«Директоры» может употребляться разве что для технического термина, обозначающего что-то направляющее что-то еще. Например, «директоры — посадка по директорам».
Если люди — то директора, конечно.
Benhealy
29.08.2021 01:55Я не соглашусь полностью, что незнание иностранцем ударения у русского слова в тексте сильно затруднит понимание. Можно прочитать как написано, и с фиксированным ударением, как в польском или чешском. Все равно половина наших тоже "кто звОнит?" И прочие. Иностранцу все равно дадут скидку что иностранец, все будет в шутку обращено, как в слове "передохнуть" (передохнУть или передОхнуть). Хорошо,хоть не тоны у нас, как у китайцев разных (но даже тампомнят о созвучии)
einhorn Автор
29.08.2021 02:01+1Я очень много общался с иностранцами на русском.
У меня без проблем получается понимать речь даже с серьёзными проблемами в грамматике.
А вот при ошибках в ударении до меня не всегда сразу доходит, что же человек вообще имеет в виду.
tyomitch
29.08.2021 11:25+2Была история, как иностранец в СПб пытался в газетном киоске купить карто́чку (с ударением как в польском), а продавщица объясняла, мол, за картошкой надо идти в овощной.
anonymous
00.00.0000 00:00marto
29.08.2021 14:49Лингвисты описывают язык, а не «помогают» или «мешают». Ну, иногда участвуют в разработке технологий, использующих язык.
С точки зрения описания языка то, о чем вы говорите — ничуть не усложнение, грамматический строй и украинского, и русского языка подразумевает согласование форм слов по родам; наоборот, эта парадигма исторически довольно сильно засбоила в некоторых лексических группах (типа названий профессий), и этот сбой обращает на себя внимание (не впервые) и стыкуется с определенным социальным запросом, который может получить, а может не получить политическую поддержку — и быть зафиксирован в том числе в языке. А лингвисты все это будут описывать и дальше, они знают историю языка за сотни, иногда тысячи лет, и их это все не очень удивляет.
С точки зрения «сложности» одинаковое грамматическое изменение всех слов по родам, включая названия профессий, скорее «проще» (впрочем, как и альтернативный вариант — распространение общего рода), а вот как сейчас в русском — «здесь играем, здесь не играем, а здесь рыбу заворачивали» — как раз лишнее несистемное усложнение; и, да, скорее всего оно имеет социально-экономическую природу.
marto
29.08.2021 16:04Да, я немного срезал угол в формулировке. Но нет ничего «простого» или «последовательного» в том, что «менеджер сказала», «швея сказала», «учительница сказала», «автор сказала». Для части профессионализмов родовая парадигма работает предсказуемо, части новых вроде как назначен общий род, часть сбоит и двоится в разных контекстах. Русский (и украинский) грамматический род действительно не означает «пол» и вообще работает довольно рандомно, судя по всему, но в отношении человеческих занятий согласование грамматики с семантикой действительно сбоит немного подозрительно, в этом отношении уже были и дискуссии, и обновления нормы и сто, и больше лет назад (обычно точечно), и это действительно достаточно регулярно приводит к тому, что из контекста неясно, действует мужчина или женщина, хотя это проще всего выразить именно грамматически. Про русские феминитивы недавно даже популярная книжка вышла от хорошего лингвиста... Хорошей лингвистки? Ирина Фуфаева, «Как называются женщины. Феминитивы: история, устройство, конкуренция». Все, как говорится, довольно сложно и интересно.
marto
29.08.2021 16:13Это не совсем «отдельные слова». Это грамматические формы, и идея, если считать, что за языковым строем стоит «идея», в том, что мы знаем или с большой вероятностью можем угадать род слова и модель, по которой его можно менять в зависимости от контекста (практические нужды не прибиты гвоздиками, они могут меняться ситуативно); и вот почему-то в некоторых классах слов бинарная схема исторически конкурирует с тренарной, когда добавляется общий род. Но история на то и история, что раньше могло быть так, а сейчас иначе (например потому, что стало больше женщин — специалистов в разных областях, и все чаще не видеть грамматические маркеры становится неудобно).
tyomitch
29.08.2021 17:38Общий род в русском (и, скорее всего, в украинском тоже) существует давно, и большинство таких слов не имеют отношения к профессиям: вики приводит в пример умница, молодчина, работяга, пьяница, задира, грязнуля, сирота, калека и пр. Все они грамматически согласуются по «половой принадлежности описываемого словом объекта». Все они вызывают у вас неприязнь, или только обозначения профессий?
marto
29.08.2021 17:58Программистам должно быть понятно, с какими сложностями и рисками сопряжена поддержка и отладка такой системы, как любой естественный язык. Не надо хейтить лингвистов, они занимаются довольно прикольной описательной наукой, которая приносит много пользы, если присмотреться. То, что правки в нормативной грамматике, которой учат в школе и используют на официальном уровне — дело всегда сложное, компромиссное и вызывающее неприятие, понятно, но время от времени оно все равно назревает, а пользоваться в быту «неправильным» языком вам все рано никто не помешает, лишь бы было понятно.
Есть языки, которые благополучно и довольно рано отказались от грамматического рода и работают нормально (см. хотя бы английский, он вообще забил на большую часть привычной нам грамматики, устроил ад в правописании и фонетике и т.п.). Для нашей кучки славянских языков мера, которую вы предлагаете в качестве «упрощения», бесконечно травматичнее того, что вам не нравится, и никаким указом ее не провернуть.
ncr
29.08.2021 17:33«Узаконивание» это, как бы, не совсем «создали разные слова».
Язык не отлит в граните и определяется не словарями или чьими-то понятиями о прекрасном, а, внезапно, самими носителями. Если носители уже так говорят и им так удобно, то однажды это станет нормой, даже если сейчас режет слух.
Я, если что, не защищаю род в словах, отнюдь. Это дикий пережиток, не добавляющий ничего к сказанному и только усложняющий изучение языка. Было бы прекрасно искоренить его совсем, но увы, уже слишком поздно. А консистентность хотя бы не хуже кучи исключений.tyomitch
29.08.2021 17:48Наличие рода делает возможной анафору: клумба в саду, за которой я ухаживаю vs. клумба в саду, за которым я ухаживаю. Попробуйте выразить это на языке без рода, например на английском.
ncr
29.08.2021 18:53+1Ваш пример, несомненно, красив, однако:
— весьма хрупок сам по себе ("куст в саду, за которым я ухаживаю").
— При недостаточно ловком обращении может и вовсе превратиться в шляпу, подъезжающую к сией станцыи и глядящую на природу в окно.
А плата, в виде комбинаторного взрыва склонений и спряжений, как-то слишком высока.
В английском, естественно, это потребует иной структуры предложения и более явного расставления акцентов. Что, скорее, хорошо.
В совсем других языках тоже можно писать красивые однострочники типаwhile (*s++ = *t++)
, а можно и не писать, смысл не пострадает.tyomitch
29.08.2021 23:42Да, классический пример неудачной анафоры — собрание жильцов нашего дома, на котором стоял вопрос :)
На мой вкус, лучше иметь в языке выразительные средства и самому решать — по возможности, по контексту и по настроению — использовать их или нет, чем не иметь их вообще. Что в естественном, что в ЯП.
snakers4
29.08.2021 16:17-1Правильно ли я понял, что:
- Алгоритм это де-факто поиск по готовому словарю + тегам из spacy если таковые есть;
- Общий "размер" получается словарь 500MB + spacy (несколько сотен мегабайт со всеми зависимостями?);
Также вы снимали какие-то метрики?
einhorn Автор
29.08.2021 16:42+1Алгоритм вкратце:
Сначала поиск по словарю. Если существует только один вариант постановки ударения, то возвращаем его.
Если нет, то проверяем каждый вариант постановки ударения на совместимость с тегом от Spacy. Если в результате этого остаётся только один вариант, то ставим ударение в соответсвии с этим вариантом.
Пример: "твои города"
Для слова "твои": в словаре есть только один вариант: "твои́". Его и выводим.
Для слова "города" в словаре есть две возможные интерпретации: как родительный падеж "го́рода" или как множественное число "города́". Поэтому мы проверяем совместимость каждой интерпретации с тегом от natasha-spacy. Natasha-spacy говорит, что это именительный падеж множественного числа, поэтому мы первый вариант отсеиваем и выводим "города́".
Размер словаря: 67МБ файла wordforms.dat и 2МБ файла lemmas.dat
Метрики точности морфологического анализа natasha-spacy приведены здесь: https://github.com/natasha/natasha-spacy. Ошибки в ударении могут появиться только из-за 1)неполноты словаря 2)несовершенства морфологического анализа - и ни с тем, ни с другим я ничего поделать не могу. Я специально показал в примерах, что ошибки бывают, и объяснил, чем они вызваны.
nshmyrev
30.08.2021 01:38Вот такой проект ещё есть:
https://github.com/nsu-ai/russian_g2peinhorn Автор
30.08.2021 01:50Посмотрел.
В качестве "ядра" (для морфологического анализа) там используется вот этот проект: https://github.com/IlyaGusev/rnnmorph
Причём, rnnmorph выдаёт результат в том же самом формате, что и natasha-spacy (возвращает тег вида "Case=Nom|Gender=Fem|Number=Sing").
То есть, возможно очень легко заменить "ядро" на rnnmorph в данном проекте. Однако, я сильно сомневаюсь, что он будет работать лучше natasha-spacy; всё-таки, natasha проверена временем, а rnnmorph, видимо, написан одним человеком (IlyaGusev).
denis_odinets
Полезная работа, особенно если заставить работать в связке с синтезом речи, для голосовых ботов.