| import json |
| import torch |
| import torch.nn.functional as F |
| from tokenizer import CharTokenizer, PAD, BOS, EOS |
| from model import TranslitModel |
| from config import CFG |
|
|
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" |
|
|
|
|
| class Trie: |
| def __init__(self): |
| self.root = {} |
|
|
| def insert(self, word, payload): |
| node = self.root |
| for c in word: |
| node = node.setdefault(c, {}) |
| node.setdefault("$", []).append(payload) |
|
|
| def prefix(self, pre, limit=400): |
| node = self.root |
| for c in pre: |
| if c not in node: |
| return [] |
| node = node[c] |
| out, stack = [], [node] |
| while stack and len(out) < limit: |
| n = stack.pop() |
| if "$" in n: |
| out.extend(n["$"]) |
| for k, v in n.items(): |
| if k != "$": |
| stack.append(v) |
| return out |
|
|
|
|
| class IME: |
| def __init__(self, ckpt_dir="checkpoints_ime"): |
| |
| with open(f"{ckpt_dir}/lexicon.json", encoding="utf-8") as f: |
| lex = json.load(f) |
| self.trie = Trie() |
| for g, targets in lex.items(): |
| for m, n in targets.items(): |
| self.trie.insert(g, (g, m, n)) |
|
|
| |
| self.tok = CharTokenizer.load(f"{ckpt_dir}/vocab.json") |
| self.model = TranslitModel( |
| len(self.tok), CFG.d_model, CFG.nhead, |
| CFG.num_layers, CFG.dim_ff, |
| dropout=0.0, max_len=64, |
| ).to(DEVICE) |
| sd = torch.load(f"{ckpt_dir}/best.pt", map_location=DEVICE)["model"] |
| sd = {k.replace("_orig_mod.", ""): v for k, v in sd.items()} |
| self.model.load_state_dict(sd) |
| self.model.eval() |
|
|
| |
| def _dict_suggest(self, q, k): |
| hits = self.trie.prefix(q, limit=400) |
| exact = [h for h in hits if h[0] == q] |
| pref = [h for h in hits if h[0] != q] |
|
|
| |
| |
| if exact: |
| top = max(h[2] for h in exact) |
| if top >= 5: |
| exact = [h for h in exact if h[2] >= max(2, top * 0.02)] |
| exact.sort(key=lambda h: -h[2]) |
|
|
| |
| |
| |
| |
| pref = [h for h in pref if h[2] >= 2 or len(h[0]) <= len(q) + 6] |
| pref.sort(key=lambda h: (-h[2], len(h[0]))) |
|
|
| out, seen = [], set() |
| for g, m, n in exact + pref: |
| if m not in seen: |
| seen.add(m) |
| out.append(m) |
| if len(out) == k: |
| break |
| return out |
|
|
| |
| @torch.no_grad() |
| def _model_suggest(self, q, k=3, beam=8, max_len=48): |
| src = torch.tensor([self.tok.encode(q)], device=DEVICE) |
| beams = [(torch.tensor([[BOS]], device=DEVICE), 0.0, False)] |
| for _ in range(max_len): |
| if all(f for _, _, f in beams): |
| break |
| cands = [] |
| for ids, lp, fin in beams: |
| if fin: |
| cands.append((ids, lp, True)) |
| continue |
| logits = self.model(src, ids)[0, -1] |
| logp = F.log_softmax(logits.float(), -1) |
| tl, ti = logp.topk(beam) |
| for l, ix in zip(tl.tolist(), ti.tolist()): |
| nids = torch.cat( |
| [ids, torch.tensor([[ix]], device=DEVICE)], 1) |
| cands.append((nids, lp + l, ix == EOS)) |
| cands.sort(key=lambda c: c[1] / (c[0].size(1) ** 0.7), |
| reverse=True) |
| beams = cands[:beam] |
|
|
| out, seen = [], set() |
| for ids, _, _ in beams: |
| s = self.tok.decode(ids[0].tolist()) |
| if s and s not in seen: |
| seen.add(s) |
| out.append(s) |
| if len(out) == k: |
| break |
| return out |
|
|
| |
| |
| def suggest(self, manglish_word, k=5): |
| q = manglish_word.strip() |
| if not q: |
| return [] |
| if self._is_passthrough(q): |
| return self._number_suggestions(q) |
| |
| ql = q.lower() |
| dict_hits = self._dict_suggest(ql, k) |
| |
| |
| has_exact = any(h[0] == ql for h in self.trie.prefix(ql, limit=50)) |
| |
| if not has_exact: |
| |
| |
| model_hits = self._model_suggest(ql, k=2) |
| merged = [] |
| for s in model_hits + dict_hits: |
| if s not in merged: |
| merged.append(s) |
| return merged[:k] |
| |
| return dict_hits[:k] |
|
|
| @staticmethod |
| def _is_passthrough(q): |
| |
| return not any(c.isalpha() and ord(c) < 128 for c in q) |
|
|
| |
| _ML_DIGITS = str.maketrans("0123456789", "൦൧൨൩൪൫൬൭൮൯") |
|
|
| def _number_suggestions(self, q): |
| out = [q] |
| if any(c.isdigit() for c in q): |
| ml = q.translate(self._ML_DIGITS) |
| if ml != q: |
| out.append(ml) |
| return out |
|
|
|
|
| if __name__ == "__main__": |
| ime = IME() |
| tests = [ |
| "thi", "thila", |
| "amma", "ente", "adukkala", "veedu", |
| "krithyamaayi", |
| "njan", "nammal", "keralam", |
| "thiruvananthapuram", |
| "blockchain", "kunjava", |
| ] |
| for w in tests: |
| print(f"{w:22s} -> {ime.suggest(w)}") |