| |
| """Viterbi lattice rescoring with 3-gram LM. |
| |
| Given per-position top-K classifier (ABZ, logp) lattice, find best path by: |
| score(path) = sum_t [ log_p_cls(t) + lambda_lm * log_p_lm(w_t | w_{t-2}, w_{t-1}) ] |
| |
| Supports both KenLM (binary) and pure-Python JSON n-gram fallback. |
| """ |
| import json, math |
| from pathlib import Path |
|
|
| class _PythonLM: |
| def __init__(self, path): |
| obj = json.load(open(path)) |
| self.V = obj['vocab_size'] |
| self.order = obj['order'] |
| self.alpha = obj.get('alpha', 1.0) |
| self.unigram = obj['unigram'] |
| self.U = sum(self.unigram.values()) |
| self.cond = obj['cond'] |
|
|
| def _cond_prob(self, ctx, w): |
| |
| while len(ctx) > 0: |
| key = ' '.join(ctx) |
| if key in self.cond: |
| ctr = self.cond[key] |
| tot = sum(ctr.values()) |
| c = ctr.get(w, 0) |
| return (c + self.alpha) / (tot + self.alpha * self.V) |
| ctx = ctx[1:] |
| c = self.unigram.get(w, 0) |
| return (c + self.alpha) / (self.U + self.alpha * self.V) |
|
|
| def score(self, context_tokens, w): |
| ctx = context_tokens[-(self.order - 1):] |
| return math.log(max(1e-20, self._cond_prob(ctx, w))) |
|
|
| def load_lm(path): |
| path = Path(path) |
| if path.suffix in ('.binary', '.arpa'): |
| try: |
| import kenlm |
| return kenlm.Model(str(path)) |
| except Exception as e: |
| print(f"kenlm unavailable ({e}); try .json instead"); raise |
| |
| return _PythonLM(path) |
|
|
| def lm_score(lm, context, w): |
| """Unified log-prob interface.""" |
| if hasattr(lm, 'BaseScore'): |
| |
| s = ' '.join(context[-2:] + [w]) if context else w |
| return lm.score(s, bos=False, eos=False) * math.log(10) |
| return lm.score(context, w) |
|
|
| def viterbi_rescore(lattice, lm, lambda_lm=0.3, beam=16): |
| """ |
| lattice: list of [(token, log_p_cls), ...] per position (sorted desc by log_p) |
| lm: KenLM or _PythonLM |
| Returns: (best_path_tokens, total_score) |
| """ |
| if not lattice: return [], 0.0 |
| |
| INF = float('-inf') |
| beams = [{(): (0.0, [])}] |
| for t, candidates in enumerate(lattice): |
| candidates = candidates[:beam] |
| new_beam = {} |
| for ctx, (s_prev, path_prev) in beams[-1].items(): |
| for w, lp_cls in candidates: |
| lp_lm = lm_score(lm, list(ctx), w) |
| new_s = s_prev + lp_cls + lambda_lm * lp_lm |
| new_ctx = tuple((list(ctx) + [w])[-2:]) |
| key = new_ctx |
| if key not in new_beam or new_beam[key][0] < new_s: |
| new_beam[key] = (new_s, path_prev + [w]) |
| |
| new_beam = dict(sorted(new_beam.items(), key=lambda kv: kv[1][0], reverse=True)[:beam]) |
| beams.append(new_beam) |
| |
| best = max(beams[-1].values(), key=lambda v: v[0]) |
| return best[1], best[0] |
|
|
| if __name__ == '__main__': |
| import argparse |
| ap = argparse.ArgumentParser() |
| ap.add_argument('--lm', required=True) |
| ap.add_argument('--lattice', required=True, help='JSONL: one list-of-(token,logp) per line') |
| ap.add_argument('--output', required=True) |
| ap.add_argument('--lambda-lm', type=float, default=0.3) |
| args = ap.parse_args() |
|
|
| lm = load_lm(args.lm) |
| with open(args.lattice) as f, open(args.output, 'w') as g: |
| for line in f: |
| lat = json.loads(line) |
| path, score = viterbi_rescore(lat, lm, lambda_lm=args.lambda_lm) |
| g.write(json.dumps({'path': path, 'score': score}) + '\n') |
| print(f"Rescored → {args.output}") |
|
|