ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

野生保护动物识别系统实战:基于ResNet与Django的图像分类部署全流程

野生保护动物识别系统实战:基于ResNet与Django的图像分类部署全流程

从野外红外相机攒下来的一堆动物照片需要人工一张张翻着看,到能直接在网页上拖一张图进去就返回"雪豹、置信度0.94",这条路我走了差不多一年。做完这套野生保护动物识别系统之后最大的体会是:深度学习模型真正难产的地方不在网络结构,而在你把ResNet50、ResNet34、VGG16这些名字背得再熟,也绕不开数据清洗、训练调参、部署联调这一整条链路。这个项目里有完整的技术文档,覆盖了从数据集构建、三种卷积神经网络对比实验到Django平台发布的所有实现细节,适合正在做图像识别课题的学生、刚接触深度学习的算法工程师,以及想给巡护监测加点自动化手段的保护区技术人员参考。我会把选型思路、训练细节和遇到过的问题全部捋一遍,照着做基本能复现一套能用的识别系统。

1. 项目整体设计与技术选型

1.1 需求拆解与场景假设

动工之前先把需求想清楚:这套系统要服务什么场景?我当时的设定是两类典型用例。第一类是野外红外相机拍摄的监测影像,这类照片往往光线杂乱、动物占比小、环境背景多,偶尔还有遮挡和运动模糊;第二类是巡护员手机拍摄的现场照片,角度随意、分辨率参差不齐,还可能出现半截身体、屁股对镜头之类的情况。识别对象锁定在十余种典型的野生保护动物,包括雪豹、东北虎、金丝猴、藏羚羊等常见监测物种,同时也必须能区分家畜和背景,避免把牛羊当成保护动物。

这样设定完之后,技术指标就变得具体了:单张图片推理时间不能超过1秒,Top-1准确率争取92%以上,模型大小控制在300MB以内,能在一台普通GPU服务器上跑Django服务。这些指标直接影响后面所有选型。

1.2 为什么是三模型对比而不是单模型

很多人会问:直接拿最好的ResNet50训到底不就行了吗,为什么要同时做ResNet50、VGG16、ResNet34三个模型?我的考虑是这样的:首先,这是一个有技术文档交付要求的项目,需要给导师或使用方说明"为什么最终选它",没有对比就没有说服力。其次,三个网络分别代表了三种设计哲学——VGG16是经典堆叠结构、ResNet34是浅层残差、ResNet50是带瓶颈结构的深层残差,放在同一批数据上训练对比,结论才立得住。最后一点比较实际:部署环境不固定,保护区现场的服务器可能没有高端GPU,ResNet34轻、ResNet50准、VGG16的预训练权重普及度最高,三套方案可以按需切换。

这种做法在真实工程项目里叫消融对比,听着挺学术,实际上就是给自己留后路。别嫌麻烦,模型不在同一个数据流上跑过一遍,你根本不知道某个精度差异到底是网络结构带来的,还是训练随机性造成的。

1.3 网络结构层面的关键判断

VGG16我最早用它做基线,原因是结构极度规整:卷积层反复堆叠,每层都用3x3卷积核,最后接三层全连接。它的弱点也很明显,全连接层参数量巨大,光这部分就占到模型的一半,训练和推理都吃力。但它作为baseline的价值在于:如果模型效果连VGG16都不如,那基本是数据出了问题,而不是网络结构问题。

ResNet34是残差网络里比较"小而美"的版本,由若干个残差块堆成,通过跳跃连接把输入加到输出上。这个设计解决的是深层网络退化问题:网络加深之后,理论上表达能力只会更强,但实际训练时误差反而变大,因为梯度在反向传播中逐渐消失。残差连接相当于给梯度开了一条近路,让信息可以跨层直接流动。ResNet34我用它跑轻量级部署,尤其适合没有独立显卡的环境。

ResNet50则把残差块升级成了瓶颈结构——先用1x1卷积把通道数降下来,再用3x3卷积做空间特征提取,最后用1x1卷积把通道数恢复。这样做的好处是用更少的乘法运算换更深的网络。我在实际对比中感觉到它的精度上限明显高于前两者,而且推理速度比VGG16还快,最终把它定为主力模型非常合理。

2. 数据集构建与预处理

2.1 数据从哪儿来

模型能不能干活,七成看数据。市面上有几类公开野生动物图像数据集,有些是按物种组织的分类数据集,有些是来自监测站、竞赛任务附带的数据包。这类公开数据适合做预训练和热身,但直接拿去部署是不够的——因为真实场景里的红外相机图片和公开数据集里的"证件照"差距非常大。

我实际操作中的数据构成是:公开数据集大约贡献了四成图片,剩下的六成来自两个渠道,一个是对红外相机导出图片做了整理和筛选,另一个是模拟场景拍摄——比如去动物园、养殖基地拍动物在不同角度、不同光线下的照片,再把背景裁掉或者保留自然遮挡来模拟野外条件。最终整理出大约1.6万张有效图片,覆盖15个动物类别加1个背景负样本类别。

这里要特别强调负样本。很多项目翻车就翻在没有"什么都不是"的分类。红外相机经常拍到空的草地、晃动的树枝、人、摩托车灯反光,如果模型没见过这些照片,它会把一切东西都往已知类别上靠,识别置信度还特别高。我专门收集了5000多张纯背景图片作为负样本,加了背景这一类之后,系统的误报率降了一个档次。

2.2 标注规范与质检流程

标注是整个项目最枯燥也最关键的一环。我用LabelImg逐个框选图片里的动物区域,框选原则有两个:一是必须完整框住动物主体,宁可多留一点背景也不要切掉耳朵或尾巴;二是遮挡严重的个体单独标注,不跟清晰个体混在一起。每张图只保留一个主要目标,出现多只动物时选画面中心、体型最大的一只作为标注对象,其余忽略。

标注完成后的质检环节经常被忽视,但我强烈建议留出一天专门做。我把验证集里预测错误的样本全部汇总,去看是标注问题还是模型问题,结果发现有大量错误标注:有的把"岩羊"标成了"盘羊",有的把混入画面的家犬框住了没删掉。质检的规则是每张训练图至少被两个人交叉确认一遍,拿不准的统一送审讨论。标注质量直接决定模型上限,这一步省不了。

2.3 数据增强与类别不均衡处理

野生动物数据天然存在长尾分布:雪豹、东北虎的样本多,藏羚羊、金丝猴的样本少,有的类别可能只有100来张。直接用原始数据训练会导致模型对高频类别过拟合,对低频类别完全认不出。我采用的方案分三层。

第一层是离线增强,对少数类别的每张图片做水平翻转、随机旋转15度、随机裁剪缩放、色彩抖动和亮度对比度调整,把每类扩充到至少400张。第二层是在线增强,训练时用Ampaugment策略对图像做随机扰动,相当于每个epoch看到的训练样本都不一样,这个对抑制过拟合特别有效。第三层是损失函数层面的处理,使用加权的CrossEntropyLoss,给样本少的类别更高的权重,让模型在梯度更新时更关注这些类别。

我在项目里还试过Focal Loss,把困难样本的梯度放大、简单样本的梯度压低。整体效果是低频类别的召回率提升明显,但收敛速度变慢了,最终我在更看重Top-1准确率的主模型上仍然用了带类权重的普通交叉熵,Focal Loss放在了ResNet34的对比实验里做参考。

3. 训练过程与调参实战

3.1 迁移学习与预训练策略

我没有从零训练任何一个模型。三套网络全部加载了ImageNet上的预训练权重,原因很直接:即使有1.6万张图,相对模型容量来说还是太少,从零初始化收敛慢而且精度低。预训练模型已经学会了边缘、纹理、形状这些通用特征,我们只需要让它在野生动物数据上做fine-tune。

这里有个细节:加载预训练权重时,最后一层全连接层要替换成自己的分类层,因为ImageNet有1000类,我们只要16类。替换之后我通常会先冻结前面所有层,只训练分类层和最后的残差块,等损失下降到拐点,再解冻全部层用很小的学习率微调。这个"冻结-解冻"两阶段策略比一步到位直接全量微调稳定得多,也不会出现前期loss剧烈震荡的情况。

3.2 超参数配置与训练过程记录

训练的硬件用的是单张中端GPU显卡,型号不提了,反正是普通深度学习工作站就能跑的级别。训练框架是PyTorch,输入图片统一resize到224x224,batch size设为32,优化器选AdamW,初始学习率3e-4,配合余弦退火调度器把学习率逐渐降到1e-6。为了稳定早期训练,我加了5个epoch的线性warmup,让优化器先适应数据分布再全力更新。

完整的训练周期是这样的:ResNet34大约90个epoch跑完,ResNet50跑了120个epoch,VGG16因为计算量大只跑到75个epoch就停了。每个模型训练完都做一次尺度测试,把输入分辨率临时调到256x256再验证一次,看看模型是否对分辨率敏感。记录显示ResNet50在224分辨率上的Top-1准确率是93.2%,提升到256后达到94.1%,但推理时间从210毫秒增加到300毫秒,为了在线服务响应速度,最终部署还是固定在224。

训练中断是个反复出现的事,我后来写了个自动保存脚本,每两个epoch存一次checkpoint,包括模型权重、优化器状态和当前epoch数。有一次跑ResNet50到第86个epoch时显卡驱动崩了,直接恢复到第84个epoch继续跑,损失了4个epoch但保住了整个训练成果。这种习惯强烈建议大家养成。

3.3 三模型对比结果分析

测试集选取和训练集严格分开,保证同一物种的不同个体不会同时出现在两边。最终指标对比如下:

模型参数量Top-1准确率平均推理耗时模型体积
VGG16约1.38亿90.4%380ms约528MB
ResNet34约2160万91.8%150ms约87MB
ResNet50约2560万93.2%210ms约102MB

从表里能看出几个规律:VGG16准确率最低却最耗资源,参数量和模型体积都垫底;ResNet34体积小且速度最快,适合本地部署;ResNet50在精度、速度、体积三者之间取得最优平衡。单类别的识别准确率差异更有意思,背景类和三色分明的动物类(如东北虎、雪豹)准确率最高,形态相似的动物(岩羊和盘羊、家犬和狼)则容易混淆,这个后面在排查章节细说。

这套对比数据写进技术文档特别好用,评审时别人问为什么不用更花哨的网络,直接甩这张表就够了。

4. Django部署与Web系统实现

4.1 系统架构与组件划分

模型训练好只是第一步,要真正让巡护人员用起来,还得套一个Web外壳。整个系统采用Django + Django REST Framework做后端,前端是原生的HTML + JavaScript + CSS,模型推理服务单独封装成一个模块,通过Python函数调用,不额外起独立的推理服务进程。整体架构分三层:数据层负责存储用户上传的图片和历史识别记录;业务层处理识别请求、调用模型、解析置信度;展示层提供上传入口、识别结果查询和物种信息展示。

数据库选了SQLite起步做本地验证,后来因为识别记录涨得快换成了PostgreSQL。Django自带的admin能直接管理识别历史、用户、模型版本,对项目管理和技术文档截图都非常方便。这里我把模型命名为"model_card",每个模型配了一条记录,包含网络类型、训练时间、准确率、当前是否启用,方便后续扩展新模型做在线升级。

4.2 模型导出与推理接口封装

PyTorch训练好的权重文件不能直接给Django用,一个是体积问题,另一个是每次推理都要重新构建模型结构容易出错。我在部署前把模型导出为ONNX格式,ONNX相当于模型格式的"通用语言",导出后可以用ONNX Runtime加速推理,用CPU也能跑得动,这样部署环境就不用强制装显卡驱动。

导出过程有个坑:如果模型里使用了动态尺寸输入,导出时会提示哪些维度是dynamic的。我只固定了输入尺寸224x224,所以导出很顺利,文件的推理时间甚至比PyTorch原生快了不少,ResNet50单张图在CPU上只要500毫秒左右,GPU环境下约180毫秒。导入Django时只要加载ONNX会话,传入经过预处理的图像数组,就能拿到输出概率和标签映射。

推理接口的封装路径要注意,我写了个独立的inference.py模块,里面定义了一个predict_image(file_path)函数,内部完成读图、缩放、归一化、推理、置信度解析全流程。归一化参数从训练时就固定好,标准Imagenet均值[0.485, 0.456, 0.406]和方差[0.229, 0.224, 0.225],这个参数必须写进技术文档,否则别人复现时预处理不一致,模型效果会断崖式下降。

4.3 Django后端与API设计

后端我用了两个核心接口:一个是POST /api/recognize,接收multipart格式的图片上传,返回识别结果JSON;另一个是GET /api/history,分页返回历史记录。前端页面直接调这两个接口。识别接口内部有超时控制,单个请求超过15秒就主动返回错误,防止大图或异常图片一直占着worker线程。

上传前前端页面做了两个约束:图片格式限定为JPG、PNG、BMP,文件大小限制在10MB以内,超过10MB前端直接拒绝,后端也做了同样的校验。双端校验看起来冗余,实际能挡掉很多奇怪的请求。上传后的图片按日期存储,比如media/2024/06/01/abc.jpg,同时生成一条识别记录,记录内容包括预测类别、置信度、上传时间、图片路径。

Django配置还做了几个关键点:关闭DEBUG模式之后要正确配置STATIC_ROOT和MEDIA_ROOT,否则静态资源和上传文件全是404;ALLOWED_HOSTS按实际部署域名填写;用Gunicorn作为WSGI服务器,配合4个worker进程,单worker并发数设为8。这套配置压测下来单机支持约20个并发识别请求,基本满足现场使用。

4.4 前端识别页面设计

前端页面核心逻辑不复杂:用户选择图片后先在Canvas里压缩预览,确认后通过FormData发送到后端,收到JSON后把识别结果展示出来。页面里每个动物类别配了一张信息卡片,内容包括物种名、科属、保护级别、分布区域和识别置信度,这些信息放在一个静态JSON文件里,修改时不用重新发布代码。

识别结果展示用了一个小技巧:置信度超过70%的显示为绿色"识别成功",30%到70%之间显示为黄色"置信度较低,请人工确认",低于30%直接显示"无法识别,请检查图片质量"。这样做不是为了做花哨效果,而是让使用方明确知道哪些结果可以直接采信,哪些需要专业人员复核。识别系统在野保场景里是辅助工具,永远不能完全替代人判断。

页面加载性能上,我前端用CDN加载Bootstrap样式,把本地图片压缩到500KB左右再上传,整个识别交互流程实测6秒以内完成。对巡护员来说,这个速度在野外环境下是可以接受的。

5. 常见问题排查与避坑实录

5.1 训练阶段:过拟合与验证集失真

项目早期踩过最大的坑是验证集准确率虚高,训练集准确率直接冲到99%。原因是我在预处理时对验证集也做了随机数据增强——本意是增加鲁棒性,结果验证集每次评估的输入都不一样,模型记忆住了训练集模式,验证结果完全失真。修掉之后验证集和测试集都只做归一化和Resize,不做随机扰动,评估指标瞬间回归正常水平。

另一个过拟合信号是loss曲线:训练集loss持续下降,验证集loss在第40个epoch开始反弹。解决手段用过Dropout、权重衰减(设为5e-4)、Label Smoothing(参数0.1)三件套,效果最明显的还是Label Smoothing,它让模型对正确类别不要过度自信,泛化能力提升不少。

5.2 类别混淆:岩羊与盘羊分不开

模型在岩羊和盘羊两类上错得最多,一开始我以为是自己标注出了问题,翻样本发现两类动物外形本来就接近,而且红外照片里经常只拍到侧影或背影。解决思路有三个方向:一是在训练集里刻意增加只拍到局部、远距离模糊样本的比例,让模型学会用角形和体型轮廓区分;二是检查有没有重复图片混在训练集和验证集里,结果还真查出了上百张重复帧;三是用条件混淆矩阵定位出错样本,把错误样本集中抽出来人肉标注了一遍。

这套排查流程做完,岩羊和盘羊的类间混淆率从18%降到了7%。这个案例特别适合写进技术文档当错误分析章节,比单纯列指标有说服力得多。

5.3 显存不足与训练效率

ResNet50跑大batch时显存不够,试过batch size = 32直接OOM。解决办法是开启PyTorch的梯度累积,batch size降到16,每2个batch累积一次梯度,等效batch size还是32。另外启用了混合精度训练,显存占用又降了不少,训练速度反而提升了30%。

多人共用GPU时还有更头疼的情况:别人的任务把显存抢占了,我的训练直接崩。后来养成了先跑一小时看稳定occupancy再挂长任务的习惯,也学会了用工具查进程和显存占用,这个对实验室环境的项目特别实用。

5.4 部署阶段:推理超时和内存泄漏

Django部署后发现两个诡异问题。第一个是偶发的推理超时,排查半天定位到原因:上传的图片有时带有GPS信息,读取图片时EXIF解析卡住,训练时预处理用的是干净图片从来没遇到过。解决办法是读取时直接用ImageFile.verify()解析基础格式,再用convert("RGB")重新编码,把EXIF所有附加数据全部剥掉。

第二个是内存泄漏,连续跑了几百个请求后内存涨了快1GB。根因是推理模块里某个全局缓存列表没清理,每次读取图片把像素数据追加进去。改成使用后立即释放引用,并且定期跑内存分析脚本,问题解决。这些部署坑都是训练时根本不会暴露的,建议所有想做到上线能用的项目都提前踩一遍。

6. 技术文档怎么写才完整

6.1 技术文档的目录结构与内容定位

这个项目的技术文档最后整理成了五个章节:第一章数据说明,记录了数据来源、标注规范、类别映射表、增强参数;第二章模型设计,详细描述了三选模型的理由、网络结构图、参数量对比;第三章训练配置,覆盖了超参数、优化器、学习率调度、训练日志和评估结果表格;第四章部署手册,写清楚了Django环境搭建、模型导出命令、接口文档、前端配置说明;第五章迭代记录,记录了每次改进版本的效果差异和踩坑过程。

写技术文档的关键心得是:每一处参数都必须能回答"为什么是这个值"——比如batch size为什么是32而不是64,因为设备显存限制;学习率为什么用3e-4,因为Adam家族在迁移学习里这个值比较稳;Label Smoothing为什么用0.1,因为消融实验里0.1比0.2效果好。没有解释的参数等于没写,复用者在调整时完全不知道从哪里下手。

6.2 记录训练日志的重要性

训练过程中的每个epoch,我把loss、准确率、学习率、显存占用全部记录到CSV文件,顺便截了几张训练曲线图放进技术文档。当时觉得是流水账,后期排查问题时感谢当时的自己。比如最后发现验证集准确率下降的拐点,就是在日志里对比出batch size调整后的影响。没有日志,调试永远只能靠重新训练一遍碰运气。

项目最终被某实验室拿去复用做物种识别扩展实验,直接把我的技术文档当作上手手册,省去了大量沟通成本。所以说把文档写好的投入产出比非常高,本质上是在帮未来的自己省时间。

7. 最终部署效果与个人复盘

系统正式跑起来之后,单张图片从上传到展示结果全程约3秒,识别准确率在抽样测试里保持在93%上下。使用方反馈最好用的功能其实是置信度分级提示,它让非专业操作员在大批量巡护照片里快速筛出需要人工复核的那一小部分,省的力气最多。后面我们又扩展了一个思路:对连续帧序列做时间维度的二次过滤,同一地点一分钟内的多张重复识别可以自动合并成一条监测记录,这对红外相机的批量数据尤其有用。

我个人的体会是:做深度学习的野保识别项目,模型的精度和网络结构真的只是地平线,真正拉开项目差距的是数据质量、工程细节和面对一堆糟心事时能不能沉住气一层层排查。如果你正在计划做类似的图像识别系统,先把数据规范化、把三模型对比跑完、把部署链路打通,每往前走一步,你都会感到后面的事情没有那么复杂。这套流程我完整走了一遍,今后做任何图像分类项目,都会按这个节奏来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进