基于TensorFlow Lite与K-Means的嵌入式异常检测实战

基于TensorFlow Lite与K-Means的嵌入式异常检测实战
1. 项目概述在嵌入式边缘实现机器状态监控在工业物联网和预测性维护领域实时监控设备运行状态、提前预警潜在故障是提升生产效率和保障设备安全的关键。传统的阈值报警或定期巡检方式往往存在滞后性无法捕捉到设备性能的早期、细微退化。近年来随着嵌入式AI技术的成熟将机器学习模型直接部署到设备端进行实时异常检测成为了一个极具吸引力的解决方案。它不仅能降低数据传输带宽和云端计算成本还能实现毫秒级的实时响应这对于振动、温度等高频信号的监控尤为重要。本次实践的核心就是将一个基于K-Means聚类算法的异常检测模型从PC端的TensorFlow训练环境完整地迁移到NXP i.MX RT1050这款资源受限的Cortex-M7微控制器上运行。整个过程涉及传感器数据采集、信号预处理、特征工程、模型训练与转换以及最终的嵌入式部署与推理是一个典型的“从数据到部署”的端到端AIoT项目。如果你正在寻找一个将TensorFlow Lite应用于实际工业场景的完整案例或者对如何在MCU上实现机器学习推理感到好奇那么接下来的内容将为你提供一份详实的“操作手册”。2. 核心思路与方案选型背后的考量2.1 为什么选择K-Means进行异常检测在开始动手之前我们必须回答一个根本问题为什么在众多异常检测算法中我们选择了K-Means聚类首先从问题本质看工业设备的振动数据在正常运行时通常呈现出稳定的模式聚集在某个特征空间区域而异常如轴承磨损、不平衡会导致数据点偏离这个区域。K-Means算法的核心思想正是将数据点划分为K个簇并计算每个点到其所属簇中心的距离。在异常检测场景下我们可以将“正常”状态的数据训练出的簇中心视为基准任何新数据点与所有簇中心的最小距离如果超过某个阈值即可判定为异常。这是一种典型的“无监督”或“半监督”学习思路特别适合工业场景中“正常数据易得异常数据难求”的情况。其次从嵌入式部署的可行性考虑K-Means模型在推理阶段的计算非常简单主要就是距离计算如欧氏距离。这避免了神经网络中大量的乘加运算和复杂的激活函数对MCU的算力和内存尤其是Flash存储模型参数、RAM进行实时计算要求大大降低。经过TensorFlow Lite for Microcontrollers的优化后一个简单的K-Means模型可以轻松运行在仅有几百KB RAM的MCU上。注意这里我们采用了“半监督”学习。即训练时不仅使用健康数据也人为注入一些异常数据如敲击设备让模型同时学习“正常”和“已知异常”的模式。这比纯粹的无监督学习仅用健康数据更具鲁棒性能更好地区分正常波动和真实故障。原始文档中提到的从无监督切换到半监督正是基于实际测试中发现纯健康数据训练的模型对微小变化过于敏感的问题。2.2 硬件平台选型为什么是i.MX RT1050NXP的i.MX RT1050是一款跨界MCU它拥有Cortex-M7内核主频高达600MHz并配备了充足的片上RAM512KB和外部存储接口。选择它主要基于以下几点性能充足600MHz的主频足以应对K-Means模型实时推理、传感器数据读取I2C和预处理滤波、RMS计算的算力需求。内存与生态其内存配置能够容纳TensorFlow Lite Micro运行时库、我们的模型以及应用程序。此外NXP提供的eIQ边缘智能软件开发环境包含了针对其芯片优化的TensorFlow Lite Micro库和丰富的示例极大地简化了开发流程。集成传感器评估板EVKB上集成了FXOS8700CQ加速度计这是一个支持I2C/SPI的数字传感器方便我们直接采集振动数据无需额外硬件。2.3 软件与工具链的搭建思路整个项目涉及两套环境PC端Python训练环境和嵌入式端C/C部署环境。PC端训练与转换核心TensorFlow用于模型训练和保存。数据采集与处理pyserial通过串口从板卡读取数据、pandas/numpy数据处理、xlwt写入Excel用于原始数据存档和分析。科学计算与可视化scipy信号处理如FFT、matplotlib绘图分析。模型转换TensorFlow Lite Converter将.pb模型转为.tflitexxd工具将.tflite转为C语言头文件。嵌入式端部署与推理核心NXP MCUXpresso SDK提供硬件驱动、RTOS支持 eIQ TensorFlow Lite Micro库提供模型推理API。开发环境可以是MCUXpresso IDE、IAR或Keil。关键步骤将转换得到的converted_model.h头文件内含模型数组加入工程调用TFLite Micro的API进行推理。这种分离的设计是边缘AI项目的典型模式利用PC的强大算力进行模型训练和验证然后将训练好的轻量化模型部署到资源受限的设备端执行。3. 从数据到特征传感器数据的采集与处理实战3.1 硬件连接与数据采集固件第一步是让板卡上的传感器动起来。我们将i.MX RT1050 EVKB固定在台式风扇的外壳上用以采集风扇运行时的振动信号。FXOS8700CQ加速度计通过I2C总线与MCU通信。在SDK中我们需要编写或使用一个数据采集应用程序。这个程序的核心任务是以固定的采样率本例中为400Hz读取加速度计的X、Y、Z三轴原始数据并通过UART串口实时发送到PC。这里有一个关键配置采样率。根据奈奎斯特采样定理要无失真地还原信号采样频率必须大于信号最高频率的2倍。通过后续的频域分析我们发现风扇振动的主要频率成分在100-300Hz之间因此400Hz的采样率是满足要求的。实操心得在编写采集程序时最好加入时间戳或帧计数器。这样当数据在PC端接收时即使因为串口缓冲或PC处理延迟导致数据流短暂中断也能通过序号发现并处理数据丢失的问题这对于后续的数据对齐和分析至关重要。3.2 PC端数据收集脚本的编写与使用在PC端我们使用Python脚本Data_Collection.py来接收串口数据。这个脚本需要完成以下工作配置串口设置与板卡输出匹配的波特率、数据位、停止位等如115200, 8N1。数据解析板卡发送的数据通常是文本格式如“ACC: X123, Y456, Z789\n”。脚本需要逐行读取并用字符串分割等方法提取出三个轴的整数值。数据存储将解析出的数据实时写入一个Excel文件.xls。为了防止程序意外崩溃导致数据全部丢失每1分钟自动保存一次文件是一个非常好的实践。注入异常在脚本运行期间我们需要人为制造一些异常。例如用手轻轻敲击风扇或板卡模拟瞬时冲击或者轻微改变风扇的平衡模拟持续的不对中状态。这些操作及其发生的大致时间需要被记录下来用于后续给数据打标签虽然K-Means是无监督学习但评估模型时需要知道哪些是真正的异常。# Data_Collection.py 核心逻辑示意 import serial import xlwt import time # 配置串口和采集时长 ser serial.Serial(COM3, 115200, timeout1) collection_time_seconds 3600 # 采集1小时 save_interval 60 # 每60秒保存一次 workbook xlwt.Workbook() sheet workbook.add_sheet(Vibration_Data) sheet.write(0, 0, Timestamp) sheet.write(0, 1, X) sheet.write(0, 2, Y) sheet.write(0, 3, Z) row 1 last_save_time time.time() start_time time.time() while time.time() - start_time collection_time_seconds: line ser.readline().decode(ascii, errorsignore).strip() if line.startswith(ACC:): # 解析数据例如: ACC: X12345, Y-567, Z10234 parts line.split(,) x_val int(parts[0].split()[1]) y_val int(parts[1].split()[1]) z_val int(parts[2].split()[1]) current_time time.time() sheet.write(row, 0, current_time) sheet.write(row, 1, x_val) sheet.write(row, 2, y_val) sheet.write(row, 3, z_val) row 1 # 定时保存 if current_time - last_save_time save_interval: workbook.save(vibration_data.xls) last_save_time current_time print(fData saved at row {row}) workbook.save(vibration_data_final.xls) ser.close()3.3 数据的时域与频域分析收集到原始数据后不能直接扔给模型。我们必须先“读懂”数据。这里主要进行两种分析时域分析直接在Excel或用matplotlib绘制三轴加速度随时间变化的曲线。观察健康状态和人为制造异常时波形的幅值、均值是否有明显变化。一个直观且有效的时域特征是均方根值它反映了振动能量的平均水平。计算RMS的窗口大小需要实验确定原文中使用了10个样本的窗口。频域分析这是理解振动信号的关键。我们编写脚本对原始信号进行快速傅里叶变换将时域信号转换为频域。观察频谱图可以找到风扇旋转的基频及其谐波。健康的设备频谱通常比较“干净”特征频率突出。而出现异常时如松动、碰磨可能会引入新的频率成分或者导致某些频率的幅值显著增大。FFT分析帮助我们确定有效的滤波频带。通过频域分析我们确认了主要振动能量集中在100-300Hz。同时我们也可能发现一些高频噪声。这引出了下一步滤波。3.4 数据预处理低通滤波与特征提取原始加速度数据中不可避免地包含高频噪声如电路噪声、环境干扰。为了突出我们关心的低频振动特征需要进行滤波。在嵌入式端进行实时滤波需要选择计算量小的算法。移动平均滤波器是一个非常好的选择它本质上是一个低通滤波器且计算只需加法和除法。原文中采用了5点移动平均A_n (x_n x_{n-1} x_{n-2} x_{n-3} x_{n-4}) / 5。在PC上用Python的pandas或numpy可以轻松实现在嵌入式C代码中维护一个长度为5的循环缓冲区即可。滤波之后我们就需要进行特征提取。直接将2000多个原始数据点输入模型不仅维度太高而且包含大量冗余信息。我们提取每个轴数据滑动窗口的RMS值作为特征。RMS计算本身也有平滑作用并能代表该窗口内的振动能量。假设我们采集了N个原始样本。经过5点移动平均滤波后得到N-4个滤波后样本。再以10个样本为窗口计算RMS最终得到(N-4)/10个特征点。原文中经过实验确定需要200个特征点作为模型输入因此反向推导出需要采集的原始样本数N 200 * 10 4 2004约为2005个。这个“2005”就是模型输入层的固定尺寸在后续嵌入式推理时必须保证。4. 模型训练、转换与验证的完整流程4.1 使用TensorFlow训练K-Means模型在PC端我们使用kmeans_development_training.py脚本来训练模型。其核心步骤是数据加载与预处理读取之前收集的多个Excel文件对每个文件中的X、Y、Z轴数据分别进行我们定义好的预处理流程移动平均滤波 - 计算RMS得到三组特征序列。K-Means聚类对每一轴的特征数据使用sklearn的KMeans或TensorFlow的tf.compat.v1.estimator.experimental.KMeans进行聚类。我们设定聚类数K3这个数字可以调整代表了我们认为“正常”状态可能存在的几种细微差异模式。训练完成后算法会给出3个聚类中心。保存中心点训练的目标就是得到这3个中心点的坐标。对于X、Y、Z三个轴我们会分别得到3个中心点共计9个值。这9个值就是我们的“模型”核心参数。# kmeans_development_training.py 核心逻辑示意 import numpy as np from sklearn.cluster import KMeans # 假设 processed_features_x 是X轴预处理后的特征数据形状为 (n_samples, 1) kmeans_x KMeans(n_clusters3, random_state0).fit(processed_features_x) centroids_x kmeans_x.cluster_centers_.flatten() # 得到3个中心点值 print(fX-axis centroids: {centroids_x}) # 同理训练Y轴和Z轴4.2 构建并保存TensorFlow推理图得到9个中心点后我们需要构建一个完整的TensorFlow计算图它的功能是输入一个新的数据点单个轴的RMS值计算该点到该轴3个中心点的距离并输出这些距离。这个计算图将被保存为TensorFlow的SavedModel格式.pb文件。这是通过Export_trained_data_model.py脚本完成的。脚本中我们手动定义这9个中心点为常量然后构建计算图对于输入的一个batch的数据分别计算其与三个中心点的欧氏距离即差的平方然后输出一个包含3个距离值的张量。# Export_trained_data_model.py 核心逻辑示意 import tensorflow as tf import numpy as np # 1. 定义中心点常量来自上一步训练结果 centroids_x_tf tf.constant([48925, 54109, 56718], dtypetf.float32) # 示例值 # ... 同理定义Y轴和Z轴中心点 # 2. 定义输入占位符 (假设我们一次处理一个轴的一组数据) input_data tf.compat.v1.placeholder(tf.float32, shape[None, 1], nameinput) # 3. 计算输入数据到每个中心点的距离 (欧氏距离的平方避免开方节省计算) # 利用广播机制input_data shape: [batch, 1], centroids shape: [3] - 相减后得到 [batch, 3] distances tf.square(input_data - centroids_x_tf) # shape: [batch, 3] # 4. 保存模型 with tf.compat.v1.Session() as sess: # 保存计算图定义和变量这里变量就是常量中心点 tf.compat.v1.saved_model.simple_save( sess, ./saved_model, inputs{input: input_data}, outputs{distances: distances} )重要提示原始文档中提到了一个关键问题tf.square算子在当时eIQ SDE所支持的TensorFlow Lite版本中并非内置算子。解决方案是将其替换为乘法distances (input_data - centroids_x_tf) * (input_data - centroids_x_tf)。这是边缘部署中常遇到的算子兼容性问题必须根据目标部署框架支持的算子列表来调整模型结构。4.3 模型转换从SavedModel到嵌入式头文件保存好的.pb模型需要经过两步转换才能被C程序使用转换为TensorFlow Lite格式使用conversion.py脚本调用TFLiteConverter加载SavedModel并将其转换为FlatBuffer格式的.tflite文件。这个文件是跨平台的包含了模型结构和参数。# conversion.py 核心逻辑 import tensorflow as tf converter tf.lite.TFLiteConverter.from_saved_model(./saved_model) tflite_model converter.convert() with open(./converted_model.tflite, wb) as f: f.write(tflite_model)转换为C头文件使用xxd工具或类似的二进制转十六进制数组工具将.tflite文件内容转换为一个C语言数组。xxd -i ./converted_model.tflite ./converted_model.h生成的converted_model.h文件内容类似unsigned char converted_model_tflite[] { 0x1c, 0x00, 0x00, 0x00, 0x54, 0x46, 0x4c, 0x33, // ... 二进制数据 }; unsigned int converted_model_tflite_len 1234;关键修改需要将unsigned char改为const char因为这个数组是常量应该存放在Flash中而非RAM。const char converted_model_tflite[] {...};4.4 模型验证在PC端测试TFLite模型在把模型烧录到板卡之前必须在PC上用TensorFlow Lite的Python API验证转换后的模型是否正确。check_tflite.py脚本就是干这个的。加载模型使用tf.lite.Interpreter加载.tflite文件。分配张量调用allocate_tensors()。设置输入将一组测试数据例如模拟的200个RMS特征值放入输入张量。运行推理调用invoke()方法。获取输出从输出张量中读取3个距离值。手动验证用同样的测试数据和中心点手动计算一遍欧氏距离的平方与模型输出对比。两者一致说明模型转换和加载无误。这一步至关重要它能确保问题出现在嵌入式端之前模型本身和转换过程是正确的。5. 嵌入式端应用程序开发与部署详解5.1 工程搭建与模型集成在MCUXpresso IDE或你喜欢的嵌入式开发环境中创建一个基于NXP SDK和eIQ TFLite Micro库的新工程。关键步骤如下添加模型头文件将修改后的converted_model.h文件添加到工程的源文件目录中。包含TFLite Micro库在工程设置中正确添加eIQ提供的TFLite Micro库的头文件路径和链接库。这些通常包含在SDK的中间件组件里。配置内存确保链接脚本为TFLite的tensor_arena用于存储中间激活张量的大型内存池分配了足够且对齐的RAM空间。这是嵌入式AI推理内存管理的核心。5.2 推理引擎的初始化与运行在应用程序的main.c或main.cpp中需要编写模型推理的代码。以下是核心流程的C代码示意#include tensorflow/lite/micro/all_ops_resolver.h #include tensorflow/lite/micro/micro_interpreter.h #include tensorflow/lite/schema/schema_generated.h #include converted_model.h // 我们生成的头文件 // 1. 定义操作解析器告诉TFLite Micro我们用了哪些算子 tflite::AllOpsResolver resolver; // 2. 从数组加载模型 const tflite::Model* model ::tflite::GetModel(converted_model_tflite); if (model-version() ! TFLITE_SCHEMA_VERSION) { // 错误处理模型版本不兼容 } // 3. 分配Tensor Arena这是最大的一块内存开销 const int tensor_arena_size 10 * 1024; // 根据模型调整例如10KB uint8_t tensor_arena[tensor_arena_size] __attribute__((aligned(16))); // 4. 创建解释器 tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, tensor_arena_size); interpreter.AllocateTensors(); // 分配输入输出张量内存 // 5. 获取输入输出张量指针 TfLiteTensor* input interpreter.input(0); TfLiteTensor* output interpreter.output(0); // 6. 准备输入数据 (假设input_data是预处理好的200个float数据) float* input_data_ptr input-data.f; for (int i 0; i 200; i) { input_data_ptr[i] preprocessed_features[i]; } // 7. 运行推理 TfLiteStatus invoke_status interpreter.Invoke(); if (invoke_status ! kTfLiteOk) { // 错误处理推理失败 } // 8. 处理输出 (output-data.f 指向包含3个距离值的数组) float* distances output-data.f; float min_distance distances[0]; for (int i 1; i 3; i) { if (distances[i] min_distance) { min_distance distances[i]; } } // 9. 异常判断如果最小距离大于阈值则判定为异常 float anomaly_threshold 1000.0f; // 阈值需要根据训练数据确定 if (min_distance anomaly_threshold) { printf(ANOMALY DETECTED!\n); } else { printf(HEALTHY\n); }5.3 实时数据流与预处理在MCU上的实现嵌入式端的挑战在于我们需要在实时流数据中复现PC端训练时的预处理流程。这需要一个滑动窗口机制。数据采集循环在定时器中断或主循环中以1.5ms的间隔对应约666Hz的采样率读取FXOS8700的加速度数据存入一个长度为2005的环形缓冲区。实时预处理移动平均滤波维护一个长度为5的滑动窗口计算当前及前4个样本的平均值输出一个滤波后的样本流。RMS计算维护一个长度为10的滑动窗口存储滤波后的数据。每当有新数据填入就计算这10个数据的RMS值。这个RMS值就是我们要的“特征点”。特征缓冲区将计算出的RMS值依次存入另一个长度为200的缓冲区。触发推理当RMS缓冲区填满200个值时将其拷贝到TFLite模型的输入张量中触发一次模型推理。结果输出根据推理输出的距离判断是否异常并通过串口打印或点亮LED等方式告警。整个流程必须精心设计时序确保数据采集、预处理和推理的总时间小于采样间隔否则会导致数据丢失。5.4 阈值设定与系统调优模型输出的是新数据点到三个聚类中心的距离。如何根据距离判断异常这就需要设定一个阈值。阈值确定在PC端验证阶段用一部分未参与训练的“健康”测试数据输入模型得到一组“健康距离”。统计这组距离的分布如均值μ和标准差σ阈值可以设定为μ n*σ例如n3。同时用“异常”测试数据验证这个阈值的有效性查准率和查全率。调优实践阈值不是一成不变的。在实际部署中可能需要根据设备的老化、季节变化温度影响进行微调。一种更高级的做法是实现在线自适应但初期可以提供一个通过串口命令修改阈值的接口方便现场调试。6. 实战中遇到的典型问题与解决方案在将这套系统跑通的过程中我遇到了不少坑。这里把关键问题和解决方案记录下来希望能帮你节省时间。6.1 算子兼容性问题与变通方案这是边缘部署TensorFlow模型最常见的问题。如前所述eIQ SDE当时基于的TFLite Micro版本可能不支持某些算子。问题模型转换成功但在MCU上初始化或推理时失败提示找不到tf.square算子。解决方案替换算子在构建SavedModel时避免使用不支持的算子。对于平方运算用乘法x*x代替tf.square(x)。查阅官方列表始终参考你所使用的TFLite Micro版本支持的算子列表。自定义算子对于无法替换的复杂算子可以实现TFLite Micro的TfLiteRegistration接口进行自定义但这需要较强的嵌入式开发和TFLite内部知识。6.2 输入输出张量维度不匹配问题在PC端用check_tflite.py测试正常但在MCU上获取到的输入/输出张量维度不对或者数据乱码。排查步骤打印张量信息在MCU代码中初始化后立即打印input-dims-data和output-dims-data确认维度是否为预期的[1, 200]batch1, 特征数200和[1, 3]。检查数据布局TFLite默认是NHWC虽然对一维数据影响不大但确保你的数据填充顺序正确。核对预处理确保MCU上的预处理滤波窗口大小、RMS窗口大小与PC训练时完全一致。一个样本点的偏差都会导致特征序列错位结果毫无意义。检查数据类型确认模型输入是float32而你填充的数据也是float类型。整型数据需要先转换为float。6.3 内存不足与性能优化问题程序运行崩溃或推理时间过长无法满足实时性要求。解决方案优化Tensor Arena大小使用interpreter.arena_used_bytes()在初始化后打印实际内存使用量然后尽可能精确地设置tensor_arena_size避免浪费。可以留出10-20%的余量。量化模型如果使用浮点模型导致速度慢或内存大可以考虑在PC端训练后进行训练后量化将float32权重和激活转换为int8。这能显著减少模型体积、提升推理速度但可能会轻微损失精度。TFLite Converter支持这一功能。使用CMSIS-NN库如果NXP的eIQ库集成了ARM的CMSIS-NN内核确保在工程中启用。它为Cortex-M系列处理器高度优化了神经网络算子尽管K-Means用不上但为未来升级到神经网络模型做准备。调整采样率和特征长度如果实时性始终无法满足可以考虑降低采样率需确保仍满足奈奎斯特定理或者减少RMS计算窗口如从10减到8从而减少模型所需的输入特征数量如从200减到160。但这需要重新训练模型。6.4 传感器数据不稳定与滤波增强问题在静止状态下加速度计读数仍有微小跳动导致RMS特征存在基线噪声可能引发误报。解决方案校准传感器上电后在设备静止时采集一段数据计算三轴的零点偏移零偏在后续读数中减去这个偏移。优化滤波器尝试不同的滤波算法或参数。例如可以尝试一阶低通数字滤波器y[n] α * x[n] (1-α) * y[n-1]其计算量也很小且滤波效果可调。在特征层面增加阈值除了模型输出的距离阈值还可以对RMS特征本身设定一个合理范围过滤掉明显不合理的极端值可能是传感器故障或强干扰。整个项目从数据采集到嵌入式部署是一个典型的软硬件协同设计过程。最大的体会是嵌入式AI项目成功的关键往往不在于算法有多高级而在于对数据流的精准把控、对资源约束的深刻理解以及大量的、细致的调试工作。每一个环节——从传感器的焊接稳固性、采样时序的精确性到预处理代码与训练脚本的一致性再到模型转换的每一步验证——都需要极高的耐心和严谨性。当风扇正常运转串口稳定输出“HEALTHY”而轻轻一敲立刻变为“ANOMALY DETECTED”时那种将智能赋予小小芯片的成就感正是嵌入式开发的魅力所在。