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.
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.
"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.
| Task | The question | What you compare | What you tune | Real example |
|---|---|---|---|---|
| Verification (1:1) | "Are these two the same person?" | One probe against one claimed template | A threshold on similarity | Phone unlock, passport gate |
| Identification (1:N) | "Who is this?" | One probe against a gallery of N templates | A ranking, plus optionally a threshold to reject | Photo tagging, watchlist search |
| Clustering | "How many people are in this pile, and which photos go together?" | Every template against every other | A 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.
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:
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 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.
| Representation | Bytes per face | 1 million faces | 1 billion faces | Dot products per 1:N query over 1B |
|---|---|---|---|---|
| 4096-d float32 bottleneck | 16,384 | 16.4 GB | 16.4 TB | 4.1 × 1012 multiply-adds |
| 128-d float32 | 512 | 512 MB | 512 GB | 1.28 × 1011 multiply-adds |
| 128-d int8 | 128 | 128 MB | 128 GB | 1.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.
"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.
| Stage | In → out | What it does | Why it is there |
|---|---|---|---|
| 1. Detect | H×W×3 photo → a box plus 5 landmarks | Find each face and locate the eye centres, nose tip and mouth corners | Everything downstream assumes one face filling the frame |
| 2. Align | box + landmarks → 112×112×3 | Fit a similarity transform (rotation, scale, translation) mapping the 5 landmarks onto a canonical template, then warp | A 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 pixels | 112×112×3 uint8 → float in [−1, 1] | (x − 127.5) / 128 | Standard conditioning; also makes the first layer's gradients well scaled |
| 4. Backbone | 112×112×3 → 7×7×512 | A ResNet-100 or similar, stride 16 overall | The representation learning. This is 99% of the FLOPs |
| 5. Output layer | 7×7×512 → 512 | ArcFace's chosen structure: BatchNorm → Dropout → fully connected → BatchNorm | The 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 normalise | 512 → 512, norm 1 | x / ‖x‖2 | This is the deployed artifact. Chapter 3 is entirely about why this line matters more than it looks |
| 7. (training only) Head | 512 → C logits | Multiply by W ∈ RC×512, apply a margin, cross-entropy | Deleted before deployment. It exists only to shape stage 6 |
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.
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.
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.
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.
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 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.
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:
| User | Their threshold | Impostor score against them | Decision |
|---|---|---|---|
| Ada | 0.72 | 0.68 | Reject — correct |
| Bo | 0.55 | 0.68 | Accept — the same impostor, the same face, the same score |
| Cyd | 0.80 | 0.68 | Reject — 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.
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.
This lesson is built on three papers that are usually taught as competitors. They are better read as three moves in one argument.
| Paper | Year | The move | The cost it introduced |
|---|---|---|---|
| FaceNet (Schroff, Kalenichenko, Philbin) | 2015 | Train 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) | 2018 | Put 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/2019 | Put 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.
| Era | What the loss optimised | What it unlocked | What stayed broken |
|---|---|---|---|
| Hand-crafted features (to ~2013) | Nothing — features were designed, then a classifier or a metric was fit on top | Eigenfaces, Fisherfaces, LBP; the first working gates | Everything hard: pose, lighting, age |
| Deep classification (2014) | Softmax over a few thousand identities; the representation is a bottleneck layer you keep by hand | DeepFace crosses 97% on LFW; deep learning arrives in faces | The 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 time | Two 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 model | Triplet 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 triplets | Two 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 inputs | The 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.
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.
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:
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.
Add a third image: a negative n, a photo of a different person. Now ask for the positive to be closer than the negative:
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.
Both problems have the same fix in two parts. First, constrain the embedding to the unit hypersphere:
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,
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.
That is the triplet loss, and it is FaceNet's entire objective. In the paper's own notation the constraint being enforced is
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.
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.
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):
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.
| Situation | Contrastive loss (m2 = 0.5) | Triplet loss (α = 0.2) |
|---|---|---|
| A's pair at d2 = 0.05 | Loss 0.05 — still pulling, forever | Zero, provided negatives are past 0.25 |
| B's pair at d2 = 0.30 | Loss 0.30 — six times A's, so B dominates the gradient purely for being varied | Zero, provided B's negatives are past 0.50 |
| A negative at d2 = 0.40 | Violates the absolute margin, gets pushed | Satisfied — it is 0.35 past A's positives |
| B negative at d2 = 0.40 | Also violates — same treatment | Violates — 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.
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):
Anchor-positive distance first, straight from the definition:
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:
| Negative | a·n | dan2 = 2 − 2 cos | θan | Loss = max(0, 0.08 − dan2 + 0.2) | Verdict |
|---|---|---|---|---|---|
| n1 = (0.96, 0.28) | 0.576 + 0.224 = 0.800 | 0.400 | 36.87° | max(0, −0.120) = 0 | Easy — already satisfied, zero gradient |
| n2 = (0.28, 0.96) | 0.168 + 0.768 = 0.936 | 0.128 | 20.61° | max(0, 0.152) = 0.152 | Semi-hard — farther than p, but inside the margin |
| n3 = (0.50, 0.866) | 0.300 + 0.693 = 0.993 | 0.014 | 6.87° | max(0, 0.266) = 0.266 | Hard — 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.
When the hinge is open (loss > 0), the max disappears and the loss is a plain quadratic. Differentiate each of the three terms:
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:
| Vector | Gradient | Update x − η∇ | Renormalised to the sphere | Angle |
|---|---|---|---|---|
| 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°:
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.
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.
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 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.
.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.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.
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:
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.
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
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 strategy | Triplets that produce gradient | Compute wasted | What actually happens to the model |
|---|---|---|---|
| Uniform random, early training | High — the model is bad, so everything violates | Little | Learns 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 dataset | 100% | None — but see the next section | Collapse, usually within a few hundred steps |
| Semi-hard within the batch | Moderate and self-sustaining | Some | Trains stably to convergence — this is FaceNet's recipe |
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:
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.
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:
FaceNet's rule is: within the current minibatch, for each anchor-positive pair, pick a negative satisfying
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.
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.
| Quantity | Arithmetic | Value |
|---|---|---|
| Identities per batch | 1800 ÷ 40 | 45 |
| Forward passes needed | one per exemplar | 1,800 |
| Anchor-positive pairs available | 45 × 40 × 39 | 70,200 |
| Negative candidates per anchor | 1800 − 40 | 1,760 |
| Candidate triplets inside one batch | 70,200 × 1,760 | 123,552,000 |
| Pairwise distance matrix to compute | 1800 × 1800 × 128 multiply-adds | 4.1 × 108 — one small matmul |
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 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:
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.
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:
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.
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.
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.
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.
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:
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.
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.
| Loss | Comparisons per sample, per step | Who supplies the negatives | Bookkeeping |
|---|---|---|---|
| Contrastive (pairs) | 1 | A sampler | Pair lists, class-balanced sampling |
| Triplet | 1 (or a few) | A sampler, plus mining inside a big batch | Identity-structured batches, B×B distance matrix, masks |
| N-pair (Sohn, 2016) | N − 1, the rest of the batch | The batch itself | One example per class per batch |
| Proxy-based (ProxyNCA) | C − 1, every class | Learned proxies — no sampler at all | None. 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.
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
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 logit for class j is
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:
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.
Set b = 0, and rescale so that ‖Wj‖ = 1 and ‖x‖ = 1. The logit reduces to a single quantity:
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.
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
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:
Rerun the same best case with s = 64. The correct logit is 64, the others are 0, and
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.
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.
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:
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 table | The text encoder's output for caption j | A proxy for the class — stored versus generated |
| cos θj = Wj · x | cos(img, txtj) | The similarity being ranked |
| Scale s = 64 | Inverse temperature 1/τ; CLIP's τ = 0.01 gives 100 | The same scalar, differently named. Both exist because cosines live in [−1, 1] |
| Sum over C classes | Sum over N in-batch captions | The negatives; the batch is a sampled subset of the class table |
| Margin m on the target | Usually none | The one genuine difference — and margin variants of InfoNCE exist for exactly this reason |
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.
| Triplet (FaceNet) | Normalised softmax (NormFace) | |
|---|---|---|
| Negatives per step | One per triplet, chosen by a sampler | All C−1 classes, automatically |
| Bookkeeping | Identity-structured batches, distance matrix, mask logic | None — shuffle the data like any classifier |
| Cost per step | O(B2) mining on top of the encoder | One (B×d)(d×C) matmul |
| Collapse risk | Real, and it is a stationary point | None — the classes must remain distinguishable or the loss is huge |
| Memory cost | Big batches | A C×d table — 85,742 × 512 floats = 176 MB, which becomes the bottleneck at 10M identities |
| What it optimises | Exactly the deployed distance | Separability 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.
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.
For sample x with true class y and nearest competitor j, plain normalised softmax is happy when
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 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.
| Method | Target logit | Decision boundary vs class j | Margin type | Paper values |
|---|---|---|---|---|
| Normalised softmax | s cos θy | cos θy = cos θj | None | s = 64 |
| SphereFace (A-Softmax, 2017) | s cos(m θy) | cos(m θy) = cos θj | Multiplicative angular | integer m = 4 |
| CosFace / LMCL (2018) | s(cos θy − m) | cos θy − m = cos θj | Additive cosine | m = 0.35, s = 64 |
| ArcFace (2018) | s cos(θy + m) | cos(θy + m) = cos θj | Additive angular | m = 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".
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:
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:
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.
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:
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
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:
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.
This is the arithmetic to carry with you. A single training sample, three classes, s = 64.
| Class | Angle θj | cos θj | Role |
|---|---|---|---|
| y (the true identity) | 60° | 0.500000 | Correct, and comfortably ahead |
| j (the nearest impostor) | 75° | 0.258819 | The runner-up — the one that matters |
| k (an unrelated identity) | 110° | −0.342020 | Irrelevant, 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
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.
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.
| Loss | Target logit | py | Loss value | Gradient available? |
|---|---|---|---|---|
| Normalised softmax | 32.000 | 0.9999998 | 1.98 × 10−7 | None — sample is "done" |
| CosFace (m = 0.35) | 9.600 | 9.44 × 10−4 | 6.965 | Large |
| ArcFace (m = 0.5) | 1.508 | 2.89 × 10−7 | 15.056 | Very large |
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.
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.
One sample is an anecdote. Sweep θy with the impostor fixed at 75° and s = 64, and the character of each loss shows up.
| θy | Softmax loss | CosFace loss (m = 0.35) | ArcFace loss (m = 0.5) | What the sample is |
|---|---|---|---|---|
| 10° | 6.6 × 10−21 | 3.5 × 10−11 | 3.0 × 10−15 | Textbook. All three agree it is finished |
| 40° | 8.0 × 10−15 | 4.3 × 10−5 | 1.2 × 10−3 | Comfortable. Only the margins are still talking |
| 60° | 2.0 × 10−7 | 6.96 | 15.06 | The worked example. Softmax has quit; margins are shouting |
| 74° | 0.29 | 21.3 | 30.6 | One degree from being misclassified |
| 80° | 5.46 | 27.9 | 37.0 | Actually 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.
ArcFace's paper ends this argument neatly by writing a combined margin with three knobs:
| Setting | Recovers |
|---|---|
| m1 = 1, m2 = 0, m3 = 0 | Normalised softmax (NormFace) |
| m1 = 4, m2 = 0, m3 = 0 | SphereFace |
| m1 = 1, m2 = 0, m3 = 0.35 | CosFace |
| m1 = 1, m2 = 0.5, m3 = 0 | ArcFace |
| m1 = 1.0, m2 = 0.3, m3 = 0.2 | A 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%.
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.
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.th / mm guard clause?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.
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 push | ArcFace angular push |
|---|---|---|---|---|---|
| 20° (well inside its class) | 0.9397 | 0.5897 | 53.86° | 33.86° | 28.65° |
| 40° | 0.7660 | 0.4160 | 65.41° | 25.41° | 28.65° |
| 60° (the worked example) | 0.5000 | 0.1500 | 81.37° | 21.37° | 28.65° |
| 85° (near the boundary) | 0.0872 | −0.2628 | 105.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 temptation is to conclude that ArcFace is strictly better. The numbers do not support anything that strong, and it is worth seeing why.
| Reported result | FaceNet (2015) | CosFace (2018) | ArcFace (2018/19) |
|---|---|---|---|
| LFW verification accuracy | 99.63% | 99.33% | 99.83% |
| Training data | 100–200M images, ~8M identities (private) | ~5M images, ~90K identities (private, cleaned) | MS1MV2: 5.8M images, 85K identities (public) |
| Backbone | Zeiler–Fergus (140M params) or Inception (7.5M) | 64-layer CNN | ResNet-100 |
| Loss | Triplet, α = 0.2, semi-hard mining | Additive cosine margin, s = 64, m = 0.35 | Additive angular margin, s = 64, m = 0.5 |
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.
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
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.
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 s | Best achievable loss (C = 85,742) | What training does |
|---|---|---|
| 1 | ln(85,741) − 1 ≈ 10.36 | Nothing. Perfect and random differ by one nat |
| 8 | ≈ 3.39 | Learns slowly, plateaus high, embeddings stay diffuse |
| 16 | ≈ 0.0096 | Works — e16 = 8.886×106 finally dwarfs the 85,741 competitors |
| 32 | ≈ 1.1 × 10−9 | Works well; common in speaker and re-ID systems |
| 64 | ≈ 1.4 × 10−23 | The face-recognition standard — plenty of headroom |
| 256 | 0 in float32 | Gradients 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.
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:
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.
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.
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:
It is positive, so descent decreases θy: the feature rotates toward its own class. Three factors multiply, and each has a clean meaning:
| Factor | Range | What it encodes |
|---|---|---|
| s | fixed, 30–64 | Global 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:
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.
| What you see | Almost always | The check | The fix |
|---|---|---|---|
| Loss flat at ln C from step 1 | Scale below the floor, or normalisation missing | Print the max logit. If it is under about 5, s is too small or you forgot to normalise | Raise s; verify both F.normalize calls |
| Loss flat at a number a little above 0 that never improves | You are at the floor — the model may be perfect | Compute ln(1 + (C−1)e−s) and compare | Raise s if you need more resolution; otherwise nothing is wrong |
| Loss explodes to 40+ in the first hundred steps, then NaN | Full margin at initialisation | Log the loss at step 0. If it is far above ln C, the margin is on | Warm up with m = 0, or ramp m, or use the easy-margin variant |
| Loss falls nicely, verification accuracy does not improve | You are tuning on the wrong quantity | Plot TAR at low FAR per epoch, not accuracy and not loss | Select on the verification metric — the margin makes training loss non-comparable across m |
| A stubborn subpopulation with cos θy < 0 | Samples past the monotonicity break, or a corrupt identity | Histogram cos θy; inspect the worst 50 images by eye | Confirm the guard clause is present; consider sub-centre ArcFace for noisy identities |
| Great on the training identities, poor on held-out ones | Margin too small, or C too small for the embedding dimension | Compare the mean intra-class angle with the mean nearest-class angle | Increase m; add identities before adding images |
| Step | What to do | Why |
|---|---|---|
| 1 | Compute the floor: s ≥ ln((C−1)PW/(1−PW)) with PW = 0.9, then take 3–4× that, capped at 64 | Guarantees the loss can reach zero with headroom for imperfect samples |
| 2 | Train one epoch with m = 0 and confirm the loss falls below ln C | If it sits at ln C, your normalisation or scale is wrong, and no margin will save you |
| 3 | Turn on the margin, ramping from 0 to the target over ~2,000 steps | Defuses the 42-nat initialisation spike computed above |
| 4 | Sweep 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 |
| 5 | Select on a hard verification benchmark at low FAR, never on training loss | The margin makes the training loss worse by design; the training loss cannot rank margins |
| 6 | Log the mean and the 5th percentile of cos θy every epoch | The 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.
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.
A verification system compares two templates and produces a similarity score. There are exactly two kinds of comparison and exactly two kinds of error.
| Term | Definition | Also called | The harm when it happens |
|---|---|---|---|
| Genuine pair | Two templates from the same person | Mated pair | — |
| Impostor pair | Two templates from different people | Non-mated pair | — |
| FMR / FAR / FPR | Fraction of impostor pairs scoring at or above the threshold | False match rate, false accept rate | A stranger unlocks your phone. A security failure |
| FNMR / FRR / FNR | Fraction of genuine pairs scoring below the threshold | False non-match rate, false reject rate | You are locked out. A usability failure |
| TAR | 1 − FNMR — the fraction of genuine pairs correctly accepted | True accept rate, verification rate, VAL | — |
| EER | The error rate at the threshold where FMR = FNMR | Equal error rate | A 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".
Twenty pairs. Ten genuine, ten impostor. Cosine scores, sorted:
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 t | Impostors ≥ t | FAR | Genuines ≥ t | TAR | FRR = 1 − TAR |
|---|---|---|---|---|---|
| 0.95 | 0 | 0.0 | 0 | 0.0 | 1.0 |
| 0.90 | 0 | 0.0 | 1 | 0.1 | 0.9 |
| 0.75 | 0 | 0.0 | 4 | 0.4 | 0.6 |
| 0.55 | 0 | 0.0 | 8 | 0.8 | 0.2 |
| 0.50 | 1 (0.52) | 0.1 | 8 | 0.8 | 0.2 |
| 0.45 | 2 (0.52, 0.47) | 0.2 | 8 | 0.8 | 0.2 |
| 0.40 | 3 | 0.3 | 9 (0.44 joins) | 0.9 | 0.1 |
| 0.30 | 5 | 0.5 | 10 (0.31 joins) | 1.0 | 0.0 |
| 0.00 | 10 | 1.0 | 10 | 1.0 | 0.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.
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.
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:
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.
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
Put in a gallery of one million and an excellent per-comparison FMR of 10−6:
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
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.
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.
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:
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.
Ten probes searched against a gallery of five identities. For each probe, record the rank at which the true identity appears:
The cumulative match characteristic at rank k is the fraction of probes whose true identity appears at rank k or better. Count them:
| k | Probes with rank ≤ k | CMC(k) | What it adds |
|---|---|---|---|
| 1 | 7 | 0.70 | The headline "rank-1 accuracy" |
| 2 | 8 | 0.80 | One probe was beaten by a single lookalike |
| 3 | 9 | 0.90 | — |
| 4 | 9 | 0.90 | — |
| 5 | 10 | 1.00 | The 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.
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.
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 template | Noise scale | Typical 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.
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.
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.
| Domain | What is an "identity"? | What is a positive pair? | What is deployed | What the field uses now |
|---|---|---|---|---|
| Speaker verification | A speaker | Two utterances from the same speaker | An embedding ("x-vector", "speaker embedding") compared by cosine | AAM-softmax — additive angular margin, which is ArcFace under a different name. ECAPA-TDNN uses m = 0.2, s = 30 |
| Visual place recognition | A physical place | Two images taken within a few metres, possibly years apart | A global descriptor searched against a map database | Weakly-supervised triplets (NetVLAD) gave way to classification-with-margin over geographic cells (CosPlace, EigenPlaces) |
| Person re-identification | A person, across non-overlapping cameras | Two crops of the same person from different cameras | A gallery ranking per camera hand-off | Batch-hard triplet with soft margin, usually combined with a margin-softmax head |
| Product / image retrieval | A product SKU | Two photos of the same product | Recall@k over a catalogue | Proxy-Anchor and margin-softmax variants; the proxy insight from Chapter 3 is native here |
| Text and multimodal embeddings | A query-document (or image-caption) pair | The annotated match | Vector search over an ANN index | InfoNCE with in-batch negatives — the batch is the mining strategy |
| Signature, iris, fingerprint, gait | An enrolled subject | Two captures of the same trait | A 1:1 match at a policy threshold | The same margin-softmax heads, with domain-specific front ends |
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.
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.
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.
| Decision | Face system | Speaker system | Reasoning |
|---|---|---|---|
| Front end | Detect, align to 112×112 by 5 landmarks | Voice activity detection, then 80-bin log-Mel, mean-normalised, ~200 frames | Both remove nuisance variation before the encoder. Alignment is to a canonical face; mean normalisation is to a canonical channel |
| Encoder | ResNet-100 → 512-d | ECAPA-TDNN → attentive statistics pooling → 192-d | Audio is variable-length, so the pooling layer is doing what the fixed crop did for faces |
| Head | ArcFace, s = 64, m = 0.5 | AAM-softmax — the same formula — typically s = 30, m = 0.2 | Derived below |
| Deployed artifact | 512 floats, cosine | 192 floats, cosine (often with score normalisation) | Identical interface |
| Metric | TAR at FAR 10−4 to 10−6 | EER and minimum detection cost on VoxCeleb | Same 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:
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.
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.
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.
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.
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:
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.
| Question | Faces | Places |
|---|---|---|
| What is a class? | A person — given by the labels | A 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 case | Constantly — adjacent cells overlap by construction |
| What breaks a naive port? | — | The margin fights genuine similarity between neighbouring classes |
| The repair | — | Group the classes so a batch never contains neighbours |
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 you must re-derive per domain | Why it does not transfer |
|---|---|
| The scale s | It 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 m | It 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 pair | This 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 FAR | A 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 normalisation | Face 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 |
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.
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 achievement | Read as a capability |
|---|---|
| Open-set: works on people never seen in training | No consent step exists anywhere in the pipeline. Being unknown to the system is not protection |
| 128 to 512 numbers per face | A country's population fits in a few hundred gigabytes — Chapter 0's arithmetic. Storage is not a constraint on anyone |
| Cosine similarity, precomputable index | Searching a billion faces is a matrix multiply. Cost is not a constraint either |
| No retraining to enrol | A gallery can be extended by anyone with photographs, silently, after the fact |
| Robust to pose, lighting, age | Robust to the subject not cooperating, not knowing, and not being recognisable to a human |
| One global threshold | One policy decision, made by an engineer, applied to everyone — including groups whose score distributions differ |
"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.
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:
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:
A 1:N search returns a ranked list of similarities. Follow what happens to that list in a real process.
| Step | What the artifact is | What is lost |
|---|---|---|
| 1. Search | A ranked list with scores — a probabilistic statement | Nothing yet |
| 2. Analyst review | "Investigative lead", top candidate | The score, the rank, the gallery size, the FMR at that threshold |
| 3. Photo lineup | A witness is shown the candidate | That the candidate was selected because they look similar — which is exactly the condition under which eyewitness identification is least reliable |
| 4. Arrest | A categorical claim about a person | Everything. 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 models in this lesson were trained on data that in several cases no longer exists, for reasons worth knowing.
| Dataset | What it was | Status |
|---|---|---|
| MS-Celeb-1M | ~10M images of ~100K "celebrities", scraped from the web; the basis for MS1MV2, which ArcFace trains on | Withdrawn 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 set | Retired by the University of Washington in 2020; Creative Commons licences permit reuse but the subjects were not the uploaders |
| DukeMTMC | Surveillance video of students on a university campus, used widely for person re-identification | Withdrawn in 2019 over consent and surveillance concerns |
| LFW | 13,233 images of 5,749 people from news photographs | Still 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.
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.
| Mitigation | What it actually buys | What it does not |
|---|---|---|
| Hash the template | Nothing — matching needs distances, and a cryptographic hash destroys them | Any 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 storage | Different systems store different templates, so a leak from one does not match another; the key can be rotated | Protection if the key leaks with the template, which is the usual case in a breach |
| Secure enclave, never leaves the device | Removes the central database entirely — the strongest mitigation by a wide margin | Any use case that genuinely needs a server-side gallery |
| Homomorphic or secure two-party matching | Real cryptographic protection for the comparison itself | Cheap latency — and it does not help if the gallery holder is the adversary |
Briefly, and with the caveat that this moves fast — check the current text before relying on any of it.
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 decision | What it removes |
|---|---|
| Enrolment and matching happen entirely on-device, in a secure enclave | No central gallery exists, so there is nothing to search, subpoena or breach |
| The template never leaves the device and is not synced or backed up | Cross-device linkage becomes impossible |
| The subject is the owner, and enrolment is an explicit act | Consent is structural, not a checkbox |
| A passcode fallback always exists | Failure 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 13 | The 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 device | Unlocking 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.
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.
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.
| Year | Method | The one-line contribution | What it left broken |
|---|---|---|---|
| 2014 | DeepFace, DeepID | Deep CNN + softmax over identities; take a bottleneck layer as the representation | The representation was a by-product; thousands of dimensions |
| 2014 | DeepID2 | Add a verification (contrastive) term to the identification loss | Two losses to balance; pair sampling |
| 2015 | FaceNet | Optimise the deployed distance directly with a triplet hinge on 128-d L2-normalised embeddings; semi-hard mining inside 1,800-exemplar batches | O(N3) objective; sampler-dependent results; collapse is a stationary point |
| 2016 | Center loss | Add a pull toward a learned per-class centre alongside softmax — intra-class compactness without triplets | Another loss weight to tune; no inter-class term |
| 2016 | L-Softmax | First multiplicative angular margin inside softmax | Hard to train; needs annealing |
| 2017 | SphereFace (A-Softmax) | Normalise weights, margin as cos(m θ) | Non-monotone; needs λ annealing and a piecewise surrogate |
| 2017 | NormFace | Normalise both features and weights; introduce the scale s | Separable but not compact — no margin |
| 2017 | ProxyNCA | Replace sampled negatives with learned class proxies; ~3× faster convergence | Named the trick that softmax had been doing all along |
| 2018 | CosFace / AM-Softmax | Additive cosine margin: s(cos θ − m), m = 0.35 | The angular demand varies with θ, weakest at the boundary |
| 2018/19 | ArcFace | Additive angular margin: s cos(θ + m), m = 0.5 — a constant geodesic gap | Non-monotone past π − m; a 42-nat loss spike at initialisation |
| 2020–21 | Partial FC, Sub-center ArcFace | Sample a fraction of class centres per step (10M identities on one machine); multiple sub-centres per class to absorb label noise | — |
| 2021–22 | MagFace, AdaFace | Make the margin a function of feature norm / image quality, so low-quality samples are not forced into the class centre | — |
| 2021– | CLIP, CLAP and friends | Same 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 | — |
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.
| Component | FaceNet, 2015 | ArcFace, 2019 |
|---|---|---|
| Batch construction | ~1,800 exemplars, ~40 per identity, identity-structured | Whatever fits — shuffle like any classifier |
| Extra forward passes | None, but the batch must be huge to mine within | None |
| Sampler code | A few hundred lines: distance matrix, masks, semi-hard selection, fallbacks | Zero lines |
| Loss code | 4 lines | ~10 lines, of which 3 are the monotonicity guard |
| Extra parameters | None | A C×d proxy table — 176 MB at 85,742 × 512, deleted at deployment |
| Hyperparameters | α, plus the entire sampling policy | s and m, both with derivable starting points |
| Failure mode to fear | Collapse — silent, and a stationary point | Divergence at initialisation — loud, and fixed by a warm-up |
| Reproducibility | Poor — results depend on batch composition and sampler details often omitted from papers | Good — 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 fit | C 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.
| Number | What it is |
|---|---|
| α = 0.2 | FaceNet's triplet margin, in squared-distance units — equivalently a cosine margin of 0.1, or 25.84° of clearance around a perfect positive |
| 128 | FaceNet's embedding dimension; 512 bytes as float32, 128 bytes quantised |
| ~1,800 / ~40 | FaceNet'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 = 64 | The scale in both CosFace and ArcFace. Floor for C = 85,742 at PW = 0.9 is s ≥ 13.56; 64 is headroom |
| m = 0.35 | CosFace'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.04 | ArcFace's loss at initialisation with C = 85,742 — 30.68 from the margin plus ln(85,742) = 11.36. Warm up or ramp |
| 10.36 | The 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 / FAR | Impostor pairs needed to measure a false-accept rate to within ±10% |
| If you want… | Go to |
|---|---|
| The general mechanics of contrastive objectives | Contrastive learning and CLIP |
| The formal account of the two forces in every loss here | Alignment and uniformity on the hypersphere |
| The same recipe in audio, with text as the proxy generator | CLAP |
| Open-set retrieval over places rather than people | NetVLAD, VPR aggregation, foundation models for VPR |
| How to choose a similarity in the first place | Similarity metrics and metric design |
| How to actually serve a billion embeddings | ANN indexes and vector embeddings |
| What can go wrong with an embedding you expose | Embedding security — including inversion attacks, which apply directly to face templates |
| How to evaluate any of this honestly | The metrics ladder |
| Step | What to do | The decision that matters |
|---|---|---|
| 1. Data | Any open-set dataset with identity labels — CASIA-WebFace for faces, VoxCeleb for speakers, Stanford Online Products for retrieval | Split by identity, never by image. A shared identity across the split makes the whole evaluation meaningless |
| 2. Backbone | Any encoder, ending in a linear layer to d = 512 | The backbone is the least interesting choice in this lesson. Do not spend the weekend here |
| 3. Head | The ArcFace module from Chapter 4, including the th/mm guard | b = 0, normalise both sides. Skip either and you are back to Chapter 3's confounds |
| 4. Scale | s from the floor formula, capped at 64 | Compute it — do not copy 64 onto a 300-class problem |
| 5. Warm-up | m = 0 for one epoch, then ramp to target over ~2,000 steps | Defuses the 42-nat initialisation spike |
| 6. Sanity check | Confirm the loss drops well below ln C in the first epoch | If it sits at ln C, your scale is below the floor. This is the single most common bug |
| 7. Diagnostics | Log mean and 5th-percentile cos θy, and the fraction of samples with cos θy < 0 | The tail tells you whether a subpopulation is stranded — the mean never will |
| 8. Evaluate | Throw away the head. Embed, L2-normalise, cosine, then TAR at your target FAR from Chapter 6's function | Report k, the number of false accepts your threshold admitted. It is your error bar |
| 9. Compare | Rerun with m = 0. The margin should cost you training loss and buy you TAR at low FAR | If it does not, your m is wrong for your data — not the method |
| 10. Audit | Split your test pairs by whatever groups you can identify and report FMR per group at the shared threshold | With the pair counts. Chapter 8 is a step in the recipe, not an appendix |
Collected from the chapters, in the order you are likely to hit them.
| Symptom | Cause | Chapter |
|---|---|---|
| Loss pinned at ln C forever | Scale below the floor, or a missing F.normalize on either side | 3 |
| Loss sits at exactly α and the embedding is constant | Triplet collapse — a stationary point, not a plateau | 2 |
| Loss goes to NaN in the first few hundred steps | Full margin at initialisation, amplified by s | 5 |
| Loss is fine, verification is not | Selecting on the wrong metric — the margin raises training loss by design | 5, 6 |
| Training loss keeps falling and the active-triplet fraction is near zero | The sampler has run out of signal; the model is no longer learning | 2 |
| Out of memory building the mining mask | A B3 boolean tensor — 5.8 GB at B = 1800 | 2 |
| Accuracy far better on the val split than in production | Split by image rather than by identity, so the same person is on both sides | 9 (recipe step 1) |
| A stubborn tail of samples with cos θy < 0 | The missing monotonicity guard, or a corrupt identity | 4, 5 |
| The reported TAR moves by 5 points between runs | Too few impostor pairs at your target FAR — you are reading Poisson noise | 6 |
| Works beautifully on one demographic group | You never measured the others separately | 8 |
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.