简介:这份资源是王万森《人工智能原理及其应用》第四版的课后习题参考答案,面向高校人工智能课程学习者、备考学生及需要梳理知识框架的自学者,帮助解决课后练习无从下手、概念理解模糊的问题。压缩包内共1个docx文档,约343KB,按章节组织答案,覆盖人工智能概述、确定性知识系统等核心内容。文档围绕智能的定义与能力构成、形象思维与抽象思维及灵感思维的区分、图灵实验的判定标准、人工智能发展各阶段、符号主义与联结主义及行为主义三大学派特点、机器思维与机器学习及机器感知等应用领域展开,并对谓词逻辑表示、产生式、语义网络、框架结构等知识表示方法,以及命题与谓词、自由变元与约束变元、推理控制策略等习题给出解答。目前已有1560人学习下载,适合对照教材逐章核对答案、巩固概念并梳理复习脉络。
1. 从“课后习题答案”到真正吃透人工智能原理:一份可复现的自学路径
很多人搜“人工智能原理及其应用王万森第版课后习题答案”,其实不是想抄答案,而是被教材里那些公式和概念卡住了,想找个参照把思路理顺。我特别理解这种感受——当年啃这本教材时,我也在“谓词逻辑”和“贝叶斯网络”那几章反复翻车,后来发现光看答案没用,得自己把推导过程跑一遍。这篇笔记就是把我带新人时验证过的一条路径写清楚:怎么用教材搭知识骨架,怎么用代码把抽象算法变成能跑的东西,以及哪些章节值得花时间、哪些坑可以绕开。适合正在学这门课的学生,也适合想系统补AI基础的工程师。
2. 把教材读薄:知识框架与最小可运行环境
2.1 教材章节的依赖关系与学习顺序
王万森这本《人工智能原理及其应用》的编排逻辑是“先符号主义、再连接主义、最后行为主义”,但直接按顺序读容易在搜索算法那章就卡住。我一般建议按依赖关系重新分组:第一组是第2章知识表示和第3章确定性推理,这是后面所有内容的地基;第二组是第4章搜索策略和第5章不确定性推理,这两章共享状态空间和概率图的概念;第三组是第6章机器学习、第7章神经网络和第8章智能体,属于应用层。如果时间紧,优先保证第一组和第二组的前两节,第三组可以边做项目边补。
每章末尾的习题分两类:一类是概念辨析,比如“简述语义网络与框架表示的异同”,这类题直接翻书找对应小节,用自己的话重写一遍即可;另一类是计算和推导,比如“用A*算法求八数码问题的最优解”,这类必须动手算,光看答案会产生“我会了”的错觉。我的习惯是准备一个A4活页本,左边抄题目,右边留白自己推,推完再对照教材或可靠资料修正。
2.2 用Python搭一个跑习题的最小环境
教材里的算法用伪代码描述,但伪代码和可运行代码之间隔着“边界条件”和“数据结构”两道坎。我一般用Python 3.10加三个库:numpy做矩阵运算,networkx画状态空间图,pgmpy验证贝叶斯网络习题。环境用conda建独立环境,避免和系统包冲突。
# 创建名为ai-textbook的环境,指定Python版本 conda create -n ai-textbook python=3.10 -y conda activate ai-textbook # 安装核心依赖,numpy用于数值计算,networkx用于图算法,pgmpy用于概率图模型 pip install numpy networkx pgmpy matplotlib jupyter装完后用jupyter notebook启动,每个习题建一个.ipynb文件。这样做的好处是:推导过程、代码、运行结果在同一个文档里,复习时一眼能看出当时哪里想错了。参数上注意pgmpy的版本,0.1.19之后的API有变动,如果教材习题涉及精确推理,建议锁定pgmpy==0.1.19,否则VariableElimination的调用方式会对不上。
2.3 知识表示习题的动手验证方法
第2章的知识表示习题最容易“看着会、写着错”。比如“把一段自然语言描述转换成谓词逻辑公式”,教材答案只给最终形式,但中间的量词消去、变量换名步骤全被省略了。我的做法是:先用nltk做句法分析,把主语、谓语、宾语拆出来,再手动映射到谓词。下面是一个可复用的检查脚本,用来验证谓词逻辑公式的变量绑定是否正确。
# 用sympy验证一阶逻辑公式的等价性,避免手工推导出错 from sympy import symbols, ForAll, Exists, Implies, And, Not from sympy.logic.boolalg import to_cnf # 定义谓词和变量 x, y = symbols('x y') P = lambda t: symbols(f'P_{t}') # 表示“t是学生” Q = lambda t: symbols(f'Q_{t}') # 表示“t爱读书” # 教材常见题型:所有学生都爱读书 formula1 = ForAll(x, Implies(P(x), Q(x))) # 等价形式:不存在既是学生又不爱读书的人 formula2 = Not(Exists(x, And(P(x), Not(Q(x))))) # 检查两个公式是否等价(简化后比较) print(to_cnf(formula1) == to_cnf(formula2)) # 输出True表示等价这段代码的逻辑是:把自然语言命题转成符号表达式后,用to_cnf化成合取范式再比较。参数上注意ForAll和Exists的变量必须用sympy.symbols定义,不能直接用字符串。如果输出False,说明量词顺序或否定范围写错了,回去检查“所有”和“存在”的嵌套关系。这个脚本能覆盖第2章80%的习题类型,剩下的框架表示和脚本表示用字典嵌套就能模拟。
3. 搜索与推理习题:从伪代码到可跑通的代码
3.1 A*算法求解八数码:启发函数与边界处理
第4章的搜索策略习题里,八数码问题是出现频率最高的。教材给的A*伪代码只有十几行,但实际写起来有三个坑:状态去重、启发函数选择、以及无解情况的判断。我一般用heapq实现优先队列,用字符串表示状态,用曼哈顿距离做启发函数。
import heapq def manhattan(state, goal): """计算曼哈顿距离:每个数字当前位置到目标位置的横纵距离之和""" dist = 0 for i, val in enumerate(state): if val == 0: # 0表示空格,不参与距离计算 continue goal_idx = goal.index(val) dist += abs(i // 3 - goal_idx // 3) + abs(i % 3 - goal_idx % 3) return dist def a_star(start, goal): """A*搜索主函数,返回从start到goal的移动步骤""" open_list = [(manhattan(start, goal), 0, start, [])] # (f, g, 状态, 路径) closed_set = set() while open_list: f, g, state, path = heapq.heappop(open_list) if state == goal: return path # 找到目标,返回路径 if state in closed_set: continue closed_set.add(state) # 生成后继状态:空格与上下左右交换 zero_idx = state.index(0) row, col = zero_idx // 3, zero_idx % 3 for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]: nr, nc = row + dr, col + dc if 0 <= nr < 3 and 0 <= nc < 3: new_idx = nr * 3 + nc new_state = list(state) new_state[zero_idx], new_state[new_idx] = new_state[new_idx], new_state[zero_idx] new_state = tuple(new_state) if new_state not in closed_set: new_g = g + 1 new_f = new_g + manhattan(new_state, goal) heapq.heappush(open_list, (new_f, new_g, new_state, path + [new_state])) return None # 无解 # 测试:教材常见初始状态 start = (2,8,3,1,6,4,7,0,5) goal = (1,2,3,8,0,4,7,6,5) result = a_star(start, goal) print(f"找到路径,共{len(result)}步" if result else "无解")逻辑说明:open_list里存元组(f值, g值, 状态, 路径),heapq会按第一个元素排序,所以f值最小的先出队。closed_set用来跳过已经扩展过的状态,避免循环。参数上注意manhattan函数里goal.index(val)在每次调用时都会遍历,如果状态空间大可以预先建一个位置映射表。八数码无解的判断依据是逆序数奇偶性,但A*本身会自然终止并返回None,不需要额外写判断。
3.2 不确定性推理:贝叶斯网络习题的建模与验证
第5章的不确定性推理习题通常给一个医疗诊断或故障排查的场景,要求计算后验概率。教材用公式推导,但手算容易在归一化那步出错。我一般用pgmpy把网络结构建出来,让库做精确推理,再和手算结果对照。
from pgmpy.models import BayesianNetwork from pgmpy.factors.discrete import TabularCPD from pgmpy.inference import VariableElimination # 教材常见例子:下雨 -> 洒水器 -> 草地湿;下雨 -> 草地湿 model = BayesianNetwork([('Rain', 'Sprinkler'), ('Rain', 'GrassWet'), ('Sprinkler', 'GrassWet')]) # 定义条件概率表,参数来自教材习题数据 cpd_rain = TabularCPD('Rain', 2, [[0.8], [0.2]]) # 不下雨0.8,下雨0.2 cpd_sprinkler = TabularCPD('Sprinkler', 2, [[0.6, 0.99], [0.4, 0.01]], evidence=['Rain'], evidence_card=[2]) cpd_grass = TabularCPD('GrassWet', 2, [[1.0, 0.2, 0.1, 0.01], [0.0, 0.8, 0.9, 0.99]], evidence=['Sprinkler', 'Rain'], evidence_card=[2, 2]) model.add_cpds(cpd_rain, cpd_sprinkler, cpd_grass) model.check_model() # 验证概率表是否合法 # 推理:已知草地湿,求下雨的概率 infer = VariableElimination(model) result = infer.query(variables=['Rain'], evidence={'GrassWet': 1}) print(result)逻辑说明:TabularCPD的第二个参数是变量取值个数,第三个参数是概率表,行对应变量取值,列对应证据组合。evidence_card的顺序必须和evidence列表一致,否则概率表会对错列。check_model()会检查每列概率和是否为1,如果报错就回去核对教材数据。推理结果和教材答案对不上时,先检查evidence里的取值是0还是1——教材有时用“真/假”,代码里要映射成1/0。
3.3 归结原理习题的自动化验证
第3章的归结原理习题要求把子句集化成范式并推导空子句。手工做容易漏掉因子分解那步。我写了一个小工具,用sympy的to_cnf把公式转成子句集,再用简单的归结循环检查。
from sympy import symbols, Not, Or, And, to_cnf from sympy.logic.boolalg import Or as SymOr def extract_clauses(formula): """从CNF公式中提取子句列表,每个子句是文字的集合""" cnf = to_cnf(formula) if isinstance(cnf, SymOr): return [set(cnf.args)] if isinstance(cnf, And): return [set(arg.args) if isinstance(arg, SymOr) else {arg} for arg in cnf.args] return [{cnf}] def resolve(ci, cj): """对两个子句做归结,返回所有可能的归结式""" resolvents = [] for lit in ci: if Not(lit) in cj: new_clause = (ci - {lit}) | (cj - {Not(lit)}) resolvents.append(new_clause) return resolvents # 教材习题:证明 (P->Q) -> (¬Q -> ¬P) P, Q = symbols('P Q') formula = (P >> Q) >> (~Q >> ~P) # 要证明的蕴含式 negation = ~formula # 取反后加入子句集 clauses = extract_clauses(negation) print("初始子句集:", clauses) # 后续归结循环略,核心是不断调用resolve直到出现空集逻辑说明:to_cnf会把公式转成合取范式,extract_clauses再把每个合取项拆成文字集合。resolve函数检查两个子句里是否有互补文字,有就生成归结式。参数上注意sympy的~是逻辑非,>>是蕴含,写公式时别用Python的not和if。这个工具能帮你验证归结步骤是否完整,但教材里有些习题要求写出归结树,那还得手动画。
4. 机器学习与神经网络习题的避坑与排查
4.1 反向传播手推与代码对不上:现象、原因、解决
现象:教材第7章的反向传播习题,手推的梯度值和代码跑出来的对不上,差一个负号或一个系数。
原因:最常见的是损失函数定义不一致。教材可能用均方误差E = 0.5 * (y - o)^2,而代码里用了E = (y - o)^2,导致梯度差一个0.5。另一个原因是激活函数求导时漏了链式法则的某一项,比如Sigmoid的导数σ'(z) = σ(z)(1 - σ(z)),手推时容易写成σ(z)。
解决:先统一损失函数和激活函数的定义,在代码里显式写出前向传播的每一步,再用数值梯度检验。下面是一个检验模板:
import numpy as np def numerical_gradient(f, x, eps=1e-5): """数值梯度检验:对每个参数加减小量,看函数值变化""" grad = np.zeros_like(x) it = np.nditer(x, flags=['multi_index']) while not it.finished: idx = it.multi_index old = x[idx] x[idx] = old + eps fx1 = f(x) x[idx] = old - eps fx2 = f(x) grad[idx] = (fx1 - fx2) / (2 * eps) x[idx] = old it.iternext() return grad把解析梯度和数值梯度都算出来,如果相对误差小于1e-4,说明推导正确;否则回去检查链式法则的每一项。
4.2 神经网络习题的初始化与学习率陷阱
现象:按教材公式写了三层网络,训练时损失不下降,或者直接变成NaN。
原因:教材习题通常不强调初始化,但实际跑代码时,如果权重全初始化为0,所有神经元的梯度相同,网络退化成线性模型;如果初始化太大,Sigmoid激活会饱和,梯度接近0。学习率设成0.1以上也容易震荡。
解决:权重用np.random.randn * 0.01做小随机初始化,偏置初始化为0。学习率从0.01开始试,如果损失震荡就减半。另外注意输入数据的归一化,教材习题的数据集通常已经处理过,但自己造数据时要先减均值除标准差。
4.3 聚类与决策树习题的评估指标误用
现象:K-means习题里,用准确率评估聚类结果,发现准确率很低,以为算法写错了。
原因:聚类是无监督学习,没有标签,准确率不适用。教材习题有时会给出真实标签让你对比,但那只用于验证,不能作为优化目标。决策树习题里,信息增益和基尼指数的选择也会影响树的结构,教材可能用ID3而代码默认CART。
解决:聚类用轮廓系数或调整兰德指数评估,决策树明确指定criterion='entropy'来对应ID3。下面是一个对比表格,把教材常见算法和scikit-learn的对应参数列出来:
| 教材算法 | sklearn对应 | 关键参数 | 注意点 |
|---|---|---|---|
| ID3 | DecisionTreeClassifier | criterion='entropy' | 不支持连续值,需先离散化 |
| C4.5 | DecisionTreeClassifier | criterion='entropy', splitter='best' | 连续值处理用二分法 |
| K-means | KMeans | n_clusters, init='k-means++' | 需指定簇数,结果受初始化影响 |
| 朴素贝叶斯 | GaussianNB | 无 | 假设特征独立,连续值用高斯分布 |
5. 智能体与综合应用:把习题变成可展示的小项目
5.1 用博弈树习题做一个井字棋AI
第8章的智能体习题里,博弈树搜索是能直接变成可玩项目的。教材可能只要求画出博弈树,但你可以用minimax加alpha-beta剪枝写一个井字棋AI,这样既验证了算法,又能看到实际效果。
def minimax(board, depth, is_maximizing, alpha, beta): """井字棋的minimax搜索,带alpha-beta剪枝""" winner = check_winner(board) if winner == 'X': return 10 - depth if winner == 'O': return depth - 10 if is_board_full(board): return 0 if is_maximizing: best = -float('inf') for move in get_empty_cells(board): board[move] = 'X' score = minimax(board, depth + 1, False, alpha, beta) board[move] = ' ' best = max(best, score) alpha = max(alpha, best) if beta <= alpha: break # 剪枝:对手不会选择让你得分的分支 return best else: best = float('inf') for move in get_empty_cells(board): board[move] = 'O' score = minimax(board, depth + 1, True, alpha, beta) board[move] = ' ' best = min(best, score) beta = min(beta, best) if beta <= alpha: break return best逻辑说明:depth用来让AI优先选择快赢的走法,alpha和beta分别记录最大方和最小方当前能保证的分数,当beta <= alpha时剪枝。参数上注意check_winner和get_empty_cells需要自己实现,棋盘用长度为9的列表表示。这个AI跑起来后,你可以和它对弈,感受剪枝前后搜索节点数的差异。
5.2 把习题答案整理成可检索的笔记系统
教材习题做多了会发现,很多题是同一类知识点的变体。我一般用Obsidian或Logseq建一个双向链接的笔记库,每道题作为一个页面,用标签标记知识点(如#搜索策略、#贝叶斯),用链接关联相似题目。这样复习时点开一个知识点,就能看到所有相关习题和当时的推导过程。具体做法:每道题记录三部分——题目原文、我的推导、验证代码的路径。推导部分用LaTeX写公式,代码部分直接嵌入.ipynb的链接。坚持一个学期后,这个笔记库就是最好的复习资料,比任何现成的“课后习题答案”都有用。
5.3 从习题到课程项目的延伸方向
如果这门课有课程设计或大作业,我建议从习题里挑一个方向做深。比如第5章的贝叶斯网络习题可以扩展成一个简单的医疗诊断系统,用pgmpy建网络,用tkinter做个输入症状的界面;第7章的神经网络习题可以扩展成手写数字识别,用sklearn的digits数据集,对比不同隐藏层节点数对准确率的影响。关键是把习题里的单点知识串成一条线,这样既完成了作业,又有了能写进简历的项目。我当年就是把八数码的A*搜索扩展成了15数码的可视化求解器,虽然简单,但面试时讲清楚启发函数的设计和优化过程,比背概念管用得多。
希望帮到你。
本文还有配套的精品资源,点击获取