知识图谱自动化构建:从非结构化文本到三元组
(艾伦·图灵,国籍,英国) (艾伦·图灵,提出,图灵测试)
实体可以是人物、地点、概念、事件等,关系描述实体之间的语义联系。从文本中自动抽取三元组的过程被称为信息抽取,主要包含命名实体识别(NER)和关系抽取(RE)两个子任务。
### 自动构建的核心流程
非结构化文本 → 预处理 → 实体识别 → 实体对齐 → 关系抽取 → 三元组生成 → 知识图谱
本教程将重点介绍如何利用现有工具和开源模型,搭建成一个实用的自动化流水线,并以处理一篇科技新闻为例进行演示。
## 工具选型与安装
我们将使用 Python 结合以下主流库:
- **spaCy**:高性能自然语言处理库,内置命名实体识别和依存句法分析。
- **Hugging Face Transformers**:提供预训练的关系抽取模型,无需从零训练。
- **NetworkX**:用于存储和简单可视化图结构。
### 环境快速配置
```bash
pip install spacy transformers torch networkx matplotlib
python -m spacy download zh_core_web_trf # 中文Transformer模型,也可选择en_core_web_trf
以上命令安装了中文的大模型管道,其NER准确率高于轻量版,且支持基于上下文的词向量,利于后续关系抽取。
第一步:文本预处理与分句
原始文本需要切分为句子,因为关系通常存在于单句或相邻句范围内。
import spacy
nlp = spacy.load("zh_core_web_trf")
text = """
艾伦·图灵1912年出生于英国伦敦。他提出了著名的图灵测试,对计算机科学影响深远。
图灵还在第二次世界大战期间为盟军破译密码,其工作地点位于布莱切利园。
"""
doc = nlp(text)
sentences = [sent.text for sent in doc.sents]
print(sentences)
输出两个句子,后续在每个句子上独立进行实体识别和关系抽取。
第二步:命名实体识别与实体对齐
实体识别
利用 spaCy 的预训练NER组件直接提取实体。
entities = []
for sent in doc.sents:
for ent in sent.ents:
entities.append((ent.text, ent.label_, sent.text))
# 打印方式
print(entities)
示例输出实体:艾伦·图灵(PERSON)、1912年(DATE)、英国伦敦(GPE)、第二次世界大战(EVENT)等。
实体对齐(共指消解简化处理)
同一个实体可能在上下文中有多种指代,例如“图灵”和“他”都指向艾伦·图灵。可以使用简单的规则或引入共指消解工具(如neuralcoref,或基于Hugging Face的coref模型)。此处为简化,我们假定文本中实体指代已经清晰,若需要生产级系统,可使用fastcoref等库。
第三步:关系抽取方法选型
关系抽取可分为有监督、远程监督和少样本三种范式。对于快速搭建,推荐使用预训练的关系抽取模型,直接从句子中推理出关系类型。
使用 Hugging Face 零样本关系抽取管道
zero-shot-classification管道的理念是:给定一个句子、一个头实体和一个尾实体,模型判断它们之间属于哪种预定义关系(即使模型未见过)。
首先定义候选关系列表,例如:国籍、出生于、提出、工作于、位于。
from transformers import pipeline
classifier = pipeline("zero-shot-classification", model="MoritzLaurer/DeBERTa-v3-base-mnli-fever-anli")
# 也可使用支持中文的多语言模型,如 "joeddav/xlm-roberta-large-xnli"
对每一对实体组合和句子,构建推理模板:
def extract_relations(doc, candidate_relations):
triples = []
for sent in doc.sents:
entities = list(sent.ents)
for i in range(len(entities)):
for j in range(i+1, len(entities)):
e1 = entities[i]
e2 = entities[j]
# 构建假设模板:“e1 的 关系 是 e2”
sequence = f"{e1.text} 和 {e2.text}"
result = classifier(sequence, candidate_relations)
# 取置信度最高的关系
top_relation = result['labels'][0]
score = result['scores'][0]
if score > 0.7: # 阈值可调
triples.append((e1.text, top_relation, e2.text))
return triples
注意:零样本方法对模板设计敏感,需要根据语种和关系类型调整。更精确的做法是使用专门的RE模型,如通过huggingface的"Babelscape/rebel-large"(英文)或中文RE模型(如"wangfan/bert-chinese-relation-extraction"),本教程以零样本为例便于理解。
基于依存句法的规则抽取(辅助方法)
对于特定关系,可以使用依存路径进行抽取。例如,“出生于”关系常出现在“出生”动词连接出生地和人物。spaCy提供依存分析:
for token in doc:
if token.pos_ == "VERB" and token.lemma_ in ["出生", "提出", "工作"]:
# 查找主语和宾语作为实体
subj = [child for child in token.children if child.dep_ == "nsubj"]
obj = [child for child in token.children if child.dep_ == "obl"]
if subj and obj:
print(f"({subj[0].text}, {token.lemma_}, {obj[0].text})")
这种方法适用于抽取模式明确的关系,但泛化能力弱。实际系统中通常结合两者。
第四步:三元组生成与质量过滤
从句子中得到多个候选三元组后,需要确保主体和客体确实是实体,且关系有效。过滤策略:
- 实体类型检查:主体和客体应为特定类型(PERSON、ORG、GPE等)。
- 关系置信度阈值:保留零样本分数高于0.7的抽取结果。
- 去除过短或无效实体(如“他”、“这”)。
- 实体标准化:利用实体链接将名称归一化(如“图灵”和“艾伦·图灵”统一为知识库ID)。可使用Wikidata API或OpenTapioca等工具,本教程不做深入展开。
valid_triples = []
for triple in extracted:
if len(triple[0]) > 1 and len(triple[2]) > 1:
valid_triples.append(triple)
将三元组存储为列表或 CSV 文件,方便导入图数据库。
第五步:构建并可视化知识图谱
使用 NetworkX 快速构建图并可视化。
import networkx as nx
import matplotlib.pyplot as plt
G = nx.DiGraph()
for subj, rel, obj in valid_triples:
G.add_edge(subj, obj, label=rel)
plt.figure(figsize=(8,6))
pos = nx.spring_layout(G, seed=42)
nx.draw(G, pos, with_labels=True, node_color='lightblue', edge_color='gray', node_size=2000, font_size=10)
edge_labels = {(u,v): d['label'] for u,v,d in G.edges(data=True)}
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)
plt.show()
执行后将看到实体之间的连线及其关系标签,形成一个小型知识图谱的雏形。
进阶技巧与完整流水线示例
使用更先进的 RE 模型
Hugging Face 上有专门的关系抽取模型,例如 "Babelscape/rebel-large" 可以一次性输出所有三元组,无需指定关系类型,并且支持同时抽取多个三元组。更改为:
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
model = AutoModelForSeq2SeqLM.from_pretrained("Babelscape/rebel-large")
tokenizer = AutoTokenizer.from_pretrained("Babelscape/rebel-large")
gen_kwargs = {"max_length": 256, "length_penalty": 1.5, "num_beams": 5, "num_return_sequences": 1}
def extract_triplets(text):
model_inputs = tokenizer(text, max_length=256, padding=True, truncation=True, return_tensors='pt')
generated_tokens = model.generate(**model_inputs, **gen_kwargs)
decoded_preds = tokenizer.batch_decode(generated_tokens, skip_special_tokens=False)
# 解析模型输出为三元组(输出格式为 "head <triplet> tail <triplet> ...")
# 省略具体解析代码,可参考huggingface博客 "End-to-end relation extraction with REBEL"