当你训练好一个TensorFlow模型后,第一步就是要把它转换成K230开发板能识别的格式。这里我强烈推荐使用ONNX作为中间格式,而不是直接转成tflite。为什么?因为在实际项目中我发现,tflite对动态输入形状的支持不太友好,经常会在后续的kmodel转换环节出问题。
先来看个简单的例子。假设我们训练了一个预测Y=2X-1的线性回归模型:
python复制import tensorflow as tf
import numpy as np
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense
# 构建模型
model = Sequential([Dense(units=1, input_shape=[1])])
model.compile(optimizer='sgd', loss='mean_squared_error')
# 训练数据
xs = np.array([-1.0, 0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)
ys = np.array([-3.0, -1.0, 1.0, 3.0, 5.0, 7.0], dtype=float)
# 训练
model.fit(xs, ys, epochs=500)
训练完成后,我们需要分两步转换:
python复制# 保存模型
model.save('linear_model')
# 转换为ONNX
import os
os.system("python3 -m tf2onnx.convert --saved-model linear_model --output model.onnx --opset 11")
这里有个坑要注意:opset版本。我实测发现K230对opset 11支持最好,太高或太低的版本都可能导致后续转换失败。转换完成后,强烈建议用onnx.checker验证一下:
python复制import onnx
onnx_model = onnx.load("./model.onnx")
check = onnx.checker.check_model(onnx_model)
print('Check: ', check) # 输出None表示成功
你以为转换完ONNX就万事大吉了?Too young!我踩过最大的坑就是输入输出维度问题。用Netron打开刚才的model.onnx,你会发现输入输出形状可能是这样的:
code复制input: [None, 1]
output: [None, 1]
这种动态维度在板端运行时很容易出问题。解决方法很简单 - 手动固定维度:
python复制onnx_model = onnx.load("./model.onnx")
# 固定输入维度为[1,1]
onnx_model.graph.input[0].type.tensor_type.shape.dim[0].dim_value = 1
# 固定输出维度为[1,1]
onnx_model.graph.output[0].type.tensor_type.shape.dim[0].dim_value = 1
onnx.save(onnx_model, './model_fixed.onnx')
这个步骤看似简单,但极其重要。我在三个不同的项目中都遇到过因为维度问题导致的推理错误,症状包括:
现在来到重头戏 - 用嘉楠的nncase工具链把ONNX转成kmodel。首先确保你已经安装了nncase:
bash复制pip install nncase
转换脚本的核心是compile_kmodel函数,这里我分享一个经过实战检验的版本:
python复制import nncase
import numpy as np
def compile_kmodel(onnx_path, output_dir):
# 1. 编译选项
compile_options = nncase.CompileOptions()
compile_options.target = "k230"
compile_options.dump_ir = True # 建议开启,方便调试
# 2. PTQ量化配置
ptq_options = nncase.PTQTensorOptions()
ptq_options.quant_type = "uint8" # 板端推理推荐uint8
ptq_options.calibrate_method = "Kld" # 对线性模型效果更好
# 3. 校准数据准备
calib_data = [np.array([[x]], dtype=np.float32) for x in [-1, 0, 1, 2, 3, 4]]
# 4. 执行转换
compiler = nncase.Compiler(compile_options)
compiler.import_onnx(open(onnx_path, 'rb').read())
compiler.use_ptq(ptq_options)
compiler.set_ptq_data(calib_data)
compiler.compile()
# 5. 保存kmodel
kmodel = compiler.gencode_tobytes()
with open(f"{output_dir}/model.kmodel", 'wb') as f:
f.write(kmodel)
这里有几个关键点:
转换完成后,建议先用nncase的模拟器测试:
python复制def simulate_kmodel(kmodel_path, input_data):
interpreter = nncase.Runtime.Interpreter()
interpreter.load_model(open(kmodel_path, 'rb').read())
# 设置输入
input_tensor = interpreter.get_input_tensor(0)
input_tensor.copy_from(input_data)
# 推理
interpreter.run()
# 获取输出
output_tensor = interpreter.get_output_tensor(0)
return output_tensor.to_numpy()
# 测试
test_input = np.array([[10.0]], dtype=np.float32)
output = simulate_kmodel("model.kmodel", test_input)
print(f"Input: 10.0, Output: {output[0][0]}") # 应该接近19
终于到了最后一步 - 把kmodel部署到K230开发板上。首先把kmodel文件放到SD卡的/sdcard/app/tests/目录下。
板端Python代码是这样的:
python复制import nncase_runtime as nn
import ulab.numpy as np
# 1. 加载模型
kpu = nn.kpu()
kpu.load_kmodel("/sdcard/app/tests/model.kmodel")
# 2. 准备输入
input_data = np.array([22], dtype=np.float32)
input_tensor = nn.from_numpy(input_data.reshape(1,1))
# 3. 推理
kpu.set_input_tensor(0, input_tensor)
kpu.run()
# 4. 获取输出
result = kpu.get_output_tensor(0)
output = result.to_numpy()[0][0]
print(f"Input: 22, Output: {output}") # 预期接近43
实际部署时我遇到过几个典型问题:
内存不足:K230的内存有限,解决方案是:
量化误差:特别是当输入超出校准数据范围时。比如:
性能优化:对于实时性要求高的场景,可以:
当模型部署后效果不理想时,可以按照以下步骤排查:
python复制# 在PC端用原始模型推理
pc_output = original_model.predict([[22]])
# 在板端用kmodel推理
board_output = run_kmodel("model.kmodel", [[22]])
print(f"PC结果: {pc_output}, 板端结果: {board_output}")
python复制compile_options.dump_ir = True
compile_options.dump_asm = True
compile_options.dump_dir = "./debug"
python复制ptq_options.dump_quant_error = True
ptq_options.dump_quant_error_symmetric_for_signed = True
性能优化方面,有几个实测有效的技巧:
当处理更复杂的模型(如MobileNet、YOLO等)时,需要额外注意:
python复制import_options = nncase.ImportOptions()
import_options.register_custom_op("CustomOp", lambda x: ...)
python复制ptq_options.quant_scheme = "mixed"
ptq_options.quant_scheme_strict_mode = False
python复制compile_options.max_memory_usage = 8 * 1024 * 1024 # 8MB
compile_options.split_w_to_act = True
我在一个图像分类项目中,将MobileNetV2成功部署到K230上的关键步骤:
最终在保证95%精度的前提下,推理速度达到15FPS。