File size: 7,344 Bytes
cd857db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""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)