简介:这份资源面向信息论、复杂系统分析与信号处理方向的学习者和研究者,聚焦传递熵计算与延迟时间估计这一具体问题。传递熵用于量化两个随机变量间的信息传递强度,而延迟时间决定信息从一个系统传到另一个系统所需的时间间隔,二者结合可支撑通信效率评估、生物信号分析、控制系统响应研究等场景。压缩包共3个文件,以2个m脚本和1张jpg图片为主,脚本承担传递熵与延迟时间的核心计算流程,图片可能用于结果示意,整体约20KB,轻量易读。内容覆盖数据预处理、联合概率分布与条件熵计算、传递熵求解,以及通过扫描不同延迟下传递熵变化来确定最优延迟点,并给出结果可视化思路。目前已有221人学习,适合希望快速上手最大熵原理与传递熵延迟估计实践的中级读者参考。
1. 传递熵与延迟时间:为什么你算的因果方向总是反的
两个传感器同时采数据,一个明显驱动另一个,可传递熵跑出来两个方向的值几乎一样大,甚至反向更大。这不是代码写错了,而是延迟时间没定对。传递熵衡量的是「X 的历史在多大程度上能降低 Y 未来状态的不确定性」,这个「历史」取多长、往未来推几步,直接决定结果。延迟时间选错,因果方向就会翻车,这是我在做多通道时序因果分析时踩过最深的坑。
标题里的「传递熵传递时间计算」要解决的就是这件事:给定两条时间序列,怎么确定最优延迟时间,再在这个延迟下算传递熵,让因果方向可信。它适合做神经信号、工业传感器、金融时序、气象数据这类多变量因果推断的人。最大熵原理在这里不是装饰——它决定了我们在估计概率分布时怎么选先验,避免样本不足导致的估计偏差。下面从延迟时间怎么定、传递熵怎么算、最大熵原理怎么嵌进去,一路讲到能跑通的代码和参数。
2. 延迟时间与嵌入维数:传递熵计算的前置条件
2.1 延迟时间为什么不能拍脑袋定
传递熵的定义建立在状态空间重构之上。对时间序列 X,我们构造延迟向量 X_t = [x_t, x_{t-τ}, x_{t-2τ}, …],其中 τ 就是延迟时间。如果 τ 取太小,相邻坐标高度相关,重构出来的吸引子被压缩在一条对角线附近,传递熵会低估;τ 取太大,坐标之间失去动力学关联,重构变成随机散布,传递熵会高估甚至出现虚假因果。
常见做法有两类:自相关函数法和互信息法。自相关法取自相关函数第一次降到 1/e 以下对应的滞后,简单但对非线性系统不够稳。互信息法取互信息第一个局部极小值对应的滞后,对非线性更鲁棒,是我一般会优先用的。下面这段代码用互信息法估计延迟时间。
import numpy as np from sklearn.neighbors import KernelDensity def mutual_information(x, y, bins=32): """用直方图法估计两序列的互信息""" c_xy = np.histogram2d(x, y, bins=bins)[0] c_x = np.histogram(x, bins=bins)[0] c_y = np.histogram(y, bins=bins)[0] # 归一化成概率 p_xy = c_xy / np.sum(c_xy) p_x = c_x / np.sum(c_x) p_y = c_y / np.sum(c_y) # 去掉零概率避免 log(0) mask = p_xy > 0 mi = np.sum(p_xy[mask] * np.log(p_xy[mask] / (p_x[:, None] * p_y[None, :])[mask])) return mi def estimate_delay_mi(signal, max_lag=50): """互信息第一个局部极小值对应的滞后""" mi_values = [] for lag in range(1, max_lag + 1): mi = mutual_information(signal[:-lag], signal[lag:]) mi_values.append(mi) mi_values = np.array(mi_values) # 找第一个局部极小 for i in range(1, len(mi_values) - 1): if mi_values[i] < mi_values[i-1] and mi_values[i] < mi_values[i+1]: return i + 1, mi_values return np.argmin(mi_values) + 1, mi_values逻辑说明:mutual_information用二维直方图估计联合概率和边缘概率,再算 MI。estimate_delay_mi对每个滞后算 MI,找第一个局部极小。参数max_lag一般取序列长度的 1/10 到 1/5,太大会引入噪声。bins取 32 到 64 之间,样本少就取小一点,否则直方图太稀疏。
注意:互信息曲线不一定有干净的局部极小,如果一路下降或一路震荡,说明数据长度不够或噪声太大,这时候要回到自相关法交叉验证。
2.2 嵌入维数怎么和延迟时间配合
延迟时间定了,嵌入维数 m 也得定。m 太小,重构不完整,传递熵偏低;m 太大,计算量爆炸且引入冗余。常用虚假最近邻法(FNN)或 Cao 方法。我一般用 Cao 方法,它对噪声更稳。Cao 方法计算 E1(m) 和 E2(m),E1 趋于饱和时对应的 m 就是最小嵌入维数。
def cao_embedding_dim(signal, max_dim=10, tau=1): """Cao 方法估计最小嵌入维数""" N = len(signal) E1 = [] for m in range(1, max_dim + 1): # 构造 m 维和 m+1 维延迟向量 M = N - m * tau if M <= 0: break Y_m = np.array([signal[i:i + m * tau:tau] for i in range(M)]) Y_m1 = np.array([signal[i:i + (m + 1) * tau:tau] for i in range(M)]) # 找每个点的最近邻 a = [] for i in range(M): dist = np.linalg.norm(Y_m - Y_m[i], axis=1) dist[i] = np.inf nn = np.argmin(dist) # m+1 维下的距离比 d_m = dist[nn] d_m1 = np.linalg.norm(Y_m1[i] - Y_m1[nn]) if d_m > 0: a.append(d_m1 / d_m) E1.append(np.mean(a)) # E1 饱和处即为嵌入维数 return E1逻辑说明:对每个维数 m,找每个点在 m 维空间的最近邻,再看这对点在 m+1 维的距离比。如果 m 已经足够,距离比接近 1,E1 不再明显变化。参数max_dim一般取 5 到 15,tau用上一步估的延迟时间。E1 曲线拐点就是嵌入维数,实际操作中取 E1 变化小于 5% 的第一个 m。
2.3 延迟时间与传递熵的耦合关系
很多人把延迟时间和传递熵里的预测步长混为一谈。延迟时间 τ 是重构用的,传递熵里还有一个预测步长 u,表示「X 的历史能预测 Y 多少步之后的状态」。常见做法是 u 取 1,但如果物理过程有明显传播延迟,u 应该取那个传播延迟。我一般会先算互相关,看 X 和 Y 的峰值滞后,那个滞后就是 u 的候选值。
| 参数 | 含义 | 常用取值 | 影响 |
|---|---|---|---|
| τ | 重构延迟时间 | 互信息第一极小 | 太小低估,太大高估 |
| m | 嵌入维数 | Cao 方法饱和点 | 太小不完整,太大冗余 |
| u | 预测步长 | 互相关峰值滞后 | 决定因果时间尺度 |
| k | 最近邻数 | 4 到 8 | 影响 KSG 估计稳定性 |
这三个参数定完,传递熵的计算才有意义。下面进入具体计算。
3. 传递熵计算:从 KSG 估计到最大熵先验
3.1 传递熵的定义与 KSG 估计
传递熵 TE(X→Y) 的定义是:
TE(X→Y) = Σ p(y_{t+u}, y_t, x_t) log [ p(y_{t+u} | y_t, x_t) / p(y_{t+u} | y_t) ]
直接估计高维概率密度不现实,Kraskov 等人提出的 KSG 估计用 k 近邻距离来估计互信息,避免显式密度估计。传递熵可以拆成几个互信息的组合:
TE(X→Y) = I(Y_{t+u}; X_t | Y_t) = I(Y_{t+u}; X_t, Y_t) - I(Y_{t+u}; Y_t)
这样就能用 KSG 分别估计两个条件互信息。下面是一个可复现的实现。
from scipy.spatial import cKDTree def ksg_mi(x, y, k=4): """KSG 估计互信息 I(X;Y)""" x = x.reshape(-1, 1) y = y.reshape(-1, 1) xy = np.hstack([x, y]) N = len(x) # 在联合空间找第 k 近邻 tree_xy = cKDTree(xy) dist_xy, _ = tree_xy.query(xy, k=k+1) eps = dist_xy[:, -1] # 第 k 近邻距离 # 在边缘空间数邻居 tree_x = cKDTree(x) tree_y = cKDTree(y) # 严格小于 eps 的邻居数 n_x = np.array([len(tree_x.query_ball_point(x[i], eps[i] - 1e-10)) - 1 for i in range(N)]) n_y = np.array([len(tree_y.query_ball_point(y[i], eps[i] - 1e-10)) - 1 for i in range(N)]) # KSG 公式 mi = np.mean(np.log(n_x + 1) + np.log(n_y + 1)) - np.mean(np.log(N)) - np.mean(np.log(eps)) return mi def transfer_entropy(x, y, tau=1, m=3, u=1, k=4): """计算 TE(X→Y)""" N = len(x) # 构造延迟向量 M = N - max((m - 1) * tau, u) Y_future = y[u:u + M] Y_past = np.array([y[i:i + m * tau:tau] for i in range(M)]) X_past = np.array([x[i:i + m * tau:tau] for i in range(M)]) # I(Y_future; X_past, Y_past) - I(Y_future; Y_past) XY_past = np.hstack([X_past, Y_past]) mi_joint = ksg_mi(Y_future, XY_past, k=k) mi_y = ksg_mi(Y_future, Y_past, k=k) return mi_joint - mi_y逻辑说明:ksg_mi是核心,在联合空间找第 k 近邻距离 eps,然后在边缘空间数严格小于 eps 的邻居数,套 KSG 公式。transfer_entropy构造延迟向量后,用两个互信息相减得到条件互信息。参数k取 4 到 8,样本多可以取大一点,样本少取 4。m和tau用第 2 章估的值。
注意:KSG 估计对距离的严格小于判断很敏感,
eps[i] - 1e-10这个微小偏移是为了避免边界点被重复计数,不加会导致结果偏大。
3.2 最大熵原理在概率估计中的角色
最大熵原理说的是:在只掌握部分约束的情况下,应该选满足约束且熵最大的分布作为先验。在传递熵计算里,这体现在两个地方。第一,当我们用直方图或核密度估计概率时,最大熵先验能防止过拟合。第二,在样本不足时,最大熵约束下的分布估计比最大似然更稳。
具体做法是:对联合分布 p(y_{t+u}, y_t, x_t) 加最大熵约束,即固定低阶矩(均值、方差、互信息),让高阶矩自由。实现上可以用最大熵密度估计替代直方图,但计算量大。我一般用简化版:在 KSG 估计前,先对数据做秩变换,把边缘分布变成均匀分布,这样最大熵先验自然满足,KSG 估计也更稳。
from scipy.stats import rankdata def rank_transform(signal): """秩变换,把边缘分布变成均匀分布""" return rankdata(signal) / len(signal) # 使用示例 x_rank = rank_transform(x) y_rank = rank_transform(y) te = transfer_entropy(x_rank, y_rank, tau=tau, m=m, u=u, k=4)逻辑说明:秩变换不改变单调关系,但把任意边缘分布变成均匀分布,均匀分布是最大熵分布,这样后续的互信息估计不受边缘分布形状影响。参数没有额外需要调的,直接对原始序列做变换即可。
3.3 显著性检验:怎么判断传递熵值是不是显著
算出一个 TE 值不够,得知道它是不是显著大于零。常用代理数据法:把 X 随机打乱多次,每次算 TE,得到零分布,再看原始 TE 在零分布里的分位数。
def te_significance(x, y, tau=1, m=3, u=1, k=4, n_surrogate=200): """代理数据法检验 TE 显著性""" te_obs = transfer_entropy(x, y, tau, m, u, k) te_null = [] for _ in range(n_surrogate): x_shuffle = np.random.permutation(x) te_null.append(transfer_entropy(x_shuffle, y, tau, m, u, k)) te_null = np.array(te_null) p_value = np.mean(te_null >= te_obs) return te_obs, p_value, te_null逻辑说明:打乱 X 破坏 X 和 Y 的因果关联但保留各自的时间结构,算出的 TE 就是零假设下的分布。n_surrogate取 200 到 1000,p 值小于 0.05 认为显著。参数k和主计算保持一致。
4. 避坑与排查:传递熵计算里最容易翻车的五个地方
4.1 现象:两个方向 TE 几乎相等,因果方向分不出来
原因:延迟时间 τ 取太小,重构向量高度共线,X 和 Y 的历史信息几乎一样,条件互信息差被淹没。解决:用互信息法重新估 τ,确保 τ 至少大于自相关衰减尺度。如果还是分不开,检查预测步长 u 是否取 1,物理传播延迟明显时 u 要取那个延迟。
4.2 现象:TE 值随样本量增加持续增大,不收敛
原因:KSG 估计里 k 固定,样本增多时第 k 近邻距离变小,log(eps) 项偏移,导致 TE 漂移。解决:k 随样本量调整,经验公式 k ≈ sqrt(N)/10,或者用固定 k 但报告时注明样本量。更稳的做法是用 k=4 但样本量控制在 1000 到 5000 之间。
4.3 现象:秩变换后 TE 值比原始数据小很多
原因:秩变换把边缘分布压成均匀,如果原始数据有强非线性,秩变换会损失部分高阶依赖。解决:对比秩变换前后的 TE,如果差异超过 30%,说明数据非线性强,应该用原始数据加最大熵密度估计,而不是简单秩变换。
4.4 现象:代理检验 p 值总是 0.05 附近,结论不稳
原因:代理数据只打乱 X,没有保留 X 的自相关结构,零分布偏窄。解决:用相位随机化代理或循环平移代理,保留自相关但破坏因果。循环平移实现简单:把 X 循环移动随机步长,再算 TE。
4.5 现象:多通道数据里 TE 矩阵不对称但物理上应该对称
原因:不同通道的噪声水平不同,KSG 估计对噪声敏感,噪声大的方向 TE 被低估。解决:先做去噪或平滑,或者用条件传递熵排除第三通道影响。如果物理上确实不对称,那不对称是对的,别强行对称化。
5. 进阶技巧:用最大熵约束稳住小样本传递熵
小样本是传递熵计算最头疼的事。样本少于 500 时,KSG 估计方差大,代理检验也做不稳。我一般用最大熵约束做正则化:在 KSG 估计的互信息上加一个最大熵惩罚项,等价于对联合分布加低阶矩约束。
具体做法是:先算数据的均值和协方差,构造最大熵分布(高斯分布是给定均值和协方差下的最大熵分布),然后用这个高斯分布的互信息作为先验,和 KSG 估计做加权平均。
def maxent_regularized_te(x, y, tau=1, m=3, u=1, k=4, alpha=0.3): """最大熵正则化的传递熵""" te_ksg = transfer_entropy(x, y, tau, m, u, k) # 高斯先验下的互信息(线性互信息) from numpy.linalg import det xy = np.hstack([x.reshape(-1,1), y.reshape(-1,1)]) cov_xy = np.cov(xy.T) cov_x = np.cov(x.reshape(-1,1).T) cov_y = np.cov(y.reshape(-1,1).T) mi_gauss = 0.5 * np.log(det(cov_x) * det(cov_y) / det(cov_xy)) # 加权融合 te_reg = (1 - alpha) * te_ksg + alpha * mi_gauss return te_reg, te_ksg, mi_gauss逻辑说明:mi_gauss是高斯假设下的互信息,计算快但只捕捉线性依赖。alpha控制正则化强度,样本越少 alpha 越大,一般取 0.2 到 0.5。样本多于 2000 时 alpha 取 0.1 以下。这个融合不是理论最优,但在我做过的神经信号和工业传感器数据上,小样本下比纯 KSG 稳很多。
验证方法:把数据分成两半,各自算 TE,看一致性。如果两半差异小于 15%,说明估计稳;差异大就增大 alpha 或增加样本。另一个验证是看 TE 随 u 的变化曲线,物理上 TE 应该在真实传播延迟处出现峰值,如果曲线平坦,说明参数没定对。
我自己的习惯是:拿到两条序列,先画互信息和自相关,定 τ;再跑 Cao 方法定 m;然后扫 u 从 1 到 20,看 TE 峰值位置;最后用代理检验确认显著性。这一套走完,因果方向基本不会翻车。希望帮到你。
本文还有配套的精品资源,点击获取