机械故障敏感特征提取与决策树分类:基于Python的轴承数据集实践 📅 发布时间:2026/9/11 14:05:35 👁 浏览次数: 机械故障敏感特征提取加决策树分类这两个词拼在一起就是一套很典型的旋转机械故障诊断流程。最近我把渥太华大学的轴承公开数据集完整跑了一遍从原始振动信号里抽特征、筛敏感项再用决策树做分类整个流程用Python实现下来踩了不少坑也积累了不少经验。这篇文章就围绕这个项目把从数据准备、特征提取、特征筛选到决策树建模评估的完整过程梳理一遍代码可以直接拿去改换自己的数据集也能用。先说清楚这个项目适合谁。如果你是做设备健康管理、工业数据分析、故障诊断方向的学生或工程师这篇内容就是给你准备的。即使你是刚上手Python的初学者只要懂一点numpy、pandas和sklearn的基本操作跟着走也能把流程跑通。文章里不光是代码更重要的是每一步背后的取舍逻辑——为什么选这些特征、为什么用决策树、特征怎么才算“敏感”、模型效果不好往哪个方向调这些才是真正值钱的经验。1. 项目到底在解决什么问题1.1 机械故障诊断的核心逻辑机械设备在运转过程中轴承、齿轮、转子这些关键部件一旦出现磨损、裂纹、点蚀等故障最直观的表现就是振动信号发生变化。故障诊断本质上做的是两件事第一把“正常”和“异常”区分开第二把“异常”进一步细分成具体的故障类型比如内圈故障、外圈故障、滚动体故障。但振动信号这东西说白了就是一堆密密麻麻的时间序列采样点动辄几万几十万个点。直接把原始信号扔给分类器不仅计算量大而且噪声干扰严重分类效果往往很差。所以行业内标准的做法是先做特征提取——从原始信号中压缩出若干能刻画信号本质属性的数值指标再用这些指标作为输入去训练分类模型。特征提取的质量直接决定了故障诊断的上限。这里有一个非常关键的认知特征提取不是越多越好而是要“敏感”。所谓敏感特征就是在不同故障状态下取值差异显著、能够稳定区分各类别的特征。有些特征你算了一堆但它们在四种状态下数值几乎重合这种特征不但没帮助反而会干扰模型。所以“提取敏感特征”这个说法重点在“敏感”二字提取之后还得多一步筛选。1.2 为什么选渥太华大学的公开数据集做这个项目数据是一切的起点。渥太华大学的这个公开数据集在故障诊断圈子里被用得比较多网上有公开下载渠道数据可以通过校园公开资源或者学术数据集汇总站点获取。当然如果暂时拿不到用凯斯西储大学CWRU的轴承数据集也能做同样的流程思路完全一致。当时选它主要考虑几点数据是真实采集的振动信号不是仿真数据流程跑通后更有说服力。包含正常、外圈故障、内圈故障、滚动体故障等多个类别分类任务的典型性很高。采样频率、工况信息清晰方便做频谱分析验证。拿到数据后第一件事不是急着写代码而是先把数据结构搞清楚。一般解压后是一堆CSV或MAT格式的文件文件名里通常会标注故障类型、故障尺寸、转速、载荷等信息。先用几行代码扫一遍文件名和文件内容心里有数再往下走。1.3 为什么是“特征提取决策树”的组合有人可能会问现在深度学习那么火端到端的故障诊断一大堆干嘛还要用“特征决策树”这种传统组合我的回答是传统方法并不过时而且在实际工程场景中往往更实用。首先深度学习虽然效果好但需要大量标注数据、昂贵的GPU训练成本以及漫长的调参周期。而在工业现场很多时候你只有几万条样本、一台普通电脑这时候传统方法的性价比反而更高。其次决策树模型的可解释性极强——它能明确告诉你“哪个特征、在什么阈值范围内、对应什么故障”这种透明的决策逻辑在工业落地时非常重要。工程师能看懂模型为什么这么判断才敢放心让它上线。另外特征提取本身也是一种降维和去噪的过程提取出来的敏感特征本身就是对设备状态的一种压缩表示即使后面换成随机森林、SVM甚至神经网络这套特征提取流程也完全复用。所以这个项目的价值不止于“跑通一个决策树”而是搭建了一套可扩展的故障诊断基础流程。2. 数据集准备与Python环境搭建2.1 数据集的下载与文件结构整理这个项目第一步是把数据集下载好并整理成统一格式。渥太华大学的数据集下载后通常是一批CSV文件每个文件对应一组工况下的振动信号。你需要确认自己的Python环境已安装完成如果没有python环境先去官网下载安装Python 3.8以上版本建议用Anaconda安装装好后在命令行输入conda create -n fault python3.8 -y建一个独立环境避免依赖冲突。下载后先不要急着跑代码把文件按故障类别归档并做一个简单的文件清单。建议把所有文件放到一个目录下命名格式大致如下data/ ├── normal/ # 正常状态 ├── outer/ # 外圈故障 ├── inner/ # 内圈故障 ├── ball/ # 滚动体故障有些版本的数据集还包含保持架故障处理方式一样。归档这一步虽然机械但极其重要——后续加载数据、打标签全靠这个目录结构省得在脚本里写一堆容易出错的条件判断。2.2 依赖库安装这个项目用到的Python库不多但有几个必须提前装好库名用途安装命令numpy数值计算pip install numpypandas数据读取与表格操作pip install pandasscipy信号处理、FFTpip install scipyscikit-learn决策树、特征筛选、评估pip install scikit-learnmatplotlib可视化pip install matplotlib如果你用的是conda直接conda install numpy pandas scipy scikit-learn matplotlib一次性搞定。建议在项目文件夹里建一个requirements.txt把依赖写进去方便换机器时一键恢复环境。这一步踩过的坑是有时候直接pip install会装到系统自带的Python环境里结果项目里跑代码又调不到包报“ModuleNotFoundError”。所以强烈建议在建好的conda环境里装装完在终端确认一下python -c import sklearn; print(sklearn.__version__)看到版本号就说明环境OK。2.3 加载数据与合理切分数据文件加载本身不难但有个细节值得专门说一个几秒钟的振动信号文件可能就有十几万个采样点如果直接把整个文件作为一条样本样本数量太少模型学不到东西。正确做法是滑窗切分——把一段长信号按照固定窗口长度切成多个子样本每个子样本作为一个独立的样本参与特征提取和模型训练。滑窗切分有两个参数要设置窗口长度和步长。窗口长度取决于信号的采样频率和故障特征频率。比如采样频率是20kHz转速是1200rpm那么轴承旋转一圈对应1000个采样点提取特征时窗口至少要覆盖几个旋转周期一般建议窗口长度取2000到4000个点。步长可以设置成窗口的一半这样相邻窗口之间有一定重叠样本数量能多出不少。代码示意import numpy as np import pandas as pd def sliding_window(data, window_size, step_size): 将一维信号切成多个窗口子序列 samples [] for start in range(0, len(data) - window_size, step_size): samples.append(data[start:start window_size]) return np.array(samples) # 示例读取一个文件切成窗口 raw_signal np.genfromtxt(data/normal/normal_1.csv, delimiter,) windows sliding_window(raw_signal, window_size2048, step_size1024) print(windows.shape) # 比如 (500, 2048)切完之后统一打标签normal对应0 outer对应1 inner对应2 ball对应3。将标签存成一列和后续的特征矩阵拼在一起形成一个标准的数据表。这一步做得干净后面的特征提取和模型训练会省很多事。3. 核心环节故障敏感特征提取3.1 时域特征的选取与计算时域特征是从信号波形本身直接计算出来的统计指标计算量小物理意义直观是故障诊断最基础的一层特征。我常用的时域特征有这些均值信号的直流分量反映信号中心位置。标准差和均方根值反映信号能量大小轴承磨损加剧时均方根通常会上升。峰值信号最大绝对值对冲击类故障比较敏感。峭度信号分布的四阶矩归一化指标滚动体或外圈剥落产生的周期性冲击会让峭度明显增大。偏度信号分布的不对称程度某些定向磨损会让偏度改变。峰值因子峰值除以均方根值用于衡量信号中冲击成分的相对强度。波形因子、脉冲因子、裕度因子思路类似都是为了放大冲击特征。这些特征各有侧重有的反映能量有的反映冲击有的反映分布形态。单个特征往往说不清楚问题组合起来就能区分多种故障类型。def extract_time_features(x): 输入一个窗口的一维信号返回时域特征列表 mean_val np.mean(x) std_val np.std(x) rms np.sqrt(np.mean(x ** 2)) peak np.max(np.abs(x)) kurtosis np.mean((x - mean_val) ** 4) / (std_val ** 4 1e-12) skewness np.mean((x - mean_val) ** 3) / (std_val ** 3 1e-12) crest_factor peak / (rms 1e-12) waveform_factor rms / (np.mean(np.abs(x)) 1e-12) pulse_factor peak / (np.mean(np.abs(x)) 1e-12) margin_factor peak / (np.mean(np.sqrt(np.abs(x))) ** 2 1e-12) return [mean_val, rms, peak, kurtosis, skewness, crest_factor, waveform_factor, pulse_factor, margin_factor]注意实现时每个除法都加了极小值1e-12防止信号幅值为零时出现除零错误。这种边界处理的细节在一堆窗口数据里很容易碰到建议一开始就写上。3.2 频域特征的提取逻辑时域特征看的是波形本身频域特征看的是振动能量在不同频率上的分布。故障发生时特定频率处的能量会发生变化比如轴承内圈故障会在故障特征频率及其倍频处出现明显的谱峰。所以频谱特征对故障诊断往往比时域特征更敏锐。频域特征的提取通常分两步先用快速傅里叶变换FFT把信号从时域变到频域再计算频谱的一些量化指标。常用的频域特征包括频谱重心能量集中的频率位置不同故障导致的主频偏移会反映在上面。均方频率各频率分量的加权平均反映频谱整体的分布情况。频率方差频谱能量在频域中相对重心的离散程度。特定频带能量占比把频谱分为若干频段统计每个频段的能量占总能量的比例。from scipy.fft import fft def extract_freq_features(x, fs20000): 输入一维信号返回频域特征列表 n len(x) spectrum np.abs(fft(x))[:n // 2] freqs np.linspace(0, fs / 2, n // 2) total_energy np.sum(spectrum ** 2) 1e-12 spectral_centroid np.sum(freqs * spectrum ** 2) / total_energy mean_square_freq np.sum(freqs ** 2 * spectrum ** 2) / total_energy freq_variance np.sum((freqs - spectral_centroid) ** 2 * spectrum ** 2) / total_energy # 按1000Hz间隔划分频带能量占比 band_ratios [] for low in range(0, int(fs / 2), 1000): mask (freqs low) (freqs low 1000) band_energy np.sum(spectrum[mask] ** 2) band_ratios.append(band_energy / total_energy) return [spectral_centroid, mean_square_freq, freq_variance] band_ratios这里有几个细节值得注意。取频谱时只取前一半因为实信号的FFT结果是关于奈奎斯特频率对称的后一半没有独立信息。频带划分的宽度跟采样频率相关采样频率越高可分析的频带就越多。我在这套数据上把频带宽度设为1000Hz组合出来十几个频带特征区分效果已经不错了。3.3 时频域特征的补充到了这一步时域特征和频域特征已经能够覆盖大部分故障类型。但有些故障尤其是不稳定运行状态下的早期微弱故障它的冲击成分在时域上出现的位置和频域上的分布都在变化单纯看全局时域或全局频谱会把这些瞬态信息平均掉。这时可以考虑补充时频域特征。最简单实用的时频方法是小波包分解它能把信号分解到不同频带和时间分辨率上每个分解子带的能量占比可以作为特征。实现上直接用现成的库就行不需要自己写小波算法。import pywt def extract_wavelet_features(x, waveletdb4, level3): 小波包分解计算各节点能量占比作为特征 wp pywt.WaveletPacket(datax, waveletwavelet, modesymmetric, maxlevellevel) nodes [node.path for node in wp.get_level(level, freq)] energies [] for node in nodes: data_node wp[node].data energies.append(np.sum(data_node ** 2)) total np.sum(energies) 1e-12 return [e / total for e in energies]注意处理之前要先把数据标准化否则小波系数幅值差异过大会影响能量占比的稳定性。3.4 特征敏感度的评估与筛选特征提取完我这里通常拿到几十个特征。问题来了这些特征都“敏感”吗不见得。有些特征在不同故障类别之间的分布几乎重叠区分能力非常弱有些特征跟别的特征高度相关存在信息冗余。我筛特征的思路是“一粗一细”两步走。粗筛单特征可区分性分析。把每个特征按类别画箱线图直观看出这个特征在不同类别下的分布是否有明显区分。如果四个类别的箱子高度重合这个特征基本可以放弃。量化指标可以用方差分析ANOVA的F值F值越大说明组间差异相对组内差异越显著特征越敏感。from sklearn.feature_selection import f_classif # feature_matrix: (n_samples, n_features) # labels: (n_samples,) f_scores, p_values f_classif(feature_matrix, labels) for idx, f in enumerate(f_scores): print(f特征{idx}: F值 {f:.2f})细筛相关性去冗余。用皮尔逊相关系数矩阵看特征之间的相关性。相关系数超过0.95的两个特征只需要保留那个单独区分度更高的另一个可以考虑去掉。这一步能明显减少特征数量让模型更简洁。corr_matrix np.corrcoef(feature_matrix, rowvarFalse) # 找出高相关特征对 high_corr_pairs [] n_features corr_matrix.shape[0] for i in range(n_features): for j in range(i 1, n_features): if abs(corr_matrix[i, j]) 0.95: high_corr_pairs.append((i, j, corr_matrix[i, j]))最后把所有筛选出来的特征拼成一个二维矩阵每一行是一个窗口样本每一列是一个特征再配上标签列。这就是可以用来训练的最终数据集拿到手之后模型的输入问题就彻底解决了。4. 决策树分类建模与评估4.1 决策树原理与参数选择决策树分类器做的事情本质上是学一串“if-else”判断规则。它自动寻找特征空间中最合适的分割点把样本一层层划分下去直到每个叶子节点内部都是同一类别或纯度足够高。它的好处是训练快、可解释性强而且对特征尺度不敏感——这意味着前面提取的一些幅值差异大的特征不需要额外归一化就能直接用省了很多预处理工作。用scikit-learn实现决策树非常简单from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( feature_matrix, labels, test_size0.3, random_state42, stratifylabels ) clf DecisionTreeClassifier( max_depth6, min_samples_split10, min_samples_leaf5, criteriongini, random_state42 ) clf.fit(X_train, y_train)有几个参数需要特别留意一下。max_depth是树的最大深度。深度太小模型学不到足够的决策规则欠拟合深度太大树会疯狂生长把训练集的细节和噪声都记住严重过拟合。我习惯从小往大试从3开始每次加2观察训练集和测试集准确率的变化找一个“测试集准确率最高且和训练集差距不太大”的深度。min_samples_split是节点继续分裂所需的最小样本数。它跟数据集规模强相关样本量大的时候可以设大一点比如10到20样本量小的时候设2到5免得树根本长不起来。min_samples_leaf是叶子节点的最小样本数。这个参数对抑制过拟合非常有效叶子节点样本太少意味着决策规则训练得太细泛化能力差一般设为样本总量的1%左右比较稳妥。另外建议在切分数据时设stratifylabels做分层采样确保训练集和测试集中各个类别的占比与原始数据一致。否则万一随机切分的时候某一类的样本全跑到测试集去了模型训练时根本没见这一类测试时一塌糊涂。4.2 模型评估指标的选择准确率是最直观的指标但对故障诊断来说光看准确率远远不够。如果正常样本占80%哪怕模型把所有故障样本都判错准确率还有80%看着挺高实际上模型完全不能用。所以必须看混淆矩阵以及每个类别的精确率、召回率和F1值。以四分类为例混淆矩阵是一个4x4的矩阵行是真实标签列是预测标签。对角线上的数字越大越好对角线之外的数字越大说明误分类越严重。from sklearn.metrics import classification_report, confusion_matrix y_pred clf.predict(X_test) print(classification_report(y_test, y_pred, target_names[normal, outer, inner, ball]))实战中我最关注的是“内圈故障被判成外圈故障”这种混淆。滚动体和内圈的故障特征频率在某些工况下比较接近模型容易搞混。遇到这种情况不要急着换模型先去检查这几类的特征分布往往能通过增加特征或调整特征组合来改善。4.3 决策树结构的可视化决策树模型最好的地方在于可以把它画出来让人直接看到它到底学了一组什么样的规则。这一步在工程汇报和方案评审的时候特别好用——你不光能告诉领导“模型准确率95%”还能告诉他“模型认为均方根超过0.85且峭度超过6的时候判为内圈故障”这个说服力是完全不同的。from sklearn.tree import export_graphviz import graphviz dot_data export_graphviz( clf, out_fileNone, feature_namesfeature_names, class_names[normal, outer, inner, ball], filledTrue, roundedTrue, special_charactersTrue ) graph graphviz.Source(dot_data) graph.render(fault_decision_tree) # 保存为PDF文件画出来之后重点关注根节点附近的前两三层的分裂规则这几层是模型最主要的判别逻辑。后面如果有特征重要性输出的需要可以直接读取clf.feature_importances_它能告诉你哪个特征对分类的贡献最大这又是一个极好的特征敏感性验证手段。4.4 参数调优的实验记录我第一次跑的时候决策树参数用的是默认配置max_depth不限制。结果训练集准确率接近100%测试集准确率只有89%典型的过拟合。后来我做了几组对比实验记录如下参数设置训练集准确率测试集准确率是否过拟合不限制深度99.8%89.2%是max_depth896.5%93.1%轻微max_depth694.2%94.6%否max_depth6, min_samples_leaf592.8%95.3%否可以看到适当限制树的复杂度测试集准确率反而上升了。这就是为什么我一直强调不要盲目追求训练集上的完美表现。在真实工业故障诊断中训练集和测试集往往还来自不同时间段、不同工况的数据泛化能力差一点点落地效果就会差非常多。5. 项目实操中的常见问题与排查技巧5.1 多分类标签与特征矩阵的Shape不一致这个问题出现的频率最高。特征提取往往是对每个窗口单独运行的如果某个文件切出来的窗口数量不是整整齐齐的特征矩阵的行数和标签列表的长度就对不上。训练模型时sklearn会直接报错说“Found input variables with inconsistent numbers of samples”。排查办法很简单在拼接特征矩阵和标签之前打印一下两边的shape进行长度验证。我习惯在每个处理阶段都加一句断言assert len(feature_list) len(label_list), 特征数与标签数不一致请检查滑窗逻辑这个小习惯能帮你省下大量排查错误的时间。5.2 数据集不平衡问题实际项目中正常状态的数据往往非常多故障状态的数据相对稀少。如果直接训练决策树会偏向样本量大的类别导致少数类故障被漏报。而故障诊断中最不能容忍的就是漏报——设备坏了你说它没事生产线上耽误一分钟都是损失。解决思路有两个一是从数据层面做重采样对样本少的类别进行过采样比如SMOTE或对样本多的类别进行欠采样二是在模型层面调整类别权重在sklearn里直接加class_weightbalanced参数让决策树在分裂时对不同类别给予不同的权重。实测下来数据量大的时候我一般先用class_weight效果不够再上采样方法。5.3 特征区分度不足分类准确率上不去这是我被问得最多的一个问题“老师我把特征都算出来了但决策树准确率只有80%怎么办”我的排查顺序是这样的。第一步先看箱线图或特征的F值确认每个特征真的在不同类别间有差异。如果很多特征基本重合问题出在特征提取环节需要回到时频域特征上去补。第二步看混淆矩阵判断到底是哪些类别之间容易混淆。如果是内圈和外圈混淆考虑提取轴承故障特征频率处的窄带能量特征。第三步看是不是样本量太少了窗口切分的重叠度不够尝试减小步长增加样本量。第四步换模型交叉验证——把决策树换成随机森林看看准确率有没有明显提升。如果随机森林明显更好说明单个决策树方差太大用集成方式把多棵树投票就能解决问题。5.4 原始信号文件读取格式兼容问题不同来源的数据集格式差异很大有的CSV文件第一行是列名有的直接是纯数据有的用逗号分隔有的用分号或制表符有的甚至要跳过起始几行注释。为此我专门写了一个统一读取函数def load_signal_file(path): 兼容多种分隔符的振动信号读取 try: df pd.read_csv(path, headerNone, sep,) except Exception: try: df pd.read_csv(path, headerNone, sep\t) except Exception: df pd.read_csv(path, headerNone, sep;) # 只取第一列数据很多输出文件第一列是时间戳 return df.iloc[:, 0].values.astype(np.float64)这个函数用三段式try-else把多种情况都兜住了数据加载这一块的兼容性问题基本都能解决。当然前提是文件里确实有一列是振动幅值数据文件结构能正常解析。5.5 安装依赖时遇到”缺少包”的错误如果运行时报错提示缺少某个模块先别乱装一堆包。仔细看一下报错信息里缺的是哪个库按需安装即可。比如提示ModuleNotFoundError: No module named pywt说明缺少小波库执行pip install PyWavelets就行。如果提示需要安装缺失节点或工作流相关的包比如某些comfyui场景下的报错这说明项目环境与其他框架混在一起了建议重新建一个专用conda环境只安装这个项目需要的库避免环境互相污染。安装后用pip list确认版本,并随手记录到requirements.txt中。6. 最后再分享两个实用技巧第一个技巧是我多次调试后总结出来的在做特征提取时不要只算特征数值一定要把“工况信息”也记录下来。同一型号的轴承在800rpm和1500rpm下提取的均方根、峭度差异非常大。如果你的数据里混了多种工况而不加区分模型会学得非常吃力。最省事的办法是特征提取时把转速或工况编号作为一个特征列加进去或者按工况分组建模。这个问题在工程现场极其常见很多人一开始忽略它后面数据分布一变模型就崩再来排查就晚了。第二个技巧关于决策树的剪枝策略。sklearn的DecisionTreeClassifier自带cost_complexity_pruning_path方法可以输出不同ccp_alpha值对应的剪枝效果配合网格搜索能得到泛化能力更好的树。我用了一版剪枝后的树测试集准确率又提升了1到2个百分点。如果你对决策树本身的要求比较高可以深入研究一下这条路。整套流程跑下来渥太华大学这个数据集的四分类准确率在我的实验里能达到95%以上如果叠加工况分层和更精细的时频特征冲到97%也不难。但这个项目给我最大的收获不是那一个准确率数字而是把“特征到底怎么算、怎么筛、怎么用”这条链路彻底打通了。往后不论换什么数据、换什么模型这套做故障诊断的思维框架都是通用的。你要是刚接触这个方向建议先照着代码把流程完整跑一遍然后再换一个数据集试试同样的方法——等你能把新数据集也做出一套像样的分类结果这个知识点就真正是你的了。