File size: 6,546 Bytes
1f71c7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | """PALIMPSESTE β Soft Phi: continuous retrieval with exponential weighting.
The standard Phi uses a hard Hamming cutoff: if a candidate's distance
exceeds the radius, it's ignored entirely. This is binary β one bit of
difference can change the result completely.
Soft Phi replaces this with a continuous weighting: ALL candidates
contribute, weighted by exp(similarity * temperature). Close candidates
dominate, distant ones contribute negligibly. The result is a *blend*
of all stored values, not just the nearest one.
This is the hypervectorial equivalent of a softmax over the memory.
It enables **interpolation**: if the query is between two stored facts,
the result blends both rather than picking one arbitrarily.
Example
-------
If M contains:
("capital of france", "paris") β similarity 0.8 to query
("capital of italy", "rome") β similarity 0.6 to query
("capital of germany", "berlin") β similarity 0.5 to query
And the query is "capital of spain" (not stored):
- Hard Phi: no match (all similarities < threshold) β fallback
- Soft Phi: blend of paris (0.8 weight), rome (0.6), berlin (0.5)
β produces an HV in the "capital city" region of hyperspace
The blended result won't be exactly "madrid" (that would require storing
it), but it will be in the right *semantic neighborhood* β which, combined
with HV-Word2Vec, means the model can produce a reasonable guess rather
than a blank stare.
"""
from __future__ import annotations
from dataclasses import dataclass, field
import numpy as np
from .hv import HV, bind, bundle, similarity, hamming, bits_to_signs, signs_to_bits
from .memory import Memory
from .phi import Phi, KernelConfig, Retrieval
__all__ = ["SoftPhi", "SoftPhiConfig"]
@dataclass
class SoftPhiConfig:
"""Configuration for Soft Phi.
Parameters
----------
temperature : float
Sharpness of the weighting. Higher = more peaked (only the closest
candidate matters). Lower = more spread (all candidates blend).
Typical: 5.0-20.0. At 0.0, all candidates contribute equally.
max_candidates : int
Maximum number of candidates to consider (for speed). The LSH
index returns candidates; we keep only the top-k by similarity.
min_weight : float
Candidates with decayed memory weight below this are ignored.
"""
temperature: float = 10.0
max_candidates: int = 100
min_weight: float = 1e-6
@dataclass
class SoftPhi:
"""Soft (continuous) read-back kernel.
Unlike :class:`Phi` which uses a hard Hamming cutoff, SoftPhi weights
all candidates by exp(similarity * temperature) and produces a blended
result. This enables interpolation between stored facts.
Parameters
----------
config : SoftPhiConfig
Configuration.
"""
config: SoftPhiConfig = field(default_factory=SoftPhiConfig)
def __call__(self, mem: Memory, query: HV) -> HV | None:
"""Compute Soft Phi(query) β the blended reconstructed value."""
cand_ids = mem.candidates(query)
if not cand_ids:
return None
# filter valid ids and limit to max_candidates
valid_ids = [cid for cid in cand_ids if 0 <= cid < len(mem.traces)]
if not valid_ids:
return None
# compute similarities and weights for all candidates
D = query.D
packed_len = len(query.bits)
# vectorized: stack candidate address bits
n = min(len(valid_ids), self.config.max_candidates)
valid_ids = valid_ids[:n]
# compute Hamming distances (vectorized with popcount table)
cand_bits = np.empty((n, packed_len), dtype=np.uint8)
for i, cid in enumerate(valid_ids):
cand_bits[i] = mem.traces[cid].address.bits
xored = np.bitwise_xor(cand_bits, query.bits[np.newaxis, :])
_POPCOUNT = np.array([bin(i).count('1') for i in range(256)], dtype=np.uint16)
hamming_dists = _POPCOUNT[xored].sum(axis=1)
sims = 1.0 - 2.0 * hamming_dists / D # (n,) in [-1, 1]
# compute exponential weights
T = self.config.temperature
# shift for numerical stability
sims_shifted = sims * T - (sims.max() * T)
exp_weights = np.exp(sims_shifted)
exp_weights = exp_weights / exp_weights.sum()
# get value HVs and memory weights
value_signs = np.zeros((n, D), dtype=np.float64)
mem_weights = np.ones(n, dtype=np.float64)
for i, cid in enumerate(valid_ids):
tr = mem.traces[cid]
w = mem.current_weight(tr)
if w < self.config.min_weight:
exp_weights[i] = 0.0
else:
mem_weights[i] = w
value_signs[i] = bits_to_signs(tr.value).astype(np.float64)
# combined weight = exp_weight * mem_weight
combined = exp_weights * mem_weights
total = combined.sum()
if total <= 0:
return None
combined = combined / total
# weighted sum of value signs β sign β HV
weighted_sum = (combined[:, np.newaxis] * value_signs).sum(axis=0)
out_signs = np.where(weighted_sum >= 0, np.int8(1), np.int8(-1))
return signs_to_bits(out_signs)
def retrieve(self, mem: Memory, query: HV) -> Retrieval:
"""Return a Retrieval object (for compatibility with Phi)."""
cand_ids = mem.candidates(query)
valid_ids = [cid for cid in cand_ids if 0 <= cid < len(mem.traces)]
if not valid_ids:
return Retrieval(query=query, config=KernelConfig(),
matches=[], sims=[], weights=[], result=None)
D = query.D
packed_len = len(query.bits)
n = min(len(valid_ids), self.config.max_candidates)
valid_ids = valid_ids[:n]
cand_bits = np.empty((n, packed_len), dtype=np.uint8)
for i, cid in enumerate(valid_ids):
cand_bits[i] = mem.traces[cid].address.bits
xored = np.bitwise_xor(cand_bits, query.bits[np.newaxis, :])
_POPCOUNT = np.array([bin(i).count('1') for i in range(256)], dtype=np.uint16)
hamming_dists = _POPCOUNT[xored].sum(axis=1)
sims = (1.0 - 2.0 * hamming_dists / D).tolist()
from .memory import Trace
matches = [mem.traces[cid] for cid in valid_ids]
weights = [mem.current_weight(tr) for tr in matches]
return Retrieval(query=query, config=KernelConfig(),
matches=matches, sims=sims, weights=weights, result=None)
|