人脸NeRF训练一直存在一个尴尬:渲染质量越高,训练时间越长。NeRFace这类以NeRF为基础的人脸建模方法,往往要做高分辨率图像监督、头部姿态估计和表情系数回归,一次训练从几小时到十几小时都很常见。原因并不只是数据量大,而是传统NeRF将坐标先映射成高频正弦位置编码,再把很长的向量送进大型MLP;这种结构在拟合高频细节时有效,但计算量和收敛速度都受到拖累。Instant NGP提出的哈希编码和多分辨率特征网格提供了一条捷径:用多级哈希表直接存储坐标对应的可学习特征,再把短特征拼接后送入一个小MLP,就能保留细节并显著缩短训练时间。这套思路同样可以迁移到NeRFace上。

一、NeRFace训练慢的根因
NeRFace通常把观测空间中的采样点变换到规范人脸坐标,再用MLP预测密度和颜色。原始NeRF会对每个三维坐标分量做高频位置编码,例如编码到第10阶时,一个坐标点会扩展成几十维向量。MLP第一层需要处理很宽的输入,参数量迅速上升。更重要的是,这种固定频率变换虽然能表达高频细节,却要求网络从头学习所有频率项与颜色、密度之间的复杂映射,优化过程非常漫长。
人脸区域的细节集中在皮肤纹理、嘴唇、睫毛和头发边缘。低频坐标本身携带的信息有限,必须靠频率编码放大坐标差异。但频率编码是无参数变换,所有高频项都直接进入MLP,网络被迫同时处理粗糙结构和细腻纹理。NeRFace如果再加入表情潜码、光照条件或头部姿态参数,输入维度会进一步膨胀,训练时间成倍增加。
还有一个容易被忽略的问题是采样效率。传统NeRF需要粗采样加细采样,粗网络和细网络各自完成一次前向与反向传播,计算量翻倍。即便只把人脸区域限制在头部包围盒内,仍然有大量采样点分布在空白空间,真正对表面细节有贡献的采样占比不高。这些因素叠加起来,让NeRFace很难满足快速建模需求。
二、哈希编码如何用多分辨率特征网格工作
哈希编码的核心是放弃全分辨率密集网格,改用多个分辨率不同的层级。每一层维护一个固定大小的哈希表,例如每层有2的19次方条特征,每条特征通常只有2维。第l层分辨率从较粗的Nmin开始按指数增长到Nmax,低层负责整体结构,高层补足局部细节。对于输入坐标x,每一层先定位到包含它的体素,再取出8个角点。
每个角点经过哈希函数映射到该层哈希表的某个索引,取出对应的F维特征。8个角点特征按照坐标在体素内的相对位置做三线性插值,得到这一层的F维编码。把所有层的编码拼接起来,就形成一个L乘F维的短向量。这个向量比原来的频率编码短得多,却已经包含了从粗糙到精细的多尺度空间信息。哈希表本身是可学习参数,训练时梯度会更新被采样到的特征向量,让高频细节逐渐沉淀到细层,低频结构由粗层表达。
import torch
import torch.nn as nn
import math
class HashGridEncoder(nn.Module):
def __init__(self, n_levels=16, n_features_per_level=2,
log2_hashmap_size=19, base_resolution=16,
finest_resolution=512):
super().__init__()
self.n_levels = n_levels
self.n_features = n_features_per_level
b = math.exp((math.log(finest_resolution) -
math.log(base_resolution)) / (n_levels - 1))
self.resolutions = [int(math.floor(base_resolution * (b ** l)))
for l in range(n_levels)]
table_size = 1 << log2_hashmap_size
self.embeddings = nn.ParameterList([
nn.Parameter(torch.zeros(table_size, n_features_per_level).uniform_(-1e-4, 1e-4))
for _ in range(n_levels)
])
def hash(self, points):
# points: [N, 3] 整数顶点坐标
primes = torch.tensor([1, 2654435761, 805459861], device=points.device)
h = points[:, 0] * primes[0] ^ points[:, 1] * primes[1] ^ points[:, 2] * primes[2]
return h % self.embeddings[0].shape[0]
def forward(self, x):
# x: [N, 3] 范围约在[0,1]
feats = []
for level, emb in enumerate(self.embeddings):
res = self.resolutions[level]
x_scaled = x * res
v0 = torch.floor(x_scaled).long()
frac = x_scaled - v0.float()
corner_feats = []
for dx in (0, 1):
for dy in (0, 1):
for dz in (0, 1):
corner = v0 + torch.tensor([dx, dy, dz], device=x.device)
corner_feats.append(emb[self.hash(corner)])
c000, c100, c010, c110, c001, c101, c011, c111 = corner_feats
f = (c000 * (1 - frac[:, 0:1]) * (1 - frac[:, 1:2]) * (1 - frac[:, 2:3]) +
c100 * frac[:, 0:1] * (1 - frac[:, 1:2]) * (1 - frac[:, 2:3]) +
c010 * (1 - frac[:, 0:1]) * frac[:, 1:2] * (1 - frac[:, 2:3]) +
c110 * frac[:, 0:1] * frac[:, 1:2] * (1 - frac[:, 2:3]) +
c001 * (1 - frac[:, 0:1]) * (1 - frac[:, 1:2]) * frac[:, 2:3] +
c101 * frac[:, 0:1] * (1 - frac[:, 1:2]) * frac[:, 2:3] +
c011 * (1 - frac[:, 0:1]) * frac[:, 1:2] * frac[:, 2:3] +
c111 * frac[:, 0:1] * frac[:, 1:2] * frac[:, 2:3])
feats.append(f)
return torch.cat(feats, dim=-1)
这段实现中,层级数、哈希表大小和特征维度都可以按显存预算调整。哈希表使用均匀小值初始化,避免训练初期出现极端特征。由于不同层共享同样的哈希表大小,但分辨率不同,细层中单个哈希条目覆盖的空间更小,碰撞对局部几何的影响也会被多级拼接稀释。
三、NeRFace接入哈希编码的改造点
NeRFace通常包含一个规范空间变换,把观测空间中的采样点通过姿态和表情参数变换到规范人脸坐标。哈希编码器可以直接作用在变换后的坐标上。与原始频率编码最大的区别是,哈希表会随训练更新,因此优化器需要同时更新MLP权重和所有层的哈希表参数。为了让训练稳定,可以先固定表情系数完成短预热,让粗层哈希表先学会整体头部结构,再逐步放开细层学习率。
如果NeRFace使用了逐表情潜码,建议把潜码与哈希特征拼接后一起输入MLP,而不是只在颜色分支里处理。这样哈希特征与表情条件才能在浅层充分交互,避免出现表情变化时面部纹理跟不上的问题。MLP可以做得非常小,例如三层网络,每层64个单元,因为空间高频信息已经由哈希特征承担。
encoder = HashGridEncoder(
n_levels=16,
n_features_per_level=2,
log2_hashmap_size=19,
base_resolution=16,
finest_resolution=512
)
class NeRFaceMLP(nn.Module):
def __init__(self, input_dim=32 + 64):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, 64),
nn.ReLU(),
nn.Linear(64, 64),
nn.ReLU(),
nn.Linear(64, 1 + 3)
)
def forward(self, hash_feat, expression_code):
x = torch.cat([hash_feat, expression_code], dim=-1)
out = self.net(x)
return out[:, :1], out[:, 1:] # density, color
改造之后,MLP参数量远小于原始NeRFace主干。哈希表虽然包含千万级参数,但它是稀疏更新的,每个训练步只有被采样到的角点特征获得梯度,计算效率很高。实际训练中可以把哈希表学习率设为MLP学习率的5到10倍,让空间特征快速适配,同时避免MLP权重剧烈震荡。
四、显存、精度与哈希碰撞的平衡
多分辨率哈希表并不是越大越好。每层2的19次方条、16层时,参数量约为16乘524288乘2个浮点数,已经接近千万级,显存完全可控。但如果在高分辨率区域出现哈希碰撞,两个不同顶点会共享同一条特征,导致嘴唇边缘或发丝处产生伪影。提高哈希表大小可以减少碰撞,但也会线性增加显存占用。
实际工程中可以把最细两层分辨率控制在512到1024,并使用2维特征。对于4K人脸图,细节并不完全依赖单层分辨率,而是靠多层拼接提供足够的频率跨度。如果出现局部模糊,优先增加细层层数或上调最细分辨率,而不是无脑增大MLP宽度。优化器推荐使用Adam,并为哈希表参数关闭权重衰减,避免高频特征被过度平滑。
另一个容易忽略的点是采样策略。NeRFace训练时若仍然沿用原始NeRF的粗采样加细采样,会与哈希编码的多尺度特性产生冗余。可以只保留围绕人脸表面的细采样,并提高每条光线的采样点密度。这样在相同训练步数下,梯度质量更高,收敛速度会进一步提升。完成这些调整后,NeRFace通常可以从小时级训练时间压缩到分钟级,同时保留清晰的面部纹理和表情细节。