• PyG搭建GCN实现链接预测


    前言

    关于链接预测的介绍以及链接预测中数据集的划分请参考:链接预测中训练集、验证集以及测试集的划分(以PyG的RandomLinkSplit为例)

    1. 数据处理

    这里以CiteSeer网络为例:Citeseer网络是一个引文网络,节点为论文,一共3327篇论文。论文一共分为六类:Agents、AI(人工智能)、DB(数据库)、IR(信息检索)、ML(机器语言)和HCI。如果两篇论文间存在引用关系,那么它们之间就存在链接关系。

    加载数据:

    dataset = Planetoid('data', name='CiteSeer')
    print(dataset[0])
    
    • 1
    • 2

    输出:

    Data(x=[3327, 3703], edge_index=[2, 9104], y=[3327], train_mask=[3327], val_mask=[3327], test_mask=[3327])
    
    • 1

    x=[3327, 3703]表示一共有3327个节点,然后节点的特征维度为3703,这里实际上是去除停用词和在文档中出现频率小于10次的词,整理得到3703个唯一词。edge_index=[2, 9104],表示一共9104条edge,数据一共两行,每一行都表示节点编号。

    利用PyG封装的RandomLinkSplit我们很容易实现数据集的划分:

    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    transform = T.Compose([
        T.NormalizeFeatures(),
        T.ToDevice(device),
        T.RandomLinkSplit(num_val=0.1, num_test=0.1, is_undirected=True,
                          add_negative_train_samples=False),
    ])
    dataset = Planetoid('data', name='CiteSeer', transform=transform)
    train_data, val_data, test_data = dataset[0]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最终我们得到train_data, val_data, test_data

    输出一下原始数据集和三个被划分出来的数据集:

    Data(x=[3327, 3703], edge_index=[2, 9104], y=[3327], train_mask=[3327], val_mask=[3327], test_mask=[3327])
    Data(x=[3327, 3703], edge_index=[2, 7284], y=[3327], train_mask=[3327], val_mask=[3327], test_mask=[3327], edge_label=[3642], edge_label_index=[2, 3642])
    Data(x=[3327, 3703], edge_index=[2, 7284], y=[3327], train_mask=[3327], val_mask=[3327], test_mask=[3327], edge_label=[910], edge_label_index=[2, 910])
    Data(x=[3327, 3703], edge_index=[2, 8194], y=[3327], train_mask=[3327], val_mask=[3327], test_mask=[3327], edge_label=[910], edge_label_index=[2, 910])
    
    • 1
    • 2
    • 3
    • 4

    从上到下依次为原始数据集、训练集、验证集以及测试集。其中,训练集中一共有3642个正样本,验证集和测试集中均为455个正样本+455个负样本。

    2. GCN链接预测

    本次实验使用GCN来进行链接预测:首先利用GCN对训练集中的节点进行编码,得到节点的向量表示,然后使用这些向量表示对训练集中的正负样本(在每一轮训练时重新采样负样本)进行有监督学习,具体来讲就是利用节点向量求得样本中节点对的内积,然后与标签求损失,最后反向传播更新参数。

    2.1 负采样

    链接预测训练过程中的每一轮我们都需要对训练集进行采样以得到与正样本数量相同的负样本,验证集和测试集在数据集划分阶段已经进行了负采样,因此不必再进行采样。

    负采样函数:

    def negative_sample():
        # 从训练集中采样与正边相同数量的负边
        neg_edge_index = negative_sampling(
            edge_index=train_data.edge_index, num_nodes=train_data.num_nodes,
            num_neg_samples=train_data.edge_label_index.size(1), method='sparse')
        # print(neg_edge_index.size(1))   # 3642条负边,即每次采样与训练集中正边数量一致的负边
        edge_label_index = torch.cat(
            [train_data.edge_label_index, neg_edge_index],
            dim=-1,
        )
        edge_label = torch.cat([
            train_data.edge_label,
            train_data.edge_label.new_zeros(neg_edge_index.size(1))
        ], dim=0)
    
        return edge_label, edge_label_index
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这里用到了negative_sampling方法,其参数有:
    在这里插入图片描述
    具体来讲,negative_sampling方法利用传入的edge_index参数进行负采样,即采样num_neg_samplesedge_index中不存在的边。num_nodes指定节点个数,method指定采样方法,有sparsedense两种方法。

    采样后将neg_edge_index与训练集中原有的正样本train.edge_label_index进行拼接以得到完整的样本集,同时也需要在原本的train_data.edge_label后面添加指定数量的0用于表示负样本。

    2.2 模型搭建

    GCN链接预测模型搭建如下:

    class GCN_LP(torch.nn.Module):
        def __init__(self, in_channels, hidden_channels, out_channels):
            super().__init__()
            self.conv1 = GCNConv(in_channels, hidden_channels)
            self.conv2 = GCNConv(hidden_channels, out_channels)
    
        def encode(self, x, edge_index):
            x = self.conv1(x, edge_index).relu()
            x = self.conv2(x, edge_index)
            return x
    
        def decode(self, z, edge_label_index):
            # z所有节点的表示向量
            src = z[edge_label_index[0]]
            dst = z[edge_label_index[1]]
            # print(dst.size())   # (7284, 64)
            r = (src * dst).sum(dim=-1)
            # print(r.size())   (7284)
            return r
    
        def forward(self, x, edge_index, edge_label_index):
            z = self.encode(x, edge_index)
            return self.decode(z, edge_label_index)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    编码器由一个两层GCN组成,用于得到训练集中节点的向量表示,解码器用于得到训练集中节点对向量间的内积。

    由前面可知训练集中的正样本数量为3642,经过负采样函数negative_sample得到3642个负样本,一共7284个样本,最终解码器返回7284个节点对间的内积。

    损失函数采用BCEWithLogitsLoss,要想弄懂BCEWithLogitsLoss,就要先了解BCELoss

    BCELoss是一种二元交叉熵损失:
    在这里插入图片描述

    BCEWithLogitsLoss则是在BCELoss的基础上增加了Sigmoid选项,即先把输入经过一个Sigmoid,然后再计算BCELoss

    评价指标采用AUC:

    roc_auc_score(data.edge_label.cpu().numpy(), out.cpu().numpy())
    
    • 1

    2.3 模型训练/测试

    代码如下:

    def test(model, data):
        model.eval()
        with torch.no_grad():
            z = model.encode(data.x, data.edge_index)
            out = model.decode(z, data.edge_label_index).view(-1).sigmoid()
            model.train()
        return roc_auc_score(data.edge_label.cpu().numpy(), out.cpu().numpy())
    
    
    def train():
        model = GCN_LP(dataset.num_features, 128, 64).to(device)
        optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01)
        criterion = torch.nn.BCEWithLogitsLoss().to(device)
        min_epochs = 10
        best_model = None
        best_val_auc = 0
        final_test_auc = 0
        model.train()
        for epoch in range(100):
            optimizer.zero_grad()
            edge_label, edge_label_index = negative_sample()
            out = model(train_data.x, train_data.edge_index, edge_label_index).view(-1)
            loss = criterion(out, edge_label)
            loss.backward()
            optimizer.step()
            # validation
            val_auc = test(model, val_data)
            test_auc = test(model, test_data)
            if epoch + 1 > min_epochs and val_auc > best_val_auc:
                best_val_auc = val_auc
                final_test_auc = test_auc
    
            print('epoch {:03d} train_loss {:.8f} val_auc {:.4f} test_auc {:.4f}'
                  .format(epoch, loss.item(), val_auc, test_auc))
    
        return final_test_auc
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    最终测试集上的AUC为:

    final best auc: 0.9076681560198044
    
    • 1
  • 相关阅读:
    计算机组成原理(谭志虎主编 )
    本地搭建Stackedit Markdown编辑器结合内网穿透实现远程访问
    最长的可整合子数组的长度
    Vue3+TypeScript学习
    SQL Server如何新建作业
    太菜了不知道啥是API
    Go+VsCode配置环境
    VMware CentOS 虚拟机扩容
    Oracle/PLSQL: NULLIF Function
    Visual Studio批量删除换行
  • 原文地址:https://blog.csdn.net/Cyril_KI/article/details/125956540