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通常插在特征提取网络和预测头之间。流程是这样的:

  1. 对每一帧图像/点云,用Backbone提取特征(比如BEV特征)。
  2. 把连续N帧的特征,按时间顺序送入LSTM。
  3. LSTM输出融合了时序信息的特征。
  4. 用这个特征去做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