File size: 6,548 Bytes
d9c1900 | 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 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | """Tests for the Learner + Encoder (``palimseste.learner``).
Verifies:
- learn() is O(1): the millionth example costs the same as the first
(we measure write cost stays flat across |M| scales)
- predict() recovers learned mappings
- context binding: same x in different ctx yields different addresses
- Encoder: ints/strs/floats/sequences/sets encode deterministically &
with the right similarity structure (floats level-coded, sequences
order-sensitive, sets order-insensitive)
"""
from __future__ import annotations
import time
import numpy as np
import pytest
from palimseste import hv
from palimseste.memory import Memory
from palimseste.phi import Phi, KernelConfig
from palimseste.learner import Learner, Encoder, Prediction
def _learner(D=2000, seed=0, radius=0) -> tuple[Learner, np.random.Generator]:
rng = np.random.default_rng(seed)
mem = Memory(D=D, rng=np.random.default_rng(seed))
phi = Phi(config=KernelConfig(radius=radius, min_weight=1e-6))
return Learner(mem=mem, phi=phi, rng=rng), rng
# ----------------------------------------------------------------- learner
def test_learn_then_predict_exact():
lr, rng = _learner(D=2000, radius=0)
x = hv.random_hv(D=2000, rng=rng)
y = hv.random_hv(D=2000, rng=rng)
lr.learn(x, y)
p = lr.predict(x)
assert p.value == y
assert p.n_matches == 1
assert p.confidence > 0.99
def test_learn_context_binding():
# same x, different ctx -> different addresses, different predictions
lr, rng = _learner(D=2000, radius=0)
x = hv.random_hv(D=2000, rng=rng)
ctx_a = hv.random_hv(D=2000, rng=rng)
ctx_b = hv.random_hv(D=2000, rng=rng)
y_a = hv.random_hv(D=2000, rng=rng)
y_b = hv.random_hv(D=2000, rng=rng)
lr.learn(x, y_a, ctx=ctx_a)
lr.learn(x, y_b, ctx=ctx_b)
pa = lr.predict(x, ctx=ctx_a)
pb = lr.predict(x, ctx=ctx_b)
assert pa.value == y_a
assert pb.value == y_b
def test_learn_is_o1_flat_cost():
# The write cost must not grow with |M|. We compare per-write time at
# small |M| vs large |M| and require they are within a generous factor.
D = 4000
rng = np.random.default_rng(7)
mem = Memory(D=D, rng=np.random.default_rng(7))
phi = Phi(config=KernelConfig(radius=10))
lr = Learner(mem=mem, phi=phi, rng=rng)
xs = [hv.random_hv(D=D, rng=rng) for _ in range(2000)]
ys = [hv.random_hv(D=D, rng=rng) for _ in range(2000)]
t0 = time.perf_counter()
for i in range(50):
lr.learn(xs[i], ys[i])
small = (time.perf_counter() - t0) / 50
for i in range(50, 2000):
lr.learn(xs[i], ys[i])
t1 = time.perf_counter()
for i in range(2000, 2050):
lr.learn(xs[i % len(xs)], ys[i % len(ys)])
large = (time.perf_counter() - t1) / 50
# O(1) amortized: large-|M| writes should be within 5x of small-|M| writes
# (generous to absorb LSH dict growth / GC noise).
ratio = large / max(small, 1e-9)
assert ratio < 5.0, f"write cost grew: small={small:.2e} large={large:.2e} ratio={ratio:.2f}"
def test_learn_many_appends_all():
lr, rng = _learner(D=1000, radius=5)
pairs = [(hv.random_hv(D=1000, rng=rng), hv.random_hv(D=1000, rng=rng)) for _ in range(30)]
traces = lr.learn_many(pairs)
assert len(traces) == 30
assert len(lr.mem) == 30
def test_predict_empty_returns_none():
lr, _ = _learner(D=500)
p = lr.predict(hv.random_hv(D=500))
assert p.value is None
assert p.confidence == 0.0
assert p.n_matches == 0
# ----------------------------------------------------------------- encoder
def _enc(D=2000, seed=3) -> Encoder:
return Encoder(D=D, rng=np.random.default_rng(seed))
def test_encode_int_deterministic():
e = _enc()
assert e.encode_int(5) == e.encode_int(5)
assert e.encode_int(5) != e.encode_int(6)
def test_encode_str_deterministic():
e = _enc()
assert e.encode_str("cat") == e.encode_str("cat")
assert e.encode_str("cat") != e.encode_str("dog")
def test_encode_float_level_similarity():
# adjacent floats are similar, distant are not.
# With 256 levels and ~0.025*D bits/step, the random-walk level code
# saturates to quasi-orthogonal within ~30 levels, so values far apart
# in [0,1] decorrelate as VSA scalar coding requires.
e = _enc(D=5000, seed=1)
a = e.encode_float(0.41)
b = e.encode_float(0.42)
c = e.encode_float(0.90)
assert hv.similarity(a, b) > 0.5
# 0.41 vs 0.90 are ~126 levels apart -> well past saturation
assert hv.similarity(a, c) < 0.3
def test_encode_float_clamps():
e = _enc()
# out-of-range clamps, no error
assert isinstance(e.encode_float(-5.0), hv.HV)
assert isinstance(e.encode_float(99.0), hv.HV)
def test_encode_float_bad_range():
e = _enc()
with pytest.raises(ValueError):
e.encode_float(0.5, lo=1.0, hi=0.0)
def test_encode_sequence_order_sensitive():
e = _enc(D=5000, seed=2)
a = e.encode_int(1)
b = e.encode_int(2)
c = e.encode_int(3)
seq_abc = e.encode_sequence([a, b, c])
seq_cba = e.encode_sequence([c, b, a])
# order matters: different permutations are dissimilar
assert hv.similarity(seq_abc, seq_cba) < 0.6
def test_encode_set_order_insensitive():
e = _enc(D=5000, seed=3)
a = e.encode_int(1)
b = e.encode_int(2)
c = e.encode_int(3)
s1 = e.encode_set([a, b, c])
s2 = e.encode_set([c, b, a])
# same set, different order -> similar (not necessarily equal due to tie-breaks,
# but strongly correlated)
assert hv.similarity(s1, s2) > 0.7
def test_encode_kv():
# Bundling is commutative ONLY when there are no ties. With an even
# number of independent bound-pairs every coordinate is an exact tie and
# the result degenerates to random tie-breaking (sim ~ 0 between orderings).
# With an odd count (>=3) ties are broken by genuine majority and the
# bundle is order-insensitive.
e = _enc(D=5000, seed=4)
pairs = [
(e.encode_str("color"), e.encode_str("red")),
(e.encode_str("size"), e.encode_int(42)),
(e.encode_str("weight"), e.encode_int(7)),
(e.encode_str("shape"), e.encode_str("cube")),
(e.encode_str("owner"), e.encode_str("ada")),
]
rec = e.encode_kv(pairs)
rec2 = e.encode_kv(list(reversed(pairs)))
assert hv.similarity(rec, rec2) > 0.7
def test_prediction_dataclass():
p = Prediction(query=hv.random_hv(10), value=None, confidence=0.0, n_matches=0)
assert p.value is None
assert p.confidence == 0.0
|