
1. 先把RandLA-Net这个项目看清楚RandLA-Net是一篇发表在CVPR 2021上的3D点云语义分割工作全称是RandLA-Net: Efficient Semantic Segmentation of Large-Scale Point Clouds。当时它和其他方法的本质区别在于一点——它证明了随机采样在点云分割里也能打而且是高效地打不需要像PointNet那样搞繁重的最远点采样也不用像SPG那样把点云切成超图来做。这个思路在当时候选方案里算是比较反直觉的。因为从PointNet开始主流观点是“采样策略决定感受野的质量”FPS最远点采样这类方法能保证采样点均匀覆盖空间随机采样虽然快但容易丢信息。RandLA-Net给出的答案是既然随机采样必然会丢信息那我们就在特征层面把信息补回来。所以它的核心不是采样本身而是采样之后的局部特征聚合模块Local Feature Aggregation简称LFA。拿SemanticKITTI来说这个数据集是KITTI Vision Benchmark Suite里专门用于自动驾驶场景语义分割的扩展包含22个序列的点云序列其中序列00到10用于训练11到21用于测试。每帧点云大概是10万到13万个点一帧数据量和ShapeNet那种单物体点云完全不是一个量级用FPS的话单帧耗时根本扛不住。S3DIS则是室内场景数据集来自6个大型室内建筑区域共271个房间每个房间的点云密度极高且分布非常不均匀墙壁、地板这类大平面动辄几十万点。这两类数据放在一起复现正好能把RandLA-Net的两个核心能力都验证一遍室外大规模稀疏点云的高效处理以及室内高密度点云的稳定分割。这篇文章我会从环境搭建、数据集预处理、代码解读、训练调参到坑点排查完整过一遍我的复现过程希望能帮你省掉至少一周的摸索时间。2. 环境准备与依赖版本选择2.1 环境版本匹配表复现深度学习项目最怕的就是版本错位RandLA-Net这个项目虽然代码开源但它是2020年写的依赖的老版本库跟你现在机器上的新版环境大概率会有冲突。我自己的环境配置供你参考实测下来可以稳定跑通训练和推理组件版本说明Ubuntu20.04 LTS18.04也可以但20.04对CUDA 11.x支持更友好Python3.83.9以上部分依赖可能出问题CUDA11.111.0到11.3均可注意和PyTorch版本匹配PyTorch1.8.0官方代码运行时是基于这个版本写的GCC7.5编译C扩展时用到9.0以上可能报错显存11GB以上2080Ti/3080等SemanticKITTI单卡能跑但Batch Size受限有一点要特别说清楚官方仓库里的代码结构比较老没有用setup.py统一管理依赖你直接clone下来之后需要自己装依赖库主要包括numpy、scipy、torch、torchvision、open3d、pandas、sklearn、tensorboardX这些。其中tensorboardX如果版本太新会和PyTorch 1.8不兼容建议锁定0.4版本。2.2 编译C扩展层RandLA-Net的代码里有个关键部分需要编译C/CUDA扩展就是Grid Sampling和Nearest Neighbor搜索这两个操作。官方在helper工具目录下提供了编译脚本但这部分经常出问题因为作者用的PyTorch版本和编译器比较老新版环境下会报各种奇怪的错误。我踩过的坑是这样的直接跑sh compile.sh会报AT_CUDNN_ENABLED未定义的错误这个在PyTorch 1.8之后的版本里已经移除了相关宏定义。解决办法是打开tool/knn.py和tool/grid_subsampling.py里的源码把#include ATen/cudnn/cudnn.h这一行注释掉然后重新编译。编译成功之后建议马上跑一个20行的测试脚本验证基本算子的输出维度是否正确我之前遇到过一次编译成功但输出shape不对的隐性bug后面运行到损失函数计算那一步才暴露排查起来非常痛苦。2.3 SemanticKITTI数据集下载与格式说明SemanticKITTI的数据获取需要去它的官网注册申请这个申请是人工审核的大概一两天会通过邮件发下载链接。数据集本体大小在80GB左右包含velodyne点云数据、label数据、calib标定文件和poses位姿文件你需要确认下载的是语义分割对应的那部分。下载后解压的目录结构按官方要求应该是SemanticKITTI/ ├── dataset/ │ ├── sequences/ │ │ ├── 00/ │ │ │ ├── velodyne/ │ │ │ ├── labels/ │ │ │ ├── calib.txt │ │ │ ├── poses.txt │ │ │ └── times.txt这里的velodyne目录下是每一帧的.bin文件每帧大约是10万点每个点有4个维度x、y、z坐标和反射强度。labels目录下是对应的.label文件里面存的是每个点的标签ID。注意标签文件里的数值并不是直接的语义类别它经过了编码需要用官方提供的映射表转换。官方代码里已经写好了数据加载和标签映射的部分核心是把原始标签ID映射到19个类别包含unlabeled在内比如0对应unlabeled、1对应car、2对应bicycle等等。这个映射表在semantic-kitti-api项目中有详细定义建议直接复用不要自己写。3. 数据预处理为什么必须先生成训练样本3.1 体素下采样与块裁剪策略RandLA-Net的训练方式是“下采样裁剪”两步走这一步和很多人的直觉不太一样。你可能会想点云分割不就像图像分割一样直接整图丢进网络吗但点云数据每帧点的数量不固定而且动辄十万没法直接批量训练。RandLA-Net的处理方法是先把输入点云体素下采样到密度可控然后裁剪成固定数量的点作为训练样本。体素下采样这一步SemanticKITTI使用的体素大小是0.06m也就是把整个场景划分成6厘米见方的小立方体每个立方体内保留一个点通常是离体素中心最近的点这样能大幅减少点数同时保留几何结构。S3DIS的体素大小则是0.04m因为室内场景更密集需要用更小的体素保留细节。下采样之后数据生成脚本会以滑动窗口法从下采样后的场景中裁剪出固定点数通常是40960个点的子点云。裁剪时有一个重要的策略问题是随机裁剪还是固定网格裁剪看官方代码你会发现SemanticKITTI数据集在训练时使用的是随机裁剪每个epoch对同一帧数据裁剪的位置都不同相当于做了数据增强。而S3DIS则提供了预先裁剪好的固定block。3.2 数据生成脚本的运行与产物以SemanticKITTI为例运行python utils/data_prepare_semantic3d.py之前需要先改脚本里的数据路径。官方代码是从Semantic3D数据集改过来的里面的路径变量需要手动指向你的SemanticKITTI目录。生成后的训练数据是以npy格式保存的每个样本包含xyz裁剪后的点云坐标shape为(N, 3)rgb点云颜色SemanticKITTI里替换为强度值labels每个点的语义标签proj_idx每个点在原始点云中的索引这个字段在验证时用到数据处理耗时大概如下SemanticKITTI的数据生成大约需要1到2小时S3DIS由于每帧点数更多大概需要3小时左右。如果机器内存不够大建议分批处理一次处理一个序列或一个Area。这里有个非常容易被忽略的细节数据生成时是否对xyz做了归一化。如果你看官方代码会发现它对坐标做了中心化处理也就是减去整个房间场景的均值。这一步很关键因为RandLA-Net的局部特征聚合是基于相对坐标计算的绝对坐标的大小会影响网络收敛。但要注意中心化时统计的是整个场景的均值不是每个裁剪块的均值否则会破坏空间关系。3.3 两种数据集的预处理差异对照处理项SemanticKITTIS3DIS数据来源车载激光雷达室内扫描仪单帧点数约10万-13万约100万-300万体素尺寸0.06m0.04m训练块大小40960点/块40960点/块类别数19有效类别13是否需归一化是按序列是按房间坐标维度xyz强度xyzrgb4. RandLA-Net核心原理与代码实现解读4.1 局部特征聚合模块LFA的工作机制要真正复现RandLA-Net并且理解它为什么有效必须吃透LFA模块。LFA由三个子模块组成局部空间编码、注意力池化、扩张残差块。局部空间编码做的事情是这样的对于中心点p_i先通过K近邻搜索找到它的K个邻居点默认K16然后对每个邻居点计算相对位置坐标。这个相对坐标会和中心点的特征拼接在一起通过一个MLP映射成高维特征。这部分的思路是让网络显式感知局部几何结构。接下来是注意力池化这个设计是LFA的精华。传统的MaxPooling会丢掉大部分响应信息只保留最大响应。注意力池化则是先通过一个共享MLP为每个邻居点计算一个注意力分数然后用softmax归一化最后对邻居点的特征做加权求和。这样网络可以自适应地决定每个邻居点的贡献权重。但单纯的LFA模块感受野还是太小只有K个邻居。所以RandLA-Net设计了扩张残差块它把多个LFA模块串联起来每一层都在特征空间内重新搜索邻居。由于特征经过变换后距离度量也发生了变化所以在特征空间里做KNN实际上相当于扩大了空间感受野。作者做了消融实验证明三个扩张组的效果比单个LFA提升明显mIoU大约提高2到3个点。4.2 网络整体架构与下采样路径RandLA-Net采用类似U-Net的编码器-解码器结构但下采样方式用的是随机采样而不是卷积步长。编码器有5层每一层通过随机采样将点的数量减半同时通过LFA模块增加特征通道数。从输入层的8维特征xyz、强度、以及LFA产生的特征逐步扩展到64、128、256、512维。解码器部分则通过k近邻插值法逐步恢复点的数量每层将点的数量翻倍。与PointNet不同的是RandLA-Net中的上采样不是简单的特征复制而是利用邻域特征加权插值相当于一种可学习的插值方式。跳跃连接采用的是拼接策略。整个网络在SemanticKITTI的配置下参数量大约是1.2M左右相比其他3D分割网络算是很轻量的这也是它能处理大规模点云的原因之一。端到端推理一帧10万点的时间在2080Ti上大约是40ms到60ms这个速度在当年是非常有竞争力的。4.3 损失函数与评估指标RandLA-Net使用的损失函数是加权交叉熵损失。为什么要加权因为点云场景的类别极不均衡比如SemanticKITTI中道路、建筑这类背景类别占据绝大多数点而行人、自行车等类别占比不到1%。如果不加权网络会偏向预测高频类别。代码里权重计算使用的是中位数频率平衡法每个类别的权重等于整个训练集中该类出现频率的中位数除以该类频率。但是官方代码里有个细节它对权重做了平方根处理作者在论文里说这样可以避免权重过大导致训练不稳定。评估指标方面项目报告使用的是mIoUmean Intersection over Union和OAOverall Accuracy。这两个指标在有类别不平衡时会给出不同的视角mIoU对每个类别的IoU取平均所以小类别表现差会明显拉低mIoUOA则是所有点中预测正确的比例容易被高频类别主导。复现时建议两个都报告别只报OA。4.4 关键代码调用逻辑梳理从训练入口train.py出发整个调用链大致是# train.py 简化流程 model RandLANet(d_in, num_classes) # 定义网络 dataset SemanticKITTI_Dataset(...) # 加载预处理后npy dataloader DataLoader(dataset, batch_size4, shuffleTrue) # 每个batch内的处理 # 1. 从npy中随机选择4个训练块 # 2. 每个块内的点云经过数据增强随机旋转、缩放、抖动 # 3. 输入网络得到每点类别预测 # 4. 计算加权交叉熵损失 # 5. 反向传播更新参数训练过程中的数据增强包括随机旋转围绕z轴0到360度随机旋转、随机缩放0.9到1.1倍、随机平移-0.5到0.5米。这些增强在代码里是通过numpy实现的不依赖GPU速度很快。需要特别提醒的是train.py中有一个参数叫val_repeats它控制验证时对同一场景进行多次随机裁剪的平均预测。把val_repeats调大比如5验证集的mIoU会有1到2个点的提升这是因为多次采样平均能降低裁剪块边界对预测的影响。但这个参数只影响验证不影响训练。5. 完整实操从数据到训练再到推理5.1 数据生成实操记录我以SemanticKITTI序列00到10作为训练集为例说下我的实操步骤。首先修改utils/data_prepare_semantic3d.py中相关路径把data_path指向存放序列的根目录save_path指向预处理产物保存目录。然后运行cd utils python data_prepare_semantic3d.py运行过程中你会看到每处理完一个场景就打印一次进度和耗时。序列00大约需要8到10分钟。处理完的npy文件会以场景ID命名每个场景可能被切分成多个块所以你会看到类似00_0.npy、00_1.npy的文件。S3DIS的预处理脚本是data_prepare_s3dis.py逻辑相同但路径和体素大小不同。这里提醒一个极易踩的坑如果你想从零复现实验且没有现成npy一定要提前确认磁盘空间。SemanticKITTI预处理后的npy文件大约需要60到80GBS3DIS大约需要100GB以上。如果你直接在原来的80GB原始数据上叠加磁盘很可能会爆掉。5.2 训练超参数配置与调优我把官方配置和我最终优化后的配置列个表参数官方默认我的配置说明batch_size44显存不足时可改为2learning_rate0.010.01初始学习率Adam优化器lr_decay0.950.95每epoch衰减5%weight_decay0.00010.0001L2正则化num_epochs100100SemanticKITTI建议100轮train_steps500500每epoch训练步数val_steps100100每epoch验证步数k_n1616K近邻个数num_layers55编码器层数num_points4096040960每块点数训练命令如下python train.py --dataset semantic_kitti --data_path /path/to/npy --save_path /path/to/logs --model_name RandLANet --batch_size 4训练SemanticKITTI时我前期用单张2080Ti跑每epoch大约40分钟100个epoch就是将近3天。后来换了3090之后每epoch缩到20分钟左右。如果你只是验证代码能不能跑通建议先把train_steps调为50num_epochs调为2确认整个流程无误后再开始正式训练。这里有一个训练策略上的经验官方代码在每100个epoch末尾会保存一次checkpoint但如果你在第50个epoch时验证mIoU已经在上升可以手动将best模型另存一份。因为后续epoch如果出现过拟合至少还能回到最优状态。我在实际训练中发现SemanticKITTI的最优验证mIoU通常出现在第70到90个epoch之间太早停下会欠拟合太晚训练可能会略微过拟合。5.3 验证与推理流程验证流程在test_semantic_kitti.py或test_s3dis.py中实现。它会把完整场景的点云按块切分后输入网络得到逐块的分割结果后将它们拼接回完整场景然后和真实标签计算mIoU和OA。因为完整场景可能非常大验证时也需要分批推理每次输入一个裁剪块。S3DIS的验证比较特殊因为官方协议里有Area5作为验证集的标准做法Area5训练时不可见用于测试泛化能力此外也有6折交叉验证的做法。复现时我建议直接用Area5作为验证集这样方便和论文中报告的结果做直接对比。推理时有个使用细节测试脚本中的test_batch_size不要设置得太大一般4或8就够了因为推理时每个点都要计算特征显存消耗不比训练小多少。5.4 推理结果的可视化与保存推理结束后脚本会把逐点预测结果保存为npy或txt文件。如果想可视化我推荐用Open3D它读取点云和渲染标签非常方便。代码大致是import open3d as o3d import numpy as np # points: (N, 3) 点云坐标 # labels: (N,) 每点预测标签 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(points) # 自定义颜色映射将标签ID映射为RGB colors label_to_color(labels) pcd.colors o3d.utility.Vector3dVector(colors) o3d.visualization.draw_geometries([pcd])颜色映射建议使用SemanticKITTI官方的color map它是在语义分割比赛中公布的不同类别的颜色区分度很好看起来不会一团浆糊。S3DIS的标签集中在地板、墙壁、桌子、椅子等类别颜色映射可以直接用评论区第三方实现。6. 常见问题与排查技巧实录6.1 显存不足OOM怎么解决复现这个项目最容易碰到的问题就是显存溢出。SemanticKITTI单帧点云10万点如果直接把整帧输入网络任何消费级显卡都会爆。解决方案有三种第一减小batch_size到2或1。RandLA-Net在训练时输入的是裁剪后的40960点块batch_size4时每step处理16万个点11GB显存勉强够用但如果你显存只有8GB那只能batch_size2。第二减小num_points比如从40960降到20480。对我来说这个方案效果明显显存占用可以减少约一半。但是要注意点数太少会损害分割精度因为网络能看到的上下文变少了。如果降到20480以下mIoU会明显下降。第三启用梯度累积。把有效batch_size保持为4但每step的batch_size设为1每4步更新一次梯度。这个方案在效果上等价于batch_size4但速度会慢一些。6.2 精度达不到论文报告值的原因分析很多人在复现后发现自己的mIoU比论文低5到6个点这通常是以下原因造成的第一训练轮数不足。论文报告的结果是训练100个epoch后得到的而且使用了多卡训练。如果你只跑了30个epoch就下结论说复现不了那结论下得太早了。RandLA-Net在SemanticKITTI上要到第50个epoch左右mIoU才会有明显提升。第二数据增强的随机种子差异。代码里用了全局random seed但这个seed设置了不等于每次运行完全一致因为numpy和PyTorch的随机数生成器是不完全同步的。如果你的实验结果和论文差1到2个点通常就是这个原因。第三类别权重的计算方式。官方代码的权重计算基于某一固定比例如果你没有完整遍历所有训练数据计算频率而是只采样了一部分权重就会偏。这会导致小类别比如摩托车、行人的预测概率被压低。第四训练集划分不一致。SemanticKITTI官方是序列00-10共11个序列作为训练集但有些版本代码默认只使用其中部分序列这会明显影响结果。6.3 数据加载慢到无法忍受RandLA-Net的训练过程是“边训练边裁剪”也就是每个epoch都从npy大文件中随机选块加载数据。如果npy文件很大I/O会成为瓶颈。我实测过直接把npy放在机械硬盘上一个epoch有60%的时间都花在数据加载上。解决办法有两个第一把npy放到SSD上这一步就能带来约3倍的训练加速。第二把数据加载逻辑改成提前把文件全部读入内存。用内存映射方式只加载需要的块但代码改动比较大。对于12GB以内的小场景直接将npy全部读入内存是可接受的。6.4 标签ID错乱问题SemanticKITTI的原始标签ID范围是0到251但其中只有少数ID是有效类别其他都是无效的忽略区域。数据预处理代码里会做ID映射转换如果你的训练数据没有经过映射网络会试图学习252个输出类别导致两个问题显存暴增和精度暴降。验证映射是否正确有一个简单方法对标签文件做histogram统计检查有效类别的ID范围和数量占比。SemanticKITTI中道路类别ID对应40和建筑类别通常占比最高如果看到标签ID集中在200以上而有效类别很少那多半是映射没执行。S3DIS也有类似问题它的原始标签是从0到12但训练代码内部会将坐标轴做对齐处理把房间坐标旋转到标准轴方向如果跳过这一步分割结果在墙壁类别上会混乱不清。6.5 训练loss正常但mIoU极低如果你的训练loss一直在下降但验证集mIoU始终在10%以下大概率是标签和数据配对错了。这种bug很隐蔽因为loss能正常收敛。排查方法是可视化从训练数据里随机抽出一块把它的点云坐标和标签一起可视化肉眼检查每个类别的颜色是否分布在该出现的位置。如果墙壁的标签被标成了地面说明数据生成阶段出了问题。我还遇到过一个诡异情况S3DIS数据集的Area5作为验证集时因为Area5房间结构与其他Area差异较大模型泛化能力不足导致mIoU偏低。这时不要急着怀疑代码问题可以先在训练集上做一个小型交叉验证确认代码本身没有bug。7. 关于训练细节的几个补充建议训练开始之前建议先确定你的目标是多少。参考论文的结果RandLA-Net在SemanticKITTI上报告了大约53到55的mIoU依据不同配置在S3DIS Area5上报告了大约68到70的mIoU。实际复现时由于硬件、框架版本、随机种子等问题结果偏差2到3个点是正常的。如果发现你的精度始终比论文低好几个点可以检查一下是否开启了混合精度训练。PyTorch的AMP在部分显卡上会降低点云分割任务的精度因为K近邻搜索中距离计算对精度比较敏感。我实测SemanticKITTI上开AMP会让mIoU下降3个点左右建议关闭AMP使用完整FP32训练。另外模型训练时数据增强的随机旋转角度范围对结果影响挺大。RandLA-Net默认是0到360度随机旋转这个对室外场景是合理的但对S3DIS这种室内正交结构明显的场景过大的旋转反而可能让网络学习到不正确的空间先验。我把S3DIS的旋转范围改小后比如正负30度mIoU有小幅提升。8. 我对RandLA-Net后续扩展的一些实践体会复现完成之后这个项目的价值不只是跑通两个数据集更在于为你后续的研究工作提供一个可修改的基座。RandLA-Net的处理范式已经被很多后续工作继承比如在某些最新的大规模点云分割框架里你依然能看到随机采样加局部特征聚合的影子。我自己后续做的一个小改动是把LFA模块中的注意力池化替换成更具表达力的自注意力机制在S3DIS上mIoU能再提升1到2个点但训练时间也涨了将近一倍。如果你的硬件资源充足值得试试在RandLA-Net框架上做类似的结构改进。如果你是想在业务场景中直接用这个模型我的建议是先在自己的数据上做一次简单的类别映射测试别直接拿开源的19类或13类权重套用。2D图像分类的预训练权重可以迁移但3D点云的语义类别与场景结构强相关跨数据集的迁移效果往往不理想。最后再分享一个小技巧RandLA-Net的checkpoint文件里保存了模型参数和优化器参数但如果你只保存了模型参数加载跑推理是没问题的如果要继续训练一定记得也要加载优化器参数否则学习率会重置导致后续训练出现振荡。我因为这个疏忽白白浪费过一个周末的训练时间。