📑 查看全课大纲(第 25 / 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.行业知识图谱应用
IBM watson Lite
约 127 分钟
IBM Watson:DeepQA 开放域问答与知识问答 Demo
小象实战讲义 · 知识图谱
本节进入第 10 章。视频开头对第 9 章语义解析的收尾(WebQuestions、逻辑形式、远监督对齐与 bridging)已在第 9 章讲过,本节主体是两块新内容。第一块是 IBM Watson:在开放域问答擂台《Jeopardy!》(危险边缘)上战胜人类冠军的完整问答系统,核心问题是怎样在断网、3 秒限时、答错扣分条件下,把一句自然语言线索变成带置信度的短答案。 第二块是基于 Elasticsearch 的知识问答 Demo:把自然语言问句解析成 logical form(逻辑形式),套模板生成检索查询并执行,覆盖实体、属性、多跳、多属性布尔组合四类问题,已在 OpenKG 开源。实操统一用 Python:先复现 DeepQA 的”候选生成—多证据打分—机器学习融合”,再复现 Demo 的”自然语言→逻辑形式→检索执行”全链路与九项进阶;课程原工程基于 Java/UIMA/Elasticsearch/Jena,本节只客观介绍流程。
💡 核心导读
- 开放域问答(Open-domain QA)直接返回精确短答案而非文档列表,处在信息检索、NLP、知识表示与推理、机器学习、人机交互的交叉点。
- Watson 的核心工件是 DeepQA 架构与 AdaptWatson 方法论:不假设能完全理解问题,先广召回候选、把每个候选当独立假设搜集证据、用上百个打分器打分,再用机器学习融合成置信度概率、输出带置信度的候选排序。
- 类型判断用类型强制(type coercion)取代预枚举答案类型:不预写分类器,围绕词汇答案类型(LAT)为每个候选动态收集 is_a 证据,资源含 PRISMATIC、YAGO、WordNet。
- DeepQA 按”易并行(embarrassingly parallel)“设计,用 UIMA-AS 把候选、证据、打分摊到异步进程以保证 3 秒限时;评测看精度-置信度曲线、实时对局胜率、组件增量增益。
- ES 知识问答 Demo 链路是自然语言 → logical form → 检索查询 → 执行:预定义四类查询模板,用分词、属性字典、实体词典、属性值 n-gram 匹配解析,用 bool.must/should 与 range 组合查询;进阶含歧义消解、refinement、别名、概念上下位、反向检索、属性 path、多元关系拆解与单位统一。
一、开放域问答与 Watson 要打的擂台
1.1 什么是开放域问答
问答系统(QA)让机器自动回答自然语言问题,它与搜索引擎的区别在输出形态:搜索引擎返回相关文档的排序列表、答案要用户自己点开找;开放域问答直接给出精确短答案——问”海拔最高的山峰”,搜索给链接,问答系统直接给”珠穆朗玛峰”。
Watson 的擂台是美国电视问答节目《Jeopardy!》(危险边缘)。题目以陈述句线索给出,选手反推它描述的实体,回答还要套”What is / Who is”句式;答对加分、答错扣分,念完即抢答,线索充满指代、省略、双关与隐含关系,主题无所不包,即开放域。
1.2 为什么押注非结构化数据
Watson 的知识主要存在于非结构化数据中:文本、图像、语音的增长远快于结构化数据,百科、教材、期刊、手册、新闻、博客沉淀着最高价值的人类知识,让机器直接读自然语言有三个难点。
- Variable(表达多变):同一个意思有无数种说法,“高”可说”身高超过两米""个子很高”,线索还用代词、同义词、转述把实体藏起来;
- Polysemous(一词多义):“down”可以是方向、羽绒、地名,“grain”可以是谷物、格令、纹理,含义随语境而变;
- Implicit(隐含不显):语言不是形式化数学构造,“国语是高棉语的国家”里并不明说”答案是一个国家”,这层关系要靠推理补出。
这三点决定了开放域问答不能靠写死规则覆盖,必须用可扩展、可融合、带概率的架构”猜测—求证—定夺”。
1.3 前史:TREC、PIQUANT 与 UIMA
Watson 之前,学术界的问答评测主要是 TREC 的 QA Track,IBM 早年的系统叫 PIQUANT,代表老式范式:预先设定静态答案类型集合(人物、城市、国家、年份……),再为每类编写专用抽取算法。它在封闭评测里能跑,上 Jeopardy 却有两个致命问题:答案类型多到无法预枚举且高度语境化;不同研究者的算法难以快速度量融合、迭代慢。
配套基础设施是 UIMA(Unstructured Information Management Architecture,非结构化信息管理架构):为文本、语音、图像的分析组件提供与算法、语言、领域无关的公共集成平台,后成为 Apache 开源项目。其组件化思想是关键:分析应用由若干 annotator(标注器)按流水线组装,消费上游标注、产出新标注;Watson 跑在这套骨架上,但换掉了 PIQUANT”预枚举类型、专用算法”哲学。
1.4 参赛约束与量化指标
课件把赢 Jeopardy 的要求归纳为三条。第一,Self-contained(自包含):现场断网、无外援,不能联网搜索或求助他人,所有可能用到的信息赛前分析并存入内存;第二,Just three seconds(3 秒限时):从解析线索、理解所问、关联已读内容、定出最佳答案到算出置信度决定抢答,全程约 3 秒。第三,Deeper analysis in(深度分析):既要深挖线索本身,也要分析所有读过、可能为答案提供依据的内容——答案靠证据论证而非字面匹配;另有计分规则答错扣分:不必每题必答、只在有把握时抢答,催生下面的 Precision@70 与抢答阈值。
对标指标是 85% Precision@70:系统选择回答 70% 的题目时,已答题准确率须达 85%,来自对人类冠军战绩的统计——冠军也大约答七成题、其中八成五答对。它同时刻画覆盖率与精确率,契合”带置信度、可弃权”的博弈:Watson 的目标不是每题必对,而是对每个候选给出校准过的正确概率供策略层决定抢答。
1.5 两大工件:DeepQA 架构与 AdaptWatson 方法论
两个技术工件。其一是 DeepQA:可扩展的问答系统软件架构,规定从问题理解到置信度融合的流水线与组件插槽;其二是 AdaptWatson:一套方法论(论文 Towards the Open Advancement of Question Answering Systems),让大量算法被快速集成、度量、推进。DeepQA 的最终产物不是孤零零的答案,而是带置信度分数的候选答案排序列表,置信度即”基于全部支撑证据该答案正确的概率”;知识来源是混合的:既查结构化资源(知识库、本体,对应 KBQA 路线),也检索非结构化文本(对应 IRQA 路线),两路证据在同一框架下平等融合。
二、DeepQA 流水线:从一句线索到一个置信答案
2.1 七阶段流水线总览
自然语言线索
│
① 问题理解:得到 LAT 词汇答案类型、题目类别、句法/语义标注
▼
② 答案与证据源:离线完成语料获取、半自动扩充、清洗与派生资源构建
▼
③ 候选答案发现(假设生成):多种解释生成多种查询,打到结构化+非结构化多源
│ 每个「候选答案 + 问题」构成独立假设(hypothesis),各自分叉
▼
④ 正确类型(类型强制 type coercion):围绕 LAT 为每个候选收集 is_a 证据
▼
⑤ 证据搜集与打分:语法特征、关系抽取等上百个独立打分器
▼
⑥ 特殊问题:谜题、隐式关系、问题分解走专用算法路径
▼
⑦ 证据合并与置信度融合:机器学习把多打分器输出合成单一概率
▼
带置信度的候选答案排序列表 ──► 游戏策略层(阈值决定抢答/弃权)前三阶段重”广”(宁多召回、不漏正确答案),后四阶段重”证”(为每个候选独立取证、打分、合成概率)。
2.2 阶段①:问题理解
问题理解对线索做分词、句法、指代、命名实体、题目类别等尽可能深的分析,关键产物是 LAT(Lexical Answer Type,词汇答案类型)——线索中暗示”答案是个什么东西”的词:“sworn in as president in Islamabad”里 LAT 是 president,“This country’s official language is Khmer”里 LAT 是 country;课件强调对线索理解得越精确,后面找到并论证答案越容易,还要把”this X”指代还原成待填空位、题目类别作约束。
2.3 阶段②:答案与证据源
证据源建设分三步:source acquisition(获取)、transformation(转换清洗)、retrieval(检索服务)。Watson 从百科、参考书这类半结构化、事实密度高的语料起步再半自动扩充。这里有个反直觉权衡:加内容不一定加分——新文本可能引入噪声、让错误候选拿到虚假证据,断网下本地存储有限,每个数据源都要经端到端评测度量净贡献。
2.4 阶段③:候选答案发现——先生成一百个假设
这是 DeepQA 哲学最集中的体现。课件明确:DeepQA 不假设能按任何既有模型、类型本体或数据库模式完全理解问题。它把解析结果派生成多种解释,每种解释生成不同查询,同时打到结构化与非结构化源上,先得到尽可能广的候选集;每个”候选答案 + 原问题”构成一个独立假设(hypothesis),作为独立证据流程的根、分叉出大量并行支线取证。
考核指标是 candidate binary recall(候选二元召回率):正确答案被生成成候选的问题占比。假设生成阶段要最大化召回,哪怕混入大量错误候选,选最佳、给概率是后半段的事——为规避串行流水线的致命伤:早期该召回的没召回、后面再精密也救不回来,与第 5 章知识融合”blocking 先保召回、对齐再保精度”同理。候选来源互补:结构化源按线索构造查询拉候选,非结构化检索用线索多种改写搜段落抽候选。(注意区分:课件 Page 21–22 的”枚举给定类型全部实体、逐一查相关特征”——如”国语为高棉语的国家”即枚举全部国家再比对其国语——那是阶段⑤ broad-domain 关系抽取/证据打分的结构化取证手法,用来给已生成的候选打分,并不是阶段③生成候选来源,不要归到这里。)
2.5 阶段④:正确类型——类型强制(Type Coercion)
PIQUANT 预枚举全部答案类型、各配专用分类组件的做法,在 Jeopardy 近乎开放、高度语境化的 LAT 面前不可行。DeepQA 代之以类型强制(type coercion):动态、灵活、重语境,拿 LAT 为每个候选提出类型假设,再用多种算法到结构化与非结构化资源搜集支持/反对证据。 以线索”Hit: to strike; down: this direction”为例,LAT 是 direction,系统对候选”down""grain”分别提出 is_a(“down”,“direction”)、is_a(“grain”,“direction”),再到词典、本体、语料统计中求证:down 作方向义项证据充分、grain 不成立。
课件强调没有任何组件被预先训练成”方向分类器""国家分类器”;类型能力来自可扩展的技术集成,资源包括 PRISMATIC(从大规模语料自动归纳的词汇-句法关系知识库)、YAGO(融合维基百科与 WordNet 的本体)、WordNet(同义词集与义项层次),输出证据强度而非二元标签。
2.6 阶段⑤:证据搜集与打分
系统为每个候选搜集额外证据并逐条打分,每个打分器产出”该证据在多大程度上支持/反驳候选”的置信分,课件分两大类技术:
- grammar-based(基于语法):利用句法分析与浅层语义特征,如线索与证据段落的句法结构是否同构、依存关系是否对齐、关键词重合度;
- 关系抽取(relation extraction):又分两路——手工模式/规则精确、可复用但费人力;统计方法自动归纳模式从训练数据学习关系表达,省人力但依赖语料。
两个例子:电影《Alexander》一题,关系抽取要从语料里挖出”影片—主演(starring)—女演员""影片—导演—导演”这类实体-关系三元组,把线索里指代出演者的”she”关联到具体实体 Angelina Jolie;“国语是高棉语(Khmer)的国家”要拆出”国家—国语—高棉语”关系再找支撑(即下面代码块的题),每条证据的支持/反驳都量化为特征。
2.7 阶段⑥:特殊问题走专用路径
Jeopardy 中少量带特殊属性的谜题(puzzle)常规流程处理不好,DeepQA 的策略是先检测、命中后让专用算法路径优先。课件点名两类技术:隐式关系抽取(Identifying Implicit Relationships,Chu-Carroll 等)发现线索中不同概念未明说的共同点;问题分解(Fact-Based Question Decomposition,Kalyanpur 等)把复合问题拆成逻辑子问题分别求证再合并,专用路径不绕开主流水线、而作为额外候选与证据汇入融合。
2.8 阶段⑦:证据合并与置信度融合
量级感很重要:一题考虑约 100 个候选,每个”证据—候选”对可被约 100 个相互独立的打分器打分,系统面对的是庞大特征矩阵而非单个分数。DeepQA 用统计机器学习框架在历史标注数据上训练融合模型,自动学习各打分器权重与组合,输出每个候选正确的后验概率(论文 A Framework for Merging and Ranking of Answers in DeepQA,Gondek 等)。不用人工配权是因为上百个打分器间存在相关、冗余与互补,人工调参易错难维护;打分算法可随时增改、重训融合模型即可,下面最小复现该链路。
# -*- coding: utf-8 -*-
# DeepQA 最小复现:候选答案发现 -> 类型强制(type coercion) -> 多证据打分 -> 机器学习融合 -> 置信度排序与抢答阈值
# 演示题(Jeopardy 风格线索,LAT=country 国家):"Its official language is Khmer; this Southeast Asian country is home to Angkor Wat."
# 正确答案:Cambodia(柬埔寨)。全部确定性计算,无外部依赖。
import math
import random
def sigmoid(z):
return 1.0 / (1.0 + math.exp(-z))
# ---- 阶段1-2:问题理解得到 LAT(lexical answer type,词汇答案类型),证据源已离线读入内存 ----
LAT = "country"
clue_terms = {"official", "language", "khmer", "angkor", "wat", "country", "southeast", "asia"}
# 结构化源:小型 KB(实体 -> 类型与关系);非结构化源:证据段落(词袋)
KB = {
"Cambodia": {"types": {"country", "asian_country"}, "rel": {"language": "khmer", "capital": "phnom penh"}, "region": "southeast asia"},
"Thailand": {"types": {"country", "asian_country"}, "rel": {"language": "thai", "capital": "bangkok"}, "region": "southeast asia"},
"Vietnam": {"types": {"country", "asian_country"}, "rel": {"language": "vietnamese", "capital": "hanoi"}, "region": "southeast asia"},
"Khmer": {"types": {"language"}, "rel": {}, "region": None},
}
PASSAGES = { # 检索回来的证据段落(非结构化源),用词袋表示
"Cambodia": {"khmer", "official", "language", "angkor", "wat", "country", "southeast", "asia", "phnom", "penh"},
"Thailand": {"thai", "official", "language", "country", "bangkok", "southeast", "asia", "buddhist"},
"Vietnam": {"vietnamese", "official", "language", "country", "hanoi", "southeast", "asia"},
"Khmer": {"khmer", "language", "austroasiatic", "spoken", "cambodia"},
}
# ---- 阶段3:候选答案发现——对线索做多种解释、打到结构化+非结构化多源,尽量广地召回 ----
candidates = list(KB.keys()) # 正确答案必须先被召回(candidate binary recall=1),后面才有机会胜出
# ---- 阶段4:类型强制 type coercion——不预写分类器,而是为每个候选收集 is_a(候选, LAT) 的支持证据 ----
def type_coercion(cand, lat):
types = KB[cand]["types"]
if lat in types or lat.replace("country", "asian_country") in types:
return 0.95 # 结构化本体(YAGO/WordNet 风格资源)直接支持
return 0.10 # 仅有弱支持(词表间接关联)
# ---- 阶段5:证据打分——多个相互独立的打分器各产出一个置信特征 ----
def passage_overlap(cand): # grammar-based 证据:线索词与证据段落的重合度
return round(len(clue_terms & PASSAGES[cand]) / len(clue_terms), 3)
def relation_evidence(cand): # 关系抽取证据:线索要求 official language = Khmer
return 1.0 if KB[cand]["rel"].get("language") == "khmer" else 0.0
def geo_consistency(cand): # 时空/地理约束证据:线索限定 Southeast Asia
return 1.0 if KB[cand]["region"] == "southeast asia" else 0.0
def features(cand):
return [type_coercion(cand, LAT), passage_overlap(cand), relation_evidence(cand), geo_consistency(cand)]
# ---- 阶段7:融合——统计机器学习把跨证据、多打分器的特征合成单一概率(逻辑斯蒂回归,梯度下降学权重) ----
# 训练集来自历史 Jeopardy 题目的人工标注(特征向量, 1=该候选确实是正确答案)。
# 这里用固定随机种子合成一份最小可复现训练集:四类证据各自带噪,标签由证据组合决定,
# 正好体现 DeepQA 的思想——没有任何单一打分器一锤定音,权重靠学习而非人工调参。
random.seed(42)
TRUE_W = (2.2, 2.2, 2.2, 1.2) # 合成标注用的隐权重(仅为生成可复现训练集)
TRAIN = []
for _ in range(200):
x = [round(random.uniform(0.05, 1.0), 2) for _ in range(4)]
z = sum(t * xi for t, xi in zip(TRUE_W, x)) - 3.6
y = 1 if random.random() < sigmoid(z) else 0 # 概率标注:证据带噪、没有任何特征一锤定音
TRAIN.append((x, y))
w = [0.0] * 5 # 首位是偏置项,零初始化保证训练确定可复现
for _ in range(8000): # 全批量梯度下降
grad = [0.0] * 5
for x, y in TRAIN:
xv = [1.0] + x
err = sigmoid(sum(wi * xi for wi, xi in zip(w, xv))) - y
for i in range(5):
grad[i] += err * xv[i]
for i in range(5):
w[i] -= 0.3 * grad[i] / len(TRAIN)
w = [round(wi, 3) for wi in w]
print("学到的融合偏置与权重 [偏置, 类型强制, 段落重合, 关系证据, 地理约束] =", w)
def confidence(cand):
xv = [1.0] + features(cand)
return round(sigmoid(sum(wi * xi for wi, xi in zip(w, xv))), 4)
# ---- 排序输出:最终产物是带置信度的候选答案排序列表 ----
ranked = sorted(((confidence(c), c, features(c)) for c in candidates), reverse=True)
print("\n候选答案排序(置信度 = 综合全部证据后该答案正确的概率):")
for conf, cand, f in ranked:
print(f" {cand:10s} conf={conf:6.3f} 特征(类型={f[0]}, 段落={f[1]}, 关系={f[2]}, 地理={f[3]})")
# ---- 参赛策略:置信度低于阈值就不抢答(答错扣分),领先时还可动态调高阈值 ----
BUZZ = 0.5
best_conf, best = ranked[0][0], ranked[0][1]
print(f"\n抢答阈值 {BUZZ}:", end="")
print(f"回答 {best}(置信度 {best_conf} 达标)" if best_conf >= BUZZ else "放弃本题")运行后可见:类型错误的 Khmer(语言而非国家)被压到极低置信度,证据缺失的泰国、越南居中,四类证据互相印证的柬埔寨接近 1——单一证据可能误导,证据合流才可靠。
2.9 大规模并行与速度:UIMA-AS 与易并行
3 秒限时下,约 100 候选 × 上百打分器 × 每条证据的检索分析,计算量惊人。DeepQA 从一开始就按”易并行(embarrassingly parallel)“设计(Making Watson Fast,Epstein 等):候选、证据、打分器间几乎无共享状态、天然并行。载体是 UIMA-AS(UIMA Asynchronous Scaleout):把任意 UIMA 应用部署为一组异步进程、经标准消息基础设施通信;问题理解后任务分发(map)到大量 worker 并行做候选生成、证据检索与打分,末端合并(reduce),是典型 map/reduce 式伸缩,延迟靠加并行度压低。
2.10 参赛:游戏策略、接口与三类评测
DeepQA 之外有两个主组件。游戏策略组件:答错扣分意味着不必每题都答,策略层按置信度决定是否抢答、高于阈值才按铃,领先时动态提阈、落后时放宽。接口组件(In the Game: The Interface between Watson and Jeopardy!,Lewis):Watson 既看不见也听不见,不处理视觉听觉线索,只接收电子文本线索、以文本推送答案。
三类评测指标:
- 精度与置信度:QA precision 对 percentage answered 的精度-置信度曲线,数据来自约 200 场盲测、约 12000 道未训练题,对标两位冠军纪录,Precision@70 约 85%。
- 比赛制胜表现:用原版抢答设备打 55 场实时未见对局(计入策略与速度),胜率约 71%。
- 组件性能:鲁棒来自组件数量与多样性、不重度依赖任一组件。先构造只含一个证据打分组件的 WASB(Watson answer-scoring baseline)基线,再逐个加法式叠加组件度量增益;不从完整系统做减法消融(冗余组件会立刻补位),从基线往上加才看得清增益。
2.11 Watson 2.0 愿景与行业落地
Watson 2.0 定位在搜索系统与形式化知识库系统之间:不像搜索只丢文档,也不像形式化系统只会答可形式化的问题;它处理完整问题场景而非单条查询,产出假设性方案并附有力证据、为结论产出证据画像(evidence profiles),并支持 mixed-initiative dialogue(混合主动对话)、可反问澄清、与人协作;IBM 把认知系统演进概括为深蓝解决规划(planning)、Watson 解决理解(understanding)、Project Debater 探索说服(persuade)。
行业落地讲了两个。医疗:医学信息增长已超临床医生消化能力,Watson Discovery Advisor 面向肿瘤医生(Oncologists),把医生成长路径 Study/Apprentice/Practice/Master 对应系统 Ingest/Learn/Test/Experience 四阶段;annotators 阅读海量文献抽取实体关系(课件称 Creating the Knowledge Graph,即从文档建图谱)、再向专家学习扩展,数分钟连接数百数据源(人工数周),讲授口径约四千万篇文档、上亿实体(课程年代口径)。法律:法律业长期缺可量化数据,关键信息靠人工抽取或庞大脆弱的规则集、成本高;机会是自动从法律文档抽取结构化数据、预测案件成本价值结果与隐藏关联(讲授提到 ROSS Intelligence)。
三、知识问答 Demo:从自然语言到 logical form 再到检索
3.1 Demo 定位与四类功能
第二块是开源教学 Demo(elasticsearch-kbqa,OpenKG 收录):在 Elasticsearch 之上做问答——解析自然语言问题,生成 ES 查询并执行。数据是人物属性三元组,对标简化版 Facebook Graph Search,支持四类查询:
| 功能 | 示例问句 | 形态 |
|---|---|---|
| ① 实体检索 → 知识卡片 | 姚明是谁?/ 谁是佟大为? | 给实体,返回全部属性 |
| ② 实体属性检索 | 姚明有多高?/ 姚明是干什么的? | 给实体+属性,返回属性值 |
| ③ 多跳检索 | 姚明的女儿的母亲是谁?/ 她的身高是多少? | 属性链逐跳跳转 |
| ④ 多属性条件检索实体 | 中国女运动员;身高大于170的中国或美国的作家;身高>200、体重小于200的中国篮球运动员 | 条件支持等于与范围,条件间支持 AND/OR |
开源地址 openkg.cn/tool/elasticsearch-kbqa(可用性以 OpenKG 现状为准);本节实操不依赖外部 ES,用 Python 内存索引复现。
3.2 索引设计:一个实体一个文档
最关键的工程决策:属性有十几种,除 height、weight 外都是字符串,课件选择把一个实体的全部属性-值存成一个文档,而非一条三元组一个文档——实体卡片一次取回完整文档、条件检索在单文档内匹配。height、weight 要支持范围检索,存成 integer 独立字段;其余属性种类多、取值开放,存成 keyword 类型放进 nested object(嵌套对象),即文档内一个 (predicate, object) 列表:
{
"subj": "姚明",
"height": 226,
"weight": 141,
"po": [
{"pred": "性别", "obj": "男"},
{"pred": "国籍", "obj": "中国"},
{"pred": "职业", "obj": "篮球运动员"},
{"pred": "女儿", "obj": "姚沁蕾"}
]
}数据准备做三件事:三元组转 JSON 文档;预处理——清理无关字符、身高体重统一单位、“职业”多值切分成多个属性值对;在 ES 建 index/type 与 mapping(声明字段存储类型,相当于 ES schema),经 insert API 批量导入。下文 JSON 与 ES 模板沿用课件字段名 subj,Python 内存复现用 name 承担同一角色。
3.3 属性同义词扩展
数据集小、属性少,Demo 用人工同义词表解决”同一属性多种问法”:每行首词是标准属性名,其余是同义说法(身高→多高、个子;职业→干什么、做什么、工作);命中同义词先映射回标准属性名再构造查询,与第 9 章 paraphrase(释义)思想一致、用字典落地。
3.4 Logical form:四类查询模板
自然语言不直接变成 ES 查询,中间经过 logical form(逻辑形式)——与第 9 章 lambda 表达式、SPARQL 同属一层抽象,这里用极简自定义语法。三种元素:S(实体)、P(predicate,属性)、O(object,属性值);单条件操作符 OP 有 :(等于)与 < > <= >=(如”职业:演员""身高>200”),条件间用逗号、And、Or 连接,对应四套模板:
| 查询类型 | LF 模板 | 示例问句 → logical form |
|---|---|---|
| 实体检索 | S | 姚明是谁 → 姚明 |
| 实体属性检索 | S:P | 姚明有多高 → 姚明:身高 |
| 多跳检索 | S:P1:P2… | 姚明的女儿的母亲是谁 → 姚明:女儿:母亲 |
| 多条件检索实体 | P1 OP O1 And/Or P2 OP O2 … | 身高大于180的中国或美国的作家 → 身高>180 And 国籍:中国 Or 国籍:美国 And 职业:作家 |
注:课件 S:P 示例写存储字段 姚明:height,本讲义统一用中文属性名 身高(映射到 height),多跳、多条件示例课件本身即用中文。
3.5 自然语言解析:识别实体名、属性名、属性值
解析要识别三种成分。分词:Demo 用 jieba 但加载自定义词典,把全部实体名加入分词词典,保证”姚沁蕾""佟大为”不被切散;属性名:属性少而封闭,用字典(含同义词表)直接匹配;实体名:分词结果查 ES,存在以该词为 subj 的文档即为实体,本质是用索引做实体链接;属性值:取值开放无法穷举,用分词后 n-gram 检索 ES 模糊匹配,判定为值后反查它最频繁对应的属性名补全省略(“中国的运动员”中”中国”是值、最常作国籍,补成”国籍:中国”)。
成分识别完做类型判定:有实体且多个属性→多跳;有实体且一个属性,看二者位置与”是/在/的”——实体在前是实体属性查询(姚明的身高),属性在前是依属性查实体(女儿是姚沁蕾的是谁),无实体则依条件查实体;随后按名值相对位置配对:缺省属性名用最频繁属性补全,缺值条件(如”身高>200”)对身高体重类属性用正则识别范围值。
3.6 生成 logical form 与检索查询模板
生成 LF 时按类型套模板填值:“或/或者”→Or,“并且/和”→And,缺省默认 And;同一属性对应两个值(“中国以及美国的作家”)也改 Or。LF 再套检索查询模板变成最终语句,ES 核心模板如下(term 精确匹配、range 范围检索、bool.must/should 对应合取析取):
# ① 按实体名检索实体文档(实体/属性查询的第一步)
{"query": {"bool": {"must": {"term": {"subj": "S1"}}}}}
# ② 等于条件 P1:O1:po 是 nested object,成对 term 必须包进 nested query,
# 约束才落在同一个 (pred,obj) 对象上(课件伪代码省略了 nested 包裹,工程上不可省)
{"query": {"nested": {"path": "po", "query": {"bool": {"must": [
{"term": {"po.pred": "P1"}},
{"term": {"po.obj": "O1"}}]}}}}}
# ③ 范围条件 P1>=O1:数值字段走 range(大于 gt、小于 lt、含等号 gte/lte)
{"query": {"range": {"P1": {"gte": O1}}}}
# ④ 条件合并:And -> bool.must 数组;Or -> bool.should 数组
{"query": {"bool": {"must": [part_query1, part_query2]}}} # And
{"query": {"bool": {"should": [part_query1, part_query2]}}} # Or实体/属性查询先用实体名做 keyword 检索取回文档再取属性值;多跳就是循环调用实体属性查询——上一跳属性值作为下一跳实体名(姚明→女儿→姚沁蕾→母亲→叶莉);多条件检索把每个条件解析成 (pred, op, obj):身高体重走 range,其余走 nested 内 term,再按 And/Or 拼进 must/should。课件 LF 不写括号,其 Or 演示的是同一属性两个取值(“中国或美国”):两个相邻 Or 条件先绑成一个 should 组,等价于 身高>180 And (国籍:中国 Or 国籍:美国) And 职业:作家,其余缺省 And;跨属性任意 Or 嵌套超出课件模板范围,下面按课件范围实现。
# -*- coding: utf-8 -*-
# 基于「搜索引擎索引」思路的 KBQA 最小实现(对应课程 elasticsearch-kbqa Demo 的 Python 等价版)
# 链路:自然语言问句 -> 识别实体名/属性名/属性值 -> 生成 logical form -> 执行(等价于生成 ES 查询并检索)
# 不依赖外部 ES 服务:用内存文档模拟「一个实体一个文档」的索引结构。
import re
# ---- 索引层:一个实体的全部属性存成一个文档(height/weight 为数值字段,其余为 PO 列表,模拟 nested object) ----
DOCS = [
{"name": "姚明", "height": 226, "weight": 141, "po": [("性别", "男"), ("国籍", "中国"), ("职业", "篮球运动员"),
("女儿", "姚沁蕾"), ("星座", "处女座")]},
{"name": "叶莉", "height": 190, "weight": 83, "po": [("性别", "女"), ("国籍", "中国"), ("职业", "篮球运动员"),
("女儿", "姚沁蕾")]},
{"name": "姚沁蕾", "height": 140, "weight": 35, "po": [("性别", "女"), ("国籍", "中国"),
("母亲", "叶莉"), ("父亲", "姚明")]},
{"name": "韩寒", "height": 172, "weight": 62, "po": [("性别", "男"), ("国籍", "中国"), ("职业", "作家"),
("职业", "赛车手")]},
{"name": "海明威", "height": 189, "weight": 90, "po": [("性别", "男"), ("国籍", "美国"), ("职业", "作家")]},
{"name": "佟大为", "height": 180, "weight": 70, "po": [("性别", "男"), ("国籍", "中国"), ("职业", "演员")]},
]
INDEX = {d["name"]: d for d in DOCS}
# ---- 属性同义词表(数据集小、属性少,人工编写;每行首词是数据集属性,其余是同义说法) ----
PRED_SYN = {
"身高": ["身高", "多高", "个子"], "体重": ["体重", "多重", "重量"],
"国籍": ["国籍", "哪国人"], "职业": ["职业", "干什么", "做什么", "工作"],
"女儿": ["女儿"], "母亲": ["母亲", "妈妈"], "父亲": ["父亲", "爸爸"],
"性别": ["性别"], "星座": ["星座"],
}
SYN2PRED = {w: p for p, ws in PRED_SYN.items() for w in ws}
NUM_PRED = {"身高": ("height", "cm"), "体重": ("weight", "kg")} # 数值型属性单独字段,支持范围检索
RANGE_WORD = {"大于": ">", "高于": ">", "超过": ">", "大于等于": ">=", "不低于": ">=",
"小于": "<", "低于": "<", "小于等于": "<=", "不高于": "<=",
">=": ">=", "<=": "<=", ">": ">", "<": "<"}
STOPWORDS = set("谁的是个吗呢啊??。,,有多少一位名")
# 概念词 -> 该概念覆盖的属性值(类型上下位的最简形态;完整上下位扩展在进阶部分展开)
CONCEPTS = {"运动员": ["篮球运动员"], "作家": ["作家"], "演员": ["演员"], "赛车手": ["赛车手"]}
def all_values(): # 索引中全部属性值(模拟 ES 中可被 n-gram 检索的属性值词典)
vals = set()
for d in DOCS:
for _, o in d["po"]:
vals.add(o)
return vals
VALUES = all_values()
def most_freq_pred(value): # 属性值 -> 出现最频繁的属性名(缺省属性名补全,如「中国运动员」缺省「国籍」)
cnt = {}
for d in DOCS:
for p, o in d["po"]:
if o == value:
cnt[p] = cnt.get(p, 0) + 1
return max(cnt, key=cnt.get) if cnt else None
# ---- 自然语言解析:自定义词典最长匹配分词(Demo 中对应 jieba 加载实体名词典) ----
def tokenize(q):
vocab = sorted(set(INDEX) | set(SYN2PRED) | set(RANGE_WORD) | VALUES | set(CONCEPTS),
key=len, reverse=True)
toks, i = [], 0
while i < len(q):
hit = next((w for w in vocab if q.startswith(w, i)), None)
if hit:
toks.append(hit); i += len(hit)
elif re.match(r"[0-9]+(?:\.[0-9]+)?", q[i:]): # 数值(含小数)
m = re.match(r"[0-9]+(?:\.[0-9]+)?", q[i:]); toks.append(m.group()); i += len(m.group())
else:
toks.append(q[i]); i += 1
return [t for t in toks if t and t not in STOPWORDS]
# ---- 解析为:实体、属性链(多跳)、条件列表[(属性, 操作符, 值, 连接词)] ----
def parse(q):
toks = tokenize(q)
entity = next((t for t in toks if t in INDEX), None)
ent_pos = toks.index(entity) if entity else 10**9
preds, conds = [], []
joiner = "And"
i = 0
while i < len(toks):
t = toks[i]
if t == entity:
pass # 实体名本身不作为条件
elif t in ("或", "或者", "还是"):
joiner = "Or"
elif t in ("和", "与", "并且", "而且", "同时"):
joiner = "And"
elif t in SYN2PRED:
pred = SYN2PRED[t]
if i + 1 < len(toks) and toks[i + 1] in RANGE_WORD: # 数值范围条件:身高 大于 170 / 身高>200
op = RANGE_WORD[toks[i + 1]]
num = float(toks[i + 2]); i += 2
conds.append((pred, op, num, joiner)); joiner = "And"
elif i < ent_pos: # 属性名在实体前:依属性查实体(女儿是姚沁蕾)
conds.append((pred, ":", entity, joiner)); joiner = "And"
else: # 实体在前:实体属性链(姚明的身高、多跳)
preds.append(pred)
elif re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", t):
pass # 裸数值已随范围词消费
elif t in CONCEPTS: # 概念词:展开为该类型下的属性值(如「运动员」-> 篮球运动员)
for v in CONCEPTS[t]:
conds.append((most_freq_pred(v), ":", v, joiner)); joiner = "And"
else: # 属性值:精确命中索引值,否则用最频繁属性补全属性名
if t in VALUES:
conds.append((most_freq_pred(t), ":", t, joiner)); joiner = "And"
i += 1
return entity, preds, conds
def fmt_num(x): # 整数不显示小数点
return str(int(x)) if float(x).is_integer() else str(x)
def to_lf(entity, preds, conds): # 按四类模板生成 logical form
if entity and not preds and not conds:
return entity
if entity and preds:
return entity + ":" + ":".join(preds)
parts = []
for p, op, o, j in conds:
body = f"{p}{op}{fmt_num(o)}" if op != ":" else f"{p}:{o}"
parts.append(((j + " ") if parts else "") + body)
return " ".join(parts)
# ---- 执行层:在内存索引上执行 logical form(等价于 ES bool.must / bool.should 模板检索) ----
def get_attr(name, pred): # 实体属性查询;多跳 = 循环调用本函数
d = INDEX[name]
if pred in NUM_PRED:
return d[NUM_PRED[pred][0]]
vals = [o for p, o in d["po"] if p == pred]
return vals[0] if len(vals) == 1 else (vals or None)
def match_cond(d, cond):
p, op, o, _ = cond
if p in NUM_PRED:
v = d[NUM_PRED[p][0]]
return {">": v > o, ">=": v >= o, "<": v < o, "<=": v <= o, ":": v == o}[op]
return o in [ov for pp, ov in d["po"] if pp == p]
def search(conds):
# 同属性、Or 连接的相邻条件(如「国籍:中国 Or 国籍:美国」)组成 should 组;其余进 must(And)
must, should_groups, i = [], [], 0
while i < len(conds):
c = conds[i]
if i + 1 < len(conds) and conds[i + 1][3] == "Or" and conds[i + 1][0] == c[0]:
g = [c]
while i + 1 < len(conds) and conds[i + 1][3] == "Or" and conds[i + 1][0] == c[0]:
g.append(conds[i + 1]); i += 1
should_groups.append(g)
else:
must.append(c)
i += 1
hits = []
for d in DOCS:
if all(match_cond(d, c) for c in must) and \
all(any(match_cond(d, c) for c in g) for g in should_groups):
hits.append(d["name"])
return hits
def execute(entity, preds, conds):
if entity and not preds and not conds: # ① 实体检索 -> 知识卡片
d = INDEX[entity]
return f"知识卡片[{entity}]:身高{d['height']}cm、体重{d['weight']}kg," + "、".join(f"{p}:{o}" for p, o in d["po"])
if entity and preds: # ②③ 实体属性 / 多跳检索
cur = entity
for p in preds:
cur = get_attr(cur, p)
return cur
return search(conds) # ④ 多属性条件布尔组合检索实体
QUESTIONS = ["姚明是谁", "姚明有多高", "姚明的女儿的母亲是谁", "中国女运动员", "身高大于170的中国或美国的作家"]
for q in QUESTIONS:
entity, preds, conds = parse(q)
print(f"问句:{q}\n logical form:{to_lf(entity, preds, conds)}\n 答案:{execute(entity, preds, conds)}\n")对照输出可见五类问句各走对应模板:卡片返回完整属性集,属性查询返回数值,多跳沿”姚明→姚沁蕾→叶莉”循环跳转,多条件把”中国或美国”编译成 should 组、其余进 must;ES 之上再挂推理机做查询扩展与重写(类型展开、逆关系补全)即 4.4、4.6。
四、Demo 进阶:从能跑到好用的九个问题
基础链路只能回答”恰好落在模板里、且用词与库一致”的问题。课件用整套进阶页面讨论九类真实问题,下面按主题归并、用代码复现核心五项。
4.1 歧义:一句多解析
“毕业于浙江大学的学者”里,“浙江大学”本应是”毕业于”这个属性的属性值(而不是一个要单独链接的实体),却可能被切成”浙江”+“大学”,误判成属性值”浙江”加属性名”大学”。课件给三条解法:依上下文排除——错误解析多出一个无值的属性名”大学”,结构不完整可丢弃;用依存句法分析(dependency parsing)与语义角色标注(semantic role labeling)判定修饰关系,知道”浙江”修饰”大学”、整体是一个属性值;多候选供选。
4.2 模板匹配升级与查询 refinement
位置规则太简单,进阶换成依存关系驱动模板匹配,一句命中多模板时都执行再排序选优。另一类是查询 refinement(查询精修/改写):查询无意义、超模板或指代不清时,生成”可执行且改动最小”的查询或多候选供选。典型例子:“姚明的爱好”库里没有”爱好”属性,“姚明的代表作品”属性存在但不属于姚明;系统不应硬返回空,而应检测属性缺失、给出最接近候选。
4.3 别名检索:name 与 alias 都是属性
问”周董有多高”,库里实体名是”周杰伦”。解法是为实体存 alias(别名)属性,检实体时同时查 name 与 alias、所有检实体处一视同仁;建模结论:不以实体名做唯一标识,而以 id 唯一标识、name 与 alias 同为属性,与第 5 章”实体 ID 与指称分离”一致。
4.4 概念检索与类型上下位扩展
为实体存 type 属性后可查”所有篮球运动员”,但用户更常问上位概念”运动员”。类型有上下位:运动员 ⊇ 篮球运动员、足球运动员;解析到父类型时展开为全部子类型分别检索再 OR 合并,工程上为 type 建上下位关系(一张微型本体)、解析时做查询扩展,即推理机查询重写。
4.5 模糊检索与按值类型分存
keyword 不分词、只能精确匹配,用户必须知道库里的确切值;字符串值改成 String(可分词)类型后即可模糊/子串匹配、按相似度返回实体。课件进一步建议属性按值类型分存不同 nested object(integer、Date、string、entity 各就各位):integer/Date 支持 range、entity 值支持多跳与逆关系、string 支持模糊匹配。
4.6 反向检索:PO 与 RSP 双份存储
库里只存了(姚明,女儿,姚沁蕾),用户问”姚沁蕾的父亲是谁”,正向索引答不了。课件方案是存储时对有逆属性的属性双份存储:作为 subject 存 (predicate, object) 的 PO 对之外,再以原客体为反向关系主语存具体反向属性对 RSP——(姚明,女儿,姚沁蕾)补出(姚沁蕾,父亲,姚明);反向属性要具体到父亲/母亲:女儿/儿子的逆按主体性别落”父亲”或”母亲”(姚明补父亲、叶莉补母亲),泛化的”女儿之逆”无法区分父母,补存后反向问题退化成普通正向 S:P 查询;查 SP 时同时发反向 PO 查询(在 RSP 中找含”(父亲,姚沁蕾)“的实体),查 PO 同理,字段命名为 entity 而非 subj。
4.7 属性 path 查询与嵌套
多跳不止”从实体出发”一个方向。“找女儿是白羊座的人”,LF 是”女儿:星座:白羊座”,执行要逆向:先查(星座,白羊座)实体集,再查”女儿”取值落在该集合的实体;条件还可嵌套(“国籍:中国 And 女儿:星座:白羊座”中每个条件可以是简单 PO、PO path 或反向 SP),系统递归拆子查询再合并,与 SPARQL 属性路径同理。
4.8 多元关系拆解与单位统一
“2000 年后在火箭队的篮球运动员”不是二元条件——“在火箭队”与”2000 年后”是同一段经历的两个侧面,课件做法与第 9 章 Freebase 的 CVT(复合值类型)一致:拆成多个二元关系再组合,形如”职业:篮球运动员 And player.member_of:火箭队 And player.join_time:>2000”(“2000 年后”是区间,故 join_time 走 range 比较 >2000,而非等于 2000)。单位问题同理:库存身高以 cm 为单位,用户写”身高>1.9m”,解析时正则识别并换算到存储单位、再生成范围查询。
下面用最小程序复现别名、反向检索、概念上下位、refinement、单位换算。
# -*- coding: utf-8 -*-
# Demo 进阶能力的最小复现:别名(alias)检索、反向检索(PO/RSP 双存)、概念上下位扩展、
# 查询 refinement(库中无此属性时给出改动最小的可执行查询)、单位统一(米->厘米)。
import re
import difflib
# ---- 索引:实体以 id 唯一标识,name 与 alias 同为属性;PO 与 RSP 双份存储支持反向检索 ----
DOCS = [
{"id": "e01", "name": "姚明", "alias": ["小巨人"], "height": 226,
"po": [("性别", "男"), ("国籍", "中国"), ("职业", "篮球运动员"), ("女儿", "e03")],
"type": ["篮球运动员", "运动员"]},
{"id": "e02", "name": "周杰伦", "alias": ["周董"], "height": 175,
"po": [("性别", "男"), ("国籍", "中国"), ("职业", "歌手"), ("星座", "摩羯座")],
"type": ["歌手", "艺人"]},
{"id": "e03", "name": "姚沁蕾", "alias": [], "height": 140,
"po": [("性别", "女"), ("国籍", "中国")], "type": []},
{"id": "e04", "name": "叶莉", "alias": [], "height": 190,
"po": [("性别", "女"), ("国籍", "中国"), ("职业", "篮球运动员"), ("女儿", "e03")],
"type": ["篮球运动员", "运动员"]},
]
BY_ID = {d["id"]: d for d in DOCS}
# 类型上下位:子类型 -> 父类型;查父类型时展开为全部子类型(查询扩展)
TYPE_TREE = {"运动员": ["篮球运动员", "足球运动员"], "艺人": ["歌手", "演员"]}
# 逆属性表:正向 (s, p, o) 存储时,同时存 (o, p^-1, s),对应课件的 PO / RSP 双存。
# 注意「女儿/儿子」的逆要按主体性别落到具体的「父亲/母亲」,不能一律映成父亲。
INVERSE = {"父亲": "子女", "母亲": "子女", "妻子": "丈夫", "丈夫": "妻子"}
def gender(eid): # 取主体性别,用于把「女儿/儿子」的逆具体化为父亲或母亲
return next((v for p, v in BY_ID[eid]["po"] if p == "性别"), None)
def reverse_pred(subj_id, p):
if p in ("女儿", "儿子"):
return "父亲" if gender(subj_id) == "男" else "母亲"
return INVERSE.get(p)
def build_index(docs):
name2id, rsp = {}, {} # name2id: name/alias -> id;rsp: (具体逆属性, 客体id) -> [主体id]
for d in docs:
name2id[d["name"]] = d["id"]
for a in d["alias"]:
name2id[a] = d["id"]
for p, o in d["po"]:
rev = reverse_pred(d["id"], p)
if rev and o in BY_ID:
rsp.setdefault((rev, o), []).append(d["id"]) # 具体反向属性对 RSP
return name2id, rsp
NAME2ID, RSP = build_index(DOCS)
def resolve(name): # 实体链接:name 与 alias 同等对待,最终以 id 唯一标识
return NAME2ID.get(name)
def resolve_name(eid):
return BY_ID[eid]["name"]
def get_forward(eid, pred): # 正向属性查询 SP
d = BY_ID[eid]
if pred == "身高":
return d["height"]
vals = [o for p, o in d["po"] if p == pred]
return [resolve_name(v) if v in BY_ID else v for v in vals]
def get_reverse(eid, pred): # 反向查询:问「姚沁蕾的父亲/母亲」走 RSP(按性别落到具体反向属性)
return [resolve_name(s) for s in RSP.get((pred, eid), [])]
def expand_type(concept): # 概念检索:父类型展开为全部子类型
return TYPE_TREE.get(concept, [concept])
def search_by_type(concept):
types = set(expand_type(concept))
return [d["name"] for d in DOCS if types & set(d["type"])]
# ---- 单位统一:库存身高单位为 cm;用户写「1.9米/1.9m」时正则识别并换算到存储单位 ----
def normalize_height(num, unit):
return num * 100 if unit in ("米", "m", "M") else num
def parse_height(text):
m = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*(米|m|M|cm|厘米)?", text)
return normalize_height(float(m.group(1)), m.group(2) or "cm")
# ---- 查询 refinement:属性不存在时,给出改动最小的可执行候选查询(字符串相似度排序) ----
def refine(pred):
known = sorted({p for d in DOCS for p, _ in d["po"]} | {"身高", "体重"})
if pred in known:
return []
return difflib.get_close_matches(pred, known, n=2, cutoff=0.0) # 改动最小的两个候选属性
def answer(q):
# ① 别名:「周董有多高」
m = re.fullmatch(r"(.+?)有多高", q)
if m:
eid = resolve(m.group(1))
if eid:
return f"{resolve_name(eid)} 的身高:{BY_ID[eid]['height']}cm(别名命中)"
# ② 反向:「姚沁蕾的父亲是谁」
m = re.fullmatch(r"(.+?)的(父亲|母亲|丈夫|妻子)是谁", q)
if m:
eid = resolve(m.group(1))
vals = get_reverse(eid, m.group(2))
return f"{m.group(1)} 的{m.group(2)}:{vals}(反向检索 RSP)"
# ③ 概念检索:「运动员有哪些」
m = re.fullmatch(r"(.+?)有哪些", q)
if m and m.group(1) in TYPE_TREE:
return f"概念「{m.group(1)}」展开为 {expand_type(m.group(1))},命中:{search_by_type(m.group(1))}"
# ④ 范围+单位:「身高超过1.9米的人」
m = re.search(r"身高(?:大于|超过|>)\s*([0-9.]+\s*(?:米|m|cm|厘米)?)", q)
if m:
thr = parse_height(m.group(1))
hits = [d["name"] for d in DOCS if d["height"] > thr]
return f"阈值换算为 {thr:g}cm,身高超过它的人:{hits}"
# ⑤ refinement:「姚明的爱好」
m = re.fullmatch(r"(.+?)的(.+)", q)
if m:
eid, pred = resolve(m.group(1)), m.group(2)
if eid:
cand = refine(pred)
if cand:
sugg = "、".join(f"「{resolve_name(eid)}的{p}」" for p in cand)
return f"库中无属性「{pred}」,refinement 给出改动最小的候选查询:{sugg}"
return "未覆盖"
for q in ["周董有多高", "姚沁蕾的父亲是谁", "姚沁蕾的母亲是谁", "运动员有哪些", "身高超过1.9米的人", "姚明的爱好"]:
print(f"问句:{q}\n -> {answer(q)}\n")4.9 refo-kbqa:直接生成 SPARQL 的另一条路线
第二个开源项目 refo-kbqa(OpenKG 收录):用正则表达式(regular expression for objects)解析问句,直接生成 SPARQL 交 Jena Fuseki 执行。它省掉 ES 这一层,逻辑形式直接对齐图查询语言,天然支持多跳与逆关系路径,代价是要求底层有 SPARQL 服务与 RDF 数据;本示例改用 Python rdflib 的内存 SPARQL 引擎替代 Jena Fuseki 完成等价查询,跑通多跳、逆关系、类型扩展。
# -*- coding: utf-8 -*-
# refo-kbqa 的现代 Python 等价:正则解析得到 logical form 后直接生成 SPARQL,由 rdflib 执行
# (课程原方案把 SPARQL 交给 Jena Fuseki;这里用 rdflib 的内存 SPARQL 引擎完成同样的事)
# 演示三类查询:多跳属性链、逆关系(反向检索)、类型上下位扩展。
from rdflib import Graph, Namespace, RDF, RDFS, Literal
EX = Namespace("http://example.org/kbqa#")
g = Graph()
g.bind("ex", EX)
# ---- 本体层:类型上下位(运动员 包含 篮球运动员) ----
g.add((EX.篮球运动员, RDFS.subClassOf, EX.运动员))
g.add((EX.足球运动员, RDFS.subClassOf, EX.运动员))
# ---- 实例数据:正向只存(姚明 女儿 姚沁蕾)、(叶莉 女儿 姚沁蕾) ----
# 按课件 PO/RSP 双存,入库时补存「方向正确的具体反向三元组」:
# 客体姚沁蕾作主语,(姚沁蕾 父亲 姚明)、(姚沁蕾 母亲 叶莉),RDF 谓词方向为主语->宾语。
triples = [
(EX.姚明, RDF.type, EX.篮球运动员),
(EX.姚明, EX.身高, Literal(226)),
(EX.姚明, EX.国籍, EX.中国),
(EX.姚明, EX.女儿, EX.姚沁蕾),
(EX.叶莉, RDF.type, EX.篮球运动员),
(EX.叶莉, EX.女儿, EX.姚沁蕾),
# RSP:方向正确的具体反向属性对(姚沁蕾 的父亲/母亲 是谁)
(EX.姚沁蕾, EX.父亲, EX.姚明),
(EX.姚沁蕾, EX.母亲, EX.叶莉),
]
for t in triples:
g.add(t)
def run(title, q):
print(f"--- {title} ---")
for row in g.query(q):
print(" ", " | ".join(str(x).split("#")[-1] for x in row))
print()
# ① 多跳:姚明的女儿的母亲是谁? logical form = 姚明:女儿:母亲
run("多跳 S:P1:P2(姚明:女儿:母亲)", """
PREFIX ex: <http://example.org/kbqa#>
SELECT ?x WHERE { ex:姚明 ex:女儿 ?d . ?d ex:母亲 ?x . }""")
# ②a 泛化逆路径:^ex:女儿 反查「所有以姚沁蕾为女儿的人」,会同时返回父亲姚明与母亲叶莉
run("泛化逆路径 ^ex:女儿(反查父母,两人都命中)", """
PREFIX ex: <http://example.org/kbqa#>
SELECT ?x WHERE { ex:姚沁蕾 ^ex:女儿 ?x . }""")
# ②b 具体反向属性(RSP 双存、方向正确):问「姚沁蕾的父亲」就是普通正向 S:P 查询 ex:姚沁蕾 ex:父亲 ?x
run("具体反向属性(姚沁蕾:父亲,RSP 双存,只命中姚明)", """
PREFIX ex: <http://example.org/kbqa#>
SELECT ?x WHERE { ex:姚沁蕾 ex:父亲 ?x . }""")
# ③ 类型上下位扩展:查「运动员」时沿 subClassOf* 展开子类(概念检索的查询扩展)
run("概念扩展(所有运动员,含子类)", """
PREFIX ex: <http://example.org/kbqa#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x WHERE { ?x rdf:type/rdfs:subClassOf* ex:运动员 . }""")四条 SPARQL 对应三个进阶主题:多跳是属性链顺序拼接;逆关系用 SPARQL 1.1 逆路径 ^ 实现,但泛化逆路径 ^ex:女儿 只反查”父母”、同时返回姚明与叶莉,无法精确到父亲——正对应 4.6 结论:反向检索要像 RSP 那样补存方向正确的具体反向三元组(原客体作反向关系主语,如姚沁蕾-父亲-姚明),补存后问”姚沁蕾的父亲”就是普通正向 S:P 查询、只命中姚明;概念扩展用属性路径 rdf:type/rdfs:subClassOf* 先经 rdf:type 落到自身类、再沿 rdfs:subClassOf* 传递展开所有子类(不是只写 subClassOf*,起头的 rdf:type 不可漏)。
📝 动手练一练
练习 1(概念辨析,用自己的话回答)
为什么用 Precision@70 而非单一准确率要求 Watson?它与”答错扣分、可以弃权”是什么关系?
候选答案发现阶段为什么把候选召回率作为首要指标、宁可混入错误候选?
类型强制(type coercion)与 PIQUANT 预枚举类型、各配专用算法的根本区别在哪?
索引设计时为什么”一个实体一个文档”而非”一条三元组一个文档”?height/weight 为何单独建 integer 字段?
keyword 与可分词 string 在属性值检索上各有什么行为?属性为何按值类型分存?
👉 点击查看参考答案
答错扣分、可以弃权,系统的真实目标是”知道自己什么时候知道”——既刻画答得多准(precision),也刻画敢答多少(percentage answered)。Precision@70 对标冠军”答七成、八成五对”的战绩,是精度与覆盖率的联合指标,单一准确率体现不了弃权策略与置信校准。
正确答案若在候选阶段没被召回,后续再精密也救不回来,早期错误不可逆向后传递,因此假设生成优先最大化召回(类比知识融合 blocking),“选最佳、给概率”交给后半段。
PIQUANT 为每类预枚举类型、固化专用分类组件,体系封闭,适应不了高度语境化、近乎开放的 LAT;类型强制不预建分类器,而对每个候选动态提出 is_a(候选, LAT) 假设,用 PRISMATIC、YAGO、WordNet 等多源资源收集证据、输出证据强度作为融合特征。
一个实体一个文档,实体卡片一次取回全部属性、多条件检索在单文档内完成;一三元组一文档会让实体查询退化成大量文档合并;height/weight 要支持 range,必须以 integer 独立字段存储,其余字符串属性放进 keyword/nested object。
keyword 不分词、整体精确匹配,要求用户给出与库中完全一致的值;可分词 string 支持模糊与子串匹配但失去精确约束;按值类型分存各取所长:integer/Date 支持 range、entity 值支持多跳与逆关系、string 支持模糊匹配。
练习 2(动手构造)
不看 3.6,为”身高大于200、体重小于200的中国篮球运动员”写出 logical form,画出对应 ES bool 查询骨架(指明哪些条件进 must、哪些字段走 term/range/nested term),再用第三节引擎验证。
👉 点击查看参考答案
logical form(四条件缺省连接词,全部 And):
身高>200 And 体重<200 And 国籍:中国 And 职业:篮球运动员ES bool 查询骨架(数值字段走 range,字符串属性走 nested 内成对 term,整体合取进 must):
{
"query": {
"bool": {
"must": [
{"range": {"height": {"gt": 200}}},
{"range": {"weight": {"lt": 200}}},
{"nested": {"path": "po", "query": {"bool": {"must": [
{"term": {"po.pred": "国籍"}},
{"term": {"po.obj": "中国"}}]}}}},
{"nested": {"path": "po", "query": {"bool": {"must": [
{"term": {"po.pred": "职业"}},
{"term": {"po.obj": "篮球运动员"}}]}}}}
]
}
}
}用第三节引擎验证:解析得到的 LF 与上面一致,样例数据命中”姚明”,叶莉因身高不足 200 被过滤。
本章小结
开放域问答直接返回精确短答案而非文档列表;Watson 以《Jeopardy!》为擂台,三条硬要求是断网自包含、3 秒限时、对线索与已读内容做深度分析,另有答错扣分规则,指标是对标冠军的 85% Precision@70。
DeepQA 七阶段:问题理解(产出 LAT)→ 证据源建设 → 候选答案发现(多解释多查询、每候选一个独立假设、最大化召回)→ 类型强制(围绕 LAT 动态收集 is_a 证据,资源含 PRISMATIC/YAGO/WordNet)→ 证据搜集与打分(语法特征 + 手工/统计关系抽取)→ 特殊问题(隐式关系、问题分解走专用路径)→ 机器学习融合(约 100 候选 × 约 100 打分器合成单一置信概率)。
两大工件是 DeepQA 架构与 AdaptWatson 方法论;工程上用 UIMA-AS 按易并行模型把假设与打分摊到异步进程;评测分精度-置信度曲线、55 场实时对局(胜率约 71%)、WASB 基线加法增益三类;Watson 2.0 走向证据画像与混合主动对话,行业落地共同模式是”文档→建图谱→证据打分→带依据洞察”。
ES Demo 链路是自然语言 → logical form → 检索查询 → 执行:一个实体一个文档、数值字段独立、其余入 keyword/nested object;四类 LF 模板覆盖实体卡片、属性、多跳、多条件布尔检索;解析靠分词加自定义实体词典、属性字典、属性值 n-gram 与最频繁属性补全,查询靠 term/range/bool.must/should 组合(nested 字段须用 nested query 包裹)。
九项进阶围绕真实可用性:歧义靠上下文/依存句法/多候选消解,超模板查询做 refinement,别名以 id 唯一标识,概念检索靠类型上下位扩展,模糊检索靠可分词 string 与按值类型分存,反向检索靠 PO/RSP 双存(具体反向属性、方向正确),属性 path 支持逆向嵌套,多元关系拆成二元组合(类 CVT)并做单位统一。
两条开源路线:elasticsearch-kbqa 把 LF 编译成 ES 查询,工程成熟、易做模糊检索;refo-kbqa 把 LF 编译成 SPARQL 交 Jena Fuseki 执行,语义严谨、多跳表达力强;Python 生态里前者用内存索引、后者用 rdflib 跑通。
📋 行动清单
- 用自己的话把 DeepQA 七阶段画成流程图,在每个阶段旁标注目标(召回优先还是精度优先)、输入与输出,对照第二节 ASCII 图查漏。
- 跑通第三节的 Python KBQA 引擎,自行扩充 3 个实体与 5 个新问句(至少含一个多跳、一个 Or、一个范围条件),观察 LF 与命中。
- 选一个进阶主题(别名、反向检索或概念上下位)移植进第三节引擎,再用 rdflib 写一条等价 SPARQL 对照两种实现。
—— 小象教研组
领取《小象 11GB VIP 课件资料包与大厂真题手册》
包含全套实战 Jupyter 源码、清洗后数据集、大厂高频面试真题与专属学员答疑交流群。
- ✔完整 Python / 数据分析 Jupyter 实战源码
- ✔大厂真实业务数据集与练习题
- ✔微信扫码添加顾问免费领取;想学什么,直接告诉顾问
微信扫码添加顾问