Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.10 应用导读:Lab 2 人脸检测系统

前九节讲的是零件与原理,这一节把它们装成一台能跑的机器:Lab 2 要求你搭一条检测数据管线、训练一个能在一张图里定位并识别人脸的模型,再用对抗样本检验它的鲁棒性,并讨论隐私与偏见。

选人脸作为载体有三重理由:① 人脸是刚性、常见、尺度变化大的目标,正好暴露检测任务的核心困难(多尺度、类不平衡、定位精度);② 人脸携带生物特征,让 3.9 节的伦理议题不是抽象讨论而是可实验的对象;③ 人脸检测的经典方法(滑动窗口 + 级联分类器)与深度学习方法(单阶段密集预测)之间的演进,几乎就是本讲的浓缩版。

本节把理论逐条对到 Lab 上
本讲原理 Lab 2 中的对应实现 出处
卷积的局部性与权值共享 小 CNN 分类器(输入 32×3232\times32 窗口) 3.1
感受野与步幅 窗口大小 = 网络感受野;滑动步幅 = 检测密度 3.2.5
输出尺寸公式 窗口裁剪与多尺度图像金字塔 3.2.2
池化与下采样 分类器主干 3.3.1
两宗罪 / 归纳偏置 为什么分类器只能用窗口而不是整图 3.1
BN 与 Dropout 训练稳定性与过拟合控制 3.3.3、3.3.4
迁移学习 用预训练骨干替换从零训练 3.6
检测 = 定位 + 分类 滑动窗口检测的全部内容 3.7.1
IoU / NMS / mAP 匹配正负样本、去重、评估 3.7.2
对抗样本 对抗扰动实验 3.9.3
偏见与隐私 分群评估与数据治理讨论 3.9.1、3.9.4

3.10.1 检测器的基本结构:滑动窗口

定义 3.10.1 · 滑动窗口检测器

用一个固定尺寸 w×ww\times w 的窗口在图像上以步幅 ss 滑动(通常还在多个尺度上重复),对每个窗口用一个小型 CNN 分类器输出「窗口内是否含目标」的分数。最终把高分窗口通过 NMS 合并为检测结果。

设图像尺寸为 H×WH\times W、窗口 ww、步幅 ss,单尺度下的窗口数为

Nwin=(⌊H−ws⌋+1)×(⌊W−ws⌋+1).N_{\text{win}}=\Bigl(\bigl\lfloor\tfrac{H-w}{s}\bigr\rfloor+1\Bigr)\times\Bigl(\bigl\lfloor\tfrac{W-w}{s}\bigr\rfloor+1\Bigr).

滑动窗口为什么慢,又为什么仍然值得先理解

它慢得可以量化:224×224224\times224 图像、32×3232\times32 窗口、步幅 4,单尺度 492=240149^2=2401 个窗口;若用 5 个尺度,就是 12,00512{,}005 次独立前向。关键在于相邻窗口的输入几乎完全重叠——这些计算是重复的。

但它值得先理解,因为它把检测的本质摊开了:检测 = 在位置与尺度上搜索「分类器认为这里有目标吗」。理解了它,再看「共享卷积特征」的做法(现代检测器)就知道那只是把搜索共享了——卷积本身已经在所有位置滑动(3.2.8 的 im2col 视角),何必再手动循环。

Lab 2 的核心体验之一正是这个"慢":当你亲手跑完一轮推理、发现它需要几十秒时,「为什么要共享特征 / 要单阶段」这个动机就不需要别人解释了。

例 3.10.1 · 滑动窗口的数量与显存 / 时间估算

某实验用 256×256256\times256 输入、24×2424\times24 窗口、步幅 4、共 4 个尺度(缩放因子 1.0,0.8,0.6,0.41.0,0.8,0.6,0.4)。

(a)单尺度的窗口数是多少?(b)四个尺度总计多少次前向?若每张图用 3 个尺度,总数是多少?(c)设单次前向(批大小 1)耗时 0.80.8 ms,全部窗口逐次前向的总耗时是多少?如果改成批大小 64 一次前向,能否减少总时间?为什么?

解

(a) 单尺度

Nwin=(⌊256−244⌋+1)2=(58+1)2=592=3481.N_{\text{win}}=\Bigl(\bigl\lfloor\tfrac{256-24}{4}\bigr\rfloor+1\Bigr)^2=(58+1)^2=59^2=3481 .

(b) 尺度 1.01.0 时 34813481 个窗口。缩放后图像尺寸变小,窗口数随之减少:

缩放因子 图像边长 窗口数
1.01.0 256 592=348159^2=3481
0.80.8 205 462=211646^2=2116
0.60.6 154 332=108933^2=1089
0.40.4 102 202=40020^2=400

四个尺度合计 3481+2116+1089+400=70863481+2116+1089+400=7086 次前向;只用 3 个尺度(1.0,0.8,0.61.0,0.8,0.6)则为 3481+2116+1089=66863481+2116+1089=6686 次。

(c) 逐次前向:7086×0.8 ms≈5.67 s7086\times0.8\ \text{ms}\approx\mathbf{5.67\ s}(3 个尺度时 5.35 s5.35\ \text{s})。

批处理能显著加速(因为单次前向在 GPU 上远未打满算力,瓶颈是 kernel 启动开销与访存),但不能消除根本问题:总计算量(FLOPs)与窗口数成正比。批处理只是把"每个窗口单独一次前向"的低效率摊销掉,没有减少重复计算——相邻窗口仍然各自完整地卷了一遍自己那一小块。

这正是「共享特征」要解决的:如果整图先过一次卷积得到特征图,那么在特征图的每个位置上"顺便"评估一个窗口,成本就与研究区域的规模同量级,而不是与窗口数量 × 窗口面积同量级。数量级差异就在这里。

∎

3.10.2 数据管线:从图像与标注到训练样本

检测数据管线的五步
1. 读入与归一化
   - 读取图像 + 一组框(标注常为 (x1, y1, x2, y2) 绝对坐标)
   - 像素缩放到 [0,1] 或按通道做 z-score(与骨干预训练一致,[3.3.5](sec33.md#sec-3.3.5))
   - 检查越界框:坐标必须裁剪到图像范围内,否则裁剪会引入形状错误的 patch
2. 坐标表示的选择与转换(最容易出 bug 的一步)
   - 角点式 (x1,y1,x2,y2):便于裁剪与 IoU 计算,但宽高可能为非法负值
   - 中心式 (cx,cy,w,h):便于回归(宽度变化时中心稳定),但需从角点式转换
   - 转换公式:
       cx = (x1+x2)/2,  cy = (y1+y2)/2,  w = x2-x1,  h = y2-y1
       x1 = cx - w/2,   y1 = cy - h/2,   x2 = cx + w/2,  y2 = cy + h/2
   - 断言检查:转换后再转回去,数值应完全相等(浮点误差 < 1e-6)
3. 多尺度 / 多位置采样:把"图 + 框"变成"窗口 + 标签"
   - 对每个窗口计算它与所有真值框的 IoU
   - IoU ≥ 正阈值(如 0.5)→ 正样本(含目标);IoU < 负阈值(如 0.3)→ 负样本(背景)
   - 处于中间区间(0.3–0.5)的样本通常被丢弃("忽略区"),避免给模型错误的监督信号
4. 类别不平衡处理(检测任务的核心难点)
   - 背景窗口远多于目标窗口(可到 100:1 以上)
   - 做法:随机负样本采样(只取一部分负样本)、难例挖掘(hard negative mining:
     优先保留被模型误判为正的负样本)、或用 focal loss 让难样本主导梯度
5. 数据增强(对检测要同时变换图像与框!)
   - 几何:水平翻转(框的 x 坐标镜像)、随机裁剪 / 缩放、随机平移
   - 光度:亮度 / 对比度 / 色彩抖动
   - 常见 bug:只增强图像不增强框 → 标签错位,模型学到"随机噪声"
   - 断言检查:在增强后的图上画出框并可视化检查(这一步几乎总能发现管线 bug)
为什么「忽略区」是必要的设计,而不是偷懒

一个窗口与真值框的 IoU 是 0.40.4 时,它到底算不算"含目标"?

  • 若判为正样本:模型被要求在一个明显偏移的窗口上说"有人脸",这会给定位分支注入错误监督,学习到的边界会变糊。
  • 若判为负样本:模型被要求在一个确实覆盖了目标大部分的窗口上说"没有",同样错误。

两种标注都不对,所以标准做法是把它从训练中排除(在损失上给 0 权重)。这不是偷懒,而是承认"这个样本的标签本身有歧义"——在标注有噪声的场景里,承认不确定性比强行二值化更安全。这与 3.9 节的原则一致:模型能表达的东西受监督信号质量限制。

代价:忽略区浪费了一部分样本。因此有些工作用软标签(按 IoU 连续加权)来利用这些样本,而不是直接丢弃。

例 3.10.2 · 匹配正负样本的手算

图像中有一个真值人脸框 G=(40,40,120,120)G=(40,40,120,120)(边长为 8080)。现在有四个候选窗口:

A=(44,44,116,116),B=(80,80,152,152),C=(0,0,40,40),D=(40,32,124,132).A=(44,44,116,116),\quad B=(80,80,152,152),\quad C=(0,0,40,40),\quad D=(40,32,124,132).

设正阈值 0.50.5、负阈值 0.30.3。分类每个候选窗口(正 / 负 / 忽略),并指出哪个候选最适合用来训练定位(框回归)分支。

解

面积:GG 的面积为 80×80=640080\times80=6400。

窗口 A (44,44,116,116)(44,44,116,116):宽高均 7272,面积 722=518472^2=5184。 交:x∈[44,116]x\in[44,116]、y∈[44,116]y\in[44,116] → 72×72=518472\times72=5184(A 完全落在 G 内)。 并 =6400+5184−5184=6400=6400+5184-5184=6400。IoU=5184/6400=0.81\mathrm{IoU}=5184/6400=\mathbf{0.81} → 正样本

窗口 B (80,80,152,152)(80,80,152,152):宽高均 7272,面积 51845184。 交:x∈[80,120]x\in[80,120]、y∈[80,120]y\in[80,120] → 40×40=160040\times40=1600。 并 =6400+5184−1600=9984=6400+5184-1600=9984。IoU=1600/9984=0.160\mathrm{IoU}=1600/9984=\mathbf{0.160} → 负样本

窗口 C (0,0,40,40)(0,0,40,40):与 GG 只在角点相接,交集面积 00(按坐标差计算,边界不算入)→ IoU=0\mathrm{IoU}=0 → 负样本

窗口 D (40,32,124,132)(40,32,124,132):宽 8484、高 100100,面积 84008400。 交:x∈[40,120]x\in[40,120](宽 8080)、y∈[40,120]y\in[40,120](高 8080)→ 64006400。 并 =6400+8400−6400=8400=6400+8400-6400=8400。IoU=6400/8400=0.762\mathrm{IoU}=6400/8400=\mathbf{0.762} → 正样本

结论与选择:

窗口 IoU 分类
A 0.81 正样本
B 0.16 负样本
C 0.00 负样本
D 0.76 正样本

用于训练定位(框回归)分支的最佳候选是 A:A 与真值形状完全一致(均为 80×8080\times80),只是位置有 44 像素偏移——这是一个"干净"的回归目标(模型要学的正是"往左上/右下各挪一点"这种小修正)。相比之下 D 的宽高与真值不同(84×10084\times100 对 80×8080\times80),回归目标同时包含尺度与长宽比的修正,虽然 IoU 也够高,但它的"偏移分布"更宽、更难学。

一条实用的工程经验:正样本的 IoU 排行经常被用作损失的加权依据(IoU 越高的样本对回归贡献越大),或者用"质量感知"的损失(如 IoU-aware 的分数预测)来避免"高分低质量框"——即在训练时就把"置信度"与"定位质量"绑定起来,而不是事后靠 NMS 挑。

∎
import torch, torch.nn.functional as F

# 滑动窗口推理的最小骨架:批量化 + 坐标还原(Lab 2 的核心循环)
def sliding_window_detect(model, img, win=32, stride=4, scale=1.0, batch=256, thresh=0.5):
    """返回原图坐标系下的 (x1, y1, x2, y2, score) 列表。"""
    model.eval()                                     # 别忘了!BN 统计量问题见 3.3.3
    H0, W0 = img.shape[-2:]
    if scale != 1.0:                                 # 多尺度:先缩放图像
        img_s = F.interpolate(img[None], scale_factor=scale, mode="bilinear",
                              align_corners=False)[0]
    else:
        img_s = img
    H, W = img_s.shape[-2:]
    wins, coords = [], []                            # 窗口张量 + 其在**缩放图**中的左上角
    for y in range(0, H - win + 1, stride):
        for x in range(0, W - win + 1, stride):
            wins.append(img_s[:, y:y + win, x:x + win])
            coords.append((x, y))
    if not wins:
        return []
    scores, boxes = [], []
    for i in range(0, len(wins), batch):             # 批量化:把上万次前向压成几十次
        chunk = torch.stack(wins[i:i + batch])       # (B, C, win, win)
        with torch.no_grad():                        # 推理不要建图,省显存
            p = torch.sigmoid(model(chunk)).squeeze(1)
        scores.append(p.cpu()); boxes.append(torch.tensor(coords[i:i + batch]))
    scores, boxes = torch.cat(scores), torch.cat(boxes)
    keep = scores > thresh
    out = []
    for (x, y), p in zip(boxes[keep].tolist(), scores[keep].tolist()):
        # 关键:把**缩放图**坐标除以 scale 还原到**原图**坐标(3.10.5 的第 5 条 bug)
        x1, y1 = x / scale, y / scale
        x2, y2 = (x + win) / scale, (y + win) / scale
        out.append((x1, y1, min(x2, W0), min(y2, H0), p))
    return nms(out, iou_thr=0.5)                     # 去重(见下)
def iou(a, b):
    """连续坐标约定:面积用 x2 - x1(不加 1)。越界请先裁剪到图像范围内。"""
    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)     # 不相交时为 0,不要取负
    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

def nms(dets, iou_thr=0.5):
    """dets: [(x1,y1,x2,y2,score)]。注意比较方向:IoU **大于**阈值才抑制。"""
    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) <= iou_thr]   # 方向写反 = 删掉所有框
    return kept
这段代码里的三个断言,正是检测调试的着力点
  1. model.eval():BN / Dropout 在训练与推理模式下行为不同(3.3.3、3.3.4)。漏掉它,单张图推理会得到与训练时完全不同的结果。
  2. x / scale:多尺度推理时,模型看到的是缩放后的图像,坐标必须先还原。漏掉它,多尺度的框会全部错位(且不会报错)——这是最隐蔽的一类 bug。
  3. iou(best, d) <= iou_thr:NMS 的方向。写反的后果是「所有框都被删掉,只剩下第一个」,表现为"检测器只输出一个框"。

为什么要把这三条写成断言:检测管线的错误大多不会抛异常——形状对、类型对、数值在合法范围内,只是语义错了。这类错误只能靠显式的断言与可视化检查抓出来。在训练循环里补三行断言(坐标范围、IoU 在 [0,1][0,1]、标签分布),通常比多训几个 epoch 更有价值。

3.10.3 训练:损失、稳定性与评估

Lab 2 的训练清单
模型
  - 小型 CNN 分类器(若干卷积 + BN + ReLU + 池化 + 全局池化 + 线性头)
  - 若允许使用预训练骨干([3.6](sec36.md)):优先用,收敛快且更稳
损失
  - 分类:二元交叉熵(是否为目标)
  - 若做框回归:分类损失 + λ · 回归损失(常为平滑 L1 / IoU 系损失)
  - 类别不平衡:正负样本加权,或 focal loss
优化
  - AdamW,学习率 1e-3 量级;batch 大小按显存取
  - 先用较小的学习率跑通 1 个 epoch,确认 loss 下降再放大
  - 早停:以验证集指标(precision / recall / F1)为准,不看训练 loss 单值
训练稳定性自查(按顺序排查)
  1. 第一步 loss 是否 ≈ -log(正样本比例)(全判背景的基线)?不是 → 数据管线有 bug
  2. 有没有断言检查张量形状与取值范围?没有 → 先加
  3. 可视化 32 个训练样本(图 + 标签)→ 标签错位一眼可见
  4. 过拟合 10 个样本能否达到接近 100% 的准确率?不能 → 模型或损失有问题(健壮性测试)
  5. 再谈调参
评估
  - 不看准确率([3.7.2](sec37.md#sec-3.7.2) 的教训):用 precision / recall / F1,
    或按 IoU 阈值计算 AP
  - 报告"漏检"与"误检"分开计数,不要只报一个综合分
  - 多尺度推理时报告不同尺度的表现(小目标通常最差)
  - 固定随机种子,报告多次运行的均值与方差(小数据下随机性极大)
「先过拟合 10 个样本」为什么是最有效的调试手段

这个做法的价值在于把"能不能学"与"数据够不够"这两件事分开。

如果模型连 10 个样本都拟合不了,那问题一定在实现层面(形状错、损失方向错、标签错、梯度没回传),而不是"数据量不足"或"模型不够大"。反过来,如果 10 个样本能完美拟合但验证集表现差,问题才在泛化(第 1 讲 1.7 节)。先做这个测试,可以避免在错误的方向上调几天参数。

对检测任务,这个测试还有一个附加用途:确认框回归的分支确实在学(可以打印预测框与真值框的坐标,观察它们是否在靠近)。只分类不回归,或回归目标被写错符号,都是常见实现错误。

Lab 2 实施中的六个高频 bug
  1. 框的坐标顺序错(把 (x1,y1,x2,y2)(x_1,y_1,x_2,y_2) 当成 (x,y,w,h)(x,y,w,h)):不会报错,只会让所有标签错位,表现为"训练 loss 能降但检测框乱飞"。用断言验证 round-trip 转换。
  2. 增强只改图像不改框:这是检测特有的 bug(分类任务不会有)。在增强后的图上可视化框是最快的检查方式。
  3. IoU 计算把边界算进了面积:应该用 x2−x1x_2-x_1 而不是 x2−x1+1x_2-x_1+1(连续坐标约定),否则 IoU 会有几个百分点的偏差,在阈值附近改变正负样本划分。
  4. NMS 的阈值方向搞反:抑制条件是「与最高分框的 IoU 大于阈值」(而不是小于);写反会把所有不重叠的框都删掉。
  5. 多尺度推理时忘记把坐标映射回原图:每个尺度上得到的框坐标是缩放后图像的坐标,必须除以缩放因子才能还原。忘了这一步,多尺度的结果会完全错位。
  6. 忘了 model.eval():BN 仍按当前批统计(3.3.3),单张图推理时统计量退化,结果与训练时完全不同。这是最容易被忽略的一行。

3.10.4 对抗样本实验:把 3.9.3 节落到代码

实验步骤:用 FGSM / PGD 攻击自己的人脸检测器
目标:让分类器把"含人脸"的窗口判成"不含"(或反过来),并量化扰动幅度。
1. 取一张测试图的一个正样本窗口 x,标签 y=1
2. 打开梯度:requires_grad(注意:此时不是训练,但需要梯度)
3. 计算损失 L = BCE(f(x), y)
4. 求 ∂L/∂x(对**输入**求导,而不是对参数)
5. FGSM:x' = clamp(x + ε·sign(∂L/∂x), 0, 1)
   注意 clamp 到合法像素范围——否则会得到"物理上不存在"的图像
6. 用 x' 前向:观察输出的置信度从 0.x 掉到多少
7. 记录指标:
   - 攻击成功率(多少窗口被成功翻转到错误标签)
   - 扰动幅度:‖δ‖_∞ = max|x'-x| 与平均绝对扰动
   - 人眼可见性:把 x'-x 放大显示(对比原图)
8. 进阶:PGD(多步迭代 + 每步投影回 ε 球内),比较它比 FGSM 强多少
9. 迁移测试:在模型 A 上生成的扰动,拿去攻击模型 B(结构不同的同类分类器)
   —— 若成功率仍显著,说明这是"模型无关"的弱点,而不是 A 的特有 bug

必须同时报告的三件事,缺一不可:① 扰动幅度(ℓ∞\ell_\infty 与平均幅度);② 攻击前后的置信度变化;③ 扰动可视化(把 δ\delta 放大若干倍显示,让读者自己判断"是否可见")。

为什么这个实验比"读懂对抗样本的定义"有价值得多

因为它会让三件抽象的事变成可观察的事实:

  1. "不可察觉"是可以被检验的。当你把 8/2558/255 的噪声放大 32 倍显示时,你会亲眼看到它是有结构的、类似纹理的图案,而不是随机噪声——因为它是沿梯度方向的、针对该模型的特定模式。这和"加高斯白噪声"完全不同。
  2. 模型的行为跃变是突然的。置信度可能从 0.990.99 直接掉到 0.010.01,而不是平滑下降——这正是"决策边界离数据点很近"的可视化证据。
  3. 防御不是显然可行的。你可以立刻尝试 3.9.3 里的简单防御(JPEG 压缩、去噪、随机缩放),然后发现:在放大攻击(自适应)下它们常常失效。

最后这一点是本实验最重要的输出:它让人从"我知道有对抗样本"变成"我理解为什么鲁棒性这么难"。这与第 1 讲"把反直觉的结论亲手验算一遍"的教学逻辑一致。

3.10.5 常见失败模式与排查表

Lab 2 故障排查表
症状 最可能的原因 检查方法
训练 loss 不下降 学习率过大 / 标签与预测顺序错 / 梯度未回传 先过拟合 10 个样本;打印 loss 与输入标签
loss 下降但检测框乱飞 框坐标顺序或表示弄错(角点 vs 中心) round-trip 断言;可视化预测框与真值框的叠加图
检测框大量重复 NMS 没做 / 阈值过大 / NMS 条件写反 打印 NMS 前后框数;检查 IoU 与阈值比较方向
大量漏检 正负阈值设得太严 / 负样本采样过多 / 尺度覆盖不全 统计训练集中正样本数量;分别报告各尺度的召回率
大量误检 负样本不足(尤其缺"难负样本")/ 阈值过低 查看误检样本内容(是否都是纹理强烈的背景?)
小目标检测不到 感受野 / 尺度不匹配;多尺度没覆盖 按目标尺寸分组统计召回率
验证指标远低于训练 数据泄漏(同一人的多张照片跨划分)或过拟合 检查划分是否按"人"而非按"图"划分
单张推理结果异常 忘了 model.eval()(BN 统计量问题) 切换 eval 后再测
多尺度结果错位 忘记把坐标映射回原图 打印各尺度输出的坐标范围并与图像尺寸对照
结果不可复现 随机种子 / 数据顺序 / 增强随机性未固定 固定种子,记录版本与超参(工具页)
从 Lab 2 走向真实系统:还需要什么

Lab 2 完成后,你会得到一个能框人脸的模型。但把它变成"系统"还差几件东西——这些恰恰是本讲后面几节讨论的:

  1. 分群评估(3.9.1):Lab 的人脸数据能按什么维度分群?如果分不出来,说明数据本身缺乏代表性,这本身就是一个需要记录的事实(也对应 3.10.6 的实验设计)。
  2. 不确定时拒答:给置信度设下限,低于它就不输出框。这比"输出所有框再让人挑"更诚实。
  3. 隐私约束(3.9.4):原始人脸图像是否需要保留?能否只留特征模板?EXIF 是否被清理?
  4. 性能与延迟预算:滑动窗口的 12,005 次前向在真实场景不可接受;真实的替换路径是:共享特征 → 单阶段检测 → 骨干替换(3.7.3)。
  5. 失败模式的可观测性:记录被拒样本、低置信样本、以及"框特别大 / 特别小"的异常输出——真实系统的改进往往来自对这些日志的分析,而不是重新训练。

一句话:Lab 2 教的不是"人脸检测",而是"把一个视觉任务从原理做到能跑、再到能负责"的完整流程。后面缺的那几件东西,正是把"能跑"变成"能负责"的部分。

3.10.6 把伦理讨论落到代码:三个可做的实验

三个可执行的小实验(每个 30–60 分钟)
实验 A · 分群评估
  1. 把验证集按"光照条件 / 性别 / 是否戴眼镜 / 图像来源"分组(能分出什么就用什么)
  2. 对每个子群分别计算 precision / recall / F1
  3. 画出各组指标对比图,并标注每组的样本量
  4. 关键:样本量过小的组要标注"结论不可靠",不要把它当成发现
  → 对应 [3.9.1](sec39.md#sec-3.9.1)
实验 B · 阈值与代价
  1. 画出调节置信度阈值时的 precision-recall 曲线
  2. 为两个不同场景各选一个工作点:
     (i) 门禁(误识代价高)→ 高阈值、高 precision;
     (ii) 相册自动整理(漏检代价高)→ 低阈值、高 recall
  3. 说明"同一个模型在两个场景应该用不同阈值"——阈值是业务决策,不是模型属性
  → 对应 [3.9.2](sec39.md#sec-3.9.2)
实验 C · 对抗与隐私
  1. FGSM 攻击([3.10.4](#sec-3.10.4))
  2. 隐私检查:读取原始图像的 EXIF,列出其中可识别个人信息(GPS、设备、时间)
  3. 讨论题(写进报告):这段代码如果部署到公共场所,应当增加哪些默认约束?
  → 对应 [3.9.3](sec39.md#sec-3.9.3) 与 [3.9.4](sec39.md#sec-3.9.4)

报告要求(这三条比结果本身更重要):

  1. 报告"没做什么":没调参、没做数据清洗、样本量不足的部分都要明说——避免把"未评估"包装成"没问题"。
  2. 区分"观测"与"解释":例如"子群 B 的召回率低 88 个百分点"是观测;"因为该子群样本少"是解释——解释需要额外证据(如样本量统计、混淆因素排查)。
  3. 给出失败判据:明确说明"在什么条件下这个模型不应该被使用"。这与 3.9.5 的出口判据一致——一个负责任的实验报告应该包含使用边界的陈述。
即时自测 · 3.10

Q1 128×128128\times128 图像、16×1616\times16 窗口、步幅 8,单尺度有多少个窗口?若用 3 个尺度(1.0,0.75,0.51.0,0.75,0.5),总计多少次前向?

Q2 真值框 (20,20,80,80)(20,20,80,80),候选窗口 (24,24,76,76)(24,24,76,76) 与 (20,20,100,100)(20,20,100,100),分别求 IoU 并判断是否为正样本(正阈值 0.50.5)。

Q3 为什么检测任务需要「忽略区」(IoU 在正负阈值之间的样本)?不用会怎样?

Q4 你训练时 loss 正常下降,但推理时每个窗口的分数几乎都相同、框的位置也几乎不动。列出最先应该检查的两件事。

Q5 用 FGSM 攻击自己的检测器时,为什么要 clamp(x', 0, 1)?不 clamp 会得到什么?

参考要点

Q1 单尺度 (⌊(128−16)/8⌋+1)2=152=225\bigl(\bigl\lfloor(128-16)/8\bigr\rfloor+1\Bigr)^2=15^2=225 个窗口。3 个尺度:1.01.0 时 225225;0.750.75 时边长为 9696,(⌊(96−16)/8⌋+1)2=112=121\bigl(\bigl\lfloor(96-16)/8\bigr\rfloor+1\Bigr)^{2}=11^{2}=121;0.50.5 时边长为 6464,(⌊(64−16)/8⌋+1)2=72=49\bigl(\bigl\lfloor(64-16)/8\bigr\rfloor+1\Bigr)^{2}=7^{2}=49。合计 225+121+49=395225+121+49=395 次前向。

Q2 候选 1:宽高均 5252,面积 27042704;交 == 52×52=270452\times52=2704(完全在真值内);并 =3600+2704−2704=3600=3600+2704-2704=3600;IoU=2704/3600=0.751\mathrm{IoU}=2704/3600=0.751 → 正样本。候选 2:宽高均 8080、面积 64006400;交 =60×60=3600=60\times60=3600;并 =3600+6400−3600=6400=3600+6400-3600=6400;IoU=3600/6400=0.5625\mathrm{IoU}=3600/6400=0.5625 → 正样本(刚过阈值)。注意两者都为正,但候选 1 更"干净"(形状与真值一致、只是位置偏移)。

Q3 中间 IoU 的窗口语义上有歧义:判正会教模型在被平移的窗口上说"有人脸"(边界变糊),判负会教它在覆盖了目标大部分的窗口上说"没有"(同样错误)。所以标准做法是用权重 0 排除它们。不设忽略区,模型会收到自相矛盾的监督,表现为置信度校准差(同类目标分数高低与 IoU 不相关)、边界不稳。

Q4 优先检查:① 忘了 model.eval()(BN 在批大小 1 时统计退化,3.3.3);② 框的坐标表示或标签错位(例如把中心式当角点式用),用 round-trip 断言与可视化检查;其次才考虑学习率 / 数据不足。

Q5 clamp 保证生成的仍然是合法的图像张量(像素在 [0,1][0,1] 之间)。不 clamp 会得到超出像素范围的"图像"——它在数值上可以让损失降得更低(离决策边界更远),但不能被真正显示或馈入后续的图像预处理,实验结论也就失去了"人眼不可察觉"这一前提。

延伸阅读
  • MIT 6.S191 的 Lab 2(人脸检测与对抗样本,仓库 github.com/aamini/introtodeeplearning)——本 Lab 的母本任务;引用时注明所对照的版本与公开材料。
  • 本教程 Lab 2 · 人脸检测系统——任务书与验收标准(本节是其理论导读)。
  • 对抗样本:3.9.3 的延伸阅读(FGSM arXiv:1412.6572、PGD arXiv:1706.07334)。
  • 公平性评估:Buolamwini & Gebru, Gender Shades(2018,FAT*);数据与模型的文档化:Datasheets for Datasets(arXiv:1803.09010)、Model Cards for Model Reporting(arXiv:1810.03993)。
  • 检测器评估口径:3.7.2(IoU / NMS / AP 与 COCO 口径)。
  • 工程调试:工程工具箱(种子、日志、版本管理、显存估算)。

本节小结:Lab 2 把本讲的零件装成一台机器——滑动窗口把分类器变成检测器(并暴露出“重复计算“这个动机,例 3.10.1);数据管线的三个关键点是框的坐标表示与 round-trip 断言、IoU 阈值与忽略区、以及图像与框必须同步增强(例 3.10.2);训练上「先过拟合 10 个样本」是最有效的调试手段;对抗样本实验的价值不在于复现攻击,而在于亲手验证「不可察觉」「行为跃变」「防御不易」这三件事;最后,把伦理讨论落到分群评估、阈值与业务代价、隐私检查这三个可执行的小实验上。做完这一步,你就把第 3 讲从“读懂了“推进到了“能负责“。