简介:面向机械振动信号处理与设备状态监测学习者的轴承故障诊断 MATLAB 代码包,围绕滚动轴承振动数据的故障特征提取与类型判别展开,适合具备一定信号处理基础、希望把课堂理论落到代码实践的高年级本科生、研究生及工程技术人员参考。压缩包共 30 个文件,约 1.04MB,以 5 个 m 脚本承载 FFT、Hilbert 包络谱、Haar 小波与数学形态学等分析流程,11 个 dat 文件提供振动数据,14 张 jpg 用于结果图或谱图对照,便于按“数据—算法—图形”线索快速梳理。内容覆盖时域无量纲参数、快速傅里叶变换、包络解调、小波多分辨率分解和形态学滤波去噪,可帮助读者理解内圈、外圈及滚动体故障特征频率的提取思路。已有 6610 人学习,适合用作课程设计、实验复现和故障诊断入门练手,通过运行与修改脚本积累信号预处理、特征构造和结果判读经验。
1. 一段 12k 采样的振动波形摆在面前:这套 MATLAB 轴承诊断代码在算什么
轴承故障诊断落到工程上,其实就一句话:从一段看似杂乱的振动波形里,把那个反复出现的冲击周期找出来。拿 matlab 打开现场采回来的 12k 采样数据,屏幕上是一片看不出规律的毛刺,直接 FFT 只见一堆转频谐波,真正管用的往往是包络解调加 haar 小波去噪这一套组合拳。这份代码包做的就是这件事——读.mat、去噪、算频谱、算包络谱、对照理论故障特征频率给出结论,一条流水线跑完。
它适合两类人:手上已经有振动数据、想快速验证“到底是内圈还是外圈”的现场工程师;以及刚接触故障诊断、需要一份能跑通的最小可用骨架的学生和转岗开发者。代码量不大,但把数据装载、分段、频谱、包络、特征提取、批量落表这几段都占全了,属于那种“跑一遍就懂、改两行就能用在别处”的结构。
2. 工程结构拆解:从 .mat 原始数据到诊断结论的完整链路
这份代码最容易被低估的地方是目录组织。很多人拿到压缩包第一反应是打开main.m从头读到尾,结果被几十行参数和绘图代码糊住,反而看不清数据是怎么流的。把它按“读入—预处理—变换—判决”四段拆开看,脉络就很清楚。
2.1 目录长什么样,每个文件负责哪一段
解压后大致是这样一棵树:
bearing_fault_diag/ ├── main.m # 单文件入口,一条信号跑完全流程 ├── batch_run.m # 批量跑整个 data/ 目录,输出特征表 ├── data/ # 原始振动数据,每个工况一个 .mat │ ├── normal_1730rpm_0hp.mat │ ├── inner_1730rpm_0hp.mat │ ├── outer_1730rpm_0hp.mat │ └── ball_1730rpm_0hp.mat ├── func/ │ ├── load_signal.m # 统一读取,兼容不同变量名 │ ├── segment_signal.m # 按点数切段、加窗 │ ├── fft_spectrum.m # 单边幅值谱 │ ├── envelope_demod.m # 带通 + 希尔伯特包络谱 │ ├── haar_denoise.m # haar 小波阈值去噪 │ ├── fault_freq.m # 四个理论故障特征频率 │ ├── feat_extract.m # 时域/频域特征 │ └── plot_helper.m # 绘图封装,统一坐标轴 ├── result/ # 特征表、图片输出 └── README.mdfunc/下每个文件只做一件事,函数签名统一以“一段信号 + 采样率”开头,返回“频率轴 + 幅值”或者“一个特征结构体”。这种切法的好处是二次开发时替换其中一环不会牵连其它模块,比如你想把 haar 换成 db4,只动haar_denoise.m一行字符串就行。常见做法是再补一个config.m把参数集中起来,避免main.m和batch_run.m各写一份转速。
| 目录/文件 | 职责 | 改动频率 |
|---|---|---|
data/ | 原始振动信号 | 高,换数据就换这里 |
func/ | 可复用的算法单元 | 中,调参或换算法 |
main.m | 单条信号演示流程 | 低 |
batch_run.m | 批量统计与落表 | 中,加特征列 |
result/ | 输出产物 | 不手动改 |
2.2 cfg 参数配置:那几个数决定结论对不对
整个流程的准确性,一半压在几个物理参数上。转速错了,后面算出来的理论故障频率全偏,包络谱上明明有峰你也对不上号。
% main.m 顶部的参数区,batch_run.m 里也有一份,务必保持一致 cfg.fs = 12000; % 采样频率 Hz,按采集卡实际设置填 cfg.rpm = 1730; % 实测转速,用转速计,别抄铭牌额定值 cfg.n_ball = 9; % 滚动体个数 cfg.d_ball = 7.94e-3; % 滚动体直径 m cfg.D_pitch = 39.04e-3; % 轴承节径 m(外圈滚道中心到内圈滚道中心) cfg.contact = 0; % 接触角,深沟球轴承取 0 cfg.seg_len = 4096; % 每段点数,决定频率分辨率 cfg.overlap = 0.5; % 相邻段重叠比例 cfg.level = 5; % haar 分解层数 cfg.band = [2000 5000];% 共振解调带通范围 Hzfs从采集卡设置里来,不是猜的。rpm也一样——铭牌 1730 rpm 是额定值,实际带载后可能差几十转,这一差在包络谱上就是好几赫兹的偏移,用眼睛找峰时够你难受半天。seg_len取 4096,是因为 12000/4096 ≈ 2.93 Hz 的频率分辨率,刚好能把 100 Hz 附近的外圈特征频率和它的边频分开;取 1024 点分辨率只有 11.7 Hz,边频直接被抹平。
提示:
d_ball和D_pitch查轴承手册,单位统一成米。手册上给的是毫米,忘了换算会让特征频率错三个数量级,这是新手最常踩的一脚。
2.3 load_signal 与信号分段:维度、直流、长度三件套
原始数据五花八门,变量名可能叫vib、DE、x、data,长度也可能不是 2 的整数次幂。load_signal.m干的活就是把这些差异吞掉。
function s = load_signal(fp) % 统一读取 .mat,兼容常见的几种变量命名 raw = load(fp); fn = fieldnames(raw); keys = {'vib','DE','x','data','signal'}; name = ''; for i = 1:numel(keys) hit = strcmpi(fn, keys{i}); % 不区分大小写匹配 if any(hit) name = fn{hit}; break; end end if isempty(name), name = fn{1}; end % 兜底取第一个变量,避免报错中断 v = double(raw.(name)); % 统一转 double,防止 int16 参与 fft v = v(:); % 压成列向量,消除行列不一致 v = v - mean(v); % 去直流,否则 0 Hz 处一个大峰 s.vib = v; s.src = fp; end三行处理的每一行都有原因。double是因为很多采集设备存的是int16,直接送进fft会参与整型运算,幅值溢出或者被截断;v(:)是防止有的文件存成行向量、有的存成列向量,拼起来的时候维度对不上;去均值则是为了压掉 0 Hz 的直流分量,不然频谱第一根柱子能顶到天上,纵轴一自动缩放,其它频率全被压成一条直线。
分段在segment_signal.m里,逻辑简单但别省:
function segs = segment_signal(x, seg_len, overlap) % 按固定点数切段,末尾不足一段的直接丢弃 step = round(seg_len * (1 - overlap)); n = floor((numel(x) - seg_len) / step) + 1; segs = zeros(n, seg_len); w = hann(seg_len).'; % 汉宁窗,减小频谱泄漏 for k = 1:n idx = (k-1)*step + (1:seg_len); seg = x(idx).'; segs(k,:) = (seg - mean(seg)) .* w; % 每段单独去均值再加窗 end end加窗会带来幅值衰减,汉宁窗的相干增益是 0.5,严格做幅值标定时要乘 2 补回来。做故障有无的判断时通常不做这个补偿,因为看的是峰的位置和相对高低,不是绝对幅值。这个取舍要先想明白,否则换了加窗函数发现幅值腰斩,会误以为代码坏了。
2.4 main.m 主流程:一条信号走完五个处理站
入口脚本本身很薄,真正的工作都在函数里。这种“薄 main”的写法值得学,读的人一眼能看清数据流向。
%% main.m —— 单条信号诊断演示 clear; clc; close all; cfg = get_cfg(); % 参数集中管理,见 2.2 fp = fullfile('data','outer_1730rpm_0hp.mat'); s = load_signal(fp); x = s.vib.'; [xd, ~, ~] = haar_denoise(x, cfg.level); % 1. 小波去噪 [f, P1] = fft_spectrum(xd, cfg.fs); % 2. 单边幅值谱 [fe, eP] = envelope_demod(xd, cfg.fs, cfg.band); % 3. 包络谱 ft = fault_freq(cfg); % 4. 理论特征频率 feat = feat_extract(xd, cfg.fs); % 5. 特征向量 fprintf('转速 %.0f rpm,转频 %.2f Hz\n', cfg.rpm, ft.fr); fprintf('理论 BPFO %.2f Hz / BPFI %.2f Hz / BSF %.2f Hz\n', ... ft.bpfo, ft.bpfi, ft.bsf); fprintf('时域峭度 %.3f,峰值因子 %.3f\n', feat.kurt, feat.crest); plot_helper(f, P1, fe, eP, ft, s.src); % 三联图:频谱 + 包络谱 + 标注按 1730 rpm、9 个滚动体、直径比 7.94/39.04 算下来,转频 28.83 Hz,外圈通过频率约 103.4 Hz,内圈约 156.1 Hz,滚动体自转约 67.9 Hz,保持架约 11.5 Hz。这几个数就是后面找峰的“标准答案”。包络谱上如果在 103 Hz 附近出现明显峰,并且 206 Hz、309 Hz 处有它的谐波,基本可以判定外圈有问题;如果峰出现在 156 Hz 且两侧带 28.8 Hz 的边频,那就是内圈——内圈随轴转,故障点进出载荷区,天然带转频调制。
注意:
plot_helper里把包络谱纵轴做了归一化,不同文件放一张图上才有可比性。如果你自己改代码去掉了归一化,几张图叠在一起会完全看不出峰。
3. FFT 与包络解调:把埋在共振带里的故障频率挖出来
新手最容易犯的错,是拿原始信号直接 FFT,然后盯着 100 Hz 附近找峰,找不到就怀疑数据有问题。实际上轴承的局部损伤产生的是一串冲击,冲击激励的是轴承座和壳体的结构共振,能量集中在几千赫兹的高频段。故障特征频率本身并不直接出现在频谱上,它只以“重复频率”的形式调制着那个高频载波。要把它提取出来,必须走包络解调这条路。
3.1 直接 FFT 为什么看不到故障频率
一个局部剥落的滚道,每被滚动体碾过一次就产生一次冲击。冲击的频谱很宽,能量铺在几百到上万赫兹;而冲击的重复频率——也就是我们关心的 BPFO 或 BPFI——只是这个宽频带的包络变化频率。原始频谱里,高频共振区的幅值被这些冲击抬高,但你要找的那个 100 Hz,混在转频谐波、不平衡、不对中这些低频成分里,被压得几乎看不见。
更麻烦的是信噪比。现场数据里,齿轮啮合、电机电磁噪声、结构共振都会盖过轴承信号。所以合格的流程一定是“先去噪、再带通选共振频带、然后做包络、最后对包络做 FFT”。少一步,很多时候就是从“能看出来”变成“看不出来”。血泪经验是:不带通直接做包络,包络谱上往往只剩转频和它的倍频,看着像模像样,其实什么都没诊断出来。
3.2 单边幅值谱的正确写法
fft_spectrum.m是整个包里被调用次数最多的函数,也是最容易写错的一个。写错的典型症状是幅值差 2 倍,或者频率轴对不上。
function [f, P1] = fft_spectrum(x, fs) % 返回单边幅值谱,频率轴单位 Hz x = x(:).'; % 强制行向量 x = x - mean(x); % 去直流,重复一次以防调用方忘了 N = numel(x); Y = fft(x); P2 = abs(Y) / N; % 双边谱幅值,先除以点数 nh = floor(N/2) + 1; % 正频率部分取到奈奎斯特 P1 = P2(1:nh); if mod(N, 2) == 0 P1(2:end-1) = 2 * P1(2:end-1); % 偶数点,去掉 0 和 Nyquist 后翻倍 else P1(2:end) = 2 * P1(2:end); % 奇数点,Nyquist 不单独占一格 end f = fs * (0:nh-1) / N; % 频率分辨率 fs/N end两个细节值得掰开说。第一个是除以N:fft出来的结果是“和”,不是“平均”,不除N的话点数越多幅值越大,换个段长结论就变了。第二个是乘 2:实信号的负频率部分是正频率的镜像,能量被劈成了两半,取单边谱就得把正频率那半翻倍补回来。至于偶数点和奇数点的区别,只在奈奎斯特那一格——偶数点时它是独立的最后一格,不能翻倍,翻了就重复计一次能量。
f = fs*(0:nh-1)/N这一行决定了你能分辨多细的频率间隔。12000 Hz 采样、4096 点,分辨率 2.93 Hz。你做一次 ±1% 的特征频率搜索,在 103 Hz 附近就是 ±1 Hz 都不到,还没一个频率分辨率宽,搜索窗口至少开到 ±3 个分辨率,也就是 ±9 Hz,否则真实峰偏离一点就漏了。
3.3 希尔伯特包络解调:带通、包络、再 FFT
function [f, envP] = envelope_demod(x, fs, band) % 带通选共振频带 -> 希尔伯特包络 -> 包络谱 x = x(:).' - mean(x); if nargin >= 3 && ~isempty(band) Wn = band / (fs/2); % 归一化到奈奎斯特 [b, a] = butter(4, Wn, 'bandpass'); % 4 阶足够,再高容易数值不稳 x = filtfilt(b, a, x); % 零相位滤波,避免包络整体时移 end env = abs(hilbert(x)); % 解析信号的模即瞬时包络 env = env - mean(env); % 去掉包络的直流,否则 0 Hz 处压过一切 [f, envP] = fft_spectrum(env, fs); envP = envP / (max(envP) + eps); % 归一化,便于多组数据同图对比 endband选哪儿,是这套流程里最依赖经验的参数。常用的判断办法是先看原始频谱,找那种“鼓起来一片、但没有明显单根谱线”的频段,那就是结构共振带。3.1 小节的例子取 2000~5000 Hz,在 12k 采样下奈奎斯特是 6000 Hz,这个范围不越界。选低了会把转频谐波漏进包络,选高了贴近奈奎斯特,滤波器过渡带被压扁,反而滤不干净。
filtfilt而不是filter,是因为前者前后各滤一遍,相位抵消,包络不会整体滞后。做单次分析时这点延迟无所谓,但如果你要拿包络去做冲击定位、算时间差,相位一偏结论就错了。butter的阶数取 4,再往上在filtfilt里容易出现数值不稳定,特别是低频截止离 0 很近的时候,会看到滤波结果开头一段剧烈振荡。
3.4 理论故障特征频率:四个公式和一次对谱
function ft = fault_freq(cfg) % 输入几何参数与转速,输出四个理论故障特征频率 fr = cfg.rpm / 60; % 转频 Hz r = cfg.d_ball / cfg.D_pitch * cosd(cfg.contact);% 直径比,接触角为 0 时为 d/D n = cfg.n_ball; ft.fr = fr; ft.bpfo = n/2 * fr * (1 - r); % 外圈通过频率 ft.bpfi = n/2 * fr * (1 + r); % 内圈通过频率 ft.bsf = cfg.D_pitch/(2*cfg.d_ball) * fr * (1 - r^2); % 滚动体自转频率 ft.ftf = fr/2 * (1 - r); % 保持架频率 end这四个式子的物理含义不难理解:滚动体经过外圈某一点,每转一圈经过n/2 * (1-r)次,乘上转频就是每秒通过次数。内圈因为滚动体还要“追”着内圈滚道跑,通过次数更多,所以是1+r。滚动体自转最快,因为它既绕轴公转又绕自身轴自转。保持架最慢,只有转频的一半左右。
对谱时不要死盯着一个点。正确的找法是:在bpfo ± 1%范围内搜包络谱最大值,再检查2×bpfo、3×bpfo处是否也有峰,以及峰的两侧有没有fr间隔的边频。谐波和边频同时成立,才叫“对上了”。只有一根孤零零的峰,很可能是电磁噪声或者某阶共振,别急着下结论。
4. Haar 小波去噪与特征提取:从一条波形到一行特征向量
去噪这一步在整条链路里的位置有点微妙。理论上包络解调本身就有抑制低频干扰的作用,但真实数据里冲击往往被宽带噪声埋得只剩一点点,直接做包络,包络谱上那根特征频率的峰可能只比底噪高两三倍,靠肉眼判断就变成玄学了。小波去噪把冲击保留、把高斯白噪声压下去,峰凸显出来,判断就有了底气。
4.1 为什么这个场景下 haar 够用
小波族很多,db4、sym8、coif5 各有拥趸,为什么这份代码默认用 haar?原因不玄乎:haar 是最简单的正交小波,滤波器长度只有 2,分解重构没有重叠带来的边界拖尾,处理几千点一段的短信号时边界效应最小。它的频域过渡带确实粗糙,频带划分不锐利,但你做的是“把噪声压掉、把冲击留住”,不是做精细的频带分离,粗糙一点无所谓。
另一个好处是可解释性强。haar 的一层分解就是把序列两两平均(近似系数)和两两求差(细节系数),人脑能跟上。调试时你可以直接把第一层细节系数画出来,冲击在哪儿一目了然,不像 db4 那样一堆卷积系数看得头晕。当然,如果你的信号里冲击很缓、频率很低,haar 的阶跃特性会显得不够平滑,那时候换 db4 更合适——改一行字符串的事。
4.2 分解、阈值、重构三步实现
function [xd, c, l] = haar_denoise(x, level, mode) % haar 小波 level 层分解,对细节系数做阈值收缩后重构 if nargin < 3, mode = 's'; end % 's' 软阈值,'h' 硬阈值 x = x(:).'; % 1. 长度截断到 2^level 的整数倍,避免 waverec 后长度对不上 n = 2^level * floor(numel(x) / 2^level); x = x(1:n); % 2. 分解 [c, l] = wavedec(x, level, 'haar'); % 3. 用第一层细节系数鲁棒估计噪声标准差 d1 = c(end - l(end) + 1 : end); sigma = median(abs(d1)) / 0.6745; thr = sigma * sqrt(2 * log(n)); % 通用阈值(VisuShrink) % 4. 只对细节系数收缩,近似系数原样保留 idx = l(1) + 1 : numel(c); c(idx) = wthresh(c(idx), mode, thr); % 5. 重构 xd = waverec(c, l, 'haar'); xd = xd(1:numel(x)); end第 3 步的median(abs(d))/0.6745是这套方法的核心。高斯白噪声的绝对值中位数和标准差之间有固定比例关系,用中位数估标准差,不会被少数几个大幅值的冲击带偏——这正是我们要的,因为冲击恰恰是要保留的信号。要是用std(d1)去估,冲击一大,估出来的 sigma 跟着变大,阈值水涨船高,最后把冲击一起削平了,这就是典型的“去噪去过头”。
第 4 步的l(1)+1 : numel(c)是把近似系数跳过、只动细节系数。近似系数里装的是信号的主体轮廓和低频成分,动了它波形就变形。l是wavedec返回的长度向量,第一个元素是最高层近似系数的长度,后面的依次是各层细节系数长度,顺序是从高层到低层,所以第一层细节系数在数组末尾。
关于阈值模式,软阈值把超过阈值的系数整体收缩,重构出来更平滑,但冲击的幅值会被削掉一块;硬阈值只把小于阈值的置零,幅值保得住,代价是可能引入一点振铃。诊断场景下我一般先用软阈值,看到冲击被压得太扁再换硬阈值。这个切换只要把mode参数从's'改成'h'。
4.3 十个特征指标:feat_extract.m 里的时域与频域量
只看谱形做判断,换个工况就容易翻车。稳妥的做法是再抽一组数值特征,既能做阈值判断,也能直接喂给分类器。
function feat = feat_extract(x, fs) % 时域 9 个 + 频域 1 个,返回结构体,方便 struct2table x = x(:).' - mean(x); ax = abs(x); feat.rms = sqrt(mean(x.^2)); % 有效值,反映整体能量 feat.peak = max(ax); % 峰值 feat.p2p = max(x) - min(x); % 峰峰值 feat.kurt = kurtosis(x); % 峭度,对冲击最敏感 feat.skew = skewness(x); % 偏度,反映波形不对称 feat.crest = feat.peak / feat.rms; % 峰值因子 feat.shape = feat.rms / mean(ax); % 波形因子 feat.impulse = feat.peak / mean(ax); % 脉冲因子 feat.margin = feat.peak / mean(sqrt(ax)).^2; % 裕度因子 feat.sk = spectral_kurtosis(x, fs); % 谱峭度,指示共振带位置 end function sk = spectral_kurtosis(x, fs) % 用短时傅里叶变换近似算谱峭度,取最大值 win = round(0.01 * fs); hop = round(win/2); [S, ~] = spectrogram(x, hann(win), win-hop, 256, fs); P = abs(S).^2 + eps; sk = mean(P.^2, 2) ./ (mean(P,2).^2) - 2; % 复高斯噪声理论值为 0 sk = max(sk); end峭度是这个列表里最值得盯的一个。正常轴承的振动接近高斯分布,峭度在 3 附近;一旦出现局部损伤,冲击把分布的尾巴拉长,峭度能飙到 5 以上甚至十几。它的好处是不依赖转速和轴承几何参数,坏处是对偶然的野点也敏感,所以一般配合峰值因子一起看。
谱峭度则是帮你选带通范围的工具。它对每个频点算“这个频带的信号有多像冲击”,值最大的频段就是共振带所在。有了它,3.3 节的band就不用凭经验试了,直接取谱峭度曲线的峰值位置往两边扩一段。这也是我在实际项目里最常用的一个组合:先谱峭度定带,再包络解调对谱。
裕度因子peak / mean(sqrt(ax))^2分母上那个平方容易写错,写成mean(sqrt(ax))^2和(mean(sqrt(ax)))^2是一回事,但如果你写成mean(ax)就退化成了峰值因子。这类细节不影响程序跑,只影响特征的可比性,改的时候对着公式核一遍。
4.4 批量跑数据与结果落表
单条信号看波形是演示,真正干活是整目录批处理。batch_run.m的结构大概是:
files = dir(fullfile('data','*.mat')); rows = struct('file',{},'rms',{},'kurt',{},'crest',{}, ... 'bpfo_ratio',{},'bpfi_ratio',{},'label',{}); for k = 1:numel(files) s = load_signal(fullfile('data', files(k).name)); xd = haar_denoise(s.vib.', cfg.level); [fe, eP] = envelope_demod(xd, cfg.fs, cfg.band); ft = fault_freq(cfg); % 特征频率附近 ±2% 的峰值与全局中位数之比,作为“有没有峰”的量化 m1 = fe > ft.bpfo*0.98 & fe < ft.bpfo*1.02; m2 = fe > ft.bpfi*0.98 & fe < ft.bpfi*1.02; rows(k).file = files(k).name; rows(k).rms = rms(xd); rows(k).kurt = kurtosis(xd); rows(k).crest = max(abs(xd)) / rms(xd); rows(k).bpfo_ratio = max(eP(m1)) / median(eP); rows(k).bpfi_ratio = max(eP(m2)) / median(eP); rows(k).label = regexp(files(k).name,'^[a-z]+','match','once'); end T = struct2table(rows); writetable(T, fullfile('result','features.csv')); disp(T);bpfo_ratio这个量比“看有没有峰”靠谱得多。分母用包络谱的中位数当底噪基准,分子用特征频率附近的峰值,比值超过 5 基本可以认为是真峰。这样即使换了设备、换了传感器灵敏度,判据依然成立,不用每次重新调阈值。label从文件名前缀抠出来,是为了后面做有监督的验证,命名规范在这里的价值就体现出来了——文件名乱起,这批数据基本就废了一半。
5. 避坑与排查:五个让诊断结论跑偏的细节
代码能跑通和结论能信,是两件事。下面这几条是实际调试里反复撞上的,按“现象—原因—解决”记下来,省得下次再从头查一遍。
5.1 包络谱上只有 0 Hz 一根巨峰,后面全是平的
现象:画出包络谱,第一根柱子顶到天上,纵轴一自动缩放,100 Hz 往后的部分被压成一条贴近横轴的直线,什么都看不见。
原因:包络信号的均值没有被去掉。abs(hilbert(x))出来的包络恒为正,均值是一个不小的直流分量,做 FFT 后全落在 0 Hz 那一格,幅值比交流成分大一到两个数量级。MATLAB 自动缩放纵轴,其余部分自然被压没了。
解决:包络之后必须再减一次均值,env = env - mean(env)。如果减完还是有个矮一点的 0 Hz 峰,那就是带通滤波器的低频残留,把band的下限往上提个几百赫兹,或者直接画图时把频率轴从 5 Hz 开始。
5.2 换一台设备,所有特征频率都对不上
现象:同样的代码、同样的轴承型号,在 A 设备上峰正好落在理论值上,搬到 B 设备,峰偏移了 4~6 Hz 甚至更多,怎么调都对不齐。
原因:一是转速用了铭牌额定值。带载之后实际转速可能比额定低几十转,转频一变,四个特征频率按比例全偏。二是滚动体存在打滑,实际通过频率通常比理论值低 1%~2%,这是滚动轴承的固有特性,不是误差。
解决:转速用转速计实测,别信铭牌;对谱窗口从 ±1% 放宽到 ±3%,同时把理论值乘 0.98 的修正系数再搜一遍。如果两个位置都有峰,那基本可以确认是打滑导致的偏移,按实测峰为准反推转速更可靠。
5.3 waverec 重构后长度和原信号对不上,画图报维度错误
现象:haar_denoise返回的xd和输入x长度差一两个点,后面做减法画对比图时报“矩阵维度必须一致”。
原因:小波分解要求信号长度是 2^level 的整数倍,wavedec内部会做边界延拓,waverec出来的长度等于延拓后的长度,不保证和原始长度相同。层数越高,差得越多。
解决:进函数前先截断,n = 2^level * floor(numel(x)/2^level); x = x(1:n);,出函数后再保险一次xd = xd(1:numel(x));。如果原始信号本来就短于 2^level,比如只有 512 点却设了 10 层分解,那floor直接得 0,整段信号被扔掉——这种情况要么降层数,要么补零,别硬上。
5.4 去噪之后冲击没了,峭度反而比去噪前更低
现象:加了一层 haar 去噪,波形确实光滑了,但峭度从 6.2 掉到 3.1,包络谱上的特征峰也跟着矮下去,等于把有用的东西一起抹了。
原因:典型的是层数给多了,或者阈值估偏了。层数越高,被收缩的细节系数越多,冲击这种宽带的瞬态成分在高层细节里也有分布,一并被压掉。另外如果用std而不是中位数去估噪声,冲击会污染 sigma,阈值被抬得过高。
解决:层数从 3 层起步试,看峭度保留情况再往上加,一般 4~5 层封顶;噪声估计坚持用median(abs(d1))/0.6745;软阈值压得太狠就换硬阈值。判断标准很简单——去噪后峭度至少要保持去噪前的 80%,掉太多说明方法用过头了。
5.5 分类准确率 99%,换一批数据就崩到 60%
现象:用自己采的数据训练一个分类器,交叉验证准确率漂亮得不像话,换个工况、换台设备重新测,准确率断崖式下跌。
原因:同一批数据是按段切分的,相邻段之间重叠 50%,训练集和测试集里存在大量高度相关的样本,等于把答案漏进了考卷。再加上特征没有归一化,rms动辄零点几、kurt是几、bpfo_ratio能到几十,量纲差两个数量级,距离-based 的分类器会被大数值特征完全主导。
解决:切分时按“文件级”而不是“段级”划分训练测试集,同一个.mat里的段只能进同一个集合;特征统一做 z-score 标准化;报告结果时给出混淆矩阵而不是单一准确率。这三件事做了,准确率往往会掉十几个点,但那才是真实水平。
6. 二次开发:换轴承型号、换采样率、接分类器的三条改造路径
拿到这份代码,多数人的下一步都是“用到自己的设备上”。改动其实集中在三个地方,而且都不需要动主流程。
第一处是参数表化。把cfg里的轴承几何参数抽成一张表,按型号查表取值:
function cfg = get_cfg(model) % 常见型号的几何参数查表,找不到就报错,别默默用默认值 T = readtable('bearing_params.csv'); % 列:model,n,d,D,contact row = T(strcmpi(T.model, model), :); assert(~isempty(row), '型号 %s 不在参数表中', model); cfg.fs = 12000; cfg.rpm = 1730; cfg.n_ball = row.n; cfg.d_ball = row.d*1e-3; cfg.D_pitch = row.D*1e-3; cfg.contact = row.contact; end第二处是采样率与频带。fs一换,band必须跟着换,因为共振带是按绝对频率选的。稳妥的做法是用 4.3 节的谱峭度自动定带,把band从手工输入改成[f_peak*0.6, f_peak*1.6],跟着数据走而不是跟着经验走。同时确认新采样率下的奈奎斯特频率高于band上限,否则butter会直接报归一化频率越界。
第三处是接分类器。特征表已经落成 CSV,直接喂给分类模型就行,用fitcecoc做多分类、按文件级划分训练测试集、输出混淆矩阵,这三步是验证的底线。我一般会把bpfo_ratio、bpfi_ratio、峭度、谱峭度这四个作为主特征,其余作参考——特征不是越多越好,冗余特征在样本量小的时候只会让模型学到噪声。
从那次 99% 准确率翻车以后,我每次做诊断验证都强制走一遍“按文件划分 + 特征标准化 + 看混淆矩阵”这三步,少一步都不敢往报告里写。这套代码的价值不在算法多新,而在于它把从原始波形到判决结论的每一环都摆在了明面上,你想改哪一环,改完立刻能在图上看到后果。希望帮到你。
本文还有配套的精品资源,点击获取