Pinned Post
Das Feature, das aus Korrekturen lernt, ohne Retraining
- Link abrufen
- X
- Andere Apps
Wenn ich in meiner App eine Kategorie von Hand korrigiere, klassifiziert sie ähnliche Transaktionen ab sofort richtig, ganz ohne Retraining-Lauf, neues Deployment oder MLOps-Pipeline. Hier ist der Trick, und nebenbei, warum „Lernen" nicht immer „Trainieren" heißt.
Im letzten Artikel habe ich gezeigt, wie ich Transaktionen klassifiziere, indem ich ihren Text-Vektor mit Vektoren von Kategorie-Beschreibungen vergleiche. Das wirft eine Frage auf: Was, wenn das Modell danebenliegt?
In den meisten ML-Systemen ist die Antwort aufwändig: Eine falsche Vorhersage wandert als Beispiel ins Trainingsset, irgendwann wird neu trainiert, evaluiert und neu deployt. Ein Zyklus, der Tage bis Wochen dauert. Genau den wollte ich vermeiden.
Die Einsicht dahinter: Wenn eine Kategorie durch Text definiert ist, dann ist „dem Modell etwas beibringen" nichts anderes als „den Text erweitern". Ordnet der Nutzer eine Transaktion manuell der richtigen Kategorie zu, hänge ich genau diesen Transaktionstext an die Definition dieser Kategorie an. Beim nächsten Mal zieht das den Vektor der Kategorie ein Stück in Richtung dieser realen Buchung.
Konkret: Jede manuelle Zuordnung wird als „Korrektur" gespeichert.
with {:ok, updated} <- assign_group(transaction, group_id) do
record_correction(updated, group_id) # <- der Lern-Schritt
{:ok, updated}
end
end
Und die Kategorie-Definition, gegen die klassifiziert wird, ist nicht mehr nur meine handgeschriebene Beschreibung, sondern die Beschreibung plus alle gelernten Korrekturen:
[group.description, correction_text] # Handkuratiert + Gelerntes
|> Enum.reject(&(&1 in [nil, ""]))
|> Enum.join(" ")
Sobald eine Korrektur hereinkommt, werfe ich den gecachten Vektor weg. Die nächste Klassifikation berechnet die Kategorie-Vektoren neu, jetzt inklusive des neuen Beispiels:
# ... Korrektur in der DB speichern ...
Classifier.invalidate_label_cache() # Cache verwerfen -> beim
end # nächsten Mal neu berechnen
Das ist der ganze „Trainings"-Loop: eine Datenbankzeile und ein verworfener Cache. Die Verbesserung wirkt sofort, nicht erst nach einem Trainingslauf.
Ein Detail, auf das ich stolz bin: Der Loop kann auch verlernen. Ordne ich denselben Text später einer anderen Kategorie zu, wird die alte Korrektur gelöscht statt beibehalten, denn sonst hätte das System widersprüchliche Signale. Umklassifizieren heißt eben: die alte Lehre ersetzen, nicht eine zweite danebenstellen.
Der zweite Punkt zum Thema Pragmatismus: ML ist die letzte Instanz, nicht die erste. Manche Buchungen muss ich gar nicht erst „erraten". Eine Mietzahlung, die jeden Monat an denselben Vermieter geht, ist eine deterministische Regel und kein Ratespiel. Deshalb ist die Klassifikations-Pipeline geschichtet:
case first_matching_rule(transaction) do
%Rule{} = rule ->
apply_rule(transaction, rule) # 1. Feste Regel? Fertig.
nil ->
case predetermined_label(transaction) do
{:ok, label} -> assign_label(transaction, label) # 2. Bekannt?
:none -> ml_classify(transaction) # 3. Erst jetzt das Modell.
end
end
end
ML läuft nur, wenn deterministische Regeln und bekannte Muster nichts hergeben. Das ist günstiger und vorhersehbarer, und es hält die Regeln bewusst aus dem ML-Lernsignal heraus. Eine Regel ist eine Anweisung, kein Trainingsbeispiel.
Das Ganze läuft asynchron über einen kleinen Agenten, der eine Transaktion pro Durchlauf abarbeitet und sich selbst neu anstößt, solange noch welche offen sind. So blockiert die erste (langsame) Modell-Inferenz nie einen Web-Request.
Die größere Lektion für alle, die ML-Features bauen: „Das Modell soll besser werden" zwingt dich nicht automatisch in einen Retraining-Apparat. Manchmal ist der Lern-Mechanismus eine Tabellenzeile und ein invalidierter Cache. Wähle die einfachste Architektur, die deinem Anspruch an Genauigkeit genügt, und nicht die, die am meisten nach „echtem ML" aussieht.
Im nächsten Teil das Architektur-Highlight: wie das Modell auf einer komplett anderen Maschine läuft, ohne dass mein Code es merkt.
→ Weiter zu Teil 4: ML-Last verschieben mit einer Zeile Code

Kommentare