本指南面向工业振动监控的初学者,手把手教你用Python完成数据采集、特征工程、异常检测模型训练,并最终将方案部署到STM32等边缘设备。核心算法采用隔离森林(Isolation Forest),兼顾精度与轻量。
1. 采样与窗口设定:让数据"看得清"故障
1.1. 三轴采样与设备频率的关系
电机振动通常采集X(水平)、Y(垂直)、Z(轴向)三轴加速度。窗口长度(即一次分析的时间片段)的设定必须与设备转频挂钩,否则要么漏掉低频故障,要么统计特征不稳。
关键公式:
- 转频 f0=转速 (RPM)60f_0 = \frac{\text{转速 (RPM)}}{60}f0=60转速 (RPM) (Hz)
- 窗口长度 TwindowT_{\text{window}}Twindow 内应包含至少 KKK 个旋转周期,推荐 K≥10K \ge 10K≥10:
Twindow≥Kf0T_{\text{window}} \ge \frac{K}{f_0}Twindow≥f0K
举例:
- 电机 1500 RPM → f0=25Hzf_0 = 25\text{Hz}f0=25Hz,周期 0.04s → Twindow≥10×0.04=0.4sT_{\text{window}} \ge 10\times0.04 = 0.4\text{s}Twindow≥10×0.04=0.4s,工程取 1s。
- 电机 300 RPM → f0=5Hzf_0 = 5\text{Hz}f0=5Hz,周期 0.2s → Twindow≥2sT_{\text{window}} \ge 2\text{s}Twindow≥2s。
1.2 窗口长度的工程权衡
| 窗口过短 | 窗口过长 |
|---|---|
| ❌ 无法分析低于 1/T1/T1/T 的低频故障 | ✅ 可捕捉极低频成分 |
| ❌ 峭度、偏度等统计量波动大,易误报 | ✅ 特征稳定,抗噪强 |
| ✅ 响应快,预警及时 | ❌ 预警延迟大 |
| ❌ FFT频率分辨率低(Δf=1/T\Delta f = 1/TΔf=1/T) | ❌ 计算量大,内存占用高 |
推荐 :对于工业电机(600~3000 RPM),取 1秒窗口。转速低于600 RPM时,适当延长至2秒。
1.3 Python实现:滑动窗口切分
python
import numpy as np
def sliding_window(data, window_size_sec, fs, step_sec=None):
"""
data: 原始一维振动信号 (numpy array)
window_size_sec: 窗口长度(秒)
fs: 采样率 (Hz)
step_sec: 步进长度(秒),默认等于窗口长度(不重叠)
"""
win_len = int(window_size_sec * fs)
step = win_len if step_sec is None else int(step_sec * fs)
return [data[i:i+win_len] for i in range(0, len(data)-win_len+1, step)]
2. 特征工程:把振动波形变成故障指纹
特征工程是决定隔离森林成败的核心。我们将提取时域+频域共12~16维特征,并给出公式和解释。
2.1 时域特征(对冲击、磨损敏感)
设窗口内有 NNN 个采样点 xix_ixi,均值为 μ\muμ,标准差为 σ\sigmaσ。
| 特征 | 公式 | 物理意义 |
|---|---|---|
| 均方根 (RMS) | 1N∑xi2\sqrt{\frac{1}{N}\sum x_i^2}N1∑xi2 | 振动能量,反映整体磨损、不平衡 |
| 峰值 (Peak) | max∣xi∣\max \vert x_i \vertmax∣xi∣ | 最大瞬时冲击 |
| 峰峰值 (P-P) | max(xi)−min(xi)\max(x_i) - \min(x_i)max(xi)−min(xi) | 振动摆幅 |
| 峭度 (Kurtosis) | 1N∑(xi−μσ)4\frac{1}{N}\sum\left(\frac{x_i-\mu}{\sigma}\right)^4N1∑(σxi−μ)4 | 冲击敏感,早期轴承点蚀的黄金指标(正常≈3,>4报警) |
| 偏度 (Skewness) | 1N∑(xi−μσ)3\frac{1}{N}\sum\left(\frac{x_i-\mu}{\sigma}\right)^3N1∑(σxi−μ)3 | 不对称性,反映摩擦、不对中 |
| 波形因子 | RMS∣均值∣\frac{RMS}{\vert \text{均值} \vert }∣均值∣RMS | 检测脉冲,对负载变化不敏感 |
| 脉冲因子 | Peak∣均值∣\frac{Peak}{\vert \text{均值}\vert }∣均值∣Peak | 类似波形因子,但对极端值更敏感 |
2.2 频域特征(定位具体故障源)
需要对每个窗口做FFT,得到幅值谱 ∣X[k]∣|X[k]|∣X[k]∣,频率分辨率 Δf=fs/N\Delta f = f_s / NΔf=fs/N。
特定频带能量(重点)
公式 :
E[flow,fhigh]=∑k=klowkhigh∣X[k]∣2E_{[f_{low}, f_{high}]} = \sum_{k=k_{low}}^{k_{high}} |X[k]|^2E[flow,fhigh]=k=klow∑khigh∣X[k]∣2
其中 klow=⌊flow/Δf⌋k_{low} = \lfloor f_{low}/\Delta f \rfloorklow=⌊flow/Δf⌋,khigh=⌈fhigh/Δf⌉k_{high} = \lceil f_{high}/\Delta f \rceilkhigh=⌈fhigh/Δf⌉。
典型频带 (假设当前转频为 f0f_0f0):
- 1×f01\times f_01×f0:不平衡
- 2×f02\times f_02×f0:不对中、松动
- 0.5×f00.5\times f_00.5×f0:机械松动、摩擦
- 高频段(如 2k~5kHz):轴承早期点蚀
- 边频带(以齿轮啮合频率/轴承故障频率为中心,宽度 ±2f0f_0f0):调制故障
Python实现:
python
def band_energy(fft_mag, fs, f_low, f_high):
"""fft_mag: 单边幅值谱(长度 N//2+1)"""
delta_f = fs / (2 * (len(fft_mag)-1))
k_low = int(f_low / delta_f)
k_high = int(f_high / delta_f)
return np.sum(fft_mag[k_low:k_high+1] ** 2)
全局频域特征
| 特征 | 公式 | 意义 |
|---|---|---|
| 重心频率 | fc=∑fkPk∑Pkf_c = \frac{\sum f_k P_k}{\sum P_k}fc=∑Pk∑fkPk | 频谱能量中心,反映刚度变化 |
| 频率方差 | ∑(fk−fc)2Pk∑Pk\frac{\sum (f_k-f_c)^2 P_k}{\sum P_k}∑Pk∑(fk−fc)2Pk | 能量分散程度 |
| 总能量 | ∑Pk\sum P_k∑Pk | 全频带振动烈度 |
(Pk=∣X[k]∣2P_k = |X[k]|^2Pk=∣X[k]∣2 为功率)
2.3 工况特征(必须包含)
- 转速 (RPM) -- 来自编码器或变频器
- 负载 (%) -- 电流或扭矩信号
工况特征能让模型区分"转速升高导致振动变大"和"故障导致振动变大"。
3. 特征分布分析:不做"睁眼瞎"
为什么要分析特征分布?
- 大多数统计模型(如3-sigma控制图)假设数据服从正态分布,而振动特征(如RMS、峭度)往往是对数正态 或长尾分布。无视分布直接设阈值,误报率极高。
- 分布形态的漂移(均值、方差、偏度变化)是设备退化的早期信号,比超阈值更灵敏。
3.1 分布分析的具体用途
| 分析工具 | 作用 |
|---|---|
| 直方图 + 核密度估计 (KDE) | 直观查看分布形态(单峰/多峰、偏斜、长尾) |
| Q-Q图 | 判断是否服从正态/对数正态分布 |
| 偏度、峰度值 | 量化偏离程度:偏度>1强右偏,峰度>3尖峰 |
| KL散度 (Kullback-Leibler) | 量化健康分布与当前分布的差异,用于趋势预警 |
3.2 如何用于特征选择?
- 剔除常数特征:分布退化为单点(方差=0),无信息量。
- 剔除极端离群分布:某个特征在健康数据中就已呈现多峰或极长尾,说明可能混入了不同工况,需分段处理或剔除。
- 决定是否做变换:若RMS呈对数正态,取对数后再标准化,模型更稳定。
3.3 Python示例
python
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.stats import probplot
# 假设 df 包含所有窗口的RMS特征
sns.histplot(df['rms'], kde=True)
probplot(df['rms'], dist='norm', plot=plt) # Q-Q图
4. 特征排序:找到"最有价值"的特征
隔离森林本身可以输出特征重要性(基于平均路径深度减少量),但在训练前,我们可以先用过滤式方法快速排序,剔除无效特征。
4.1 常用排序方法
| 方法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 方差阈值 | 方差 < 0.01 的特征剔除 | 极快 | 只能剔除常数特征 |
| 互信息 (Mutual Information) | 衡量特征与标签(如果有)的非线性相关性 | 不依赖模型 | 需要标签(无监督场景慎用) |
| 基于隔离森林的特征重要性 | 训练后查看 model.feature_importances_ |
与最终模型一致 | 需先训练一次 |
| 主成分分析 (PCA) 载荷 | 第一主成分的系数绝对值 | 降维同时排序 | 线性假设 |
4.2 推荐流程(无监督场景)
- 计算每个特征的方差,剔除方差为0的特征。
- 计算特征之间的相关性矩阵(皮尔逊),对于相关系数>0.95的组,保留其中一个(参见第五节)。
- 用全部剩余特征训练一个临时隔离森林,输出特征重要性,排序后保留前15~20个。
python
from sklearn.ensemble import IsolationForest
iso = IsolationForest(contamination=0.01, random_state=42)
iso.fit(X_train) # X_train 为标准化后的特征矩阵
importance = iso.feature_importances_ # 注意:scikit-learn 0.22+ 支持
sorted_idx = np.argsort(importance)[::-1]
5. 特征相关性分析(皮尔逊相关系数):去冗余 & 动态监测
5.1 皮尔逊相关系数公式
对于两个特征 X,YX, YX,Y:
r=∑(xi−xˉ)(yi−yˉ)∑(xi−xˉ)2∑(yi−yˉ)2r = \frac{\sum (x_i - \bar{x})(y_i - \bar{y})}{\sqrt{\sum (x_i - \bar{x})^2 \sum (y_i - \bar{y})^2}}r=∑(xi−xˉ)2∑(yi−yˉ)2 ∑(xi−xˉ)(yi−yˉ)
取值范围 [−1,1][-1, 1][−1,1],绝对值越接近1,线性相关性越强。
5.2 用途一:特征筛选(去冗余)
问题:RMS和峰峰值常常高度相关(r>0.95),同时保留会使隔离森林的随机切分偏向这个冗余方向,降低效率。
做法:设定阈值(如0.95),对相关性矩阵中的高相关组,每组只保留一个特征(保留与目标变量相关性最高或方差最大的)。
python
def remove_correlated_features(df, threshold=0.95):
corr_mat = df.corr().abs()
upper_tri = corr_mat.where(np.triu(np.ones(corr_mat.shape), k=1).astype(bool))
to_drop = [column for column in upper_tri.columns if any(upper_tri[column] > threshold)]
return df.drop(columns=to_drop)
5.3 用途二:动态关系监测(高级异常)
在电机健康状态下,计算两个特征(如转速 vs RMS)的相关系数 rhealthyr_{healthy}rhealthy。在线运行时,滑动窗口实时计算 rcurrentr_{current}rcurrent。如果 ∣rcurrent−rhealthy∣>0.3|r_{current} - r_{healthy}| > 0.3∣rcurrent−rhealthy∣>0.3,说明系统动态特性改变(如联轴器松动、负载突变),即使单个特征正常也应报警。
5.4 用途三:故障溯源
当隔离森林报告异常后,将当前窗口的特征与历史故障特征库(每个故障类型对应一组典型特征)做皮尔逊相关,相关性最高的故障类型即为诊断结果。
6. 隔离森林模型训练:边缘上的哨兵
6.1 数据量要求
| 场景 | 建议数据量(训练集) |
|---|---|
| 最小可用 | 200~500 条 |
| 工程推荐 | 1000~5000 条 |
| 高稳定性 | 5000+ 条 |
换算:1秒窗口,30分钟健康数据 = 1800条,完全够用。
6.2 训练步骤(Python)
python
from sklearn.ensemble import IsolationForest
from sklearn.preprocessing import StandardScaler
import numpy as np
# 1. 加载健康数据(无故障)的特征矩阵 X_healthy (n_samples, n_features)
# 假设已经完成特征工程,得到 X_healthy
# 2. 标准化(必须!否则大数值特征主导切分)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_healthy)
# 3. 训练隔离森林
model = IsolationForest(
n_estimators=100, # 树的数量,50~100足够
max_samples='auto', # 每棵树采样数,默认256
contamination=0.01, # 预期异常比例(健康数据中一般<1%)
random_state=42,
bootstrap=False
)
model.fit(X_scaled)
# 4. 保存模型和标准化器(用于后续部署)
import joblib
joblib.dump(model, 'iso_model.pkl')
joblib.dump(scaler, 'scaler.pkl')
6.3 参数调优建议
n_estimators:越大越稳定,但模型也越大。边缘设备用50,云端用100。contamination:设一个略高的值(如0.05)以降低漏报,后续用阈值调节。max_samples:数据量<10000时保持默认"auto"(=256);数据量大时可适当增大。
6.4 模型评估(无监督)
没有标签时,可通过以下方式间接评估:
- 重构误差:将正常验证集输入,看异常分数分布是否集中。
- 注入人工异常:在健康信号中加入冲击、噪声,看模型能否检出。
- 时序一致性:连续窗口的异常分数不应剧烈跳变,否则模型不稳定。
7. 从Python到MCU:轻量化部署要点
7.1 模型压缩
- 树数量减至
n_estimators=30~50 - 最大深度
max_depth=8~10(限制树的生长) - 模型大小可控制在 10~25 KB(STM32完全可承受)
7.2 特征计算优化
- FFT使用CMSIS-DSP库(ARM官方优化)
- 频带能量预计算索引,避免重复计算边界
- 时域特征采用递推算法(如RMS滑动更新)降低计算量
7.3 C++推理示例(伪代码)
cpp
#include "isolation_forest.h" // 假设已有模型加载库
float features[12]; // 实时计算的特征
float anomaly_score = iso_forest_predict(features); // 返回异常分数(负值越大越异常)
if (anomaly_score < -0.3) { // 阈值需根据Python调优确定
trigger_alert();
}
8. 完整工作流程图
采集原始振动信号
滑动窗口分片
时域特征提取
FFT + 频域特征提取
合并特征向量
特征分布分析与标准化
相关性分析去冗余
训练隔离森林模型
保存模型 + 标准化器
Python验证
转换为C++模型
部署到MCU实时推理
9. 常见问题与避坑指南
| 问题 | 解决方案 |
|---|---|
| 特征超过50维,模型效果变差 | 用相关性分析 + 特征重要性筛选到15维以内 |
| 训练数据只有几百条 | 使用滑动窗口重叠(步长0.5秒)扩展样本,或做数据增强(加噪声、缩放) |
| 转速变化大,频带能量不靠谱 | 动态更新频带边界:根据实时转速重新计算1x, 2x等频带的频率范围 |
| MCU内存不足 | 限制树的数量和深度,将float64转为float32,甚至量化到int16 |
| 误报率高 | 1)降低contamination;2)提高异常分数阈值;3)增加峭度等确认逻辑(连续3个窗口报警才上报) |
最后一句:先从Python跑通流程,用你自己的电机数据验证,再移植到MCU。不要一上来就追求复杂模型,隔离森林+15个精心挑选的特征,已经能解决80%的工业异常检测问题。
附:FFT泄漏问题分析与解决方案(工程实战篇)
在电机振动分析中,FFT是提取频域特征的核心工具。但初学者常遇到一个"隐形杀手"------频谱泄漏。它会让本该尖锐的谱线变得"拖尾"、能量扩散到相邻频点,导致频带能量计算严重失真,最终让隔离森林模型学到错误特征。
什么是FFT泄漏?
FFT泄漏 是指:当信号的频率不是FFT频率分辨率 Δf=fs/N\Delta f = f_s / NΔf=fs/N的整数倍时,信号能量会"扩散"到相邻的多个频率分量上,导致频谱变宽、幅值降低、相位失真。
产生原因(直观理解)
FFT 默认对时域信号进行周期性延拓 。如果你的采样窗口内不是整数个信号周期,延拓后的波形在连接处会出现突变(不连续)。这个突变等效于一个高频脉冲,其频谱会布满整个频域,从而污染真实的谱线。
数学表达
设信号 x(t)=Acos(2πf0t)x(t) = A \cos(2\pi f_0 t)x(t)=Acos(2πf0t),采样后做 N 点 FFT。若 f0=m⋅Δff_0 = m \cdot \Delta ff0=m⋅Δf(m 为整数),则谱线正好落在第 m 根谱线上,无泄漏。
若 f0=(m+δ)⋅Δff_0 = (m + \delta) \cdot \Delta ff0=(m+δ)⋅Δf(0<δ<10<δ<10<δ<1),则能量会泄漏到第 m、m+1 及更远的谱线上。δ 越接近 0.5,泄漏越严重。
对电机振动分析的影响
- 1×转频频带能量:计算值偏小,可能误判为"故障减弱"。
- 边频带能量:微弱的调制边带被泄漏淹没,无法识别轴承/齿轮早期故障。
- 重心频率:向低频方向漂移。
解决方案(恒速电机场景)
方案一:加窗(最通用,强烈推荐)
原理:在时域对信号乘以一个两端平滑衰减到零的窗函数(如汉宁窗),强制消除边界突变。加窗后,泄漏的能量被约束在窗函数主瓣附近的有限范围内,旁瓣衰减极快。
数学本质:加窗相当于原信号的频谱与窗函数频谱做卷积。窗函数频域的主瓣宽度决定了频率分辨率,旁瓣高度决定了泄漏程度。汉宁窗的旁瓣衰减可达 -31 dB,比矩形窗(-13 dB)好得多。
对振动分析的适配:
- 时域特征(RMS、峭度、偏度等)必须在加窗前从原始信号计算。
- 频域特征(频带能量、重心频率等)使用加窗后信号的FFT。
- 频带能量不进行能量修正(因为后续模型只关心相对变化)。
方案二:整周期采样(恒速电机可选)
原理 :动态调整窗口长度 Twindow=K/f0T_{\text{window}} = K / f_0Twindow=K/f0(K 为整数个周期),使窗口内恰好包含整数个旋转周期,信号天然连续,无需加窗。
优点 :无泄漏,频率分辨率最高。
缺点:需要精确测速;窗口长度变化,不利于特征标准化;实现稍复杂。
结论 :对于恒速且转速非常稳定的电机,整周期采样是优雅的。但加窗法更鲁棒(不依赖精确测速,且能抑制噪声),建议作为首选。
Python 代码示例(加汉宁窗 + 原理注释)
python
import numpy as np
import matplotlib.pyplot as plt
# ========== 参数设置 ==========
fs = 1024 # 采样率 (Hz)
N = 1024 # FFT点数,也是窗口长度
t = np.arange(N) / fs
# 模拟恒速电机:转频 24.5 Hz(不是 Δf=1Hz 的整数倍,会产生泄漏)
f0 = 24.5
signal = np.sin(2 * np.pi * f0 * t)
# ========== 加窗处理 ==========
# 汉宁窗定义: w[n] = 0.5 - 0.5*cos(2πn/(N-1))
# 作用:让信号两端平滑到零,消除边界突变,抑制频谱泄漏
window = np.hanning(N) # 直接调用 numpy 生成
signal_win = signal * window # 时域相乘
# ========== 做 FFT ==========
# rfft 返回单边频谱(实信号)
X_no_window = np.fft.rfft(signal) # 不加窗的频谱(复数)
X_window = np.fft.rfft(signal_win) # 加窗后的频谱
# 转换为幅值谱(dB表示,便于观察泄漏的旁瓣)
mag_no = 20 * np.log10(np.abs(X_no_window) + 1e-10)
mag_win = 20 * np.log10(np.abs(X_window) + 1e-10)
freqs = np.fft.rfftfreq(N, 1/fs) # 对应的频率轴
# ========== 绘图对比 ==========
plt.figure(figsize=(10, 4))
plt.plot(freqs, mag_no, label='不加窗(矩形窗)', alpha=0.7)
plt.plot(freqs, mag_win, label='加汉宁窗', alpha=0.7)
plt.xlim(20, 30) # 聚焦在转频附近
plt.axvline(f0, color='red', linestyle='--', label=f'真实频率 {f0} Hz')
plt.xlabel('频率 (Hz)')
plt.ylabel('幅值 (dB)')
plt.title('FFT泄漏抑制效果对比(恒速电机 24.5Hz)')
plt.legend()
plt.grid(True)
plt.show()
# ========== 结果解释 ==========
# 不加窗时:能量向两侧扩散,24Hz和25Hz处都有明显幅值,真实峰值被压低。
# 加窗后:能量集中,主瓣变宽但旁瓣快速衰减,24.5Hz处出现清晰峰值。
# 对于频带能量特征(如23~27Hz),加窗后的累加值更能反映真实振动能量。
恒速电机下的实施建议
- 固定窗口长度:1秒(或包含10个以上周期)。即使转频不是Δf的整数倍,加窗也能保证频带能量特征稳定。
- 预处理顺序:原始信号 → 计算时域特征 → 加汉宁窗 → FFT → 计算频域特征。
- 不必刻意追求整周期采样:除非你的转速测量非常精确(误差<0.1%),否则加窗法更简单可靠。
- 验证泄漏是否被抑制:用上面的代码跑一下你的实际电机数据,观察转频附近频谱是否出现明显的"单峰"而非"平顶"。