Files
hexone2086 d1d3310543 feat: add TBPTT training with cross-batch hidden state carryover
- config: seq_len=128, batch_size=4 for long-sequence TBPTT
- dataset: create_tbptt_loader with non-overlapping windows, strict temporal order
- model: forward() accepts/exposes hidden state h; add step() for single-frame stateful inference
- train: carry detached hidden state across batches, reset at epoch boundary
- benchmark: fix model call for new (v_body, h) return signature

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-07-29 16:32:41 +08:00

8.3 KiB
Raw Permalink Blame History

UZH-FPV 速度预测 — 问题分析

一、训练方案问题

1.1 归一化被注释掉,训练与评估不一致

transforms.py:142,153NormalizeVelocity() 在 train 和 val 管线中均被注释。这意味着:

  • 训练时模型直接回归原始速度值(v_right 约 -3~+3 m/sv_forward 约 0~+8 m/s),数值范围大,梯度尺度不稳定,收敛慢。
  • 评估时 evaluate.py:58-61benchmark/evaluate.py:130-133 却假设输出是归一化的,做了 preds * std + mean 反归一化。如果模型输出的是原始速度,反归一化后结果完全错误。
  • 结论:要么启用 NormalizeVelocity() 并保持评估一致,要么去掉评估中的反归一化。当前状态是两边对不上。

1.2 验证集与测试集重叠

config.py:40-53

VAL_SCENES = ["indoor_forward_3"]
TEST_SCENES = ["indoor_forward_3"]

验证集和测试集都是同一个场景 indoor_forward_3。这意味着:

  • 早停选择的 checkpoint 已经在这个场景上过拟合了,测试指标无意义。
  • 无法衡量泛化能力。

1.3 训练集包含测试场景

config.py:31-38TRAIN_SCENES 包含 outdoor_forward_1outdoor_forward_5,而 TEST_SCENES 中也有它们(注释中)。虽然当前 TEST_SCENES 只写了 indoor_forward_3,但注释里残留的测试场景与训练集重叠,容易误用。

1.4 滑窗 stride = seq_len,无重叠

config.py:112

sliding_window_stride: int = 32

seq_len 也是 32,所以滑窗不重叠。对于 1000 帧的场景,只产生约 31 个序列(1000/32),数据利用率低。通常 stride=1(全重叠)或 stride=seq_len//250% 重叠)能大幅增加训练样本量。

1.5 训练时使用滑窗,评估时使用 stateful 逐帧推理,两者不一致

  • 训练model(events, tilt) 一次输入整个序列 (B, S, 1, H, W),GRU 内部状态在序列内传播,但序列间重置。
  • 评估model.step(events, tilt, h) 逐帧推理,手动维护 GRU hidden state 跨帧传播。
  • 两种模式下的 GRU 行为不同:训练时每个序列从零状态开始,评估时状态持续累积。如果训练时序列长度不够覆盖相关时间尺度,评估时累积的长程状态可能产生分布偏移。

1.6 损失函数只监督最后一帧

train.py:57-58

pred = model(events, tilt)               # (B, 2)
target_last = target[:, -1, :]           # (B, 2)
loss = criterion(pred, target_last)

序列中前 S-1 帧完全没有损失信号。GRU 只在最后一帧收到梯度,前序时间步的隐藏状态更新缺乏直接监督。这可能导致 GRU 的中间状态退化。

1.7 学习率调度器步长与 epoch 数不匹配

config.py:103,106

epochs: int = 1000
lr_scheduler_step: int = 30

StepLR 每 30 个 epoch 衰减一次 gamma=0.5。1000 epoch 内衰减约 33 次,最终学习率约为 1e-3 * 0.5^33 ≈ 1.16e-13,几乎为零。模型在后几百个 epoch 基本停止学习。


二、输入输出问题

2.1 输出维度命名不一致

多个地方对输出维度的命名互相矛盾:

位置 命名
model.py:95 docstring [v_forward, v_lateral]
model.py:134 docstring [v_forward, v_lateral]
model.py:172 docstring [v_right, v_forward]
config.py:86 [v_right, v_forward]
transforms.py:107 [v_right, v_forward]
evaluate.py:36 [v_right, v_forward]
AGENTS.md [v_right, v_forward]

model.py 的 docstring 写的是 [v_forward, v_lateral](前向、侧向),但实际代码和配置文件都使用 [v_right, v_forward](右向、前向)。docstring 与实现不一致,容易误导。

2.2 输入 tilt 的定义与使用存在歧义

transforms.py:77-90ComputeTilt 计算的是 body up 向量(世界坐标系下的机体上方向量),维度 (3,),单位向量。

model.py 中 PoseMLP 的输入标注为 tiltdocstring 写的是 "tilt rotation vector"。实际上输入是 body up 向量(编码 pitch/roll),不是 rotation vector(轴角表示)。命名和文档有误导性。

2.3 速度归一化统计量可能不准确

config.py:15-16

VELOCITY_MEAN = [-2.902497, 3.837231]
VELOCITY_STD  = [3.453774,  3.722085]

注释说 "computed from forward scenes only, 28363 frames"。但:

  • 归一化被注释掉了,这些统计量从未被使用。
  • 如果未来启用,需要确认这些统计量是否覆盖了所有训练场景的分布,特别是 45° 飞行场景(侧向速度更大)。

三、模型架构问题

3.1 CNN 编码器被禁用(输出全零)

model.py:138-139 注释掉的代码:

# cnn_feat = events.new_zeros(B, S, self.cnn.out_dim) # 全零替代

当前 CNN 实际在运行(cnn_feat = self.cnn(events)),但注释表明曾经尝试过禁用 CNN。如果 CNN 输出被置零,模型完全依赖 PoseMLP + GRU,相当于只用 tilt 信息预测速度,事件帧信息被丢弃。这与项目目标(从事件相机预测速度)矛盾。

3.2 CNN 架构对事件帧不友好

model.py:28-36 的 CNN 使用标准 Conv-BN-ReLU-Pool 结构,但事件帧是稀疏的(大部分像素为 0,只有边缘处为 ±1):

  • BatchNorm 在稀疏输入上统计量不稳定(均值和方差被大量零像素拉偏)。
  • MaxPool 对稀疏信号不友好:如果事件只占几个像素,Pool 窗口内最大值可能始终为 0。
  • 没有使用空洞卷积或更大的感受野来捕获事件的空间结构。

3.3 PoseMLP 容量可能不足

config.py:69-71

input_dim: int = 3
hidden_dim: int = 32
output_dim: int = 64

PoseMLP 只有 2 层线性层(3→32→64),隐藏层仅 32 维。对于编码 tilt 信息(pitch/roll 的非线性映射),容量可能偏低。

3.4 GRU 单层且无 dropout

config.py:77-79

hidden_size: int = 128
num_layers: int = 1
dropout: float = 0.0
  • 单层 GRU 表达能力有限,难以建模长时间依赖。
  • 无 dropout,容易过拟合(特别是训练数据量不大时)。

3.5 Head 输出层初始化过小

model.py:122-123

self.head[-1].weight.data.mul_(0.01)
self.head[-1].bias.data.zero_()

输出层权重缩小 100 倍,初始输出接近零。这有助于训练初期避免大梯度,但也意味着模型需要更多迭代才能"激活"输出层。如果训练 epoch 不够,模型可能一直输出接近零的值。

3.6 缺少序列级损失监督

模型只对最后一帧计算 losstarget[:, -1, :]),GRU 的中间时间步输出被完全忽略。可以添加辅助损失:

  • 每个时间步的预测与对应 GT 的损失(teacher forcing 风格)
  • 速度变化量的平滑性约束(相邻帧速度差的正则化)

3.7 没有位置编码或时间信息

模型输入不包含时间戳或帧间间隔信息。事件帧之间的时间间隔可能不均匀(实际 DAVIS 相机帧率有波动),但模型假设所有帧等间隔。


四、数据管线问题

4.1 SimulateEvents 跨 shard 状态残留(已知问题)

AGENTS.md 已记录:EventProcessor_prev_frame 在 shard 边界不重置,导致新 shard 第一帧的事件帧错误。

4.2 滑窗跨 shard 边界(已知问题)

AGENTS.md 已记录:滑窗不感知 shard 边界,序列可能跨越两个 shard。

4.3 验证集使用滑窗而非逐帧

dataset.py:139 中验证集也使用滑窗(stride=32),与评估时的逐帧 stateful 推理不一致。验证 loss 不能反映实际推理时的性能。


五、总结优先级

优先级 问题 影响
P0 归一化被注释 + 评估假设归一化 评估结果完全错误
P0 验证集 = 测试集 无法评估泛化
P1 CNN 可能被禁用 事件信息被丢弃
P1 只监督最后一帧 GRU 中间状态无梯度
P1 训练滑窗 vs 评估 stateful 不一致 训练/评估分布偏移
P2 输出维度命名混乱 代码可维护性差
P2 学习率衰减过快 后期训练无效
P2 滑窗无重叠 数据利用率低
P3 CNN 架构对稀疏事件不友好 特征提取效率低
P3 缺少时间编码 忽略帧间间隔变化