
1. 从训练到端侧YOLOv5 自定义模型为什么总卡在部署这一步YOLOv5 训练自己的数据集网上教程一抓一大把但真正让人头疼的往往不是训练本身而是训练完之后怎么把.pt权重变成一个能在 C 工程里跑起来的推理文件。我见过太多人卡在 ONNX 导出报错、MNN 转换后输出张量对不上、C 里 decode 出来的框全是乱的。这篇就按我实际跑通的流程把 YOLOv5 训练、导出 ONNX、转 MNN、C 推理这条链路完整走一遍同时把模型服务调用这一层用 TaoToken 统一 Key 管起来避免每个模型、每个环境都去维护一套零散的鉴权配置。先说清楚这套东西适合谁如果你已经会用 YOLOv5 跑通官方 demo想换成自己的数据集训练并且最终要落到 C 端侧或者服务端做推理那这篇就是给你写的。如果你还没装过 YOLOv5建议先把官方仓库拉下来跑一遍detect.py再回来看部署部分。整条链路可以拆成四段数据准备与训练、权重导出 ONNX、ONNX 转 MNN、C 加载 MNN 做推理。每一段都有坑我会把踩过的坑和对应报错一起写出来。另外训练完之后如果想把模型能力接到一个统一的服务入口做验证或批量调用TaoToken 的 API 通道可以省掉自己搭鉴权网关的麻烦后面会给可复制的配置。核心检索词先摆出来YOLOv5 自定义数据集训练、C 部署 MNN 框架、ONNX 转 MNN、MNN 推理输出张量解析。这几个词基本覆盖了从训练到端侧部署的全流程下面按顺序展开。2. 训练环境与数据准备YOLOv5 自定义数据集训练避坑指南2.1 版本选择与依赖安装YOLOv5 版本迭代很快5.0 和后面的 6.0、7.0 在模型结构上有差异最典型的就是 SPPF 模块。如果你用 5.0 的代码去加载 6.0 的预训练权重会直接报AttributeError: Cant get attribute SPPF。所以第一步就是把版本锁死。wget https://github.com/ultralytics/yolov5/archive/refs/tags/v5.0.zip unzip v5.0.zip cd yolov5-5.0 pip install -r requirements.txt装完之后先跑一遍官方图片验证环境python detect.py --weights yolov5s.pt --source data/images/bus.jpg如果能看到4 persons, 1 bus, 1 fire hydrant这类输出说明 PyTorch、CUDA、OpenCV 这条链没问题。注意这里下载的yolov5s.pt要和你代码版本对应5.0 的代码就用 5.0 的权重别混用。2.2 数据集目录结构YOLOv5 支持 VOC 和 YOLO 两种标注格式。如果你手上是 VOC 的 XML需要转成 YOLO 的 txt。目录建议这样组织datasets/ └── VOCdevkit/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── VOC2007/ ├── Annotations/ └── JPEGImages/Annotations放 XMLJPEGImages放对应 jpg。转换脚本的核心逻辑是读 XML 里的bndbox归一化成cls x_center y_center w h写入 txt。转换时注意classes列表要和你的实际类别一致TRAIN_RATIO控制训练验证比例。2.3 训练配置修改复制一份数据配置cp data/coco.yaml data/trainData.yaml改三个地方train和val指向你的图片目录nc改成类别数names改成类别名列表。train: /home/ubuntu/yolov5-5.0/datasets/VOCdevkit/images/train/ val: /home/ubuntu/yolov5-5.0/datasets/VOCdevkit/images/val/ nc: 1 names: [code]模型配置复制models/yolov5s.yaml为yolov5s_self.yaml同样把nc改成 1。然后启动训练python train.py --data data/trainData.yaml --cfg models/yolov5s_self.yaml --weights weights/yolov5s.pt --device 0 --epochs 3002.4 训练常见报错报错一SPPF 找不到。原因就是权重版本和代码版本不匹配。解决办法是把对应版本的yolov5s.pt放到weights/目录同时在项目根目录也放一份避免训练时自动去下载新版权重。报错二RuntimeError: result type Float cant be cast to the desired output type long int。这是 5.0 版本在部分 PyTorch 版本下的已知问题改utils/loss.py两处# 原来 anchors self.anchors[i] gain[2:6] torch.tensor(p[i].shape)[[3, 2, 3, 2]] # 改成 anchors, shape self.anchors[i], p[i].shape gain[2:6] torch.tensor(p[i].shape)[[3, 2, 3, 2]]# 原来 indices.append((b, a, gj.clamp_(0, gain[3] - 1), gi.clamp_(0, gain[2] - 1))) # 改成 indices.append((b, a, gj.clamp_(0, shape[2] - 1), gi.clamp_(0, shape[3] - 1)))改完重新跑loss 能正常下降就说明数据管道通了。训练过程中重点看mAP.5有没有上升如果一直是 0大概率是标注格式或者类别索引对不上。3. 权重导出与 MNN 转换ONNX 转 MNN 完整命令与配置3.1 pt 转 ONNX训练完成后权重在runs/train/exp/weights/best.pt。导出 ONNXpython models/export.py --weights runs/train/exp/weights/best.pt --img 640 --batch 1 --device 0成功后会生成best.onnx。这里注意--img要和训练时的输入尺寸一致否则后面 C 里 resize 的尺寸对不上框会偏。3.2 ONNX 简化原始 ONNX 里有很多冗余节点用 onnxsim 简化python -m onnxsim best.onnx best-sim.onnx简化后会看到Gather、Shape这类节点数量下降模型体积基本不变但推理图更干净。3.3 ONNX 转 MNN先编译 MNN 的转换工具。拉取 MNN 源码后在CMakeLists.txt里打开 converteroption(MNN_BUILD_CONVERTER Build Converter ON)编译完成后执行转换./MNNConvert -f ONNX --modelFile /path/to/best-sim.onnx --MNNModel /path/to/best-sim.mnn --bizCode MNN转换成功会打印输入输出张量名inputTensors : [ images, ] outputTensors: [ 417, 437, output, ]这三个输出名要记下来C 里getSessionOutput用的就是它们。不同版本 YOLOv5 输出层名字可能不一样以实际打印为准。3.4 用 TaoToken 统一管理模型服务调用训练和转换都在本地但如果你想把模型能力接到一个统一入口做验证、批量推理或者给其他服务调用自己搭鉴权网关比较费事。TaoToken 提供统一 Key 和 API 通道把模型服务调用这层收敛到一个 Base URL 上。在控制台创建 Key 后配置如下{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: 你的模型ID }三件套就是 Base URL、Key、Model ID。如果你用 Cline 或 Claude Code 这类工具做辅助开发MCP 配置里同样填这三个字段。注意 Base URL 用https://taotoken.net/api不要带多余路径。4. C 加载 MNN 推理从张量解析到画框的完整代码骨架4.1 工程结构yolov5_mnn/ ├── CMakeLists.txt ├── main.cpp ├── include/ │ └── MNN/ ├── lib/ │ └── libMNN.so ├── model/ │ └── best-sim.mnn └── src/ └── Yolo.cpp4.2 CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(yolov5_mnn) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -fopenmp) include_directories(${CMAKE_SOURCE_DIR}/include) include_directories(${CMAKE_SOURCE_DIR}/include/MNN) find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) add_library(libmnn SHARED IMPORTED) set_target_properties(libmnn PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/lib/libMNN.so) aux_source_directory(${CMAKE_SOURCE_DIR}/src SRC_LIST) add_executable(yolov5_mnn main.cpp ${SRC_LIST}) target_link_libraries(yolov5_mnn ${OpenCV_LIBS} libmnn)4.3 推理主流程#include MNN/Interpreter.hpp #include opencv2/opencv.hpp #include Yolo.h int main() { std::string model_name ../model/best-sim.mnn; int num_classes 1; std::vectorYoloLayerData layers{ {437, 32, {{116, 90}, {156, 198}, {373, 326}}}, {417, 16, {{30, 61}, {62, 45}, {59, 119}}}, {output, 8, {{10, 13}, {16, 30}, {33, 23}}}, }; std::vectorstd::string labels{code}; std::shared_ptrMNN::Interpreter net( MNN::Interpreter::createFromFile(model_name.c_str())); if (nullptr net) return 0; MNN::ScheduleConfig config; config.numThread 4; config.type static_castMNNForwardType(MNN_FORWARD_CPU); MNN::BackendConfig backendConfig; backendConfig.precision (MNN::BackendConfig::PrecisionMode)2; config.backendConfig backendConfig; MNN::Session *session net-createSession(config); int INPUT_SIZE 640; cv::Mat raw_image cv::imread(../bus.jpg); cv::Mat image; cv::resize(raw_image, image, cv::Size(INPUT_SIZE, INPUT_SIZE)); image.convertTo(image, CV_32FC3); image image / 255.0f; std::vectorint dims{1, INPUT_SIZE, INPUT_SIZE, 3}; auto nhwc_Tensor MNN::Tensor::createfloat(dims, NULL, MNN::Tensor::TENSORFLOW); std::memcpy(nhwc_Tensor-hostfloat(), image.data, nhwc_Tensor-size()); auto inputTensor net-getSessionInput(session, nullptr); inputTensor-copyFromHostTensor(nhwc_Tensor); net-runSession(session); MNN::Tensor *tensor_scores net-getSessionOutput(session, output); MNN::Tensor *tensor_boxes net-getSessionOutput(session, 417); MNN::Tensor *tensor_anchors net-getSessionOutput(session, 437); MNN::Tensor scores_host(tensor_scores, tensor_scores-getDimensionType()); MNN::Tensor boxes_host(tensor_boxes, tensor_boxes-getDimensionType()); MNN::Tensor anchors_host(tensor_anchors, tensor_anchors-getDimensionType()); tensor_scores-copyToHostTensor(scores_host); tensor_boxes-copyToHostTensor(boxes_host); tensor_anchors-copyToHostTensor(anchors_host); std::vectorBoxInfo result; yolocv::YoloSize yolosize{INPUT_SIZE, INPUT_SIZE}; float threshold 0.3, nms_threshold 0.7; auto boxes decode_infer(scores_host, layers[2].stride, yolosize, INPUT_SIZE, num_classes, layers[2].anchors, threshold); result.insert(result.end(), boxes.begin(), boxes.end()); // 同理处理 layers[1] 和 layers[0] nms(result, nms_threshold); scale_coords(result, INPUT_SIZE, INPUT_SIZE, raw_image.cols, raw_image.rows); cv::Mat frame_show draw_box(raw_image, result, labels); cv::imwrite(../output.jpg, frame_show); return 0; }4.4 关键点说明MNN::Tensor::create的维度顺序是 NHWC因为 MNN 默认按 TensorFlow 布局。如果你的模型输入是 NCHW需要改成MNN::Tensor::CAFFE。copyFromHostTensor之前一定要确认 host tensor 的数据类型和模型输入一致否则会静默出错。decode_infer里做的是把每个 grid 的预测值按 anchor 解码成框核心公式是x (sigmoid(tx) * 2 - 0.5 grid_x) * stride。这部分如果写错框会整体偏移或者尺寸不对。5. 部署排错MNN 推理常见报错与 C 输出张量对不上怎么办5.1 报错local proxy failed或 401如果你在调用 TaoToken API 做模型服务验证时遇到 401先检查 Key 是否带上了Bearer前缀以及 Base URL 是不是https://taotoken.net/api。local proxy failed一般是本地网络配置问题确认没有多余的代理环境变量干扰。5.2 报错reading choices相关这类报错通常出现在用 OpenAI 兼容接口调用时返回体里choices字段解析失败。检查请求体里的model字段是否和你在控制台看到的 Model ID 完全一致大小写敏感。5.3 输出张量名对不上MNN 转换后打印的outputTensors是417, 437, output但 C 里如果写死成别的名字getSessionOutput会返回 nullptr后面copyToHostTensor直接崩。解决办法是转换时把打印的输出名记下来C 里严格对应。不同 YOLOv5 版本输出层命名规则不同5.0 是output、417、4376.0 可能是output0、output1、output2。5.4 框位置偏移最常见的原因是scale_coords的宽高比算错了。w_ratio w_to / w_from其中w_from是模型输入尺寸w_to是原图宽度。如果原图是 1920x1080模型输入 640x640那么 x 方向缩放比是 1920/640y 方向是 1080/640不能统一用一个比例。5.5 OAuth 相关报错如果你用 Claude Code 或类似工具接入遇到 OAuth 报错检查配置文件里的auth.json或settings.json是否正确写了 Base URL 和 Key。三件套缺一不可Base URL、Key、Model ID。6. 把模型服务接进统一通道TaoToken API 验证与 Coding Plan 选择训练和 C 部署跑通之后下一步通常是把模型能力接到一个统一入口做批量验证或者给其他服务调用。TaoToken 的 API 通道可以省掉自己维护鉴权、限流、日志这些事。验证模型是否可用直接调模型对话接口curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:你的Model ID,messages:[{role:user,content:test}]}如果返回正常说明 Key 和通道都没问题。接入文档在https://taotoken.net/doc里面有各语言的示例。如果你长期做编码或者 Agent 类任务Coding Plan 比按次调用更划算适合高频使用场景。API Keys 管理在控制台https://taotoken.net/console/api-keys可以按项目分 Key方便排查问题。最后说一个实际经验MNN 在 ARM 设备上跑的时候config.numThread不要设太大4 线程在多数边缘设备上已经够用设多了反而因为线程调度开销导致延迟上升。另外backendConfig.precision设成 2 是低精度模式速度会快一些但如果你的类别对精度敏感建议先用默认精度跑一遍对比。