多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLO-FastestV2实战:从训练到Android NCNN部署的轻量级目标检测全流程

YOLO-FastestV2实战:从训练到Android NCNN部署的轻量级目标检测全流程 去年有个项目需要在树莓派上跑实时目标检测当时试了一圈YOLO系列从YOLOv5s到YOLOv8n帧率始终不理想模型和算力之间的矛盾折腾了我整整一周。后来接触到YOLO-FastestV2这个模型让事情变得简单了——不到1.5MB的模型文件在树莓派4B上推理耗时能做到单帧30毫秒量级。如果你也在做移动端、嵌入式设备或者边缘计算场景的目标检测这篇实战手册能帮你省掉我当初踩过的那些坑。YOLO-FastestV2是目前极少数专门以“极致轻量”为目标设计的检测模型核心代码基于YOLOv5早期版本改造骨干网络换成ShuffleNetV2检测头做了大幅裁剪。它解决的核心矛盾是边缘设备算力有限但又要跑得动实时检测。这篇文章会从环境搭建、数据集准备、模型训练、pt转ONNX转NCNN一直到Android端NCNN推理全流程写一遍内容包括每个环节容易出的问题以及解决办法。适合正在做移动端部署、嵌入式开发或者有边缘AI需求的读者参考。1. 先搞清楚YOLO-FastestV2到底“轻”在哪1.1 参数量、计算量和实测速度官方仓库给出的数据是YOLO-FastestV2的参数量约25万250K浮点计算量约0.23GFLOPsFP32精度的模型权重文件约1.2MB。这个体量是什么概念YOLOv5s参数量约700万是它的28倍YOLOv8n参数量约300万是它的12倍。通常轻量检测模型会把参数压到几百万直接压到几十万量级的YOLO-FastestV2算是走得比较极端的。我在树莓派4BCPU为Cortex-A72四核1.5GHz上实测过NCNN框架、单线程、352×352输入FP32推理耗时约120~150ms开四线程后能到40~60ms。在Android中端手机上用GPUVulkan加速FP16推理大约能跑15~25ms。这些数据意味着在树莓派这类设备上它能做到20帧以上在手机上做到40帧以上满足大部分实时检测需求。1.2 和YOLOv5s/YOLOv8n的选型对比很多人在轻量模型选型时直接选YOLOv5s或YOLOv8n这两个确实在通用场景下精度更好但移动端部署不只是看精度还要看内存占用、推理耗时、发热等多个维度。模型参数量计算量(GFLOPs)权重大小(FP32)树莓派4B推理耗时适用场景YOLOv5s~7.0M16.5~14MB500ms以上服务器或Jetson类设备YOLOv8n~3.2M8.7~12MB200ms以上中端手机、边缘盒子YOLO-FastestV2~0.25M0.23~1.2MB40~60ms(4线程)树莓派、低端手机、MCU类设备实际选型时有个经验如果目标设备的CPU主频低于1.8GHz或者内存在1GB以下直接放弃YOLOv5系列YOLO-FastestV2是更合适的选择。如果设备是骁龙8系旗舰手机、Jetson Orin这类有GPU加速能力的平台YOLOv8n也不差。关键还是先评估硬件底牌再定模型。1.3 第一代YOLO-Fastest和V2的差别YOLO-Fastest第一版基于Darknet框架实现使用ResNet-DW做骨干模型大小约1.3MB。V2版本换成PyTorch框架骨干换为ShuffleNetV2最大的变化在于训练生态更好可以直接用YOLOv5的很多工具链不用碰Darknet那些反人类的配置。检测头做了重新设计从原来的三尺度输出精简为双尺度输出两个检测层计算量进一步下降。支持与YOLOv5一脉相承的Mosaic数据增强、自动锚框计算等训练策略。所以现在新项目建议直接上V2除非你有必须用Darknet C推理的历史包袱。2. 训练阶段环境依赖、数据集与关键参数2.1 环境依赖细节Python和PyTorch版本真有讲究YOLO-FastestV2官方代码基于YOLOv5早期分支对依赖版本有一定的兼容范围。我的实测结论是Python 3.7~3.8最稳定3.9以上部分依赖安装会报错。PyTorch建议用1.7~1.9版本不要直接上PyTorch 2.x推理能跑但训练时经常出现算子和旧代码不匹配的问题。torchvision版本跟着PyTorch配套装。安装命令建议这样写conda create -n yolofastest python3.8 source activate yolofastest pip install torch1.9.0 torchvision0.10.0 git clone https://github.com/dog-qiuqiu/Yolo-FastestV2 cd Yolo-FastestV2 pip install -r requirements.txt我在PyTorch 1.11上试过一次训练时Loss能正常下降但转到onnx时就会遇到算子兼容问题排查成本很高建议不要在这个环节浪费时间。2.2 数据集格式VOC和YOLO txt两种按需选择YOLO-FastestV2同时支持VOC格式XML标注和YOLO格式txt标注这一点继承了YOLOv5的做法。项目目录结构如下datasets/ voc/ images/ train/ # 训练图片 val/ # 验证图片 annotations/ # VOC xml文件 yolo/ images/ train/ val/ labels/ train/ # 每个txt文件对应一张图片的标注 val/我习惯用YOLO格式因为后期标注工具输出基本都是txt省去一次转换。txt标注格式为类别id x_center y_center width height注意坐标是归一化到0~1之间的相对值。比如一张640×480的图上一个目标边界框左上角(100, 120)、宽200、高160那txt这一行就是0 0.3125 0.4167 0.3125 0.3333计算过程x_center (100 200/2) / 640 200/640 ≈ 0.3125y_center (120 160/2) / 480 200/480 ≈ 0.4167。这个换算出错是新手标注时最常见的错误之一。2.3 配置文件修改类别数、anchor和输入尺寸训练前要修改模型配置文件models/yolofastestv2.yaml里面最关键的是检测头输出层的anchor设置。YOLO-FastestV2采用双尺度输出每层有3个anchor一共6个组。如果你训练自己的数据集不要手动去算anchor直接删除配置文件里的anchor字段YOLOv5的训练逻辑会自动用K-Means从你的数据集中重新聚类。但输入尺寸建议提前定好默认是352×352。如果目标物体普遍比较小可以调到416甚至512精度会涨一些但推理耗时也会增加移动端部署时先跑一下自己的场景再定。data/voc.yaml里要改的是nc类别数和names类别名称列表这两个必须一致否则训练必报错。2.4 训练命令和参数选择逻辑这是我最常用的一组训练命令python train.py --data data/voc.yaml --cfg models/yolofastestv2.yaml --weights --batch-size 32 --img 352 --epochs 200 --device 0一些参数选择的实际经验--batch-size取决于显存大小。YOLO-FastestV2很轻普通6GB显存显卡可以开64以上但batch太大会降低收敛稳定性32比较稳妥。--img训练尺寸和后面部署时的输入尺寸最好保持一致这样可以避免精度损失。比如你部署时打算用352输入训练时就用352。--epochs小模型收敛比大模型快一般150~200轮足够不是越多越好300轮以上反而容易过拟合。训练过程中如果发现训练集Loss还在下降但验证集指标上不去就说明过拟合了停止训练减小模型容量或增加数据增强。3. 模型转换链路pt→ONNX→NCNN中间堆了多少坑3.1 为什么移动端不能用PyTorch推理PyTorch官方虽然提供了TorchScript和PyTorch Mobile方案但在移动端的工程实践里大家还是更倾向NCNN。原因很直接PyTorch Mobile的模型加载和预热时间比NCNN长内存占用也更大。NCNN针对ARM、x86、Vulkan等后端做了大量算子级优化推理效率更高。NCNN是纯C实现对Android JNI调用更友好接入成本低。YOLO-FastestV2官方仓库提供了NCNN模型和转换脚本这也是我选择它的一个重要原因。3.2 ONNX导出的关键坑opset版本、动态尺寸和算子兼容ONNX是中间格式但导出时有很多细节容易被忽略。直接跑官方提供的导出脚本python models/export_yolov5_onnx.py --weights weights/yolofastestv2.pt --img 352 --batch 1如果没有脚本也可以手动用torch.onnx导出核心代码逻辑类似import torch from models.experimental import attempt_load model attempt_load(weights/yolofastestv2.pt, map_locationcpu) model.eval() dummy_input torch.randn(1, 3, 352, 352) torch.onnx.export( model, dummy_input, yolofastestv2.onnx, opset_version11, input_names[images], output_names[output] )这里有个值得注意的点onnx模型默认保持动态输入也就是可以接受任意batch和任意分辨率的输入但NCNN转换时对动态尺寸支持不好最好把输入尺寸写死。还需要设置--dynamicFalse或者在导出时加torch.onnx.export的dynamic_axes参数保持静态。否则转换到NCNN后推理尺寸对不上直接崩。还有一个常见问题是导出后onnx模型里的P6层输出形状是(1, 255, 88, 88)这种四维结构有些后处理代码需要的是(1, 88*88*3, 85)转换时需要注意reshape。导出后用onnx-simplifier过一遍能处理掉很多冗余算子pip install onnx-simplifier python -m onnxsim yolofastestv2.onnx yolofastestv2_sim.onnx3.3 onnx2ncnn转换报错排查拿到简化后的onnx就可以转NCNN了。官方工具链git clone https://github.com/Tencent/ncnn cd ncnn mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease .. make -j4 # 编译完成后工具在 build/tools/onnx/onnx2ncnn ./onnx2ncnn yolofastestv2_sim.onnx yolofastestv2.param yolofastestv2.bin这里有几个高频报错“Unsupported ONNX op: ReduceMax”类错误通常是opset版本过高或算子未实现先用简化器试着消除不行就降opset到10或11。“MatMul with dynamic shape is not supported yet”这是onnx里有动态形状的MatMul说明导出时没有固定输入维度回上一步重新固定。转换完成但是没有报错但.param文件里少了某些层比如DetectionOutput层这是正常的。YOLO-FastestV2的检测头是普通卷积reshapeNCNN不会自动生成decode逻辑后处理需要自己写。转换之后一定要做一个完整的前后对比用PyTorch跑一张图和用NCNN跑同一张图对比原始输出张量的数值。误差在1e-4以内才算正常超过这个范围说明转换过程有精度损失要检查是否有未支持的算子被降级了。这个验证步骤被很多人跳过结果到Android端才发现识别结果不对排查起来特别痛苦。3.4 官方NCNN Demo的目录结构YOLO-FastestV2作者提供了一个Android NCNN Demo目录在models/ncnn/Android/下面项目是基于NCNN自带examples改的。里面关键的三个文件yolofastestv2_ncnn.cppJNI层实现负责图像预处理、推理和后处理。yolofastestv2.h和yolofastestv2.cpp封装了检测结果结构体classId、score、bbox。MainActivity.javaAndroid上层调用展示结果。我建议先把Demo在Android Studio里跑通再根据自己需求改造。跑通Demo能验证你的模型文件和整个工具链没问题之后换成自己的模型会很快。4. Android端NCNN推理的核心细节4.1 图像预处理这一步做错后面全白搭NCNN推理对输入图像有严格要求YOLO-FastestV2训练时的预处理逻辑是图缩放到352×352RGB三通道每个像素除以255归一化数据布局为NCHW。在Android端相机或相册返回的图像通常是NV21格式或Bitmap格式需要先转换成RGB再缩放。这里容易踩的坑是通道顺序。训练时用的是RGB但Android相机和很多第三方库默认输出是BGR。如果不做转换用BGR数据直接喂给模型检测结果就是混乱的。我调试时发现检测框位置和类别完全对不上排查了半天发现就是RGB/BGR问题。NCNN的图像输入还有一个更隐蔽的细节如果你的网络参数里用了pixel_type字段指定了图像类型NCNN会自动做转换但YOLO-FastestV2的转换脚本输出的是普通卷积层不会自动处理需要自己在JNI层做好缩放和归一化。ncnn::Mat in ncnn::Mat::from_pixels_resize(rgb_data, ncnn::Mat::PIXEL_RGB, width, height, 352, 352); const float norm_vals[3] {1.f/255.f, 1.f/255.f, 1.f/255.f}; in.substract_mean_normalize(0, norm_vals);4.2 模型加载和推理会话配置NCNN模型加载方式有两种从文件加载和从内存加载。Android工程里通常把.param和.bin文件放到assets目录运行时用ncnn::Net加载ncnn::Net net; net.opt.use_vulkan_compute true; // 有GPU就用GPU net.opt.use_fp16_packed true; // 半精度混合推理 net.load_param(yolofastestv2_ncnn.param); net.load_model(yolofastestv2_ncnn.bin);有几个参数值得特别说明use_vulkan_compute开启后使用GPU推理但低端手机的GPU驱动不一定稳定实测部分GPU会导致首次推理特别慢或者掉帧。我的建议是默认开GPU如果在低端机上不稳定就关掉CPU多线程也能跑。use_fp16_packedFP16推理能提高速度但精度会有一点损失一般检测场景影响不大。use_fp16_storage和上面配合使用降低内存带宽。还有一个容易被忽略的是net.opt.num_threads不设置的话默认用4线程但有些四核大核小核架构的手机设置成大核数量能获得更好的性能。推理核心代码ncnn::Extractor ex net.create_extractor(); ex.input(images, in); ncnn::Mat out; ex.extract(output, out);4.3 后处理anchor解码和NMS手写还是调库YOLO-FastestV2的输出需要在后处理阶段解码才能得到最终的检测框和类别。它有两个检测头分别处理不同尺度的特征图。后处理逻辑包括将原始输出拆分成边界框回归x,y,w,h、置信度和类别概率。解码得到最终的坐标值将归一化的坐标还原到原图尺度。用NMS去掉重复框。NCNN没有内置YOLO检测头所以这部分需要自己实现。Demo里的后处理代码大约100行核心逻辑可以复用。但有一个细节YOLO-FastestV2输出头的通道排列顺序和YOLOv5不完全一样直接套用YOLOv5的解码代码会出错。以我的经验后处理最需要关注的是anchor的顺序。YOLO-FastestV2的anchor定义在模型的yaml配置里转换到NCNN后不会自动包含anchor信息需要手动在代码里写。anchor顺序如果搞反检测框的位置就会错乱——比如把大目标的anchor用在了小目标检测层上。NMS的实现有两种方式一种是自己写循环遍历另一种是用std::sortIoU计算的方式。数据量小几百个候选框自己写完全没问题不要引第三方库增加包体积。4.4 性能实测CPU/GPU、单线程/多线程的差别有多大以下是我在Pixel 4骁龙855上跑YOLO-FastestV2时的实测数据输入尺寸352×352仅供参考配置单帧耗时备注CPU 1线程88ms能跑但不流畅CPU 4线程32ms日常推荐CPU 4线程 FP1627ms速度和精度折中GPU Vulkan18ms帧率稳定发热明显GPU Vulkan FP1614ms最优性能组合从数据能看出对YOLO-FastestV2这种小模型CPU四线程已经能满足30帧需求GPU提升效果有但有限。这主要是因为模型本身计算量太小调度和内存拷贝的开销占比变高了。在树莓派这类纯CPU设备上四线程是性能上限超线程反而会导致CPU负载过高发热降频。5. 进一步压榨性能的四条优化路径5.1 INT8量化耗时、内存和精度的三重权衡NCNN支持INT8量化YOLO-FastestV2转INT8后模型从1.2MB降到约300KB推理速度在CPU上能再提升30%~50%但精度会有明显下降尤其对小目标。如果模型训练时的mAP本身就在80%以下量化后掉到70%以下的风险很高。NCNN的INT8量化需要准备一个校准集用真实场景的图片来统计每层激活值的范围./ncnn2table yolofastestv2.param yolofastestv2.bin calibration_set.txt list.txt mean0 norm0.0039215686274509803 shape352x352x3 pixelrgb thread4 methodkl ./ncnn2int8 yolofastestv2.param yolofastestv2.bin yolofastestv2_int8.param yolofastestv2_int8.bin table校准集建议涵盖所有类别的典型样本数量在100~500张之间。校准集选不好量化后的精度损失会非常剧烈。5.2 输入分辨率剪枝速度翻倍的一条捷径YOLO-FastestV2默认输入是352×352但很多场景下不一定需要这么高的分辨率。当检测目标体在画面中占比比较大时比如摄像头架在产线上方拍工件把输入降到288×288或320×320计算量会大幅下降推理速度提升接近40%。但注意改推理分辨率后模型输出的感受野和anchor匹配关系会变最好的方式是训练时就直接用目标分辨率。如果已经用352训练完了硬要降到288推理检测精度会下降约3~5个百分点但多数场景还是能用的。5.3 后处理代码级别的优化后处理虽然占比不高但在帧率要求严格的场景下也是一块收益。具体做法提前过滤低置信度框得分低于0.25的候选框直接丢掉避免进入NMS阶段能减少80%以上的IoU计算。用循环展开或SIMD指令优化解码循环。NMS排序用std::nth_element而不是完整std::sort只需要知道最高分的那几个框就够了。5.4 线程配置和内存复用这块是容易被忽略的。推理框架默认会在每次推理时分配新的内存如果你的程序是连续检测视频帧就会有大量重复分配和释放导致GC开销过高。建议在初始化阶段就配置NCNN的gpu_heap_allocator和cpu_allocator让网络复用分配好的内存块。线程数的设置原则是优先占用高性能核心不要盲目开满所有线程。在大小核架构的ARM芯片上把小核线程数控制到最低能显著降低功耗和发热。最后再聊几句这一整套流程走下来我的体会是YOLO-FastestV2最大的优势并不是某一个数值特别亮眼而是整个工具链对边缘部署非常友好从训练到NCNN都有现成脚本省掉了大量移植和调试时间。如果你在部署中遇到精度突然对不上的问题优先排查RGB/BGR顺序和anchor顺序这两个坑占了我遇到问题的七成以上。后续如果你想把模型进一步部署到像K210这种更低算力的MCU上INT8量化并配合FBNet这类更极端的骨干是可以试试的方向。
返回列表