HORST / prepare.py
Bayernator's picture
Pixel-Design und Trainingsstufe 2 (KV-Cache-Dekodierer)
3345e65 verified
Raw History Blame Contribute Delete
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)