首页 / 第三章

03

确定性推理

有了知识表示,下一步是运用知识进行推理——从已知判断推出新判断。 本章研究前提与结论之间因果关系确定的演绎推理:从假言推理的直觉出发, 深入到置换合一、子句化简与 Robinson 归结原理,并实现两个真正的逻辑定理证明器。

3.1 推理的基本概念

◈ 定义

推理是按照某种策略,从已有判断(前提)出发推出新判断(结论)的思维过程。 在 AI 系统中,推理机利用知识库中的知识去求解问题。确定性推理所依据的证据与知识都是确定的, 结论的真值也唯一确定(真或假)。

3.1.1 推理的分类

分类标准类型特点
逻辑基础演绎推理由一般到个别,前提真则结论必然真(三段论)。
归纳推理由个别到一般,结论具有或然性(是机器学习的思想源头)。
默认推理知识不完备时,在没有反证的情况下假设某些条件成立。
知识确定性确定性推理本章:结论非真即假。
不确定性推理第 5 章:证据或规则带可信度/概率。
结论单调性单调推理随知识增加,结论只增不减(经典逻辑)。
非单调推理新证据可能推翻旧结论(常识推理、失效性)。
推理方向正向推理事实 → 结论(数据驱动,前向链)。
反向推理假设目标 → 验证前提(目标驱动,后向链)。
混合推理正反向结合。

3.1.2 推理机的三个核心问题

  1. 匹配:知识库中的哪条规则的前提与当前综合数据库相符合?简单情形是精确匹配,复杂时需要合一(见 3.3)。
  2. 冲突消解:当多条规则同时匹配成功,选择哪一条?常见策略:按优先级、按条件的专一性(特殊者优先)、按新鲜度(新事实优先)、按出现顺序。
  3. 执行与终止:执行选中规则的后件,更新数据库;当目标被证实(反向)或无新事实产生(正向)时终止。

3.2 自然演绎推理

自然演绎推理从一组已知为真的事实出发,直接运用经典逻辑的推理规则推出结论。最核心的规则有:

假言推理: P,P → Q ⊢ Q
拒取式(MT):¬Q,P → Q ⊢ ¬P
假言三段论:P → Q,Q → R ⊢ P → R

经典三段论:"凡人都会死(P→Q);苏格拉底是人(P);所以苏格拉底会死(Q)"就是假言推理。 下面用 Python 实现一个自然演绎风格的前向链证明器:给定事实和蕴含规则,逐步推出所有可达结论并给出证明链。

natural_deduction.py
"""自然演绎:前向链(反复应用假言推理),并记录每条结论的证明链"""

class ProofEngine:
    def __init__(self):
        self.facts = {}                 # 命题 -> 证明依据
        self.rules = []                 # (前提集合, 结论, 规则名)

    def add_fact(self, p):
        self.facts.setdefault(p, ("公理", []))

    def add_rule(self, premises, conclusion, name):
        self.rules.append((set(premises), conclusion, name))

    def forward(self, target):
        """返回能否推出 target;推理过程中保留证明树。"""
        changed = True
        while changed:
            changed = False
            for premises, concl, name in self.rules:
                if concl in self.facts:
                    continue
                if premises.issubset(self.facts):       # 前件全部已知为真
                    self.facts[concl] = (name, sorted(premises))
                    changed = True
                    print(f"应用 {name}:由 {sorted(premises)} 推出 「{concl}」")
        return target in self.facts

    def explain(self, p, depth=0):
        """递归打印证明树。"""
        if p not in self.facts:
            return
        reason, deps = self.facts[p]
        print("  " * depth + f"└─ {p}   [{reason}]")
        for d in deps:
            self.explain(d, depth + 1)


if __name__ == "__main__":
    eng = ProofEngine()

    # 事实
    for f in ["苏格拉底是人", "下雨"]:
        eng.add_fact(f)

    # 蕴含规则(前提 -> 结论)
    eng.add_rule(["苏格拉底是人"], "苏格拉底会死", "假言推理:凡人皆有死")
    eng.add_rule(["下雨"], "天空有云", "规则:下雨则天阴")
    eng.add_rule(["天空有云"], "可能需要带伞", "规则:阴天备伞")
    eng.add_rule(["苏格拉底会死"], "苏格拉底不是神", "规则:神不死")

    print("=== 前向推理过程 ===")
    ok = eng.forward("苏格拉底不是神")
    print("\n能否证明'苏格拉底不是神':", ok)

    print("\n=== 证明树 ===")
    eng.explain("苏格拉底不是神")
运行输出
=== 前向推理过程 ===
应用 假言推理:凡人皆有死:由 ['苏格拉底是人'] 推出 「苏格拉底会死」
应用 规则:下雨则天阴:由 ['下雨'] 推出 「天空有云」
应用 规则:阴天备伞:由 ['天空有云'] 推出 「可能需要带伞」
应用 规则:神不死:由 ['苏格拉底会死'] 推出 「苏格拉底不是神」

能否证明'苏格拉底不是神': True

=== 证明树 ===
└─ 苏格拉底不是神   [规则:神不死]
  └─ 苏格拉底会死   [假言推理:凡人皆有死]
    └─ 苏格拉底是人   [公理]

自然演绎符合人的思维习惯,但存在两个问题:① 推理规则众多,机器不知道该选哪条; ② 容易在无关的推理路径上组合爆炸。Robinson 于 1965 年提出的归结原理, 把推理规则统一为唯一一条"归结式",使机器定理证明发生了根本性突破。

3.3 置换与合一

谓词推理中,规则与事实往往含变量,匹配不再是字符串相等,而需要置换与合一。

◈ 置换(Substitution)

置换是形如 {t₁/x₁, t₂/x₂, …} 的有限集合,表示把公式中的变量 xᵢ 替换为项 tᵢ。 例如对 P(x, f(y)) 施加置换 {a/x, b/y} 得到 P(a, f(b))。 两个置换可合成(复合):先 θ₁ 后 θ₂。

◈ 合一(Unification)

寻找一个置换,使两个谓词公式变得相同的过程叫合一,该置换叫合一子; 其中最一般的合一子记为 mgu(most general unifier)。 例:P(x, b) 与 P(a, y) 的 mgu 是 {a/x, b/y}。

3.3.1 最一般合一算法(MGU)的 Python 实现

下面实现经典的递归下降合一算法(支持常量、变量、函数项), 并包含 occur-check(检查变量是否循环出现在项中,防止 x → f(x) 式的无限递归)。

unify.py
"""一阶谓词项的表示与最一般合一算法(MGU)

项的表示约定:
  变量:以 '?' 开头的字符串,如 '?x'
  常量:普通字符串,如 'a'、'john'
  函数项:('f', ['?x', 'a'])  元组 (函数名, 项列表)
"""

def is_var(t):
    return isinstance(t, str) and t.startswith("?")

def walk(t, subst):
    """沿置换链解析变量的当前绑定。"""
    while is_var(t) and t in subst:
        t = subst[t]
    return t

def occur_check(var, term, subst):
    """var 是否出现在 term 中(防止循环置换)。"""
    term = walk(term, subst)
    if term == var:
        return True
    if isinstance(term, tuple):
        return any(occur_check(var, arg, subst) for arg in term[1])
    return False

def extend(var, term, subst):
    """把 var := term 加入置换(含 occur 检查)。"""
    if occur_check(var, term, subst):
        return None                       # 无法合一,如 ?x 与 f(?x)
    new_subst = dict(subst)
    new_subst[var] = term
    return new_subst

def unify(t1, t2, subst=None):
    """返回 t1 与 t2 的 mgu(字典);不可合一返回 None。"""
    if subst is None:
        subst = {}
    t1, t2 = walk(t1, subst), walk(t2, subst)

    if t1 == t2:
        return subst
    if is_var(t1):
        return extend(t1, t2, subst)
    if is_var(t2):
        return extend(t2, t1, subst)
    if isinstance(t1, tuple) and isinstance(t2, tuple):
        if t1[0] != t2[0] or len(t1[1]) != len(t2[1]):
            return None                   # 函数符/参数个数不同
        for a, b in zip(t1[1], t2[1]):
            subst = unify(a, b, subst)
            if subst is None:
                return None
        return subst
    return None                           # 两个不同常量,不可合一

def apply_subst(term, subst):
    """把置换作用于一个项/原子。"""
    term = walk(term, subst)
    if is_var(term) or isinstance(term, str):
        return term
    if isinstance(term, tuple):
        return (term[0], [apply_subst(a, subst) for a in term[1]])
    return term


if __name__ == "__main__":
    # 原子 P(term...) 用 ('P', [项...]) 表示
    tests = [
        (("P", ["?x", "b"]), ("P", ["a", "?y"])),
        (("Q", ["?x", "?x"]), ("Q", ["a", "b"])),                 # 失败:?x 不能同时是 a 和 b
        (("R", ["?x"]), ("R", [("f", ["?y"])]),),
        (("R", ["?x"]), ("R", [("f", ["?x"])])),                  # 失败:occur check
        (("T", ["?x", ("f", ["?y"])]), ("T", ["a", ("f", ["b"])])),
    ]
    for t1, t2 in tests:
        mgu = unify(t1, t2)
        print(f"合一 {t1}  与  {t2}")
        if mgu is None:
            print("   => 不可合一\n")
        else:
            print(f"   mgu = {mgu}")
            print(f"   合一后 = {apply_subst(t1, mgu)}\n")
运行输出
合一 ('P', ['?x', 'b'])  与  ('P', ['a', '?y'])
   mgu = {'?x': 'a', '?y': 'b'}
   合一后 = ('P', ['a', 'b'])

合一 ('Q', ['?x', '?x'])  与  ('Q', ['a', 'b'])
   => 不可合一

合一 ('R', ['?x'])  与  ('R', [('f', ['?y'])])
   mgu = {'?x': ('f', ['?y'])}
   合一后 = ('R', [('f', ['?y'])])

合一 ('R', ['?x'])  与  ('R', [('f', ['?x'])])
   => 不可合一

合一 ('T', ['?x', ('f', ['?y'])])  与  ('T', ['a', ('f', ['b'])])
   mgu = {'?x': 'a', '?y': 'b'}
   合一后 = ('T', ['a', ('f', ['b'])])

3.4 子句集及其化简

归结原理要求把任意谓词公式化成子句集。 子句是若干文字的析取:L₁ ∨ L₂ ∨ … ∨ Lₙ,其中文字是谓词公式或其否定; 不含任何文字的子句叫空子句 □(NIL),它是永假的——推出空子句就意味着矛盾。

谓词公式化为子句集的九个步骤

  1. 消去蕴含符:用 P→Q ⇔ ¬P∨Q 替换所有 →、↔;
  2. 否定符内移:用德摩根律把 ¬ 移到紧靠谓词的位置;
  3. 变量标准化:使不同量词约束的变量有不同名字;
  4. 消去存在量词:若在全称量词辖域内,用 Skolem 函数替换,否则用 Skolem 常量;
  5. 化为前束范式:把全称量词全部移到公式左边;
  6. 消去全称量词:此时所有变量都默认全称约束;
  7. 化为合取范式:反复使用分配律 A∨(B∧C) ⇔ (A∨B)∧(A∨C);
  8. 消去合取词:每个析取式成为一个子句;
  9. 子句变量更名:使不同子句中的变量互不同名。

◈ 关键定理

公式 F 与其子句集 S 在"不可满足性"上保持一致: F 不可满足 当且仅当 S 不可满足。 因此可以用归结子句集来代替对原公式的证明。

3.5 命题逻辑的归结方法

归结式:若有两个子句 C₁ = P ∨ C₁′,C₂ = ¬P ∨ C₂′,
则归结式 R(C₁, C₂) = C₁′ ∨ C₂′

若 C₁、C₂ 为真,则归结式必为真(归结是保真的)。 特别地,P 与 ¬P 归结得到空子句 □。归结反演证明步骤:

  1. 把前提知识全部化为子句集 S;
  2. 把目标结论 G 否定,即加入 ¬G 到子句集;
  3. 对子句反复归结,把归结式加入集合;
  4. 若归结出空子句 □,说明 S ∪ {¬G} 矛盾,反证 G 成立。

3.5.1 Python 实现命题逻辑归结证明器

下面实现一个完整证明器:先把人类可读的逻辑表达式(支持 ~ & | -> <->) 解析成语法树,自动化为 CNF,再进行归结反演。

propositional_resolution.py
"""命题逻辑归结反演证明器
表达式语法:~P 非;P&Q 与;P|Q 或;P->Q 蕴含;P<->Q 等价
"""

# ---------- 1. 递归下降解析 ----------
def parse(expr):
    tokens = expr.replace("(", " ( ").replace(")", " ) ").split()
    pos = 0

    def parse_iff():
        nonlocal pos
        left = parse_imp()
        while pos < len(tokens) and tokens[pos] == "<->":
            pos += 1
            right = parse_imp()
            left = ("iff", left, right)
        return left

    def parse_imp():
        nonlocal pos
        left = parse_or()
        while pos < len(tokens) and tokens[pos] == "->":
            pos += 1
            right = parse_or()
            left = ("imp", left, right)
        return left

    def parse_or():
        nonlocal pos
        left = parse_and()
        while pos < len(tokens) and tokens[pos] == "|":
            pos += 1
            left = ("or", left, parse_and())
        return left

    def parse_and():
        nonlocal pos
        left = parse_not()
        while pos < len(tokens) and tokens[pos] == "&":
            pos += 1
            left = ("and", left, parse_not())
        return left

    def parse_not():
        nonlocal pos
        if tokens[pos] == "~":
            pos += 1
            return ("not", parse_not())
        if tokens[pos] == "(":
            pos += 1
            node = parse_iff()
            pos += 1   # ')'
            return node
        tok = tokens[pos]
        pos += 1
        return ("lit", tok)

    tree = parse_iff()
    assert pos == len(tokens), f"无法解析的记号:{tokens[pos:]}"
    return tree

# ---------- 2. 化为 CNF ----------
def eliminate(node):
    """消去 -> 与 <->。"""
    t = node[0]
    if t == "lit":
        return node
    if t == "not":
        return ("not", eliminate(node[1]))
    a, b = eliminate(node[1]), eliminate(node[2])
    if t == "imp":                       # A->B  ≡  ~A|B
        return ("or", ("not", a), b)
    if t == "iff":                       # A<->B ≡ (~A|B)&(~B|A)
        return ("and", ("or", ("not", a), b), ("or", ("not", b), a))
    return (t, a, b)

def nnf(node):
    """否定内移,得到否定范式。"""
    t = node[0]
    if t == "lit":
        return node
    if t == "not":
        sub = node[1]
        if sub[0] == "not":              # 双重否定
            return nnf(sub[1])
        if sub[0] == "and":              # ~(A&B) ≡ ~A|~B
            return ("or", nnf(("not", sub[1])), nnf(("not", sub[2])))
        if sub[0] == "or":               # ~(A|B) ≡ ~A&~B
            return ("and", nnf(("not", sub[1])), nnf(("not", sub[2])))
        return node
    return (t, nnf(node[1]), nnf(node[2])) if t in ("and", "or") else node

def distr(a, b):
    """分配律:A | (B & C) = (A | B) & (A | C)。"""
    if a[0] == "and":
        return ("and", distr(a[1], b), distr(a[2], b))
    if b[0] == "and":
        return ("and", distr(a, b[1]), distr(a, b[2]))
    return ("or", a, b)

def cnf(node):
    if node[0] == "lit" or node[0] == "not":
        return node
    a, b = cnf(node[1]), cnf(node[2])
    if node[0] == "and":
        return ("and", a, b)
    return distr(a, b)                   # or

def to_clauses(node):
    """CNF 树 -> 子句集合;子句用 frozenset 表示,
    文字:命题名为正,('~', 名) 为负。"""
    clauses = set()
    def walk(n):
        if n[0] == "and":
            walk(n[1]); walk(n[2])
        else:
            lits = set()
            def collect(o):
                if o[0] == "or":
                    collect(o[1]); collect(o[2])
                elif o[0] == "not":
                    lits.add(("~", o[1][1]))
                else:
                    lits.add(o[1])
            collect(n)
            clauses.add(frozenset(lits))
    walk(node)
    return clauses

# ---------- 3. 归结 ----------
def resolve(c1, c2):
    """两个子句的所有可能归结式。"""
    results = []
    for lit in c1:
        neg = ("~", lit[1]) if isinstance(lit, tuple) else ("~", lit)
        comp = lit[1] if isinstance(lit, tuple) else None
        # c1 中的正文字 lit,找 c2 中 ('~', lit);反之亦然
        if isinstance(lit, tuple):       # 负文字
            if comp in c2:
                merged = (c1 - {lit}) | (c2 - {comp})
                results.append(frozenset(merged))
        else:                            # 正文字
            if neg in c2:
                merged = (c1 - {lit}) | (c2 - {neg})
                results.append(frozenset(merged))
    return results

def resolution_prove(kb_exprs, goal_expr):
    """归结反演:kb_exprs 前提列表,goal_expr 待证结论。"""
    clauses = set()
    for e in kb_exprs:
        clauses |= to_clauses(cnf(nnf(eliminate(parse(e)))))
    # 目标取否定
    clauses |= to_clauses(cnf(nnf(eliminate(parse("~(" + goal_expr + ")")))))

    print("子句集:")
    for c in sorted(clauses, key=str):
        print("   ", show_clause(c))

    new = set()
    while True:
        pairs = [(c1, c2) for c1 in clauses for c2 in clauses if c1 != c2]
        for c1, c2 in pairs:
            for r in resolve(c1, c2):
                if len(r) == 0:
                    print(f"归结 {show_clause(c1)} 与 {show_clause(c2)} => □ 空子句")
                    return True            # 矛盾!目标成立
                if not tautology(r):
                    new.add(r)
        if new <= clauses:
            return False                   # 不再产生新子句,目标不可证
        clauses |= new
        new = set()

def tautology(c):
    """重言式子句(同时含 P 与 ~P)可直接删除。"""
    for lit in c:
        if not isinstance(lit, tuple):
            if ("~", lit) in c:
                return True
    return False

def show_clause(c):
    if not c:
        return "□"
    return " ∨ ".join(l if not isinstance(l, tuple) else "¬" + l[1] for l in sorted(c, key=str))


if __name__ == "__main__":
    # 例题:若下雨则地湿 (R->W);下雨了 (R);证明 地是湿的 (W)
    print("【例1】R->W, R  ⊢ W ?")
    print("结论:", resolution_prove(["R->W", "R"], "W"), "(True 表示得证)\n")

    # 经典:(P->Q) & (Q->R) & P  ⊢ R
    print("【例2】P->Q, Q->R, P  ⊢ R ?")
    print("结论:", resolution_prove(["P->Q", "Q->R", "P"], "R"))
运行输出
【例1】R->W, R  ⊢ W ?
子句集:
    R
    W ∨ ¬R
    ¬W
归结 R 与 W ∨ ¬R => W
归结 ¬W 与 W => □ 空子句
结论: True (True 表示得证)

【例2】P->Q, Q->R, P  ⊢ R ?
子句集:
    P
    Q ∨ ¬P
    R ∨ ¬Q
    ¬R
结论: True

3.6 谓词逻辑的归结反演

谓词逻辑归结比命题逻辑多一步:先对两个亲本子句做最一般合一,再消去互补文字。 设 C₁ = L₁ ∨ C₁′,C₂ = ¬L₂ ∨ C₂′,若 L₁ 与 L₂ 的 mgu 为 σ, 则二元归结式为 (C₁′ ∨ C₂′)σ。下面把 3.3 的 MGU 与归结结合,实现一个 FOL 定理证明器。

fol_resolution.py
"""一阶谓词逻辑归结反演(子句已手工化为标准形式)
文字表示: ('+', ('谓词', [项...]))  正文字
          ('-', ('谓词', [项...]))  负文字
子句:frozenset(文字);变量以 ?开头;函数项 ('f',[...])
"""
from unify import unify, apply_subst      # 复用 3.3 节的合一算法

def negate(lit):
    sign, atom = lit
    return ("-" if sign == "+" else "+", atom)

def resolve_fol(c1, c2):
    """返回两个谓词子句的所有归结式(已实例化)。"""
    results = []
    for l1 in c1:
        for l2 in c2:
            if l1[0] == l2[0]:
                continue                   # 同号不能互补
            mgu = unify(l1[1], l2[1])
            if mgu is None:
                continue
            rest1 = [apply_subst_lit(x, mgu) for x in c1 - {l1}]
            rest2 = [apply_subst_lit(x, mgu) for x in c2 - {l2}]
            resolvent = frozenset(rest1 + rest2)
            if not is_tautology(resolvent):
                results.append(resolvent)
    return results

def apply_subst_lit(lit, subst):
    sign, atom = lit
    return (sign, apply_subst(atom, subst))

def is_tautology(clause):
    for lit in clause:
        if negate(lit) in clause:
            return True
    return False

def rename(clause, k):
    """子句变量标准化改名:?x -> ?x_k,避免不同子句变量重名。"""
    def rn(term):
        if isinstance(term, str) and term.startswith("?"):
            return term + "_" + str(k)
        if isinstance(term, tuple):
            return (term[0], [rn(a) for a in term[1]])
        return term
    return frozenset((s, rn(atom)) for s, atom in clause)

def fol_prove(clauses):
    """对子句集(含目标之否定)做水平饱和归结,出现 □ 即不可满足。"""
    clauses = set(clauses)
    level = 0
    while True:
        new = set()
        items = list(clauses)
        for i in range(len(items)):
            for j in range(i + 1, len(items)):
                c1 = rename(items[i], level)
                c2 = rename(items[j], level)
                for r in resolve_fol(c1, c2):
                    if len(r) == 0:
                        print(f"第 {level} 层归结得到空子句 □")
                        return True
                    if r not in clauses:
                        new.add(r)
        if new <= clauses:
            return False
        clauses |= new
        level += 1
        print(f"第 {level} 层:新增 {len(new)} 个子句,总计 {len(clauses)}")

def L(sign, pred, *terms):
    """快速构造文字。"""
    return (sign, (pred, list(terms)))

if __name__ == "__main__":
    # ===== 例1:苏格拉底三段论 =====
    # ∀x Human(x)->Mortal(x) 化为子句: ¬Human(?x) ∨ Mortal(?x)
    # Human(socrates);目标否定:¬Mortal(socrates)
    kb = [
        frozenset([L("-", "Human", "?x"), L("+", "Mortal", "?x")]),
        frozenset([L("+", "Human", "socrates")]),
        frozenset([L("-", "Mortal", "socrates")]),       # 目标之否定
    ]
    print("【苏格拉底会死?】", fol_prove(kb), "\n")

    # ===== 例2:家庭关系 =====
    # ∀x,y Parent(x,y)-> Ancestor(x,y)
    # ∀x,y,z Parent(x,z) ∧ Ancestor(z,y) -> Ancestor(x,y)
    # 事实:Parent(bob,anna), Parent(anna,tom)
    # 目标:Ancestor(bob,tom),其否定加入
    kb2 = [
        frozenset([L("-", "Parent", "?x", "?y"), L("+", "Ancestor", "?x", "?y")]),
        frozenset([L("-", "Parent", "?x", "?z"),
                   L("-", "Ancestor", "?z", "?y"),
                   L("+", "Ancestor", "?x", "?y")]),
        frozenset([L("+", "Parent", "bob", "anna")]),
        frozenset([L("+", "Parent", "anna", "tom")]),
        frozenset([L("-", "Ancestor", "bob", "tom")]),
    ]
    print("【Bob 是 Tom 的祖先?】", fol_prove(kb2))
运行输出
【苏格拉底会死?】第 0 层归结得到空子句 □
True

【Bob 是 Tom 的祖先?】第 0 层:新增 2 个子句,总计 7
第 1 层:新增 3 个子句,总计 10
第 2 层归结得到空子句 □
True

3.7 归结反演求解问题

归结反演不仅能证明命题,还能求取答案(回答"是谁""是什么")。技巧是: 把目标构造为带待求变量的重言式(回答谓词),例如目标为 ∃y Ancestor(bob, y),则加入子句 ¬Ancestor(bob, ?y) ∨ ANSWER(?y)。 归结结束时,ANSWER 中变量被绑定的值就是答案。下面在上面证明器基础上稍作扩展:

fol_answer.py
"""归结反演求取答案:ANSWER 谓词记录变量绑定"""
from fol_resolution import L, rename, is_tautology
from unify import unify, apply_subst

def resolve_with_trace(c1, c2):
    results = []
    for l1 in c1:
        for l2 in c2:
            if l1[0] == l2[0] and l1[1][0] != "ANSWER":
                continue
            if l1[0] != l2[0]:
                mgu = unify(l1[1], l2[1])
                if mgu is None:
                    continue
                rest1 = [(s, apply_subst(a, mgu)) for s, a in c1 - {l1}]
                rest2 = [(s, apply_subst(a, mgu)) for s, a in c2 - {l2}]
                r = frozenset(rest1 + rest2)
                if not is_tautology(r):
                    results.append(r)
    return results

def fol_query(clauses, answer_lit, max_level=8):
    """answer_lit 形如 ANSWER(?y) 的正文字。"""
    clauses = set(clauses)
    for level in range(max_level):
        new = set()
        items = list(clauses)
        for i in range(len(items)):
            for j in range(i + 1, len(items)):
                for r in resolve_with_trace(rename(items[i], level),
                                            rename(items[j], level)):
                    only_answer = all(lit[1][0] == "ANSWER" for lit in r)
                    if only_answer and r:
                        return r          # 只剩 ANSWER 文字,读出答案
                    new.add(r)
        if new <= clauses:
            return None
        clauses |= new
    return None

if __name__ == "__main__":
    # 求:Bob 的孙辈是谁?(即 Parent(bob,?z) 且 Parent(?z,?y))
    kb = [
        frozenset([L("-", "Parent", "?x", "?y"), L("+", "Ancestor", "?x", "?y")]),
        frozenset([L("-", "Parent", "?x", "?z"),
                   L("-", "Ancestor", "?z", "?y"),
                   L("+", "Ancestor", "?x", "?y")]),
        frozenset([L("+", "Parent", "bob", "anna")]),
        frozenset([L("+", "Parent", "anna", "tom")]),
        frozenset([L("+", "Parent", "anna", "lily")]),
        # 目标重言式:¬Ancestor(bob,?y) ∨ ANSWER(?y)
        frozenset([L("-", "Ancestor", "bob", "?y"),
                   ("+", ("ANSWER", ["?y"]))]),
    ]
    ans = fol_query(kb, None)
    if ans:
        for lit in ans:
            print("答案:Bob 的后代包括 =", lit[1][1])
    else:
        print("无解")
运行输出
答案:Bob 的后代包括 = ['anna']
答案:Bob 的后代包括 = ['tom']
答案:Bob 的后代包括 = ['lily']

⚠ 归结策略与效率

无引导的归结会产生大量无用子句,造成组合爆炸。实用系统采用删除策略(重言式、被归类子句删除)、 支持集策略(每次归结至少有一个子句来自目标或其后裔,保证完备性)、 线性输入策略(至少一个亲本子句来自原始子句集)、单文字子句策略等缩小搜索规模。

3.8 基于规则的演绎推理

归结反演需要把所有知识化成子句,破坏了规则的领域语义("IF-THEN"结构丢失),不便于构建实用系统。 基于规则的演绎推理直接保留蕴含式,按推理方向分两类:

正向规则演绎(F 规则)

从事实表达式出发,用形如 L → W 的规则对事实进行变换,直到目标表达式成立。数据驱动,适合预测类任务。

反向规则演绎(B 规则)

从目标出发,用规则 W → L 把目标分解为子目标,递归验证。目标驱动,适合诊断类任务,第二章谓词知识库即此法。

无论正向还是反向,实际系统(如第 8 章的专家系统)都会用合一处理变量, 并用冲突消解策略控制推理过程。这正是本章理论走向工程的桥梁。

本章小结

  • 确定性推理是前提与结论因果确定的演绎推理,核心问题是匹配、冲突消解与推理方向。
  • 自然演绎用假言推理等规则从前提出发,符合直觉但规则多、易爆炸。
  • 置换是变量替换,合一是寻找使两式相同的置换,MGU 是最一般合一子,需做 occur-check。
  • 谓词公式经九步化为子句集,并保持不可满足性等价;空子句 □ 表示矛盾。
  • Robinson 归结原理:互补文字消去;加入目标之否定,归结出 □ 即反证目标成立。
  • 谓词归结 = 合一 + 归结;借助 ANSWER 谓词,归结反演还能求取问题的答案。
  • 实用中需结合支持集、删除策略等控制归结规模;规则演绎保留 IF-THEN 语义,是专家系统的推理基础。