语义网络把知识拆成「主谓宾」三条一组的断言,再用一张图把它们连起来。 语义网是同一套想法放到整个互联网上。 这一节从 17 条三元组出发,走图查询、跑规则推理、看属性继承, 最后落到两个最难的地方:实体对齐和开放世界假设。
上一节的检索还是「按字面找」,这一节要的是「按关系找」。 问「张三的祖父是谁」,字面检索帮不上忙,得沿着「父亲」这条边跳两跳。 它和 11.2 的共现语义是一对照:那边靠统计猜「谁跟谁像」,这边靠断言说「谁跟谁是什么关系」。 一个是软的、模糊的;一个是硬的、可推理的。
一、三元组:一句话就是一个事实
最朴素的存法:每句话拆成「主体、关系、客体」三条,一条一行。
| 主语 | 关系 | 宾语 |
|---|---|---|
| 张三 | 父亲 | 张一 |
| 张一 | 父亲 | 张老 |
| 宜昌 | 位于 | 湖北 |
| 湖北 | 位于 | 中国 |
| 十七条三元组,十六个节点。 | ||
| 按「主语 + 关系」归类,就得到一张邻接表——这就是图。 | ||
| 它和关系数据库的差别在哪? | ||
| 数据库的列是固定的,加一种新关系就得改表结构; | ||
| 三元组里加一种关系,只是多几行。 | ||
| 代价是没有约束,什么都能往里塞。 |
二、图查询:沿边跳,跳数必须限定
「张三的祖父是谁」拆成两跳:先找父亲,再找父亲的父亲。 一条 1 跳、一条 2 跳,拼起来就是答案:张老。 单跳还好,多跳就麻烦了。从张三出发、不限关系地走:
| 步数 | 这一步新摸到 | 累计 |
|---|---|---|
| 1 | 4 个 | 5 |
| 2 | 6 个 | 11 |
| 3 | 4 个 | 15 |
| 4 | 1 个 | 16 |
| 四步就把全图十六个节点摸遍了——从任意一个点出发,一个不漏。 | ||
| 而这个图只有 17 条边,真实知识图谱是十亿条量级。 | ||
| 所以图查询的第一条纪律是:写清「走哪种边、最多几步」。 | ||
| 不限类型地乱走,三步就铺满半个图,查询根本跑不完。 |
三、推理:没写下的事实能不能自己长出来
加一条规则:如果 x 位于 y、y 位于 z,那么 x 位于 z。 然后反复套,直到再也推不出新东西。
| 轮次 | 新增事实 | 推出来的是 |
|---|---|---|
| 1 | 2 条 | 宜昌 位于 中国、湖北 位于 亚洲 |
| 2 | 1 条 | 宜昌 位于 亚洲 |
| 3 | 0 条 | 不动点,停 |
| 17 条事实推出 3 条新的,最后 20 条。 | ||
| 「宜昌 位于 中国」这一条,原库里根本没有,是算出来的。 | ||
| 注意必须跑到不动点。 | ||
| 只跑一遍会漏:「宜昌 位于 亚洲」要等第二轮才推得出来, | ||
| 因为它依赖第一轮刚生成的「湖北 位于 亚洲」。 | ||
| 这就是「推理会链式触发」的意思,也是它比查询贵得多的原因。 | ||
| 规则一多、图一大,一轮就能生成海量新事实,还可能是错的。 |
四、本体:让属性只写一份
把「类」也写成节点,就能表达层次:动物 → 人 → 学生 → 大学生。 每个类只声明自己新增的属性: 动物需要呼吸、人会说话、学生有学号、大学生一条都没声明。 实例属于某个类,就顺着层次往上把属性全继承下来: 张三(学生)拿到「有学号、会说话、会用工具、需要呼吸」四条。 李四(大学生)拿到的完全一样。 这就是本体的用处:知识只写一份,靠层次传下去。 跟数据库的继承是两回事——数据库的表结构没有「大学生是学生」这种语义。 代价也很实在:继承链一长,改一个上层属性会影响多少下层,很难一眼看清。 这也是大型本体难维护的原因。
五、两个绕不开的难点
第一个:实体对齐。「张三」「张 三」「张先生」「Zhang San」「张三(工程师)」—— 五个字符串指的是同一个人,字面比对只有一个能对上。 这是知识图谱里最贵的工序。 存、查、推理都不难,难的是「把说的都是谁对齐」。 第二个:没说的事算不算假?
| 假设 | 「张三会游泳」怎么判 |
|---|---|
| 封闭世界(数据库) | 没写就是假 |
| 开放世界(语义网) | 没写就是不知道,可能为真 |
| 数据库默认封闭世界,所以「查不到」等于「不存在」,这是 SQL 能高效执行的前提。 | |
| 语义网默认开放世界,所以它永远不敢断言「没有」——这也是语义网喊了二十年却推不动的原因之一。 | |
| 两种假设各有道理,混着用就会出荒谬结论。 | |
| 今天的知识图谱工程,大多是「图的结构 + 数据库的封闭世界」这种折中。 |
六、动手 Demo:把知识拆成一句一句的主谓宾
五段:三元组与邻接表、沿边查询、规则推理到不动点、类层次与属性继承、 实体对齐和两种世界假设。 没有用图数据库,也没有用推理机。 十七个节点、三个 while 循环,够把每一步看清了。
TRI = [(u"张三", u"父亲", u"张一"), (u"张一", u"父亲", u"张老"),
(u"宜昌", u"位于", u"湖北"), (u"湖北", u"位于", u"中国")]
EDGE = {} # 主语 + 关系 -> 宾语列表
for s, p, o in TRI:
EDGE.setdefault((s, p), []).append(o)
# 反复套规则,直到不动点
while True:
new = set()
for s, p, o in KNOWN:
if p != u"位于":
continue
for s2, p2, o2 in KNOWN:
if p2 == u"位于" and s2 == o:
new.add((s, u"位于", o2))
new -= KNOWN
if not new:
break
KNOWN |= new
实跑输出:
--------------------------------------------------------------------
把知识拆成一句一句的主谓宾
--------------------------------------------------------------------
==============================================================
[1] 三元组:一句话就是一个事实
17 条三元组,每条都是「主体、关系、客体」:
主语 关系 宾语
张三 父亲 张一
张一 父亲 张老
张三 居住在 宜昌
宜昌 位于 湖北
湖北 位于 中国
张三 是 学生
……(共 17 条)
按「谁 + 什么关系」归一下,就是一张邻接表:
节点 出边
张三 父亲→张一、居住在→宜昌、是→学生、喜欢→编程
张一 父亲→张老、是→工程师
宜昌 位于→湖北、属于→城市
学生 属于→人
**同一批事实,换一种存法就换一种问法。**
**属性表和三元组能互转,区别只在于「关系」能不能带层次。**
[2] 查「张三的祖父是谁」:沿边跳两跳
单跳只要查表,两跳就要拼:先找父亲,再找父亲的父亲。
张三 --父亲--> 张一
张一 --父亲--> 张老
所以张三的祖父是:张老
跳数一多,能摸到的范围会迅速铺开。下面这张表是「从张三出发、不限关系、只走 k 步」:
步数 k 这一步新摸到 累计节点数 新摸到的是
1 4 5 学生、宜昌、张一、编程
2 6 11 人、城市、工程师、张老、技能、湖北
3 4 15 中国、动物、省、退休工人
4 1 16 亚洲
**四步就把全图 16 个节点摸遍了——从 1 个出发,一个不漏。**
**而这个图只有 17 条边,真实图谱是十亿条量级。**
**真实知识图谱里一个节点有几十种关系,不限定类型地乱走,三步就能铺满半个图。**
**所以图查询必须写清「走哪种边、最多几步」。**
[3] 推理:没写下来的事实,能不能自己推出来
加一条规则:**如果 x 位于 y,且 y 位于 z,那么 x 位于 z。**
然后反复套这条规则,直到再也推不出新东西:
轮次 新增事实 这次推出来的
第 1 轮 2 宜昌 位于 中国、湖北 位于 亚洲
第 2 轮 1 宜昌 位于 亚洲
第 3 轮 0 (不动点,停)
**17 条事实推出 3 条新事实,最后停在 20 条。**
现在问「宜昌在哪个国家」,一条链两跳就答上了:宜昌 → 湖北 → 中国。
这两条事实里,「宜昌 位于 中国」原来并不在库里,是推出来的。
**注意推理是可以链式触发的:第一轮推出的「宜昌 位于 中国」,**
**第二轮又能和「中国 位于 亚洲」接着拼。所以必须跑到不动点,不能只跑一遍。**
[4] 本体:把「类」也写成节点,属性就能继承
类 父类 自己声明的属性
动物 — 需要呼吸
人 动物 会说话、会用工具
学生 人 有学号
大学生 学生 (一条都没声明)
实例属于某个类,就能顺着类层次往上把属性全继承下来:
张三(学生):有学号、会说话、会用工具、需要呼吸
李四(大学生):有学号、会说话、会用工具、需要呼吸
**「大学生」自己一条属性都没声明,可它继承了学生的学号、人的说话、动物的呼吸。**
**这就是本体的用处:知识只写一份,靠层次传下去。**
**代价是——继承链一长,改一个上层属性会影响多少下层,很难一眼看清。**
[5] 落地难在哪:同一件事有好几种写法
五个字符串,指的是同一个人。字面比对的结果:
写法 和「张三」完全一样 字数
张三 是 2
张 三 否 3
张先生 否 3
Zhang San 否 9
张三(工程师) 否 7
**五个里只有一个能对上。**这就是实体消解问题——
**图谱里最贵的工序不是存,是把「说的都是谁」对齐。**
还有一条更根本的分歧:**没写下来的事实,算「不知道」还是算「假」?**
假设 「张三会游泳」怎么判
封闭世界(数据库) 没写就是假
开放世界(语义网) 没写就是不知道,可能为真
**数据库默认封闭世界,所以「查不到」等于「不存在」。**
**语义网默认开放世界,所以它永远不敢说「没有」——这也是它推不动的原因之一。**
两种假设各有各的道理,混着用就会出荒谬的结论。
五段连起来:**三元组把知识拆成一句一句的主谓宾**;
**图查询靠沿边走,跳数必须限定**;
**规则能把没写下的事实推出来,但要跑到不动点**;
**本体靠层次让属性只写一份**;
**而真正贵的是「对齐」——同一个东西有几个名字,几套假设。**
第 1 段那张邻接表就是图。 「张三」有三条出边,「学生」只有一条——出边条数就是这个词在知识里的角色丰富程度。 第 2 段表格最右一列(累计)比中间那列更值得看: 第三、四步新摸到的越来越少,说明这个图很小、已经快摸完了。 真实图谱的半径是五六跳,而且每跳都成千上万。 第 3 段那个「第二轮才推出宜昌位于亚洲」是关键。 它说明推理顺序会影响结果——只跑一遍和跑到不动点,答案不一样。 第 4 段「李四」和「张三」的属性完全一样,不是巧合: 大学生自己没声明属性,它拿到的每一条都是继承来的。 第 5 段那张两行的表,是这一节真正的结论。 封闭世界和开放世界不是技术细节,是两种看世界的态度。
30 秒实验
- 往第 1 段加一条「张老 是 退休工人」,重跑第 2 段。 四步就摸不到全图了——多一个节点,半径就得再加一跳。
- 把第 3 段的规则从「位于」换成「属于」,重跑。 能推出「张三 属于 动物」这类新事实—— 同一个引擎,换个关系名就是另一套推理,这就是规则和数据结构分离的好处。
- 在第 5 段加一个「张三 会 游泳」的三元组,重跑。 封闭世界下它从「假」变成「真」—— 一条事实就能翻转结论,这是开放世界的麻烦,也是它的老实。
常见误区
- 以为「知识图谱就是一张大图」。图只是存法。 真正让它是「知识」的,是关系的语义和能推理的规则。
- 以为「三元组能表达一切」。「张三每个月工资从 8000 涨到 9000」这种带时间、带变化的事实, 朴素三元组表达不了,得加时间戳或者把事件本身变成节点。
- 以为「推理越多越好」。规则一多,推出来的废话和错话也成倍增长。 真实系统要限制推理深度,宁可少推,不可乱推。
- 以为「语义网失败是因为技术不行」。它的核心假设是「开放世界加全球统一本体」。
- 这个假设和工程现实冲突太大。今天的知识图谱走的是另一条路:小范围、封闭、够用就行。
自测三问(点开对答案)
1. 为什么「张三的祖父」要跳两跳? —— 因为**知识里只写了「谁的父亲是谁」,没写「谁的祖父是谁」**。 **祖父这个关系是两跳拼出来的:先找人,再找那个人的父亲。** **跳数越多,路径数量涨得越快,所以查询必须限定步数。** 2. 为什么推理要跑到不动点? —— 因为**新推出的事实可能又能和别的规则拼**。**Demo 第 3 段:「宜昌 位于 亚洲」要等第二轮,** **因为它要用第一轮刚生成的「湖北 位于 亚洲」。** **只跑一遍就会漏掉这类链式结论。** 3. 封闭世界和开放世界的差别在哪? —— **只在于「没写下来的事实」怎么处理。** **数据库说「没写就是假」(封闭),语义网说「没写就是不知道」(开放)。** **前者能高效查询,后者更老实但永远不敢下结论。**第 12 章到这里结束。 下一章换到机器那一侧:这些计算到底跑在什么机器上,一台不够时怎么拼成一群。 分布式、并行、硬件配置、软件配置,四节把「算力」这件事讲清楚。
- ☐ (本章完,下一节 13.1 分布式计算和并行计算)