| import torch |
| import torch.nn as nn |
| from copy import deepcopy |
| import torch.nn.functional as F |
| from einops import rearrange |
| from torch_cluster import fps |
| from os import sys, path |
| import math |
| import numpy as np |
| from collections import (defaultdict, OrderedDict, deque) |
| import os |
|
|
| from SoftGroup.softgroup.ops import (voxelization_idx, voxelization) |
| from model.basic_vae import (Encoder, Decoder) |
| from model.NGC import get_NGC_structure |
| from model.common import (ResidualConv, CappedLayerNorm) |
| import pyvista as pv |
| import matplotlib.pyplot as plt |
|
|
| |
|
|
|
|
| class VQVAE(nn.Module): |
| def __init__(self, input_dim, |
| hidden_dim, |
| codebook_size, |
| embedding_dim, |
| num_points, |
| voxel_size, |
| spconv_channels, |
| blocks_num, |
| smooth_end_epoch, |
| beta, |
| lambda_chamfer, |
| lambda_vq_start, |
| lambda_vq_final, |
| lambda_construction, |
| lambda_usage, |
| warmup_steps, |
| ema_decay, |
| token_N1_stage, |
| token_N2_stage, |
| token_N3_stage, |
| decoder_layer, |
| buffer_stage_num): |
| super(VQVAE, self).__init__() |
| self.device = device = torch.device("cuda") |
| self.blocks_num = blocks_num |
| self.spconv_channels = spconv_channels |
| self.encoder = Encoder(input_dim, hidden_dim, self.spconv_channels, self.blocks_num) |
| self.hidden_dim = hidden_dim |
| self.embedding_dim = embedding_dim |
| self.codebook_size = codebook_size |
| self.num_points = num_points |
| self.voxel_size = voxel_size |
| self.side_length_patchs = [(2 ** i) for i in range(self.voxel_size)] |
|
|
| self.token_N3_stage = token_N3_stage |
| |
| self.token_N2_stage = token_N2_stage |
| self.token_N1_stage = token_N1_stage |
| self.token_N1_nums = 2 ** (token_N1_stage - 1) |
| self.token_N2_nums = 2 ** (token_N2_stage - 1) |
| self.token_N3_nums = 2 ** (token_N3_stage - 1) |
|
|
| self.token_stage = self.token_N2_stage + buffer_stage_num |
| self.token_stage_nums_list = [(2 ** i) for i in range(self.token_stage)] |
|
|
| self.if_normal_compress = True |
| self.num_freqs = 6 |
| self.pos_dim = 3 + 2 * 3 * self.num_freqs |
| self.pos_linear = nn.Linear(self.pos_dim, self.spconv_channels) |
| self.after_spconv = nn.Sequential( |
| nn.Linear(spconv_channels, hidden_dim), |
| nn.LayerNorm(hidden_dim), |
| nn.ReLU(), |
|
|
| nn.Linear(hidden_dim, hidden_dim), |
| nn.LayerNorm(hidden_dim), |
| nn.ReLU(), |
|
|
| nn.Linear(hidden_dim, embedding_dim), |
| CappedLayerNorm(embedding_dim) |
| ) |
| nn.init.xavier_uniform_(self.pos_linear.weight) |
| nn.init.zeros_(self.pos_linear.bias) |
|
|
| self.embeddings = nn.Embedding(codebook_size, embedding_dim) |
| self.embeddings.weight.data.uniform_(-1.0 / embedding_dim , 1.0 / embedding_dim) |
|
|
| self.smooth_end_epoch = smooth_end_epoch |
| self.phi = nn.ModuleList([ |
| ResidualConv(embedding_dim, embedding_dim) |
| for _ in range(self.token_stage) |
| ]) |
| self.beta = beta |
| self.decoder = Decoder(embedding_dim, hidden_dim, input_dim, decoder_layer) |
| self.if_freeze_spconv = False |
|
|
| self.lambda_chamfer = lambda_chamfer |
| self.warmup_steps = warmup_steps |
| self.lambda_vq_start = lambda_vq_start |
| self.lambda_vq_final = lambda_vq_final |
| self.store_step = 200 |
| self.epoch_step = 20 |
| self.lambda_construction = lambda_construction |
|
|
| self.alpha = nn.Parameter(torch.tensor(0.1)) |
| self.act = nn.GELU() |
| self.log_data = torch.zeros(8, dtype=torch.float32) |
| self.usage_weight = lambda_usage |
| self.ema_decay = ema_decay |
|
|
| self.vq_window = deque(maxlen=10) |
| self.triggers = 0 |
| self.patience_counter = 0 |
| self.safe_vq_pct_thr = 0.40 |
| self.lowest_vq_weight = 0.03 |
| |
| self.init_ema() |
|
|
| def init_ema(self): |
| |
| self.embeddings.weight.requires_grad = False |
|
|
| |
| |
| if not hasattr(self, 'cluster_size'): |
| self.register_buffer('cluster_size', torch.zeros(self.codebook_size, dtype=torch.float32)) |
| if not hasattr(self, 'embed_avg'): |
| self.register_buffer('embed_avg', torch.zeros(self.codebook_size, self.embedding_dim, dtype=torch.float32)) |
| |
| self.cluster_size = self.cluster_size.to(self.device) |
| self.embed_avg = self.embed_avg.to(self.device) |
| self.ema_cluster_size = None |
| self.ema_embed_sum = None |
| self.ema_weights_list = [((i + 1) / self.token_stage) for i in range(self.token_stage)] |
|
|
| def init_from_ckpt(self, path, ignore_keys, if_only_init_param): |
| ignore_keys=list() |
| sd = torch.load(path, map_location=self.device) |
| if not if_only_init_param: |
| curr_epoch = sd['epoch'] |
| params = sd['model'] |
| opt = sd['optimizer'] |
| missing, unexpected = self.load_state_dict(params, strict=False) |
| print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys") |
| if len(missing) > 0: |
| print(f"Missing keys: {missing}") |
| if len(unexpected) > 0: |
| print(f"Unexpected keys: {unexpected}") |
| return opt, curr_epoch |
| else: |
| params = sd['model'] |
| keys = list(params.keys()) |
| missing, unexpected = self.load_state_dict(params, strict=False) |
| print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys") |
| if len(missing) > 0: |
| print(f"Missing keys: {missing}") |
| if len(unexpected) > 0: |
| print(f"Unexpected keys: {unexpected}") |
| return None, None |
|
|
|
|
| @torch.no_grad() |
| def update_ema(self, decay, eps=1e-5): |
| """ |
| z_e: (B, N, D) encoder outputs (float) |
| codes: (B, N) long tensor of indices in [0, num_embeddings) |
| This updates self.cluster_size and self.embed_avg, then writes to embeddings. |
| """ |
| cluster_size_batch = self.ema_cluster_size |
| embed_sum_batch = self.ema_embed_sum |
|
|
| |
| self.cluster_size.mul_(decay).add_(cluster_size_batch, alpha=1.0 - decay) |
| self.embed_avg.mul_(decay).add_(embed_sum_batch, alpha=1.0 - decay) |
|
|
| |
| n = self.cluster_size.sum() |
| |
| cluster_size_normalized = ((self.cluster_size + eps) / (n + self.codebook_size * eps)) * n |
|
|
| |
| |
| denom = cluster_size_normalized.unsqueeze(1) |
| |
| new_emb = self.embed_avg / denom |
| |
| if torch.isnan(new_emb).any() or torch.isinf(new_emb).any(): |
| cur = self.embeddings.weight.data.to(device) |
| nan_mask = (~torch.isfinite(new_emb)) |
| new_emb[nan_mask] = cur[nan_mask] |
|
|
| |
| self.embeddings.weight.data.copy_(new_emb) |
|
|
| def point_cloud_normalize(self, x): |
| x_normalized = None |
| if self.if_normal_compress: |
| x_max_minus_min = x.max(dim=1, keepdim=True)[0] - x.min(dim=1, keepdim=True)[0] + 1e-8 |
| max_val = x_max_minus_min.max(dim=-1, keepdim=True)[0] |
| x_norm = x_max_minus_min / (max_val + 1e-8) |
| x_normalized = ((x - x.min(dim=1, keepdim=True)[0]) / x_max_minus_min) * x_norm |
| pass |
| else: |
| x_normalized = (x - x.min(dim=1, keepdim=True)[0]) / (x.max(dim=1, keepdim=True)[0] - x.min(dim=1, keepdim=True)[0] + 1e-8) |
| scaled_x_normalized = x_normalized * (self.side_length_patchs[-1] - 1) |
| x_normalized = 2. * x_normalized - 1. |
| return x_normalized, scaled_x_normalized |
|
|
| def point_cloud_to_voxel(self, x): |
| B, N, C = x.shape |
| |
| |
| x_normalized, scaled_x_normalized = self.point_cloud_normalize(x) |
| x_normalized_long = scaled_x_normalized.long().cpu() |
|
|
| batch_ids = torch.arange(B, dtype=torch.long).view(B, 1, 1).expand(B, N, 1) |
| coords_long = torch.cat([batch_ids, x_normalized_long], dim=-1) |
| coords_long = rearrange(coords_long, 'b n c -> (b n) c') |
|
|
| batch_ids = batch_ids.to(x_normalized.device) |
| coords_float = torch.cat([batch_ids, scaled_x_normalized], dim=-1) |
| coords_float = rearrange(coords_float, 'b n c -> (b n) c') |
|
|
| voxel_coords, v2p_map, p2v_map = voxelization_idx(coords_long, B) |
| spatial_shape = [self.side_length_patchs[-1]] * 3 |
| p2v_map = p2v_map.to(x_normalized.device) |
|
|
| voxel_feats = voxelization(coords_float, p2v_map) |
| batch_ids = voxel_feats[:, 0].long() |
| voxel_feats = voxel_feats[:, 1:] |
| voxel_feats_split = [voxel_feats[batch_ids == b] for b in range(B)] |
|
|
| voxel_coords_split = [] |
| for i in range(B): |
| voxel_coords_split.append(voxel_feats_split[i].long()) |
|
|
| return voxel_feats_split, voxel_coords_split, spatial_shape, x_normalized |
|
|
| def fourier_embed(self, x, num_freqs): |
| x_norm = self.normalize_to_minus1_1(x) |
| freqs = 2 ** torch.arange(num_freqs, device=x_norm.device) * torch.pi |
| x_proj = x_norm[..., None] * freqs |
| embed = torch.cat([x_norm, torch.sin(x_proj).flatten(-2), torch.cos(x_proj).flatten(-2)], dim=-1) |
| embed = embed * (self.side_length_patchs[-1] / 2.0) |
| return embed |
|
|
| def flat_tokens(self, voxel_feats_total, voxel_batch_id_total, voxel_floatcoords_total): |
| |
| |
| L = self.token_N3_nums |
| B = len(voxel_feats_total) |
| sampled_feats_total = [] |
| sampled_floatcoords_total = [] |
|
|
| for batch_id in range(B): |
| voxel_batch_id = voxel_batch_id_total[batch_id].squeeze(1).long() |
| voxel_floatcoords = voxel_floatcoords_total[batch_id] |
| voxel_feats = voxel_feats_total[batch_id].features |
| points_num = voxel_batch_id.shape[0] |
| if points_num < L: |
| sample_idx = np.random.choice(points_num, L, replace=True) |
| else: |
| sample_idx = fps(voxel_floatcoords, voxel_batch_id, ratio=L/points_num) |
| sampled_feats = voxel_feats[sample_idx] |
| sampled_floatcoords = voxel_floatcoords[sample_idx] |
| sampled_feats_total.append(sampled_feats) |
| sampled_floatcoords_total.append(sampled_floatcoords) |
|
|
| sampled_feats_total = torch.stack(sampled_feats_total, dim=0) |
| sampled_floatcoords_total = torch.stack(sampled_floatcoords_total, dim=0) |
| floatcoords_total = sampled_floatcoords_total |
|
|
| |
| pos_embedding = self.act(self.pos_linear(self.fourier_embed(sampled_floatcoords_total, self.num_freqs))) |
| token_feats = sampled_feats_total + self.alpha * pos_embedding |
| token_feats = self.after_spconv(token_feats) |
|
|
| |
| norm_floatcoords_total = self.normalize_to_minus1_1(floatcoords_total) |
|
|
| return token_feats, norm_floatcoords_total |
| |
| |
|
|
| def encode(self, voxel_feats, voxel_coords, spatial_shape, epoch): |
|
|
| |
| voxel_feats_total, voxel_coords_total, voxel_batch_id_total = self.encoder(voxel_feats, voxel_coords, spatial_shape) |
|
|
|
|
| |
| token_feats_N3, floatcoords_N3 = self.flat_tokens(voxel_feats_total, voxel_batch_id_total, voxel_feats) |
|
|
| |
| structure_gt02_bin, structure_gt02_dec, token_feats_N2, floatcoords_N2 = get_NGC_structure(token_feats_N3, |
| floatcoords_N3, |
| self.token_N2_nums, |
| self.token_N1_nums, |
| self.token_stage) |
| B, L, C = token_feats_N2.shape |
| for b in range(B): |
| structure_gt02_dec[b] = structure_gt02_dec[b][:, :self.token_stage] |
| structure_gt02_dec = torch.stack(structure_gt02_dec, dim=0).permute(0, 2, 1) |
|
|
| |
| self.ema_cluster_size = torch.zeros(self.codebook_size, device=self.device) |
| self.ema_embed_sum = torch.zeros(self.codebook_size, C, device=self.device) |
|
|
| |
| f_BLC = token_feats_N2 |
| f_no_grad = f_BLC.detach() |
| f_rest = f_no_grad.clone() |
| f_hat = torch.zeros_like(f_rest) |
| embedding = self.embeddings.weight |
| min_encoding_stages_indices = [] |
| SN = len(self.token_stage_nums_list) |
| mean_vq_loss: torch.Tensor = 0.0 |
| vq_loss_dict = defaultdict(float) |
|
|
| for si in range(SN): |
| |
| if si == SN - 1: |
| structure_map = torch.arange(L, device=self.device, dtype=torch.long).unsqueeze(0).repeat(B, 1) |
| else: |
| structure_map = structure_gt02_dec[:, si, :] |
| h_BLC_list = [] |
| min_encoding_indices = [] |
| for b in range(B): |
| structure_b = structure_map[b] |
| f_rest_b = f_rest[b] |
| uniq, inv = torch.unique(structure_b, return_inverse=True) |
| M = uniq.numel() |
| class_sums = torch.zeros(M, C, device=f_rest.device) |
| class_sums.scatter_add_(0, inv.unsqueeze(-1).expand(-1, C), f_rest_b) |
| counts = torch.zeros(M, device=f_rest.device) |
| counts.scatter_add_(0, inv, torch.ones_like(inv, dtype=counts.dtype)) |
| rest_NC_b = class_sums / counts.unsqueeze(-1) |
| d = ( |
| rest_NC_b.pow(2).sum(1, keepdim=True) |
| + embedding.pow(2).sum(1) |
| - 2 * rest_NC_b @ embedding.T |
| ) |
| if epoch is not None and self.smooth_end_epoch != -1 and epoch < self.smooth_end_epoch: |
| logit = F.softmax(d.max(dim=-1, keepdim=True)[0] - d, dim=-1) |
| idx = torch.argmax(logit, dim=-1) |
| one_hot = F.one_hot(idx, self.codebook_size).type_as(logit) |
| one_hot = one_hot - logit.detach() + logit |
| h_NC_b = one_hot @ embedding |
| else: |
| idx = torch.argmin(d, dim=1) |
| h_NC_b = embedding[idx] |
|
|
| |
| |
| self.ema_cluster_size.scatter_add_(0, idx, torch.ones_like(idx, dtype=self.ema_cluster_size.dtype)) |
| |
| self.ema_embed_sum.scatter_add_(0, idx.unsqueeze(-1).expand(-1, C), rest_NC_b.detach() * self.ema_weights_list[si]) |
| |
| min_encoding_indices.append(idx) |
| h_LC_b = h_NC_b[inv] |
| h_BLC_list.append(h_LC_b) |
|
|
| h_BLC = torch.stack(h_BLC_list, dim=0) |
| h_BLC = self.phi[int(si/SN)](h_BLC) |
|
|
| f_hat = f_hat + h_BLC |
| f_rest -= h_BLC |
| mean_vq_loss_i = F.mse_loss(f_hat.detach(), f_BLC).mul_(self.beta) + F.mse_loss(f_hat, f_no_grad) |
| |
| vq_loss_dict[f'vq_loss_{si}'] = mean_vq_loss_i.item() |
| mean_vq_loss += mean_vq_loss_i |
|
|
| min_encoding_stages_indices.append(min_encoding_indices) |
|
|
| mean_vq_loss *= 1. / SN |
| f_hat = f_hat.detach() - f_no_grad + f_BLC |
|
|
| return f_hat, f_BLC, mean_vq_loss, min_encoding_stages_indices, vq_loss_dict, floatcoords_N2 |
|
|
| def normalize_to_minus1_1(self, x): |
|
|
| x_max_minus_min = x.max(dim=1, keepdim=True)[0] - x.min(dim=1, keepdim=True)[0] + 1e-8 |
| max_val = x_max_minus_min.max(dim=-1, keepdim=True)[0] |
| x_norm = x_max_minus_min / (max_val + 1e-8) |
| x_normalized = ((x - x.min(dim=1, keepdim=True)[0]) / x_max_minus_min) * x_norm |
| x_normalized = 2. * x_normalized - 1. |
| return x_normalized |
|
|
| def downsample(self, points): |
| B, N, C = points.shape |
| device = points.device |
| out = [] |
| for b in range(B): |
| pc = points[b] |
| ratio = self.token_N2_nums / N |
|
|
| idx = fps(pc, ratio=ratio) |
| pc_out = pc[idx] |
| out.append(pc_out) |
| return torch.stack(out, dim=0) |
| |
| def chamfer_distance(self, x, y): |
| xx = torch.sum(x**2, dim=2) |
| yy = torch.sum(y**2, dim=2) |
| zz = torch.matmul(x, y.transpose(2, 1)) |
| rx = xx.unsqueeze(2).expand(-1, -1, y.size(1)) |
| ry = yy.unsqueeze(1).expand(-1, x.size(1), -1) |
| P = rx + ry - 2*zz |
| return torch.mean(torch.min(P, dim=2)[0]) + torch.mean(torch.min(P, dim=1)[0]) |
| |
| def get_vq_weight(self, step): |
| vq_w_start = self.lambda_vq_start |
| vq_w_end = self.lambda_vq_final |
| if step >= self.warmup_steps: |
| return vq_w_end |
| |
| alpha = step / self.warmup_steps |
| return vq_w_start *(1 - alpha) + vq_w_end * alpha |
| |
| def compute_usage_loss(self, eps=1e-12): |
| """ |
| Use EMA cluster_size to compute global usage entropy loss |
| """ |
| cluster_size = self.ema_cluster_size |
| p = cluster_size / (cluster_size.sum() + eps) |
|
|
| entropy = - (p * (p + eps).log()).sum() |
| |
| usage_loss = -entropy |
| return usage_loss |
| |
|
|
| def forward(self, x, epoch, global_step): |
|
|
| |
| voxel_feats, voxel_coords, spatial_shape, x_normalized = self.point_cloud_to_voxel(x) |
| x_normalized = self.downsample(x_normalized) |
|
|
| |
| |
| f_hat, f_gt, vq_loss, token_label_codebook_idxs, vq_loss_dict, floatcoords = self.encode(voxel_feats, voxel_coords, spatial_shape, epoch) |
| vq_weight = self.get_vq_weight(epoch) |
| vq_loss = vq_weight * vq_loss |
|
|
| |
| reconstructed = self.normalize_to_minus1_1(self.decoder(f_hat)) |
| |
| |
| chamfer_loss = self.chamfer_distance(x_normalized, reconstructed) |
| chamfer_loss = self.lambda_chamfer * chamfer_loss |
|
|
| |
| construction_loss = F.smooth_l1_loss(reconstructed, floatcoords, reduction='mean') |
| construction_loss = self.lambda_construction * construction_loss |
|
|
| |
| usage_loss = self.compute_usage_loss() |
| usage_loss = self.usage_weight * usage_loss |
| |
| |
| total_loss = construction_loss + vq_loss + chamfer_loss + usage_loss |
|
|
| |
| self.update_ema(self.ema_decay) |
|
|
| |
| current_vq_pct = vq_loss.detach() / (vq_loss.detach() + construction_loss.detach() + chamfer_loss.detach()) |
| self.vq_stateful_controller(current_vq_pct, epoch) |
|
|
|
|
| |
| if global_step % 20 == 0: |
| print("[",epoch,"/",global_step,"/",x.shape[0],"]") |
| print("[Monitor] Token_feats(z_e) std: ", f_gt.std(dim=1).mean().item()) |
| print("[Monitor] Recon_feats(z_q) std: ", f_hat.std(dim=1).mean().item()) |
| active_codes = (self.ema_cluster_size > 1e-3).sum().item() |
| print("[Monitor] Active embeddings:", active_codes, "/", self.codebook_size) |
| print("[Monitor] Embedding weight std:",self.embeddings.weight.std().item()) |
| |
| probs = self.cluster_size / (self.cluster_size.sum() + 1e-10) |
| perplexity = torch.exp(-torch.sum(probs * torch.log(probs + 1e-10))) |
|
|
| print("[Monitor] Perplexity:", perplexity.item()) |
| |
| return reconstructed, total_loss, construction_loss, vq_loss, chamfer_loss, usage_loss |
|
|
| def vq_stateful_controller(self, current_vq_pct, epoch): |
| if epoch < 100: |
| return |
| self.vq_window.append(current_vq_pct) |
| vq_pct_ma = sum(self.vq_window) / len(self.vq_window) |
|
|
| if vq_pct_ma > self.safe_vq_pct_thr: |
| self.patience_counter += 1 |
| else: |
| self.patience_counter = 0 |
|
|
| if self.patience_counter >= 10 and self.triggers < 100: |
| self.lambda_vq_final = max(self.lambda_vq_final * 0.99, self.lowest_vq_weight) |
| self.triggers += 1 |
| self.patience_counter = 0 |