10.1 模型准备
本阶段介绍如何构建示例 1D-CNN 模型、将其导出为 int8 TFLite 格式、生成量化输入数据,以及生成嵌入 MCU 固件的 C 字节数组。 本阶段的源文件(model_keras.py、export_tflite.py、gen_input_data.py)随本文档一同提供。在运行任何脚本之前,请先在主机上安装一次依赖项:
python3 -m venv .venv && source .venv/bin/activate
pip install numpy tensorflow
10.1.1 参考模型架构
示例模型是一个使用随机权重的最小 1D-CNN。| 参数 | 值 |
|---|---|
| 架构 | Conv2D + Mean (GlobalAvgPool) + FullyConnected |
| TFLM 算子 | 3 个:AddConv2D、AddMean、AddFullyConnected |
| 框架 | Keras(TensorFlow 2.x),通过 TFLiteConverter 导出 |
| 输入形状 | [1, 1, 200, 1](batch、height、length、channels) |
| 输入数据类型 | int8,每个推理窗口 200 字节 |
| 输出形状 | [1, 2],2 分类 logits |
| 模型文件大小 | 2,096 字节(cnn1d_minimal_int8.tflite) |
| Tensor arena | TFLI_ARENA_KB = 4(在目标设备上测量 g_tfli_arena_used 以进行调优) |
10.1.1.1 模型计算图
BatchNormalization 在导出过程中被折叠进 Conv2D 权重中,不会作为单独的算子出现在量化计算图中。该 int8 模型恰好包含 3 个算子。 input
|
[1 x 1 x 200 x 1] int8
|
v
+--------------------------------+
| Conv2D |
| filter [4 x 1 x 3 x 1] |
| bias [4] |
| padding SAME |
| BatchNorm folded into weights |
+--------------------------------+
| ReLU |
+--------------------------------+
|
[1 x 1 x 200 x 4]
|
v
+--------------------------------+
| Mean |
| axis [2] (global avg pool) |
+--------------------------------+
|
[1 x 4]
|
v
+--------------------------------+
| FullyConnected |
| weights [2 x 4] |
+--------------------------------+
|
[1 x 2]
|
v
output_0
10.1.2 源文件
10.1.2.1 model_keras.py:模型定义
定义 Keras 模型架构。
"""Minimal Keras CNN1D for TFLM MCU demo.
Architecture: 1 x Conv2D(4 filters, k=1x3, same) + BN + ReLU
+ GlobalAveragePool2D + Dense(num_classes)
TFLM ops needed: Conv2D, Mean, FullyConnected (3 ops, no MaxPool/Reshape)
"""
import tensorflow as tf
INPUT_HEIGHT = 1
INPUT_LENGTH = 200
INPUT_CHANNELS = 1
def build_model(num_classes: int = 2, channels: int = 4) -> tf.keras.Model:
inputs = tf.keras.Input(
batch_shape=(1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS), name="input")
x = tf.keras.layers.Conv2D(
channels, kernel_size=(1, 3), padding="same")(inputs)
x = tf.keras.layers.BatchNormalization()(x)
x = tf.keras.layers.ReLU()(x)
x = tf.keras.layers.GlobalAveragePooling2D()(x)
outputs = tf.keras.layers.Dense(num_classes, name="output")(x)
return tf.keras.Model(inputs, outputs, name="CNN1D_minimal")
if __name__ == "__main__":
model = build_model()
model.summary()
n = sum(tf.size(w).numpy() for w in model.trainable_weights)
print(f"trainable parameters: {n}")
10.1.2.2 export_tflite.py:导出 Float32 和 Int8 TFLite
基于 model_keras.py 构建模型,使用随机代表性数据集执行全整数量化,并验证两个输出。
"""Export minimal CNN1D to TFLite float32 and full-integer int8.
Outputs
-------
model_files/cnn1d_minimal_float.tflite -- float32 (reference)
model_files/cnn1d_minimal_int8.tflite -- int8, deploy on MCU
"""
import argparse
import os
import numpy as np
import tensorflow as tf
from model_keras import build_model, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS
REPR_SAMPLES = 100
def representative_dataset():
rng = np.random.default_rng(42)
for _ in range(REPR_SAMPLES):
sample = rng.standard_normal(
(1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS)).astype(np.float32)
yield [sample]
def export_float(model, path):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
data = converter.convert()
with open(path, "wb") as f:
f.write(data)
print(f" wrote {path} ({len(data):,} bytes)")
return data
def export_int8(model, path):
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = representative_dataset
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
converter.inference_input_type = tf.int8
converter.inference_output_type = tf.int8
data = converter.convert()
with open(path, "wb") as f:
f.write(data)
print(f" wrote {path} ({len(data):,} bytes)")
return data
def validate_and_report(tflite_bytes, dtype, label):
interp = tf.lite.Interpreter(model_content=tflite_bytes)
interp.allocate_tensors()
inp = interp.get_input_details()[0]
out = interp.get_output_details()[0]
print(f" [{label}] input shape={inp['shape']} dtype={inp['dtype'].__name__}"
f" scale={inp['quantization'][0]:.6f} zero_pt={inp['quantization'][1]}")
print(f" [{label}] output shape={out['shape']} dtype={out['dtype'].__name__}"
f" scale={out['quantization'][0]:.6f} zero_pt={out['quantization'][1]}")
x = np.zeros((1, INPUT_HEIGHT, INPUT_LENGTH, INPUT_CHANNELS), dtype=dtype)
interp.set_tensor(inp["index"], x)
interp.invoke()
result = interp.get_tensor(out["index"])
print(f" [{label}] zero-input output: {result}")
try:
ops = interp._get_ops_details()
for op in ops:
print(f" op_index={op['index']} name={op['op_name']}")
except AttributeError:
print(" (op detail API not available in this TF version)")
return inp['quantization']
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--num-classes", type=int, default=2)
ap.add_argument("--channels", type=int, default=4)
ap.add_argument("--out-dir", default="model_files")
args = ap.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
model = build_model(num_classes=args.num_classes, channels=args.channels)
float_path = os.path.join(args.out_dir, "cnn1d_minimal_float.tflite")
int8_path = os.path.join(args.out_dir, "cnn1d_minimal_int8.tflite")
print("--- float32 ---")
fb = export_float(model, float_path)
validate_and_report(fb, np.float32, "float32")
print("\n--- int8 ---")
ib = export_int8(model, int8_path)
scale, zero_pt = validate_and_report(ib, np.int8, "int8")
print(f"\nInput quantization: scale={scale:.6f} zero_point={zero_pt}")
print(f" float->int8: q = clip(round(x / {scale:.6f}) + {zero_pt}, -128, 127)")
print(f"\nDeploy: {int8_path}")
if __name__ == "__main__":
main()
python export_tflite.py
| 文件 | 用途 |
|---|---|
model_files/cnn1d_minimal_float.tflite | Float32 模型,仅供参考 |
model_files/cnn1d_minimal_int8.tflite | Int8 模型,嵌入 MCU 固件 |
gen_input_data.py 内部使用的输入量化参数(scale、zero_point)。
10.1.2.3 gen_input_data.py:生成量化输入文件
直接从导出的 int8 模型中读取量化参数,然后基于真实数据集或合成数据生成 200 个样本的 int8 窗口。
"""Generate int8 and float32 input files for the minimal CNN1D model.
Outputs
-------
inputs/input_0.bin -- 200 int8 bytes, ready for MCU g_cnn_input[]
inputs/input_0_f32.raw -- 200 float32 = 800 bytes, for reference
inputs/input_list.txt -- list of .bin paths for batch testing
"""
import argparse
import os
import numpy as np
import tensorflow as tf
WINDOW_LEN = 200
DATASET_LEN = 1696
DTYPE_F32 = np.dtype("<f4")
def quantize(data_f32, scale, zero_pt):
q = np.round(data_f32 / scale).astype(np.int32) + zero_pt
return np.clip(q, -128, 127).astype(np.int8)
def get_quant_params(model_path):
interp = tf.lite.Interpreter(model_path=model_path)
interp.allocate_tensors()
inp = interp.get_input_details()[0]
scale, zero_pt = inp["quantization"]
if scale == 0.0:
raise ValueError(f"{model_path}: input tensor has scale=0 -- not an int8 model")
return float(scale), int(zero_pt)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--model", default="model_files/cnn1d_minimal_int8.tflite")
ap.add_argument("--dataset", default=None,
help="Raw float32 LE dataset (1,696 values). Omit for synthetic data.")
ap.add_argument("--num-windows", type=int, default=8)
ap.add_argument("--out-dir", default="inputs")
args = ap.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
if os.path.exists(args.model):
scale, zero_pt = get_quant_params(args.model)
print(f"quant params from {args.model}: scale={scale:.6f} zero_point={zero_pt}")
else:
scale, zero_pt = 1.0 / 128.0, 0
print("model not found -- using default quant params: scale=1/128 zero_point=0")
if args.dataset:
data = np.fromfile(args.dataset, dtype=DTYPE_F32)
print(f"loaded dataset: {args.dataset} ({data.size} values)")
else:
rng = np.random.default_rng(0)
data = rng.standard_normal(DATASET_LEN).astype(DTYPE_F32)
print(f"synthesized dataset: {data.size} values")
bin_files = []
for i in range(args.num_windows):
start = (i * WINDOW_LEN) % (len(data) - WINDOW_LEN + 1)
window_f32 = data[start : start + WINDOW_LEN]
window_i8 = quantize(window_f32, scale, zero_pt)
bin_path = os.path.join(args.out_dir, f"input_{i}.bin")
window_i8.tofile(bin_path)
bin_files.append(bin_path)
f32_path = os.path.join(args.out_dir, f"input_{i}_f32.raw")
window_f32.tofile(f32_path)
print(f" window {i}: start={start} "
f"f32=[{window_f32.min():.3f}, {window_f32.max():.3f}] "
f"i8=[{window_i8.min()}, {window_i8.max()}] -> {bin_path}")
list_path = os.path.join(args.out_dir, "input_list.txt")
with open(list_path, "w") as f:
for p in bin_files:
f.write(os.path.abspath(p) + "\n")
print(f"\nwrote {list_path} ({len(bin_files)} entries)")
print(f"Primary MCU input file: {os.path.abspath(bin_files[0])}")
print(f" use as g_cnn_input[] in input_data.cpp (200 int8 bytes)")
if __name__ == "__main__":
main()
python gen_input_data.py --num-windows 8
# Pass --dataset /path/to/data.raw to use a real 1,696-value float32 LE dataset
inputs/ 文件夹中):
| 文件 | 大小 | 用途 |
|---|---|---|
input_0.bin … input_7.bin | 每个 200 字节 | 预量化的 int8 窗口,将 input_0.bin 嵌入固件 |
input_0_f32.raw … input_7_f32.raw | 每个 800 字节 | Float32 原始数据,仅用于主机端验证 |
input_list.txt | — | 供批量测试工具使用的绝对路径 |
10.1.3 在主机上验证
在将模型集成到 MCU 固件之前,请确认模型能够使用生成的输入正确运行:import tensorflow as tf
import numpy as np
interp = tf.lite.Interpreter("model_files/cnn1d_minimal_int8.tflite")
interp.allocate_tensors()
inp = interp.get_input_details()[0]
out = interp.get_output_details()[0]
print(f"Input: shape={inp['shape']} dtype={inp['dtype']} "
f"scale={inp['quantization'][0]:.6f} zp={inp['quantization'][1]}")
print(f"Output: shape={out['shape']} dtype={out['dtype']} "
f"scale={out['quantization'][0]:.6f} zp={out['quantization'][1]}")
x = np.fromfile("inputs/input_0.bin", dtype=np.int8).reshape(inp['shape'])
interp.set_tensor(inp['index'], x)
interp.invoke()
y = interp.get_tensor(out['index'])
print(f"Output logits (int8): {y}")
print(f"Predicted class: {np.argmax(y)}")
Input: shape=[1 1 200 1] dtype=int8 scale=0.007812 zp=0
Output: shape=[1 2] dtype=int8 scale=... zp=...
Output logits (int8): [[...]]
Predicted class: 0 or 1

