Download docs/semantic/sem_core.py from Lawless1987/covenant: direct link, hf CLI and curl.
- Browser
- Download file 3.24 kB
-
https://huggingface.co/Lawless1987/covenant/resolve/main/docs/semantic/sem_core.py
- Command line
-
hf download hf://Lawless1987/covenant/docs/semantic/sem_core.py
-
curl -L -o sem_core.py https://huggingface.co/Lawless1987/covenant/resolve/main/docs/semantic/sem_core.py
3.24 kB
| """Count-based semantic space: PPMI over a sliding window, then truncated SVD. | |
| Deliberately not a neural embedding. Everything here is a deterministic | |
| function of (corpus bytes, vocab_size, window, dim, seed) and the space | |
| carries a sha256 of those inputs so two runs can prove they used the same one. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import re | |
| import unicodedata | |
| from dataclasses import dataclass | |
| import numpy as np | |
| import scipy.sparse as sp | |
| from scipy.sparse.linalg import svds | |
| WORD = re.compile(r"[^\W\d_]+", re.UNICODE) | |
| def tokenize(text: str): | |
| text = unicodedata.normalize("NFC", text.lower()) | |
| return WORD.findall(text) | |
| class Space: | |
| lang: str | |
| words: list # index -> word, ordered by descending frequency | |
| index: dict # word -> index | |
| vecs: np.ndarray # (V, d) unit-norm rows | |
| counts: np.ndarray # (V,) raw unigram counts | |
| n_tokens: int | |
| sig: str | |
| def rank(self, w): | |
| return self.index.get(w) | |
| def has(self, w): | |
| return w in self.index | |
| def build_space(tokens, lang, vocab_size=20000, min_count=20, window=4, | |
| dim=300, alpha=0.75, seed=0): | |
| from collections import Counter | |
| c = Counter(tokens) | |
| kept = [(w, n) for w, n in c.most_common() if n >= min_count][:vocab_size] | |
| words = [w for w, _ in kept] | |
| counts = np.array([n for _, n in kept], dtype=np.float64) | |
| index = {w: i for i, w in enumerate(words)} | |
| V = len(words) | |
| ids = np.fromiter((index.get(t, -1) for t in tokens), dtype=np.int64, | |
| count=len(tokens)) | |
| M = sp.csr_matrix((V, V), dtype=np.float32) | |
| for off in range(1, window + 1): | |
| a, b = ids[:-off], ids[off:] | |
| m = (a >= 0) & (b >= 0) | |
| a = a[m].astype(np.int32); b = b[m].astype(np.int32) | |
| w = np.float32(1.0 / off) # harmonic weighting, both directions | |
| v = np.full(a.size, w, dtype=np.float32) | |
| M = M + sp.coo_matrix((v, (a, b)), shape=(V, V)).tocsr() | |
| M = M + sp.coo_matrix((v, (b, a)), shape=(V, V)).tocsr() | |
| del a, b, v | |
| # PPMI with context-distribution smoothing | |
| M = M.astype(np.float64) | |
| total = M.sum() | |
| rsum = np.asarray(M.sum(axis=1)).ravel() | |
| csum = np.asarray(M.sum(axis=0)).ravel() ** alpha | |
| csum_tot = csum.sum() | |
| M = M.tocoo() | |
| with np.errstate(divide="ignore", invalid="ignore"): | |
| pmi = np.log((M.data * total * csum_tot) | |
| / (rsum[M.row] * csum[M.col] * total)) | |
| pmi[~np.isfinite(pmi)] = 0.0 | |
| pmi = np.maximum(pmi, 0.0) | |
| keep = pmi > 0 | |
| P = sp.coo_matrix((pmi[keep], (M.row[keep], M.col[keep])), | |
| shape=(V, V)).tocsr() | |
| k = min(dim, min(P.shape) - 1) | |
| rng = np.random.default_rng(seed) | |
| v0 = rng.standard_normal(min(P.shape)) | |
| U, S, _ = svds(P, k=k, v0=v0) | |
| order = np.argsort(-S) | |
| U, S = U[:, order], S[order] | |
| X = U * (S ** 0.5) # eigenvalue weighting p=0.5 | |
| X /= (np.linalg.norm(X, axis=1, keepdims=True) + 1e-12) | |
| sig = hashlib.sha256( | |
| f"{lang}|{len(tokens)}|{V}|{window}|{dim}|{alpha}|{min_count}|{seed}" | |
| .encode()).hexdigest()[:12] | |
| return Space(lang, words, index, X, counts, len(tokens), sig) | |