最近我在改一篇面向大场景重建的投稿,核心思路就是标题里那句话:让训练图像的分辨率跟着场景自己涨。这套方案跑下来,单块卡上训练速度能快1.4倍,重建精度还能涨0.9dB。今天不聊论文行文,就聊这个“分辨率怎么涨”“浮点精度怎么配合涨”背后的设计逻辑,以及我在实验里踩过的坑。
不少做三维重建的人应该都有过这种感受:大场景数据动辄几百张高分辨率图像,训练时一股脑全按原图喂进去,显存吃紧、迭代变慢,最后效果还不一定好。问题不在于算力不够,而在于我们把太多的计算花在了训练初期那些根本还看不清的细节上。下面我把这套“自适应分辨率 + 浮点精度调度”的做法拆开讲,希望能给做NeRF、3DGS或者类似重建方向的朋友一点参考。
1. 大场景重建的“分辨率错配”:为什么固定高分辨率训练很亏
1.1 训练早期的分辨率需求其实很低
三维重建的训练过程有一个特点:一开始网络或高斯点云对场景的估计是全局粗糙的,此时哪怕是原图分辨率的四分之一,loss的下降趋势也几乎一样。你可以做一个简单实验:用同一份数据,分别固定1/4分辨率和原始分辨率训练前几千步,观察校验集上的PSNR,两者的差距通常不超过0.1dB。但计算量差了好几倍。
这就是所谓“分辨率错配”——训练需求最高的阶段在后期,而不是前期。固定分辨率训练本质上是用后期的精度标准,去约束前期的优化过程,造成了大量的无效计算。尤其是大场景,前景物体和远景建筑混杂,早期阶段你根本没法让模型同时顾及所有尺度,硬上高分辨率只会让优化平面变得更震荡。
1.2 大场景的稀疏性加剧了浪费
大场景重建里,图像内容的空间分布非常不均衡。一张街景图可能有一半是天空,有大量像素属于低纹理区域。在全分辨率下,这些像素对应的采样点、渲染计算、loss回传全都做了,但提供的信息增益趋近于零。更合理的做法是早期先用低分辨率把这些“容易区城”的大结构定下来,等场景的几何骨架收敛了,再提升分辨率去刻画招牌、栏杆、树叶间隙这些高频细节。
我自己统计过,固定原始分辨率训练时,前30%的迭代步数里,真正对最终PSNR有贡献的有效像素占比不到一半。剩下那一半计算完全可以省下来,分给后面对细节敏感的迭代。
1.3 常见的提速思路为什么治标不治本
有人会说,那直接用混合精度、梯度累积、减少采样点不就行了吗?这些确实有效,但它们都是在“固定分辨率”的前提下做减法。混合精度能降低显存和带宽压力,可它不改变浮点运算总量;减少采样点又很容易在后期牺牲重建质量。
这里要区分清楚:我们需要的是“让计算精度跟着优化阶段走”,而不是“全程都省一点”。自适应分辨率增长的思路,是把大场景重建当做一个从粗到精的连续过程来对待。这跟图像处理里的高斯金字塔、coarse-to-fine配准是同一个思想,只不过在这里,调度条件不是人为指定的,而是由场景本身的状态决定的。这也是标题里“让分辨率跟着场景自己涨”这句话的来源。
2. 分辨率“自己涨”的调度逻辑:三个信号源与两个控制旋钮
2.1 如何判断场景已经“配得上”更高分辨率
“跟着场景涨”不等于每隔N步翻一倍,而是要观察场景当前重建到什么程度。我在实验里主要用三个信号来判断:
第一个信号是重投影误差的下降斜率。如果最近若干步的loss下降速度仍然很快,说明当前分辨率下还有很多结构没学出来,这时候贸然升分辨率,优化平面会被突然增加的高频梯度打乱,反而容易掉点。我一般取最近500步的loss滑动平均斜率,斜率绝对值低于某个阈值才考虑升档。
第二个信号是深度图或密度场的空间一致性。3DGS类方法可以计算当前高斯分布下的深度方差,NeRF类方法可以看密度场的频域能量集中在哪个尺度。当低频结构已经稳定(比如深度方差在小范围内波动),说明场景几何骨架基本定型,可以升分辨率去学纹理细节。
第三个信号是有效像素占比。把当前分辨率下渲染图与真实图的差异做阈值过滤,统计“真正处在学习状态的像素”比例。如果这个比例超过一定值,说明场景大部分区域还在变化,升分辨率能让这些区域受益;如果大量像素已经收敛,单纯升分辨率反而过拟合到噪声。
这三个信号合起来,我给每个训练样本计算一个“场景成熟度”指标,再对全数据集做一次平均,得到一个全局标量。这个标量就是分辨率调度的依据。值得注意的是,不要用单张图来判断,因为大场景不同视角的内容复杂度差异很大,单张图的成熟度波动太剧烈,容易导致分辨率抖动。
2.2 分辨率决议的两个控制旋钮:升档节奏与回退机制
理论上,成熟度越高分辨率越高,但实际工程里直接映射会造成两个问题:一是高频切换导致训练不稳定,二是如果某个场景区域的成熟度信号噪声太大了,会来回抖。我采用控制论里最常见的方式:迟滞比较器。
具体做法是设两个阈值,升档阈值和降档阈值,且升档阈值高于降档阈值。比如当前在1/4分辨率,只有成熟度连续10个step超过0.75,才升到1/2分辨率;等到了1/2分辨率,如果成熟度跌到0.45以下,才允许回退。中间区间是死区,保持当前档位不变。这个设计非常管用,我用一个词概括就是“别让分辨率成为新的超参震荡源”。
另外还有个关键细节:分辨率提升幅度不要用整数倍跳跃。1/4直接跳到1/2,单个像素对应的场景尺度突然缩小一半,优化目标的频率特性突变很大,容易崩。我测试过更平滑的过渡方式,比如先升到1/2.6、再到1/2、再到1/1.6,最后才到原始分辨率。虽然调度步数变多了,但整体训练更稳,最终精度反而更高。幅度序列本身不需要太讲究,只要保证相邻档位的频率差距不要太悬殊就行。
2.3 数据预处理:多分辨率金字塔一次性建好
既然训练过程中图像分辨率会动态变化,那就不能像固定分辨率训练那样在dataloader里实时resize。我的做法是在训练开始前,把每张训练图像预先做一次高斯金字塔,分别生成1/8、1/4、1/2、原始分辨率四档,直接存成多分辨率版本。因为大场景数据集本身就很大,这里需要额外的磁盘空间,但带来的好处是训练时取图零延迟,不会在I/O上卡住。
这里有一个常见的坑:下采样时一定要用带低通滤波的缩放,不能用最近邻插值。如果用最近邻,缩小后的图像会产生大量锯齿状的高频伪影,模型会在早期阶段就去拟合这些伪影,等到升分辨率时这些错误的先验会被放大,导致最后的重建结果出现波状纹理。用带抗锯齿的插值(比如PyTorch里torchvision.transforms.Resize默认的双三次插值)就能把这个风险压到最低。
3. 浮点精度“跟着涨”的自适应机制:混合精度不是一刀切
3.1 重建任务里低精度误差为什么会“攒”成位置偏差
再来说标题里“浮点是怎么长出来的”这一半。做神经渲染或3DGS的人应该都有体会,全FP16训练经常出现训练损失在下降,但重建几何精度反而变差的情况。原因在于,位置、法向、颜色这些量的误差在重建任务里不是孤立存在的——小误差经过反投影会变成可见的几何漂移。
举个例子,在3DGS里如果高斯基元的位置参数用FP16存,训练后期梯度的绝对值越来越小,低于FP16的最小分辨率时,位置更新就会变成无效更新。表面上看loss还在降,但三维结构已经卡在了一个错误的局部最优附近。这时候单纯提高Loss scale只能缓解,不能根治,因为Loss scale是一个全局乘子,对于不同量纲的参数,它的保护作用完全不同。
3.2 按参数通路分离精度策略
所以我在设计“浮点跟着涨”的方案时,没有把整套模型简单分成FP32和FP16两层,而是按参数通路的敏感度来分配。具体分三组:
第一组是密度、不透明度这一类的“存在性”参数。这类参数决定了场景哪里有东西、哪里没有,一旦误差累计,会导致重建出的表面破碎、飞出浮点。这组我全程保持FP32,不做任何降精度处理。
第二组是颜色、球谐系数、材质参数。这些参数对精度相对宽容,因为颜色值的微小抖动在视觉上不容易被感知,而且它们通常带有一定的正则约束。这组可以在低分辨率阶段用FP16加动态Loss scaling,到高分辨率阶段切换回FP32,或者继续FP16但把累积梯度用FP32保存。
第三组是位置、旋转、尺度这类几何参数。这组在低分辨率阶段可以放心用FP16,因为早期梯度幅度比较大,FP16的量化误差相对可以忽略;但到了高分辨率阶段,几何参数会变得非常敏感,我建议换回FP32,并且配合小一点的学习率。
这套“精度跟着分辨率走”的调度,实际上就是把浮点位数也当成一个可变的资源。低分辨率阶段不需要那么细的数值表示,省下来的显存和带宽可以用来提高有效批大小;高分辨率阶段再切换回高精度,保证细节重建的上限不被浮点误差锁死。实测下来这样的组合比全程FP16混合精度稳很多,而且速度损失仅在最后的10%训练阶段。
3.3 损失缩放与梯度裁剪的配合细节
自适应浮点精度如果不处理梯度缩放,FP16的数值范围很容易溢出。我的做法是每100步动态计算一次梯度统计,如果梯度均方根小于当前scale能表达的最小值,就把scale提升两倍;如果出现inf或nan,就回退一半scale并跳过这一步。这个逻辑本身不复杂,但要注意一个细节:当分辨率升档时,梯度的绝对值会突然变大(因为高频成分进来了),如果在梯度统计里用的是滑动平均,容易导致scale反应慢半拍,出现一次溢出。我最后改成升档后的前500步强制使用FP32优化器状态,同时把Loss scale重置回初始值,等梯度分布稳定后再降回FP16。从实验结果看,这个重置操作是避免升档后“第一波震荡”的关键。
另外梯度裁剪也需要跟着调整。低分辨率阶段我用的梯度裁剪阈值比较大,是为了让早期优化能快速探索;高分辨率阶段阈值缩小,防止个别高频像素点主导整个batch的梯度方向。这个配合逻辑和浮点精度的切换是同步的,设计起来就像给训练过程加了一个“档位表”。
4. 单块卡训练快1.4倍:算力账、显存账与0.9dB的来源
4.1 快出来的1.4倍在哪里省下的
快1.4倍不是某一个模块带来的,而是三部分累积的效果。最核心的部分来自分辨率课程本身:训练流程里设计为前20%的迭代在1/4分辨率下运行,中间50%在1/2分辨率,最后30%在原始分辨率。由于二维卷积和渲染采样的计算量基本与像素数成正比,这三段的FLOPs加权平均下来,相比全程原始分辨率大约省了55%的渲染计算。考虑到中间需要切换分辨率的开销,以及升档时的重启训练机制,整体时间大概能省到65%左右。
第二部分是自适应浮点精度带来的显存收益。因为低分辨率阶段大量参数跑在FP16,同样一批数据下能够容纳更大的batch size。我在实验配置里把batch size从原来固定分辨率的8提到12,这个提升直接提高了GPU的带宽利用率。大场景重建的训练瓶颈往往在数据读取和内存带宽上,而不是纯算力,更大的batch能明显减少权重更新次数。
第三部分是省掉了无效的优化步骤。分辨率课程让loss曲面更平滑,每一步梯度都更“有效”,这个不易量化,但确实能减少中途反复震荡的时间。我对比过同样step数下,自适应策略的收敛曲线更干净,不会出现训练中后段loss突然反弹的情况。
4.2 0.9dB提升不是精度调度的功劳,而是“先粗后精”的收益
很多人可能会误以为涨0.9dB是因为用了高精度的浮点,但实际上真正起作用的是分辨率课程。把图像金字塔的调度去掉、只保留浮点精度切换,精度提升大约只有0.2dB;反过来只保留分辨率课程、全流程都用FP32,精度也能提升0.7dB以上。这说明0.9dB的主要来源是“先粗后精”这个优化路径本身更平滑,浮点精度只是把果实的这部分上限完整保留下来了。
为什么先粗后精能提升精度?我的理解是它避免了模型在早期就陷入高频噪声的过拟合。大场景数据中不可避免地存在标注误差、运动模糊、反光等噪声,直接在原始分辨率上训练,模型很容易把这些噪声当成细节来拟合。而低分辨率阶段相当于天然的低通滤波,模型只能先学到全局一致的低频结构,等结构稳定了再向高频推进,噪声的干扰就被大幅削弱。
另外,分辨率课程还有一个等价正则化效果:低分辨率阶段的优化步长在“高频方向”上天然被抑制,相当于给高频分量加了一个随时间衰减的约束,这和余弦退火学习率的意义很像,只不过作用在图像频域上。
4.3 复现时的三个翻车点
第一个翻车点是采样策略。有些实现为了省事,在低分辨率档位直接用重采样后的低分辨率图像,但渲染分辨率没有相应改变,导致光线的数量和图像分辨率不匹配,loss计算张冠李戴。我这里说的方案里,渲染分辨率必须跟当前档位严格一致,也就是对每个像素采样光线的数目要按照这一档的分辨率来配置,否则分辨率课程就失去了意义。
第二个翻车点是评估指标。训练过程中如果一直在低分辨率档位,校验集上的PSNR天然偏低(因为你对齐的是低通图),如果拿它和固定分辨率训练的历史曲线对比,会误以为效果变差了。这里需要在日志里记录一个统一的评估分辨率,每次都上采样到这个分辨率再算PSNR。等训练结束再用原始分辨率做最终评估,记录0.9dB的提升。
第三个翻车点是并行处理不同分辨率样本时的batch组成。我的dataloader会同时存在不同成熟度的样本,有的还在低分辨率档,有的已经升档。如果直接混在一个batch里,高分辨率样本的梯度会淹没低分辨率样本的梯度,导致低分辨率样本的学习停滞。我最后是把batch按档位分桶,每个桶内部保持同分辨率,依次更新,等同于让每个样本按自己的节奏前进。这个做法对最终0.9dB的贡献很大,因为大场景里不同视角的复杂度差异太大了。
5. 消融实验里的陷阱与后续可以扩展的方向
5.1 “分辨率假象”:测试集也要按场景复杂度分层
做消融实验时我发现一个很有意思的问题:如果把测试集里大俯拍、纯天空背景这类简单视角去掉,只保留复杂街道场景,0.9dB的优势会扩大到1.3dB左右。这说明自适应策略的优势在复杂场景上更明显,简单场景上二者差距不大。如果论文里只报总体PSNR,等于把最有利的证据掩盖住了。我建议把测试集按视角复杂度分层,分别报高复杂度、中复杂度、低复杂度的PSNR,这样读者能更清楚方法在什么条件下有效。
另外要注意的是,评估时不能为了公平而把低成本方法也拉成固定高分辨率训练,这等于让对手把算力浪费在无效区域,显然不公平。正确的方式是让对比方法也使用同等的总计算量预算,即允许它们在早期用低分辨率,但用它们自己的调度策略。这样才能说明“自适应调度”本身是更优的,而不是单纯因为省了算力。
5.2 成熟度指标的自证偏差:不能只看渲染loss
如果成熟度指标里用了渲染loss作为信号,就要小心自证偏差。因为模型在低分辨率下训练,它在低分辨率下的loss自然会低;升档后loss立刻反弹,成熟度又会下降,形成一个循环。除了loss下降斜率、有效像素占比之外,我建议加一个独立的“场景结构稳定性”信号,比如同一场景不同视角下密度场的一致性。只有当几何结构在多视角下保持一致时,才能说场景学到了“结构”,而不是只适应了当前分辨率下的图像统计。
我实验里成熟度指标最终被拆成三部分加权:损失下降趋势占40%,深度或密度一致性占40%,有效像素占比占20%。刚开始只用了损失趋势和有效像素,结果高纹理区域会导致成熟度虚高,提前升档;加入几何一致性之后,升档时机更保守,但最终结果更稳,且训练总时间几乎没有增加。
5.3 后续扩展:训练分辨率只是第一步
这套思路不光能用在训练阶段,同样可以扩展到渲染推理阶段。训练结束后,模型在原始分辨率上已经有很好的重建效果,你可以根据目标显示设备的分辨率去动态选择渲染级别,比如在移动端用1/2分辨率渲染,再配合超分模型,这与训练阶段的浮点调度是同一套哲学:算力用在刀刃上。
另一个我准备尝试的方向是把分辨率课程和稀疏化结合:低分辨率阶段同时压低斯基元的数量上限,高分辨率阶段再放开。这样训练早期的内存占用可以再降一截,并且生成的场景会天然带LOD(细节层级),对大规模场景的端侧部署非常有帮助。目前这个方向还在实验阶段,等结果出来了再单独写一篇分享。
在我实际跑这套方案的过程中,最大的体会是“先粗后精”不是一个新鲜的学术概念,难的是把课程节奏的控制权交给场景本身,再加上浮点精度自适应配合。这是一个工程调优问题,也是系统设计的思路转变:不要总想着在所有时刻都用最好的工具,而是想清楚每个时刻真正需要什么。如果你正在做大场景重建相关的训练,建议从这三点入手:多分辨率金字塔建起来、成熟度检测器做起来、参数通路的精度分开来,单卡1.4倍与0.9dB这样的收益,大概率也能在你自己的场景里跑出来。