每遇到一个新词,都要回答:盒子?东西?贴纸?
📌 本部分为中文精读版作者的原创分析,原书没有对应章节,因此不提供英文对照。
一句话:每遇到一个新名词,你都要回答一个三选一的问题——“它是个盒子、是盒子里的一件东西、还是贴在盒子上的一个标签?” 这三个身份分别是类、实例、属性值。
先说生活直觉。
假设你在整理家里的储物间,你要给每样东西贴标签。
这就是本体的全部动作:分盒子、放东西、贴标签。 而这本书里 4.4、4.5、4.6 三节反复纠缠的,就是怎么判断一个新词该分到哪一类。
正式定义:
| 身份 | 定义 | 中文对照 |
|---|---|---|
| 类(class) | 描述领域中的一个概念;它的外延是一批对象 | 酒、红酒、博若莱 |
| 实例(instance) | 某个类的一个具体成员 | 你手里那杯 2015 年的拉菲 |
| 属性值(slot value) | 某个属性在该对象上的取值 | 颜色=红 |
本书第 2 章给的定义是“类描述领域中的概念”“具体的酒是这个类的实例”;4.6 节给了一条判据:
个体实例是知识库里所表示的最具体的概念。
最容易被搞混的一对是“类”和“属性值”——因为同一个区分,两种建法都说得通。
现场实测:你想表达“有的酒是红的,有的是白的”。怎么建?
| 方案 | 建法 | 后果 |
|---|---|---|
| A(建类) | 建 RedWine、WhiteWine 两个类 | 它们能参与层次、能被别的属性当约束、能被继承 |
| B(建属性值) | 只建 Wine 类,加一个 color 属性,值填 RED / WHITE | 简洁;但 RED 只是一个字面量,没有名字之外的任何能力 |
质的不同,体现在两边的能力清单上:
| 能力 | 类 | 属性值 |
|---|---|---|
| 能挂自己的属性吗? | ✅ RedWine 可以挂 tanninLevel | ❌ 无 |
| 能被继承吗? | ✅ Pauillac 承到 RedWine 的属性 | ❌ 无 |
| 能作为别的类的属性的约束(range)吗? | ✅ “pairingGoodFor 的 range 是 RedWine” | ❌ 无法表达“配红酒” |
| 能参与互斥声明吗? | ✅ 声明 RedWine ⊥ WhiteWine,机器能查错 | ❌ 机器不知道 RED 和 WHITE 互斥 |
| 能参与多重继承吗? | ✅ Port ⊂ RedWine ∩ DessertWine | ❌ 无法表达 |
| 适用场景 | 这个区分会影响别的建模决策时 | 这个区分只是一个描述性标签时 |
| 运作逻辑 | 参与逻辑推理:从 X ∈ PinotNoir 推出 X ∈ RedWine | 只参与查询过滤:WHERE color = 'RED' |
换句话说,两者的分水岭是:这个区分会不会“活起来”。
痛点的名字叫:建模的粒度失控。 两个方向都会翻车。
场景:你想在餐厅酒瓶本体里建一个 ChilledWine(冰镇酒)类。
会怎样:一位顾客点了一瓶酒,服务生从冰柜里拿出来——这瓶酒立刻变成 ChilledWine 的实例。喝了一会儿,酒温了——它又变成 WarmWine 的实例。下一桌客人又点它,再冰一次——又变回来。
具体后果:
原文的判据(4.5 节):一个个体实例所属的类,不应该经常变。通常,当我们用外在属性而不是内在属性来区分类时,这些类的实例就会频繁地在类之间迁移。
场景:你图省事,把颜色做成属性值:Wine.color ∈ {RED, WHITE},不建 RedWine/WhiteWine 类。
会怎样:后来有人录了一条数据,颜色填了 RED 又填了 WHITE,或者某个类同时被声明为这两种。
具体后果:机器一声不吭。 它不知道 RED 和 WHITE 不能共存——因为“互斥”(4.8 节)这个声明只能声明在类之间,属性值没有互斥机制。于是这个错误一路潜伏到报表里。
场景:你为每一个区分都建类:DelicateWine、ModerateWine、StrongWine;FullBodyWine、MediumBodyWine、LightBodyWine……
会怎样:颜色 3 种 × 酒体 3 种 × 风味 3 种 × 糖分 3 种 = 81 个类(还没算葡萄品种)。真实的酒类组合还会更多。
具体后果:
Red wine、White wine、Rose wine 这些类,是因为这个区分在酒的世界里是自然存在的。我们没有为“清淡的酒”“中等酒体的酒”等等建类。场景:你把 Bourgogne region 建成实例,然后又想表达 Cotes d'Or region 是一个 Bourgogne region。
会怎样:做不到。 原文 4.6 节说得很干脆:只有类才能被组织成层次——知识表示系统里没有“子实例”这个概念。
具体后果:你被迫做一堆随意的判断——“哪几个产区算类、哪几个算实例”,而这个判断本来就没有客观标准。原文的解法是把所有产区统统升成类,然后把它们标成 Abstract(抽象类,不能有直接实例)。
「本店冰镇酒周转率」取决于统计时刻,而不是事实。
颜色做成属性值 → 红与白的互斥无处声明。
颜色×酒体×风味×糖分 = 81 个类。推理机算不动。
把产区建成实例,就再也无法表达「金丘是勃艮第的一种」。
它是一道分诊台,卡在第 3 步和第 4/5/7 步之间:
第3步 列出术语清单(不做分类)
↓
┌─────────────────┐
│ ★ 三分判定台 ★ │ ← 你在这里
└─────────────────┘
↓ ↓ ↓
类 属性 (暂时搁置)
↓ ↓
第4步 建类层次 第5步 定义属性
↓
第7步 建实例 ←—— 从这里往下,"最具体的概念"变成实例
注意:它是一个“分诊台”,不是“一次性的关卡”。 每加一个新词,都要过一次。
| 触发情形 | 原文出处 |
|---|---|
| 每往本体里加一个新名词时 | 第 3 步→第 4 步的转换 |
| 当发现某个类的实例在频繁地换类 | 4.5 节“迁移率”判据 |
| 当某个类只有 1 个直接子类时 | 4.2 节(见报告一 9.3 的 Bordeaux 案例) |
| 当某个区分需要成为别的属性的约束时 | 4.5 节判据一 |
| 当领域专家习惯上会做这个区分时 | 4.4 节末(哪怕你的应用用不上) |
| 当要判断“本体的最细粒度在哪”时 | 4.6 节“回到能力问题” |
禁用条件一:领域范围还没定的时候,不要用它。
原文 4.5 节说得很清楚:答案通常取决于我们为本体定义的范围。—— “白葡萄酒”这个概念在我们的领域里有多重要? 同一个词,在“印酒标的工厂”和“做酒菜搭配”两个领域里,答案相反。所以:第 1 步没做完,这道判定就没有输入,得出的结论是假的。 强行判定,等于凭空猜。
禁用条件二:不要用它来回答“要不要建更多的类”。
那是 4.4 节的活,用的是另一套判据(“子类有没有新属性/新约束/新关系”)。三分判定回答的是“它是什么身份”,不是“该不该有它”。 两者混用会出这种错:判定某个术语是“属性值”,就以为“不用建这个类了”——但 4.4 节明确说,即使不引入新属性,参照层次和专家共识这两种情况仍然应该建类。
禁用条件三:不要用它来决定“命名”。
“单数还是复数”(6.2 节)、“用空格还是驼峰”(6.1 节)是命名问题。把命名问题误当身份问题,会得出“用复数就说明它是类”这种谬论——原文 6.2 节明确说两种都行,只要一致。
禁用条件四:不要用“这个区分在现实世界里为不为真”作为唯一判据。
这是最反直觉的一条,也是 4.7 节 Experimenter 案例的教训:
Experimenter 是 BiologicalOrganism 的子类——这句话在现实中为真。但不该放进本体,因为它会给所有实验者实例塞进体重、年龄、物种这些无关字段。
判据不是“真不真”,而是“对我的应用有没有用、会不会有害”。
| 输入 | 说明 | 来源 |
|---|---|---|
| 候选术语 t | 待判定的那个词(如“白葡萄酒”、“冰镇”、“2015 年”) | 第 3 步术语清单 |
| 领域范围 S | 这个本体覆盖什么、答什么问题 | 第 1 步 |
| 能力问题 Q | 本体的考卷 | 第 1 步 |
| 专家的直觉 | “我们平时会把这两个看成不同的东西吗” | 领域专家 |
注意第三个输入是硬要求。 原文 4.5 节说答案取决于范围——没有范围,判断不成立。
三道筛子的因果逻辑:
Sterling Vineyards Merlot 在“酒菜搭配”应用里是实例;但如果你要记录每瓶酒的库存,它就得变成类(它的每个年份/每瓶是实例)。如果带着不同属性值的那些概念,会成为其他类中不同属性的限制条件,那我们应当为这个区分建一个新类。
如果一个区分在领域里很重要,并且我们会把在该区分上取不同值的对象看作是不同种类的对象,那我们就应当为这个区分建一个新类。
一个判定结论,外加一个理由记录:
理由记录不是可选项。 原文 4.7 节明确要求:
我们应该把这类设计决策记录在本体的文档里,以造福那些将查看这个本体、却可能不了解我们当初设想的是什么应用的用户。否则,打算把这个本体复用到其他应用的人,可能会试图把experimenter当作person的子类使用,而不知道原始建模里并没有包含这个事实。
这一节要分两边算账。
得到:可继承(子类自动拿到属性);可被别的属性当约束(pairingGoodFor 的 range 可以写 RedWine);可参与互斥声明 → 机器能替你查错(4.8 节);可参与多重继承(4.3 节 Port 的例)。
牺牲:类数量指数膨胀(颜色 × 酒体 × 风味 × 糖分 = 乘法增长);推理成本上升(类数量直接决定推理搜索空间);维护成本上升(加一个维度,所有组合都要重算)。
在什么场景下会真的疼:
当那个区分是“正交维度”的时候。
“颜色”和“酒体”是两个独立的维度——一款酒可以是“红 + 饱满”,也可以是“红 + 清淡”。如果你为每个组合建类,你就把两个维度的笛卡尔积硬编码成了类列表。
原文 4.4 节末踩的正是这个坑:我们没有为“清淡的酒”“中等酒体的酒”建类。
得到:本体扁平、简洁;新增维度零成本(加一个枚举值即可);查询过滤简单直接。
牺牲:完全失去推理性。“红酒有单宁含量”这句话无处安放——你没法在 Wine 上挂 tanninLevel(白酒没有),也没法挂在一个不存在的 RedWine 类上。失去校验能力。机器不知道 RED 和 WHITE 互斥。
在什么场景下会真的疼,而且疼得最厉害:
当你需要“某个区分才有的字段”时——路线 B 直接失灵,没有替代方案。
这就是原文 4.5 节里Red Merlot/White Merlot那个例子:红梅洛和白梅洛确实是不同的酒(用同一种葡萄酿成),如果我们建的是一个细致的酒本体,这个区分就很重要。
量化这个“疼”:
把 tanninLevel 挂在 Wine 上:10 款酒全部被授予该属性,但其中只有 6 款是红酒(真该有)→ Precision = 6/10 = 0.6。
建 RedWine 类再挂:6 款全部命中 → Precision = 6/6 = 1.0。
精确度从 0.6 到 1.0,代价就是多建一个类。这笔交易通常划算。
| 上游 | 喂什么 | 原文出处 |
|---|---|---|
| 第 1 步的领域范围 | 硬依赖。范围不确定,判定就不成立 | 4.5 节 |
| 第 1 步的能力问题 | 决定“最细粒度在哪”——筛 1 的依据 | 4.6 节 |
| 第 3 步的术语清单 | 待判定的候选词 | 第 3 步 |
| 领域专家 | “我们平时会不会把这两个看作不同的东西” | 4.4 节末 |
| 现实世界的演化 | 仙粉黛从“全是红的”到出现白仙粉黛——输入变了,判定要重做 | 4.1 节 |
| 下游 | 影响 | 原文出处 |
|---|---|---|
| 第 4 步 类层次 | 判成“类”的进入层次;判成“属性值”的不进入 | 第 4 步 |
| 第 5 步 属性定义 | 判成“属性”的成为属性;判成“属性值”的成为属性的候选值 | 第 5 步 |
| 第 6 步 属性的约束 | 判成“类”的可以被写成 range;判成“属性值”的进入枚举列表 | 第 6 步 |
| 第 7 步 实例 | 判成“实例”的直接进入知识库 | 第 7 步 |
| 4.8 互斥声明 | 只有判成“类”的才能参与互斥 | 4.8 节 |
| 4.3 多重继承 | 只有判成“类”的才能多重继承 | 4.3 节 |
| 推理机 / 分析工具 | 判成“类”的参与推理与诊断;判成“属性值”的不参与 | 第 7 章(Chimaera) |
| 本体文档 | 判决理由必须写进文档 | 4.7 节 |
有一个下游反馈环值得单独点出:
判定 → 建类 → 实例投入使用 → 发现实例在频繁换类
↑ ↓
└────────── 回头重判 ←────────────────┘
这就是迭代(第 3 章原则 2)在这道分诊台上的具体形态。
你在电脑上整理文件:
2026年度预算.xlsx)现在问你:“已备份”应该是文件夹还是属性?
答案取决于行为:
判据就是那句话:这个区分会不会“活起来”。
这是最精准的一个比方,因为它自带“什么该是分类、什么不该是分类”的直觉。
这正是原文 4.5 节 ChilledWine 案例的同构:
| 生物学的直觉 | 本体的判据 | 原因 |
|---|---|---|
| 物种 → 分类层级 | RedWine → 建类 | 本质属性,不变 |
| 毛色 → 描述性特征 | color → 属性值 | 外在属性,会变 |
| 冰镇 → 描述性状态 | chilled → 布尔属性值 | 外在属性,天天变 |
为什么生物学不会把“白猫”单列为一个物种? 因为那会让分类学失控——一只猫剃了毛,它就换物种了。本体面对的是一模一样的问题。
用过 Jira / 看板的人都懂这个区别:
“冰镇酒”该是状态还是标签? 状态。而状态每天在变——所以它不该是一个独立的“文件夹”。原文的迁移率判据,本质上就是在问:“这是个状态,还是个分类?”
「已备份」是属性还是文件夹?看它会不会活起来。
物种决定本质(建类),毛色是外在特征(属性值)。
工作流状态是类,标签是属性值。状态天天变。
场景:一家餐厅的酒瓶本体。这里会同时出现“该建类”和“该建属性值”两种正确判断,以及它们各自的数值依据。
领域范围:管理餐厅酒窖的 100 瓶库存,支持“按类别查库存”“酒菜搭配推荐”。
能力问题:包括“哪些红酒有高单宁?”“现在有几瓶冰着的酒?”“波尔多配什么?”
观测数据(一天营业,8 小时):
| 观测项 | 数值 |
|---|---|
| 库存酒瓶总数 N | 100 瓶 |
| 一天内被取出冰柜(cold → chilled)的次数 | 60 次 |
| 其中被放回常温(chilled → cold)的次数 | 35 次 |
| 一天内红酒变白酒的次数 | 0 次 |
| 一天内波尔多变成其他产区的次数 | 0 次 |
公式 1:迁移率(对应第四步的“禁用条件”判断)
| 符号 | 含义 |
|---|---|
| M(C) | 观察期内,C 的实例发生的身份变更次数(进入 + 离开都算一次) |
| N | 该类实例总数 |
| Δt | 观察期(单位:天) |
| ρ(C) | 迁移率,单位:次/(实例·天) |
判决规则(阈值 θ):ρ(C) > θ → 属性值;ρ(C) ≤ θ → 类。
公式 2:挂载精确度(对应第六步的“代价权衡”)
读法:挂在 c 上的属性 s,落在“真正该有它”的实例上的比例。越接近 1 越好。
公式 3:区分的信息增益(辅助判据,用来回答“这个区分值不值得建类”)
其中 H 是香农熵 H(X) = −Σ pi log₂ pi。
读法:G(t) 越大,说明知道 t 之后对预测的把握提升越多——这个区分越“有用”,越值得建类。G(t)=0 说明这个区分什么信息都没提供。
① 迁移率:ChilledWine vs RedWine(阈值 θ = 0.1 次/(瓶·天))
ChilledWine(冰镇酒):身份变更次数 60 次进 + 35 次出 = M = 95;ρ = 95 / (100 × 1) = 0.95 次/(瓶·天)。
RedWine(红酒):一天内红酒变白酒 0 次 → M = 0;ρ = 0 / (100 × 1) = 0。
口算验算:0.95 是 0.1 的 9.5 倍——ChilledWine越界非常明显,不是边界情况。
结论落地(原文 4.5 节的处方):在一张描述餐厅里酒瓶的本体中,不应该有Chilled wine这个类。chilled属性应该就是“瓶中酒”的一个属性值而已。于是本体里加一个布尔属性chilled(默认false),不建类。
② 挂载精确度:tanninLevel 挂哪里?
数据:Wine 有 10 个实例,其中 6 个是红酒(真该有单宁含量),4 个是白酒(不该有)。
| 方案 | Instance(c) | 交集 | Precision |
|---|---|---|---|
A:挂在 Wine 上 | 10 | 6 | 0.60 |
B:建 RedWine 类,挂在它上面 | 6 | 6 | 1.00 |
提升 0.40,代价是一个类。
更关键的是:方案 A 不只是“不够准”,它在语义上是错的——它等于断言“每一款酒都有单宁含量”,而白酒没有单宁含量是客观事实。原文对此的判词是:再往上泛化一步——把tannin level挂到Wine类上——就不对了,因为我们不用单宁含量来描述白葡萄酒。
方案 B 还有额外收益:RedWine这个类可以参与互斥声明(RedWine ⊥ WhiteWine),把“红与白不可兼得”变成机器可校验的断言。
③ 信息增益:为什么 color 特殊?
原文说了一句很有意思的话:通常,数字、颜色、地点都是属性值,不会导致新建类。 不过,酒是个显著的例外,因为颜色对酒的描述来说太重要了。
用信息增益量化这句话。假设要考虑的“下一个问题”是“这款酒配什么食物”,食物只有两类:肉 和 海鲜。在 6 款已知的酒里:3 款红酒全部配肉,3 款白酒中 2 款配海鲜、1 款配肉。
这就是“颜色太重要了”的量化版本。 只知道颜色,就能把“配什么菜”的不确定性砍掉一半(0.918 → 0.459)。在信息论里,G = 0.459 比特是个很高的值。
对比一下“酒标用什么纸”这个区分:它对“配什么菜”的信息增益是 G = 0——零增益的区分,永远不该建类(这正是原文 4.7 节“酒标用什么纸不该进本体”的数学表述)。
import java.util.*;
import java.util.stream.Collectors;
/**
* 三分判定器:实现本报告 9.2 节的三条公式
*/
public class TripleClassification {
/** 判定结果 */
enum Kind { CLASS, INSTANCE, SLOT_VALUE }
// ---------- 公式1:迁移率 ----------
// ρ(C) = M(C) / (N · Δt)
record Migration(double changes, int total, double days) {
double rho() { return changes / (total * days); }
}
static final double THETA = 0.1; // 阈值 θ:次/(实例·天)
/**
* 第四步·禁用条件:迁移率超阈值 → 降级为属性值
* 这是 4.5 节"一个个体实例所属的类,不应该经常变"的可执行版本
*/
static Kind decideByMigration(Migration m) {
return m.rho() > THETA ? Kind.SLOT_VALUE : Kind.CLASS;
}
// ---------- 公式2:挂载精确度 ----------
static double precision(Set<String> instancesOfC, Set<String> shouldHaveS) {
if (instancesOfC.isEmpty()) return 0.0; // 除零保护
Set<String> hit = new HashSet<>(instancesOfC);
hit.retainAll(shouldHaveS); // 交集
return (double) hit.size() / instancesOfC.size();
}
// ---------- 公式3:信息增益 ----------
// G(t) = H(Y) - H(Y|t),H 为香农熵,底数 2
static double entropy(List<String> labels) {
if (labels.isEmpty()) return 0.0;
Map<String, Long> freq = labels.stream()
.collect(Collectors.groupingBy(x -> x, Collectors.counting()));
double n = labels.size(), h = 0.0;
for (long c : freq.values()) {
double p = c / n;
h -= p * (Math.log(p) / Math.log(2)); // 换底公式:log2(p)
}
return h;
}
static double infoGain(List<String> y, List<String> t) {
double base = entropy(y); // H(Y)
Map<String, List<Integer>> groups = new LinkedHashMap<>();
for (int i = 0; i < t.size(); i++) {
groups.computeIfAbsent(t.get(i), k -> new ArrayList<>()).add(i);
}
double cond = 0.0; // H(Y|t) 加权平均
for (List<Integer> idx : groups.values()) {
List<String> sub = idx.stream().map(y::get).toList();
cond += (double) idx.size() / y.size() * entropy(sub);
}
return base - cond; // G(t)
}
public static void main(String[] args) {
System.out.println("=== 公式1:迁移率判定(θ = " + THETA + ")===");
// ChilledWine:100 瓶,一天内 60 次冰镇 + 35 次回温
Migration chilled = new Migration(60 + 35, 100, 1.0);
// RedWine:100 瓶,一天内 0 次身份变更
Migration red = new Migration(0, 100, 1.0);
System.out.printf(" ChilledWine: ρ=%.2f → %s%n", chilled.rho(), decideByMigration(chilled));
System.out.printf(" RedWine : ρ=%.2f → %s%n", red.rho(), decideByMigration(red));
System.out.println("\n=== 公式2:tanninLevel 挂哪里?===");
Set<String> allWines = Set.of("w1","w2","w3","w4","w5","w6","w7","w8","w9","w10");
Set<String> redWines = Set.of("w1","w2","w3","w4","w5","w6");
Set<String> shouldHaveTannin = redWines; // 客观事实:只有红酒该有单宁
System.out.printf(" 挂在 Wine : Precision = %.2f%n", precision(allWines, shouldHaveTannin));
System.out.printf(" 挂在 RedWine : Precision = %.2f%n", precision(redWines, shouldHaveTannin));
System.out.println("\n=== 公式3:区分的信息增益 ===");
List<String> food = List.of("肉","肉","肉","海鲜","海鲜","肉"); // 6 款酒配的菜
List<String> color = List.of("红","红","红","白","白","白"); // 颜色
List<String> paper = List.of("A","B","A","B","A","B"); // 酒标用什么纸(无关属性)
System.out.printf(" G(color) = %.3f 比特%n", infoGain(food, color));
System.out.printf(" G(labelPaper) = %.3f 比特 ← 零增益,不该建类%n", infoGain(food, paper));
}
}
运行结果(已核对,与 9.3 手工验算一致):
=== 公式1:迁移率判定(θ = 0.1)===
ChilledWine: ρ=0.95 → SLOT_VALUE
RedWine : ρ=0.00 → CLASS
=== 公式2:tanninLevel 挂哪里?===
挂在 Wine : Precision = 0.60
挂在 RedWine : Precision = 1.00
=== 公式3:区分的信息增益 ===
G(color) = 0.459 比特
G(labelPaper) = 0.000 比特 ← 零增益,不该建类
| 代码块 | 对应哪一步 | 为什么 |
|---|---|---|
Migration 记录类 | 第五步·输入 | 把“实例换了几次类”这个原始观测,变成判定的原材料 |
THETA = 0.1 这个常量 | 第六步·代价权衡 | 阈值就是权衡的旋钮。θ 调高 → 更多东西被判成类 → 结构成本上升;θ 调低 → 更多被判成属性值 → 推理性丧失 |
decideByMigration() 用 > 而不是 >= | 第四步·禁用条件 | 边界语义:恰好等于阈值时不降级,留给人工判断 |
precision() 的 retainAll 求交集 | 第六步·代价权衡 | 公式 2 落地。这个数字就是“挂错位置”的直接损失计量 |
entropy() 里的换底 | 第三步·存在意义 | Java 没有 log2,换底公式保证单位是比特——“颜色太重要了”这句话必须有单位才有意义 |
infoGain() 的加权平均 | 第三步·存在意义 | 条件熵 H(Y|t) 的实现。它回答“这个区分到底提供了多少信息” |
main() 里 G(labelPaper) = 0.000 | 第四步·禁用条件 | 零增益 = 零价值 = 不该进本体。这是 4.7 节“酒标纸不该进本体”的数学化 |
两个 Precision 的对比(0.60 vs 1.00) | 第八步·旧知缝合 | 把“隔板 vs 贴纸”这个比喻变成了一个可以算的数字 |