ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CNN人脸表情识别实战:从FER2013训练到OpenCV实时推理

CNN人脸表情识别实战:从FER2013训练到OpenCV实时推理 简介这是一个基于卷积神经网络CNN的人脸面部表情识别完整项目集源码、论文、数据集与训练好的模型于一体专为计算机相关专业准备毕业设计或课程大作业的学生设计同时适合需要项目实战练习的Python学习者。项目评审得分98分代码经过严格调试可正常运行可帮助理解图像分类、特征提取与模型训练的完整流程。压缩包共包含2000个文件以1963张jpg图片数据集为主体另有21个png图片、3个Python源码文件、2个XML配置、2个h5模型权重文件、1个Jupyter Notebook教学脚本以及json、yaml等辅助文件整体约131.94MB规模适中便于下载使用。已有182人学习浏览。项目中训练好的model.h5与model_weight.h5可直接加载验证识别效果FaceEmotionClassifier.ipynb逐步展示了从数据预处理、卷积网络搭建、训练到评估的实现过程配合fer2013等表情图片数据集既能复现论文实验也便于二次开发与拓展。1. 人脸表情识别这个CNN项目值不值得做先看清四件套的分量用Python和卷积神经网络做人脸面部表情识别深度学习一套跑下来不难真正花时间的是数据和对齐。这个项目标题把源码、论文、数据集、训练好的模型四件套打包在一起本质上是为了让你走通一条完整的工程链路从一张带表情的人脸图像出发经过卷积神经网络的多层特征提取输出七分类中的某一个——生气、厌恶、恐惧、开心、难过、惊讶、中性。它适合深度学习入门者当全流程练习也适合毕设党拿它当底座去改结构、换数据、加界面。但想真正复现出论文里的准确率先得做好踩坑准备。2. 需求分析与CNN选型表情识别不是普通图像分类的简单搬运2.1 表情识别的任务定义与7类标签映射人脸表情识别要做的事情是把输入的人脸区域映射到一个离散的情绪类别上。学术和工程里最常用的基准是FER2013数据集一共35887张48x48像素的灰度图按训练集、公开测试集、私有测试集三部分划分标签是0到6的整数。这个映射顺序在几乎所有开源项目里都是同一套项目包里的论文和源码一般也会写明。编码0123456表情angry 生气disgust 厌恶fear 恐惧happy 开心sad 难过surprise 惊讶neutral 中性拿到项目包里的CSV数据集时第一件事就是确认标签顺序和这张表一致。如果项目作者换过顺序而你没发现后面所有训练、评估、论文准确率对比全部作废。更隐蔽的情况是有人把标签从1开始编号这样0就成了空白类模型的输出维度变成8推理时取argmax会整体偏移一个位置画出来的结果是错的但loss看起来还正常。还要注意这个任务是单标签分类不是多标签也不是目标检测。输入是一张对齐好的人脸区域输出是一个概率分布取argmax得到最终类别。检测负责把脸从背景里框出来识别负责对框里的表情分类这两个是独立环节。项目里一般也是分开跑的训练只用识别网络推理时才把检测接在前面。2.2 为什么选CNN而不是ViT或传统机器学习标题指名要卷积神经网络这不是随便拍的48x48的小尺寸灰度图上CNN是目前性价比最高的方案。传统机器学习路线比如HOG特征加SVM在表情识别上不是不能跑但表情差异是局部且非刚性的嘴角上扬几度、眼角皱纹轻微聚集这些细粒度特征HOG很难稳定刻画需要手工设计特征工作量大还容易过拟合。Vision Transformer这两年很热但在这个场景里容易翻车。ViT依赖注意力机制需要大量数据或者强大的预训练权重去拟合FER2013整个数据集不到三万张训练图直接训ViT很容易欠拟合准确率打不过小型CNN。除非你想做迁移学习用ImageNet预训练的ViT骨干来提特征那项目复杂度和训练成本都会上一个大台阶CPU机器基本跑不动。小型CNN的优势很具体参数量在几百万量级一张入门级显卡就能训CPU也能慢慢扛完一个epoch推理速度快接到摄像头实时流里毫无压力。这类项目选CNN是稳妥的工程决策。你去看项目里的网络结构图多半是三到四个Conv-BN-ReLU块加全连接层。堆叠更深或引入注意力模块属于进阶改动先把基础结构跑通再谈。2.3 四件套的正确打开顺序与环境准备标题里四样东西打包本质上是一个可复现工程包。正确打开顺序不是先跑train.py而是先验证、再读数据、最后训练。第一步先加载训练好的模型对样例图做推理确认环境通、模型没坏。第二步读数据集的读取代码理解像素是怎么从CSV里解析出来的这决定你后面换自己的数据集时改哪里。第三步看论文里的数据划分、预处理和训练超参数这是复现准确率的前提。第四步才回到网络结构这时候你已经知道数据形态看结构就不会抽象。环境准备上也有些规律。这种项目对Python版本不挑3.8到3.10都能跑核心依赖就是PyTorch、OpenCV、NumPy、pandas这几样。很多人在环境上卡半天其实问题不在版本高低而是装了两个相互冲突的PyTorch版本。我的习惯是用虚拟环境单独给项目建一个环境pip install torch opencv-python numpy pandas一步到位不往系统Python里瞎塞。深度学习入门的第一个门槛往往不是算法而是环境但这一关过了后面就顺了。3. 数据准备与预处理数据集读入、人脸对齐与增强参数设置3.1 FER2013数据集的CSV读入与标签映射FER2013的数据不是图片文件而是一个CSV。每行有emotion、pixels、Usage三列pixels是2304个0到255的灰度值用空格分隔的字符串。读取代码的常见写法如下import pandas as pd import numpy as np df pd.read_csv(fer2013.csv) pixels df[pixels].tolist() emotions df[emotion].tolist() X np.zeros((len(pixels), 48, 48), dtypenp.uint8) for i, p in enumerate(pixels): arr np.array(p.split(), dtypeint) X[i] arr.reshape(48, 48)逻辑说明先把像素字符串拆成2304个整数再reshape成48x48的单通道灰度图。np.array(p.split(), dtypeint)是兼容性最稳的写法比np.fromstring在新版本NumPy里少很多警告。dtypenp.uint8是图像的标准存储类型能省内存如果随手写成float64显存占用会是8倍训练速度肉眼可见地变慢。读完后按Usage列拆训练集和测试集train_mask df[Usage] Training public_mask df[Usage] PublicTest private_mask df[Usage] PrivateTest X_train, y_train X[train_mask], emotions[train_mask] X_val, y_val X[public_mask], emotions[public_mask] X_test, y_test X[private_mask], emotions[private_mask]这里必须用布尔掩码不要按行号硬切。有些项目包为了简化直接按前80%后20%切这会破坏官方的数据分布导致验证集准确率虚高或偏低和论文数字永远对不上。官方划分里训练集有28709张公开测试集3589张私有测试集3589张如果你看到的数字不是这样数据读取这一步就有问题。3.2 人脸检测与对齐不去除背景会让模型学会“认背景”FER2013的数据本身已经是裁剪对齐好的人脸训练时不需要再来一步检测。但一旦你想换自己的数据集或者要跑摄像头实时识别就必须在流程里加人脸检测和对齐。图里如果带着头发、衣服、背景CNN很容易学到“这个人穿红衣服所以是开心”这种乌龙相关性。在公开数据集上不明显因为都是正脸框但自制数据和实拍视频里非常突出。常见做法是用OpenCV自带的Haar Cascade做人脸检测import cv2 face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) )逻辑说明detectMultiScale返回一组(x, y, w, h)矩形。拿到矩形后把人脸区域裁剪出来再缩放到48x48输入模型。检测本身不参与训练只负责把有效区域框出来。参数说明scaleFactor1.1表示每次搜索窗口放大10%值越小检测越慢但越精准minNeighbors5是邻居阈值越大误检越少但漏检越多minSize(48, 48)过滤掉太小的框太小的脸缩放到48x48后细节全丢识别没有意义。注意Haar Cascade对侧脸和低头姿态很脆弱如果你做的不是正脸表情识别需要换MTCNN或MediaPipe做检测。对齐细节容易被忽略检测出的人脸框不一定严格居中两只眼睛可能不在同一水平线。严谨的项目会再做眼睛定位和仿射变换把两眼连线拉到水平方向。但这是加分项不是必选项。如果训练数据已经是对齐过的推理时只要以检测框中心裁出来效果差别不大。3.3 数据增强与归一化参数怎么设才不过拟合表情识别训练集不到三万张直接硬训很容易过拟合数据增强是标配。PyTorch里的常见组合如下from torchvision import transforms train_transform transforms.Compose([ transforms.RandomRotation(10), transforms.RandomHorizontalFlip(p0.5), transforms.RandomAffine(degrees0, translate(0.05, 0.05)), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]) ])逻辑说明先做几何扰动再转Tensor最后归一化到[-1, 1]。RandomRotation(10)是±10度内随机旋转RandomHorizontalFlip是水平翻转RandomAffine做轻微平移模拟摄像头抖动。参数说明旋转角度不要超过15度。脸歪太多表情语义就变了开心可能被模型认成惊讶。水平翻转可以做因为中性、开心、难过这些表情在左右方向上没有语义差异但如果数据集里有文字、logo这类左右不对称的标志物翻转就要慎重。归一化的mean[0.5], std[0.5]是灰度图最常用的设定把0到255的像素压到-1到1。如果输入直接是numpy数组没走ToTensor要手动除以255再归一化否则模型精度会掉加载训练好的权重时还会出现特征分布错乱。一个经常被忽略的点验证集和测试集不要做随机增强只做缩放和归一化。测试时的随机旋转会让准确率不稳定每次评估结果都不一样论文里的数字就没法对齐。4. 用PyTorch训练表情识别CNN网络结构、训练循环与模型落盘4.1 网络结构定义Conv-BN-ReLU堆叠与残差连接的取舍小型表情识别CNN最常见的结构是三个Conv-BN-ReLU块再接全连接层。下面是这类项目的典型写法import torch.nn as nn class EmotionCNN(nn.Module): def __init__(self, num_classes7): super().__init__() self.features nn.Sequential( nn.Conv2d(1, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Dropout(0.5), nn.Linear(128 * 6 * 6, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.3), nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x))逻辑说明输入是1x48x48的单通道灰度图经过三次卷积加池化特征图从48x48依次缩到24x24、12x12、6x6通道数从1涨到128。最后全连接部分用两个Dropout防止过拟合。kernel_size3, padding1让卷积不改变特征图尺寸只有池化负责下采样边界信息保留得更好。参数说明128 * 6 * 6这个数字不是乱写的48除以三次2的池化正好等于6改输入尺寸时这里必须同步改否则会报维度不匹配。inplaceTrue能省显存但某些显卡上会和小批量并行冲突遇到CUDA报错就改回False。残差连接在这个规模下收益很有限。三到四层浅网络加跳跃连接实验做下来提升不到一个点还徒增代码复杂度。如果想把网络加深到五六层再考虑在每个Conv块里加nn.Identity()的残差分支那是后话。4.2 训练循环与损失函数类别不均衡的两种处理表情数据集的类别分布不均匀生气和厌恶的样本数比开心少很多。直接训练模型会倾向把模糊样本预测成高频类别训练集准确率好看测试时短板暴露无遗。两类常用补救给损失函数加权或者采样时加权。先从最简单的class weight开始import torch from torch.utils.data import DataLoader from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau class_freq torch.tensor([3995, 436, 4097, 7215, 4830, 3171, 4965], dtypetorch.float32) class_weight class_freq.sum() / (7 * class_freq) criterion torch.nn.CrossEntropyLoss(weightclass_weight) model EmotionCNN(num_classes7) optimizer Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler ReduceLROnPlateau(optimizer, modemax, factor0.5, patience3) for epoch in range(60): model.train() for X_batch, y_batch in train_loader: optimizer.zero_grad() logits model(X_batch) loss criterion(logits, y_batch) loss.backward() optimizer.step() val_acc evaluate(model, val_loader) scheduler.step(val_acc)逻辑说明class_weight用频率倒数归一让少样本类别在loss里的权重更大。CrossEntropyLoss传入weight后每个样本的loss会乘以对应类别权重。优化器用Adam比SGD稳定不需要手调momentum。参数说明lr1e-3是Adam在图像分类上的安全起点跑几个epoch后如果loss震荡不降再手动降到3e-4。weight_decay1e-4是合适的L2正则强度太大让权重绑太死。ReduceLROnPlateau的modemax表示监控验证集准确率连续3个epoch不涨就学习率减半。这个策略比固定步长衰减实用。patience3是耐心值设置太小会让学习率下探太快模型还没收敛就停在低学习率训练曲线后期会很平。采样加权是另一个思路用WeightedRandomSampler让每个batch里低频类别出现的概率更高和loss加权叠加使用。但我一般先只做loss加权因为改动小、不容易引入训练集epoch分布错乱的问题。4.3 训练好的模型怎么保存、加载与评估训练结束后模型和配套信息都要落盘这就是项目包里“训练好的模型”的来源torch.save({ model_state_dict: model.state_dict(), class_to_idx: {0: angry, 1: disgust, 2: fear, 3: happy, 4: sad, 5: surprise, 6: neutral}, input_size: 48 }, emotion_cnn.pt)逻辑说明保存state_dict而不是整个模型对象这样以后换Python版本或PyTorch版本都能加载不会碰到反序列化兼容问题。把标签映射一起存进去推理时才能把类别ID转回可读名称。加载并评估的写法checkpoint torch.load(emotion_cnn.pt, map_locationcpu) model EmotionCNN(num_classes7) model.load_state_dict(checkpoint[model_state_dict]) model.eval() correct 0 total 0 with torch.no_grad(): for X_batch, y_batch in test_loader: preds model(X_batch).argmax(dim1) correct (preds y_batch).sum().item() total y_batch.size(0) print(fTest Acc: {correct / total:.4f})逻辑说明map_locationcpu让模型先加载到CPU之后要用GPU再.to(cuda)避免在无GPU环境下直接崩。model.eval()会关闭Dropout和BatchNorm的统计更新漏掉这行推理结果每次都不一样。参数说明load_state_dict报size mismatch时头号嫌疑是网络结构和保存时不一致最常见的是num_classes传错。评估务必包在torch.no_grad()里否则每个batch都建计算图显存很快被吃干净。评估指标不能只看准确率类别不均衡下准确率很骗人。sklearn.metrics.classification_report能打出每类的精确率、召回率和F1顺手看一下混淆矩阵哪两个表情互相混淆严重就清楚了。常见的是“难过”和“中性”互认、“恐惧”和“惊讶”互认这是数据集本身标注噪声导致的不是模型训练出了问题别在这上面死磕。5. 表情识别项目避坑指南5条真实踩过又踩回去的坑5.1 训练集和验证集数据错位准确率虚高但模型是废的现象训练loss正常下降验证集准确率一开始就异常高超过0.8。你抽一张图出来看发现图像内容和标签对不上。原因读取CSV时像素字符串解析出错最常见的是忘了reshape或者用错分隔符。数据错位后模型学到的是“某种像素分布和某个标签的相关性”不是真正的表情语义换个环境立刻打回原形。解决训练前先做可视化自检从每个类别里各抽5张图用matplotlib拼成网格标题打上标签人眼确认一遍。这步30秒能省后面一整天的排错。数据划分一定要用官方Usage列做布尔掩码不要用行号切片。5.2 灰度图还是彩色图通道数不一致导致加载失败现象用训练好的模型推理时报size mismatch或者不报错但预测结果全是同一个类别。原因训练用单通道灰度图推理时cv2.imread读进来是BGR三通道或者PIL的Image.open没做.convert(L)。通道数不一致要么模型第一层权重形状对不上直接报错要么你没报错但像素分布完全不同模型按训练时的灰度特征去解读三通道数据输出自然一团糟。解决统一入口。推理时统一走Image.open(path).convert(L).resize((48, 48))numpy数组用cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)转灰度。把预处理封装成函数训练、测试、摄像头推理三段代码都调它不要在两端各写一份复制粘贴漏改是这类问题的主要来源。5.3 过拟合严重训练集0.98验证集只有0.62现象训练集准确率冲到0.98验证集卡在0.6上下训练loss还在降验证loss却在涨。原因模型容量对小数据集偏大数据增强强度不够。三万多张图配128通道三层卷积参数足够把训练集背下来。另一个常见原因是Dropout设太低有人为了“提精度”把0.5改成0.2直接过拟合。解决增强加档位Rotation从10度加到15度Affine平移从0.05加到0.1Dropout回到0.5weight_decay保持1e-4。一次只改一个变量不要同时调三个否则根本不知道哪个起作用。调完后验证集如果还能涨到72到75就说明是过拟合问题继续小幅微调。5.4 模型推理很快但接摄像头后FPS跌到2帧现象GPU上单张推理不到10毫秒一接OpenCV摄像头就卡到2到3帧画面一顿一顿。原因瓶颈不在CNN在人脸检测。Haar Cascade在每一帧全分辨率图上检测1920x1080的帧一检测就是200毫秒以上。还有人每帧都做缩放和归一化Python端的cv2操作加起来比模型推理还慢。解决抽帧检测而不是逐帧检测。每5帧做一次人脸检测中间的帧沿用上一次的检测框。检测前先把帧缩放到640x480再喂给Haar Cascade。CNN的输入始终是48x48整体FPS能回到10以上。GPU显存富余的话把model.half()转半精度推理速度还能再快一点。5.5 论文里的准确率和自己复现对不上现象论文声称准确率0.91你按代码严格训练只有0.78怎么调都上不去。原因评估口径不一致。论文可能用了投票集成对每张测试图做多次数据增强预测概率平均后取argmax这通常能带来3到5个点的提升。也可能论文在测试前做了额外对齐预处理你只用了原始灰度图。还有一种可能是训练轮数不同论文训了200个epoch你只跑50个。解决先加载项目包里的训练好的模型测一次确认论文数字是可复现的。然后复现时对齐三件事数据划分用官方Usage列、预处理和论文一致、epoch和学习率调度一致。不要在模型结构上做发散改动那只会让数字更难对上。有条件就把测试时投票也加上让结果尽量贴近论文。6. 把训练好的模型接到摄像头实时表情推理的落地技巧训练好的模型不接到真实画面上价值少一半。用OpenCV读摄像头、抽帧检测、裁剪人脸、交给CNN推理、把标签刷新到画面上这段流程代码量不大import cv2 import numpy as np import torch cap cv2.VideoCapture(0) model.eval() frame_id 0 face_rect None while True: ok, frame cap.read() if not ok: break frame_id 1 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if frame_id % 5 0: faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(48, 48)) if len(faces) 0: face_rect max(faces, keylambda r: r[2] * r[3]) if face_rect is not None: x, y, w, h face_rect roi cv2.resize(gray[y:yh, x:xw], (48, 48)) roi roi.astype(np.float32) / 255.0 roi (roi - 0.5) / 0.5 inp torch.from_numpy(roi).unsqueeze(0).unsqueeze(0) with torch.no_grad(): prob torch.softmax(model(inp), dim1)[0] idx prob.argmax().item() label checkpoint[class_to_idx][idx] cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow(emotion, frame) if cv2.waitKey(1) 0xFF ord(q): break逻辑说明frame_id % 5 0是抽帧检测的核心检测到的脸框会保留给中间5帧用帧率能从3提到10左右代价只是人脸快速转开时框会短暂跟丢。归一化推理端手动做了喂给模型的张量形状是1x1x48x48和训练时一致。参数说明检测框取最大面积的那个因为近距离的人脸通常是画面里最需要识别的。prob用softmax转成概率方便看类别信心。想要更稳的标签显示对5帧里的预测结果做滑动平均再取argmax能明显减少表情在临界状态下的抖动。我做这类项目有个固定习惯模型的输入预处理单独封装成一个函数训练、测试、摄像头推理三段代码都调它。归一化、尺寸、通道顺序只改一处其他环节永远不会因为复制粘贴漏改而翻车。这个习惯救过我很多次希望帮到你。本文还有配套的精品资源点击获取
返回列表