ROC-криві. Оглядова стаття

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Я Data Scientist уже майже 10 років, працювала над розробкою, впровадженням і моніторингом сотень моделей, над рекомендаціями з model governance для різних компаній. Нині я Staff Data Scientist у компанії thredUP, до цього займалася Data Science Consulting.

Ця стаття буде корисна тим, хто хоче краще розуміти метрики, важливі для оцінювання ML моделей.

Що таке ROC крива

Можна було би класично почати із означення ROC-кривої, переказати своїми словами Вікіпедію і потім перестрибнути одразу до прикладів використання. Але мені дуже хочеться піти дещо іншим шляхом і почати з практичної сторони питання.

Уявіть, що ви як ML engineer або Data Scientist побудували модель. Для простоти візьмемо дані із Titanic Competition на Kaggle.

import pandas as pd
df = pd.read_csv('train.csv')
df.sample(5)

Датасет — це перелік пасажирів Титаніку з даними щодо пасажирів. Колонка Survived показує, вижив пасажир чи ні (1 — вижив, 0 — ні). 38% пасажирів вижили.

print("% of people survived on Titanic is "+ str("{:.0%}".format(df['Survived'].mean())))

Побудуємо найпростішу логістичну модель, використовуючи пакет sklearn. Я свідомо, не розбиватиму дані на тест і трейн, щоб спростити читання коду і сфокусуватись на темі статті.

from sklearn.linear_model import LogisticRegression
X = df[['Pclass', 'SibSp','Parch', 'Fare']].values
y = df['Survived'].values
clf = LogisticRegression(random_state=0,solver = 'lbfgs').fit(X, y)

Після того, як модель побудована, подивимось на результати прогнозу, зроблені моделлю.

clf.predict(X)

Що означає цей масив? Що для кожного користувача модель спрогнозувала бінарну відповідь: виживе людина на Титаніку чи ні. У цей момент постає одне очевидне і одне трохи менш очевидне але, можливо, навіть більш важливе, питання:

  1. Наскільки точна модель?
  2. Як нею користуватись?

Щоб відповісти на перше питання, зазвичай будують матрицю невідповідностей і порівнюють справжній клас із прогнозованим.

from sklearn.metrics import plot_confusion_matrix
plot_confusion_matrix(clf, X, y)  

Матриця невідповідностей дозволяє нам порахувати, для скількох людей наше передбачення було правильним:

accuracy = (461+152)/(461+152+190+88) =0.69

Наша модель правильно спрогнозувала виживання для 69% людей. Це перша і найбільш очевидна метрика, якою часто користуються у задачах класифікації. Проте якщо задуматися добре це чи погано, одразу виникає багато питань. Власне, ми підходимо до наступного питання: усе залежить від того, як ви плануєте користуватись на практиці. Давайте уявимо, що надворі 1912 рік і ви власник корабля, ідентичного до Титаніку. Уряд зобов’язав вас при продажі квитків вказувати шанси виживання у разі катастрофи для кожного пасажира, що купує квиток. Ви плануєте користуватись описуваною моделлю, щоб задовольнити вимоги уряду. Які практичні питання можуть з’явитися?

1. Заможні люди чули, що у каютах першого класу вижили набагато більш людей. Ваша модель це в цілому підтверджує і при продажі квитків у перший клас більшість пасажирів отримує прогноз “1” - will survive. Але пасажири знають, що модель не точна. Вони цікавляться, як часто модель правильно прогнозує, що людина виживе, або вони цікавляться True Positive Rate (або Recall).

True Positive Rate = True Positives / (True Positives + False Negatives) = 152/(152+190) = 0.44

Тобто якщо пасажир вижив на Титаніку, модель правильно це спрогнозує тільки у 44% випадків.

2. Пасажири обурені низькою точністю моделі. Але ви переконуєте їх, що модель не така погана. Ви показуєте, як часто модель передбачає, що пасажир виживе, а насправді він не виживає. Іншими словами, ви показуєте який у вашої моделі False Positive Rate (або помилка першого роду). Давайте порахуємо.

False Positive Rate =False Positives / False Positives + True Negatives = 88/(88+461) =0.16

Тобто ймовірність того, що пасажир загине, якщо ми передбачили, що він виживе, 16%. Це трохи заспокоює пасажирів першого класу.

3. Люди, що змушені подорожувати третім класом не можуть купити дорогі квитки, тому коли вони дізналися True Positive Rate і False Positive Rate, вони починають сумніватися, чи здатна модель взагалі визначити, хто виживе на кораблі. На їхні прохання ви вираховуєте Positive Predictive Power моделі (або Precision).

Positive Predictive Power =True PositivesTrue Positives + False Positives = 152 / (152 + 88) =0.63

Тобто якщо модель прогнозує виживання, вона має рацію у 63% випадків. Інтуїтивно не зрозуміло, чому цей результат відрізняється від загальної точності моделі (69%). Загальна точність показує можливість моделі прогнозувати обидва класи (виживе/не виживе) і часто трапляється так, що модель краще розпізнає належність до одного конкретного класу.

4. Уявіть, що після того, як ви показали більше даних щодо точності моделі, уряд попросив замість бінарної відповіді 1/0 надавати пасажирам ймовірність виживання. Тут час задуматись, чому модель повертає 1 або 0. Насправді логістична регресія повертає не бінарну відповідь, а ймовірність належності об’єкта до кожного з класів p у вигляді числа в інтервалі [0;1]. У випадку бінарної класифікації це зазвичай ймовірність належності до класу поміченого «1» (хоча завжди варто звертати увагу на деталі імплементації кожного конкретного алгоритму у різних мовах). Для того, щоб перетворити ймовірність належності об’єкта у бінарну відповідь, застосовується поріг класифікації.

if p <threshold than 0 else 1

Sikit-learn за замовчуванням використовує поріг 0.5. Проте застосування дефолтного порогу зовсім не обов«язково оптимальне. Більше того, у багатьох випадках поріг може бути взагалі не потрібен і достатньо оперувати ймовірністю належності до одного з класів. Ось як виглядатимуть ймовірності виживання для пасажирів Титаніку, передбачені нашою моделлю:

clf.predict_proba(X)

У нашому випадку ми можемо уявити, що повернути ймовірність виживання може бути кориснішим, ніж повертати прогноз.

Тепер порахуємо True Positive Rate та False Positive Rate для кожного порогу класифікації від 0 до 1 і зобразимо результати на графіку. Отримана крива називатиметься Receiver Characteristic Curve або просто ROC curve

from sklearn.metrics import roc_curve, RocCurveDisplay
fpr, tpr, _ = roc_curve(y, clf.predict_proba(X)[:, 1], pos_label=1)
roc_display_clf = RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

Перевага використання такої кривої — можливість абстрагуватися від використання конкретного порогу і оцінити, наскільки гарно модель здатна відсортувати об’єкти у вибірці залежно від ймовірності належності до кожного класу.

Уявімо модель, яка випадковим чином відносить об’єкти до того чи іншого класу. Апріорі така модель не здатна нічого спрогнозувати. Як виглядатиме ROC-крива для такої моделі?

import numpy as np
random_pred = np.random.uniform(low=0, high=1, size=(len(y),))
random_pred

from sklearn.metrics import roc_curve, RocCurveDisplay
fpr, tpr, _ = roc_curve(y, random_pred, pos_label=1)
roc_display_random = RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

Як бачимо, ROC крива для такої моделі — це діагональна лінія, яка означає, що для будь-якого порогу класифікації у нас буде однаковий відсоток False Positive Rate і True Negative Rate.

А як виглядатиме крива для ідеальної моделі, яка правильно класифікує усі об’єкти?

fpr, tpr, _ = roc_curve(y, y, pos_label=1)
roc_display_ideal = RocCurveDisplay(fpr=fpr, tpr=tpr).plot()

У такої моделі True Positive Rate завжди дорівнює одиниці, а False Negative Rate дорівнює нулю.

Побудуємо усі ROC-криві на одному графіку.

import matplotlib.pyplot as plt
ax = plt.gca()
roc_display_clf.plot(ax=ax)
roc_display_random.plot(ax=ax)
roc_display_ideal.plot(ax=ax)
plt.show()

Як бачимо, ROC-крива для нашої моделі класифікації віддалена від кривої для випадкової моделі, але все ще досить далеко від кривої для «ідеальної» моделі.

Коли ми створюємо модель класифікації, нам хочеться, щоб модель працювала краще, ніж модель, яка ділить об’єкти за класами випадковим чином. У реальних застосунках досягнути ідеальної точності класифікації неможливо. Якщо обчислити площу під кривою, можна зрозуміти міру її «хорошості»: чим далі крива від діагональної лінії, тим вона краща. Площу під ROC кривою називають Area Under Curve бо скорочено AUC.

AUC — дуже популярна і універсальна метрика вимірювання якості моделей класифікації. Вона варіюється між 0.5 і 1, де 0.5 — площа під кривою, що описує «рандомну» модель, а 1 — площа під кривою, що описує «ідеальну» модель.

Давайте порахуємо площу під кожною з кривих.

from sklearn.metrics import roc_auc_score
print("Area under curve for random model " + str(roc_auc_score(y, random_pred)))
print("Area under curve for survival prediction model " + str(roc_auc_score(y, clf.predict_proba(X)[:, 1])))
print("Area under curve for ideal model " + str(roc_auc_score(y, y)))

Як бачимо, AUC побудованої нами моделі складає 0.7. Це свідчить про посередню якість прогнозу. На практиці таку модель можна було би застосовувати у деяких випадках, але дуже бажано було би підвищити AUC хоча би до 0.8. На Kaggle наразі є моделі для Titanic challenge AUC яких складає 0.99.

Які види ROC-кривих існують і де їх застосовують

ROC-крива

У попередньому розділі ми детально розглянули Receiver Operating Curve, криву, що описує залежність True Positive Rate від False Positive Rate. Це дуже поширена, класична ROC-крива, що застосовується у машинному навчанні. Давайте розглянемо коли її доцільно застосовувати і наведемо декілька прикладів із бізнесу.

Застосування у Machine Learning

ROC-криву часто застосовують для вимірювання якості моделі класифікатора. Відсутність необхідності обирати поріг для віднесення об’єкта до одного із класів робить AUC зручною метрикою. AUC часто застосовують як критерій при виборі оптимальних параметрів для моделі класифікатора. ROC можна побудувати для будь-якої моделі класифікації, що повертає не бінарну відповідь, а ймовірність належності до кожного класу. У своїй роботі я постійно використовую AUC при використанні логістичної регресії, ensemble models (Xgboost, Random Forest, Gradient Boosting), Neural Networks.

Цікаве питання використання ROC для мультиклас-моделей. Типове рішення — розглядати точність класифікації у форматі «один проти всіх», коли послідовно розглядається точність класифікації кожного класу, а належність до всіх інших класів трактується як не-належність до конкретного класу. Іншими словами, для кожного класу ми розглядаємо модель як бінарну. Такий підхід досить поширений і дозволяє дата саєнтисту відносно легко трактувати результати. Проте варто пам’ятати, що високий AUC для певного класу не гарантує, що об’єкти будуть віднесені до цього класу, оскільки ймовірність потрапляння до інших класів може бути вища.

Існує інший підхід, коли замість кривої будується площина (Receiver Operator Surface) і обчислюється об’єм під площиною, розмірність якої співпадає з кількістю класів моделі. Таке узагальнення гарно обґрунтовується з математичної точки зору. На практиці я поки що не зустрічала частого використання Receiver Operator Surface, ймовірно через труднощі інтерпретації такої метрики.

Precision-Recall крива

Класичну ROC криву доцільно застосовувати коли класи умовно збалансовані. Якщо відсоток одного з класів дуже маленький (наприклад 3% — 6% задачах конверсії), метрика AUC не врахує в достатній мірі, чи здатна модель розпізнати мінорний клас. У тому випадку, коли кількість подій складає невеликий відсоток у вибірці, ми зазвичай менше зацікавлені у розпізнаванні «нулів», тобто ситуацій, коли подія не трапилась. Для таких задач True Negative Rate буде великим і, відповідно, False Positive Rate — низьким, але це не свідчитиме про те, що модель буде корисною для вирішення практичної задачі. В таких ситуаціях доцільніше орієнтуватися на здатність моделі розпізнавати позитивні випадки (Positive Predictive Power, Precision) і True Positive Rate (Recall). Ці дві метрики допомагають оцінити, наскільки модель здатна виявити об’єкти, що належать до мінорного класу, або, іншими словами, наскільки модель здатна виявити подію, що рідко трапляється.

Давайте побудуємо Precision-Recall криву для нашої моделі виживання.

from sklearn.metrics import precision_recall_curve
from sklearn.metrics import PrecisionRecallDisplay
prec, recall, _ = precision_recall_curve(y, clf.predict_proba(X)[:, 1])
                                         
pr_display_clf = PrecisionRecallDisplay(precision=prec, recall=recall).plot()

Як бачимо, на відміну від класичної ROC-кривої, Precision-Recall крива не монотонна. Це пов’язано з тим, що Recall завжди монотонно зростає при зростанні порогу класифікації, а Precision може як зростати, так і падати.

Давайте побудуємо PR-криві для «випадкової» та «ідеальної» моделі.

Для «випадкової» моделі PR крива буде прямою лінією, оскільки для будь-якого значення порогу класифікації відсоток True Positives буде незмінним і дорівнюватиме відсотку одиничок у вибірці. У випадку Titanic датасету Precision складатиме 38% (як ми обчислили на початку статті).

prec, recall, _ = precision_recall_curve(y, random_pred)
                                         
pr_display_rand = PrecisionRecallDisplay(precision=prec, recall=recall).plot()

Для «ідеальної» моделі відсоток True Positives для будь-якого порогу складатиме 100%, тому PR-крива також буде прямою лінією.

prec, recall, _ = precision_recall_curve(y, y)
                                         
pr_display_ideal = PrecisionRecallDisplay(precision=prec, recall=recall).plot()

Зобразимо PR-криві для усіх моделей на одному графіку

import matplotlib.pyplot as plt
ax = plt.gca()
pr_display_rand.plot(ax=ax)
pr_display_clf.plot(ax=ax)
pr_display_ideal.plot(ax=ax)
plt.show()

Як видно з графіку, PR-крива побудованого нами класифікатора знаходиться між кривими для «випадкової» та «ідеальної» моделей.

Аналогічно з тим, як ми оцінювали точність моделі обчислюючи площу під ROC-кривою, можна оцінювати точність моделі, обчисливши площу під PR-кривою. Давайте обчислимо площу під PR-кривою для «випадкової» моделі, нашої моделі та «ідеальної» моделі.

precision, recall, thresholds = precision_recall_curve(y, random_pred)
print("Area under PR curve for random model " + str(str(metrics.auc(recall,precision))))
precision, recall, thresholds = precision_recall_curve(y, clf.predict_proba(X)[:, 1])
print("Area under PR curve for survival prediction model " + str(metrics.auc(recall,precision)))
precision, recall, thresholds = precision_recall_curve(y, y)
print("Area under PR curve for ideal model " + str(str(metrics.auc(recall,precision))))

Як бачимо, у нашому прикладі площа під PR-кривою більша, ніж площа під кривою, що описує «випадкову» модель. Модель має середні показники точності. Проте для моделі з незбалансованими класами класична ROC-крива може давати надто оптимістичну оцінку і варто завжди будувати не тільки ROC, а і PR-криву.

Приклади бізнес задач

ROC-криву почали використовувати у теорії розпізнавання сигналів у часи Другої cвітової війни. Тоді вона допомагала оцінити точність виявлення літаків противника на радарі. Після цього ROC-криву почали використовувати у медицині для аналізу точності діагностичних тестів і у психофізиці. Зараз ROC-крива інтенсивно використовується у дослідженнях, пов’язаних з доказовою медициною та епідеміологією.

У машинному навчанні ROC-криві застосовуються для оцінювання моделей класифікації у різних індустріях, наприклад:

  • моделі кредитного скорингу;
  • моделі прогнозу конверсії користувачів;
  • розпізнавання несправностей на вишках мобільних операторів;
  • прогноз того, чи перестане клієнт користуватися сервісом/ продовжить підписку;
  • модель виявлення діагнозів у медицині.

Список можна продовжувати до нескінченності, оскільки застосування моделей класифікації тільки зростає.

Висновки

ROC-криві — популярний і зручний засіб оцінювання моделей класифікації. ROC-криві дають змогу оцінювати і порівнювати між собою моделі класифікації, не прив’язуючись до конкретного порогу класифікації. Кожному Data Scientist, що тільки починає роботу варто розібратися і почати використовувати ROC-криві. Розібратися з ними зовсім не складно, математичний апарат, необхідний для цього, практично на шкільному рівні.

Що почитати

  1. How to Use ROC Curves and Precision-Recall Curves for Classification in Python/
  2. Demystifying ROC Curves
  3. Confusion Matrix
  4. Sensitivity and specificity
  5. Type I and type II errors
  6. ROC Curves for Continuous Data
👍ПодобаєтьсяСподобалось3
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Классная статья!

Спасибо за:
1) Код без лишнего — читается как просто текст, позволяет сконцентрироваться на поднятой теме.
2) Теорию по кривой ROC рассказывают во многих статьях типа «фундаментальная классификация с помощью нейронных сетей». Но вы раскрыли темы, о которых я раньше не думал, или думал но не мог сформулировать словами.
3) Что вы рассказали о применении для мультиклас-моделей и «Receiver Operator Surface».

Что бы хотелось еще увидеть:
1) Интересный кейс из вашей практики.

Тимофей, спасибо за отзыв! Цель статьи была сделать обзорный материал, поэтому я старалась сделать, по возможности, простые общие примеры. Из интересного из личной практики: были случаи когда модели давали сходный ROC AUC для данных без большой расбалансировки ( примерно 25% на 75%) но оказывалось что одна модель лучше классифицирует важный класс ( из-за разных фич или разных алгоритмов). В таких случаях важно было смотреть на PR AUC либо просто строить графики с процентом целевой в бинах скора ( такие графики проще обьяснить бизнесу чем ROC)

Підписатись на коментарі