- 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>
8.3 KiB
UZH-FPV 速度预测 — 问题分析
一、训练方案问题
1.1 归一化被注释掉,训练与评估不一致
transforms.py:142,153 中 NormalizeVelocity() 在 train 和 val 管线中均被注释。这意味着:
- 训练时模型直接回归原始速度值(
v_right约 -3~+3 m/s,v_forward约 0~+8 m/s),数值范围大,梯度尺度不稳定,收敛慢。 - 评估时
evaluate.py:58-61和benchmark/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-38 的 TRAIN_SCENES 包含 outdoor_forward_1 和 outdoor_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//2(50% 重叠)能大幅增加训练样本量。
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-90 中 ComputeTilt 计算的是 body up 向量(世界坐标系下的机体上方向量),维度 (3,),单位向量。
但 model.py 中 PoseMLP 的输入标注为 tilt,docstring 写的是 "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 缺少序列级损失监督
模型只对最后一帧计算 loss(target[:, -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 | 缺少时间编码 | 忽略帧间间隔变化 |