0c-sequence-d512 / README.md
kevinwatt's picture
Link the 0c project website
df4b9f0 verified
|
Raw History Blame Contribute Delete
6.91 kB
---
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.