
简介这份ArcFace-PyTorch工程包面向人脸识别入门与实战开发者以ArcFace算法为核心提供从数据加载、模型搭建、损失函数到评估指标的可运行训练与测试流程。压缩包共20个文件体积约36KB内含数据目录及11个Python脚本分别负责数据集加载、ResNet骨干网络、ArcFace指标计算、Focal Loss和训练/测试入口另有依赖清单、工程配置、README使用说明、网络结构示意图便于按模块索引。工程内置checkpoints与config等模块方便保存权重和调整超参数按README安装依赖后可一键执行人脸识别训练与测试也可替换数据或调整参数复现实验效果。该资源已有1550人学习下载适合学生、算法工程师作为课程设计、毕业设计或算法复现的参考资料。1. 人脸识别项目实战 ArcFace-PyTorch从损失函数到门禁设备能用的完整链路人脸识别和人脸检测是两件事。检测回答“脸在哪”识别回答“这张脸是谁”。工程落地上识别环节绕不开特征嵌入模型而过去五年开源人脸识别基准和商用 SDK 选得最多的损失函数就是 ArcFace。它把分类任务改造成在超球面上做角度间隔划分让模型输出的 512 维向量直接用于余弦比对——门禁机、考勤系统、会员注册里的 1:1 验证和 1:N 检索都吃这一套。这篇文章按一条能直接复现的路径展开先自己实现 ArcFace 损失函数并讲清 s 和 m 的边界再补齐训练前的人脸对齐、数据增强与采样策略然后给出主干网络和一个能收敛的训练配置最后落到 ONNX 导出、阈值标定和边缘部署细节。中间穿插一些不跑一遍看不出来的问题比如损失函数为什么需要数值保护、权重衰减设错为什么会掉点、分类头显存是在哪里爆掉的。适合两类人一类是手里已经有人脸检测模型、想补全识别链路的工程师另一类是跑通过普通分类但没端到端训过人脸 Embedding想低成本试错的团队。后面的代码都以 PyTorch 为主环境版本差异不影响主体逻辑。2. ArcFace 损失函数的 PyTorch 实现从公式到 s 与 m 参数边界2.1 为什么 Softmax 之后还要加角度边距先理解余弦空间的决策边界普通 Softmax 分类器对输入特征 x 和分类权重 W 计算内积 W_jᵀx展开就是 |W_j||x|cos(θ_j)。如果把特征和权重都做 L2 归一化内积只剩 cos(θ_j)分类边界变成了“哪一类中心在角度上离我最近”。问题随之而来Softmax 只要求把不同类别推开并不强制同类聚拢到中心附近而人脸识别的测试身份永远不在训练类别里模型必须保证同一身份的特征在超球面上紧凑才能让后端的余弦相似度可靠。ArcFace 的做法是在正确类别对应的角度上动手。对标签 y 对应的夹角 θ_y正常分类器优化 cos(θ_y)ArcFace 优化 cos(θ_y m)。m 是一个固定角度边距相当于给正确类别加了一道更难的题模型不仅要把 θ_y 压小还得压得比其它类别小出至少 m 的余量。损失写成L -log( e^{s·cos(θ_y m)} / ( e^{s·cos(θ_y m)} Σ_{j≠y} e^{s·cos(θ_j)} ) )s 是缩放因子负责把归一化后接近 [-1,1] 的余弦值放大到 softmax 友好的数值区间。ArcFace 和 CosFace 的差别也在这里CosFace 直接减余弦值ArcFace 先加角度再取余弦。在人脸基准上ArcFace 对难样本的类内压缩更明显这也是它后来成为开源人脸识别模型最常用训练损失的原因。2.2 ArcFace 损失函数的 PyTorch 实现与数值保护import math import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, in_features512, out_features10000, s64.0, m0.50): super().__init__() self.in_features in_features self.out_features out_features self.s s self.m m # 每个训练身份对应超球面上的一个中心向量 self.weight nn.Parameter( torch.FloatTensor(out_features, in_features) ) nn.init.xavier_normal_(self.weight) # 预计算 cos(m) / sin(m)以及角度接近 pi 时的保护值 self.cos_m math.cos(m) self.sin_m math.sin(m) self.th math.cos(math.pi - m) self.mm math.sin(math.pi - m) * m def forward(self, x, labels): # 特征与权重都归一化内积严格等于余弦相似度 x F.normalize(x) w F.normalize(self.weight) cosine F.linear(x, w) # (batch, num_classes) sine torch.sqrt((1.0 - cosine.pow(2)).clamp(0, 1)) # cos(theta m) cos(theta)cos(m) - sin(theta)sin(m) phi cosine * self.cos_m - sine * self.sin_m # 角度接近 pi 时合角公式产生错误梯度换成线性近似 phi torch.where(cosine self.th, phi, cosine - self.mm) one_hot torch.zeros_like(cosine) one_hot.scatter_(1, labels.view(-1, 1), 1) # 只对正确类别使用 phi其余类别保持 cosine最后统一缩放 logits (one_hot * phi (1 - one_hot) * cosine) * self.s return F.cross_entropy(logits, labels)代码里最关键的是不用反三角函数算 θ而是用合角公式展开 cos(θm)避免 θ 接近 0 时 arccos 的导数趋近无穷。torch.where那行是数值保护当 θ 超过 π - m合角公式算出的 cos(θm) 会先增后减梯度方向就错了改成线性项cosine - self.mm保证函数单调递减。推理时模型只留主干和 Embedding 层ArcFaceLoss 是训练专用模块接在主干后面即可。2.3 s 和 m 参数怎么定两个超参的取值边界与调法参数推荐范围取值偏小取值偏大s32~64论文默认 64类别多时温度过低训练初期梯度弱余弦差异被过度放大对噪声敏感m0.3~0.6默认 0.5类内压缩不足验证集 TAR 偏低训练不容易收敛损失震荡甚至出现 NaN实际项目里需要动默认值主要看两个信号。一是训练身份数不足两千的小数据集m 建议从 0.3 起步。0.5 意味着每个身份至少要在一批样本里被压出半弧度的余量身份少、样本也少时模型撑不住。二是数据本身质量差比如监拍图、大面积遮挡m 大于 0.4 时损失会因为难样本比例过高而震荡。这时先尝试把 s 从 64 提到 128观察一轮而不是急着改 m。判断 s、m 是否匹配有一个简单方法在验证集上把同一身份对和不同身份对的余弦分数分别统计画两条分布曲线。两条曲线分界清晰说明边距设置对路重叠严重先查对齐和数据质量调参通常是最后一步。3. 训练管线怎么做五点对齐、数据增强与类别均衡采样3.1 输入为什么必须是 112×112 五点对齐图以及检测器怎么选ArcFace 和后续一批开源人脸模型在训练、推理时几乎都用同一套输入规范人脸经过相似变换映射到 112×112双眼中心、鼻尖、左右嘴角对齐到固定坐标。原因很直接Embedding 的方向必须对姿态差异稳定。不做对齐直接裁检测框同一个人的头部只要转动十几度特征向量在超球面上可能偏移几十度余弦相似度直接跌破阈值。数据管线第一步是用检测器拿人脸框和五个关键点。我一般用 RetinaFace 或 SCRFD两者都输出协调一致的五个关键点SCRFD 在边缘设备上速度更有优势RetinaFace 在质量不稳定的小图上更稳。公开训练集的标签通常直接给五点坐标注意是浮点坐标不要在传给变换矩阵前取整否则对齐误差会变成一种额外的数据噪声。3.2 仿射对齐的 PyTorch/OpenCV 实现与参数说明import cv2 import numpy as np # 112x112 标准五点参考坐标ArcFace 训练管线的通用约定 REF_5_POINTS_112 np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) def align_face(img, landmarks): # landmarks: shape (5, 2)顺序必须与参考坐标一致 # estimateAffinePartial2D 求相似变换只含旋转、平移、等比缩放 M, inliers cv2.estimateAffinePartial2D( landmarks, REF_5_POINTS_112, methodcv2.LMEDS ) if M is None: # 关键点退化时退回最小二乘避免直接抛异常 M, _ cv2.estimateAffinePartial2D(landmarks, REF_5_POINTS_112) aligned cv2.warpAffine(img, M, (112, 112), flagscv2.INTER_LINEAR, borderValue(0, 0, 0)) return aligned, M注意这里必须用estimateAffinePartial2D而不是estimateAffine2D。前者只允许相似变换不会产生斜切变形门禁抓拍以正面为主相似变换足够。如果硬用 6 自由度仿射侧面角度的人脸会被拉出不自然的宽度Embedding 稳定性明显下降。warpAffine的插值训练阶段用INTER_LINEAR就够推理时若目标分辨率低于输入建议换INTER_AREA。关键点被遮挡导致变换估计失败时常见做法是丢弃这一帧而不是硬对齐。错误对齐样本在困难样本里的占比一旦上去会拖低整个模型的识别精度。3.3 人脸识别数据增强对照表与同身份采样策略增强方式对人脸识别的影响建议亮度/对比度/色彩抖动低风险提升光照鲁棒性常用水平翻转无风险收益高必加注意关键点映射要同步翻转随机擦除 Cutout低风险模拟遮挡门禁、监控场景建议加随机旋转中高风险限制在 ±5°先对齐再旋转随机裁剪中风险只能小范围裁避免切到眉眼采样策略比增强更容易被忽视。普通 DataLoader 随机采样时一个 batch 里可能每个身份只出现一张图ArcFace 分类头相当于每步只看到每个类别一个样本中心向量的更新噪声很大。我一般用按身份分组的采样器保证每个 batch 里同一身份至少出现 2 到 4 张图。实现上写一个自定义GroupSampler按身份分桶桶内打乱后再按桶拼接最后切 batch。这个改动在 batch size 512 时验证集得分能拉开 0.5 到 1 个百分点的差距。数据部分还直接影响实验节奏。MS1MV2 这一档的百万级数据集单卡训练 ResNet50 一轮就要十几分钟一个完整实验往往十几小时。我的建议是先在 10% 子集上把整条 Pipeline 跑通确认损失趋势正常再上全量数据否则排查数据加载问题会浪费一整轮训练周期。4. 主干网络与训练配置ResNet50 从零开始训练到收敛4.1 带 BN 的 512 维 Embedding 层设计ArcFace 复现里最常见的主干是 ResNet50最后接 512 维 Embedding。设计上和 ImageNet 分类有几点不同Embedding 前要有 BNEmbedding 后不要接 ReLU因为后面直接做 L2 归一化。import torch import torch.nn as nn from torchvision.models import resnet50 class FaceEncoder(nn.Module): def __init__(self, embedding_size512): super().__init__() base resnet50(weightsNone) # 剔除 avgpool 和 fc保留 conv1 到 layer4 self.features nn.Sequential(*list(base.children())[:-2]) self.bn1 nn.BatchNorm1d(2048, affineFalse) self.fc nn.Linear(2048, embedding_size) self.bn2 nn.BatchNorm1d(embedding_size, affineTrue) def forward(self, x): x self.features(x) # (batch, 2048, 7, 7) x x.mean([2, 3]) # 全局平均池化 x self.bn1(x) x self.fc(x) x self.bn2(x) return xbn1设置affineFalse只做标准化不做缩放目的是让特征进入分类头前保持稳定分布bn2保持affineTrue给最终 Embedding 一个可学习的尺度偏移。训练时 ArcFaceLoss 内部还会再做一次F.normalize所以bn2之后不要加 ReLU否则归一化前的特征分布会被人为截断。用 ImageNet 预训练权重初始化主干在中小数据集上能明显加快早期收敛数据量足够大身份数超过十万时从零训练也能收敛且往往更贴合人脸分布。PyTorch 2.x 下可以顺手用torch.compile包一层主干多数卡上能拿到 1.2 到 1.5 倍的训练加速不影响损失计算逻辑。4.2 训练超参表与混合精度设置配置项推荐值说明输入分辨率112×112训练与推理必须一致batch size256~512小于 128 时 BN 统计不稳定优化器SGDmomentum0.9nesterovTrue比 Adam 系更容易收敛初始学习率0.1batch 256增大 batch 同步放大 lr上限 0.4权重衰减5e-4太小泛化差太大 TAR 掉点学习率计划cosine 或 step[10, 18, 24]总训练轮数 25~35混合精度torch.autocast GradScaler分类头显存和计算量都明显下降混合精度的标准写法如下核心是 loss 必须经过 GradScaler 反向传播scaler torch.cuda.amp.GradScaler() for img, label in loader: img, label img.cuda(), label.cuda() with torch.autocast(device_typecuda, dtypetorch.float16): embedding model(img) loss arcface_loss(embedding, label) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() optimizer.zero_grad()不使用 ImageNet 预训练而从零开始时初始学习率 0.1 配合 batch 256 是一个比较稳妥的起点。如果前几百步 loss 就直接冲高震荡先把 lr 降到 0.05而不是动 s 和 m。分类头的显存开销等于 batch size × 类别数 × 4 字节遇到 Glint360K 这种百万级类别的数据batch 512 时分类头约 700MB比主干还占显存。这类场景常用做法是采样类别训练或把分类头切成多段做异步更新普通项目很难踩到这一步但要心里有数。4.3 不收敛时的排查顺序与显存控制训练不收敛时我按固定顺序排查。第一看输入对齐图是否出现大量黑边、五点顺序是否与参考坐标一致。第二看标签labels 是否落在 [0, num_classes) 区间类别数和分类头维度是否匹配这是最常见的隐性 bug。第三看损失函数输入特征向量是否经过了归一化数值范围是否异常损失计算前打印一次 embedding 的均值和方差能立刻区分是模型问题还是损失函数问题。如果 loss 在正常下降但验证集 TAR 不涨问题通常在数据侧某个身份样本太少或者增强强度过大把身份特征破坏了。一个很有效的检查是把训练集里同身份样本的余弦相似度拉出来看如果最小值低于 0.3说明数据或增强出了问题调 lr 和 m 都救不回来。5. 部署到门禁机ONNX 导出、1:N 阈值标定与边缘细节5.1 用 ROC 定相似度阈值而不是拍脑袋选 0.5门禁机上 1:N 识别在工程上的核心不是模型精度而是阈值怎么定。取 0.5 是常见误用因为阈值必须和误识率绑定。标准做法是在验证集上构造同一身份对和不同身份对算余弦分数后画 ROC 曲线在目标 FAR 下取阈值。import numpy as np from sklearn.metrics import roc_curve def pick_threshold(pos_scores, neg_scores, far1e-4): y np.array([1] * len(pos_scores) [0] * len(neg_scores)) score np.concatenate([pos_scores, neg_scores]) fpr, tpr, thresholds roc_curve(y, score) idx int(np.argmin(np.abs(fpr - far))) return thresholds[idx]安全等级要求高的门禁场景FAR 通常压到 1e-4 或 1e-5对应阈值会比直觉上的 0.5 高不少。阈值标定必须用部署同款的推理结果重新做一遍因为 FP16 或量化后的向量分布会漂移直接沿用 FP32 阈值会造成误识率超标。5.2 边缘设备上最容易忽略的三个细节第一导出 ONNX 时只需导出 FaceEncoder不要带 ArcFaceLoss并设置动态 batchdummy torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy, face_encoder.onnx, input_names[input], output_names[embedding], dynamic_axes{input: {0: batch}}, opset_version17 )第二后端无论用 C#OpenCvSharp 集成、Java 还是 C都建议直接接 ONNX Runtime保持算法和业务解耦。第三注册模板不要只用一帧。门禁设备常见做法是连续抓 3 到 5 帧先做质量筛选比如用 Laplacian 方差过滤模糊帧、检查双眼可见度再把这若干帧的 Embedding 取平均后归一化作为底库模板。底库注册完成后再用部署环境的 FP16 推理重测一遍阈值确认 FAR 达标这个动作能避免上线后误识率飙高。本文还有配套的精品资源点击获取