为什么数据库里“没有”就是没有,知识图谱里却不是?深入理解 Owlready2 的 `close_world()`
最近在研究 Python 里的知识图谱工具 Owlready2。
项目地址:
https://github.com/pwin/owlready2
前面如果只是看看 Owlready2 的 API,你可能会觉得它就是:
Python 类
+
RDF
+
OWL
+
SPARQL

换了一套方式存数据而已。
但真正开始使用 OWL 推理之后,很快就会遇到一个非常反直觉的问题:
我明明没有给这个对象设置某个属性,为什么推理器就是不肯认为它“没有这个属性”?
比如有一个药:
drug.active_principles = []
它没有任何有效成分。
我们定义:
没有有效成分的药 = Placebo
按照普通程序员的思维:
if len(drug.active_principles) == 0:
print("这是安慰剂")
结束。
但是到了 OWL 世界里,事情完全不是这么简单。
这里就涉及知识图谱领域一个非常重要的概念:
Open World Assumption
也就是:
开放世界假设。
一、先看数据库程序员最熟悉的世界
假设数据库有张表:
CREATE TABLE drug_active_principle (
drug_id BIGINT,
principle_id BIGINT
);
查询某个药物:
SELECT *
FROM drug_active_principle
WHERE drug_id = 1001;
结果:
0 rows
绝大多数业务系统会直接理解成:
drug 1001 没有 active principle
于是可以写:
SELECT d.*
FROM drug d
WHERE NOT EXISTS (
SELECT 1
FROM drug_active_principle p
WHERE p.drug_id = d.id
);
这种逻辑实际上隐含了一个假设:
数据库里没有记录,就认为这个事实不存在。
这种思维非常接近:
Closed World Assumption
关闭世界假设
也就是:
不知道 = 假
但 OWL 不是这样工作的。
二、OWL 认为“不知道”不等于“不存在”
假设我们知道:
张三养了一只猫
知识图谱中可能是:
张三 -> hasPet -> 小白
现在问:
张三有没有养狗?
数据库程序员可能查询:
没有 hasDog 记录
于是回答:
没有。
但 OWL 推理器的回答其实更像:
我不知道。
因为当前知识库中没有记录:
张三养狗
并不能证明:
张三不养狗
也许只是:
这个事实还没有录入。
因此:
没有记录
和:
明确不存在
在 OWL 中是两件完全不同的事情。
可以简单理解成:
数据库:
没查到
↓
不存在
OWL:
没查到
↓
不知道
这就是开放世界假设。
三、用 Owlready2 做一个药品知识图谱
来看一个具体例子。
安装:
pip install owlready2
代码:
from owlready2 import *
onto = get_ontology(
"http://example.org/drug.owl"
)
with onto:
class Drug(Thing):
pass
class ActivePrinciple(Thing):
pass
class has_active_principle(
Drug >> ActivePrinciple
):
pass
这里定义三个东西:
Drug
ActivePrinciple
has_active_principle
关系是:
Drug
|
| has_active_principle
v
ActivePrinciple
创建两个有效成分:
aspirin = ActivePrinciple("aspirin")
caffeine = ActivePrinciple("caffeine")
再创建几个药:
drug_a = Drug("drug_a")
drug_a.has_active_principle = [
aspirin
]
drug_b = Drug("drug_b")
drug_b.has_active_principle = [
aspirin,
caffeine
]
drug_c = Drug("drug_c")
drug_c.has_active_principle = []
看起来:
drug_a
└─ aspirin
drug_b
├─ aspirin
└─ caffeine
drug_c
└─ 什么都没有
接下来我们希望让 AI 自动判断药物类型。
四、OWL 最强大的地方:类可以由“规则”定义
普通 Python 可能这样写:
if len(drug.has_active_principle) == 1:
category = "SingleDrug"
elif len(drug.has_active_principle) >= 2:
category = "CompoundDrug"
elif len(drug.has_active_principle) == 0:
category = "Placebo"
但 OWL 可以直接把规则写进 Ontology。
例如:
with onto:
class SingleActivePrincipleDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.exactly(
1,
ActivePrinciple
)
]
它表达的不是普通继承:
SingleActivePrincipleDrug
继承 Drug
而是在说:
SingleActivePrincipleDrug
等价于
Drug
AND
恰好拥有 1 个 ActivePrinciple
逻辑表达式:
Drug
∩
( has_active_principle exactly 1 ActivePrinciple )
再定义复方药:
with onto:
class CompoundDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.min(
2,
ActivePrinciple
)
]
意思:
有效成分 >= 2
就属于:
CompoundDrug
到这里都很好理解。
真正有意思的是:
安慰剂怎么定义?
五、定义“没有有效成分”的药
可以写:
with onto:
class Placebo(Drug):
equivalent_to = [
Drug
&
Not(
has_active_principle.some(
ActivePrinciple
)
)
]
这里:
has_active_principle.some(
ActivePrinciple
)
表示:
至少存在一个 ActivePrinciple
类似逻辑:
∃ has_active_principle.ActivePrinciple
加上:
Not(...)
于是:
Not(
has_active_principle.some(
ActivePrinciple
)
)
表达:
不存在任何 ActivePrinciple
所以:
Placebo
=
Drug
AND
不存在 active principle
逻辑上很完美。
那么现在运行:
sync_reasoner()
是不是:
drug_c.__class__
就会自动变成:
Placebo
不一定。
问题就在这里。
六、为什么 [] 不能证明“没有”?
我们写了:
drug_c.has_active_principle = []
人类看到以后觉得:
它没有有效成分。
但 OWL 推理器的理解是:
当前知识库中,
我不知道 drug_c 有什么 active principle。
注意:
这两个结论完全不同。
第一种:
不存在 ActivePrinciple
第二种:
目前不知道 ActivePrinciple
OWL 默认采用第二种。
假设以后另一个系统又导入了一条知识:
drug_c
has_active_principle
aspirin
这在开放知识库中完全可能发生。
所以推理器不会因为现在:
查询结果为空
就武断地认为:
永远不存在。
七、这时候 close_world() 就出现了
Owlready2 提供了一个很重要的方法:
close_world()
例如:
close_world(Drug)
简单理解:
告诉推理器:关于 Drug 当前已知的信息,现在可以认为是完整的。
也就是说,从:
我现在不知道 drug_c 有有效成分
变成:
我确认 drug_c 已知的有效成分就是这些,没有其他的了。
于是:
close_world(Drug)
sync_reasoner()
之后推理器才能进一步得到:
drug_c
没有 ActivePrinciple
然后满足:
Not(
has_active_principle.some(
ActivePrinciple
)
)
最终:
drug_c
↓
Placebo
这一步非常关键。
八、完整代码
把前面的例子组合起来:
from owlready2 import *
onto = get_ontology(
"http://example.org/drug.owl"
)
with onto:
class Drug(Thing):
pass
class ActivePrinciple(Thing):
pass
class has_active_principle(
Drug >> ActivePrinciple
):
pass
class SingleActivePrincipleDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.exactly(
1,
ActivePrinciple
)
]
class CompoundDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.min(
2,
ActivePrinciple
)
]
class Placebo(Drug):
equivalent_to = [
Drug
&
Not(
has_active_principle.some(
ActivePrinciple
)
)
]
aspirin = ActivePrinciple(
"aspirin"
)
caffeine = ActivePrinciple(
"caffeine"
)
drug_a = Drug("drug_a")
drug_a.has_active_principle = [
aspirin
]
drug_b = Drug("drug_b")
drug_b.has_active_principle = [
aspirin,
caffeine
]
drug_c = Drug("drug_c")
drug_c.has_active_principle = []
AllDifferent([
aspirin,
caffeine
])
close_world(Drug)
sync_reasoner()
print(
drug_a.__class__
)
print(
drug_b.__class__
)
print(
drug_c.__class__
)
理论上推理结果对应:
drug_a
→ SingleActivePrincipleDrug
drug_b
→ CompoundDrug
drug_c
→ Placebo
这里发生的事情已经不是:
if else
而是:
事实
+
Ontology
+
Description Logic
+
Reasoner
↓
新知识
九、some() 到底是什么意思?
Owlready2 中非常值得理解的几个方法包括:
some()
only()
exactly()
min()
max()
它们背后其实对应 OWL / Description Logic。
例如:
has_active_principle.some(
ActivePrinciple
)
意味着:
至少存在一个关系
has_active_principle
它指向一个
ActivePrinciple
数学上可以理解为:
∃ has_active_principle.ActivePrinciple
例如:
某个药
|
├── has_active_principle
|
aspirin
就满足:
has_active_principle.some(
ActivePrinciple
)
十、only() 和 some() 完全不是一回事
一个很容易踩坑的地方是:
has_active_principle.only(
ActivePrinciple
)
很多人第一次会理解成:
这个药必须存在 ActivePrinciple
实际上它表达的是:
如果存在 has_active_principle,那么目标只能属于 ActivePrinciple。
它更接近:
∀ has_active_principle.ActivePrinciple
注意一个非常反直觉的地方。
一个完全没有:
has_active_principle
关系的对象,也可能满足:
has_active_principle.only(
ActivePrinciple
)
因为不存在反例。
所以如果真正想表达:
至少有一个 ActivePrinciple
并且
所有 active principle 都是 ActivePrinciple
通常需要组合:
has_active_principle.some(
ActivePrinciple
)
&
has_active_principle.only(
ActivePrinciple
)
也就是:
some
+
only
十一、exactly() 是基数约束
例如:
has_active_principle.exactly(
1,
ActivePrinciple
)
表示:
恰好一个
对应:
= 1
所以:
class SingleActivePrincipleDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.exactly(
1,
ActivePrinciple
)
]
就形成:
有效成分 = 1
↓
单一有效成分药
十二、min() 可以构造更复杂的自动分类
例如:
has_active_principle.min(
2,
ActivePrinciple
)
表示:
至少两个
于是:
class CompoundDrug(Drug):
equivalent_to = [
Drug
&
has_active_principle.min(
2,
ActivePrinciple
)
]
相当于声明:
Drug
+
ActivePrinciple >= 2
→ CompoundDrug
以后哪怕新建:
drug_x = Drug("drug_x")
drug_x.has_active_principle = [
a,
b,
c,
d
]
只需要重新运行推理:
sync_reasoner()
它就可以自动被归类。
这也是 Ontology 和传统数据库最大的区别之一。
数据库一般负责:
存事实
Ontology 还可以描述:
事实之间意味着什么
十三、为什么一定要理解 AllDifferent()?
再来看:
AllDifferent([
aspirin,
caffeine
])
为什么官方示例里会出现这种代码?
因为 OWL 中还有另一个非常重要的哲学:
两个名字不同
≠
一定是两个不同对象
比如:
aspirin
acetylsalicylic_acid
可能其实是同一个东西。
知识图谱经常存在:
不同名称
同一实体
因此 OWL 默认不会仅仅根据名字不同,就把两个 Individual 永远视为不同。
如果你的推理依赖:
数量
比如:
min(
2,
ActivePrinciple
)
那么实体是否明确互不相同就会变得重要。
所以可以声明:
AllDifferent([
aspirin,
caffeine,
ibuprofen
])
明确告诉推理器:
aspirin ≠ caffeine
aspirin ≠ ibuprofen
caffeine ≠ ibuprofen
然后基数推理会更加明确。
十四、这也是知识图谱和数据库一个巨大的思维差异
传统数据库强调:
Schema
Table
Column
Row
Foreign Key
比如:
drug
和:
active_principle
之间建立关系。
程序负责:
if count == 0:
placebo()
elif count == 1:
single()
elif count >= 2:
compound()
知识图谱换了一种方式。
我们声明:
什么叫 Placebo
什么叫 SingleActivePrincipleDrug
什么叫 CompoundDrug
然后把事实交给推理器。
于是程序结构变成:
事实
\
\
Ontology
/
规则 /
↓
Reasoner
↓
新事实
真正有意思的是:
规则并不一定写在业务代码的 if...else 里。
它们可以成为知识模型本身的一部分。
十五、为什么 AI 时代这个能力反而更重要?
到了大模型时代,这种机制其实更加有价值。
因为 LLM 擅长的是:
从非结构化内容提取事实
比如让大模型阅读:
病历
PDF
论文
合同
产品说明
网页
然后抽取:
Entity
Property
Relation
例如:
Patient001
├─ has_symptom → Fever
├─ has_symptom → Cough
└─ age → 71
但大模型生成事实之后,我们还有一个问题:
如何得到稳定、可解释、可重复的推理结果?
如果所有判断继续问 LLM:
这个人是不是高风险患者?
模型每次实际上都需要重新推理。
而 Ontology 可以预先定义:
Age >= 65
AND
hasSymptom some RespiratorySymptom
属于:
HighRiskPatient
最后形成:
LLM
↓
抽取事实
Ontology
↓
约束知识
Reasoner
↓
逻辑推理
这其实就是现在很多:
Knowledge Graph
GraphRAG
Ontology
Agent
系统继续往前发展的一个重要方向。
十六、close_world() 什么时候应该用?
这里尤其需要注意:
不要看到 close_world() 就全局乱用。
因为它本质上改变了:
未知事实
的处理方式。
比较适合的场景是:
某个局部领域的数据已经明确完整
例如:
某个订单当前有哪些商品
某张处方有哪些药
某个课程有哪些章节
某个产品有哪些组成部件
某台服务器有哪些已安装组件
假设订单已经完整加载:
Order001
├── ProductA
├── ProductB
└── ProductC
这时可以合理认为:
Order001 当前商品集合已经完整。
但是如果是:
张三认识哪些人?
就很危险。
数据库里只存了:
张三认识李四
并不意味着:
张三只认识李四。
如果贸然关闭世界,就可能得到错误结论。
所以应该理解为:
close_world()
不是:
让 OWL 更聪明
而是在告诉 OWL:
这个范围的数据我保证已经收集完整。
十七、最后再理解 Owlready2 官方示例,就完全不一样了
Owlready2 官方推理文档中有一个药品 Ontology 示例。
它定义:
class Placebo(Drug):
equivalent_to = [
Drug
&
Not(
has_for_active_principle.some(
ActivePrinciple
)
)
]
又创建:
drug3 = Drug(
active_principles=[]
)
然后有一个特别关键的步骤:
close_world(Drug)
最后:
sync_reasoner()
推理器才可以得到:
drug3
↓
Placebo
如果不知道 Open World Assumption,你可能会觉得:
close_world(Drug)
只是一个普通 API。
实际上这一行背后涉及的是整个语义 Web、OWL 和知识表示领域非常核心的一个哲学问题:
“没有证据证明是真的”,和“已经证明它是假的”,到底是不是一回事?
传统业务数据库中,我们经常把二者混在一起。
而 OWL 明确把它们区分开了。
写在最后
这也是我觉得 Owlready2 最值得研究的地方。
如果只是:
Person("zhangsan")
或者:
person.age = 18
其实没有体现出知识图谱真正的价值。
Owlready2 真正有意思的是:
Python
+
OWL Description Logic
+
Ontology
+
HermiT / Pellet
+
自动推理
尤其当你真正理解:
some
only
min
max
exactly
Not
equivalent_to
AllDifferent
close_world
以后,就会发现:
Ontology 不是换了一种格式存数据,而是在给数据增加“逻辑”。
传统数据库回答的是:
我存了什么?
而 Ontology + Reasoner 开始回答:
根据我已经知道的这些事实,
还能推出什么?
这才是 Owlready2 最值得深入研究的部分。
项目:
https://github.com/pwin/owlready2
官方源码中的 reasoning 文档:
https://github.com/pwin/owlready2/blob/master/doc/reasoning.rst

浙公网安备 33010602011771号