[Из песочницы] Пирожки в дистрибутивной семантике

Уже несколько месяцев с любопытством гляжу в сторону дистрибутивной семантики — познакомился с теорией, узнал про word2vec, нашёл соответствующую библиотеку для Питона (gensim) и даже раздобыл модель лексических векторов, сформированную по национальному корпусу русского языка. Однако для творческого погружения в материал не хватало душезабирающих данных, которые было бы интересно через дистрибутивную семантику покрутить. Одновременно с этим увлечённо почитывал стишки-пирожки (эдакий синтез задиристых частушек и глубокомысленных хокку) — некоторые даже заучивал наизусть и по случаю угощал знакомых. И вот, наконец, увлечённость и любопытство нашли друг друга, породив воодушевляющую идею в ассоциативных глубинах сознания — отчего бы не совместить приятное с полезным и не собрать из подручных средств какой-нибудь «поэтичный» поисковик по базе пирожков.

из ложных умозаключений
мы можем истину сложить
примерно как перемножают
два отрицательных числа


«Поэтичность» поиска предполагалось реализовать за счёт врождённой способности дистрибутивных векторов показывать степень семантического сходства лексем самым что ни на есть действительным числом (чем меньше угол между векторами слов, тем с большей вероятностью эти слова близки по смыслу — косинусная мера, классика жанра, в общем). Например, «принцесса» и «пастух» гораздо менее близки, чем «пастух» и «овца»: 0.139 против 0.603, что, наверное, логично — вектора национального корпуса должны отражать суровую реальность, а не сказочный мир Г.Х. Андерсена. Способ же расчёта глубины корреляции (диффузии) запроса и пирожка проявился практически сам собой (дёшево и сердито) как нормализованная сумма сходств каждого слова из списка X с каждым словом списка Y (стоп-слова выкидывались, все остальные приводились к нормальной форме, но об этом позже).

Код расчёта семантической диффузии
def semantic_similarity(bag1, bag2: list, w2v_model, unknown_coef=0.0) -> float:
    sim_sum = 0.0
    for i in range(len(bag1)):
        for j in range(len(bag2)):
            try:
                sim_sum += w2v_model.similarity(bag1[i], bag2[j])
            except Exception:
                sim_sum += unknown_coef
    return sim_sum / (len(bag1) * len(bag2))



Результаты поэтического поиска и порадовали, и позабавили. Например, на запрос «музыка» был выдан следующий poem-list:

[('оксане нравилось фламенко'
  'олегу классика и джаз'
  'они вдвоём со сцены пели'
  'про лагеря и мусоров',
  0.25434666007036322),
 ('зашлась в оргазме пианистка'
  'в тумане ноты и рояль'
  'а ей играть ещё фермату'
  'пятнадцать тактов и финал',
  0.19876923472322899),
 ('люблю тебя как шум прибоя'
  'как тёплый ветер как стихи'
  'а толика люблю как танцы'
  'как поцелуи как поспать',
  0.19102709737990775),
 ('мне снится рокот космодрома'
  'и ледяная синева'
  'но я не тычу это людям'
  'об этом песен не пою',
  0.15292901301609391),
 ('индийский танец зита гите'
  'танцует страстно у костра'
  'но не отбрасывает тени'
  'сестра',
  0.14688091047781876)]


Здесь примечательно, что слова «музыка» нет ни в одном пирожке, из занесённых в базу. Однако все пирожковые ассоциации весьма музыкальны и степень их семантической диффузии с запросом довольно высока.

Теперь по порядку о проделанной работе (исходники на GitHub).

Библиотеки и ресурсы


  • Модуль pymorphy2 — для приведения слов к нормальной грамматической форме
  • Модуль gensim — подключение word2vec модели для семантической обработки
  • Так же для работы необходима дистрибутивная модель лексических векторов ruscorpora (320 Мб)


Формирование модели данных


олег представил в виде текста
все что оксана говорит
разбил на главы и абзацы
и на отдельные слова


Первым делом из текстового файла (poems.txt), в котором содержатся стишки-пирожки (порядка восьми сотен), нарезается список, собственно, содержащий эти пирожки в виде строк. Далее из каждого строко-пирожка выжимается мешок слов (bag of words), в котором каждое слово приведено в нормальную грамматическую форму и из которого выкинуты шумовые слова. После чего для каждого мешка вычисляется семантическая «плотность» (интро-диффузия) пирожка и формируется специфический ассоциативный список (помогает понять, какой смысловой слой превалирует с точки зрения модели дистрибутивных векторов). Всё это удовольствие укладывается в словарь под соответствующие ключи и записывается в файл в формате json.

Код выжимания bag of words
def canonize_words(words: list) -> list:
    stop_words = ('быть', 'мой', 'наш', 'ваш', 'их', 'его', 'её', 'их',
                  'этот', 'тот', 'где', 'который', 'либо', 'нибудь', 'нет', 'да')
    grammars = {'NOUN': '_S',
                'VERB': '_V', 'INFN': '_V', 'GRND': '_V', 'PRTF': '_V', 'PRTS': '_V',
                'ADJF': '_A', 'ADJS': '_A',
                'ADVB': '_ADV',
                'PRED': '_PRAEDIC'}

    morph = pymorphy2.MorphAnalyzer()
    normalized = []
    for i in words:
        forms = morph.parse(i)
        try:
            form = max(forms, key=lambda x: (x.score, x.methods_stack[0][2]))
        except Exception:
            form = forms[0]
            print(form)
        if not (form.tag.POS in ['PREP', 'CONJ', 'PRCL', 'NPRO', 'NUMR']
                or 'Name' in form.tag
                or 'UNKN' in form.tag
                or form.normal_form in stop_words):  # 'ADJF'
            normalized.append(form.normal_form + grammars.get(form.tag.POS, ''))
    return normalized



Код формирования модели данных
def make_data_model(file_name: str) -> dict:
    poems = read_poems(file_name)
    bags, voc = make_bags(poems)
    w2v_model = sem.load_w2v_model(sem.WORD2VEC_MODEL_FILE)
    sd = [sem.semantic_density(bag, w2v_model, unknown_coef=-0.001) for bag in bags]
    sa = [sem.semantic_association(bag, w2v_model) for bag in bags]
    rates = [0.0 for _ in range(len(poems))]
    return {'poems'       : poems,
            'bags'        : bags,
            'vocabulary'  : voc,
            'density'     : sd,
            'associations': sa,
            'rates'       : rates}



Общий анализ модели


Самый «твёрдый» пирожок:

убей обиду гнев и похоть
гордыню зависть и тоску
а то что от тебя осталось
из милосердия добей


Плотность, выжимка, ассоциативный список
 0.16305980883482543
['убить_V', 'обида_S', 'гнев_S', 'похоть_S', 'гордыня_S', 'зависть_S', 'тоска_S', 'остаться_V', 'милосердие_S', 'добить_V']
['жалость_S', 'ненависть_S', 'злоба_S', 'ревность_S', 'страх_S', 'страсть_S', 'злость_S', 'стыд_S', 'печаль_S', 'презрение_S']



Ожидаемо в топ «твёрдых» попали пирожки, в которых наличествует много близких по смыслу слов — как синонимичных и так антонимичных (причём нечётко) и без труда обобщаемых в какую-нибудь категорию (в данном случае — это эмоции).

Самый «мягкий» пирожок:

на этом судне мы спасёмся
не будь я прародитель ной
поставьте судно не дурачьтесь
больной


Плотность, выжимка, ассоциативный список
 -0.023802562235525036
['судно_S', 'спастись_V', 'прародитель_S', 'поставить_V', 'дурачиться_V', 'больной_A']
['корабль_S', 'заболевать_V', 'парусник_S', 'больной_S', 'заболеть_V', 'теплоход_S', 'бригантина_S', 'лодка_S', 'припадочный_S', 'бот_S']



Здесь отрицательная плотность, как мне кажется, во многом обусловлена тем, что в ассоциативный список не попал больничный смысл слова «судно». Это вообще одно из слабых мест дистрибутивно-семантических моделей — в них, как правило, одно значение лексемы подавляет популярностью все остальные.

Поисковые запросы по модели


О том как работает поиск, в сущности, было написано выше — пирожки беззатейливо сортируются по уровню их диффузии (обобщённого семантического сходства) со словами запроса.

Код
def similar_poems_idx(poem: str, poem_model, w2v_model, topn=5) -> list:
    poem_bag = dm.canonize_words(poem.split())
    similars = [(i, sem.semantic_similarity(poem_bag, bag, w2v_model))
                for i, bag in enumerate(poem_model['bags'])]
    similars.sort(key=lambda x: x[1], reverse=True)
    return similars[:topn]



Несколько примеров:

Сознание
>> pprint(similar_poems("сознание", pm, w2v, topn=5))
[('олег пытается не думать'
  'но мысли проникают в мозг'
  'скорей всего тому виною'
  'негерметичность головы',
  0.13678271365987432),
 ('моё прекрасно настроенье'
  'красивы тело и лицо'
  'лишь мысли грязные немного'
  'но что поделаешь весна',
  0.1337333519127788),
 ('по логике общаться с зомби'
  'весьма полезней чем с людьми'
  'для них мозги имеют ценность'
  'и важно что у вас внутри',
  0.12728715072640368),
 ('землянин где твоя землянка'
  'не в смысле выемка в земле'
  'а человек с твоей планеты'
  'и выемка внутри него',
  0.12420312280907075),
 ('вы ничего сказала ольга'
  'вы очень даже ничего'
  'ничтожество пустое место'
  'знак ноль на ткани бытия',
  0.11909834879893783)]



Свобода воли
>> pprint(similar_poems("свобода воли", pm, w2v, topn=5))
[('андрей любил немного выпить'
  'а много выпить не любил'
  'но заставлял себя надраться'
  'железной воли человек',
  0.12186796715891397),
 ('убей обиду гнев и похоть'
  'гордыню зависть и тоску'
  'а то что от тебя осталось'
  'из милосердия добей',
  0.10667187095852899),
 ('забудьте слово секс геннадий'
  'у нас в стране не принят секс'
  'у нас альтернатива сексу'
  'у нас любовь и доброта',
  0.10161426827828646),
 ('приятно быть кому то музой'
  'и знать что если бы не ты'
  'его бы творческому дару'
  'кранты',
  0.10136245188273822),
 ('я шла и на меня напали'
  'отняли всё но не смогли'
  'отнять любовь к земле и к людям'
  'и веру в мир и доброту',
  0.098855948557813059)]



Зима
>> pprint(similar_poems("зима", pm, w2v, topn=5))
[('зимой дороги убирают'
  'под грязный снег и гололед'
  'ну а когда зима минует'
  'дороги снова достают',
  0.1875936291758869),
 ('идет безногий анатолий'
  'стоп как же он идет без ног'
  'а так как снег идет как осень'
  'идет за летом в сентябре',
  0.18548772093805863),
 ('илья готовил сани летом'
  'телегу ладил он зимой'
  'так и возился постоянно'
  'кататься он не успевал',
  0.16475609244668787),
 ('захарий смотрит исподлобья'
  'на проходящую весну'
  'на девок бегающих в поле'
  'на трактор тонущий в реке',
  0.14671085483137575),
 ('я не хочу как все в могилу'
  'и в крематорий не хочу'
  'хочу быть скормленным весною'
  'грачу',
  0.13253569027346904)]



Очевидно, что поиск сквозь призму других дистрибутивно-семантических моделей (различные корпуса, алгоритмы обучения, размерности векторов) будет давать другие результаты. В целом технология работает, и работает весьма удовлетворительно. Нечёткий смысловой поиск реализуется легко и беззаботно (по крайней мере, на относительно небольшом объёме данных). В дальнейшем, если дойдут руки и, самое главное, догонит голова, планирую реализовать оценку рейтинга пирожков на основе обучающей выборки. Для начала — простой взвешенной суммой (в роли весового коэффициента будет выступать семантическая диффузия). Потом, возможно, пригодится что-нибудь из machine learning.

© Habrahabr.ru