搞野生动物监测的同学应该都有这种体会:真正挡住你的往往不是模型结构,而是数据。训练集不是缺类别就是缺数量,标注质量参差不齐,想换骨干网络还得重构整套数据管线,一套折腾下来,精力全耗在“搬砖”上,模型反而没时间调。
这个项目做的就是一件事:把“动物识别数据集 + Pytorch目标检测训练流程”打包成一套开箱即用的完整方案。数据集总量超过26,000张,覆盖多个常见野生动物类别,标注格式统一为COCO JSON,代码层同时支持ResNet、MobileNet两类骨干网络,配套详细的训练、测试、推理文档。不管你是要做野生动物保护AI监测,还是单纯想跑通一次从数据到模型的目标检测全流程,这套东西都能直接拿来落地。
我花了大量时间整理和验证这套流程,把容易被忽略的坑都填平了。下面会从数据集设计、标注处理、模型选型、训练参数、代码结构到踩坑实录,一层层拆开讲清楚,保证你照着操作能少走一半弯路。
1. 项目整体思路与核心设计
1.1 为什么要把数据集和训练代码绑在一起
市面上其实有不少公开的动物识别数据集,但拿来直接做工程,问题一堆。最常见的是标注格式混乱,有的给XML,有的给TXT,有的干脆只有类别标签没有框;其次是类别体系不统一,同一物种在不同数据集里名称都不一样,合并起来要写一堆脚本;最头疼的是有些数据集来自网络抓图,分辨率参差不齐、水印杂物遍地,模型训出来泛化能力很成问题。
所以我决定自己整理一套“干净”的数据集,并且把训练代码一起配套好。数据侧统一用COCO JSON格式,这是Pytorch生态里支持最好、转换成本最低的标注格式之一;代码侧把数据加载、模型构建、训练循环、评估推理拆成独立模块,换骨干网络只需要改一行配置。这样用户拿到压缩包之后,不需要自己写数据适配层,也不用纠结“这个数据集该配什么代码”,解压、配置、开训三条命令搞定。
设计思路上坚持一个原则:数据优先,模型其次。模型结构再先进,数据不过关也白搭。所以数据集构建阶段花了最多时间,包括类别筛选、图片筛选、标注清洗和分布校验。因为深度学习的上限由数据决定,训练只是逼近这个上限的过程。
1.2 目标场景与适用人群
这套方案的服务对象非常明确:
野生动物保护AI监测是最核心的场景。具体来说就是红外相机部署在野外,持续拍摄照片或视频,AI模型负责从海量图像中识别出动物并框出位置,辅助生态研究人员统计物种数量、分析活动规律。这个场景对模型的诉求很典型:类别要覆盖常见物种、小目标检测能力要强(动物离相机远时在画面里就是一小块)、单张推理速度要够快(边缘设备资源有限)。
计算机视觉模型训练入门是另一个重要场景。很多刚接触目标检测的朋友,最大的障碍就是找不到一份“能直接跑起来”的数据集和代码。这个项目提供了完整闭环:数据怎么组织、模型怎么建、损失怎么算、指标怎么评,全链路可见。用这份数据练手,比用MNIST或CIFAR做分类有意思得多,也更接近真实业务。
适合的人群包括:生态保护领域的技术人员、做边缘AI部署的工程师、计算机视觉方向的学生研究者。前两类人更关注数据集质量和部署效果,后一类人更关注代码结构和训练流程。
1.3 为什么选ResNet和MobileNet作为骨干网络
骨干网络的选择不是随便定的,背后是权衡了精度、速度、部署难度之后的折中方案。
ResNet是目前迁移学习最成熟的骨干网络,Pytorch官方预训练权重直接可用。ResNet50在ImageNet上表现稳定,作为检测模型的backbone,特征提取能力有保障。训练时加载预训练权重做finetune,收敛速度快,小数据集上也不容易过拟合。缺点是模型体积大、计算量大,不适合跑在嵌入式设备上。
MobileNet系列(比如MobileNetV3)是给边缘设备准备的。深度可分离卷积把计算量压下来,参数量只有ResNet的零头,在CPU上也能跑到可用的帧率。精度会比ResNet略低,但在红外相机画面这类相对简单的场景里,差距不大。对部署在野外低功耗设备上的监测系统来说,MobileNet几乎是默认选择。
这套代码里两种骨干网络都做了实现,并且统一了接口,切换时只要改配置文件里的一个字段。比如需求是离线分析大量图片,选ResNet50跑高精度;需求是边缘盒子实时监测,选MobileNetV3跑低延迟。这种兼顾精度与部署的灵活性,就是为什么初始设计时就把“支持多种骨干网络”当成硬性要求。
经验之谈:不要迷信“更先进的骨干”。实际项目里往往是速度和精度的平衡,两种骨干够用、稳定、权重好找,这对工程落地非常关键。
2. 数据集的构建与标注细节
2.1 类别体系与图片来源分析
整个数据集一共15个类别,覆盖了中国大部分地区红外相机能拍到的常见野生动物。类别包括白鹭、苍鹭、野猪、猕猴、赤狐、黄鼠狼、豹猫、狗獾、雉鸡、野兔、小麂、水獭、松鼠、竹鼠和鼠类。为什么是这些类别?因为我在设计的时候参考了某野生动物保护项目的实际需求,这15类是该区域红外相机拍到的频次最高的物种,也是在监测报告里出现最多的对象。
类别的粒度选择上做了两个考量:一是不能太粗,比如“鸟类”一个类涵盖所有鸟,这对生态监测没有意义,研究人员需要知道具体是哪个物种;二是不能太细,比如把鼠类分成四五种,标注难度飙升不说,模型分类的置信度也会明显下降,因为近缘物种长得太像。所以鼠类就作为单一类别保留,这对实际业务来说是合理的取舍。
图片来源主要由三部分构成:红外相机拍摄照片、单反/手机人工拍摄照片、公开图库的合法授权图片。红外相机照片占比大约六成,风格偏暗、带红外反光、动物远距离小目标,这是模型最终要面对的典型场景;人工拍摄照片占比三成,光线条件好、目标尺寸大,帮助模型学到更清晰的物种特征;公开图库占比一成,用来补充一些红外照片里角度不足的样本。
注意:构建数据集时,图片版权问题是红线。公开网络抓图必须确认授权,不能直接拿别人的图做商用。我这个项目里用的公开图全部来自可自由使用的图库,这一点在做同类工作时值得特别注意。
2.2 标注规范与质量控制流程
标注质量直接影响模型上限,这是数据集构建中最费精力的部分。标注规范上我做了几条硬性规定:
边界框贴合度:目标框必须紧贴动物主体,四肢伸展开时包含四肢,蜷缩时可以适当内收。框不用包含尾巴尖,如果尾巴和主体分得太开,宁可舍弃尾巴。这条规则保证了正样本的一致性,减少回归头的无效学习。
遮挡处理:动物被树干、草丛遮挡超过一半,或者只露出一个头且没有明显身体特征,直接不标。遮挡面积小于一半的,正常标注,但框必须覆盖可见部分。红外相机画面里大量动物是半遮挡状态,这条规则保证了数据不是“只挑好看的标”。
小目标标注:画面里动物小于32×32像素的,训练时几乎学不到有效特征,直接标注会引入大量低质量正样本。所以这类目标统一不标,宁可模型漏检,也不能让模型学到错误特征。
标注完成后,我做了两轮交叉校验。第一轮由另一个标注者独立复标100张图,对比两轮结果的IoU,低于0.7的图片重新讨论修正;第二轮用预训练模型对全量数据做一次预测,把预测框和人工标注框的重合度差异大的样本拉出来人工复核。这两轮校验花的时间不少,但把低级错误清掉了大半。
2.3 数据划分与样本均衡策略
数据集一共26,500多张,划分为训练集、验证集、测试集三部分。划分比例接近7:2:1,具体是:训练集18,600张左右,验证集5,300张左右,测试集2,600张左右。
划分时不是随机切,而是按拍摄场景和设备维度切。因为同一批设备的照片光线、角度、背景都很相似,如果同一设备的照片同时出现在训练集和测试集,模型等于见到了“开卷考试”的题目,测试分数会虚高。按设备划分之后,测试集里的照片来源在训练阶段从未见过,评估结果更能反映真实泛化能力。
类别分布方面,各物种数量并不均匀,这也是真实业务数据的常态。赤狐、野猪这类常见物种样本量在2,500张以上,水獭、豹猫这类稀有物种只有800张左右。类别不平衡会造成稀有类学不好,我的处理方案是:
- 加权采样:训练时给样本量少的类别更高的采样概率,让每个epoch里各类别出现频率更接近均衡。
- Mosaic增强:把4张图拼成一张训练图,增加单张图的目标密度,提高小目标样本的有效利用。
- Hard Negative Mining:验证集上稀有类别AP明显偏低时,从训练集里筛选出该类别的困难样本(错检、漏检的图)单独加强训练。
注意:测试集分布要贴近真实场景。我特意保留了原始数据中1/10的“脏数据”在测试集里,比如过暗的图、虚焦的图、被雨水遮挡的图。这样测试集分数虽然会难看一点,但更有参考价值。
3. 模型选型与训练配置详解
3.1 骨干网络的选择逻辑与适配方案
代码里同时实现了ResNet系和MobileNet系骨干网络。具体做了以下适配:
ResNet系列:用Pytorch官方torchvision库里的resnet50作为主力backbone,也支持resnet34和resnet101。加载ImageNet预训练权重后,去掉最后一层全连接分类器,把stage4输出的特征图作为检测头的输入。ResNet101理论上精度更高,但训练显存占用多出近一半,实际效果提升有限,所以默认配置是ResNet50。
MobileNet系列:用mobilenet_v3_large作为轻量级的默认选择。相比ResNet50,参数量约为1/8,单张推理速度快3到5倍。MobileNetV3的倒残差结构在低算力平台上非常友好,支持CPU推理,对没有GPU的部署环境很实用。
特征金字塔:骨干网络不同层输出的特征图尺度不同。深层特征图语义信息丰富但分辨率低,适合识别大目标;浅层特征图分辨率高但语义弱,适合定位小目标。代码里实现了FPN(Feature Pyramid Network)结构,把P3、P4、P5三个尺度的特征图融合,让不同大小的目标都在合适的层上被检测出来,这对红外场景里的小动物检测至关重要。
3.2 训练参数配置与调优建议
训练配置上,我踩过不少坑,最终形成了一套相对稳定的参数组合。这里直接把我验证过的配置贴出来:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 输入尺寸 | 640×640 | 分辨率适中,兼顾小目标与显存占用 |
| Batch Size | 16 | RTX 3060级别显卡可用,显存小就降为8 |
| 初始学习率 | 0.005(SGD)/ 0.0005(AdamW) | 骨干网络冻结层用更小学习率 |
| 学习率策略 | 余弦退火CosineAnnealing | 比StepLR更稳,后期收敛效果好 |
| 训练轮数 | 80~120 epoch | 类别多、数据量大时取上限 |
| 权重衰减 | 5e-4 | 防止过拟合 |
| 优化器 | SGD + Momentum=0.9 | finetune场景比Adam更稳 |
| Warmup | 3 epoch线性预热 | 避免模型前期震荡 |
| 数据增强 | Mosaic、随机擦除、HSV扰动 | 有效提升泛化能力 |
为什么不要直接套Adam?迁移学习场景里,加载了预训练权重的骨干网络已经有很好的特征表达,Adam自适应学习率反而容易在初期破坏这些参数。我的经验是SGD+momentum配合较小的初始学习率,finetune效果更好,收敛也更稳定。当然后期想快速收敛,改用AdamW微调已加载的检测头也是可以的。
学习率warmup的必要性:刚初始化时模型输出不稳定,直接用大学习率容易让loss突跳到NaN。前3个epoch从0线性增加到目标学习率,可以让训练平稳起步。这个细节看起来不起眼,但在目标检测任务里经常是“能不能训起来”的分水岭。
3.3 损失函数与评价指标
检测模型包含分类和回归两个分支,所以损失函数也是两部分:
- 分类损失:用Focal Loss。为什么不用普通交叉熵?因为检测任务里背景框远多于前景框,正负样本极端不平衡。Focal Loss通过调制因子降低易分类样本的loss权重,让模型把注意力集中在难分类的正样本上。参数
alpha=0.25、gamma=2.0是按常见经验值设置的。 - 回归损失:用Smooth L1 Loss(或CIoU Loss)。Smooth L1在误差小时候梯度平缓,误差大时梯度恒定为1,受异常框坐标影响较小。CIoU考虑框的重叠面积、中心点距离和长宽比,收敛更快,但数值稳定性需要额外留意。
评价指标用COCO标准里的mAP@IoU=0.5:0.95,这表示在IoU阈值从0.5到0.95以0.05步长递增的所有情况下的平均AP。这个指标比较严格,一个合格的模型在这个指标上通常不会特别高,所以别拿它和分类准确率对比。实际业务里,还要单独看小目标的AP(AP_small),因为野生动物监测对小目标的检测能力往往决定了系统好不好用。
4. 代码结构与完整训练流程
4.1 项目文件组织与核心模块说明
代码结构按功能拆分为清晰的模块。拿到压缩包后先看整体目录,按下面这个框架来理解就不会迷路:
animal-detection/ ├── configs/ │ └── config.yaml # 可修改的配置:骨干网络、训练参数、路径 ├── datasets/ │ ├── data_prepare.py # 数据集检查和预处理 │ ├── coco_dataset.py # 自定义Dataset类,加载COCO格式 │ └── transform.py # 训练/验证数据增强pipeline ├── models/ │ ├── backbone.py # ResNet/MobileNet骨干网络构建 │ ├── fpn.py # 特征金字塔融合模块 │ └── detector_head.py # 分类头+回归头 ├── utils/ │ ├── losses.py # Focal Loss + Smooth L1/CIoU │ ├── metrics.py # mAP计算工具 │ └── visualize.py # 预测结果可视化 ├── train.py # 训练主流程 ├── evaluate.py # 测试集评估 ├── inference.py # 单张/批量推理脚本 └── requirements.txt # 依赖库列表设计上最关键的模块是backbone.py和coco_dataset.py。backbone.py里把ResNet和MobileNet的构建逻辑封装成同一个接口,输入参数指定backbone_name即可切换;coco_dataset.py里则把图片读取、标注解析、数据增强串成一条pipeline,并且实现了多线程异步加载,训练时GPU不会因为等数据而空转。
4.2 数据加载pipeline的关键实现
自定义Dataset类这块,几个细节值得重点关注:
标注格式解析。COCO JSON的标注数据是“图片-标注”分离的结构,需要写一个转换函数把annotations里的image_id映射到对应的图片路径。这一步看着简单,但处理26,000多张图片时,用字典批量构建索引比逐条遍历高效得多。
数据增强的触发逻辑。训练集和验证集的增强策略不同。训练集用Mosaic、随机翻转、尺度扰动、颜色抖动;验证集只做Resize和归一化,保证评估的稳定性。这个区别一定要在代码里严格分开,否则验证集结果波动会很大。
归一化参数。Pytorch预训练模型默认用ImageNet的均值(0.485, 0.476, 0.406)和标准差(0.229, 0.224, 0.225)做归一化。如果加载的是ImageNet预训练权重,这个参数不能改,改了等于把骨干网络扔进了一个它不熟悉的输入分布里。
核心加载逻辑参考:
class AnimalDataset(Dataset): def __init__(self, img_dir, ann_file, transform=None, is_train=True): self.img_dir = img_dir self.transform = transform self.is_train = is_train # 解析COCO标注文件 with open(ann_file, 'r', encoding='utf-8') as f: coco_data = json.load(f) self.images = {img['id']: img for img in coco_data['images']} self.annotations = coco_data['annotations'] # 构建image_id -> 标注列表 的索引 self.img_to_anns = {} for ann in self.annotations: self.img_to_anns.setdefault(ann['image_id'], []).append(ann) self.img_ids = list(self.images.keys()) def __len__(self): return len(self.img_ids) def __getitem__(self, idx): img_id = self.img_ids[idx] img_info = self.images[img_id] img_path = os.path.join(self.img_dir, img_info['file_name']) image = cv2.imread(img_path) image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # 提取标注框和类别ID boxes, labels = [], [] for ann in self.img_to_anns.get(img_id, []): x, y, w, h = ann['bbox'] boxes.append([x, y, x + w, y + h]) # 转为xyxy格式 labels.append(ann['category_id'] - 1) # 类别从0开始 boxes = torch.as_tensor(boxes, dtype=torch.float32) labels = torch.as_tensor(labels, dtype=torch.int64) # 做数据增强 if self.transform: image, boxes, labels = self.transform(image, boxes, labels) return image, boxes, labels4.3 训练、评估与推理的完整流程
拿到代码之后的完整使用流程是这样的。我先以训练为例说明:
第一步:准备环境。安装Pytorch 1.10以上版本、torchvision、pycocotools、opencv-python等依赖,文件在requirements.txt里列好了。建议直接用conda建一个干净的环境,避免依赖冲突。
第二步:修改配置。打开configs/config.yaml,按自己的需求修改三类配置:训练资源相关(batch_size、设备、线程数)、模型相关(backbone选择resnet50还是mobilenet_v3_large、类别数量设为15)、路径相关(数据集根目录、输出目录)。这是唯一需要手工修改的文件,其他代码不需要动。
第三步:启动训练。命令行执行:
python train.py --config configs/config.yaml训练流程会自动加载预训练骨干权重,打印每个epoch的loss和验证集mAP,并在每个epoch结束后保存一次最佳模型权重。日志默认输出到output/logs/目录,用TensorBoard也能方便地查看训练曲线。
评估和推理更加轻量。评估时运行:
python evaluate.py --checkpoint output/best_model.pth --data val推理时运行:
python inference.py --image demo.jpg --output result.jpg推理脚本默认加载模型后做一次forward,输出标注了边界框和类别置信度的图片,同时打印识别结果到终端。我通常用这个脚本快速验证模型在不同场景下的真实表现,比只看mAP数值更有感觉。
5. 常见问题与调参避坑实录
5.1 训练Loss不下降或直接NaN
这是最常遇到的问题。Loss从一开始就跳到NaN,十有八九是学习率过大。加载预训练权重的骨干网络本身输出已经很稳定,但新的检测头是随机初始化的,如果整体学习率设太高,回归头的预测值会在前几步产生极大的梯度,把loss炸掉。解决办法是:先给新初始化的层设置比骨干网络大10倍的学习率(骨干网络0.0001,新层0.001),并用warmup把前几个epoch的学习率从小往大拉。我一开始直接用0.01的学习率,模型直接发散,换成0.005SGD并用3个epoch warmup后,loss曲线就非常平滑了。
还有一种情况是loss一直在某个区间抖动不下降,这通常是数据增强度过强或者Mosaic概率太高。我实验时把Mosaic概率调到0.8,模型迟迟不收敛,降到0.3后大约30个epoch就有了明显下降。增强的目的是提高泛化能力,但增强过猛会让模型连训练集都学不好,这个度需要根据自己的数据反复试。
5.2 小目标漏检问题
野生动物监测里,动物在画面里往往只占很小一块区域。模型对这类目标经常漏检,mAP数值也主要被小目标AP拖后腿。我的排查和优化思路是这样:
先确认是不是输入分辨率不够。原来用416×416输入,小目标特征在深层几乎消失,提升到640×640后AP_small直接涨了5个点左右。如果显卡显存允许,甚至可以考虑768×768。
检查是否启用了FPN多尺度层。如果代码里只用骨干网络最后一层特征图做检测,小目标信息在深层已经损失殆尽。FPN结构是必须的,P3层服务小目标,P5层服务大目标,缺一不可。
增加小目标的训练样本权重。在采样时按目标面积赋予权重,面积小于32×32的样本在Mosaic增强中保留率更高。这条在代码里实现起来不复杂,但对小目标AP的改善很明显。
5.3 红外图像和自然图像风格差异大
红外相机照片的特点是:单通道灰度(或者伪彩色)、亮度偏低、对比度不强、经常有红外反光。用自然图片训练出来的模型直接推理红外图,效果会打折扣。我的处理方案是:
数据层面:把红外照片单独整理成一个subset,在训练时以一定比例混入,比例大概在30%。这样模型既能学自然的颜色特征,也能适应红外的灰度风格。
增强层面:对红外照片额外做灰度化、亮度抖动、对比度增强。相当于用数据增强模拟从自然光到红外的风格迁移。
测试层面:推理前先做预处理,把红外图像做一次CLAHE自适应直方图均衡,提升对比度后再送进模型。这一步单独跑一个脚本,也能做成预处理管线的一部分。
5.4 显存不够怎么办
训练目标检测模型对显存要求不低。RTX 3060 12G带ResNet50+640×640输入,batch_size设16勉强能跑,但如果用了更强的主干或者更大的分辨率,OOM几乎是必然的。这里有几个实用技巧:
梯度累积:把batch_size从16降到4,梯度累积4步再更新参数,等效于batch size 16的训练效果。代码里加一个accumulation_steps参数就行。
混合精度训练:Pytorch自带的torch.cuda.amp可以把显存占用降到原来的60%左右,精度损失微乎其微。我实测ResNet50+640输入+batch16,开启AMP后显存占用从11.2G降到7.6G。
关掉梯度对输入图的存储:数据增强时如果开启了对输入图像的梯度追踪,显存会额外增加。确保requires_grad=False,只对模型参数保留梯度。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 推荐解法 |
|---|---|---|
| Loss前期NaN | 学习率过大 | 降低学习率、开启warmup |
| Loss不降 | 增强过猛/学习率过低 | 降低Mosaic概率、调高学习率 |
| 小目标漏检多 | 输入分辨率太低 | 从416提到640或768 |
| 验证集波动大 | 验证集增强未关闭 | 验证只做Resize+归一化 |
| 单类AP特别低 | 类别样本不均衡 | 加权采样+困难样本挖掘 |
| 推理速度快但精度差 | 骨干网络太轻量 | 换ResNet50或加深FPN层 |
| 显存不足 | 分辨率/批次太大 | 梯度累积+混合精度训练 |
6. 项目扩展与部署实战思考
6.1 从训练到边缘设备部署
训练完之后,真正到业务落地还有一个部署环节。目前代码里提供了两种部署路径:
服务端GPU推理:适合对实时性要求较高的场景,比如管理站内批量分析红外相机回传图片。直接用Pytorch导出模型,配合CUDA推理,RTX 3060上ResNet50跑640×640输入大概能到30ms一帧,完全够用。如果需要更高吞吐,可以把模型转换为ONNX格式,用TensorRT做优化,推理速度还能再翻一倍。
边缘设备CPU推理:MobileNetV3就是为这个准备的。我测试过在树莓派级别的设备上,去掉预处理和NMS时间,MobileNetV3单帧推理时间在300ms左右,可以做到准实时分析。部署时把模型导出为ONNX格式,用ONNX Runtime加载,不需要Pytorch环境也能跑。代码里没有直接提供导出脚本,但搞清楚流程之后自己写一个很简单,核心就是把模型设置为eval模式并固定输入尺寸。
6.2 数据集的后续扩充方向
数据集目前覆盖15个类别26,000多张,但野生动物保护的需求远不止这些。后续扩充可以从三个方向做:
类别维度:增加两栖类、爬行类物种,比如各种蛙类、蛇类。这些在红外相机里也经常出现,但目前不在数据集里。
场景维度:目前数据主要集中在山林环境的红外相机影像,可以补充湿地、农田、城市边缘等不同生境的图片,让模型在不同部署环境里都保持稳定的识别能力。
任务维度:从单纯的检测延伸到个体识别,比如通过花纹特征区分不同的豹个体。这是野生动物保护里很前沿的方向,但需要更大的数据量和更精细的标注。
6.3 用这套方案做迁移学习的技巧
最后分享一个做迁移学习的技巧。如果你需要识别的是数据集之外的物种,不要从零训练。我的做法是:
先加载当前动物检测模型的权重作为预训练模型,然后冻结骨干网络的前几层,只训练后面几层和检测头。需要标注的新类别数据只要几百张就能完成微调,准确率能达到80%以上。这比直接从头训练省了10倍以上的数据和时间。实践证明,在相近的视觉任务之间做迁移学习,效果远好于从ImageNet直接迁移,因为骨干网络已经学会了“动物长什么样”的先验知识。
训练集里如果新物种的样本不够,还可以用“仅分类、不回传框回归梯度”的方式做soft label训练,把检测模型当作特征提取器用。这些扩展思路代码里虽然没有内置,但理解了整个数据流和训练流程之后,自己动手改并不是难事。
我个人在实际操作中最深的体会是:数据集本身就是最好的“模型”。把数据整理干净、类别定义清楚、标注质量把控好,后面所有的事情都会变得异常顺畅。这个项目前期的数据工作占了80%的时间,但正因为底子打得扎实,训练阶段几乎没出过什么重大问题。希望这份整理好的数据集和方法论,能让正在做动物识别或目标检测的你少走我走过的弯路。