Träningsdata för språkmodeller: filtrering och dedup
Kunna bygga en filtrerad, deduplicerad textkorpus och mäta kvalitet.
Förkunskaper
Intuition
Rå webbtext är till största delen oanvändbar: boilerplate, navigeringsmenyer, autogenererad text, spam och dubbletter. Filtreringen är det som avgör modellens kvalitet — mer än arkitekturen.
Pipelinen, i ordning:
| Steg | Kastar typiskt |
|---|---|
| 1. Extrahera text ur HTML | taggar, skript, meny |
| 2. Språkidentifiering | fel språk |
| 3. Kvalitetsheuristik | för kort, för många symboler, ordlistor |
| 4. Deduplicering | 30–60 % av allt |
| 5. Toxicitets- och PII-filter | skadligt innehåll, personuppgifter |
| 6. Kontamineringskontroll | testdata som läckt in |
Steg 4 är det mest effektfulla. Lee m.fl. (2021) visade att deduplicering både minskar memorering och förbättrar modellen — färre upprepningar innebär att modellen inte lär sig samma text hundra gånger.
En tumregel som håller: tio gånger mindre men rensad data slår tio gånger mer orensad.
Formellt
Deduplicering på tre nivåer:
| Nivå | Hittar | Metod |
|---|---|---|
| Exakt | identiska dokument | hash av normaliserad text |
| Nära-dubbletter | samma text med små ändringar | MinHash + LSH |
| Substrängsnivå | återkommande stycken i olika dokument | suffixarray |
MinHash approximerar Jaccard-likheten mellan shingle-mängder, och LSH gör det möjligt att hitta kandidaterna utan att jämföra alla par — utan det är dedup på miljardskala omöjligt.
Kvalitetsheuristik (från Gopher- och C4-arbetena) — enkla regler som tar bort mycket skräp:
| Regel | Kastar |
|---|---|
| < 50 eller > 100 000 ord | fragment och dumpar |
| Medelordlängd utanför 3–10 tecken | kod, brus, tabeller |
| > 90 % av raderna börjar med punktlista | navigering |
| < 80 % av orden i en ordlista | brus och kodslask |
| Andel symboler > 10 % | skript, tabeller |
| Saknar interpunktion | listor och menyer |
Modellbaserad filtrering går ett steg längre: träna en klassificerare på «bra» text (Wikipedia, böcker) mot slumpmässig webbtext och behåll det som liknar den första. Effektivt, men det homogeniserar korpuset och riskerar att systematiskt sortera bort dialekter, minoritetsspråk och informella register.
Kontaminering är den kontroll som oftast saknas. Har testmängden råkat ingå i träningsdatan blir alla utvärderingar meningslösa. Kontrollera med n-gram-överlapp mellan korpus och varje benchmark du tänker använda, och rapportera resultatet.
Personuppgifter i korpuset kräver en uttalad hållning: detektera och maskera e-post, telefonnummer, personnummer och adresser. Det räcker inte för att göra korpuset GDPR-säkert, men det tar bort det mest uppenbara.
Mät korpuset, inte bara storleken:
| Mått | Berättar |
|---|---|
| Tokens totalt och unika | volym och variation |
| Andel efter varje filtersteg | var datan försvinner |
| Perplexitet under en referensmodell | skräp i svansarna |
| Domänfördelning | vad modellen kommer att kunna |
| Språkfördelning | särskilt viktigt för svenska |
| Kontaminationsgrad per benchmark | om utvärderingen håller |
Kod
import hashlib, re, unicodedata
from collections import Counter
def normalisera(text: str) -> str:
t = unicodedata.normalize("NFKC", text).lower()
return re.sub(r"\s+", " ", t).strip()
def exakt_dedup(dokument):
sedda, ut = set(), []
for d in dokument:
h = hashlib.sha256(normalisera(d).encode()).hexdigest()
if h not in sedda:
sedda.add(h); ut.append(d)
return ut
# MinHash + LSH för nära-dubbletter
def shingles(text, n=5):
ord_ = normalisera(text).split()
return {" ".join(ord_[i:i + n]) for i in range(max(len(ord_) - n + 1, 1))}
def minhash(s, antal=128, fro=0):
import random
rng = random.Random(fro)
frön = [rng.getrandbits(64) for _ in range(antal)]
sig = []
for f in frön:
sig.append(min(hash((sh, f)) & 0xFFFFFFFF for sh in s) if s else 0)
return tuple(sig)
def lsh_band(sig, band=16):
r = len(sig) // band
return [hash(sig[i * r:(i + 1) * r]) for i in range(band)]
def nara_dedup(dokument, band=16, troskel=0.8):
hinkar, behall = {}, []
for i, d in enumerate(dokument):
s = shingles(d)
sig = minhash(s)
kandidater = set()
nycklar = lsh_band(sig, band)
for nyckel in nycklar:
kandidater |= hinkar.get(nyckel, set())
dubblett = any(
len(s & shingles(dokument[j])) / max(len(s | shingles(dokument[j])), 1) > troskel
for j in kandidater)
if not dubblett:
behall.append(d)
for nyckel in nycklar:
hinkar.setdefault(nyckel, set()).add(i)
return behall
# Kvalitetsheuristik
SYMBOLER = set("#<>{}[]|\\@^~`")
def kvalitet_ok(text, ordlista=None):
ord_ = text.split()
if not 50 <= len(ord_) <= 100_000:
return False, "längd"
if not 3 <= sum(len(o) for o in ord_) / len(ord_) <= 10:
return False, "medelordlängd"
rader = [r for r in text.splitlines() if r.strip()]
if rader and sum(r.lstrip().startswith(("•", "-", "*")) for r in rader) / len(rader) > 0.9:
return False, "punktlista"
if sum(c in SYMBOLER for c in text) / max(len(text), 1) > 0.10:
return False, "symboler"
if not re.search(r"[.!?]", text):
return False, "saknar interpunktion"
if ordlista and sum(o.strip(".,!?").lower() in ordlista for o in ord_) / len(ord_) < 0.80:
return False, "ordlista"
return True, "ok"
# Kontaminationskontroll mot benchmarks
def kontaminering(korpus_ngram: set, benchmark_texter, n=13):
traffar = 0
for t in benchmark_texter:
ord_ = normalisera(t).split()
grams = {" ".join(ord_[i:i + n]) for i in range(max(len(ord_) - n + 1, 1))}
if grams & korpus_ngram:
traffar += 1
return {"kontaminerade": traffar, "av": len(benchmark_texter),
"andel": round(traffar / max(len(benchmark_texter), 1), 4)}
# Maskera personuppgifter
MONSTER = {
"E-POST": r"\b[\w.+-]+@[\w-]+\.[\w.]+\b",
"TELEFON": r"\b0[\d\s-]{7,12}\b",
"PERSONNUMMER": r"\b(19|20)?\d{6}[-+]?\d{4}\b",
}
def maskera_pii(text):
antal = Counter()
for namn, m in MONSTER.items():
text, n = re.subn(m, f"<{namn}>", text)
antal[namn] += n
return text, dict(antal)
# Rapportera var datan försvinner
def pipeline_rapport(dokument, ordlista=None):
steg = {"in": len(dokument)}
d = exakt_dedup(dokument); steg["efter exakt dedup"] = len(d)
d = [x for x in d if kvalitet_ok(x, ordlista)[0]]; steg["efter kvalitet"] = len(d)
d = nara_dedup(d); steg["efter nära-dedup"] = len(d)
steg["kvar_andel"] = round(len(d) / max(len(dokument), 1), 3)
return d, steg
Behärskning innebär
- Bygger en filtreringspipeline
- Deduplicerar på flera nivåer
- Mäter korpuskvalitet och kontaminering
Logga in för att göra övningarna och bygga upp din behärskning.
Källor
- arXiv — Deduplicating Training Data Makes Language Models Better — arXiv (öppen åtkomst; licens per artikel)
- arXiv — Scaling Language Models: Methods, Analysis & Insights from Training Gopher — arXiv (öppen åtkomst; licens per artikel)
- arXiv — The RefinedWeb Dataset for Falcon LLM — arXiv (öppen åtkomst; licens per artikel)