先说结论:PointNet++的环境配置本身不难,难点在于“你根本不知道哪一步会给你埋雷”。我前后在四台不同配置的机器上装过,Windows和Ubuntu都试过,踩过CUDA版本不匹配、gcc版本过高、ninja缓存玄学报错、S3DIS数据集下载到一半挂掉等各种问题。这篇文章把我最后稳定跑通的完整流程、版本组合、命令行逐条写清楚,顺便把训练和可视化里容易忽略的细节也一并交代完整。
这个项目适合正在做3D点云分类、语义分割、目标检测相关工作的同学,也适合刚入门点云深度学习但被环境劝退的初学者。我的建议是:先按文章给的版本组合把环境装好,用官方预训练参数跑一遍模型,再动手改代码。
1. 动手之前先把版本矩阵理清楚,这个环节省不得
很多人一上来就git clone仓库,然后pip install -r requirements.txt,结果编译报错开始疯狂搜Google。实际上PointNet++这类带CUDA自定义算子的项目,版本组合是确定的,先花十分钟确认自己的GPU驱动、CUDA、PyTorch三者关系,比事后排查省一天时间。
我用的是RTX 4090(24GB显存),Ubuntu 20.04系统,最终跑通的组合如下:
| 组件 | 版本 | 说明 |
|---|---|---|
| GPU驱动 | 535.104.05 | nvidia-smi查看 |
| CUDA Toolkit | 11.8 | 不需要和驱动完全一致,向下兼容 |
| Python | 3.8 | 3.9也能跑,但3.8踩坑最少 |
| Anaconda | 23.1.0 | 建议用conda管理虚拟环境 |
| PyTorch | 1.13.1 | cu118版本 |
| GCC/G++ | 7.5 | 默认9.4编译旧算子会报错,后面细说 |
先解释一下为什么nvidia-smi显示的CUDA版本和PyTorch要求的CUDA版本可以不一样。nvidia-smi里显示的是GPU驱动支持的最高CUDA版本,而PyTorch是自带CUDA运行时的,它只要求你的驱动版本不低于它需要的CUDA最低版本即可。所以驱动版本是CUDA 12.0也没关系,照样可以装cu118的PyTorch。
提示:如果你的机器之前装过其他深度学习框架,建议严格用conda新建独立环境,不要直接装在base环境里。我遇到过protobuf版本冲突导致训练时崩溃的案例,conda环境隔离是最省心的做法。
还有一点值得注意:PyTorch的版本不要追新。PointNet++的一些经典实现是在PyTorch 1.x时代写的,用2.x跑通常也能编译通过,但某些算子的张量形状推断会有微妙差异。如果不想折腾,直接按照我表格里的版本组合来。
驱动的安装这里不多讲,nvidia-smi能正常显示就说明驱动没问题。真正需要检查的是nvcc是否可用:
nvcc --version如果提示找不到nvcc,说明CUDA Toolkit没装或者没加到PATH里。但如果你像我一样用的是PyTorch自带的CUDA运行时,其实不装完整的CUDA Toolkit也能编译算子,前提是编译时能找到nvcc。这里有个细节:编译PointNet++的CUDA算子时,setup.py里会调用torch.utils.cpp_extension.CUDAExtension,它需要nvcc在环境变量里。而nvcc通常位于/usr/local/cuda/bin目录下。
验证一下:
ls /usr/local/cuda/bin/nvcc如果不存在,可以安装:
sudo apt install nvidia-cuda-toolkit注意:通过apt install安装的CUDA Toolkit版本可能和PyTorch需要的版本不一致,但只要nvcc存在且版本不是过分老(比如10.0以下),一般都能编译通过。因为torch.utils.cpp_extension会自动把PyTorch自带的CUDA头文件路径加进去,不依赖系统的CUDA头文件。
2. 代码分支选择:不是所有PointNet++实现都值得复现
PointNet++的代码实现有很多个版本,我在复现时对比了好几个仓库,最终选定了一个最顺手的。这里不直接发链接,但给出仓库的关键特征帮助识别:
- 官方版(charlesq34/pointnet2):原作者的TensorFlow实现,只包含分类和语义分割部分,依赖老版本TF,环境配置非常痛苦,不建议复现。
- erikwijmans/Pointnet2_PyTorch:PyTorch实现,结构清晰,支持C++/CUDA加速算子,训练和测试代码完整,是目前主流选择。
- yanx27/Pointnet_Pointnet2_pytorch:纯PyTorch实现,没有自定义CUDA算子,安装简单但训练速度较慢,适合阅读代码和理解网络结构。
- fuyq34/pointnet2:对分类和分割任务都做了完整的训练和测试支持,代码风格较为统一。
我的选择是erikwijmans/Pointnet2_PyTorch这个分支,理由有三:一是它提供了完整的训练脚本,二是它保持了官方版本的模块化结构(pointnet2_utils.py、pointnet2_modules.py),三是它使用了自定义的CUDA算子,训练速度比纯PyTorch版本快很多。
克隆仓库:
git clone https://github.com/erikwijmans/Pointnet2_PyTorch.git cd Pointnet2_PyTorch这里有个容易忽略的结构问题:仓库根目录下有pointnet2包(这个是核心库),也有examples目录(包含分类、分割、语义分割的具体实现)。网上很多复现教程只讲了根目录的结构,导致初学的人找不到训练脚本。
3. 环境配置实操:一套命令从零到编译通过
3.1 创建conda虚拟环境
conda create -n pointnet2 python=3.8 conda activate pointnet2Python版本我特意选了3.8,不是3.7也不是3.9,原因是:erikwijmans/Pointnet2_PyTorch代码里有类型注解的写法在3.7以下会报错,而3.9在某些旧版Python包装上又有兼容性问题。3.8是我测试过最稳的。
3.2 安装PyTorch
pip install torch==1.13.1+cu118 torchvision==0.14.1+cu118 --extra-index-url https://download.pytorch.org/whl/cu118如果网络条件不好,可以用国内镜像源:
pip install torch==1.13.1+cu118 torchvision==0.14.1+cu118 -i https://pypi.tuna.tsinghua.edu.cn/simple安装完验证一下GPU是否可用:
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"如果输出True,说明CUDA环境正常。注意这个torch.__version__会显示类似1.13.1+cu118的字符串,确认有+cu118后缀。
3.3 安装依赖
pip install numpy tqdm pyyaml matplotlib这里没有用requirements.txt,因为这个仓库的requirements写得非常随意,直接安装反而会把一些不必要的包带进来,比如opencv-python这种在训练中根本用不到的(如果只做点云分类),浪费时间。
3.4 编译CUDA算子
进入仓库根目录:
cd Pointnet2_PyTorch python setup.py install如果一切顺利,编译完成后可以看到类似Successfully built pointnet2的输出。但在我复现时这一步报错了,这个坑必须提前交代。
报错信息大概长这样:
nvcc fatal : Unsupported gpu architecture 'compute_90'这个问题出现在RTX 4090上,因为4090的算力是compute_90,而老版本的PyTorch(1.13.x)自带的CUDA 11.8是不支持Hopper架构的(注意,这个限制主要影响编译时的arch标志,实际运行通过TORCH_CUDA_ARCH_LIST可以绕过去)。
解决办法是设置环境变量:
export TORCH_CUDA_ARCH_LIST="8.9+PTX"8.9对应RTX 4090的Ada Lovelace架构(注意4090实际是compute_89,不是90,90是H100的),加+PTX是因为PyTorch编译会生成PTX代码用于未来兼容。
如果你用的是30系显卡(Ampere架构),对应的是8.6;20系是7.5;V100是7.0。这个信息可以用nvidia-smi配合torch.cuda.get_device_capability()查:
import torch print(torch.cuda.get_device_capability())还有一个我会提前做的小优化:设置MAX_JOBS限制编译并发数,避免内存不足导致编译中断:
export MAX_JOBS=4编译完成后验证算子是否正常:
import torch from pointnet2_utils import BallQuery device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") ball_query = BallQuery(radius=0.1, nsample=32) points = torch.randn(1, 1024, 3).to(device) centers = torch.randn(1, 128, 3).to(device) result = ball_query(points, centers) print("BallQuery output shape:", result.shape)如果能输出结果,说明CUDA算子编译成功且基本功能正常。
3.5 GCC版本问题
Ubuntu 20.04默认的gcc是9.4版本,编译PointNet++时会遇到一个老生常谈的报错:
error: identifier "__nvsof" is undefined或者:
error: invalid conversion from 'long int' to 'int' [-fpermissive]这个问题的根源是torch/extension.h头文件在gcc 9.x下和老版CUDA Toolkit(11.x)存在兼容性问题。最简单的解决方案是安装gcc 7.5:
sudo apt install gcc-7 g++-7然后用update-alternatives切换默认gcc版本:
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 70 --slave /usr/bin/g++ g++ /usr/bin/g++-7切换后验证:
gcc --version g++ --version再执行python setup.py install,基本就能顺利通过。
4. 数据集准备:S3DIS和ModelNet40的下载与格式处理
PointNet2_PyTorch仓库的训练脚本默认支持三个数据集:S3DIS(室内场景语义分割)、ModelNet40(点云分类)、ShapeNet(部件分割)。其中S3DIS因为原始文件格式是txt,需要转成npy,这一步很多人卡住。
4.1 ModelNet40
下载地址在Princeton ModelNet官网,但访问速度不稳定,建议从公开镜像下载h5版本。如果实在找不到,可以在GitHub上搜索ModelNet40_h5,很多PointNet相关仓库都带了这个文件的下载脚本。
关键点:ModelNet40的官方数据格式是off和ply,但训练脚本直接读取h5格式。我建议用h5版本,因为它已经把点云采样到固定点数(默认1024个点),数据处理一步到位。
下载完成后解压,目录结构应该是:
modelnet40_ply_hdf5_2048/ ├── train_files.txt ├── test_files.txt ├── ply_data_train0.h5 ├── ply_data_train1.h5 ├── ply_data_train2.h5 ├── ply_data_test0.h5 ├── ply_data_test1.h5 └── ...4.2 S3DIS
斯坦福的S3DIS数据集,原始下载地址在斯坦福的服务器上,文件比较大(压缩包约6GB左右)。如果你在内网或访问国外资源比较困难,可以在HuggingFace等平台搜索S3DIS,有用户上传了处理过的版本。
需要说明的是,S3DIS有Stanford3dDataset_v1.2(原始版)和Stanford3dDataset_v1.2_Aligned_Version(对齐版)两个版本。PointNet2_PyTorch仓库的代码同时兼容两者,但强烈建议使用对齐版,因为不同区域的坐标系统一后,语义分割训练的效果和可视化都更稳定。
下载后解压目录结构:
Stanford3dDataset_v1.2_Aligned_Version/ ├── Area_1/ ├── Area_2/ ├── Area_3/ ├── Area_4/ ├── Area_5/ ├── Area_6/ └── ...每个Area下面又包含多个房间,每个房间里有多个txt文件,是房间内不同物体的点云。
4.3 S3DIS数据转换:从txt到npy
PointNet2_PyTorch的训练脚本期望的数据格式是npy,而原始数据是txt,所以需要先转换。网上有个流行的转换脚本,核心逻辑是:读取txt文件的六列数据(x, y, z, r, g, b),采样固定点数(默认为4096),保存为npy文件。
我提供一个标准转换代码:
import os import numpy as np import pandas as pd def txt_to_npy(txt_file, num_points=4096): data = pd.read_csv(txt_file, sep=' ', header=None, names=['x', 'y', 'z', 'r', 'g', 'b']) pts = data[['x', 'y', 'z']].values.astype(np.float32) colors = data[['r', 'g', 'b']].values.astype(np.float32) colors = colors / 255.0 # 归一化到[0,1] # 采样固定点数 if len(pts) >= num_points: idx = np.random.choice(len(pts), num_points, replace=False) pts = pts[idx] colors = colors[idx] else: # 复制填充 dup_idx = np.random.choice(len(pts), num_points - len(pts), replace=True) pts = np.vstack([pts, pts[dup_idx]]) colors = np.vstack([colors, colors[dup_idx]]) # 合并为 [N, 6] 数组,对应代码中 data 格式是 [N, 3+3] return np.concatenate([pts, colors], axis=1) # 遍历所有txt文件 base_dir = 'path/to/Stanford3dDataset_v1.2_Aligned_Version' output_dir = 'path/to/s3dis_npy' for root, dirs, files in os.walk(base_dir): for file in files: if file.endswith('.txt'): txt_path = os.path.join(root, file) data = txt_to_npy(txt_path) room_name = os.path.basename(root) output_path = os.path.join(output_dir, f'{room_name}.npy') np.save(output_path, data) print(f'Processed {txt_path} -> {output_path}')这段代码有几个细节值得注意:
颜色通道从整数0-255归一化到浮点数0-1,这一步在训练脚本里没有做,却直接影响收敛速度。不做归一化的话,模型前几个epoch的loss会出现明显震荡。
采样策略用
np.random.choice不放回采样,当点数不足时再用有放回采样填充。这保证训练数据均衡。输出文件命名和训练脚本中读取的路径要一致。在
train_semseg.py脚本中,数据读取逻辑是按房间名匹配的,所以文件名直接用房间名最保险。
转换完成后,需要改训练脚本中的data_path:
# 在 train_semseg.py 中找到类似下面的行 data_path = 'data/s3dis_npy'改完后,建议先打印一下加载的第一个样本的shape验证一下:
data = np.load(os.path.join(data_path, os.listdir(data_path)[0])) print(data.shape)输出是[N, 6](N是点数,通常为4096)就说明数据没问题。
5. 训练与复现:核心参数对照和benchmark验证
5.1 分类任务:ModelNet40
进入examples/classification目录,直接跑:
python train_classification.py --dataset /path/to/modelnet40_ply_hdf5_2048 --batch_size 24 --num_points 1024 --epoch 200如果你按上面流程处理数据,--dataset参数指向包含train_files.txt的目录。
分类任务的复现指标参考:
| 指标 | 结果 |
|---|---|
| Overall Accuracy | 92.4%~93.1% |
| Mean Class Accuracy | 90.5%~91.3% |
| 训练时间 | 单卡RTX 4090约2小时 |
不同实现的IOU指标可能有细微差异,这是因为数据增强策略、点云采样点数、batch size都会影响最终结果。我自己复现时,93%的准确率和论文中报告的接近,说明代码正确。
有个容易踩坑的地方:分类训练脚本默认使用--num_points 1024,但PointNet++论文中使用的也是1024个点。如果你在测试时改变了点数,准确率会明显下降,这属于正常的泛化损失。
5.2 语义分割任务:S3DIS
在examples/sem_seg目录下:
python train_semseg.py --data_path /path/to/s3dis_npy --test_area 5 --batch_size 8 --num_point 4096 --epoch 50注意几个关键参数:
--test_area:指定Area 5作为测试集,Area 1-4和6作为训练集。这是S3DIS的标准协议,这样可以和其他论文的结果进行比较。--num_point:S3DIS默认4096,如果显存有限可以降到2048,但IOU会掉1-2个百分点。--batch_size:8在RTX 4090上大概占用6-7GB显存,如果是2080Ti或者更小的卡,建议降到4。
语义分割的训练时间比分类长不少,RTX 4090上大概需要4小时左右跑完50个epoch。训练过程中每隔几个epoch会输出验证集IOU。
复现指标参考:
| 指标 | 结果 |
|---|---|
| mIoU(Area 5) | 63.5%~65.2% |
| Overall Accuracy | 86.0%~86.8% |
| 平均类准确率 | 67.0%~68.5% |
这几个数字是国内外的PointNet++复现实验中比较常见的范围,如果结果明显低于这个范围,不是网络实现有问题,而是数据处理部分有偏差。常见的偏差包括:xyz坐标没有归一化、颜色没有归一化、训练集和测试集数据泄露(比如同一房间的点云被切分到了两个集合)。
5.3 训练过程中常见的行为特征
第一次跑PointNet++训练时,前面几个epoch的loss混沌不清是正常的,不用慌张。我从实际观察来看,一般到第10个epoch左右,分类准确率会突然从30%跳到70%左右,这是因为PointNet++的网络结构中,分组和聚合层在这个阶段才完整地学到了点云的空间分布特征。
如果在第50个epoch后loss还在剧烈波动(幅度超过20%),优先检查学习率。默认学习率是0.001,配合每20个epoch乘以0.5的衰减策略,理论上在第50个epoch时应该处于稳定收敛阶段。
我用的一个经验技巧:训练完一次后,在推理阶段把num_points参数调大,比如从1024调到2048,模型的分类准确率会有微弱提升(约0.5个百分点),这是因为输入的信息量变多了。如果你想更精细地掌握这个技巧,可以做一个简单的实验,分别用num_points=1024、2048、4096跑推理,看准确率的变化趋势。
6. 我排过的几个经典坑:完整排查链路
这部分我专门说一下踩坑后的排查过程,因为过程中遇到的几个错误,在网上查到的解决方案都是治标不治本,走了不少弯路。
6.1 编译时报错internal compiler error
错误特征:编译到pointnet2_api.cpp或者ball_query.cpp时,闪退或者抛出一堆nested模板的报错信息。
排查过程:一开始我以为是代码问题,反复git clone了不下三次,后来发现错误信息里有一行In file included from /usr/local/lib/python3.8/dist-packages/torch/include/pybind11/detail/common.h:250:0,这时突然意识到是编译器的问题。
验证方法:用gcc 9.4和gcc 7.5分别编译一遍,发现只有gcc 9.4报错。确认后切换到gcc 7.5,顺利通过。
根因:PyTorch 1.13.1的C++扩展头文件在老版CUDA 11.8配套的gcc版本组合下,模板推导存在兼容性问题,换gcc 7.5即可解决。这个现象本质是_GLIBCXX_USE_CXX11_ABI宏定义在gcc 9和gcc 7之间的ABI差异导致的。
6.2 训练时AssertionError: Probability tensor contains either inf or nan
错误特征:训练到第几十个epoch时,loss突然变成nan,然后程序崩溃。
排查过程:我先打印了最后一个隐藏层的输出,发现有大量inf值。然后逐层排查,发现是BatchNorm计算方差时出现了数值不稳定。这个问题的触发条件是在有限数据上训练的后期,某些通道的激活值分布过于集中,方差接近0,除0导致了inf。
解决方案:在训练脚本中的torch.nn.BatchNorm1d参数里加eps=1e-3(默认是1e-5)。别小看这个改动,对于点云数据这种高维稀疏输入,更大的eps可以让数值更稳定。修改后在同样的数据集上跑,nan问题消失。
替代方案:如果不想改代码,也可以采用--lr 0.0005降低学习率,使梯度变化更平缓,但代价是收敛速度变慢。
6.3 语义分割时点云可视化输出错乱
错误特征:使用仓库自带的visualize.py脚本可视化预测结果时,模型预测的每个点都错位,看起来像是点云被随机置换了一样。
排查过程:这个问题的难度比前两个大很多,一开始我怀疑是模型训练不充分,但checkpoint训练了50个epoch后IOU已经到60%以上,按理说不该出现这种结果。
后来我在可视化脚本里打印了模型输入点的坐标和输出标签的对应关系,发现模型输出的语义标签是按点云索引排列的,而可视化脚本使用的是按文件索引顺序,两者在点云预处理时发生了索引偏移。
也就是说仓库自带的visualize.py在做采样时没有保存原始索引,导致模型输出和原始点云的对应关系丢失。
解决方案:重写可视化脚本,在采样时显式保存索引。如果你不想改脚本,也可以在使用--num_point时保持和训练时一致(4096),同时在可视化代码中用相同的np.random.seed(0)来保证采样顺序一致。
6.4 显存不足CUDA out of memory
错误特征:batch_size=8的时候在12GB的卡上爆显存。
排查过程:PointNet++的显存消耗分为两部分,一部分是模型参数和中间特征,另一部分是CUDA算子在反向传播时保存的临时变量。后者尤其消耗显存,特别是ball_query和knn这类需要计算点对距离的算子。
解决方案:
- 将
--batch_size降到4或2; - 使用梯度累积,设置
--accumulate_grads 4,相当于batch_size不变但显存占用更小; - 在
pointnet2_utils.py中把use_xyz设为False,可以减少部分特征计算量。
6.5 常见错误速查表
| 错误信息 | 根本原因 | 解决方案 |
|---|---|---|
nvcc fatal: Unsupported gpu architecture 'compute_90' | PyTorch 1.x不识别40系架构 | 设置TORCH_CUDA_ARCH_LIST="8.9+PTX" |
error: identifier "__nvsof" is undefined | GCC版本过高 | 切换至GCC 7.5 |
ImportError: libcudart.so.11.0: cannot open shared object file | CUDA运行时路径未配置 | export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH |
AssertionError: Probability tensor contains either inf or nan | BatchNorm数值不稳定 | 调整eps或降低学习率 |
RuntimeError: All values in view are not unique | 点云索引冲突 | 检查采样是否重复,增加num_points |
7. 复现完成后的扩展:从“能跑”到“会用”
当你把点云分类、语义分割跑通后,PointNet++这个项目就可以作为工具库来用了。我自己后期做的一些扩展思路,这里一并列出来供参考:
7.1 当成特征提取器
把pointnet2包中的PointNet2SSG或PointNet2MSG单独拿出来,接一个简单的分类头,就能用在自定义的点云特征提取任务上。这个做法适合做点云检索和配准的前置特征网络。
7.2 应用到自己的数据
如果你有自定义的点云数据,只需要构造一个包含xyz坐标(和可选rgb)的npy数组,然后用PointNet2的DataLoader读取就行。不需要遵循原有数据集的目录结构,只需要保持数据格式是N×3(或N×6)的numpy数组。
比如我自己在做一个室内家具点云分类的项目,数据是自己采集的:
import numpy as np import torch from pointnet2.models import PointNet2ClassificationSSG # 假设 points 是 N×3 的numpy数组 model = PointNet2ClassificationSSG(num_classes=5) model.eval() with torch.no_grad(): points_tensor = torch.tensor(points).unsqueeze(0).cuda() # 这里要注意加 batch 维度 pred = model(points_tensor)7.3 与Voxel方法对比
PointNet++的核心思想是在点云上直接做分组和特征提取,省去了体素化的精度损失。在真实场景中做点云分类时,如果数据分布不均匀、点数差异大(比如靠近相机的区域点很密,远处点很稀),PointNet++的密度自适应能力(MSG结构)会明显优于固定的Voxel方法。
7.4 训练细节的魔改空间
- pointnet++的
msg和ssg结构选择:如果你追求精度,用MSG(Multi-Scale Grouping),代码中对应PointNet2MSG;如果追求速度,用SSG(Single-Scale Grouping)。在S3DIS上,MSG比SSG的mIoU大约高2个百分点,但训练时间增加50%。 - 特征维度设计:PointNet++对特征的最后一层维度很敏感,
512和1024的效果在部分数据集上有明显差异,但这个差异不是线性的,需要自己试。
最后聊两句实在的心得
环境配置加复现PointNet++整个过程,我做下来最大的感受是:这个项目本身代码没有问题,问题出在“时间差”上。代码是在某个特定的PyTorch/CUDA版本时代写的,而现在大家手里的显卡、驱动、框架版本全都变了,需要做的其实是把这几样东西恢复到代码诞生的那个“时代背景”下,一切就自然跑通了。
如果你想拿这个项目学点东西,不要只跑训练脚本就完事。花点时间读一下pointnet2_modules.py里的PointNetSetAbstraction类,理解sample、group、pointnet三个步骤在做什么,再看看pointnet2_utils.py里的BallQuery和KNN怎么实现,对你后面做点云相关工作帮助很大。
如果你在配置过程中遇到我上面没提到的报错,可以在评论区留言,我看到了都会帮你看一下。最后祝大家都能顺利跑通,不再被环境折腾。