> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs-staging.qualcomm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Model Preparation

# 10.1	模型准备

本阶段介绍如何构建示例 1D-CNN 模型、将其导出为 int8 TFLite 格式、生成量化输入数据，以及生成嵌入 MCU 固件的 C 字节数组。

本阶段的源文件（`model_keras.py`、`export_tflite.py`、`gen_input_data.py`）随本文档一同提供。在运行任何脚本之前，请先在主机上安装一次依赖项：

```bash theme={null}
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` 模型架构。

```python theme={null}
"""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` 构建模型，使用随机代表性数据集执行全整数量化，并验证两个输出。

```python theme={null}
"""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()
```

**运行：**

```bash theme={null}
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 窗口。

```python theme={null}
"""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()
```

**运行：**

```bash theme={null}
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 固件之前，请确认模型能够使用生成的输入正确运行：

```python theme={null}
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
```
