ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PointNet++详解:从设计动机到训练实践,一篇讲透点云层次特征提取

PointNet++详解:从设计动机到训练实践,一篇讲透点云层次特征提取 PointNet这个名字在三维点云深度学习里基本是绕不开的。我自己项目里第一次用PointNet做家具分类时模型把“带扶手的椅子”和“不带扶手的椅子”混得一塌糊涂后来换成PointNet同样的数据规模、几乎同样的参数量准确率直接涨了十几个点。这个差异不是网络变深了而是PointNet把PointNet“一杆子捅到全局特征”的结构换成了逐层提取局部几何结构、再慢慢聚合的层次化结构。下面我会从一个实际使用者的角度把PointNet的设计动机、网络结构、复现参数和踩坑过程完整讲一遍。无论你是刚接触点云深度学习的学生还是要在项目里落地点云模型的工程师读完至少能回答几个问题PointNet到底改了什么、为什么这么改、以及真到自己训练推理时应该怎么设参数、怎么避坑。1. PointNet的全局特征为什么不够用从“桌椅不分”的失败案例说起1.1 点云识别和图像识别最大的不同图像是稠密且规则排列的像素网格卷积核天然作用在一个局部邻域上然后通过堆叠层把局部模式组合成全局语义。点云不一样它是一堆无序、稀疏、密度不均匀的三维点没有规则网格也没有天然的“从左到右、从上到下”顺序。早期PointNet的解决办法很直接对每个点做MLP升维再用一个最大池化把所有点的特征压成一个全局向量。这个对称操作保证了网络对点的输入顺序不敏感也天然支持任意点数所以PointNet成了奠基性工作。但它的问题同样明显——最大池化只保留每个特征通道里响应最强的那个点其他点的信息几乎全部被丢弃。打个比方你走进一间房间如果只允许你说出一个印象最深刻的视觉线索你可以说出“有张桌子”但你很难说清楚“桌子旁边有把椅子、椅子左侧有个台灯”这种空间关系。PointNet的全局maxpool就是这个“只允许说一个印象”的机制。1.2 PointNet的全局maxpool到底丢了什么具体来说PointNet把输入点云映射成若干通道的逐点特征然后对所有点的特征做逐通道max。这样得到的全局特征向量中第c个维度的值只来自某个特定点其他点在这个维度上的贡献被直接忽略。带来的实际后果有三类无法区分“物体的一部分”和“一个物体”。比如桌面上放着一个杯子和单独一个杯子两者的全局点云特征可能很接近因为桌面方向的特征通道只挑了“杯口圆形”这个最强响应而“桌面与杯子的相对位置”没有编码进全局向量。局部几何线索丢失。扶手的弧度、椅腿的支撑方式、桌面的边缘倒角这些细节往往只体现在局部点的相对位置上但全局maxpool根本不知道“局部”是什么。特征分不清位置和组合。两个结构完全相同的组件放在不同相对位置PointNet会给出几乎一样的全局特征。我当时遇到的“扶手椅和无扶手椅分不清”就是这么来的。椅子A和椅子B在大部分几何体上相似真正的区分点在扶手这个局部凸起结构上。PointNet的全局特征被坐垫和靠背这两个主要表面主导了扶手对全局特征的贡献太小。1.3 多尺度分组的灵感来源要解决这个问题最自然的想法就是模仿CNN的卷积池化结构先在小邻域内提取局部特征然后逐步扩大感受野最终在更高层做全局聚合。PointNet的核心贡献就是把这张“点云金字塔”搭了出来。它把PointNet原本的“单层全局抽象”拆成了多个阶段的“集合抽象”Set Abstraction简称SA。每经过一个SA层点的数量会减少相当于池化每个点的特征表示的区域范围会变大相当于感受野扩大。这样低层特征包含精细的局部几何高层特征包含更抽象的语义最后再做分类或者分割。这个思路听起来简单但实现里有三个关键问题需要解决怎么选中心点、怎么划分邻域、怎么从邻域提特征。这正是PointNet的集合抽象层要做的事。2. 集合抽象层Set Abstraction拆解FPS、球查询和局部PointNet各司其职2.1 FPS用“互相离得最远”的策略选中心点集合抽象层的第一步是从当前点云中选出一批中心点。PointNet没有用随机采样而是用了最远点采样Farthest Point SamplingFPS。FPS的贪心过程很简单先随机选一个点作为初始中心然后每次从剩余点里选一个距离当前所有已选中心点“最近距离”最大的点加入中心点集合。重复这个过程直到达到预设数量。这样选出来的中心点在几何空间里分布比较均匀能覆盖整个物体形状不会出现所有中心点都挤在一块的情况。实际代码里一般维护一个距离数组每次迭代更新每个点到当前中心集合的最小距离然后取最大值点。复杂度是O(NK)N是当前点数K是要采样的中心点数。在N1024左右时CPU上的循环也还好但N到8192甚至几万时FPS就成了训练瓶颈之一这个问题后面在踩坑部分展开。FPS只依赖坐标xyz与特征无关。这意味着它是纯几何下采样与CNN里按响应强度池化是两回事。也正因为这一点FPS采出的中心点不会因为某个局部有丰富特征而被过度采样保证结构覆盖优先。2.2 球查询固定半径的邻域定义选好中心点后就要为每个中心点定义一个局部邻域。PointNet原文用的是球查询Ball Query以每个中心点为球心以固定半径r画一个球落在球内的点都算作邻居。如果邻居数超过上限nsample就随机采样nsample个如果不足nsample就重复填充到nsample个并记录mask。为什么不用K近邻K近邻在每个中心点附近都取K个点看似省心但点云密度不均匀时同样取K个点在稀疏区域可能覆盖了很大一片空间在稠密区域只覆盖了一个很小的角落这样局部邻域的实际几何尺度就乱了。球查询固定了半径r相当于固定了“这个局部到底看多大范围”不同区域的局部特征才具有可比性。半径怎么定这是一个很关键的超参数。实践里通常先把点云归一化到单位球或者边长为2的立方体范围内然后令第一层半径是0.1到0.2高层逐层增大。如果直接输入真实尺度的坐标比如室内扫描点云范围是几十米那半径必须跟着放大否则每个球里可能一个邻居都没有。2.3 局部PointNet把邻域几何编成特征向量拿到邻域点集后下一步就是对每个局部邻域做特征提取。PointNet用的仍然是一个“小PointNet”首先把邻域内每个点的原始坐标减去中心点坐标得到相对坐标。这样做的意义是让网络聚焦于“中心点周围的几何形状”而不是绝对坐标位置。比如一个扶手的局部不管它出现在椅子的左边还是右边只要相对几何结构一致局部特征就一致这种局部不变性对物体识别很有价值。如果原始输入除了xyz还有颜色、法向量等属性一般也会把这些属性拼接到相对坐标后面一起送进MLP。然后对邻域内所有点做逐点MLP最后用maxpool聚合邻域特征得到这个中心点的局部特征向量。这里要特别强调局部PointNet的maxpool只作用于一个局部邻域内部而不是全局点云。所以它不会丢失“这是哪个局部区域”的信息——每个中心点都有自己独立的局部特征只是这些特征会在更高层被进一步聚合。2.4 一次完整SA的输入输出和实际配置一个标准的SA层由三部分组成FPS采样、球查询分组、局部PointNet。输入是N个点的坐标和特征输出是K个中心点的特征其中K远小于N。以PointNet分类任务常见配置为例SA层采样点数球查询半径邻域最大点数MLP输出通道SA15120.232[64, 64, 128]SA21280.464[128, 128, 256]SA31全局-[256, 512, 1024]第一层SA将1024个点下采样到512个中心点每个中心点聚合半径0.2内最多32个点的局部特征输出128维特征第二层再把512个点下采样到128个点感受野扩展到0.4输出256维特征第三层直接对128个点做一次全局聚合得到1个1024维全局特征再接全连接分类器。如果让我用伪代码概括sample_and_group的核心逻辑大概是这样的def sample_and_group(npoint, radius, nsample, xyz, points): # xyz: (B, N, 3) # points: (B, N, C) 或 None new_xyz farthest_point_sample(xyz, npoint) # (B, npoint, 3) group_idx ball_query(radius, nsample, xyz, new_xyz) # (B, npoint, nsample) grouped_xyz index_points(xyz, group_idx) # (B, npoint, nsample, 3) grouped_xyz grouped_xyz - new_xyz.unsqueeze(2) # 相对坐标 if points is not None: grouped_points index_points(points, group_idx) grouped_points torch.cat([grouped_points, grouped_xyz], dim-1) else: grouped_points grouped_xyz return new_xyz, grouped_points有了SA层的概念PointNet的核心结构就清楚了多个SA层堆叠点数递减、通道数递增最终形成一个编码器。3. 密度不一样怎么办MSG与MRG给出的两个不同答案3.1 非均匀密度为什么会让单尺度分组失效前面说的单尺度SA即论文里的SSG结构隐含了一个假设整个点云的密度大致均匀所以固定半径内的点数差不多局部特征可靠。但真实世界的点云很少均匀。激光雷达扫描一个室内场景近处的墙密密麻麻远处的家具稀疏模糊同一个物体上正面和背面的采样密度也可能差很多。如果第一层SA只用固定半径0.2在稠密区域球内可能有50个邻居在稀疏区域可能一个邻居都没有。这样稀疏区域的局部特征会变得非常不稳甚至退化成只有中心点自己的信息。FPS保证了中心点的空间覆盖均匀却保证不了每个中心点邻域内的点数量一致。所以PointNet论文特意讨论了这个密度兼容问题并给出了两个方案MSG和MRG。3.2 MSG多半径分组特征拼接MSG全称是Multi-Scale Grouping多尺度分组。它的做法很直观对同一个中心点用多个半径分别做球查询然后对每个尺度的邻域分别过一个局部PointNet得到多个局部特征向量最后把这些特征向量沿通道维度拼接起来作为该中心点的最终局部特征。以论文里的设置为例一个中心点可能同时用0.1、0.2、0.4三个半径提取特征三个尺度各自输出32维拼接后得到96维。这样即使某个尺度在稀疏区域失效其他更大尺度的特征还能补上。网络通过后续MLP可以自己学会在不同密度区域、不同尺度下怎么组合特征。MSG的好处是鲁棒性很强尤其适合输入点数多、尺度差异大的场景坏处也明显——每个尺度都要做一次分组和一次局部PointNet计算量和显存占用成倍增长。训练PointNet做S3DIS室内分割时如果每层都用三个尺度数据加载如果不做预计算显存真的会被撑爆。我在实际项目里测过同样数据量下MSG比SSG训练时间大约多一半以上。3.3 MRG用上一层特征补足小尺度的缺失MRG全称是Multi-Resolution Grouping多分辨率分组。它的思路比MSG更节省不直接用多个尺度的原始邻域特征而是综合“上一层特征”和“本层特征”两种分辨率。具体来说对第l层的某个中心点MRG会做两件事从本层用一个固定尺度邻域提取局部特征这相当于SSG当前层的特征在这个中心点的邻域内把上一层已经提取好的更稠密的点特征上一层点数更多、每个特征对应的区域更小再过一次PointNet聚合得到一个小尺度分辨率下的特征。然后把这两个特征拼接起来。也就是说MRG用“上一层更精细点的特征”代替了MSG里“额外一个小半径球查询”的工作。因为上一层特征已经在之前的SA层里算过了这里只是再做一次局部聚合所以计算量比MSG小很多。从效果来看MRG在密度不均匀的场景下能保留不错的性能又没有MSG那么贵所以适合显存紧张、又要处理真实扫描数据的场景。3.4 怎样在自己的任务里选SSG、MSG还是MRG我一般建议这样选应用场景推荐方案原因合成点云、模型数据集、密度均匀SSG速度快显存占用低精度已经足够真实扫描、点数多、密度差异大且显存充足MSG多尺度互补鲁棒性最强真实扫描、多尺度特征重要但显存紧张MRG较省资源密度适应性比SSG好注意MSG和MRG带来的收益在类别少、几何差异大的任务上不一定明显。如果只是分椅子、飞机、汽车这种类别差异很大的物体SSG完全够用强行上MSG反而拖慢训练速度。只有当类别之间差异很微妙比如椅子是否有扶手、或者场景里物体遮挡严重时多尺度带来的特征冗余才有价值。4. 把特征还回每一个点特征传播层在分割任务中的路由逻辑4.1 从稀疏中心点回到密集原始点距离加权插值SA层不断下采样后点的数量从原始N逐层减少。分类任务只需要最后一个全局特征但分割任务要求每个输入点都有一个标签所以需要一种“解码器”把高层稀疏特征传播回所有原始点。PointNet给出的是特征传播层Feature PropagationFP。它的核心操作是距离加权插值假设我们要把第l层的K_l个点特征传播回第l-1层的K_{l-1}个点对每一个待插值的点在已知特征点中找最近的3个中心点然后以这个点与3个中心点的空间距离平方倒数为权重对这3个点的特征做加权求和并归一化。这里用3个近邻点是有讲究的。三维空间中3个非共线点可以形成一个平面在这3个点之间的插值更接近真实几何关系距离平方加权则让越近的点对结果影响越大。如果只用最近1个点特征会非常“硬”容易出现标签边界锯齿如果用太多点又会让边界区域的特征被过度平均丢失细节。4.2 跳跃连接和Unit PointNet的作用单靠插值得到的特征还不够。原因在于高层特征虽然语义丰富但空间分辨率低几何细节已经在下采样过程中被天然抹掉了。如果直接把插值后的特征用于逐点分类物体边界会非常粗糙。PointNet的做法很像U-Net把特征传播层的结果与编码器中对应层保存的逐点特征做拼接形成跳过连接。这样每个点同时拥有来自高层的类别语义和来自低层的精细几何位置信息。拼接之后特征还要过一个“Unit PointNet”——本质是一个共享MLP加上BN和ReLU。这个MLP负责把高、低层特征真正融合起来同时也做了跨通道特征交互。论文里通常用两层MLP完成这个融合比如[256, 128]这样的结构。我个人理解没有跳跃连接的特征传播只能算“上采样”加了跳跃连接的FP才是真正的“语义重组”。后者让网络有机会在边界处“看到”底层几何而不是盲目相信高层插值。4.3 分割网络整体将编码器与解码器如何拼接PointNet分割网络的一般结构是“4层SA下采样 4层FP上采样”。一个常用配置大致是SA11024点半径0.1邻域32输出32维SA2256点半径0.2邻域32输出64维SA364点半径0.4邻域32输出128维SA416点半径0.8邻域32输出256维FP4把16点特征传回64点与SA3输出拼接融合为128维FP3把64点特征传回256点与SA2输出拼接融合为64维FP2把256点特征传回1024点与SA1输出拼接融合为32维FP1把1024点特征传回原始点数输出逐点特征再接分类器得到每点类别。在室内场景分割里这个结构既能覆盖大尺度空间关系如墙体、地面又能保留椅子腿、桌面边缘这类小尺度细节。相比直接对每个点做MLP分割mIoU往往能高出不少。5. 复现与训练PointNet必须知道的参数、增强与踩坑记录5.1 数据增强旋转、抖动、缩放一个都不能少点云模型和图像模型一样吃数据增强。我通常做的增强操作包括绕Z轴随机旋转任意角度因为很多点云任务特别是物体分类绕重力方向旋转不影响语义随机平移幅度在[-0.2, 0.2]之间能增加模型对位置变化的鲁棒性随机缩放比如0.8到1.25倍让模型适应不同尺度随机抖动也就是给每个点坐标加高斯噪声这能模拟传感器噪声随机丢弃一些点比如随机剔除5%到10%的点模拟遮挡和近处点缺失。需要注意的是如果任务本身有明确的方向语义比如室内场景中地面永远在y轴下方那绕Z轴旋转可以但绕X轴或Y轴旋转会破坏“地板在下、天花板在上”的先验反而不利于分割。增强不是越多越好必须和任务语义对齐。5.2 关键超参数一览点数、半径、batch size、学习率PointNet对超参数比较敏感主要关注这几个超参数常见取值说明输入点数num_points1024 / 2048 / 4096越多越准但显存和FPS耗时都涨第一层球查询半径0.1 ~ 0.2前提是点云已归一化到单位尺度邻域最大点数nsample16 / 32 / 64太小特征不稳太大计算量高batch size16 / 32小于8时需要注意BN稳定性学习率0.001Adam原论文默认Adam很多人也喜欢用SGD训练轮数200 ~ 300配合学习率余弦衰减更好一个我吃亏过的细节输入点云缩放范围不统一radius设多大都没意义。所以我会在数据预处理阶段做一次全局归一化把所有点云的最长边缩放到1这样才能让radius和nsample成为“可迁移”的配置。如果不同数据集尺度差异很大直接沿用论文里的0.2和0.4经常翻车。5.3 FPS太慢、空邻域、BN不稳定三个高频坑第一个坑FPS在PyTorch里如果用最朴素的循环实现N4096、K1024时一个样本就要几十毫秒批量训练时很拖后腿。原版TensorFlow实现和部分PyTorch实现都做了优化可以把FPS改成让每个中心点的最近距离通过矩阵运算一次性更新。如果点数特别大建议用更粗的网格降采样或者随机采样替代FPS然后用更多训练轮数去弥补。第二个坑球查询会出现空邻域。点云边界、稀疏区域、或者radius设太小时某些中心点的球内找不到足够点。如果代码不做重复填充训练时就会NaN或者梯度异常。常见做法是对不足nsample的邻域做有放回的随机重复采样同时维护一个mask在后续maxpool或loss计算里忽略无效点。更简单的做法是先把radius调大保证绝大多数邻域非空。第三个坑BN在小batch下不稳定。PointNet几乎每个MLP后面都跟BN如果batch size只有4或者2BN统计量抖动很厉害验证精度会时高时低。解决办法有三条用更大的batch、用SyncBN、或者干脆把BN换成GroupNorm并在某些层移除BN。我测试下来在batch size为8以上时原版BN基本没问题低于8建议优先换成GroupNorm或者用更大batch再调其他参数。还有一个小坑是混合精度训练。PointNet里FPS和Ball Query如果用half类型距离计算和索引容易出问题。我在A100上用AMP训练时遇到过精度下降的情况后来把FPS和分组部分的计算保留为fp32只让MLP部分走fp16问题就解决了。如果只是为了跑通实验可以先不用混合精度。6. PointNet之后我的一些反思6.1 它改变了点云学习的什么现在回看PointNet我认为它最重要的贡献不是某个具体算子而是确立了“局部几何聚合层级结构”这个范式。后面的PointCNN、DGCNN、PointTransformer甚至很多基于稀疏卷积的三维网络都在不同程度上延续了“找邻域、聚合邻域特征”的思路。区别只是聚合方式从MLP换成了图卷积、自注意力或者稀疏卷积。这也是为什么现在问一个点云模型入门建议很多人的回答仍然是“先跑通PointNet”。它的结构足够经典代码实现多从分类到分割再到部分分割都有完整开源参考是理解三维深度学习的理想起点。6.2 它解决不了的问题后来的模型又补了什么PointNet并非没有短板。它的局部聚合依赖固定半径对点云密度变化的适应是通过MSG/MRG这种“堆尺度”的方式实现代价是计算量它的maxpool聚合还是过于简单容易把邻域内多个不同物体的特征混在一起FPS采样在超大点云上也效率不高。因此后来的工作开始尝试可变形卷积、图注意力、Transformer等更强的聚合算子也出现了不少用体素或稀疏卷积替代原始点集处理的方案。但换个角度看这些后续模型的很多基础模块里仍然能找到PointNet的影子——中心点与邻域的概念、相对坐标编码、层级堆叠和特征传播。理解这一点后你会更容易读懂新论文里的模型结构它们大多是在回答PointNet某个环节“做得不够好”的问题。我现在的实践习惯是拿到一个新点云任务第一版一定先上PointNet做baselineSSG起步、数据增强做全、分类和分割两个跑通保证整个流程可控。等指标稳定之后再根据瓶颈决定要不要换更强的聚合器、要不要增加输入点数、要不要引入预训练特征。这套打法让我少走了很多弯路也希望对你有一点帮助。
返回列表