Задача классификации и её функция потерь

⏱ 40 мин · 🧩 заданий: 6

Что такое классификация

Классификация — предсказание класса из конечного набора: спам / не спам, цифра 0–9, порода собаки, следующее слово (класс — слово из словаря!). Модель выдаёт не просто ответ, а распределение вероятностей по классам, и мы учим её давать правильному классу высокую вероятность.

Бинарная классификация

Два класса (0 и 1). Сеть выдаёт одно число — логит z, сигмоида превращает его в вероятность класса 1:

p = \sigma(z) = \frac{1}{1 + e^{-z}}

Функция потерь — бинарная кросс-энтропия (BCE), минус логарифм вероятности правильного класса:

\ell = -\big[y \ln p + (1 - y)\ln(1 - p)\big] = \begin{cases} -\ln p, & y = 1 \\ -\ln(1 - p), & y = 0 \end{cases}

Её градиент по логиту поразительно прост — «предсказание минус ответ»:

\frac{\partial \ell}{\partial z} = p - y

Многоклассовая классификация

C классов. Сеть выдаёт C логитов, softmax превращает их в распределение, потеря — кросс-энтропия:

p = \text{softmax}(z), \qquad \ell = -\ln p_y, \qquad \frac{\partial \ell}{\partial z} = p - \text{onehot}(y)

Двигайте логиты и посмотрите, как меняются вероятности и потеря (правильный класс — «кошка»):

  • Потеря необученной модели (все логиты одинаковые) — \ln C: для 10 классов 2.30. Если обучение стартует с сильно другого значения, проверьте инициализацию выходного слоя.
  • Потеря 0 недостижима: для неё нужна вероятность ровно 1, то есть бесконечный логит. Поэтому без регуляризации логиты могут расти бесконечно, а модель — становиться излишне самоуверенной.

Почему не MSE и не точность

Точность (доля правильных ответов) — то, что нас интересует в итоге, но её нельзя оптимизировать градиентным спуском: она меняется скачками, и её градиент почти везде равен нулю.

MSE между вероятностями и one-hot формально работает, но хуже: вместе с сигмоидой/softmax градиент MSE содержит множитель p(1 - p), который почти обнуляется, когда модель уверенно ошибается (p \approx 0 для правильного класса). Именно тогда, когда учиться нужнее всего, градиент исчезает. У кросс-энтропии в той же ситуации градиент максимален.

import numpy as np
import vis

z = np.linspace(-6, 6, 300)          # логит правильного класса (бинарный случай, y = 1)
p = 1 / (1 + np.exp(-z))
vis.line(z, {"кросс-энтропия: |p − 1|": np.abs(p - 1), "MSE: |2(p − 1)·p(1 − p)|": np.abs(2 * (p - 1) * p * (1 - p))},
         title="Модуль градиента по логиту, когда правильный ответ 1", xlabel="логит z (слева — уверенная ошибка)")

Слева (модель уверенно ошибается) градиент кросс-энтропии близок к 1, а у MSE — к нулю.

Устойчивые вычисления

Считать \ln(\text{softmax}(z)) «в лоб» опасно: при больших логитах экспонента переполняется, а при маленьких вероятностях логарифм от нуля даёт -\infty. Поэтому:

  • softmax считают, вычитая максимум логита;
  • log-softmax — сразу по формуле z_k - \max z - \ln\sum_j e^{z_j - \max z};
  • BCE — через логит: \max(z, 0) - zy + \ln(1 + e^{-|z|}).

Поэтому функции потерь в библиотеках (nt.cross_entropy, nt.binary_cross_entropy, а в PyTorch — F.cross_entropy, BCEWithLogitsLoss) принимают логиты, а не вероятности. Частая ошибка — поставить softmax в модель и ещё раз применить его внутри функции потерь.

import numpy as np
import nanotorch as nt

logits = nt.tensor([[2.0, 1.0, 0.1], [0.2, 3.0, -1.0]], requires_grad=True)
targets = np.array([0, 1])
loss = nt.cross_entropy(logits, targets)
loss.backward()
print("потеря:", round(loss.item(), 4))
print("вероятности:\n", logits.softmax().data.round(3))
print("градиент = (p − onehot) / N:\n", logits.grad.round(3))

Пример: три спирали

import numpy as np
import nanotorch as nt
import vis

X, y = nt.make_spirals(450, classes=3, noise=0.15, seed=0)
X_train, X_test, y_train, y_test = nt.train_test_split(X, y, test_size=0.3, seed=0)

nt.manual_seed(0)
model = nt.Sequential(nt.Linear(2, 64), nt.ReLU(), nt.Linear(64, 64), nt.ReLU(), nt.Linear(64, 3))
hist = nt.fit(model, X_train, y_train, nt.cross_entropy, nt.Adam(model.parameters(), lr=0.01),
              epochs=100, batch_size=32, X_val=X_test, y_val=y_test, metric=nt.accuracy, verbose=False)
print(f"точность: train {hist['metric'][-1]:.1%}, test {hist['val_metric'][-1]:.1%}")
vis.line({"train": hist["loss"], "test": hist["val_loss"]}, title="Кросс-энтропия", xlabel="эпоха")

with nt.no_grad():
    vis.boundary(lambda P: model(nt.Tensor(P)).data.argmax(1), X, y, title="Предсказанный класс")
    probs = model(nt.Tensor(X_test)).softmax().data
conf = probs.max(axis=1)
print(f"средняя уверенность на тесте: {conf.mean():.2f}; на ошибках: {conf[probs.argmax(1) != y_test].mean():.2f}")

Обычно модель менее уверена на объектах, где ошибается, — уверенность можно использовать, например, чтобы отправлять сомнительные случаи человеку на проверку. Но нейросети склонны к излишней самоуверенности, и их вероятности не всегда можно понимать буквально; для этого существуют методы калибровки.

Несколько меток (multi-label)

Если у объекта может быть несколько классов одновременно (на фото и кошка, и собака), softmax не подходит: он заставляет вероятности конкурировать. Вместо этого каждый класс — отдельная бинарная задача: C логитов, сигмоида у каждого и BCE, усреднённая по классам.

Итог

  • Бинарная: 1 логит → сигмоида → BCE; многоклассовая: C логитов → softmax → кросс-энтропия; multi-label: C сигмоид → BCE.
  • Градиент по логитам — «вероятность минус ответ», не затухает при уверенных ошибках (в отличие от MSE).
  • Точность нельзя оптимизировать градиентом; кросс-энтропия — её гладкая замена.
  • Функции потерь принимают логиты и считают всё устойчиво; потеря необученной модели ≈ \ln C.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Заполнить таблицу

Какой выход и функцию потерь выбрать?

1 логит + сигмоида + BCEC логитов + softmax + CEC логитов + сигмоида у каждого + BCE
Письмо — спам или нет
Какая цифра (0–9) на картинке
Какие объекты есть на фото: кошка, собака, машина (может быть несколько сразу)
Тональность отзыва: негативная, нейтральная, позитивная
Болен ли пациент
Отметьте ячейку — «+», пустая — «−».
Задание 2 Один вариант ответа

Почему точность (accuracy) нельзя использовать как функцию потерь для градиентного спуска?

Задание 3 Ввод ответа

Для 4 классов модель выдала логиты [0, 0, 0, 0]. Чему равна кросс-энтропия (натуральный логарифм, два знака)?

Задание 4 Один вариант ответа

Логит бинарного классификатора равен 0. Какую вероятность класса 1 выдаёт модель?

Задание 5 Программа на Python

Устойчивая бинарная кросс-энтропия по логитам. Наивная формула -[y\ln\sigma(z) + (1-y)\ln(1-\sigma(z))] ломается при больших |z|: \sigma(z) округляется до 0 или 1, и логарифм даёт -\infty. Эквивалентная устойчивая форма:

\ell(z, y) = \max(z, 0) - z y + \ln\left(1 + e^{-|z|}\right)

Напишите bce_with_logits(z, y) (массивы логитов и меток 0/1) — среднее по объектам, float. Используйте np.log1p для \ln(1 + \cdot).

Какие тесты проверяют код (2)
z, y = np.array([0.5, -1.2, 2.0]), np.array([1.0, 0.0, 0.0])
p = 1 / (1 + np.exp(-z))
naive = -(y * np.log(p) + (1 - y) * np.log(1 - p)).mean()
assert abs(bce_with_logits(z, y) - naive) < 1e-12, "На обычных логитах результат должен совпадать с наивной формулой"

v = bce_with_logits(np.array([100.0, -100.0]), np.array([0.0, 1.0]))
assert np.isfinite(v) and abs(v - 100) < 1e-9, f"При логитах ±100 и неверных метках потеря должна быть 100, получено {v}"
assert bce_with_logits(np.array([50.0]), np.array([1.0])) < 1e-20

Задание 6 Программа на Python

Предсказание и уверенность. Напишите predict(logits) для многоклассовой модели: по матрице логитов (N, C) верните пару (classes, confidence) — номер класса с максимальной вероятностью и саму эту вероятность (после softmax) для каждого объекта. Без циклов, softmax — устойчивый.

Какие тесты проверяют код (2)
cls, conf = predict(np.array([[2.0, 1, 0.1], [0, 0, 0], [-1, 5, 5]]))
assert list(cls[:2]) == [0, 0] and cls[2] in (1, 2)
assert np.allclose(conf, [np.exp(2) / (np.exp(2) + np.e + np.exp(0.1)), 1 / 3, 0.5 / (1 + 0.5 * np.exp(-6))], atol=1e-6)

cls, conf = predict(np.array([[1000.0, 0]]))
assert cls[0] == 0 and np.isfinite(conf[0]) and abs(conf[0] - 1) < 1e-12

Войдите, чтобы отмечать пройденные уроки.