Вы никогда не задумывались, почему тексты классических русских писателей так ценятся, а сами писатели считаются мастерами слова? Дело явно не только в сюжетах произведений, не только в том, о чём написано, но и в том, как написано. Но при быстром чтении по диагонали осознать это трудно. Кроме того, текст какого-нибудь значимого романа нам просто не с чем сравнить: почему, собственно, так прекрасно, что в этом месте появилось именно это слово, и чем это лучше какого-то другого? В какой-то мере реальное словоупотребление могло бы контрастно оттенить потенциальное, которое можно найти в черновиках писателя. Писатель не сразу вдохновенно пишет свой текст от начала до конца, он мучается, выбирает между вариантами, те, что кажутся ему недостаточно выразительными, он вычеркивает и ищет новые. Но черновики есть не для всех текстов, они отрывочны и читать их сложно. Однако можно провести такой эксперимент: заменить все поддающиеся замене слова на похожие, и читать классический текст параллельно с тем, которого никогда не было, но который мог бы возникнуть в какой-то параллельной вселенной. Попутно мы можем попытаться ответить на вопрос, почему это слово в этом контексте лучше, чем другое, похожее на него, но всё-таки другое.
А сейчас всё это (кроме собственно чтения) можно сделать автоматически.
Дистрибутивная семантика aka векторные модели
На Хабре уже была статья о том, как использовать дистрибутивную семантику в поиске по т.н. «пирожкам». Дистрибутивная семантика — это довольно простая, но отлично работающая идея, что значение слова связано с его окружением в тексте. Слова с похожим значением будут появляться в сходных контекстах и наоборот. Сам контекст можно представить в виде вектора (отсюда и «векторные модели») и посчитать сходство и различие в значении разных слов. Для русского языка на основе этой идеи сделан превосходный сервис — RusVectōrēs, который не только позволяет найти семантически наиболее похожие слова, но и предоставляет в свободный доступ посчитанные создателями ресурса модели.
Процессинг
Берём 5 классических русских романов. Дмитрий Быков где-то говорил, что самыми знаковыми для русской литературы являются романы, в названиях которых есть союз «и». Значит: «Преступление и наказание», «Война и мир», «Отцы и дети», «Мастер и Маргарита». Ну и «Евгений Онегин», тоже важный русский роман, хотя и в стихах.
Кроме того, нам потребуется модель с сайта RusVectōrēs, которая построена на текстах Национального корпуса русского языка и русской википедии. Работать с ней нам позволит библиотека Gensim. Чтобы искать в этой модели т.н. квазисинонимы, то есть фактически ближайшие соседи семантического графа (а эти соседи не всегда «настоящие» синонимы, часто наиболее семантически близким словом оказывается антоним, хотя это и кажется контринтуитивным), нам потребуется нормализованная форма слова (лемма), получить которую должен помочь морфологический анализатор, программа умеющая эту самую форму находить. Сначала я думал использовать Mystem от Яндекса, но в итоге остановился на другом, Pymorphy2, дальше будет понятно, почему.
import gensim
import pymorphy2
model = gensim.models.KeyedVectors.load_word2vec_format("ruwikiruscorpora_0_300_20.bin.gz", binary=True)
model.init_sims(replace=True)
morph = pymorphy2.MorphAnalyzer()
Итак, идём по тексту, берём из него слова, нормализуем (то есть восстанавливаем инфинитив для глаголов или именительный падеж единственного числа для имён), подыскиваем в модели наиболее похожее слово, при этом нужно следить за тем, чтобы это слово было той же части речи, что и исходное, потому что по умолчанию это не обязательно будет так. См., например, наиболее близкие слова к синева: тут есть и такие же существительные голубизна, чернота, но есть и прилагательное голубоватый, и глагол синеть. Я уж не говорю о том, что каких-то слов в модели и не будет совсем. Дело в том, что для низкочастотных в исходном корпусе слов вектор по соображениям оптимизации не строится, и квазисиноним найти не получится. Значит, оставим в этом месте исходное слово. Кроме того, нет смысла искать замены для местоимений, предлогов и прочих неполнозначных частей речи.
Генерация форм
А вот дальше самое интересное. В исходном тексте слово стоит в какой-нибудь косвенной форме, а из модели мы получили лемму. Теперь её нужно поставить в ту же форму, чтобы текст оказался связным и читабельным, а не казался каким-нибудь плохим переводом с инопланетного типа Мама мыть рама. И в этом как раз нам может помочь тот же Pymorphy2, который умеет не только разбирать слова, но и ставить их в нужную форму по заданным признакам. Mystem этого не может, а если при разборе запоминать не только лемму, которую выдал анализатор, но и набор признаков формы слова (те же число и падеж), то их потом очень удобно будет снова отправить в ту же программу уже для генерации формы. Правда, тут выяснилось, что не все те теги, которые выдаёт Pymorphy2-анализатор, знает Pymorphy2-генератор, то есть его правая рука не всегда знает, что делает левая. Но это не страшно, немного танцев с бубном и мы получаем нужную форму:
def flection(lex_neighb, tags):
tags = str(tags)
tags = re.sub(',[AGQSPMa-z-]+? ', ',', tags)
tags = tags.replace("impf,", "")
tags = re.sub('([A-Z]) (plur|masc|femn|neut|inan)', '\\1,\\2', tags)
tags = tags.replace("Impe neut", "")
tags = tags.split(',')
tags_clean = []
for t in tags:
if t:
if ' ' in t:
t1, t2 = t.split(' ')
t = t2
tags_clean.append(t)
tags = frozenset(tags_clean)
prep_for_gen = morph.parse(lex_neighb)
Допиливание
Теперь собираем всё вместе, не забываем обработать капитализацию и знаки препинания. Вуаля! Альтернативная русская литература, которой не было, у нас на экране:
Поговорить об Ювенале,
В середине записки оставить vale,
Да вспомнил, хоть не без прегрешения,
Из Энеиды два стихотворения.
Потолковать об Ювенале,
В конце письма поставить vale,
Да помнил, хоть не без греха,
Из Энеиды два стиха.
Но всё-таки что-то не то. Местами текст всё-таки превышает допустимый градус бессвязности:
Дружки Людмилы и Руслана!
С героиней моего повести
Без послесловий, посей же полчаса
Позвольте познакомиться вас
Друзья Людмилы и Руслана!
С героем моего романа
Без предисловий, сей же час
Позвольте познакомить вас
Дело в роде существительных! Из модели мы достаём ту же часть речи и ставим её в ту же форму, но мы не учли такой постоянный признак существительных, как род. Он у разных существительных разный, и правильная генерация формы не спасёт от несогласованности. Теперь вводим правило, по которому существительные, которые выдала нам модель, ещё дополнительно проверяем на родовую принадлежность:
def flection(lex_neighb, tags):
tags = str(tags)
tags = re.sub(',[AGQSPMa-z-]+? ', ',', tags)
tags = tags.replace("impf,", "")
tags = re.sub('([A-Z]) (plur|masc|femn|neut|inan)', '\\1,\\2', tags)
tags = tags.replace("Impe neut", "")
tags = tags.split(',')
tags_clean = []
for t in tags:
if t:
if ' ' in t:
t1, t2 = t.split(' ')
t = t2
tags_clean.append(t)
tags = frozenset(tags_clean)
prep_for_gen = morph.parse(lex_neighb)
ana_array = []
for ana in prep_for_gen:
if ana.normal_form == lex_neighb:
ana_array.append(ana)
for ana in ana_array:
try:
flect = ana.inflect(tags)
except:
print(tags)
return None
if flect:
word_to_replace = flect.word
return word_to_replace
return None
Стало лучше:
Дружки Людмилы и Руслана!
С персонажем моего рассказа…
и т.д.
Что получилось
Теперь можно заняться медленным чтением:
Оригинал:
Глава 1
Никогда не разговаривайте с неизвестными
Однажды весною, в час небывало жаркого заката, в Москве, на Патриарших прудах, появились два гражданина. Первый из них, одетый в летнюю серенькую пару, был маленького роста, упитан, лыс, свою приличную шляпу пирожком нес в руке, а на хорошо выбритом лице его помещались сверхъестественных размеров очки в черной роговой оправе. Второй – плечистый, рыжеватый, вихрастый молодой человек в заломленной на затылок клетчатой кепке – был в ковбойке, жеваных белых брюках и в черных тапочках.
«Перевод»:
Глава 1
Никогда не беседуйте с невыясненными
Случайно весною, в полдень невиданно жаркого восхода, в Казани, на Митрополичьих ручьях, появились два согражданина. Первый из них, щеголеватый в десятилетнюю голубенькую пару, был крошечного прироста, тучен, плешив, свою порядочную шапку пирогом тащил в ладони, а на плохо выбритом лице его размещались потусторонних диаметров очки в черной роговой дужке. Второй – широкоплечий, курчавый, белобрысый смуглянки разум в заломленной на лоб цветастой кепочке – был в куртке, жеваных белых штанах и в черных тапочках.
Ну, и, конечно, при всех серьёзных и даже почти научных целях, о которых я писал в начале статьи, кое-где это просто-напросто смешно, хотя и неполиткорректно:
– Вы – фашист? – осведомился Безнадзорный.
– Я-то?.. – Переспросил доцент и вдруг задумался. – Да, пожалуй, фашист… – сказал он.
– Вы – немец? – осведомился Бездомный.
– Я-то?.. – Переспросил профессор и вдруг задумался. – Да, пожалуй, немец… – сказал он.
Полный текст романов (версия 3.0):
Оставшиеся проблемы
Вообще-то не всё хорошо согласовывается. Нужно отбирать слова, учитывая и другие грамматические категории, среди которых напрашиваются переходность и залог. Ну, и pymorphy2 не идеален в выборе леммы. Он подсказывает наиболее вероятную. Но наиболее вероятный ответ не всегда правильный. Например, форма молодой распознаётся как родительный падеж от существительного молодая, наиболее похожим на него словом оказывается смуглянка, а pymorphy2 радостно ставит смуглянку в форму родительного падежа. Вот так из вихрастый молодой человек получается белобрысый смуглянки разум.
Кое-где лучше определить форму могла бы помочь буква ё, которую в текстах романов обычно не воспроизводят:
>>> morph.parse('черт')
[Parse(word='черт', tag=OpencorporaTag('NOUN,inan,femn plur,gent'), normal_form='черта', score=0.588235, methods_stack=((<DictionaryAnalyzer>, 'черт', 55, 8),)), Parse(word='чёрт', tag=OpencorporaTag('NOUN,anim,masc sing,nomn'), normal_form='чёрт', score=0.411764, methods_stack=((<DictionaryAnalyzer>, 'чёрт', 3019, 0),))]
>>> morph.parse('чёрт')
[Parse(word='чёрт', tag=OpencorporaTag('NOUN,anim,masc sing,nomn'), normal_form='чёрт', score=1.0, methods_stack=((<DictionaryAnalyzer>, 'чёрт', 3019, 0),))]
Поэтому:
Усмехаться и подумать про себя:
Когда же особенностей возьмет тебя!’
Вздыхать и думать про себя:
Когда же черт возьмет тебя!
Описание идеи «векторных романов» и дополнительные материалы лежат на специальной странице.
Весь код для замены выложен на Github.
Приятного чтения!
Ссылки
- RusVectōrēs, конкретно ссылка на использованную модель (420 Мб).
- Gensim
- Pymorphy2
- Замена слов на похожие в «Гордости и предубеждении» (нет проблем с морфологией)
UPD.: kdenisk предоставил ёфицированные тексты романов, что позволило избавиться от ряда огрехов в распознавании форм. Добавлена фильтрация по залогу глагола.