前阵子做设备状态监测现场采集来的振动信号像一锅粥工频基波、三次谐波、五次谐波叠在里头还混着随机噪声。数据量不是实验桌上的几千个点而是以百万为单位的连续采样。以前我用SVD加软阈值去噪效果没得说可矩阵一旦超过几千阶Matlab就卡得让人想砸键盘。后来我把随机奇异值分解Randomized SVD引进来和软阈值配合使用去噪精度几乎不变计算时间却降了一个数量级。这篇就把完整思路和Matlab实现端出来供做信号去噪、谐波分析、振动监测的同行参考。1. 传统SVD软阈值去噪为什么数据一变大就卡死1.1 SVD去噪的底层逻辑SVD去噪算不上新鲜。把一维信号按固定窗口做延迟嵌入得到一个Hankel矩阵也叫轨迹矩阵。这个矩阵天然具有结构如果信号本身由几个谐波叠加而成矩阵的秩大约就是谐波数的两倍。比如一个50Hz基波加上150Hz、250Hz、350Hz三个谐波对应Hankel矩阵的有效秩大概就是8每个正弦分量贡献2阶。而白噪声对应的矩阵没有这种低秩结构奇异值铺得很平。于是只要对奇异值做收缩处理把绝对值小的那批用软阈值压到零再用前面的奇异向量重构就能把噪声滤掉。这个逻辑和“把一本书里的关键章节抽出来重排丢掉废字”是一回事。软阈值处理的是奇异值序列不是时域波形这是它区别于普通低通滤波的核心。1.2 复杂度才是真正的敌人直接SVD的问题不在精度而在规模。对M点信号窗口长度取M/2的话矩阵大约是M/2乘M/2标准SVD复杂度在O(M³/8)量级。M2000时矩阵1000×1000Matlab大概零点几秒能算完M4000时矩阵2000×2000就要四五秒了。再往上走矩阵变成10000×10000标准SVD可能要几分钟而且内存占用直线上升。我在实际项目里遇到过连续采集20万点振动数据的情况。如果一次构出10万×10万的Hankel矩阵double类型光是存储就要80GB压根不是“慢”的问题是直接内存不足。强行缩小窗口倒是能跑但窗口一缩频率分辨率下降谐波和噪声在频域上更难分开去噪效果大打折扣。所以必须在算法层面做文章不能靠堆硬件硬扛。2. 用随机SVD近似奇异值分解 软阈值这对组合好在哪里2.1 随机SVD的三步套路随机SVD的核心思想是如果矩阵本身就低秩那我们就先随机抽样到它主要的列空间再在这个低维空间里做一次精确SVD。标准流程只有三步生成一个随机高斯矩阵Ω大小是n×r其中rkpk是目标秩p是额外的采样数通常取5到10计算YAΩ再对Y做QR分解得到正交基QQ的列张成了A的主要列空间在投影矩阵BQᵀA上做标准SVD再把左奇异向量乘回Q最终得到U≈Q·Ub、S、V。这样一来大矩阵A根本不用完整访问只需要几次矩阵乘法和一次小矩阵SVD。复杂度从O(mn²)降到O(mnkk²n)级别。用大白话说就是先通过随机投影“摸一遍”整本书的轮廓然后再精读抽出来的那几十页而不是把每页都翻十遍。2.2 软阈值为什么好过硬阈值软阈值的操作很简洁对每个奇异值s用ŝsign(s)·max(|s|-τ,0)更新τ是设定阈值。和硬阈值相比软阈值不是直接截断而是把大于τ的奇异值也都往零方向压缩一个τ。好处是重构出的波形更平滑不会因为某个奇异值被硬切而产生额外的高频“咔嚓”声。从优化角度看软阈值是近端梯度法的标准形式和L1范数惩罚的稀疏优化在数学上是同一回事。谐波信号恰好满足“奇异值稀疏”这一前提所以它在工程上非常稳。换硬阈值虽然也能保留主要成分但在低信噪比场景下硬阈值会让重建信号呈现出明显的分段跳跃实测效果远不如软阈值。2.3 为什么适合谐波去噪场景谐波成分数目少、频率固定对应的Hankel矩阵秩低随机SVD恰好擅长逼近低秩矩阵。大数据场景下我们不关心矩阵万分之零点几的F范数误差只关心主谐波是否被保留、噪声是否被抑制。随机SVD引入的近似误差集中在高秩尾部而软阈值本来就要干掉这些尾部等于随机误差被后处理“顺手”清理了。这个互补关系是这对组合能在工程上站稳的关键。3. Matlab完整实现从Hankel构造到软阈值重构3.1 核心函数随机SVD、软阈值、Hankel重构先写三个最基本的子函数再串主流程。随机SVD函数是这样function [U, S, V] rsvd(A, k, p) % rsvd - 随机SVD返回A的前k个奇异值分解近似 % 输入: % A - m x n 矩阵 % k - 目标秩保留的奇异值数量 % p - 额外采样数通常 k p min(m,n) % 输出: % U, S, V - 满足 A ≈ U*S*V if nargin 3 || isempty(p) p 10; end r k p; Omega randn(n, r); Y A * Omega; % 随机投影 [Q, ~] qr(Y, 0); % 得到列空间的正交基 B Q * A; % 投影后的小矩阵 [Ub, Sb, Vb] svd(B, econ); U Q * Ub; S Sb(1:k, 1:k); U U(:, 1:k); V Vb(:, 1:k); end软阈值函数更简单function dt softThr(d, tau) % softThr - 对奇异值向量做软阈值收缩 % d 为异值向量tau 为阈值 dt sign(d) .* max(abs(d) - tau, 0); end然后是Hankel矩阵构造和反对角平均重构function H trajectoryMatrix(x, L) % trajectoryMatrix - 延迟嵌入生成Hankel/轨迹矩阵 % x 为行向量L 为窗口长度 x x(:).; N numel(x); H hankel(x(1:L), x(L:N)); endfunction y antiDiagAverage(H) % antiDiagAverage - 对矩阵每条反对角线求平均恢复一维信号 [m, n] size(H); N m n - 1; y zeros(1, N); cnt zeros(1, N); for i 1:m for j 1:n idx i j - 1; y(idx) y(idx) H(i, j); cnt(idx) cnt(idx) 1; end end y y ./ cnt; end注意Hankel矩阵每一条反对角线上的元素都是原始信号同一个位置的值所以重构时必须要做反对角平均不能直接对矩阵的行求平均。3.2 主流程与分块处理这样吃下大数据把上面几个函数串起来得到最基础的去噪函数function x_den denoiseBySVD(x, L, k, tau, p) % denoiseBySVD - 单段SVD软阈值去噪 % x 为输入信号L 为窗口长度 H trajectoryMatrix(x, L); [U, S, V] rsvd(H, k, p); d diag(S); dt softThr(d, tau); H_den U * diag(dt) * V; x_den antiDiagAverage(H_den); end如果数据只有几千点这个函数够用。但到了几十万点别硬拗。我的做法是分块加重叠把长序列切成段每段长度固定对每段独立做去噪然后重叠相加。function x_den blockDenoise(x, blockLen, overlap, L, k, tau, p) % blockDenoise - 分块重叠去噪适用于大数据量 % x 为输入信号blockLen 为块长overlap 为重叠长度 x x(:).; N numel(x); step blockLen - overlap; x_den zeros(1, N); wsum zeros(1, N); window hann(blockLen, periodic).; for start 1:step:N stop min(start blockLen - 1, N); seg x(start:stop); if numel(seg) blockLen seg [seg, zeros(1, blockLen - numel(seg))]; end y denoiseBySVD(seg, L, k, tau, p); y y(1:numel(seg)); wseg window(1:numel(seg)); x_den(start:stop) x_den(start:stop) y .* wseg; wsum(start:stop) wsum(start:stop) wseg; end x_den x_den ./ (wsum eps); end调用示例fs 2000; t (0:1/fs:20); % 20秒共40000点 x sin(2*pi*50*t) 0.5*sin(2*pi*150*t) 0.3*sin(2*pi*250*t) 0.2*sin(2*pi*350*t); x_noisy x 0.1*randn(size(t)); blockLen 4000; overlap 200; L blockLen / 2; k 12; tau 0.5; p 10; x_den blockDenoise(x_noisy, blockLen, overlap, L, k, tau, p);这里L取块长的一半这样每块的Hankel矩阵接近方阵随机SVD的误差控制也最好。3.3 阈值怎么定没有万能参数阈值tau是这套方法里最敏感的参数。我在实际项目里试过几种定法谈不上完美但都可用。噪声段标定从原始信号里截取一段近似纯噪声的数据计算标准差σ_noise然后取τσ_noise·sqrt(2·log(N))N是奇异值个数。来自Donoho经典阈值对高斯白噪声比较准。奇异值拐点法先快速跑一次随机SVD把奇异值序列画出来。谐波对应的大奇异值通常集中在前十几个往下会有一个明显拐点拐点之后多为噪声。取拐点奇异值的1/3到1/2作为τ。迭代试探把τ从0.1倍到5倍噪声标准差扫一遍看频谱中谐波谱线是否稳定。工程上更稳妥但耗时。强烈建议不要一开始就在全图上调参而是取一段有代表性的数据比如1万点把奇异值分布打出来肉眼判断k和τ的范围。这比盲目尝试快得多。4. 实测随机SVD把运行时间砍掉一个数量级4.1 仿真信号和评估指标为了复现我搭了一个标准测试。基频50Hz三次、五次、七次谐波叠加再加高斯白噪声信噪比约10dB。窗口L1000采样点数N2000单块。去噪后用SNR提升来评判精度SNR_in 10*log10(sum(x.^2)/sum((x_noisy-x).^2)); SNR_out 10*log10(sum(x.^2)/sum((x_den-x).^2));相同参数下分别跑直接SVD和随机SVD各10次取平均值。4.2 同规模下传统SVD与随机SVD的对比方法矩阵规模SVD耗时(s)总去噪耗时(s)输出SNR(dB)直接SVD1000×10000.580.6622.4随机SVD(k12,p10)1000×10000.060.1222.1直接SVD2000×20004.525.1022.6随机SVD(k12,p10)2000×20000.210.4522.3表格里的数字是我自己机器上跑的不同配置会有出入但趋势一致。矩阵从1000阶涨到2000阶直接SVD耗时涨了快8倍随机SVD只涨了3到4倍绝对时间始终保持在半秒以内。输出SNR几乎不变说明随机投影带来的误差没有造成实质性的信息损失。4.3 分块处理200k点的实测结果再放大到20万点。直接构10万×10万的矩阵是不可能的我用blockDenoise分块每块4000点、L2000共50块随机SVD处理。每块随机SVD总去噪耗时约0.45秒50块约23秒含重叠后略多一点。如果用传统SVD逐块跑每块5秒50块就是250秒差了一个数量级还不止内存压力也完全不在一个量级。这个差距在实际项目中意味着什么现场需要快速看波形趋势时随机SVD版本可以做到“秒级出结果”传统SVD只能离线慢慢算。特别是连续监测多通道信号时传统SVD很可能拖垮整个流程。5. 工程落地最容易踩的五个坑5.1 秩k和采样数p选错去噪变去信号k如果太小会把有用谐波的奇异值连同噪声一起丢掉重构出来的波形虽然干净但幅值明显被削弱。k如果太大噪声又被放进来。一般取“谐波个数×22~4”比较稳。比如信号明显有基波、三次、五次、七次谐波那k取10到12。如果现场不知道有几个谐波就先在频谱上数谱峰。p取10基本够我试过p3结果会差一截p15和p10差别不大反而多花时间。固定rng对随机SVD特别重要尤其做对比实验时不固定种子两次结果会有零点几dB的抖动。5.2 Hankel重构的边缘失真怎么处理反对角平均本身没有偏置但矩阵两端能参与平均的元素少重构出来的首尾几十个点误差偏大。单块处理时可以丢掉每块首尾约50点重叠块则靠重叠区域互相校正。我在blockDenoise里把重叠设成200就是让边界部分被至少两段数据共同加权失真明显减弱。还有一个小坑antiDiagAverage里忘记除以每个反对角线上元素的个数会导致首尾幅值偏大一倍。我第一次实现时直接求和忘了除count波形中间正常、两端隆起排查了很久才发现。5.3 内存比你想象的更早爆有人可能会想既然随机SVD能处理大矩阵那是不是可以把20万点一次性构出来想多了。就算用随机SVDA矩阵本身也要存在内存里20万点构出的Hankel矩阵是10万×10万double要80GB照样炸。随机SVD降低的是计算复杂度不改变存储需求。真正处理大规模数据还得靠分块或流式这也是我在代码里直接提供blockDenoise的原因。5.4 随机性带来的非确定性随机SVD用的是随机高斯矩阵每次跑结果有小幅抖动。抖动一般小于0.1dB对工程判断没影响但如果你需要严苛可比性比如对比两种去噪算法谁更好必须在开头写rng(seed)。在批次处理多天数据时我习惯固定同一个seed保证处理算法确定性。否则你连算法是不是改进了都说不清楚。5.5 噪声方差估计不能拍脑袋阈值τ如果凭感觉给很容易把谐波削秃。有一次我图省事用了固定τ1结果基波幅值从1.0掉到0.6谐波更是惨不忍睹。后来老老实实截取噪声段计算标准差再按Donoho公式换算效果恢复正常。非平稳背景噪声下最好分段估计τ每段独立去噪。不要一劳永逸设置一个阈值跑完所有数据。6. 写在最后经验总结和更好的后手6.1 这套方案的适用范围随机SVD加软阈值这套组合最适合“谐波型周期干扰 低秩背景”的数据电网谐波、电机振动、结构模态、齿轮箱周期啮合频率等。它擅长把离散的谱线保留下来把宽带的随机噪声压下去。遇到冲击类信号比如打桩、爆破、敲击就别硬套。瞬态信号的Hankel矩阵秩会很高低秩逼近会把冲击削成圆弧。那种场景更适合小波或形态学滤波。判断标准很简单先对一小段信号做FFT如果谱线干净、噪声是扁平底噪这套方法基本稳如果频谱一堆旁瓣、噪声有结构就换思路。6.2 可以再往前走一步的改进如果觉得固定阈值不够聪明可以考虑每次迭代后自动更新τ用广义软阈值或平滑截断绝对偏差代替简单软阈值。也可以把随机SVD结果作为初始解再用一步梯度下降做精调。在实时场景下我会把随机SVD换成随机化块Krylov方法进一步压延时不过那是另一篇的容量了。最后分享一个操作细节Matlab自带的hankel函数会在首末重叠处自动处理但用antiDiagAverage恢复时别忘记除以每条反对角线上出现的次数。我第一次偷懒直接求sum结果首尾幅值偏了一倍找了两天才反应过来。所有大问题最后往往都栽在这种小细节上。
阅读完成 · 觉得有帮助?