输出和预期对不上时,先别调权重、别重训。只要你能拿到同一批输入样本、能重复跑同一份推理配置、能读到每路模态的时间戳,就可以用两组对照实验把它切到「模态配对」或「时序窗口」其中一侧。做不到这三件事,后面所有判断都只是猜测,第一步应该先补上数据链路的可观测性。
判断顺序建议:先确认各模态的形状、单位、归一化是否一致,再确认时间轴是否真的按预期对齐,最后才动时序窗口参数。适用于能复跑同一批样本、能读到原始时间戳的场景;操作上固定权重、样本顺序和随机种子,每次只改一个变量;验证方式是比对同一绝对时间戳下输出偏差的起点与形态;风险边界是上游已丢帧或时间戳被覆盖时,单靠输出对照无法区分,必须先修数据链路。
固定同一批样本,先只改模态配对方式再跑一次
这一步的目标是把变量锁在「模态怎么配对」上。对照组设置建议这样:A 组保持当前配对规则(例如图像帧索引与本体状态索引同号),B 组只改配对规则,可选的改法有三种——按最近时间戳匹配、整体错开一帧、对缺失模态做前向填充。除配对方式以外,权重、样本顺序、随机种子、窗口长度、步长、推理精度、batch 全部冻结不动。两次跑的日志目录分开存放,别覆盖。
需要记录的输入字段建议至少包含:sample_id、每路模态的原始时间戳数组、对齐后的索引映射、对齐后张量的 shape 与 dtype、归一化均值方差、缺失掩码、padding 长度与填充方向。这些字段决定了后面能不能解释差异,而不只是看到差异。
输出差异的观察点:同一绝对时间戳下,输出首次出现明显偏差是在第几帧;偏差是恒定偏移,还是随步数累积变大;只看与某一模态对应的输出通道时,误差是否同样变大;多个样本是否在相同的相对位置出错。若换成最近时间戳配对后偏差明显变小,问题更偏模态对齐;若两种配对下偏差形态几乎一致,就先别在配对上继续花时间。
再固定模态配对,只改时序窗口长度与步长
第二轮把配对方式固定为上一轮里表现更合理的那种,只让窗口参数变化。相关参数包括:window_len、stride、窗口起点偏移、是否强制因果(只使用当前帧及之前)、不足窗口时是补零、重复末帧还是直接丢弃。
步长变化范围不用扫全空间,取两三个值就够看趋势:stride = 1(滑窗重叠)与 stride = window_len(不重叠)。窗口长度可在当前值的 0.5 倍和 2 倍之间取上下界探测,先看趋势再抠细节。改动时保持同一段连续数据、同一配对方式、同一权重。
输出前后对照方式:把同一段连续序列按不同窗口切分分别推理,再把输出按绝对时间戳拼回一条时间轴,观察拼接点附近是否有跳变。如果误差只在窗口边界处冒出来、窗口内部基本正常,问题更偏时序窗口;如果误差在窗口内部也均匀存在,窗口长度通常不是主因。
for window_len in [w0 // 2, w0, w0 * 2]:
for stride in [1, window_len]:
out = infer(model, sample, window_len=window_len, stride=stride,
pairing=PAIRING_FIXED, seed=SEED)
dump_meta(out, tag=f'w{window_len}_s{stride}')
在加载数据的位置打印每路模态的维度与时间戳
对齐是否真的发生,不能靠肉眼比对两个数组的首元素,要在数据加载函数里打印。建议打印的字段:每路模态的时间戳首值、末值、前若干个相邻差值、时间维长度、其余维度、dtype、数值范围,以及对齐后使用的索引数组前若干项。
期望值通常是:同一物理时刻的各路模态时间戳差值小于半个采样周期;相邻时间戳差值接近标称周期且没有跳变或回退;时间维长度等于当前设定的 window_len,或等于单样本序列长度(未切窗时)。
偏差示例:一路 30Hz、一路 100Hz 时,直接按索引一一配对会得到近似 3:1 的错位,短序列上看起来只是“稍微不准”,序列一长就累积成明显偏移;时间戳出现重复值或突然变小,通常说明上游丢帧或缓冲重排,这时无论怎么调窗口都救不回来。
for name, arr, ts in modals:
ts = np.asarray(ts)
print(name, 'shape', arr.shape, 'dtype', arr.dtype)
print(name, 'ts_head', ts[:5], 'dt_head', np.diff(ts)[:5])
print(name, 'range', float(arr.min()), float(arr.max()))
把两次对照结果并排放,判断问题更偏向哪一侧
把第一轮(只改配对)和第二轮(只改窗口)的输出各自对齐到绝对时间戳,做成同一张对照表再下判断。判断表建议按下面的对应关系来读:
| 观察到的现象 | 更偏向的原因 | 下一步动作 |
|---|---|---|
| 换配对规则后误差明显变小,换窗口几乎不变 | 模态对齐 | 固定最近时间戳匹配,回到数据链路检查采样率与丢帧 |
| 换窗口后边界跳变消失,换配对照旧 | 时序窗口 | 固定配对,调 window_len 与 stride,检查因果设置 |
| 两种配对都差,且误差随序列长度累积 | 更可能在对齐或数据单位 | 先查归一化、单位、坐标系,再查对齐 |
| 误差与配对和窗口都无关,固定出现在某些样本 | 样本或上游数据 | 单独复现该样本,检查该样本时间戳是否异常 |
排除顺序建议:形状与单位 → 归一化参数 → 时间戳对齐 → 窗口参数 → 模型本身。按这个顺序排,能避免在错误的前提上调参数。
如果两张对照表看不出差别,下一步可以按这个顺序再切一次:先把时间维压到 1(单帧或单步输入)。单步就错,基本可以排除时序窗口;单步正常,序列一长才开始错,时序方向的嫌疑更大。再把输入退化成单模态跑一遍:单模态也错,说明问题不在模态间的配对,而在该模态自身的预处理或该路输出的后处理。
用最小样本复现问题,并写下能复现的输入条件
最小样本的构成:截取一段连续数据,长度取两到三个窗口长度即可,至少包含两路模态,保留原始时间戳不重排;同时保留对齐后数组和索引映射两份。样本连同当时的配置一起存成一个目录,交给他人复核时只给这个目录。
复现脚本骨架如下,替换掉 load_sample 和 infer 即可接到你自己的推理入口:
def load_sample(path):
d = np.load(path, allow_pickle=True).item()
# d 至少含: frames, frame_ts, state, state_ts
return d
def run(cfg_path, sample_path):
cfg = json.load(open(cfg_path))
d = load_sample(sample_path)
report(d) # 打印维度与时间戳
out = infer(d, **cfg['infer']) # 配对方式与窗口参数从配置读入
save(out, cfg, tag=cfg['tag'])
return out
每次运行按固定格式记一条记录,方便并排比较:
{
"sample_id": "...",
"pairing": "index | nearest_ts | offset_-1",
"window_len": 0,
"stride": 0,
"causal": true,
"first_divergence_step": 0,
"error_shape": "constant | accumulating | boundary_only",
"notes": "..."
}
记录里 first_divergence_step 和 error_shape 是最有区分度的两项:前者说明偏差从哪一步开始,后者说明偏差是整体偏移还是局部跳变。把这两项连同配对方式和窗口参数一起写清楚,别人拿到样本和配置就能复跑,判断也才有依据。需要结合你的采样率、丢帧情况和上游时间戳质量确认具体阈值,本文不给统一数值。