ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RK3588跑通FaceNet:模型转换、RKNN量化与边缘部署实战指南

RK3588跑通FaceNet:模型转换、RKNN量化与边缘部署实战指南 1. 项目整体设计与流程思路1.1 为什么选Facenet为什么不追新模型这几年边缘设备上做人脸识别的需求越来越常见考勤机、门禁闸机、园区安防、AI工位机都在做类似的事情。我在RK3588上选Facenet不是因为它在最新的人脸识别榜单上分数多高而是因为它在“边缘端落地”这个维度上实在太皮实了。Facenet的核心思路是把人脸图像映射到一个欧氏空间输出固定维度的embedding向量我用的Facenet PyTorch版本输出是512维之后人脸比对就退化成简单的向量距离计算。这个结构天然适合1:N人脸底库检索也方便配合常见数据库做向量索引。相比之下现在很多新模型动不动就上Transformer、混合注意力、多分支结构单看精度确实好但到了NPU转换环节算子能不能被工具链完整吃下来要打一个大大的问号。RK3588这颗芯片在国内边缘计算圈有多火不用多说8核CPU加上6TOPS算力的NPU关键还支持混合量化。也就是说模型里不同层可以用不同bit位宽去跑这对精度敏感型模型非常有价值。我用的是RK3588S核心板配LPDDR4 8G内存不带风扇的情况下跑Facenet这种量级的模型绰绰有余。选它的另一个原因是可以灵活扩展USB摄像头、MIPI屏、串口等外设对于做人脸识别场景IO接口丰富度很重要。对比项RK3588Jetson Nano树莓派4BNPU/GPU算力6 TOPS INT8472 GFLOPS无混合量化支持不支持CUDA无内存最大32G可选2G/4G2G/4G/8G模型工具链rknn-toolkit2TensorRTTFLite边缘部署难易度中等中等较难1.2 转换链路的选择逻辑从零到板端跑通整个模型链路是PyTorch训练好的模型权重 - 导出ONNX - 图优化与精度校验 - 用rknn-toolkit2转成RKNN格式 - RK3588板端用NPU加载推理。有人会问为什么不直接从PyTorch转到RKNN说实话rknn-toolkit2虽然支持直接加载PyTorch但面对动态控制流、某些自定义算子、特殊激活函数时报错信息非常劝退。而ONNX格式作为行业中间格式被瑞芯微工具链反复打磨了很多个版本兼容性最稳定。所以我的建议是宁可中间多一步也不要图省事直接怼PyTorch。另外一个部署方式上的选择RK3588实际有两种方案一种是在开发板上安装rknn-server做实时模型转换另一种是在x86宿主机上提前转好.rknn文件再拷贝到板端运行。我自己只把宿主机转换当作正式方案板端实时转换只在调试烧录阶段偶尔用。原因很简单宿主机上报错信息完整python环境好控制跑一轮转换速度快得多。等你把转换脚本调通、模型验证没问题了板端就只是干净地做推理出事概率低很多。2. 开发环境搭建宿主机与板端两头一起踩坑2.1 宿主机准备rknn-toolkit2我强烈建议在独立的Python虚拟环境里安装rknn-toolkit2不要直接装到系统Python。因为rknn-toolkit2的依赖和基础环境经常打架我就见过有人把Ubuntu系统Python装坏后面修了半天pip的依赖。宿主机我用的Ubuntu 20.04 x86_64Python 3.8。rknn-toolkit2的版本选1.6.0这个版本在RK3588上已经比较成熟。安装之前先把几个基础库装好sudo apt-get update sudo apt-get install -y libxslt1-dev zlib1g-dev libglib2.0-0 libsm6 \ libxext6 libxrender-dev libgomp1 libgl1-mesa-glx然后是创建虚拟环境并安装rknn-toolkit2python3 -m venv rknn_env source rknn_env/bin/activate pip install --upgrade pip pip install rknn-toolkit21.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple这里有两个坑一个是rknn-toolkit2安装包比较大建议用国内源不然下载时间很长另一个是安装完以后务必跑一下官方自带的demo验证安装是否成功不要等转模型时才发现环境有问题。验证方式是进入rknn-toolkit2包目录下的examples文件夹直接跑一个不带量化的简易demo比如mobilenet的分类示例。如果连这个示例都跑不通那基本是环境不对先把这一步解决再往下走。2.2 RK3588板端系统与NPU驱动板端我刷的是Ubuntu系统镜像具体版本是官方提供的Ubuntu 22.04桌面版。这里有一个容易忽略的点如果你后续打算做摄像头图像采集和OpenCV图像处理建议刷桌面版带完整GPU驱动的镜像不要为了省资源用server版因为有些基础库还是要自己装一批。板端需要安装对应版本的rknn runtime库也就是rknpu2驱动和rknn-toolkit-lite2。以我用的1.6.0版本为例去rknn-toolkit2仓库的rknpu2目录下把对应的deb或库文件拷贝到板子调试最好# 板端安装 sudo cp librknnrt.so /usr/lib/ sudo cp include/* /usr/include/ # 检查NPU设备是否识别 ls /dev/rknpu如果/dev/rknpu设备节点不存在先检查驱动程序有没有正确加载。另外也需要确认板子的固件版本和rknpu2库版本匹配这一条我放到后面问题排查小节说因为这里踩坑的人太多了。2.3 PyTorch侧环境配置Facenet我用的是facenet-pytorch库这个库比较老了直接pip install默认版本在某些情况下和最新版PyTorch会不兼容比如可视化的模块内部有老函数被删掉的问题。我的建议是固定依赖pip install facenet-pytorch1.0.2 pip install torch2.0.1 torchvision0.15.2这两个版本搭配我自己实测是能顺利加载vggface2预训练权重并且正常导出ONNX的。另外有一点需要注意facenet-pytorch这个库封装了MTCNN人脸检测但我们部署时往往只需要InceptionResnetV1这个特征提取部分也就是FaceNet的核心主干检测部分用OpenCV或者推理效率更高的模型代替即可。3. FaceNet模型导出ONNX全流程3.1 吃透模型结构导出前心里有数Facenet在facenet-pytorch里的默认主干是Inception-ResNet-v1网络内部由Inception块和残差连接堆叠组成最后接一个全连接层输出256维或512维的特征向量。针对RKNN转换我对模型规格做了两个限制第一输入尺寸固定为160x160x3因为onnx导出时如果保留动态宽高RKNN转换器对动态Shape的处理相对保守部分算子容易报错。第二batch维度保留为动态方便后面板端推理时按实际需要一次送1张或多张图。输出是512维的embedding向量并且模型内部已经做了L2归一化。Facenet的核心思想就是让相同身份的人脸向量距离近不同身份的距离远。这个L2归一化很关键转RNK时要注意这个算子是否被工具链兼容我在5.3小节会详细说。3.2 导出ONNX的完整脚本在正式导出之前务必把模型切到eval模式并且把梯度关闭否则导出的模型里会混入很多与推理无关的节点。下面是我最终跑通的导出脚本可以直接复制使用import torch from facenet_pytorch import InceptionResnetV1 # 1. 加载预训练权重 model InceptionResnetV1(pretrainedvggface2, classifyFalse, num_classesNone).eval() # 2. 关闭梯度减少导出图的冗余 for param in model.parameters(): param.requires_grad False # 3. 构造一个固定输入尺寸的tensor input_names [input] output_names [embedding] dynamic_axes { input: {0: batch_size}, embedding: {0: batch_size}, } dummy_input torch.randn(1, 3, 160, 160) # 4. 导出ONNX torch.onnx.export( model, dummy_input, facenet.onnx, input_namesinput_names, output_namesoutput_names, dynamic_axesdynamic_axes, opset_version12, do_constant_foldingTrue, verboseFalse, ) print(导出完成)这里opset_version我固定用12不要追高到17、18。rknn-toolkit2对旧版opset的支持很成熟新版opset反而可能出现某些新op不兼容的问题。3.3 导出过程的常见报错与处理老版本facenet-pytorch和PyTorch不兼容时最常见的报错是module has no attribute _apply或者forward() got an unexpected keyword argument。这类问题基本是库之间的版本冲突解决方案就是把torch降到2.0.1。另一个坑是导出时提示某些算子不支持比如部分版本的PyTorch会自动导出为aten::native_batch_norm这倒不影响后续转换。真正需要警惕的是模型里出现adaptive_avg_pool、std这类算子ONNX里能表示但RKNN工具链不一定转得动。Facenet主干的池化结构还算收敛实测没有遇到太离谱的算子。导完之后一定要打印一下模型输入输出节点确认尺寸import onnx model onnx.load(facenet.onnx) print(onnx.helper.printable_graph(model.graph))4. ONNX模型优化与精度对齐4.1 用onnxsim做一次图优化ONNX导出图一般带着很多冗余节点例如Identity节点、Cast节点、无用Shape输出等。这些冗余节点不会影响计算结果但到了RKNN转换阶段多一个节点就多一个天然的风险点。所以推荐转RKNN之前先做一次ONNX结构优化。安装onnxsimpip install onnx-simplifier命令行简化python -m onnxsim facenet.onnx facenet_sim.onnx简化完之后模型文件体积通常会略微变小图结构也清爽很多。实测Facenet模型从原始ONNX简化后节点数能减少大概10%到20%RKNN转换时的报错概率明显降低。4.2 用ONNX Runtime验证输出一致性转RKNN之前必须先确认ONNX模型和PyTorch原模型的输出是否一致。这一步很多人会跳过觉得反正都是同一个模型不会有问题。但实际上ONNX导出时因为opset算子的差异某些层的计算顺序会被重排输出结果可能和PyTorch有细微差别。这一点差距在分类任务上可能只影响最后的softmax结果但在Facenet这种embedding型模型上会直接影响相似度计算不可忽略。验证方式非常简单import onnxruntime as ort import numpy as np import torch # 随机生成输入 x torch.randn(1, 3, 160, 160) # pytorch推理 with torch.no_grad(): pt_out model(x).numpy() # onnx推理 sess ort.InferenceSession(facenet_sim.onnx, providers[CPUExecutionProvider]) ort_out sess.run([embedding], {input: x.numpy()})[0] # 计算余弦相似度 def cos_sim(a, b): return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) print(余弦相似度:, cos_sim(pt_out, ort_out))正常情况余弦相似度应该在0.9999以上。如果相似度偏低优先检查输入数据是否对齐再考虑onnx导出时是否有算子精度丢失。4.3 图像预处理一定要跟训练时保持一致这是整个项目里最容易被忽略、但影响最大的一个环节。Facenet-pytorch预训练模型的预处理逻辑是先把图像像素值从0到255归一化到0到1然后做均值0.5、方差0.5的标准化换算成像素公式就是最终输入 (原始像素值 / 255 - 0.5) / 0.5 原始像素值 / 127.5 - 1也就是说最终给到模型的输入范围在-1到1之间。在RKNN转换和板端推理时都要严格使用同一套预处理公式。很多人在板端直接用OpenCV读图然后把uint8数组传给模型结果精度掉到没法看问题绝大多数出在这。另外一个坑是通道顺序。OpenCV默认读进来是BGR但Facenet训练时用的是RGB。处理方法是先用cvtColor转一次或者在后处理时调换通道。我自己习惯统一在预处理函数里做严格顺序控制避免代码里到处散落转换逻辑降低出错概率。5. ONNX转RKNN量化配置与精度控制实战5.1 转换脚本与核心参数讲解RKNN转换最核心的是rknn.config里的参数配置。这一步直接决定了模型后续的推理精度和速度。先给出完整转换代码from rknn.api import RKNN rknn RKNN() # 1. 配置目标平台和预处理参数 rknn.config( mean_values[[127.5, 127.5, 127.5]], std_values[[127.5, 127.5, 127.5]], target_platformrk3588 ) # 2. 加载ONNX模型 ret rknn.load_onnx(modelfacenet_sim.onnx) assert ret 0, load onnx failed # 3. 构建RKNN模型 ret rknn.build(do_quantizationTrue, dataset./dataset.txt) assert ret 0, build rknn failed # 4. 导出RKNN文件 ret rknn.export_rknn(facenet.rknn) assert ret 0, export rknn failed print(转换完成)关于mean_values和std_values这里的原理是RKNN工具链会对模型的输入做一步预处理output (input - mean) / std。我们希望在板端输入原始图像时工具链自动完成x / 127.5 - 1的操作。换一个思路算如果设mean127.5std127.5实际计算就是(x - 127.5) / 127.5这等价于x/127.5 - 1和PyTorch里的Normalize(0.5,0.5)完全一致。这样板端代码里就不用再做一遍浮点归一化直接把uint8图像数组传给NPU就行省了一次批量内存转换。这是一个值得在同事之间共享的小技巧。5.2 dataset量化校准集怎么准备当do_quantizationTrue时rknn-toolkit2会读取一个dataset.txt文件里面是用于量化校准的图片路径列表每行一个路径。./calibration_data/0001.jpg ./calibration_data/0002.jpg ./calibration_data/0003.jpg ...量化校准图的选择有几个原则数量不用太多每组200到300张足够再多收益也不明显。图片内容必须来自真实部署场景。比如最终是门口闸机场景就不要拿一推网图人脸来量化尽量用实际摄像头拍摄的不同角度、不同光照的人脸。图片在送进dataset之前要确保已经进行过对齐或者裁剪最好和实际推理的输入内容分布一致。我实测过用500张通用人脸图片量化和用200张实际场景图片量化最终模型在场景数据上的余弦相似度表现后者明显更稳。当然如果没有条件采真实场景数据用训练集里面的一部分图片也是可行方案但务必保证类别多样、样本均衡。5.3 量化精度掉的常见原因和混合量化策略量化之后Facenet的embedding输出和FP32版本相比数值一般会有一定偏差这是正常的。关键看两件事第一同一个人两张照片的余弦相似度是否依然高第二不同人之间的相似度是否依然低。如果量化后这两组距离的区分度模糊了那就要处理。如果明确定位是量化导致的精度问题最简单的办法是检查哪些层对量化敏感。FaceNet的网络结构里最后的embedding输出前的全连接层和后接的L2归一化层往往对量化误差极其敏感。处理方式是把这些层回退到FP16甚至FP32其余层保持INT8。rknn-toolkit2在较新版本提供了层级混合精度配置。我用的1.6.0版本中可以在build之前通过指定custom_quantize配置把一个层列表设置为不量化。由于不同版本的接口可能会迭代当前版本的用法建议查一下对应版本的examples思路是先用默认INT8量化转一版在PC端跑推理对比精度找出误差最大的几个层再针对这些层做混合精度回退。整个过程听起来繁琐但实测收益明显。我当时把最终embedding输出的全连接层和前面两层Inception块的输出部分切成了FP16整体精度就恢复到了可用状态代价是推理时间增加不到10%完全值得。5.4 转换后在PC端做一次完整验证无论如何转换完以后先在PC端用rknn-toolkit2的模拟推理跑一遍不要直接拿到板端折腾。方式是在转换脚本里加载rknn模型并用同一个输入图片做推理# 在转换脚本末尾追加 outputs rknn.inference(inputs[img]) print(outputs[0])这里要对比的是同一张输入图片经过Facenet ONNX推理得到的embedding和经过RKNN INT8推理得到的embedding余弦相似度是否还在合理范围内。我自己的评判标准是余弦相似度大于0.95算及格大于0.98算优秀。低于0.9就要警觉低于0.8基本说明模型已经废了赶紧查量化。6. 板端NPU部署与C推理实现6.1 板端运行库安装板端推理不需要安装完整的rknn-toolkit2只需要安装轻量的运行库。RK3588对应的库叫rknn-toolkit-lite2它封装了底层NPU推理接口。在开发板Ubuntu系统上操作pip install rknn-toolkit-lite21.6.0 sudo cp librknnrt.so /usr/lib/ sudo ldconfig安装完毕后用python接口快速自测from rknnlite.api import RKNNLite rknn_lite RKNNLite() ret rknn_lite.load_rknn(facenet.rknn) ret rknn_lite.init_runtime() print(init runtime ok)如果init_runtime不报错说明NPU驱动和库版本匹配可以继续写C代码。6.2 C推理代码框架实际项目里我最终是用C写的推理代码因为板端要做多路视频流处理、结果上报和业务逻辑对接。下面给出一个精简但可运行的Facenet RKNN推理框架。#include cstdio #include cstring #include vector #include rknn_api.h #include opencv2/opencv.hpp static unsigned char *load_file(const char *path, int *size) { FILE *fp fopen(path, rb); fseek(fp, 0, SEEK_END); long sz ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char *buf (unsigned char *)malloc(sz); fread(buf, 1, sz, fp); fclose(fp); *size sz; return buf; } int main() { // 1. 加载RKNN模型 int model_size 0; unsigned char *model_data load_file(facenet.rknn, model_size); rknn_context ctx; int ret rknn_init(ctx, model_data, model_size, 0, nullptr); if (ret 0) { printf(rknn_init failed, ret%d\n, ret); return -1; } // 2. 查询模型输入输出信息 rknn_input_output_num io_num; rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, io_num, sizeof(io_num)); printf(input num: %d, output num: %d\n, io_num.n_input, io_num.n_output); rknn_tensor_attr input_attr; memset(input_attr, 0, sizeof(input_attr)); input_attr.index 0; rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, input_attr, sizeof(input_attr)); rknn_tensor_attr output_attr; memset(output_attr, 0, sizeof(output_attr)); output_attr.index 0; rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, output_attr, sizeof(output_attr)); // 3. 读取图片并做预处理已经由rknn.config里的mean/std处理归一化 cv::Mat bgr cv::imread(test_face.jpg); cv::Mat rgb; cv::cvtColor(bgr, rgb, cv::COLOR_BGR2RGB); cv::Mat resized; cv::resize(rgb, resized, cv::Size(160, 160)); // 4. 构造输入 rknn_input inputs[1]; memset(inputs, 0, sizeof(inputs)); inputs[0].index 0; inputs[0].type RKNN_TENSOR_UINT8; inputs[0].size 160 * 160 * 3; inputs[0].fmt RKNN_TENSOR_NHWC; inputs[0].buf resized.data; ret rknn_inputs_set(ctx, 1, inputs); if (ret 0) { printf(rknn_inputs_set failed, ret%d\n, ret); return -1; } // 5. 执行推理 ret rknn_run(ctx, nullptr); if (ret 0) { printf(rknn_run failed, ret%d\n, ret); return -1; } // 6. 获取输出 rknn_output outputs[1]; memset(outputs, 0, sizeof(outputs)); outputs[0].want_float 1; ret rknn_outputs_get(ctx, 1, outputs, nullptr); if (ret 0) { printf(rknn_outputs_get failed, ret%d\n, ret); return -1; } float *embedding (float *)outputs[0].buf; printf(embedding[0..4]: %f %f %f %f %f\n, embedding[0], embedding[1], embedding[2], embedding[3], embedding[4]); // 7. 释放资源 rknn_outputs_release(ctx, 1, outputs); rknn_destroy(ctx); free(model_data); return 0; }这段代码里有几个值得说明的地方。输入fmt设置了RKNN_TENSOR_NHWC这是因为模型推理时最常用的图像数据排布就是NHWCOpenCV的Mat数据直接可以对齐省去维度变换操作。预处理方面因为转换时已经设置了mean和std所以这里直接用uint8数据填进去NPU内部会自动执行归一化。如果你在转换时没有设mean/std那么这里输入的type就得改成RKNN_TENSOR_FLOAT32并手动把浮点归一化的结果填进去。两种方式我都试过推荐前者性能更好也省事。还有一个隐藏细节outputs[0].want_float 1表示让NPU把输出做反量化转成float格式。如果设成0拿到的就是int8量化后的定点数据需要自己在代码里根据scale和zero_point反推出float值。为了方便我直接让库帮我转float因为embedding层的输出数据量很小性能开销可忽略。6.3 性能实测与优化方向在RK3588上实测Facenet单个160x160输入NPU推理耗时大约5到8毫秒加上OpenCV的图像缩放、通道转换等预处理开销单帧端到端耗时稳定在12毫秒左右。这个数字完全满足实时视频流人脸识别的需求即使做多路视频流也还有大量余量。性能优化可以根据场景再做比如如果同时跑多路摄像头可以在一帧图像裁出多个人脸框后一次性打包成batch送进NPU充分利用batch并行能力。图像缩放尽量用成熟的硬件加速库比如RK3588的RGA硬件缩放比OpenCV的cpu版要快很多。特征比对阶段如果底库很大建议引入向量检索库或直接做特征抽查把搜索范围缩小后再做精确距离计算。7. 常见问题速查与防坑指南以下是我在实际项目中遇到的问题整理成的速查表几乎每一条都是花了大半天时间才定位出来的问题现象可能原因处理方案rknn模型推理输出全为0NPU初始化失败或输入数据为空检查/dev/rknpu设备降低rknn_init阶段错误init_runtime报错CRITICALrknn-toolkit-lite2版本与rknpu2驱动不匹配升级或降级对应版本保持两者版本一致量化后余弦相似度骤降校准集与真实场景差异大更换真实场景图片增加量化样本数量量化后部分人脸比对失败敏感性层被量化对全连接层等关键层回退FP16预处理不一致导致整体输出偏移均值方差或通道顺序错误核对训练时的transform逻辑统一mean/std转换时报Unsupported OperatorONNX图里包含工具链不支持的算子用onnxsim简化图或调整模型结构绕开该算子板端推理时内存持续增长输出资源未释放确认每次rknn_outputs_get后调用rknn_outputs_release推理速度特别慢实际上在跑CPU回退查看日志中有没有算子被打回CPU处理对应算子除了表格里的问题我再分享三个经验第一个是关于加载模型路径的坑。板端加载RKNN模型文件时路径太长或者中文路径可能导致加载失败尽量把模型放到/opt或家目录下的英文路径避免一些嵌入式文件系统对路径处理的限制。第二个经验和NPU多实例相关。如果业务同时需要跑几个人脸相关模型比如检测模型和Facenet模型不要重复调用rknn_init换模型再跑。合理做法是一次性把多个模型都初始化好或者使用同一个ctx在不同模型之间切换时注意重置状态。RK3588的NPU可以同时承载多个会话但每个会话之间要规划好内存抖动。第三个是日志调优。rknn-toolkit2的日志默认比较详细板端runtime有时也会打印很多调试信息。正式部署前建议通过环境变量把日志级别调低减少不必要的IO开销这个细节在很多慢问题排查中能起到意想不到的效果。最后的一点经验这一路从PyTorch到ONNX再到RKNN实际上的核心工作量并不在写推理代码而是前面那段“模型搬家”的过程。一个模型最终能不能在NPU上稳定跑评估的关键不是原模型精度多高而是它的算子能不能在工具链里被完整支持、量化之后语义区分度还在不在、预处理逻辑能不能原样对齐。我强烈建议任何做RK3588部署的朋友都先把模型的结构吃透再动手去做转换不要拿着模型就一把梭。宁可多花两小时检查图和参数也不要让后面整个项目为这两小时买单。如果你也在RK3588上跑Facenet或者类似的人脸模型希望这份指南能帮你少走几个最没技术含量、却最浪费时间的弯路。
返回列表