用图神经网络进行SEO分析

在一个由互联性主导的数字世界中,网页之间的链接不仅仅是超链接,而是定义站点架构、导航性和信息价值的复杂结构。每个网站都可以表示为一个图:页面是节点,页面之间的链接是边。 分析这个网络不仅允许你理解站点的逻辑结构,还可以优化其在SEO、可用性和转化方面的性能。

图神经网络(GNNs)提供了一种现代且结构化的方法来建模这些关系。

这使它们非常适合分析复杂的网络,其中页面的意义不仅取决于其自身的属性,还取决于它与其他页面的链接方式。

在本文中,我们将展示如何构建一个模拟现实网站结构的合成图。节点代表不同类型的页面(主页、博客、产品、着陆页等),而弧代表放置在页面不同区域(标题、正文、页脚)的内部或外部链接。每个节点和边将被分配反映SEO领域常见属性的属性:例如PageRank、平均停留时间、DOM中的链接位置和锚文本。

目标是训练一个GNN来分类战略性链接,即最大化点击概率或对排名最相关。这个任务可以表述为二元分类(战略性 vs 非战略性)或回归(CTR预测)。

一旦GNN训练完成,我们将使用 GNNExplainer 来解释它的决策。

GNNExplainer 是一个可解释性工具,允许你了解哪些节点属性和子图对分类产生了最大的影响。这在SEO环境中特别有用,因为自动化决策需要以非技术利益相关者能够理解的方式进行解释。例如,我们可以看到哪些链接模式或哪些页面类型和锚文本的组合使链接特别具有战略性。

这种方法的兴趣不仅仅在于理论:任何从事SEO、信息架构、UX或漏斗优化的人将会发现一个模型非常有用,该模型不仅可以预测,还可以解释。网站的结构可以极大地影响用户行为和搜索引擎排名:使用GNN来建模和解释这些动态是迈向更科学和透明SEO的重要一步。

总之,通过阅读本文,您将学到:

  • 如何将网站结构建模为图,并用Python构建带有真实SEO属性的图(例如PageRank、锚文本、DOM中的位置等)
  • 如何训练一个图神经网络(GNN)来分类最大化点击率或改善SEO的战略链接
  • 如何使用GNNExplainer解释模型的决策,使输出即使对非技术人员也易于理解
  • 如何应用GNN来分析和优化网站导航、转化率和信息结构

1、图神经网络简介

图神经网络(GNNs)是一类设计用于处理结构化数据(如图)的深度学习模型。

与传统的神经网络不同,传统神经网络处理向量输入或矩阵(图像、文本、表格),GNN利用图的拓扑结构,结合节点与其邻居的信息。这使它们非常适合分析社交网络、分子系统、知识图谱,以及在我们的情况下,网站结构。

在网站的背景下,每一页可以表示为一个节点,每个超链接作为有向边。诸如页面类型、模拟的PageRank或平均停留时间等属性与节点相关联;其他属性如链接在DOM中的位置或链接类型(内部或外部)可以与弧相关联。GNN允许我们建模这些信息如何在页面之间传播,以及一个节点的行为不仅取决于其自身特征,还取决于链接页面的特征。

GNN的基本原理是邻居聚合。每个节点通过结合相邻节点的信息来更新其表示(“嵌入”)。这个过程重复一定数量的“跳数”(步骤),使每个节点能够获得对网络越来越广泛的视图。

在最流行的架构风格中,我们找到:

  • GCN(图卷积网络):应用度归一化的邻居加权聚合。它简单且稳定,但假设图是无向的且具有同质属性。
  • GAT(图注意力网络):引入注意力机制,在聚合过程中对每个邻居进行不同的加权。它更灵活,适用于具有复杂结构和异构方向的图。
  • 其他更先进的架构包括GraphSAGE、图同构网络(GIN)和异构模型(HeteroGNN),但在本文中,我们将专注于GCN和GAT,因为它们在标准库中更容易获得。

我们将解决的任务可以有两种方式表述:

  • 二元分类:对于每条链接,模型预测它是否是战略性的(标签0/1)。
  • 回归:对于每条链接,模型估计点击率(CTR),提供连续输出。

在两种情况下,GNN学习一个函数,将图的结构和语义特征与每个节点(页面)或边(链接)的预测相关联。输出可以直接用于分类,或用于根据相关性对链接进行排序。

GNN的一个优势是它们在非欧几里得结构和非线性分布下的泛化能力。然而,这种预测能力通常伴随着缺乏透明度:因此需要像GNNExplainer这样的解释工具,我们将在下一节中深入探讨。

2、什么是GNNExplainer?

图神经网络的主要限制之一是其可解释性较差。虽然它们是捕捉复杂结构模式的强大模型,但很难理解为什么GNN会以某种方式对节点或边进行分类。这在实际应用中尤其成问题,因为在这些应用中,向非技术人员的利益相关者、SEO专家或开发人员解释模型的决策至关重要。为了解决这个问题,引入了GNNExplainer,这是一种旨在使GNN的预测透明的方法。

GNNExplainer通过选择一个最小但足够的图信息、子图和节点特征的子集来解释模型的预测。更具体地说,给定一个关于节点或边的预测,算法会识别:

  • 对分类或回归最重要的节点特征
  • 对模型输出贡献最大的局部子图(邻域)

该操作通过微分过程进行优化:GNNExplainer搜索边缘和特征上的最佳掩码,惩罚过于复杂的解决方案,以获得简洁易懂的解释。结果是对图的哪些部分和哪些属性引导了GNN的决策的可视化和数值表示。

在我们的模拟场景中,GNNExplainer可以帮助我们回答以下问题:

  • 什么特征使一个链接成为“战略性的”?
  • 哪些锚文本、DOM中的位置和源/目标页面类型的组合最大可能点击?
  • 哪些内部链接模式最有效?

实际例子:假设GNN将从博客到着陆页的链接分类为“战略性”。GNNExplainer可能会显示:

  • DOM中的位置(例如,标题)权重很高。
  • 锚文本是信息丰富的(例如,“发现优惠”)。
  • 源节点(博客)平均停留时间很高。
  • 目标节点(着陆页)连接到其他高PageRank页面

通过可视化GNNExplainer突出显示的局部子图,我们可以验证是否存在由该预测驱动的相关页面社区。这对于评估GNN是否学会了连贯的模式或过度拟合训练集也很有用。

另一个有用的方面是能够比较战略和非战略链接的解释。这种比较使我们能够识别网络学到的隐式规则,这些规则可以转化为操作指南以优化网站结构。

3、模拟场景

为了研究图神经网络在类似网站结构的上下文中的行为,我们构建了一个现实的合成图

目标是模拟一个拥有数百个页面并由内部和外部链接连接的网站,以便测试GNN的训练及其随后使用GNNExplainer的解释。

3.1 网络生成

使用 NetworkX 的 scale_free_graph 模型(networkx.generators.directed.scale_free_graph(n=500))生成图,这反映了网络的典型特性:度分布遵循幂律(尺度自由定律)。

基本上,少数页面集中了许多链接(中心节点),而大多数页面只有很少的链接。这种模式在真实的爬取数据中经常观察到(例如,维基百科、电子商务网站、博客),比随机图更合理。

初始图是

  • 有向的:链接从一个页面指向另一个页面;
  • 没有自环:我们不考虑指向同一页面的链接;
  • 简化为简单图:两个节点之间的每条弧都是唯一的,即使最初可能存在多重性。

我们得到了大约500个节点和2500到4000条边,具体取决于种子和生成器的特性。

import networkx as nx  
import numpy as np  
import pandas as pd  
from sklearn.preprocessing import StandardScaler  
from scipy.special import expit  # logistic function  
import random  
import matplotlib.pyplot as plt  

# Initial settings  
np.random.seed(42)  
random.seed(42)  
n_nodes = 500  

# === 1. Graph Creation ===  
G_raw = nx.scale_free_graph(n=n_nodes, seed=42)  
G_raw = nx.DiGraph(G_raw)  # grafo orientato  
G_raw.remove_edges_from(nx.selfloop_edges(G_raw))  # remove self-loop  
G = nx.DiGraph()  
G.add_edges_from(set(G_raw.edges()))  # convert to simple graph  

# Visualization of the graph  
plt.figure(figsize=(10, 10))  
pos = nx.spring_layout(G)   
nx.draw(G, pos, with_labels=False, node_size=20, edge_color='gray', alpha=0.6)  
plt.show()

3.2 节点属性

每个节点(页面)被分配了反映SEO和UX关键属性的特征:

  • page_type: 页面类型,从固定集合(home, blog, landing, product, contacts)中选择,非均匀分配(更多“blog”和“product”而不是“home”)。
  • avg_time: 平均页面停留时间,用10到240秒之间的均匀分布模拟。
  • pagerank: 在图上使用PageRank算法计算,然后使用z-score标准化。
  • word_count: 内容中的单词数,用正态分布(μ = 800, σ = 400)分配,截断在100到2000之间。
  • depth: 从指定为主页的节点的距离(从最连接的节点中选择),使用 shortest_path_length 计算。

这些特征用于表示页面在站点内的结构和语义重要性。

# === 2. ADDING ATTRIBUTES TO NODES ===  
page_types = ['home', 'blog', 'landing', 'product', 'contacts']  
page_type_dist = [0.01, 0.4, 0.2, 0.3, 0.09]  # non-unform distribution  
pagerank_dict = nx.pagerank(G)  
pagerank_vals = np.array(list(pagerank_dict.values()))  
pagerank_scaled = StandardScaler().fit_transform(pagerank_vals.reshape(-1, 1)).flatten()  

home_node = max(pagerank_dict, key=pagerank_dict.get)  
depth_dict = nx.single_source_shortest_path_length(G.reverse(), home_node)  # distance from homepage  

for i, node in enumerate(G.nodes()):  
    depth = depth_dict.get(node, None)  
    G.nodes[node]['page_type'] = np.random.choice(page_types, p=page_type_dist)  
    G.nodes[node]['avg_time'] = np.random.uniform(10, 240)  
    G.nodes[node]['pagerank'] = pagerank_scaled[i]  
    G.nodes[node]['word_count'] = int(np.clip(np.random.normal(800, 400), 100, 2000))  
    # Sostituisce np.inf con -1 se il nodo non è raggiungibile  
    G.nodes[node]['depth'] = depth if depth is not None else -1

3.3 弧属性

每条链接(弧)包含影响其可见性和相关性的上下文信息:

  • Link type: indoor or outdoor (simulated with 90% probability indoor, 10% outdoor).
  • dom_position: header, body, or footer, a layout with an unbalanced distribution (header 15%, body 70%, footer 15%).
  • anchor_keyword: A boolean indicating whether the anchor text contains a strategic keyword (e.g., “buy,” “discover,” “offer”).
  • anchor_length: Link text length (1–7 words, skewed distribution).
# === 3. ADDING ARC ATTRIBUTES ===  
for u, v in G.edges():  
    G[u][v]['link_type'] = np.random.choice(['internal', 'external'], p=[0.9, 0.1])  
    G[u][v]['dom_position'] = np.random.choice(['header', 'body', 'footer'], p=[0.15, 0.7, 0.15])  
    G[u][v]['anchor_keyword'] = np.random.choice([0, 1], p=[0.7, 0.3])  
    G[u][v]['anchor_length'] = np.random.randint(1, 8)

3.4 模拟目标

二进制目标 is_strategic 表示链接是否被认为是“战略性的”,意味着对转换或流程相关。它是以下特征的逻辑函数

  • 如果 dom_position 是 "header",则更有可能,
  • 如果 anchor_keyword 为 True,
  • 如果源页面的 pagerank 很高,
  • 如果目标页面的 page_type 是 "landing" 或 "product"

这种方法允许模拟链接属性与其“战略重要性”之间的非线性和合理关系,符合实际SEO优化实践和内部链接设计。

通过这个模拟场景,我们获得了与真实网站结构一致的数据集,足够复杂以测试 GNN 模型和可解释性工具,同时可控以进行分析和可视化。

# === 4. CALCULATION OF TARGET (is_strategic) ===  
def logistic(x): return expit(x)  

edges_data = []  
for u, v, attrs in G.edges(data=True):  
    dom_weight = {'header': 1.0, 'body': 0.5, 'footer': 0.2}[attrs['dom_position']]  
    keyword = attrs['anchor_keyword']  
    pagerank_src = G.nodes[u]['pagerank']  
    page_type_dst = G.nodes[v]['page_type']  
    dst_weight = 1 if page_type_dst in ['landing', 'product'] else 0  

    score = 2.5 * dom_weight + 1.5 * keyword + 1.0 * pagerank_src + 1.0 * dst_weight  
    prob = logistic(score)  
    is_strategic = np.random.binomial(1, prob)  

    edges_data.append({  
        'source': u,  
        'target': v,  
        'is_strategic': is_strategic,  
        **attrs,  
        'pagerank_src': pagerank_src,  
        'page_type_dst': page_type_dst,  
        'dom_weight': dom_weight  
    })  

df_edges = pd.DataFrame(edges_data)  

print("Example of strategic simulated links:")  
print(df_edges.sample(5))  

>>>  

Example of strategic simulated links:  
     source  target  is_strategic link_type dom_position  anchor_keyword  \  
461     172       9             1   internal        body               0     
122     483       0             1   external        body               0     
163      33       1             1   internal        header             0     
746     230     139             1   internal        body               0     
763     237     155             1   internal        header             0     

     anchor_length  pagerank_src page_type_dst  dom_weight    
461              3     -0.163020          blog         0.5    
122              7     -0.163020       product         0.5    
163              3      0.586682       product         1.0    
746              3     -0.163020          blog         0.5    
763              5     -0.163020          blog         1.0

3.5 模型实现

模型实现遵循一个一致且优化的管道,用于对表示网站结构的合成图进行边分类。代码依赖于 PyTorch Geometric 来定义和训练一个图神经网络(GNN),该网络基于结构和语义特征预测链接是否是战略性的。该任务被表述为一个边级别的二元分类

预处理

我们从之前构建的 networkx 图开始。节点属性(例如 pagerank、avg_time、word_count、depth、page_type)被转换为每个节点的数值特征矩阵。分类变量(如 page_type)使用 one-hot 编码进行编码。边使用 edge_index 矩阵表示,这是 PyG 格式的典型表示,二进制目标 is_strategic 与每条边相关联。

为了确保模型的正确评估,边被分层并分为三个集合:训练集(70%)、验证集(15%)和测试集(15%)。

import torch  
import torch.nn.functional as F  
from torch_geometric.data import Data  
from torch_geometric.nn import GCNConv  
from torch.utils.data import DataLoader  
from sklearn.preprocessing import OneHotEncoder  
from sklearn.model_selection import train_test_split  

# === 1. Preprocessing dei nodi ===  
node_df = pd.DataFrame.from_dict(dict(G.nodes(data=True)), orient='index')  
categorical = pd.get_dummies(node_df['page_type'])  
numerical = node_df[['avg_time', 'pagerank', 'word_count', 'depth']]  
X_nodes = torch.tensor(np.hstack([numerical.values, categorical.values]), dtype=torch.float)  

# === 2. Preprocessing degli archi e target ===  
edge_index = torch.tensor(df_edges[['source', 'target']].values.T, dtype=torch.long)  
edge_label = torch.tensor(df_edges['is_strategic'].values, dtype=torch.float)  

# Split archi in train/val/test  
edges_idx = np.arange(edge_index.shape[1])  
train_idx, test_idx = train_test_split(edges_idx, test_size=0.3, stratify=edge_label, random_state=42)  
val_idx, test_idx = train_test_split(test_idx, test_size=0.5, stratify=edge_label[test_idx], random_state=42)  

train_idx = torch.tensor(train_idx, dtype=torch.long)  
val_idx = torch.tensor(val_idx, dtype=torch.long)  
test_idx = torch.tensor(test_idx, dtype=torch.long)  

# === 3. Costruzione oggetto Data ===  
data = Data(x=X_nodes, edge_index=edge_index)

3.6 模型架构

该架构包括两个主要组件:

  1. GCNEncoder:一个两层的 GCNConv,学习每个节点的表示(嵌入),利用图的结构。节点特征通过邻居进行聚合,并通过非线性函数(ReLU)进行变换。
  2. EdgeClassifier:一个多层感知机(MLP),将每个边(u, v)的源节点和目标节点嵌入进行拼接作为输入,并产生该链接是战略性的概率。在评估时,输出通过 sigmoid 进行缩放(损失使用 BCEWithLogitsLoss,因此在训练时不显式应用 sigmoid)。

整个模型是模块化的,使其易于切换到其他架构(例如,将 GCNConv 替换为 GATConv)。

# === 4. Model GCN + MLP for edge classification ===  
class GCNEncoder(torch.nn.Module):  
    def __init__(self, in_channels, hidden_channels):  
        super().__init__()  
        self.conv1 = GCNConv(in_channels, hidden_channels)  
        self.conv2 = GCNConv(hidden_channels, hidden_channels)  

    def forward(self, x, edge_index):  
        x = F.relu(self.conv1(x, edge_index))  
        return self.conv2(x, edge_index)  

class EdgeClassifier(torch.nn.Module):  
    def __init__(self, encoder, hidden_channels):  
        super().__init__()  
        self.encoder = encoder  
        self.classifier = torch.nn.Sequential(  
            torch.nn.Linear(2 * hidden_channels, hidden_channels),  
            torch.nn.ReLU(),  
            torch.nn.Linear(hidden_channels, 1)  
        )  

    def forward(self, x, edge_index, edge_pairs):  
        z = self.encoder(x, edge_index)  
        src, dst = edge_pairs  
        edge_feat = torch.cat([z[src], z[dst]], dim=1)  
        return self.classifier(edge_feat).squeeze()

3.7 训练

训练循环运行100次优化周期,使用Adam,使用二元交叉熵作为损失函数。每10个周期打印以下内容:

  • 训练集和验证集的损失
  • 验证集的分类准确率。

在验证和测试期间,模型使用0.5的阈值对链接进行战略或非战略分类。准确率通过将预测与实际标签进行比较来计算。

 === 4. Model GCN + MLP for edge classification ===  
class GCNEncoder(torch.nn.Module):  
    def __init__(self, in_channels, hidden_channels):  
        super().__init__()  
        self.conv1 = GCNConv(in_channels, hidden_channels)  
        self.conv2 = GCNConv(hidden_channels, hidden_channels)  

    def forward(self, x, edge_index):  
        x = F.relu(self.conv1(x, edge_index))  
        return self.conv2(x, edge_index)  

class EdgeClassifier(torch.nn.Module):  
    def __init__(self, encoder, hidden_channels):  
        super().__init__()  
        self.encoder = encoder  
        self.classifier = torch.nn.Sequential(  
            torch.nn.Linear(2 * hidden_channels, hidden_channels),  
            torch.nn.ReLU(),  
            torch.nn.Linear(hidden_channels, 1)  
        )  

    def forward(self, x, edge_index, edge_pairs):  
        z = self.encoder(x, edge_index)  
        src, dst = edge_pairs  
        edge_feat = torch.cat([z[src], z[dst]], dim=1)  
        return self.classifier(edge_feat).squeeze()  

# === 5. Initialization ===  
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')  
model = EdgeClassifier(GCNEncoder(in_channels=X_nodes.shape[1], hidden_channels=32), hidden_channels=32).to(device)  
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)  
loss_fn = torch.nn.BCEWithLogitsLoss()  

data = data.to(device)  
edge_label = edge_label.to(device)  
train_idx = train_idx.to(device)  
val_idx = val_idx.to(device)  
test_idx = test_idx.to(device)  

# === 6. Training loop ===  
for epoch in range(1, 101):  
    model.train()  
    optimizer.zero_grad()  
    pred = model(data.x, data.edge_index, data.edge_index[:, train_idx])  
    loss = loss_fn(pred, edge_label[train_idx])  
    loss.backward()  
    optimizer.step()  

    model.eval()  
    with torch.no_grad():  
        val_pred = model(data.x, data.edge_index, data.edge_index[:, val_idx])  
        val_loss = loss_fn(val_pred, edge_label[val_idx])  
        val_acc = ((val_pred > 0).float() == edge_label[val_idx]).float().mean()  
    if epoch % 10 == 0:  
        print(f"Epoch {epoch:03d}, Train Loss: {loss.item():.4f}, Val Loss: {val_loss.item():.4f}, Val Acc: {val_acc:.4f}")  

>>>  

Epoch 010, Train Loss: 0.3738, Val Loss: 0.3991, Val Acc: 0.8750  
Epoch 020, Train Loss: 0.3725, Val Loss: 0.3987, Val Acc: 0.8750  
Epoch 030, Train Loss: 0.3720, Val Loss: 0.3985, Val Acc: 0.8750  
Epoch 040, Train Loss: 0.3714, Val Loss: 0.3994, Val Acc: 0.8750  
Epoch 050, Train Loss: 0.3710, Val Loss: 0.4002, Val Acc: 0.8750  
Epoch 060, Train Loss: 0.3706, Val Loss: 0.4013, Val Acc: 0.8750  
Epoch 070, Train Loss: 0.3702, Val Loss: 0.4021, Val Acc: 0.8750  
Epoch 080, Train Loss: 0.3700, Val Loss: 0.4030, Val Acc: 0.8750  
Epoch 090, Train Loss: 0.3697, Val Loss: 0.4037, Val Acc: 0.8750  
Epoch 100, Train Loss: 0.3695, Val Loss: 0.4044, Val Acc: 0.8750

然而,测试集的评估如下所示:

 === 7. Final test ===  
model.eval()  
with torch.no_grad():  
    test_pred = model(data.x, data.edge_index, data.edge_index[:, test_idx])  
    test_acc = ((test_pred > 0).float() == edge_label[test_idx]).float().mean()  
print(f"Test Accuracy: {test_acc:.4f}")  

>>>  
Test Accuracy: 0.8672

4、可视化和结果

在完成对模拟图中战略链接分类的GCN模型训练后,我们可以分析模型的有效性并可视化关键结果。目的是评估GNN在模拟页面间的超链接中学习结构和语义模式的能力。

4.1 模型性能

训练进行了100个周期,优化了二元交叉熵损失(BCEWithLogitsLoss)。训练期间记录的日志显示了一个非线性动态,这通常是受特征归一化和编码敏感的模型的特征。

有趣的结果:

  • 从第20个周期开始,模型达到了稳定的验证准确率87%,表明所学的表示允许有效的分类。
  • 在第40个周期出现暂时的性能下降,可能是由于局部过拟合或小批量分布的变化,之后模型稳定下来。
  • 在第100个周期时,验证损失为0.4005,测试准确率达到85.2%,确认模型在未见过的链接上也能正确泛化。

这些值与用于模拟目标的逻辑函数一致,该函数包括高度信息性的变量:DOM中的位置、锚文本中的关键词、PageRank和落地页类型。GNN仅从结构数据中重建这一规则的事实表明,模型具有良好的学习节点及其连接的有意义表示的能力

4.2 嵌入可视化

为了深入了解模型的行为,我们可以将从节点中学习到的嵌入投影到二维空间,可视化战略与非战略链接。我们使用TSNE进行降维。

from sklearn.manifold import TSNE  
import matplotlib.pyplot as plt  

model.eval()  
with torch.no_grad():  
    z = model.encoder(data.x, data.edge_index).cpu()  

# Extract embedding of the arcs  
edge_src = data.edge_index[0].cpu().numpy()  
edge_dst = data.edge_index[1].cpu().numpy()  
edge_emb = torch.cat([z[edge_src], z[edge_dst]], dim=1).numpy()  
labels = edge_label.cpu().numpy()  

# Dimensional reduction with t-SNE  
tsne = TSNE(n_components=2, random_state=42)  
emb_2d = tsne.fit_transform(edge_emb)  

# Plot  
plt.figure(figsize=(8, 6))  
plt.scatter(emb_2d[labels == 0, 0], emb_2d[labels == 0, 1], c='lightgray', label='Non strategic', alpha=0.5)  
plt.scatter(emb_2d[labels == 1, 0], emb_2d[labels == 1, 1], c='crimson', label='Strategic', alpha=0.6)  
plt.legend()  
plt.title("t-SNE projection of the embedding of links")  
plt.xlabel("Dimension 1")  
plt.ylabel("Dimension 2")  
plt.grid(True)  
plt.tight_layout()  
plt.show()

4.3 使用GNNExplainer进行可解释性

图神经网络(GNN)模型的主要挑战之一是可解释性。因此,我们集成了GNNExplainer,这是一个设计用来识别对模型预测最有影响力的节点、边和特征的子集的算法。在本研究中,我们专注于解释网络中战略链接的分类。

解释为什么模型预测某条弧(两个节点之间的链接)是战略性的,突出最具影响力的节点特征和局部连接。

我们从EdgeClassifier模型中提取了GCN编码器,保留了其权重,以便使用PyTorch Geometric的Explainer API。这是因为解释是相对于图进行的,而不是直接针对完整边分类器的输出。

4.4 选择要解释的链接

我们从原始数据集中随机选择了一条标记为“战略”的链接。这使我们能够专注于模型对目标类别做出积极预测的情况。

我们从原始数据集中随机选择了一条标记为“战略”的链接。这使我们能够专注于模型对目标类别做出积极预测的情况。

import torch  
import torch.nn.functional as F  
from torch_geometric.nn import GCNConv  
from torch_geometric.explain import Explainer, GNNExplainer  
import matplotlib.pyplot as plt  

# === 1. Wrapper compatible with Explainer ===  
class EdgeExplainerWrapper(torch.nn.Module):  
    def __init__(self, model, edge_idx):  
        super().__init__()  
        self.model = model  
        self.edge_idx = edge_idx  # singolo indice dell’arco  

    def forward(self, x, edge_index):  
        edge_pair = edge_index[:, self.edge_idx].view(2, 1)  
        return self.model(x, edge_index, edge_pair)  

# === 2. Prepare  the data ===  
x_cpu = data.x.cpu()  
edge_index_cpu = data.edge_index.cpu()  

# === 3. Select a strategic link to explain===  
strategic_edges = df_edges[df_edges['is_strategic'] == 1].reset_index(drop=True)  
link_idx = strategic_edges.index[0]  # ad es. il primo arco strategico  
source_id = strategic_edges.loc[link_idx, 'source']  
target_id = strategic_edges.loc[link_idx, 'target']  
print(f"Explanation for strategic link: {source_id} → {target_id} (index {link_idx})")  

# === 4. Create the model wrapper ===  
wrapped_model = EdgeExplainerWrapper(model, edge_idx=link_idx)  

# === 5. Define the Explainer ===  
explainer = Explainer(  
    model=wrapped_model,  
    algorithm=GNNExplainer(epochs=100),  
    explanation_type='model',  
    node_mask_type='attributes',  
    edge_mask_type='object',  
    model_config=dict(  
        mode='binary_classification',  
        task_level='edge',  
        return_type='raw'  
    )  
)  

# === 6. Obtain the explanation ===  
explanation = explainer(x=x_cpu, edge_index=edge_index_cpu)  

# === 7. Analyze  masks ===  
edge_mask = explanation.get('edge_mask')  
node_mask = explanation.get('node_mask')  

# === 8. Visualize top-k important arcs ===  
if edge_mask is not None:  
    topk = torch.topk(edge_mask, k=5)  
    print("Top 5 most influential arc:")  
    for idx, score in zip(topk.indices, topk.values):  
        src, tgt = edge_index_cpu[:, idx]  
        print(f"{src.item()} → {tgt.item()} — weight: {score.item():.4f}")  
>>>  

Explanation for strategic link: 58 → 1 (index 0)  
Top 5 most influential arcs:  
411 → 0 — weight: 0.3008  
3 → 1   - weight: 0.2921  
343 → 0 — weight: 0.2892  
130 → 1 — weight: 0.2887  
222 → 0 — weight: 0.2885

这一阶段使您可以获得以下见解:

  • 哪些属性影响链接的分类
  • 哪些局部网络结构在预测中具有更大的权重
import matplotlib.pyplot as plt  
import networkx as nx  
import torch  
import numpy as np  

# === PARAMETER: number of top arcs to visualize ===  
k = 5  # you can modificy it (es. 20, 50, ecc.)  

# === 1. Extract top-k arcs from edge_mask ===  
topk = torch.topk(edge_mask, k=k)  
top_edge_indices = topk.indices.cpu().numpy()  
top_edge_weights = topk.values.cpu().numpy()  

# === 2. Create filtered graph with top-k arcs ===  
G_top = nx.DiGraph()  

for idx, weight in zip(top_edge_indices, top_edge_weights):  
    u = edge_index_cpu[0, idx].item()  
    v = edge_index_cpu[1, idx].item()  
    G_top.add_edge(u, v, weight=weight)  

# === 3. Add weights to the nodes===  
node_mask_np = node_mask.detach().cpu().numpy().flatten()  # <-- fix qui  

for node in G_top.nodes():  
    raw_value = node_mask_np[node]  
    safe_value = float(np.nan_to_num(raw_value, nan=0.0, posinf=0.0, neginf=0.0))  
    G_top.nodes[node]['weight'] = safe_value  

# === 4. Layout and grafic attributes ===  
pos = nx.spring_layout(G_top, seed=42)  
nodelist = list(G_top.nodes())  
node_sizes = [G_top.nodes[n].get('weight', 0.0) * 1000 for n in nodelist]  

edgelist = list(G_top.edges())  
edge_widths = [G_top[u][v]['weight'] * 4 for u, v in edgelist]  
edge_colors = ['red' if (u == source_id and v == target_id) else 'gray' for u, v in edgelist]  

# === 5. Visualization ===  
plt.figure(figsize=(10, 7))  
nx.draw_networkx_nodes(G_top, pos, nodelist=nodelist, node_size=node_sizes, alpha=0.9)  
nx.draw_networkx_edges(G_top, pos, edgelist=edgelist, width=edge_widths, edge_color=edge_colors, alpha=0.7)  
nx.draw_networkx_labels(G_top, pos, font_size=8)  
plt.title(f"GNNExplainer — Top-{k} influential arcs")  
plt.axis("off")  
plt.tight_layout()  
plt.show()

5、优点和局限性

使用图神经网络进行缺失链接分类带来了许多优势,无论是从预测还是解释的角度来看。

  • 强大的结构模型
    GNN特别适合捕捉关系数据的拓扑结构。在我们的案例中,模型能够学习潜在的节点表示,这些表示不仅包含个体特征,还包含局部结构上下文,如邻居的存在、连接和交互模式。这使我们能够克服基于独立表格特征的模型的局限性,从而做出更明智和一致的推断,与图架构一致。
  • 考虑链接之间的关系 与局部基于规则的方法(例如,节点相似性或共现)不同,我们的方法明确考虑了边之间的依赖关系。这对于观察到的关系与系统机制相关的情况(例如组织约束、层次结构或隐含的信息流)至关重要。此外,图结构允许建模间接边或关系链,这些在还原主义观点下不会出现。
  • 解释支持
    集成GNNExplainer代表了迈向可解释性的重要一步。能够识别预测的相关特征和子图,允许验证结果并识别任何偏差或意外模式。这在决策上下文中特别有用,因为模型的透明度可以使可用输出与不透明输出区分开来。

尽管有其优势,这种方法也存在一些操作和概念上的挑战。

  • 计算复杂性
    训练GNN并使用像GNNExplainer这样的解释方法需要大量的计算资源。随着节点、边和特征的数量增加,复杂性迅速增长。此外,许多操作(如批次构建或图归一化)需要特别注意,以确保效率和数值稳定性。
  • 有限的可扩展性 虽然该模型适用于中等大小的图,但直接应用于非常大的网络(例如国家或国际网站图)可能成本过高。压缩、抽样或分区策略是必要的,以使它适用于大规模的实际场景。然而,这些技术可能会引入扭曲或结构信息的丢失。
  • 概念过拟合的风险(合成数据集) 使用模拟数据集的优势在于控制和内部验证,但它限制了泛化性。模型可能会学习生成过程的具体模式,而不是真实动态。这引入了概念过拟合的风险,即过度适应于一个“理想”结构,这可能无法反映观察到的现场数据的复杂性。因此,必须在经验或半合成数据集上验证模型,这些数据集再现了现实的约束和噪声。

6、结束语

本研究展示了使用图神经网络(GNNs)预测复杂网络中缺失战略链接的可行性和有效性。开发的流程集成了:

  • 生成具有类似真实组织网络特征的合成数据集;
  • GCN 基础模型的监督训练;预测性能的评估;
  • 使用 GNNExplainer 模块分析特征和连接的重要性。

获得的结果表明,基于 GNN 的模型可以在正确分类缺失边方面达到高精度(约 85%),区分战略边和噪声边。此外,使用解释方法允许识别模型决策的相关子图和局部属性,增加了预测过程的透明度。


原文链接:SEO Analysis with Graph Neural Network: model the structure of a website as a graph

汇智网翻译整理,转载请标明出处