Instructions to use TigreGotico/openlid-v2-onnx with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- fastText
How to use TigreGotico/openlid-v2-onnx with fastText:
from huggingface_hub import hf_hub_download import fasttext model = fasttext.load_model(hf_hub_download("TigreGotico/openlid-v2-onnx", "model.bin")) - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- README.md +152 -0
- config.json +9 -0
- labels.json +1 -0
- openlid-v2.int8.onnx +3 -0
- openlid-v2.onnx +3 -0
- openlid_v2_hash.py +144 -0
- vocab.txt +0 -0
README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: gpl-3.0
|
| 3 |
+
tags:
|
| 4 |
+
- onnx
|
| 5 |
+
- language-identification
|
| 6 |
+
- fasttext
|
| 7 |
+
library_name: onnx
|
| 8 |
+
pipeline_tag: text-classification
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# OpenLID-v2 - ONNX
|
| 12 |
+
|
| 13 |
+
ONNX export of [`laurievb/OpenLID-v2`](https://huggingface.co/laurievb/OpenLID-v2)
|
| 14 |
+
`model.bin`, an improved fastText supervised language identifier covering
|
| 15 |
+
200 language varieties in `iso639-3_Script` label form (for example
|
| 16 |
+
`eng_Latn`, `por_Latn`, `glg_Latn`), built on the
|
| 17 |
+
[OpenLID-v2 dataset](https://huggingface.co/datasets/laurievb/OpenLID-v2), an
|
| 18 |
+
updated version of Burchell et al., *An Open Dataset and Model for Language
|
| 19 |
+
Identification* (ACL 2023).
|
| 20 |
+
|
| 21 |
+
The licence is **GPL-3.0**, inherited unchanged from the original model.
|
| 22 |
+
|
| 23 |
+
> [!NOTE]
|
| 24 |
+
> The upstream model card recommends normalising/cleaning text with
|
| 25 |
+
> `openlid_normer.clean_line` before classification for best results. This
|
| 26 |
+
> export does not vendor that normalizer - it hashes raw text exactly like
|
| 27 |
+
> `TigreGotico/openlid-onnx` and `TigreGotico/glotlid-onnx` do, and parity was
|
| 28 |
+
> measured against fastText's own `predict()` on raw (uncleaned) text, so the
|
| 29 |
+
> two stay comparable. Apply the same cleaning step yourself before calling
|
| 30 |
+
> the featurizer if you want to match the upstream-recommended pipeline.
|
| 31 |
+
|
| 32 |
+
## What the graph does and does not do
|
| 33 |
+
|
| 34 |
+
fastText inference has five steps. The ONNX graph holds only the last two:
|
| 35 |
+
|
| 36 |
+
| Step | Where |
|
| 37 |
+
|---|---|
|
| 38 |
+
| 1. Tokenize the text | Python (`openlid_v2_hash.py`) |
|
| 39 |
+
| 2. Character n-grams per word (`minn`..`maxn`) | Python |
|
| 40 |
+
| 3. Hash the n-grams into buckets | Python |
|
| 41 |
+
| 4. Average the embedding rows of all feature ids | **ONNX** |
|
| 42 |
+
| 5. Multiply by the output matrix, then softmax | **ONNX** |
|
| 43 |
+
|
| 44 |
+
Steps 1 to 3 are string processing. ONNX has no portable operator for
|
| 45 |
+
fastText's FNV-1a byte hash over UTF-8, so that work stays in Python. The
|
| 46 |
+
split keeps the graph a pure numeric pipeline:
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
input_ids -> Gather(input_matrix) -> ReduceMean(axis=0) -> MatMul(output_matrix) -> Softmax -> probs
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
`openlid_v2_hash.py` is the reference implementation of steps 1 to 3, ported
|
| 53 |
+
from the same recipe used for [`TigreGotico/glotlid-onnx`](https://huggingface.co/TigreGotico/glotlid-onnx).
|
| 54 |
+
Two details are easy to get wrong:
|
| 55 |
+
|
| 56 |
+
* fastText casts each byte to a **signed** `int8_t` before the FNV-1a XOR, so
|
| 57 |
+
bytes >= 0x80 are sign-extended. Without this, every non-ASCII n-gram lands
|
| 58 |
+
in the wrong bucket.
|
| 59 |
+
* Each line ends with the `</s>` end-of-sentence token, and that token
|
| 60 |
+
contributes its own vocabulary row.
|
| 61 |
+
|
| 62 |
+
This model uses plain softmax loss (not hierarchical softmax), so it does
|
| 63 |
+
not need the Huffman-tree combination step that `TigreGotico/lid176-onnx`
|
| 64 |
+
(fastText's classic `lid.176.bin`, trained with `loss=hs`) requires.
|
| 65 |
+
|
| 66 |
+
## Files
|
| 67 |
+
|
| 68 |
+
| File | Size | Purpose |
|
| 69 |
+
|---|---|---|
|
| 70 |
+
| `openlid-v2.onnx` | 1.13 GB | fp32 graph |
|
| 71 |
+
| `openlid-v2.int8.onnx` | 290 MB | dynamic int8 graph |
|
| 72 |
+
| `labels.json` | ~4 KB | 200 labels, in output order |
|
| 73 |
+
| `vocab.txt` | 1.8 MB | 185286 vocabulary words, in id order |
|
| 74 |
+
| `config.json` | 122 B | `dim`, `minn`, `maxn`, `bucket`, `nwords`, `nlabels`, `loss` |
|
| 75 |
+
| `openlid_v2_hash.py` | 5.2 KB | reference feature extractor |
|
| 76 |
+
|
| 77 |
+
## Model arguments
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
dim=256 minn=2 maxn=5 bucket=1000000 wordNgrams=1 loss=softmax
|
| 81 |
+
nwords=185286 nlabels=200
|
| 82 |
+
input_matrix=(1185286, 256) # nwords + bucket
|
| 83 |
+
output_matrix=(200, 256)
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## Tensors
|
| 87 |
+
|
| 88 |
+
| Name | Direction | Type | Shape |
|
| 89 |
+
|---|---|---|---|
|
| 90 |
+
| `input_ids` | input | int64 | `[num_features]` |
|
| 91 |
+
| `probs` | output | float32 | `[200]` |
|
| 92 |
+
|
| 93 |
+
`probs` is indexed by the order of `labels.json`.
|
| 94 |
+
|
| 95 |
+
## Usage
|
| 96 |
+
|
| 97 |
+
```python
|
| 98 |
+
import json
|
| 99 |
+
import numpy as np
|
| 100 |
+
import onnxruntime as ort
|
| 101 |
+
from huggingface_hub import snapshot_download
|
| 102 |
+
|
| 103 |
+
from openlid_v2_hash import FastTextFeaturizer
|
| 104 |
+
|
| 105 |
+
d = snapshot_download("TigreGotico/openlid-v2-onnx")
|
| 106 |
+
feat = FastTextFeaturizer.from_files(f"{d}/vocab.txt", f"{d}/config.json")
|
| 107 |
+
labels = json.load(open(f"{d}/labels.json", encoding="utf-8"))
|
| 108 |
+
sess = ort.InferenceSession(f"{d}/openlid-v2.onnx", providers=["CPUExecutionProvider"])
|
| 109 |
+
|
| 110 |
+
def detect(text, k=5):
|
| 111 |
+
probs = sess.run(None, {"input_ids": feat(text)})[0]
|
| 112 |
+
top = np.argsort(-probs)[:k]
|
| 113 |
+
return [(labels[i], float(probs[i])) for i in top]
|
| 114 |
+
|
| 115 |
+
print(detect("O tempo está moi bo hoxe en Santiago"))
|
| 116 |
+
# [('__label__glg_Latn', 0.9...), ...]
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
## Parity with fastText
|
| 120 |
+
|
| 121 |
+
59 short samples spanning 59 languages, chosen for script and resource
|
| 122 |
+
diversity: Portuguese, Galician, Catalan, Basque, Spanish, English, French,
|
| 123 |
+
German, Italian, Dutch, Arabic, Chinese, Japanese, Russian, Hindi, Greek,
|
| 124 |
+
Ukrainian, Swahili, Turkish, Polish, Czech, Finnish, Hungarian, Romanian,
|
| 125 |
+
Swedish, Hebrew, Persian, Thai, Vietnamese, Indonesian, Tagalog, Amharic,
|
| 126 |
+
Hausa, Yoruba, Igbo, Zulu, Somali, Bengali, Tamil, Telugu, Malayalam, Nepali,
|
| 127 |
+
Georgian, Armenian, Icelandic, Welsh, Irish, Maltese, Esperanto, Quechua,
|
| 128 |
+
Guarani, Haitian Creole, Malagasy, Kinyarwanda, Mongolian, Khmer, Lao,
|
| 129 |
+
Burmese, Sinhala.
|
| 130 |
+
|
| 131 |
+
Each sample was compared against fastText's own `f.predict(text + "\n", 1,
|
| 132 |
+
0.0, "strict")` (the pybind entry point - `fasttext-wheel`'s Python
|
| 133 |
+
`.predict()` wrapper is broken under numpy>=2), on raw uncleaned text (see
|
| 134 |
+
the note above about the upstream normalizer).
|
| 135 |
+
|
| 136 |
+
| Model | Top-1 agreement |
|
| 137 |
+
|---|---|
|
| 138 |
+
| `openlid-v2.onnx` (fp32) | **100.00 %** (59/59) |
|
| 139 |
+
| `openlid-v2.int8.onnx` | **100.00 %** (59/59) |
|
| 140 |
+
|
| 141 |
+
## Citation
|
| 142 |
+
|
| 143 |
+
```
|
| 144 |
+
@inproceedings{burchell-etal-2023-open,
|
| 145 |
+
title = "An Open Dataset and Model for Language Identification",
|
| 146 |
+
author = "Burchell, Laurie and Birch, Alexandra and Bogoychev, Nikolay and Heafield, Kenneth",
|
| 147 |
+
booktitle = "Proceedings of the 61st Annual Meeting of the Association for Computational Linguistics (Volume 2: Short Papers)",
|
| 148 |
+
year = "2023",
|
| 149 |
+
publisher = "Association for Computational Linguistics",
|
| 150 |
+
url = "https://aclanthology.org/2023.acl-short.75",
|
| 151 |
+
}
|
| 152 |
+
```
|
config.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"dim": 256,
|
| 3 |
+
"minn": 2,
|
| 4 |
+
"maxn": 5,
|
| 5 |
+
"bucket": 1000000,
|
| 6 |
+
"nwords": 185286,
|
| 7 |
+
"nlabels": 200,
|
| 8 |
+
"loss": "softmax"
|
| 9 |
+
}
|
labels.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
["__label__eng_Latn", "__label__arb_Arab", "__label__rus_Cyrl", "__label__por_Latn", "__label__pol_Latn", "__label__ekk_Latn", "__label__ell_Grek", "__label__slk_Latn", "__label__pes_Arab", "__label__slv_Latn", "__label__nld_Latn", "__label__hun_Latn", "__label__lvs_Latn", "__label__dan_Latn", "__label__swe_Latn", "__label__lit_Latn", "__label__fin_Latn", "__label__cmn_Hant", "__label__mlt_Latn", "__label__nob_Latn", "__label__ind_Latn", "__label__uzn_Latn", "__label__fil_Latn", "__label__ukr_Cyrl", "__label__hin_Deva", "__label__cmn_Hans", "__label__afr_Latn", "__label__mar_Deva", "__label__ceb_Latn", "__label__heb_Hebr", "__label__ilo_Latn", "__label__zul_Latn", "__label__xho_Latn", "__label__jpn_Jpan", "__label__vie_Latn", "__label__guj_Gujr", "__label__amh_Ethi", "__label__hrv_Latn", "__label__nya_Latn", "__label__tsn_Latn", "__label__sna_Latn", "__label__tso_Latn", "__label__tha_Thai", "__label__spa_Latn", "__label__deu_Latn", "__label__eus_Latn", "__label__tur_Latn", "__label__bul_Cyrl", "__label__fra_Latn", "__label__ewe_Latn", "__label__nso_Latn", "__label__tam_Taml", "__label__mya_Mymr", "__label__twi_Latn", "__label__lin_Latn", "__label__yor_Latn", "__label__ben_Beng", "__label__urd_Arab", "__label__ibo_Latn", "__label__ita_Latn", "__label__tir_Ethi", "__label__azj_Latn", "__label__tpi_Latn", "__label__run_Latn", "__label__kin_Latn", "__label__ron_Latn", "__label__ces_Latn", "__label__sin_Sinh", "__label__kat_Geor", "__label__zsm_Latn", "__label__pap_Latn", "__label__mkd_Cyrl", "__label__bem_Latn", "__label__mal_Mlym", "__label__kir_Cyrl", "__label__smo_Latn", "__label__hye_Armn", "__label__kan_Knda", "__label__fij_Latn", "__label__pan_Guru", "__label__kor_Hang", "__label__als_Latn", "__label__hau_Latn", "__label__epo_Latn", "__label__gaz_Latn", "__label__srp_Cyrl", "__label__hat_Latn", "__label__lua_Latn", "__label__pag_Latn", "__label__war_Latn", "__label__pbt_Arab", "__label__tel_Telu", "__label__tat_Cyrl", "__label__sag_Latn", "__label__lug_Latn", "__label__oci_Latn", "__label__tum_Latn", "__label__npi_Deva", "__label__swh_Latn", "__label__umb_Latn", "__label__ktu_Latn", "__label__bos_Latn", "__label__gle_Latn", "__label__mos_Latn", "__label__lus_Latn", "__label__som_Latn", "__label__khk_Cyrl", "__label__tuk_Latn", "__label__quy_Latn", "__label__ayr_Latn", "__label__luo_Latn", "__label__tgk_Cyrl", "__label__asm_Beng", "__label__cat_Latn", "__label__ssw_Latn", "__label__nno_Latn", "__label__apc_Arab", "__label__cym_Latn", "__label__kik_Latn", "__label__ory_Orya", "__label__kmb_Latn", "__label__bel_Cyrl", "__label__uig_Arab", "__label__gug_Latn", "__label__khm_Khmr", "__label__arz_Arab", "__label__ast_Latn", "__label__jav_Latn", "__label__bak_Cyrl", "__label__yue_Hant", "__label__fur_Latn", "__label__bho_Deva", "__label__hne_Deva", "__label__kbp_Latn", "__label__kam_Latn", "__label__kab_Latn", "__label__kaz_Cyrl", "__label__gla_Latn", "__label__snd_Arab", "__label__mri_Latn", "__label__lim_Latn", "__label__mni_Beng", "__label__plt_Latn", "__label__sun_Latn", "__label__san_Deva", "__label__srd_Latn", "__label__isl_Latn", "__label__vec_Latn", "__label__glg_Latn", "__label__scn_Latn", "__label__fao_Latn", "__label__ltz_Latn", "__label__cjk_Latn", "__label__lmo_Latn", "__label__mai_Deva", "__label__szl_Latn", "__label__min_Latn", "__label__prs_Arab", "__label__fon_Latn", "__label__sat_Olck", "__label__lij_Latn", "__label__ary_Arab", "__label__wol_Latn", "__label__dik_Latn", "__label__ars_Arab", "__label__ckb_Arab", "__label__lao_Laoo", "__label__shn_Mymr", "__label__aeb_Arab", "__label__bjn_Latn", "__label__crh_Latn", "__label__dyu_Latn", "__label__ace_Latn", "__label__ban_Latn", "__label__kmr_Latn", "__label__ltg_Latn", "__label__fuv_Latn", "__label__kac_Latn", "__label__azb_Arab", "__label__taq_Latn", "__label__zgh_Tfng", "__label__bam_Latn", "__label__awa_Deva", "__label__bug_Latn", "__label__dzo_Tibt", "__label__kas_Deva", "__label__ace_Arab", "__label__knc_Arab", "__label__mag_Deva", "__label__nus_Latn", "__label__taq_Tfng", "__label__bjn_Arab", "__label__knc_Latn", "__label__kas_Arab", "__label__kea_Latn", "__label__acm_Arab", "__label__bod_Tibt", "__label__sot_Latn", "__label__acq_Arab", "__label__ydd_Hebr"]
|
openlid-v2.int8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f85944feb2f699a5c4fe2ae69501c0410317e88c7b56dc50a7e4b3dcb1fdece9
|
| 3 |
+
size 303485738
|
openlid-v2.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5b6c8380d951e83a5779f138b705ff278c4f2020baacdab63b2604b2bf4f42bc
|
| 3 |
+
size 1213938021
|
openlid_v2_hash.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""fastText feature hashing, vendored from TigreGotico/glotlid-onnx's
|
| 2 |
+
``glotlid_hash.py`` (https://huggingface.co/TigreGotico/glotlid-onnx).
|
| 3 |
+
|
| 4 |
+
Kept logically identical on purpose, including the sign-extension of bytes
|
| 5 |
+
>= 0x80 before the FNV-1a XOR: fastText's C++ ``Dictionary::hash`` treats
|
| 6 |
+
each byte as a signed ``int8_t`` before XOR-ing it into the hash, so
|
| 7 |
+
non-ASCII n-grams must be hashed the same way here or every non-Latin-script
|
| 8 |
+
bucket lookup breaks.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
from typing import Dict, List, Sequence
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
|
| 18 |
+
EOS = "</s>"
|
| 19 |
+
BOW = "<"
|
| 20 |
+
EOW = ">"
|
| 21 |
+
SEPARATORS = " \t\n\v\f\r"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def fnv1a(s: str) -> int:
|
| 25 |
+
h = 2166136261
|
| 26 |
+
for b in s.encode("utf-8"):
|
| 27 |
+
if b >= 0x80:
|
| 28 |
+
b |= 0xFFFFFF00
|
| 29 |
+
h = ((h ^ b) * 16777619) & 0xFFFFFFFF
|
| 30 |
+
return h
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def tokenize(text: str) -> List[str]:
|
| 34 |
+
for sep in SEPARATORS[1:]:
|
| 35 |
+
text = text.replace(sep, " ")
|
| 36 |
+
return [t for t in text.split(" ") if t] + [EOS]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def compute_subwords(word: str, minn: int, maxn: int, bucket: int,
|
| 40 |
+
nwords: int) -> List[int]:
|
| 41 |
+
chars = list(word)
|
| 42 |
+
out: List[int] = []
|
| 43 |
+
n = len(chars)
|
| 44 |
+
for i in range(n):
|
| 45 |
+
for j in range(i + minn, min(n, i + maxn) + 1):
|
| 46 |
+
ngram = "".join(chars[i:j])
|
| 47 |
+
out.append(nwords + fnv1a(ngram) % bucket)
|
| 48 |
+
return out
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class FastTextFeaturizer:
|
| 52 |
+
"""Maps raw text to the fastText feature ids the ONNX graph expects."""
|
| 53 |
+
|
| 54 |
+
def __init__(self, words: Sequence[str], nwords: int, minn: int = 2,
|
| 55 |
+
maxn: int = 5, bucket: int = 1_000_000):
|
| 56 |
+
self.words: List[str] = list(words)
|
| 57 |
+
self.nwords = nwords
|
| 58 |
+
self.minn = minn
|
| 59 |
+
self.maxn = maxn
|
| 60 |
+
self.bucket = bucket
|
| 61 |
+
self.word2id: Dict[str, int] = {w: i for i, w in enumerate(self.words)}
|
| 62 |
+
self._cache: Dict[int, List[int]] = {}
|
| 63 |
+
|
| 64 |
+
@classmethod
|
| 65 |
+
def from_files(cls, vocab_path: str, meta_path: str) -> "FastTextFeaturizer":
|
| 66 |
+
with open(meta_path, encoding="utf-8") as fh:
|
| 67 |
+
meta = json.load(fh)
|
| 68 |
+
with open(vocab_path, encoding="utf-8") as fh:
|
| 69 |
+
words = fh.read().split("\n")
|
| 70 |
+
if words and words[-1] == "":
|
| 71 |
+
words.pop()
|
| 72 |
+
return cls(words, meta["nwords"], meta["minn"], meta["maxn"],
|
| 73 |
+
meta["bucket"])
|
| 74 |
+
|
| 75 |
+
def subwords_of_known(self, wid: int) -> List[int]:
|
| 76 |
+
cached = self._cache.get(wid)
|
| 77 |
+
if cached is None:
|
| 78 |
+
word = self.words[wid]
|
| 79 |
+
if word == EOS:
|
| 80 |
+
cached = [wid]
|
| 81 |
+
else:
|
| 82 |
+
cached = [wid] + compute_subwords(
|
| 83 |
+
BOW + word + EOW, self.minn, self.maxn, self.bucket,
|
| 84 |
+
self.nwords)
|
| 85 |
+
self._cache[wid] = cached
|
| 86 |
+
return cached
|
| 87 |
+
|
| 88 |
+
def add_subwords(self, line: List[int], token: str) -> None:
|
| 89 |
+
wid = self.word2id.get(token, -1)
|
| 90 |
+
if wid < 0:
|
| 91 |
+
if token != EOS:
|
| 92 |
+
line.extend(compute_subwords(BOW + token + EOW, self.minn,
|
| 93 |
+
self.maxn, self.bucket,
|
| 94 |
+
self.nwords))
|
| 95 |
+
elif self.maxn <= 0:
|
| 96 |
+
line.append(wid)
|
| 97 |
+
else:
|
| 98 |
+
line.extend(self.subwords_of_known(wid))
|
| 99 |
+
|
| 100 |
+
def __call__(self, text: str) -> np.ndarray:
|
| 101 |
+
line: List[int] = []
|
| 102 |
+
for token in tokenize(text):
|
| 103 |
+
self.add_subwords(line, token)
|
| 104 |
+
return np.asarray(line, dtype=np.int64)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class HSCombiner:
|
| 108 |
+
"""Combines the ONNX graph's per-Huffman-node ``node_probs`` into
|
| 109 |
+
per-label probabilities for hierarchical-softmax fastText models
|
| 110 |
+
(e.g. fastText's own ``lid.176.bin``).
|
| 111 |
+
|
| 112 |
+
fastText's hierarchical softmax does not reduce to a flat softmax over
|
| 113 |
+
the output matrix: each row of the output matrix is a binary classifier
|
| 114 |
+
for one internal node of a Huffman tree built over the label
|
| 115 |
+
frequencies, and a label's probability is the product of the sigmoid
|
| 116 |
+
(or 1-sigmoid) values along the root-to-leaf path. Tree construction
|
| 117 |
+
(``hs_tree.build_tree``) is deterministic given the label counts, so it
|
| 118 |
+
is precomputed once at export time into ``hs_tree.json``; this class
|
| 119 |
+
just walks the paths, which is cheap pure-Python work analogous to the
|
| 120 |
+
string hashing above - there is no portable ONNX op for it.
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
def __init__(self, paths: List[List[int]], codes: List[List[bool]]):
|
| 124 |
+
self.paths = paths
|
| 125 |
+
self.codes = codes
|
| 126 |
+
|
| 127 |
+
@classmethod
|
| 128 |
+
def from_file(cls, path: str) -> "HSCombiner":
|
| 129 |
+
with open(path, encoding="utf-8") as fh:
|
| 130 |
+
data = json.load(fh)
|
| 131 |
+
return cls(data["paths"], data["codes"])
|
| 132 |
+
|
| 133 |
+
def __call__(self, node_probs: np.ndarray) -> np.ndarray:
|
| 134 |
+
"""node_probs: sigmoid(dot(hidden, node)) for every Huffman node.
|
| 135 |
+
Returns: probability per label, same order as labels.json."""
|
| 136 |
+
log_f = np.log(np.clip(node_probs, 1e-12, 1.0))
|
| 137 |
+
log_1mf = np.log(np.clip(1.0 - node_probs, 1e-12, 1.0))
|
| 138 |
+
out = np.empty(len(self.paths), dtype=np.float64)
|
| 139 |
+
for i, (path, code) in enumerate(zip(self.paths, self.codes)):
|
| 140 |
+
s = 0.0
|
| 141 |
+
for node, bit in zip(path, code):
|
| 142 |
+
s += log_f[node] if bit else log_1mf[node]
|
| 143 |
+
out[i] = s
|
| 144 |
+
return np.exp(out)
|
vocab.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|