简介:这份人工智能经典习题集与各章总结面向高校人工智能课程学习者及期末备考学生,围绕概念梳理、习题训练与章节复盘,帮助快速建立从理论到解题的复习路径。压缩包内共1个doc文件,约2.19MB,内容以章节小结和习题解答为主,覆盖绪论、知识与知识表示、推理等核心模块,并涉及产生式规则、框架表示、语义网络、归结演绎推理等典型题型与解题过程。已有101人学习,适合在考前对照课堂重点查漏补缺。读者可借助各章总结串联人工智能的发展阶段、研究学派与主要研究领域,再通过习题答案理解知识表示、推理求解等问题的分析步骤与规范写法,提升理论理解与实际解题能力。
1. 背了两遍习题集还是不会做题:问题出在少了一条可验证的路径
期末前两周,备考群里最常见的一句话是「人工智能经典习题集刷了两遍,换个数字还是不会」。这不是题做得少,而是把习题当成了背诵材料:答案记住了,生成答案的那条路径没记住。人工智能经典习题集和各章总结真正的价值,在于每一章背后都对应一套可运行的机制——搜索、推理、学习、优化,每一类都能用十几行代码跑出来,跑通之后再回头对答案,错在哪一步是可见的;只对着答案看,对错都是一笔糊涂账。
这套做法适合三类人:正在准备期末考试、想把题型真正吃透的人;要带习题课、需要一套能被质疑和验证的讲法的人;以及想用最小代价把经典算法重新捡起来的开发者。下面按「先建索引、再逐类跑通、最后压缩成自测矩阵」的顺序展开,每一步都落到能跑、能对拍、能复现。
2. 给习题集建三轴索引:按章、按题型、按考点打标的最小可行方案
散落的习题最大的问题不是难,而是不可检索。同一道题你可能在教材课后题、往年卷、习题册三处各见过一次,但脑子里没有一条线把它们串起来,于是每次都是重新学一遍。索引解决的就是这件事:把「我做过这道题」变成「我知道这类题在考什么、我上次错在哪个判断上」。
2.1 为什么页码索引在考前两周必然失效
按页码整理笔记,本质上是按「资料的组织方式」整理,而不是按「你的掌握方式」整理。教材第三章第二节的题目和第五章第一节的题目,可能考的是同一个东西——比如都是用信息增益做特征选择,只是一个在分类章、一个在特征工程章。页码索引会把你自己的知识割成互不相干的块。
三轴打标换了一个切法:章(chapter)负责定位知识块,题型(type)负责定位解题动作,考点(topic)负责定位判断依据。考前两周真正的复习单位是考点,不是章节。你会发现按考点聚合之后,几十道题收敛成十几个考点,每个考点下有两三道典型题,剩下的都是同一套判据的变形。
我一般会把题型固定成五类,不要更多:计算(calc)、证明(proof)、设计(design)、简答(short)、编程(code)。固定枚举的好处是能统计——统计出来哪一类占比最高、自己哪一类正确率最低,复习顺序就有依据了,不用凭感觉。
考点这一轴最难统一,常见翻车是同一件事写成三种说法:「信息增益」「ID3 的划分标准」「熵减」其实是同一个考点。处理办法是先写一版自由文本,跑二十道题之后把高频说法归并成受控词表,再回头批量替换。这一步值得花半小时,后面所有筛选都靠它。
提示:考点词表控制在 30~50 个之间。少于 30 说明粒度太粗,筛出来还是一堆题;多于 50 说明粒度太细,几乎每条都是新词,索引退化成标签垃圾场。
2.2 用一段 Python 把散题整理成可筛选的 JSONL 索引
存储格式用 JSONL,一行一条记录,好处是可以直接用文本编辑器手改、能进版本管理、读进来逐行校验。字段固定成七个,缺一个就报错,逼自己填完整。
import json, re from collections import Counter # 最小字段集:少一个就拒绝载入,避免"半成品索引"混进来 FIELDS = ["id", "chapter", "topic", "type", "difficulty", "source", "state"] def load(path): rows = [] with open(path, encoding="utf-8") as f: for lineno, line in enumerate(f, 1): line = line.strip() if not line: continue r = json.loads(line) missing = [k for k in FIELDS if k not in r] if missing: raise ValueError(f"第 {lineno} 行缺字段: {missing}") rows.append(r) return rows def normalize(rows): # 章号统一成 int:"第三章" / "ch3" / "3" 全部归一 # 题型和状态统一成小写英文枚举,方便 Counter 统计 for r in rows: digits = re.sub(r"\D", "", str(r["chapter"])) r["chapter"] = int(digits) if digits else 0 r["type"] = str(r["type"]).strip().lower() r["state"] = str(r["state"]).strip().lower() r["difficulty"] = int(r["difficulty"]) return rows def stats(rows): by_chapter = Counter(r["chapter"] for r in rows) by_type = Counter(r["type"] for r in rows) # state 只有 new / done / wrong 三个值,wrong 是复习队列的第一优先级 weak = [r["id"] for r in rows if r["state"] in ("new", "wrong")] return { "by_chapter": sorted(by_chapter.items()), "by_type": by_type.most_common(), "weak_count": len(weak), "weak_ids": weak, } def pick(rows, chapters=None, types=None, max_difficulty=5, limit=20): out = [] for r in rows: if chapters and r["chapter"] not in chapters: continue if types and r["type"] not in types: continue if r["difficulty"] > max_difficulty: continue if r["state"] == "done": # 已经能复述的题先排除 continue out.append(r) return out[:limit] if __name__ == "__main__": data = normalize(load("exercises.jsonl")) print(stats(data)) print(pick(data, chapters=[3, 5], types=["calc"], max_difficulty=3))逻辑说明:load做的是结构校验,它不管内容对不对,只保证后续所有脚本拿到的记录字段齐全。这一步看着啰嗦,但省掉它之后最常见的场景是——统计脚本跑到一半 KeyError,然后你去翻是哪道题漏填了状态,翻了二十分钟。
normalize做的是取值归一。真实笔记里章号几乎不可能统一,有人写「第三章」,有人写「ch3」,有人直接写「决策树」。归一的规则是「能抽出数字就抽数字,抽不出就置 0」,置 0 的记录会被单独挑出来人工处理,不会静默丢失。
stats和pick是两个最小查询:前者回答「我的习题集长什么样」,后者回答「今晚做哪二十道」。pick里把state == "done"直接排掉,用的是「能复述才算 done」的判据而不是「做过一遍」——这条判据的差别,直接决定复习队列的质量。
参数上,max_difficulty建议第一次筛时压在 2~3,先把简单题的判断链路走顺;limit给 20 是因为单次复习超过 20 道之后,后十道的正确率会明显掉,那是疲劳不是不会。
注意:
state字段只允许new/done/wrong三个值。加第四个值(比如review)之后你会发现自己在纠结「到底算不算 review」,判据一模糊,索引就开始自欺。
2.3 索引合不合格,用三个问题当场验收
第一问:给定一个考点,能不能在 5 秒内列出所有相关题?如果做不到,说明topic词表没归并好,回去做受控词表。第二问:能不能说清自己最弱的是哪一类题型?说不清说明type用得太随意,出现了「综合」「其他」这种兜底值。第三问:随机抽一道wrong的题,能不能在 30 秒内说出上次错在哪个判断上?
第三问最关键,也是索引和「题号清单」的分水岭。如果wrong状态只是打个标记、不记录错因,那这个索引只解决「找题」,不解决「复习」。我一般会在记录里再加一个note字段,一句话写错因,比如「A* 里启发函数用了欧氏距离但允许斜走,估低了,展开顺序错」。这句话的价值远高于答案本身,因为期末考试换的往往就是这一层参数。
验收通过之后,这份 JSONL 就是后续所有工作的底座:跑代码时从里面挑题、自测时从里面抽题、压缩考点矩阵时直接对topic做聚合。
3. 搜索类习题从手算表到脚本:A* 的启发函数怎么选、怎么验
搜索这一章的习题有个特点:手算能算出来,但一换地图就废。原因是手算过程里藏着一堆默认约定——扩展顺序按什么排、重复节点是丢弃还是更新、代价是不是允许对角走。这些约定在脚本里必须显式写出来,写出来的过程就是把「会做题」变成「真懂」的过程。
3.1 状态空间题的四个必填项,少一个就写不出代码
任何一道搜索题,动笔之前先把四项填清楚:
| 项 | 含义 | 常见错误 |
|---|---|---|
| 状态表示 | 用什么东西唯一标识一个局面 | 用列表导致不可哈希,无法进集合 |
| 算子集合 | 从一个状态能到哪些状态、代价多少 | 斜走代价写成 1,与直走同价 |
| 目标判定 | 什么条件算到达 | 写成「走到终点附近」导致提前终止 |
| 代价定义 | 路径总代价怎么累加 | 累加了节点数而不是边权 |
这四项填完,代码几乎是自动生成的。真正区分人的是对第三项和第四项的处理:目标判定必须在出队时做而不是入队时做,否则在有重复路径的图上会返回次优解;代价定义里如果允许斜走,斜走的边权是根号二而不是 1,写成 1 会导致路径代价算错,但解本身看起来仍然「像对的」——这是最阴的一类错误,答案截图对得上,数字对不上。
启发函数的选择是第二个分水岭。曼哈顿距离只适用于四连通,欧氏距离在允许斜走时是低估的(可采纳,能保证最优),八方向网格里最紧的可采纳估计是 octile 距离:直走数加上斜走数乘以根号二。用曼哈顿距离配八方向,会高估,最优性就没了;用欧氏距离配八方向,最优性保住但展开节点更多。
3.2 A* 的最小实现:优先队列、g 值表与路径回溯
import heapq # 0 可走,1 障碍;坐标用 (x, y),x 是列,y 是行 GRID = [ [0, 0, 0, 0, 0, 0], [0, 1, 1, 0, 1, 0], [0, 0, 0, 0, 1, 0], [1, 1, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0], ] MOVES = [(1, 0), (-1, 0), (0, 1), (0, -1), (1, 1), (1, -1), (-1, 1), (-1, -1)] SQRT2 = 2 ** 0.5 def heuristic(a, b, mode="octile"): dx, dy = abs(a[0] - b[0]), abs(a[1] - b[1]) if mode == "manhattan": # 只对四连通可采纳 return dx + dy if mode == "euclidean": # 对八连通是可采纳但偏松 return (dx * dx + dy * dy) ** 0.5 # octile:八方向下最紧的可采纳估计 return (dx + dy) + (SQRT2 - 2) * min(dx, dy) def astar(grid, start, goal, mode="octile", weight=1.0): open_heap = [(weight * heuristic(start, goal, mode), 0.0, start, None)] g_best = {start: 0.0} parent = {} closed = set() expanded = 0 while open_heap: f, g, node, par = heapq.heappop(open_heap) if node in closed: # 同一节点的旧条目直接跳过 continue closed.add(node) parent[node] = par expanded += 1 if node == goal: # 目标判定放在出队时 break for dx, dy in MOVES: nx, ny = node[0] + dx, node[1] + dy if not (0 <= ny < len(grid) and 0 <= nx < len(grid[0])): continue if grid[ny][nx] == 1: continue step = SQRT2 if (dx and dy) else 1.0 ng = g + step nxt = (nx, ny) if ng < g_best.get(nxt, float("inf")): g_best[nxt] = ng heapq.heappush(open_heap, (ng + weight * heuristic(nxt, goal, mode), ng, nxt, node)) if goal not in parent: return None, float("inf"), expanded path, cur = [], goal while cur is not None: path.append(cur) cur = parent[cur] return path[::-1], g_best[goal], expandedopen_heap里存的是四元组:f 值、g 值、节点、父节点。父节点直接塞进堆里,是为了省掉一个额外的parent字典写入时机判断——出队时写入,简洁且不会漏。堆里的条目有可能重复,所以出队第一件事是查closed,重复条目直接丢弃,这就是常说的「惰性删除」。
g_best是那张手算题里的 g 值表。入队时比较ng < g_best[nxt],只有更优才更新并入堆,这一条对应手算表里「发现更短路径就把旧值划掉重写」的动作。如果漏掉这个比较直接入堆,脚本仍然能出解,但出的是先到先得的解,不一定最优。
weight是权衡系数:等于 1 是标准 A*,大于 1 变成加权 A*,展开节点明显变少但不再保证最优。用来做「同一道题不同启发函数的展开数对比」很直观,考试里那种「比较几种启发函数的效率」的简答题,直接跑一遍就有数据支撑,不用凭感觉写。
expanded是展开节点数,这个计数是验收指标。同一个起终点,mode="octile"的展开数应当小于mode="euclidean",而路径代价应当相同;如果代价不同,说明某处不可采纳,回去查启发函数。
3.3 手算表和程序输出对不上时,先查这三处
第一处,扩展顺序。手算时我们习惯按 f 值从小到大排,f 相同按字母序或按加入顺序。程序里遇到 f 相同,比较的是堆里的第二个元素 g,g 相同再比节点元组。所以当手算表里出现两个 f 相等的节点时,扩展顺序和程序一定对不上,但两边的最终代价应该一致。判断标准不是顺序一致,是代价一致。
第二处,重复节点的处理。手算题经常「发现节点已在 closed 里就跳过」,但如果新路径更短,正确做法是更新并重新入堆(在不一致启发函数下),或者至少在一致启发函数下保证不会出现更短路径。不少习题答案默认启发函数一致,直接跳过,这个前提要在笔记里写清楚,否则换了地图就出错。
第三处,坐标轴方向。这个错误低级但高频:GRID[y][x]和GRID[x][y]写反,小地图上看不出问题,因为障碍对称;换一张不对称的图立刻炸。我的习惯是全程用(x, y)表示坐标,取格子时写grid[y][x],并在注释里标一行,避免三天后自己看不懂。
验收的可靠做法是跟 Dijkstra 对拍:把启发函数强制置 0,跑一遍,看代价是否相同。相同说明主循环和代价累加没问题,剩下不一致的地方必然在启发函数上。这个对拍只需要改一个参数,比逐行读代码快得多。
4. 学习与优化类习题的数值对拍:梯度下降和反向传播怎么验对错
学习和优化这一章的习题,答案往往是一串数字。数字对不对,光看答案没法判断——因为你不知道自己错在公式、错在符号,还是错在归一化。唯一可靠的办法是让答案变成可执行的判定:脚本跑出来的数和手算的数对得上,才算过。
4.1 习题里最常被跳过的一步:先把损失函数的定义写死
均方误差有三种常见写法:不除任何数、除以 N、除以 2N。三种写法对应的梯度分别差常数倍,而学习率又是乘在梯度上的,于是「同一个学习率」在三种写法下表现完全不同。习题答案和你手算对不上,八成是这里。
我一般会在笔记首页固定一张约定表,只写三行:损失用哪种写法、梯度是上升还是下降、对数用什么底。这张表看起来像废话,但它能省掉后面所有的符号困惑。尤其是决策树那一章,熵用 log2 和用 ln 算出来的信息增益数值不同,但划分结果相同——如果习题只问划分结果,用哪个底都行;如果问具体数值,必须和答案的约定一致。
初始化同样被跳过。线性回归从全零初始化没问题,但神经网络从全零初始化会因为对称性导致所有隐藏单元学到同样的东西,损失下降极慢。习题里问「为什么不能全零初始化」,标准答案是「对称性无法打破」,这句话要能落到代码上:跑一次全零、跑一次小随机,看两条损失曲线,就再也不会忘。
4.2 用 NumPy 复现一元线性回归的梯度下降并和手算首步对齐
import numpy as np rng = np.random.default_rng(0) # 造数据:y = 3x + 2 + 噪声,真值用来最后验收 X = rng.uniform(-1, 1, size=(200, 1)) y = 3.0 * X[:, 0] + 2.0 + rng.normal(0, 0.05, size=200) def mse_loss(w, b): pred = X[:, 0] * w + b return np.mean((pred - y) ** 2) # 除以 N,不除以 2N def grad(w, b): err = X[:, 0] * w + b - y return 2 * np.mean(err * X[:, 0]), 2 * np.mean(err) # 第一步手算对齐:w=b=0 时,gw = 2*mean(-y*x),gb = 2*mean(-y) print("首步梯度:", grad(0.0, 0.0)) w, b, lr = 0.0, 0.0, 0.1 for step in range(500): gw, gb = grad(w, b) w -= lr * gw b -= lr * gb if step % 100 == 0: print(f"step={step:3d} w={w:.4f} b={b:.4f} loss={mse_loss(w,b):.6f}") print(f"final w={w:.4f} b={b:.4f}")grad里那个系数 2 来自对平方项求导,np.mean已经完成了除以 N 的动作,所以不需要再写/ len(y)。这就是「损失定义写死」的好处——梯度和损失严格对应,改一处必须改两处,改了忘一处立刻对不上。
首步对齐是这道题最有效的验收方式:把w=0, b=0代进去,手算2*mean(-y*x)和2*mean(-y),和打印结果逐位对比。第一步对上,说明损失定义、梯度公式、数据方向三者都一致;第一步对不上,后面跑五百步也没有意义。
学习率lr=0.1在这组数据上是安全的,因为x的取值范围在 [-1, 1],x的均方值约 0.33,梯度的量级被压在个位数。如果特征没有归一化,比如x在 [0, 1000],同样的 0.1 会让损失直接爆成 inf——这是「学习率太大」最常见的原因,不是学习率本身的问题,是特征尺度的问题。收敛判断也别只看步数,看abs(gw) + abs(gb)小于某个阈值就停,比固定跑五百步更靠谱。
4.3 反向传播的梯度检查:中心差分对拍与量纲错误排查
反向传播的习题错起来非常隐蔽:损失在降,但降得慢,你以为是学习率问题,其实是某一层的梯度漏了一个转置或者除错了一个 batch size。可靠的做法是数值梯度对拍,也就是用中心差分近似导数,和解析梯度逐个比。
import numpy as np rng = np.random.default_rng(1) def unpack(theta, d_in, d_hid): i = 0 W1 = theta[i:i + d_in * d_hid].reshape(d_in, d_hid); i += d_in * d_hid b1 = theta[i:i + d_hid]; i += d_hid W2 = theta[i:i + d_hid].reshape(d_hid, 1); i += d_hid b2 = theta[i:i + 1]; i += 1 return W1, b1, W2, b2 def loss_and_grad(theta, X, y, d_hid): n, d_in = X.shape W1, b1, W2, b2 = unpack(theta, d_in, d_hid) h = np.tanh(X @ W1 + b1) out = (h @ W2 + b2)[:, 0] err = out - y L = np.mean(err ** 2) # 与 mse_loss 同一约定 dout = (2.0 / n) * err[:, None] # dL/dout,已含 2/n gW2 = h.T @ dout gb2 = dout.sum(axis=0) dz = (dout @ W2.T) * (1 - h ** 2) # tanh 导数:1 - tanh^2 gW1 = X.T @ dz gb1 = dz.sum(axis=0) return L, np.concatenate([gW1.ravel(), gb1, gW2.ravel(), gb2]) def numeric_grad(theta, X, y, d_hid, eps=1e-6): g = np.zeros_like(theta) for k in range(theta.size): tp = theta.copy(); tp[k] += eps tm = theta.copy(); tm[k] -= eps g[k] = (loss_and_grad(tp, X, y, d_hid)[0] - loss_and_grad(tm, X, y, d_hid)[0]) / (2 * eps) return g X = rng.normal(0, 1, (32, 4)); y = rng.normal(0, 1, 32) theta = rng.normal(0, 0.3, 4 * 3 + 3 + 3 * 1 + 1) _, ga = loss_and_grad(theta, X, y, 3) gn = numeric_grad(theta, X, y, 3) rel = np.abs(ga - gn) / (np.abs(ga) + np.abs(gn) + 1e-12) print("最大相对误差:", rel.max()) # 期望 < 1e-7eps=1e-6是中心差分的常用取值:太大截断误差明显,太小浮点相减会丢有效位。判断标准用相对误差而不是绝对误差,因为不同参数的梯度量级可能差两三个数量级,绝对误差会被大梯度掩盖。经验阈值是1e-7量级算通过,1e-4量级说明有 bug,1e-2量级基本就是公式错了。
最常见的三类错法,都能被这个对拍抓出来:一是dout漏了2/n,相对误差会稳定在常数倍;二是dz写成1 - h(把 tanh 的导数记成了 sigmoid 的形式),误差很大但不均匀;三是X.T @ dz写成dz.T @ X,形状不对会直接报错,反而好发现。真正难查的是「形状刚好对得上」的错误,比如 batch 维度和特征维度恰好相等,这时候只能靠对拍。
对拍跑通之后,反向传播的习题就从「背公式」变成了「有裁判」。考试里问到某一层梯度怎么推,你能顺着dout → dz → gW1这条链讲出来,而不是默写一堆符号。
5. 排查清单:习题复盘里最容易自欺的 5 个地方
复盘阶段最危险的不是不会,是「以为自己会」。下面五条都是我实际翻车过或者见人翻车过的,按「现象 → 原因 → 解决」记,遇到对不上号的情况照着查。
5.1 数字对不上:符号约定、损失归一化、对数底数
现象:手算结果和脚本差一个负号,或者差整数倍,或者只差小数点后几位。答案的划分结果和你的一样,数值不一样。
原因:符号约定不一致(梯度上升写成下降)、损失归一化不一致(除以 N 还是 2N)、对数底数不一致(log2 还是 ln)。这三类问题的共同点是「不改变结论,只改变数值」,所以最容易被放过。
解决:在笔记首页固定一张约定表,只写三行,每道题开始前先看一眼。对数底数这条尤其要写清楚,因为熵和信息增益的习题里,底数不同会导致数值差 1.44 倍左右,很多人以为是算错了。
现象:同样的题,昨天算对了今天算错了。原因:中间步骤没写,全靠脑内缓存。解决:所有计算题强制写出三行中间量,熵、条件熵、信息增益各一行,写完再算最后一步。
5.2 换个数字就不会:只记住了路径没记住判据
现象:原题会做,把数据换一组就卡住,尤其是那种「表格里给五个样本,问先按哪个特征划分」的题。
原因:记住的是「这道题选第二个特征」,而不是「比较各特征的信息增益,取最大」。判据没提取出来,题目就变成了一道道独立的记忆题。
解决:做完一道题,强制写一句「判据是什么」。这句话要能套到任意数据上,比如「算每个特征的信息增益,取最大;并列时取先出现的」。另外做一次参数扰动自测:把样本数从 5 改成 6、把某个标签改掉,重新做一遍,看结论是否随数据合理变化。变化方向不对,说明判据记反了。
现象:简答题写得满满当当,得分不高。原因:写的是过程描述,不是判据。解决:每段开头先给结论句,再补理由,判卷的人先看到的是结论。
5.3 复现结果飘:随机种子、切分顺序、浮点累加
现象:同一个脚本,两次运行结果不同,或者换台机器结果不同。放在习题上就是「上次跑出来是 0.87,这次是 0.83,到底哪个对」。
原因:随机初始化没固定种子、数据切分没固定顺序、浮点累加顺序不确定(尤其是并行或集合遍历)。聚类和神经网络这两章的习题最容易中招。
解决:脚本开头统一写rng = np.random.default_rng(0),所有随机都从这一个 rng 出;数据切分前先shuffle一次并记录顺序;比较结果时用相对误差而不是相等判断。这三条做完,结果至少在自己的机器上是可重复的。
现象:结论对,但数值在小数点后第四位开始飘。原因:浮点累加顺序不同,属于正常误差。解决:不用管,比较时给 1e-6 的容差。真正要警惕的是量级级别的差异,那种一定是逻辑问题不是精度问题。
5.4 时间分配失衡:把八成时间花在会做的题上
现象:复习了一整天,感觉收获很大,第二天模考还是那些不会的题错。原因:做会做的题有即时反馈,做不会的题痛苦,人会本能地滑向舒适区。
解决:用第 2 章那个pick函数,只筛state in ("new", "wrong")的题,done的题在考前一天之前不再碰。这个规则一开始会觉得浪费,坚持三天就会发现弱项收敛得比想象中快。
6. 考前一天只看一页:把各章总结压成考点矩阵并做参数扰动自测
到考前最后一天,任何新的习题都不该再做了,该做的是把二十几页的各章总结压成一页。压缩的方式不是重抄一遍,是做一张考点矩阵:行是考点,列是「判据 / 典型题号 / 我的错因 / 扰动后还成立吗」。
矩阵的填法有个硬要求:判据那一列必须是能直接执行的动作,不能是名词。写「信息增益」不合格,写「算每个特征的信息增益取最大,并列取先出现」才合格。典型题号从第 2 章的索引里直接抄,错因从note字段里抄,不用重新回忆。
| 考点 | 判据(可执行) | 典型题号 | 我的错因 | 扰动自测 |
|---|---|---|---|---|
| A* 最优性 | 启发函数可采纳则最优,八方向用 octile | S-07, S-12 | 用了曼哈顿配八方向 | 换障碍图重跑,代价与 Dijkstra 一致 |
| 梯度下降收敛 | 梯度模小于阈值停,先查特征尺度 | L-03, L-09 | 未归一化导致学习率偏大 | 把 x 放大 100 倍,观察发散 |
| 反向传播 | 中心差分对拍,相对误差 < 1e-7 | L-15 | dout 漏了 2/n | 改 batch size,误差量级不变 |
| 决策树划分 | 信息增益最大,并列取先出现 | C-04, C-11 | 只记住结论没记判据 | 改一个标签,看重算后的划分 |
| 聚类 | 初始中心影响结果,需固定种子 | C-18 | 两次运行结果不同 | 固定 rng 后重跑三次 |
扰动自测这一列是整张表的关键。它的逻辑很简单:如果把参数改掉结论就崩,说明你掌握的是这道题,不是这个考点。具体的扰动方式按考点定——搜索题换地图、优化题改尺度、分类题改标签、聚类题固定种子重跑。每个考点做一次,一次两分钟,十几个考点一小时内能过完,效果比再刷二十道题扎实。
考前一天我一般只做两件事:把这张表从头到尾讲一遍给自己听,讲到哪一行卡住就回去看那一行的典型题;然后把所有wrong状态的题抽五道,只看题面不复述答案,三十秒内说出判据就算过。卡住的那两道,就是第二天早上进考场前最后要看的东西。
血泪经验是:别在最后一天补新章节。我见过太多人前一天晚上翻开从没看过的第六章,第二天连前五章熟的都答串了。矩阵上一个考点都填不出判据的时候,就该停止扩张,回到已经有的东西上做收敛。
这套流程的核心不是多做了多少题,而是每道题都留了一条可验证的痕迹:索引里有它的位置,脚本里有它的对拍,矩阵里有它的判据。期末考试换的往往就是参数,判据在,参数随便换。希望帮到你。
本文还有配套的精品资源,点击获取