两个隐蔽的训练错误:四元数归一化与饱和 Alpha

0 阅读6分钟

磁刻技术实录 · 第 14 篇 · 工程实践 · 技术基线 2026-10-31

我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。

在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。

在磁刻的训练实现中,细小的求导差异也可能累积成明显的画质问题。我们选择分享四元数与饱和 Alpha 这两个例子,是因为它们很容易在简单测试中被掩盖,却能说明前向函数、数值稳定性和反向公式为什么必须一起检查。

训练结果逐渐变好,并不意味着每条梯度都正确。颜色和位置可能补偿部分旋转错误,让损失仍然下降;某些高不透明度情况只出现在训练后期,也可能躲过最初的测试。磁刻的训练核心修复过两类典型问题:四元数归一化链遗漏,以及接近饱和 Alpha 时的反向数值稳定性。

以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。

这两个案例适合一起讨论,因为它们都发生在很短的数学代码中,却会影响整个训练器。真正有效的修复依赖针对性样本,而不是仅仅增加迭代次数观察画面是否改善。

两个局部公式怎样影响训练

图 1:错误可能在简单场景中隐藏,在边界条件下放大

前向归一化,反向也要经过归一化

设原始四元数为 q,前向使用单位向量 u=q/||q|| 构造旋转。若已知损失对 u 的梯度 g,那么对 q 的梯度应为 (g-u(uᵀg))/||q||。这个表达式会去掉沿单位向量径向的分量,并考虑原始长度。

遗漏这一步,相当于对另一个函数求导。即使 Adam 更新后再次把四元数归一化,错误的径向梯度仍会进入逐分量一阶、二阶矩,影响后续更新。参数被投回合法范围,并不能抵消优化器内部状态已经受到的污染。

当前代码明确计算四元数范数、单位分量和径向投影,再累加最终梯度。验证使用旋转的各向异性椭球,因为球形分布对于旋转不敏感,容易让错误测试看起来正常。

Alpha 导数包含后景影响

对于当前高斯,像素贡献不仅是它自己的颜色,还包括它对后景的遮挡。某些推导会通过总颜色或透射率反推后方贡献,并出现除以 1-alpha 的步骤。当 Alpha 接近一时,分母非常小,浮点误差会被放大。

当前反向从后向前维护单位透射率下的后景颜色 tail。若当前颜色为 c、前方透射率为 T,那么颜色对当前 Alpha 的影响可写成 T×(c-tail),再与损失颜色梯度做内积。后景按 tail=alpha×c+(1-alpha)×tail 递推,无需除以极小的 1-alpha。

四元数归一化也必须参与求导

float norm = Length(raw_quaternion);
if (norm <= epsilon) return HandleDegenerateQuaternion();
Vec4 unit = raw_quaternion / norm;
Vec4 gradient_unit = RotationMatrixBackward(unit, gradient_matrix);
float radial = Dot(unit, gradient_unit);
Vec4 gradient_raw = (gradient_unit - unit * radial) / norm;

前向真正使用的是单位四元数 unit,优化器更新的却是原始参数,所以反向必须经过归一化。减去径向分量,是因为沿四元数自身方向放大,只改变长度,不改变归一化后的旋转;这样的方向不应产生旋转梯度。

一个很实用的测试是把相同四元数乘以不同正数,检查前向旋转不变、反向按链式规则变化。只用单位四元数测试,可能碰巧避开遗漏。接近零的四元数则需要独立处理,因为归一化本身不稳定,不能通过把分母随意改成一就假装公式仍成立。

定位一个梯度错误

图 2:从可复现小场景出发,先解释差异再修改公式

稳定公式不能随意丢弃梯度

面对极端 Alpha,简单把梯度置零可能避免异常数值,却改变了优化行为。更好的办法是使用与前向等价、数值条件更好的表达,再对非有限结果进行防护。这样既保留可解释的导数,也避免把错误隐藏在大量特殊分支里。

测试需要从低透明度覆盖到接近饱和的情况,加入不同前后景颜色和多个遮挡层。若只使用单色背景与相同前景颜色,Alpha 变化对结果可能很小,无法有效暴露问题。

为什么要同时比较方向和大小

一个梯度的绝对误差看起来不大,方向却可能相反。参数量级变化时,单一相对误差也容易失真。因此,应同时观察绝对误差、相对误差以及对损失的局部下降方向。对接近零的真梯度尤其需要谨慎解释相对值。

修复后还要回到完整训练:检查参数是否有限、不同线程结果是否一致、训练与留出视角是否改善。局部公式通过,并不自动证明所有工程链路都正确;但没有局部证据,端到端好看也难以说明原因。

饱和 Alpha 附近避免除以零

// 稳定写法:后景从后向前递推。
Vec3 tail = background;
for (int k = last; k >= 0; --k) {
    float a = alpha[k];
    float d_alpha = trans_before[k] * Dot(d_pixel, colors[k] - tail);
    AccumulateAlphaGradient(k, d_alpha);
    tail = a * colors[k] + (1.0f - a) * tail;
}

若为了恢复前一层透射率而除以 1-alpha,当 Alpha 接近一时,舍入误差会被放大;等于一时则直接出现非法数。上面的递推使用前向保存的透射率,不需要这种除法。这里的 background 在当前黑背景训练中为零,推广到其他背景时也必须匹配前向。

验证应包含多个重叠高斯和接近完全不透明的前景。仅检查所有输出都是有限数仍然不够,还要比较数值梯度、颜色变化方向及遮挡后景的影响。稳定公式与正确公式都要满足,不能通过简单跳过所有非有限梯度来掩盖推导问题。

这类排错经验说明,自研训练器的技术深度体现在对边界条件的处理。复杂系统往往被一个看似普通的归一化或递推公式影响,而可靠回归能让这样的错误从偶发现象变成可定位、可防止复发的问题。