生物医学网络科学Biomedical Network Science
Example11 / 13

节点嵌入——谱嵌入、node2vec 式随机游走嵌入与简单基线的链接预测比较

所属专题
结果呈现与研究前沿
下载
Notebook · .py

从空手道网络中随机隐藏 15% 的边作为测试正例,并抽同样数量的非边作为负例;只在剩余网络上学习。 (1) 拉普拉斯特征映射:对称归一化拉普拉斯最小的非零特征向量作为 d 维坐标; (2) node2vec 式嵌入:二阶有偏随机游走(返回参数 p、进出参数 q),统计窗口内共现, 做正点互信息(PPMI)矩阵并截断 SVD——这与 DeepWalk/skip-gram 的矩阵分解解释相对应(Qiu 等 2018); (3) 基线:共同邻居数、Adamic–Adar 指数。打分用嵌入向量的内积,评价用 AUC。 解读:比较的是 10 次随机划分的均值 ± 标准差;方法之间的差距要和划分间的波动对照着看。 任何嵌入方法都要和共同邻居这类无需训练的基线比较,且测试边不能参与嵌入的学习(否则就是泄漏)。

Output输出图

节点嵌入——谱嵌入、node2vec 式随机游走嵌入与简单基线的链接预测比较:输出图 1

Output文字输出

每次划分:测试正例 11 条、负例 11 条;共 10 次划分
谱嵌入(内积)                      AUC = 0.650 ± 0.126
node2vec 式 PPMI-SVD(内积)      AUC = 0.650 ± 0.085
共同邻居                         AUC = 0.660 ± 0.107
Adamic–Adar                  AUC = 0.697 ± 0.104

Code代码

# 从空手道网络中随机隐藏 15% 的边作为测试正例,并抽同样数量的非边作为负例;只在剩余网络上学习。
# (1) 拉普拉斯特征映射:对称归一化拉普拉斯最小的非零特征向量作为 d 维坐标;
# (2) node2vec 式嵌入:二阶有偏随机游走(返回参数 p、进出参数 q),统计窗口内共现,
#     做正点互信息(PPMI)矩阵并截断 SVD——这与 DeepWalk/skip-gram 的矩阵分解解释相对应(Qiu 等 2018);
# (3) 基线:共同邻居数、Adamic–Adar 指数。打分用嵌入向量的内积,评价用 AUC。
# 解读:比较的是 10 次随机划分的均值 ± 标准差;方法之间的差距要和划分间的波动对照着看。
# 任何嵌入方法都要和共同邻居这类无需训练的基线比较,且测试边不能参与嵌入的学习(否则就是泄漏)。
import numpy as np
import networkx as nx

def auc(pos, neg):
    pos, neg = np.asarray(pos), np.asarray(neg)
    return (np.sum(pos[:, None] > neg[None, :]) + 0.5 * np.sum(pos[:, None] == neg[None, :])) / (len(pos) * len(neg))

def node2vec_walks(G, num_walks, length, p, q, rng):
    nbrs = {v: list(G.neighbors(v)) for v in G}
    walks = []
    for _ in range(num_walks):
        for s in rng.permutation(list(G.nodes())):
            walk = [s]
            while len(walk) < length and nbrs[walk[-1]]:
                cur = walk[-1]
                if len(walk) == 1:
                    walk.append(rng.choice(nbrs[cur])); continue
                prev = walk[-2]
                w = np.array([1 / p if x == prev else (1.0 if G.has_edge(x, prev) else 1 / q) for x in nbrs[cur]])
                walk.append(nbrs[cur][rng.choice(len(w), p=w / w.sum())])
            walks.append(walk)
    return walks

def ppmi_svd_embedding(G, dim, rng, p=1.0, q=1.0, window=3):
    n = G.number_of_nodes()
    C = np.zeros((n, n))
    for walk in node2vec_walks(G, 20, 20, p, q, rng):
        for i, u in enumerate(walk):
            for v in walk[max(0, i - window): i] + walk[i + 1: i + 1 + window]:
                C[u, v] += 1
    total = C.sum(); row = C.sum(1, keepdims=True); col = C.sum(0, keepdims=True)
    with np.errstate(divide="ignore", invalid="ignore"):
        pmi = np.log(C * total / (row * col))
    M = np.where(np.isfinite(pmi) & (pmi > 0), pmi, 0.0)
    U, S, _ = np.linalg.svd(M)
    return U[:, :dim] * np.sqrt(S[:dim])

def spectral_embedding(G, dim):
    L = nx.normalized_laplacian_matrix(G, nodelist=range(G.number_of_nodes())).toarray()
    vals, vecs = np.linalg.eigh(L)
    return vecs[:, 1:dim + 1]

G = nx.karate_club_graph()
edges, non_edges = list(G.edges()), list(nx.non_edges(G))
results = {k: [] for k in ["谱嵌入(内积)", "node2vec 式 PPMI-SVD(内积)", "共同邻居", "Adamic–Adar"]}
for split in range(10):                                   # 10 次随机划分:单次划分的测试边太少,波动很大
    rng = np.random.default_rng(split)
    test_pos = [edges[i] for i in rng.choice(len(edges), int(0.15 * len(edges)), replace=False)]
    test_neg = [non_edges[i] for i in rng.choice(len(non_edges), len(test_pos), replace=False)]
    G_train = G.copy(); G_train.remove_edges_from(test_pos)
    Z_spec = spectral_embedding(G_train, 4)
    Z_n2v = ppmi_svd_embedding(G_train, 8, rng, p=1.0, q=0.5)
    scores = {
        "谱嵌入(内积)": lambda u, v: Z_spec[u] @ Z_spec[v],
        "node2vec 式 PPMI-SVD(内积)": lambda u, v: Z_n2v[u] @ Z_n2v[v],
        "共同邻居": lambda u, v: len(list(nx.common_neighbors(G_train, u, v))),
        "Adamic–Adar": lambda u, v: next(nx.adamic_adar_index(G_train, [(u, v)]))[2],
    }
    for name, f in scores.items():
        results[name].append(auc([f(u, v) for u, v in test_pos], [f(u, v) for u, v in test_neg]))
print(f"每次划分:测试正例 {len(test_pos)} 条、负例 {len(test_neg)} 条;共 10 次划分")
for name, r in results.items():
    print(f"{name:28s} AUC = {np.mean(r):.3f} ± {np.std(r):.3f}")

# 作图:10 次随机划分的 AUC(点)与均值(横线)
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(6.4, 4.2))
en = ["spectral\nembedding", "node2vec-style\nPPMI-SVD", "common\nneighbors", "Adamic-Adar"]
for j, r in enumerate(results.values()):
    ax.plot(np.full(10, j) + np.linspace(-0.15, 0.15, 10), r, "o", alpha=0.6, ms=5)
    ax.hlines(np.mean(r), j - 0.25, j + 0.25, color="k", lw=2)
ax.axhline(0.5, color="gray", ls="--", label="chance")
ax.set_xticks(range(4)); ax.set_xticklabels(en); ax.set_ylabel("link-prediction AUC"); ax.legend(fontsize=8)
ax.set_title("Split-to-split spread is as large as method differences", fontsize=10)
plt.tight_layout(); plt.show()

Run在浏览器中运行

首次加载 Python 环境约需 20–40 秒;可以修改参数后重新运行。

节点嵌入——谱嵌入、node2vec 式随机游走嵌入与简单基线的链接预测比较