Spaces:
Running on Zero
Running on Zero
File size: 6,699 Bytes
d58cfe6 | 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 | """Decoding a direction back to words, against a synthetic vocabulary."""
import numpy as np
import pytest
import ulens
from ulens import decode
from ulens._math import unit
HIDDEN = 24
WORDS = ["clear", "cloudy", "maybe", "certain", "doubtful", "obvious", "ambiguous",
"plain", "murky", "sure", "unsure", "vague", "a", "xy", "123", "!!", "Clear"]
@pytest.fixture
def voc(monkeypatch, tmp_path):
"""A vocabulary of len(WORDS) tokens whose transported rows we control."""
rng = np.random.default_rng(0)
n = len(WORDS)
W = rng.normal(size=(n, HIDDEN)).astype(np.float32)
gamma = np.ones(HIDDEN, np.float32)
J = np.eye(HIDDEN, dtype=np.float32)
from ulens import lensfile
monkeypatch.setattr(lensfile, "matrix", lambda lens, block: J)
spec = ulens.ModelSpec(key="fakevoc", hf="fake/voc", revision="0" * 40, n_blocks=6,
hidden_size=HIDDEN, readout_block=3,
lens=ulens.models.LensFile(path=str(tmp_path / "n.npy")),
think_end="</think>")
ulens.MODELS["fakevoc"] = spec
v = decode.Vocabulary(spec, 3, cache_dir=tmp_path)
monkeypatch.setattr(v, "_open", lambda: (None, "w", gamma))
monkeypatch.setattr(v, "_rows", lambda chunk: iter([(0, W * gamma[None, :])]))
monkeypatch.setattr(type(v), "n_tokens", property(lambda self: n))
monkeypatch.setattr(v, "word", lambda t: WORDS[int(t)])
v.W = W
yield v
del ulens.MODELS["fakevoc"]
ulens.models._ALIASES.pop("fakevoc", None)
def test_norms_match_the_transported_row_norms(voc):
np.testing.assert_allclose(voc.norms, np.linalg.norm(voc.W @ voc.J, axis=1), rtol=1e-5)
def test_norms_are_cached_to_disk(voc):
p = voc.cache_path
assert not p.exists()
first = voc.norms
assert p.exists()
voc._norms = None
np.testing.assert_array_equal(voc.norms, first)
def test_scores_are_the_cosine_with_every_transported_row(voc):
u = unit(np.random.default_rng(1).normal(size=HIDDEN).astype(np.float32))
D = unit(voc.W @ voc.J)
np.testing.assert_allclose(voc.scores(u), D @ u, rtol=1e-4, atol=1e-5)
def test_scores_without_cosine_are_the_raw_dot_product(voc):
u = np.random.default_rng(2).normal(size=HIDDEN).astype(np.float32)
np.testing.assert_allclose(voc.scores(u, cosine=False), (voc.W @ voc.J) @ u, rtol=1e-4)
def test_a_direction_decodes_to_its_own_word_first(voc):
"""The direction of one token's transported row must rank that token top."""
for t in (0, 3, 6):
d = unit(voc.W[t] @ voc.J)
assert voc.top(d, k=1, min_len=1, alpha_only=False)[0].word == WORDS[t]
def test_the_negative_end_is_the_opposite_ranking(voc):
d = unit(voc.W[0] @ voc.J)
pos = voc.top(d, k=3, min_len=1, alpha_only=False)
neg = voc.top(d, k=3, sign="-", min_len=1, alpha_only=False)
assert pos[0].score > 0 > neg[0].score
assert pos[0].word != neg[0].word
def test_filters_drop_fragments_and_dedupe_case(voc):
d = unit(np.ones(HIDDEN, np.float32))
hits = voc.top(d, k=20, scan=100)
got = [h.word for h in hits]
assert "a" not in got and "xy" not in got and "123" not in got and "!!" not in got
assert len({w.lower() for w in got}) == len(got)
assert all(len(w) >= decode.MIN_LEN for w in got)
def test_rejects_a_direction_of_the_wrong_width(voc):
with pytest.raises(ValueError, match="wide"):
voc.scores(np.ones(HIDDEN + 1, np.float32))
def test_decode_handles_an_axis_a_cone_and_a_bare_direction(voc):
ax = ulens.Axis("one", unit(voc.W[0] @ voc.J))
cone = ulens.axes.from_axes([ax, ulens.Axis("two", unit(voc.W[3] @ voc.J))])
assert [d.name for d in voc.decode(ax, k=2, min_len=1)] == ["one"]
assert [d.name for d in voc.decode(cone, k=2, min_len=1)] == ["one", "two"]
assert [d.name for d in voc.decode(np.ones(HIDDEN), k=2, min_len=1)] == ["direction"]
def test_report_shows_both_ends_of_every_axis(voc):
cone = ulens.axes.from_axes([ulens.Axis(n, unit(voc.W[i] @ voc.J))
for i, n in ((0, "alpha"), (3, "beta"))])
txt = voc.report(cone, k=2, min_len=1)
assert "alpha" in txt and "beta" in txt
assert txt.count("+") >= 2 and txt.count("-") >= 2
def test_purify_removes_the_shared_direction():
"""The two removals are sequential, not jointly orthogonalised — the same order the
reference implementation uses — so a trace of `mu` survives removing the components
after it. The principal components themselves come out exactly."""
rng = np.random.default_rng(3)
shared = unit(rng.normal(size=HIDDEN).astype(np.float32))
D = unit(shared[None, :] * 3.0 + rng.normal(size=(500, HIDDEN)).astype(np.float32) * 0.2)
mu, V = decode.common_component(D, k=2)
assert abs(float(unit(mu) @ shared)) > 0.9
v = unit(rng.normal(size=HIDDEN).astype(np.float32))
p = decode.purify(v, mu, V)
assert abs(float(unit(p) @ mu)) < 1e-3
for row in V:
assert abs(float(p @ row)) < 1e-5
def test_purify_with_no_components_removes_the_mean_exactly():
rng = np.random.default_rng(31)
D = unit(rng.normal(size=(200, HIDDEN)).astype(np.float32))
mu, V = decode.common_component(D, k=0)
v = unit(rng.normal(size=HIDDEN).astype(np.float32))
assert abs(float(decode.purify(v, mu, V) @ mu)) < 1e-6
def test_purify_with_k_zero_only_removes_the_mean():
rng = np.random.default_rng(4)
D = unit(rng.normal(size=(100, HIDDEN)).astype(np.float32))
mu, V = decode.common_component(D, k=0)
assert V.shape == (0, HIDDEN)
v = rng.normal(size=HIDDEN).astype(np.float32)
assert abs(float(unit(decode.purify(v, mu, V)) @ mu)) < 1e-4
def test_purifying_a_direction_changes_what_it_decodes_to(voc):
d = unit(np.ones(HIDDEN, np.float32))
pur = decode.common_component(unit(voc.W @ voc.J), k=1)
plain = [h.word for h in voc.top(d, k=4, min_len=1, alpha_only=False)]
cleaned = [h.word for h in voc.top(d, k=4, min_len=1, alpha_only=False,
purify_with=pur)]
assert plain != cleaned
def test_sample_directions_are_unit_and_the_right_shape(voc):
S = voc.sample_directions(n=8, seed=0)
assert S.shape == (8, HIDDEN)
np.testing.assert_allclose(np.linalg.norm(S, axis=1), 1.0, atol=1e-5)
def test_save_decode_writes_both_ends(voc, tmp_path):
ds = voc.decode(unit(voc.W[0] @ voc.J), k=2, min_len=1)
import json
blob = json.loads(decode.save_decode(ds, tmp_path / "d.json").read_text())
assert blob[0]["name"] == "direction"
assert len(blob[0]["positive"]) == 2 and len(blob[0]["negative"]) == 2
assert {"word", "score", "id"} == set(blob[0]["positive"][0])
|