Source code for holovec.models.hrr

"""HRR (Holographic Reduced Representations) VSA model.

HRR uses circular convolution for binding, which provides approximate
inverse through circular correlation. This is the classic VSA model
developed by Tony Plate.

Properties:
- Binding: Circular convolution
- Unbinding: Circular correlation (approximate inverse)
- Commutative: Yes
- Exact inverse: No (approximate)
- Complexity: O(D log D) via FFT

References:
- Plate (1991): "Holographic Reduced Representations"
- Plate (1995): "Holographic reduced representations: Distributed
  representation for cognitive structures"
- Plate (2003): Full book on HRR
"""

from __future__ import annotations

from typing import Optional, Sequence

from ..backends import Backend
from ..backends.base import Array
from ..spaces import RealSpace, VectorSpace
from .base import VSAModel


[docs] class HRRModel(VSAModel): """HRR (Holographic Reduced Representations) model. Binding: circular convolution (via FFT) Unbinding: circular correlation (via FFT) Bundling: element-wise addition + normalization Permutation: circular shift Uses RealSpace with Gaussian distribution N(0, 1/D). """
[docs] def __init__( self, dimension: int = 10000, space: Optional[VectorSpace] = None, backend: Optional[Backend] = None, seed: Optional[int] = None ): """Initialize HRR model. Args: dimension: Dimensionality of hypervectors (recommend 1000-10000) space: Vector space (defaults to RealSpace) backend: Computational backend seed: Random seed for space """ if space is None: from ..backends import get_backend backend = backend if backend is not None else get_backend() space = RealSpace(dimension, backend=backend, seed=seed) super().__init__(space, backend)
@property def model_name(self) -> str: return "HRR" @property def is_self_inverse(self) -> bool: return False # Requires correlation, not same operation @property def is_commutative(self) -> bool: return True # Convolution is commutative @property def is_exact_inverse(self) -> bool: return False # Correlation gives approximate inverse
[docs] def bind(self, a: Array, b: Array) -> Array: """Bind using circular convolution. Implemented via FFT: conv(a, b) = IFFT(FFT(a) * FFT(b)) Args: a: First vector b: Second vector Returns: Bound vector c = a ⊛ b (circular convolution) """ # Circular convolution in frequency domain result = self.backend.circular_convolve(a, b) # Do NOT normalize - preserves magnitude for proper unbinding via # circular correlation. Normalization would interfere with the # mathematical relationship required for unbind recovery. return result
[docs] def unbind(self, a: Array, b: Array) -> Array: """Unbind using circular correlation (approximate inverse of convolution). This is the classic HRR unbinding operation that uses circular correlation to approximately recover the original vector from a bound pair. Args: a: Bound vector c = x ⊛ b (result of circular convolution) b: Key vector (second operand in binding) Returns: Approximate recovery of x (original vector), normalized to unit length Notes ----- **Mathematical Foundation:** HRR binding via circular convolution: c = x ⊛ b In frequency domain (Fourier): C(ω) = X(ω) · B(ω) Unbinding via circular correlation: x̂ = c ⋆ b = IFFT(C(ω) · B*(ω)) Where B*(ω) is the complex conjugate of B(ω). Substituting C(ω) = X(ω) · B(ω): x̂ = IFFT(X(ω) · B(ω) · B*(ω)) = IFFT(X(ω) · |B(ω)|²) For random vectors with approximately uniform power spectrum (|B(ω)|² ≈ 1), this gives x̂ ≈ x. **Approximation Quality:** Recovery similarity depends on: - Dimension D: Higher D → better recovery - Noise level: Clean binding → better unbind - Bundle size: More items → more interference Empirical performance (D=10000): - Clean unbind: similarity ≈ 0.65-0.75 (approximate inverse) - After bundling 2 items: similarity ≈ 0.57 - After bundling 10 items: similarity ≈ 0.30 - After bundling 100 items: similarity decreases further References ---------- - Plate (1995): "Holographic Reduced Representations" - Plate (2003): "Holographic Reduced Representations" (full book) Examples -------- >>> model = VSA.create('HRR', dim=10000) >>> x = model.random(seed=1) >>> b = model.random(seed=2) >>> c = model.bind(x, b) >>> x_recovered = model.unbind(c, b) >>> similarity = model.similarity(x, x_recovered) >>> print(f"Recovery similarity: {similarity:.3f}") # ~0.99 """ # Transform to frequency domain fa = self.backend.fft(a) fb = self.backend.fft(b) # Circular correlation: C(ω) * conj(B(ω)) # This is the classic HRR unbinding operation (Plate, 1995) fr = self.backend.multiply(fa, self.backend.conjugate(fb)) # Transform back to time domain time = self.backend.ifft(fr) # Take real part (imaginary part should be near zero due to real inputs) result = self.backend.real(time) # Normalize to unit length for consistent comparison with other vectors return self.normalize(result)
[docs] def bundle(self, vectors: Sequence[Array]) -> Array: """Bundle using element-wise addition (superposition). For HRR, bundling is simple vector addition without normalization. This preserves the magnitude relationships needed for proper unbinding. Args: vectors: Sequence of vectors to bundle Returns: Bundled vector (unnormalized sum) Raises: ValueError: If vectors is empty Notes: Unlike some VSA models, HRR does NOT normalize after bundling. Normalization would interfere with the circular correlation unbinding operation. The unbind() method handles normalization of its output. """ if not vectors: raise ValueError("Cannot bundle empty sequence") vectors = list(vectors) # Sum all vectors (simple superposition, no normalization) result = self.backend.sum(self.backend.stack(vectors, axis=0), axis=0) return result
[docs] def permute(self, vec: Array, k: int = 1) -> Array: """Permute using circular shift. Shifts vector elements by k positions to the right. Negative k shifts left. Args: vec: Vector to permute k: Number of positions to shift Returns: Permuted vector """ return self.backend.roll(vec, shift=k)
def __repr__(self) -> str: return (f"HRRModel(dimension={self.dimension}, " f"space={self.space.space_name}, " f"backend={self.backend.name})")