Spaces:
Running
Running
| """Experimental encoders that are intentionally separate from the serving path. | |
| ``NumberbatchEncoder`` has a fixed vocabulary: it never uses a subword or | |
| semantic fallback. Lookup tries, in this exact order: (1) the supplied surface | |
| form, (2) that form with one leading Hebrew servile prefix removed when it | |
| starts with one of ื, ื, ื, ื, ื, ื, ืฉ, and (3) underscores/spaces exchanged | |
| for the exact and prefix-stripped forms, in that order. An unresolved word is | |
| represented by an all-NaN row so experiment code can explicitly exclude it as | |
| OOV. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| import numpy as np | |
| from . import DATA_DIR | |
| DATA = Path(DATA_DIR) | |
| SERVILE_PREFIXES = frozenset("ืืืืืืืฉ") | |
| class NumberbatchEncoder: | |
| """Fixed-vocabulary Hebrew ConceptNet Numberbatch 19.08 vectors.""" | |
| def __init__(self) -> None: | |
| self.model_id = "conceptnet-numberbatch-he-19.08" | |
| with (DATA / "numberbatch_he_vocab.json").open(encoding="utf-8") as source: | |
| self.vocab = json.load(source) | |
| self.vectors = np.load(DATA / "numberbatch_he.npy", mmap_mode="r") | |
| if self.vectors.ndim != 2 or self.vectors.dtype != np.float32: | |
| raise ValueError("Numberbatch vectors must be a 2-D float32 array") | |
| if len(self.vocab) != self.vectors.shape[0]: | |
| raise ValueError("Numberbatch vocabulary and vector rows are misaligned") | |
| if len(set(self.vocab)) != len(self.vocab): | |
| raise ValueError("Numberbatch vocabulary contains duplicate surface terms") | |
| self.word_to_row = {word: row for row, word in enumerate(self.vocab)} | |
| def dim(self) -> int: | |
| return int(self.vectors.shape[1]) | |
| def _candidates(self, word: str): | |
| """Yield documented, trivial lookup variants once each.""" | |
| base = [word] | |
| if word and word[0] in SERVILE_PREFIXES: | |
| base.append(word[1:]) | |
| seen: set[str] = set() | |
| # Exact surface form, then one prefix-stripped form. | |
| for candidate in base: | |
| if candidate not in seen: | |
| seen.add(candidate) | |
| yield candidate | |
| # Finally try only the two trivial multiword spelling exchanges. | |
| for candidate in base: | |
| for variant in (candidate.replace("_", " "), candidate.replace(" ", "_")): | |
| if variant not in seen: | |
| seen.add(variant) | |
| yield variant | |
| def _row_for(self, word: str) -> int | None: | |
| for candidate in self._candidates(word): | |
| row = self.word_to_row.get(candidate) | |
| if row is not None: | |
| return row | |
| return None | |
| def embed(self, words) -> np.ndarray: | |
| words = list(words) | |
| result = np.full((len(words), self.dim), np.nan, dtype=np.float32) | |
| for output_row, word in enumerate(words): | |
| row = self._row_for(word) | |
| if row is not None: | |
| result[output_row] = self.vectors[row] | |
| elif len(words) < 100: | |
| result[output_row] = 0.0 | |
| return result | |
| class BlendEncoder: | |
| """Concatenated L2-normalized blend of fastText and Numberbatch.""" | |
| def __init__(self, w_ft: float, w_nb: float) -> None: | |
| self.model_id = f"blend_ft_{w_ft}_nb_{w_nb}" | |
| from .probe import make_encoder | |
| self.ft = make_encoder("fasttext") | |
| self.nb = NumberbatchEncoder() | |
| self.w_ft = w_ft | |
| self.w_nb = w_nb | |
| def embed(self, words) -> np.ndarray: | |
| words = list(words) | |
| V_ft = self.ft.embed(words) | |
| V_nb = self.nb.embed(words) | |
| V_nb_clean = np.nan_to_num(V_nb, nan=0.0) | |
| V_blend = np.concatenate([self.w_ft * V_ft, self.w_nb * V_nb_clean], axis=-1) | |
| norms = np.linalg.norm(V_blend, axis=1, keepdims=True) | |
| V_blend /= norms + 1e-9 | |
| return V_blend | |
| def make_exp_encoder(key: str): | |
| """Return the experimental Numberbatch encoder, a BlendEncoder, or a registered probe encoder.""" | |
| if key == "numberbatch": | |
| return NumberbatchEncoder() | |
| if key.startswith("blend_"): | |
| parts = key.split("_") | |
| if len(parts) == 3: | |
| w_ft = float(parts[1]) | |
| w_nb = float(parts[2]) | |
| return BlendEncoder(w_ft, w_nb) | |
| from .probe import make_encoder | |
| return make_encoder(key) | |
| def _selftest() -> None: | |
| encoder = NumberbatchEncoder() | |
| words = ["ืืื", "ืฉืืืื", "ื ืืจ", "ืคืจืืื"] | |
| vectors = encoder.embed(words) | |
| cosines = vectors @ vectors.T | |
| print(f"model_id={encoder.model_id}") | |
| print(f"dim={encoder.dim} N={len(encoder.vocab)}") | |
| print("pairwise_cosines") | |
| print(" " + " ".join(f"{word:>8}" for word in words)) | |
| for word, row in zip(words, cosines, strict=False): | |
| print(f"{word:>6} " + " ".join(f"{value:8.4f}" for value in row)) | |
| oov = encoder.embed(["ืืืืืืื"])[0] | |
| print(f"oov_all_nan={bool(np.isnan(oov).all())}") | |
| if not np.isnan(oov).all(): | |
| raise SystemExit("OOV handling failed") | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--selftest", action="store_true") | |
| args = parser.parse_args() | |
| if args.selftest: | |
| _selftest() | |
| else: | |
| parser.print_help() | |