RK3588上从零部署Facenet人脸识别:PyTorch转ONNX再到RKNN的完整避坑指南

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               # 显示详细信息
)

这里有几个重要的选择:

  1. opset_version:我选择12,这是目前RKNN支持较好的版本。版本过高可能导致某些算子不支持,版本过低可能缺少需要的算子。

  2. dynamic_axes:设置动态维度可以让模型支持不同的batch size。虽然RK3588推理时通常batch size为1,但保留这个选项更灵活。

  3. 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参数,使用固定维度。

为了方便排查问题,我整理了一个常见错误对照表:

错误信息 可能原因 解决方案
目标检测(Object Detection)是计算机视觉领域的一个核心问题,其主要任务是找出图像中所有感兴趣的目标(物体),并确定它们的类别和位置。以下是对目标检测的详细阐述: 一、基本概念 目标检测的任务是解决“在哪里?是什么?”的问题,即定位出图像中目标的位置并识别出目标的类别。由于各类物体具有不同的外观、形状和姿态,加上成像时光照、遮挡等因素的干扰,目标检测一直是计算机视觉领域最具挑战性的任务之一。 二、核心问题 目标检测涉及以下几个核心问题: 分类问题:判断图像中的目标属于哪个类别。 定位问题:确定目标在图像中的具体位置。 大小问题:目标可能具有不同的大小。 形状问题:目标可能具有不同的形状。 三、算法分类 基于深度学习的目标检测算法主要分为两大类: Two-stage算法:先进行区域生成(Region Proposal),生成有可能包含待检物体的预选框(Region Proposal),再通过卷积神经网络进行样本分类。常见的Two-stage算法包括R-CNN、Fast R-CNN、Faster R-CNN等。 One-stage算法:不用生成区域提议,直接在网络中提取特征来预测物体分类和位置。常见的One-stage算法包括YOLO系列(YOLOv1、YOLOv2、YOLOv3、YOLOv4、YOLOv5等)、SSD和RetinaNet等。 四、算法原理 以YOLO系列为例,YOLO将目标检测视为回归问题,将输入图像一次性划分为多个区域,直接在输出层预测边界框和类别概率。YOLO采用卷积网络来提取特征,使用全连接层来得到预测值。其网络结构通常包含多个卷积层和全连接层,通过卷积层提取图像特征,通过全连接层输出预测结果。 五、应用领域 目标检测技术已经广泛应用于各个领域,为人们的生活带来了极大的便利。以下是一些主要的应用领域: 安全监控:在商场、银行
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值