Download source/scripts/benchmarks/rime_lattice.py from Voltline/vimeml-tiny-zh-v1: direct link, hf CLI and curl.
- Browser
- Download file 8.07 kB
-
https://huggingface.co/Voltline/vimeml-tiny-zh-v1/resolve/main/source/scripts/benchmarks/rime_lattice.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-zh-v1/source/scripts/benchmarks/rime_lattice.py
-
curl -L -o rime_lattice.py https://huggingface.co/Voltline/vimeml-tiny-zh-v1/resolve/main/source/scripts/benchmarks/rime_lattice.py
8.07 kB
| """Whole-input candidate pools for the Standard Chinese IME benchmark: Rime first, then a | |
| word lattice over the rime-ice dictionary. | |
| For a key sequence Rime's sentence maker returns one whole-input sentence; its other | |
| candidates cover only a prefix of the input. So the pool for an item is | |
| 1. Rime's whole-input candidates, in Rime order (rime_candidates.py query), then | |
| 2. the k best whole-input paths of a lattice built from the same rime-ice dictionaries | |
| (cn_dicts/8105, base, ext, tencent, others, as imported by rime_ice.dict.yaml), | |
| deduplicated, up to --n. Every candidate records its origin, so the Rime-only recall and the | |
| pool recall are reported separately, and the order without an LM is Rime's. | |
| Lattice model: unigram over dictionary words, score = sum log(weight / total weight). Words | |
| without a code (the tencent list) get codes the way librime encodes phrases: every | |
| combination of their characters' readings, using only readings with at least 5% of the | |
| character's total weight (librime's kMinimalWeightProportionForWordMaking; 了 liao is not | |
| used for 了解 this way, 长 chang and zhang both are). Entries without a weight (others: extra | |
| readings of known words) take the word's highest weight elsewhere, else 1. | |
| index build <work>/lattice-index.pkl from the rime-ice checkout | |
| pool <rime-input.jsonl> <rime-candidates.jsonl> <out.jsonl> --track full|abbrev | |
| The full track splits keys into syllables (every split is tried: xian = xian | xi an); the | |
| abbreviated track reads each key letter as the first letter of one syllable. | |
| """ | |
| import argparse | |
| import collections | |
| import heapq | |
| import itertools | |
| import json | |
| import math | |
| from pathlib import Path | |
| import pickle | |
| ROOT=Path(__file__).resolve().parents[2] | |
| ENGINE=ROOT/'datasets/ime-standard-zh-v1/engine' | |
| TABLES=['cn_dicts/8105','cn_dicts/base','cn_dicts/ext','cn_dicts/tencent','cn_dicts/others'] | |
| MIN_PROPORTION=0.05 | |
| PER_CODE=40 # words kept per code (by weight) | |
| MAX_WORD=8 # syllables per dictionary word in the lattice | |
| ABBREV_MAX_WORD=4 | |
| def entries(path): | |
| body=False | |
| for line in path.read_text(encoding='utf-8').split('\n'): | |
| if not body: | |
| body=line.strip()=='...'; continue | |
| if not line or line.startswith('#'): continue | |
| yield line.rstrip('\r').split('\t') | |
| def build_index(shared): | |
| shared=Path(shared); rows=[] | |
| for t in TABLES: | |
| for parts in entries(shared/f'{t}.dict.yaml'): | |
| text=parts[0] | |
| code=parts[1] if len(parts)>=2 and not parts[1].isdigit() else None | |
| w=parts[-1] if len(parts)>=2 and parts[-1].isdigit() else None | |
| rows.append((t,text,code,int(w) if w is not None else None)) | |
| char_codes=collections.defaultdict(collections.Counter) | |
| for t,text,code,w in rows: | |
| if t=='cn_dicts/8105' and code and len(text)==1: char_codes[text][code]+=w or 0 | |
| usable={c:[k for k,v in cs.items() if v>=MIN_PROPORTION*sum(cs.values())] for c,cs in char_codes.items()} | |
| best_weight=collections.Counter() | |
| for t,text,code,w in rows: | |
| if w is not None: best_weight[text]=max(best_weight[text],w) | |
| table=collections.defaultdict(dict); unencodable=0 | |
| for t,text,code,w in rows: | |
| w=max(1,w if w is not None else best_weight.get(text,1)) # some 8105 rare chars have weight 0 | |
| if code: codes=[tuple(code.split())] | |
| elif all(c in usable for c in text): | |
| codes=list(itertools.islice(itertools.product(*(usable[c] for c in text)),64)) | |
| else: | |
| unencodable+=1; continue | |
| for cd in codes: | |
| if len(cd)!=len(text): continue # letters, digits or erhua spellings | |
| d=table[cd]; d[text]=max(d.get(text,0),w) | |
| total=sum(max(d.values()) for d in table.values()) | |
| full={}; abbrev=collections.defaultdict(dict) | |
| for cd,d in table.items(): | |
| top=heapq.nlargest(PER_CODE,d.items(),key=lambda x:x[1]) | |
| full[' '.join(cd)]=[(text,math.log(w/total)) for text,w in top] | |
| if len(cd)<=ABBREV_MAX_WORD: | |
| a=abbrev[''.join(s[0] for s in cd)] | |
| for text,w in top: a[text]=max(a.get(text,0),w) | |
| abbrev={k:[(text,math.log(w/total)) for text,w in heapq.nlargest(PER_CODE,d.items(),key=lambda x:x[1])] | |
| for k,d in abbrev.items()} | |
| syllables=sorted({s for cd in table for s in cd if s.isalpha() and s.isascii()}) | |
| return dict(full=full,abbrev=abbrev,syllables=syllables, | |
| stats=dict(rows=len(rows),codes=len(table),unencodable=unencodable,syllables=len(syllables))) | |
| class Lattice: | |
| def __init__(self,index): | |
| self.full,self.abbrev=index['full'],index['abbrev'] | |
| self.syl=set(index['syllables']); self.maxlen=max(map(len,self.syl)) | |
| def edges_full(self,keys): | |
| """(start, end, text, logp): words whose syllables spell keys[start:end].""" | |
| n=len(keys); out=[] | |
| def walk(i,j,syls): | |
| if syls: | |
| for text,lp in self.full.get(' '.join(syls),()): out.append((i,j,text,lp)) | |
| if len(syls)==MAX_WORD: return | |
| for k in range(j+1,min(n,j+self.maxlen)+1): | |
| if keys[j:k] in self.syl: walk(i,k,syls+[keys[j:k]]) | |
| for i in range(n): walk(i,i,[]) | |
| return out | |
| def edges_abbrev(self,keys): | |
| out=[] | |
| for i in range(len(keys)): | |
| for j in range(i+1,min(len(keys),i+ABBREV_MAX_WORD)+1): | |
| for text,lp in self.abbrev.get(keys[i:j],()): out.append((i,j,text,lp)) | |
| return out | |
| def kbest(self,keys,track,k): | |
| edges=self.edges_full(keys) if track=='full' else self.edges_abbrev(keys) | |
| by_start=collections.defaultdict(list) | |
| for i,j,text,lp in edges: by_start[i].append((j,text,lp)) | |
| best=collections.defaultdict(dict); best[0]['']=0.0 # position -> text -> best score | |
| for i in range(len(keys)): | |
| if not best[i]: continue | |
| here=heapq.nlargest(k,best[i].items(),key=lambda x:x[1]) | |
| for j,text,lp in by_start[i]: | |
| d=best[j] | |
| for prefix,s in here: | |
| t=prefix+text | |
| if s+lp>d.get(t,-math.inf): d[t]=s+lp | |
| return [t for t,_ in heapq.nlargest(k,best[len(keys)].items(),key=lambda x:x[1])] | |
| def cmd_index(a): | |
| out=Path(a.work)/'lattice-index.pkl' | |
| index=build_index(a.shared) | |
| out.parent.mkdir(parents=True,exist_ok=True) | |
| with open(out,'wb') as fh: pickle.dump(index,fh,protocol=pickle.HIGHEST_PROTOCOL) | |
| print(json.dumps(dict(out=str(out),**index['stats']))) | |
| def cmd_pool(a): | |
| with open(Path(a.work)/'lattice-index.pkl','rb') as fh: lat=Lattice(pickle.load(fh)) | |
| rime={} | |
| for line in open(a.args[1],encoding='utf-8'): | |
| if line.strip(): r=json.loads(line); rime[r['id']]=r | |
| with open(a.args[0],encoding='utf-8') as fi, open(a.args[2],'w',encoding='utf-8') as fo: | |
| for line in fi: | |
| if not line.strip(): continue | |
| q=json.loads(line); r=rime[q['id']]; assert r['keys']==q['keys'] | |
| pool=[dict(text=t,origin='rime') for t in r['candidates'][:a.n]] | |
| seen={c['text'] for c in pool} | |
| for t in lat.kbest(q['keys'],a.track,a.n): | |
| if len(pool)>=a.n: break | |
| if len(t)==q['n_han'] and t not in seen: pool.append(dict(text=t,origin='lattice')); seen.add(t) | |
| fo.write(json.dumps(dict(id=q['id'],keys=q['keys'],track=a.track,engine=r['engine'],candidates=pool), | |
| ensure_ascii=False)+'\n') | |
| def main(): | |
| p=argparse.ArgumentParser(description=__doc__,formatter_class=argparse.RawDescriptionHelpFormatter) | |
| p.add_argument('command',choices=['index','pool']) | |
| p.add_argument('args',nargs='*') | |
| p.add_argument('--shared',default=str(ENGINE/'rime-ice')) | |
| p.add_argument('--work',default=str(ROOT/'outputs/benchmarks/ime-standard-zh-v1/rime')) | |
| p.add_argument('--track',choices=['full','abbrev'],default='full') | |
| p.add_argument('--n',type=int,default=20) | |
| a=p.parse_args() | |
| dict(index=cmd_index,pool=cmd_pool)[a.command](a) | |
| if __name__=='__main__': | |
| main() | |