4、模型转换入门:从 Keras 模型导出 .tflite、量化选项(float32 vs int8)、转换脚本编写

好,咱们进入正题。模型转换,说白了就是把你在 PC 上训练好的模型,变成能在嵌入式设备上跑的东西。这一步要是没做好,后面全白搭。我见过不少新手,在 Keras 上跑得飞起,一转到单片机就崩了——嗯,大概率是转换这步出了问题。

4.1 为什么需要模型转换?

你想想看,TensorFlow 在 PC 上用的是 float32 精度,模型文件动不动几十上百 MB。但嵌入式设备呢?RAM 可能就几百 KB,Flash 也就几 MB。直接拿原始模型去跑,根本塞不进去。

所以我们需要一个「瘦身」过程:

  • 格式转换:从 Keras 的 .h5 或 SavedModel 变成 .tflite
  • 精度压缩:从 float32 降到 int8,体积直接缩到 1/4
  • 算子优化:去掉一些 PC 上才有、嵌入式用不到的算子

我个人习惯把转换脚本放在项目根目录的 tools/ 文件夹下,命名成 convert.py。这样每次改模型,跑一遍脚本就行,不会手忙脚乱。

4.2 基础转换:从 Keras 到 .tflite(float32)

先来个最简单的。假设你有一个训练好的 Keras 模型,叫 my_model.h5。转换脚本长这样:

import tensorflow as tf

# 加载 Keras 模型
model = tf.keras.models.load_model('my_model.h5')

# 创建转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)

# 执行转换
tflite_model = converter.convert()

# 保存 .tflite 文件
with open('my_model_float32.tflite', 'wb') as f:
    f.write(tflite_model)

print("转换完成!输出文件:my_model_float32.tflite")

就这么几行。跑完之后,你会得到一个 float32 精度的 .tflite 文件。体积和原来的 .h5 差不多,但格式已经变了,可以在 TFLite 的运行时里加载。

小提示: 如果你用的是 SavedModel 格式,把 from_keras_model 换成 from_saved_model 就行。我个人更推荐用 SavedModel,因为它包含了模型的结构和权重,不容易出兼容性问题。

4.3 量化:从 float32 到 int8

好,重点来了。float32 模型在嵌入式上跑,说实话有点奢侈。一个 float32 数占 4 字节,int8 只占 1 字节。你想想看,一个 100MB 的模型,量化后直接变成 25MB。对于 Flash 只有 2MB 的 MCU 来说,这差距就是「能用」和「不能用」的区别。

量化的核心思想很简单:用更少的比特数来表示权重和激活值。但代价是精度会掉一点。我在项目中遇到过,有些模型量化后精度只掉了 0.5%,有些直接掉了 5%。所以量化后一定要做验证。

4.3.1 后训练量化(Post-Training Quantization)

这是最常用的方式。你不需要重新训练模型,直接在转换时加上量化参数就行。

import tensorflow as tf

model = tf.keras.models.load_model('my_model.h5')

# 创建转换器
converter = tf.lite.TFLiteConverter.from_keras_model(model)

# 开启 int8 量化
converter.optimizations = [tf.lite.Optimize.DEFAULT]

# 指定量化类型为 int8
converter.target_spec.supported_types = [tf.int8]

# 设置代表数据集(用于校准)
def representative_dataset():
    # 假设你有 100 张图片用于校准
    for _ in range(100):
        # 这里需要从你的数据集中取样本
        data = np.random.randn(1, 224, 224, 3).astype(np.float32)
        yield [data]

converter.representative_dataset = representative_dataset

# 执行转换
tflite_quant_model = converter.convert()

# 保存
with open('my_model_int8.tflite', 'wb') as f:
    f.write(tflite_quant_model)
注意: 代表数据集(representative_dataset)这一步不能省。我曾经偷懒,随便传了几个随机数进去,结果量化后的模型在推理时直接输出 NaN。后来老老实实从验证集里抽了 200 张图片做校准,才恢复正常。这个数据集不需要太大,100-500 张就够了,但一定要能代表真实数据分布。

4.3.2 float32 vs int8:怎么选?

我整理了一个对比表,方便你决策:

对比项 float32 int8
模型体积 大(基准) 缩小约 4 倍
推理速度 慢(无硬件加速时) 快(支持硬件加速)
精度 高(基准) 略有下降(通常 < 1%)
硬件要求 需要 FPU 普通 MCU 即可
适用场景 原型验证、高精度需求 量产部署、资源受限设备

我的建议是:先跑 float32 版本验证功能,确认无误后再量化成 int8。如果 int8 精度掉得太多,可以考虑用混合量化(部分层用 float32,部分层用 int8),或者干脆用 float16 量化——体积减半,精度几乎不变。

4.4 转换脚本编写:一个完整的模板

嗯,这里我直接给你一个我常用的脚本模板。你拿去改改就能用:

import tensorflow as tf
import numpy as np
import argparse
import os

def convert_model(model_path, output_path, quantize_type='float32'):
    """
    模型转换函数
    :param model_path: Keras 模型路径 (.h5 或 SavedModel)
    :param output_path: 输出 .tflite 文件路径
    :param quantize_type: 量化类型,可选 'float32', 'float16', 'int8'
    """
    # 加载模型
    print(f"正在加载模型:{model_path}")
    model = tf.keras.models.load_model(model_path)
    
    # 创建转换器
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    
    # 根据量化类型设置参数
    if quantize_type == 'float32':
        # 默认就是 float32,不需要额外设置
        pass
    
    elif quantize_type == 'float16':
        converter.optimizations = [tf.lite.Optimize.DEFAULT]
        converter.target_spec.supported_types = [tf.float16]
    
    elif quantize_type == 'int8':
        converter.optimizations = [tf.lite.Optimize.DEFAULT]
        converter.target_spec.supported_types = [tf.int8]
        
        # 代表数据集(这里需要你根据实际情况修改)
        def representative_dataset():
            # 从验证集中取 100 张图片
            for i in range(100):
                # 请替换为你的数据加载逻辑
                data = np.random.randn(1, 224, 224, 3).astype(np.float32)
                yield [data]
        
        converter.representative_dataset = representative_dataset
    
    else:
        raise ValueError(f"不支持的量化类型:{quantize_type}")
    
    # 执行转换
    print(f"正在转换为 {quantize_type}...")
    tflite_model = converter.convert()
    
    # 保存文件
    os.makedirs(os.path.dirname(output_path), exist_ok=True)
    with open(output_path, 'wb') as f:
        f.write(tflite_model)
    
    # 打印文件大小
    size_mb = len(tflite_model) / (1024 * 1024)
    print(f"转换完成!输出文件:{output_path}")
    print(f"文件大小:{size_mb:.2f} MB")

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Keras 模型转 TFLite')
    parser.add_argument('--model', type=str, required=True, help='输入模型路径')
    parser.add_argument('--output', type=str, default='model.tflite', help='输出文件路径')
    parser.add_argument('--quantize', type=str, default='float32', 
                        choices=['float32', 'float16', 'int8'],
                        help='量化类型')
    
    args = parser.parse_args()
    convert_model(args.model, args.output, args.quantize)

用法示例:

# 转 float32
python convert.py --model my_model.h5 --output models/float32.tflite

# 转 int8
python convert.py --model my_model.h5 --output models/int8.tflite --quantize int8
避坑指南: 我曾经在转换一个语义分割模型时,int8 量化后输出全是黑的。排查了半天,发现是代表数据集里忘了做归一化——训练时输入是 [0,1] 范围,校准数据却是 [0,255]。记住:校准数据的预处理方式必须和训练时完全一致

4.5 验证转换后的模型

转换完别急着部署。先跑一下推理,看看结果对不对:

import tensorflow as tf
import numpy as np

# 加载 TFLite 模型
interpreter = tf.lite.Interpreter(model_path='my_model_int8.tflite')
interpreter.allocate_tensors()

# 获取输入输出信息
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()

print(f"输入形状:{input_details[0]['shape']}")
print(f"输入类型:{input_details[0]['dtype']}")
print(f"输出形状:{output_details[0]['shape']}")
print(f"输出类型:{output_details[0]['dtype']}")

# 准备测试数据
test_data = np.random.randn(1, 224, 224, 3).astype(np.float32)

# 如果是 int8 模型,需要做量化
if input_details[0]['dtype'] == np.int8:
    input_scale = input_details[0]['quantization'][0]
    input_zero_point = input_details[0]['quantization'][1]
    test_data = test_data / input_scale + input_zero_point
    test_data = test_data.astype(np.int8)

# 推理
interpreter.set_tensor(input_details[0]['index'], test_data)
interpreter.invoke()
output = interpreter.get_tensor(output_details[0]['index'])

print(f"推理结果形状:{output.shape}")
print(f"推理结果前 5 个值:{output.flatten()[:5]}")

这一步能帮你快速发现转换中的问题。比如输入输出形状不对、量化参数没对齐等等。

4.6 小结

模型转换这件事,说难不难,说简单也不简单。核心就三点:

  1. 格式要转:从 Keras 到 TFLite,这是必经之路
  2. 量化要选对:float32 保精度,int8 保体积,根据你的硬件资源来定
  3. 验证不能省:转换完一定要跑推理,确认结果正确

下一章我们会讲怎么在嵌入式设备上部署这个 .tflite 文件,包括内存管理、算子注册、性能调优这些实战内容。到时候你会看到,转换只是第一步,真正的坑还在后面等着呢。