Source code for nengo_spa.algebras.vtb_algebra

import warnings

import nengo
import numpy as np

from nengo_spa.algebras.base import (
    AbstractAlgebra,
    CommonProperties,
    ElementSidedness,
    GenericSign,
)
from nengo_spa.networks.vtb import VTB


[docs]class VtbAlgebra(AbstractAlgebra): r"""Vector-derived Transformation Binding (VTB) algebra. VTB uses elementwise addition for superposition. The binding operation :math:`\mathcal{B}(x, y)` is defined as .. math:: \mathcal{B}(x, y) := V_y x = \left[\begin{array}{ccc} V_y' & 0 & 0 \\ 0 & V_y' & 0 \\ 0 & 0 & \ddots \end{array}\right] x with :math:`d'` blocks where .. math:: V_y' = d^{\frac{1}{4}} \left[\begin{array}{cccc} y_1 & y_2 & \dots & y_{d'} \\ y_{d' + 1} & y_{d' + 2} & \dots & y_{2d'} \\ \vdots & \vdots & \ddots & \vdots \\ y_{d - d' + 1} & y_{d - d' + 2} & \dots & y_d \end{array}\right] and .. math:: d'^2 = d. The approximate inverse :math:`y^+` for :math:`y` is permuting the elements such that :math:`V_{y^+} = V_y^T`. Note that VTB requires the vector dimensionality to be square. The VTB binding operation is neither associative nor commutative. Furthermore, there are right inverses and identities only. By transposing the :math:`V_y` matrix, the closely related `.TvtbAlgebra` (Transposed VTB) algebra is obtained which does have two-sided identities and inverses. Additional information about VTB can be found in * `Gosmann, Jan, and Chris Eliasmith (2019). Vector-derived transformation binding: an improved binding operation for deep symbol-like processing in neural networks. Neural computation 31.5, 849-869. <https://direct.mit.edu/neco/article/31/5/849/8469/Vector-Derived-Transformation-Binding-An-Improved>`_ * `Jan Gosmann (2018). An Integrated Model of Context, Short-Term, and Long-Term Memory. UWSpace. <https://uwspace.uwaterloo.ca/handle/10012/13498>`_ .. seealso:: `.TvtbAlgebra` """ _instance = None def __new__(cls): if type(cls._instance) is not cls: cls._instance = super(VtbAlgebra, cls).__new__(cls) return cls._instance
[docs] def is_valid_dimensionality(self, d): """Checks whether *d* is a valid vector dimensionality. For VTB all square numbers are valid dimensionalities. Parameters ---------- d : int Dimensionality Returns ------- bool *True*, if *d* is a valid vector dimensionality for the use with the algebra. """ if d < 1: return False sub_d = np.sqrt(d) return sub_d * sub_d == d
def _get_sub_d(self, d): sub_d = int(np.sqrt(d)) if sub_d * sub_d != d: raise ValueError("Vector dimensionality must be a square number.") return sub_d
[docs] def create_vector(self, d, properties, *, rng=None): """Create a vector fulfilling given properties in the algebra. Creating positive vectors requires SciPy. Parameters ---------- d : int Vector dimensionality properties : set of str Definition of properties for the vector to fulfill. Valid set elements are constants defined in `.VtbProperties`. rng : numpy.random.RandomState, optional The random number generator to use to create the vector. Returns ------- ndarray Random vector with desired properties. """ properties = set(properties) if rng is None: rng = np.random.RandomState() if {VtbProperties.UNITARY, VtbProperties.POSITIVE} <= properties: properties -= {VtbProperties.UNITARY, VtbProperties.POSITIVE} v = self.identity_element(d, sidedness=ElementSidedness.RIGHT) warnings.warn( "The only positive unitary vector in VTB is the identity. " "Use the identity directly to avoid this warning." ) elif VtbProperties.UNITARY in properties: properties.remove(VtbProperties.UNITARY) v = self.make_unitary(rng.randn(d)) elif VtbProperties.POSITIVE in properties: try: from scipy.linalg import sqrtm except ImportError as err: # From Python 3.6 onward we get a ModuleNotFoundError. We want # to re-raise the same type to be as specific as possible. raise type(err)( "Creating positive VTB vectors requires SciPy to be available.", name=err.name, path=err.path, ) properties.remove(VtbProperties.POSITIVE) sub_d = self._get_sub_d(d) v = rng.randn(d) v /= np.linalg.norm(v) mat = v.reshape((sub_d, sub_d)) v = sqrtm(np.dot(mat, mat.T)).flatten() else: v = rng.randn(d) v /= np.linalg.norm(v) if len(properties) > 0: raise ValueError("Invalid properties: " + ", ".join(properties)) return v
[docs] def make_unitary(self, v): sub_d = self._get_sub_d(len(v)) m = np.array(v.reshape((sub_d, sub_d))) for i in range(1, sub_d): y = -np.dot(m[:i, i:], m[i, i:]) A = m[:i, :i] m[i, :i] = np.linalg.solve(A, y) m /= np.linalg.norm(m, axis=1)[:, None] m /= np.sqrt(sub_d) return m.flatten()
[docs] def superpose(self, a, b): return a + b
[docs] def bind(self, a, b): d = len(a) if len(b) != d: raise ValueError("Inputs must have same length.") m = self.get_binding_matrix(b) return np.dot(m, a)
[docs] def invert(self, v, sidedness=ElementSidedness.TWO_SIDED): """Invert vector *v*. A vector bound to its inverse will result in the identity vector. VTB has a right inverse only. .. deprecated:: 1.2.0 Calling this method with the default ``sidedness=ElementSidedness.TWO_SIDED`` returns the right inverse for backwards compatibility, but has been deprecated and will be removed in the next major release. Parameters ---------- v : (d,) ndarray Vector to invert. sidedness : ElementSidedness Must be set to `ElementSidedness.RIGHT`. Returns ------- (d,) ndarray Right inverse of vector. """ if sidedness is ElementSidedness.LEFT: raise NotImplementedError("VtbAlgebra does not have a left inverse.") if sidedness is ElementSidedness.TWO_SIDED: warnings.warn( DeprecationWarning( "VtbAlgebra does not have a two-sided inverse, returning " "the right inverse instead. Please change your code to " "request the right inverse explicitly with " "`sidedness=ElementSidedness.RIGHT`." ) ) sub_d = self._get_sub_d(len(v)) return v.reshape((sub_d, sub_d)).T.flatten()
[docs] def binding_power(self, v, exponent): r"""Returns the binding power of *v* using the *exponent*. The binding power is defined as binding (*exponent*-1) times bindings of *v* to itself. Fractional binding powers are supported for "positive" vectors if SciPy is available. Note the following special exponents: * an exponent of -1 will return the inverse, * an exponent of 0 will return the identity vector, * and an *exponent* of 1 will return *v* itself. Be aware that the binding power for the VTB algebra does *not* satisfy the usual properties of exponentiation: * :math:`\mathcal{B}(v^a, v^b) = v^{a+b}` does *not* hold, * :math:`(v^a)^b = v^{ab}` does *not* hold. Parameters ---------- v : (d,) ndarray Vector to bind repeatedly to itself. exponent : int or float Exponent of the binding power. Returns ------- (d,) ndarray Binding power of *v*. See also -------- .sign """ try: from scipy.linalg import fractional_matrix_power except ImportError as err: if int(exponent) == exponent: exponent = int(exponent) # Provide fallback for integer-only powers def fractional_matrix_power(m, exp): power = np.eye(len(m)) for _ in range(exp): power = np.dot(power, m) return power else: # From Python 3.6 onward we get a ModuleNotFoundError. We want # to re-raise the same type to be as specific as possible. raise type(err)( "Fractional VTB binding powers require SciPy to be available.", name=err.name, path=err.path, ) if exponent == 0: return self.identity_element(len(v), sidedness=ElementSidedness.RIGHT) if int(exponent) != exponent and not self.sign(v).is_positive(): raise ValueError( "Fractional binding powers are only supported for 'positive' vectors." ) if exponent < 0: v = self.invert(v, sidedness=ElementSidedness.RIGHT) sub_d = self._get_sub_d(len(v)) power = fractional_matrix_power( v.reshape((sub_d, sub_d)) * np.sqrt(sub_d), abs(exponent) - 1 ).flatten() / np.sqrt(sub_d) assert np.allclose(power.imag, 0) return self.bind(v, power.real)
[docs] def get_binding_matrix(self, v, swap_inputs=False): sub_d = self._get_sub_d(len(v)) m = np.sqrt(sub_d) * np.kron(np.eye(sub_d), v.reshape((sub_d, sub_d))) if swap_inputs: m = np.dot(self.get_swapping_matrix(len(v)), m) return m
[docs] def get_swapping_matrix(self, d): """Get matrix to swap operands in bound state. Parameters ---------- d : int Dimensionality of vector. Returns ------- (d, d) ndarry Matrix to multiply with a vector to switch left and right operand in bound state. """ return self.get_inversion_matrix(d, sidedness=ElementSidedness.RIGHT)
[docs] def get_inversion_matrix(self, d, sidedness=ElementSidedness.TWO_SIDED): """Returns the transformation matrix for inverting a vector. VTB has a right inverse only. .. deprecated:: 1.2.0 Calling this method with the default ``sidedness=ElementSidedness.TWO_SIDED`` returns the right transformation matrix for the right inverse for backwards compatibility, but has been deprecated and will be removed in the next major release. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness Must be set to `ElementSidedness.RIGHT`. Returns ------- (d, d) ndarray Transformation matrix to invert a vector. """ if sidedness is ElementSidedness.LEFT: raise NotImplementedError("VtbAlgebra does not have a left inverse.") if sidedness is ElementSidedness.TWO_SIDED: warnings.warn( DeprecationWarning( "VtbAlgebra does not have a two-sided inverse, returning " "the right inverse instead. Please change your code to " "request the right inverse explicitly with " "`sidedness=ElementSidedness.RIGHT`." ) ) sub_d = self._get_sub_d(d) return np.eye(d).reshape(d, sub_d, sub_d).T.reshape(d, d)
[docs] def implement_superposition(self, n_neurons_per_d, d, n): node = nengo.Node(size_in=d) return node, n * (node,), node
[docs] def implement_binding(self, n_neurons_per_d, d, unbind_left, unbind_right): net = VTB(n_neurons_per_d, d, unbind_left, unbind_right) return net, (net.input_left, net.input_right), net.output
[docs] def sign(self, v): m = self.get_binding_matrix(v) if not np.allclose(m, m.T): return VtbSign(None) eigenvalues = np.linalg.eigvalsh(m) if np.all(eigenvalues > 0): return VtbSign(1) elif np.all(eigenvalues < 0): return VtbSign(-1) elif np.allclose(eigenvalues, 0): return VtbSign(0) else: return VtbSign(None)
[docs] def abs(self, v): # No inverse of sign required because in VTB sign vectors are their # own inverse. return self.bind(v, self.sign(v).to_vector(len(v)))
[docs] def absorbing_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """VTB has no absorbing element except the zero vector. Always raises a `NotImplementedError`. """ raise NotImplementedError("VtbAlgebra does not have any absorbing elements.")
[docs] def identity_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Return the identity element of dimensionality *d*. VTB has a right identity only. .. deprecated:: 1.2.0 Calling this method with the default ``sidedness=ElementSidedness.TWO_SIDED`` returns the right identity for backwards compatibility, but has been deprecated and will be removed in the next major release. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness Must be set to `ElementSidedness.RIGHT`. Returns ------- (d,) ndarray Right identity element. """ if sidedness is ElementSidedness.LEFT: raise NotImplementedError("VtbAlgebra does not have a left identity.") if sidedness is ElementSidedness.TWO_SIDED: warnings.warn( DeprecationWarning( "VtbAlgebra does not have a two-sided identity, returning " "the right identity instead. Please change your code to " "request the right identity explicitly with " "`sidedness=ElementSidedness.RIGHT`." ) ) sub_d = self._get_sub_d(d) return (np.eye(sub_d) / d ** 0.25).flatten()
[docs] def negative_identity_element(self, d, sidedness=ElementSidedness.TWO_SIDED): r"""Return the negative identity element of dimensionality *d*. VTB has a right negative identity only. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional Must be set to `ElementSidedness.RIGHT`. Returns ------- (d,) ndarray Negative identity element. """ if sidedness is not ElementSidedness.RIGHT: raise NotImplementedError("VTB only has a right negative identity.") return -self.identity_element(d, sidedness)
[docs] def zero_element(self, d, sidedness=ElementSidedness.TWO_SIDED): """Return the zero element of dimensionality *d*. The zero element produces itself when bound to a different vector. For VTB this is the zero vector. Parameters ---------- d : int Vector dimensionality. sidedness : ElementSidedness, optional This argument has no effect because the zero element of the VTB algebra is two-sided. Returns ------- (d,) ndarray Zero element. """ return np.zeros(d)
[docs]class VtbSign(GenericSign): """Represents a sign in the `.VtbAlgebra`. The sign depends on the symmetry and positive/negative definiteness of the binding matrix derived from the vector. For all non-symmetric matrices, the sign is indefinite. It is also indefinite, if the matrices' eigenvalues have different signs. A symmetric, positive (negative) definite binding matrix corresponds to a positive (negative) sign (equivalent to all eigenvalues being greater than 0, respectively lower than 0). If all eigenvalues are equal to 0, the sign is also 0. """
[docs] def to_vector(self, d): if self.sign is None: raise NotImplementedError( "There is no vector corresponding to an indefinite sign." ) elif self.sign > 0: return VtbAlgebra().identity_element(d, sidedness=ElementSidedness.RIGHT) elif self.sign < 0: return VtbAlgebra().negative_identity_element( d, sidedness=ElementSidedness.RIGHT ) elif self.sign == 0: return VtbAlgebra().zero_element(d) else: raise AssertionError( "Invalid value for sign, this code should be unreachable." )
[docs]class VtbProperties: """Vector properties supported by the `.VtbAlgebra`.""" UNITARY = CommonProperties.UNITARY """A unitary vector does not change the length of a vector it is bound to.""" POSITIVE = CommonProperties.POSITIVE """A positive vector does not change the sign of a vector it is bound to. A positive vector allows for fractional binding powers. """