LLM-Fine-Tuning mit einem handgeprüften Seed aus echten Daten verankern
So baust du einen kleinen Seed aus echten Daten fürs Fine-Tuning: maßgebliche Quellen wählen, geschützte Seiten und PDFs als Markdown abrufen, Zeilen formatieren, validieren und einen Holdout zurückhalten.
Trainiere ausschließlich auf synthetischen Daten, Zyklus um Zyklus, und das Modell entfernt sich jedes Mal ein Stück weiter von dem echten Wissen, das es eigentlich aufnehmen sollte. Beim dritten oder vierten Retraining imitiert es gut, wie Fachtexte klingen, und gibt schlecht wieder, was in ihnen steht.
Abhilfe schafft ein Seed aus echten Daten: eine kleine Menge von Hand geprüfter Beispiele aus maßgeblichen Quellen, gesammelt, bevor irgendeine synthetische Erweiterung beginnt. Er muss klein genug sein, dass du noch jede Zeile prüfen kannst. Er muss maßgeblich sein, weil das Modell die Zeilen als Ground Truth behandelt. Und oft muss er von Orten kommen, an die normale Crawler nicht herankommen, denn viel vom wertvollsten Fachwissen liegt hinter Zugriffskontrollen.
Im Folgenden geht es darum, einen solchen Seed zu sammeln, ihn in Trainingszeilen zu bringen, ihn zu validieren und zu prüfen, ob das Fine-Tuning gehalten hat. Die Themen der Reihe nach:
- warum eine rein synthetische Schleife driftet
- die richtigen Quellen auswählen
- geschützte Seiten als sauberes Markdown abrufen
- mit PDFs und anderen Nicht-HTML-Dokumenten umgehen
- Inhalte in Instruktions-Output-Paare, Präferenzpaare oder reinen Fachtext verwandeln
- den Seed validieren und das Ergebnis messen
Warum rein synthetische Schleifen driften
Statistisch verhält sich Fine-Tuning anders als Pretraining. Beim Pretraining verteilt sich Rauschen über Milliarden Tokens, ein einzelner schlechter Absatz geht im Durchschnitt unter. Ein Fine-Tuning-Datensatz hat vielleicht ein paar hundert Zeilen, und dort macht eine einzige falsch gelabelte Extraktion einen messbaren Teil des gesamten Trainingssignals aus.
In der üblichen synthetischen Pipeline schreibt ein Generator Beispiele mit den Gewohnheiten des Teacher-Modells. Ein Judge filtert sie dann anhand genau dieser Gewohnheiten, und der Student übernimmt die Gewohnheiten samt ihren Verzerrungen. Ohne äußeren Anker baut jedes Retraining auf dem vorigen auf. Das Modell lernt, wie Fachtext zu klingen, statt ihn widerzuspiegeln.
Ein echter Seed durchbricht diesen Kreislauf. Er gibt dem Generator einen Anker und dem Judge einen Vergleichsmaßstab, der nicht seine eigene Ausgabe ist. Auf die Größe kommt es nicht an. Ein paar hundert starke Beispiele reichen für eine Extraktionsaufgabe meist aus. Entscheidend ist, dass sich jede Zeile über eine prüfbare Extraktion auf eine Quelle zurückführen lässt, für die du geradestehen kannst.
Der Haken: Maßgebliches Material ist meist am schwersten zu beschaffen. Es steckt hinter JavaScript-Rendering, IP-basierten Rate Limits und Zugriffsregeln, an denen Standard-Crawler scheitern.
Was hineingehört
Lege die Inhalte fest, bevor du Code schreibst. Für ein E-Commerce-Modell sind das Produktkataloge und verifizierte Kundenbewertungen. Ein Finanzmodell braucht SEC-Filings, Transkripte von Earnings Calls und regulatorische Leitlinien. Ein medizinisches Modell braucht klinische Leitlinien, Fachinformationen zu Arzneimitteln und begutachtete Studien.
Vier Kriterien gelten in jeder Domäne:
- Maßgeblich: Praktiker im Fach vertrauen der Quelle, nicht nur eine Suchmaschine.
- Strukturiert: Der Inhalt folgt konsistenten Mustern, auf die ein Extraktionsschema zielen kann.
- Aktuell: Er beschreibt die Domäne, wie sie heute ist, nicht wie vor zwei Jahren.
- Relevant: Er hängt direkt mit der Aufgabe zusammen, statt nur danebenzuliegen.
Lass SEO-Content-Farmen weg, KI-generierten Text und alles, dessen Herkunft du nicht benennen könntest, wenn jemand fragt, woher die Trainingsdaten stammen.
Geschützte Seiten abrufen
Ein nackter requests.get()-Aufruf schickt einen schlichten TLS-Fingerprint und keine Browser-Header, und geschützte Seiten erkennen das sofort. Rendert das Ziel per JavaScript, kommt nur eine leere Hülle zurück.
Eine Fetch-API kann Rendering, Zugriffsarbeit und Markdown-Konvertierung in einem einzigen Request erledigen. Im Beispiel schaltet mode=auto den adaptiven Stealth-Modus ein: Der Dienst wechselt pro Request zwischen JavaScript-Rendering und Proxy-Eskalation und bleibt bei der Konfiguration, die das Ziel verlangt.
import os
import requests
from dotenv import load_dotenv
load_dotenv()
apikey = os.getenv("ZENROWS_API_KEY")
url = "https://www.scrapingcourse.com/ecommerce/"
params = {
"url": url,
"apikey": apikey,
"mode": "auto",
"response_type": "markdown",
}
response = requests.get("https://api.zenrows.com/v1/", params=params)
print(response.text)
Die zurückgegebenen Daten sind kleiner und sauberer als rohes HTML, weil Navigation, Footer-Text und Boilerplate größtenteils schon entfernt sind. Weniger Rauschen am Eingang heißt weniger Rauschen, aus dem das Modell lernt.
PDFs und andere Nicht-HTML-Quellen
Viele der besten Fachquellen sind gar keine Webseiten. Fachinformationen, regulatorische Einreichungen, klinische Leitlinien und technische Normen werden als PDF verteilt. Setze dafür response_type=pdf und außerdem js_render=true, das für PDF-Antworten erforderlich ist.
params = {
"url": url,
"apikey": apikey,
"js_render": "true",
"response_type": "pdf",
}
response = requests.get("https://api.zenrows.com/v1/", params=params)
with open("source-document.pdf", "wb") as f:
f.write(response.content)
Der Dienst liefert die Datei, extrahiert aber nicht den Text darin. Den holst du mit pypdf oder pdfplumber heraus und teilst ihn in Chunks, bevor irgendetwas davon zum Trainingsbeispiel wird. Schneide entlang der Struktur statt nach fester Zeichenzahl. Die eigenen Überschriften und Abschnitte eines Dokuments sind natürliche Grenzen, so behält jeder Chunk seinen Kontext und kein Gedankengang wird mittendrin zerrissen.
Herkunft festhalten
Ein maßgeblicher Seed bleibt nur maßgeblich, wenn du sagen kannst, woher jede Zeile stammt. Hänge ein Metadatenfeld mit Quell-URL, Abrufdatum und Lizenz an, dann bleibt der Datensatz prüfbar und vertretbar.
source = {
"url": pdf_url,
"retrieved": date.today().isoformat(),
"license": license_url,
}
Am wichtigsten wird das, wenn jemand die Ausgaben des Modells in Frage stellt. Dann willst du sagen können, welche Zeile aus welcher Quelle kam und ob diese Quelle eine solche Nutzung erlaubt.
Drei Formen einer Trainingszeile
Ein Fine-Tuning-Beispiel kann drei Formen annehmen, und jede bringt etwas anderes bei.
Instruktions-Output-Paare sind die übliche Wahl für Supervised Fine-Tuning. Jedes Beispiel verbindet das von einer Seite abgerufene Markdown mit dem richtigen strukturierten Ergebnis, sodass das Modell lernt, was es extrahieren soll und in welchem Format es das zurückgibt.
def to_training_example(instruction, markdown_content, extracted_json):
return {
"messages": [
{"role": "system", "content": instruction},
{"role": "user", "content": markdown_content},
{"role": "assistant", "content": json.dumps(extracted_json)},
]
}
Präferenzpaare dienen DPO und RLHF. Sie enthalten zwei Ausgaben zur selben Eingabe, von denen eine als bevorzugt markiert ist. Damit schiebst du ein Modell, das schon ordentliche Ergebnisse liefert, zu besseren.
def to_preference_pair(prompt, chosen, rejected):
return {
"prompt": prompt,
"chosen": chosen,
"rejected": rejected,
}
Fortsetzung von Fachtext ist reiner Domänentext ohne Aufteilung in Eingabe und Ausgabe. Er macht das Modell mit Vokabular und Stil vertraut, taugt aber wenig für Extraktion, weil das Modell nie sieht, was herausgezogen werden soll.
Für Extraktionsaufgaben passen also Instruktions-Output-Paare am besten. Die beiden anderen Formen dienen anderen Zielen.
Filtern, bevor etwas in den Seed kommt
Nicht jede gescrapte Seite verdient einen Platz im Datensatz. Jede Zeile muss vorher drei Prüfungen bestehen:
def keep(markdown, extracted):
if len(markdown.strip()) < 200:
return False
try:
validate(instance=extracted, schema=schema)
except ValidationError:
return False
h = hashlib.sha256(" ".join(markdown.lower().split()).encode()).hexdigest()
if h in seen:
return False
seen.add(h)
return True
Die erste Prüfung verwirft Seiten, die nach dem Rendering fast leer herauskamen. Die zweite, ein Schema-Check, lehnt falsche Typen und leere Felder ab. Die dritte vergleicht Content-Hashes und markiert exakte Kopien sowie Kopien, die sich nur in der Formatierung unterscheiden. Eine Seite muss alle Prüfungen bestehen, um ins Training zu gelangen.
Messen, ob es funktioniert hat
Bevor synthetische Daten ins Spiel kommen, trenne 10 bis 20 Prozent des echten Seeds ab und trainiere nie darauf. Wenn das Fine-Tuning fertig ist, lass das Modell über diesen Holdout laufen und zähle die Ausgaben, die zur erwarteten Antwort passen.
random.seed(42)
random.shuffle(seed_examples)
split = int(len(seed_examples) * 0.85)
train_examples = seed_examples[:split]
holdout_examples = seed_examples[split:]
Wiederhole die Prüfung nach jedem Retraining und protokolliere den Wert über die Zeit. Ein stabiler oder steigender Wert zeigt, dass der Seed wirkt. Ein fallender ist eine Frühwarnung vor Drift, sichtbar, bevor sie sich aufschaukelt.
Was du mitnehmen solltest
- Ein Seed aus ein paar hundert hochwertigen echten Beispielen ist mehr wert als ein großer synthetischer Datensatz ohne einen solchen.
- Maßgebliche Quellen liegen fast immer hinter Zugriffskontrollen, an denen Standard-Crawler nicht vorbeikommen.
- Der adaptive Modus kümmert sich pro Request um JavaScript-Rendering und Bot-Erkennung, ohne Konfiguration pro Website.
- Teile PDFs nach Struktur statt nach Zeichenzahl, damit jedes Stück in sich stimmig bleibt.
- Prüfe jede Zeile beim Eingang, denn eine einzige schlechte Extraktion wiegt in einem kleinen Trainingsset schwer.
- Halte vor jeder synthetischen Erweiterung einen Teil der echten Daten zurück und bewerte das getunte Modell darauf.
Für das Sammeln in größerem Maßstab ist ein Batch-Endpoint, der Parallelität und Retries über lange URL-Listen übernimmt, der naheliegende nächste Schritt.