Q1–Q3:原理篇
Q1:Σ 的旋转-尺度分解
三维高斯的协方差矩阵不直接存 6 个独立元素,而是参数化为
Σ=Rdiag(s2)R⊤
其中 R 由单位四元数 q 生成的旋转矩阵,s=(s1,s2,s3) 是三条轴长(实现对数存储,加载时 exp 恢复)。这样做换来三件事:
- 正定免费。任意 R、任意 s 代入,结果必然是对称正定矩阵——Σ 的特征值就是 si2≥0。直接优化 6 元素则要在每步之后检查正定性,或忍受非法协方差。
- 参数解耦。旋转和轴长是两个语义独立的旋钮,梯度不会互相污染;对数尺度还天然保证了轴长非负。
- 训练稳定。6 元素直优化时,梯度会把矩阵拉向不正定的方向,需要额外投影或重参数化,实测更不稳定。
几何读法:Σ 定义一个椭球,R 决定朝向,s 决定三条半轴长度。
Q2:J 与局部线性化
透视投影 μ2d=p(μ3D) 是非线性函数(含除深度 z)。EWA(Elliptical Weighted Average,椭圆加权平均)泼溅用它在高斯中心 μ 处的一阶泰勒展开近似:
p(μ+δ)≈p(μ)+Jδ,J=∂μ3D∂pμ
于是屏幕协方差 Σ′=JΣviewJ⊤(J 是 2×3 雅可比,Σview=WΣW⊤ 是相机系协方差)。这叫局部线性化:只在 μ 附近把投影当线性函数。
失效时机:泰勒余项是二阶项,相对误差随高斯贴近相机(z 变小)按 1/z2 放大。离相机很近或很大的高斯,一阶近似会明显失真——这是 3DGS 原生前向的固有近似,不是实现 bug。
Q3:从密度到 α,conic 与 +0.3
把 2D 高斯密度峰值归一(乘以 α 使中心处贡献为 α),像素偏移 Δ=(Δx,Δy) 处的不透明度为
αpix=o⋅exp(−21Δ⊤Σ′−1Δ)=o⋅exp(power)
o 是该高斯的(sigmoid 后的)不透明度。conic 就是 Σ′−1——2D 协方差的逆。名字来自二次曲线(conic section):Δ⊤Σ′−1Δ=c 的等高线是椭圆,二次曲线方程的系数矩阵正是这个逆矩阵。代码里存 conic 而不是 cov2d,是因为逐像素算 power 只需要逆,预处理阶段求逆一次即可。
+0.3 抗混叠:给 Σ′ 对角线加 0.3(像素²),即 Σ′←Σ′+0.3I。小于一个像素的高斯投影后方差接近零,power 在亚像素尺度剧烈变化,采样不足产生锯齿/闪烁;膨胀 0.3 保证任何高斯至少覆盖约一个像素量级。注意它”只救小的”:对大高斯,+0.3 相对其方差可忽略,不改变画面。
参考
Q4–Q5:算法与反向
Q4:alpha 混合的 under 递推
前到后(front-to-back)合成一个像素,只需两条递推:
C += c_i · α_i · T // 累积颜色
T *= (1 - α_i) // 剩余透光率
T 初始为 1,表示”还没有东西挡在前面”。αi 越大,当前高斯吃掉越多剩余透光率。这就是 under 算子(AunderB 等价于” A 在 B 后面”的前到后展开)的工程形态。
配套两个机制:
- 早退:T<10−4 时后面的高斯贡献不足万分之一,直接退出循环。
- n_contrib:记录实际参与合成的高斯个数(截止点)。反向传播必须以它为界——见 Q5。
under 与 over 等价性:把 over 递推(后到前)按定义完全展开,和前到后展开的每一项逐一对等,最终颜色相同。工程上选 under 形态纯粹因为早退:前到后能在 T 耗尽时立刻停,后到前必须从头算到尾。
平移零重排:排序键取 VP 矩阵(view·proj)第 2 行的点积——也就是高斯在前向轴上的深度。键是位置的线性函数,系数向量固定为前向轴。相机纯平移 Δ 时,每个键同加 −forward⋅Δ 这个常数,全体次序不变,无需重排。旋转改变前向轴本身,才需要重排。这是 antimatter15/splat 沿用的优化。
Q5:反向的 T 链与 dL/dα
前向消掉了中间量 Ti(只留最终 T),反向要逐元素求梯度,必须恢复它们。per-tile 逆序重放时一行除回:
Ti=1−αiT
(当前手里的 T 是 Ti+1=Ti(1−αi),除以 (1−αi) 即得 Ti。)
αi 出现在两处:自己的项 ciαiTi,以及所有后续高斯的 Tj(j>i,每个都含因子 (1−αi))。求导得
∂αi∂L=Ti(∂C∂L⋅ci−acci)
其中 acci 是”尾部贡献”的归一化累计,逆序一行递推:
acc = α_i · (dL/dC·c_i) + (1 - α_i) · acc // 从后往前走
天然自检:当 ∂L/∂C=1 时,递推走到第一个高斯后,acc 的值恰好等于前向算出的像素颜色 C。反向实现对了,这个等式自动成立;不成立就是反向写错了。
截止之后梯度为零:早退发生在第 n_contrib 个高斯处,其后高斯对像素贡献为零,所有梯度一律为零。反向循环必须从 n_contrib−1 开始往回走;漏掉这个契约(比如反向遍历了全部高斯)会让 T 链整体错位一个 (1−α) 因子——这是评审中真实抓掉的 bug。
参考
Q6–Q7:二阶与 3.5 倍
Q6:μ3D 梯度的两条路径
损失对 3D 中心 μ3D 的梯度不止”挪动投影中心”这一条路。链式展开:
∂μ3D∂L=一阶:挪中心W⊤J⊤∂μ2d∂L+住在 J 里经由 J 的二阶项
- 一阶路径:μ3D 经投影 p 决定 μ2d,∂μ2d/∂μ3D=J,再过视图矩阵 W 回到世界系。这条是直觉中的”把椭球中心往 loss 降低的方向挪”。
- 二阶路径:屏幕协方差 Σ′=JΣviewJ⊤ 里的 J 本身是 μ3D 的函数(投影的偏导随位置变化)。对 Σ′ 关于 μ3D 求导,必然出现 J 的导数——也就是投影函数的二阶导。这条路径写作
K′=H⋅J⋅Σview,H=∂μ3D2∂2μ2d
H 是投影映射自己的 Hessian(三阶张量收缩后的矩阵形式),J 是它的一阶兄弟。漏掉这一项(只写一阶路径)是真实评审中抓掉的 bug:”μ3D 缺二阶 VJP(向量-雅可比积)“。梯度方向在远处近似还行,高斯靠近相机(投影非线性强)时偏差明显。
记忆法:J 在哪出现,哪里就藏着一条 H 的路径。Σ′=JΣviewJ⊤ 里 J 出现了两次,对 μ3D 求导时每个 J 都贡献一项含 H 的项。
Q7:logit 链式与 3.5 倍错梯度
不透明度链路:logit σ o×exp(power) α。逐段链式:
∂logit∂L=∂α∂L⋅∂o∂α⋅∂logit∂o=∂α∂L⋅exp(power)⋅σ(1−σ)
利用 α=σ⋅exp(power) 消去 exp(power)=α/σ,得紧凑形式
∂logit∂L=∂α∂L⋅α(1−σ)
错版:把 sigmoid 导数里的 σ 误写成像素级 α,得到 α(1−α)。两版比值
α(1−σ)α(1−α)=1−o1−α
设计数值里恰好等于 3.5。更糟的是方向性:exp(power)≤1 恒成立,所以 α≤o,错版梯度永远偏大(1−α≥1−o)。这类错的阴险之处在于训练照常收敛、loss 照常下降——只是不透明度学习系统性过激,画面发糊。公式错一个字母,没有任何断言会救你,只能靠数值对拍(gradcheck)抓出来。
符号约定提醒:本系列 σ 一律指 sigmoid 后的不透明度 o=σ(logit),α 指像素级不透明度 α=o⋅exp(power),两者不要混用。
参考
Q8 与 Q10①:循环的两面
Q8:致密化 NaN 案件链
Adam 的更新带偏差修正:m^=m/(1−β1t),其中 t 是该参数参与优化的步数。致密化(densification)会动态克隆/分裂出新高斯。案件链四步:
- 子代高斯记录出生时刻
born = iter + 1;
- 当迭代走到
iter 时 Adam 先跑,算 t=iter−born+1=0;
- 分母 1−β10=0,而此时子代动量 m 也是 0;
- 0/0=NaN,一次致密化实测污染出 210 万个 NaN。
修法三件套:
born = iter:致密化排在本迭代 Adam 步之前执行,子代第一步就是 t=1;
- per-slot t:每个参数槽位自带步数计数,不再用全局 iter 推算——动态出生的参数生命周期各不相同;
- guard 钳位:
t = max(iter − born + 1, 1)(adam.comp),即使前两条被未来的改动破坏,钳位兜底。
盲区与通用规则:静态测试用例抓不到这个 bug——初始高斯全部 t≥1,只有训练中动态出生的参数才触发。规则一句话:动态参数必带优化器状态(m、v、bornIter 都要跟着参数走;clone 继承全部,split 继承旋转与 SH)。
Q10①:gradcheck 数值对拍
中心差分(central difference)逼近解析梯度:
∂x∂f≈2εf(x+ε)−f(x−ε)
- 步长 ε=10−6(位置参数用 2×10−6)。截断误差 O(ε2),舍入误差 O(ϵmach/ε),ε 太小被舍入淹没,太大截断失真,10−6 是双精度下的甜点区。
- 门禁:相对误差 <10−3。GPU f32 前向的舍入约 10−7,已经能淹没差分信号,所以差分基准必须是双精度 CPU 复刻(且这份复刻本身先经前向对拍验证过,“验证工具自身也要被验证”)。
- 零梯度参数改用绝对差。相对误差 ∣差分−解析∣/∣解析∣ 在解析梯度为 0 时分母为零,此时改判绝对差是否接近机器精度,否则大量合法参数会被误杀。
覆盖要点:gradcheck 用例要专门构造 isolated / overlap / clamp / earlyexit / ssim / boundary 六类场景,每类”构造什么、抓什么、断言什么”三件套写全,才称得上门禁。
参考
Q9 与 Q10②:约定与验证
Q9:七个约定检查点,判据必须是几何的
跨系统(Unity 采集 → nerfstudio 训练 → 自研查看器)对接时,图像能对上不代表约定对了。七个检查点,每个配一条几何判据而不是数值指标:
| 检查点 | 一行判据 |
|---|
| 手性(左右手系) | 已知左右关系的物体在画面中方向正确 |
| 竖直翻转 | 低头 28° 机位的地平线应落在第 43 行像素,翻转后在 1037 行 |
| 半像素 | 像素中心是 (u+0.5,v+0.5),投影点偏移半像素即错 |
| 归一化 | 四元数模长恰为 1(模长一根柱,见 S3) |
| up 轴 | 重力方向与画面下方一致 |
| 尺度 | 已知尺寸物体渲染出已知像素尺寸 |
| SH 旋转 | 视角相关颜色随相机转动变化正确(SH(球谐)系数必须随坐标变换旋转) |
为什么禁 PSNR:PSNR 管好坏,不管对错。实测一个 23 dB 的模型,竖直翻转前后 PSNR 是 19.8 vs 20.1 dB——差 0.3 dB,淹没在训练噪声里。没有外部锚点(真值图)时 PSNR 更是纯自评。约定错了系统依然”自洽地渲染出一个错的世界”,只有几何不变量(地平线位置、模长、左右关系)能给出对/错的二元判决。
Q10②:三层验证各抓什么、各盲什么
| 层 | 抓 | 盲 |
|---|
| 差分(gradcheck) | 公式错误:梯度推错、链式漏项 | 公式本身错但前后一致(错得自洽) |
| overfit(过拟合单场景) | 表达能力:模型容量、管线能否收敛到 48 dB | 目标本身错了也能被完美背下 |
| 对拍(黄金字节/参考实现) | 语义错误:与独立实现逐字节/逐像素比对 | 两边抄了同一个错 |
48 dB 为什么骗人:真实案件——前向把基础色写成 col = C0·dc,漏了 +0.5(正确是 0.5 + C0·dc)。训练把 dc 整体学成 +1.77 来自我补偿,overfit 照样 48 dB,内部完全自洽;一导出到查看器,外部全是白雾。过拟合只证明”模型能背下你给的目标”,不证明目标对。
破案靠单像素实锤:手工追踪一个像素的全链路,CPU 语义算得 0.372,GPU 实际输出 0.375——0.003 的差异落在舍入量级内说明管线一致,差异落在 0.5 量级就指向 +0.5 这类语义错误。对拍的判据不是”差异小”,而是”差异可解释”:每个字节差都要能归因到精度或已知约定差。
参考
快答:S1–S4
S1:像素 → 射线与竖直 FOV
c2w 矩阵三列是相机系的 [right, −up, backward](第二列已是向下向量 down)。顶部原点(PNG/标准图像约定,v 从画面顶端往下数)的像素射线:
d∝r⋅fxu−cx+down⋅fyv−cy+bwd⋅(−1)
注意这条式子依赖 v 原点约定:若数据是左下原点(GL/ReadPixels 存储格式),down 项要变号——行序颠倒就是经典的竖直翻转 bug。
竖直 FOV 由焦距反推:FOVv=2arctan(fh/2)。代入 h=1080、f=935.3074:
FOVv=2arctan935.3074540=2arctan(0.57736)≈2×30.0°=60°
540/935.3074≈1/3 不是巧合,焦距就是按 60° 竖直视场设计的。
S2:手算 Adam 一步
β1=0.9, β2=0.999,恒定梯度 g=0.1(指数滑动平均(EMA)状态 m=g=0.1,v=g2=0.01),偏差修正 m^=m/(1−β1t)、v^=v/(1−β2t):
| t | m^ | v^ | 更新 =lr⋅m^/v^ |
|---|
| 1 | 0.1/0.1=1.000 | 0.01/0.001=10 | 1/10⋅lr=0.316lr |
| 2 | 0.1/0.19=0.526 | 0.01/0.002=5 | 0.526/5⋅lr=0.235lr |
梯度没变,步长却在收:开局几步偏差修正项主导更新量,“先大后稳”。配套考点是 gradcheck 用例设计三件套:构造什么输入、抓哪类错误、断言写在哪(含 guard 断言)。
S3:模长 4.3 的四元数
旋转矩阵 R(q) 的元素是 q 分量的二次齐次式。q 不归一(实测 nerfstudio 导出模长最大 4.3)时,R(q) 不再正交:旋转的同时按 ∥q∥2 量级膨胀——“边转边膨胀”,协方差 Σ=Rdiag(s2)R⊤ 被整体吹大。
- 排查:画全体四元数模长的分布直方图。健康实现是一根立在 1 处的细柱;这里有从 1 到 4.3 的拖尾,一图定位。
- 修复:加载端一行 q←q/∥q∥。
- 教训:交换格式(PLY)不写”四元数已归一化”这种隐含契约,消费方必须自验。
S4:tileBits 手算与黄金对拍
tileBits:排序键里划给 tile 编号的位数,按自适应键位公式 tileBits=32−clz(nTiles−1)(clz = 前导零计数)。nTiles=8160 时 nTiles−1=8159,而 212=4096<8159<8192=213,故需 13 bit。位给少了 tile 编号截断、排序键冲突;给多了挤占深度位。
黄金对拍判据:与参考实现(antimatter15/splat 语义)的输出逐字节比——黄金文件 39,241,600 字节,实测差 1159 字节。判通过的标准不是”差异小”,而是差异可解释:差异率、差异的字节分布(集中在哪些字段)、逐字节溯源到具体精度或约定差。解释不了的差异,再小也不过。
参考