Spaces:
Running on Zero
Running on Zero
Download prepare.py from Bayernator/HORST: direct link, hf CLI and curl.
- Browser
- Download file 8.61 kB
-
https://huggingface.co/spaces/Bayernator/HORST/resolve/main/prepare.py
- Command line
-
hf download hf://spaces/Bayernator/HORST/prepare.py
-
curl -L -o prepare.py https://huggingface.co/spaces/Bayernator/HORST/resolve/main/prepare.py
8.61 kB
| """Daten vorbereiten: echte Fehlerpaare laden, saubere Sätze sammeln, Tokenizer trainieren. | |
| Kaggle-CPU-Session: python prepare.py --out /kaggle/working/data | |
| Lokaler Mini-Lauf: python prepare.py --out data --max-sents 20000 --vocab-size 8000 | |
| """ | |
| import argparse | |
| import io | |
| import json | |
| import os | |
| import re | |
| import socket | |
| import tarfile | |
| import time | |
| import urllib.request | |
| import zipfile | |
| from collections import Counter | |
| TOKEN_RE = re.compile(r"\d+(?:[.,]\d+)+|\w+(?:[-'’]\w+)*|[^\w\s]") | |
| SENT_SPLIT = re.compile(r"(?<=[.!?])\s+(?=[A-ZÄÖÜ„\"])") | |
| ALLOWED = re.compile(r"[\w\s.,;:!?\-–'’\"„“‚‘()%/&]+") | |
| BOYD_URL = "https://github.com/adrianeboyd/boyd-wnut2018/releases/download/wnut2018/data.tar.gz" | |
| OWN_URL = "https://huggingface.co/datasets/Bayernator/german-spelling-data" | |
| BENCH_URL = "https://huggingface.co/datasets/Bayernator/german-ortho/resolve/main/rechtschreibung_hard_benchmark.zip" | |
| ABBREV = set("bzw usw ca vgl z B d h u a o v n Chr Nr Dr Prof St etc geb gest sog evtl ggf inkl Jh Mio Mrd bspw Hrsg Bd S f ff Abb Tab".split()) | |
| REAL_FILES = ["fm-train.src", "fm-train.trg", "fm-dev.src", "fm-dev.trg", "fm-dev.m2", | |
| "fm-test.src", "fm-test.trg", "fm-test.m2", "wiki.1M.src", "wiki.1M.trg"] | |
| _getaddrinfo = socket.getaddrinfo | |
| socket.getaddrinfo = lambda *a, **k: sorted(_getaddrinfo(*a, **k), key=lambda x: x[0] != socket.AF_INET) # IPv4 zuerst: kaputtes IPv6 lässt urllib sonst ewig hängen | |
| def get(url): | |
| return urllib.request.urlopen(url, timeout=120).read() | |
| def tokenize(text): | |
| # ponytail: Regex-Tokenizer, Abkürzungen ("z. B.", "bzw.") werden gesplittet; reicht, weil Quelle und Ziel gleich tokenisiert sind | |
| return " ".join(TOKEN_RE.findall(text)) | |
| def detokenize(text): | |
| text = re.sub(r" ([.,;:!?)\]}“%])", r"\1", text) | |
| text = re.sub(r"([(\[{„]) ", r"\1", text) | |
| text = re.sub(r"(\w) - (und|oder|bzw)\b", r"\1- \2", text) # Ergänzungsstrich: "Manga- und Animeserien" | |
| out, open_q = [], {'"': False, "'": False} | |
| for tok in text.split(): | |
| if tok in open_q: # gerade Anführungszeichen paarweise: öffnend klebt am nächsten Wort, schließend am vorherigen | |
| open_q[tok] = not open_q[tok] | |
| if open_q[tok]: | |
| out.append(tok + "\0") # Marker: kein Leerzeichen danach | |
| elif out: | |
| out[-1] += tok | |
| else: | |
| out.append(tok) | |
| else: | |
| out.append(tok) | |
| return " ".join(out).replace("\0 ", "").replace("\0", "") | |
| def split_sentences(text): | |
| for line in text.split("\n"): | |
| yield from SENT_SPLIT.split(line.strip()) | |
| def good(s): | |
| return (20 <= len(s) <= 300 and s[0].isupper() and s[-1] in ".!?" and ALLOWED.fullmatch(s) | |
| and sum(c.isdigit() for c in s) < 0.1 * len(s)) | |
| def fetch_real(out): | |
| os.makedirs(f"{out}/real", exist_ok=True) | |
| if all(os.path.exists(f"{out}/real/{f}") for f in REAL_FILES): | |
| return | |
| print("Lade Falko-MERLIN + Wikipedia-Edits (Boyd 2018, CC BY-SA 4.0) ...") | |
| blob = get(BOYD_URL) | |
| with tarfile.open(fileobj=io.BytesIO(blob)) as tar: | |
| for f in REAL_FILES: | |
| with open(f"{out}/real/{f}", "wb") as fh: | |
| fh.write(tar.extractfile(f"data/{f}").read()) | |
| def _pair_split(text): | |
| """Satztrennung für Absatzpaare, die auch klein geschriebene Satzanfänge (Fehler!) trennt.""" | |
| out = [] | |
| for piece in re.split(r"(?<=[.!?])\s+", text.strip()): | |
| last = re.split(r"[\s(]", out[-1])[-1][:-1] if out else "" | |
| if out and (last in ABBREV or last.isdigit() or len(last) <= 1): # "v. Chr.", "3. Februar" | |
| out[-1] += " " + piece | |
| else: | |
| out.append(piece) | |
| return out | |
| def sentence_pairs(inp, tgt): | |
| """Absatzpaar -> Satzpaare; der letzte Satz fällt weg, wenn der Absatz mitten im Text abgeschnitten ist.""" | |
| ss, ts = _pair_split(inp), _pair_split(tgt) | |
| if len(ss) != len(ts): | |
| return [] | |
| if ts[-1][-1:] not in ".!?\"“)": | |
| ss, ts = ss[:-1], ts[:-1] | |
| return [(tokenize(s), tokenize(t)) for s, t in zip(ss, ts) if len(t.split()) <= 80] | |
| def fetch_own(out): | |
| """Eigene HF-Datasets: Hard-Benchmark (nur Evaluation) + Trainingspaare ohne Benchmark-Überschneidung.""" | |
| if os.path.exists(f"{out}/real/own.src"): | |
| return | |
| print("Lade Bayernator/german-spelling-data + Hard-Benchmark ...") | |
| z = zipfile.ZipFile(io.BytesIO(get(BENCH_URL))) | |
| bench = z.read("rechtschreibung_hard_benchmark/benchmark_hard_5000.jsonl").decode() | |
| with open(f"{out}/real/bench.jsonl", "w", encoding="utf-8") as fh: | |
| fh.write(bench) | |
| rows = [json.loads(l) for l in bench.splitlines() if l.strip()] | |
| banned = {r["input"] for r in rows} | {r["target"] for r in rows} | |
| files = json.loads(get(OWN_URL.replace("/datasets/", "/api/datasets/") + "/tree/main/data")) | |
| kept = dropped = 0 | |
| with open(f"{out}/real/own.src", "w", encoding="utf-8") as fs, open(f"{out}/real/own.trg", "w", encoding="utf-8") as ft: | |
| for f in files: | |
| for line in get(f"{OWN_URL}/resolve/main/{f['path']}").decode().splitlines(): | |
| r = json.loads(line) if line.strip() else None | |
| if not r or r["input"] in banned or r["target"] in banned: | |
| dropped += bool(r) | |
| continue | |
| for s, t in sentence_pairs(r["input"], r["target"]): | |
| fs.write(s + "\n") | |
| ft.write(t + "\n") | |
| kept += 1 | |
| print(f"{kept:,} eigene Satzpaare, {dropped:,} Absätze wegen Benchmark-Überschneidung entfernt") | |
| def collect_clean(out, max_sents, fineweb_share, max_hours=8.0): | |
| import numpy as np | |
| from datasets import load_dataset | |
| sources = [("wikimedia/wikipedia", "20231101.de", int(max_sents * (1 - fineweb_share))), | |
| ("epfml/FineWeb2-HQ", "deu_Latn", max_sents)] # FineWeb füllt bis max_sents auf | |
| seen, offsets, words, pos = set(), [], Counter(), 0 | |
| deadline = time.time() + max_hours * 3600 # Kaggle-Session: 12 h, Tokenizer-Training braucht danach noch Zeit | |
| with open(f"{out}/clean.txt", "wb") as fh: | |
| for name, config, quota in sources: | |
| for doc in load_dataset(name, config, split="train", streaming=True): | |
| for s in split_sentences(doc["text"]): | |
| if not good(s): | |
| continue | |
| t = tokenize(s) | |
| n = t.count(" ") + 1 | |
| if not 5 <= n <= 80 or hash(t) in seen: | |
| continue | |
| seen.add(hash(t)) | |
| if len(offsets) < 5_000_000: # Wortschatz aus den ersten 5 Mio. Sätzen reicht | |
| words.update(w for w in t.split() if w.isalpha()) | |
| b = (t + "\n").encode() | |
| offsets.append(pos) | |
| fh.write(b) | |
| pos += len(b) | |
| if len(offsets) % 1_000_000 == 0: | |
| print(f"{len(offsets):,} Sätze") | |
| if len(offsets) >= quota or time.time() > deadline: | |
| break | |
| offsets.append(pos) # Ende des letzten Satzes | |
| np.save(f"{out}/clean.idx.npy", np.array(offsets, dtype=np.uint64)) | |
| min_count = 5 if len(offsets) > 1_000_000 else 2 | |
| with open(f"{out}/vocab.txt", "w") as fh: | |
| fh.writelines(f"{w}\n" for w, c in words.most_common(100_000) if c >= min_count) | |
| print(f"{len(offsets) - 1:,} saubere Sätze gespeichert") | |
| def train_tokenizer(out, vocab_size): | |
| import sentencepiece as spm | |
| spm.SentencePieceTrainer.train( | |
| input=f"{out}/clean.txt", model_prefix=f"{out}/spm", vocab_size=vocab_size, | |
| model_type="unigram", character_coverage=0.9995, byte_fallback=True, split_digits=True, | |
| normalization_rule_name="identity", input_sentence_size=5_000_000, shuffle_input_sentence=True, | |
| pad_id=0, unk_id=1, bos_id=2, eos_id=3, num_threads=os.cpu_count(), minloglevel=2) | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--out", default="data") | |
| ap.add_argument("--max-sents", type=int, default=30_000_000) | |
| ap.add_argument("--fineweb-share", type=float, default=0.5) | |
| ap.add_argument("--vocab-size", type=int, default=32000) | |
| ap.add_argument("--max-hours", type=float, default=8.0, help="Zeitbudget fürs Sammeln sauberer Sätze") | |
| a = ap.parse_args() | |
| os.makedirs(a.out, exist_ok=True) | |
| fetch_real(a.out) | |
| fetch_own(a.out) | |
| collect_clean(a.out, a.max_sents, a.fineweb_share, a.max_hours) | |
| train_tokenizer(a.out, a.vocab_size) | |
| print("Fertig:", a.out) | |