Логистическая регрессия и вероятностный ответ¶
Автор: Алексей Чернуха
CC BY 4.0.
Выполняйте ячейки сверху вниз. Сначала решите задачи, затем обратитесь к решениям.
Чему научимся¶
Хороший спам-фильтр не просто объявляет письмо «спамом» или «не спамом». Он оценивает, насколько уверен: «это спам с вероятностью 95%», «а это — всего с вероятностью 55%». Такая уверенность полезна: первое письмо можно сразу убрать, а второе оставить во входящих и пометить как подозрительное.
Это оценка модели, а не обещание, что среди любых ста писем с оценкой 95% ровно 95 окажутся спамом. Проверять такие оценки нужно на новых размеченных данных.
После занятия вы сможете:
- Посчитать вероятность класса по признакам объекта с помощью логистической регрессии.
- Объяснить, как порог меняет ошибки, и посчитать accuracy, precision и recall.
- Сравнить логистическую регрессию и решающее дерево на одинаковых данных.
Время и подготовка: ориентир 8–10 часов с практикой. Нужны линейная сумма из 1.3, дроби, доли и основы Python. Сигмоида и порог составляют основной маршрут; логарифмическая ошибка и производная ниже выделены как углубление.
План занятия¶
- На одном письме пройдём путь от признаков до оценки вероятности спама.
- Превратим вероятность в решение, передвинем порог и посчитаем ошибки.
- На отдельном наборе сравним логистическую регрессию и дерево.
От признаков письма к вероятности¶
Пусть фильтр смотрит на два признака письма:
- $x_1$ — сколько ссылок в тексте;
- $x_2$ — знаком ли отправитель: 1, если он есть в контактах, и 0, если нет.
Ответ тоже запишем числом: спам — $y=1$, обычное письмо — $y=0$. Это два класса.
Идея простая: каждый признак вносит свой вклад в «подозрительность» письма, а вклады складываются. Вклад признака — это сам признак, умноженный на его вес. К сумме добавляют ещё свободный член $b$ — число, с которого начинается счёт. Получается оценка письма:
$$z=b+w_1x_1+w_2x_2.$$
Допустим, обучение дало такие числа: $b=-2$, $w_1=1{,}2$, $w_2=-1{,}5$. Тогда
$$z=-2+1{,}2\cdot x_1-1{,}5\cdot x_2.$$
Каждое слагаемое читается отдельно:
- $-2$ — стартовое значение: письмо без ссылок от незнакомого отправителя выглядит скорее обычным;
- $+1{,}2\cdot x_1$ — каждая ссылка добавляет 1,2 к подозрительности;
- $-1{,}5\cdot x_2$ — знакомый отправитель убавляет её на 1,5.
Возьмём письмо с тремя ссылками от незнакомого отправителя: $x_1=3$, $x_2=0$. Подставляем:
$$z=-2+1{,}2\cdot3-1{,}5\cdot0=-2+3{,}6=1{,}6.$$
Оценка 1,6 — ещё не вероятность. Вероятность лежит между 0 и 1, а оценка может быть любым числом: и −50, и 700. Нужен способ уложить любое число в промежуток от 0 до 1.
Обозначим через $p$ вероятность того, что письмо — спам. Получить $p$ из оценки $z$ помогает сигмоида:
$$p=\sigma(z)=\frac{1}{1+e^{-z}}.$$
Рисунок 1 — Преобразование оценки модели в вероятность с помощью сигмоиды
Сигмоида берёт любое число и сжимает его в промежуток от 0 до 1. Чем больше $z$, тем ближе $p$ к 1; чем меньше $z$, тем ближе $p$ к 0; при $z=0$ получается ровно 0,5. Поэтому результат можно читать как вероятность спама.
Посчитаем для нашего письма. Здесь $e\approx2{,}718$ — математическая константа:
$$e^{-1{,}6}\approx0{,}202,\qquad p=\frac{1}{1+0{,}202}=\frac{1}{1{,}202}\approx0{,}832.$$
Модель оценила вероятность спама примерно в 83%.
Соберём все шаги в одну формулу. Модель берёт признаки письма, считает оценку и пропускает её через сигмоиду:
$$p=\sigma(w_1x_1+w_2x_2+b)=\frac{1}{1+e^{-(w_1x_1+w_2x_2+b)}}.$$
Это и есть логистическая регрессия. Её ответ $p$ — вероятность класса 1, то есть спама.
Почему «регрессия»
Название сбивает с толку: регрессией обычно называют предсказание чисел, а здесь мы определяем класс письма. Модель действительно выдаёт число — вероятность $p$. Класс письма определяется по ней отдельным шагом, о котором речь пойдёт ниже. Поэтому логистическая регрессия решает задачу классификации.
Углубление: откуда берутся веса¶
Для основного опыта достаточно знать, что fit подбирает веса по размеченным письмам. Следующие формулы раскрывают, как можно уменьшать ошибку маленькими шагами; к метрикам и порогам можно перейти сразу, а к выводу вернуться позже.
Числа $b=-2$, $w_1=1{,}2$, $w_2=-1{,}5$ мы просто назвали. На самом деле модель подбирает их сама по письмам с известными ответами. Для этого нужна мера ошибки: чем увереннее модель ошибается, тем больше штраф.
Для одного письма с настоящим ответом $y$ и вероятностью спама $p$ штраф такой:
$$\ell=-\bigl(y\ln p+(1-y)\ln(1-p)\bigr).$$
Здесь $\ln$ — натуральный логарифм. Формула выглядит сложнее, чем есть. Для спама ($y=1$) второе слагаемое обнуляется, и остаётся $\ell=-\ln p$. Для обычного письма ($y=0$) остаётся $\ell=-\ln(1-p)$. В обоих случаях штраф — это $-\ln$ от вероятности, которую модель дала правильному ответу: чем она меньше, тем штраф больше.
Спаму с $p=0{,}9$ достанется $-\ln0{,}9\approx0{,}105$, а спаму с $p=0{,}1$ — уже $-\ln0{,}1\approx2{,}303$: уверенная ошибка стоит дорого.
Средний штраф по всем обучающим письмам обозначим $L$. Его называют логарифмической ошибкой (log loss), или просто лоссом. Посчитаем её для двух писем: спама с тремя ссылками от незнакомого ($p\approx0{,}832$) и обычного письма с одной ссылкой от знакомого ($z=-2+1{,}2-1{,}5=-2{,}3$, $p\approx0{,}091$):
$$L=\frac{-\ln0{,}832-\ln(1-0{,}091)}{2}\approx\frac{0{,}184+0{,}096}{2}=0{,}140.$$
Веса подбирают маленькими шагами: каждый раз немного сдвигают их так, чтобы $L$ уменьшилась. Это градиентный спуск. Направление сдвига подсказывает производная $\frac{\partial L}{\partial w_1}$: она показывает, как меняется ошибка, если чуть увеличить вес $w_1$. Положительная производная значит, что ошибка растёт и вес надо уменьшать; отрицательная — что вес надо увеличивать. Для логистической регрессии она получается простой:
$$\frac{\partial L}{\partial w_1}=\frac1n\sum(p-y)\,x_1.$$
Здесь $n$ — число писем, а знак $\sum$ означает «сложить по всем письмам». Разность $p-y$ показывает промах модели на письме, а множитель $x_1$ — вклад ссылок в этот промах. Подставим наши два письма:
$$\frac{\partial L}{\partial w_1}=\frac{(0{,}832-1)\cdot3+(0{,}091-0)\cdot1}{2}=\frac{-0{,}504+0{,}091}{2}\approx-0{,}206.$$
Чтобы уменьшить ошибку $L$, от каждого веса отнимают производную лосса по этому весу, умноженную на небольшое число $\eta$ — скорость обучения. Стрелка $\leftarrow$ значит «записать новое значение веса»:
$$w_1\leftarrow w_1-\eta\,\frac{\partial L}{\partial w_1}.$$
Возьмём $\eta=0{,}5$:
$$w_1\leftarrow1{,}2-0{,}5\cdot(-0{,}206)\approx1{,}303.$$
Вес ссылок вырос: спаму с тремя ссылками модель дала слишком малую вероятность, и после шага оценка таких писем вырастет.
Один шаг лишь немного уменьшает ошибку. Обучение — это сотни таких шагов подряд, для всех весов сразу ($w_1$, $w_2$ и $b$ обновляются одинаково); в библиотеке их выполняет метод fit.
Как оценить ответы: accuracy, precision, recall¶
Модель выдаёт вероятность, но письмо нужно куда-то отправить, а качество ответов — измерить. Начнём со слов, которыми описывают любой классификатор с двумя классами.
Один класс называют положительным (positive) — это то, что мы ищем: спам, болезнь, дефект. Обычно ему соответствует класс 1. Второй класс — отрицательный (negative). Модель называет каждый объект положительным или отрицательным, и исходов получается четыре:
Рисунок 2 — Четыре исхода ответа классификатора
Аббревиатуры читаются по двум буквам. Первая: права ли модель (True — да, False — ошиблась). Вторая: что она ответила (Positive — «положительный», Negative — «отрицательный»). Например, FP — «ошиблась, ответив „положительный“». Ошибки FP называют ложными срабатываниями, а FN — пропусками.
Из четырёх чисел собирают три метрики:
- accuracy — доля верных ответов среди всех: $$\text{accuracy}=\frac{TP+TN}{TP+TN+FP+FN};$$
- precision (точность) — какая доля объектов, названных положительными, действительно положительна, то есть можно ли верить ответу «положительный»: $$\text{precision}=\frac{TP}{TP+FP};$$
- recall (полнота) — какую долю всех настоящих положительных объектов модель нашла: $$\text{recall}=\frac{TP}{TP+FN}.$$
Вернёмся к почте. Положительный класс здесь — спам. Пока решаем просто: если $p\ge0{,}5$, письмо уходит в спам. Наше письмо с $p\approx0{,}832$ попадёт в спам.
Возьмём шесть писем, для которых модель уже посчитала вероятность спама. В третьем столбце — настоящий класс, в четвёртом — ответ модели при $p\ge0{,}5$.
Таблица 1 — Вероятности спама, настоящие классы и ответы модели
| Письмо | $p$ | Настоящий класс | Ответ модели |
|---|---|---|---|
| А | 0,10 | обычное | обычное |
| Б | 0,35 | спам | обычное |
| В | 0,45 | обычное | обычное |
| Г | 0,60 | спам | спам |
| Д | 0,75 | обычное | спам |
| Е | 0,90 | спам | спам |
Разложим письма по исходам: Г и Е — TP (спам найден), Д — FP (обычное письмо ушло в спам), Б — FN (спам пропущен), А и В — TN. Подставляем:
$$\text{accuracy}=\frac{2+2}{6}\approx0{,}67,\qquad \text{precision}=\frac{2}{2+1}\approx0{,}67,\qquad \text{recall}=\frac{2}{2+1}\approx0{,}67.$$
Precision 0,67: в папке «спам» три письма (Г, Д, Е), и спам из них только два. Recall 0,67: настоящих спамов три (Б, Г, Е), и найдено два.
Порог двигает метрики¶
Число 0,5 мы взяли просто как самое очевидное, но обязательным оно не является. Введём порог $t$: письмо уходит в спам, если $p\ge t$. Вероятности при этом не меняются — меняется только правило, по которому мы превращаем их в ответ.
Рисунок 3 — Ответы классификатора при разных порогах вероятности
Снизим порог до 0,3: к письмам Г, Д и Е, которые уходили в спам при пороге 0,5, добавятся Б и В. Спам Б мы теперь поймали, но обычное письмо В ошибочно убрали. При пороге 0,8 срабатывает только Е.
Таблица 2 — Метрики классификации при разных порогах вероятности
| Порог | TP | FP | FN | TN | accuracy | precision | recall |
|---|---|---|---|---|---|---|---|
| 0,3 | 3 | 2 | 0 | 1 | $4/6\approx0{,}67$ | $3/5=0{,}6$ | $3/3=1$ |
| 0,5 | 2 | 1 | 1 | 2 | $4/6\approx0{,}67$ | $2/3\approx0{,}67$ | $2/3\approx0{,}67$ |
| 0,8 | 1 | 0 | 2 | 3 | $4/6\approx0{,}67$ | $1/1=1$ | $1/3\approx0{,}33$ |
Accuracy на всех трёх порогах одна и та же ($4/6$), а precision и recall меняются в разные стороны:
- при низком пороге 0,3 мы ловим весь спам (recall $=1$), но в папку попадают и обычные письма, поэтому precision падает до 0,6;
- при высоком пороге 0,8 в папке только спам (precision $=1$), но два спама из трёх пропущены, и recall падает до $1/3$.
Значит, порогом можно двигать метрики, не переобучая модель. Выбирают его по задаче: если дороже пропустить спам, порог снижают; если дороже потерять обычное письмо в папке «спам», порог повышают.
Практика: определяем опухоль по снимку клеток¶
Проверим всё на учебном применении реального набора данных, где многое зависит от порога. Набор breast_cancer из scikit-learn — это 569 опухолей. Для каждой измерены 30 признаков по снимку клеток: радиус, площадь, гладкость контура и другие. В этом упражнении модель различает злокачественную опухоль (класс 1, таких 212) и доброкачественную (класс 0). Это демонстрация метода, а не модель для медицинских решений.
Ошибки здесь неравноценны. Пропустить злокачественную опухоль (FN) гораздо опаснее, чем отправить здорового человека на лишнее обследование (FP). Поэтому порог выберем не по привычке, а по цене ошибок. Сравним логистическую регрессию и решающее дерево глубины 4.
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
data = load_breast_cancer()
X = data.data
y = (data.target == 0).astype(int) # 1 — злокачественная, 0 — доброкачественная
# Делим данные один раз: 60% — обучение, 20% — выбор порога, 20% — итоговая проверка.
# stratify сохраняет долю злокачественных опухолей в каждой части.
X_rest, X_test, y_rest, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
X_train, X_valid, y_train, y_valid = train_test_split(
X_rest, y_rest, test_size=0.25, random_state=42, stratify=y_rest
)
# Площадь измеряется сотнями, гладкость — сотыми долями, поэтому логистической
# регрессии нужна одна шкала признаков: StandardScaler. make_pipeline соединяет
# масштабирование и модель, а масштаб подбирается только по обучающей части.
models = {
"Логистическая регрессия": make_pipeline(
StandardScaler(), LogisticRegression(max_iter=1000)
),
"Дерево": DecisionTreeClassifier(max_depth=4, random_state=42),
}
for model in models.values():
model.fit(X_train, y_train)
# По вероятностям класса 1 и порогу считает ошибки, метрики и цену ошибок.
def report(y_true, probabilities, threshold):
predictions = probabilities >= threshold # True — ответ «злокачественная»
tp = int((predictions & (y_true == 1)).sum())
fp = int((predictions & (y_true == 0)).sum())
fn = int((~predictions & (y_true == 1)).sum())
return {"FP": fp, "FN": fn,
"accuracy": round(1 - (fp + fn) / len(y_true), 3),
"precision": round(tp / (tp + fp), 3) if tp + fp else 0.0,
"recall": round(tp / (tp + fn), 3),
"цена": 4 * fn + fp} # пропуск в четыре раза хуже ложной тревоги
Выбираем порог на проверочной части¶
Для учебного сравнения условимся считать один пропуск в четыре раза дороже одной ложной тревоги: $C=4\,\mathrm{FN}+\mathrm{FP}$. Число 4 придумано для упражнения и не является медицинской нормой. Для каждой модели выберем из порогов 0,2; 0,5; 0,8 тот, при котором эта условная цена на проверочной части минимальна.
chosen = {}
for name, model in models.items():
# predict_proba возвращает по столбцу на каждый класс;
# столбец 1 — вероятность класса 1 (злокачественная).
probabilities = model.predict_proba(X_valid)[:, 1]
costs = {}
for threshold in [0.2, 0.5, 0.8]:
row = report(y_valid, probabilities, threshold)
costs[threshold] = row["цена"]
print(name, threshold, row)
chosen[name] = min(costs, key=costs.get) # порог с наименьшей ценой
У логистической регрессии на проверочной части при всех трёх порогах ровно один пропуск, поэтому выбор решают ложные тревоги: побеждает самый строгий порог 0,8. Дереву лучше всего подходит порог 0,2. Порог у каждой модели свой: мы выбираем для каждой лучший, поэтому сравнение честное — обе показаны в самом выгодном для себя режиме.
Итоговая проверка¶
Тестовая часть — это 20% данных: 114 опухолей, из них 42 злокачественные.
# Тестовую часть используем один раз: пороги уже выбраны на проверочной.
for name, model in models.items():
probabilities = model.predict_proba(X_test)[:, 1]
print(name, "порог:", chosen[name],
report(y_test, probabilities, chosen[name]))
Таблица 3 — Итоговые метрики и цена ошибок двух моделей
| Модель | Порог | FP | FN | accuracy | precision | recall | Цена |
|---|---|---|---|---|---|---|---|
| Логист. регр. | 0,8 | 0 | 5 | 0,956 | 1,000 | 0,881 | $4\cdot5+0=20$ |
| Дерево | 0,2 | 4 | 6 | 0,912 | 0,900 | 0,857 | $4\cdot6+4=28$ |
Логистическая регрессия лучше по всем метрикам. Но на тесте порог 0,8 пропустил 5 опухолей из 42, хотя на проверочной части был всего один пропуск: проверочная часть небольшая, и порог по ней получился слишком строгим. Подправлять порог по тесту теперь нельзя: тест нужен как независимая проверка, а подгонка под него сделает оценку нечестной.
Метрики не обязательно считать самому: они есть в библиотеке.
from sklearn.metrics import accuracy_score, precision_score, recall_score
name = "Логистическая регрессия"
probabilities = models[name].predict_proba(X_test)[:, 1]
predictions = probabilities >= chosen[name]
print("accuracy:", round(accuracy_score(y_test, predictions), 3))
print("precision:", round(precision_score(y_test, predictions), 3))
print("recall:", round(recall_score(y_test, predictions), 3))
Значения совпадают с таблицей: 0,956; 1,000; 0,881.
Самостоятельно. Правило «всегда доброкачественная» никогда не называет опухоль злокачественной. Посчитайте для него на тесте accuracy и recall. Затем на проверочной части найдите для логистической регрессии наименьший порог из [0.05, 0.1, ..., 0.95], при котором precision не ниже 0,95.
Итоги¶
Логистическая регрессия превращает линейную оценку в число от 0 до 1; порог превращает его в класс. Accuracy считает все верные ответы, precision описывает надёжность положительного ответа, recall — долю найденных положительных объектов. Проверьте себя: что изменится при повышении порога? Почему одинаковая accuracy может скрывать разные ошибки? На какой части данных выбирают порог? Подсказки и разборы — ниже.
Самопроверка¶
1. Логистическая регрессия считает оценку $z=b+w_1x_1+w_2x_2$ и применяет к ней сигмоиду. Зачем?¶
A. Увеличить число признаков.
B. Получить число между 0 и 1.
C. Сразу выбрать порог.
D. Заменить обучающие ответы.
2. Чему равна $\sigma(z)=\frac{1}{1+e^{-z}}$ при $z=0$?¶
A. 0.
B. 1.
C. 0,5.
D. Зависит от порога.
3. Оценка письма $z=-2+1{,}2x_1-1{,}5x_2$, где $x_1$ — число ссылок, $x_2=1$ для знакомого отправителя. Чему равна $z$ у письма с одной ссылкой от знакомого?¶
A. −2,3.
B. 0,7.
C. 1,6.
D. 4,7.
4. Модель относит объект к классу 1, если $p\ge t$. Она выдала $p=0{,}4$, порог $t=0{,}3$. Какой класс получим?¶
A. Класс 0, потому что $p<0{,}5$.
B. Класс 1, потому что $p\ge0{,}3$.
C. Оба класса.
D. Нужны настоящие ответы.
5. Спам-фильтр отправил в спам обычное письмо. Какая это ошибка?¶
A. Пропуск FN.
B. Ложное срабатывание FP.
C. Верный ответ.
D. Ошибка разбиения данных.
6. Письмо уходит в спам, если $p\ge t$. Порог $t$ повышаем, вероятности не меняются. Что будет с FP и FN?¶
A. FP не увеличится, FN не уменьшится.
B. FP не уменьшится, FN не увеличится.
C. Обе ошибки обязательно уменьшатся.
D. Вероятности изменятся.
7. Для спама ошибка равна $-\ln p$. Модель дала письму $p=0{,}1$ вместо $p=0{,}9$. Что станет с ошибкой?¶
A. Уменьшится.
B. Не изменится.
C. Увеличится.
D. Всегда будет нулевой.
8. Данные делят на обучающую, проверочную и тестовую части. На какой выбирают порог?¶
A. На обучающей части.
B. На проверочной части.
C. На тестовой части.
D. На всех данных сразу.
9. При пороге 0,5 у модели TP = 6, FP = 2, FN = 3. Чему равны precision и recall?¶
A. 0,75 и 0,67.
B. 0,67 и 0,75.
C. 0,8 и 0,6.
D. 0,6 и 0,8.
10. Правило «всегда доброкачественная» на тесте из 114 опухолей (42 злокачественные) даёт accuracy $72/114\approx63\%$. Почему этого мало?¶
A. Accuracy нельзя считать для двух классов.
B. Его recall равен 0: оно не находит ни одной злокачественной опухоли.
C. Его precision равен 1.
D. Правило использует тестовую часть.
Решения задач¶
Задача 1¶
В тесте 114 опухолей, из них 42 злокачественных. Правило «всегда 0» даёт TP = 0 и FN = 42: accuracy $=72/114\approx0{,}632$, recall $=0/42=0$. Высокая доля верных ответов не значит, что модель находит нужный класс.
always_zero = np.zeros(len(y_test))
print(report(y_test, always_zero, 0.5))
probabilities = models["Логистическая регрессия"].predict_proba(X_valid)[:, 1]
for threshold in [round(0.05 * k, 2) for k in range(1, 20)]:
if report(y_valid, probabilities, threshold)["precision"] >= 0.95:
print("порог:", threshold, report(y_valid, probabilities, threshold))
break
Получится порог 0,35: на проверочной части FP = 2, FN = 1, precision $\approx0{,}955$, recall $\approx0{,}977$.
Подсказки и решения¶
Вопрос 1¶
Подсказка
Линейная сумма может равняться и −5, и 12.
Решение
B. Сигмоида превращает оценку в вероятность класса 1. Порог применяют следующим шагом.
Вопрос 2¶
Подсказка
Подставьте $e^0=1$ в формулу.
Решение
C. $p=1/(1+1)=0{,}5$. Порог не участвует в вычислении вероятности.
Вопрос 3¶
Подсказка
Подставьте $x_1=1$ и $x_2=1$.
Решение
A. $-2+1{,}2-1{,}5=-2{,}3$. Соответствующая вероятность примерно 0,091, меньше 0,5.
Вопрос 4¶
Подсказка
Используйте заданный порог.
Решение
B. Сравниваем с 0,3: $0{,}4\ge0{,}3$. Настоящий ответ нужен для оценки ошибки, а не для прогноза.
Вопрос 5¶
Подсказка
Модель сработала на объекте, где класса 1 на самом деле нет.
Решение
B. Предсказан класс 1 при настоящем классе 0. Пропуск — обратный случай: настоящий спам оставили во входящих.
Вопрос 6¶
Подсказка
При более высоком пороге меньше объектов получают класс 1.
Решение
A. Срабатываний становится меньше или столько же. Часть ложных срабатываний может исчезнуть, часть настоящих положительных ответов — превратиться в пропуски.
Вопрос 7¶
Подсказка
Для настоящего спама используем $-\ln p$.
Решение
C. Ошибка вырастет примерно с 0,105 до 2,303. Модель была уверена в неверном ответе.
Вопрос 8¶
Подсказка
Одна часть обучает веса, другая выбирает настройку, третья проверяет готовое решение.
Решение
B. Порог — настройка решения. Тестовая часть остаётся для итоговой оценки после выбора.
Вопрос 9¶
Подсказка
Precision делит TP на все срабатывания, recall — на все настоящие объекты класса 1.
Решение
A. precision $=6/(6+2)=0{,}75$, recall $=6/(6+3)\approx0{,}67$. Вариант B путает формулы местами.
Вопрос 10¶
Подсказка
Сколько злокачественных опухолей это правило отметит?
Решение
B. Большинство опухолей доброкачественные, поэтому доля верных ответов высокая, но TP = 0 и recall $=0$. Поэтому вместе с accuracy смотрят на recall и precision.