CVPR 2024 | LMDrive:使用大语言模型的闭环端到端自动驾驶 模块实现

文摘   2024-07-25 08:30   上海  
arxiv:https://arxiv.org/abs/2312.07488
projection:https://github.com/opendilab/LMDrive

本期概述

喽大家好,上期我们重点分享了LMDrive模型的框架,数据流,训练方法,数据集以及实验部分,空降来的宝子们强烈推荐首先熟悉!CVPR 2024 | LMDrive:使用大语言模型的闭环端到端自动驾驶 Pipeline详解

LMDrive是一个语言引导、端到端的闭环自动驾驶框架。LMDrive处理并集成多模态传感器数据与自然语言指令,在现实指令设置中与人类和导航软件进行交互。小毛认为这个研究成果最新颖的地方在于使用ChatGPT生成指令?!我们来看看吧!

实施细节

复习:LMDrive模型的结构包括两个主要组件:1) 视觉编码器,用于处理多视角多模态传感器数据(摄像头和激光雷达),以实现场景理解并生成视觉标记;2) 大型语言模型及其相关组件(标记器、Q-Former和适配器),处理所有历史视觉标记和语言指令(导航指令和可选通知指令),以预测控制信号并判断指令是否完成。视觉编码器负责将传感器数据转换为视觉标记,大型语言模型则结合历史数据和当前指令,生成控制信号并检测指令完成情况。

视觉编码器:在视觉编码器部分,使用了1层编码器和3层解码器。特征提取方面,采用ResNet的第5阶段的特征作为提取的特征图,然后应用一个多层感知器(MLP)层将其维度转换为768,这是后续Q-Former的特征维度。参考LAV ,构建了一个简化版的PointNet,通过多个MLP层和批量归一化(Batch Normalization)层来编码LiDAR点云数据。对于Q-Former,使用了BLIP-2 的模型架构和预训练权重。在文章的工作中,输入到Q-Former的视觉token包括400个鸟瞰视图(BEV)token、5个未来航点token和1个交通信号灯token。

传感器配置:传感器配置方面,使用了一个前置摄像头、两个侧面摄像头和一个后置摄像头来收集RGB图像。每个摄像头的分辨率为800×600,水平视场角(FOV)为100°。两个侧面摄像头的角度为60°。对于前置图像,作者将前置摄像头输入的短边缩放到256,并裁剪其中心部分,得到224×224的图像补丁。对于焦点视图图像,直接裁剪前置摄像头输入的中心,得到一个128×128的图像补丁,可以捕捉远处的交通信号灯状态。对于其他图像,将摄像头输入的短边缩放到160,然后裁剪中心部分,得到128×129的图像补丁。LiDAR传感器的旋转频率为10Hz,上/下视场角为10/-30,通道数为64。

训练细节:在训练细节部分,首先介绍了视觉编码器预训练的细节。采用了AdamW优化器和余弦学习率调度器进行训练。对于transformer编码器和3D骨干网,初始学习率设置为BatchSize/512×5e-4;对于2D骨干网,初始学习率为BatchSize/512×2e-4,因为2D骨干网初始化使用了ImageNet的预训练权重。训练模型35个epoch,前5个epoch用于预热。为了增强收集到的RGB图像的数据,进行了0.9到1.1的随机缩放和颜色抖动。

指令微调接下来介绍了指令微调阶段的训练细节。同样采用了余弦学习率调度器,初始学习率为1e-4,batch size为32。训练模型15个epoch,前2000个迭代步骤用于预热阶段。权重衰减设置为0.07。最大历史时间跨度(Tmax)设置为40,如果帧数超过40,将截断数据片段以保留最近40帧。

Q-Former实现细节

本期文章在模块的实现上其实并没有太多创新点,其中,多模态模型采用了BLIP-2论文提出的框架Q-Former:[2301.12597v3] BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models。我们直接对Q-Former进行介绍:

对应的代码库为:

https://github.com/salesforce/LAVIS/tree/main/projects/blip2。

Q-Former包括两个Transformer。左侧Transformer的输入是查询(Queries)和预训练图像Encoder生成的图像编码。其中Queries是一些随机初始化的向量,目的是用来和图像进行corss attention,生成相应的转换后的表征。右侧Transformer输入文本。

1.查询嵌入(Query embedding)在Q-Former中,作者创建了一组可学习的Query embedding作为图像变换器的输入。这些Query embedding在自注意力层中彼此交互,并通过交叉注意力层(每隔一个变换器块加入一个交叉注意力层)与冻结的图像编码器输出的图像嵌入进行交互。此外,这些Query嵌入还通过相同的自注意力层与文本嵌入进行交互。作者使用Bert Base的预训练权重来初始化Q-Former,其中交叉注意力层是随机初始化的。Q-Former总共包含188M个参数(包括Query嵌入)。

2.注意力掩膜(attention Mask):使用了不同类型的注意力掩膜来控制注意力机制的行为。这些注意力掩膜用于管理和限制注意力头在处理不同类型数据(例如图像和文本)时的行为。其包含两类:
双向多模态因果注意力掩膜(Bidirectional Multimodal Causal Attention Masking):
计算方法:双向掩膜允许所有位置之间的注意力,这意味着每个位置都可以关注序列中的任何其他位置,无论它是在前面还是后面。具体的实现通常涉及生成一个全1矩阵,这样每个位置都可以看到所有其他位置。
作用:在多模态(图像-文本)数据中,这种掩膜允许模型同时关注输入图像和文本的所有位置,促进它们之间的信息融合和交互。
单向因果注意力掩膜 (Unimodal Causal Attention Masking)
计算方法:单向因果掩膜仅允许当前位置关注其前面的所有位置,具体实现为一个上三角矩阵,其中上三角部分为1,其他部分为0。这确保了注意力仅能向前传播。
作用:这种掩膜用于生成任务,确保生成的每个词只能关注它之前的词,防止信息泄露(即未来的信息被用于当前的生成)。
3.Q-Former的输入Embedding:用于从词嵌入和位置嵌入构建输入表示。词嵌入层和位置嵌入层,分别生成词汇和位置的嵌入向量。LayerNorm层用于规范化嵌入,Dropout层用于防止过拟合。此外,类还注册了一个位置ids缓冲区,用于存储位置索引。在前向传播过程中,首先根据输入的token ids生成词嵌入,再根据位置ids生成位置嵌入,并将二者相加。如果提供了查询嵌入(query_embeds),则将其与词嵌入拼接。最后,对嵌入进行规范化和Dropout处理,返回最终的嵌入表示。
class BertEmbeddings(nn.Module):    """从词嵌入和位置嵌入构建BERT的嵌入层。"""
def __init__(self, config): super().__init__() # 定义词嵌入层,嵌入维度为config.hidden_size,padding_idx为config.pad_token_id self.word_embeddings = nn.Embedding( config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id ) # 定义位置嵌入层,嵌入维度为config.hidden_size self.position_embeddings = nn.Embedding( config.max_position_embeddings, config.hidden_size )
# 定义LayerNorm层,用于规范化 self.LayerNorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps) # 定义Dropout层,用于防止过拟合 self.dropout = nn.Dropout(config.hidden_dropout_prob)
# 注册position_ids缓冲区,用于存储位置索引 self.register_buffer( "position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)) ) # 获取位置嵌入类型,默认为"absolute" self.position_embedding_type = getattr( config, "position_embedding_type", "absolute" )
# 存储配置 self.config = config
def forward( self, input_ids=None, # 输入的token ids position_ids=None, # 输入的位置 ids query_embeds=None, # 查询嵌入 past_key_values_length=0, # 过去的键值对长度 ): if input_ids is not None: # 获取序列长度 seq_length = input_ids.size()[1] else: seq_length = 0
if position_ids is None: # 如果未提供position_ids,则根据past_key_values_length和序列长度生成它们 position_ids = self.position_ids[ :, past_key_values_length : seq_length + past_key_values_length ].clone()
if input_ids is not None: # 获取词嵌入 embeddings = self.word_embeddings(input_ids) if self.position_embedding_type == "absolute": # 获取位置嵌入并将其加到词嵌入上 position_embeddings = self.position_embeddings(position_ids) embeddings = embeddings + position_embeddings
if query_embeds is not None: # 如果提供了query_embeds,将其与词嵌入拼接 embeddings = torch.cat((query_embeds, embeddings), dim=1) else: # 如果未提供input_ids,则直接使用query_embeds embeddings = query_embeds
# 对嵌入进行规范化和Dropout embeddings = self.LayerNorm(embeddings) embeddings = self.dropout(embeddings) return embeddings

4.Q-Former的一个Transformer层结构:包含自注意力、交叉注意力(可选)和前馈网络。初始化时,根据配置决定是否添加交叉注意力,并定义中间层和输出层。在前向传播中,首先应用自注意力,然后根据需要应用交叉注意力。处理查询长度,应用前馈网络生成最终输出。类中的feed_forward_chunk和feed_forward_chunk_query方法分别处理普通和查询前馈计算。此层结构在处理输入时,灵活支持自注意力和交叉注意力,以适应不同任务需求。

class BertLayer(nn.Module):    def __init__(self, config, layer_num):        super().__init__()        self.config = config        self.chunk_size_feed_forward = config.chunk_size_feed_forward        self.seq_len_dim = 1        self.attention = BertAttention(config)  # 定义自注意力机制        self.layer_num = layer_num
# 如果需要交叉注意力,并且当前层是交叉注意力层 if ( self.config.add_cross_attention and layer_num % self.config.cross_attention_freq == 0 ): self.crossattention = BertAttention( config, is_cross_attention=self.config.add_cross_attention ) self.has_cross_attention = True else: self.has_cross_attention = False
self.intermediate = BertIntermediate(config) # 定义中间层 self.output = BertOutput(config) # 定义输出层
self.intermediate_query = BertIntermediate(config) # 定义查询中间层 self.output_query = BertOutput(config) # 定义查询输出层
def forward( self, hidden_states, # 输入的隐藏状态 attention_mask=None, # 注意力掩码 head_mask=None, # 头部掩码 encoder_hidden_states=None, # 编码器隐藏状态(用于交叉注意力) encoder_attention_mask=None, # 编码器注意力掩码 past_key_value=None, # 过去的键值对 output_attentions=False, # 是否输出注意力权重 query_length=0, # 查询长度 ): # 解码器单向自注意力缓存键/值对 self_attn_past_key_value = ( past_key_value[:2] if past_key_value is not None else None ) # 自注意力输出 self_attention_outputs = self.attention( hidden_states, attention_mask, head_mask, output_attentions=output_attentions, past_key_value=self_attn_past_key_value, ) attention_output = self_attention_outputs[0] # 注意力输出 outputs = self_attention_outputs[1:-1] # 其余输出
present_key_value = self_attention_outputs[-1] # 当前键值对
if query_length > 0: query_attention_output = attention_output[:, :query_length, :] # 查询注意力输出
if self.has_cross_attention: assert ( encoder_hidden_states is not None ), "交叉注意力层必须提供 encoder_hidden_states" cross_attention_outputs = self.crossattention( query_attention_output, attention_mask, head_mask, encoder_hidden_states, encoder_attention_mask, output_attentions=output_attentions, ) query_attention_output = cross_attention_outputs[0] outputs = ( outputs + cross_attention_outputs[1:-1] ) # 如果输出注意力权重,添加交叉注意力
layer_output = apply_chunking_to_forward( self.feed_forward_chunk_query, self.chunk_size_feed_forward, self.seq_len_dim, query_attention_output, ) if attention_output.shape[1] > query_length: layer_output_text = apply_chunking_to_forward( self.feed_forward_chunk, self.chunk_size_feed_forward, self.seq_len_dim, attention_output[:, query_length:, :], ) layer_output = torch.cat([layer_output, layer_output_text], dim=1) else: layer_output = apply_chunking_to_forward( self.feed_forward_chunk, self.chunk_size_feed_forward, self.seq_len_dim, attention_output, ) outputs = (layer_output,) + outputs
outputs = outputs + (present_key_value,)
        return outputs

5.Q-Former的Attention部分:包括自注意力机制(Self-Attention)和交叉注意力机制(Cross-Attention),具体实现如下:

class BertSelfAttention(nn.Module):    def __init__(self, config, is_cross_attention):        super().__init__()        self.config = config        # 检查 hidden_size 是否是 num_attention_heads 的倍数        if config.hidden_size % config.num_attention_heads != 0 and not hasattr(            config, "embedding_size"        ):            raise ValueError()
self.num_attention_heads = config.num_attention_heads # 注意力头的数量 self.attention_head_size = int(config.hidden_size / config.num_attention_heads) # 每个注意力头的大小 self.all_head_size = self.num_attention_heads * self.attention_head_size # 所有注意力头的总大小
# 定义 query 线性变换 self.query = nn.Linear(config.hidden_size, self.all_head_size) if is_cross_attention: # 如果是交叉注意力 self.key = nn.Linear(config.encoder_width, self.all_head_size) # 定义 key 线性变换 self.value = nn.Linear(config.encoder_width, self.all_head_size) # 定义 value 线性变换 else: # 如果是自注意力 self.key = nn.Linear(config.hidden_size, self.all_head_size) # 定义 key 线性变换 self.value = nn.Linear(config.hidden_size, self.all_head_size) # 定义 value 线性变换
self.dropout = nn.Dropout(config.attention_probs_dropout_prob) # 定义 dropout self.position_embedding_type = getattr( config, "position_embedding_type", "absolute" ) # 获取位置嵌入类型 if ( self.position_embedding_type == "relative_key" or self.position_embedding_type == "relative_key_query" ): # 如果使用相对位置嵌入 self.max_position_embeddings = config.max_position_embeddings # 最大位置嵌入数 self.distance_embedding = nn.Embedding( 2 * config.max_position_embeddings - 1, self.attention_head_size ) # 定义距离嵌入
self.save_attention = False # 是否保存注意力权重

使用ChatGPT生成指令集

本文考虑了56种不同类型的导航和通知指令,并使用ChatGPT为相同的导航指令生成八种不同的短语。表9展示了为导航指令“右转”生成的八个短语示例。完整的56种导航和通知指令列表在表10中展示。表11展示了生成误导性指令的方法。第一列是代理所在的驾驶场景,第二列是框架生成的可能误导性指令。例如,当自动驾驶车辆在单车道上时,误导性指令“变更路线到左侧车道”违反交通规则并引发安全问题。最后展示了一些连接指令的示例。

由ChatGPT API生成的一个“转”指令的8个不同类型。

框架中考虑的56种不同类型的导航指令和注意指令的完整列表

误导性的指令及其相应的驾驶场景。

在LangAuto基准测试中考虑的连接导航指令的示例。

本期结语

结合两期内容,我们学习了一个语言引导、端到端的闭环自动驾驶框架LMDrive第一期进行LMDrive模型的框架,数据流,数据集,训练以及实验部分进行方法论层面的解读,第二期对具体策略进一步深挖,同步对源码进行逐行解读辅助理解!本篇论文完结撒花~

如果对你的开发、科研有帮助,拜托拜托关注我们,我们将持续奉上优秀的端到端自动驾驶领域研究的分享干货!


温馨提示:点赞=学会,收藏=精通,点击在看,我们一起充电!


端到端自动驾驶
关注AD(Autonomous Driving)行业最前沿的人工智能解决方案,致力于打造为一个自动驾驶从业者及相关科研学者们的课外充电,技术分享,以及社区交流的服务平台!
 最新文章