圖機器學習(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
英文地址,請注明出處:http://en.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;用戶行為審計在網絡安全和數據隱私保護方面起到了重要的作用。 用戶行為審計可以幫助發現和預防網絡安全威助。通過對用戶的行為進行…