Зачем она нужна
Детектор объектов вроде RetinaNet проверяет на одной картинке около ста тысяч «якорей» (anchors) — прямоугольников, в каждом из которых может оказаться объект. Объектов на кадре единицы, всё остальное — фон. Большинство фоновых якорей тривиальны: небо, стена, асфальт. Модель быстро учится уверенно называть их фоном, но кросс-энтропия всё равно начисляет за каждый маленькую потерю. Сто тысяч маленьких потерь складываются в большую — и эта сумма заглушает сигнал от немногих трудных примеров, ради которых всё и затевалось.
Лин, Гоял, Гиршик, Хе и Доллар (2017) предложили не выбрасывать лёгкие примеры и не пересэмплировать данные, а изменить саму потерю: умножить кросс-энтропию на $(1-p_t)^\gamma$. Для уверенно распознанного примера множитель почти ноль, для трудного — почти единица. Где это используется:
- одностадийные детекторы (one-stage detectors) — RetinaNet, а за ней FCOS, ATSS и другие плотные детекторы;
- сегментация — особенно медицинская, где интересующая область занимает доли процента пикселей (часто в паре с Dice);
- табличные задачи с редким классом — мошенничество, отток, аномалии; в бустинге focal loss задают как пользовательскую целевую функцию;
- калибровка глубоких сетей — focal loss иногда применяют против переуверенности больших сетей, обученных на CE (Mukhoti и др., 2020).
$p_t$ — вероятность, которую модель дала правильному классу. $\mathrm{FL}=-(1-p_t)^\gamma\log p_t$: при $\gamma=0$ это обычная кросс-энтропия, при $\gamma=2$ пример с $p_t=0{,}9$ весит в 100 раз меньше, а с $p_t=0{,}3$ — всего в 2 раза. Множитель $\alpha_t$ дополнительно задаёт вес классов. Цена — FL не учит честным вероятностям: прогнозы смещаются к середине, и их калибровку надо проверять.
Интуиция: громкость для каждого примера
Представьте класс, где учитель отвечает на вопросы учеников. Тысяча учеников задают вопросы, ответ на которые они и так знают, и двадцать — действительно трудные. Если уделять каждому вопросу одинаковое время, на трудные его не останется. Focal loss — это регулятор громкости: чем увереннее модель уже отвечает на пример, тем тише его голос в общей сумме.
Потяните точку и меняйте $\gamma$. Серая пунктирная кривая — кросс-энтропия $-\ln p_t$. Синяя — focal loss: в правой половине, где примеры уже классифицированы верно, она прижимается к нулю гораздо раньше.
Focal loss почти не трогает трудные примеры и очень сильно гасит лёгкие. При $p_t\to0$ множитель $(1-p_t)^\gamma\to1$, и $\mathrm{FL}\approx\mathrm{CE}$. При $p_t\to1$ потеря убывает как $(1-p_t)^{\gamma+1}$, а не как $1-p_t$ у CE.
Формула по косточкам
- $p=\sigma(z)$
- вероятность класса 1, которую модель получила из логита $z$ (через сигмоиду; в многоклассовом случае — через softmax);
- $p_t$
- вероятность правильного класса: насколько модель права на этом примере; $p_t>0{,}5$ — пример классифицирован верно;
- $-\log p_t$
- обычная кросс-энтропия: ноль при $p_t=1$, бесконечность при $p_t\to0$;
- $(1-p_t)^\gamma$
- модулирующий множитель (modulating factor): «насколько модель ещё ошибается» в степени $\gamma$;
- $\gamma\ge0$
- параметр фокусировки (focusing parameter): $\gamma=0$ — CE, чем больше, тем сильнее гасятся лёгкие примеры; в RetinaNet $\gamma=2$;
- $\alpha_t$
- вес класса: $\alpha$ для позитивов и $1-\alpha$ для негативов; в RetinaNet $\alpha=0{,}25$.
Запись через $p_t$ — главный приём: вместо двух ветвей бинарной кросс-энтропии $-y\log p-(1-y)\log(1-p)$ получается одна, $-\log p_t$. В терминах логита удобно ввести $z_t=z$ при $y=1$ и $z_t=-z$ при $y=0$; тогда $p_t=\sigma(z_t)$.
| $p_t$ | $\mathrm{CE}$ | $(1-p_t)^2$ | $\mathrm{FL}_{\gamma=2}$ | $\mathrm{CE}/\mathrm{FL}$ |
|---|---|---|---|---|
| 0,1 | 2,303 | 0,81 | 1,865 | 1,23 |
| 0,3 | 1,204 | 0,49 | 0,590 | 2,04 |
| 0,5 | 0,693 | 0,25 | 0,173 | 4 |
| 0,7 | 0,357 | 0,09 | 0,0321 | 11,1 |
| 0,9 | 0,105 | 0,01 | 0,00105 | 100 |
| 0,99 | 0,0101 | 0,0001 | $1{,}0\cdot10^{-6}$ | 10 000 |
Последний столбец — во сколько раз focal loss меньше кросс-энтропии; это ровно $1/(1-p_t)^\gamma$. Пример из шпаргалки: при $\gamma=2$ лёгкий пример $p_t=0{,}9$ даёт $0{,}00105$ против $0{,}105$ (в 100 раз меньше), трудный $p_t=0{,}3$ — $0{,}590$ против $1{,}204$ (примерно вдвое).
На обычной шкале значения $0{,}001$ и $0{,}000001$ неразличимы, поэтому полезно посмотреть на потери в логарифмической шкале: там множитель превращается в вертикальный сдвиг.
Свойства
- Обобщает кросс-энтропию: $\gamma=0$, $\alpha_t=1$ дают $\mathrm{CE}=-\log p_t$.
- Не больше CE: $0\le\mathrm{FL}\le\mathrm{CE}$, потому что $0\le(1-p_t)^\gamma\le1$ при $\gamma\ge0$.
- Убывает по $p_t$: оба сомножителя $(1-p_t)^\gamma$ и $-\log p_t$ неотрицательны и убывают, поэтому чем увереннее верный ответ, тем меньше потеря.
- Асимптотика у края: при $p_t\to1$ $\mathrm{FL}\approx(1-p_t)^{\gamma+1}$, так как $-\log p_t\approx1-p_t$.
- Не выпукла по логиту при $\gamma>0$: для очень трудных примеров (при $\gamma=2$ — для $p_t<0{,}058$, $z_t<-2{,}79$) вторая производная по $z_t$ отрицательна. Кросс-энтропия по логиту выпукла всюду.
- Не является строго собственным правилом оценки (proper scoring rule): минимум ожидаемой FL достигается не на истинной вероятности. Поэтому вероятности, выученные на FL, нужно калибровать (раздел про $\alpha$).
Взвешивание классов ($\alpha$) меняет вес класса целиком, одинаково для лёгких и трудных примеров. Пересэмплирование и hard negative mining выбрасывают часть примеров по жёсткому правилу. Focal loss меняет вес каждого примера плавно, по тому, насколько модель уже справляется с ним, — и не требует сэмплирования.
Производная: градиент по логиту
Модель учится через градиент по логиту $z$, поэтому именно он показывает, кого «слышит» обучение. Для кросс-энтропии он прост: $\partial\,\mathrm{CE}/\partial z_t=-(1-p_t)$ — чем сильнее ошибка, тем сильнее градиент, монотонно. У focal loss появляется второй член.
Вывод по шагам
Шаг 1. Производная по $p_t$ — правило произведения для $-(1-p_t)^\gamma\log p_t$:
Шаг 2. Производная сигмоиды: $\partial p_t/\partial z_t=p_t(1-p_t)$ (см. сигмоиду).
Шаг 3. Цепное правило и вынесение общего множителя $(1-p_t)^\gamma$:
Шаг 4. Для исходного логита $z$: $z_t=\pm z$, поэтому $\partial\,\mathrm{FL}/\partial z=\pm\,\partial\,\mathrm{FL}/\partial z_t$ (плюс при $y=1$, минус при $y=0$), а $\alpha_t$ просто умножает результат. Проверка: при $\gamma=0$ получаем $p_t-1$, градиент кросс-энтропии.
В квадратных скобках — градиент CE, $1-p_t$, плюс неотрицательная добавка $\gamma p_t|\log p_t|$. Снаружи — тот же множитель $(1-p_t)^\gamma$. Для лёгких примеров множитель побеждает, а для очень трудных добавка может сделать градиент даже больше, чем у CE.
У CE чем хуже пример, тем сильнее он тянет. У FL с $\gamma>0$ самый сильный градиент — у примеров с $p_t$ около нескольких процентов, а дальше к $p_t\to0$ он снова падает до 1. Для $\gamma=1$ граница, где FL тянет сильнее CE, ровно $p_t=1/e\approx0{,}368$. Если в данных есть ошибочные метки, они как раз попадают в эту зону — focal loss будет усиленно их «учить».
Поверхность FL(p_t, γ) в 3D
Сведём всё семейство кривых в одну поверхность над плоскостью $(p_t,\gamma)$. Срез при $\gamma=0$ — кросс-энтропия; при движении по $\gamma$ поверхность «оседает», причём тем быстрее, чем больше $p_t$.
Почему прямая: $\log\mathrm{FL}=\log\mathrm{CE}+\gamma\log(1-p_t)$. По $\gamma$ потеря затухает экспоненциально, и скорость затухания — $|\ln(1-p_t)|$: для $p_t=0{,}5$ это $0{,}69$ на единицу $\gamma$, для $p_t=0{,}9$ — $2{,}30$, для $p_t=0{,}99$ — $4{,}61$. Параметр $\gamma$ раздвигает лёгкие и трудные примеры по шкале порядков.
Дисбаланс: откуда берётся потеря
Смоделируем ситуацию одностадийного детектора. На каждый позитив приходится 1000 негативов. Негативы в основном лёгкие: логит правильного класса $z_t\sim N(4,\,1)$, то есть типичный $p_t=\sigma(4)\approx0{,}98$. Позитивы трудные: $z_t\sim N(0,\,1)$, типичный $p_t=0{,}5$. Посчитаем, какая доля суммарной потери приходится на каждую группу.
Именно это и есть «фокусировка». Градиентный спуск минимизирует сумму, и в CE эта сумма почти целиком состоит из примеров, которые уже решены. Focal loss перераспределяет «бюджет внимания»: потери лёгких примеров гасятся в десятки и сотни раз, и в сумме начинают доминировать трудные негативы и позитивы — те, на которых модель действительно может улучшиться.
До 2017 года одностадийные детекторы заметно уступали двухстадийным (Faster R-CNN) по точности. Двухстадийные справлялись с дисбалансом за счёт этапа предложений и сэмплирования позитивов и негативов в пропорции 1 : 3. Лин и соавторы показали, что главная причина отставания — именно дисбаланс, и простая замена потери (RetinaNet = ResNet + FPN + focal loss) позволила одностадийному детектору превзойти двухстадийные. Работа получила награду за лучшую студенческую статью на ICCV 2017.
α-балансировка и калибровка
Множитель $\alpha_t$ — обычное взвешивание классов. Интуиция подсказывает дать редкому классу больший вес: $\alpha>0{,}5$. Но в RetinaNet лучшим оказалось $\alpha=0{,}25$ — позитивы весят меньше негативов. Причина в том, что $\gamma$ уже сделал работу: лёгкие негативы погашены, и теперь перевес скорее у позитивов. $\alpha$ и $\gamma$ надо подбирать вместе; авторы советуют $\gamma=2$, $\alpha=0{,}25$; в их опытах выигрыш над CE сохранялся при всех $\gamma$ от $0{,}5$ до $5$, а с ростом $\gamma$ лучшее $\alpha$ немного уменьшалось.
Есть и тонкость, о которой забывают. Возьмём самую простую «модель» — одно число $p$, одинаковое для всех примеров. Если доля позитивов $\pi$, то кросс-энтропия минимальна ровно при $p=\pi$: CE — собственное правило оценки, оно учит честным вероятностям. С focal loss это уже не так.
Вывод: модуль $(1-p_t)^\gamma$ тянет предсказания к середине — он «не любит» уверенность, потому что за уверенность перестают платить. Поэтому выход модели, обученной на focal loss, нельзя читать как вероятность: для порогов и ранжирования это нормально, а если нужны вероятности — откалибруйте их на валидации (Platt scaling, изотоническая регрессия, температурное шкалирование). Без $\alpha$ оптимальный прогноз FL — возрастающая функция истинной вероятности, равная $0{,}5$ при $0{,}5$ (Charoenphakdee и др., 2021): порядок и решение по порогу $0{,}5$ сохраняются, искажаются сами значения. А у больших сетей, которые на CE становятся переуверенными, этот сдвиг к середине иногда даже улучшает калибровку (Mukhoti и др., 2020) — но это эмпирика, а не гарантия.
В начале обучения сеть выдаёт $p\approx0{,}5$ для всех якорей, и потери 100 000 негативов взрывают первые шаги. Поэтому сдвиг последнего слоя инициализируют так, чтобы $p=\pi$: $b=-\ln\frac{1-\pi}{\pi}$; для $\pi=0{,}01$ это $b\approx-4{,}595$. Кроме того, сумму FL по всем якорям делят на число позитивных якорей, а не на число всех.
Focal loss в обучении
Обучим логистическую регрессию на двумерных данных с дисбалансом 1 : 20 (400 негативов, 20 позитивов) двумя способами — на CE и на FL — с одинаковым стартом (сдвиг инициализирован под долю позитивов, как в RetinaNet) и одинаковым шагом. Размер каждой точки пропорционален её градиенту $|\partial L/\partial z|$: так видно, какие примеры «слышит» обучение.
Что видно на рисунке. У CE крупные точки рассеяны по всему облаку негативов: каждый из 400 лёгких примеров добавляет свою небольшую порцию градиента. У FL крупными остаются только точки у границы и позитивы — остальные почти исчезают. Сама граница у двух моделей получается похожей: для линейной модели на таких данных оптимальное направление почти одно и то же. Разница в другом: FL приходит к своей границе за несколько десятков шагов и больше её не двигает, а CE продолжает наращивать веса, делая лёгкие примеры ещё увереннее. В глубокой сети, где параметров миллионы, это различие решающее: градиент, потраченный на «и так понятный» фон, — это ёмкость сети, не потраченная на трудные случаи.
Focal loss — не волшебное средство от дисбаланса. Если модель простая, а классы хорошо разделимы, она даст почти ту же границу, что и CE, только с менее уверенными вероятностями. Выигрыш появляется там, где лёгких примеров на порядки больше, чем трудных, и они реально отнимают градиент: плотная детекция, сегментация, большие таблицы с редким классом.
В коде
Считайте focal loss по логитам, а не по вероятностям: $\log p_t=\log\sigma(z_t)=-\operatorname{softplus}(-z_t)$ — без $\log0$ и переполнения. Ниже — NumPy-версия с градиентом и проверкой на примере из шпаргалки.
pythonimport numpy as np
def log_sigmoid(z):
return -np.logaddexp(0.0, -z) # log σ(z) без переполнения
def focal_loss(z, y, gamma=2.0, alpha=None):
"""Бинарная focal loss по логитам z и меткам y ∈ {0, 1}, поэлементно."""
zt = np.where(y == 1, z, -z) # логит правильного класса: p_t = σ(z_t)
log_pt = log_sigmoid(zt)
pt = np.exp(log_pt)
at = 1.0 if alpha is None else np.where(y == 1, alpha, 1 - alpha)
return -at * (1 - pt) ** gamma * log_pt
def focal_grad(z, y, gamma=2.0, alpha=None):
"""dFL/dz = ±α_t (1 − p_t)^γ (γ p_t log p_t + p_t − 1)."""
zt = np.where(y == 1, z, -z)
log_pt = log_sigmoid(zt)
pt = np.exp(log_pt)
at = 1.0 if alpha is None else np.where(y == 1, alpha, 1 - alpha)
g = at * (1 - pt) ** gamma * (gamma * pt * log_pt + pt - 1)
return np.where(y == 1, g, -g)
z = np.array([np.log(9.0), np.log(3 / 7), -np.log(9.0)]) # p_t = 0.9, 0.3, 0.9
y = np.array([1, 1, 0])
print(focal_loss(z, y, gamma=0.0).round(4)) # [0.1054 1.204 0.1054] — это CE
print(focal_loss(z, y).round(5)) # [0.00105 0.58995 0.00105]
print(focal_grad(z, y, gamma=0.0).round(4)) # [-0.1 -0.7 0.1]
print(focal_grad(z, y).round(4)) # [-0.0029 -0.697 0.0029]
В PyTorch бинарная версия есть в torchvision; многоклассовую легко написать через log_softmax. Последняя строка — инициализация сдвига под редкий класс.
pythonimport math
import torch
import torch.nn as nn
from torchvision.ops import sigmoid_focal_loss
# Бинарная (по каждому якорю и классу): α = 0.25, γ = 2 — как в RetinaNet
logits = torch.tensor([math.log(9.0), math.log(3 / 7)])
targets = torch.tensor([1.0, 1.0])
loss = sigmoid_focal_loss(logits, targets, alpha=0.25, gamma=2.0, reduction="none")
# ≈ [0.00026, 0.14749] = 0.25 · [0.00105, 0.58995]
# Многоклассовая версия через log_softmax
def focal_loss_multiclass(logits, target, gamma=2.0):
logp = torch.log_softmax(logits, dim=-1)
logp_t = logp.gather(-1, target[:, None]).squeeze(-1)
return (-(1 - logp_t.exp()) ** gamma * logp_t).mean()
# Инициализация сдвига последнего слоя под редкий класс (π = 0.01)
cls_head = nn.Conv2d(256, 9 * 80, kernel_size=3, padding=1) # A = 9 якорей × K = 80 классов
nn.init.constant_(cls_head.bias, -math.log((1 - 0.01) / 0.01)) # ≈ −4.595
Численная производная $\bigl(\mathrm{FL}(z+\varepsilon)-\mathrm{FL}(z-\varepsilon)\bigr)/2\varepsilon$ при $\varepsilon=10^{-6}$ совпадает с focal_grad до четырёх знаков: $-0{,}0029$, $-0{,}697$, $0{,}0029$. При $p_t=0{,}3$ градиенты FL и CE почти равны ($0{,}697$ против $0{,}7$): это рядом с точкой пересечения $p_t\approx0{,}298$ на рисунке 3.
Частые ошибки
- $\alpha$ «по частоте» вместе с $\gamma$. Ставить $\alpha=0{,}99$ для класса с долей 1% при $\gamma=2$ — двойная компенсация. Начинайте с $\gamma=2$, $\alpha=0{,}25$ и подбирайте их вместе.
- Нет инициализации под редкий класс. Со сдвигом 0 первые итерации тонут в потерях фона; в статье RetinaNet обучение с обычной CE без инициализации $\pi=0{,}01$ расходилось.
- Нормировка на число всех примеров. Среднее по 100 000 якорей делает потерю крошечной и зависит от размера картинки; в детекции делят на число позитивов.
- Focal loss по вероятностям с
clamp. $\log(\max(p,\varepsilon))$ обрывает градиент логарифма у самых трудных примеров ($p<\varepsilon$) — там, где он нужнее всего; а $p=\sigma(z)$ к тому же насыщается в float32. Считайте по логитам. - Чтение выхода как вероятности. FL смещает прогнозы к середине (рисунок 6); для оценки рисков и ставок вероятности нужно калибровать.
- Большое $\gamma$ при шумных метках. Ошибочные метки выглядят как очень трудные примеры, а FL усиливает их относительный вес. При подозрении на шум уменьшайте $\gamma$ или чистите разметку.
Связи с другими формулами
- BCE и кросс-энтропия: FL с $\gamma=0$, $\alpha_t=1$ совпадает с ними.
- Сигмоида: $p_t=\sigma(z_t)$, и её производная $p_t(1-p_t)$ входит в градиент FL.
- Softmax: в многоклассовом случае $p_t$ — компонента softmax для правильного класса.
- Хьюбер — противоположная философия: Хьюбер ослабляет вклад самых больших ошибок, считая их выбросами, а FL ослабляет самые маленькие, чтобы большие стали заметнее.
- Triplet loss решает родственную задачу в обучении метрик: лёгкие тройки с нулевой потерей не дают градиента, и обучение фокусируется на трудных (hard negative mining).
Итоги
1) $\mathrm{FL}=-\alpha_t(1-p_t)^\gamma\log p_t$ — кросс-энтропия, умноженная на «степень ошибки» $(1-p_t)^\gamma$; $\gamma=0$ — CE. 2) Лёгкие примеры гасятся как $(1-p_t)^{\gamma+1}$: при $\gamma=2$ пример с $p_t=0{,}9$ весит в 100 раз меньше. 3) Градиент по логиту $(1-p_t)^\gamma(\gamma p_t\log p_t+p_t-1)$ не монотонен: при $\gamma=2$ максимум у $p_t\approx0{,}06$. 4) При дисбалансе 1 : 1000 FL переносит основную часть потерь с лёгких негативов на позитивы и трудные негативы. 5) Практика RetinaNet: $\gamma=2$, $\alpha=0{,}25$, инициализация $b=-\ln\frac{1-\pi}{\pi}$, нормировка на число позитивов; вероятности на выходе нужно калибровать.