Text Classification
Transformers
ONNX
Safetensors
Transformers.js
modernbert
language-identification
language-detection
lid
multilingual
mmbert
text-embeddings-inference
Instructions to use Horizon-Labs/language-detection-small with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Horizon-Labs/language-detection-small with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="Horizon-Labs/language-detection-small")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("Horizon-Labs/language-detection-small") model = AutoModelForSequenceClassification.from_pretrained("Horizon-Labs/language-detection-small", device_map="auto") - Transformers.js
How to use Horizon-Labs/language-detection-small with Transformers.js:
// npm i @huggingface/transformers import { pipeline } from '@huggingface/transformers'; // Allocate pipeline const pipe = await pipeline('text-classification', 'Horizon-Labs/language-detection-small'); - Notebooks
- Google Colab
- Kaggle
Download code/lid/prep_lid.py from Horizon-Labs/language-detection-small: direct link, hf CLI and curl.
- Browser
- Download file 7.34 kB
-
https://huggingface.co/Horizon-Labs/language-detection-small/resolve/main/code/lid/prep_lid.py
- Command line
-
hf download hf://Horizon-Labs/language-detection-small/code/lid/prep_lid.py
-
curl -L -o prep_lid.py https://huggingface.co/Horizon-Labs/language-detection-small/resolve/main/code/lid/prep_lid.py
7.34 kB
| """Language identification data: FineWeb-2 / FineWeb (ODC-BY) training text for the FLORES-200 language set, and | |
| FLORES-200 (CC-BY-SA-4.0, evaluation only) dev/devtest evals. python lid/prep_lid.py OUT [PER_LANG] | |
| Labels are FLORES-200 codes (ISO 639-3 + script), except Chinese: FineWeb-2 has one cmn_Hani subset, so zho_Hans and | |
| zho_Hant are merged into one label zho_Hani. Languages without a FineWeb-2 subset are dropped (listed in stats.json). | |
| Training samples per language: 2/3 "long" (1-3 consecutive sentences, <= 300 chars) and 1/3 "short" (a random span | |
| of 15-60 characters on word boundaries), so the model works on short strings. Any sample containing a FLORES sentence | |
| (normalised exact match) is removed. | |
| OUT/train.parquet, val.parquet (text, label, kind), eval/flores_devtest.parquet (full sentences), eval/flores_short.parquet | |
| (first 20-40 characters of each devtest sentence, cut at a word boundary where possible), labels.json, stats.json | |
| """ | |
| import io, json, os, random, re, sys, tarfile, urllib.request | |
| from multiprocessing import Pool | |
| OUT = sys.argv[1] | |
| PER = int(sys.argv[2]) if len(sys.argv) > 2 else 6000 | |
| os.makedirs(f"{OUT}/eval", exist_ok=True) | |
| norm = lambda s: re.sub(r"\s+", " ", s.strip().lower()) | |
| SPLIT = re.compile(r"(?<=[.!?。!?।؟።])\s+|\n+") | |
| def flores(): | |
| data = urllib.request.urlopen("https://dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz", timeout=600).read() | |
| tf = tarfile.open(fileobj=io.BytesIO(data)) | |
| out = {"dev": {}, "devtest": {}} | |
| for m in tf.getmembers(): | |
| parts = [x for x in m.name.split("/") if x not in ("", ".")] # members look like ./flores200_dataset/devtest/eng_Latn.devtest | |
| if len(parts) >= 3 and parts[-2] in out and m.isfile() and parts[-1].endswith("." + parts[-2]): | |
| code = parts[-1].rsplit(".", 1)[0] | |
| out[parts[-2]][code] = tf.extractfile(m).read().decode("utf-8").splitlines() | |
| return out | |
| # FLORES code -> FineWeb-2 subset where the names differ (the label stays the FLORES code). Not aliased on purpose: | |
| # prs_Arab (Dari, no subset), ajp_Arab (South Levantine; only North Levantine apc_Arab exists), acq_Arab. | |
| ALIAS = {"est_Latn": "ekk_Latn", "pes_Arab": "fas_Arab", "tgl_Latn": "fil_Latn", "yue_Hant": "yue_Hani", "grn_Latn": "gug_Latn", | |
| "aka_Latn": "twi_Latn", "kon_Latn": "kng_Latn", "zho_Hans": "cmn_Hani", "zho_Hant": "cmn_Hani"} | |
| # lid/fw_files.json: the first parquet files of every FineWeb-2 subset, listed from the (authenticated) control plane, so | |
| # jobs make no Hub API calls (the cluster's shared IP gets rate-limited for unauthenticated API use). | |
| FW = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "fw_files.json"))) | |
| def fw_config(code, configs): | |
| if code == "eng_Latn": | |
| return ("HuggingFaceFW/fineweb", FW["fineweb_eng"]) | |
| c = ALIAS.get(code, code) | |
| return ("HuggingFaceFW/fineweb-2", FW["fineweb2"][c]) if c in configs else None | |
| def label_of(code): | |
| return "zho_Hani" if code in ("zho_Hans", "zho_Hant") else code | |
| def collect(args): | |
| label, repo, cfg, bad = args | |
| import time | |
| import pyarrow.parquet as pq | |
| from huggingface_hub import HfFileSystem | |
| rng = random.Random(sum(map(ord, label))) | |
| fs = HfFileSystem() | |
| def retry(fn): | |
| for k in range(6): | |
| try: | |
| return fn() | |
| except Exception: | |
| if k == 5: | |
| raise | |
| time.sleep(20 * (k + 1)) | |
| def texts(): # read the text column row group by row group (no datasets schema casting; cheap range reads) | |
| for f in cfg: | |
| pf = retry(lambda: pq.ParquetFile(fs.open(f"datasets/{repo}/{f}"))) | |
| for g in range(pf.num_row_groups): | |
| for t in retry(lambda: pf.read_row_group(g, columns=["text"])).column("text").to_pylist(): | |
| yield t | |
| longs, shorts, n_docs = [], [], 0 | |
| try: | |
| it = texts() | |
| except Exception as e: | |
| return label, [], f"load failed: {e}" | |
| for text in it: | |
| n_docs += 1 | |
| sents = [s.strip() for s in SPLIT.split(text or "") if len(s.strip()) >= 15] | |
| if not sents: | |
| continue | |
| i = rng.randrange(len(sents)); k = rng.choice([1, 1, 2, 3]) | |
| t = " ".join(sents[i:i + k])[:300] | |
| if norm(t) not in bad and not any(norm(s) in bad for s in sents[i:i + k]): | |
| longs.append(t) | |
| s = sents[rng.randrange(len(sents))] | |
| L = rng.randint(15, 60) | |
| if len(s) > L: | |
| st = rng.randrange(0, len(s) - L + 1) | |
| sp = s[st:st + L] | |
| if " " in sp.strip(): # cut to word boundaries for space-separated scripts | |
| sp = sp[sp.find(" ") + 1:] if st > 0 else sp | |
| sp = sp[:sp.rfind(" ")] if sp.rfind(" ") > 5 else sp | |
| s = sp | |
| if len(s.strip()) >= 5 and norm(s) not in bad: | |
| shorts.append(s.strip()) | |
| if len(longs) >= PER * 2 // 3 and len(shorts) >= PER // 3 or n_docs >= PER * 3: | |
| break | |
| rows = [(t, label, "long") for t in longs[: PER * 2 // 3]] + [(t, label, "short") for t in shorts[: PER // 3]] | |
| return label, rows, f"docs {n_docs}" | |
| if __name__ == "__main__": | |
| import pandas as pd | |
| fl = flores() | |
| codes = sorted(fl["devtest"]) | |
| bad = {norm(x) for sp in fl.values() for v in sp.values() for x in v} | |
| configs = set(FW["fineweb2"]) | |
| jobs, dropped = {}, [] | |
| for c in codes: | |
| fc = fw_config(c, configs) | |
| if fc is None: | |
| dropped.append(c) | |
| else: | |
| jobs[label_of(c)] = (label_of(c),) + fc + (bad,) | |
| print("FLORES languages", len(codes), "mapped labels", len(jobs), "dropped", dropped, flush=True) | |
| with Pool(int(os.environ.get("NPROC", 8))) as p: | |
| res = p.map(collect, list(jobs.values())) | |
| rows, stats = [], {} | |
| for label, r, info in res: | |
| stats[label] = dict(n=len(r), info=info); rows += r | |
| print(label, len(r), info, flush=True) | |
| labels = sorted(l for l, s in stats.items() if s["n"] >= 300) | |
| df = pd.DataFrame(rows, columns=["text", "label", "kind"]) | |
| df = df[df.label.isin(labels)].drop_duplicates(subset=["text"]).sample(frac=1.0, random_state=0).reset_index(drop=True) | |
| nv = int(len(df) * 0.02) | |
| df.iloc[nv:].to_parquet(f"{OUT}/train.parquet"); df.iloc[:nv].to_parquet(f"{OUT}/val.parquet") | |
| ev = [(s, label_of(c), c) for c in codes for s in fl["devtest"][c]] | |
| e = pd.DataFrame(ev, columns=["text", "label", "flores_code"]) | |
| e = e[e.label.isin(labels)] # languages we cannot train on (no FineWeb-2 data) are listed in stats.json instead | |
| e.to_parquet(f"{OUT}/eval/flores_devtest.parquet") | |
| rng = random.Random(1) | |
| def short(s): | |
| L = rng.randint(20, 40) | |
| if len(s) <= L: | |
| return s | |
| t = s[:L] | |
| return t[: t.rfind(" ")] if " " in t and t.rfind(" ") >= 10 else t | |
| e.assign(text=e.text.map(short)).to_parquet(f"{OUT}/eval/flores_short.parquet") | |
| json.dump(labels, open(f"{OUT}/labels.json", "w")) | |
| json.dump(dict(stats=stats, dropped_no_fineweb=dropped, dropped_too_few=[l for l, s in stats.items() if s["n"] < 300], | |
| n_train=len(df) - nv, n_val=nv), open(f"{OUT}/stats.json", "w"), indent=1) | |
| print("labels", len(labels), "train", len(df) - nv, "val", nv, "| eval sentences", len(e), flush=True) | |
| os._exit(0) | |