Как предсказать гипероним слова (и зачем). Моё участие в соревновании по пополнению таксономии

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Как может машина понимать смысл слов и понятий, и вообще, что значит — понимать? Понимаете ли вы, например, что такое спаржа? Если вы скажете мне, что спаржа — это (1) травянистое растение, (2) съедобный овощ, и (3) сельскохозяйственная культура, то, наверное, я останусь убеждён, что вы действительно знакомы со спаржей. Лингвисты называют такие более общие понятия гиперонимами, и они довольно полезны для ИИ. Например, зная, что я не люблю овощи, робот-официант не стал бы предлагать мне блюда из спаржи. Но чтобы использовать подобные знания, надо сначала откуда-то их добыть.

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

Про гиперонимы и таксономию

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

Пример подграфа таксономии RuWordNet, связанного со спаржей

Готовые пары гипоним-гипероним можно найти в специальных словарях, тезаурусах, куда включены целые графы гиперонимов, таксономии. Это, например, wiktionary (есть питонячья обёртка), или WordNet и RuWordNet. Обычно единицей такого словаря является синсет — множество слов, обладающих примерно одинаковым смыслом. Многозначные слова входят в несколько синсетов сразу. Отношения гипоним-гипероним (и некоторые другие, например часть-целое или тема-объект темы) устанавливаются именно между синсетами.

Нафига?

У читателя может возникнуть закономерный вопрос: а зачем вообще в 2к20 нужны какие-то тезаурусы? Есть же машиннообученные word2vec, fastText, и даже простите BERT, почему бы не использовать их напрямую для всех задач? На самом деле, конечно, делать так можно, и все так обычно и делают. Но есть несколько "но":

  1. Модели, основанные на статистике со-встречаемости слов, смешивают в одну кучу разные виды связей между словами: схожесть написания, общую тему, отношения "общее/частное", "часть/целое", синонимы, антонимы… Если хочется работать с одним конкретным видом связанности слов, нужен дополнительный сигнал, и тезаурус — проверенный источник такого сигнала.
  2. Чисто статистические модели часто выдают непрозрачные результаты, а в некоторых задачах важна полная интерпретируемость. Опять же, проверенность словаря — решает.
  3. Как было видно из того же примера с кудахтаньем, статистические модели выдают довольно шумные результаты, и если есть способ дополнительно отфильтровать этот шум, то почему бы им не воспользоваться.

Кроме этих логических доводов, есть ещё и эстетические: тезаурусом в виде питонячьего пакета очень приятно пользоваться. Вы посмотрите сами, как удобно работать с синсетами:

for sense in wn.get_senses('замок'):     print(sense.synset) # Synset(id="126228-N", title="СРЕДНЕВЕКОВЫЙ ЗАМОК") # Synset(id="114707-N", title="ЗАМОК ДЛЯ ЗАПИРАНИЯ")

Для каждого синсета можно глядеть на гиперонимы...

wn.get_senses('спаржа')[0].synset.hypernyms # [Synset(id="348-N", title="ОВОЩИ"), #  Synset(id="4789-N", title="ТРАВЯНИСТОЕ РАСТЕНИЕ"), #  Synset(id="6878-N", title="ОВОЩНАЯ КУЛЬТУРА")]

… или, наоборот, на гипонимы

wn.get_senses('спаржа')[0].synset.hypernyms[0].hyponyms # [Synset(id="107993-N", title="АРТИШОК"), # Synset(id="108482-N", title="СПАРЖА"), # Synset(id="118660-N", title="ЗЕЛЕНЫЙ ГОРОШЕК"), # ...

Одно из забавных применений таксономии — измерять непохожесть между понятиями как сумму расстояний до ближайшего общего гиперонима. Возьмём, например, такую детскую задачку: нужно исключить одно из слов ДИВАН, ШКАФ, ЛАМПА, СТОЛ. Нарисуем подграф их гиперонимов (хоть он и странный):

По картинке видно, что расстояние от "лампы" до остальных предметов — больше, чем между ними, так что она тут лишняя. А если не хочется рисовать картинки, то вычислить расстояние по таксономии можно и кодом — простым обходом графа.

        ДИВАН   ШКАФ    ЛАМПА   СТОЛ ДИВАН   0       3       10      3        ШКАФ    3       0       5       2        ЛАМПА   10      5       0       7        СТОЛ    3       2       7       0

Задача

Словари типа RuWordNet очень качественные, потому что собраны лингвистами вручную. Но поэтому же наполнение таких словарей не очень высокое. Хотелось бы научиться добавлять новые понятия в таксономии автоматически, или хотя бы полуавтоматически (машина предлагает варианты, лингвист их утверждает). Для этого компьютерные лингвисты из Сколтеха и Вышки организовали соревнование (раз два три), приуроченное к конференции Диалог, и уже даже написали про это статью. Идея соревнования: для слова, пока не включённого в таксономию RuWordNet, надо найти его гиперонимы из этой таксономии, предложив 10 вариантов. Засчитывались как прямые гиперонимы слова-запроса, так их их гиперонимы (т.е. гиперонимы второго порядка). Искались гиперонимы и оценивались результаты раздельно для существительных и для глаголов. Подробнее на данные, скрипты для оценки и бейзлайны можно посмотреть в репозитории соревнования.

Наш алгоритм

Поставленная задача выглядит похоже на типичную задачу информационного поиска, только здесь нужно искать гиперонимы. А значит, можно попробовать решить эту задачу как поисковую: для входного слова-запроса отобрать кандидатов в гиперонимы, а потом переранжировать их по какой-то формуле. Конкретно у меня лучше всего взлетел как раз бейзлайновый вариант этого решения:

  1. Используя модель word2vec, вычислить эмбеддинги (представления в виде числовых векторов) для всех понятий в таксономии;
  2. Найти 100 ближайших соседей по сходству этих эмбеддингов с эмбеддингом слова-запроса;
  3. Заставить каждого из этих соседей "голосовать" за свои гиперонимы 1 и 2 порядка;
  4. Отранжировать гиперонимы-кандидаты по взвешенной сумме набранных голосов и отобрать первые 10.

Почему такое решение вообще может работать? Оказалось, что у 90% новых существительных и 99% новых глаголов есть "сёстры" в имеющейся таксономии, т.е. понятия с хотя бы одним общим гиперонимом. Эти "сёстры" по смыслу тесно связаны с запросом, а потому, согласно дистрибутивной гипотезе, часто встречаются рядом с теми же словами, рядом с которыми встречается и запрос. Значит, если сопоставить словам векторы из модели, обученной угадывать слово по контексту (например, word2vec, FastText, ELMO или BERT), то среди ближайших соседей слова по таким представлениям будет много "сестёр", и в качестве ответа можно использовать их гиперонимы.

Ещё несколько деталей алгоритма:

  • при использовании модели w2v, для слов, не входящих в её словарь, мы искали в словаре слова с самым длинным общим префиксом и использовали их эмбеддинги;
  • для представления понятий, составленных из нескольких слов, мы просто усредняли эмбеддинги этих слов с весами, зависящими от части речи (но можно придумать лучше);
  • мы L2-нормализовали эмбеддинги, чтобы было удобнее искать ближайших соседей по косинусному расстоянию;
  • каждый сосед голосовал за все свои гиперонимы 1 и 2 порядка, но голосам за гипероним 2 порядка давался вдвое меньший вес;
  • вес каждого голоса домножался на функцию, резко убывающую при росте расстояния от запроса до найденного соседа, чтобы самые близкие соседи получили преимущество в голосовании.

Более подробное описание и обсуждение моего алгоритма и решений других участников можно найти в сборнике конференции "Диалог".

Упрощённый код

Здесь разобран упрощённый питонячий код моего алгоритма. Полную версию модели, которую я засабмитил на лидерборд, можно посмотреть на гитхабе, но код там довольно грязный. Более простую и опрятную версию можно запустить, склонировав себе репозиторий python-ruwordnet.

Для работы с тезаурусом я пользуюсь самописной библиотекой ruwordnet, которая скоро появится на PyPI.

from ruwordnet import RuWordNet wn = RuWordNet() wn.load_from_xml(root='data')

Для получения векторов слов я использовал модель word2vec с сайта RusVectores. В качестве более легковесной альтернативы можно использовать сжатые вектора fastText. Вектор текста — нормализованная сумма векторов всех слов длиной хотя бы в 3 символа.

import numpy as np import compress_fasttext ft = compress_fasttext.models.CompressedFastTextKeyedVectors.load(     'https://github.com/avidale/compress-fasttext/releases/download/v0.0.1/ft_freqprune_100K_20K_pq_100.bin' ) def vectorize(text):     vec = np.sum([ft[word] for word in text.lower().split() if len(word) >= 3], axis=0)     vec /= sum(vec**2) ** 0.5      return vec

Вектора всех глагольных фраз можно положить в KDTree — одну из структур данных, позволяющих быстро искать ближайших соседей.

from sklearn.neighbors import KDTree words, vectors, synset_ids = [], [], [] for synset in wn.synsets:     if synset.part_of_speech != 'V':         continue     for sense in synset.sense:         words.append(sense.name)         vectors.append(vectorize(sense.name))         synset_ids.append(synset.id) vectors = np.stack(vectors) tree = KDTree(vectors)

Веса для соседей, найденных в дереве, будем вычислять в зависимости от расстояния до соседа, по вот такой формуле (я придумал её довольно стихийно, а параметры подобрал по сетке):

def distance2vote(d, a=3, b=5):     sim = np.maximum(0, 1 - d**2/2)     return np.exp(-d**a) * sim **b

Для примера, попробуем вычислить гиперонимы для слова "кудахтать". Первые три найденные соседа — так себе по качеству, и гиперонимы у них неправильные, но среди 97 других соседей есть "повизгивать", "гавкать", "фырчать", и много других животных звуков.

votes = Counter() dists, ids = tree.query(vectorize('кудахтать').reshape(1, -1), k=100) for idx, distance in zip(ids[0], dists[0]):     for hyper in wn[synset_ids[idx]].hypernyms:         votes[hyper.id] += distance2vote(distance)     print(words[idx], [t.title for t in wn[synset_ids[idx]].hypernyms]) # БАРАХТАТЬСЯ ['ДВИЖЕНИЕ, ПЕРЕМЕЩЕНИЕ', 'ПЛЕСКАТЬСЯ В ВОДЕ'] # ГОГОТАТЬ ['СМЕЯТЬСЯ (ИЗДАВАТЬ СМЕХ)', 'РАЗРАЗИТЬСЯ (БУРНО ВЫРАЗИТЬ)'] # ГУКАТЬ ['ПРОИЗНЕСТИ, ВЫГОВОРИТЬ, ПРОГОВОРИТЬ'] # ...

В результате после суммирования голосов правильный гипероним, "издать звук", лидирует с большим отрывом.

for sid, score in votes.most_common(10):     print(score,  wn[sid].title) # 0.6925543543920146 ИЗДАТЬ ЗВУК # 0.4306341411813687 ПРОИЗНЕСТИ, ВЫГОВОРИТЬ, ПРОГОВОРИТЬ # 0.2957854226709537 ДВИЖЕНИЕ, ПЕРЕМЕЩЕНИЕ # ...

Результаты

При оценке на тестовой выборке около 40% предложенных моделью кандидатов оказались настоящими гиперонимами слов-запросов. Это на 15% хуже, чем наилучшее решение для существительных (оно использовало кучу дополнительных источников данных — wordnet, викисловарь, результаты поиска в Яндексе и Гугле). Однако моё решение оказалось наилучшим для глаголов. Скорее всего, это означает, что искать гиперонимы для глаголов — в целом непростая задачка, и никто ещё не придумал, как решать её достаточно круто. Ну, что ж ?\_(?)_/?.

Какого рода косяки делает моя модель в тех 60% случаев, когда она не права? Есть несколько важных видов ошибок:

  • Попадание в тему, но неточное попадание в смысл слова. Например, для слова "заряжание" модель предсказала гиперонимы "прицеливание" и "лафет", которые тоже связаны с огнестрельным оружием, но не непосредственно с заряжанием.
  • Неумение обрабатывать многозначность. Например, для слова "выгорание" модель предсказала гипероним "гореть", проигнорировав другие смыслы этого слова — потерю цвета и эмоциональное выгорание.
  • Непонимание синтаксиса. Например, для фразы "прогревание больного места" модель предложила гиперонимы "больной человек" и "место в пространстве".
  • Неумение работать со словообразованием, незнание фактов о мире, неумение работать с абстрактными понятиями, путаница между субъектом и объектом глагола, и ещё много разных косяков.

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

Итак, хорошая новость в том, что даже если использовать очень простую модель, 40% предложенных ею гиперонимов годятся. И это достаточно много, чтобы имело смысл соединить в один конвейер роботов и людей: роботы предлагают гиперонимы, люди их валидируют, и таксономия пополняется очень быстро.

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


Источник: habr.com

Комментарии: