Deploy model to Maix-I(M1) K210 series development boards

This document is not translate yet, translation is welcome

欢迎修改和补充

一般使用 tensorflow 训练出浮点模型, 再使用转换工具将其转换成 K210 所支持的 Kmodel 模型,然后将模型部署到 K210 开发板上。

K210 上的 KPU

K210 上的 AI 硬件加速单元取名为KPUKPU 实现了 卷积、批归一化、激活、池化 这 4 种基础操作的硬件加速, 但是它们不能分开单独使用,是一体的加速模块。

所以, 在 KPU 上面推理模型, 以下要求:

内存限制

K210 有 6MB 通用 RAM 和 2MB KPU 专用 RAM。模型的输入和输出特征图存储在 2MB KPU RAM 中。权重和其他参数存储在 6MB 通用 RAM 中,在转换模型时,会打印模型使用的内存大小以及临时最大内存使用情况。

哪些算子可以被 KPU 完全加速?

nncase 支持的算子:

下面的约束需要全部满足。

  • 特征图尺寸:输入特征图小于等于 320x240 (宽x高) 同时输出特征图大于等于 4x4 (宽x高),通道数在 1 到 1024。
  • Same 对称 paddings (TensorFlow 在 stride=2 同时尺寸为偶数时使用非对称 paddings)。
  • 普通 Conv2D 和 DepthwiseConv2D,卷积核为 1x1 或 3x3,stride 为 1 或 2。
  • 最大池化 MaxPool (2x2 或 4x4) 和 平均池化 AveragePool (2x2 或 4x4)。
  • 任意逐元素激活函数 (ReLU, ReLU6, LeakyRelu, Sigmoid...), KPU 不支持 PReLU。

哪些算子可以被 KPU 部分加速?

  • 非对称 paddings 或 valid paddings 卷积, nncase 会在其前后添加必要的 Pad 和 Crop(可理解为 边框 与 裁切)。
  • 普通 Conv2D 和 DepthwiseConv2D,卷积核为 1x1 或 3x3,但 stride 不是 1 或 2。 nncase 会把它分解为 KPUConv2D 和一个 StridedSlice (可能还需要 Pad)。
  • MatMul 算子, nncase 会把它替换为一个 Pad(到 4x4)+ KPUConv2D(1x1 卷积和) + Crop(到 1x1)。
  • TransposeConv2D 算子, nncase 会把它替换为一个 SpaceToBatch + KPUConv2D + BatchToSpace。

以上说明来自这里

训练出浮点模型

对于 K210, 建议使用 TensorFlow,因为它的转换工具对其支持最好。

tensorflow 举个例子, 两分类模型, 这里是随便叠的层结构

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

input_shape = (240, 320, 3)

model = tf.keras.models.Sequential()

model.add(layers.ZeroPadding2D(input_shape = input_shape, padding=((1, 1), (1, 1))))
model.add(layers.Conv2D(32, (3,3), padding = 'valid', strides = (2, 2)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu')); #model.add(MaxPool2D());


model.add(layers.ZeroPadding2D(padding=((1, 1), (1, 1))));
model.add(layers.Conv2D(32, (3,3), padding = 'valid',strides = (2, 2)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));


model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));
model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.ZeroPadding2D(padding=((1, 1), (1, 1))));
model.add(layers.Conv2D(32, (3,3), padding = 'valid',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));
model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));


model.add(layers.ZeroPadding2D(padding=((1, 1), (1, 1))));
model.add(layers.Conv2D(32, (3,3), padding = 'valid',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));
model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));


model.add(layers.ZeroPadding2D(padding=((1, 1), (1, 1))));
model.add(layers.Conv2D(32, (3,3), padding = 'valid',strides = (2, 2)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));
model.add(layers.Conv2D(32, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.ZeroPadding2D(padding=((1, 1), (1, 1))));
model.add(layers.Conv2D(64, (3,3), padding = 'valid',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));

model.add(layers.Conv2D(64, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));
model.add(layers.Conv2D(64, (3,3), padding = 'same',strides = (1, 1)));model.add(layers.BatchNormalization());model.add(layers.Activation('relu'));


model.add(layers.Flatten())
model.add(layers.Dropout(0.5))
model.add(layers.Dense(2))
model.add(layers.Activation('softmax'))

model.summary()

model.compile(
              loss ='sparse_categorical_crossentropy',
              optimizer = 'adam',
              metrics =['accuracy'])

mode.fit(...)

这里你可能注意到了, 在 conv 层中stride != 1 时, 都加了一个 zeropadding 层, 这是 K210 硬件支持的模式, 如果不这样做, 转换成 V3 模型时(使用 nncase v0.1.0 RC5) 则直接报错, 使用 V4 模型(nncase V0.2.0转换)可以通过,但是是使用软件运算的, 会消耗大量内存和时间, 会发现内存占用大了很多!!! 所以设计模型时也需要注意

转换工具

使用 K210 芯片官方提供的 nncase 工具来进行转换。

需要注意的是,工具版本更新迭代比较多, K210属于第一代芯片,算子支持有限,并且内存只有6MiB(通用)+2MiB(AI专用)内存,所以最新版本的工具可能并不是最好的选择,根据需求选择合适的版本。

由于代码更新, 在过程中模型格式产生了两个大版本, V3V4, 其中 V3 模型是指用 nncase v0.1.0 RC5 转换出来的模型; V4模型指用 nncase v0.2.0 转换出来的模型,以及 V5 或更新版本等等。

两者有一定的不同,所以现在两者共存, V3 代码量更少,占用内存小,效率也高,但是支持的算子少; V4 支持的算子更多,但是都是软件实现的,没有硬件加速,内存使用更多,所以各有所长。 MaixPy 的固件也可以选择是否支持 V4, 如果你的模型 V3 能够满足算子支持,强烈建议用 V3,在遇到算子不支持而且一定要用那个算子时再用V4

运行测试模型

使用 MaixPy 来运行模型,也可以用 C SDK 写。

比如使用 MaixPy固件, 将模型放到 SD 卡, 然后使用代码加载

   import KPU as kpu
   import image
   m = kpu.load("/sd/test.kmodel")
   img = image.Image("/sd/test.jpg")
   img = img.resize(224, 224)
   img.pix_to_ai()
   feature_map = kpu.forward(m, img)
   p_list = feature_map[:]

更多参考

上传分享到 MaixHub

可以上传分享你的模型到到 MaixHub 的模型库,可以让更多人发现并使用你的模型~ 一起做出更多有趣的项目吧!(K210 模型支持加密分享)

另外你也可以使用 MaixHub 的模型库,下载别人分享的模型,或者使用在线训练出的模型,直接使用或者参考模型结构。