链接预测中训练集、验证集以及测试集的划分(以PyG的RandomLinkSplit为例)

1. 链接预测的基本概念

在图任务中,所谓链接预测,一般有两种含义:在静态网络中,链接预测用于发现缺失的链接,而在动态网络中,链接预测用于预测未来可能出现的链接。

之前学术界关于链接预测的方案主要可以分为三类:启发式方法(Heuristic Methods),Network Embedding的方法和图神经网络方法。

  • 启发式方法(Heuristic Methods): 主要考虑利用两个节点之间一些预先定义的特征来衡量节点之间的相似度,比如,节点的共同邻居(Common Neighbor),Jaccard相似度,Katz Index等,这样的方法假设存在边关系的节点之间存在某种特定的结构特性,但这样的假设不一定对任意网络都有效。
  • Network Embeddings方法: 使用基于游走的方法得到经过某个节点的多条路径,再通过Skip-Gram和CBOW的方式,对路径上mask的节点进行预测。这样的方法,没有直接将链接预测任务嵌入到有监督学习的流程中,并且无法较好的利用用户的节点属性,无法达到较好的预测精度。
  • 图神经网络(graph neural network,GNN): 主要分为两类,节点为中心的图神经网络模型(Node-centric GNN)和边为中心的图神经网络模型(Edge-centric GNN)。

在GNN链接预测中,我们一般将链接预测转换为一个二分类问题:图中存在的边我们称之为正样本,不存在的边我们称之为负样本。

一个现实的问题是:网络中存在的链接数往往都是远小于不存在的链接数的,也就是说图中的正样本数量远小于负样本数量,为了使模型训练较为均衡,我们通常先将正样本分为训练集、验证集和测试集,然后再分别从三个数据集中采样等同数量的负样本参与训练、验证以及测试。

2. 一些术语

在PyG的链接预测中,一共有4种类型的边:

  1. training message edges:用于GNN的消息传递和聚合的边。
  2. training supervision edges:分为正样本和负样本,在利用training message edges得到节点的向量表示后,利用training supervision edges进行有监督学习。
  3. validation edges:用于验证的边。
  4. testing edges:用于测试的边。

需要注意的是:validation edgestesting edges不能和训练阶段的两种边有交集。

一个问题:模型训练阶段中的training meaasge edgestraining supervision edges允许出现相同边吗?会不会造成数据泄露?

一般来说,在消息传递和监督中使用相同的边集可能会在训练阶段导致数据泄漏,但这取决于模型的能力。例如,GAE使用基于GCN的编码器和基于点积的解码器,编码器和解码器的能力都有限,因此模型的数据泄漏能力也有限。

以上内容来自PyG作者在一个issue中的回答:RandomLinkSplit 中的拆分错误 #3668

3. 实例

3.1 数据集介绍

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

加载数据:

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

输出:

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

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

3.2 RandomLinkSplit

得到数据集后,接下来我们需要划分训练集、验证集以及测试集。

划分的标准为:训练集中不能包含验证集和测试集中存在的链接,验证集中不能包含测试集中存在的链接。

利用PyG封装的RandomLinkSplit我们很容易实现数据集的划分。RandomLinkSplit的具体参数如下所示:
在这里插入图片描述
介绍几个常用的参数:

  1. num_val:验证集中边的比例,默认为0.1。
  2. num_test:测试集中边的比例,默认为0.1。
  3. is_undirected:如果为True,则假定图是无向图。
  4. add_negative_train_samples:是否为链接预测添加负训练样本,如果模型已经执行了负采样,则该选项应设置为False。一般我们设置为False,也就是训练集中不包含负样本,然后每一轮训练时在训练集中重新采样与正样本相同数量的负样本进行训练,这样可以保证每一轮训练中采样得到的负样本都是不一样的,可以有效提高模型泛化能力。默认为False,也就是不采样。
  5. neg_sampling_ratio:采样的正负样本的比例,默认为1。即验证集和测试集中正负样本个数一致。
  6. disjoint_train_ratio:如果设置为大于0的值,则不会为消息传递和监督共享训练边。

利用RandomLinkSplit对CiteSeer进行划分:

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]

最终我们得到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])

从上到下依次为原始数据集、训练集、验证集以及测试集。

可以发现,验证集和测试集中都有:

edge_label=[910], edge_label_index=[2, 910])

也就是说验证集和测试集中都一共包含了910条边,这里edge_label为01数据,其中正样本标签为1,负样本标签为0,输出一下edge_label之和:

print(val_data.edge_label.sum())
tensor(455., device='cuda:0')

910是455的两倍,说明验证集和测试集中正负样本的比例为1,这是因为neg_sampling_ratio默认为1。

对于训练集:

edge_index=[2, 7284], edge_label=[3642], edge_label_index=[2, 3642]
print(train_data.edge_label.sum())
tensor(3642., device='cuda:0')

说明训练集中包含了一共3642条边,并且这些边都是正样本,也就是原始图中存在的边,值得注意的是,由于设置了disjoint_train_ratio=0,那么训练集中消息传递和监督是会有重叠的,具体来讲就是训练过程中用于监督训练的边edge_label_index包含在了用于消息传递和聚合的边edge_index中。我们可以简单验证如下:

train_edge = [(train_data.edge_index[0][i].item(), train_data.edge_index[1][i].item()) for i in range(train_data.edge_index.size(1))]
train_label_edge = [(train_data.edge_label_index[0][i].item(), train_data.edge_label_index[1][i].item()) for i in range(train_data.edge_label_index.size(1))]
s = 0
for x in train_label_edge:
    if x in train_edge:
        s += 1
print('s=', s)

输出s=3642,说明edge_label_index完全包含在了edge_index中。

因此,原始图中一共9104条边,在0.1验证和0.1测试的比例下,验证集和测试集都分得了910个样本,这其中包括455个正样本和455个负样本,训练集分得了3642个正样本(负样本在训练时进行采样)。

而我们知道,9104的0.8和0.1分别为7283和910,这其中训练集一共得到了7284条边用于训练,这7284条边中的一半也就是3642条边用于有监督学习;测试集和验证集都得到了910条边(这里不全是正样本)。

<think>好的,我现在需要帮助用户了解如何使用PyG中的RandomLinkSplit来处理异构图数据。首先,用户提到了RandomLinkSplit的示教程,所以我得先回忆一下这个类的功能和常用参数。 根据用户提供的引用[^2],他们是在处理异构图,从CSV文件构建图数据,所以应该是在异构场景下使用RandomLinkSplit。我需要确定RandomLinkSplit在异构图中的使用方式,特别是如何分割边类型的数据。 首先,RandomLinkSplit的主要作用是将图的边划分为训练、验证和测试集,常用于链接预测任务。对于异构图来说,不同的边类型可能需要分别处理。因此,参数edge_types和rev_edge_types会很重要。需要确保用户指定了正确的边类型,并且可能还需要处理反向边。 接下来,我需要考虑如何初始化RandomLinkSplit如,设置split参数来分配比,比如[0.8, 0.1, 0.1],然后指定num_val和num_test。另外,disjoint_train_split参数可能也需要说明,因为它影响训练边是否与验证/测试边重叠。 然后,在应用transform的时候,需要将数据集传入,并正确获取划分后的训练、验证和测试数据。这里需要注意,对于异构图,返回的数据对象可能包含不同边类型的划分结果,用户需要知道如何访问这些部分。 用户可能需要一个具体的代码示,结合他们提到的从CSV构建异构图的情况。所以,我需要提供一个子,展示在加载数据后如何使用RandomLinkSplit进行分割。同时,要指出关键参数的作用,比如edge_types和add_negative_train_samples,后者是否需要在训练集中添加负样本。 此外,用户可能容易出错的地方是没有正确指定edge_types,或者忘记处理反向边。需要提醒用户根据实际边类型调整参数,并检查数据分割后的结果是否符合预期。 最后,相关问题的部分需要考虑用户可能进一步的问题,比如如何处理负采样、参数调整的影响,或者在同构图中的应用。这些可以作为相关问题列出,帮助用户扩展理解。</think>以下是如何使用PyG中`RandomLinkSplit`处理异构图数据的逐步说明: ### 1. 导入必要组件 ```python from torch_geometric.transforms import RandomLinkSplit from torch_geometric.data import HeteroData ``` ### 2. 创建异构图数据 假设已构建包含作者-论文关系的异构图: $$ HeteroData(author, paper, writes) $$ 其中`writes`边类型连接作者和论文 ### 3. 初始化RandomLinkSplit ```python split = RandomLinkSplit( num_val=0.1, # 验证集 num_test=0.2, # 测试集 edge_types=[("author", "writes", "paper")], # 需要分割的边类型 rev_edge_types=[("paper", "rev_writes", "author")], # 自动生成反向边 is_undirected=True # 是否视为无向图 ) ``` ### 4. 执行数据划分 ```python # 假设data是原始HeteroData对象 train_data, val_data, test_data = split(data) ``` ### 5. 获取划分结果 - `train_data`: 包含80%原始边+负样本 - `val_data`: 包含10%边作为验证正样本+负样本 - `test_data`: 包含20%边作为测试正样本+负样本 ### 关键参数说明表 | 参数 | 说明 | 示值 | |------|------|--------| | edge_types | 要分割的边类型列表 | [("author","writes","paper")] | | rev_edge_types | 反向边类型列表 | [("paper","rev_writes","author")] | | add_negative_train_samples | 是否添加负样本 | True | | disjoint_train_split | 是否保证训练边与验证/测试边不重叠 | True | ### 完整示流程 ```python # 加载原始数据集 data = HeteroData() # ... 添加节点和边的代码 ... # 执行划分 transform = RandomLinkSplit(num_val=0.1, num_test=0.2) train, val, test = transform(data) # 检查划分结果 print("训练边数:", train["author", "writes", "paper"].edge_index.shape[1]) print("验证边数:", val["author", "writes", "paper"].edge_label_index.shape[1]) ``` ### 注意事项 1. 确保`edge_types`与数据中定义的边类型完全匹配 2. 反向边类型需要显式定义在`rev_edge_types`中 3. 使用`disjoint_train_split=True`时,训练边不会出现在验证/测试集 4. 负采样数量通过`neg_sampling_ratio`参数控制
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyril_KI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值