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

211 lines
8.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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//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`
```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 | 缺少时间编码 | 忽略帧间间隔变化 |