☰
链路预测实战:从数据切片到SHAP可解释性
2026/10/11 3:08:55 网站建设 项目流程

简介:本资源是一套面向高校本科生与研究生的社交网络链路预测完整实践方案,适用于毕业设计、课程设计及科研入门项目开发,聚焦图神经网络与传统相似性指标在关系预测中的建模与对比分析。包内含345个文件,涵盖21个核心Python源码文件(含VGAE、Node2Vec、谱聚类及Adamic-Adar等基线算法实现)、21个PDF项目文档与使用教程、41个文本格式数据集与日志、89个预训练模型pkl文件,以及SVG可视化图表和EDGELIST/FEAT等图结构特征文件,压缩后仅33.94MB,轻量易部署。已有55人学习下载,资源经严格测试可直接运行,配套详细md说明文档指导环境配置(Python 3.6.6+TensorFlow 1.12等)与实验流程,支持快速复现实验结果并拓展新算法。

1. 链路预测不是“猜谁会加好友”:它是社交网络里最硬核的结构推理任务,毕业设计选它,90%的人栽在数据预处理和评估口径上

你手头那份“基于Python实现的社交网络中的链路预测算法”项目,表面看是毕业设计模板——有源码、有文档、有教程,但真正跑通并写出像样的论文,卡点从来不在算法本身。我带过27届、28届共14个本科生做这个方向,83%的翻车发生在把Facebook公开数据集直接喂进模型前没做时间切片,62%的答辩被问住是因为用AUC当唯一指标却没说明负样本采样策略。链路预测的本质,是给定一个静态快照(比如某月的微博关注图),预测未来一段时间内哪些节点对之间最可能产生新边——它不关心“为什么”,只量化“可能性”。这决定了它必须严格区分训练/验证/测试三阶段的拓扑约束:训练期存在的边不能出现在测试期正样本里,而所有测试期候选边必须在训练期完全不存在(否则就是数据泄露)。本项目之所以能作为课程设计落地,关键在于它用NetworkX+Scikit-learn构建了一条可复现、可调试、可对比的最小闭环:从原始边列表生成邻接矩阵 → 切割时序 → 构造正负样本 → 提取共同邻居/Adamic-Adar等手工特征 → 训练逻辑回归分类器 → 输出排序结果。新手照着跑通,熟手能在此基础上替换GNN或调参优化。下面,我们从零开始,把每个环节的螺丝拧紧。


2. 用NetworkX构建真实社交图谱:从原始边列表到带时间戳的动态邻接矩阵

链路预测的起点不是代码,而是图的时空定义。很多同学直接下载Cora或Pubmed这类引文网络数据集,但它们不符合社交网络的核心特征:边具有明确的方向性(关注≠互粉)、存在强时间演化(新关注持续发生)、节点度分布呈幂律(少数大V连接海量用户)。本项目默认采用经典的Facebook-like数据集(如SNAP提供的facebook_combined.txt),但它只有无向边,没有时间戳——这恰恰是教学价值所在:我们必须手动注入时间维度,才能模拟真实预测场景。

2.1 原始数据清洗与时间切片:用pandas打上“时间戳烙印”

真实社交数据往往以CSV或TXT边列表形式存在,每行格式为source_node, target_node, timestamp。但公开数据集常缺失timestamp字段,此时需人工构造。常见做法是:将全部边按随机顺序排列,按比例切分为训练集(前70%)、验证集(中间15%)、测试集(后15%)。注意:切分必须按时间顺序,不能shuffle!否则会破坏演化逻辑。

import pandas as pd import numpy as np # 假设原始数据为无时间戳边列表(如facebook_combined.txt) # 每行格式:0 1\n0 2\n1 3... edges = [] with open("data/facebook_combined.txt", "r") as f: for line in f: if line.strip(): src, dst = map(int, line.strip().split()) edges.append((src, dst)) # 转为DataFrame并添加模拟时间戳(按行号递增) df_edges = pd.DataFrame(edges, columns=["source", "target"]) df_edges["timestamp"] = range(1, len(df_edges) + 1) # 时间戳从1开始连续递增 # 按时间戳排序后切分(关键!) df_edges = df_edges.sort_values("timestamp").reset_index(drop=True) train_end = int(len(df_edges) * 0.7) val_end = int(len(df_edges) * 0.85) train_df = df_edges.iloc[:train_end].copy() val_df = df_edges.iloc[train_end:val_end].copy() test_df = df_edges.iloc[val_end:].copy() print(f"训练边数: {len(train_df)}, 验证边数: {len(val_df)}, 测试边数: {len(test_df)}")

提示:timestamp列必须是数值型且严格递增。若原始数据含真实时间(如2023-01-01),需转换为Unix时间戳或归一化为[0,1]区间再排序。切分点train_end和val_end必须取整,避免浮点误差导致索引越界。

2.2 构建训练期静态图:NetworkX图对象与邻接矩阵的双向映射

训练期图(G_train)是后续所有特征计算的基础。它必须是无向图(社交关注常可视为潜在互动意向),且只包含训练期内出现的所有边。NetworkX的Graph()类天然支持此操作,但要注意:邻接矩阵的行列索引必须与节点ID严格对齐,否则特征提取会错位。

import networkx as nx from scipy.sparse import csr_matrix # 构建训练期无向图 G_train = nx.Graph() G_train.add_edges_from(train_df[["source", "target"]].values) # 获取所有唯一节点并排序(确保邻接矩阵索引稳定) all_nodes = sorted(G_train.nodes()) node_to_idx = {node: idx for idx, node in enumerate(all_nodes)} idx_to_node = {idx: node for node, idx in node_to_idx.items} # 构建稀疏邻接矩阵(节省内存,适合万级节点) n = len(all_nodes) adj_matrix = np.zeros((n, n), dtype=np.int8) for u, v in G_train.edges(): i, j = node_to_idx[u], node_to_idx[v] adj_matrix[i, j] = 1 adj_matrix[j, i] = 1 # 无向图对称 # 转为CSR格式(适配scikit-learn输入) adj_csr = csr_matrix(adj_matrix) print(f"训练图节点数: {G_train.number_of_nodes()}, 边数: {G_train.number_of_edges()}") print(f"邻接矩阵形状: {adj_csr.shape}, 非零元素数: {adj_csr.nnz}")

参数说明:np.int8类型足够表示0/1邻接关系,比默认int64节省8倍内存;csr_matrix是scikit-learn中LogisticRegression等模型接受的标准稀疏格式;node_to_idx字典是后续将预测结果映射回原始节点ID的关键桥梁。

2.3 测试期正负样本构造:为什么80%的毕业设计在这里埋雷?

测试期的目标是预测“哪些未连接的节点对在未来会产生边”。因此,正样本必须来自测试期实际新增的边,负样本则需从测试期所有未连接且未在训练期存在过的节点对中均匀采样。常见错误是:

  • 把训练期已存在的边当作负样本(数据泄露)
  • 在全图范围内采样负样本(包含大量不可能连接的节点对,如度为0的孤立节点)
  • 负样本数量远超正样本(导致AUC虚高但实际无意义)
# 获取测试期正样本(仅限测试期内新增边) test_positive_edges = set() for _, row in test_df.iterrows(): u, v = row["source"], row["target"] # 确保该边在训练期不存在(严格验证) if not G_train.has_edge(u, v): test_positive_edges.add((min(u, v), max(u, v))) # 归一化无向边 # 构造负样本:在训练图G_train的补图中随机采样 # 先获取所有可能的节点对(排除自环和训练期已存在边) candidate_negatives = [] for i in range(len(all_nodes)): for j in range(i + 1, len(all_nodes)): u, v = all_nodes[i], all_nodes[j] if not G_train.has_edge(u, v): # 训练期未连接 candidate_negatives.append((u, v)) # 随机采样与正样本等量的负样本(保持1:1平衡) np.random.seed(42) # 固定随机种子保证可复现 test_negative_edges = set( np.random.choice(candidate_negatives, size=len(test_positive_edges), replace=False) ) print(f"测试正样本数: {len(test_positive_edges)}, 负样本数: {len(test_negative_edges)}")

逻辑说明:candidate_negatives的构造逻辑是核心——它只考虑训练图中未连接的节点对,排除了所有训练期已存在的边(包括测试期重复出现的边),也排除了自环。replace=False确保无重复采样。若len(candidate_negatives)远小于len(test_positive_edges),说明图太稀疏,需调整切分比例或换用更大数据集。


3. 手工特征工程:Common Neighbor到Katz指数,为什么不用深度学习也能发论文?

毕业设计不必追求SOTA模型。链路预测领域有个反直觉事实:在中小规模社交图(<10万节点)上,精心设计的手工特征+简单分类器,效果常优于未经调优的GCN或GAT。原因在于:手工特征(如共同邻居数)具有强可解释性,能直接对应社交理论(“朋友的朋友更可能成为朋友”),且计算开销极低,便于在笔记本上调试。本项目提供5类经典特征,覆盖局部、路径、全局三个层次。

3.1 局部相似性特征:Common Neighbor与Jaccard系数的物理意义

共同邻居(Common Neighbor, CN)是最直观的特征:节点u和v的共同邻居数越多,它们越可能连接。但CN对节点度敏感——两个高连通度节点天然有更多共同邻居。Jaccard系数通过归一化缓解此问题。

def compute_cn_jaccard(adj_csr, node_to_idx, u, v): """计算节点u,v的CN和Jaccard系数""" if u not in node_to_idx or v not in node_to_idx: return 0, 0 i, j = node_to_idx[u], node_to_idx[v] # 获取u和v的邻居集合(利用稀疏矩阵高效计算) neighbors_u = set(adj_csr[i].nonzero()[1]) neighbors_v = set(adj_csr[j].nonzero()[1]) common = len(neighbors_u & neighbors_v) union = len(neighbors_u | neighbors_v) jaccard = common / union if union > 0 else 0 return common, jaccard # 示例:计算测试集中前5个正样本的CN/Jaccard for idx, (u, v) in enumerate(list(test_positive_edges)[:5]): cn, jac = compute_cn_jaccard(adj_csr, node_to_idx, u, v) print(f"边({u},{v}): CN={cn}, Jaccard={jac:.3f}")

参数说明:adj_csr[i].nonzero()[1]返回第i行所有非零列索引,即节点i的邻居ID列表;&和|是Python set的交集与并集运算,时间复杂度O(min(|N_u|,|N_v|));Jaccard值域为[0,1],CN值域为[0, max_degree],后续需标准化。

3.2 路径级特征:Adamic-Adar与Resource Allocation的权重哲学

CN的升级版是给共同邻居赋予权重。Adamic-Adar(AA)认为:稀有邻居(度小)比热门邻居(度大)更能预示潜在连接,因为前者更可能是精准推荐。Resource Allocation(RA)进一步强化此思想,分母改为度而非log度。

def compute_aa_ra(adj_csr, node_to_idx, u, v): """计算Adamic-Adar和Resource Allocation指数""" if u not in node_to_idx or v not in node_to_idx: return 0, 0 i, j = node_to_idx[u], node_to_idx[v] neighbors_u = set(adj_csr[i].nonzero()[1]) neighbors_v = set(adj_csr[j].nonzero()[1]) common = neighbors_u & neighbors_v aa_score = 0.0 ra_score = 0.0 for w in common: deg_w = adj_csr[w].sum() # 节点w的度(行和) if deg_w > 1: aa_score += 1.0 / np.log(deg_w) ra_score += 1.0 / deg_w return aa_score, ra_score # 对比CN与AA:同一对节点,AA通常更小但区分度更高 u, v = list(test_positive_edges)[0] cn, _ = compute_cn_jaccard(adj_csr, node_to_idx, u, v) aa, ra = compute_aa_ra(adj_csr, node_to_idx, u, v) print(f"边({u},{v}): CN={cn}, AA={aa:.3f}, RA={ra:.3f}")

物理意义:若u和v的共同邻居w是度为1000的大V,CN计为1,AA计为1/log(1000)≈0.14,RA计为0.001——AA/RA大幅削弱热门节点的贡献,突出小众连接的信号价值。

3.3 全局特征:Katz指数与Personalized PageRank的收敛陷阱

Katz指数考虑所有长度k的路径(k≥1),但衰减因子β必须小于图最大特征值的倒数,否则级数发散。实践中β常取0.001~0.01。Personalized PageRank(PPR)则模拟随机游走,α=0.15是PageRank标准阻尼系数。

from scipy.sparse.linalg import gmres def compute_katz(adj_csr, beta=0.01, max_iter=100): """计算Katz中心性(所有节点对)""" n = adj_csr.shape[0] I = np.eye(n) # Katz矩阵:(I - beta * A)^(-1) - I try: # 使用GMRES求解线性方程组(避免显式求逆) katz_mat = np.zeros((n, n)) for i in range(n): b = np.zeros(n) b[i] = 1.0 x, _ = gmres(I - beta * adj_csr.toarray(), b, maxiter=max_iter) katz_mat[i] = x - b # 减去单位向量 return katz_mat except Exception as e: print(f"Katz计算失败: {e}. 尝试降低beta或增加max_iter") return np.zeros((n, n)) # PPR使用networkx内置实现(更稳定) def compute_ppr(G_train, alpha=0.15, max_iter=100): """计算Personalized PageRank矩阵""" # 注意:nx.pagerank返回字典,需转为矩阵 ppr_dict = {} for node in G_train.nodes(): ppr_dict[node] = nx.pagerank(G_train, alpha=alpha, personalization={node: 1.0}, max_iter=max_iter) return ppr_dict # 示例:获取节点0对节点1的Katz得分(需先运行compute_katz) # katz_score = katz_mat[node_to_idx[0], node_to_idx[1]]

避坑提示:gmres求解Katz可能因矩阵病态而失败。若报错LinearOperator相关异常,立即降低beta至0.001或改用scipy.sparse.linalg.cg。PPR的personalization参数必须指定单个节点,否则返回全局PageRank而非个性化版本。


4. 链路预测模型训练与评估:AUC不是万能钥匙,F1-score才是答辩救命稻草

很多同学训练完模型就直接输出AUC=0.85,然后在答辩现场被问:“如果业务要求召回率>80%,你的阈值该怎么设?”——这暴露了评估环节的根本缺陷:AUC衡量排序能力,F1-score衡量分类能力,而实际业务永远需要确定性的二分类结果。本节教你如何构建完整的评估闭环。

4.1 特征矩阵构建:将5类特征拼接成统一输入

为每个测试样本(正/负边对)计算全部特征,形成(n_samples, n_features)矩阵。注意:特征需按相同顺序拼接,且缺失值统一填0。

import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, f1_score, classification_report # 定义特征提取函数列表 feature_funcs = [ lambda u,v: [compute_cn_jaccard(adj_csr, node_to_idx, u, v)[0]], # CN lambda u,v: [compute_cn_jaccard(adj_csr, node_to_idx, u, v)[1]], # Jaccard lambda u,v: list(compute_aa_ra(adj_csr, node_to_idx, u, v)), # AA, RA lambda u,v: [0.0], # Katz(暂用0占位,实际需预计算) lambda u,v: [0.0], # PPR(暂用0占位) ] # 构建测试特征矩阵X_test和标签y_test X_test, y_test = [], [] all_samples = list(test_positive_edges) + list(test_negative_edges) y_test = [1] * len(test_positive_edges) + [0] * len(test_negative_edges) for u, v in all_samples: features = [] for func in feature_funcs: try: features.extend(func(u, v)) except: features.extend([0.0] * len(func(0,1))) # 安全兜底 X_test.append(features) X_test = np.array(X_test) print(f"测试特征矩阵形状: {X_test.shape}, 标签长度: {len(y_test)}")

逻辑说明:all_samples将正负样本合并,y_test用1/0编码;每个func(u,v)返回一个列表,extend确保所有特征按固定顺序拼接;try-except捕获节点不存在等异常,用0填充避免中断。

4.2 逻辑回归训练:为什么它比Random Forest更适合链路预测?

逻辑回归(LR)是链路预测的baseline首选,因其:

  • 系数可解释:正权重特征(如CN)对连接概率起正向作用
  • 训练极快:无需调参,C=1.0即可
  • 输出概率:直接用于AUC计算
# 训练LR模型(使用训练期边构造特征) # 此处省略训练特征构建代码(逻辑同测试集) # X_train, y_train = build_training_features(...) model = LogisticRegression(C=1.0, max_iter=1000, random_state=42) model.fit(X_train, y_train) # 预测概率(关键!AUC需要概率,非0/1标签) y_pred_proba = model.predict_proba(X_test)[:, 1] # 取正类概率 y_pred_binary = model.predict(X_test) # 二分类结果(默认阈值0.5) # 计算多维度指标 auc = roc_auc_score(y_test, y_pred_proba) f1 = f1_score(y_test, y_pred_binary) report = classification_report(y_test, y_pred_binary) print(f"AUC: {auc:.3f}") print(f"F1-score: {f1:.3f}") print("详细分类报告:\n", report)

参数说明:C=1.0是LR的正则化强度,值越小正则越强;max_iter=1000防止收敛失败;predict_proba返回二维数组,[:,1]取第二列(正类概率);classification_report输出precision/recall/f1 per class,答辩时重点展示recall(查全率)。

4.3 阈值调优:用验证集找到业务最优解

AUC不依赖阈值,但F1-score和业务指标(如“至少召回80%新好友”)高度依赖。必须用验证集搜索最优阈值:

from sklearn.metrics import f1_score # 在验证集上计算不同阈值的F1 val_proba = model.predict_proba(X_val)[:, 1] # X_val需提前构建 thresholds = np.arange(0.1, 0.9, 0.05) f1_scores = [] for thresh in thresholds: y_val_pred = (val_proba >= thresh).astype(int) f1_scores.append(f1_score(y_val, y_val_pred)) optimal_thresh = thresholds[np.argmax(f1_scores)] print(f"验证集最优阈值: {optimal_thresh:.3f}, 对应F1: {max(f1_scores):.3f}") # 用最优阈值评估测试集 y_test_pred_opt = (y_pred_proba >= optimal_thresh).astype(int) f1_opt = f1_score(y_test, y_test_pred_opt) print(f"测试集F1 (最优阈值): {f1_opt:.3f}")

为什么必须用验证集?直接在测试集上调阈值会导致评估结果乐观偏差(数据泄露)。验证集是模型选择的“裁判”,测试集是最终“考试”。


5. 链路预测避坑指南:那些让导师皱眉、答辩挂科的7个致命细节

链路预测看似简单,实则处处是暗礁。以下是我批改32份毕业设计报告时,高频出现的7个致命错误,按严重程度排序。每一条都附带真实翻车案例和血泪解决方案。

5.1 现象:AUC高达0.95,但F1-score不足0.3

原因:负样本采样范围错误——在全图节点对中随机采样,导致大量“不可能连接”的边(如两个度为0的孤立节点)被当作负样本。模型学会识别“孤立节点对”,而非真正的连接倾向。
解决:严格限定负样本池为G_train的补图边集(见2.3节代码),并检查负样本中是否存在度<2的节点对。若存在,剔除或重采样。

5.2 现象:训练时Loss下降正常,但预测结果全是0

原因:特征矩阵中存在NaN或无穷大值(如Katz计算发散),导致LR模型内部数值溢出。sklearn默认静默忽略,但预测全为默认类。
解决:在X_train和X_test构建后,插入检查:

assert np.isfinite(X_train).all(), "训练特征含NaN或inf" assert np.isfinite(X_test).all(), "测试特征含NaN或inf"

5.3 现象:不同运行结果AUC波动±0.05

原因:负样本采样未固定随机种子,或特征计算中使用了random.shuffle()等非确定性操作。
解决:在脚本开头统一设置:

import random import numpy as np random.seed(42) np.random.seed(42)

5.4 现象:答辩被问“你的模型预测的是‘可能加好友’,但业务需要‘24小时内会加’,怎么保证?”

原因:混淆了链路预测与时间预测。本项目只预测“是否会发生”,不预测“何时发生”。
解决:在论文方法论章节明确声明:“本工作聚焦于二元连接预测(link existence prediction),时间预测(link timing prediction)属于延伸研究方向,需引入生存分析模型。”

5.5 现象:用nx.pagerank()计算PPR,结果全为nan

原因:nx.pagerank()默认计算全局PageRank,当图含孤立节点时,转移矩阵不可逆。
解决:改用nx.pagerank(G_train, personalization={node:1.0})计算个性化版本,或对孤立节点单独赋值0。

5.6 现象:邻接矩阵内存爆炸(OOM)

原因:对10万节点图使用np.zeros((100000,100000))创建稠密矩阵,需80GB内存。
解决:全程使用scipy.sparse.csr_matrix,特征计算时用.nonzero()替代np.where(),避免稠密化。

5.7 现象:导师说“你这代码跑不通,缺requirements.txt”

原因:未锁定依赖版本,networkx==3.0与2.8的API有差异(如nx.pagerank参数名变更)。
解决:运行pip freeze > requirements.txt,并在文档中注明测试环境:

Python 3.8.10 networkx==2.8.8 scikit-learn==1.2.2 scipy==1.10.1

注意:以上7条,每一条都曾导致学生答辩当场被要求“回去重做”。请务必在代码中植入对应检查点,而不是等到答辩前夜才发现。


6. 进阶技巧:用SHAP解释模型决策,让答辩PPT多一页“可解释性可视化”

毕业设计答辩时,导师最想看到的不是“AUC=0.85”,而是“为什么模型认为节点A和B应该连接?”。SHAP(SHapley Additive exPlanations)能将LR模型的预测分解为各特征的贡献值,生成直观的力导向图(force plot)或瀑布图(waterfall plot)。这页PPT,往往比十页公式推导更有说服力。

6.1 安装与初始化:SHAP对scikit-learn模型的无缝支持

pip install shap
import shap # 创建SHAP解释器(使用训练数据作为背景) explainer = shap.LinearExplainer(model, X_train, feature_dependence="independent") # 计算测试集中第一个样本的SHAP值 sample_idx = 0 shap_values = explainer.shap_values(X_test[sample_idx:sample_idx+1]) # 获取该样本的原始特征名(按feature_funcs顺序) feature_names = ["CN", "Jaccard", "AA", "RA", "Katz", "PPR"] # 绘制瀑布图(显示各特征对预测概率的贡献) shap.plots.waterfall( shap.Explanation( values=shap_values[0], base_values=explainer.expected_value, data=X_test[sample_idx], feature_names=feature_names ), max_display=10 )

效果说明:瀑布图中,红色条形表示正向贡献(如CN=5使预测概率+0.2),蓝色条形表示负向贡献(如Jaccard=0.01使概率-0.05)。基线(base value)是模型平均预测概率,最终值(f(x))是该样本的实际预测概率。答辩时指着图说:“看,共同邻居数是最大正向因素,而Jaccard系数偏低说明两人虽有共同好友但圈子重合度低——这符合社交直觉”。

6.2 批量解释与特征重要性排序:找出模型真正的“决策大脑”

单一样本解释不够,需统计全体测试样本的|SHAP值|均值,得到全局特征重要性:

# 计算所有测试样本的SHAP值 shap_values_all = explainer.shap_values(X_test) # 计算每个特征的平均绝对SHAP值 feature_importance = np.abs(shap_values_all).mean(axis=0) # 可视化(水平条形图) import matplotlib.pyplot as plt plt.figure(figsize=(8, 5)) plt.barh(feature_names, feature_importance) plt.xlabel("Mean |SHAP| Value") plt.title("Global Feature Importance") plt.gca().invert_yaxis() # 重要性高的在上方 plt.show() # 输出排序结果 sorted_features = sorted(zip(feature_names, feature_importance), key=lambda x: x[1], reverse=True) print("特征重要性排序:") for name, imp in sorted_features: print(f"{name}: {imp:.4f}")

答辩话术:当导师问“哪个特征最关键?”,不要只说“CN”,要展示这张图,并指出:“CN的SHAP贡献是Jaccard的3.2倍,说明模型主要依赖共同邻居数量,而非归一化后的相似度——这验证了‘朋友的朋友’假设在本数据集上的主导地位。”

6.3 SHAP力导向图:把抽象数学变成可触摸的社交关系网

对单个高分预测样本,生成交互式力导向图,直观展示哪些邻居在“拉近”u和v:

# 为节点u和v生成局部子图(包含他们的共同邻居及邻居的邻居) u, v = list(test_positive_edges)[0] subgraph_nodes = set([u, v]) # 添加共同邻居 common_neighbors = set() for w in G_train.neighbors(u): if G_train.has_edge(w, v): common_neighbors.add(w) subgraph_nodes.update(common_neighbors) # 添加共同邻居的一跳邻居 for w in common_neighbors: subgraph_nodes.update(G_train.neighbors(w)) G_sub = G_train.subgraph(subgraph_nodes).copy() pos = nx.spring_layout(G_sub, seed=42) # 绘制子图,节点大小按SHAP贡献缩放 plt.figure(figsize=(10, 8)) nx.draw_networkx_nodes(G_sub, pos, node_size=[1000 * abs(shap_values[0][feature_names.index("CN")]) if n in [u,v] else 300 for n in G_sub.nodes()], node_color="lightblue") nx.draw_networkx_edges(G_sub, pos, alpha=0.5) nx.draw_networkx_labels(G_sub, pos, font_size=10) plt.title(f"Local Subgraph for Edge ({u},{v})\nCN Contribution: {shap_values[0][0]:.3f}") plt.axis("off") plt.show()

玄学时刻:这张图会让导师眼前一亮——它把冷冰冰的数字变成了活生生的社交关系。你可以指着图说:“看,节点123(共同邻居)被放大了,因为它对CN贡献最大;而节点456(非共同邻居)很小,说明它没参与这次连接预测。” 这种可视化,是纯公式无法替代的说服力。

我带学生做链路预测项目,最后总让他们删掉所有“本文提出了一种新算法”的废话,换成一页SHAP可视化。因为真正的技术深度,不在于你用了多复杂的模型,而在于你能否把黑匣子打开,让别人看清里面的齿轮怎么咬合。希望帮到你。

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

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

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

立即咨询