ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ArcFace人脸识别实战:从Softmax到角度间隔的工程实现

ArcFace人脸识别实战:从Softmax到角度间隔的工程实现 简介这是一套基于ArcFace的人脸识别实战项目适合具备一定Python与深度学习基础、希望从零搭建完整流程的开发者。项目将人脸图像映射到高维特征空间通过归一化与角度度量完成识别核心网络由多层卷积、池化及反向传播优化构成。压缩包内共20个文件以11个Python脚本为主覆盖模型定义、特征提取、损失函数、训练与测试等模块另含配置说明、依赖清单、README及PDF文档总大小仅36KB结构紧凑、可直接配套工程使用。目前已有1550人学习浏览验证了其实用性。借助该项目读者可快速理解ArcFace的训练与推理逻辑按README安装依赖后一键运行适合课程设计、算法复现或人脸识别入门实践。1. 为什么ArcFace比Softmax更适合开集人脸识别上个月帮朋友调人脸门禁他之前用Softmax训练的分类模型测试集准确率超过95%但换一批新用户注册后识别经常出错。问题出在Softmax只做“闭集分类”而人脸识别实际是“开集比对”需要特征在超球面上有更强的判别力。ArcFace在余弦空间上给目标类别加了一个角度间隔m让同一个人的特征更聚拢不同人的特征彼此推开。这个arcface-pytorch项目把完整训练和测试流程打包好了数据、配置、训练测试脚本都在拿来就能复现也适合在此基础上改自己的业务逻辑。2. 角度间隔的几何意义与项目模块拆解2.1 为什么角间隔比欧式间隔更稳人脸识别最常用的一类做法是让模型输出高维嵌入向量预测阶段用余弦相似度判断两张脸是否属于同一人。换句话说分类层后面加的损失函数决定了这个嵌入空间长什么样。经典的Softmax损失只要求每个类别分开并不会约束同类样本向一个中心收缩所以学到的特征在超球面上类内方差偏大。ArcFace的思路是把特征向量x和最后一个全连接层权重W都做归一化让二者的内积只取决于夹角θ。然后在目标类别对应的角度上追加一个间隔m损失函数变成L -log( exp(s·cos(θ_y m)) / ( exp(s·cos(θ_y m)) Σ_j≠y exp(s·cos θ_j) ) )其中s是特征尺度常用64m是角度间隔常用0.5。这个约束等价于在超球面上给每个类画出一个更紧凑的扇区训练时网络必须把同一身份的特征向中心挤不同身份之间至少要隔开m的角度。相比直接对特征加欧式距离惩罚角度间隔和预测时用的余弦相似度完全一致因此训练和推理的度量方式更匹配。2.2 项目结构与模块职责项目拆分非常清晰每个文件负责一个独立环节表里是核心部分文件/目录职责models/resnet.py骨干网络用于提取图像特征models/metrics.pyArcMarginProduct角度间隔分类头models/focal_loss.pyFocal Loss实现处理难样本dataset.py自定义Dataset扫描图片并生成标签config/config.py集中管理超参数方便切换实验train.py训练入口保存checkpointtest.py测试入口计算相似度和准确率checkpoints/存放训练好的模型权重data/训练集和测试集图片这里最核心的设计是models下把骨干网络、分类头、损失函数拆成独立模块。骨干网络只负责输出特征向量metrics.py负责在训练时把特征向量映射成带角度间隔的logitsfocal_loss.py负责计算梯度。这样替换backbone或者换损失函数时不需要动训练主流程。2.3 数据流从图像到嵌入向量训练阶段和测试阶段的数据流略有不同项目里也分别由train.py和test.py承担# 训练阶段 image - resnet backbone - embedding(512维) embedding - ArcMarginProduct - logits - FocalLoss # 测试阶段 image - resnet backbone - embedding(512维) embedding_normalized - 余弦相似度 - 是否同一人train.py只关心前者test.py则把embedding拿出来做相似度计算。训练时ArcMarginProduct的权重矩阵每一行是一个类别的中心向量和归一化后的embedding做点积得到余弦值测试时这个权重矩阵不再参与直接用embedding之间的距离做判定。3. 数据组织方式与config超参配置3.1 数据目录与标签生成人脸识别数据集最常见的组织方式是按人分文件夹每个人的所有图片放在同一目录下目录名就是身份ID。如果你用了公开数据集比如CASIA-WebFace或MS-Celeb-1M下载后基本都是这种结构。项目里的data目录也遵循这个约定。dataset.py的职责就是扫描这些文件夹把每张图片的路径和标签解析出来。import os import cv2 import torch from torch.utils.data import Dataset class FaceDataset(Dataset): def __init__(self, root_dir, transformNone): self.classes sorted(os.listdir(root_dir)) self.class_to_idx {cls: i for i, cls in enumerate(self.classes)} self.samples [] for cls in self.classes: cls_dir os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): self.samples.append((os.path.join(cls_dir, fname), self.class_to_idx[cls])) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (112, 112)) if self.transform: img self.transform(img) return img, label这段代码里root_dir指向data/train读取时按文件夹名排序生成从0开始的连续标签。cv2默认读取BGR格式需要先转成RGB因为ImageNet预训练模型的颜色通道顺序是RGB。resize到112x112是ArcFace论文中的标准输入尺寸如果想用更大分辨率如224需要同步调整backbone第一层的输入通道和后续全连接层维度。3.2 config.py里的关键超参数config是整个实验的“总闸”所有会影响收敛的变量都应该集中放在这里。打开项目的config/config.py常见配置项如下参数推荐值说明input_size112输入图像边长embedding_size512骨干网络输出的特征维度backboneresnet50可选resnet18/34/50/100margin0.5ArcFace角度间隔训练时最敏感的参数scale64.0余弦缩放因子过大或过小都会影响收敛batch_size128根据显存调整一般不低于64learning_rate0.1SGD初始学习率配合warmup使用milestones[10, 20, 30]学习率衰减的epoch节点num_classes按数据自动计算身份证ID数量这里margin和scale是ArcFace模型独有的超参数。margin太大会让目标类别的角度越过决策边界模型难以收敛scale太小会让logits幅度不足Softmax输出的概率分布过于平滑梯度变小。项目里把这两个参数放在config里就是为了方便做消融实验。3.3 数据增强与预处理细节人脸识别对数据增强比较敏感我一般会在训练时用随机水平翻转、颜色抖动、随机擦除但不会用随机裁剪太多因为人脸对齐后的图片本身已经比较规整。测试阶段只做归一化不做任何随机增强。归一化均值建议用[0.5, 0.5, 0.5]或ImageNet标准均值注意要和预训练权重匹配。from torchvision import transforms train_transform transforms.Compose([ transforms.ToTensor(), transforms.RandomHorizontalFlip(p0.5), transforms.ColorJitter(brightness0.1, contrast0.1), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) test_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ])这里的ToTensor必须放在Resize之后它会自动把HWC转成CHW并缩放到 [0,1]然后Normalize再缩放到 [-1,1]。如果你用了OpenCV做预处理要注意Tensor格式和NumPy数组的维度顺序差异这是训练时最容易出bug的地方。4. 训练主循环ArcMarginProduct与损失组合4.1 ArcMarginProduct的数值稳定实现models/metrics.py里的ArcMarginProduct是整个算法的关键它本质是一个带约束的全连接层。PyTorch里的常见实现方式是用Parameter保存类别中心矩阵然后在forward里逐个样本修改目标类别的logit值。import torch import torch.nn as nn import torch.nn.functional as F class ArcMarginProduct(nn.Module): def __init__(self, in_features, out_features, s64.0, m0.5): super().__init__() self.s s self.m m self.weight nn.Parameter(torch.FloatTensor(out_features, in_features)) nn.init.xavier_uniform_(self.weight) def forward(self, x, labels): # 归一化特征和权重 x F.normalize(x) W F.normalize(self.weight) # 计算余弦值 cos_theta F.linear(x, W) cos_theta torch.clamp(cos_theta, -1.0 1e-7, 1.0 - 1e-7) # 反解角度并加上间隔 theta torch.acos(cos_theta) target_logits torch.cos(theta self.m) # 只修改目标类别的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 return output * self.s这段代码里最容易被忽略的是torch.clamp。由于浮点误差cosine值可能略小于-1或略大于1不截断的话acos会产生NaN模型训练几个batch后loss直接变nan。one_hot用scatter_把目标类别的掩码置1然后只对目标logit加角度间隔其他类别logit保持原余弦值。最后统一乘s放大差异。4.2 训练主循环和优化器组合train.py里的训练循环并不复杂但要把model、metric的参数同时交给优化器否则ArcMarginProduct里的类别中心权重不会更新。下面是一个标准的训练片段for epoch in range(cfg.epochs): model.train() metric.train() for batch_idx, (inputs, labels) in enumerate(train_loader): inputs, labels inputs.cuda(), labels.cuda() features model(inputs) logits metric(features, labels) loss focal_loss(logits, labels) optimizer.zero_grad() loss.backward() optimizer.step() if batch_idx % 50 0: print(fEpoch {epoch} Batch {batch_idx} Loss {loss.item():.4f})这里的focal_loss可以换成nn.CrossEntropyLoss()但原项目选择了Focal Loss。人脸数据集里有些图片姿态极端、光线很差Focal Loss会给这类难样本更高的权重从而避免训练被大量简单样本主导。使用Focal Loss时要注意调整alpha和gamma这两个超参数一般来说alpha0.25、gamma2是默认起点。优化器我用SGD比较多初始学习率设置0.1配合CosineAnnealingLR或StepLR。如果换成Adam学习率要降到1e-4左右否则损失容易震荡。另一个容易踩的坑是weight_decay对ArcMarginProduct的权重矩阵最好也做L2正则否则会出现部分类别中心向量模长异常。4.3 测试脚本与最优阈值评估test.py不会走ArcMarginProduct它直接加载训练好的backbone权重输出512维归一化特征然后计算pair之间的余弦相似度。下面是一段简化的测试逻辑def extract_feature(model, img_tensor): model.eval() with torch.no_grad(): feat model(img_tensor) feat F.normalize(feat) return feat.cpu().numpy() # 计算一对图像的相似度 emb1 extract_feature(model, img1) emb2 extract_feature(model, img2) similarity np.dot(emb1, emb2.T).item()测试时需要定义“同一人”的判定阈值。LFW等标准数据集会提供正负pair可以用来画ROC曲线并选出EER点对应的阈值。工程上我一般多加一步把验证集里所有类内相似度和类间相似度分布画出来取两个分布交叉处附近的阈值这样比单纯依赖标准数据集更贴合实际场景。验证集指标计算方式TAR阈值下正确接受的正样本比例FAR阈值下错误接受的负样本比例EERTAR和FAR相等时的错误率4.4 训练过程的常见问题最容易出现的问题是loss为NaN。除了前面提到的acos截断还有一个典型原因是学习率过高尤其是加margin之后梯度范围变化较大建议SGD前5个epoch做线性warmup把学习率从0逐步升到目标值。第二个常见问题是GPU显存溢出当类别数达到几万时ArcMarginProduct的权重矩阵会非常大此时需要对类别中心做采样比如按batch内出现的正样本类别动态更新中心而不是维护全量类别矩阵。第三个问题是预热和衰减策略不匹配很多人直接把学习率设小结果模型收敛极慢训练几十个epoch准确率都上不去。5. 部署验证特征比对与阈值调优5.1 与人脸检测器组成完整链路实际业务里ArcFace模型前面通常还要接一个人脸检测器比如SCRFD或RetinaFace。检测器先输出人脸框和关键点坐标再用相似变换把人脸对齐到112x112然后才送进ArcFace。这个组合是目前人脸识别落地的标准流程。项目里的test.py只负责特征提取和比对但你在部署时需要把检测、对齐、归一化这三个环节串起来。如果不做对齐人脸角度差异会明显拉低识别效果即使ArcFace本身对角度有一定容忍度。5.2 用Embedding分布快速选定阈值训练好的模型有一个很实用的验证技巧随便挑10个人的测试图片提取全部embedding计算类内相似度和类间相似度的直方图。以我经验看类间相似度通常在0.3以下类内相似度在0.7以上中间就是一个比较理想的阈值区间。如果两个分布重叠严重说明模型还没收敛或者数据增强太弱。5.3 快速推理脚本示例torch.backends.cudnn.benchmark True model load_arcface_backbone(checkpoints/epoch_30.pth) model.cuda().eval() aligned_face preprocess_detected_face(face_img) # 112x112 tensor with torch.no_grad(): embedding model(aligned_face.unsqueeze(0)) embedding torch.nn.functional.normalize(embedding) # 与注册库特征做余弦相似度比较 similarity embedding gallery_embeddings.T这段代码在每台机器上都能直接跑注意开启cudnn.benchmark可以提升固定输入尺寸下的推理速度。在工程落地时我通常还会用PCA把这512维embedding压到128维配合Faiss的IVF索引做大规模底库检索识别耗时能从毫秒级进一步降到亚毫秒级。本文还有配套的精品资源点击获取
返回列表