简介:这是一套基于卷积神经网络的人脸表情识别系统完整项目,面向Python开发者和计算机视觉学习者,尤其适合用于课程设计、毕业设计或表情识别技术入门实践。系统采用Keras、OpenCV和PyQt5构建,基于fer2013公开表情数据库完成训练,功能上支持从本地载入图片、调用摄像头实时拍摄分析,并可切换不同模型处理图像,整体覆盖数据预处理、模型训练、界面交互到结果展示的完整流程。压缩包共49个文件,总大小约12.49MB,内含Python源码、图片素材与测试图片、预训练模型文件、UI界面文件、演示视频、答辩PPT及说明文档,类型涵盖代码、数据、模型和展示材料,便于按需查阅和运行。目前已有119人学习下载。借助该资源,读者可以获得可运行的系统源码、训练好的模型权重、fer2013数据集的整理与使用思路、PyQt5界面设计文件,以及实际运行录屏和论文答辩演示文稿,能够快速复现系统效果,并在此基础上进行算法改进或功能扩展。
1. 人脸表情识别是深度学习最值得做的入门项目,但别急着下载源码
如果你正在找一个能真正跑起来、能看到效果、又能拿出去讲的 Python 深度学习项目,人脸表情识别几乎是性价比最高的选择。它不像目标检测那样需要复杂的锚框和后处理,也不像语音识别那样要处理时序依赖,它的任务边界足够清晰:给一张人脸图片,判断是高兴、悲伤、愤怒、惊讶、恐惧、厌恶还是中性。这个任务直观到外行也能看懂结果,又足够支撑起卷积神经网络(CNN)的完整训练链路,从数据预处理、模型搭建、训练调参到推理部署全都能走一遍。项目标题里带源码、图片素材、测试图片和演示文档,说明这是一套完整的交付物,不是零散的算法片段。对正在做课程设计、毕业设计,或者想从纯调库进阶到看懂模型训练的人,这套东西能帮你省掉大量找数据和踩环境的时间。不过我得先把丑话说在前面:拿到源码直接跑通不算本事,能把它拆开、按自己的数据重新训练、并说清楚每个参数为什么这样设,这项目才算真正进你脑子了。
2. 为什么表情识别非 CNN 不可:从选型理由到系统总体架构
2.1 传统方法与人脸表情识别的矛盾:手工特征根本描述不了“表情”
在卷积神经网络流行之前,做表情识别的主流路子是手工特征加分类器,常用的是 LBP 纹理特征、HOG 方向梯度直方图,配上 SVM 支持向量机。这个路线的问题是:表情不是一个静态的纹理模式,它是肌肉群在空间上的协同形变。高兴的时候嘴角上扬、眼轮匝肌收缩、脸颊鼓起;惊讶的时候眉毛抬高、眼睛睁大、下颌放松。这些变化反映在像素上,是不同尺度、不同区域的局部纹理同时发生变化,而 LBP 或 HOG 这类特征只能描述某个固定邻域内的梯度或纹理分布,捕捉不到跨区域的联合变化。也就是说,表情识别本质上是一个需要多层特征抽象的任务,低层看边缘和纹理,中层看眼睛、嘴巴的形状,高层看这些部位的组合模式。这正是卷积神经网络天然擅长的:卷积层堆叠的过程,就是在用数据驱动的方式自动完成从局部纹理到全局语义的特征提取,完全不需要人手工设计特征算子。
2.2 为什么选 Python 和 PyTorch 而不是 TensorFlow 或 Keras
项目标题锁定了 Python,实际动手时框架选择还有余地。我的建议是直接用 PyTorch,原因有三个。第一,PyTorch 的调试体验对新手最友好,训练过程中任何一行张量的 shape 都可以直接用 print 打出来看,不像是 TensorFlow 1.x 那样要先建图再执行,出错了只能在抽象报错里猜。第二,PyTorch 在学术圈和工业界的采用率已经占了绝对主流,遇到问题去搜索引擎查,十有八九的解决方案都是 PyTorch 写的,这对新手尤为重要。第三,表情识别这种规模的模型用 PyTorch 写起来非常直接,不需要依赖第三方高级 API 的封装,正好能让你把卷积、池化、批归一化这些基础概念在代码里一个个对应上。如果你用的是标题里提到的源码包,里面大概率也是 PyTorch 实现的,因为这是当前做 CNN 项目最主流的姿势。环境方面,Python 版本建议 3.8 到 3.10 之间,PyTorch 装 2.x 版本,CPU 也能跑,但训练速度会慢到让你怀疑人生,后面我会专门讲这个问题。
2.3 系统总体架构:检测、预处理、分类三段式的职责划分
整个人脸表情识别系统从输入一张图片到输出表情类别,中间要经过三个串行模块。第一个是人脸检测模块,用的是 OpenCV 自带的 Haar 级联分类器,它的任务是先把画面里的人脸框出来。这一步必须有,因为你拿到的测试图片大概率不是干净的证件照,可能是一群人的合影、一个带复杂背景的自拍,直接整图送进模型会让模型完全懵掉。第二个是预处理模块,负责把检测到的人脸区域缩放成固定尺寸,做灰度化、直方图均衡化,再归一化成模型输入要求的张量格式。第三个就是 CNN 分类模块,这也是核心,负责把预处理后的人脸图映射到 7 个表情类别的概率分布上。
提示:很多人做表情识别项目翻车,不是模型训练得不好,而是前面的人脸检测漏检或误检,导致输入模型的根本就不是一张干净的人脸。后面我讲踩坑会重点展开。
这里要理解一个关键的设计决策:为什么把检测和识别分开,而不是用一个端到端的模型同时做人脸定位和表情分类?端到端方案确实存在,比如用目标检测模型直接框出人脸并带上表情类别,但它需要的数据标注成本高得多。而两段式方案里,检测用 OpenCV 现成的、零成本,识别用自己训练的 CNN,每一段都可以独立调优和替换。对入门项目和课程设计来说,两段式是可靠的工程折中方案。
3. 从图片素材到干净的数据集:目录划分、预处理与人脸对齐
3.1 数据集来源与目录规划:先解决“有多少数据、按什么结构存放”的问题
标题里说带图片素材,这说明不用你满世界找数据。但素材放在那里,和能直接用来训练,中间还隔着一层整理工作。我见过的源码包里,图片素材有两种常见形态:一种是已经按类别分好文件夹的,比如train/happy、train/sad这样;另一种是散装图片,需要你自己按文件名或标签文件归类。不管是哪种,第一步先建立统一的目录结构,这是我每次开工前必做的事。结构如下:
face_expression/ ├── dataset/ │ ├── train/ │ │ ├── angry/ │ │ ├── disgust/ │ │ ├── fear/ │ │ ├── happy/ │ │ ├── neutral/ │ │ ├── sad/ │ │ └── surprise/ │ ├── val/ │ │ └── (同 train 的子目录结构) │ └── test/ │ └── (同 train 的子目录结构) ├── models/ ├── checkpoints/ └── utils/这个目录结构里,train、val、test 三份数据的划分比例我一般按 8:1:1 来做。注意 val 和 test 的角色不能混:val 集用来在训练过程中监控模型状态、做模型选择;test 集是训练结束时一次性评估用的,不能反复拿它调参,否则评估结果就是假的。表情识别是 7 分类任务(angry、disgust、fear、happy、neutral、sad、surprise),所以每个类别目录对应一个标签。如果你拿到的素材是按其他类别划分的,比如只有 4 类或 5 类,那就需要先做标签合并。常见做法是把 contempt(轻蔑)合并到 neutral,或者直接砍掉样本太少的类别。disgust 这个类别在公开数据集里通常样本量最少,得做好它准确率偏低的心理准备。
3.2 写一个数据整理脚本:把图片素材变成模型能吃的数据集
如果你拿到的图片是散装带标签文件的,比如一个 CSV 或者 txt 文件里写了图片文件名和对应的表情标签,那就要写个脚本整理成上面的目录结构:
import os import shutil import csv from pathlib import Path def organize_dataset(csv_path, src_dir, dst_root): # csv 格式:图片文件名, 表情标签, 用途(train/val/test) with open(csv_path, 'r', encoding='utf-8') as f: reader = csv.reader(f) header = next(reader) for row in reader: img_name, label, split = row[0], row[1], row[2] dst_dir = Path(dst_root) / split / label dst_dir.mkdir(parents=True, exist_ok=True) src_file = Path(src_dir) / img_name if src_file.exists(): shutil.copy(src_file, dst_dir / img_name) # 统计每个 split 下每个类别的样本数,打印出来确认分布 for split in ['train', 'val', 'test']: for label in os.listdir(Path(dst_root) / split): count = len(list((Path(dst_root) / split / label).glob('*.*'))) print(f'{split}/{label}: {count} 张') if __name__ == '__main__': organize_dataset('labels.csv', 'raw_images', 'dataset')逻辑说明:脚本的核心就是读标签文件、建目录、复制图片。这里有个细节值得说——用Path.mkdir(parents=True, exist_ok=True)会自动创建多级目录并且不会在目录已存在时报错,这是写数据整理脚本时避免踩坑的关键参数。最后的统计循环一定要跑一遍,因为表情数据集里类别不均衡是常态,你需要在训练前知道哪个类别样本少,再决定要不要做数据增强补偿。
如果拿到的是已分好类的目录,那就不用写这个脚本了,直接在下一步用torchvision.datasets.ImageFolder读取即可,它会按子目录名自动生成标签映射。
3.3 预处理与人脸对齐三个关键操作:灰度、均衡化、归一化
数据准备好之后,每个样本在进模型前还要过三道预处理。第一道是灰度化,因为表情识别主要靠纹理和形状线索,颜色信息贡献有限,而灰度图能把输入通道从 3 降到 1,显著减少计算量。第二道是直方图均衡化,它能增强对比度,尤其是在光线不均匀的自拍图、暗光环境下,这一步能有效提升特征可见度。第三道是归一化,因为像素值范围是 0 到 255,而神经网络训练时输入需要落在相对稳定的数值范围,否则梯度更新容易震荡。这些操作在 PyTorch 里可以组合成一个变换流水线:
from torchvision import transforms # 训练集的数据增强与预处理 train_transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), # 强制转灰度,1个通道 transforms.Resize((64, 64)), # 统一尺寸:64x64 transforms.RandomHorizontalFlip(p=0.5), # 随机水平翻转:模拟镜像人脸 transforms.RandomAffine(degrees=10, translate=(0.05, 0.05)), # 轻微旋转/平移 transforms.ToTensor(), # 转张量:HWC→CHW,值域0~1 transforms.Normalize(mean=[0.5], std=[0.5]) # 归一化到-1~1 ]) # 验证集与测试集不做随机增强,只做必要预处理 val_transform = transforms.Compose([ transforms.Grayscale(num_output_channels=1), transforms.Resize((64, 64)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ])参数说明:Resize((64, 64))这个分辨率是入门级项目常见的折中选择,再低像 32x32 会丢掉纹理细节,表情区分度不够;再高像 128x128 会增加训练耗时,而收益在简单 CNN 上不明显。RandomHorizontalFlip是表情识别里最安全的数据增强,因为人脸左右对称,水平翻转不会改变表情含义。RandomAffine的 degrees 设为 10 度是经验值,旋转太大会让人脸姿态失真。这里要特别提醒:像RandomErasing随机遮挡这种增强在表情识别里要慎用,它可能遮住眼睛或嘴巴区域,反而让模型学到错误的线索。
3.4 Dataset 类与训练/验证数据加载:翻车率最高的 DataLoader 环节
数据整理完、预处理定义好,接下来就是写数据加载器。这里用ImageFolder是最快的路径,它自动递归读取子目录、生成标签索引。但这里有一个非常隐蔽的坑:不同平台的路径分隔符问题,以及在 Windows 上num_workers设置不当导致的数据加载卡死。我的写法如下:
from torch.utils.data import DataLoader from torchvision.datasets import ImageFolder def build_dataloader(data_dir, transform, batch_size=64, shuffle=True, num_workers=2): dataset = ImageFolder(root=data_dir, transform=transform) loader = DataLoader( dataset, batch_size=batch_size, shuffle=shuffle, num_workers=num_workers, # Windows上建议≤2,Linux可以到4或8 pin_memory=True, # 显存足够时开启,能加速数据传输 drop_last=False # 不丢弃最后不足一个batch的样本 ) print(f'数据集类别映射: {dataset.class_to_idx}') # 打印标签映射 print(f'样本总数: {len(dataset)}') return loader逻辑说明:ImageFolder返回的 dataset 里,class_to_idx就是文件夹名到数字标签的映射字典,这决定了模型输出的第几个神经元对应哪种表情,打印出来能避免后面推理时标签对不上。batch_size设为 64 是经验值,太大容易显存溢出,太小会让收敛变慢且梯度震荡。pin_memory=True在 GPU 训练时能减少主机到显存的数据拷贝时间,但如果 CPU 内存不足会引发新的问题,跑 16GB 内存的机器上建议关掉。
注意:Windows 上
num_workers设大了(比如 4 或 8)很容易引发“DataLoader worker exited unexpectedly”崩溃,或者训练卡死。实测下来 Windows 上设 0 或 2 最稳,服务器 Linux 环境再调到 4 以上。
4. 从零写出可跑的 CNN 表情识别模型:网络结构、训练脚本与参数调优
4.1 网络结构设计:三层卷积加全连接,每个模块为什么放这里
表情识别任务的输入是 64x64 的灰度图,这种分辨率和任务复杂度决定了不需要特别深的网络。ResNet 50 或 VGG 16 在这个任务上属于杀鸡用牛刀,参数量太大、容易过拟合、训练还慢。我一般会设计一个 3 层卷积的轻量 CNN,结构如下:
import torch import torch.nn as nn class ExpressionCNN(nn.Module): def __init__(self, num_classes=7): super().__init__() # 第一层:1通道输入 → 32个特征图 self.conv1 = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), # 输出 32x64x64 nn.BatchNorm2d(32), # 批归一化:加速收敛 nn.ReLU(inplace=True), nn.MaxPool2d(2) # 输出 32x32x32 ) # 第二层:32 → 64 self.conv2 = nn.Sequential( nn.Conv2d(32, 64, kernel_size=3, padding=1), # 输出 64x32x32 nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2) # 输出 64x16x16 ) # 第三层:64 → 128 self.conv3 = nn.Sequential( nn.Conv2d(64, 128, kernel_size=3, padding=1), # 输出 128x16x16 nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2) # 输出 128x8x8 ) # 全连接分类头 self.classifier = nn.Sequential( nn.Dropout(p=0.5), # 随机失活:防过拟合 nn.Linear(128 * 8 * 8, 512), nn.ReLU(inplace=True), nn.Dropout(p=0.5), nn.Linear(512, num_classes) ) def forward(self, x): x = self.conv1(x) x = self.conv2(x) x = self.conv3(x) x = x.view(x.size(0), -1) # 展平:从 [batch,128,8,8] → [batch,8192] x = self.classifier(x) return x模型结构说明见代码注释,但有几个设计参数要说透。第一层卷积核大小选 3x3 加 padding=1,这样卷积不会改变特征图尺寸,尺寸缩小完全由池化层负责,每个模块的特征图边长经过一次池化减半,从 64→32→16→8,这是清晰可控的下采样节奏。BatchNorm2d放在卷积和激活之间,作用是对每一层的输出做标准化,抑制内部协变量偏移,用大白话说就是让每层的输入分布不要跑偏太多,训练能更稳、收敛更快。两个Dropout(p=0.5)放在全连接层,因为全连接层的参数量占整个网络的大头,是过拟合的重灾区。最后x.view(x.size(0), -1)把三维特征图展平成二维矩阵,-1表示自动推断展平后的维度,这里实际是 128×8×8=8192。
4.2 训练脚本:训练循环、验证逻辑与检查点保存
模型定义好以后,训练脚本是整个项目里最容易出问题的一环。新手最常见的问题是训练循环和验证循环的职责分不清,甚至用验证集的数据去更新模型权重。下面是完整的最小可用训练脚本:
import torch import torch.optim as optim import torch.nn as nn num_classes=7)和交叉熵损失配合使用时,模型的输出层不需要手动加 Softmax,因为 `nn.CrossEntropyLoss` 内部已经做了 LogSoftmax 和 NLLLoss 的组合。如果在输出层手动加 Softmax 再交给这个损失函数,会造成数值不稳定,训练 loss 可能一直不降。 优化器用的是 AdamW,weight_decay 设置为 5e-4。这里的 AdamW 是 Adam 的修正版,它把权重衰减从梯度更新中解耦,实际效果就是更不容易过拟合,已经在 PyTorch 里成了默认推荐。学习率 1e-3 是 Adam 家族最常见的起始值,不是所有任务都适用,但表情识别这种中小规模数据上基本可以。训练过程中每轮计算一下验证集准确率和 loss,并且只在验证准确率提升时保存模型权重——这比每轮都保存要省心,也保证最后留下的 checkpoint 一定是最优的那一版。 > 提示:判断“是否该保存模型”一定要看验证集指标,不能看训练集指标。训练准确率一定是一直涨的,但那是模型记住了训练数据,不代表泛化能力。如果只看训练准确率保存模型,你会存下一堆过拟合的废品。 ### 4.4 超参数速查:一批可以直接拿去用的默认配置 为了方便你直接开工,我把这套配置整理成了一张速查表。这些参数不是最优解,但绝对是一个可靠的经验起点,你可以在它的基础上做小范围扰动实验。 | 参数项 | 推荐值 | 调整方向说明 | |---|---|---| | 输入尺寸 | 64x64 灰度图 | 数据质量差可升到 96x96,注意耗时翻倍 | | 卷积核大小 | 3x3,padding=1 | 不变,3x3 是性价比最高的选择 | | 卷积通道数 | 32→64→128 | 数据量大时每层翻倍到 64→128→256 | | batch_size | 64 | 显存不够降到 32,但收敛会慢一些 | | 学习率 | 1e-3 | 训练震荡时降到 3e-4 或 1e-4 | | 优化器 | AdamW | weight_decay 设 5e-4 | | 损失函数 | CrossEntropyLoss | 不要手动加 Softmax,以免数值不稳 | | Dropout 概率 | 0.5 | 全连接层之间用,过拟合严重提到 0.6 | | 训练轮数 | 50 | 收敛后早停,别盲目拉长 | | 数据增强 | 水平翻转 + 轻度仿射 | 不要加随机擦除/遮罩 | ## 5. 训练翻车与部署前的常见问题:一组亲身踩过的坑 ### 5.1 训练准确率接近 100%,但验证准确率只有 60%:过拟合要拆招 这个现象太典型了,属于深度学习入门必踩的坑。原因是模型参数量相对于数据量太大了,70 万样本不算多但模型有 800 多万参数,模型完全有能力把训练集“背下来”,但没有学到可泛化的表情特征。解决办法按优先级排序:第一,把 Dropout 概率从 0.5 提到 0.6 或 0.7;第二,检查数据增强是否生效,如果数据加载时忘了传 `train_transform` 而用了 `val_transform`,那就等于完全没有增强;第三,减少全连接层参数量,把中间的 512 降到 256;第四,加大 `weight_decay` 从 5e-4 到 1e-3。如果以上都做了还不行,再考虑加一层卷积并同时增加 Dropout。血泪经验是:从 Dropout 和增强入手最立竿见影,调整通道数是最后手段。 ### 5.2 用 CPU 训练一个 epoch 要 10 分钟:显存不足与设备选择的死局 这个坑几乎每个用自己笔记本跑的人都会遇到。现象是训练日志里每个 iteration 都要等很久,一个 epoch 下来要十来分钟,50 个 epoch 等于跑半天以上。原因很简单:CPU 算卷积本来就慢,而 64 的 batch_size 又加剧了负担。解决方案分两种情况。如果你有 NVIDIA 显卡,需要确认 PyTorch 装的是 CUDA 版本而不是 CPU 版本,可以用 `torch.cuda.is_available()` 验证,只要返回 True 就可以在训练脚本里加 `device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')`,并把模型和数据都 `.to(device)`。如果你没有显卡,那就要把 `batch_size` 降到 16,输入尺寸降到 48x48,卷积通道数减半,并把训练轮数缩短到 30 轮。这些改动以牺牲精度的代价换来能跑完。常见做法是先用小参数跑通流程、确认代码没问题,再逐步恢复参数上 GPU 或云主机跑正式实验。 ### 5.3 OpenCV 人脸检测把非人脸框进来:Haar 级联的边界 用 `cv2.CascadeClassifier` 自带的人脸检测模型,在合影照片上经常出现把人脸框错的现象,比如把墙上的海报人脸、手臂、甚至阴影框进来。原因是 OpenCV 自带的 Haar 模型是为了通用人脸检测训练的,精度有限且没有对齐能力。解决思路不是去换更重的检测模型,而是加一道过滤:把检测到的人脸框按宽高比过滤,正常人脸宽高比在 0.7 到 1.3 之间;如果一张图检测到多张脸,就只保留面积最大的那一张;如果检测不到脸,就直接跳过该图。这就是我在第 2 章讲两段式架构时说的,把检测和识别解耦,这里的检测如果不够稳,识别再好也没用。 ### 5.4 模型推理时对戴眼镜、遮挡的人脸频繁误判:数据增强覆盖不到的场景 这是个非常典型的部署场景坑。训练数据里大多是干净、无遮挡的人脸,但实际测试图片里可能会有人戴黑框眼镜、戴口罩或者刘海遮住眉毛。模型没见过这些形态,自然容易误判。解决方案是针对性做数据增强:`transforms.RandomErasing(p=0.3, scale=(0.02, 0.2))` 模拟遮挡,或者用 `transforms.GaussianBlur` 模拟模糊输入。这里要特别说明,我在第 3.3 节说不要在训练初期加 RandomErasing,但在你已经有一个 baseline 之后,针对遮挡场景加上它做二次训练是完全值得的。另外,如果测试图片里真的有戴口罩的样本,7 分类任务本身就得不到正确答案,这是数据分布的问题,不是模型缺陷,你需要自己判断这个测试用例是否合理。 ### 5.5 训练 loss 不降反升,或者直接变成 NaN:学习率与输入规范化问题的排查 loss 变成 NaN 是训练过程最吓人的一件事。排查顺序固定是:先检查输入数据里有没有异常值或全黑图,个别样本像素值爆炸会拖垮梯度计算;再检查归一化参数是否写反,比如 `Normalize(mean=[0.5], std=[0.5])` 应用在没转成 Tensor 的图上就会出问题;最后检查学习率是否过大,1e-3 起步偏大时可以降到 3e-4,尤其在使用 BatchNorm 的情况下,学习率太大会让统计量震荡。如果 loss 只是不降但数值正常,那多半是数据加载顺序被打乱了——`shuffle=False` 会让模型反复看到同分布的样本,失去随机梯度下降的意义。 ## 6. 把模型封装成可用的识别工具:推理脚本、可视化与最后的调优 训练完的模型要真正投入使用,需要写一个推理脚本。这个脚本做的事情是:读入一张图片、OpenCV 检测人脸、预处理、送入模型、打印出概率分布和预测结果。核心代码大概是: ```python def predict_single_image(model, image_path, transform, device, face_cascade): img = cv2.imread(image_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5) if len(faces) == 0: print('未检测到人脸') return None # 取面积最大的人脸 x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) face = gray[y:y+h, x:x+w] # 转 PIL Image 并走预处理流水线 face_pil = Image.fromarray(face) input_tensor = transform(face_pil).unsqueeze(0).to(device) with torch.no_grad(): output = model(input_tensor) probs = torch.softmax(output, dim=1).cpu().numpy()[0] return probs # 返回长度为7的概率向量这段代码里最容易出错的地方是预处理尺寸必须和训练时保持一致,这里 64x64 就是之前定死的,改了任何一边都会导致模型输入 shape 不匹配而报错。scaleFactor=1.1, minNeighbors=5这两个参数是 OpenCV 人脸检测经验值:scaleFactor 越小检测越慢但越准,minNeighbors 越大误检越少但漏检越多。如果你想把识别结果可视化,可以把表情标签和概率画在检测框旁边,做成一个实时摄像头识别脚本,这在演示文档视频里是非常出效果的功能点。最后的调优建议是:如果你训练出来的模型对某一类(通常是 disgust)准确率特别差,优先检查该类样本数,如果样本数确实少,用类别加权损失函数CrossEntropyLoss(weight=class_weights)给少数类更高的惩罚权重。我个人习惯是把这类拉胯类别单独打印出来观察,看看模型到底是把 disgust 错分到哪一类,如果是混到 surprise 或 neutral,说明这个类别本身定义模糊,不要死磕准确率,适当降低它的分类权重更实际。这套项目做到这里,从数据处理到模型训练再到部署推理,整条链路就通了,希望这些经验对你有帮助。
本文还有配套的精品资源,点击获取