Векторные модели и русская литература

Вы никогда не задумывались, почему тексты классических русских писателей так ценятся, а сами писатели считаются мастерами слова? Дело явно не только в сюжетах произведений, не только в том, о чём написано, но и в том, как написано. Но при быстром чтении по диагонали осознать это трудно. Кроме того, текст какого-нибудь значимого романа нам просто не с чем сравнить: почему, собственно, так прекрасно, что в этом месте появилось именно это слово, и чем это лучше какого-то другого? В какой-то мере реальное словоупотребление могло бы контрастно оттенить потенциальное, которое можно найти в черновиках писателя. Писатель не сразу вдохновенно пишет свой текст от начала до конца, он мучается, выбирает между вариантами, те, что кажутся ему недостаточно выразительными, он вычеркивает и ищет новые. Но черновики есть не для всех текстов, они отрывочны и читать их сложно. Однако можно провести такой эксперимент: заменить все поддающиеся замене слова на похожие, и читать классический текст параллельно с тем, которого никогда не было, но который мог бы возникнуть в какой-то параллельной вселенной. Попутно мы можем попытаться ответить на вопрос, почему это слово в этом контексте лучше, чем другое, похожее на него, но всё-таки другое.

 

А сейчас всё это (кроме собственно чтения) можно сделать автоматически.

 

Дистрибутивная семантика 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-генератор, то есть его правая рука не всегда знает, что делает левая. Но это не страшно, немного танцев с бубном и мы получаем нужную форму:

 

Функция, в которой автор пляшет с бубном вокруг 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.

 

Приятного чтения!

 

Ссылки

 

 

UPD.: kdenisk предоставил ёфицированные тексты романов, что позволило избавиться от ряда огрехов в распознавании форм. Добавлена фильтрация по залогу глагола.

 

UPD2: Тест по материалам «Анны Карениной»