广告位 · 页头(后台-模块管理-新建模块后替换此区域)

TensoRF 中 CP 分解和 Tucker 分解分别是如何加速辐射场训练的?

神经辐射场(NeRF)通常把场景表示成多层感知机,每次查询颜色和密度都要经过完整的前向计算,训练时间动辄数小时甚至一天。TensoRF 的核心思路不再让 MLP 独立承担所有场景表达,而是维护一个三维特征张量,再对这个张量做低秩分解。CP 分解和 Tucker 分解正是两种最典型的低秩近似方式。CP 分解把三阶张量拆成多个向量外积的累加,参数量小、计算直接;Tucker 分解在保留核心张量的同时引入三个因子矩阵,表达力更强。二者都能显著减少显存占用和训练迭代成本,但在实现细节与收敛行为上有明显区别。

TensoRF 中 CP 分解和 Tucker 分解分别是如何加速辐射场训练的?

一、TensoRF 如何把辐射场变成张量

原始 NeRF 输入一个三维坐标和观察方向,输出体积密度与颜色。为了让场景能够被快速查询,TensoRF 在空间内划分规则网格,每个格点保存特征向量。给定任意一个空间点,先通过三线性插值取出特征,再把特征送入一个小型 MLP 或球谐函数,得到该点的密度和颜色。这样做的好处是,空间结构不再完全依赖深层网络,而是由可训练的特征张量显式表达。

如果直接存储一个分辨率为 300×300×300、每个格点 16 维特征的张量,参数量会达到 300 的三次方乘以 16,内存压力巨大。于是 TensoRF 对这个特征张量进行低秩分解。假设三维张量的尺寸分别为 I、J、K,原始参数量为 IJK。CP 分解在秩为 R 时只需要 R(I+J+K) 个参数,Tucker 分解在秩为 R1、R2、R3 时则需要 R1R2R3+I·R1+J·R2+K·R3 个参数。相比原始稠密张量,低秩结构把空间复杂度从立方级拉低到线性或接近线性的水平,这是它能加速训练的根本原因。

下面这段代码展示了一个简单的三维特征张量初始化方式,以及未分解情况下参数量的大致计算。实际使用时很少直接创建完整张量,而是直接定义分解后的参数。

import torch
resolution = [300, 300, 300]
feature_dim = 16
dense_tensor = torch.randn(1, *resolution, feature_dim)
num_params = dense_tensor.numel()
print('dense parameters:', num_params)
# dense parameters: 300*300*300*16 = 432000000

二、CP 分解的数学形式与前向实现

CP 分解也叫 CANDECOMP/PARAFAC 分解,它把一个三阶张量 T 写成 R 个向量外积的累加:T = ∑ v_r^X ∘ v_r^Y ∘ v_r^Z。其中每个 v_r 是一个一维向量,分别对应 X、Y、Z 三个轴。秩 R 控制分解的近似程度,R 越大越接近原始张量,但参数也会线性增加。在 TensoRF 中,特征张量通常带有特征维数 F,因此每个向量实际形状为 (I, R, F)、(J, R, F)、(K, R, F)。

CP 分解的前向过程非常直观。给定一个空间坐标,先在三个轴上分别进行线性插值,得到三个轴上的插值向量,然后相乘并沿秩维度求和,最终输出该坐标处的特征。这种计算不需要显式重建完整张量,因此内存占用极低。下面是使用 PyTorch 实现的一个简化版 CP 分解特征场。

import torch
import torch.nn as nn
class CPDecomposedField(nn.Module):
    def __init__(self, resolution, rank, feature_dim=16):
        super().__init__()
        self.res = resolution
        self.rank = rank
        self.feature_dim = feature_dim
        self.vectors = nn.ParameterList([
            nn.Parameter(torch.randn(1, resolution[i], rank, feature_dim) * 0.01)
            for i in range(3)
        ])
    def forward(self, xyz):
        xyz = xyz.clamp(0.0, 1.0 - 1e-6) * (self.res[0] - 1)
        idx0 = xyz[:, 0].long()
        idx1 = xyz[:, 1].long()
        idx2 = xyz[:, 2].long()
        fx = xyz[:, 0] - idx0.float()
        fy = xyz[:, 1] - idx1.float()
        fz = xyz[:, 2] - idx2.float()
        vx = self.vectors[0][0, idx0] * (1 - fx[:, None, None, None]) + \
             self.vectors[0][0, idx0 + 1] * fx[:, None, None, None]
        vy = self.vectors[1][0, idx1] * (1 - fy[:, None, None, None]) + \
             self.vectors[1][0, idx1 + 1] * fy[:, None, None, None]
        vz = self.vectors[2][0, idx2] * (1 - fz[:, None, None, None]) + \
             self.vectors[2][0, idx2 + 1] * fz[:, None, None, None]
        feature = torch.sum(vx * vy * vz, dim=1)
        return feature

可以看到,代码里并没有出现完整的三维张量,所有操作都在分解后的向量参数上进行。CP 分解的优势在于参数极少、前向速度快,适合对训练时间要求非常高的场景。不过它的缺点也很明显:低秩假设过强,当秩不够大时,细节表达能力不足;而增大秩又会削弱参数优势。因此 CP 分解在很多实验中作为快速预览或初始化方案,并不总能达到最高渲染质量。

三、Tucker 分解与 VM 分解的改进

Tucker 分解的形式比 CP 分解更灵活,它把三阶张量表示为一个核心张量沿三个模式分别与因子矩阵相乘:T = G ×_1 A ×_2 B ×_3 C。其中 G 是核心张量,尺寸为 R1×R2×R3,A、B、C 分别是三个因子矩阵。相比 CP 分解,Tucker 分解允许不同模式使用不同秩,表达力更强,但核心张量仍然是三维结构,计算和存储成本相对更高。

TensoRF 论文里实际使用的 VM 分解可以看作 Tucker 分解的一种轻量替代。VM 分解不再存储完整的三维核心张量,而是将其中一个模式保留为向量,另外两个模式合并为矩阵。例如沿 X 轴分解时可以写成 T = ∑ v_r^X ∘ M_r^{YZ}。这样核心部分的存储从三维张量降为二维矩阵,同时保留了较强的表达能力。实际训练中,通常会同时使用三个方向的 VM 分解并累加,以便各个轴都能获得足够的建模能力。

下面是一个简化版的 Tucker 分解前向计算示例。为了便于理解,这里直接使用三线性插值后的因子向量与核心张量做 einsum 运算。

import torch
import torch.nn as nn
class TuckerDecomposedField(nn.Module):
    def __init__(self, resolution, rank, feature_dim=16):
        super().__init__()
        self.res = resolution
        self.rank = rank
        self.core = nn.Parameter(torch.randn(rank, rank, rank, feature_dim) * 0.01)
        self.factors = nn.ParameterList([
            nn.Parameter(torch.randn(resolution[i], rank) * 0.01)
            for i in range(3)
        ])
    def forward(self, xyz):
        xyz = xyz.clamp(0.0, 1.0 - 1e-6) * (self.res[0] - 1)
        idx = []
        frac = []
        for i in range(3):
            low = xyz[:, i].long()
            high = low + 1
            f = xyz[:, i] - low.float()
            idx.append((low, high))
            frac.append(f)
        def interpolate_axis(axis):
            low, high = idx[axis]
            f = frac[axis][:, None]
            return self.factors[axis][low] * (1 - f) + self.factors[axis][high] * f
        ax = interpolate_axis(0)
        ay = interpolate_axis(1)
        az = interpolate_axis(2)
        feature = torch.einsum('nr,ns,nt,rstf->nf', ax, ay, az, self.core)
        return feature

这段代码的核心在于最后一行 einsum,它直接实现了 Tucker 分解的乘加过程。相比 CP 分解,Tucker 分解需要额外的核心张量参数,因此前向开销略大,但在表达复杂光照和几何细节时通常更稳定。工程上可以通过降低核心张量秩或改用 VM 分解来平衡性能与质量。

四、加速效果与工程实践中的选择

从训练速度看,CP 分解和 Tucker 分解都比稠密特征网格快得多。稠密网格每步需要更新全体参数,梯度计算量和显存占用都很高。CP 分解把更新范围缩小到三个轴上的向量,Tucker 分解虽然多了一个核心张量,但核心张量尺寸通常远小于原始网格,因此整体更新成本仍然可控。具体提升幅度取决于分辨率和秩的设置。一般来说,TensoRF 相比原版 NeRF 可以将单场景训练时间从数小时压缩到几十分钟甚至十几分钟。

在渲染质量上,Tucker 分解通常优于同参数量下的 CP 分解。原因在于 Tucker 的核心张量能够建模不同模式之间的交互,而 CP 分解只能通过多个并行外积累加来逼近这种交互。实际项目中,如果目标是在有限训练时间内获得更好的 PSNR,可以优先选择 VM 分解或 Tucker 分解;如果只是做快速原型验证或对质量要求不高,CP 分解是更轻便的选择。

工程实现时还有几个细节需要注意。首先是初始化,分解参数通常使用小方差随机初始化,避免训练初期出现过大的特征值。其次是学习率,分解后参数尺度变化较大,往往需要针对不同部分设置不同学习率或使用自适应优化器。最后是正则化,TensoRF 常配合 TV 损失或稀疏约束来抑制噪声,这在低秩表示下尤为重要,因为低秩结构本身会对高频细节产生一定平滑作用。

下表对比了两种分解方式在参数规模与计算特性上的差异,可以作为选型参考。

分解方式参数规模表达力前向开销
CP 分解R(I+J+K)较低低
Tucker 分解R1R2R3+I·R1+J·R2+K·R3较高中
VM 分解介于二者之间较高中低

总而言之,TensoRF 通过张量分解将辐射场的空间特征压缩到可训练的少量参数上。CP 分解胜在简单快速,Tucker 分解和 VM 分解则提供更好的重建质量。理解它们在数学形式和工程实现上的差异,有助于在训练时间、显存占用和渲染精度之间做出合适的权衡。

广告位 · 文章底部(后台-模块管理替换此区域)