简介:这份文档是清华大学出版社人工智能教材的完整课后习题答案,面向高校计算机及相关专业学生、考研复习者与自学者,帮助解决课后练习无从下手、解题过程不清晰的问题。资源共1个doc文件,压缩包约1009KB,内容按章节组织,涵盖状态空间搜索、启发式函数、归结原理、合一算法、谓词逻辑推理、模式识别与分类等核心知识点。文档以8数码问题为例,详细给出不在位将牌数与距离和两种启发函数的求解过程,并完整演示命题公式与谓词公式的归结证明步骤,包括最一般合一mgu的求解、李明快乐与激动人心生活的逻辑推导,以及斑马、长颈鹿、企鹅等动物特征分类示例。此外还涉及主观贝叶斯方法中概率与几率计算的具体推导。已有59人学习,适合需要对照标准答案梳理解题思路、巩固人工智能基础理论的读者参考。
1. 一份课后习题答案文档,为什么值得当成技术资产来拆
“完整word版人工智能课后习题答案清华大学出版社(1).doc”这个标题,第一次看到的人多半会愣一下:这不就是一份教材配套答案吗,跟工程落地有什么关系?但如果你带过新人、做过内部培训、或者维护过一套算法知识库,就会意识到这类文档的真正价值不在“答案”本身,而在于它是一份结构化的知识点索引。它把人工智能课程里的搜索、推理、学习、神经网络等章节,按题号和知识点一一对应起来,天然就是一份可检索、可复用的知识地图。
我见过太多团队把这类资料当成一次性消耗品:下载、翻两页、丢进网盘吃灰。真正会用它的人,会把它拆成可查询的问答对、可导入的知识库、可对照的复习清单。这篇文章要讲的,就是怎么把一份 Word 格式的课后习题答案,从“能看”变成“能用”——包括文档解析、结构化提取、检索增强,以及那些我踩过的坑。适合谁看?带新人的算法工程师、做内部知识库的开发者、以及想把教材内容变成自己工具链一部分的从业者。
2. 先搞清楚这份文档里到底有什么:结构拆解与内容边界
2.1 从章节标题反推知识点分布
拿到一份 Word 文档,第一步不是急着写代码,而是先人工翻一遍,搞清楚它的组织方式。常见的人工智能教材课后答案文档,结构大致是这样的:按教材章节分大节,每节下面按题号排列,每题包含“题目描述”和“参考答案”两部分,有些还会附上解题思路或公式推导。
我一般会先做一次“目录扫描”,把一级标题、二级标题、题号层级全部列出来,形成一张结构表。这张表决定了后面解析脚本怎么写。比如下面这种典型结构:
| 层级 | 示例内容 | 解析要点 |
|---|---|---|
| 章标题 | 第3章 搜索推理技术 | 作为一级分组键 |
| 节标题 | 3.2 启发式搜索 | 作为二级分组键 |
| 题号 | 3.2.1 简答题 | 作为题目唯一标识 |
| 题目正文 | 什么是A*算法? | 需要与答案分离 |
| 参考答案 | A*算法是一种… | 需要保留段落结构 |
这张表看起来简单,但它直接决定了你后面用 python-docx 还是用 pandoc 转 Markdown,决定了题号和答案怎么配对。很多解析翻车,就是因为没先做这一步,直接上代码,结果题号和答案错位。
2.2 用 python-docx 读取段落与样式的最小脚本
Word 文档的解析,最稳的起点是 python-docx。它不像直接解 XML 那么底层,也不像某些转换工具那样丢样式。下面这段代码是我常用的“探针脚本”,目的不是提取内容,而是先把文档的段落结构和样式打印出来,看清楚每一段到底是什么级别。
from docx import Document doc = Document("人工智能课后习题答案.docx") for i, para in enumerate(doc.paragraphs): text = para.text.strip() if not text: continue style_name = para.style.name if para.style else "None" # 打印段落索引、样式名、前50个字符 print(f"[{i}] style={style_name} | {text[:50]}")这段代码的逻辑很直接:遍历所有段落,跳过空行,输出样式名和文本片段。参数上唯一需要注意的是para.style.name,它返回的是 Word 内置样式名,比如Heading 1、Heading 2、Normal。如果文档作者用的是自定义样式,这里可能返回None或者自定义名称,那就需要结合字体大小、加粗等属性来判断层级。
跑完这个脚本,你会得到一份段落清单。如果发现题号和答案都在Normal样式里,没有用标题样式区分,那就说明这份文档的“结构”是视觉结构,不是语义结构。这时候要么手动补样式,要么在解析时用正则匹配题号模式,比如^\d+\.\d+这种。
2.3 题号与答案分离的三种常见模式
题号和答案怎么分离,是这类文档解析的核心难点。我总结下来常见三种模式:
第一种,题号和答案在同一段落,用“答:”或“参考答案:”分隔。这种最好处理,直接按关键词切分。
第二种,题目一段,答案一段,交替出现。这种需要判断段落顺序,通常题号段落以数字开头,答案段落以文字开头。
第三种,题目和答案在表格里,左边题号,右边答案。这种需要用doc.tables来读,而不是doc.paragraphs。
我一般会先写一个判断函数,检测文档里有没有表格,表格里有没有题号特征。如果有,优先走表格解析路径;如果没有,再走段落解析。这个判断逻辑不复杂,但能省掉后面大量返工。
提示:不要假设所有 Word 文档都是“标准格式”。我见过同一本教材的答案,前六章是段落,后四章是表格,原因是不同人整理的。解析脚本必须能兼容多种结构,或者至少能报错告诉你哪里不兼容。
3. 把 Word 变成可检索知识库:解析、清洗与入库
3.1 用正则提取题号、题干与答案的完整流程
假设我们已经确认文档是“题号+题干+答案”的段落模式,接下来就是用正则把三者拆出来。下面这段代码是我在实际项目中反复用过的版本,核心是三条正则:题号匹配、答案起始匹配、章节匹配。
import re from docx import Document doc = Document("人工智能课后习题答案.docx") # 匹配题号,如 3.2.1 或 3.2 pattern_qno = re.compile(r'^(\d+(?:\.\d+){1,3})\s*(.*)') # 匹配答案起始关键词 pattern_ans = re.compile(r'^(答|参考答案|解答)[::]\s*(.*)') # 匹配章标题,如 第3章 pattern_chap = re.compile(r'^第(\d+)章\s*(.*)') current_chap = "" current_qno = "" current_question = "" current_answer = [] records = [] for para in doc.paragraphs: text = para.text.strip() if not text: continue chap_match = pattern_chap.match(text) if chap_match: current_chap = text continue qno_match = pattern_qno.match(text) if qno_match: # 遇到新题号,先保存上一题 if current_qno: records.append({ "chapter": current_chap, "qno": current_qno, "question": current_question, "answer": "\n".join(current_answer) }) current_qno = qno_match.group(1) current_question = qno_match.group(2) current_answer = [] continue ans_match = pattern_ans.match(text) if ans_match: current_answer.append(ans_match.group(2)) else: # 没有答案关键词时,默认追加到答案区 if current_qno: current_answer.append(text) # 保存最后一题 if current_qno: records.append({ "chapter": current_chap, "qno": current_qno, "question": current_question, "answer": "\n".join(current_answer) }) print(f"共提取 {len(records)} 条记录")这段代码的关键参数是pattern_qno里的{1,3},它控制题号层级深度。如果题号是3.2.1.4这种四级编号,就要改成{1,4}。另外,答案追加逻辑里,我故意把没有匹配到“答:”的段落也追加到答案区,因为很多文档的答案并不以关键词开头,而是直接跟在题目后面。这个策略会带来少量误判,但比漏掉答案要好。
3.2 清洗噪声:页眉、页码、重复标题的处理
Word 文档转出来的文本,通常带着一堆噪声:页眉、页脚、页码、重复的章标题、甚至水印文字。这些东西如果不清理,入库后会严重干扰检索。比如你搜“A*算法”,结果返回一堆“第3章 搜索推理技术”的页眉,体验很差。
我一般会做三层清洗:
第一层,按长度过滤。少于 5 个字符的段落,大概率是页码或编号,直接丢弃。
第二层,按重复频率过滤。如果某个段落文本在文档里出现超过 10 次,且不是题号,大概率是页眉或页脚,丢弃。
第三层,按关键词过滤。包含“页码”“页眉”“水印”“仅供”等词的段落,直接丢弃。
from collections import Counter # 统计段落频率 all_texts = [p.text.strip() for p in doc.paragraphs if p.text.strip()] freq = Counter(all_texts) def is_noise(text): if len(text) < 5: return True if freq[text] > 10: return True noise_words = ["页码", "页眉", "水印", "仅供", "内部资料"] for w in noise_words: if w in text: return True return False clean_records = [] for r in records: if is_noise(r["question"]) and is_noise(r["answer"]): continue clean_records.append(r) print(f"清洗后剩余 {len(clean_records)} 条")这段代码里,freq[text] > 10这个阈值不是固定的,取决于文档长度。如果文档只有 20 页,阈值可以设 5;如果 200 页,设 10 到 15 都合理。我一般会先打印频率最高的 20 个段落,人工看一眼,再定阈值。
3.3 写入 SQLite 并建立全文索引
清洗完的数据,下一步是入库。对于这种规模的知识库,SQLite 足够用,而且自带 FTS5 全文索引,检索速度很快。下面是把记录写入 SQLite 并建索引的代码。
import sqlite3 conn = sqlite3.connect("ai_qa.db") cur = conn.cursor() # 建表 cur.execute(""" CREATE TABLE IF NOT EXISTS qa ( id INTEGER PRIMARY KEY AUTOINCREMENT, chapter TEXT, qno TEXT, question TEXT, answer TEXT ) """) # 建全文索引表 cur.execute(""" CREATE VIRTUAL TABLE IF NOT EXISTS qa_fts USING fts5( question, answer, content='qa', content_rowid='id' ) """) # 插入数据 for r in clean_records: cur.execute( "INSERT INTO qa (chapter, qno, question, answer) VALUES (?, ?, ?, ?)", (r["chapter"], r["qno"], r["question"], r["answer"]) ) # 同步全文索引 cur.execute("INSERT INTO qa_fts(qa_fts) VALUES('rebuild')") conn.commit() conn.close()这里的关键是content='qa'和content_rowid='id',它让 FTS5 索引表跟主表关联,避免数据冗余。rebuild命令会在每次插入后重建索引,数据量不大时没问题;如果数据量上万,建议改成增量插入。检索时用SELECT * FROM qa_fts WHERE qa_fts MATCH 'A*算法'就能快速命中。
注意:FTS5 对中文分词支持有限,默认按字符切分。如果检索效果不理想,可以考虑先做一遍中文分词,把分词结果存到单独字段,再建索引。常见做法是用 jieba 分词后空格拼接。
4. 避坑指南:解析课后答案文档时最容易翻车的五个点
4.1 题号识别错误导致答案错位
现象:检索“什么是深度学习”,返回的答案却是“什么是神经网络”的内容。
原因:题号正则写得太宽,把正文里的数字列表也当成了题号。比如答案里出现“1. 首先……2. 其次……”,被误判为新题号,导致答案被截断。
解决:题号正则加上层级限制,比如要求至少包含一个点号,且点号前后都是数字。同时,题号段落通常较短,可以加一个长度阈值,超过 50 字符的段落不当作题号。
4.2 表格与段落混排时漏读内容
现象:解析完发现少了整整一章的题目。
原因:那一章的内容在 Word 里是表格形式,而脚本只遍历了doc.paragraphs,没有遍历doc.tables。
解决:解析前先检查len(doc.tables),如果有表格,单独写一套表格解析逻辑。表格里通常第一列是题号,第二列是题目,第三列是答案,按列读取即可。
4.3 答案跨段落被截断
现象:答案只保留了第一段,后面的推导过程全丢了。
原因:脚本遇到下一个题号时,直接保存当前答案,但答案本身可能包含多个段落,中间没有题号。
解决:答案收集逻辑要持续到下一个题号出现为止,而不是遇到空行就停。我一般会用一个current_answer列表,持续追加,直到匹配到新题号才保存。
4.4 编码问题导致中文乱码
现象:读取出来的文本全是\uXXXX或者问号。
原因:Word 文档本身编码没问题,但如果你先用 pandoc 转成 txt,再用默认编码读取,就可能乱码。
解决:直接用 python-docx 读取,它内部处理了编码。如果必须转 txt,确保指定encoding='utf-8'。另外,Windows 下用open()时记得加encoding参数,不要依赖默认编码。
4.5 全文索引对公式和符号不友好
现象:搜“梯度下降”能搜到,搜“∇”或“α”搜不到。
原因:FTS5 默认分词器会把符号过滤掉,公式里的特殊字符无法命中。
解决:如果文档里公式多,建议在入库前把公式转成文字描述,比如“∇”转成“梯度算子”。或者单独建一个公式字段,用 LIKE 查询兜底。我一般会在检索层做一次查询改写,把常见符号映射成文字。
5. 让答案文档真正好用:检索增强与问答对导出
5.1 用 FTS5 做关键词检索的最小查询
入库之后,最简单的用法就是关键词检索。下面这段代码演示了怎么用 FTS5 查询,并返回高亮片段。
import sqlite3 conn = sqlite3.connect("ai_qa.db") cur = conn.cursor() def search(keyword, limit=5): cur.execute(""" SELECT qa.chapter, qa.qno, qa.question, snippet(qa_fts, 1, '【', '】', '...', 20) as ans_snippet FROM qa_fts JOIN qa ON qa.id = qa_fts.rowid WHERE qa_fts MATCH ? LIMIT ? """, (keyword, limit)) return cur.fetchall() results = search("A*算法") for r in results: print(f"章节:{r[0]} | 题号:{r[1]}") print(f"题目:{r[2]}") print(f"答案片段:{r[3]}") print("-" * 40)snippet函数的参数依次是:索引表名、列索引(1 表示 answer 列)、高亮前缀、高亮后缀、省略号、片段长度。这个函数在展示检索结果时非常有用,能让用户一眼看到关键词在答案里的位置。
5.2 把问答对导出成 JSONL 供下游使用
检索只是第一步,很多团队需要把问答对导出成 JSONL,喂给下游的模型训练或 RAG 系统。导出逻辑很简单,但要注意字段命名和编码。
import json def export_jsonl(db_path, out_path): conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute("SELECT chapter, qno, question, answer FROM qa") with open(out_path, "w", encoding="utf-8") as f: for row in cur.fetchall(): obj = { "chapter": row[0], "qno": row[1], "question": row[2], "answer": row[3] } f.write(json.dumps(obj, ensure_ascii=False) + "\n") conn.close() export_jsonl("ai_qa.db", "ai_qa.jsonl")ensure_ascii=False是关键参数,不加的话中文会被转成\uXXXX,虽然不影响解析,但可读性差。导出的 JSONL 可以直接被 LangChain、LlamaIndex 这类框架读取,作为知识库的原始数据。
5.3 用简单规则做问答匹配的兜底方案
FTS5 适合关键词检索,但如果用户输入的是自然语言问题,比如“A*算法和Dijkstra有什么区别”,直接 MATCH 可能命中率不高。这时候可以加一层规则兜底:先提取问题里的关键词,再用关键词去检索。
import jieba def extract_keywords(question): # 去掉停用词,保留名词和动词 words = jieba.lcut(question) stopwords = {"的", "是", "什么", "区别", "和", "与", "有", "吗"} return [w for w in words if w not in stopwords and len(w) > 1] def smart_search(question): keywords = extract_keywords(question) if not keywords: return [] # 用 OR 连接关键词,提高召回 query = " OR ".join(keywords) return search(query) results = smart_search("A*算法和Dijkstra有什么区别") for r in results: print(r[2])这段代码的核心是extract_keywords,它用 jieba 分词后过滤停用词。smart_search把关键词用 OR 连接,牺牲精确率换召回率。对于知识库场景,召回率通常比精确率重要,因为用户能自己筛选结果。
提示:如果检索结果太多,可以在 OR 查询后面加
ORDER BY rank,FTS5 自带 rank 排序,能按相关度返回。
6. 从一份文档到一套工具:我的复用习惯与验证方法
把一份课后习题答案文档变成可检索知识库,这件事本身不难,难的是让它持续可用。我自己的习惯是,每拿到一份新的教材答案文档,先跑一遍“探针脚本”,看清楚结构,再决定用哪套解析策略。解析完不急着入库,先随机抽 10 条记录,人工对照原文,确认题号和答案没有错位。这个验证步骤花不了十分钟,但能避免后面大量返工。
验证检索效果时,我会准备一组“黄金问题”,比如“什么是启发式搜索”“神经网络的反向传播怎么推导”,然后看检索结果是否命中对应题号。如果命中率低于 80%,就回头检查分词和索引配置。这个习惯让我在多个内部知识库项目里少踩了很多坑。
还有一个技巧:把解析脚本和验证脚本放在同一个目录,用 Makefile 串起来。每次更新文档,只需要make parse和make verify两条命令,就能完成从解析到验证的全流程。这样即使文档更新频繁,也不会手忙脚乱。
最后说一个我自己的教训。早期我做这类解析时,总想一步到位,写一个“万能脚本”兼容所有格式。结果脚本越来越复杂,维护成本越来越高,遇到新格式还是翻车。后来我改成“一格式一脚本”,每种文档结构对应一个独立解析器,用配置文件切换。虽然看起来笨,但稳定性高得多。希望这个思路能帮到你。
本文还有配套的精品资源,点击获取