1. 从零上手K210模型训练:一个嵌入式视觉玩家的踩坑实录
K210这颗芯片在嵌入式视觉圈子里火了挺长一段时间了,双核RISC-V加KPU的设计,让它在一众MCU里显得格外能打。我手上这块开发板是某款基于K210的经典板型,板载摄像头、LCD屏、麦克风阵列,官方定位是AI视觉和语音开发平台。买回来吃灰了小半年,最近终于抽时间把模型训练这条链路完整跑了一遍。这篇文章不聊虚的,就说说怎么从零训练一个能跑在K210上的模型,中间会遇到哪些坑,以及我是怎么一步步趟过来的。
先说清楚这篇文章适合谁看。如果你手上正好有一块K210开发板,想跑自己训练的模型而不是只用官方例程,那这篇笔记基本能帮你省下至少两三个晚上的折腾时间。如果你还没入手但正在观望,也可以先看看整个流程的复杂度,评估一下投入产出比。我会从数据采集、标注、训练环境搭建、模型转换、部署验证这几个环节逐一展开,每个环节都附上我实际操作中遇到的问题和解决思路。
需要提前说明的是,K210的KPU对模型的支持有比较明确的限制,不是随便拿个YOLOv5或者MobileNet就能直接往上扔的。你得用特定版本的训练框架,走特定的量化流程,最后转成kmodel格式才能被KPU识别。这个链路里每一步都有坑,而且坑和坑之间是串联的——前面一步没做对,后面直接报错,排查起来很头疼。所以我会尽量把每个环节的关键参数和注意事项讲透,让你少走弯路。
2. 训练前的整体思路与方案选型
2.1 为什么不能直接用常规深度学习那一套
K210的KPU本质上是一个定点运算加速器,它不支持浮点运算,也不支持动态计算图。这意味着你拿PyTorch或者TensorFlow训练出来的浮点模型,不能直接部署上去。必须经过量化——把浮点权重和激活值转成定点整数,通常是int8。这个过程会带来精度损失,但K210的KPU在设计上对量化做了一些优化,实际跑起来效果还能接受。
另一个限制是算子支持。KPU只支持有限的算子集合,主要是卷积、深度可分离卷积、池化、全连接这些。像一些自定义的激活函数、复杂的注意力机制、动态shape操作,统统不支持。所以你在选模型架构的时候,就得从KPU支持的算子里挑,不能随心所欲。
我一开始想用MobileNetV2做骨干,因为它在分类任务上表现不错,参数量也小。但实际转换的时候发现,K210的量化工具对某些版本的MobileNetV2支持不好,特别是涉及倒残差结构里的特定激活函数时,转换会失败。后来换成了更简单的自定义小网络,反而顺利跑通了。这个经历告诉我,在K210上做模型训练,架构选择的第一原则不是精度最高,而是兼容性最好。
2.2 官方工具链与社区方案的取舍
K210的模型训练主要有两条路可走。一条是官方提供的在线训练平台,上传数据集,选模型,等结果,下载kmodel。这条路适合完全不想碰代码的人,但灵活性很差,支持的模型架构有限,而且数据集大小有上限。另一条是本地训练,用官方提供的nncase工具链做转换。这条路折腾,但可控性强,能自己调参、自己改网络结构。
我选的是本地训练这条路。原因很简单:我想跑一个自定义的检测任务,官方在线平台不支持。本地训练的核心工具是nncase,这是K210官方的神经网络编译器,负责把训练好的模型转成kmodel。nncase有Python版本和命令行版本,我用的是Python版本,方便集成到训练脚本里。
训练框架方面,官方推荐用TensorFlow 1.x或者Keras。我试过用TensorFlow 2.x,但nncase对TF2的支持不太稳定,经常报一些奇怪的错误。后来老老实实降级到TF1.15,配合Keras 2.2.4,整个流程就顺畅多了。这里提醒一句:如果你打算走本地训练这条路,环境版本一定要严格按照官方文档来,不要自作主张升级。
2.3 数据集准备的核心原则
数据集这块,我的建议是宁缺毋滥。K210的KPU算力有限,模型容量也小,你喂给它太多复杂数据,它学不动,反而容易过拟合。我一开始收集了大概两千张图片,分五类,结果训练出来的模型在测试集上表现很差。后来精简到八百张,分三类,每类两百多张,效果反而好很多。
数据采集的时候要注意光照和角度的多样性。K210通常搭配的摄像头是OV2640或者OV5640,成像质量一般,对光照变化比较敏感。如果你训练数据全是在室内白光下拍的,拿到室外阳光下跑,识别率会断崖式下跌。我的做法是每个类别都在不同时间段、不同光照条件下拍,尽量覆盖实际使用场景。
标注工具我用的是LabelImg,导出YOLO格式的txt标注文件。这里有个细节:K210的检测模型训练通常需要VOC格式或者YOLO格式,但nncase转换的时候对标注格式有具体要求。我建议直接用YOLO格式,因为后续转换脚本对YOLO格式的支持最完善。标注的时候边界框要贴紧目标,不要留太多背景,否则量化后精度损失会更明显。
3. 核心细节解析与实操要点
3.1 模型架构设计的约束与技巧
在K210上跑模型,架构设计要遵循几个硬约束。第一,输入分辨率不能太大。KPU的内存有限,输入分辨率太高会导致中间特征图放不下。我实测下来,224x224或者320x240是比较稳妥的选择。第二,网络深度不能太深。层数太多,量化误差会累积,而且推理速度会明显下降。我用的网络大概十几层,推理一帧大概几十毫秒,勉强能满足实时性要求。
第三,激活函数优先选ReLU或者ReLU6。KPU对这两个激活函数的量化支持最好,换成LeakyReLU或者Swish,转换的时候可能会报错。第四,尽量避免使用BatchNorm。虽然BatchNorm在训练时能加速收敛,但推理时它会被折叠进卷积层,如果折叠逻辑没处理好,量化后精度会掉得很厉害。我的做法是训练时用BatchNorm,导出模型前把它融合进卷积,nncase有现成的融合工具。
还有一个容易被忽略的点:权重的初始化方式。K210的量化是逐层进行的,如果某一层的权重分布太集中或者太分散,量化后的精度损失会很大。我习惯用He初始化,配合较小的学习率,让权重分布尽量均匀。这个经验是从多次失败中总结出来的,不一定有理论保证,但实测有效。
3.2 量化流程的详细拆解
量化是K210模型训练里最核心也最容易出问题的环节。nncase的量化流程大致分三步:首先是校准,用一批代表性数据跑一遍浮点模型,统计每层激活值的分布范围;然后是量化,根据统计结果把浮点权重和激活值映射到int8;最后是验证,用测试集跑量化后的模型,看精度损失是否在可接受范围内。
校准数据的选取很关键。官方文档说用几百张图片就够了,但我实测下来,校准数据最好和训练数据同分布,而且数量不能太少。我一开始只用了五十张校准图,结果量化后模型几乎不可用。后来加到三百张,覆盖所有类别和光照条件,量化后的精度就上来了。
量化参数里有一个叫“量化粒度”的选项,可以选逐层量化或者逐通道量化。逐通道量化精度更高,但KPU对逐通道量化的支持有限,某些层可能不支持。我的建议是先用逐层量化跑一遍,如果精度不达标再尝试逐通道。另外,nncase提供了一个叫“量化感知训练”的功能,可以在训练阶段就模拟量化误差,让模型提前适应。这个功能对精度提升帮助很大,但训练时间会变长,适合对精度要求高的场景。
3.3 模型转换与kmodel生成
训练完的模型是h5或者pb格式,需要经过nncase转换成kmodel。转换脚本的核心是配置一个转换参数文件,里面指定输入shape、输出节点名称、量化校准数据路径、目标平台等。这里有几个参数特别容易填错。
输入shape要和训练时完全一致,包括通道顺序。K210的摄像头输出通常是RGB565或者RGB888,但nncase默认输入是RGB888,通道顺序是CHW还是HWC要看具体配置。我一开始没注意通道顺序,转换出来的模型推理结果完全乱套。后来在转换配置里明确指定了“input_layout”为NHWC,问题才解决。
输出节点名称也要和训练时一致。如果你用的是Keras,可以用model.summary()查看每层的名称,然后选最后一层的名称作为输出节点。如果选错了,转换出来的模型可能没有输出,或者输出维度不对。
转换完成后,nncase会生成一个kmodel文件和一个报告文件。报告文件里会列出每层的量化误差和内存占用,这个报告一定要仔细看。如果某一层的量化误差特别大,说明这一层的权重分布可能有问题,需要回头调整训练策略。
4. 实操过程与核心环节实现
4.1 环境搭建的完整步骤
环境搭建是第一步,也是最容易劝退的一步。我用的系统是Ubuntu 18.04,Python版本是3.6。为什么不用更新的版本?因为nncase对Python 3.7以上的支持有问题,TensorFlow 1.15也只支持到Python 3.7。所以如果你打算走本地训练这条路,建议直接装Ubuntu 18.04加Python 3.6的组合,能省很多事。
安装TensorFlow 1.15的时候,用pip install tensorflow==1.15.0就行。但要注意,如果你的机器有NVIDIA显卡,需要装对应的CUDA和cuDNN版本。TF1.15对应的是CUDA 10.0和cuDNN 7.4。我一开始装了CUDA 10.2,结果TF死活认不到GPU,后来降级到10.0才正常。
nncase的安装稍微麻烦一点。官方提供了whl包,但版本要和你的Python版本匹配。我用的nncase版本是0.2.0,对应的whl包在官方仓库里能找到。安装完之后,可以用python -c "import nncase"测试一下,没报错就说明装好了。
还有一个工具是labelImg,用来标注数据。这个用pip install labelImg就能装,但它在Python 3.6下可能会有一些依赖问题,需要手动装PyQt5。我装的时候遇到了libGL报错,后来用apt install libgl1-mesa-glx解决了。
4.2 数据采集与标注的实操记录
数据采集我用的就是K210开发板本身,跑官方例程里的摄像头采集程序,把图片存到SD卡里。采集的时候要注意,尽量让目标在画面中的位置和大小有变化,不要所有图片都是同一个角度。我每个类别采集了大概三百张,最后筛选出两百多张质量好的。
标注用的是LabelImg,打开图片文件夹,设置好标注类别,然后一张张画框。这个过程很枯燥,但没办法跳过。我标注的时候有个习惯:边界框尽量贴紧目标,但不要贴得太死,留一两个像素的余量。因为K210的摄像头有畸变,如果边界框太紧,实际推理的时候可能会框不全。
标注完成后,LabelImg会生成和图片同名的xml文件。我需要把这些xml转成YOLO格式的txt。网上有现成的转换脚本,但要注意类别索引的对应关系。我一开始没注意,转换出来的类别索引和训练脚本里的类别列表对不上,导致训练出来的模型把所有类别都识别成同一类。后来在转换脚本里加了一个类别映射表,问题才解决。
4.3 训练脚本的关键配置
训练脚本我是在官方例程的基础上改的。核心配置有几个:输入分辨率设为224x224,batch size设为16,学习率初始设为0.001,用余弦退火衰减。优化器用Adam,损失函数用YOLO的复合损失。
训练轮数我设了200轮,但实际在150轮左右就收敛了。判断收敛的标准是验证集损失不再下降,同时精度不再提升。这里有个技巧:每训练10轮保存一次权重,最后从保存的权重里挑验证集表现最好的那个。不要只看最后一轮的权重,因为过拟合可能发生在训练后期。
训练过程中要监控两个指标:训练损失和验证损失。如果训练损失持续下降但验证损失开始上升,说明过拟合了,需要加正则化或者减少模型容量。我一开始没加正则化,训练到100轮左右验证损失就开始上升,后来加了L2正则化和Dropout,过拟合问题才缓解。
还有一个细节是数据增强。K210的模型容量小,数据增强能有效提升泛化能力。我用了随机翻转、随机裁剪、亮度调整这几种增强方式。但要注意,增强的幅度不能太大,否则模型学到的特征会偏离实际场景。比如随机裁剪的幅度我控制在10%以内,亮度调整范围控制在正负20%。
4.4 模型转换与部署验证
训练完成后,我把最好的权重导出成h5文件,然后用nncase的转换脚本转成kmodel。转换脚本的核心配置如下:输入shape设为[1, 224, 224, 3],输入layout设为NHWC,输出节点选最后一层卷积的输出,量化校准数据用训练集里随机抽取的三百张图片。
转换过程大概花了几分钟,生成的kmodel文件大小在几百KB左右。转换完成后,nncase会输出一个报告,显示每层的量化误差。我检查了一下,大部分层的误差都在可接受范围内,只有最后一层的误差稍微大一点,但整体精度损失在5%以内,可以接受。
部署验证是在K210开发板上跑的。我把kmodel文件拷到SD卡里,然后跑官方提供的kmodel加载例程。第一次跑的时候,推理结果完全不对,所有图片都识别成同一类。排查了半天,发现是输入图像的预处理有问题。训练时我对图像做了归一化,把像素值从0-255缩放到0-1,但部署时忘了加这一步。后来在例程里加了归一化代码,推理结果就正常了。
实测下来,模型在开发板上的推理速度大概是每帧几十毫秒,对于简单的检测任务够用了。识别率方面,在训练集同分布的场景下能达到90%以上,但在光照变化较大的场景下会降到70%左右。这个表现符合预期,毕竟K210的算力和模型容量摆在那里。
5. 常见问题与排查技巧实录
5.1 训练阶段的典型问题
训练阶段最常见的问题是损失不下降或者下降很慢。我遇到过几次,排查下来原因各不相同。有一次是学习率设得太小,模型更新太慢,后来把学习率从0.0001调到0.001就正常了。还有一次是数据标注有问题,某些图片的边界框坐标超出了图像范围,导致损失计算异常。后来写了个脚本检查所有标注文件,把越界的框修正了。
另一个常见问题是过拟合。表现是训练损失持续下降但验证损失开始上升。我的解决思路是三步走:先加数据增强,再加正则化,最后考虑减小模型容量。数据增强是最有效的手段,但要注意增强方式要和实际场景匹配。正则化方面,L2正则化和Dropout都能用,但Dropout的比例不要太高,0.3左右就够了。
还有一个比较隐蔽的问题是类别不平衡。如果某一类的样本数远多于其他类,模型会偏向于预测这一类。我的做法是对样本少的类别做过采样,或者用类别权重来平衡损失。Keras的fit函数支持class_weight参数,可以直接传一个字典进去。
5.2 量化转换阶段的报错与解决
量化转换阶段是报错最集中的地方。我整理了几个常见的报错和解决方法。
第一个报错是“Unsupported operator”。这说明你的模型里用了KPU不支持的算子。解决方法很简单:找到那个算子,换成支持的算子。比如把LeakyReLU换成ReLU,把GlobalAveragePooling换成AveragePooling加Reshape。
第二个报错是“Quantization calibration failed”。这通常是校准数据的问题。检查校准数据的路径是否正确,图片格式是否支持,数量是否足够。我一开始只用了十张校准图,nncase直接报错说数据量不足。后来加到三百张就正常了。
第三个报错是“Input shape mismatch”。这个错误说明转换配置里的输入shape和模型实际输入shape不一致。用model.input_shape打印一下模型的输入维度,然后和转换配置里的对比,改成一致的就行。
第四个报错是“Output node not found”。这个错误说明转换配置里指定的输出节点名称在模型里不存在。用model.summary()查看所有层的名称,然后选一个存在的层作为输出节点。
5.3 部署运行阶段的异常排查
部署阶段的异常通常表现为推理结果不对或者程序崩溃。推理结果不对的原因有很多,我按排查顺序列一下。
首先检查输入预处理。训练时对图像做了什么处理,部署时就要做同样的处理。归一化、通道顺序、尺寸缩放,这些都要一一对应。我建议把训练时的预处理代码单独抽出来,部署时直接复用。
然后检查模型加载。kmodel文件是否正确拷贝到SD卡,文件路径是否正确,加载时的内存分配是否足够。K210的内存有限,如果kmodel文件太大,加载会失败。我遇到过因为kmodel文件超过内存限制导致加载失败的情况,后来把模型参数量砍了一半才跑起来。
最后检查推理后的后处理。检测模型通常需要做NMS(非极大值抑制)来去除重叠框。如果NMS的参数设得不对,可能会出现框重叠或者漏检。我一开始用的NMS阈值是0.5,后来调到0.3,重叠框的问题就解决了。
程序崩溃的话,通常是内存越界或者栈溢出。K210的RAM不大,如果模型中间层的特征图太大,可能会撑爆内存。解决方法要么是减小输入分辨率,要么是减少网络层数。我建议在训练阶段就用nncase的报告功能查看每层的内存占用,提前发现潜在的内存问题。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 训练损失不下降 | 学习率过小、标注错误 | 检查学习率、抽查标注文件 | 调大学习率、修正标注 |
| 验证损失上升 | 过拟合 | 对比训练和验证损失曲线 | 加数据增强、加正则化 |
| 量化转换报错 | 算子不支持、校准数据不足 | 查看报错信息、检查校准集 | 换算子、增加校准数据 |
| 推理结果全同一类 | 预处理不一致、类别索引错位 | 对比训练和部署预处理代码 | 统一预处理、修正类别映射 |
| 程序崩溃 | 内存不足、栈溢出 | 查看nncase内存报告 | 减小输入分辨率、简化网络 |
| 识别率低 | 量化误差大、数据分布不匹配 | 查看量化报告、对比训练测试分布 | 用量化感知训练、补充数据 |
这个表是我在实际操作中总结出来的,基本上覆盖了八成以上的常见问题。遇到新问题的时候,我建议先按这个表排查一遍,大部分情况都能找到线索。
6. 一些个人体会和后续扩展思路
整个流程跑下来,我最大的感受是:K210的模型训练链路虽然折腾,但一旦跑通,后续迭代就快很多了。第一次跑通花了大概一周的业余时间,后面再训练新模型,基本上一天就能搞定。关键是要把环境固定下来,把脚本模板化,每次只需要换数据集和调整少量参数。
另外一点体会是,不要追求高精度。K210的定位是边缘端低成本推理,它的算力和内存决定了它跑不了太复杂的模型。与其花大量时间调精度,不如把精力放在数据质量和场景适配上。我见过很多人拿着K210想跑ResNet50,最后要么跑不动,要么精度惨不忍睹。选一个轻量级的网络,把输入分辨率降下来,反而能得到更实用的结果。
后续我打算尝试几个方向。一个是多模型切换,把多个kmodel文件放在SD卡里,根据场景动态加载。另一个是尝试量化感知训练,看看能不能在保持推理速度的前提下把精度再提升几个点。还有一个想法是把训练流程脚本化,做成一个一键式的工具,输入数据集路径和配置文件,自动完成训练、量化、转换全流程。这些等我有新的进展再另开一篇笔记来写。
如果你也在折腾K210的模型训练,欢迎交流踩坑经验。这个平台虽然小众,但社区里还是有不少热心人的。遇到问题的时候,除了查官方文档,也可以去社区里搜搜看,大概率有人已经踩过同样的坑了。