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

> 对照 MapTR 上游代码，用可运行的 PyTorch 示例验证实例与点查询的广播相加、512维嵌入拆分、batch 布局与地图输出形状。

作者：[汤国放](https://tonwork.fun/#person)
发布日期：2025-07-19
更新日期：2026-10-10
原文：https://tonwork.fun/blog/transformermaptr-%E6%A0%B8%E5%BF%83%E6%80%9D%E6%83%B3%E4%B8%8E%E6%B5%81%E7%A8%8B%E6%80%BB%E7%BB%93/

---

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

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

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

![MapTR 整体架构](https://tonwork.fun/_astro/maptr_teaser.tOKJSHks_UJhPu.webp)

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

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

## 源码里的组合其实是 512 维

[查询头实现](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py) 在 `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 轴和输出头接起来检查：

```python
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 实现](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/modules/transformer.py) 再把它沿通道轴拆成 `query_pos` 和 `query`，扩展 batch 轴。解码器接收的内容查询与位置查询都是 `[1000, B, 256]`；由位置查询生成的初始参考点是 `[B, 1000, 2]`，经 sigmoid 归一化。

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

![MapTR 框架详细设计](https://tonwork.fun/_astro/maptr_framework.DDsg3tz7_Z2aRFiw.webp)

“查询去地图上找相关信息”这个直觉没问题，但把它直接写成对整张 BEV 做 `QKᵀ`，就把解释模型和实际实现混在一起了。[所用配置](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/configs/maptr/maptr_tiny_r50_24e.py) 的解码器使用 `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]` 值直接理解成米。源码还从点集计算包围框，并返回各解码层的结果。[分类与坐标处理](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py)

![MapTR 可视化结果](https://tonwork.fun/_astro/maptr_visualizations.D8I0wsMA_1o4wps.webp)

实例数、点数、骨干网络和 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](https://tonwork.fun/blog/maptr-query-shapes.py)。只需要 Python 和 PyTorch，CPU 即可，不需要下载 MapTR、数据集、模型权重或编译 MMCV。已有 PyTorch 环境可以跳过安装：

```bash
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` 本身，而是展平之后有没有把“建筑师—测量员”的对应关系弄乱：

```python
# 每个实例用 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：嵌入初始化](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py#L185-L188)、[组合与展平](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py#L223-L228) |
| 位置/内容拆分、参考点与 batch 轴变换 | [transformer.py](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/modules/transformer.py#L328-L346) |
| 解码层输出的堆叠顺序 | [decoder.py](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/modules/decoder.py#L44-L81) |
| 分类聚合、坐标处理与各层输出 | [maptr_head.py](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py#L270-L313) |

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

### 为什么配置写了 `num_query=900`，这里却是 1000？

这份配置确实写了 `num_query=900`，但对应版本的 `MapTRHead` 在初始化时用 `num_vec * num_pts_per_vec` 设置查询数。所以在 `instance_pts` 路径下，实际展开的是 `50 × 20 = 1000`。看配置时不能只抓住一个字段，还要看它有没有被构造逻辑重新设置。[配置字段](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/configs/maptr/maptr_tiny_r50_24e.py#L78-L83)、[查询数初始化](https://github.com/hustvl/MapTR/blob/a6872d8d9670bde17b4b01560f1221f88b443d55/projects/mmdet3d_plugin/maptr/dense_heads/maptr_head.py#L116-L136)

### 形状都对了，就说明实现没问题吗？

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

如果接下来关心部署，读 [BEV 感知的实时性与性能口径](https://tonwork.fun/blog/%E9%BB%91%E7%AE%B1%E6%8F%AD%E7%A7%98-%E8%B7%91%E6%BB%A130fps%E7%9A%84%E9%87%8F%E4%BA%A7bev%E6%84%9F%E7%9F%A5%E7%B3%BB%E7%BB%9F/)；想把训练、地图预测、融合放在同一条阅读路径上，见 [BEV 感知工程导读](https://tonwork.fun/topics/bev/)。
