RK3588实战:从零部署Facenet人脸识别模型的完整避坑手册
如果你刚拿到一块RK3588开发板,想在上面跑一个像Facenet这样的人脸识别模型,可能会觉得无从下手。PyTorch模型怎么转成RKNN格式?转换过程中遇到奇怪的算子不支持怎么办?量化后精度掉得厉害怎么解决?这些问题我都遇到过,而且花了不少时间才找到答案。
今天我就把自己在RK3588上部署Facenet模型的全过程整理出来,从环境搭建到模型转换,再到最后的板端部署,每个环节的坑都帮你填平。无论你是嵌入式开发的新手,还是有一定经验的开发者,这篇文章都能帮你少走弯路。
1. 环境准备:搭建稳定的开发基础
在开始模型转换之前,一个稳定、兼容的开发环境是成功的一半。RKNN工具链对系统版本、Python版本和依赖库版本都有特定要求,配置不当会导致各种莫名其妙的问题。
1.1 系统与Python环境配置
我强烈建议使用Ubuntu 22.04作为开发环境。虽然官方文档说支持18.04及以上,但我在实际使用中发现,22.04的兼容性最好,各种依赖库的安装也最顺利。
Python版本的选择同样重要。RKNN-Toolkit2目前对Python 3.6到3.11都支持,但我推荐使用Python 3.10。这个版本既稳定,又与RKNN-Toolkit2的最新版本兼容性最佳。
注意:不要使用系统自带的Python,一定要通过虚拟环境管理工具来创建独立的环境。这样可以避免不同项目间的依赖冲突。
我习惯用Miniconda来管理Python环境,安装和配置都很简单:
# 下载Miniconda安装脚本
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
# 安装Miniconda
bash Miniconda3-latest-Linux-x86_64.sh
# 创建专门用于RKNN开发的虚拟环境
conda create -n rknn_env python=3.10
# 激活环境
conda activate rknn_env
创建好环境后,先别急着安装RKNN-Toolkit2,我们需要先配置好pip的国内镜像源,否则下载速度会很慢:
# 配置清华镜像源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip config set global.trusted-host pypi.tuna.tsinghua.edu.cn
1.2 RKNN-Toolkit2安装详解
RKNN-Toolkit2是瑞芯微官方提供的模型转换工具,支持将PyTorch、TensorFlow、ONNX等格式的模型转换为RKNN格式。安装过程有几个关键点需要注意。
首先,从GitHub克隆官方仓库:
git clone https://github.com/airockchip/rknn-toolkit2 --depth 1
cd rknn-toolkit2
进入packages目录,你会看到针对不同Python版本的依赖文件。找到对应Python 3.10的文件:
cd rknn-toolkit2/packages/x86_64
ls requirements_cp310*.txt
安装依赖时,我建议先手动安装几个核心依赖,避免自动安装时出现版本冲突:
# 先安装基础依赖
pip install numpy==1.26.4
pip install opencv-python==4.8.1.78
pip install onnx==1.14.1
pip install onnxruntime==1.16.3
pip install torch==2.0.1
# 然后安装requirements文件中的其他依赖
pip install -r requirements_cp310-2.3.0.txt
最后安装RKNN-Toolkit2的wheel包:
pip install rknn_toolkit2-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
验证安装是否成功:
>>> from rknn.api import RKNN
>>> print("RKNN导入成功")
如果没有任何报错,说明安装成功。如果遇到libGL.so.1之类的错误,需要安装一些系统库:
sudo apt-get update
sudo apt-get install libgl1-mesa-glx libglib2.0-0 libsm6 libxrender1 libxext6
1.3 开发板环境配置
PC端环境准备好后,还需要配置开发板的环境。RK3588开发板通常预装了Debian或Ubuntu系统,我们需要确认NPU驱动和相关服务是否正常。
首先通过串口或SSH连接到开发板,检查NPU驱动版本:
dmesg | grep -i rknpu
你应该能看到类似这样的输出:
[ 3.747957] [drm] Initialized rknpu 0.9.2 20230825 for fdab0000.npu on minor 1
如果看不到NPU相关信息,可能是系统没有安装NPU驱动。这时需要重新编译内核并启用NPU支持,或者刷写官方提供的完整固件。
接下来检查RKNN服务是否正常运行:
# 查看rknn_server服务状态
systemctl status rknn_server
# 如果没有运行,手动启动
restart_rknn.sh
正常启动后,你会看到类似这样的输出:
start rknn server, version:1.5.2 (8babfea build@2023-08-25T10:29:51)
I NPUTransfer: Starting NPU Transfer Server, Transfer version 2.1.0
最后,检查RKNN运行库的版本是否与PC端的RKNN-Toolkit2版本匹配:
# 检查librknnrt.so版本
strings /usr/lib/librknnrt.so | grep -i "librknnrt version"
版本信息应该与PC端的RKNN-Toolkit2版本基本一致。如果版本不匹配,可能会导致模型无法加载或推理结果异常。
2. PyTorch模型分析与预处理
在开始转换之前,我们需要对原始的PyTorch模型有充分的了解。特别是Facenet这样的复杂模型,如果不做适当的预处理,转换过程会遇到各种问题。
2.1 模型结构检查
首先下载Facenet的PyTorch实现。我使用的是bubbliiiing/facenet-pytorch这个仓库,它基于MobileFaceNet,比较轻量,适合嵌入式部署。
import torch
import torch.nn as nn
from facenet_pytorch import InceptionResnetV1, MTCNN
# 加载预训练模型
model = InceptionResnetV1(pretrained='vggface2').eval()
# 查看模型结构
print(model)
但这里有个关键问题:很多预训练模型只保存了权重参数,没有保存模型结构。用torch.load()加载时,如果只有.pth文件,需要先定义好模型结构,再加载权重。
如何判断下载的模型文件是否包含结构信息?一个简单的方法是使用Netron可视化工具,或者直接检查文件内容:
import torch
# 尝试加载模型文件
checkpoint = torch.load('facenet_mobilenet.pth', map_location='cpu')
if isinstance(checkpoint, dict):
print("这是一个只包含权重的checkpoint")
print("Keys:", checkpoint.keys())
else:
print("这是一个完整的模型")
对于只有权重的模型,我们需要先定义模型结构:
import torch
import torch.nn as nn
import torch.nn.functional as F
class MobileFaceNet(nn.Module):
def __init__(self, embedding_size=128, num_classes=None):
super(MobileFaceNet, self).__init__()
# 这里需要根据实际模型结构定义网络层
self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=2, padding=1)
# ... 其他层定义
def forward(self, x, mode="predict"):
if mode == 'predict':
# 推理时的前向传播
x = self.backbone(x)
x = self.avg(x)
x = x.view(x.size(0), -1)
x = self.Dropout(x)
x = self.Bottleneck(x)
x = self.last_bn(x)
x = F.normalize(x, p=2, dim=1) # 注意这一行!
return x
# ... 训练时的前向传播
# 创建模型实例并加载权重
model = MobileFaceNet()
model.load_state_dict(torch.load('facenet_mobilenet.pth', map_location='cpu'))
model.eval()
2.2 关键问题:L2正则化层的处理
在Facenet模型中,输出特征向量前通常会进行L2归一化(F.normalize(x, p=2, dim=1))。这个操作在PyTorch中很简单,但在转换为ONNX时会产生一个ReduceL2算子,而RKNN目前不支持这个算子。
我最初转换时遇到了这个错误:
E RKNN: Catch exception when building RKNN model!
E RKNN: Traceback (most recent call last):
E RKNN: File "rknn/api/rknn_base.py", line 815, in rknn.api.rknn_base.RKNNBase.build
E RKNN: File "rknn/api/rknn_base.py", line 2028, in rknn.api.rknn_base.RKNNBase._quantize_model
E RKNN: File "rknn/api/rknn_base.py", line 1725, in rknn.api.rknn_base.RKNNBase._generate_quantized_model
E RKNN: File "rknn/api/rknn_base.py", line 1695, in rknn.api.rknn_base.RKNNBase._optimize_quantized_model
E RKNN: ValueError: The following operators are not implemented: ['ReduceL2']
解决方法是在导出ONNX前,注释掉L2归一化层。推理完成后,在RK3588上手动进行L2归一化:
class MobileFaceNet(nn.Module):
def __init__(self, embedding_size=128, num_classes=None):
super(MobileFaceNet, self).__init__()
# ... 网络层定义
def forward(self, x, mode="predict"):
if mode == 'predict':
x = self.backbone(x)
x = self.avg(x)
x = x.view(x.size(0), -1)
x = self.Dropout(x)
x = self.Bottleneck(x)
x = self.last_bn(x)
# 注释掉这一行,不在模型中做L2归一化
# x = F.normalize(x, p=2, dim=1)
return x
这样修改后,模型输出的是未归一化的特征向量。在RK3588上推理完成后,我们需要在代码中手动进行L2归一化:
import numpy as np
from sklearn.preprocessing import normalize
# RK3588推理得到特征向量
features = rknn.inference(inputs=[image])[0]
# 手动进行L2归一化
normalized_features = normalize(features, norm='l2')
2.3 输入输出规格确认
在转换模型前,必须明确模型的输入输出规格。Facenet通常接受160x160的RGB图像作为输入,输出是128维或512维的特征向量。
我们可以用Netron打开原始的PyTorch模型(如果有的话),或者通过代码查看:
# 创建一个随机输入测试模型
dummy_input = torch.randn(1, 3, 160, 160)
# 前向传播获取输出
with torch.no_grad():
output = model(dummy_input, mode='predict')
print(f"输入形状: {dummy_input.shape}")
print(f"输出形状: {output.shape}")
print(f"输出范围: [{output.min():.4f}, {output.max():.4f}]")
记录下这些信息,在后续的转换和部署中会用到。
3. PyTorch到ONNX的转换实战
ONNX作为中间格式,是连接PyTorch和RKNN的桥梁。转换过程看似简单,但有很多细节需要注意。
3.1 正确的导出方法
使用torch.onnx.export()函数导出ONNX模型时,有几个关键参数需要特别注意:
import torch
import torch.onnx
# 准备模型和输入
model.eval()
dummy_input = torch.randn(1, 3, 160, 160)
# 导出ONNX模型
torch.onnx.export(
model, # 要导出的模型
dummy_input, # 模型输入(可以是tuple)
"facenet_mobilenet.onnx", # 输出文件名
export_params=True, # 是否导出模型参数
opset_version=12, # ONNX算子集版本
do_constant_folding=True, # 是否进行常量折叠优化
input_names=['input'], # 输入节点名称
output_names=['output'], # 输出节点名称
dynamic_axes={ # 动态维度设置
'input': {0: 'batch_size'}, # 第0维(batch)是动态的
'output': {0: 'batch_size'}
},
verbose=True # 显示详细信息
)
这里有几个重要的选择:
-
opset_version:我选择12,这是目前RKNN支持较好的版本。版本过高可能导致某些算子不支持,版本过低可能缺少需要的算子。
-
dynamic_axes:设置动态维度可以让模型支持不同的batch size。虽然RK3588推理时通常batch size为1,但保留这个选项更灵活。
-
verbose:设为True可以在导出时看到详细过程,有助于调试。
3.2 ONNX模型验证与优化
导出ONNX后,不要直接进行下一步,先验证模型是否正确:
import onnx
import onnxruntime as ort
# 加载并验证ONNX模型
onnx_model = onnx.load("facenet_mobilenet.onnx")
onnx.checker.check_model(onnx_model)
print("ONNX模型验证通过")
# 使用ONNX Runtime进行推理测试
ort_session = ort.InferenceSession("facenet_mobilenet.onnx")
# 准备输入
import numpy as np
input_data = np.random.randn(1, 3, 160, 160).astype(np.float32)
# 运行推理
ort_inputs = {ort_session.get_inputs()[0].name: input_data}
ort_outputs = ort_session.run(None, ort_inputs)
print(f"ONNX Runtime输出形状: {ort_outputs[0].shape}")
如果ONNX Runtime能正常推理,说明模型导出基本正确。但为了确保与PyTorch的结果一致,最好进行数值对比:
# PyTorch推理
with torch.no_grad():
torch_output = model(torch.from_numpy(input_data), mode='predict')
# 对比结果
print(f"PyTorch输出: {torch_output.numpy()[:5]}")
print(f"ONNX输出: {ort_outputs[0][:5]}")
print(f"最大差异: {np.max(np.abs(torch_output.numpy() - ort_outputs[0]))}")
如果差异在可接受范围内(通常小于1e-5),说明转换成功。
3.3 常见问题与解决方案
在转换过程中,我遇到过几个典型问题:
问题1:算子不支持
Unsupported: ONNX version 12 of Resize is not supported.
解决方案:降低opset_version到11或10,或者更新RKNN-Toolkit2到最新版本。
问题2:维度不匹配
Input 0 of node ... was passed float from ... inconsistent with expected int64
解决方案:检查模型中是否有类型不匹配的操作,比如用float作为索引。可以在PyTorch模型中显式转换数据类型。
问题3:动态维度问题
Shape inference failed for node: ...
解决方案:如果不需要动态batch,可以去掉dynamic_axes参数,使用固定维度。
为了方便排查问题,我整理了一个常见错误对照表:
| 错误信息 | 可能原因 | 解决方案 |
|---|


9820

被折叠的 条评论
为什么被折叠?



