Шпаргалка формул нейросетей · Следующий уровень

Triplet Loss: свой ближе чужого

Функция потерь, которая учит не классы, а расстояния: якорь должен быть ближе к «своему», чем к «чужому», хотя бы на зазор m. Разбираем по косточкам формулу, три режима троек, градиенты, майнинг batch-hard, нормализацию на сфере и то, как на этой математике держатся FaceNet и повторная идентификация людей (Re-ID).

Javharbek Abdulatipov · 35 мин чтения
$$L(a,p,n)=\max\bigl(0,\;d(a,p)-d(a,n)+m\bigr)$$

Зачем она нужна

Обычный классификатор знает фиксированный список классов. Но система распознавания лиц должна узнавать людей, которых не было в обучающей выборке, а система повторной идентификации (re-identification, Re-ID) — находить того же человека на записи другой камеры, в другом ракурсе и при другом свете. Переобучать сеть ради каждого нового человека невозможно.

Выход — учить не классы, а метрику (metric learning). Сеть $f$ превращает изображение в вектор-эмбеддинг (embedding) $f(x)\in\mathbb R^d$ так, чтобы снимки одного человека оказывались рядом, а разных — далеко. Тогда «кто это?» сводится к поиску ближайших соседей по расстоянию, и новый человек добавляется одним эмбеддингом в базу.

Triplet Loss задаёт это требование напрямую. Берём тройку: якорь $a$ (anchor), свой $p$ (positive, тот же человек) и чужой $n$ (negative, другой человек). Требуем, чтобы свой был ближе чужого хотя бы на зазор $m$ (margin).

  • распознавание лиц — FaceNet (Google, 2015): 128-мерные эмбеддинги, зазор $0{,}2$, точность $99{,}63\%$ на наборе LFW;
  • повторная идентификация людей между камерами — batch-hard triplet loss (Херманс и др., 2017); в библиотеке torchreid его можно обучать вместе с кросс-энтропией по личностям (движок ImageTripletEngine);
  • поиск похожих изображений, товаров, голосов и вообще любой поиск по смыслу, где вопрос звучит «что ближе к этому образцу?».
Главное за минуту

$L=\max\bigl(0,\,d(a,p)-d(a,n)+m\bigr)$. Если чужой дальше своего больше чем на $m$, потеря равна нулю и тройка ничему не учит. Иначе градиент тянет своего к якорю и отталкивает чужого. Почти все случайные тройки лёгкие, поэтому главное искусство — выбирать трудные (майнинг), например самого далёкого своего и самого близкого чужого в батче (batch-hard).

Интуиция: свой ближе чужого

Нарисуем эмбеддинги на плоскости. Вокруг якоря $a$ проведём окружность через своего $p$ — её радиус $d(a,p)$. Добавим вторую, шире на зазор: радиус $d(a,p)+m$. Требование формулы простое: чужой должен лежать за внешней окружностью. Попал в оранжевое кольцо или внутрь — потеря положительна.

Рисунок 1. Тяните якорь, своего и чужого. Оранжевые стрелки — направления, в которых градиентный спуск сдвинет каждую точку. В начальном положении $d(a,p)=0{,}9$, $d(a,n)=1{,}0$, $m=0{,}2$: чужой внутри кольца, $L=0{,}9-1{,}0+0{,}2=0{,}1$. Вытащите его за кольцо — стрелки исчезнут.
Что заметить

Стрелки у своего и чужого всегда направлены вдоль линий к якорю, а в обычной (не квадратной) метрике их длины одинаковы при любом положении точек: норма градиента равна 1, важна не величина ошибки, а сам факт нарушения. Стрелка якоря — сумма двух единичных векторов, её длина от 0 до 2. Включите квадраты расстояний — длины стрелок станут пропорциональны расстояниям.

Формула по косточкам

Запишем потерю полностью — с сетью, расстояниями и усреднением по тройкам:

$$\mathcal L=\frac{1}{|T|}\sum_{(a,p,n)\in T}\max\Bigl(0,\;\bigl\|f(x_a)-f(x_p)\bigr\|-\bigl\|f(x_a)-f(x_n)\bigr\|+m\Bigr)$$
$f(x)$
эмбеддинг: выход сети для изображения $x$, вектор из $\mathbb R^d$ (в FaceNet $d=128$), часто нормированный на единичную длину;
$d(a,p)$
расстояние до своего, $\|f(x_a)-f(x_p)\|$; его хотим уменьшить;
$d(a,n)$
расстояние до чужого, $\|f(x_a)-f(x_n)\|$; его хотим увеличить;
$m>0$
зазор: чужой должен быть дальше своего не просто, а с запасом $m$;
$\max(0,\cdot)$
шарнир (hinge): выполненное требование не штрафуется и не даёт градиента;
$T$
множество троек, по которому усредняем, — обычно тройки внутри батча (см. раздел о майнинге).

Удобно ввести одну переменную — разность расстояний $\Delta=d(a,p)-d(a,n)$. Тогда $L=\max(0,\Delta+m)$: это ReLU, сдвинутая на $m$. Отрицательная $\Delta$ значит, что свой уже ближе; но пока $\Delta>-m$, запаса не хватает.

Примеры при $m=0{,}2$: $d(a,p)=0{,}4$, $d(a,n)=1{,}1$ дают $\Delta=-0{,}7$ и $L=\max(0;\,-0{,}5)=0$ — тройка уже разделена. $d(a,p)=0{,}9$, $d(a,n)=1{,}0$ дают $\Delta=-0{,}1$ и $L=0{,}1$: свой ближе, но зазора мало.

Рисунок 2. Потеря как функция $\Delta$ (сверху) и её производная (снизу). Производная — ступенька: 0 для выполненных троек и 1 для нарушенных, без промежуточных значений. Пунктир — «мягкий зазор» (soft margin) $\ln(1+e^{\Delta})$ из работы Херманса и др.: он никогда не обнуляется, а его производная — сигмоида $\sigma(\Delta)$. При $\Delta=-0{,}1$: hinge даёт $0{,}1$, мягкий вариант — $0{,}644$.
Зазор — относительный

Triplet Loss не требует, чтобы свои были ближе какого-то фиксированного порога, — только ближе чужих. Поэтому у разных людей кластеры могут иметь разный размер. Порог для ответа «это тот же человек» подбирают потом, на валидации.

Три режима: лёгкие, полутрудные, трудные

Каждая тройка попадает в один из трёх режимов в зависимости от $d(a,p)$ и $d(a,n)$:

РежимУсловие$L$Градиент
лёгкая (easy)$d(a,n)\ge d(a,p)+m$$0$нет
полутрудная (semi-hard)$d(a,p)<d(a,n)<d(a,p)+m$от $0$ до $m$есть
трудная (hard)$d(a,n)\le d(a,p)$не меньше $m$есть

На плоскости $(d(a,p),\,d(a,n))$ границы режимов — две параллельные прямые: $d(a,n)=d(a,p)$ и $d(a,n)=d(a,p)+m$. Потеря $d(a,p)-d(a,n)+m$ — наклонная плоскость, линии уровня которой параллельны этим прямым; выше верхней прямой она обрезана нулём.

Рисунок 3. Потеря на плоскости $(d(a,p),\,d(a,n))$: чем насыщеннее красный, тем она больше. Точки — все 576 троек батча из рисунка 5 (4 человека по 4 снимка). При $m=0{,}2$ из них 510 лёгких, 31 полутрудная и 35 трудных: только 66 троек из 576 дают градиент. Увеличьте зазор — полоса полутрудных расширится.
Почему случайные тройки бесполезны

Как только сеть немного обучилась, подавляющее большинство случайных троек становится лёгкими: их потеря и градиент равны нулю. Усреднение по ним разбавляет полезный сигнал, и обучение почти останавливается. Отсюда майнинг — целенаправленный выбор трудных и полутрудных троек.

Градиенты: куда тянет каждую точку

Для активной тройки ($L>0$) потеря равна $d(a,p)-d(a,n)+m$, и всё сводится к градиенту расстояния. Для евклидовой нормы $\nabla_u\|u-v\|=\frac{u-v}{\|u-v\|}$ при $u\ne v$ — единичный вектор от $v$ к $u$ (при $u=v$ норма не дифференцируема; её субградиенты — все векторы длины не больше 1, в коде обычно берут 0).

Вывод градиентов по трём точкам

Обозначим $a,p,n$ эмбеддинги и $d_{ap}=\|a-p\|$, $d_{an}=\|a-n\|$. Дифференцируем $d_{ap}=\sqrt{(a-p)^\top(a-p)}$ по правилу цепочки:

$$\nabla_a d_{ap}=\frac{1}{2d_{ap}}\cdot2(a-p)=\frac{a-p}{d_{ap}},\qquad \nabla_p d_{ap}=\frac{p-a}{d_{ap}}$$

Аналогично для $d_{an}$. Подставляем в $L=d_{ap}-d_{an}+m$:

$$\nabla_p L=\frac{p-a}{d_{ap}},\qquad \nabla_n L=-\frac{n-a}{d_{an}},\qquad \nabla_a L=\frac{a-p}{d_{ap}}-\frac{a-n}{d_{an}}$$

Сумма трёх градиентов равна нулю: сдвиг всех трёх точек на один вектор не меняет расстояний, значит, и потеря не меняется. Для квадратов расстояний $L=d_{ap}^2-d_{an}^2+m$ получаем $\nabla_pL=2(p-a)$, $\nabla_nL=-2(n-a)$ и $\nabla_aL=2(n-p)$ — у якоря градиент вообще не зависит от его положения.

Шаг градиентного спуска идёт против градиента: свой $p$ сдвигается к якорю, чужой $n$ — от якоря, а якорь — к своему и от чужого. В обычной метрике сдвиги своего и чужого имеют длину 1 при любых расстояниях (сдвиг якоря — от 0 до 2); в квадратной — $2\,d(a,p)$ и $2\,d(a,n)$, то есть пропорциональны расстояниям.

Это видно на 3D-поверхности. Закрепим якорь в начале координат и своего на расстоянии $0{,}6$; как зависит потеря от положения чужого $n=(x,y)$? Получается перевёрнутый конус: в центре $L=0{,}6+m$, к краю круга радиуса $0{,}6+m$ — ноль. Склон конуса везде одинаковый, $|\nabla L|=1$.

Рисунок 4. Потеря как функция положения чужого (или своего) при закреплённом якоре; поверхность можно вращать. Справа — срез вдоль луча из якоря: сама потеря и норма градиента. При $m=0{,}4$ и обычном расстоянии конус имеет высоту $1{,}0$ и радиус $1{,}0$. Переключитесь на $d^2$: поверхность станет параболоидом высотой $0{,}76$ и радиусом $0{,}872$, а градиент у самого якоря обнулится. Для своего (чужой на расстоянии $1{,}2$, обычное расстояние) — воронка: потеря растёт за окружностью радиуса $1{,}2-0{,}4=0{,}8$.
Квадрат расстояния и трудные чужие

С квадратами расстояний сила отталкивания $2\,d(a,n)$ тем слабее, чем ближе чужой к якорю — то есть именно для самых опасных ошибок. В обычной метрике она постоянна. Поэтому в Re-ID (Херманс и др., 2017) рекомендуют обычное евклидово расстояние; FaceNet использует квадраты, но на нормированных векторах и с полутрудными тройками.

Майнинг: какие тройки брать

В наборе из $N$ изображений троек порядка $N^3$ — перебрать их все нельзя, да и почти все лёгкие. Тройки выбирают онлайн, внутри батча. Батч строят по схеме $P\times K$: $P$ человек, по $K$ снимков каждого. Внутри такого батча

$$|T|=\underbrace{PK}_{\text{якорей}}\cdot\underbrace{(K-1)}_{\text{своих}}\cdot\underbrace{(P-1)K}_{\text{чужих}}$$

При $P=4$, $K=4$ это 576 троек, при $P=18$, $K=4$ (такой батч использовали Херманс и др.) — уже 14 688. Основные стратегии:

  • batch-all — берём все тройки батча и усредняем потерю только по активным ($L>0$), чтобы лёгкие не разбавляли сигнал (у Херманса и др. — вариант BA$_{\ne0}$);
  • batch-hard — для каждого якоря одна тройка: самый далёкий свой и самый близкий чужой; $PK$ троек, самые информативные;
  • semi-hard (FaceNet) — для пары $(a,p)$ берут чужого, который дальше своего, но внутри зазора; так обучение мягче на старте.
$$\mathcal L_{\text{BH}}=\frac{1}{PK}\sum_{a}\max\Bigl(0,\;\max_{p:\,y_p=y_a}d(a,p)-\min_{n:\,y_n\ne y_a}d(a,n)+m\Bigr)$$
Рисунок 5. Батч $P=4$, $K=4$ в пространстве эмбеддингов. Выберите якорь и стратегию. В режиме batch-hard жирные линии ведут к самому далёкому своему и самому близкому чужому, пунктирная окружность — радиус $d_{\max}(a,p)+m$. При $m=0{,}2$ batch-hard даёт $L=0{,}353$, batch-all — $0{,}306$ по 66 активным тройкам из 576. У первого якоря из 36 троек 34 лёгкие и 2 полутрудные.
Опасность самых трудных

Самые трудные тройки часто оказываются ошибками разметки или почти неразличимыми снимками. В начале обучения, когда эмбеддинги случайны, погоня за ними может «схлопнуть» модель: все $f(x)$ стягиваются в одну точку, где $L=m$ для любой тройки. Помогают полутрудные тройки на старте, нормировка эмбеддингов, мягкий зазор и комбинация с кросс-энтропией.

Как это работает в обучении

Соберём всё вместе. Пусть у каждого снимка два признака: $x_1$ — помеха (ракурс, освещение), которая сильно меняется у одного и того же человека, и $x_2$ — признак личности, слегка искривлённый помехой. Во входном пространстве два снимка одного человека могут быть дальше друг от друга, чем снимки разных людей: верно упорядочены только $62{,}7\%$ троек.

Маленькая сеть $f:\mathbb R^2\to\mathbb R^2$ (12 нейронов tanh) учится с batch-hard triplet loss при $m=0{,}5$ и оптимизатором Adam. Ей не говорят, где какой человек должен оказаться, — только что свои ближе чужих.

Рисунок 6. Слева — входы 4 человек по 12 снимков, справа — их эмбеддинги, внизу — потеря. Нажмите «Пуск». В начале $L=1{,}266$, все 48 якорей активны, верно упорядочено $64{,}9\%$ троек. К шагу 60 упорядочены все $100\%$, но зазора ещё не хватает 42 якорям; к шагу 300 активных остаётся 4, к шагу 600 потеря равна нулю. Сеть «выпрямила» полосы: несмотря на помеху $x_1$, каждый снимок теперь ближе ко всем своим, чем к любому чужому, с запасом $0{,}5$.

Так же устроена Re-ID: запрос (query) — снимок человека с одной камеры, галерея (gallery) — снимки со всех остальных. Эмбеддинги галереи сортируют по расстоянию до запроса и смотрят, на каком месте первый верный ответ. Метрики — Rank-1 (верный ответ на первом месте) и mAP (средняя точность по всем верным).

Откуда это пошло

Идея зазора между своими и чужими — из метода ближайших соседей с большим зазором (LMNN, Вайнбергер, Блитцер и Сол, 2005). Пары своих и чужих с зазором ввели в контрастной потере (Хадселл, Чопра и Лекун, 2006). Шрофф, Калениченко и Филбин сделали тройки основой FaceNet (2015), а Херманс, Байер и Лейбе в работе «In Defense of the Triplet Loss for Person Re-Identification» (2017) показали, что с batch-hard майнингом тройки хорошо работают и в Re-ID.

Нормализация на сфере и выбор зазора

Без ограничений сеть может увеличивать все расстояния, просто растягивая пространство: в обычной метрике зазор $m$ теряет смысл, если масштаб не фиксирован. Поэтому эмбеддинги часто нормируют: $\hat f=f/\|f\|$, все точки лежат на единичной сфере. Для единичных векторов

$$\|u-v\|^{2}=\|u\|^2+\|v\|^2-2\,u^\top v=2-2\cos\theta$$

где $\theta$ — угол между векторами. Квадрат расстояния меняется от 0 (совпадают) до 4 (противоположны), и зазор $m=0{,}2$ в FaceNet — это $5\%$ всего диапазона. Требование $d^2(a,n)\ge d^2(a,p)+m$ превращается в требование к углам: $\cos\theta_{an}\le\cos\theta_{ap}-m/2$.

Рисунок 7. Нормированные эмбеддинги на единичной окружности (тяните точки по ней) и квадрат расстояния как функция угла. Оранжевая дуга — зона, где чужой нарушал бы зазор. При $\theta_{ap}=40^\circ$ и $m=0{,}2$ чужой должен быть не ближе $48{,}2^\circ$ от якоря; в начальном положении он на $45^\circ$: $d^2(a,p)=0{,}468$, $d^2(a,n)=0{,}586$, $L=0{,}082$.
Как выбрать зазор

Зазор измеряется в единицах расстояния, поэтому зависит от нормировки и от того, квадрат расстояния или нет. Типичные значения: $0{,}2$ в FaceNet ($d^2$ на нормированных эмбеддингах) и $0{,}3$ в torchreid (обычное $d$ на ненормированных признаках); у nn.TripletMarginLoss по умолчанию $m=1$. Слишком маленький зазор почти не требует разделения, слишком большой может быть недостижим и держит все тройки активными.

В коде: батч-майнинг и устойчивость

Ключ к эффективности — матрица всех попарных расстояний батча, из которой майнинг берётся масками без циклов. Численная тонкость: $\|u-v\|$ не дифференцируема при $u=v$ (производная корня в нуле бесконечна), а через формулу $\|u\|^2-2u^\top v+\|v\|^2$ из-за округления может получиться крошечное отрицательное число. Поэтому под корень ставят $\max(\cdot,\varepsilon)$, а nn.TripletMarginLoss добавляет $\varepsilon=10^{-6}$ к разности векторов. У неё есть и параметр swap=True — «перестановка расстояний» (distance swap, Балнтас и др., 2016): вместо $d(a,n)$ берётся $\min\bigl(d(a,n),\,d(p,n)\bigr)$, то есть чужой сравнивается с более близкой из двух «своих» точек.

pythonimport numpy as np

def pairwise_dist(E, eps=1e-12):
    """Матрица евклидовых расстояний между строками E (n × d)."""
    sq = (E * E).sum(1)
    d2 = sq[:, None] - 2.0 * E @ E.T + sq[None, :]
    return np.sqrt(np.maximum(d2, eps))     # max: защита от −1e-16 и корня из нуля

def batch_hard_triplet(E, labels, margin=0.3):
    D = pairwise_dist(E)
    same = labels[:, None] == labels[None, :]
    hardest_pos = np.where(same, D, 0.0).max(1)       # самый далёкий «свой»
    hardest_neg = np.where(same, np.inf, D).min(1)    # самый близкий «чужой»
    has_pos = same.sum(1) > 1                         # якорь без «своих» в батче пропускаем
    loss = np.maximum(hardest_pos - hardest_neg + margin, 0.0)
    return loss[has_pos].mean()

E = np.array([[0.0, 0.0], [0.9, 0.0], [0.6, 0.8]])   # якорь, свой, чужой
labels = np.array([0, 0, 1])
print(batch_hard_triplet(E, labels, margin=0.2))      # ≈ 0.1728 = (0.1 + 0.2456) / 2: у чужого нет «своих»
pythonimport torch
import torch.nn as nn
import torch.nn.functional as F

# 1) Тройки собраны заранее: готовая потеря (по умолчанию margin=1.0, p=2, swap=False)
loss_fn = nn.TripletMarginLoss(margin=0.2, p=2)
a = torch.tensor([[0.0, 0.0]], requires_grad=True)
p = torch.tensor([[0.9, 0.0]], requires_grad=True)
n = torch.tensor([[0.6, 0.8]], requires_grad=True)
loss = loss_fn(a, p, n)            # max(0, 0.9 − 1.0 + 0.2) ≈ 0.1
loss.backward()
print(p.grad, n.grad)              # ≈ [1, 0] и [−0.6, −0.8]: единичные векторы

# 2) Майнинг внутри батча P × K (batch-hard)
def batch_hard(emb, labels, margin=0.3, normalize=True):
    if normalize:
        emb = F.normalize(emb, dim=1)          # точки на единичной сфере
    D = torch.cdist(emb, emb)                  # n × n евклидовых расстояний
    same = labels[:, None] == labels[None, :]
    pos = (D * same).max(1).values             # самый далёкий «свой»
    neg = D.masked_fill(same, float("inf")).min(1).values   # самый близкий «чужой»
    has_pos = same.sum(1) > 1                  # якорь без «своих» в батче пропускаем
    return F.relu(pos - neg + margin)[has_pos].mean()
Практика

Батч $P\times K$ (например, $P=16$, $K=4$) нужен обязательно: без нескольких снимков одного человека в батче не будет своих. Triplet loss обычно комбинируют с кросс-энтропией по личностям — так обучение быстрее и устойчивее.

Частые ошибки

  1. Случайные батчи вместо $P\times K$. В случайном батче у многих якорей нет ни одного своего, и тройки не собираются.
  2. Обучение только на случайных тройках. Почти все они лёгкие, градиент нулевой, и потеря «застывает» на маленьком значении, хотя модель плохая.
  3. Самые трудные тройки с первого шага без нормировки и разогрева — риск схлопывания всех эмбеддингов в точку.
  4. Зазор без учёта масштаба. Без нормировки сеть растягивает пространство; на сфере $m$ нужно соотносить с максимумом расстояния (2 для $d$, 4 для $d^2$).
  5. Корень из нуля. Расстояние точки до самой себя на диагонали матрицы даёт NaN в градиенте, если не добавить $\varepsilon$ или не маскировать диагональ.

Ближайший родственник — контрастная потеря (contrastive loss): она работает с парами и абсолютными порогами, а не с относительным сравнением. Для пары с меткой $y=1$ (свои) и $y=0$ (чужие):

$$L_{\text{contr}}=y\,d^{2}+(1-y)\,\max(0,\;m-d)^{2}$$
  • ReLU: шарнир $\max(0,\Delta+m)$ — это ReLU от сдвинутой разности расстояний; его производная — ступенька.
  • Сигмоида: производная мягкого зазора $\ln(1+e^\Delta)$ равна $\sigma(\Delta)$.
  • Softmax и кросс-энтропия: N-pair loss и InfoNCE обобщают тройку на одного своего против многих чужих через softmax по сходствам; ArcFace переносит зазор в угол внутри softmax.
  • MSE: квадрат евклидова расстояния — та же сумма квадратов разностей.
  • Focal Loss: та же идея «учиться на трудных», только через вес примера, а не через выбор троек.
  • Adam: оптимизатор симуляции на рисунке 6.

Итоги

1) $L=\max(0,\,d(a,p)-d(a,n)+m)$: свой должен быть ближе чужого с запасом $m$; это ReLU от $\Delta+m$. 2) Режимы: лёгкие ($L=0$, нет градиента), полутрудные ($0<L<m$), трудные ($L\ge m$). 3) Градиенты — единичные векторы вдоль линий к якорю (для $d^2$ — пропорциональные расстояниям), их сумма равна нулю. 4) Тройки выбирают в батче $P\times K$: batch-all, batch-hard, semi-hard. 5) Нормировка на сфере даёт $d^2=2-2\cos\theta$ и осмысленный зазор. 6) На этом держатся FaceNet и Re-ID: эмбеддинг плюс поиск ближайших соседей.

Проверь себя

1. $m=0{,}2$, $d(a,p)=0{,}9$, $d(a,n)=1{,}0$. Чему равна потеря?
2. Тройка с $d(a,p)=0{,}5$, $d(a,n)=0{,}4$ при $m=0{,}2$ — это…
3. Куда направлен шаг градиентного спуска для чужого $n$ в активной тройке (обычная метрика)?
4. Что выбирает batch-hard для каждого якоря?
5. Для единичных векторов под углом $90^\circ$ квадрат расстояния равен…