乐趣区

关于深度学习:技术博客丨我用深度学习做个视觉AI微型处理器

作者:张强,Datawhale 成员

讲多了算法,如何真正将算法利用到产品畛域?本文将带你从 0 用深度学习打造一个视觉 AI 的微型处理器。文章含残缺代码,知识点绝对独立,欢送点赞珍藏,跟着本文做完,你也能够做一个本人的嵌入式 AI 小产品!

背景

随着硬件尤其是显卡性能降级,以及 Pytorch,TensorFlow 深度学习框架日趋完善,视觉 AI 算法在多个畛域遍地开花,其中就包含嵌入式设施。这是一种微型处理器,它的要害单元就是外部小小的计算芯片。嵌入式设施和咱们日常用的电脑相比体积小,只蕴含必要外设。一些针对特定工作的嵌入式设施往往不会运载咱们罕用的比方 Windows、Linux 零碎,而是间接将代码烧录进去运行。

在嵌入式设施上尝试部署深度学习算法开始较早,1989 年一家叫做 ALVIVN 的公司就将神经网络用在汽车上了。现如今,工程师们将其用在安防、机器人、主动驾驶等畛域。因而,懂得如何设计、训练算法,又能将其部署到边缘硬件产品上,能帮咱们实现许多产品的想法。

然而,视觉算法部署在产品中仍有许多难点,比方:(1)模型通常须要在 CPU/GPU/NPU/FPGA 等各种各样不同类型的平台上部署;(2)嵌入式算力 / 内存 / 存储空间都十分无限;跑在云端服务器上,须要实时联网又不很优雅;(3)模型训练时可能会应用不同的 AI 框架(比方 Pytorch/TensorFlow 等)、不同硬件(比方 GPU、NPU),互相适配产生问题[1]。

因而笔者开始思考下列问题:

  • 有什么亲民价格的芯片能解决部署视觉 AI 算法?
  • 如何将深度学习算法部署到嵌入式设施上?

对第一个问题,在通过调研后,还真有这样的芯片,那就是嘉楠科技的 K210 芯片。一个芯片几十元,对应的开发板在某宝上两百多就能够买到。依据嘉楠官网的形容,K210 具备双核 64-bit RISC-V RV64IMAFDC (RV64GC) CPU / 400MHz(可超频到 600MHz),双精度 FPU,8MiB 64bit 片上 SRAM(6MiB 通用 SRAM+2MiB 的 AI 专用 SRAM)。对于这块芯片更具体的介绍能够参考[2]。

市面上有许多搭载 K210 的开发板,笔者这里选了雅博一款性能较全的 K210 开发板,开始了嵌入式 AI 的折腾之路。

对于第二个问题,办法就多了,不同深度学习框架,不同硬件选型都决定着不同技术路线。基本路线能够为 深度学习平台训练 -> 模型剪枝、量化 -> 参数转换 -> 转换为硬件平台上能运行的模型

对深度学习平台选型,笔者决定选用当下最风行的 Pytorch 平台。最初一步往往取决于这个硬件的生态,如果没有相干生态反对,可能须要手写 C 语言代码加载参数运行。调研发现,K210 有一个深度网络优化平台NNCASE,能减速深度模型部署。

调研过程中发现在这块板子上部署模型大多数都是从 Keras、TensorFlow 开始训练并最终部署,而研究者罕用的 Pytorch 居然没有教程,于是明天就尝试来讲一讲。

接下来,咱们将从应用 Pytorch 训练手写体辨认的例子开始,买通从训练到嵌入式平台部署的流程。

01 应用 Pytorch 训练分类网络模型

必要软件包装置

pip install tensorbay pillow torch torchvision numpy

数据集获取

一个 AccessKey 获取所有数据集。

咱们应用一个开源数据集平台:gas.graviti.com,这个网站汇总了 AI 开发者常见的公开数据集,调用其 SDK 就能间接在线训练,而且许多数据集间接在国内网络下连贯间接应用,还是十分不便的。

a. 关上本文对应数据集链接 https://gas.graviti.com/datas…
b. 右上角注册登录
c. fork 数据集

d. 点击网页上方开发者工具,获取应用 SDK 所需的 AccessKey,获取到 AccessKey 后,将其存在我的项目根目录的 gas_key.py 里:

KEY = "<Your-Key>"

通过 AccessKey 能够上传数据、读取数据、应用数据,灵便对接模型开发和训练,与数据 pipeline 疾速集成。

e. AccessKey 写入后就能够写代码读取数据了,读取后能够应用一行代码自行下载,或者能够开启缓存性能,在读取过后会主动将数据存储到本地。将下载后的数据放在 data 文件夹下:

import numpy as np\
from PIL import Image\
\
from tensorbay import GAS\
from tensorbay.dataset import Dataset\
from tensorbay.dataset import Segment\
\
def read_gas_image(data):\
    with data.open() as fp:\
        image = Image.open(fp)\
    return np.array(image)\
  \
KEY = "用你的 Key 替换掉这个字符串"\
# Authorize a GAS client.\
gas = GAS(KEY)\
# Get a dataset.\
dataset = Dataset("MNIST", gas)\
\
# 开启上行语句在以后门路下的 data 目录缓存数据 \
# dataset.enable_cache("data")\
\
# List dataset segments.\
segments = dataset.keys()\
# Get a segment by name\
segment = dataset["train"]\
for data in segment:\
    # 图片数据 \
    image = read_gas_image(data)\
    # 标签数据 \
    label = data.label.classification.category

怎么把这个数据集集成到 Pytorch 里呢?官网文档也为此写了不少例子[4]。笔者尝试过感觉挺不便,在为不同工作训练嵌入式 AI 模型时,只需更换数据集的名字,就能集成,不必再关上浏览器、期待下载以及解决很久了。有了数据集之后,咱们接下来用其训练一个分类任务模型。

深度网络模型选型

联合硬件特点设计网络。

在思考硬件部署的工作时,网络的设计就要受到些许限度。

首先,大而深的模型是不行的,K210 的 RAM 是 6M,这意味着模型 + 程序都要烧到这个空间。当然咱们能够放到内存卡中,但实时性要受影响。

其次,还要思考 AI 编译器对特定算子的优化,以 K210 NNCASE 为例[3],其反对 TFLite、Caffe、ONNX 共三个平台的算子。

关上对应平台,可能到具体有哪些算子曾经实现了低层优化。能够看到对 ONNX 算子优化还是比拟多的。如果所选用的网络算子较新,抑或是模型太大,都要在本步多加思考与设计。

如果如果最初部署不胜利,往往须要回到这一步思考网络的设计。为了尽可能减少算子的应用,本文设计一个只基于卷积 +ReLU+Pool 的 CNN:

代码文件名:models/net.py

class Net(nn.Module):\
    def __init__(self):\
        super().__init__()\
        self.conv1 = nn.Conv2d(1, 6, 5)\
        self.relu1 = nn.ReLU()\
        self.pool1 = nn.MaxPool2d(2)\
\
        self.conv2 = nn.Conv2d(6, 16, 5)\
        self.relu2 = nn.ReLU()\
        self.pool2 = nn.MaxPool2d(2)\
\
        self.conv3 = nn.Conv2d(16, 32, 4)\
        self.relu3 = nn.ReLU()\
\
        self.conv4 = nn.Conv2d(32, 64, 1)\
        self.relu4 = nn.ReLU()\
\
        self.conv5 = nn.Conv2d(64, 32, 1)\
        self.relu5 = nn.ReLU()\
\
        self.conv6 = nn.Conv2d(32, 10, 1)\
        self.relu6 = nn.ReLU()\
\
    def forward(self, x):\
        y = self.conv1(x)\
        y = self.relu1(y)\
        y = self.pool1(y)\
        y = self.conv2(y)\
        y = self.relu2(y)\
        y = self.pool2(y)\
        y = self.conv3(y)\
        y = self.relu3(y)\
        y = self.conv4(y)\
        y = self.relu4(y)\
        y = self.conv5(y)\
        y = self.relu6(y)\
        y = self.conv6(y)\
        y = self.relu6(y)\
\
        y = y.view(y.shape[0], -1)\
\
        return y

网络训练

设计好模型后,应用如下脚本进行训练。接下来脚本文件大抵浏览一下,明确其中的工作原理即可。

代码文件名:1.train.py

留神将其中的 ACCESS_KEY 替成你本人的 AccessKey。

from __future__ import print_function\
import argparse\
import torch\
import torch.nn.functional as F\
import torch.optim as optim\
from torch.optim.lr_scheduler import StepLR\
from models.net import Net\
from PIL import Image\
from torch.utils.data import DataLoader, Dataset\
from torchvision import transforms\
\
from tensorbay import GAS\
from tensorbay.dataset import Dataset as TensorBayDataset\
\
\
class MNISTSegment(Dataset):\
    """class for wrapping a MNIST segment."""\
\
    def __init__(self, gas, segment_name, transform, cache=True):\
        super().__init__()\
        self.dataset = TensorBayDataset("MNIST", gas)\
        if cache:\
            self.dataset.enable_cache("data")\
        self.segment = self.dataset[segment_name]\
        self.category_to_index = self.dataset.catalog.classification.get_category_to_index()\
        self.transform = transform\
\
    def __len__(self):\
        return len(self.segment)\
\
    def __getitem__(self, idx):\
        data = self.segment[idx]\
        with data.open() as fp:\
            image_tensor = self.transform(Image.open(fp))\
\
        return image_tensor, self.category_to_index[data.label.classification.category]
        \
\
def create_loader(key):\
    to_tensor = transforms.ToTensor()\
    normalization = transforms.Normalize(mean=[0.485], std=[0.229])\
    my_transforms = transforms.Compose([to_tensor, normalization])\
\
    train_segment = MNISTSegment(GAS(key), segment_name="train", transform=my_transforms)\
    train_dataloader = DataLoader(train_segment, batch_size=4, shuffle=True, num_workers=0)\
    test_segment = MNISTSegment(GAS(key), segment_name="test", transform=my_transforms)\
    test_dataloader = DataLoader(test_segment, batch_size=4, shuffle=True, num_workers=0)\
    return train_dataloader, test_dataloader\
\
\
def train(args, model, device, train_loader, optimizer, epoch):\
    model.train()\
    for batch_idx, (data, target) in enumerate(train_loader):\
        data, target = data.to(device), target.to(device)\
        optimizer.zero_grad()\
        output = model(data)\
        loss = F.cross_entropy(output, target)\
        loss.backward()\
        optimizer.step()\
        if batch_idx % args.log_interval == 0:\
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(\
                epoch, batch_idx * len(data), len(train_loader.dataset),\
                       100. * batch_idx / len(train_loader), loss.item()))\
            if args.dry_run:\
                break\
                \
\
def test(model, device, test_loader):\
    model.eval()\
    test_loss = 0\
    correct = 0\
    with torch.no_grad():\
        for data, target in test_loader:\
            data, target = data.to(device), target.to(device)\
            output = model(data)\
            test_loss += F.cross_entropy(output, target, reduction='sum').item()  # sum up batch loss\
            pred = output.argmax(dim=1, keepdim=True)  # get the index of the max log-probability\
            correct += pred.eq(target.view_as(pred)).sum().item()\
\
    test_loss /= len(test_loader.dataset)\
\
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(\
        test_loss, correct, len(test_loader.dataset),\
        100. * correct / len(test_loader.dataset)))\
        \
\
def main():\
    # Training settings\
    parser = argparse.ArgumentParser(description='PyTorch MNIST')\
    parser.add_argument('--batch-size', type=int, default=64, metavar='N',\
                        help='input batch size for training (default: 64)')\
    parser.add_argument('--test-batch-size', type=int, default=1000, metavar='N',\
                        help='input batch size for testing (default: 1000)')\
    parser.add_argument('--epochs', type=int, default=14, metavar='N',\
                        help='number of epochs to train (default: 14)')\
    parser.add_argument('--lr', type=float, default=1.0, metavar='LR',\
                        help='learning rate (default: 1.0)')\
    parser.add_argument('--gamma', type=float, default=0.7, metavar='M',\
                        help='Learning rate step gamma (default: 0.7)')\
    parser.add_argument('--no-cuda', action='store_true', default=False,\
                        help='disables CUDA training')\
    parser.add_argument('--dry-run', action='store_true', default=False,\
                        help='quickly check a single pass')\
    parser.add_argument('--seed', type=int, default=1, metavar='S',\
                        help='random seed (default: 1)')\
    parser.add_argument('--log-interval', type=int, default=10, metavar='N',\
                        help='how many batches to wait before logging training status')\
    parser.add_argument('--save-model', action='store_true', default=False,\
                        help='For Saving the current Model')\
    args = parser.parse_args()\
    use_cuda = not args.no_cuda and torch.cuda.is_available()
    \
    torch.manual_seed(args.seed)\
\
    device = torch.device("cuda" if use_cuda else "cpu")\
\
    train_kwargs = {'batch_size': args.batch_size}\
    test_kwargs = {'batch_size': args.test_batch_size}\
    if use_cuda:\
        cuda_kwargs = {'num_workers': 1,\
                       'pin_memory': True,\
                       'shuffle': True}\
        train_kwargs.update(cuda_kwargs)\
        test_kwargs.update(cuda_kwargs)\
\
    ACCESS_KEY = 'Accesskey-4669e1203a6fa8291d5d7744ba313f91'\
    train_loader, test_loader = create_loader(ACCESS_KEY)\
    \
    model = Net().to(device)\
    optimizer = optim.Adadelta(model.parameters(), lr=args.lr)\
    scheduler = StepLR(optimizer, step_size=1, gamma=args.gamma)\
    for epoch in range(1, args.epochs + 1):\
        train(args, model, device, train_loader, optimizer, epoch)\
        test(model, device, test_loader)\
        scheduler.step()\
        \
    if args.save_model:\
        torch.save(model.state_dict(), "outputs/mnist.pt")\
\
\
if __name__ == '__main__':\
    main()

运行形式

关上终端

mkdir -p outputs
python 1.train.py --save-model

运行后果:

Train Epoch: 1 [0/60000 (0%)]    Loss: 2.305400
Train Epoch: 1 [640/60000 (1%)]    Loss: 1.359776
....

训练结束,在 outputs 文件下有 mnist.pt 模型文件。

02 参数转换

Pytorch 模型导出为 ONNX

“各家都有一套语言,换到一个规范再谈话”。

凋谢神经网络替换(Open Neural Network Exchange,简称 ONNX)是微软和 Facebook 提出用来示意深度学习模型的 凋谢格局。所谓凋谢就是 ONNX 定义了一组和环境,平台均无关的规范格局,来加强各种 AI 模型的可交互性。

换句话说,无论你应用何种训练框架训练模型(比方 TensorFlow /Pytorch /OneFlow /Paddle),在训练结束后你都能够将这些框架的模型对立转换为 ONNX 这种对立的格局进行存储。留神 ONNX 文件不仅仅存储了神经网络模型的权重,同时也存储了模型的构造信息以及网络中每一层的输入输出和一些其它的辅助信息。

ONNX 开源在了 [5], 笔者也为大家筹备了一些参考学习材料 ,可参考[5-10] 进行学习。
Pytorch 平台训练好模型后,应用下列脚本将模型转化为 ONNX:

首先装置用到的包为:

pip install onnx coremltools onnx-simplifier

脚本名称:2.pt2onnx.py

from models.net import Net
import torch.onnx
import os


def parse_args():
    import argparse
    parser = argparse.ArgumentParser(description='PyTorch pt file to ONNX file')
    parser.add_argument('-i', '--input', type=str, required=True)
    return parser.parse_args()


def main():
    args = parse_args()
    dummy_input = torch.randn(1, 1, 28, 28)
    model = Net()

    print("Loading state dict to cpu")
    model.load_state_dict(torch.load(args.input, map_location=torch.device('cpu')))
    name = os.path.join(os.path.dirname(args.input), os.path.basename(args.input).split('.')[0] + ".onnx")

    print("Onnx files at:", name)
    torch.onnx.export(model, dummy_input, name)


if __name__ == '__main__':
    main()

运行形式:

python3 2.pt2onnx.py -i outputs/mnist.pt

运行后果:

Loading state dict to cpu
Onnx files at: outputs/mnist.onnx

运行结束,在 outputs 文件可发现已将 pt 模型转为 mnist.onnx 模型文件。

ONNX 模型转化成 KModel

应用 NNCASE 将 ONNX 模型转为 K210 上能运行的 KModel,因为 NNCASE 环境配置较为依赖环境,咱们应用 Docker 实现环境配置,对于 docker 只须要装置并正确配置即可,不相熟的能够参考我写的 Docker 教程[12]。

代码文件:3.onnx2kmodel.py

import os\
import onnxsim\
import onnx\
import nncase\
\
\
def parse_args():\
    import argparse\
    parser = argparse.ArgumentParser(description='ONNX file to KModel')\
    parser.add_argument('-i', '--input', type=str, required=True)\
    return parser.parse_args()
    \
\
def parse_model_input_output(model_file):\
    onnx_model = onnx.load(model_file)\
    input_all = [node.name for node in onnx_model.graph.input]\
    input_initializer = [node.name for node in onnx_model.graph.initializer]\
    input_names = list(set(input_all) - set(input_initializer))\
    input_tensors = [node for node in onnx_model.graph.input if node.name in input_names]\
\
    # input\
    inputs = []\
    for _, e in enumerate(input_tensors):\
        onnx_type = e.type.tensor_type\
        input_dict = {}\
        input_dict['name'] = e.name\
        input_dict['dtype'] = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[onnx_type.elem_type]\
        input_dict['shape'] = [(i.dim_value if i.dim_value != 0 else d) for i, d in zip(\
            onnx_type.shape.dim, [1, 3, 224, 224])]\
        inputs.append(input_dict)\
\
    return onnx_model, inputs
    \
\
def onnx_simplify(model_file):\
    onnx_model, inputs = parse_model_input_output(model_file)\
    onnx_model = onnx.shape_inference.infer_shapes(onnx_model)\
    input_shapes = {}\
    for input in inputs:\
        input_shapes[input['name']] = input['shape']\
\
    onnx_model, check = onnxsim.simplify(onnx_model, input_shapes=input_shapes)\
    assert check, "Simplified ONNX model could not be validated"\
\
    model_file = os.path.join(os.path.dirname(model_file), 'simplified.onnx')\
    onnx.save_model(onnx_model, model_file)\
    return model_file\
    \
\
def read_model_file(model_file):\
    with open(model_file, 'rb') as f:\
        model_content = f.read()\
    return model_content\
\
\
def main():\
    args = parse_args()\
    model_file = args.input\
    target = 'k210'\
\
    # onnx simplify\
    model_file = onnx_simplify(model_file)\
\
    # compile_options\
    compile_options = nncase.CompileOptions()\
    compile_options.target = target\
    compile_options.dump_ir = True\
    compile_options.dump_asm = True\
    compile_options.dump_dir = 'tmp'\
\
    # compiler\
    compiler = nncase.Compiler(compile_options)\
\
    # import_options\
    import_options = nncase.ImportOptions()\
\
    # import\
    model_content = read_model_file(model_file)\
    compiler.import_onnx(model_content, import_options)\
\
    # compile\
    compiler.compile()\
\
    # kmodel\
    kmodel = compiler.gencode_tobytes()\
    name = os.path.basename(model_file).split(".")[0]\
    with open(f'{name}.kmodel', 'wb') as f:\
        f.write(kmodel)\
\
\
if __name__ == '__main__':\
    main()

运行形式:

为简化部署形式,在部署好 NNCASE 的 Docker 镜像中间接运行 Python 脚本。首先用 Docker 拉取 NNCASE 镜像,再进入到镜像中运行 Python 代码

# 拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/kendryte/nncase:latest
# 进入到容器外部
docker run -it --rm -v `pwd`:/mnt -w /mnt registry.cn-hangzhou.aliyuncs.com/kendryte/nncase:latest /bin/bash -c "/bin/bash"
# 运行
python3 3.onnx2kmodel.py -i outputs/mnist.onnx

运行后果:

1. Import graph...
2. Optimize target independent...
3. Optimize target dependent...
5. Optimize target dependent after quantization...
6. Optimize modules...
7.1. Merge module regions...
7.2. Optimize buffer fusion...
7.3. Optimize target dependent after buffer fusion...
8. Generate code...
WARN: Cannot find a decompiler for section .rdata
WARN: Cannot find a decompiler for section .text

SUMMARY
INPUTS
0       input.1 f32[1,1,28,28]
OUTPUTS
0       18      f32[1,10]

MEMORY USAGES
.input     3.06 KB      (3136 B)
.output   40.00 B       (40 B)
.data    313.00 KB      (320512 B)
MODEL      4.58 MB      (4802240 B)
TOTAL      4.89 MB      (5125928 B)

运行结束,在 outputs 文件下有 mnist.kmodel 文件,这个文件。

代码解说:

(1)首先应用 onnx-simplifier 简化 ONNX 模型

为什么要简化?这是因为在训练完深度学习的 pytorch 或者 tensorflow 模型后,有时候须要把模型转成 onnx,然而很多时候,很多节点比方 cast 节点,Identity 这些节点可能都不须要,咱们须要进行简化[11],这样会不便咱们后续在嵌入式平台部署。onnx-simplifier 的开源地址见[9]。

次要代码为

def onnx_simplify(model_file):
    onnx_model, inputs = parse_model_input_output(model_file)
    onnx_model = onnx.shape_inference.infer_shapes(onnx_model)
    input_shapes = {}
    for input in inputs:
        input_shapes[input['name']] = input['shape']

    onnx_model, check = onnxsim.simplify(onnx_model, input_shapes=input_shapes)
    assert check, "Simplified ONNX model could not be validated"

    model_file = os.path.join(os.path.dirname(model_file), 'outputs/simplified.onnx')
    onnx.save_model(onnx_model, model_file)
    return model_file

(2)应用 NNCASE 转换 ONNX 参数,外围代码为

    model_content = read_model_file(model_file)
    compiler.import_onnx(model_content, import_options)

    # compile
    compiler.compile()

    # kmodel
    kmodel = compiler.gencode_tobytes()

这一步时,离部署到嵌入式不远了,那么咱们持续来看 K210 的局部。

03 K210 开发环境

开发这个 K210 的姿态总结如下有三种:(1)应用 Micropython 固件 开发(2)应用 standalone SDK 进行开发(3)应用FreeRTOS 进行开发。

K210 是反对好几种编程环境的,从最根本的 cmake 命令行开发环境,到 IDE 开发环境,到 Python 脚本式开发环境都反对,这几种开发方式没有优劣之分,有的人喜爱用命令行 +vim,有的人喜爱 IDE 图形界面,也有的人基本不关怀编译环境,感觉人生苦短只想写 Python。

一般来说越根底的开发方式自由度越大,比方 C 语言 + 官网库,能充分发挥出芯片的各种外设性能,但同时开发难度比拟高,过程很繁琐;越顶层的开发方式比方写脚本,尽管非常地便捷,甚至连下载程序的过程都不须要了,然而程序性能的实现极度依赖于 MicroPython 的 API 更新,且很多高级零碎性能无奈应用[2]。

为升高大家在开发中的不友好度,本文介绍第一种开发方法,当前有机会能够介绍应用 C SDK 间接进行开发。不论用什么零碎都能进行 K210 的开发,咱们须要实现下列内容的筹备:

(1)将 K210 通过 USB 连入你的电脑
(2)CH340 驱动已装置
(3)cmake
(4)kflash 或 kflash GUI,一个烧录程序用以将编译好的.bin 文件烧录到硬件中。前者命令行,后者图形界面
(5)Micropython 固件

第一步 下载 Micropython 固件。到 https://dl.sipeed.com/shareUR… 下载一个 bin 文件,这里笔者应用的是 minimum_with_kmodel_v4_support

wget https://dl.sipeed.com/fileList/MAIX/MaixPy/release/master/maixpy_v0.6.2_72_g22a8555b5/maixpy_v0.6.2_72_g22a8555b5_minimum_with_kmodel_v4_support.bin

第二步 查看 K210 的串口号。以笔者应用的 MacOS 为例:

ls /dev/cu.usbserial-*
# /dev/cu.usbserial-14330

第三步 烧录。

应用命令行进行烧录示例:

kflash  -p /dev/cu.usbserial-14330 -b 115200 -t maixpy_v0.6.2_72_g22a8555b5_minimum_with_kmodel_v4_support.bin

笔者比拟懒,不想每次指定串口号,所以间接用 /dev/cu.usbserial-*。如果你电脑只有一个以 /dev/cu.usbserial 结尾的串口,那就不必指定,间接用我这种办法:

kflash  -p /dev/cu.usbserial-* -b 115200 -t *.bin

到这里,如果没有问题的话,阐明你曾经胜利在 K210 上部署了。怎么应用其进行编程呢?笔者倡议读完 [16],更多信息能够从参考 Micropython 的文档[14] 和 Github[15]。

04 硬件平台部署

KModel 制作好,接下来须要把 KModel 部署到硬件。有两种形式,上面别离介绍,任选一种即可。

办法一:将 Model 烧录到 Flash 中

关上 Kflash GUI,配置如下烧录到 0x300000 地址

办法二:将 KModel 放在 SD 卡中

间接将 KModel 放在 SD 卡中即可,留神 TF 卡须要是 MBR 分区 FAT32 格局 [17]。如果不是这个格局,是加载不到 SD 卡的。在 Mac 中,能够关上磁盘管理工具–> 抉择对应磁盘–> 抹掉–> 格局–>MS-DOS(FAT) 间接一步格式化。

最终运行
代码文件:4.k210_main.py

import sensor, lcd, image
import KPU as kpu

lcd.init(invert=True)
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.set_windowing((224, 224))  # set to 224x224 input
sensor.set_hmirror(0)  # flip camera
task = kpu.load(0x300000)  # load model from flash address 0x200000
a = kpu.set_outputs(task, 0, 10, 1, 1)
sensor.run(1)
while True:
    img = sensor.snapshot()
    lcd.display(img, oft=(0, 0))  # display large picture
    img1 = img.to_grayscale(1)  # convert to gray
    img2 = img1.resize(32, 32)  # resize to mnist input 32x32
    a = img2.strech_char(1)  # preprocessing pictures, eliminate dark corner
    lcd.display(img2, oft=(240, 32))  # display small 32x32 picture
    a = img2.pix_to_ai();  # generate data for ai
    fmap = kpu.forward(task, img2)  # run neural network model
    plist = fmap[:]  # get result (10 digit's probability)
    pmax = max(plist)  # get max probability
    max_index = plist.index(pmax)  # get the digit
    lcd.draw_string(224, 0, "%d: %.3f" % (max_index, pmax), lcd.WHITE, lcd.BLACK)

运行形式:
有多种形式,上面介绍两种,更多形式可参考:https://wiki.sipeed.com/soft/…

形式 1:rshell
正如应用 linux 终端一样,应用 rshell 的 cp 命令即可简略地复制文件到开发板

依照 rshell 我的项目主页的阐明装置好 rshell

sudo apt-get install python3-pip
sudo pip3 install rshell
rshell -p /dev/ttyUSB1 # 这里依据理论状况抉择串口

Copy

ls /flash
cp ./test.py /flash/ #复制电脑当前目录的文件 test.py 

形式 2:SD 卡主动拷贝到 Flash 文件系统

为了不便将 SD 卡的内容拷贝到 Flash 文件系统,只须要将要拷贝到 Flash 文件系统的文件重命名为 cover.boot.py 或者 cover.main.py, 而后放到 SD 卡根目录,开发板断电插入 SD 卡,而后开发板上电,程序就会主动将这两个文件拷贝到 /flash/boot.py 或者 /flash/main.py,这样就算前面取出了 SD 卡,程序曾经在 /flash/boot.py 或者 /flash/main.py 了

五、总结

本文是笔者折腾嵌入式 AI 的一篇实际,涵盖以下内容:

  • 视觉模型的数据筹备、网络设计、训练
  • ONNX 基本知识与 ONNX 的简化
  • 应用 AI 编译器 NNCASE 将 ONNX 转化为 KModel
  • Micropython + KModel 在 K210 上的部署

本文办法因为在 Micropython 上进行模型加载,在系统资源调用以及软件适配上会有许多限度。做成产品时倡议用 C +K210 SDK 开发。因为篇幅限度,下篇将摸索应用 C 语言对模型进行部署,欢送关注更新!因为集体能力无限,如有疑难以及勘误欢送和笔者进行交换:Github/QiangZiBro!

本文应用到的代码都托管在这个仓库里,大家能够自在查看:https://github.com/QiangZiBro…

参考资料(5-10 是为读者筹备的参考学习材料)
[1] : 模型部署的场景、挑战和技术计划 https://zhuanlan.zhihu.com/p/…

[2] : 嵌入式 AI 从入门到放肆【K210 篇】– 硬件与环境 https://zhuanlan.zhihu.com/p/…

[3] : encase https://github.com/kendryte/n…

[4] : How to integrate MNIST with Pytorch https://tensorbay-python-sdk….

[5] : ONNX Github https://github.com/onnx/onnx

[6] : ONNX 学习笔记 https://zhuanlan.zhihu.com/p/…

[7] : ONNX 教程 https://github.com/onnx/tutor…

[8] : ONNX 预训练 SOTA 模型 https://github.com/onnx/models

[9] : ONNX 简化器 https://github.com/daquexian/…

[10] : nncase Github https://github.com/kendryte/n…

[11] : https://mp.weixin.qq.com/s/OT…

[12] : https://github.com/QiangZiBro…

[13] : 训练好的深度学习模型原来这样部署的!https://mp.weixin.qq.com/s/tq…

[14] : https://wiki.sipeed.com/soft/…

[15] : https://github.com/sipeed/MaixPy

[16] : 编辑并执行文件 https://wiki.sipeed.com/soft/…

[17] : https://wiki.sipeed.com/soft/…

[18] : https://wiki.sipeed.com/soft/…

更多信息请拜访格物钛官网

退出移动版