PyTorch图神经网络实践(五)链路预测

链路预测是网络科学里面的一个经典任务,其目的是利用当前已获取的网络数据(包含结构信息和属性信息)来预测网络中会出现哪些新的连边。

本文计划利用networkx包中的网络来进行链路预测,因为目前PyTorch Geometric包中封装的网络还不够多,而很多网络方便用networkx包生成或者处理。

环境配置

首先,安装一个工具包,DeepSNAP。这个包提供了networkx到PyTorch Geometric的接口,可以方便地将networkx中的网络转换成PyTorch Geometric所要求的数据格式。DeepSNAP有两种安装方法:

第一种安装方法

$ pip install deepsnap

第二种安装方法

$ git clone https://github.com/snap-stanford/deepsnap
$ cd deepsnap
$ pip install .

在我服务器上第一种方法报错,使用第二种方法成功了。

其他环境配置参考我之前的系列博文。

链路预测

使用图神经网络进行链路预测包含以下基本步骤:

  1. 导入图数据
  2. 分割数据集(划分训练边、测试边)
  3. 标注正边、采样负边
  4. 训练神经网络
  5. 测试模型效果

链路预测最开始是一个无监督学习任务,即根据已经看到的网络结构(或者其他属性信息)来推断未知连边是否存在,但是这样的话就比较难以验证。只有在动态网络(或称时序网络)中才会有这样的数据以供实验验证,可以用前一段时间的网络结构来预测后一段时间的网络结构。然而,很多网络没有时间信息,在这样的网络中如何验证呢?

后来,学者提出了用有监督的方式来进行链路预测,也就是将其视为二分类任务,将网络中存在的边都视为正样本(即正边),不存在的连边都当作负样本(即负边)。然后,将这些边分为两部分,一部分为训练集,一部分为测试集。训练集和测试集中都包含正边和负边,目的是在训练集上训练出一个模型能够准确分类这两种边,然后再在测试集上验证效果。

然而,大多数网络都是稀疏的,也就是说存在边的数量差不多是节点数量的几倍左右,而网络中不存在的边的数量差不多是节点数量的平方(在无向网络中,不存在边的数量等于 ( n − 1 ) n / 2 − m (n-1)n/2-m (n1)n/2m,其中 n n n为节点数, m m m为边数)。这样不存边的数量就远远大于存在边的数量,在有监督学习中就意味着负样本远大于正样本,类别极其不平衡。怎么解决这个问题呢?大家很自然地想到了负采样,就是每次训练的时候随机抽取与正样本等比例的负样本,这样就避免了类别不平衡。

训练结束后,就可以用测试集中的正边和负边来验证模型的效果了。

代码解读

完整代码如下

import networkx as nx
from deepsnap.graph import Graph
import torch
import torch.nn.functional as F
from sklearn.metrics import roc_auc_score
from torch_geometric.utils import negative_sampling
from torch_geometric.nn import GCNConv 
from torch_geometric.utils import train_test_split_edges

G = nx.karate_club_graph()
data = Graph(G)
data.num_features = 3
data.x = torch.ones((data.num_nodes, data.num_features), dtype=torch.float32)
data = train_test_split_edges(data)

class Net(torch.nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = GCNConv(data.num_features, 128)
        self.conv2 = GCNConv(128, 64)

    def encode(self):
        x = self.conv1(data.x, data.train_pos_edge_index)
        x = x.relu()
        return self.conv2(x, data.train_pos_edge_index)

    def decode(self, z, pos_edge_index, neg_edge_index):
        edge_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1)
        logits = (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1)
        return logits

    def decode_all(self, z):
        prob_adj = z @ z.t()
        return (prob_adj > 0).nonzero(as_tuple=False).t()


device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model, data = Net().to(device), data.to(device)
optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)


def get_link_labels(pos_edge_index, neg_edge_index):
    E = pos_edge_index.size(1) + neg_edge_index.size(1)
    link_labels = torch.zeros(E, dtype=torch.float, device=device)
    link_labels[:pos_edge_index.size(1)] = 1.
    return link_labels


def train():
    model.train()
    neg_edge_index = negative_sampling(
        edge_index=data.train_pos_edge_index, num_nodes=data.num_nodes,
        num_neg_samples=data.train_pos_edge_index.size(1),
        force_undirected=True,
    )
    optimizer.zero_grad()
    z = model.encode()
    link_logits = model.decode(z, data.train_pos_edge_index, neg_edge_index)
    link_labels = get_link_labels(data.train_pos_edge_index, neg_edge_index)
    loss = F.binary_cross_entropy_with_logits(link_logits, link_labels)
    loss.backward()
    optimizer.step()
    return loss


@torch.no_grad()
def test():
    model.eval()
    perfs = []
    for prefix in ["val", "test"]:
        pos_edge_index = data[f'{prefix}_pos_edge_index']
        neg_edge_index = data[f'{prefix}_neg_edge_index']
        z = model.encode()
        link_logits = model.decode(z, pos_edge_index, neg_edge_index)
        link_probs = link_logits.sigmoid()
        link_labels = get_link_labels(pos_edge_index, neg_edge_index)
        perfs.append(roc_auc_score(link_labels.cpu(), link_probs.cpu()))
    return perfs


best_val_perf = test_perf = 0
for epoch in range(1, 11):
    train_loss = train()
    val_perf, tmp_test_perf = test()
    if val_perf > best_val_perf:
        best_val_perf = val_perf
        test_perf = tmp_test_perf
    log = 'Epoch: {:03d}, Loss: {:.4f}, Val: {:.4f}, Test: {:.4f}'
    print(log.format(epoch, train_loss, best_val_perf, test_perf))

z = model.encode()
final_edge_index = model.decode_all(z)

第11行:将networkx中的graph对象转化为torch_geometric的Data对象
第13行:构造节点特征矩阵(原网络不存在节点特征)
第14行:分割训练边集、验证边集(默认占比0.05)以及测试边集(默认占比0.1)
第16-34行:构造一个简单的图卷积神经网络(两层),包含编码(节点嵌入)、解码(分数预测)等操作
第37行:指定设备
第38行:将模型和数据送入设备
第39行:指定优化器
第42-46行:将训练集中的正边标签设置为1,负边标签设置为0
第49-63行:训练函数,每次训练重新采样负边,计算模型损失,反向传播误差,更新模型参数
第67-78行:测试函数,评估模型在验证集和测试集上的预测准确率
第81-89行:训练模型,每次训练完,输出模型在验证集和测试集上的预测准确率
第91-92行:利用训练好的模型计算网络中剩余所有边的分数

上述代码输出为

Epoch: 001, Loss: 0.9718, Val: 0.8889, Test: 0.9388
Epoch: 002, Loss: 0.6701, Val: 0.8889, Test: 0.9388
Epoch: 003, Loss: 0.7293, Val: 0.8889, Test: 0.9388
Epoch: 004, Loss: 0.7009, Val: 0.8889, Test: 0.9388
Epoch: 005, Loss: 0.6880, Val: 0.8889, Test: 0.9388
Epoch: 006, Loss: 0.6905, Val: 0.8889, Test: 0.9388
Epoch: 007, Loss: 0.6906, Val: 0.8889, Test: 0.9388
Epoch: 008, Loss: 0.6835, Val: 0.8889, Test: 0.9388
Epoch: 009, Loss: 0.6779, Val: 0.8889, Test: 0.9388
Epoch: 010, Loss: 0.6788, Val: 0.8889, Test: 0.9388

训练集中的负边是每次随机采样得到的(第51-55行),而验证集和测试集中的负边则在第14行就已经固定了,所以结果中训练集上的loss一直在变化,而验证集和测试集上的得分没有变化。

  • 40
    点赞
  • 219
    收藏
    觉得还不错? 一键收藏
  • 37
    评论
神经网络(Graph Neural Network,GNN)是一种用于处理形数据的神经网络链路预测是指在一个已知的中,预测两个节点之间是否存在一条边。下面,我们将介绍如何使用 Python 实现神经网络链路预测。 一、安装 PyTorch 和 DGL 首先,我们需要安装 PyTorch 和 DGL(Deep Graph Library)。可以通过以下命令来安装它们: ``` pip install torch pip install dgl ``` 二、准备数据 我们将使用一个来自 DGL 的数据集来演示链路预测。该数据集包含了一个论文引用网络,其中每个节点表示一篇论文,边表示引用关系。我们的任务是预测两篇论文之间是否存在引用关系。 我们可以使用以下代码来加载数据: ``` import dgl.data dataset = dgl.data.CoraGraphDataset() g = dataset[0] ``` 在这个例子中,我们加载了 Cora 数据集,并获取了其中的第一个。 三、构建模型 我们将使用 GNN 模型来预测链路。我们将使用 PyTorch Geometric 库来构建模型。以下是我们的代码: ``` import torch import torch.nn.functional as F from torch_geometric.nn import GCNConv class Net(torch.nn.Module): def __init__(self): super(Net, self).__init__() self.conv1 = GCNConv(dataset.num_features, 16) self.conv2 = GCNConv(16, dataset.num_classes) def forward(self, g, inputs): h = self.conv1(g, inputs) h = F.relu(h) h = F.dropout(h, training=self.training) h = self.conv2(g, h) return h ``` 我们定义了一个名为 Net 的类,它继承自 torch.nn.Module。在构造函数中,我们初始化了两个 GCNConv 层,分别用于输入层和输出层。在前向传递中,我们首先使用第一个层对输入进行卷积,然后使用 ReLU 激活函数和 Dropout 层进行激活和正则化,最后使用第二个层进行卷积并返回输出。 四、训练模型 在训练模型之前,我们需要定义一些超参数,如学习率、迭代次数等。以下是我们的代码: ``` import time import numpy as np import torch.optim as optim from torch.utils.data import DataLoader from torch.utils.data.sampler import SubsetRandomSampler # 设置超参数 lr = 0.01 epochs = 200 batch_size = 32 train_size = 0.6 # 划分数据集 num_nodes = g.num_nodes() indices = np.random.permutation(num_nodes) split_idx = int(num_nodes * train_size) train_loader = DataLoader(dataset, batch_size=batch_size, sampler=SubsetRandomSampler(indices[:split_idx])) test_loader = DataLoader(dataset, batch_size=batch_size, sampler=SubsetRandomSampler(indices[split_idx:])) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # 初始化模型和优化器 model = Net().to(device) optimizer = optim.Adam(model.parameters(), lr=lr) # 训练模型 model.train() start_time = time.time() for epoch in range(epochs): train_loss = 0.0 for batch_idx, (inputs, targets, edge_index) in enumerate(train_loader): inputs, targets, edge_index = inputs.to(device), targets.to(device), edge_index.to(device) optimizer.zero_grad() outputs = model(g, inputs) loss = F.binary_cross_entropy_with_logits(outputs[edge_index[0]], targets.float()) loss.backward() optimizer.step() train_loss += loss.item() * inputs.size(0) train_loss /= len(train_loader.dataset) print('Epoch: {:04d}'.format(epoch+1), 'train_loss: {:.4f}'.format(train_loss), 'time: {:.4f}s'.format(time.time()-start_time)) ``` 在这个例子中,我们先将数据集划分为训练集和测试集。然后,我们使用 DataLoader 类将训练集和测试集转换为可迭代的数据集。接下来,我们初始化模型和优化器。在训练循环中,我们对每个批次进行前向传递和反向传递,并更新模型参数。我们还计算了训练集的平均损失,并打印了每个 epoch 的结果。 、测试模型 最后,我们使用以下代码测试模型的性能: ``` def test(model, loader): model.eval() correct = 0 for batch_idx, (inputs, targets, edge_index) in enumerate(loader): inputs, targets, edge_index = inputs.to(device), targets.to(device), edge_index.to(device) with torch.no_grad(): outputs = model(g, inputs) pred = (outputs[edge_index[0]] > 0).float() correct += (pred == targets.float()).sum().item() accuracy = correct / len(loader.dataset) return accuracy train_acc = test(model, train_loader) test_acc = test(model, test_loader) print('Train accuracy: {:.4f}'.format(train_acc)) print('Test accuracy: {:.4f}'.format(test_acc)) ``` 在这个例子中,我们使用 test() 函数对模型进行测试。在测试循环中,我们对每个批次进行前向传递,并计算预测准确率。最后,我们打印了训练集和测试集的准确率。 六、总结 在本文中,我们介绍了如何使用 Python 实现神经网络链路预测。我们使用了 PyTorch 和 DGL 库来构建和训练 GNN 模型,并使用 PyTorch Geometric 库进行模型构建。通过本文的学习,您应该能够了解如何使用 Python 实现神经网络链路预测,并且能够应用这些知识到实际的项目中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 37
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值