☰
传递熵计算:用条件互信息识别时序因果方向
2026/10/3 11:05:15 网站建设 项目流程

简介:面向信息论与复杂系统分析需求,这份传递熵计算资源为从事信号处理、通信网络及因果分析的研究人员与工程师提供了可直接使用的MATLAB实现,内容紧密围绕传递熵、延迟时间与最大熵原理三大核心概念,通过联合概率与条件熵计算帮助量化两个系统间信息的传递方向与程度。压缩包共3个文件,以m源码为主,并附带一张示意图说明算法流程,整体仅20KB,轻量便于快速部署;代码覆盖数据读取、联合概率与条件熵计算、传递熵求解以及不同延迟时间下的扫描寻优,能定位使传递熵最大化的时间间隔,辅助研究者分析系统响应速度与耦合机制,便于后续在此基础上做进一步算法改造与功能扩展。已有221人学习该资源,读者可在此基础上结合自身实验数据开展验证,进一步扩展数据预处理与可视化流程,适用于通信链路效能评估、神经信号耦合分析、金融市场联动探测等多种实际任务。

1. 传递熵计算是什么:用信息流找“谁领先谁几拍”

时序分析做到第三、四周,很多人都会从“看相关性”转向“找因果性”。两个序列算出的皮尔逊相关系数再高,也没法告诉你到底是 A 拽着 B,还是 B 拖着 A;滞后互相关能找到一个延迟峰值,但面对非线性耦合和噪声干扰时,这个峰经常出现在错误的方向上。传递熵计算解决的就是这个问题:用条件互信息衡量“在已知接收方过去的前提下,发送方的过去对接收方未来额外消除了多少不确定”,从而给出一个有方向的、可扫描延迟的量。我第一次接触这套方案是在做新能源功率序列分析时,天气系统、风功率和光伏出力互相纠缠,线性方法全都在贡献伪相关,最后是靠传递熵把主导方向刻出来的。

这类项目经常以“传递熵计算”脚本包的形式出现,压缩包里通常就是三块东西:概率密度估计函数、延迟时间扫描逻辑、以及最大熵原理相关的辅助估计模块。阅读这篇文章的人,不需要懂复杂数学,只要会 Python 和 numpy,跟着代码走完一遍,就能在自己的双变量时序上算出 TE 曲线,再配合最大熵谱估计缩小滞后搜索范围。下面我会从公式、实现、参数到坑位,把整个流程拆给你看。

2. 传递熵的公式拆解:条件互信息与两个关键参数

2.1 条件互信息视角下的传递熵

传递熵 $TE_{Y \to X}$ 的定义用条件互信息写最直观:

$$ TE_{Y \to X}(s) = I(X_{n+s}; Y_n \mid X_n) = \sum p(x_{n+s}, x_n, y_n) \log \frac{p(x_{n+s} \mid x_n, y_n)}{p(x_{n+s} \mid x_n)} $$

这里 $Y$ 是发送方,$X$ 是接收方,$s$ 是延迟步数。右侧的条件概率比里,分子是同时知道 $X_n$ 和 $Y_n$ 后对 $X_{n+s}$ 的预测能力,分母是只知道 $X_n$ 时的预测能力。如果 $Y$ 对 $X$ 的将来没有任何额外信息,条件概率相等,TE 为零;一旦存在耦合,信息差就会让 TE 变成正数。

相比互信息,传递熵天然是非对称的。$TE_{Y \to X}$ 和 $TE_{X \to Y}$ 可能在数值上有明显差异,这正是判断因果方向的依据。需要注意,这种“因果”是预测意义上的,不是实验干预意义上的因果,所以更严谨的说法是“发现方向性的信息流,而不是证明物理因果”。

2.2 延迟时间 s 决定信息传递的时滞

延迟时间 $s$ 是传递熵计算里最重要的参数。$s$ 的意义是:发送方今天状态贡献的信息,需要经过多少个采样间隔才在接收方未来状态中体现出来。实际使用中我没有只算一个 s,而是扫描一个范围,例如s = 1..20,画出 $TE_{Y \to X}(s)$ 的曲线。曲线出现明显峰值的那个位置,就是估计的传递延迟。

如果你有嵌入维数概念,这里还要注意一点:公式里的 $X_n$ 和 $Y_n$ 往往只是一个标量,可真实物理系统可能是高阶动力学。一种扩展做法是把 $X_n$ 替换成过去的嵌入向量 $X_n^{(m)}$,把 $Y_n$ 替换成 $Y_n^{(l)}$,得到更全面的条件互信息。代价是联合概率估计的维度上升,数据量需求急剧增大,所以工程上经常先跑一阶近似,把峰值方向选出来,再针对候选延迟做高阶验证。

2.3 平稳化与去趋势预处理必须在前

传递熵本质上是一堆概率密度的比值,它对联合分布的平稳性很敏感。趋势项会让 $x_n$ 和 $x_{n+s}$ 的均值位置漂移,联合密度结构被时间趋势主导,算出来的 TE 往往要么虚高,要么完全掩盖真实耦合。我一般按三步走:

第一步,对每个序列做一阶差分,消除线性趋势;第二步,如果还有明显的周期性,用滚动窗口减去局部均值;第三步,把两个序列都减均值、除以标准差,让数值尺度统一。需要注意,差分会放大高频噪声,如果信号本身信噪比很低,可以考虑换成符号化方式,用排序模式代替原始数值,这个我在最后一章再展开。

3. 用原生 Python 把传递熵计算跑通

3.1 直方图最小实现:三维联合密度估计

最简单的传递熵实现是直方图法。我们把原始序列拆成三列向量:接收方过去 $x_n$、发送方过去 $y_n$、接收方未来 $x_{n+s}$,然后估计三维联合概率分布,再通过降维得到条件概率比。下面是完整可跑的最小实现。

import numpy as np def hist_te(x, y, lag, bins=8): x = np.asarray(x, dtype=float).ravel() y = np.asarray(y, dtype=float).ravel() n = len(x) - lag if n <= 0: raise ValueError(f"lag={lag} 超过了序列长度") x_past = x[:n] y_past = y[:n] x_fut = x[lag:] # 三个向量长度都是 n samples = np.stack([x_past, y_past, x_fut], axis=1) # 三维直方图:维度 0=x_past, 1=y_past, 2=x_fut counts, edges = np.histogramdd(samples, bins=bins) P3 = counts / counts.sum() # 二维边际概率 Pxy = P3.sum(axis=2) # p(x_past, y_past) Pzx = P3.sum(axis=1) # p(x_past, x_fut) Px = P3.sum(axis=(1, 2)) # p(x_past) # p(x_fut | x_past, y_past) cond_y = P3 / Pxy[:, :, None] # p(x_fut | x_past) cond_x = Pzx / Px[:, None] # 条件概率比,NaN/inf 留在 P3 == 0 的位置,之后被忽略 with np.errstate(divide='ignore', invalid='ignore'): ratio = cond_y / cond_x[:, None, :] log_ratio = np.log(ratio) te = np.sum(P3 * log_ratio, where=(P3 > 0)) return te

这段代码里最需要注意的是np.histogramdd返回的轴顺序。传入samples的三列分别是x_past、y_past、x_fut,所以三维数组第 0 维是x_past的格子,第 1 维是y_past的格子,第 2 维是x_fut的格子。降维求和时,axis=2消掉未来维度得到发送方和接收方联合;axis=1消掉发送方得到接收方过去与未来联合;axis=(1,2)消掉发送方和未来得到接收方过去的边际概率。

计算ratio之前我特意用了np.errstate,因为在稀疏直方图里,cond_x可能为零,产生除零警告。真正参与求和时,where=(P3 > 0)保证只有联合概率非零的格子才计入 TE,避免了0 * inf产生 NaN。

3.2 KDE 版本:用核密度估计替代格子

直方图的主要问题是格子边界不连续,高维情况下样本稀疏,边际概率容易塌缩。KDE 的解法是把事件点周围的信息用核函数扩散开,密度估计更光滑,对中等长度数据更稳。sklearn 的 KernelDensity 可以直接用来估计对数密度,简洁地算出 TE。

from sklearn.neighbors import KernelDensity def kde_te(x, y, lag=1, bandwidth=0.3): x = np.asarray(x, dtype=float).ravel() y = np.asarray(y, dtype=float).ravel() n = len(x) - lag x_past = x[:n] y_past = y[:n] x_fut = x[lag:] X3 = np.c_[x_past, y_past, x_fut] X_xy = np.c_[x_past, y_past] X_xz = np.c_[x_past, x_fut] X_x = x_past.reshape(-1, 1) kd3 = KernelDensity(bandwidth=bandwidth).fit(X3) kd_xy = KernelDensity(bandwidth=bandwidth).fit(X_xy) kd_xz = KernelDensity(bandwidth=bandwidth).fit(X_xz) kd_x = KernelDensity(bandwidth=bandwidth).fit(X_x) # 在样本点本身处评估 log 密度 log3 = kd3.score_samples(X3) log_xy = kd_xy.score_samples(X_xy) log_xz = kd_xz.score_samples(X_xz) log_x = kd_x.score_samples(X_x) # TE = E[ log p(x_fut,x_past,y_past) + log p(x_past) # - log p(x_past,y_past) - log p(x_past,x_fut) ] te = np.mean(log3 + log_x - log_xy - log_xz) return te

这个版本巧妙绕开了显式计算条件概率,直接在样本点上用对数密度做期望。为什么可以这样?因为条件概率比展开后等于联合密度乘积比,所以 TE 近似为四个 KDE 对数密度值的差值平均。KDE 在高斯核下对数据尺度敏感,所以在调用前最好把序列标准化。

坏处也不是没有:KDE 在训练样本点本身做评估会存在轻微的正偏差,因为每个点对自己有贡献。对比两个方向的 TE 时这个偏差大致对称,影响不大;如果要做严格的显著性检验,最好把数据集分成训练和评估两半,但那样样本量要翻倍,短序列容易撑不住。

3.3 参数表:bins、bandwidth、样本量与计算量

参数直方图版本KDE 版本我的默认经验
离散化参数binsbandwidthbins 取 8~12;bandwidth 取 0.2~0.5 或 Scott 规则
最小样本量500 起步800 起步,希望更稳选 2000+少于 300 不要做三维直方图
计算复杂度随 bins 的立方增长训练 O(N log N),评估 O(N)N=5000 时直方图秒级,KDE 约分钟级
敏感性对边界位置敏感对带宽极其敏感参考下一章的等概率分箱

表格里最需要记住的是:直方图不只是看格子数,还要看每个格子里的期望样本数。如果bins=20但数据只有 1000 点,三维空间就有 8000 个格子,平均一个格子只有 0.125 个样本,结果全是噪声。KDE 的带宽同理,带宽太小,密度函数变成一堆尖刺,TE 完全由个体差异主导;带宽太大,所有概率分布都变得接近均匀,传递熵信息被抹平。

3.4 用合成信号做自检:先确认方向没有被代码搞反

任何新写的 TE 函数都要先跑合成数据验证方向。我常用的生成方式是构造一个严格的单向耦合:让 $x$ 是白噪声,$y$ 在延迟两步之后跟随 $x$,但反向 $x$ 不跟随 $y$。这样理论上 $TE_{X \to Y}$ 应该明显大于 $TE_{Y \to X}$。

rng = np.random.default_rng(42) n = 3000 x = rng.standard_normal(n) y = rng.standard_normal(n) for t in range(2, n): y[t] = 0.6 * x[t - 2] + 0.2 * y[t - 1] + 0.1 * rng.standard_normal() te_x_to_y = hist_te(x, y, lag=2, bins=8) te_y_to_x = hist_te(y, x, lag=2, bins=8) print(f"X -> Y: {te_x_to_y:.4f}") print(f"Y -> X: {te_y_to_x:.4f}")

如果代码写错轴顺序,最常见的结果是两个方向几乎相等,或者反向比正向还大。直方图法在小样本下总是会高估 TE,所以不必追求接近理论零值,只要两个方向有显著差异且方向正确,就可以继续往下做延迟扫描。

4. 延迟时间扫描与最大熵原理:把短样本问题交给约束优化

4.1 别只看单点 TE,扫一段滞后曲线

真实耦合不是只在一个整数延迟上存在,信号经过调制、传播和滤波后,信息可能分布在连续几个滞后上。我一般是取lag_max = 20或者 1/4 的采样数量,从 1 扫到lag_max,得到一个 TE 曲线。曲线峰值所在的位置最有价值,但峰的形状同样重要:如果曲线只在某个延迟处尖锐凸起,说明动态关系明确;如果整个曲线都高,说明可能是共驱动因素,需要做条件传递熵。

为什么要先扫延迟而不是直接猜一个?因为传递熵对滞后很敏感。把 s 设小,可能没覆盖到真实传播时延;把 s 设大,条件概率中接收方过去的信息已经包含了部分发送方影响,TE 会被低估。只有在正确的 s 上,接收方过去不会“抢走”发送方未来信息,TE 的额外信息量才是最大的。

4.2 最大熵原理是什么:为什么适合短序列谱估计

延迟扫描范围不是越宽越好。如果信号里有明显周期成分,最自然的做法是把最大扫描延迟设成主周期长度的一倍到两倍,避免在无关滞后上反复试。问题是短序列的傅里叶周期图旁瓣大,谱峰不稳定,这时候就轮到最大熵原理上场。

最大熵原理的基本思想是:在已知约束条件下,选择熵最大的概率分布作为估计结果,因为这是对未知部分最“审慎”的假设。应用到时间序列里,假设已知自相关函数前 p 个值,其他一切未知,那么熵最大化得到的谱正是 AR(p) 模型对应的功率谱。这也是为什么最大熵谱估计在短样本上往往比经典周期图更平滑、峰更稳定——它不假设观测窗口之外数据为零,而是用满足约束的最随机延拓来推断。

4.3 用 Yule-Walker 方程实现最大熵谱估计并圈定延迟范围

一段能直接跑的 Yule-Walker 最大熵谱估计函数如下:

from scipy.linalg import toeplitz def maxent_spectrum(x, order=20, fs=1.0, n_freqs=256): x = np.asarray(x, dtype=float).ravel() x = x - np.mean(x) N = len(x) # 自相关函数,除以 N 得到无偏估计的近似 r = np.correlate(x, x, mode='full')[N - 1:] / N # Yule-Walker 方程:R * a = -r[1:] R = toeplitz(r[:order]) rhs = -r[1:order + 1] ar_coef = np.linalg.solve(R, rhs) # AR 系数,第一个是 1 a = np.r_[1.0, ar_coef] freqs = np.linspace(0, fs / 2, n_freqs) A = np.zeros_like(freqs, dtype=complex) for k, coef in enumerate(a): A += coef * np.exp(-2j * np.pi * k * freqs / fs) # 最大熵谱密度与 |A(freq)|^2 成反比 spec = 1.0 / (np.abs(A) ** 2) return freqs, spec # 使用示例 freqs, spec = maxent_spectrum(x, order=30, fs=1.0) peak_freq = freqs[np.argmax(spec)] print(f"主导频率: {peak_freq:.4f} Hz, 对应周期约 {1/peak_freq:.1f} 个采样点")

参数order是 AR 阶数,控制谱的复杂度。order 太小,谱峰太平,看不出周期;order 太大,会把噪声当成真实谱峰,经验值是order <= N / 10,我习惯从 20 起步。得到主周期 T 之后,把延迟扫描范围设置成 1 到 T 之间,最多到 2*T,就能避开大量无效滞后。

这套流程特别适合短样本数据,最大熵谱估计在 500 点时依然能给出可用的主峰,而周期图可能已经出现多个不相关旁瓣。注意它给出的是发送方或接收方各自内部的主导周期,不是交互延迟;交互延迟还是要靠 TE 曲线峰定位,谱估计只负责缩小候选区。

4.4 用最大熵思想设置三维直方图的等概率边界

很多人在 hist_te 里传bins=8,默认等宽划分,但等宽对数据分布不均匀的场景很吃亏。一段信号如果大部分集中在 0 附近,等宽格子里外围格子完全为空,三维联合概率的估计效率变差。最大熵思想在这里给出一个实用改良:让每个维度上的边界点是样本分位数,使每个一维区间包含大致相等的样本数。

def quantile_edges(data, n_bins): q = np.linspace(0, 1, n_bins + 1) return np.quantile(data, q) # 替换 hist_te 中的 hist_te 调用方法 n_bins = 8 ex = quantile_edges(x_past, n_bins) ey = quantile_edges(y_past, n_bins) ez = quantile_edges(x_fut, n_bins) counts, _ = np.histogramdd(samples, bins=[ex, ey, ez])

这里没有真正去求解带约束的熵最大化,但等概率分箱在离散化过程中最小化信息损失,与最大熵原则的方向一致。相比等宽分箱,它对尾部样本更友好,能够保住少量极端事件对信息流的贡献。注意x_past和x_fut虽然来自同一个序列,但对应时间段不同,经验上建议各自算边界,不要共用一份分位数;直接用同一份边界也不会错,只是边缘区间可能样本分配不均。

5. 传递熵计算避坑指南:五个常见翻车场景

5.1 零滞后伪峰:算出来的 TE 在 s=0 处特别大

现象:把lag=0放进 TE 函数,得到的数值比所有正延迟都大,而且两个方向几乎对称。

原因:零滞后时,$x_{n+s}$ 就是 $x_n$,条件互信息实际上在测“同一时刻两个变量的同步耦合”,而不是预测意义下的信息传递。如果两个序列受同一个外部驱动,比如同一段电网电压波动或者同一个天气过程,零滞后同步会被误判成双向信息流。

解决:永远从lag=1开始扫描。如果确实需要评估瞬时同步,把结果单独标注为“同步强度”,不要和传递熵混为一谈。另一个补救是用置换检验,把发送方序列随机循环平移后重算 TE,过滤掉共同趋势带来的假阳性。

5.2 样本长度不够,直方图结果像随机数

现象:用 200 个点跑完以后,TE 曲线在多个延迟上来回跳,调整 bins 数值,结论彻底反转。

原因:三维直方图要估计的联合概率空间很大。200 个样本放进 8^3 = 512 个格子里,平均每个格子不到 1 个样本,统计波动极大;bins 一变,格子边界一变,密度结构立刻就变。

解决:我的底线是 N = 500 起步,再多也不嫌多。如果只有 200 点,优先用 KDE 或者符号化 TE,不要硬上三维直方图。另一个折中是只估计二维条件互信息,减少一个概率维度,但那样会牺牲高阶耦合信息。

5.3 bin 数固定导致稀疏矩阵和除零

现象:代码报RuntimeWarning: divide by zero,最后 TE 结果是 NaN 或者无穷大。

原因:固定 bins 下,很多格子的counts是零,cond_x分母出现零。虽然用了np.errstate屏蔽警告,但如果在处理前给P3加了1e-12平滑,会把所有空格子都当成极小概率参与 log 计算,噪声被线性放大。

解决:不要在密度估计上人为加小常数,而是用where=(P3 > 0)掩码处理对数项。如果要用平滑,也要用 Dirichlet 先验密度,比如在counts + alpha之后重新归一化,alpha 取 0.5 这类值,而不是随意加一个常数。

5.4 带宽选择让 KDE 方向性消失

现象:KDE 版本算出来两个方向的 TE 几乎一样,或者数值大到离谱。

原因:bandwidth 设得太小时,KDE 密度函数退化成一系列脉冲,每个样本点只看得到自己,条件概率比趋近于 1,TE 被高估;bandwidth 设得太大时,所有密度函数都变成同一个扁平高斯,条件概率比趋近于 1,TE 又被低估。两个极端都会抹平方向差异。

解决:用 Scott 规则给一个起点:bandwidth = N ** (-1 / (d + 4)),其中 d 是维度。三维联合密度用 d=3,二维边际用 d=2,但 KDE 函数里我用的是同一个带宽,简化处理。靠谱的做法是分别用 0.5、1、2 倍默认带宽各跑一遍,看 TE 峰值位置是否稳定;峰值位置能稳定,数值有差别,结论才值得信任。

5.5 不做置换检验,任何正数 TE 都没有意义

现象:算出的 TE = 0.02,看起来是正数,于是宣布找到了方向性耦合。

原因:传递熵的估计量是正偏的,直方图法和 KDE 法在有限样本下都会产生正的“基础噪声”。两个白噪声序列也能算出一个明显大于零的 TE,这并不代表有信息流。

解决:做零假设检验。把发送方序列循环平移随机步,破坏原始配对关系,再重新计算 TE,重复 200 到 1000 次,得到零分布。如果真实 TE 超过了零分布的 95% 分位点,才可以说存在显著信息流。循环平移比简单打乱好,因为它能保留发送方序列内部的时间相关性,避免因为自相关结构改变而产生假结论。

6. 进阶用法:多变量条件 TE 与稳定性验证

6.1 条件传递熵剔除公共驱动

当第三个序列 Z 同时驱动 X 和 Y 时,普通 TE 会把虚假方向算出来。条件传递熵在估计条件概率时,把 Z 的过去也加入条件集合,也就是把p(x_{n+s} | x_n, y_n)改成p(x_{n+s} | x_n, y_n, z_n),并与不包含 y_n 的条件概率做比值。实现上就是在直方图或 KDE 里多加一列数据,样本需求会大幅上升,所以高阶场景建议直接用符号化。

6.2 符号化排序近似

符号化 TE 不估计连续密度,而是把三维样本按数值排序。对每个时刻 t,看x_past、y_past、x_fut的大小顺序,用排列模式代替原始值,然后在离散模式上计数,计算条件互信息。这个方法对异常值和噪声极稳,参数只有一个延迟 s,不需要调带宽。缺点是三变量的排序模式最多 6 种,信息分辨率有限,适合作为 TE 结果的交叉验证。

6.3 稳定性验证:我自己的三条结局检查

我现在拿到任何 TE 结果,都要求自己把这三件事做齐:第一,画 TE 随延迟变化的曲线而不是只报一个峰值;第二,把数据切成两段,分别计算,看峰值延迟是否一致;第三,至少做 500 次置换检验,把原始 TE 和零分布画在同一张图上。如果峰值移动超过两个采样点,或者 p 值大于 0.05,我宁可不出报告。多年下来,这个习惯帮我抓住了很多本来会写进结论里的假因果。

实现层面还有一个实用技巧:把延迟扫描和带宽测试封装成一个循环,输出一个三列数组,包含延迟、两个方向的 TE、以及置换 p 值;这样每个项目只要跑一次,结果就能直接进周报和幻灯片。做传递熵计算时,“算得出来”永远不是终点,“稳定可复现”才是。希望帮到你。

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

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

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

立即咨询