我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Python卷积神经网络实现人脸表情识别:从数据集到实时摄像头

Python卷积神经网络实现人脸表情识别:从数据集到实时摄像头 简介这份资源面向计算机相关专业正在做毕业设计的学生以及需要项目实战练习的学习者提供了一套基于卷积神经网络的人脸表情识别系统完整方案。内容涵盖源代码、数据集、论文与训练好的模型项目经导师指导并通过评审源码均经本地编译与严格调试可稳定运行难度适中。压缩包共36个文件约446.05MB包含14个py脚本、5个ipynb笔记本、5个docx与1个doc论文、1个pdf、1个pptx答辩演示、1个mp4示例视频以及pkl模型文件、xml人脸检测配置和多个zip子项目包覆盖CNN、VGG、ResNet等多种网络实现与对比测试。已有156人学习下载。读者可据此获得从数据预处理、模型训练到测试评估的完整流程参考论文与答辩材料梳理研究思路借助预训练模型快速复现效果适合作为毕业设计模板或深度学习入门实战案例。1. 从零复现人脸表情识别一套能跑通的 Python 卷积神经网络方案长什么样你手上如果只有一台带显卡的笔记本、一份 FER2013 或者 RAF-DB 数据集想在一周内跑出一个能实时识别人脸七种表情的 demo那这套「Python 卷积神经网络 人脸表情识别」的组合就是最省事的路径。它解决的核心问题很具体把一张 48×48 的灰度人脸图映射到生气、厌恶、恐惧、开心、悲伤、惊讶、中性这七个类别上。适合谁适合已经会写 Python、装过 PyTorch 或 TensorFlow、但没完整做过一个视觉项目的人也适合手里有数据集却卡在预处理和调参上的熟手。整套东西拆开就是四块源代码、数据集、训练好的模型权重、以及一份把实验讲清楚的论文。下面我按自己实际跑过的顺序把每一块怎么落地讲透。2. 数据集怎么选怎么读FER2013 与 RAF-DB 的取舍和加载代码2.1 两个主流数据集的差异与选型理由人脸表情识别能用的公开数据集不少但真正适合个人复现的主要是 FER2013 和 RAF-DB。FER2013 是 2013 年 Kaggle 竞赛放出来的全部是 48×48 灰度图训练集 28709 张、验证集 3589 张、测试集 3589 张七类标签优点是格式统一、体量小、一张 1080Ti 半小时能跑完一轮缺点是标注噪声大尤其「恐惧」和「厌恶」两类错标严重很多图肉眼都分不清。RAF-DB 是真实场景下的网络图片分辨率 100×100 彩色标注质量高得多基本版 15339 张训练、3068 张测试但类别不均衡而且下载要走申请流程。我的建议是第一次复现先用 FER2013 把流程跑通因为它的 CSV 格式最省心一张表三列——emotion、pixels、Usagepixels 是 2304 个空格分隔的灰度值。等流程顺了再换 RAF-DB 提升精度。常见做法是把 FER2013 的 CSV 直接读进内存转成 numpy 数组再喂给 DataLoader。2.2 用 pandas 读 FER2013 并转成可训练张量import pandas as pd import numpy as np import torch from torch.utils.data import Dataset, DataLoader class FER2013Dataset(Dataset): def __init__(self, csv_path, usageTraining, transformNone): # 只读取指定划分避免训练时混入验证集 df pd.read_csv(csv_path) df df[df[Usage] usage].reset_index(dropTrue) # pixels 列是空格分隔的字符串拆成 2304 维再 reshape 成 48x48 self.images np.array( [np.fromstring(p, dtypenp.float32, sep ) for p in df[pixels]] ).reshape(-1, 48, 48) self.labels df[emotion].values.astype(np.int64) self.transform transform def __len__(self): return len(self.labels) def __getitem__(self, idx): img self.images[idx] # 归一化到 [0,1]CNN 对输入尺度敏感这步不能省 img img / 255.0 if self.transform: img self.transform(img) # 增加通道维变成 1x48x48 img np.expand_dims(img, axis0) return torch.tensor(img, dtypetorch.float32), torch.tensor(self.labels[idx]) train_set FER2013Dataset(fer2013.csv, usageTraining) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2)这段代码的关键点有三个。第一np.fromstring在新版 numpy 里已经弃用如果报 DeprecationWarning换成np.frombuffer配合bytes转换或者直接用np.array(p.split(), dtypenp.float32)后者慢一点但兼容性好。第二Usage字段有三个值Training、PublicTest、PrivateTest验证时别把 PublicTest 当训练数据用这是新手最容易翻车的地方。第三num_workers在 Windows 上设成 0 更稳设 2 以上有时会卡在共享内存上。2.3 数据增强别在灰度小图上乱用 ColorJitterFER2013 是灰度图很多人习惯性加上 ColorJitter结果亮度对比度抖动把表情特征也抖没了。我一般只做三件事随机水平翻转概率 0.5、随机小角度旋转±10 度、随机裁剪先 padding 4 再裁回 48。水平翻转要注意「厌恶」和「恐惧」翻转后语义不变可以放心用。旋转角度别超过 15 度否则嘴角和眉毛的相对位置会失真模型学到的就是噪声。from torchvision import transforms train_transform transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(p0.5), transforms.RandomRotation(10), transforms.RandomCrop(48, padding4), transforms.ToTensor(), ])注意ToPILImage要求输入是 HWC 格式的 uint8 或 float前面归一化到 [0,1] 的 float 也能吃但更稳妥的做法是先转 uint8 再进 transform避免精度损失。3. 卷积神经网络结构怎么搭一个 4 层 CNN 的完整实现与参数解释3.1 为什么不用 ResNet 直接上很多人一上来就搬 ResNet18把第一层改成单通道结果在 FER2013 上过拟合得厉害验证集准确率卡在 55% 上不去。原因很简单FER2013 只有两万多张 48×48 的小图ResNet 的参数量对这个小数据集来说太大了。我实际对比过一个 4 层卷积 3 层全连接的轻量网络参数量控制在 130 万左右验证集能到 66% 上下比硬套 ResNet 高 8 到 10 个百分点。所以结构选型的原则是数据集越小网络越浅正则化越重。3.2 四层卷积网络的 PyTorch 实现import torch.nn as nn import torch.nn.functional as F class EmotionCNN(nn.Module): def __init__(self, num_classes7): super().__init__() # 第一层1x48x48 - 64x24x24 self.conv1 nn.Conv2d(1, 64, kernel_size3, padding1) self.bn1 nn.BatchNorm2d(64) # 第二层64x24x24 - 128x12x12 self.conv2 nn.Conv2d(64, 128, kernel_size3, padding1) self.bn2 nn.BatchNorm2d(128) # 第三层128x12x12 - 256x6x6 self.conv3 nn.Conv2d(128, 256, kernel_size3, padding1) self.bn3 nn.BatchNorm2d(256) # 第四层256x6x6 - 256x3x3 self.conv4 nn.Conv2d(256, 256, kernel_size3, padding1) self.bn4 nn.BatchNorm2d(256) self.pool nn.MaxPool2d(2, 2) self.dropout nn.Dropout(0.5) # 第四层输出 256x3x3 2304 维 self.fc1 nn.Linear(256 * 3 * 3, 512) self.fc2 nn.Linear(512, 256) self.fc3 nn.Linear(256, num_classes) def forward(self, x): x self.pool(F.relu(self.bn1(self.conv1(x)))) x self.pool(F.relu(self.bn2(self.conv2(x)))) x self.pool(F.relu(self.bn3(self.conv3(x)))) x self.pool(F.relu(self.bn4(self.conv4(x)))) x x.view(x.size(0), -1) # 展平 x self.dropout(F.relu(self.fc1(x))) x self.dropout(F.relu(self.fc2(x))) return self.fc3(x)逐层解释一下。每个卷积块都是 Conv → BatchNorm → ReLU → MaxPool 的顺序BatchNorm 放在 ReLU 前面是标准做法能让训练稳定很多学习率可以开到 1e-3 而不炸。四次池化把 48 降到 3感受野足够覆盖整张脸。Dropout 设 0.5 是经验值设 0.3 容易过拟合设 0.7 又欠拟合。全连接层从 2304 降到 512 再降到 256最后输出 7 类中间两次降维是为了减少参数量。3.3 训练循环与三个必调参数import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model EmotionCNN(num_classes7).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5) for epoch in range(60): model.train() for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() scheduler.step()三个必调参数学习率初始 1e-3配合 StepLR 每 20 轮乘 0.5这是我在 FER2013 上试出来收敛最稳的组合weight_decay 设 1e-4再大模型会欠拟合batch_size 设 64设 128 时显存占用翻倍但精度提升不到 1 个点。训练轮数 60 轮足够第 40 轮之后验证集准确率基本不动了再训就是浪费电。4. 训练完的模型怎么用推理、实时摄像头与常见翻车排查4.1 单张图片推理与置信度输出import cv2 import torch.nn.functional as F def predict_emotion(model, face_gray, device): # face_gray 是已经裁剪好的 48x48 灰度图 face_gray face_gray / 255.0 tensor torch.tensor(face_gray, dtypetorch.float32).unsqueeze(0).unsqueeze(0).to(device) model.eval() with torch.no_grad(): logits model(tensor) probs F.softmax(logits, dim1) conf, pred torch.max(probs, dim1) return pred.item(), conf.item()推理时一定要加model.eval()和torch.no_grad()前者关掉 Dropout 和 BatchNorm 的训练行为后者省显存。unsqueeze(0)两次是把 48×48 变成 1×1×48×48对应 batch 和 channel 两个维度。置信度低于 0.4 的时候我一般直接标成「不确定」因为 FER2013 训出来的模型在「恐惧」和「厌恶」上置信度普遍偏低硬报类别反而误导。4.2 摄像头实时识别的完整流程实时识别要串三件事人脸检测、裁剪对齐、表情分类。人脸检测用 OpenCV 自带的 Haar 级联就够虽然老但快CPU 上也能跑 30 帧。face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) cap cv2.VideoCapture(0) while True: ret, frame cap.read() gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.3, 5) for (x, y, w, h) in faces: roi gray[y:yh, x:xw] roi cv2.resize(roi, (48, 48)) pred, conf predict_emotion(model, roi, device) label f{EMOTIONS[pred]} {conf:.2f} cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(Emotion, frame) if cv2.waitKey(1) 0xFF ord(q): breakdetectMultiScale的 1.3 是缩放步长5 是邻域阈值调大 5 能减少误检但会漏掉侧脸。裁剪出来的 ROI 直接 resize 到 48×48不做直方图均衡因为训练时也没做保持一致最重要。4.3 避坑与排查五个我踩过的坑现象一验证集准确率一直在 25% 左右不动。原因通常是标签没对齐pixels 解析出来的顺序和 emotion 列错位了。解决方法是打印前 10 个样本的标签和图像肉眼看几张确认「开心」的图嘴角是上扬的。现象二训练 loss 下降但验证 loss 上升。典型过拟合。先加 Dropout 到 0.5再加 weight_decay 到 1e-4还不行就减一层卷积。别急着上数据增强增强在小数据集上有时反而拖慢收敛。现象三摄像头推理时画面卡顿。原因是每帧都在做model.eval()和 softmax其实 eval 只需在循环外调一次。把model.eval()提到 while 外面帧率能从 15 提到 28。现象四换 RAF-DB 后准确率暴跌。RAF-DB 是彩色 100×100直接 resize 到 48×48 灰度会丢信息。正确做法是保持三通道输入把第一层 Conv2d 的 in_channels 改成 3输入尺寸改成 100×100网络结构不用大动。现象五保存的模型加载后预测全是一个类。多半是保存时存了model.state_dict()但加载时用了torch.load直接赋给模型对象。正确写法是model.load_state_dict(torch.load(best.pth))加载前先实例化同结构模型。5. 把精度再往上推迁移学习、注意力机制与论文写作的取舍5.1 用预训练权重做迁移的两种姿势FER2013 上纯从头训66% 基本是天花板。想上 70%最实际的路子是迁移学习。第一种姿势是把在 ImageNet 上训好的 ResNet18 第一层改成单通道然后只训全连接层卷积层冻结学习率设 1e-4训 20 轮。第二种姿势是全部解冻学习率设 1e-5 微调训 30 轮。我实测第一种能到 68%第二种能到 71%但第二种容易过拟合需要把 Dropout 提到 0.6。import torchvision.models as models backbone models.resnet18(pretrainedTrue) # 把第一层改成单通道权重取三通道均值 backbone.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) with torch.no_grad(): backbone.conv1.weight[:] backbone.conv1.weight.mean(dim1, keepdimTrue) backbone.fc nn.Linear(512, 7)注意pretrainedTrue在新版 torchvision 里改成了weightsIMAGENET1K_V1如果报错就换新写法。第一层权重取均值是个小技巧比随机初始化收敛快 5 轮左右。5.2 加一个 SE 注意力模块值不值SESqueeze-and-Excitation模块在表情识别上确实有用因为它能让网络关注眉毛、嘴角这些判别性区域。加在第三层和第四层卷积之后参数量只增加不到 5%验证集能提 1.5 到 2 个点。但别每层都加加多了反而稀释特征。class SEBlock(nn.Module): def __init__(self, channels, reduction16): super().__init__() self.fc 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() y F.adaptive_avg_pool2d(x, 1).view(b, c) y self.fc(y).view(b, c, 1, 1) return x * yreduction 设 16 是原论文的默认值设 8 会多一倍参数但提升不明显。这个模块插在 conv3 和 conv4 后面forward 里改成x self.se_block(x)即可。5.3 论文部分怎么写才不空配套论文别写成「综述 实验」的八股。我一般按这个结构写引言部分用一段说清楚 FER2013 的标注噪声问题相关工作只提三篇最相关的比如原始 CNN 表情识别、VGG 迁移、注意力机制方法部分把网络结构和 SE 模块的公式写清楚实验部分给三张表——不同结构的准确率对比、混淆矩阵、以及和公开结果的对比。混淆矩阵一定要放它能直观看出「恐惧」和「厌恶」互相混淆有多严重这是审稿人最想看的。结论部分别写「未来展望」写「本方案在 CPU 上的推理延迟是 12ms适合嵌入式部署」这种具体数字。5.4 一个我坚持了三年的习惯每次改完网络结构我一定先跑 5 个 epoch 看验证集准确率有没有超过上一版的同期水平超了就继续没超就回滚。这个习惯帮我省了无数个通宵——很多看起来「应该有用」的改动其实在早期就露出苗头了。模型训练这件事后悔药是没有的但早停和回滚是有的。希望帮到你。本文还有配套的精品资源点击获取
返回列表