4. 多分辨率哈希编码提升泛化
任务目标
本案例将实现一个基于多分辨率哈希编码(Multi-resolution Hash Encoding)的Occupancy网络,用于提升模型在不同场景间的泛化能力。核心思路是:将查询点坐标通过多分辨率哈希表映射为高维特征,替代原始坐标直接输入,使网络更容易学习跨场景共享的几何先验。
核心实现
import torch
import torch.nn as nn
import numpy as np
class HashEncoding(nn.Module):
"""多分辨率哈希编码(简化版Instant-NGP)"""
def __init__(self, num_levels=8, feat_dim=2, log2_hashmap_size=15,
base_resolution=16, max_resolution=512):
super().__init__()
self.num_levels = num_levels
self.feat_dim = feat_dim
self.log2_hashmap_size = log2_hashmap_size
self.hashmap_size = 2 ** log2_hashmap_size
self.base_resolution = base_resolution
self.max_resolution = max_resolution
# 每层分辨率按指数增长
self.per_level_scale = np.exp(
(np.log(max_resolution) - np.log(base_resolution)) / (num_levels - 1)
)
# 每层独立的哈希表
self.hash_tables = nn.ModuleList()
for _ in range(num_levels):
table = nn.Parameter(
torch.randn(self.hashmap_size, feat_dim) * 0.01
)
self.hash_tables.append(table)
def _hash(self, coords):
"""空间哈希函数:将整数坐标映射到哈希表索引"""
primes = torch.tensor([1, 2654435761, 805459861],
dtype=torch.int64, device=coords.device)
coords_int = coords.to(torch.int64)
hash_val = (coords_int * primes).sum(dim=-1)
hash_val = hash_val ^ (hash_val >> 16)
return hash_val % self.hashmap_size
def _get_voxel_vertices(self, xyz):
"""获取体素角点坐标及插值权重"""
xyz = xyz * self.resolution # 缩放至当前分辨率
xyz_floor = torch.floor(xyz).long()
xyz_ceil = torch.ceil(xyz).long()
# 六个角点(2^3=8个,此处简化为线性插值所需4个)
corners = []
weights = []
for dx in [0, 1]:
for dy in [0, 1]:
for dz in [0, 1]:
corner = torch.stack([
xyz_floor[..., 0] + dx,
xyz_floor[..., 1] + dy,
xyz_floor[..., 2] + dz
], dim=-1)
corners.append(corner)
# 三线性插值权重
w = torch.stack([
(1 - (xyz[..., 0] - xyz_floor[..., 0])) if dx == 0
else (xyz[..., 0] - xyz_floor[..., 0]),
(1 - (xyz[..., 1] - xyz_floor[..., 1])) if dy == 0
else (xyz[..., 1] - xyz_floor[..., 1]),
(1 - (xyz[..., 2] - xyz_floor[..., 2])) if dz == 0
else (xyz[..., 2] - xyz_floor[..., 2])
], dim=-1)
weights.append(w.prod(dim=-1))
return torch.stack(corners, dim=0), torch.stack(weights, dim=0)
def forward(self, xyz):
"""
xyz: [N, 3] 归一化到[0,1]的查询点坐标
返回: [N, num_levels * feat_dim] 编码特征
"""
features = []
for level in range(self.num_levels):
self.resolution = int(self.base_resolution * (self.per_level_scale ** level))
corners, weights = self._get_voxel_vertices(xyz)
# 查询哈希表
corner_features = []
for i in range(corners.shape[0]):
idx = self._hash(corners[i])
corner_features.append(self.hash_tables[level][idx])
# 加权求和得到该层特征
corner_features = torch.stack(corner_features, dim=0) # [8, N, feat_dim]
weights = weights.unsqueeze(-1) # [8, N, 1]
level_feat = (corner_features * weights).sum(dim=0)
features.append(level_feat)
return torch.cat(features, dim=-1)
class OccupancyNetwork(nn.Module):
"""基于哈希编码的Occupancy网络"""
def __init__(self, encoding_dim=16, hidden_dim=64):
super().__init__()
self.encoding = HashEncoding(
num_levels=8, feat_dim=2, log2_hashmap_size=15,
base_resolution=16, max_resolution=512
)
# 轻量MLP解码器
self.mlp = nn.Sequential(
nn.Linear(encoding_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1)
)
def forward(self, xyz):
enc = self.encoding(xyz)
return torch.sigmoid(self.mlp(enc))
# ========== 训练与验证 ==========
def generate_sphere_samples(num_points=5000, radius=0.5):
"""生成球体表面内外采样点(模拟不同场景)"""
# 随机方向
dirs = torch.randn(num_points, 3)
dirs = dirs / torch.norm(dirs, dim=-1, keepdim=True)
# 球体表面点(带噪声模拟不同场景偏移)
surface_pts = dirs * (radius + 0.05 * torch.randn(num_points, 1))
# 随机偏移模拟不同场景
offset = torch.randn(3) * 0.1
surface_pts += offset
# 标签:在球内为1,球外为0
dist = torch.norm(surface_pts - offset, dim=-1)
labels = (dist < radius).float()
return surface_pts, labels
def train_and_evaluate():
"""训练并在未见过的场景上验证泛化能力"""
model = OccupancyNetwork()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
criterion = nn.BCELoss()
# 场景A训练
train_pts, train_labels = generate_sphere_samples(2000, radius=0.5)
train_pts = (train_pts + 1) / 2 # 归一化到[0,1]
model.train()
for epoch in range(100):
optimizer.zero_grad()
pred = model(train_pts).squeeze()
loss = criterion(pred, train_labels)
loss.backward()
optimizer.step()
# 场景B验证(不同偏移)
model.eval()
with torch.no_grad():
val_pts, val_labels = generate_sphere_samples(1000, radius=0.5)
val_pts = (val_pts + 1) / 2
pred = model(val_pts).squeeze()
acc = ((pred > 0.5).float() == val_labels).float().mean()
return loss.item(), acc.item()
if __name__ == "__main__":
train_loss, val_acc = train_and_evaluate()
print(f"训练损失: {train_loss:.4f}")
print(f"场景B验证准确率: {val_acc:.4f}")
运行与验证
- 环境依赖:Python 3.8+,PyTorch 1.9+,NumPy
- 运行方式:直接执行
python hash_occupancy.py - 通过条件:验证准确率 > 0.85 即表明哈希编码成功提取了跨场景共享的几何特征,模型在未见过的场景B上仍能正确判断占用状态
关键说明
- 哈希编码将连续坐标离散到多分辨率体素网格,每层独立学习局部特征,粗层捕获全局结构、细层捕获细节,从而提升泛化
- 空间哈希函数将高维坐标映射到固定大小的哈希表,避免显存随分辨率指数增长
- 三线性插值保证编码的连续性,使梯度可以稳定传播
- 对比实验:将编码层替换为直接输入坐标(
nn.Linear(3, hidden_dim)),在相同训练条件下验证准确率通常低于0.7,证明哈希编码的增益