欠拟合是3D深度学习里非常典型但又容易被忽视的问题。它的表现很有欺骗性:训练损失下降到一定程度就卡住不动,验证集精度和训练集精度都很低,甚至加大训练轮数也无济于事。很多初学者会把注意力全部放在过拟合和正则化上,结果模型其实连训练数据都没有拟合好,正则化做得越狠,效果反而越差。本文围绕3D模型欠拟合这一主题,从模型容量、数据表达、训练策略三个层面展开分析,并给出可落地的代码改进方案。

一、什么是欠拟合,以及如何在3D任务中识别它
欠拟合指的是模型表达能力不足,无法捕捉训练数据中的规律,导致训练误差和验证误差都偏高。与过拟合相反,过拟合是训练误差很低但验证误差高,而欠拟合是两边都下不去。在3D任务中,欠拟合有一个非常直观的信号:分割或分类的预测结果往往趋同,比如点云分割网络把大部分点都预测成同一个类别,或者生成的3D形状缺乏细节、变成模糊的一团。
识别欠拟合时不要只看验证集曲线,一定要同时看训练集曲线。如果训练损失经过几十个epoch后仍然明显高于理论下界,或者训练准确率远低于预期,基本可以断定模型容量不足或训练配置有问题。举个具体例子,用PointNet做ShapeNet部件分割,如果训练集mIoU长期停留在0.3左右,而论文中训练集mIoU应该接近0.9,那就不是数据的问题,而是模型或者训练本身出了问题。
还有一个容易混淆的场景:损失下降很慢不一定是欠拟合,可能是学习率太小或者数据归一化有问题。区分的办法是做一个 sanity check,先拿一小批数据(比如几十个样本)做严重过拟合测试,如果模型连这几十个样本都拟合不了,那欠拟合的诊断就可以确定了。
二、模型容量不足是欠拟合的核心原因
3D数据的维度和复杂度远高于2D图像。一张点云动辄上万个点,每个点携带坐标、法向量甚至颜色信息,几何结构之间存在大量长程依赖。如果网络容量不够,特征提取就会非常粗糙,最终输出只能覆盖数据分布中最平庸的模式。
具体到不同网络结构,容量瓶颈的表现形式不一样。第一类是基于点云的网络,如PointNet系列,它的共享MLP加上最大池化结构虽然参数不多,但全局特征只靠一次对称聚合得到,表达能力有限,对于需要细粒度局部结构的任务容易欠拟合。第二类是基于体素的3D卷积网络,容量瓶颈在于体素分辨率,如果输入只有32的三次方体素,再深的网络也无法分辨细小结构。第三类是基于图卷积的网格网络,欠拟合往往来自聚合层数太少,感受野覆盖不了整个曲面。
提升容量的常见手段包括:增加网络深度和通道宽度,例如把PointNet中mlp的通道数从64提升到128;引入分层局部特征提取,比如PointNet++的set abstraction结构;在体素网络中提高输入分辨率或使用稀疏卷积代替稠密卷积。下面用PyTorch演示一个点云分类网络的容量改进:
import torch
import torch.nn as nn
# 容量不足的版本:单层共享MLP + 最大池化
class SmallPointNet(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(3, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU()
)
self.head = nn.Linear(64, num_classes)
def forward(self, x):
# x: (B, N, 3)
feat = self.mlp(x)
feat = torch.max(feat, dim=1)[0] # 全局最大池化
return self.head(feat)
# 提升容量的版本:更宽的MLP + 局部聚合 + 残差连接
class WiderPointNet(nn.Module):
def __init__(self, num_classes=10):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(3, 128),
nn.ReLU(),
nn.Linear(128, 256),
nn.ReLU(),
nn.Linear(256, 256),
nn.ReLU()
)
self.head = nn.Sequential(
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, num_classes)
)
def forward(self, x):
feat = self.mlp(x)
local_feat = torch.mean(feat, dim=1) # 局部均值聚合
global_feat = torch.max(feat, dim=1)[0] # 全局最大聚合
# 拼接两种聚合特征,增强表达能力
fused = torch.cat([global_feat, local_feat], dim=1)
return self.head(self.fuse(fused)) if hasattr(self, 'fuse') else self.head(global_feat)
上面第二个版本通过加宽网络、增加聚合方式提升了容量,在同样的训练配置下,训练损失通常能明显下降。需要注意的是,容量也不能无脑加大,参数量暴增会带来显存压力和训练速度下降,合理的做法是从一个中等规模模型出发,观察训练曲线再逐步调整。
三、训练策略不当同样会导致欠拟合
模型容量够不够,还要看训练有没有把容量释放出来。实际工程中,相当一部分欠拟合问题出在训练配置上,而不是结构本身。第一个常见坑是正则化过强。Dropout率设到0.5以上、weight decay过大、数据增强过于激进(比如对点云做大幅随机缩放和抖动),都会让模型学不到有效特征。判断方法是先关掉全部正则化项跑一遍,看训练损失能不能降下去,如果能,再逐项加回来。
第二个坑是学习率策略。学习率太小时,损失下降极慢,看起来像欠拟合;学习率太大时,损失来回震荡,同样降不下去。对3D网络来说,通常从0.001左右的Adam初始学习率起步,配合余弦退火或多阶段衰减。第三个坑是归一化不当,点云坐标如果没有归一化到单位球内,或者体素密度分布差异过大,会导致梯度失衡,模型收敛到次优解。第四个坑是训练轮数不足,3D任务收敛普遍比2D慢,部件分割任务往往需要训练两三百个epoch才能充分拟合。
下面给出一份针对欠拟合问题的训练配置检查清单和对应的代码调整示例:
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
def build_training_setup(model, train_loader, epochs=300):
# 针对欠拟合:适度降低weight decay,避免过度约束
optimizer = optim.Adam(
model.parameters(),
lr=1e-3,
weight_decay=1e-4 # 欠拟合时先减小,不要一上来就1e-2
)
# 余弦退火调度,保证后期学习率足够小以精细拟合
scheduler = CosineAnnealingLR(optimizer, T_max=epochs, eta_min=1e-5)
def train_one_epoch(epoch_idx):
model.train()
total_loss = 0
for points, labels in train_loader:
# 点云归一化到单位球,避免尺度失衡
center = points.mean(dim=1, keepdim=True)
points = points - center
scale = points.norm(dim=2).max(dim=1, keepdim=True)[0]
points = points / (scale + 1e-8)
optimizer.zero_grad()
preds = model(points)
loss = nn.functional.cross_entropy(preds, labels)
loss.backward()
optimizer.step()
total_loss += loss.item()
scheduler.step()
return total_loss / len(train_loader)
return train_one_epoch
排查顺序建议固定下来:先做小数据过拟合测试确认诊断,再检查数据归一化和增强强度,然后放宽正则化,最后才动网络结构。这个顺序可以避免盲目改模型浪费算力,大部分欠拟合问题在前两步就能定位。
四、容量与数据规模的匹配原则
解决欠拟合不能只盯着模型,还要考虑数据和任务的匹配关系。容量不足是相对于任务复杂度而言的,同样一个网络,做40类的ModelNet分类够用,做16类部件的细粒度分割可能就不够。数据集越大、类别越细,需要的容量越高。一个经验规律是,当训练样本数在万级以上时,点云网络的参数量通常需要达到百万级才能充分拟合;如果样本只有几千个,反而要注意别把容量加过头,直接跳到过拟合。
另外要区分欠拟合和噪声拟合不足的情况。如果数据本身标注噪声大,训练损失天然有一个下界,模型拟合不到零损失是正常的。这种情况下盲目加容量只会把噪声也学进去。可以通过交叉验证估计数据噪声水平,再决定容量提升的目标区间。
总结一下,解决3D模型欠拟合的核心思路是:先确认诊断无误,再从数据表达、训练策略、网络容量三个方向逐层排查。多数情况下,放宽正则化、修正归一化、训练足够多的epoch这三步就能解决大半问题,剩下的才是结构层面的改进。掌握这套排查框架后,无论是点云、体素还是网格模型,遇到训练损失降不下去的情况都能有条理地定位和修复。