Relation Extraction: Извлечение связей между сущностями
nlprelation-extractionknowledge-graphsllmopensource
Введение
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 системы