一、实验目的:
熟悉一阶谓词逻辑和产生式表示法,掌握产生式系统的运行机制,以及基于规则推理的基本方法。
二、实验内容:
运用所学知识,设计并编程实现一个小型人工智能系统(如分类、诊断、预测等类型)。
三、实验步骤:
1)系统设置,包括设置系统名称和系统谓词,给出谓词名及其含义。
2)编辑知识库,通过输入规则或修改规则等,完成整个规则库的建立。
3)建立事实库(综合数据库),输入多条事实或结论。
4)运行推理,包括正向推理和反向推理,给出相应的推理过程、事实区和规则区。
如果在macOS下Pyside2运行有问题去看我另一篇文章
一共有需要三个txt文件,分别是Plants,Feature和Rule。具体文件路径需要在py文件里自行更改。
文件内容为:
Plants:
玫瑰
荷花
仙人球
水棉
苹果树
油菜
海带
松树
Feature:
种子有果皮
被子植物
种子无果皮
裸子植物
无茎叶
无根
藻类植物
有托叶
蔷薇科
吸引菜粉蝶
十字花科
十字形花冠
缺水环境
仙人掌科
有刺
玫瑰
水生
可食用
结果实
荷花
喜阳
仙人球
药用
水棉
木本
苹果树
黄色花
油菜
有白色粉末
海带
叶片针状
松树
Rule:
种子有果皮 被子植物
种子无果皮 裸子植物
无茎叶 无根 藻类植物
被子植物 有托叶 蔷薇科
被子植物 吸引菜粉蝶 十字花科
被子植物 十字形花冠 十字花科
被子植物 缺水环境 仙人掌科
被子植物 蔷薇科 有刺 玫瑰
被子植物 水生 可食用 结果实 荷花
被子植物 仙人掌科 喜阳 有刺 仙人球
藻类植物 水生 药用 水棉
被子植物 蔷薇科 木本 可食用 结果实 苹果树
被子植物 十字花科 黄色花 可食用 结果实 油菜
藻类植物 水生 可食用 有白色粉末 海带
裸子植物 木本 叶片针状 结果实 松树
(数据和代码框架借鉴https://blog.youkuaiyun.com/weixin_53056212/article/details/124394320)在它的框架下添加了功能并优化了GUI界面。
import sys
from PySide2.QtWidgets import QApplication, QMainWindow, QPushButton, QPlainTextEdit, QMessageBox
# --- 全局知识库 ---
# 这些变量在程序启动时从文件加载一次,作为静态知识库。
Plants = [] # 存储所有可能的结论(植物名称)
Feature = [] # 存储所有已知特征
Rules = [] # 存储所有规则
# --- 全局可配置项 ---
# 这些变量可以由UI修改
weici1 = '如果' # 默认谓词1
weici2 = '那么' # 默认谓词2
class GUI():
def __init__(self):
self.window = QMainWindow()
self.window.resize(600, 500)
self.window.move(300, 310)
self.window.setWindowTitle("简单产生式系统")
# --- UI 控件定义 (与原版保持一致) ---
self.textEdit6 = QPlainTextEdit(self.window)
self.textEdit6.setPlaceholderText("请输入系统名称")
self.textEdit6.move(180, 420)
self.textEdit6.resize(120, 30)
self.textEdit7 = QPlainTextEdit(self.window)
self.textEdit7.setPlaceholderText("请输入系统谓词1")
self.textEdit7.move(330, 420)
self.textEdit7.resize(120, 30)
self.textEdit7.setPlainText(weici1)
self.textEdit8 = QPlainTextEdit(self.window)
self.textEdit8.setPlaceholderText("请输入系统谓词2")
self.textEdit8.move(460, 420)
self.textEdit8.resize(120, 30)
self.textEdit8.setPlainText(weici2)
self.textEdit = QPlainTextEdit(self.window)
self.textEdit.setPlaceholderText("请输入已有特征信息的序号(以空格分隔)")
self.textEdit.move(220, 50)
self.textEdit.resize(110, 300)
info = []
for i in range(len(Feature)):
s = f"{i + 1}: {Feature[i]}"
info.append(s)
self.textEdit2 = QPlainTextEdit(self.window)
self.textEdit2.setPlainText('\n'.join(info))
self.textEdit2.move(10, 50)
self.textEdit2.resize(200, 300)
self.textEdit2.setReadOnly(True)
self.textEdit3 = QPlainTextEdit(self.window)
self.textEdit3.setPlainText('所有特征')
self.textEdit3.move(10, 15)
self.textEdit3.resize(120, 30)
self.textEdit3.setReadOnly(True)
self.textEdit4 = QPlainTextEdit(self.window)
self.textEdit4.setPlaceholderText("推理结果将在此显示")
self.textEdit4.move(430, 50)
self.textEdit4.resize(150, 300)
self.textEdit4.setReadOnly(True)
self.button1 = QPushButton('正向推理', self.window)
self.button1.move(330, 130)
self.button1.clicked.connect(self.handle_forward)
self.button2 = QPushButton('反向推理', self.window)
self.button2.move(330, 190)
self.button2.clicked.connect(self.handle_backward)
self.button3 = QPushButton('展示所有规则', self.window)
self.button3.resize(150, 100)
self.button3.move(10, 380)
self.button3.clicked.connect(self.display_rules)
self.textEdit5 = QPlainTextEdit(self.window)
self.textEdit5.setPlaceholderText("请输入要添加的规则 (前提和结论用空格分隔)")
self.textEdit5.move(180, 360)
self.textEdit5.resize(250, 50)
self.button4 = QPushButton('添加规则', self.window)
self.button4.resize(100, 50)
self.button4.move(460, 360)
self.button4.clicked.connect(self.add_rule)
self.button5 = QPushButton('设置系统标题', self.window)
self.button5.move(180, 455)
self.button5.clicked.connect(self.set_title)
self.button6 = QPushButton('设置系统谓词', self.window)
self.button6.move(380, 455)
self.button6.clicked.connect(self.set_weici)
def set_title(self):
self.window.setWindowTitle(self.textEdit6.toPlainText())
def set_weici(self):
global weici1, weici2
weici1 = self.textEdit7.toPlainText()
weici2 = self.textEdit8.toPlainText()
QMessageBox.information(self.window, "成功", "谓词已更新!")
def display_rules(self):
# 每次都重新读取文件,以反映动态添加的规则
temp_rules = []
try:
with open(file3_path, 'r', encoding='utf8') as f:
for line in f:
if line.strip():
temp_rules.append(line.split())
except FileNotFoundError:
QMessageBox.critical(self.window, '错误', f'规则文件未找到: {file3_path}')
return
info = []
for i, rule in enumerate(temp_rules):
premises = " 和 ".join(rule[:-1])
conclusion = rule[-1]
s = f"{i + 1}: {weici1} {premises} {weici2} {conclusion}"
info.append(s)
QMessageBox.about(self.window, '所有规则', '\n'.join(info))
def add_rule(self):
new_rule_text = self.textEdit5.toPlainText().strip()
if not new_rule_text:
QMessageBox.warning(self.window, '警告', '规则内容不能为空!')
return
try:
with open(file3_path, 'a', encoding='utf8') as file:
file.write('\n' + new_rule_text)
# 全局规则库也需要同步更新
Rules.append(new_rule_text.split())
self.textEdit5.clear()
QMessageBox.information(self.window, '成功', f'规则 "{new_rule_text}" 已成功添加!')
except Exception as e:
QMessageBox.critical(self.window, '错误', f'添加规则失败: {e}')
# --- 修正后的推理处理函数 ---
def handle_backward(self):
info = self.textEdit.toPlainText().strip()
if not info:
QMessageBox.warning(self.window, "警告", "请输入特征信息的序号!")
return
try:
nums = list(map(int, info.split()))
initial_facts = [Feature[i - 1] for i in nums if 0 < i <= len(Feature)]
except ValueError:
QMessageBox.warning(self.window, "错误", "输入格式不正确,请确保是空格分隔的数字!")
return
# 调用新的反向推理函数
result, process_log = backward_chaining(initial_facts)
output_text = (f"您输入的初始特征有:\n{' '.join(initial_facts)}\n\n"
f"【反向推理过程】:\n{process_log}\n\n"
f"【最终结论是】:\n{result}")
self.textEdit4.setPlainText(output_text)
def handle_forward(self):
info = self.textEdit.toPlainText().strip()
if not info:
QMessageBox.warning(self.window, "警告", "请输入特征信息的序号!")
return
try:
nums = list(map(int, info.split()))
initial_facts = [Feature[i - 1] for i in nums if 0 < i <= len(Feature)]
except ValueError:
QMessageBox.warning(self.window, "错误", "输入格式不正确,请确保是空格分隔的数字!")
return
# 调用新的正向推理函数
result, process_log = forward_chaining(initial_facts)
output_text = (f"您输入的初始特征有:\n{' '.join(initial_facts)}\n\n"
f"【正向推理过程】:\n{process_log}\n\n"
f"【最终结论是】:\n{result}")
self.textEdit4.setPlainText(output_text)
def Database(file1, file2, file3):
"""从文件加载知识库"""
try:
with open(file1, 'r', encoding='utf8') as f1:
for line in f1:
Plants.extend(line.strip().split())
with open(file2, 'r', encoding='utf8') as f2:
for line in f2:
Feature.extend(line.strip().split())
with open(file3, 'r', encoding='utf8') as f3:
for line in f3:
if line.strip(): # 避免空行
Rules.append(line.strip().split())
return True
except FileNotFoundError as e:
print(f"错误:无法找到文件 {e.filename}。请检查文件路径。")
return False
except Exception as e:
print(f"加载数据库时发生错误: {e}")
return False
def forward_chaining(initial_facts):
"""
正确的正向推理实现
:param initial_facts: 用户输入的初始事实列表
:return: 一个元组 (最终结论, 推理过程日志)
"""
if not initial_facts:
return "请输入初始特征", ""
facts = set(initial_facts)
log = [f"初始事实: {', '.join(facts)}"]
# 循环直到没有新事实产生
while True:
new_fact_added = False
for i, rule in enumerate(Rules):
premises = set(rule[:-1])
conclusion = rule[-1]
# 检查所有前提是否都存在于已知事实中,并且结论是新的
if premises.issubset(facts) and conclusion not in facts:
facts.add(conclusion)
new_fact_added = True
# 记录日志
log.append(f"\n匹配规则: {weici1} {' 和 '.join(premises)} {weici2} {conclusion}")
log.append(f"-> 新增事实: {conclusion}")
log.append(f"当前事实库: {', '.join(sorted(list(facts)))}")
# 如果在一整轮循环中没有新事实产生,则推理结束
if not new_fact_added:
log.append("\n没有更多规则可匹配,推理结束。")
break
# 检查最终事实中是否有目标植物
final_conclusions = [fact for fact in facts if fact in Plants]
if final_conclusions:
result = ', '.join(final_conclusions)
else:
result = "无法推断出任何已知植物。"
return result, "".join(log)
def backward_chaining(initial_facts):
"""
正确的反向推理实现
:param initial_facts: 用户输入的初始事实列表
:return: 一个元组 (最终结论, 推理过程日志)
"""
if not initial_facts:
return "请输入初始特征", ""
facts = set(initial_facts)
log = []
# 对每一个可能的植物目标进行验证
for goal in Plants:
log.append(f"\n--- 正在尝试证明目标: {goal} ---")
# proven_facts 用于在当前递归路径中防止无限循环 (e.g., A->B, B->A)
if prove_goal(goal, facts, set(), log, ""):
log.append(f"\n结论: 目标 {goal} 被成功证明。")
return goal, "".join(log)
else:
log.append(f"-> 无法证明目标: {goal}")
return "无法通过反向推理证明任何已知植物。", "".join(log)
def prove_goal(goal, facts, path, log, indent):
"""
反向推理的递归辅助函数
:param goal: 当前要证明的子目标
:param facts: 初始已知事实
:param path: 在当前证明路径中已经访问过的目标,用于防止无限循环
:param log: 推理过程日志
:param indent: 日志缩进,用于美化输出
:return: True如果目标可以被证明,否则False
"""
# 基础情况1: 目标本身就是已知事实
if goal in facts:
log.append(f"\n{indent}✅ 目标 '{goal}' 是已知事实,成立。")
return True
# 防止无限循环
if goal in path:
log.append(f"\n{indent}🔄 检测到循环,停止证明 '{goal}'。")
return False
path.add(goal)
# 查找所有能推导出当前目标的规则
relevant_rules = [rule for rule in Rules if rule[-1] == goal]
if not relevant_rules:
path.remove(goal) # 回溯
return False
# 尝试每一条相关规则
for rule in relevant_rules:
premises = rule[:-1]
log.append(f"\n{indent}为证明 '{goal}', 需证明前提: {', '.join(premises)} (据规则: {' '.join(rule)})")
# 递归地证明所有前提 (all()确保所有前提都为True)
if all(prove_goal(premise, facts, path, log, indent + " ") for premise in premises):
log.append(f"\n{indent}👍 所有前提均成立,因此目标 '{goal}' 成立。")
path.remove(goal) # 回溯
return True # 找到一条可行的路径就够了
# 如果所有相关规则都无法证明目标
path.remove(goal) # 回溯
return False
if __name__ == "__main__":
# !!! 重要 !!!
# !!! 请将下面的文件路径替换为你自己电脑上的实际路径 !!!
file1_path = "请在这里替换为你的Plants.txt文件路径"
file2_path = "请在这里替换为你的Feature.txt文件路径"
file3_path = "请在这里替换为你的Rule.txt文件路径"
# 示例路径:
# file1_path = "/Users/YourName/Desktop/AnimalSystem/Plants.txt"
# file2_path = "/Users/YourName/Desktop/AnimalSystem/Feature.txt"
# file3_path = "/Users/YourName/Desktop/AnimalSystem/Rule.txt"
app = QApplication(sys.argv)
if not Database(file1_path, file2_path, file3_path):
# 如果文件加载失败,显示一个错误消息并退出
error_msg = QMessageBox()
error_msg.setIcon(QMessageBox.Critical)
error_msg.setText("知识库文件加载失败!")
error_msg.setInformativeText("请检查文件路径是否正确,并确保程序有权限读取这些文件。")
error_msg.setWindowTitle("启动错误")
error_msg.exec_()
sys.exit(1) # 退出程序
gg = GUI()
gg.window.show()
sys.exit(app.exec_())