Alvaros
.
- Регистрация
- 14.05.16
- Сообщения
- 21.452
- Реакции
- 101
- Репутация
- 204
Как может машина понимать смысл слов и понятий, и вообще, что значит — понимать? Понимаете ли вы, например, что такое спаржа? Если вы скажете мне, что спаржа — это (1) травянистое растение, (2) съедобный овощ, и (3) сельскохозяйственная культура, то, наверное, я останусь убеждён, что вы действительно знакомы со спаржей. Лингвисты называют такие более общие понятия гиперонимами, и они довольно полезны для ИИ. Например, зная, что я не люблю овощи, робот-официант не стал бы предлагать мне блюда из спаржи. Но чтобы использовать подобные знания, надо сначала откуда-то их добыть.
В этом году компьютерные лингвисты организовали
Про гиперонимы и таксономию
Итак, ещё раз определение:
Пример подграфа таксономии RuWordNet, связанного со спаржей
Готовые пары гипоним-гипероним можно найти в специальных словарях, тезаурусах, куда включены целые графы гиперонимов, таксономии. Это, например,
Нафига?
У читателя может возникнуть закономерный вопрос: а зачем вообще в 2к20 нужны какие-то тезаурусы? Есть же машиннообученные word2vec, fastText, и даже простите BERT, почему бы не использовать их напрямую для всех задач? На самом деле, конечно, делать так можно, и все так обычно и делают. Но есть несколько "но":
Кроме этих логических доводов, есть ещё и эстетические: тезаурусом в виде питонячьего пакета очень приятно пользоваться. Вы посмотрите сами, как удобно работать с синсетами:
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 очень качественные, потому что собраны лингвистами вручную. Но поэтому же наполнение таких словарей не очень высокое. Хотелось бы научиться добавлять новые понятия в таксономии автоматически, или хотя бы полуавтоматически (машина предлагает варианты, лингвист их утверждает). Для этого компьютерные лингвисты из Сколтеха и Вышки организовали соревнование (
Наш алгоритм
Поставленная задача выглядит похоже на типичную задачу информационного поиска, только здесь нужно искать гиперонимы. А значит, можно попробовать решить эту задачу как поисковую: для входного слова-запроса отобрать кандидатов в гиперонимы, а потом переранжировать их по какой-то формуле. Конкретно у меня лучше всего взлетел как раз бейзлайновый вариант этого решения:
Почему такое решение вообще может работать? Оказалось, что у 90% новых существительных и 99% новых глаголов есть "сёстры" в имеющейся таксономии, т.е. понятия с хотя бы одним общим гиперонимом. Эти "сёстры" по смыслу тесно связаны с запросом, а потому, согласно
Ещё несколько деталей алгоритма:
Более подробное описание и обсуждение моего алгоритма и решений других участников можно найти в
Упрощённый код
Здесь разобран упрощённый питонячий код моего алгоритма. Полную версию модели, которую я засабмитил на лидерборд, можно посмотреть на
Для работы с тезаурусом я пользуюсь самописной библиотекой
from ruwordnet import RuWordNet
wn = RuWordNet()
wn.load_from_xml(root='data')
Для получения векторов слов я использовал модель word2vec с сайта
import numpy as np
import compress_fasttext
ft = compress_fasttext.models.CompressedFastTextKeyedVectors.load(
'
)
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% предложенных ею гиперонимов годятся. И это достаточно много, чтобы имело смысл соединить в один конвейер роботов и людей: роботы предлагают гиперонимы, люди их валидируют, и таксономия пополняется очень быстро.
Плохая же новость в том, что даже довольно сложные модели не смогли справиться сильно лучше, так что задачу обогащения таксономии пока что нельзя считать полностью решённой. Но на то мы и компьютерные лингвисты, чтобы не сдаваться (-:
В этом году компьютерные лингвисты организовали
You must be registered for see links
по поиску гиперонимов для новых слов. Я тоже попробовал в нём поучаствовать. Нормально получилось собрать только довольно примитивный алгоритм, основанный на поиске ближайших соседей по эмбеддингам из word2vec. Однако этот простой алгоритм каким-то образом оказался наилучшим решением для поиска гиперонимов для глаголов. Послушать про него можно в
You must be registered for see links
моего выступления, а если вы предпочитаете читать, то добро пожаловать под кат.
Про гиперонимы и таксономию
Итак, ещё раз определение:
You must be registered for see links
— это более общее понятие из пары, а гипоним — его частный случай. "Овощ" — гипероним по отношению к "спаржа", а "спаржа" — гипоним по отношению к "овощ". Прямых гиперонимов может быть много ("спаржа" — это не только "овощ", а ещё и "трава"). Кроме того, у гиперонимов могут быть свои собственные гиперонимы (так, "трава" — это частный случай "растения", а "растение" — частный случай "живого организма").
Пример подграфа таксономии RuWordNet, связанного со спаржей
Готовые пары гипоним-гипероним можно найти в специальных словарях, тезаурусах, куда включены целые графы гиперонимов, таксономии. Это, например,
You must be registered for see links
(есть питонячья
You must be registered for see links
), или
You must be registered for see links
и
You must be registered for see links
. Обычно единицей такого словаря является синсет — множество слов, обладающих примерно одинаковым смыслом. Многозначные слова входят в несколько синсетов сразу. Отношения гипоним-гипероним (и некоторые другие, например часть-целое или тема-объект темы) устанавливаются именно между синсетами.Нафига?
У читателя может возникнуть закономерный вопрос: а зачем вообще в 2к20 нужны какие-то тезаурусы? Есть же машиннообученные word2vec, fastText, и даже простите BERT, почему бы не использовать их напрямую для всех задач? На самом деле, конечно, делать так можно, и все так обычно и делают. Но есть несколько "но":
- Модели, основанные на статистике со-встречаемости слов, смешивают в одну кучу разные виды связей между словами: схожесть написания, общую тему, отношения "общее/частное", "часть/целое", синонимы, антонимы… Если хочется работать с одним конкретным видом связанности слов, нужен дополнительный сигнал, и тезаурус — проверенный источник такого сигнала.
- Чисто статистические модели часто выдают непрозрачные результаты, а в некоторых задачах важна полная интерпретируемость. Опять же, проверенность словаря — решает.
- Как было видно из того же примера с кудахтаньем, статистические модели выдают довольно шумные результаты, и если есть способ дополнительно отфильтровать этот шум, то почему бы им не воспользоваться.
Кроме этих логических доводов, есть ещё и эстетические: тезаурусом в виде питонячьего пакета очень приятно пользоваться. Вы посмотрите сами, как удобно работать с синсетами:
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 очень качественные, потому что собраны лингвистами вручную. Но поэтому же наполнение таких словарей не очень высокое. Хотелось бы научиться добавлять новые понятия в таксономии автоматически, или хотя бы полуавтоматически (машина предлагает варианты, лингвист их утверждает). Для этого компьютерные лингвисты из Сколтеха и Вышки организовали соревнование (
You must be registered for see links
You must be registered for see links
You must be registered for see links
), приуроченное к конференции
You must be registered for see links
, и уже даже написали про это
You must be registered for see links
. Идея соревнования: для слова, пока не включённого в таксономию
You must be registered for see links
, надо найти его гиперонимы из этой таксономии, предложив 10 вариантов. Засчитывались как прямые гиперонимы слова-запроса, так их их гиперонимы (т.е. гиперонимы второго порядка). Искались гиперонимы и оценивались результаты раздельно для существительных и для глаголов. Подробнее на данные, скрипты для оценки и бейзлайны можно посмотреть в
You must be registered for see links
.Наш алгоритм
Поставленная задача выглядит похоже на типичную задачу информационного поиска, только здесь нужно искать гиперонимы. А значит, можно попробовать решить эту задачу как поисковую: для входного слова-запроса отобрать кандидатов в гиперонимы, а потом переранжировать их по какой-то формуле. Конкретно у меня лучше всего взлетел как раз бейзлайновый вариант этого решения:
- Используя модель word2vec, вычислить эмбеддинги (представления в виде числовых векторов) для всех понятий в таксономии;
- Найти 100 ближайших соседей по сходству этих эмбеддингов с эмбеддингом слова-запроса;
- Заставить каждого из этих соседей "голосовать" за свои гиперонимы 1 и 2 порядка;
- Отранжировать гиперонимы-кандидаты по взвешенной сумме набранных голосов и отобрать первые 10.
Почему такое решение вообще может работать? Оказалось, что у 90% новых существительных и 99% новых глаголов есть "сёстры" в имеющейся таксономии, т.е. понятия с хотя бы одним общим гиперонимом. Эти "сёстры" по смыслу тесно связаны с запросом, а потому, согласно
You must be registered for see links
, часто встречаются рядом с теми же словами, рядом с которыми встречается и запрос. Значит, если сопоставить словам векторы из модели, обученной угадывать слово по контексту (например, word2vec, FastText, ELMO или BERT), то среди ближайших соседей слова по таким представлениям будет много "сестёр", и в качестве ответа можно использовать их гиперонимы.Ещё несколько деталей алгоритма:
- при использовании модели w2v, для слов, не входящих в её словарь, мы искали в словаре слова с самым длинным общим префиксом и использовали их эмбеддинги;
- для представления понятий, составленных из нескольких слов, мы просто усредняли эмбеддинги этих слов с весами, зависящими от части речи (но можно придумать лучше);
- мы L2-нормализовали эмбеддинги, чтобы было удобнее искать ближайших соседей по косинусному расстоянию;
- каждый сосед голосовал за все свои гиперонимы 1 и 2 порядка, но голосам за гипероним 2 порядка давался вдвое меньший вес;
- вес каждого голоса домножался на функцию, резко убывающую при росте расстояния от запроса до найденного соседа, чтобы самые близкие соседи получили преимущество в голосовании.
Более подробное описание и обсуждение моего алгоритма и решений других участников можно найти в
You must be registered for see links
.Упрощённый код
Здесь разобран упрощённый питонячий код моего алгоритма. Полную версию модели, которую я засабмитил на лидерборд, можно посмотреть на
You must be registered for see links
, но код там довольно грязный. Более простую и опрятную версию можно запустить, склонировав себе
You must be registered for see links
.Для работы с тезаурусом я пользуюсь самописной библиотекой
You must be registered for see links
, которая скоро появится на PyPI.from ruwordnet import RuWordNet
wn = RuWordNet()
wn.load_from_xml(root='data')
Для получения векторов слов я использовал модель word2vec с сайта
You must be registered for see links
. В качестве более легковесной альтернативы можно использовать
You must be registered for see links
вектора fastText. Вектор текста — нормализованная сумма векторов всех слов длиной хотя бы в 3 символа.import numpy as np
import compress_fasttext
ft = compress_fasttext.models.CompressedFastTextKeyedVectors.load(
'
You must be registered for see links
')
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% предложенных ею гиперонимов годятся. И это достаточно много, чтобы имело смысл соединить в один конвейер роботов и людей: роботы предлагают гиперонимы, люди их валидируют, и таксономия пополняется очень быстро.
Плохая же новость в том, что даже довольно сложные модели не смогли справиться сильно лучше, так что задачу обогащения таксономии пока что нельзя считать полностью решённой. Но на то мы и компьютерные лингвисты, чтобы не сдаваться (-:



