图机器学习(15)——链接预测在社交网络分析中的应用

图机器学习(15)——链接预测在社交网络分析中的应用

    • 0. 链接预测
    • 1. 数据处理
    • 2. 基于 node2vec 的链路预测
    • 3. 基于 GraphSAGE 的链接预测
      • 3.1 无特征方法
      • 3.2 引入节点特征
    • 4. 用于链接预测的手工特征
    • 5. 结果对比

0. 链接预测

如今,社交媒体已成为最具价值且丰富多元的信息源之一。每日涌现数十万条新连接、无数用户加入社群、数十亿贴文被分享。这些自发性且非结构化的交互活动,通过图结构得以数字化呈现,从而建立秩序化关联。
在社交图分析中,机器学习能有效解决诸多重要问题。通过合理配置,可从海量数据中提取关键洞察:优化营销策略、识别危险行为用户、预测用户阅读新帖的概率等。
其中,链路预测是该领域最具价值的研究方向之一。根据社交图中连接关系的不同含义,预测未来边可应用于好友推荐、电影推荐或商品购买预测等场景,该任务旨在评估节点间未来建立连接的可能性,可通过多种机器学习算法实现。
接下来,将介绍如何应用监督与无监督图嵌入算法,继续在 SNAP Facebook 社交图上预测潜在连接,并评估节点特征对预测任务的贡献度。

1. 数据处理

为执行链路预测任务,需对数据集进行预处理。该问题将作为监督学习任务处理:算法输入为节点对,目标值为二元标签——若节点在实际网络中相连则标记为"已连接",否则标记为"未连接"。
由于采用监督学习框架,需创建训练集与测试集。我们将生成两个节点数相同但边数不同的子图(通过移除部分边作为算法训练/测试的正样本):

import torch
from torch_geometric.data import Data
from torch_geometric.utils import train_test_split_edges, negative_sampling
from node2vec import Node2Vec
from sklearn.ensemble import RandomForestClassifier
from sklearn import metrics
import numpy as np
import networkx as nx# 转换为 PyG 的 Data 格式用于边分割
edge_index = torch.tensor(list(G.edges)).t().contiguous()
data = Data(edge_index=edge_index, num_nodes=len(G.nodes))# 1. 边分割
# 第一次分割:取出 10% 作为测试边
data_temp = train_test_split_edges(data, test_ratio=0.1, val_ratio=0)
test_pos_edge = data_temp.test_pos_edge_index.t().numpy()  # [num_test_edges, 2]
test_neg_edge = data_temp.test_neg_edge_index.t().numpy()  # [num_test_edges, 2]# 第二次分割:从剩余的 90% 中再取 10% 作为训练边
remaining_edges = data_temp.train_pos_edge_index
n_remaining = remaining_edges.size(1)
n_train = int(0.9 * n_remaining)  # 保留 90% 的剩余边perm = torch.randperm(n_remaining)
train_pos_edge = remaining_edges[:, perm[:n_train]].t().numpy()  # [num_train_edges, 2]
val_pos_edge = remaining_edges[:, perm[n_train:]].t().numpy()   # [num_val_edges, 2]# 生成训练集和验证集的负样本(使用 PyG 的 negative_sampling)
train_neg_edge = negative_sampling(edge_index=remaining_edges[:, perm[:n_train]],num_nodes=data.num_nodes,num_neg_samples=n_train
).t().numpy()val_neg_edge = negative_sampling(edge_index=remaining_edges[:, perm[n_train:]],num_nodes=data.num_nodes,num_neg_samples=remaining_edges.size(1) - n_train
).t().numpy()# 合并样本和标签
samples_train = np.vstack([train_pos_edge, train_neg_edge])
labels_train = np.hstack([np.ones(len(train_pos_edge)), np.zeros(len(train_neg_edge))])samples_test = np.vstack([test_pos_edge, test_neg_edge])
labels_test = np.hstack([np.ones(len(test_pos_edge)), np.zeros(len(test_neg_edge))])

接下来,我们将介绍三种链接预测方法:

  • 基于 node2vec 的无监督嵌入:通过 node2vec 无监督学习训练图的节点嵌入,将生成的嵌入向量作为监督分类算法的输入特征,用于判断节点对是否实际相连
  • 图神经网络 GraphSAGE 的端到端学习:采用基于图神经网络的 GraphSAGE 算法,同步完成节点嵌入学习和分类任务
  • 人工特征工程与节点 ID 结合:从图中提取人工设计的特征,结合节点 ID 作为监督分类器的输入特征

2. 基于 node2vec 的链路预测

(1) 使用 node2vec 从训练图 graph_train 中生成无监督节点嵌入:

node2vec = Node2Vec(G, dimensions=128, walk_length=80, num_walks=10, workers=4)
model = node2vec.fit(window=10, min_count=1, batch_words=4)

(2) 通过 hadamard_embedding 为每对嵌入节点生成联合特征向量,作为分类器输入:

def hadamard_embedding(u, v):return model.wv[str(u)] * model.wv[str(v)]train_embeddings = np.array([hadamard_embedding(u, v) for u, v in samples_train])
test_embeddings = np.array([hadamard_embedding(u, v) for u, v in samples_test])

(3) 采用基于决策树的集成算法随机森林进行分类训练:

rf = RandomForestClassifier(n_estimators=10)
rf.fit(train_embeddings, labels_train)

(4) 应用训练好的模型为测试集生成嵌入向量:

y_pred = rf.predict(test_embeddings)

(5) 使用训练好的模型进行预测并输出评估指标:

print('Precision:', metrics.precision_score(labels_test, y_pred))
print('Recall:', metrics.recall_score(labels_test, y_pred))
print('F1-Score:', metrics.f1_score(labels_test, y_pred))

输出结果如下所示,可以看到基于 node2vec 的嵌入方法能为 Facebook 合并自我网络提供强大的链路预测表征能力:

Precision: 0.971125
Recall: 0.9458242025809593
F1-Score: 0.9583076353768348

3. 基于 GraphSAGE 的链接预测

接下来,我们将使用 GraphSAGE 来学习节点嵌入并进行边分类。我们将构建一个双层 GraphSAGE 架构:该架构接收带标签的节点对作为输入,输出对应的节点嵌入向量。随后,这些嵌入向量将通过一个全连接神经网络进行处理,最终生成链路预测结果。需要注意的是,GraphSAGE 模型和全连接网络串联起来进行端到端训练,使得嵌入学习阶段能够受到预测结果的反向传播影响。

3.1 无特征方法

(1) GraphSAGE 需要节点描述符(特征),这些特征在数据集中可能有,也可能没有。我们首先分析不考虑现有节点特征的情况。此时,常见的做法是为每个节点分配一个长度为 ∣V∣|V|V (图中节点总数)的独热编码特征向量,其中只有对应节点的位置为 1,其余位置为 0

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.data import Data
from torch_geometric.nn import SAGEConv
from torch_geometric.loader import LinkNeighborLoader 
from torch_geometric.utils import negative_sampling
from sklearn.metrics import precision_score, recall_score, f1_score
from torch_geometric.utils import train_test_split_edges, negative_sampling
import numpy as np# 转换为 PyG 的 Data 格式
edge_index = torch.tensor(list(G.edges)).t().contiguous()
num_nodes = G.number_of_nodes()
data = Data(edge_index=edge_index, num_nodes=num_nodes)# 边分割(训练/测试)
# 第一次分割:取出 10% 作为测试边
data_temp = train_test_split_edges(data, test_ratio=0.1, val_ratio=0)
test_pos_edge = data_temp.test_pos_edge_index
test_neg_edge = data_temp.test_neg_edge_index# 第二次分割:从剩余的 90% 中取 10% 作为训练边(即原图的 9%)
remaining_edges = data_temp.train_pos_edge_index
n_remaining = remaining_edges.size(1)
n_train = int(0.9 * n_remaining)  # 保留 90% 的剩余边(即原图的 81%)perm = torch.randperm(n_remaining)
train_pos_edge = remaining_edges[:, perm[:n_train]]
val_pos_edge = remaining_edges[:, perm[n_train:]]# 生成负样本
train_neg_edge = negative_sampling(edge_index=train_pos_edge,num_nodes=num_nodes,num_neg_samples=train_pos_edge.size(1)
)val_neg_edge = negative_sampling(edge_index=val_pos_edge,num_nodes=num_nodes,num_neg_samples=val_pos_edge.size(1)
)# 创建训练图和测试图
graph_train = Data(edge_index=train_pos_edge,num_nodes=num_nodes,x=torch.eye(num_nodes)  # 单位矩阵作为节点特征
)
graph_test = Data(edge_index=test_pos_edge,num_nodes=num_nodes,x=torch.eye(num_nodes)
)
# 准备训练/测试样本和标签
samples_train = torch.cat([train_pos_edge.t(), train_neg_edge.t()], dim=0).numpy()
labels_train = np.concatenate([np.ones(train_pos_edge.size(1)), np.zeros(train_neg_edge.size(1))])samples_test = torch.cat([test_pos_edge.t(), test_neg_edge.t()], dim=0).numpy()
labels_test = np.concatenate([np.ones(test_pos_edge.size(1)), np.zeros(test_neg_edge.size(1))])
num_nodes = graph_train.num_nodes
graph_train.x = torch.eye(num_nodes)  # 使用单位矩阵作为节点特征
graph_test.x = torch.eye(num_nodes)# 创建数据加载器
batch_size = 64
num_neighbors = [4, 4]  # 每层采样的邻居数# 训练数据加载器
train_loader = LinkNeighborLoader(data=graph_train,num_neighbors=num_neighbors,edge_label_index=torch.tensor(samples_train).t(),edge_label=torch.tensor(labels_train),batch_size=batch_size,shuffle=True
)# 测试数据加载器
test_loader = LinkNeighborLoader(data=graph_test,num_neighbors=num_neighbors,edge_label_index=torch.tensor(samples_test).t(),edge_label=torch.tensor(labels_test),batch_size=batch_size,shuffle=False
)

(2) 定义 GraphSAGE 模型,包含两个隐藏层,每个隐藏层的大小为 20,且每个层都有偏置项,并添加了一个 Dropout 层以减少过拟合。然后,GraphSAGE 模块的输出与一个链接分类层连接,该层接收节点嵌入( GraphSAGE 的输出)的对,使用二元运算符(在本节是内积)生成边嵌入,最后将这些嵌入传递通过一个全连接神经网络进行分类:

class GraphSAGE(nn.Module):def __init__(self, in_channels, hidden_channels, out_channels):super().__init__()self.conv1 = SAGEConv(in_channels, hidden_channels)self.conv2 = SAGEConv(hidden_channels, out_channels)self.dropout = nn.Dropout(0.3)def forward(self, x, edge_index):x = self.conv1(x, edge_index)x = F.relu(x)x = self.dropout(x)x = self.conv2(x, edge_index)return xclass LinkPredictor(nn.Module):def __init__(self, in_channels):super().__init__()self.lin = nn.Linear(in_channels * 2, 1)def forward(self, z_src, z_dst):h = torch.cat([z_src, z_dst], dim=-1)return torch.sigmoid(self.lin(h)).squeeze()

(3) 创建模型,使用 Adam 优化器,损失函数使用均方误差:

in_channels = num_nodes  # 输入特征维度
hidden_channels = 20
out_channels = 20encoder = GraphSAGE(in_channels, hidden_channels, out_channels)
predictor = LinkPredictor(out_channels)device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
encoder = encoder.to(device)
predictor = predictor.to(device)optimizer = torch.optim.Adam(encoder.parameters(), lr=1e-3)
criterion = nn.MSELoss()

(4) 定义训练函数,并训练模型 10epoch

# 训练函数
def train():encoder.train()predictor.train()total_loss = 0for batch in train_loader:batch = batch.to(device)optimizer.zero_grad()h = encoder(batch.x, batch.edge_index)pred = predictor(h[batch.edge_label_index[0]], h[batch.edge_label_index[1]])loss = criterion(pred, batch.edge_label.float())loss.backward()optimizer.step()total_loss += loss.item()return total_loss / len(train_loader)# 测试函数
@torch.no_grad()
def test(loader):encoder.eval()predictor.eval()y_pred, y_true = [], []for batch in loader:batch = batch.to(device)h = encoder(batch.x, batch.edge_index)pred = predictor(h[batch.edge_label_index[0]], h[batch.edge_label_index[1]])y_pred.append(pred.cpu())y_true.append(batch.edge_label.cpu())y_pred = torch.cat(y_pred).numpy()y_true = torch.cat(y_true).numpy()y_pred_binary = np.round(y_pred)return {'precision': precision_score(y_true, y_pred_binary),'recall': recall_score(y_true, y_pred_binary),'f1': f1_score(y_true, y_pred_binary)}# 训练循环
epochs = 10
for epoch in range(1, epochs + 1):loss = train()print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}')

输出结果如下所示:

Epoch: 08, Loss: 0.1569
Epoch: 09, Loss: 0.1565
Epoch: 10, Loss: 0.1565

(5) 训练完成后,评估模型性能:

train_metrics = test(train_loader)
test_metrics = test(test_loader)print("\nTraining Metrics:")
print(f'Precision: {train_metrics["precision"]:.4f}')
print(f'Recall: {train_metrics["recall"]:.4f}')
print(f'F1-Score: {train_metrics["f1"]:.4f}')print("\nTest Metrics:")
print(f'Precision: {test_metrics["precision"]:.4f}')
print(f'Recall: {test_metrics["recall"]:.4f}')
print(f'F1-Score: {test_metrics["f1"]:.4f}')

输出结果如下所示:

Training Metrics:
Precision: 0.7633
Recall: 0.8286
F1-Score: 0.7946Test Metrics:
Precision: 0.7404
Recall: 0.8197
F1-Score: 0.7780

可以看到,性能低于 node2vec 方法,但我们还没有考虑真实的节点特征,而这些特征能够提供重要信息,接下来我们引入真实节点特征。

3.2 引入节点特征

(1) 为合并的自我网络提取节点特征的过程较为繁琐,这是由于每个自我网络都通过多个文件以及所有特征名称和值来描述。编写辅助函数来解析所有自我网络以提取节点特征:

  • load_features函数解析每个自我网络并创建两个字典:
    • feature_index,将数字索引映射到特征名称
    • inverted_feature_indexes,将名称映射到数字索引
def load_features():import globfeat_file_name = 'tmp.txt'if not os.path.exists(feat_file_name):feat_index = {}featname_files = glob.iglob("facebook/*.featnames")for featname_file_name in featname_files:featname_file = open(featname_file_name, 'r')for line in featname_file:# example line:# 0 birthday;anonymized feature 376index, name = parse_featname_line(line)feat_index[index] = namefeatname_file.close()keys = feat_index.keys()keys = sorted(keys)out = open(feat_file_name,'w')for key in keys:out.write("%d %s\n" % (key, feat_index[key]))out.close()index_file = open(feat_file_name,'r')for line in index_file:split = line.strip().split(' ')key = int(split[0])val = split[1]feature_index[key] = valindex_file.close()for key in feature_index.keys():val = feature_index[key]inverted_feature_index[val] = key
  • parse_nodes函数接收组合的自我网络 G 和自我节点的 ID。然后,网络中的每个自我节点被分配上通过 load_features 函数加载的相应特征:
def parse_nodes(network, ego_nodes):for node_id in ego_nodes:featname_file = open(f'facebook/{node_id}.featnames','r')feat_file     = open(f'facebook/{node_id}.feat','r')egofeat_file  = open(f'facebook/{node_id}.egofeat','r')edge_file     = open(f'facebook/{node_id}.edges','r')ego_features = [int(x) for x in egofeat_file.readline().split(' ')]network.nodes[node_id]['features'] = np.zeros(len(feature_index))i = 0for line in featname_file:key, val = parse_featname_line(line)if ego_features[i] + 1 > network.nodes[node_id]['features'][key]:network.nodes[node_id]['features'][key] = ego_features[i] + 1i += 1for line in feat_file:featname_file.seek(0)split = [int(x) for x in line.split(' ')]node_id = split[0]features = split[1:]network.nodes[node_id]['features'] = np.zeros(len(feature_index))i = 0for line in featname_file:key, val = parse_featname_line(line)if features[i] + 1 > network.nodes[node_id]['features'][key]:network.nodes[node_id]['features'][key] = features[i] + 1i += 1featname_file.close()feat_file.close()egofeat_file.close()edge_file.close()

(2) 接下来,调用这些函数以加载每个节点在组合自我网络中的特征向量:

load_features()  
parse_nodes(G, ego_nodes)

(3) 通过打印网络中任意节点(如 ID0 的节点)信息来检查结果:

print(G.nodes[0])

输出结果如下所示:

{'features': array([1., 1., 1., ..., 0., 0., 0.])}

可以看到,节点包含一个以 features 为键的字典,其对应值即为分配给该节点的特征向量。

(4) 接下来,按照之前的步骤重复训练 GraphSAGE 模型,使用数据集中节点特征构建训练数据:

# 准备节点特征
node_features = []
node_id_map = {node_id: i for i, node_id in enumerate(G.nodes())}
num_nodes = len(node_id_map)for node_id in G.nodes():node_features.append(G.nodes[node_id]['features'])node_features = np.array(node_features)
x = torch.tensor(node_features, dtype=torch.float)# 准备边索引
edge_index = []
for u, v in G.edges():edge_index.append([node_id_map[u], node_id_map[v]])edge_index.append([node_id_map[v], node_id_map[u]])  # 无向图edge_index = torch.tensor(edge_index, dtype=torch.long).t().contiguous()# 创建初始Data对象
data = Data(x=x, edge_index=edge_index)# 数据集划分(按照您指定的方式)
# 第一次分割:取出10%作为测试边
data_temp = train_test_split_edges(data, test_ratio=0.1, val_ratio=0)
test_pos_edge = data_temp.test_pos_edge_index
test_neg_edge = data_temp.test_neg_edge_index# 第二次分割:从剩余的90%中取10%作为训练边(即原图的9%)
remaining_edges = data_temp.train_pos_edge_index
n_remaining = remaining_edges.size(1)
n_train = int(0.9 * n_remaining)  # 保留90%的剩余边(即原图的81%)perm = torch.randperm(n_remaining)
train_pos_edge = remaining_edges[:, perm[:n_train]]
val_pos_edge = remaining_edges[:, perm[n_train:]]# 生成负样本
train_neg_edge = negative_sampling(edge_index=train_pos_edge,num_nodes=num_nodes,num_neg_samples=train_pos_edge.size(1)
)val_neg_edge = negative_sampling(edge_index=val_pos_edge,num_nodes=num_nodes,num_neg_samples=val_pos_edge.size(1)
)# 创建训练图和测试图
graph_train = Data(x=data.x,edge_index=train_pos_edge,num_nodes=num_nodes
)graph_test = Data(x=data.x,edge_index=test_pos_edge,num_nodes=num_nodes
)# 准备训练/测试样本和标签
samples_train = torch.cat([train_pos_edge.t(), train_neg_edge.t()], dim=0).numpy()
labels_train = np.concatenate([np.ones(train_pos_edge.size(1)), np.zeros(train_neg_edge.size(1))])samples_test = torch.cat([test_pos_edge.t(), test_neg_edge.t()], dim=0).numpy()
labels_test = np.concatenate([np.ones(test_pos_edge.size(1)), np.zeros(test_neg_edge.size(1))])# 创建数据加载器
batch_size = 64
num_neighbors = [4, 4]  # 每层采样的邻居数# 训练数据加载器
train_loader = LinkNeighborLoader(data=graph_train,num_neighbors=num_neighbors,edge_label_index=torch.tensor(samples_train).t(),edge_label=torch.tensor(labels_train),batch_size=batch_size,shuffle=True
)# 测试数据加载器
test_loader = LinkNeighborLoader(data=graph_test,num_neighbors=num_neighbors,edge_label_index=torch.tensor(samples_test).t(),edge_label=torch.tensor(labels_test),batch_size=batch_size,shuffle=False
) 

(5) 最后,创建模型,编译模型,并训练 10epoch

class GraphSAGE(torch.nn.Module):def __init__(self, in_channels, hidden_channels, out_channels):super().__init__()self.conv1 = SAGEConv(in_channels, hidden_channels)self.conv2 = SAGEConv(hidden_channels, out_channels)def forward(self, x, edge_index):x = self.conv1(x, edge_index).relu()x = self.conv2(x, edge_index)return xclass LinkPredictor(torch.nn.Module):def __init__(self, in_channels):super().__init__()self.lin = torch.nn.Linear(in_channels * 2, 1)def forward(self, z_src, z_dst):h = torch.cat([z_src, z_dst], dim=1)return torch.sigmoid(self.lin(h)).squeeze()  # 使用sigmoid将输出限制在[0,1]范围内model = GraphSAGE(data.num_features, 64, 64)
predictor = LinkPredictor(64)device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
predictor = predictor.to(device)
optimizer = torch.optim.Adam(list(model.parameters()) + list(predictor.parameters()), lr=0.001
)# 训练和测试函数
def train():model.train()predictor.train()total_loss = 0for batch in train_loader:batch = batch.to(device)optimizer.zero_grad()h = model(batch.x, batch.edge_index)h_src = h[batch.edge_label_index[0]]h_dst = h[batch.edge_label_index[1]]pred = predictor(h_src, h_dst)# 使用MSE损失loss = torch.nn.functional.mse_loss(pred, batch.edge_label.float())loss.backward()optimizer.step()total_loss += float(loss) * pred.size(0)return total_loss / len(train_loader.dataset)@torch.no_grad()
def test(loader):model.eval()predictor.eval()preds, targets = [], []for batch in loader:batch = batch.to(device)h = model(batch.x, batch.edge_index)h_src = h[batch.edge_label_index[0]]h_dst = h[batch.edge_label_index[1]]pred = predictor(h_src, h_dst)preds.append(pred.cpu().numpy())targets.append(batch.edge_label.cpu().numpy())preds = np.concatenate(preds, axis=0)targets = np.concatenate(targets, axis=0)preds_binary = (preds > 0.5).astype(int)precision = metrics.precision_score(targets, preds_binary)recall = metrics.recall_score(targets, preds_binary)f1 = metrics.f1_score(targets, preds_binary)return precision, recall, f1# 8. 训练循环
for epoch in range(1, 11):loss = train()print(f'Epoch: {epoch:02d}, Loss: {loss:.4f}')train_precision, train_recall, train_f1 = test(train_loader)test_precision, test_recall, test_f1 = test(test_loader)print(f'Train Precision: {train_precision:.4f}, Recall: {train_recall:.4f}, F1: {train_f1:.4f}')print(f'Test Precision: {test_precision:.4f}, Recall: {test_recall:.4f}, F1: {test_f1:.4f}')

输出结果如下所示,可以看到,引入真实的节点特征模型性能发生了显著的改进:

Epoch: 10, Loss: 0.2299
Train Precision: 0.7927, Recall: 0.6746, F1: 0.7289
Test Precision: 0.8746, Recall: 0.3787, F1: 0.5285

最后,我们将评估一种浅层嵌入方法,该方法将使用手工构建的特征来训练监督分类器。

4. 用于链接预测的手工特征

浅层嵌入方法是处理监督任务的一种简单而强大的方法。本质上,对于每条输入边,我们将计算一组指标作为分类器的输入特征。

在本节中,对于每个由节点对 (u,v)(u,v)(u,v) 表示的输入边,将考虑四个指标:

  • 最短路径:uuuvvv 之间的最短路径的长度。如果 uuuvvv 通过一条边直接连接,那么在计算最短路径之前将移除这条边;如果 uuu 不可从 vvv 到达,则使用值 0 表示
  • Jaccard 系数:给定一对节点 (u,v)(u,v)(u,v),定义为u和v的邻居集合的交集与并集的比值。设 s(u)s(u)s(u) 表示节点 uuu 的邻居集合,s(v)s(v)s(v) 表示节点 vvv 的邻居集合,则 Jaccard 系数为:
    j(u,v)=s(u)∩s(v)s(u)∪s(v)j(u,v)=\frac {s(u)\cap s(v)}{s(u)\cup s(v)} j(u,v)=s(u)s(v)s(u)s(v)
  • uuu 中心度:计算节点 uuu 的度中心性
  • vvv 中心度:计算节点 vvv 的度中心性
  • uuu 社区:使用 Louvain 启发式算法分配给节点 uuu 的社区 ID
  • vvv 社区:使用 Louvain 启发式算法分配给节 vvv 的社区 ID

(1) 编写辅助函数,通过 Pythonnetworkx 计算以上指标:

def get_shortest_path(G, u, v):"""返回节点u,v之间的最短路径长度(临时移除直接边后计算)"""removed = Falseif G.has_edge(u, v):removed = TrueG.remove_edge(u, v)  # 临时移除边try:sp = len(nx.shortest_path(G, u, v))except:sp = 0if removed:G.add_edge(u, v)  # 恢复被移除的边return spdef get_hc_features(pyg_data, samples_edges, labels):# 将PyG图转换为NetworkX图G = to_networkx(pyg_data, to_undirected=True)# 预计算指标centralities = nx.degree_centrality(G)parts = community_louvain.best_partition(G)feats = []for (u, v), l in zip(samples_edges, labels):shortest_path = get_shortest_path(G, u, v)j_coefficient = next(nx.jaccard_coefficient(G, ebunch=[(u, v)]))[-1]u_centrality = centralities[u]v_centrality = centralities[v]u_community = parts.get(u)v_community = parts.get(v)# 添加特征向量feats.append([shortest_path, j_coefficient, u_centrality, v_centrality])return np.array(feats)

(2) 接下来,计算训练集和测试集上每条边的特征:

# 转换训练和测试数据
feat_train = get_hc_features(graph_train, samples_train, labels_train)
feat_test = get_hc_features(graph_test, samples_test, labels_test)

(3) 将以上特征将直接作为输入用于随机森林分类器:

# 训练随机森林分类器
rf = RandomForestClassifier(n_estimators=10)
rf.fit(feat_train, labels_train)

(4) 计算模型性能:

# 预测并评估
y_pred = rf.predict(feat_test)
print('Precision:', metrics.precision_score(labels_test, y_pred))
print('Recall:', metrics.recall_score(labels_test, y_pred))
print('F1-Score:', metrics.f1_score(labels_test, y_pred))

输出结果如下所示:

Precision: 0.9636952636282395  
Recall: 0.9777853337866939  
F1-Score: 0.9706891701828411  

5. 结果对比

我们训练了三种算法(含监督/无监督)来学习适用于链接预测的嵌入表示。结果汇总如下:

算法嵌入节点特征准确率召回率F1 分数
node2vec无监督0.970.950.96
GraphSAGE含监督0.740.810.77
GraphSAGE含监督0.870.370.52
浅层方法手工特征0.960.980.9

如上表所示,基于 node2vec 的方法无需监督学习和节点信息就能实现较高预测性能。这种优异表现与联合自我网络的特殊结构有关。由于该网络具有高度子模块化特性(由多个自我网络组成),预测两个用户是否连接很可能与这两个候选节点在网络内部的连接方式密切相关。
例如,可能存在一种系统性规律:当两个用户都连接到同一自我网络中的多个用户时,他们彼此连接的概率也很高。反之,属于不同自我网络或相距甚远的用户则不太可能产生连接,这使得预测任务相对简单。这一推断也得到浅层方法优异表现的佐证。
然而,这种网络特性可能对 GraphSAGE 等复杂算法造成干扰,特别是在引入节点特征时。举例来说,两个兴趣相似的用户本应建立连接,但由于分属不同自我网络(对应自我用户可能身处世界两端),最终并未产生连接。不过,这类算法可能实际上预测的是更长期的连接趋势,由于联合自我网络只是特定时间段的快照,真实网络可能早已演化出不同形态。
机器学习算法的可解释性是领域内最富挑战性的课题。因此,需要深入分析数据集,合理解读结果表现。需要注意的是,本节未对任何算法进行超参数调优,通过适当调整完全可能获得更优结果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/92562.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92562.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

每日一算:华为-批萨分配问题

题目描述"吃货"和"馋嘴"两人到披萨店点了一份铁盘(圆形)披萨,并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。但是粗心的服务员将披萨切成了每块大小都完全不同的奇数块,且肉眼能分辨出大小。由于两人都…

Transfusion,Show-o and Show-o2论文解读

目录 一、Transfusion 1、概述 2、方法 二、Show-o 1、概述 2、方法 3、训练 三、Show-o2 1、概述 2、模型架构 3、训练方法 4、实验 一、Transfusion 1、概述 Transfusion模型应该是Show系列,Emu系列的前传,首次将文本和图像生成统一到单…

聊聊 Flutter 在 iOS 真机 Debug 运行出现 Timed out *** to update 的问题

最近刚好有人在问,他的 Flutter 项目在升级之后出现 Error starting debug session in Xcode: Timed out waiting for CONFIGURATION_BUILD_DIR to update 问题,也就是真机 Debug 时始终运行不了的问题: 其实这已经是一个老问题了&#xff0c…

《R for Data Science (2e)》免费中文翻译 (第1章) --- Data visualization(2)

写在前面 本系列推文为《R for Data Science (2)》的中文翻译版本。所有内容都通过开源免费的方式上传至Github,欢迎大家参与贡献,详细信息见: Books-zh-cn 项目介绍: Books-zh-cn:开源免费的中文书籍社区 r4ds-zh-cn …

【机器学习【9】】评估算法:数据集划分与算法泛化能力评估

文章目录一、 数据集划分:训练集与评估集二、 K 折交叉验证:提升评估可靠性1. 基本原理1.1. K折交叉验证基本原理1.2. 逻辑回归算法与L22. 基于K折交叉验证L2算法三、弃一交叉验证(Leave-One-Out)1、基本原理2、代码实现四、Shuff…

CodeBuddy三大利器:Craft智能体、MCP协议和DeepSeek V3,编程效率提升的秘诀:我的CodeBuddy升级体验之旅(个性化推荐微服务系统)

🌟 嗨,我是Lethehong!🌟 🌍 立志在坚不欲说,成功在久不在速🌍 🚀 欢迎关注:👍点赞⬆️留言收藏🚀 🍀欢迎使用:小智初学计…

Spring Boot 整合 Redis 实现发布/订阅(含ACK机制 - 事件驱动方案)

Spring Boot整合Redis实现发布/订阅&#xff08;含ACK机制&#xff09;全流程一、整体架构二、实现步骤步骤1&#xff1a;添加Maven依赖<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter…

Sklearn 机器学习 线性回归

💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Sklearn 机器学习线性回归实战详解 线性回归是机器学习中最基础也最经典的算法之一,…

AJAX案例合集

案例一&#xff1a;更换网站背景JS核心代码<script>document.querySelector(.bg-ipt).addEventListener(change, e > {//选择图片上传&#xff0c;设置body背景const fd new FormData()fd.append(img, e.target.files[0])axios({url: http://hmajax.itheima.net/api/…

vscode环境下c++的常用快捷键和插件

本文提供一些能够在vscode的环境下&#xff0c;提高c代码书写效率的快捷键&#xff0c;插件以及设置等等。 快捷键ctrlshiftx&#xff1a; 弹出插件菜单ctrlshiftp&#xff1a;弹出命令面板可以快捷执行一些常见命令插件安装这个后&#xff0c;可以按住ctrl跳转到方法的实现&am…

React + ts 中应用 Web Work 中集成 WebSocket

一、Web Work定义useEffect(() > {let webSocketIndex -1const websocketWorker new Worker(new URL(./websocketWorker.worker.ts?worker, import.meta.url),{type: module // 必须声明模块类型});//初始化WEBSOCKET&#xff08;多个服务器选择最快建立连接…

RabbitMQ面试精讲 Day 3:Exchange类型与路由策略详解

【RabbitMQ面试精讲 Day 3】Exchange类型与路由策略详解 文章标签 RabbitMQ,消息队列,Exchange,路由策略,AMQP,面试题,分布式系统 文章简述 本文是"RabbitMQ面试精讲"系列第3天内容&#xff0c;深入解析RabbitMQ的核心组件——Exchange及其路由策略。文章详细剖析…

深入解析Hadoop MapReduce Shuffle过程:从环形缓冲区溢写到Sort与Merge源码

MapReduce与Shuffle过程概述在大数据处理的经典范式MapReduce中&#xff0c;Shuffle过程如同人体血液循环系统般连接着计算框架的各个组件。作为Hadoop最核心的分布式计算模型&#xff0c;MapReduce通过"分而治之"的思想将海量数据处理分解为Map和Reduce两个阶段&…

Kafka MQ 消费者

Kafka MQ 消费者 1 创建消费者 在读取消息之前,需要先创建一个KafkaConsumer对象。创建KafkaConsumer对象与创建KafkaProducer对象非常相似—把想要传给消费者的属性放在Properties对象里。本章后续部分将深入介绍所有的配置属性。为简单起见,这里只提供3个必要的属性:boo…

人工智能——Opencv图像色彩空间转换、灰度实验、图像二值化处理、仿射变化

一、图像色彩空间转换&#xff08;一&#xff09;颜色加法1、直接相加1、直接相加2、调用cv.add()函数进行饱和操作 在OpenCV中进行颜色的加法&#xff0c;我们说图像即数组&#xff0c;所以从数据类型来说我们可以直接用numpy的知识来进行直接相加&#xff0c;但是存在…

【JToken】JToken == null 判断无效的问题

if (innerNode null) {continue; }Debug.Log($"toNode type: {node["toNode"]?.GetType()}");发现这个JToken 无法正确的判断 是否为 null&#xff0c;再排除逻辑问题后&#xff0c;我基本能确定的是 这个对象 不返回的不是真正的C# NULL 输出类型后是 N…

C++基于libmodbus库实现modbus TCP/RTU通信

今天看到了一个参考项目中用到了modbus库&#xff0c;看着使用很是方便&#xff0c;于是记录一下。后面有时间了或者用到了再详细整理。 参考&#xff1a;基于libmodbus库实现modbus TCP/RTU通信-CSDN博客 一、介绍 1.1库文件包含 1.2最简单的使用 本人在QT6.5下&#xff0…

【原创】微信小程序添加TDesign组件

前言 TDesign 是腾讯公司推出的一款UI界面库,至于腾讯的实力嘛,也不用多说了。 官网:https://tdesign.tencent.com/ 源码:https://github.com/Tencent/tdesign 目前处于活跃状态,发文前5日,该库仍在更新中… 遇到的问题 虽然腾讯为微信小程序开发提供了一个讨论的论坛,…

Vue的路由模式的区别和原理

路由模式 Vue 的路由模式指的是 Vue Router 提供的 URL 处理方式&#xff0c;主要有两种&#xff1a;Hash 模式和History 模式。 Hash模式 在 Vue Router 中&#xff0c;默认使用的是 hash 模式&#xff0c;即 mode: hash。如果想要使用 history 模式&#xff0c;可以设置 mode…

通过TPLink路由器进行用户行为审计实战

用户行为审计是指对用户在网络平台上的行为进行监控和记录&#xff0c;以便对其行为进行分析和评估的过程。随着互联网的普及和发展&#xff0c;用户行为审计在网络安全和数据隐私保护方面起到了重要的作用。 用户行为审计可以帮助发现和预防网络安全威助。通过对用户的行为进行…