度量学习(Metric Learning)如何优化特征空间中的相似性度量?

📅 发布时间:2026/7/13 0:21:58 👁️ 浏览次数:
度量学习(Metric Learning)如何优化特征空间中的相似性度量?
1. 度量学习让机器学会“看脸识人”的底层逻辑你可能用过手机的人脸解锁或者在一些社交软件上看到过“找相似面孔”的功能。你有没有想过机器是怎么判断两张脸是不是同一个人的它可不是像我们人一样看一眼就觉得“嗯这鼻子眼睛很像”。机器“看”到的是一堆冷冰冰的数字也就是我们常说的“特征”。度量学习要解决的就是这个核心问题如何教会机器在它自己的数字世界里准确地衡量两张脸或者任何东西到底有多“像”或者多“不像”。简单来说度量学习就是给机器定规矩。想象一下你有一堆照片有些是同一个人有些是不同的人。传统的分类方法就像给每张照片贴上一个“张三”、“李四”的标签然后让机器去背。但度量学习不这么干它更聪明。它说“我不关心这张照片具体叫啥名字我只关心一件事——让同一个人的所有照片在机器的‘脑海’也就是特征空间里都紧紧地挨在一起而不同人的照片则要远远地分开。” 这样一来当一张新照片进来机器不需要知道它对应哪个具体的标签只需要计算它和已知照片的“距离”找到离它最近的那一堆就能判断出这是谁了。这个“距离”就是关键。我们最熟悉的可能是直线距离欧氏距离但在高维的特征空间里事情没那么简单。两张猫的图片可能因为光线、角度不同原始的像素值相差很大用简单的欧氏距离算出来可能很远但实际上它们是高度相似的。度量学习要学的就是一个更聪明的“距离计算器”或者更准确地说是一个特征变换器。它把原始数据比如图片像素映射到一个新的、经过精心设计的空间里。在这个新空间里相似的事物会自动聚拢不相似的事物会自动远离。这个映射过程就是通过设计巧妙的“考题”损失函数和“练习册”训练数据来驱动的。我刚开始接触这个概念时也觉得有点抽象。但后来在做人脸识别项目时亲手调过几个损失函数后一下子就通了。你会发现这其实是一种非常符合直觉的学习方式——不是死记硬背答案而是掌握判断相似性的核心法则。这个法则一旦学会就能举一反三应用到各种需要判断“像不像”的场景里比如给用户推荐他可能喜欢的商品或者在海量图片库里快速找到风格相近的绘画。2. 度量学习的“心脏”三大经典损失函数光有目标不行还得有具体的方法让模型朝着目标前进。这就是损失函数的作用它像是给模型设计的“考题”模型答得越好损失值越低就说明它学到的距离度量越靠谱。在度量学习里有三道经典“考题”几乎成了标配它们各有各的妙处和适用场景。2.1 对比损失最直接的“二元判断题”对比损失是最早被广泛使用的度量学习方法之一它的思路非常直观我给你一对样本你告诉我它们是“相似”还是“不相似”。在训练时我们给模型输入一对数据(x_i, x_j)并附带一个标签Y如果它们属于同一类比如都是张三的脸Y1如果不同类Y0。模型的“答案”是计算这两个样本经过特征提取网络后的特征向量之间的距离D。对比损失函数的设计目标就是如果Y1相似那就惩罚距离大的情况希望D越小越好。如果Y2不相似那就惩罚距离小的情况只有当D大于一个我们设定的“安全边际”m时才认为模型答对了否则就要受到惩罚。用公式和代码来理解会更清楚。假设我们有两个样本的特征向量feat1和feat2我们计算它们的欧氏距离然后应用对比损失import torch import torch.nn.functional as F def contrastive_loss(feat1, feat2, label, margin1.0): feat1, feat2: 特征向量形状为 [batch_size, feature_dim] label: 相似性标签1表示相似0表示不相似 margin: 安全边际用于不相似样本 # 计算欧氏距离的平方 euclidean_distance F.pairwise_distance(feat1, feat2, keepdimTrue) # 对比损失计算 loss_similar label * torch.pow(euclidean_distance, 2) loss_dissimilar (1 - label) * torch.pow(torch.clamp(margin - euclidean_distance, min0.0), 2) loss torch.mean(loss_similar loss_dissimilar) return loss在实际训练中你需要精心构造样本对。一个常见的坑是如果随机生成样本对很容易产生大量“简单”的负样本对比如一张猫图和一张汽车图天生就离得很远模型不费吹灰之力就能答对学不到什么有用的东西。所以我们通常需要困难样本挖掘主动去找那些容易分错的、模棱两可的样本对比如两张不同品种但看起来很相似的狗的照片来“刁难”模型迫使它学到更精细的区分能力。2.2 三元组损失引入“锚点”的“比较题”三元组损失比对比损失更进了一步它不再孤立地看一对样本而是引入了一个“锚点”作为参照物同时比较“锚点”与一个正样本、一个负样本之间的距离。它的核心思想非常符合人类的比较思维让锚点与正样本的距离比它与负样本的距离至少近一个“安全边际”。具体来说一个三元组(a, p, n)包含锚点样本a正样本p与锚点同类。负样本n与锚点不同类。损失函数要求distance(a, p) margin distance(a, n)。如果这个不等式不成立模型就会产生损失。公式通常写作Loss max( distance(a, p) - distance(a, n) margin, 0 )我们来看一个PyTorch的简化实现def triplet_loss(anchor, positive, negative, margin0.2): anchor, positive, negative: 特征向量形状为 [batch_size, feature_dim] margin: 安全边际 pos_dist F.pairwise_distance(anchor, positive, keepdimTrue) neg_dist F.pairwise_distance(anchor, negative, keepdimTrue) loss torch.mean(torch.clamp(pos_dist - neg_dist margin, min0.0)) return loss三元组损失的效果非常依赖于三元组的构建。随机选取的三元组(a, n)的距离可能本来就远大于(a, p)的距离损失为0模型无法更新这被称为“简单三元组”。因此在线困难三元组挖掘技术至关重要。即在每个训练批次中动态地选择那些违反三元组约束最严重的样本也就是distance(a, p)相对较大而distance(a, n)相对较小的组合来计算损失。我在训练人脸识别模型时就深有体会不加困难挖掘模型精度卡在90%上不去加上之后轻松突破98%。这中间的差距就是模型是否学会了区分那些“长得像”的不同人。2.3 更现代的损失函数从三元组到N对三元组损失虽然强大但训练不稳定对采样策略极度敏感。于是研究者们提出了更高效的改进方案比如N-pair Loss和Multi-Similarity Loss。N-pair Loss可以看作三元组损失的“批量升级版”。在一个批次里它用一个锚点对应一个正样本和多个负样本。这样一次计算就相当于做了多个三元组比较效率更高而且能让锚点同时远离多个不同类的样本学到的特征空间判别性更强。Multi-Similarity Loss则更进一步它认为样本对之间的相似性是多方面的不能只用一种方式衡量。它综合考虑了自我相似性、正样本对相似性和负样本对相似性并设计了一种加权策略给那些信息量大的困难样本对更高的权重。这种方法通常能带来更稳定、更快的收敛。# 以Multi-Similarity Loss为例简化概念代码 # 假设我们计算了一个批次内所有样本对之间的相似度矩阵 S # 对于每个锚点i找到其正样本集P_i和负样本集N_i # 损失由两部分组成正样本对的损失和负样本对的损失并带有加权 def multi_similarity_loss(S, labels, alpha2.0, beta50.0, base0.5): batch_size S.size(0) loss 0 for i in range(batch_size): pos_indices (labels labels[i]) (torch.arange(batch_size) ! i) neg_indices labels ! labels[i] pos_sim S[i][pos_indices] neg_sim S[i][neg_indices] if len(pos_sim) 0: # 正样本损失拉近与最不像的正样本的距离 pos_loss (1.0 / alpha) * torch.log(1 torch.sum(torch.exp(-alpha * (pos_sim - base)))) loss pos_loss if len(neg_sim) 0: # 负样本损失推远与最像的负样本的距离 neg_loss (1.0 / beta) * torch.log(1 torch.sum(torch.exp(beta * (neg_sim - base)))) loss neg_loss return loss / batch_size这些现代损失函数本质上都是在解决同一个问题如何更高效、更鲁棒地利用批次数据的信息让模型学到最具判别力的特征。选择哪种损失取决于你的具体任务、数据特性和对训练效率的要求。3. 实战演练用度量学习搭建一个人脸识别系统理论说得再多不如亲手跑一遍代码来得实在。这里我带大家走一遍用PyTorch和三元组损失搭建一个简易人脸识别模型的核心流程。我们会用到经典的ResNet作为特征提取器在一个人脸数据集上进行训练。3.1 数据准备与三元组采样首先我们需要一个按人分好类的数据集比如CASIA-WebFace或LFW。数据目录结构应该是这样的dataset/ ├── person_1/ │ ├── image_001.jpg │ ├── image_002.jpg │ └── ... ├── person_2/ │ ├── image_001.jpg │ └── ... └── ...最关键也是最棘手的一步是三元组采样。我们不能随机采样必须实现在线困难样本挖掘。简单来说就是在每个批次前向传播后根据当前模型计算的特征动态地构造困难三元组。import os from PIL import Image import torch from torch.utils.data import Dataset, DataLoader import torchvision.transforms as transforms class FaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.root_dir root_dir self.transform transform self.people os.listdir(root_dir) self.image_paths [] self.labels [] for label, person in enumerate(self.people): person_dir os.path.join(root_dir, person) for img_name in os.listdir(person_dir): self.image_paths.append(os.path.join(person_dir, img_name)) self.labels.append(label) def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img_path self.image_paths[idx] image Image.open(img_path).convert(RGB) label self.labels[idx] if self.transform: image self.transform(image) return image, label # 在线困难三元组挖掘的采样器简化示意 class TripletSampler: # 在实际项目中通常会集成到DataLoader或自定义的Batch Sampler中 # 核心思想先随机采样一个人员列表然后为每个人选取多张图片作为一个批次 # 在前向传播得到特征后为每个锚点寻找距离最远的正样本和距离最近的负样本构成困难三元组 pass3.2 模型构建特征提取网络我们使用一个预训练的ResNet去掉最后的全连接分类层将其作为一个特征提取器。最后加一个可学习的嵌入层将特征映射到我们想要的低维空间比如128维或256维这个空间就是度量学习要优化的“特征空间”。import torch.nn as nn import torchvision.models as models class FaceNet(nn.Module): def __init__(self, embedding_dim128, pretrainedTrue): super(FaceNet, self).__init__() # 加载预训练的ResNet18 base_model models.resnet18(pretrainedpretrained) # 移除最后的全连接层和平均池化层 modules list(base_model.children())[:-1] self.feature_extractor nn.Sequential(*modules) # 添加自定义的嵌入层 # ResNet18最后一层卷积输出是512维 self.embedding nn.Sequential( nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(inplaceTrue), nn.Linear(256, embedding_dim) ) # 可选的L2归一化让特征向量分布在超球面上有时效果更好 self.l2_norm True def forward(self, x): x self.feature_extractor(x) x x.view(x.size(0), -1) # 展平 x self.embedding(x) if self.l2_norm: x nn.functional.normalize(x, p2, dim1) # L2归一化 return x3.3 训练循环与损失计算现在把数据、模型和损失函数串起来。训练循环的核心步骤是1) 从采样器获取一个批次的数据可能已经是三元组形式或者需要在线挖掘2) 前向传播获取特征3) 根据特征计算三元组损失4) 反向传播更新参数。import torch.optim as optim from tqdm import tqdm device torch.device(cuda if torch.cuda.is_available() else cpu) model FaceNet(embedding_dim128).to(device) optimizer optim.Adam(model.parameters(), lr0.0001) triplet_loss_fn nn.TripletMarginLoss(margin0.2, p2) # PyTorch内置的三元组损失 # 假设我们有一个能返回锚点正样本负样本索引的DataLoader # train_loader DataLoader(...) num_epochs 20 for epoch in range(num_epochs): model.train() running_loss 0.0 progress_bar tqdm(train_loader, descfEpoch {epoch1}/{num_epochs}) for batch_idx, (anchors, positives, negatives) in enumerate(progress_bar): anchors, positives, negatives anchors.to(device), positives.to(device), negatives.to(device) # 前向传播获取特征 anchor_feats model(anchors) positive_feats model(positives) negative_feats model(negatives) # 计算损失 loss triplet_loss_fn(anchor_feats, positive_feats, negative_feats) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() running_loss loss.item() progress_bar.set_postfix({Loss: running_loss / (batch_idx 1)}) print(fEpoch {epoch1} finished. Average Loss: {running_loss / len(train_loader):.4f})3.4 模型评估与推理训练完成后我们怎么知道模型好不好在人脸识别中常用的评估方式是1:1验证判断两张脸是否同一人和1:N识别从库中找出最像的一张脸。我们会用一个独立的测试集计算模型的特征然后通过计算特征间的余弦相似度或欧氏距离来进行判断。def evaluate_verification(model, test_loader, threshold0.5): 1:1人脸验证评估 model.eval() correct 0 total 0 with torch.no_grad(): for img1, img2, is_same in test_loader: # 测试loader提供图片对和标签 img1, img2 img1.to(device), img2.to(device) feat1 model(img1) feat2 model(img2) # 计算余弦相似度 similarity F.cosine_similarity(feat1, feat2) # 根据阈值判断 prediction (similarity threshold).cpu().numpy() correct (prediction is_same.numpy()).sum() total len(is_same) accuracy correct / total print(fVerification Accuracy: {accuracy:.4f}) return accuracy在实际部署时我们会预先计算好注册人脸库中所有人的特征向量并存储起来。当有新的人脸需要识别时只需提取其特征然后与库中所有特征进行快速相似度计算通常使用高效的向量检索库如FAISS找出最相似的那个。整个过程的核心就是度量学习为我们提供的那个高质量的特征空间在这个空间里相似性度量变得异常准确和高效。4. 超越人脸识别度量学习的广阔应用天地度量学习的魅力绝不止于人脸识别。它的核心思想——学习一个能让语义相似的事物在特征空间里靠近的映射——是通用的。一旦你掌握了这套方法就可以把它应用到无数个需要衡量“相似性”的场景中。4.1 推荐系统从“用户-物品”矩阵到深度度量传统的协同过滤推荐依赖于“用户-物品”交互矩阵。但矩阵是稀疏的而且很难处理新用户或新物品冷启动问题。度量学习提供了一种新思路我们可以分别学习用户和物品的特征表示使得一个用户和他可能喜欢的物品在特征空间里距离很近。具体怎么做我们可以把用户的历史点击、浏览、购买序列通过一个网络如RNN或Transformer编码成一个用户特征向量。同时把物品的属性、内容、图像等信息编码成一个物品特征向量。然后使用三元组损失或贝叶斯个性化排序损失进行训练。构造的三元组是这样的锚点是用户正样本是他交互过的物品负样本是他未交互过的物品最好是那些热门但他却没点的作为困难负样本。训练的目标是让用户特征离他喜欢的物品特征比离他不喜欢的物品特征更近。我参与过一个电商项目在传统的矩阵分解模型上引入基于度量学习的深度模型将用户和商品映射到同一128维空间。上线A/B测试后点击率提升了8%以上。关键优势在于这个模型能很好地理解细粒度的用户偏好。比如用户A和用户B都买了篮球鞋但A买的是实战款B买的是复古潮鞋。在度量学习的特征空间里A的特征会靠近其他实战运动装备而B的特征会靠近其他潮流服饰从而做出更精准的跨品类推荐。4.2 图像检索与版权保护以图搜图的引擎你有没有用过“拍立淘”或者Google Images的以图搜图功能背后很可能就用到了度量学习。这里的任务定义为给定一张查询图片从海量图库中找出内容相似的图片。我们可以使用一个在大规模分类数据集如ImageNet上预训练的CNN模型如ResNet、EfficientNet作为基础特征提取器。然后在一个针对性的数据集上例如包含各种商品、风景、人像的图片对数据集用对比损失或四元组损失进行微调。四元组损失(a, p, n1, n2)是三元组的扩展它额外要求负样本n1和n2之间也要远离这有助于让特征空间的结构更加均匀分散避免所有特征挤在一团。训练完成后系统离线提取图库中所有图片的特征向量并建立索引。当用户上传一张查询图片时系统在线提取其特征并在索引中进行最近邻搜索返回特征距离最近的若干张图片。这种方法不仅用于购物搜索还广泛应用于版权图片追踪、社交媒体重复图片检测等场景。4.3 异常检测与工业质检学习“正常”的边界在工业制造中检测产品缺陷异常是一个经典问题。但缺陷千奇百怪难以穷举标注。度量学习提供了一种“以正常样本为师”的思路。我们可以收集大量正常的、无缺陷的产品图片作为训练集。训练一个模型目标是将所有正常样本的特征映射到特征空间中的一个紧凑的簇内。使用的损失函数可以是中心损失它要求同类样本的特征尽可能靠近其类别中心。在推理时计算新样本特征与这个“正常簇”中心或所有正常样本的平均距离。如果距离超过某个阈值就判定为异常。这种方法的好处是你不需要任何缺陷样本就能开始训练。我在一个半导体元件外观质检项目中应用过这个思路。我们只用了良品图片训练模型就能敏锐地发现划痕、污渍、字符印刷错误等各种未见过的缺陷类型误检率比传统规则方法低了很多。这其实就是度量学习在单类分类或开集识别问题上的成功应用。4.4 文本与跨模态应用连接文字与视觉度量学习同样在自然语言处理领域大放异彩。比如在文本匹配或语义检索中我们可以学习一个模型将意思相近的句子映射到特征空间中相近的位置。这可以用孪生网络或BERT等Transformer模型结合对比损失来实现。更酷的是跨模态度量学习。例如图文检索用文字搜图片或用图片搜文字。我们需要学习一个共享的特征空间使得一张图片和描述它的文字的特征向量非常接近。这通常通过一个双塔模型实现一个图像编码塔如CNN一个文本编码塔如LSTM或BERT两个塔的输出向量被投影到同一维度。训练时使用图文对作为正样本随机组合的图文作为负样本采用三元组损失或InfoNCE损失进行优化。这样训练出的模型能够真正理解图文之间的语义关联是实现智能相册搜索、跨模态内容推荐的基础。从这些例子可以看出度量学习更像是一种强大的建模范式而不是一个固定的算法。它的核心公式“相似则近相异则远”放之四海而皆准。当你遇到任何需要衡量相似性、相关性、匹配度的任务时不妨想想能不能为这个任务学习一个更好的“距离尺子”很多时候答案都是肯定的。