ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于VGG16的人脸表情识别:从模型改造到工程实战

基于VGG16的人脸表情识别:从模型改造到工程实战 简介基于深度学习VGG16网络的人脸表情识别项目面向Python开发者与图像识别初学者解决六类人脸表情愤怒、快乐、惊讶、厌恶、悲伤、恐惧的分类建模问题。项目以VGG16为骨干网络完整覆盖数据集整理、模型训练、评估与推理流程适合作为CNN入门及表情识别实战的参考实现。压缩包共7个文件包含5个Python脚本、1个数据集压缩包及1份说明文档整体大小约59MB代码按模型定义、数据加载、训练、评估等模块拆分便于直接运行和二次修改。已有139人学习下载。除核心训练脚本外资源还提供了数据预处理与增强逻辑、模型保存/加载接口以及推理示例并针对数据集解压与标签生成给出了可操作的注意事项开发者可据此快速复现模型并针对自定义表情数据集进行迁移学习或参数调优。1. 用深度学习 VGG16 网络做人脸表情识别这份工程到底能跑到什么程度把“基于深度学习 VGG16 的人脸表情识别”这种课设级项目从 zip 压缩包变成真正能出指标的东西中间隔着不少坑。这份工程的价值在于把 model、data、train 三个目录的边界拆得很清楚训练、评估、推理三个脚本互不干扰而且 README 里那句“标签生成一次后放回注释”本身就是作者踩过坑之后留下的操作记号。整体要做的是训练一个 CNN 分类模型识别愤怒、快乐、惊讶、厌恶、悲伤、恐惧六种表情。如果你是刚接触图像识别的学生或者手头正好需要一份能直接改的代码基线这份资源比从零搭省下两三天的环境调试时间。我按提示解压数据集、放开注释生成标签、再启动 train.py四十分钟左右看到了正常的 loss 下降曲线。2. 把 VGG16 改造成 6 分类表情识别网络结构与分类头替换2.1 VGG16 的固定配方卷积块、池化堆叠与层级特征VGG16 是牛津大学 Visual Geometry Group 在 2014 年 ImageNet 竞赛里拿出的经典卷积网络结构特点是“规整到有点无聊”13 个卷积层加 3 个全连接层卷积层按通道数分成五个大块每一块末尾接一个 2×2 的最大池化层。第一块是两个 64 通道的 3×3 卷积第二块是两个 128 通道第三、四、五块逐步升级成三个 256 或 512 通道的卷积堆叠。因为卷积核统一用 3×3stride 固定为 1padding 为 1特征图尺寸在块内保持不变只有池化层会让宽高减半。这套结构适合人脸表情识别的原因在于感受野是逐层扩大的。浅层卷积关注眼角的纹理、嘴边的边缘方向这类局部细节深层卷积把局部细节组合成“嘴角上扬”“眉毛皱起”这种具有语义含义的局部模式最终全连接层把这些模式映射到类别概率。相比后来各种残差结构、注意力机制VGG16 没有花哨设计训练和调试都更容易控制变量对表情这种类间差异集中在五官局部区域的任务来说特征提取能力完全够用。“16”这个数字代表的是 13 个卷积层加上 3 个全连接层五层池化不计入命名——这是很多人看结构图时会疑惑的点。2.2 输出从 1000 类改成 6 类分类头怎么改ImageNet 上的 VGG16 最后输出 1000 类表情识别只保留 6 类所以需要把最后一层全连接替换掉。常用的做法是保留前面所有卷积块的预训练权重只把 fc8 从 1000 维改成 6 维然后在自己的数据集上重新训练。如果训练数据量不大fc6 和 fc7 也建议保留原始参数只微调最后几层这样能显著降低过拟合风险。需要注意的一点是ImageNet 预训练模型的 fc6 输入维度是 25088这个数字对应输入图像经过五层池化后变成 7×7×512展平就是 25088。如果换成 48×48 的灰度表情图特征图根本到不了 7×7这个维度就必须同步改。工程代码 VGGModel.py 里一般会给出两种处理方式一是把输入统一 resize 成 224×224完全复用原始结构二是保留特征提取部分把 VGG16 的 avgpool 换成nn.AdaptiveAvgPool2d((1,1))让特征图自适应压缩成 1×1×512然后把 fc6 改成 512 到 6 的全连接。第二种方案训练和推理都快很多是这个资源包里更常见的改法。在 PyTorch 里改输出维度并不复杂常见做法是import torch.nn as nn def build_vgg16_6class(base_model): # base_model 是 torchvision 里加载的 VGG16(pretrainedTrue) # 替换分类头原结构是 (fc64096, fc74096, fc81000) base_model.classifier[3] nn.Linear(4096, 4096) # fc7 保持 4096 base_model.classifier[6] nn.Linear(4096, 6) # fc8 换成 6 类 return base_model这里的逻辑是只改最后一层映射维度前面两层全连接保留 4096 维因为 VGG16 在高维特征上的表达能力靠的就是这两个大宽层的组合。改成 6 之后模型输出的是一个 6 维的 logits 向量后面配合交叉熵损失就能直接训练。如果数据集是 48×48 的灰度 FER2013 类别数据还需要处理通道数和特征图尺寸问题。最常见的处理顺序是灰度图复制成三通道先用torchvision.transforms.Resize((224,224))或者替换池化层二选一。先把输入问题解决再谈训练不然第一轮 forward 就会卡在维度匹配上。2.3 网络结构速查表从输入到输出的每一层尺寸下面这张表是我改分类头前后都会对着核对一遍的尺寸清单输入以 224×224×3 为例模块层组成输出张量尺寸说明block1conv3-64 ×2 maxpool112×112×64池化后宽高减半block2conv3-128 ×2 maxpool56×56×128通道翻倍、宽高减半block3conv3-256 ×3 maxpool28×28×256结构加深到三个卷积block4conv3-512 ×3 maxpool14×14×512感受野覆盖大部分人脸block5conv3-512 ×3 maxpool7×7×512最后一层特征图flatten展平250887×7×512 展平classifierfc6(4096) fc7(4096) fc8(6)6fc8 已改成 6 类这套链路里最容易出错的是 flatten 前的尺寸。用 224 输入五层池化后是 7×7用 48 输入五层池化后理论值是 1.5×1.5PyTorch 会做取整最后变成 1×1。这也是很多人在这个工程里第一次跑训练就报size mismatch的根本原因。我的习惯是先把模型 summary 打出来确认每个模块的输出尺寸再决定是否需要动 avgpool 层不要等报错再回头猜。3. 数据准备解压 emotion.zip、生成一次标签、预处理链路3.1 目录结构与解压步骤拿到工程压缩包后第一件事不是看代码而是把目录结构理清。train 文件夹里放着 dataset.py、VGGModel.py、train.py、evaluate.py、play.pydata 文件夹里是 emotion.zipREADME 说明训练时解压。model 文件夹在初始化时是空的训练完权重会存进去。这样划分的好处是数据、模型、训练脚本三个层面互相独立换数据集时不用动训练逻辑。解压数据集用命令行或者 Python 都行。我习惯用 Python 脚本统一处理顺便校验压缩包完整性import zipfile import os data_dir data os.makedirs(data_dir, exist_okTrue) with zipfile.ZipFile(data/emotion.zip, r) as zf: zf.extractall(data_dir) print(解压文件列表:, zf.namelist())这段代码把 emotion.zip 解压到 data 目录下。之所以用zf.namelist()打印文件列表是为了确认里面是 CSV 还是图片文件夹。FER2013 这类公开表情数据集常见的是 CSV 格式每行是像素序列加标签也有些版本是已经按类别分好的图片目录。两者处理逻辑完全不一样先看清楚再决定 dataset.py 怎么写。3.2 标签生成把注释里的代码放开跑一次再放回去这是 README 里强调最多的操作train 文件夹里有段代码是被注释掉的首次运行要取消注释执行一次标签生成然后把注释恢复再启动 train.py。很多人不理解为什么这么麻烦我实际跑了一遍才明白——这段注释代码做的事是把 CSV 里的像素串解析成 numpy 数组同时按比例切分训练集和测试集生成对应的标签文件存成.npy格式。如果每次启动训练都重新执行这段解析会有两个问题。一是浪费时间FER2013 有接近三万张图每轮训练前重新解析纯属白耗 CPU二是可能破坏切分结果因为解析脚本里通常有随机打乱逻辑如果每次跑都重新切分训练集和测试集的划分就完全不一致前一秒还在训练的样本下一秒可能出现在验证集里指标自然虚高。把生成过程固定成一次之后训练都加载已经存好的.npy才能保证同一批数据从同一个状态出发。这类脚本骨架大致长这样import pandas as pd import numpy as np # !!! 首次使用取消注释下面这段运行一次生成标签后立即恢复注释 !!! # df pd.read_csv(data/fer2013.csv) # pixels df[pixels].values # labels df[emotion].values # train_data, test_data, train_label, test_label ... # np.save(data/train_data.npy, train_data) # np.save(data/train_label.npy, train_label) # np.save(data/test_data.npy, test_data) # np.save(data/test_label.npy, test_label)逻辑说明整个脚本的核心目的是把原始数据持久化成本地文件。训练脚本通过np.load()直接读取这些文件不会再碰原始 CSV。参数上需要注意的是切分比例常见的是训练集 80%、测试集 20%如果类别不均衡还可以在切分时指定stratifylabels保持每类占比一致。提示标签生成只执行一次之后放回注释。重复运行不仅慢还会改变数据切分导致训练和评估失去可比性。3.3 预处理链路灰度图复制三通道、归一化、数据增强FER2013 是 48×48 的灰度图而 VGG16 设计输入是三通道。既然不想改网络第一层结构最省事的方式是在数据读取端把灰度图复制成三通道也就是把48×48变成3×48×48。这个操作在 dataset.py 的__getitem__里做比在网络层做通道适配灵活得多。配合前面提到的AdaptiveAvgPool2d方案整个模型可以完全不动卷积层结构只替换最后分类头。归一化方面常见做法是直接除以 255 把像素压缩到 0 到 1 区间再按 ImageNet 的均值方差做标准化。表情识别这种任务用 ImageNet 的统计量主要是为了对齐预训练权重期望的输入分布实际效果差距不大但保持一致能省很多调试时间。代码实现一般长这样import torch from torch.utils.data import Dataset from torchvision import transforms class EmotionDataset(Dataset): def __init__(self, data, labels, train_modeTrue): self.data data # shape: (N, 48, 48) self.labels labels if train_mode: self.transform transforms.Compose([ transforms.ToPILImage(), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]), ]) else: self.transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize(mean[0.5], std[0.5]), ]) def __len__(self): return len(self.labels) def __getitem__(self, idx): img self.data[idx] img img.astype(uint8) img self.transform(img) img img.repeat(3, 1, 1) # (1,48,48) - (3,48,48) return img, self.labels[idx]这里的关键参数有三个RandomHorizontalFlip概率取 0.5水平翻转对表情识别是合理的增强方式因为人脸左右对称表情语义不会因为翻转改变Normalize的均值和方差直接用 0.5因为输入已经是单通道灰度后续复制成三通道等价于每个通道都按同样参数做标准化repeat(3, 1, 1)是在通道维度上复制目标是把单通道变成适配 VGG16 输入的 RGB 三通道。测试集不启用随机翻转保证评估结果可复现。很多人在这步会犯一个错数据增强做过头。表情识别里旋转超过 20 度、随机裁剪比例过大都很容易把眼睛和嘴的位置切到正常范围之外反而破坏语义信息。我的经验是只做翻转和轻微平移不做旋转这类任务里小幅扰动比大幅变换可靠得多。4. 训练到推理train.py、evaluate.py、play.py 的三段式流程4.1 train.py训练循环、损失函数与模型保存train.py 是整个工程的入口负责加载数据、构建模型、跑训练循环、保存权重。训练的第一步是判断计算设备优先用 GPU没有就退回 CPU。表情识别这种任务48×48 输入在 CPU 上也能跑就是 epoch 时间会长不少。损失函数和优化器的选择有固定套路。多分类任务用CrossEntropyLoss它内部已经做了 log_softmax不需要在模型输出后单独接 Softmax。优化器我习惯用 Adam初始学习率设为 1e-4如果发现收敛太慢再把学习率提到 1e-3。Adam 自带自适应学习率对新手更友好SGD 需要手调动量和学习率衰减策略在这个规模的数据集上收益有限。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from dataset import EmotionDataset from VGGModel import build_vgg16_6class def train(): device torch.device(cuda if torch.cuda.is_available() else cpu) train_data torch.load(data/train_data.pt) train_label torch.load(data/train_label.pt) train_set EmotionDataset(train_data, train_label, train_modeTrue) train_loader DataLoader(train_set, batch_size64, shuffleTrue, num_workers2) model build_vgg16_6class().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-4) model.train() for epoch in range(30): total_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() total_loss loss.item() * images.size(0) avg_loss total_loss / len(train_set) print(fepoch {epoch:02d}, loss: {avg_loss:.4f}) torch.save(model.state_dict(), fmodel/vgg16_fer_{epoch:02d}.pth) if __name__ __main__: train()这段代码的逻辑并不复杂每个 batch 先把梯度清零forward 算输出用交叉熵算出 lossbackward 回传梯度optimizer 更新参数。代码里存权重时带了 epoch 编号这样能随时回滚到中间某个 epoch 的权重。我一般会每个 epoch 都存一次磁盘占用不大但后悔药的价值很高——如果第 25 个 epoch 开始过拟合可以直接回到第 20 个 epoch 的权重继续微调不用重新跑。DataLoader里的num_workers2在 Windows 上如果报错就改成 0这和数据量无关纯粹是系统兼容问题。4.2 超参数与防过拟合batch_size、epochs 与早停超参数是训练阶段最影响结果的部分也是最难一概而论的部分。我见过不少人上来就把 batch_size 设成 256结果显存直接爆掉。这个工程里 64 是一个合适的起点太小收敛慢而且每个 batch 的梯度噪声大太大对显存不友好且 VGG16 前几层卷积的梯度更新在大 batch 下容易收敛到尖锐极小值。学习率从 1e-4 开始如果前三个 epoch 的 loss 纹丝不动可以先确认数据有没有归一化再考虑调整学习率。不要一上来就调到 1e-2VGG16 这种网络在 1e-2 下很容易让 loss 原地发散。epochs 设多少要看验证集表现。README 里没写死 epoch 数我自己的做法是设一个较大上限比如 50配合早停机制当验证集准确率连续 5 个 epoch 没有提升就终止训练并恢复最佳权重。代码层面可以在每个 epoch 结束后跑一次验证记录最好的准确率和对应权重文件这对防止过拟合比任何正则化手段都直观。额外的正则化手段包括weight_decay参数。Adam 优化器里设weight_decay1e-4是常见配置作用是对大权重做惩罚让模型参数保持在较小的范围内。这个值不宜超过 1e-3否则会让模型欠拟合。如果你加上数据增强和早停已经能看到验证集准确率稳定上升weight_decay不加也行这个工程里数据量不算大加一点更保险。4.3 evaluate.py 与 play.py验证指标、单图推理与实时摄像头训练完了接下来要做两件事在测试集上评估、在实际图片或摄像头上推理。evaluate.py 的逻辑是加载保存好的权重在测试集上做一次完整的前向传播统计 top-1 准确率。测试时必须先model.eval()再包一层torch.no_grad()否则模型里的 Dropout 层和 BatchNorm 层会把测试结果带偏。def evaluate(): device torch.device(cuda if torch.cuda.is_available() else cpu) model build_vgg16_6class().to(device) model.load_state_dict(torch.load(model/best.pth, map_locationdevice)) model.eval() test_data torch.load(data/test_data.pt) test_label torch.load(data/test_label.pt) test_set EmotionDataset(test_data, test_label, train_modeFalse) test_loader DataLoader(test_set, batch_size64, shuffleFalse) correct 0 total 0 with torch.no_grad(): for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() print(ftest accuracy: {100 * correct / total:.2f}%)torch.max(outputs, 1)返回每一行的最大值和对应索引索引就是预测类别。如果你要在测试协议里按类别分别统计准确率可以用sklearn.metrics.classification_report它会把六类表情各自的 precision、recall、F1 都打出来这在第 5 章讲类别不均衡时会用到。play.py 则是把模型从离线验证搬到真实场景。它会用 OpenCV 读取图片或者摄像头画面先用 Haar 级联检测器框出人脸区域裁剪后缩放成网络需要的输入尺寸然后进行一次前向推理把六类概率里最大的那个标签画在检测框上方。这里需要注意人脸检测框和模型输入尺寸的对应关系——把裁剪出的人脸直接cv2.resize成 48×48 或 224×224 即可不需要保持原始宽高比人脸轻微拉伸对分类结果影响很小。import cv2 import torch import numpy as np def predict_face(frame, model, device): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) faces face_cascade.detectMultiScale( gray, scaleFactor1.1, minNeighbors5, minSize(48, 48) ) for (x, y, w, h) in faces: face gray[y:yh, x:xw] face cv2.resize(face, (48, 48)) face_tensor torch.from_numpy(face).float().div(255.0) face_tensor face_tensor.repeat(3, 1, 1).unsqueeze(0).to(device) with torch.no_grad(): outputs model(face_tensor) probs torch.softmax(outputs, dim1) confidence, pred torch.max(probs, 1) label emotions[pred.item()] cv2.rectangle(frame, (x, y), (xw, yh), (0, 255, 0), 2) cv2.putText(frame, f{label}({confidence.item():.2f}), (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return framescaleFactor1.1代表每次检测窗口缩放的比例越小越慢但检测框更精细minNeighbors5表示每个候选框需要至少 5 个邻居投票才算有效值越大漏检越多但误检更少。这两个参数在多人场景或侧面人脸上需要来回试没有绝对标准。torch.softmax拿到六类概率后max返回的 confidence 直接显示在框上低于 0.5 的预测基本可以忽略宁可显示 unknown 也不要硬给一个错误标签。5. 避坑记录五个把新手卡住的翻车现场5.1 标签生成脚本重复运行训练集和验证集悄悄混在一起现象模型在训练集上准确率逼近 98%测试集准确率却只有 60% 出头而且每次跑测试准确率波动很大像是撞运气。原因README 里那句“标签生成一次后放回注释”被忽略了。每次启动训练都重新解析原始 CSV随机切分逻辑会让同一张图在这一次属于训练集、下一次属于验证集模型实际接触过的数据远多于正常训练集导致验证指标完全失真。解决严格按注释提示操作——第一次放开生成脚本运行完立刻放回注释之后训练、评估都用固定的.npy文件。如果你拿到的是别人跑过的工程先检查 data 目录下是否已有生成的.npy文件如果有直接注释掉生成逻辑。5.2 48×48 灰度图喂给 VGG16通道和全连接维度都报错现象训练第一轮 forward 直接崩溃报错信息要么是Expected 3 input channels, got 1要么是size mismatch for classifier.0: expected 25088, got 512。原因VGG16 第一层接受三通道输入而 FER2013 只有单通道同时原始 VGG16 的全连接层按 224×224 输入设计48×48 经过五层池化后特征图变成 1×1×512不是 7×7×512。解决两处都要改。第一处在数据集端把灰度图复制成三通道第二处在模型端把 avgpool 替换成nn.AdaptiveAvgPool2d((1,1))并同步把 fc6 的in_features改成 512。具体代码在第 2.2 节改完这两个位置就能跑通。5.3 类别不均衡让喜乐类碾压厌恶和恐惧现象测试集整体准确率有 70%但逐个类别看快乐类准确率 90%厌恶类只有 40%恐惧类也长期在 50% 上下。整体指标看起来不错实际根本没法投产。原因公开表情数据集里天然存在标注偏差厌恶和恐惧这类表情样本数量少、实际拍摄时也容易和其他类别混淆。模型在多数类上训练充分少数类还没学够就到 epoch 上限了。解决三个办法叠加使用。第一在切分标签时用stratify保持训练集和测试集类别比例一致第二数据增强时对少数类加大增强强度第三在CrossEntropyLoss里传入weight参数给样本少的类别更高权重。权重计算常见做法是用类别样本数的倒数做归一化这样训练时每个类别的有效贡献基本持平。5.4 微调时把整个网络全放开显存直接溢出现象加载了 ImageNet 预训练权重之后直接把requires_grad_(True)应用到所有层batch_size 调到 128跑了不到一个 epoch显卡报CUDA out of memory。原因VGG16 参数量接近 1.4 亿大部分集中在全连接层。全参数微调意味着反向传播要保存所有中间激活值显存占用是训练分类头的好几倍小显卡根本扛不住。解决如果不是专攻表情识别方向不要全参数微调。把前四个卷积块全部冻结只让最后一个卷积块和全连接层参与训练batch_size 回落到 64显存占用能降一半以上准确率损失很小。冻结在 PyTorch 里就是对层设置requires_grad False优化器也只传入需要更新参数的层。5.5 训练在 GPU、推理在 CPU加载权重报错现象训练完把model/vgg16_fer.pth拷到另一台没显卡的机器上加载时直接报错提示无法解析 key 或者找不到设备。原因PyTorch 保存的state_dict里张量带有cuda:0的设备信息CPU 机器直接加载时不知道往哪放。解决保存权重时建议统一保存成 CPU 版本加载时也加map_location。代码里用model.state_dict()保存不要保存整个模型对象加载时写torch.load(model/best.pth, map_locationcpu)这样不管是 GPU 还是 CPU 环境都能稳定加载。想更省心的话保存时可以先把模型挪回 CPU 再存存出来的文件体积也略小一点。6. 进阶技巧冻结微调、混淆矩阵与更快的推理6.1 冻结前四个卷积块只训练最后一层和全连接在数据量有限的情况下全网络微调很容易过拟合。常见做法是冻结 VGG16 的 features 前四块只训练最后一块卷积和 classifier。做法是遍历模型参数以层名作为判断条件设置requires_gradfor name, param in model.named_parameters(): if name.startswith(features.18) or name.startswith(classifier): param.requires_grad True else: param.requires_grad False optimizer optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr1e-4)features.18是第五个卷积块的起点编号具体数字以实际模型层级为准。优化器只接收需要更新的参数冻结部分的权重在反向传播中不会有梯度更新。这样训练显存占用低很多收敛也更快。如果你发现验证集准确率卡住不动了再逐步放开更靠前的卷积层而不是一次性全放开。6.2 用混淆矩阵看哪两类表情最容易被搞混准确率只回答“对错”的问题不回答“错成什么”。验证阶段把全部预测结果收集起来用sklearn画一张 6×6 混淆矩阵能直观看到错在哪。from sklearn.metrics import confusion_matrix, classification_report import seaborn as sns import matplotlib.pyplot as plt # all_preds 和 all_labels 在评估循环里收集 cm confusion_matrix(all_labels, all_preds) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabelsemotions, yticklabelsemotions) plt.show()实际测试里最容易混的是“愤怒”和“厌恶”其次是“悲伤”和“恐惧”这种混淆符合人类认知不必过分纠结。如果某两类的混淆明显超出合理范围优先检查数据标注是否有误其次是增强策略是否把两类特征拉得太近。6.3 导出 TorchScript 或 ONNX 模型推理速度提升一截训练用的 PyTorch 模型在 CPU 上跑单张推理前向传播在几十毫秒量级作为 demo 没问题但部署到服务端或嵌入式设备上就显得吃力了。可以先把模型转成 TorchScript 格式不需要额外依赖部署时直接用torch.jit.load加载。scripted torch.jit.script(model.eval().cpu()) scripted.save(model/vgg16_fer_scripted.pt) # 部署端加载 loaded torch.jit.load(model/vgg16_fer_scripted.pt, map_locationcpu)TorchScript 把网络结构和权重打包成一个文件推理时不依赖原始 Python 类定义加载速度更快进程启动也更干净。如果你要把模型接到 OpenCV 或移动端导出 ONNX 是更通用的选择但第一步先用 TorchScript 把流程串起来后续再换 ONNX 也不迟。这个表情识别工程整体数据流清晰适合当第一份深度学习图像分类项目来复现。从那以后我每拿到一个类似工程第一件事都是先看数据生成脚本是不是“只跑一次”的这个动作帮我躲掉了很多玄学问题。如果你顺着这份笔记跑通了 train.py再往后的方向就很多了——换 EfficientNet、加注意力模块、做多任务属性识别都可以在这个骨架上继续长。希望帮到你。本文还有配套的精品资源点击获取
返回列表