Skip to content

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:

Setup complete!

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

  1. Classification: All three models achieve good accuracy on structured data
  2. Noise Robustness: FHRR and Binary maintain similarity better under noise
  3. Capacity: Higher dimensions → more capacity; bundling degrades similarity
  4. Speed: MAP is typically fastest; FHRR uses FFT (still fast); Binary simple but needs more dims
  5. 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:

jupyter notebook examples/notebooks/tutorial_05_model_comparison.ipynb

Or copy the code snippets above into your own Python script or notebook!