CS221 · 人工智能:原理与技术
Lecture 15 · 逻辑 I:命题逻辑
源文件:lecture-15.md
Lecture 15 · 逻辑 I:命题逻辑
CS221: Artificial Intelligence — Principles and Techniques · Stanford · Autumn 2025
📅 Nov 10 · 💻propositional_logic.py
从概率推理到逻辑推理
上一单元(贝叶斯网络,Lecture 12 · 贝叶斯网络 I:建模与推断 → Lecture 14 · 贝叶斯网络 III:参数学习) 用概率表示对世界的不确定信念:先用联合分布与条件独立建模,再做推断 $P(\text{query} \mid \text{evidence})$。概率的强项是处理不确定性——真实世界是嘈杂的、随机的,我们的观测也不完整。
本单元(逻辑推理) 换一种表示知识、进行推理的语言:逻辑(logic)。本讲先讲最简单的命题逻辑(propositional logic),下一讲 Lecture 16 · 逻辑 II:一阶逻辑 再升级到一阶逻辑。逻辑的强项不在处理随机性,而在表达力(expressivity)——用非常紧凑的公式表示复杂的知识,并支持精确的推理。
graph LR
P["感知 Perceive"] --> R["推理 Reason"]
R --> A["行动 Act"]
A -.反馈.-> L["学习 Learn"]
L -.改进.-> R
style R fill:#ffe0b2,stroke:#e65100
style L fill:#e1f5fe,stroke:#0277bd
一个引子:若 $A + B = 10$ 且 $A - B = 4$,问 $A$ 是多少?你当然不会穷举 $A, B$ 的所有取值去搜索,而是用代数:两式相加得 $2A = 14$,于是 $A = 7$。这就是符号推理 / 逻辑推理的原型——不逐个枚举可能的世界,而是操作符号,几步就把结论推出来。
提示
逻辑推理的威力在于:面对天文数字般的可能世界($n$ 个变量就有 $2^n$ 种组合),它不逐个检查,而是用少数几条符号变换规则直接把结论”算”出来。本讲的主线就是搞清楚:什么时候这种符号操作是合法的(可靠性),以及它能不能把所有真结论都推出来(完备性)。
历史小注:逻辑是 1990 年代之前 AI 的主导范式——专家系统、Prolog、定理证明都建立在它之上。它有两块短板:(1)它是确定性的,不处理不确定性——后来由概率图模型(本课的贝叶斯网络单元)来补;(2)它靠人手写规则,不利用数据——后来由机器学习(Lecture 1 · 张量、梯度与监督学习 开启的整条主线)来补。但它的长板至今无可替代:以极紧凑的方式提供强大的表达力与精确推理,这也是 Z3 这类求解器今天仍在程序验证、芯片验证中大量使用的原因。
把逻辑当作一门语言,它有两个目标:表示(represent) 关于世界的知识,推理(reason) 推出新知识。
1. 逻辑系统的三要素
1.1 语法 / 语义 / 推理规则
任何一个逻辑系统都由三样东西定义——语法规定哪些符号串合法、语义赋予它们含义、推理规则在不看语义的前提下做纯符号操作:
| 要素 | 回答什么问题 | 命题逻辑中的具体内容 |
|---|---|---|
| 语法(syntax) | 哪些表达式是合法的? | 命题符号 + 联结词 $\neg\ \wedge\ \vee\ \to\ \leftrightarrow$ 组成的公式 |
| 语义(semantics) | 这些表达式是什么意思? | 在每个模型(真值赋值)下公式为真/假 |
| 推理规则(inference rules) | 如何推出新的合法公式? | modus ponens、归结……(纯操作符号) |
提示
为什么要把三层严格分开?打个编程的比方:语法像 parser(判断程序合不合法),语义像解释器(跑出程序的结果),推理规则像编译器的等价变换(不运行程序就改写它,且保证结果不变)。分开的收益是:语义计算(枚举所有模型)代价可能是指数级,而推理规则只做符号操作,可以快得多——前提是我们能证明这些符号操作忠于语义(第 8 节的可靠性与完备性)。
1.2 为什么不用自然语言?
因为自然语言太滑溜(slippery)了:同一个词在不同语境含义漂移,推理很容易翻车。
注意
- 一分钱(penny)比什么都没有(nothing)好。
- 什么都没有(nothing)比世界和平(world peace)好。
- 所以……一分钱比世界和平好??
问题出在 “nothing” 在两句话里含义漂移:第一句是”空无一物”,第二句是”没有任何事物(比世界和平好)”。符号相同、语义不同,三段论就失效了。
例子
一角钱比五分钱好;五分钱比一分钱好;所以一角钱比一分钱好。这里 “better than” 全程含义一致,且具有传递性,推理才站得住。
形式语言(编程语言、逻辑语言)通过精确的语法和语义消除了这种歧义。命题逻辑就是最简单的一种形式逻辑语言。
1.3 语法 vs 语义:不要混淆
语法是符号串本身,语义是它指向的含义,两者可以脱钩:
| 关系 | 例子 |
|---|---|
| 不同语法,相同语义 | $2 + 3$ 和 $3 + 2$,语义都是 $5$ |
| 相同语法,不同语义 | 3 / 2 在 Python 2.7 里是 1,在 Python 3 里是 1.5 |
说明
语法管”怎么写”,语义管”什么意思”,推理规则管”怎么算”。学逻辑就是把这三层严格分开,再用解释函数(语法→语义)和可靠性/完备性(推理规则→语义)把它们缝起来。
源码骨架(propositional_logic.py 用 Z3 的表达式类型作为底层):
from z3 import Bool, BoolRef, ExprRef, Not, And, Or, Implies, \
is_not, is_and, is_or, is_implies, is_eq, Solver, sat
import itertools
# 三个核心类型别名
PropositionalSymbol = BoolRef # 命题符号,如 Bool("Rain")
Formula = ExprRef # 公式(符号 + 联结词组成的表达式树)
Model = dict[PropositionalSymbol, bool] # 模型 = 对所有符号的真值赋值
2. 命题逻辑:语法
2.1 公式的归纳定义
语法回答:哪些是合法公式(formula)?定义是归纳的:
- 基础情形:命题符号(原子公式 / atomic formula)本身是公式;
- 归纳情形:设 $f, g$ 为任意公式,则 $\neg f$、$f \wedge g$、$f \vee g$、$f \to g$、$f \leftrightarrow g$ 也是公式;
- 封闭条款:除此之外一律不是公式(nothing else is a formula)。
# 基础情形:命题符号(原子公式)
P = Bool("P"); Rain = Bool("Rain"); Wet = Bool("Wet")
# 归纳情形:设 f、g 是公式,则以下也是公式(五个联结词 ¬ ∧ ∨ → ↔)
f, g = Rain, Wet
h1 = Not(f) # ¬f 非,negation
h2 = And(f, g) # f ∧ g 合取,conjunction
h3 = Or(f, g) # f ∨ g 析取,disjunction
h4 = Implies(f, g) # f → g 蕴涵,implication
h5 = (f == g) # f ↔ g 等价 / 双条件,biconditional
提示
归纳定义的妙处:任意复杂的公式都是一棵语法树——叶子是命题符号、内部节点是联结词。这棵树正是下一节解释函数递归求值的结构基础:语法怎么归纳定义,语义就怎么递归计算。
2.2 合法与非法公式
# ✅ 合法公式
f = Or(Not(P), Q) # ¬P ∨ Q
f = Implies(P, Or(Q, Not(P))) # P → (Q ∨ ¬P)
# ❌ 不是命题逻辑的公式
# P ¬Q 两个符号并排,没有联结词
# P + Q "+" 不是逻辑联结词
# P(A) ∨ Q(B) 带参数的谓词——那是一阶逻辑(下一讲)
我们现在知道了哪些是合法公式,但它们到底是什么意思? → 语义。
3. 命题逻辑:语义
公式本身只是符号(语法),还没有含义(语义)。语义部分要引入一连串概念,源码的组织顺序就是最好的学习路线:
flowchart LR
M["模型 model<br/>世界的状态"] --> I["解释函数 ℐ(f,w)<br/>缝合语法与语义"]
I --> MF["M(f)<br/>公式的模型集"]
MF --> KB["知识库 KB<br/>公式的集合"]
KB --> REL["蕴含 / 矛盾 / 或然"]
REL --> AT["Ask / Tell"]
AT --> SAT["可满足性 satisfiability"]
style M fill:#c8e6c9,stroke:#2e7d32
style I fill:#f3e5f5,stroke:#7b1fa2
style MF fill:#e1f5fe,stroke:#0277bd
style KB fill:#e1f5fe,stroke:#0277bd
style REL fill:#ffe0b2,stroke:#e65100
style SAT fill:#fce4ec,stroke:#c2185b
3.1 模型(model):世界的一种状态
定义:命题逻辑中的一个模型 $w$,就是对所有命题符号的一次真值赋值(assignment of truth values)。
一个模型 = 一个”可能的世界”。$n$ 个命题符号就有 $2^n$ 个模型——每个符号独立地取真或假。
A = Bool("A"); B = Bool("B"); C = Bool("C")
# 2^3 = 8 个可能的模型(对 A, B, C 的全部真值组合)
models = [
{A: False, B: False, C: False}, # (F, F, F)
{A: False, B: False, C: True}, # (F, F, T)
# ... 中间 4 个省略 ...
{A: True, B: True, C: True}, # (T, T, T)
]
说明
“模型”这个词在机器学习里指训练出来的函数,在逻辑里却指”世界的一种可能状态”,两个含义完全不同。逻辑里的模型对应贝叶斯网络里的一次完整赋值(assignment)——第 5.2 节会把这组对应关系列全。
3.2 解释函数(interpretation function)$\mathcal{I}$
解释函数把语法(公式)和语义(模型)缝合起来:
定义:解释函数 $\mathcal{I}(f, w)$ 把公式 $f$ 和模型 $w$ 映射到一个真值——true 表示 $f$ 在 $w$ 下为真,false 表示为假。
它沿着公式的语法树递归求值:原子公式直接查 $w$ 的赋值表,复合公式先递归求子公式、再按联结词组合。其中蕴涵的定义值得单独写出来:
$$\mathcal{I}(g \to h,\ w) \;=\; \neg\,\mathcal{I}(g, w) \vee \mathcal{I}(h, w)$$
即 $g \to h$ 与 $\neg g \vee h$ 语义等价。源码里的 I 按参数个数分派联结词:
def I(f: Formula, w: Model) -> bool:
"""解释函数:给定公式 f 和模型 w,判断 w 是否满足 f。"""
if f.num_args() == 0: # 原子公式:直接查表
return w[f]
if f.num_args() == 1 and is_not(f): # 一元:¬g
return not I(f.arg(0), w)
if f.num_args() == 2: # 二元联结词
g, h = f.arg(0), f.arg(1)
if is_and(f): return I(g, w) and I(h, w) # g ∧ h
if is_or(f): return I(g, w) or I(h, w) # g ∨ h
if is_implies(f): return (not I(g, w)) or I(h, w) # g → h ≡ ¬g ∨ h
if is_eq(f): return I(g, w) == I(h, w) # g ↔ h
raise ValueError(f"Unsupported formula: {f}")
例子
公式 $f = (\neg A \wedge B) \leftrightarrow C$ 在模型 $w = \{A{:}\,\text{T},\ B{:}\,\text{T},\ C{:}\,\text{F}\}$ 下:$\neg A = \text{F}$ → $\neg A \wedge B = \text{F}$ → $\text{F} \leftrightarrow \text{F} = \text{T}$,所以 $\mathcal{I}(f, w) = \text{true}$。
注意
$f \to g$ 只在”$f$ 真而 $g$ 假”时为假,其余情况一律为真——包括前件 $f$ 为假的情况(所谓”空真”,vacuously true)。别用日常的”因果关系”直觉去套:逻辑蕴涵只是真值表定义,”如果 $1+1=3$ 那么月亮是奶酪做的”是一个真命题。
五个联结词的真值表:
| $f$ | $g$ | $\neg f$ | $f \wedge g$ | $f \vee g$ | $f \to g$ | $f \leftrightarrow g$ |
|---|---|---|---|---|---|---|
| F | F | T | F | F | T | T |
| F | T | T | F | T | T | F |
| T | F | F | F | T | F | F |
| T | T | F | T | T | T | T |
3.3 公式的模型集合 $\mathcal{M}(f)$
定义:公式 $f$ 的模型集合是所有使它为真的模型:
$$\mathcal{M}(f) = \{\, w : \mathcal{I}(f, w) = \text{true} \,\}$$
计算上,枚举所有 $2^n$ 个赋值、逐个用 I 检查即可:
def get_models(f: Formula, symbols: list[PropositionalSymbol]) -> list[Model]:
"""返回所有满足公式 f 的模型。"""
models = []
for values in itertools.product([False, True], repeat=len(symbols)):
w = dict(zip(symbols, values)) # 一个模型 = 对符号的一次真值赋值
if I(f, w): # f 在 w 下为真吗?
models.append(w)
return models
Rain = Bool("Rain"); Wet = Bool("Wet")
get_models(Or(Rain, Wet), [Rain, Wet]) # M(Rain ∨ Wet):3 个模型
get_models(And(Rain, Wet), [Rain, Wet]) # M(Rain ∧ Wet):1 个模型
提示
一个(小小的)公式紧凑地表示了一个(可能很大的)模型集合——这正是逻辑”表达力”的来源。Or(Rain, Wet) 只用三个符号就框定了 $\{FT, TF, TT\}$ 三种世界(只排除了 $FF$);若符号有 100 个,一条短公式就在 $2^{100}$ 个世界中划出了一半。用集合直接列举做不到这么省。
3.4 知识库(knowledge base, KB)
定义:知识库 KB 是一个公式的集合;把它想成一堆你会随时间不断添加的事实。
语义:$\mathcal{M}(\text{KB})$ 是同时满足 KB 中每一条公式的模型——“给定我们的知识,我们可能身处的世界”。
关键性质:KB 的模型集等于各公式模型集的交集,也等价于把各公式用合取(conjunction)串起来:
$$\mathcal{M}(\text{KB}) \;=\; \bigcap_{f \in \text{KB}} \mathcal{M}(f) \;=\; \mathcal{M}\Big(\bigwedge_{f \in \text{KB}} f\Big)$$
def intersect(l1: list[Model], l2: list[Model]) -> list[Model]:
return [m for m in l1 if m in l2] # 两个模型列表的交集
def to_formula(kb: list[Formula]) -> Formula:
f = kb[0]
for g in kb[1:]:
f = And(f, g) # 用 ∧ 把 KB 串成单个公式
return f
kb = [Rain, Implies(Rain, Wet)] # KB = {Rain, Rain → Wet}
models_kb = intersect(get_models(Rain, [Rain, Wet]),
get_models(Implies(Rain, Wet), [Rain, Wet])) # = M(KB)
# 语义上 KB 等价于其合取:M(to_formula(kb)) 与 models_kb 相同
提示
KB 里每加一条事实,就是加一个”过滤器”:只有同时通过所有过滤器的世界才留在交集里。事实越多,剩下的可能世界越少——知识增长的过程,就是不确定性收缩的过程。这个”收缩”的三种程度正是下一节的主题。
4. 蕴含、矛盾与或然
4.1 添加知识 = 收缩模型集
核心洞见:把新公式 $f$ 加进 KB 后模型集只会收缩不会扩大:
$$\text{KB} \subseteq \text{KB} \cup \{f\} \implies \mathcal{M}(\text{KB} \cup \{f\}) \subseteq \mathcal{M}(\text{KB})$$
原因很直接:新模型集是旧模型集与 $\mathcal{M}(f)$ 的交集,交集不可能比原集合大。
提示
随着知识增加,我们缩小了可能身处的世界范围。问题只剩一个——这次收缩了多少? 完全没缩、缩成空集、缩掉一部分,三种情况恰好对应三个核心概念。
graph TD
ALL["M(KB)<br/>当前可能的世界"] --> E["蕴含 Entailment<br/>完全不缩:M(KB∪f) = M(KB)"]
ALL --> CON["矛盾 Contradiction<br/>缩成空集:M(KB∪f) = ∅"]
ALL --> CTG["或然 Contingency<br/>部分收缩:∅ ≠ M(KB∪f) ≠ M(KB)"]
style ALL fill:#ffe0b2,stroke:#e65100
style E fill:#c8e6c9,stroke:#2e7d32
style CON fill:#ffcdd2,stroke:#c62828
style CTG fill:#e1f5fe,stroke:#0277bd
4.2 三个定义
三个概念的实现都只是比较 $\mathcal{M}(\text{KB})$ 与 $\mathcal{M}(\text{KB} \cup \{f\})$:
- ① 蕴含(entailment) $\text{KB} \models f$:$\mathcal{M}(\text{KB} \cup \{f\}) = \mathcal{M}(\text{KB})$。直觉:$f$ 没带来新信息——KB 的每个模型本来就满足 $f$。
- ② 矛盾(contradiction):$\mathcal{M}(\text{KB} \cup \{f\}) = \varnothing$。直觉:$f$ 与 KB 不相容,没有任何世界能同时满足二者。
- ③ 或然(contingency):$\varnothing \neq \mathcal{M}(\text{KB} \cup \{f\}) \neq \mathcal{M}(\text{KB})$。直觉:$f$ 带来了真正的新信息——排除了一部分世界,但没有全排除。
symbols = [Rain, Wet]
def M(kb): return get_models(to_formula(kb), symbols) # 取 KB 的模型集
def entails(kb, f): return M(kb + [f]) == M(kb) # ①
def contradicts(kb, f): return M(kb + [f]) == [] # ②
def contingent(kb, f): return M(kb + [f]) != [] and M(kb + [f]) != M(kb) # ③
assert entails([Rain, Implies(Rain, Wet)], Rain) # KB 显然蕴含 Rain
assert contradicts([Rain, Wet], Not(Wet)) # 已知 Wet 又说 ¬Wet,冲突
assert contingent([Rain], Wet) # 只知道 Rain,Wet 可真可假
蕴含与矛盾的关系(一个非常有用的等价,第 6 节归约到可满足性时全靠它):
$$\text{KB 与 } f \text{ 矛盾} \iff \text{KB} \models \neg f$$
说明
“KB 与 $f$ 矛盾”意思是没有模型同时满足 KB 和 $f$;换句话说,KB 的每一个模型都不满足 $f$,即都满足 $\neg f$;而”KB 的每个模型都满足 $\neg f$”正是 $\text{KB} \models \neg f$ 的定义。三步改写,两个概念就接通了。
4.3 重言、矛盾、或然(相对空 KB)
当 KB 为空(无任何背景知识、$\mathcal{M}(\text{KB})$ 为全体模型)时,上述三类关系退化为公式自身的分类:
| 术语 | 定义(相对空 KB) | 例子 |
|---|---|---|
| 重言式(tautology) | 在所有模型下都为真,$\mathcal{M}(f)$ = 全体 | $P \vee \neg P$ |
| 矛盾式(contradiction) | 在任何模型下都为假,$\mathcal{M}(f) = \varnothing$ | $P \wedge \neg P$ |
| 或然式(contingency) | 有时真有时假,$\varnothing \neq \mathcal{M}(f) \neq$ 全体 | $P$、$P \to Q$ |
5. Ask 与 Tell:操作知识库
有了 KB,我们能做两件事:Ask[f](向 KB 问是非问题)和 Tell[f](向 KB 添加新陈述)。两者的回答都取决于 KB 与 $f$ 的关系(蕴含 / 矛盾 / 或然)——所以恰好各有三种回答。
5.1 Ask 与 Tell 的实现
Ask 三种回答:Yes / No / I don’t know;Tell 三种回答:I already knew that / I don’t buy that / I learned something new。二者共享同一套分派(old/new 分别是 $\mathcal{M}(\text{KB})$ 与 $\mathcal{M}(\text{KB} \cup \{f\})$):
def ask(kb, f) -> str:
old, new = M(kb), M(kb + [f])
if new == old: return "Yes" # 蕴含
if new == []: return "No" # 矛盾
return "I don't know" # 或然
def tell(kb, f):
old, new = M(kb), M(kb + [f])
if new == old: return kb, "I already knew that" # 蕴含
if new == []: return kb, "I don't buy that" # 矛盾
return kb + [f], "I learned something new" # 或然:才真正更新 KB
ask([Rain, Wet], Or(Rain, Wet)) # → Yes
ask([Wet, Not(Rain)], Rain) # → No
ask([Rain], Wet) # → I don't know
tell([Rain, Implies(Rain, Wet)], Wet) # → "I already knew that"
tell([Rain, Wet], Not(Rain)) # → "I don't buy that"
tell([Rain], Not(Wet)) # → "I learned something new"
提示
Ask/Tell 是逻辑智能体与世界打交道的接口:Tell 把观测灌进 KB,Ask 从 KB 里问出决策依据。注意只有”或然”时 Tell 才真正扩充 KB——蕴含的陈述是废话(早知道了),矛盾的陈述是谎话(不能收)。一个理性的 agent 只吸收带新信息且自洽的知识。
| KB 与 $f$ 的关系 | 模型集 | Ask[f] | Tell[f] |
|---|---|---|---|
| 蕴含 | $= \mathcal{M}(\text{KB})$ | Yes | I already knew that |
| 矛盾 | $= \varnothing$ | No | I don’t buy that |
| 或然 | 部分收缩 | I don’t know | I learned something new |
5.2 与贝叶斯网络的联系(承接上一单元)
命题逻辑与 Lecture 12 · 贝叶斯网络 I:建模与推断 的概率推理在结构上高度对应:
| 贝叶斯网络 | ↔ | 命题逻辑 |
|---|---|---|
| 随机变量(random variables) | ↔ | 命题符号(symbols) |
| 赋值(assignments) | ↔ | 模型(models) |
| 证据(evidence) | ↔ | 知识库 KB |
| 查询(query) | ↔ | Ask[] 的对象 |
概率推断 $P(\text{query} \mid \text{evidence})$ 对应 Tell[evidence]; Ask[query],例如 $P(\text{Rain} \mid \text{Wet}=1)$ 对应 Tell[Wet]; Ask[Rain]。
关键区别:贝叶斯网络里 query/evidence 只能是简单的变量赋值(如 $\text{Rain}=1, \text{Wet}=0$);命题逻辑里可以是任意公式(如 $(\text{Rain} \wedge \text{Wet}) \vee (\text{Rain} \wedge \neg\text{Snow})$)——这是逻辑表达力更强之处。反过来,逻辑的 Yes/No/不知道 可以被概率推广为 0 到 1 之间的数:给每个世界 $w$ 一个概率 $P(W = w)$,则
$$P(\text{KB}) = \sum_{w \in \mathcal{M}(\text{KB})} P(W = w), \qquad P(f \mid \text{KB}) = \frac{P(\text{KB} \cup \{f\})}{P(\text{KB})}$$
其中分子是同时满足 KB 与 $f$ 的世界的概率质量。蕴含对应 $P(f \mid \text{KB}) = 1$,矛盾对应 $P(f \mid \text{KB}) = 0$,或然对应中间值——逻辑是概率推理在 $\{0, 1\}$ 两端的特例。源码用 bayes.py 的 ProbTable 演示 $P(\text{Rain}, \text{Wet})$ 的联合分布,把逻辑的”是非判断”嵌入概率框架。
6. 可满足性(satisfiability)
6.1 归约:三合一
到目前为止 Ask/Tell 都靠枚举 $\mathcal{M}(\text{KB})$——$O(2^n)$ 级别,符号一多就指数爆炸。为了更快,先把蕴含/矛盾/或然三个判断全部归约到一个操作:
定义:KB 是可满足的(satisfiable)当且仅当 $\mathcal{M}(\text{KB}) \neq \varnothing$(至少存在一个满足它的世界)。
由于一次可满足性检查只给 1 比特信息,而我们要区分 3 种结果(至少需要 2 比特),所以需要两次调用:
flowchart TD
START["输入 KB 和公式 f"] --> C1{"KB ∪ {f}<br/>可满足?"}
C1 -->|不可满足| CONTRA["矛盾 / No"]
C1 -->|可满足| C2{"KB ∪ {¬f}<br/>可满足?"}
C2 -->|不可满足| ENT["蕴含 / Yes<br/>(KB 与 ¬f 矛盾)"]
C2 -->|可满足| CTG["或然 / I don't know"]
style START fill:#e1f5fe,stroke:#0277bd
style CONTRA fill:#ffcdd2,stroke:#c62828
style ENT fill:#c8e6c9,stroke:#2e7d32
style CTG fill:#ffe0b2,stroke:#e65100
第二步用到了 4.2 的等价:
$$\text{KB} \models f \iff \text{KB 与 } \neg f \text{ 矛盾} \iff \text{KB} \cup \{\neg f\} \text{ 不可满足}$$
6.2 模型检查(model checking)与 Z3
模型检查(model checking):判定一个 KB 是否可满足的任务——输入一个 KB,输出它是否可满足(可满足时还能给出一个满足它的模型作为”证据”)。
源码直接调用 Z3 求解器做模型检查:
Rain = Bool("Rain"); Wet = Bool("Wet")
kb = [Rain, Wet]
solver = Solver()
for f in kb:
solver.add(f) # 把每条公式加入求解器
result = solver.check() # 执行模型检查
if result == sat: # 若可满足
w = solver.model() # 返回 M(KB) 中的一个模型
说明
命题逻辑的可满足性就是著名的 SAT 问题——1971 年 Cook 证明它是第一个 NP-完全问题,理论上最坏情况仍是指数时间。但现代 SAT 求解器在实际问题上远比朴素枚举快:DPLL 本质上是带回溯的深度优先搜索(与 Lecture 5 · 搜索算法 I 的回溯搜索同源),加上单元传播(unit propagation)等剪枝;CDCL(conflict-driven clause learning,冲突驱动子句学习)进一步在每次搜索撞上矛盾时学出一条新子句加入 KB,避免将来再犯同样的错。工业级求解器能处理百万变量级的实例,这是”NP-完全 ≠ 实践中不可解”的经典案例。
6.3 合取范式(CNF)与归结(resolution)
SAT 求解器通常先把公式转成合取范式(Conjunctive Normal Form, CNF)——一堆子句(clause)的合取,每个子句是若干文字(literal,即命题符号或其否定)的析取,例如 $(P \vee \neg Q) \wedge (\neg P \vee R) \wedge (Q \vee R)$。任意命题公式都可以等价(或保持可满足性地)转成 CNF。在 CNF 上,一条核心推理规则是归结(resolution):两个子句若含互补文字,可消去该文字、合并剩余部分:
$$\frac{P \vee Q \qquad \neg P \vee R}{Q \vee R}$$
例子
- 把 KB 与结论的否定转成 CNF 子句:$\{\text{Rain}\}$、$\{\neg\text{Rain} \vee \text{Wet}\}$(即 $\text{Rain} \to \text{Wet}$)、$\{\neg\text{Wet}\}$(结论的否定)。
- 对 Wet 归结:$\neg\text{Rain} \vee \text{Wet}$ 与 $\neg\text{Wet}$ 得 $\neg\text{Rain}$。
- 对 Rain 归结:$\text{Rain}$ 与 $\neg\text{Rain}$ 得空子句 $\square$(矛盾)。
- $\text{KB} \cup \{\neg\text{Wet}\}$ 不可满足 $\implies \text{KB} \models \text{Wet}$。∎
注意
精确地说,归结是反驳完备(refutation-complete)的:若一个子句集不可满足,反复归结必定能推出空子句;但它并不能从 KB 直接”生成”所有被蕴含的公式(比如从 $\{P\}$ 归结不出 $P \vee Q$ 这个新子句)。好在这已经足够——判定任何蕴含 $\text{KB} \models f$,都可以转化为对 $\text{KB} \cup \{\neg f\}$ 做反驳。所以”归结 + 反证法”构成命题逻辑上可靠且完备的判定程序。
小结:蕴含/矛盾/或然都可归约到可满足性;用模型检查(Z3 / SAT 求解器,内部 DPLL/CDCL、工作在 CNF 上)来高效计算它。
7. 推理规则:从语义到语法
前面 Ask/Tell 走的是语义路线(枚举或检查模型)。但回到开头的引子——解 $A + B = 10,\ A - B = 4$ 时,你并没有枚举世界,而是操作符号。这就是推理规则要做的事:完全绕开模型,在语法层直接生成新公式。
7.1 Modus Ponens(假言推理)
一个具体推理:下雨(Rain);如果下雨则地湿($\text{Rain} \to \text{Wet}$);所以地湿(Wet)。写成一般的 modus ponens 规则(横线上是前提,横线下是结论):
$$\frac{p \qquad p \to q}{q}$$
一般的推理规则(inference rule)形如:前提是一组公式 $f_1, \dots, f_k$,结论是公式 $g$,记作 $f_1, \dots, f_k \vdash g$。
注意
推理规则只操作语法(符号模式匹配),不碰语义(不看任何模型)。它是纯粹的符号变换——这正是它比枚举 $2^n$ 个模型高效的原因,但也意味着必须另行证明它忠于语义(第 8 节)。
graph LR
P["p :Rain"] --> MP["modus ponens"]
IMP["p → q :Rain → Wet"] --> MP
MP --> Q["⊢ q :Wet"]
style P fill:#e1f5fe,stroke:#0277bd
style IMP fill:#e1f5fe,stroke:#0277bd
style MP fill:#ffe0b2,stroke:#e65100
style Q fill:#c8e6c9,stroke:#2e7d32
7.2 前向推理(forward inference)
反复套用规则,把能推出的公式全部加进 KB:
- 输入:一组推理规则 Rules,初始知识库 KB;
- 重复直到 KB 不再变化:从 KB 中选一组公式 $f_1, \dots, f_k$,若存在匹配的规则 $f_1, \dots, f_k \vdash g$,则把 $g$ 加入 KB。
若 $f$ 最终被加入 KB,就说 KB 推导出(derives)/ 证明了(proves) $f$,记作 $\text{KB} \vdash f$。
例子
从 $\{\text{Rain},\ \text{Rain} \to \text{Wet}\}$ 出发,用 modus ponens 可推出 Wet;但推不出 $\neg\text{Wet}$、$\text{Rain} \to \text{Slippery}$——KB 里没有能匹配上的前提与规则。这两条本来也不被 KB 蕴含,推不出是好事(可靠性的体现)。真正暴露 modus ponens 局限的是:$\{\text{Rain}\} \models \text{Rain} \vee \text{Wet}$,但 modus ponens 无法推出它——这是不完备(见第 8 节)。
graph LR
R["Rain"] --> W["Wet ✓"]
RW["Rain → Wet"] --> W
W -.推不出.-> NW["¬Wet ✗"]
W -.推不出.-> RS["Rain → Slippery ✗"]
style R fill:#c8e6c9,stroke:#2e7d32
style RW fill:#c8e6c9,stroke:#2e7d32
style W fill:#c8e6c9,stroke:#2e7d32
style NW fill:#ffcdd2,stroke:#c62828
style RS fill:#ffcdd2,stroke:#c62828
8. 可靠性与完备性:缝合语法与语义
现在有了两条平行的路线,必须问:它们一致吗?
- 语法:$\text{KB} \vdash f$(能从 KB 推导 / 证明出的公式)
- 语义:$\text{KB} \models f$(被 KB 蕴含、即在 KB 的所有模型中为真的公式)
课程用一个绝妙的比喻(法庭誓词)说明理想目标是 “the truth, the whole truth, and nothing but the truth“(真相、全部真相、且只有真相):
| 性质 | 定义 | 直觉 | 一句话 |
|---|---|---|---|
| 可靠性(soundness) | $\{f : \text{KB} \vdash f\} \subseteq \{f : \text{KB} \models f\}$ | 推出来的都是对的 | nothing but the truth(只有真相) |
| 完备性(completeness) | $\{f : \text{KB} \models f\} \subseteq \{f : \text{KB} \vdash f\}$ | 所有对的都能推出来 | the whole truth(全部真相) |
提示
可靠 = 不说假话(推出的绝不出错,但可能推不全);完备 = 不漏真话(真的都能推到,但若规则太猛可能连假的也一起推出)。二者兼备时 $\vdash$ 与 $\models$ 完全重合——纯符号操作精确复刻了语义真理,这是逻辑系统的圣杯。对 AI 而言这意味着:agent 可以只做快速的符号计算,却得到与”枚举所有可能世界”完全相同的结论。
规则集的强弱决定落在哪一侧:
- 规则太弱(如只有 modus ponens):可靠但不完备——$\{\text{Rain}\} \models \text{Rain} \vee \text{Wet}$ 却推不出。不过有一个重要特例:当 KB 全部由 Horn 子句(至多一个正文字的子句,即”事实”与”$a_1 \wedge \cdots \wedge a_k \to b$”型规则)组成、且查询是原子公式时,modus ponens 就是完备的——这正是下一讲确定子句推理的理论基础。
- 规则太猛(乱加规则):完备但不可靠——什么都能推出来,包括假的。
- 恰到好处:归结(resolution)配合反证法,对整个命题逻辑既可靠又(反驳)完备——这正是 SAT 求解如此重要的原因。
graph LR
E["⊢ ⊂ ⊧<br/>只可靠:推不全"] --> BAL["⊢ = ⊧<br/>可靠 且 完备<br/>(归结)"]
O["⊧ ⊂ ⊢<br/>只完备:会推错"] --> BAL
style E fill:#e1f5fe,stroke:#0277bd
style O fill:#ffcdd2,stroke:#c62828
style BAL fill:#c8e6c9,stroke:#2e7d32
9. 总结
mindmap
root((命题逻辑))
三要素
语法
语义
推理规则
语法
命题符号
五个联结词
公式的归纳定义
语义
模型即真值赋值
解释函数
公式的模型集
知识库即公式集合
三种关系
蕴含
矛盾
或然
Ask 与 Tell
推理
可满足性
模型检查与 SAT
DPLL 与 CDCL
归结与 CNF
元性质
可靠性
完备性
关键要点:
- 一门逻辑由语法 / 语义 / 推理规则三要素定义;把逻辑当作一门语言——用于表示知识与推理出新知识。
- 命题逻辑:命题符号 + 联结词 $\neg\ \wedge\ \vee\ \to\ \leftrightarrow$ 归纳出公式;解释函数 $\mathcal{I}(f, w)$ 沿语法树递归求值,把语法接到语义。
- 模型是对所有符号的真值赋值($n$ 个符号 $2^n$ 个模型);$\mathcal{M}(f)$ 是使 $f$ 为真的模型集;一个小公式紧凑表示一大片世界——这是逻辑表达力的来源。
- 向 KB 加公式会收缩模型集,程度分三类:蕴含(不变)/ 矛盾(空集)/ 或然(部分收缩);对应 Ask 的 Yes/No/不知道 与 Tell 的三种回答。
- 三类关系都可归约到可满足性(两次 SAT 调用),用模型检查(Z3,内部是 DPLL/CDCL)高效计算;归结在 CNF 上配合反证法可靠且反驳完备。
- 推理规则(modus ponens、归结)只操作语法;可靠性(推出的都对)与完备性(对的都能推出)度量 $\vdash$ 与 $\models$ 的重合程度。
下一讲预告:命题逻辑的表达力仍然有限——想说”每个学生都有一个 TA”,就得给每个学生各写一条公式;想谈论无穷多个对象(如”所有偶数”)则彻底无能为力。一阶逻辑(first-order logic)引入量词 $\forall\ \exists$、谓词、函数与对象,用一句话表达无穷多命题。→ Lecture 16 · 逻辑 II:一阶逻辑
复习自测
题目
例:3 / 2 在 Python 2 与 Python 3 中分别是 1 与 1.5(同语法不同语义);$2+3$ 与 $3+2$ 都指 $5$(不同语法同语义)。推理规则可以只做符号操作是因为我们另行证明了可靠性:规则的每次应用都保持”前提的模型集 ⊆ 结论的模型集”,所以符号层的推导链条在语义层步步为真。没有可靠性保证的符号操作只是乱涂乱画。
题目
(⟹)矛盾即 $\mathcal{M}(\text{KB} \cup \{f\}) = \varnothing$:没有模型同时满足 KB 与 $f$,故 KB 的每个模型都不满足 $f$,即都满足 $\neg f$,即 $\mathcal{M}(\text{KB} \cup \{\neg f\}) = \mathcal{M}(\text{KB})$,这就是 $\text{KB} \models \neg f$。(⟸)反向逐步倒回去即可。这个等价是”蕴含归约到可满足性”的桥梁:$\text{KB} \models f \iff \text{KB} \cup \{\neg f\}$ 不可满足。
题目
枚举 4 个模型:$\text{Rain} \vee \text{Wet}$ 排除 $(F,F)$,$\neg\text{Rain}$ 排除 $(T,\cdot)$,故 $\mathcal{M}(\text{KB}) = \{(\text{Rain}=F, \text{Wet}=T)\}$,单点集。加入 Wet 后模型集不变 ⟹ 蕴含 ⟹ 回答 Yes。(这其实是一次隐式的归结:$\text{Rain} \vee \text{Wet}$ 与 $\neg\text{Rain}$ 归结出 $\text{Wet}$。)
题目
一次可满足性检查只输出 1 比特(可满足/不可满足),而 Ask 有三种结果(Yes/No/不知道),信息论上至少要 2 比特。流程:先查 $\text{KB} \cup \{f\}$——不可满足则矛盾(No);再查 $\text{KB} \cup \{\neg f\}$——不可满足则蕴含(Yes);两者都可满足则或然(不知道)。两次调用的顺序可以交换,结论不变。
题目
例:KB $= \{\text{Rain}\}$,$f = \text{Rain} \vee \text{Wet}$。语义上 KB 的唯一约束是 Rain 为真,此时 $\text{Rain} \vee \text{Wet}$ 必真,故 $\text{KB} \models f$;但 modus ponens 需要”$p$ 与 $p \to q$”的模式,KB 里没有任何蕴涵式可用,推不出这个析取。这说明 modus ponens 可靠但不完备。补救:换用归结 + 反证法(对 $\text{KB} \cup \{\neg f\}$ 反驳),即可覆盖全部命题逻辑蕴含。
参考资料
- 💻 propositional_logic.py — 本讲源码(Z3 + 解释函数 + Ask/Tell)
- 📖 CS221 Logic 模块(Autumn 2023 讲义)
- 📖 Russell & Norvig, Artificial Intelligence: A Modern Approach — 第 7 章 Logical Agents(命题逻辑、蕴含、归结、可靠性与完备性)
- 🔗 Z3 Theorem Prover — 本讲用来做模型检查的 SAT/SMT 求解器
- 🔗 The Z3 Guide(在线教程) — 交互式学习 SAT/SMT
- 🌐 CS221 课程主页