去年做金融时序特征工程时,我被一个多变量信号去噪问题卡了将近两周:三只高度联动的日频收益率序列,单通道变分模态分解(VMD)跑出来非常干净,但各路独立处理完再合回来时,通道间的共动信息却被破坏了。后来我在MATLAB里用多元变分模态分解(MVMD)配合多通道去趋势波动分析(MFDFA)才把问题解决。这篇文章就把这套方法完整拆开,讲原理、讲能跑的代码、讲参数调优和踩过的坑,适合正在研究金融多变量去噪、或者想把MVMD/MFDFA落地的同学收藏参考。
1. 单通道VMD在金融组合数据上为什么会翻车
1.1 金融多变量信号的三个特点
金融时间序列和工业振动信号有个很大差别:它几乎没有“干净的真值”可以对照。振动信号你可以在无故障状态下测一段作为基准,金融数据你永远不知道哪一段才是真实的“不含噪声”价格。所以金融去噪不能只追求波形好看,更关键的是结构关系不能破坏。
从信号处理角度看,金融多变量序列通常具备三个特点。第一是非平稳性,均值、方差都会随时间漂移,存在明显的趋势项和结构性跳变。第二是变量间强耦合,同一市场环境下一组股票或一组资产往往共享同一个潜在因子,这个共同驱动因子会在多个通道同时引起波动。第三是低信噪比,金融噪声不是理想的高斯白噪声,它带有长期记忆性质,和真实信息在频带上大量重叠。
正是这三个特点,决定了“单通道独立去噪”这条路线很容易走歪。因为当你单独处理每只股票时,算法只看得见自己的频谱结构,看不到其他通道在同时发生什么。说得直白点,它无法区分“这一小段波动来自共同市场因子”还是“来自本通道特有的随机扰动”。
1.2 各通道独立分解的致命问题
我先说一个我实际遇到的现象。三只标的在同一时段受同一个市场因子驱动,因为标的流动性不同,这个因子在不同股票上的频谱表现并不完全一致:在A上能量集中在0.1Hz附近,在B上变成0.12Hz,在C上跑到0.15Hz。单通道VMD各自按能量划分,结果就会出问题:在A标的里,这个因子被归进第3个模态;在B标的里因为能量排布不同,被归进第2个模态;到C标的又是另外的序号。
等我把三路的“第N模态重构信号”拼在一起做后续分析时,取的是不同物理含义的模态,共动因子当然对不上。我一开始以为是VMD参数没调好,试了很多alpha和K值都没有用。后来才意识到,问题不在参数,而在算法范式:各通道共享的频率结构,被独立分解彻底拆散了。
这就是MVMD存在的基本逻辑:让同一个模态在所有通道里共享一个中心频率。它只允许每个通道在这个中心频率附近赋予不同的幅值和相位,不允许把频率结构各自乱排。用合唱来类比,单通道VMD是每位乐手凭自己感觉定调,MVMD则是先让指挥给出一个统一标准音高,各声部在这个音高下唱自己的旋律。
1.3 MVMD+MFDFA的组合逻辑
MVMD解决的是“怎么分解才不破坏共动结构”,但分解完之后还有个更实际的问题:K个模态里哪些是噪声,哪些是有效信息?如果全靠人肉去看频谱,能看一两个序列,但三五个通道、七八个模态就忙不过来了。
这里轮到多通道去趋势波动分析上场。它本质上是一种长程相关性统计:对某个模态的多通道重构结果计算联合Hurst指数,如果这个指数显著偏离0.5,说明模态内部有可被描述的结构,保留;如果指数接近0.5,说明它就是接近白噪声的随机成分,剔除。
所以MVMD和MFDFA是一对互补的工具:MVMD管分解,MFDFA管筛选,合在一起才变成一套完整的去噪流水线。这也是我在MATLAB里最终落地时的固定组合。
2. MVMD算法拆解与MATLAB实现基础
2.1 从VMD到MVMD的关键改动
先回顾一下VMD在做什么。VMD把一维信号分解成K个带宽受限的本征模态函数,优化目标是最小化每个模态的梯度L2范数,等价于限制带宽;约束条件是所有模态之和等于原始信号。求解时利用拉格朗日乘子和交替方向乘子法,在频域里不断迭代,每轮更新一次模态、中心频率和乘子。
MVMD的数学改动一句话讲完:把目标函数从单通道扩展到C个通道。对K个模态、C个通道,目标是让所有C个通道、所有K个模态的总带宽最小;约束条件是每个通道自己的分解结果之和等于该通道的原始信号。因为约束是分通道写的,模态之间通过共享的中心频率被强制对齐。
迭代逻辑和VMD基本一致,区别在于更新中心频率时,不再只看单通道的模态能量谱,而是把所有通道同一个模态的能量谱加起来,再取频率重心。这样某个通道能量再强也不能独占中心频率,必须所有通道一起投票。直观理解是:低能量通道虽然贡献小,但在频率投票里仍然能把信息顶出来,不会完全被高能量通道淹没。
2.2 一个能跑通的MVMD核心函数
下面是我整理过的教学版MVMD函数。我只保留了主迭代骨架,省略了一些工程细节,比如边界镜像和双端延拓,但核心逻辑可以跑通,适合对照论文理解。
function [modes, omega] = mvmd_simple(x, K, alpha, tau, tol, maxN) % 简化版多元变分模态分解(MVMD) % x: C x N 多变量信号,C为通道数 % K: 模态数量 % alpha: 带宽控制参数,越大模态频带越窄 % tau: 拉格朗日乘子更新步长,越大对噪声越敏感 % 输出 modes: K x C x N,omega: K x 1 中心频率 [C, N] = size(x); f = (0:N-1) / N * 2 * pi; % 数字角频率轴 xhat = fft(x, N, 2); % 每个通道做FFT % 初始化解 uhat = zeros(K, C, N); omega = (1:K)' * pi / (K + 1); % 均匀初始中心频率 lambda_hat = zeros(C, N); u_old = uhat; for iter = 1:maxN % 第1步:更新全部模态 for k = 1:K sum_u = sum(uhat, 1) - uhat(k, :, :); numerator = xhat - squeeze(sum_u) - lambda_hat / 2; % 频域维纳滤波:中心频率omega(k)处的带通 filter = 1 ./ (1 + 2 * alpha * (f - omega(k)).^2); uhat(k, :, :) = numerator .* filter; end % 第2步:更新中心频率,所有通道联合投票 for k = 1:K A2 = abs(squeeze(uhat(k, :, :))).^2; % C x N 能量谱 omega(k) = sum(A2(:) .* repmat(f, C, 1), 'all') / sum(A2, 'all'); end % 第3步:更新拉格朗日乘子 residual = xhat - squeeze(sum(uhat, 1)); lambda_hat = lambda_hat + tau * residual; % 收敛判断:前后两次模态变化的相对误差 err = sum(abs(uhat(:) - u_old(:)).^2) / sum(abs(u_old(:)).^2 + eps); if err < tol break; end u_old = uhat; end % 频域结果转回时域 modes = zeros(K, C, N); for k = 1:K modes(k, :, :) = real(ifft(squeeze(uhat(k, :, :)), N, 2)); end end这段代码里最值得玩味的是中心频率更新那几行。它就是把所有通道在模态k上的能量谱叠加,再求加权平均频率。如果你把它换成只取通道1的谱,整个函数就退化回了单通道VMD。所以多通道联合分解的“灵魂”就在这里。
当然,教学版和论文原版还有差距。真实使用中我建议再补上两点:对边界做镜像延拓再切回,以及把频率轴限制在正半轴以提升数值稳定性。否则序列长度很短时,边界容易出来一堆伪模态。
提示:教学版函数省略了镜像延拓和半谱处理。正式使用前请先在长度512以上的模拟信号上验证,确认分解结果正确后再套到金融数据上。
2.3 输入参数到底怎么给
参数调参是最让新手头疼的部分。我给一张我常用的经验表:
| 参数 | 含义 | 我用的初始范围 | 调整提示 |
|---|---|---|---|
| K | 模态数量 | 4~8(日频),8~12(分钟级) | K过小模态混叠,过大会出现虚假模态 |
| alpha | 带宽约束 | 200~2000 | 越大频带越窄,但过大容易丢信息 |
| tau | 乘子步长 | 0.05~0.5 | 越小越稳,越大收敛越快但可能过拟合噪声 |
| tol | 收敛阈值 | 1e-7~1e-5 | 达到后停迭代 |
| maxN | 最大迭代次数 | 200~1000 | 防止不收敛死循环 |
我的经验是K一定不要拍脑袋定。先对多通道均值谱画一张图,数一数有几个比较明显的谱峰,K至少不小于峰数。实际操作中我经常先按谱峰数定K的下限,再用“中心频率是否过于接近”来做上限判断,这个在第五节会细说。
alpha的敏感度相对低,500左右通常能给出比较合理的带宽。tau其实不用太纠结,0.2是个比较均衡的值。真正影响结果的是K和数据的标准化方式。
3. 多通道去趋势波动分析:噪声模态的“体温计”
3.1 DFA和多通道版本到底在算什么
去趋势波动分析(DFA)最早是被用来检测时间序列里的长程相关性的。它的基本动作分四步:先把原序列减均值、做累积离差;然后按一系列窗口长度s把累积序列切成段;每段用多项式拟合出局部趋势并去掉;最后把所有段残差的均方根汇总成波动函数F(s)。如果F(s)和s之间满足幂律F(s)~s^H,那么这个指数H就是著名的Hurst指数。
H的值怎么解读:0.5是最重要的分界线。H=0.5意味着序列在统计上与白噪声等价,没有可被利用的记忆结构;H>0.5说明序列有正的长程相关性,今天的运动倾向于带动明天的同向运动;H<0.5则说明有反持续性,涨完容易跌、跌完容易涨。金融序列里很多宏观和价格序列落在H>0.5的区间,而纯随机噪声则贴在0.5附近。
多通道去趋势波动分析和单通道的区别,在于波动函数F(s)的计算把C个通道一起纳入:每个窗口里逐通道做去趋势,残差平方全部累加,再除以总样本数。这样得到的是一个“跨通道联合”的波动函数,对应的H描述的是整个通道组的整体长程相关结构,而不是单个序列的。
3.2 用Hurst指数给模态分类
MVMD分解完成后,每个模态k对应一个C×N的多通道重构矩阵。把这个矩阵直接送进多通道DFA函数,就能得到这个模态的联合Hurst指数。注意我建议送入的是某个模态的各个通道重构结果,不是原始信号。这样每个模态有一条独立的“体温”,便于判断它是否值得保留。
function H = mfdfa_multichannel(data, scales, order) % data: C x N,多通道信号 % scales: 窗口尺度向量,比如 10:10:floor(N/4) % order: 多项式阶数,通常用1 C = size(data, 1); N = size(data, 2); X = cumsum(data - mean(data, 2), 2); F = zeros(numel(scales), 1); for s_idx = 1:numel(scales) s = scales(s_idx); nSeg = floor(N / s); res = 0; for v = 1:nSeg seg = X(:, (v-1)*s + 1 : v*s); t = (1:s)'; for c = 1:C p = polyfit(t, seg(c, :), order); trend = polyval(p, t); res = res + sum((seg(c, :) - trend).^2); end end F(s_idx) = sqrt(res / (nSeg * C * s)); end p = polyfit(log(scales), log(F), 1); H = p(1); end拿到每个模态的H之后,我的筛选规则很简单:保留|H-0.5|大于阈值的模态,剔除接近0.5的模态。阈值我常用0.06。这个值的含义是允许模态的H在0.44到0.56之间浮动,超出这个区间我就认为它有结构。太严会丢掉低频趋势,太松会留下随机成分,0.06是多数金融日频序列上比较平衡的点。
注意:阈值0.06来自日频到分钟级金融序列的经验值。如果你处理的是信噪比极低的高频逐笔数据,建议先放宽到0.08,观察保留模态的质量再收紧。
3.3 多通道联合估计比单通道稳定在哪
有同学可能会问:为什么不直接用单通道DFA,对每个通道分别算一遍H,再取平均?我在项目里试过,差别很明显。单通道DFA在短序列上方差很大,同一个模态在标A上算出H=0.67,在标B上算出H=0.44,在标C上算出0.55。如果按单通道分别决策,A要保留、B要删除、C很纠结,完全没法统一。
多通道联合估计把三个通道放在同一个波动函数里,相当于增加了有效样本量,估计方差明显下降。而且当几个通道共享同一个潜在因子时,联合DFA会让共同结构变得更突出,同时把通道特有的随机波动平均掉。同样的数据,联合估计得到H=0.58,决策就变成“这是一个有结构的模态,保留”,非常干脆。
所以我把MFDFA称作噪声模态的“体温计”:它不关心模态幅度大不大,只关心模态内部有没有可检测的统计结构。体温正常(H大约0.5)的是噪声,体温偏高或偏低的是信息。
4. 完整去噪流程:从分钟线到重构信号
4.1 数据准备与标准化
完整流程的第一件事是把拿到的原始数据组装成C×N矩阵。以分钟线为例,选三只关联标的的5分钟收益率,剔除非交易时段、填充缺失值之后,得到C行N列的矩阵。这里C是通道数,N是观测数。
然后是标准化。这个步骤容易做错,我有一次因为想当然地先做了标准化、分解完又对每个模态分别归一化,结果重构后的信号量纲全乱了。正确顺序是:先对每个通道做z-score标准化,让所有通道落在相近量纲,再进行MVMD分解;分解完把保留模态相加重构,最后把标准化前的均值和标准差乘回去。标准化在分解之前做,反标准化在重构之后做,顺序不能反。
顺序可以总结为:zscore → MVMD → MFDFA筛选 → 模态重构 → 反标准化。
这个顺序有个重要原因:MVMD的共享中心频率本质是能量加权投票,如果股票A的价格量级上千,股票B的波动率只有几个百分点,不标准化的话A会以绝对能量优势主导中心频率,B的信息几乎不参与投票。金融数据尤其要小心这个,不同指标的物理单位完全不同。
4.2 分解-筛选-重构三步走
把前面两个核心工具串起来,主流程代码是这样的:
% 主流程:MVMD + MFDFA 多变量去噪 X = [assetA_ret; assetB_ret; assetC_ret]; % 3 x N X_mean = mean(X, 2); X_std = std(X, 0, 2); X_norm = (X - X_mean) ./ X_std; K = 6; [modes, omega] = mvmd_simple(X_norm, K, 500, 0.2, 1e-6, 300); % 对每个模态做多通道去趋势波动分析 scales = 10:10:floor(N / 4); H_list = zeros(K, 1); for k = 1:K mk = squeeze(modes(k, :, :)); % C x N H_list(k) = mfdfa_multichannel(mk, scales, 1); end % 保留显著偏离0.5的模态 threshold = 0.06; keep = abs(H_list - 0.5) > threshold; X_den = zeros(size(X_norm)); for k = 1:K if keep(k) X_den = X_den + squeeze(modes(k, :, :)); end end % 反标准化 X_clean = X_den .* X_std + X_mean;这里有几个细节。scales建议从10开始、步长10,最大窗口取N/4左右。窗口太短拟合不稳定,窗口太长段数太少统计量噪声大。N如果是1000,scales取10:10:250比较合适。保留模态的阈值写在变量里,方便后面调。
4.3 模拟验证方案
真实金融序列没有干净真值,怎么验证这套流程确实在去噪而不是在乱删信息?我的做法是先构造模拟数据:生成三通道信号,每个通道都由一个共同低频正弦、一个通道特有的中等频率正弦,以及标准差可调的高斯白噪声叠加而成。这样“干净信号”是已知的,可以量化评估。
验证指标我通常看两个:去噪结果与干净信号的均方根误差RMSE,以及重构结果保持通道间相关系数的程度。下面是模拟项目X上的一次对照实验数字,参数不同会有浮动,但相对关系稳定:
| 方案 | RMSE | 通道间相关系数保持度 |
|---|---|---|
| 原始带噪信号 | 0.182 | 1.00(基准) |
| 独立VMD + 逐通道DFA | 0.121 | 0.47 |
| MVMD + 逐通道DFA | 0.093 | 0.66 |
| MVMD + MFDFA联合筛选 | 0.054 | 0.84 |
看到结果时我自己也挺吃惊:MVMD单向贡献其实有限,真正的质量提升来自MFDFA联合筛选这一步。它把那些“看着有波动但没有统计结构”的模态剔除得比较干净。而独立VMD之所以相关系数保持度差,就是因为模态错位问题,这在第1节已经解释过。
5. 实测踩坑记录:参数、边界和模态混叠
5.1 K值选错带来的模态混叠
踩坑最多的地方就是K。有一段时间我图省事,所有标的一律K=4,结果某组分钟线上模态3的Hurst算出来0.51,被当成噪声删掉了,但日频趋势其实就藏在里面。后来把保留前的模态拿出来画频谱,发现模态3的频谱明显有两个峰:一个在0.08Hz,一个在0.13Hz,中间还有个凹谷。这说明K=4时,本应分开的两部分被揉进了一个模态,DFA把它们平均成了白噪声特征。
排查方法很直接:把K从小到大试一圈,每次打印中心频率向量。如果出现两个中心频率之差小于某个阈值,比如频率轴总跨度的2%,说明这两个模态很可能在争抢同一段频谱,K可能给大了;如果某个模态频谱呈明显双峰,则说明K给小了,该模态里混有两种物理上不同的成分。
实操中我会写一个K扫描脚本,对K=3到K=10分别跑一遍MVMD,记录中心频率和Hurst,汇总成表。多做几次之后,我基本能一眼看出哪一排K是合理的。这比在单个K上反复调参高效得多。
提示:K扫描是最值得花时间的调试手段。一次扫描能同时发现模态混叠和K过大问题,比盲调参数高效一个数量级。
5.2 量纲差异与标准化顺序
第二节提过标准化顺序不能反,这里展开讲一个我真实翻车的场景。某次我把一只股票的价格和另一只股票的成交量放进同一个通道组,价格在几百到几千之间,成交量是几千万量级,一时间忘了标准化。MVMD把几乎全部能量都分配给了成交量通道,中心频率被成交量通道主导,价格通道的有效信息被压成接近零幅度的模态。
后来我在标准化时又犯了第二个错:先对每个通道z-score,但分解完直接把保留模态相加,忘记反标准化。结果是去噪信号的方差明显比原始序列小,因为所有通道都被压到了单位方差量级。记住:标准化是预处理手段,不是最终输出形态,重构之后必须把原始均值和标准差乘回去。
5.3 边界效应、短序列与中心频率初始化
MVMD和大部分基于FFT的分解方法一样,在序列两端有边界效应。金融序列本身噪声重,边界常常被分成高频伪模态。我的处理习惯是:去噪完成后,把每个通道首尾各去掉10到20个重构点再用于后续分析;如果后续要用全长度,则用镜像延拓配合MVMD分解,再切回原始区间。
序列长度也要把关。N小于300时,MFDFA的窗口段数严重不足,Hurst估计方差大,筛选结果基本不可信。我知道有人用内插去强行拉长序列,这不可取,内插会引入额外相关性。务实建议是:要么拉长原始数据,要么放弃这个方法改用更简单的滤波。
最后说中心频率初始化。我的教学代码里用了均匀初始化,这是最稳的方案。如果改成随机初始化,同一个数据跑两次可能得到不同的模态结构,因为目标函数非凸、存在多个局部解。想验证是不是陷入局部解,就把同样的数据跑5遍,看中心频率向量是否基本一致。如果不一致,调大tau让迭代更快收敛到稳定分支,再交叉K扫描一起看。
6. 效果怎么证明:去噪之后的对账清单
6.1 定量指标清单
这套流程跑完之后,不能只拿眼睛说“看起来平滑了”,要有一份对账清单。我在每个项目结束时会固定检查四件事,全部通过才算去噪有效:
| 检查项 | 说明 | 我用的合格线 |
|---|---|---|
| 通道间相关系数保持 | 去噪前后两两相关性不应大幅改变 | 变化绝对值 < 0.1 |
| 模拟场景SNR提升 | 在有干净真值的模拟数据上验证 | SNR提升 > 5dB |
| 模态Hurst分布 | 保留模态应远离0.5,剔除模态应在0.5附近 | 剔除模态H在0.45到0.55之间 |
| 协整检验稳定性 | 对日频对数价格去噪后重跑协整检验 | 迹统计量波动显著降低 |
第一项尤其重要:去噪如果伤到通道间相关性,后面的协整、回归、因子模型都没法用。MVMD+MFDFA在这项上表现稳定,因为它从头到尾都在保护通道间的共同结构,不是各管各的。
6.2 去噪后对协整检验的影响
说一个实际案例。某次我处理三只标的的日频对数价格,原始序列上跑协整检验,迹统计量在样本前段显示存在协整关系,后段又显示不存在,结论非常不稳定,让人怀疑是不是数据本身没有长期均衡。取对数价格去噪后再跑,迹统计量的波动明显收敛,整个样本区间结论一致。
这个现象背后的道理不复杂:噪声会污染长期均衡关系的估计,尤其是在短样本里,随机波动会被误当成偏离均衡的部分。MVMD把共享频率结构以外的随机成分拿掉之后,低频趋势对应的协整关系才浮出水面。这并不代表我能拿它去预测涨跌,但它让后续统计推断的稳定性确实上了一个台阶。
6.3 后续扩展:K自适应、Hurst特征与跨领域移植
这套组合除了直接做去噪,还可以向几个方向扩展。第一个是K自适应:与其手工敲定K,不如对K=3到K=12都跑一遍,用某种准则,比如模态间中心频率间距、总重建误差、信息准则,选最优。第二个是把MFDFA算出来的Hurst指数当作特征输入后续模型:毕竟每个模态的H描述的是该频带上的长程记忆强度,这个特征比原始模态幅度更具可解释性。
第三个方向是跨领域移植。MVMD+MFDFA并不是金融专属,多通道振动信号、气象观测序列、脑电多导联数据都有类似需求。但移植时参数逻辑要改:金融数据关心的是共动结构和协整,工业信号关心的可能是故障特征频段的能量,筛选阈值和K的选择都要重新标定。我不建议把金融场景的参数直接套过去。
就我个人经验来说,使用这套流程后我最大的收获不是某个具体指标变好看了,而是终于有一个可解释的去噪决策链路:MFDFA告诉我“你删掉的这个模态为什么是噪声”,而不是像盲目低通滤波那样把一切都圆滑掉。最后再分享一个日常调试小技巧:每次跑完都打印中心频率向量,只要看到某两个omega挨得特别近,先别调其他参数,去确认K是不是给大了。这一步能帮你省下大量瞎调参数的时间。