简介面向三维点云处理开发者的PointNet预训练模型权重包覆盖点云分类、零件分割、语义分割三类经典任务适用于自动驾驶、机器人感知、三维场景理解等方向。模型权重以TorchScript、ONNX、OpenVINO、TensorRT四种格式导出对应PyTorch、C后端、移动端、Intel平台以及NVIDIA GPU等不同推理环境可直接加载运行省去重新训练的时间和算力成本。整包共21个文件包含3个pt、3个onnx、3个engine权重文件以及配套的6个bin与6个xml配置文件压缩包大小176.81MB文件按cls、part_seg、sem_seg三个子任务分别组织便于按需选用。四种格式的导出版本可帮助开发者在不同硬件平台间灵活切换配合OpenVINO与TensorRT的优化推理能显著提升实际部署时的响应速度。已有360人学习下载适合希望低成本复用预训练模型、快速验证算法的工程师直接集成到现有流水线中。1. 为什么你得先搞懂 pointnet 模型权重而不是直接跑代码如果你是在 GitHub 上打开 PointNet 官方仓库想赶紧把分类或分割跑起来大概率会遇到一个反直觉的现状这个项目没有像 torchvision 那样开箱即用的预训练权重下载通道。仓库里默认的part_seg、cls、seg目录下训练脚本跑完才会在本地生成.pth文件你要是直接去试model.load_state_dict(torch.load(cls/cls_model_79.pth))很多时候会因为缺文件或形状对不上直接抛异常。也就是说pointnet模型权重这件事核心不是“去哪下一个现成文件”而是“我该怎么让这个项目自己长出权重”以及“别人给的权重我怎么安全接到自己的代码里”。这篇笔记我从实际使用者的角度把PointNet权重的来龙去脉拆开讲一遍模型哪几层真正存了参数、官方脚本怎么保存和命名、加载时哪些地方最容易翻车、以及你自己训一套权重时该盯哪些指标。你可能是想跑通分类 demo 的入门者也可能是想改分割头做自己数据集的熟手这两类人读这篇都会有能直接照着做的部分。2. 看懂 PointNet 的权重结构哪些层有参数哪些层只是计算2.1 从网络结构反推“权重”二字到底指什么PointNet 的结构其实非常精简核心是一组共享的多层感知机MLP 一个 Max Pooling再加两个小的 T-Net。它不像 ResNet 那样动辄几十上百层所以权重的体积和分布非常集中输入变换网路 T-NetInput Transform里面有 Conv1d、Conv1d、Conv1d 加全连接层这部分虽然只处理 3 维坐标但它是完整的可训练子网络参数量不大但不能随手置零因为你一旦置零对齐效果就没了整个模型的精度会明显掉。特征变换网络 Feature Transform结构类似 T-Net但输入是 64 维特征中间层更宽这是权重文件里占有一定体量的一块。主干 MLP 部分即conv1到conv3以及后面的全连接层fc1、fc2、fc3。分类模式下最后输出的是 40 类ModelNet40或你自己的类别数分割模式下最后一个卷积层输出通道数是 50ShapeNetPart 的 50 个部件类别。BatchNorm 层PointNet 每一层 Conv 后面都跟着 BatchNormBatchNorm 的running_mean、running_var、weight、bias全部被算进权重文件里。我的建议是你拿到一个.pth文件后不要立刻 load先打印一下它的键名这样你能看清它对应于哪一版结构。这个习惯能帮你避免后面“用了分割权重跑分类模型”这种尴尬错误。import torch # 以官方分类模型结构为例打印权重文件的键和对应形状 ckpt torch.load(cls_model_79.pth, map_locationcpu) if isinstance(ckpt, dict) and model_state_dict in ckpt: state ckpt[model_state_dict] else: state ckpt # 有些自定义保存直接存了 state_dict for k, v in state.items(): if weight in k or bias in k or running in k: print(k, tuple(v.shape))这段代码做的事很简单把权重文件的每一层键名和形状列出来。逻辑判断里先把model_state_dict这个壳拆掉因为官方老版脚本直接用torch.save(model.state_dict(), ...)新版有些分支会套一层字典。打印结果里你会看到input_transform.conv1.weight、feature_transform.conv1.weight、conv1.weight、bn1.running_mean之类的键这些就对应我们前面说的 T-Net 和主干 MLP。参数说明map_locationcpu是为了避免服务器上没 GPU 时报错你如果确定在 GPU 机器上跑可以改成map_locationcuda:0但先映射到 CPU 再转设备更稳妥。2.2 官方模型文件名的含义cls_model_79.pth 到底代表什么官方仓库里分类和分割的权重文件命名是有规律的。分类文件名类似cls_model_79.pth表示“分类模型第 79 个 epoch 的权重”。分割文件名类似seg_model_89.pth。注意它没有把“谁是这个 epoch 里最好的模型”标出来也就是说你拿到的可能是最后一个 epoch 的权重而不一定是最优的。如果你自己训练我建议额外保存一个best_model.pth否则你复现时可能发现精度比论文低两三个点然后开始怀疑代码有 bug其实只是“最后一个 epoch 过拟合了”。官方训练脚本train_classification.py里每 N 个 epoch 会调用一次torch.save(model.state_dict(), cls_model_%d.pth % epoch)。这是个很朴素的策略定期存档不做任何评估筛选。你如果想要一个真正“能复现论文数字”的权重就得自己加验证集筛选逻辑只保留在未见过的数据上精度最高的那一版。2.3 为什么 PointNet 的权重文件普遍只有几十 MBPointNet 总参数量按分类版本来算大概是 3.5M 左右转成 Float32 存储.pth文件一般在 14~15 MB。分割版本加上最后一层的 50 通道输出参数会多一些但也就 20 MB 上下。对比一下ResNet50 的权重是 90 多 MBPointNet 小了将近 5 倍。这个事对你有什么实际意义呢它意味着你完全可以把权重文件放进 Git 仓库或者临时文件服务里分发给团队不必搞复杂的权重管理系统。你在做工业项目时PointNet 的权重存储成本可以忽略不计了模型加载速度也很快。不过文件小并不代表你可以随意改结构。PointNet 有一个很反直觉的点它的输入变换和特征变换网络是“隐式”的它们的输出会参与主干网络的计算但如果你用的不是官方原版结构而是自己魔改过的变体权重文件大概率加载失败。常见翻车场景是有人为了提速把 T-Net 去掉然后还去 load 原版权重结果 pytorch 报Missing key(s)。这其实是结构对不上不是权重文件坏了。3. 自己训练一套 pointnet 模型权重分类和分割的完整路径3.1 开始之前先选对数据集ModelNet40 和 ShapeNetPart 的组织方式PointNet 的分类实验标准数据集是 ModelNet40里面有 40 个类别的 CAD 模型官方给的是点云采样后的.npy格式存在data/modelnet40_normal_resampled/目录下。每个文件是一个N×3或N×6的数组前 3 列是坐标后 3 列是法向量。分割实验标准数据集是 ShapeNetPart16 个大类每个点有一个部件标签。你需要先确认训练脚本里的data_path指向对地方否则FileNotFoundError来得比别人都快。我给一个快速检查数据是否加载成功的办法先跑一小段训练比如 2 个 epoch如果损失正常下降说明数据没问题如果 loss 卡在初始值附近往往是数据归一化出了问题。PointNet 官方代码里没有显式做坐标归一化它把原始点云直接喂进去这对 ModelNet40 这种本身已经对齐过的数据集没问题但你换成自己的数据时最好先把点云平移到质心并缩放到单位球不然训练确实会不稳定。3.2 分类权重训练从空权重到收敛需要盯住的关键参数PointNet 官方训练分类模型用的是 SGDmomentum 0.9初始学习率 0.01每 20 个 epoch 学习率乘 0.5总共 250 个 epoch。batch size 官方给的是 32。这个配置在 ModelNet40 上大概能到 89% 左右的准确率。这里我想提醒你一个容易踩的坑PointNet 的 BatchNorm 对 batch size 很敏感。你如果显存不够把 batch size 降到 8 或更少会发现训练特别难收敛因为统计量波动太大。这时候不要盲目调学习率先试试把 BatchNorm 的momentum从默认 0.1 调低到 0.05或者改用更大的 batch size。训练时你需要监控两个东西分类准确率和 loss 曲线。我自己习惯每 5 个 epoch 在验证集上算一次整体准确率顺便保存当前权重。下面的代码是一个精简的“训练后保存最佳权重”模板你可以直接套到自己的项目里。import torch import torch.nn as nn from torch.optim import SGD from torch.optim.lr_scheduler import StepLR # 假设 model 是 PointNet 分类网络train_loader/val_loader 已构造好 device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) optimizer SGD(model.parameters(), lr0.01, momentum0.9, weight_decay1e-4) scheduler StepLR(optimizer, step_size20, gamma0.5) criterion nn.CrossEntropyLoss() best_acc 0.0 for epoch in range(1, 251): model.train() total_loss 0.0 for points, labels in train_loader: points, labels points.to(device), labels.to(device) optimizer.zero_grad() logits, _, _ model(points) # PointNet 返回 (logits, trans_feat, feat) loss criterion(logits, labels) loss.backward() optimizer.step() total_loss loss.item() scheduler.step() # 验证集评估并保存最佳权重 model.eval() correct 0 total 0 with torch.no_grad(): for points, labels in val_loader: points, labels points.to(device), labels.to(device) logits, _, _ model(points) pred logits.argmax(dim1) correct (pred labels).sum().item() total labels.size(0) acc correct / total if acc best_acc: best_acc acc torch.save({ model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), epoch: epoch, best_acc: best_acc, }, best_model.pth) if epoch % 20 0: print(fEpoch {epoch}, Loss: {total_loss:.4f}, Val Acc: {acc:.4f})代码逻辑梳理每个 epoch 里对 batch 做前向、计算交叉熵、反向传播。PointNet 的前向返回值里有一个trans_feat这是特征变换网络的输出。官方训练时会用它做一个正则项loss 0.001 * torch.mean((feat feat.T - torch.eye(feat.size(1)).to(device))**2)目的是让特征变换矩阵尽量接近正交矩阵。我在上面代码里没写这个正则项但真实训练时建议加回去不然 PointNet 的特征变换层学出来的矩阵很容易退化精度会有小幅下降。参数方面weight_decay1e-4对 PointNet 而言不是必须的官方默认没有加但加上它能让权重范数更小某些数据集上有正则化收益你可以自己对比实验。3.3 分割权重训练头结构和 Loss 的差异点分割版本的 PointNet 网络最后一个卷积层的输出通道数等于你的部件类别数ShapeNetPart 是 50并且会把全局特征和局部特征拼接起来再上采样。Loss 用的是nn.CrossEntropyLoss但要注意 PointNet 官方代码里对分割任务传入的是logits.view(-1, num_classes)和target.view(-1)也就是说它把每个点的分类当成独立的样本。训练代码和分类几乎一样唯一要改的是前向调用和最后读取预测的方式# 分割训练核心片段 logits, _, _ model(points) # points: (B, 3, N), logits: (B, num_seg_classes, N) logits logits.permute(0, 2, 1).contiguous().view(-1, num_seg_classes) labels labels.view(-1) loss criterion(logits, labels)这里的permute(0, 2, 1)把 logits 从(B, C, N)变成(B, N, C)再 view 成二维。参数说明num_seg_classes在 ShapeNetPart 上为 50但你做自己数据集时这个值必须是“你标注的所有部件类别数”不是 50。很多人在这一步翻车以为自己下载的权重是分割模型加载后输出只有 50 类自己数据却有 80 个部件最后一层形状对不上。分割训练收敛速度和分类相比要慢很多一般要训到 150 个 epoch 之后才稳定。你需要同时盯 mIoU均值交并比而不是只看 loss。我自己的做法是每 10 个 epoch 在验证集上算一次逐类 IoU取平均值然后保存 mIoU 最高的权重。分类任务里“最后一个 epoch 不一定最好”的规律在分割里更明显过拟合之后 mIoU 会明显下滑。3.4 用 T-SNE 或可视化手段确认权重学到的特征是否合理当你训练完一套权重不要只盯着准确率数字看。我强烈建议你把 PointNet 最后一个全连接层之前的特征就是 1024 维那个全局特征抽出来用 T-SNE 降维到 2D 后散点图看一眼。如果是分类模型不同类别的点应该大致聚成团如果是分割模型不同部件的特征分布会更有意思。抽取特征的代码很简单from sklearn.manifold import TSNE import numpy as np # 收集验证集特征 model.eval() features, labels_list [], [] with torch.no_grad(): for points, labels in val_loader: points points.to(device) _, _, feat model(points) # feat: (B, 1024) features.append(feat.cpu().numpy()) labels_list.append(labels.numpy()) features np.concatenate(features, axis0) labels_all np.concatenate(labels_list, axis0) tsne TSNE(n_components2, perplexity30, n_iter1000) vis tsne.fit_transform(features)这段代码本质上是“把高维特征压到二维方便人眼观察”。feat是 PointNet 经过 Max Pooling 后的全局特征它理论上应该包含整个点云形状的语义信息。如果你发现可视化结果是一团乱麻类与类完全重叠那说明权重根本没有学到有效特征这时候再去看准确率数字也没有意义了。4. 加载现有 pointnet 权重load_state_dict 的完整打开方式和四个血泪坑4.1 不同来源权重的兼容检查strictTrue是第一道过滤器当你从网上拿到一个 PointNet 权重文件第一件事是用torch.load把它读进来然后和你的模型 state_dict 做键名匹配检查。不要上来就model.load_state_dict(state)因为如果键名对不上pytorch 会抛RuntimeError: Error(s) in loading state_dict虽然信息很全但一堆Missing key和Unexpected key混在一起新手容易看晕。我建议先打印一遍直观看到差异model PointNetClassification(num_classes40) state torch.load(some_pointnet_weights.pth, map_locationcpu) if model_state_dict in state: state state[model_state_dict] model_keys set(model.state_dict().keys()) ckpt_keys set(state.keys()) missing model_keys - ckpt_keys unexpected ckpt_keys - model_keys print(Missing:, missing) print(Unexpected:, unexpected)这段代码做的事情是“集合求差”把你模型里缺的键和权重里多余的键分别列出来。逻辑说明model.state_dict().keys()返回当前模型结构的所有参数名权重文件里的键名是它保存时的结构参数名。如果两者完全一致missing和unexpected都为空。参数说明如果你看到missing里有fc3.weight和fc3.bias多半是这个权重是分割模型的最后分类层维度不同如果你看到unexpected里有类似feature_transform开头的键而你的模型没有特征变换层说明你用的是不带 T-Net 的简化版 PointNet。4.2 设备迁移map_location不写就会卡在 CPU 与 GPU 之间在服务器上模型和权重之间的设备不匹配是报错重灾区。如果你在 GPU 机器上训练保存的权重里张量默认在cuda:0拿到只有 CPU 的环境去跑直接torch.load会先报错因为默认尝试把张量加载到原设备。解决办法如下if torch.cuda.is_available(): ckpt torch.load(model.pth, map_locationcuda:0) else: ckpt torch.load(model.pth, map_locationcpu)然后给模型配设备device next(model.parameters()).device # 或者你自己指定 model.load_state_dict(ckpt[model_state_dict]) model.to(device)注意顺序先把权重加载到 model 上再执行model.to(device)。如果反过来先to(device)再加载权重只要权重本身是从 CPU 张量转过来的也不会出错但有些低版本 pytorch 因为张量设备不一致会直接报错。我现在的习惯是统一map_locationcpu加载成功后再把整个模型搬进 GPU这样最稳也避免了显存里同时存在两份 GPU 张量。4.3 去掉module.前缀DataParallel 训练出来的权重常见的坑很多人做单卡训练时为了保险会加一句model nn.DataParallel(model)然后训练完保存的是module.conv1.weight这种键名。下次单卡推理时加载就会报Unexpected key(s): module.conv1.weight。你当然可以把模型也包在 DataParallel 里继续跑但更干净的做法是从权重里剥离前缀state torch.load(ddp_model.pth, map_locationcpu) if model_state_dict in state: state state[model_state_dict] new_state {} for k, v in state.items(): if k.startswith(module.): new_state[k[len(module.):]] v else: new_state[k] v model.load_state_dict(new_state)这段代码的核心是字符串切片k[len(module.):]把键名开头的module.去掉。逻辑说明DataParallel 保存的键名相比原始模型在每个键前面都多了一个module.前缀所以要遍历全部键做一个映射。参数说明如果你是用torch.nn.DataParallel包过模型但保存时调用的是model.module.state_dict()那保存下来的键名是干净的不需要这一步。4.4 类别数不匹配40 类还是 50 类最后一层的维度必须对齐PointNet 官方仓库给两个任务的权重分类是 40 类输出分割是 50 类输出。如果你要把官方分类权重拿来跑自己的 10 类数据集直接 load 会报错因为fc3.weight形状是(40, 256)但你模型的fc3是(10, 256)。正确处理方式是“加载已知层重训新分类头”model PointNetClassification(num_classes10) state torch.load(cls_model_79.pth, map_locationcpu) if model_state_dict in state: state state[model_state_dict] # 过滤掉与当前模型形状不一致的层这里处理的是类别数导致的尺寸变化 new_state {k: v for k, v in state.items() if k in model.state_dict() and v.shape model.state_dict()[k].shape} model.load_state_dict(new_state, strictFalse)代码逻辑先用 dict comprehension 把形状匹配的键筛选出来剩下那些类别数不同的层比如fc3不会被加载然后strictFalse允许缺失键存在。参数说明v.shape model.state_dict()[k].shape这一步很关键它不只看键名还比较张量形状。如果只是类别数不同但键名都是fc3.weight没有这一步的话即使strictFalse也会因为形状不一致而报错。这样处理后未被加载的层保持随机初始化你只需要在自己的数据集上微调最后一层前面的权重已经包含通用的形状特征提取能力。5. 避坑PointNet 权重使用与复现的 5 条踩坑记录5.1 现象加载后 loss 不下降训练几轮后准确率还在 10% 左右原因权重加载成功了但数据集没有做对齐网络输入数据和权重训练时见过的数据分布差异很大。最常见的是坐标系方向混乱或者点云没有平移到质心。解决方法是检查数据预处理把每个点云先减去质心再除以整体坐标标准差缩放到近似单位球范围。5.2 现象torch.load报错pickle.UnpicklingError文件打不开原因文件损坏、下载不完整或者根本不是 pytorch 权重文件比如被错误保存成 numpy 的.npy格式。解决方法是先确认文件大小是否符合预期用file命令或head -c 100看一眼文件头。PointNet 权重文件一般以 PyTorch zip 格式存储头部有PYTORCH字样。如果文件是别人转存的 numpy 数组必须用np.load读出来再手工转成 state_dict不过这个操作不常见建议直接找原始权重来源。5.3 现象分割模型加载官方权重后输出 mask 全是一个类别原因使用了预训练权重但忘了把num_classes对应起来。官方分割权重的最后一层是 50 个部件类别你自己数据集的类别数如果不同且你没有正确修改模型结构那么网络结构可能对不上或者加载时静默丢弃了最后一层。解决方法是统一类别数要么把标注类别重新映射到 50 类以内的 id要么改造最后一层并微调。我在实际项目里更推荐后者因为 PointNet 前几层学到的几何特征是可迁移的。5.4 现象训练过程中 loss 正常下降但 val_acc 一直在 80% 左右上不去原因PointNet 原始网络不加正则化时在 ModelNet40 上上限就是 89% 左右。你如果拿完整测试集测可能在 87%~89% 之间如果只有 80%多半是输入点数太少B1024 点的效果远好于 512 点或者训练时没做随机旋转/抖动数据增强。解决方法是把输入点数至少保持在 1024并且在每个 epoch 对 batch 内点云做随机旋转和随机 dropout 一部分点这是 PointNet 官方训练里确实会做的事。5.5 现象加载官方权重后batch size 为 1 时分割结果异常点数越多越准原因BatchNorm 在推理时使用的是训练阶段累计的 running_mean 和 running_var正常不会因为 batch size 变化而波动。但如果你在加载权重后误把模型设置成model.train()而不是model.eval()BatchNorm 会重新计算当前 batch 的统计量batch size1 时统计量完全失真。解决方法是推理时务必model.eval()同时用torch.no_grad()包裹前向过程。6. 进阶用法冻结主干微调自己的点云分类模型先前所有内容都在讲“怎么拿到权重、怎么加载、怎么训”最后我想给你一个我实际项目中最高频的操作——用官方权重做初始化冻结前几层只微调最后一两层这样就算你自己数据集只有几千个样本也能快速得到一个可用的分类器。具体做法是把 PointNet 主干网络的参数requires_grad设为False只让最后的全连接层可训练。代码如下# 冻结主干 for name, param in model.named_parameters(): if fc3 not in name: # 只允许最后一层更新 param.requires_grad False optimizer SGD(filter(lambda p: p.requires_grad, model.parameters()), lr0.001, momentum0.9)这段代码的核心是requires_grad控制反向传播时梯度只会传播到fc3这层的参数上前面所有层保持官方权重原样。逻辑说明filter(lambda p: p.requires_grad, model.parameters())把不需要梯度的参数过滤掉避免优化器对它们做任何更新。参数说明这里的学习率我调到了 0.001比正常训练低一个数量级因为只训练最后一层不需要很大的步长。如果你的数据集和 ModelNet40 差异较大可以放宽冻结范围把fc2和fc3都设为可训练学习率 0.001~0.003 之间再做选择。微调几个 epoch 后如果你发现最后一层已经收敛可以尝试解冻全部层用很小的学习率比如 0.0001做全局微调通常精度还能再涨一两个点。这一步要注意解冻时必须把原来冻结层的requires_grad改回True并且重新构建优化器因为旧的优化器里根本没有那些参数for param in model.parameters(): param.requires_grad True optimizer SGD(model.parameters(), lr0.0001, momentum0.9)还有个技巧收尾推荐给你如果你要部署到 CPU 上做实时推理可以把推理阶段的 BatchNorm 层融合进前面的卷积层合并成纯卷积计算。具体做法是用torch.quantization.fuse_modules把Conv1d BatchNorm1d融合成一个带偏置的 Conv1d再去掉 BatchNorm 层这样权重文件也会更小推理速度能快 20% 左右。前提是你已经确认模型不再训练融合后无法再继续微调。我一般在融合前会把原始权重备份一份毕竟别人给的权重再跑一遍训练不容易留着就是后悔药。希望帮到你。本文还有配套的精品资源点击获取