首页 / 第八章

08

专家系统

专家系统是人工智能从实验室走向产业的第一个成功范式:它把人类专家的经验沉淀为知识库, 用推理机模拟专家的问题求解过程。本章构建三个可运行的系统—— 动物识别、目标驱动的假设验证、带可信度的医疗分诊——并实现"为什么"的解释能力。

8.1 专家系统概述

◈ 定义

专家系统(Expert System)是一种在特定领域内,运用领域专家的知识与推理能力, 解决原本需要人类专家才能解决的复杂问题的智能计算机程序系统。

专家系统的特点

对比项传统程序专家系统
处理对象数据知识
求解方式按固定算法启发式推理 + 搜索
知识与控制混在一起知识库与推理机分离
结果特征精确、唯一可能不精确、可解释、多假设
解释能力无有 Why / How 解释

里程碑式的系统

8.2 专家系统的结构

一个完整的专家系统通常由六部分组成:

知识库

存放领域知识(事实、规则),是专家能力的来源,与推理机分离。

综合数据库

又称工作存储器,存放当前问题的事实与推理中间结论。

推理机

调度知识进行推理的程序:匹配、冲突消解、执行(正向/反向/混合)。

解释机构

记录推理路径,回答用户"为什么问这个""结论怎么来的"。

知识获取机构

把专家知识转化为知识库内容(人工或半自动机器学习)。

人机接口

用户、领域专家与系统交互的界面。

8.3 专家系统的开发过程

  1. 需求分析与领域选择:选择边界清晰、专家知识可形式化、确有高价值的狭窄领域;
  2. 知识获取:通过访谈、案例分析从专家处提炼事实与规则(最困难的瓶颈环节);
  3. 知识表示与知识库设计:选定产生式、框架等表示法,设计规则结构;
  4. 推理机与原型实现:确定推理方向、冲突消解、不确定处理;
  5. 测试评估:用真实案例验证准确率与解释合理性,反复修订知识库;
  6. 维护与扩充:知识随业务持续更新。

8.4 实战一:正向推理动物识别专家系统

动物识别是经典教学案例。系统根据观察到的特征(毛发、食性、颜色斑纹……), 经 15 条产生式规则逐步推出动物类别,并给出完整推理链解释(How)。

animal_expert.py
"""动物识别专家系统:正向推理 + 冲突消解 + 推理链解释"""

class Rule:
    def __init__(self, rid, conditions, conclusion, desc):
        self.id = rid
        self.conditions = set(conditions)
        self.conclusion = conclusion
        self.desc = desc

class AnimalExpertSystem:
    def __init__(self):
        self.facts = set()
        self.rules = []
        self.trace = []          # 推理记录:(规则, 结论)

    def load_knowledge(self):
        R = [
            Rule("R1",  ["有毛发"], "哺乳动物", "有毛发的是哺乳动物"),
            Rule("R2",  ["有奶"], "哺乳动物", "有奶的是哺乳动物"),
            Rule("R3",  ["有羽毛"], "鸟类", "有羽毛的是鸟"),
            Rule("R4",  ["会飞", "会下蛋"], "鸟类", "会飞且会下蛋的是鸟"),
            Rule("R5",  ["吃肉"], "食肉动物", "吃肉的是食肉动物"),
            Rule("R6",  ["有犬齿", "有爪", "眼盯前方"], "食肉动物", "肉食性齿爪特征"),
            Rule("R7",  ["哺乳动物", "有蹄"], "有蹄类动物", "有蹄的哺乳动物"),
            Rule("R8",  ["哺乳动物", "反刍"], "有蹄类动物", "反刍的哺乳动物"),
            Rule("R9",  ["哺乳动物", "食肉动物", "黄褐色", "有暗斑点"], "金钱豹", "金钱豹识别规则"),
            Rule("R10", ["哺乳动物", "食肉动物", "黄褐色", "有黑色条纹"], "老虎", "老虎识别规则"),
            Rule("R11", ["有蹄类动物", "长脖子", "长腿", "有暗斑点"], "长颈鹿", "长颈鹿识别规则"),
            Rule("R12", ["有蹄类动物", "有黑色条纹"], "斑马", "斑马识别规则"),
            Rule("R13", ["鸟类", "长脖子", "长腿", "不会飞", "黑白二色"], "鸵鸟", "鸵鸟识别规则"),
            Rule("R14", ["鸟类", "会游泳", "不会飞", "黑白二色"], "企鹅", "企鹅识别规则"),
            Rule("R15", ["鸟类", "善飞"], "信天翁", "信天翁识别规则"),
        ]
        self.rules = R
        return self

    def run(self, observations):
        """正向推理:数据驱动,直到无新事实。"""
        self.facts = set(observations)
        fired = set()
        changed = True
        while changed:
            changed = False
            candidates = [r for r in self.rules
                          if r.id not in fired and r.conditions <= self.facts]
            if candidates:
                rule = sorted(candidates, key=lambda r: r.id)[0]   # 编号优先的冲突消解
                self.facts.add(rule.conclusion)
                fired.add(rule.id)
                self.trace.append(rule)
                changed = True

    def how(self, target):
        """解释目标结论是如何得到的。"""
        print(f"\n【How 解释】结论「{target}」的推理过程:")
        chain = [r for r in self.trace if r.conclusion == target]
        if not chain:
            print("  该结论未被推出(属于初始事实或证据不足)。")
            return
        r = chain[0]
        print(f"  由规则 {r.id}:{r.desc}")
        print(f"  因为满足条件:{sorted(r.conditions)}")
        for cond in r.conditions:
            src = [x for x in self.trace if x.conclusion == cond]
            if src:
                print(f"    其中「{cond}」由规则 {src[0].id} 推出:{src[0].desc}")
            else:
                print(f"    其中「{cond}」是你提供的观察事实")

if __name__ == "__main__":
    es = AnimalExpertSystem().load_knowledge()
    obs = ["有毛发", "吃肉", "黄褐色", "有黑色条纹"]
    print("观察到的特征:", obs)
    es.run(obs)
    animals = {"金钱豹", "老虎", "长颈鹿", "斑马", "鸵鸟", "企鹅", "信天翁"}
    found = es.facts & animals
    print("识别结果:", found if found else "特征不足,无法识别具体动物")
    if found:
        es.how(next(iter(found)))
运行输出
观察到的特征: ['有毛发', '吃肉', '黄褐色', '有黑色条纹']
识别结果: {'老虎'}

【How 解释】结论「老虎」的推理过程:
  由规则 R10:老虎识别规则
  因为满足条件:['哺乳动物', '食肉动物', '黄褐色', '有黑色条纹']
    其中「哺乳动物」由规则 R1 推出:有毛发的是哺乳动物
    其中「食肉动物」由规则 R5 推出:吃肉的是食肉动物
    其中「黄褐色」是你提供的观察事实
    其中「有黑色条纹」是你提供的观察事实

解释器把推理链完整回放给用户——这正是专家系统区别于"黑箱"模型的重要价值, 在医疗、金融等需要审计的场景尤为关键。

8.5 实战二:反向推理(目标驱动)专家系统

诊断类任务往往先有假设("是不是老虎?""是不是肺炎?"), 再去找证据。反向推理从假设目标出发,匹配结论为该目标的规则,递归验证其前提; 无法由规则推出的前提就向用户提问,并能回答"你为什么问这个(Why)"。

backward_expert.py
"""反向推理专家系统:假设-验证 + Why 解释(答案预置,便于演示)"""

class BackwardExpert:
    def __init__(self, rules, user_answers):
        # rules: [(前提集合, 结论, 规则号)]
        self.rules = rules
        self.user_answers = user_answers   # 模拟用户对提问的回答 {事实: True/False}
        self.known = {}                    # 已确认事实缓存
        self.questions = []

    def prove(self, goal, reason=None):
        """尝试证明 goal 为真;reason 记录'为什么需要它',用于 Why 解释。"""
        if goal in self.known:
            return self.known[goal]

        why = f"(为了验证「{reason}」)" if reason else ""
        # 1) 寻找结论=goal 的规则,逐条尝试
        for conds, concl, rid in self.rules:
            if concl != goal:
                continue
            if all(self.prove(c, goal) for c in conds):
                self.known[goal] = True
                print(f"✓ 规则 {rid} 成立:{sorted(conds)} => 「{goal}」")
                return True

        # 2) 没有可用规则,向用户询问
        answer = self.ask(goal, why)
        self.known[goal] = answer
        return answer

    def ask(self, fact, why):
        self.questions.append(fact)
        ans = self.user_answers.get(fact, False)
        print(f"提问:是否观察到「{fact}」?{why} -> 用户回答:{'是' if ans else '否'}")
        return ans

if __name__ == "__main__":
    rules = [
        ({"有毛发"}, "哺乳动物", "R1"),
        ({"有奶"}, "哺乳动物", "R2"),
        ({"吃肉"}, "食肉动物", "R5"),
        ({"哺乳动物", "食肉动物", "黄褐色", "有黑色条纹"}, "老虎", "R10"),
        ({"哺乳动物", "食肉动物", "黄褐色", "有暗斑点"}, "金钱豹", "R9"),
    ]
    # 用户只观察到了这些特征(其余问题回答"否")
    answers = {"有毛发": True, "吃肉": True, "黄褐色": True, "有黑色条纹": True}

    es = BackwardExpert(rules, answers)
    hypothesis = "老虎"
    print(f"提出假设:该动物是「{hypothesis}」,开始反向求证……\n")
    result = es.prove(hypothesis)
    print(f"\n假设「{hypothesis}」{'成立 ✔' if result else '不成立 ✘'}")
    print(f"系统共向用户确认了 {len(es.questions)} 个原始特征问题")
运行输出
提出假设:该动物是「老虎」,开始反向求证……

提问:是否观察到「有毛发」?(为了验证「老虎」) -> 用户回答:是
✓ 规则 R1 成立:['有毛发'] => 「哺乳动物」
提问:是否观察到「吃肉」?(为了验证「老虎」) -> 用户回答:是
✓ 规则 R5 成立:['吃肉'] => 「食肉动物」
提问:是否观察到「黄褐色」?(为了验证「老虎」) -> 用户回答:是
提问:是否观察到「有黑色条纹」?(为了验证「老虎」) -> 用户回答:是
✓ 规则 R10 成立:['哺乳动物', '食肉动物', '黄褐色', '有黑色条纹'] => 「老虎」

假设「老虎」成立 ✔
系统共向用户确认了 4 个原始特征问题

反向推理只询问与当前假设相关的问题,不收集无关信息,提问目的性强, 非常适合医疗诊断、故障排查。实践中常采用正反向混合:先用少量事实正向触发候选假设, 再对候选逐一反向求证。

8.6 实战三:带可信度的医疗分诊系统

真实症状与诊断之间不是非黑即白。把第五章的可信度模型嵌入专家系统: 规则带强度 CF、症状带观察可信度,推理后输出带置信度排序的多个诊断与建议, 这才接近 MYCIN 的实际形态。

medical_es.py
"""不确定专家系统:CF 模型 + 多假设排序 + 解释(教学演示,非真实医疗建议)"""

def cf_and(a, b):
    return min(a, b)

def cf_combine(cf1, cf2):
    if cf1 >= 0 and cf2 >= 0:
        return cf1 + cf2 - cf1 * cf2
    if cf1 < 0 and cf2 < 0:
        return cf1 + cf2 + cf1 * cf2
    return (cf1 + cf2) / (1 - min(abs(cf1), abs(cf2)))

class MedicalExpert:
    def __init__(self):
        # (前提症状集合, 结论, 规则强度 CF, 解释)
        self.rules = [
            ({"高烧", "咽痛", "无咳嗽"}, "普通感冒", 0.45, "高热咽痛但少咳"),
            ({"低烧", "鼻塞", "打喷嚏"}, "普通感冒", 0.55, "典型上呼吸道卡他症状"),
            ({"高烧", "肌肉酸痛", "突发寒战"}, "流感", 0.80, "流感样症状群"),
            ({"咳嗽", "高烧", "呼吸困难"}, "肺炎", 0.75, "下呼吸道受累征象"),
            ({"咳嗽", "持续两周以上", "午后低热"}, "肺结核", 0.70, "慢性消耗性表现"),
        ]

    def diagnose(self, symptoms_cf):
        """symptoms_cf: {症状: 观察可信度 0~1}"""
        results = {}
        evidence = {}
        for conds, disease, rule_cf, note in self.rules:
            if not conds <= set(symptoms_cf):
                continue
            e_cf = cf_and(*[symptoms_cf[c] for c in conds])
            cf = rule_cf * e_cf                       # 不确定性传递
            evidence.setdefault(disease, []).append((cf, conds, note, rule_cf))
            results[disease] = (cf_combine(results[disease], cf)
                                if disease in results else cf)

        print("症状输入:", symptoms_cf, "\n")
        ranked = sorted(results.items(), key=lambda kv: -kv[1])
        for i, (disease, cf) in enumerate(ranked, 1):
            level = ("高度可能" if cf >= 0.7 else "可能"
                     if cf >= 0.4 else "不能排除")
            print(f"{i}. {disease}:CF = {cf:.2f}({level})")
            for c, conds, note, rc in evidence[disease]:
                print(f"     依据 {sorted(conds)},规则强度 {rc},"
                      f"本次贡献 {c:.2f}({note})")
        return ranked

if __name__ == "__main__":
    es = MedicalExpert()
    patient = {"高烧": 0.9, "肌肉酸痛": 0.8, "突发寒战": 0.7,
               "咳嗽": 0.6, "呼吸困难": 0.5}
    es.diagnose(patient)
    print("\n建议:系统输出为辅助参考,确诊需结合化验与影像检查。")
运行输出
症状输入: {'高烧': 0.9, '肌肉酸痛': 0.8, '突发寒战': 0.7, '咳嗽': 0.6, '呼吸困难': 0.5}

1. 流感:CF = 0.56(可能)
     依据 ['突发寒战', '肌肉酸痛', '高烧'],规则强度 0.8,本次贡献 0.56(流感样症状群)
2. 肺炎:CF = 0.38(不能排除)
     依据 ['呼吸困难', '咳嗽', '高烧'],规则强度 0.75,本次贡献 0.38(下呼吸道受累征象)

建议:系统输出为辅助参考,确诊需结合化验与影像检查。

8.7 专家系统开发工具

⚠ 专家系统的局限

知识获取瓶颈、规则间冲突与维护困难("知识工程悖论")、缺乏学习能力、处理不了知识库之外的情形。 这些局限正是机器学习与数据驱动方法兴起的原因,而二者结合是当前的主流方向。

本章小结

  • 专家系统 = 知识库 + 综合数据库 + 推理机 + 解释器 + 知识获取 + 人机接口,核心特征是知识与推理分离、可解释。
  • 正向推理数据驱动,适合预测与监控;反向推理目标驱动、按需提问,适合诊断与排查。
  • 冲突消解可按规则优先级、专一性、新鲜度等策略进行。
  • 把可信度模型嵌入规则,即得到能输出多假设置信排序的不确定专家系统(MYCIN 范式)。
  • 专家系统擅长可解释、高价值的狭窄领域,但受知识获取瓶颈限制;与机器学习/大模型融合是现代发展方向。