Category-Quantized Feature-wise Linear Modulation Regressor
A novel deep learning architecture for tabular data that treats categorical features in a discrete quantized space, inspired by Vector Quantization from VQ-VAE.
Categorical features should be processed in a discrete space, not as continuous embeddings.
Traditional tabular deep learning models treat categorical features as continuous embeddings, which may not capture their inherently discrete nature. CQFiLM addresses this by:
- Vector Quantization: Inspired by VQ-VAE's discrete representation learning
- Discrete Latent Space: Categorical features are mapped to a finite codebook
- FiLM Conditioning: Quantized categorical information modulates numerical feature processing
Input: Categorical Features + Numerical Features
│
├─► Categorical Branch:
│ ├─ Embedding Layer (per category)
│ ├─ Shared Encoder
│ └─ Category Quantizer (VQ-inspired) ──► z_q (discrete codes)
│
└─► Numerical Branch:
├─ Linear Layer
├─ FiLM Layer (modulated by z_q)
└─ Linear Layer ──► Conditioned Numerical Features
┌───────────────────────────────┐
│ Concatenate [z_q, num_latent] │
└───────────────────────────────┘
│
Regression Head
│
Output
Given categorical embeddings z_e, find the nearest codebook vector:
z_q = argmin_{e_k ∈ Codebook} ||z_e - e_k||²
Loss Components:
L_codebook = ||z_e - sg(z_q)||² # Move codebook towards encoder
L_commit = β ||sg(z_e) - z_q||² # Commitment loss
L_vq = L_codebook + L_commit
where sg() denotes stop-gradient operation.
Categorical information modulates numerical features:
FiLM(x, γ, β) = γ ⊙ x + β
where:
γ = W_γ(z_q) # Scale parameter
β = W_β(z_q) # Shift parameter
h_num = FiLM(ReLU(W₁(x_num)), γ, β)
h_final = Concat([z_q, ReLU(W₂(h_num))])
ŷ = MLP_head(h_final)
class CategoryQuantizer(nn.Module):
"""
Vector Quantization layer inspired by VQ-VAE.
Maps continuous embeddings to discrete codebook entries.
"""
def __init__(self, num_embeddings, embedding_dim, commitment_cost):
# num_embeddings: Size of the codebook (K)
# embedding_dim: Dimension of each code vector
# commitment_cost: Weight for commitment loss (β)Key Features:
- Discrete latent space representation
- Straight-through estimator for backpropagation
- Learnable codebook with commitment loss
class FiLMLayer(nn.Module):
"""
Feature-wise Linear Modulation.
Applies affine transformation conditioned on categorical codes.
"""
def forward(self, x, gamma, beta):
return gamma * x + betaPurpose: Allows categorical information to dynamically adjust numerical feature processing.
class CQFiLM_RegressionModel(nn.Module):
"""
Main model architecture combining:
- Categorical embedding + quantization
- Numerical feature processing with FiLM
- Regression head
"""Architecture Details:
- Categorical Path: Embedding → Encoder → Quantizer →
z_q - Numerical Path: Linear → FiLM(conditioned by
z_q) → Linear - Output: Concat → BatchNorm → Dropout → Regression
Category "A" → [0.23, 0.45, 0.12, ...] (any real values)
Category "B" → [0.67, 0.21, 0.89, ...]
❌ Categories can drift anywhere in continuous space
Category "A" → Codebook Entry #5 → [0.2, 0.5, 0.1, ...]
Category "B" → Codebook Entry #12 → [0.7, 0.2, 0.9, ...]
✅ Categories are forced to choose from a finite set of "prototypes"
- Structured Representation: Categories share a common discrete codebook
- Regularization: Prevents overfitting by limiting representation space
- Interpretability: Each code in the codebook represents a categorical "prototype"
- Generalization: Similar categories map to nearby codebook entries
from CQFiLMRegressor import CQFiLM_Wrapper
# Define features
cat_features = ['gender', 'department', 'diagnosis']
num_features = ['age', 'blood_pressure', 'heart_rate', 'temperature']
target_col = 'length_of_stay'
# Initialize model
model = CQFiLM_Wrapper(
cat_features=cat_features,
num_features=num_features,
target_col=target_col,
num_cq_embeddings=64, # Codebook size (K)
embedding_dim=16, # Code dimension
commitment_cost=0.25, # β in VQ loss
cat_embedding_dim=8, # Initial categorical embedding
num_hidden_dim=32, # Hidden dimension for numerical path
learning_rate=0.001,
batch_size=32,
epochs=100,
seed=42
)
# Train
model.fit(X_train, y_train)
# Predict
predictions = model.predict(X_test)
# Evaluate
r2 = model.score(X_test, y_test)from sklearn.model_selection import GridSearchCV
param_grid = {
'num_cq_embeddings': [32, 64, 128],
'embedding_dim': [8, 16, 32],
'commitment_cost': [0.1, 0.25, 0.5],
'learning_rate': [0.001, 0.0001]
}
grid_search = GridSearchCV(
CQFiLM_Wrapper(cat_features=cat_features, num_features=num_features),
param_grid,
cv=5,
scoring='r2'
)
grid_search.fit(X_train, y_train)The current implementation (CQFiLM_RegressionModel) is designed for regression tasks with:
- L1 Loss (MAE)
- MinMaxScaler for target normalization
- Single continuous output
CQFiLM can be easily adapted for classification tasks:
# Classification variant (conceptual)
class CQFiLM_ClassificationModel(nn.Module):
def __init__(self, ..., num_classes):
# Same architecture as regression
# Replace regression head with:
self.classification_head = nn.Sequential(
nn.Linear(final_input_dim, 64),
nn.ReLU(),
nn.BatchNorm1d(64),
nn.Dropout(0.1),
nn.Linear(64, num_classes) # Multi-class output
)Loss Function:
- Cross-Entropy Loss for classification
- Combined with VQ loss:
L_total = L_ce + L_vq
| Parameter | Description | Default | Range |
|---|---|---|---|
num_cq_embeddings |
Codebook size (K) | 64 | 16-256 |
embedding_dim |
Dimension of each code | 16 | 8-64 |
commitment_cost |
β in VQ loss | 0.25 | 0.1-1.0 |
Guidelines:
- Larger codebook: More expressive but may lead to codebook collapse
- Higher commitment cost: Forces encoder to commit to codes
- Embedding dim: Should match the complexity of categorical relationships
| Parameter | Description | Default |
|---|---|---|
cat_embedding_dim |
Initial categorical embedding | 8 |
num_hidden_dim |
Numerical path hidden dim | 32 |
learning_rate |
Adam learning rate | 0.001 |
batch_size |
Training batch size | 32 |
epochs |
Training epochs | 100 |
L_total = L_regression + L_vq
= L_regression + (L_codebook + β * L_commit)
where:
L_regression: L1 Loss (MAE) between prediction and targetL_codebook: Moves codebook towards encoder outputL_commit: Encourages encoder to commit to chosen codes
-
Forward Pass:
- Encode categorical features →
z_e - Quantize →
z_q(with VQ loss) - Modulate numerical features via FiLM
- Predict output
- Encode categorical features →
-
Loss Calculation:
- Compute regression loss
- Add VQ loss from quantizer
- Total loss = regression + VQ
-
Backward Pass:
- Straight-through estimator for quantization
- Update encoder, quantizer, and predictor
- Missing values: Handled with placeholder
'__MISSING__' - Encoding: Automatic factorization (0, 1, 2, ...)
- Unseen categories: Mapped to missing value code
- Missing values: Imputed with column means
- Scaling: StandardScaler (z-score normalization)
- NaN handling: Automatic imputation pipeline
- Scaling: MinMaxScaler [0, 1]
- Inverse transform: Automatic in
.predict()
Problem: Some codebook entries may never be used, leading to an effectively smaller codebook.
Symptoms:
- Only a few codes are selected during training
- Reduced model expressiveness
- Suboptimal performance
Potential Solutions:
# 1. Exponential Moving Average (EMA) updates
# 2. Codebook reset: Replace unused codes
# 3. Entropy regularization: Encourage uniform code usage
# 4. Gumbel-Softmax relaxation
# 5. Commitment cost schedulingFuture Implementation:
class CategoryQuantizer(nn.Module):
def __init__(self, ..., use_ema=True, decay=0.99):
# EMA updates for codebook
self.use_ema = use_ema
self.decay = decay
self.cluster_size = nn.Parameter(
torch.zeros(num_embeddings), requires_grad=False
)Current: Uniform initialization U(-1/K, 1/K)
Alternatives:
- K-means clustering on training data
- Random selection from encoder outputs
- Xavier/Kaiming initialization
Limitation: Memory scales with O(K * d) for codebook
Solutions:
- Product quantization (split into subspaces)
- Hierarchical codebooks
- Adaptive codebook size
-
High-cardinality categorical features
- Many categories benefit from shared codebook structure
-
Mixed data types
- Both categorical and numerical features present
- Complex interactions between feature types
-
Structured categorical relationships
- Categories have underlying similarity structure
- VQ can learn meaningful category prototypes
-
Small to medium datasets
- Discrete representation provides regularization
- Prevents overfitting compared to free embeddings
-
Pure numerical data
- Use standard MLP/TabNet instead
-
Very few categorical features
- Overhead of VQ not justified
-
Extremely large datasets
- Training time may be prohibitive
- Consider simpler models first
# Rule of thumb: K ≈ sqrt(num_categories) to 2*sqrt(num_categories)
num_categories = sum(n_unique for cat in categorical_features)
num_cq_embeddings = int(np.sqrt(num_categories) * 1.5)# Start with β = 0.25
# Increase if codes aren't being used (collapse)
# Decrease if encoder output deviates too much from codes# Cosine annealing or ReduceLROnPlateau
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=epochs
)# Monitor validation loss
# Stop if no improvement for N epochs# Extract learned codebook
codebook = model.model.quantizer.embedding.weight.data.cpu().numpy()
# Visualize with t-SNE/UMAP
from sklearn.manifold import TSNE
tsne = TSNE(n_components=2)
codebook_2d = tsne.fit_transform(codebook)
plt.scatter(codebook_2d[:, 0], codebook_2d[:, 1])# Track which codes are used during inference
def get_code_usage(model, X):
model.eval()
with torch.no_grad():
z_e = model.encode(cat_embeddings)
_, _, codes = model.quantizer(z_e)
return codes.cpu().numpy()# Analyze γ and β values
# High magnitude indicates strong modulation
gamma_values = model.model.film_gamma_generator1.weight.data
beta_values = model.model.film_beta_generator1.weight.data- VQ-VAE: Neural Discrete Representation Learning (van den Oord et al., 2017)
- VQ-VAE-2: Generating Diverse High-Fidelity Images (Razavi et al., 2019)
- FiLM: Visual Reasoning with a General Conditioning Layer (Perez et al., 2017)
torch>=2.0.0
numpy>=1.20.0
pandas>=1.3.0
scikit-learn>=1.0.0
# Automatic GPU/CPU selection
device = 'cuda' if torch.cuda.is_available() else 'cpu'
# Manual override
model = CQFiLM_Wrapper(..., device='cuda:0')# Fixed seed for all random operations
model = CQFiLM_Wrapper(..., seed=42)
# Sets: random, numpy, torch, torch.cuda