ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习人脸识别与表情识别:从数据准备到端侧部署的工程实践

深度学习人脸识别与表情识别:从数据准备到端侧部署的工程实践 简介这份资源面向计算机视觉入门与进阶开发者提供一套基于TensorFlow与OpenCV的人脸识别和表情识别完整项目方案可用于安全监控、人机交互、社交媒体分析等场景的学习与二次开发。压缩包共23个文件约9.29MB以Python脚本和编译文件为主另含hdf5模型权重、xml配置、pdf说明、mp4演示、gif效果图及md文档覆盖从数据预处理、模型构建训练到实际预测的完整链路。项目围绕人脸检测与七类基本表情分类展开涉及MTCNN、FaceNet、VGG16、ResNet等模型思路并借助OpenCV完成人脸定位、灰度转换与归一化等预处理。已有500人学习下载适合希望理解深度学习在计算机视觉中落地流程、掌握模型微调与表情识别系统搭建的开发者参考实践。1. 从一张门禁机抓拍说起人脸识别和表情识别到底怎么串起来很多人第一次接触这个方向是因为一台人脸识别门禁机——员工走到闸机前摄像头抓拍屏幕先框出人脸再显示工号和姓名有的还会顺带标一个“微笑”或“疲惫”的标签。这背后其实是两条并行的技术链路一条做人脸识别回答“你是谁”另一条做表情识别回答“你现在什么状态”。两条链路共享同一张人脸检测结果但特征提取的目标完全不同。人脸识别追求身份的可区分性同一个人不同表情要映射到相近的特征向量表情识别追求表情类间的可分性同一个人不同表情要拉开距离。把这两件事放在一个工程里做最容易翻车的地方不是模型选型而是数据标注和损失函数的取舍。这篇笔记面向想用深度学习把这两个任务落地到实际场景的工程师从数据准备、模型搭建、训练调参到部署验证把能复现的路径和踩过的坑讲清楚。2. 人脸识别与表情识别的数据准备从公开集到自有数据的清洗流程2.1 两个任务的数据需求差异人脸识别训练通常需要大规模身份标注数据每个身份多张不同姿态、光照、表情的图片目的是让模型学到身份不变性。表情识别则相反它需要同一身份在不同表情下的图片并且表情标签要均衡。常见做法是人脸识别用公开大规模人脸集做预训练或直接训练表情识别用 FER2013、RAF-DB 这类表情数据集或者从自有门禁抓拍数据里人工标注。如果两个任务共用一个骨干网络数据采样策略要分开。我一般会维护两个 DataLoader一个按身份采样每个 batch 里每个身份抽 N 张另一个按表情类别采样每个 batch 里每个表情类别抽 N 张。这样能避免身份分布和表情分布互相干扰。2.2 人脸检测与对齐的预处理脚本无论做人脸识别还是表情识别第一步都是把人脸从原图里抠出来并对齐。常用做法是用 MTCNN 或 RetinaFace 做检测再根据 5 个关键点做相似变换把人脸对齐到 112×112 或 224×224。下面是一个基于 insightface 的检测对齐脚本示例import cv2 import numpy as np from insightface.app import FaceAnalysis # 初始化检测模型指定推理尺寸和使用的设备 app FaceAnalysis(namebuffalo_l, providers[CUDAExecutionProvider]) app.prepare(ctx_id0, det_size(640, 640)) def detect_and_align(img_path, output_size(112, 112)): img cv2.imread(img_path) faces app.get(img) if len(faces) 0: return None # 取最大人脸避免背景小人脸干扰 face max(faces, keylambda f: (f.bbox[2]-f.bbox[0])*(f.bbox[3]-f.bbox[1])) # 使用关键点做对齐insightface 内部已做相似变换 aligned face.normed_embedding # 这是 embedding不是对齐图 # 如果需要对齐后的图用 face.crop 或自己根据 kps 做 warp return aligned # 批量处理时建议先过滤掉检测置信度低于 0.5 的样本这段代码的关键参数是det_size门禁场景人脸较大640×640 足够如果做课堂状态检测这类远距离场景要调到 1280×1280 甚至更大。ctx_id0表示用第一块 GPU没有 GPU 就改成 -1 走 CPU但速度会慢很多。检测置信度阈值一般设 0.5低于这个值的样本直接丢弃否则对齐后的图会歪训练时 loss 震荡。2.3 数据清洗的三个硬指标自有数据最大的问题是脏。我一般会过三道筛第一道检测框面积小于整图 5% 的丢弃避免把背景误检当人脸第二道关键点置信度均值低于 0.6 的丢弃避免大角度侧脸第三道用预训练人脸识别模型算同一身份内的特征余弦相似度如果某个身份内部平均相似度低于 0.3说明这个身份的图可能混入了别人需要人工复核。表情数据还要额外检查标签一致性同一张图两个人标出不同表情的直接剔除。3. 骨干网络与损失函数选型ArcFace 做人脸CNN 做表情怎么共享底层3.1 人脸识别为什么常用 ArcFace人脸识别本质是一个度量学习问题目标是让同一个人的特征向量尽量靠近不同人的尽量远离。Softmax 分类损失只能保证类间可分不能保证类内紧凑。ArcFace 在角度空间上给类内样本加了一个角度间隔 m使得同类样本被压缩在更小的角度范围内。常见做法是骨干用 ResNet50 或 MobileFaceNet输出 512 维特征接 ArcFace 头训练。import torch import torch.nn as nn import math class ArcFaceHead(nn.Module): def __init__(self, embedding_size512, num_classes10000, margin0.5, scale64): super().__init__() self.weight nn.Parameter(torch.randn(num_classes, embedding_size)) nn.init.xavier_uniform_(self.weight) self.margin margin self.scale scale self.cos_m math.cos(margin) self.sin_m math.sin(margin) self.th math.cos(math.pi - margin) self.mm math.sin(math.pi - margin) * margin def forward(self, emb, label): # 归一化特征和权重计算余弦相似度 cosine nn.functional.linear(nn.functional.normalize(emb), nn.functional.normalize(self.weight)) sine torch.sqrt(1.0 - cosine.pow(2).clamp(0, 1)) # 只在目标类别上加上角度间隔 phi cosine * self.cos_m - sine * self.sin_m phi torch.where(cosine self.th, phi, cosine - self.mm) one_hot torch.zeros_like(cosine) one_hot.scatter_(1, label.view(-1, 1), 1) output one_hot * phi (1 - one_hot) * cosine return output * self.scalemargin0.5是 ArcFace 原论文的推荐值实际门禁场景如果身份数少于 1000可以降到 0.35 左右否则训练初期 loss 很难下降。scale64控制 logits 的幅度太小模型学不动太大容易过拟合。训练时学习率用 0.1 起步配合 warmup 和余弦退火。3.2 表情识别的 CNN 结构与注意力机制表情识别常用 7 类或 8 类分类骨干可以用轻量 CNN也可以复用 ResNet18。表情的关键区域集中在眉毛、眼睛、嘴角所以加一个空间注意力模块能明显提升。常见做法是在骨干的每个 stage 后接一个 CBAM 或 SE 模块。class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.fc nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(channels, channels // reduction, 1), nn.ReLU(inplaceTrue), nn.Conv2d(channels // reduction, channels, 1), nn.Sigmoid() ) def forward(self, x): # 通道注意力让模型关注表情相关的特征通道 return x * self.fc(x)reduction16是 SE 模块的默认压缩比表情识别任务通道数不大用 8 也可以。如果数据量少于 2 万张建议冻结骨干前两个 stage只训练后面和注意力模块否则容易过拟合。3.3 两个任务共享底层特征的取舍如果两个任务在同一台设备上跑共享底层能省算力。我一般会共享前两个 stage后面分两个头人脸头接 ArcFace表情头接全连接分类。训练时两个 loss 加权求和人脸 loss 权重 1.0表情 loss 权重 0.5 到 0.8。如果表情数据量远小于人脸数据表情 loss 权重可以再降避免表情任务把底层特征带偏。反过来如果表情识别是主任务人脸识别只是辅助那就把人脸 loss 当正则项权重设 0.2 左右。4. 训练调参与避坑学习率、batch size、数据增强的实操参数4.1 学习率与 batch size 的搭配人脸识别训练对 batch size 敏感因为 ArcFace 的类内紧凑性依赖足够多的同类样本。单卡 24G 显存ResNet50 输入 112×112batch size 一般能到 256。如果显存不够用梯度累积模拟大 batch。学习率按线性缩放规则base_lr0.1 对应 batch size 256如果 batch size 降到 128学习率用 0.05。表情识别数据量小batch size 64 或 128 就够学习率用 0.01 起步配合 StepLR 每 20 个 epoch 降 0.1 倍。如果 loss 在前 5 个 epoch 不降先检查数据标签有没有对错再检查学习率是不是太大。4.2 数据增强的边界人脸识别的增强要小心水平翻转可以用但垂直翻转、大角度旋转会破坏人脸结构反而降低识别率。颜色抖动幅度要小亮度变化 ±0.2、对比度 ±0.2 足够。表情识别的增强可以更激进因为表情对几何变换更鲁棒随机裁剪、小角度旋转、甚至 Cutout 都能用。提示表情识别不要用水平翻转因为左右脸表情不对称翻转后标签可能失真。如果一定要用只对中性、惊讶这类对称表情翻转。4.3 常见训练翻车现场第一个坑loss 突然变成 nan。原因通常是 ArcFace 的 margin 太大或者学习率太高导致梯度爆炸。解决方法是把 margin 降到 0.3学习率降 10 倍加梯度裁剪 max_norm5。第二个坑验证集准确率远低于训练集。人脸识别里常见原因是验证集的身份在训练集里没出现过这是正常的应该用验证集里每个身份注册一张图剩下的做测试算 1:N 检索准确率。表情识别里常见原因是表情数据标注不一致同一种表情不同标注员标成不同类需要重新清洗标签。第三个坑两个任务一起训练时人脸识别准确率下降。原因是表情 loss 的梯度回传到共享底层把身份特征带偏了。解决办法是给表情分支加一个梯度反转层或者直接分开训练两个模型部署时再合并推理。5. 部署验证从 PyTorch 到 ONNX 再到门禁机端侧推理5.1 导出 ONNX 的关键参数训练完的模型要部署到门禁机或边缘设备常见做法是导出 ONNX再用 TensorRT 或 ONNX Runtime 推理。导出时注意动态轴设置batch 维度设为动态方便后续调整。import torch.onnx model.eval() dummy_input torch.randn(1, 3, 112, 112).cuda() torch.onnx.export( model, dummy_input, face_expression.onnx, input_names[input], output_names[embedding, expression_logits], dynamic_axes{input: {0: batch}, embedding: {0: batch}, expression_logits: {0: batch}}, opset_version11 )opset_version11兼容性最好TensorRT 7 以上都支持。如果用了 SE 模块导出时注意 AdaptiveAvgPool2d 在 opset 11 里支持没问题但某些自定义算子可能需要手动注册。5.2 端侧推理的量化与加速门禁机算力有限一般要做 INT8 量化。用 ONNX Runtime 的量化工具准备 500 张校准图量化后人脸识别准确率下降控制在 0.5% 以内表情识别下降 1% 到 2%。如果下降太多检查校准集是否覆盖了不同光照和角度。推理时人脸检测和对齐可以放在 CPU 上跑特征提取和表情分类放 NPU 或 GPU。我一般会把检测阈值调低到 0.4因为门禁场景误检比漏检代价小漏检会导致用户反复站到闸机前。5.3 验证指标与线上监控人脸识别看两个指标1:1 验证的 TARFAR1e-4门禁场景一般要求 99% 以上1:N 检索的 Top1 准确率1000 人以内要求 98% 以上。表情识别看混淆矩阵重点关心中性、高兴、惊讶三类的召回率这三类在门禁场景最常见。上线后要监控特征分布的漂移每周抽 1000 张抓拍图算 embedding 的均值方差如果和训练集偏差超过 10%说明摄像头角度或光照变了需要重新标注数据微调。6. 一个具体技巧用表情识别结果动态调整人脸识别阈值门禁场景有个实际问题用户戴口罩或表情夸张时人脸识别分数会偏低如果阈值固定要么误拒要么误放。我试过一个做法用表情识别头的输出动态调阈值。具体是如果表情分类为“中性”阈值用 0.6如果为“高兴”或“惊讶”阈值降到 0.5如果为“厌恶”或“愤怒”阈值升到 0.65因为这类表情可能伴随皱眉、嘴角下拉特征形变较大容易误识。def dynamic_threshold(expression_logits, base_threshold0.6): # expression_logits 是 7 类表情的 logits prob torch.softmax(expression_logits, dim-1) neutral_prob prob[0][0].item() # 假设第 0 类是中性 happy_prob prob[0][1].item() if neutral_prob 0.7: return base_threshold elif happy_prob 0.5: return base_threshold - 0.1 else: return base_threshold 0.05这个技巧在自有门禁数据上试过误拒率从 3.2% 降到 1.8%误放率没明显上升。参数不是固定的要根据自己场景的 FAR 和 FRR 曲线调。我一般会先跑一遍验证集画出不同阈值下的 FAR/FRR再选一个平衡点作为 base_threshold。另一个习惯是每次换摄像头或补光灯都重新跑一遍校准集确认特征分布没漂移。人脸识别和表情识别都是对光照和角度敏感的任务没有一劳永逸的模型只有持续迭代的数据闭环。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进