基于Python-CNN的狗狗表情识别:从数据集到PyQt界面全流程实战
简介这份资源面向希望入门或实践计算机视觉的Python开发者与深度学习学习者提供一套基于PyTorch框架的狗狗表情识别完整代码方案可用于课程设计、毕业项目或算法练手。压缩包共906个文件以896张jpg与4张jpeg表情图片构成数据集主体另含3个txt说明文本和3个py脚本整体约80.35MB目录结构清晰便于按类别检索与复现。代码对数据集做了针对性预处理对短边补灰边使图片变为正方形并通过旋转、翻转等方式扩增样本再依次运行数据集文本生成、模型训练与PyQt界面脚本即可完成从读取图片路径与标签、训练并保存本地模型到可视化交互的完整链路。已有114人学习关注适合想快速跑通CNN图像分类流程、理解数据增强与界面部署的读者参考借鉴。1. 狗狗表情识别项目拆解从数据集到 PyQt 界面的完整落地路径拿到一个「基于 Python-CNN 深度学习的狗狗表情识别」压缩包第一反应往往不是兴奋而是怀疑——数据集里那些带_rotated45、_flip后缀的图片到底是怎么来的训练脚本跑完模型存哪了PyQt 界面能不能直接双击运行这个项目本质上是一套完整的图像分类流水线用 PyTorch 搭建 CNN对狗狗的多种表情做监督学习最后套一个桌面端 UI 做推理演示。它适合刚学完卷积神经网络、想找一个能跑通全流程的深度学习项目练手的人也适合需要快速搭一个图像分类 Demo 交差的开发者。压缩包里已经带了图片数据集省去了自己爬图、清洗的麻烦但环境配置和路径处理这两关才是真正决定你能不能跑起来的关键。2. 环境搭建与依赖安装requirement.txt 里没写清楚的几件事2.1 为什么 PyTorch 版本比 Python 版本更致命项目正文里提到「环境需要自行配置」并给了一篇 CSDN 博文链接作为参考。但很多人会忽略一个事实CNN 训练代码能不能跑第一道坎不是 Python 版本而是 PyTorch 和 torchvision 的版本匹配。如果你用pip install torch直接装最新版很可能遇到torchvision的transforms接口变动或者 CUDA 版本和显卡驱动对不上。常见做法是先用conda创建一个独立环境再根据显卡情况选择安装命令。没有 NVIDIA 显卡的机器直接装 CPU 版即可训练速度慢但代码逻辑完全一致。# 创建独立环境Python 3.8 是比较稳的选择 conda create -n dog_expression python3.8 conda activate dog_expression # CPU 版安装没有独显就用这个 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 如果有 CUDA 11.8 的显卡用下面这行 # pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118这里的关键参数是--index-url它决定了你去哪个源下载对应版本的 wheel 包。很多人直接pip install torch走默认源结果装了一个和 CUDA 不匹配的版本训练时torch.cuda.is_available()返回False代码不会报错但全程跑在 CPU 上速度差几十倍。2.2 requirement.txt 之外还需要补什么项目正文说requirement.txt里介绍了如何安装环境但根据这类项目的常见构成除了torch、torchvision通常还需要numpy、Pillow、PyQt5、matplotlib这几个包。numpy和Pillow用于图片读取和数组转换PyQt5是界面框架matplotlib可能在训练脚本里用来画 loss 曲线。# 一次性补齐常用依赖 pip install numpy Pillow PyQt5 matplotlib tqdmtqdm不是必须的但训练脚本里如果有进度条体验会好很多。如果你不确定某个包缺不缺最直接的办法是先跑01数据集文本生成制作.py报ModuleNotFoundError就装对应的包。这种「报错驱动」的安装方式虽然不够优雅但对新手来说最不容易漏。提示不要在一个已经装了太多包的 base 环境里折腾依赖冲突会让你怀疑人生。独立环境是后悔药。3. 数据集预处理与文本生成01 脚本到底干了什么3.1 图片增强策略灰边填充和旋转翻转的工程意义从项目正文给出的文件名可以看出数据集里除了原图还有_rotated45和_flip后缀的图片。这说明项目在预处理阶段做了两类增强旋转 45 度和水平翻转。另外正文提到「通过在较短边增加灰边使得图片变为正方形」这是一个很实用的工程细节。为什么非要变成正方形因为 CNN 的输入层通常要求固定尺寸比如 224×224。如果直接把长方形图片 resize 成正方形物体会被拉伸变形狗狗的嘴巴、耳朵比例失真模型学到的特征就不准了。灰边填充的做法是以较长边为基准在较短边两侧补灰边这样图片内容不变形只是多了无信息的边缘区域。from PIL import Image, ImageOps def pad_to_square(img_path, fill_color(128, 128, 128)): 将图片以灰边填充为正方形不拉伸内容 img Image.open(img_path).convert(RGB) w, h img.size max_side max(w, h) # 计算左右或上下需要填充的像素数 padding ( (max_side - w) // 2, # left (max_side - h) // 2, # top (max_side - w 1) // 2, # right (max_side - h 1) // 2 # bottom ) img_padded ImageOps.expand(img, borderpadding, fillfill_color) return img_padded这段代码的核心逻辑是ImageOps.expand它接受一个border元组和填充颜色。fill_color选 128 的灰色是因为它在归一化后接近 0不会给模型引入额外的强信号。如果你用纯黑或纯白边缘区域在卷积核看来可能变成一种「伪特征」反而干扰训练。3.2 01 脚本如何生成训练和验证的 txt 清单01数据集文本生成制作.py的任务是遍历数据集文件夹读取每个类别子文件夹下的图片路径并分配标签最后写成 txt 文件。通常输出两个文件train.txt和val.txt每行格式是图片路径 标签。import os import random def generate_txt(data_root, output_dir, val_ratio0.2): 遍历类别文件夹生成 train.txt 和 val.txt classes sorted(os.listdir(data_root)) all_samples [] for label, cls_name in enumerate(classes): cls_dir os.path.join(data_root, cls_name) if not os.path.isdir(cls_dir): continue for fname in os.listdir(cls_dir): if fname.lower().endswith((.jpg, .jpeg, .png)): all_samples.append((os.path.join(cls_dir, fname), label)) random.shuffle(all_samples) split_idx int(len(all_samples) * (1 - val_ratio)) train_samples all_samples[:split_idx] val_samples all_samples[split_idx:] # 写入文件 for name, samples in [(train.txt, train_samples), (val.txt, val_samples)]: with open(os.path.join(output_dir, name), w, encodingutf-8) as f: for path, label in samples: f.write(f{path} {label}\n)val_ratio0.2表示 20% 的数据用于验证。这个比例不是固定的如果数据集本身很小可以降到 0.1 或 0.15保证训练集有足够样本。random.shuffle之前一定要确保all_samples已经收集完毕否则每个类别的样本顺序会影响划分的随机性。标签从 0 开始递增对应classes列表的索引后面推理时要用同样的顺序映射回类别名。注意如果你在 Windows 上运行路径里的反斜杠\在写入 txt 后可能被后续读取脚本误解析。建议在写入前用path.replace(\\, /)统一成正斜杠。4. CNN 模型训练与保存02 脚本的参数与排错4.1 模型结构选型自己搭还是用预训练项目标题写的是「CNN 深度学习」没有明确说是自定义网络还是迁移学习。从这类教学项目的惯例来看大概率是一个简单的卷积网络几层Conv2dMaxPool2dReLU最后接Linear分类头。这种结构参数量小训练快适合理解 CNN 的基本流程。但如果你想让准确率好看一点可以把 backbone 换成resnet18或mobilenet_v2用torchvision.models加载预训练权重只训练最后的全连接层。代价是训练时间变长显存占用增加。import torch.nn as nn from torchvision import models def build_model(num_classes, use_pretrainedFalse): 构建 CNN 模型可选预训练 backbone if use_pretrained: model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) # 冻结前面的卷积层只训练分类头 for param in model.parameters(): param.requires_grad False model.fc nn.Linear(model.fc.in_features, num_classes) else: model nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(), nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(128, num_classes) ) return modelAdaptiveAvgPool2d((1, 1))是一个很实用的层它把任意尺寸的特征图压缩成 1×1这样就不用担心输入图片尺寸变化导致全连接层维度不匹配。use_pretrained参数控制是否走迁移学习路线新手建议先用False跑通流程再尝试True看效果差异。4.2 训练循环里的三个关键参数02深度学习模型训练.py的核心是一个标准的训练循环前向传播、计算 loss、反向传播、更新参数。这里面有三个参数直接决定模型能不能收敛参数常见取值作用调参建议学习率 lr0.001 / 0.0001控制参数更新步长太大震荡不收敛太小收敛慢batch_size16 / 32 / 64每次送入模型的样本数显存不够就调小太小梯度不稳epochs20 / 50 / 100训练轮数观察验证集准确率不再上升就停import torch from torch.utils.data import DataLoader, Dataset from PIL import Image from torchvision import transforms class DogExpressionDataset(Dataset): def __init__(self, txt_path, transformNone): self.samples [] with open(txt_path, r, encodingutf-8) as f: for line in f: path, label line.strip().rsplit( , 1) self.samples.append((path, int(label))) self.transform transform def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label self.samples[idx] img Image.open(path).convert(RGB) if self.transform: img self.transform(img) return img, label # 定义预处理训练集加随机增强验证集只做 resize 和归一化 train_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize里的均值和标准差是 ImageNet 的统计值用预训练模型时必须保持一致。如果你从零训练也可以用自己数据集的均值和方差但差别不会太大。RandomHorizontalFlip是训练时的随机增强验证集不要加否则每次验证结果都在变没法比较。训练完成后脚本通常会用torch.save(model.state_dict(), model.pth)保存权重。注意保存的是state_dict而不是整个模型对象这样加载时更灵活不会因为代码结构变动而失败。4.3 训练不收敛时的排查顺序遇到 loss 不下降或者准确率卡在随机水平按这个顺序查先看数据标签有没有对错用train.txt里的路径随机抽几张图打开确认再看学习率是不是太大把 lr 降到 0.0001 试几轮然后检查输入图片的归一化是否和模型预期一致最后看模型是不是太简单欠拟合了加一层卷积或换预训练 backbone。血泪经验是大部分「模型不学习」的问题根源都在数据上而不是网络结构。5. PyQt 界面集成与推理03 脚本的坑与避坑指南5.1 界面加载模型时最容易翻车的三个点03pyqt_ui界面.py的任务是提供一个图形界面让用户选一张图片点击按钮后显示预测结果。这个脚本本身不复杂但把训练好的模型集成进去时有三个高频翻车点。第一个是类别映射丢失。训练时classes sorted(os.listdir(data_root))得到的顺序和推理时界面里显示的类别名必须完全一致。如果你在 03 脚本里重新os.listdir一次而文件夹顺序变了预测结果就会张冠李戴。稳妥做法是在 01 脚本生成 txt 时顺便把classes列表存成一个classes.txt03 脚本直接读取。第二个是图片预处理不一致。训练时用了Resize((224, 224))和Normalize推理时也必须走同样的transforms。很多人只在推理时做了Resize忘了Normalize导致输入分布偏移预测结果乱七八糟。第三个是模型加载时的map_location。如果你在 GPU 上训练保存的权重默认带 CUDA 信息在 CPU 机器上加载会报错。加载时加map_locationcpu就能解决。import torch from torchvision import transforms from PIL import Image def predict(image_path, model_path, classes_path, devicecpu): 加载模型并对单张图片做推理 with open(classes_path, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] model build_model(num_classeslen(classes), use_pretrainedFalse) # map_location 保证 GPU 训练的权重能在 CPU 上加载 model.load_state_dict(torch.load(model_path, map_locationdevice)) model.eval() tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) tensor tf(img).unsqueeze(0) # 增加 batch 维度 with torch.no_grad(): output model(tensor) pred_idx output.argmax(dim1).item() return classes[pred_idx]unsqueeze(0)是把单张图片的[C, H, W]变成[1, C, H, W]因为模型期望的输入是带 batch 维度的。torch.no_grad()关闭梯度计算推理时省显存也提速。5.2 避坑与常见问题排查现象一运行 01 脚本报FileNotFoundError提示找不到数据集文件夹。原因通常是脚本里的data_root写的是绝对路径换一台机器就失效了。解决方法是改成相对路径比如data_root ./dataset并确保脚本的工作目录和数据集文件夹在同一级。现象二训练时 loss 一直是nan。原因可能是学习率太大或者输入数据里有损坏的图片导致除零。先把 lr 降到 0.0001如果还不行在 Dataset 的__getitem__里加 try-except跳过无法打开的图片并打印出问题文件路径。现象三PyQt 界面能打开但点击按钮后程序卡死。原因是推理过程在主线程里执行模型加载和图片处理耗时较长界面无法响应。常见做法是把推理逻辑放到QThread里或者至少在点击后先QApplication.processEvents()刷新界面再执行推理。现象四预测结果永远是同一个类别。先检查classes.txt的顺序和训练时是否一致再确认推理时的transforms和训练时是否完全相同。如果都没问题可能是模型欠拟合回到 02 脚本增加 epochs 或换预训练模型。现象五在 Windows 上路径包含中文或空格导致Image.open失败。PIL对中文路径的支持取决于版本稳妥做法是在读取前用path.encode(utf-8).decode(utf-8)处理或者直接把数据集放在纯英文路径下。这个问题在 Linux 上很少见但 Windows 用户经常踩。提示每次修改 01 脚本的类别顺序后一定要重新生成classes.txt和 txt 清单否则 03 脚本的预测结果会全部错位。6. 进阶技巧用混淆矩阵和单图测试验证模型真实水平训练脚本打印的准确率只是一个整体数字它掩盖了类别不平衡的问题。比如狗狗的「开心」表情样本有 200 张「生气」只有 50 张模型全预测成「开心」也能拿到 80% 的准确率但实际毫无用处。验证模型真实水平最直接的方法是画混淆矩阵。import numpy as np import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay def plot_confusion(model, val_loader, classes, devicecpu): 在验证集上跑一遍画混淆矩阵 model.eval() all_preds, all_labels [], [] with torch.no_grad(): for imgs, labels in val_loader: imgs imgs.to(device) outputs model(imgs) preds outputs.argmax(dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) disp ConfusionMatrixDisplay(confusion_matrixcm, display_labelsclasses) disp.plot(cmapBlues, xticks_rotation45) plt.title(Dog Expression Confusion Matrix) plt.tight_layout() plt.savefig(confusion_matrix.png, dpi150) plt.show()confusion_matrix的横轴是预测标签纵轴是真实标签对角线上的数字越大越好。如果某个类别的样本大量跑到另一类说明这两类的特征在模型看来很相似可能需要增加更多区分性强的训练数据或者调整数据增强策略。另一个实用技巧是单图测试。从验证集里挑几张模型预测错误的图片单独跑一遍推理把原始图片和预测概率一起打印出来。如果概率分布很平均比如 0.3、0.25、0.2说明模型对这张图本身就没把握如果某一类概率极高但预测错了那可能是标签标错了。def inspect_single(image_path, model, classes, devicecpu): 打印单张图片的各类别概率 tf transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) img Image.open(image_path).convert(RGB) tensor tf(img).unsqueeze(0).to(device) model.eval() with torch.no_grad(): probs torch.softmax(model(tensor), dim1).squeeze().cpu().numpy() for cls, p in sorted(zip(classes, probs), keylambda x: -x[1]): print(f{cls}: {p:.4f})我一般会在训练结束后先跑混淆矩阵看整体分布再挑 5 到 10 张错分图片做单图检查。这个习惯帮我发现过好几次标签写反的问题——数据集里「开心」和「张嘴」两个文件夹的图片混了模型学得再久也没用。从那以后我每次拿到新数据集都强制先抽检每个类别的前 20 张图确认标签和内容对得上再开始训练。希望这个流程能帮你少走一段弯路。本文还有配套的精品资源点击获取