Оценка: точността лъже
Накратко: Моделът се оценява на данни, които не е виждал: матрицата на грешките брои четирите изхода, а precision и recall казват повече от точността.
Проблем
99% точност, нула хванати измами
Колега показва модела си за измами: „Точност 99%!“ Звучи отлично. Ето модел, който постига същото, без да прави нищо:
const frauds = fraud.reduce((sum, y) => sum + y, 0);// „винаги нормално“: никога не спира, затова е прав за всяко честно плащанеconst accuracy = 100 * (4000 - frauds) / 4000;console.log(`плащания: 4000, измами: ${frauds}`);console.log(`„винаги нормално“: точност ${accuracy.toFixed(2)}%, хванати измами: 0 от ${frauds}`);плащания: 4000, измами: 40„винаги нормално“: точност 99.00%, хванати измами: 0 от 40Това е един месец с 4000 плащания, а измамите са 40: едно на сто. Моделът „винаги нормално“ не спира нищо. За 3960 честни плащания е прав, за 40 измами греши. Точност 99,00%, хванати измами: нула.
Точността е делът на верните отговори. Когато единият клас е 99%, моделът, който винаги казва него, е 99% точен. Числото е вярно, но не отговаря на въпроса на магазина: „Колко измами хващаме и колко честни клиенти ядосваме?“
Виж го
Месецът в код
Истинските плащания идват от базата на магазина. Тук месецът е направен по правило, за да е малък и еднакъв на всеки език. Всяко плащане е измама с вероятност 1%. Измамите обикновено са 3× до 12× обичайната сума, а понякога са малка проба (0,3× до 1,3×). Честните обикновено са 0,2× до 2,2×, а понякога са голяма покупка до 6×.
Случайните числа са от малък генератор със семе (LCG): едно и също семе дава едни и същи числа на всеки език и при всяко пускане.
// генератор със семе: еднакви числа на всеки езикclass Lcg { constructor(seed) { this.state = seed; }
next() { this.state = (this.state * 1664525 + 1013904223) % 4294967296; return this.state / 4294967296; }}// един месец плащания: около 1 на 100 е измамаconst rng = new Lcg(319);const ratios = [];const fraud = [];for (let i = 0; i < 4000; i++) { const isFraud = rng.next() < 0.01; if (isFraud) // измама: обикновено 3× до 12×, понякога малка проба ratios.push(rng.next() < 0.85 ? 3 + 9 * rng.next() : 0.3 + rng.next()); else // честно: обикновено 0.2× до 2.2×, понякога голяма покупка ratios.push(rng.next() < 0.03 ? 2.5 + 3.5 * rng.next() : 0.2 + rng.next() + rng.next()); fraud.push(isFraud ? 1 : 0);}state * 1664525 е под 2⁵³, затова числото (double) е точно, без загуба. Генераторът дава същите числа като в другите езици.Признакът е като в урока за класификацията: сумата спрямо обичайната за клиента.
Идеята
Обучение и тест
Преди да мерим каквото и да е, има по-важен въпрос: върху кои данни мерим. Ето най-простият „модел“: помни сумите на измамите, които е видял, и спира само тях. Месецът е разбъркан и разделен: 70% за обучение, 30% за проверка.
// разбъркване (Fisher–Yates) със същия генератор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(`обучение: ${train.length} плащания, ${countFrauds(fraud, train)} измами`);console.log(`тест: ${test.length} плащания, ${countFrauds(fraud, test)} измами`);// помни сумите на измамите, които е видялconst seen = new Set(train.filter((i) => fraud[i] === 1).map((i) => ratios[i]));report("наизустяване върху обучението", seen, ratios, fraud, train);report("наизустяване върху теста", seen, ratios, fraud, test);обучение: 2800 плащания, 24 измамитест: 1200 плащания, 16 измаминаизустяване върху обучението: точност 100.00%, хванати 24 от 24наизустяване върху теста: точност 98.67%, хванати 0 от 16Върху плащанията, от които се е учил, моделът е перфектен: 100%, 24 от 24. Върху нови не хваща нито една измама, защото нова измама никога не е със сума, точно равна на стара. Моделът не е научил нищо за измамите, само ги е запомнил.
Затова данните се делят на две още преди обучението:
- данни за обучение (training set): от тях моделът се учи;
- тестови данни (test set): с тях се мери накрая, и моделът не ги вижда до този момент.
Тестът замества утрешните плащания. Разбъркването преди деленето е важно: ако месецът е подреден по дата, тестът ще е само последната седмица.
Виж го
Матрицата на грешките
Сега истинският модел: логистичната регресия от урока за класификацията. Функцията е същата, само че получава и rows: кои редове да ползва. Учи се само от обучението:
// учи се само от редовете rowsfunction 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 };}Плащанията от теста могат да свършат по четири начина. Моделът спира или пуска, а плащането е измама или честно:
| моделът спира | моделът пуска | |
|---|---|---|
| измама | спряна измама (TP) | пропусната измама (FN) |
| честно | спрян честен (FP) | пуснат честен (TN) |
Това е матрицата на грешките (confusion matrix). Буквите са от английски: true/false positive/negative. „Положителен“ е класът, който търсим: измамата.
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++; // спряна измама else if (stop) fp++; // спрян честен клиент else if (fraud[i] === 1) fn++; // пропусната измама else tn++; // пуснат честен клиент } 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(`спрени измами (TP): ${tp}`);console.log(`спрени честни (FP): ${fp}`);console.log(`пропуснати измами (FN): ${fn}`);console.log(`пуснати честни (TN): ${tn}`);console.log(`точност ${accuracy.toFixed(2)}%, precision ${precision.toFixed(2)}%, recall ${recall.toFixed(2)}%`);w = 1.0702, b = -7.1054спрени измами (TP): 7спрени честни (FP): 0пропуснати измами (FN): 9пуснати честни (TN): 1184точност 99.25%, precision 100.00%, recall 43.75%Точността е 99,25%, малко над 98,67% на „винаги нормално“ върху същия тест. А моделът хваща 7 от 16 измами и не спира нито един честен клиент. Двете точности почти не се различават, а моделите са съвсем различни. Нужни са числа, които гледат измамите.
Виж го
Матрицата се пълни
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++; // спряна измама else if (stop) fp++; // спрян честен клиент else if (fraud[i] === 1) fn++; // пропусната измама else tn++; // пуснат честен клиент } return { tp, fp, fn, tn };}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);Стъпка по стъпка, с думи
- 1200 плащания от теста, които моделът не е виждал, праг 0,5. Редът е истината, колоната е какво каза моделът.
- Първо 16-те измами, една по една: 7 спрени (TP), 9 пропуснати (FN).
- После честните: 0 спрени (FP), 1184 пуснати (TN). Почти всичко е в един ъгъл.
- Точността е диагоналът: (7 + 1184) / 1200 = 99,25%. „Винаги нормално“ е 98,67% на същия тест.
- Precision е колоната „спри“: от спрените колко са измами? 7 / 7 = 100,00%.
- Recall е редът „измама“: от измамите колко хванахме? 7 / 16 = 43,75%.
- Точността гледа всичко и е висока. Precision и recall гледат измамите: всяко спиране е измама, но хващаме по-малко от половината.
1184 от 1200 плащания са в долния десен ъгъл: пуснати честни. Затова точността е висока при всеки модел, който пуска честните. Интересното е в горния ред.
Идеята
Precision и recall с думите на магазина
Два въпроса, две числа:
- Precision: от спрените плащания колко наистина са измами? TP / (TP + FP). Ниска precision значи много ядосани честни клиенти.
- Recall: от всички измами колко хванахме? TP / (TP + FN). Нисък recall значи пропуснати измами и загубени пари.
Моделът при праг 0,5 е с precision 100% и recall 43,75%. Всяко спиране е измама, но хваща по-малко от половината. При „винаги нормално“ recall е 0%, и това се вижда веднага, без да се лъжем с 99%.
Двете се дърпат. За да хванеш повече измами (recall), спираш повече съмнителни плащания, а сред тях има и честни (precision пада).
Пробвай
Попълни
Precision дели на всички спрени. На какво дели recall?
const precision = 100 * tp / (tp + fp);
const recall = 100 * tp / (tp + );Recall е TP / (TP + FN): хванатите измами делено на всички измами, хванати и пропуснати. Precision гледа колоната „спри“ в матрицата, а recall гледа реда „измама“.
Пробвай
Предвиди изхода
Десет плащания: истината (1 = измама) и какво е спрял моделът (1 = спрян). Какво ще отпечата?
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)}%`);Спрени са 4 плащания, от тях 2 са измами (TP = 2, FP = 2): precision е 2 / 4 = 50%. Измамите са 3, а хванати са 2 (FN = 1): recall е 2 / 3 ≈ 66,67%. Точността (7 от 10) е третото число, което нищо не казва за измамите.
Идеята
Прагът и цената в евро
Прагът от урока за класификацията сега има числа. Същият модел върху същия тест, при четири прага:
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(`праг ${threshold.toFixed(2)}: TP ${tp}, FP ${fp}, FN ${fn} → precision ${precision.toFixed(2)}%, recall ${recall.toFixed(2)}%`);}праг 0.50: TP 7, FP 0, FN 9 → precision 100.00%, recall 43.75%праг 0.30: TP 10, FP 1, FN 6 → precision 90.91%, recall 62.50%праг 0.10: TP 11, FP 13, FN 5 → precision 45.83%, recall 68.75%праг 0.02: TP 14, FP 28, FN 2 → precision 33.33%, recall 87.50%Колкото по-нисък е прагът, толкова повече измами хващаме (recall расте от 43,75% до 87,5%) и толкова повече честни спираме (precision пада от 100% до 33,33%). Кое е по-добре? Отговорът е в евро. Нека пропусната измама струва 250 € (стоката и таксата за върнатото плащане), а спрян честен клиент 10 € (обаждане до поддръжката и изгубено време). Цените са примерни:
// примерни цени: пропусната измама и спрян честен клиент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(`праг ${threshold.toFixed(2)}: ${fn} × ${missedCost} € + ${fp} × ${blockedCost} € = ${cost} €`); if (cost < bestCost) { bestCost = cost; best = threshold; }}console.log(`най-евтин праг: ${best.toFixed(2)}`);праг 0.50: 9 × 250 € + 0 × 10 € = 2250 €праг 0.30: 6 × 250 € + 1 × 10 € = 1510 €праг 0.10: 5 × 250 € + 13 × 10 € = 1380 €праг 0.02: 2 × 250 € + 28 × 10 € = 780 €най-евтин праг: 0.02С тези цени най-евтин е най-строгият праг: 0,02, с 780 € вместо 2250 € при 0,5. Една пропусната измама струва колкото 25 спрени клиенти, затова си струва да спираме по-често. Ако спреният клиент си тръгва завинаги и струва много повече, отговорът е друг. Това е упражнението накрая.
Едно предупреждение. Тук прагът е избран с теста. Така тестът вече участва в решение и цената от 780 € е малко по-оптимистична от истината. В истински проект прагът се избира с трети дял от данните, валидацията, а тестът се пуска веднъж, накрая. Как, е в следващия урок.
Пробвай
Намери грешката
Колега дели данните така. Числата на теста му са отлични, а в магазина моделът хваща много по-малко. На кой ред е грешката?
Избери реда с грешката.
Тестът взима първите 1200 реда, а те са и в обучението (първите 2800). Моделът се проверява на плащания, които е виждал, затова числата са твърде добри. Тестът трябва да е останалата част: от 2800 до 4000. Това е изтичане на данни (data leakage): тестът знае нещо, което утрешните плащания няма да знаят.
Идеята
Кога вреди
- Точност при неравни класове. Измами, болести, повреди, спам: щом търсеното е рядко, точността е почти безполезна. Гледай матрицата, precision и recall.
- Тест, който прилича на обучението повече от бъдещето. Измамниците сменят похватите си. Ако тестът е разбъркан с минали месеци, той е по-лесен от следващия месец. За такива данни тестът е по-новият период: учиш се до 30 септември, мериш октомври.
- Малко примери на рядкото. В теста има 16 измами. Една измама повече или по-малко мести recall с 6,25%. Числата тук са за урока, а истинските екипи събират хиляди измами, преди да вярват на втория знак.
- Едно число за всичко. Precision и recall са две, защото грешките са две и струват различно. Ако ги смачкаш в едно, губиш решението за цената.
- Тестът, пипан много пъти. Всеки път, когато сменяш модела, защото тестът е казал „лошо“, тестът става малко обучение.
По-точно, за любопитните
F1. Хармоничното средно на precision и recall: 2 · P · R / (P + R). Едно число, което пада, ако някое от двете е ниско. Полезно е за сравнение, но не знае цената на грешките.
Кривата. Ако пуснеш прага от 1 до 0 и рисуваш precision срещу recall, получаваш PR кривата. Площта под нея (PR AUC) мери модела без конкретен праг. Сродната ROC крива (дял хванати измами срещу дял спрени честни) е по-известна, но при 1 на 100 изглежда по-добре, отколкото е, защото честните са толкова много.
Прагът от цената. Ако вероятностите на модела са верни, най-евтиният праг е цената на спрения делено на сбора на двете цени: 10 / (10 + 250) ≈ 0,04. Програмата намери 0,02 от четирите прага, които пробва. Моделът тук е учен на 1% измами, затова вероятностите му са близо до истинските. Модел, научен на 40% измами (като в урока за класификацията), вижда измама твърде често и прагът му трябва да е по-висок.
Разделяне по класове. При 40 измами случайното делене може да прати 30 в обучението и 10 в теста, или обратното. Стратифицирано делене пази един и същ дял измами в двете части.
В истинския код. В scikit-learn са train_test_split (с stratify= за класовете), confusion_matrix, precision_score, recall_score и precision_recall_curve. В ML.NET TrainTestSplit и BinaryClassificationMetrics дават същото. Самото следене на модела в продукция (дали recall не пада с месеците) е част от MLOps и се учи в ZeroToOps.
Set пази числа като ключове: seen.has(3.7) е вярно само за точно същото число.Упражнение
Скъпият честен клиент
Реши задачата в редактора си.
Магазинът продава абонаменти. Клиент, спрян по погрешка, често си отива при конкурента, затова спреният честен клиент струва 60 €, а пропусната измама пак 250 €. Пусни програмата с цената със 60 € и намери най-евтиния праг.
праг 0.50: 9 × 250 € + 0 × 60 € = 2250 €праг 0.30: 6 × 250 € + 1 × 60 € = 1560 €праг 0.10: 5 × 250 € + 13 × 60 € = 2030 €праг 0.02: 2 × 250 € + 28 × 60 € = 2180 €най-евтин праг: 0.30Сега най-евтин е 0,3: спира 10 от 16 измами и само 1 честен клиент. Моделът е същият, данните са същите, а прагът е друг, защото грешките струват друго.
Още задачи
- По-гъста мрежа от прагове. Пробвай праговете от 0,02 до 0,5 през 0,02. Има ли по-евтин праг от 0,3?
- Тест по време. Вместо разбъркване вземи първите 2800 плащания за обучение и последните 1200 за тест. Колко измами има в теста сега и как се промени recall при праг 0,3?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Map и Set · Какво е машинно учене · Градиентно спускане · Класификация: измама или не
- Продължава в: Преобучение: моделът, който наизустява · Обучението от начало до край · Цифрите от касовата бележка · Оценяване: тестов набор за асистента
- Задачи по темата: Как учи модел