Статья Автор: Деникина Н.В., Деникин А.В.

Если ответ не число, а класс

До сих пор наша модель предсказывала число — например, время доставки. Но во многих задачах ответ устроен иначе: нужно выбрать один из двух классов.

Например:

  • успеет заказ или опоздает;
  • письмо — спам или не спам;
  • на изображении есть нужный объект или нет.

Условимся обозначать правильный ответ буквой y:

y = 1   — объект относится к классу 1
y = 0   — объект относится к классу 0

Сам градиентный спуск от этого не меняется. Мы по-прежнему будем считать градиент и обновлять параметры по правилу:

w = w − lr · gradient

Меняются две вещи:

  1. как из линейного выхода модели получить вероятность;
  2. какой функцией потерь измерять ошибку.

От линейного выхода к вероятности

Сначала модель, как и раньше, считает обычную линейную комбинацию признаков:

z = w₁ · x₁ + w₂ · x₂ + ... + b

Число z может быть любым: отрицательным, положительным, большим или маленьким. Но вероятность должна лежать между 0 и 1.

Для этого используют сигмоиду:

σ(z) = 1 / (1 + e−z)

Её значение всегда находится между нулём и единицей:

0 < σ(z) < 1

Дальше значение сигмоиды будем обозначать буквой p:

p = σ(z)

p будем интерпретировать как предсказанную моделью вероятность класса 1.

Например:

p = 0.9   → модель сильно склоняется к классу 1
p = 0.5   → модель не уверена
p = 0.1   → модель склоняется к классу 0

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

Почему не взять обычный порог

Можно было бы попробовать сделать совсем просто:

если z > 0:
    ответ = 1
иначе:
    ответ = 0

Для готового решения такое правило порога использовать можно. Но для обучения оно неудобно.

Такая ступенчатая функция почти везде имеет производную, равную нулю, а в точке скачка производная вообще не существует.

Градиентному спуску нужен наклон: именно по нему он понимает, как изменить веса. Если наклон равен нулю почти везде, программе не за что зацепиться.

Сигмоида, наоборот, изменяется плавно, поэтому через неё можно передавать градиент.

Почему для классификации используют кросс-энтропию

После сигмоиды мы могли бы взять уже знакомую MSE:

L = (p − y)²

Так сделать можно. Но в сочетании с сигмоидой MSE часто неудобна для обучения бинарной классификации.

Посмотрим на особенно неприятный случай: правильная метка равна единице,

y = 1

а модель очень уверена в обратном и выдала:

z = −6

Тогда:

p = σ(−6) ≈ 0.002473

То есть правильный ответ — 1, а модель почти уверена, что ответ 0.

Ошибка огромная. Кажется естественным ожидать большой градиент, чтобы модель быстро исправила параметры. Но у MSE происходит обратное.

z σ(z) наклон MSE по z наклон кросс-энтропии по z во сколько раз больше
−6 0.002473 −0.004921 −0.997527 202.7
−2 0.119203 −0.184956 −0.880797 4.8
0 0.5 −0.25 −0.5 2
2 0.880797 −0.025031 −0.119203 4.8

Важно: сейчас мы сравниваем не сами значения двух функций потерь, а их наклоны по отношению к z. Именно этот наклон определяет силу сигнала, который получит градиентный спуск.

Для MSE:

L = (p − y)²

По цепному правилу:

∂L/∂z
=
2 · (p − y) · p · (1 − p)

Первый множитель p − y действительно большой по модулю. При y = 1 и p ≈ 0.0025 он почти равен −1.

Но появляется ещё множитель:

p · (1 − p)

Это производная сигмоиды. В области, где сигмоида почти прижалась к нулю или единице, этот множитель становится очень маленьким.

В результате модель уверенно ошибается, а градиент MSE получается почти нулевым. Исправлять такую ошибку градиентный спуск будет очень медленно.


Верный ответ — единица, модель выдала z = −6, то есть σ(z) ≈ 0.0025. Что произойдёт с наклоном функции потерь (σ(z) − 1)² по z в этой точке?

Binary cross-entropy

Для бинарной классификации обычно используют другую функцию потерь — binary cross-entropy, или BCE:

L = −[ y · ln p + (1 − y) · ln(1 − p) ]

где:

  • y — правильная метка, 0 или 1;
  • p — вероятность класса 1, которую выдала модель.

Если правильный ответ равен единице, то y = 1:

L = −ln p

Если правильный ответ равен нулю, то y = 0:

L = −ln(1 − p)

В общей формуле одно из двух слагаемых всегда умножается на ноль. Поэтому для каждого объекта фактически работает только один из двух случаев.

Логарифм устроен так, что уверенная ошибка обходится модели очень дорого.

Например, если:

y = 1
p ≈ 0.0025

то модель почти уверена в неправильном классе, и значение BCE становится большим.

Удивительно простая производная

Главное удобство сочетания sigmoid + BCE становится видно при вычислении производной.

После всех преобразований получается:

∂L/∂z = p − y

Множитель

p · (1 − p)

из производной сигмоиды при выводе сокращается.

Поэтому вместо длинного выражения остаётся простая разность:

что предсказали − что было

p − y

Например, если:

y = 1
p = 0.2

то:

∂L/∂z = 0.2 − 1 = −0.8

Знак минус говорит, что для уменьшения потерь значение z нужно увеличить.

Если же:

y = 0
p = 0.8

то:

∂L/∂z = 0.8 − 0 = 0.8

Теперь градиент положительный, значит при шаге против градиента z будет уменьшаться.

От производной по z к производной по весу

Но модель обучает не само число z. Она меняет веса w₁, w₂, ... и свободный член b.

Вспомним линейную часть:

z = w₁ · x₁ + w₂ · x₂ + ... + b

Посмотрим на один вес w_j. Если остальные параметры не менять, то производная z по этому весу равна соответствующему признаку:

∂z/∂w_j = x_j

Теперь применяем цепное правило:

∂L/∂w_j
=
∂L/∂z
·
∂z/∂w_j

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

∂L/∂w_j = (p − y) · x_j

А для свободного члена:

∂z/∂b = 1

поэтому:

∂L/∂b = p − y

Сравним с линейной регрессией

В линейной регрессии с MSE вклад одного объекта в производную по весу был:

2 · промах · признак

Теперь, для логистической регрессии с sigmoid + BCE:

(p − y) · признак

Сама идея осталась той же:

  1. получить прогноз;
  2. сравнить его с правильным ответом;
  3. получить вклад этого объекта в градиент;
  4. обновить веса против градиента.

Один объект руками

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

x = 2

Правильная метка:

y = 1

Модель выдала:

p = 0.2

Сначала находим сигнал ошибки:

p − y = 0.2 − 1 = −0.8

Теперь вклад в производную по весу:

(p − y) · x
=
−0.8 · 2
=
−1.6

Если lr = 0.1, то обновление веса:

w_new
=
w_old − 0.1 · (−1.6)

w_new = w_old + 0.16

Вес увеличился. Это логично: правильный ответ был 1, а модель дала слишком маленькую вероятность. Нужно увеличить z, а значит в данном примере увеличить вес.

Что будет, если объектов много

Пока мы разобрали вклад одного объекта:

градиент по весу =
(p − y) · признак

Если объектов несколько, поступаем так же, как в линейной регрессии: считаем такой вклад для каждого объекта, складываем их и усредняем.

Например:

g1 = 0

для каждого объекта:
    посчитать p
    signal = p − y
    g1 += signal · x1

g1 = g1 / число_объектов

После этого делаем обычный шаг:

w1 = w1 − lr · g1

Но здесь появляется новый вопрос.

Что делать, если обучающих объектов не восемь и не восемьдесят, а несколько миллионов? Неужели перед каждым обновлением весов обязательно проходить всю огромную базу?

Необязательно. Градиент можно оценивать по небольшой части данных.

Именно к этому мы перейдём в следующем разделе: batch gradient descent, SGD и mini-batch обучение.

Главное из этого раздела

линейная часть:
z = w₁x₁ + w₂x₂ + ... + b

вероятность:
p = sigmoid(z)

правильный ответ:
y = 0 или 1

binary cross-entropy:
L = −[y ln p + (1 − y) ln(1 − p)]

градиент по z:
∂L/∂z = p − y

градиент по весу:
∂L/∂w_j = (p − y) · x_j

градиент по свободному члену:
∂L/∂b = p − y

В выходном слое именно с величины p − y начинается обратное распространение ошибки. В более глубокой сети дальше цепное правило пересчитает этот сигнал для весов предыдущих слоёв.

Печать