Hoppa till innehållet
AI-grafen
F· AI engineeringmodelltraning-finjustering· ca 90 min· volatil — kontrolleras ofta· verifierad 2026-09-21

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:

StegKastar typiskt
1. Extrahera text ur HTMLtaggar, skript, meny
2. Språkidentifieringfel språk
3. Kvalitetsheuristikför kort, för många symboler, ordlistor
4. Deduplicering30–60 % av allt
5. Toxicitets- och PII-filterskadligt innehåll, personuppgifter
6. Kontamineringskontrolltestdata 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åHittarMetod
Exaktidentiska dokumenthash av normaliserad text
Nära-dubblettersamma text med små ändringarMinHash + LSH
Substrängsnivååterkommande stycken i olika dokumentsuffixarray

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:

RegelKastar
< 50 eller > 100 000 ordfragment och dumpar
Medelordlängd utanför 3–10 teckenkod, brus, tabeller
> 90 % av raderna börjar med punktlistanavigering
< 80 % av orden i en ordlistabrus och kodslask
Andel symboler > 10 %skript, tabeller
Saknar interpunktionlistor 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åttBerättar
Tokens totalt och unikavolym och variation
Andel efter varje filterstegvar datan försvinner
Perplexitet under en referensmodellskräp i svansarna
Domänfördelningvad modellen kommer att kunna
Språkfördelningsärskilt viktigt för svenska
Kontaminationsgrad per benchmarkom 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

Alla källor och licenser