尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

自定义MobileNetV2转NBG报错?STM32Cube.AI输出节点丢失排障全记录

自定义MobileNetV2转NBG报错?STM32Cube.AI输出节点丢失排障全记录 前段时间在STM32MP257上做图像分类部署自定义训练了一个MobileNetV2模型用ST的工具链生成NBG文件的时候直接给我甩了一句“Generation does not contain any output”当场有点懵。更让人迷惑的是同一个工具链直接拿ST Model Zoo里现成的MobileNetV2转换一遍过啥事没有。折腾了两天把工具链日志、模型导出方式、输入输出节点全排查了一遍终于搞明白了问题到底出在哪。这篇文章就详细记录这次排障过程包括报错原理、根因分析、具体修复步骤以及几个以后能直接避开的坑。先交代一下背景STM32MP257用的是STM32Cube.AI一类的工具链把神经网络转换成NBGNetwork Binary Graph格式扔给板载NPU跑推理。自定义模型和Model Zoo模型在转换结果上一个失败一个成功问题几乎可以锁定在“模型本身的结构/导出方式”和“工具链对输入输出图的解析”这两个环节而不是NPU算力不够或者内存不足这类硬件问题。只要理解了工具链生成NBG的完整链路就能明白这条报错背后藏着的真正原因。1. 先搞懂“Generation does not contain any output”到底在说什么1.1 NBG生成链路中这个报错出现的阶段STM32Cube.AI生成NBG不是一步到位的它内部大致分三个阶段模型解析与图优化、量化/校准、代码/NBG生成。模型解析阶段工具会读取你给的ONNX、TFLite或者Keras模型构建出一份内部计算图然后做一轮又一轮的优化比如算子融合、常量折叠、死节点消除。等到生成阶段工具链会在计算图上找“输出张量”把最终结果写到NBG文件里。“Generation does not contain any output”这条报错字面意思就是工具链在分析完模型之后没有在这张计算图上找到任何合法的输出节点。换句话说模型读进来了解析也没报“文件损坏”之类的错但图里面找不到它可以作为推理结果的张量。这种情况和“模型太大”“内存不足”“算子不支持”完全不是一个层面后者通常会在更靠后的阶段才炸出来。所以排查方向很明确不是去调NPU配置也不是换工具链版本而是先把模型的输入输出结构搞清楚让工具链能正常识别出推理结果的张量。1.2 为什么一个“看起来正常”的模型会找不到输出最常见的场景有两个。第一个是输出节点在导出的时候被编译器或者序列化过程悄悄改写了工具链按照默认规则找输出张量名字结果名字对不上。第二个是模型里包含了某种工具链无法映射的算子结构导致输出张量在优化过程中被当成“死节点”剪掉了。死节点消除这个机制特别容易坑人。如果模型在导出时输出节点没有被正确标记或者输出节点后面的算子链在优化器看来是“无用的”优化器就会毫不犹豫地把这整条链删掉。Keras转ONNX的坑我后面细说这里先记住一个原则工具链判断“这是不是输出”的依据往往是模型文件里显式声明的输出节点而不是你自己心里想的那个张量。你觉得自己训练了一个MobileNetV2输出自然是分类概率但如果你导出时没有把Softmax或Logits作为正式输出暴露出来图优化阶段就可能什么都留不下。1.3 用ST Model Zoo模型做对照实验的意义ST Model Zoo里的MobileNetV2能顺利生成NBG说明工具链本身对MobileNetV2这类经典结构的支持没有任何问题。那问题就集中在“你的自定义模型”和“官方模型”之间的差异上。这个差异无非集中在几个点输出层写法、输入/输出节点命名、预处理层归一化/缩放是否在模型内部、动态shape设置、量化校准数据集格式。把差异点列出来之后逐一对比排查效率是最高的。下面我把我列出的几个差异和对应的验证方法完整展开。2. 自定义模型与ST Model Zoo模型的五个关键差异2.1 输出层结构Softmax不是万能保险我自定义训练时用的是TensorFlow/Keras最后一层是Dense(num_classes, activationsoftmax)导出时tf2onnx正常转换成ONNXNetron里面也能看到完整的Softmax输出节点。按理说这没问题但问题恰恰出在这个“按理说”上。ST Model Zoo里的MobileNetV2输出层是Logits也就是不带Softmax的线性输出而且输出节点名字被非常明确地固定下来。工具链对官方模型的输出张量名有预期所以解析很顺利。自定义模型Softmax当然也能用关键是导出后输出节点的名字和位置不能被搞乱。我这次遇到的情况是Keras在保存H5模型时把输出层标记丢了转ONNX后输出节点变成了一个中间张量名工具链去匹配的时候扑了个空。给我的教训是自定义模型输出层越朴素越好。推荐的做法是最后一层用不带激活函数的Dense导出后手动加一个Identity节点固定输出张量名比如output名字完全交给工具链能识别的规范。这不是什么玄学是为了减少变量。2.2 Keras模型导出ONNX时的输出节点丢失这是最常见也最隐蔽的一个坑。如果你在Keras里这样定义模型inputs tf.keras.Input(shape(224, 224, 3)) x tf.keras.layers.Rescaling(1.0 / 127.5, offset-1)(inputs) x tf.keras.applications.MobileNetV2(...)(x) outputs tf.keras.layers.Dense(1000, activationsoftmax, nameoutput)(x) model tf.keras.Model(inputs, outputs)看起来很正常对吧但如果你用model.save(model.h5)保存再用tf2onnx.convert.from_keras(model, output_pathmodel.onnx)转换有些版本会默认只保留最后一个操作节点的第一个输出作为ONNX输出。更离谱的是如果这个操作节点在后续优化里被折叠掉ONNX里就只剩下一个“悬空”的输出张量引用或者输出节点根本被移除。我在Netron里打开转换后的ONNX文件发现输出节点名字变成了model_dense_4_output_0这种自动生成的名字而且这个张量在计算图里没有对应的消费者虽然Netron能显示出来但STM32Cube.AI在静态分析时把这个节点标记为“非输出”于是计算图最终没有任何输出。解决办法在Keras导出前不用着急先检查model.outputsprint(model.outputs) # 确认输出张量名如果不放心最好用函数式API显式指定输出并且在转换时指定outputs参数import tf2onnx import tensorflow as tf spec (tf.TensorSpec((1, 224, 224, 3), tf.float32, nameinput)) onnx_model, _ tf2onnx.convert.from_keras( model, input_signaturespec, output_pathmobilenetv2_custom.onnx, opset13 )转换完成后一定要打印ONNX图的输出节点确认根因import onnx model onnx.load(mobilenetv2_custom.onnx) print([o.name for o in model.graph.output])如果输出节点是空白或者名字不对说明导出过程已经把输出丢了需要重新处理模型文件本身。很多时候问题不是出在ST工具链而是模型文件在导出那一刻就已经“残缺”了。2.3 动态batch和动态shape带来的解析偏差Keras在导出时默认有一个动态batch维度None。你用tf.TensorSpec((None, 224, 224, 3))导出ONNX图上一般会写成dim_param(None)。ST的模型解析器对动态维度支持并不算好尤其在生成NBG做静态内存规划时它倾向于要求所有维度都是常量。Model Zoo里的模型全部固定成batch1所有张量shape都是静态的。自定义模型如果带着动态维度工具链虽然不会直接报“shape错误”但在图遍历时某些算子因为shape无法确定输出推断不下来最后就会走到“没有输出”这个结局。我在排查时特意用onnxruntime验证了一下动态维度模型在CPU上推理没问题因为runtime是动态分配内存的。但STM32Cube.AI做的不是runtime推理它是在编译期规划内存动态shape直接导致后续所有张量的内存偏移算不出来。修复方法导出ONNX时固定batch为1并且把所有输入shape设为静态。tf2onnx的写法spec (tf.TensorSpec((1, 224, 224, 3), tf.float32, nameinput))固定shape之后重新用Netron检查确保所有中间张量shape都带具体数字没有问号。2.4 模型内置预处理层增加识别成本很多自定义模型习惯把归一化直接做进模型里输入层加一个Rescaling或者Normalization层这样部署时应用端不用再额外做预处理。想法很好但代价是转换链路上多了一层处理而ST模型解析器对这个层的融合能力有限。Model Zoo的模型默认输入范围是[0,1]或者[-1,1]归一化是在推理前由外部代码做的。这样模型文件本身非常干净解析器一眼就能看懂输入和输出的张量语义。自定义模型把Rescaling(1.0/127.5, offset-1)放在输入层之后相当于在计算图最前面增加了一个“处理节点”而工具链在生成NBG时可能无法把这个节点融合到头层的卷积/BN里导致图结构出现异常。踩过一轮之后我的建议是训练和推理时预处理逻辑放模型外。模型输入就是原始像素0~255或0~1都行归一化放在C代码里做或者放在工具的预处理配置里做。这样做最保险转换链路短模型文件也符合工具链的预期结构。2.5 量化校准数据集与模型输入不匹配STM32Cube.AI在生成NBG时如果开启量化通常int8需要一个代表数据集Representative Dataset来做激活值范围校准。如果你的模型输入是224x224x3但校准数据给的图片尺寸不对或者数值范围不对校准过程可能直接失败然后整个生成流程走入异常分支。Model Zoo配套的校准脚本和模型输入完全匹配所以能顺利通过。自定义模型如果在校准集格式上没有严格对齐就会在量化阶段静默失败最终表现为“没有任何输出”。这个坑比较隐蔽因为报错不会直接提示“量化失败”它只是没有输出。我的做法是写一个简单的Python脚本直接从验证集抽100张图Resize到模型输入尺寸转成uint8数组存成.npy或者工具链要求的目录结构然后先关掉量化跑一次再开量化跑一次两步对比能快速定位是否为校准集的问题。3. 从零开始的完整修复步骤3.1 第一步固定输入输出节点并重新导出ONNX把模型在Python里重新导出一次这是整个排查的起点。以Keras为例我最终采用的完整导出代码如下import tensorflow as tf import tf2onnx model tf.keras.models.load_model(custom_mobilenetv2.h5) # 固定batch1的输入签名避免动态shape spec (tf.TensorSpec((1, 224, 224, 3), tf.float32, nameinput),) # 用opset13兼容性较好 onnx_model, _ tf2onnx.convert.from_keras( model, input_signaturespec, opset13, output_pathcustom_mobilenetv2_fixed.onnx, ) import onnx m onnx.load(custom_mobilenetv2_fixed.onnx) print(inputs:, [i.name for i in m.graph.input]) print(outputs:, [o.name for o in m.graph.output])这段代码跑完检查控制台打印出的输入输出名。正常情况下输出应该有一个明确的张量名比如logits或者dense_1。如果输出列表为空你的模型文件本身就有问题先修这个。3.2 第二步用Netron和onnxruntime双重验证打开Netron重点看不只是最后一层节点而是整个计算图的末端。你要确认输出张量是一个有生产者节点、并且没有消费者节点的张量输出张量天然没有消费者。然后再用onnxruntime做一次推理验证确保模型在CPU上能正常跑通import onnxruntime as ort import numpy as np sess ort.InferenceSession(custom_mobilenetv2_fixed.onnx) input_name sess.get_inputs()[0].name output_name sess.get_outputs()[0].name print(ORT output:, output_name) dummy np.random.rand(1, 224, 224, 3).astype(np.float32) result sess.run([output_name], {input_name: dummy}) print(shape:, result[0].shape)如果这一步结果正常模型文件本身是健康的问题极大概率出在ST工具链侧对某个节点的解析上。3.3 第三步先关闭量化纯FP32生成一次在STM32Cube.AI的图形界面里或者命令行里先把量化选项关掉直接用FP32生成NBG或者至少跑一次分析。如果FP32能生成成功说明模型结构和工具链兼容问题锁定在量化校准环节。命令行方式大概是这个风格stm32ai generate -m custom_mobilenetv2_fixed.onnx \ -o generated_fp32 \ --name my_model \ --verbosity 3--verbosity 3是关键它会输出分析阶段的详细日志能看到工具链识别出的每个输入输出张量名。如果日志里输出张量列表为空那就说明工具链根本没有找到输出问题还在模型文件侧。3.4 第四步校准数据集的格式对齐如果确认量化是罪魁祸首那就重新准备代表数据集。工具链通常要求一个目录里面放着若干张原始图片它的预处理配置里再设置和模型输入一致的scale/mean/offset。这里有一个容易忽略的细节图片的文件格式和色彩通道顺序也要对齐。如果你训练时用的是RGB但校准图片里有灰度图或者BGR顺序反了量化校准出来的范围就是错的有时候不会直接报错只是模型精度崩掉或者干脆输出为空。我的校准集准备脚本大概长这样import os import cv2 import numpy as np calib_dir calib_imgs os.makedirs(calib_dir, exist_okTrue) for i, img_path in enumerate(image_list[:100]): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (224, 224)) # 存成uint8工具链预处理里再做归一化 cv2.imwrite(os.path.join(calib_dir, fimg_{i}.png), img)然后在工具的预处理配置里填上scale1.0/127.5、offset-1把数值转换交给工具链而不是在导出的ONNX里内嵌归一化层。这种方法能让模型文件干净校准过程也更符合工具链的预期。3.5 第五步用官方Model Zoo模型验证工具链配置无误如果上面四步都走完还是失败别急着怀疑人生先把ST Model Zoo的MobileNetV2下载下来用完全相同的工具链版本和配置生成一次。如果官方模型能生成NBG说明工具链配置没问题问题缩小到“工具链对你的模型某层不支持”或者“ONNX文件某些字段不规范”。如果官方模型也失败那就不是模型的问题而是工具链安装/许可证/环境变量的问题这时候需要重新检查ST工具链安装、证书激活以及是否有对应STM32MP257的补丁包。这种“先排除环境再排除模型”的思路是排查部署问题的基本法。很多人一上来就盯着模型结构死磕结果发现是工具链版本不匹配白白浪费大量时间。4. 常见报错变体与排查方向速查我整理了一张表记录我在网上查资料、以及自己实践过程中遇到的各类相关报错和对应的处理方向方便你直接对照排查。报错现象大概率原因优先排查方向Generation does not contain any output输出节点在导出时丢失或未被识别检查ONNX graph.output固定输出节点名输出节点识别到一个但shape是空动态shape或未知维度固定输入shape确保所有张量维度静态量化开启后失败FP32正常校准数据集与模型输入不匹配重做校准集检查尺寸/通道/预处理配置报Unsupported operator模型里有工具链不支持的算子查看日志定位具体算子替换或重写该层转换过程中内存/闪存超限模型参数量超过目标资源用量化、通道剪枝降低模型体积生成NBG但推理结果全为0预处理不一致或输出层被错误裁剪检查输入输出尺度跑一遍板端测试这条表只是排查的起点遇到具体报错还是要把日志翻到最前面看第一处warning和error很多工具链会在warning里提示“ignore output node xxx”这才是真正的线索。5. 实操心得自定义模型部署到MP257的几条靠谱经验5.1 模型导出后第一时间检查计算图完整性我现在的习惯是任何模型要跑NBG生成之前一定先用脚本把ONNX的输入输出节点名打印出来再在Netron里肉眼过一遍计算图末端。不要因为模型在训练框架里能推理就默认导出文件没问题训练框架的运行时宽容度远高于静态转换工具链。计算图里一个“悬空”的输出节点在训练框架里可能只是个小瑕疵但到了STM32Cube.AI这里就会发展成“没有输出”的致命错误。5.2 模型文件越“朴素”越好少堆技巧这个结论可能有点反直觉但部署到嵌入式平台时模型文件里多一个节点就多一分风险。像Softmax这种在分类头里看起来必不可少的层在生成NBG时可以先用没有激活的Dense输出把Softmax拿到板端C代码里算或者干脆用工具链自带的输出处理。预处理层同理能放外部就放外部。模型文件结构越接近“纯卷积BNReLU全连接”这种经典形态工具链的兼容性就越好。我在第一次排查时就是因为舍不得去掉模型里的Rescaling层绕了很久。后来狠心把预处理从模型里抽掉一步就过了。当然这会牺牲一点应用端的便利性需要在C代码里手动做归一化但对部署稳定性的提升是肉眼可见的。5.3 善用命令行和详细日志别只盯着图形界面STM32Cube.AI的图形界面用起来很方便但排查问题的时候还是命令行模式更直接。重点看两个信息一是解析阶段找到的输入输出张量列表二是第一个error出现前的warning信息。命令行工具支持--verbosity参数值越高日志越详细。遇到解析类问题直接开到最大基本能把模型图结构里每个节点的处理情况都打出来。这一步能节省大量猜测时间。我当时就是因为命令行日志里出现了[warning] output name ... not found in graph outputs才最终确认是输出节点名字对不上而不是什么复杂的算子兼容问题。5.4 注意工具链对输出节点解析的“静默裁剪”行为有一个很容易被忽略的行为工具链在分析模型时如果发现某个节点尤其是尾部节点没有任何消费者并且没有在模型文件里被声明为输出它就会在优化阶段直接把这个节点删掉。如果你模型的最后一层恰好只是某个中间张量挂了个名字但ONNX文件里的graph.output没有正确引用它转换出来的NBG就是空壳。给输出增加一个Identity节点是当前比较靠谱的避免方式。在PyTorch侧可以这样导出import torch class ModelWrapper(torch.nn.Module): def __init__(self, model): super().__init__() self.model model self.identity torch.nn.Identity() def forward(self, x): y self.model(x) return self.identity(y) torch.onnx.export( ModelWrapper(model), dummy_input, model.onnx, input_names[input], output_names[output], opset_version13, )Keras侧可以用tf.identity包一层再导出。反正核心思想就是“让输出张量有一个明确、稳定、且不被优化的名字。”5.5 和模型结构相比工具链版本的一致性更该提前确认最后提醒一点STM32Cube.AI对ONNX算子支持范围和版本密切相关。Model Zoo模型可能在旧版本工具链上能过换到新版本反而报错或者反过来。我这次遇到的情况虽然和模型导出有关但排查过程中也发现如果换一个工具链版本报错信息可能会从“Generation does not contain any output”变成“Unsupported operator”之类的别的东西。所以排查前先固定工具链版本、记录官方模型在当前版本下的表现再开始动模型这是最稳妥的路线。说一下我自己形成的最终工作流导出ONNX后先打印输入输出节点然后关量化做FP32分析验证结构再开量化做校准集验证一旦某个环节不通过就在这一步深挖绝不跳过。这套流程帮我在之后部署EfficientNet-Lite和自研分类模型时省了大量时间基本能保证“一次生成NBG成功”。代码里那些细节都是一个个坑填出来的希望你能直接用上。
返回列表