DuoMatching 仓库跑通蒸馏脚本、先验一遍数据加载与分辨率切分

文章导读
把蒸馏脚本第一次跑通,建议按三段来验:数据能被读出来、分辨率与帧数切分没有改错维度、只跑一个 step 时损失是有限值。三段各有一个可以观察的通过标准,报错时先判断卡在哪一段,再决定动配置还是动数据,不要一上来调学习率和损失权重。下面的命令和片段都是通用骨架,字段名需要按 DuoMatching 仓库实际的 Dataset 和 collate 函数替换。
📋 目录
  1. Ⅰ 用一条命令确认数据目录结构与标注能被正常读取
  2. Ⅱ 单独跑数据加载器,打印一个 batch 的形状与取值范围
  3. Ⅲ 核对分辨率与帧数切分前后的张量维度
  4. Ⅳ 只跑一个 step,观察损失是否为有限值
  5. Ⅴ 把跑通所需的路径、切分与 batch 参数固化成一份配置
A A

把蒸馏脚本第一次跑通,建议按三段来验:数据能被读出来、分辨率与帧数切分没有改错维度、只跑一个 step 时损失是有限值。三段各有一个可以观察的通过标准,报错时先判断卡在哪一段,再决定动配置还是动数据,不要一上来调学习率和损失权重。下面的命令和片段都是通用骨架,字段名需要按 DuoMatching 仓库实际的 Dataset 和 collate 函数替换。

首次跑通 DuoMatching 蒸馏脚本,先不要碰超参。用一条命令确认数据目录与标注可读;单独跑 DataLoader 看 batch 的 shape、dtype 和取值范围;再核对分辨率与帧数切分前后的张量维度顺序;最后只跑一个 step 看损失是否有限。四步都过,说明配置主体正确,之后换数据只改配置文件里的路径与切分字段。任何一步没打印出预期值,就先停在原地定位,不要继续往下跑。

用一条命令确认数据目录结构与标注能被正常读取

路径类和格式类错误应该在训练启动前就挡掉。先按仓库约定摆出目录,下面只是示例,实际以仓库 README 或 dataset 代码里的相对路径为准:

DuoMatching/
  configs/
    distill_debug.yaml
  data/
    train/
      clip_0001/
        frames/000001.jpg 000002.jpg ...
        meta.json
    val/
  annotations/
    train_list.txt
  scripts/
    train_distill.py

在仓库根目录执行一段只做读取的脚本,不加载模型、不建 DataLoader:

import json, pathlib
root = pathlib.Path('data/train')
items = sorted(root.glob('*/meta.json'))
print('clips:', len(items))
print('first:', items[0] if items else None)
m = json.loads(items[0].read_text())
print('keys:', sorted(m.keys()))
print('frames_on_disk:', len(list((items[0].parent / 'frames').glob('*.jpg'))))

期望看到的第一条打印是 clips: N,N 是正整数并且与 data/train 下的 clip 目录数一致;first 打印出真实存在的 meta.json 路径;keys 里能看到帧数、分辨率一类的字段;frames_on_disk 与 meta.json 里记录的帧数对得上。如果 clips 打印 0,是路径写错或目录层级多了一层;如果 json.loads 抛异常,是标注里有 BOM、单引号或尾逗号这类非标准 JSON;如果 frames_on_disk 少于标注帧数,说明标注和帧文件不是同一批,先补齐再谈训练。

单独跑数据加载器,打印一个 batch 的形状与取值范围

这一步只验证归一化和类型转换。构造 dataset 和 dataloader 时把 batch_size 设成 2,num_workers 设成 0,避免多进程把报错吞掉:

DuoMatching 仓库跑通蒸馏脚本、先验一遍数据加载与分辨率切分
batch = next(iter(loader))
print('batch_type:', type(batch).__name__)
v = batch['video'] if isinstance(batch, dict) else batch[0]
print('shape:', tuple(v.shape), 'dtype:', v.dtype)
print('min:', float(v.min()), 'max:', float(v.max()))
print('mean:', float(v.float().mean()))

取值范围检查的做法是同时看 dtype 和 min/max:经过 ToTensor 加 Normalize 之后,dtype 通常是 float32,数值大致落在 -1 到 1 或 0 到 1 之间,具体取决于用的均值和方差;只要 min 或 max 出现 255、出现 nan 或 inf,就说明预处理链路有问题。常见异常与可能原因对照:

  • dtype 是 uint8:Normalize 没接上,或 transform 顺序写反了。
  • min 和 max 都是 0:读图返回了空张量或全零占位,先回到上一步确认文件路径。
  • 通道维不是 3:读到了 RGBA 或灰度图,需要显式 convert('RGB')。
  • max 接近 255:归一化缺失,或做了两次乘 255 的缩放。
  • 出现 nan 或 inf:Normalize 的 std 写成 0,或某处除法分母为 0。

核对分辨率与帧数切分前后的张量维度

切分最容易出错的地方不是切多大,而是切完之后的维度顺序。先打印切分前后的 shape,再确认每一步 reshape 或 permute 的语义:

print('before:', tuple(v.shape))          # 期望 [B, T, C, H, W]
patches = split_resolution(v, size=256, stride=192)
print('after:', tuple(patches.shape))
print('frames_before:', v.shape[1], 'frames_after:', patches.shape[1])

常用的写法是:切分前 [B, T, C, H, W],unfold 之后得到类似 [B, T, n_h, n_w, C, size, size] 的排布,再 reshape 并 permute 回 [B*T*n_h*n_w, C, size, size] 送进模型。帧数切分则是在 T 维上按固定窗口滑,切分后 T 变成窗口数乘以窗口长度,或者被聚合成一个维度,两种做法都合理,但必须和自己后续的 loss 计算对齐。断言写法建议把通道和帧数这两条先固定下来:

B, T, C, H, W = v.shape
assert C == 3, f'channel order wrong: {C}'
assert patches.shape[-3:] == (C, size, size), patches.shape
assert patches.shape[0] % (B * T) == 0, patches.shape

维度不符时的定位步骤:先把 T 固定成 1、只取一个样本,排除 batch 维干扰;再用 torch.arange 造一个已知数值的张量跑一次切分函数,看数值落在哪个位置,判断是 n_h 和 C 换了位还是 permute 少写了一维;确认函数本身没问题,再回到真实数据上跑。这一步通常比反复读模型代码快。

DuoMatching 仓库跑通蒸馏脚本、先验一遍数据加载与分辨率切分

只跑一个 step,观察损失是否为有限值

前面三步都过了,再启动训练循环。把配置压到最小:batch_size 设为 1 或 2,num_frames 设为 4,resolution 用单个尺度、不做多尺度混合,关闭数据增强,关闭混合精度,max_steps 或 epoch 上限设成 1,让循环在第一个 optimizer.step() 之后就能退出。损失打印放在反向前后的合适位置,至少打印总损失和各个分项:

loss, parts = model(batch)
print('step', i, 'loss', float(loss), {k: float(v) for k, v in parts.items()})
loss.backward()
optimizer.step()

出现 nan 或异常大值时,按这个顺序排查:先关掉混合精度,排除 fp16 溢出;再把 batch_size 降到 1、帧数降到 1,排除某个样本本身有问题;然后关掉增强,确认不是几何变换把坐标弄到图外;接着检查损失里有没有 log 或除以 std 的位置,分母为 0 是常见来源;最后确认教师模型的权重是否真的加载成功,加载失败时教师输出全是初始化值,蒸馏项会异常。每一步只改一个变量,改完重跑一个 step,看损失是否恢复成有限值。

把跑通所需的路径、切分与 batch 参数固化成一份配置

四步都过了之后,把这些参数收进一个 yaml,下次换数据只改这个文件。字段含义用注释写在旁边,方便别人接手时不用再翻代码:

data:
  root: data/train          # 数据集根目录,下挂各 clip
  ann: annotations/train_list.txt   # 样本清单
  num_frames: 4             # 采样帧数,与切分窗口保持一致
  resolution: [256, 256]    # 单块切分尺寸
  stride: 192               # 滑窗步长,需小于 resolution
batch:
  size: 2                   # 首跑先用小 batch
  workers: 0                # 首跑关多进程,方便看报错
train:
  max_steps: 1              # 首跑只验证一个 step
  amp: false                # 首跑关闭混合精度

复跑验证的方法是:换一份数据或改一个字段后,重新执行前面四步里的打印脚本,确认 clips 数量、batch 的 shape 与取值范围、切分后的维度、单步损失四项都还符合预期。一般建议在 yaml 里给 root 和 ann 留出命令行覆盖参数,调试时临时指向小数据集,验证通过后再改回配置,避免把临时路径提交进仓库。