Rishe models
These are the model files for Rishe, a Persian NLP engine written in Rust. There are four ONNX task packs: part-of-speech tagging, named-entity recognition, ezafe detection and dependency parsing. There are also two static Persian word-embedding packs: rishe-embed-fa-300-v2 (the current one) and the original rishe-embed-fa-300. Each task is a separate pack, so you only download the one you need.
The encoder for all four task packs is HooshvareLab/albert-fa-zwnj-base-v2. I fine-tuned one model per task and exported each to plain fp32 ONNX. The token classifiers use batch size 1 with a dynamic sequence axis; the dependency parser uses a fixed 192-token sequence (the runtime pads) with a dynamic word axis. You can run the packs with ONNX Runtime or with tract, which is what Rishe uses internally.
The embedding packs are different: they are Word2Vec skip-gram models, 300 dimensions, loaded by rishe.Embeddings without ONNX. rishe-embed-fa-300-v2 is published twice — as a word2vec text .vec file and as .rvec, Rishe's compact binary format that stores each value as IEEE 754 binary16 and decodes to f32 on load — so the same matrix is available to other tools as well.
What is in the repository
| task | pack | labels | max length | file size | license |
|---|---|---|---|---|---|
| POS | pos-compact |
16 UPOS tags | 192 | 44.8 MB | Apache-2.0 |
| NER | ner-compact |
21 BIO tags | 192 | 44.8 MB | Apache-2.0 |
| Ezafe | ezafe-compact |
NO_EZAFE, EZAFE |
128 | 44.8 MB | Apache-2.0 |
| Dependency parsing | depparse-compact |
45 UD relations | 192 | 48.1 MB | CC BY-SA 4.0 |
| Static embeddings | rishe-embed-fa-300-v2 |
300d, 68,144 words | — | 42 MB (.rvec) / 236 MB (.vec) |
CC BY-SA 4.0 |
| Static embeddings (previous) | rishe-embed-fa-300 |
300d, 38,864 words | — | 135 MB | CC BY-SA 4.0 |
Each task directory has model.onnx, tokenizer.json, tokenizer_config.json, labels.json and metrics.json. The depparse/ directory also has its own MODEL_CARD.md, LICENSE and NOTICE.md. The root manifest.json has the sha256 and size of every file in every task.
The embeddings/ directory holds the 30,000-article pack: rishe-embed-fa-300.vec plus its metrics.json, MODEL_CARD.md, NOTICE.md, LICENSE, manifest.json and SHA256SUMS. The embeddings/rishe-embed-fa-300-v2/ directory is a self-contained pack with the same file set, the text payload rishe-embed-fa-300-v2.vec and the compact rishe-embed-fa-300-v2.rvec.
The files are independent. Pulling the four task packs costs about 182 MB; the embedding packs are 42 MB (v2 binary) and 135 MB (v1) on their own. 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 |
Dependency parsing, single-root MST, punctuation included:
| test treebank | UAS | LAS | LAS no punct |
|---|---|---|---|
| UD-Seraji | 85.38 | 77.99 | 78.18 |
| UD-PerDT | 91.84 | 87.31 | 87.33 |
Across seeds 42, 43 and 44, mean ± sample std LAS is 76.70 ± 1.18 on Seraji and 87.71 ± 0.43 on PerDT. The released parser is seed 42. It is a compact model: below BERT-scale Hazm and spaCy trf, above spaCy's compact sm on PerDT. Full details are in depparse/MODEL_CARD.md.
Static embeddings
rishe-embed-fa-300-v2 is Word2Vec skip-gram on 150,000 Persian Wikipedia articles: 300 dimensions, 68,144 words, 3,830,930 sentences, 57,661,561 tokens, 15 epochs. It ships as rishe-embed-fa-300-v2.rvec (42 MB, compact binary) and as rishe-embed-fa-300-v2.vec (236 MB, word2vec text); both hold the same matrix, and decoding the binary file moves a value by at most 4.9e-4 relative.
On an eight-pair sanity set the related-pair mean is 0.4740 against a 0.1393 random baseline (gap 0.3347), and most_similar takes about 11.7 ms. On ParsiNLU entailment with mean sentence vectors, accuracy is 0.4071, macro F1 0.2985 and token coverage 97.2%, against 0.3796 / 0.2438 / 82.6% for the 30,000-article pack. Hazm's 2.33 GB fastText model has a wider intrinsic gap (0.3509) and slightly higher coverage (98.5%) but scores 0.2602 macro F1 on the downstream task. This is a compact, fast model, not a Hazm replacement.
The 30,000-article pack is still here as rishe-embed-fa-300 for anyone pinning it. Full details are in embeddings/rishe-embed-fa-300-v2/MODEL_CARD.md.
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. - The dependency parser is not state of the art. It is a compact ALBERT model with a biaffine arc scorer and a head-conditioned relation classifier, decoded with Chu–Liu/Edmonds MST.
- The embedding numbers come from mean pooling, a weak sentence encoder. Contextual encoders score higher on the same entailment task; these packs are for fast, dependency-free word and sentence vectors.
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. - Dependency parsing: UD Persian Seraji + PerDT, 10 epochs, batch 16, AdamW at 2e-5 with linear warmup/decay, seed 42, best epoch 8 by development LAS.
- Static embeddings:
wikimedia/wikipediaconfig20231101.fa, streamed; Rishe's own sentence and word tokenizer; Word2Vec skip-gram, dimension 300, window 5, negative samples 5, sample 1e-4, minimum count 20, maximum final vocabulary 200,000, 15 epochs, seed 42. - The older compact packs used 6 epochs for POS, 5 for NER and 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 embedding comparison underscripts/compare_embeddings.py. The training notebooks are not part of the public repo.
Using a model
The POS, NER and ezafe models are word-level token classifiers. 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]
])
The dependency parser is different: it takes input_ids, attention_mask, word_positions, word_mask and head_indices, then decodes a single-root MST before scoring relations. Use the Rishe runtime or follow depparse/MODEL_CARD.md.
Using the packs with Rishe
Rishe loads all four packs directly; there is no ONNX Runtime install and no Python ML stack to set up.
rishe hub pull pos-compact
rishe hub pull depparse-compact
from rishe import ModelPack, Pipeline, hub
pos = ModelPack(hub.path_pack("pos-compact"))
pos.tag("کتابهای خوب را به دوستانم میدهم.")
parser = Pipeline(depparse_pack=hub.path_pack("depparse-compact"))
for item in parser.depparse("کتابهای خوب را به دوستانم میدهم."):
print(item.text, item.head, item.relation)
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)?, or use Pipeline::depparse for the parser.
The embedding packs are separate downloads and do not need the models feature:
rishe hub pull rishe-embed-fa-300-v2
from rishe import Embeddings, hub
vectors = Embeddings(hub.path("rishe-embed-fa-300-v2"))
vectors.dtype # 'f16'
len(vectors) # 68144
vectors.most_similar("کتاب", 5)
vectors.sentence_similarity("این کتاب خوب است", "کتاب خوبی بود")
Embeddings reads .vec and .rvec files alike, and dtype reports which width the file used. The previous pack is still available as rishe hub pull rishe-embed-fa-300. To convert your own word2vec file to the compact format, use rishe convert model.vec model.rvec --dtype f16.
Limits
- The task packs are token classifiers and a parser, not sentence embeddings. Word and sentence vectors are in the separate
embeddings/packs. - The embedding models are static: one vector per word type, no subword fallback, and Wikipedia-derived coverage. They are not contextual sentence encoders.
- 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.
- The parser is compact, trained on UD treebank conventions, and truncates to 192 subword positions.
- fp32 only for the task packs. 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.- Wikipedia (
wikimedia/wikipedia,20231101.fa): CC BY-SA and the GFDL.
The POS, NER and ezafe weights are released under Apache-2.0. The dependency-parsing weights are derived from the CC BY-SA 4.0 UD treebanks and are released under CC BY-SA 4.0. The depparse/LICENSE and depparse/NOTICE.md files carry the full terms; the root Apache-2.0 license does not apply to that directory. The embedding vectors are trained on Wikipedia and released under CC BY-SA 4.0; see embeddings/LICENSE and embeddings/NOTICE.md for the original pack, and embeddings/rishe-embed-fa-300-v2/LICENSE and its NOTICE.md for v2. See LICENSE and NOTICE.md for the rest of the repository.
Trained and published by Mahdi Alikhah.
Model tree for MahdiAlikhah/rishe-models
Base model
HooshvareLab/albert-fa-zwnj-base-v2