# 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`: ```python 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`: ```python 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`: ```python 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`: ```python 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`: ```python 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` 注释掉的代码: ```python # 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`: ```python 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`: ```python hidden_size: int = 128 num_layers: int = 1 dropout: float = 0.0 ``` - 单层 GRU 表达能力有限,难以建模长时间依赖。 - 无 dropout,容易过拟合(特别是训练数据量不大时)。 ### 3.5 Head 输出层初始化过小 `model.py:122-123`: ```python 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 | 缺少时间编码 | 忽略帧间间隔变化 |