2025年7月19日
Last updated on

MapTR 的层级查询设计:用类比理解维度推导

作者:


MapTR 把地图元素表示成点集,再用实例查询与点查询组合出结构化查询。我觉得最容易弄错的地方有两个:组合是广播相加,不是拼接;在下面核对的实现里,查询嵌入先是 2D,随后拆成位置和内容两个 D 维部分。MapTR 论文

本文对照的是上游 main 在本次核对时的提交 a6872d8d9670bde17b4b01560f1221f88b443d55,使用 maptr_tiny_r50_24e.py 配置说明形状。它是源码解读,未重跑完整模型或复现论文指标,MapTRv2 和其他配置也不能直接套用这些维度。

用“项目团队”理解层级查询

MapTR 整体架构

可以把 50 个实例查询看成 50 位建筑师,每位负责一个潜在地图元素;20 个点查询是一组共享的测量员索引。把每位建筑师与每个测量员组合,得到 50 × 20 = 1000 个查询,每个查询对应“某个元素的某个点”。点查询不是预先钉死的起点、转角点角色,具体位置仍由模型学习。

这个类比只解释查询如何组织。MapTR 的另一个关键是把一个地图元素的等价点排列纳入建模和匹配:同一条线换个合理的点序,并不应该变成完全不同的目标。仅把输出写成固定点集,还不足以概括论文的方法。MapTR 论文:结构化建模与层级匹配

源码里的组合其实是 512 维

查询头实现 在 query_embed_type='instance_pts' 时,实例和点嵌入都使用 embed_dims * 2。当 D=256 时:

步骤形状含义
实例嵌入[50, 512]每个潜在地图元素一条嵌入
点嵌入[20, 512]每个点索引一条共享嵌入
实例插入轴[50, 1, 512]等待对点轴广播
点插入轴[1, 20, 512]等待对实例轴广播
相加[50, 20, 512]所有实例与点的组合
展平前两轴[1000, 512]送给 Transformer 的查询嵌入

对每个实例 i、点 j、通道 k,结果都是 instance[i, k] + point[j, k]。共享点嵌入节省的是参数组织,不会把最后 1000 个查询压缩成 70 个计算位置。

先看组合这一步。下面这段可以在 PyTorch 环境中单独运行,后面再把 batch 轴和输出头接起来检查:

import torch
num_instances, num_points, dim = 50, 20, 256
instance = torch.zeros(num_instances, 2 * dim)
point = torch.zeros(num_points, 2 * dim)
combined = instance[:, None, :] + point[None, :, :]
query_embed = combined.flatten(0, 1)
query_pos, query = query_embed.split(dim, dim=1)
assert combined.shape == (50, 20, 512)
assert query_pos.shape == query.shape == (1000, 256)

Transformer 实现 再把它沿通道轴拆成 query_pos 和 query,扩展 batch 轴。解码器接收的内容查询与位置查询都是 [1000, B, 256];由位置查询生成的初始参考点是 [B, 1000, 2],经 sigmoid 归一化。

注意力类比,不能当成实际计算图

MapTR 框架详细设计

“查询去地图上找相关信息”这个直觉没问题,但把它直接写成对整张 BEV 做 QKᵀ,就把解释模型和实际实现混在一起了。所用配置 的解码器使用 MultiheadAttention 做查询之间的自注意力,使用 CustomMSDeformableAttention 从 BEV 特征取信息。

密集交叉注意力可以用 Q: [1000, D]、K: [N, D]、权重 [1000, N] 来讲入门概念,但它不是这份配置的真实交叉注意力。可变形注意力围绕参考点,以学习的偏移和权重采样少量位置,不必显式构建这张完整的密集权重矩阵。与此同时,查询间的自注意力仍有自己的计算开销,不能因此说整个网络复杂度都变成了线性。

也要区分两层查询:BEV 编码器用自己的 BEV 查询组织鸟瞰特征,地图解码器才用这里的实例—点查询预测地图元素。

输出头如何拿回地图元素

每层解码后的查询整理成 [B, 1000, 256]。分类分支先 reshape 成 [B, 50, 20, 256],在 20 个点上取平均,再输出 [B, 50, C] 的类别分数;这个配置的地图类别是 divider、ped_crossing、boundary,C=3。

回归分支逐点预测坐标偏移,与参考点在 inverse-sigmoid 空间结合后再做 sigmoid,最终得到 [B, 50, 20, 2] 的归一化点坐标。把坐标恢复到车辆空间还要结合 pc_range,不能把输出的 [0, 1] 值直接理解成米。源码还从点集计算包围框,并返回各解码层的结果。分类与坐标处理

MapTR 可视化结果

实例数、点数、骨干网络和 BEV 编码器都属于配置选择。图中的预测展示有助于理解输出形式,但不能替代对不同配置的速度、精度和复杂路形的定量评估。

怎么实际跑一遍 query 到输出的维度?

先用 I=2 个实例、P=3 个点、D=4 跑一个看得见的例子,再换成上面配置里的 I=50、P=20、D=256、L=6 检查形状。B=2 是为了让 batch 轴和 query 轴不容易混淆,不是在声称原配置的训练 batch 就是 2。

完整脚本在 maptr-query-shapes.py。只需要 Python 和 PyTorch,CPU 即可,不需要下载 MapTR、数据集、模型权重或编译 MMCV。已有 PyTorch 环境可以跳过安装:

Terminal window
python3 -m venv .venv-maptr-shapes
source .venv-maptr-shapes/bin/activate
python -m pip install torch
curl -fL https://tonwork.fun/blog/maptr-query-shapes.py -o maptr-query-shapes.py
python maptr-query-shapes.py

先说明边界:这个脚本验证的是查询组合、拆分、轴变换,以及简化输出头的输入输出形状。解码器输出用占位张量代替,分类和回归头用单层 Linear 代替;没有运行自注意力、BEV 可变形采样、逐层参考点更新、匹配或训练。它不是完整 MapTR 推理,更不能用来测速度和精度。

脚本里最值得检查的不是 shape 本身,而是展平之后有没有把“建筑师—测量员”的对应关系弄乱:

# 每个实例用 10 * i 标记,每个共享点用 j 标记。
instance = torch.arange(instances, dtype=torch.float32)[:, None].repeat(1, 2 * dim) * 10
point = torch.arange(points, dtype=torch.float32)[:, None].repeat(1, 2 * dim)
combined = instance[:, None, :] + point[None, :, :]
query_embed = combined.flatten(0, 1)
for i in range(instances):
for j in range(points):
torch.testing.assert_close(query_embed[i * points + j], instance[i] + point[j])

小例子的展平顺序是 (0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2),第一通道应为 [0, 1, 2, 10, 11, 12]。也就是说,第 q 个查询对应实例 q // P、点 q % P。这些值只是帮助定位轴顺序的人工标记,不是训练得到的嵌入。

两组参数应打印以下形状,末尾均为 all assertions passed:

脚本检查点小例子tiny 配置维度
广播相加 combined[2, 3, 8][50, 20, 512]
展平 query_embed[6, 8][1000, 512]
拆分并扩展 batch,query_pos / query[2, 6, 4][2, 1000, 256]
初始参考点 reference[2, 6, 2][2, 1000, 2]
解码器入口,sequence-first[6, 2, 4][1000, 2, 256]
占位解码器输出 hs,[L, Q, B, D][2, 6, 2, 4][6, 1000, 2, 256]
简化分类头,[L, B, I, C][2, 2, 2, 3][6, 2, 50, 3]
简化点回归头,[L, B, I, P, 2][2, 2, 2, 3, 2][6, 2, 50, 20, 2]

注意这里的 hs 要先从 [L, Q, B, D] 换成 [L, B, Q, D],才按实例和点分组。分类是在点轴上平均,每个实例输出一组类别分数;回归仍保留每个点的位置。脚本还检查坐标有限且处于 [0, 1],这只验证数值范围,不说明预测正确,也不意味着坐标已经换成米。

这些步骤分别对应哪段源码?

以下链接都固定在本文核对的提交,避免上游更新后行号和实现悄悄变化:

检查内容上游位置
2D 嵌入与实例—点广播相加maptr_head.py:嵌入初始化、组合与展平
位置/内容拆分、参考点与 batch 轴变换transformer.py
解码层输出的堆叠顺序decoder.py
分类聚合、坐标处理与各层输出maptr_head.py

实际解码器还会迭代更新参考点,后续输出层不总是用初始参考点。为了单独看清形状,脚本每个占位层都复用同一份初始参考点,并将生成参考点的线性层置零,让它们从 0.5 开始。这两处都是演示简化,不能搬进实际模型当成等价实现。

为什么配置写了 num_query=900,这里却是 1000?

这份配置确实写了 num_query=900,但对应版本的 MapTRHead 在初始化时用 num_vec * num_pts_per_vec 设置查询数。所以在 instance_pts 路径下,实际展开的是 50 × 20 = 1000。看配置时不能只抓住一个字段,还要看它有没有被构造逻辑重新设置。配置字段、查询数初始化

形状都对了,就说明实现没问题吗?

还不够。比如把点轴和实例轴交换后再展平,查询总数仍然是 I × P,单看形状不一定能发现错误。这个例子额外检查了 q=i*P+j 的数值对应关系。至于点序等价性、匹配、真实注意力和坐标还原,仍要回到模型与数据里验证,不能用这几个断言代替。

如果接下来关心部署,读 BEV 感知的实时性与性能口径;想把训练、地图预测、融合放在同一条阅读路径上,见 BEV 感知工程导读。