作者申明:该文章仅在微信平台授权给公众号Datawhale(ID:Datawhale)首发原创(地址:https://mp.weixin.qq.com/s/oTLPzmlChxoXDziurl38-Q ),其他平台暂无授权。
如何将深度学习算法部署到嵌入式设备?本文将带给你一个从Pytorch平台训练,到部署到嵌入式设备K210的完整方案。章节之间跨度大,知识点相对独立,欢迎点赞收藏慢慢看!跟着本文做完,你也可以做一个自己的嵌入式AI小产品!
作者介绍:张强,DataWhale成员,宁波大学研究生,研究方向计算机视觉与深度学习
零、前言
随着硬件尤其是显卡性能升级,以及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训练手写体识别的例子开始,打通从训练到嵌入式平台部署的流程。
一、使用Pytorch训练分类网络模型
必要软件包安装
1 pip install tensorbay pillow torch torchvision numpy
数据集获取
一个AccessKey获取所有数据集
我们使用一个开源数据集平台:https://gas.graviti.cn/,这个网站汇总了AI开发者常见的公开数据集,调用其SDK就能直接在线训练,更方便的是,许多数据集直接在国内网络下连接直接使用,不需要到国外网站进行下载,还是非常方便的!
打开本文对应数据集链接 https://gas.graviti.cn/dataset/qiangzibro/MNIST
右上角注册登录
fork数据集
点击网页上方开发者工具,获取使用SDK所需的AccessKey,获取到 AccessKey 后,将其存在项目根目录的gas_key.py
里:
然后即可以通过AccessKey可以上传数据、读取数据、使用数据,灵活对接模型开发和训练,与数据pipeline快速集成。
AccessKey写入后就可以写代码读取数据了,读完了一行下载代码就可以进行下载。将下载后的数据放在data
文件夹下
获取到AccessKey后,我们便能自由地操作数据了:
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 import numpy as npfrom PIL import Imagefrom tensorbay import GASfrom tensorbay.dataset import Datasetfrom tensorbay.dataset import Segmentdef read_gas_image (data ): with data.open () as fp: image = Image.open (fp) return np.array(image) KEY = "用你的Key替换掉这个字符串" gas = GAS(KEY) dataset = Dataset("MNIST" , gas) segments = dataset.keys() 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],其支持三个平台的算子:
打开对应平台,能够到具体有哪些算子已经实现了低层优化。可以看到对ONNX算子优化还是比较多的。如果所选用的网络算子较新,抑或是模型太大,都要在本步多加思考与设计。
如果如果最后部署不成功,往往需要回到这一步考虑网络的设计。为了尽可能减少算子的使用,本文设计一个只基于卷积+ReLU+Pool的CNN:
代码文件名: models/net.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 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。
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 from __future__ import print_functionimport argparseimport torchimport torch.nn.functional as Fimport torch.optim as optimfrom torch.optim.lr_scheduler import StepLRfrom models.net import Netfrom PIL import Imagefrom torch.utils.data import DataLoader, Datasetfrom torchvision import transformsfrom tensorbay import GASfrom tensorbay.dataset import Dataset as TensorBayDatasetclass 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() pred = output.argmax(dim=1 , keepdim=True ) 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 (): 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()
运行方式:
打开终端
1 2 mkdir -p outputs python 1.train.py --save-model
运行结果:
1 2 3 Train Epoch: 1 [0/60000 (0%)] Loss: 2.305400 Train Epoch: 1 [640/60000 (1%)] Loss: 1.359776 ....
训练完毕,在outputs文件下有mnist.pt
模型文件。
二、参数转换
Pytorch模型导出为ONNX
“各家都有一套语言,换到一个标准说话”
开放神经网络交换(Open Neural Network Exchange)简称ONNX是微软和Facebook提出用来表示深度学习模型的开放 格式。所谓开放就是ONNX定义了一组和环境,平台均无关的标准格式,来增强各种AI模型的可交互性。
换句话说,无论你使用何种训练框架训练模型(比如TensorFlow/Pytorch/OneFlow/Paddle),在训练完毕后你都可以将这些框架的模型统一转换为ONNX这种统一的格式进行存储。注意ONNX文件不仅仅存储了神经网络模型的权重,同时也存储了模型的结构信息以及网络中每一层的输入输出和一些其它的辅助信息。
ONNX开源在了[5],笔者为大家准备了一些参考学习资料,可参考[5-10]进行学习。
Pytorch平台训练好模型后,使用下列脚本将模型转化为ONNX
首先安装用到的包
1 pip install onnx coremltools onnx-simplifier
脚本名称: 2.pt2onnx.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 from models.net import Netimport torch.onnximport osdef 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()
运行方式:
1 python3 2.pt2onnx.py -i outputs/mnist.pt
运行结果:
1 2 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
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 import osimport onnxsimimport onnximport nncasedef 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] 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' model_file = onnx_simplify(model_file) compile_options = nncase.CompileOptions() compile_options.target = target compile_options.dump_ir = True compile_options.dump_asm = True compile_options.dump_dir = 'tmp' compiler = nncase.Compiler(compile_options) import_options = nncase.ImportOptions() model_content = read_model_file(model_file) compiler.import_onnx(model_content, import_options) compiler.compile () 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代码
1 2 3 4 5 6 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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]。
主要代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 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参数,核心代码为
1 2 3 4 5 6 7 8 model_content = read_model_file(model_file) compiler.import_onnx(model_content, import_options) compiler.compile () kmodel = compiler.gencode_tobytes()
这一步时,离部署到嵌入式不远了,那么我们继续来看K210的部分。
三、K210开发环境
开发这个K210的姿势总结如下有三种:(1)使用Micropython固件 开发 (2)使用standalone SDK 进行开发 (3)使用FreeRTOS 进行开发。
K210是支持好几种编程环境的,从最基本的cmake命令行开发环境
,到IDE开发环境
,到Python脚本式
开发环境都支持,这几种开发方式没有优劣之分,有的人喜欢用命令行+vim,有的人喜欢IDE图形界面,也有的人根本不关心编译环境觉得人生苦短只想写Python。
一般来说越基础的开发方式比如C语言+官方库会自由度越大,能充分发挥出芯片的各种外设功能,但是开发难度比较高,过程很繁琐;越顶层的开发方式比如写脚本,虽然十分地便捷,甚至连下载程序的过程都不需要了,但是程序功能的实现极度依赖于MicroPython的API更新,且很多高级系统功能无法使用[2]。
为降低大家在开发中的不友好度,本文介绍第一种开发方法,以后有机会可以介绍使用C SDK直接进行开发。不管用什么系统都能进行K210的开发,我们需要完成下列内容的准备:
(0)将K210通过USB连入你的电脑
(1)CH340驱动已安装
(2)cmake
(3)kflash或kflash GUI,一个烧录程序用以将编译好的.bin文件烧录到硬件中。前者命令行,后者图形界面
(4)Micropython固件
第一步 下载Micropython固件 到https://dl.sipeed.com/shareURL/MAIX/MaixPy/release/master 下载一个bin文件,这里笔者使用的是minimum_with_kmodel_v4_support
1 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为例:
第三步 烧录
使用命令行进行烧录示例
1 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
开头的串口,那就不用指定,直接用我这种方法:
1 kflash -p /dev/cu.usbserial-* -b 115200 -t *.bin
到这里,如果没有问题的话,说明你已经成功在K210上部署了。怎么使用其进行编程呢?笔者建议读完[16],更多信息可以从参考Micropython的文档[14]和Github[15]。
四、硬件平台部署
KModel制作好,接下来需要把KModel部署到硬件 。有两种方式,下面分别介绍,任选一种即可。
方法一:将Model烧录到Flash中
打开Kflash GUI,配置如下烧录到0x300000地址
方法二:将KModel放在SD卡中
直接将KModel放在SD卡中即可,注意TF卡需要是MBR 分区 FAT32 格式[17]。如果不是这个格式,是加载不到SD卡的。在Mac中,可以打开磁盘管理工具–>选择对应磁盘–>抹掉–>格式–>MS-DOS(FAT)直接一步格式化。
最终运行
代码文件:4.k210_main.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 import sensor, lcd, imageimport KPU as kpulcd.init(invert=True ) sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_windowing((224 , 224 )) sensor.set_hmirror(0 ) task = kpu.load(0x300000 ) a = kpu.set_outputs(task, 0 , 10 , 1 , 1 ) sensor.run(1 ) while True : img = sensor.snapshot() lcd.display(img, oft=(0 , 0 )) img1 = img.to_grayscale(1 ) img2 = img1.resize(32 , 32 ) a = img2.strech_char(1 ) lcd.display(img2, oft=(240 , 32 )) a = img2.pix_to_ai(); fmap = kpu.forward(task, img2) plist = fmap[:] pmax = max (plist) max_index = plist.index(pmax) lcd.draw_string(224 , 0 , "%d: %.3f" % (max_index, pmax), lcd.WHITE, lcd.BLACK)
运行方式:
有多种方式,下面介绍两种,更多方式从参考https://wiki.sipeed.com/soft/maixpy/zh/get_started/get_started_upload_script.html
方式1:rshell
正如使用 linux
终端一样, 使用 rshell 的 cp
命令即可简单地复制文件到开发板
按照 rshell
项目主页的说明安装好 rshell
1 2 3 sudo apt-get install python3-pip sudo pip3 install rshell rshell -p /dev/ttyUSB1
Copy
1 2 ls /flash cp ./test.py /flash/
方式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
了
看看部署到K210的识别效果:
五、总结
本文是笔者折腾嵌入式AI的一篇实践,涵盖以下内容:
视觉模型的数据准备、网络设计、训练
ONNX基本知识与ONNX的简化
使用AI编译器NNCASE将ONNX转化为KModel
Micropython + KModel在K210上的部署
本文方法由于在Micropython上进行模型加载,在系统资源调用以及软件适配上会有许多限制。做成产品时建议用C+K210 SDK开发。由于篇幅限制,下篇将探索使用C语言对模型进行部署,欢迎关注更新!由于个人能力有限,如有疑问以及勘误欢迎和笔者进行交流:Github/QiangZiBro!
本文使用到的代码都托管在这个仓库里:
https://github.com/QiangZiBro/pytorch-k210.git
参考资料
[ 1 ] : 模型部署的场景、挑战和技术方案 https://zhuanlan.zhihu.com/p/387575188
[ 2 ] : 嵌入式AI从入门到放肆【K210篇】-- 硬件与环境 https://zhuanlan.zhihu.com/p/81969854
[ 3 ] : nncase https://github.com/kendryte/nncase
[ 4 ] : tensorbay mnist https://tensorbay-python-sdk.graviti.com/en/stable/integrations/pytorch.html
[ 5 ] : ONNX Github https://github.com/onnx/onnx
[ 6 ] : ONNX学习笔记 https://zhuanlan.zhihu.com/p/346511883
[ 7 ] : ONNX 教程 https://github.com/onnx/tutorials
[ 8 ] : ONNX 预训练SOTA模型 https://github.com/onnx/models
[ 9 ] : ONNX简化器 https://github.com/daquexian/onnx-simplifier
[ 10 ] : nncase Github https://github.com/kendryte/nncase
[ 11 ] : https://mp.weixin.qq.com/s/OTUSDSqGTgTJ6-KA-o0rQw
[ 12 ] : https://github.com/QiangZiBro/learn-docker-in-a-smart-way
[ 13 ] : 训练好的深度学习模型原来这样部署的! https://mp.weixin.qq.com/s/tqSmFcR-aQjDhaEyQBzeUA
[ 14 ] : https://wiki.sipeed.com/soft/maixpy/zh/index.html
[ 15 ] : https://github.com/sipeed/MaixPy
[ 16 ] : 编辑并执行文件 https://wiki.sipeed.com/soft/maixpy/zh/get_started/get_started_edit_file.html
[ 17 ] : https://wiki.sipeed.com/soft/maixpy/zh/others/maixpy_faq.html#Micro-SD-%E5%8D%A1%E8%AF%BB%E5%8F%96%E4%B8%8D%E5%88%B0
[ 18 ] : https://wiki.sipeed.com/soft/maixpy/zh/get_started/get_started_upload_script.html