Zum Inhalt
wannacode

Auswertung: Die Genauigkeit lügt

Kurz gesagt: Ein Modell wird mit Daten ausgewertet, die es nicht gesehen hat: Die Konfusionsmatrix zählt die vier Ausgänge, und Präzision und Trefferquote sagen mehr als die Genauigkeit.

Problem

99 % Genauigkeit, null Betrugsfälle erkannt

Ein Kollege zeigt dir sein Betrugsmodell: „99 % Genauigkeit!“ Klingt großartig. Hier ist ein Modell, das dasselbe erreicht, ohne etwas zu tun:

const frauds = fraud.reduce((sum, y) => sum + y, 0);
// „immer normal“: blockt nie, deshalb liegt es bei jeder ehrlichen Zahlung richtig
const accuracy = 100 * (4000 - frauds) / 4000;
console.log(`Zahlungen: 4000, Betrugsfälle: ${frauds}`);
console.log(`„immer normal“: Genauigkeit ${accuracy.toFixed(2)}%, erkannte Betrugsfälle: 0 von ${frauds}`);
Ausgabe
Zahlungen: 4000, Betrugsfälle: 40
„immer normal“: Genauigkeit 99.00%, erkannte Betrugsfälle: 0 von 40

Das ist ein Monat mit 4.000 Zahlungen, und 40 davon sind Betrug: einer von hundert. Das Modell „immer normal“ stoppt nichts. Bei 3.960 ehrlichen Zahlungen liegt es richtig, bei 40 Betrugsfällen falsch. Genauigkeit 99,00 %, erkannte Betrugsfälle: null.

Die Genauigkeit (accuracy) ist der Anteil der richtigen Antworten. Wenn eine Klasse 99 % ausmacht, ist das Modell, das immer diese Klasse sagt, zu 99 % genau. Die Zahl stimmt, aber sie beantwortet nicht die Frage des Shops: „Wie viele Betrugsfälle erkennen wir, und wie viele ehrliche Kunden ärgern wir?“

Sieh es dir an

Der Monat im Code

Echte Zahlungen kommen aus der Datenbank des Shops. Hier wird der Monat nach einer Regel erzeugt, damit er klein und in jeder Sprache gleich ist. Jede Zahlung ist mit einer Wahrscheinlichkeit von 1 % Betrug. Betrugsfälle sind meist das 3- bis 12-Fache des üblichen Betrags, manchmal ein kleiner Test (0,3× bis 1,3×). Ehrliche Zahlungen liegen meist beim 0,2- bis 2,2-Fachen, manchmal ist es ein großer Einkauf bis zum 6-Fachen.

Die Zufallszahlen kommen aus einem kleinen Generator mit Seed (ein LCG): Derselbe Seed liefert in jeder Sprache und bei jedem Lauf dieselben Zahlen.

// ein Generator mit Seed: in jeder Sprache dieselben Zahlen
class Lcg {
constructor(seed) {
this.state = seed;
}
next() {
this.state = (this.state * 1664525 + 1013904223) % 4294967296;
return this.state / 4294967296;
}
}
// ein Monat Zahlungen: etwa 1 von 100 ist Betrug
const rng = new Lcg(319);
const ratios = [];
const fraud = [];
for (let i = 0; i < 4000; i++) {
const isFraud = rng.next() < 0.01;
if (isFraud) // Betrug: meist 3× bis 12×, manchmal ein kleiner Test
ratios.push(rng.next() < 0.85 ? 3 + 9 * rng.next() : 0.3 + rng.next());
else // ehrlich: meist 0.2× bis 2.2×, manchmal ein großer Einkauf
ratios.push(rng.next() < 0.03 ? 2.5 + 3.5 * rng.next() : 0.2 + rng.next() + rng.next());
fraud.push(isFraud ? 1 : 0);
}
Das Produkt state * 1664525 liegt unter 2⁵³, deshalb ist die Zahl (ein double) exakt, ohne Verlust. Der Generator liefert dieselben Zahlen wie in den anderen Sprachen.

Das Merkmal ist dasselbe wie in der Lektion zur Klassifikation: der Betrag im Vergleich zum üblichen Betrag des Kunden.

Die Idee

Training und Test

Bevor etwas gemessen wird, gibt es eine wichtigere Frage: Mit welchen Daten messen wir? Hier ist das einfachste „Modell“: Es merkt sich die Beträge der Betrugsfälle, die es gesehen hat, und stoppt nur diese. Der Monat wird gemischt und geteilt: 70 % zum Trainieren, 30 % zum Prüfen.

// Mischen (Fisher–Yates) mit demselben Generator
const order = Array.from({ length: 4000 }, (_, i) => i);
for (let i = 3999; i > 0; i--) {
const j = Math.floor(rng.next() * (i + 1));
[order[i], order[j]] = [order[j], order[i]];
}
const train = order.slice(0, 2800);
const test = order.slice(2800);
console.log(`Training: ${train.length} Zahlungen, ${countFrauds(fraud, train)} Betrugsfälle`);
console.log(`Test: ${test.length} Zahlungen, ${countFrauds(fraud, test)} Betrugsfälle`);
// merkt sich die Beträge der Betrugsfälle, die es gesehen hat
const seen = new Set(train.filter((i) => fraud[i] === 1).map((i) => ratios[i]));
report("Auswendiglernen auf den Trainingsdaten", seen, ratios, fraud, train);
report("Auswendiglernen auf den Testdaten", seen, ratios, fraud, test);
Ausgabe
Training: 2800 Zahlungen, 24 Betrugsfälle
Test: 1200 Zahlungen, 16 Betrugsfälle
Auswendiglernen auf den Trainingsdaten: Genauigkeit 100.00%, erkannt 24 von 24
Auswendiglernen auf den Testdaten: Genauigkeit 98.67%, erkannt 0 von 16

Bei den Zahlungen, aus denen es gelernt hat, ist das Modell perfekt: 100 %, 24 von 24. Bei neuen erkennt es keinen einzigen Betrug, weil ein neuer Betrug nie genau derselbe Betrag ist wie ein alter. Das Modell hat nichts über Betrug gelernt, es hat sich die Fälle nur gemerkt.

Darum werden die Daten vor dem Training in zwei Teile geteilt:

  • Trainingsdaten (training set): Das Modell lernt aus ihnen;
  • Testdaten (test set): Sie werden am Ende zum Messen benutzt, und das Modell sieht sie bis dahin nicht.

Der Test steht für die Zahlungen von morgen. Das Mischen vor dem Teilen ist wichtig: Wenn der Monat nach Datum sortiert ist, besteht der Test nur aus der letzten Woche.

Sieh es dir an

Die Konfusionsmatrix

Jetzt das echte Modell: die logistische Regression aus der Lektion zur Klassifikation. Die Funktion ist dieselbe, nur bekommt sie zusätzlich rows: welche Zeilen sie benutzen soll. Sie lernt nur aus den Trainingsdaten:

// lernt nur aus den Zeilen in rows
function trainLogistic(xs, ys, rows, rate, epochs) {
let w = 0, b = 0;
const n = rows.length;
for (let epoch = 0; epoch < epochs; epoch++) {
let gw = 0, gb = 0;
for (const i of rows) {
const p = sigmoid(w * xs[i] + b);
gw += (p - ys[i]) * xs[i];
gb += p - ys[i];
}
w -= rate * gw / n;
b -= rate * gb / n;
}
return { w, b };
}

Eine Testzahlung kann auf vier Arten enden. Das Modell stoppt oder lässt durch, und die Zahlung ist Betrug oder ehrlich:

Modell stoppt Modell lässt durch
Betrug gestoppter Betrug (TP) übersehener Betrug (FN)
ehrlich gestoppter Ehrlicher (FP) durchgelassener Ehrlicher (TN)

Das ist die Konfusionsmatrix (confusion matrix). Die Buchstaben stehen für wahr/falsch positiv/negativ. „Positiv“ ist die Klasse, die wir suchen: Betrug.

function confusion(ratios, fraud, rows, w, b,
threshold) {
let tp = 0, fp = 0, fn = 0, tn = 0;
for (const i of rows) {
const stop = block(ratios[i], w, b, threshold);
if (stop && fraud[i] === 1) tp++; // gestoppter Betrug
else if (stop) fp++; // gestoppter ehrlicher Kunde
else if (fraud[i] === 1) fn++; // übersehener Betrug
else tn++; // durchgelassener ehrlicher Kunde
}
return { tp, fp, fn, tn };
}
const { w, b } = trainLogistic(ratios, fraud, train, 1, 1000);
const { tp, fp, fn, tn } =
confusion(ratios, fraud, test, w, b, 0.5);
const accuracy = 100 * (tp + tn) / test.length;
const precision = 100 * tp / (tp + fp);
const recall = 100 * tp / (tp + fn);
console.log(`w = ${w.toFixed(4)}, b = ${b.toFixed(4)}`);
console.log(`gestoppter Betrug (TP): ${tp}`);
console.log(`gestoppte Ehrliche (FP): ${fp}`);
console.log(`übersehener Betrug (FN): ${fn}`);
console.log(`durchgelassene Ehrliche (TN): ${tn}`);
console.log(`Genauigkeit ${accuracy.toFixed(2)}%, precision ${precision.toFixed(2)}%, recall ${recall.toFixed(2)}%`);
Ausgabe
w = 1.0702, b = -7.1054
gestoppter Betrug (TP): 7
gestoppte Ehrliche (FP): 0
übersehener Betrug (FN): 9
durchgelassene Ehrliche (TN): 1184
Genauigkeit 99.25%, precision 100.00%, recall 43.75%

Die Genauigkeit beträgt 99,25 %, ein wenig mehr als die 98,67 % von „immer normal“ im selben Test. Trotzdem erkennt das Modell 7 von 16 Betrugsfällen und stoppt keinen einzigen ehrlichen Kunden. Die beiden Genauigkeiten sind fast gleich, und die Modelle sind völlig verschieden. Wir brauchen Zahlen, die auf die Betrugsfälle schauen.

Sieh es dir an

Die Matrix füllt sich

function confusion(ratios, fraud, rows, w, b,
threshold) {
let tp = 0, fp = 0, fn = 0, tn = 0;
for (const i of rows) {
const stop = block(ratios[i], w, b, threshold);
if (stop && fraud[i] === 1) tp++; // gestoppter Betrug
else if (stop) fp++; // gestoppter ehrlicher Kunde
else if (fraud[i] === 1) fn++; // übersehener Betrug
else tn++; // durchgelassener ehrlicher Kunde
}
return { tp, fp, fn, tn };
}
Schritt für Schritt, in Worten
  1. 1200 Testzahlungen, die das Modell nie gesehen hat, Schwellenwert 0,5. Die Zeile ist die Wahrheit, die Spalte ist, was das Modell gesagt hat.
  2. Zuerst die 16 Betrugsfälle, einer nach dem anderen: 7 gestoppt (TP), 9 übersehen (FN).
  3. Dann die Ehrlichen: 0 gestoppt (FP), 1184 durchgelassen (TN). Fast alles landet in einer Ecke.
  4. Die Genauigkeit ist die Diagonale: (7 + 1184) / 1200 = 99,25 %. „Immer normal“ erreicht im selben Test 98,67 %.
  5. Die Präzision ist die Spalte „stoppen“: Wie viele der gestoppten Zahlungen waren Betrug? 7 / 7 = 100,00 %.
  6. Die Trefferquote ist die Zeile „Betrug“: Wie viele der Betrugsfälle haben wir erkannt? 7 / 16 = 43,75 %.
  7. Die Genauigkeit betrachtet alles und ist hoch. Präzision und Trefferquote betrachten die Betrugsfälle: Jeder Stopp ist ein Betrug, aber wir erkennen weniger als die Hälfte.

1.184 der 1.200 Zahlungen liegen in der Ecke unten rechts: durchgelassene Ehrliche. Darum ist die Genauigkeit bei jedem Modell hoch, das ehrliche Zahlungen durchlässt. Das Interessante steht in der oberen Zeile.

Die Idee

Präzision und Trefferquote in den Worten des Shops

Zwei Fragen, zwei Zahlen:

  • Präzision (precision): Wie viele der gestoppten Zahlungen sind wirklich Betrug? TP / (TP + FP). Eine niedrige Präzision bedeutet viele verärgerte ehrliche Kunden.
  • Trefferquote (recall): Wie viele aller Betrugsfälle haben wir erkannt? TP / (TP + FN). Eine niedrige Trefferquote bedeutet übersehene Betrugsfälle und verlorenes Geld.

Beim Schwellenwert 0,5 hat das Modell eine Präzision von 100 % und eine Trefferquote von 43,75 %. Jeder Stopp ist ein Betrug, aber es erkennt weniger als die Hälfte. Bei „immer normal“ ist die Trefferquote 0 %, und du siehst das sofort, ohne dich mit 99 % zu täuschen.

Die beiden ziehen in entgegengesetzte Richtungen. Um mehr Betrugsfälle zu erkennen (Trefferquote), stoppst du mehr verdächtige Zahlungen, und einige davon sind ehrlich (die Präzision sinkt).

Probier es aus

Lücke füllen

Die Präzision teilt durch alle gestoppten Zahlungen. Wodurch teilt die Trefferquote?

const precision = 100 * tp / (tp + fp);
const recall = 100 * tp / (tp + );

Probier es aus

Sage die Ausgabe voraus

Zehn Zahlungen: die Wahrheit (1 = Betrug) und was das Modell gestoppt hat (1 = gestoppt). Was wird ausgegeben?

const actual = [1, 0, 0, 1, 0, 0, 0, 1, 0, 0];
const blocked = [1, 1, 0, 0, 0, 1, 0, 1, 0, 0];
let tp = 0, fp = 0, fn = 0;
for (let i = 0; i < actual.length; i++) {
if (blocked[i] === 1 && actual[i] === 1) tp++;
else if (blocked[i] === 1) fp++;
else if (actual[i] === 1) fn++;
}
console.log(`precision ${(100 * tp / (tp + fp)).toFixed(2)}%, recall ${(100 * tp / (tp + fn)).toFixed(2)}%`);
Antwort

Die Idee

Der Schwellenwert und die Kosten in Euro

Der Schwellenwert aus der Lektion zur Klassifikation bekommt jetzt Zahlen. Dasselbe Modell auf demselben Test, bei vier Schwellenwerten:

for (const threshold of [0.5, 0.3, 0.1, 0.02]) {
const { tp, fp, fn } = confusion(ratios, fraud, test, w, b, threshold);
const precision = 100 * tp / (tp + fp);
const recall = 100 * tp / (tp + fn);
console.log(`Schwellenwert ${threshold.toFixed(2)}: TP ${tp}, FP ${fp}, FN ${fn} → precision ${precision.toFixed(2)}%, recall ${recall.toFixed(2)}%`);
}
Ausgabe
Schwellenwert 0.50: TP 7, FP 0, FN 9 → precision 100.00%, recall 43.75%
Schwellenwert 0.30: TP 10, FP 1, FN 6 → precision 90.91%, recall 62.50%
Schwellenwert 0.10: TP 11, FP 13, FN 5 → precision 45.83%, recall 68.75%
Schwellenwert 0.02: TP 14, FP 28, FN 2 → precision 33.33%, recall 87.50%

Je niedriger der Schwellenwert, desto mehr Betrugsfälle erkennen wir (die Trefferquote steigt von 43,75 % auf 87,5 %) und desto mehr ehrliche Kunden stoppen wir (die Präzision fällt von 100 % auf 33,33 %). Was ist besser? Die Antwort steht in Euro. Sagen wir, ein übersehener Betrug kostet 250 € (die Ware und die Rückbuchungsgebühr), und ein gestoppter ehrlicher Kunde kostet 10 € (ein Anruf beim Support und verlorene Zeit). Die Preise sind ein Beispiel:

// Beispielpreise: ein übersehener Betrug und ein gestoppter ehrlicher Kunde
const missedCost = 250, blockedCost = 10;
let best = -1;
let bestCost = Infinity;
for (const threshold of [0.5, 0.3, 0.1, 0.02]) {
const { fp, fn } = confusion(ratios, fraud, test, w, b, threshold);
const cost = fn * missedCost + fp * blockedCost;
console.log(`Schwellenwert ${threshold.toFixed(2)}: ${fn} × ${missedCost} € + ${fp} × ${blockedCost} € = ${cost} €`);
if (cost < bestCost) {
bestCost = cost;
best = threshold;
}
}
console.log(`günstigster Schwellenwert: ${best.toFixed(2)}`);
Ausgabe
Schwellenwert 0.50: 9 × 250 € + 0 × 10 € = 2250 €
Schwellenwert 0.30: 6 × 250 € + 1 × 10 € = 1510 €
Schwellenwert 0.10: 5 × 250 € + 13 × 10 € = 1380 €
Schwellenwert 0.02: 2 × 250 € + 28 × 10 € = 780 €
günstigster Schwellenwert: 0.02

Mit diesen Preisen ist der strengste Schwellenwert der billigste: 0,02, mit 780 € statt 2.250 € bei 0,5. Ein übersehener Betrug kostet so viel wie 25 gestoppte Kunden, also lohnt es sich, öfter zu stoppen. Wenn ein gestoppter Kunde für immer geht und viel mehr kostet, sieht die Antwort anders aus. Das ist die Übung am Ende.

Eine Warnung. Hier wird der Schwellenwert mit dem Test gewählt. Der Test hat also schon an einer Entscheidung teilgenommen, und die 780 € sind ein wenig optimistischer als die Wahrheit. In einem echten Projekt wird der Schwellenwert mit einem dritten Teil der Daten gewählt, den Validierungsdaten (validation set), und der Test läuft nur einmal, ganz am Ende. Wie, steht in der nächsten Lektion.

Probier es aus

Finde den Fehler

Ein Kollege teilt die Daten so. Seine Testzahlen sind hervorragend, und im Shop erkennt das Modell viel weniger. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Wann es wehtut

  • Genauigkeit bei ungleichen Klassen. Betrug, Krankheit, Ausfälle, Spam: Wenn das, was du suchst, selten ist, ist die Genauigkeit fast nutzlos. Schau auf die Matrix, die Präzision und die Trefferquote.
  • Ein Test, der den Trainingsdaten ähnlicher sieht als der Zukunft. Betrüger ändern ihre Tricks. Wenn der Test mit vergangenen Monaten gemischt ist, ist er leichter als der nächste Monat. Bei solchen Daten ist der Test der neuere Zeitraum: Du lernst bis zum 30. September und misst den Oktober.
  • Wenige Beispiele der seltenen Klasse. Der Test hat 16 Betrugsfälle. Ein Betrug mehr oder weniger verschiebt die Trefferquote um 6,25 %. Die Zahlen hier sind für die Lektion, und echte Teams sammeln Tausende Betrugsfälle, bevor sie der zweiten Stelle trauen.
  • Eine Zahl für alles. Präzision und Trefferquote sind zwei, weil es zwei Fehler gibt und sie unterschiedlich viel kosten. Wenn du sie zu einer Zahl zusammenpresst, verlierst du die Entscheidung über die Kosten.
  • Ein oft angefasster Test. Jedes Mal, wenn du das Modell änderst, weil der Test „schlecht“ gesagt hat, wird der Test ein bisschen zum Training.
Genauer, für Neugierige

F1. Das harmonische Mittel aus Präzision und Trefferquote: 2 · P · R / (P + R). Eine Zahl, die fällt, wenn eine der beiden niedrig ist. Sie ist nützlich zum Vergleichen, kennt aber die Kosten der Fehler nicht.

Die Kurve. Wenn du den Schwellenwert von 1 auf 0 bewegst und die Präzision gegen die Trefferquote zeichnest, erhältst du die PR-Kurve. Die Fläche darunter (PR AUC) misst das Modell ohne einen bestimmten Schwellenwert. Die verwandte ROC-Kurve (Anteil der erkannten Betrugsfälle gegen Anteil der gestoppten Ehrlichen) ist bekannter, sieht aber bei 1 zu 100 besser aus, als sie ist, weil es so viele ehrliche Zahlungen gibt.

Der Schwellenwert aus den Kosten. Wenn die Wahrscheinlichkeiten des Modells stimmen, ist der billigste Schwellenwert die Kosten eines gestoppten Kunden geteilt durch die Summe der beiden Kosten: 10 / (10 + 250) ≈ 0,04. Das Programm hat unter den vier Schwellenwerten, die es probiert hat, 0,02 gefunden. Das Modell hier ist mit 1 % Betrug gelernt, seine Wahrscheinlichkeiten liegen also nahe an den echten. Ein Modell, das mit 40 % Betrug gelernt wurde (wie in der Lektion zur Klassifikation), sieht Betrug zu oft, und sein Schwellenwert muss höher sein.

Teilen nach Klassen. Bei 40 Betrugsfällen kann ein zufälliges Teilen 30 ins Training und 10 in den Test schicken oder umgekehrt. Ein stratifiziertes Teilen behält in beiden Teilen denselben Anteil an Betrugsfällen.

Im echten Code. In scikit-learn sind das train_test_split (mit stratify= für die Klassen), confusion_matrix, precision_score, recall_score und precision_recall_curve. In ML.NET liefern TrainTestSplit und BinaryClassificationMetrics dasselbe. Das Beobachten des Modells in der Produktion (ob die Trefferquote Monat für Monat sinkt) gehört zu MLOps und wird in ZeroToOps unterrichtet.

Set speichert Zahlen als Schlüssel: seen.has(3.7) ist nur für genau dieselbe Zahl wahr.

Übung

Der teure ehrliche Kunde

Löse das in deinem Editor.

Der Shop verkauft Abonnements. Ein irrtümlich gestoppter Kunde wechselt oft zur Konkurrenz, deshalb kostet ein gestoppter ehrlicher Kunde 60 €, und ein übersehener Betrug kostet weiterhin 250 €. Lass das Kostenprogramm mit 60 € laufen und finde den billigsten Schwellenwert.

Ausgabe
Schwellenwert 0.50: 9 × 250 € + 0 × 60 € = 2250 €
Schwellenwert 0.30: 6 × 250 € + 1 × 60 € = 1560 €
Schwellenwert 0.10: 5 × 250 € + 13 × 60 € = 2030 €
Schwellenwert 0.02: 2 × 250 € + 28 × 60 € = 2180 €
günstigster Schwellenwert: 0.30

Jetzt ist 0,3 der billigste: Er stoppt 10 von 16 Betrugsfällen und nur 1 ehrlichen Kunden. Das Modell ist dasselbe, die Daten sind dieselben, und der Schwellenwert ist ein anderer, weil die Fehler unterschiedlich viel kosten.

Weitere Aufgaben

  • Ein feineres Raster von Schwellenwerten. Probiere die Schwellenwerte von 0,02 bis 0,5 in Schritten von 0,02. Gibt es einen billigeren als 0,3?
  • Test nach Zeit. Nimm statt zu mischen die ersten 2800 Zahlungen zum Trainieren und die letzten 1200 für den Test. Wie viele Betrugsfälle sind jetzt im Test, und wie hat sich die Trefferquote bei 0,3 verändert?

Test

Teste dich

1. Betrug kommt 1 von 100 Mal vor. Ein Modell sagt bei jeder Zahlung „normal“. Wie hoch ist seine Genauigkeit?
2. Warum wird ein Modell mit Daten geprüft, die es beim Training nicht gesehen hat?
3. Die Präzision beträgt 90 %. Was bedeutet das für den Shop?
4. Ein übersehener Betrug kostet 250 €, ein gestoppter ehrlicher Kunde kostet 10 €. In welche Richtung sollte der Schwellenwert wandern?
5. Ein Kollege wählt den Schwellenwert mit den niedrigsten Kosten auf dem Test und meldet diese Kosten. Was ist falsch?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·