Relation Extraction: Извлечение связей между сущностями

nlprelation-extractionknowledge-graphsllmopensource
← Back to Blog

Введение

Relation Extraction (RE) — это задача определения отношений между именованными сущностями в тексте. Вместе с NER она является основой для построения knowledge graphs и извлечения структурированных данных из неструктурированного текста.


Что такое Relation Extraction?

Текст:
  "Илон Маск является CEO Tesla."

Сущности:
  Илон Маск → PERSON
  Tesla → ORGANIZATION

Отношения:
  (Илон Маск, CEO_of, Tesla)

Типичные отношения:

  • WORK_FOR — работает в
  • LOCATED_IN — расположен в
  • PART_OF — часть
  • FOUNDED_BY — основан
  • BORN_IN — родился в
  • SUBSIDARY_OF — дочерняя компания
  • MERGED_WITH — объединился с
  • INVESTED_IN — инвестировал в

Классические подходы

Классификация пар сущностей

from transformers import AutoModelForSequenceClassification, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("bert-base-multilingual")
model = AutoModelForSequenceClassification.from_pretrained(
    "relation-model",
    num_labels=len(RELATION_TYPES)
)

def extract_relation(text, ent1, ent2):
    # Создаём предложение с сущностями
    masked = text.replace(ent1, "[ENT1]", 1).replace(ent2, "[ENT2]", 1)
    input_text = f"[ENT1] {ent1} [ENT2] {ent2} : {masked}"
    
    inputs = tokenizer(input_text, return_tensors="pt")
    outputs = model(**inputs)
    prediction = torch.argmax(outputs.logits, dim=-1)
    return RELATION_TYPES[prediction]

CNN/BiLSTM для RE

class RelationClassifier(nn.Module):
    def __init__(self, hidden_dim, num_relations):
        super().__init__()
        self.lstm = nn.LSTM(
            input_size=768,
            hidden_size=hidden_dim,
            bidirectional=True
        )
        self.fc1 = nn.Linear(hidden_dim * 2, 128)
        self.fc2 = nn.Linear(128, num_relations)
        self.relu = nn.ReLU()
    
    def forward(self, ent1_emb, ent2_emb, context_emb):
        # Объединяем эмбеддинги сущностей и контекста
        combined = torch.cat([ent1_emb, ent2_emb, context_emb], dim=-1)
        lstm_out, _ = self.lstm(combined.unsqueeze(0))
        out = self.fc1(lstm_out.squeeze(0))
        out = self.relu(out)
        return self.fc2(out)

Distance-aware embedding

# Учитываем расстояние между сущностями
def position_aware_encoding(ent1_pos, ent2_pos, seq_len, embed_dim):
    """
    Кодируем позиции сущностей для лучшего понимания контекста
    """
    # Relative distance
    dist = abs(ent2_pos - ent1_pos)
    dist_embed = torch.sin(dist * 10000 ** (torch.arange(0, embed_dim//2) / embed_dim))
    
    # Absolute positions
    pos1_embed = torch.sin(ent1_pos * 10000 ** (torch.arange(0, embed_dim//4) / embed_dim))
    pos2_embed = torch.sin(ent2_pos * 10000 ** (torch.arange(0, embed_dim//4) / embed_dim))
    
    return torch.cat([dist_embed, pos1_embed, pos2_embed])

Relation Extraction с LLM

Zero-shot RE

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {
            "role": "system",
            "content": """Извлеки отношения между сущностями.
Типы отношений: WORK_FOR, LOCATED_IN, FOUNDED_BY, PART_OF, MERGED_WITH

Ответь в формате JSON:
{
  "relations": [
    {"subject": "...", "object": "...", "relation": "..."}
  ]
}"""
        },
        {"role": "user", "content": "OpenAI была основана в Сан-Франциско. Илон Маск работает в Tesla и SpaceX."}
    ],
    response_format={"type": "json_object"}
)

# Результат:
# {
#   "relations": [
#     {"subject": "OpenAI", "object": "Сан-Франциско", "relation": "LOCATED_IN"},
#     {"subject": "Илон Маск", "object": "Tesla", "relation": "WORK_FOR"},
#     {"subject": "Илон Маск", "object": "SpaceX", "relation": "WORK_FOR"}
#   ]
# }

Few-shot RE

messages = [
    {"role": "system", "content": "Извлекай отношения в формате JSON."},
    
    # Пример 1
    {"role": "user", "content": "Google купил YouTube за 1.65 миллиардов долларов."},
    {"role": "assistant", "content": '{"relations": [{"subject": "Google", "object": "YouTube", "relation": "ACQUIRED"}]}'},
    
    # Пример 2
    {"role": "user", "content": "Apple находится в Купертино, Калифорния."},
    {"role": "assistant", "content": '{"relations": [{"subject": "Apple", "object": "Купертино", "relation": "LOCATED_IN"}]}'},
    
    # Запрос
    {"role": "user", "content": "Microsoft приобрела GitHub в 2018 году."}
]

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages
)

Structured output с llama.cpp

import requests

prompt = """Извлеки отношения между сущностями.

Текст: "В 2022 году Meta представила LLaMA, открытую модель для исследователей."

Отношения:
- Meta → ORGANIZATION
- LLaMA → PRODUCT
- 2022 → DATE

Результат:
{
  "relations": [
    {"subject": "Meta", "object": "LLaMA", "relation": "CREATED"},
    {"subject": "LLaMA", "object": "2022", "relation": "RELEASED_IN"}
  ]
}"""

response = requests.post(
    "http://localhost:8080/completion",
    json={
        "prompt": prompt,
        "max_tokens": 300,
        "temperature": 0.1
    }
)

Построение Knowledge Graph

Pipeline: NER → RE → KG

Текст:
  "Amazon основал Bezos в 1994 году в Сиэтле."

Step 1 - NER:
  Amazon → ORG
  Bezos → PERSON
  1994 → DATE
  Сиэтле → LOC

Step 2 - RE:
  (Bezos, founded, Amazon)
  (Amazon, located_in, Сиэтле)
  (Amazon, founded_in, 1994)

Step 3 - KG:
  Nodes: [Amazon, Bezos, 1994, Сиэтле]
  Edges: [founded, located_in, founded_in]

Код построения KG

import networkx as nx

def build_knowledge_graph(text):
    # Step 1: NER
    entities = ner_extract(text)
    
    # Step 2: RE
    relations = re_extract(text, entities)
    
    # Step 3: Build graph
    graph = nx.DiGraph()
    
    for ent in entities:
        graph.add_node(ent["text"], type=ent["type"])
    
    for rel in relations:
        graph.add_edge(
            rel["subject"],
            rel["object"],
            relation=rel["relation"]
        )
    
    return graph

# Использование
graph = build_knowledge_graph(
    "Apple была основана Стивом Джобсом в 1976 году в Калифорнии."
)

# Запрос: кто основал Apple?
founders = [
    (u, v) for u, v, d in graph.edges(data=True)
    if d["relation"] == "FOUNDED"
]

Визуализация KG

import matplotlib.pyplot as plt
import networkx as nx

graph = nx.DiGraph()
graph.add_edges_from([
    ("Илон Маск", "Tesla", {"relation": "CEO"}),
    ("Илон Маск", "SpaceX", {"relation": "CEO"}),
    ("Tesla", "США", {"relation": "LOCATED_IN"}),
    ("SpaceX", "США", {"relation": "LOCATED_IN"})
])

# Цвета по типам
colors = {
    "PERSON": "#FFB6C1",
    "ORG": "#87CEEB",
    "LOC": "#90EE90"
}

pos = nx.spring_layout(graph)
nx.draw(graph, pos, with_labels=True, 
        node_color=[colors.get(graph.nodes[n].get("type"), "#FFF"), 
                   for n in graph.nodes()])
plt.show()

Типы Relation Extraction задач

Single-relation extraction

Одно отношение на пару:
  (Илон Маск, CEO_of, Tesla) → WORK_FOR

Multi-relation extraction

Несколько отношений на пару:
  (OpenAI, GPT-4) → CREATED
  (OpenAI, GPT-4) → PUBLISHED_IN
  (OpenAI, GPT-4) → TRAINED_ON

Event extraction

Извлечение событий:
  Текст: "Apple представила iPhone 15 в сентябре 2023."
  
  Event:
    Type: PRODUCT_LAUNCH
    Entity: iPhone 15
    Company: Apple
    Date: сентябрь 2023

Fine-tuning для RE

Подготовка данных

{"text": "OpenAI выпустила GPT-4.", "entities": [{"text": "OpenAI", "type": "ORG"}, {"text": "GPT-4", "type": "PRODUCT"}], "relation": {"subject": "OpenAI", "object": "GPT-4", "type": "CREATED"}}
{"text": "Google находится в Mountain View.", "entities": [{"text": "Google", "type": "ORG"}, {"text": "Mountain View", "type": "LOC"}], "relation": {"subject": "Google", "object": "Mountain View", "type": "LOCATED_IN"}}

Fine-tuning DeBERTa для RE

from transformers import AutoModelForSequenceClassification, Trainer, TrainingArguments

model = AutoModelForSequenceClassification.from_pretrained(
    "microsoft/deberta-v3-large",
    num_labels=num_relation_types
)

# Input: [text, ent1_start, ent1_end, ent2_start, ent2_end]
def preprocess_function(examples):
    inputs = tokenizer(
        examples["text"],
        max_length=512,
        truncation=True
    )
    # Добавляем позиции сущностей
    inputs["ent1_start"] = examples["ent1_start"]
    inputs["ent1_end"] = examples["ent1_end"]
    inputs["ent2_start"] = examples["ent2_start"]
    inputs["ent2_end"] = examples["ent2_end"]
    return inputs

trainer = Trainer(
    model=model,
    args=TrainingArguments(
        output_dir="./re-model",
        num_train_epochs=5,
        per_device_train_batch_size=16
    ),
    train_dataset=dataset
)

trainer.train()

RE в составе RAG

Улучшение RAG через relations

from llama_index.core import VectorStoreIndex, KnowledgeGraphIndex

# Обычный RAG
index = VectorStoreIndex.from_documents(documents)

# RAG с knowledge graph
kg_index = KnowledgeGraphIndex(
    documents,
    max_triplets_per_chunk=5,
    storage_context=storage
)

# Hybrid search
query_engine = kg_index.as_query_engine(
    similarity_top_k=3,
    kg_top_k=2
)

response = query_engine.query("Кто работает в каких компаниях?")

Graph-enhanced retrieval

def graph_enanced_retrieval(query):
    # 1. Извлекаем сущности из запроса
    query_ents = ner_extract(query)
    
    # 2. Ищем связанные узлы в KG
    related_nodes = []
    for ent in query_ents:
        neighbors = kg.get_neighbors(ent["text"])
        related_nodes.extend(neighbors)
    
    # 3. Извлекаем контекст
    context = []
    for node in related_nodes:
        context.append(kg.get_context(node))
    
    # 4. Формируем ответ
    return llm.generate(query, context)

Оценка Relation Extraction

Метрики

Exact Match:
  Предсказанное отношение должно точно совпадать с реальным
  
F1 для каждого типа отношения:
  Precision: сколько правильных предсказаний
  Recall: сколько нашли из всех правильных
  
Graph-level metrics:
  Entity F1: точность извлечения сущностей
  Relation F1: точность определения отношений
  Joint F1: совместная точность

Код оценки

def evaluate_re(true_relations, pred_relations):
    exact_matches = 0
    for true in true_relations:
        for pred in pred_relations:
            if (true["subject"] == pred["subject"] and
                true["object"] == pred["object"] and
                true["type"] == pred["type"]):
                exact_matches += 1
                break
    
    precision = exact_matches / len(pred_relations) if pred_relations else 0
    recall = exact_matches / len(true_relations) if true_relations else 0
    f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0
    
    return {"precision": precision, "recall": recall, "f1": f1}

Практические применения

1. Автоматическое заполнение KG

# Извлечение из новостей
articles = load_tech_news()

for article in articles:
    ents = ner_extract(article["text"])
    rels = re_extract(article["text"], ents)
    
    for rel in rels:
        kg.add_triplet(
            rel["subject"],
            rel["relation"],
            rel["object"]
        )

2. Поиск инсайдеров

# Кто связан с кем в бизнесе
def find_connections(company):
    nodes = kg.get_neighbors(company)
    connections = {}
    
    for node in nodes:
        relations = kg.get_edge_relations(company, node)
        connections[node] = relations
    
    return connections

# Результат:
# Tesla: {"Илон Маск": ["CEO"], "SpaceX": ["SAME_CEO"]}

3. Автоматическое аннотирование

# Аннотирование научных статей
paper = load_paper("attention-is-all-you-need.pdf")

ents = ner_extract(paper["text"], types=["METHOD", "DATASET", "RESULT"])
rels = re_extract(paper["text"], ents)

# Результат:
# (Transformer, achieves, 68.4 BLEU on WMT)
# (Attention, used_in, Transformer)

Open Source инструменты

Библиотеки:
  ✅ spaCy (relation extraction)
  ✅ Stanza (Stanford RE)
  ✅ HuggingFace transformers
  ✅ OpenIE (Stanford)

KG хранилища:
  ✅ Neo4j (Community)
  ✅ RDFLib (Python)
  ✅ NetworkX (Python)
  ✅ Apache Jena

API:
  ✅ Google Cloud NLP
  ✅ AWS Comprehend
  ✅ Azure Language

Итоги

Relation Extraction — ключевой компонент для извлечения знаний из текста. LLM значительно упрощают задачу, позволяя zero-shot подходу работать с любыми типами отношений.

Ключевые выводы:

  • Zero-shot RE с GPT-4o покрывает большинство случаев
  • Fine-tuning улучшает точность для домена
  • RE + NER = автоматическое построение KG
  • Knowledge graphs улучшают RAG системы