文章目录
一、前言
本博客皆在展示如何在Orange Pi 5 上使用 RKNN C API 使用C语言来进行模型的部署,不设计以及讨论PPO网络的实现以及细节
当前,深度学习的热潮久退不下。许多爱好者也会使用 Pytorch 等工具在自己的 Windows 环境下结合训练自己的深度学习网络模型,然后再进行模型验证,在仿真环境发现模型能够达到自己的需求,许多人就到此就结束了,因为可能目标本就是学习目的或者是了解一二,仅此而已,也就不必再进一步。但是另一批人希望自己的模型能够部署在一些嵌入式设备上实现从仿真到实验的跳跃。现在有两条比较成熟的方案:
~~~~
1. 将嵌入式设备的数据发送到 Windows 上位机,在 Windows 配置好的环境下推理完毕后再将结果发送给嵌入式设备,实现 嵌入式->上位机->嵌入式 数据流的传输与控制。这个方法的优点是 Windows 环境的推理能力较为强劲,在处理一些例如图像分类,图像识别等任务的时候能够很好的完成需求,但是缺点是数据在各个端点传输的时候时间延迟应该会较大,对于一些对时间延迟较为苛刻的任务,此方法不能够保证实时性.
~~~~
2. 直接将模型部署在嵌入式设备上,实现端设备直接推理模型的功能。这样可以保证实时性,但是缺点是对嵌入式设备的计算能力需求较大,且部署时需要对厂家提供的API较为熟悉。对于小型网络,选此方法为上上签。
本博客将会展示如何将一个简单 PPO (Proximal Policy Optimization) 算法模型通过使用 RKNN C API 部署到 Orange Pi 5 上并实现一套完整的 推理+控制 流程。
1️⃣、Orange Pi 是什么?
引用自 Orange Pi 5 介绍页,跳转连接
Orange Pi 是深圳市迅龙软件有限公司的开源产品品牌。简单来说就是国产树莓派。本博客使用其旗下的 Orange Pi 5 系列产品。Orange Pi 5 采用了 瑞芯微RK3588S 新一代八核64位处理器,具体为四核A76+四核A55,采用了8nm工艺设计,主频最高可达2.4GHz,集成ARM Mali-G610 MP4 GPU,内嵌高性能3D和2D图像加速模块,内置高达6 Tops算力的AI加速器NPU,拥有4GB/8GB/16GB(LPDDR4/4X),具有高达8K显示处理能力。下图就是 Orange Pi 5 的俯视图了。
2️⃣、PPO 是什么?
引用自《PPO(Proximal Policy Optimization)算法原理及实现,详解近端策略优化》,跳转链接。
PPO(Proximal Policy Optimization,近端策略优化)是一种强化学习算法,由 John Schulman 等人在2017年提出。PPO属于策略梯度方法,这类方法直接对策略(即模型的行为)进行优化,试图找到使得期望回报最大化的策略。PPO旨在改进和简化以前的策略梯度算法,如TRPO(Trust Region Policy Optimization,信任域策略优化),它通过几个关键的技术创新提高了训练的稳定性和效率。
PPO的主要特点包括:
裁剪的概率比率:PPO使用一个目标函数,其中包含了一个裁剪的概率比率,这个比率是旧策略和新策略产生动作概率的比值。这个比率被限制在一个范围内,防止策略在更新时做出太大的改变。
多次更新:在一个数据批次上可以安全地进行多次更新,这对于样本效率非常重要,尤其是在高维输入和实时学习环境中。
简单实现:与TRPO相比,PPO更容易实现和调整,因为它不需要复杂的数学运算来保证策略更新的安全性。
平衡探索与利用:PPO尝试在学习稳定性和足够的探索之间取得平衡,以避免局部最优并改进策略性能。
PPO已被广泛应用于各种强化学习场景,包括游戏、机器人控制以及自然语言处理中的序列决策问题。它是目前最流行的强化学习算法之一。
3️⃣、RKNN 是什么?
RKNN 是 Rockchip npu 平台使用的模型类型,以.rknn后缀结尾的模型文件。Rockchip 提供了完整了模型转换 Python 工具,方便用户将自主研发的算法模型转换成 RKNN 模型,同时 Rockchip 也提供了C/C++和Python API 接口。也就是说 RKNN 是来自瑞芯微公司旗下的产品,RK3588s 的 RKNN系列工具有其GitHub链接,跳转连接,以供参考。我们需要借助这个工具实现模型在 Orange Pi 上的部署。我们部署主要使用的是 RKNN API, 所以这里也放上 RKNN API的用户手册以供参考,跳转连接。其余的代码我会在下面的内容里直接贴出,就不附送跳转链接了。
对于 Ternsorflow, PyTorch 等其他模型,想要在 RK3588 平台运行,需要先进行模型转换。可以在搭载 Ubuntu 的PC上使用RKNN-Toolkit2工具将模型转化为 RKNN 格式,将其交叉编译后才可以部署到开发板上。
总体开发流程(以pytorch框架开发,C++部署):
1. 在 PC 端安装 RKNN-Toolkit2 工具
2. 使用 pytorch 搭建网络并训练,保存为 .pth 文件
3. 在 PC 端使用将 .pth 文件转化为 .onnx 文件,接下来使用 RKNN-Toolkit2 将 .onnx 文件转化为 .rknn 文件
4. 使用 RKNN SDK 提供的 C 接口 API 按照官方给定的调用流程交叉编译后即可执行调用 .rknn 模型文件
总结:如果想要部署模型,必须将模型转为 .rknn 格式的文件,设备才能够读取运行。但是由于一些限制以及不可抗力因素,在上位机训练得到的 .pth 文件不建议直接转为 .rknn,此时需要借助一个中间格式的文件,即:.onnx 格式文件。
3️⃣、ONNX 是什么?
ONNX是一种针对机器学习所设计的开放式的文件格式,用于存储训练好的模型。它使得不同的深度学习框架( Pytorch, MXNet)可以采用相同格式存储模型数据。简而言之,ONNX是一种便于在各个主流深度学习框架中迁移模型的中间表达格式。本文不着重介绍此格式。
二、项目简介
本次的部署基于一个具体的项目,项目的目标是控制一个末端搭载了动量轮和电机的杆子抵达目标角度。整个系统是水平摆放,所以并不受到重力的影响。传统PID可以有效的控制这个系统,但是传统PID为了迅速让目标接近系统,很有出现超调现象,并且传统PID并不能实现中间过程零力矩的控制方法。使用基于强化学习训练的模型来控制整个系统,可实现零超调。因为强化学习训练本质就是一种求最优策略的过程。整个系统的概念图如下所示,
m
b
,
m
w
m_b,m_w
mb,mw 为系统主体和飞轮的重量。
I
b
I_b
Ib 为主体绕枢轴的转动惯量。
I
w
I_w
Iw 为轮子和电机转子绕电机旋转轴的转动惯量。
l
w
l_w
lw 为电机轴与枢轴之间的距离。
l
b
l_b
lb 为系统主体质心与枢轴之间的距离。由于整个系统垂直于重力方向,因此系统不需要考虑重力。
T
m
T_m
Tm 为电机提供的扭矩,
C
w
,
C
b
C_w,C_b
Cw,Cb为轮子和系统主体的动摩擦系数。
在这个模型中,我们很容易知道轴绕着一个点旋转,飞轮绕着枢轴点旋转。所以,当我们进行分析时,我们需要分别包括飞轮的平动动能和旋转动能以及轴的旋转动能。
从拉格朗日法中,我们可以知道以下方程,
f
(
x
)
=
{
T
=
1
2
I
b
θ
˙
b
2
+
1
2
I
w
(
θ
˙
b
+
θ
˙
w
)
2
+
1
2
m
w
(
l
w
θ
˙
b
)
2
V
=
0
f(x)= \begin{cases} T=\frac{1}{2}I_b\dot\theta_b^2+\frac{1}{2}I_w(\dot\theta_b+\dot\theta_w)^2+\frac{1}{2}m_w(l_w\dot\theta_b)^2\\ V=0 \end{cases}
f(x)={T=21Ibθ˙b2+21Iw(θ˙b+θ˙w)2+21mw(lwθ˙b)2V=0
最后我们可以解出此方程为,
θ
¨
b
=
−
T
m
+
C
w
θ
˙
w
−
C
b
θ
˙
b
I
b
+
m
w
l
w
2
\ddot\theta_b=\frac{-T_m+C_w\dot\theta_w-C_b\dot\theta_b}{I_b+m_wl_w^2}
θ¨b=Ib+mwlw2−Tm+Cwθ˙w−Cbθ˙b
θ
¨
w
=
(
I
b
+
I
w
+
m
w
l
w
2
)
(
T
m
−
C
w
θ
˙
b
)
I
w
(
I
b
+
m
w
l
w
2
)
+
C
b
θ
˙
b
I
b
+
m
w
l
w
2
\ddot\theta_w=\frac{(I_b+I_w+m_wl_w^2)(T_m-C_w\dot\theta_b)}{I_w(I_b+m_wl_w^2)}+\frac{C_b\dot\theta_b}{I_b+m_wl_w^2}
θ¨w=Iw(Ib+mwlw2)(Ib+Iw+mwlw2)(Tm−Cwθ˙b)+Ib+mwlw2Cbθ˙b
那么,现在拥有了模型,就可以基于模型来进行强化学习的训练了。
整个模型的控制大概是,
1. 实时给模型输入三个系统的当前状态,即:
θ
b
,
θ
˙
b
,
θ
˙
w
\theta_b,\dot\theta_b,\dot\theta_w
θb,θ˙b,θ˙w,
2. 模型在接收到三个变量后,会放进网络中运算,给出三个概率
三个概率对应着的是反向最大力矩,零力矩,正向最大力矩的概率。所以可以说系统只能够以这三个状态进行运动。最后模型训练成功后可以得到如下的仿真,
三、部署流程
1️⃣、PPO 网络结构
本次在上位机训练的 PPO 的网络结构如下所示,创建了一个 1x3 输入,1x3输出的网路,中间层使用线性层链接。由于篇幅有限,且本次重点不在于PPO网络,故不展示完全代码。
# 创建价值网络
model = torch.nn.Sequential(torch.nn.Linear(3, nnn * 2), torch.nn.ReLU(),
torch.nn.Linear(nnn * 2, nnn), torch.nn.ReLU(),
torch.nn.Linear(nnn, 1))
for module in model.modules():
if isinstance(module, torch.nn.Linear):
torch.nn.init.orthogonal_(module.weight)
nnn = 512
# 创建策略网络
policy = torch.nn.Sequential(torch.nn.Linear(3, nnn * 2), torch.nn.Tanh(),
torch.nn.Linear(nnn * 2, nnn), torch.nn.Tanh(),
torch.nn.Linear(nnn, 3), torch.nn.Softmax(dim=1))
2️⃣、PPO 输出模型,模型转换,以及对比检查
经过训练,最后能够得到一个 .pth 文件,这里我分享一下我的 .pth文件,下载连接。 随后,我们需要将其转换为 .onnx 中间格式文件。代码如下:
import torch
if torch.cuda.is_available():
device = torch.device("cuda:0")
print("Running on the GPU")
else:
device = torch.device("cpu")
print("Running on the CPU")
torch.cuda.set_device(device)
nnn = 512
# 实例化策略网络
policy = torch.nn.Sequential(torch.nn.Linear(3, nnn * 2), torch.nn.Tanh(), # 双曲正切激活函数
torch.nn.Linear(nnn * 2, nnn), torch.nn.Tanh(),
torch.nn.Linear(nnn, 3), torch.nn.Softmax(dim=1)) # 计算每个动作的概率
policy.to(device)
policy.load_state_dict(
torch.load('C:/Users/Administrator/Desktop/Cases/RL-balance-robot/horizontal/training/outputs/5degrees-PPO-zero-torque.pth'))
policy.eval()
dummy_input = torch.randn(1, 3, device='cuda').reshape(1, 3)
torch.onnx.export(policy, dummy_input, "PPO.onnx", opset_version=11, verbose=True)
这里需要注意,因为我的输入是 1x3, 所以这里设置 torch.randn(1,3)。 如果你们的输入是1x5,则相应的应该换为torch.randn(1,5)。
这样我们就得到了一个 .onnx 中间格式的模型文件了。
现在我我们来测量模型转换是否成功。测试的方法是使用 .pth 模型文件,随机输入数据,看输出的三个概率是多大。然后再载入转换后的 .onnx 模型文件,输入同样的数据,看输出的结果是否和 .pth 模型文件输出的一样。若相同,则转换成功。测试 .pth 模型文件的代码如下所示,
import torch
device = torch.device("cpu")
nnn = 512
policy1 = torch.nn.Sequential(torch.nn.Linear(3, nnn * 2), torch.nn.Tanh(),
torch.nn.Linear(nnn * 2, nnn), torch.nn.Tanh(),
torch.nn.Linear(nnn, 3), torch.nn.Softmax(dim=1))
policy1.load_state_dict(torch.load('C:/Users/Administrator/Desktop/Cases/RL-balance-robot/horizontal/training/outputs/5degrees-PPO-zero-torque.pth'))
policy1.to('cpu')
prob = policy1(torch.FloatTensor([[10, 200, 200]]))
print(prob)
输出结果如下所示,
tensor([[1.0021e-28, 1.5017e-13, 1.0000e+00]], grad_fn=<SoftmaxBackward0>)
可以看出,在此时的输入条件下,模型预测的结果第三个数最大。
现在看看 .onnx 文件输入相同的数据,是否能够和 .pth 模型输出一样的结果。测试 .onnx 模型文件的代码如下所示,
import onnx
import onnxruntime
# Load the ONNX model
model = onnx.load("PPO.onnx")
# Check that the IR is well formed
onnx.checker.check_model(model)
# Print a human readable representation of the graph
print(onnx.helper.printable_graph(model.graph))
session = onnxruntime.InferenceSession('PPO.onnx', None)
raw_result = session.run([], {"onnx::Gemm_0": [[10, 200, 200]]})
print(raw_result)
输出结果如下所示,
[array([[1.0021013e-28, 1.5016719e-13, 1.0000000e+00]], dtype=float32)]
.onnx 模型文件在输入数据相同的情况下,输出和 .pth 文件一样
为了预防偶然情况,本博客又多测试了几组数据,
输入数据 | .onnx预测结果 | .pth预测结果 |
---|---|---|
[90,200,0] | [5.2034653e-33, 9.1289922e-17, 1.0000000e+00] | [5.2035e-33, 9.1290e-17, 1.0000e+00] |
[-50,0,300] | [1.0000000e+00, 1.9378372e-13, 1.6095759e-31] | [1.0000e+00, 1.9378e-13, 1.6096e-31] |
[30,50,50] | [2.486136e-35, 7.525642e-19, 1.000000e+00] | [2.4861e-35, 7.5257e-19, 1.0000e+00] |
[-60,2000,0] | [4.5575372e-27, 7.3239895e-12, 1.0000000e+00] | [4.5576e-27, 7.3240e-12, 1.0000e+00] |
[80,0,-3000] | [8.3561228e-28, 1.2708616e-14, 1.0000000e+00] | [8.3561e-28, 1.2709e-14, 1.0000e+00] |
从表格测试结果来看,模型转换完全没问题。
3️⃣、.onnx -> .rknn
接下来我们需要用到 Linux 系统,这里使用的是虚拟机上的 Ubuntu 20.04 系统。我们需要在 Linux系统上将转换的 .onnx 文件再次转换为 .rknn 文件。使用的Ubuntu系统如下图所示:
我们还需要安装 RKNN-Toolkit2 环境以进行模型的转换,由于我已经安装好了,所以我这里就不再带领大家进行这一步了。这里给大家一个教程自行进行安装。这里是跳转链接
在安装 RKNN-Toolkit2 环境完毕以后,使用如下代码进行模型的转换:
from rknn.api import RKNN
import os
if __name__ == '__main__':
platform = 'rk3588s'
'''step 1: create RKNN object'''
rknn = RKNN()
'''step 2: load the .onnx model'''
rknn.config(target_platform='rk3588s')
print('--> Loading model')
ret = rknn.load_onnx('PPO.onnx')
if ret != 0:
print('load model failed')
exit(ret)
print('done')
'''step 3: building model'''
print('-->Building model')
ret = rknn.build(do_quantization=False)
if ret != 0:
print('build model failed')
exit()
print('done')
'''step 4: export and save the .rknn model'''
OUT_DIR = 'rknn_models'
RKNN_MODEL_PATH = './{}/PPO.rknn'.format(OUT_DIR)
if not os.path.exists(OUT_DIR):
os.mkdir(OUT_DIR)
print('--> Export RKNN model: {}'.format(RKNN_MODEL_PATH))
ret = rknn.export_rknn(RKNN_MODEL_PATH)
if ret != 0:
print('Export rknn model failed.')
exit(ret)
print('done')
'''step 5: release the model'''
rknn.release()
这样我们就可以得到一个转换后的 .rknn 模型文件
4️⃣、RK3588s 推理 .rknn 模型
那么现在我们拥有了.rknn模型文件,理论上应该可以进行部署了,但是在此之前我们应该也在我们的Orange Pi 上安装好 RKNN 的运行环境。这里我们需要在 Orange Pi 上安装的是 RKNPU2。RKNPU2 提供访问Rockchip的NPU的便利接口——动态链接库librknnrt.so和C头文件rknn_api.h,我们可以编写C++版本的AI应用并利用RKNPU2加速。这里安装不详细介绍,提供一个跳转链接。
那么在安装完毕后,我们的板子理论上就可以运行 .rknn 模型文件了。首先打开MobaXterm进行ssh连接Orange Pi 然后将刚刚的模型文件上传到板子上。然后参考 RKNN C API 的用户手册,进行代码编写。
RK3588上有两组API可以使用,分别是通用API接口和零拷贝流程API接口。两组API的主要区别在于,通用接口API每次更新帧数据,需要将外部模块分配的数据拷贝到NPU运行时的输入内存。而零拷贝流程的接口会直接使用预先分配的内存(包括NPU运行时创建的或外部其他框架创建的,比如DRM框架),减少了内存拷贝的花销。当用户输入数据只有虚拟地址时,只能使用通用API接口,当用户输入数据有物理地址或者fd时,两组接口都可以使用。对于通用API接口,首先初始化rknn_input结构体,帧数据包含在该结构体中,使用RKNN_inputs_set函数设置模型输入,等待推理结束后,使用rknn_outputs_get函数获取推理的输出,进行后处理。在每次推理前,更新帧数据。从RKNPU用户手册里摘抄的输入/输出内存由运行时分配的零拷贝API调用流程如下所示,
这里就不介绍通用API的流程了。虽然通用API可以将模型部署在rk3588s的npu之上,运行更快,但是我在尝试的时候发现我的模型使用通用API部署精度会大打折扣,目前还不知道有什么方法可以解决。于是本篇幅着重介绍零拷贝API的使用,这里给出一个零拷贝接口的参考例程。但是注意,本例程使用的零拷贝API无法将模型部署在npu之上,整个程序由cpu运行。初步估计可能是 rknn 给的 C 接口接受的是 float16 的变量类型,但是由 .onnx 转换的 .rknn 文件是 float32 类型的,故npu无法运行,会被切换至cpu运行。
/*-------------------------------------------
Includes
-------------------------------------------*/
#include "rknn_api.h"
#include <sys/time.h>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
/*-------------------------------------------
RKNN
-------------------------------------------*/
const int state_row = 1; // input dimension
const int state_col = 3; // input dimension
const int act_dim = 3; // input dimension
float input_data[state_row][state_col] = {0.0 ,0.0 ,0.0}; // input matrix
int output_index = 0; //index of the max value
static void dump_tensor_attr(rknn_tensor_attr *attr);
static inline int64_t getCurrentTimeUs();
/*-------------------------------------------
Main Functions
-------------------------------------------*/
int ret = 0;
rknn_context ctx = 0;
rknn_sdk_version sdk_ver;
rknn_input_output_num io_num;
rknn_tensor_attr input_attrs;
rknn_tensor_attr output_attrs;
int main(int argc, char *argv[])
{
char *model_path = argv[1];
// Load RKNN model
int model_len = 0;
ret = rknn_init(&ctx, &model_path[0], 0, 0, NULL);
if (ret < 0){
printf("rknn_init fail! ret=%d\n", ret);
}
// Get sdk and driver version
ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &sdk_ver, sizeof(sdk_ver));
if (ret != RKNN_SUCC){
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printf("rknn_api/rknnrt version: %s, driver version: %s\n", sdk_ver.api_version, sdk_ver.drv_version);
// Get Model Input Output Info
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
if (ret != RKNN_SUCC){
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
printf("input tensors:\n");
memset(&input_attrs, 0, sizeof(rknn_tensor_attr));
input_attrs.index = 0;
ret = rknn_query(ctx, RKNN_QUERY_INPUT_ATTR, &(input_attrs), sizeof(rknn_tensor_attr));
if (ret < 0){
printf("rknn_init error! ret=%d\n", ret);
return -1;
}
input_attrs.type = RKNN_TENSOR_FLOAT32;
input_attrs.size = input_attrs.size_with_stride = input_attrs.n_elems * sizeof(float);
dump_tensor_attr(&input_attrs);
printf("output tensors:\n");
memset(&output_attrs, 0, sizeof(rknn_tensor_attr));
output_attrs.index = 0;
ret = rknn_query(ctx, RKNN_QUERY_OUTPUT_ATTR, &(output_attrs), sizeof(rknn_tensor_attr));
if (ret != RKNN_SUCC){
printf("rknn_query fail! ret=%d\n", ret);
return -1;
}
output_attrs.type = RKNN_TENSOR_FLOAT32;
output_attrs.size = output_attrs.size_with_stride = output_attrs.n_elems * sizeof(float);
dump_tensor_attr(&output_attrs);
// create input tensor memory
rknn_tensor_mem *input_mems;
input_mems = rknn_create_mem(ctx, input_attrs.size);
// create output tensor memory
rknn_tensor_mem *output_mems;
output_mems = rknn_create_mem(ctx, output_attrs.size);
// Set input tensor memory
ret = rknn_set_io_mem(ctx, input_mems, &input_attrs);
if (ret < 0){
printf("rknn_set_io_mem fail! ret=%d\n", ret);
return 1;
}
ret = rknn_set_io_mem(ctx, output_mems, &output_attrs);
if (ret < 0){
printf("rknn_set_io_mem fail! ret=%d\n", ret);
return 1;
}
input_data[0][0] = -50;
input_data[0][1] = 0;
input_data[0][2] = 300;
// copy input data to input tensor memory
memcpy(input_mems->virt_addr, input_data, sizeof(input_data));
// Run
int64_t start_us = getCurrentTimeUs();
ret = rknn_run(ctx, NULL);
int64_t elapse_us = getCurrentTimeUs() - start_us;
if (ret < 0){
printf("rknn run error %d\n", ret);
return 1;
}
printf("Elapse Time = %.2fms\n", elapse_us / 1000.f);
// print the result
float *buffer = (float *)(output_mems->virt_addr);
output_index = max_element(buffer,buffer+3) - buffer;
for (int i = 0; i < state_col; i++){
cout << input_data[0][i] << endl;
}
for (int i = 0; i < act_dim; i++){
cout << buffer[i] << endl;
}
cout << output_index << endl;
// Destroy rknn memory
rknn_destroy_mem(ctx, input_mems);
rknn_destroy_mem(ctx, output_mems);
// Destroy
rknn_destroy(ctx);
return 0;
}
static inline int64_t getCurrentTimeUs()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000 + tv.tv_usec;
}
static void dump_tensor_attr(rknn_tensor_attr *attr)
{
printf(" index=%d, name=%s, n_dims=%d, dims=[%d, %d, %d, %d], n_elems=%d, size=%d, fmt=%s, type=%s, qnt_type=%s, "
"zp=%d, scale=%f\n",
attr->index, attr->name, attr->n_dims, attr->dims[0], attr->dims[1], attr->dims[2], attr->dims[3],
attr->n_elems, attr->size, get_format_string(attr->fmt), get_type_string(attr->type),
get_qnt_type_string(attr->qnt_type), attr->zp, attr->scale);
}
其中,通过直接赋值进行输入数据的更新,第一行的意思是 θ b = − 5 0 ∘ \theta_b=-50^\circ θb=−50∘,第二行的意思是 θ ˙ d = 0 ∘ / s \dot\theta_d=0^\circ/s θ˙d=0∘/s,第三行的意思是 θ w = 30 0 ∘ / s \theta_w=300^\circ/s θw=300∘/s。
input_data[0][0] = -50;
input_data[0][1] = 0;
input_data[0][2] = 300;
运行结果如下所示,
可以看到本模型使用了0.47ms就运行完毕一次循环,输入数据是[-50,0,300],输出概率为[1,6.10948e-05,6.10948e-05],因为0号位的概率最大,所以最后会实行0号位的动作。
为了防止偶然误差,输入和上方测试一样的数据,进行数据输出结果的对比,消除偶然误差。
输入数据 | .rknn预测结果 |
---|---|
[90,200,0] | [6.10948e-05,6.10948e-05,1] |
[-50,0,300] | [1,6.10948e-05,6.10948e-05] |
[30,50,50] | [6.10948e-05,6.10948e-05,1] |
[-60,2000,0] | [6.10948e-05,6.10948e-05,1] |
[80,0,-3000] | [6.10948e-05,6.10948e-05,1] |
由于数据类型的限制,rknn输出的值最小为 6.10948e-05, 但总体结果匹配一致,可以证明此次部署成功。
总结
本次展示了如何在 OrangePi 5 上部署深度学习模型的例子,皆在展示可行性。但问题仍然存在,例如尝试使用通用API接口部署失败,无法使用npu运行模型等等。博主仍在调查研究当中,希望早日解决此类问题。