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

资讯详情

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

YOLOv5+ArcFace:从人脸检测到身份识别的完整改进实践

YOLOv5+ArcFace:从人脸检测到身份识别的完整改进实践 简介这是一套面向高校计算机、电子信息、数学等专业学生进行课程设计、期末大作业或毕业设计的人脸识别改进方案基于YOLOv5目标检测框架实现包含完整源码、测试图片与说明文档。包内共95个文件压缩后约8.94MB核心为36个Python脚本涵盖模型定义、训练、测试、人脸数据集转换与TensorRT加速等流程另有19个YAML配置参数文件、JPG/PNG示例图片、Shell脚本及Markdown说明文档便于快速搭建环境与二次开发。资源结构清晰涉及WIDER Face数据集处理、BlazeFace与YOLOv5的改进尝试等模块说明文档对代码结构、数据集处理和改进细节进行了系统整理可据此理清整体思路、复现核心流程并自行调试、扩展功能。已有791人学习下载适合具备一定Python与深度学习基础、希望深入人脸检测方向的学习者也可作为相关项目开发的基线参考。1. 用 YOLOv5 做“人脸识别改进”先想清楚检测和识别的边界很多人拿到基于 YOLOv5 的人脸识别资源包第一反应是直接训练模型然后出结果。但真正落地时最容易被“检测”和“识别”两个环节的边界卡住YOLOv5 原版做的是 COCO 80 类目标检测训练出来的模型只能告诉你“图像里哪个位置有一张脸”不能告诉你“这张脸是谁”。标题里的“改进”二字重点不在把框调得有多准而是把 YOLOv5 当成前端抓拍器通过改 Head、加注意力模块提升小目标人脸召回率同时在检测之后单独训练一个特征提取分支用特征向量之间的余弦距离完成身份比对形成一套可以在本地离线跑通的完整流程。这套方案解决的典型问题有两个一是常见的 Haar 级联在侧脸、遮挡环境下漏检严重二是直接调用云服务做人脸比对会有数据出域的合规风险和时延问题。适合的人主要也是两类一类是想把 YOLOv5 从开源分类任务迁到垂直场景的算法工程师另一类是做人脸门禁机、考勤一体机等边缘设备交付的嵌入式开发者。前者的重点是“改进检测”后者的重点是“离线的身份比对”。后面的章节按照这个分工一步步展开。2. 人脸识别改进第一步把 YOLOv5 作为人脸抓拍前端2.1 为什么从 MTCNN、Haar 换到 YOLOv5传统人脸检测方案里Haar 级联是最容易上手的OpenCV 三行代码就能跑起来但它的本质是弱分类器级联对光照、口罩、大角度侧脸几乎无能为力。MTCNN 比 Haar 好很多通过 P-Net、R-Net、O-Net 三级网络逐层精修能同时输出人脸框和五个关键点。但它的痛点是三个网络串行推理在 CPU 上处理一帧 1080p 图像通常要几十毫秒而且第一级 P-Net 的滑窗对小目标召回有限监控大图里几十像素的侧脸经常在第一层就被滤掉了。YOLOv5 走的是 anchor-based 单阶段路线一次前向直接输出所有候选框检测和人脸框回归在一个网络里完成。它的 CSP 结构让梯度在 backbone 里传递更充分自适应锚框计算也降低了调参成本。实际对比下来在 WIDER FACE 这类包含大量小脸和密集人脸的公开数据集上YOLOv5s 的召回率明显高于 MTCNN同时推理延迟可以控制在相近水平。这里说的“改进”第一步就是把检测器从级联架构换成 YOLOv5而不是在 Haar 之后缝缝补补。2.2 人脸数据集的 YOLO 标注格式与关键的“外扩”技巧YOLOv5 训练需要 txt 标注文件每行表示一个目标格式是“类别 cx cy w h”其中 cx、cy 是目标中心点坐标w、h 是宽高全部除以图像宽高做归一化。一张 640x640 的图像里一个人脸框的中心点落在 (372, 293)宽 99、高 145对应标注就是0 0.582031 0.458984 0.154297 0.226562这里的0是类别编号对应face。和 COCO 目标检测那种紧贴目标的标注习惯不同人脸框不能贴着眉毛和下巴画。更稳的做法是向外扩 10% 到 15%把额头和下颌边缘包进框里。原因有两个一是识别分支裁剪人脸后还要做关键点对齐框太紧会把下巴截掉导致后续识别精度下降二是 YOLOv5 训练时的数据增强会对真实框做轻微扰动框一旦贴得太紧增强后边缘人脸区域就残缺了。如果你的数据是用 MTCNN 或 RetinaFace 自动生成的人脸框建议在生成时对外扩参数留一个接口。常见的做法是在脚本里对 w 和 h 分别乘一个大于 1 的系数再对中心点保持不变这样五个关键点仍然落在框内不需要额外处理。2.3 模型规模怎么选yolov5s / yolov5m 与 Jetson 部署的取舍YOLOv5 官方按深度和宽度系数把模型分成 n、s、m、l、x 五档。做人脸检测时模型规模不能只看 mAP还要给后面的特征提取分支留出 CPU 和内存余量。单尺度 640 输入下几档模型的大致体量如下模型参数量单尺度 640 计算量训练显存需求推理场景yolov5n约 1.9M约 4.5 GFLOPs4-6 GB摄像头端、低功耗设备yolov5s约 7.2M约 16 GFLOPs6-8 GBJetson Nano、边缘盒子yolov5m约 21.2M约 49 GFLOPs10-12 GB服务器批量推理这里的显存是训练阶段的估算值实际随 batch size 和图像尺寸变化。在 Jetson Nano 上yolov5s 可以用 TensorRT 加速到接近实时yolov5m 则会把显存占满且发热压不住。服务器场景可以选 m但同时要意识到人脸识别链路里检测只占一部分计算量特征提取同样吃资源。建议先拿 s 版本把整条链路跑通确认业务指标后再决定是否升级模型而不是一开始就上大模型。3. 改进一YOLOv5 改单类输出并用注意力模块提升小脸召回3.1 把 COCO 的 80 类输出改成 face 单类YOLOv5 的仓库里默认按 COCO 80 类训练直接拿来做检测会输出一堆 person、dog、car 的框。要做人脸识别改进第一步是新建一个数据配置文件和对应模型配置文件把类别数改成 1。数据配置face.yaml这样写# face.yaml path: ./datasets/face train: images/train val: images/val nc: 1 names: 0: facepath是数据集根目录train和val都是相对于path的路径不需要写绝对路径。nc是类别数因为只检测人脸所以是 1。names的索引和标注文件里的类别编号一一对应这里0就是 face。配套的模型配置文件yolov5s_face.yaml只需要改一个参数# yolov5s_face.yaml nc: 1 depth_multiple: 0.33 width_multiple: 0.50 anchors: - [10, 13, 16, 30, 33, 23] - [30, 61, 62, 45, 59, 119] - [116, 90, 156, 198, 373, 326]anchors可以沿用 COCO 的默认值训练时 YOLOv5 会通过自适应锚框机制重新计算。如果自己的数据集里人脸尺寸分布比较极端比如全部是闸机近景大头照可以用 kmeans 对训练集标注框做一次聚类把聚类结果填到anchors里能省下前几十轮的收敛时间。3.2 在 SPPF 之后插入 SE 注意力模块改动最小改进 YOLOv5 检测能力最直接的刀法是加注意力。人脸检测的难点集中在低照度、肤色相近的背景、小尺寸目标而注意力模块能让网络在通道维度上重新加权把响应集中到人脸边缘和五官区域。这里推荐在 SPPF 模块之后接一个 SEBlock因为它只增加两个全连接层对整体推理速度影响很小却能在不调整 loss 的前提下提升难例召回。import torch import torch.nn as nn class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.squeeze nn.AdaptiveAvgPool2d(1) self.excitation nn.Sequential( nn.Linear(channels, channels // reduction), nn.ReLU(inplaceTrue), nn.Linear(channels // reduction, channels), nn.Sigmoid() ) def forward(self, x): b, c, _, _ x.size() w self.squeeze(x).view(b, c) w self.excitation(w).view(b, c, 1, 1) return x * w.expand_as(x)这段代码先通过全局平均池化把每个通道压缩成一个标量再用两个全连接层建模通道之间的依赖关系最后用 Sigmoid 生成 0 到 1 之间的权重逐通道乘回原特征图。使用时把它加到 YOLOv5 的models/common.py里然后在 Detect 前的 C3 模块输出后实例化并调用即可。注意不是每个 C3 后面都插只在最后一层或后两层插入否则反向传播的梯度会被稀释训练速度明显变慢。提示加完 SEBlock 后跑一次python train.py --data face.yaml前先用小 batch 验证模型能正常前向避免 forward 维度不匹配报错。3.3 用一条命令训练 YOLOv5 人脸检测模型参数怎么设环境配置好之后训练命令和官方训练 COCO 的方式完全一致只是数据配置和模型配置换了python train.py \ --data face.yaml \ --cfg yolov5s_face.yaml \ --weights yolov5s.pt \ --epochs 100 \ --batch-size 16 \ --imgsz 640 \ --hyp hyp.scratch-low.yaml \ --multi-scale--weights建议用 COCO 预训练权重初始化从零训练的话前 30 轮基本在学底层特征收敛很慢。--imgsz 640是默认值如果数据集里小目标多可以升到 768但显存占用会接近翻倍。--multi-scale让输入尺寸在 0.5 到 1.5 倍之间随机缩放对小人脸友好代价是每个 batch 的耗时变长。超参数文件hyp.scratch-low.yaml是按 COCO 任务调好的直接用到人脸数据集上要改几处。这是 YOLOv5 超参数里最值得关注的部分参数默认值人脸场景建议原因mosaic1.00.5马赛克增强会把小脸裁得更小密集人脸场景触发大量无效学习hsv_h0.0150.0人脸肤色对色相偏移敏感强行改变色调会产生大量难负样本fliplr0.50.5水平翻转不破坏人脸语义可以保留lr00.010.005人脸数据集量级通常只有数万张初始学习率过大会震荡这些改动都集中在hyp.scratch-low.yaml里训练命令不用变。启动日志第一行会打印最终生效的超参数可以确认自己的修改是否被读取。3.4 训练自己的数据集时最容易踩的两个坑马赛克增强与 NMS 阈值第一个坑是数据集里全是近景大头照但测试场景是远距离监控。这种数据不匹配会导致训练 loss 下降很快实测却检不出小脸。除了在数据层面补小脸样本训练侧也要把mosaic从 1.0 降到 0.5因为马赛克会把四张图缩小拼接等于把大量人脸压到几十像素模型看到的小脸全部是模糊的。配合--multi-scale让网络在多种尺度下都能看到清晰的人脸结构。第二个坑是密集人群里的重叠脸。YOLOv5 默认的 NMS IoU 阈值是 0.45在多人场景下相邻人脸的框会被互相抑制导致漏检。验证和推理时建议把置信度阈值调到 0.25IoU 阈值降到 0.3python val.py --data face.yaml --weights runs/train/exp/weights/best.pt --conf-thres 0.25 --iou-thres 0.3 python detect.py --source test.jpg --weights runs/train/exp/weights/best.pt --conf-thres 0.25 --iou-thres 0.3conf-thres低于该值的框直接丢弃iou-thres越低保留的相邻框越多。密集场景下宁可让检测多输出几个候选框也不要因为 NMS 把真脸压掉因为后面的识别分支还有一次过滤机会。4. 改进二检测之后接特征提取分支用 ArcFace 做身份识别4.1 明确分工YOLOv5 出框ArcFace 出特征检测改进解决的是“脸在哪里”识别改进解决的是“这个人是谁”。这是两个完全不同的任务不能指望 YOLOv5 一个网络同时完成。经典的人脸识别算法采用“检测器 对齐 特征网络”三件套YOLOv5 负责抓拍把检测框裁剪出来resize 到 112x112再送入特征提取网络输出一个固定长度的 embedding 向量。识别时把当前向量和底库向量做余弦相似度比对超过阈值就认为是同一个人。这个方案里真正的识别改进在于损失函数。如果用普通的 softmax 分类头训练模型只在训练集的身份上有效遇到没见过的 ID特征向量的分布是乱的。换成 ArcFace 之后网络学习的是“同一身份拉近、不同身份推开”的超球面特征空间训练集里没有的新身份也能通过向量距离完成比对。这也解释了为什么人脸识别项目普遍用开源好用的可离线的人脸识别模型作为 backbone而不是自己从零设计分类网络。4.2 ArcFace 头在 PyTorch 里的最小实现ArcFace 的核心是在 softmax 之前对目标类别的角度加上一个间隔 m。下面是一份可以直接嵌入训练脚本的 PyTorch 实现片段import math import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceHead(nn.Module): def __init__(self, feat_dim512, num_classes10000, s64.0, m0.5): super().__init__() self.s s self.m m self.weight nn.Parameter(torch.FloatTensor(num_classes, feat_dim)) nn.init.xavier_normal_(self.weight) def forward(self, x, labels): x F.normalize(x, dim1) w F.normalize(self.weight, dim1) cos_theta F.linear(x, w) theta torch.acos(torch.clamp(cos_theta, -1.0 1e-7, 1.0 - 1e-7)) target_logits torch.cos(theta self.m) one_hot torch.zeros_like(cos_theta) one_hot.scatter_(1, labels.view(-1, 1).long(), 1.0) output cos_theta * (1 - one_hot) target_logits * one_hot output * self.s return outputs64是特征缩放因子把人脸特征映射到半径为 64 的超球面上目的是让梯度在训练初期更稳定。m0.5是角度间隔间隔越大类内聚得越紧但训练难度也越高。one_hot把目标类别的 logit 替换成cos(theta m)其他类别保持原 logit这样反向传播时只有目标类别感受到额外的角度惩罚。推理时丢掉这个分类头只保留 backbone 输出的 512 维向量即可。不同的损失函数对训练难度影响很大选择时可以参考这张对比损失函数类内约束训练难度适用场景Softmax弱低封闭集分类不适合开放集Triplet强高需要频繁挖负样本小规模闭集ArcFace强且稳定中大规模开放集识别4.3 训练数据怎么组织目录结构、样本对与清洗特征提取网络需要的是“每类是一个人”的数据目录结构一般这样组织datasets/face_recognition/ ├── train/ │ ├── id00001/ │ │ ├── 001.jpg │ │ └── 002.jpg │ ├── id00002/ │ │ ├── 001.jpg │ │ └── 002.jpg └── val/ ├── id00001/ └── id00003/每个子目录是一个身份 ID里面的图片是该身份在不同角度、不同光线下的照片。训练前需要清洗数据剔除模糊和严重遮挡的图片同一个身份少于 5 张图的类直接删掉否则 ArcFace 学不出稳定的类中心用检测器把所有图跑一遍框质量不过关的图也删掉。这个清洗步骤决定了整个识别效果的瓶颈很多人训练出来效果差问题不在模型而在数据里混了大量错标和模糊样本。4.4 训练参数embedding 维度、margin 与学习率特征提取网络的训练命令可以这样组织python train_face.py \ --backbone resnet50 \ --head arcface \ --embedding-size 512 \ --batch-size 256 \ --lr 0.1 \ --weight-decay 5e-4 \ --epochs 30 \ --warmup-epoch 5embedding-size是特征向量长度。128 维在门禁场景够用512 维在跨年龄、跨设备场景更稳但底库检索和存储成本也更高。margin的取值要匹配数据分布0.5 是常规值如果同一身份的照片差异很大比如身份证照片和现场抓拍建议降到 0.3如果活体要求高需要把不同人拉得更开可以试 0.7。lr 0.1配合warmup-epoch 5前 5 个 epoch 从 0 线性升到 0.1避免训练初期梯度爆炸。这里要特别提醒一个现象如果用了 ArcFace训练集里见过的 ID 精度很高但没见过的 ID 特征距离没有区分度通常是 backbone 宽度不够或者训练数据量太少。此时先别调 margin先把数据量和数据质量补上去再考虑换更大的 backbone。5. 把检测和识别串成一条推理链路用 LFW 距离分布定阈值5.1 一个能直接跑的推理函数检测和识别都训练好之后需要把它们串成一条完整链路。下面是一段最小可用的推理代码逻辑是先用 YOLOv5 出框再对每个框裁剪、resize、提取特征最后和底库特征做余弦相似度比对。import cv2 import numpy as np import torch import torch.nn.functional as F def recognize(frame, detector, encoder, db_feats, db_names, thresh0.55): boxes detector(frame)[0] # 假设输出 [N, 6]: x1 y1 x2 y2 conf cls results [] for box in boxes: x1, y1, x2, y2 [int(v) for v in box[:4]] face cv2.resize(frame[y1:y2, x1:x2], (112, 112)) face torch.from_numpy(face.transpose(2, 0, 1)).float().div(255).unsqueeze(0) emb F.normalize(encoder(face), dim1).detach().numpy()[0] sims db_feats emb # db_feats 是 [M, 512]已归一化 idx int(np.argmax(sims)) if sims[idx] thresh: results.append((db_names[idx], float(sims[idx]), box)) else: results.append((unknown, float(sims[idx]), box)) return resultsdb_feats是底库所有人员特征组成的矩阵每一行是某个身份的 embedding录入时用同一个 encoder 提取并归一化。db_feats emb一次性算出该人脸和所有人的余弦相似度向量化比 for 循环快一个数量级。底库更新时直接向db_feats追加一行即可不需要重新训练模型。5.2 距离阈值不该拍脑袋用正负样本对画 ROC 来定阈值直接决定误识率和拒识率。最稳的方法是用验证集构造两类样本对同一个人不同照片组成正样本对不同人的照片组成负样本对分别计算余弦相似度再画 ROC 曲线取等错误率点。import numpy as np from sklearn.metrics import roc_curve y_true np.concatenate([np.ones(len(pos_sims)), np.zeros(len(neg_sims))]) y_score np.concatenate([pos_sims, neg_sims]) fpr, tpr, thresholds roc_curve(y_true, y_score) fnr 1 - tpr eer_idx np.argmin(np.abs(fpr - fnr)) print(EER threshold:, thresholds[eer_idx])pos_sims是正样本对的相似度数组neg_sims是负样本对的。等错误率点处的阈值误识率和拒识率相等这是最基础的基准值。实际部署时要按场景调整人脸识别门禁机这种安全敏感设备期望零误识阈值应比 EER 高 0.1 到 0.15考勤打卡这种追求少漏打的场景阈值可以取在 EER 附近或略低。最后还有一条经验底库照片的采集光线尽量和现场抓拍保持一致否则再过一个月你会发现阈值在中午和晚上两个时段需要两套值。本文还有配套的精品资源点击获取
返回列表