上一篇文章里,我们好不容易打赢了“数值之战”,让模型终于能看清楚东西了。我美滋滋地想,导师给的玉米穗模型部署任务,这下总该顺利了吧?
于是,我满心期待地把模型从 COCO 的yolo11n.rknn
换成了自己的MTDC-UAV.rknn
。结果……程序又扑街了。
新副本:从“看错”到“崩溃”
这次倒不是瞎指认了,而是干脆利落的**段错误(Segmentation Fault)**——程序在推理成功后,刚进后处理函数就当场崩溃。
吃一堑长一智,我第一反应就是:模型结构又作什么妖了?赶紧加日志,把模型真实的输入输出张量数量打印出来瞅瞅。果然:
- COCO
yolo11n.rknn
模型:9 个输出 - 我的
MTDC-UAV.rknn
模型:7 个输出
破案了。我之前写的后处理代码,和那些 C++示例一样,天真地以为模型永远会吐出 9 个输出。当它试图去访问我模型里压根不存在的第 8、第 9 个输出张量时,就直接一头撞墙上——内存访问越界了。
这时候我才算彻底明白,为啥当初 C++示例跑不通我的模型,为啥我非得折腾这个重构项目。真不是我闲得慌非要追新,是这坑就摆在这,不填平它,任务根本进行不下去啊!
关键破局点:别猜了,让模型自己“交代”
我悟了:不能再猜模型长啥样了,得写个代码让它自己交代。我需要个“模型侦察兵”,而“侦察情报”就是模型自己的元数据。
我没瞎琢磨,而是干了件挺有用的事:数据驱动调试。我让程序分别加载两个模型,把它们所有输出张量的属性(维度、名称等)全打印出来。
情报一:YOLOv11n (COCO) 的输出张量属性
1 | --- 输出张量属性诊断 --- |
情报二:YOLOv11n (玉米穗) 的输出张量属性
1 | --- 输出张量属性诊断 --- |
把这两份“情报”放一块,谜底揭晓了:
- “套路”还是那个套路:虽然输出总数不同,但它们都遵循一个基本模式——在同一个空间尺寸(比如
80x80
)上,总会有一组相关的张量。 - 张量的“身份证”:我摸到一个还算靠谱的启发式规则来区分它们。负责预测框的Box 张量,其通道数
dims[1]
是个固定值(比如64
);而负责预测类别的Score 张量,其通道数dims[1]
恰恰就是类别总数(COCO 是 80,玉米穗就 1 个)。 - “被优化”了:最有趣的是,玉米穗模型在
20x20
这个尺度上,只有 Box 张量,没对应的 Score 张量!怪不得少俩输出。估计是 ONNX 转 RKNN 时,优化器觉得这小目标检测分支对我的任务没啥用,顺手就给“剪”了。
算法的演进:从“写死”到“动态配对”
真相大白。不能依赖任何固定的输出数量或顺序,唯一可靠的,就是“相同空间尺寸的 Box 和 Score 张量是一对儿”这个基本原理。
基于这,我琢磨出一个新的、能自适应的后处理算法。我不再关心总共是 7 个还是 9 个输出,我的代码现在像是在玩一个动态的“配对游戏”:
第一步:给所有张量发“身份证”(动态识别)
我遍历所有输出张量,根据刚摸清的规则,给它们贴上“身份”标签,然后塞进一个列表里。
1 | // rkyolo-core/src/postprocess.rs |
第二步:按尺寸配对并处理(动态配对与调用)
接下来是核心部分。代码不再写死循环,而是遍历所有被认出是Box
的,然后为每一个去找尺寸一模一样的Score
。只要配上一对,就立马把它们塞给解码函数去处理。
1 | // rkyolo-core/src/postprocess.rs |
这代码妙就妙在它的描述性。它没死命令式地去“访问第 0、1、3 个……”张量,而是描述了个匹配规则:“给我找个 Box,再给它配个一样大的 Score,然后你俩一起处理去”。这么一来,张量数量变没变、顺序乱没乱,它根本不在乎。
小结与收获:从“搞定”到“学乖”
当我用这套新的“动态配对”算法换掉旧的硬编码后,嘿,还真成了!
无论是 9 个输出的 COCO 模型,还是被“剪枝”成 7 个输出的玉米穗模型,甚至以后别的什么妖魔鬼怪模型,我这后处理代码一个字都不用改,大概率都能蒙混过关。
这次折腾下来,最大的感触就是:
- 别脑补,看日志:与其瞎猜,不如让数据(模型元数据)自己说话,让它指导代码该怎么写。
- 拥抱变化:写死的代码最怕变化。一个皮实的系统,得能优雅地处理各种意外(比如模型被优化了)。
- 挖到底:只有挖到问题的根上(模型输出的内在模式),才能想出“一劳永逸”的笨办法,而不是不停打补丁。
这个过程让我学到的,不止是修好一个程序,更是对怎么写“抗造”的代码有了点感觉。算是从一个只会埋头解决问题的“码农”,朝一个能预感到变化、会写适应性代码的“码农 plus”挪了一小步吧。