PyTorch实战:从CNN到ResNet的人脸表情识别系统全流程解析
简介本资源是一套基于PyTorch实现的人脸表情识别完整项目涵盖CNN、VGG与ResNet三种主流网络结构的代码实现与对比分析专为计算机相关专业学生设计适用于期末大作业、课程设计及毕业设计等实践场景尤其适合深度学习入门者快速上手并理解模型构建、训练与评估全流程。压缩包共15个文件含13个Python源码如model_CNN.py、model_ResNet.py、data_separation.py等覆盖数据预处理、模型定义、GPU加速训练、可视化分析及多模型性能对比、1个Haar级联人脸检测XML配置文件和1份详细说明文档README.md整体仅162KB轻量易部署。已有166人下载学习所有代码经导师指导并高分通过评审99分结构清晰、注释充分、依赖明确小白可直接运行无需额外调试即可复现结果同时提供图像映射、数据集划分、训练曲线绘制等实用功能模块显著降低实战门槛。1. 项目概述与核心价值最近在整理过去的项目资料翻到了一个挺有意思的“老伙计”——一个基于PyTorch实现的人脸表情识别系统。这个项目麻雀虽小五脏俱全它把CNN、VGG和ResNet这几个深度学习里的“明星”架构给串了起来从数据准备、模型搭建、训练调优到最终部署测试走完了一个完整的流程。现在回头看里面有不少当时踩坑总结出来的经验比如怎么处理表情数据的不均衡、怎么在有限算力下让VGG这种“大家伙”跑起来、以及ResNet的残差连接到底在实际训练中起了什么作用。我觉得把这些实操细节和思考过程分享出来比单纯扔一份源码更有价值无论你是刚入门PyTorch想找个综合项目练手还是已经有一定基础想深入理解经典网络在具体任务上的应用应该都能从中找到一些参考。人脸表情识别FER这个任务看似简单——不就是给人脸图片分个类嘛高兴、生气、悲伤等。但真做起来你会发现它浓缩了计算机视觉领域的很多典型挑战光照变化、头部姿态、遮挡、以及个体间的巨大差异。用深度学习尤其是卷积神经网络CNN来解决这个问题已经成为主流。这个项目没有停留在用一个简单的CNN上而是引入了VGG和ResNet这两个在ImageNet上经受住考验的架构让我们能直观地对比更深的网络VGG、引入了残差学习的网络ResNet在FER这个特定任务上究竟带来了哪些性能提升和训练上的变化理解了这些你再去面对其他图像分类任务心里会更有底。2. 项目整体设计与思路拆解2.1 核心需求与技术选型逻辑这个项目的核心目标很明确构建一个能够准确识别输入人脸图像表情类别的系统。我们选择了七种基本情绪作为分类目标愤怒Angry、厌恶Disgust、恐惧Fear、高兴Happy、悲伤Sad、惊讶Surprise和中立Neutral。选择PyTorch作为框架几乎是当前研究和工业界入门深度学习的首选它的动态计算图设计让模型调试和实验变得非常灵活对于理解模型内部运作机制特别友好。为什么选择CNN、VGG、ResNet这个组合这里面的思路是递进的基础CNN这是我们自己从头搭建的一个相对简单的卷积网络。它的作用是建立基线Baseline让我们理解卷积、池化、全连接这些基本操作是如何在表情识别任务中起作用的。通过它我们可以快速验证数据管道是否通畅评估任务的基本难度。VGGNetVGG的核心思想是使用连续的3x3小卷积核来替代大的卷积核如5x57x7通过堆叠更多的层来增加网络深度从而提升模型的表征能力。在项目中我们通常不会使用原版VGG-16或VGG-19参数量太大容易过拟合而是采用一个精简版的VGG结构例如VGG-11或更浅的变体。引入VGG是为了探究“深度”对表情特征提取的影响。表情是细微的面部肌肉运动更深的网络理论上能捕捉到更抽象、更全局的特征。ResNet当网络变得很深时就会遇到梯度消失/爆炸和网络退化问题导致训练困难性能不升反降。ResNet通过引入“残差块”Residual Block和“快捷连接”Shortcut Connection让网络可以学习输入与输出之间的残差即变化部分这使得训练极深的网络如ResNet-18, ResNet-34成为可能。在表情识别中引入ResNet是为了解决VGG可能遇到的训练难题并验证残差学习是否能让模型更好地学习到表情的细微差别。这个“三部曲”的设计本质上是一个由浅入深、对比学习的实验过程。它能让你亲手体验从“造轮子”到“用轮子”再到“改进轮子”的全过程对深度学习的模型演进有更立体的认识。2.2 数据准备与预处理管道任何机器学习项目数据都是基石。对于人脸表情识别公开数据集有很多比如FER2013、CK、JAFFE等。这个项目通常以FER2013数据集为例因为它数据量相对充足且直接在Kaggle上提供获取方便。数据预处理流程是关键直接决定了模型能看到什么样的“世界”数据读取与划分FER2013数据集通常以CSV文件提供里面每一行是一张48x48像素的灰度人脸图像像素值序列和一个表情标签。我们需要将其解析为图像矩阵和标签。然后按照一定比例如8:1:1划分为训练集、验证集和测试集。验证集用于在训练过程中监控模型表现防止过拟合测试集用于最终评估在整个训练过程中模型“从未见过”。人脸对齐与裁剪可选但重要原始数据集中的人脸可能并不完全居中或大小不一。更严谨的做法是使用人脸检测器如Dlib或MTCNN先检测出人脸关键点然后进行对齐和裁剪确保输入网络的人脸区域是标准化的。这对于提升模型鲁棒性很有帮助。在初始版本中为了简化流程我们可以暂时跳过这一步直接使用数据集提供的已裁剪好的小图。图像增强Data Augmentation这是解决数据量不足、增加模型泛化能力的利器。对于训练集我们会应用一系列随机变换随机水平翻转人脸基本是对称的水平翻转是安全且有效的增强方式。随机旋转小角度模拟头部轻微的倾斜。亮度/对比度微调模拟光照变化。标准化Normalization将像素值从[0, 255]缩放到[0, 1]或[-1, 1]并减去均值、除以标准差。这能加速模型收敛提升训练稳定性。通常我们会计算数据集的均值和标准差或者使用ImageNet的统计值如果使用预训练模型。数据加载器构建使用PyTorch的Dataset和DataLoader类来构建高效的数据管道。Dataset负责定义如何读取单张图片和标签DataLoader负责批量加载、打乱顺序、并行读取数据。注意表情数据集普遍存在类别不均衡问题。例如“高兴”的图片可能远多于“厌恶”。如果不处理模型会倾向于预测多数类。解决方法包括对少数类进行过采样复制、对多数类进行欠采样或者在损失函数中使用类别权重torch.nn.CrossEntropyLoss的weight参数。3. 核心模型解析与PyTorch实现要点3.1 基础CNN模型搭建我们先从最简单的CNN开始。一个典型用于表情识别的CNN可能包含2-3个卷积块每个块由卷积层Conv2d、激活函数ReLU和池化层MaxPool2d组成最后接上全连接层Linear进行分类。import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes7): super(SimpleCNN, self).__init__() # 输入假设为 1x48x48 (灰度图) self.conv1 nn.Conv2d(in_channels1, out_channels32, kernel_size3, padding1) self.pool1 nn.MaxPool2d(kernel_size2, stride2) # 输出: 32x24x24 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool2 nn.MaxPool2d(2, 2) # 输出: 64x12x12 self.conv3 nn.Conv2d(64, 128, kernel_size3, padding1) self.pool3 nn.MaxPool2d(2, 2) # 输出: 128x6x6 # 全连接层 self.fc1 nn.Linear(128 * 6 * 6, 512) # 计算特征图展平后的尺寸 self.fc2 nn.Linear(512, num_classes) self.dropout nn.Dropout(p0.5) # 防止过拟合 def forward(self, x): x self.pool1(F.relu(self.conv1(x))) x self.pool2(F.relu(self.conv2(x))) x self.pool3(F.relu(self.conv3(x))) x x.view(-1, 128 * 6 * 6) # 展平 x F.relu(self.fc1(x)) x self.dropout(x) x self.fc2(x) # 输出logits未经过softmax return x实现要点卷积核与填充使用kernel_size3, padding1可以保持特征图空间尺寸不变当stride1时方便计算。展平操作在进入全连接层前必须将多维的特征图“拍平”成一维向量。x.view(-1, channels * height * width)中的-1表示让PyTorch自动计算这个维度通常是batch size。Dropout在全连接层之间加入Dropout是防止小模型过拟合的有效手段。注意Dropout只在训练时生效在模型验证和测试时需要调用model.eval()来关闭它。3.2 精简版VGG模型实现原版VGG-16有16个带参数的层参数量超过1.3亿对于表情识别这种小数据集来说过于庞大。因此我们实现一个精简版通常称为“VGG-like”或“Mini-VGG”。class MiniVGG(nn.Module): def __init__(self, num_classes7): super(MiniVGG, self).__init__() # 特征提取部分多个卷积块 self.features nn.Sequential( # 第一个卷积块2个卷积层 nn.Conv2d(1, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(64, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 48-24 # 第二个卷积块2个卷积层 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(128, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 24-12 # 第三个卷积块3个卷积层 nn.Conv2d(128, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.Conv2d(256, 256, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2, 2), # 12-6 ) # 分类器部分 self.classifier nn.Sequential( nn.Linear(256 * 6 * 6, 4096), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), nn.Linear(4096, 4096), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), nn.Linear(4096, num_classes), ) def forward(self, x): x self.features(x) x x.view(x.size(0), -1) # 展平 x self.classifier(x) return x实现要点与思考nn.Sequential的使用将连续的层组合成块让代码结构更清晰。features部分专门负责特征提取classifier部分负责分类。inplaceTrue参数在ReLU中设置inplaceTrue可以节省一点内存它直接修改输入而不创建新的输出张量。但需注意这可能会影响某些需要保留原始输入的计算图操作。参数量与过拟合即使这个“迷你”版全连接层参数量依然很大256*6*6 - 4096。这是VGG网络容易过拟合的主要原因。在实际训练中我们可能会进一步减小第一个全连接层的神经元数量例如从4096改为1024并加大Dropout的比例。预训练权重VGG通常在ImageNet彩色三通道图像上预训练。我们的表情数据是灰度图单通道。直接加载预训练权重需要处理通道数不匹配的问题。一种常见做法是将预训练的第一层卷积核权重在通道维度上取均值复制成单通道的权重。3.3 ResNet模型实现与迁移学习ResNet的结构相对复杂得益于PyTorch的torchvision.models模块我们可以轻松地调用预定义的ResNet模型。这里以ResNet-18为例。import torchvision.models as models class ResNetFER(nn.Module): def __init__(self, num_classes7, pretrainedTrue): super(ResNetFER, self).__init__() # 加载预训练的ResNet-18骨干网络 resnet models.resnet18(pretrainedpretrained) # 处理输入通道ResNet预训练模型输入是3通道我们是1通道灰度图 # 方法将第一个卷积层的权重在输入通道维度取平均复制成1通道 if pretrained: weight resnet.conv1.weight resnet.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) with torch.no_grad(): resnet.conv1.weight.copy_(weight.mean(dim1, keepdimTrue)) else: resnet.conv1 nn.Conv2d(1, 64, kernel_size7, stride2, padding3, biasFalse) # 修改最后的全连接层以适应我们的分类数 num_features resnet.fc.in_features resnet.fc nn.Linear(num_features, num_classes) self.base_model resnet def forward(self, x): return self.base_model(x)实现要点与迁移学习策略输入通道适配这是使用ImageNet预训练模型处理灰度图的关键步骤。我们将原第一层卷积核形状为[64, 3, 7, 7]在第二个维度输入通道维度dim1上取均值得到一个[64, 1, 7, 7]的权重然后赋给新的单通道卷积层。这样相当于把RGB三通道的信息融合到了单通道中是一种简单有效的迁移方法。微调Fine-tuning策略策略一全网络微调解冻所有层用较小的学习率如预训练阶段的1/10训练整个网络。这适用于数据量相对充足的情况。策略二部分微调冻结骨干网络resnet的前面大部分层例如除了最后两个残差块和全连接层之外的所有层只训练这些解冻的层和新的分类头。这适用于数据量较少防止过拟合的场景。可以通过设置param.requires_grad False来冻结参数。网络适配ResNet原设计输入是224x224而我们的数据是48x48。直接输入会导致特征图尺寸过早地被压缩为1x1丢失信息。因此在实际项目中我们可能需要移除原ResNet的第一个最大池化层resnet.maxpool或者修改其步长。或者更常见的做法是在数据预处理阶段就将人脸图像缩放到224x224需要插值可能会引入模糊。两种方法各有利弊需要实验对比。4. 模型训练、调优与评估全流程4.1 训练循环构建与核心组件有了模型和数据接下来就是训练。一个标准的训练循环包含以下几个核心部分import torch.optim as optim from torch.optim.lr_scheduler import StepLR # 初始化模型、损失函数、优化器 device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) # 或 MiniVGG(), ResNetFER() criterion nn.CrossEntropyLoss() # 交叉熵损失内置了Softmax optimizer optim.Adam(model.parameters(), lr0.001) # Adam是常用选择 scheduler StepLR(optimizer, step_size10, gamma0.1) # 学习率衰减 num_epochs 50 best_val_acc 0.0 for epoch in range(num_epochs): # 训练阶段 model.train() running_loss 0.0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() # 清零梯度 outputs model(images) loss criterion(outputs, labels) loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss loss.item() # 验证阶段 model.eval() val_correct 0 val_total 0 with torch.no_grad(): # 关闭梯度计算节省内存和计算 for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs.data, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_acc 100 * val_correct / val_total print(fEpoch [{epoch1}/{num_epochs}], Loss: {running_loss/len(train_loader):.4f}, Val Acc: {val_acc:.2f}%) # 学习率调度 scheduler.step() # 保存最佳模型 if val_acc best_val_acc: best_val_acc val_acc torch.save(model.state_dict(), best_model.pth)核心组件解析损失函数CriterionCrossEntropyLoss是分类任务的标准选择。它结合了LogSoftmax和NLLLoss数值稳定。注意它的输入应该是模型的原始输出logits不需要在模型最后加Softmax层。优化器OptimizerAdam优化器自适应调整学习率在大多数情况下表现良好是默认的起点。SGD随机梯度下降配合动量momentum和适当的学习率衰减在调优后可能达到更好的最终精度但需要更多超参数调整。学习率调度器SchedulerStepLR是一种简单的调度策略每隔一定步数将学习率乘以一个衰减因子gamma。这模拟了训练后期精细调整参数的过程。更复杂的策略如ReduceLROnPlateau当验证指标不再提升时降低学习率也值得尝试。模型状态切换model.train()和model.eval()至关重要。前者会启用Dropout、BatchNorm等的训练模式后者会关闭它们确保评估结果的一致性。4.2 超参数调优与实验管理训练深度学习模型很大程度上是在调超参数。以下是一些关键的调优点和我的经验学习率Learning Rate这是最重要的超参数。可以从0.001Adam或0.01SGD with momentum开始。如果训练损失不下降可能是学习率太大震荡或太小下降慢。使用学习率查找器如PyTorch Lightning中的lr_finder或简单的网格搜索来寻找合适范围。批大小Batch Size受限于GPU内存。较大的批大小如64, 128通常能使训练更稳定但可能会降低模型泛化能力。较小的批大小如16, 32可能带来正则化效果但梯度估计噪声更大。需要根据硬件条件折中。优化器选择从Adam开始快速原型。如果追求更高精度可以后期切换到SGD (momentum0.9) 并进行精细调优。权重衰减Weight Decay即L2正则化在优化器中设置optim.Adam(..., weight_decay1e-4)。有助于防止过拟合。数据增强强度增强太弱效果有限增强太强可能破坏图像语义如把人脸旋转过大。需要根据任务特性调整。实验管理建议使用torch.utils.tensorboard或第三方工具如Weights Biases, MLflow来记录每次实验的超参数、训练损失、验证准确率曲线。这能帮你清晰地对比不同设置的效果避免“拍脑袋”调参。4.3 模型评估与性能分析训练完成后需要在独立的测试集上进行最终评估。def evaluate_model(model, test_loader, device): model.eval() all_preds [] all_labels [] with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) from sklearn.metrics import classification_report, confusion_matrix, accuracy_score accuracy accuracy_score(all_labels, all_preds) print(fTest Accuracy: {accuracy:.4f}) print(\nClassification Report:) print(classification_report(all_labels, all_preds, target_namesclass_names)) print(\nConfusion Matrix:) print(confusion_matrix(all_labels, all_preds)) return accuracy评估指标解读准确率Accuracy最直观的指标但在类别不均衡的数据集上可能具有误导性。分类报告Classification Report提供每个类别的精确率Precision、召回率Recall和F1-score。这是更全面的评估方式。精确率在所有被预测为A类的样本中真正是A类的比例。关注预测的“准不准”。召回率在所有真正的A类样本中被成功预测出来的比例。关注找的“全不全”。F1-score精确率和召回率的调和平均数是综合衡量指标。混淆矩阵Confusion Matrix以矩阵形式展示每个类别的样本被预测成其他类别的情况。它能清晰揭示模型容易混淆哪些类别例如把“悲伤”误判为“中立”把“恐惧”误判为“惊讶”。分析混淆矩阵是改进模型的重要步骤。如果某些类别间混淆严重可能需要检查这些类别的训练样本是否不足或质量不高。考虑这些类别在视觉特征上是否本身就难以区分是否需要引入更细致的特征如使用注意力机制。尝试类别平衡策略或调整损失函数的类别权重。5. 项目部署与优化思考5.1 模型导出与轻量化训练好的模型最终需要部署。PyTorch提供了torch.jit.trace或torch.jit.script将模型转换为TorchScript格式便于在非Python环境中如C加载运行。# 示例使用 torch.jit.trace 导出模型 model.load_state_dict(torch.load(best_model.pth)) model.eval() example_input torch.rand(1, 1, 48, 48).to(device) # 一个示例输入 traced_script_module torch.jit.trace(model, example_input) traced_script_module.save(fer_model.pt)对于资源受限的环境如移动端、嵌入式设备需要考虑模型轻量化知识蒸馏Knowledge Distillation用一个大模型教师模型去指导一个小模型学生模型训练让小模型获得接近大模型的性能。剪枝Pruning移除网络中不重要的连接或通道减少参数量和计算量。量化Quantization将模型权重和激活从浮点数如FP32转换为低精度整数如INT8大幅减少模型体积和推理耗时。PyTorch提供了torch.quantization工具包。5.2 构建实时推理管道一个完整的人脸表情识别应用除了核心分类模型还需要前置的人脸检测和对齐模块。# 伪代码展示流程 import cv2 # 假设我们使用OpenCV的DNN模块加载人脸检测模型和我们的表情模型 face_detector cv2.dnn.readNetFromCaffe(face_prototxt, face_model) emotion_model torch.jit.load(fer_model.pt) emotion_model.eval() cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break # 1. 人脸检测 h, w frame.shape[:2] blob cv2.dnn.blobFromImage(frame, scalefactor1.0, size(300, 300), mean(104, 177, 123)) face_detector.setInput(blob) detections face_detector.forward() for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 置信度阈值 box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (x1, y1, x2, y2) box.astype(int) # 2. 人脸区域裁剪与预处理 face_roi frame[y1:y2, x1:x2] if face_roi.size 0: continue # 转换为灰度缩放到模型输入尺寸标准化... face_gray cv2.cvtColor(face_roi, cv2.COLOR_BGR2GRAY) face_resized cv2.resize(face_gray, (48, 48)) face_tensor torch.from_numpy(face_resized).float().unsqueeze(0).unsqueeze(0) / 255.0 # 3. 表情识别推理 with torch.no_grad(): outputs emotion_model(face_tensor) _, predicted torch.max(outputs, 1) emotion_label emotion_classes[predicted.item()] # 4. 绘制结果 cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, emotion_label, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow(FER Demo, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()5.3 常见问题与排查技巧实录在实际操作这个项目的过程中我遇到了不少典型问题这里总结一下排查思路问题1训练损失不下降准确率随机波动约等于瞎猜。可能原因学习率设置不当过高或过低数据预处理出错如图像和标签没对应上模型输出层维度与类别数不匹配损失函数用错。排查步骤检查数据可视化几批训练数据确保图像显示正常标签正确。检查模型用一个极小的batch如2张图做一次前向传播打印输出形状和值看是否符合预期。检查损失手动计算一个batch的损失与PyTorch计算的对比。降低学习率尝试将学习率降到非常小如1e-5看损失是否开始缓慢下降。如果是说明原学习率可能太大。问题2模型在训练集上表现很好但在验证集上准确率很低过拟合。可能原因模型复杂度过高相对于数据量数据增强不够训练时间过长。解决策略增强正则化增加Dropout比例添加或增大权重衰减weight decay。加强数据增强引入更多样化的增强方法如随机裁剪、颜色抖动等。早停Early Stopping持续监控验证集损失当其在连续多个epoch不再下降时停止训练。简化模型换用更小的网络如用ResNet-18代替ResNet-50或减少全连接层的神经元数量。使用预训练模型并冻结部分层这是应对小数据集过拟合最有效的手段之一。问题3使用预训练的ResNet/VGG时训练初期损失就为NaN。可能原因输入数据没有进行正确的标准化。ImageNet预训练模型期望输入是使用特定均值和标准差标准化后的。如果直接输入[0,1]或[0,255]的像素值可能会在深层网络导致数值爆炸。解决方案确保你的数据预处理包含了与预训练模型匹配的标准化。对于PyTorch的torchvision.models通常使用transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225])。对于灰度图如果采用单通道复制三通道的方式也需要用同样的值。问题4GPU内存不足CUDA out of memory。可能原因Batch Size太大模型太大中间变量没有及时释放。解决策略减小batch_size。使用梯度累积Gradient Accumulation假设想模拟batch_size64但内存只够16。可以设置实际batch_size16每4个迭代步骤accumulation_steps4才做一次参数更新optimizer.step()并在每次迭代后不清零梯度optimizer.zero_grad()只在累积步骤完成后调用。使用混合精度训练AMPPyTorch的torch.cuda.amp模块可以自动将部分计算转换为半精度FP16显著减少内存占用并可能加速训练。检查代码中是否有不必要的大张量被长期引用。这个项目从零开始实现并整合了三种经典的CNN模型贯穿了数据准备、模型设计、训练调优、评估分析的完整生命周期。它更像一个实验平台你可以很方便地替换其中的组件比如尝试不同的优化器、添加注意力模块、换用更先进的数据集如AffectNet来探索哪些改进对表情识别任务真正有效。深度学习动手实践的魅力就在于此理论上的优劣需要在具体任务和数据上去验证和感受。希望这份详细的梳理和附带的经验能帮你更顺畅地跑通整个流程并激发你更多的实验想法。本文还有配套的精品资源点击获取