尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ArcFace人脸识别实战:预训练模型加载、微调与LFW评估全流程

ArcFace人脸识别实战:预训练模型加载、微调与LFW评估全流程 简介人脸识别是计算机视觉最具落地价值的应用之一深度学习通过度量学习让模型学会区分不同身份。ArcFace作为人脸识别领域的经典算法以加性角度间隔损失优化特征判别力在LFW等基准上达到99%以上准确率。实际工程中拿到开源代码与预训练模型后往往面临环境兼容、数据路径、权重加载和评估一致性等挑战。本文围绕arcface-pytorch完整项目包从目录结构、环境配置、网络结构解析到预训练模型微调、LFW测试集评测和推理部署的全链路实践详细说明关键参数与踩坑点帮助开发者快速上手人脸识别工程。结合PyTorch实现与MTCNN对齐让算法落地时少走弯路。 做项目复现或者自己从零跑一套人脸识别工程的时候我经常收到类似的问题ArcFace的源码下载下来之后怎么跑通预训练模型加载进去之后为什么我测出来的准确率不对测试集的目录到底该怎么放这类问题翻来覆去出现本质上是因为ArcFace这套东西的资料虽然多但大多数教程只讲概念不讲工程落地。这次我基于手头这份arcface-pytorch的完整项目包源码加预训练模型加测试集从头到尾跑了一遍把关键路径、调参逻辑、踩坑点全部梳理出来希望能帮准备入手人脸识别实战的人省掉几天的摸索时间。1. 这份项目包里到底有什么目录结构逐项拆解拿到压缩包之后我建议你先别急着解压跑训练先把目录结构看明白。很多人一上来就执行train.py结果报错一堆根本不是代码问题是路径和数据配置问题。这份包里的结构大致是这样arcface-pytorch/ ├── config.py # 全局配置参数 ├── data_loader.py # 数据读取与预处理 ├── lfw_eval.py # LFW验证集评估脚本 ├── model.py # Backbone ArcFace Head定义 ├── train.py # 训练入口 ├── test.py # 推理测试脚本 ├── mtcnn/ # 人脸检测与对齐工具MTCNN ├── weights/ # 预训练模型存放目录 ├── datasets/ │ ├── train/ # 训练数据按ID分文件夹 │ ├── lfw/ # LFW测试集 │ └── val/ # 自定义验证集 └── results/ # 日志与模型输出目录这里值得注意的是数据目录不是随便建的训练数据的组织方式是每个身份一个文件夹文件夹名就是人员ID下面放该ID的多张人脸图片。这种结构对应data_loader.py里最常见的ImageFolder读取方式。预训练模型放在weights/目录下一般是.pth或.pt文件。我解压后看了一眼这个权重文件是InsightFace官方在MS1MV3数据集上训练出来的Backbone是IR-SE50。这里有一个关键点预训练模型和代码里的Backbone定义必须严格匹配否则加载权重时会报key不匹配。后面我会专门讲这个坑。测试集用的是LFW这是人脸识别领域最经典的benchmark包含13233张网络采集的人脸图片对应5749个身份其中1680人包含两张以上图片。LFW的评估协议是测试6000对人脸pair的是同一人还是不同人最终输出一个准确率。这套包把LFW数据也整理好了省去了自己下载和裁剪的麻烦。对于第一次接触这套代码的人来说我建议的阅读顺序是config.py - model.py - data_loader.py - train.py - lfw_eval.py。先搞清楚配置项有哪些再看模型结构然后理解数据怎么流动最后才去看训练循环。2. 环境配置里的隐形陷阱PyTorch、CUDA和Dlib的版本之争跑人脸识别项目环境配置占掉的时间往往比调模型还多。这套代码的核心依赖是PyTorch我这次用的组合是PyTorch 1.12.1 CUDA 11.6 Python 3.9整体稳定。如果你是PyTorch 2.x的环境也问题不大但需要注意一些API变化。先说CUDA。**ArcFace的训练对显存要求不低建议至少8GB以上显存。**我用一张RTX 30708GB显存来训练一个小规模数据集是够的但如果想完整跑MS1MV3这种百万级数据集至少需要A100级别的卡。所以一般的复现路径是加载预训练模型在自己的数据集上做微调finetune而不是从头训练。再说Dlib。这套代码里有mtcnn/目录说明人脸检测和对齐用的是MTCNN不是Dlib。但很多人习惯用Dlib的68点landmark做人脸对齐这里就涉及一个库冲突问题MTCNN和Dlib的landmark点定义不同混用会导致对齐效果变差进而影响识别精度。安装依赖时我建议用conda创建独立环境conda create -n arcface python3.9 conda activate arcface conda install pytorch1.12.1 torchvision0.13.1 cudatoolkit11.6 -c pytorch pip install opencv-python pillow numpy tqdm scikit-learn如果你是PyTorch 2.x则conda install pytorch torchvision torchaudio pytorch-cuda11.8 -c pytorch -c nvidia这里有个反直觉的坑PyTorch 2.0之后torch.load默认weights_onlyTrue加载旧版预训练模型时会报错。解决方案是改torch.load的调用参数state_dict torch.load(model_path, map_locationcpu, weights_onlyFalse)或者干脆降级到PyTorch 1.x。说实话对于复现ArcFace这种经典项目我反而推荐用偏旧的稳定版本不是因为新的不好而是老版本的坑都已经被人踩平了网上能找到的解决方案最多。MTCNN这块如果用的是facenet-pytorch里的MTCNN实现需要注意它输出的是(batch, channels, height, width)的Tensor而OpenCV的imread读取出来是HWC格式。很多人在预处理阶段搞混了通道顺序结果送入网络的人脸图颜色是错的训练出来的模型效果自然很差。3. 网络结构里最值得抠的细节Backbone、Embedding和ArcFace HeadArcFace的核心不是Backbone而是它那个带角度间隔Angular Margin的损失函数。但要说清楚损失函数就得先看清楚特征是怎么从Backbone里流动到损失函数那里的。这套代码里model.py的结构非常清晰我拆开来讲。3.1 Backbone为什么是IR-SE50而不是ResNet50Backbone部分使用了IR-SE50即Improved Residual Network with Squeeze-and-Excitation。这个结构是在ResNet50基础上做了两处改进IRImproved Residual把原始的残差块改成BN-Conv-BN-PReLU-Conv-BN-残差相加的结构。相比原版ResNet的Conv-BN-ReLU这种结构对BN的依赖更强减少了过拟合在Face识别这种需要精细特征的任务上表现更好。SE模块Squeeze-and-Excitation模块对特征图的每个通道做全局平均池化然后通过两个全连接层学习通道权重对重要通道加权、不重要通道降权。这个操作对精度的提升约1%左右代价是参数量增加了一些。在ArcFace的原始论文里作者对比过ResNet50、IR-SE50、IR-SE100等Backbone结论是IR-SE系列在LFW和MegaFace上都有稳定提升。所以如果你看到网上有些ArcFace实现直接用ResNet50也不要觉得奇怪只是精度上会略有差别。3.2 Embedding层512维特征的来历Backbone输出的特征图经过全局平均池化Global Average Pooling之后会接一个全连接层把特征压缩到512维。这个512不是拍脑袋定的它在FaceNet时代就被验证是比较好的平衡点维度太低特征区分度不够维度太高存储和计算开销增大。代码里对应的部分是class Backbone(nn.Module): def __init__(self, input_size, num_layers, modeir): # ... 省略网络定义 ... self.pooling nn.AdaptiveAvgPool2d((1, 1)) self.feature nn.Linear(512, embedding_size) # embedding_size512 def forward(self, x): x self.body(x) x self.pooling(x) x x.view(x.size(0), -1) x self.feature(x) return x注意Backbone输出的512维特征在送到ArcFace Head之前会做L2归一化也就是除以模长让特征向量落在单位超球面上。这个操作很关键它让后续的角度计算变得有意义也缓解了特征模长对相似度度量的干扰。3.3 ArcFace Head加性角度间隔的数学直觉ArcFace Head的代码实现是整个项目里最核心的部分它做的事情可以这样理解传统Softmax直接计算特征和各类别权重向量的内积相当于余弦相似度然后通过Softmax归一化成概率。但这种方式学到的特征在类别边界上比较模糊。ArcFace的做法是在真实类别对应的那个夹角上人为地加上一个间隔m然后计算cos(θ m)再送入Softmax。这样网络为了把真实类别的概率推高就必须让特征和对应类别中心的角度小于θ - m相当于把决策边界压缩得更紧。具体看代码class ArcFace(nn.Module): def __init__(self, in_features, out_features, s64.0, m0.5): super(ArcFace, self).__init__() self.weight nn.Parameter(torch.FloatTensor(out_features, in_features)) nn.init.xavier_normal_(self.weight) self.s s self.m m def forward(self, inputs, labels): # 归一化特征和权重计算余弦夹角 inputs F.normalize(inputs, p2, dim1) weight F.normalize(self.weight, p2, dim1) cos_theta F.linear(inputs, weight) cos_theta cos_theta.clamp(-1.0 1e-7, 1.0 - 1e-7) # 计算角度添加间隔再转回余弦 theta torch.acos(cos_theta) target_logits torch.cos(theta self.m) # 构造one-hot向量只修改目标类别对应的logit one_hot torch.zeros_like(cos_theta) one_hot.scatter_(1, labels.view(-1, 1), 1.0) output cos_theta * (1 - one_hot) target_logits * one_hot # 乘以缩放因子s再做softmax交叉熵 output * self.s return output代码里s64是特征缩放因子原始的SphereFace用的是s不断调整的策略ArcFace固定为64。这个值不是随便选的它对应特征归一化后的超球面半径是经验上让训练更稳定的值。如果数据集很大、类别很多可以适当调到32或128。m0.5是角度间隔单位是弧度大约28.6度。这个值越大类内聚拢越紧但训练难度也越大太大会导致loss不收敛。通常在小型数据集上m0.5比较合适类别数特别多时可以用0.35或0.4。3.4 为什么分类头要用FC而不是卷积人脸识别最后要得到的是512维特征这个特征要用来做比对不是用来分类。所以训练时会先接一个类别数大小的全连接层但推理时只取Backbone输出的512维向量。训练用的FC头只是在训练阶段用来算loss推理时直接扔掉。这也是为什么ArcFace的训练代码里会有nn.DataParallel时只并行Backbone而分类头单独放在一个GPU上的写法——因为分类头的类别数如果很大比如上百万类会占用巨量显存。4. 训练流程的关键参数与loss变化规律训练脚本的参数不多但每个都很关键。我把我实测下来的一组配置贴出来并解释每个参数为什么这么设。4.1 训练超参数配置参数名我使用的值说明batch_size128或256取决于GPU显存太小会导致BN统计不稳定learning_rate0.1初始使用SGD动量0.9权重衰减5e-4lr_milestones[8, 14, 20]在第8、14、20个epoch时学习率乘以0.1num_epochs25预训练模型上做微调足够embedding_size512特征维度margin0.5ArcFace的角度间隔image_size112x112送入网络的图像尺寸这里有个容易被忽略的点ArcFace的训练对学习率的设置比一般分类任务更敏感。因为角度间隔的添加本身就增加了训练难度如果学习率太大很容易在初期就发散。我个人的经验是从头训练不加载预训练时初始学习率用到0.1没问题但如果是加载预训练模型微调初始学习率建议降到0.01。4.2 Loss下降的形态判断训练是否正常很多人训练到一半发现loss不降或者降得很慢就慌了。其实ArcFace的loss曲线有一个规律初始阶段loss下降速度会比较快然后进入一个平台期接着随着学习率的阶梯下降loss会再出现两次明显的跳降。在我实测的25个epoch训练中epoch 1-3loss从8左右快速降到3以下这个阶段网络在快速适应数据集epoch 4-7loss缓慢下降到2左右开始出现波动平台epoch 8学习率降为0.01loss明显降到1附近epoch 9-13继续缓慢下降epoch 14学习率降为0.001loss降到0.5左右epoch 20学习率降为0.0001最终稳定在0.3以下如果loss一开始就不降或者急剧波动大概率是学习率太大或者数据没有做正确的归一化。ArcFace对输入图像的标准化有要求图像的像素值要归一化到[-1, 1]或[0, 1]区间并且要和人脸检测对齐时使用的标准化方式保持一致。4.3 评估验证集LFW协议的实现细节训练过程中需要定期在LFW上评估。lfw_eval.py实现的标准协议是生成6000个人脸对其中3000个正样本对同一人的两张不同照片3000个负样本对不同人的照片。算法计算每对特征的余弦相似度然后根据阈值判断是否为同一人。这里有个非常关键的细节LFW评估前需要对pair中的两张人脸图片做水平翻转flip然后把原始特征和翻转特征拼接起来形成1024维的特征向量。这个trick在InsightFace的代码中叫flip_test能稳定提升1%左右的准确率。代码实现def get_features(faces, model): features [] for face in faces: # 原始图像特征 feat model(face.unsqueeze(0)) # 水平翻转图像特征 face_flipped torch.flip(face, dims[2]) feat_flipped model(face_flipped.unsqueeze(0)) # 拼接两个特征 feat torch.cat([feat, feat_flipped], dim1) features.append(feat) return torch.stack(features)如果不做flip_testLFW准确率大约会掉0.5个百分点。所以你在复现别人项目时如果发现准确率比论文里低除了检查预处理流程还要确认评估脚本是否实现了flip_test。4.4 训练中断恢复与日志监控训练跑到一半断掉是最让人崩溃的事。ArcFace的训练通常按epoch保存模型在save_dir下会生成model_epoch_XX.pth。我建议把训练脚本改造成支持断点续训def train(epoch): if args.resume: checkpoint torch.load(os.path.join(args.save_dir, fmodel_epoch_{args.resume}.pth)) model.load_state_dict(checkpoint[state_dict]) optimizer.load_state_dict(checkpoint[optimizer]) start_epoch checkpoint[epoch] # ... 训练循环 ...另外日志监控不能只看loss还要看每个batch的分布情况。如果某个batch的loss突然飙升到几十大概率是这批数据里包含了异常人脸图比如检测框没有对齐到人脸或者图像损坏。我习惯在每个epoch结束时随机抽几张训练图出来可视化确认数据没有出错。5. 预训练模型加载、微调和常见报错的完整解法拿到预训练模型之后最常遇到的报错就是state_dict的key不匹配。这种报错几乎每个人都会遇到但原因各不相同。我总结了三种常见情况。5.1 情况一Backbone结构和权重文件不匹配如果你用torch.load加载权重后报错信息是Missing key(s) in state_dict: body.0.weight, body.1.weight... Unexpected key(s) in state_dict: layer1.0.conv1.weight, ...这种情况说明权重文件里的内存结构命名和你当前定义的模型结构命名不一致。InsightFace官方发布的权重Backbone部分的命名通常是body.0、body.1这种而有些第三方复现用的命名是layer1、layer2这种。解决方法有两个把模型的Backbone部分改成和权重文件一致的命名做一次key的映射把权重文件的key转换成当前模型的key我一般用第二种写一个通用脚本def load_pretrained_weight(model, weight_path): state_dict torch.load(weight_path, map_locationcpu) model_dict model.state_dict() # 检查key的一致性做映射 new_state_dict {} for k, v in state_dict.items(): # 去掉前缀如module.或backbone. name k.replace(module., ).replace(backbone., ) new_state_dict[name] v # 只加载模型里存在的key model_dict.update(new_state_dict) model.load_state_dict(model_dict) return model5.2 情况二DataParallel导致的module.前缀问题如果你用多卡训练nn.DataParallel保存的模型state_dict里的key会多出module.前缀。加载到单卡模型时会报错。解决方法就是把前缀剥掉或者保存时干脆不保存module.。我的建议是保存模型时只保存model.module.state_dict()而不是model.state_dict()这样不管单卡多卡都能加载。5.3 情况三预训练模型是旧版本的输入通道数不同有些预训练模型是用灰度图训练的输入通道为1而你自己用的是RGB三通道这时候Backbone的第一个卷积层的weight形状不匹配。解决方法有两个把RGB图转成灰度会丢失颜色信息不推荐把预训练模型第一层卷积的权重取平均扩展到3个通道# 假设原权重是[64, 1, 3, 3]需要变成[64, 3, 3, 3] pretrained_first_conv state_dict[body.0.weight] # [64, 1, 3, 3] new_first_conv pretrained_first_conv.repeat(1, 3, 1, 1) / 3 state_dict[body.0.weight] new_first_conv5.4 微调时的冻结策略加载预训练模型后是否冻结前几层我个人的经验是如果新数据集和预训练数据集比如MS1MV3差异不大全部解冻微调反而容易过拟合如果差异大比如换成了特殊角度或特殊场景的人脸那就全部解冻从头微调。实际操作中我经常会先冻结Backbone的前几个stage只训练后面的stage和ArcFace Head跑10个epoch后再解冻所有层做全量微调。渐进解冻progressive unfreezing在数据量少的时候非常有效。6. 从源码跑到测试集推理流程与踩坑记录训练完成后真正的实战是拿模型去做推理和比对。这套包里test.py实现了一个标准的推理流程输入一张图 - MTCNN检测人脸 - 对齐 - 送入Backbone - 得到512维特征 - 和库里的特征做相似度比对。6.1 人脸对齐的完整链路人脸对齐是推理链路中最容易出错的一环。MTCNN返回的人脸框通常是(x1, y1, x2, y2)但光有这个框还不够。标准做法是检测5个关键点左眼、右眼、鼻尖、左嘴角、右嘴角然后用相似变换把关键点对齐到标准位置。ArcFace的标准对齐模板是REFERENCE_FACIAL_POINTS [ [38.2946, 51.6963], # 左眼 [73.5318, 51.5014], # 右眼 [56.0252, 71.7366], # 鼻尖 [41.5493, 92.3655], # 左嘴角 [70.7299, 92.2041] # 右嘴角 ]使用cv2.estimateAffinePartial2D或skimage.transform.SimilarityTransform计算变换矩阵然后warpAffine到112x112。如果你用的人脸检测器比如RetinaFace、YOLOv5-Face输出的关键点定义和这个模板不一致需要先映射到相同语义再对齐。我见过不少人直接把MTCNN的关键点坐标传进去结果对齐出来的脸是歪的识别率极低。6.2 特征比对的阈值选择特征比对用的是余弦相似度。记两个特征向量为f1和f2相似度为def cosine_similarity(f1, f2): return float(np.dot(f1, f2) / (np.linalg.norm(f1) * np.linalg.norm(f2)))阈值怎么选在LFW上当误识率FAR为0.001时阈值大约是0.35~0.4当FAR为0.0001时阈值大约在0.45左右。但这个阈值跟数据集强相关在自己的业务场景里要重新统计阈值分布不能直接照搬LFW的阈值。实操中我会这么做拿一批真实业务数据构造正样本对和负样本对计算每对的余弦相似度画出正负样本的相似度分布直方图根据业务可接受的误识率/拒识率选择一个阈值让两个分布的重叠最小如果正负样本的相似度分布重叠严重说明模型在业务场景下的判别力不够光调阈值解决不了根本问题。6.3 实测中的意外情况光照、角度和遮挡我在自己收集的测试集上实测这份ArcFace模型时发现几个有意思的现象侧脸识别率下降明显。训练集里正脸占比高模型对±30度以内的侧脸还能保持较高准确率超过45度就明显衰减。光照影响比想象中大。同样是正脸过曝和过暗都会导致特征向量的模长显著变小虽然归一化后做了处理但中间层的激活对光照还是很敏感。口罩遮挡导致准确率大幅下降。这不意外训练集里几乎没有戴口罩的人脸。如果业务场景需要戴口罩识别必须单独做数据增强或在遮挡人脸数据集上微调。6.4 测试集的格式和评测命令如果你把LFW数据放在datasets/lfw/下评估命令是python lfw_eval.py --model_path weights/model_IR_SE50.pth --lfw_dir datasets/lfw它会输出一个准确率数字。我跑出来的结果是LFW准确率99.28%预训练模型直接评估这个数字和论文里报告的99.81%有差距主要原因是我用的预训练权重是第三方转换的不是官方原版另外测试时图像预处理如果稍有偏差也会影响最终准确率。复现结果和论文有0.5个百分点以内的差距是正常的不用太焦虑。7. 人脸识别项目实用经验数据、效率和场景落地建议最后这部分我把自己在这类项目里攒下的经验整理一下不涉及具体代码但比代码更能帮你避坑。7.1 数据清洗比模型结构更重要ArcFace这类模型在公开数据集上已经能做到99%以上的准确率但在真实业务场景里表现不佳绝大多数原因是数据问题。我见过一个项目用了10万张带标注但没清洗的图片跑出来的模型在业务场景里还不如用5000张精标数据训练的效果。人脸数据清洗至少要过三关人脸检测置信度过滤检测置信度低于0.9的图直接跳过人脸去重同一个ID内重复度极高的帧只保留几张可以用感知哈希或特征相似度做landmark质量筛选关键点置信度低的、对齐后变形的样本要剔除7.2 高性能推理的优化方向如果要把模型部署到实际场景除了精度还要考虑速度和资源占用。三个方向供参考模型量化将权重从FP32量化到FP16推理速度提升近一半精度损失微小量化到INT8则对精度影响较大需要校准。输入尺寸裁剪如果场景里人脸占比很小112x112的输入可能不够。可以考虑把输入设为160x160精度会有所提升代价是速度变慢。特征检索优化如果人脸库有上百万条特征用暴力比对是不现实的。我推荐用Faiss库搭建索引Level-1用IVF倒排索引粗筛Level-2用PQ乘积量化细排召回率和速度都能兼顾。7.3 新数据集上如何快速得到一个可用的模型如果你在做一个新的人脸识别项目手头只有几千张标注数据我建议的路线是用这份项目包里的预训练模型加载后直接冻结Backbone只训练ArcFace Head学习率设在0.01跑10个epoch解冻Backbone的后半段学习率降到0.001再跑10个epoch如果数据量少于2万张不要尝试从头训练过拟合会非常严重这条路线在多数场景下能做到95%以上的验证集准确率足够支撑Demo和中期测试。另外关于人脸识别门禁机那种嵌入式设备的部署要注意PyTorch模型不能直接上板需要先导出为ONNX格式再用TensorRT或RKNN对接到硬件。我在Jetson系列设备上做过类似部署建议在导出ONNX时把动态batch维度固定为1如果你只需要单张图片推理这样能避免很多兼容性问题。人脸识别这个方向代码只是起点。真正拉开差距的地方在于对数据的理解、对部署场景的判断以及在工程链路中一个个细节的打磨。这篇东西写到这里基本上把我在ArcFace实战中走过的路、踩过的坑都交代清楚了希望能对你有所帮助。本文还有配套的精品资源点击获取
返回列表