简介:凯斯西储大学(CWRU)轴承数据集是故障诊断与机器学习领域广泛使用的公开基准,该资源包在原始mat文件基础上配好Python封装程序与使用说明,面向需要快速加载训练集、测试集及标签的研究者、学生或工程开发人员。包内共172个文件,含165个mat格式振动信号数据、3个Python脚本、2个pyc缓存文件及2个txt说明文档,压缩包约240.31MB,目录结构清晰,便于按需提取。整理后的cwru模块支持传入采样频率(如12kHz驱动端)、转速(0至3马力对应1797至1730转/分)和信号长度等参数,调用后可直接获得X_train、y_train、X_test、y_test、labels与nclasses等字段;标签覆盖0.007、0.014、0.021英寸不同损伤直径下的球、内圈、外圈故障及正常状态共16类,并可利用循环统计每类训练样本长度,用于调整训练/测试窗口大小。已有1072人浏览学习,适合正在做轴承故障诊断、特征提取或分类模型验证的Python用户对照使用。
1. CWRU轴承数据集:为什么故障诊断的入门一定要从它开始
在实验室里搭好一套振动采集系统,传感器贴好、采样率设好,正想验证自己的故障诊断算法到底靠不靠谱,却发现手里没有一份带标准答案的故障数据。这种处境,做设备健康管理的人应该都碰过。凯斯西储大学(CWRU)轴承数据集,就是用来解决这个问题的:它把正常、内圈、外圈、滚动体四种轴承状态的振动信号公开出来,配了采样率、转速、载荷标注,是全球故障诊断领域用得最多的基准数据集之一。你可以拿它跑通特征提取、训练分类模型、验证深度学习结构,也可以拿它当“尺子”,先看看自己的算法在干净数据上能到多少分,再上现场数据。适合这几类人:刚接触故障诊断的研究生、做预测性维护的工程师、想快速验证小波或神经网络思路的算法工程师。它替代不了现场数据,但它能让你把流程跑顺、把坑提前踩完。
2. 拿到数据包先拆解:三种文件格式与mat数据结构解析
2.1 数据包的目录结构:驱动端/风扇端/正常数据怎么分
把下载的压缩包解开,第一件事不是急着写代码,而是先看目录。常见发布版的文件夹划分是三个:Normal Baseline Data(正常基线数据)、12k Drive End Bearing Fault Data(12kHz驱动端故障数据)、12k Fan End Bearing Fault Data(12kHz风扇端故障数据)。有些整理版还会带一个48k Drive End文件夹,采样率更高。每个文件夹里放着一批.mat文件,文件名大多是三位数字,像105.mat、118.mat这种,光看文件名看不出故障类型,必须去对照readme里的编号表。
readme里的编号表是这个数据包最关键的对照文件。它会把每个mat文件的编号对应到故障位置、故障直径(英寸)和载荷(hp)。比如105.mat在一些版本里对应的是驱动端内圈故障、故障直径0.007英寸、载荷0hp,但不同整理版对编号的对应关系不完全一致,有的版本还会把文件名改成X105_DE_time这种带通道的格式。我一般会先把readme里的表拷出来核对一遍,确认自己手上这份是不是标准编号,顺便检查有没有遗漏文件。千万别跳过这一步,后面所有标签映射都建立在这张表上。
除了目录,还要分清文件格式。官方最早期发布的是MATLAB的.mat格式,后面社区整理过几个版本,有些把数据转成了.csv,有些直接切成npz。我建议优先用原始mat,因为mat里保留了DE、FE、BA三个通道的完整信号,而csv版为了省空间,常见做法是只留下驱动端通道,做通道对比时就缺数据了。数据包里的“整理Python程序”一般也围绕mat展开,所以先把mat读通,后面换格式都方便。
2.2 mat文件内部的sensor结构:DE、FE、BA通道怎么取
用任意一个能读mat的工具打开文件,会看到里面变量名长这样:X097_DE_time、X097_FE_time、X097_BA_time。命名规则很直白:DE对应Drive End,是贴在驱动端轴承座上的加速度传感器;FE对应Fan End,贴在风扇端;BA对应Base,在电机底座上。三个通道采集的是同一个轴承振动,但因为安装位置不同,信号特征差很多。DE离故障点最近,冲击特征最明显,是故障诊断的主力通道;FE隔着端盖,高频成分衰减明显;BA主要反映底座的结构响应,机械噪声混得比较厉害。
这里有个易踩的坑:mat里存的是一维采样点序列,不是带时间戳的二维数组。采样间隔完全由采样率决定,12kHz就是一秒12000个点,48kHz就是一秒48000个点。不要去找mat里的时间列,没有。需要算转速和频率映射时,直接用采样率fs做换算。我习惯把mat读进来后统一转成float64,原始采集数据一般是int16,做FFT或包络谱时如果不转,幅值精度和数值溢出都会出问题。
2.3 样本量与载荷表:12kHz与48kHz怎么选
CWRU的试验工况分四档载荷:0hp、1hp、2hp、3hp。载荷不同,电机转速也不同。官方readme里给出的近似转速如下表,实际运行时因为滑差会有波动,做特征频率估算时按这个表取数足够。
| 载荷 | 约测转速 | 转频fr |
|---|---|---|
| 0 hp | 1797 rpm | 29.95 Hz |
| 1 hp | 1772 rpm | 29.53 Hz |
| 2 hp | 1750 rpm | 29.17 Hz |
| 3 hp | 1730 rpm | 28.83 Hz |
12kHz和48kHz两档采样率怎么选,取决于你要分析什么。12kHz采样率下,奈奎斯特频率是6000Hz,覆盖轴承内外圈故障冲击的主频带够用,而且一个mat文件约12万个点,滑窗切样本后计算量适中,适合快速迭代。48kHz能看到更宽的频带,尤其滚动体故障的冲击持续时间短,激发出数千赫兹的高频共振成分,在12kHz下可能被滤波掉一部分。代价是数据量是4倍,特征提取时间也接近4倍。我的选择顺序是:先用12kHz把所有流程跑通,特征工程和模型结构定下来后,再用48kHz数据做一轮验证。
3. 用Python读取CWRU数据:loadmat版本陷阱与信号可视化
3.1 最小读取代码:兼容新旧scipy的loadmat写法
环境上只需要三个库:numpy、scipy、matplotlib。安装方式就是pip install numpy scipy matplotlib,在VSCode里配好Python解释器,选3.8以上的版本,直接跑命令就行。Linux服务器上也同理,用conda建一个Python3环境再pip安装,没有额外依赖。这里要提一个前置注意点:scipy的loadmat不是万能的,下面这段读取函数是CWRU上我一直在用的基础版本:
import numpy as np from scipy.io import loadmat def load_cwru_mat(path): mat = loadmat(path, squeeze_me=True, struct_as_record=False) keys = [k for k in mat.keys() if not k.startswith('__')] if len(keys) == 1: return mat[keys[0]] return {k: mat[k] for k in keys}这段代码的逻辑是:loadmat把整个mat文件解析成一个字典,字典里除了数据变量,还有__header__、__globals__这类由scipy自动生成的元信息,要过滤掉。squeeze_me=True的作用是把单维数组压成一维,否则读出来的DE_time可能是shape为(1, N)或(N, 1)的矩阵,后面做滑窗切分时容易多出维度。struct_as_record=False是针对MATLAB结构体的,CWRU的mat大多数是纯变量数组,用不上,但保留这个参数能避免读到结构体时被转成numpy.recarray导致字段名不好取。
返回值可能是int16,建议在调用处加一行data.astype(np.float64)。信号范围大概在正负几伏之间,int16的数值直接拿去fft不会崩,但做希尔伯特变换时精度会受限。如果loadmat报“EOFError”或“Unknown mat file type”,基本可以断定你拿到的不是老式v5的mat,而是v7.3格式的HDF5文件。v7.3是MATLAB为了支持超大变量引入的,scipy读不了。解决办法是改用h5py:
import h5py def load_cwru_h5(path): with h5py.File(path, 'r') as f: key = list(f.keys())[0] data = np.array(f[key]).squeeze() return datah5py读出来的是HDF5引用,必须先np.array取数,再squeeze把单维去掉,不然拿到的是一层虚包装。判断文件版本可以用一个笨办法:先用loadmat试,报错就换h5py;也可以直接看文件大小,v7.3因为内部用HDF5组织,文件通常比同内容的v5大不少。
3.2 信号拼接与轴转速对齐:把10秒样本切成多个样本
一个mat文件通常是10秒左右的连续采样,12kHz下就是12万个点。如果把这10秒当作一条完整样本,一个文件只贡献一个训练样本,对训练一个像样的分类模型来说是远远不够的。行业里的通行做法是滑窗切分。窗口长度怎么定?要覆盖至少几个故障周期。CWRU在0hp载荷下,内圈故障特征频率约5.43倍转频,转频约30Hz,算下来BPFI约163Hz,周期约6毫秒。12kHz采样率下,一个周期约74个采样点。取1024点作为窗口,大概覆盖14个故障周期,既能保留足够的冲击周期性,又不会因为窗口太大把低载荷和高载荷的转速差异放得过大。
def sliding_window(signal, window=1024, step=None): step = step or window n = len(signal) indices = range(0, n - window + 1, step) slices = np.array([signal[i:i+window] for i in indices]) return slices滑窗参数里最容易忽略的是step。step等于window时,相邻两个样本没有重叠,样本之间独立性最好,适合做训练集和测试集的划分;step小于window时,样本数量和训练集规模都增加了,但相邻样本高度相关,模型很容易记住“这段数据来自哪个文件”而记住不了真正的故障特征。我在切CWRU时,第一轮永远用step=window,先把独立样本跑通;如果正负样本比例失衡,再考虑用重叠滑窗补齐少数类。
3.3 波形可视化:一个plt代码看四种状态的区别
切完样本后,可视化是判断数据有没有读错的直接手段。把正常、内圈、外圈、滚动体四类信号各取一段画出来,一眼就能看出差异:
import matplotlib.pyplot as plt def plot_waveforms(samples, titles, fs=12000): fig, axes = plt.subplots(2, 2, figsize=(12, 8)) for ax, x, t in zip(axes.ravel(), samples, titles): x = np.asarray(x, dtype=np.float64) time = np.arange(len(x)) / fs ax.plot(time, x, lw=0.5) ax.set_title(t) ax.set_xlabel('Time (s)') ax.xaxis.set_major_locator(plt.MaxNLocator(4)) fig.tight_layout() plt.show()画图的第一个坑就是横坐标太密集:1024个点堆在几厘米宽的坐标轴里,刻度挤成一团,所以用MaxNLocator(4)把x轴刻度抽稀到只有几个标签。正常信号的波形幅值平稳,看不出明显周期性冲击;内圈故障在波形上能看到周期性出现的冲击尖峰,尖峰幅度比正常段大好几倍;外圈故障的冲击更尖锐,而且方向性明显;滚动体故障的冲击会出现周期性“消失”的现象,因为滚动体每转一圈进入载荷区的位置会变化。如果画出图像是几条平稳直线,大概率是读取时把通道取错了,或者数据被归一化到接近零。处理这类问题时的经验是:先看原始波形对不对,再谈特征提取。
4. 特征提取的落地顺序:时域指标与包络谱参数
4.1 时域特征:RMS、峰值因子、峭度的物理含义与python实现
CWRU数据的波形差异肉眼可见,但直接把原始波形塞给分类器,效果通常不稳定。原因很简单:原始波形对转速波动、载荷变化、传感器灵敏度都敏感,同一个故障换一档载荷,幅值就变了。所以特征提取的第一层,我通常先抽时域统计量,它们计算快、物理含义清晰、适合做基线对比。
时域指标里最常用的是RMS、峰值因子、峭度和脉冲因子。RMS反映振动能量,内圈故障出现后RMS会明显上升,但它也会随载荷增大而增大,所以跨载荷比较时不单独看RMS;峰值因子是峰值除以RMS,能反映出“是否存在大的瞬时冲击”,外圈故障的峰值因子通常偏高;峭度是四阶中心矩归一化,正常轴承的峭度接近3,出现瞬态冲击时峭度会升到5以上,滚动体故障尤其明显;脉冲因子对单个冲击尖峰敏感,但容易受零点漂移影响,所以用之前要先做去均值。
def time_features(x): x = np.asarray(x, dtype=np.float64) x = x - np.mean(x) rms = np.sqrt(np.mean(x**2)) peak = np.max(np.abs(x)) crest = peak / (rms + 1e-12) kurt = np.mean((x - np.mean(x))**4) / ((np.var(x) + 1e-12)**2) impulse = peak / (np.mean(np.abs(x)) + 1e-12) return {"RMS": rms, "峰值因子": crest, "峭度": kurt, "脉冲因子": impulse}分母上加1e-12是为了防除零,这个数在后续融合特征时再统一消除。峭度计算里先对x做了一次中心化,再用方差平方做归一化,所以峭度无量纲,理论上高斯分布接近3。在CWRU上,如果画特征散点图会发现:正常样本的峭度集中在2.5到3.5,内圈故障样本分散在4到10,滚动体故障的峭度波动最大。这说明滚动体故障的冲击本身就不稳定,不能只靠一个时域指标定结论。
4.2 频域特征:FFT与包络谱在CWRU上的参数选择
时域特征能区分“有没有故障”,但区分不了故障位置。要分内圈、外圈、滚动体,得看故障特征频率。CWRU用的轴承型号公开资料里写得很清楚,是SKF 6205-2RS深沟球轴承,滚珠数9个。基于轴承几何参数算出来的特征频率系数如下表,直接乘转频fr就是各故障的绝对频率。
| 故障位置 | 特征频率系数 | 0hp下的频率(约) |
|---|---|---|
| 外圈 | 3.57×fr | 107 Hz |
| 内圈 | 5.43×fr | 163 Hz |
| 滚动体 | 2.32×fr | 69 Hz |
这些系数怎么用?不是直接对原始FFT找尖峰,原始FFT里能量大多集中在低频转频及其谐波上,故障冲击的调制边带反而被淹没。正确做法是做包络谱:先对信号做希尔伯特变换得到解析信号,取包络,再对包络做FFT。包络谱能把冲击调制的高频成分解调出来,特征频率处的尖峰就明显了。
from scipy.signal import hilbert, hann def envelope_spectrum(x, fs=12000): x = x - np.mean(x) analytic = hilbert(x) env = np.abs(analytic) win = hann(len(env)) spec = np.fft.rfft(env * win) freq = np.fft.rfftfreq(len(env), d=1/fs) return freq, np.abs(spec)这段代码的逻辑是先去掉直流分量,希尔伯特变换把实信号变成解析信号,取绝对值就是包络。加汉宁窗是为了抑制频谱泄露,窗口函数的类型对峰值定位有影响,汉宁对幅值的频率泄漏抑制效果比较稳。rfft只输出正频率部分,频率分辨率是fs/N,N是窗口长度。比如1024点窗口、fs=12000,分辨率约11.7Hz,读谱时搜索半径设±5Hz基本足够。内圈故障在163Hz左右找尖峰,外圈在107Hz左右,滚动体在69Hz附近。实际CWRU数据因为转速有波动,尖峰不会严格落在理论值上,搜索半径放宽到±5Hz更稳妥。
5. CWRU数据处理,最容易让人翻车的五个细节
5.1 loadmat不是万能的:HDF5格式把它难住了
现象:同一套读取代码,在自己电脑上跑得好好的,换到实验室同事的电脑上就报EOFError或者Unknown mat file type,代码和数据包都没变。
原因:新旧MATLAB版本保存的mat格式不一样。老版本保存的是v5格式,scipy的loadmat直接支持;新版MATLAB默认或勾选保存v7.3时,文件内部变成HDF5结构性格式,scipy读不了。
解决:写一个兼容读取函数,先试loadmat,报错就改用h5py,把两种情况都包住:
def smart_read_mat(path): try: return load_cwru_mat(path) except Exception: return load_cwru_h5(path)这个函数是后续所有步骤的地基,我在整理数据包时第一个写的就是它。注意h5py读出来的变量可能是引用类型,一定要np.array转一下再加squeeze,不然后续滑窗函数拿到的对象不对。
5.2 DE和FE通道混着用,模型学了个寂寞
现象:把DE_time和FE_time两个通道的滑窗样本都放进同一个故障标签里,训练集随机森林分类准确率99%,测试集掉到85%,而且怎么调参都回不去。
原因:两个通道的传感器安装位置不同,DE在驱动端轴承座,FE在风扇端,信号幅值、频带、信噪比差异都很大。混在一起等于给同一个标签喂了两种分布完全不同的数据,模型学到的是传感器位置的分布,而不是故障特征的分布。
解决:每个通道单独建数据集。跑模型前先明确一个问题:部署时传感器会贴在哪个位置?如果是贴驱动端,就只用DE通道;如果要做位置无关的通用诊断,那就把DE和FE各当一个独立数据集,分别训练分别评估,不要混。
5.3 外圈故障的相位,让你白做一轮可视化
现象:可视化外圈故障样本时,发现波形基本都是负向冲击,取绝对值后特征才正常;再翻另一个外圈文件,冲击方向又变成正的了。同一类故障,正负极性不一致。
原因:CWRU的故障是用电火花加工在外圈上打出的孔,打了不同角度的位置。有的在载荷区正下方,有的在3点钟或12点钟方向,滚动体进入和离开载荷区时,传感器感受到的冲击方向不一样,反映在波形上就是极性不同。
解决:在特征提取前做对称化处理。常见的做法是每次滑窗后先对信号取绝对值,或者直接用希尔伯特包络后再算统计量。千万别拿原始波形直接算峰值因子,正负极性翻转会把特征方差拉得很大,分类器会学错方向。
5.4 转速变化被忽略,特征频率凭空漂移
现象:把0hp和3hp的数据混在一起检测BPFI,搜索半径设成理论值±5Hz,结果时好时坏,有时候在160Hz附近怎么都找不到尖峰。
原因:载荷从0hp增加到3hp,转速从1797rpm降到1730rpm,BPFI从约163Hz降到约156Hz。直接用0hp的理论值去全局搜索,3hp数据里的真实峰值不在搜索范围内。
解决:两种方式都行。按载荷分组,每组单独用对应转速计算特征频率;或者更通用一点,把绝对频率除以转频fr,转成无量纲的故障特征阶次。阶次对转速变化不敏感,0hp和3hp的数据放在一起也能稳定比对。
5.5 滑窗切分造成的假高分,换个场景就翻车
现象:训练集准确率95%,测试集准确率99%,模型看起来很好;拿到现场数据一测,完全不行。回头看代码,发现滑窗切分后,把同一段连续信号切出的窗口随机分到了训练集和测试集。
原因:相邻窗口有大量重叠数据,重叠部分同时出现在训练和测试里,模型等于提前见过答案。CWRU的10秒信号切出几百个窗口,随机划分后这个现象非常严重。
解决:做文件级划分。每个mat文件只能完整属于训练集或测试集,不能把同一个文件切出的窗口拆开。scikit-learn里有GroupShuffleSplit,把文件id传成group参数就能实现。这样切虽然可用样本变少,但指标真实多了。
6. 把CWRU整理成能训练的数据集:npz格式与最小模型验证
6.1 构建npz数据集与标签映射
前面几步串起来就可以构建一个可直接训练的数据集。我习惯把切好的滑窗样本和标签存成npz格式,numpy的压缩存储格式,给别人换环境读起来都方便。标签映射先约定好:0正常、1内圈、2外圈、3滚动体。文件名里通常有IR、OR、B、Normal这些关键词,可以用关键词做映射,但前提是每份数据包的命名都规范。
import os import numpy as np def build_dataset(data_dir, window=1024, step=None): samples, labels = [], [] name_to_label = {"NORMAL": 0, "IR": 1, "OR": 2, "B": 3} for fname in os.listdir(data_dir): if fname.endswith(".mat"): sig = smart_read_mat(os.path.join(data_dir, fname)) label = None for key, val in name_to_label.items(): if key in fname.upper(): label = val break if label is None: continue for seg in sliding_window(sig, window, step): samples.append(seg) labels.append(label) return np.array(samples), np.array(labels) X, y = build_dataset("data/12k_DE") np.savez_compressed("cwru_12k_de.npz", X=X, y=y)这段代码的逻辑是:遍历目录下所有mat文件,smart_read_mat读取信号,通过文件名关键词确定标签,滑窗切分后把每个窗口和标签存入列表。np.savez_compressed会做无损压缩,10秒信号切出的样本量不算大,压缩后磁盘占用能省不少。但要注意,这段代码默认文件名包含关键词;如果你拿到的整理版文件名全是三位数字编号,就必须先按readme编号表做一个编号到标签的映射字典再替换name_to_label。
6.2 用最小分类器验证数据集是否可用
数据集构建完先不要急着上深度卷积网络。先用一个轻量模型做冒烟测试,验证读取、切分、特征、标签这条链路有没有错。随机森林是个不错的选择,对高维输入不敏感,训练快。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GroupShuffleSplit X_flat = X.reshape(len(X), -1) # 这里是要害:groups必须用每个样本对应的mat文件编号 groups = np.load("sample_file_ids.npy") gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(gss.split(X_flat, y, groups)) clf = RandomForestClassifier(n_estimators=200, random_state=0) clf.fit(X_flat[train_idx], y[train_idx]) print(clf.score(X_flat[test_idx], y[test_idx]))这段代码里的groups是关键,官方文档里GroupShuffleSplit要求传入每个样本所属的组,这里就是原来的mat文件id。如果沿用随机划分,即使前面被告知文件级划分的重要性,代码一写快就又容易图省事,这是我踩过多次的教训。在特征没抽坏的前提下,CWRU上用原始波形直接训练随机森林,准确率通常能超过95%。这个数字本身没有意义,它只能证明整理流程没有把标签和样本弄错。
CWRU跑通之后,我会把同一套特征和模型拿到SEU东南大学轴承数据集和帕德劳恩(Paderborn)轴承故障数据集上再做一轮迁移验证。这两个公开数据集的工况、故障类型、采样设置都不一样,能检验特征和模型是不是只记住了CWRU的某种风格特征。我的习惯是先拿CWRU做冒烟测试,再换这两个数据集看结果有没有普适性。现场采集的数据往往噪声更大、转速波动更明显,CWRU上算对的BPFI到了现场可能还要加阶次跟踪或者重新标定搜索半径,所以别把CWRU上的高分当成实战保障。希望这些整理数据时的经验能帮到你,少走几趟我走过的弯路。
本文还有配套的精品资源,点击获取