PyTorch中DenseNet实现宠物行为分类实战:从数据预处理到模型调优

发布时间:2026/9/14 1:36:22
PyTorch中DenseNet实现宠物行为分类实战:从数据预处理到模型调优 简介这是一份基于PyTorch与DenseNet模型的宠物行为图像分类代码包面向需要快速搭建图像分类流程的初学者、课程设计或小型项目开发者。代码将数据清单生成、CNN训练与PyQt可视化界面拆分为3个独立Python脚本结构简单直观训练部分采用DenseNet作为主干网络适合用于抓痒、舔毛、打盹、汪汪叫等常见宠物行为的识别。资源共9个文件压缩包仅227KB包含01生成txt.py、02CNN训练数据集.py、03pyqt界面.py三个核心脚本以及requirements.txt环境依赖、docx图文说明文档和4张类别提示图片可帮助使用者快速确认数据集放置方式。所有代码均配有逐行中文注释并给出AnacondaPython3.7/3.8PyTorch1.7.1/1.8.1的推荐安装方案即使基础薄弱也能按文档自行配置环境数据集文件夹支持自定义分类放入对应图片即可训练。目前已有141人学习适合作为PyTorch图像分类入门或宠物行为识别方向的参考实现。1. DenseNet 图像分类与行为识别先看懂这份 zip 的底牌宠物行为分类最磨人的不是网络而是同一个动作的两帧之间几乎没有共同点「伸爪」前一帧像在挠地后一帧已经够到玩具模型要把这种连续动作压进一个静态类别里。这份名为「densenet模型-python语言pytorch框架的图像分类宠物行为分类识别-不含数据集图片-含逐行注释和说明文档.zip」的项目去掉压缩包外壳核心是三样东西DenseNet 在 PyTorch 里的实现、一套适合宠物行为细粒度分类的代码骨架、以及逐行注释。行为分类和普通图像分类最大的区别在于类间差异不是「猫」与「狗」那样肉眼可辨而是「扑咬」与「玩耍」、「警惕」与「发呆」这类同行小、动作重叠的细粒度问题。DenseNet 靠密集连接让每一层复用浅层特征在数据量有限、靠微调的场景下往往比同深度的 ResNet 更稳。这份 zip 不附数据集图片意味着你得自己组织数据和标签下面这条技术路线就按这个前提展开。2. 数据组织与预处理没有数据集图片时从零搭出 DenseNet 的输入管线2.1 项目结构和类别目录的约定解压 zip 后先找 requirements.txt 和说明文档里对数据路径的描述。常见做法是作者把数据入口写成 torchvision.datasets.ImageFolder也就是要求你准备一个 train 目录每个类别一个子目录。宠物行为分类建议最多拆到「动物/行为」两级不要用「dog」「dog play」这种带空格的目录名。我一般先建一个 work 目录结构如下data/ ├── train/ │ ├── cat_play/ │ ├── cat_attack/ │ ├── cat_neutral/ │ └── dog_bark/ └── val/ ├── cat_play/ └── ...ImageFolder 会把子目录名按字母序映射成 0、1、2…所以类别名不要出现「cat1」和「cat10」这种会排错序的写法。行为类往往有大量「中性/无事发生」帧把它们单独设一个类比让模型硬归到其他动作里好得多。数据不齐时宁可用旧的手机视频抽帧也比从网上下载一堆尺寸和角度差异巨大的照片更可控。2.2 用 torchvision.transforms 做与 ImageNet 匹配的预处理DenseNet 的 PyTorch 预训练权重是在 ImageNet 上练的归一化参数写死在 torchvision 的文档里mean[0.485, 0.456, 0.406]std[0.229, 0.224, 0.225]。很多人微调时只记得 Resize忘记归一化结果模型输出概率几乎全在 0.5 附近又找不到原因。行为分类的输入是视频抽帧或照片最大边先缩到 256再中心裁剪到 224和官方评测口径一致。如果直接 224x224 硬拉宠物肢体比例会被拉扁行为特征的相对位置就变了。from torchvision import transforms train_transform transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里先用 CenterCrop 是为了跑通基线后面换 RandomResizedCrop 等训练增强时再单独放到训练分支。ToTensor 把像素从 0~255 转到 0.0~1.0Normalize 做的是 (x - mean) / std数值域不是为了让图片好看而是让预训练模型的 BatchNorm 统计量和输入分布一致。这套预处理对 torchvision 里的 densenet121、densenet161 全部通用。阶段缩放裁剪水平翻转用途验证/推理Resize(256)CenterCrop(224)关保证指标可复现训练随机缩放 0.8~1.2RandomResizedCrop(224)按需增加动作姿态多样性2.3 DataLoader 与训练/验证集的划分让 DenseNet 每次迭代吃到的都是「帧」项目不含数据集图片你需要自己写 Dataset。我用最直接的列表式实现一个 csv每行是图片路径和数字标签。这样做的好处是标签名不会被 ImageFolder 的字母序绑架也方便后续做分层抽样。import pandas as pd import torch from PIL import Image class PetBehaviorDataset(torch.utils.data.Dataset): def __init__(self, csv_path, transformNone): self.df pd.read_csv(csv_path) self.transform transform def __len__(self): return len(self.df) def __getitem__(self, idx): row self.df.iloc[idx] image Image.open(row[path]).convert(RGB) if self.transform: image self.transform(image) return image, int(row[label])csv 至少要有两列path 建议写相对路径避免换机器后要重新改全路径label 直接用整数不要在 Dataset 里反复做字符串到数字的映射。接着用 sklearn 的train_test_split划分出验证集比例 8:2 就够行为类如果样本极少在train_test_split里传stratifydf[label]避免某一类在验证集里消失。train_dataset PetBehaviorDataset(train.csv, train_transform) val_dataset PetBehaviorDataset(val.csv, val_transform) train_loader torch.utils.data.DataLoader( train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue)batch_size32在 224x224 输入下约占 8~10GB 显存DenseNet121 的中间特征图比 ResNet50 大显存不够就先降到 16 或 8。num_workers在 Windows 上设置大于 0 时训练主进程要包在if __name__ __main__里否则会报 DataLoader worker 重复启动的错。pin_memoryTrue只在 CUDA 训练时有意义CPU 训练开着反而增加拷贝开销。3. PyTorch 实现加载预训练 DenseNet、微调分类头并跑通训练与验证3.1 torchvision 里 DenseNet 的开箱用法与模型导入的正确姿势PyTorch torchvision 里可以直接拿到 ImageNet 预训练的 densenet121、densenet161、densenet169 和 densenet201。宠物行为分类的样本量通常只有几千到几万张随机初始化训练 DenseNet 效果很差常见做法是加载预训练权重后把最后一层分类器替换成自己的类别数。densenet 的 classifier 只是一个nn.Linear(1024, 1000)替换成本比其他模型低得多。import torch.nn as nn import torchvision.models as models from torchvision.models import DenseNet121_Weights weights DenseNet121_Weights.IMAGENET1K_V1 model models.densenet121(weightsweights) num_features model.classifier.in_features num_classes 6 model.classifier nn.Linear(num_features, num_classes)这里必须用model.classifier.in_features去取输入维度而不是写死 1024因为 densenet121 和 densenet169 的 growth rate 不同最后一个 BN 层的输出通道也不同。DenseNet 的features子模块里已经包含了最后的nn.BatchNorm2d和nn.ReLUclassifier前面还有一层nn.AdaptiveAvgPool2d替换分类器不会破坏特征提取和池化之间的顺序。如果从说明文档里抄完整模型定义要检查最后一个 DenseBlock 的 growth rate 是否与官方预训练权重一致不一致时加载权重会报 size mismatch。模型主干参数量分类器输入维度适用场景densenet121约 7M1024默认选择显存友好densenet161约 27M2208样本多、动作特征细微densenet169约 13M1664中间档densenet201约 18M1920追求上限但样本要够3.2 训练循环与验证交叉熵、AdamW 与学习率参数微调 DenseNet 时不建议用大学习率去动预训练主干。新加的线性层用 1e-3预训练部分用 1e-4优化器选 AdamW配合余弦退火。下面这段是训练循环的骨架也是 zip 里「逐行注释」经常展开的部分。import torch import torch.nn as nn device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) optimizer torch.optim.AdamW([ {params: model.features.parameters(), lr: 1e-4}, {params: model.classifier.parameters(), lr: 1e-3}, ], weight_decay1e-4) scheduler torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) criterion nn.CrossEntropyLoss() for epoch in range(30): model.train() running_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() running_loss loss.item() * images.size(0) scheduler.step() print(fepoch {epoch:02d} loss {running_loss / len(train_loader.dataset):.4f})用 param groups 分开设置学习率让分类头收敛更快又不破坏预训练特征。AdamW 的 weight_decay 只加到权重上带偏置的参数不加会更稳但行为分类这类中等规模数据上差别不大。每轮训练完必须跑一次验证验证要放在torch.no_grad()下并且model.eval()会关闭 Dropout、让 BatchNorm 用全局统计量忘记切 eval 是训练曲线正常、验证指标忽高忽低的最常见原因。余弦退火的T_max要和总 epoch 数一致否则学习率还没降到最低就提前停轮。3.3 断点保存与单图推理把 DenseNet 从训练脚本里摘出来训练完只保存权重文件最省事但下次要继续训练还得手动重建 optimizer 和 epoch所以把 optimizer 状态、scheduler 状态、epoch 和权重一起打包更稳。推理脚本里只需要读model_state_dict然后对一张图走完整的 transform 到 softmax 流程。checkpoint { model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), scheduler_state_dict: scheduler.state_dict(), epoch: epoch, } torch.save(checkpoint, model_best.pt) def predict(image_path, model, class_names, topk3): transform val_transform image Image.open(image_path).convert(RGB) x transform(image).unsqueeze(0).to(device) model.eval() with torch.no_grad(): probs torch.softmax(model(x), dim1)[0] top_idx probs.topk(topk).indices.tolist() return [(class_names[i], probs[i].item()) for i in top_idx]unsqueeze(0)是必须的模型要的是 NCHW 四维张量单张图读完是 CHW 三维不加 batch 维会直接报错。topk3输出前三个候选对行为分类很有用当 top1 置信度不高时看 top3 里是否包括「中性」可以判断模型是否在犹豫。保存时把class_names单独存成一个 json不要依赖训练脚本里的列表否则换环境后标签顺序错位模型准确率会高得离谱。4. 行为分类特有的调参与踩坑DenseNet 从通用分类器到行为识别器4.1 行为类别与置信度阈值——别被准确率骗了普通图像分类里 95% 准确率是好事行为分类里它可能意味着模型把所有帧都认成「中性」。宠物行为视频大量时间没有明显动作正样本占比往往不到 30%如果直接对全帧算准确率模型把每帧都判成「其他」也能刷到 70% 以上。我先把置信度阈值定为 0.5推理时低于阈值的输出统一标记为 unknown再重新计算各类别的 precision 和 recall而不是只盯 accuracy。行为分类还常见「双峰」问题模型对「扑咬」的输出概率在 0.45 和 0.98 之间跳前者来自动作刚开始的帧后者来自动作完成态。把阈值从 0.5 抬到 0.7可以减少大量误触发代价是召回率下降。调阈值时在验证集上画一条置信度-准确率曲线选曲线平台期的值比拍脑袋定 0.5 靠谱。4.2 数据增强组合不要把 ImageNet 的增强原样搬过来ImageNet 训练常见的 RandomResizedCrop RandomHorizontalFlip在宠物行为上要谨慎。水平翻转会把「左爪刨地」变成「右爪刨地」对动物来说左右并非完全对称RandomResizedCrop 裁剪到很小区域时可能把耳朵和尾巴裁掉而这些恰恰是行为识别的关键部位。建议训练增强只做小角度旋转、随机缩放 0.8~1.2、轻微颜色抖动和 Cutout。from torchvision import transforms import torchvision.transforms.v2 as v2 train_transform v2.Compose([ v2.Resize((256, 256)), v2.RandomRotation(10), v2.RandomAffine(degrees0, translate(0.05, 0.05), scale(0.9, 1.1)), v2.ColorJitter(brightness0.2, contrast0.2, saturation0.1), v2.RandomErasing(p0.2, scale(0.02, 0.1)), v2.RandomOrder([ v2.RandomHorizontalFlip(p0.5), v2.Identity(), ]), v2.ToImage(), v2.ToDtype(torch.float32, scaleTrue), v2.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])这里用 torchvision.transforms.v2把 RandomErasing 和 ToTensor 的流程整合得更顺v2.Normalize要求输入已经是 float 张量所以要先ToDtype。RandomHorizontalFlip 放进 RandomOrder让翻转与不翻转各占一半并不是完全禁用而是防止模型只见过正向宠物。RandomErasing 对部分遮挡的鲁棒性提升明显p0.2 不喧宾夺主。4.3 类别不平衡、标签噪声和学习率三个必调参数参数推荐起点说明调试线索lr主干1e-4预训练权重只做微调太大会破坏特征loss 先降后升lr分类头1e-3新分类器从零学可以更快前两轮 acc 无变化weight_decay1e-4AdamW 下不设反而易过拟合验证 loss 反弹label_smoothing0.1行为标签噪声高平滑可缓解过度自信训练 acc 接近 1 但验证 acc 低batch_size32DenseNet 特征图显存开销比 ResNet 大显存不足降 16类别不平衡不要先砍样本先用 WeightedRandomSampler让训练时每个 batch 里各类出现的期望次数接近。实现上按 label 的逆频率算每个样本权重import numpy as np from torch.utils.data import WeightedRandomSampler labels train_df[label].to_numpy() classes, counts np.unique(labels, return_countsTrue) weights 1.0 / counts sample_weights weights[labels] sampler WeightedRandomSampler(sample_weights, num_sampleslen(labels), replacementTrue)replacementTrue允许重复采样数据量少的时候靠它把样本少的类反复喂给模型。另一个容易被忽略的问题行为数据多数来自连续视频训练集和验证集如果按帧随机切分同一个视频的相邻帧会同时出现在两边验证结果会虚高。正确做法是按视频片段分组一个视频的所有帧只能进训练集或只能进验证集否则模型记住的是背景而不是动作。5. 验证你的模型混淆矩阵、Grad-CAM 与连续帧投票5.1 混淆矩阵看模型到底错在哪一类行为分类里准确率不告诉你错在哪。用 sklearn 的 confusion_matrix 输出归一化矩阵如果「追赶」和「玩耍」互相混淆说明动作姿态本身接近需要更多时序信息如果「中性」被误判成「玩耍」通常是你没给中性类足够样本模型只能硬着头皮二选一。可视化时直接用 class_names 索引行列方便贴到报告里对照。5.2 Grad-CAM看 DenseNet 在关注哪里行为分类的关键是模型是否盯着爪、尾巴、头而不是背景。对 DenseNet 做 Grad-CAM需要把 features 模块的输出和梯度都抓下来activations {} def forward_hook(module, input, output): activations[feat] output.detach() gradients {} def backward_hook(module, grad_input, grad_output): gradients[grad] grad_output[0].detach() h1 model.features.register_forward_hook(forward_hook) h2 model.features.register_full_backward_hook(backward_hook) out model(x) model.zero_grad() out[0, out.argmax(dim1).item()].backward() A activations[feat][0] # C, H, W G gradients[grad][0] # C, H, W weights G.mean(dim(1, 2), keepdimTrue) cam (weights * A).sum(dim0, keepdimTrue).clamp(min0) cam cam / cam.max()register_forward_hook抓 features 最后的输出register_full_backward_hook抓反向时传回的梯度。Grad-CAM 对每个通道算梯度的空间平均再对激活加权求和最后 clamp 掉负值得到特征图上的热力区。DenseNet 的特征图分辨率约 7x7上采样回原图看热区如果热点全落在背景或另一只宠物身上说明数据里有混淆项先清洗数据再调模型。5.3 连续帧滑动窗口投票让 DenseNet 从图像分类变成行为分类单帧分类不稳定是行为识别的固有难点。常见做法是每隔 3~5 帧取一帧对最近 N 帧的 softmax 概率求平均再取 argmax并且仍要过置信度阈值。这样能抑制「某一帧恰好形似攻击姿势」的抖动同时保留动作开始和结束的过渡信息。from collections import deque window deque(maxlen7) for prob in stream_of_frame_probs: window.append(prob) if len(window) 7: continue avg torch.stack(list(window)).mean(dim0) conf, cls avg.max(dim0) if conf.item() 0.6: print(unknown) else: print(f{class_names[cls.item()]} {conf.item():.2f})窗口大小根据动作时长定偷吃、扑咬这种 0.5 秒内的动作7 帧以 20fps 抽帧约等于 0.35 秒比较合适长时间舔毛可以用 15 帧。deque(maxlen7)不需要手动清旧帧达到长度后自动挤掉最老的帧。最后把每个视频片段分别聚合出标签再算视频级准确率这比逐帧指标更接近真实诉求。用这个方法在验证集上跑一遍如果视频级准确率比帧级准确率高 5 个点以上说明投票窗口确实压掉了噪声。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询