4、基于LSTM的时序建模:LSTM原理回顾、LSTM在Occupancy中的应用、时序特征提取、代码实战:LSTM-Occupancy模块
各位同学,欢迎来到第四章。
前面我们聊了单帧的Occupancy预测,说白了就是给一张图,模型猜一下当前时刻的占据情况。但自动驾驶是在动的,车在动,障碍物也在动。你想想看,如果只看当前帧,你根本不知道前面那辆车下一秒是刹车还是加速。所以,时序建模就变得非常关键。
这一章,我们来啃LSTM。别怕,这东西虽然名字听着唬人,但核心思想其实很朴素。我会带着大家从原理回顾开始,然后落到Occupancy场景里,最后手写一个LSTM-Occupancy模块。
4.1 LSTM原理回顾:别被门控吓到
先问大家一个问题:为什么普通的RNN搞不定长序列?
原因很简单——梯度消失。序列一长,早期的信息就传不过来了。我在项目中遇到过,用普通RNN处理10帧以上的时序,效果基本就崩了。LSTM就是为了解决这个问题而生的。
LSTM的核心,是三个门和一个记忆细胞。我习惯这么理解:
- 遗忘门:决定要不要忘掉过去的信息。说白了,就是“该扔的扔”。
- 输入门:决定要不要把新信息写进记忆。就是“该记的记”。
- 输出门:决定当前时刻要输出什么。就是“该说的说”。
- 记忆细胞:一条高速公路,信息可以在上面畅通无阻地跑。
公式我就不抄书了,大家记住一个关键点:LSTM通过门控机制,让梯度可以沿着时间轴顺畅地回传。这就是它能处理长序列的根本原因。
重要理解: LSTM不是魔法,它只是给信息流修了一条“高速公路”,并在路口设置了红绿灯(门控)。
4.2 LSTM在Occupancy中的应用:为什么需要它?
好,原理讲完了。那LSTM怎么用到Occupancy里?
Occupancy预测的本质,是给每个体素打标签。单帧预测只能看到当前时刻的占据情况。但真实场景中,很多信息是时序相关的:
- 一个被遮挡的行人,前一帧还在,这一帧突然消失了。单帧模型会认为这里空着,但时序模型知道“这里可能有人”。
- 一辆车正在变道,单帧模型可能判断不准它的朝向,但连续几帧一看,轨迹就清晰了。
我记得有一次做夜间场景测试,单帧模型在遮挡严重的路口频繁误检。加上LSTM之后,误检率直接降了30%。为什么?因为LSTM学会了“记忆”——它知道前一帧那个位置有东西,这一帧即使被遮挡了,它也不会立刻判空。
具体到网络结构上,LSTM通常插在特征提取网络和预测头之间。流程是这样的:
- 对每一帧图像/点云,用Backbone提取特征(比如BEV特征)。
- 把连续N帧的特征,按时间顺序送入LSTM。
- LSTM输出融合了时序信息的特征。
- 用这个特征去做Occupancy预测。
我的经验: 帧数不是越多越好。我试过用20帧,效果反而比8帧差。因为场景变化太快,太早的信息反而成了噪声。一般8-12帧是个不错的起点。
4.3 时序特征提取:怎么把时间信息“揉”进去?
这里有个关键问题:LSTM的输入是什么?
你不能直接把原始图像塞进去。那样计算量太大了,而且LSTM处理不了高维空间信息。我建议的做法是:
- 先降维:用Backbone把每帧图像/点云压缩成一个特征向量,或者一个低分辨率的特征图。
- 再序列化:把连续N帧的特征,按时间顺序堆叠成一个序列。
- 送入LSTM:LSTM的每个时间步,输入的就是一帧的特征。
举个例子,假设你的BEV特征图是128x128x64的。你可以先做全局平均池化,得到一个64维的向量。然后把这个向量作为LSTM每个时间步的输入。当然,更精细的做法是保留空间结构,用ConvLSTM——这个我们后面会讲。
注意: 时序对齐很重要。如果帧率不稳定,或者有丢帧,LSTM的效果会大打折扣。我曾经因为这个踩过坑——训练时用的是30fps的数据,部署时相机降到了15fps,结果模型直接崩了。后来我加了时间戳编码,才解决了这个问题。
4.4 代码实战:LSTM-Occupancy模块
好了,理论讲完了,咱们直接上代码。下面是一个简单的LSTM-Occupancy模块实现。
import torch
import torch.nn as nn
class LSTMOccupancyModule(nn.Module):
def __init__(self, input_dim=64, hidden_dim=128, num_layers=2, num_classes=2):
super().__init__()
# LSTM层
self.lstm = nn.LSTM(
input_size=input_dim,
hidden_size=hidden_dim,
num_layers=num_layers,
batch_first=True,
dropout=0.2 if num_layers > 1 else 0
)
# 分类头
self.classifier = nn.Sequential(
nn.Linear(hidden_dim, 64),
nn.ReLU(),
nn.Linear(64, num_classes)
)
def forward(self, x):
# x: [batch, seq_len, input_dim]
# 输出: [batch, seq_len, num_classes]
lstm_out, (h_n, c_n) = self.lstm(x)
# 取最后一个时间步的输出
last_out = lstm_out[:, -1, :] # [batch, hidden_dim]
logits = self.classifier(last_out) # [batch, num_classes]
return logits
# 使用示例
if __name__ == "__main__":
model = LSTMOccupancyModule(input_dim=64, hidden_dim=128, num_layers=2)
dummy_input = torch.randn(4, 10, 64) # batch=4, seq_len=10, feat_dim=64
output = model(dummy_input)
print(f"输出形状: {output.shape}") # [4, 2]
这段代码很简单,但有几个点我想强调一下:
- batch_first=True:我习惯把batch放在第一维,这样更符合直觉。
- 取最后一个时间步:因为我们做的是当前帧的预测,所以只取最后一个时间步的输出。如果你要做未来帧预测,可以取所有时间步。
- dropout:多层LSTM容易过拟合,加一点dropout很有必要。
避坑指南: 我曾经在训练时忘记初始化LSTM的隐藏状态,结果模型一直不收敛。后来发现,PyTorch默认会帮你初始化,但如果你用多卡训练,记得把隐藏状态也搬到对应的设备上。
当然,实际项目中,我们很少直接用这么朴素的LSTM。更常见的做法是:
- ConvLSTM:保留空间结构,适合处理BEV特征图。
- 双向LSTM:如果场景允许,可以同时看过去和未来的信息。
- 注意力机制+LSTM:用注意力来增强LSTM对关键帧的感知。
但不管怎么变,核心思想是一样的:用LSTM把时间信息“揉”进特征里,让模型学会“记忆”。
好了,这一章就到这里。下一章我们会讲更高级的时序建模方法——Transformer在Occupancy中的应用。到时候你会发现,LSTM和Transformer其实有很多相通的地方。
公众号:蓝海资料掘金营,微信deep3321