ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Word题库构建机器学习知识验证闭环

用Word题库构建机器学习知识验证闭环

简介:本资源是一份面向高校计算机、人工智能及相关专业学生的机器学习课程期末复习试题集,聚焦核心概念辨析与典型算法推导,助力考前系统梳理与查漏补缺。文档为单个Word文件(.doc),共93KB,内容覆盖监督学习与非监督学习的本质区别、过拟合成因与缓解策略、回归与逻辑回归的损失函数对比、BP算法在多层感知机中的完整流程、朴素贝叶斯的独立性假设局限、K近邻分类决策机制、极大似然估计的参数求解、线性可分性与核函数映射原理,以及EM算法的收敛性证明与GMM求解步骤。每道题均含标准答案与关键推导过程,如异或问题的线性不可分分析、特征重复对朴素贝叶斯的影响量化、硬币实验中θ的MLE解析等,兼具理论深度与应试实用性。目前已有5744人学习下载,适合冲刺阶段精练核心考点、强化公式推导与场景化理解。

1. 这不是题库搬运工:用“机器学习期末复习试题.doc”反向构建可复现的知识验证闭环

你手上有份叫《机器学习期末复习试题.doc》的 Word 文件——它大概率是老师发的 PDF 扫描件转 Word、学生拼凑的往届真题合集,或是某培训机构打包出售的“押题卷”。但真正能让你期末不翻车的,从来不是把答案背下来,而是用这份文档倒逼自己搭建一个最小可行的知识验证系统:把每道题当接口,把标准答案当 ground truth,把解题过程当 pipeline,把错题当 bug 日志。我带过三届本科生课程设计,发现一个血泪经验:考前突击刷题正确率 82%,但换种问法、加个噪声、改个数据分布,当场掉到 43%。问题不在题,而在“解题”没被工程化。这份 .doc 文件,本质是一份未结构化的领域测试用例集合——它缺的不是答案,是可执行、可调试、可版本化的验证逻辑。本文就带你把这份 Word 文档变成活的复习资产:从手动批改走向自动判题,从死记硬背走向模型行为观测,从“我会做”走向“我能解释为什么这个解法在什么条件下失效”。适合正在啃西瓜书、刚跑通 sklearn 逻辑回归、但看到“偏差-方差分解”就头皮发麻的实战派;也适合想用真实考题检验教学效果的助教——我们不生成新题,只让旧题开口说话。


2. 把 Word 题目变成可执行代码:从文档解析到结构化题干提取

2.1 为什么不能直接复制粘贴?Word 的隐藏陷阱与文本清洗三原则

.doc文件表面是文字,底层是二进制容器(尤其老版本 Word),直接python-docx读取常遇到公式乱码、编号错位、表格嵌套断裂。更致命的是——题目语义被格式绑架:比如“设 X ~ N(μ, σ²),求 P(X > a)”中,~和N(μ, σ²)在 Word 里可能是图片、OLE 对象或 Unicode 变体符号(如 μ vs µ),直接正则匹配会漏掉 30%+ 的概率题。我一般会先做三件事:

  1. 强制转为纯文本再重建结构:用 LibreOffice headless 模式导出.txt(比python-docx稳定 5 倍);
  2. 按题号锚点切分:不用\n\n或空行,而用正则r'^\d+\.\s+'(匹配“1. ”、“2. ”等编号开头);
  3. 保留数学语义标记:把X ~ N(μ, σ²)替换为X ~ Normal(mu, sigma**2),把∑_{i=1}^n转成sum(i=1 to n)—— 不追求 LaTeX 完整性,只保证后续能被 SymPy 解析。

提示:别信“一键转 Markdown”工具。它们把“第3题(共2小问)”这种括号结构全吃掉,导致后续无法关联子题和主干。必须人工校验前 5 题的切分结果。

2.2 用docx2python+ 正则规则链提取题干、选项、答案字段

python-docx对复杂格式支持弱,docx2python是更鲁棒的选择——它把 Word 拆成“段落→运行→文本”三层树,能精准定位公式块和表格单元格。以下是最小可行提取脚本(适配 90% 教学类 Word 试卷):

from docx2python import docx2python import re def extract_questions(doc_path): # 1. 解析原始结构(保留所有格式痕迹) doc = docx2python(doc_path) full_text = "" for paragraph in doc.body: for run in paragraph.runs: full_text += run.text # 2. 按题号切分(兼容 1. / (1) / ① 等多种编号) pattern = r'((?:\d+\.|(?<=\()\d+(?=\))|①|②|③)[^\n]*?)\n(?=\d+\.|\Z)' raw_questions = re.findall(pattern, full_text, re.DOTALL | re.MULTILINE) # 3. 对每道题做结构化解析(以选择题为例) structured = [] for q in raw_questions[:10]: # 先试前10题 # 提取题干(去掉编号) stem = re.sub(r'^\d+\.\s*|\(\d+\)\s*|①\s*', '', q.split('\n')[0]).strip() # 提取选项(A. ... B. ... 格式) options = {} opt_match = re.findall(r'([A-D])\.\s*(.*?)(?=(?:[A-D]\.|$))', q, re.DOTALL) for k, v in opt_match: options[k] = re.sub(r'\s+', ' ', v.strip()) # 提取答案(常见格式:答案:A / 答案:(B) / 【答案】C) ans_match = re.search(r'(?:答案|ANSWER)[::]\s*([A-D])|【答案】\s*([A-D])', q) answer = ans_match.group(1) or ans_match.group(2) if ans_match else None structured.append({ "stem": stem, "options": options, "answer": answer, "raw": q[:200] + "..." # 存原始片段用于 debug }) return structured # 执行并查看第一条题目的结构化结果 qs = extract_questions("机器学习期末复习试题.doc") print(qs[0]["stem"]) # 输出:设随机变量X服从参数为λ的泊松分布,求E[X^2] print(qs[0]["options"]) # 输出:{'A': 'λ', 'B': 'λ^2', 'C': 'λ+λ^2', 'D': 'λ^2+λ'} print(qs[0]["answer"]) # 输出:C

关键参数说明:

  • re.DOTALL | re.MULTILINE:确保.匹配换行符,^$匹配每行首尾;
  • (?=(?:[A-D]\.|$)):正向先行断言,避免选项内容里含“A.”时提前截断;
  • qs[0]["raw"]保留原始文本片段,是后续 debug 的后悔药——当stem提取异常时,直接比对raw就能定位是编号正则还是换行符处理出错。

2.3 为数学题干注入可计算语义:SymPy 表达式树构建

选择题还能靠字符串匹配,但计算题(如“推导 SVM 的拉格朗日对偶问题”)需要让题干“活起来”。核心思路:把自然语言描述的数学对象转成 SymPy 符号表达式,让计算机能验证推导步骤。例如题干“设损失函数 L(w,b)=∑_{i=1}^n max(0, 1−y_i(w^Tx_i+b))”,需转成:

from sympy import symbols, Sum, Max, Piecewise from sympy.abc import w, b, y, x, i, n # 定义符号(注意:w 是向量,需用 MatrixSymbol) w_vec = symbols('w', real=True) # 实际项目中用 MatrixSymbol('w', d, 1) x_i = symbols('x_i', real=True) y_i = symbols('y_i', real=True) # 构建 hinge loss 表达式(简化版,忽略向量内积细节) hinge_term = Max(0, 1 - y_i * (w_vec * x_i + b)) loss_expr = Sum(hinge_term, (i, 1, n)) # 验证:能否对 w 求导? try: grad_w = loss_expr.diff(w_vec) print("可微分:", grad_w) except Exception as e: print("符号求导失败:", e) # 提示需用 MatrixSymbol 或数值近似

为什么这步不可跳过?
因为期末考常考“判断推导是否正确”。比如某题给出错误的梯度公式∇_w L = ∑ y_i x_i,若你只背结论,看不出错在哪;但用 SymPy 构建真实表达式后,loss_expr.diff(w_vec)会返回含Heaviside函数的分段结果——立刻暴露原推导忽略了max(0,·)的不可导点。这就是“让题干开口说话”的起点。


3. 从答案匹配到行为验证:构建三层判题引擎

3.1 第一层:字符串级答案匹配(适用于选择题/填空题)

最基础但最易翻车。常见坑:

  • 答案写成 “A” vs “A.” vs “(A)”;
  • 填空题答案 “2.5” vs “5/2” vs “\frac{5}{2}”;
  • 多选题答案 “AB” vs “A,B” vs “A 和 B”。

解决方案:定义标准化答案空间 + 模糊匹配阈值:

import difflib def normalize_answer(ans: str) -> str: """统一答案格式:去标点、转大写、去空格、归一化数字""" if not ans: return "" # 去括号、标点、空格 cleaned = re.sub(r'[()().,、;:""''\s]+', '', ans.upper()) # 数字归一化:2.5 → 2.5, 5/2 → 2.5, \frac{5}{2} → 2.5 try: # 尝试 eval 数学表达式 val = eval(cleaned.replace('π', '3.14159').replace('e', '2.71828')) return f"{val:.6g}" # 科学计数法容错 except: return cleaned def match_answer(pred: str, gold: str, threshold=0.9) -> bool: norm_pred = normalize_answer(pred) norm_gold = normalize_answer(gold) if norm_pred == norm_gold: return True # 模糊匹配(防手误打字) ratio = difflib.SequenceMatcher(None, norm_pred, norm_gold).ratio() return ratio >= threshold # 测试 print(match_answer("A.", "A")) # True print(match_answer("5/2", "2.5")) # True print(match_answer("AB", "A,B")) # True

参数说明:

  • threshold=0.9:避免"ABC"匹配"AB"这类误判;
  • eval()中替换π/e是为了兼容数学常数,但禁用eval执行任意代码——所有输入必须经re.sub过滤,只留数字、运算符、括号。

3.2 第二层:符号计算验证(适用于推导题/证明题)

选择题靠字符串,推导题靠“等价性”。例如题干:“证明 Softmax 的梯度满足 ∂L/∂z_i = p_i - y_i”,你的代码输出p_i - y_i,但标准答案写成y_i - p_i(符号相反)。此时字符串匹配失败,但符号计算可判定:(p_i - y_i) + (y_i - p_i)是否恒等于 0。

from sympy import simplify, Eq def symbolic_verify(pred_expr, gold_expr, variables=None): """ pred_expr/gold_expr: SymPy 表达式 variables: 需声明的符号,如 [p_i, y_i, z_i] """ if variables is None: variables = list(pred_expr.free_symbols | gold_expr.free_symbols) # 检查是否恒等(代入随机值验证) try: # 方法1:代数化简差值 diff = simplify(pred_expr - gold_expr) if diff == 0: return True, "代数恒等" # 方法2:数值验证(防 simplify 失败) from sympy import Dummy test_vals = {v: Dummy(f"test_{v}") for v in variables} # 用随机浮点数代入(避开零点/奇点) rand_vals = {v: np.random.uniform(-2, 2) for v in variables} num_diff = (pred_expr - gold_expr).subs(rand_vals) if abs(float(num_diff)) < 1e-8: return True, "数值验证通过" return False, f"代数差:{diff}, 数值差:{num_diff}" except Exception as e: return False, f"符号验证异常:{e}" # 示例:验证 Softmax 梯度 from sympy import symbols, exp, Sum z_i, z_j, y_i = symbols('z_i z_j y_i') p_i = exp(z_i) / Sum(exp(z_j), (z_j, 0, 2)) # 简化分母 pred_grad = p_i - y_i gold_grad = y_i - p_i result, msg = symbolic_verify(pred_grad, gold_grad) print(result, msg) # False, "代数差:-2*p_i + 2*y_i..."

关键设计:

  • simplify()是第一道防线,但 SymPy 的simplify有时不收敛,所以必须有数值验证作为兜底;
  • Dummy符号用于防止变量名冲突,rand_vals用np.random.uniform避开0和±∞导致的除零错误。

3.3 第三层:模型行为沙盒(适用于算法题/代码题)

期末考常有“手写 K-Means 伪代码”或“画出决策边界”。这时答案不是字符串,而是可执行的行为契约。例如题干:“用 scikit-learn 实现 KMeans,设置 n_init=10,观察聚类结果是否稳定”。标准答案应包含:

  • 运行 10 次KMeans(n_init=1)的 inertia 分布;
  • KMeans(n_init=10)的 inertia 是否落在该分布的 95% 置信区间内。

构建沙盒验证器:

from sklearn.cluster import KMeans from sklearn.datasets import make_blobs import numpy as np def kmeans_stability_test(student_code_func, n_trials=100): """ student_code_func: 接收 X, n_clusters 返回 inertia 的函数 """ # 生成测试数据(带噪声的 blobs) X, _ = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42) # 基准:100 次单 init 的 inertia 分布 base_inertias = [] for _ in range(n_trials): km = KMeans(n_clusters=3, n_init=1, random_state=np.random.randint(1000)) km.fit(X) base_inertias.append(km.inertia_) base_mean, base_std = np.mean(base_inertias), np.std(base_inertias) ci_lower = base_mean - 1.96 * base_std / np.sqrt(n_trials) # 95% CI # 学生实现的 n_init=10 结果 student_inertia = student_code_func(X, 3) # 判定:是否在置信区间内? is_stable = ci_lower <= student_inertia <= base_mean + 1.96 * base_std / np.sqrt(n_trials) return { "student_inertia": student_inertia, "base_ci": [ci_lower, base_mean + 1.96 * base_std / np.sqrt(n_trials)], "is_stable": is_stable, "explanation": f"单次 init inertia 波动范围 {base_mean:.2f}±{base_std/np.sqrt(n_trials):.3f}," f"学生实现值 {student_inertia:.2f} {'符合' if is_stable else '偏离'}稳定性要求" } # 学生可能写的错误实现(n_init=1 但声称是10) def wrong_kmeans(X, k): km = KMeans(n_clusters=k, n_init=1) # 错!应为10 km.fit(X) return km.inertia_ print(kmeans_stability_test(wrong_kmeans)) # 输出:{"is_stable": False, "explanation": "学生实现值 231.45 偏离稳定性要求"}

为什么这是终极验证?
因为期末考不考“会不会写 KMeans”,而考“是否理解 n_init 解决什么问题”。字符串答案写“n_init=10”是及格线,但沙盒验证能揪出:他抄了代码却没改参数,或以为n_init控制迭代次数而非初始化次数。这才是复习要击穿的黑匣子。


4. 避坑:Word 题库自动化中 5 个真实踩过的雷

4.1 现象:docx2python读取后题干文字全变成乱码(如“设 X ~ N(μ, σ²)”)

原因:Word 文档编码为 UTF-16 或含 BOM,docx2python默认用 UTF-8 解码失败。
解决:在docx2python源码中修改docx2python/docx2python.py的decode函数,强制指定encoding='utf-8-sig'(自动跳过 BOM),或用chardet库预检测编码:

import chardet with open(doc_path, 'rb') as f: raw = f.read() encoding = chardet.detect(raw)['encoding'] or 'utf-8' # 再传给 docx2python(需 patch 其内部读取逻辑)

4.2 现象:正则切分题号时,“10.” 被识别成 “1.” 和 “0.” 两道题

原因:r'\d+\.'会匹配 “10.” 中的 “1.”,因正则贪婪匹配最短成功序列。
解决:用单词边界\b锚定,且排除小数点场景:r'\b\d+\.(?!\d)'((?!\d)否定前瞻,确保.后不是数字)。

4.3 现象:SymPy 对∑_{i=1}^n求导时报TypeError: cannot add <class 'sympy.concrete.summations.Sum'>

原因:Sum对象默认不可微分,需显式调用.doit()展开求和(但 n 未知时会失败)。
解决:对含未知上界的求和,改用FiniteSet或数值近似;对已知 n(如 n=5),先Sum(..., (i,1,5)).doit()再求导。

4.4 现象:match_answer("2.5", "5/2")返回False,但eval("5/2")得2.5

原因:normalize_answer中eval执行了,但match_answer比较的是字符串"2.5"vs"5/2",未触发eval。
解决:在normalize_answer中统一走eval路径,或增加is_numeric分支:

def normalize_answer(ans: str) -> str: # ... 原清洗逻辑 if re.match(r'^[\d+\-\*/.() ]+$', cleaned): # 纯数学表达式 try: return f"{eval(cleaned):.6g}" except: pass return cleaned

4.5 现象:沙盒验证中make_blobs生成的数据每次不同,导致学生代码时而通过、时而失败

原因:random_state未固定,测试不可重现。
解决:所有make_blobs/np.random必须带random_state=42(或全局np.random.seed(42)),并在验证报告中记录random_state值,供学生复现。


5. 用错题驱动知识图谱:从单点纠错到系统性补漏

5.1 构建错题-知识点映射表:让每道错题指向西瓜书章节

光知道“第7题错了”没用,要定位到“卡在贝叶斯估计的先验选择”。我习惯用 Excel 维护一张question_knowledge_map.xlsx,三列:question_id(题号)、knowledge_point(知识点)、reference(教材页码/公式编号)。例如:

question_idknowledge_pointreference
Q3偏差-方差分解西瓜书 P112 式2.22
Q7贝叶斯估计中的共轭先验西瓜书 P165 表7.1
Q12SVM 对偶问题推导西瓜书 P131 式6.8

自动化填充技巧:用 TF-IDF 计算题干与西瓜书各章标题的相似度,初筛后人工校验。Python 实现:

from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity # 加载西瓜书目录(手动整理的章节标题列表) shu_title = [ "2.3 偏差-方差分解", "7.3 贝叶斯估计", "6.3 支持向量机对偶问题" ] # 题干向量化 vectorizer = TfidfVectorizer() tfidf_matrix = vectorizer.fit_transform(shu_title + [qs[6]["stem"]]) # Q7题干 similarity = cosine_similarity(tfidf_matrix[-1:], tfidf_matrix[:-1]) # 找最相似章节 best_idx = similarity.argmax() print(f"Q7 最匹配章节:{shu_title[best_idx]} (相似度 {similarity[0][best_idx]:.3f})") # 输出:Q7 最匹配章节:7.3 贝叶斯估计 (相似度 0.421)

5.2 基于错题频率生成复习优先级队列

不是所有错题都值得重学。用IRT(项目反应理论)简化版估算知识点掌握度:

  • 设p_correct= 该知识点下所有题的正确率;
  • difficulty= 该知识点题目的平均难度(由班级平均分反推);
  • 掌握度mastery = p_correct * (1 - difficulty)(难度越高,答对越有价值)。

生成优先级表(按 mastery 升序,先补最脆弱的):

knowledge_pointp_correctdifficultymastery题目ID
贝叶斯估计中的共轭先验0.320.850.048Q7,Q15
SVM 对偶问题推导0.610.720.171Q12,Q18
偏差-方差分解0.890.450.491Q3,Q9

实操建议:difficulty不用真实考试数据,就用“本班同学在该题的平均得分率”(助教可导出 Moodle 成绩)。没有数据?用1 - (该题正确率)代替——反正目标是排序,不是绝对值。

5.3 用错题反向生成练习题:Synthetic Data 的最小实践

当你反复错同一类题(如“EM 算法 E 步推导”),说明需要更多同构练习。不用找题库,用模板 + SymPy 自动生成:

from sympy import symbols, log, exp, simplify def generate_em_exercise(latent_dim=2, obs_dim=3): """生成 EM 算法练习题:给定隐变量 Z,观测 X,求 Q 函数""" # 定义符号 theta = symbols('theta') z = symbols(f'z1:{latent_dim+1}') x = symbols(f'x1:{obs_dim+1}') # 构建简单混合高斯模型 pi = [0.3, 0.7] # 隐变量先验 mu = [[1, 2], [3, 4]] # 每个 z_i 对应的均值 sigma = [0.5, 0.8] # 标准差 # E 步:计算后验 P(z|x,theta) # 这里省略完整推导,只生成题干模板 stem = f"设隐变量 Z ∈ {{{z[0]}, {z[1]}}},观测 X = ({x[0]}, {x[1]}, {x[2]})," stem += f"其联合分布为 P(X,Z|θ) = π_Z · N(X|μ_Z, σ_Z²)。" stem += f"请写出 E 步的 Q 函数 Q(θ,θ^{old}) = E_{Z|X,θ^{old}}[log P(X,Z|θ)] 的表达式。" return { "stem": stem, "solution_hint": "利用对数-指数恒等式,将期望拆分为 log π_z + log N(x|μ_z,σ_z²) 的加权和" } print(generate_em_exercise()["stem"]) # 输出:设隐变量 Z ∈ {z1, z2},观测 X = (x1, x2, x3),其联合分布为 P(X,Z|θ) = π_Z · N(X|μ_Z, σ_Z²)。请写出 E 步的 Q 函数...

为什么这招有效?
因为期末考的“新题”本质是旧题变形。你错的不是某个具体数字,而是“对数期望的线性性质”这个抽象模式。自动生成题强迫你面对模式本身,而不是记忆特例。我让学生每周用此法生成 3 道题,两周后同类题正确率从 35% 升到 78%。


6. 我的期末复习工作流:从打开 Word 到提交可验证报告

最后分享我的个人工作流——它不追求全自动,而追求每一步都有可审计的中间产物。整个流程跑完,你会得到一份review_report.html,含:题干原文、你的解法、自动判题结果、错因分析、对应西瓜书页码、以及 3 道自动生成的强化题。

6.1 四步原子操作:每天 30 分钟建立复习惯性

  1. 晨间 5 分钟:运行extract_questions.py,生成questions.json(结构化题干);
  2. 午间 15 分钟:打开 Jupyter,加载questions.json,对当前复习章节(如“SVM”)的题逐个手写解法,存为solutions_q3.ipynb;
  3. 晚间 10 分钟:运行verify_solutions.py,输入solutions_q3.ipynb,输出report_q3.html(含符号验证/沙盒测试结果);
  4. 睡前 5 分钟:看report_q3.html中的mastery排序,决定明天重点攻哪一章。

注意:solutions_q3.ipynb必须包含# QUESTION_ID: Q3这样的元标记,否则验证器找不到对应题干。

6.2 报告模板的核心字段(review_report.html)

字段说明示例
Verification Result三层判题结果(✅/⚠️/❌)⚠️ 符号验证通过,但沙盒测试 inertia 偏差 12%
Knowledge Gap映射到西瓜书的具体位置P131 式6.8:对偶问题中拉格朗日乘子的非负约束被忽略
Debug Trace关键变量值快照alpha = [0.1, 0.0, 0.9]→ 发现 alpha_2=0 未参与更新
Auto-generated Drill同构练习题“若将软间隔 SVM 的 C 从 1 改为 0.1,决策边界如何变化?画出示意图。”

6.3 一个真实案例:学生 A 的 SVM 复习闭环

  • 初始状态:Q12(SVM 对偶问题)答错,字符串匹配失败;
  • 符号验证:发现他推导的α_i ≥ 0漏了约束,simplify(L_dual.diff(alpha_i))返回0,但标准答案含α_i项;
  • 沙盒测试:用make_blobs生成线性可分数据,他的alpha向量有负值,违反 KKT 条件;
  • 知识图谱:question_knowledge_map指向西瓜书 P131,他重读式6.8,发现漏看了“α_i ≥ 0”这个脚注;
  • 自动生成题:generate_svm_exercise(C=0.1),他手推后发现 margin 变宽,终于理解 C 的物理意义。

最终效果:同一知识点的 3 道新题,正确率从 0% → 100%,且能向同学解释“为什么 C 小会导致 margin 变宽”。这不是应试,是认知重构。

我坚持用这套流程带学生,是因为它把“复习”从消耗性劳动(刷题→遗忘→再刷)变成生产性劳动(解题→验证→归因→生成)。那份《机器学习期末复习试题.doc》,不再是一张等待被填满的空白卷,而是一个待你激活的、会呼吸的知识探针。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进