链路预测与相似性指标:基于MATLAB的AUC评估实战 简介这是一套基于MATLAB的社交网络链路预测程序包主要面向数据挖掘、网络科学及复杂网络分析方向的研究生、科研人员和MATLAB开发者。压缩包共含37个文件包括34个.m源码脚本、2个.asv自动缓存文件和1个txt说明整体大小仅22KB结构紧凑便于直接阅读和二次开发。源码实现了共同邻居、Jaccard系数、Adamic-Adar指数、资源分配、局部路径、Katz等经典算法并配有训练/测试集划分与AUC评估函数可量化评价预测结果。借助程序附带的示例数据用户能快速跑通完整流程进而应用于好友推荐、社区发现、网络演化分析等场景。已有2420人浏览学习适合作为课程设计、毕业论文或科研入门阶段的参考实现有助于理解链路预测原理并掌握MATLAB算法设计思路。1. 链路预测社交网络里“看似无关的人其实只差一次AUC”在社交网络分析中链路预测要回答的问题很具体给定一张静态网络快照哪些尚未连边的节点对最可能产生新连边这听起来像推荐系统但它的核心不是用户行为日志而是网络结构相似性。做推荐、风控或生物网络研究的人都知道多数真实网络是稀疏的观测到的边只是真实关系的一部分。这个名为 Code.rar 的 MATLAB 压缩包恰好把链路预测里最常用的十几条基线指标从 CN 到 SimRank 一次性实现了并附带划分训练/测试集与 AUC 评估脚本。对于刚接触复杂网络的研究生它是最快的“跑通基线”的方案对于已经做过网络分析的人它的意义在于可以用同一份邻接矩阵横向对比多种指标省去重复造轮子的时间。2. 相似性指标谱系从 CN 到 SimRank哪些值得放进 Matlab 工具箱链路预测算法虽多但大部分都能归到“给每个未连边节点对打分”这个框架里。分数越高预测存在连边的概率越大。判断一个节点对有多“像”常见做法是分成三类局部指标只看一阶或二阶邻域拟局指标利用有限长度路径全局指标则考虑整个网络的拓扑信息。这个包里同时出现了 CN、AA、RA、PA、Jaccard、Salton、HPI、HDI、LHN、LHNII、ACT、SimRank、Katz、RWR、LRW、SRW、TSRWR基本覆盖了这三类。2.1 局部相似性Common Neighbors 与它的加权变体局部指标是计算代价最低的一类。Common NeighborsCN直接统计两个节点的共同邻居数数学上就是邻接矩阵二次幂的非对角元。Adamic-AdarAA在 CN 基础上对每个共同邻居取度数的对数倒数目的是降低高连接度“中心节点”的权重——因为一个节点连接了全网一半的人它的共同邻居身份参考价值有限。Resource AllocationRA比 AA 更激进直接把 log 去掉用度数的倒数。PAPreferential Attachment则相反认为新连边概率与两端度的乘积成正比适合幂律分布明显的社交网络。实际项目里一般把 AA 和 RA 当作局部基线它们对稠密社区比较敏感。下面是一段可以直接在 MATLAB 命令行运行的 CN 计算利用了稀疏矩阵乘法function S cn_score(A) % A: n x n 邻接矩阵无向无权 n size(A, 1); S A * A; % S(i,j) 是 i 和 j 的公共邻居数 S S - diag(diag(S)); % 去掉对角线上的自身度数 end这段代码里A*A本质上是把二阶路径数量累加diag(diag(S))取出对角线并构造对角矩阵减掉。因为A是对称矩阵所以S也是对称的如果输入的A是有向图这里算出的就不再是共同邻居而是“共同出度目标”要注意区分。局部指标的计算量主要花在这两次矩阵乘法上稀疏矩阵下规模到十万节点也不太吃力。2.2 拟局与全局指标路径、随机游走与 SimRank 的代价当网络直径较大或者局部邻居信息稀疏时局部指标会失效。此时可以往拟局指标走。Local PathLP在 CN 的基础上叠加长度为 3 的路径数包里的 LocalPath.m 就是在做这件事通常还会给一阶项和二阶项分别加权。Katz 则把任意长度路径按照长度指数衰减求和衰减因子beta必须小于邻接矩阵最大特征值的倒数否则级数发散。另一个被频繁使用的是 SimRank如果两个节点的邻居相似那么它们自己也相似本质上是“递归的同构性度量”。SimRank 的计算通常需要迭代到收敛复杂度较高但结果对结构相似的节点对非常鲁棒。全局指标里还有一类基于随机游走的RWR 计算从源节点出发的随机游走回到每个节点的概率分布LRW 是有限步随机游走SRW 是平均随机游走。这个包里的 RWR.m、LRW.m、SRW.m、TSRWR.m 都在这个框架下。全局方法的共同特点是“贵”对 n 个节点做一次完整预测往往需要 n 次矩阵迭代或线性方程求解实际网络中常用来和局部指标做融合。2.3 指标选型数据稀疏度与计算规模怎么权衡选择哪一类指标首先看网络规模。百万节点级别的社交网络里全局指标几乎跑不动SimRank 更是灾难此时 CN/AA/RA/PA 是唯一现实选择。网络规模在万节点以下、且对精度敏感时Katz 和 RWR 能把局部指标落后的一截补回来。还要看任务目标如果是预测缺失边局部指标在稠密网络上表现不错如果是预测未来新增边PA 往往因为长尾效应而占优。下面的表把包里的常见指标按类别和复杂度整理了一下方便跑试验前快速定位指标类别核心思想计算复杂度参考CN, AA, RA, Salton, Jaccard, HDI, HPI, LHN局部共同邻居及邻居度加权O(m) 到 O(n^2)LocalPath, Katz, ACT拟局有限路径数/全路径衰减矩阵乘幂或伪逆SimRank, RWR, LRW, SRW全局递归相似度/随机游走多次迭代收敛慢实际工作中我会先跑局部指标把 AUC 数据拿到手再根据网络规模决定要不要花时间跑全局指标。这套代码包的价值就在于此它把这么多指标放在同一个数据结构上切换成本很低。3. Code.rar 代码解剖Main.m、DivideNet.m 与 CalcAUC.m 的分工压缩包里 31 个 m 文件看起来很多但核心流程只有三步把边表变成邻接矩阵划分训练/测试集然后对每个指标算分并评估。熟悉了这个数据流后面替换自己的网络数据就很容易。3.1 文件结构与数据流先看仓库里的文件。下表列出的是会直接调用的关键脚本文件角色Main.m主流程加载数据、调指标、输出结果test.m / test1.m调试与单元级别的测试脚本FormNet.m将边列表或邻接列表转换为稀疏邻接矩阵DivideNet.m把完整邻接矩阵按比例拆成训练集和测试集CalcAUC.m在测试集上计算 AUCCN.m / AA.m / RA.m / PA.m / Katz.m / RWR.m ...各相似性指标实现Copy_of_Main.m / Main.asv备份与自动保存版本从命名可以看出Main.m 是入口。asv文件是 MATLAB 编辑器自动备份和主脚本内容基本一致可以直接忽略。Copy_of_Main.m看起来是想保留一份对比实验主程序类似项目里这么干很常见跑参数扫描时只要复制主脚本再改一行参数避免破坏稳定版本。数据流是这样的nettest.txt原始边表 →FormNet.m生成邻接矩阵 A →DivideNet.m把 A 的边拆成训练集 A_train 和测试集 A_test → 各个指标函数在 A_train 上计算分数矩阵 S →CalcAUC.m用 S 结合 A_train 和 A_test 计算 AUC。3.2 核心函数解读FormNet.m负责构造邻接矩阵。常见的传入参数有两种一种是 n×2 的边列表另一种是 n×n 的邻接矩阵。边列表到矩阵的转换在 MATLAB 里可以用sparse一行实现function A FormNet(edges, n) % edges: m x 2 的边列表节点编号从 1 开始 % n: 节点数 A sparse(edges(:,1), edges(:,2), 1, n, n); A double(A | A); % 转置后取并集保证无向对称 end如果输入的边列表带有重边sparse会自动累加权重。A | A把有向的邻居关系变成无向对称这在社交网络里是默认操作你要是处理有向网络这行要去掉。MATLAB 的稀疏矩阵在后续矩阵乘法中会自动调用稀疏算法这也是整个包能跑大图的基础。DivideNet.m做的是边级划分。很多初学者会把训练/测试集按节点划分这在链路预测里是错的测试边必须从完整边集中抽出来但节点集合要保持不变否则会泄漏节点属性。一个可靠的划分方式是先记录所有非对角边的下标随机打乱后按比例取前 80% 作为训练集后 20% 作为测试集function [A_train, A_test] DivideNet(A, ratio, seed) % ratio: 训练集占比默认 0.8 rng(seed, twister); n size(A, 1); [I, J] find(triu(A, 1)); % 只取上三角避免重复 m length(I); perm randperm(m); train_idx perm(1:round(ratio*m)); test_idx perm(round(ratio*m)1:end); A_train sparse(n, n); A_test sparse(n, n); for k train_idx A_train(I(k), J(k)) 1; A_train(J(k), I(k)) 1; end for k test_idx A_test(I(k), J(k)) 1; A_test(J(k), I(k)) 1; end end这里故意用triu(A, 1)取出上三角元素避免每条边被处理两次。rng(seed)让随机划分可复现跑实验时应该固定seed否则 AUC 的波动可能不是算法差异而是数据划分差异。CalcAUC.m是评估模块。AUC 统计的是从测试边里随机抽一条边它的得分高于从不存在边里随机抽一条边的概率。常见做法是采样估计也可以用全量排序。小网络可用后一种大网络用采样function auc CalcAUC(score, A_train, A_test, n_samples) % score: n x n 得分矩阵 % 未连边样本从 A_train A_test 的补集中抽样 if nargin 4 n_samples 100000; end n size(score, 1); [I, J] find(triu(A_test, 1)); pos_pairs sub2ind([n n], I, J); [I, J] find(triu(A_train A_test, 1) 0); neg_pairs sub2ind([n n], I, J); pidx randi(length(pos_pairs), n_samples, 1); nidx randi(length(neg_pairs), n_samples, 1); spos score(pos_pairs(pidx)); sneg score(neg_pairs(nidx)); auc (sum(spos sneg) 0.5 * sum(spos sneg)) / n_samples; endsub2ind把行列下标转成线性索引避免在二维矩阵上反复取值。triu(A_train A_test, 1) 0选中上三角中不是原始边的位置A_train A_test正好补回完整边集。注意这里score要对称否则triu取到的上三角分数和实际预测方向不对应。3.3 测试脚本与备份文件test.m和test1.m的区别不大通常前者用内置小图验证函数正确性后者跑真实边表。Copy_of_Main.m这个备份名提醒我们修改主流程前先复制一份避免把验证过的脚本改坏。不少同学直接改 Main.m改到一半发现 AUC 计算有问题想回退却找不到原始版本只能从 asv 里恢复。如果你打算在这个包上做二次开发第一件事就是把 Main.m 复制成 Main_backup.m然后基于 Copy_of_Main.m 改。这不算什么高深技巧但能省掉很多不必要的错误定位时间。4. 跑通一次链路预测从 nettest.txt 到 AUC下面以nettest.txt为例给出一套可以直接执行的流程。实际数据不一定是这个格式但下面的步骤可以照搬到自己的边表上。4.1 准备输入数据与邻接矩阵包里的nettest.txt一般是每行一条边的简表格式类似1 2 1 3 2 4 3 4第一列是边的一端第二列是另一端。节点编号必须是从 1 开始的连续整数如果不是先重映射。读取时可以这样写edges load(nettest.txt); n max(max(edges)); A FormNet(edges, n);如果文件有表头load会失败改用readmatrix(nettest.txt, NumHeaderLines, 1)。建好A后建议先检查规模与对称性fprintf(节点数%d边数%d\n, n, nnz(A)/2); assert(issymmetric(A), 邻接矩阵不对称);nnz(A)/2是因为无向矩阵中每条边存了两次。这一步能过滤掉大部分格式错误。注意如果nettest.txt是带权重的边列表FormNet里的sparse第三个参数需要改成权重列否则会丢失权重信息。4.2 划分训练集和测试集默认用 80% 边训练20% 边测试rng(42); ratio 0.8; [A_train, A_test] DivideNet(A, ratio, 42);这里有两个关键点。第一ratio不能太极端否则测试集样本太少AUC 置信区间会很宽。第二固定随机种子很关键。为了快速比较多种指标建议把“划分数据”和“跑指标”分成两个阶段避免每次跑指标都重新打乱训练集。如果包里的DivideNet不允许传入种子可以像上一节那样自行封装。4.3 计算多指标得分得到A_train后所有指标函数接口基本一致S_cn CN(A_train); S_aa AA(A_train); S_ra RA(A_train); S_pa PA(A_train); S_katz Katz(A_train, 0.01); % beta 参数见下文这些函数返回的S是 n×n 矩阵S(i,j)是模型预测 i 到 j 存在连边的得分。对于无向网络S应当对称如果某个指标函数返回的结果不对称检查它内部有没有(SS)/2的对称化步骤。Katz.m的beta不能随便填。beta要小于邻接矩阵最大特征值的倒数否则计算(I - beta*A)^(-1)会发散。处理时可以用max(abs(eigs(A_train, 1, largestabs)))探测谱半径保守取谱半径倒数的 0.5 倍。RWR 的restart概率一般在 0.7 到 0.9 之间越小越偏向全局传播越大越集中在局部邻居。指标关键参数推荐范围Katzbeta小于谱半径倒数建议取其 0.5 倍RWR / TSRWRrestart0.7 ~ 0.9LRW / SRW步长3 ~ 5LocalPath路径长度权重长度 3 项权重取 0.01 量级运行完所有指标后得分矩阵会占不少内存建议及时把中间结果写盘save(scores.mat, S_cn, S_aa, S_ra, S_pa, S_katz);4.4 用 AUC 比较预测精度打分只是中间产物最终都要用 AUC 衡量。对于小网络可以直接做全量 AUC也可以用采样方式auc_cn CalcAUC(S_cn, A_train, A_test, 100000); fprintf(CN AUC %.4f\n, auc_cn);如果CalcAUC内部是采样实现建议把样本数至少设到 100000AUC 的标准误大约在 sqrt(0.25/n_samples) 级别十万样本对应约 0.0016 的误差足够判断指标之间的差距。测试边数量特别少时采样会不稳定这种时候直接用秩和检验计算等效 AUCMATLAB 的ranksum可以做pos S_cn(find(A_test 1)); [I_neg, J_neg] find(A_train A_test 0); neg S_cn(sub2ind(size(S_cn), I_neg, J_neg)); % 对 pos 和 neg 做逐对比较即可得到与 CalcAUC 等价的 AUC注意A_train A_test 0找的是正样本补集也就是原始网络中不存在的边。如果网络有向这里要分别处理每个方向的非边。5. 进阶技巧多指标投票、随机游走参数调节与验证陷阱拿到基本 AUC 后下一步是让结果更可信。5.1 多指标融合与秩平均社交网络里不同指标擅长捕获不同微观结构CN 擅长三角形闭包PA 擅长捕捉 hub 节点的吸引力Katz 能利用长路径。与其选一个指标不如把几个指标的排序结果做秩平均。做法是先将每个指标得分矩阵转成排名然后逐元素平均。一段可用的 MATLAB 代码function S_ens rank_ensemble(S_list) % S_list: cell 数组每个元素是一个 n x n 得分矩阵 n size(S_list{1}, 1); S_ens zeros(n); for k 1:length(S_list) flat S_list{k}(:); % 展成列向量 r tiedrank(flat); % 全局秩处理并列 S_ens S_ens reshape(r, n, n); % 还原成矩阵 end S_ens S_ens / length(S_list); end秩平均能减少不同指标量纲差异带来的影响避免某个指标分数天然数值大而主导投票。测试集合规模较小时直接秩平均比加权回归更稳方差小。5.2 随机游走参数重跑的约定RWR 的 restart 概率、Katz 的 beta、LRW 的步长都是关键参数。我的习惯是在文件名里带上参数比如Katz_beta_0.01_AUC_0.7234.mat而不是直接覆盖 scores.mat。这样调参时可以回溯“之前那个 0.72 是哪组参数跑出来的”而不是靠记忆。每次改参数都要固定相同的 train/test 划分否则换了数据划分AUC 差别可能大于参数影响。5.3 验证 AUC 是否被高估的简单办法链路预测最常见的错误是测试边在训练阶段泄漏给了模型。例如A_train与A_test的并集如果参与了指标计算AUC 会虚高。验证方法是把A_test里的边随机隐藏一批重新计算 AUC如果 AUC 波动很大说明模型对个别边过度敏感。另一个方法是构造空模型把A做度保持的随机重连在随机网络上重跑同样的流程。随机网络上的 AUC 应该接近 0.5如果明显高于 0.5就要检查指标函数里是否无意识地访问了A_test的信息或者代码里是否把测试边也放进了邻接矩阵。本文还有配套的精品资源点击获取