☰
CWRU轴承数据集实战指南:从文件解析到机器学习
2026/10/2 4:01:37 网站建设 项目流程

简介:凯斯西储大学(CWRU)轴承数据集是旋转机械故障诊断领域广泛使用的公开基准数据。这个数据包将原始mat数据、整理好的Python封装程序及使用说明整合在一起,解决大家重复下载、格式杂乱、预处理繁琐的痛点。使用者可调用cwru.CWRU接口,选择12kHz驱动端故障数据,按0~3hp负载对应1797/1772/1750/1730 rpm转速,以及300~700之间的信号长度参数,一键划分训练集与测试集,获得0~15共16类故障标签,并直接映射到滚动体、内圈、外圈等具体故障名称及多种故障直径,非常适合故障诊断、机器学习分类研究的初学者与工程师。整个压缩包共172个文件,以165个.mat数据文件为核心,配合3个Python脚本、2个.pyc与2个txt说明文档,整体大小约240.31MB,目录结构清晰,离线可用。目前已有1064人学习浏览,是一份开箱即用的高价值参考资料。

1. CWRU轴承数据集是故障诊断绕不开的“标准答案”:先看清它再动手

CWRU轴承数据集是故障诊断圈子里绕不开的“标准答案”:论文按它出结果,竞赛用它当基准,新手入门也几乎都是从它开始。它提供的是真实电机轴承在电火花加工出的单点故障下的振动信号,不是合成数据,采样率、故障直径、负载工况都标得清清楚楚。这份资源在原始采集数据之外多配了一套Python整理程序和一份能直接照着跑的使用说明,等于把官网那堆让人头大的.mat文件掰开揉碎摆平了。适合刚接触振动诊断的工程师、要做故障分类方向的学生,前提是你愿意先把工况表读明白再往上堆模型。

2. 数据包结构与工况表:先看懂.mat里存的是什么再做整理

2.1 实验台与故障注入方式

这套数据来自凯斯西储大学的电机轴承实验台:电机驱动端装的是SKF 6205深沟球轴承,风扇端是SKF 6203。故障不是自然磨损出来的,而是用电火花在轴承零件上直接打出单点凹坑,位置分为内圈、外圈、滚动体三类,故障直径又分0.007、0.014、0.021英寸三档。电机负载有0、1、2、3 HP四挡,转速对应从1797 RPM逐渐降到1720 RPM左右。

对这个数据集来说,故障注入方式和负载配置就是标签体系的底层来源。分类任务里常见的inner、outer、ball、normal这四个类别,其实就是在这个实验条件下定义的。常有人忽略“负载”这个维度,直接拿所有负载的数据混在一起切训练集和测试集,这种做法在刷准确率时没问题,一旦你把模型部署到现场,换个负载立刻打回原形。所以整理数据的第一步不是急着读信号,而是把工况维度先梳理清楚。

2.2 通道变量与采样率:DE、FE、BA到底指什么

每个.mat文件里存的不是一个数组,而是好几个同长度的通道变量。文件名形如X097_DE_time、X097_FE_time、X097_BA_time,命名的规律是X + 文件编号 + 通道名 + _time。

通道名采集位置常见用途
DE驱动端加速度传感器驱动端轴承故障诊断的主信号
FE风扇端加速度传感器风扇端故障或做对照实验
BA电机基座加速度传感器结构传递路径分析

采样率方面,常见套餐是12 kHz驱动端数据和48 kHz驱动端数据。12 kHz数据里DE、FE、BA三通道齐全,48 kHz数据采样点数更密,适合做高频包络分析,但文件体积也大得多。基座信号BA在部分场景下反而是绕过电机自身振动干扰的好选择,做故障特征频率提取时值得留一份备用。

还有一个容易漏掉的通道:X097_RPM。它记录了这段信号对应的实时转速。官网说明里给出的转速是额定值,但实际每次录信号转速有小幅波动,整理程序里保留了RPM字段,后面做故障特征频率计算时用的是实际转速而不是额定转速,这点对新手很关键。

2.3 编号与工况的对应关系:不能凭文件名猜

官网下载页面附带了一张Excel表,里面把每个文件编号对应的故障位置、故障直径、负载都列清楚了。例如97号到100号对应正常工况的0到3 HP负载,105号附近是内圈0.007英寸故障的一组数据。这张表是整套数据的“身份证”,整理程序里把它的核心字段抽出来维护成了一张META.csv,这样后续所有脚本都能通过文件编号反查工况。

你可能会想:文件名里不是已经写了故障类型吗?其实并没有。.mat变量名只有X105_DE_time这样的格式,不写故障位置,也不写直径。如果不拿编号去对照工况表,你看到的只是一堆数字序列,根本分不清谁是内圈谁是外圈。这也是数组整理程序的第一个价值:把编号自动翻译成可读的工况标签。

3. Python整理程序解读:从.mat到规整DataFrame的三步流水线

3.1 第一步:用scipy.io读取.mat文件

配套程序的核心读盘函数用scipy.io.loadmat实现,逐通道提取数据。这里有一个细节:loadmat返回的是字典,键名就是.mat里的变量名,但不同版本的MATLAB文件可能带__header__等附加键,必须用正则把真正的信号通道筛出来。

from pathlib import Path import scipy.io as sio import re import numpy as np def read_cwru_mat(mat_path): """读取单个 CWRU .mat 文件,返回 DE/FE/BA 通道和转速""" mat = sio.loadmat(str(mat_path)) channels = {} for key in mat.keys(): # 变量名形如 X097_DE_time / X097_FE_time / X097_BA_time if re.fullmatch(r'X\d+_(DE|FE|BA)_time', key): channel_name = key.split('_')[1] channels[channel_name] = mat[key].flatten() rpm = None for key in mat.keys(): # 转速变量形如 X097_RPM if re.fullmatch(r'X\d+_RPM', key): rpm = float(mat[key].flatten()[0]) break return channels, rpm

这段代码做了三件事:遍历字典里的所有键,用re.fullmatch精确匹配通道名格式,把匹配到的数据从二维列向量拉平成一维数组,最后单独抽出转速。之所以用flatten()而不是reshape(-1),是因为loadmat读出来的数组默认是N x 1的列向量,直接拿去切窗会出现意外的二维形状,后面拼接特征矩阵时容易翻车。

3.2 第二步:文件编号反查工况表

读出来只是拿到了一堆数组,还缺标签。第二步是用文件名里的编号去查META.csv,把故障位置、直径、负载这些元信息拼进来。

import pandas as pd META_PATH = Path('programs/META.csv') def load_meta(): # META.csv 列:file_id, fault, diameter, load, position return pd.read_csv(META_PATH) def parse_file_id(name): """从 X105_DE_time 这类变量名里取出文件编号 105""" m = re.search(r'X(\d+)_', name) return int(m.group(1)) if m else None

这里要注意的是position列。外圈故障的数据里,故障点相对载荷区的方向不同,官网用“中心”“正交”“反对”来描述,.mat变量名里也能看到OR相关的方向后缀。整理程序把它单独存成一列,而不是简单归到outer一个类别里,就是为了保留这个维度的信息。你也可以在后续建模时决定是丢弃它还是把它当作特征。

3.3 第三步:切窗、构造标签、落盘

原始信号每条长达十几万点,直接整条喂给分类器不现实,常规做法是滑窗切片。程序里默认window_len=2048点、stride=1024点,12 kHz采样率下每个窗口约0.17秒,既够算包络谱,又保证样本量充足。标签由fault和diameter组合生成:正常为0,内圈0.007英寸为1,内圈0.014英寸为2,其余依此类推。

def build_dataset(root_dir, window_len=2048, stride=1024): rows = [] meta = load_meta() mat_files = sorted(Path(root_dir).rglob('*.mat')) for mat_path in mat_files: file_id = parse_file_id(mat_path.name) if file_id is None: continue row = meta[meta['file_id'] == file_id] if row.empty: continue # 工况表里没有对应记录,跳过 channels, rpm = read_cwru_mat(mat_path) if 'DE' not in channels: continue sig = channels['DE'] for start in range(0, len(sig) - window_len, stride): window = sig[start:start + window_len] rows.append({ 'file_id': file_id, 'fault': row['fault'].iloc[0], 'diameter': row['diameter'].iloc[0], 'load': row['load'].iloc[0], 'rpm': rpm, 'signal': window, 'label': make_label(row['fault'].iloc[0], row['diameter'].iloc[0]), }) return pd.DataFrame(rows)

切窗时range的上界是len(sig) - window_len,这是为了避免最后一个不完整窗口被硬塞进来。窗口长度2048点不是拍脑袋定的:它刚好是2的11次方,做FFT和WELCH功率谱密度估计时不需要额外补零,频率分辨率约5.86 Hz,对轴承故障特征频率来说完全够用。如果你要识别更低频的保持架故障(FTF),可以把window_len加到4096甚至8192。

落盘环节程序提供了两个选项:一是把DataFrame整体存成parquet格式,保留signal整列;二是把每段窗口单独np.save成.npy文件,后续训练时用np.load流式读取。我一般推荐前者,因为parquet列式存储对signal这种定长数组支持不错,而且能和pandas无缝衔接。

4. 特征提取与实验划分:把振动信号变成能进模型的特征矩阵

4.1 时域统计特征:峭度、峰值因子、RMS的取舍

原始振动信号直接进线性模型效果有限,常规做法是先算一组时域统计特征,再拼成特征矩阵。最常用的四个特征:均方根值RMS反映能量大小,峰值因子反映冲击特性,峭度反映信号分布尾部厚度,波形因子用来区分平稳振动和冲击振动。

import numpy as np from scipy import stats def time_domain_features(x): rms = float(np.sqrt(np.mean(x ** 2))) peak = float(np.max(np.abs(x))) rms = rms if rms > 1e-12 else 1e-12 # 除零保护 return { 'rms': rms, 'peak': peak, 'crest_factor': peak / rms, # 峰值因子 'kurtosis': float(stats.kurtosis(x)), # 峭度,Fisher 定义 'waveform_factor': rms / float(np.mean(np.abs(x)) + 1e-12), }

峭度这里用的是scipy.stats.kurtosis的Fisher定义,即正态分布峭度为0,而不是经典教材里正态分布峭度为3的Pearson定义。你在对比别人论文里的峭度数值前先确认他用的哪种定义,否则对不上是正常的。RMS出现极小时加一个1e-12的epsilon保护,不然峰值因子会除零报错。这套特征对正常轴承和点蚀故障的区分非常灵敏,内圈故障信号里冲击成分多,峭度通常明显偏高。

4.2 频域特征:包络谱与故障特征频率对照表

时域特征只看统计量,分不清到底是内圈还是外圈故障,这时候要用频域信息。轴承故障信号的特点是高频共振被低频的故障脉冲调制,直接对原始信号做FFT看不出明显峰值,必须先做希尔伯特变换取包络,再对包络做功率谱,这就是包络谱分析。

from scipy.signal import hilbert, welch def envelope_spectrum(x, fs=12000): analytic = hilbert(x) envelope = np.abs(analytic) freq, psd = welch(envelope, fs=fs, nperseg=min(len(envelope), 1024)) return freq, psd

hilbert返回的是解析信号,实部是原信号、虚部是希尔伯特变换结果,取绝对值就得到了包络。welch用平均周期图法估计功率谱,nperseg默认取窗口长度和1024的较小值,这样短窗口信号不会报错。计算完成后,在频谱里找峰值位置,对照理论故障特征频率就能判定故障位置。

以SKF 6205为例,滚动体数9个,节圆直径39.04 mm,滚动体直径7.94 mm,接触角近似0度。设转频fr = RPM / 60,各故障的特征频率按下表估算:

故障位置特征频率近似系数(×fr)对应约30Hz转频的值
外圈 BPFO0.5×n×(1 - d/D×cosα)×fr3.58约107 Hz
内圈 BPFI0.5×n×(1 + d/D×cosα)×fr5.42约162 Hz
滚动体 BSFD/(2d)×(1-(d/D)²)×fr2.36约71 Hz
保持架 FTF0.5×(1 - d/D×cosα)×fr0.40约12 Hz

实际频谱里看到的峰值不会精确等于理论值,因为转速有波动、接触角不严格为零、轴承加工有公差,通常允许±1%到±2%的偏差。如果你发现峰值偏差超过5%,先查转速是不是用的额定值,而不是实测RPM通道值。

4.3 训练集与测试集划分:按文件分组而不是按窗口随机切

很多人在CWRU上翻车就翻在数据划分。如果直接把所有窗口合在一起随机切训练集和测试集,同一个文件切出来的相邻窗口会同时出现在两边,模型等于先背了答案,测试准确率虚高到0.99以上,换个负载立刻崩盘。正确做法是按file_id分组,整个文件的窗口要么全在训练集、要么全在测试集。

from sklearn.model_selection import GroupShuffleSplit def split_by_file(X, y, groups, train_size=0.7, random_state=42): gss = GroupShuffleSplit(n_splits=1, train_size=train_size, random_state=random_state) train_idx, test_idx = next(gss.split(X, y, groups=groups)) return train_idx, test_idx

GroupShuffleSplit的第三参数groups传file_id数组,它保证同一个组的数据不会被拆散。random_state固定成42不是为了玄学,而是为了让实验可复现。如果你想验证模型对不同负载的泛化能力,还可以用“留一负载法”:拿0、1、2 HP的数据训练,3 HP的数据测试,这种做法在故障诊断论文里很常见,也更贴近现场工况迁移的真实场景。

5. 常见问题与排查:五个翻车现场和处理办法

翻车现场一:训练集准确率99%,换负载后准确率掉到70%。

现象:同一个模型,随机划分时测试集准确率极高,一旦按负载划分立刻大幅下降。原因:随机划分把同一文件的相邻窗口同时分到训练和测试集,数据严重泄漏,模型学的是窗口间的局部噪声而非真正的故障特征。解决:一律按file_id分组,用GroupShuffleSplit或直接按负载划分。做完划分后检查一下训练集和测试集有没有重复的file_id,这一步值得每次跑实验前都验证一遍。

翻车现场二:把12 kHz和48 kHz的数据混在同一个数据集里,训练直接报维度错误。

现象:报错信息通常是ValueError: operands could not be broadcast,或者特征矩阵行数对不上。原因:12 kHz和48 kHz的同一段信号长度差4倍,切窗后每个窗口的样本点数完全不同,特征维度自然不一致。解决:要么只用12 kHz子集做全套实验,要么把48 kHz数据重采样到12 kHz再合并。重采样用scipy.signal.resample,注意先滤波再抽点,避免混叠。

翻车现场三:外圈故障的标签错乱,同一类故障准确率特别低。

现象:outer类别被分成了好几簇,混淆矩阵里outer互相混。原因:外圈故障有“中心”“正交”“反对”三种位置,故障点相对载荷区的角度不同,振动传递路径不同,包络谱特征差异明显。把它们当成同一个类,特征分布变成多峰,模型自然学不好。解决:整理阶段保留position字段,建模时要么把位置作为独立类别,要么固定只用某一个位置的样本做训练。我见过一些论文把外圈三个位置当作三个类别,准确率反而更高,因为物理上它们确实是三种不同工况。

翻车现场四:包络谱里找不到理论故障频率的峰值。

现象:明明用了hilbert和welch,频谱里却找不到对应107 Hz或162 Hz的尖峰。原因:转速用的额定值而不是实测值,转频偏差导致特征频率偏移;另一个常见原因是窗口太短,频率分辨率不足,相邻频率分量糊在一起。解决:先用RPM通道算出实际转频,再代入公式;窗口长度至少设1024点,做低速轴承分析时建议4096点起步。另外原始信号里有直流偏置,先做x - np.mean(x)去均值再算包络,否则零点附近的低频分量会盖住故障峰值。

翻车现场五:loadmat直接报错,提示不是有效的MAT文件。

现象:scipy.io.loadmat抛出NotImplementedError或ValueError,但文件在MATLAB里能正常打开。原因:CWRU官网部分文件用MATLAB 7.3及以上版本保存,用的是HDF5格式,loadmat老版本不支持。解决:用h5py读取这类文件,或者先确认文件头是MATLAB 5.0 MAT-file还是HDF5。整理程序里我加了一个格式探测分支,按文件头自动选择loadmat还是h5py,你在本地复现时如果报错,优先检查这一层。

6. 进阶:把整理好的数据接到机器学习分类流程

整理程序输出的parquet文件已经是一张标准的宽表:每行一个窗口样本,含signal数组、工况标签和特征列。进阶用法是跳过手工特征,直接把signal数组当输入,交给随机森林或一维卷积网络做端到端分类。这里用一个随机森林跑通基线作为示范。

import pandas as pd import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler df = pd.read_parquet('cwru_processed.parquet') X = np.stack(df['signal'].values) # 形状:(样本数, 2048) y = df['label'].values train_idx, test_idx = split_by_file( X, y, groups=df['file_id'].values, train_size=0.7) model = make_pipeline( StandardScaler(), RandomForestClassifier(n_estimators=200, random_state=42) ) model.fit(X[train_idx], y[train_idx]) print('Test Accuracy:', model.score(X[test_idx], y[test_idx]))

np.stack把signal列里的多个一维数组堆叠成二维矩阵,这是把窗口数据送入sklearn的标准姿势。StandardScaler对每个特征维度做零均值单位方差归一化,对树模型影响不大,但如果你后面换用SVM或KNN就有必要。随机森林的优势在于不需要调参就能拿到一个可靠的基线准确率,200棵树在这个数据规模下训练很快。

验证环节我习惯跑三件事:第一,打印分类报告,看在normal和inner之间有没有误判;第二,把模型对某个外圈故障窗口的预测概率画出来,确认不是靠类别不平衡硬猜;第三,用留一负载法重新训练一版,记录准确率下降幅度。这三步走完,对模型到底学到什么才算心里有底。

CWRU这套数据最大的优点也是最大的坑:它太干净了,故障是打出来的标准坑,不存在实际工况里的变转速、变负载和噪声干扰。用它可以验证算法流程,但别把它的准确率当成现场部署的预期值。从那以后,我每次拿CWRU做基准实验都强制走一遍按文件分组划分、检查重复file_id的小脚本,再顺手看一眼测试集里每个类别的样本数是否均衡。这套习惯帮我挡掉了很多次“高准确率”的假象,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询