之前多数项目都直接放在Linux服务器上跑,这次需要把过程挪到一台Win10电脑上:训练要用GPU,测试却只能走CPU。听起来有点“分裂”,但做下来发现这套组合其实很顺手。本文就把这个过程的完整流程、关键配置和踩过的坑整理一遍,方便有类似需求的朋友直接参考。
先说结论:Nanodet这类轻量级目标检测模型,天然适合这种“GPU训练、CPU测试”的工作流。训练阶段用GPU加速反向传播,把迭代验实验的时间压到最短;测试阶段用CPU模拟真实部署环境,得出的推理速度才更有参考价值。整个过程不需要太高端的硬件,也不需要跨平台迁移,在Windows本地就能完整跑通。
1. 项目整体设计思路:为什么要拆成“CPU测试、GPU训练”
1.1 先聊清楚Nanodet是什么
Nanodet是一个轻量级的anchor-free目标检测模型,核心卖点是模型参数量小、计算量低。同样是检测任务,传统检测模型的backbone可能需要几千万甚至上亿参数,而Nanodet常用ShuffleNetV2这类轻量backbone,整个模型参数量压缩到百万级别。这就带来一个很关键的后果:它在CPU上也能跑出可用的速度。
锚框方面也省心。传统模型需要手工设计锚框尺寸和比例,碰上小目标或者长宽比悬殊的物体,还得反复调参。Nanodet直接走anchor-free路线,把物体检测变成“中心点+边框回归”的问题,配置里少了一大块锚框参数。这点在做自定义数据集时非常省事,这也是当初没有继续用系列工具的原因之一。
1.2 “双模式”工作流的合理性
开发过程中,训练和测试其实承担着不同的任务。
训练阶段,大部分时间花在梯度计算和参数更新上,这是典型的并行计算场景。GPU动辄几千个核心同时工作,一个小数据集在CPU上可能要跑两三个小时,在GPU上十几分钟就结束。如果硬要用CPU训练,试错成本会高到让人崩溃,每调一次学习率都要等半天。
测试阶段则完全相反。部署环境通常没有高端GPU,甚至就是一台普通办公电脑。模型最终要在这些设备上运行,那测出来的速度才有参考价值。如果测试也安排在GPU上,跑出来的几十毫秒延迟到了真实部署环境会变成几百毫秒,误导性非常强。
所以“GPU训练、CPU测试”其实是性价比最高的方案:GPU管开发速度,CPU管部署真实性。
1.3 完整流程串一遍
这里直接给出我实际跑通的路径,后面所有章节围绕这条线展开:
- 在Win10上安装Python环境和Pytorch(CPU和GPU版本共存或按需切换)
- 采集并标注自定义数据集,整理成VOC格式
- 把VOC标注转换成Nanodet需要的COCO格式
- 修改配置文件,指定类别数、训练参数、设备类型
- 用GPU启动训练,监控loss和显存占用
- 训练结束后拿到权重文件,用CPU跑推理测试
- 记录性能数据,调整线程数等参数做进一步优化
这个过程下来,整个链路都清晰了,后面换数据集换场景也只是替换数据而已。
2. Win10环境搭建实操
2.1 我的这套Win10环境清单
先放硬件参考,方便对照:
| 项目 | 配置 |
|---|---|
| 操作系统 | Windows 10 22H2 |
| CPU | 六核十二线程,主频2.6GHz |
| 内存 | 16GB DDR4 |
| GPU | NVIDIA入门级独立显卡,6GB显存 |
| 硬盘 | 512GB SSD |
这套配置算是入门级,显卡型号别太在意,重点是6GB显存能跑得动大多数单模型训练任务。Nanodet的显存占用大概4GB左右,batch size设保守一点就能稳定跑。
软件层面我用了Anaconda管理Python环境,版本锁定3.8。这个版本对Pytorch和后续编译工具兼容性最好,不需要用更新的3.10或3.11,没必要去踩版本兼容的坑。
2.2 依赖安装顺序与版本匹配
环境搭建最忌讳想到什么装什么,顺序错了很容易出现“A依赖B,B依赖C,最后C和A打架”的情况。我按下面这个顺序装完没有再出过问题:
conda create -n nanodet python=3.8 conda activate nanodet pip install torch torchvision torchaudio --index-url 你的CUDA版本对应地址 pip install opencv-python pyyaml tqdm cython这里最需要留意的是Pytorch和CUDA的匹配关系。NVIDIA驱动会提供一个CUDA运行版本,但Pytorch官方是不是支持这个版本要看对应发布说明。实际操作中先查本机显卡驱动支持的CUDA版本,再选对应的Pytorch安装命令,就不会出现torch.cuda.is_available()返回False的情况。
常用的匹配关系可以参考:
| CUDA版本 | Pytorch支持情况 |
|---|---|
| 11.1 | 需要安装nightly或早期版本 |
| 11.3 | 对应Pytorch 1.10以上 |
| 11.8 | 对应Pytorch 2.x |
| 12.1 | 对应Pytorch 2.1以上 |
确认匹配关系后,安装Pytorch时优先用官方匹配命令,避免用默认的PyPI源装上CPU版。
另外Windows上有一个隐形的依赖:Visual Studio Build Tools。Nanodet部分C++扩展在Windows上需要编译,比如pycocotools或者NMS模块,没有编译环境会直接报Microsoft Visual C++ 14.0 is required。这个不是装Visual Studio全家桶,装Build Tools工具集就行。
2.3 环境自检这样做
装完依赖后,先跑一段代码确认环境状态:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.device_count()) print(torch.cuda.get_device_name(0))预期输出里torch.cuda.is_available()必须是True,否则后面训练时指定GPU会报设备不可用。如果返回False,优先检查是不是装了CPU版Pytorch,其次检查CUDA版本匹配。
3. 准备数据并拆解配置文件
3.1 把标注数据整理成VOC格式
我用标注工具按VOC格式整理了一个小数据集,名称叫模拟项目X,样本数量1000张左右,覆盖了5个类别。目录结构如下:
datasets/ VOCdevkit/ VOC2007/ Annotations/ image_001.xml image_002.xml JPEGImages/ image_001.jpg image_002.jpg ImageSets/ Main/ train.txt val.txt每个xml记录了一张图片里所有目标的位置和类别,train.txt里按行写图片名,如image_001,不带扩展名。这个格式是后续转COCO的输入,目录必须规范,不能随便改。
3.2 VOC转COCO的操作与原因
Nanodet数据加载内部走的是COCO格式,即使你只有VOC标注,也得先转换。转换工具在官方仓库的tools/dataset_converters目录下,按脚本里的说明填写源路径和目标路径执行。转换后会生成一个annotations目录,包含json标注文件和对应的图片文件名。
这一步容易踩的一个坑是类别编号对应关系。VOC格式的xml里标签名是字符串,转到COCO后变成数字id,这个映射顺序必须和后续yaml配置里的类别列表保持一致。转换完成后,打开json文件人工抽查一遍,确认categories里的类别顺序和配置一致,能少走很多弯路。
3.3 yaml配置文件的逐项拆解
Nanodet通过一个yaml文件管理几乎全部训练配置。这里展示一个简化后的实例:
save_dir: workspace/nanodet_x model: type: NanoDet backbone: name: ShuffleNetV2 head: num_classes: 5 data: train: dataset: name: XMLDataset image_path: datasets/VOCdevkit/VOC2007/JPEGImages label_path: datasets/VOCdevkit/VOC2007/Annotations test: dataset: name: XMLDataset schedule: epoch: 120 batch_size: 16 lr: 0.02 device: cuda逐项说明一下关键配置:
save_dir:模型输出目录,建议一个项目独立目录,命名改成项目名,不然多个项目共用会乱。head.num_classes:自定义数据集的类别总数,不包括背景。这个数必须和标注映射一致。data.train.dataset.image_path和label_path:训练数据路径,注意Windows下写成正斜杠或者双反斜杠,单反斜杠会被yaml当成转义符。schedule.epoch:训练轮数,小数据集100轮左右足够,数据集大就适当增加到200。schedule.batch_size:显存6GB下,16是个比较稳的值,8G显存可以开32,但也要看图片分辨率。schedule.lr:初始学习率,batch越大学习率可以适当调高,batch 16配0.02比较常见。device:训练时填cuda,测试时需要改成cpu。
4. GPU训练过程与参数监控
4.1 训练前的自查清单
在启动训练前,我习惯花两分钟快速过一遍下面这些点:
- 配置文件里的类别数和标注是否一致,错一个就全乱套。
- 数据路径下是否有足够的图片,建议每个类别至少50张,数据太少梯度很容易崩。
- 显卡驱动是否能被Pytorch识别,那句
torch.cuda.is_available()已经提前跑过了。 - batch size是否在显存可承受范围内,不确定就先开8试跑几个iteration,看CUDA memory变化。
这些检查看起来琐碎,但能在正式跑训练前拦截掉大部分浪费时间的问题。
4.2 启动训练的完整过程
进入Nanodet工程目录,执行:
python tools/train.py --config config/nanodet_x.yml训练启动后,控制台会滚动打印每个iteration的loss值、当前epoch数、学习率、单次迭代耗时等。我第一次跑模拟项目X时,loss从刚开始的3.5左右慢慢降到接近0.5,整体曲线是下降的趋势。
训练过程会用掉约4.2GB显存,GPU利用率在80%到95%之间波动,训练期间电脑还能正常浏览网页,但不建议同时跑其他大负载程序。
配置了120个epoch,整个训练过程大约55分钟。每个epoch结束都会保存一次权重文件,最终目录里会有一个model_final.pth和一个model_best.pth。best是按验证集损失或mAP挑出来的最优权重,后面测试阶段默认用这个文件。
4.3 训练中loss震荡和学习率策略
训练过程中最常见的现象是loss上下波动,看起来“不稳”。其实轻微震荡是正常的,尤其是batch size较小的场景。但如果loss一直降不下来,或者掉到某个值后反弹得很厉害,就比较需要注意了。
Nanodet默认带warm up机制,前几个epoch用一个较小学习率逐步过渡到正式值,这能大幅减少起始阶段的震荡。我一般还会在配置里加上cosine衰减,让学习率在训练后半段逐步降低,这样模型在最后阶段能收敛得更细腻。
调优经验:模型不收敛时,第一反应是把学习率降到原来的三分之一或四分之一,而不是盲目加epoch。学习率太高会导致loss在某个值附近反复横跳,很难落到更低的位置。
5. CPU推理测试:把模型从GPU搬下来
5.1 测试环境怎么搭最省事
最省事的方式是用原来的conda环境直接跑CPU推理,因为Pytorch支持CPU和GPU两种模式切换。如果在配置阶段单独装了GPU版Pytorch,CPU推理时它也会自动退化为CPU模式,不需要重新装环境。
我在实际测试中特意把环境和工程目录复制了一份到另一台没有NVIDIA显卡的笔记本上,用来验证“干净环境”下的部署表现。注意权重文件是用GPU训练出来的,加载时加一个map_location='cpu'就彻底摆脱GPU依赖了。
5.2 CPU推理的几条调优路径
CPU推理性能主要受三个因素影响:
- 推理线程数:Pytorch默认不限制线程数,但线程太多反而会在CPU多核之间频繁切换上下文。我实测下来,六核机器设置4个线程时性能最好。可以通过
torch.set_num_threads(4)设置。 - 输入分辨率:这是最直接的性能杠杆。图片从640x640降到320x320,推理时间几乎降到三分之一,精度损失通常不大。
- 模型是否进入eval模式:Pytorch的BN层和dropout在train和eval模式下行为不同,忘记调用
model.eval()会导致推理结果不稳定、速度变慢。
5.3 用官方demo脚本跑一张图
官方工程里通常带demo脚本,推理命令大致长这样:
python tools/demo.py --config config/nanodet_x.yml --model workspace/nanodet_x/model_best.pth --image test.jpg --device cpu注意--device cpu这个参数,这一步是“CPU测试”的关键。运行完成后会在输出目录生成一张画好框的图片,画框越准确说明训练效果越好。
如果找不到demo脚本,也可以自己写一个最小推理脚本:
import torch import cv2 import numpy as np from nanodet.util import cfg, load_config from nanodet.model.arch import build_model load_config(cfg, 'config/nanodet_x.yml') model = build_model(cfg.model) weights = torch.load('workspace/nanodet_x/model_best.pth', map_location='cpu')['state_dict'] model.load_state_dict(weights) model.to('cpu').eval() image = cv2.imread('test.jpg') h, w = image.shape[:2] # 按配置参数做预处理并转tensor # 模型输出检测框坐标、类别的概率,解析后画到图片上这段代码里最容易漏的就是map_location='cpu'。不加这个参数,在纯CPU机器上会直接报错“找不到GPU设备”,加了之后权重就能强制映射到CPU内存。
5.4 实测性能记录
用模拟项目X的权重在CPU上测了几张不同分辨率的图片,结果如下:
| 输入分辨率 | CPU推理耗时 | 帧率参考 |
|---|---|---|
| 320x320 | 38ms | 26 FPS |
| 416x416 | 72ms | 13 FPS |
| 640x640 | 168ms | 5 FPS |
如果只是验证模型“能不能跑”,320x320的分辨率已经足够,耗时控制在40毫秒左右。需要更细致的检测边框时再切到416以上。
训练完的模型在GPU上推理一张640x640图片大约8ms,CPU慢了二十倍左右,但依然属于可用范围,毕竟轻量级模型的设计初衷就是低算力环境部署。
6. 常见报错与避坑实录
6.1 我实际碰到的几个问题
直接罗列我这次实操中遇到的报错和解决过程。
第一个是CUDA out of memory。出现原因很简单,batch size设了32,6GB显存装不下。解决方法是把batch size减成16,同时关掉数据加载器里不必要的辅助进程。
第二个是FileNotFoundError: ROOT does not exist。现象是路径明明存在,但程序找不到。原因出在Windows路径上,配置里写的是反斜杠路径,yaml解析时把\N当成了转义。把所有路径统一改成正斜杠/后就正常了。
第三个是ImportError: Microsoft Visual C++ 14.0 is required。这个在Windows平台很常见,安装Visual Studio Build Tools的C++桌面开发组件后解决。这个工具集体积比较大,但属于一次安装长期受益。
6.2 防坑速查表
| 出现环节 | 典型坑 | 解法 |
|---|---|---|
| 环境安装 | Pytorch装了CPU版 | 确认安装命令包含cuda版本标识 |
| 环境安装 | CUDA版本和显卡驱动不匹配 | 更新驱动或选匹配的Pytorch |
| 数据准备 | VOC转COCO后类别顺序错乱 | 打开json检查categories映射 |
| 配置修改 | 路径含中文或空格 | 把项目放到纯英文路径 |
| 配置修改 | yaml里单反斜杠被转义 | 路径统一用正斜杠 |
| 训练 | GPU显存不足 | 降低batch size |
| 训练 | loss震荡明显 | 调低初始学习率,开启warm up |
| 推理 | 纯CPU机器加载GPU权重报错 | 加载时加map_location='cpu' |
| 推理 | 输出框偏移 | 确认是否调用model.eval() |
6.3 两条独家避坑心得
一是训练前先去任务管理器看一眼GPU的利用率和显存。如果NVIDIA显卡利用率是0,大概率环境没装对,不要盲目开始训练。
二是训练过程的日志要保存下来。训练结束后翻日志看loss曲线,能客观判断模型是否正常收敛。如果训练中途重启过,记得备份上一次的权重文件,避免覆盖。
整个流程做完,我自己最大的体会是:开发阶段用GPU跑训练、部署阶段用CPU跑测试,看似是在折腾两套环境,实际上是把“开发速度”和“部署真实感”都照顾到了。与其纠结一块显卡同时干两件事,不如明确分工,反而省心。
最后再分享一个小技巧:如果后续场景需要进一步提高CPU推理速度,可以考虑把训练好的权重导出成ONNX格式,再借助一些本地的CPU加速库来跑推理。Nanodet官方工程里带导出脚本,导出的模型在CPU上还能明显再快一截,甚至可以脱离Pytorch环境运行,部署时的依赖少了很多。这个方法实测可用,值得一试。