Source code for torchhd.tensors.fhrr

#
# MIT License
#
# Copyright (c) 2023 Mike Heddes, Igor Nunes, Pere Vergés, Denis Kleyko, and Danny Abraham
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import math
import torch
from typing import Set
from torch import Tensor
import torch.nn.functional as F

from torchhd.tensors.base import VSATensor

type_conversion = {
    torch.complex64: torch.float32,
    torch.complex128: torch.float64,
}


[docs] class FHRRTensor(VSATensor): """Fourier Holographic Reduced Representation Proposed in `Holographic Reduced Representation: Distributed Representation for Cognitive Structures <https://philpapers.org/rec/PLAHRR/>`_, this model uses complex phaser hypervectors. """ supported_dtypes: Set[torch.dtype] = {torch.complex64, torch.complex128}
[docs] @classmethod def empty( cls, num_vectors: int, dimensions: int, *, dtype=torch.complex64, device=None, requires_grad=False, ) -> "FHRRTensor": """Creates a set of hypervectors representing empty sets. When bundled with a random-hypervector :math:`x`, the result is :math:`x`. The empty vector of the FHRR model is a set of 0 values in both real and imaginary part. Args: num_vectors (int): the number of hypervectors to generate. dimensions (int): the dimensionality of the hypervectors. dtype (``torch.dtype``, optional): the desired data type of returned tensor. Default: if ``None`` is torch.complex64. device (``torch.device``, optional): the desired device of returned tensor. Default: if ``None``, uses the current device for the default tensor type (see torch.set_default_tensor_type()). ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. requires_grad (bool, optional): If autograd should record operations on the returned tensor. Default: ``False``. Examples:: >>> torchhd.FHRRTensor.empty(3, 6) FHRR([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]]) >>> torchhd.FHRRTensor.empty(3, 6, dtype=torch.complex128) FHRR([[0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], [0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j]], dtype=torch.complex128) """ if dtype is None: dtype = torch.complex64 if dtype not in cls.supported_dtypes: name = cls.__name__ options = ", ".join([str(x) for x in cls.supported_dtypes]) raise ValueError(f"{name} vectors must be one of dtype {options}.") result = torch.zeros( num_vectors, dimensions, dtype=dtype, device=device, requires_grad=requires_grad, ) return result.as_subclass(cls)
[docs] @classmethod def identity( cls, num_vectors: int, dimensions: int, *, dtype=torch.complex64, device=None, requires_grad=False, ) -> "FHRRTensor": """Creates a set of identity hypervectors. When bound with a random-hypervector :math:`x`, the result is :math:`x`. Args: num_vectors (int): the number of hypervectors to generate. dimensions (int): the dimensionality of the hypervectors. dtype (``torch.dtype``, optional): the desired data type of returned tensor. Default: if ``None`` is torch.complex64. device (``torch.device``, optional): the desired device of returned tensor. Default: if ``None``, uses the current device for the default tensor type (see torch.set_default_tensor_type()). ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. requires_grad (bool, optional): If autograd should record operations on the returned tensor. Default: ``False``. Examples:: >>> torchhd.FHRRTensor.identity(3, 6) FHRR([[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j]]) >>> torchhd.FHRRTensor.identity(3, 6, dtype=torch.complex128) FHRR([[1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j], [1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j]], dtype=torch.complex128) """ if dtype is None: dtype = torch.complex64 if dtype not in cls.supported_dtypes: name = cls.__name__ options = ", ".join([str(x) for x in cls.supported_dtypes]) raise ValueError(f"{name} vectors must be one of dtype {options}.") result = torch.ones( num_vectors, dimensions, dtype=dtype, device=device, requires_grad=requires_grad, ) return result.as_subclass(cls)
[docs] @classmethod def random( cls, num_vectors: int, dimensions: int, *, dtype=torch.complex64, device=None, requires_grad=False, generator=None, ) -> "FHRRTensor": """Creates a set of random independent hypervectors. The resulting hypervectors are sampled uniformly at random from the ``angle`` between -pi and +pi. Args: num_vectors (int): the number of hypervectors to generate. dimensions (int): the dimensionality of the hypervectors. generator (``torch.Generator``, optional): a pseudorandom number generator for sampling. dtype (``torch.dtype``, optional): the desired data type of returned tensor. Default: if ``None`` is torch.complex64. device (``torch.device``, optional): the desired device of returned tensor. Default: if ``None``, uses the current device for the default tensor type (see torch.set_default_tensor_type()). ``device`` will be the CPU for CPU tensor types and the current CUDA device for CUDA tensor types. requires_grad (bool, optional): If autograd should record operations on the returned tensor. Default: ``False``. Examples:: >>> torchhd.FHRRTensor.random(3, 6) FHRR([[ 0.2082-0.9781j, 0.7038-0.7104j, -0.6297-0.7768j, -0.9632-0.2689j, -0.4941+0.8694j, 0.9771-0.2128j], [ 0.9820-0.1887j, -0.0395+0.9992j, -0.5139+0.8579j, -0.8415-0.5402j, -0.6696-0.7427j, 0.2312+0.9729j], [-0.9786+0.2057j, 0.1714+0.9852j, -0.5925-0.8056j, -0.5698-0.8218j, -0.4632-0.8863j, 0.6996-0.7145j]]) >>> torchhd.FHRRTensor.random(3, 6, dtype=torch.long) FHRR([[-0.9996-0.0285j, -0.0688-0.9976j, 0.6900-0.7238j, 0.9519-0.3064j, 0.8131-0.5821j, 0.9942-0.1077j], [-0.9199-0.3922j, 0.8073-0.5902j, 0.8683+0.4960j, 0.1250+0.9922j, 0.6248+0.7808j, -0.2495+0.9684j], [ 0.0178+0.9998j, -0.3006-0.9538j, -0.9346+0.3557j, 0.9017-0.4324j, 0.4029-0.9153j, 0.4818-0.8763j]], dtype=torch.complex128) """ if dtype is None: dtype = torch.complex64 if dtype not in cls.supported_dtypes: name = cls.__name__ options = ", ".join([str(x) for x in cls.supported_dtypes]) raise ValueError(f"{name} vectors must be one of dtype {options}.") dtype = type_conversion[dtype] size = (num_vectors, dimensions) angle = torch.empty(size, dtype=dtype, device=device) angle.uniform_(-math.pi, math.pi, generator=generator) result = torch.complex(angle.cos(), angle.sin()) result.requires_grad = requires_grad return result.as_subclass(cls)
[docs] def bundle(self, other: "FHRRTensor") -> "FHRRTensor": r"""Bundle the hypervector with other using element-wise sum. This produces a hypervector maximally similar to both. The bundling operation is used to aggregate information into a single hypervector. Args: other (FHRR): other input hypervector Shapes: - Self: :math:`(*)` - Other: :math:`(*)` - Output: :math:`(*)` Examples:: >>> a, b = torchhd.FHRRTensor.random(2, 6) >>> a FHRR([ 0.9556-0.2948j, 0.1746+0.9846j, -0.6270-0.7790j, -0.2423-0.9702j, 0.6358+0.7719j, 0.9965-0.0834j]) >>> b FHRR([-0.9539-0.3000j, -0.1279+0.9918j, -0.4610+0.8874j, -0.3638-0.9315j, 0.9554+0.2952j, 0.8659+0.5003j]) >>> a.bundle(b) FHRR([-1.6885+0.4104j, -0.4094-1.4874j, 0.0090-0.0058j, 0.1039-0.9365j, 0.0413-1.8657j, 0.6276+1.8385j]) >>> a, b = torchhd.FHRRTensor.random(2, 10, dtype=torch.complex128) >>> a FHRR([ 0.4521-0.8920j, 0.7917-0.6109j, 0.5414-0.8408j, -0.9550-0.2967j, 0.9320+0.3626j, -0.8509-0.5253j], dtype=torch.complex128) >>> b FHRR([ 0.6954-0.7186j, -0.5621-0.8270j, 0.4685+0.8835j, -0.9319+0.3627j, -0.8310-0.5563j, 0.2545+0.9671j], dtype=torch.complex128) >>> a.bundle(b) FHRR([ 1.1475-1.6106j, 0.2296-1.4379j, 1.0099+0.0427j, -1.8869+0.0660j, 0.1010-0.1937j, -0.5964+0.4417j], dtype=torch.complex128) """ return torch.add(self, other)
[docs] def multibundle(self) -> "FHRRTensor": """Bundle multiple hypervectors""" return torch.sum(self, dim=-2, dtype=self.dtype)
[docs] def bind(self, other: "FHRRTensor") -> "FHRRTensor": r"""Bind the hypervector with other using element-wise multiplication. This produces a hypervector dissimilar to both. Binding is used to associate information, for instance, to assign values to variables. Args: other (FHRR): other input hypervector Shapes: - Self: :math:`(*)` - Other: :math:`(*)` - Output: :math:`(*)` Examples:: >>> a, b = torchhd.FHRRTensor.random(2, 6) >>> a FHRR([ 0.9317-0.3632j, 0.7320+0.6813j, -0.8588+0.5123j, -0.9723-0.2339j, -0.9631-0.2692j, -0.4093-0.9124j]) >>> b FHRR([ 0.9983+0.0578j, -0.5043-0.8635j, 0.5505-0.8349j, 0.9805+0.1966j, 0.9656+0.2599j, -0.5609-0.8279j]) >>> a.bind(b) FHRR([ 0.9511-0.3087j, 0.2191-0.9757j, -0.0450+0.9990j, -0.9073-0.4204j, -0.8600-0.5102j, -0.5257+0.8507j]) >>> a, b = torchhd.FHRRTensor.random(2, 6, dtype=torch.complex128) >>> a FHRR([ 0.7838-0.6210j, -0.0258+0.9997j, 0.0263+0.9997j, 0.9617+0.2742j, 0.1281-0.9918j, -0.4321+0.9018j], dtype=torch.complex128) >>> b FHRR([-0.9995+0.0308j, 0.4550-0.8905j, 0.2793+0.9602j, 0.0025-1.0000j, 0.4470+0.8946j, 0.8314-0.5557j], dtype=torch.complex128) >>> a.bind(b) FHRR([-0.7643+0.6449j, 0.8785+0.4778j, -0.9525+0.3045j, 0.2766-0.9610j, 0.9444-0.3287j, 0.1419+0.9899j], dtype=torch.complex128) """ return torch.mul(self, other)
[docs] def multibind(self) -> "FHRRTensor": """Bind multiple hypervectors""" return torch.prod(self, dim=-2, dtype=self.dtype)
[docs] def inverse(self) -> "FHRRTensor": r"""Invert the hypervector for binding. For FHRR the inverse of hypervector is its conjugate, this returns the conjugate of the hypervector. Shapes: - Self: :math:`(*)` - Output: :math:`(*)` Examples:: >>> a = torchhd.FHRRTensor.random(1, 6) >>> a FHRR([[ 0.9855+0.1698j, -0.0927-0.9957j, -0.8316-0.5554j, -0.1433-0.9897j, 0.8328+0.5536j, 0.2071+0.9783j]]) >>> a.inverse() FHRR([[ 0.9855-0.1698j, -0.0927+0.9957j, -0.8316+0.5554j, -0.1433+0.9897j, 0.8328-0.5536j, 0.2071-0.9783j]]) >>> a = torchhd.FHRRTensor.random(1, 6, dtype=torch.complex128) >>> a FHRR([[-0.9983-0.0574j, -0.4825+0.8759j, 0.9631-0.2692j, 0.9066-0.4219j, 0.7099-0.7044j, -0.1313-0.9913j]], dtype=torch.complex128) >>> a.inverse() >>> a.inverse() FHRR([[-0.9983+0.0574j, -0.4825-0.8759j, 0.9631+0.2692j, 0.9066+0.4219j, 0.7099+0.7044j, -0.1313+0.9913j]], dtype=torch.complex128) """ # Resolve conj to ensure the the returned tensor does not share the same memory return torch.conj(self).resolve_conj()
[docs] def negative(self) -> "FHRRTensor": r"""Negate the hypervector for the bundling inverse. Shapes: - Self: :math:`(*)` - Output: :math:`(*)` Examples:: >>> a = torchhd.FHRRTensor.random(1, 6) >>> a FHRR([[-0.0187-0.9998j, 0.1950-0.9808j, 0.5203+0.8540j, 0.8587+0.5124j, 0.9998+0.0203j, -0.6237-0.7816j]]) >>> a.negative() FHRR([[ 0.0187+0.9998j, -0.1950+0.9808j, -0.5203-0.8540j, -0.8587-0.5124j, -0.9998-0.0203j, 0.6237+0.7816j]]) >>> a = torchhd.FHRRTensor.random(1, 6, dtype=torch.complex128) >>> a FHRR([[ 0.8255+0.5644j, -0.8352-0.5500j, 0.9751-0.2218j, -0.9808-0.1950j, -0.3840-0.9233j, 0.4106-0.9118j]], dtype=torch.complex128) >>> a.negative() FHRR([[-0.8255-0.5644j, 0.8352+0.5500j, -0.9751+0.2218j, 0.9808+0.1950j, 0.3840+0.9233j, -0.4106+0.9118j]], dtype=torch.complex128) """ return torch.negative(self)
[docs] def permute(self, shifts: int = 1) -> "FHRRTensor": r"""Permute the hypervector. The permutation operator is used to assign an order to hypervectors. Args: shifts (int, optional): The number of places by which the elements of the tensor are shifted. Shapes: - Self: :math:`(*)` - Output: :math:`(*)` Examples:: >>> a = torchhd.FHRRTensor.random(1, 6) >>> a FHRR([[-0.3286-0.9445j, 0.2161-0.9764j, -0.6484+0.7613j, -0.4020+0.9156j, 0.8282-0.5605j, -0.9869+0.1613j]]) >>> a.permute() FHRR([[-0.9869+0.1613j, -0.3286-0.9445j, 0.2161-0.9764j, -0.6484+0.7613j, -0.4020+0.9156j, 0.8282-0.5605j]]) >>> a = torchhd.FHRRTensor.random(1, 6, dtype=torch.complex128) >>> a FHRR([[-0.9500-0.3123j, -0.0234+0.9997j, -0.1071-0.9943j, -0.8558-0.5174j, 0.9631-0.2690j, 0.5470-0.8371j]], dtype=torch.complex128) >>> a.permute() FHRR([[ 0.5470-0.8371j, -0.9500-0.3123j, -0.0234+0.9997j, -0.1071-0.9943j, -0.8558-0.5174j, 0.9631-0.2690j]], dtype=torch.complex128) """ return torch.roll(self, shifts=shifts, dims=-1)
[docs] def normalize(self) -> "FHRRTensor": r"""Normalize the hypervector. The normalization preserves the element phase but sets the magnitude to one. Shapes: - Self: :math:`(*)` - Output: :math:`(*)` Examples:: >>> x = torchhd.FHRRTensor.random(4, 6).multibundle() >>> x FHRRTensor([ 1.0878+0.9382j, 2.0057-1.5603j, -2.2828-1.4410j, 1.9643-1.8269j, -0.9710-0.0120j, -0.7432+0.6956j]) >>> x.normalize() FHRRTensor([ 0.7572+0.6531j, 0.7893-0.6140j, -0.8456-0.5338j, 0.7322-0.6810j, -0.9999-0.0124j, -0.7301+0.6833j]) """ angle = self.angle() return torch.complex(angle.cos(), angle.sin())
[docs] def dot_similarity(self, others: "FHRRTensor") -> Tensor: """Inner product with other hypervectors""" if others.dim() >= 2: others = others.transpose(-2, -1) return torch.real(torch.matmul(self, torch.conj(others)))
[docs] def cosine_similarity(self, others: "FHRRTensor", *, eps=1e-08) -> Tensor: """Cosine similarity with other hypervectors""" self_dot = torch.sum(torch.real(self * torch.conj(self)), dim=-1) self_mag = torch.sqrt(self_dot) others_dot = torch.sum(torch.real(others * torch.conj(others)), dim=-1) others_mag = torch.sqrt(others_dot) if self.dim() >= 2: magnitude = self_mag.unsqueeze(-1) * others_mag.unsqueeze(-2) else: magnitude = self_mag * others_mag if torch.isclose(magnitude, torch.zeros_like(magnitude), equal_nan=True).any(): import warnings warnings.warn( "The norm of a vector is nearly zero, this could indicate a bug." ) magnitude = torch.clamp(magnitude, min=eps) return self.dot_similarity(others) / magnitude