# Lecture 15 · 逻辑 I：命题逻辑

> **CS221: Artificial Intelligence — Principles and Techniques** · Stanford · Autumn 2025
> 📅 Nov 10 · 💻 [`propositional_logic.py`](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/propositional_logic.py)

---

## 从概率推理到逻辑推理

**上一单元（贝叶斯网络，[[Lecture 12 · 贝叶斯网络 I：建模与推断]] → [[Lecture 14 · 贝叶斯网络 III：参数学习]]）** 用**概率**表示对世界的不确定信念：先用联合分布与条件独立建模，再做推断 $P(\text{query} \mid \text{evidence})$。概率的强项是处理**不确定性**——真实世界是嘈杂的、随机的，我们的观测也不完整。

**本单元（逻辑推理）** 换一种表示知识、进行推理的语言：**逻辑（logic）**。本讲先讲最简单的**命题逻辑（propositional logic）**，下一讲 [[Lecture 16 · 逻辑 II：一阶逻辑]] 再升级到一阶逻辑。逻辑的强项不在处理随机性，而在**表达力（expressivity）**——用非常紧凑的公式表示复杂的知识，并支持精确的推理。

```mermaid
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$。这就是**符号推理 / 逻辑推理**的原型——不逐个枚举可能的世界，而是**操作符号**，几步就把结论推出来。

> [!tip] 直觉
> 逻辑推理的威力在于：面对天文数字般的可能世界（$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、归结……（纯操作符号） |

> [!tip] 直觉
> 为什么要把三层严格分开？打个编程的比方：语法像 parser（判断程序合不合法），语义像解释器（跑出程序的结果），推理规则像编译器的等价变换（不运行程序就改写它，且保证结果不变）。分开的收益是：语义计算（枚举所有模型）代价可能是指数级，而推理规则只做符号操作，可以快得多——前提是我们能证明这些符号操作**忠于语义**（第 8 节的可靠性与完备性）。

### 1.2 为什么不用自然语言？

因为自然语言太**滑溜（slippery）**了：同一个词在不同语境含义漂移，推理很容易翻车。

> [!warning] 一个经典的自然语言谬误
> - 一分钱（penny）比什么都没有（nothing）好。
> - 什么都没有（nothing）比世界和平（world peace）好。
> - 所以……一分钱比世界和平好？？
>
> 问题出在 "nothing" 在两句话里含义漂移：第一句是"空无一物"，第二句是"没有任何事物（比世界和平好）"。符号相同、语义不同，三段论就失效了。

> [!example] 合法的推理长什么样
> 一角钱比五分钱好；五分钱比一分钱好；**所以**一角钱比一分钱好。这里 "better than" 全程含义一致，且具有传递性，推理才站得住。

形式语言（编程语言、逻辑语言）通过精确的语法和语义消除了这种歧义。命题逻辑就是**最简单的**一种形式逻辑语言。

### 1.3 语法 vs 语义：不要混淆

**语法**是**符号串**本身，**语义**是它指向的**含义**，两者可以脱钩：

| 关系 | 例子 |
| :--- | :--- |
| **不同语法，相同语义** | $2 + 3$ 和 $3 + 2$，语义都是 $5$ |
| **相同语法，不同语义** | `3 / 2` 在 Python 2.7 里是 `1`，在 Python 3 里是 `1.5` |

> [!note] 本质
> 语法管"怎么写"，语义管"什么意思"，推理规则管"怎么算"。学逻辑就是把这三层严格分开，再用解释函数（语法→语义）和可靠性/完备性（推理规则→语义）把它们缝起来。

**源码骨架**（`propositional_logic.py` 用 Z3 的表达式类型作为底层）：

```python
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）。

```python
# 基础情形：命题符号（原子公式）
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
```

> [!tip] 直觉
> 归纳定义的妙处：任意复杂的公式都是一棵**语法树**——叶子是命题符号、内部节点是联结词。这棵树正是下一节解释函数递归求值的结构基础：语法怎么归纳定义，语义就怎么递归计算。

### 2.2 合法与非法公式

```python
# ✅ 合法公式
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. 命题逻辑：语义

公式本身只是符号（语法），还没有含义（语义）。语义部分要引入一连串概念，源码的组织顺序就是最好的学习路线：

```mermaid
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$ 个模型——每个符号独立地取真或假。

```python
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)
]
```

> [!note]
> "模型"这个词在机器学习里指训练出来的函数，在逻辑里却指"世界的一种可能状态"，两个含义完全不同。逻辑里的模型对应贝叶斯网络里的一次**完整赋值**（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` 按参数个数分派联结词：

```python
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}")
```

> [!example] 手算一遍
> 公式 $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}$。

> [!warning] 易错点
> $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` 检查即可：

```python
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 个模型
```

> [!tip] 直觉
> 一个（小小的）公式**紧凑地**表示了一个（可能很大的）模型集合——这正是逻辑"表达力"的来源。`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)$$

```python
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 相同
```

> [!tip] 直觉
> 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)$ 的交集，交集不可能比原集合大。

> [!tip] 直觉
> 随着知识增加，我们**缩小**了可能身处的世界范围。问题只剩一个——**这次收缩了多少？** 完全没缩、缩成空集、缩掉一部分，三种情况恰好对应三个核心概念。

```mermaid
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$ 带来了真正的新信息——排除了一部分世界，但没有全排除。

```python
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$$

> [!note] 为什么这个等价成立
> "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\})$）：

```python
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"
```

> [!tip] 直觉
> 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 比特），所以需要**两次调用**：

```mermaid
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** 求解器做模型检查：

```python
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) 中的一个模型
```

> [!note] 底层：SAT 求解器
> 命题逻辑的可满足性就是著名的 **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}$$

> [!example] 归结反证法：证明 $\{\text{Rain},\ \text{Rain} \to \text{Wet}\} \models \text{Wet}$
> 1. 把 KB 与结论的否定转成 CNF 子句：$\{\text{Rain}\}$、$\{\neg\text{Rain} \vee \text{Wet}\}$（即 $\text{Rain} \to \text{Wet}$）、$\{\neg\text{Wet}\}$（结论的否定）。
> 2. 对 Wet 归结：$\neg\text{Rain} \vee \text{Wet}$ 与 $\neg\text{Wet}$ 得 $\neg\text{Rain}$。
> 3. 对 Rain 归结：$\text{Rain}$ 与 $\neg\text{Rain}$ 得**空子句** $\square$（矛盾）。
> 4. $\text{KB} \cup \{\neg\text{Wet}\}$ 不可满足 $\implies \text{KB} \models \text{Wet}$。∎

> [!warning] 易错点：归结的"完备"是反驳完备
> 精确地说，归结是**反驳完备（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$。

> [!warning] 易错点
> 推理规则只操作**语法**（符号模式匹配），**不碰语义**（不看任何模型）。它是纯粹的符号变换——这正是它比枚举 $2^n$ 个模型高效的原因，但也意味着必须另行证明它忠于语义（第 8 节）。

```mermaid
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：

1. 输入：一组推理规则 Rules，初始知识库 KB；
2. 重复直到 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$。

> [!example] 推得出与推不出
> 从 $\{\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 节）。

```mermaid
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（全部真相） |

> [!tip] 直觉
> **可靠 = 不说假话**（推出的绝不出错，但可能推不全）；**完备 = 不漏真话**（真的都能推到，但若规则太猛可能连假的也一起推出）。二者兼备时 $\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 求解如此重要的原因。

```mermaid
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. 总结

```mermaid
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：一阶逻辑]]

---

## 复习自测

> [!question]- Q1：「相同语法、不同语义」与「不同语法、相同语义」各举一例。推理规则明明只碰语法，为什么还能得出语义上正确的结论？
> 例：`3 / 2` 在 Python 2 与 Python 3 中分别是 `1` 与 `1.5`（同语法不同语义）；$2+3$ 与 $3+2$ 都指 $5$（不同语法同语义）。推理规则可以只做符号操作是因为我们**另行证明了可靠性**：规则的每次应用都保持"前提的模型集 ⊆ 结论的模型集"，所以符号层的推导链条在语义层步步为真。没有可靠性保证的符号操作只是乱涂乱画。

> [!question]- Q2：证明「KB 与 $f$ 矛盾 $\iff$ KB $\models \neg f$」。
> （⟹）矛盾即 $\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\}$ 不可满足。

> [!question]- Q3：算一算：KB $= \{\text{Rain} \vee \text{Wet},\ \neg\text{Rain}\}$，Ask[Wet] 的回答是什么？
> 枚举 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}$。）

> [!question]- Q4：为什么把 Ask 归约到可满足性需要两次 SAT 调用？一次为什么不够？先查哪一次？
> 一次可满足性检查只输出 1 比特（可满足/不可满足），而 Ask 有三种结果（Yes/No/不知道），信息论上至少要 2 比特。流程：先查 $\text{KB} \cup \{f\}$——不可满足则矛盾（No）；再查 $\text{KB} \cup \{\neg f\}$——不可满足则蕴含（Yes）；两者都可满足则或然（不知道）。两次调用的顺序可以交换，结论不变。

> [!question]- Q5：给出一个「KB $\models f$ 但只用 modus ponens 推不出 $f$」的例子，并说明这暴露了什么性质的缺失。
> 例：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](https://github.com/stanford-cs221/autumn2025-lectures/blob/main/propositional_logic.py) — 本讲源码（Z3 + 解释函数 + Ask/Tell）
- 📖 [CS221 Logic 模块（Autumn 2023 讲义）](https://stanford-cs221.github.io/autumn2023/modules/module.html#include=logic%2Foverview.js&mode=print6pp)
- 📖 Russell & Norvig, *Artificial Intelligence: A Modern Approach* — **第 7 章 Logical Agents**（命题逻辑、蕴含、归结、可靠性与完备性）
- 🔗 [Z3 Theorem Prover](https://github.com/Z3Prover/z3) — 本讲用来做模型检查的 SAT/SMT 求解器
- 🔗 [The Z3 Guide（在线教程）](https://microsoft.github.io/z3guide/) — 交互式学习 SAT/SMT
- 🌐 [CS221 课程主页](https://stanford-cs221.github.io/)
