📑 查看全课大纲(第 13 / 26 节)
- 1.知识图谱和语音技术概述
- 2.典型知识库项目简介
- 3.知识图谱技术概览
- 4.典型应用案例
- 5.早期知识表示简介
- 6.基于语义网的知识表示框架
- 7.典型知识库项目的知识表示
- 8.基于本体工具的知识建模实践
- 9.面向非结构化数据的知识抽取
- 10.面向结构化数据的知识抽取
- 11.面向半结构化数据的知识抽取
- 12.实践:基于百科数据的知识抽取
- 13.面向文本的知识抽取
- 14.知识挖掘
- 15.从一个例子开始
- 16.图数据库介绍
- 17.什么是知识融合
- 18.知识融合的基本技术流程
- 19.典型知识融合工具简介
- 20.典型案例简介
- 21.LIMES实战演练
- 22.知识推理
- 23.语义搜索
- 24.知识问答
- 25.IBM watson Lite
- 26.行业知识图谱应用
面向文本的知识抽取
约 81 分钟
小象实战讲义 · 知识图谱
上一讲我们解决了结构化与半结构化数据的抽取:把关系数据库、网页表格、百科 infobox 这类“本身就带结构”的数据映射成三元组。现实中体量最大、也最难处理的是非结构化的自然语言文本,本节就把战场转移到文本上,核心任务是关系抽取(relation extraction)。我们先铺开关系抽取的技术地图,再用经典框架 DeepDive 完整走一遍“从上市公司公告抽取股权交易关系”的工程,最后介绍不预定义关系类型的开放域关系抽取 OpenIE。原课程的 DeepDive 工具链基于 PostgreSQL 与 Stanford NLP,本讲在讲透其原理与算法的同时,统一用 Python 3.12 标准库与 rdflib 给出可运行的最小等价实现。
💡 核心导读
- 任务定位:面向文本的抽取以关系抽取为核心,基本在句子级别进行;关系不只有狭义二元关系,事件抽取因含多个要素,在建模时常借用多元(n-ary)结构来表示(但事件抽取是与关系抽取并列的独立子任务,并不等同于关系抽取里的多元关系)。
- 两条路线:传统关系抽取预先定义关系类型、精度高但规模小;开放域关系抽取(OpenIE)不预定义关系、全网规模但精度低,二者互补。
- DeepDive 方法论:特征工程 + 远程监督(distant supervision)+ 因子图全局优化,用吉布斯采样做近似推断、用 SGD 做近似学习,追求的是整张图的联合(而非单条三元组)证据融合——注意这是近似方法,并不保证真正意义上的全局最优。
- 一条完整流水线:先验数据与文章入库 → NLP 预处理 → 候选实体(mention)抽取 → 候选实体对 → 关系特征 → 远程监督与规则打标投票 → 因子图 + 吉布斯采样推断置信度 → 迭代调试。
- 开放域两代系统:第一代 TextRunner、WOE 用自监督分类与依存路径;第二代 Reverb(动词模式)、OLLIE(名词/形容词模板)、ClauseIE(子句)针对第一代的从句误抽与信息损失做改进。
1. 面向文本的抽取:任务地图
1.1 从结构化到非结构化:文本抽取为什么最难
回顾前面几讲:结构化数据(关系表)有清晰 schema,半结构化数据(百科 infobox、网页表格)有标签和排版可利用,它们到三元组是“结构到结构”的映射,规则相对确定。自然语言文本则没有显式结构——同一个“收购”关系可写成“甲收购乙”“乙被甲收购”“甲以现金购买乙持有的股权”,还夹杂否定、指代、省略与跨句陈述。因此面向文本的抽取必须先用自然语言处理(NLP)把句子“结构化”,再做识别与判断。
面向文本的知识抽取通常包含三类任务:
- 实体识别(NER,Named Entity Recognition):从句子中识别出命名实体的边界与类型,例如把“青鸟华光”“康欣新材”识别为组织机构(ORG)。
- 关系抽取(RE,Relation Extraction):判断句子中两个(或多个)实体之间是否存在某种预定义关系,例如“青鸟华光”与“康欣新材”之间是否存在“股权交易”关系。
- 事件抽取(Event Extraction):识别“谁在什么时间、什么地点、对谁做了什么”,是与实体识别、关系抽取并列的独立信息抽取子任务;事件天然带有多个要素(触发词、施事、受事、时间、地点),因此在建模时常常借用多元(n-ary)结构来表示,但并不等同于关系抽取里的多元关系。
本节工程主线是关系抽取,且绝大多数方法工作在句子级别:先在一句话里定位实体,再判断实体间关系。关系证据分布在多个句子中的跨句推理是更难的拓展方向。
1.2 关系抽取的两条路线:传统 IE 与开放域 IE
课件 Page 5 把关系抽取首先划分为两条路线(对应 Page 46-47 的对比):
- 传统关系抽取(traditional IE):针对特定领域,事先预定义好关系类型(如
has_spouse、works_for、has_transaction),依赖领域专家知识与标注语料,抽取规模相对小但精度高。课件 Page 48 的“主要系统”图把 Freebase(课件原文排版作 FREEBASE)、DBpedia 列在传统 IE 一侧(它们本身是知识库,即传统 IE 主要填充/入库的对象,而非抽取器;YAGO 在该页被单列为“语义化扩展”)。 - 开放域关系抽取(OpenIE,Open Information Extraction):面向全网开放文本,不预定义关系类型,关系短语直接从句子里的动词/名词结构中归纳出来(如 “was awarded”“be co-founder of”),依赖通用句法特征,规模可达全网级,但精度相对低。TextRunner、WOE、Reverb、OLLIE、ClauseIE 是代表系统。
两条路线不是替代而是互相补充:传统 IE 产出规范、可直接入库的高质量三元组;OpenIE 产出海量但关系短语异构的三元组,可用来扩充知识库、或按现有规范把更多网络数据链接进来。需要说明一个口径问题:课件 Page 48 的主要系统图把 DeepDive 也列在 Open IE 一侧(因其面向大规模文本、弱监督运行),但它抽取的 has_transaction 仍是预定义关系,因此本节第 3 节把 DeepDive 作为“预定义关系 + 远程监督”的 KBC 代表来讲,第 4.1 节会再点出这一归类差异,第 4 节主体则是 TextRunner、Reverb 等真正不预定义关系的 OpenIE。
1.3 从方法、数据、拓展三个维度给关系抽取分类
课件 Page 5 的分类图除了按路线划分,还给出了三个正交的分类维度,这对理解后续算法很有帮助:
按使用的方法(model)分:
- 特征向量(feature-based):把实体对的上下文做成离散特征(词序列、词性序列、实体类型、触发词等),喂给分类器;DeepDive 的因子图就建立在大量手工/自动特征之上。
- 核函数(kernel-based):如字符串核(string kernel)、树核(tree kernel),用核函数度量两段句法结构的相似度,本课程不展开细节。
- 深度学习(deep learning):用神经网络自动学习特征表示,例如 BiLSTM-CRF 做命名实体识别、端到端(end-to-end)模型同时做实体与关系。
- 通用模型 / 模板匹配 / 图推理:包括基于通用统计模型、基于依存模板匹配、以及在图结构上做推理的方法。
按监督数据的来源(data)分:
- 人工构建(human/supervised):人工标注训练语料,质量高但成本大。
- 远程监督(distant supervision):用已有知识库自动对齐语料产生训练样本——凡是句子中共同出现了知识库中具有某关系的两个实体,就把该句当作该关系的训练样本。成本低但有噪声(共现不代表表达了该关系)。
- 自举(bootstrap):用少量种子实例或模板,迭代地学习新模板、发现新实例,滚雪球式扩充。
- 无监督(unsupervised):不依赖关系标签,直接对上下文聚类得到关系。
按任务拓展(extension)分:
- 多元关系(n-ary relation):关系抽取从二元向多要素扩展的方向(课件 Page 5 将其列为关系抽取的拓展维度);事件抽取因含多个要素,也常借用多元结构来建模。
- 跨句推理(cross-sentence / document-level):关系的证据分布在多个句子中,需要跨句整合。
- 联合抽取(joint extraction):传统流水线(pipeline)先做 NER 再做 RE,前一步的错误会传播到后一步;联合模型(多任务 multitask 或统一结构)同时识别实体与关系,缓解错误传播。
1.4 mention 与 entity:从句子到知识库的最后一跳
DeepDive 反复强调两个层次的区分(讲授中明确区分,候选实体 mention 表见课件 Page 19-21,实体链接/消歧的模型在本章第 2 节“知识挖掘”中展开):
- mention level(提及层):句子里被 NER 识别出的实体名字片段,称为 mention(提及)。例如句子“Barack Obama and Michelle Obama are married”中,“Barack Obama”“Michelle Obama”是两个 mention,“are married”是一个关系短语。
- entity level(实体层):把 mention 通过实体链接(entity linking)/实体消歧(entity disambiguation)映射到知识库中的标准实体(URI)后,才得到实体层的关系。同一个实体在文本里可能有多种写法(全称、简称、别名、代词),实体链接负责把它们归一到同一个标准名。
于是从句子到知识库的路径是:句子 → NER 得到 mention → 识别关系短语(如 are married 映射到谓词 has_spouse)→ 实体链接把 mention 归一为标准实体 → 实体层三元组(知识库)。DeepDive 的目标就是把这条从 sentence 直接到 knowledge base 的链路自动化(此处 KB 与 KG 本质相同)。
2. KBC 与 DeepDive:自动搭建知识库的框架
2.1 KBP 与 KBC:知识库填充与知识库构建
KBP(Knowledge Base Population,知识库填充)关注如何往知识库补充实体与关系;KBC(Knowledge Base Construction,知识库构建) 更进一步,研究如何自动搭建知识库构建系统。课件 Page 7 给出 KBC 系统的输入输出:输入是非结构化文章、半结构化 HTML 与表格等多源数据,输出是结构化知识库,系统要把多源数据填充、融合到同一个知识库中。
DeepDive 就是自动搭建 KBC 系统的框架(课件 Page 8,斯坦福大学团队研发)。它让领域专家(金融、生物、古文本等)不必深入掌握统计学习与分布式系统,只需提供领域知识(目标关系、特征、规则、少量先验数据)即可搭建高质量的知识库构建系统。
2.2 DeepDive 的三个关键词:特征工程、远程监督、图优化
课件 Page 8 把 DeepDive 的方法论概括为三块:
- 特征工程(feature engineering):从文本中自动抽取海量特征(词、词性、命名实体标签、依存路径、窗口 n-gram、词典命中……),把判断关系是否成立的证据显式化。
- 远程监督(distant supervision):用已有知识库(如金融领域的国泰安数据库)自动产生训练标签,避免大规模人工标注。
- 图优化(graph optimization / 因子图推断):把所有候选关系实例与特征组织成一张因子图(factor graph),在图上做全局概率推断。
最关键的设计思想是全局联合推断:传统流水线逐条独立判断三元组;DeepDive 把所有候选实例放进一张概率图,让它们通过共享特征、对称约束等因子相互影响,为每个实例给出边缘概率(置信度)。这是一种近似的联合推断(吉布斯采样近似边缘概率、SGD 近似训练权重),旨在让整张图的证据相互融合,而不是孤立地逐条判断;但它并不保证求出真正的全局最优解。
2.3 KBC 工作流四环节与 DeepDive 工程组成
课件 Page 9 给出 KBC 处理流程的四个环节:
- 特征抽取(feature extraction):包括 OCR(针对扫描文档)、NLP 工具(分词、词性、命名实体、依存分析)、以及用户自定义脚本。
- 专业知识融合(knowledge integration):在整个知识库层面融合多种关系与多来源知识,处理冲突与冗余。
- 监督学习(supervision / statistical learning & inference):利用远程监督与规则产生标签,训练因子图的权重。
- 迭代优化(iterative improvement):分析模型错误,补充特征、规则与先验数据,循环提升。
课件 Page 8 指出工程上的技术难点:需要设计可迭代的 KBC 工作流,用户通过一种类似 Datalog 的声明式语言 ddlog(DeepDive 的应用描述语言,文件 app.ddlog) 来描述数据表与处理流程,而不必关心底层算法与性能;底层利用分布式数据库支撑大规模数据。DeepDive 只支持 Linux 与 macOS,不支持 Windows。
一个 DeepDive 工程(课件 Page 11)通常由五部分组成:
- app.ddlog(主程序):声明数据表结构、表与表之间的派生规则(head :- body),并调用用户自定义脚本(UDF)。
- db.url(数据库连接):指定数据表存储位置,即 PostgreSQL 的连接串(地址、端口、用户名、数据库名,通过 JDBC 驱动连接)。
- deepdive.conf(工程配置):设置采样与训练参数,例如留出比例 holdout fraction(取约 1/4 数据作测试集)、学习率 learning rate、迭代次数 iteration、采样参数等。
- input/(输入目录):放训练先验数据与待抽取文本,DeepDive 会自动寻找与表同名的 CSV 文件建表导入。
- udf/(用户自定义脚本):Python 脚本与 NLP 工具封装(如调用 Stanford NLP 的
nlp_markup.sh、bazaar 目录下的解析器)。
3. DeepDive 实战:从上市公司公告抽取股权交易关系
本节按原课的实战目标,从上市公司公告中抽取相关交易(股权交易)关系(课件 Page 10)。目标关系命名为 has_transaction,是一个二元关系,其主体和客体都是上市公司(主、客体实体类型均为公司/ORG)。下面严格按照 DeepDive 的流水线顺序展开,每一步都说明原工程做法,并给出对应的可运行 Python 最小实现。
3.1 先验数据与待抽取文章入库
先验数据表 pos_transaction(课件 Page 12):来自国泰安数据库,存放已知发生股权交易的公司对,字段包括 company1_name、company2_name、text(描述文本),并对关键属性加 @key 注解。DeepDive 执行 deepdive do pos_transaction 时,会自动到 input 目录找 pos_transaction.csv,在 PostgreSQL 中建表并导入。这张表是远程监督的“知识库”。
待抽取文章表 articles(课件 Page 13):字段为 doc_id(文章编号)与 content(正文),content 加 @searchable 注解以支持全文索引;数据来自 articles.csv,即上市公司公告。执行 deepdive do articles 完成导入。@key 与 @searchable 这类注解会在后续的可视化搜索(Mindbender)中被用来定位实体与特征。
3.2 文本预处理:sentences 表与 NLP 标注
DeepDive 对 articles 逐篇逐句做 NLP 标注,结果写入 @extraction 类型的 sentences 表(课件 Page 15-17)。派生规则形如 sentences :- articles,对每篇文章调用 nlp_markup 函数;该函数封装在 nlp_markup.sh 中,进一步调用 udf/bazaar/parser/run.sh,底层是集成了 Stanford NLP(Stanford CoreNLP)的 jar 包。输入时把文章的每一行转成 JSON(key 为 doc_id,value 为 content),输出逐句的标注结果。
sentences 表的主要字段(课件 Page 17):
- tokens:分词后的词序列;
- lemmas:词形还原(lemmatization;讲授在该字段处用 morphology 一词指代形态层面的归一)的结果,把动词时态、名词单复数等词形变化归一为词典原形词元(lemma);注意它与只做截断、取词干(stem)的词干提取(stemming)不是一回事;
pos_tags:词性标注(POS,part-of-speech);ner_tags:命名实体标签。本讲示例代码采用 BIO 标注模式(B 为实体首词、I 为实体内部、O 为非实体,如 B-ORG/I-ORG)把实体边界显式编码;课件 Page 19-20 的说法是“抽取/合并连续的 ORG 标签”,Stanford NER 默认按 token 输出实体类别标签(如 ORGANIZATION)、并不自带 B-/I- 前缀,BIO 只是把连续同类标签转成实体跨度时常用的等价编码。类别上,讲授给出七类:人名(PERSON)、地点(LOCATION)、组织机构(ORGANIZATION)、公司(COMPANY)、日期(DATE)、货币(CURRENCY)、百分比(PERCENT);需注意标准 Stanford CoreNLP 七类英文模型实际输出的是 Person、Location、Organization、Date、Money、Percent、Time——公司并入 Organization、金额标为 Money(并无独立的 COMPANY/CURRENCY 标签),具体以加载的模型为准;- offsets:每个 token 在原文中的字符偏移(起始/结束位置);
- 依存关系(dependency parse):依存类型与依存指向的 token,构成一棵依存树(根节点指向 -1),刻画词与词之间的主谓宾、修饰等语法关系。
这一步是整个流水线中最耗时的环节之一,因为要对海量句子逐句跑完整 NLP。DeepDive 本身不做 NER,而是依赖外部 NLP 工具;实战中完全可以把 Stanford NLP 替换成 CRF 序列标注模型,或 BiLSTM-CRF 等深度学习 NER。
3.3 候选实体抽取:把连续 NER 标签合并成 mention
有了逐句的 BIO 标签,下一步是从句子里抽出候选实体,写入 company_mention 表(课件 Page 19-21),字段包括 mention_id、mention_text、doc_id、sentence_index、begin_index、end_index。这一步由 UDF 脚本 map_company_mention.py 完成,算法思想很直接:定位每一个 ORG 实体标签的起始位置,然后向后遍历直到该实体结束,把连续的实体标签合并成一个 mention;mention_id 用“文档号_句子号_起始下标_结束下标”拼接,保证全局唯一。在合并过程中还可以顺手过滤掉 NER 误识别出的非实体片段。
下面的代码块 1 用标准库模拟了 articles → sentences(分词 + 规则 NER)→ company_mention 的完整链路:用小词典前向最大匹配代替分词器,用公司词典规则代替 Stanford NER,用连续标签合并还原 map_company_mention 的核心逻辑。
# 代码块1:文本预处理 + 候选实体(mention)抽取的最小 Python 版
# 对应 DeepDive 流程:articles -> sentences(NLP) -> company_mention
# 原课用 Stanford CoreNLP 产出 token/lemma/POS/NER(BIO)/依存;这里用标准库做一个
# 可运行的最小等价演示:小词典前向最大匹配分词、词典规则 NER、连续 ORG 标签合并为 mention。
import re
# 内联样例:上市公司公告中的几句话(自包含,不依赖外部文件)
articles = [
("doc1", "青鸟华光以现金购买康欣新材持有的股权。"),
("doc2", "东旭集团投资康欣新材,双方完成相关交易。"),
("doc3", "青鸟华光与东旭集团签署股权转让协议。"),
("doc4", "华工科技投资青鸟华光。"), # “华工科技”被切成两个 ORG token,演示 B-/I- 合并
]
# 领域词典:公司名(ORG)与普通词,供前向最大匹配分词使用
# 注:“华工”“科技”分别成词,连在一起即多 token 公司名,用于触发 I-ORG 连续合并
ORG_LEX = ["青鸟华光", "康欣新材", "东旭集团", "华工", "科技"]
WORD_LEX = ["现金", "购买", "持有", "股权", "投资", "双方", "完成", "相关",
"交易", "签署", "转让", "协议", "以", "的", "与", ",", "。"]
LEXICON = sorted(ORG_LEX + WORD_LEX, key=len, reverse=True) # 长词优先
def segment(text):
"""前向最大匹配分词,返回 token 列表。"""
tokens, i = [], 0
while i < len(text):
for w in LEXICON:
if text.startswith(w, i):
tokens.append(w)
i += len(w)
break
else: # 词典未覆盖的单字(如标点外的零散字)原样切出
tokens.append(text[i])
i += 1
return tokens
def ner_tag(tokens):
"""规则 NER(BIO 模式):命中公司词典的 token,若前一个 token 也是 ORG 则标 I-ORG
(实体内部),否则标 B-ORG(实体首词);未命中标 O。这样连续 ORG token 会组成一个实体。"""
tags = []
for k, t in enumerate(tokens):
if t in ORG_LEX:
prev_is_org = k > 0 and tokens[k - 1] in ORG_LEX
tags.append("I-ORG" if prev_is_org else "B-ORG")
else:
tags.append("O")
return tags
def nlp_markup(doc_id, content):
"""模拟 nlp_markup:一篇文章 -> 逐句 -> 分词 + NER(真实系统还含 lemma/POS/依存)。"""
sentences = []
for sidx, sent in enumerate(re.split(r"[。!?]", content)):
sent = sent.strip()
if not sent:
continue
toks = segment(sent)
tags = ner_tag(toks)
sentences.append({"doc_id": doc_id, "sent_idx": sidx,
"text": sent, "tokens": toks, "ner": tags})
return sentences
def map_company_mention(sent):
"""对应 map_company_mention.py:定位每个 ORG 起始位置,向后遍历到结束位置,
把连续的 ORG 标签合并成一个候选实体 mention,并记录起止 token 下标。"""
mentions, i, toks, tags = [], 0, sent["tokens"], sent["ner"]
while i < len(tags):
if tags[i] == "B-ORG": # 找到一个 ORG 起始位置
begin = i
j = i + 1
while j < len(tags) and tags[j] == "I-ORG": # 向后遍历找结束位置
j += 1
end = j - 1
mentions.append({
"mention_id": f'{sent["doc_id"]}_{sent["sent_idx"]}_{begin}_{end}',
"text": "".join(toks[begin:end + 1]),
"doc_id": sent["doc_id"], "sent_idx": sent["sent_idx"],
"begin": begin, "end": end,
})
i = j
else:
i += 1
return mentions
# 跑通 articles -> sentences -> company_mention 全链路
all_mentions = []
for doc_id, content in articles:
for sent in nlp_markup(doc_id, content):
print(f'[{sent["doc_id"]} 句{sent["sent_idx"]}] 分词: {sent["tokens"]}')
print(f'{"":10s}NER : {sent["ner"]}')
for m in map_company_mention(sent):
all_mentions.append(m)
print(f'{"":10s}-> mention {m["mention_id"]} = 「{m["text"]}」'
f'(token {m["begin"]}..{m["end"]})')
print()
print("company_mention 候选实体表(共 %d 条):" % len(all_mentions))
for m in all_mentions:
print(" ", m["mention_id"], m["text"])[doc1 句0] 分词: ['青鸟华光', '以', '现金', '购买', '康欣新材', '持有', '的', '股权']
NER : ['B-ORG', 'O', 'O', 'O', 'B-ORG', 'O', 'O', 'O']
-> mention doc1_0_0_0 = 「青鸟华光」(token 0..0)
-> mention doc1_0_4_4 = 「康欣新材」(token 4..4)
[doc2 句0] 分词: ['东旭集团', '投资', '康欣新材', ',', '双方', '完成', '相关', '交易']
NER : ['B-ORG', 'O', 'B-ORG', 'O', 'O', 'O', 'O', 'O']
-> mention doc2_0_0_0 = 「东旭集团」(token 0..0)
-> mention doc2_0_2_2 = 「康欣新材」(token 2..2)
[doc3 句0] 分词: ['青鸟华光', '与', '东旭集团', '签署', '股权', '转让', '协议']
NER : ['B-ORG', 'O', 'B-ORG', 'O', 'O', 'O', 'O']
-> mention doc3_0_0_0 = 「青鸟华光」(token 0..0)
-> mention doc3_0_2_2 = 「东旭集团」(token 2..2)
[doc4 句0] 分词: ['华工', '科技', '投资', '青鸟华光']
NER : ['B-ORG', 'I-ORG', 'O', 'B-ORG']
-> mention doc4_0_0_1 = 「华工科技」(token 0..1)
-> mention doc4_0_3_3 = 「青鸟华光」(token 3..3)
company_mention 候选实体表(共 8 条):
doc1_0_0_0 青鸟华光
doc1_0_4_4 康欣新材
doc2_0_0_0 东旭集团
doc2_0_2_2 康欣新材
doc3_0_0_0 青鸟华光
doc3_0_2_2 东旭集团
doc4_0_0_1 华工科技
doc4_0_3_3 青鸟华光3.4 候选实体对:同句自连接(self-join)
股权交易关系需要两个公司实体,因此要在候选实体之上生成候选实体对,写入 transaction_candidate 表(课件 Page 23)。在 app.ddlog 中,这是对 company_mention 表做一次自连接(self-join):把同一张表取两个别名 a、b,连接条件是——两个 mention 出现在同一篇文档、同一个句子里、实体名不相等、位置不重合(一个实体不能既是 P1 又是 P2)。满足条件的两个实体分别记为 P1、P2,构成一个待判断的候选关系实例。之所以只在同句内配对,是因为本节的关系证据限定在单句内,跨句配对会引入大量噪声。
3.5 关系特征抽取:ddlib 的窗口特征
生成候选对后,DeepDive 调用 extract_transaction_features.py,借助自带的特征库 ddlib 为每个候选对生成上下文特征,写入 transaction_feature 表(课件 Page 25-28)。做法是把每个 mention 的起止位置与长度封装成 ddlib 的“实体跨度(entity span)”,再调用通用关系特征函数(generic relation feature),围绕两个实体抽取上下文窗口特征。课件 Page 27 给出了窗口特征表,主要包括:
NGRAM_[WINDOW]:实体对之间指定窗口大小的词语 n-gram 序列;长度为 k 的词序列在窗口 n 下产生 k-n+1 个 n-gram;WORD_SEQ_[ALL]:实体对之间完整的词语序列;NER_SEQ_[ALL]/POS_SEQ_[ALL]:实体对之间完整的命名实体标签序列 / 词性序列;W_LEFT_[WINDOW]/W_RIGHT_[WINDOW]:实体对左侧 / 右侧窗口的词序列;W_WORD_L_WINDOW_R_WINDOW[ALL]:左右窗口混合特征;IN_DICT_[WORD]:词典命中特征,即实体对上下文是否出现预定义的触发词(trigger word,如“购买”“股权”“投资”);BETW_L_[WORD]/BETW_D_[WORD]:实体对之间基于语法依存的词序列 / 词性序列特征。
每个候选实体对都会产生成百上千条特征,这一步同样耗时。代码块 2 用标准库实现了候选对自连接与主要窗口特征(n-gram、完整词序列、左右窗口、词典命中、间隔词数)。
# 代码块2:候选实体对生成(同句自连接)+ ddlib 风格的关系窗口特征
# 对应 DeepDive 流程:company_mention -> transaction_candidate -> transaction_feature
# 原课在 app.ddlog 里对 company_mention 做自连接(self-join),并用 ddlib 生成窗口特征;
# 这里用标准库集合/列表给出最小可运行等价实现。
from itertools import combinations
# 内联样例:已经过 NLP 的句子(tokens + NER),以及在其中抽到的候选实体 mention
sentences = [
{"doc": "doc1", "sidx": 0,
"tokens": ["青鸟华光", "以", "现金", "购买", "康欣新材", "持有", "的", "股权"],
"ner": ["B-ORG", "O", "O", "O", "B-ORG", "O", "O", "O"],
"mentions": [{"p": "青鸟华光", "b": 0, "e": 0}, {"p": "康欣新材", "b": 4, "e": 4}]},
{"doc": "doc2", "sidx": 0,
"tokens": ["东旭集团", "投资", "康欣新材", ",", "双方", "完成", "相关", "交易"],
"ner": ["B-ORG", "O", "B-ORG", "O", "O", "O", "O", "O"],
"mentions": [{"p": "东旭集团", "b": 0, "e": 0}, {"p": "康欣新材", "b": 2, "e": 2}]},
{"doc": "doc3", "sidx": 0,
"tokens": ["青鸟华光", "与", "东旭集团", "签署", "股权", "转让", "协议"],
"ner": ["B-ORG", "O", "B-ORG", "O", "O", "O", "O"],
"mentions": [{"p": "青鸟华光", "b": 0, "e": 0}, {"p": "东旭集团", "b": 2, "e": 2}]},
]
# 触发词词典(对应 ddlib 的 IN_DICT 特征):出现这些词更可能在描述股权交易
TRIGGER_WORDS = {"购买", "收购", "投资", "转让", "股权", "现金", "交易", "签署"}
def build_candidates(sent):
"""候选实体对:同一句中两个不同实体、且位置不重合(ddlog 自连接的等价实现)。"""
cands = []
for m1, m2 in combinations(sent["mentions"], 2): # 自连接 company_mention a, b
if m1["p"] == m2["p"]: # 实体名不能相同
continue
if not (m1["e"] < m2["b"] or m2["e"] < m1["b"]): # 位置不能重合
continue
a, b = (m1, m2) if m1["b"] < m2["b"] else (m2, m1) # 保证 a 在 b 左侧
cands.append({"doc": sent["doc"], "sidx": sent["sidx"], "p1": a, "p2": b})
return cands
def ngrams(words, n):
"""词语 n-gram:窗口为 n 时,长度为 k 的词序列产生 k-n+1 个 n-gram。"""
return ["".join(words[i:i + n]) for i in range(len(words) - n + 1)]
def extract_features(sent, cand):
"""对一个候选实体对抽取上下文窗口特征(命名对齐课件 Page 27 的 ddlib 特征表)。"""
toks = sent["tokens"]
a, b = cand["p1"], cand["p2"]
between = toks[a["e"] + 1:b["b"]] # 实体对之间的词
left = toks[max(0, a["b"] - 2):a["b"]] # 实体对左侧窗口
right = toks[b["e"] + 1:b["e"] + 3] # 实体对右侧窗口
feats = {}
feats["WORD_SEQ_ALL"] = between # 实体对间完整词序列
feats["NGRAM_1"] = ngrams(between, 1) # 窗口 n-gram(n=1)
feats["NGRAM_2"] = ngrams(between, 2) # 窗口 n-gram(n=2)
feats["W_LEFT_2"] = left # 左侧窗口词序列
feats["W_RIGHT_2"] = right # 右侧窗口词序列
context = left + between + right # 实体对完整上下文窗口
feats["IN_DICT"] = sorted(set(context) & TRIGGER_WORDS) # 上下文中命中的触发词
feats["BETW_LEN"] = len(between) # 中间词数(打标规则会用到)
return feats
# 串联:候选对 -> 特征
all_rows = []
for sent in sentences:
for cand in build_candidates(sent):
feats = extract_features(sent, cand)
all_rows.append((cand, feats))
print(f'{cand["doc"]}: ({cand["p1"]["p"]}, {cand["p2"]["p"]})')
for k, v in feats.items():
print(f" {k:12s} = {v}")
print()
print("共生成候选实体对 %d 个,每个都带上一组关系特征。" % len(all_rows))doc1: (青鸟华光, 康欣新材)
WORD_SEQ_ALL = ['以', '现金', '购买']
NGRAM_1 = ['以', '现金', '购买']
NGRAM_2 = ['以现金', '现金购买']
W_LEFT_2 = []
W_RIGHT_2 = ['持有', '的']
IN_DICT = ['现金', '购买']
BETW_LEN = 3
doc2: (东旭集团, 康欣新材)
WORD_SEQ_ALL = ['投资']
NGRAM_1 = ['投资']
NGRAM_2 = []
W_LEFT_2 = []
W_RIGHT_2 = [',', '双方']
IN_DICT = ['投资']
BETW_LEN = 1
doc3: (青鸟华光, 东旭集团)
WORD_SEQ_ALL = ['与']
NGRAM_1 = ['与']
NGRAM_2 = []
W_LEFT_2 = []
W_RIGHT_2 = ['签署', '股权']
IN_DICT = ['签署', '股权']
BETW_LEN = 1
共生成候选实体对 3 个,每个都带上一组关系特征。3.6 样本打标:远程监督 + 规则 + 投票
有了候选实例和特征,还需要训练标签。DeepDive 不做大规模人工标注,而是用两路信号自动打标,写入 transaction_label 并汇总为 transaction_label_resolved(课件 Page 30-32):
- 远程监督(来自知识库):若候选实体对能与
pos_transaction中的已知交易对对应(实体名先做大小写、全半角归一化),就标为正例,规则名记为 positive from DB,权重较高(课件示例设为 +3)。 - 用户规则(
supervise_transaction.py):领域专家编写的启发式规则,例如——两个实体之间间隔的词数超过最大限制时判负例;出现“甲购买乙的股权”这类明确模式时判正例。
初始时所有候选标签为 0(未知)。多条规则与知识库信号会对同一个候选实例投票(vote),把各票权重求和:总和大于 0 判正例、小于 0 判负例、等于 0 保持未知(交给后续因子图推断)。需要注意远程监督本身有噪声——两个实体在某句话中共现,并不代表这句话就在表达它们的交易关系,这也是为什么还要叠加规则与全局推断来纠错。代码块 3 演示了这一投票过程。
# 代码块3:样本打标——远程监督(distant supervision)+ 规则 + 投票(vote)
# 对应 DeepDive 流程:pos_transaction(知识库先验) + supervise_transaction.py(规则)
# -> transaction_label -> transaction_label_resolved
# 标签语义:0 未知(待因子图推断),正数倾向正例(越大越强),负数倾向负例。
# 远程监督的“知识库”:国泰安数据中已知具有股权交易关系的公司对(先验正例)
# 真实系统会先做大小写/全半角归一化,这里直接用规范名
pos_transaction = {("青鸟华光", "康欣新材")}
KB_WEIGHT = 3 # 课件中知识库命中的权重设为 +3
MAX_BETWEEN_LEN = 4 # 两实体中间词数上限,超过判负
# 候选实体对(内联,携带实体间词序列与右侧上下文窗口)
candidates = [
{"id": "v1", "p1": "青鸟华光", "p2": "康欣新材",
"between": ["以", "现金", "购买"], "right": ["持有", "的", "股权"]},
{"id": "v2", "p1": "东旭集团", "p2": "康欣新材",
"between": ["投资"], "right": [",", "双方", "完成", "相关", "交易"]},
{"id": "v3", "p1": "青鸟华光", "p2": "东旭集团",
"between": ["与"], "right": ["签署", "股权", "转让", "协议"]},
{"id": "v4", "p1": "东旭集团", "p2": "青鸟华光",
"between": ["发布", "了", "年度", "公告", ","], "right": ["出席", "会议"]},
]
ACTION_WORDS = {"购买", "收购", "投资", "转让", "签署"} # 交易动作词
OBJECT_WORDS = {"股权", "现金", "交易", "协议"} # 交易对象词
def rule_votes(cand):
"""对应 supervise_transaction.py 中的一组规则,每条规则投一票(带权重)。"""
votes = []
context = cand["between"] + cand["right"]
# 规则1:两实体间隔词过多,不太可能是直接交易关系 -> 负例
if len(cand["between"]) > MAX_BETWEEN_LEN:
votes.append(("R1_间隔过长", -1))
# 规则2:上下文中同时出现交易动作词与交易对象词 -> 正例(“A 购买 B 股权”模式)
if (set(context) & ACTION_WORDS) and (set(context) & OBJECT_WORDS):
votes.append(("R2_交易模式", 1))
# 规则3:上下文完全没有任何交易触发词 -> 负例
if not (set(context) & (ACTION_WORDS | OBJECT_WORDS)):
votes.append(("R3_无触发词", -1))
return votes
def distant_supervision_vote(cand):
"""远程监督:候选对命中知识库已知正例,则投一个高权重正票。"""
pair = (cand["p1"], cand["p2"])
if pair in pos_transaction or (pair[1], pair[0]) in pos_transaction:
return [("DB_知识库先验", KB_WEIGHT)]
return []
# 汇总:每条候选对收集所有票,按权重求和(vote),得到最终标签
resolved = []
for cand in candidates:
votes = distant_supervision_vote(cand) + rule_votes(cand)
score = sum(w for _, w in votes)
label = 1 if score > 0 else (-1 if score < 0 else 0)
resolved.append((cand["id"], cand["p1"], cand["p2"], votes, score, label))
tag = {1: "正例", -1: "负例", 0: "未知(待推断)"}[label]
print(f'{cand["id"]} ({cand["p1"]}, {cand["p2"]})')
for src, w in votes:
print(f" 票: {src:14s} {w:+d}")
print(f" => 求和 {score:+d},标签 {label}({tag})\n")
n_pos = sum(1 for r in resolved if r[5] == 1)
n_neg = sum(1 for r in resolved if r[5] == -1)
n_unk = sum(1 for r in resolved if r[5] == 0)
print(f"transaction_label_resolved:正例 {n_pos}、负例 {n_neg}、未知 {n_unk}(未知项交给因子图推断)")v1 (青鸟华光, 康欣新材)
票: DB_知识库先验 +3
票: R2_交易模式 +1
=> 求和 +4,标签 1(正例)
v2 (东旭集团, 康欣新材)
票: R2_交易模式 +1
=> 求和 +1,标签 1(正例)
v3 (青鸟华光, 东旭集团)
票: R2_交易模式 +1
=> 求和 +1,标签 1(正例)
v4 (东旭集团, 青鸟华光)
票: R1_间隔过长 -1
票: R3_无触发词 -1
=> 求和 -2,标签 -1(负例)
transaction_label_resolved:正例 3、负例 1、未知 0(未知项交给因子图推断)3.7 因子图:变量、因子、可能世界
打标之后,DeepDive 并不直接把正例写入知识库,而是构建一张因子图(factor graph)做概率推断,最终输出 has_transaction 表(课件 Page 34-36)。最终表的字段是 P1_id、P2_id 与 label,其中 label 用 ? 标注,表示它是一个需要被推导的随机变量。
按课件 Page 35 的用户模式(user schema):
- 变量(variable):每个候选关系实例是一个变量元组
v_i∈ V,映射为二值随机变量σ_v∈ {0,1}(1 表示关系成立,0 表示不成立)。 - 可能世界(possible world):对所有变量的一组 0/1 赋值 Iσ: V → B 称为一个可能世界;n 个变量就有
2^n个可能世界。 - 先验变量(prior):正例集合 P ⊆ V(取值固定为 1)与负例集合 N ⊆ V(取值固定为 0),它们在推断中被钳制(clamp)。所有可能世界构成集合 τ,其中与先验取值一致的子集记为 τe。
- 因子(factor):因子是作用在若干变量上的势函数,分两类:
- F1 特征因子:当某变量具有某个特征时就关联一个因子,其权重 weight = func(feature),是需要通过监督学习训练的模型参数(实数,可正可负)。ddlib 为候选关系实例生成特征;关键在于同一个特征词若同时出现在多个候选实例的上下文里,这些候选就由一个跨变量因子耦合起来——这正是变量之间相互“连带”、全局联合推断得以发生的机制。课件 Page 36 的因子表正体现这一点:f1(v1,v2)=func(
NGRAM_1[‘投资’])、f2(v2,v3)=func(NGRAM_LENGTH_[2 2])、f3(v1,v4)=func(NGRAM_1[‘股权’]),都是把“共享同一特征”的两个候选连到同一个因子上;只有 f4(v4)=func(NGRAM_1[‘现金’]) 只出现在 v4 一个候选上,才是作用于单变量的一元因子。所以连接多变量的跨变量因子才是耦合来源,一元因子只承载单个候选自身的证据。 - F2 对称因子:
has_transaction是对称关系(甲与乙有交易,则乙与甲也有交易),这是确定性的领域约束,对应一个固定权重(课件示例 3.0)的对称因子 f5(v3,v4),不需要学习。
- F1 特征因子:当某变量具有某个特征时就关联一个因子,其权重 weight = func(feature),是需要通过监督学习训练的模型参数(实数,可正可负)。ddlib 为候选关系实例生成特征;关键在于同一个特征词若同时出现在多个候选实例的上下文里,这些候选就由一个跨变量因子耦合起来——这正是变量之间相互“连带”、全局联合推断得以发生的机制。课件 Page 36 的因子表正体现这一点:f1(v1,v2)=func(
因子图是一张二部图(bipartite graph):一边是变量节点,一边是因子节点。一个因子连到某个变量,构成对该变量的一条证据(evidence);当多个正权重证据指向同一变量时,它更倾向于成立。变量之间通过共享因子相互耦合(coupling)形成约束(constraint):一般的特征因子会让相连变量的取值彼此影响,而对称/等值这类因子则会直接拉动相连变量取值趋于一致。这正是全局优化的来源:一个没有任何先验的未知变量,也能通过与高置信变量共享因子而被“连带”判定。
数学上,因子在某个可能世界上的聚合值为 g_j(fid, Iσ) = weight_fid · aggr(vars_fid);一个可能世界的未归一化权重为 Z(Iσ) = exp{ Σ_j Σ_fid g_j(fid, Iσ) }(注意 exp{…} 是未归一化权重,还不是概率)。由于先验变量被钳制,推断只在与先验一致的合法可能世界集合 τe 上进行(不一致的世界被排除、权重视为 0)。于是某未知变量取 1 的边缘概率(marginal probability) P(v_e=1),等于“τe 中该变量取 1 的世界的未归一化权重之和”除以“τe 中全部世界的未归一化权重之和(配分函数)”,归一化后才得到概率。
3.8 吉布斯采样与权重学习
可能世界有 2^n 个,变量一多就无法枚举,DeepDive 用吉布斯采样(Gibbs sampling)近似求解(课件 Page 37):先随机初始化一个可能世界 I0;然后依次对每个变量,根据与它相连的变量(马尔可夫毯)计算其条件概率 P(v|马尔可夫毯) 并更新取值,得到 I1;如此循环多轮。彼此不共享因子的变量相互独立,可以并行独立采样。采样过程中统计每个变量取 1 的频率,作为其边缘概率(置信度)的近似估计。
权重学习(课件 Page 38) 的优化目标,课件表述为“最大化与先验变量取值一致的可能世界数目”;用概率图的准确语言说,就是最大化先验标签(观测数据)的条件(对数)似然,让与先验一致的配置获得更高概率、不一致的配置概率更低,而不是简单地对离散世界计数。DeepDive 在采样得到的样本上做随机梯度下降(SGD)——一次吉布斯采样、一次梯度更新,迭代直到收敛;工程上针对硬件做了优化,支持分布式、增量式训练。课件 Page 38 明确指出,执行 deepdive do probabilities 就是开始训练:在采样样本上用 SGD 迭代更新上述特征因子的权重,收敛后再输出每个候选实例的推断概率(置信度)。也就是说,这条命令同时完成了权重学习与概率推断两步,而不只是“跑一次推断”。
代码块 4 用标准库完整实现了这套推断:构造变量、特征因子与对称因子,先在小规模(2 个未知变量)上枚举全部 4 个可能世界得到精确边缘概率,再用吉布斯采样近似,两者几乎一致,直观展示了采样的有效性与全局推断的传播效果。
# 代码块4:因子图(factor graph)建模 + 吉布斯采样(Gibbs sampling)求边缘概率
# 对应 DeepDive 流程:transaction_candidate/feature + 先验标签 -> 因子图
# -> possible world 全局推断 -> 每个候选关系实例的置信度
# 因子图是二部图:变量节点(候选关系实例,取值 0/1)+ 因子节点(特征因子/对称因子)。
import math
import random
from itertools import product
random.seed(42) # 固定随机种子,保证采样结果可复现
# ---- 1. 变量:4 个候选关系实例 v1..v4,二值随机变量;先验变量取值固定 ----
# 课件示例:v3 是知识库/规则确认的正例(P),v2 是确认的负例(N),v1、v4 待推断
variables = ["v1", "v2", "v3", "v4"]
clamped = {"v2": 0, "v3": 1} # 先验变量 P/N,采样中始终固定
unknown = ["v1", "v4"]
# ---- 2. 因子:(作用域, 权重, 聚合方式),对应课件 Page 36 因子表 ----
# 同一个特征词若出现在多个候选实例的上下文里,就用一个跨变量因子把这些候选耦合起来(全局连带的来源):
# f1(v1,v2)=func(NGRAM_1['投资'])、f2(v2,v3)=func(NGRAM_LENGTH_[2 2]):跨变量特征因子(aggr='and');
# f3(v1,v4)=func(NGRAM_1['股权']):v1、v4 共享'股权'特征的跨变量因子;
# f4(v4)=func(NGRAM_1['现金']):只出现在 v4 一个候选上,是一元因子。
# 权重 func(feature) 由监督学习训练得到(可正可负),这里直接给“训练后”的数值用于演示推断。
# 对称因子 f5(v3,v4) aggr='eq':两变量取值相同才激活(has_transaction 是对称关系,固定权重 3.0)。
# 说明:v2 被先验钳为 0(负例),故 f1、f2 这两个连到 v2 的跨变量因子在本次钳制推断中不激活
# (aggr='and' 需 v2=1);它们在把标签当弱证据的设定下才参与。本次 v1、v4 的连带主要经 f3。
factors = [
(("v1", "v2"), 0.7, "and", "f1 NGRAM_1[投资](v1、v2 共享'投资',跨变量因子)"),
(("v2", "v3"), 0.6, "and", "f2 NGRAM_LENGTH_[2 2](v2、v3 共享 n-gram 长度特征,跨变量因子)"),
(("v1", "v4"), 1.0, "and", "f3 NGRAM_1[股权](v1、v4 共享'股权',跨变量因子)"),
(("v4",), 0.5, "and", "f4 NGRAM_1[现金](仅 v4 命中,一元因子)"),
(("v3", "v4"), 3.0, "eq", "f5 symmetry 对称约束(v3、v4 取值相同才激活)"),
]
def factor_value(scope, kind, assign):
"""单个因子在某个可能世界(assign)下的聚合值 aggr(vars)。"""
if kind == "and": # 特征因子:全部变量为 1 才激活
return 1 if all(assign[v] == 1 for v in scope) else 0
if kind == "eq": # 对称因子:取值相等才激活
return 1 if len({assign[v] for v in scope}) == 1 else 0
raise ValueError(kind)
def world_energy(assign):
"""可能世界的总聚合分:Σ_j Σ_fid weight_fid * aggr(vars_fid)。"""
return sum(w * factor_value(scope, kind, assign) for scope, w, kind, _ in factors)
# ---- 3. 精确解:未知变量只有 2 个,枚举全部 2^2=4 个可能世界,按 exp(能量) 归一化 ----
print("== 枚举所有可能世界(与先验 v2=0、v3=1 一致的才合法)==")
worlds = []
for x1, x4 in product([0, 1], repeat=2):
a = dict(clamped); a["v1"], a["v4"] = x1, x4
e = world_energy(a)
worlds.append((a, e, math.exp(e)))
z = sum(wt for _, _, wt in worlds) # 配分函数 Z = Σ exp(能量)
for a, e, wt in worlds:
print(f' world v1={a["v1"]},v4={a["v4"]} (v2=0,v3=1) 能量={e:.1f} 未归一化权重={wt:.3f} 概率={wt/z:.3f}')
def exact_marginal(var):
return sum(wt for a, _, wt in worlds if a[var] == 1) / z
# ---- 4. 吉布斯采样:变量多时无法枚举 2^n,用近似采样估计边缘概率 ----
def conditional_prob(var, assign):
"""给定其余变量,P(var=1|其余) ∝ exp(该变量取1时的能量),用 sigmoid 写成比值。"""
def energy_with(val):
trial = dict(assign); trial[var] = val
return sum(w * factor_value(scope, kind, trial)
for scope, w, kind, _ in factors if var in scope)
e1, e0 = energy_with(1), energy_with(0)
return 1.0 / (1.0 + math.exp(-(e1 - e0))) # sigmoid(e1 - e0)
assign = dict(clamped)
for v in unknown: assign[v] = 0
SWEEPS, BURN_IN = 8000, 1000
hit = {v: 0 for v in unknown}
for sweep in range(SWEEPS + BURN_IN):
for v in unknown: # 不共享因子的变量本可并行独立采样
p = conditional_prob(v, assign)
assign[v] = 1 if random.random() < p else 0
if sweep >= BURN_IN: # 丢弃燃烧期,统计取值为 1 的频率
for v in unknown:
hit[v] += assign[v]
print("\n== 边缘概率 P(v=1):精确枚举 vs 吉布斯采样 ==")
for v in unknown:
gibbs = hit[v] / SWEEPS
print(f" {v}: 精确={exact_marginal(v):.3f} Gibbs≈{gibbs:.3f}")
print("\n解读:v4 经对称因子 f5 与正例 v3 强绑定、又有自身一元特征 f4,边缘概率很高;"
"v1 没有标签先验、自身也没有激活的一元特征(它的'投资'因子 f1 因 v2 被钳为 0 而不激活),"
"主要靠与 v4 共享'股权'特征的跨变量因子 f3 被'连带'抬高——"
"这正是因子图做全局联合(而非逐条三元组)推断的效果。")== 枚举所有可能世界(与先验 v2=0、v3=1 一致的才合法)==
world v1=0,v4=0 (v2=0,v3=1) 能量=0.0 未归一化权重=1.000 概率=0.008
world v1=0,v4=1 (v2=0,v3=1) 能量=3.5 未归一化权重=33.115 概率=0.265
world v1=1,v4=0 (v2=0,v3=1) 能量=0.0 未归一化权重=1.000 概率=0.008
world v1=1,v4=1 (v2=0,v3=1) 能量=4.5 未归一化权重=90.017 概率=0.719
== 边缘概率 P(v=1):精确枚举 vs 吉布斯采样 ==
v1: 精确=0.727 Gibbs≈0.726
v4: 精确=0.984 Gibbs≈0.986吉布斯采样的近似结果与枚举精确解几乎重合:v4 被对称因子 f5 与正例 v3 强绑定、又有自身一元特征 f4,边缘概率高达 0.98 以上;没有标签先验的 v1 自身也没有激活的一元特征(它的“投资”因子 f1 因 v2 被钳为 0 而不参与本次推断),主要经与 v4 共享“股权”特征的跨变量因子 f3 被连带抬高到约 0.73。这里要区分两个层次。其一,在课件 Page 35 的形式化推导以及上面的演示里,先验变量集合 P/N(如 v2、v3)取值固定、在推断中被钳制(clamp),只有 v1、v4 这类未打标变量会被全局信号更新。其二,讲授中特别提醒:真实 DeepDive 里远程监督与规则给出的标签是带噪声的弱监督证据(远程监督本身就会误标)。在把标签当作加权弱证据(而非硬钳制)的工程设定下,系统用吉布斯采样做近似推断、SGD 做近似学习,在整张图上联合更新各实例概率,置信度结果表(讲授称之为 transaction_label_inference,对应课件 Page 40 的“置信度结果表”)会为每条实例重新估计概率;由于采样与 SGD 都是近似方法,它并不保证真正的全局最优,但当周围证据强烈矛盾时,其最终概率仍可能与初始标签不一致——这也正是后续要用校准图、Mindtagger 反复核查并补充先验数据的原因。需要强调两种设定不要混淆:Page 35 的“固定先验”是为推导吉布斯采样而做的简化设定(此时 P/N 变量保持钳制、只有未打标变量被推断,代码块 4 即按此演示);而在弱证据设定下标签并非绝对硬约束,已标实例也可能被全局证据修正。
3.9 置信度结果与迭代调试:校准图、Mindtagger、Mindbender
推断完成后,DeepDive 除了生成底层数据表,还会为被推导的标记关系生成一张带置信度的结果表(讲授称 transaction_label_inference,即课件 Page 40 所说的“置信度结果表”):每条候选关系实例的 label 都附带一个概率估计(期望);期望超过阈值的正例,最终才作为目标关系 has_transaction 的三元组写入知识库。换句话说,transaction_label_inference 保存的是“每个候选实例 + 概率”,has_transaction 是筛出高置信正例后落地的关系,二者是同一推断结果的两个阶段,并不矛盾。随后进入 KBC 流程的“迭代优化”环节,DeepDive 提供了三类调试工具:
- 校准图(calibration plots,课件 Page 42):执行
deepdive do calibration_plots,在 run/model/calibration_plots下生成图。图 (a) 横轴是模型分数、纵轴是实际正例率,曲线越接近对角标准线,说明置信度越“校准”(说 0.8 就约有 80% 为真);图 (b) 是测试集上的置信度分布,越靠近两端(接近 0 或接近 1)说明区分度越好,若大量样本挤在中间则特征判别力不足;图 (c) 是全量数据上的置信度分布。 - Mindtagger(可视化标注/排查工具,课件 Page 43):执行
mindbender tagger labeling/*/mindtagger.conf启动一个本地 Web 服务(localhost:8000),把高分/低分候选实例及其特征列出来供人工核查。通过分析“为什么某个错例置信度很高/很低”,回溯到相关特征的权重,进而补充规则、特征或先验数据。 - Mindbender search(可视搜索,课件 Page 44):执行
mindbender search update建索引、mindbender search gui起界面,可以快速定位某个实体(如“东旭”)出现在哪些mention_text、哪些特征里、是作为 P1 还是 P2;这依赖建表时的 @key 与 @searchable 注解。
3.10 DeepDive 的定位与后续:Snorkel、cn-deepdive
课件 Page 45 对 DeepDive 做了总结:
- 模块化、便于替换:NLP、特征、规则、推断各环节解耦,想换更好的 NER(如端到端神经网络模型)只需替换对应 UDF,因为 NLP 质量对最终结果影响很大;
- 便于分析与迭代开发:通过校准图与可视化工具,可以持续加特征、加规则、加先验数据来提升效果;
- 端到端完整方案:从原始文本到带置信度的知识库三元组,DeepDive 给出了完整闭环。
DeepDive 官方在 2017 年前后停止维护,其思想(弱监督、数据编程)被后续项目 Snorkel 继承;在 OpenKG 上还有中文维护版本 cn-deepdive(http://www.openkg.cn/tool/cn-deepdive )。今天复现这套方法,完全可以用 Python 生态替代:用 spaCy/HanLP 等做 NLP、用 pandas/数据库做表与自连接、用 pgmpy 等概率图库或手写吉布斯采样做因子推断——本讲的代码块 1-4 正是这样的最小等价实现。
4. 开放域关系抽取 OpenIE:不预定义关系
4.1 传统 IE 与 OpenIE 的对比与互补
回到课件 Page 46-48。传统 IE 与 OpenIE 的核心差异在于是否预定义关系:
| 维度 | 传统关系抽取(IE) | 开放域关系抽取(OpenIE) |
|---|---|---|
| 关系类型 | 事先预定义(如 has_transaction) | 不预定义,关系短语从文本归纳 |
| 领域 | 特定领域,依赖领域专家知识 | 通用领域,依赖句法特征 |
| 规模与精度 | 规模较小、精度高 | 全网规模、精度相对低 |
| 代表 | Freebase、DBpedia 的填充(YAGO 被课件单列为“语义化扩展”) | TextRunner、WOE、Reverb、OLLIE、ClauseIE(课件该页另把 DeepDive 也列在此侧) |
课件 Page 48 把 DeepDive 列在开放抽取一侧,是因为它同样面向大规模异构文本、弱监督运行;但就“关系是否预定义”而言,DeepDive 抽取的 has_transaction 仍是预定义目标,更接近传统 KBC,本节据此把它放在第 3 节讲。两类系统的产出互补:OpenIE 的三元组可用于扩充知识库,或按现有知识库规范把更多网络数据链接进来。
4.2 第一代:TextRunner 与 WOE
TextRunner(课件 Page 49,华盛顿大学) 是第一个有影响力的 OpenIE 系统,采用自监督(self-supervised):抽取命名实体(NER)、词性(POS)、依存句法(dependency parsing)三类特征,训练分类器判断候选三元组是否可信,学习模型为朴素贝叶斯(Naive Bayes)与条件随机场(CRF,Conditional Random Field)。它用少量自动标注的种子样本训练后对全网文本运行,无需为每种关系单独标注。
WOE(Wikipedia-based Open Extractor,课件 Page 49) 在 TextRunner 的规模上,进一步把核心语法依存路径(dependency path)本身当作关系。经典例子是被动句 “Albert Einstein was awarded the Nobel Prize”:依存分析中,Einstein 通过被动主谓关系 nsubjpass 连到谓语 was awarded,而 the Nobel Prize 通过直接宾语 dobj 连到 awarded,于是沿依存路径 Einstein —nsubjpass—> was awarded <—dobj— the Nobel Prize 就能得到三元组 (Albert Einstein, was awarded, the Nobel Prize)。需要说明箭头约定:课件这张示意图把箭头画成由论元指向谓语中心词(依存方→head,两条弧在 awarded 处汇合);而 Universal Dependencies 的通行画法是 head→dependent(即 awarded 分别指向 Einstein 与 Prize),两种画法表达的依存关系相同、只是方向相反。把依存路径作为关系,比只看表层词序列更能抓住句法结构。
4.3 第一代的两类典型错误:从句误抽与多元关系信息损失
课件 Page 50 总结了第一代 OpenIE 的两大挑战,概括起来是“关系不一致、不准确,以及提取的关系不含有效信息”:
- 从句误抽(把非主句的关系安到错误主体上):例如 “Peter thought that John began his career as a scientist”,正确三元组是 (John, began, his career as a scientist);若不区分子句,系统会错误地抽出 (Peter, began, …),因为 began 在 Peter 管辖的 that 从句里。
- 多元关系/事件信息损失:例如 “Al-Qaeda claimed responsibility for the 9/11 attacks”,正确关系应是完整的 (Al-Qaeda, claimed responsibility, for the 9/11 attacks);第一代系统容易把它截断成 (Al-Qaeda, claimed, responsibility),丢掉了真正有信息量的“为 9/11 袭击负责”这一整体语义。
4.4 第二代:Reverb、OLLIE、ClauseIE
第二代系统针对上述问题,从关系短语的形态与句子的结构入手。
Reverb(课件 Page 51) 是基于动词的关系抽取,围绕动词词组定义关系模式 V | VP | VW*P(星号表示 W 可重复零到多次;V 为动词可带小品词/副词,W 为名词/形容词/副词/代词/限定词,P 为介词/小品词/不定式标记):关系短语要么是动词组 V,要么是动词加介词 VP,要么是“动词 + 若干 W + 介词”的 VW*P(如 “is the CEO of”)。它先定位动词再向两侧切出主体与客体,约束了关系短语形态。Reverb 来自 KnowItAll 项目(GitHub 的 knowitall 工程,OpenKG 收录:http://www.openkg.cn/tool/reverb )。
OLLIE(Open Language Learning for Information Extraction,课件 Page 51) 在 Reverb 基础上,增加了名词与形容词中包含的语义信息。例如 “Microsoft co-founder Bill Gates spoke at the conference”,由于 co-founder 是名词而非动词,Reverb 抽不到想要的关系;OLLIE 通过名词/形容词模板能补出 (Bill Gates, be co-founder of, Microsoft)。它把 Reverb 抽取的高置信关系作为种子(seed)做 bootstrap(自举),学习更多表达模板(OpenKG 收录:http://www.openkg.cn/tool/ollie )。
ClauseIE(课件 Page 52) 是基于子句(clause)的抽取:先把句子切分成若干从句,再识别每个从句的类型,最后在各从句上抽取关系。从句类型由语法规则加句法依存、通过一棵决策树(Decision Tree)判定;流程是“抽取从句集合 → 识别从句类型 → 按类型抽取关系”。于是 “Peter thought that John began …” 被切成两个从句,began 归到 John 所在从句,避免误抽。中文“一逗到底”的长句若直接整句依存分析,搜索空间极大、甚至内存溢出(OOM),先切子句也能缓解;代价是单子句信息可能不完整,有时需组合多个子句或只选取特定子句抽取。
代码块 5 用标准库正则与序列规则,最小化地还原了 Reverb 的动词模式、OLLIE 的名词模板、WOE 的依存路径以及 ClauseIE 的子句切分。
# 代码块5:开放域关系抽取(OpenIE)两代方法的最小可运行演示
# 第一代 TextRunner/WOE 依赖 NER/POS/依存特征 + 自监督分类;第二代 Reverb 围绕动词词组、
# OLLIE 扩展到名词/形容词、ClauseIE 先算子句。这里用标准库正则/序列规则还原它们的核心思想。
# 说明:英文模式直接对应课件;中文语法不同,实战需替换为中文句法规则。
import re
# ---------- (1) Reverb:围绕动词词组的关系模式 V | VP | VW*P ----------
# V=动词(可含助词/副词) W=(名词|形容词|副词|代词|限定词) P=(介词|小品词|不定式标记)
# 真实系统用 POS tagger + 句法块分析得到标签;演示用内联小标签词典
TAG = {
# Einstein was awarded the Nobel Prize
"Einstein": "N", "was": "V", "awarded": "V", "the": "DET", "Nobel": "N", "Prize": "N",
# Microsoft co-founder Bill Gates spoke at the conference
"Microsoft": "N", "co-founder": "N", "Bill": "N", "Gates": "N",
"spoke": "V", "at": "PREP", "conference": "N",
# Bill Gates is the CEO of Microsoft (VW*P:名词+介词构成关系短语)
"is": "V", "CEO": "N", "of": "PREP",
}
V_TAGS, W_TAGS, P_TAGS = {"V"}, {"N", "ADJ", "ADV", "PRON", "DET"}, {"PREP", "PRT"}
def reverb_extract(words):
"""按 V | VP | VW*P 在动词周围切出 主体/关系短语/客体。"""
tags = [TAG[w] for w in words]
vi = next((i for i, t in enumerate(tags) if t in V_TAGS), None)
if vi is None:
return None
rel_end = vi + 1
# 动词组后先吞并列的动词/助词/副词
while rel_end < len(words) and tags[rel_end] in {"V", "PRT", "ADV"}:
rel_end += 1
# VW*P:若动词组之后出现介词 P,则把中间的 W*(名词/限定词…)和该介词并入关系短语
rest = tags[rel_end:]
p_idx = next((k for k, t in enumerate(rest) if t in P_TAGS), None)
if p_idx is not None and all(t in W_TAGS for t in rest[:p_idx]):
rel_end += p_idx + 1
subj = " ".join(words[:vi])
rel = " ".join(words[vi:rel_end])
obj = " ".join(words[rel_end:])
return (subj, rel, obj) if subj and obj else None
print("== Reverb(基于动词词组)==")
for s in ["Einstein was awarded the Nobel Prize",
"Bill Gates is the CEO of Microsoft",
"Microsoft co-founder Bill Gates spoke at the conference"]:
words = s.split()
tri = reverb_extract(words)
print(f" 句: {s}\n -> {tri}")
print(" 注意:第三句真正想要的 (Bill Gates, be co-founder of, Microsoft) 中 co-founder 是\n"
" 前置名词定语,Reverb 的动词模式抽不到;它把整个名词短语当作主体,只抽到\n"
" (Microsoft co-founder Bill Gates, spoke at, the conference),未单独切出 Bill Gates。")
# ---------- (1b) OLLIE:在 Reverb 种子之外,增加名词/形容词模板,补上动词模式抽不到的关系 ----------
# 以 Reverb 抽得的关系作种子做 bootstrap,学习更多模板;这里给一条名词同位语模板示例:
# “<机构> co-founder <人名> ...” -> (<人名>, be co-founder of, <机构>)
def ollie_noun_template(s):
m = re.match(r"([A-Za-z]+)\s+(co-founder|founder|CEO)\s+([A-Za-z]+(?:\s[A-Za-z]+)?)", s)
if m:
org, role, person = m.group(1), m.group(2), m.group(3)
return (person, f"be {role} of", org)
return None
print("\n== OLLIE(名词/形容词模板,以 Reverb 结果为种子 bootstrap)==")
s3 = "Microsoft co-founder Bill Gates spoke at the conference"
print(f" 句: {s3}")
print(f" -> Reverb 抽不到的,OLLIE 名词模板补出: {ollie_noun_template(s3)}")
# ---------- (2) WOE:把核心语法依存路径本身当作关系 ----------
# 对 “Einstein was awarded the Nobel Prize” 的一棵最小依存树(nsubjpass 被动主谓 / dobj 直接宾语)
dep = {"root": "awarded", "nsubjpass:awarded": "Einstein", "dobj:awarded": "Prize"}
print("\n== WOE(依存路径作为关系)==")
head = dep["root"]
s, o = dep["nsubjpass:" + head], "the Nobel " + dep["dobj:" + head]
print(f" 依存路径: {s} --nsubjpass--> {head} <--dobj-- {o}")
print(f" 三元组 : ({s}, was {head}, {o})")
# ---------- (3) ClauseIE:先把长句拆成子句(clause),再按子句类型抽取 ----------
def split_clauses(sentence):
"""按逗号与从句引导词(that/because/which)切分子句集合(真实系统用决策树判子句类型)。"""
parts = re.split(r"\s*(?:,\s*|\s+that\s+|\s+because\s+|\s+which\s+)", sentence)
out = []
for p in parts:
p = re.sub(r"^(and|but|or)\s+", "", p.strip()) # 去掉切分后残留的并列连词
if p:
out.append(p)
return out
print("\n== ClauseIE(先拆子句,避免第一代的从句误抽)==")
long_sents = [
"Peter thought that John began his career as a scientist",
"Al-Qaeda claimed responsibility for the 9/11 attacks, and officials confirmed the report",
]
for s in long_sents:
cls = split_clauses(s)
print(f" 原句: {s}")
for c in cls:
print(f" 子句 -> {c}")
print(" 逐子句抽取时,‘began his career’ 归到 John 子句,不会再误抽成 (Peter, began, ...);")
print(" 多元事件也能在更完整的子句上保留 ‘claimed responsibility for ...’ 这类完整关系短语。")== Reverb(基于动词词组)==
句: Einstein was awarded the Nobel Prize
-> ('Einstein', 'was awarded', 'the Nobel Prize')
句: Bill Gates is the CEO of Microsoft
-> ('Bill Gates', 'is the CEO of', 'Microsoft')
句: Microsoft co-founder Bill Gates spoke at the conference
-> ('Microsoft co-founder Bill Gates', 'spoke at', 'the conference')
注意:第三句真正想要的 (Bill Gates, be co-founder of, Microsoft) 中 co-founder 是
前置名词定语,Reverb 的动词模式抽不到;它把整个名词短语当作主体,只抽到
(Microsoft co-founder Bill Gates, spoke at, the conference),未单独切出 Bill Gates。
== OLLIE(名词/形容词模板,以 Reverb 结果为种子 bootstrap)==
句: Microsoft co-founder Bill Gates spoke at the conference
-> Reverb 抽不到的,OLLIE 名词模板补出: ('Bill Gates', 'be co-founder of', 'Microsoft')
== WOE(依存路径作为关系)==
依存路径: Einstein --nsubjpass--> awarded <--dobj-- the Nobel Prize
三元组 : (Einstein, was awarded, the Nobel Prize)
== ClauseIE(先拆子句,避免第一代的从句误抽)==
原句: Peter thought that John began his career as a scientist
子句 -> Peter thought
子句 -> John began his career as a scientist
原句: Al-Qaeda claimed responsibility for the 9/11 attacks, and officials confirmed the report
子句 -> Al-Qaeda claimed responsibility for the 9/11 attacks
子句 -> officials confirmed the report4.5 更多进展与 OpenIE 的应用
课件 Page 53 介绍了开放抽取的更多进展方向:在模型层面,模板匹配与深度学习结合、用矩阵分解(matrix factorization,课件原文笼统称“分类器”,它本质属表示学习/降维类模型而非分类器)等模型提升判别力;在源数据层面,引入结构化知识库、做联合训练(joint training);以及训练一个统一模型同时抽取实体和关系(即联合抽取),并反过来利用知识库做更好的实体链接与特征抽取。
课件 Page 54 总结了 OpenIE 的应用:
- 直接回答问题(问答 QA):很多 Web 问句很短、基于单一关系,可以把问题表示成 (A1, ?, A2) 的形式(已知一个实体与答案类型,缺关系/另一实体),用抽取出的三元组直接匹配作答;多关系问句则有相应的扩展方法。
- 作为其他 NLP 任务的特征:开放抽取得到的关系三元组可作为文本理解、语义相似度比较等任务的结构化特征。
- 支撑阅读理解、文本蕴含(textual entailment)与语义相似度匹配等深层任务。
开放抽取与自动问答的结合在工业界有持续投入,例如 Paul Allen 支持的 Allen AI Institute(AI2),其负责人 Oren Etzioni 正是华盛顿大学 KnowItAll/OpenIE 系列项目的原负责人,目标之一就是利用大规模抽取的知识来构建问答系统。
5. 抽取结果落地与现代 Python 工具链
5.1 把高置信三元组写进知识库:rdflib 落地
DeepDive 流水线的终点是带置信度的实体层关系实例。把它们真正写入知识库时,需要完成第 1.4 节说的 mention → entity 归一:实体用 URI 表示,中文标准名存为 rdfs:label,关系用预定义谓词(如 ex:hasTransaction)。这里只做落地三元组所必需的最小归一(把同一公司的 mention 归并到一个规范实体/URI);完整的实体消歧与链接(把 mention 链到知识库中已有实体、处理同名与多名问题)属于本章第二部分“知识挖掘”的内容,本节不展开。置信度不是 RDF 原生语义,标准做法是先用 RDF 具体化(reification,rdf:Statement 及其 subject/predicate/object)把一条三元组变成可被描述的资源,再用自定义属性(本例的 ex:confidence)把数值挂上去。需要提醒:W3C RDF 1.1 规范注明 reification 词汇主要为历史兼容而保留、并不推荐新应用直接使用,工程上承载陈述级元数据(置信度、来源、时间)更推荐命名图(named graph)或 RDF-star;这里用具体化只是因为它把“陈述即资源”演示得最直观。代码块 6 用 rdflib 完成落地,并用一条 SPARQL 查询筛出高置信关系(SPARQL 1.1,经 rdflib 实际执行校验)。
# 代码块6:把因子图推断后的高置信关系实例落地为知识库三元组(rdflib 可校验)
# 对应 DeepDive 的最后一步:transaction_label_inference(带置信度)-> 结构化知识库
# 实体用 URI 表示(entity level),中文标准名放 rdfs:label;
# 先用 RDF 具体化(rdf:Statement)把三元组变成可描述资源,再用自定义属性 ex:confidence 挂置信度。
from rdflib import Graph, Namespace, URIRef, Literal, RDF, RDFS
from rdflib.namespace import XSD
EX = Namespace("http://example.org/kg/")
g = Graph()
g.bind("ex", EX)
# 内联样例:因子图输出的候选关系实例及其边缘概率(置信度)
inferred = [
("qingniao", "青鸟华光", "kangxin", "康欣新材", 0.977),
("dongxu", "东旭集团", "kangxin", "康欣新材", 0.86),
("qingniao", "青鸟华光", "dongxu", "东旭集团", 0.71),
]
def org(slug, name):
u = URIRef(EX + "org/" + slug) # 实体在知识库中以 URI 标识(entity linking 之后),中文标准名另存为 rdfs:label
g.add((u, RDF.type, EX.ListedCompany))
g.add((u, RDFS.label, Literal(name, lang="zh")))
return u
for sa, na, sb, nb, conf in inferred:
a, b = org(sa, na), org(sb, nb)
stmt = URIRef(EX + "stmt/" + sa + "_" + sb) # 具体化节点:一条“带置信度的陈述”
g.add((stmt, RDF.subject, a))
g.add((stmt, RDF.predicate, EX.hasTransaction))
g.add((stmt, RDF.object, b))
g.add((stmt, EX.confidence, Literal(conf, datatype=XSD.double)))
g.add((a, EX.hasTransaction, b)) # 规范方向的关系三元组(置信度挂在上面的 stmt 上)
g.add((b, EX.hasTransaction, a)) # 对称关系补一个反向三元组(演示对称性;
# 工程上可把 hasTransaction 声明为 owl:SymmetricProperty,或为反向也各建一个具体化节点)
print("== 落地到知识库的关系三元组(简化打印,仅作节选示意)==")
for s, p, o in g.triples((None, EX.hasTransaction, None)):
print(f" <{s.split('/')[-1]}> ex:hasTransaction <{o.split('/')[-1]}>")
# 用 SPARQL 查出置信度 >= 0.8 的股权交易关系(SPARQL 1.1,经 rdflib 执行校验)
q = """
PREFIX ex: <http://example.org/kg/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?na ?nb ?conf WHERE {
?s ex:confidence ?conf ;
rdf:subject ?a ; rdf:predicate ex:hasTransaction ; rdf:object ?b .
?a rdfs:label ?na . ?b rdfs:label ?nb .
FILTER(?conf >= 0.8)
} ORDER BY DESC(?conf)
"""
print("== 高置信(>=0.8)股权交易关系 ==")
for row in g.query(q):
print(f" {row.na} --相关交易--> {row.nb} 置信度={float(row.conf):.3f}")== 落地到知识库的关系三元组(简化打印,仅作节选示意)==
<qingniao> ex:hasTransaction <kangxin>
<dongxu> ex:hasTransaction <kangxin>
<kangxin> ex:hasTransaction <qingniao>
<dongxu> ex:hasTransaction <qingniao>
<kangxin> ex:hasTransaction <dongxu>
<qingniao> ex:hasTransaction <dongxu>
== 高置信(>=0.8)股权交易关系 ==
青鸟华光 --相关交易--> 康欣新材 置信度=0.977
东旭集团 --相关交易--> 康欣新材 置信度=0.860上面这段“节选”只是为了人眼可读做的简化打印:用谓词短名 ex:hasTransaction、只打印本地名、句末不加句号,属于自定义的简化三元组示意格式,并非标准 N-Triples(标准 N-Triples 要求完整 URI 用尖括号包裹、每条以 . 结尾)。真正要序列化时,rdflib 的 g.serialize(format="nt") 才会输出规范的 N-Triples(或 Turtle)。
5.2 旧工具链到现代 Python 的对照
DeepDive 是一套重工程(PostgreSQL + Stanford NLP + 分布式运行时 + ddlog),理解其原理后,用现代 Python 即可搭出等价的轻量流水线,对应关系如下:
| DeepDive 原工程环节 | 现代 Python 等价做法 |
|---|---|
| app.ddlog 建表与派生规则 | 用 pandas DataFrame 或普通字典/集合表示表,用函数表达派生 |
| PostgreSQL 自连接生成候选对 | itertools.combinations + 同句/不重合过滤(代码块 2) |
Stanford NLP(nlp_markup)产出 token/POS/NER/依存 | spaCy、HanLP、LTP 等中文 NLP 库;教学演示可用词典规则(代码块 1) |
| ddlib 窗口特征 | 列表切片 + n-gram + 集合命中(代码块 2) |
| 远程监督 + supervise 规则 + vote | 知识库集合匹配 + 规则函数 + 权重求和(代码块 3) |
| 因子图 + 吉布斯采样 + SGD 权重学习 | pgmpy 概率图库,或手写马尔可夫网与 Gibbs(代码块 4) |
| calibration / Mindtagger / Mindbender | 用 sklearn 校准曲线(calibration_curve)、matplotlib 直方图、简单检索脚本替代 |
| 结果写入知识库 | rdflib 建图、序列化为 Turtle/N-Triples、SPARQL 查询(代码块 6) |
旧工具链的价值在于它把“弱监督 + 全局推断 + 迭代调试”这套方法论固化成了可复用的工程范式;工具会过时,方法论不会。
📝 动手练一练
练习 1(特征与打标):在代码块 2、3 的样例中再增加一句公告“康欣新材收购了一家境外公司,青鸟华光未参与本次交易。”,其中包含公司“康欣新材”“青鸟华光”以及一个未登录的境外公司名。请思考并动手:(a) 规则 NER 对未登录公司名会打成什么标签?这会如何影响候选对生成?(b) 句中的否定词“未参与”会对 R2 交易模式规则造成什么误判?应如何增加一条否定规则来纠正?
👉 点击查看参考答案
(a) 未登录的境外公司名不在公司词典中,规则 NER 会把它的每个 token 都打成 O,于是 map_company_mention 不会为它生成 mention,候选对里也就不会出现它——这暴露出词典/规则 NER 召回受限的问题,真实系统应改用统计或深度 NER(CRF、BiLSTM-CRF)。(b) R2 只看上下文是否同时出现交易动作词与对象词,“收购”“交易”同时出现会让(康欣新材, 青鸟华光)被误判为正例;但“青鸟华光未参与本次交易”是明确否定。应增加一条否定规则:若实体对与交易词之间出现“未、没有、不、并未、无”等否定标记(可检测否定词是否落在实体与触发词的窗口内),则投一张高权重负票(例如 -2),使其总和转负。这说明纯词面特征需要结合否定与句法结构,否则极易把“不成立”的关系判成“成立”。
练习 2(因子图推断):在代码块 4 中,把对称因子 f5(v3,v4) 的权重从 3.0 改为 0(即去掉“交易关系对称”这一约束),其余不变。先定性预测 v4 的边缘概率会升高还是降低,再运行验证;并解释为什么特征因子通常需要学习权重、而对称因子可以直接写死为固定权重。
👉 点击查看参考答案
去掉对称因子后,v4 不再被正例 v3 通过“v3=1 则 v4 应=1”的强约束拉动,只剩自身一元特征因子 f4(v4)=0.5 以及与 v1 共享“股权”特征的跨变量因子 f3(v1,v4)。此时主导世界(v1=1,v4=1)的能量从 4.5 降到 1.5(恰好少了对称因子 f5 贡献的 3.0),边缘概率明显下降(精确枚举:v4 从约 0.984 降到约 0.754,v1 从约 0.727 降到约 0.674,可自行运行验证)。原因在于:特征因子刻画的是“某上下文特征在多大概率上指示关系成立”,这个强度因领域、语料而异,必须从数据中学习(可正可负);而对称因子表达的是 has_transaction 这类关系在定义上的逻辑性质(若甲和乙有交易,乙和甲必然也有交易),是确定性的领域公理,不依赖语料统计,因此可以直接赋予一个足够大的固定权重来表达硬约束。
本章小结
本节围绕“从自然语言文本抽取知识”展开,主线是关系抽取:
- 任务地图:文本抽取含实体识别、关系抽取、事件抽取等并列子任务(关系抽取还可向多元关系、跨句、联合抽取拓展);关系抽取分传统 IE(预定义关系、高精、小规模)与 OpenIE(不预定义、全网、较低精)两条路线,并可按方法(特征/核函数/深度学习/模板/图推理)、数据(人工/远程监督/自举/无监督)、拓展(多元、跨句、联合)三维度分类;要区分 mention 层与 entity 层,实体链接是从句子到知识库的关键一跳。
- DeepDive(KBC 框架):方法论是特征工程 + 远程监督 + 因子图全局优化;工程上由 app.ddlog、db.url、deepdive.conf、input、udf 组成,流水线为“入库 → NLP 预处理(sentences)→ 候选实体(
company_mention)→ 候选对(transaction_candidate)→ 窗口特征(transaction_feature,ddlib)→ 远程监督与规则投票(transaction_label_resolved)→ 因子图推断(has_transaction概率)→ 校准与可视化迭代”。 - 因子图推断:变量是候选关系实例(二值),因子分特征因子(权重需学习,跨变量因子把共享同一特征的候选耦合起来)与对称因子(固定权重的逻辑约束);可能世界随变量数指数增长,用吉布斯采样近似边缘概率、SGD 在采样样本上近似学习权重;共享因子让未知变量被高置信变量连带判定,实现全局联合的证据融合(注意这是近似推断/近似学习,并不保证真正的全局最优),在证据充分一致时缓解远程监督噪声。
- OpenIE 两代系统:第一代 TextRunner(自监督,朴素贝叶斯/CRF)与 WOE(依存路径作关系),存在从句误抽与多元信息损失;第二代 Reverb(动词模式 V|VP|VW*P)、OLLIE(名词/形容词模板、bootstrap 种子)、ClauseIE(先切子句、决策树判类型);OpenIE 可直接服务问答(A1,?,A2)并作为其他 NLP 任务的特征。
- 落地与现代化:抽取得到的高置信关系实例可落地为三元组——实体经最小归并用 URI 表示(完整的实体消歧与链接留到“知识挖掘”一节),置信度经 RDF 具体化把陈述变为资源后再用自定义属性(或命名图/RDF-star)承载,可用 rdflib 序列化为 Turtle 并以 SPARQL 查询;DeepDive 的重工程可用 Python(NLP 库、集合/itertools、概率图库、rdflib)轻量复现,工具会过时,“弱监督 + 全局推断 + 迭代调试”的方法论长期有效。
📋 行动清单
- 不看代码,口述 DeepDive 从 articles 到带置信度
has_transaction的完整流水线,说清每一步的输入表、输出表与对应 UDF/规则。 - 把代码块 1-4 连起来改成抽取另一种二元关系(如“公司—首席执行官”
has_ceo):替换公司/人物词典、触发词与打标规则,跑通 mention→候选对→特征→投票→吉布斯采样全链路。 - 任选一段英文财经新闻,手工标注其中的动词关系短语,对照 Reverb 的 V|VP|VW*P 模式判断哪些能抽到、哪些需要 OLLIE 的名词模板或 ClauseIE 的子句切分。
—— 小象教研组
领取《小象 11GB VIP 课件资料包与大厂真题手册》
包含全套实战 Jupyter 源码、清洗后数据集、大厂高频面试真题与专属学员答疑交流群。
- ✔完整 Python / 数据分析 Jupyter 实战源码
- ✔大厂真实业务数据集与练习题
- ✔微信扫码添加顾问免费领取;想学什么,直接告诉顾问
微信扫码添加顾问