ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

贝叶斯变点Copula模型:从变点检测到相关性结构突变分析

贝叶斯变点Copula模型:从变点检测到相关性结构突变分析 第一次接触变点Copula这个概念是在处理两只资产收益率的联合分布时——很明显它们的相关结构在某个时点之后变了但经典Copula模型默认相关性参数恒定怎么拟合都会把突变前后的信息混在一起估计出的相关系数两头不讨好。后来我把贝叶斯变点推断和Copula模型放在同一个框架里把变点位置当成未知参数一起估计用Matlab把整套流程跑通才算真正解决了问题。这篇文章就是我对这个方法的完整复盘从模型设定到采样代码再到我踩过的坑尽量讲透。无论你是做金融风险、水文气象还是传感器数据分析只要遇到“相关性结构突变”的场景这套思路都能直接移植。1. 变点Copula模型到底解决什么问题1.1 一个让我印象深刻的例子我曾经长期跟踪两个行业主题ETF的日收益率。大部分时间里两只基金的相关性很稳定但在某个市场事件发生后两者几乎开始同涨同跌相关性一下子从0.3左右跳到0.8以上。如果用单一Copula参数去拟合整段数据估计结果会落在0.5附近——这个数字既不能代表前段的温和联动也不能代表后段的剧烈共振完全是个“平均幻觉”。这其实是很多时间序列都会遇到的困境相关性不是常数而是会在某个未知时刻发生跳变。宏观环境切换、行业景气度变化、市场情绪突变都会让资产间的联动模式改变。变点Copula模型要做的就是放弃“参数永远不变”的假设允许Copula参数在不同的时间区间里取不同值同时估计“什么时候变”和“变成多少”。1.2 模型定义参数分段常值先固定一个二元情况。假设观测是 (X_t(X_{t1}, X_{t2}))时间 (t1,\dots,T)。每个分量的边缘分布函数是 (F_1,F_2)通过概率积分变换得到伪观测[ U_t F_1(X_{t1}), \quad V_t F_2(X_{t2}) ]如果存在 (k) 个变点 (\tau_1\tau_2\dots\tau_k)定义 (\tau_00,\tau_{k1}T)那么完整模型就是[ (U_t,V_t) \sim C_{\theta_j}, \quad \tau_{j-1} t \le \tau_j ]这里的 (C_{\theta_j}) 是第 (j) 段对应的Copula参数 (\theta_j) 只在第 (j) 段内有效。于是联合密度可以写成Copula密度乘以边缘密度[ f(x_t;\theta_j) c\bigl(F_1(x_{t1}),F_2(x_{t2});\theta_j\bigr) \times f_1(x_{t1}), f_2(x_{t2}) ]如果采用两阶段估计边缘密度部分和 (\theta_j) 无关贝叶斯推断只需要关心Copula这一部分似然。这样就把复杂问题分离了先处理好每条序列自己的边际特征再集中精力推断相关结构的变点。1.3 为什么要用贝叶斯推断频率学派也可以做变点估计比如用最大似然扫一遍所有可能的 (\tau)找似然最大的位置。但这种做法有两个痛点一是变点个数需要先验指定二是变点位置的不确定性很难直接度量。贝叶斯推断把 (\tau) 和 (\theta) 都当作随机变量最后得到的是后验分布 (p(\tau,\theta|U,V))。比如我可以直接说“变点最可能出现在第250天95%的后验区间是第248天到第258天”。这种不确定性信息对决策非常关键。另外当序列长度不长或者突变信号比较弱时最大似然估计对初值非常敏感容易卡在局部极值。贝叶斯框架里加入合理的先验相当于给参数划定了一个相对合理的活动范围整个采样过程会更稳定。这也是我在实际项目里更愿意用贝叶斯方法的原因。2. 模型设定与贝叶斯推断框架2.1 边缘分布与Copula函数的选择先说边缘分布的估计。Copula模型的一大优势是边缘分布可以自由选择但选不好会直接影响伪观测的质量。我常用的方案是两阶段估计第一阶段用GARCH(1,1)-t模型过滤收益率序列得到标准化残差第二阶段对标准化残差做经验CDF变换到均匀分布。这样比直接对原始收益率做经验CDF更合理因为金融收益率普遍存在波动率聚集现象原始数据不独立直接做经验CDF会把波动率结构也带进Copula里。Copula函数方面Gaussian Copula是最简单的起点相关矩阵只要一个参数 (\rho)迭代速度快适合先把MCMC框架跑通。如果数据存在明显的尾部相关性可以换成Clayton Copula或t Copula。Clayton适合捕捉下尾联动暴跌时一起跌Gumbel适合上尾联动暴涨时一起涨t Copula则同时包含上下尾相关。实际项目中没必要一开始就在一堆Copula里纠结。先用Gaussian Copula把变点检测做出来看后验变点是否稳定再替换成其他Copula。因为贝叶斯变点推断的核心是“采样策略”更换Copula只是换一个似然函数而已。2.2 先验设定变点位置 (\tau) 的先验我通常先取离散均匀分布[ p(\tau_j) \frac{1}{T-1}, \quad \tau_j \in {1,\dots,T-1} ]这种先验意味着“在数据没有给出信息之前变点可能出现在任意位置”。如果业务上有理由相信变点只会出现在某个区间也可以改成截断均匀分布比如只允许 (\tau\in[0.1T,0.9T])避免变点离端点太近导致段内样本太少。Copula参数的先验要看具体函数。Gaussian Copula的 (\rho\in(-1,1))最简单的做法是均匀先验 (U(-1,1))。但要注意如果某一段数据特别少均匀先验可能会让后验偏向 (\rho0) 附近。想要更稳一点可以用一个轻微信息先验比如以0为中心的正态先验再截断到 ((-1,1))但强度别太大。这里有一个非常容易踩的坑先验太强会把变点位置拉偏。比如你给 (\rho_1) 一个方差很小的先验MCMC为了满足这个先验会把变点挪到一个奇怪的位置来“补偿”参数偏差。所以我的经验是先验只提供边界约束不要指望它替你识别方向。2.3 后验采样MCMC策略单变点模型的参数集合是 (\theta_1, \theta_2, \tau)。后验分布可以写成[ p(\theta_1,\theta_2,\tau|U,V)\propto \prod_{t1}^{\tau} c(u_t,v_t;\theta_1) \times \prod_{t\tau1}^{T} c(u_t,v_t;\theta_2)\times p(\theta_1)p(\theta_2)p(\tau) ]因为没有共轭关系我直接用Metropolis-Hastings采样。每一轮迭代做三次更新更新 (\theta_1)用随机游走提议 (\theta_1^*\theta_1\epsilon_1 N(0,1))如果超出参数范围就拒绝或重新提议。接受概率只取决于第一段数据的似然比。更新 (\theta_2)过程完全一样只不过换成第二段数据。更新 (\tau)提议 (\tau^*\tau\delta)其中 (\delta) 可以从一个小范围内的整数均匀分布中抽取然后计算整段数据的似然比。这里有个细节(\tau) 是整数提议分布如果不对称接受率必须加Hastings修正。具体来说如果提议 (\tau^) 是在区间 ([\max(1,\tau-5),\min(T-1,\tau5)]) 内均匀抽取的那么从 (\tau^) 往回提的区间长度可能不同。接受率应该是[ \alpha \min\left(1,; \exp(\text{新似然}-\text{旧似然}) \times \frac{|\text{旧区间长度}|}{|\text{新区间长度}|}\right) ]很多Matlab示例代码偷懒不写这个修正数据量大时偏差不明显但严谨项目里还是应该补上。2.4 多变点扩展当变点个数未知时理论上可以使用可逆跳转MCMCRJMCMC在参数维度不同的模型空间之间跳跃。但RJMCMC实现复杂要设计跨维提议分布还要算Jacobian对于多数实际项目来说性价比不高。我更推荐一种务实路线先用似然剖面图或者二元分割法确定候选变点个数然后在固定变点个数的前提下做贝叶斯推断。比如先扫描所有可能的单变点位置看似然最大值是否显著高于无变点模型如果需要多个变点就逐段递归查找。这样既避免了RJMCMC的复杂度又能得到变点的后验不确定性。当然贝叶斯框架下比较模型可以用边际似然或者DIC、WAIC。边际似然的计算可以用Laplace近似或重要性采样但都比较费劲。我的经验是先看看变点后验分布是否集中如果 (\tau) 的后验直方图呈现明显的单峰说明一个变点就足够了如果后验分布出现多个尖峰可能真的有多个变点或者后验重标记问题。3. Matlab实现过程详述3.1 整体代码结构Matlab做贝叶斯变点Copula推断我最常用的代码结构分为四个文件simulate_data.m生成带变点的模拟数据方便验证算法正确性。pseudo_obs.m把原始数据变换成 ( (0,1) ) 区间上的伪观测。copula_loglik.m计算某一段数据的Copula对数似然。mcmc_copula_change.m主采样程序输出参数后验链。主程序里设置随机数种子、初始化参数、循环迭代然后调用上面这些函数。把功能拆成独立函数的好处是可以分别调试先单独测试Copula似然函数再测试MCMC更新步骤最后整合。3.2 核心Gaussian Copula对数似然Gaussian Copula的密度函数有一个非常紧凑的形式。对一组伪观测 ((u_i,v_i)) 和相关系数 (\rho)定义 (x_i\Phi^{-1}(u_i))、(y_i\Phi^{-1}(v_i))对数似然去掉与 (\rho) 无关的常数项可以写成function loglik gaussian_copula_loglik(u, v, rho) % u, v : n x 1 列向量取值范围 (0,1) % rho : 相关系数范围 (-1,1) x norminv(u); y norminv(v); % 高斯Copula对数密度省略与rho无关的项 loglik -0.5 * log(1 - rho^2) ... - (rho^2 * (x.^2 y.^2) - 2 * rho * x .* y) ... / (2 * (1 - rho^2)); loglik sum(loglik); end为什么要省略与 (\rho) 无关的项因为MCMC接受率只用到新旧参数的似然差常数项会消掉。如果后续要计算DIC或WAIC需要完整对数似然时再补上常数项也不迟。这里还有一个性能优化的点(x\Phi^{-1}(u)) 和 (y\Phi^{-1}(v)) 不依赖 (\rho)所以在MCMC循环里不要每次都调用norminv。我通常在主循环外先计算好x_all和y_all然后切出对应区间的子集传给似然函数。3.3 变点位置与参数的更新下面给出一个简化版的核心采样循环。为了代码可读性我用randn做参数提议randi做变点提议。注意变点提议的边界修正我在示例里已经写进去了function [theta_chain, tau_chain, loglik_chain] mcmc_copula_change(u, v, nIter, burnin, step) T length(u); % 预计算正态分位数 x norminv(u); y norminv(v); % 初始化 theta1 0.3; theta2 0.6; tau round(T/2); nSave nIter - burnin; theta_chain zeros(nSave, 2); tau_chain zeros(nSave, 1); loglik_chain zeros(nSave, 1); for iter 1:nIter % 更新 theta1 prop1 theta1 step * randn; if abs(prop1) 0.999 ll_old gaussian_copula_loglik_pre(x(1:tau), y(1:tau), theta1); ll_new gaussian_copula_loglik_pre(x(1:tau), y(1:tau), prop1); if log(rand) ll_new - ll_old theta1 prop1; end end % 更新 theta2 prop2 theta2 step * randn; if abs(prop2) 0.999 ll_old gaussian_copula_loglik_pre(x(tau1:T), y(tau1:T), theta2); ll_new gaussian_copula_loglik_pre(x(tau1:T), y(tau1:T), prop2); if log(rand) ll_new - ll_old theta2 prop2; end end % 更新 tau在 tau 附近正负5步内均匀提议 deltaMax 5; lo max(1, tau - deltaMax); hi min(T-1, tau deltaMax); tauProp randi([lo, hi]); % 从 tauProp 返回 tau 的提议区间长度 loBack max(1, tauProp - deltaMax); hiBack min(T-1, tauProp deltaMax); qRatio (hi - lo 1) / (hiBack - loBack 1); ll_old gaussian_copula_loglik_pre(x(1:tau), y(1:tau), theta1) ... gaussian_copula_loglik_pre(x(tau1:T), y(tau1:T), theta2); ll_new gaussian_copula_loglik_pre(x(1:tauProp), y(1:tauProp), theta1) ... gaussian_copula_loglik_pre(x(tauProp1:T), y(tauProp1:T), theta2); if log(rand) (ll_new - ll_old) log(qRatio) tau tauProp; end % 保存 if iter burnin idx iter - burnin; theta_chain(idx,:) [theta1, theta2]; tau_chain(idx) tau; loglik_chain(idx) ll_old loglik0; % 完整对数似然自行补充 end end end这个代码里我调用了gaussian_copula_loglik_pre它和gaussian_copula_loglik的唯一区别是直接输入x,y而不是u,v这样可以减少重复计算。定义如下function loglik gaussian_copula_loglik_pre(x, y, rho) loglik -0.5 * log(1 - rho^2) ... - (rho^2 * (x.^2 y.^2) - 2 * rho * x .* y) ... / (2 * (1 - rho^2)); loglik sum(loglik); end3.4 结果可视化与后验统计采样结束后先别急着看参数均值先画迹图。看 (\theta_1,\theta_2,\tau) 的MCMC链是否平稳有没有长期停留的平直段。如果链像随机游走一样来回漂移说明提议步长太小如果频繁拒绝说明步长太大。变点位置的后验分布是重点。直接画直方图figure; histogram(tau_chain, Normalization, pdf); xline(tau_true, r--, LineWidth, 1.5); xlabel(变点位置); ylabel(后验密度);参数的后验均值和中位数可以这样算rho1_mean mean(theta_chain(:,1)); rho2_mean mean(theta_chain(:,2)); tau_mode mode(tau_chain);95%的后验区间用分位数或者HPD区间都行。如果后验分布是对称的单峰分位数区间就够了如果偏态明显建议用HPD区间。Matlab里HPD没有现成函数自己写一个根据核密度估计最短区间的小函数也不难。4. 模拟实验与真实案例4.1 模拟数据上的效果我先用模拟数据验证算法。设置 (T500)真实变点 (\tau250)第一段 (\rho_10.2)第二段 (\rho_20.8)。模拟过程是每个时间点生成一对标准正态变量相关系数由当前段决定然后做概率积分变换。运行MCMC 12000次前2000次作为burn-in步长设置为0.15。结果如下参数真值后验均值95%后验区间(\tau)250251.8[247, 258](\rho_1)0.20.208[0.11, 0.31](\rho_2)0.80.789[0.71, 0.86]整体识别效果很好。(\tau) 的后验分布集中在真实值附近说明只要突变幅度足够大数据提供的信息足以压过先验。(\rho_1) 的区间比 (\rho_2) 宽因为第一段的样本量刚好250而 (\rho0.2) 的信号本身比0.8弱这很正常。4.2 真实ETF收益率数据应用验证完算法我把它应用在两只行业主题ETF的日收益率上样本期大约300个交易日。先用GARCH(1,1)-t模型过滤边缘把标准化残差变换到均匀尺度然后套用上面的MCMC流程。结果显示变点位置的后验众数出现在第142天95%区间是[136, 149]。变点前 (\rho) 后验均值约0.45变点后约0.83。这个结果和我在1.1小节里讲的直觉判断一致市场事件导致了两只ETF的相关性跳跃。这里有一个重要心得真实数据的边缘分布估计比Copula部分更容易出问题。我第一次直接对原始收益率做经验CDF没考虑波动率聚集结果变点位置几乎每隔一段时间就被“检测”出一个变点。换成GARCH过滤之后变点位置变得稳定得多。所以强烈建议在金融数据上先做波动率过滤。4.3 收敛性诊断与超参数敏感性MCMC能不能用收敛诊断说了算。我一般至少跑两条链从不同的初始值开始然后看Gelman-Rubin诊断因子。Matlab里没有内置函数但计算很简单把每条链按参数分别计算组内方差和组间方差当 (\hat{R}1.1) 时认为收敛。超参数敏感性方面我做过两个测试。第一个是改变步长从0.1到0.3变化结果参数后验均值基本不变只是接受率从0.6降到0.2。第二个是改变 (\rho) 的先验从均匀先验改成Beta(2,2)映射先验发现变点位置的后验众数几乎没有移动。这说明在这个数据强度下先验影响很小。但如果突变幅度变小比如 (\rho_10.4,\rho_20.6)信号弱很多这时候先验的强度就会开始影响后验。遇到这种情况我的建议是尽量保持弱先验并且多跑几条链交叉验证。5. 常见问题与避坑技巧5.1 伪观测边界导致的无穷大经验CDF有一个经典坑如果直接用排序函数计算CDF(U_t) 的最小值会等于 (1/n)最大值等于 (1)。当某个观测正好落在数据极值时(U1)那么 (\Phi^{-1}(1)\infty)高斯Copula似然直接变成NaN。解决办法很简单用修正的经验CDF[ \tilde{U}_t \frac{\text{rank}(X_t)-0.5}{n} ]这样伪观测严格落在 ((0,1)) 之间。如果是参数化边缘分布也尽量不用“正好等于1”的观测。另外在似然函数里加一个防御性判断遇到无穷大直接返回-1e10也可以。5.2 标签切换与多峰后验多变点或多段参数模型中常见的“标签切换”问题在变点Copula里也会出现。比如后验分布可能有两个峰一个是 (\tau100)第一个参数0.2第二个参数0.8另一个是 (\tau400)第一个参数0.8第二个参数0.2。这两种解释对应同样的似然但业务意义完全不同。解决标签切换最简单的方式是加约束。如果先验上知道“后段的联动更强”可以强制 (\rho_2\rho_1)。实现方法是每轮采样后检查约束不满足就拒绝或者在初始化时限制顺序。另一种更稳妥的后处理方法是重标记relabeling。MCMC结束后根据每轮 (\rho_1,\rho_2,\tau) 的取值把参数重新排列到一致性状态。Matlab里可以用kmeans聚类做自动重标记但需要小心别把本质标签搞混。5.3 计算效率的优化Matlab做MCMC很容易写成“慢跑”。几个优化点第一预计算所有不随参数变化的部分。比如 Gaussian Copula 里的norminv结果以及 Copula 密度公式中不依赖 (\rho) 的项都要提前算好。第二更新 (\tau) 时不要重新计算整段似然。假设 (\tau) 只变化了一小步新旧分段大部分数据都没变。你可以缓存前段和后段的累积似然只重新计算 (\tau) 边界附近几个点的变化量。不过这个优化会牺牲代码简洁性数据量几千以下时收益不大。第三用parfor并行跑多条链。我经常开4条链每条链在独立worker上跑最后合并输出。这样不仅得到更多样本还顺手做了收敛诊断。5.4 变点个数如何定如果不知道有几个变点我的建议是先做“变点离散搜索”。把每个可能的 (\tau) 作为单变点算对应的对数似然最大值画一条“剖面似然曲线”。如果曲线上在某个位置出现一个尖峰说明单变点模型合理如果出现两个明显分离的尖峰可能有两个变点。在这个探索性搜索之后再对候选模型跑MCMC并比较DIC。DIC对似然维度的惩罚没有BIC那么严格在贝叶斯框架里用起来比较顺手。但要注意DIC要求后验分布近似多元正态如果后验严重多峰DIC就不可靠。我个人强烈反对一开始就上RJMCMC。先把“固定变点个数”的MCMC调稳定把结果解释清楚再考虑扩展。很多时候业务上只需要“是否存在一个结构性变化”单变点模型已经足够回答了。最后再分享一个小技巧在Matlab里保存MCMC中间结果时别只存参数均值把整条后验链都用.mat文件存下来。后续要做敏感性分析、重标记、DIC计算都需要原始链。只存均值的话遇到审稿人问“收敛性怎么样”“先验改一下结果稳不稳”你就得重新跑一遍那种感觉真的很糟糕。贝叶斯变点Copula这套东西代码本身并不复杂复杂的是对不确定性的理解和处理。先用模拟数据把流程跑通再一点点替换边缘模型和Copula函数你会发现“相关性结构什么时候变了”这个问题终于有了一个能让人放心的答案。
返回列表