第 3 讲习题集 · 深度计算机视觉
四组难度递进:A 概念判断 → B 推导证明 → C 编程实现 → D 开放设计。答案默认折叠,点击展开。 建议用法:先闭卷做 A 组,再对照答案;B 组手写推导(不许看答案);C 组上机并跑通验收标准(验收标准本身就是判分线);D 组写成半页方案。 所有题目的答案都能在 3.1–3.10 正文中定位;与 综合自测卷 不重复(个别共用同一组数字的题已在题面注明)。
A 组 · 概念判断(8 题)
A1. 「卷积网络天生具有平移不变性,所以把物体挪几个像素不影响分类结果。」这句话错在哪?请给出两条正确表述,并指明「不变」是哪一步换来的。 (依据:3.1、例 3.3.1)
答案
A2. 「ViT 完全没有归纳偏置,所以只要数据够多就一定强过 CNN。」请分别指出这句话的前半句与后半句的问题。 (依据:3.5、3.5)
答案
前半句错:ViT 的归纳偏置比 CNN 弱,而不是没有。patchify 本身假设每个 token 是一个连续方块(局部性),线性投影等价于一个无重叠的 卷积,patch 内像素的相对排布由投影权重学到(定义 3.5.1)。
后半句错:这是条件性结论,而且「强过」必须有控制变量支撑。ViT-Large 只用 ImageNet-1k 时比 ViT-Base 更差,要到大得多的数据上才展现出规模收益;DeiT 证明差距很大程度来自训练配方(不蒸馏已达 );ConvNeXt 用逐步控制变量实验说明,把配方与微观设计对齐后纯卷积可与同规模 Transformer 持平(ConvNeXt-T 对 Swin-T )。所以选型依据是数据量、延迟、部署工具链与是否需要多尺度输出,而不是「谁更新」(3.5.5)。
A3. 判断并说明理由:「ResNet 之所以能训到几百层,是因为残差连接让梯度变得更大。」 (依据:定理 3.4.1 与其后的两个补充观察)
答案
不准确,只说对了现象的一半。 残差的价值有两件互补的事:① 让恒等映射变便宜——如果某几层的最优行为是「什么都不做」,普通网络要拼出恒等映射(很难),残差块只需把 学成 ,因为 这条通路已经免费提供(优化视角);② 让梯度有一条不经过任何变换的路——,展开式 中唯一不含 的项是 。
对「更大」这个说法的两点纠正:① 是矩阵意义下的直通,一个具体参数收到的梯度是否真的大,还取决于后续层的组合方式与总深度——残差缓解梯度消失,不等于「梯度不会消失」;② 展开式里的高阶交叉项在深层会累积,这是残差网络并非「无条件可训」的原因之一,实践中通常还要配合归一化与合理初始化。所以准确表述是「让深网络在优化上变得可达」,而不是「让梯度变大」。
A4. 「BatchNorm 的作用是把数据标准化,所以把它放在网络的任何位置、任何 batch size 下都能提升效果。」请用三个具体事实反驳。 (依据:定义 3.3.3、3.3.3、例 3.3.4)
答案
三个事实:
- 它依赖批统计,与 batch size 强耦合:统计量在每个通道上、跨该批全部样本的 个元素计算(定义 3.3.3)。检测 / 分割常以 训练,统计噪声大、训练不稳,因此这些框架几乎默认换 GroupNorm(定义 3.3.4)。
- 训练与推理必须用两套统计量:训练用当前批的 ,推理用训练期累积的运行统计量(命题 (3.8) 与动量更新式)。若推理也按当前批算,同一条数据的输出会随它与谁同批而变——部署不可接受,且单样本时方差退化为 。此外领域偏移时运行统计量本身成为误差来源。
- 「哪里都加」会改变层的语义:把 BN 放在残差的加法之后会污染那条恒等通路(pre-activation 的动机,定义 3.4.4);而紧跟 BN 的卷积层偏置是冗余的(例 3.3.3(b)),但 BN 自己的 却必需。另外在逐像素回归类任务(去噪 / 超分)上,BN 曾被观察到反而有害(3.3.3 延伸阅读)。
A5. 「池化层的主要作用是提供平移不变性。」请给出一个反例数字,并说出池化长期存在的三条更实际的理由。 (依据:例 3.3.1)
答案
反例(数字来自正文):对 做 最大池化得 ;把 向右循环平移一格后,输出变化很小(本例中窗口内最大值的位置恰好未改变取值)——但这是「窗口内没有第二个同样强激活」这一条件下才成立的;一旦两个激活强度接近,池化输出会随平移在两者之间跳变(不连续、不可导)。所以这个不变性很弱且有条件。
更实际的三条理由:① 缩小特征图,省后续算力与显存;② 放大后续层的感受野(配合 命题 3.2.4 的递推);③ 提供少许可学的非线性(取最大是一种非线性)。另外 命题 3.3.1 说明最大池化与单调激活可交换,所以「Conv → ReLU → MaxPool」与「Conv → MaxPool → ReLU」数值等价——但这与「提供不变性」是两件事。
A6. 「检测任务只要总体准确率高就可以上线。」请用 个候选位置、 个真实目标算一次,并说明应该看哪些指标。 (依据:例 3.7.1(c))
答案
A7. 「LRN 与 BatchNorm 都是归一化,所以 AlexNet 用 LRN、ResNet 用 BN 只是命名不同。」判断并说明理由;再顺带指出 AlexNet 里 参数在哪里、这个事实导致了什么后续设计。 (依据:3.4.1 的六项创新表与 例 3.4.1)
答案
错。两者机制不同,且 LRN 不是 AlexNet 成功的关键。 LRN(局部响应归一化)只在相邻通道之间做局部的响应抑制,是当时受生物视觉启发的做法,已被 BatchNorm 取代(正文明确写「LRN 本身不是关键」)。BN 则是跨批、逐通道的标准化并带可学习仿射(定义 3.3.3)。
AlexNet 的 参数在全连接层(卷积层仅占 :卷积合计约 ,全连接合计约 ,总计 )。原因是把 维特征直接展平接 维全连接,一层就是 个参数。这个事实催生了两条后续设计:① 全局平均池化 小分类头(3.3.1);② 全卷积网络(把全连接换成卷积用于分割,3.7.4)。VGG16 把这个缺陷放大到 参数在分类器(例 3.4.2)。
A8. 「对抗样本是因为模型过拟合,多训一会儿就没了;而且反正人看不出差别,现实世界不可能发生。」请逐句纠正。 (依据:定义 3.9.3、3.9.3 的误解框)
答案
逐句纠正:
- 不是过拟合。对抗样本在训练集上也存在,而且能跨模型迁移(在一个模型上生成的扰动可能骗过结构相近的另一个模型);把成因归给过拟合会导向「多训一会儿就好」的错误结论。
- 「人看不出异常」不代表安全。模型的决策边界与人的语义边界不是同一条:ReLU 网络是高维分段线性函数,边界在部分方向上离数据点极近,而人眼对高频内容不敏感、浅层卷积却对高频模式敏感。所以靠肉眼审查输出保证安全是不可靠的。
- 物理世界可行,但需要精心构造。打印图片、贴纸、眼镜等条件下的实现已被证明可行,但光照、视角、拍摄距离等条件比数字域苛刻得多——准确表述是「可行但需精心构造」,既不是「随手就能攻击任何系统」,也不是「不可能」。
- 附带纠正:鲁棒性永远相对于攻击模型与预算(、范数、步数),任何「某某模型已经鲁棒」的表述都必须附攻击设置,否则没有意义。
B 组 · 推导与证明(6 题)
B1. 推导输出尺寸公式 (3.2) 的取整来源,并据此手算五个配置:
(a) ;(b) ;(c) ;(d) ;(e) 。
要求:涉及形状的先写形状,不许写「略」。
(依据:命题 3.2.2 与其 proof、例 3.2.1)
提示与答案
推导:窗口左边界只能取整数。设最后一个合法窗口的左边界为 ,其右边界 必须落在填充后的输入范围内(长度 ),故 。 从 出发每次加 ,可取个数为
向下取整的来源:边界处剩余像素不足 个、放不下完整窗口时该位置被丢弃;框架(如 PyTorch Conv2d)不会补齐不完整位置, 时直接报错。
手算(一维,二维逐轴独立):
- (a) ——
valid约定,边界丢两圈信息。 - (b) ——
same约定,尺寸不变。 - (c) —— 精确减半,骨干降采样常用。
- (d) , —— 空洞卷积保尺寸扩视野,参数仍是 9 个而视野变 。
- (e) —— 这就是 ResNet 首层输出(再接 最大池化得 )。
三条随手可得的规律: 且 ( 奇)→ 尺寸不变; → 大致减半; → 核撑大而参数不变。
B2. ResNet-50 的瓶颈块在 上做三次卷积:()、(,)、(),全部 。 (a) 分别写出三步的参数量与 MACs;(b) 求整块参数量与 MACs;(c) 与「单层 、」比较,给出参数与算力的比值;(d) 用一句话说明这个设计把省下的预算换成了什么。 (依据:例 3.2.2、定义 3.4.4)
提示与答案
B3. 从 出发推导残差网络的梯度结构 (3.10):
(a) 写出 与 ;(b) 展开连乘并指出哪一项是「直通项」;(c) 设某一层残差分支的雅可比满足 且各层相同,比较「有残差」与「无残差(梯度为纯连乘 )」两种情形下传回浅层的梯度量级(取 );(d) 说明 为什么不会被训练改掉,并指出这个论证不能推出什么。
(依据:定理 3.4.1 与其 proof)
提示与答案
(a) 由 , 再由 即得 (3.10)。
(b) 记 ,则 展开式里每一项都含至少一个 ,唯一不含 的项是 ——它就是直通项(它给出 ,即从深层原样带回的一份梯度)。
(c) 取 、:
- 无残差: —— 已经彻底消失。
- 有残差:(近似按标量 ) 与 同量级,不会降到零。
(注意这里是量级估计:矩阵情形下非交换性会使确切的范数依赖结构,但「有一项恒为 」这一点不依赖结构。)
(d) 不依赖于任何权重,所以它不可能被梯度下降「学掉」;类比是给梯度修了一条无需维护的高架桥。
不能推出什么:① 不能说「梯度一定很大」—— 是矩阵意义的直通,具体参数收到的梯度大小还取决于后续层的组合与总深度;② 不能忽略展开式里的高阶交叉项在深层的累积,这正是残差网络并非「无条件可训」的原因之一,实践中还需归一化与合理初始化;③ 不能推出「更深一定更好」——原文里千层网络能训起来,但精度并不比百层更好。
B4. BN 的三条代数性质:(a) 证明 BN 对「整通道加常数」不变;(b) 证明紧跟 BN 的卷积层偏置是冗余的(训练与推理两种情形都要说);(c) 说明 BN 的额外计算量为何在 FLOPs 上可忽略、却在内存带宽上代价明显。 (依据:例 3.3.3)
提示与答案
(a) 设 ( 遍历该通道全部元素)。则 ,且 ,故 ,于是 (推理时用运行统计量同理:训练与推理的常数一致则抵消,不一致会引入系统性偏移——这正是领域偏移下 BN 成为误差源的原因。)
(b) 设卷积输出 。训练时 BN 算 ,而
故分子中 被完全减掉;既然结果与 无关,该参数永远冗余(其作用可由 BN 的 承担)。推理时运行统计量 同样吸收了等量偏移,抵消关系不变。工程含义:紧跟 BN 的卷积可以设 bias=False,省参数也省一次加法。
(c) BN 在每个激活值上做:一次减法 一次乘以预计算好的 ,约 次逐元素运算。对照一层 、 卷积:每个输出元素要 次 MAC。故 BN 相对卷积约 ,FLOPs 上可忽略。但它需要两次全张量归约与读写(求均值、求方差),访存量与激活张量同阶——所以「BN 拖慢小模型推理」的观感来自内存带宽而非 FLOPs(例 3.3.3(c))。
B5. 写出 InfoNCE 损失 (3.11),并推导它对相似度的梯度: (a) 记 ,证明 以及 时 ; (b) 由此写出 的分段形式; (c) 解释「难负样本主导学习」「正样本已分对时该项不再贡献梯度」「温度 越小梯度整体越大」这三句话分别对应公式里的哪一项。 (依据:定义 3.6.3、推导 3.6.1)
提示与答案
(a) 由 、,得 ,于是
(b) 由 : 其中 是负样本上的 softmax 分布()。
(c) 换回 后, 是梯度下降方向,它把 往正样本拉、从负样本推开,权重分别是 与 :
- 难负样本主导学习: 大(当前被判得越像正样本)的负样本被推得越用力;
- 正样本已分对时不再贡献:,吸引项消失;
- 越小梯度越大: 在分母上,整体缩放因子是 ,因此需要配合更小的学习率。
附带的等价性:这一切正是「在表示空间做 类分类」的梯度——(3.11) 与交叉熵同形。
B6. 分群评估的算术:某二分类人脸验证系统在 人的测试集上评估,群体 A 有 人、错误率 ,群体 B 有 人、错误率 。 (a) 求总体准确率与两个群体的准确率;(b) 求「错误拒绝人数」并解释为什么绝对人数接近不代表公平;(c) 用一段话说明评估报告应当包含什么;(d) 举例说明什么情况下「错误率接近」也不能当作问题已解决的证据。 (依据:例 3.9.1、3.9.2)
提示与答案
(a) 总错误 ,总体准确率 ;群体 A ,群体 B 。
(b) 错误拒绝人数:群体 A 人;群体 B 人。绝对人数接近(90 对 120),但相对概率相差 倍——意味着群体 B 的每个个体被挡在门外的概率是群体 A 的 倍。这是个体层面的差别对待;而从系统层面看,占比 的 B 群体的高错误率在总体平均里被「稀释」了,这个偏差不会出现在任何只看总体准确率的报表上。
(c) 报告必须包含分组混淆矩阵(分组 precision / recall / FPR / FNR),并且要检查各组样本量是否足够得出有意义的区间估计——小样本子群上的指标方差很大,「看起来有差异」也可能只是噪声。这两条要同时做。
(d) 三条例由(3.9.2):① 该数据集本身可能不代表部署环境;② 错误率相近不等于特定阈值下的误识比例相近(阈值选择会放大或缩小差异);③ 不同地区的采集、光照、摄像头质量差异没有被覆盖。所以这类结论必须带条件陈述,不能作为普遍保证。
C 组 · 编程实现(6 题)
依赖只用
numpy/torch。每题末尾给出可运行的验收标准——请把它当成判分线,先跑断言,再看解释。
C1. 卷积 = 互相关:手写 conv2d 并与 nn.Conv2d 数值比对。
实现 conv2d(x, w, b, stride, padding)(支持方形核、方形步幅、零填充、多输入输出通道),要求用显式循环或 F.unfold 皆可。
(依据:定义 3.2.1、3.2.8 的 im2col 代码)
- 验收标准:
x=(2,3,224,224)、nn.Conv2d(3,64,3,stride=2,padding=1)时输出形状断言(2,64,112,112);且sum(p.numel() for p in conv.parameters()) == 1792。- 与
nn.Conv2d的最大绝对误差 (fp32,随机权重与输入)。 - 用 im2col 版本时打印
cols.shape,并断言「内存放大倍数 」( 时 9 倍)。 - 反向检查:把核翻转 180° 后再做上述比对,输出应等于「数学卷积」的结果(两次翻转再翻转即还原)——用来确认你知道框架算的是互相关。
参考实现
import torch, torch.nn as nn, torch.nn.functional as F
def conv2d_via_unfold(x, weight, bias=None, stride=1, padding=1):
# x: (B,C_in,H,W); weight: (C_out,C_in,k,k) —— 关键:核的第 0 维是输出通道
B, C, H, W = x.shape
C_out, _, k, _ = weight.shape
if padding:
x = F.pad(x, (padding,) * 4)
cols = F.unfold(x, kernel_size=k, stride=stride) # (B, C*k*k, L)
out = weight.view(C_out, -1) @ cols # (B, C_out, L)
if bias is not None:
out = out + bias.view(1, -1, 1)
Ho = (H + 2 * padding - k) // stride + 1 # 与公式 (3.2) 一致
Wo = (W + 2 * padding - k) // stride + 1
return out.view(B, C_out, Ho, Wo), cols
torch.manual_seed(0)
x = torch.randn(2, 3, 224, 224)
conv = nn.Conv2d(3, 64, 3, stride=2, padding=1)
y = conv(x)
assert y.shape == (2, 64, 112, 112) # 验收 1
assert sum(p.numel() for p in conv.parameters()) == 1792 # 验收 1
y2, cols = conv2d_via_unfold(x, conv.weight, conv.bias, stride=2, padding=1)
print("最大误差 =", (y - y2).abs().max().item()) # 验收 2:期望 <1e-5
print("cols", tuple(cols.shape), "放大倍数 C_in*k^2 =", 3 * 9) # 验收 3
# 验收 4:翻转核 → 数学卷积
w_flip = torch.flip(conv.weight, dims=[2, 3])
y_math, _ = conv2d_via_unfold(x, w_flip, conv.bias, stride=2, padding=1)
print("与翻转核输出的差 =", (y - y_math).abs().max().item()) # 不为 0,说明两者不同约定
C2. 感受野与跳距递推器:给一串层配置,自动输出 、 表,并复算正文三组结果。
实现 trace_receptive_field(layers),输入形如 [("conv",7,2),("pool",3,2),("conv",3,1)] * n 的列表,输出每步的 。
(依据:命题 3.2.4、例 3.2.3、例 3.3.2)
- 验收标准:
- 层 :断言 ( 全通过)。
- ResNet 风格浅层骨架: → 池化 → 12 层 :断言 。
- 4 次 最大池化 :断言 序列为 。
- 4 次 卷积:断言最终 且 与池化版同阶(正文给出 )。
- 加一条自检: 时断言 不变()。
参考实现
def trace(layers, first=True):
"""layers: [('conv'|'pool', k, s), ...];首层直接作用输入时取初始条件 r1=k, j1=s。"""
r = j = 0; out = []
for kind, k, s in layers:
r, j, first = (k, s, False) if first else (r + (k - 1) * j, j * s, False)
out.append((kind, k, s, r, j)) # r_{l+1}=r_l+(k-1)j_l, j_{l+1}=j_l*s
return out
assert all(trace([('conv', 3, 1)] * L)[-1][3] == 2 * L + 1 for L in range(1, 21)) # 验收 1
assert trace([('conv', 7, 2), ('pool', 3, 2)] + [('conv', 3, 1)] * 12)[-1][3:] == (107, 4) # 验收 2
assert [x[3:] for x in trace([('pool', 3, 2)] * 4)] == [(5, 2), (9, 4), (17, 8), (33, 16)] # 验收 3
assert trace([('conv', 3, 2)] * 4)[-1][4] == 16 and trace([('conv', 1, 1)])[-1][3] == 1 # 验收 4、5
(注:验收 4 中「4 次 卷积」的 与池化版不同,原因是首层的初始条件不同——池化版首层 直接作用于输入,卷积版按 例 3.3.2 的记法给出 。)
C3. 深度可分离 / 分组卷积的省算比:参数与 MACs 双重核对。
用 nn.Conv2d(..., groups=...) 搭出标准卷积、分组卷积()与深度可分离卷积(逐通道 逐点),比较三者在 上的参数量与比值。
(依据:定义 3.2.5、命题 3.2.6、例 3.2.4)
- 验收标准:
- 参数量断言:标准 ;分组 为 ;深度可分离合计 。
- 实测比值断言:
sep/standard落在 (正文给 );分组比值约 ()。 - 理论核对:断言 ( 时约 );并验证「含偏置时实测比值略大于该式」。
- 形状断言:三条路径输出形状都必须是 。
- 附一条实验:把
groups从 1 扫到 64,断言参数量随组数单调不增。
参考实现
import torch, torch.nn as nn
x = torch.randn(2, 64, 56, 56)
std = nn.Conv2d(64, 64, 3, padding=1) # 标准
grp = nn.Conv2d(64, 64, 3, padding=1, groups=4) # 分组 g=4
dw = nn.Conv2d(64, 64, 3, padding=1, groups=64) # 逐通道 groups=C_in=C_out
pw = nn.Conv2d(64, 64, 1) # 逐点
n = lambda m: sum(p.numel() for p in m.parameters())
assert (n(std), n(grp), n(dw) + n(pw)) == (36928, 9280, 4800) # 验收 1、2
assert all(m(x).shape == (2, 64, 56, 56) for m in (std, grp, dw, pw)) # 验收 4
assert abs((1 / 64 + 1 / 9) - 0.127) < 1e-3 # 验收 3
r = [n(nn.Conv2d(64, 64, 3, padding=1, groups=g)) for g in [1, 2, 4, 8, 16, 32, 64]]
assert r == sorted(r, reverse=True) # 验收 5:单调不增
print("sep/std =", round(4800 / 36928, 3), " grp/std =", round(9280 / 36928, 3))
C4. BN 的训练 / 推理差异与运行统计量更新:三层验收。
(a) 手算最小对照并与 nn.BatchNorm2d 对齐;(b) 演示同一输入在 train() 与 eval() 下输出不同;(c) 验证「紧跟 BN 的卷积偏置被吃掉」。
(依据:定义 3.3.3、例 3.3.3、例 3.3.4)
- 验收标准:
- 手算对齐:输入
x=torch.tensor([1.,2.,3.,4.,10.,20.,30.,40.]).view(2,4,1,1),nn.BatchNorm2d(4, eps=0, affine=False, momentum=None, track_running_stats=False)在train()下输出应逐元素等于[[-1,-1,-1,-1],[1,1,1,1]](容差 );同一输入用nn.GroupNorm(2,4,eps=0,affine=False)应得到全 的模式(保留组内递增结构)。 - train/eval 差异:先用
nn.BatchNorm2d(4)跑若干批随机数据,再固定z,断言train与eval两次前向的最大差 。 - 偏置冗余:固定
running_mean=0、running_var=1,对同一输入把卷积偏置整体平移 ,断言两次输出的最大差 。 - 统计量更新方向:
momentum=0.1时手动用running = (1-m)·running + m·batch_stat复算一轮,断言与框架的running_mean/var误差 。 - 用同一份代码打印「batch size 为 1 时
train()输出的方差」——断言接近 (这就是推理必须eval()的原因)。
- 手算对齐:输入
参考实现
import torch, torch.nn as nn
# 验收 1:与例 3.3.4 的手算逐元素对齐
x = torch.tensor([1., 2., 3., 4., 10., 20., 30., 40.]).view(2, 4, 1, 1)
bn = nn.BatchNorm2d(4, eps=0, affine=False, momentum=None, track_running_stats=False).train()
assert torch.allclose(bn(x).view(2, 4), torch.tensor([[-1., -1., -1., -1.], [1., 1., 1., 1.]]))
gn = nn.GroupNorm(2, 4, eps=0, affine=False) # g=2:每样本两组
assert torch.allclose(gn(x).view(2, 4, 2), torch.tensor([[[-1., 1.], [-1., 1.]]] * 2))
# 验收 2、5:train/eval 差异 + batch=1 时统计退化
bn2 = nn.BatchNorm2d(4)
for _ in range(5): _ = bn2(torch.randn(8, 4, 4, 4)) # 让 running stats 离开初值
z = torch.randn(1, 4, 4, 4)
bn2.train(); a = bn2(z); bn2.eval(); b = bn2(z)
assert (a - b).abs().max().item() > 1e-4 and a.var().item() < 1e-6 # 方差≈0 → 必须 eval()
# 验收 3:卷积偏置被 BN 吃掉(必须对同一个固定输入比较两次输出)
conv = nn.Conv2d(3, 8, 3, bias=True); bn3 = nn.BatchNorm2d(8).eval()
bn3.running_mean.zeros_(); bn3.running_var.fill_(1.0)
a_in = torch.randn(2, 3, 8, 8)
with torch.no_grad():
conv.bias.data.normal_(); y1 = bn3(conv(a_in))
conv.bias.data.add_(100.0); y2 = bn3(conv(a_in))
assert (y1 - y2).abs().max().item() < 1e-4
# 验收 4:手动复算 running stats
bn4 = nn.BatchNorm2d(4, momentum=0.1); d = torch.randn(6, 4, 5, 5); _ = bn4(d)
mu, var = d.mean(dim=(0, 2, 3)), d.var(dim=(0, 2, 3), unbiased=False)
assert torch.allclose(bn4.running_mean, 0.1 * mu, atol=1e-5)
assert torch.allclose(bn4.running_var, 0.9 * 1 + 0.1 * var, atol=1e-5)
(提醒:running_mean 的初值是 、running_var 的初值是 ,所以第一轮的更新式写出来是 与 ——把这两个初值写错是最常见的实现级错误。)
C5. 检测后处理:IoU 匹配、NMS 与 AP 手算复现(含两种口径)。 (a) 用正文 例 3.10.2 的四个窗口做正 / 负 / 忽略区分类;(b) 实现 NMS 并验证阈值方向;(c) 用正文 例 3.7.1 的 12 个判定序列复现 点插值 AP 与 COCO 风格 AP。 (依据:定义 3.7.2、定义 3.7.3、方法 3.7.1、例 3.10.2)
- 验收标准:
- IoU 数值断言(容差 ):窗口 A / B / C / D 相对真值 分别为 、、、;分类为 正 / 负 / 负 / 正(正阈值 、负阈值 );并断言「最适合训练回归分支的是 A」(形状与真值一致,回归目标只有平移)。
- IoU 边界约定:断言
iou((0,0,10,10),(5,5,15,15)) == 25/175(),且不相交时返回 (不许出现负数)。 - NMS 方向:5 个互相 IoU 的框,
iou_thr=0.5时断言输出恰好 1 个框;把判据写成「IoU 大于阈值才抑制」,并断言写反版本会删掉所有框(输出只剩 1 个且是小分数那个)。 - AP 复现:给定 、12 个按置信度排序的判定
TP,TP,FP,TP,FP,TP,TP,FP,TP,FP,TP,FP,断言 点插值 AP 落在 (正文 ),并打印 COCO 风格结果,断言它低于 11 点结果(正文约 ,差约 4 个百分点)。 - 平凡预测的准确率:断言「全部判背景」在 个位置、 个目标时准确率 而 recall 。
参考实现
def iou(a, b):
ix1, iy1 = max(a[0], b[0]), max(a[1], b[1])
ix2, iy2 = min(a[2], b[2]), min(a[3], b[3])
iw, ih = max(0.0, ix2 - ix1), max(0.0, iy2 - iy1) # 连续坐标:x2 - x1,不加 1
inter = iw * ih
union = (a[2]-a[0])*(a[3]-a[1]) + (b[2]-b[0])*(b[3]-b[1]) - inter
return inter / union if union > 0 else 0.0
G = (40, 40, 120, 120)
cands = {"A": (44, 44, 116, 116), "B": (80, 80, 152, 152), "C": (0, 0, 40, 40), "D": (40, 32, 124, 132)}
exp = {"A": 0.81, "B": 0.160, "C": 0.0, "D": 0.762}
for k, box in cands.items():
assert abs(iou(G, box) - exp[k]) < 1e-3, (k, iou(G, box)) # 验收 1
assert abs(iou((0,0,10,10), (5,5,15,15)) - 25/175) < 1e-6 # 验收 2
def nms(dets, thr=0.5):
dets = sorted(dets, key=lambda d: -d[4])
kept = []
while dets:
best = dets.pop(0); kept.append(best)
dets = [d for d in dets if iou(best, d) <= thr] # 大于阈值才抑制
return kept
dets = [(0, 0, 20, 20, 0.9), (1, 1, 21, 21, 0.8), (2, 2, 22, 22, 0.7), (3, 3, 23, 23, 0.6), (4, 4, 24, 24, 0.5)]
assert len(nms(dets, thr=0.5)) == 1 # 验收 3
def ap_11pt(judgements, n_gt):
tp = fp = 0; prec = []; rec = []
for j in judgements:
tp += (j == "TP"); fp += (j == "FP")
if tp + fp == 0: continue
prec.append(tp / (tp + fp)); rec.append(tp / n_gt)
out = []
for r in [i / 10 for i in range(11)]:
cand = [p for p, rr in zip(prec, rec) if rr >= r - 1e-12]
out.append(max(cand) if cand else 0.0)
return sum(out) / 11, out
J = ["TP","TP","FP","TP","FP","TP","TP","FP","TP","FP","TP","FP"]
ap, pts = ap_11pt(J, 10)
assert 0.58 <= ap <= 0.60, ap # 验收 4(正文 0.589)
print("11 点 AP =", round(ap, 4), "各点 =", [round(x, 3) for x in pts])
print("全部判背景的准确率 =", 1 - 10 / 1_000_000, " recall =", 0) # 验收 5
C6. 迁移学习的防泄漏验收 + FGSM 攻击与防御失败演示(两个小实验,各附断言)。 (a) 冻结骨干只训线性头,复现「可训练参数量差约 倍」的量级并跑通一次训练;(b) 对分类器做 FGSM,报告攻击成功率、 幅度与置信度变化,并演示一个简单防御在自适应攻击下失效。 (依据:例 3.6.1、3.6.5、3.10.4、例 3.9.2)
- 验收标准:
- 参数量与显存量级:用
torchvision.models.resnet50(weights=None),断言「只训头」可训练参数在 (Linear(2048,20)为 ),全网络为 ,比值落在 ;并断言 fp32 梯度显存估算 MB、含 Adam 动量 MB(量级即可,允许 )。 - 迁移优于随机初始化:在一个人造小数据集(如
sklearn.datasets.load_digits,或自建 张图 / 类)上,比较「冻结骨干 线性头」与「同结构随机初始化 线性头」,断言冻结方案在验证集上的指标更高且更快到达(例如 20 个 epoch 内)。 - 防泄漏划分:若数据来自若干「实体」(同一人的多张照片 / 同一设备的多次拍摄),必须按实体划分;断言
set(train_ids) & set(val_ids) == set(),并断言随机按图划分会让验证指标虚高(打印两种划分下的指标差)。 - FGSM 有效且方向正确:对固定的小 CNN, 时断言攻击成功率 ;断言扰动 幅度 (在未触发 clamp 的样本上);断言
clamp(x', 0, 1)后x'全部落在 。 - 防御失效演示:在「已知预处理的攻击」设定下,JPEG 压缩 / 高斯去噪等简单防御的攻击成功率下降幅度 (即仍然大部分成功)——用这个断言证明「预处理不是保证」。
- 参数量与显存量级:用
参考实现
import torch, torch.nn as nn, torch.nn.functional as F
from torchvision.models import resnet50
# 验收 1:可训练参数量与显存量级(对应例 3.6.1)
m = resnet50(weights=None); m.fc = nn.Linear(2048, 20)
head = sum(p.numel() for p in m.fc.parameters())
for p in m.parameters(): p.requires_grad_(False)
for p in m.fc.parameters(): p.requires_grad_(True)
trainable = sum(p.numel() for p in m.parameters() if p.requires_grad)
assert 3e4 < head < 5e4 and trainable == head
full = 25_500_000
assert 500 < full / head < 800 # ≈622 倍
print("梯度显存 ≈", head * 4 / 1e6, "MB;含 Adam ≈", head * 12 / 1e6, "MB")
print("全网络:≈", full * 4 / 1e6, "MB;含 Adam ≈", full * 12 / 1e6, "MB")
# 验收 4:FGSM(对输入求导,不是对参数)
def fgsm(model, x, y, eps=8/255):
model.eval()
x = x.clone().detach().requires_grad_(True)
loss = F.binary_cross_entropy_with_logits(model(x).squeeze(1), y.float()) # 二分类人脸/背景
(g,) = torch.autograd.grad(loss, x)
x_adv = (x + eps * g.sign()).clamp(0, 1) # clamp 到合法像素范围
return x_adv.detach(), g
x, y = torch.rand(64, 3, 32, 32), torch.randint(0, 2, (64,))
# 用一个已训好的 model;此处只演示流程与断言
class Net(nn.Module):
def __init__(s):
super().__init__(); s.net = nn.Sequential(
nn.Conv2d(3, 32, 3, 2, 1), nn.ReLU(), nn.Conv2d(32, 64, 3, 2, 1), nn.ReLU(),
nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Linear(64, 1))
def forward(s, v): return s.net(v)
model = Net()
x_adv, g = fgsm(model, x, y)
assert x_adv.min() >= 0 and x_adv.max() <= 1 # 验收 4:合法像素
print("‖δ‖∞ =", (x_adv - x).abs().max().item(), "(期望 = 8/255 ≈", 8/255, ")")
print("扰动放大 32 倍后可可视化:", ((x_adv - x)[0] * 32).abs().max().item()) # 有结构、非白噪声
# 验收 5:简单防御在自适应攻击下仍大部分成功
def jpeg_like(x, k=8): # 用低通近似「压缩」这个预处理
return F.avg_pool2d(F.pad(x, (k, k, k, k), mode="reflect"), 2 * k + 1, 1)
# 正确做法:在攻击时把这个 k 纳入前向(自适应),再比较成功率下降幅度
D 组 · 开放设计(4 题)
每题必须给出四项:方案 + 理由 + 失败判据 + 验证计划。缺少任何一项按未完成处理。
D1. 数据偏见的缓解与评测清单。 场景:你要为一个公共场所门禁人脸验证系统交付模型。数据来自某厂商的监控抓拍,已知年龄与肤色分布不均,且采集时未按人群分层。约束:不能重新采集(成本与合规都不允许),可用算力约 张消费级显卡,交付周期 周。 要求:给出缓解与评测清单,并说明哪些做法你明确不做以及为什么。 (依据:3.9.1、3.9.2 的缓解清单、3.10.6 的实验 A、出口判据)
参考方案与评分要点
方案(分层):
- 数据层:先做子群样本量统计(性别 / 年龄段 / 肤色区间 / 采集设备 / 光照条件),对样本过少的子群明确标注「无法评估」而不是并入总体;记录数据来源、授权范围与保留期限;对网络爬取部分标注潜在偏差。
- 模型与阈值层:不用默认阈值,按业务代价选择阈值(门禁误识代价高 → 提高阈值,宁可拒识);报告分组混淆矩阵(各组 precision / recall / FPR / FNR)与置信区间;分组校准作为有争议的备选,需法务与伦理评审后再决定。
- 系统层:人在回路(高风险判定必须复核)、可追溯日志与申诉通道、数据最小化(只存不可逆模板,且模板按用途绑定)、明确用途与时限。
理由:偏见有四个进入点(采集、标注、任务定义、部署),不能只治一处;放大会通过反馈回路自我强化,因此必须在数据进模型之前和模型出结论之后两端设卡。
失败判据(可测量):① 任一分组样本量 时该组的结论视为不可靠,不得写成「已评估」;② 分组 FPR 相对差 倍时必须停止上线流程重新设计(正文 例 3.9.1 的反例是 倍);③ 总体准确率与最差子群准确率之差 时必须给出书面风险说明与缓解计划。
验证计划:① 分群评估复现(含样本量与置信区间);② 用「阈值—代价曲线」为门禁场景选定工作点,并记录选择依据;③ 在一批留出的、按实体划分的数据上做一次盲测(防止同一人多张照片跨划分造成虚高);④ 上线后按周监控被拒样本的分布漂移。
明确不做的:① 不把「只存特征模板」当隐私解决方案(模板可被反演、且不可撤销,3.9.4);② 不用总体准确率作为验收依据;③ 不在样本不足的子群上宣称公平性。
评分要点:分层清单 分;失败判据可测量 分;验证计划含防泄漏设计 分;风险与「不做什么」的自觉 分。
D2. 检测系统的上线清单。 场景:一个工业质检检测器(缺陷框 + 类别),产线节拍要求单图推理 ms、漏检代价高(缺陷流入下一工序)、误检会带来人工复检成本。数据只有 张标注图,缺陷尺度差异极大(有的只占 像素)。 要求:给出从数据、模型、评估到上线的完整清单,并写出上线门槛数字与回滚条件。 (依据:3.7.2、3.7.3、3.7.4 的失败分析、3.10.3、例 3.7.1)
参考方案与评分要点
方案:
- 数据:统计目标尺寸分布并对小目标增采样;按批次/设备分层划分(避免同批相似图跨划分造成泄漏);正负样本比极高,用加权采样或 focal loss(3.7.3)。
- 模型:骨干用与域接近的自监督预训练权重(3.6.5 第 2 步),先冻结只训头再解冻;小目标必须用多尺度特征(FPN 类结构,3.7.3)或特征金字塔式的多尺度推理,不能只靠全局注意力 + 粗网格(3.7.5 已说明这类架构天然对小目标不友好)。
- 评估:报 AP 并说明口径(IoU 阈值集合、是否跨类别);按目标尺寸分组统计召回(3.7.4 的分组表:小目标 面积是最易崩的一组);漏检与误检分开计数;固定随机种子并报多轮均值与方差。
- 上线:置信度下限以下拒答(交给人工);保留被拒样本日志;按周抽查漂移。
上线门槛数字(示例,需按产线确认):① 小目标分组召回 、整体误检率 /图;② 单图 P99 延迟 ms;③ 拒答率 (超过说明数据或阈值需要重设)。
失败判据:① 小目标分组召回低于大目标 以上 → 回退到更高分辨率特征或多尺度推理;② 拒答率 或人工复检负载超预算 → 重新标定阈值;③ 连续两批生产的漏检率超阈值 → 立即回滚到上一版模型并冻结产线该工序。
验证计划:① 先过拟合 10 个样本确认实现无误(3.10.3);② 按尺寸分组的 PR 曲线与工作点选择;③ 与上一代方案的同一测试集盲测;④ 上线后按批做分组召回监控,并维护「被拒 / 低置信 / 异常大小框」三类日志。
评分要点:清单覆盖数据—模型—评估—上线四层 分;门槛与回滚条件可测量 分;识别小目标与口径两处陷阱 分。
D3. 迁移学习方案:小数据、领域有偏移、有延迟约束。 场景:医疗影像二分类(如病灶良恶性),目标域与自然图像差别大,只有 张标注图( 类),单张推理延迟必须 ms(边缘设备),且必须可复现、可审计。 要求:在「全参数微调 / 只训头 / 轻量适配(LoRA 类)/ 目标域自监督预训练」四条路线中选择并组合,给出方案、理由、失败判据与验证计划(含防泄漏设计)。 (依据:定义 3.6.1、方法 3.6.1、例 3.6.1、定义 3.6.2、3.6.5、定义 3.8.3 的 LoRA)
参考方案与评分要点
选择:目标域自监督预训练(能拿到大量无标签影像的话)→ 冻结骨干只训头 → 解冻后段微调(小学习率),即「自监督(可选) 两阶段迁移」;不选全参数微调作为起点,也不把 LoRA 作为首选(它更适合显存受限的大模型场景,在 张图、小骨干上收益有限)。
理由:
- 目标域与自然图像差异大 → 浅层特征也不再通用,迁移收益下降,故先做领域适配(最便宜的版本是重估归一化统计量:冻结全部参数,用目标域无标签数据重跑前向重新累积 running stats,几十到几百个 batch 即可,3.6.2);有条件则做目标域自监督预训练。
- 张图很危险:全网络微调的可训练参数是「只训头」的约 倍(例 3.6.1 的量级),小数据下应把可训练参数压到 量级起步。
- 骨干学习率需比头小约一个量级,或使用分层衰减 ( 取 )。
- 延迟 ms → 选小骨干并量化;迁移策略本身不影响推理结构(冻结 / 微调是训练期的事)。
- 可审计 → 记录骨干来源与版本、预处理统计量、随机种子与划分文件。
失败判据:① 冻结版与解冻版的验证 AUC 差 → 保持冻结(解冻没带来收益却增加了过拟合风险);② 解冻后验证指标反而下降 → 回退并把骨干学习率再降 倍;③ 分组(按设备 / 中心 / 性别)指标相对差 倍 → 进入 3.9 的治理流程;④ 单张延迟实测 ms → 换更小骨干或量化,不接受把延迟转嫁给临床流程。
验证计划:① 按患者(实体)划分,断然不能按图划分——同一患者的多张影像跨划分是最典型的泄漏(3.10.5 的「验证指标远低于训练」一行的正解);② 患者级别的 折交叉验证(小数据下比单一划分更可信);③ 预处理一致性核对(分辨率、归一化统计量、通道顺序必须与骨干匹配,否则初始 loss 会异常高);④ 报告多轮运行均值与方差,并明确「没做什么」。
评分要点:路线选择与理由 分(必须出现「先冻结后解冻」「重估统计量」「骨干学习率小一个量级」中至少两点);失败判据可测量 分;验证计划含患者级划分 分;可复现 / 审计要求 分。
D4. 对抗鲁棒性的取舍。 场景:安全相关的行人检测系统(例如施工区域闯入告警)。业务要求:① 干净数据上的精度不能明显下降;② 要能对一定的恶意扰动保持稳定;③ 训练预算有限(既有算力只能再训一轮)。 要求:给出(a)鲁棒性的可检验定义(必须带攻击模型与预算);(b) 防御方案与预算再分配;(c) 失败判据;(d) 验证计划(含自适应攻击)。 (依据:3.9.3、例 3.9.2、3.9.5 的 think 框、3.10.4)
参考方案与评分要点
(a) 可检验定义:鲁棒性必须写成「在 (或 )且攻击算法为 PGD-(步长 、重启次数 )的攻击下,分组召回率的下降不超过某阈值」。示例:、PGD-、单次重启,要求「行人分组召回下降 」。没有攻击设置的「已鲁棒」不构成陈述。
(b) 方案与预算分配:把有限的一轮训练预算分给三件事——
- 对抗训练(选最小代价的版本):用 FGSM 或 PGD-2/3 而不是 PGD-10 做内层最大化,只对正样本与难负样本做(检测任务里全量对抗训练成本过高);明确接受「只对训练时的 与范数有效」。
- 输入侧与系统侧的纵深:多尺度一致性检查(同一目标在不同尺度下应给出相近分数)、更大感受野的上下文特征、把「不确定」变成一次拒答 / 复核而不是一次错误告警——这条最便宜且不会被自适应攻击绕过。
- 不做:不做「堆一层检测器来识别对抗样本」的方案——攻击是自适应的,攻击者只需找到一个漏洞,防御要堵住所有漏洞,成本不对称(3.9.5 的 think 框)。
失败判据:① 干净精度下降 → 减小对抗训练比例或降低 ;② 攻击预算升到 或换成 范数后成功率没有明显上升 → 先怀疑评测脚本有 bug(正常鲁棒性都是相对的,换预算应当变弱);③ 自适应攻击(攻击者知道你的预处理 / 多尺度检查)下成功率下降幅度 → 说明该防御当前不成立,不能写进验收报告。
(c)/(d) 验证计划:① 固定攻击脚本与随机种子,报告攻击成功率、 与平均扰动、置信度变化、扰动可视化(把 放大若干倍显示,让评审自己判断可见性——3.10.4 要求三件事缺一不可);② 迁移测试:在模型 A 上生成的扰动去攻击结构不同的模型 B,若成功率仍显著,说明这是模型无关的弱点;③ 分组评估:鲁棒性差距也要按子群报告,因为鲁棒性增益在不同子群上不均。
评分要点:定义带攻击设置 分;方案与预算分配合理(含「最便宜的系统侧纵深」) 分;失败判据可测量 分;验证计划含自适应攻击与迁移测试 分。