第二部分 · 报告二

类 / 实例 / 属性值 三分判定

每遇到一个新词,都要回答:盒子?东西?贴纸?

📌 本部分为中文精读版作者的原创分析,原书没有对应章节,因此不提供英文对照。

一直击本质

一句话:每遇到一个新名词,你都要回答一个三选一的问题——“它是个盒子、是盒子里的一件东西、还是贴在盒子上的一个标签?” 这三个身份分别是类、实例、属性值。

先说生活直觉。

假设你在整理家里的储物间,你要给每样东西贴标签。

这就是本体的全部动作:分盒子、放东西、贴标签。 而这本书里 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 个类(还没算葡萄品种)。真实的酒类组合还会更多。

具体后果:

事故四:把实例当类——“子实例”根本不存在

场景:你把 Bourgogne region 建成实例,然后又想表达 Cotes d'Or region 是一个 Bourgogne region。

会怎样:做不到。 原文 4.6 节说得很干脆:只有类才能被组织成层次——知识表示系统里没有“子实例”这个概念。

具体后果:你被迫做一堆随意的判断——“哪几个产区算类、哪几个算实例”,而这个判断本来就没有客观标准。原文的解法是把所有产区统统升成类,然后把它们标成 Abstract(抽象类,不能有直接实例)。

📊

① 统计失真

「本店冰镇酒周转率」取决于统计时刻,而不是事实。

🤐

② 机器查不出错

颜色做成属性值 → 红与白的互斥无处声明。

💥

③ 类爆炸

颜色×酒体×风味×糖分 = 81 个类。推理机算不动。

🚧

④ 子实例不存在

把产区建成实例,就再也无法表达「金丘是勃艮第的一种」。

〔说人话〕这四种事故合起来说明:三分判定不是“学术洁癖”,它是防止本体随时间腐化的免疫机制。
判错的代价不是“模型难看”,而是——统计失真、推理失效、机器查不出错、推理机跑不动。

四流程定位

1)它处于哪个环节或接口上?

它是一道分诊台,卡在第 3 步和第 4/5/7 步之间:

第3步 列出术语清单(不做分类)
          ↓
    ┌─────────────────┐
    │   ★ 三分判定台 ★   │  ← 你在这里
    └─────────────────┘
     ↓        ↓        ↓
   类        属性       (暂时搁置)
     ↓        ↓
第4步 建类层次  第5步 定义属性
     ↓
第7步 建实例 ←—— 从这里往下,"最具体的概念"变成实例

注意:它是一个“分诊台”,不是“一次性的关卡”。 每加一个新词,都要过一次。

2)启动条件:什么时候必须启动它?

触发情形原文出处
每往本体里加一个新名词时第 3 步→第 4 步的转换
当发现某个类的实例在频繁地换类4.5 节“迁移率”判据
当某个类只有 1 个直接子类时4.2 节(见报告一 9.3 的 Bordeaux 案例)
当某个区分需要成为别的属性的约束时4.5 节判据一
当领域专家习惯上会做这个区分时4.4 节末(哪怕你的应用用不上)
当要判断“本体的最细粒度在哪”时4.6 节“回到能力问题”

3)禁用条件:什么情况下绝对不该用它?

禁用条件一:领域范围还没定的时候,不要用它。

原文 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 节说答案取决于范围——没有范围,判断不成立。

传导:三道筛子,按顺序过

新词 t(白葡萄酒?冰镇?2015年?) 筛 1 · 粒度筛 它是「本应用里最具体的条目」吗? 是 → 实例 筛 2 · 约束筛(核心) 它会成为别的类的属性约束吗?自身能自然分层吗? 是 → 类 筛 3 · 迁移筛(兜底) 它的实例会频繁换类吗? 会 → 属性值 不换 → 类
筛 1 先过(实例是相对应用的),筛 2 是核心判据,筛 3 用迁移率兜底。

三道筛子的因果逻辑:

输出:产出什么?

一个判定结论,外加一个理由记录:

decide(t) ∈ { 类, 实例, 属性值 }

理由记录不是可选项。 原文 4.7 节明确要求:

我们应该把这类设计决策记录在本体的文档里,以造福那些将查看这个本体、却可能不了解我们当初设想的是什么应用的用户。否则,打算把这个本体复用到其他应用的人,可能会试图把 experimenter 当作 person 的子类使用,而不知道原始建模里并没有包含这个事实。
〔说人话〕输出的那个“为什么”比“是什么”更重要。 因为“是什么”是给现在用的,“为什么”是给三年后的复用者用的——他们只有看到理由,才知道能不能改。

六代价权衡

这一节要分两边算账。

路线 A:判成“类”

得到:可继承(子类自动拿到属性);可被别的属性当约束(pairingGoodFor 的 range 可以写 RedWine);可参与互斥声明 → 机器能替你查错(4.8 节);可参与多重继承(4.3 节 Port 的例)。

牺牲:类数量指数膨胀(颜色 × 酒体 × 风味 × 糖分 = 乘法增长);推理成本上升(类数量直接决定推理搜索空间);维护成本上升(加一个维度,所有组合都要重算)。

在什么场景下会真的疼:

当那个区分是“正交维度”的时候。

“颜色”和“酒体”是两个独立的维度——一款酒可以是“红 + 饱满”,也可以是“红 + 清淡”。如果你为每个组合建类,你就把两个维度的笛卡尔积硬编码成了类列表。

原文 4.4 节末踩的正是这个坑:我们没有为“清淡的酒”“中等酒体的酒”建类。

路线 B:判成“属性值”

得到:本体扁平、简洁;新增维度零成本(加一个枚举值即可);查询过滤简单直接。

牺牲:完全失去推理性。“红酒有单宁含量”这句话无处安放——你没法在 Wine 上挂 tanninLevel(白酒没有),也没法挂在一个不存在的 RedWine 类上。失去校验能力。机器不知道 RED 和 WHITE 互斥。

在什么场景下会真的疼,而且疼得最厉害:

当你需要“某个区分才有的字段”时——路线 B 直接失灵,没有替代方案。

这就是原文 4.5 节里 Red Merlot / White Merlot 那个例子:红梅洛和白梅洛确实是不同的酒(用同一种葡萄酿成),如果我们建的是一个细致的酒本体,这个区分就很重要。

量化这个“疼”:

Precision = |实际该有这个属性的实例| / |被授予这个属性的实例|

把 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)在这道分诊台上的具体形态。

八旧知缝合

比方一(最好用的一个):像给文件夹分类

你在电脑上整理文件:

现在问你:“已备份”应该是文件夹还是属性?

答案取决于行为:

判据就是那句话:这个区分会不会“活起来”。

比方二:像生物学里的“物种”和“毛色”

这是最精准的一个比方,因为它自带“什么该是分类、什么不该是分类”的直觉。

这正是原文 4.5 节 ChilledWine 案例的同构:

生物学的直觉本体的判据原因
物种 → 分类层级RedWine → 建类本质属性,不变
毛色 → 描述性特征color → 属性值外在属性,会变
冰镇 → 描述性状态chilled → 布尔属性值外在属性,天天变

为什么生物学不会把“白猫”单列为一个物种? 因为那会让分类学失控——一只猫剃了毛,它就换物种了。本体面对的是一模一样的问题。

比方三:像项目管理里的“里程碑”和“标签”

用过 Jira / 看板的人都懂这个区别:

“冰镇酒”该是状态还是标签? 状态。而状态每天在变——所以它不该是一个独立的“文件夹”。原文的迁移率判据,本质上就是在问:“这是个状态,还是个分类?”

🗂️

文件夹分类

「已备份」是属性还是文件夹?看它会不会活起来。

🐱

物种 vs 毛色

物种决定本质(建类),毛色是外在特征(属性值)。

🏷️

状态 vs 标签

工作流状态是类,标签是属性值。状态天天变。

这个区分,是对象的「物种」,还是对象的「状态」?
物种 → 类 · 状态 → 属性值 · 最具体的那个 → 实例

九数值案例串讲

场景:一家餐厅的酒瓶本体。这里会同时出现“该建类”和“该建属性值”两种正确判断,以及它们各自的数值依据。

9.1 场景与数据

领域范围:管理餐厅酒窖的 100 瓶库存,支持“按类别查库存”“酒菜搭配推荐”。
能力问题:包括“哪些红酒有高单宁?”“现在有几瓶冰着的酒?”“波尔多配什么?”

观测数据(一天营业,8 小时):

观测项数值
库存酒瓶总数 N100 瓶
一天内被取出冰柜(cold → chilled)的次数60 次
其中被放回常温(chilled → cold)的次数35 次
一天内红酒变白酒的次数0 次
一天内波尔多变成其他产区的次数0 次

9.2 公式

公式 1:迁移率(对应第四步的“禁用条件”判断)

ρ(C) = M(C) / (N · Δt)
符号含义
M(C)观察期内,C 的实例发生的身份变更次数(进入 + 离开都算一次)
N该类实例总数
Δt观察期(单位:天)
ρ(C)迁移率,单位:次/(实例·天)

判决规则(阈值 θ):ρ(C) > θ → 属性值;ρ(C) ≤ θ → 类。

公式 2:挂载精确度(对应第六步的“代价权衡”)

Precision(s, c) = | Instance(c) ∩ ShouldHave(s) | / | Instance(c) |

读法:挂在 c 上的属性 s,落在“真正该有它”的实例上的比例。越接近 1 越好。

公式 3:区分的信息增益(辅助判据,用来回答“这个区分值不值得建类”)

G(t) = H(对象集) − H(对象集 | t)

其中 H 是香农熵 H(X) = −Σ pi log₂ pi。
读法:G(t) 越大,说明知道 t 之后对预测的把握提升越多——这个区分越“有用”,越值得建类。G(t)=0 说明这个区分什么信息都没提供。

9.3 手工验算

① 迁移率:ChilledWine vs RedWine(阈值 θ = 0.1 次/(瓶·天))

ChilledWine(冰镇酒):身份变更次数 60 次进 + 35 次出 = M = 95;ρ = 95 / (100 × 1) = 0.95 次/(瓶·天)。

0.95 > 0.1 ⟹ ChilledWine 判为属性值

RedWine(红酒):一天内红酒变白酒 0 次 → M = 0;ρ = 0 / (100 × 1) = 0。

0 < 0.1 ⟹ RedWine 判为类
🧊 ChilledWine 放冰箱 → 「是」冰镇酒了 拿出来 → 又不是了 ρ = 0.95 次/(瓶·天) 天天搬家 → 别建类 🍷 RedWine 红酒变白酒:0 次 从未变过 ρ = 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 上1060.60
B:建 RedWine 类,挂在它上面661.00
提升 0.40,代价是一个类。

更关键的是:方案 A 不只是“不够准”,它在语义上是错的——它等于断言“每一款酒都有单宁含量”,而白酒没有单宁含量是客观事实。原文对此的判词是:再往上泛化一步——把 tannin level 挂到 Wine 类上——就不对了,因为我们不用单宁含量来描述白葡萄酒。

方案 B 还有额外收益:RedWine 这个类可以参与互斥声明(RedWine ⊥ WhiteWine),把“红与白不可兼得”变成机器可校验的断言。

③ 信息增益:为什么 color 特殊?

原文说了一句很有意思的话:通常,数字、颜色、地点都是属性值,不会导致新建类。 不过,酒是个显著的例外,因为颜色对酒的描述来说太重要了。

用信息增益量化这句话。假设要考虑的“下一个问题”是“这款酒配什么食物”,食物只有两类:肉 和 海鲜。在 6 款已知的酒里:3 款红酒全部配肉,3 款白酒中 2 款配海鲜、1 款配肉。

不区分颜色时:p(肉)=4/6=0.667,p(海鲜)=2/6=0.333
H(食物) = −(0.667·log₂0.667 + 0.333·log₂0.333) = 0.918 比特

知道颜色之后:红酒全部配肉 → H=0;白酒 1 肉 2 海鲜 → H=0.918
H(食物|颜色) = (3/6)×0 + (3/6)×0.918 = 0.459

G(color) = 0.918 − 0.459 = 0.459 比特
这就是“颜色太重要了”的量化版本。 只知道颜色,就能把“配什么菜”的不确定性砍掉一半(0.918 → 0.459)。在信息论里,G = 0.459 比特是个很高的值。

对比一下“酒标用什么纸”这个区分:它对“配什么菜”的信息增益是 G = 0——零增益的区分,永远不该建类(这正是原文 4.7 节“酒标用什么纸不该进本体”的数学表述)。

9.4 Java 代码

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 比特  ← 零增益,不该建类

9.5 逐行对照八步

代码块对应哪一步为什么
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 贴纸”这个比喻变成了一个可以算的数字
← 上一章 下一章:报告三 · 属性解剖 →