Jarbas commited on
Commit
91d5f18
·
verified ·
1 Parent(s): 926ef4e

Upload folder using huggingface_hub

Browse files
Files changed (7) hide show
  1. README.md +152 -0
  2. config.json +9 -0
  3. labels.json +1 -0
  4. openlid-v2.int8.onnx +3 -0
  5. openlid-v2.onnx +3 -0
  6. openlid_v2_hash.py +144 -0
  7. 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