从zip数据集到CNN猫行为识别:训练调参与避坑指南
发布时间:2026/10/1 3:03:31来源:尧图网络
简介这是一套基于PyTorch的CNN猫行为识别项目内含完整代码与配套图片数据集适合深度学习初学者或图像分类实践者参考。项目通过卷积网络对猫的多种行为进行分类训练流程清晰且针对图片进行预处理包括在较短边增加灰边统一为正方形原正方形图片则不处理并旋转角度扩增数据集有助于提升模型泛化能力。压缩包共544个文件大小约41.35MB主要包含538张jpg样本图片、3个Py脚本及3个txt说明文件其中Py脚本分别对应数据集生成、模型训练与PyQt界面展示txt文件记录了类别图片路径和标签按序运行即可完成从数据准备到训练再到界面交互的完整流程。目前已有102人学习适合需要快速搭建图像识别实验并观察数据增强效果的读者下载使用。1. 一个 zip 压缩包凭什么让猫行为识别从玄学变成工程做深度学习项目的人多半经历过这种尴尬模型结构抄来了训练脚本也改好了最后卡在数据上——标注不规范、类别不均衡、图片尺寸乱七八糟跑出来的准确率还没抛硬币靠谱。这个标题里的“含图片数据集.zip”才是真正的核心资产猫行为识别这个任务本身反而不是最难的部分。CNN 做图像分类已经是成熟套路但把“抓拍到的猫片”变成“能用的训练集”这件事决定了项目是 3 天跑通还是 3 周烂尾。这篇笔记面向两类人一是刚接触 CNN、想找个完整练手项目的初学者二是已经跑过分类任务、但被数据集质量折磨过的工程师。我会从 zip 包怎么解、数据怎么清洗开始到 CNN 的配置、训练参数怎么调再到最常见的翻车点和对应排查手段。这个项目能解决的问题很具体用普通摄像头拍到的猫图片区分出睡觉、进食、玩耍、梳理毛发等行为状态为宠物监护、行为分析提供分类结果。目标不是 SOTA而是用最小成本把流程跑通、把坑踩平。2. 先跟 zip 包过招解压、清数据和标签组织2.1 解压前先验货文件损坏与伪加密的识别拿到“cat_behavior_dataset.zip”这类压缩包第一步不是双击解压而是先验证完整性。很多下载中断的 zip 文件解压到一半报 CRC 错误你以为是代码问题其实是压缩包本身缺了字节。我一般用命令行工具做校验Windows 和 Linux 下都有对应方案。# Linux / macOS 下检查 zip 完整性 unzip -t cat_behavior_dataset.zip # 输出末尾出现 No errors detected 才说明文件完整 # Windows PowerShell 下可用 .NET 的 ZipFile 类做验证 Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::OpenRead(C:\data\cat_behavior_dataset.zip).Entries.Count # 能正常打开且文件数不为 0说明至少没有结构性问题unzip -t是全量校验会逐个文件计算 CRC32 并与压缩包内记录比对文件数多时可能要等几分钟。如果报错信息里有bad CRC或者invalid compressed data别犹豫重新下载或找来源方要完整包。还有一种隐蔽问题是 zip 伪加密。有些打包工具会篡改压缩包的加密标志位让unzip误以为文件有密码实际上数据本身没加密。用zipinfo -v查看压缩包的加密标志如果显示encryption: none但解压时又提示输入密码基本就是伪加密。处理方式是修正标志位或用 7-Zip 的“保留损坏文件”模式强制解压但最省事的做法是找未加密的原包。2.2 数据集的目录结构怎么摆训练集、验证集、测试集三分法CNN 训练最怕数据集组织混乱。常见的烂摊子是所有图片堆在一个文件夹里标注靠文件名后缀没有划分脚本。这个项目的数据集大概率是按行为类别分目录的但为了后续能稳定复现我会先把它整理成标准结构dataset/ ├── train/ │ ├── sleeping/ # 睡觉行为 │ ├── eating/ # 进食行为 │ ├── playing/ # 玩耍行为 │ └── grooming/ # 梳理毛发 ├── val/ │ └── 与 train 相同的子目录 └── test/ └── 与 train 相同的子目录在动手拆分之前先统计每类的图片数量。直接用find命令按类别计数再写个 Python 脚本做划分。这比手动拖拽文件夹更可控随机种子固定下来后续复现实验结果不用重分一次相当于给训练过程买了份“后悔药”。import os import random import shutil random.seed(42) # 固定随机种子保证可复现 source_dir cat_behavior_dataset # 原始解压目录 target_dir dataset split_ratio (0.7, 0.15, 0.15) # train / val / test 比例 for behavior in os.listdir(source_dir): behavior_path os.path.join(source_dir, behavior) if not os.path.isdir(behavior_path): continue images [f for f in os.listdir(behavior_path) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(images) train_cut int(len(images) * split_ratio[0]) val_cut train_cut int(len(images) * split_ratio[1]) for split_name, subset in zip( [train, val, test], [images[:train_cut], images[train_cut:val_cut], images[val_cut:]] ): dest_dir os.path.join(target_dir, split_name, behavior) os.makedirs(dest_dir, exist_okTrue) for img in subset: shutil.copy( os.path.join(behavior_path, img), os.path.join(dest_dir, img) )这段脚本的核心是把“按类别划分数据”从一次性手工操作变成可复用的工程步骤。random.seed(42)这行不能省没有它每次运行划分结果都不同模型对比实验就失去了基准。比例上我用 7:1.5:1.5 而不是常见的 8:1:1因为猫行为数据集的类别内差异很大给验证集多留一点空间调参时看到的效果更接近真实泛化水平。2.3 脏数据清洗模糊、重复和标注错误是三大杀手解压完、分完目录别急着进模型。真实世界的数据集必然混着垃圾。我通常会写一个简单的清洗脚本先用 OpenCV 检查三件事图片能否正常解码、是否模糊、是否重复。import cv2 import hashlib import os bad_images [] dup_hashes {} blur_threshold 100.0 # Laplacian 方差低于该值视为模糊 for root, _, files in os.walk(dataset/train): for fname in files: path os.path.join(root, fname) img cv2.imread(path) if img is None: bad_images.append(path) # 损坏文件解码失败 continue gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) laplacian_var cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var blur_threshold: bad_images.append((path, blur, laplacian_var)) # 用 MD5 检查内容级重复 file_hash hashlib.md5(open(path, rb).read()).hexdigest() dup_hashes.setdefault(file_hash, []).append(path) # 打印疑似问题文件人工确认后再删除 for img_info in bad_images: print(BAD:, img_info) for h, paths in dup_hashes.items(): if len(paths) 1: print(DUP:, paths)其他地方都好说这个canny边缘检测的替代方案——Laplacian 方差——需要特别解释。它衡量的是图片的梯度变化强度猫在睡觉时拍摄的画面通常主体静止但背景有轻微噪声方差值不会太低而真正模糊的图片失焦、运动模糊方差会显著偏小。阈值 100 是经验值如果发现误删了正常的暗光图片就调低到 50 左右如果还有明显糊图漏网就往上调到 150。重复图片是隐蔽问题。同一只猫同一个姿势拍几十张放进训练集后模型会对这个场景过拟合。MD5 哈希查重只能识别完全相同的图片如果数据集里有轻微压缩变化的近似重复可以用感知哈希pHash进一步查重但先跑 MD5 就够解决大部分问题了。3. 从零搭 CNN网络结构怎么选、参数怎么设3.1 CNN 基本结构卷积、池化、全连接三层怎么配合新手最常问“CNN 到底在学什么”。一句话解释卷积层在找局部特征猫的耳朵轮廓、饭碗的圆形边缘池化层在压缩特征图尺寸、保留主要信息全连接层把前面提到的特征组合成“睡觉/进食/玩耍/梳理”这样的类别得分。这个项目的输入是普通 RGB 猫照片特征不算复杂不需要上 ResNet 这种上百层的深度网络。适合猫行为识别的结构一般是“3 个卷积块 2 层全连接”第一个卷积块输出通道 32卷积核 3×3池化后特征图缩小一半第二个卷积块输出通道 64卷积核 3×3池化后继续缩小第三个卷积块输出通道 128卷积核 3×3池化后得到 4×4×128 的特征全连接层先展平经过 256 维的中间层最后输出 4 类得分为什么用 3×3 卷积核而不是 5×5两个 3×3 卷积堆叠的感受野等于一个 5×5但参数量少一半18 个对 25 个非线性还多了一层拟合能力更强。这是现代 CNN 的通行做法这个小项目也直接受益。3.2 在 PyTorch 里跑通最小模型训练脚本 参数说明用 PyTorch 实现上述结构代码量很少。下面这个脚本可以直接跑数据加载部分用torchvision.datasets.ImageFolder读取我们刚才整理好的目录结构。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import DataLoader from torchvision import datasets, transforms, models # 数据增强 归一化归一化均值/方差按 ImageNet 默认值 data_transforms { train: transforms.Compose([ transforms.Resize((128, 128)), # 统一尺寸降低显存压力 transforms.RandomHorizontalFlip(p0.5), # 随机水平翻转扩充样本 transforms.ColorJitter(brightness0.2, contrast0.2), # 颜色扰动 transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]), val: transforms.Compose([ transforms.Resize((128, 128)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]), } class CatBehaviorCNN(nn.Module): 3 个卷积块 2 层全连接的轻量 CNN def __init__(self, num_classes4): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.BatchNorm2d(32), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size3, padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(128 * 16 * 16, 256), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), # 抑制过拟合 nn.Linear(256, num_classes) ) def forward(self, x): return self.classifier(self.features(x)) model CatBehaviorCNN(num_classes4) criterion nn.CrossEntropyLoss() # 多分类标准损失 optimizer optim.Adam(model.parameters(), lr1e-3) # 初始学习率 # 数据加载train/val 目录直接映射到数据集 train_dataset datasets.ImageFolder(dataset/train, data_transforms[train]) val_dataset datasets.ImageFolder(dataset/val, data_transforms[val]) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size32, shuffleFalse, num_workers4) # 训练循环简化版只跑 3 个 epoch 验证流程是否通 for epoch in range(3): model.train() running_loss 0.0 for images, labels in train_loader: optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) epoch_loss running_loss / len(train_dataset) print(fEpoch {epoch1} Loss: {epoch_loss:.4f})这里有几个参数值得琢磨。BatchNorm2d放在卷积和激活之间能让每层输入分布稳定训练收敛快得多尤其适合小数据集不加它你很可能看到 loss 震荡得让人怀疑人生。Dropout(p0.5)是全连接层后的标准防过拟合手段训练时随机丢弃一半神经元推理时自动不生效这是 PyTorch 的默认行为不需要手动切换。Resize((128, 128))是一个折中选择。原图可能是 800×600 甚至更大。直接全尺寸输入显存会爆压得太小比如 64×64猫的胡须、饭碗边缘这些细节全丢了。128×128 对轻量 CNN 来说信息量足够训练速度也友好。你的显卡如果是老型号可以再降到 96×96准确率下降通常不超过 2%。3.3 与其从零训练不如尝尝迁移学习的甜头自己搭 CNN 从零训练在几千张图的小数据集上很容易陷入过拟合——训练集准确率 95%验证集只有 70%。常见做法是先用 ImageNet 预训练好的 ResNet18把最后一层全连接换掉只微调后面的层效果会好很多。# 用预训练 ResNet18 做特征提取器替换分类头 from torchvision import models model models.resnet18(weightsmodels.ResNet18_Weights.IMAGENET1K_V1) num_features model.fc.in_features model.fc nn.Linear(num_features, 4) # 换成 4 类输出 # 冻结除最后一层外的所有参数只训练分类头 for param in model.parameters(): param.requires_grad False for param in model.fc.parameters(): param.requires_grad True optimizer optim.Adam(model.fc.parameters(), lr1e-3)迁移学习的逻辑在于ImageNet 上学到的边缘、纹理、形状特征对猫照片同样有效。你要做的只是在那些通用特征之上“重新组合”出行为类别。这比从零训练省了大概 10 倍的收敛时间。数据量越少迁移学习的优势越明显。如果只训练分类头准确率还是不够就解冻后面几个残差块用更小的学习率1e-4微调。这种“分层微调”是工程上最稳妥的路线既避免全量微调带来的灾难性遗忘又比只训头部更有上限。4. 让模型真的能干活训练策略、数据增强和玄学调参4.1 数据增强怎么做才不过火很多人一上来就把旋转、裁剪、缩放、颜色抖动全加上结果模型在验证集上反而更差。要理解这些增强手段的适用前提。猫行为识别里“睡觉”和“玩耍”的姿势差异巨大但“进食”和“梳理毛发”在某些照片上可能看起来相似这时过火的颜色抖动可能让模型把注意力放在错误的地方。我建议按优先级排序添加增强水平翻转猫照片没有左右语义差异安全有效闭眼加轻微旋转±15°模拟摄像头安装角度误差要加随机裁剪scale 0.8~1.0模拟不同拍摄距离可加颜色扰动只在类别颜色确实有差异时用谨慎data_transforms { train: transforms.Compose([ transforms.Resize(160), transforms.RandomRotation(degrees15), transforms.RandomResizedCrop(128, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(p0.5), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) }注意这个 Resize(160) 配合 RandomResizedCrop(128)相当于先放大后随机裁剪比直接 Resize(128) 效果好。原因很简单它给模型提供了多尺度的视角同一张图在不同 epoch 里看到的可能是猫头特写也可能是全身视角。这个细节是血泪经验换来的。4.2 学习率和 batch size两个必调参数训练 CNN 时最常被问“准确率上不去怎么办”十有八九是学习率设置不对。一个朴素但有效的经验法则batch size 翻倍学习率也翻倍。原因是梯度噪声随 batch 增大而降低需要更大的步长来保持同样的更新幅度。# batch_size64 时建议把学习率从 1e-3 提到 2e-3 optimizer optim.Adam(model.parameters(), lr2e-3)另外别用一个固定学习率跑到底。我习惯用 PyTorch 的ReduceLROnPlateau或者手写一个简单的分段衰减前 5 个 epoch 用初始学习率摸清走势loss 平台期到了就砍一半。scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) # 每个 epoch 结束时基于验证集 loss 调整 # scheduler.step(val_loss)观察训练日志时有一个反直觉现象训练集 loss 还在下降但验证集 loss 开始回升这就是过拟合的准确信号。早停法是有效手段也可以用上面这个 scheduler 让学习率自动降下来给模型一个“跳出局部最优”的机会。Adam 已经内置了自适应学习率但在小数据集上配合 scheduler 仍然管用。4.3 类别不均衡睡觉的猫太多玩耍的猫太少梳理毛发这个行为可能只占数据集的 5%而睡觉占了 40%。如果你的损失函数没有特殊处理模型学到的最优策略是“把所有图片都判成睡觉”因为这样做整体准确率也有 40%。这在行为识别场景里完全是废品。常见做法有两种一是对 loss 做类别加权二是过采样少数类。# 方式一按样本量倒数比例设置权重 from sklearn.utils.class_weight import compute_class_weight labels [] # 收集所有训练集的标签 class_weights compute_class_weight( class_weightbalanced, classesnp.unique(labels), ylabels ) class_weights torch.tensor(class_weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightclass_weights)compute_class_weight背后的逻辑是样本数越少的类别给它设越大的 loss 权重让模型“更努力”地学会它。这个方案简单且无需改动数据。过采样则是在 DataLoader 层面让少数类的图片每个 epoch 被多抽几次两者可以一起用但不建议同时上先把加权 loss 试起来看混淆矩阵再决定是否加过采样。5. 避坑与排查从 zip 伪加密到 GPU 显存爆炸的 6 条踩坑记录5.1 zip 文件解压报错伪加密标志导致“明明没密码却要密码”现象双击 zip 弹出输入密码框但数据来源方声称未加密用unzip命令解压也提示需要密码。原因压缩包生产工具在写入本地文件头时把加密标志位错误地置为 1即 zip 伪加密。文件本身没有真正的 AES 或 ZipCrypto 加密数据只是标志位骗过了大多数解压器。解决用 7-Zip 打开时通常能直接看到文件列表选择所有文件执行“复制到”指定目录绕过标准解压流程或使用 Python 的zipfile模块显式检查文件的flag_bits。zipfile.ZipFile在读取open()时如果检测到伪加密会抛RuntimeError: File is encrypted但底层_ZipDecrypter并不会用于未真正加密的数据可以手动改标志位后再解压。最省心的做法还是回源找未加密的重新打包版本。5.2 图片解码失败OpenCV 能读PIL 报错训练中断现象训练到一半torchvision报Image file is truncated或OSError: cannot write mode P as JPEG但单独用 OpenCVimread又是好的。原因部分数据集里的 JPEG 文件是渐进式编码或带有非标准 EXIF 信息。Pillow 对 JPEG 的容错比 OpenCV 差遇到截断的 JPEG 尾部直接抛异常。解决给ImageFile类加载前加一行from PIL import ImageFile; ImageFile.LOAD_TRUNCATED_IMAGES True。这行代码允许 Pillow 解码部分损坏的 JPEG用截断后的数据继续加载。这是针对这类数据集的通用解法不会引入数据泄漏因为训练时模型看到的仍然是完整图片内容。5.3 验证集准确率高真实摄像头一测就翻车现象离线验证集准确率 90%把训练好的模型部署到树莓派 USB 摄像头上识别结果完全不可用猫换个角度就分不出来。原因数据集是“人工筛选过的完美照片”拍摄角度统一、光线良好、背景干净。真实场景中摄像头角度低、光线不均匀、猫运动的运动模糊严重这些分布差异让模型失效本质是训练集和推理环境的分布不匹配俗称“训练时没加噪声部署时全是噪声”。解决采集真实场景数据加入训练集数量至少占总数据量的 30%。如果暂时采不到用数据增强模拟——随机亮度抖动幅度加大到 0.4加入高斯噪声以及随机遮挡。调参只能缓解真正的后悔药是在项目启动时就留一部分真实场景数据做扩展集。5.4 GPU 显存不足batch size 32 直接 OOM现象刚开始训练几秒CUDA out of memory程序退出。原因这是最常见也最容易解决的环境问题。你的显卡显存相对模型输入尺寸来说太小。Resize(128, 128)的 3 通道输入在 batch size 32 时约占用 6MB 的显存用于特征图加上反向传播存储的中间变量累计起来轻松超过 2GB 老显卡的极限。解决按优先级尝试——降低 batch size 到 16 或 8调整num_workers防止 CPU 数据加载成为瓶颈再不行就降低输入分辨率到 96×96删掉torchvision的预训练模型里不需要的中间层缓存.eval()模式下不做反向传播省一半显存。多数情况下把 batch size 从 32 降到 16显存占用直接砍半训练收敛速度只慢一点。5.5 训练 loss 不下降学习率太大或太小都能导致现象Epoch 1 Loss: 1.386跑到第 10 个 epoch 还是 1.386纹丝不动。原因1.386 是 4 分类的交叉熵初始天花板ln4loss 一直钉在这里说明模型没有学到任何有效信息。学习率太大梯度在最优解附近震荡权重无法收敛学习率太小梯度更新量微乎其微loss 也降不动。解决先用单个 batch 做调试——只喂 4 张图跑 1 个 epoch如果 loss 能降下去说明代码链路没问题问题出在数据加载或学习率上。然后做一次学习率扫描把学习率从 1e-2 到 1e-6 按数量级逐档试观察前 20 个 iteration 的 loss 下降速度选择能够稳定下降的最大学习率。5.6 验证集用的是增强后的数据评估结果虚高现象训练和验证用的data_transforms[train]完全一致验证集准确率看起来不错但换到测试集上掉了一截。原因验证集这里被“污染”了。验证集应当模拟真实推理场景只做必要的尺寸调整和归一化不能加随机翻转、随机裁剪。增强后的数据会让模型看到“模糊的、旋转过的、裁剪过的”图片评估结果包含了数据增强带来的随机误差。解决严格区分训练和验证的 transform训练用全套增强验证只用Resize ToTensor Normalize。这是最基本的数据可视化正确性问题但踩过这个坑的人不少。6. 进阶把准确率变成可解释的结论和可部署的服务模型训练完精确率、召回率、F1 这些指标都好看但工程上远没结束。最后一个实战技巧是可视化分类结果用混淆矩阵找出“为什么这只猫被分错了”。from sklearn.metrics import confusion_matrix, classification_report import numpy as np all_preds [] all_labels [] model.eval() with torch.no_grad(): for images, labels in val_loader: outputs model(images) _, preds torch.max(outputs, 1) all_preds.extend(preds.numpy()) all_labels.extend(labels.numpy()) cm confusion_matrix(all_labels, all_preds) report classification_report(all_labels, all_preds, target_namestrain_dataset.classes) print(cm) print(report)打出来的混淆矩阵如果显示“玩耍”经常被误判为“进食”此刻不要急着调模型而是回头看数据——很可能玩耍类里的猫玩具是碗状的或进食类里混入了猫玩食物的照片。用matplotlib把误分类的图片按 “预测概率 真实类别” 排列输出一次能看到 20 张左右视觉检查 5 分钟比改 3 天网络结构更有效。这是模型调试中的“黑匣子”打开方式。部署时我习惯导出为 TorchScript 模型不依赖 Python 环境运行scripted_model torch.jit.script(model.eval()) scripted_model.save(cat_behavior_mobilenet.pt)TorchScript 模型可以被 C、Java 或者移动端直接加载推理延迟比 Python 调用低一半以上。如果你要部署到树莓派或 Jetson Nano这个步骤比写 Flask API 更关键。我自己的项目里从头到尾用过三次猫行为识别每次最花时间的都不是模型结构而是数据清洗和边界情况的处理。熬夜调参换来的教训是CNN 模型本身不会骗你但数据会。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网