改进卷积模型实现人脸性别与情感分类:从训练到部署

发布时间:2026/10/19 7:04:45
改进卷积模型实现人脸性别与情感分类:从训练到部署
毕业答辩现场评委老师听完我念完题目问的第一句话就是“你题目里的改进到底改在哪儿”这句话我印象很深。人脸性别和情感分类这个方向网上教程和开源代码非常多如果只是把两三个现成模型跑一遍、换一个数据集复现结果那题目里的“基于人工智能”和“改进”基本就成了装饰词答辩分数也不会好看。这个项目围绕卷积模型的改进做成了一套完整的人脸性别与情感分类系统先设计数据集与预处理流程再改造卷积骨干网络加入注意力机制和更轻量的卷积单元最后用桌面应用把模型实时跑起来。无论你是准备做毕设还是想系统了解图像分类任务从训练到部署的完整链路下面的内容应该都能帮到你。1. 项目整体设计与改进思路拆解1.1 课题拆解一个模型搞定两个任务先把这个题目拆开看。“人脸性别和情感分类”是两个分类任务性别是二分类通常分“男/女”情感是多分类常见做法是分成生气、厌恶、恐惧、开心、难过、惊讶、中性七类。它们共享一个核心输入——人脸图像但又各有各的难点。性别分类主要依赖整体轮廓、五官比例这些粗粒度特征情感分类更依赖眼睛、眉毛、嘴角这些局部区域的细微变化属于细粒度特征。我当时的第一版方案是两个独立小模型分别训练理由很简单任务互不干扰实现门槛低。但真正跑起来之后发现两个问题——第一两套前向推理浪费算力在只有入门级独显甚至纯CPU的机器上实时性很难看第二两个模型各自有各自的偏差放在同一个演示系统里显得很割裂。后来改成了多任务共享骨干网络一个特征提取主干两个分类分支性别任务和情感任务在最后一个全连接层之前完全共用。这个改动的本质是让模型同时学习“人脸长什么样”和“人脸表达了什么”。共享层接收两个任务的梯度回传相当于一种隐式的正则化单一任务过拟合风险会小一些。推理时只跑一次前向计算两个结果同时出来实时性大大提高。对毕设展示来说“一个模型同时输出性别和情感”这个效果本身就很有记忆点。注意多任务共享骨干并不是万灵丹。只有当两个任务确实共享底层特征时才划算。性别和情感正好属于这种关系。如果两个任务完全无关比如“人脸性别”和“图片是不是猫”强行共享骨干只会互相拖累。1.2 改进卷积模型的三个方向与选型理由“改进卷积模型”这句话必须落地成具体技术动作。我的改进主要落在三个方向。第一个方向是轻量化改造。传统教科书式的卷积网络喜欢堆普通卷积参数多、计算量大在CPU上推理一张图经常要几百毫秒。我的方案是引入深度可分离卷积把标准卷积拆成“逐通道卷积逐点卷积”两步参数量和计算量都大幅下降。第二个方向是注意力机制。深度可分离卷积虽然轻但特征表达能力不如标准卷积丰富轻量化之后往往精度受损。为了补回精度我在网络中插入了通道注意力模块让模型自动学习不同特征通道的重要程度。这个模块有很强的可解释性答辩时和评委解释“模型自己学会了重点看嘴部区域来判断开心还是难过”比单纯报准确率有说服力得多。第三个方向是训练策略优化。模型结构改完之后我发现情感分类数据集类别不平衡很严重“开心”和“中性”样本很多“厌恶”和“恐惧”样本很少。结构再改也解决不了数据分布问题。于是我在损失函数上做了调整引入Focal Loss来降低容易样本的权重让模型更关注那些被错分的少数类样本。这三个方向不是堆砌而是有因果关系轻量化导致能力下降所以要加注意力补能力加了注意力后训练更难收敛所以要调损失函数。毕设答辩问“为什么这样改”这条逻辑链就是最好的回答。1.3 技术栈与开发环境规划整个项目的技术选型我在开工前就定好了避免做一半换框架。模块选型选择理由编程语言Python 3.9生态最全PyTorch和OpenCV无缝配合深度学习框架PyTorch 2.x动态图调试方便实验改结构效率高模型部署ONNX Runtime不需要装深度学习框架也能跑钉钉演示不怕环境出问题图像处理OpenCV 4.x人脸检测、仿射变换、图像读取一条龙界面开发PyQt5桌面程序稳定评审现场不需要依赖浏览器和网络硬件普通笔记本CPU能实时推理不依赖高性能计算设备这个组合的核心理念是训练阶段用PyTorch方便改代码部署阶段用ONNX Runtime保证CPU上的推理速度。PyQt5做界面是偏保守的选择但恰恰是这种保守让我在答辩现场从来没担心过“网络不通”“浏览器兼容性”这些破事。2. 数据准备与预处理工程2.1 数据集组织与标注格式这个项目用了两类公开学术数据集。性别分类用的是一个人脸属性数据集包含约2.4万张覆盖不同年龄、肤色、光照条件的正脸和近正脸图像标注字段里有性别、年龄等属性。情感分类用的是大家非常熟悉的灰度表情数据集每张图片是48x48像素共计3.5万余张标注为七类基本表情。拿到数据后的第一件事不是训练而是把所有图片整理成统一的目录结构每个类一个文件夹。这个习惯帮我省了很多麻烦后面做加载器、做混淆矩阵分析都变得非常轻松。情感数据集的类别分布很不均匀这是公开表情数据集的通病。安全驾驶、客服满意度这些实际场景里“平静”“微笑”本来就是最常见的状态“恐惧”“厌恶”天然就少。训练之前我先统计了每一类的样本量把类别权重计算好备用。性别数据集相对均衡男性和女性数量基本持平不需要额外处理。关于数据划分要特别强调一点训练集、验证集、测试集要从原始数据层面就切开千万不要整个人工智能项目只做一个大列表然后在代码里随机选百分之二十当测试。更极端的情况是同一张人脸的不同照片出现在训练集和测试集里模型相当于“开卷考试”测试精度虚高。答辩时如果被评委抓到这种数据泄漏那基本等于学术态度问题。2.2 人脸检测、对齐与归一化流程原始数据里的图像不是都适合直接进模型尤其是性别数据集有些图带着背景、头发、衣领直接丢给卷积网络会引入大量无关信息。我处理流程的标准化版本是四步第一步人脸检测。用OpenCV的DNN模块加载一个人脸检测模型检测整张图中所有人脸框然后取面积最大的那个作为目标人脸。这样处理的好处是图片里出现多个人时模型不会因为突然跳框导致输出乱跳。第二步关键点对齐。人脸检测模型同时会输出五官关键点包括两只眼睛、鼻尖、左右嘴角。我利用双眼坐标计算一个旋转角度再做仿射变换把两只眼睛旋转到水平位置。这一步非常重要。卷积网络对几何变化非常敏感同一张脸偏转五度提取到的特征可能就完全变了。如果不去对齐模型只能用大量数据硬学旋转不变性代价太高。对齐就像拍证件照之前要正对镜头人脸对齐也是把输入标准化到一个统一姿态。第三步裁剪缩放。对齐之后按人脸框裁剪再缩放成统一尺寸。我在工程里规定性别分支输入是112x112的三通道彩色图情感分支输入是48x48的灰度图。这里要注意的是情感数据集本身是灰度图如果非要把它转成彩色图再缩放成112x112反而会丢失数据本身的统计特性。所以我是直接按骨干网络的分支要求分别预处理。第四步归一化。把像素值从0-255归一化到0-1之间再按照训练集算出的通道均值和方法做标准化。归一化能加速收敛尤其是用了预训练权重的Backbone输入分布不对会让权重适配得很慢。提示归一化用的均值和方法只能从训练集统计然后统一应用到验证集和测试集绝对不能用全数据集统计再去划分训练测试否则验证集的均值信息就间接泄漏到模型里了。我在实测中发现这种泄漏会让验证集精度虚高1-2个百分点。2.3 数据增强策略与类不均衡处理数据增强是对抗过拟合最直接的手段。对于情感分类任务我使用了随机水平翻转、小角度旋转、亮度对比度抖动、随机擦除四种增强方式。其中随机水平翻转对情感任务基本安全因为大部分表情在左右翻转后语义不变“开心的笑”不会变成“生气的哭”。随机擦除是我特别推荐的一种增强它会随机遮住图片中的一小块区域强迫模型不要只依赖某一个局部特征。性别分类任务的增强策略我反而做得比较轻只加了水平翻转和轻微亮度扰动。原因是性别分类本身已经足够简单——主流算法在这个任务上都能达到95%以上的准确率过度增强反而可能把男性脸上的一些特征给放大成错误信号拉低精度。对于情感数据集的少数类我用的是“过采样加权损失”双管齐下。数据加载器里把“厌恶”“恐惧”这些少数类样本的重复采样概率调高让每个epoch里模型看到它们的次数更多。同时计算损失时每个类别乘以一个反比于样本量的权重。两个手段一起上模型在少数类上的表现提升明显。刚开始我只做了加权损失发现效果有限后来补上过采样才把macro-F1拉上来这两个是互补关系而不是替代关系。3. 改进卷积模型的结构设计解析3.1 骨干网络选择精度与速度的平衡模型骨干网络我对比了两条路线一条是ResNet18这类经典残差网络另一条是MobileNetV2这类轻量高效网络。ResNet18的思路是“加深跳跃连接”残差结构解决了深层网络梯度消失问题分类精度很有保障。但它的基础卷积单元是普通卷积参数量和计算量都偏大。我实测在纯CPU上跑一张112x112的图大约要80毫秒以上做成实时视频流会明显发卡。MobileNetV2这类结构的核心是深度可分离卷积参数量只有ResNet18的一个零头。刚开始我担心它精度不够但实测下来人脸这种结构化的图像任务精度损失并不明显。尤其当我把注意力机制加进去之后精度几乎追平了ResNet18参数量却少了一半多。毕设环境普遍是普通笔记本加入门级显卡选后者是理性的决定。3.2 注意力机制模块的插入位置我在每个基本单元的最后加了一个通道注意力模块。这个模块的代码实现非常经典核心思想是先压缩空间维度再用两个全连接层学习通道之间的依赖关系import torch import torch.nn as nn class ChannelAttention(nn.Module): def __init__(self, channels, reduction16): super().__init__() mid max(channels // reduction, 8) self.fc1 nn.Conv2d(channels, mid, kernel_size1) self.fc2 nn.Conv2d(mid, channels, kernel_size1) self.relu nn.ReLU(inplaceTrue) self.sigmoid nn.Sigmoid() def forward(self, x): # 全局平均池化将每个通道压缩成一个标量 squeeze x.mean(dim(2, 3), keepdimTrue) # 全连接层学习通道间非线性关系 excite self.fc2(self.relu(self.fc1(squeeze))) # Sigmoid 输出每个通道的权重范围0-1 scale self.sigmoid(excite) # 将权重乘回原始特征图 return x * scale这段代码我一直没有改动过因为它实在简单且有效。它发挥作用的方式可以这样理解卷积层提取出的几十上百个特征通道里不是每个通道对当前任务都有同等价值。有的人脸在判断性别时主要看下颌线相关特征在判断情绪时主要看嘴角相关特征。通道注意力模块相当于是给每个通道算了一个置信度再按置信度重新加权。插入位置是有讲究的。我对比过把注意力模块插在网络的浅层和深层最终选在每阶段的最后一层。浅层特征图分辨率高插入注意力会明显增加计算量而且浅层特征本身还很通用不适合过早强调某类信息。深层特征图分辨率低、语义明确正是做通道加权的最佳位置。建议毕设实验一定要做“加与不加注意力模块”的对照实验哪怕只是两行对比也能让评委清楚看到这个模块的真实贡献。不要因为觉得太简单就不写进论文。3.3 高效卷积单元改造与参数量对比深度可分离卷积替换标准卷积是整个项目最核心的改动。标准卷积会同时考虑所有输入通道和空间邻域的信息计算量是输入通道数×输出通道数×卷积核大小。深度可分离卷积把过程拆成两步第一步每个输入通道单独用一个小卷积核做空间特征提取第二步用1x1卷积把所有通道信息融合起来。拆开之后计算量变成输入通道数×卷积核大小输入通道数×输出通道数。直观来说一个3x3的标准卷积处理64通道输入、128通道输出的计算量被压缩到了原来的八分之一还多。这一步的预期收益是推理速度大幅提升代价是精度略微下降。我在改造后的分类头也做了一处调整。传统做法是全局平均池化后接一个Flatten再拼接全连接层。为了支撑多任务输出我在最后的全连接层改成了两个独立分支每个分支负责各自的分类。分支共享主干只在最后一层分叉class FaceMultiTaskNet(nn.Module): def __init__(self, backbone, feature_dim1280, gender_classes2, emotion_classes7): super().__init__() self.backbone backbone self.gender_head nn.Linear(feature_dim, gender_classes) self.emotion_head nn.Linear(feature_dim, emotion_classes) def forward(self, x_gender, x_emotionNone): if x_emotion is not None: # 实际使用时情感分支输入会先缩放到48x48 pass feat self.backbone(x_gender) gender_logits self.gender_head(feat) emotion_logits self.emotion_head(feat) return gender_logits, emotion_logits参数量和计算量的对比我做了个简单的表格记进了实验记录模型方案参数量(百万)CPU单帧推理耗时(ms)性别准确率情感准确率普通堆叠卷积Baseline5.24295.1%63.5%深度可分离卷积骨干2.12896.4%67.8%深度可分离卷积注意力模块2.33197.2%71.2%参数从5.2M降到2.3M推理耗时从42ms降到31ms精度反而提升了。这个结果让我确信结构上的恰当改进带来的收益远比单纯堆参数要大。4. 训练策略与实验效果对比4.1 损失函数、优化器与学习率调度训练策略和模型结构同等重要。我用的是AdamW优化器初始学习率1e-3权重衰减1e-4批量大小64。学习率采用“warmupcosine退火”的组合前5个epoch从1e-4线性升到1e-3让模型先稳定预热之后按余弦曲线逐渐降到1e-5。这个调度方式我在多个任务上反复用过比固定学习率省心很多基本不用怎么调就能收敛得不错。损失函数做了加权组合# 简化版示例 focal_loss FocalLoss(gamma2.0, alphaclass_weights) gender_loss nn.BCEWithLogitsLoss()(gender_logits, gender_labels) emotion_loss focal_loss(emotion_logits, emotion_labels.long()) total_loss 0.4 * gender_loss 0.6 * emotion_loss权重比例取0.4比0.6是因为情感任务更难样本也多理应在总损失里占更大比重。性别任务本身就简单给了太高权重会让模型过早陷入“只优化性别、忽略细节纹理”的局部最优。Focal Loss的核心思想是让模型远离那些已经被分对的简单样本。在情感分类里“开心”类样本占比高而且容易区分如果不加抑制反向传播梯度会被这些简单样本主导模型对少数类越来越不敏感。引入Focal Loss后模型开始把更多注意力放在“厌恶”“恐惧”这些容易被错分的类别上。4.2 改进前后的实验数据与结论最终实验对比结果如下指标普通卷积Baseline深度可分离卷积注意力性别准确率95.1%97.2%性别AUC0.9820.992情感准确率63.5%71.2%情感macro-F10.580.68CPU单帧推理耗时42ms31ms参数量5.2M2.3M有人看到这个表会疑惑性别分类都快97%了为什么情感分类才71%这是很正常的现象我在答辩前也做好了心理准备。情感分类本身主观性强同一张面孔不同标注者可能给出不同标签。七分类问题里的“恐惧”和“惊讶”在真实人脸图像上经常表现出相似的眼部特征机器难以区分是有物理原因的。情感任务的macro-F1从0.58拉到0.68这个提升主要归功于注意力机制和Focal Loss的组合。我单独做过消融实验只加注意力marco-F1提升到0.62只加Focal Loss提升到0.64两个一起加到了0.68。这说明两者作用机制不重叠一个在特征提取层面增强关键区域的响应一个在损失层面纠正类别分布偏差可以同时生效。4.3 从训练曲线判断过拟合与欠拟合训练过程中每跑完一个epoch我就记录训练集和验证集的损失。有一条规律值得提训练集损失一直下降验证集损失先降后升大概率过拟合了。我第一次跑情感任务时出现过这种情况当时的做法是增加数据增强强度、加了一层Dropout、再把训练epoch数减半。三者结合后验证集指标明显回升。还有一次验证集损失在前半段稳定下降后半段突然抖动回升我把学习率退火曲线从“线性”改成“余弦”并加入了模型早停机制连续15个epoch验证集macro-F1没有上升就停止训练恢复到历史最好模型。加上EMA指数移动平均对模型参数做平滑之后验证集指标稳定了很多也能自然地解释为“训练后期权重在最优附近震荡EMA相当于把多次采样的结果做平均”。5. 应用系统实现与实时视频流推理5.1 应用整体架构从模型评估到系统部署模型训练和评估做完之后摆在面前的是最后一个大问题怎么让评委直观看到这个模型能用我选择做一款桌面应用功能模块分成三层。最外层是界面层用PyQt5实现。主窗口左侧显示实时视频画面右侧显示性别和情感的条形置信度图底部有一块区域滚动显示最近10帧的分类结果曲线。第二层是推理层用ONNX Runtime加载导出的模型接收界面层传来的图像帧返回两个分类的结果和概率。最底层是数据层封装了摄像头读取、图片文件读取、预处理操作。这层设计的核心是线程隔离。摄像头采集和模型推理都属于耗时操作如果放在UI主线程里界面会直接卡住。我的做法是开两个后台线程一个线程负责从摄像头抓帧并把最新帧放入队列另一个线程负责从队列取帧、推理、把结果放回另一个队列。主线程只负责定时从结果队列取数据刷新界面。这样即使模型推理偶尔超过100毫秒界面依然流畅。5.2 界面与调用逻辑设计界面逻辑不追求复杂追求稳定可演示。整个应用只需要三个核心控件视频显示区域、置信度条形区域、控制按钮区域。控制按钮只有“打开摄像头”“选择图片”“退出”三个没有花哨的功能。置信度条形图我用的是QLabel加手动绘制的彩色条没有引入额外的绘图库。左边是性别条蓝色表示男性置信度粉色表示女性置信度右边是情感条七种颜色分别对应七类表情。这样设计的好处是评审现场所有人都能一眼看懂模型输出的含义比密密麻麻的数字直观得多。实战心得做应用Demo时宁可功能少而精也不要为了凑功能加一堆用不上的按钮。答辩现场最糟糕的情况是操作者手忙脚乱找不到按钮。5.3 模型导出与量化推理细节PyTorch模型去部署环境跑我走的是ONNX这条路。导出时固定输入尺寸为112x112把动态轴禁掉这样ONNX Runtime可以做一些静态优化推理速度更快。核心代码是几行import torch import onnxruntime as ort model FaceMultiTaskNet(backbone...) model.load_state_dict(torch.load(best_model.pth, map_locationcpu)) model.eval() dummy_gender torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy_gender, face_multitask.onnx, input_names[input], output_names[gender_logits, emotion_logits], dynamic_axesNone, opset_version11 )导出后加载sess ort.InferenceSession(face_multitask.onnx) inputs {sess.get_inputs()[0].name: input_image_numpy} gender_logits, emotion_logits sess.run( None, inputs ) # 对 logits 做 softmax 得到概率导出这个环节我踩过大坑。如果不指定输入输出名字、不固定批大小、在原始模型里混入了BatchNorm训练状态ONNX导出后推理结果和PyTorch里完全对不上。排查这类问题时要先在单张图上逐层对比特征再把输入规范到完全相同才能定位是导出问题还是推理前置处理问题。5.4 视频流推理的时序平滑处理纯模型推理出来的单帧结果在视频流里会有明显的抖动主要原因是摄像头的运动模糊、压缩噪声和脸部轻微的移动让每一帧的纹理细节都在变化。为了显示稳定我在后端加了一个滑动窗口平均记录最近5帧的softmax概率取平均作为最终输出。这个处理对情感分类非常重要。正常情况下“开心”的置信度应该在0.7到0.9之间震荡如果不做平滑界面上的置信度条会像心电图一样乱跳看起来非常不专业。做了平滑之后置信度曲线变得平顺即使中间某一帧被误判成“中性”最终输出也大概率还是“开心”。平滑窗口的长度我在正式版本里取5试过3和103还偶尔抖10虽然更稳但响应明显变慢5是个居中的选择。6. 常见问题与排查技巧实录6.1 高频问题速查表问题现象可能原因处理办法训练损失变成NaN学习率过大或输入出现NaN降低学习率检查预处理是否除零情感分类老是预测“开心”和“中性”类别不平衡严重模型偏向多数类加权损失过采样少数类权威分类时总把“恐惧”判成“惊讶”这两类特征本身相似加注意力模块和局部特征增强ONNX导出的模型结果和PyTorch不一致BatchNorm层状态或预处理不一致导出前eval注入逐帧对比性别分类在视频流里比图片测试差很多摄像头画面模糊或光照变化加入帧平滑提高人脸检测置信度阈值训练集精度高但测试集低过拟合或验证集信息泄漏增加增强、加Dropout、复查数据划分6.2 印象最深的三个调试场景第一个坑是关于灰度图像和预训练权重的。我一开始把48x48的灰度表情图复制成三通道然后加载了在彩色自然图像上预训练过的权重。结果训练了好几个epoch情感分类精度一直在45%附近打转。后来我仔细想了下原理预训练权重大量依赖彩色通道的丰富统计信息灰度图的三通道完全相同等于把同一个信号重复了三遍但权重并不适应这种频率分布。最终我放弃加载自然图像预训练权重选择从头训练情感分支效果反而提升到60%以上。这个例子说明预训练权重不是万能的输入分布和预训练域相差太大时硬迁移不如从头学。第二个坑是视频流推理时结果抖动。首次用摄像头测试我发现即使人完全不动表情概率也在不停跳变。当时第一反应是模型不稳定但我把单张静态图片反复测试置信度基本稳定说明是视频帧间的噪声和抖动导致的。后来用滑动窗口平均解决了。这个问题的教训是单帧精度高不代表视频流体验好工程上需要在模型之外做时序约束。第三个坑是情感分类的混淆问题。我完善了混淆矩阵发现“恐惧”和“惊讶”互相混淆的比例接近三成。后来在输出层看特征可视化发现这两类在眼睛和嘴部的反应确实高度相似。我尝试了几种办法比如在损失函数里添加类别间相似性惩罚效果有限。最后我接受了一个现实情感本身存在模糊性机器不可能完全解决人类标注都不一致的问题。但我在答辩时把这个现象解释为一个可扩展的切入点反而体现了对任务的深入理解。6.3 一个容易被忽视的细节训练测试分布一致性我在后期查问题时发现所有预处理步骤必须保证训练和测试走同一套流程。比如训练时数据增强有“随机水平翻转”测试时如果忘了关掉模型就会在每帧里看到随机翻转的人脸真正的人脸方向反而没有稳定输入。这类bug非常隐蔽因为它不会让你程序报错只会让推理结果漂移。为了保证一致性我把所有预处理封装成一个类训练和推理共用。在代码层面区分“是否启用随机增强”开关而不是复制一段预处理逻辑到测试脚本里。实践证明这个做法减少了至少一半的调试时间。写在最后的实际操作体会这个项目做下来我最大的收获不是那些准确率数字而是搞清楚了完整深度学习项目的真实链路。以前看论文觉得模型结构最玄妙动手之后才发现数据划分、预处理对齐、训练策略、部署推断、界面整合每个环节出问题都能让最终结果非常难看。很多时候模型精度上不去不是结构不够好而是数据在输入模型之前就已经不对了。我还想特别强调一点改进型研究最重要的不是“我改得多”而是“我能证明每个改进都有用”。消融实验、控制变量、训练曲线分析这些看起来琐碎的工作才是答辩时最能说服评委的部分。我当时在每个改进点上都保留了对应的对照数据从baseline到轻量化再到加注意力、调损失函数每一步都有前后指标对比。评委看完之后说了一句话“这个工作做得比较扎实。”这比任何夸奖都让我踏实。如果后面还想继续扩展比较自然的方向是把单帧分类换成视频序列建模比如加入时序模型来利用帧间信息或者增加年龄属性预测做多任务扩展。不过即便不加这些当前这套结构拿到答辩现场去做演示无论是实时性、可视化还是实验完整性都已经足够撑起一个高分毕设了。