Логистическая регрессия и вероятностный ответ

ЧерновикМатериалы дорабатываются и будут дополняться.

Автор: Алексей Чернуха

Чему научимся

Хороший спам-фильтр не просто объявляет письмо «спамом» или «не спамом». Он оценивает, насколько уверен: «это спам с вероятностью 95%», «а это — всего с вероятностью 55%». Такая уверенность полезна: первое письмо можно сразу убрать, а второе оставить во входящих и пометить как подозрительное.

Это оценка модели, а не обещание, что среди любых ста писем с оценкой 95% ровно 95 окажутся спамом. Проверять такие оценки нужно на новых размеченных данных.

После занятия вы сможете:

  • Посчитать вероятность класса по признакам объекта с помощью логистической регрессии.
  • Объяснить, как порог меняет ошибки, и посчитать accuracy, precision и recall.
  • Сравнить логистическую регрессию и решающее дерево на одинаковых данных.

Время и подготовка: ориентир 8–10 часов с практикой. Нужны линейная сумма из 1.3, дроби, доли и основы Python. Сигмоида и порог составляют основной маршрут; логарифмическая ошибка и производная ниже выделены как углубление.

План занятия

  1. На одном письме пройдём путь от признаков до оценки вероятности спама.
  2. Превратим вероятность в решение, передвинем порог и посчитаем ошибки.
  3. На отдельном наборе сравним логистическую регрессию и дерево.

От признаков письма к вероятности

Пусть фильтр смотрит на два признака письма:

  • x1x_1 — сколько ссылок в тексте;
  • x2x_2 — знаком ли отправитель: 1, если он есть в контактах, и 0, если нет.

Ответ тоже запишем числом: спам — y=1y=1, обычное письмо — y=0y=0. Это два класса.

Идея простая: каждый признак вносит свой вклад в «подозрительность» письма, а вклады складываются. Вклад признака — это сам признак, умноженный на его вес. К сумме добавляют ещё свободный член bb — число, с которого начинается счёт. Получается оценка письма:

z=b+w1x1+w2x2.z=b+w_1x_1+w_2x_2.

Допустим, обучение дало такие числа: b=−2b=-2, w1=1,2w_1=1{,}2, w2=−1,5w_2=-1{,}5. Тогда

z=−2+1,2⋅x1−1,5⋅x2.z=-2+1{,}2\cdot x_1-1{,}5\cdot x_2.

Каждое слагаемое читается отдельно:

  • −2-2 — стартовое значение: письмо без ссылок от незнакомого отправителя выглядит скорее обычным;
  • +1,2⋅x1+1{,}2\cdot x_1 — каждая ссылка добавляет 1,2 к подозрительности;
  • −1,5⋅x2-1{,}5\cdot x_2 — знакомый отправитель убавляет её на 1,5.

Возьмём письмо с тремя ссылками от незнакомого отправителя: x1=3x_1=3, x2=0x_2=0. Подставляем:

z=−2+1,2⋅3−1,5⋅0=−2+3,6=1,6.z=-2+1{,}2\cdot3-1{,}5\cdot0=-2+3{,}6=1{,}6.

Оценка 1,6 — ещё не вероятность. Вероятность лежит между 0 и 1, а оценка может быть любым числом: и −50, и 700. Нужен способ уложить любое число в промежуток от 0 до 1.

Обозначим через pp вероятность того, что письмо — спам. Получить pp из оценки zz помогает сигмоида:

p=σ(z)=11+e−z.p=\sigma(z)=\frac{1}{1+e^{-z}}.
График сигмоиды: S-образная кривая, слева приближается к 0, справа к 1; в точке 0 равна 0,5. Отмечены z = −2,3, z = 0 и z = 1,6.
Рисунок 1 — Преобразование оценки модели в вероятность с помощью сигмоиды

Сигмоида берёт любое число и сжимает его в промежуток от 0 до 1. Чем больше zz, тем ближе pp к 1; чем меньше zz, тем ближе pp к 0; при z=0z=0 получается ровно 0,5. Поэтому результат можно читать как вероятность спама.

Посчитаем для нашего письма. Здесь e≈2,718e\approx2{,}718 — математическая константа:

e−1,6≈0,202,p=11+0,202=11,202≈0,832.e^{-1{,}6}\approx0{,}202,\qquad p=\frac{1}{1+0{,}202}=\frac{1}{1{,}202}\approx0{,}832.

Модель оценила вероятность спама примерно в 83%.

Соберём все шаги в одну формулу. Модель берёт признаки письма, считает оценку и пропускает её через сигмоиду:

p=σ(w1x1+w2x2+b)=11+e−(w1x1+w2x2+b).p=\sigma(w_1x_1+w_2x_2+b)=\frac{1}{1+e^{-(w_1x_1+w_2x_2+b)}}.

Это и есть логистическая регрессия. Её ответ pp — вероятность класса 1, то есть спама.

Углубление: откуда берутся веса

Для основного опыта достаточно знать, что fit подбирает веса по размеченным письмам. Следующие формулы раскрывают, как можно уменьшать ошибку маленькими шагами; к метрикам и порогам можно перейти сразу, а к выводу вернуться позже.

Числа b=−2b=-2, w1=1,2w_1=1{,}2, w2=−1,5w_2=-1{,}5 мы просто назвали. На самом деле модель подбирает их сама по письмам с известными ответами. Для этого нужна мера ошибки: чем увереннее модель ошибается, тем больше штраф.

Для одного письма с настоящим ответом yy и вероятностью спама pp штраф такой:

ℓ=−(yln⁡p+(1−y)ln⁡(1−p)).\ell=-\bigl(y\ln p+(1-y)\ln(1-p)\bigr).

Здесь ln⁡\ln — натуральный логарифм. Формула выглядит сложнее, чем есть. Для спама (y=1y=1) второе слагаемое обнуляется, и остаётся ℓ=−ln⁡p\ell=-\ln p. Для обычного письма (y=0y=0) остаётся ℓ=−ln⁡(1−p)\ell=-\ln(1-p). В обоих случаях штраф — это −ln⁡-\ln от вероятности, которую модель дала правильному ответу: чем она меньше, тем штраф больше.

Спаму с p=0,9p=0{,}9 достанется −ln⁡0,9≈0,105-\ln0{,}9\approx0{,}105, а спаму с p=0,1p=0{,}1 — уже −ln⁡0,1≈2,303-\ln0{,}1\approx2{,}303: уверенная ошибка стоит дорого.

Средний штраф по всем обучающим письмам обозначим LL. Его называют логарифмической ошибкой (log loss), или просто лоссом. Посчитаем её для двух писем: спама с тремя ссылками от незнакомого (p≈0,832p\approx0{,}832) и обычного письма с одной ссылкой от знакомого (z=−2+1,2−1,5=−2,3z=-2+1{,}2-1{,}5=-2{,}3, p≈0,091p\approx0{,}091):

L=−ln⁡0,832−ln⁡(1−0,091)2≈0,184+0,0962=0,140.L=\frac{-\ln0{,}832-\ln(1-0{,}091)}{2}\approx\frac{0{,}184+0{,}096}{2}=0{,}140.

Веса подбирают маленькими шагами: каждый раз немного сдвигают их так, чтобы LL уменьшилась. Это градиентный спуск. Направление сдвига подсказывает производная ∂L∂w1\frac{\partial L}{\partial w_1}: она показывает, как меняется ошибка, если чуть увеличить вес w1w_1. Положительная производная значит, что ошибка растёт и вес надо уменьшать; отрицательная — что вес надо увеличивать. Для логистической регрессии она получается простой:

∂L∂w1=1n∑(p−y) x1.\frac{\partial L}{\partial w_1}=\frac1n\sum(p-y)\,x_1.

Здесь nn — число писем, а знак ∑\sum означает «сложить по всем письмам». Разность p−yp-y показывает промах модели на письме, а множитель x1x_1 — вклад ссылок в этот промах. Подставим наши два письма:

∂L∂w1=(0,832−1)⋅3+(0,091−0)⋅12=−0,504+0,0912≈−0,206.\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.

Чтобы уменьшить ошибку LL, от каждого веса отнимают производную лосса по этому весу, умноженную на небольшое число η\eta — скорость обучения. Стрелка ←\leftarrow значит «записать новое значение веса»:

w1←w1−η ∂L∂w1.w_1\leftarrow w_1-\eta\,\frac{\partial L}{\partial w_1}.

Возьмём η=0,5\eta=0{,}5:

w1←1,2−0,5⋅(−0,206)≈1,303.w_1\leftarrow1{,}2-0{,}5\cdot(-0{,}206)\approx1{,}303.

Вес ссылок вырос: спаму с тремя ссылками модель дала слишком малую вероятность, и после шага оценка таких писем вырастет.

Один шаг лишь немного уменьшает ошибку. Обучение — это сотни таких шагов подряд, для всех весов сразу (w1w_1, w2w_2 и bb обновляются одинаково); в библиотеке их выполняет метод fit.

Как оценить ответы: accuracy, precision, recall

Модель выдаёт вероятность, но письмо нужно куда-то отправить, а качество ответов — измерить. Начнём со слов, которыми описывают любой классификатор с двумя классами.

Один класс называют положительным (positive) — это то, что мы ищем: спам, болезнь, дефект. Обычно ему соответствует класс 1. Второй класс — отрицательный (negative). Модель называет каждый объект положительным или отрицательным, и исходов получается четыре:

Четыре исхода: TP — положительный распознан; FN — положительный принят за отрицательный (пропуск); FP — отрицательный принят за положительный (ложное срабатывание); TN — отрицательный распознан.
Рисунок 2 — Четыре исхода ответа классификатора

Аббревиатуры читаются по двум буквам. Первая: права ли модель (True — да, False — ошиблась). Вторая: что она ответила (Positive — «положительный», Negative — «отрицательный»). Например, FP — «ошиблась, ответив „положительный“». Ошибки FP называют ложными срабатываниями, а FN — пропусками.

Из четырёх чисел собирают три метрики:

  • accuracy — доля верных ответов среди всех:
    accuracy=TP+TNTP+TN+FP+FN;\text{accuracy}=\frac{TP+TN}{TP+TN+FP+FN};
  • precision (точность) — какая доля объектов, названных положительными, действительно положительна, то есть можно ли верить ответу «положительный»:
    precision=TPTP+FP;\text{precision}=\frac{TP}{TP+FP};
  • recall (полнота) — какую долю всех настоящих положительных объектов модель нашла:
    recall=TPTP+FN.\text{recall}=\frac{TP}{TP+FN}.

Вернёмся к почте. Положительный класс здесь — спам. Пока решаем просто: если p≥0,5p\ge0{,}5, письмо уходит в спам. Наше письмо с p≈0,832p\approx0{,}832 попадёт в спам.

Возьмём шесть писем, для которых модель уже посчитала вероятность спама. В третьем столбце — настоящий класс, в четвёртом — ответ модели при p≥0,5p\ge0{,}5.

Таблица 1 — Вероятности спама, настоящие классы и ответы модели
Письмо pp Настоящий класс Ответ модели
А 0,10 обычное обычное
Б 0,35 спам обычное
В 0,45 обычное обычное
Г 0,60 спам спам
Д 0,75 обычное спам
Е 0,90 спам спам

Разложим письма по исходам: Г и Е — TP (спам найден), Д — FP (обычное письмо ушло в спам), Б — FN (спам пропущен), А и В — TN. Подставляем:

accuracy=2+26≈0,67,precision=22+1≈0,67,recall=22+1≈0,67.\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 мы взяли просто как самое очевидное, но обязательным оно не является. Введём порог tt: письмо уходит в спам, если p≥tp\ge t. Вероятности при этом не меняются — меняется только правило, по которому мы превращаем их в ответ.

Три порога для тех же писем: 0,3 даёт два ложных срабатывания и ноль пропусков; 0,5 — по одной ошибке; 0,8 — ноль ложных срабатываний и два пропуска.
Рисунок 3 — Ответы классификатора при разных порогах вероятности

Снизим порог до 0,3: к письмам Г, Д и Е, которые уходили в спам при пороге 0,5, добавятся Б и В. Спам Б мы теперь поймали, но обычное письмо В ошибочно убрали. При пороге 0,8 срабатывает только Е.

Таблица 2 — Метрики классификации при разных порогах вероятности
Порог TP FP FN TN accuracy precision recall
0,3 3 2 0 1 4/6≈0,674/6\approx0{,}67 3/5=0,63/5=0{,}6 3/3=13/3=1
0,5 2 1 1 2 4/6≈0,674/6\approx0{,}67 2/3≈0,672/3\approx0{,}67 2/3≈0,672/3\approx0{,}67
0,8 1 0 2 3 4/6≈0,674/6\approx0{,}67 1/1=11/1=1 1/3≈0,331/3\approx0{,}33

Accuracy на всех трёх порогах одна и та же (4/64/6), а precision и recall меняются в разные стороны:

  • при низком пороге 0,3 мы ловим весь спам (recall =1=1), но в папку попадают и обычные письма, поэтому precision падает до 0,6;
  • при высоком пороге 0,8 в папке только спам (precision =1=1), но два спама из трёх пропущены, и recall падает до 1/31/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 FN+FPC=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⋅5+0=204\cdot5+0=20
Дерево 0,2 4 6 0,912 0,900 0,857 4⋅6+4=284\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.

Решение

В тесте 114 опухолей, из них 42 злокачественных. Правило «всегда 0» даёт TP = 0 и FN = 42: accuracy =72/114≈0,632=72/114\approx0{,}632, recall =0/42=0=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 ≈0,955\approx0{,}955, recall ≈0,977\approx0{,}977.

Итоги

Логистическая регрессия превращает линейную оценку в число от 0 до 1; порог превращает его в класс. Accuracy считает все верные ответы, precision описывает надёжность положительного ответа, recall — долю найденных положительных объектов. Проверьте себя: что изменится при повышении порога? Почему одинаковая accuracy может скрывать разные ошибки? На какой части данных выбирают порог? Подсказки и разборы — ниже.

Самопроверка

Вопрос 1

Логистическая регрессия считает оценку z=b+w1x1+w2x2z=b+w_1x_1+w_2x_2 и применяет к ней сигмоиду. Зачем?

Подсказка

Линейная сумма может равняться и −5, и 12.

Решение

B. Сигмоида превращает оценку в вероятность класса 1. Порог применяют следующим шагом.

Вопрос 2

Чему равна σ(z)=11+e−z\sigma(z)=\frac{1}{1+e^{-z}} при z=0z=0?

Подсказка

Подставьте e0=1e^0=1 в формулу.

Решение

C. p=1/(1+1)=0,5p=1/(1+1)=0{,}5. Порог не участвует в вычислении вероятности.

Вопрос 3

Оценка письма z=−2+1,2x1−1,5x2z=-2+1{,}2x_1-1{,}5x_2, где x1x_1 — число ссылок, x2=1x_2=1 для знакомого отправителя. Чему равна zz у письма с одной ссылкой от знакомого?

Подсказка

Подставьте x1=1x_1=1 и x2=1x_2=1.

Решение

A. −2+1,2−1,5=−2,3-2+1{,}2-1{,}5=-2{,}3. Соответствующая вероятность примерно 0,091, меньше 0,5.

Вопрос 4

Модель относит объект к классу 1, если p≥tp\ge t. Она выдала p=0,4p=0{,}4, порог t=0,3t=0{,}3. Какой класс получим?

Подсказка

Используйте заданный порог.

Решение

B. Сравниваем с 0,3: 0,4≥0,30{,}4\ge0{,}3. Настоящий ответ нужен для оценки ошибки, а не для прогноза.

Вопрос 5

Спам-фильтр отправил в спам обычное письмо. Какая это ошибка?

Подсказка

Модель сработала на объекте, где класса 1 на самом деле нет.

Решение

B. Предсказан класс 1 при настоящем классе 0. Пропуск — обратный случай: настоящий спам оставили во входящих.

Вопрос 6

Письмо уходит в спам, если p≥tp\ge t. Порог tt повышаем, вероятности не меняются. Что будет с FP и FN?

Подсказка

При более высоком пороге меньше объектов получают класс 1.

Решение

A. Срабатываний становится меньше или столько же. Часть ложных срабатываний может исчезнуть, часть настоящих положительных ответов — превратиться в пропуски.

Вопрос 7

Для спама ошибка равна −ln⁡p-\ln p. Модель дала письму p=0,1p=0{,}1 вместо p=0,9p=0{,}9. Что станет с ошибкой?

Подсказка

Для настоящего спама используем −ln⁡p-\ln p.

Решение

C. Ошибка вырастет примерно с 0,105 до 2,303. Модель была уверена в неверном ответе.

Вопрос 8

Данные делят на обучающую, проверочную и тестовую части. На какой выбирают порог?

Подсказка

Одна часть обучает веса, другая выбирает настройку, третья проверяет готовое решение.

Решение

B. Порог — настройка решения. Тестовая часть остаётся для итоговой оценки после выбора.

Вопрос 9

При пороге 0,5 у модели TP = 6, FP = 2, FN = 3. Чему равны precision и recall?

Подсказка

Precision делит TP на все срабатывания, recall — на все настоящие объекты класса 1.

Решение

A. precision =6/(6+2)=0,75=6/(6+2)=0{,}75, recall =6/(6+3)≈0,67=6/(6+3)\approx0{,}67. Вариант B путает формулы местами.

Вопрос 10

Правило «всегда доброкачественная» на тесте из 114 опухолей (42 злокачественные) даёт accuracy 72/114≈63%72/114\approx63\%. Почему этого мало?

Подсказка

Сколько злокачественных опухолей это правило отметит?

Решение

B. Большинство опухолей доброкачественные, поэтому доля верных ответов высокая, но TP = 0 и recall =0=0. Поэтому вместе с accuracy смотрят на recall и precision.

Файлы