简介:这是一份基于ResNet的人脸表情识别Python期末项目,面向深度学习入门者及计算机视觉课程学生,适合作为课程设计或期末大作业参考。项目涵盖图像预处理、ResNet残差网络搭建、FER2013类数据集准备、模型训练、混淆矩阵可视化及视频实时测试等完整环节,可帮助读者系统掌握人脸表情识别从数据到部署的流程。压缩包共16个文件、5.2MB,主要包含3个Python脚本(模型定义、训练、测试)、7张表情样本图片、说明文档、依赖库清单及一个演示视频。已有236人学习下载。通过学习该资源,读者不仅能理解ResNet解决梯度消失的原理,还能实践数据增强、优化器选择、损失函数定义等关键操作,并借助提供的代码与文档快速复现实验、对比不同模型效果,为后续图像分类任务打下基础。
1. 基于ResNet的人脸表情识别期末大作业:从选题到交付的完整技术路线
“python期末大作业基于ResNet的人脸表情识别”这个标题我看着眼熟,很多同学会把它当做一个“交差项目”:跑通一个模型,出个准确率,截图塞进报告。但把这个方向拆透了你会发现,它其实是把深度学习入门里最完整的一条链路串起来了——数据清洗、图像预处理、迁移学习、训练调参、模型推理,每一环都是期末答辩时老师最爱追问的点。这里想把这条链路从头到尾展开:数据集怎么选、ResNet18为什么够用、训练代码怎么写、踩过哪些坑,以及最后怎么让演示现场不出丑。适合零基础到刚入门的学生,也适合想快速复现一个表情识别方案的工程师。
2. 表情识别为什么选ResNet:七类标签与残差结构的匹配度分析
2.1 数据集先定生死:FER2013的标签分布与自建数据的成本
人脸表情识别最常见的数据集是FER2013,Kaggle上的经典比赛数据。它包含35887张48x48的灰度人脸图,分成7类:angry、disgust、fear、happy、sad、surprise、neutral。拿来做期末作业最省事:数据集现成,标签已对齐,网上能直接下载csv格式。但这里有个需要注意的地方,disgust类样本量少得可怜,只有几百张,而happy有七千多张,这种不均衡会在后面的训练和评估里造成实实在在的翻车点,先有个心理准备。
如果不想用现成数据集,也可以自建:用OpenCV调用摄像头,自己摆表情拍几百张,再用Haar或MTCNN把脸框出来裁成小图。自建的好处是答辩时故事完整,老师问“数据从哪来的”你能讲清楚;缺点是从采集、清洗到标注全是工作量,期末周时间通常不够。我的建议是:以FER2013为主,自建少量(比如20-30张自己的脸)作为最后的demo验证,两头兼顾。
关于数据集是否要统一为灰度:FER2013本身就是48x48灰度图,而自建的数据用cv2.cvtColor转成灰度后要resize到与训练一致的尺寸。常见做法是训练用48x48,让输入图和标签一一对应,避免resize不一致带来的“训练准、推理不准”的玄学问题,这类问题在表情识别这种对局部纹理敏感的任务上尤其明显。
2.2 ResNet18的残差结构在表情任务里的实际收益
很多同学上来就想用ResNet50或ResNet101,觉得层数越多越厉害。但表情识别是典型的“小数据集+分类数少”任务,FER2013只有三万张图,用ResNet50很容易过拟合,而且训练时间长,期末答辩之前如果还没跑完一个epoch,心态直接崩。ResNet18的参数量只有约11M,在CPU上也能完成一次完整训练(慢一点而已),在GPU上几分钟一个epoch,这才是能让你反复调参的体量。
残差结构在这类任务里的意义,不是单纯把网络加深,而是让梯度在反向传播时能通过shortcut直接流回浅层,缓解深层网络退化。对表情识别来说,表情特征是局部的(眼睛、嘴角的纹理变化),不需要特别大的感受野,ResNet18的卷积堆叠足够提取这些边缘和纹理信息。换成VGG16也可以做,但参数量是ResNet18的几倍,训练速度慢不少,在期末作业这个场景里没有优势。
我一般会直接用torchvision里现成的resnet18,改最后一层全连接输出为7就行。这样既不用自己从零写残差块,又能借用ImageNet预训练权重里的底层特征,对应标题里的resnet预训练模型这个关键词。这里要说明:ImageNet预训练权重和表情任务虽然域不同(自然图像 vs 人脸),但浅层的边缘、纹理、色彩特征是可以迁移的,所以迁移学习几乎总比随机初始化收敛更稳。训练部分会给出具体写法。
2.3 预处理链路:人脸对齐、数据增强与归一化
预处理直接影响模型能不能学到表情,而不是学到背景或头发。常见做法是先用OpenCV的人脸检测器定位人脸,把检测框扩一点边再裁出来,resize到固定尺寸,然后再做灰度转换(如果模型按单通道输入)。FER2013本身已经是对齐好的人脸,不用再检测,只需要在DataLoader里做resize和归一化。
数据增强方面,我常用的组合是RandomCrop加Padding、RandomHorizontalFlip、RandomRotation十度以内。幅度不能太大,否则人脸被裁掉关键部位,表情标签就失真了。ColorJitter在这种灰度图上意义不大,可以不加。
下面这段是PyTorch里最常见的训练预处理写法:
# transforms.py from torchvision import transforms # 训练集:加了水平翻转和小角度旋转,模拟头部姿态变化 train_transform = transforms.Compose([ transforms.Resize((48, 48)), transforms.RandomHorizontalFlip(p=0.5), transforms.RandomAffine(degrees=10, translate=(0.1, 0.1)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) # 灰度图单通道 ]) # 验证集/测试集:只做最基础的resize和归一化,不引入随机性 val_transform = transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ])RandomHorizontalFlip是水平翻转,概率设为0.5,意思是有一半的样本会做镜像。RandomAffine做小角度旋转和轻微平移,模拟头部姿态变化,但旋转角度别超过10度,旋转过大人眼都看不出表情,网络会更迷惑。Normalize用0.5和0.5是因为图像被ToTensor转到[0,1]后,归一化到[-1,1]区间,ResNet对输入范围的预期就是[-1,1],这算是一个隐性约束。
关于python安装cv2这个操作,如果本地环境没装OpenCV,先pip install opencv-python,import cv2不报错再往下走。很多同学卡在第一步就是因为跳过了环境验证就直接跑代码,报错后还以为是代码问题。
3. 用PyTorch搭训练代码:迁移学习、数据加载与Loss收敛
3.1 从FER2013的csv写出一个可复用的Dataset类
在动手写训练脚本之前,先把python环境配置这一步确认好:torch、torchvision、opencv-python、pandas这几个包装上,版本不需要太新,torch 2.x配上对应的torchvision即可。环境这种问题虽然不起眼,但期末机房或新电脑上最容易在这里耗掉大半天。
数据加载从写好一个Dataset类开始。FER2013的csv有两列:emotion标签和pixels像素值,pixels是48x48=2304个整数,用空格分隔。
# dataset.py import pandas as pd import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class Fer2013Dataset(Dataset): def __init__(self, csv_path, transform=None): df = pd.read_csv(csv_path) self.transform = transform self.labels = df['emotion'].values.astype(np.int64) self.pixels = df['pixels'].values def __len__(self): return len(self.labels) def __getitem__(self, idx): # csv里的pixels是一串空格分隔的灰度值,先拆再reshape成48x48 pixels = np.array(self.pixels[idx].split(' '), dtype=np.float32) img = pixels.reshape(48, 48) img = Image.fromarray(img.astype(np.uint8), mode='L') if self.transform: img = self.transform(img) return img, torch.tensor(self.labels[idx])这个Dataset的核心逻辑是把csv里的一行字符串拆成2304个数值,reshape成48x48的单通道灰度图,再用PIL的Image.fromarray转成图像对象。为什么要转成PIL图像而不是直接用numpy数组?因为torchvision的transforms里大部分操作(Resize、RandomAffine等)都接受PIL图像作为输入,直接喂numpy数组在个别版本里会踩坑。
标签映射是0到6的整数,对应7类表情。为了让答辩时输出的预测结果可读,建议在代码里写一个常量字典:emotion_labels = {0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy', 4: 'sad', 5: 'surprise', 6: 'neutral'}。当模型输出一个logits向量时,取argmax得到索引,再查这个字典就能得到表情名。这个小细节在写推理部分时非常重要,别等到最后才补。
另外,训练集和验证集要分开读。常见做法是直接把csv按行切分:80%训练、20%验证,切分前先shuffle,不然按原始顺序切会让某些类别只出现在某一侧。pandas的sample(frac=1).reset_index(drop=True)配合drop参数可以完成shuffle。
3.2 加载预训练ResNet18并替换最后一层
这是整个项目里最核心的代码片段,对应resnet预训练模型这个关键词。
# model.py import torchvision.models as models import torch.nn as nn def get_model(num_classes=7): # 新版本torchvision推荐用weights参数,而不是pretrained=True model = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1) in_features = model.fc.in_features model.fc = nn.Linear(in_features, num_classes) # 把1000分类换成7分类 return model在较新版本的torchvision里,写pretrained=True会有DeprecationWarning,推荐用weights参数显式指定。model.fc是ResNet18最后一层全连接,原来输出1000类(ImageNet),把它换成输出7类的Linear层,前面的卷积层和残差块的参数全部保留预训练权重。这一步是迁移学习的标准操作,逻辑很简单:底层特征复用,顶层分类器替换。
有个容易忽略的坑:替换fc层之后,模型的参数被分成两部分,卷积层参数是ImageNet预训练好的,fc层参数是随机初始化的。训练时fc层学习速度要快得多,如果学习率设置太大,容易出现前几个epoch验证集准确率原地不动、训练loss快速下降的诡异现象,这是因为fc在乱跳而卷积层还没动。第5章会展开讲这个问题的排查思路。
提示:第一次运行时会自动下载约45MB的resnet18权重文件,如果网络状态不理想,提前手动下载放到torchvision缓存目录,避免训练到一半卡在下载。
3.3 训练循环、验证集评估与best_model保存
训练循环我习惯写得简单直接,不用工程化的封装,因为期末作业重在可读性和可复现。
# train.py import torch import torch.nn as nn from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR from dataset import Fer2013Dataset from model import get_model model = get_model(num_classes=7).to(device) criterion = nn.CrossEntropyLoss() optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4) scheduler = CosineAnnealingLR(optimizer, T_max=20) best_val_acc = 0.0 for epoch in range(20): model.train() total_loss, correct, total = 0, 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() total_loss += loss.item() * images.size(0) _, predicted = torch.max(outputs, 1) correct += (predicted == labels).sum().item() total += labels.size(0) scheduler.step() # 每个epoch末尾在验证集上做一次评估 model.eval() val_correct, val_total = 0, 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, 1) val_correct += (predicted == labels).sum().item() val_total += labels.size(0) val_acc = val_correct / val_total if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), "best_model.pt") # 保留验证集上最好的权重 train_acc = correct / total train_loss = total_loss / total print(f"epoch={epoch}, loss={train_loss:.4f}, acc={train_acc:.4f}, val_acc={val_acc:.4f}, lr={scheduler.get_last_lr()[0]:.8f}")这段代码里有几个关键参数值得解释。第一个是学习率1e-4,直接调大5倍到5e-4,初期loss会掉得很快,但后面很可能在某个点开始震荡不收敛。表情识别数据集小,预训练模型的学习率就应该比从头训练小一个量级。第二个是weight_decay=1e-4,它是对权重做L2正则,抑制过拟合,配合数据增强一起使用。CosineAnnealingLR把学习率从1e-4按余弦曲线降到接近0,配合20个epoch刚好覆盖一个完整周期。
每个epoch打印loss、train acc、val acc和lr这四样就够监控了。val acc稳步上升是正常的,如果train acc比val acc高出一大截,说明过拟合在提前发生;如果loss曲线出现锯齿状抖动,常见原因是batch_size太小导致梯度噪声大,可以把batch_size从32提到64或128看看。保存best_model.pt的逻辑也很关键:训练后期会出现val acc先升后降的过拟合阶段,保留验证集上表现最好的权重,比默认用最后一个epoch的权重可靠得多。
4. 让期末演示不出丑:推理脚本、摄像头实时识别与答辩验证
4.1 单张图片推理脚本:前处理必须与训练一致
训练完的模型要能对实际图片做预测,这一步很简单但容易写错。先加载模型权重,再对一张图片走一遍与训练完全相同的前处理流程。
# predict.py import cv2 import torch from PIL import Image import torchvision.transforms as transforms from model import get_model emotion_labels = {0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy', 4: 'sad', 5: 'surprise', 6: 'neutral'} model = get_model(num_classes=7) model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) model.eval() transform = transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) img = cv2.imread("test.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) pil_img = Image.fromarray(gray) tensor = transform(pil_img).unsqueeze(0) with torch.no_grad(): logits = model(tensor) pred_idx = torch.argmax(logits, dim=1).item() print("predict:", emotion_labels[pred_idx])这段代码最关键的是训练和推理的前处理必须完全一致:都是48x48、都是灰度、都用一样的Normalize参数。如果训练时用RandomAffine做增强,推理时不要带随机增强,只保留Resize、ToTensor、Normalize这确定性三步。有同学把训练的transform复制到推理里,结果随机增强让同一个人脸在不同时刻预测出不同表情,这就是把随机性带进了确定性环节。
加载权重时注意map_location="cpu"这个参数,如果模型是用GPU训练的,在没GPU的机器上直接load会报错。用map_location把权重映射到CPU是最稳的做法,期末演示现场机器往往没有GPU。torch.load里有个weights_only选项,如果加载时报错,改成weights_only=False再试,一般是因为权重文件里混入了额外的pickle对象,属于小概率事件但碰到了很浪费时间。
4.2 摄像头实时推理:检测与识别解耦,帧率才稳
期末现场演示摄像头识别是很多人的保留节目,也是最容易翻车的地方。常见的翻车原因是:每帧都跑一次人脸检测加一次模型推理,检测模块本身要几十毫秒,推理又要几十毫秒,结果画面变成PPT。
我的做法是把人脸检测和表情识别分开跑,用一个队列做缓冲,检测线程负责逐帧找脸,识别主循环每次从队列里取最新的一帧做推理,频率由识别侧自己控制,不跟着摄像头帧率走。
# camera_demo.py import cv2 import threading import queue import torch from PIL import Image import torchvision.transforms as transforms from model import get_model emotion_labels = {0: 'angry', 1: 'disgust', 2: 'fear', 3: 'happy', 4: 'sad', 5: 'surprise', 6: 'neutral'} transform = transforms.Compose([ transforms.Resize((48, 48)), transforms.ToTensor(), transforms.Normalize(mean=[0.5], std=[0.5]) ]) model = get_model(num_classes=7) model.load_state_dict(torch.load("best_model.pt", map_location="cpu")) model.eval() face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml") q = queue.Queue(maxsize=2) # 队列只保留最新一帧,放不下的旧帧直接丢弃 def detect_face(): cap = cv2.VideoCapture(0) while True: ok, frame = cap.read() if not ok: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48)) if len(faces) > 0: if q.full(): q.get() # 清掉旧帧,保证识别侧拿到的永远是最新的 q.put((frame.copy(), faces[0])) threading.Thread(target=detect_face, daemon=True).start() while True: try: frame, (x, y, w, h) = q.get(timeout=0.5) except queue.Empty: continue # 检测线程还没出结果,先等下一轮 face = frame[y:y+h, x:x+w] face = cv2.cvtColor(face, cv2.COLOR_BGR2GRAY) face = cv2.resize(face, (48, 48)) pil_img = Image.fromarray(face) tensor = transform(pil_img).unsqueeze(0) with torch.no_grad(): pred = torch.argmax(model(tensor), dim=1).item() cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, emotion_labels[pred], (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2) cv2.imshow("face", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break这个模式的核心是queue的maxsize=2,检测线程往队列塞最新的人脸帧,如果队列满了就先丢旧的再放新的,避免内存堆积。识别主循环从队列取帧,取不到就continue,这样即使检测慢,画面也不会卡死。detectMultiScale的参数scaleFactor=1.1是每次缩放搜索的比例,越小越慢但越准;minNeighbors=5表示一个候选框至少被附近5个框确认才算人脸,太大可能漏检,太小会出一堆假框。
摄像头演示还有一个人脸检测器的选择问题。Haar检测器是OpenCV内置的,不用额外装模型文件,速度够快,但侧脸和暗光下容易漏检。如果期末现场灯光不好,建议提前用手机图片或校园卡照片多测几张。另一种选择是MTCNN或YOLOv8-face,精度更高但依赖安装更多,期末作业没必要折腾,Haar够用。
4.3 答辩时防追问的验证材料:混淆矩阵与分类报告
期末答辩时老师问的最多的不是“你准确率多少”,而是“你这个模型对哪些类分得不好、为什么”。这时候拿出混淆矩阵,比任何口述都有说服力。
# evaluate.py import torch from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt model.eval() all_preds, all_labels = [], [] with torch.no_grad(): for images, labels in val_loader: images = images.to(device) outputs = model(images) preds = torch.argmax(outputs, dim=1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm = confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues") plt.savefig("confusion_matrix.png") print(classification_report(all_labels, all_preds, target_names=list(emotion_labels.values())))分类报告里的precision、recall、f1-score比单个准确率信息量大得多。如果fear的recall只有0.3,说明大量fear样本被分成了别的类,你可以预先想好解释:fear和surprise在表情上本来就有极大的视觉重叠,模型分不清是数据特性,不是实现bug。这种准备做到位,答辩就不慌。
5. 常见问题与避坑排查:期末周最常翻车的五个真实场景
5.1 训练Loss不下降:预训练权重可能根本没加载上
现象:训练的loss从第一步开始就停留在2.0附近(7类均匀分布的交叉熵约等于1.95),多个epoch后基本不动,精度也在14%左右徘徊。
原因:最常被忽略的是模型初始化问题。如果代码里写的是models.resnet18(pretrained=False),或者用了旧接口pretrained=True但没有真正下载权重文件,那实际是随机初始化训练,几十个epoch都未必能从零学起来。另一个可能:学习率设得太小,1e-4对随机初始化的ResNet来说几乎推不动。
解决:在训练脚本开头加一个权重检查。打印model.conv1.weight的统计值,如果预训练权重加载成功,conv1的weight应该有一个比较明显的取值范围;如果所有数值都接近均匀分布,说明没加载成功。再确认get_model里是否显式传了weights参数,检查代码里有没有在get_model之后又写了一行models.resnet18()把模型覆盖掉。这种低级错误在期末周特别常见,排查顺序一定是:先确认权重真的进来了,再谈调参。
5.2 训练集精度98%、验证集只有40%:过拟合还是数据泄露
现象:train_acc一路升到98%,val_acc停在40%上下,验证loss不降反升。
原因:一是数据增强太弱或没开,模型把训练集整体背下来了;二是预处理不一致,训练时Resize到48x48但验证时用了不同尺寸;三是自建数据时切分没有shuffle,验证集和训练集的人脸来源不一致,比如验证集里全是另一个人的脸。FER2013本身是按人分割的,不存在第三种情况,但自建数据很容易中招。
解决:把val_transform和train_transform对齐(Resize、Normalize必须一致);加大RandomHorizontalFlip和RandomAffine的强度,必要时加RandomErasing;观察验证loss曲线,如果它在下降后反弹,就在反弹前的epoch保存模型作为best_model,而不是默认用最后一个epoch。这些动作做完,val_acc至少能拉回10个点。
5.3 中文路径、imread返回None与Python环境配置的三个提醒
现象:代码在Linux上跑得好好的,拷到Windows上,cv2.imread(“测试.jpg”)返回None,直接崩;或者数据集文件路径带了中文,在图像读取阶段莫名失败。
原因:OpenCV的imread在Windows上对中文路径支持有问题,这是老生常谈了。另外,如果项目路径里有中文用户名(比如C:\Users\张三\Desktop),很多图像读取接口都会出类似问题。
解决:图像路径统一改用英文,项目目录树里不要出现中文和空格;如果一定要读中文路径,用cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)代替imread。灰度图同理,decode后再转。这个问题其实属于python环境配置的范畴:装好OpenCV后,先做一个最小验证,imread一张纯英文路径的图看返回是不是None,再往下写业务逻辑,能省掉后面一大半排查时间。
5.4 摄像头推理卡成PPT:检测和识别挤在一个线程里
现象:摄像头画面每2-3秒才跳一下,人在镜头前动一动,画面完全跟不上。
原因:人脸检测和表情识别是两级串行计算,Haar检测本身占30-50ms,ResNet18推理一次在CPU上约50-100ms,两者加起来再把waitKey的延迟算上,帧率自然掉到个位数。这个属于“整体流程写通了但没做并发拆分”的典型问题,期末演示时特别容易翻车。
解决:用4.2节的队列方案把检测和推理解耦。如果还卡,就把检测从每帧改成每3帧一次,识别同样跳帧处理,牺牲一点实时性换取演示流畅。比在摄像头脚本里堆各种超参更有效的做法是先保证画面流畅,再去谈检测精度,这决定了你现场演示是“哇”还是“哦”。
5.5 显存溢出:batch_size、图片尺寸与num_workers的折算
现象:代码跑第一个epoch就报CUDA out of memory,或者程序运行一段时间后内存持续上涨直到被杀。期末机房的老机器显存只有4G的话,这个问题几乎是必现的。
原因:batch_size太大、图片尺寸太大、DataLoader的num_workers开得太高、训练和验证循环同时加载了太多中间变量。
解决:把batch_size从128降到64或32,依次减半试;图片尺寸从48x48换成32x32是一种极端的降级方案,表情分类对分辨率不敏感,32x32也能跑;num_workers在Windows上保持默认0就好,开多线程容易出别的错。训练完一个epoch后可以显式del掉不再用的中间变量再调torch.cuda.empty_cache(),虽然有人觉得这是玄学,但对长时间运行的脚本确实有缓解作用。
注意:OOM排查顺序别搞反,先降batch_size,再降尺寸,最后才考虑换模型。直接换ResNet50在4G显存上大概率更困难。
6. 把期末作业做成能被记住的东西:验证、错误案例与演示脚本
最后的验证环节我习惯做三件事:第一,在验证集上重新跑一遍分类报告,确认每个类的f1-score记录在案;第二,从验证集里挑出10张被分错的样本,把预测标签、真实标签和图像缩略图一起打印出来,看一眼是哪几类在互相混淆;第三,录一段自己的表情视频,用训练好的模型逐帧预测,看输出的类别序列是否在合理范围内跳动。
这三件事做完,你对这个模型的理解深度就和“我跑通了一个项目”完全不同了。比如你会发现大部分fear被分成了surprise,说明这两个类别在FER2013的标注上本身就模糊,这不是能靠调参彻底解决的,而是数据层面的特性,答辩时直接如实说明比强行解释效果更好。
我自己踩过一个印象很深的坑:第一次做表情识别时,为了赶时间直接在最后一层输出2类,把七分类问题强行做成“开心/不开心”二分类,准确率倒是好看,但老师一追问就露馅了。后来老老实实做七分类,虽然准确率只有六成多,但我能说出每个类错在哪、数据长什么样、下一步怎么改,成绩反而比“高准确率的假模型”好很多。
这也算是我对这类项目的一个态度:期末大作业最好的交付物,不是一个漂亮的准确率截图,而是一条你能从头讲清楚的数据与模型链路。如果你手头正卡在某个报错上,按第5章的排查顺序走一遍,大概率能解决;如果都跑通了,那就去验证集上多翻几页错误样本,那才是真正拉开差距的地方。希望帮到你。
本文还有配套的精品资源,点击获取