Логистическая регрессия и вероятностный ответ
ЧерновикМатериалы дорабатываются и будут дополняться.
Чему научимся
Хороший спам-фильтр не просто объявляет письмо «спамом» или «не спамом». Он оценивает, насколько уверен: «это спам с вероятностью 95%», «а это — всего с вероятностью 55%». Такая уверенность полезна: первое письмо можно сразу убрать, а второе оставить во входящих и пометить как подозрительное.
Это оценка модели, а не обещание, что среди любых ста писем с оценкой 95% ровно 95 окажутся спамом. Проверять такие оценки нужно на новых размеченных данных.
После занятия вы сможете:
- Посчитать вероятность класса по признакам объекта с помощью логистической регрессии.
- Объяснить, как порог меняет ошибки, и посчитать accuracy, precision и recall.
- Сравнить логистическую регрессию и решающее дерево на одинаковых данных.
Время и подготовка: ориентир 8–10 часов с практикой. Нужны линейная сумма из 1.3, дроби, доли и основы Python. Сигмоида и порог составляют основной маршрут; логарифмическая ошибка и производная ниже выделены как углубление.
План занятия
- На одном письме пройдём путь от признаков до оценки вероятности спама.
- Превратим вероятность в решение, передвинем порог и посчитаем ошибки.
- На отдельном наборе сравним логистическую регрессию и дерево.
От признаков письма к вероятности
Пусть фильтр смотрит на два признака письма:
— сколько ссылок в тексте; — знаком ли отправитель: 1, если он есть в контактах, и 0, если нет.
Ответ тоже запишем числом: спам —
Идея простая: каждый признак вносит свой вклад в «подозрительность» письма, а вклады складываются. Вклад признака — это сам признак, умноженный на его вес. К сумме добавляют ещё свободный член
Допустим, обучение дало такие числа:
Каждое слагаемое читается отдельно:
— стартовое значение: письмо без ссылок от незнакомого отправителя выглядит скорее обычным; — каждая ссылка добавляет 1,2 к подозрительности; — знакомый отправитель убавляет её на 1,5.
Возьмём письмо с тремя ссылками от незнакомого отправителя:
Оценка 1,6 — ещё не вероятность. Вероятность лежит между 0 и 1, а оценка может быть любым числом: и −50, и 700. Нужен способ уложить любое число в промежуток от 0 до 1.
Обозначим через
Сигмоида берёт любое число и сжимает его в промежуток от 0 до 1. Чем больше
Посчитаем для нашего письма. Здесь
Модель оценила вероятность спама примерно в 83%.
Соберём все шаги в одну формулу. Модель берёт признаки письма, считает оценку и пропускает её через сигмоиду:
Это и есть логистическая регрессия. Её ответ
Углубление: откуда берутся веса
Для основного опыта достаточно знать, что fit подбирает веса по размеченным письмам. Следующие формулы раскрывают, как можно уменьшать ошибку маленькими шагами; к метрикам и порогам можно перейти сразу, а к выводу вернуться позже.
Числа
Для одного письма с настоящим ответом
Здесь
Спаму с
Средний штраф по всем обучающим письмам обозначим
Веса подбирают маленькими шагами: каждый раз немного сдвигают их так, чтобы
Здесь
Чтобы уменьшить ошибку
Возьмём
Вес ссылок вырос: спаму с тремя ссылками модель дала слишком малую вероятность, и после шага оценка таких писем вырастет.
Один шаг лишь немного уменьшает ошибку. Обучение — это сотни таких шагов подряд, для всех весов сразу (fit.
Как оценить ответы: accuracy, precision, recall
Модель выдаёт вероятность, но письмо нужно куда-то отправить, а качество ответов — измерить. Начнём со слов, которыми описывают любой классификатор с двумя классами.
Один класс называют положительным (positive) — это то, что мы ищем: спам, болезнь, дефект. Обычно ему соответствует класс 1. Второй класс — отрицательный (negative). Модель называет каждый объект положительным или отрицательным, и исходов получается четыре:
Аббревиатуры читаются по двум буквам. Первая: права ли модель (True — да, False — ошиблась). Вторая: что она ответила (Positive — «положительный», Negative — «отрицательный»). Например, FP — «ошиблась, ответив „положительный“». Ошибки FP называют ложными срабатываниями, а FN — пропусками.
Из четырёх чисел собирают три метрики:
- accuracy — доля верных ответов среди всех:
- precision (точность) — какая доля объектов, названных положительными, действительно положительна, то есть можно ли верить ответу «положительный»:
- recall (полнота) — какую долю всех настоящих положительных объектов модель нашла:
Вернёмся к почте. Положительный класс здесь — спам. Пока решаем просто: если
Возьмём шесть писем, для которых модель уже посчитала вероятность спама. В третьем столбце — настоящий класс, в четвёртом — ответ модели при
| Письмо | Настоящий класс | Ответ модели | |
|---|---|---|---|
| А | 0,10 | обычное | обычное |
| Б | 0,35 | спам | обычное |
| В | 0,45 | обычное | обычное |
| Г | 0,60 | спам | спам |
| Д | 0,75 | обычное | спам |
| Е | 0,90 | спам | спам |
Разложим письма по исходам: Г и Е — TP (спам найден), Д — FP (обычное письмо ушло в спам), Б — FN (спам пропущен), А и В — TN. Подставляем:
Precision 0,67: в папке «спам» три письма (Г, Д, Е), и спам из них только два. Recall 0,67: настоящих спамов три (Б, Г, Е), и найдено два.
Порог двигает метрики
Число 0,5 мы взяли просто как самое очевидное, но обязательным оно не является. Введём порог
Снизим порог до 0,3: к письмам Г, Д и Е, которые уходили в спам при пороге 0,5, добавятся Б и В. Спам Б мы теперь поймали, но обычное письмо В ошибочно убрали. При пороге 0,8 срабатывает только Е.
| Порог | TP | FP | FN | TN | accuracy | precision | recall |
|---|---|---|---|---|---|---|---|
| 0,3 | 3 | 2 | 0 | 1 | |||
| 0,5 | 2 | 1 | 1 | 2 | |||
| 0,8 | 1 | 0 | 2 | 3 |
Accuracy на всех трёх порогах одна и та же (
- при низком пороге 0,3 мы ловим весь спам (recall
), но в папку попадают и обычные письма, поэтому precision падает до 0,6; - при высоком пороге 0,8 в папке только спам (precision
), но два спама из трёх пропущены, и recall падает до .
Значит, порогом можно двигать метрики, не переобучая модель. Выбирают его по задаче: если дороже пропустить спам, порог снижают; если дороже потерять обычное письмо в папке «спам», порог повышают.
Практика: определяем опухоль по снимку клеток
Проверим всё на учебном применении реального набора данных, где многое зависит от порога. Набор 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} # пропуск в четыре раза хуже ложной тревоги
Выбираем порог на проверочной части
Для учебного сравнения условимся считать один пропуск в четыре раза дороже одной ложной тревоги:
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]))
| Модель | Порог | FP | FN | accuracy | precision | recall | Цена |
|---|---|---|---|---|---|---|---|
| Логист. регр. | 0,8 | 0 | 5 | 0,956 | 1,000 | 0,881 | |
| Дерево | 0,2 | 4 | 6 | 0,912 | 0,900 | 0,857 |
Логистическая регрессия лучше по всем метрикам. Но на тесте порог 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.
Решение
В тесте 114 опухолей, из них 42 злокачественных. Правило «всегда 0» даёт TP = 0 и FN = 42: accuracy
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
Итоги
Логистическая регрессия превращает линейную оценку в число от 0 до 1; порог превращает его в класс. Accuracy считает все верные ответы, precision описывает надёжность положительного ответа, recall — долю найденных положительных объектов. Проверьте себя: что изменится при повышении порога? Почему одинаковая accuracy может скрывать разные ошибки? На какой части данных выбирают порог? Подсказки и разборы — ниже.
Самопроверка
Проверено 0 из 10
Подсказка
Линейная сумма может равняться и −5, и 12.
Решение
B. Сигмоида превращает оценку в вероятность класса 1. Порог применяют следующим шагом.
Подсказка
Подставьте
Решение
C.
Подсказка
Подставьте
Решение
A.
Подсказка
Используйте заданный порог.
Решение
B. Сравниваем с 0,3:
Подсказка
Модель сработала на объекте, где класса 1 на самом деле нет.
Решение
B. Предсказан класс 1 при настоящем классе 0. Пропуск — обратный случай: настоящий спам оставили во входящих.
Подсказка
При более высоком пороге меньше объектов получают класс 1.
Решение
A. Срабатываний становится меньше или столько же. Часть ложных срабатываний может исчезнуть, часть настоящих положительных ответов — превратиться в пропуски.
Подсказка
Для настоящего спама используем
Решение
C. Ошибка вырастет примерно с 0,105 до 2,303. Модель была уверена в неверном ответе.
Подсказка
Одна часть обучает веса, другая выбирает настройку, третья проверяет готовое решение.
Решение
B. Порог — настройка решения. Тестовая часть остаётся для итоговой оценки после выбора.
Подсказка
Precision делит TP на все срабатывания, recall — на все настоящие объекты класса 1.
Решение
A. precision
Подсказка
Сколько злокачественных опухолей это правило отметит?
Решение
B. Большинство опухолей доброкачественные, поэтому доля верных ответов высокая, но TP = 0 и recall