Зачем она нужна
Обычный классификатор знает фиксированный список классов. Но система распознавания лиц должна узнавать людей, которых не было в обучающей выборке, а система повторной идентификации (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, важна не величина ошибки, а сам факт нарушения. Стрелка якоря — сумма двух единичных векторов, её длина от 0 до 2. Включите квадраты расстояний — длины стрелок станут пропорциональны расстояниям.
Формула по косточкам
Запишем потерю полностью — с сетью, расстояниями и усреднением по тройкам:
- $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$: свой ближе, но зазора мало.
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$ — наклонная плоскость, линии уровня которой параллельны этим прямым; выше верхней прямой она обрезана нулём.
Как только сеть немного обучилась, подавляющее большинство случайных троек становится лёгкими: их потеря и градиент равны нулю. Усреднение по ним разбавляет полезный сигнал, и обучение почти останавливается. Отсюда майнинг — целенаправленный выбор трудных и полутрудных троек.
Градиенты: куда тянет каждую точку
Для активной тройки ($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)}$ по правилу цепочки:
Аналогично для $d_{an}$. Подставляем в $L=d_{ap}-d_{an}+m$:
Сумма трёх градиентов равна нулю: сдвиг всех трёх точек на один вектор не меняет расстояний, значит, и потеря не меняется. Для квадратов расстояний $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$.
С квадратами расстояний сила отталкивания $2\,d(a,n)$ тем слабее, чем ближе чужой к якорю — то есть именно для самых опасных ошибок. В обычной метрике она постоянна. Поэтому в Re-ID (Херманс и др., 2017) рекомендуют обычное евклидово расстояние; FaceNet использует квадраты, но на нормированных векторах и с полутрудными тройками.
Майнинг: какие тройки брать
В наборе из $N$ изображений троек порядка $N^3$ — перебрать их все нельзя, да и почти все лёгкие. Тройки выбирают онлайн, внутри батча. Батч строят по схеме $P\times K$: $P$ человек, по $K$ снимков каждого. Внутри такого батча
При $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)$ берут чужого, который дальше своего, но внутри зазора; так обучение мягче на старте.
Самые трудные тройки часто оказываются ошибками разметки или почти неразличимыми снимками. В начале обучения, когда эмбеддинги случайны, погоня за ними может «схлопнуть» модель: все $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. Ей не говорят, где какой человек должен оказаться, — только что свои ближе чужих.
Так же устроена 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\|$, все точки лежат на единичной сфере. Для единичных векторов
где $\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$.
Зазор измеряется в единицах расстояния, поэтому зависит от нормировки и от того, квадрат расстояния или нет. Типичные значения: $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 обычно комбинируют с кросс-энтропией по личностям — так обучение быстрее и устойчивее.
Частые ошибки
- Случайные батчи вместо $P\times K$. В случайном батче у многих якорей нет ни одного своего, и тройки не собираются.
- Обучение только на случайных тройках. Почти все они лёгкие, градиент нулевой, и потеря «застывает» на маленьком значении, хотя модель плохая.
- Самые трудные тройки с первого шага без нормировки и разогрева — риск схлопывания всех эмбеддингов в точку.
- Зазор без учёта масштаба. Без нормировки сеть растягивает пространство; на сфере $m$ нужно соотносить с максимумом расстояния (2 для $d$, 4 для $d^2$).
- Корень из нуля. Расстояние точки до самой себя на диагонали матрицы даёт NaN в градиенте, если не добавить $\varepsilon$ или не маскировать диагональ.
Связи с другими формулами
Ближайший родственник — контрастная потеря (contrastive loss): она работает с парами и абсолютными порогами, а не с относительным сравнением. Для пары с меткой $y=1$ (свои) и $y=0$ (чужие):
- 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: эмбеддинг плюс поиск ближайших соседей.