☰
基于深度学习的人脸表情识别:CNN从零构建到调参实战
2026/10/16 2:50:24 网站建设 项目流程

做面部表情识别这个项目,我前后折腾了大概三周。如果你正打算练手深度学习,或者需要交一个能打的项目,我强烈建议你试试这个方向。它听起来只是一个图像分类任务,但真正跑通之后你会发现,数据处理、模型设计、训练调参、评估部署,整条流水线全走了一遍,对CNN的理解也会上一个台阶。这篇文章我试着把从思路到实操的完整过程拆开讲,包含我踩过的坑和调参心得,希望能让你少走弯路。

1. 为什么值得做:从问题定义到技术选型

1.1 表情识别到底在解决什么问题

面部表情识别,本质上是一个图像分类问题。输入一张人脸图片,模型输出对应的情绪类别。目前学术和工业界比较常用的是七分类:愤怒、厌恶、恐惧、开心、悲伤、惊讶、中性。也有做三分类的(积极、消极、中性),但七分类信息量更大,也更能体现模型区分细微特征的能力。

很多人以为表情识别就是“看嘴和眼睛”,做完了才发现不是这么简单。同一个表情在不同人脸上差异极大,有人开心只是嘴角微微上扬,有人开心是咧嘴大笑;光照变化、头部姿态、眼镜遮挡、胡子、皮肤颜色都会影响特征提取。这就决定了它不是一个“一眼就能分对”的任务,模型需要学到的是一组相对鲁棒的表情特征,而不是简单的像素模板。正因如此,它非常适合做深度学习项目的入门级课题,麻雀虽小五脏俱全。

如果你准备做毕设或者个人项目,这个主题还能自然扩展到实时摄像头检测、疲劳驾驶预警、课堂专注度分析等场景,后续可玩性很高。

1.2 为什么选深度学习而不是传统方法

早些年做表情识别,主流方案是先用人脸关键点检测定位眼睛、眉毛、嘴巴等区域,再提取几何特征或纹理特征(比如LBP、HOG),最后接SVM或随机森林分类器。这套流程我试过,有两个痛点:第一,特征工程非常依赖人工经验,光照一旦变化明显,特征分布就乱了;第二,关键点检测在侧脸、遮挡场景下经常失效,整个识别链路就崩了。

深度学习方案,尤其是CNN,把“特征提取”和“分类”两件事一起学出来了。你给它原始像素,它自己会找到有用的纹理和局部结构,不需要你手工设计特征。推理时也简单:输入图像,输出一个概率分布。从工程角度看,这条链路更短、更稳定,精度上限也更高。

当然,深度学习不是万能药。它需要大量标注数据、需要GPU训练(或者耐心用CPU跑小模型)、需要处理过拟合。但整体收益明显大于成本,所以现在的表情识别系统基本都是基于CNN或其变体做的。

1.3 技术路线选择:从零搭建还是迁移学习

这里通常有两条路线。

第一条,从零设计并训练一个小型CNN。好处是结构完全可控,训练速度快,对硬件要求低,而且你能从底层理解卷积、池化、激活函数这些组件到底在干嘛。坏处是精度上限受模型容量限制,在复杂数据集上可能只有60%到70%的准确率。

第二条,迁移学习。比如加载在ImageNet上预训练好的ResNet18、MobileNetV2或EfficientNet,把最后一层全连接换成7分类,然后微调全模型或只微调最后几层。好处是收敛快、精度高,数据少也不容易过拟合;坏处是你需要装预训练权重,显存占用也会大一些。

我的建议是两条路都走一遍:先用自己的小模型跑通流程,再切换迁移学习做对比。这样你既能看清CNN的底层逻辑,又能知道真实项目中为什么大家偏爱预训练模型。做对比实验也是项目汇报里很加分的部分。后面我主要讲自制小CNN的方案,迁移学习作为扩展方案给出来。

2. 数据准备与预处理:决定模型上限的第一关

2.1 数据集选型与标注特点

公开的表情识别数据集不算多,常用的是FER2013(约35000张48x48灰度人脸图)、RAF-DB(约30000张真实场景彩色图)、AffectNet(更大,但有争议)。如果你只是想快速跑通流程,我建议先用FER2013,因为它下载方便、格式统一、图片尺寸小,训练起来快。而且它包含7个类别的标签,正好匹配七分类任务。

有一个事情你要提前知道:FER2013的标签噪声偏高。有人做过统计,清洗后的准确率会比原始标签高不少。也就是说,不是你模型不行,而是部分标签本身标错了。看到验证集准确率卡在65%左右上不去,先不要急着调参,可以考虑抽样看看到底是预测错了还是标签错了。这对初学者是个很实用的心态建设。

RAF-DB的优势是更接近真实场景,有遮挡、光照变化、夸张角度,模型的鲁棒性会更好。缺点是彩色图像尺寸不一致,预处理要多做一些工作。你可以在小模型稳定跑通之后,再用RAF-DB做一轮训练作为对比。

2.2 预处理细节

表情识别和普通图像分类有个区别:人脸区域占比很重要。如果整张图里人脸很小、背景很杂,模型很容易学偏。所以预处理流程一般是:

  1. 人脸检测,裁出人脸区域。
  2. 缩放到统一尺寸(比如48x48或64x64)。
  3. 灰度化(FER2013本来就是灰度图,但如果是自己采集的数据,灰度化可以降低光照和肤色影响)。
  4. 归一化,把像素值从[0, 255]映射到[0, 1]或标准正态分布。

以PyTorch为例,很多人习惯用transforms.Normalize((0.5,), (0.5,)),这样会把像素归一化到[-1, 1],也可以。如果加载的是预训练模型,要注意用模型要求的分辨率和归一化参数。

数据增强是另一个重要环节。表情识别很容易过拟合,因为同一个人在不同照片里的表情高度相似。如果不做增强,模型会把“某个人”当作“某种表情”。我的增强组合是:

  • 随机水平翻转(概率0.5)
  • 随机旋转(在-10度到10度之间)
  • 随机亮度、对比度扰动
  • 随机遮挡(模拟眼镜、口罩,用Cutout随机遮掉一小块区域)

这些操作都在线完成,相当于每一轮训练都会看到略微不同的样本,能明显提升泛化能力。但也别增强过度,我试过把旋转调到30度,结果模型把“惊讶”和“开心”都学混了,因为角度变化会让嘴型失真。

2.3 划分训练集与验证集的坑

这个坑我必须单独拿出来说:人脸数据一定要按人头划分,不要按图片随机划分。

什么意思?如果你直接把所有图片随机打乱,那么同一个人的多张照片会同时出现在训练集和验证集里。模型相当于见过这个人的“部分照片”,再在验证集里看到他“另一张照片”,这种信息泄露会让验证集准确率虚高,而且到了真实场景会断崖式下跌。

正确做法是拿到数据时先看每个身份(subject)有多少张图片,按身份分出一部分作为验证集,另一部分作为训练集。FER2013原始划分是已经固定好的,不需要改;如果你用RAF-DB或者自定义数据,就要注意这一点。我当时用RAF-DB时吃足了亏,训练集验证集准确率都到了八成,换成摄像头实测只有五成多,就是因为没按人头分。

3. 模型设计与关键参数解析

3.1 CNN基础结构设计

一个适合表情识别的小型CNN,不需要特别复杂。我的常用结构可以概括为:卷积块堆叠 + 全局池化 + 全连接分类。

一个卷积块通常是:卷积(Conv2d)→ 批归一化(BatchNorm2d)→ ReLU激活 → 最大池化(MaxPool2d)。具体到表情识别,我用过的一个稳定结构长这样:

  1. 第一块:Conv2d(1, 32, kernel_size=3, padding=1),输出32通道特征图,MaxPool缩小一半。
  2. 第二块:Conv2d(32, 64, kernel_size=3, padding=1),输出64通道,MaxPool再缩小一半。
  3. 第三块:Conv2d(64, 128, kernel_size=3, padding=1),输出128通道,MaxPool第三次缩小。
  4. 最后接一个全局平均池化(Global Average Pooling),把特征图压缩成128维向量。
  5. 全连接层分类,输出7类。

输入如果是48x48灰度图,经过三次池化后变成6x6,信息量刚刚好。如果你用64x64输入,可以再加一块。核心思路是:随着网络变深,通道数增加,空间分辨率下降,让模型先从局部纹理慢慢抽象到整体表情语义。

3.2 为什么要用批归一化和Dropout

先讲批归一化。它的作用是在每个批次内对特征做归一化,让每层输入分布尽量稳定。很多初学者发现模型loss降不下去,加一个BN往往就顺了。因为BN缓解了梯度弥散,允许你用相对较大的学习率,收敛速度肉眼可见地变快。在表情识别这种小模型上,BN基本是标配。

Dropout的作用是防止过拟合。训练时按一定概率随机“丢弃”神经元,相当于同时训练多个子网络,推理时再合在一起,能起到集成效果。我一般会在全连接层前加一个Dropout(0.5),如果发现过拟合严重,就把概率提高到0.6甚至0.7。

要注意Dropout和BN有微妙的冲突:如果模型里每个卷积后面都接BN,再在全连接前加Dropout,效果通常是好的;但如果卷积后面又加Dropout又加BN,可能会互相抵消。我的习惯是BN放在卷积后、激活前,Dropout只放在最后接入全连接之前。

3.3 损失函数与优化器选择

表情识别是个多分类问题,损失函数首选交叉熵(CrossEntropyLoss)。它已经把Softmax和负对数似然合在一起实现了,所以不需要在网络最后一层单独加Softmax,直接输出7维logits即可。

优化器我推荐AdamW,相比经典Adam,它的权重衰减更规范,在小模型上表现稳定。初始学习率设1e-3,配合余弦退火或StepLR衰减。如果训练到一半发现loss震荡,可以把学习率降到3e-4甚至1e-4。

还有一个参数容易被忽略:Batch Size。FER2013单张图只有48x48灰度图,很轻量,所以我一般设置64或128。Batch Size太小(比如8)会导致梯度噪声大、不稳定;太大(比如512)虽然稳,但每个epoch更新次数少,训练周期要拉长。如果显存不够,减小Batch Size的同时建议稍微降低学习率。

4. 训练流程与评估:别只盯着准确率

4.1 训练策略

训练流程本身不复杂,每一轮做三件事:跑训练集前向传播、算loss、反向传播更新参数;然后跑验证集算准确率和loss;保存效果最好的模型参数。

有几个策略我记得特别清楚:

  • 早停法。每个epoch之后看验证集loss,连续5个epoch没下降,就提前结束训练。很多时候训练到20个epoch就过拟合了,硬跑50个epoch纯属浪费算力。
  • 学习率衰减。我一般用StepLR,每10个epoch把学习率乘0.1。先大步快跑,再小步精调,效果比固定学习率好不少。
  • 模型保存。保存时需要包括模型结构参数、优化器状态、当前epoch、最佳准确率。这样中途想换机器接着训练,可以直接加载,不会白跑。

另外,如果你用的是自制小CNN,强烈建议先用少量数据(比如每个类别100张)过一遍流程,确认代码没有bug,再全量训练。我第一次作图时没这么做,直接全量训练,跑到第3个epoch才发现数据加载器有个归一化bug,白白浪费了几个小时。

4.2 评估指标

准确率是最直观的指标,但在表情识别上,准确率会骗人。原因在于数据不均衡:开心、中性这类样本占比高,厌恶、恐惧这类占比低。模型哪怕把所有样本都预测成“开心”,准确率也可能接近30%到40%,看起来还行,实际上一点用都没有。

所以一定要看混淆矩阵和各类别的精确率(Precision)、召回率(Recall)、F1分数。我当时第一次训练完,总体准确率78%,但看混淆矩阵发现“恐惧”只有42%的召回率,大量“恐惧”被分成了“悲伤”。这个信息是准确率看不出来的。

针对类别不均衡,有两个方法比较有效。一是给损失函数加类别权重,让稀缺类别错分的惩罚更大,权重可以按1/类别频率来算,再归一化。二是用Focal Loss,它让模型更关注难分样本,适合表情这种类间有重叠的任务。二选一即可,我自己的体验是Focal Loss调参稍微麻烦一些,先用类别权重比较省事。

4.3 可视化与调试

训练过程中建议把loss曲线和准确率曲线画出来,观察是收敛还是震荡。还有一个容易被忽略的调试方法:把预测错误和预测正确但置信度低的样本打印出来看。你可能会发现,有些“错误”其实是标签错了,模型反而是对的;有些错误是图像太模糊,人眼都难以判断。这两种样本不应该成为优化目标,非要模型在这些样本上得分,只会导致过拟合到噪声上。

5. 实操:完整复现流程

5.1 环境与依赖

我的环境是Python 3.10 + PyTorch 2.x + OpenCV + NumPy + scikit-learn + Matplotlib。用conda创建环境比较省心:

conda create -n facial-expression python=3.10 conda activate facial-expression pip install torch torchvision opencv-python numpy scikit-learn matplotlib

PyTorch的安装命令会因操作系统和CUDA版本而异,如果你没有NVIDIA显卡,直接装CPU版本也能训练这个小模型,就是慢一些。为了快速迭代,我建议至少先在小数据集上确认流程是对的就切全量。

5.2 数据加载

以FER2013的CSV格式为例,每行是label,pixel0,pixel1,...,pixel是48x48的灰度值。读取和转换可以用下面的代码:

import pandas as pd import numpy as np import torch from torch.utils.data import Dataset, DataLoader from torchvision import transforms from PIL import Image class FER2013Dataset(Dataset): def __init__(self, csv_path, train=True, transform=None): df = pd.read_csv(csv_path) # FER2013自带Usage列:Training / PublicTest / PrivateTest if train: df = df[df['Usage'] == 'Training'] else: df = df[df['Usage'] == 'PublicTest'] self.labels = df['emotion'].values.astype(np.int64) self.pixels = df['pixels'].values self.transform = transform def __len__(self): return len(self.labels) def __getitem__(self, idx): pixel_string = self.pixels[idx] arr = np.fromstring(pixel_string, dtype=int, sep=' ') arr = arr.reshape(48, 48).astype(np.uint8) img = Image.fromarray(arr, mode='L') if self.transform: img = self.transform(img) return img, self.labels[idx] train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) val_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)), ]) train_dataset = FER2013Dataset('fer2013.csv', train=True, transform=train_transform) val_dataset = FER2013Dataset('fer2013.csv', train=False, transform=val_transform) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=128, shuffle=False, num_workers=4)

需要注意,np.fromstring在NumPy 2.x中可能被移除建议,如果报错就改成np.fromstring(pixel_string, dtype=int, sep=' ')换成np.array(list(map(int, pixel_string.split(' '))))。

5.3 模型定义

下面是一个适合入门的小型CNN,结构和前面讲的相符:

import torch.nn as nn class ExpressionCNN(nn.Module): def __init__(self, num_classes=7): super().__init__() self.features = nn.Sequential( # block1: 48x48 -> 24x24 nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # block2: 24x24 -> 12x12 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # block3: 12x12 -> 6x6 nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.global_pool = nn.AdaptiveAvgPool2d(1) self.dropout = nn.Dropout(0.5) self.classifier = nn.Linear(128, num_classes) def forward(self, x): x = self.features(x) x = self.global_pool(x) x = torch.flatten(x, 1) x = self.dropout(x) x = self.classifier(x) return x

这里用AdaptiveAvgPool2d(1)做全局平均池化,好处是不管输入分辨率是多少,最后都能压成128维向量。如果你后续想切换到64x64输入,这个模型不需要改结构。

5.4 训练主循环

训练代码的核心是一个双层循环加验证。下面是一个可运行的最小版本:

import torch.optim as optim import torch.nn as nn from sklearn.metrics import confusion_matrix, classification_report, accuracy_score torch.manual_seed(42) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = ExpressionCNN(num_classes=7).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1) epochs = 40 best_acc = 0.0 for epoch in range(epochs): model.train() running_loss = 0.0 for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) model.eval() val_preds, val_labels = [], [] with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) preds = torch.argmax(outputs, dim=1) val_preds.extend(preds.cpu().numpy()) val_labels.extend(labels.cpu().numpy()) val_acc = accuracy_score(val_labels, val_preds) print(f"Epoch {epoch+1}/{epochs} | Loss: {running_loss/len(train_dataset):.4f} | Val Acc: {val_acc:.4f}") if val_acc > best_acc: best_acc = val_acc torch.save({ 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'epoch': epoch, 'best_acc': best_acc, }, 'best_model.pt') scheduler.step()

训练完成后,加载最优权重,打印混淆矩阵和分类报告:

checkpoint = torch.load('best_model.pt', map_location=device) model.load_state_dict(checkpoint['model_state_dict']) cm = confusion_matrix(val_labels, val_preds) print(cm) print(classification_report(val_labels, val_preds, digits=4))

我当时跑了40个epoch,验证集准确率大约到了72%到78%之间。不算特别高,但作为小型CNN在FER2013上的表现,已经算合理水平。如果换成ResNet18微调,同样的数据能到82%左右。

5.5 扩展到实时摄像头识别

训练完静态图片之后,最自然的就是接摄像头做实时识别。流程是:OpenCV读取视频帧 → 人脸检测器框出人脸 → 截取人脸区域 → 预处理成48x48灰度图 → 输入模型推理 → 把标签和置信度画到画面上。

import cv2 face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') model.eval() cap = cv2.VideoCapture(0) emotion_labels = ['Angry', 'Disgust', 'Fear', 'Happy', 'Sad', 'Surprise', 'Neutral'] while True: ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(48, 48)) for (x, y, w, h) in faces: face = gray[y:y+h, x:x+w] face = cv2.resize(face, (48, 48)) face_tensor = torch.from_numpy(face).float().unsqueeze(0).unsqueeze(0) face_tensor = (face_tensor - 127.5) / 127.5 face_tensor = face_tensor.to(device) with torch.no_grad(): output = model(face_tensor) prob = torch.softmax(output, dim=1) pred_idx = torch.argmax(prob, dim=1).item() confidence = prob[0][pred_idx].item() label = f"{emotion_labels[pred_idx]} {confidence:.2f}" cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, label, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow('Facial Expression Recognition', frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()

这里有一个重要的归一化细节:如果训练时用transforms.Normalize((0.5,), (0.5,)),等价于(像素值 / 255 - 0.5) / 0.5,也就是像素值 * (1/255) * 2 - 1。所以上面代码里(face_tensor - 127.5) / 127.5在数值上基本等价。如果你用到OpenCV读出来的灰度图是0到255范围的uint8,必须记住先转成float再归一化,否则模型收到的是0到255的权重,输出概率会乱成一片。

6. 常见问题与排查技巧实录

6.1 训练损失不下降

先检查数据预处理。最常见的坑是像素值没归一化,输入变成0到255、CrossEntropyLoss对超大logits输出的梯度极小,网络几乎学不动。其次看学习率,太高会在初始阶段震荡甚至loss爆炸,太低则收敛极慢。我一般先用1e-3快速试跑100个batch,看loss是否明显下降,再决定调高或调低。

6.2 过拟合

症状是训练准确率逼近99%,但验证准确率停滞在60%左右。这时候优先做的事不是加大模型,而是:加大Dropout比例、增加数据增强强度、加上早停。我试过一个有意思的现象:强行加深模型反而更容易过拟合,不如保持小模型加正则。对小项目来说,一个简单可靠的小模型+强数据增强,往往比复杂大模型效果好。

6.3 类别不均衡

如果某个类别(比如厌恶、恐惧)准确率明显低,可以先算类别频率分布。FER2013里“厌恶”样本只有几百张,“开心”有近万张,天然不均衡。一种快速解决方式是在DataLoader里用WeightedRandomSampler,让稀有类别每个epoch被采样到更多次。我实际对比过,加了这个采样器之后,“厌恶”的召回率能从20%出头提升到40%左右。

6.4 摄像头实时识别卡顿

卡顿的主要原因不是模型太大,而是人脸检测器在高分辨率帧上耗CPU。解决办法有两个:一是把摄像头画面缩小后再做检测,比如原始1280x720,先缩一半到640x360,检测速度会快很多;二是降低推理频率,比如每3帧才检测一次,检测到后人脸位置用上一帧的结果平滑跟踪。亲测之后,帧率能从每秒10帧提升到25帧以上。

6.5 不同数据集之间迁移效果差

在FER2013上训练好的模型,拿摄像头实测往往效果打折扣。原因很简单:数据集图像的采集环境和你摄像头实际环境不一样,包括人脸姿态、光照、图像清晰度。实用建议是在部署前,用摄像头采集50到100张自己的照片,做一次轻量微调(fine-tune)。这个操作能大幅提升真实场景的使用体验。

6.6 环境安装报错

最常见的是NumPy版本和PyTorch冲突,导致导入dataloader报错。我的建议是用conda统一管理环境,不要用系统Python直接装;另外优先装PyTorch官方源提供的wheel,避免源码编译。如果你只有一个老显卡,驱动和CUDA版本对不上,最简单的办法就是装CPU版本,小模型训练慢一点,但完全够用。

参数速查表

参数项推荐值说明
输入尺寸48x48 或 64x64灰度图,CHW格式
初始学习率1e-3AdamW配合时可以略大
Batch Size64 ~ 128越小噪声越大,越大训练越稳但更慢
Epochs30 ~ 50配合早停使用
Dropout0.5过拟合严重时调到0.6
优化器AdamW,weight_decay=1e-4比经典Adam更稳
损失函数CrossEntropyLoss类别不均衡时加class weight
数据增强翻转、旋转10度、亮度对比度扰动不要过度旋转
验证集划分按人头分组防止数据泄漏

常见问题速查表

现象可能原因排查方向
训练和验证准确率都很低预处理错误或学习率不当检查归一化、试试1e-3学习率
训练高验证低过拟合加Dropout、加增强、早停
某一类准确率特别低数据不均衡WeightedRandomSampler或class weight
验证高实测差数据分布不同或数据泄漏按人头划分,微调模型
摄像头卡顿人脸检测太耗时降低检测分辨率,隔帧检测
加载权重报错模型结构和保存时不一致检查num_classes和输入通道

做完整套流程,我最大的感受是,表情识别这个项目并不需要多么天才的想法,它考察的是你按部就班完成一个系统的能力。你从数据清洗开始,到模型设计,到训练调参,到评估分析,再到部署验证,每一步都有清晰的反馈。哪怕最终准确率只有70%多,你也亲手搭建了一个从输入到输出的完整系统,这比背概念、刷题要实用得多。

最后再分享一个我踩过的坑:保存模型时一定要把优化器状态和当前epoch一起保存。我第一次只存了model.state_dict(),后来想接着训练,加载权重后学习率已经不重要了,但优化器的动量信息全丢了,前几个epoch的loss波动特别大,白白浪费了不少时间。如果你计划中途休息或者换机器跑,这个细节能省很多事。

做项目的乐趣就在于一点点把坑填平。如果你也跑通了这个流程,建议下一步试试对实时流做多人识别,或者把模型导出成ONNX再部署到移动端,那又是另一个值得折腾的世界。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询