3. 手写数字识别

本章使用PaddlePaddle深度学习框架,以手写数字识别为例,简单介绍下:

  • 介绍下分类问题以及卷积神经网络等相关知识

  • 如何使用paddlepaddle构建和优化一个神经网络

  • 如何将训练的模型转换为rknn模型

  • 如何在板卡上部署模型

提示

测试环境:鲁班猫板卡使用Debian10/11,PC端是ubuntu20.04,rknn-Toolkit2版本1.5.0。

3.1. 卷积神经网络

卷积神经网络(convolutional neural network,CNN)是目前计算机视觉中使用最普遍的模型结构。 基于卷积神经网络架构的模型在计算机视觉领域中占主导地位,当今几乎所有的图像识别、目标检测或语义分割相关问题都以这种方法为基础。

3.2. 训练MINST手写数字识别模型

手写数字识别是一个典型的图像分类问题, 图像分类是卷积神经网络的一项重要应用,根据图像的语义信息对不同类别图像进行区分,是计算机视觉的核心, 是物体检测、图像分割、物体跟踪、行为分析、人脸识别等其他高层次视觉任务的基础。 图像分类在许多领域都有着广泛的应用,如:安防领域的人脸识别和智能视频分析等,交通领域的交通场景识别, 互联网领域基于内容的图像检索和相册自动归类,医学领域的图像识别等。

常见的图像分类的卷积神经网络:LeNet、AlexNet、VGG、GoogLeNet、ResNet等等。

下面训练一个手写数字识别模型然后在板端部署的完整过程:

  • 准备数据集

  • 构建神经网络模型

  • 训练模型

  • 评估模型

  • 导出模型和模型转换,模拟推理

  • 板端部署推理

下面我们将按照上面步骤,在板卡上训练手写数字识别模型, 具体操作可以参考 handwritten.ipynb

3.2.1. 准备数据集

MNIST数据集是机器学习中常见的数据集,可以从该网址下载: http://yann.lecun.com/exdb/mnist/ , 该数据集包含四个文件:

  • t10k-images-idx3-ubyte.gz,是训练集的图片,总共10 000张。

  • t10k-labels-idx1-ubyte.gz,是训练集图片的标签(label),从0~9的数字,总共10 000条。

  • train-images-idx3-ubyte.gz,是训练集的图片,总共60 000张。

  • train-labels-idx1-ubyte.gz,是训练集图片的标签(label),从0~9的数字,总共60 000条。

使用PaddlePaddle深度学习框架,可以直接使用自带的API接口,下载MNIST数据集并读取该数据集(其他深度度学习框架也类似)。 默认会下载到用户目录下的缓存数据集路径下(~/.cache/paddle/dataset/mnist/)。

通常将样本集合分成训练集、校验集和测试集。

  • 训练集,用于训练模型的参数,即训练过程中主要完成的工作。

  • 校验集,用于对模型超参数的选择,比如网络结构的调整、正则化项权重的选择等。

  • 测试集,用于模拟模型在应用后的真实效果。因为测试集没有参与任何模型优化或参数训练的工作,所以它对模型来说是完全未知的样本。 在不以校验数据优化网络结构或模型超参数时,校验数据和测试数据的效果是类似的,均更真实的反映模型效果。

这里将导入训练和测试数据,也就是只分为训练集和测试集。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 导入必要的paddle模块
# paddle.nn 是组网相关的API
# paddle.vision 视觉领域相关API,包括常见的数据集,相关处理等
# paddle.io 输入输出相关API,主要是数据集的导入读取等
import paddle
from paddle.nn import Conv2D, MaxPool2D, Linear
import paddle.nn.functional as F
from paddle.vision.transforms import Normalize
import numpy as np
import os

# 下载数据集并初始化,导入训练和测试数据集,并对数据进行处理
transform = Normalize(mean=[127.5], std=[127.5])

train_dataset = paddle.vision.datasets.MNIST(mode='train', transform=transform)
test_dataset = paddle.vision.datasets.MNIST(mode='test', transform=transform)

3.2.2. 模型设计

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 定义模型结构
# 多层卷积神经网络实现
class MNIST(paddle.nn.Layer):
    def __init__(self):
        super(MNIST, self).__init__()

        # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
        self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2)
        # 定义池化层,池化核的大小kernel_size为2,池化步长为2
        self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
        # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
        self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2)
        # 定义池化层,池化核的大小kernel_size为2,池化步长为2
        self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
        # 定义一层全连接层,输出维度是10
        self.fc = Linear(in_features=980, out_features=10)

    # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出
    # 卷积层激活函数使用Relu,全连接层激活函数使用softmax
    def forward(self, inputs, label):
        x = self.conv1(inputs)
        x = F.relu(x)
        x = self.max_pool1(x)
        x = self.conv2(x)
        x = F.relu(x)
        x = self.max_pool2(x)
        x = paddle.reshape(x, [x.shape[0], -1])
        x = self.fc(x)
        if label is not None:
            acc = paddle.metric.accuracy(input=x, label=label)
            return x, acc
        else:
            return x

模型定义前面部分使用paddle.nn 提供的Conv2D和MaxPool2D函数,网络第一层是卷积层,

3.2.3. 模型训练

先使用 paddle.io.DataLoader API读取数据,该接口实现异步数据读取, 数据会由Python线程预先读取,并异步送入一个队列中,读取到的数据不断的放入缓存区,无需等待模型训练就可以启动下一轮数据读取,数据读取和模型训练并行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def train(model):
    model.train()

    # 调用加载数据的函数,设置mini-batch 中样本个数为100,设置shuffle=True打乱顺序
    train_loader = paddle.io.DataLoader(train_dataset, batch_size=100, shuffle=True)

    # 四种优化算法的设置方案,可以逐一尝试效果
    opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
    # opt = paddle.optimizer.Momentum(learning_rate=0.01, momentum=0.9, parameters=model.parameters())
    # opt = paddle.optimizer.Adagrad(learning_rate=0.01, parameters=model.parameters())
    # opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters())

    EPOCH_NUM = 10
    iter=0
    iters=[]
    losses=[]

    for epoch_id in range(EPOCH_NUM):
        for batch_id, data in enumerate(train_loader()):
            # 准备数据
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)

            # 前向计算的过程,同时计算分类准确率
            predicts, acc = model(images, labels)
            #predicts = model(images)

            # 计算损失,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)

            # 每训练了100批次的数据,打印下当前Loss的情况
            if batch_id % 100 == 0:
                print("epoch: {}, batch: {}, loss is: {}, acc is {}".format(epoch_id, batch_id, avg_loss.item(), acc.item()))
                iters.append(iter)
                losses.append(avg_loss.item())
                iter = iter + 100

            # 后向传播,更新参数的过程
            avg_loss.backward()
            # 最小化loss,更新参数
            opt.step()
            # 清除梯度
            opt.clear_grad()

    # 保存模型参数
    paddle.save(model.state_dict(), './paddle/mnist.pdparams')

    return iters, losses

# 创建模型
model = MNIST()

#paddle.summary(model, (1, 1, 28, 28))

# 启动训练过程
#开启GPU
#use_gpu = True
#paddle.device.set_device('gpu:0') if use_gpu else paddle.device.set_device('cpu')
iters, losses = train(model)

# 画出训练过程中Loss的变化曲线
plt.figure()
plt.title("train loss", fontsize=24)
plt.xlabel("iter", fontsize=14)
plt.ylabel("loss", fontsize=14)
plt.plot(iters, losses,color='red',label='train loss')
plt.grid()
plt.show()

上面程序如果取消paddle.summary(model, (1, 1, 28, 28))的注释,会打印网络结构:

---------------------------------------------------------------------------
Layer (type)       Input Shape          Output Shape         Param #
===========================================================================
Conv2D-3       [[1, 1, 28, 28]]     [1, 20, 28, 28]          520
MaxPool2D-3    [[1, 20, 28, 28]]     [1, 20, 14, 14]           0
Conv2D-4      [[1, 20, 14, 14]]     [1, 20, 14, 14]        10,020
MaxPool2D-4    [[1, 20, 14, 14]]      [1, 20, 7, 7]            0
Linear-2          [[1, 980]]            [1, 10]             9,810
===========================================================================
Total params: 20,350
Trainable params: 20,350
Non-trainable params: 0
---------------------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.19
Params size (MB): 0.08
Estimated Total Size (MB): 0.27
---------------------------------------------------------------------------

不开启打印网络结构,直接训练显示的Loss的变化曲线:

broken

3.2.4. 模型测试

这里用测试集来简单评估下模型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 定义预测函数
def evaluation(model):
    # 加载模型参数
    params_file_path = './paddle/mnist.pdparams'
    param_dict = paddle.load(params_file_path)
    model.load_dict(param_dict)

    model.eval()
    eval_loader = paddle.io.DataLoader(test_dataset)

    acc_set = []
    avg_loss_set = []
    for batch_id, data in enumerate(eval_loader()):
        images, labels = data
        images = paddle.to_tensor(images)
        labels = paddle.to_tensor(labels)
        predicts, acc = model(images, labels)
        loss = F.cross_entropy(input=predicts, label=labels)
        avg_loss = paddle.mean(loss)
        acc_set.append(float(acc.numpy()))
        avg_loss_set.append(float(avg_loss.numpy()))

    # 计算多个batch的平均损失和准确率
    acc_val_mean = np.array(acc_set).mean()
    avg_loss_val_mean = np.array(avg_loss_set).mean()

    print('loss={}, acc={}'.format(avg_loss_val_mean, acc_val_mean))

model = MNIST()
evaluation(model)

测试输出:

loading mnist model from  ./paddle/mnist.pdparams
loss=0.05035130242863519, acc=0.9844

3.2.5. 导出rknn模型和模拟推理测试

Paddle模型不能直接转换成rknn模型,需要先转换为onnx模型,然后使用rknn-toolkit2转换模型成rknn模型。

1、将我们训练的模型保存为onnx模型

1
2
3
4
5
6
# 需要保存的路径,当前onnx目录下
save_path = 'onnx/handwritten'
# 为模型指定输入的形状和数据类型
x_spec = paddle.static.InputSpec([1, 1, 28, 28], 'float32', 'x')
# 生成ONNX模型
paddle.onnx.export(model, save_path, input_spec=[x_spec], opset_version=11)

转换成功后,会在当前onnx目录下生成handwritten.onnx模型文件,运行输出:

2023-09-09 09:03:22 [INFO]  Static PaddlePaddle model saved in onnx/paddle_model_static_onnx_temp_dir.
[Paddle2ONNX] Start to parse PaddlePaddle model...
[Paddle2ONNX] Model file path: onnx/paddle_model_static_onnx_temp_dir/model.pdmodel
[Paddle2ONNX] Paramters file path: onnx/paddle_model_static_onnx_temp_dir/model.pdiparams
[Paddle2ONNX] Start to parsing Paddle model...
[Paddle2ONNX] Use opset_version = 11 for ONNX export.
[Paddle2ONNX] PaddlePaddle model is exported as ONNX format now.
2023-09-09 09:03:22 [INFO]  ONNX model saved in onnx/handwritten.onnx.

2、 使用rknn-toolkit2转换模型成rknn模型,并进行简单推理测试

我们将在PC端,使用安装了rknn-toolkit2的环境中进行转换,rknn-toolkit2的安装参考下前面章节。 本例中rknn-toolkit2的转换模型和推理的流程:

  • 创建RKNN对象,初始化RKNN环境;

  • 设置模型预处理参数,通过模拟器运行模型时需要调用config接口设置模型的预处理参数;

  • 导入模型,使用load_onnx接口导入模型;

  • 构建RKNN模型,调用build接口构建RKNN模型;

  • 调用export_rknn接口导出RKNN模型,同时可以使用init_runtime初始化运行环境,在模拟器上模拟推理;

  • 初始化运行环境后,可以调用inference接口进行模拟推理;

  • 最后调用release接口释放RKNN对象。

测试文件的主体部分:

onnx_to_rknn.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
if __name__ == '__main__':

    # 创建RKNN对象
    rknn = RKNN()

    # 配置,这里测试lubancat-4指定平台是rk3588,如果是lubancat0/1/2指定平台rk3566、rk3568
    print('--> Config model')
    rknn.config(mean_values=[[127.5]], std_values=[[127.5]], target_platform='rk3588')
    print('done')

    # 导入onnx模型
    print('--> Loading model')
    ret = rknn.load_onnx(model=ONNX_MODEL)
    if ret != 0:
        print('Load model failed!')
        exit(ret)
    print('done')

    # 构建模型,这里QUANTIZE_ON=False,不使用量化
    print('--> Building model')
    #ret = rknn.build(do_quantization=QUANTIZE_ON, dataset=DATASET)
    ret = rknn.build(do_quantization=QUANTIZE_ON)
    if ret != 0:
        print('Build model failed!')
        exit(ret)
    print('done')

    # 导出rknn模型
    print('--> Export rknn model')
    ret = rknn.export_rknn(RKNN_MODEL)
    if ret != 0:
        print('Export rknn model failed!')
        exit(ret)
    print('done')

    # Init runtime environment
    print('--> Init runtime environment')
    ret = rknn.init_runtime()
    if ret != 0:
        print('Init runtime environment failed!')
        exit(ret)
    print('done')

    # 读取图像
    img = cv2.imread(IMG_PATH)
    img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
    img = cv2.resize(img, (IMG_SIZE, IMG_SIZE))
    img = np.expand_dims(img, 0)
    img = np.expand_dims(img, 0)

    # 模拟推理
    print('--> Running model')
    outputs = rknn.inference(inputs=[img], data_format='nchw')
    print('done')

    # 打印输出结果
    print("outputs: ", outputs)
    print("本次预测的数字是:", np.argmax(outputs))

    rknn.release()

运行onnx_to_rknn.py显示:

W __init__: rknn-toolkit2 version: 1.5.0+1fa95b5c
--> Config model
done
--> Loading model
W load_onnx: It is recommended onnx opset 12, but your onnx model opset is 11!
Loading : 100%|█████████████████████████████████████████████████████| 7/7 [00:00<00:00, 9288.24it/s]
done
--> Building model
done
--> Export rknn model
done
--> Init runtime environment
W init_runtime: Target is None, use simulator!
done
--> Running model
Analysing : 100%|███████████████████████████████████████████████████| 8/8 [00:00<00:00, 4285.92it/s]
Preparing : 100%|███████████████████████████████████████████████████| 8/8 [00:00<00:00, 1258.94it/s]
done
outputs:  [array([[ 8.6328125 , -0.21862793,  3.5253906 ,  0.38549805, -1.8681641 ,
        -0.39990234,  1.3476562 , -0.90722656, -1.328125  ,  0.8574219 ]],
    dtype=float32)]
本次预测的数字是: 0

转换出的handwritten.rknn文件,将保存到变量RKNN_MODEL指定的目录下。

3.2.6. 板端部署推理

前面小节我们使用rknn-toolkit2工具,转换出来handwritten.rknn文件,那如何在板卡端部署? 我们可以使用RKNN API接口(C/C++)或者RKNN Toolkit Lite2接口(Python),这些接口的使用参考下前面章节, 下面我们演示下在鲁班猫板卡上部署模型。

1、使用Toolkit Lite2部署测试

Toolkit Lite2的使用流程参考下前面 《RKNN Toolkit Lite2介绍》 。 下面是手写数字识别模型在板端部署推理的程序:

rknn_inference.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 创建RKNNLite对象
# rknn_lite = RKNNLite(verbose=True)
rknn_lite = RKNNLite()

# 调用load_rknn接口导入RKNN模型,需要对应平台(rk356x/rk3588)的模型
print('--> Load RKNN model')
ret = rknn_lite.load_rknn(rknn_model)
if ret != 0:
    print('Load RKNN model failed')
    exit(ret)
print('done')

# 调用init_runtime接口初始化运行时环境
print('--> Init runtime environment')
ret = rknn_lite.init_runtime()
if ret != 0:
    print('Init runtime environment failed!')
    exit(ret)
print('done')

# 读取图像,对图像数据预处理
img = cv2.imread(IMG_PATH)
img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
img = cv2.resize(img,(28,28))
img = np.expand_dims(img, 0)

# runing model
print('--> Running model')
outputs = rknn_lite.inference(inputs=[img])

# 输出结果
print("outputs: ", outputs)
print("板端本次预测的数字是:", np.argmax(outputs))

# 调用release接口释放RKNNLite对象
rknn_lite.release()

在板卡上运行输出(lubancat-4):

cat@lubancat:~/handwritten$ python3 rknn_inference.py
--> Load RKNN model
done
--> Init runtime environment
I RKNN: [14:15:06.736] RKNN Runtime Information: librknnrt version: 1.5.0 (e6fe0c678@2023-05-25T08:09:20)
I RKNN: [14:15:06.736] RKNN Driver Information: version: 0.8.8
I RKNN: [14:15:06.736] RKNN Model Information: version: 4, toolkit version: 1.5.0+1fa95b5c(compiler version: 1.5.0 (e6fe0c678@2023-05-25T16:15:03)),
target: RKNPU v2, target platform: rk3588, framework name: ONNX, framework layout: NCHW, model inference type: static_shape
done
--> Running model
outputs:  [array([[ 8.640625  , -0.20239258,  3.53125   ,  0.3876953 , -1.8496094 ,
        -0.40698242,  1.3486328 , -0.91503906, -1.3388672 ,  0.8515625 ]],
    dtype=float32)]
板端本次预测的数字是: 0

2、使用RKNN API部署测试

在板端部署手写数字识别模型,使用RKNN API 接口,具体调用流程参考下 《RKNN API》 。 这里简单测试,使用通用API接口,主要部分代码如下(详细查看下配套例程,配套例程可能没有及时更新):

main.cc
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// .....前面省略
/*-------------------------------------------
                Main Functions
-------------------------------------------*/
int main(int argc, char** argv)
{

// ....省略
// 初始化rknn,导入rknn模型
printf("Loading mode %s ...\n", model_name);
int            model_data_size = 0;
unsigned char* model_data      = load_model(model_name, &model_data_size);
ret                            = rknn_init(&ctx, model_data, model_data_size, 0, NULL);
if (ret < 0) {
    printf("rknn_init error ret=%d\n", ret);
    return -1;
}

// 打印输出RKNN驱动版本,API版本
rknn_sdk_version version;
ret = rknn_query(ctx, RKNN_QUERY_SDK_VERSION, &version, sizeof(rknn_sdk_version));
if (ret < 0) {
    printf("rknn_init error ret=%d\n", ret);
    return -1;
}
printf("sdk version: %s driver version: %s\n", version.api_version, version.drv_version);

// 查询模型输入和输出,并打印输出
rknn_input_output_num io_num;
ret = rknn_query(ctx, RKNN_QUERY_IN_OUT_NUM, &io_num, sizeof(io_num));
    if (ret < 0) {
        printf("rknn_init error ret=%d\n", ret);
        return -1;
    }
    printf("model input num: %d, output num: %d\n", io_num.n_input, io_num.n_output);
}

// .....中间部分省略....
// 读取图像
printf("Read %s ...\n", image_name);
cv::Mat orig_img = cv::imread(image_name, 1);
if (!orig_img.data) {
    printf("cv::imread %s fail!\n", image_name);
    return -1;
}

cv::Mat img;
cv::cvtColor(orig_img, img, cv::COLOR_RGB2GRAY);
img_width  = img.cols;
img_height = img.rows;
printf("img width = %d, img height = %d\n", img_width, img_height);

// 重新设置图像尺寸,例程测试就简单使用opencv
if (img_width != width || img_height != height) {
    printf("resize with Opencv! \n");
    cv::Mat resize_image;
    cv::resize(img, resize_image, cv::Size(width, height), cv::INTER_LINEAR);
    inputs[0].buf = (void*)resize_image.data;
} else {
    inputs[0].buf = (void*)img.data;
}

// 设置输入输出
rknn_inputs_set(ctx, io_num.n_input, inputs);
rknn_output outputs[1];
outputs[0].want_float = 1;

// 运行推理,得到推理结果
gettimeofday(&start_time, NULL);
ret = rknn_run(ctx, NULL);
ret = rknn_outputs_get(ctx, io_num.n_output, outputs, NULL);
gettimeofday(&stop_time, NULL);
printf("once run use %f ms\n", (__get_us(stop_time) - __get_us(start_time)) / 1000);

// 打印输出
float*   buffer = (float*)outputs[0].buf;
printf("outputs: ");
for (int i = 0; i < 10; i++) {
    printf("%.6f ", buffer[i]);
}
printf("\n");

// 输出预测的值
findMaxAndPosition(buffer, 10);

// 释放
ret = rknn_destroy(ctx);
if (model_data) {
    free(model_data);
}

return 0;
}

复制配套文件到板卡上,编译源码(配套例程没有适配在PC上交叉编译,有需要可以自行修改CMakeFiles.txt和添加库文件等), 编译使用板卡默认安装的rknn库,如果需要更新库或者添加头文件,参考下前面 《RKNN API》

执行命令编译程序:

# 切换到手写数字例程handwritten/c++目录下,然后编译,这里测试是lubancat-4板卡
cat@lubancat:~/lubancat-ai/handwritten/c++$ mkdir build && cd build
cat@lubancat:~/lubancat-ai/handwritten/c++/build$ cmake ../
-- The C compiler identification is GNU 10.2.1
-- The CXX compiler identification is GNU 10.2.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Found OpenCV: /usr (found version "4.5.1")
-- Configuring done
-- Generating done
-- Build files have been written to: /home/cat/lubancat-ai/handwritten/c++/build

# 然后执行make命令编译,生成文件handwritten
cat@lubancat:~/lubancat-ai/handwritten/c++/build$ make
Scanning dependencies of target handwritten
[ 50%] Building CXX object CMakeFiles/handwritten.dir/main.cc.o
[100%] Linking CXX executable handwritten
[100%] Built target handwritten
cat@lubancat:~/lubancat-ai/handwritten/c++/build$

执行部署输出(lubancat-4):

# 根据板卡不同,选择不同的模型
cat@lubancat:~/lubancat-ai/handwritten/c++/build$ ./handwritten ../../model/RK3588/handwritten.rknn  ../0.jpg
Loading mode ../../model/RK3588/handwritten.rknn ...
sdk version: 1.5.0 (e6fe0c678@2023-05-25T08:09:20) driver version: 0.8.8
model input num: 1, output num: 1
index=0, name=x, n_dims=4, dims=[1, 28, 28, 1], n_elems=784, size=1568, w_stride = 32, size_with_stride=1792, fmt=NHWC, type=FP16, qnt_type=AFFINE, zp=0, scale=1.000000
index=0, name=linear_2.tmp_1, n_dims=2, dims=[1, 10], n_elems=10, size=20, w_stride = 0, size_with_stride=20, fmt=UNDEFINED, type=FP16, qnt_type=AFFINE, zp=0, scale=1.000000
model is NHWC input fmt
model input height=28, width=28, channel=1
Read ../0.jpg ...
img width = 219, img height = 217
resize with Opencv!
once run use 0.188000 ms
outputs: 8.632812 -0.218262 3.525391 0.385742 -1.869141 -0.399170 1.348633 -0.907715 -1.328125 0.857910
本次预测的数字是: 0