☰
深度学习人脸对比实战:特征提取与相似度判定全流程解析
2026/10/14 7:07:17 网站建设 项目流程

人脸对比,说白了就是判断两张人脸图片是不是同一个人。这个需求在安防、考勤、相册分类、支付验证这些场景里到处都是。我最早接触深度学习人脸识别实战项目时,就是从“用模型对比两张人脸图片”入门的,后来发现这个方向虽然看起来只是“算一个相似度”,但里面牵扯到模型结构、特征表示、图像预处理、阈值标定一整条链路,随便哪一环出了问题,结果都会离谱到怀疑人生。这篇文章就把这套流程完整走一遍:从加载一个训练好的特征提取模型开始,讲清楚怎么对两张人脸图提取特征、比较相似度、做最终判定,同时把我在实际项目里踩过的坑一并写出来。不管你是刚入门人脸识别的学生,还是想把手头模型快速验证一下的工程师,都可以照着跑通。

1. 人脸对比这个任务,先搞清楚它到底在解决什么问题

1.1 人脸识别、人脸验证与对比任务的关系

很多人一听到“人脸识别”,第一反应是训练一个分类网络,把输入图片分类成某个人的ID。这种思路在封闭集里有效,但在真实业务里非常受限。你想一下,如果公司有10万员工,识别系统就要输出10万个ID;明天新来一个人,分类头就得重新训练,这显然不现实。

所以实际产品里大量的需求其实是“人脸验证”,也就是两两对比:手里有两张图,判断是不是同一个人。人脸识别系统一般也是由这个对比模块作为底层能力,再往上去做检索(在海量底库里找谁)、做聚类(自动整理相册)等任务。人脸对比的核心逻辑不是“认出这张脸是谁”,而是“这两张脸的特征像不像”。特征足够接近,就认为是同一个人;差距很大,就判定为不同人。

我第一次做这个项目时,也差点陷进“先训练分类模型”的坑里。后来换成了“特征提取 + 相似度度量”的思路,才发现这才是通用性强、落地也方便的正路。

1.2 为什么选深度学习,而不继续用传统图像特征

在人脸特征提取这件事上,我见过不少传统方案:局部二值模式(LBP)、方向梯度直方图(HOG)、甚至直接用像素差值。它们简单、可解释性强,但遇到光照变化、姿态变化、戴口罩、年龄增长这些实际情况时,效果会快速劣化。原因也很直观:这些手工设计的特征对人脸“高级语义”的表达能力太弱,它们看到的是纹理和梯度,而不是“五官结构”和“身份信息”。

深度学习方案则完全不同:网络在海量人脸样本上通过损失函数自动学习出高判别性的特征表示。简单来说,模型被训练成一种“人脸编码器”,输入一张人脸图,输出一个固定长度的向量。训练时会让同一个人的不同照片对应向量尽量靠近,不同人的照片对应向量尽量拉远。等模型训练收敛后,你不需要关心“它到底记住了什么”,只需要把向量拿出来做比较就行。

我自己的体会是:传统特征适合做快速原型验证,但真正要上线、要扛住真实场景,还是直接用深度特征提取模型更省心。而且现在有很多训练好的模型权重可以直接加载,不需要我们从零训练,学习成本一下降了不少。

1.3 本次最小闭环需要准备的三样东西

这个项目虽然名字里有“实战”,但搭一个最小可运行的闭环并不需要太多东西。我总结下来,有三样是必须的:

  1. 一个训练好的特征提取模型(也就是人脸编码器),可以是自己训练的,也可以是现成的预训练权重。它负责把人脸图变成特征向量。
  2. 一套规范的图像预处理函数。人脸图在喂给模型之前,要经过人脸对齐、缩放、归一化等操作。这一步非常影响最终效果,很多人结果差就是栽在这里。
  3. 相似度计算与阈值判定逻辑。拿到两个特征向量后,计算余弦相似度,再跟阈值比较,输出“是同一人”还是“不是同一人”。

如果你手里已经有训练好的模型权重,那这篇文章的重点就是后面两样:把预处理做正确,把判定逻辑做合理。如果你的目标是训练自己的模型,也可以先用别人的权重把整条链路跑通,再回来替换自己的模型。

2. 核心方案选型:特征提取模型、向量维度与相似度度量

2.1 特征提取模型到底长什么样

特征提取模型本质上就是一个卷积神经网络,常见的是各种残差网络或轻量网络。我们一般把它叫作Backbone,也就是主干网络。输入是一张标准化的人脸图片,经过一系列卷积、池化、激活操作之后,在最后的全连接层或Embedding层输出一个固定长度的向量。

我拿一个非常直观的例子说明:假设输入一张112×112或224×224的彩色人脸图,模型内部不断把空间尺寸缩小、把通道数增加,最终得到一个形如[1, 128]或[1, 512]的输出向量。这个向量就是这张人脸的“数字签名”。

选型的时候主要看两个因素:精度和算力。服务端有GPU,可以上ResNet50这类比较大而强的结构;移动端或嵌入式设备,就要选MobileFaceNet、GhostNet这类轻量结构,牺牲一点精度换速度。文章后面的示例代码里,我用的是ResNet18的结构,因为它足够简单,能把流程讲清楚,也方便替换成其他结构。

2.2 特征维度:128、256还是512

特征向量的维度是一个值得想一下的参数。128维比较省空间,计算也快,适合做大规模检索;256维算一个折中;512维在很多人脸模型里比较常见,信息容量更大,但存储和计算成本也更高。

有一点要提醒新手:特征维度不是越高越好。维度太高,如果训练数据不够充分,反而可能学到很多跟身份无关的噪声;维度太低,又装不下足够的人脸区分信息。实际上你用别人训练好的模型时,基本不用纠结这个问题——模型输出多少维,就用多少维。文章示例代码里我设置成128,只是为了演示方便。

2.3 相似度度量:为什么我选余弦相似度

拿到两个特征向量后,怎么衡量它们“像不像”?

最常用的方法是余弦相似度,也就是计算两个向量夹角的余弦值。公式是cos(a,b) = (a·b)/(|a||b|),结果落在[-1,1]之间,越接近1说明方向越一致,人脸也就越像。之所以常用,是因为人脸识别模型在训练时经常会把特征约束在一个超球面上,而余弦相似度衡量的恰恰是方向一致性,跟向量的长度无关。

这也解释了为什么推理时一般要先做L2归一化再算相似度。如果不做归一化,两张图片可能因为亮度、对比度差异导致特征向量长度不同,而这个长度信息往往并不是“身份信息”。把向量缩放到单位长度后再比较,能让结果更加稳定。我刚开始跑这个项目时偷懒没做归一化,结果明明是同一个人,相似度忽高忽低,后来加了这一步才算正常。

3. 动手前的环境准备和图像预处理,决定你后面省不省心

3.1 运行环境与依赖安装

这个项目用到的依赖不多,主要是PyTorch、TorchVision、OpenCV、NumPy和Pillow。我在一台普通的Linux服务器上跑,Python版本是3.8以上都没问题。安装命令很简单:

pip install torch torchvision opencv-python pillow numpy

如果你的机器有NVIDIA GPU并且已经装好CUDA,PyTorch会自动用GPU跑;没有GPU的话,小模型用CPU推理也能接受,就是速度慢一些。我在没有GPU的笔记本上跑过ResNet18,一张图大概几百毫秒,做演示够用,生产环境肯定还是建议上GPU或做推理优化。

3.2 预处理流水线:对齐、缩放、归一化

很多新手会把一张生活照直接丢给模型,然后抱怨效果差。我可以负责任地告诉你:预处理环节对人脸识别结果的影响,往往比模型选型的影响还大。我踩过这个坑,所以现在习惯把所有预处理拆成四步:

第一步是人脸检测。先从原图里把人脸区域框出来。如果输入图片已经是从公开数据集里裁好的标准人脸图,这一步可以跳过。真实场景里,图片里可能有多张脸,需要先做人脸检测,把每张脸单独截出来。

第二步是对齐。这一步最容易被忽略,也最影响结果。人脸对齐是通过眼睛、鼻子、嘴角这些关键点,把人脸仿射变换到一个标准模板上,让脸“摆正”。不摆正就直接输入模型,同一个人拍歪一点的照片,特征差距可能比不同人还大。你可以简单理解为:模型训练时看到的人脸都是基本摆正的样子,你在推理时给个歪脸,它当然懵。

第三步是缩放。把对齐后的人脸图统一缩放到模型要求的尺寸,常见的是112×112或224×224。文章示例代码用224×224,因为这个尺寸正好适配TorchVision自带的ResNet结构。

第四步是归一化和格式转换。一般做法是把像素值从[0,255]映射到[-1,1],比如(pixel - 127.5) / 127.5,然后把图像的通道顺序从HWC转成NCHW,变成模型需要的张量格式。别小看这一步,像素范围不对,模型提取出来的特征会完全乱掉。

3.3 输入尺寸的坑:112还是224

人脸识别模型对输入尺寸非常敏感。有的专用人脸模型用的是112×112,有的用96×96,也有用224×224的。TorchVision自带的ResNet系列默认是224×224,所以文章示例代码先统一用224。如果你换了别人训练好的模型,一定要去看它的模型说明或者代码里的预处理配置,按它的要求来,不要自己想当然。

我曾经为了图省事,把别人训练好的模型拿过来,没改预处理尺寸就直接跑,结果同一人对比只有0.2的相似度,看着像陌生人。后来查了半天才发现输入尺寸不对,改回来之后分数马上就正常了。这种事多踩几次,你就会养成先查模型输入规格的习惯。

4. 核心实现:模型加载、特征提取和相似度判定的完整流程

4.1 搭建人脸编码模型并加载权重

先看模型搭建部分。我用TorchVision里的ResNet18作为主干,把最后的全连接层替换成一个128维的输出,这样就得到一个简化版的人脸编码器。真实项目中,你可以直接替换成自己训练好的模型或其他人脸模型。

import torch import torch.nn as nn import torchvision.models as models embedding_dim = 128 class FaceEncoder(nn.Module): def __init__(self, dim=embedding_dim): super(FaceEncoder, self).__init__() backbone = models.resnet18(weights=None) backbone.fc = nn.Linear(backbone.fc.in_features, dim) self.backbone = backbone def forward(self, x): return self.backbone(x) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = FaceEncoder().to(device) state = torch.load("face_encoder.pth", map_location=device) model.load_state_dict(state) model.eval()

这里有个细节一定要提醒:加载完权重后必须调用model.eval()。因为神经网络里的BatchNorm和Dropout在训练和推理时的行为不一样,不切到eval模式,模型输出的特征会不稳定,同一个人输入两次可能得到两个差异很大的向量。很多人说模型效果差,其实不是模型问题,是忘了这一行。

4.2 图像预处理与特征提取函数

接下来是图像预处理函数。假设输入图片已经裁好并且只含一张正脸,我们做缩放、归一化和格式转换三步:

import cv2 import numpy as np def preprocess(image_path, input_size=224): img = cv2.imread(image_path) if img is None: raise ValueError(f"读取图片失败: {image_path}") img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (input_size, input_size)) img = img.astype(np.float32) img = (img - 127.5) / 127.5 img = img.transpose((2, 0, 1)) img = np.expand_dims(img, axis=0) return torch.from_numpy(img).to(device)

注意cv2.cvtColor(img, cv2.COLOR_BGR2RGB)不能省。OpenCV读进来的图通道顺序是BGR,而PyTorch模型接收的是RGB顺序。很多新手在这里出错,图片看起来颜色没区别,但特征已经不对了。

特征提取函数更简单一些:

def extract_embedding(image_path): x = preprocess(image_path) with torch.no_grad(): emb = model(x) emb = nn.functional.normalize(emb, p=2, dim=1) return emb

torch.no_grad()是推理时节省内存的标准操作,不需要计算梯度。L2归一化对应前面说过的稳定性考虑,这一步我建议保留。

4.3 两张人脸图片的相似度计算

现在到了整个项目最核心的一步:计算两两对比分数。PyTorch自带余弦相似度接口,直接调用就行:

def compare_faces(path1, path2): emb1 = extract_embedding(path1) emb2 = extract_embedding(path2) sim = torch.cosine_similarity(emb1, emb2).item() return sim if __name__ == "__main__": score = compare_faces("person_a_1.jpg", "person_a_2.jpg") print(f"相似度: {score:.4f}") threshold = 0.5 print("判定结果:", "同一个人" if score >= threshold else "不同人")

这样跑完之后,你会得到一个具体分数。分数本身没有绝对意义,必须配合阈值来解释:分数高于阈值就判为同一个人,低于阈值就判为不同人。关键是这个阈值怎么选。

4.4 判定阈值:别拍脑袋,用数据说话

我第一次做阈值时就是随手填了个0.5,后来才发现不同模型、不同数据,阈值差得远。正确做法是用验证集来标定:

先准备两类样本对:正样本对是同一个人的不同照片,负样本对是不同人的照片。数量不用太多,各一百对就够。把所有样本对输入模型算相似度,然后把两类的分数分布画出来,通常会出现两个近似钟形的曲线。选两条曲线交叉点附近作为阈值,是比较直观的做法。

举个例子,假设同一个人照片的相似度集中在0.65到0.92,不同人的相似度集中在-0.1到0.42,那阈值放在0.5左右就挺合理。

阈值高低的选择还跟业务强相关,并不是“准确率最高”就行。我给一个表格说明:

| 阈值倾向 | 误把陌生人放进来 | 误把本人拒之门外 | 适用场景 | | 高阈值 | 概率低 | 概率高 | 支付验证、门禁等安全优先场景 | | 低阈值 | 概率高 | 概率低 | 海量检索、相册聚类等召回优先场景 |

所以阈值是个业务参数,前期可以靠分布图选,后期会根据误识率和拒识率持续调整。

4.5 换成批量场景:底库特征提前算好

如果你要做的是“一个人脸去库里找最像的人”,逐张跑模型太浪费了。正确做法是提前把底库里所有人脸都过一遍模型,得到特征向量存起来。运行时只需要计算当前人脸特征和底库向量之间的相似度。

保存单个特征很简单:

import numpy as np emb = extract_embedding("person_a_1.jpg") np.save("person_a_1.npy", emb.cpu().numpy())

批量比对时,把所有底库向量堆成一个二维矩阵,当前向量和它做矩阵乘法,就能一次性得到所有相似度。由于底库特征已经做了归一化,矩阵乘法其实就是批量余弦相似度计算。这个思路在真实项目里非常常用,相当于用空间换时间。

5. 常见问题排查与调优心得,让对比结果更稳

5.1 效果异常排查速查表

这部分是我最想分享的,因为很多问题不是理论不清楚,而是实操中反复踩坑。我整理了一张排查表,覆盖我遇到过的几类典型情况:

| 现象 | 可能原因 | 解决办法 | | 同一个人相似度偏低 | 人脸没对齐、预处理不一致 | 统一对齐流程和缩放尺寸 | | 不同人相似度偏高 | 模型没切eval模式、特征没归一化 | 加model.eval(),推理时L2归一化 | | 加载权重报错 | 权重由DataParallel保存,带module前缀 | 加载前去掉前缀或先包一层DataParallel | | CPU推理非常慢 | 模型结构过大 | 换轻量Backbone或转ONNX | | 同图输入两次分数不同 | 忘了eval模式或特征未归一化 | 加eval模式,算相似度前归一化 |

这里重点解释两条。同一个人相似度低,我遇到最多的情况就是人脸没有对齐。生活照里人脸可能是侧着的、低头的,甚至有一半被遮挡,直接喂给模型,特征自然对不上。另一个高频原因是不同批次图片的预处理不一致,比如一张缩放到112×112,另一张缩放到224×224,那模型看到的根本是不同尺寸的世界,分数自然不稳定。

不同人相似度偏高,大部分原因在模型层面。一旦忘记调model.eval(),BatchNorm层的统计量会混乱,模型输出的特征质量就会下降。特征不做归一化也有影响,极端情况下向量长度差异会盖过方向相似度,导致不同人的分数虚高。

5.2 三步定位法:把问题缩小到具体环节

当效果不正常时,我习惯用一套三步定位法,能很快判断问题出在预处理、模型还是后处理。

第一步,验证同一张图输入两次的相似度。如果结果不是约等于1,说明模型或者预处理不稳定。优先检查是否加了model.eval(),以及输入数据是否每次都会变化。

第二步,打印特征的数值范围。正常特征经过L2归一化后,每个维度应该在-1到1之间,向量的整体长度为1。如果特征里出现大量0或者极端值,很可能是数据预处理出了问题,例如像素没有正确归一化。

第三步,换一对“应当非常像”的正样本试试。比如同一个人同一张照片的两次裁剪,或者同一个人的两张高清正面照。如果这都判不对,问题多半在模型权重或输入尺寸不匹配,而不是阈值设置。

这套方法帮我省过很多时间。与其看全流程瞎猜,不如用可控的小实验把问题边界缩小。

5.3 数据使用与隐私底线

人脸数据非常敏感,做实验时我习惯遵守一条底线:优先使用公开数据集或者自己的照片,不要随手拿身边同学、同事、路人的照片来跑测试,更不要在没有授权的情况下把人脸数据传到公网服务上。另外,演示用的样本不要包含可识别个人身份的真实姓名,代码和日志里也不要留敏感信息。这一点在工程上同样重要,很多公司对用户人脸数据有严格的安全要求。

如果你要在自己的公众号或博客里贴实验截图,建议对原图打码或使用虚拟生成的人脸图片。人脸特征本身也属于个人敏感信息,保护它的意识应该从第一个实验就开始养成。

5.4 让结果更稳的实用小技巧

最后分享几个我在实际调试中学到的小技巧,都是常规文档里不太会写的内容。

第一,多模板平均。如果同一人在底库里有多张照片,不要只选一张做特征,而是把多个特征向量取平均,再重新归一化。这样得到的“参考特征”比单张更稳健,能抵抗光线、表情、角度带来的随机波动。我在一个考勤项目里这么改之后,误拒率明显下降。

第二,推理时统一用RGB而不是BGR。这个问题很隐蔽,不少模型在训练时用的就是RGB,你如果随意切换通道顺序,分数会异常。最好固定一套预处理代码,不管谁接手都不要改动。

第三,模型效果差的时候,先怀疑数据预处理,再怀疑模型结构。很多时候我们把问题归结为“模型精度不够”,其实根本不是。我先跑一遍同图对比和正样本对比,如果这两个都能过,再去看模型能力也不迟。

第四,如果要把流程做成服务,可以考虑把模型导出成ONNX格式。这样推理时不用依赖原始训练框架,CPU上也能快不少。导出的时候记得把输入输出的尺寸固定下来,否则有些部署框架会处理不好动态变化。

我自己在这个人脸对比项目里收获最大的,不是掌握了某个模型,而是真正理解了“特征提取 + 相似度判定”这套范式。很多人觉得人脸识别很神秘,跑通一遍之后你会发现,它的核心逻辑其实非常清晰:预处理把图摆正,模型把脸变成向量,阈值决定最终判断。后面如果再有人问我怎么入门,我一定会让他先按这个流程跑通一次,把预处理和阈值标定这几个环节摸透,比背模型论文有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询