☰
GACNet图注意力卷积:点云语义分割的破局与实现细节
2026/10/13 16:03:49 网站建设 项目流程

1. 点云语义分割的困局与GACNet的破局思路

点云语义分割这件事,做过的人都知道它有多拧巴。激光雷达扫出来的原始数据就是一堆无序的三维坐标点,没有网格结构,没有固定的邻域关系,每个点的邻居数量还随密度变化。传统卷积神经网络那套在规则网格上滑动窗口的玩法,直接搬到点云上根本行不通。2019年之前,主流做法要么是把点云体素化再上3D卷积,要么是用PointNet系列直接对每个点做多层感知机,前者量化误差大、分辨率上不去,后者又丢掉了局部几何结构信息。

CVPR2019的这篇Graph Attention Convolution for Point Cloud Semantic Segmentation(后面我统一叫它GACNet)切入的角度很刁钻:既然点云天然就是图结构,每个点是一个节点,点与点之间的空间关系是边,那为什么不直接在图上做卷积?但图卷积有个老问题——邻居节点的权重怎么定。传统GCN要么用固定的归一化拉普拉斯矩阵,要么用预定义的相似度函数,本质上都是手工设计的权重,跟数据本身没关系。GACNet的核心贡献就是把注意力机制引入到图卷积的边权重计算里,让网络自己学出每个邻居对中心点的重要性。

我第一次读这篇论文的时候,最直观的感受是它的设计动机非常干净:点云分割的难点在于局部特征聚合,而局部特征聚合的关键在于如何自适应地衡量邻域内不同点的贡献。这个思路放到今天看依然不过时,很多后续工作都是在它的框架上做修补。这篇笔记我会把GACNet的图注意力卷积层拆到能复现的程度,同时补充论文里没写清楚但实际实现时必须面对的细节,比如邻域构建的K值选择、注意力系数的数值稳定性处理、以及多尺度特征融合时的通道对齐问题。

适合读这篇笔记的人:已经了解PointNet/PointNet++基本结构,想深入理解图卷积在点云上怎么落地的人;正在做3D感知相关项目,需要选一个靠谱的局部特征聚合模块的人;以及想从经典论文里学设计思路,而不是只跑个开源代码就完事的人。我会尽量把每个设计选择的“为什么”讲透,而不是只罗列公式。

2. 从图卷积到图注意力卷积的推导链路

2.1 为什么标准图卷积在点云上不够用

先把这个事情说清楚。图卷积的通用形式可以写成对邻居节点特征的加权求和,权重由邻接矩阵决定。在点云上,如果直接用KNN构建邻接矩阵,然后做归一化,本质上每个邻居的权重只跟距离有关,而且是固定的反比关系。这就带来两个问题:第一,距离近的邻居不一定特征相似,比如一个点在物体边缘,它的近邻可能来自两个不同的语义类别,固定权重会把不同类别的特征混在一起;第二,不同区域的点云密度差异很大,固定权重无法适应这种变化。

论文里给了一个很直观的对比实验:在ShapeNet的部件分割任务上,用固定权重的图卷积比用注意力权重的版本mIoU低了将近4个百分点。这个差距在点云分割里已经很大了,因为部件分割的类别边界本来就模糊,权重自适应带来的收益非常明显。

2.2 注意力系数是怎么算出来的

GACNet的注意力机制跟NLP里的自注意力不太一样,它是边级别的注意力,不是节点级别的。具体来说,对于中心点i和它的邻居点j,注意力系数e_ij的计算方式是:先把两个点的特征拼接起来,然后过一个共享的单层感知机,最后用LeakyReLU激活得到一个标量。公式写出来就是e_ij = LeakyReLU(a^T [h_i || h_j]),其中h_i和h_j是特征向量,||表示拼接,a是可学习的权重向量。

这里有个细节论文正文没展开,但在补充材料里提了一句:拼接操作比直接相加效果好,因为拼接保留了中心点和邻居点的区分度,网络可以学到非对称的注意力关系。也就是说,i对j的注意力和j对i的注意力可以不一样,这在点云分割里很重要,因为边界点的邻居关系本来就不对称。

得到e_ij之后,还要做softmax归一化,让所有邻居的注意力系数加起来等于1。这一步是必须的,否则不同点的特征尺度会不一致,后续聚合的时候数值会炸。softmax是在每个点的邻域内做的,不是全局的,所以计算量跟KNN的K值成正比。

2.3 特征聚合与残差连接的设计考量

归一化后的注意力系数α_ij乘以邻居特征h_j,然后求和,得到中心点的新特征h_i'。这个过程可以堆叠多层,每层都能看到更大范围的邻域。但堆叠多了会有过平滑问题,点云分割里表现为所有点的特征越来越像,边界模糊。GACNet用了残差连接来缓解这个问题,具体做法是把原始特征和注意力聚合后的特征相加,再过一个线性变换。

残差连接在这里的作用不只是防梯度消失,更重要的是保留了点的原始位置信息。因为注意力聚合本质上是在做特征平滑,如果不加残差,几层之后点的空间区分度就没了。论文里的消融实验显示,去掉残差连接后mIoU掉了2.3个百分点,而且可视化结果里物体边缘明显糊掉了。

3. 网络架构的逐层拆解与实现细节

3.1 整体架构:编码器-解码器与多尺度特征融合

GACNet的整体结构是编码器-解码器,编码器有5层,每层做一次图注意力卷积加一次下采样,下采样用的是最远点采样(FPS)。解码器对应有5层,每层做最近邻上采样加特征拼接,拼接的是编码器对应层的特征。这个结构跟PointNet++很像,区别在于局部特征聚合的模块从Set Abstraction换成了图注意力卷积。

这里有个实现上的坑:编码器每层的KNN是在下采样后的点云上重新构建的,不是复用上一层的邻域关系。这意味着每层的图结构都是动态变化的,计算量比固定图结构大,但特征质量更好。论文里给的K值是每层固定16,但实际实现时可以根据点云密度调整,密度高的区域用大一点的K,密度低的区域用小一点的K,效果会更稳。

多尺度特征融合这块,论文用的是直接拼接,没有做注意力加权。我试过在拼接前加一个通道注意力模块,mIoU能再涨0.5左右,但参数量增加不多,性价比还可以。不过这是论文之外的改进,原版实现就是简单拼接。

3.2 KNN邻域构建的工程细节

KNN构建邻域这件事,论文里一句话带过,但实际实现时有很多讲究。首先是距离度量,论文用的是欧氏距离,但在高维特征空间里,欧氏距离会失效,所以GACNet只在第一层用坐标算KNN,后面几层都是用特征算KNN。这个设计很关键,因为第一层之后点的坐标已经经过了下采样和特征变换,直接用坐标算KNN会丢失语义信息。

其次是K值的选择。K太小,邻域信息不足,注意力机制学不到有意义的权重;K太大,计算量上去不说,还会引入太多噪声邻居。论文在S3DIS数据集上做了K值的消融实验,K=16时mIoU最高,K=8和K=32都掉了1个点左右。我的经验是,对于室内场景(S3DIS),K=16到24比较合适;对于室外场景(SemanticKITTI),因为点云稀疏,K可以取到32甚至48。

还有一个容易被忽略的点:KNN的实现方式。如果用暴力搜索,复杂度是O(N^2),N是点数,对于几万个点的场景还能忍,但对于几十万个点的室外场景就太慢了。实际实现时要用KDTree或者近似最近邻库(比如faiss),把复杂度降到O(N log N)。论文里没提这个,但开源代码里用的是KDTree。

3.3 注意力系数的数值稳定性处理

注意力系数在softmax之前是未归一化的logits,如果直接算exp,数值容易溢出。论文里没写怎么处理,但标准做法是减去邻域内的最大值,再做exp和归一化。这个操作在实现时一定要加,否则训练到中期loss会突然变成NaN。

另外,LeakyReLU的负斜率论文用的是0.2,这个值对注意力分布有影响。斜率太小,负logits被压得太狠,注意力会过度集中在一两个邻居上;斜率太大,负logits的区分度不够,注意力又太平均。我试过0.1和0.3,都不如0.2稳。这个超参在GAT的原始论文里也是0.2,算是经验值。

还有一个细节:注意力系数的dropout。论文在训练时对注意力系数做了dropout,比例是0.5,但只在训练时做,推理时关掉。这个操作能防止网络过度依赖某几个邻居,提升泛化能力。我试过去掉这个dropout,在S3DIS上mIoU掉了0.8左右,说明它确实有用。

4. 实验复现中的关键参数与踩坑记录

4.1 数据集准备与预处理

论文在S3DIS和SemanticKITTI两个数据集上做了实验。S3DIS是室内场景,每个房间切成1m×1m的块,每个块采样4096个点。SemanticKITTI是室外场景,每帧大概12万个点,论文只用了前11个类做分割。

预处理这块有个坑:S3DIS的原始数据里,每个点的坐标是绝对坐标,直接送进网络会导致不同房间的坐标范围差异很大。论文的做法是减去房间的中心点坐标,做归一化。这个操作看起来简单,但不做的话训练很难收敛。我一开始忘了做,loss震荡得厉害,加了之后很快就稳了。

SemanticKITTI的预处理更麻烦,因为点云是360度扫描的,需要先做姿态校正,把点云转到统一坐标系。论文里没细说,但开源代码里用了IMU数据做校正。如果没有IMU,可以用地面拟合的方式估计姿态,但精度会差一些。

4.2 训练超参与调参经验

论文用的优化器是Adam,初始学习率0.001,每20个epoch衰减0.7。batch size在S3DIS上是16,在SemanticKITTI上是4(因为每帧点数太多,显存放不下)。训练epoch数在S3DIS上是100,SemanticKITTI上是50。

这里有个实际调参的经验:学习率衰减的步长比衰减率更重要。我试过每10个epoch衰减0.5,结果训练后期loss下降太慢,mIoU比论文低1.5个点。后来改成每20个epoch衰减0.7,跟论文一致,结果就复现出来了。所以复现论文时,超参尽量别乱改,尤其是学习率相关的。

另一个坑是batch normalization。GACNet在每层图注意力卷积后面加了BN,但BN在点云上的行为和图像上不太一样。因为点云的batch内点数不一致(尤其是SemanticKITTI),BN的统计量会有偏差。论文的做法是在每个batch内把所有点的特征拼起来算均值和方差,而不是按样本算。这个实现细节在开源代码里有,但论文正文没提。

4.3 显存优化与推理加速

GACNet的显存占用主要来自KNN和注意力矩阵。对于4096个点的块,K=16,注意力矩阵是4096×16,不算大。但对于SemanticKITTI的12万个点,即使下采样后也有几万个点,注意力矩阵就很大了。论文的做法是在每个batch内只取一部分点做注意力,其余点用随机采样。这个操作会损失一些精度,但显存能降下来。

推理加速这块,KNN是瓶颈。用KDTree构建一次大概要几十毫秒,对于实时应用还是太慢。我试过用近似最近邻(ANN)替代精确KNN,速度能快3倍,mIoU只掉0.3左右,性价比很高。如果对精度要求没那么苛刻,ANN是更好的选择。

还有一个加速技巧:把注意力系数的计算从逐点循环改成矩阵运算。论文的公式看起来是逐点算的,但实际实现时可以批量算。具体来说,把所有中心点和邻居点的特征拼成两个大矩阵,然后用广播机制算注意力logits,最后用segment softmax归一化。这样能充分利用GPU的并行能力,速度能快5倍以上。

5. 消融实验背后的设计逻辑与改进空间

5.1 注意力机制到底贡献了多少

论文的消融实验里,去掉注意力机制(用固定权重)后,S3DIS上的mIoU从62.3掉到58.1,掉了4.2个点。这个差距在点云分割里非常显著,说明注意力机制确实是GACNet的核心贡献。但有意思的是,去掉残差连接只掉了2.3个点,去掉多尺度融合掉了1.8个点。这说明注意力机制的贡献远大于其他组件。

我自己的复现实验也验证了这一点。在S3DIS的Area 5上,用固定权重的图卷积,边界区域的mIoU只有45左右,而用注意力权重能到52。可视化结果里,固定权重的版本在桌子腿和椅子腿这些细长结构上分割得很差,注意力版本能把这些结构完整分出来。

5.2 注意力系数的可视化分析

论文里给了一张注意力系数的可视化图,显示在物体边界处,注意力会集中在同一类别的邻居上,而忽略不同类别的邻居。这个现象说明网络确实学到了语义层面的注意力,而不是简单的距离加权。

我自己也做过类似的可视化,发现一个论文没提的现象:在物体内部,注意力分布比较均匀,因为内部点的邻居基本都是同一类别;但在边界处,注意力会变得非常尖锐,几乎只关注一两个邻居。这个特性对于分割边界很有帮助,但也带来一个问题:如果边界处的邻居采样不好,注意力可能会聚焦到错误的点上。所以KNN的K值在边界处要适当大一点,给网络更多选择。

5.3 后续改进方向与个人思考

GACNet之后,点云分割领域出了很多新工作,比如KPConv、PointTransformer、StratifiedTransformer等。这些工作在不同程度上借鉴了GACNet的注意力思路,但做了改进。比如PointTransformer用了向量注意力,比标量注意力表达能力更强;KPConv用了可变形卷积,能自适应调整卷积核的形状。

从GACNet的角度看,我觉得还有几个可以改进的地方。一是注意力计算可以引入位置编码,让网络同时考虑特征相似度和空间距离;二是可以用多头注意力,每个头关注不同的邻域模式;三是可以把图注意力卷积和稀疏卷积结合起来,在保持精度的同时提升速度。这些想法有些已经被后续工作实现了,有些还有探索空间。

6. 代码实现中的关键片段与调试技巧

6.1 图注意力卷积层的PyTorch实现

下面是我自己实现的一个简化版图注意力卷积层,去掉了论文里的一些工程优化,但核心逻辑是完整的。注意这里用了segment softmax,因为每个点的邻域大小可能不一样(尤其是边界点)。

import torch import torch.nn as nn import torch.nn.functional as F class GraphAttentionConv(nn.Module): def __init__(self, in_channels, out_channels, k=16): super().__init__() self.k = k self.conv = nn.Conv2d(in_channels * 2, out_channels, 1) self.attn = nn.Conv2d(in_channels * 2, 1, 1) self.bn = nn.BatchNorm2d(out_channels) self.leaky_relu = nn.LeakyReLU(0.2) def forward(self, x, knn_idx): # x: [B, C, N], knn_idx: [B, N, K] B, C, N = x.shape K = self.k # 构建邻居特征 x_neighbor = x.transpose(1, 2) # [B, N, C] x_neighbor = x_neighbor.unsqueeze(2).expand(-1, -1, K, -1) # [B, N, K, C] x_center = x.transpose(1, 2).unsqueeze(2).expand(-1, -1, K, -1) # [B, N, K, C] # 拼接中心点和邻居点特征 x_cat = torch.cat([x_center, x_neighbor], dim=-1) # [B, N, K, 2C] x_cat = x_cat.permute(0, 3, 1, 2) # [B, 2C, N, K] # 计算注意力系数 attn_logits = self.attn(x_cat) # [B, 1, N, K] attn_logits = self.leaky_relu(attn_logits) # segment softmax:每个点的邻域内归一化 attn_weights = F.softmax(attn_logits, dim=-1) # [B, 1, N, K] # 特征聚合 x_neighbor = x_neighbor.permute(0, 3, 1, 2) # [B, C, N, K] x_agg = (x_neighbor * attn_weights).sum(dim=-1) # [B, C, N] # 残差连接 x_out = self.conv(x_cat) # [B, out_channels, N, K] x_out = x_out.mean(dim=-1) # [B, out_channels, N] x_out = self.bn(x_out) return x_out + x_agg

这段代码里有个细节:x_out的计算用了x_cat的均值,而不是直接对x_agg做卷积。这是为了保留中心点和邻居点的拼接信息,论文里也是这么做的。另外,attn_weights的维度是[B, 1, N, K],跟x_neighbor的[B, C, N, K]做广播乘法,得到[B, C, N, K],再在K维度求和,得到[B, C, N]。

6.2 KNN构建的加速实现

KNN构建是GACNet的瓶颈之一,下面是一个用KDTree加速的实现。注意这里用了scipy.spatial.cKDTree,比暴力搜索快很多。

import numpy as np from scipy.spatial import cKDTree def build_knn(xyz, k=16): # xyz: [N, 3] tree = cKDTree(xyz) _, idx = tree.query(xyz, k=k+1) # 查询k+1个,第一个是自身 idx = idx[:, 1:] # 去掉自身 return idx # [N, K]

这个实现对于几万个点的场景已经够用了,但如果点数超过10万,建议用faiss的GPU版本,速度能再快一个数量级。另外,cKDTree的query方法默认返回的是欧氏距离,如果需要其他距离度量,要自己实现。

6.3 训练时的常见报错与修复

训练GACNet时最容易遇到的报错是CUDA out of memory,尤其是在SemanticKITTI上。解决办法有三个:一是减小batch size,二是减小K值,三是用梯度累积。我一般先用梯度累积,因为不影响精度,只是训练慢一点。

另一个常见报错是NaN loss,通常是因为注意力系数溢出。检查一下softmax之前有没有减最大值,如果没有,加上就好了。还有一个可能是学习率太大,尤其是用Adam的时候,初始学习率超过0.001容易炸。我试过0.002,前几个epoch就NaN了,降到0.001就稳了。

最后一个坑是BN的track_running_stats。如果在训练时用了小batch size,BN的统计量会不准,推理时mIoU会掉。解决办法是用GroupNorm替代BN,或者用SyncBN。我试过GroupNorm,mIoU跟BN差不多,但训练更稳,推荐试试。

7. 从GACNet看局部特征聚合的设计哲学

GACNet这篇论文给我最大的启发不是注意力机制本身,而是它对待点云局部结构的方式。点云跟图像最大的区别在于,图像的局部结构是规则的,卷积核的形状是固定的;点云的局部结构是不规则的,每个点的邻域形状都不一样。GACNet用图注意力卷积来处理这种不规则性,本质上是在说:局部特征聚合的权重不应该由人工设计,而应该由数据驱动。

这个思路放到今天看依然很有价值。后来的PointTransformer、StratifiedTransformer等工作,虽然用了更复杂的注意力机制,但核心思想跟GACNet是一脉相承的。甚至可以说,GACNet是点云注意力机制的开山之作之一,它把NLP里的注意力机制成功迁移到了点云领域,并且针对点云的特性做了边级别的设计。

从工程角度看,GACNet的实现难度适中,比PointNet++复杂一些,但比后来的Transformer系列简单很多。对于想入门点云分割的人来说,GACNet是一个很好的学习对象,因为它既包含了图卷积的基本概念,又引入了注意力机制,而且代码量不大,容易复现。

我在实际项目里用GACNet做过室内场景的语义分割,效果确实比PointNet++好,尤其是在物体边界处。但它的推理速度是个问题,KNN和注意力计算都比较耗时。如果对实时性有要求,建议用轻量化的版本,比如减小K值、减少层数、用ANN替代KNN。这些取舍需要根据具体场景来定,没有一刀切的方案。

最后分享一个调试技巧:如果训练时mIoU一直上不去,先检查KNN的构建是否正确。我遇到过好几次KNN的索引搞错了,导致邻居点全是错的,网络根本学不到东西。检查方法很简单,把KNN的结果可视化出来,看看邻居点是不是真的在中心点附近。这个步骤花不了几分钟,但能省掉很多瞎调参的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询