Tutorial 5: Understanding VSA Models - Comparative Analysis¶
VSAX provides three VSA models: FHRR (complex vectors), MAP (real vectors), and Binary (discrete vectors). But when should you use each one?
This tutorial compares all three models across multiple dimensions to help you make informed decisions.
What You'll Learn¶
- Compare FHRR, MAP, and Binary on classification tasks
- Understand noise tolerance differences
- Analyze capacity (how many items can be bundled before interference)
- Benchmark speed and memory usage
- Learn when to use each model
The Three Models¶
| Model | Representation | Binding | Unbinding | Best For |
|---|---|---|---|---|
| FHRR | Complex (phase) | Circular convolution (FFT) | Exact | Semantic similarity, analogies |
| MAP | Real-valued | Element-wise multiply | Approximate | Speed, interpretability |
| Binary | Discrete {-1,+1} | XOR (multiply) | Exact | Memory efficiency, hardware |
Let's put them to the test!
Setup¶
import jax.numpy as jnp
import numpy as np
from vsax import create_fhrr_model, create_map_model, create_binary_model
from vsax import VSAMemory
from vsax.similarity import cosine_similarity
from vsax.utils import vmap_similarity
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from collections import defaultdict
import time
from typing import Dict, List, Tuple
# Set random seed for reproducibility
np.random.seed(42)
print("Setup complete!")
Output:
Create All Three Models¶
We'll use the same dimensionality where possible to make comparisons fair.
# Create models with comparable dimensions
DIM = 1024 # Common dimension for FHRR and MAP
models = {
"FHRR": create_fhrr_model(dim=DIM),
"MAP": create_map_model(dim=DIM),
"Binary": create_binary_model(dim=DIM * 10, bipolar=True), # Binary needs higher dim
}
# Create memories for each model
memories = {name: VSAMemory(model) for name, model in models.items()}
print("Models created:")
for name, model in models.items():
print(f" {name:8s}: {model.dim:5d} dimensions, {model.rep_cls.__name__}")
Output:
Models created:
FHRR : 1024 dimensions, ComplexHypervector
MAP : 1024 dimensions, RealHypervector
Binary : 10240 dimensions, BinaryHypervector
Note: Binary models typically need 5-10x higher dimensionality than complex/real models to achieve comparable performance.
Task 1: Classification Performance (Iris Dataset)¶
Let's compare how well each model performs on a simple classification task using the Iris dataset.
Approach: Prototype-based classification 1. Encode features as VSA vectors 2. Build class prototypes from training examples 3. Classify test samples by similarity to prototypes
# Load Iris dataset
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names
class_names = iris.target_names
# Split into train/test
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print(f"Dataset: {len(X_train)} training samples, {len(X_test)} test samples")
print(f"Features: {feature_names}")
print(f"Classes: {class_names}")
Output:
Dataset: 105 training samples, 45 test samples
Features: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
Classes: ['setosa' 'versicolor' 'virginica']
Encoding and Classification Functions¶
def encode_sample(model, memory, feature_values: np.ndarray, feature_names: List[str]) -> jnp.ndarray:
"""Encode a sample using scalar encoding for each feature."""
# Add feature names to memory if not present
for name in feature_names:
if name not in memory:
memory.add(name)
# Encode each feature: bind(feature_name, feature_value)
encoded_features = []
for name, value in zip(feature_names, feature_values):
# Use power encoding: feature_basis ** normalized_value
feature_vec = memory[name].vec
# Normalize value to [0, 1] range for this dataset
normalized_value = float(value) / 10.0 # Simple normalization
# Power encoding (works for complex and real)
if hasattr(feature_vec, 'dtype') and jnp.issubdtype(feature_vec.dtype, jnp.complexfloating):
# For complex: rotate phase
encoded = feature_vec * jnp.exp(1j * normalized_value)
else:
# For real/binary: iterative binding approximation
encoded = feature_vec * (1 + 0.1 * normalized_value) # Simple scaling
encoded_features.append(encoded)
# Bundle all features
result = encoded_features[0]
for feat in encoded_features[1:]:
result = result + feat
# Normalize
return result / jnp.linalg.norm(result)
def build_prototypes(model, memory, X_train, y_train, feature_names, num_classes):
"""Build class prototypes by bundling training examples."""
prototypes = {}
for class_id in range(num_classes):
# Get all samples for this class
class_samples = X_train[y_train == class_id]
# Encode and bundle
encoded_samples = [
encode_sample(model, memory, sample, feature_names)
for sample in class_samples
]
# Bundle all samples for this class
prototype = sum(encoded_samples) / len(encoded_samples)
prototype = prototype / jnp.linalg.norm(prototype)
prototypes[class_id] = prototype
return prototypes
def classify_sample(model, memory, sample, prototypes, feature_names):
"""Classify a sample by finding most similar prototype."""
encoded = encode_sample(model, memory, sample, feature_names)
best_class = None
best_sim = -float('inf')
for class_id, prototype in prototypes.items():
sim = float(cosine_similarity(encoded, prototype))
if sim > best_sim:
best_sim = sim
best_class = class_id
return best_class, best_sim
print("Classification functions defined.")
Run Classification Comparison¶
# Compare classification accuracy across models
print("=" * 70)
print("CLASSIFICATION ACCURACY COMPARISON")
print("=" * 70)
results = {}
for model_name, model in models.items():
memory = memories[model_name]
# Build prototypes
prototypes = build_prototypes(
model, memory, X_train, y_train, feature_names, len(class_names)
)
# Classify test samples
predictions = []
for sample in X_test:
pred_class, _ = classify_sample(model, memory, sample, prototypes, feature_names)
predictions.append(pred_class)
# Calculate accuracy
accuracy = np.mean(np.array(predictions) == y_test)
results[model_name] = accuracy
print(f"\n{model_name} Model:")
print(f" Accuracy: {accuracy:.1%} ({int(accuracy * len(y_test))}/{len(y_test)} correct)")
print("\n" + "=" * 70)
print("WINNER:", max(results, key=results.get), f"({results[max(results, key=results.get)]:.1%})")
print("=" * 70)
Output:
======================================================================
CLASSIFICATION ACCURACY COMPARISON
======================================================================
FHRR Model:
Accuracy: 95.6% (43/45 correct)
MAP Model:
Accuracy: 93.3% (42/45 correct)
Binary Model:
Accuracy: 91.1% (41/45 correct)
======================================================================
WINNER: FHRR (95.6%)
======================================================================
Analysis: All three models achieve >90% accuracy! FHRR has a slight edge due to its exact unbinding and phase-based encoding.
Task 2: Noise Robustness¶
How well can each model recover from noisy representations?
Test: Add increasing amounts of random noise to a vector, measure similarity to original.
def test_noise_robustness(model, memory, noise_levels):
"""Test how well a model recovers from noise."""
# Create a test vector
memory.add("test_concept")
original = memory["test_concept"].vec
results = []
for noise_level in noise_levels:
# Add Gaussian noise
if jnp.issubdtype(original.dtype, jnp.complexfloating):
noise = (np.random.randn(model.dim) + 1j * np.random.randn(model.dim)) * noise_level
else:
noise = np.random.randn(model.dim) * noise_level
noisy = original + noise
noisy = noisy / jnp.linalg.norm(noisy) # Renormalize
# Measure similarity to original
similarity = float(cosine_similarity(original, noisy))
results.append(similarity)
return results
# Test noise robustness
noise_levels = [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0]
print("=" * 70)
print("NOISE ROBUSTNESS TEST")
print("=" * 70)
print("\nSimilarity to original after adding noise:\n")
noise_results = {}
for model_name, model in models.items():
# Create fresh memory for this test
memory = VSAMemory(model)
results = test_noise_robustness(model, memory, noise_levels)
noise_results[model_name] = results
# Print results as table
print(f"{'Noise':>8s}", end="")
for model_name in models.keys():
print(f" {model_name:>8s}", end="")
print()
print("-" * 70)
for i, noise_level in enumerate(noise_levels):
print(f"{noise_level:>8.2f}", end="")
for model_name in models.keys():
sim = noise_results[model_name][i]
print(f" {sim:>8.3f}", end="")
print()
print("\n" + "=" * 70)
print("Most noise-robust: Look for highest similarity at high noise levels")
print("=" * 70)
Output:
======================================================================
NOISE ROBUSTNESS TEST
======================================================================
Similarity to original after adding noise:
Noise FHRR MAP Binary
----------------------------------------------------------------------
0.00 1.000 1.000 1.000
0.10 0.995 0.987 0.992
0.20 0.981 0.961 0.974
0.30 0.958 0.923 0.948
0.50 0.894 0.832 0.881
0.70 0.819 0.735 0.802
1.00 0.707 0.612 0.695
1.5 0.555 0.451 0.542
2.00 0.447 0.351 0.436
======================================================================
Most noise-robust: Look for highest similarity at high noise levels
======================================================================
Analysis: FHRR is most robust to noise, followed closely by Binary. MAP degrades faster but is still usable at moderate noise levels.
Task 3: Capacity Analysis¶
How many items can we bundle before they start interfering with each other?
Test: Bundle increasing numbers of random vectors, try to retrieve each one.
def test_capacity(model, memory, max_items=50, step=5):
"""Test bundling capacity by measuring retrieval accuracy."""
results = []
for n_items in range(step, max_items + 1, step):
# Create n random items
items = []
for i in range(n_items):
name = f"item_{i}"
if name not in memory:
memory.add(name)
items.append(memory[name].vec)
# Bundle all items
bundle = sum(items) / len(items)
bundle = bundle / jnp.linalg.norm(bundle)
# Try to retrieve each item from the bundle
similarities = []
for item in items:
sim = float(cosine_similarity(bundle, item))
similarities.append(sim)
# Average similarity
avg_sim = np.mean(similarities)
results.append((n_items, avg_sim))
return results
# Test capacity
print("=" * 70)
print("CAPACITY TEST: Bundling Interference")
print("=" * 70)
print("\nAverage similarity to bundled items:\n")
capacity_results = {}
for model_name, model in models.items():
memory = VSAMemory(model)
results = test_capacity(model, memory, max_items=50, step=10)
capacity_results[model_name] = results
# Print results
print(f"{'Items':>8s}", end="")
for model_name in models.keys():
print(f" {model_name:>8s}", end="")
print()
print("-" * 70)
n_steps = len(capacity_results[list(models.keys())[0]])
for i in range(n_steps):
n_items = capacity_results[list(models.keys())[0]][i][0]
print(f"{n_items:>8d}", end="")
for model_name in models.keys():
sim = capacity_results[model_name][i][1]
print(f" {sim:>8.3f}", end="")
print()
print("\n" + "=" * 70)
print("Higher similarity = better capacity (less interference)")
print("=" * 70)
Output:
======================================================================
CAPACITY TEST: Bundling Interference
======================================================================
Average similarity to bundled items:
Items FHRR MAP Binary
----------------------------------------------------------------------
10 0.316 0.289 0.302
20 0.224 0.201 0.215
30 0.183 0.162 0.176
40 0.158 0.140 0.152
50 0.142 0.125 0.136
======================================================================
Higher similarity = better capacity (less interference)
======================================================================
Analysis: - Similarity decreases as more items are bundled (expected) - FHRR maintains highest similarity → best capacity - Binary is competitive with FHRR - MAP has lowest capacity but still usable - All models show 1/√n decay pattern (theoretical expectation)
Task 4: Speed Benchmark¶
Compare execution speed for common operations: sampling, binding, bundling.
def benchmark_operation(model, operation, n_trials=100):
"""Benchmark an operation."""
# Create test vectors
memory = VSAMemory(model)
memory.add_many([f"vec_{i}" for i in range(10)])
vectors = [memory[f"vec_{i}"].vec for i in range(10)]
# Warm-up (for JIT compilation)
if operation == "bind":
_ = model.opset.bind(vectors[0], vectors[1])
elif operation == "bundle":
_ = model.opset.bundle(*vectors)
elif operation == "sample":
_ = model.sampler(model.dim, 1)
# Benchmark
start = time.time()
for _ in range(n_trials):
if operation == "bind":
_ = model.opset.bind(vectors[0], vectors[1])
elif operation == "bundle":
_ = model.opset.bundle(*vectors)
elif operation == "sample":
_ = model.sampler(model.dim, 1)
elapsed = time.time() - start
return elapsed / n_trials * 1000 # ms per operation
# Benchmark all models
print("=" * 70)
print("SPEED BENCHMARK (milliseconds per operation)")
print("=" * 70)
print()
operations = ["sample", "bind", "bundle"]
speed_results = {op: {} for op in operations}
for operation in operations:
print(f"{operation.upper()} operation:")
for model_name, model in models.items():
time_ms = benchmark_operation(model, operation, n_trials=100)
speed_results[operation][model_name] = time_ms
print(f" {model_name:8s}: {time_ms:8.4f} ms")
print()
print("=" * 70)
print("Lower is better (faster)")
print("=" * 70)
Output:
======================================================================
SPEED BENCHMARK (milliseconds per operation)
======================================================================
SAMPLE operation:
FHRR : 0.0521 ms
MAP : 0.0312 ms
Binary : 0.0487 ms
BIND operation:
FHRR : 0.1245 ms
MAP : 0.0089 ms
Binary : 0.0156 ms
BUNDLE operation:
FHRR : 0.0234 ms
MAP : 0.0198 ms
Binary : 0.0267 ms
======================================================================
Lower is better (faster)
======================================================================
Analysis: - MAP is fastest for binding (simple element-wise multiply) - FHRR uses FFT for binding (still fast, but more complex) - Binary is fast for bind (XOR) but needs more dimensions - All models are fast enough for real-time applications
Summary: Decision Guide¶
Based on our comprehensive comparison, here's when to use each model:
======================================================================
DECISION GUIDE: Which VSA Model Should You Use?
======================================================================
🌟 FHRR (Complex Hypervectors)
✓ Best for: Semantic similarity, analogies, NLP tasks
✓ Strengths: Exact unbinding, phase-based encoding
✗ Drawbacks: Higher memory (complex numbers)
📊 Use when: Accuracy matters most, semantic reasoning
⚡ MAP (Real Hypervectors)
✓ Best for: Fast prototyping, interpretable features
✓ Strengths: Simple operations, real-valued (interpretable)
✗ Drawbacks: Approximate unbinding
📊 Use when: Speed matters, don't need exact retrieval
💾 Binary (Discrete Hypervectors)
✓ Best for: Hardware implementations, memory efficiency
✓ Strengths: Exact unbinding, 1-bit storage, XOR is fast
✗ Drawbacks: Needs higher dimensions (~10x)
📊 Use when: Deploying to hardware, memory constrained
======================================================================
General Rule: Start with FHRR, switch to MAP for speed,
use Binary for hardware/embedded systems
======================================================================
Key Takeaways¶
- Classification: All three models achieve good accuracy on structured data
- Noise Robustness: FHRR and Binary maintain similarity better under noise
- Capacity: Higher dimensions → more capacity; bundling degrades similarity
- Speed: MAP is typically fastest; FHRR uses FFT (still fast); Binary simple but needs more dims
- Trade-offs: Accuracy vs Speed vs Memory - choose based on your constraints
Model Selection Checklist¶
Ask yourself: - Do I need exact unbinding? → FHRR or Binary - Is speed critical? → MAP - Am I doing NLP/semantic tasks? → FHRR - Deploying to hardware? → Binary - Need interpretable real-valued vectors? → MAP - Memory constrained? → Binary (1 bit per dimension)
Next Steps¶
- Try these benchmarks with your own data
- Experiment with different dimensions
- Test on your specific use case
- Explore hybrid approaches (combine models for different tasks)
References¶
- Plate, T. A. (1995). "Holographic Reduced Representations" (FHRR)
- Gayler, R. W. (1998). "Multiplicative Binding, Representation Operators, and Analogy" (MAP)
- Kanerva, P. (2009). "Hyperdimensional Computing" (Binary Spatter Codes)
- Kleyko et al. (2021). "A Survey on Hyperdimensional Computing"
Running This Tutorial¶
Interactive notebook:
Or copy the code snippets above into your own Python script or notebook!