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