尧图精选

基于Python+CNN的狗狗表情识别:从数据集标注到迁移学习实战

🕒 发布时间:2026/10/2 14:06:42 📁 来源:尧图网络
简介这份资源面向具备一定Python与深度学习基础、希望上手图像分类实战的开发者与学习者提供了一套基于PyTorch框架的CNN狗狗表情识别完整方案可用于课程设计、毕业项目或表情识别方向的入门练手。压缩包共906个文件以896张jpg与4张jpeg图片构成数据集主体另含3个py脚本与3个txt说明文件整体约80.35MB体积轻便便于本地运行。代码对数据集做了针对性预处理对短边补灰边使图片统一为正方形并通过旋转、翻转等方式扩增样本脚本依次完成数据集文本生成、模型训练与PyQt界面调用训练结束后模型会保存至本地方便后续推理与展示。目前已有114人学习关注适合想快速跑通一套从数据处理到可视化交互完整流程的读者参考借鉴。1. 狗狗表情识别到底在识别什么从一张歪头照说起你手机里肯定存过这种照片狗歪着头、耳朵后压、嘴巴微张看起来像在“笑”但同一只狗换个角度、换种光线同一套判断逻辑立刻翻车。基于 Python CNN 深度学习的狗狗表情识别要解决的就是把这种“人眼觉得像”的模糊判断变成模型可以稳定输出的分类结果。它适合两类人一类是手里已经有一批狗脸图片、想跑通一个完整深度学习项目的学生和初级算法工程师另一类是做过图像分类、但没系统处理过“动物面部”这种特殊目标的从业者。这个方向的门槛不在 CNN 本身而在于数据集的标注质量、狗脸对齐和类别定义——这三件事决定了你的模型是 85% 还是 55%。2. 先想清楚分类体系狗狗表情识别的标签怎么定才不返工2.1 为什么“开心/生气/害怕”这种标签最容易做废很多人拿到图片数据集第一反应是照搬人类表情的六分类高兴、悲伤、愤怒、恐惧、惊讶、厌恶。放到狗身上这套体系几乎必然出问题。狗的面部肌肉结构和人差异很大所谓“笑”更多是张嘴散热或安抚信号和人类的高兴不是一回事。更麻烦的是标注一致性同一张图A 标成“开心”B 标成“警觉”模型学到的就是噪声。我一般会建议把标签收敛到 3 到 4 类并且用可观察的行为特征来定义而不是用情绪词。比如标签名可观察特征易混淆点放松耳朵自然、嘴闭合或微张、眼神柔和与“专注”接近警觉耳朵前竖、头部抬起、目光直视与“紧张”接近紧张耳朵后压、身体压低、露出眼白与“攻击前兆”接近攻击龇牙、鼻梁皱起、耳朵前压样本少容易过拟合这样定义的好处是标注员不需要猜狗的心理只需要看耳朵、嘴、眼睛三个区域的状态。类别少了每类样本更容易凑够模型收敛也更快。常见做法是先做 3 类跑通之后再考虑加类。2.2 图片数据集的目录结构怎么组织标题里带了“含图片数据集”说明数据是随项目给的。但拿到手之后第一件事不是直接喂给模型而是检查目录结构。一个能直接跑通ImageFolder的结构应该是这样dataset/ train/ relaxed/ dog_001.jpg dog_002.jpg alert/ dog_101.jpg nervous/ dog_201.jpg val/ relaxed/ alert/ nervous/如果你的数据集是平铺的、或者按品种分文件夹就需要先写脚本重新划分。下面这段代码做两件事按 8:2 划分训练集和验证集并保证每个类别都按比例分配。import os import random import shutil SRC_DIR raw_images # 原始图片按类别分子文件夹 DST_DIR dataset # 输出目录 SPLIT_RATIO 0.8 # 训练集比例 random.seed(42) # 固定随机种子保证可复现 for cls in os.listdir(SRC_DIR): cls_path os.path.join(SRC_DIR, cls) if not os.path.isdir(cls_path): continue imgs [f for f in os.listdir(cls_path) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) cut int(len(imgs) * SPLIT_RATIO) train_imgs, val_imgs imgs[:cut], imgs[cut:] for subset, files in [(train, train_imgs), (val, val_imgs)]: out_dir os.path.join(DST_DIR, subset, cls) os.makedirs(out_dir, exist_okTrue) for f in files: shutil.copy(os.path.join(cls_path, f), os.path.join(out_dir, f)) print(f{cls}: train{len(train_imgs)}, val{len(val_imgs)})逻辑说明random.seed(42)是为了让每次划分结果一致方便复现和对比实验。SPLIT_RATIO设 0.8 是图像分类的常规起点如果某类样本少于 200 张建议调到 0.7给验证集留够样本。参数上唯一需要你改的是SRC_DIR和DST_DIR其余保持默认即可。跑完之后检查每个类别的 train/val 数量如果某一类验证集少于 20 张评估结果会非常不稳定。提示划分前先做一次去重。很多网络图片数据集里存在同一张图的不同尺寸副本重复样本会让验证集虚高。3. 用 CNN 把狗狗表情分类跑通从数据加载到第一次训练3.1 数据增强和 DataLoader 的关键参数狗脸图片的挑战在于姿态变化大侧脸、俯拍、遮挡都很常见。如果只做随机翻转模型很快会对角度过拟合。我一般用一套组合增强随机裁剪、水平翻转、小角度旋转、颜色抖动。注意不要用垂直翻转狗脸上下颠倒不是自然场景。from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf transforms.Compose([ transforms.Resize((224, 224)), # 统一尺寸匹配主干网络输入 transforms.RandomResizedCrop(224, scale(0.7, 1.0)), # 模拟不同距离 transforms.RandomHorizontalFlip(p0.5), # 水平翻转狗脸左右对称 transforms.RandomRotation(15), # 小角度旋转模拟歪头 transforms.ColorJitter(brightness0.3, contrast0.3), # 光照变化 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # ImageNet 统计量 ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) train_ds datasets.ImageFolder(dataset/train, transformtrain_tf) val_ds datasets.ImageFolder(dataset/val, transformval_tf) train_loader DataLoader(train_ds, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue) val_loader DataLoader(val_ds, batch_size32, shuffleFalse, num_workers4, pin_memoryTrue) print(train_ds.classes) # 确认类别顺序逻辑说明RandomResizedCrop的scale(0.7, 1.0)表示每次裁取原图 70% 到 100% 的区域这对狗脸这种主体占比不固定的数据很有效。Normalize用的均值和方差是 ImageNet 的统计量如果你从零训练而不是用预训练权重可以改成自己数据集的统计量但差异通常不大。batch_size32是 8GB 显存下的安全值显存不够就降到 16同时把学习率按比例调小。num_workers在 Windows 上如果报错改成 0 先跑通。3.2 一个够用的 CNN 结构不要一上来就 ResNet50标题强调的是 CNN 深度学习没有指定具体网络。我的建议是第一次跑通用一个 4 层卷积的自定义网络确认流程没问题之后再换预训练模型提精度。自定义网络的好处是训练快、显存占用低、每一层的输出你都能看懂。import torch import torch.nn as nn class DogExprCNN(nn.Module): def __init__(self, num_classes3): super().__init__() self.features nn.Sequential( # 输入 3x224x224 nn.Conv2d(3, 32, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 32x112x112 nn.Conv2d(32, 64, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 64x56x56 nn.Conv2d(64, 128, 3, padding1), nn.ReLU(), nn.MaxPool2d(2), # 128x28x28 nn.Conv2d(128, 256, 3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d(1) # 256x1x1 ) self.classifier nn.Sequential( nn.Flatten(), nn.Dropout(0.5), # 防止全连接层过拟合 nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model DogExprCNN(num_classes3) print(sum(p.numel() for p in model.parameters())) # 参数量约 40 万逻辑说明AdaptiveAvgPool2d(1)把任意空间尺寸压成 1x1这样你换输入尺寸时不用改全连接层。Dropout(0.5)在小数据集上是必要的如果训练集超过 5000 张可以降到 0.3。num_classes必须和train_ds.classes的长度一致写错会在损失计算时报维度错误。这个网络参数量约 40 万在 CPU 上也能跑适合先验证数据管道。3.3 训练循环里必须盯住的三个量训练代码本身不长但有三个量决定了你是真在进步还是在自我欺骗训练损失、验证损失、验证准确率。只看训练准确率是没有意义的小数据集上模型背答案太容易了。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) best_acc 0.0 for epoch in range(30): model.train() train_loss 0.0 for imgs, labels in train_loader: imgs, labels imgs.to(device), labels.to(device) optimizer.zero_grad() out model(imgs) loss criterion(out, labels) loss.backward() optimizer.step() train_loss loss.item() * imgs.size(0) train_loss / len(train_ds) model.eval() correct, total, val_loss 0, 0, 0.0 with torch.no_grad(): for imgs, labels in val_loader: imgs, labels imgs.to(device), labels.to(device) out model(imgs) val_loss criterion(out, labels).item() * imgs.size(0) correct (out.argmax(1) labels).sum().item() total labels.size(0) val_loss / len(val_ds) val_acc correct / total print(fEpoch {epoch1:02d} | train_loss{train_loss:.4f} f| val_loss{val_loss:.4f} | val_acc{val_acc:.4f}) if val_acc best_acc: best_acc val_acc torch.save(model.state_dict(), best_dog_expr.pth)逻辑说明weight_decay1e-4是 Adam 的常用正则化强度数据量小可以加到 1e-3。每个 epoch 结束后打印三个量重点看验证损失有没有在训练损失下降时反而上升——那是过拟合的明确信号。保存策略用“验证准确率最高时保存”而不是最后一个 epoch因为后面很可能已经过拟合了。lr1e-3对自定义网络合适如果你换成预训练 ResNet要降到 1e-4 甚至更低否则预训练权重会被冲掉。4. 狗狗表情识别的避坑与排查那些让准确率卡住的真实原因4.1 验证准确率远高于实际表现现象验证集准确率到 90%但拿新图片测试一塌糊涂。原因通常是验证集和训练集来自同一批图片的相邻帧或者同一只狗同时出现在两个集合里。解决划分数据时按“个体”划分同一只狗的所有照片只进一个集合。如果数据里没有个体 ID至少按拍摄场景或来源文件夹做分组划分。4.2 某一类永远预测不对现象三类里总有一类的召回率接近 0。原因有两个常见方向一是该类样本太少模型直接放弃二是该类和其他类在视觉上确实难分比如“警觉”和“紧张”都以耳朵变化为主。解决先打印混淆矩阵确认是哪两类在互相误判然后要么合并这两类要么对该类做过采样。过采样用WeightedRandomSampler比直接复制图片更干净。4.3 训练损失不下降现象跑了 10 个 epoch损失几乎不动。原因可能是学习率太小、数据归一化没做、或者标签和图片没对齐。解决先拿 20 张图做一次过拟合测试如果模型连 20 张都记不住说明网络或数据管道有问题。检查ImageFolder的classes顺序和你的标签映射是否一致这个错我见过太多次。4.4 显存溢出但 batch_size 已经很小现象batch_size 降到 8 还是 OOM。原因通常是图片分辨率太高或者num_workers开太多导致内存复制堆积。解决先把输入降到 128x128 跑通确认流程后再升到 224。另外pin_memoryTrue在显存紧张时可以先关掉。4.5 数据增强把关键特征抹掉了现象加了增强之后准确率反而下降。原因是某些增强和任务冲突比如狗脸识别里用大角度旋转会把耳朵和眼睛的空间关系破坏掉。解决增强强度从小往大调每次只加一种观察验证准确率变化。RandomRotation(15)是安全值超过 30 就要谨慎。5. 把准确率再抬一截迁移学习和狗脸对齐的实战技巧自定义 CNN 跑通之后如果你想把验证准确率从 70% 推到 85% 以上最有效的两步是迁移学习和狗脸对齐。迁移学习不用多解释用 ImageNet 预训练的 ResNet18 或 EfficientNet-B0把最后一层换成你的类别数前几层冻结、只训练分类头跑 10 个 epoch 通常就能超过从零训练 30 个 epoch 的结果。关键是学习率要小我一般用 1e-4 起步配合余弦退火。狗脸对齐是很多人忽略的一步。狗的脸型和人不同用人类的人脸检测器经常框不准。常见做法是用一个轻量的动物面部检测模型先裁出狗脸区域再送进分类网络。如果没有检测模型至少做一次中心裁剪加 padding让狗脸尽量居中。这一步在姿态变化大的数据集上能带来 5 到 10 个百分点的提升。验证方法上不要只看总体准确率。打印混淆矩阵看每一类的召回率如果某一类召回率低于 60%优先补该类样本而不是调网络结构。另外做一个 5 折交叉验证确认你的准确率不是某一次划分的运气。最后把模型在真实场景下的失败案例单独存一个文件夹每隔一段时间看一次比盯着准确率数字有用得多。我自己踩过的最大坑是花了两天调网络结构最后发现是验证集里混进了训练集的重复图片。从那以后我拿到任何图片数据集第一件事永远是去重和检查划分。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →