简介面向山东大学软件学院2022级人工智能导论课程的实验资料包对应软工22级7班的四次课程实验适合正在修读该课程、需要复盘实验过程或自查实现细节的学生参考也可作为入门神经网络实践的自学素材。压缩包共74个文件、约185.78MB主体为11个Python脚本与4份docx实验报告配套4组MNIST原始数据集、16个gz压缩数据与21张png结果图另有4个按实验分装的zip源码包和1份md说明按实验一至实验四分目录归档。内容涵盖感知机与BP网络的手写数字识别、ResNet18与CNN卷积模型搭建、CGAN图像生成等典型任务源码可直接运行调试实验报告则记录目的、参数设置与结果分析读者能据此理清从数据加载、模型定义到训练评估的完整链路。目前已有200人学习下载。1. 不是六份编程作业人工智能导论实验在多数软件工程专业的培养方案里排在大二下学期紧跟在概率论、线性代数和离散数学之后。很多人把它当成六份互不相干的编程作业交完就忘等到做课程设计或毕设、真要给系统加一个智能决策模块时才发现搜索、评估函数、梯度下降这些东西本该在这里打通。这套方案来自某高校软件工程专业人工智能导论实验的完整整理六个实验模块、一套统一的工程骨架、可复现的随机种子管理以及一份能对着答辩追问逐条回答的验收清单。它适合正准备交实验报告的人也适合想把实验内容改造成课程设计、甚至作为毕设前期原型的人——六个模块的代码量加起来不到两千行但每一块都能独立扩展成一个完整课题。2. 实验体系设计六个模块的能力映射与工程量估算课程大纲给出的是一串知识点状态空间搜索、对抗搜索、概率推理、监督学习、神经网络。实验要交付的却是另一回事——能跑起来、能复现、能在答辩时讲清楚为什么这么设计的工程产物。从知识点到实验任务之间的翻译工作通常没人替你做于是最常见的做法是照着课件抄一遍伪代码跑通了就截图交差。这套整理里必须补上这一层。2.1 从课程知识点到可提交实验任务的拆解六个模块按认知难度递进前三个偏确定性算法、后三个偏统计学习每个模块都能同时给出“跑通”和“跑好”两条验收线。下表是这套整理里我实际使用的任务定义工程量一栏是单人独立完成的经验值。模块任务载体核心技术点代码量工时评分权重实验一八数码与迷宫寻路BFS / DFS / 一致代价 / A*约 250 行6 h15%实验二井字棋与黑白棋极小极大 α-β 剪枝约 200 行5 h15%实验三文本情感分类朴素贝叶斯、拉普拉斯平滑约 150 行4 h10%实验四表格数据分类KNN、决策树、交叉验证约 200 行6 h15%实验五手写数字识别反向传播、梯度检查约 300 行10 h20%实验六小型图像分类卷积、池化、正则化约 350 行12 h25%拆解的关键在于让每个模块都产出三个东西可执行脚本、可对比的指标、一段能解释指标差异的分析。只交脚本拿不到分只交分析没有说服力。比如实验一里八数码用 BFS 能解但展开节点数爆炸换成 A* 之后节点数掉一个数量级这个对比本身就是报告里最值钱的部分。实验二同理加不加 α-β 剪枝搜索节点数差多少用同一局棋面跑两遍就有答案。任务的输入输出也要提前定死避免做到一半改接口。我一般约定每个实验的入口是python -m exp1.main --config configs/exp1.yaml输出统一写到results/exp1/下包含一个metrics.json和若干图片。这样六个实验的产物结构一致后面写报告、做汇总对比都省事。2.2 技术选型纯手写、调库与框架的三条路线取舍这个环节最容易纠结。全用 NumPy 手写能体现理解但实验六写到卷积反向传播时会非常痛苦全用框架调 API三天能交完可答辩时问一句“梯度怎么传的”就答不上来。我的做法是分模块分层算法骨架手写工程细节交给库。路线适用模块优点风险纯 NumPy 手写实验一至三、实验五逻辑透明答辩好讲性能差易出索引错误scikit-learn 调库实验四、实验三对照快指标现成说不清内部实现会被追问PyTorch 框架实验六训练稳定可视化方便容易变成调参流水线具体到实验五我的取舍是前向传播和反向传播全部手写只用 NumPy 做矩阵运算不引入任何自动微分。理由是这一层的数学是课程重点手写一遍梯度公式比看十遍推导都有用。到了实验六卷积层的手写成本已经超出实验课时能承受的范围这时候用 PyTorch 反而合理——但要额外补一页说明写清楚卷积核尺寸、步长、填充怎么影响输出特征图大小公式是 $(W - F 2P)/S 1$把这一行写进报告追问就少一半。还有一条必须守住的底线同一个实验里不要混用两套数据划分。有人用 sklearn 的train_test_split划分一遍又用自己写的函数划分一遍做对比最后两份结果的测试集有交集指标看着漂亮实际是自欺欺人。2.3 统一的工程骨架与提交目录约定六个实验共用一个仓库目录结构固定下来能省掉大量重复劳动。下面是我用的骨架实测下来比每个实验建一个独立文件夹清爽得多。ai-intro-lab/ ├── configs/ # 每个实验一份 yaml 配置 │ ├── exp1.yaml │ └── exp5.yaml ├── data/ # 原始数据只读不提交到版本库 │ ├── raw/ │ └── splits/ # 固定的划分文件保证可复现 ├── src/ │ ├── common/ # 种子、日志、指标、绘图 │ ├── exp1_search/ │ ├── exp5_bpnet/ │ └── exp6_cnn/ ├── results/ # 每个实验的输出目录 └── reports/ # 实验报告与图表配置外置这件事值得单独强调。把学习率、批大小、随机种子、数据路径全写进 yaml改参数不用动代码做对比实验时只需要复制一份配置改两行报告里的超参数表也能直接从配置文件生成。下面这段是公共工具模块的核心部分六个实验都会调用。# src/common/experiment_utils.py import os, json, random, time import numpy as np def set_seed(seed: int 42) - None: 固定所有随机源保证同一配置多次运行结果一致。 random.seed(seed) # Python 内置随机 np.random.seed(seed) # NumPy 随机 os.environ[PYTHONHASHSEED] str(seed) try: import torch torch.manual_seed(seed) # CPU 与 GPU 的种子分开设置 torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False except ImportError: pass # 前几个实验不需要 torch def save_metrics(exp_name: str, metrics: dict) - str: 把指标连同时间戳一起落盘便于多次运行横向对比。 out_dir os.path.join(results, exp_name) os.makedirs(out_dir, exist_okTrue) metrics dict(metrics) metrics[_timestamp] time.strftime(%Y-%m-%d %H:%M:%S) path os.path.join(out_dir, metrics.json) with open(path, w, encodingutf-8) as f: json.dump(metrics, f, ensure_asciiFalse, indent2) return pathset_seed里有个容易被忽略的点torch.backends.cudnn.deterministic True会拖慢训练速度但实验六的数据量很小慢几秒换来结果可复现这笔买卖划算。save_metrics加时间戳是为了防止覆盖——同一份配置跑三次取平均是报告里说明“结果稳定”的唯一硬证据覆盖掉就没了。这套骨架搭完之后每新增一个实验只需要在configs/加一份配置、在src/加一个包剩下的基础设施直接复用。3. 搜索与博弈实验落地状态建模、启发式与剪枝搜索类实验看起来最简单实际上是拉开差距的地方。八数码谁都能写出 BFS但写出一个能在两秒内解出困难局面、并且能解释为什么快的实现就需要在状态建模和启发式设计上下功夫。博弈实验同理极小极大是模板剪枝写对才是本事。3.1 状态空间建模与 BFS/DFS/A* 的统一接口我习惯先抽一个Problem基类把“初始状态、目标判定、后继状态、单步代价”四件事定死四种搜索算法共用同一套接口。这样做的好处是算法代码完全不知道具体是什么问题换个迷宫问题只要重写子类搜索部分一行不用改。# src/exp1_search/problem.py from abc import ABC, abstractmethod class Problem(ABC): 搜索问题的统一抽象状态用不可变对象表示便于放进 set 去重。 abstractmethod def initial_state(self): ... abstractmethod def is_goal(self, state) - bool: ... abstractmethod def successors(self, state): 返回 [(next_state, action, step_cost), ...] ... class EightPuzzle(Problem): GOAL (1, 2, 3, 4, 5, 6, 7, 8, 0) # 0 表示空格 def __init__(self, start): self.start tuple(start) def initial_state(self): return self.start def is_goal(self, state) - bool: return state self.GOAL def successors(self, state): idx state.index(0) r, c divmod(idx, 3) moves [(-1, 0, 上), (1, 0, 下), (0, -1, 左), (0, 1, 右)] result [] for dr, dc, name in moves: nr, nc r dr, c dc if 0 nr 3 and 0 nc 3: nidx nr * 3 nc lst list(state) lst[idx], lst[nidx] lst[nidx], lst[idx] result.append((tuple(lst), name, 1)) return result状态用tuple而不是list是为了能直接塞进set做去重——用 list 会直接抛TypeError: unhashable type这是新手第一个翻车点。successors返回三元组而不是只返回状态是为了让一致代价搜索能读到step_cost八数码每步代价都是 1看起来多余但换成迷宫带地形成本时这个设计就体现出价值了。A* 的实现需要注意优先队列里存什么。只存状态不行因为要比较 f 值只存 f 值也不行因为要取状态。我的做法是存(f, g, counter, state)counter是自增计数器纯粹为了在 f 相等时避免比较状态元组导致报错——两个元组之间没法比大小Python 会抛异常。这个问题在节点数少的时候不一定触发一旦跑到上万节点就必然出现属于典型的“小数据测试通过、大数据崩溃”。算法数据结构完备性最优性八数码平均展开节点BFS队列是是单位代价约 15000DFS栈否需限深否平均 3000 但解很长一致代价优先队列是是约 14000A*曼哈顿优先队列是是约 300这张表建议直接放进报告。节点数是同一批测试用例的平均值跑一遍就有比任何文字描述都有说服力。3.2 启发式函数设计与可采纳性验证A* 之所以快全靠启发式函数。八数码常用的两个错位棋子数每个不在目标位的棋子计 1和曼哈顿距离每个棋子当前位置到目标位置的横纵距离之和。曼哈顿距离更紧展开节点更少这是因为前者是后者的下界。可采纳性admissible是 A* 保证最优解的前提启发式估计值不能超过真实剩余代价。这个性质不是靠嘴说的我一般让学生写一段抽样验证——随机采若干状态用 BFS 算出真实最短距离再和启发式值比全部满足h(n) h*(n)才算过关。# src/exp1_search/heuristic.py from collections import deque def manhattan(state) - int: 曼哈顿距离每个数字块到目标位置的横纵距离之和。 total 0 for i, v in enumerate(state): if v 0: continue goal_r, goal_c divmod(v - 1, 3) # 数字 v 的目标行列 cur_r, cur_c divmod(i, 3) total abs(goal_r - cur_r) abs(goal_c - cur_c) return total def misplaced(state) - int: 错位棋子数恒小于等于曼哈顿距离也是可采纳的。 return sum(1 for i, v in enumerate(state) if v ! 0 and v ! EightPuzzle.GOAL[i]) def verify_admissible(problem, heuristic, samples200): 抽样比对启发式值不得超过 BFS 求出的真实最短距离。 import random violations 0 for _ in range(samples): state problem.initial_state() for _ in range(random.randint(5, 20)): # 随机游走制造测试局面 succ problem.successors(state) if succ: state random.choice(succ)[0] true_cost bfs_distance(problem, state) # 用 BFS 求真实最短路 if true_cost is not None and heuristic(state) true_cost: violations 1 return violationsmisplaced里排除空格是必须的空格不属于任何数字它的位置不该计入错位数漏掉这个判断会让启发式偏大虽然有概率仍然可采纳但已经失去了理论保证。verify_admissible返回 0 才敢在报告里写“本实验所用启发式满足可采纳性”这个数字本身就是答辩时的挡箭牌。还有一类非可采纳但更快的启发式比如把曼哈顿距离乘个权重weighted A*能在更短时间内找到解但不再保证最优。这个思路值得写进报告的“扩展讨论”一节说明自己知道最优性和效率之间的权衡比单纯刷分更能体现理解。3.3 博弈树与 alpha-beta 剪枝的工程实现井字棋的搜索空间小正好用来验证剪枝逻辑是否正确。核心是极小极大值的递归轮到己方走取最大值轮到对方走取最小值α 记录当前路径上己方能保证的下界β 记录对方能保证的上界一旦α β就直接剪掉。# src/exp2_game/alphabeta.py WIN_LINES [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)] _stats {nodes: 0, cuts: 0} # 统计剪枝效果报告里要用 def evaluate(board, player) - int: 终局评估己方赢返回正分越早赢分数越高平局为 0。 for a, b, c in WIN_LINES: if board[a] board[b] board[c] ! 0: return 10 * board[a] * player if 0 not in board: return 0 return None # 未终局 def alphabeta(board, player, depth, alpha, beta, max_depth9): player: 1 表示 X-1 表示 O。返回 (分数, 最佳落子)。 _stats[nodes] 1 score evaluate(board, player) if score is not None: return score, None if depth max_depth: return 0, None best_move, best_score None, -float(inf) for i in range(9): if board[i] ! 0: continue board[i] player # 关键换手后对对手调用取反得到己方视角分数 opp_score, _ alphabeta(board, -player, depth 1, -beta, -alpha, max_depth) board[i] 0 if -opp_score best_score: best_score, best_move -opp_score, i alpha max(alpha, best_score) if alpha beta: # 剪枝对手不会允许走到这个分支 _stats[cuts] 1 break return best_score, best_move三个地方最容易写错。第一是-beta, -alpha这个参数交换用的是负极大值形式把“对方取最小”转成“己方取最大后取负”代码短了但极容易把符号搞反如果发现 AI 下棋像在帮对手八成是这里写错了。第二是alpha的更新必须放在循环内放在循环外等于没剪枝。第三是评估函数里的player乘子漏乘会导致双方都认为自己是赢家棋力直接归零。我一般让学生先用不加剪枝的版本跑一遍记录节点数再加上剪枝跑一遍两个数字对比写进报告——井字棋上通常能砍掉一半以上节点这个比例就是剪枝有效性的直接证据。4. 学习类实验落地从 KNN 到 BP 网络再到 CNN 识别从确定性算法跨到统计学习最大的变化是“结果不再唯一”。同一个数据集跑两遍准确率差三个点是玄学还是错误这一章要解决的就是把不确定性关进笼子里让每一次指标波动都有解释。4.1 数据管线与特征工程的统一约定学习类实验的第一步永远是数据不是模型。我强制要求把数据划分固定成文件而不是每次运行时随机切分——随机切分意味着每次跑出来的测试集都不一样指标根本没有可比性。# src/common/data_pipeline.py import numpy as np def make_split(n_samples: int, ratio(0.6, 0.2, 0.2), seed42): 一次性生成划分索引并保存后续所有实验复用同一份。 rng np.random.RandomState(seed) idx rng.permutation(n_samples) n_train int(n_samples * ratio[0]) n_val int(n_samples * ratio[1]) return { train: idx[:n_train], val: idx[n_train:n_train n_val], test: idx[n_train n_val:], } def standardize(train_x, other_x): 用训练集统计量标准化防止测试集信息泄漏。 mu train_x.mean(axis0) sigma train_x.std(axis0) 1e-8 # 防止除零 return (train_x - mu) / sigma, (other_x - mu) / sigmastandardize这个函数看着简单却是数据泄漏最高发的地方。正确的做法是只在训练集上算均值和标准差然后把这组统计量应用到验证集和测试集如果对全体数据一起标准化测试集的分布信息就提前泄露进模型指标会虚高而且高得不多很难被发现。同样的道理适用于归一化、PCA、特征选择凡是“需要拟合的预处理”一律只在训练集上 fit。4.2 KNN 与决策树的基线实现与对比实验四的定位是建立基线。任何一个复杂模型都应该先和一个简单模型比比不过就说明复杂模型没调好。KNN 和决策树恰好代表两种极端前者没有训练过程全靠距离度量后者靠递归划分可解释性强。对比项KNNk5决策树max_depth8训练耗时几乎为 0约 0.3 s预测耗时随样本数线性增长常数级对特征尺度极度敏感不敏感可解释性弱强可导出规则典型准确率0.920.89KNN 对尺度敏感这一点必须在报告里体现不做标准化直接跑准确率往往掉五到十个点因为数值范围大的特征主导了距离计算。这个对比实验花不了几分钟却是理解“为什么预处理重要”最直观的例子。决策树的深度要限制不限制的话它会把训练集背下来训练准确率 100%、测试准确率 80%典型的过拟合现场正好用来讲正则化的必要性。4.3 手写 BP 网络前向、反向与梯度检查实验五是整个课程的分水岭。要求用 NumPy 手写一个两层全连接网络做手写数字识别输入 784 维隐藏层 128输出 10 类激活函数用 ReLU损失用交叉熵。# src/exp5_bpnet/mlp.py import numpy as np class MLP: def __init__(self, n_in784, n_hidden128, n_out10, seed42): rng np.random.RandomState(seed) # He 初始化适配 ReLU避免一开始就大面积失活 self.W1 rng.randn(n_in, n_hidden) * np.sqrt(2.0 / n_in) self.b1 np.zeros(n_hidden) self.W2 rng.randn(n_hidden, n_out) * np.sqrt(2.0 / n_hidden) self.b2 np.zeros(n_out) def forward(self, x): self.x x self.z1 x self.W1 self.b1 self.a1 np.maximum(0, self.z1) # ReLU self.z2 self.a1 self.W2 self.b2 # softmax 前先减最大值防止指数溢出 exp np.exp(self.z2 - self.z2.max(axis1, keepdimsTrue)) self.prob exp / exp.sum(axis1, keepdimsTrue) return self.prob def backward(self, y_onehot, lr0.01): n y_onehot.shape[0] dz2 (self.prob - y_onehot) / n # 交叉熵 softmax 的合并梯度 dW2 self.a1.T dz2 db2 dz2.sum(axis0) da1 dz2 self.W2.T dz1 da1 * (self.z1 0) # ReLU 导数 dW1 self.x.T dz1 db1 dz1.sum(axis0) self.W1 - lr * dW1 self.b1 - lr * db1 self.W2 - lr * dW2 self.b2 - lr * db2dz2 (prob - y_onehot) / n这一行是整段代码的核心交叉熵配 softmax 的梯度化简后就是这么干净手推一遍能省掉大量调试时间。self.z2 - self.z2.max(...)是数值稳定性处理不加的话在训练后期 logits 变大时会出现nan而且往往在跑到第几十个 epoch 才出现属于最难定位的那类 bug。写完反向传播必须做梯度检查这是唯一能验证推导对不对的手段。思路是取一个参数用极小的扰动算数值梯度和解析梯度比对相对误差。def grad_check(model, x, y_onehot, eps1e-5): 只抽查 W2 的部分元素全量检查太慢。 model.forward(x) model.backward(y_onehot, lr0.0) # lr0 表示只算梯度不更新 analytic model.a1.T ((model.prob - y_onehot) / x.shape[0]) max_err 0.0 for _ in range(10): i np.random.randint(0, model.W2.shape[0]) j np.random.randint(0, model.W2.shape[1]) orig model.W2[i, j] model.W2[i, j] orig eps loss_plus -np.log(model.forward(x)[np.arange(len(y_onehot)), y_onehot.argmax(1)] 1e-12).mean() model.W2[i, j] orig - eps loss_minus -np.log(model.forward(x)[np.arange(len(y_onehot)), y_onehot.argmax(1)] 1e-12).mean() model.W2[i, j] orig numeric (loss_plus - loss_minus) / (2 * eps) denom max(abs(numeric), abs(analytic[i, j]), 1e-8) max_err max(max_err, abs(numeric - analytic[i, j]) / denom) return max_err # 相对误差小于 1e-6 认为推导正确相对误差的判据要写成abs(a-b)/max(abs(a),abs(b),1e-8)直接用绝对误差会因为梯度量级差异误判。lr0.0是为了在检查时不污染权重这个细节很多人忽略检查完发现模型被改坏了白跑一轮训练。4.4 小型 CNN 训练与实验记录规范实验六用 PyTorch 做小规模图像分类两层卷积加两层全连接就够重点不在刷准确率而在把训练过程记录清楚每个 epoch 的训练损失、验证损失、验证准确率、学习率全部落盘。# src/exp6_cnn/train.py import torch def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0.0, 0, 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad(set_to_noneTrue) # 比 zero_grad() 稍快 logits model(x) loss criterion(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm5.0) optimizer.step() total_loss loss.item() * x.size(0) correct (logits.argmax(1) y).sum().item() total x.size(0) return total_loss / total, correct / totalclip_grad_norm_是防梯度爆炸的后悔药学习率设大了、数据里有异常样本时不加这一行很容易在第几个 epoch 突然 loss 变nan。set_to_noneTrue是个小优化把梯度置为 None 而不是零张量显存和速度都略好属于知道了就会一直用的习惯。记录规范上我要求报告里必须有一张“训练曲线 关键超参”的对照图。学习率 0.1、0.01、0.001 各跑一次三条曲线画在一张图上收敛速度和最终精度一眼可见。这比写一大段“经过调参模型效果良好”有用得多也是答辩老师最爱看的东西。5. 避坑与排查实验里最容易翻车的五个点这些坑的共同特点是报错信息不明确或者根本不报错只是结果不对。整理出来按“现象、原因、解决”三步走出问题时对着排查能省下大半时间。5.1 结果不可复现与数据泄漏现象一同一份代码跑两次准确率差三到五个点。原因几乎总是随机种子没固定全或者数据加载顺序受多线程影响。Python 的random、NumPy、框架各自的随机源是独立的只固定其中一个等于没固定。解决方式就是第 2 章那个set_seed并且在配置文件里把种子写成显式参数而不是藏在代码里。现象二测试集准确率高得离谱换一批数据就崩。原因是在划分数据之前做了标准化、归一化或者特征选择测试集的信息提前进入了预处理环节。解决方式是把所有fit类操作限制在训练集上验证集和测试集只做transform。这个错误特别隐蔽因为指标只是“稍微好一点”不会好到让人怀疑。5.2 搜索与博弈类算法的逻辑陷阱现象三A找到的解比 BFS 长。* 原因通常是启发式不可采纳或者优先队列在 f 值相等时对比了状态对象导致节点丢失。解决方式是先用第 3 章那段抽样验证跑一遍可采纳性再检查优先队列里是否放了自增计数器。还有一种可能是把 g 值和 f 值搞混弹出队列时用了g排序。现象四AI 下棋水平还不如随机。原因基本锁定在评估函数符号或者-beta, -alpha的参数交换。解决方式很朴素把搜索深度限制为 1看它会不会走“下一步就赢”的位置如果不会说明符号反了把评估函数的player乘子去掉再试。5.3 训练过程中的数值问题现象五损失降到一半突然变成nan。三种常见原因学习率过大、softmax 没有做减最大值处理、梯度爆炸。排查顺序是先打印每个 batch 的损失找到第一次出现nan的位置再看那一批数据的输入范围是否异常最后加梯度裁剪并把学习率降一个数量级。我一般会把torch.isnan(loss)的判断写进训练循环一旦命中就打印当前 batch 的统计量并中断比跑到最后看到一个nan强得多。提示以上五条里现象一和现象二属于工程习惯问题现象三到五属于理论理解问题。前者靠模板代码就能规避后者只能靠推导加验证别指望复制粘贴解决。6. 验收与答辩让实验报告经得起追问验收环节真正被问倒的往往不是“这个算法是什么”而是“你为什么不那样做”。所以准备的重点不是背概念是给每个设计决策准备一个理由并且这个理由最好有数据支撑。6.1 验收清单与评分维度对齐下面这张表是我按评分维度整理的自查项交之前逐条过一遍能挡掉绝大部分扣分点。维度检查项佐证材料正确性算法输出与理论预期一致小规模手工验算样例可复现换机器同配置结果一致固定种子 划分文件 依赖版本完整性六个模块均有指标输出metrics.json 与图表分析深度至少一组对照实验有/无剪枝、标准化前后、不同学习率工程规范配置外置、日志完整、目录统一仓库结构与配置样例最容易漏的是“分析深度”。很多人六个实验都跑通了但每个都只有一组结果报告读起来像流水账。补救办法很便宜每个实验补一组对照搜索类补剪枝对比学习类补超参对比半小时的活报告质量立刻上一个台阶。6.2 答辩现场的三类高频追问与应答思路第一类是“为什么选这个启发式”。答案不能是“因为常见”要落到可采纳性和紧致度上曼哈顿距离是错位数量的下界更紧所以展开更少并且有抽样验证数据。第二类是“你怎么保证不是碰巧”。答案是固定种子跑多次取均值方差加上固定的数据划分文件这两样东西拿出来质疑基本就结束了。第三类是“如果数据量放大十倍会怎样”。这类问题考的是复杂度意识KNN 预测是线性复杂度会变慢A* 的内存占用会爆炸手写 BP 的全批量梯度下降会跑不动——能说出瓶颈在哪、该怎么改比给出确定答案更重要。我印象最深的一次是某同学被问到为什么 KNN 要标准化他答“因为教程里都这么做”。这个回答直接把一个送分题变成了扣分题。后来我自己带实验时定了个规矩任何一行预处理代码如果讲不出“不做会怎样”就删掉重写。从那以后我每次交实验报告前都强制自己走一遍“删代码测试”——把某个处理步骤注释掉跑一次看指标掉多少。掉得多的写进报告当论据掉不动的说明这步本来就没必要。这个方法很土但比任何自查清单都管用。希望帮到你。本文还有配套的精品资源点击获取