技術部落格丨我用深度學習做個視覺AI微型處理器!

格物鈦Graviti發表於2022-01-11

作者:張強,Datawhale成員 

講多了演算法,如何真正將演算法應用到產品領域?本文將帶你從0用深度學習打造一個視覺AI的微型處理器。文章含完整程式碼,知識點相對獨立,歡迎點贊收藏,跟著本文做完,你也可以做一個自己的嵌入式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] 。

技術部落格丨我用深度學習做個視覺AI微型處理器!

市面上有許多搭載K210的開發板,筆者這裡選了雅博一款功能較全的K210開發板,開始了嵌入式AI的折騰之路。 

技術部落格丨我用深度學習做個視覺AI微型處理器!

對於第二個問題,方法就多了,不同深度學習框架,不同硬體選型都決定著不同技術路線。基本路線可以為深度學習平臺訓練 -> 模型剪枝、量化 -> 引數轉換 ->轉換為硬體平臺上能執行的模型

對深度學習平臺選型,筆者決定選用當下最流行的Pytorch平臺。最後一步往往取決於這個硬體的生態,如果沒有相關生態支援,可能需要手寫C語言程式碼載入引數執行。調研發現,K210有一個深度網路最佳化平臺NNCASE,能加速深度模型部署。

調研過程中發現在這塊板子上部署模型大多數都是從Keras、TensorFlow開始訓練並最終部署,而研究者常用的Pytorch竟然沒有教程,於是今天就嘗試來講一講。

接下來,我們將從使用Pytorch訓練手寫體識別的例子開始,打通從訓練到嵌入式平臺部署的流程。

01 使用Pytorch訓練分類網路模型

必要軟體包安裝

pip install tensorbay pillow torch torchvision num

資料集獲取

一個AccessKey獲取所有資料集。

我們使用一個開源資料集平臺:https://gas.graviti.com ,這個網站彙總了AI開發者常見的公開資料集,呼叫其SDK就能直接線上訓練,而且許多資料集直接在國內網路下連線直接使用,還是非常方便的。

a. 開啟本文對應資料集連結 https://gas.graviti.com/dataset/data-decorators/MNIST

b. 右上角註冊登入

c. fork資料集

技術部落格丨我用深度學習做個視覺AI微型處理器!

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/shareURL/MAIX/MaixPy/release/master 下載一個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地址

技術部落格丨我用深度學習做個視覺AI微型處理器!

方法二:將KModel放在SD卡中

直接將KModel放在SD卡中即可,注意TF卡需要是MBR 分割槽 FAT32 格式[17]。如果不是這個格式,是載入不到SD卡的。在Mac中,可以開啟磁碟管理工具–>選擇對應磁碟–>抹掉–>格式–>MS-DOS(FAT)直接一步格式化。

技術部落格丨我用深度學習做個視覺AI微型處理器!

最終執行

程式碼檔案: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/maixpy/zh/get_started/get_started_upload_script.html

方式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

看看部署到K210的識別效果:

技術部落格丨我用深度學習做個視覺AI微型處理器!

五、總結

本文是筆者折騰嵌入式AI的一篇實踐,涵蓋以下內容:

  • 視覺模型的資料準備、網路設計、訓練
  • ONNX基本知識與ONNX的簡化
  • 使用AI編譯器NNCASE將ONNX轉化為KModel
  • Micropython + KModel在K210上的部署

本文方法由於在Micropython上進行模型載入,在系統資源呼叫以及軟體適配上會有許多限制。做成產品時建議用C+K210 SDK開發。由於篇幅限制,下篇將探索使用C語言對模型進行部署,歡迎關注更新!由於個人能力有限,如有疑問以及勘誤歡迎和筆者進行交流:Github/QiangZiBro!

本文使用到的程式碼都託管在這個倉庫裡,大家可以自由檢視:https://github.com/QiangZiBro/pytorch-k210.git

參考資料(5-10是為讀者準備的參考學習資料)

[ 1 ] : 模型部署的場景、挑戰和技術方案 https://zhuanlan.zhihu.com/p/387575188

[ 2 ] : 嵌入式AI從入門到放肆【K210篇】-- 硬體與環境 https://zhuanlan.zhihu.com/p/81969854

[ 3 ] : encase https://github.com/kendryte/nncase

[ 4 ] : How to integrate MNIST with Pytorch    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


更多資訊請訪問格物鈦官網

相關文章