# Architecture of Motion-S RVQ-VAE This implementation is a **Residual Vector Quantized Variational Autoencoder (RVQ-VAE)** designed for motion sequence compression and tokenization. Let me break down each component: --- ## 1. **Overall Architecture Flow** ``` Input Motion (B, D_POSE, N) ↓ [Encoder] → Continuous Latent (B, d, n) ↓ [RVQ] → Quantized Latent (B, d, n) + Discrete Tokens ↓ [Decoder] → Reconstructed Motion (B, D_POSE, N) ``` Where: - **B** = Batch size - **D_POSE** = Motion feature dimension (e.g., 263 for body pose) - **N** = Original sequence length (frames) - **d** = Latent dimension (default 256) - **n** = Downsampled sequence length (N // downsampling_ratio) --- ## 2. **MotionEncoder: Convolutional Downsampling** ### Purpose Compresses motion sequences both **spatially** (D_POSE → d) and **temporally** (N → n). ### Architecture ```python Input: (B, D_POSE, N) # Treats D_POSE as channels, N as sequence length Layer Structure (4 layers default): Conv1D(D_POSE → 512, kernel=3, stride=2, padding=1) # Temporal downsampling ReLU + BatchNorm Conv1D(512 → 512, kernel=3, stride=2, padding=1) # More downsampling ReLU + BatchNorm Conv1D(512 → 512, kernel=3, stride=1, padding=1) # Maintain resolution ReLU + BatchNorm Conv1D(512 → 256, kernel=3, stride=1, padding=1) # Project to latent_dim ReLU + BatchNorm Output: (B, 256, n) # n ≈ N/4 for downsampling_ratio=4 ``` ### Key Design Choices - **Stride=2 for first log₂(ratio) layers**: Achieves 4x downsampling with two stride-2 convolutions - **BatchNorm**: Stabilizes training by normalizing activations - **1D Convolutions**: Efficient for sequential data vs 2D/RNNs --- ## 3. **ResidualVectorQuantizer (RVQ): Hierarchical Quantization** ### Purpose Converts continuous latents into **discrete tokens** using a codebook hierarchy. ### Core Concept: Residual Quantization Instead of quantizing once, RVQ quantizes the **residual error** iteratively: ``` Step 0: Quantize input → b⁰ = Q₀(r⁰), where r⁰ = continuous latent Step 1: Quantize residual → b¹ = Q₁(r¹), where r¹ = r⁰ - b⁰ Step 2: Quantize new residual → b² = Q₂(r²), where r² = r¹ - b¹ ... Step V: Final residual → bⱽ = Qᵥ(rⱽ) Final Output: Σ(b⁰, b¹, ..., bⱽ) # Sum of all quantized codes ``` ### Architecture ```python num_quantizers = 6 # V+1 layers (0 to 5) For each layer v: 1. Calculate distances to codebook: distances = ||z - embedding||² # (B*n, num_embeddings) 2. Find nearest code: indices = argmin(distances) # (B*n,) 3. Lookup quantized vector: quantized = embedding[:, indices] # (B, d, n) 4. Compute next residual: residual = residual - quantized ``` ### VectorQuantizer: Single-Layer Quantization Each layer has: - **Codebook**: `embedding` tensor of shape `(d, num_embeddings=512)` - 512 learnable code vectors, each of dimension 256 - **EMA Updates** (Exponential Moving Average): ```python cluster_size = (1-decay) * new_counts + decay * old_counts embedding_avg = (1-decay) * new_codes + decay * old_codes embedding = embedding_avg / cluster_size # Normalize ``` - Prevents codebook collapse (dead codes) - No explicit gradient descent on codebook - **Straight-Through Estimator**: ```python quantized_st = inputs + (quantized - inputs).detach() ``` - Forward: Use quantized values - Backward: Gradients flow through inputs (bypassing non-differentiable argmin) - **Commitment Loss**: ```python loss = λ * ||quantized - inputs||² ``` - Encourages encoder to produce latents close to codebook entries --- ## 4. **MotionDecoder: Convolutional Upsampling** ### Purpose Reconstructs original motion from quantized latent. ### Architecture ```python Input: (B, 256, n) Layer Structure (mirror of encoder): ConvTranspose1D(256 → 512, kernel=3, stride=2, padding=1, output_padding=1) ReLU + BatchNorm ConvTranspose1D(512 → 512, kernel=3, stride=2, padding=1, output_padding=1) ReLU + BatchNorm Conv1D(512 → 512, kernel=3, stride=1, padding=1) ReLU + BatchNorm Conv1D(512 → D_POSE, kernel=3, stride=1, padding=1) # Final layer, no activation Output: (B, D_POSE, N) # Restored to original dimensions ``` ### Key Design Choices - **ConvTranspose1D**: Learns upsampling (better than fixed interpolation) - **output_padding**: Ensures exact size matching after strided convolutions - **No activation on final layer**: Allows unrestricted output range --- ## 5. **Loss Function: Multi-Component Objective** ```python Total Loss = L_rec + λ_global·L_global + λ_commit·L_commit + λ_vel·L_vel ``` ### Components 1. **Reconstruction Loss** (L_rec): ```python L_rec = SmoothL1(reconstructed, target) ``` - Main objective: Match overall motion 2. **Global/Root Loss** (L_global): ```python L_global = SmoothL1(reconstructed[:, :4], target[:, :4]) ``` - Focuses on first 4 dimensions: - Root rotation velocity - Root linear velocity (X/Z) - Root height - Weighted 1.5x to prioritize global motion 3. **Velocity Loss** (L_vel): ```python pred_vel = reconstructed[:, :, 1:] - reconstructed[:, :, :-1] target_vel = target[:, :, 1:] - target[:, :, :-1] L_vel = SmoothL1(pred_vel, target_vel) ``` - Ensures temporal smoothness - Prevents jittery motion - Weighted 2.0x for importance 4. **Commitment Loss** (L_commit): ```python L_commit = Σ(||quantized_v - inputs_v||²) for all RVQ layers ``` - From RVQ: encourages encoder outputs near codebook - Weighted 0.02x (small to avoid over-constraining) --- ## 6. **Training Features** ### Quantization Dropout ```python if training and rand() < 0.2: num_active_layers = randint(1, num_quantizers+1) ``` - Randomly uses 1 to V+1 quantization layers - Improves robustness and generalization - Forces lower layers to capture more information ### Masking Support ```python loss = mean_flat(error * mask) / (mask.sum() + ε) ``` - Handles variable-length sequences with padding - Only computes loss on valid frames --- ## 7. **Token Representation** ### Encoding to Tokens ```python tokens = [indices_0, indices_1, ..., indices_V] # List of (B, n) tensors ``` - Each token sequence represents one RVQ layer - Token values ∈ [0, 511] (for 512 codebook entries) - Total vocabulary size: 512^(V+1) combinations ### Decoding from Tokens ```python quantized = Σ(embedding[:, tokens_v]) for v in layers reconstructed = decoder(quantized) ``` - Lookup codes from each layer's codebook - Sum all codes to get final latent - Pass through decoder --- ## 8. **Key Hyperparameters** | Parameter | Default | Purpose | |-----------|---------|---------| | `input_dim` | 263 | Motion feature dimension | | `latent_dim` | 256 | Bottleneck dimension | | `downsampling_ratio` | 4 | Temporal compression (N → N/4) | | `num_quantizers` | 6 | RVQ hierarchy depth (V+1) | | `num_embeddings` | 512 | Codebook size per layer | | `commitment_cost` | 1.0 | Weight for commitment loss | | `decay` | 0.99 | EMA decay for codebook updates | | `quantization_dropout` | 0.2 | Probability of layer dropout | --- --- ## 9. **Usage Example** ```python # Training model = RVQVAE(input_dim=263, output_dim=263) reconstructed, tokens, commit_loss = model(motion) total_loss, loss_dict = compute_rvq_loss(reconstructed, motion, commit_loss) # Inference: Motion → Tokens tokens = model.encode_to_tokens(motion) # List of (B, n) discrete tokens # Inference: Tokens → Motion reconstructed = model.decode_from_tokens(tokens) ``` --- license: apache-2.0 ---