Schroff, Kalenichenko, Philbin — FaceNet, arXiv:1503.03832 · Wang et al. — CosFace, arXiv:1801.09414 · Deng, Guo, Xue, Zafeiriou — ArcFace, arXiv:1801.07698

FaceNet to ArcFace: the metric-learning canon

Your phone recognises you without ever having been trained on you. That single sentence is impossible for a classifier and routine for an embedding — and the ten years between those two papers are the story of how the impossible became one line of loss code.

Prerequisites: the dot product and softmax cross-entropy. Distances, margins, mining, hyperspheres and ROC curves are all derived from zero.
10
Chapters
7
Interactive Sims
128-d
FaceNet Embedding
99.83%
ArcFace on LFW

Chapter 0: The Open Set

You take a new phone out of the box. It asks you to turn your head slowly in a circle. Twenty seconds later it locks, you look at it, and it opens.

Stop and notice what just happened, because it should be impossible. That model shipped months ago. It was trained on a cluster you will never see, on faces that are not yours, and then frozen and burned into silicon. It received no gradient step during those twenty seconds. It has never been told your name. And it recognises you — with a claimed false-match rate of about one in a million — on the strength of a few seconds of enrolment.

Nothing a classifier can do looks like this. A classifier's last layer is a matrix with one row per class; recognising a new person means growing a new row, which means labelled data, a gradient step, an optimiser state, a validation run and a redeploy. What your phone did instead was measure. It turned your face into a short list of numbers and asked whether that list sits close to the list it saved at enrolment.

The whole lesson in one sentence. If you can learn a function that maps an image to a vector such that distance in that vector space means identity, then recognition stops being classification and becomes arithmetic — and arithmetic generalises to people the model has never seen. Everything else in these ten chapters is about how to actually train that function, and how to know whether you succeeded.

Three tasks, one operation

"Face recognition" is three different products wearing one name, and confusing them is the most common way to ship a broken system. Pin them down first.

TaskThe questionWhat you compareWhat you tuneReal example
Verification (1:1)"Are these two the same person?"One probe against one claimed templateA threshold on similarityPhone unlock, passport gate
Identification (1:N)"Who is this?"One probe against a gallery of N templatesA ranking, plus optionally a threshold to rejectPhoto tagging, watchlist search
Clustering"How many people are in this pile, and which photos go together?"Every template against every otherA threshold and a linkage rule"People" albums in a photo app

Three products; one primitive. Every row of that table is implemented as the similarity between two embedding vectors. Verification thresholds one similarity. Identification sorts N of them. Clustering builds a graph out of all of them. Change nothing about the model and you get all three, which is why an embedding is the deliverable and a classifier is not.

The distinction that governs everything: closed set versus open set. A closed-set problem is one where the identities you will meet at test time are a subset of the identities you trained on. Digit recognition is closed-set: there will never be an eleventh digit. An open-set problem is one where the test identities are disjoint from the training identities — the model will spend its entire deployed life looking at people it has never seen, and must still say "same" or "different" correctly. Face recognition, speaker verification, place recognition and product retrieval are all open-set. This one word is why the softmax you already know is the wrong tool.

Why a classification head cannot do this — the mechanical argument

Walk the shapes and the wall becomes visible. A classifier takes an image, runs it through an encoder to a feature vector x of dimension d, and does exactly one more thing:

z = W x + b,   W ∈ RC×d,  b ∈ RC,  then p = softmax(z)

Row j of W is the only thing in the model that knows about identity j. To recognise a new person you need a new row. A new row is 512 new parameters that have never received a gradient, and a randomly initialised row produces a random logit — so you must train it, which means you must have labelled images of that person, on the device, at enrolment time, with an optimiser. And then you must retrain or at least recalibrate the other C rows, because softmax is a competition: adding a competitor changes everyone's probabilities.

Now put numbers on the absurdity. Suppose you ship to a billion phones. Each phone has, say, 4 enrolled faces. A classification approach would need a per-device head of 4 rows trained on device from 20 seconds of video — roughly 400 frames of one person, with no negatives except whatever the factory shipped. You are asking a randomly-initialised linear layer to learn a discriminative boundary from a single class. It cannot be done, and the failure is not a tuning problem.

FaceNet's introduction names the pre-2015 workaround exactly: earlier deep systems trained a classification layer over a set of known identities and then took an intermediate bottleneck layer as the representation. The authors call this out as indirect and inefficient — you hope that a layer trained for one purpose happens to be good for another — and note the resulting representations were usually thousands of dimensions wide.

The claim FaceNet makes, stated now so you can hold it to account. Do not train for classification and hope the bottleneck is useful. Train directly on the quantity you will deploy: the distance between two embeddings. FaceNet's embedding is 128 floats, it is trained so that squared Euclidean distance is directly a measure of face similarity, and on the LFW benchmark it reaches 99.63% accuracy — a roughly 30% cut in error against the best prior published systems, with an embedding an order of magnitude smaller.

Why 128 numbers is an engineering decision, not an aesthetic one

The size of the embedding is the size of your entire database, and the cost of every query. Work it out.

A 4096-dimensional bottleneck in float32 costs 4096 × 4 = 16,384 bytes per face. A 128-dimensional embedding in float32 costs 128 × 4 = 512 bytes. FaceNet also showed the embedding survives quantisation to one byte per dimension with only minor loss, giving 128 bytes per face.

RepresentationBytes per face1 million faces1 billion facesDot products per 1:N query over 1B
4096-d float32 bottleneck16,38416.4 GB16.4 TB4.1 × 1012 multiply-adds
128-d float32512512 MB512 GB1.28 × 1011 multiply-adds
128-d int8128128 MB128 GB1.28 × 1011 integer ops

The middle column is why photo apps can cluster your library on your laptop. The right-hand column is why a national-scale search is a rack, not a data centre. Hold on to the bottom-right cell: 128 GB is a desk drawer. Chapter 8 is about what follows from the fact that the entire face-print of a country fits on a hard drive you can carry, and this arithmetic is where that chapter starts.

What an embedding actually is — every shape, end to end

"Map the image to a vector" hides five stages, and each one is a decision. Here is the whole pipeline of a modern face embedder, with tensor shapes, so you could implement it from this table alone.

StageIn → outWhat it doesWhy it is there
1. DetectH×W×3 photo → a box plus 5 landmarksFind each face and locate the eye centres, nose tip and mouth cornersEverything downstream assumes one face filling the frame
2. Alignbox + landmarks → 112×112×3Fit a similarity transform (rotation, scale, translation) mapping the 5 landmarks onto a canonical template, then warpA convolutional network is translation-equivariant but not scale- or rotation-invariant. Every degree of freedom you leave in the input is one the network must learn to ignore, multiplying the data you need
3. Normalise pixels112×112×3 uint8 → float in [−1, 1](x − 127.5) / 128Standard conditioning; also makes the first layer's gradients well scaled
4. Backbone112×112×3 → 7×7×512A ResNet-100 or similar, stride 16 overallThe representation learning. This is 99% of the FLOPs
5. Output layer7×7×512 → 512ArcFace's chosen structure: BatchNorm → Dropout → fully connected → BatchNormThe paper studies several output structures and settles on this one. The final BN keeps the pre-normalisation feature scale stable across the batch
6. L2 normalise512 → 512, norm 1x / ‖x‖2This is the deployed artifact. Chapter 3 is entirely about why this line matters more than it looks
7. (training only) Head512 → C logitsMultiply by W ∈ RC×512, apply a margin, cross-entropyDeleted before deployment. It exists only to shape stage 6
Read row 7 twice. The largest single tensor in the training graph — 85,742 × 512 parameters, 176 MB in float32 before optimiser state — is thrown away at deployment. Everything you will learn about margins in Chapters 4 and 5 is about a matrix that never ships. It is a scaffold whose only job is to leave stage 6 in the right shape, which is the cleanest example in this lesson of the difference between the training objective and the deployed function.

One more consequence of alignment worth stating plainly: the embedding is only as good as its front end. If your detector fails on a profile view, the model never sees the face at all, and no loss function in this lesson can help. In the deployment failure post-mortems that people write, the split is usually about half detection and alignment, half embedding — and the half that gets all the research attention is the second one.

See all three tasks running off one embedding

Before any loss functions, play with the object we are trying to build. The simulation below shows a tiny two-dimensional embedding space — real ones are 128- or 512-dimensional, but the geometry of the argument is identical. Four identities have three photos each. Move the threshold and watch the same numbers become a verification decision, a ranked identification, or a clustering.

One space, three products

Switch tasks. Drag the threshold. Then press "enrol a stranger" — a person whose photos the model has never seen — and notice that all three tasks keep working with no retraining, because none of them ever consulted a class list.

Task:
Threshold 0.70

Three things worth noticing while you play. First, the threshold is the whole policy: one scalar decides how paranoid the system is, and Chapter 6 is about how to choose it honestly. Second, the stranger costs one forward pass and zero gradient steps — that is the open-set property, made concrete. Third, clustering never needed a threshold per person; the same number works for everyone, which only holds if the embedding space is calibrated globally. Chapter 3 is about why normalisation is what makes that true.

What "distance means identity" actually demands

It is easy to nod along to "similar faces should be close". The requirement is stronger and stranger than it sounds, and writing it precisely tells you what the loss function has to do.

Requirement 1 — intra-class compactness
Every photo of one person must land near every other photo of that person, across lighting, age, pose, glasses, beard, resolution.
↓ and simultaneously …
Requirement 2 — inter-class discrepancy
Every photo of one person must land far from every photo of every other person — including siblings, including people the model has never seen.
↓ and, the one everybody forgets …
Requirement 3 — a global scale
The same numeric threshold must separate "same" from "different" everywhere in the space, for identities never trained on. Local separability is not enough.

Requirement 3 is the one that makes this hard. A classifier only ever has to make each training identity separable from the others — the boundary can sit anywhere between them, and the amount of room left over is unconstrained. An open-set embedding has to leave a gap wide enough that a stranger, whose position nobody optimised, still falls outside it. Margins, the subject of Chapters 4 and 5, exist entirely to buy that gap.

Inline concept check — answer before reading on. Suppose your model achieves 100% accuracy on a 10,000-way closed-set face classification task. Does that guarantee good verification on unseen identities?  …  No, and the gap is not subtle. Perfect closed-set accuracy only requires that each training identity's features fall on the correct side of 9,999 boundaries. Two identities can be perfectly separable while sitting at an angle of two degrees from each other, and an unseen third person can land between them. Verification asks for a margin, and closed-set accuracy never measured one.

Requirement 3, with numbers — why the threshold must be global

The third requirement sounds bureaucratic until you try to violate it. Suppose you build a system where each enrolled user gets their own threshold, tuned at enrolment: a tight one for people whose enrolment photos agreed closely, a loose one for people whose did not. Reasonable? Work it through.

Three users enrol. Their per-user thresholds come out as:

UserTheir thresholdImpostor score against themDecision
Ada0.720.68Reject — correct
Bo0.550.68Accept — the same impostor, the same face, the same score
Cyd0.800.68Reject — correct

One attacker, one photograph, one similarity value — and the system's answer depends on which account they aim at. The system-wide false match rate is now the average of wildly different per-user rates, and an attacker only has to find the loosest account, so the effective security is the worst one. Per-user calibration hands the attacker a search problem they can solve by trying everybody.

The same argument, one level up, is Chapter 8. If per-user thresholds are indefensible, so are per-group thresholds — and for the same reason: the security of the system is set by its loosest operating point, and an attacker chooses which one to attack. That is why the honest way to report demographic differentials is per-group error rates at a shared threshold, never per-group thresholds that make the numbers match. Chapter 6 builds the measurement; Chapter 8 shows what it exposes.

So the loss function has to produce a space where one number works everywhere. Nothing in a classification objective asks for that. Everything in the losses of Chapters 1 through 5 does.

The three papers, and what each one is actually for

This lesson is built on three papers that are usually taught as competitors. They are better read as three moves in one argument.

PaperYearThe moveThe cost it introduced
FaceNet (Schroff, Kalenichenko, Philbin)2015Train the distance directly with a triplet loss on 128-d L2-normalised embeddings. Recognition becomes a nearest-neighbour query.You now need to choose triplets, and almost all of them are useless. Chapter 2 is entirely about this bill.
CosFace / LMCL (Wang, Wang, Zhou, Ji, Li, Gong, Zhou, Liu)2018Put the margin back inside a softmax, on the cosine: normalise features and weights, then subtract a constant m from the target class's cosine.Two new hyperparameters, s and m, that can silently prevent the model from ever learning. Chapter 5.
ArcFace (Deng, Guo, Xue, Zafeiriou)2018/2019Put the margin on the angle instead of the cosine, so the penalty is a constant arc length on the hypersphere — a genuine geodesic margin.A non-monotone target function that needs a guard clause, and an initialisation that can blow up. Chapter 5 again.

Between CosFace and ArcFace sits a fourth idea that everyone borrows and nobody cites carefully: proxy-based metric learning (ProxyNCA, 2017). It is the hinge that makes the jump from triplets to margin-softmax feel inevitable rather than arbitrary, and it gets its own section in Chapter 3.

How the field got here

EraWhat the loss optimisedWhat it unlockedWhat stayed broken
Hand-crafted features (to ~2013)Nothing — features were designed, then a classifier or a metric was fit on topEigenfaces, Fisherfaces, LBP; the first working gatesEverything hard: pose, lighting, age
Deep classification (2014)Softmax over a few thousand identities; the representation is a bottleneck layer you keep by handDeepFace crosses 97% on LFW; deep learning arrives in facesThe representation was a by-product, not the objective; thousands of dimensions
Joint identification + verification (2014)Softmax plus a contrastive term on pairs (DeepID2)Explicit pressure on the metric for the first timeTwo losses to balance; pair sampling is fragile
Direct metric learning (2015)Triplet ranking on the deployed distance (FaceNet)128-d embeddings; verification, identification and clustering from one modelTriplet selection dominates the engineering; huge batches; unstable
Margins inside softmax (2016–2018)Softmax with an angular or cosine margin (L-Softmax, SphereFace, NormFace, CosFace, ArcFace)Stable training, no sampler, and better embeddings than tripletsTwo knobs with cliff edges; and a class table that grows with your dataset
Scale and quality (2019–)The same margin, plus sampling of class centres and quality-adaptive margins (Partial FC, MagFace, AdaFace)Hundreds of millions of identities; graceful handling of blurry inputsThe measurement and governance problems of Chapters 6 and 8, which no loss fixes

Read the last column downward. Each era paid off the previous era's debt and issued new debt of its own. That is the honest shape of this literature, and it is why the last two chapters of this lesson are about evaluation and ethics rather than about a better loss.

Where we are going

Chapters 1–2 — the triplet era
Derive the triplet loss from the collapse it has to avoid, then discover that the loss is the easy part and choosing which triplets to feed it is the whole job
Chapters 3–5 — the margin era
Replace sampled negatives with learned proxies, normalise everything onto a hypersphere, then derive CosFace's and ArcFace's margins and hand-compute the same sample through both
Chapters 6–9 — the honest half
Compute a ROC by hand and learn why LFW accuracy is nearly meaningless → watch the same recipe run speech, places and products → then the dual-use chapter, then the canon
Why can a softmax classification head not, even in principle, enrol a new person at deployment time?
A model gets 100% accuracy classifying its 10,000 training identities. What does that tell you about its verification performance on strangers?

Chapter 1: The Triplet, Derived

We want a function f that maps an image to a vector, such that distance means identity. Let us try to write the loss for that requirement and watch it fail twice before it works. The two failures are not a detour — each one explains a term in the final formula, and if you skip them the margin looks like a magic number.

Attempt 1: just pull the same person together

The obvious first loss. Take two photos of the same person, an anchor a and a positive p, and minimise the distance between their embeddings:

L1 = ‖ f(a) − f(p) ‖2

Now find the minimiser. Set f(x) = c for every image x and any constant vector c. Then every distance is zero, L1 = 0 everywhere, and the loss is globally minimised. The network has learned to output the same vector for every photograph in the world. This is called collapse, and it is not a rare failure mode you tune away — it is the exact optimum of the objective you wrote.

Read the collapse as information, not as a bug. Any loss made only of attraction terms is minimised by a constant function. Every working metric-learning loss in this lesson — triplet, contrastive, CosFace, ArcFace, InfoNCE in CLIP and CLAP — therefore contains a repulsion term, and the design of that repulsion term is the design of the method.

Attempt 2: pull together and push apart

Add a third image: a negative n, a photo of a different person. Now ask for the positive to be closer than the negative:

L2 = ‖ f(a) − f(p) ‖2 − ‖ f(a) − f(n) ‖2

Collapse is dead: a constant function makes both terms zero and L2 = 0, which is no longer the minimum, because the loss can go negative without bound by pushing negatives to infinity. Which is exactly the new problem. L2 keeps rewarding you forever. A triplet that is already resolved — the positive is right on top of the anchor and the negative is on the other side of the space — still produces a large negative loss and a nonzero gradient. So the optimiser keeps spending capacity on triplets that were finished long ago, and the pairs that are actually confusable get drowned out.

There is also a scale problem. If the network can make all its outputs bigger, it can improve L2 without improving any decision, simply by multiplying the whole embedding by 10 — every distance grows, and the difference of squared distances grows fastest for the already-easy triplets. The loss is not invariant to something we do not care about.

Attempt 3: a hinge with a margin, on a sphere

Both problems have the same fix in two parts. First, constrain the embedding to the unit hypersphere:

‖ f(x) ‖2 = 1  for every x

This is one line of code (divide by the norm) and it buys three things at once. Scale cheating becomes impossible. Squared distance becomes bounded: expand the square and use the unit norms,

‖ u − v ‖2 = ‖u‖2 + ‖v‖2 − 2 u·v = 2 − 2 cos θ

so every squared distance lives in [0, 4] and is a strictly decreasing function of the cosine. And third, a single numeric margin now means the same thing everywhere in the space, which is Requirement 3 from Chapter 0.

Second, replace the unbounded difference with a hinge: demand that the negative be farther than the positive by at least a margin, and stop caring the instant that is true.

L(a, p, n) = max { 0,  ‖f(a)−f(p)‖2 − ‖f(a)−f(n)‖2 + α }

That is the triplet loss, and it is FaceNet's entire objective. In the paper's own notation the constraint being enforced is

‖f(xa) − f(xp)‖2 + α  <  ‖f(xa) − f(xn)‖2   for all triplets in the training set

with α = 0.2 in all of FaceNet's experiments. The hinge is what makes the loss satisfiable: once a triplet obeys the constraint, its loss is exactly zero and its gradient is exactly zero, and the optimiser moves on. That property is the entire reason the method works, and — as Chapter 2 shows — it is also the reason the method is so painful to train.

What does α = 0.2 mean in human units? The margin lives in squared-distance units, so translate it. Using d2 = 2 − 2cos θ, the constraint dan2 − dap2 ≥ 0.2 becomes 2cos θap − 2cos θan ≥ 0.2, that is cos θap − cos θan ≥ 0.1. FaceNet's famous 0.2 is a cosine margin of 0.1. Keep that number in your pocket: in Chapter 4 CosFace will ask for a cosine margin of 0.35, three and a half times larger, and it will be able to ask for that much only because it stopped sampling negatives.

One more reading. If the positive is perfect — θap = 0, so dap2 = 0 — then the constraint says dan2 ≥ 0.2, so cos θan ≤ 0.9, so θan ≥ 25.84°. FaceNet is asking for about twenty-six degrees of angular clearance around each identity in the best case. Chapter 4's ArcFace will ask for 28.65° from every sample unconditionally. The two methods are closer than their formulas look.

Why three images and not two?

There is an older, simpler loss that also avoids collapse, and understanding why FaceNet did not use it is understanding what "metric" means here. The contrastive loss (Chopra, Hadsell and LeCun, 2005–2006) takes a pair and a binary label y (1 if same person):

Lpair = y · d2 + (1 − y) · max(0, m − d)2

Same-person pairs are pulled to distance zero; different-person pairs are pushed to at least m. It works, and it has one structural flaw: both targets are absolute. Every identity in the world is required to collapse to a single point, and every other identity is required to sit at least m away from it, with m a global constant.

Consider two identities in a real dataset. Identity A appears in 4 studio portraits: same lighting, same pose, same year. Identity B appears in 300 photographs spanning fifteen years, a beard, three continents and two decades of camera technology. The contrastive loss demands both collapse to a point. For B that is either impossible or, if the network manages it, achieved by discarding the very information that makes the embedding useful — it must learn to ignore everything that changed, which is almost everything.

The triplet loss asks for something strictly weaker and much more sensible: relative ordering with a margin. Identity B is allowed to occupy a large region of the sphere, as long as every other identity stays further from each of B's photos than B's other photos are. Formally the constraint is per-anchor, not per-identity, and never mentions an absolute distance.

Put numbers on it. Suppose after training, identity A's photos are all within 0.05 squared distance of each other and identity B's are spread over 0.30.

SituationContrastive loss (m2 = 0.5)Triplet loss (α = 0.2)
A's pair at d2 = 0.05Loss 0.05 — still pulling, foreverZero, provided negatives are past 0.25
B's pair at d2 = 0.30Loss 0.30 — six times A's, so B dominates the gradient purely for being variedZero, provided B's negatives are past 0.50
A negative at d2 = 0.40Violates the absolute margin, gets pushedSatisfied — it is 0.35 past A's positives
B negative at d2 = 0.40Also violates — same treatmentViolates — only 0.10 past B's positives, so it is pushed. Correctly

Read the last two rows: the triplet loss gives the two identities different effective margins, derived from their own data, from one global constant α. FaceNet's paper puts it as enforcing a margin "between each pair of faces from one person to all other faces", and notes this allows the faces of one identity to live on a manifold rather than a point.

The price of that flexibility is paid in Chapter 2. Making the constraint relative means it involves three things instead of two, and the number of constraints goes from O(N2) pairs to O(N3) triplets. The entire mining problem is the bill for this one design decision, and margin-softmax will eventually find a way to keep the relative constraint while paying only O(N·C).

A worked triplet, by hand

Two dimensions, so you can check every number with a pencil. Take unit vectors (each has norm exactly 1 — verify: 0.62 + 0.82 = 0.36 + 0.64 = 1):

a = (0.60, 0.80)    p = (0.80, 0.60)    n1 = (0.96, 0.28)    n2 = (0.28, 0.96)    n3 = (0.50, 0.866)

Anchor-positive distance first, straight from the definition:

dap2 = (0.60 − 0.80)2 + (0.80 − 0.60)2 = 0.04 + 0.04 = 0.08

Cross-check with the cosine identity: a·p = 0.60(0.80) + 0.80(0.60) = 0.48 + 0.48 = 0.96, so d2 = 2 − 2(0.96) = 0.08. The two routes agree, as they must. The angle is arccos(0.96) = 16.26°.

Now the three negatives, each with α = 0.2:

Negativea·ndan2 = 2 − 2 cosθanLoss = max(0, 0.08 − dan2 + 0.2)Verdict
n1 = (0.96, 0.28)0.576 + 0.224 = 0.8000.40036.87°max(0, −0.120) = 0Easy — already satisfied, zero gradient
n2 = (0.28, 0.96)0.168 + 0.768 = 0.9360.12820.61°max(0, 0.152) = 0.152Semi-hard — farther than p, but inside the margin
n3 = (0.50, 0.866)0.300 + 0.693 = 0.9930.0146.87°max(0, 0.266) = 0.266Hard — the negative is closer than the positive

Three negatives, three completely different training signals from the same anchor and positive. n1 contributes a forward pass, a backward pass and exactly nothing else. That single fact is the whole of Chapter 2.

The gradient, and one step by hand

When the hinge is open (loss > 0), the max disappears and the loss is a plain quadratic. Differentiate each of the three terms:

∂L/∂a = 2(a−p) − 2(a−n) = 2(n − p)     ∂L/∂p = 2(p − a)     ∂L/∂n = 2(a − n)

Read them physically. Descending on p means stepping along −2(p−a), which is straight toward the anchor. Descending on n means stepping along −2(a−n) = 2(n−a), straight away from the anchor. And the anchor itself moves along 2(p−n): toward the positive and away from the negative in one motion. Attraction and repulsion fall out of the algebra; nobody had to write them separately.

Take the semi-hard case (a, p, n2) with learning rate η = 0.05 and step all three:

VectorGradientUpdate x − η∇Renormalised to the sphereAngle
a = (0.600, 0.800)2(n−p) = (−1.040, 0.720)(0.652, 0.764)/1.00439 = (0.6492, 0.7607)49.51°
p = (0.800, 0.600)2(p−a) = (0.400, −0.400)(0.780, 0.620)/0.99639 = (0.7828, 0.6222)38.49°
n2 = (0.280, 0.960)2(a−n) = (0.640, −0.320)(0.248, 0.976)/1.00702 = (0.2463, 0.9692)75.75°

Recompute the loss with the new geometry. The anchor-positive angle shrank from 16.26° to 49.51 − 38.49 = 11.02°, and the anchor-negative angle grew from 20.61° to 75.75 − 49.51 = 26.24°:

dap2 = 2 − 2cos(11.02°) = 2 − 1.96312 = 0.03688
dan2 = 2 − 2cos(26.24°) = 2 − 1.79385 = 0.20615
L = max(0, 0.03688 − 0.20615 + 0.2) = 0.0307  (was 0.152)

One step, one triplet, and the loss fell by 80%. Two more steps of this size and the hinge closes; from then on this triplet is free to compute and worth nothing. Nothing in the optimiser knows that in advance.

Why squared distances rather than distances? Three reasons, all practical. The square avoids a square root, whose derivative is unbounded as the distance goes to zero — precisely where positive pairs live, so the un-squared version has an exploding gradient exactly at its own optimum. The square keeps the loss a smooth quadratic in the embeddings, so the gradients above are linear and cheap. And on the unit sphere the squared distance is an exact affine function of the cosine, d2 = 2 − 2cos θ, which is what lets us translate FaceNet's margin into CosFace's units in one line.

Play with the hinge

Triplet margin explorer

The anchor is fixed. Move the positive and negative around the unit circle and change the margin. The bar chart is the constraint being tested: the loss is whatever the negative's bar falls short of the dashed line at dap2 + α. Arrows show the gradient on each point — watch them vanish the instant the constraint is met.

Positive at 16°
Negative at 21°
Margin α 0.20

Set the margin to zero and watch what happens: the loss becomes active only when the negative is strictly closer than the positive, so the model stops as soon as the ordering is right, with the two classes touching. That is the closed-set optimum from Chapter 0, and it is exactly the configuration that fails on strangers. The margin is the price of admission to open-set.

The realisation — what this is in code

The loss itself is four lines, and the shapes matter more than the arithmetic.

python — triplet loss, explicitimport torch, torch.nn.functional as F

# emb: (B, 128) raw encoder output — NOT yet on the sphere
emb = F.normalize(encoder(images), p=2, dim=1)   # (B, 128), every row has norm 1

a, p, n = emb[a_idx], emb[p_idx], emb[n_idx]         # (T, 128) each, T triplets

d_ap = (a - p).pow(2).sum(1)                       # (T,)  squared, no sqrt
d_an = (a - n).pow(2).sum(1)                       # (T,)
loss = F.relu(d_ap - d_an + 0.2).mean()             # the hinge, alpha = 0.2

# Diagnostic you must log from day one:
active = (d_ap - d_an + 0.2 > 0).float().mean()   # fraction of triplets doing any work

That last line is the most important diagnostic in triplet training, and it is the one people forget. If active is 0.01, then 99% of your batch produced a gradient of exactly zero and you paid full price for it. If active is 1.0 after a few epochs, something is wrong in the other direction — the model is not learning at all, or the mining is picking pathological negatives.

The subtlety in .mean(). Averaging over all triplets, including the inactive ones, means the reported loss shrinks as the model improves for two different reasons that you cannot separate: the active triplets got better, or fewer triplets are active. Many implementations divide by the number of active triplets instead. Both are defensible; mixing them up between runs makes your loss curves incomparable, and that has cost more debugging hours than any bug in the formula.
Why does the triplet loss use a hinge (a max with zero) rather than simply minimising dap2 − dan2?
On unit-norm embeddings, FaceNet's α = 0.2 in squared-distance units corresponds to what constraint on cosines?

Chapter 2: The Mining Problem

Chapter 1 gave you a loss that is four lines long and provably does the right thing. This chapter is about the fact that the loss is the easy part, and that FaceNet's real contribution — the part that took a team at Google and a cluster to get right — is deciding which triplets to compute it on.

First, the size of the problem

FaceNet trained on 100 to 200 million face thumbnails covering about 8 million identities. Take the middle: N = 200M images, C = 8M identities, so roughly 200M/8M = 25 images per identity. Count the triplets.

For a given identity with k images, the number of ordered anchor-positive pairs is k(k−1). Each of those can pair with any image of any other identity, which is about N − k ≈ N negatives. So:

#triplets ≈ C · k(k−1) · N = 8×106 · (25)(24) · 2×108 = 8×106 · 600 · 2×1089.6 × 1017

Nearly 1018 triplets. At FaceNet's batch size of roughly 1,800 exemplars, and even if every batch somehow produced a million usable triplets, you would need 1012 batches to see them all once. The universe is about 4×1017 seconds old. Enumeration is not a strategy, and no amount of engineering makes it one.

This is the structural cost of the triplet loss and you should feel it as a cost. Cross-entropy touches every training image exactly once per epoch and its bookkeeping is O(N). Triplet loss defines its objective over an O(N3)-sized set. Everything painful about the 2015–2017 era — enormous batches, custom samplers, unreproducible results, models that train beautifully for two days and then collapse — descends from this one line of arithmetic. Chapters 3 to 5 are the field's answer to it.

Second, the fatal fact: almost every triplet is useless

Sample a triplet uniformly at random. What is the chance it is active — that its hinge is open and its gradient is nonzero?

Put a simple model on it. Suppose that after a little training the anchor-positive squared distances concentrate around dap2 ≈ 0.25 (about 29°) and random anchor-negative squared distances concentrate around dan2 ≈ 1.6 (about 76°), with a spread of about 0.25 either way. The triplet is active when

dan2 < dap2 + α = 0.25 + 0.20 = 0.45

We need a random negative to land at squared distance below 0.45 when it is typically at 1.6, which is about 4.6 standard deviations away. For a roughly Gaussian tail that is on the order of 2 in a million. Even being extremely generous and calling it one in a thousand, a batch of 1,800 exemplars sampled at random gives you a couple of useful triplets and 1,798 forward passes of pure heat.

Sampling strategyTriplets that produce gradientCompute wastedWhat actually happens to the model
Uniform random, early trainingHigh — the model is bad, so everything violatesLittleLearns fast, then stalls as the easy structure is exhausted
Uniform random, after a few epochs<1%, falling toward zero>99%Loss curve flattens near zero; accuracy stops improving; the model has erased its own training signal
Hardest negative in the dataset100%None — but see the next sectionCollapse, usually within a few hundred steps
Semi-hard within the batchModerate and self-sustainingSomeTrains stably to convergence — this is FaceNet's recipe
The self-defeating loop, stated plainly. The triplet loss is defined only on violating triplets. As the model improves, violations become rarer. So the better the model gets, the less signal random sampling delivers, and training asymptotes not because the model is finished but because it can no longer find anything to learn from. A loss that destroys its own gradient supply needs an active search procedure bolted on. That procedure is called mining, and it is not an optimisation — it is a load-bearing part of the method.

Third: why the obvious fix destroys the model

If you want active triplets, take the most active ones: for each anchor-positive pair, search the dataset for the negative that is closest to the anchor. Every triplet now violates, every gradient is large, no compute is wasted. FaceNet tried it. The paper reports that selecting the hardest negatives "can in practice lead to bad local minima early on in training, specifically it can result in a collapsed model (i.e. f(x) = 0)."

The word "collapse" is worth deriving rather than accepting, because the derivation is three lines and it makes the failure permanent knowledge.

Suppose the network outputs the same vector for every image: f(x) = c for all x. Then for every triplet a = p = n = c, and from Chapter 1:

∂L/∂a = 2(n − p) = 0    ∂L/∂p = 2(p − a) = 0    ∂L/∂n = 2(a − n) = 0

Every gradient is exactly zero, and the loss value is exactly α for every triplet. So total collapse is an exact stationary point of the triplet loss, sitting at loss α = 0.2, which looks like a plateau rather than a catastrophe on a loss curve. That is why it is a trap: the loss does not spike, it flatlines at a plausible-looking number, and if you are only watching the loss you will let it run overnight.

Now the mechanism that walks you into the trap. Real face datasets contain label noise: mislabelled images, two different people under one identity, occlusions, near-duplicates across identities. The globally hardest negative for an anchor is very often one of these — a photo labelled as a different person that is in fact the same person, or a photo so degraded that its embedding is meaningless. Hard mining seeks out precisely the examples where the label is most likely to be wrong. The gradient then commands the model to separate two images that genuinely should be together. Repeated a few thousand times with maximum gradient magnitude, the cheapest way to satisfy a pile of contradictory constraints is to satisfy none of them and go flat.

The general principle, worth more than the face-recognition instance. Any procedure that selects training examples by "the model is most wrong here" is also a procedure that selects "the label is most likely wrong here". Hard-example mining, active learning, focal loss and hard-negative mining in dense retrieval all share this exposure. The standard defence is the same in all four: bound the hardness rather than maximise it.

Semi-hard mining, derived

We want the hardest triplets that are still trustworthy. Draw the number line for dan2 with dap2 and dap2 + α marked on it, and there are exactly three regions:

Region 1 — hard negatives: dan2 < dap2
The negative is closer than the positive. The ordering is wrong. Loss is large, gradient is large — and this region is where mislabels live. Avoid.
Region 2 — semi-hard: dap2 < dan2 < dap2 + α
The ordering is already right but the gap is too small. Loss is positive but bounded above by α. The model is nearly correct and needs a nudge. This is the goldilocks band.
Region 3 — easy: dan2 > dap2 + α
Constraint satisfied. Loss zero, gradient zero. Free to compute and worth nothing.

FaceNet's rule is: within the current minibatch, for each anchor-positive pair, pick a negative satisfying

‖f(xa) − f(xp)‖2  <  ‖f(xa) − f(xn)‖2    (and, being in the batch's active set, still inside the margin)

The bound on loss is the point. In Region 2 the loss is at most α = 0.2, so no single triplet can dominate a batch, so a mislabelled pair cannot swing the update. You get the largest gradient that is still bounded, which is the practical definition of a safe hard-example strategy.

Batch construction: the 1,800-exemplar story

Semi-hard selection requires knowing the distances, which requires embeddings, which requires forward passes. Doing that over the whole dataset every step is impossible. FaceNet's answer — the design that everyone copied — is online mining inside a large, deliberately structured minibatch.

The paper's setup: minibatches of around 1,800 exemplars, constructed so that roughly 40 faces are sampled per identity, with randomly sampled negative faces filling the rest. Do the arithmetic on what one such batch buys.

QuantityArithmeticValue
Identities per batch1800 ÷ 4045
Forward passes neededone per exemplar1,800
Anchor-positive pairs available45 × 40 × 3970,200
Negative candidates per anchor1800 − 401,760
Candidate triplets inside one batch70,200 × 1,760123,552,000
Pairwise distance matrix to compute1800 × 1800 × 128 multiply-adds4.1 × 108 — one small matmul
This table is why the batch is 1,800 and not 180. 1,800 forward passes yield 123 million candidate triplets, and finding the semi-hard ones among them is a single 1800×1800 matrix multiply — utterly negligible next to the convolutions. The batch size stopped being a memory setting and became a modelling hyperparameter: it is the size of the pool you are allowed to mine. The identical logic reappears as "large batches matter for contrastive learning" in CLIP, CLAP and SimCLR, and it is the same reason.

Two more details from the paper that are easy to skim past and expensive to rediscover:

Use all anchor-positive pairs, not the hardest one. Symmetry suggests mining hard positives too — for each anchor, the farthest same-identity image. FaceNet reports that using all anchor-positive pairs in the minibatch was more stable and converged slightly faster at the start of training. Hardest-positive mining has the same label-noise exposure as hardest-negative mining, pointed the other way: the farthest "positive" is disproportionately likely to be someone else's photo filed under this identity.

Offline versus online. You can also mine offline: every n steps, embed a subset of the data with the most recent checkpoint and compute the argmin/argmax. FaceNet describes both and uses the online version, because offline mining goes stale — the checkpoint that computed the distances is not the model taking the step, and the staler it gets, the more the "hard" negatives are hard for a model that no longer exists.

The deeper reason random sampling fails: distances concentrate

The estimate above ("about one in a thousand") came from a plausible model. There is a sharper, geometric reason that is worth knowing because it is a fact about high-dimensional space rather than about faces.

Take two independent, uniformly random unit vectors in d dimensions. Their inner product has mean 0 and standard deviation 1/√d. So the squared distance d2 = 2 − 2cos θ has mean 2 and standard deviation 2/√d. Put in FaceNet's d = 128:

E[d2] = 2,   SD[d2] = 2/√128 = 2/11.31 = 0.177

Almost every random pair sits at squared distance 2 ± 0.35. In 512 dimensions the spread shrinks further, to 2/22.6 = 0.088. This is concentration of measure: in high dimensions, random points are all about the same distance apart, and the useful structure lives in a vanishingly thin sliver of the distribution.

Now the consequence for mining. A useful negative must land below dap2 + α ≈ 0.45. Against a distribution centred at 2.0 with standard deviation 0.177, that is (2.0 − 0.45)/0.177 = 8.8 standard deviations below the mean. Even accounting for the fact that real embeddings are far from uniform — identities cluster, and similar-looking people are genuinely close — you are fishing in the extreme tail. Uniform sampling is the wrong instrument for finding tail events; that is what importance sampling is for, and mining is importance sampling by another name.

The paper that made this precise. Wu, Manmatha, Smola and Krähenbühl's "Sampling Matters in Deep Embedding Learning" (2017) derives the density of pairwise distances on the sphere — it concentrates sharply at √2 — and shows that hardest-negative mining also has a variance problem, not only a noise problem: the gradient of the hinge with respect to the embedding has a magnitude that becomes numerically unstable as the distance shrinks. Their fix, distance-weighted sampling, draws negatives with probability inversely proportional to the analytic distance density, which spreads the selection across the useful range instead of piling onto the extreme. It beats both random and hardest, and it is the most principled member of this family.

Batch-hard: the variant that actually shipped everywhere else

Two years later, Hermans, Beyer and Leibe's "In Defense of the Triplet Loss for Person Re-Identification" simplified the recipe into the form most code uses today. Build a batch of P identities × K images each (typical: P = 18, K = 4, so B = 72). For each image in the batch as anchor, take the hardest positive and the hardest negative within the batch only:

LBH = ∑a max{0,  maxp∈A(a) d(a,p) − minn∉A(a) d(a,n) + α}

The trick is that "hardest in a batch of 72" is a moderate difficulty, not a pathological one — a bounded form of hard mining, where the bound comes from the batch being small. Same principle as semi-hard, different implementation. They also propose the soft-margin variant, replacing max(0, x) with ln(1 + ex), which removes the hyperparameter α entirely and never has exactly zero gradient.

Mining strategy comparator

A batch of 8 identities × 6 images on the circle. Slide "training progress" to tighten the clusters the way real training does, then compare strategies. Watch the active fraction collapse for random sampling as the model improves, and watch the hardest-negative strategy lock onto the deliberately mislabelled point (marked with a cross) as soon as it can.

Training progress 55%
Strategy:

The realisation — mining in code

All of it is one distance matrix and two masks. Read the shapes.

python — online semi-hard mining inside a batchemb = F.normalize(encoder(x), dim=1)          # (B, 128), B = 1800
D   = torch.cdist(emb, emb).pow(2)               # (B, B) squared distances — one matmul

same = labels[:, None] == labels[None, :]        # (B, B) bool: same identity?
pos  = same & ~torch.eye(B, dtype=torch.bool)   # exclude self-pairs
neg  = ~same

# For every (anchor, positive) pair we need negatives in the band
#     d_ap  <  d_an  <  d_ap + alpha
d_ap   = D.unsqueeze(2)                          # (B, B, 1) — anchor a, positive p
d_an   = D.unsqueeze(1)                          # (B, 1, B) — anchor a, negative n
band   = (d_an > d_ap) & (d_an < d_ap + 0.2)      # (B, B, B) semi-hard mask
valid  = pos.unsqueeze(2) & neg.unsqueeze(1) & band

losses = F.relu(d_ap - d_an + 0.2)[valid]        # only the goldilocks band
loss   = losses.mean() if losses.numel() else D.sum() * 0.0

# Fallback matters: if no semi-hard triplet exists in the batch, the
# standard recipe falls back to the hardest AVAILABLE negative rather
# than emitting zero — otherwise late training silently stops.

Note the memory line nobody mentions: the mask band is B3 booleans. At B = 1800 that is 5.8 × 109 entries — 5.8 GB — so production implementations never materialise it. They loop over anchors, or they select one negative per anchor-positive pair with a masked argmin, keeping everything at O(B2). Writing the naive version once, seeing it die, and then fixing it is a good afternoon.

Concept + realisation checkpoint. You now know why triplet training in 2016 required a research team: a loss over 1018 terms, a sampler that must be re-derived every step from a 1,800-way distance matrix, a collapse mode that looks like a plateau, and a hyperparameter (α) that is entangled with the sampler's definition of "semi-hard". Compare that with what you are about to build in Chapter 3: a softmax with two extra lines, no sampler, no batch structure, and a loss that cannot collapse. That is the trade the field made, and the next chapter is the derivation of why it was available all along.
FaceNet reports that mining the globally hardest negatives leads to a collapsed model. What is the mechanism?
Why does semi-hard mining bound the loss of any selected triplet above by α?

Chapter 3: Onto the Sphere

Here is the move that dissolves Chapter 2. It arrives in three steps, and each one is a single line of code with a real derivation behind it.

Step 1: replace sampled negatives with learned proxies

The expensive thing about a triplet is that the negative is a data point, so finding a good one means searching the data. Suppose instead each identity owns one learnable vector — call it a proxy — that stands in for all of that identity's images. Then a triplet becomes (x, proxy of my class, proxy of some other class), and the search space shrinks from "every image in the dataset" to "the C proxies", which you can simply enumerate.

This is ProxyNCA (Movshovitz-Attias et al., 2017). Its loss is neighbourhood component analysis with proxies substituted for data points:

LProxyNCA = − log  exp(−d(x, πy)) / ∑j ≠ y exp(−d(x, πj))

where πj is the proxy for class j and both x and the proxies are normalised. The paper reports roughly three times faster convergence than sampling-based metric learning while improving Recall@1 on the standard retrieval benchmarks. The reason is exactly the counting argument from Chapter 2: every sample now compares against all classes on every step, so there is no sampler, no staleness, and no possibility of spending a batch on triplets that were resolved last week.

The observation that makes the rest of this lesson obvious. Look at that formula again and squint. A negative-log of an exponential over a sum of exponentials is a softmax cross-entropy. And the proxies — one learned vector per class, compared against the feature by a similarity — are exactly the rows of a classifier's weight matrix W. A softmax classifier was always a proxy-based metric learner. Row j of W is the proxy for class j; the logit Wj·x is the similarity; cross-entropy is the ranking loss. Nobody had to invent proxies for face recognition. They had been sitting in the last layer since 2014, mis-labelled as "the classification head".

That reframing is what turns Chapters 4 and 5 from a bag of tricks into a single idea: if the classifier head is a metric learner, then fix the metric it is learning. Two things are wrong with it out of the box.

The counting argument is worth doing, because it is the whole reason the field moved.

LossComparisons per sample, per stepWho supplies the negativesBookkeeping
Contrastive (pairs)1A samplerPair lists, class-balanced sampling
Triplet1 (or a few)A sampler, plus mining inside a big batchIdentity-structured batches, B×B distance matrix, masks
N-pair (Sohn, 2016)N − 1, the rest of the batchThe batch itselfOne example per class per batch
Proxy-based (ProxyNCA)C − 1, every classLearned proxies — no sampler at allNone. Shuffle like any classifier

Read the third column downward. Each row buys negatives more cheaply than the last, and the last row buys all of them for the price of one matrix multiply. That is why the sampler disappears: not because someone found a better sampling strategy, but because the thing being sampled stopped being necessary.

What the proxies become, and how they get there

A proxy is initialised randomly, so at first it means nothing. Watch what the gradient does to it. For softmax cross-entropy with logits zj = Wj · x, the gradient on class j's proxy from a single sample x with true class y is

∂L/∂Wj = (pj − {1 if j = y else 0}) · x

Read the two cases. For the correct class, py − 1 is negative, so descending moves Wy along +x: the proxy is pulled toward the sample. For every wrong class, pj is positive, so Wj is pushed along −x, away from the sample — and the push is proportional to how much that class currently believes it owns this sample. Sum over an epoch and Wy becomes a weighted average of its own class's features, weighted toward the ones the model finds hardest. The proxy converges to something like the class mean, and it was never told to.

Two useful consequences follow immediately. First, a class with few images gets few pulls, so its proxy stays close to its random initialisation and its logit stays weak — the long-tail problem, visible in the weight norms. Second, because the proxy is a running summary of the class rather than a specific image, the loss is far less sensitive to any single mislabelled photo than hardest-negative mining was: one bad image contributes one term to an average instead of selecting itself as the entire gradient. That robustness, more than the speed, is why margin-softmax replaced triplets.

The bill for this convenience is memory, and it comes due at scale. The proxy table is C×d. At ArcFace's 85,742 identities and d = 512 that is 43.9M parameters — 176 MB in float32, plus a gradient buffer and two Adam moments, so roughly 700 MB before you have stored a single activation. At 10 million identities it is 5.1 billion parameters and about 20 GB for the weights alone: larger than the network. This is exactly the wall Partial FC (2020) breaks by computing the softmax over a random sample of the class centres each step — typically 10% — and showing the accuracy loss is negligible. The negatives were free; storing them was not.

Step 2: kill the bias, and see the two confounds in the norms

The logit for class j is

zj = Wj · x + bj = ‖Wj‖ · ‖x‖ · cos θj + bj

Three factors and an offset, and only one of them is about identity. Take them one at a time.

The bias bj. It shifts a class's score regardless of where the feature points. A class with a large bias wins arguments it should lose. At deployment you throw W and b away entirely and compare features to features, so every unit of capacity spent on b is capacity that does nothing for the thing you ship — and worse, it lets the network satisfy training without organising the geometry. Every method from SphereFace onward sets b = 0. One line, no downside.

The weight norm ‖Wj‖. It absorbs class frequency. If identity A has 500 training images and identity B has 12, gradient descent grows ‖WA‖ much larger, because a bigger norm reduces loss on every one of A's 500 images. The classifier then wins by prior, not by direction. Concretely, suppose ‖WA‖ = 12 and ‖WB‖ = 4, and a test feature (unit norm) sits at 70° from WA and 40° from WB:

zA = 12 · cos 70° = 12(0.3420) = 4.104     zB = 4 · cos 40° = 4(0.7660) = 3.064

The classifier says A. The geometry says B, by a wide margin — 40° versus 70°. After normalising the weights the logits become cos 70° = 0.342 and cos 40° = 0.766 and the answer flips to B, which is the answer a nearest-neighbour search on the embedding would have given. Since nearest-neighbour search is what you deploy, the unnormalised classifier was optimising the wrong decision rule.

The feature norm ‖x‖. It absorbs image quality. A sharp, well-lit, frontal face produces a large-norm feature; a blurred profile at 30 pixels produces a small one. Since the logit scales with ‖x‖, the easiest way for the network to reduce total loss is to inflate the norms of samples it already gets right — the gradient is larger there, and the improvement is free. So a plain softmax spends its capacity making easy samples easier, which is the opposite of what an open-set system needs.

An honest footnote, because this is not the whole story. Feature norm correlating with quality is a real signal, not only a nuisance. MagFace (2021) later reintroduces it deliberately: it makes the margin a function of the feature norm, so high-quality samples are pushed closer to their class centre and low-quality ones are allowed to stay near the boundary, giving you a built-in quality score for free. The lesson is not "norms are bad" — it is "an uncontrolled norm silently reweights your loss, so either remove it or use it on purpose".

Step 3: normalise both, and you are on a hypersphere

Set b = 0, and rescale so that ‖Wj‖ = 1 and ‖x‖ = 1. The logit reduces to a single quantity:

zj = cos θj    where θj is the angle between the feature and class j's proxy

Every feature and every proxy now lives on the unit hypersphere Sd−1. All information is angular. And here is the geometric fact that Chapter 5 turns on: on a unit sphere, the geodesic distance between two points — the shortest path along the surface — is exactly the angle θ between them, in radians. Arc length equals radius times angle, and the radius is 1. So "angle" is not a proxy for distance on the sphere; it is the distance.

This is NormFace (Wang et al., 2017), and it immediately breaks in an instructive way.

Why normalising alone stops training dead

Cosines live in [−1, 1]. The softmax over such logits can never be confident. Take the best case imaginable: the feature sits exactly on its own proxy (cos θy = 1) and is orthogonal to all C−1 others (cos = 0). Then

py = e1 / (e1 + (C−1)e0) = 2.71828 / (2.71828 + 85,741) = 3.170 × 10−5

using C = 85,742, the identity count of MS1MV2, the dataset ArcFace trains on. The cross-entropy is −ln(3.170×10−5) = 10.36. That is the floor: a perfect model, with every sample exactly on its proxy, cannot get the loss below 10.36. Compare that with the loss of a randomly-initialised model, which is ln(85,742) = 11.36. Perfection buys you one nat out of eleven. The gradient signal distinguishing "perfect" from "random" is a rounding error, and training does essentially nothing.

The fix is a single scalar. Multiply the logits by a scale s:

zj = s · cos θj    with s = 64 in both CosFace and ArcFace

Rerun the same best case with s = 64. The correct logit is 64, the others are 0, and

py = e64 / (e64 + 85,741) ≈ 1 − 85,741 · e−64 = 1 − 85,741(1.604×10−28) = 1 − 1.375×10−23

with a loss of 1.4 × 10−23. Now the loss can actually reach zero, so gradients mean something all the way down. The scale s is not a tuning knob you found empirically — it is the repair for a defect that normalisation introduced, and Chapter 5 derives how small it is allowed to be.

What normalisation actually removes

Two class proxies, one test feature. In "raw" mode the proxies have different lengths (class imbalance) and the feature has its own length (image quality), and the decision follows the logits. Switch to "normalised" and the decision follows the angles. Find a setting where the two disagree — it is not hard, which is the point.

‖WA 1.50
‖x‖ quality 1.00
Feature angle 75°

The last proxy: a function instead of a table

One more step is available, and it is the one that connects this entire lesson to modern multimodal models. The proxies are a lookup table: C rows, one per class, learned. What if the proxy for a class were computed instead of stored?

That is exactly what CLIP and CLAP do. Write their loss for one image i in a batch of N, where Eimg and Etxt are L2-normalised:

LInfoNCE = − log  e(1/τ) cos(imgi, txti) ⁄ ∑j e(1/τ) cos(imgi, txtj)

Put that next to Chapter 3's normalised softmax and the correspondence is exact, term for term.

Margin-softmax (this lesson)InfoNCE (CLIP, CLAP)The same thing
Wj, row j of a C×d tableThe text encoder's output for caption jA proxy for the class — stored versus generated
cos θj = Wj · xcos(img, txtj)The similarity being ranked
Scale s = 64Inverse temperature 1/τ; CLIP's τ = 0.01 gives 100The same scalar, differently named. Both exist because cosines live in [−1, 1]
Sum over C classesSum over N in-batch captionsThe negatives; the batch is a sampled subset of the class table
Margin m on the targetUsually noneThe one genuine difference — and margin variants of InfoNCE exist for exactly this reason
The scale and the temperature are the same knob, and this is the best sanity check in the lesson. ArcFace uses s = 64 for 85,742 classes; CLIP uses 1/τ = 100 against a batch of 32,768. Chapter 3's floor formula, ln((C−1)PW/(1−PW)), gives 13.6 for the first and ln(32,767 × 9) = 12.6 for the second — nearly identical, and both papers landed 4–8× above their floor by independent experiment. Two literatures that barely cite each other tuned the same constant to the same place, because it is the same constant.

So the arc of this lesson has one more step in it than the title suggests: sampled negatives → stored proxies → generated proxies. Each step removes a bookkeeping burden and buys a new kind of generalisation. Chapter 9 closes the loop; our CLAP veanor derives the generated-proxy version end to end in the audio domain.

The scoreboard so far

Triplet (FaceNet)Normalised softmax (NormFace)
Negatives per stepOne per triplet, chosen by a samplerAll C−1 classes, automatically
BookkeepingIdentity-structured batches, distance matrix, mask logicNone — shuffle the data like any classifier
Cost per stepO(B2) mining on top of the encoderOne (B×d)(d×C) matmul
Collapse riskReal, and it is a stationary pointNone — the classes must remain distinguishable or the loss is huge
Memory costBig batchesA C×d table — 85,742 × 512 floats = 176 MB, which becomes the bottleneck at 10M identities
What it optimisesExactly the deployed distanceSeparability against proxies, which is not the same thing

That last row is the remaining hole, and it is the hole Chapters 4 and 5 fill. Normalised softmax asks each feature to be closer to its own proxy than to any other proxy. Satisfying it by one degree scores as well as satisfying it by thirty. For an open-set system, the thirty degrees is the entire product.

python — normalised softmax (NormFace), the base every margin builds onclass CosineHead(nn.Module):
    def __init__(self, d, n_classes, s=64.0):
        super().__init__()
        self.W = nn.Parameter(torch.randn(n_classes, d) * 0.01)   # the proxies — NO bias
        self.s = s

    def forward(self, x):                    # x: (B, d) raw encoder features
        x = F.normalize(x, dim=1)            # kill the quality confound
        W = F.normalize(self.W, dim=1)       # kill the frequency confound
        return self.s * x @ W.T           # (B, C) — every entry is s·cos(theta)

Three lines of change from a normal linear head, and the model that comes out is a metric learner with no sampler. Everything from here is one more term inside that final expression.

Why must a scale s be introduced once features and weights are L2-normalised?
In what precise sense was a plain softmax classifier "already" doing metric learning?

Chapter 4: Two Margins

We have a normalised softmax whose logits are s·cos θj. It trains stably, needs no sampler, and produces embeddings that are separable. Chapter 0's Requirement 3 asked for more than separable. This chapter is where we ask for it, in two different ways, and hand-compute the difference.

What the objective currently demands, exactly

For sample x with true class y and nearest competitor j, plain normalised softmax is happy when

s cos θy > s cos θj  ⇔   cos θy > cos θj  ⇔   θy < θj

The decision boundary between two classes is the angular bisector, and "correct" means being on your own side of it by any amount whatsoever. Nothing in the loss says how far.

Now remember what happens at deployment. The proxies W are thrown away. A stranger arrives, gets embedded, and must be distinguishable from a different stranger. Neither of them was ever optimised. They will land somewhere in the neighbourhood of the training identities that look like them. If the training identities were packed together with zero clearance, there is nowhere for the strangers to go.

The one-line statement of why margins exist. Closed-set training only requires that training identities be separated. Open-set deployment requires that the space have room. A margin is how you write "leave room" in the language of a loss function — you make the model solve a harder problem than the one you will grade it on, and the surplus is what generalises.

The three ways to insert a margin

The target logit is s·cos θy. There are exactly three places to put a number m into cos θy, and the literature tried all three in order.

MethodTarget logitDecision boundary vs class jMargin typePaper values
Normalised softmaxs cos θycos θy = cos θjNones = 64
SphereFace (A-Softmax, 2017)s cos(m θy)cos(m θy) = cos θjMultiplicative angularinteger m = 4
CosFace / LMCL (2018)s(cos θy − m)cos θy − m = cos θjAdditive cosinem = 0.35, s = 64
ArcFace (2018)s cos(θy + m)cos(θy + m) = cos θjAdditive angularm = 0.5 rad = 28.65°, s = 64

SphereFace came first and is the reason the other two exist. Multiplying the angle is elegant — the required gap grows with the angle — but cos(m θ) is only monotonically decreasing for θ ∈ [0, π/m], so beyond 45° (with m = 4) the "loss" starts rewarding larger angles. SphereFace patches this with a piecewise surrogate ψ(θ) = (−1)kcos(m θ) − 2k and then, because even the patched version will not train from scratch, anneals it: start at ordinary softmax and gradually increase the margin's influence with a schedule on a parameter λ. Two hyperparameters and a schedule to make one margin behave. CosFace and ArcFace are both, in large part, "the same idea without the schedule".

CosFace, derived

Start from the requirement and work backwards. We want a classifier that is only satisfied when the correct class beats the runner-up by a fixed amount of cosine:

cos θy − m > cos θj  for all j ≠ y

The trick is that you do not have to change the loss to demand this — you change the logit and keep ordinary cross-entropy. Subtract m from the true class's cosine before the softmax, and cross-entropy will not be happy until the shifted logit wins. The full loss, which the CosFace paper calls the Large Margin Cosine Loss:

LLMC = − log  es(cos θy − m) ⁄ { es(cos θy − m) + ∑j≠y es cos θj }

Note carefully what m does not touch: the other classes' logits are untouched. The margin is a handicap applied to the class that is supposed to win. During training the model is graded as if its correct answer were worse than it is; at inference you drop the handicap entirely and simply use the embedding.

ArcFace, derived

Same construction, different quantity. ArcFace observes that the thing you actually care about — the separation on the hypersphere, the geodesic distance from Chapter 3 — is the angle, not its cosine. So add the margin to the angle before taking the cosine:

LArcFace = − log  es cos(θy + m) ⁄ { es cos(θy + m) + ∑j≠y es cos θj }

and, because cos is decreasing on [0, π], adding m to θy lowers the target logit exactly as subtracting m from the cosine did. The satisfied condition is

cos(θy + m) > cos θj  ⇔   θy + m < θj

Read that right-hand form slowly, because it is the entire argument of the next chapter. ArcFace's constraint, after the cosines cancel, is a statement about angles with no cosines in it at all: your angle to your own class, plus a constant, must be less than your angle to any other class. A constant angular gap. Which, on a unit sphere, is a constant arc length — a constant distance.

Expanding with the angle-sum identity shows what it costs to compute:

cos(θy + m) = cos θy cos m − sin θy sin m,    sin θy = √(1 − cos2 θy)

so you never call arccos. With m = 0.5 rad, cos m = 0.877583 and sin m = 0.479426 are constants; the whole margin costs one square root and two multiplies per sample. That efficiency is not a footnote — it is why ArcFace was adopted so fast. It is three lines added to a standard classification head, with no sampler, no schedule and no measurable slowdown.

One sample, three losses, all by hand

This is the arithmetic to carry with you. A single training sample, three classes, s = 64.

ClassAngle θjcos θjRole
y (the true identity)60°0.500000Correct, and comfortably ahead
j (the nearest impostor)75°0.258819The runner-up — the one that matters
k (an unrelated identity)110°−0.342020Irrelevant, included for realism

Plain normalised softmax. Logits: 64(0.5) = 32.000, 64(0.258819) = 16.564, 64(−0.342020) = −21.889. Subtract the max (32) for stability and exponentiate: e0 = 1, e−15.436 = 1.979×10−7, e−53.889 ≈ 4×10−24. So

py = 1 / (1 + 1.979×10−7) = 0.99999980  →   L = −ln py = 1.98 × 10−7

The model is fifteen degrees from a different identity — genuinely tight for face verification — and the loss is two ten-millionths. This sample is, as far as training is concerned, finished. Nothing further will be learned from it. Compare that with the mining problem of Chapter 2 and notice it is the same disease: an objective that stops caring long before the geometry is good.

CosFace, m = 0.35. Only the target logit changes: 64(0.500000 − 0.35) = 64(0.15) = 9.600. The others are unchanged, so the largest logit is now the impostor's 16.564. Shift by that max: e9.600−16.564 = e−6.964 = 9.449×10−4; e0 = 1; e−38.454 ≈ 2×10−17.

py = 9.449×10−4 / (1 + 9.449×10−4) = 9.440×10−4  →   L = 6.965

ArcFace, m = 0.5 rad = 28.65°. The target angle becomes 60° + 28.65° = 88.65°, whose cosine is 0.023562 (it is sin 1.35°). Target logit: 64(0.023562) = 1.508. Shift by the impostor's 16.564: e1.508−16.564 = e−15.056 = 2.891×10−7.

py = 2.891×10−7  →   L = 15.056
LossTarget logitpyLoss valueGradient available?
Normalised softmax32.0000.99999981.98 × 10−7None — sample is "done"
CosFace (m = 0.35)9.6009.44 × 10−46.965Large
ArcFace (m = 0.5)1.5082.89 × 10−715.056Very large
A shortcut worth memorising. When the margin pushes the target logit well below the top competitor, the softmax is dominated by that competitor and the loss is almost exactly the logit gap. Check it: CosFace gave 64(0.258819 − 0.15) = 64(0.108819) = 6.964, matching the 6.965 we computed the long way. ArcFace gave 64(0.258819 − 0.023562) = 64(0.235257) = 15.056, matching exactly. So loss ≈ s × (competitor cosine − margined target cosine). Once you internalise this you can predict what any (s, m) pair will do to your loss curve without running anything — and Chapter 5's failure modes become obvious rather than mysterious.

What each loss is asking the sample to do

The loss value is a scold. The useful question is: where does the sample have to move before the scolding stops? Solve each condition for θy, with the impostor fixed at 75°.

CosFace needs cos θy − 0.35 > 0.258819, so cos θy > 0.608819, so θy < arccos(0.608819) = 52.50°. The sample must travel 7.5°.

ArcFace needs θy + 28.65° < 75°, so θy < 46.35°. The sample must travel 13.65°.

Same nominal "margin idea", nearly double the demand in this instance. And that ratio is not fixed — it depends on where the sample sits, which is the entire subject of the next chapter.

Angular margin logit visualiser

Left: the sphere, seen edge-on as a circle. Your class proxy points right; the nearest impostor proxy is at 75°. Drag the feature and watch the three shaded "satisfied" arcs shrink as the margins bite. Right: the target logit as a function of θ, with the impostor's logit as a horizontal line — wherever a curve is below that line, the loss is roughly the vertical gap.

Feature θy 60°
CosFace m 0.35
ArcFace m 0.50
Scale s 64

The same three losses across the whole range

One sample is an anecdote. Sweep θy with the impostor fixed at 75° and s = 64, and the character of each loss shows up.

θySoftmax lossCosFace loss (m = 0.35)ArcFace loss (m = 0.5)What the sample is
10°6.6 × 10−213.5 × 10−113.0 × 10−15Textbook. All three agree it is finished
40°8.0 × 10−154.3 × 10−51.2 × 10−3Comfortable. Only the margins are still talking
60°2.0 × 10−76.9615.06The worked example. Softmax has quit; margins are shouting
74°0.2921.330.6One degree from being misclassified
80°5.4627.937.0Actually wrong. Everything is loud

Two readings. Down a column: every loss grows as the sample gets worse, as it must. Across a row: at 40° the margined losses are ten orders of magnitude larger than the plain one, and at 80° only five to seven times larger. The margin's relative effect is largest exactly where the plain loss has given up — on samples that are correct but unremarkable, which is the overwhelming majority of a training set and precisely the population whose geometry determines open-set performance.

And look at the first row, where ArcFace's loss is four orders of magnitude smaller than CosFace's. That is not a mistake: near its own class centre, ArcFace's cosine-space penalty is only 0.122 against CosFace's flat 0.35, so it leaves easy samples alone and saves its pressure for the middle of the range. Chapter 5 derives exactly where that crossover happens and why it is the right shape.

A prediction you can check in the simulation. Because loss ≈ s(cos θj − margined target), and because both margins reduce the target by a bounded amount, the ratio of margined loss to plain loss must fall as θ grows. So a margin is not "a stronger loss" — it is a reallocation of gradient toward the middle of the distribution. Drag the feature slider and watch the three bars converge as you approach the boundary.

One formula that contains all three

ArcFace's paper ends this argument neatly by writing a combined margin with three knobs:

target logit = s · cos(m1 θy + m2) − s m3
SettingRecovers
m1 = 1, m2 = 0, m3 = 0Normalised softmax (NormFace)
m1 = 4, m2 = 0, m3 = 0SphereFace
m1 = 1, m2 = 0, m3 = 0.35CosFace
m1 = 1, m2 = 0.5, m3 = 0ArcFace
m1 = 1.0, m2 = 0.3, m3 = 0.2A blend, which the paper reports works about as well as any single one

That last row is the honest summary of a three-year argument: these are three points in one small family, and the differences between well-tuned members are smaller than the differences between training sets. Which is worth remembering the next time a leaderboard moves by 0.05%.

The realisation — ArcFace in code, including the part that bites

python — ArcFace head, reference formclass ArcFace(nn.Module):
    def __init__(self, d, n_classes, s=64.0, m=0.5):
        super().__init__()
        self.W = nn.Parameter(torch.randn(n_classes, d))
        nn.init.xavier_normal_(self.W)
        self.s, self.m = s, m
        self.cos_m, self.sin_m = math.cos(m), math.sin(m)   # 0.877583, 0.479426
        self.th = math.cos(math.pi - m)                     # -0.877583
        self.mm = math.sin(math.pi - m) * m                 #  0.239713

    def forward(self, x, labels):
        cos = F.linear(F.normalize(x), F.normalize(self.W))    # (B, C) cosines
        sin = torch.sqrt((1.0 - cos.pow(2)).clamp(0, 1))          # clamp: float error
        phi = cos * self.cos_m - sin * self.sin_m              # cos(theta + m)

        # Guard: cos(theta+m) is only decreasing while theta <= pi - m.
        # Past that, fall back to a LINEAR penalty so the target logit
        # keeps decreasing in theta instead of turning around.
        phi = torch.where(cos > self.th, phi, cos - self.mm)

        onehot = F.one_hot(labels, cos.size(1)).float()
        logits = self.s * (onehot * phi + (1 - onehot) * cos)   # margin ONLY on target
        return F.cross_entropy(logits, labels)

Everything after phi = cos * cos_m - sin * sin_m exists because of a problem the formula does not advertise. cos(θ + m) decreases in θ only while θ + m ≤ π. Past θ = π − m = 180° − 28.65° = 151.35°, the cosine turns around and starts increasing, so a sample that is even further from its class would get a higher target logit and a lower loss. The optimiser would be rewarded for making things worse. The two constants patch this: past the threshold cos θ = cos(π − m) = −0.877583, use the linear fallback cos θ − m·sin(π − m) = cos θ − 0.239713, which keeps decreasing forever.

Why the mask is onehot * phi + (1 − onehot) * cos and not simply phi. The margin applies to the true class alone. Applying it to every logit would subtract a constant-ish amount from all of them, and softmax is invariant to shifts that are constant across classes — a huge amount of the effect would cancel and the rest would be noise. The asymmetry is the mechanism: you handicap the answer you want, so the model must earn a bigger lead.
In the worked example (θy = 60°, impostor at 75°, s = 64), plain normalised softmax gives a loss of 2×10−7 while CosFace gives 6.97. What does that difference represent?
Why does the ArcFace implementation need the th / mm guard clause?

Chapter 5: Why Angular Wins, and How s and m Kill You

Two margins, one sphere. This chapter answers why ArcFace's version is the principled one, and then spends the rest of its length on the two numbers that decide whether your training run converges or produces a very expensive constant function.

The geodesic argument

Chapter 3 established that on the unit hypersphere the geodesic distance between two points is the angle between them in radians. So the natural notion of "how much room is there around this identity" is measured in radians, and a margin that is constant in radians is a margin that is constant in distance on the manifold the model actually lives on.

ArcFace's satisfied condition was θy + m < θj: a fixed arc length, everywhere, for every sample. CosFace's was cos θy − m > cos θj, which is fixed in cosine units. Since cosine is a nonlinear function of angle, a fixed cosine gap is a varying angular gap. Compute how much it varies.

For CosFace, a sample at angle θ is treated as though it were at the angle θ′ = arccos(cos θ − m). The effective angular push is θ′ − θ:

Sample's true angle θcos θCosFace: cos θ − 0.35θ′ = arccos(·)CosFace angular pushArcFace angular push
20° (well inside its class)0.93970.589753.86°33.86°28.65°
40°0.76600.416065.41°25.41°28.65°
60° (the worked example)0.50000.150081.37°21.37°28.65°
85° (near the boundary)0.0872−0.2628105.24°20.24°28.65°

CosFace's demand varies by a factor of 1.67 across the range, and — look at the direction — it demands most from samples that are already well inside their class, and least from the samples sitting near the boundary. Those boundary samples are the ones that will be confused with a stranger at deployment. The margin is weakest exactly where the product fails.

The mirror-image statement, so you understand both methods rather than cheering for one. ArcFace's margin is constant in angle and therefore varying in cosine. Using the sum-to-product identity, the cosine-space gap it imposes is cos θ − cos(θ + m) = 2 sin(m/2) sin(θ + m/2) = 0.4948 sin(θ + 14.32°) for m = 0.5. That runs from 0.122 at θ = 0°, to 0.476 at 60°, peaking at 0.495 around θ = 75.7°. So ArcFace is gentle on samples near their class centre and firm on the ones near the boundary. Whether you call that "principled" or merely "a better-shaped curriculum" is a matter of taste — but the shape is right for the failure mode we care about.

The honest comparison

The temptation is to conclude that ArcFace is strictly better. The numbers do not support anything that strong, and it is worth seeing why.

Reported resultFaceNet (2015)CosFace (2018)ArcFace (2018/19)
LFW verification accuracy99.63%99.33%99.83%
Training data100–200M images, ~8M identities (private)~5M images, ~90K identities (private, cleaned)MS1MV2: 5.8M images, 85K identities (public)
BackboneZeiler–Fergus (140M params) or Inception (7.5M)64-layer CNNResNet-100
LossTriplet, α = 0.2, semi-hard miningAdditive cosine margin, s = 64, m = 0.35Additive angular margin, s = 64, m = 0.5
Do not read that table as a ranking. Three papers, three private-or-different training sets, three backbones, three years of unrelated engineering progress. The only clean comparisons are the ones inside a single paper, where everything else is held fixed — and inside ArcFace's own controlled experiments the gap over CosFace is real but small on saturated benchmarks (fractions of a percent on LFW, where 99.83% versus 99.81% is a difference of one or two pairs out of 6,000) and larger on the hard ones, IJB-B and IJB-C, where errors are measured at low false-accept rates. Chapter 6 explains exactly why LFW cannot resolve these differences and IJB-C can.

ArcFace's own paper concedes the closeness in the most useful way possible: it defines a combined margin, cos(m1θ + m2) − m3, which contains SphereFace (m1), ArcFace (m2) and CosFace (m3) as special cases, and reports that combinations of the three work about as well as any single one. The three papers are three points in one family, not three theories.

The s knob: a floor and a cliff

Chapter 3 showed that s exists because the loss has a floor without it. Now derive how small s can be.

Suppose you want a well-classified sample to reach probability PW on its correct class. Best case again: cos θy = 1 and the other C−1 classes are orthogonal. Then

PW = es / (es + (C−1))  ⇒  es = (C−1) PW/(1−PW)  ⇒  s = ln{(C−1)PW/(1−PW)}

CosFace derives essentially this bound (with a (C−1)/C factor from a slightly more careful setup). Put ArcFace's numbers in: C = 85,742 and a modest target of PW = 0.9.

s ≥ ln(85,741 × 9) = ln(771,669) = 13.56

So s must be at least about 14 just to make a perfect sample confident. Real samples are not perfect — they sit at 20° or 40°, not 0° — so you need headroom, which is why the papers use 64 and speaker-verification systems, with far fewer classes, use 30.

Scale sBest achievable loss (C = 85,742)What training does
1ln(85,741) − 1 ≈ 10.36Nothing. Perfect and random differ by one nat
8≈ 3.39Learns slowly, plateaus high, embeddings stay diffuse
16≈ 0.0096Works — e16 = 8.886×106 finally dwarfs the 85,741 competitors
32≈ 1.1 × 10−9Works well; common in speaker and re-ID systems
64≈ 1.4 × 10−23The face-recognition standard — plenty of headroom
2560 in float32Gradients scale with s; loss becomes hinge-like; one mislabelled sample can wreck a step

Check the s = 8 row by hand: e8 = 2980.96, so p = 2980.96/(2980.96 + 85,741) = 0.03360, and −ln(0.03360) = 3.393. A model that has solved the problem perfectly still reports a loss of 3.4, and every diagnostic you have will tell you it is broken.

The cliff at the other end is about gradients, not floors. Differentiating cross-entropy through the scaled logit gives ∂L/∂cos θy = s(py − 1), so every gradient carries a factor of s. At s = 64 a confidently wrong sample delivers a gradient 64 times what an unscaled loss would; with a mislabelled image, that is 64 times the wrong thing. In practice: keep s in [16, 64], and if you must raise it, lower the learning rate proportionally.

The m knob: three distinct ways to die

Failure 1: the initialisation blow-up. At initialisation the proxies are random, and in high dimensions two random vectors are nearly orthogonal — so θy ≈ 90° for every sample and every class. Plain softmax then gives all logits ≈ 0, so p = 1/C and the loss is ln(85,742) = 11.36, which is exactly what you see in step 1 of any classifier. Now add ArcFace's margin:

target logit = 64 cos(90° + 28.65°) = 64(−0.4794) = −30.68,   others ≈ 0
L ≈ 30.68 + ln(85,741) = 30.68 + 11.36 = 42.04

The margin adds thirty nats of loss at step zero, before the model has done anything wrong, and every one of those nats is multiplied by s in the gradient. Many runs diverge in the first few hundred steps. The standard remedies: warm up with m = 0 for an epoch and then switch on the margin; or ramp m linearly over the first few thousand steps (SphereFace's λ annealing is the ancestor of this); or use the easy-margin variant, which applies the margin only when cos θ > 0, so the worst case at initialisation is neutral rather than catastrophic.

Failure 2: the non-monotone tail. Chapter 4's guard clause. Without it, samples past θ = π − m are rewarded for getting worse, and a subpopulation of your data can migrate to the far side of the sphere and stay there, invisible on the training loss.

Failure 3: an unsatisfiable constraint. Push m high enough and the model literally cannot arrange 85,742 classes with that much clearance in the space available, so the loss plateaus high, accuracy falls, and eventually training destabilises. CosFace sweeps m from 0 up to 0.45 and finds performance improving to about 0.35 and then degrading, with the large settings unstable. ArcFace sweeps m and reports 0.5 as the best, with 0.4 and 0.6 close behind and larger values worse. These are not universal constants: they are the right answers for ~85K identities in 512 dimensions with a ResNet-100. Change the class count, the dimension or the data quality and the optimum moves.

s and m are not independent, and this is the most useful practical fact in the chapter. The penalty the margin actually applies to the loss is s × Δcos, from the shortcut in Chapter 4. So the product is what matters: CosFace's (64, 0.35) applies s·m = 22.4 nats of handicap. If you halve s to 32, keeping the same handicap needs m = 0.7 — well outside the stable range, which is why you cannot simply trade one for the other. Tune s first (from the floor formula and your class count), then m (by sweep), and re-sweep m whenever you change s.
The s and m failure map

Top: the best loss achievable at each scale, for your chosen class count — the region left of the knee is where training cannot work at all. Bottom: the ArcFace target logit against θ, with and without the monotonicity guard, plus the loss the model would see at initialisation. Push m past 0.9 with the guard off and watch the curve turn around.

Scale s 64
Margin m (rad) 0.50
Classes C 85,742

Where the gradient actually points, derived

One more derivation, because it settles the ArcFace-versus-CosFace question more convincingly than any benchmark. Differentiate the loss with respect to the angle — the quantity we actually care about — using the chain rule through the target logit.

For cross-entropy, ∂L/∂zy = py − 1. For ArcFace, zy = s cos(θy + m), so ∂zy/∂θy = −s sin(θy + m). Multiply:

∂L/∂θy = s · (1 − py) · sin(θy + m)    (ArcFace)

It is positive, so descent decreases θy: the feature rotates toward its own class. Three factors multiply, and each has a clean meaning:

FactorRangeWhat it encodes
sfixed, 30–64Global gain. Every gradient in the model carries it — the reason a large s needs a smaller learning rate
1 − py[0, 1]"How wrong am I." Vanishes when the model is confident, which is what makes cross-entropy self-limiting
sin(θy + m)[0, 1]The geometric factor, and the whole argument. It is maximal when θy + m = 90°

Now run the same derivation for CosFace, where zy = s(cos θy − m) and ∂zy/∂θy = −s sin θy:

∂L/∂θy = s · (1 − py) · sin θy    (CosFace — note that m has vanished from the geometry)
There is the difference, in one line. CosFace's geometric factor peaks at θy = 90° — on samples that are already orthogonal to their own class, which in a trained model is nearly nobody. ArcFace's peaks at θy = 90° − m = 90 − 28.65 = 61.35°, which is squarely inside the bulk of a real angle distribution. The additive angular margin does not merely make the loss bigger; it moves the peak of the geometric gradient onto the population that exists. Plain softmax (m = 0) is the degenerate case, aiming its strongest geometric push at samples that are already lost.

Check it numerically on the Chapter 4 sample (θy = 60°, impostor 75°, s = 64). ArcFace: py = 2.89×10−7, so the gradient is 64 × 0.9999997 × sin(88.65°) = 64 × 0.99972 = 63.98 per radian. CosFace: py = 9.44×10−4, so 64 × 0.99906 × sin(60°) = 64 × 0.8652 = 55.37. About 16% more angular force on exactly the sample that needs it, from the same s and comparable m.

Reading a training curve: symptom to cause

What you seeAlmost alwaysThe checkThe fix
Loss flat at ln C from step 1Scale below the floor, or normalisation missingPrint the max logit. If it is under about 5, s is too small or you forgot to normaliseRaise s; verify both F.normalize calls
Loss flat at a number a little above 0 that never improvesYou are at the floor — the model may be perfectCompute ln(1 + (C−1)e−s) and compareRaise s if you need more resolution; otherwise nothing is wrong
Loss explodes to 40+ in the first hundred steps, then NaNFull margin at initialisationLog the loss at step 0. If it is far above ln C, the margin is onWarm up with m = 0, or ramp m, or use the easy-margin variant
Loss falls nicely, verification accuracy does not improveYou are tuning on the wrong quantityPlot TAR at low FAR per epoch, not accuracy and not lossSelect on the verification metric — the margin makes training loss non-comparable across m
A stubborn subpopulation with cos θy < 0Samples past the monotonicity break, or a corrupt identityHistogram cos θy; inspect the worst 50 images by eyeConfirm the guard clause is present; consider sub-centre ArcFace for noisy identities
Great on the training identities, poor on held-out onesMargin too small, or C too small for the embedding dimensionCompare the mean intra-class angle with the mean nearest-class angleIncrease m; add identities before adding images

A tuning procedure you can actually follow

StepWhat to doWhy
1Compute the floor: s ≥ ln((C−1)PW/(1−PW)) with PW = 0.9, then take 3–4× that, capped at 64Guarantees the loss can reach zero with headroom for imperfect samples
2Train one epoch with m = 0 and confirm the loss falls below ln CIf it sits at ln C, your normalisation or scale is wrong, and no margin will save you
3Turn on the margin, ramping from 0 to the target over ~2,000 stepsDefuses the 42-nat initialisation spike computed above
4Sweep m over {0.2, 0.3, 0.4, 0.5} (angular) or {0.2, 0.3, 0.35, 0.4} (cosine)The optimum depends on your C, d, and label noise — the paper values are not constants
5Select on a hard verification benchmark at low FAR, never on training lossThe margin makes the training loss worse by design; the training loss cannot rank margins
6Log the mean and the 5th percentile of cos θy every epochThe best single diagnostic: the mean tells you if it is learning, the tail tells you if a subpopulation is stranded

Step 5 deserves emphasis. Adding a margin raises the training loss on purpose — that is what a handicap does. If you tune m by watching the loss go down, you will choose m = 0 every time. This is the most common way engineers "disprove" ArcFace on their own data.

Why is a constant angular margin the more principled choice on a normalised embedding?
You set s = 8 with 85,742 classes and the loss stalls around 3.4 no matter how long you train. What is happening?

Chapter 6: TAR at FAR, and Why Accuracy Lies

You have an embedding. Somebody asks how good it is. If your answer is a single accuracy number, you have not answered, and this chapter is about why — with every number computed by hand on a set small enough to check on paper.

The vocabulary, pinned down

A verification system compares two templates and produces a similarity score. There are exactly two kinds of comparison and exactly two kinds of error.

TermDefinitionAlso calledThe harm when it happens
Genuine pairTwo templates from the same personMated pair
Impostor pairTwo templates from different peopleNon-mated pair
FMR / FAR / FPRFraction of impostor pairs scoring at or above the thresholdFalse match rate, false accept rateA stranger unlocks your phone. A security failure
FNMR / FRR / FNRFraction of genuine pairs scoring below the thresholdFalse non-match rate, false reject rateYou are locked out. A usability failure
TAR1 − FNMR — the fraction of genuine pairs correctly acceptedTrue accept rate, verification rate, VAL
EERThe error rate at the threshold where FMR = FNMREqual error rateA single summary number; convenient and almost never the operating point

The two error rates trade off through one scalar: the threshold. Raise it and impostors are rejected along with some genuine users; lower it and everybody gets in. There is no setting that reduces both. Reporting one without the other is meaningless, which is why the field's standard unit of measurement is TAR at a fixed FAR — "how many real users do we let in, given that we accept a stranger only once in ten thousand tries".

A ROC computed entirely by hand

Twenty pairs. Ten genuine, ten impostor. Cosine scores, sorted:

genuine:  0.92, 0.88, 0.81, 0.77, 0.74, 0.69, 0.61, 0.55, 0.44, 0.31
impostor:  0.52, 0.47, 0.41, 0.38, 0.33, 0.29, 0.24, 0.18, 0.11, 0.05

For a threshold t, accept a pair when score ≥ t. Then FAR(t) = (impostors ≥ t)/10 and TAR(t) = (genuines ≥ t)/10. Sweep t downward and count.

Threshold tImpostors ≥ tFARGenuines ≥ tTARFRR = 1 − TAR
0.9500.000.01.0
0.9000.010.10.9
0.7500.040.40.6
0.5500.080.80.2
0.501 (0.52)0.180.80.2
0.452 (0.52, 0.47)0.280.80.2
0.4030.39 (0.44 joins)0.90.1
0.3050.510 (0.31 joins)1.00.0
0.00101.0101.00.0

Read the answers straight off the table.

TAR at FAR = 0 is 0.80, at threshold 0.55: we can accept 8 of 10 genuine pairs while admitting zero impostors, because the best impostor scores 0.52 and the eighth-best genuine scores 0.55. The whole system's quality lives in that three-hundredths of a gap.

EER: find where FAR = FRR. At t = 0.45, FAR = 0.2 and FRR = 0.2 exactly. The equal error rate is 20%, at threshold 0.45. Note it corresponds to a threshold that is worse for both metrics than 0.55 — EER is a summary of the curve, not a recommended operating point.

Accuracy, if you insist: at t = 0.45 we accept 8 genuine (8 correct) and reject 8 impostors (8 correct), so 16/20 = 80%. That single number hides that we let in two strangers, which for a door lock is the only fact anybody cares about.

The reason accuracy is the wrong metric is not statistical prudishness — it is that accuracy assumes a 50/50 prior. Our toy set has ten genuine and ten impostor pairs. A phone unlock has a prior more like a million impostor attempts per genuine attempt over the device's lifetime, and a watchlist search has a prior of essentially zero genuine matches. Under a realistic prior, a system with 99.9% accuracy on a balanced set can produce far more false matches than true ones. LFW's headline "accuracy" is computed on a balanced 6,000-pair set. It answers a question nobody deploys.

Why LFW cannot tell ArcFace from CosFace

LFW's standard protocol has 6,000 pairs: 3,000 genuine and 3,000 impostor. Two consequences follow immediately.

Resolution. The smallest nonzero FAR you can measure is 1/3,000 = 3.3×10−4. Any question about behaviour at FAR = 10−6 — the operating point of an actual access-control system — is unanswerable on LFW in principle, not for want of a better model.

Noise. At 99.83% accuracy the model makes 0.0017 × 6,000 ≈ 10 errors in total. A rival at 99.80% makes 12. The difference is two pairs. Rerun with a different fold split, or fix two mislabelled pairs in the benchmark (LFW is known to contain a handful), and the ranking flips. Differences in the third decimal place on LFW are noise, and papers that rank methods by them are ranking noise.

This is why the field moved to IJB-B and IJB-C. IJB-C's 1:1 protocol contains on the order of 19,600 genuine and 15.6 million impostor comparisons, which supports measurement down to FAR = 10−6 and separates methods that are indistinguishable on LFW.

How many pairs do you need? A Poisson argument

Suppose the true FAR at your threshold is 10−6 and you test with exactly 106 impostor pairs. The expected number of false accepts is 106 × 10−6 = 1. The observed count is Poisson with mean 1: you will see 0 about 37% of the time, 1 about 37%, 2 about 18%, 3 or more about 8%. Your FAR estimate is therefore 0, 10−6, 2×10−6 or worse, essentially at random.

The relative standard error of a count k is 1/√k. To pin FAR down to ±10% you need about k = 100 false accepts, so:

#impostor pairs ≥ 100 / FAR  →   FAR = 10−6 needs 108 impostor comparisons

And impostor pairs are cheap only if identities are plentiful: with G identities and one template each, you get G(G−1)/2 impostor pairs, so 108 pairs needs about 14,150 identities. This is the arithmetic behind every "we need a bigger benchmark" paper of the last decade.

Identification is harder than verification, and the arithmetic says how much

Now the 1:N task. A probe is compared against a gallery of N templates, and the system returns a ranked list. Two measurements:

Closed-set: the CMC curve. Rank-k identification rate is the fraction of probes whose true identity appears in the top k. Rank-1 is the headline; the curve to rank-20 tells you whether the failures are near-misses (the curve shoots up) or catastrophic (it stays flat). A flat CMC curve means the errors are not "the model was unsure between two people" but "the correct person is nowhere near the top", which usually indicates a data problem rather than a model problem.

Open-set: false positives compound with N. This is the part that gets systems deployed into disaster. If each comparison has an independent false-match probability FMR, then the probability that at least one of the N gallery entries falsely matches is

P(at least one false match) = 1 − (1 − FMR)N

Put in a gallery of one million and an excellent per-comparison FMR of 10−6:

1 − (1 − 10−6)1,000,000 = 1 − e−1 = 0.632

Nearly two thirds of searches against a one-million gallery return at least one wrong person — using a per-comparison error rate that is genuinely excellent. To get that down to a 1% chance of any false match you need

1 − (1−p)106 = 0.01 ⇒ p = −ln(0.99)/106 = 0.01005/106 = 1.005 × 10−8

a hundred times stricter than the FMR you started with — and, by the Poisson argument above, a rate you would need 1010 impostor comparisons to even measure.

The sentence to remember from this chapter. A verification threshold that is excellent at 1:1 is reckless at 1:N, and the conversion factor is the gallery size. Any system that searches a large gallery must operate at a per-comparison FMR that is smaller by roughly a factor of N — and must report FPIR (false positive identification rate), not FMR, or it is reporting a number that does not describe its own behaviour. Chapter 8 is about what happens when this arithmetic is not done before the system is pointed at people.
ROC and TAR at FAR playground

Top: genuine (green) and impostor (red) score distributions with your threshold. Bottom left: the ROC on a logarithmic FAR axis — the axis every biometric paper uses, because all the interesting behaviour is squashed against zero on a linear one. Bottom right: the operating numbers. Now push the "group B shift" slider a little and watch what a small distribution shift does to FMR at a shared threshold.

Separation 0.55
Score spread 0.12
Threshold 0.45
Group B shift 0.00

The demographic differential, computed before we discuss it

That last slider deserves its own arithmetic, because Chapter 8's argument depends on it and it is usually asserted rather than derived.

Model impostor scores for two groups as Gaussians with the same spread σ = 0.05 but slightly different means: group B at 0, group A at 0.05 — one standard deviation higher, a shift so small you would never see it on a histogram. Set a single global threshold at t = 0.25. The false match rate for each group is the Gaussian tail above t:

group B:  z = (0.25 − 0.00)/0.05 = 5.0 →  FMR = Φ(−5) = 2.87 × 10−7
group A:  z = (0.25 − 0.05)/0.05 = 4.0 →  FMR = Φ(−4) = 3.17 × 10−5
ratio = 3.17×10−5 ÷ 2.87×10−7 = 110×

A one-sigma shift in the mean impostor score — the kind of difference produced by, say, a training set with fewer examples of a group, or a face detector calibrated on different skin tones — becomes a hundredfold difference in false match rate at a shared threshold. Not because the tail is unfair, but because it is exponential. This is the mechanism behind the demographic differentials that NIST measured across the industry, and it is why "our model has the same accuracy for everyone" is not the claim that matters.

A CMC curve, also by hand

Ten probes searched against a gallery of five identities. For each probe, record the rank at which the true identity appears:

ranks = [ 1, 1, 1, 2, 1, 3, 1, 1, 5, 1 ]

The cumulative match characteristic at rank k is the fraction of probes whose true identity appears at rank k or better. Count them:

kProbes with rank ≤ kCMC(k)What it adds
170.70The headline "rank-1 accuracy"
280.80One probe was beaten by a single lookalike
390.90
490.90
5101.00The last failure was ranked dead last of five

CMC is non-decreasing by construction, and its shape is the diagnostic. A curve that jumps from 0.70 to 0.95 by rank 3 says the model has the right neighbourhood and is losing on fine discrimination — more capacity or a bigger margin will help. A curve that crawls says the failures are not near-misses at all: the correct identity is nowhere near the top, which points at the front end (a bad crop, a wrong pose) rather than the embedding.

A trap in this table. Rank-1 of 70% against a gallery of five is dismal; against a gallery of five million it would be remarkable. CMC without the gallery size is meaningless, and CMC curves computed on different gallery sizes cannot be compared — which does not stop people from putting them on the same axes. Always report N.

Templates: many photos, one vector

Real systems rarely have one image per person. Enrolment gives you several; a video gives you hundreds. The standard move is to aggregate into a template: average the embeddings, then renormalise.

t = ( ∑i f(xi) ) / ‖ ∑i f(xi) ‖

The arithmetic of why this works is worth doing. Model each embedding as the identity's true direction plus isotropic noise with per-component standard deviation σ. Averaging k independent samples divides the noise standard deviation by √k while leaving the signal unchanged, so the angular error to the true direction shrinks by the same factor:

Images in the templateNoise scaleTypical angular error
1σθ
4σ/2θ/2
9σ/3θ/3
100σ/10θ/10 — and here the model stops being the bottleneck

Two details decide whether you get that √k. Average before you normalise, not after: the pre-normalisation norm carries quality information (Chapter 3), so summing raw features quietly downweights the blurry frames — a free quality-weighted average. And the frames must be independent: 100 consecutive frames of a video are not 100 samples, they are approximately one sample with 100 copies of the same noise, so the √k never materialises. This is also why "we evaluated on video and got 99.9%" claims deserve suspicion.

The realisation — evaluation in code

python — TAR at a target FAR, the function you will write a hundred timesimport numpy as np

def tar_at_far(gen_scores, imp_scores, target_far=1e-4):
    # The threshold is set by the IMPOSTORS only — never by the genuine pairs.
    imp = np.sort(imp_scores)[::-1]            # descending
    k   = int(np.floor(target_far * len(imp)))     # how many false accepts we allow
    if k == 0:
        raise ValueError(
            f"Only {len(imp)} impostor pairs — cannot resolve FAR={target_far}. "
            "You need at least 1/FAR pairs to see one false accept, and ~100/FAR to measure it.")
    thr = imp[k - 1]                              # the k-th highest impostor score
    tar = (np.asarray(gen_scores) >= thr).mean()
    return tar, thr, k                          # ALWAYS return k — it is your error bar

# Report per demographic group at the SHARED threshold, not per-group thresholds:
for g, idx in groups.items():
    fmr_g = (imp_scores[idx] >= thr).mean()
    print(g, "FMR", fmr_g, "n_pairs", len(idx))     # n_pairs is the honesty column

Two details in that snippet are the difference between an evaluation and a decoration. The threshold is derived from impostor scores alone, so it never peeks at the genuine set. And k — the number of false accepts your threshold actually admits — is returned, because k = 3 means your TAR figure has an error bar of ±58% and nobody should quote its third digit.

Using the twenty toy scores above, what is TAR at FAR = 0.1?
Your model has a per-comparison FMR of 10−6, excellent by any standard. You deploy it to search a gallery of one million people. What fraction of searches returns at least one false match?

Chapter 7: The Same Recipe, Everywhere Else

Nothing in Chapters 1 through 5 mentioned faces. Go back and check: the derivations used "identity", "positive", "negative", "proxy" and "angle". Faces were the motivating example and the benchmark, not an assumption. So the machinery transfers, and it transferred fast — usually within a year of each paper, and usually with the same hyperparameters.

The transfer table

DomainWhat is an "identity"?What is a positive pair?What is deployedWhat the field uses now
Speaker verificationA speakerTwo utterances from the same speakerAn embedding ("x-vector", "speaker embedding") compared by cosineAAM-softmax — additive angular margin, which is ArcFace under a different name. ECAPA-TDNN uses m = 0.2, s = 30
Visual place recognitionA physical placeTwo images taken within a few metres, possibly years apartA global descriptor searched against a map databaseWeakly-supervised triplets (NetVLAD) gave way to classification-with-margin over geographic cells (CosPlace, EigenPlaces)
Person re-identificationA person, across non-overlapping camerasTwo crops of the same person from different camerasA gallery ranking per camera hand-offBatch-hard triplet with soft margin, usually combined with a margin-softmax head
Product / image retrievalA product SKUTwo photos of the same productRecall@k over a catalogueProxy-Anchor and margin-softmax variants; the proxy insight from Chapter 3 is native here
Text and multimodal embeddingsA query-document (or image-caption) pairThe annotated matchVector search over an ANN indexInfoNCE with in-batch negatives — the batch is the mining strategy
Signature, iris, fingerprint, gaitAn enrolled subjectTwo captures of the same traitA 1:1 match at a policy thresholdThe same margin-softmax heads, with domain-specific front ends
The tell that a problem belongs to this family. Ask one question: will the system meet classes at deployment that it did not see in training, and must it decide "same or different" about them? If yes, it is an open-set metric-learning problem, and everything in this lesson applies — the collapse, the mining, the normalisation, the margin, the TAR-at-FAR evaluation, and the s and m failure modes. If no, train a classifier and go home.

Speaker verification: the cleanest transplant in machine learning

Speaker verification has exactly the structure of face verification: enrol a speaker from a few seconds of audio, then accept or reject future utterances. The field went through the same sequence with a delay of a year or two — i-vectors and PLDA (hand-designed), then x-vectors with a softmax classification bottleneck (the "take an intermediate layer" trick FaceNet criticised), then triplet losses, then AAM-softmax.

The migration is unusually literal: AAM-softmax is ArcFace's formula, with the same two hyperparameters, differently tuned. The typical speaker setting is s = 30 and m = 0.2 rather than s = 64 and m = 0.5, and Chapter 5 predicts both changes. Speaker corpora have thousands of speakers rather than 85,000 identities, so the floor formula demands a much smaller s: with C = 6,000 and PW = 0.9, s ≥ ln(5,999 × 9) = ln(53,991) = 10.9, and 30 gives ample headroom. And utterance embeddings have larger intra-class variance than face crops, so a smaller margin is what the data can actually satisfy.

The evaluation transferred too, including its problems: VoxCeleb reports EER and minimum detection cost, systems are compared at the third decimal place, and the same "is this difference real" question applies.

Visual place recognition, and the loop back to this site's veanors

Place recognition asks whether two photographs show the same physical location, with the same open-set structure — a robot will visit places absent from its training set. NetVLAD's contribution was a differentiable aggregation layer plus a weakly supervised triplet loss, which handles the fact that GPS gives you "these two photos were taken near each other" but not "these two photos show the same thing". Its positives are therefore potential positives, and the loss takes the best one — a mining problem with an extra layer of uncertainty on top of Chapter 2's.

Then the same reformulation happened as in faces: CosPlace and EigenPlaces recast VPR as classification with a margin over discretised geographic cells, precisely to escape the sampler. If Chapters 2 and 3 felt familiar while reading those papers, that is why.

Cross-domain bridge
A face gallery and a place database are the same index with different photographs in it
Both embed a query once, search a precomputed table of descriptors by cosine, and threshold. Both are open-set. Both fail in the same way at scale — false positives compound with gallery size, exactly as computed in Chapter 6 — and both were rescued by the same move from sampled triplets to margin-softmax. Read NetVLAD for the aggregation half of the story, VPR aggregation for what came after, and foundation models for VPR for the current frontier. Then note that vector embeddings and ANN indexes are the serving layer for all of it: an embedding is only useful at scale if something can search a billion of them in milliseconds.

A port, worked all the way through: speaker verification

Do not take "it transfers" on faith. Here is the whole port, decision by decision, for a system that verifies a speaker from a few seconds of audio.

DecisionFace systemSpeaker systemReasoning
Front endDetect, align to 112×112 by 5 landmarksVoice activity detection, then 80-bin log-Mel, mean-normalised, ~200 framesBoth remove nuisance variation before the encoder. Alignment is to a canonical face; mean normalisation is to a canonical channel
EncoderResNet-100 → 512-dECAPA-TDNN → attentive statistics pooling → 192-dAudio is variable-length, so the pooling layer is doing what the fixed crop did for faces
HeadArcFace, s = 64, m = 0.5AAM-softmax — the same formula — typically s = 30, m = 0.2Derived below
Deployed artifact512 floats, cosine192 floats, cosine (often with score normalisation)Identical interface
MetricTAR at FAR 10−4 to 10−6EER and minimum detection cost on VoxCelebSame ROC, different summary conventions — and the same statistical-power problem

Why s = 30 and not 64. Apply Chapter 5's floor formula with a speaker corpus of about 6,000 identities and a target confidence of 0.9:

s ≥ ln( (C−1) PW / (1−PW) ) = ln(5,999 × 9) = ln(53,991) = 10.90

Roughly a third of the 13.56 that 85,742 face identities demanded, because the floor grows only with the logarithm of the class count. Three times the floor gives about 33, and the field settled on 30. The face value of 64 is about 4.7× its floor; 30 is 2.8× its own. Both are in the same regime, which is the point: the number is derived, not inherited.

Why m = 0.2 and not 0.5. The margin is a claim about how tightly a class can be squeezed, and that is a property of the data. A three-second utterance varies with the sentence spoken, the room, the microphone, the speaker's health and their mood; an aligned face crop has already had pose, scale and rotation removed by the front end. Ask for 28.65° of clearance from utterances and a large fraction of the training set becomes unsatisfiable — the loss plateaus high and the embedding gets worse, which is Chapter 5's third failure mode arriving in a different domain.

Person re-identification and dense retrieval — the mining story repeats

Two more instances, both of which rediscovered Chapter 2 rather than Chapter 5.

Person re-ID matches a pedestrian across non-overlapping cameras. Identities number in the low thousands, images are noisy crops from surveillance video, and the standard recipe is batch-hard triplet with P = 18 identities × K = 4 images, often summed with a margin-softmax head. Why triplets survived here when faces abandoned them: with only a few thousand identities the proxy table is tiny and the classification signal is weak, while the batch-hard bound keeps mining honest at B = 72. Small C is the regime where triplets remain competitive.

Dense text retrieval embeds queries and documents into a shared space and searches by cosine — the same interface again. It began with in-batch negatives (the batch is the sampler), and then discovered that in-batch negatives become too easy as the model improves, which is precisely Chapter 2's self-defeating loop. The field's answer was to mine hard negatives from the corpus with the current model and refresh them periodically — ANCE, RocketQA and their descendants — which is FaceNet's offline mining, with the staleness problem and all. Read those papers with Chapter 2 in hand and there are no surprises left in them.

The unifying view: alignment and uniformity

Every loss in this lesson can be read as two forces, and Wang and Isola's 2020 analysis of contrastive representation learning gives them names.

Alignment
Positive pairs should map to nearby features. This is the ‖f(a) − f(p)‖2 term in the triplet loss, and the −s cos θy term in every margin-softmax.
↓ and, in tension with it …
Uniformity
Features should spread out over the sphere, preserving as much information as possible. This is the −‖f(a) − f(n)‖2 term, and the log-sum-exp over the other classes.
↓ and the knobs you have been tuning …
m and s are the dial between them
The margin m strengthens alignment (get closer to your own proxy than you thought you needed to). The scale s sharpens the softmax, which is exactly a temperature on the uniformity term. Chapter 5's "s and m are not independent" is this tension, restated.

Collapse, from Chapter 1, is perfect alignment with zero uniformity. A model that spreads everything uniformly with no alignment is a random projection. Every method in the canon is a different way of holding the two in balance, and our alignment and uniformity veanor works through the formal version.

Visual place recognition, worked

VPR is the port where the "what is an identity?" question is genuinely hard, which makes it the most instructive one.

A place is not a discrete object. Two photographs taken five metres apart show the same place; two taken five metres apart around a corner do not. GPS gives you proximity, not visual overlap. NetVLAD's answer was weak supervision: treat every geographically-near image as a potential positive, and let the loss pick the best one:

L = ∑j max{0,  mini d(q, pi)2 + α − d(q, nj)2}

The min over candidate positives is the new part — a mining problem inside the positive set, on top of Chapter 2's mining problem in the negative set. It works, and it inherits every difficulty this lesson catalogued, doubled.

Then the field did what faces did. CosPlace and EigenPlaces discretise the map into cells — a few metres of position crossed with a heading bin — call each cell a class, and train a margin-softmax over tens of thousands of such classes. The sampler disappears; training becomes a classification run; the deployed artifact is still a descriptor compared by cosine. One extra wrinkle is worth noting because it has no analogue in faces: adjacent cells are genuinely similar, so pushing them apart with a hard margin is fighting the data. The fix is to partition the cells into groups whose members are far apart, and train on one group at a time, so no batch ever contains two neighbouring cells. That is a domain-specific repair to a domain-agnostic recipe, and it is the shape most good ports take.

QuestionFacesPlaces
What is a class?A person — given by the labelsA cell of the map — invented by you, and the cell size is a hyperparameter
Are two classes ever legitimately similar?Rarely, and when they are (twins) it is a known hard caseConstantly — adjacent cells overlap by construction
What breaks a naive port?The margin fights genuine similarity between neighbouring classes
The repairGroup the classes so a batch never contains neighbours

The skeleton, once, for any domain

python — the whole recipe, domain-agnostic# 1. An encoder that turns your thing into a vector. Faces: a ResNet on a
#    112x112 crop. Speech: a TDNN with attentive pooling. Places: a CNN with
#    a VLAD or GeM aggregation. Text: a transformer with a pooled token.
emb = F.normalize(encoder(x), dim=1)              # (B, d) on the unit sphere

# 2. A margin head over whatever you decided a class is.
loss = arcface_head(emb, class_ids)                # scalar

# 3. Deployment: delete the head. Store vectors. Compare with a dot product.
gallery = F.normalize(encoder(enrol_batch), dim=1)   # (N, d) — the database
scores  = F.normalize(encoder(probe), dim=1) @ gallery.T
# scores >= threshold  ->  verification
# scores.argmax()      ->  identification
# graph over scores    ->  clustering

# 4. Evaluate with TAR@FAR on identities the encoder has never seen,
#    reported per subgroup at a SHARED threshold, with the pair counts.

Four steps, and only step 1 is domain knowledge. That is what it means to say the canon transferred: everything below the encoder became a solved, shared substrate, and the research moved to the front end and to the data.

What actually changes when you port this

What you must re-derive per domainWhy it does not transfer
The scale sIt depends on your class count C through the floor formula. Copying s = 64 to a 500-class problem wastes headroom; copying it to a 10M-class problem may not be enough
The margin mIt depends on how much intra-class variance your data genuinely has. Utterances vary more than aligned face crops; product photos vary more than either
The definition of a positive pairThis is where domain knowledge lives, and it is the single most common source of silent failure. Two crops from the same video frame are not a positive pair in any useful sense — they teach the model to recognise JPEG artifacts
The target FARA phone unlock, a border gate and a photo-app clustering step operate at FARs that differ by four orders of magnitude, so they need different thresholds and different benchmarks
The front-end normalisationFace pipelines align by landmarks before embedding; speaker pipelines mean-normalise features; VPR must handle illumination and season. The embedding cannot fix a front end that has thrown information away
The one thing that always transfers. The evaluation discipline of Chapter 6. Whatever the domain, report TAR at a stated FAR with the number of impostor comparisons that supports it, and report it per subgroup at a shared threshold. Every other choice in this lesson is domain-specific; that one is not.
Speaker-verification systems typically use s = 30, m = 0.2 where face systems use s = 64, m = 0.5. Which explanation is right?
In the alignment/uniformity framing, what is a collapsed model?

Chapter 8: The Honest Chapter

Every other technology in this lesson's family — speaker embeddings, place recognition, product retrieval — is a tool. Face recognition is a tool that is also a political object, and a lesson that teaches you to build one without teaching you when not to has not finished the job. This chapter is not a disclaimer bolted on the end. It is the part where the arithmetic of Chapters 0 and 6 gets pointed at people.

Start from the properties we deliberately engineered

Go back through the lesson and list what we worked to achieve. Then read the list again as a threat model, because it is the same list.

The engineering achievementRead as a capability
Open-set: works on people never seen in trainingNo consent step exists anywhere in the pipeline. Being unknown to the system is not protection
128 to 512 numbers per faceA country's population fits in a few hundred gigabytes — Chapter 0's arithmetic. Storage is not a constraint on anyone
Cosine similarity, precomputable indexSearching a billion faces is a matrix multiply. Cost is not a constraint either
No retraining to enrolA gallery can be extended by anyone with photographs, silently, after the fact
Robust to pose, lighting, ageRobust to the subject not cooperating, not knowing, and not being recognisable to a human
One global thresholdOne policy decision, made by an engineer, applied to everyone — including groups whose score distributions differ
This is what dual use means concretely. There is no version of the technology that unlocks your phone but cannot be pointed at a crowd. The properties are the same properties. The difference between the two deployments is entirely in the governance around the model — where the gallery lives, who can add to it, whether the subject can decline, and what happens downstream of a match. Which means the governance is not somebody else's department. It is the deployment.

The bias findings, stated precisely

"Face recognition is biased" is true and too vague to act on. The measured findings are specific, and the specificity is what makes them useful.

NIST FRVT Part 3: Demographic Effects (NISTIR 8280, December 2019) evaluated 189 algorithms from 99 developers on large operational datasets. Its findings, in the report's own framing:

Gender Shades (Buolamwini and Gebru, 2018) audited commercial gender classification from face images and found error rates ranging from 0.8% on lighter-skinned men to 34.7% on darker-skinned women. Be precise about this one: gender classification is a different task from identity verification, and quoting it as a face-recognition error rate is a common overreach. It belongs here because it demonstrated the same root cause — benchmark and training sets whose demographic composition nobody had audited — and because it is what made the industry start measuring.

Why the loss functions in this lesson cannot fix it

Chapter 6 already did this derivation, and it is worth restating as the mechanism rather than an accusation.

The margin loss enforces its constraint per class, identically for all classes. It is scrupulously fair in that narrow sense. But the resulting embedding's score distributions need not be identical across groups: if a group is under-represented, or if the face detector and landmark aligner upstream were tuned on a different distribution, that group's impostor scores can sit slightly higher. And Chapter 6 computed what "slightly" does:

a one-σ shift in mean impostor score  →  Φ(−4) / Φ(−5) = 3.17×10−5 / 2.87×10−7 = 110× the false match rate

at a shared threshold. The exponential tail is an amplifier: distribution differences too small to see become error-rate differences of two orders of magnitude. Three consequences follow, and all three are actionable:

1. Aggregate metrics hide it by construction
A single TAR-at-FAR figure is an average over the population of the test set. If a group is 5% of that set, a 100× error rate on them moves the aggregate by almost nothing. The metric cannot see the failure.
2. You must report per-group FMR at a shared threshold
Not per-group thresholds — that hides the differential by absorbing it, and it is also usually illegal and always indefensible. Shared threshold, separate FMR, with the pair count that supports each estimate.
3. Measuring it needs enormous test sets
Chapter 6's Poisson argument, per group. To measure a group's FMR at 10−6 to within ±10% you need 108 impostor pairs from that group. Most audits cannot support the claim they make, in either direction.

The failure chain is not the model

A 1:N search returns a ranked list of similarities. Follow what happens to that list in a real process.

StepWhat the artifact isWhat is lost
1. SearchA ranked list with scores — a probabilistic statementNothing yet
2. Analyst review"Investigative lead", top candidateThe score, the rank, the gallery size, the FMR at that threshold
3. Photo lineupA witness is shown the candidateThat the candidate was selected because they look similar — which is exactly the condition under which eyewitness identification is least reliable
4. ArrestA categorical claim about a personEverything. There is no number left anywhere in the artifact

At each step a probabilistic ranking is laundered into a categorical assertion, and by step 4 nothing in the paperwork records that the original object had a confidence attached to it. There are publicly documented wrongful arrests in the United States following face-recognition leads — among them Robert Williams and Porcha Woodruff in Detroit and Nijeer Parks in New Jersey — all of Black Americans, in cases where the technical match was one input among several failures.

The engineering lesson inside the civil-liberties story. Calibrating your model does not fix step 2. If the score does not travel with the result, and if the receiving process has no rule for what a given score licenses, then improving the model changes nothing about the outcome distribution — it just moves errors around. If you are building the search, the score, the gallery size, the operating FMR and the expected number of false candidates per search must be attached to the output as a required field, not an optional one. That is a system design decision, and it is yours to make.

The datasets, and what happened to them

The models in this lesson were trained on data that in several cases no longer exists, for reasons worth knowing.

DatasetWhat it wasStatus
MS-Celeb-1M~10M images of ~100K "celebrities", scraped from the web; the basis for MS1MV2, which ArcFace trains onWithdrawn by Microsoft in 2019 after reporting that many subjects were not public figures and none had consented
MegaFace~4.7M photos of ~672K identities from Flickr Creative Commons uploads — the standard million-scale distractor setRetired by the University of Washington in 2020; Creative Commons licences permit reuse but the subjects were not the uploaders
DukeMTMCSurveillance video of students on a university campus, used widely for person re-identificationWithdrawn in 2019 over consent and surveillance concerns
LFW13,233 images of 5,749 people from news photographsStill standard, still used — and, per Chapter 6, no longer able to distinguish modern systems

Note the asymmetry that matters: the datasets were retracted; the models trained on them were not. Weights trained on MS-Celeb-1M are still downloaded thousands of times a week. "We removed the dataset" is not a remedy when the artifact that encodes it has already been distributed.

The template is not anonymous, and it cannot be reissued

A common defence of storing face embeddings is that they are "just numbers" — irreversible, unlike a photograph. Two facts make that defence weaker than it sounds, and both are technical rather than political.

Embeddings can be inverted. Given a target vector and access to a face model, you can optimise an image (or train a decoder) so that its embedding matches. The reconstructions are not photographs of the person, but they are recognisably that person — enough for a human or another matcher. This is exactly the same attack surface as text-embedding inversion, and our embedding security lesson works through the general case. The practical consequence: a leaked template database should be treated as a leaked photo database, not as a leaked hash.

A face is not a password. Chapter 0's convenience — no enrolment cost, works on strangers, stable across years — is the same property that makes a breach permanent. A leaked password is rotated in seconds. A leaked template cannot be, because the credential is your face, you cannot change it, and it is the same credential at every other system that ever enrols you. Biometrics have no revocation story, and every deployment inherits that.

MitigationWhat it actually buysWhat it does not
Hash the templateNothing — matching needs distances, and a cryptographic hash destroys themAny of it. This does not work and is worth knowing so you can say so in a design review
Cancelable biometrics: a per-user random projection or permutation applied before storageDifferent systems store different templates, so a leak from one does not match another; the key can be rotatedProtection if the key leaks with the template, which is the usual case in a breach
Secure enclave, never leaves the deviceRemoves the central database entirely — the strongest mitigation by a wide marginAny use case that genuinely needs a server-side gallery
Homomorphic or secure two-party matchingReal cryptographic protection for the comparison itselfCheap latency — and it does not help if the gallery holder is the adversary

Where the line is drawn in law

Briefly, and with the caveat that this moves fast — check the current text before relying on any of it.

What good practice looks like when the use is legitimate

Phone unlock is the honest counter-example, and it is instructive precisely because it is built to eliminate almost every property in the threat-model table.

Design decisionWhat it removes
Enrolment and matching happen entirely on-device, in a secure enclaveNo central gallery exists, so there is nothing to search, subpoena or breach
The template never leaves the device and is not synced or backed upCross-device linkage becomes impossible
The subject is the owner, and enrolment is an explicit actConsent is structural, not a checkbox
A passcode fallback always existsFailure is an inconvenience rather than a lockout — and the FNMR budget can be spent on a stricter threshold
The published false-match figure (about 1 in 1,000,000 for a random person) comes with stated caveats for twins, siblings and children under 13The number is falsifiable and its limits are named — the opposite of "99.9% accurate"
Attention detection: the eyes must be open and directed at the deviceUnlocking a sleeping or unwilling person

None of that is about the loss function. All of it is architecture and policy, decided before any model was trained.

A checklist for the decision you may actually face

You will more likely be asked to build one of these than to decide whether one should exist. Here is the shortest honest list of conditions under which the answer should be no.

Do not build it if …
The subject cannot decline — if being recognised is a condition of walking down a street, entering a building they must enter, or receiving a benefit they are entitled to.
Do not build it if …
The consequence of a false match is adjudicated by someone who will never see the score, the threshold or the gallery size. Chapter 6's arithmetic must survive to the point of decision, or it was decoration.
Do not build it if …
You cannot measure per-group FMR at your operating threshold with enough pairs for statistical power. If you cannot measure the harm, you cannot claim its absence.
Do not build it if …
The gallery can grow without the knowledge of the people in it. A system whose enrolment step is "somebody uploaded a photo" has no consent model at all.
And ask first …
Would a badge, a PIN, a QR code or a name typed by the person do the job? Biometrics are irrevocable. A leaked password is reissued in a minute; a leaked face template is permanent, and it is the same face that unlocks every other system.
The dual-use point, made without flinching in either direction. The same embedding organises a bereaved family's photo library, lets a blind user know who just walked into the room, reunites people separated in a disaster, and unlocks a billion phones without a password. Those are real goods and pretending otherwise is its own kind of dishonesty. The argument of this chapter is not that the technology is illegitimate; it is that the properties that make it good at the benign uses are exactly the properties that make the malign uses cheap, so the only lever that distinguishes them is the one outside the model — and engineers are the people best placed to see when that lever is missing.
NIST found that algorithms developed in China did not show the elevated false-match rate on East Asian faces that many US-developed algorithms did. Why is that specific comparison so important?
Why can a system report an excellent aggregate TAR at FAR = 10−5 while producing a 100× higher false-match rate for one demographic group?

Chapter 9: The Canon, and a Cheat Sheet

Ten years, one idea, and a long argument about where to put a constant. Here is the whole lineage in one table, then the numbers, then what to read next, then a recipe you can run this weekend.

The lineage

YearMethodThe one-line contributionWhat it left broken
2014DeepFace, DeepIDDeep CNN + softmax over identities; take a bottleneck layer as the representationThe representation was a by-product; thousands of dimensions
2014DeepID2Add a verification (contrastive) term to the identification lossTwo losses to balance; pair sampling
2015FaceNetOptimise the deployed distance directly with a triplet hinge on 128-d L2-normalised embeddings; semi-hard mining inside 1,800-exemplar batchesO(N3) objective; sampler-dependent results; collapse is a stationary point
2016Center lossAdd a pull toward a learned per-class centre alongside softmax — intra-class compactness without tripletsAnother loss weight to tune; no inter-class term
2016L-SoftmaxFirst multiplicative angular margin inside softmaxHard to train; needs annealing
2017SphereFace (A-Softmax)Normalise weights, margin as cos(m θ)Non-monotone; needs λ annealing and a piecewise surrogate
2017NormFaceNormalise both features and weights; introduce the scale sSeparable but not compact — no margin
2017ProxyNCAReplace sampled negatives with learned class proxies; ~3× faster convergenceNamed the trick that softmax had been doing all along
2018CosFace / AM-SoftmaxAdditive cosine margin: s(cos θ − m), m = 0.35The angular demand varies with θ, weakest at the boundary
2018/19ArcFaceAdditive angular margin: s cos(θ + m), m = 0.5 — a constant geodesic gapNon-monotone past π − m; a 42-nat loss spike at initialisation
2020–21Partial FC, Sub-center ArcFaceSample a fraction of class centres per step (10M identities on one machine); multiple sub-centres per class to absorb label noise
2021–22MagFace, AdaFaceMake the margin a function of feature norm / image quality, so low-quality samples are not forced into the class centre
2021–CLIP, CLAP and friendsSame loss family, but the proxies are produced by a text encoder instead of a lookup table — so the class list becomes a string you type at inference
The last row is the punchline of the whole canon. Chapter 3 showed that the classifier's weight matrix is a table of learned proxies, one row per class. CLIP and CLAP replace that table with a function — a text encoder that generates a proxy for any class name you can write. Metric learning removed the need to retrain for a new person; language-supervised contrastive learning removed the need to retrain for a new category. It is the same move, applied one level up. Our CLAP veanor derives that version in the audio domain, and its similarity matrix is the same object as the (B, C) logit matrix you built in Chapter 3.

The same system, two implementations

If you have to choose one today, choose the right-hand column. But the comparison is more useful than the verdict, because it shows exactly which parts of a system a loss function decides.

ComponentFaceNet, 2015ArcFace, 2019
Batch construction~1,800 exemplars, ~40 per identity, identity-structuredWhatever fits — shuffle like any classifier
Extra forward passesNone, but the batch must be huge to mine withinNone
Sampler codeA few hundred lines: distance matrix, masks, semi-hard selection, fallbacksZero lines
Loss code4 lines~10 lines, of which 3 are the monotonicity guard
Extra parametersNoneA C×d proxy table — 176 MB at 85,742 × 512, deleted at deployment
Hyperparametersα, plus the entire sampling policys and m, both with derivable starting points
Failure mode to fearCollapse — silent, and a stationary pointDivergence at initialisation — loud, and fixed by a warm-up
ReproducibilityPoor — results depend on batch composition and sampler details often omitted from papersGood — the loss is a pure function of the batch
Still the right choice when…C is small (thousands), labels are noisy, or the class table would not fitC is large and the labels are reasonably clean — the usual case

Note what did not change: the embedding, L2 normalisation, cosine similarity at inference, the threshold, and every line of the evaluation code. Ten years of progress happened entirely inside the training objective, behind an interface that FaceNet fixed in 2015 and nobody has had a reason to move since.

The cheat sheet

NumberWhat it is
α = 0.2FaceNet's triplet margin, in squared-distance units — equivalently a cosine margin of 0.1, or 25.84° of clearance around a perfect positive
128FaceNet's embedding dimension; 512 bytes as float32, 128 bytes quantised
~1,800 / ~40FaceNet's minibatch size and faces per identity — 45 identities, 70,200 anchor-positive pairs, 123.5M candidate triplets per batch
d2 = 2 − 2cos θThe identity that makes squared distance and cosine interchangeable on the unit sphere
s = 64The scale in both CosFace and ArcFace. Floor for C = 85,742 at PW = 0.9 is s ≥ 13.56; 64 is headroom
m = 0.35CosFace's additive cosine margin — a constant in cosine, varying from 33.9° to 20.2° in angle
m = 0.5 rad = 28.65°ArcFace's additive angular margin — a constant geodesic gap; cosine equivalent runs 0.122 to 0.495
151.35°π − m, past which cos(θ + m) stops decreasing and the guard clause takes over with cos θ − 0.2397
42.04ArcFace's loss at initialisation with C = 85,742 — 30.68 from the margin plus ln(85,742) = 11.36. Warm up or ramp
10.36The loss floor of an unscaled cosine softmax at C = 85,742 — a perfect model, one nat better than random
99.63% / 99.33% / 99.83%LFW accuracy for FaceNet / CosFace / ArcFace — on three different training sets, and separated by a handful of pairs out of 6,000
1 − e−1 = 63.2%Chance of at least one false match searching a 1M gallery at FMR = 10−6
110×FMR ratio produced by a one-σ shift in a group's impostor scores at a shared threshold
100 / FARImpostor pairs needed to measure a false-accept rate to within ±10%

Where to go from here

If you want…Go to
The general mechanics of contrastive objectivesContrastive learning and CLIP
The formal account of the two forces in every loss hereAlignment and uniformity on the hypersphere
The same recipe in audio, with text as the proxy generatorCLAP
Open-set retrieval over places rather than peopleNetVLAD, VPR aggregation, foundation models for VPR
How to choose a similarity in the first placeSimilarity metrics and metric design
How to actually serve a billion embeddingsANN indexes and vector embeddings
What can go wrong with an embedding you exposeEmbedding security — including inversion attacks, which apply directly to face templates
How to evaluate any of this honestlyThe metrics ladder

Build it yourself — the weekend recipe

StepWhat to doThe decision that matters
1. DataAny open-set dataset with identity labels — CASIA-WebFace for faces, VoxCeleb for speakers, Stanford Online Products for retrievalSplit by identity, never by image. A shared identity across the split makes the whole evaluation meaningless
2. BackboneAny encoder, ending in a linear layer to d = 512The backbone is the least interesting choice in this lesson. Do not spend the weekend here
3. HeadThe ArcFace module from Chapter 4, including the th/mm guardb = 0, normalise both sides. Skip either and you are back to Chapter 3's confounds
4. Scales from the floor formula, capped at 64Compute it — do not copy 64 onto a 300-class problem
5. Warm-upm = 0 for one epoch, then ramp to target over ~2,000 stepsDefuses the 42-nat initialisation spike
6. Sanity checkConfirm the loss drops well below ln C in the first epochIf it sits at ln C, your scale is below the floor. This is the single most common bug
7. DiagnosticsLog mean and 5th-percentile cos θy, and the fraction of samples with cos θy < 0The tail tells you whether a subpopulation is stranded — the mean never will
8. EvaluateThrow away the head. Embed, L2-normalise, cosine, then TAR at your target FAR from Chapter 6's functionReport k, the number of false accepts your threshold admitted. It is your error bar
9. CompareRerun with m = 0. The margin should cost you training loss and buy you TAR at low FARIf it does not, your m is wrong for your data — not the method
10. AuditSplit your test pairs by whatever groups you can identify and report FMR per group at the shared thresholdWith the pair counts. Chapter 8 is a step in the recipe, not an appendix

The bugs that eat a weekend

Collected from the chapters, in the order you are likely to hit them.

SymptomCauseChapter
Loss pinned at ln C foreverScale below the floor, or a missing F.normalize on either side3
Loss sits at exactly α and the embedding is constantTriplet collapse — a stationary point, not a plateau2
Loss goes to NaN in the first few hundred stepsFull margin at initialisation, amplified by s5
Loss is fine, verification is notSelecting on the wrong metric — the margin raises training loss by design5, 6
Training loss keeps falling and the active-triplet fraction is near zeroThe sampler has run out of signal; the model is no longer learning2
Out of memory building the mining maskA B3 boolean tensor — 5.8 GB at B = 18002
Accuracy far better on the val split than in productionSplit by image rather than by identity, so the same person is on both sides9 (recipe step 1)
A stubborn tail of samples with cos θy < 0The missing monotonicity guard, or a corrupt identity4, 5
The reported TAR moves by 5 points between runsToo few impostor pairs at your target FAR — you are reading Poisson noise6
Works beautifully on one demographic groupYou never measured the others separately8

References worth reading next

  1. Schroff, F., Kalenichenko, D., Philbin, J. "FaceNet: A Unified Embedding for Face Recognition and Clustering," CVPR 2015 — arXiv:1503.03832. Chapters 1 and 2.
  2. Deng, J., Guo, J., Xue, N., Zafeiriou, S. "ArcFace: Additive Angular Margin Loss for Deep Face Recognition," CVPR 2019 — arXiv:1801.07698. Chapters 4 and 5.
  3. Wang, H., Wang, Y., Zhou, Z., Ji, X., Gong, D., Zhou, J., Li, Z., Liu, W. "CosFace: Large Margin Cosine Loss for Deep Face Recognition," CVPR 2018 — arXiv:1801.09414. The additive cosine margin and the bound on s.
  4. Liu, W., Wen, Y., Yu, Z., Li, M., Raj, B., Song, L. "SphereFace: Deep Hypersphere Embedding for Face Recognition," CVPR 2017 — arXiv:1704.08063. The multiplicative ancestor, and why annealing was needed.
  5. Wang, F., Xiang, X., Cheng, J., Yuille, A. "NormFace: L2 Hypersphere Embedding for Face Verification," ACM MM 2017 — arXiv:1704.06369. Chapter 3's normalisation and the scale.
  6. Movshovitz-Attias, Y., Toshev, A., Leung, T., Ioffe, S., Singh, S. "No Fuss Distance Metric Learning using Proxies," ICCV 2017 — arXiv:1703.07464. The proxy idea that makes Chapter 3 inevitable.
  7. Hermans, A., Beyer, L., Leibe, B. "In Defense of the Triplet Loss for Person Re-Identification," 2017 — arXiv:1703.07737. Batch-hard mining and the soft margin.
  8. Wu, C.-Y., Manmatha, R., Smola, A., Krähenbühl, P. "Sampling Matters in Deep Embedding Learning," ICCV 2017 — arXiv:1706.07567. The most careful treatment of Chapter 2's problem.
  9. Wang, T., Isola, P. "Understanding Contrastive Representation Learning through Alignment and Uniformity on the Hypersphere," ICML 2020 — arXiv:2005.10242. Chapter 7's unifying frame.
  10. Meng, Q. et al. "MagFace: A Universal Representation for Face Recognition and Quality Assessment," CVPR 2021 — arXiv:2103.06627; Kim, M., Jain, A., Liu, X. "AdaFace: Quality Adaptive Margin," CVPR 2022 — arXiv:2204.00964. Feature norm, used on purpose.
  11. An, X. et al. "Partial FC: Training 10 Million Identities on a Single Machine," 2020 — arXiv:2010.05222. What to do when the C×d proxy table stops fitting.
  12. Grother, P., Ngan, M., Hanaoka, K. "Face Recognition Vendor Test Part 3: Demographic Effects," NISTIR 8280, NIST, 2019. Chapter 8's numbers, at their source.
  13. Buolamwini, J., Gebru, T. "Gender Shades: Intersectional Accuracy Disparities in Commercial Gender Classification," FAT* 2018. The audit that started the measurement.
  14. Maze, B. et al. "IARPA Janus Benchmark-C: Face Dataset and Protocol," ICB 2018. The benchmark that can resolve what LFW cannot.
Cross-domain bridge
The classifier head was a lookup table of proxies, and every retrieval system is one too
A vector database stores one embedding per document and answers a query by embedding it once and ranking by cosine. A margin-softmax head stores one proxy per class and answers a training example by ranking it against all of them. They are the same data structure, read in opposite directions. Which is why the ArcFace head can simply be deleted at deployment: the gallery replaces it, and enrolment is an INSERT rather than a gradient step. Once you see it this way, the “how do I add a class without retraining” question answers itself in every domain — see vector embeddings and ANN indexes for the serving half.
"What I cannot create, I do not understand."
A ResNet, a 512-d linear layer, twenty lines of ArcFace head and any dataset with identity labels. Train it, delete the head, and watch it recognise people it never saw. The 99.83% stops being a number you read.
Exit gate — teach it back before you leave.

Without scrolling up: (1) derive the triplet loss from the two failed attempts, and say what α = 0.2 means in cosine units; (2) explain why random triplet sampling stops working and why hardest-negative mining collapses — including why collapse is a stationary point; (3) explain why normalising features and weights forces you to introduce s, with the loss-floor arithmetic; (4) hand-compute the loss of a sample at θy = 60° with a competitor at 75° under plain softmax, CosFace and ArcFace at s = 64; (5) state why a constant angular margin is a geodesic margin and a constant cosine margin is not; (6) compute TAR at FAR = 0.1 on the twenty toy scores, and say why LFW cannot separate ArcFace from CosFace; (7) explain how a one-σ shift in a group's impostor scores becomes a 110× FMR differential. If any of the seven stalls, its chapter is one tap away.

Which single sentence best captures what changed between FaceNet and ArcFace?
You add ArcFace's margin to your existing classifier and the training loss goes up and stays up. What should you conclude?