部署模型到 Maix-I(M1) K210 系列开发板

K210 模型转换和部署
Maix-I 系列 K210
高性价比带硬件 AI 加速的单片机
1Tops@INT8,有限算子加速

欢迎修改和补充

一般使用 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 的模型库,下载别人分享的模型,或者使用在线训练出的模型,直接使用或者参考模型结构。