ДокументацияПрактические руководства (Cookbooks)Сопоставление сущностей графа знаний

Сопоставление сущностей графа знаний

Decides which of 450 candidate pairs from two beer catalogues describe the same product. One TypeSafe Score question carries the whole decision, because its three levels are the three things you can do with a pair: merge it, leave it unlinked, or hand it to a curator. There is no threshold to fit, a…

Определение того, какие из 450 пар-кандидатов из двух каталогов пива описывают один и тот же продукт. Один вопрос TypeSafe Score решает всю задачу, поскольку три его уровня соответствуют трем возможным действиям: объединить, оставить без связи или передать на проверку куратору. Нет необходимости подбирать пороговые значения вручную, а три сопутствующих вопроса Noul в том же запросе сообщают куратору, по каким именно полям данные расходятся.

Ключевая проблема в графах знаний — определение того, дублирует ли новая сущность уже существующую, особенно когда в качестве источника доступны только разрозненные тексты на естественном языке. Для заданных пар потенциальных дубликатов один вопрос TypeSafe Score определяет, является ли пара дубликатом или требует детального анализа куратором.

Представьте, что два источника данных содержат пересекающиеся наборы одних и тех же объектов, и вам нужно сопоставить запись с одной стороны с соответствующей записью с другой стороны. В графах знаний такие записи называются сущностями (entities), и они содержат зафиксированные факты о каждом объекте. Быстрый и грубый первичный проход уже сопоставил два источника и отобрал 450 пар-кандидатов, заслуживающих пристального внимания. Остается вынести решение по каждой паре.

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

Решение формулируется как вопрос Score с отдельным уровнем для каждого из трех исходов:

  • different product (разные продукты) — оставить две сущности не связанными;
  • related, but possibly not the same (связанные, но, возможно, не одинаковые) — передать куратору-человеку для принятия решения;
  • same product (один и тот же продукт) — объединить сущности.

Мы используем вопрос типа Score, потому что хотим сопоставить семантическую метку (критерии оценки) непосредственно каждому исходу, включая промежуточный. Вопрос Noul мог бы решить эту задачу лишь косвенно через пороговую фильтрацию числовых вероятностей, а вопрос Choice потерял бы естественную упорядоченность трех исходов.

Затем для каждого поля сущности, которое мы хотим проанализировать, в том же самом запросе отправляются вопросы Noul о том, совпадают ли эти поля. Эти вопросы Noul предоставляют детализированную информацию для куратора, если итоговая оценка попадает между крайними уровнями шкалы.

В результате вы получаете функцию route(), которая принимает пару-кандидата и возвращает один из трех исходов без необходимости подбирать пороги под свои данные.

MERMAID ACTIONS={TRUE} THEME={NULL} api.wedstack.ru/v1
flowchart LR
    PAIR["одна пара кандидатов<br/><i>обе сущности, одно состояние</i>"] --> CALL

    subgraph CALL["один запрос, четыре вопроса"]
        direction TB
        S["<b>Score:</b> как соотносятся продукты?<br/>· разные продукты<br/>· похожие, но не факт<br/>· один и тот же продукт"]
        N["<b>Nouls:</b> по одному на каждое поле<br/>· совпадает название?<br/>· совпадает пивоварня?<br/>· совпадает сорт/стиль?"]
        S ~~~ N
    end

    S --> R{"округление до<br/>ближайшего уровня"}
    R -->|"разные"| DROP["оставить без связи"]
    R -->|"одинаковые"| M["утвердить sameAs"]
    R -->|"связанные"| Q["очередь куратора"]
    N -.->|"по какому полю<br/>не сошлись"| Q

Настройка

BASH THEME={NULL} api.wedstack.ru/v1
pip install matplotlib ipython "typesafe-sdk>=0.5.7" cooksafe --extra-index-url https://pypi.typesafe.ai/

Задайте TYPESAFE_API_KEY. Каждый вызов кэшируется в json_cache.json, поставляемый вместе с кукбуком, поэтому повторный запуск воспроизводит опубликованные числа без реальных обращений к API. Удалите этот файл для выполнения живых запросов.

Приведенные ниже показатели получены на модели jev-1.12.

PYTHON THEME={NULL} api.wedstack.ru/v1
import json
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

import matplotlib
import matplotlib.pyplot as plt
from cooksafe import JsonCache, make_playground_link
from IPython.display import Markdown, display
from typesafe_sdk import Noul, Score, TypeSafeClient

matplotlib.use("Agg")  # рендеринг без GUI

TYPESAFE_MODEL = "jev-1.12"
MAX_WORKERS = 6  # небольшой пул потоков для соблюдения рейт-лимитов

client = TypeSafeClient(
    api_key=os.environ.get(
        "TYPESAFE_API_KEY", "cache-only"
    ),
    base_url=os.environ.get("TYPESAFE_ENDPOINT"),
    timeout=120.0,
)
json_cache = JsonCache(Path("json_cache.json"))

Загрузка пар кандидатов

Пары взяты из публичного бенчмарка Beer коллекции Magellan: два каталога пива, спарсенных с разных сайтов и предварительно отфильтрованных до 450 пар-кандидатов. Каждая сущность содержит четыре поля: название (name), пивоварня (brewery), сорт (style) и крепость (alcohol content). Каждая пара также содержит эталонный ответ бенчмарка known_same_as.

Текст оставлен без предобработки: сущности HTML, апострофы в виде отдельных слов и артефакты кодировок сохранены как есть.

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

PYTHON THEME={NULL} api.wedstack.ru/v1
PAIRS = json.loads(Path("candidate_pairs.json").read_text(encoding="utf-8"))
BY_ID = {pair["id"]: pair for pair in PAIRS}

print(f"{len(PAIRS)} candidate pairs. The first one, as the model will see it:")
print(json.dumps({k: PAIRS[0][k] for k in ("entity_a", "entity_b")}, indent=2)[:420])
PLAINTEXT api.wedstack.ru/v1
450 candidate pairs. The first one, as the model will see it:
{
  "entity_a": {
    "name": "C N Red Imperial Red Ale",
    "brewery": "Redwood Lodge",
    "style": "American Amber / Red Ale",
    "abv": "8.10 %"
  },
  "entity_b": {
    "name": "Kinetic Infrared Imperial Red Ale",
    "brewery": "Kinetic Brewing Company",
    "style": "American Strong Ale",
    "abv": "9.30 %"
  }
}

Один вопрос Score и три вопроса Noul на каждую пару

Обе сущности помещаются в единое состояние как entity_a и entity_b, поэтому вопросы задаются о паре, а не о каждой стороне по отдельности. Все четыре вопроса передаются в одном запросе.

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

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

Словарь OUTCOME задает названия трех исходов. Исход слияния назван assert sameAs, так как предикат sameAs является стандартом для обозначения эквивалентности сущностей в графах знаний.

Для трех полей задаются вопросы Noul: название, пивоварня и стиль. Крепость напитка (алкоголь) не требует нейросетевого вопроса, так как сравнение чисел — это простая арифметика, реализуемая в коде.

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1

Пример обработки четырех характерных пар: c446 — один продукт, c427 — два разных продукта. Две другие пары попадают в средний уровень по разным причинам: у c100 одинаковые название и пивоварня, но формулировка стиля отличается в источниках; у c428 сравнивается базовый сорт пива с его фруктовой модификацией.

PYTHON THEME={NULL} api.wedstack.ru/v1
for pair_id in ("c446", "c427", "c100", "c428"):
    show(pair_id)
    print()
PLAINTEXT api.wedstack.ru/v1
c446  score 1.94  confidence 0.92  ->  assert sameAs
    Thomas Hooker Old Marley Barleywine           Thomas Hooker Brewing Company   American Barleywine
    Thomas Hooker Old Marley Barleywine           Thomas Hooker Brewing Company   Barley Wine
    name 0.97   brewery 0.99   style 0.81

c427  score 0.03  confidence 0.95  ->  leave unlinked
    Frost Quake Bourbon Barrel Aged Barley Wine   Wellington County Brewery       American Barleywine
    Lompoc Bourbon Barrel Aged Proletariat Red A  Lompoc Brewing                  Amber Ale
    name 0.02   brewery 0.09   style 0.08

c100  score 1.30  confidence 0.27  ->  curator queue
    Belle Gueule Rousse                           Brasseurs R.J.                  American Amber / Red A
    Belle Gueule Rousse                           Brasseurs RJ                    Amber Lager/Vienna
    name 0.95   brewery 0.94   style 0.35

c428  score 1.10  confidence 0.77  ->  curator queue
    Ambleside Amber Ale                           Bridge Brewing Company          American Amber / Red A
    Bridge Ambleside Amber Ale - Pomegranate & G  Bridge Brewing Company          Amber Ale
    name 0.63   brewery 0.98   style 0.74

Маршрутизация всех пар-кандидатов

PYTHON EXPANDABLE THEME={NULL} api.wedstack.ru/v1
PLAINTEXT api.wedstack.ru/v1
assert sameAs      40  ( 8.9%)
curator queue      50  (11.1%)
leave unlinked    360  (80.0%)
output

Два пороговых значения, в которых route() меняет ответ, — это точки отсечения (0.5 и 1.5). Большинство пар классифицируются автоматически: 360 получают оценку ниже 0.5, а 40 — выше 1.5. На долю куратора остается всего 50 пограничных пар (11.1%).

В этом наборе оценки не распределяются исключительно по целым числам. Большинство группируется около 0.25: у двух сортов пива может быть одинаковое название стиля или схожее оформление пивоварни, поэтому модель выделяет промежуточному уровню некоторую долю вероятности. Судьбу пары решает то, по какую сторону отсечения она оказалась.

Точки отсечения 0.5 и 1.5 не требуют настройки: обе естественным образом следуют из формулировок критериев уровней. Изменяя формулировку среднего уровня, вы можете гибко балансировать объем ручной работы куратора и точность автоматического связывания.

Открыть пример в Playground

Ссылка ниже открывает пару c428 в TypeSafe Playground: оценка составила 1.10, и пара была передана куратору. Здесь сопоставляются Ambleside Amber Ale и Bridge Ambleside Amber Ale - Pomegranate & Galena Hops от одной и той же пивоварни с одинаковой крепостью.

PYTHON THEME={NULL} api.wedstack.ru/v1
playground_link = make_playground_link(
    {"entity_a": BY_ID["c428"]["entity_a"], "entity_b": BY_ID["c428"]["entity_b"]},
    QUESTIONS,
    models=[TYPESAFE_MODEL],
)
display(
    Markdown(
        f"🔗 [Открыть эту пару и вопросы в TypeSafe Playground]({playground_link})"
    )
)

Открыть эту пару и вопросы в TypeSafe Playground →