File size: 6,911 Bytes
66eb4a8 36d9085 66eb4a8 df4b9f0 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 66eb4a8 36d9085 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | ---
license: mit
language:
- zh
library_name: onnxruntime
tags:
- onnx
- bopomofo
- zhuyin
- input-method
- traditional-chinese
---
# 0c D512 Zhuyin Candidate Reranker (PF512)
**Project website:** [0c.biggo.com](https://0c.biggo.com)
A neural candidate reranker for a Traditional Chinese Zhuyin (Bopomofo) input method.
Given confirmed preceding text (up to 64 characters), Zhuyin readings, and supplied candidates,
the model computes log P(candidate | context, reading). These scores can be combined with an input-method engine's
scores to decide whether to override its default candidate. A candidate source is required; the model does not generate candidates itself.
- Architecture: a dedicated causal Transformer decoder, 8 layers, hidden size 512, 8 attention heads, FFN size 768, pretrained from random initialization and then fine-tuned for candidate ranking
- Format: ONNX (opset 18), CPU inference
- Vocabulary: 27,857 characters and 416 Zhuyin bases (`vocab.json`)
## Files
| File | Description |
|---|---|
| `model.onnx` | Candidate scoring model; SHA-256 `71af1f0e77b2752cc71cbc04bf19c8e8f2e765662246f766e65a2822bab4e66e` |
| `vocab.json` | Special tokens, character vocabulary, and Zhuyin bases; SHA-256 `556dffaffbf4c05211a846dcee55ef735cea68c5754f9bffcc6f9588124172b3` |
| `manifest.json` | Operating points, input limits, and file hashes |
## Installation
Requires Python 3.9 or later. The examples use CPU inference.
```sh
pip install onnxruntime numpy huggingface_hub
```
The first run downloads the model from Hugging Face (approximately 130 MB). Subsequent runs use the local cache.
## Usage
Save the complete example below as `rank.py` and run it. Supply one string per Zhuyin syllable
(for example, `ㄍㄨㄥ` and `ㄩㄢˊ`). Each candidate must have exactly one character per syllable.
- With supplied candidates only: use `rank()` to order homophonic candidates by model score and find the best match for the context.
- With an input-method engine: use `choose()` to combine engine and model scores, overriding the engine default only when the required score margin is met.
```python
import json
import numpy as np
import onnxruntime as ort
from huggingface_hub import snapshot_download
path = snapshot_download("Funmula/0c-sequence-d512")
manifest = json.load(open(f"{path}/manifest.json", encoding="utf-8"))
vocab = json.load(open(f"{path}/vocab.json", encoding="utf-8"))
CHARS = {c: i + 8 for i, c in enumerate(vocab["characters"])} # IDs 0–7 are special tokens; 4 = UNK
BASES = {b: i + 2 for i, b in enumerate(vocab["bases"])}
TONES = {"ˊ": 2, "ˇ": 3, "ˋ": 4, "˙": 5} # No tone mark = first tone
NEUTRAL = set(manifest.get("neutral_tolerant_last", [])) # Final particles are often typed with first tone
session = ort.InferenceSession(f"{path}/model.onnx", providers=["CPUExecutionProvider"])
def syllable(s):
base = s[:-1] if s[-1] in TONES else s
return BASES[base], TONES.get(s[-1], 1)
def log_prob(context, reading, candidate):
# [BOS] context [SEP] reading×n [OUT] first n−1 candidate characters.
# Return log P(candidate | context, reading).
head = [1] + [CHARS.get(c, 4) for c in context[-64:]] + [2]
sounds = [syllable(s) for s in reading]
cand = [CHARS[c] for c in candidate] # Unknown candidate characters raise KeyError for caller fallback
n, out = len(cand), len(head) + len(sounds)
ids = head + [0] * n + [3] + cand[:-1]
bases = [0] * len(head) + [b for b, _ in sounds] + [0] * n
tones = [0] * len(head) + [t for _, t in sounds] + [0] * n
types = [0] * len(head) + [1] * n + [0] + [2] * (n - 1)
L = len(ids)
row = lambda x: np.array([x], dtype=np.int64)
feeds = {"ids": row(ids), "bases": row(bases), "tones": row(tones), "types": row(types),
"positions": row(list(range(L))), "mask": np.tril(np.ones((L, L), dtype=bool))[None, None],
"proj_pos": np.arange(out, L, dtype=np.int64), "target_node": row(list(range(n))),
"target_ids": row([x - 6 for x in cand])}
return float(session.run(["scores"], feeds)[0][0])
def score(context, reading, candidate):
s = log_prob(context, reading, candidate)
last = reading[-1]
base = last[:-1] if last[-1] in TONES else last
if base in NEUTRAL and last[-1] != "˙": # Also score the final syllable with neutral tone and keep the higher score
s = max(s, log_prob(context, reading[:-1] + [base + "˙"], candidate))
return s
def choose(context, reading, candidates, engine_scores, w=manifest["weight"], tau=manifest["margin"]):
# candidates[0] is the engine default; engine_scores are engine log scores.
# Return the selected candidate index.
try:
neural = [score(context, reading, c) for c in candidates]
except KeyError:
return 0
fused = [e + w * s for e, s in zip(engine_scores, neural)]
best = max(range(len(fused)), key=fused.__getitem__)
return best if best != 0 and fused[best] - fused[0] > tau else 0
def rank(context, reading, candidates):
# Rank supplied homophonic candidates by model score alone, highest first.
return sorted(candidates, key=lambda c: score(context, reading, c), reverse=True)
print(rank("下週就是", ["ㄑㄧˊ", "ㄓㄨㄥ", "ㄎㄠˇ"], ["其中考", "期中考"])) # ['期中考', '其中考']
print(rank("", ["ㄐㄧㄣ", "ㄊㄧㄢ", "ㄊㄧㄢ", "ㄑㄧˋ", "ㄏㄣˇ", "ㄏㄠˇ"],
["金天天氣很好", "今天天氣很好", "今天天器很好"])) # ['今天天氣很好', ...]
# With an input-method engine, override candidates[0] only when the fused score warrants it.
candidates = ["公元", "公園"]
print(candidates[choose("今天天氣很好我們去", ["ㄍㄨㄥ", "ㄩㄢˊ"], candidates, [-7.0, -7.5])]) # 公園
```
## Operating Points and Limitations
- Fused score = engine score + w × model score, with w = 2. Override only if another candidate has the highest score and exceeds the engine default by more than τ = 0.
- Supported operating range: 2–32 syllables, at most 16 candidates, and the last 64 context characters.
- If any candidate contains a character outside the vocabulary, `choose()` retains the engine default for the entire case.
- Training candidates came from the 0c dictionary (`dictionary_sha256 = 517e6c8bb5cbb145c0554edc3cc91c2b55fc753dd42e11194d83f6b0e119aea3`). Calibrate w and τ again when using a different engine's score scale.
- Shared candidate prefixes can also be scored together as a tree, using an ancestor attention mask and multiple `target_node`/`target_ids` rows. Scores match individual candidate scoring.
## License and Release Scope
Model weights and the accompanying inference examples are released under the MIT license (see `LICENSE`).
This release covers this D512 checkpoint only. Training code and training materials are not included.
|