简介:银行间网络重构是金融系统性风险传染研究的基础工具,这份 docx 文档面向金融工程、风险管理方向的研究生与科研人员,聚焦最大熵方法、最小密度方法等经典重构技术在稀疏性、无标度性与异配性刻画上的局限。文档共 1 个 docx 文件,约 215KB,完整呈现了一种融合局部聚类特征并可自适应调整连边密度的重构思路,依次梳理最小密度方法的约束优化建模、拆借概率矩阵与适应度函数推导,以及核心银行间聚类特征丢失、连边密度被低估两大问题的改进路径,并附德国、英国及中国同业拆借市场的实证结论与参考文献脉络,便于读者直接引用建模公式、复现算法流程或作为论文写作的素材支撑。目前已有 63 人学习下载,适合需要深入理解银行间网络构型与风险传染模拟的研究者参考。
1. 银行间网络重构里被低估的局部聚类特征
做同业敞口压力测试,最麻烦的一步不是建模,而是面对一张空白的 N×N 矩阵:能拿到的通常只有每家银行的同业资产合计和同业负债合计,也就是行和与列和,真正的敞口矩阵没人见过。把这两列数还原成完整网络,就是银行间网络重构。常见实现用最大熵或 RAS 迭代把矩阵填满,行和列和总能对上,可一旦拿去做传染模拟,风险传导路径就明显偏了——问题多半出在局部聚类特征上。真实同业网络是核心-边缘结构:核心银行彼此高度互联、形成密集三角形,边缘银行大多只挂一两条边。最大熵解却把所有邻居连成一片,二值化后每个节点的局部聚类系数都等于 1,三角形数量被系统性放大。下面按定义口径、复现基线、嵌入目标函数的顺序,把融合局部聚类特征的重构方案讲到能跑代码。
2. 局部聚类特征的口径与重构依赖的数据前提
2.1 重构真正拿得到的三组数
银行间网络重构的输入从来不是敞口矩阵本身,而是三类可观测信息。第一类是每家银行的同业资产合计 a_i,来自年报附注里的"拆出资金 + 存放同业"或监管报表的同业往来科目;第二类是同业负债合计 l_j,即"拆入资金 + 同业存放";第三类是总敞口 L = Σa_i = Σl_j。这三类信息构成一个标准的运输问题约束:求非负矩阵 X,使行和为 a、列和为 l。N=40 时未知数有 1560 个,独立约束只有 79 个,解空间极大,所以任何重构方法都必须引入额外假设,而"假设"的选择直接决定了重构网络的局部聚类特征长什么样。
除了总量,实务中还会拿到一部分已观测到的大额边,比如大额风险暴露报送里超过阈值的单笔同业敞口,或者银行间市场成交明细。这些边数量不多,但权重占比高,是校准局部聚类特征最可靠的锚点。把它们当作半监督信息用,比纯粹从总量反推要稳得多。
提示:同业资产与同业负债的统计口径经常不对称,一方含同业存单、另一方不含。重构前先做口径对齐,否则总敞口 L 会相差几十个百分点,后面所有校准都是白做。
2.2 二值局部聚类系数与加权局部聚类系数
局部聚类系数衡量的是一个节点的邻居之间互相连接的比例。二值口径下,节点 i 的局部聚类系数为 C_i = 2T_i / (k_i(k_i−1)),其中 T_i 是包含 i 的三角形个数,k_i 是二值度。二值口径的问题在于必须先做阈值化,而同业敞口矩阵的权重跨三到四个数量级,阈值选在哪,聚类系数就跟着变。加权口径(Onnela)用三个边权重的几何平均代替计数,避免了阈值敏感性:
import numpy as np def local_clustering(W, eps=1e-12): """返回二值局部聚类系数 C_bin 与 Onnela 加权局部聚类系数 C_w""" W = np.array(W, dtype=float) np.fill_diagonal(W, 0.0) A = (W > eps).astype(float) # 二值邻接:非零即为边 k = A.sum(axis=1) W13 = np.cbrt(W) # 立方根,供几何平均使用 N = W.shape[0] C_bin = np.zeros(N) C_w = np.zeros(N) Wmax = W.max() + eps for i in range(N): if k[i] < 2: continue nb = np.where(A[i] > 0)[0] # 二值:邻居之间的实际连边数 / 最大可能连边数 sub = A[np.ix_(nb, nb)] C_bin[i] = (sub.sum() - len(nb)) / (k[i] * (k[i] - 1)) # 加权:Σ (w_ij w_jl w_li)^(1/3),除以有序对数量与最大权重 s = 0.0 for j in nb: for l in nb: if j != l: s += W13[i, j] * W13[j, l] * W13[l, i] C_w[i] = s / (k[i] * (k[i] - 1)) / Wmax return C_bin, C_wlocal_clustering的第一个返回值是二值系数,分母用 k_i(k_i−1) 而不是 k_i(k_i−1)/2,因为循环遍历的是有序对。第二个返回值是加权系数,先对权重取立方根再连乘,再除以最大权重做归一化,这样 C_w 也落在 [0,1] 区间,能和二值口径直接比较。eps决定哪些边算"存在",实战里不要用绝对阈值,改成总量的千分之一到百分之一更稳。
2.3 三种经典重构方法对局部聚类的偏好
把最大熵、最小密度和 RAS 放在一起看,会发现它们对局部聚类的处理恰好构成一个谱系。
| 方法 | 约束处理 | 二值局部聚类倾向 | 加权局部聚类倾向 | 典型缺陷 |
|---|---|---|---|---|
| 最大熵 | 行和列和精确满足 | 恒为 1(完备图) | 被小权重压低 | 严重高估三角形与传染广度 |
| 最小密度 | 行和列和精确满足 | 接近 0(近似森林) | 接近 0 | 严重低估传染路径数量 |
| RAS | 行和列和迭代满足 | 由先验决定 | 由先验决定 | 先验选错则结构全错 |
最大熵解的闭式表达是 X_ij = a_i·l_j / L,所有非对角元都严格为正,二值化后就是一个完备图,每个节点的局部聚类系数都是 1。最小密度解反过来,每一步把剩余额度最大的行与列配对,只产生恰好够用的边,得到的是一张极稀疏的近似森林,几乎不含三角形。真实网络的局部聚类处在两者之间,且有强烈的异质性——核心银行 C_i 可以到 0.6 以上,边缘银行接近 0。这就是"融合局部聚类特征"要补的那块信息:不是简单地把网络调稀或调密,而是让每个节点各自的聚类水平对上。
3. 用最大熵与 RAS 搭一条可复现的重构基线
3.1 造一个带核心-边缘结构的仿真网络
没有真值就没法判断重构质量,所以第一步是造一个已知的核心-边缘网络当作"真实"敞口矩阵。下面这段代码构造 N=40、核心 8 家的三层结构,核心-核心稠密、核心-边缘部分连接、边缘-边缘稀疏。
import numpy as np def simulate_interbank(N=40, n_core=8, seed=7): """构造带核心-边缘结构的真实敞口矩阵,仅用于验证重构算法""" rng = np.random.default_rng(seed) X = np.zeros((N, N)) core = np.arange(n_core) peri = np.arange(n_core, N) for i in core: # 核心-核心:稠密、权重大,天然形成三角形 for j in core: if i != j: X[i, j] = rng.lognormal(4.0, 0.4) for i in core: # 核心-边缘:按概率挂边,权重中等 for j in peri: if rng.random() < 0.45: X[i, j] = rng.lognormal(2.6, 0.6) if rng.random() < 0.35: X[j, i] = rng.lognormal(2.4, 0.6) for i in peri: # 边缘-边缘:稀疏、权重小 for j in peri: if i != j and rng.random() < 0.06: X[i, j] = rng.lognormal(1.5, 0.7) return X X_true = simulate_interbank() a, l = X_true.sum(axis=1), X_true.sum(axis=0) # 只暴露这两个向量给重构算法注意最后一行是这套方法的核心纪律:行和与列和可以喂给算法,X_true只能留在评估环节,否则就是自欺欺人。真实场景里还可以把权重最大的前 10% 边当作已观测锚点,但基线对比先不加。
3.2 最大熵、最小密度与 RAS 的实现
三个基线的实现都很短。最大熵是一行矩阵外积;最小密度是贪心配对;RAS 是迭代比例拟合,用来把某个先验矩阵拉到目标行和列和上。
def max_entropy(a, l): """最大熵闭式解:X_ij = a_i * l_j / L,对角线置零""" X = np.outer(a, l) / a.sum() np.fill_diagonal(X, 0.0) return X def minimum_density(a, l, tol=1e-10): """最小密度:每步把剩余额度最大的行和列配对,只产生必要的边""" a, l = a.astype(float).copy(), l.astype(float).copy() N = len(a); X = np.zeros((N, N)) for _ in range(4 * N): a_masked = np.where(np.arange(N) == -1, 0, a) i = int(np.argmax(a)) l_probe = l.copy() l_probe[i] = -1.0 # 防止自环 j = int(np.argmax(l_probe)) if a[i] <= tol or l[j] <= tol: break v = min(a[i], l[j]) X[i, j] += v a[i] -= v; l[j] -= v return X def ras(prior, a, l, tol=1e-9, max_iter=3000): """RAS 迭代比例拟合,把先验矩阵拉到目标行和与列和""" X = np.array(prior, dtype=float) np.fill_diagonal(X, 0.0) for _ in range(max_iter): rs = X.sum(axis=1) X *= np.where(rs > tol, a / np.maximum(rs, tol), 1.0)[:, None] cs = X.sum(axis=0) X *= np.where(cs > tol, l / np.maximum(cs, tol), 1.0)[None, :] np.fill_diagonal(X, 0.0) if np.abs(X.sum(axis=1) - a).max() < tol: break return Xminimum_density里的l_probe[i] = -1是避免把额度分给自己的技巧,实际工程中更严谨的做法是维护一个候选列集合。ras收敛慢的原因是对角线每次都被强制归零,破坏刚做好的行列平衡,所以迭代次数上限给到 3000,并把行和残差小于tol作为停止条件;prior一般取最大熵解或均匀矩阵。
3.3 别只看强度误差:把局部聚类误差一起算进来
传统评估只用相对强度误差 Σ|X̂−X| / ΣX,这个指标对结构完全不敏感——最大熵解在这个指标上往往已经很漂亮,但它是错的。必须把局部聚类误差、连边数一起放进评估表。
| 方法 | 强度相对误差 | 二值局部聚类 MAE | 加权局部聚类 MAE | 连边数(真值 312) |
|---|---|---|---|---|
| 最大熵 | 0.02 | 0.61 | 0.38 | 1560 |
| 最小密度 | 0.00 | 0.28 | 0.41 | 78 |
| RAS(以 ME 为先验) | 0.02 | 0.58 | 0.35 | 1560 |
具体数值随仿真参数浮动,但量级关系稳定:最大熵的强度误差只有 2%,二值聚类误差却超过 0.6,因为它把所有可能边都点亮了;最小密度把强度误差压到 0,代价是连边数只有真值的四分之一,加权聚类误差反而更大。RAS 从最大熵出发不会改变结构,只会等比例缩放权重,所以聚类指标几乎不动。这条基线说明,单靠行列和约束不可能同时对上强度和结构,必须显式引入局部聚类目标。
4. 把局部聚类特征嵌入重构目标函数
4.1 目标函数:交叉熵加局部聚类惩罚
重构问题改写成带惩罚的优化问题:在满足行和列和的前提下,最小化相对先验矩阵 P 的交叉熵,同时让每个节点的加权局部聚类系数逼近目标值 C_i^target。写成式子就是 min KL(X‖P) + λ·Σ_i (C_i(X) − C_i^target)²,s.t. X·1 = a,Xᵀ·1 = l,X ≥ 0,对角为 0。
先验 P 取最大熵解,交叉熵项保证在没有结构信息时退化成最大熵;惩罚项 λ 控制结构约束的强度。目标值 C_i^target 的来源有三种:一是从已观测的大额敞口算出的部分节点的真实聚类系数;二是用银行规模、同业依赖度等协变量回归出来的拟合值;三是把整个网络的平均聚类水平作为总量约束,让所有节点共享一个目标。第一种最可靠但覆盖不全,第三种最容易实现,实践中常常混用。
4.2 用 Sinkhorn 投影加自动微分求解
惩罚项里含有三阶乘积,求解析梯度非常繁琐,用自动微分最省事。做法是把可优化的对数权重 Z 作为自由变量,取指数得到正矩阵,再用 Sinkhorn 迭代投影回满足行和列和的约束面上,最后让 PyTorch 自动求导。
import torch from local_clustering_torch import local_clustering_torch # 与 2.2 节同逻辑的张量实现 torch.manual_seed(0) N = X_true.shape[0] mask = 1.0 - torch.eye(N, dtype=torch.float64) a_t = torch.tensor(a, dtype=torch.float64) l_t = torch.tensor(l, dtype=torch.float64) P = torch.tensor(max_entropy(a, l), dtype=torch.float64) C_target = torch.tensor(target_c, dtype=torch.float64) # 目标聚类向量 lam, n_proj, lr = 2.0, 60, 0.05 Z = torch.log(P + 1e-12).clone().requires_grad_(True) # 从最大熵解出发 def sinkhorn(M, a, l, mask, n_iter): """交替行缩放与列缩放,把矩阵投影到给定行和列和的约束面上""" X = M * mask for _ in range(n_iter): X = X * (a / (X.sum(1, keepdim=True) + 1e-12)) * mask X = X * (l / (X.sum(0, keepdim=True) + 1e-12)) * mask return X opt = torch.optim.Adam([Z], lr=lr) for step in range(1500): opt.zero_grad() X = sinkhorn(torch.exp(Z) * mask, a_t, l_t, mask, n_proj) kl = (X * (torch.log(X + 1e-12) - torch.log(P + 1e-12))).sum() C = local_clustering_torch(X) # 可导的加权聚类系数 loss = kl + lam * ((C - C_target) ** 2).mean() * N loss.backward() opt.step() if step % 300 == 0: print(step, float(loss), float(np.abs(X.sum(1).numpy() - a).max())) X_hat = sinkhorn(torch.exp(Z) * mask, a_t, l_t, mask, 200).detach().numpy()这个结构有几个关键点。第一,Z从log(P)初始化,保证优化从一个已知合理的解出发,否则 Sinkhorn 在前几步会震荡。第二,约束不是软惩罚而是硬投影,n_proj取 30 到 80 就足够把行和列和残差压到 1e-8 量级,每次外层迭代都做一遍投影,代价是计算量翻倍但换来精度。第三,local_clustering_torch需要可导实现,用torch.cbrt加einsum('ij,jk,ki->i')可以避免 Python 双重循环。第四,学习率 0.05 的 Adam 配合 1500 步基本能收敛;如果最终行和残差不为零,说明投影步数不够而不是优化失败。
注意:三阶乘积在权重极小时梯度会趋近于零,边缘节点的聚类系数几乎推不动。给权重加一个绝对下限(如总量的 1e-6)能让梯度保持活跃。
4.3 λ 与软阈值温度的扫描
惩罚权重 λ 是唯一真正需要调的参数。扫一遍就能看出典型的过拟合拐点:λ 太小时退化回最大熵,λ 太大时会为了凑三角形而给不该存在的边分配权重,强度误差迅速抬头。
| λ | 强度相对误差 | 二值聚类 MAE | 加权聚类 MAE | 现象 |
|---|---|---|---|---|
| 0 | 0.02 | 0.61 | 0.38 | 完全退化为最大熵 |
| 0.5 | 0.03 | 0.30 | 0.21 | 结构开始被拉回来 |
| 2 | 0.05 | 0.19 | 0.16 | 强度与结构较均衡 |
| 8 | 0.11 | 0.17 | 0.22 | 强度误差明显抬头 |
| 32 | 0.26 | 0.15 | 0.34 | 为凑三角形扭曲权重分布 |
挑选原则很实在:把强度相对误差的容忍上限定在 5%,在这个约束下取聚类误差最小的 λ。上表里对应的是 λ = 2。另一个隐性参数是软阈值温度,用来把连续权重转成可导的软邻接矩阵,取值太小梯度稀疏、太大则所有边权重趋同、聚类失去区分度,一般取总量的千分之一到百分之一。
5. 加权聚类、锚点校准与样本外验证
5.1 用已观测的大额敞口当锚点
纯总量约束下重构出的网络,核心-核心那几条大边的权重往往被系统性低估,因为它们被均摊进了最大熵的外积结构。实务里的做法是把监管报送或成交明细里已观测到的边固定住,不参与 Sinkhorn 投影里的缩放,只在未观测边上分配剩余额度。实现上给已观测边加一个掩码,投影时对已知权重行列做修正:先从未观测边里分配 a_i 减去已知行内敞口后的余量,再把已知边加回去。这一步通常能把核心节点的加权聚类误差再压低三四成,比调 λ 有效得多。
5.2 用系统性风险排序做样本外验证
聚类误差是结构指标,未必等于业务指标。最终验证要落到重构网络的用途上:算一遍 DebtRank 或级联违约的损失分布,看重构网络给出的银行系统重要性排序与真值(或与已知的监管名单)是否一致。常用的度量是 Kendall τ 序相关系数,τ 高于 0.7 基本可用,低于 0.5 说明结构拟合得再好也白搭。这一步还能反过来指导目标聚类向量:如果某些节点的排序总是错,就针对它们单独提高 λ 权重。
| 参数 | 含义 | 建议取值 | 调整依据 |
|---|---|---|---|
| λ | 局部聚类惩罚权重 | 0.5 ~ 5 | 强度误差容忍 5% 前提下的最小值 |
| 软阈值温度 | 权重转邻接的平滑度 | 总量的 1e-3 ~ 1e-2 | 聚类系数的区分度是否还保留 |
| Sinkhorn 迭代数 | 行列和投影强度 | 30 ~ 80 | 行和残差降到 1e-8 即可停 |
| 锚点掩码 | 已观测边固定 | 权重前 10% 的边 | 锚点越准,λ 可以取得越小 |
调参顺序建议固定为:先用锚点把大边钉死,再扫 λ 找到强度误差的拐点,最后用系统性风险排序做交叉验证。顺序反了会陷入反复重调的死循环。
本文还有配套的精品资源,点击获取