ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MobileViT三尺寸选型与迁移学习:端侧表情识别实战

MobileViT三尺寸选型与迁移学习:端侧表情识别实战 简介本资源面向深度学习入门与进阶开发者提供基于MobileViT的xxs、xs、s三个轻量级版本实现的人脸表情识别迁移学习分类实战项目框架为PyTorchPython适合需要快速上手图像分类迁移学习、完成课程设计或竞赛实战的读者。压缩包共约2000个文件包含1989张jpg表情图像构成的数据集、5个py训练与推理脚本、3个pt模型权重、1个txt环境依赖说明、1个readme使用指南及1个json配置文件整体约45.05MB目录按train、val、test划分结构清晰。项目覆盖交叉熵损失、AdamW优化器评估指标含loss、acc、混淆矩阵、recall、precision、F1分数等train脚本负责训练并生成权重与曲线val脚本评估测试集性能infer脚本支持单张图像推理。目前已有245人学习适合希望掌握MobileViT迁移学习全流程的读者参考实践。1. 从一次表情识别翻车说起MobileViT 三个尺寸到底怎么选去年帮一个做在线课堂的团队调表情识别模型他们拿 ResNet50 在 FER2013 上刷到 66% 就上不去了部署到端上推理一帧要 200 多毫秒安卓中端机直接卡成幻灯片。换成 MobileViT 的 xs 版本参数量掉到 2.3M 左右精度反而涨了两个点单帧推理压到 30ms 以内。这就是我后来一直推这套「基于 MobileViT 的 xxs、xs、s 三版本 7 类表情迁移学习」实战资源的原因——它不是玩具 demo是把轻量骨干、迁移学习、端侧部署这条链路完整跑通的工程包附带数据集和完整代码。适合两类人一类是想把 CNN 换成 Transformer 系轻量骨干、但被显存和延迟卡住的算法同学另一类是要在移动端落地表情/情绪识别、需要一套能直接改的基线代码的工程同学。xxs、xs、s 三个尺寸对应三种算力预算选型逻辑后面会拆开讲。2. MobileViT 三尺寸的选型逻辑与迁移学习原理2.1 为什么是 MobileViT 而不是纯 CNN 或纯 ViT纯 ViT 的问题在于自注意力是全局的计算量随分辨率平方增长端侧根本扛不住纯 CNN 的感受野又受卷积核限制对表情这种需要全局上下文眉眼嘴角联动的任务浅层 CNN 容易把局部纹理当全部信息。MobileViT 的核心思路是「CNN 提局部 MobileViT Block 提全局」先用深度可分离卷积做下采样和局部特征提取再在低分辨率特征图上做轻量化的注意力建模最后用卷积把全局信息融合回去。这样既保留了 CNN 的归纳偏置又拿到了 Transformer 的长程依赖能力。三个尺寸的差异主要在宽度通道数和 MobileViT Block 的堆叠次数上。xxs 最窄适合 224 以下输入、算力极紧的场景xs 是甜点档多数端侧表情任务用它s 更宽精度上限高但延迟和显存也上去了。下面这张表是我实测下来三个版本在 FER2013 七分类上的大致区间具体数值随训练策略浮动但量级关系稳定版本参数量量级输入 224 单帧延迟中端机七分类精度区间适用场景xxs~1.3M15-20ms63%-65%低端机、多任务并行xs~2.3M25-35ms65%-67%主流端侧首选s~5.6M50-70ms66%-68%精度优先、算力充足注意精度区间不是绝对值迁移学习策略和数据集划分影响很大别拿这张表当 benchmark 硬指标。2.2 迁移学习的两种接法特征提取 vs 微调这套资源里迁移学习不是简单 load 权重就完事它给了两种接法。第一种是冻结骨干、只训分类头适合目标数据集小几千张且和预训练域接近的情况收敛快、不容易过拟合。第二种是分层解冻微调先冻骨干训头几个 epoch再逐步解冻靠后的 stage学习率用余弦退火压小。表情识别和 ImageNet 的自然图像域差异不小我一般建议先跑冻结版拿到基线再上微调版对比别一上来就全量微调血泪经验是显存炸了不说小数据集上还容易把预训练权重带偏。import torch import torch.nn as nn from mobilevit import mobilevit_xxs, mobilevit_xs, mobilevit_s def build_model(archxs, num_classes7, freeze_backboneTrue): # 按尺寸选骨干pretrainedTrue 加载 ImageNet 预训练权重 if arch xxs: model mobilevit_xxs(pretrainedTrue) feat_dim 320 elif arch xs: model mobilevit_xs(pretrainedTrue) feat_dim 384 else: model mobilevit_s(pretrainedTrue) feat_dim 640 # 替换分类头表情七分类 model.classifier nn.Linear(feat_dim, num_classes) if freeze_backbone: # 只放开分类头骨干全部冻结 for name, param in model.named_parameters(): if classifier not in name: param.requires_grad False return model # 冻结版学习率可以给大一点1e-3 model_frozen build_model(xs, freeze_backboneTrue) # 微调版分层解冻时学习率要压到 1e-5 ~ 1e-4 model_finetune build_model(xs, freeze_backboneFalse)这段代码的关键参数有三个arch决定用哪个尺寸num_classes按你的数据集类别数改freeze_backbone控制迁移策略。feat_dim是各版本骨干输出维度xxs 是 320、xs 是 384、s 是 640这个值写错分类头就对不上会直接报维度不匹配。冻结版训练时优化器只传filter(lambda p: p.requires_grad, model.parameters())否则会把冻结参数也塞进优化器浪费显存。2.3 数据增强与七类表情的类别不平衡处理FER2013 这类数据集有个通病happy 和 neutral 样本多disgust 和 fear 少得可怜直接训模型会偏向多数类。资源里的做法是组合增强 加权损失。增强用 RandomHorizontalFlip、RandomRotation(10)、ColorJitter 轻微调注意表情识别里水平翻转是安全的左右脸对称但垂直翻转和大幅旋转会破坏表情语义别乱加。类别不平衡用带权重的 CrossEntropyLoss权重按类别频率的倒数算。from torchvision import transforms from torch.utils.data import DataLoader, WeightedRandomSampler train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p0.5), # 表情左右对称安全 transforms.RandomRotation(10), # 小角度别超过 15 transforms.ColorJitter(brightness0.2, contrast0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) # 按类别频率算采样权重缓解 disgust/fear 样本过少 def make_sampler(labels): class_count torch.bincount(torch.tensor(labels)) weight_per_class 1.0 / class_count.float() samples_weight weight_per_class[torch.tensor(labels)] return WeightedRandomSampler(samples_weight, len(samples_weight)) train_loader DataLoader(train_set, batch_size32, samplermake_sampler(train_labels), num_workers4)WeightedRandomSampler让少数类被采到的概率提高配合num_workers4加速数据加载。batch_size 在 xxs 上可以开到 64s 版本建议 16 或 32看显存。Normalize 的均值方差用 ImageNet 的因为骨干是 ImageNet 预训练的这一步对齐了迁移效果才稳。3. 从数据集到训练完整跑通七分类流程3.1 数据集目录结构与标签映射资源里的数据集按七类分文件夹存放标准 ImageFolder 结构就能直接读。七类通常是 angry、disgust、fear、happy、sad、surprise、neutral。跑之前先确认每个类别的样本数如果某类少于 200 张要么补数据要么在损失里加大权重否则那一类的召回率会很难看。# 数据集目录长这样 dataset/ ├── train/ │ ├── angry/ │ ├── disgust/ │ ├── fear/ │ ├── happy/ │ ├── sad/ │ ├── surprise/ │ └── neutral/ └── val/ └── (同样七个类别文件夹) # 快速统计每类样本数 for d in dataset/train/*/; do echo -n $d: ; ls $d | wc -l; done这个统计命令一定要先跑一遍我见过太多人拿到数据集直接开训训完发现 disgust 只有几十张模型压根没学会。标签映射由 ImageFolder 自动按文件夹名排序生成class_to_idx打印出来确认一下顺序后面推理时类别索引要对得上。3.2 训练循环与关键超参设置训练脚本里几个超参直接决定能不能收敛。优化器用 AdamWweight_decay 给 0.05冻结阶段学习率 1e-3微调阶段 1e-5 到 1e-4余弦退火 5 个 epoch 的 warmup。早停看验证集准确率patience 设 8 到 10别设太小表情任务验证集波动大patience3 很容易误停。import torch.optim as optim from torch.optim.lr_scheduler import CosineAnnealingLR def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss, correct, total 0, 0, 0 for imgs, labels in loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() outputs model(imgs) loss criterion(outputs, labels) loss.backward() optimizer.step() total_loss loss.item() * imgs.size(0) correct (outputs.argmax(1) labels).sum().item() total imgs.size(0) return total_loss / total, correct / total # 冻结阶段 optimizer optim.AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr1e-3, weight_decay0.05) scheduler CosineAnnealingLR(optimizer, T_max30, eta_min1e-6) criterion nn.CrossEntropyLoss(weightclass_weights.to(device))filter(lambda p: p.requires_grad, ...)这行是冻结训练的关键只把需要梯度的参数交给优化器。class_weights就是前面按类别频率算的权重张量。T_max设成总 epoch 数eta_min是学习率下限别设 0留一点让后期还能微调。3.3 三版本对比实验怎么设计才公平想验证 xxs、xs、s 哪个适合你的场景实验设计要控制变量同一份数据划分、同一套增强、同样的 epoch 数和早停策略只换骨干。记录三个指标——验证集准确率、单帧推理延迟、峰值显存。延迟测试要在目标设备上跑别拿服务器 GPU 的数字糊弄自己端侧部署看的是手机上的表现。import time def measure_latency(model, input_size(1, 3, 224, 224), devicecpu, runs50): model.eval().to(device) dummy torch.randn(*input_size).to(device) # 预热避免首次推理的初始化开销污染结果 for _ in range(10): _ model(dummy) start time.time() with torch.no_grad(): for _ in range(runs): _ model(dummy) return (time.time() - start) / runs * 1000 # 毫秒预热那 10 次不能省第一次推理有算子初始化和缓存开销不预热测出来的延迟偏高。runs50取平均单次测量噪声太大。三个版本各跑一遍把准确率和延迟放一起看选那个精度够用、延迟留有余量的版本别一味追高精度。4. 避坑与排查迁移学习表情识别最常见的五个翻车点4.1 现象训练 loss 不降准确率卡在 14% 左右原因基本是标签没对齐或者分类头维度写错。七分类随机猜是 14.3%卡在这个数说明模型啥也没学到。先打印class_to_idx确认标签映射再检查分类头nn.Linear(feat_dim, num_classes)里的feat_dim是否和骨干输出一致xxs/xs/s 分别是 320/384/640写错会静默出错或直接报维度错误。4.2 现象验证集准确率远低于训练集差距 20 个点以上过拟合小数据集上尤其常见。解决顺序先加数据增强水平翻转、小角度旋转、ColorJitter再上 weight_decay 和 dropout最后考虑减小模型尺寸——xs 过拟合就换 xxs。别一上来就加正则加到欠拟合我一般按这个顺序试。4.3 现象disgust 和 fear 两类召回率极低几乎全预测成 neutral类别不平衡的典型表现。检查WeightedRandomSampler有没有真正传进 DataLoader以及CrossEntropyLoss的weight参数有没有设。两个都设了还不行就去看这两类的样本是不是标注有问题表情数据集里 disgust 和 fear 的标注一致性本来就差必要时合并或重标。4.4 现象微调阶段 loss 突然爆炸变成 nan学习率太大或者解冻太激进。微调阶段学习率要压到 1e-5 到 1e-4别沿用冻结阶段的 1e-3。分层解冻时一次只解冻一个 stage解冻后先跑几个 epoch 观察 loss 再继续。另外检查输入 Normalize 是否和预训练一致均值方差对不上也会导致数值不稳。4.5 现象端侧推理延迟远高于预期常见原因是输入分辨率没降下来或者模型没做推理优化。表情识别 224 输入在 xxs 上够用别硬上 320。导出时用 ONNX 或 TorchScript开算子融合。还有一个隐蔽的坑测试延迟时忘了model.eval()和torch.no_grad()BN 层和梯度计算会白白吃掉大量时间。5. 进阶技巧用分层解冻 余弦退火把 xs 精度再抬两个点前面讲的冻结训练能快速拿到基线但想再往上抬精度分层解冻微调是性价比最高的手段。我的习惯是前 5 个 epoch 全冻骨干只训头第 6 个 epoch 开始解冻最后一个 stage第 11 个 epoch 解冻倒数第二个 stage学习率同步从 1e-3 降到 1e-4 再降到 1e-5配合余弦退火平滑过渡。这样骨干的浅层通用特征不被破坏深层任务相关特征逐步适配表情域。def unfreeze_stage(model, stage_name): # 按 stage 名解冻对应层其余保持冻结 for name, param in model.named_parameters(): if stage_name in name: param.requires_grad True # 训练循环里按 epoch 触发解冻 for epoch in range(total_epochs): if epoch 5: unfreeze_stage(model, layers.3) # 解冻最后一个 stage optimizer.param_groups[0][lr] 1e-4 elif epoch 10: unfreeze_stage(model, layers.2) optimizer.param_groups[0][lr] 1e-5 train_loss, train_acc train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step() # 早停判断 if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_xs.pth) patience_counter 0 else: patience_counter 1 if patience_counter 10: breakunfreeze_stage里的stage_name要和你用的 MobileViT 实现里的层命名对上不同开源实现的命名可能不一样先print([n for n, _ in model.named_parameters()])看一眼再写。解冻的同时手动调optimizer.param_groups[0][lr]因为新解冻的参数需要更小的学习率不然容易把预训练权重冲垮。早停的patience给 10表情任务验证集抖动大给太小会提前停。验证这套流程有没有效果别只看最终准确率画一条验证集准确率随 epoch 的曲线看解冻点前后有没有明显抬升。如果解冻后反而掉了说明学习率还是大再降一个数量级。我一般会存三个 checkpoint——冻结收敛点、第一次解冻后、第二次解冻后分别测端侧延迟挑精度和延迟平衡最好的那个上线。从那以后我每次做迁移学习都强制先跑冻结基线、再分层解冻、最后对比三个尺寸的延迟绝不跳过基线直接微调。这套流程帮我省了无数次返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表