知识图谱构建实战:实体识别与关系抽取

“知识图谱的价值取决于知识的质量——而知识的质量取决于抽取的精度。”

一、知识构建的整体流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
原始数据(文本/表格/数据库)

┌───────────────────────────────────────┐
│ Step 1: 知识抽取 │
│ - 命名实体识别(NER):提取实体 │
│ - 关系抽取(RE):提取关系 │
│ - 事件抽取:提取事件及参与者 │
├───────────────────────────────────────┤
│ Step 2: 实体链接 │
│ - 实体消歧:解决"苹果"是多义词 │
│ - 实体统一:将不同写法映射到同一实体 │
├───────────────────────────────────────┤
│ Step 3: 知识融合 │
│ - 实体对齐:合并来自不同源的重复实体 │
│ - 冲突检测:检测矛盾的知识 │
├───────────────────────────────────────┤
│ Step 4: 知识补全 │
│ - 知识推理:发现缺失的知识 │
│ - 知识验证:检测错误的知识 │
└───────────────────────────────────────┘

知识图谱(三元组存储)

二、命名实体识别(NER)

2.1 什么是 NER?

NER(Named Entity Recognition)是从文本中识别出有特定意义的实体,并将其分类到预定义类别。

1
2
3
4
5
6
7
8
# 示例
文本: "马化腾创办了腾讯公司,总部位于深圳。"

NER 结果:
实体 类别 位置
马化腾 → 人物 [0, 3]
腾讯公司 → 组织 [6, 10]
深圳 → 地点 [14, 16]

常见实体类型:

类别 示例
人物(PER) 马化腾、习近平
组织(ORG) 腾讯、Google
地点(LOC) 北京、深圳
时间(TIME) 1998年、昨天
货币(MON) 100万美元
百分比(PERCENT) 增长 30%

2.2 基于规则的方法

早期的 NER 主要依赖规则,但适用性有限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import re

def rule_based_ner(text):
"""基于规则的 NER(简单示例)"""
entities = []

# 规则1:检测公司后缀
company_pattern = re.compile(r'[\u4e00-\u9fa5]{2,10}(公司|集团|企业|医院|学校)')
for match in company_pattern.finditer(text):
entities.append({
"text": match.group(),
"type": "ORG",
"start": match.start(),
"end": match.end()
})

# 规则2:检测"XX是YY"的模式
# 简单但有效
return entities

# 缺点:规则难以覆盖所有情况,容易误识别

2.3 基于深度学习的 NER

现代 NER 主要使用 BERT + CRF / BERT + Span 方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch

class NERModel:
def __init__(self, model_name="dslim/bert-base-NER"):
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
self.model = AutoModelForTokenClassification.from_pretrained(model_name)
self.labels = ['O', 'B-MISC', 'I-MISC', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC']

def extract_entities(self, text):
inputs = self.tokenizer(text, return_tensors="pt", truncation=True, max_length=512)

with torch.no_grad():
outputs = self.model(**inputs)

predictions = torch.argmax(outputs.logits, dim=2)[0].tolist()
tokens = self.tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])

entities = []
current_entity = None

for token, pred in zip(tokens, predictions):
label = self.labels[pred]

if label.startswith('B-'): # Begin
if current_entity:
entities.append(current_entity)
current_entity = {"text": token, "type": label[2:], "start": None}
elif label.startswith('I-') and current_entity: # Inside
current_entity["text"] += token.replace("##", "")
else: # O (Outside)
if current_entity:
entities.append(current_entity)
current_entity = None

if current_entity:
entities.append(current_entity)

return entities

# 使用
ner = NERModel()
entities = ner.extract_entities("Bill Gates founded Microsoft in New Mexico")
# [{'text': 'Bill Gates', 'type': 'PER'},
# {'text': 'Microsoft', 'type': 'ORG'},
# {'text': 'New Mexico', 'type': 'LOC'}]

2.4 中文 NER 的挑战

中文 NER 比英文更难,因为:

1
2
3
4
5
6
7
8
英文: "Elon Musk founded SpaceX in California"
分词: [Elon Musk] [founded] [SpaceX] [in] [California]
识别: B-PER I-PER O B-ORG O B-LOC I-LOC

中文: "埃隆·马斯克创办了SpaceX"
分词: 埃隆 / 马斯克 / 创办 / 了 / SpaceX
NER: ↓_________↓ O O O ↓______↓
人物实体 B-PER I-PER 组织 B-ORG I-ORG

挑战

  1. 没有天然分词边界:英文 tokenize 后每个词有空格分隔,中文需要额外分词
  2. 嵌套实体:”清华大学”是组织,”清华”是大学的一部分,中文里嵌套很常见
  3. 简称与全称:”腾讯”和”深圳市腾讯计算机系统有限公司”是同一个实体

解决方案:使用 BERT-Biaffine-NERGlobalPointer 等嵌套NER模型。

1
2
3
4
5
6
7
8
from transformers import BertTokenizerFast
from modelscope.outputs import OutputKeys
from modelscope.pipelines import pipeline

# 使用中文 NER 模型(大规模预训练中文模型)
p = pipeline("token-classification", model="damo/nlp_raner_named-entity-recognition_chinese-base-company")
result = p("腾讯是一家中国互联网巨头,总部位于深圳")
# 输出: [{'entity': '腾讯', 'type': '企业'}, {'entity': '深圳', 'type': '地点'}]

三、关系抽取(Relation Extraction)

3.1 什么是关系抽取?

关系抽取是从文本中识别出实体之间的语义关系。

1
2
3
4
5
文本: "马化腾创办了腾讯公司"

三元组:
("马化腾", "创办", "腾讯公司")
主语 谓语 宾语

3.2 关系类型

关系类型 示例
创始人关系 马化腾 — 创办 — 腾讯
任职关系 张小龙 — 任职于 — 腾讯
地理位置关系 腾讯 — 总部位于 — 深圳
产品关系 腾讯 — 产品 — 微信
股权关系 马化腾 — 持股 — 腾讯
竞争关系 腾讯 — 竞争 — 阿里巴巴
合作关系 腾讯 — 合作 — 京东

3.3 关系抽取的方法

方法1:监督学习(Supervised RE)

1
2
3
4
5
6
7
8
9
10
11
# Pipeline 方式:先识别实体,再判断关系
# Step 1: NER
entities = ner.extract_entities("马化腾创办了腾讯")

# Step 2: 对每对实体判断关系
for entity1 in entities:
for entity2 in entities:
if entity1 != entity2:
relation = relation_classifier(entity1, entity2, text)
if relation != "无关系":
triplets.append((entity1, relation, entity2))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 关系分类模型示例
from transformers import BertModel, BertPreTrainedModel

class RelationClassifier(BertPreTrainedModel):
def __init__(self, config, num_labels):
super().__init__(config)
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_size)
self.classifier = nn.Linear(config.hidden_size * 3, num_labels)

def forward(self, input_ids, attention_mask, entity_positions):
outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)

# 获取句子向量和实体向量
sequence_output = outputs.last_hidden_state

# 将三个向量拼接:[CLS] + 头实体 + 尾实体
pooled = torch.cat([
sequence_output[:, 0, :], # 句子表示
head_entity_vec,
tail_entity_vec
], dim=-1)

logits = self.classifier(self.dropout(pooled))
return logits

方法2:远程监督(Distant Supervision)

核心思想:利用已有知识图谱自动生成训练数据,减少人工标注。

1
2
3
4
5
6
7
8
9
10
# 远程监督示例
# 假设 Wikidata 中已有 ("马化腾", "创始人", "腾讯")

# 从 Wikipedia 中自动抽取包含这对实体的句子作为正例:
句子1: "马化腾是腾讯公司的创始人"
句子2: "1998年,马化腾与张志东等人共同创办了腾讯"

# 自动生成标注数据:
# (句子1, "创始人")
# (句子2, "创始人")

问题:远程监督会引入噪声标签——实体对同时出现在某关系中,不代表包含该关系的句子就表达了这种关系。

缓解方法:多示例学习(Multi-Instance Learning),将句子打包成袋(bag),只对袋子做标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# PCNN(多示例学习)关系抽取
class PCNN(nn.Module):
def __init__(self, embedding_dim, hidden_dim, num_relations):
super().__init__()
self.embedding = nn.Embedding(vocab_size, embedding_dim)
self.position_embedding = nn.Embedding(max_seq_len * 2, embedding_dim)
self.conv = nn.Conv2d(1, 100, (3, embedding_dim * 3))
self.maxpool = nn.AdaptiveMaxPool1d(3) # Piecewise Max Pooling
self.classifier = nn.Linear(300, num_relations)

def forward(self, x):
# x: [batch, seq_len, embedding_dim * 3]
# Conv → MaxPool → Linear
x = x.unsqueeze(1) # [batch, 1, seq_len, embedding_dim * 3]
x = torch.relu(self.conv(x))
x = x.squeeze(-1)
x = self.maxpool(x).view(x.size(0), -1)
return self.classifier(x)

方法3:Prompt-based RE(大模型时代)

用大模型做关系抽取,无需训练:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from openai import OpenAI

def llm_relation_extraction(text: str, entities: list) -> list:
"""用 GPT-4 做零样本关系抽取"""
entity_str = ", ".join([f"{e['text']}({e['type']})" for e in entities])

prompt = f"""
从以下文本中抽取实体之间的关系,以三元组格式输出。

文本:{text}

已识别的实体:{entity_str}

请输出所有实体对之间的关系,格式:
(主体, 关系, 客体)

如果没有特殊关系,输出"无特殊关系"。
"""

response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}]
)

return parse_triplets(response.choices[0].message.content)

3.4 联合抽取(Joint Extraction)

传统 Pipeline 方法(先NER后RE)会积累错误。联合抽取模型同时做 NER 和 RE。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 联合抽取示例(CasRel模型)
class CasRel(nn.Module):
"""
假设给定句子和所有主语,识别所有(主语, 关系, 宾语)三元组
"""
def __init__(self, bert_model, num_relations):
super().__init__()
self.bert = bert_model
self.num_relations = num_relations

# 头实体识别
self.head_select = nn.Linear(768, 2)

# 每种关系对应的尾实体识别
self.tail_select = nn.ModuleList([
nn.Linear(768, 2) for _ in range(num_relations)
])

def forward(self, input_ids, attention_mask):
outputs = self.bert(input_ids, attention_mask)
sequence_output = outputs.last_hidden_state

# 1. 识别所有头实体
head_logits = self.head_select(sequence_output) # [batch, seq_len, 2]

# 2. 对每个关系,识别对应的尾实体
for r_idx in range(self.num_relations):
tail_logits = self.tail_select[r_idx](sequence_output)
# 如果头实体位置是1,且尾实体位置是1,则存在 (头, 关系, 尾) 三元组

return triplets

四、实体链接(Entity Linking)

4.1 什么是实体链接?

将文本中识别的实体,链接到知识图谱中唯一的实体 ID

1
2
3
4
5
6
文本中的实体: "苹果"
知识图谱中的候选实体:
- 苹果公司 (Apple Inc.)
- 苹果 (水果)

实体链接:确定这里的"苹果"指的是哪个实体

4.2 实体消歧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def entity_disambiguation(entity_mention: str, context: str, 
candidate_entities: list) -> str:
"""上下文消歧"""

# 1. 提取上下文的向量表示
context_embedding = embed(context)

# 2. 计算每个候选实体与上下文的相似度
scores = []
for entity in candidate_entities:
entity_desc = f"{entity.name}: {entity.description}"
entity_embedding = embed(entity_desc)
similarity = cosine_similarity(context_embedding, entity_embedding)
scores.append((entity.id, similarity))

# 3. 选择相似度最高的候选实体
best_entity = max(scores, key=lambda x: x[1])
return best_entity[0]

# 示例
entity_disambiguation(
entity_mention="苹果",
context="乔布斯创立了苹果公司,开发了iPhone",
candidate_entities=[
{"id": "Apple_Inc.", "name": "苹果公司", "description": "美国科技公司"},
{"id": "Apple_fruit", "name": "苹果", "description": "一种水果"}
]
)
# 返回: "Apple_Inc." (因为上下文提到乔布斯、iPhone)

4.3 共指消解(Coreference Resolution)

处理同一个实体在不同表达中的指代问题。

1
2
3
4
5
6
7
8
9
10
11
12
文本: "马化腾创办了腾讯。他后来成为全球最富有的人之一。"

指代"马化腾"

共指消解结果: "他" → 马化腾

# 使用 SpanBERT 进行共指消解
from span_transformers import SpanResolver

resolver = SpanResolver.from_pretrained("michiyasunaga/spanscope-espire-base")
clusters = resolver.resolve(document)
# [[(马化腾, 0, 2), (他, 7, 8)]] → 两个 mention 指代同一实体

五、知识融合(Knowledge Fusion)

5.1 实体对齐(Entity Alignment)

将来自不同数据源的同一实体合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def entity_alignment(kg1: Graph, kg2: Graph) -> list:
"""发现两个知识图谱中的等价实体对"""

aligned_pairs = []

# 1. 找到候选等价实体对(通过属性相似度)
for entity1 in kg1.entities:
for entity2 in kg2.entities:
similarity = calculate_similarity(entity1, entity2)
if similarity > threshold:
candidate_pairs.append((entity1, entity2, similarity))

# 2. 使用 GNN 做联合嵌入,对候选对排序
aligned_pairs = gnn_entity_matching(candidate_pairs)

return aligned_pairs

def calculate_similarity(e1: Entity, e2: Entity) -> float:
"""计算实体对的相似度"""
score = 0

# 属性相似度
for attr in shared_attributes(e1, e2):
score += string_similarity(e1[attr], e2[attr])

# 邻居相似度(如果实体的邻居也相似,则实体更可能相似)
neighbors_e1 = get_neighbors(e1)
neighbors_e2 = get_neighbors(e2)
score += len(neighbors_e1 & neighbors_e2) * 0.3

return score / len(shared_attributes)

5.2 冲突检测与解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def conflict_detection_and_resolution(triplets1, triplets2) -> Graph:
"""检测并解决冲突知识"""

# 1. 构建知识图谱
kg = merge(triplets1, triplets2)

# 2. 检测冲突
conflicts = []
for entity_pair in find_entity_pairs_with_conflicts(kg):
relations = get_relations_between(entity_pair)
if has_conflict(relations):
conflicts.append({
"entity": entity_pair,
"conflicting_relations": relations
})

# 3. 解决冲突(策略:置信度优先 / 时间优先 / 多票决定)
for conflict in conflicts:
resolved_value = resolve_conflict(
conflict["conflicting_relations"],
strategy="confidence" # 或 "recency" / "majority"
)
kg.update(entity_pair, resolved_value)

return kg

六、知识补全(Knowledge Completion)

6.1 知识图谱嵌入(KGE)

用嵌入向量表示知识图谱中的实体和关系,支持链接预测(预测缺失的关系)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# TransE 模型
class TransE(nn.Module):
def __init__(self, num_entities, num_relations, embedding_dim):
super().__init__()
self.entity_embedding = nn.Embedding(num_entities, embedding_dim)
self.relation_embedding = nn.Embedding(num_relations, embedding_dim)

def forward(self, head, relation, tail):
"""TransE 核心思想:h + r ≈ t"""
h = self.entity_embedding(head)
r = self.relation_embedding(relation)
t = self.entity_embedding(tail)

# L2 距离越小越好
score = torch.norm(h + r - t, p=2, dim=-1)
return score

# 训练
model = TransE(num_entities=10000, num_relations=100, embedding_dim=256)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for epoch in range(100):
for batch in dataloader:
h, r, t = batch # (head, relation, tail)

# 正例
pos_score = model(h, r, t)

# 负例:随机替换 head 或 tail
neg_h = torch.randint_like(h, num_entities)
neg_t = torch.randint_like(t, num_entities)
neg_score = model(neg_h, r, t)

# margin-based ranking loss
loss = torch.relu(pos_score + margin - neg_score).mean()

optimizer.zero_grad()
loss.backward()
optimizer.step()

# 预测:给定头实体和关系,预测尾实体
def predict_tail(head, relation, top_k=10):
h_emb = model.entity_embedding(head)
r_emb = model.relation_embedding(relation)

# 候选尾实体 = h + r - t ≈ 0 → t ≈ h + r
target = h_emb + r_emb

# 计算与所有实体的距离
all_t_emb = model.entity_embedding.weight
distances = torch.norm(all_t_emb - target, p=2, dim=-1)

return torch.argsort(distances)[:top_k]

6.2 其他嵌入模型

模型 核心思想 特点
TransE h + r ≈ t 简单高效,适合一对一关系
TransH h + r ≈ t(t在关系超平面上) 解决多关系建模
TransR M_r × h ≈ M_r × t 实体和关系在不同空间
RotatE h × r ≈ t(旋转) 建模对称/反对称/合成关系
ConvE 2D卷积 + 全连接 高表达能力
CompGCN GNN + 组合操作 利用图结构信息

七、开源工具推荐

工具 类型 特点
LSP4KG 完整pipeline Stanford开源,支持NER+RE
DeepKE 中文NER/RE/RE 清华开源,专为中文设计
SPADE 事件抽取 基于LSTM的事件流水线
OpenNRE 关系抽取 清华开源,多种RE模型
REL 实体链接 综合实体链接框架
Siegrist 实体对齐 GNN-based对齐模型

总结

  1. 知识构建:知识抽取 → 实体链接 → 知识融合 → 知识补全
  2. NER:从文本识别实体,BERT+CRF 是主流方案,中文需注意嵌套实体
  3. 关系抽取:Pipeline(先NER后RE)或联合抽取,大模型可以做零样本 RE
  4. 实体链接:上下文消歧 + 向量相似度匹配
  5. 知识补全:TransE 等嵌入模型支持链接预测,发现缺失知识

相关文章: