Rishe compact models
These are the first model files for Rishe, a Persian NLP engine written in Rust. There are three tasks: part-of-speech tagging, named-entity recognition and ezafe detection. Each task is a separate ONNX file, so you only download the one you need.
The encoder is HooshvareLab/albert-fa-zwnj-base-v2. I fine-tuned one model per task and exported each to plain fp32 ONNX with batch size 1 and a dynamic sequence axis. You can run them with ONNX Runtime or with tract, which is what Rishe uses internally.
What is in the repository
| task | labels | max length | file size |
|---|---|---|---|
| POS | 16 UPOS tags | 192 | 44.8 MB |
| NER | 21 BIO tags | 192 | 44.8 MB |
| Ezafe | NO_EZAFE, EZAFE |
128 | 44.8 MB |
Each directory has model.onnx, tokenizer.json, tokenizer_config.json, labels.json and metrics.json. The root manifest.json has the sha256 and size of every file.
The files are independent. Pulling all three costs about 134 MB; pulling one costs about 45 MB. There is no shared backbone in this release, and I am not pretending there is.
Results
I evaluated on the same test sets used in the Rishe benchmarks.
| task | dataset | Rishe | Shekar | Hazm |
|---|---|---|---|---|
| POS micro F1 | UD-Seraji | 94.89 | 82.50 | 90.66 |
| POS micro F1 | UD-PerDT | 96.80 | 97.39 | 84.90 |
| NER exact-span F1 | PEYMA test | 94.05 | 57.14 | N/A |
| NER entity F1 | PEYMA+Arman mixed | 92.48 | N/A | N/A |
| Ezafe F1 | HomoRich held-out | 94.86 | N/A | N/A |
A few honest notes:
- The POS model is the better generalist: it is 12.4 points ahead of Shekar on Seraji and 0.59 behind on PerDT. One model across both treebanks was the goal, so I did not fine-tune a PerDT-only variant.
- NER is the clear win. The 94.05 is exact-span F1 on the PEYMA test file after stripping whitespace and ZWNJ, against 57.14 for Shekar's NER on the same file.
- The NER taxonomy here is wider than Shekar's: 21 tags covering DAT, EVE, FAC, LOC, MON, ORG, PCT, PER, PRO and TIM.
- Ezafe labels come from the
1ezafe marker in HomoRich'sMapped Phoneme. Rows where the grapheme and phoneme token counts did not match were skipped. The 94.86 is on a held-out HomoRich split, not on the refined subset used by other ezafe models.
How I trained them
- POS: UD Persian Seraji + PerDT train splits, evaluated on each test split separately.
- NER:
AliFartout/PEYMA-ARMAN-Mixed, using its train/validation/test splits, plus a PEYMA-only evaluation on the Rishe test file. - Ezafe:
MahtaFetrat/HomoRich-G2P-Persian, shuffled, with the first 150k rows scanned and up to 100k kept for training. - 6 epochs for POS, 5 for NER, 3 for ezafe; learning rate 3e-5, seed 42, one GPU. The accuracy harness is in the Rishe repository under
scripts/benchmark_models.py, the runtime benchmark underscripts/benchmark_modelpack.py, and the training notebook is not part of the public repo.
Using a model
The models are word-level. A word can be split into several subword pieces; the prediction for the word comes from the first piece.
import json
import numpy as np
import onnxruntime as ort
from tokenizers import Tokenizer
task = "pos" # or "ner", "ezafe"
tokenizer = Tokenizer.from_file(f"{task}/tokenizer.json")
labels = json.load(open(f"{task}/labels.json"))["labels"]
session = ort.InferenceSession(f"{task}/model.onnx", providers=["CPUExecutionProvider"])
def encode(words, max_len=192):
ids = [tokenizer.token_to_id("[CLS]")]
positions = []
for word in words:
pieces = tokenizer.encode(word, add_special_tokens=False).ids
if not pieces:
pieces = [tokenizer.token_to_id("<unk>")]
positions.append(len(ids))
ids.extend(pieces)
if len(ids) >= max_len - 1:
break
ids.append(tokenizer.token_to_id("[SEP]"))
ids = ids[:max_len]
return (
np.array([ids], dtype=np.int64),
np.ones((1, len(ids)), dtype=np.int64),
positions,
)
words = "کتابهای خوب را به دوستانم میدهم.".split()
input_ids, attention_mask, positions = encode(words)
logits = session.run(
None, {"input_ids": input_ids, "attention_mask": attention_mask}
)[0][0]
print([
(word, labels[int(logits[position].argmax())])
for word, position in zip(words, positions)
if position < logits.shape[0]
])
Using the packs with Rishe
Rishe 0.5.0 loads these packs directly; there is no ONNX Runtime install and no Python ML stack to set up.
rishe hub pull pos-compact
from rishe import ModelPack, hub
pos = ModelPack(hub.path_pack("pos-compact"))
pos.tag("کتابهای خوب را به دوستانم میدهم.")
# [('کتابهای', 'NOUN'), ('خوب', 'ADJ'), ('را', 'ADP'), ('به', 'ADP'),
# ('دوستانم', 'NOUN'), ('میدهم', 'VERB'), ('.', 'PUNCT')]
The other pack names are ner-compact and ezafe-compact; use
hub.pull_pack(name) to download and hub.path_pack(name) for an existing
cache entry. From Rust, enable the models feature and call
rishe::models::ModelPack::load(path)?.tag_text(text)?.
Limits
- These are token classifiers, not sentence embeddings. Use the Rishe embedding engine for vectors.
- The POS PerDT number is slightly behind Shekar; if that half-point matters, this pack is not the one for you.
- The ezafe labels are derived from the HomoRich phoneme annotation, not hand-checked word by word.
- The NER tag set is wider than the older five-class Persian NER benchmarks, so mixed-dataset comparisons need care.
- fp32 only. A distilled or int8 variant would be a different release; tract's int8 path is not fast enough to ship.
Data and license
- Base model:
HooshvareLab/albert-fa-zwnj-base-v2, Apache-2.0. - UD Persian Seraji and PerDT: CC BY-SA 4.0.
AliFartout/PEYMA-ARMAN-Mixed: MIT. The Arman corpus behind it was released for academic research; check that provenance if you plan commercial use.MahtaFetrat/HomoRich-G2P-Persian: CC0-1.0.
The fine-tuned weights are released under Apache-2.0, with attribution to the base model and datasets. See LICENSE and NOTICE.md.
Trained and published by Mahdi Alikhah.
Model tree for MahdiAlikhah/rishe-models
Base model
HooshvareLab/albert-fa-zwnj-base-v2