为什么数据库里“没有”就是没有,知识图谱里却不是?深入理解 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

posted @ 2026-09-24 15:30  JavaPub  阅读(5)  评论(0)    收藏  举报