File size: 5,264 Bytes
df41178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
embed_cards.py -- 巨大モデルから抜いた「単語カードの表」を引く

  ★ 2.35GB をメモリに載せない。必要な行だけ seek して読む(=表引き)
  ★ 掛け算は 2本のベクトルを比べる時だけ。層を通す推論は一切しない
"""
import os, json, base64, struct, functools

HERE  = os.path.dirname(os.path.abspath(__file__))
MINED = os.path.join(HERE, "mined")


class EmbedCards:
    def __init__(self, tag="Kimi-K2-Instruct", tokfile="kimi.tiktoken.model",
                 tok_kind="tiktoken"):
        meta = json.load(open(os.path.join(MINED, f"{tag}.embed.json")))
        self.bin   = os.path.join(MINED, f"{tag}.embed.bin")
        self.rows, self.dim = meta["shape"]
        self.dtype = meta["dtype"]
        assert self.dtype == "BF16", f"未対応の型: {self.dtype}"
        self.rowbytes = self.dim * 2
        self.f = open(self.bin, "rb")
        # トークナイザ: バイト列 → 番号
        self.kind = tok_kind
        self.vocab = {}
        path = os.path.join(MINED, tokfile)
        if tok_kind == "tiktoken":
            # tiktoken 形式(base64 と順位が1行ずつ)
            for line in open(path):
                b64, rank = line.split()
                self.vocab[base64.b64decode(b64)] = int(rank)
        elif tok_kind == "unigram":
            # SentencePiece Unigram(tokenizer.json)。並び順が番号
            d = json.load(open(path, encoding="utf-8"))
            for i, ent in enumerate(d["model"]["vocab"]):
                tok = ent[0] if isinstance(ent, list) else ent
                self.vocab[tok.encode("utf-8")] = i
        else:
            raise ValueError(f"未対応のトークナイザ: {tok_kind}")
        self.maxlen = max(len(k) for k in self.vocab)

    # ---- 表引き(ここが軽さの核心) ----
    @functools.lru_cache(maxsize=20000)
    def row(self, tid):
        """番号 tid の行を読む。ファイルの該当位置へ跳んで 14KB 読むだけ"""
        if not (0 <= tid < self.rows):
            return None
        self.f.seek(tid * self.rowbytes)
        raw = self.f.read(self.rowbytes)
        # BF16 は float32 の上位16ビット。下に0を足せば float32 になる
        n = self.dim
        u16 = struct.unpack(f"<{n}H", raw)
        f32 = struct.unpack(f"<{n}f", struct.pack(f"<{n}I", *(x << 16 for x in u16)))
        return f32

    # ---- 単語 → 番号の並び(最長一致) ----
    def encode(self, s):
        if self.kind == "unigram":
            # 語そのもの、または語頭印つきで引けるかを見る
            for cand in (s, "\u2581" + s):
                t = self.vocab.get(cand.encode("utf-8"))
                if t is not None:
                    return [t]
            return self._greedy(s)
        return self._greedy(s)

    def _greedy(self, s):
        b, out, i = s.encode("utf-8"), [], 0
        while i < len(b):
            for L in range(min(self.maxlen, len(b) - i), 0, -1):
                t = self.vocab.get(b[i:i + L])
                if t is not None:
                    out.append(t); i += L; break
            else:
                i += 1                      # どうしても引けないバイトは飛ばす
        return out

    # ---- 単語のベクトル(複数トークンなら平均) ----
    def vec(self, word):
        ids = self.encode(word)
        if not ids:
            return None
        vs = [self.row(t) for t in ids]
        vs = [v for v in vs if v]
        if not vs:
            return None
        n = self.dim
        return [sum(v[i] for v in vs) / len(vs) for i in range(n)]

    # ---- 近さ ----
    @staticmethod
    def cos(a, b):
        d = sa = sb = 0.0
        for x, y in zip(a, b):
            d += x * y; sa += x * x; sb += y * y
        return d / ((sa ** .5) * (sb ** .5)) if sa and sb else 0.0

    def similarity(self, a, b):
        va, vb = self.vec(a), self.vec(b)
        return self.cos(va, vb) if va and vb else 0.0

    def nearest(self, word, candidates, threshold=0.0):
        v = self.vec(word)
        if not v:
            return None, 0.0
        best, sc = None, -1.0
        for c in candidates:
            vc = self.vec(c)
            if not vc:
                continue
            s = self.cos(v, vc)
            if s > sc:
                best, sc = c, s
        return (best, sc) if sc >= threshold else (None, 0.0)


if __name__ == "__main__":
    import sys, time
    e = EmbedCards()
    print(f"表: {e.rows} 語 × {e.dim} 次元  ({os.path.getsize(e.bin)/1e9:.2f} GB)")
    if len(sys.argv) > 2:
        t0 = time.time()
        print(f"{e.similarity(sys.argv[1], sys.argv[2]):.4f}  ({(time.time()-t0)*1000:.1f} ms)")
    else:
        pairs = [("猫","犬"),("猫","石"),("写真","画像"),("写真","石"),
                 ("まとめて","移動"),("まとめて","削除"),("片付ける","整理"),
                 ("デスクトップ","Desktop"),("机の上","デスクトップ")]
        t0 = time.time()
        for a, b in pairs:
            print(f"  {a:8} - {b:8} : {e.similarity(a,b):+.4f}")
        print(f"\n  {len(pairs)}組で {(time.time()-t0)*1000:.0f} ミリ秒")