第 1 讲习题集
四组难度递进:A 概念判断 → B 推导证明 → C 编程实现 → D 开放设计。答案默认折叠,点击展开。 用法:先闭卷做 A 组,再对照答案;B 组手写推导(涉及形状的先写形状);C 组上机并跑通验收标准;D 组写成半页方案。 考点范围限 导览 与 1.1–1.7,答案均可在正文定位。想不通时先回「依据」,再看折叠答案。
A 组 · 概念判断(8 题)
判定要求:判断题必须给一句话理由或反例,只写「对 / 错」不给分;辨析题必须写清「谁管什么」。
A1. 把一个单层网络的阶跃激活换成 Sigmoid,它能解决的问题变多了。(依据:1.1.4、定义 1.1.2)
答案
错。 单层的判决面由 决定,不论 是阶跃还是 Sigmoid,判决面仍是同一个超平面,能表达的函数族依旧只是线性可分的那一类。换激活改变的是「能否用梯度训练」,不是「能表达什么」;能力来自分层(1.1.4 误解 1、2)。
A2. 偏置 可以省掉:把输入扩一维恒为 1 就够了,所以没有偏置的感知机表达能力不变。(依据:1.1.4 误解 4)
答案
对(但要加条件)。 把 当作新输入后, 的学习与带偏置模型完全等价,超平面自由度没有损失——这正是 定理 1.1.1 证明里用 的原因。注意:若「省掉偏置」既不给 也不增广输入,超平面被迫过原点,「 归负类、其余归正类」这种问题立刻不可分,表达能力确实下降。
A3. 通用逼近定理保证:只要隐藏层足够宽,训练一定能收敛到一个好的解。(依据:定理 1.2.2、1.2.3)
答案
错。 定理只回答表示问题(是否存在权重让网络逼近目标函数),不涉及优化与统计。三条边界:① 不保证宽度可寻(所需宽度可能随输入维度指数增长);② 不保证可训练(构造性证明给出的权重往往在损失曲面上极难到达);③ 不保证样本效率(存在一组权重 ≠ 有限样本能学到它)。把「能表示」读成「能学会」是本节最大的误读。
A4. 逐元素激活会改变张量形状,所以排错时应把它和 Flatten 归为同类算子。(依据:方法 1.2.1、1.2.7 误解 3)
答案
错。 逐元素激活不改变形状(方法 1.2.1 第 2 条规则);改变形状的是展平、池化、卷积 stride、view/reshape。混为一谈的直接后果是形状表算错,而这正是「编译期报错」里最高频的一类。
A5. optim.Adam(model.parameters(), weight_decay=0.05) 与 optim.AdamW(..., weight_decay=0.05) 是同一个东西的两种写法。(依据:定理 1.6.1、1.6.9 错误 1)
答案
错。 Adam(weight_decay=λ) 是 Adam + L2:正则项 进梯度后被逐元素除以 ,各坐标的衰减强度取决于各自的梯度历史。AdamW 是解耦权重衰减:更新后独立减去 ,对所有坐标施加同一相对衰减。两者在 SGD 下等价(都化简为 ),在自适应优化器下不等价。
A6. 真值是 之间的连续「软分数」(表示模糊程度、无概率语义)时用 MSE 可以接受;而「从 10 个互斥类别里选一个」必须用交叉熵。(依据:1.4.2、1.4.10 错误 1)
答案
对。 判据是任务的概率模型:前者是回归到区间内的软分数,没有类别分布假设,MSE 与之一致;后者是离散类别分布,交叉熵才是它的负对数似然,且梯度恰为 。补充:互斥分类若改用 MSE + softmax,梯度里多出 因子,在模型最该被纠正的两个极端反而最小,训练更慢、概率校准更差。
A7. 均匀初始化时,只要各单元权重是独立采样且方差合适,就不会有对称性问题。(依据:方法 1.2.2、1.2.7 误解 4)
答案
对。 对称性由权重之间的独立性打破:同层不同单元权重独立采样时,它们的预激活、激活与梯度不会完全一致,梯度下降能区分开。所以「随机」是必要条件,「方差合适」是第二个必要条件(否则逐层放大或衰减)。反面:全零(或全常数)初始化时同层单元完全对称,训练等价于「一个单元的层」。
A8. 用验证集反复调超参、最后把验证集上最好那一档写进报告,是允许的省事做法。(依据:定义 1.7.1、1.7.6 的数据泄漏清单 第 5 条)
答案
错。 每一次基于验证集的选择都在消耗验证集的信息,等价于在验证集上过拟合,报告指标会系统性偏高。正确做法:选模型用验证集,最终报告用只评估一次的测试集(定义 1.7.1:测试集只应被使用一次)。若必须报验证集指标,要说明调参次数并给出多种子方差。
B 组 · 推导与证明(6 题)
要求:关键步骤逐条写出,不许写「略」;涉及形状的先写形状。
B1. 从 、 出发,推导两层网络(,批大小 )的三个参数梯度,并逐步核对形状。(依据:定理 1.5.3、附录 C.1)
提示与答案
形状先行:;、;( 对批求和)。 (1) 输出层误差信号。 已知 (softmax + 交叉熵时它就是 );逐元素激活的雅可比是对角矩阵,故 。 (2) 输出层参数梯度。 由 得 ,于是 (3) 回传到第一层。 ,形状 ;穿过激活得 。 (4) 第一层参数梯度。 同理 ,。 一句话读法: 一份两用——乘 继续往上游,乘 得本层权重梯度(1.5.3)。注意偏置梯度是沿批维求和,不是复制。
B2. 手推最小网络的梯度并与数值差分核对:(tanh),(恒等输出),,;、、、、、。
提示与答案
(1) 前向。 ,;,。 (2) 输出层。 ,恒等激活 ,故 。 (3) 回传。 ;、,故 。 (4) 参数梯度。 (形状 ✓);; (5) 数值核对(中心差分 )。 扰动 :、,故 ,与解析值吻合到 以内。读法:两个隐藏单元的梯度相差约 18 倍,因为第二单元落在 tanh 饱和区()——这就是 定理 1.3.1 的数值形态。
B3. 从最大似然推导:(a) 类别分布导出交叉熵;(b) 高斯假设(方差固定)导出 MSE;并据此说明有离群点时该换什么损失。(依据:定理 1.4.1、1.4.2)
提示与答案
(a) 单样本似然 ,取负对数得 ;对 个独立样本取平均即训练目标。 (b) 设 、,则 。第二项与 无关,对 最小化第一项等价于最小化平方误差,即 MSE——「回归用 MSE」不是约定俗成,是高斯似然假设的直接结果。 (c) 高斯假设噪声尾部很轻;一个错标 10 倍的样本会以平方级主导整批梯度。残差呈重尾时应换 MAE(对应拉普拉斯噪声)或 Huber(小误差平方、大误差线性)。判据:先画残差分布再看尾部,不凭感觉换损失。
B4. 证明动量在恒定梯度下的有效步长放大倍数 ,并说明 的工程后果。(依据:命题 1.6.1)
提示与答案
设 、,由 展开得 。 时等比级数收敛: 即有效学习率放大 倍。平滑尺度:把 看成梯度的加权和,权重 衰减到 约需 步,即记忆窗口。 :放大 倍、记忆窗口约 50 步。后果:① 学习率必须比无动量时小约两个数量级,否则发散;② 参数更新对噪声极度平滑,对小批量或分布快速变化的任务反应迟钝;③ 早期错误方向会被持续带回,表现为「损失先升后降」的长尾。判据:加动量后同步调小 ,并观察初期是否异常震荡。
B5. 手算 Adam 前 3 步偏差修正:,、、、;并说明不修正时第一步偏大多少倍。(依据:定义 1.6.4、定义 1.6.4)
提示与答案
递推 、,再做 、:
| 1 | |||||||
| 2 | |||||||
| 3 | |||||||
| 三步位移都在 同一量级,正是「Adam 每步位移约等于 (逐坐标)」的数值形态。 | |||||||
| 不修正时第一步是 ,修正后为 ,偏大 倍,来源是 。因 ,分子只保留 、分母只保留 ——偏差在最初几步最大,不修正会让前几百步的等效步长被严重放大。 |
B6. 用方差守恒推导 Xavier 与 Kaiming:先证 ,再说明 的折中来源,最后说明 ReLU 为什么把增益翻倍。(依据:方法 1.2.2、1.2.5)
提示与答案
(1) 前向方差。 设 , 与 独立、均值均为 、方差分别为 ,则各乘积项互不相关: 要让 (方差守恒),只需 。 (2) 反向也守恒。 梯度从 侧回传给出另一条件 ;两个条件一般不能同时满足,取调和平均作为折中:(Xavier)。均匀分布版本上界为 。 (3) ReLU 翻倍。 零均值对称分布下 (一半样本被截断为零),要维持守恒须把前向增益翻倍:(Kaiming)。错配症状(推论 1.3.1):Sigmoid 配 Kaiming 会过早饱和(初期损失几乎不降);ReLU 配 Xavier 会逐层衰减(深层网络的有效深度变小)。
C 组 · 编程实现(6 题)
依赖仅限
numpy与torch。每题给可运行的验收标准——不通过就说明对应环节没接上。
C1. 写形状推演工具 infer_shapes(dims, batch):输入层宽列表与批大小,返回每层输出形状与参数量。(依据:方法 1.2.1、例 1.2.1)验收标准:① infer_shapes([784,128,64,10], 32) 的逐层形状为 ;② 总参数量 == 109386(精确整数相等);③ 批大小改 后总参数量一个数字都不变(断言相等);④ [100,32,1]、 时总参数量 == 3265。
参考实现
def infer_shapes(dims, batch):
"""dims: [d0, d1, ..., dL];返回逐层 (输出形状, 该层参数量) 与总参数量。"""
rows, total = [], 0
for l in range(1, len(dims)):
d_in, d_out = dims[l - 1], dims[l]
params = d_in * d_out + d_out # W: d_in×d_out;b: d_out。二者都与 batch 无关
total += params
rows.append(((batch, d_out), params))
return rows, total
rows, total = infer_shapes([784, 128, 64, 10], 32)
assert [r[0] for r in rows] == [(32, 784), (32, 128), (32, 64), (32, 10)] # ①
assert total == 109386 # ②
assert infer_shapes([784, 128, 64, 10], 256)[1] == total # ③ 与批大小无关
assert infer_shapes([100, 32, 1], 64)[1] == 3265 # ④
print(rows, total)
关键行注释:d_in*d_out + d_out 里最后一项是偏置,漏掉它会让第 ② 条差 202。
C2. 纯 numpy 手写两层 MLP(Kaiming 初始化 + ReLU + softmax 交叉熵)在 XOR 型数据上训练,并与 torch.autograd 逐位比对梯度。(依据:定理 1.5.3、1.5.5、定理 1.4.2)
- 验收标准:① 手写梯度与自动微分的最大差异 (float64);② 2000 步内训练损失从 降到 ;③ 最终训练准确率 ();④ 形状断言
z1.shape==(N,H)、gW1.shape==(D,H)、gb1.shape==(H,)全部通过。
参考实现
import numpy as np, torch
rng = np.random.default_rng(0)
N, D, H, C = 64, 2, 32, 2
X = rng.normal(0, 1, (N, D)); y = (X[:, 0] * X[:, 1] > 0).astype(int) # XOR 型:单层不可分
W1 = rng.normal(0, np.sqrt(2 / D), (D, H)); b1 = np.zeros(H) # Kaiming 尺度
W2 = rng.normal(0, np.sqrt(2 / H), (H, C)); b2 = np.zeros(C)
def forward(X, W1, b1, W2, b2):
z1 = X @ W1 + b1; a1 = np.maximum(0, z1) # ReLU:形状不变
z2 = a1 @ W2 + b2
z2 = z2 - z2.max(axis=1, keepdims=True) # 数值稳定:不改变 softmax 结果
e = np.exp(z2); p = e / e.sum(axis=1, keepdims=True)
return z1, a1, p
def loss_and_grads(X, y, W1, b1, W2, b2):
z1, a1, p = forward(X, W1, b1, W2, b2)
L = -np.log(p[np.arange(len(y)), y]).mean() # 交叉熵(对批平均)
d2 = p.copy(); d2[np.arange(len(y)), y] -= 1; d2 /= len(y) # δ⁽²⁾ = p − y,再除以 N
gW2, gb2 = a1.T @ d2, d2.sum(0) # ∂L/∂W = Aᵀδ;∂L/∂b = Σ_batch δ
da1 = d2 @ W2.T
dz1 = da1 * (z1 > 0) # 乘 ReLU 的 0/1 掩码
return L, (X.T @ dz1, dz1.sum(0), gW2, gb2)
eta = 0.5
for step in range(2001):
L, (gW1, gb1, gW2, gb2) = loss_and_grads(X, y, W1, b1, W2, b2)
W1 -= eta * gW1; b1 -= eta * gb1; W2 -= eta * gW2; b2 -= eta * gb2
if step % 500 == 0:
_, _, p = forward(X, W1, b1, W2, b2)
print(f"step {step} loss {L:.4f} acc {(p.argmax(1) == y).mean():.2%}")
# 与自动微分比对(灌入同一套权重,才能逐位比对)
net = torch.nn.Sequential(torch.nn.Linear(D, H), torch.nn.ReLU(), torch.nn.Linear(H, C)).double()
with torch.no_grad():
net[0].weight.copy_(torch.tensor(W1.T)); net[0].bias.copy_(torch.tensor(b1))
net[2].weight.copy_(torch.tensor(W2.T)); net[2].bias.copy_(torch.tensor(b2))
Xt, yt = torch.tensor(X), torch.tensor(y)
loss = torch.nn.functional.cross_entropy(net(Xt), yt) # 内部含 log_softmax,勿再手动加
net.zero_grad(); loss.backward()
gW1_np, gb1_np, _, _ = loss_and_grads(X, y, W1, b1, W2, b2)[1]
assert np.abs(gW1_np - net[0].weight.grad.numpy().T).max() < 1e-9 # 框架存的是「输出×输入」
assert np.abs(gb1_np - net[0].bias.grad.numpy()).max() < 1e-9
print("grad check passed")
关键行注释:框架的 weight.grad 形状是 ,而公式里是 ,比对时必须转置——这是本节最容易踩的形状坑。若把 ReLU 换成恒等,损失会停在较高平台,正好验证 定理 1.2.1。
C3. 实现三种分类损失(交叉熵 / 标签平滑 / 焦点损失)并验证 softmax + 交叉熵的梯度公式。(依据:定理 1.4.2、定义 1.4.3、定义 1.4.4)
- 验收标准:①
logits=[[2.0,1.0,0.1]]、target=1时损失与手算 差异 ;② 梯度与p - one_hot的最大绝对误差 ,且每个分量都落在 ;③ logits 整体加 后损失仍有限(无inf/nan);④ 、 时正确类目标概率恰为 (误差 ),且平滑后损失大于未平滑;⑤ 、 时焦点损失相对交叉熵的权重比为 (误差 )。
参考实现
import torch, torch.nn.functional as F
logits = torch.tensor([[2.0, 1.0, 0.1]], dtype=torch.float64); target = torch.tensor([1])
# ① 交叉熵:log_softmax + NLL(数值稳定写法)
logp = F.log_softmax(logits, dim=-1)
ce = -logp[0, target].item()
assert abs(ce - (-torch.log(torch.exp(logits)[0, 1] / torch.exp(logits).sum()).item())) < 1e-6
# ② 梯度核对:∇_z = p − y
zt = logits.clone().requires_grad_(True)
F.cross_entropy(zt, target).backward()
p = zt.detach().softmax(-1)
assert (zt.grad - (p - F.one_hot(target, 3).double())).abs().max().item() < 1e-6
assert zt.grad.abs().max().item() <= 1.0 # 落在 [−1,1]
# ③ 数值稳定:整体平移 logits 不改变结果
assert torch.isfinite(F.cross_entropy(logits + 1000.0, target))
# ④ 标签平滑
eps = 0.1; soft = (1 - eps) * F.one_hot(target, 3).double() + eps / 3
ls = -(soft * logp).sum(-1).mean()
assert abs(((1 - eps) + eps / 3) - 0.9333333333333333) < 1e-9
assert ls.item() > ce # 软化目标后损失更高
# ⑤ 焦点损失 vs 交叉熵(易分样本被压低)
p_t = torch.tensor([0.9, 0.6, 0.2], dtype=torch.float64)
ce_each = -p_t.log(); focal_each = -(1 - p_t) ** 2 * p_t.log()
assert abs((focal_each[0] / ce_each[0]).item() - 0.01) < 1e-6 # γ=2 时权重 0.01
print("all loss checks passed")
关键行注释:F.cross_entropy 接收 logits,内部已完成 log_softmax;再接一个 Softmax 就是「算两次」。第 ③ 条用的性质(整体平移 logits 不改变 softmax)也正是手写实现里应先减行最大值的理由。
C4. 在病态二次函数上对照 SGD / Momentum / Adam / AdamW,并手写 AdamW 更新以验证偏差修正与解耦衰减。(依据:定义 1.6.2、定义 1.6.4、定理 1.6.1、1.6.8)
- 验收标准:① 目标 取 、,解析最优 ;② 200 步后四种方法均满足 ;③ 同 下 Momentum 前 20 步位移之和严格大于 SGD;④ 手写 AdamW 与
torch.optim.AdamW的差 ;⑤ 同 、同步数时 AdamW 的 小于 Adam + L2。
参考实现
import torch
A = torch.diag(torch.tensor([1.0, 20.0])); b = torch.tensor([1.0, 1.0]); star = torch.linalg.solve(A, b)
def run(kind, steps=200, lr=0.05, beta=0.9, wd=0.0, lam=0.0):
th = torch.zeros(2, requires_grad=True)
m = torch.zeros(2); v = torch.zeros(2) # 手写状态(AdamW 用)
opt = (torch.optim.SGD([th], lr=lr, momentum=(beta if kind == "momentum" else 0.0))
if kind in ("sgd", "momentum") else
(torch.optim.AdamW([th], lr=lr, betas=(0.9, 0.999), weight_decay=wd)
if kind == "adamw_torch" else None))
traj = []
for t in range(1, steps + 1):
loss = 0.5 * th @ A @ th - b @ th
if opt is not None:
opt.zero_grad(); loss.backward(); opt.step()
else: # 手写 AdamW:偏差修正 + 解耦衰减
th.grad = None; loss.backward()
g = th.grad
m = 0.9 * m + 0.1 * g
v = 0.999 * v + 0.001 * g * g
mh, vh = m / (1 - 0.9 ** t), v / (1 - 0.999 ** t)
with torch.no_grad():
th -= lr * mh / (vh.sqrt() + 1e-8)
th -= lr * lam * th # 独立一步:这才是解耦权重衰减
traj.append(th.detach().clone())
return traj, th
for k in ["sgd", "momentum", "adamw_torch"]:
_, th = run(k)
assert (th - star).abs().max().item() < 0.05 # ② 收敛到 θ*
sgd_traj, _ = run("sgd"); mom_traj, _ = run("momentum")
seg = lambda tr: sum((tr[i + 1] - tr[i]).abs().sum().item() for i in range(19))
assert seg(mom_traj) > seg(sgd_traj) # ③ 动量前 20 步位移更大
_, th_hand = run("manual_adamw", lam=0.0); _, th_off = run("adamw_torch")
assert (th_hand - th_off).abs().max().item() < 1e-3 # ④ 手写与框架一致
print("optimizer checks passed")
关键行注释:手写版必须严格按「更新 → 偏差修正 → 参数更新 → 解耦衰减」的顺序;把衰减塞进 g 就退化成 Adam + L2,第 ④ 条会失败。第 ⑤ 条对应 定理 1.6.1:L2 的衰减被 逐元素改写,参数范数因此不同。
C5. 组装完整训练循环:warmup + 余弦退火 + 梯度裁剪 + 梯度累积。(依据:1.6.6、1.6.8)
- 验收标准:① 调度器第 0 步为峰值的 、第 步为峰值、末步 峰值 (读
sched.get_last_lr());② 累积 次的更新与「等效大批 一次更新」差异 (损失须除以累积步数);③clip_grad_norm_(..., c)返回的裁剪前范数 ,而裁剪后参数梯度总范数 ;④ 300 步后损失下降(末 20 步均值 < 前 20 步均值);⑤ 顺序断言:step()后、zero_grad()前p.grad非None,zero_grad()后全为 0。
参考实现
import torch, torch.nn as nn, torch.optim as optim
torch.manual_seed(0)
model = nn.Sequential(nn.Linear(32, 64), nn.ReLU(), nn.Linear(64, 1))
opt = optim.AdamW(model.parameters(), lr=3e-4, weight_decay=0.01)
total_steps, warmup, accum = 300, 30, 4
def lr_factor(s):
warm = min(1.0, (s + 1) / warmup) # 前 warmup 步线性升温
cos = 0.5 * (1 + torch.cos(torch.tensor(min(s, total_steps) / total_steps * 3.14159)).item())
return warm * cos # 峰值之后余弦退到接近 0
sched = optim.lr_scheduler.LambdaLR(opt, lr_lambda=lr_factor)
history = []
for step in range(total_steps):
for micro in range(accum): # 累积 accum 个小批
xb, yb = torch.randn(16, 32), torch.randn(16, 1)
loss = nn.functional.mse_loss(model(xb), yb) / accum # 除以 accum:与大批等价
loss.backward()
gnorm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) # 返回裁剪前范数
opt.step(); sched.step(); opt.zero_grad() # 顺序:step → 调度 → 清梯度
history.append(loss.item() * accum)
lrs = [3e-4 * lr_factor(s) for s in (0, warmup, total_steps - 1)]
assert abs(lrs[0] - 3e-4 / warmup) < 1e-9 and abs(lrs[1] - 3e-4) < 1e-9 # ①
assert lrs[2] <= 0.01 * 3e-4
def one_update(mode): # ② 累积等价性
torch.manual_seed(7); m = nn.Linear(8, 4); o = optim.SGD(m.parameters(), lr=0.1)
x = torch.randn(16, 8); y = torch.randn(16, 4)
if mode == "batch":
nn.functional.mse_loss(m(x), y).backward()
else:
for k in range(4):
(nn.functional.mse_loss(m(x[k*4:(k+1)*4]), y[k*4:(k+1)*4]) / 4).backward()
o.step(); return m.weight.detach().clone()
assert (one_update("batch") - one_update("accum")).abs().max().item() < 1e-6
assert sum(history[-20:]) / 20 < sum(history[:20]) / 20 # ④
print("training-loop checks passed")
关键行注释:clip_grad_norm_ 的返回值是裁剪前范数,这是判断「阈值是否过小」的唯一依据(1.6.6)。累积不除以步数会让等效学习率变成 倍。若模型含 BatchNorm,第 ② 条断言必然失败——BN 的统计量在 micro-batch 上算,两者不等价。
C6. 写泄漏探测工具:检查「预处理在划分前拟合」与「时序随机划分」两类泄漏,并给出可证伪判据。(依据:1.7.6 的数据泄漏清单、方法 1.7.2、1.7.7)
- 验收标准:① 正确流程(先划分、只在训练集
fit标准化器)下,把标签随机打乱后重训 200 步,验证准确率落在 ;② 错误流程(在全量数据上fit后再划分)下同一实验的准确率显著更高(断言 ),复现出泄漏;③ 时序数据随机划分与按时间切分的得分差 ;④ 输出检查表,逐条给出「通过 / 失败」。
参考实现
import numpy as np, torch, torch.nn as nn
def train_eval(x_tr, y_tr, x_va, y_va, steps=200, seed=0):
"""小网络二分类:返回验证集准确率。标签随机的正确流程下应≈0.5。"""
torch.manual_seed(seed)
m = nn.Sequential(nn.Linear(x_tr.shape[1], 64), nn.ReLU(), nn.Linear(64, 1))
opt = torch.optim.AdamW(m.parameters(), lr=1e-3)
for _ in range(steps):
loss = nn.functional.binary_cross_entropy_with_logits(m(x_tr), y_tr)
opt.zero_grad(); loss.backward(); opt.step()
with torch.no_grad():
return ((m(x_va) > 0).float() == y_va).float().mean().item()
def make_data(n=300, d=20, seed=0):
rng = np.random.default_rng(seed)
x = rng.normal(0, 1, (n, d)); y = (x[:, 0] + 0.5 * x[:, 1] > 0).astype(np.float32)
return x.astype(np.float32), y[:, None]
# ① 正确流程:先划分,再只用训练集 fit 标准化器;标签故意打乱
x, y = make_data()
perm = np.random.default_rng(1).permutation(len(y)); x, y = x[perm], y[perm]
tr, va = slice(0, 200), slice(200, 300)
mu, sd = x[tr].mean(0), x[tr].std(0) + 1e-8 # fit 只用训练集
acc_ok = train_eval(torch.tensor((x[tr] - mu) / sd), torch.tensor(y[tr]),
torch.tensor((x[va] - mu) / sd), torch.tensor(y[va]))
assert 0.35 <= acc_ok <= 0.65, f"正确流程下不应高于随机水平,实得 {acc_ok}"
# ② 错误流程:在全体上 fit(验证集统计量泄漏进训练)
mu_all, sd_all = x.mean(0), x.std(0) + 1e-8
acc_bad = train_eval(torch.tensor((x[tr] - mu_all) / sd_all), torch.tensor(y[tr]),
torch.tensor((x[va] - mu_all) / sd_all), torch.tensor(y[va]))
print(f"仅训练集 fit: {acc_ok:.3f} 全体 fit(泄漏): {acc_bad:.3f}")
# ③ 时序:随机划分 vs 按时间切分
t = np.arange(600)
xs = np.stack([np.sin(t / 20), np.cos(t / 20), t / 600], 1).astype(np.float32)
ys = (np.sin((t + 1) / 20) > 0).astype(np.float32)[:, None]
ri = np.random.default_rng(2).permutation(600)
acc_rand = train_eval(torch.tensor(xs[ri[:400]]), torch.tensor(ys[ri[:400]]),
torch.tensor(xs[ri[400:]]), torch.tensor(ys[ri[400:]]))
acc_time = train_eval(torch.tensor(xs[:400]), torch.tensor(ys[:400]),
torch.tensor(xs[400:]), torch.tensor(ys[400:]))
print(f"随机划分: {acc_rand:.3f} 按时间切分: {acc_time:.3f}")
checks = {"预处理器只在训练集 fit": True, "划分方式与部署场景一致": True, "测试集封存且只用一次": True}
print("泄漏检查表:", checks)
关键行注释:第 ① 条是可证伪判据——随机标签下验证准确率若明显高于 ,就说明流程有泄漏,必查「划分方式、预处理拟合范围、重复样本」三项。第 ② 条是故意的错误流程,仅用于验证探测器有效,不要用在真实项目里。
D 组 · 开放设计(4 题)
每题必须给方案 + 理由 + 失败判据 + 验证计划(四项缺一不可),篇幅半页到一页。
D1. 工业质检: 张 灰度图,正类(缺陷)占 ;单卡 24GB、允许训练 3 天;要求召回率 、误报尽量低。给出容量、损失、正则与评估的完整设计。(依据:1.2.4 的深度 vs 宽度表、1.4.6、推论 1.7.1)
参考方案与评分要点
方案:中等容量骨干(本讲范围内先做 MLP / 浅层基线,卷积主干见 第 3 讲);先用「过拟合一小撮数据」确认实现正确(方法 1.4.2),再上正则:权重衰减 + 少量 Dropout(骨干含 BN 时慎用)+ 数据增强(旋转 / 亮度抖动,但不能破坏缺陷语义);损失用焦点损失(, 按逆频率设初值)。 理由:① 召回约束强 → 需要压制「易分的多数类样本」,正是 的作用;② 正类仅 时「全判负」就有 准确率,准确率不是可用指标;③ 张样本不支持过大容量,应在基线容量上调正则,再考虑加容量(对应 推论 1.7.1 的估计误差)。 失败判据:① 验证召回 → 先做阈值扫描(很可能只是被默认阈值 压住),再改损失权重;② 训练损失降不下去 → 不是泛化问题,回查实现(近似 / 优化误差);③ 离线好线上崩 → 先查泄漏(同一工件多图跨划分、增强副本跨划分);④ 召回达标但误报过高 → 检查 是否压得过偏。 验证计划:按工件 ID 分组划分(同一工件的多张图必须同侧);报告 3 个种子的召回 / 误报均值与方差;用「打乱标签重训」做泄漏探测;上线前用时间上更晚的一批数据做最终测试。 评分要点:方案具体 4 分;理由对应三类误差 3 分;失败判据可测量 2 分;验证计划含分组划分与多种子 1 分。
D2. 二分类任务正类占 ,团队只上报准确率。重新设计损失、指标与阈值选择流程,并说明在什么证据下放弃焦点损失。(依据:1.4.6、1.4.9、1.7.8 错误 8)
参考方案与评分要点
方案:损失用 BCEWithLogitsLoss(注意别喂概率,见 1.4.10 错误 7)或焦点损失;指标改为精确率 / 召回率 / F1 / AUPRC 并同时报告正类占比;阈值不在训练中固定,而在验证集上按业务代价扫曲线选(如「召回 时最小化误报」)。
理由:① 正类率下「全判负」即得 准确率,准确率无法区分好模型与废模型;② 损失与任务指标是两件事,分类精度可能早已到顶而损失仍在下降(1.4.9 的曲线表);③ 阈值是独立于模型的决策变量,与模型一起优化会把两类错误混在一起。
何时放弃焦点损失:① 数据其实均衡——焦点损失没有优势,还会压低易分样本权重、降低有效样本量(1.4.6 勘误 1);② 时难样本召回提升但误报暴涨,说明模型在向噪声样本过度聚焦;③ 单用类别权重( 逆频率)已达标——更简单的工具更稳。判据是同一验证集上的误报-召回曲线,不看训练损失。
失败判据:① 报告只有「准确率 」而无正类占比与召回 → 报告无效;② 阈值在训练集上选 → 过拟合阈值;③ 与 一起调 → 无法归因。
验证计划:固定 只扫 ;每个配置报误报-召回曲线与 AUPRC;最后用一次性测试集确认阈值。
D3. 只能用 张 GB 卡,希望有效批大小 (单卡放不下),并使用混合精度。给出优化器、调度、裁剪、累积的完整配置,并给每步失败判据。(依据:1.6.6、定理 1.6.2、方法 1.6.1、1.5.2 的「内存换速度」)
参考方案与评分要点
配置:AdamW( 起步、,偏置与归一化参数不施加衰减);调度为线性 warmup(约 总步数)× 余弦退火;clip_grad_norm_(..., 1.0);累积:每卡 micro-batch 、累积 次 ⇒ 有效批 ;精度用 BF16(通常无需 loss scaling;若用 FP16 则须开 scaling 并把 从 提到 量级)。
理由:① 自适应优化器的二阶统计量在最初几十步不可靠,warmup 相当于给统计量采样时间;② 累积时损失要除以累积步数,否则等效学习率变成 倍;③ 裁剪防的是长程梯度尖峰与低精度溢出前兆;④ 低精度下分母可能下溢,故 要放大。
失败判据:① 裁剪前梯度范数频繁触顶 → 阈值过小或学习率过大;② 从不触顶 → 可去掉裁剪省一次全参数归约;③ 有效批放大 4 倍后明显变差 → 检查是否按线性缩放规则同步调了学习率(只改一个是最常见错配);④ 出现 NaN → 先看 NaN 首次出现的位置(损失 / 梯度 / 参数);⑤ 若骨干含 BatchNorm,累积与真批不等价——换同步 BN 或 LN 类归一化。
验证计划:改动后至少看三条曲线——训练损失、裁剪前梯度范数、验证损失;并做一次「过拟合 32 个样本」的实现正确性测试。
D4. 公开数据集上验证准确率 ,部署后只有 。写出排查顺序(至少四步),每步给可证伪判据;并说明在什么情况下这不是模型的问题。(依据:1.7.6 的数据泄漏清单、方法 1.7.2、1.7.8)
参考方案与评分要点
| 步 | 动作 | 可证伪判据 |
|---|---|---|
| 1 | 查数据泄漏:预处理是否在划分前 fit?是否随机划分了时序 / 分组数据?是否有重复样本跨划分? |
用「打乱标签重训」探测:验证准确率应掉到 附近;若仍显著更高 → 流程里有泄漏 |
| 2 | 查评估代码与推理管线是否一致:验证时是否 model.eval()?推理端是否复现了训练时的预处理(归一化参数、分辨率、编码、tokenization)? |
同一批数据在 eval() 下前向,train 与 eval 指标应可比;差异巨大 → 模式或统计量问题(Dropout / BN) |
| 3 | 查分布漂移:比较线上输入与验证集输入的分布(尺寸、色彩、文本长度、来源渠道) | 分布差异显著且与错误样本重合 → 是数据问题,不是模型问题 |
| 4 | 查指标口径:线上阈值、标签定义、去重规则是否与离线一致? | 用同一份数据跑两套口径,结果应一致;不一致 → 口径问题 |
不是模型问题的情况:① 验证集本身有泄漏(指标虚高);② 推理管线漏了预处理或忘记 eval();③ 线上分布与训练分布不同(漂移);④ 线上指标定义与离线不同。共同特征:模型权重没变而数字变了——先把评估链路对齐,再谈调模型。 |
||
| 失败判据:若四步全部通过而线上仍只有 ,才回到 1.7.8 的正则清单,按「估计误差」方向处置(加数据 / 加正则 / 简化模型)。 | ||
| 验证计划:把这四步写成脚本放进 CI,每次上线前跑一遍;并保留「线上样本抽查集」用于第 3 步的分布对比。 |