0. 规则库¶
先看初始事实和规则表。每条规则都有一组前提和一个结论,推理过程就是不断检查“前提是否已经满足”。
In [2]:
# 动物分类专家系统:用规则把观察事实推到物种结论。
initial_facts = {"有毛发", "吃肉", "黄褐色", "有黑色条纹"}
goal = "老虎"
rules = [
{"id": "R1", "facts": ["有毛发"], "then": "哺乳动物"},
{"id": "R2", "facts": ["有奶"], "then": "哺乳动物"},
{"id": "R3", "facts": ["有羽毛"], "then": "鸟类"},
{"id": "R4", "facts": ["哺乳动物", "吃肉"], "then": "食肉动物"},
{"id": "R5", "facts": ["哺乳动物", "有蹄"], "then": "有蹄类"},
{"id": "R6", "facts": ["食肉动物", "黄褐色", "有黑色斑点"], "then": "猎豹"},
{"id": "R7", "facts": ["食肉动物", "黄褐色", "有黑色条纹"], "then": "老虎"},
]
facts_df = pd.DataFrame({"初始事实": sorted(initial_facts)})
rules_df = pd.DataFrame(
[
{"规则": rule["id"], "前提": " + ".join(rule["facts"]), "结论": rule["then"]}
for rule in rules
]
)
display(facts_df)
display(rules_df)
| 初始事实 | |
|---|---|
| 0 | 吃肉 |
| 1 | 有毛发 |
| 2 | 有黑色条纹 |
| 3 | 黄褐色 |
| 规则 | 前提 | 结论 | |
|---|---|---|---|
| 0 | R1 | 有毛发 | 哺乳动物 |
| 1 | R2 | 有奶 | 哺乳动物 |
| 2 | R3 | 有羽毛 | 鸟类 |
| 3 | R4 | 哺乳动物 + 吃肉 | 食肉动物 |
| 4 | R5 | 哺乳动物 + 有蹄 | 有蹄类 |
| 5 | R6 | 食肉动物 + 黄褐色 + 有黑色斑点 | 猎豹 |
| 6 | R7 | 食肉动物 + 黄褐色 + 有黑色条纹 | 老虎 |
1. 前向链¶
前向链适合回答“这些事实还能推出什么”。它每一轮寻找可触发规则,把新结论加入已知事实集合。
In [3]:
# 前向链:从已知事实出发,反复触发前提已满足的规则。
def forward_chain(initial_facts, rules, goal):
known = set(initial_facts)
fired = set()
rows = []
round_id = 0
while True:
candidates = []
for rule in rules:
if rule["id"] in fired:
continue
missing = [fact for fact in rule["facts"] if fact not in known]
if not missing:
candidates.append(rule)
if not candidates:
break
for rule in candidates:
round_id += 1
before = set(known)
known.add(rule["then"])
fired.add(rule["id"])
rows.append({
"轮次": round_id,
"触发规则": rule["id"],
"前提": " + ".join(rule["facts"]),
"新增事实": rule["then"] if rule["then"] not in before else "已存在",
"已知事实": "、".join(sorted(known)),
"达到目标": rule["then"] == goal or goal in known,
})
if goal in known:
return known, pd.DataFrame(rows)
return known, pd.DataFrame(rows)
forward_facts, forward_trace = forward_chain(initial_facts, rules, goal)
display(forward_trace)
print("推理结果:", goal in forward_facts)
| 轮次 | 触发规则 | 前提 | 新增事实 | 已知事实 | 达到目标 | |
|---|---|---|---|---|---|---|
| 0 | 1 | R1 | 有毛发 | 哺乳动物 | 吃肉、哺乳动物、有毛发、有黑色条纹、黄褐色 | False |
| 1 | 2 | R4 | 哺乳动物 + 吃肉 | 食肉动物 | 吃肉、哺乳动物、有毛发、有黑色条纹、食肉动物、黄褐色 | False |
| 2 | 3 | R7 | 食肉动物 + 黄褐色 + 有黑色条纹 | 老虎 | 吃肉、哺乳动物、有毛发、有黑色条纹、老虎、食肉动物、黄褐色 | True |
推理结果: True
2. 后向链¶
后向链适合回答“为了证明目标,还缺哪些条件”。它从目标出发,把目标拆成更小的子目标,直到命中初始事实。
In [4]:
# 后向链:从目标倒推需要哪些前提,再逐个证明这些前提。
def backward_chain(goal, initial_facts, rules):
rows = []
active = set()
def prove(target, depth=0):
if target in initial_facts:
rows.append({
"深度": depth,
"待证明": target,
"使用规则": "初始事实",
"子目标": "",
"结论": "成立",
})
return True
if target in active:
rows.append({
"深度": depth,
"待证明": target,
"使用规则": "循环依赖",
"子目标": "",
"结论": "失败",
})
return False
active.add(target)
matched = [rule for rule in rules if rule["then"] == target]
if not matched:
active.remove(target)
rows.append({
"深度": depth,
"待证明": target,
"使用规则": "无可用规则",
"子目标": "",
"结论": "失败",
})
return False
for rule in matched:
subgoals = rule["facts"]
ok = all(prove(item, depth + 1) for item in subgoals)
rows.append({
"深度": depth,
"待证明": target,
"使用规则": rule["id"],
"子目标": "、".join(subgoals),
"结论": "成立" if ok else "失败",
})
if ok:
active.remove(target)
return True
active.remove(target)
return False
success = prove(goal)
trace = pd.DataFrame(rows)
return success, trace.sort_index(ascending=False).reset_index(drop=True)
backward_success, backward_trace = backward_chain(goal, initial_facts, rules)
display(backward_trace)
print("推理结果:", backward_success)
| 深度 | 待证明 | 使用规则 | 子目标 | 结论 | |
|---|---|---|---|---|---|
| 0 | 0 | 老虎 | R7 | 食肉动物、黄褐色、有黑色条纹 | 成立 |
| 1 | 1 | 有黑色条纹 | 初始事实 | 成立 | |
| 2 | 1 | 黄褐色 | 初始事实 | 成立 | |
| 3 | 1 | 食肉动物 | R4 | 哺乳动物、吃肉 | 成立 |
| 4 | 2 | 吃肉 | 初始事实 | 成立 | |
| 5 | 2 | 哺乳动物 | R1 | 有毛发 | 成立 |
| 6 | 3 | 有毛发 | 初始事实 | 成立 |
推理结果: True
3. 推理路径图¶
图里每一行是一条被触发的规则。绿色是初始事实,蓝色是中间结论,橙色是最终目标。
In [5]:
# 画出规则触发路径:每条规则一行,多前提规则会分层连到同一个规则节点。
def draw_rule_flow(initial_facts, rules, trace, title):
if "触发规则" in trace:
used_order = [rid for rid in trace["触发规则"].tolist() if str(rid).startswith("R")]
else:
used_order = [rid for rid in trace.get("使用规则", []).tolist() if str(rid).startswith("R")]
used_order = list(dict.fromkeys(used_order))
selected_rules = [rule for rid in used_order for rule in rules if rule["id"] == rid]
fig, ax = plt.subplots(figsize=(10.8, 5.0))
ax.set_facecolor("#fbfcfd")
def chip_style(text, role):
if role == "initial":
return {"fc": "#dcfce7", "ec": "#16a34a"}
if role == "goal":
return {"fc": "#ffedd5", "ec": "#f97316"}
if role == "derived":
return {"fc": "#eff6ff", "ec": "#2563eb"}
return {"fc": "#ffffff", "ec": "#94a3b8"}
def draw_chip(x, y, text, role):
style = chip_style(text, role)
ax.text(
x,
y,
text,
ha="center",
va="center",
fontsize=9.2,
color="#0f172a",
zorder=5,
bbox={
"boxstyle": "round,pad=0.38",
"fc": style["fc"],
"ec": style["ec"],
"lw": 1.45,
},
)
def fact_role(fact, conclusion=False):
if fact == goal:
return "goal"
if fact in initial_facts:
return "initial"
if conclusion:
return "derived"
return "other"
y_positions = list(reversed(range(len(selected_rules))))
for row_index, (rule, y) in enumerate(zip(selected_rules, y_positions), start=1):
ax.axhspan(y - 0.46, y + 0.46, color="#f8fafc" if row_index % 2 else "#ffffff", zorder=0)
if len(rule["facts"]) == 1:
premise_offsets = [0]
else:
center = (len(rule["facts"]) - 1) / 2
premise_offsets = [(center - idx) * 0.22 for idx in range(len(rule["facts"]))]
for fact, offset in zip(rule["facts"], premise_offsets):
fy = y + offset
draw_chip(0.56, fy, fact, fact_role(fact))
ax.annotate(
"",
xy=(1.78, y),
xytext=(1.10, fy),
arrowprops={"arrowstyle": "-|>", "color": "#64748b", "lw": 2.0, "shrinkA": 6, "shrinkB": 8},
zorder=2,
)
ax.text(
2.08,
y,
rule["id"],
ha="center",
va="center",
fontsize=10,
fontweight="bold",
color="#1e3a8a",
zorder=5,
bbox={"boxstyle": "round,pad=0.42", "fc": "#dbeafe", "ec": "#2563eb", "lw": 1.7},
)
ax.annotate(
"",
xy=(3.28, y),
xytext=(2.38, y),
arrowprops={"arrowstyle": "-|>", "color": "#2563eb", "lw": 2.5, "shrinkA": 8, "shrinkB": 8},
zorder=2,
)
draw_chip(3.9, y, rule["then"], fact_role(rule["then"], conclusion=True))
ax.text(0.56, len(selected_rules) - 0.24, "前提", ha="center", fontweight="bold", color="#334155")
ax.text(2.08, len(selected_rules) - 0.24, "规则", ha="center", fontweight="bold", color="#334155")
ax.text(3.9, len(selected_rules) - 0.24, "结论", ha="center", fontweight="bold", color="#334155")
ax.set_title(title, loc="left", fontsize=14, fontweight="bold", color="#0f172a")
ax.set_xlim(-0.18, 4.55)
ax.set_ylim(-0.65, len(selected_rules) - 0.02)
ax.axis("off")
plt.tight_layout()
plt.show()
draw_rule_flow(initial_facts, rules, forward_trace, "动物分类专家系统:前向链触发路径")