多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

K230 NPU部署实战:从ResNet34训练到量化推理全流程

K230 NPU部署实战:从ResNet34训练到量化推理全流程 1. 为什么要在K230上折腾AI部署这件事第一次拿到K230开发板的时候我脑子里想的其实很简单跑个MobileNet分类demo看看NPU到底有多快。结果demo跑通了心里反而更痒——因为demo用的是官方预编译好的模型跟我自己训练出来的东西完全是两码事。真正把自己训的模型塞进这块板子跑起来中间隔着的坑比我想象的多得多。K230是嘉楠科技推出的边缘AI芯片核心卖点就是内置了自研的NPU神经网络处理单元专门用来加速神经网络推理。它跟纯CPU跑推理完全不是一个概念——CPU靠通用算力硬扛矩阵运算NPU则是把卷积、池化这些操作做成了硬件电路能效比高出一大截。这也是为什么现在端侧AI硬件部署这么火不是所有场景都能把数据传到云端延迟、带宽、隐私随便哪一条都足以让本地推理成为刚需。这篇内容适合谁看如果你手上有K230已经跑通了官方demo想把自己训练的模型比如ResNet34、YOLO系列部署上去或者你正在选型端侧AI硬件想搞清楚从训练到部署这条链路到底长什么样那这篇就是写给你的。我会把整个流程拆开讲模型怎么训、怎么转、怎么量化、怎么在板子上跑起来以及每一步我踩过的坑。先说一个反直觉的结论在K230上部署AI训练反而是最简单的一步。真正耗时间的是模型转换和量化调优尤其是当你发现量化后的精度掉得离谱时那种感觉就像考试前背了一整本书结果考的全是没背的。2. K230的NPU到底能吃什么、不能吃什么2.1 NPU和CPU、GPU的分工逻辑很多人第一次接触NPU会把它当成更快的CPU这个理解是错的。CPU擅长逻辑控制和串行任务GPU擅长大规模并行浮点运算而NPU是专门为神经网络推理设计的——它把卷积、激活、池化这些操作固化成了硬件流水线。你可以这么理解CPU是瑞士军刀什么都能干但都不精GPU是一把大砍刀砍什么都快但费电NPU则是一台专用冲床只干一件事但干得又快又省电。K230的NPU算力标称是6TOPSINT8这个数字在端侧芯片里算中上水平。但要注意算力数字好看不代表你的模型就能跑得快因为NPU有自己支持的算子列表不在列表里的算子会回退到CPU执行一回退速度就断崖式下跌。2.2 算子支持清单部署前必须核对的事我在部署ResNet34的时候第一版模型里用了AdaptiveAvgPool2d结果转换工具直接报错说不支持。后来换成固定尺寸的AvgPool2d才通过。这件事给我的教训是训练模型的时候就要考虑部署端的算子支持而不是训完了再想办法。K230的NPU通过KModel格式加载模型转换工具是nncase。nncase支持的算子覆盖了常见的卷积、深度可分离卷积、全连接、BN、ReLU、ReLU6、Sigmoid、Softmax、MaxPool、AvgPool、Concat、Add等。但像AdaptiveAvgPool、某些变体的Upsample、自定义算子就不在支持范围内。实操建议训练前先翻一遍nncase的算子支持文档把模型结构里可能不支持的层替换掉。比如分类网络最后的全局池化直接用AvgPool2d(kernel_size7)替代AdaptiveAvgPool2d(1)前提是你的输入尺寸固定。2.3 输入尺寸和通道数的硬约束K230的NPU对输入张量有格式要求通常是NCHW或者NHWC具体取决于你用的转换配置。输入尺寸方面虽然没有严格的必须是224这种限制但尺寸最好是32的倍数因为NPU的卷积单元按块处理非对齐的尺寸会导致padding浪费算力。通道数方面第一层卷积的输入通道如果是3RGBNPU处理起来没问题。但如果你用的是灰度图1通道建议在预处理阶段就复制成3通道避免NPU走特殊路径。3. 从ResNet34开始训练阶段就要为部署埋好伏笔3.1 数据集准备和标注的坑我这次用的数据集是自己采集的工业零件缺陷图大概8000张分5类。标注工具用的是LabelImg导出YOLO格式。这里有个细节如果你的任务同时涉及检测和分类建议先做检测框标注再根据框裁剪出分类数据集这样两套数据同源后面部署的时候可以串成一个pipeline。数据增强方面训练阶段我用了RandomResizedCrop、ColorJitter和水平翻转。但要注意部署时的预处理必须和训练时的验证预处理一致。训练时验证集用的是ResizeCenterCrop部署时如果忘了CenterCrop精度会掉几个点。这个坑我在第一次部署时踩过模型在PC上验证准确率92%部署到板子上只有85%查了半天才发现是预处理不一致。3.2 模型结构的选择与修改ResNet34是个很稳的backbone34层参数量大概21M浮点模型大小约85MB。这个尺寸对K230来说偏大因为板子的内存和Flash都有限。所以我在训练阶段就做了两件事第一把最后的全连接层从512维降到128维因为我的类别只有5类没必要用那么大的特征维度。第二把第一层7x7卷积改成3x3卷积减少计算量。这两处修改让模型参数量降到了18M左右精度只掉了0.3%。训练超参方面我用的是SGDmomentum 0.9weight decay 1e-4初始学习率0.01cosine退火训练了120个epoch。batch size 64在单张RTX 3060上大概跑了4个小时。3.3 训练完成后的第一件事导出ONNXPyTorch训练完的模型是.pth格式K230的转换工具不认需要先导出成ONNX。导出的时候有几个关键点import torch import torch.onnx model ResNet34Modified(num_classes5) model.load_state_dict(torch.load(best.pth)) model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, resnet34_k230.onnx, opset_version11, input_names[input], output_names[output], dynamic_axesNone # K230不支持动态shape必须固定 )注意opset_version建议用11太高或太低都可能遇到算子不支持的问题。dynamic_axes必须设为None因为K230的NPU只支持静态shape。导出后一定要用onnxruntime验证一下ONNX模型的输出和PyTorch是否一致误差在1e-4以内才算正常。我遇到过导出后输出全乱的情况原因是模型里有torch.nn.functional.interpolate导出时被转成了不支持的算子。4. 模型转换与量化精度掉点的重灾区4.1 nncase转换流程拆解nncase是K230官方的模型转换工具支持从ONNX、TFLite转到KModel。整个流程分两步先编译成kmodel再量化。但实际操作中我建议直接用nncase的Python API把编译和量化串起来。安装nncasepip install nncase pip install nncase-kpu转换脚本的核心逻辑import nncase import numpy as np # 加载ONNX with open(resnet34_k230.onnx, rb) as f: model_content f.read() # 编译配置 compile_options nncase.CompileOptions() compile_options.target k230 compile_options.input_shape [1, 3, 224, 224] compile_options.input_type uint8 # 输入是量化后的uint8 compile_options.input_range [0, 255] compile_options.mean [0.485, 0.456, 0.406] compile_options.std [0.229, 0.224, 0.225] compile_options.input_layout NCHW compile_options.output_layout NCHW # 创建编译器 compiler nncase.Compiler(compile_options) compiler.import_onnx(model_content) # 量化配置 ptq_options nncase.PTQTensorOptions() ptq_options.samples_count 100 # 校准集数量 ptq_options.set_tensor_data(calib_data) # 校准数据 # 编译 compiler.use_ptq(ptq_options) compiler.compile() kmodel compiler.gencode() with open(resnet34_k230.kmodel, wb) as f: f.write(kmodel)4.2 校准集的选择量化精度的命门PTQ训练后量化的精度高度依赖校准集。校准集的作用是让量化工具统计每一层激活值的分布从而确定量化参数scale和zero_point。如果校准集不能代表真实数据分布量化后的精度就会崩。我的经验是校准集从训练集里随机抽100到200张但要保证每个类别都有。如果某个类别样本特别少可以适当过采样。另外校准集的预处理必须和推理时完全一致包括归一化参数。有一次我偷懒直接用验证集当校准集结果验证集里有一类样本特别多量化后那一类的精度很高其他类惨不忍睹。后来改成按类别均匀采样整体精度才恢复正常。4.3 量化精度掉点的排查思路量化后精度掉点是常态关键是怎么定位问题。我的排查顺序是这样的第一步对比浮点模型和量化模型在PC上的输出。用nncase的模拟器跑量化模型输入同一张图看输出差异。如果PC上模拟器精度就掉了说明量化本身有问题如果模拟器精度正常板子上掉那就是部署环节的问题。第二步如果量化本身有问题先检查校准集。把校准集数量从100增加到500看精度是否回升。如果回升明显说明校准集不够代表性。第三步如果增加校准集没用考虑混合量化。nncase支持对特定层保持浮点比如第一层和最后一层。这两层对精度影响最大保持浮点能显著缓解掉点。第四步如果还不行就得回训练阶段做QAT量化感知训练。QAT在训练时模拟量化误差让模型适应量化后的数值分布。代价是训练时间翻倍但精度通常能恢复到浮点模型的99%以上。5. 板端部署从KModel到实际推理5.1 K230开发环境搭建K230的板端开发有两种方式一是用官方SDK编译固件二是用MicroPython直接调用NPU接口。我推荐先用MicroPython快速验证因为编译固件太耗时而且调试不方便。MicroPython的固件里已经集成了nncase的运行时和image模块。烧录固件后通过串口或者IDE连接板子就可以直接跑推理脚本。板子上的文件系统里/sdcard或者/flash用来放kmodel和测试图片。我一般把kmodel放在/flash/models/下测试图片放在/flash/images/。5.2 推理脚本的完整实现下面是一个完整的MicroPython推理脚本跑ResNet34分类import sensor import image import lcd import nncase import time from machine import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240 sensor.run(1) # 初始化LCD lcd.init() # 加载kmodel kmodel_path /flash/models/resnet34_k230.kmodel with open(kmodel_path, rb) as f: kmodel f.read() # 创建推理引擎 interp nncase.Interpreter(kmodel, 1) # 1个输入tensor # 类别标签 labels [class0, class1, class2, class3, class4] while True: img sensor.snapshot() # 预处理resize到224x224转RGB888 img_resize img.resize(224, 224) img_rgb img_resize.to_rgb888() # 转成numpy数组注意K230的image对象转numpy的方式 input_data img_rgb.to_numpy_ref() # 设置输入 interp.set_input_tensor(0, input_data) # 推理 start time.ticks_ms() interp.run() end time.ticks_ms() # 获取输出 output interp.get_output_tensor(0) # 找最大值 max_idx 0 max_val output[0] for i in range(1, len(output)): if output[i] max_val: max_val output[i] max_idx i # 显示结果 img.draw_string(10, 10, labels[max_idx], color(255, 0, 0), scale2) img.draw_string(10, 40, {}ms.format(end - start), color(0, 255, 0), scale2) lcd.display(img)这段代码里有个细节to_numpy_ref()返回的是引用不是拷贝所以速度很快。但要注意如果后面修改了img对象input_data也会变。推理前确保img不再被修改。5.3 串口通信与结果回传实际项目里K230通常不是孤立运行的需要把推理结果传给主控或者上位机。串口是最常用的方式。K230的MicroPython里UART的使用如下uart UART(1, baudrate115200, txPin(3), rxPin(4)) # 发送结果 result_str {},{:.2f}\n.format(labels[max_idx], max_val) uart.write(result_str)注意串口的TX和RX引脚要跟你的硬件连接对应。K230的引脚定义在官方文档里有不同板子可能不一样。我用的这块板子UART1的TX是GPIO3RX是GPIO4。如果数据量比较大比如要传检测框坐标建议用二进制协议而不是字符串减少传输开销。我一般用struct.pack打包成二进制上位机再用struct.unpack解包。6. 实测性能与优化空间6.1 推理耗时拆解ResNet34量化后在K230上的实测推理耗时大概是45ms左右其中NPU计算占30ms预处理resize格式转换占10ms后处理找最大值占5ms。这个速度对于分类任务够用了但如果要做实时检测比如YOLO就得进一步优化。优化方向有几个一是降低输入分辨率从224降到160推理时间能降到25ms左右精度掉2个点二是把预处理放到NPU里做nncase支持把resize和归一化融合进模型这样能省掉10ms的CPU预处理时间三是用双缓冲一边推理一边采集下一帧把采集和推理重叠起来。6.2 内存和功耗的平衡K230的内存不大跑ResNet34的时候kmodel加载后占大概20MB加上摄像头缓冲和LCD缓冲内存占用在60%左右。如果模型再大比如ResNet50就可能OOM。这时候要么换更小的模型要么用外部Flash做内存映射但后者会拖慢加载速度。功耗方面K230满载推理时大概1.5W左右待机0.3W。如果是电池供电的场景建议加个策略没有检测到目标时降频或者休眠检测到再唤醒。这个在MicroPython里可以用machine.freq()调频实现。6.3 从分类到检测YOLO部署的额外坑如果你要部署YOLO除了上面说的算子支持问题还有两个坑一是NMS非极大值抑制通常不在NPU里做得在CPU上实现这会增加后处理时间二是YOLO的输出解码把anchor偏移转成实际坐标也得在CPU上做。这两步加起来可能比NPU推理还耗时。我的做法是把NMS和解码用C写编译成MicroPython的native module比纯Python快5倍以上。如果不想写C那就尽量简化NMS比如用单类别NMS或者把置信度阈值调高减少候选框数量。7. 一些让我少走弯路的经验第一训练前先确认算子支持。别等训完了才发现模型转不过去那时候改结构就得重新训时间成本太高。第二校准集要按类别均匀采样。别偷懒直接用验证集验证集的分布往往和真实场景不一致。第三预处理必须和训练时一致。归一化参数、resize方式、通道顺序任何一个不一致都会掉精度。第四量化掉点先别急着上QAT。先检查校准集和混合量化这两招能解决80%的掉点问题。QAT是最后的手段因为训练成本太高。第五板端调试用MicroPython别一上来就编译固件。MicroPython改一行跑一行固件编译一次十分钟调试效率差太多。第六串口通信加校验。工业环境里串口干扰很常见不加校验偶尔会收到乱码。我一般用CRC16虽然多几个字节但稳定。第七留出温度余量。K230长时间满载会发热温度高了NPU会降频。如果做产品散热片或者小风扇得提前规划。最后说个我自己的体会端侧AI部署这件事难点不在AI在工程。模型训练现在有太多工具可以帮你但把模型塞进一块资源受限的板子里跑起来需要你对硬件、对工具链、对系统都有理解。K230是个很好的练手平台因为它足够开放也足够有挑战。把ResNet34跑通之后再上YOLO、上分割路就顺了。
返回列表