本期概述
哈喽大家好,上期我们重点分享了LMDrive模型的框架,数据流,训练方法,数据集以及实验部分,空降来的宝子们强烈推荐首先熟悉!CVPR 2024 | LMDrive:使用大语言模型的闭环端到端自动驾驶 Pipeline详解
LMDrive是一个语言引导、端到端的闭环自动驾驶框架。LMDrive处理并集成多模态传感器数据与自然语言指令,在现实指令设置中与人类和导航软件进行交互。小毛认为这个研究成果最新颖的地方在于使用ChatGPT生成指令?!我们来看看吧!
实施细节
视觉编码器:在视觉编码器部分,使用了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嵌入)。
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模型的框架,数据流,数据集,训练以及实验部分进行方法论层面的解读,第二期对具体策略进一步深挖,同步对源码进行逐行解读辅助理解!本篇论文完结撒花~
如果对你的开发、科研有帮助,拜托拜托关注我们,我们将持续奉上优秀的端到端自动驾驶领域研究的分享干货!
温馨提示:点赞=学会,收藏=精通,点击在看,我们一起充电!