PointNet 与 MLP:提取道路拓扑时,两者到底差在哪
同样是把元素喂给网络,逐元素 MLP 和 PointNet 的共享 MLP + 对称池化差在哪里?本文从直觉、公式、代码到车道图实战,说清两者的共性与分野,以及为什么补上“边”才是读懂拓扑的解药。
把一段路网喂给网络,它要懂的不是“这里有哪些车道”,而是“谁连着谁”。这一步,MLP 和 PointNet 都做不到——差别只在于,它们做不到的方式不一样。
1. 引子:路网里最难的那件事
先想清楚一件事:什么叫“道路拓扑”。
它不是“这条路上有几条车道、每条多宽”,那是几何和属性。拓扑是连接关系:这条车道通向哪条,在路口是左转、直行还是汇入,哪两条是相邻车道,哪里分叉、哪里合流。一句话,谁连着谁。
这件事难,是因为它不是一个“集合”性质,而是一个“关系”性质。你有一堆车道段(lane segment),把它们装进一个袋子晃一晃,几何还是那些几何,但如果你不知道它们之间连了哪几条边,你的路网就从“十字路口”变成了“T 字口”,甚至是“八条互不相干的断头路”。
偏偏深度学习里最常见的两块积木——MLP 和 PointNet——在“关系”这件事上,都不太行。而且它们不行的原因,恰好能讲清楚这两块积木的区别。今天就把这件事掰开说。
2. MLP:一块最朴素的积木
2.1 它到底在做什么
多层感知机(MLP)就是若干全连接层叠在一起:
# 单层:y = σ(Wx + b)
# 多层:h = σ(W2 · σ(W1 · x + b1) + b2)
它的本质是逐元素(pointwise)的特征变换:吃进去一个向量,吐出来另一个向量,中间做升维、降维、加非线性。你给它一堆 token,它就是对每个 token 各自做一遍同样的变换——因为权重是共享的,所以它不关心“这是第几个 token”。
这里已经藏着第一个关键:MLP 层本身不产生元素之间的交流。处理第 3 条车道的时候,它完全不知道第 4 条车道长什么样。
2.2 用在路网上的两种姿势
那 MLP 怎么用在路网上?通常两种姿势。
姿势一:当成“元素编码器”。 每条车道段有自己的特征(几何采样点、类别、宽度……),过几层 MLP 变成每条车道段的 embedding。此时每条车道段是独立编码的,互相零交流。这在工程上非常常见——MapTR 里的 point query、VectorNet 里的子图编码,本质上都是拿 MLP 当局部编码器。
姿势二:把所有元素拍平(flatten)喂进去。 比如把 N 条车道段的特征拼成一个大向量,过 MLP 输出结果。这下元素之间“有交流”了,但交流的方式很心虚:它靠的是位置。而元素在输入里的位置,是你人为排的序——车道 A 排第一还是排第三,语义上没有差别。网络却会把位置当成信号去记,学出一堆伪相关(spurious correlation)。换个排序,输出就变了。
所以 MLP 用在路网上,一个是“元素之间不说话”,一个是“只能靠人为顺序瞎说话”。都不理想。
3. PointNet:给 MLP 加一层对称池化
3.1 三个设计
PointNet(Qi et al., CVPR 2017)是为无序点云设计的。点云有个要命的性质:它是个集合,点的顺序没有任何意义,你打乱列表,物体还是那个物体。普通全连接层在这里直接崩——它给“第 1 个点”和“第 2 个点”分配了不同的权重。
PointNet 的解法非常克制,就三件事:
一是共享 MLP(shared MLP)。 同一个小 MLP,对每个点独立跑一遍,权重完全共享。因为每个点用的是同一套权重,打乱顺序只会打乱输出,不会改变每个点自己的结果。
二是对称池化(symmetric pooling)。 每个点算出特征后,在“点”这个维度上做一次池化,通常是 max。max 是置换不变的——取最大值这件事,跟谁先谁后无关。于是整个点云被压成一个固定长度的全局描述子。
三是 T-Net。 一个小网络,学一个仿射变换矩阵,把输入点云对齐到规范姿态,解决旋转、平移问题。它本身也是一个迷你 PointNet,还配了个正交正则化防止学歪。
合起来,核心公式就一行:
f 作用于点集 x1 到 xn,输出约等于 γ( max_i h(x_i) )
这里 h 是共享 MLP,max 是对称函数,γ 是最后几层 MLP。
3.2 为什么 max 最好用
这篇论文比较过几种对称函数,max 池化效果最好。原因很直观:它天然在做“关键点选择”。每个特征通道取最大值,相当于自动挑出最能定义这个物体的那批点(边缘、角点),其余点被忽略。这种“稀疏选择”让模型对噪声、对丢点都很鲁棒。
论文还给了一个漂亮的理论保证(Theorem 1):在 Hausdorff 距离意义下,任何连续的点集函数,都能被“γ 复合 max 复合 h”逼近到任意精度。也就是说,PointNet 是集合函数的通用逼近器。同时,网络的输出只依赖于一个不超过 K 个元素的“关键点集”(K 是池化后的瓶颈维度)。
这个理论很漂亮。但注意它的措辞——它逼近的是集合函数。这句话后面我们还会回来算账。
4. 共性:它们其实是同一家人
聊完两个,你会发现一个有点反直觉的事实:PointNet 就是 MLP 搭出来的。
拆开看,PointNet 的 h 是 MLP,γ 也是 MLP,中间的 max 是一个不带参数的操作。所以 PointNet = 共享 MLP + 一个对称池化。它不是 MLP 的替代品,它是 MLP 在“集合”这种数据结构上的一次加固。
由此,共性有三条:
第一,两者都是“逐元素”的操作,都没有关系归纳偏置(relational inductive bias)。 MLP 逐元素变换;PointNet 是逐元素变换之后再加池化。它们的设计里都没有“边”这个概念,没有邻域,没有谁影响谁。元素与元素之间的作用,要么没有(逐元素 MLP),要么被压成一次与顺序无关的池化(PointNet),要么被交给人为排序(flatten MLP)。
第二,两者都是“结构不可知”的通用逼近器。 你喂什么特征它就学什么。想让它懂一点结构,你得自己把结构编码成输入特征。架构本身不会替你产生结构先验。这一点上,MLP 和 PointNet 站在同一条船上。
第三,MLP 是两者的公共底座。 不管是最普通的全连接层,还是 PointNet 里的共享 MLP,还是 MapTR、VectorNet 里的元素编码器,都是这一块积木。PointNet 相对 MLP 的“新”,全在新在那个池化上。
5. 分野:一条“置换不变”的先验,是天堑
既然是同一家人,那区别在哪?区别全在 PointNet 多加的那条先验:置换不变。
这条先验带来了一串连锁反应:
置换不变性。 PointNet 有(靠对称池化),MLP 没有(flatten 之后顺序敏感)。
聚合能力。 PointNet 有一次全局池化,能把“一整个集合”压成一个描述子;MLP 没有,它要么逐元素,要么定长拍平。
输入基数。 PointNet 能处理任意数量的点(顺序无关,个数也可以变);MLP 拍平之后输入维度是固定的,点多了少了它接不住。
鲁棒性。 PointNet 靠“关键点集”这个性质,丢几个点、混几个噪点,输出基本不抖;MLP 没有这层保护。
表达自由度。 反过来看,MLP 更“自由”——它不设任何对称性约束,理论上可以拟合任意定长的映射,包括那些对结构敏感的映射。PointNet 的置换不变是个更强的先验,而更强的先验意味着更强的信息丢弃。
一句话总结分野:PointNet 用一条先验换来了对“无序集合”的正确归纳;MLP 什么先验都不加,把一切交给数据和输入编码。
到这里都很顺。但接下来是本文真正想说的一句:在“道路拓扑”这件事上,这条分野其实救不了谁。
6. 关键洞察:对拓扑而言,两者的短板是同一个
回到第 3.2 节那个措辞——PointNet 逼近的是集合函数。而道路拓扑,是一个图函数,不是集合函数。
我们用反证法,构造一个两个路网:
- 路网甲:一个标准十字路口,四条车道段,两两之间有连接边。
- 路网乙:一个 T 字口,同样的四条车道段(几何一模一样),但少了一条连接边。
现在把两者的车道段集合分别喂给一个 PointNet。因为两条路网的车道段集合完全相同(同样的点、同样的特征),共享 MLP 出来的 per-element 特征一模一样,max 池化出来的全局描述子也一模一样。PointNet 输出无法区分甲和乙。
道理很简单:对称池化会把“节点集合”里的连接信息洗掉。 图在节点置换下不变的前提,是“边也跟着一起置换”;只对节点做对称池化,边就被彻底丢掉了。所以朴素 PointNet(全局池化版)在原理上就表达不了拓扑。这不是训练不够,是架构的信息瓶颈。
那换成 MLP 呢?如果输入只有“每条车道段的特征”,MLP 同样看不到边——输入里根本没有的东西,网络再大也变不出来。如果为了让 MLP“看到结构”,你把它拍平、靠排序暗含连接,那又回到第 2.2 节的老问题:排序是人为的,MLP 学到的是伪相关,泛化会崩。
于是两者的共同短板浮出水面:都没有关系归纳偏置。 具体表现为两种病:
- PointNet 是“先验太强”:一条置换不变的假设,把本来就不该对称的东西强行对称化,结构被洗掉。
- MLP 是“先验太空”:没有任何结构约束,要么看不到关系,要么只能靠数据硬记关系,样本效率低、泛化差。
6.1 顺带说清:要的是“置换等变”,不是“置换不变”
这里有个特别容易被含混过去的点,值得单独拎出来。
置换不变(invariance):把输入元素打乱,整体输出不变。PointNet 的对称池化做的就是这件事——点云里谁先谁后,压出来的描述子都一样。
置换等变(equivariance):把输入元素打乱,输出跟着一起重排。A 点的表示还是 A 点的表示,只是换了个位置。
对一张图来说,正确的性质是“等变”,不是“不变”。节点换序的时候,每个节点的表示应该跟着换位;只有当整张图同构(节点和边一起换)时,全局输出才应该保持不变。而 PointNet 的做法是:只对节点做对称池化,边却没有跟着一起置换——于是只要两个图节点特征的多重集相同,不管边怎么连,池化完之后都是同一个结果。十字路口和 T 字口,就是这么被混成一类的。
所以一句话记住:置换不变是“无视顺序”,置换等变是“尊重顺序,但允许它换位置”。 处理无序点云要前者,处理拓扑、序列要后者。这也是为什么 PointNet 家族一旦要碰结构,就得靠 PointNet++ 的分组、EdgeConv 的动态图把“位置感”重新找回来。
看清楚了这一点,解药也就浮出来了:补上“边”,或者说,让信息沿着关系流动。
7. 补边:从 PointNet++ 到 LaneGCN、MapTR
有意思的是,PointNet 家族自己就是这么进化的。
PointNet++(2017) 发现一次性全局池化丢掉了局部几何,于是引入分层:最远点采样挑中心点,ball query 圈出邻域,在每个邻域里再跑一遍迷你 PointNet。它在空间上“造”出了邻域结构。
DGCNN(2019) 更进一步,提出 EdgeConv:在特征空间里给每个点动态找 k 近邻,建一张图,然后对“中心点与邻居的差”跑共享 MLP。注意,从“点集”到“图”的这一步跨界,恰恰是通过补边完成的。
到了车道图,思路更彻底。LaneGCN(Uber ATG, ECCV 2020) 直接把路网建成一张车道图:节点是车道段,边是四类关系——前驱、后继、左邻、右邻。然后用图卷积(LaneConv)沿边聚合信息,再配合空间注意力。VectorNet(Waymo, CVPR 2020) 走的是“子图 + 全局图”两级结构:先用类 PointNet 的方式把每条折线(车道线、轨迹)编码成一个子图向量,再用 GNN 在元素之间做交互。
MapTR(ICLR 2023) 则代表了另一条路线——注意力。它基于 DETR 框架,用一个置换等价的集合预测范式来输出矢量化地图元素,靠层级化的 query(instance 级 + point 级)和 transformer decoder 里的注意力,让元素之间、点之间彼此交互。
把这些串起来,其实是一条很清楚的技术谱系:
| 结构补丁 | 代表 | 补的是什么 | 对拓扑的能力 |
|---|---|---|---|
| 逐元素 MLP | 元素编码器 | 无 | 无(看不到关系) |
| 全局池化 PointNet | PointNet | 集合聚合 | 无(关系被洗掉) |
| 空间邻域 | PointNet++ | 局部空间结构 | 有局部几何,无显式拓扑 |
| 特征空间图 | DGCNN | 动态 kNN 边 | 有局部拓扑 |
| 语义图 + 消息传递 | LaneGCN / VectorNet / GAT | 沿边的信息流动 | 有(显式建模连通性) |
| 集合预测 + 注意力 | MapTR | 元素间注意力交互 | 有(隐式建模关系) |
实践里最常见的搭配是两者结合:用 MLP 或类 PointNet 的结构当“元素编码器”,把它们编码成 token,再交给 GNN 或 Transformer 当“关系聚合器”。LaneGCN 的车道图 + 编码器、MapTR 的 transformer decoder 都是这个套路。
8. 玩具实验:MLP / PointNet / GNN,谁分得清路口
光讲道理不够直观,写个小实验。目标:区分“十字路口”和“T 字口”,两者车道段集合完全相同,只有连接边不同。
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(0)
S, D = 4, 8 # S 条车道段,每条 D 维几何特征
N_CLS = 2 # 0: 十字路口, 1: T 字口
def make_batch(n):
"""两类场景:节点特征集合一致,唯一的差别是邻接矩阵 A。"""
x = torch.randn(n, S, D) # 车道段几何特征(两类共用同一分布)
A = torch.zeros(n, S, S)
for i in range(n):
if i % 2 == 0: # 十字:4 条边、闭环
A[i] = torch.tensor([[0,1,0,1],[1,0,1,0],[0,1,0,1],[1,0,1,0]]).float()
else: # T 字:少一条边
A[i] = torch.tensor([[0,1,0,1],[1,0,1,0],[0,1,0,0],[1,0,0,0]]).float()
y = (torch.arange(n) % 2).long()
return x, A, y
# —— 模型 A:逐元素 MLP + 全局平均池化(PointNet 式,忽略边)——
class PoolMLP(nn.Module):
def __init__(self):
super().__init__()
self.mlp = nn.Sequential(nn.Linear(D, 64), nn.ReLU(), nn.Linear(64, 64))
self.head = nn.Linear(64, N_CLS)
def forward(self, x, A):
h = self.mlp(x) # [n, S, 64],逐元素
return self.head(h.mean(dim=1)) # 对称池化,与顺序无关,也与边无关
# —— 模型 B:拍平 MLP(靠人为顺序编入结构)——
class FlatMLP(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(nn.Linear(S * D, 128), nn.ReLU(), nn.Linear(128, N_CLS))
def forward(self, x, A):
return self.net(x.flatten(1)) # 顺序敏感:换个排序结果就变
# —— 模型 C:一层图消息传递(沿边聚合)——
class GNN(nn.Module):
def __init__(self):
super().__init__()
self.enc = nn.Linear(D, 64)
self.msg = nn.Linear(64, 64)
self.head = nn.Linear(64, N_CLS)
def forward(self, x, A):
h = self.enc(x) # [n, S, 64]
deg = A.sum(-1, keepdim=True).clamp(min=1) # 归一化,防度数爆炸
agg = torch.bmm(A, h) / deg # 沿边聚合邻居信息
h = F.relu(self.msg(agg) + h) # 消息传递 + 残差
return self.head(h.mean(dim=1))
用同一份数据训练三个模型,看测试准确率:
for name, M in [("A: 池化 MLP", PoolMLP), ("B: 拍平 MLP", FlatMLP), ("C: GNN", GNN)]:
model, opt = M(), None
opt = torch.optim.Adam(model.parameters(), lr=3e-3)
x, A, y = make_batch(512)
for _ in range(400):
loss = F.cross_entropy(model(x, A), y)
opt.zero_grad(); loss.backward(); opt.step()
xt, At, yt = make_batch(256)
acc = (model(xt, At).argmax(-1) == yt).float().mean().item()
print(f"{name}: acc={acc:.3f}")
说明:这是一个刻意设计的玩具场景,不是基准测试。结论方向是结构性的,具体数值随随机种子浮动。
预期结果(示意,非实测):
- A(池化 MLP):约等于瞎猜。它只看到车道段的特征集合,而两类场景的集合分布完全一样,边是 invisible 的——它在原理上就分不开,不是训练不够。
- B(拍平 MLP):能靠数据的固定排列“背”出一点,但由于排序是人为的、没有语义,本质是记住了数据里的巧合。换一个排列或换一批数据,泛化会明显变差。
- C(GNN):能稳定分开。因为两类场景唯一的差别就是边,而消息传递恰恰沿边进行——信息从边里流进来了。
这个小实验想说的是同一件事:结构不在输入和算子里的地方,模型学不到。 想让网络懂拓扑,你得让信息沿关系走。
9. 总结与边界
回到最初的问题:MLP 和 PointNet,对道路拓扑特征的提取能力,有什么区别和共性?
共性:它们是一家人。PointNet 就是“共享 MLP + 对称池化”,两者都是逐元素、无关系偏置的结构不可知逼近器。想联网结构,都得靠输入显式喂进去。
分野:PointNet 多了一条“置换不变”的先验,换来对无序集合的正确归纳、可变基数、抗噪鲁棒;代价是这条强先验会洗掉连接信息。MLP 不加任何先验,更自由,但也更容易学出伪相关。
对拓扑而言:两者的短板是同一个——都没关系归纳偏置。而且 PointNet 在这件事上更危险一点,因为它看起来“很懂集合”,容易让人误以为它懂结构,但全局池化对图拓扑是无能为力的。再往前推一步,图要的性质其实是置换等变而非置换不变:节点换序时表示该跟着一起换,只有整张图同构时全局输出才该不变。
边界:一句“补边就行”其实也不轻松。显式建图(LaneGCN 那种)依赖你事先知道边该怎么定义,且大规模路网的图会很大、消息传递很贵;注意力路线(MapTR 那种)虽然不必手写边,但代价是二次复杂度,以及“注意力到底学到了什么关系”这件事依然不好解释。邻域怎么划、边怎么定义、关系怎么聚合,仍然是工程上要反复调的东西。
所以更准确的表述是:MLP 和 PointNet 负责“看懂每个元素”,而拓扑这件事,得由“边”来收尾。 什么时候用哪块积木,取决于你的拓扑先验有多强、你的数据里结构信号有多弱。
参考资料
- Qi et al., PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation, CVPR 2017
- Qi et al., PointNet++: Deep Hierarchical Feature Learning on Point Sets in a Metric Space, NeurIPS 2017
- Wang et al., Dynamic Graph CNN for Learning on Point Clouds, TOG 2019
- Liang et al., Learning Lane Graph Representations for Motion Forecasting (LaneGCN), ECCV 2020
- Gao et al., VectorNet: Encoding HD Maps and Agent Dynamics from Vectorized Representation, CVPR 2020
- Liao et al., MapTR: Structured Modeling and Learning for Online Vectorized HD Map Construction, ICLR 2023
- Li et al., HDMapNet: An Online HD Map Construction and Evaluation Framework, ICRA 2022