all lessons / computer_vision / 12 · metric learning lesson 12 / 19

Metric learning and visual retrieval

度量学习与视觉检索

Lesson 11 ended on a loose end: the Re-ID tracker matched identities by embedding each crop into a vector and measuring the distance between vectors. We treated "small distance = same thing" as a given. This lesson earns it. We learn an embedding space where geometry encodes similarity, then make distance search scale to a billion items. This is also the conceptual root of the contrastive learning that powers lessons 14 (self-supervised) and 15 (CLIP).

第 11 课留下了一个未解的线头:重识别(Re-ID)跟踪器通过把每个裁剪框嵌入成一个向量、再度量向量之间的距离来匹配身份。我们把"距离小 = 同一个东西"当成了理所当然。本课要把它讲透。我们学习一个几何结构编码相似度的嵌入空间,再让距离检索能扩展到十亿量级的条目。这也是驱动第 14 课(自监督)和第 15 课(CLIP)的对比学习的概念源头。

The plan计划
Four moves. (1) Define the embedding space and show why L2-normalizing turns a dot product into cosine similarity. (2) Write the three losses that shape it — contrastive, triplet (with its margin), and InfoNCE (with its temperature) — and earn each formula. (3) Confront the practical killer: most pairs are uninformative, so we need hard-negative mining, plus the ArcFace margin for the verification regime. (4) Make it scale — why exact nearest-neighbor dies at a billion items and how IVF/PQ/HNSW indexes trade recall for latency — and define the metrics recall@k and retrieval mAP. A widget lets you push and pull points and watch the loss fall. 四步走。(1) 定义嵌入空间,并说明为什么L2 归一化能把点积变成余弦相似度。(2) 写出塑造这个空间的三种损失——对比损失、三元组损失(带间隔 margin)、以及 InfoNCE(带温度)——并把每个公式讲透。(3) 直面真正的实战杀手:大多数样本对是无信息的,所以我们需要难负样本挖掘,外加用于验证场景的 ArcFace 间隔。(4) 让它可扩展——为什么精确最近邻在十亿量级失效,以及 IVF/PQ/HNSW 索引如何用召回率换延迟——并定义 recall@k 和检索 mAP 这两个指标。有一个小部件让你推拉这些点,看着损失下降。

1 · The embedding space and why we normalize

1 · 嵌入空间,以及我们为何要归一化

A metric-learning model is an encoder fθ that maps an image to a vector z = fθ(x) ∈ ℝd (d typically 128–2048). Unlike a classifier (lesson 08), which outputs a fixed label, the embedding output is open-ended: we never name the classes, we only require that similar images land near each other and dissimilar ones land far apart. "Near" needs a metric. Three are common: Euclidean distance ‖za − zb, dot product za · zb, and cosine similarity (za · zb) / (‖za‖ ‖zb‖).

度量学习模型是一个编码器 fθ,它把一张图像映射成一个向量 z = fθ(x) ∈ ℝd(d 通常为 128–2048)。与输出固定标签的分类器(第 08 课)不同,嵌入的输出是开放式的:我们从不为类别命名,只要求相似的图像彼此靠近、不相似的图像彼此远离。"靠近"需要一个度量。常见的有三种:欧氏距离 ‖za − zb、点积 za · zb,以及余弦相似度 (za · zb) / (‖za‖ ‖zb‖)

Almost everyone L2-normalizes the embedding — divides it by its own length so ‖z‖ = 1, putting every point on the unit hypersphere. The payoff is immediate algebra: once both vectors have length 1, the denominator of cosine similarity is 1, so

几乎所有人都会对嵌入做 L2 归一化——把它除以自身的长度,使 ‖z‖ = 1,从而把每个点都放到单位超球面上。回报立竿见影,就是一步代数:一旦两个向量的长度都为 1,余弦相似度的分母就是 1,于是

za · zb = ‖za‖ ‖zb‖ cos∠ = 1 · 1 · cos∠ = cos∠

The dot product literally becomes the cosine of the angle between them. This matters for three reasons. (1) It removes magnitude: a bright vs dim photo of the same object differs in vector length but not direction, and cosine ignores length — so the representation focuses on what the thing is, not how strong the activation happened to be. (2) It bounds similarity to [−1, 1], making a single threshold meaningful across the whole corpus. (3) On the unit sphere, squared Euclidean distance and cosine are tied by ‖za − zb‖² = 2 − 2 za·zb, so "maximize cosine" and "minimize distance" are the same objective — you can use whichever the loss prefers.

点积就直接变成了两者夹角的余弦。这一点重要有三个原因。(1) 它去掉了幅值:同一物体的亮照片与暗照片在向量长度上不同,但方向相同,而余弦忽略长度——所以表示聚焦于这个东西是什么,而不是激活恰好有多强。(2) 它把相似度限定在 [−1, 1],使得单一阈值在整个语料库上都有意义。(3) 在单位球面上,欧氏距离的平方与余弦通过 ‖za − zb‖² = 2 − 2 za·zb 绑定在一起,所以"最大化余弦"和"最小化距离"是同一个目标——你可以用损失更偏好的那一个。

2 · The losses that shape the space

2 · 塑造这个空间的损失

2a · Contrastive loss (pairs)

2a · 对比损失(样本对)

The original formulation operates on pairs with a label y (1 = similar, 0 = dissimilar) and a margin m:

最初的表述作用在样本对上,带一个标签 y(1 = 相似,0 = 不相似)和一个间隔 m

L = y · d² + (1 − y) · max(0, m − d)² , d = ‖za − zb

Read it: for a similar pair (y=1) the loss is — pull them together, all the way. For a dissimilar pair (y=0) the loss is max(0, m − d)² — push them apart, but only until they are at least m apart, after which the loss is zero. The margin stops the model from wasting capacity shoving already-far negatives even farther; only violators contribute gradient.

这样读它:对一个相似对(y=1),损失是 ——把它们一路拉到一起。对一个不相似对(y=0),损失是 max(0, m − d)²——把它们推开,但只推到相距至少 m 为止,之后损失为零。这个间隔阻止模型把已经很远的负样本再往更远推、白白浪费容量;只有违反间隔者才贡献梯度。

2b · Triplet loss (anchor / positive / negative)

2b · 三元组损失(锚点 / 正样本 / 负样本)

Pairs ignore that similarity is relative. The triplet loss takes three points at once — an anchor a, a positive p (same identity as the anchor), and a negative n (different) — and asks only that the anchor be closer to its positive than to its negative, by at least a margin m:

样本对忽略了相似性是相对的。三元组损失一次取三个点——一个锚点 a、一个正样本 p(与锚点身份相同)、一个负样本 n(不同身份)——它只要求锚点到正样本的距离比到负样本的距离更近,且至少近一个间隔 m

Ltriplet = max( 0, d(a, p) − d(a, n) + m )

If d(a,n) already exceeds d(a,p) + m, the bracket is negative, the max clamps to 0, and that triplet contributes nothing. Only triplets where the negative is too close (or the positive too far) produce gradient: the loss pulls p toward a and pushes n away until the margin is satisfied. Worked example: say d(a,p) = 0.4, d(a,n) = 0.7, margin m = 0.5. Loss = max(0, 0.4 − 0.7 + 0.5) = max(0, 0.2) = 0.2 — positive, so this triplet still teaches: the negative at 0.7 is closer than the required 0.4 + 0.5 = 0.9. Move the negative out to 1.0 and the loss becomes max(0, 0.4 − 1.0 + 0.5) = 0 — satisfied, silent.

如果 d(a,n) 已经超过 d(a,p) + m,括号内为负,max 截断为 0,这个三元组就什么也不贡献。只有负样本太近(或正样本太远)的三元组才产生梯度:损失把 p 拉向 a、把 n 推开,直到间隔被满足。算个具体例子:d(a,p) = 0.4、d(a,n) = 0.7、间隔 m = 0.5。损失 = max(0, 0.4 − 0.7 + 0.5) = max(0, 0.2) = 0.2——为正,所以这个三元组仍在教学:位于 0.7 的负样本比所要求的 0.4 + 0.5 = 0.9 更近。把负样本挪到 1.0,损失就变成 max(0, 0.4 − 1.0 + 0.5) = 0——已满足,沉默。

2c · InfoNCE (one positive against many negatives)

2c · InfoNCE(一个正样本对抗多个负样本)

Triplets use a single negative; InfoNCE (Noise-Contrastive Estimation) uses many at once, framing the problem as classification: "which of these N candidates is the true positive for the anchor?" With similarity s(a,b) = cosine and a temperature τ:

三元组只用一个负样本;InfoNCE(噪声对比估计)一次用许多负样本,把问题框定为分类:"这 N 个候选中,哪一个才是锚点的真正正样本?"取相似度 s(a,b) = 余弦,并带一个温度 τ

LInfoNCE = − log [ exp(s(a, p)/τ) / ( exp(s(a, p)/τ) + ∑n exp(s(a, n)/τ) ) ]

That is exactly a softmax cross-entropy where the logits are similarities and the "correct class" is the positive. The temperature τ sharpens or softens the distribution: small τ (e.g. 0.05) makes the softmax peaky, so the loss focuses almost entirely on the hardest negatives (those with similarity nearest the positive's) — strong gradient, but sensitive; large τ spreads attention across all negatives — gentler, slower.

这恰好就是一个 softmax 交叉熵,其中 logit 是相似度,"正确类别"就是正样本。温度 τ 会锐化或软化这个分布:小的 τ(例如 0.05)让 softmax 变得尖锐,于是损失几乎完全聚焦于最难的负样本(那些相似度最接近正样本的)——梯度强,但敏感;大的 τ 把注意力铺散到所有负样本上——更温和,也更慢。

Worked example (do this one carefully — it is the root of lessons 14–15). Anchor–positive cosine s(a,p) = 0.9, and two negatives: an easy one at 0.2 and a hard one at 0.85 (almost as similar as the positive). Take temperature τ = 0.1. Divide every similarity by τ to get logits, exponentiate, normalize:

算个具体例子(这个要仔细做——它是第 14–15 课的根基)。锚点–正样本余弦 s(a,p) = 0.9,还有两个负样本:一个简单的位于 0.2,一个的位于 0.85(几乎和正样本一样相似)。取温度 τ = 0.1。把每个相似度除以 τ 得到 logit,再取指数、归一化:

logits = sim/τ :   0.9/0.1 = 9  (pos),   0.85/0.1 = 8.5  (hard neg),   0.2/0.1 = 2  (easy neg)
exp :   e9 ≈ 8103 ,   e8.5 ≈ 4915 ,   e2 ≈ 7.39
Z = 8103 + 4915 + 7.39 ≈ 13025
ppos = 8103 / 13025 ≈ 0.622  ⟹  L = −ln(0.622) ≈ 0.475

The easy negative (e2 ≈ 7) is numerically negligible against the positive (e9 ≈ 8103); essentially all the "competition" in the denominator comes from the hard negative (e8.5 ≈ 4915). So the loss is almost entirely a fight between the positive and the one hard negative — exactly the behaviour we want.

与正样本(e9 ≈ 8103)相比,简单负样本(e2 ≈ 7)在数值上可以忽略不计;分母里几乎所有的"竞争"都来自那个难负样本(e8.5 ≈ 4915)。所以损失几乎完全是正样本与那一个难负样本之间的较量——正是我们想要的行为。

Now the temperature effect. Re-run with τ = 0.5 instead. Logits become 0.9/0.5 = 1.8, 0.85/0.5 = 1.7, 0.2/0.5 = 0.4:

现在看温度的效应。改用 τ = 0.5 重新算一遍。logit 变为 0.9/0.5 = 1.80.85/0.5 = 1.70.2/0.5 = 0.4

exp :   e1.8 ≈ 6.05 ,   e1.7 ≈ 5.47 ,   e0.4 ≈ 1.49
Z = 6.05 + 5.47 + 1.49 ≈ 13.01
ppos = 6.05 / 13.01 ≈ 0.465  ⟹  L = −ln(0.465) ≈ 0.766

Two things changed. The loss rose (0.475 → 0.766), and the easy negative now carries real weight (1.49 out of 13.01 ≈ 11% of the mass, versus 0.06% before). The lesson: small τ sharpens the softmax so the loss concentrates on the hard negative (0.85) and all but ignores the easy one — strong, focused gradient but sensitive to noise; large τ softens it, spreading attention across all negatives, including the uninformative easy one, so the signal is gentler and less discriminating.

有两件事发生了变化。损失上升了(0.475 → 0.766),而且简单负样本现在承载了实实在在的权重(13.01 里占 1.49 ≈ 11% 的质量,而之前只有 0.06%)。这一课的要点是:小的 τ 锐化 softmax,使损失集中在难负样本(0.85)上、几乎无视简单的那个——梯度强而聚焦,但对噪声敏感;大的 τ 软化它,把注意力铺散到所有负样本上,包括那个无信息的简单负样本,于是信号更温和、判别力更弱。

InfoNCE's structural strength is that every other item in the batch serves as a negative for free, so a big batch supplies hundreds of negatives per anchor. This loss is the engine of SimCLR (lesson 14) and CLIP (lesson 15) — remember it.

InfoNCE 的结构性优势在于,批次里其他每一个条目都免费充当负样本,所以一个大批次能为每个锚点提供成百上千个负样本。这个损失是 SimCLR(第 14 课)和 CLIP(第 15 课)的引擎——记住它。

Loss损失Inputs输入Key knob关键旋钮Gotcha
Contrastive对比损失A pair + label一个样本对 + 标签margin m间隔 mNeeds informative negative pairs.需要有信息量的负样本对。
Triplet三元组Anchor, positive, negative锚点、正样本、负样本margin m间隔 mMost triplets are easy → zero gradient.大多数三元组都很简单 → 零梯度。
InfoNCEInfoNCE1 positive, many negatives1 个正样本、多个负样本temperature τ温度 τBatch composition heavily shapes learning.批次构成会强烈影响学习。
ArcFace/CosFaceArcFace/CosFaceClass-labeled samples带类别标签的样本angular margin角度间隔Needs known classes at train time.训练时需要已知类别。

3 · Hard negatives, and the ArcFace margin

3 · 难负样本,以及 ArcFace 间隔

The recurring villain across all three losses: most pairs are uninformative. Two random images are obviously different, so the negative term is already satisfied and the gradient is zero. If you sample triplets at random, the overwhelming majority teach nothing and training crawls. Hard-negative mining fixes this by deliberately constructing informative examples: for each anchor, pick negatives that are close in the current embedding (the model's current confusions) rather than easy far-away ones. Variants: hardest negative (closest of all — strong signal but unstable, can chase label noise), semi-hard (closer than the positive but still outside the margin — FaceNet's choice, a stable middle ground), and in-batch mining (just use the hardest negatives already present in the minibatch). Curating negatives is often more decisive than the choice of loss.

贯穿这三种损失的反复出现的大反派是:大多数样本对是无信息的。两张随机图像显然不同,所以负样本项早已满足、梯度为零。如果你随机采样三元组,绝大多数什么也教不了,训练就爬行般缓慢。难负样本挖掘通过刻意构造有信息量的样本来解决这个问题:对每个锚点,挑选在当前嵌入里靠得近的负样本(即模型当前的混淆点),而不是那些容易的、远远的负样本。几种变体:最难负样本(所有里最近的——信号强但不稳定,可能去追逐标签噪声)、半难(比正样本更近但仍在间隔之外——FaceNet 的选择,是一个稳定的折中)、以及批内挖掘(直接使用小批次里已有的最难负样本)。精心筛选负样本往往比损失的选择更具决定性。

ArcFace / CosFace — margins for face recognition

ArcFace / CosFace —— 用于人脸识别的间隔

Face recognition is the verification regime: train on a closed set of identities, deploy on identities never seen. A plain softmax classifier learns features that are separable but not necessarily discriminative — clusters can sit right next to each other with no gap, which is fatal when a new identity must be told apart from look-alikes. ArcFace and CosFace work on the L2-normalized sphere and inject an explicit angular margin into the softmax: ArcFace adds margin m to the angle between a sample and its true class center, requiring cos(θ + m) instead of cos θ; CosFace subtracts the margin from the cosine, cos θ − m. Either way the model must place each sample not just on the correct side but a margin inside its class — forcing tight, well-separated clusters that generalize to unseen faces. Worked: with margin m = 30°, a sample sitting at angle θ = 20° from its true class center is scored by the loss as if it were at θ + m = 50° — penalized as the much worse 50° fit — so the gradient keeps pulling it inward, and the loss only relaxes once the sample sits at a genuinely small angle. Crucially the margin is added only to the true class's logit: the competing class centers keep their plain cos θ scores, so the sample must out-score them from a deliberately handicapped position. Beating the other classes despite that handicap is precisely what forces each class into a tight cluster with room to spare. The leftover 30° headroom shows up as a visible angular gap between classes on the sphere. This is the dominant face-recognition training objective.

人脸识别属于验证场景:在一个封闭的身份集合上训练,却部署到从未见过的身份上。一个普通的 softmax 分类器学到的特征是可分但不一定有判别性的——各个簇可以紧挨在一起、毫无间隙,而当一个新身份必须与长相相似者区分开时,这是致命的。ArcFaceCosFace 工作在 L2 归一化的球面上,并向 softmax 注入一个显式的角度间隔:ArcFace 给样本与其真实类别中心之间的夹角加上间隔 m,要求 cos(θ + m) 而非 cos θ;CosFace 则从余弦中减去间隔,即 cos θ − m。无论哪种,模型都必须把每个样本放到的不只是正确的一侧,而是要在其类别内部再深入一个间隔——从而逼出紧凑、彼此分隔良好、能泛化到未见人脸的簇。算一算:取间隔 m = 30°,一个与其真实类别中心成 θ = 20° 夹角的样本,会被损失当作处于 θ + m = 50° 来打分——按 50° 这个糟糕得多的拟合来惩罚——于是梯度会不断把它往里拉,只有当样本真正落在一个很小的角度上时,损失才会放松。关键在于,间隔只加到真实类别的 logit 上:竞争的类别中心保持它们朴素的 cos θ 得分,所以样本必须从一个被刻意设置了障碍的位置去胜过它们。尽管带着这个障碍仍能击败其他类别,正是这一点把每个类别逼成一个留有余量的紧凑簇。剩下的 30° 余量,会在球面上表现为类别之间一道可见的角度间隙。这是当前占主导地位的人脸识别训练目标。

4 · Interactive · push and pull an embedding

4 · 交互 · 推拉一个嵌入

The widget below is the triplet/contrastive update made visible. An anchor (blue), a positive (green, same class), and several negatives (red) live in a 2-D embedding. Press Gradient step and the triplet update fires: the positive is pulled toward the anchor, and any negative that sits inside the margin (closer to the anchor than d(a,p) + m) is pushed out. Negatives already beyond the margin are left alone — that is the max(0, ·) clamp doing its job, and you can watch the loss-over-steps plot fall to zero as the configuration becomes satisfied.

下面的小部件把三元组/对比更新可视化了。一个锚点(蓝色)、一个正样本(绿色,同类)和若干负样本(红色)位于一个二维嵌入中。按下梯度步(Gradient step),三元组更新就会触发:正样本被拉向锚点,任何落在间隔以内(比 d(a,p) + m 更靠近锚点)的负样本都会被推出去。已经在间隔之外的负样本则不予理会——这正是 max(0, ·) 截断在起作用,而你可以看着"损失随步数"的曲线随配置逐渐被满足而降到零。

Start with the default: the positive is a little far and one negative is intruding. The loss bar is high — the bug is the lesson. Step a few times and watch the geometry resolve and the loss curve drop. Drag the margin up to demand more separation (loss rises again — you just made the task harder), or switch to the contrastive rule to compare the per-pair behavior.

先从默认状态开始:正样本有点远,还有一个负样本在闯入。损失条很高——这个 bug 就是本课的内容。多步几次,看着几何结构理顺、损失曲线下降。把间隔(margin)往上拖以要求更大的分隔(损失又会上升——你刚把任务变难了),或者切换到对比(contrastive)规则,来对比逐对的行为。

Embedding push/pull — the triplet update, one step at a time
嵌入推拉——三元组更新,一次一步
Blue = anchor, green = positive, red = negatives. A gradient step pulls the positive in and pushes margin-violating negatives out (satisfied negatives are left alone). Bottom strip: loss over steps. Drag points to set up your own hard case.
蓝 = 锚点,绿 = 正样本,红 = 负样本。一次梯度步把正样本拉进来,并把违反间隔的负样本推出去(已满足的负样本不予理会)。底部条带:损失随步数的变化。拖动这些点来搭建你自己的难例。
margin m
间隔 m
0.25
d(a, pos)
d(a, 正样本)
d(a, nearest neg)
d(a, 最近负样本)
loss
损失
Show the core JS (≈22 lines)查看核心代码(约 22 行)
// points: a (anchor), p (positive), N negatives — all 2-D
function dist(u,v){ return Math.hypot(u.x-v.x, u.y-v.y); }

function tripletLossAndStep(lr, m){
  const dp = dist(a, p);
  let loss = 0;
  // positive: pull toward anchor (gradient of d(a,p))
  const gp = unit(sub(p, a));            // direction p← a
  p.x -= lr * gp.x; p.y -= lr * gp.y;    // move p toward a
  for (const n of negs){
    const dn = dist(a, n);
    const viol = dp - dn + m;            // triplet margin term
    if (viol > 0){                       // only margin-violating negs move
      loss += viol;
      const gn = unit(sub(n, a));        // push n away from anchor
      n.x += lr * gn.x; n.y += lr * gn.y;
    }
  }
  return loss;                           // plotted over steps
}

5 · Making retrieval scale: ANN indexes

5 · 让检索可扩展:ANN 索引

With a trained encoder, retrieval is "embed the query, find its nearest neighbors in the corpus." For a few thousand items, brute force is fine — compute the query's similarity to every stored vector and sort. But exact nearest-neighbor search does not scale. With N vectors of dimension d, each query costs O(N·d): at N = 1 billion and d = 512, that is ~5×1011 multiply-adds per query — seconds of compute and the whole index must sit in RAM. Worse, in high dimensions there are no useful shortcuts (the "curse of dimensionality" defeats tree structures like k-d trees). So we give up exact answers and accept Approximate Nearest Neighbor (ANN) search, which trades a little recall (fraction of the true top-k it actually returns) for a large drop in latency. The three workhorse index families:

有了训练好的编码器,检索就是"把查询嵌入,在语料库中找它的最近邻"。对几千个条目,暴力搜索没问题——算出查询与每个存储向量的相似度再排序即可。但精确最近邻搜索无法扩展。对 N 个维度为 d 的向量,每次查询的代价是 O(N·d):当 N = 10 亿、d = 512 时,那就是每次查询约 5×1011 次乘加——需要数秒的计算,而且整个索引必须驻留在内存里。更糟的是,在高维中没有有用的捷径("维度灾难"击溃了 k-d 树这类树结构)。于是我们放弃精确答案,接受近似最近邻(ANN)搜索,它用一点召回率(真实 top-k 中它实际返回的比例)换取延迟的大幅下降。三个主力索引家族:

Index索引Idea思路Trades权衡
IVF (inverted file)(倒排文件)k-means-cluster the corpus into cells; at query time search only the few cells nearest the query.用 k-means 把语料库聚成若干个单元(cell);查询时只搜索离查询最近的少数几个单元。Fewer cells probed = faster but lower recall (a true neighbor in an unprobed cell is missed).探查的单元越少 = 越快但召回率越低(落在未探查单元里的真实近邻会被漏掉)。
PQ (product quantization)(乘积量化)Split each vector into chunks, quantize each chunk to a small codebook; store compact codes and compute approximate distances on them.把每个向量切成若干块,将每一块量化到一个小码本上;存储紧凑的码,并在码上计算近似距离。Compresses 512 floats → a few bytes (fits a billion vectors in RAM) at the cost of distance precision.把 512 个浮点数压成几个字节(让十亿向量能装进内存),代价是距离精度。
HNSW (graph)(图)Build a navigable small-world graph; greedily hop toward the query through nearest links across multiple layers.构建一个可导航的小世界图;在多层之间,沿最近的连边贪心地朝查询跳跃。Excellent recall/latency, but the graph is memory-hungry and slow to build.召回率/延迟俱佳,但这个图很吃内存、构建也慢。

In production these compose: IVF+PQ (probe a few clusters, compare compressed codes) is the classic billion-scale recipe (FAISS), while HNSW dominates when memory is available and recall is paramount. The dial is always the same — probe more / compress less / search a bigger graph frontier for higher recall, at higher latency and memory. A typical pipeline is embed → ANN candidate retrieval → exact re-rank of the top few hundred → threshold, so the approximate stage only needs to get true neighbors into the candidate set, and the exact re-rank fixes the ordering.

在生产中这些是组合使用的:IVF+PQ(探查少数几个簇、比较压缩后的码)是经典的十亿量级配方(FAISS),而当内存充足、召回率至上时则由 HNSW 主导。旋钮永远是同一个——探查更多 / 压缩更少 / 搜索更大的图前沿来换取更高召回率,代价是更高的延迟和内存。一条典型的流水线是嵌入 → ANN 候选检索 → 对前几百个做精确重排(re-rank)→ 阈值判定,所以近似阶段只需要把真实近邻捞进候选集,精确重排再把排序修正过来。

6 · Retrieval metrics, and a threshold trap

6 · 检索指标,以及一个阈值陷阱

Recall@k: for a query, did at least one correct item appear in the top k returned? Averaged over queries, recall@1 / recall@5 / recall@10 are the standard retrieval scores — they grade the candidate list. Retrieval mAP (mean Average Precision) is stricter: for each query it averages precision at every rank where a relevant item appears (rewarding relevant items ranked high), then means over queries — it grades ranking quality, not just presence. Worked example: a query has two relevant items in the corpus, and the system returns a ranked list of 5 where the relevant ones land at rank 1 and rank 3. Average Precision looks only at the ranks where a relevant item appears and records the precision there: at rank 1 we have found 1 of 1 so far → precision@1 = 1/1 = 1.0; at rank 3 we have found 2 relevant out of the top 3 → precision@3 = 2/3 ≈ 0.667. AP = (1.0 + 0.667)/2 ≈ 0.833 (average over the relevant items). Had the same two relevant items landed at ranks 4 and 5 instead, the precisions would be 1/4 and 2/5 → AP = (0.25 + 0.40)/2 = 0.325 — same recall, far worse AP, because mAP rewards ranking relevant items high. mAP is the mean of this AP over all queries.

Recall@k:对一个查询,返回的前 k 个里是否至少出现了一个正确条目?在所有查询上取平均,recall@1 / recall@5 / recall@10 就是标准的检索得分——它们评判的是候选列表。检索 mAP(mean Average Precision,平均精度均值)更严格:对每个查询,它在每个出现相关条目的名次上计算精度并求平均(奖励把相关条目排得靠前),再在所有查询上求均值——它评判的是排序质量,而不只是有没有出现。算个具体例子:某个查询在语料库里有两个相关条目,系统返回一个长度为 5 的排序列表,相关的两个落在第 1 名和第 3 名。Average Precision 只看相关条目出现的那些名次,并记录那里的精度:在第 1 名,我们到目前为止找到了 1 个中的 1 个 → precision@1 = 1/1 = 1.0;在第 3 名,前 3 个里找到了 2 个相关 → precision@3 = 2/3 ≈ 0.667。AP = (1.0 + 0.667)/2 ≈ 0.833(在相关条目上取平均)。若这同样的两个相关条目改为落在第 4 名和第 5 名,精度就会是 1/4 和 2/5 → AP = (0.25 + 0.40)/2 = 0.325——召回率相同,AP 却差得多,因为 mAP 奖励把相关条目排得靠前。mAP 就是这个 AP 在所有查询上的均值。

Retrieval vs verification — a threshold trap检索 vs 验证——一个阈值陷阱
High recall@k can hide bad thresholding. Retrieval asks "which candidates?" — a ranking question. Verification asks "same or different?" — a yes/no decision that needs an absolute similarity threshold. A model can rank perfectly (great recall@k) yet have no clean threshold separating same-from-different, because per-query the gap sits in different places. The threshold is a product decision balancing a false match (let a stranger in) against a false non-match (lock out the rightful user) — costs that differ wildly by application, so pick the operating point on the precision-recall (or ROC) curve, don't infer it from recall@k. 很高的 recall@k 可能掩盖糟糕的阈值设定。检索问的是"哪些候选?"——一个排序问题。验证问的是"相同还是不同?"——一个需要绝对相似度阈值的是/否判定。一个模型可以排序完美(recall@k 很棒),却没有一个干净的阈值能把"相同"与"不同"分开,因为逐查询来看,那道间隙落在不同的位置上。阈值是一个产品决策,要在误匹配(放一个陌生人进来)与漏匹配(把正主拒之门外)之间权衡——而这两种代价因应用而天差地别,所以要在精确率-召回率(或 ROC)曲线上挑选工作点,别从 recall@k 去反推它。

Where this points next

这将指向何处

We have built a representation where geometry means similarity, trained with contrastive/triplet/InfoNCE, and made it searchable at scale. But notice what every encoder in this track has assumed: a CNN backbone, whose locality and translation-equivariance (lesson 06) are baked-in inductive biases. Those biases are a blessing on small data and a ceiling on large data — they hard-code that nearby pixels matter most and distant relations are reached only by stacking layers. The next question is whether a more flexible architecture, one that can relate any patch to any other from layer one and that scales its capability with data rather than fighting a fixed prior, can do better. Lesson 13 introduces the Vision Transformer: patchify the image, and let self-attention learn the relations a convolution assumes.

我们已经构建了一个几何即相似度的表示,用对比/三元组/InfoNCE 训练它,并让它能在大规模下被检索。但请注意这门课里每一个编码器都假定了什么:一个 CNN 骨干网络,它的局部性和平移等变性(第 06 课)是内建的归纳偏置。这些偏置在小数据上是福音、在大数据上却是天花板——它们把"邻近像素最重要、远距离关系只能靠堆叠层数才够得着"写死进了架构。下一个问题是:一个更灵活的架构,一个从第一层就能把任意图块与任意其他图块关联起来、并且随数据扩展其能力而非与固定先验搏斗的架构,能否做得更好。第 13 课将引入视觉 Transformer(Vision Transformer):把图像切成图块,让自注意力去学习卷积所假定的那些关系。

Takeaway要点
Metric learning trains an encoder so that distance encodes similarity. L2-normalize and the dot product becomes cosine — magnitude drops out, thresholds become meaningful. Shape the space with contrastive (pairs + margin), triplet max(0, d(a,p) − d(a,n) + m), or InfoNCE (one positive vs many negatives, temperature τ) — and feed them hard negatives, because most pairs teach nothing; ArcFace/CosFace add an angular margin for face verification. At scale, exact NN is O(N·d) and dies, so use ANN indexes — IVF, PQ, HNSW — that trade recall for latency. Grade ranking with recall@k and retrieval mAP, but set a separate threshold for verification. This contrastive machinery is the root of lessons 14 and 15. 度量学习训练一个编码器,使得距离编码相似度。做 L2 归一化,点积就变成余弦——幅值被消掉,阈值变得有意义。用对比损失(样本对 + 间隔)、三元组 max(0, d(a,p) − d(a,n) + m),或 InfoNCE(一个正样本对抗多个负样本,温度 τ)来塑造这个空间——并给它们喂难负样本,因为大多数样本对什么也教不了;ArcFace/CosFace 为人脸验证加上一个角度间隔。在大规模下,精确最近邻是 O(N·d)、会失效,所以用 ANN 索引——IVF、PQ、HNSW——它们用召回率换延迟。用 recall@k检索 mAP 评判排序,但为验证单独设定一个阈值。这套对比机制是第 14、15 课的根基。

Interview prompts

面试题