v0.5: factual generalization via dense analogy. learn_relation + solve_analogy implement Mikolov paris-france+spain=madrid in normalized dense space, averaged over known examples, excluding the query slot. Wired into structural answer() so unseen slots get plain-text answers. Measured end-to-end via the Brain with fastText: capitals holdout goes 0% (char) / 0% (structural) -> ~50-67% factual-correct (spain->madrid, portugal->lisbon, greece->athens, norway->oslo recovered correctly). from_fasttext uses batch projection (fast load). The semantic wall is broken. 109 tests.
595d852 verified | """Tests for the dense→HV projection and the embedding expert.""" | |
| from __future__ import annotations | |
| import pytest | |
| import numpy as np | |
| from ensemble.projection import make_projection, dense_to_hv | |
| from ensemble import EmbeddingExpert, Brain, Expert | |
| from palimseste.hv import similarity | |
| class TestProjection: | |
| def test_make_projection_shape(self): | |
| R = make_projection(D=1000, dim=50, seed=1) | |
| assert R.shape == (1000, 50) | |
| assert R.dtype == np.int8 | |
| # entries are {+1,-1} | |
| assert set(np.unique(R)).issubset({-1, 1}) | |
| def test_make_projection_deterministic(self): | |
| a = make_projection(D=500, dim=20, seed=7) | |
| b = make_projection(D=500, dim=20, seed=7) | |
| assert np.array_equal(a, b) | |
| def test_different_seeds_differ(self): | |
| a = make_projection(D=500, dim=20, seed=1) | |
| b = make_projection(D=500, dim=20, seed=2) | |
| assert not np.array_equal(a, b) | |
| def test_dense_to_hv_identical_vectors(self): | |
| R = make_projection(D=2000, dim=30, seed=0) | |
| v = np.random.default_rng(1).standard_normal(30) | |
| h1 = dense_to_hv(v, R) | |
| h2 = dense_to_hv(v, R) | |
| assert h1 == h2 | |
| assert h1.D == 2000 | |
| def test_projection_preserves_cosine(self): | |
| """Higher cosine => higher HV similarity (monotone).""" | |
| R = make_projection(D=4000, dim=100, seed=42) | |
| rng = np.random.default_rng(0) | |
| base = rng.standard_normal(100) | |
| base /= np.linalg.norm(base) | |
| def with_cos(c): | |
| o = rng.standard_normal(100); o -= base*np.dot(o, base); o /= np.linalg.norm(o) | |
| return c*base + (1-c*c)**0.5*o | |
| sims = [] | |
| for c in [1.0, 0.5, 0.0]: | |
| hv = dense_to_hv(with_cos(c), R) | |
| sims.append(similarity(hv, dense_to_hv(base, R))) | |
| # monotone decreasing as cosine drops | |
| assert sims[0] > sims[1] > sims[2] | |
| def test_dim_mismatch_raises(self): | |
| R = make_projection(D=100, dim=10, seed=0) | |
| with pytest.raises(ValueError): | |
| dense_to_hv(np.zeros(20), R) | |
| class TestEmbeddingExpert: | |
| CORPUS = ( | |
| "the cat sat on the mat and purred. the dog ran and barked loudly. " | |
| "cats and dogs are animals pets. the car drove fast on the road. " | |
| ) * 30 | |
| def test_from_corpus_local_builds(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| assert e.D == 2000 | |
| assert e.vocab_size > 5 | |
| assert "cat" in e.vectors or "the" in e.vectors | |
| def test_get_word_hv(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| hv = e.get_word_hv("cat") | |
| assert hv is not None | |
| assert hv.D == 2000 | |
| def test_get_word_hv_oov(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| assert e.get_word_hv("xyzqwert") is None | |
| def test_relevance_positive(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| r = e.relevance("the cat sat") | |
| assert 0.0 <= r <= 1.0 | |
| def test_candidate_hv(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| hv = e.candidate_hv("the cat") | |
| assert hv is not None | |
| assert hv.D == 2000 | |
| def test_candidate_hv_empty(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| # all OOV words -> None | |
| assert e.candidate_hv("xyzqwert zzz") is None | |
| def test_answer_returns_string(self): | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| a = e.answer("the cat") | |
| assert isinstance(a, str) | |
| def test_couples_to_brain(self): | |
| """The embedding expert plugs into a brain without changes.""" | |
| e = EmbeddingExpert.from_corpus_local(self.CORPUS, D=2000, dim=40) | |
| lex = Expert.from_text("hello world. " * 20, D=2000) | |
| brain = Brain() | |
| brain.add_expert(lex) | |
| brain.add_expert(e) | |
| assert brain.n_experts == 2 | |
| res = brain.query("the cat") | |
| assert isinstance(res.answer, str) | |
| def test_semantic_similarity_above_noise(self): | |
| """Related words should be more similar than unrelated in a rich corpus.""" | |
| corpus = ( | |
| "king queen royal throne crown prince princess monarchy. " | |
| "cat dog pet animal fur paw tail. " | |
| "car truck vehicle road wheel engine drive. " | |
| ) * 40 | |
| e = EmbeddingExpert.from_corpus_local(corpus, D=3000, dim=60) | |
| king, queen = e.get_word_hv("king"), e.get_word_hv("queen") | |
| cat, dog = e.get_word_hv("cat"), e.get_word_hv("dog") | |
| king_dog = similarity(king, dog) if king and dog else 0 | |
| king_queen = similarity(king, queen) if king and queen else 0 | |
| # related (king~queen) should beat unrelated (king~dog) — or at least | |
| # the within-category pairs should be positive | |
| if king and queen and cat and dog: | |
| assert king_queen > -0.2 # not strongly anti-correlated | |
| def test_solve_analogy_returns_answer(self): | |
| """Analogy with structured synthetic vectors recovers the right word.""" | |
| rng = np.random.default_rng(0) | |
| dim = 40 | |
| base_c = rng.standard_normal(dim) | |
| base_C = rng.standard_normal(dim) | |
| vecs = {} | |
| pairs = {'france': 'paris', 'germany': 'berlin', 'italy': 'rome'} | |
| for c, C in pairs.items(): | |
| noise = rng.standard_normal(dim) * 0.2 | |
| vecs[c] = base_c + noise | |
| vecs[C.lower()] = base_C + noise | |
| # unseen: spain -> madrid (same noise pattern) | |
| noise = rng.standard_normal(dim) * 0.2 | |
| vecs['spain'] = base_c + noise | |
| vecs['madrid'] = base_C + noise | |
| R = make_projection(D=2000, dim=dim, seed=0) | |
| word_hvs = {w: dense_to_hv(v, R) for w, v in vecs.items()} | |
| e = EmbeddingExpert(domain='t', D=2000, vectors=vecs, word_hvs=word_hvs, | |
| projection=R, signature_hv=word_hvs['france'], dim=dim) | |
| e.learn_relation('capital_of', pairs) | |
| best, sim = e.solve_analogy('capital_of', 'spain') | |
| assert best == 'madrid' | |
| assert sim > 0.5 | |
| def test_solve_analogy_unknown_relation(self): | |
| e = EmbeddingExpert.from_corpus_local("a b c d. " * 30, D=1000, dim=20) | |
| best, sim = e.solve_analogy('nonexistent', 'x') | |
| assert best is None | |
| def test_solve_analogy_excludes_slot(self): | |
| """The query slot must not be returned as its own answer.""" | |
| rng = np.random.default_rng(1) | |
| dim = 30 | |
| vecs = {w: rng.standard_normal(dim) for w in | |
| ['france', 'paris', 'germany', 'berlin', 'spain', 'madrid']} | |
| R = make_projection(D=1000, dim=dim, seed=0) | |
| whvs = {w: dense_to_hv(v, R) for w, v in vecs.items()} | |
| e = EmbeddingExpert(domain='t', D=1000, vectors=vecs, word_hvs=whvs, | |
| projection=R, signature_hv=whvs['france'], dim=dim) | |
| e.learn_relation('capital_of', {'france': 'paris', 'germany': 'berlin'}) | |
| best, _ = e.solve_analogy('capital_of', 'spain') | |
| assert best != 'spain' | |