Класификация: измама или не
Накратко: Двоичната класификация отговаря с „да“ или „не“: логистичната регресия свива числото със сигмоида до вероятност между 0 и 1, а прагът решава.
Проблем
Измама или не
Клиентът натиска „Плати“. Магазинът има десетина милисекунди да реши: пуска ли плащането, или го спира за проверка. Ако пусне измама, губи стоката и плаща такса за върнатото плащане. Ако спре честен клиент, той се ядосва и купува другаде.
Тук отговорът не е число като времето за доставка, а „измама“ или „не“. Имаме 20 минали плащания. Признакът е сумата спрямо обичайната за клиента: 3.0 значи „три пъти повече от обикновено“. Етикетът е 1 за измама и 0 за честно плащане:
# сумата спрямо обичайната за клиента: 3.0 е три пъти повечеratios = [0.4, 0.6, 0.7, 0.9, 1.0, 1.1, 1.2, 1.3, 1.5, 2.0, 2.6, 3.2, 0.8, 3.6, 4.1, 5.0, 6.0, 7.5, 8.2, 9.0]# 1 = измама, 0 = честно плащанеfraud = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]Първата идея е правата от линейната регресия: етикетите са 0 и 1, а над 0,5 спираме. Научена със същото спускане:
line = train_line(ratios, fraud, 0.01, 5000)print(f"права, плащане 0.4×: {line[0] * 0.4 + line[1]:.2f}")print(f"права, плащане 9.0×: {line[0] * 9.0 + line[1]:.2f}")# малка стъпка: 40× е голямо числоratios2 = ratios + [40.0]fraud2 = fraud + [1]moved = train_line(ratios2, fraud2, 0.01, 5000)report("права", line, ratios2, fraud2)report("права с плащането 40×", moved, ratios2, fraud2)права, плащане 0.4×: 0.03права, плащане 9.0×: 1.24права: 0.5 при 3.75×, пропуснати измами: 2 от 9права с плащането 40×: 0.5 при 7.31×, пропуснати измами: 5 от 9Правата казва 1,24 за плащане 9×. Това не е вероятност: „124% измама“ няма смисъл. По-лошото е долу. Пристига една измама от 40× обичайната сума. Тя е очевидна, а накланя правата толкова, че границата ѝ отива от 3,75× на 7,31×. Сега правата пропуска 5 от 9 измами, вместо 2. Една ясна измама направи модела по-лош за всички останали.
Идеята
Отговорът е клас
Когато моделът избира между няколко отговора („измама“ или „честно“, „лош“ или „добър“ отзив), задачата се казва класификация, а отговорите са класове. При линейната регресия отговорът беше число на права линия. Тук искаме друго: число между 0 и 1, което казва колко вероятна е измамата. После решаваме с него.
Нужни са две неща:
- функция, която свива всяко число между 0 и 1, без да реже;
- граница, над която спираме плащането.
Идеята
Сигмоидата
Сигмоидата е sigmoid(z) = 1 / (1 + e−z). Голямо z я прави почти 1, голямо отрицателно z я прави почти 0, а z = 0 дава точно 0,5:
def sigmoid(z): return 1 / (1 + math.exp(-z))for z in range(-4, 5, 2): print(f"z = {z}: {sigmoid(z):.2f}")z = -4: 0.02z = -2: 0.12z = 0: 0.50z = 2: 0.88z = 4: 0.98math.exp(x) е ex от модула math. Програмата започва с import math.Колкото и голямо да е z, резултатът не минава 1. Затова плащането от 40× не може да „дръпне“ кривата като правата: за него сигмоидата вече казва почти 1 и няма какво повече да поправя.
Виж го
Логистична регресия
Моделът е същата права, но пусната през сигмоидата. Първо смята оценка (score): w · x + b, като в линейната регресия. После сигмоидата я прави вероятност p. Накрая прагът решава: p ≥ 0,5 значи „спри“.
def sigmoid(z): return 1 / (1 + math.exp(-z))
def block(ratio, w, b, threshold): score = w * ratio + b p = sigmoid(score) return p >= thresholdТова е логистична регресия. Името е подвеждащо: тя не предсказва число, а клас. „Регресия“ е заради правата вътре.
С научените w и b (как, е в следващата карта) за три нови плащания:
w, b = train_logistic(ratios, fraud, 0.1, 3000)for ratio in [1.0, 3.0, 5.0]: p = sigmoid(w * ratio + b) verdict = "спри" if block(ratio, w, b, 0.5) else "пропусни" print(f"{ratio:.1f}×: вероятност за измама {p:.2f} → {verdict}")1.0×: вероятност за измама 0.06 → пропусни3.0×: вероятност за измама 0.46 → пропусни5.0×: вероятност за измама 0.91 → сприПлащане 3× е 0,46: подозрително, но под прага. Числото между 0 и 1 е полезно и само по себе си. Магазинът може да пусне 0,46 без въпроси, да поиска потвърждение от банката (3-D Secure) при 0,6 и да спре 0,95.
Виж го
Учи се със същото спускане
Как се намират w и b? Точно както в урока за градиентното спускане: започваме от нула, смятаме колко греши моделът за всяко плащане и местим теглата малко срещу наклона. Ето обучението на правата от проблема и до него логистичното:
def train_line(xs, ys, rate, epochs): w, b = 0.0, 0.0 n = len(xs) for epoch in range(epochs): gw, gb = 0.0, 0.0 for x, y in zip(xs, ys): p = w * x + b gw += (p - y) * x gb += p - y w -= rate * gw / n b -= rate * gb / n return w, bdef train_logistic(xs, ys, rate, epochs): w, b = 0.0, 0.0 n = len(xs) for epoch in range(epochs): gw, gb = 0.0, 0.0 for x, y in zip(xs, ys): p = sigmoid(w * x + b) # единственият различен ред gw += (p - y) * x gb += p - y w -= rate * gw / n b -= rate * gb / n return w, bРазличен е само един ред: предсказанието минава през сигмоидата. Стъпката (p − y) · x е същата. Ако измамата е казала 0,9, грешката е малка и теглата почти не мърдат. Ако е казала 0,1, грешката е голяма и стъпката също.
За да видим, че модел се учи, мерим грешката му. При „да“ и „не“ мярката е log loss: колко изненадан е моделът от верния отговор. Защо не средната квадратна грешка, е в „По-точно“ накрая.
def log_loss(xs, ys, w, b): total = 0.0 for x, y in zip(xs, ys): p = sigmoid(w * x + b) total -= math.log(p) if y == 1 else math.log(1 - p) return total / len(xs)for epochs in [10, 100, 1000, 3000]: w, b = train_logistic(ratios, fraud, 0.1, epochs) loss = log_loss(ratios, fraud, w, b) print(f"епоха {epochs}: грешка {loss:.4f}, w = {w:.4f}, b = {b:.4f}")епоха 10: грешка 0.5895, w = 0.2139, b = -0.1837епоха 100: грешка 0.3899, w = 0.5246, b = -1.4580епоха 1000: грешка 0.2906, w = 1.1696, b = -3.6490епоха 3000: грешка 0.2897, w = 1.2624, b = -3.9459Грешката пада и спира около 0,29. Теглата стигат w = 1,26 и b = −3,95. Сигмоидата минава 0,5, където оценката е 0, тоест при x = −b / w ≈ 3,13×. А плащането от 40× от проблема?
ratios2 = ratios + [40.0]fraud2 = fraud + [1]report("сигмоида", train_logistic(ratios, fraud, 0.1, 3000), ratios2, fraud2)report("сигмоида с плащането 40×", train_logistic(ratios2, fraud2, 0.1, 3000), ratios2, fraud2)сигмоида: 0.5 при 3.13×, пропуснати измами: 1 от 9сигмоида с плащането 40×: 0.5 при 3.13×, пропуснати измами: 1 от 9Границата не се мести. Пропусната остава само измамата от 0,8×: малко плащане, с което крадецът проверява дали картата работи. По сумата тя не се отличава от честните и никой праг по тази сума няма да я хване. Трябва друг признак (нова държава, ново устройство, нощ).
Виж го
Сигмоидата и прагът
def sigmoid(z): return 1 / (1 + math.exp(-z))
def block(ratio, w, b, threshold): score = w * ratio + b p = sigmoid(score) return p >= thresholdСтъпка по стъпка, с думи
- 20-те плащания: отляво надясно е сумата спрямо обичайната. Измамите са горе (1), честните долу (0).
- Права през нулите и единиците: при 9× казва 1,24. Това не е вероятност.
- Сигмоидата огъва правата между 0 и 1. Тя минава 0,5, където оценката е 0: при 3,13×.
- Всяко плащане получава вероятност и сяда на кривата. Червените са измами, зелените са честни.
- Праг 0,5: над него спираме, под него пускаме. Пропуснати измами: 1, спрени честни клиенти: 1.
- Праг 0,3 е по-строг: пропуснати измами: 1, спрени честни клиенти: 2.
- Праг 0,7 е по-мек: пропуснати измами: 2, спрени честни клиенти: 0.
- Моделът дава вероятност, а прагът е решение: по-нисък хваща повече измами и спира повече честни клиенти.
Правата излиза над 1 и под 0. Сигмоидата остава между тях, а хоризонталната линия на прага реже плащанията на „спри“ и „пусни“. Когато прагът слезе, по-малко измами минават, но се спират и повече честни клиенти.
Пробвай
Попълни
Кой е единственият ред, който прави от обучението на правата логистична регресия?
for x, y in zip(xs, ys):
p = (w * x + b)
gw += (p - y) * x
gb += p - yПредсказанието минава през sigmoid, затова p е между 0 и 1. Останалото е същото спускане: наклонът е (p − y) · x.
Пробвай
Предвиди изхода
Моделът е с w = 1,26 и b = −3,95, а прагът е 0,5. Какво ще отпечата за плащания 1,5×, 3,0× и 3,5×?
w, b = 1.26, -3.95for ratio in [1.5, 3.0, 3.5]: print("спри" if block(ratio, w, b, 0.5) else "пропусни")Прагът 0,5 е там, където оценката w · x + b е 0: при x = 3,95 / 1,26 ≈ 3,13. Плащания 1,5× и 3,0× са под тази граница, а 3,5× е над нея. Не е нужно да смяташ сигмоидата: p ≥ 0,5 точно когато оценката е ≥ 0.
Идеята
Прагът е бизнес решение
Моделът дава вероятност. Какво правим с нея, решава прагът, а него не го учи моделът. Ето същите 20 плащания при четири прага:
for threshold in [0.3, 0.5, 0.7, 0.9]: missed, blocked = 0, 0 for ratio, y in zip(ratios, fraud): stop = block(ratio, w, b, threshold) if y == 1 and not stop: missed += 1 if y == 0 and stop: blocked += 1 print(f"праг {threshold:.1f}: пропуснати измами {missed}, спрени честни {blocked}")праг 0.3: пропуснати измами 1, спрени честни 2праг 0.5: пропуснати измами 1, спрени честни 1праг 0.7: пропуснати измами 2, спрени честни 0праг 0.9: пропуснати измами 3, спрени честни 0Нисък праг е строг: при 0,3 пропускаме 1 измама, но спираме 2 честни клиенти. Висок праг е мек: при 0,9 не спираме нито един честен, но пропускаме 3 измами. Няма праг без грешки, защото измамите и честните плащания се застъпват.
Кой праг е верният? Зависи колко струва всяка грешка. Пропусната измама за 300 € струва 300 € и таксата. Спрян честен клиент струва един ядосан човек, обаждане до поддръжката и понякога изгубен клиент. Как се сравняват двете с числа, когато измамите са 1 на 100, е следващият урок.
Пробвай
Намери грешката
Колега пише сигмоидата направо в block. Тестовете му са с измами и минават. В петък магазинът спира всяко плащане, и честните. На кой ред е грешката?
Избери реда с грешката.
Делението е преди събирането: 1 / 1 + e^(−score) е 1 + e−score, а то винаги е над 1. Така p ≥ прага при всяко плащане. Скобите са задължителни: 1 / (1 + ...). Тестове само с измами не могат да хванат модел, който спира всичко. Трябва и честни плащания в тестовете.
Идеята
Кога вреди
Логистичната регресия е добро първо решение: бърза е, лесно се обяснява („спряхме, защото сумата е 6 пъти обичайната“) и дава вероятност. Ето кога не стига или вреди:
- Границата е една. Моделът спира над един праг по претеглена сума от признаците. Ако опасното е в средата (нито много малки, нито много големи суми), една граница не може да го отдели. Трябва друг признак или друг модел, например дърво или невронна мрежа (следващият модул).
- Вероятността е толкова вярна, колкото данните. Тук измамите са 8 от 20, защото така имаме от какво да учим. В истинския поток са около 1 на 100. Модел, научен на 40% измами, вижда измама твърде често. Това е следващият урок.
- Правило, което е закон, не се учи. „Не приемаме карти от държава под санкции“ е
if, а не модел. Моделът е за това, което не можеш да напишеш като правило. - Признак, който не съществува в момента на решението. „Клиентът оспори плащането“ е чудесен признак за измама, но идва седмици след плащането. Ако е в данните за обучение, моделът изглежда отличен, а в момента на решението е сляп.
- Решенията за хора. Кредит, работа, застраховка: там моделът трябва да е честен към групи хора и да може да се обясни. Това е отделна тема, и често закон.
По-точно, за любопитните
Защо log loss, а не MSE. Log loss за едно плащане е −log(p), ако е измама, и −log(1 − p), ако е честно. Ако моделът каже 0,01 за истинска измама, грешката е −log(0,01) ≈ 4,6. При MSE същата грешка е само (1 − 0,01)² ≈ 0,98. Log loss наказва увереното грешно много по-силно. А наклонът на log loss по w е точно (p − y) · x, затова кодът е толкова кратък. Със сигмоида и MSE наклонът има още един множител p · (1 − p), който става почти 0, когато моделът е уверен и греши: ученето спира точно когато трябва да е най-бързо.
Защо стъпките са различни. Правата е научена със стъпка 0,01, а логистичната регресия с 0,1. При правата грешката расте с x, а плащането от 40× дава огромен наклон: при стъпка 0,1 правата се разлита. При сигмоидата p − y е между −1 и 1, затова и голямо x не я разлюлява толкова.
Повече признаци. С няколко признака (сумата, нощ ли е, ново ли е устройството) оценката е w₁ · x₁ + w₂ · x₂ + w₃ · x₃ + b, а кодът е същият с масиви от тегла. Границата е права в равнина, или равнина в пространство: пак една.
Повече от два класа. За „добър / неутрален / лош“ има по една оценка за всеки клас, а softmax ги превръща във вероятности със сбор 1. Ще го срещнеш при езиковите модели: те избират следващия токен точно така, между десетки хиляди класове.
В истинския код. В scikit-learn това е LogisticRegression, в ML.NET е SdcaLogisticRegression, в Tribuo (Java) е LogisticRegressionTrainer. Вътре е същата идея с по-умно спускане и с регуляризация, която ще видиш два урока по-нататък.
Упражнение
Лош ли е отзивът
Реши задачата в редактора си.
Под всеки продукт има отзиви. Магазинът иска лошите да стигат до поддръжката веднага. Признакът е колко думи от списък („счупен“, „върнах“, „закъсня“…) има в отзива. Имаш 18 отзива: броят на лошите думи е 0, 0, 1, 0, 2, 0, 1, 0, 1, 3, 2, 3, 4, 1, 5, 3, 2, 4, а етикетите (1 = лош отзив) са десет нули, после осем единици. Научи логистична регресия със стъпка 0,5 за 2000 епохи и отпечатай вероятността и решението (праг 0,5) за отзив с 0 до 5 лоши думи.
лоши думи: 0 → 0.05, добърлоши думи: 1 → 0.20, добърлоши думи: 2 → 0.52, лошлоши думи: 3 → 0.83, лошлоши думи: 4 → 0.96, лошлоши думи: 5 → 0.99, лошОтзив с 2 лоши думи е на ръба: 0,52. Там са и добрите отзиви, които казват „не е счупен, но закъсня“.
Още задачи
- Различен праг. Поддръжката иска да вижда всичко съмнително. Кой праг пуска до нея и отзивите с 1 лоша дума?
- Нов признак. Добави звездите (1 до 5) като втори признак: w₁ · думи + w₂ · звезди + b. Колко се смени вероятността при 2 лоши думи и 5 звезди?
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Параметри и return · Какво е машинно учене · Линейна регресия · Градиентно спускане
- Продължава в: Оценка: точността лъже · Неврон · Слоеве · Обратно разпространение
- Задачи по темата: Как учи модел