Ich habe ein 4B-Modell auf 25 Jahre eigener Arbeit feintrainiert

· Mag. Michael Allerstorfer · English version

Ich wollte wissen, ob ein kleines Modell, trainiert auf einer einzelnen Consumer-Grafikkarte, lernen kann, Webbauteile so zu bauen wie ich. Nicht nach einem öffentlichen Bestand aus HTML und CSS, sondern nach meinen eigenen Konventionen, aus meinen eigenen Projekten, über 25 Jahre gesammelt. Nur eigene Projekte, keine Kundenprojekte.

Die kurze Antwort ist ja, und besser als erwartet. Die längere ist, dass ich das meiste nicht daraus gelernt habe, dass das Training richtig lief, sondern daraus, dass die Messung falsch war.

Es läuft nirgends, es hängt nichts daran. Es baut ein Bauteil, keine Anwendung, und ich stelle es nicht zum Download. Warum nicht, steht am Ende.

Was herauskam

Der Anteil erzeugter Bauteile, die alle meine Hausregeln erfüllen, ging von 12,7 Prozent untrainiert auf 74,6 Prozent, ohne dass im Prompt eine einzige Regel genannt wird. Beide Zahlen stammen aus 500 Erzeugungen je Modell, 50 Aufgaben mit je zehn Läufen, und die Unsicherheit ist ein 95-Prozent-Bereich aus einem Bootstrap über Aufgaben statt über Läufe (6,7 bis 19,3 untrainiert, 67,8 bis 81,2 trainiert). Über Aufgaben, weil zehn Läufe derselben Aufgabe zehn korrelierte Antworten sind und es die Unsicherheit stark unterschätzt, sie als unabhängig zu behandeln. Die Regeln betreffen semantische Gliederung, ARIA-Attribute, Kontrast, sichtbaren Fokus, keine Konsolenfehler, keine fremden Abhängigkeiten.

Dieser Zahl habe ich nicht getraut, also habe ich zusätzlich 360 Bauteile von Hand beurteilt, blind: undurchsichtige Namen, gemischte Reihenfolge, Herkunft unbekannt. 150 aus Lauf 13, 150 aus Lauf 1, 60 aus dem untrainierten Modell als Kontrolle. Beurteilt habe ich Lauf 13, nicht Lauf 14, der später kam.

Nach Augenschein lieferte das untrainierte Modell in 33 Prozent der Fälle etwas Brauchbares, Lauf 13 in 77, ein gesicherter Abstand von 41,7 Punkten (Bereich 27,7 bis 55,3). Zwei völlig verschiedene Maßstäbe, dieselbe Richtung. Nicht dieselbe Antwort: Die Prüfung nennt 12,7 Prozent der Bauteile des Basismodells fehlerfrei, während ich 33 Prozent davon brauchbar fand. Über das Basismodell sind die beiden also deutlich uneins und einig nur über Größe und Richtung der Verbesserung. Diese Übereinstimmung hat trotzdem mehr für mein Vertrauen getan als jede der beiden Zahlen allein.

Zwei Dinge dieser Reihe sind statistisch gesichert, der Rest nicht. Gesichert: der Abstand zum untrainierten Modell, und Lauf 1 auf Lauf 14, das sind +8,8 Punkte bei einem Bereich von +1,6 bis +16,4. Nicht gesichert: jeder einzelne Schritt dazwischen. Lauf 13 auf Lauf 14 etwa sind +2,4 Punkte bei einem Bereich von −3,2 bis +8,4, der die Null einschließt. Die Reihe als Ganzes ist also irgendwohin gekommen, zusammengesetzt aus Schritten, von denen ich keinen einzeln verteidigen konnte.

Vierzehn Trainingsläufe über vier Wochen, je zweieinhalb bis dreieinhalb Stunden auf einer RTX 4070 Ti mit 12 GB. Im Rückblick hatte der erste Lauf schon fast den ganzen Gewinn. Alles zwischen Lauf 1 und Lauf 12 war Seitwärtsbewegung, und ich konnte das damals nicht sehen, weil ich schlecht gemessen habe.

Der Teil, den ich zwei Wochen lang falsch hatte

Zwei Wochen lang habe ich das Basismodell gemessen und dachte, ich messe mein Finetuning.

Der Adapter wurde über eine Funktion geladen, die trainierte Gewichte an das Basismodell hängt, indem sie Modulnamen abgleicht. Die Namen passten nicht, um ein einziges Stück im Pfad. PEFT hängte an, was passte, und ließ den Rest auf null. Einen Fehler oder eine Warnung habe ich damals nicht bemerkt; welche Version ich damals laufen hatte, kann ich heute nicht sagen, und als ich den Fehler später auf peft 0.20.0 reproduziert habe, gab es eine Zeile über fehlende Adapterschlüssel. Entweder war es damals still, oder ich habe darüber hinweggelesen. Mit diesen Gewichten auf null trägt der Adapter exakt nichts bei, das Modell verhielt sich also Token für Token wie ein untrainiertes.

Jede Zahl aus diesen zwei Wochen war echt, reproduzierbar und galt dem falschen Modell. Ich habe darauf Theorien gebaut. „Finetuning eines 4B-Modells bringt wenig" passte zu meinen Erwartungen, also habe ich es ausgebaut statt geprüft.

Aufgebrochen hat es erst, als ich einen zweiten, unabhängigen Weg dazunahm, das Modell laufen zu lassen. Die beiden widersprachen sich. Nur deshalb habe ich es gefunden.

Seitdem prüft der Dienst beim Start, ob der Adapter überhaupt etwas verändert, indem er die rohe Ausgabe mit und ohne ihn vergleicht, und verweigert den Start bei identischem Ergebnis. Das kostet eine Zehntelsekunde.

Die drei Zeilen, die ich mir früher gewünscht hätte

Wer einen LoRA-Adapter mit PEFT lädt, sollte das im eigenen Code haben. Es vergleicht die rohe Ausgabe mit und ohne Adapter und bricht ab, wenn sie gleich ist, denn das ist das einzige Symptom, das eine stille Nichtübereinstimmung erzeugt:

import torch
from peft import PeftModel

# basis und tok sind das bereits geladene Basismodell und sein Tokenizer.
modell = PeftModel.from_pretrained(basis, "<adapter path>")

probe = tok("irgendein fester Prompt", return_tensors="pt").to(modell.device)
with torch.no_grad():
    with modell.disable_adapter():
        ohne = modell(**probe).logits
    mit = modell(**probe).logits
assert not torch.equal(mit, ohne), "Adapter ohne Wirkung"

Die übliche Ursache, und meine, sind multimodale Basismodelle. Ein Modell, das über AutoModelForImageTextToText geladen wird, trägt in seinen Modulpfaden ein zusätzliches language_model-Stück, das dasselbe Modell über AutoModelForCausalLM nicht hat. Trainiert man über den einen Weg und lädt über den anderen, passen die Namen nicht zusammen. Auf peft 0.20.0, wo ich das geprüft habe, gibt die Nichtübereinstimmung eine Zeile über fehlende Adapterschlüssel aus. Das ist eine Zeile inmitten der Ladeausgabe, das Modell läuft danach normal, und der erzeugte Text sieht gut aus, deshalb ist die Zusicherung der Teil, der wirklich hilft.

Drei weitere derselben Art

Zwei Adapter mit demselben Namen. Einer funktionierte, einer war eine Niete, beide 41 MB, in zwei Verzeichnissen, die ich beide benutzt hatte. Eine Messung kam auf 7 Prozent, bevor mir auffiel, welche Datei gelesen wurde.

Eine Quantisierung, die eine Klassifikation umgedreht hat. Ich hatte ein kleines Modell gebaut, das mit Ja oder Nein über Bauteilqualität urteilen sollte. In 8 Bit antwortete es auf alles „Nein". In 16 Bit war es in Ordnung. Der Fehler war bei freiem Text unsichtbar und bei einer Einzeltoken-Antwort total.

Eine Sonde, die das falsche Wort suchte. Ich habe eine Weile daran gearbeitet, den Adapter zu veröffentlichen, und dazu gehörte, wissen zu wollen, ob er Bezeichner aus meinen eigenen Projekten wiedergibt. Das Werkzeug, das genau das prüft, war von der Anonymisierung selbst mit umgeschrieben worden und suchte danach die Ersatznamen, die das Modell unmöglich gelernt haben kann. Es hätte mir Entwarnung gegeben. Nach der Reparatur meldete es zweimal gewöhnliche Wörter als Leck, darunter das englische Wort für ein Beschwerdeformular in einem Beschwerdeformular.

Als es dann funktionierte, war die Antwort beruhigend: In 120 Erzeugungen, absichtlich auf die Bauteilarten gerichtet, deren Vorlagen markante interne Bezeichner tragen, kam kein einziger vor. Zu diesem Zeitpunkt hatte ich mich schon entschieden, die Sache nicht herauszugeben, die Sonde hat also am Ende eine Frage beantwortet, die niemand gestellt hat. Ich würde sie trotzdem wieder laufen lassen.

Zwei dieser vier passten zu meiner Erwartung, und genau diese zwei haben überlebt. Das Basismodell zu messen ergab das Ergebnis, das ich erwartete, nämlich dass Finetuning eines 4B wenig bringt, also habe ich es erklärt statt geprüft, zwei Wochen lang. Die Sonde gab mir Entwarnung, und das wollte ich hören, also hätte ich es hingenommen und veröffentlicht.

Die anderen zwei widersprachen mir, und beide waren fast sofort gefunden. Eine Messung von 7 Prozent war zu schlecht, um wahr zu sein, und das war der einzige Hinweis, den ich brauchte. Ein Richter, der auf jede einzelne Frage „Nein" antwortet, ist offensichtlich kaputt, noch bevor man sich die Logprobs ansieht.

Der Mechanismus ist also nicht, dass angenehme Fehler häufiger sind. Er ist, dass ein Fehler, der der Erwartung widerspricht, gesucht wird, bis er gefunden ist, und einer, der sie bestätigt, aufgeschrieben wird.

Und einen habe ich bei dieser Vorbereitung selbst verursacht. Das Werkzeug, das Projektnamen entfernt, lief über den ganzen Verzeichnisbaum und behandelte die Tokenizer-Datei des Modells als gewöhnlichen Text, weil sie auf .json endet. Es überschrieb vier Einträge eines Vokabulars von 248044 Einträgen, in 17 Dateien. Gefunden habe ich es an einem Zeitstempel: Die Datei war Stunden nach dem Ende des Trainings verändert. Ein Tokenizer ist keine Textdatei, egal welche Endung er trägt.

Was mir das Training wirklich beigebracht hat

Ein Wert lässt sich verschieben, eine Struktur nicht. Eine Farbe, die ich verwende, wurde aus 167 Beispielen übernommen. Eine umhüllende Struktur wurde aus 117 nicht übernommen. Dieselben Daten, derselbe Lauf. Es sei denn, die Vorprägung des Modells ist zu stark: Tailwinds Standardblau überlebte 346 Gegenbeispiele. Diese Asymmetrie hat das meiste von dem bestimmt, was ich danach getan habe, und ich verstehe sie noch nicht vollständig.

Kleine Anteile kommen durch. Eine defekte Vorlage von 152, also 0,7 Prozent der Daten, erzeugte über drei aufeinanderfolgende Trainingsläufe eine Fehlerquote von 2,4 Prozent. Ich habe diese eine Datei repariert, und die Fehlerquote ging auf exakt null. Gesucht hatte ich die Ursache in den Trainingsparametern.

Training und Prompt konkurrieren. Sobald das Modell die Regeln kannte, verschlechterte es die Ausgabe messbar, dieselben Regeln in den Prompt zu schreiben. Am untrainierten Modell war derselbe Text die größte Verbesserung, die ich je gemessen habe. Eine Hilfe wirkt, solange die Fähigkeit fehlt, und steht danach im Weg.

Eine Regel ohne konkreten Wert wird nicht gelernt. Das Modell greift von selbst zu #777, und das ist ein Kontrastverhältnis von 4,48 gegen eine Schwelle von 4,5. „Achte auf ausreichenden Kontrast" änderte nichts, weil das Problem eine Rechnung ist, die niemand im Kopf macht. Beispiele mit richtigen Werten änderten es.

Training auf eigenen Ausgaben kauft Gleichförmigkeit, nicht Qualität. Der letzte Lauf, Lauf 14, wurde auf einer Auswahl von Bauteilen trainiert, die er selbst erzeugt und selbst bestanden hatte. Nach den Regeln kam er vor Lauf 13 heraus, 74,6 gegen 72,2 Prozent, das sind +2,4 Punkte und innerhalb der Unsicherheit, also überhaupt keine gesicherte Verbesserung. Dann habe ich die beiden nebeneinandergestellt, 50 Vergleiche, und Lauf 13 19-mal gegen 11-mal vorgezogen.

Diese Aufteilung hat einen Vorzeichentest-p von 0,20, etwas so Schiefes käme also etwa in einem von fünf Fällen durch Zufall zustande, damit ist auch das nicht entschieden. Was bleibt, sind zwei Maßstäbe, die über meine letzten beiden Modelle uneins sind, einer auf 500 Erzeugungen gestützt und einer auf 50 meiner eigenen Urteile. Meine Lesart ist, dass Lauf 14 gelernt hatte, seinen eigenen besten Einfall zu wiederholen statt zur Aufgabe zu passen, und dass die Regelmessung den Unterschied nicht sehen kann, weil Gleichförmigkeit für eine Prüfung wie Verlässlichkeit aussieht.

Wo die Messung aufhört, und warum das der eigentliche Befund ist

Über alle 360 von Hand beurteilten Bauteile bestanden 211 jede Regel, die ich geschrieben hatte, und mit 45 dieser 211 war ich trotzdem nicht zufrieden. Diese 211 sind über die drei Modelle der Stichprobe zusammengefasst, mischen also ein gutes Modell mit einem schlechten. Für das trainierte Modell allein, Lauf 13: 105 seiner 150 Bauteile bestanden jede Regel, und 18 dieser 105 genügten mir nicht. Etwa jedes sechste.

Also habe ich drei weitere Regeln geschrieben, genau auf diese 45 zusammengefassten Fälle gerichtet: überlappende Inhalte, falscher Anfangszustand, Bedienelemente ohne Wirkung. Zusammen fingen sie etwa 7 Prozent der 45 ein.

Der Rest waren Abstände, Proportion, Ausrichtung und Dinge, die technisch vorhanden waren, aber nicht funktionierten. Ich sehe das in einer halben Sekunde und ich konnte es nicht aufschreiben. Diese verbleibenden 93 Prozent der 45 Fälle sind das, wovor ich jeden warnen würde, der die Beurteilung generierten Codes automatisieren will. Die Wand kommt viel früher als man denkt, und mehr Regeln schließen sie nicht.

Was es nicht kann

Es baut ein Bauteil. Kein Routing, kein Zustand über Seiten, kein Datenmodell. Es baut nach meinen Konventionen, jeder andere bekäme also ein Modell, das überzeugt daneben liegt. Es erwartet die Aufgabe auf Deutsch. Jedes vierte Bauteil verletzt noch eine Regel. Von den regelkonformen würde mir etwa jedes sechste nicht genügen, gemessen an Lauf 13, den ich von Hand beurteilt habe; der letzte Lauf, Lauf 14, hat diese Prüfung nie durchlaufen.

Warum es keinen Download gibt

Ich habe das gebaut, um etwas herauszufinden, und ich habe es herausgefunden. Das Modell selbst löst kein Problem: Es schreibt Bauteile im Hausstil einer einzigen Person, und diese Person bin ich, was es für alle anderen unbrauchbar und für mich unnötig macht, weil ich sie selbst schreiben kann und schneller. Eine 41-MB-Datei hochzuladen, damit sie dort unbenutzt liegt, ist kein Ergebnis, sondern Hausarbeit.

Was die Arbeit hervorgebracht hat, steht in diesem Text. Die Messung war viermal hintereinander falsch, und die zwei Fälle, die zu meiner Erwartung passten, sind die zwei, die lange gehalten haben, eine einzige defekte Vorlage von 152 war drei Trainingsläufe lang in der Ausgabe zu sehen, und die Lücke zwischen „besteht jede Prüfung" und „ist gut" erwies sich als größtenteils nicht in Regeln schreibbar. Das sind die Teile, die etwas wert sind, und sie brauchen keinen Download.

Eine Veröffentlichung habe ich vorbereitet, weit genug, um dabei zwei weitere Fehler zu finden, einen davon selbst verursacht. Die behalte ich im Bericht, weil sie das Nützlichste sind, was aus der Vorbereitung herausgekommen ist.

Die Vorlagen und die Trainingsdaten bleiben ohnehin, wo sie sind. Sie sind 25 Jahre eigener Arbeit, und in ihnen stecken die Konventionen, die dieses Modell wiedergeben soll; sie herauszugeben hieße, den Hausstil selbst herauszugeben.

← Alle Artikel