从TensorFlow到Rockchip NPU:MobileNet V2模型在YY3568开发板的完整部署实践

发布时间:2026/5/19 6:51:12

从TensorFlow到Rockchip NPU:MobileNet V2模型在YY3568开发板的完整部署实践 1. 项目概述与核心目标在上一章的探索中我们使用官方提供的例程对YY3568开发板上的NPU神经网络处理单元有了初步的“手感”。那感觉就像拿到一台新相机先用自动模式拍了几张大概知道了它的对焦速度和色彩倾向。但作为一名开发者我们的目标从来不是仅仅运行别人的Demo而是要让这块强大的硬件为我们自己的任务服务。本章我们就将迈出这关键一步将一个来自TensorFlow官方模型库的预训练模型经过一系列“翻译”和“适配”最终部署到YY3568的NPU上并完成端到端的推理验证。这个过程的核心是打通从通用深度学习框架TensorFlow到专用硬件加速平台Rockchip NPU的链路。我选择的模型是Google在ImageNet数据集上预训练的MobileNet V2宽度乘数0.35输入尺寸224x224。这个选择基于几点考量首先MobileNet系列是轻量级模型的代表非常适合在嵌入式设备上部署其次其结构相对标准有助于我们聚焦于模型转换流程本身而非复杂的模型结构调整最后从官方渠道获取的模型确保了其正确性便于我们在转换前后进行结果比对精准定位问题。整个流程将围绕两个核心问题展开第一如何将TensorFlow SavedModel格式的模型使用RKNN-Toolkit2工具链转换为YY3568 NPU可识别的.rknn格式第二也是实践中极易踩坑的一点为何转换后的模型在PC上仿真的结果与原始模型推理结果不一致我们将深入细节从环境搭建、模型转换、参数调试到最终的上板实测完整复现这一过程并分享其中遇到的依赖冲突、配置陷阱及其解决方案。2. 环境准备与模型获取在开始模型转换的“魔法”之前我们必须先搭建一个稳定且兼容的“魔法工作台”。这个环境的核心是RKNN-Toolkit2它是Rockchip提供的官方模型转换、推理和性能评估工具包。然而直接将它与我们从TensorFlow Hub下载的模型对接并非一帆风顺。2.1 模型来源与初始状态分析我直接从TensorFlow Hub获取了目标模型imagenet/mobilenet_v2_035_224/classification/5。下载后解压其目录结构如下imagenet_mobilenet_v2_035_224_classification_5/ ├── saved_model.pb └── variables/ ├── variables.data-00000-of-00001 └── variables.index这是一个标准的TensorFlow SavedModel格式包含一个定义计算图的pb文件和一个存储权重的variables目录。RKNN-Toolkit2虽然支持TensorFlow模型导入但其load_tensorflow接口在加载SavedModel时往往对模型内部的算子Op版本和结构有特定要求直接加载成功率不高。更常见的做法是提供一个“冻结”的FrozenGraphDef.pb文件即计算图和权重都被固化在同一个文件里。因此我们的第一步是在Python环境中加载这个SavedModel并将其转换为一个包含所有权重信息的、单一的冻结图文件。这里我使用了tensorflow_hub来加载模型并用tf.saved_model.save再次保存为Keras可识别的格式最后通过一个自定义函数将其冻结。注意模型格式的“冻结”是关键一步。在训练阶段模型的权重是存储在独立的变量中的便于更新。而在部署阶段我们需要一个静态的、所有参数都变为常量的计算图这就是“冻结”。冻结后的模型体积稍大但结构完整消除了对变量检查点的依赖非常适合转换和部署。2.2 Python环境与依赖冲突的攻坚这里遇到了本次实践的第一个主要挑战依赖冲突。我最初在Solus系统上使用Python 3.8环境但在尝试冻结模型时TensorFlow的一些内部函数报错提示某些功能不被支持。经过排查发现与TensorFlow版本和Python版本的兼容性有关。转而尝试Python 3.10环境。根据RKNN-Toolkit2的发布说明其1.5.0版本已支持Python 3.10但这是在Ubuntu系统下的测试结果。在Solus上通过pip安装rknn_toolkit2-1.5.0的wheel包时一个关键的依赖项tensorflow-estimator卡住了我们。pip从PyPI拉取的tensorflow-estimator2.8.0版本在其setup.py中声明的Python版本支持上限是3.7这导致在Python 3.10下安装被拒绝。然而通过查看该项目的GitHub仓库我发现其主分支的代码已经更新移除了对Python 3.10的限制。这意味着我们需要手动构建一个支持Python 3.10的tensorflow-estimator包。解决方案如下克隆源码并修改版本标识克隆tensorflow/estimator仓库修改setup.py文件。关键改动有两处一是将版本号_VERSION从2.8.0改为一个开发版标识如2.8.0.dev2021122109以避免与官方稳定版冲突二是将项目名称project_name改为tf-estimator-nightly方便后续识别。-_VERSION 2.8.0 _VERSION 2.8.0.dev2021122109 -project_name tensorflow_estimator project_name tf-estimator-nightly使用Bazel构建该项目使用Bazel构建系统。不能简单地运行python setup.py sdist。正确的命令是bazel build //tensorflow_estimator/tools/pip_package:build_pip_package bazel-bin/tensorflow_estimator/tools/pip_package/build_pip_package /tmp/estimator_pip这会在/tmp/estimator_pip目录下生成一个.whl格式的安装包。安装自定义包使用pip安装本地构建的wheel包。python3.10 -m pip install /tmp/estimator_pip/tensorflow_estimator-*.whl解决了这个“拦路虎”后再安装RKNN-Toolkit2的wheel包就顺利了。这个过程的教训是在Linux边缘计算开发中遇到Python包依赖问题直接查阅上游GitHub仓库的Issue和最新代码往往是比盲目搜索更高效的解决方式。很多嵌入式工具链的依赖可能尚未及时更新到PyPI。2.3 模型冻结与验证脚本环境就绪后我编写了以下脚本用于加载TensorFlow Hub模型、进行推理测试并最终冻结模型。脚本的核心是h5_to_pb函数虽然我们是从SavedModel开始但函数名沿用了旧习惯它利用tf.function和convert_variables_to_constants_v2将模型的计算图与权重合并。#!/usr/bin/python3.10 import tensorflow as tf import tensorflow_hub as hub import numpy as np import sys from tensorflow.python.framework import convert_to_constants as _convert_to_constants def h5_to_pb(model_save_path): 将保存的模型转换为冻结的GraphDef pb文件。 model tf.keras.models.load_model(model_save_path, compileFalse) full_model tf.function(lambda Input: model(Input)) full_model full_model.get_concrete_function(tf.TensorSpec(model.inputs[0].shape, model.inputs[0].dtype)) frozen_func _convert_to_constants.convert_variables_to_constants_v2(full_model) tf.io.write_graph(graph_or_graph_deffrozen_func.graph, logdir./frozen_models, namemodel.pb, as_textFalse) print(冻结模型已保存至 ./frozen_models/model.pb) # 加载标签文件 def load_label_file(path): with open(path, r) as f: return [line.strip() for line in f.readlines()] # 预处理图片函数 def load_and_format_img(path, img_size224): image tf.io.read_file(path) image tf.image.decode_jpeg(image, channels3) image tf.image.resize(image, [img_size, img_size]) image image / 255.0 # 归一化到[0,1] return np.expand_dims(image.numpy(), axis0) # 增加batch维度 # 主流程 LABEL_PATH ./ImageNetLabels.txt MODEL_PATH ./imagenet_mobilenet_v2_035_224_classification_5 CHECK_SRC ./goldfish_299x299.jpg labels load_label_file(LABEL_PATH) # 加载Hub模型并包装为Keras模型 hub_module hub.load(MODEL_PATH) model tf.keras.Sequential([hub.KerasLayer(hub_module)]) model.build([None, 224, 224, 3]) # 测试原始模型推理 test_image load_and_format_img(CHECK_SRC) predictions model.predict(test_image, verbose0) predicted_class_id np.argmax(predictions[0]) print(f原始模型推理结果: 类别ID {predicted_class_id}, 标签 {labels[predicted_class_id]}) # 保存为SavedModel格式然后再冻结 tf.saved_model.save(model, ./temp_saved_model) h5_to_pb(./temp_saved_model)运行此脚本我们得到了关键的frozen_models/model.pb文件同时也在终端看到了原始模型对测试图片一条金鱼的推理结果类别ID为2对应标签“goldfish”。这为后续的对比建立了基准。3. RKNN模型转换与关键参数解析有了冻结的PB文件我们就可以使用RKNN-Toolkit2进行转换了。这个过程不仅仅是格式转换还包含了模型优化、量化等步骤以适应NPU的硬件特性。我参考了工具包中inception_v3_qat的示例但其中藏着一个至关重要的陷阱。3.1 转换脚本编写与初步尝试转换脚本的主要步骤是创建RKNN对象、配置预处理参数、加载TensorFlow模型、构建含量化、导出RKNN模型、然后在PC上进行仿真推理。最初的脚本我几乎照搬了示例中的配置。from rknn.api import RKNN INPUT_SIZE 224 PB_FILE ./frozen_models/model.pb RKNN_MODEL_PATH ./mobilenet_v2_035_224.rknn if __name__ __main__: rknn RKNN(verboseTrue) # 配置模型 print(-- Config model) rknn.config(mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3568) print(done) # 加载TensorFlow模型 print(-- Loading model) ret rknn.load_tensorflow(tf_pbPB_FILE, inputs[Input], outputs[Identity], input_size_list[[1, INPUT_SIZE, INPUT_SIZE, 3]]) if ret ! 0: print(Load model failed!) exit(ret) print(done) # 构建RKNN模型进行量化 print(-- Building model) ret rknn.build(do_quantizationTrue, dataset./dataset.txt) if ret ! 0: print(Build model failed!) exit(ret) print(done) # 导出模型 print(-- Export rknn model) ret rknn.export_rknn(RKNN_MODEL_PATH) if ret ! 0: print(Export rknn model failed!) exit(ret) print(done) # 以下为仿真测试部分...运行这个脚本转换过程看似成功生成了.rknn文件。但在PC上进行仿真推理时发现输出的分类概率分布与原始模型的结果相差甚远Top-5的类别完全不对。问题出在哪里3.2 核心陷阱预处理参数mean_values与std_values问题的根源就在于rknn.config()中的mean_values和std_values这两个参数。这是模型转换中最容易出错也最关键的配置之一。参数含义这两个参数用于在NPU推理前对输入的图像数据进行归一化预处理。其计算公式通常为normalized_data (input_data - mean_values) / std_values。错误来源Inception V3示例中使用了mean_values[[127.5, 127.5, 127.5]]和std_values[[127.5, 127.5, 127.5]]。这是因为示例中的模型预期输入是uint8类型0-255且预处理为(input - 127.5) / 127.5最终将数值范围映射到[-1, 1]之间。我们的情况回顾我们之前的数据预处理函数load_and_format_img我们对图片进行的操作是image image / 255.0。这意味着我们提供给模型的输入数据已经是float32类型且范围在[0, 1]之间。而MobileNet V2模型通常期望的输入正是这个范围。因此如果我们照搬示例的配置RKNN模拟器会在推理前对我们的输入数据x做如下计算(x - 127.5) / 127.5。由于x在0~1之间减去127.5会得到一个巨大的负数再除以127.5完全扭曲了数据分布导致推理结果荒谬也就不足为奇了。正确的配置应该是rknn.config(mean_values[[0, 0, 0]], std_values[[1, 1, 1]], target_platformrk3568)或者更常见的是模型期望输入为0-255的整数预处理为(x - mean) / std其中mean和std是在ImageNet数据集上计算得到的RGB三通道的均值和标准差例如mean[123.675, 116.28, 103.53], std[58.395, 57.12, 57.375]。但我们的预处理步骤已经做了/255.0相当于把0-255映射到了0-1所以对于已经归一化到0-1的数据mean0std1就是正确的。实操心得预处理对齐是生命线。模型转换后结果不一致十有八九是预处理归一化参数没对上。务必仔细检查原始模型训练和推理时的预处理流程并与rknn.config中的配置进行精确匹配。一个笨但有效的方法是用同一张图片分别用原始模型框架和RKNN仿真推理打印出模型第一层卷积的输入数据或第一个算子的输入逐个像素对比很快就能发现差异。3.3 修正后的转换与仿真验证修正预处理参数后重新运行转换脚本。这次在仿真推理部分我们加载转换好的RKNN模型并对同一张金鱼图片进行推理。为了与原始模型的输出Softmax后的概率进行公平对比我们需要对RKNN模型的原始输出可能是量化后的整数或未经Softmax的logits进行后处理。在我的脚本中RKNN推理的原始输出outputs[0]被认为是logits未经过Softmax的得分因此我手动计算了Softmaxoutputs rknn.inference(inputs[img]) x outputs[0] # 手动计算Softmax将logits转换为概率 output np.exp(x) / np.sum(np.exp(x)) outputs [output] show_outputs(outputs) # 自定义函数打印Top-5结果修正后的仿真结果令人振奋red imagenet -----TOP 5----- [2]: 0.9564507007598877 [122]: 0.007486562244594097 [121]: 0.005268004257231951 [964]: 0.0018354274798184633 [119]: 0.0017108423635363579Top-1类别ID为2概率高达95.6%与之前原始TensorFlow模型的结果完全吻合。这表明模型转换成功且预处理、后处理流程正确无误。4. 模型部署与YY3568实测仿真通过只是第一步真正的考验在开发板上。我们需要将生成的.rknn模型文件、测试图片以及一个能在YY3568上运行的可执行程序一起放到板子上进行实测。4.1 编译与准备板端推理程序Rockchip为RK3568提供了丰富的示例代码。我选择了rknn_common_test这个程序因为它是一个通用的RKNN模型加载和推理示例支持单张图片输入并输出分类结果非常适合我们的测试场景。定位源码在RKNN-Toolkit2的SDK中或YY3568的BSP源码包里找到external/rknpu2/examples/rknn_common_test目录。交叉编译在x86开发机上使用YY3568对应的交叉编译工具链进行编译。通常需要指定工具链路径、RKNN库的头文件和库文件路径。# 假设环境已配置好 cd rknpu2/examples/rknn_common_test mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE../../cross_compile_toolchain.cmake .. make -j4编译成功后会生成rknn_common_test可执行文件。文件传输将编译好的可执行文件、转换正确的mobilenet_v2_035_224.rknn模型文件、以及测试图片goldfish_224x224.jpg通过SD卡、ADB或NFS等方式传输到YY3568开发板上。我使用的是NFS挂载方便多次调试。4.2 板端执行与结果分析通过串口或SSH登录到YY3568开发板进入文件所在目录执行以下命令./rknn_common_test mobilenet_v2_035_224.rknn goldfish_224x224.jpg程序运行后会首先打印模型信息包括输入输出张量的细节如维度、数据类型、量化参数等然后进行推理并输出性能和时间最后打印Top-5的分类结果。以下是我在YY3568上实测的输出片段rknn_api/rknnrt version: 1.3.0, driver version: 0.7.2 model input num: 1, output num: 1 input tensors: index0, nameInput:0, n_dims4, dims[1, 224, 224, 3], n_elems150528, size150528, fmtNHWC, typeINT8, qnt_typeAFFINE, zp-128, scale0.003922 output tensors: index0, nameIdentity:0, n_dims2, dims[1, 1001], n_elems1001, size1001, fmtUNDEFINED, typeINT8, qnt_typeAFFINE, zp-66, scale0.070292 ... Begin perf ... 0: Elapse Time 3.36ms, FPS 297.53 ---- Top5 ---- 11.738696 - 2 7.029160 - 122 6.958868 - 121 5.693619 - 119 5.553036 - 964关键信息解读模型信息输入为INT8量化类型格式NHWC尺寸1x224x224x3。输出也是INT8量化。这证实了我们在rknn.build(do_quantizationTrue)时启用的量化是生效的它显著减少了模型大小并提升了NPU的推理效率。性能数据单次推理耗时3.36毫秒换算成帧率FPS约为297.5。这个速度对于224x224分辨率的图像分类任务来说非常出色充分体现了NPU的加速能力。推理结果输出显示的是量化后的得分可能是经过某种缩放后的logits而不是概率。排名第一的类别ID是2得分远高于其他类别。这与我们在PC上仿真得到的结果类别2为金鱼完全一致且排序也与仿真结果的Top-5顺序2, 122, 121, 119, 964基本吻合略有浮动是量化带来的微小误差。注意事项量化误差。启用do_quantizationTrue后模型权重和激活值从浮点数转换为8位整数这会引入轻微的精度损失。因此板端运行的量化模型输出得分与PC仿真或原始浮点模型的概率值无法直接数值比较但类别排序关系尤其是Top-1应该保持一致。如果排序发生巨大变化则需要检查量化数据集dataset.txt是否具有代表性或尝试调整量化算法参数。5. 常见问题与深度排查指南在整个流程中除了上述关键的预处理参数问题还可能遇到其他“坑”。这里将常见问题、排查思路和解决方案汇总如下希望能帮你节省大量调试时间。5.1 模型加载失败问题现象rknn.load_tensorflow或rknn.load_onnx等接口返回错误。排查步骤确认模型格式确保提供给RKNN的是冻结的FrozenGraphDef.pb文件。使用Netron等可视化工具打开模型检查输入输出节点名称是否与代码中inputs和outputs参数指定的一致。检查算子支持RKNN-Toolkit2对TensorFlow/ONNX/PyTorch算子的支持是有限的。查看工具包文档中的《算子支持列表》确认模型中所有算子都在支持范围内。遇到不支持算子可能需要修改模型结构或等待RKNN更新。版本兼容性确认RKNN-Toolkit2版本与TensorFlow/PyTorch/ONNX版本之间的兼容性。使用过高的深度学习框架版本可能导致未知错误。5.2 仿真结果正确但板端结果错误问题现象PC仿真rknn.inference结果与预期相符但部署到YY3568上结果完全错误。排查步骤预处理一致性再次强调这是最常见原因。确保板端推理程序如rknn_common_test对输入图片的预处理裁剪、缩放、颜色空间转换、归一化与模型转换时rknn.config中设定的mean_values、std_values以及channel_mean_value等参数完全匹配。一个有效的方法是在PC端用Python脚本模拟板端程序的预处理流程生成一个二进制文件同时在板端程序中将预处理后的数据也保存下来然后用diff工具比较必须完全一致。数据布局Layout确认输入数据的格式是NHWC还是NCHW。RKNN模型信息中fmtNHWC就指明了格式。OpenCV默认读取图片是HWC格式需要确保传递给模型的四维数据[1, H, W, C]符合要求。量化问题如果使用了量化检查板端输出层的scale和zp零點参数。在PC仿真时RKNN库可能自动进行了反量化而板端C程序可能需要手动处理。对比仿真和板端输出的原始整数值在反量化之前看它们是否呈线性关系。5.3 性能未达预期问题现象推理速度慢FPS远低于官方示例或预期。排查步骤启用NPU而非CPU首先通过rknn.init_runtime(targetrk3568)或在板端设置环境变量export NPU_LOG1等方式确认推理确实运行在NPU上而不是回退到了CPU。模型优化级别在rknn.build时可以尝试optimization_level参数设置为3最高优化级别可能会带来性能提升但需测试精度是否可接受。输入输出耗时板端测试程序打印的Elapse Time是否包含了图片加载、预处理的时间确保计时区间只包含纯推理rknn_inputs_set到rknn_outputs_get的部分。电源与频率检查开发板是否运行在性能模式。有些板子为了省电默认是平衡或节能模式需要手动切换到性能模式如echo performance /sys/devices/system/cpu/cpufreq/policy0/scaling_governor。5.4 内存不足或推理崩溃问题现象板端程序在初始化或推理时段错误Segmentation Fault或直接退出。排查步骤检查内存使用free -m命令查看系统可用内存。较大的模型可能需要较多内存。RKNN在初始化时会加载模型权重推理时需要输入输出缓冲区。模型尺寸检查生成的.rknn文件大小。过大的模型可能超出板载内存限制。考虑使用更小的模型、更激进的量化如INT8量化或模型剪枝。栈空间在编译板端程序时可以尝试增加栈大小如通过-Wl,-z,stack-size1048576链接器选项。日志分析运行板端程序前设置export RKNN_LOG_LEVEL3或更高等级将详细的日志重定向到文件分析崩溃前的最后几条日志信息。整个从TensorFlow模型到YY3568 NPU部署的流程走通后最大的体会是细节决定成败。任何一个环节的微小偏差如预处理的一个参数、数据格式的一个维度都可能导致最终结果的失败。建立一套可复现、可对比的调试方法如保存中间数据、进行逐层对比至关重要。这次成功部署MobileNet V2只是一个起点它为后续部署更复杂的自定义模型铺平了道路。当你掌握了这套流程和排查方法后让YY3568的NPU为你的AI应用加速就变成了一项按部就班、富有成就感的工作。

相关新闻