Spaces:
Running
Running
Download datainventor/text.py from codelion/data-inventor: direct link, hf CLI and curl.
- Browser
- Download file 5.61 kB
-
https://huggingface.co/spaces/codelion/data-inventor/resolve/main/datainventor/text.py
- Command line
-
hf download hf://spaces/codelion/data-inventor/datainventor/text.py
-
curl -L -o text.py https://huggingface.co/spaces/codelion/data-inventor/resolve/main/datainventor/text.py
5.61 kB
| """Text primitives shared by every stage: normalization, token estimates, n-grams, MinHash, hashed features. | |
| Pure standard library so the hosted preview needs no heavy installs. numpy is used when present to speed up MinHash. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import math | |
| import re | |
| import struct | |
| import unicodedata | |
| try: # optional speed-up for MinHash on large runs | |
| import numpy as _np | |
| except Exception: # pragma: no cover | |
| _np = None | |
| WORD = re.compile(r"\w+", re.U) | |
| _MERSENNE = (1 << 61) - 1 | |
| _MAXH = (1 << 32) - 1 | |
| def norm(text: str) -> str: | |
| """Lowercase, strip accents and punctuation, squeeze whitespace. Used for exact dedup and n-gram matching.""" | |
| t = unicodedata.normalize("NFKD", text.lower()) | |
| t = "".join(c for c in t if not unicodedata.combining(c)) | |
| return " ".join(WORD.findall(t)) | |
| def words(text: str) -> list[str]: | |
| return WORD.findall(text.lower()) | |
| def est_tokens(text: str) -> int: | |
| """About four characters per token for English prose. Good enough to plan a budget, not to bill one.""" | |
| return max(1, len(text) // 4) | |
| def h64(s: str) -> int: | |
| return int.from_bytes(hashlib.blake2b(s.encode("utf-8", "ignore"), digest_size=8).digest(), "little") | |
| def doc_hash(text: str) -> str: | |
| return hashlib.blake2b(norm(text).encode(), digest_size=12).hexdigest() | |
| def ngrams(ws: list[str], n: int) -> list[str]: | |
| return [" ".join(ws[i:i + n]) for i in range(len(ws) - n + 1)] if len(ws) >= n else ([" ".join(ws)] if ws else []) | |
| # ---------------------------------------------------------------- MinHash + LSH (near-duplicate detection) | |
| class MinHasher: | |
| """MinHash over word 5-gram shingles. 112 permutations in 14 bands of 8 rows find pairs at roughly 0.8 Jaccard, | |
| the setting FineWeb used for near-duplicate removal.""" | |
| def __init__(self, num_perm: int = 112, bands: int = 14, shingle: int = 5, seed: int = 1): | |
| assert num_perm % bands == 0 | |
| self.num_perm, self.bands, self.rows, self.shingle = num_perm, bands, num_perm // bands, shingle | |
| import random | |
| rng = random.Random(seed) | |
| # a, b < 2**32 keep a*x + b inside 64 bits, so the numpy and pure-Python paths agree exactly | |
| self.a = [rng.randrange(1, _MAXH) for _ in range(num_perm)] | |
| self.b = [rng.randrange(0, _MAXH) for _ in range(num_perm)] | |
| if _np is not None: | |
| self._a = _np.array(self.a, dtype=_np.uint64) | |
| self._b = _np.array(self.b, dtype=_np.uint64) | |
| def signature(self, text: str) -> list[int]: | |
| sh = {h64(g) & _MAXH for g in ngrams(words(text), self.shingle)} or {0} | |
| if _np is not None: | |
| x = _np.fromiter(sh, dtype=_np.uint64)[:, None] | |
| v = ((x * self._a + self._b) % _np.uint64(_MERSENNE)) & _np.uint64(_MAXH) | |
| return v.min(axis=0).tolist() | |
| return [min(((a * x + b) % _MERSENNE) & _MAXH for x in sh) for a, b in zip(self.a, self.b)] | |
| def band_keys(self, sig: list[int]) -> list[bytes]: | |
| r = self.rows | |
| return [bytes([i]) + struct.pack(f"<{r}I", *sig[i * r:(i + 1) * r]) for i in range(self.bands)] | |
| def jaccard_sig(a: list[int], b: list[int]) -> float: | |
| return sum(1 for x, y in zip(a, b) if x == y) / len(a) | |
| class NearDupIndex: | |
| """In-memory LSH index. Holds one band table per band; fine for tens of millions of documents on a large box. | |
| Past that, shard by a hash prefix and run one index per shard.""" | |
| def __init__(self, hasher: MinHasher | None = None, threshold: float = 0.8): | |
| self.h = hasher or MinHasher() | |
| self.threshold = threshold | |
| self.tables: list[dict] = [dict() for _ in range(self.h.bands)] | |
| self.sigs: dict[int, list[int]] = {} | |
| def check_add(self, key: int, text: str) -> int | None: | |
| """Return the key of an earlier near-duplicate, or None after adding this document.""" | |
| return self.check_add_sig(key, self.h.signature(text)) | |
| def check_add_sig(self, key: int, sig: list[int]) -> int | None: | |
| """Same, with a signature computed elsewhere (in a worker process).""" | |
| cands = set() | |
| for i, k in enumerate(self.h.band_keys(sig)): | |
| hit = self.tables[i].get(k) | |
| if hit is not None: | |
| cands.add(hit) | |
| for c in cands: | |
| if jaccard_sig(sig, self.sigs[c]) >= self.threshold: | |
| return c | |
| for i, k in enumerate(self.h.band_keys(sig)): | |
| self.tables[i].setdefault(k, key) | |
| self.sigs[key] = sig | |
| return None | |
| # ---------------------------------------------------------------- hashed features (cheap classifier + relevance) | |
| def features(text: str, dim: int = 1 << 18) -> dict[int, float]: | |
| """Hashed unigrams and bigrams, log-scaled and L2-normalized. Works for any language with word characters.""" | |
| ws = words(text[:20000]) | |
| counts: dict[int, float] = {} | |
| for g in ws + ngrams(ws, 2): | |
| k = h64(g) % dim | |
| counts[k] = counts.get(k, 0.0) + 1.0 | |
| for k in counts: | |
| counts[k] = 1.0 + math.log(counts[k]) | |
| n = math.sqrt(sum(v * v for v in counts.values())) or 1.0 | |
| return {k: v / n for k, v in counts.items()} | |
| def cosine(a: dict[int, float], b: dict[int, float]) -> float: | |
| if len(a) > len(b): | |
| a, b = b, a | |
| return sum(v * b.get(k, 0.0) for k, v in a.items()) | |
| def centroid(vecs: list[dict[int, float]]) -> dict[int, float]: | |
| c: dict[int, float] = {} | |
| for v in vecs: | |
| for k, x in v.items(): | |
| c[k] = c.get(k, 0.0) + x | |
| n = math.sqrt(sum(x * x for x in c.values())) or 1.0 | |
| return {k: x / n for k, x in c.items()} | |