Уже несколько месяцев с любопытством гляжу в сторону дистрибутивной семантики — познакомился с теорией, узнал про word2vec, нашёл соответствующую библиотеку для Питона (gensim) и даже раздобыл модель лексических векторов, сформированную по национальному корпусу русского языка. Однако для творческого погружения в материал не хватало душезабирающих данных, которые было бы интересно через дистрибутивную семантику покрутить. Одновременно с этим увлечённо почитывал стишки-пирожки (эдакий синтез задиристых частушек и глубокомысленных хокку) — некоторые даже заучивал наизусть и по случаю угощал знакомых. И вот, наконец, увлечённость и любопытство нашли друг друга, породив воодушевляющую идею в ассоциативных глубинах сознания — отчего бы не совместить приятное с полезным и не собрать из подручных средств какой-нибудь «поэтичный» поисковик по базе пирожков.
«Поэтичность» поиска предполагалось реализовать за счёт врождённой способности дистрибутивных векторов показывать степень семантического сходства лексем самым что ни на есть действительным числом (чем меньше угол между векторами слов, тем с большей вероятностью эти слова близки по смыслу — косинусная мера, классика жанра, в общем). Например, «принцесса» и «пастух» гораздо менее близки, чем «пастух» и «овца»: 0.139 против 0.603, что, наверное, логично — вектора национального корпуса должны отражать суровую реальность, а не сказочный мир Г.Х. Андерсена. Способ же расчёта глубины корреляции (диффузии) запроса и пирожка проявился практически сам собой (дёшево и сердито) как нормализованная сумма сходств каждого слова из списка X с каждым словом списка Y (стоп-слова выкидывались, все остальные приводились к нормальной форме, но об этом позже).
Результаты поэтического поиска и порадовали, и позабавили. Например, на запрос «музыка» был выдан следующий poem-list:
Здесь примечательно, что слова «музыка» нет ни в одном пирожке, из занесённых в базу. Однако все пирожковые ассоциации весьма музыкальны и степень их семантической диффузии с запросом довольно высока.
Теперь по порядку о проделанной работе (исходники на GitHub).
Первым делом из текстового файла (poems.txt), в котором содержатся стишки-пирожки (порядка восьми сотен), нарезается список, собственно, содержащий эти пирожки в виде строк. Далее из каждого строко-пирожка выжимается мешок слов (bag of words), в котором каждое слово приведено в нормальную грамматическую форму и из которого выкинуты шумовые слова. После чего для каждого мешка вычисляется семантическая «плотность» (интро-диффузия) пирожка и формируется специфический ассоциативный список (помогает понять, какой смысловой слой превалирует с точки зрения модели дистрибутивных векторов). Всё это удовольствие укладывается в словарь под соответствующие ключи и записывается в файл в формате json.
Самый «твёрдый» пирожок:
Ожидаемо в топ «твёрдых» попали пирожки, в которых наличествует много близких по смыслу слов — как синонимичных и так антонимичных (причём нечётко) и без труда обобщаемых в какую-нибудь категорию (в данном случае — это эмоции).
Самый «мягкий» пирожок:
Здесь отрицательная плотность, как мне кажется, во многом обусловлена тем, что в ассоциативный список не попал больничный смысл слова «судно». Это вообще одно из слабых мест дистрибутивно-семантических моделей — в них, как правило, одно значение лексемы подавляет популярностью все остальные.
О том как работает поиск, в сущности, было написано выше — пирожки беззатейливо сортируются по уровню их диффузии (обобщённого семантического сходства) со словами запроса.
Несколько примеров:
Очевидно, что поиск сквозь призму других дистрибутивно-семантических моделей (различные корпуса, алгоритмы обучения, размерности векторов) будет давать другие результаты. В целом технология работает, и работает весьма удовлетворительно. Нечёткий смысловой поиск реализуется легко и беззаботно (по крайней мере, на относительно небольшом объёме данных). В дальнейшем, если дойдут руки и, самое главное, догонит голова, планирую реализовать оценку рейтинга пирожков на основе обучающей выборки. Для начала — простой взвешенной суммой (в роли весового коэффициента будет выступать семантическая диффузия). Потом, возможно, пригодится что-нибудь из machine learning.
из ложных умозаключений
мы можем истину сложить
примерно как перемножают
два отрицательных числа
«Поэтичность» поиска предполагалось реализовать за счёт врождённой способности дистрибутивных векторов показывать степень семантического сходства лексем самым что ни на есть действительным числом (чем меньше угол между векторами слов, тем с большей вероятностью эти слова близки по смыслу — косинусная мера, классика жанра, в общем). Например, «принцесса» и «пастух» гораздо менее близки, чем «пастух» и «овца»: 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.
rocknrollnerd
Черт, вот на чем надо тренировать эти ваши рекуррентные сети. К черту Шекспира, нам нужен генератор пирожков! Не поделитесь, откуда выборку брали и можно ли достать еще?
По теме — выглядит круто, никогда не занимался текстом, но word2vec издалека выглядел очень интересной штукой. Дилетантский вопрос — а сделать условный шаг дальше и семантически сравнивать предложения/фразы кто-нибудь же наверняка пробовал, да?
drafterleo
Пирожки (и порошки :)) надёргал в интернете, сколько было не лень. Даже пришлось небольшой скриптик выдумывть, чтобы отсеять повторы. Честно говоря, сам был бы рад найти увесистый сборник одним файлом, но не нашёл.
Обрабатывать фразы word2vec, конечно же, уже пробовали (всё украдено до нас :)) — просто сливали несколько слов в одну лексему, а потом всё то же самое. В английском это проще, в русском сложней — у нас, в виду синтетичности языка, многие слова сами по себе уже маленькие предложения, отчего грамматическая нормализация вносит гораздо больше смысловых искажений.
Mayflower
На всякий случай — вот сайт с целой кучей пирожков perashki.ru
mihaild
Обучил lstm Карпатого на github.com/drafterleo/pie-poem/blob/master/poems.txt. Гласные и строчки считать более-менее научилось, слова — примерно, согласования никакого.
я был живёшь веселою клей
я тронангию истязный
чем банков можно сементами
стрелы не знала и назвать
прохожих как внором из солнцев
возьмите на девять силят
я надо открываю главку
и сразу сершия с небюс
rocknrollnerd
Черт, вы меня опередили) Сколько слоев/нейронов? Я пару ночей гонял два по 512, получается примерно такая же фигня:
мы все умер и под другой
тога значит режет стены
в водьше наступили к куру
труда не выновить в культор
кричит олег а под летом
воя с насон присопяты
закончится веси беспорность
а теля понячный продкайся
с Шекспиром у Карпатого как-то лучше получалось, так что я теперь пробую три слоя)
mihaild
2 слоя, 128 нейронов
drafterleo
rocknrollnerd
Я настолько никогда не занимался текстом, что даже не знаю, что это такое) Плохие результаты на текущем этапе, по крайней мере у меня, кажется, объясняются для начала тем, что я скармливаю сети слишком маленький кусок данных для предсказания следующей буквы (нужно по куску из 20 букв предсказать 21-ю). По той же ссылке на Карпатого у него сотня букв — попробую теперь так)
Насколько я понимаю, на пальцах это должно работать примерно так — сеть должна держать в памяти такое число знаков, чтобы в него помещалась структура, которую мы хотим воспроизвести. Если говорить только о рифме, то получается, что нужно помнить последние две строки, скажем. Если о смысле — то четыре, и тут мне уже просто интересно, насколько это вообще реально — воспроизвести осмысленные (согласованные) четыре строки подряд.
mihaild
Только она держит не сами знаки, а какую-то метаинформацию о них (см. картинки).
Я бы предположил, что проблема еще и в малом размере обучающей выборки (Шекспира было в полтора раза больше).
rocknrollnerd
Там ниже в комментах привезли еще — вместе получается 1.4 мегабайта примерно.
mihaild
1.4 в UTF8. Т.е. полезного почти в 2 раза меньше.
drafterleo
Абстрактные грамматики — штука в общем-то простая. По сути, вероятностный генератор (алгоритм) последовательностей символов на марковской цепи (несколько лет назад делал по этой теме программку, правда с уклоном в людей, а не в нейросети :)). Последовательности, сгенерированные разными алгоритмами, классифицируются на раз (через n-граммы или наивным байесом), а вот реконструировать алгоритм по последовательности (т.е. построить изоморфный генератор) — задача куда менее тривиальная. Судя по тому, что я вижу (эксперименты с шекспиром и пирожками), рекуррентные сети должны щёлкать абстрактные грамматики как семечки. По идее — школьная задачка в этой сфере (ведь можно легко проверить результат, посчитать степень корреляции генерируемых последовательностей). Но в сети ничего не нашёл (впрочем, искал не очень глубоко :)). Быть может, вам что-нибудь попадалось об этом?
rocknrollnerd
О, спасибо за ликбез) Быстрый гуглинг выдал вот такую штуку.
drafterleo
Да, это оно. Взаимное спасибо. Правда, там, насколько я разобрался, основной упор делается на распознавание грамматики, а не на генерацию «фальшивых» (в хорошем смысле этого слова :)) последовательностей. Хотя, если рекуррентная сеть может узнавать, то она, видимо, способна и продуцировать «подделки» малоотличимые от оригинала. Так?
varagian
Ещё стоило бы отметить, что дистрибутивная семантика является одним из краегольных камней алгоритмов глубокого обучения (и вообще NN и многих вероятностных моделей), поэтому очень полезно и важно с ней разобраться :-)
Подробнее можно посмотреть в видео-лекциях школы по глубокому обучению вот тут и ещё можно глянуть крутой курс от Udacity вот тут.
impwx
В результаты среди пирожков затесались порошки.
drafterleo
Насколько я смог уловить поэтику, «порошок» это искажённое произнесение «пирожка» — вроде как по смыслу то же самое, а по форме немного другое :).
gous32
Не совсем так. Пирожок — 9-8-9-8, ямб без рифмы, порошок или порох — 9-8-9-2, с рифмой 2-4, если память не изменяет. Исторически порошки появились из пирожков, но стали вполне себе самостоятельным форматом.
Есть еще другие похожие формы. Самая распространенная — двустрочная, например:
зачем учить нас как работать
вы научитесь как платить
© bazzlan
drafterleo
Вот видите как замечательно, я в порошке увидел озорного сынишку пирожка, а вы узрели воспламеняющий порох. Поэзия однако — стихия субъективной глубины :). В целом, соглашусь, порошки за счёт (неожиданной, как правило) рифмовки «отжигают» подинамичней пирожков. На мой взгляд, если хороший пирожок что-то в душе разминирует, то хороший порошок наоборот — подрывает. Вот мой любимый (и кажется в тему :))
Atos
хм, а мне тут виделась отсылка к мему «порошок уходи» :)
gous32
Вообще, пирожки можно печь на лету, если внутренний голос на ритм настроить. Плохие, но формально — пирожки.
Настолько просто это делать
что я в полуночном бреду
решил скостылить быстро скриптик
и всех поэтов заменить
И вообще да, надо в эту сторону подумать. А вы могли бы прислать ваш корпус пирожков для таких целей?
rocknrollnerd
В посте есть ссылка. Я тоже прямо заинтересовался)
drafterleo
С этими пирожками — главное не объестся. Я вот, чувствую, за последнюю неделю переел. Но всё же, если, вдруг, пополните корпус пирожков — надеюсь на ссылку алаверды :).
skjame
Да это же шикарно!!! как только появится свободное время, реализуются подобное у себя на машине и улучшу базу пирожков ^_^
Stas911
Очень интересно, спасибо! А по теории что почитать рекомендуете?
drafterleo
Откровенно говоря, затрудняюсь ответить, ибо не являюсь большим специалистом в этой области. Если что-то обзорное по дистрибутивной семантике — есть неплохая лекция Андрея Кутузова. Если поразбираться, что под капотом word2vec — вот, например, подробная статья со схемами и матаном.
WerewolfPrankster
Как я правильно понимаю похожесть слов «корабль» и «судно» уже встроенны в word2vec?
WerewolfPrankster
Ай ай, я уже нашел про модель лексических векторов.
drafterleo
;)
sebres
а что судно — утка? (чисто «научный» интерес))… особенно в сравнении с симили судна с другими водоплавающими (например — гусём)
и потом корабль с ними же…
drafterleo
sebres
спасибо, надо пощупать…
drafterleo
Благодаря стараниям коллеги wiygn (не поленился же человек, наковырял изюму :)) корпус пирожков существенно пополнился (теперь их больше 7500). Мусор я, вроде, вычистил, повторы своими кустарными средствами поудалял, однако если какой-нибудь добрый человек найдёт время перепроверить 1.3 мегабайта чистой поэзии — будет вообще хорошо.
hior
Ух, как интересно. Могу поделиться базой со своего Поэтория. Думаю теперь, вот, реализовать что-то подобное у себя.
drafterleo
Конечно же делитесь! Это прямо-таки из серии «мечты сбываются» — буквально неделю назад грезил о базе Поэтория :). Кстати, могли бы вы предоставить эту базу вместе с рейтингами — есть огромное желание протащить поэтический материал через машинное обучение на предмет предсказания качества (популярности) пирожка. Поверить, так сказать, алгеброй гармонию — посальерить моцартов :).
hior
Надо было не грезить, а написать сразу :) Отправил контакты в личку.
drafterleo
Опять-таки при содействии wiygn база пирожков существенно подросла (теперь в ней порядка 33000 штук — poems_33000.txt).
Примеры «насыщенного» поиска по экзотичным словам: