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>
This commit is contained in:
+210
@@ -0,0 +1,210 @@
|
||||
# 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 | 缺少时间编码 | 忽略帧间间隔变化 |
|
||||
Reference in New Issue
Block a user