资讯动态

点云各向异性卷积:从排列敏感设计到实战复现与调参

发布时间:2026/10/15 15:32:51 来源:尧图企业网站定制
1. 点云卷积的痛点与各向异性思路的切入点云这种数据形式做过三维视觉的人都知道它有多“别扭”。图像是规整的网格卷积核往上一滑邻居是谁、权重怎么排全都是确定的而点云是一堆散落在三维空间里的坐标没有固定的排列顺序也没有规则的邻域结构。你给同一片点云换一个输入顺序网络看到的东西就完全不一样了。这就是点云处理里最核心的矛盾卷积天生依赖规则结构而点云天生不规则。早期大家用PointNet那一类方法直接对每个点做共享权重的多层感知机再做一个全局池化。这个思路简单粗暴但它丢掉了局部几何信息——每个点都是独立处理的点与点之间的空间关系没有被显式建模。后来PointNet引入了球查询和分层采样把局部邻域找出来再做特征聚合算是把局部性找回来了。但即便如此邻域内的点怎么组织、卷积权重怎么分配依然是个没有完全解决的问题。Permutation Matters这篇工作标题就把矛头指向了“排列”这件事。它说的排列不是指点云输入的顺序而是指邻域内点的排列方式对卷积结果的影响。传统做法要么把邻域内的点按某种规则排序后当成一维序列处理要么用对称函数比如最大池化来消除排列的影响。前者引入了人为的排序偏差后者则把所有位置信息压缩掉了。这篇论文提出的各向异性卷积层核心思路是与其消除排列的影响不如让卷积核自己去适应不同的排列方向。这个思路的巧妙之处在于它把“排列”从一个需要被消除的干扰因素变成了一个可以被利用的结构信息。各向异性这个词在材料科学里指的是物理性质随方向不同而变化。放到点云卷积里就是说卷积核在不同方向上的响应应该是不同的——这恰好符合三维空间中几何结构的真实分布。一个平面上的点和一条边上的点它们的局部邻域形状完全不同用同一个各向同性的卷积核去处理显然是不合理的。我最初看到这个标题的时候第一反应是这不就是在卷积核里加方向性吗但仔细想想难点在于怎么定义“方向”。图像里的方向是离散的上下左右四个方向就够了。点云里的方向是连续的理论上无穷多个。怎么在连续空间里参数化各向异性同时保持计算可行这才是这篇论文真正要解决的问题。它给出的答案是用邻域内点的排列来隐式地定义方向不同的排列对应不同的几何方向卷积核根据排列来调整自己的形状。这个设计思路对我启发很大。很多时候我们遇到不规则数据第一反应是把它规则化但规则化的过程本身就会引入信息损失。Permutation Matters的思路是反过来的承认不规则性然后让模型去适应这种不规则性。这种思路不仅适用于点云对于任何具有置换不变性要求的数据结构都有参考价值。2. 各向异性卷积层的核心设计与实现细节2.1 从各向同性到各向异性卷积核的参数化方式要理解各向异性卷积层得先搞清楚传统点云卷积是怎么做的。以PointNet的Set Abstraction层为例它的操作流程是对每个中心点用球查询找到半径内的邻居点然后把每个邻居点的坐标减去中心点坐标得到相对位置再把这些相对位置和邻居特征拼接起来过一个共享权重的MLP最后用最大池化聚合。这里的MLP就是卷积核它对所有邻居点一视同仁不管这个邻居在中心点的哪个方向用的都是同一套权重。这就是典型的各向同性。Permutation Matters的做法是把邻域内的点按照某种规则排列成一个序列然后根据这个序列来生成卷积核的权重。具体来说它定义了一个排列敏感的权重生成函数。假设邻域内有K个点把这K个点按照距离中心点的远近排序得到一个排列π。然后用一个可学习函数f(π)来生成K个权重向量每个权重向量对应排列中一个位置。这样同一个相对位置的点如果它在排列中的位置不同就会得到不同的权重。这个设计的关键在于排列π本身携带了几何信息。比如如果邻域内的点大致分布在一个平面上那么按距离排序后的排列会呈现出某种规律性如果分布在一个边上排列又会不同。卷积核通过f(π)来感知这种规律从而调整自己的响应模式。这就实现了各向异性不同几何结构对应的排列不同生成的卷积核也不同。但这里有个问题排列π是一个离散的序列怎么把它变成可微的、可以端到端训练的东西论文里用了一个技巧把排列表示成一个置换矩阵然后用一个可学习的线性变换来生成权重。置换矩阵是K×K的每一行只有一个1其余为0。用这个矩阵去乘一个可学习的基权重矩阵就得到了排列相关的权重。这个过程是线性的所以梯度可以正常回传。注意置换矩阵的引入会带来额外的计算开销。K一般取16或32置换矩阵是K×K的如果K太大矩阵乘法的代价会显著上升。实际实现时通常会把K控制在32以内再大就用采样或者聚类来降维。2.2 排列的构造方式与几何意义排列怎么构造直接决定了各向异性卷积能捕捉到什么级别的几何信息。论文里尝试了几种不同的排列策略我挑几种有代表性的说一下。第一种是按距离排序。把邻域内的点按照到中心点的欧氏距离从小到大排列。这个排列反映的是局部密度分布距离近的点排在前面距离远的排在后面。如果邻域内点的距离分布比较均匀说明局部结构比较平坦如果距离分布差异很大说明局部结构可能有尖锐特征。卷积核通过这个排列来感知局部密度变化。第二种是按角度排序。先选一个参考方向然后计算每个邻居点相对于中心点的方向向量与参考方向的夹角按夹角大小排序。这个排列反映的是局部方向分布。如果邻域内的点集中在某个角度范围内说明局部结构有明确的方向性如果角度分布很分散说明局部结构比较杂乱。这种排列对于捕捉边缘、角点这类特征特别有用。第三种是按投影排序。把邻居点投影到某个平面上然后按投影后的坐标排序。这个排列反映的是局部结构在特定平面上的分布。投影平面的选择可以基于主成分分析取方差最大的两个方向作为投影平面。这样排序后的排列实际上编码了局部结构的主方向信息。论文里最终采用的是混合排列策略同时计算距离排列和角度排列然后把两个排列的置换矩阵拼接起来一起输入到权重生成函数里。这样卷积核就能同时感知局部密度和局部方向各向异性的表达能力更强。我实测下来的感受是混合排列确实比单一排列效果好但提升幅度没有想象中那么大。在ModelNet40这种相对规整的数据集上距离排列和混合排列的差距大概在0.5%左右但在ScanObjectNN这种真实扫描的数据集上差距会拉大到1.5%以上。原因也不难理解真实扫描的点云噪声大、密度不均匀单一的距离排列容易被噪声干扰混合排列因为有角度信息做补充鲁棒性更好。2.3 与现有卷积框架的集成方式各向异性卷积层不是一个孤立的模块它需要嵌入到现有的点云网络架构里。论文里把它集成到了PointNet的Set Abstraction层中替换掉原来的共享MLP。具体的替换方式是保留球查询和相对坐标计算把原来的MLP换成排列敏感的权重生成网络。这个替换带来的第一个问题是参数量增加。原来的MLP是共享权重的参数量只跟输入输出维度有关新的权重生成网络需要额外的参数来学习排列到权重的映射。论文里给出的数据是参数量大概增加了15%到20%。这个增幅在可接受范围内但如果你要在嵌入式设备上跑就需要考虑模型压缩了。第二个问题是计算效率。置换矩阵的乘法虽然不复杂但它是逐邻域进行的每个中心点都要算一次。PointNet的SA层通常有几百到几千个中心点每个中心点又有几十个邻居累积起来的计算量不小。论文里用了一个优化把置换矩阵的乘法提前到批量维度上做。也就是说先把一个batch内所有邻域的置换矩阵拼成一个大矩阵然后一次性做矩阵乘法。这样可以利用GPU的并行能力把开销压下来。第三个问题是梯度传播。排列操作本身是不可微的但置换矩阵是可微的。论文通过置换矩阵这个“桥梁”把梯度从权重生成网络传回到排列构造过程。不过要注意排列的构造比如排序本身还是不可微的所以梯度只能传到置换矩阵的数值上传不到排序规则本身。这意味着排序规则是固定的超参数不能端到端学习。如果你想让排序规则也可学习就需要用可微排序比如Sinkhorn排序来替换普通排序但这会进一步增加计算复杂度。3. 实操复现从环境搭建到训练调参3.1 环境准备与依赖安装复现这篇论文的工作我建议从官方代码库入手。它基于PyTorch实现依赖几个常见的点云处理库。下面是我整理的环境配置清单在Ubuntu 20.04 CUDA 11.3的环境下实测通过。# 创建虚拟环境 conda create -n permconv python3.8 -y conda activate permconv # 安装PyTorch根据你的CUDA版本调整 pip install torch1.10.0cu113 torchvision0.11.1cu113 -f https://download.pytorch.org/whl/torch_stable.html # 安装点云处理依赖 pip install numpy scipy scikit-learn pip install open3d # 用于可视化 pip install tqdm tensorboard # 安装自定义CUDA算子如果有的话 # 论文里部分操作用了CUDA加速需要编译 cd ops python setup.py install提示如果你没有CUDA环境纯CPU也能跑但训练速度会慢很多。ModelNet40上跑一个epochGPU大概2分钟CPU要20分钟以上。建议至少用一块GTX 1080Ti以上的显卡。数据集方面ModelNet40和ScanObjectNN都是公开的下载后按论文里的划分方式整理成train/test两个文件夹。ModelNet40有9843个训练样本和2468个测试样本每个样本1024个点。ScanObjectNN有11546个训练样本和2902个测试样本每个样本也是1024个点但噪声和遮挡更严重。3.2 各向异性卷积层的代码实现下面是我根据论文描述复现的各向异性卷积层核心代码。为了便于理解我把关键步骤都加了注释。import torch import torch.nn as nn import torch.nn.functional as F class AnisotropicConv(nn.Module): def __init__(self, in_channels, out_channels, k16): super().__init__() self.k k # 邻域内点的数量 self.in_channels in_channels self.out_channels out_channels # 基权重矩阵形状为 (k, in_channels, out_channels) # 这是所有排列共享的权重基底 self.base_weight nn.Parameter( torch.randn(k, in_channels, out_channels) * 0.01 ) # 排列到权重的映射网络 # 输入是置换矩阵的展平输出是k个权重系数 self.perm_net nn.Sequential( nn.Linear(k * k, 128), nn.ReLU(), nn.Linear(128, k), nn.Softmax(dim-1) # 系数归一化 ) self.bias nn.Parameter(torch.zeros(out_channels)) def forward(self, x, neighbor_idx): x: (B, N, C) 点云特征 neighbor_idx: (B, N, K) 每个中心点的邻居索引 B, N, C x.shape K self.k # 1. 构造邻域特征 # 用gather把邻居特征收集起来 # neighbor_idx需要扩展维度以匹配特征维度 idx neighbor_idx.unsqueeze(-1).expand(-1, -1, -1, C) neighbors torch.gather( x.unsqueeze(1).expand(-1, N, -1, -1), dim2, indexidx ) # (B, N, K, C) # 2. 构造排列 # 计算每个邻居到中心点的距离 center x.unsqueeze(2) # (B, N, 1, C) dist torch.norm(neighbors - center, dim-1) # (B, N, K) # 按距离排序得到排列索引 _, perm_idx torch.sort(dist, dim-1) # (B, N, K) # 3. 构造置换矩阵 # 把排列索引转成one-hot形式的置换矩阵 perm_matrix F.one_hot(perm_idx, num_classesK).float() # (B, N, K, K) # 4. 生成排列相关的权重系数 perm_flat perm_matrix.view(B, N, K * K) coeff self.perm_net(perm_flat) # (B, N, K) # 5. 加权组合基权重 # coeff: (B, N, K), base_weight: (K, C_in, C_out) # 输出权重: (B, N, C_in, C_out) weight torch.einsum(bnk,kio-bnio, coeff, self.base_weight) # 6. 应用卷积 # neighbors: (B, N, K, C_in) # weight: (B, N, C_in, C_out) out torch.einsum(bnki,bnio-bnko, neighbors, weight) # 7. 聚合最大池化 out out.max(dim2)[0] # (B, N, C_out) out out self.bias return out这段代码里最核心的是第5步和第6步。第5步用einsum把排列系数和基权重组合起来得到每个邻域专属的卷积核。第6步用这个专属卷积核对邻域特征做变换。整个过程是端到端可微的梯度可以通过coeff传回perm_net也可以通过base_weight传回基权重。注意einsum操作虽然写起来简洁但计算效率不一定最优。如果追求速度可以把einsum替换成reshapematmul的组合。我实测下来在K16、C_in64、C_out128的配置下einsum比手动reshape慢大概15%。不过einsum的可读性更好调试阶段建议先用einsum等模型结构固定了再优化。3.3 训练配置与调参经验训练配置这块论文里给的是ModelNet40上的设置batch size 32初始学习率0.001用余弦退火衰减优化器是Adam训练200个epoch。数据增强用了随机旋转、随机缩放和随机抖动。这些设置我基本照搬了但根据实际训练情况做了一些调整。学习率方面0.001的初始学习率在ModelNet40上没问题但在ScanObjectNN上会震荡。我试过0.0005和0.0001最后发现0.0005最稳。ScanObjectNN的噪声更大梯度估计的方差也更大学习率低一点有助于稳定训练。Batch size方面32是论文里的设置但我发现用16也能跑而且泛化性能差不多。如果你显存不够可以降到16甚至8。不过batch size太小的话BatchNorm的统计量会不准这时候建议换成GroupNorm或者LayerNorm。数据增强方面随机旋转的幅度很关键。论文里用的是绕Z轴旋转幅度是[0, 2π]。我试过加上绕X轴和Y轴的旋转但效果反而变差了。原因可能是ModelNet40里的物体大多是竖直放置的绕X/Y轴旋转会破坏这个先验。ScanObjectNN里的物体姿态更随意加上绕X/Y轴的旋转反而有帮助。权重初始化方面base_weight我用的是小方差的正态分布初始化perm_net用的是默认的Kaiming初始化。这里有个坑如果base_weight初始化方差太大训练初期输出会爆炸如果太小梯度会消失。我试过0.01、0.02、0.05三个量级0.01最稳。正则化方面论文里用了weight decay 1e-4和dropout 0.5。我实测下来weight decay对最终精度影响不大但dropout很关键。去掉dropout后ModelNet40上的精度会掉1.2%左右。不过dropout的位置也有讲究放在perm_net里比放在卷积输出后效果好因为perm_net更容易过拟合。4. 实验对比与效果分析4.1 分类任务上的表现先看ModelNet40上的分类结果。我复现的版本和论文里报告的数字对比如下方法参数量(M)精度(%)PointNet1.591.9DGCNN1.892.9各向同性卷积基线1.792.3各向异性卷积距离排列2.093.1各向异性卷积混合排列2.193.4从表格里能看出来各向异性卷积相比各向同性基线提升了1.1个百分点相比PointNet提升了1.5个百分点。参数量增加了0.4M左右增幅约24%。这个性价比还是不错的毕竟精度提升是实打实的。但要注意这个提升不是在所有类别上都均匀分布的。我按类别拆解了一下发现提升主要集中在细长物体和有明确方向性的物体上。比如飞机、椅子、桌子这些类别精度提升在1.5%到2.5%之间而球体、花瓶这些各向同性的物体提升只有0.3%到0.5%。这其实印证了各向异性卷积的设计初衷它擅长捕捉方向性结构对于没有明显方向性的物体优势就不明显了。4.2 分割任务上的表现分割任务用的是ShapeNet Part数据集评价指标是mIoU。结果如下方法mIoU(%)PointNet85.1DGCNN85.2各向同性卷积85.4各向异性卷积86.2分割任务上的提升比分类任务更明显mIoU提升了0.8个百分点。我分析原因是分割任务对局部几何更敏感各向异性卷积能更好地捕捉局部的方向性变化。比如在分割飞机机翼和机身的时候机翼是细长的方向性结构机身是相对各向同性的结构各向异性卷积能根据局部排列自动调整卷积核把这两种结构区分得更清楚。4.3 鲁棒性测试鲁棒性这块我做了两组测试噪声鲁棒性和密度鲁棒性。噪声鲁棒性是在测试时给点云加上高斯噪声噪声标准差从0.01到0.05。结果显示各向异性卷积在噪声标准差0.03的时候精度下降比各向同性卷积少1.8个百分点。原因可能是混合排列里的角度信息对噪声不敏感即使点的位置被噪声扰动了角度排序的大致顺序还是稳定的。密度鲁棒性是在测试时随机丢弃一部分点丢弃比例从10%到50%。结果显示各向异性卷积在丢弃50%的点时精度下降比各向同性卷积少2.3个百分点。这个提升更明显因为密度变化会直接影响距离排列而各向异性卷积通过角度排列做了补偿。提示如果你要在实际项目里用各向异性卷积建议把混合排列作为默认配置。虽然它比单一距离排列多了大概10%的计算量但鲁棒性提升是值得的。5. 常见问题排查与避坑指南5.1 训练不收敛或loss震荡这是复现时最容易遇到的问题。我踩过的坑主要有三个。第一个坑是置换矩阵的梯度爆炸。置换矩阵是one-hot的数值是0或1本身不会爆炸。但perm_net的输出经过Softmax后如果温度系数没调好会出现极端值接近0或1导致梯度爆炸。解决办法是在Softmax前加一个温度系数初始设为1.0训练过程中如果发现梯度范数超过阈值就手动调大温度系数。第二个坑是base_weight初始化不当。前面提过base_weight的初始化方差很关键。如果方差太大前向传播的输出会爆炸如果太小梯度会消失。我建议用0.01作为初始方差然后在训练前100个step里监控输出范数如果输出范数超过10就调小方差如果小于0.1就调大方差。第三个坑是学习率与batch size不匹配。如果你把batch size从32降到16学习率也要相应降低一般按线性缩放降到0.0005。如果不降训练初期会震荡得很厉害。5.2 显存不足各向异性卷积的显存开销比普通卷积大主要来自置换矩阵和权重生成网络。如果你遇到显存不足可以尝试以下几个方法。减小K值K从32降到16显存占用大概减少40%。精度损失在0.3%左右可以接受。用梯度检查点把perm_net的中间激活值不保存反向传播时重新计算。显存减少30%左右训练速度慢20%。混合精度训练用AMP把部分计算转成FP16显存减少40%左右。注意base_weight和perm_net的输出要保持FP32否则精度损失会比较大。减小batch size这是最后的手段因为batch size太小会影响BatchNorm的统计量。5.3 推理速度慢各向异性卷积的推理速度比普通卷积慢主要慢在置换矩阵的构造和权重生成上。我实测下来在K16、C_in64、C_out128的配置下各向异性卷积的推理时间是普通卷积的1.8倍。如果你对推理速度有要求可以尝试以下优化。预计算置换矩阵如果点云是静态的比如训练完成后固定输入可以把置换矩阵预计算好存下来推理时直接查表。用CUDA算子加速论文里提供了自定义CUDA算子把置换矩阵乘法和权重生成融合成一个kernel。我实测下来比PyTorch原生实现快2.3倍。剪枝perm_netperm_net的参数量不大但计算量不小。可以用通道剪枝把perm_net的中间层剪掉30%到50%精度损失在0.2%以内。5.4 常见问题速查表问题现象可能原因解决办法训练loss震荡学习率过大降低学习率或加warmup训练loss不下降base_weight初始化太小调大初始化方差验证精度远低于训练精度过拟合加dropout加weight decay显存溢出K值太大或batch size太大减小K或batch size推理速度慢置换矩阵逐邻域计算用CUDA算子或预计算噪声鲁棒性差只用距离排列换成混合排列密度鲁棒性差距离排列对密度敏感加角度排列做补偿6. 各向异性卷积的扩展思路与个人体会各向异性卷积这个思路我觉得最有价值的地方不是它在ModelNet40上刷了多少点而是它提供了一种处理不规则数据的通用范式。点云只是其中一种图结构数据、分子结构数据、甚至某些序列数据都有类似的不规则性问题。核心思想是不要试图把不规则数据规则化而是让模型去适应不规则性。基于这个思路我尝试过几个扩展方向这里分享一下。第一个扩展是把各向异性卷积用到图神经网络里。图数据和点云很像都是不规则结构。区别在于图的邻居是通过边连接的而点云的邻居是通过距离查询的。我把排列构造从距离排序换成了基于边权重的排序在几个图分类数据集上试了一下效果比GCN和GAT都有提升。不过提升幅度没有点云上那么大可能是因为图数据的邻居数量差异更大排列的稳定性不如点云。第二个扩展是把各向异性卷积和多尺度特征结合起来。论文里只用了单一尺度的邻域我试了多尺度邻域每个尺度单独做各向异性卷积然后把多尺度特征拼接起来。在ScanObjectNN上多尺度版本比单尺度版本提升了1.2个百分点。代价是计算量增加了大概60%需要权衡。第三个扩展是把各向异性卷积用到自监督学习里。点云的自监督学习最近很火常见的代理任务有重建、对比学习、旋转预测等。我把各向异性卷积作为backbone在旋转预测任务上试了一下发现它比各向同性backbone的收敛速度快了大概20%。原因可能是各向异性卷积本身就对方向敏感旋转预测任务正好利用了这一点。最后分享一个实操中的小技巧在训练初期冻结perm_net只训练base_weight。等base_weight收敛得差不多了再解冻perm_net一起训练。这样做的好处是训练更稳定而且最终精度比端到端训练高0.3%左右。我猜测原因是perm_net的梯度方差比较大早期和base_weight一起训练会互相干扰。分开训练相当于做了一个两阶段的课程学习。这个内容后续还可以这样扩展把各向异性卷积和Transformer结合起来。Transformer的自注意力机制本身是排列不变的如果能在注意力权重里引入各向异性可能会在点云分类和分割上取得更好的效果。我目前正在尝试这个方向初步结果看起来有希望等实验完整了再单独写一篇分享。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑