File size: 7,144 Bytes
0bee73d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595d852
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""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'