硬件平台介绍
这次体验的硬件平台是来自飞凌嵌入式的OK3588-C开发板,该开发板基于Rockchip新一代旗舰 RK3588处理器开发,采用核心板+底板的分体式设计,将FET3588-C核心板的全部功能引脚以最便利的方式引出,并针对不同的功能做了深度优化,方便用户二次开发的同时简化用户设计。
rk3588 NPU及开发工具介绍
rk3588专门针对机器学习模型部署的需求配置了性能强劲的NPU,rk3588配置的NPU不仅提供6TOPS的算力,还支持INT4/INT8/INT16/FP16格式的混合操作。除了非常不错的硬件支持之外,Rockchip公司还提供了便捷的开发工具rknn-toolkit2 和 rknpu2-api,让开发者可以方便的将自己的机器学习模型进行转换和部署。
rknn-toolkit2
根据rockchip提供的《Rockchip_User_Guide_RKNN_Toolkit2》开发文档介绍:
RKNN-Toolkit2 是为用户提供在 PC、Rockchip NPU 平台上进行模型转换、推理和性能评估的
开发套件,用户通过该工具提供的 Python 接口可以便捷地完成以下功能:
- 模型转换: 支持 Caffe、TensorFlow、TensorFlow Lite、ONNX、DarkNet、PyTorch 等模型
转为 RKNN 模型,并支持 RKNN 模型导入导出,RKNN 模型能够在 Rockchip NPU 平台
上加载使用。 - 量 化 功 能 : 支 持 将 浮 点 模 型 量 化 为 定 点 模 型 , 目 前 支 持 的 量 化 方 法 为 非 对 称 量 化
( asymmetric_quantized-8 ) , 并 支 持 混 合 量 化 功 能 。 - 模型推理:能够在 PC 上模拟 Rockchip NPU 运行 RKNN 模型并获取推理结果;或将 RKNN
模型分发到指定的 NPU 设备上进行推理并获取推理结果。 - 性能和内存评估:将 RKNN 模型分发到指定 NPU 设备上运行,以评估模型在实际设备上
运行时的性能和内存占用情况。 - 量化精度分析:该功能将给出模型量化前后每一层推理结果与浮点模型推理结果的余弦距
离,以便于分析量化误差是如何出现的,为提高量化模型的精度提供思路。
安装
可以从github上的rockchip官方仓库中下载最新的适合于本地python环境的rknn_toolkit2安装文件,目前github上最新的版本已经支持python3.10了。下载完python安装文件后,可以用pip直接安装,这里以python3.10版本为例。需要注意的是rknn_toolkit2的安装是以来tensorflow2.8版本tf-estimator-nightly特定版本的。所以在安装rknn_toolkit2之前先利用pip安装tensorflow2.8版本,对于已经安装其他版本tensorflow的,要先进行卸载,再重新安装。
pip3 install tensorflow==2.8
pip3 install tf-estimator-nightly==2.8.0.dev2021122109 -i https://pypi.org/simple
接着用pip安装刚下在的rknn_toolkit2安装包。
pip3 install rknn_toolkit2-1.5.2%2Bb642f30c-cp310-cp310-linux_x86_64.whl
下图是安装完成后的提示信息:
测试
在安装完成之后,可以直接使用rknn_toolkit2提供的demo程序进行测试。这里我们选择tensorflow构建的机器学习模型进行测试。
官方提供的测试程序在rknn-toolkit2/examples/tensorflow/ssd_mobilenet_v1路径下。
具体测试程序如下:
import numpy as np
import re
import math
import random
import cv2
from rknn.api import RKNN
INPUT_SIZE = 300
NUM_RESULTS = 1917
NUM_CLASSES = 91
Y_SCALE = 10.0
X_SCALE = 10.0
H_SCALE = 5.0
W_SCALE = 5.0
def expit(x):
return 1. / (1. + math.exp(-x))
def unexpit(y):
return -1.0 * math.log((1.0 / y) - 1.0)
def CalculateOverlap(xmin0, ymin0, xmax0, ymax0, xmin1, ymin1, xmax1, ymax1):
w = max(0.0, min(xmax0, xmax1) - max(xmin0, xmin1))
h = max(0.0, min(ymax0, ymax1) - max(ymin0, ymin1))
i = w * h
u = (xmax0 - xmin0) * (ymax0 - ymin0) + (xmax1 - xmin1) * (ymax1 - ymin1) - i
if u <= 0.0:
return 0.0
return i / u
def load_box_priors():
box_priors_ = []
fp = open('./box_priors.txt', 'r')
ls = fp.readlines()
for s in ls:
aList = re.findall('([-+]?\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?', s)
for ss in aList:
aNum = float((ss[0]+ss[2]))
box_priors_.append(aNum)
fp.close()
box_priors = np.array(box_priors_)
box_priors = box_priors.reshape(4, NUM_RESULTS)
return box_priors
if __name__ == '__main__':
# Create RKNN object
rknn = RKNN(verbose=True)
# Pre-process config
print('--> Config model')
rknn.config(mean_values=[127.5, 127.5, 127.5], std_values=[127.5, 127.5, 127.5], target_platform='rk3566')
print('done')
# Load model (from https://github.com/fvmassoli/Deep-Learning-SSD-Object-Detection)
print('--> Loading model')
ret = rknn.load_tensorflow(tf_pb='./ssd_mobilenet_v1_coco_2017_11_17.pb',
inputs=['Preprocessor/sub'],
outputs=['concat', 'concat_1'],
input_size_list=[[1, INPUT_SIZE, INPUT_SIZE, 3]])
if ret != 0:
print('Load model failed!')
exit(ret)
print('done')
# Build Model
print('--> Building model')
ret = rknn.build(do_quantization=True, dataset='./dataset.txt')
if ret != 0:
print('Build model failed!')
exit(ret)
print('done')
# Export rknn model
print('--> Export rknn model')
ret = rknn.export_rknn('./ssd_mobilenet_v1_coco.rknn')
if ret != 0:
print('Export rknn model failed!')
exit(ret)
print('done')
# Set inputs
orig_img = cv2.imread('./road.bmp')
img = cv2.cvtColor(orig_img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, (INPUT_SIZE, INPUT_SIZE), interpolation=cv2.INTER_CUBIC)
# Init runtime environment
print('--> Init runtime environment')
ret = rknn.init_runtime()
if ret != 0:
print('Init runtime environment failed!')
exit(ret)
print('done')
# Inference
print('--> Running model')
outputs = rknn.inference(inputs=[img])
print('done')
predictions = outputs[0].reshape((1, NUM_RESULTS, 4))
np.save('./tensorflow_ssd_mobilenet_v1_0.npy', outputs[0])
outputClasses = outputs[1].reshape((1, NUM_RESULTS, NUM_CLASSES))
np.save('./tensorflow_ssd_mobilenet_v1_1.npy', outputs[0])
candidateBox = np.zeros([2, NUM_RESULTS], dtype=int)
classScore = [-1000.0] * NUM_RESULTS
vaildCnt = 0
box_priors = load_box_priors()
# Post Process
# got valid candidate box
for i in range(0, NUM_RESULTS):
topClassScore = -1000
topClassScoreIndex = -1
# Skip the first catch-all class.
for j in range(1, NUM_CLASSES):
score = expit(outputClasses[0][i][j])
if score > topClassScore:
topClassScoreIndex = j
topClassScore = score
if topClassScore > 0.4:
candidateBox[0][vaildCnt] = i
candidateBox[1][vaildCnt] = topClassScoreIndex
classScore[vaildCnt] = topClassScore
vaildCnt += 1
# calc position
for i in range(0, vaildCnt):
if candidateBox[0][i] == -1:
continue
n = candidateBox[0][i]
ycenter = predictions[0][n][0] / Y_SCALE * box_priors[2][n] + box_priors[0][n]
xcenter = predictions[0][n][1] / X_SCALE * box_priors[3][n] + box_priors[1][n]
h = math.exp(predictions[0][n][2] / H_SCALE) * box_priors[2][n]
w = math.exp(predictions[0][n][3] / W_SCALE) * box_priors[3][n]
ymin = ycenter - h / 2.
xmin = xcenter - w / 2.
ymax = ycenter + h / 2.
xmax = xcenter + w / 2.
predictions[0][n][0] = ymin
predictions[0][n][1] = xmin
predictions[0][n][2] = ymax
predictions[0][n][3] = xmax
# NMS
for i in range(0, vaildCnt):
if candidateBox[0][i] == -1:
continue
n = candidateBox[0][i]
xmin0 = predictions[0][n][1]
ymin0 = predictions[0][n][0]
xmax0 = predictions[0][n][3]
ymax0 = predictions[0][n][2]
for j in range(i+1, vaildCnt):
m = candidateBox[0][j]
if m == -1:
continue
xmin1 = predictions[0][m][1]
ymin1 = predictions[0][m][0]
xmax1 = predictions[0][m][3]
ymax1 = predictions[0][m][2]
iou = CalculateOverlap(xmin0, ymin0, xmax0, ymax0, xmin1, ymin1, xmax1, ymax1)
if iou >= 0.45:
candidateBox[0][j] = -1
# Draw result
for i in range(0, vaildCnt):
if candidateBox[0][i] == -1:
continue
n = candidateBox[0][i]
xmin = max(0.0, min(1.0, predictions[0][n][1])) * INPUT_SIZE
ymin = max(0.0, min(1.0, predictions[0][n][0])) * INPUT_SIZE
xmax = max(0.0, min(1.0, predictions[0][n][3])) * INPUT_SIZE
ymax = max(0.0, min(1.0, predictions[0][n][2])) * INPUT_SIZE
print("%d @ (%d, %d) (%d, %d) score=%f" % (candidateBox[1][i], xmin, ymin, xmax, ymax, classScore[i]))
cv2.rectangle(orig_img, (int(xmin), int(ymin)), (int(xmax), int(ymax)),
(random.random()*255, random.random()*255, random.random()*255), 3)
cv2.imwrite("result.jpg", orig_img)
rknn.release()
以上程序主要包括以下几个步骤和对应的接口(省略了参数调用):
- 创建RKNN对象: rknn=RKNN()
- 模型输入的预处理配置: rknn.config()
- 加载tensorflow生成的pb模型:rknn.load_tensorflow()
- 构建rknn模型:rknn.build()
- 导出rknn模型:rknn.export_rknn()
- 初始化模型运行环境:rknn.init_runtime()
- 设置模型的输入:按照实际模型输入的要求准备好输入数据
- 利用rknn模型进行推理: rknn.inference()
- 模型推理结果的后处理:将模型输出的推理结果处理成方便理解和使用的形式
以上是针对tensorflow框架下的机器学习模型的转换和运行步骤,rknn-toolkit2还支持以下机器学习模型框架:
机器学习模型框架 | 输入模型文件后缀 | 对应模型加载API |
---|---|---|
Caffe | .prototxt | load_caffe |
TensorFlow | .pb | load_tensorflow |
TensorFlow Lite | .tflite | load_tflite |
ONNX | .onnx | load_onnx |
DarkNet | .cfg | load_darknet |
PyTorch | .pt | load_pytorch |
连板运行
rknn模型的推理运行有两种方式,一种是直接在PC上利用模拟环境调用rknn模型进行推理运算。还有一种是利用rk3588上的硬件NPU进行推理运算。第二种方式需要在PC和RK3588开发板之间进行数据传输,主要包括以下步骤:
- 用USB转typeC的线,将开发板上的TypeC0口和PC的USB口进行连接
- 在RK3588开发板上启动rknn_server。在终端中运行
/usr/bin/rknn_server start
rknn_server启动后会打印相关信息,并且在有PC端连接时也会打印相应信息,如下图所示:
- 修改上面的测试程序,在运行环境初始化时指定运行的目标平台:
rknn.init_runtime(target='rk3588')
- 运行测试程序得到推理结果
python3 test.py
得到输出结果如下:
从上面的打印信息可以看出, PC上的rknn-toolkit2成功与开发板上运行的rknn服务器连接成功,并且通过调用rk3588的硬件NPU进行模型的推理运算,得出运算结果。
下图是通过连板运行模型推理得到的结果,可以看出目标检测模型成功检测出凸显各种的人,自行车,汽车等目标。
总结
通过rknn-toolkit2开发工具,可以方便的将各种框架下得到的机器学习模型转化为rk3588需要的rknn模型,并且调用rk3588的NPU进行模型的推理运算。考虑到rk3588的NPU具有很强的运算能力(6TOPS),将机器学习模型部署到rk3588上并由专用的NPU提供算力支持,可以大大提高模型的计算速度和能力,为机器学习模型的终端部署提供有效保证。
当然,在实际产品开发中,如果是使用python环境,其实是通过在rk3588中调用rknn-lite工具包调用rknn模型进行推理计算。而更高效的方式则是利用rockchip公司提供的rknpu的C语言API对转换后的rknn模型进行调用和推理,关于这部分内容,有机会博主会在后续的文章中详细总结整理。