📑 查看全课大纲(第 13 / 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_spouseworks_forhas_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 的方法论概括为三块:

  1. 特征工程(feature engineering):从文本中自动抽取海量特征(词、词性、命名实体标签、依存路径、窗口 n-gram、词典命中……),把判断关系是否成立的证据显式化。
  2. 远程监督(distant supervision):用已有知识库(如金融领域的国泰安数据库)自动产生训练标签,避免大规模人工标注。
  3. 图优化(graph optimization / 因子图推断):把所有候选关系实例与特征组织成一张因子图(factor graph),在图上做全局概率推断。

最关键的设计思想是全局联合推断:传统流水线逐条独立判断三元组;DeepDive 把所有候选实例放进一张概率图,让它们通过共享特征、对称约束等因子相互影响,为每个实例给出边缘概率(置信度)。这是一种近似的联合推断(吉布斯采样近似边缘概率、SGD 近似训练权重),旨在让整张图的证据相互融合,而不是孤立地逐条判断;但它并不保证求出真正的全局最优解。

2.3 KBC 工作流四环节与 DeepDive 工程组成

课件 Page 9 给出 KBC 处理流程的四个环节:

  1. 特征抽取(feature extraction):包括 OCR(针对扫描文档)、NLP 工具(分词、词性、命名实体、依存分析)、以及用户自定义脚本。
  2. 专业知识融合(knowledge integration):在整个知识库层面融合多种关系与多来源知识,处理冲突与冗余。
  3. 监督学习(supervision / statistical learning & inference):利用远程监督与规则产生标签,训练因子图的权重。
  4. 迭代优化(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_namecompany2_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_idmention_textdoc_idsentence_indexbegin_indexend_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):

  1. 远程监督(来自知识库):若候选实体对能与 pos_transaction 中的已知交易对对应(实体名先做大小写、全半角归一化),就标为正例,规则名记为 positive from DB,权重较高(课件示例设为 +3)。
  2. 用户规则(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_idP2_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),不需要学习。

因子图是一张二部图(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 的两大挑战,概括起来是“关系不一致、不准确,以及提取的关系不含有效信息”:

  1. 从句误抽(把非主句的关系安到错误主体上):例如 “Peter thought that John began his career as a scientist”,正确三元组是 (John, began, his career as a scientist);若不区分子句,系统会错误地抽出 (Peter, began, …),因为 began 在 Peter 管辖的 that 从句里。
  2. 多元关系/事件信息损失:例如 “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 report

4.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 的子句切分。

—— 小象教研组

配套学习资源与课件
  • 第4章课件:知识抽取与挖掘 II
    下载
  • 第4章 DeepDive 实战说明
    下载
  • 知识图谱课程思维导图(KG_Centralized.xmind 全课程结构图)
    下载
🎁 免费学习资源

领取《小象 11GB VIP 课件资料包与大厂真题手册》

包含全套实战 Jupyter 源码、清洗后数据集、大厂高频面试真题与专属学员答疑交流群。

  • 完整 Python / 数据分析 Jupyter 实战源码
  • 大厂真实业务数据集与练习题
  • 微信扫码添加顾问免费领取;想学什么,直接告诉顾问
微信二维码:扫码添加课程顾问微信扫码添加顾问