news 2026/10/19 9:43:05

基于Python CNN的猫疲劳识别:从图像分类到状态检测的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python CNN的猫疲劳识别:从图像分类到状态检测的完整实践

选“基于python-CNN卷积神经网络对猫是否疲劳识别”当毕业设计题目,第一眼看起来像是个常规图像分类问题,实际上它踩中了计算机视觉里比较有意思的一块:不是判断“这是什么猫”,而是判断“猫现在状态怎么样”。整个项目要解决的核心问题是,给定一张猫的图片或者一帧视频,模型能输出它属于疲劳状态还是精神状态的置信度。如果你正在选毕业设计方向、想系统地走一遍CNN图像分类流程,或者对动物行为分析、宠物智能硬件感兴趣,这个题能让你在一个学期内把数据、模型、评估、部署的完整闭环都跑通。接下来我把整个实现路线拆开讲,包括数据怎么准备、模型怎么选、训练里哪些坑我踩过、最后怎么把它变成一个能用的系统。

1. 项目整体设计与思路拆解

1.1 从题面看本质:这不是“猫分类器”这么简单

“猫是否疲劳识别”听起来像是“给猫图片打个标签”,但这题真正考的是细粒度状态识别。人的疲劳一般看眼神、打哈欠、闭眼频率,猫的疲劳则复杂得多,通常体现在耳朵是否放松、眼睛是否半闭、胡须位置、身体是否蜷缩、活动幅度如何等等。单个维度都不够可靠,但图像的好处是能把多个维度同时呈现给模型,让模型自己去学哪些线索组合更有效。这也是我最后坚持用CNN而不是传统特征工程的原因。

如果用传统方案,比如先提取HOG特征再扔给SVM,最大的问题是“疲劳”这个状态没有清晰边界,人工设计的特征很难覆盖各种姿势和光照。有的猫趴着但耳朵竖得很高,这说明它只是闭目养神,警惕性还在;有的猫眼睛全闭、全身放松、四肢摊开,这才是深度疲劳。这种细微差别,只有让模型从大量样本里自己归纳特征才靠谱。CNN的卷积核天然适合捕捉局部纹理和姿态线索,而且具有平移不变性,猫在画面里偏左偏右都不太影响判断。

1.2 技术选型背后的三条关键理由

技术栈上我选了Python,原因很实际。深度学习框架对Python的支持最完整,数据预处理、模型训练、可视化、部署都围绕Python生态展开,遇到问题也最容易找到解决方案。模型结构上,我一开始并没有直接上大网络,而是先搭一个轻量CNN跑通全流程,之后再用迁移学习切换成ResNet50。这样做不是因为大网络一定好,而是毕业设计需要“能解释的过程”:先有baseline,再通过改进体现对比,答辩时也更好讲。

第三条理由我放在迁移学习上。猫疲劳识别这个场景,公开数据集很少,自建数据集撑死几千张图片,靠随机初始化训练一个深层网络很容易过拟合。而ImageNet上预训练过的模型已经学会了大量通用纹理、边缘、形状特征,拿过来只微调最后几层,可以用很小的数据量达到不错的效果。这一步不是我图省事,而是小数据场景下最稳妥的工程选择。

1.3 项目总体流程与模块划分

整体流程我拆成了七个模块,每个模块都有明确的输入输出:

  1. 数据采集与清洗:从公开宠物图片库和自行拍摄的视频抽帧里收集原始素材,剔除模糊、重复、姿态严重遮挡的样本。
  2. 标注与整理:定义疲劳/精神两类标签,建立标注规范并逐张确认。
  3. 数据预处理与增强:统一尺寸、归一化、做随机裁剪翻转等增强操作,扩充样本多样性。
  4. 模型构建:先用自建CNN验证流程,再替换为预训练模型进行迁移学习。
  5. 模型训练:设置损失函数、优化器、学习率策略,配合早停和模型保存。
  6. 评估与优化:计算精确率、召回率、F1值,分析混淆矩阵,针对性补充困难样本。
  7. 部署集成:把训练好的模型封装成命令行工具和一个简单的Web上传界面。

这个流程的关键点在于:每一步都可以单独验证。比如数据没准备好,后续模型再换也是白搭。我见过很多同学一上来就调模型,最后发现是label给错了,返工成本极高。所以我把数据环节放在最前面,而且投入了最多时间。

2. 数据集获取与预处理:决定模型上限的环节

2.1 数据来源与标注规范

我建的数据集总量是两万多张,其中疲劳类8000多张,精神类12000多张,剩下的是模糊样本。数据来源主要有两个:一是公开的猫类图像数据集,里面包含大量不同品种、不同场景的猫图;二是自己拍视频抽帧,用手机拍家里的猫,白天晚上各拍一段,再按帧导出。公开数据集的优点是数量大、场景丰富,缺点是很多图片都是宠物摆拍,精神状态不够典型;自己拍的数据虽然量少,但和真实应用场景更贴近。

标注是这里面最耗精力的环节。我最初想简单分成“疲劳”和“精神”两类,但很快发现有些图片根本没法归入任何一类,比如猫正在伸懒腰、正在舔毛、或者回头张望,这些动作容易误导模型。后来我改成三分类思路:疲劳、精神、丢弃。丢弃类不参与训练,只用来过滤。所有标注都遵循一个标准:眼睛半闭或全闭、耳朵放松下垂、身体蜷缩不动,满足两个以上才算疲劳;眼睛圆睁、耳朵竖直、有活动姿态,算精神。

标注工具我用的是开源图像标注软件,直接在界面上给图片打标签,导出成CSV,字段包含图片路径和类别。整个过程分批完成,每批标完就随机抽一部分自己复查,尤其是两类边界上的样本。这个复查动作非常重要,因为疲劳本身是连续状态,强行二分类必然产生噪声,只能靠标注规范降低错误率。

2.2 图像预处理与数据增强策略

原始图片尺寸五花八门,有500×400的,也有1200×900的,直接丢给CNN会非常麻烦。我统一处理成224×224,因为这是ResNet50的标准输入尺寸。处理时不是简单拉伸,而是先按短边等比缩放,再中心裁剪到224×224,这样能避免大部分形变。归一化我参考了ImageNet的均值方差,像素值减去[0.485, 0.456, 0.406]再除以[0.229, 0.224, 0.225],迁移学习场景下这个操作能让预训练权重发挥得更稳定。

数据增强我用了几个组合方案:

  1. 水平随机翻转,猫左右对称,这操作不改变语义。
  2. 随机旋转,范围在-15度到15度之间,角度太大会出现黑边,所以我开了fill_mode补边。
  3. 随机亮度调整,范围0.8到1.2,用来模拟不同光照环境。
  4. 随机缩放和裁剪,缩放比例0.9到1.1,让模型适应猫在画面里大小不同的情况。
  5. 随机擦除,在图片上随机遮挡一块小区域,让模型不要只盯着某一个局部特征做判断。

这些增强不是越多越好,关键是增强后的图片仍然能被人类明确判断是疲劳还是精神,否则只会增加噪声。我实测下来,翻转、旋转、亮度调整收益最明显,随机擦除对提升鲁棒性有帮助,但训练时间会变长。

2.3 类别平衡处理

疲劳类只有8000多张,和精神类12000多张相比差距接近1.5倍。直接训练的话,模型会偏向预测多数类,体现在准确率上还行,但疲劳类的召回率会偏低。我用了两种方式解决:一是训练时对疲劳类做在线过采样,确保每个batch里两类数量大致相等;二是在损失函数里给少数类更高的权重,让模型对疲劳类误判付出更大代价。这两种手段可以同时用,不会冲突。最后模型在测试集上的疲劳类召回率比单独使用一种方式时高了大概6个百分点。

3. CNN模型搭建与训练

3.1 先搭一个轻量CNN打通流程

我第一版模型没有直接用预训练网络,而是先用一个轻量CNN把整个流程跑通。结构很简单:输入层接3个卷积块,每个卷积块包含3×3卷积、批归一化、ReLU激活、2×2最大池化;卷积通道数从32增长到64再增长到128;最后接一个全局平均池化,一个Dropout层,一个全连接层,输出2类。代码用常见深度学习框架写,核心部分大约是这样:

import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=2): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Flatten(), nn.Dropout(0.5), nn.Linear(128, num_classes), ) def forward(self, x): return self.classifier(self.features(x))

这个模型只有100万左右的参数量,在一张普通显卡上训练很快。它存在的意义不是刷精度,而是让我验证数据加载、标签读取、训练循环、模型保存这些环节是否正常。我记得第一次训练时loss不降,查了半天发现问题出在标签文件里路径多了一个空格。这种基础问题用大模型排查反而麻烦,用轻量模型跑得快,调试效率高很多。

3.2 换上预训练ResNet50做迁移学习

流程跑通之后,我换成了ResNet50作为主干网络。具体做法是加载ImageNet预训练权重,去掉最后一层全连接分类头,保留卷积特征提取部分;然后冻结前面大部分层,只微调最后两个残差块和新增的分类头。因为猫的纹理、轮廓和自然图像通用特征差异不大,不需要让所有层都重新学习。

新加的分类头我用了这样的结构:全局平均池化,接一个256维全连接层,ReLU激活,Dropout 0.3,最后接2维输出。这里用全局平均池化代替Flatten,能减少参数量,同时保留空间信息。冻结参数的时候,我把新加的全连接层和最后两个残差块设置为可训练,其余层设置requires_grad为False。

微调时学习率设得比从头训练低很多,我用的是1e-4。预训练模型已经处在一个较好的局部最优附近,学习率太大容易把权重破坏掉。优化器选了Adam,虽然SGD配合学习率调整通常泛化更好,但Adam对初始参数不敏感,更适合微调场景。训练30个epoch,前面15个epoch只训练分类头,后面15个epoch把最后两个残差块也放开一起训练,这样分阶段微调比一步到位更稳。

3.3 训练中的关键参数与技巧

训练参数上,图像尺寸224×224,batch size设成32。刚够把一张12GB显存的显卡占满,如果显存不够就降到16,配合梯度累积也可以。损失函数用交叉熵,因为这是标准的多分类任务。为了应对类别不平衡,我额外给疲劳类设了更高的class weight,具体值是1.2,精神类保持1.0,这样模型在训练时会更在意疲劳类的正确率。

我加了三个回调机制:第一个是EarlyStopping,监控验证集loss,连续8个epoch没有改善就停止训练;第二个是ReduceLROnPlateau,验证集loss停滞3个epoch时把学习率降到原来的十分之一;第三个是ModelCheckpoint,每轮结束保存当前最好的模型权重,避免训练中断后从头再来。这三个机制从第二次训练开始就固定使用,效果非常稳定。

训练过程中的loss下降曲线值得多说一句。迁移学习开始后,第一个epoch的loss往往很低,因为预训练模型虽然没学过猫疲劳分类,但它提取的特征已经很能区分自然图像了。如果你看到loss不降反升,大概率是学习率太大,或者数据标签有严重错误。我在实验记录里看到过一次loss反弹,最后定位到数据增强里的随机擦除区域过大,感觉像是给图片贴了一块黑斑,导致标签语义被破坏,把擦除范围从原来的30%缩小到15%之后,loss曲线就恢复正常了。

4. 模型评估与结果分析

4.1 不要只看准确率,四个指标一起看

很多初学者看到测试集准确率98%就以为大功告成,这在疲劳识别场景里是非常危险的判断。疲劳识别是典型的“漏报代价高”的任务,如果一只疲劳的猫被判断为精神,后续可能错过健康问题的早期信号;反过来,把精神的猫误判为疲劳,最多是虚惊一场。所以我评估时主要盯四个指标:准确率、精确率、召回率、F1值。

指标含义我的关注点
准确率全部预测中正确的比例只做参考,类别不平衡时有迷惑性
精确率预测为疲劳的样本中真正疲劳的比例防止把精神状态误报为疲劳
召回率实际疲劳样本中被正确找出的比例漏报越少越好,这个指标最重视
F1值精确率和召回率的调和平均综合评价模型在疲劳类上的表现

训练完之后,我在测试集上得出的结果是:准确率93.6%,精确率92.1%,召回率94.8%,F1值93.4%。单独看准确率似乎不错,但其实最关键的召回率比准确率高了1个多点,这才是对疲劳识别更有利的方向。如果召回率掉到85%以下,即使准确率再高,这个模型在真实场景里也不太敢用。

4.2 混淆矩阵与典型误判分析

混淆矩阵能告诉我具体错在哪里。我导出测试集的混淆矩阵后,发现误判集中在两类图片上:一类是猫侧身躺着但耳朵没有完全放松,模型容易把这种半警惕状态判成精神;另一类是光线很暗的图片,瞳孔放大,眼睛看起来很亮,模型有时会把这种状态误判为精神。为了改进,我从训练集里专门抽了这两类图片各200张,重新确认标签是否准确,再通过数据增强让模型多看类似角度和光照条件。一顿操作之后,误判数量下降了大约三分之一。

另一个发现是,短毛猫和长毛猫的误判模式不同。短毛猫的耳朵轮廓更清晰,模型容易学到“耳朵竖直”这个特征,对姿势变化敏感;长毛猫因为毛发遮挡,耳朵特征不明显,模型更多地依赖眼睛闭合程度来判断。这说明模型确实学到了多个维度,不是单一特征在起作用。后来我做测试时,会刻意把测试集按猫的品种类型分组,分别计算指标,分组结果比整体指标更有说服力。

4.3 阈值调整与置信度校准

分类模型最终输出的不是标签而是概率,通常默认以0.5作为分界线。但0.5未必是最优阈值。我把验证集上每个样本的疲劳概率拉出来,从小到大排序,手动设定不同的阈值做模拟,然后观察召回率和精确率的变化。结果发现,把疲劳判断阈值从0.5降到0.4,召回率能提升3个百分点,精确率只下降1个点。这在实际应用中很划算,因为漏报的代价更高。

阈值调整的本质是在漏报和误报之间找一个平衡点。如果你的应用场景是宠物健康监测,希望尽可能不遗漏异常状态,那就把阈值调低一点;如果场景是自动喂食器,频繁误判会影响体验,那阈值可以保持在0.5以上。这个逻辑不复杂,但很多教程不会讲,我强烈建议每个人都做一次阈值扫描实验,用数据说话,而不是拍脑袋定0.5。

5. 系统落地与简易交互界面

5.1 命令行批量预测脚本

模型训练完只躺在磁盘里没什么用,我做的第一步是把推理封装成命令行工具。输入一张图片路径,输出预测类别和置信度。脚本核心逻辑大约是:

import torch from PIL import Image from torchvision import transforms def predict(image_path, model, device): transform = transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]), ]) img = Image.open(image_path).convert("RGB") tensor = transform(img).unsqueeze(0).to(device) with torch.no_grad(): outputs = model(tensor) prob = torch.softmax(outputs, dim=1)[0] fatigue_prob = prob[0].item() label = "疲劳" if fatigue_prob >= 0.4 else "精神" return label, fatigue_prob

这里面有两个细节值得注意。第一个是预测预处理必须和训练时保持一致,Resize和CenterCrop的顺序、Normalize的数值都不能变,否则结果会明显变差;第二个是推理时一定用torch.no_grad()包裹,避免构建计算图,既省内存又提速。批量预测时,可以再加一个数据加载器,分批处理文件夹里的所有图片,输出结果写到CSV里,方便后续统计。

5.2 做一个简易Web上传界面

命令行工具自己用可以,但给导师演示或者期末展示不够直观。我花半天时间做了一个极简Web界面:网页端上传一张图片,后端加载模型,返回预测结果和置信度,页面展示原图并显示“疲劳”或“精神”。实现上不需要复杂的框架,直接用Flask或者Streamlit都可以。我个人推荐Streamlit,因为代码量少,上传组件和展示组件都是现成的,适合毕设场景。

Streamlit版本的界面核心逻辑就是:页面标题、文件上传组件、调用预测函数、显示结果。全程不到100行代码。这里有一个经验,第一次加载时要初始化模型并放到全局变量里,不要每次请求都重新加载权重,否则几秒钟才能出结果,体验非常差。模型加载一次之后,后续图片推理通常只需要几十毫秒,作为演示完全够用。

5.3 朝向摄像头实时识别扩展

如果想把项目做得更有亮点,可以加一个摄像头实时识别功能。用OpenCV读取视频流,每5帧抽1帧送入模型,然后把结果叠加到画面左上角。直接每帧推理也行,但没必要,猫的动作本身有一定持续时间,抽帧策略能大幅降低计算压力。实时性上,如果是在CPU上推理,单帧耗时可能超过200毫秒,画面会卡顿;换成GPU之后能跑到每秒15帧以上,基本可用。

帧间平滑是个实用小技巧。单帧预测很容易出现抖动,比如连续几帧中有一帧被误判为疲劳。我做了个简单的滑动窗口:记录最近5帧的预测结果,取众数作为当前帧的输出,这样能有效过滤偶发的抖动。这个小功能在最终答辩演示时很加分,因为现场演示往往光线不稳定,直接推理容易翻车,平滑处理后输出就稳定多了。

6. 常见问题与排查技巧实录

6.1 训练loss不降或Loss为NaN

我遇到过两次loss不降,排查顺序是:先看数据加载是否正确,确认标签和图片路径对齐;再看预处理是否一致,归一化参数有没有写错;然后检查学习率,过大容易导致loss震荡,过小导致不收敛;最后检查模型输出层有没有配合对应的损失函数。loss出现NaN则大概率是学习率过大或者某些图片像素值异常,比如读入了损坏的图片文件,用PIL打开时没有异常处理,混进了一些全黑或者全白的异常图,把这些图剔除后问题就消失了。

6.2 过拟合明显怎么办

过拟合的典型表现是训练集准确率接近100%,验证集准确率只有85%,两者差距越来越大。我的处理步骤是:先降低模型复杂度,把全连接层从256维降到128维,加大Dropout比例;再增强数据多样性,把旋转角度从10度加到15度,增加随机亮度扰动;最后配合早停,只要验证集指标开始下降就停止训练,不要等训练集指标继续上涨。按这个顺序处理后,训练集和验证集的差距从15个百分点缩小到6个百分点左右。

6.3 类别不平衡导致疲劳类识别差

疲劳类样本少的时候,模型会偷懒,把所有图片都预测成精神,因为这样也能得到很高的准确率。解决办法我在前面提过两个:class weight和过采样。如果这两种都试了效果仍然不好,就要考虑扩充数据,而不是继续在模型上折腾。我自己最后实际是把疲劳类的样本从5000张增加到8000张,提升比任何调参都明显。数据量才是小样本分类任务最硬的约束。

6.4 GPU内存不足与训练速度慢

显存不够最常见,batch size改成16或者8就能解决。如果还想用更大batch,可以考虑梯度累积,每多个小batch累积一次梯度再更新参数。训练速度慢也要分情况,如果GPU利用率偏低,可能是数据加载成了瓶颈,把数据加载器的num_workers调高,或者改用预处理后的Tensor缓存。如果用的是普通CPU训练,几万张图片可能要跑十几个小时,建议先跑一个小数据集验证代码,再切到完整数据集。

6.5 推理结果与训练指标不一致

模型在测试集指标很好,但实际跑新图片总是出错,这种情况很可能是训练数据和应用场景分布不一致。比如训练集大多是室内拍摄的清晰猫图,实际测试却是光线很暗的手机照片。解决方法是采集一部分目标场景的图片加入训练集,哪怕是几十张,也能明显改善。这里没有捷径,数据分布是绕不过去的,模型只能学到训练数据里存在的信息。

最后分享一点实际体会

整个项目做下来,我最深的感触是疲劳识别真正的难点不在模型结构,而在“疲劳”这个定义本身。人眼判断都需要看多个特征,模型能学到什么程度,完全取决于你喂给它的数据长什么样。如果让我再做一次,我会在标注阶段引入多人交叉标注,至少两个人独立给同一批图片打标签,不一致的样本单独讨论,这样能显著降低标签噪声。另外,模型的最终形态也不一定非要做成二分类,输出疲劳概率本身就有意义,配合阈值调节可以适配不同应用场景,这个思路比固定一个标签更灵活。如果你正好也在做类似题目,建议不要急着堆模型,先把数据扎扎实实做好,后面的一切都会顺畅很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:41:23

BYOVD攻击与防御:漏洞驱动如何绕过签名篡改内核硬件ID

最近在整理威胁情报时,又看到不少关于 BYOVD 的讨论。标题里的“Intel 漏洞驱动硬件 ID 欺骗工具”,说白了就是一组拿漏洞驱动当跳板、在内核态做数据篡改的工具。这类工具这两年很有热度,核心手法是 BYOVD——带漏洞的合法签名驱动&#xff…

作者头像 李华
网站建设 2026/10/19 9:41:00

基于YOLO26的PCB缺陷检测系统:从训练到部署全流程实践

去年底有个做电子制造的兄弟找我,说他们产线上AOI设备误报率太高,焊点附近的铜箔缺陷总是漏检,人工复检速度又跟不上。聊到最后,核心需求就一句话:能不能拿深度学习模型试一把,直接在光学检测环节就把缺陷框…

作者头像 李华
网站建设 2026/10/19 9:40:14

YOLO11-C3k2-RVB:PCB缺陷检测实战改造与部署全解析

在PCBA产线上跟缺陷检测打交道久了的工程师都有一个体会:模型在论文数据集上拿高分是一回事,能稳定扛住产线的光照抖动、板面脏污和不同批次板卡的色差是另一回事。我去年用YOLO11-C3k2-RVB这个改进结构替掉原来产线上的检测模型,最大的触动不…

作者头像 李华
网站建设 2026/10/19 9:40:11

毕业设计实战:爬虫、舆情分析与可视化一站式打通

毕业设计怎么把“爬虫、舆情分析、可视化”三个坑一次填平每年到这个时候,总有一批人要做“基于大数据的社交平台数据爬虫舆情分析可视化系统”这类毕业设计。我想说的是,这个题目看上去是三个词拼一起,实际上它是一个完整的数据工程链路——…

作者头像 李华
网站建设 2026/10/19 9:39:30

STM32驱动ADS1220高精度ADC:从寄存器配置到多通道低功耗采集

简介:这份ADS1220驱动代码面向使用STM32F407进行嵌入式开发的工程师与学习者,针对24位高精度ADC在传感器接口、医疗设备及精密测量等场景下的驱动实现问题,提供可直接参考的底层代码方案。资源包共2个文件,包含1个ads1220.h头文件…

作者头像 李华
网站建设 2026/10/19 9:38:50

无感FOC零速低速段怎么控?脉振高频注入法原理与实战

第一次给板子写完无感FOC固件,我按下启动键,电机在零速附近“嗡”了一下,然后开始不紧不慢地反向爬行,电流波形像心电图一样乱跳——那一刻我就知道,转子位置估计在零点附近栽了跟头。做永磁同步电机(PMSM&…

作者头像 李华