ARTICLE DETAIL

资讯详情

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

基于PyTorch的人脸表情识别:Fer2013+CNN/ResNet/VGG实战

基于PyTorch的人脸表情识别:Fer2013+CNN/ResNet/VGG实战 简介基于PyTorch卷积神经网络的人脸面部表情识别系统研究完整覆盖CNN、ResNet、VGG三种模型附带源码、论文与训练好的模型权重面向计算机相关专业毕业设计、课程作业及人脸表情识别入门研究者。压缩包共31个文件总大小约101.28MB包含14个Python脚本模型定义、训练与测试、5个Jupyter Notebook交互式实验、1份论文文档、1个答辩PPT、1个模型pkl权重文件、1个xml人脸检测器以及演示视频等目录结构清晰便于按模块学习。已有247人学习/下载经测试可正常运行并支持通过README快速上手。内容不限于模型代码还包括数据集处理脚本、数据划分脚本、图像表情映射工具、人脸标注工具、参考论文及其他开源项目打包足以支撑一个完整的表情识别科研或课设流程提供视频演示与答辩PPT方便展示成果。整体覆盖悲伤、害怕、厌恶、快乐、气愤、惊讶、中性七种基本表情能够直观展现深度学习在情感计算中的应用。1. 从Fer2013到七类表情这个表情识别系统在解决什么人脸表情识别在工业界和学术界的热度一直没降过但真正动手做过的人都知道难点不在模型结构本身而在数据分布、训练稳定性和从离线模型到实时视频的衔接。这个项目基于PyTorch实现了三种主流架构——CNN、ResNet和VGG——来完成七类基础表情的分类含完整源码、论文和训练好的模型文件适合作为毕业设计、课程项目或者作为你研究表情识别任务的代码基线。我拆解这套系统的核心逻辑数据层用Fer2013灰度图模型层用三个不同深度的卷积神经网络做横向对比应用层用Haar级联检测人脸后送入模型做实时推理。从准确率曲线看ResNet在七分类任务上收敛速度明显快于普通CNN但VGG在小数据集上容易过拟合——这个结论在你自己的实验中也能复现。下面从数据管道开始逐步拆。2. 数据管道Fer2013预处理、标签映射与PyTorch数据加载2.1 Fer2013的原始格式与读取逻辑Fer2013数据集以CSV格式存储每一行包含三个字段emotion标签、pixels像素值、Usage用途标记。pixels字段是48x48灰度图像的像素序列用空格分隔取值范围0到255。Usage字段标记该样本属于Training、PublicTest还是PrivateTest。项目中的data_process.py和data_separation.py就是处理这个格式的。下面是根据项目结构还原的读取代码import pandas as pd import numpy as np def load_fer2013(csv_path): df pd.read_csv(csv_path) images [] labels [] usages [] for idx, row in df.iterrows(): pixels np.array(row[pixels].split(), dtypenp.float32) images.append(pixels.reshape(48, 48, 1)) labels.append(row[emotion]) usages.append(row[Usage]) images np.array(images) labels np.array(labels) train_mask np.array([u Training for u in usages]) val_mask np.array([u PublicTest for u in usages]) test_mask np.array([u PrivateTest for u in usages]) return (images[train_mask], labels[train_mask]), \ (images[val_mask], labels[val_mask]), \ (images[test_mask], labels[test_mask])这里的关键点是Fer2013的官方划分已经固定不需要自己随机拆分。PublicTest和PrivateTest的区别在于前者来自与训练集相同分布的人脸后者来自不同来源所以PrivateTest的准确率通常略低于PublicTest这是评估泛化能力的更真实指标。2.2 标签映射与七类表情编码项目中image_emotion_mapping.py负责把数字标签映射为可读的表情名称。Fer2013的标签0到6分别对应愤怒、厌恶、恐惧、快乐、悲伤、惊讶和中性。注意这里的中性neutral是后来加入的第七类原始Ekman六分类里没有它。EMOTION_LABELS { 0: angry, 1: disgust, 2: fear, 3: happy, 4: sad, 5: surprise, 6: neutral } def emotion_to_idx(name): for idx, label in EMOTION_LABELS.items(): if label name: return idx raise ValueError(fUnknown emotion: {name}) def idx_to_emotion(idx): return EMOTION_LABELS[int(idx)]这个映射看似简单但实际项目中容易出错的地方在于训练时用CrossEntropyLoss要求标签是整数索引而不是one-hot编码很多初学者在这里会多写一步多余的one-hot转换。PyTorch的交叉熵损失内部已经包含了log_softmax和nll_loss直接传入整数标签即可。2.3 DataLoader配置与图像增强项目中dataset目录下的数据加载脚本用torch.utils.data.Dataset封装了标准流程from torch.utils.data import Dataset, DataLoader from torchvision import transforms class Fer2013Dataset(Dataset): def __init__(self, images, labels, transformNone): self.images images self.labels labels self.transform transform def __len__(self): return len(self.labels) def __getitem__(self, idx): img self.images[idx].astype(np.uint8) label self.labels[idx] # 将单通道灰度图转为三通道便于ResNet等模型使用预训练权重 img np.repeat(img, 3, axis-1) from PIL import Image img Image.fromarray(img, modeRGB) if self.transform: img self.transform(img) return img, label train_transform transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ]) val_transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.5, 0.5, 0.5], std[0.5, 0.5, 0.5]) ])配置DataLoader时需要注意num_workers和pin_memory这两个参数。在Windows上num_workers大于0可能导致spawn重复执行主模块建议在if __name__ __main__保护下运行Linux服务器上可以开到CPU核心数减一。pin_memoryTrue配合.cuda(non_blockingTrue)能减少主机到GPU的拷贝延迟。3. 三路模型实现CNN基线、VGG迁移与ResNet残差结构的代码对比3.1 自定义CNN基线模型的结构设计项目中的model_CNN.py定义了一个轻量级CNN。这里重建了核心结构并解释每一层的设计意图import torch.nn as nn class EmotionCNN(nn.Module): def __init__(self, num_classes7): super(EmotionCNN, self).__init__() self.features nn.Sequential( # 48x48输入第一层卷积提取低阶纹理特征 nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.Conv2d(32, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 24x24 nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 12x12 nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 6x6 ) self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x这个结构的关键参数卷积核全部采用3x3padding为1保持特征图尺寸不变特征图尺寸由池化层按2倍递减从48到24到12到6。AdaptiveAvgPool2d((1,1))把任意尺寸的特征图压缩到1x1避免全连接层输入维度写死。3.2 VGG模型的参数裁剪与正则化model_VGG.py参考VGG16的堆叠思路但针对48x48小尺寸输入做了裁剪。原始VGG16的Flatten层输入维度是7x7x512直接在小图上用会损失大量信息。项目中常见的做法是减小全连接层维度并加强Dropoutclass EmotionVGG(nn.Module): def __init__(self, num_classes7): super(EmotionVGG, self).__init__() # 使用PyTorch官方预训练VGG16的特征提取部分 from torchvision.models import vgg16 base vgg16(pretrainedTrue) self.features base.features # 冻结前10层只微调后面的卷积层减少过拟合 for param in self.features[:10].parameters(): param.requires_grad False self.classifier nn.Sequential( nn.AdaptiveAvgPool2d((3, 3)), nn.Flatten(), nn.Linear(512 * 3 * 3, 512), nn.ReLU(inplaceTrue), nn.Dropout(0.6), nn.Linear(512, 256), nn.ReLU(inplaceTrue), nn.Dropout(0.6), nn.Linear(256, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return xVGG在表情识别上有个特点前几层VGG提取的是通用边缘、颜色、纹理特征这些特征在ImageNet上学习后可以直接复用到人脸图像上。保留前10层参数不动、只微调后面层相当于用已懂视觉基础的人去学新任务比从零训练收敛快得多。3.3 ResNet的残差连接为什么更适合小数据集model_ResNet.py使用ResNet18的变体。残差块的核心代码值得展开class BasicBlock(nn.Module): expansion 1 def __init__(self, in_channels, out_channels, stride1): super(BasicBlock, self).__init__() self.conv1 nn.Conv2d(in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d(out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse) self.bn2 nn.BatchNorm2d(out_channels) self.shortcut nn.Sequential() if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d(in_channels, out_channels, kernel_size1, stridestride, biasFalse), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity x out F.relu(self.bn1(self.conv1(x))) out self.bn2(self.conv2(out)) out self.shortcut(identity) out F.relu(out) return out残差连接解决的是深层网络退化问题——理论上网络越深表达能力越强但实际训练中梯度回传经过多个非线性层后容易消失。这里的out self.shortcut(identity)让梯度多了一条直通路。在Fer2013这种约3.5万样本的中小规模数据集上ResNet18比ResNet34更合适因为参数增多带来的收益抵不过过拟合风险。模型对比时项目model_All_Compare.py会把三个模型实例化统计各自的参数量和推理时间def count_parameters(model): return sum(p.numel() for p in model.parameters() if p.requires_grad) cnn EmotionCNN() vgg EmotionVGG() resnet resnet18(num_classes7) for name, model in [(CNN, cnn), (VGG, vgg), (ResNet, resnet)]: print(f{name}: {count_parameters(model) / 1e6:.2f}M params)4. 训练策略与多模型对比学习率设定、损失曲线和评估指标4.1 训练参数配置与优化器选择model_CNN_test.py和model_ResNet_test.py中的训练流程使用交叉熵损失和Adam优化器。关键超参数如下超参数CNNVGGResNet初始学习率0.0010.00010.001批大小643264Epoch数503050权重衰减1e-45e-41e-4学习率衰减StepLR(step10, gamma0.5)StepLR(step8, gamma0.5)StepLR(step10, gamma0.5)提前停止patient8patient5patient8VGG用Adam时学习率必须调低一个数量级因为预训练权重已经处于较好局部最优附近过大的学习率会破坏这些特征。我在复现时把VGG的学习率从0.001调低到0.0001后验证集准确率从59%提升到66%。训练循环的标准写法如下def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 correct 0 total 0 for images, labels in dataloader: images images.to(device) labels labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_accoptimizer.zero_grad()必须在backward()之前调用否则梯度会在多次迭代间累积。PyTorch的梯度是累加式的这个设计本意是方便梯度累积操作但新手容易忽略导致loss曲线周期性震荡。4.2 准确率随epoch变化的观察记录以PrivateTest为准三个模型的收敛曲线特征差异明显。CNN模型前10个epoch准确率快速攀升到50%左右之后增速放缓最终稳定在58%到62%区间。VGG迁移学习在第3个epoch就达到55%但第20个epoch后验证集准确率开始波动说明全连接层的Dropout不足以完全抑制过拟合。ResNet的收敛最平稳从第15个epoch开始稳定突破65%最终在67%到70%之间。Fer2013数据集上的七分类任务学术界的state-of-the-art大约在73%到75%因为这个数据集本身存在标签噪声——部分人脸表情是中性的但被标成了其他类别肉眼都难以区分。一个60%到65%的模型在这个数据集上已经不算差。4.3 混淆矩阵与类别不平衡处理项目中的data_view.py用于可视化数据分布和混淆矩阵。Fer2013的类别极不平衡disgust样本只有几百张而happy有近9000张这会导致模型对disgust的召回率极低import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns def plot_confusion_matrix(model, dataloader, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in dataloader: images images.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) plt.figure(figsize(10, 8)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsEMOTION_LABELS.values(), yticklabelsEMOTION_LABELS.values()) plt.xlabel(Predicted) plt.ylabel(True) plt.savefig(confusion_matrix.png, dpi150)如果发现disgust和fear的召回率异常低处理方法不是简单加数据而是采用类权重。PyTorch中传入torch.FloatTensor权重给CrossEntropyLossfrom sklearn.utils.class_weight import compute_class_weight class_weights compute_class_weight(balanced, classesnp.arange(7), ytrain_labels) class_weights torch.FloatTensor(class_weights).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)加了类权重后模型对少数类的召回率会有明显提升但整体准确率可能略微下降这是trade-off。5. 模型落地Haar级联人脸检测与实时视频表情推理5.1 Haar级联的使用方式和参数调优项目根目录的haarcascade_frontalface_default.xml是OpenCV自带的预训练人脸检测器。相比现在更流行的深度学习人脸检测如RetinaFace、MTCNNHaar级联的优势是CPU上跑得快、无额外依赖但漏检率较高特别是有遮挡或侧脸的时候。import cv2 face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) def detect_faces(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 参数scaleFactor越小检测越慢但越准确 # minNeighbors越大误检越少但漏检越多 faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) return facesdetectMultiScale的三个参数需要按场景调视频流中人脸频繁前后移动时scaleFactor建议1.1太大会漏掉小幅移动的人脸minNeighbors5是经验值在会议室场景可以提高到6到7降低误检在人流密集场景需要降到3到4防止漏检。5.2 预处理对齐与推理管线检测到人脸框之后需要把框出的区域缩放到48x48并做与训练时一致的标准化处理才能输入模型。这里常见的一个坑训练时用了ColorJitter和随机翻转但推理时必须关闭所有随机增强只保留ToTensor和Normalize。def predict_emotion(frame, face_box, model, device, transform): x, y, w, h face_box face_roi frame[y:yh, x:xw] # 推理时统一缩放到48x48 face_roi cv2.resize(face_roi, (48, 48)) face_roi cv2.cvtColor(face_roi, cv2.COLOR_BGR2RGB) from PIL import Image pil_img Image.fromarray(face_roi) tensor transform(pil_img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): outputs model(tensor) probs torch.softmax(outputs, dim1) conf, pred torch.max(probs, 1) emotion_name idx_to_emotion(pred.item()) return emotion_name, conf.item()torch.no_grad()在推理时是强制要求否则PyTorch会构建计算图大量占用显存。model.eval()切换BatchNorm和Dropout到评估模式——BatchNorm在eval模式下使用训练阶段统计的running_mean和running_var而不是当前batch的统计量。5.3 实时视频推理的帧率优化处理视频时example_dsh.mp4可以在本地验证完整流程。简单的逐帧处理在CPU上大约只能跑到8到10 FPS如果达不到实时要求可以用跳帧策略每处理一帧检测人脸后续2到3帧直接沿用上一帧的人脸框位置只做表情推理。另外OpenCV的VideoCapture读取本身有缓冲可以适当调小缓冲池降低延迟cap cv2.VideoCapture(example_dsh.mp4) # 降低缓冲帧数减少延迟 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)在实际的终端展示中还可以在画面上叠加置信度阈值。当最大概率低于0.6时显示不确定而不显示具体的表情类别这在课堂专注度分析、驾驶疲劳监测这类对误判容忍度低的场景中更实用。人脸框出现抖动时对检测框做简单的一阶低通滤波让框的位置平滑过渡。本文还有配套的精品资源点击获取
返回列表