Разделение смыслов через Sense Embeddings: как отличать омонимы

от автора

Привет, Хабр.

Иногда можно столкнуться с ситуацией, когда простые текстовые эмбеддинги не могут распознать контекст слова, и в итоге тот же «python» моделируется как змея, а не язык программирования (или наоборот).

Мы поговорим о том, что такое Sense Embeddings, почему контекстно‑зависимые модели всё ещё могут путаться, и как решить задачу омонимии.

Коротко: что такое WSD

Word Sense Disambiguation — это задача определять, в каком именно смысле используется слово. «Bank» может быть «берег реки» или «банк (фин.)»; «apple» — «фрукт» или «компания»; «python» — «язык программирования» или «змея». Каждое омонимичное слово порождает несколько разных «смыслов» (по‑английски — sense). Когда мы строим обычные Word2Vec, GloVe, FastText — на одно слово получается один вектор: он усреднён между всеми значениями. Но на практике часто нужно различать значения, потому что:

  • Для полнотекстового поиска в документах важно понимать, о чём речь.

  • При работе с медицинскими текстами «arrest» может означать «остановка сердца» или «арест» в криминальном смысле.

  • В новостях «apple» в контексте финансового отчёта не равен фрукту.

Проще говоря, нужно научить модель: «apple (компания) — это один вектор, а „apple (фрукт)“ — другой».

Если у нас статические эмбеддинги (word2vec, glove), то у слова «apple» будет ровно один вектор, усреднённый по всем контекстам в обучающем корпусе. Иногда мы пытаемся использовать контекстно‑зависимые эмбеддинги (GPT, BERT, ELMo, Flair), и это действительно улучшает понимание. Но даже они не всегда умеют «чётко фиксировать» конкретное значение. BERT зачастую обрабатывает контекст (предложение), а дальше можно смотреть на токен «apple» для уточнения, но в случае омонимии вблизи коротких контекстов модель может запутаться.

Sense Embeddings предлагают явное разделение:

  • «apple (company)» — вектор v1,v2,…v1, v2, …

  • «apple (fruit)» — вектор u1,u2,…u1, u2, …

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

Основные подходы к WSD

Существует несколько способов, как решать задачу разделения смыслов:

  1. Словари и лексические базы (WordNet, BabelNet).

  2. Статистические методы (алгоритм Lesk, адаптированные модели).

  3. Sense Embeddings (Sense2Vec, ELMo‑based, другие вариации).

  4. Комбинации контекстных эмбеддингов (BERT/Flair) с внешними базами.

В жизни никто не останавливается на чём‑то одном, а комбинируют методы в зависимости от задачи и сферы применения.

Как устроены Sense2Vec-эмбеддинги (пример с library)

Sense2Vec был изначально частью экосистемы spaCy. Идея в том, чтобы не просто обучать «слово → вектор», а обучать «слово|POS → вектор». Причём часть речи — это ещё не вся история: можно дополнительно ставить тэги вроде python|PROPN (если Python упоминается как имя собственное) и python|NOUN (если говорится о змее). Модель учится различать эти случаи и формирует несколько эмбеддингов.

Когда мы используем Sense2Vec, мы получаем готовую модель, в которой для слова «python» может быть три‑четыре вариации представлений, в зависимости от корпуса, и каждое представление «привязано» к определённой части речи/контексту. Если нужно ещё более тонкое разделение (например, «python|PROPN (programming language)» и «python|PROPN (Monty Python)»), то придётся вручную метить корпус и обучать на нём.

Установим:

pip install sense2vec

Далее нужно скачать модель, которая была предобучена на каком‑то корпусе (например, Reddit, новости и т. д.). Бывает, что модель распространяется отдельно (архив, который нужно распаковать), после чего загружаем его:

from sense2vec import Sense2Vec  # Пусть в каталоге /models/s2v_reddit разместили распакованный vocab s2v = Sense2Vec().from_disk("/models/s2v_reddit")  query = "python|PROPN" # Ищем похожие “смыслы” similar_senses = s2v.most_similar(query, n=5)  for sense, score in similar_senses:     print(sense, score)

В результате получим что‑то вроде:

('java|PROPN', 0.85) ('perl|PROPN', 0.84) ('programming|NOUN', 0.81) ...

Отлично, значит модель понимает, что python|PROPN ближе к другим языкам и понятиям, связанным с программированием.

Словари и базы: WordNet и BabelNet

Если нужна классика, где для каждого слова расписаны значения, WordNet из NLTK — отличный вариант. В нём, правда, нет встроенных векторов — там всё хранится как «synset» (набор синонимов) с определением, примерами, гипонимами/гиперонимами. Это помогает, когда нужно объяснить выбор модели: «мы выбрали смысл „apple (fruit)“, потому что в контексте речь о еде, а не о корпорации».

Код на Python (NLTK):

import nltk from nltk.corpus import wordnet  nltk.download('wordnet')  # Нужно один раз synsets = wordnet.synsets('apple') for syn in synsets:     print(syn.name(), syn.definition())

Вывод может выглядеть так:

apple.n.01 the fruit of the apple tree apple.n.02 a tech company from California...

В разных версиях WordNet определения немного отличаются.

BabelNet — это агрегатор WordNet + Wikipedia + Wiktionary и т. д. Там очень много информации, и есть API, которым можно пользоваться. Однако нужно оформить ключ, и это может быть накладно, если у вас большой трафик. Плюс BabelNet даёт много «шума»: у слова может быть 20 значений, из которых реально используется 2–3.

Ниже небольшой пример кода с использованием BabelNet API через HTTP‑запросы. Предполагается, что есть зарегистрированный API‑ключ:

import requests  API_KEY = "ВАШ_КЛЮЧ_ОТСЮДА" lemma = "apple" language = "EN"  # Сначала получаем список synset'ов для заданного слова url_synsets = (     f"https://babelnet.io/v6/getSynsetIds?lemma={lemma}"     f"&searchLang={language}&key={API_KEY}" ) response_synsets = requests.get(url_synsets) synset_ids = response_synsets.json()  print("Найдено synset ID:", synset_ids)  # Затем для каждого synset'а можно узнать подробную информацию for syn_id in synset_ids:     url_synset_info = f"https://babelnet.io/v6/getSynset?id={syn_id}&key={API_KEY}"     resp_info = requests.get(url_synset_info)     data = resp_info.json()     # Здесь вы можете парсить необходимые поля (glosses, senses, и т.д.)     print(f"\nSynset ID: {syn_id}")     print("Glosses:", data.get("glosses", []))     print("Senses:", data.get("senses", []))

Также можно продумать логику отбора: у одного слова может быть множество synset’ов, но реально использоваться в тексте только некоторые.

Flair, ELMo и другие контекстные модели

Когда мы говорим о разделении смыслов, стоит обратить внимание на современные контекстные эмбеддинги — их основная задача как раз в том, чтобы учитывать окружение (контекст) при формировании вектора для каждого токена. Если в классических Word2Vec/Glove на слово выделяется один усреднённый вектор, то модели вроде ELMo, GPT, BERT и Flair создают разные вектора одного и того же слова, если оно встречается в разных предложениях.

Установим Flair:

pip install flair

И возьмём небольшой пример:

from flair.embeddings import FlairEmbeddings, Sentence from scipy.spatial.distance import cosine  # Инициализируем предобученную модель Flair # "news-forward" – это одна из вариаций; есть еще "news-backward", "mix" и т.д. embedding = FlairEmbeddings('news-forward')  sentence1 = Sentence("I wrote a script in Python yesterday.") sentence2 = Sentence("Yesterday, I saw a python near the river.")  # Генерируем эмбеддинги для каждого токена в предложении embedding.embed(sentence1) embedding.embed(sentence2)  # Предположим, что в первом предложении 4-й токен = 'Python' vec_python_lang = sentence1[4].embedding  # Во втором предложении 5-й токен = 'python' (змея) vec_python_snake = sentence2[5].embedding  # Считаем косинусное сходство (через 1 - distance) similarity = 1 - cosine(vec_python_lang, vec_python_snake) print("Cosine similarity:", similarity)

Если мы выведем similarity, скорее всего окажется, что сходство (число от 0 до 1) не очень высокое, ведь модель на уровне контекстных эмбеддингов понимает, что в первом предложении «Python» — язык программирования, а во втором речь идёт о змее.

Flair (как и BERT, GPT, ELMo) пформирует разные скрытые состояния в зависимости от окружения слова, но не выделяет явным образом «вот это смысл № 1, вот это смысл № 2». Он просто строит вектор, отражающий конкретный контекст.

Возникает вопрос: «Где хранить варианты эмбеддинга?»

Допустим, у нас в тексте слово «apple» встречается десять раз, и в каждом случае контекст эмбеддинга может быть другим. В стандартных контекстных моделях нет механизма, который говорит:

«У меня есть три официальных значения „apple“: фрукт, компания и, возможно, имя домашнего питомца — держи три отдельных вектора на все времена.»

Вместо этого модель выдаст 10 похожих, но всё же немного разных векторов (один для каждого употребления), поскольку она формирует представление динамически. Если хотим получить чёткие «смыслы», нужно дополнительно сгруппировать эти вектора.

Как же из чистой контекстной модели получить дискретные смысловые вектора? Для этого используют методики кластеризации:

  1. Собираем все предложения, где встречается «apple».
    Например, выгружаем из корпуса 5–10 тысяч предложений, в которых встретилась лексема «apple».

  2. Прогоняем каждое предложение через BERT/Flair (или любую другую модель контекстных эмбеддингов).
    Извлекаем вектор соответствующего токена «apple» (или нескольких его сабтокенов, если речь о BERT — можно усреднить/брать последний слой).

  3. Получаем набор эмбеддингов (5–10 тысяч векторов — по одному на каждое употребление).

  4. Кластеризуем (DBSCAN или k‑Means).

    • Выбираем алгоритм кластеризации.

    • Устанавливаем гиперпараметры (например, число кластеров для k‑Means или eps для DBSCAN).

    • Запускаем алгоритм, чтобы сгруппировать эмбеддинги по «похожести» в подпространстве.

  5. Каждый кластер = потенциальный «смысл».

    • Например, в одном кластере модель соберёт контексты «apple Inc., apple product, apple event, ceo, iphone» и т. д. — то есть компания.

    • Во втором — «green apple, eat an apple, fruit basket» — то есть фрукт.

    • Если где‑то упоминалось иное значение, может сформироваться отдельный небольшой кластер.

  6. Назначаем «центральный вектор» кластера как «среднюю репрезентацию» (или медианную) для данного «смысла».
    Таким образом появляется фиксированный набор векторов — например, 2–3 кластера, по одному на каждый употребимый в реальном языке смысл слова «apple».

  7. Используем эти векторы для быстрого определения смысла при новом употреблении.
    Когда встречается новое предложение с «apple», снова извлекаем контекстный вектор, сравниваем его (по косинусному сходству) со всеми «кластерами смыслов». Если ближе всего к «cluster #1 (company)», значит модель решает, что это про корпорацию.

Flair, ELMo, BERT и им подобные отлично ловят смысл слова в конкретном предложении — вектор динамически подстраивается под окружение, и чаще всего этого хватает. Но если нужно жёстко разделить «вот тут фрукт, вот тут компания» и иметь отдельные вектора под каждый смысл — придётся самому кластеризовать эмбеддинги из разных контекстов. Работает, но требует корпуса и немного телодвижений. Важно понимать, сколько это будет есть ресурсов и как потом объяснять результат — тут может выручить WordNet или BabelNet с явными человеческими определениями. Контекстные модели — сильная база, но если нужны чёткие границы между смыслами, докручивать всё равно придётся.

Пример мини-сервиса

Допустим, хотим построить API, которое по входному слову и контексту возвращает «какой смысл был выбран». Можно сделать так:

  1. Храним sense‑эмбеддинги, либо используем Flair/BERT для получения контекстного вектора.

  2. Сопоставляем с набором известных смыслов слова (к примеру, из WordNet).

  3. Берём тот смысл, чей вектор ближе.

Проще всего представить в виде следующего кода:

from fastapi import FastAPI from pydantic import BaseModel import numpy as np from flair.embeddings import FlairEmbeddings, Sentence from scipy.spatial.distance import cosine  app = FastAPI() embedding = FlairEmbeddings('news-forward')  class DisambiguationRequest(BaseModel):     word: str     context: str     senses: list  # список строк, каждую мы хотим проверить  @app.post("/disambiguate/") def disambiguate(req: DisambiguationRequest):     # Сформируем контекстный эмбеддинг:     sentence = Sentence(req.context)     embedding.embed(sentence)      # Ищем токен, который совпадает с req.word (упрощённо):     target_vec = None     for token in sentence:         if token.text.lower() == req.word.lower():             target_vec = token.embedding             break          if target_vec is None:         return {"error": "No such word in context"}          # Заранее у нас может быть словарь sense -> эмбеддинг или описание. Предположим, упростим:     # Мы сами строим sentence для каждого sense:     best_sense = None     best_score = -1     for sense_description in req.senses:         # Например, берем короткое предложение "This is sense_description" или что-то более хитрое         s = Sentence(sense_description)         embedding.embed(s)         sense_vec = s[2].embedding  # "sense_description"         score = 1 - cosine(target_vec, sense_vec)         if score > best_score:             best_score = score             best_sense = sense_description          return {         "chosen_sense": best_sense,         "score": best_score     }

Тут очень много упрощений, но идея в том, что можно подобрать репрезентацию для каждого возможного смысла и сравнить её с тем, что извлекли из реального предложения. Или хранить в базе заранее обученные вектора для смысла. Это уже архитектурные детали самого приложения.

Как улучшить качество?

  1. Несколько контекстных моделей: берём BERT, Flair, ELMo, усредняем/конкатенируем векторы. Получаем более устойчивые представления.

  2. Дополнительные признаки: часть речи, лемма, NER‑тег. Часто POS‑тег уже даёт подсказку, какое значение у слова.

  3. Знания из словарей: можно свериться с WordNet — если в окружении слова есть «company», «stocks», «ceo», то «apple» почти точно про корпорацию.

  4. Кластеризация на кастомном корпусе: как упоминалось выше, если есть исторические тексты компании, где «python» встречается чаще как язык, вы явно выделяете это значение в модели.


В заключение всем интересующимся NLP рекомендую посетить открытые уроки в Otus:

  • 7 апреля. Используем BERT для решения NLP задач.
    На уроке вы узнаете, что представлет из себя модель BERT и как с ее помощью можно легко и эффективно решать разнообразные NLP задачи. Записаться

  • 21 апреля. Машинный перевод seq2seq: и как обучить модель понимать языки.
    Узнаете, как работает архитектура seq2seq и как она решает задачи машинного перевода. Записаться


ссылка на оригинал статьи https://habr.com/ru/articles/897236/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *