用ESP-NN在ESP32-S3上部署一个学过sin的神经网络
全景流程
PC (Python)
① train.py — 训练 float 模型
② quantize.py — float32 → int8 量化
③ convert_to_c.py — .tflite → C 数组 model.h
拷贝
model.h
→
ESP32-S3
④ app_main.cpp — 加载模型 → 推理 → 打印结果
环境
- Python 3.8+,用uv管理虚拟环境
- ESP-IDF v5.0+
- ESP32-S3开发板(ESP32/ESP32-C3也兼容,S3加速比最高)
训练
数据
1000个[0, 2π]均匀分布的随机点,对应sin(x)值。选sin的原因:非线性(Dense必须弯曲才能拟合)、有界(输出[-1,1],量化时范围可控)、周期性(训练数据天然覆盖完整定义域)。
网络
model = tf.keras.Sequential([
tf.keras.layers.Dense(16, activation='relu', input_shape=(1,)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])三层全连接,305个参数。300轮训练,batch_size=64,20%验证集。
训练代码(train.py)
import math
import numpy as np
import tensorflow as tf
# 1. 生成训练数据:1000个 [0, 2π] 的随机点
x_values = np.random.uniform(0, 2 * math.pi, 1000).astype(np.float32)
np.random.shuffle(x_values)
y_values = np.sin(x_values).astype(np.float32)
# 2. 构建模型
model = tf.keras.Sequential([
tf.keras.layers.Dense(16, activation='relu', input_shape=(1,)),
tf.keras.layers.Dense(16, activation='relu'),
tf.keras.layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
model.summary()
# 3. 训练 300 轮
model.fit(x_values, y_values, epochs=300, batch_size=64, validation_split=0.2)
# 4. 验证
test_x = np.array([0, math.pi / 4, math.pi / 2, math.pi, 3 * math.pi / 2, 2 * math.pi], dtype=np.float32)
test_y = model.predict(test_x, verbose=0).flatten()
for x, y_pred in zip(test_x, test_y):
y_true = math.sin(x)
print(f" sin({x:.4f}) = {y_true:+.4f} | 预测 = {y_pred:+.4f} | 误差 = {abs(y_pred - y_true):.4f}")
# 5. 保存
model.save("sin_model_float.keras")
converter = tf.lite.TFLiteConverter.from_keras_model(model)
with open("sin_model_float.tflite", "wb") as f:
f.write(converter.convert())pip install uv
uv venv .venv && uv pip install tensorflow numpy
python train.py训练结果
sin(0.00) = +0.000 | 预测 = +0.001 | 误差 0.001
sin(π/2) = +1.000 | 预测 = +0.991 | 误差 0.009
sin(π) = -0.000 | 预测 = -0.013 | 误差 0.013产物:sin_model_float.keras(可继续训练)、sin_model_float.tflite(float TFLite,约3KB)。
量化
为什么需要量化
ESP32-S3的FPU只有单精度,且没有向量浮点加速。int8整数运算配合ESP-NN的汇编优化(利用ESP32-S3的PIE向量指令,一条指令处理4个int8),推理速度比float快一个数量级。模型体积也缩小4倍。
量化原理
量化建立float和int8之间的线性映射:
float_value = (int8_value - zero_point) × scalescale和zero_point通过校准数据集统计每层数值范围后自动计算。量化脚本中的representative_dataset用于观察每层激活值的分布,与训练无关。
量化代码(quantize.py)
import math
import numpy as np
import tensorflow as tf
# 1. 加载 float 模型
converter = tf.lite.TFLiteConverter.from_keras_model(
tf.keras.models.load_model("sin_model_float.keras")
)
# 2. 配置 int8 全量化
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 校准数据集:100个随机样本,用来统计每层数值范围
def representative_dataset():
for x in np.random.uniform(0, 2 * math.pi, 100).astype(np.float32):
yield [x]
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
# 3. 转换并保存
tflite_quant_model = converter.convert()
with open("sin_model_int8.tflite", "wb") as f:
f.write(tflite_quant_model)
# 4. 验证
interpreter = tf.lite.Interpreter(model_content=tflite_quant_model)
interpreter.allocate_tensors()
input_details = interpreter.get_input_details()
output_details = interpreter.get_output_details()
print(f"输入量化参数: scale={input_details[0]['quantization'][0]:.6f}, "
f"zero_point={input_details[0]['quantization'][1]}")
print(f"输出量化参数: scale={output_details[0]['quantization'][0]:.6f}, "
f"zero_point={output_details[0]['quantization'][1]}")
test_points = [0, math.pi / 4, math.pi / 2, math.pi, 3 * math.pi / 2]
for x in test_points:
input_scale = input_details[0]['quantization'][0]
input_zp = input_details[0]['quantization'][1]
x_quant = int(x / input_scale + input_zp)
x_quant = np.clip(x_quant, -128, 127)
interpreter.set_tensor(input_details[0]['index'], np.array([[x_quant]], dtype=np.int8))
interpreter.invoke()
y_quant = interpreter.get_tensor(output_details[0]['index']).flatten()[0]
output_scale = output_details[0]['quantization'][0]
output_zp = output_details[0]['quantization'][1]
y_pred = float((int(y_quant) - output_zp) * output_scale)
y_true = math.sin(x)
print(f" sin({x:.4f}) = {y_true:+.4f} | int8 预测 = {y_pred:+.4f} | 误差 = {abs(y_pred - y_true):.4f}")量化结果
输入: scale=0.024349, zero_point=-128
输出: scale=0.008386, zero_point=7
sin(0.00) = +0.000 | int8 预测 = +0.025 | 误差 0.025
sin(π/4) = +0.707 | int8 预测 = +0.713 | 误差 0.006
sin(π/2) = +1.000 | int8 预测 = +1.006 | 误差 0.006
sin(3π/2) = -1.000 | int8 预测 = -1.115 | 误差 0.115边界处误差较大,可通过量化感知训练(QAT)改善。产物:sin_model_int8.tflite(约3.4KB)。
转C数组
.tflite文件无法直接被C代码读取,转为const unsigned char数组嵌入固件:
import sys
def convert_to_c_array(tflite_path, output_path):
with open(tflite_path, "rb") as f:
model_data = f.read()
with open(output_path, "w") as f:
f.write('// Auto-generated by convert_to_c.py\n')
f.write('// Do not edit manually\n\n')
f.write('#ifndef MODEL_H_\n')
f.write('#define MODEL_H_\n\n')
f.write(f'constexpr unsigned int g_model_len = {len(model_data)};\n')
f.write('alignas(16) const unsigned char g_model[] = {\n')
for i, byte in enumerate(model_data):
if i % 12 == 0:
f.write(' ')
f.write(f'0x{byte:02x}')
if i < len(model_data) - 1:
f.write(', ')
if i % 12 == 11:
f.write('\n')
f.write('\n};\n\n')
f.write('#endif // MODEL_H_\n')
if __name__ == "__main__":
input_file = sys.argv[1] if len(sys.argv) > 1 else "sin_model_int8.tflite"
output_file = sys.argv[2] if len(sys.argv) > 2 else "../esp32s3/main/model.h"
convert_to_c_array(input_file, output_file)alignas(16)是ESP32-S3的DMA和向量指令的对齐要求。
ESP32-S3部署
项目结构
esp32s3/
├── CMakeLists.txt
├── components/
│ └── esp-tflite-micro/ # TFLite Micro 组件(通过 -DESP_NN 调用 ESP-NN)
├── managed_components/
│ └── espressif__esp-nn/ # ESP-NN(idf.py build 自动下载)
└── main/
├── CMakeLists.txt
├── idf_component.yml
├── app_main.cpp
└── model.h # convert_to_c.py 生成main/idf_component.yml只声明esp-nn依赖,esp-tflite-micro通过本地components目录引入:
dependencies:
espressif/esp-nn:
version: ">=1.1.1"main/CMakeLists.txt必须在PRIV_REQUIRES中声明esp-tflite-micro,否则编译器找不到头文件:
idf_component_register(
SRCS "app_main.cpp"
INCLUDE_DIRS "."
PRIV_REQUIRES spi_flash esp-tflite-micro
)ESP-NN通过esp-tflite-micro的CMakeLists.txt中的-DESP_NN宏自动启用。
推理代码
/*
* sin_demo - 在 ESP32-S3 上运行 sin(x) 推理
*
* 流程:
* 1. 加载 int8 量化模型
* 2. 循环输入 x 值
* 3. 执行推理(ESP-NN 自动加速)
* 4. 反量化输出,打印结果
*/
#include <cstdio>
#include <cinttypes> // PRId32 / PRIu32 宏
#include <cmath>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
#include "tensorflow/lite/schema/schema_generated.h"
#include "model.h"
// TFLite Micro 需要的内存池
constexpr int kTensorArenaSize = 2048;
alignas(16) static uint8_t tensor_arena[kTensorArenaSize];
// 模型需要的算子数量(只有 1 个全连接层类型)
static tflite::MicroMutableOpResolver<1> resolver;
extern "C" void app_main(void)
{
printf("\n========================================\n");
printf(" sin_demo: ESP-NN + TFLite Micro\n");
printf("========================================\n\n");
// 1. 加载模型
const tflite::Model *model = tflite::GetModel(g_model);
if (model->version() != TFLITE_SCHEMA_VERSION) {
printf("ERROR: 模型版本不匹配! 期望 %d, 实际 %" PRIu32 "\n",
TFLITE_SCHEMA_VERSION, model->version());
return;
}
printf("[OK] 模型已加载 (%d bytes)\n", g_model_len);
if (resolver.AddFullyConnected() != kTfLiteOk) {
printf("ERROR: 注册 FullyConnected 算子失败\n");
return;
}
// 2. 创建解释器
static tflite::MicroInterpreter interpreter(
model, resolver, tensor_arena, kTensorArenaSize);
if (interpreter.AllocateTensors() != kTfLiteOk) {
printf("ERROR: 分配张量内存失败\n");
return;
}
printf("[OK] 解释器已初始化 (arena: %d bytes)\n", kTensorArenaSize);
TfLiteTensor *input = interpreter.input(0);
TfLiteTensor *output = interpreter.output(0);
// ESP32-S3 上 int32_t 是 long int,必须用 PRId32 而非 %d
printf("[OK] 输入量化参数: scale=%.6f zero_point=%" PRId32 "\n",
(double)input->params.scale, input->params.zero_point);
printf("[OK] 输出量化参数: scale=%.6f zero_point=%" PRId32 "\n\n",
(double)output->params.scale, output->params.zero_point);
// 3. 循环推理
printf(" %-12s %-12s %-12s %-12s\n", "x", "sin(x)真实值", "推理结果", "误差");
printf(" %-12s %-12s %-12s %-12s\n", "--------", "----------", "--------", "--------");
for (int i = 0; i <= 20; i++) {
float x = (float)i / 20.0f * 2.0f * M_PI;
// float → int8
int8_t x_quant = (int8_t)(x / input->params.scale + input->params.zero_point);
input->data.int8[0] = x_quant;
// 推理(ESP-NN 自动加速)
if (interpreter.Invoke() != kTfLiteOk) {
printf("ERROR: 推理失败\n");
return;
}
// int8 → float
int8_t y_quant = output->data.int8[0];
float y_pred = (y_quant - output->params.zero_point) * output->params.scale;
float y_true = sinf(x);
printf(" %-12.4f %-12.4f %-12.4f %-12.4f\n",
x, y_true, y_pred, fabsf(y_pred - y_true));
vTaskDelay(pdMS_TO_TICKS(500));
}
printf("\n推理完成!\n");
}编译烧录
idf.py set-target esp32s3
idf.py build
idf.py --port COM3 flash monitor输出
========================================
sin_demo: ESP-NN + TFLite Micro
========================================
[OK] 模型已加载 (3456 bytes)
[OK] 解释器已初始化 (arena: 2048 bytes)
[OK] 输入量化参数: scale=0.024349 zero_point=-128
[OK] 输出量化参数: scale=0.008386 zero_point=7
x sin(x)真实值 推理结果 误差
-------- ---------- -------- --------
0.0000 0.0000 0.0252 0.0252
0.3142 0.3090 0.3438 0.0348
0.6283 0.5878 0.5786 0.0092
0.9425 0.8090 0.8134 0.0044
1.2566 0.9511 0.9560 0.0049
1.5708 1.0000 1.0063 0.0063
1.8850 0.9511 0.9308 0.0202
2.1991 0.8090 0.7967 0.0124
2.5133 0.5878 0.5115 0.0762
2.8274 0.3090 0.2432 0.0658
3.1416 -0.0000 -0.0168 0.0168
3.4558 -0.3090 -0.2600 0.0491
3.7699 -0.5878 -0.5619 0.0259
4.0841 -0.8090 -0.7967 0.0124
4.3982 -0.9511 -0.9728 0.0217
4.7124 -1.0000 -1.1153 0.1153
5.0265 -0.9511 -0.9224 0.0286
5.3407 -0.8090 -0.7212 0.0878
5.6549 -0.5878 -0.5115 0.0762
5.9690 -0.3090 -0.3187 0.0096
6.2832 0.0000 0.0419 0.0419
推理完成!ESP-NN加速机制
ESP-NN通过-DESP_NN宏替换TFLite Micro的默认算子实现。本例涉及的是FullyConnected算子。
ESP32-S3的加速比最高,因其有PIE向量指令,一条指令同时处理4个int8数据。ESP-NN的汇编内核直接利用了这个特性。
性能数据来源:ESP-NN GitHub 和 ESP Component Registry 中的Person Detection示例基准测试。
后续
- 换更大的模型(如CNN做MNIST),体会卷积层的量化
- 用ESP-DL替代TFLite Micro,支持双核调度和静态内存规划
- 接传感器,把
input->data.int8换成真实的传感器数据
链路不变:训练 → 量化 → 转C → 推理。
本文由 AI 辅助生成,可能存在错误或遗漏,请以实际资料和官方文档为准。