目录

  • 节点分类与回归
    • 概述
    • 定义GNN模型
    • 模型训练
    • 异构图上的节点分类模型训练
  • 边分类与回归
    • 概述
    • 模型训练
    • 异构图上的边预测模型训练(边的回归)
    • 在异构图中预测已有边的类型
  • 链接预测
    • 概述
    • 模型训练
    • 异构图上的链接预测模型训练
  • 整图分类
    • 概述
    • 模型定义
    • 模型训练
    • 异构图的整图分类模型

本篇内容为GNN的训练,全面总结GNN的四大任务:

  • 节点分类与回归
  • 边分类与回归
  • 链接预测
  • 整图分类

假设用户的图以及所有的节点和边特征都能存进GPU。对于无法全部载入的情况,需额外了解在大图上的随机(批次)训练;

此处补充异构图的样例训练数据,这个 hetero_graph 异构图有以下这些边的类型:

  • ('user', 'follow', 'user')
  • ('user', 'followed-by', 'user')
  • ('user', 'click', 'item')
  • ('item', 'clicked-by', 'user')
  • ('user', 'dislike', 'item')
  • ('item', 'disliked-by', 'user')
import dgl
import numpy as np
import torchn_users = 1000
n_items = 500
n_follows = 3000
n_clicks = 5000
n_dislikes = 500
n_hetero_features = 10
n_user_classes = 5
n_max_clicks = 10"""
randint(low=0, high, size)
值在low到high之间,形状由size决定
torch.randint(6,(2,2))tensor([[5, 1],[4, 3]])
"""
follow_src = np.random.randint(0, n_users, n_follows)
follow_dst = np.random.randint(0, n_users, n_follows)
click_src = np.random.randint(0, n_users, n_clicks)
click_dst = np.random.randint(0, n_items, n_clicks)
dislike_src = np.random.randint(0, n_users, n_dislikes)
dislike_dst = np.random.randint(0, n_items, n_dislikes)hetero_graph = dgl.heterograph({('user', 'follow', 'user'): (follow_src, follow_dst),('user', 'followed-by', 'user'): (follow_dst, follow_src),('user', 'click', 'item'): (click_src, click_dst),('item', 'clicked-by', 'user'): (click_dst, click_src),('user', 'dislike', 'item'): (dislike_src, dislike_dst),('item', 'disliked-by', 'user'): (dislike_dst, dislike_src)})hetero_graph.nodes['user'].data['feature'] = torch.randn(n_users, n_hetero_features)
hetero_graph.nodes['item'].data['feature'] = torch.randn(n_items, n_hetero_features)
hetero_graph.nodes['user'].data['label'] = torch.randint(0, n_user_classes, (n_users,))
hetero_graph.edges['click'].data['label'] = torch.randint(1, n_max_clicks, (n_clicks,)).float()# 在user类型的节点和click类型的边上随机生成训练集的掩码
hetero_graph.nodes['user'].data['train_mask'] = torch.zeros(n_users, dtype=torch.bool).bernoulli(0.6)
hetero_graph.edges['click'].data['train_mask'] = torch.zeros(n_clicks, dtype=torch.bool).bernoulli(0.6)print(torch.zeros(n_clicks, dtype=torch.bool))
# tensor([False, False, False,  ..., False, False, False]) torch.Size([5000])
print(hetero_graph.edges['click'].data['train_mask'])
# tensor([False,  True, False,  ...,  True, False,  True]) torch.Size([5000])

节点分类与回归

概述

对于图神经网络来说,最常见和被广泛使用的任务之一就是节点分类。 图数据中的训练、验证和测试集中的每个节点都具有从一组预定义的类别中分配的一个类别,即正确的标注。 节点回归任务也类似,训练、验证和测试集中的每个节点都被标注了一个正确的数字。

为了对节点进行分类,图神经网络执行了消息传递范式中的消息传递机制,利用节点自身的特征和其邻节点及边的特征来计算节点的隐空间表示。 消息传递可以重复多轮,以利用更大范围的邻居信息。

定义GNN模型

对于图上的深度学习模型,通常需要一个多层的图神经网络,并在这个网络中要进行多轮的信息传递。 可以通过堆叠图卷积模块来实现这种网络架构,具体如下所示:

# 构建一个2层的GNN模型
import dgl.nn.pytorch as dglnn
import torch.nn as nn
import torch.nn.functional as Fclass SAGE(nn.Module):def __init__(self, in_feats, hid_feats, out_feats):super().__init__()# 实例化SAGEConve,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregator_type是聚合函数的类型self.conv1 = dglnn.SAGEConv(in_feats=in_feats, out_feats=hid_feats, aggregator_type='mean')self.conv2 = dglnn.SAGEConv(in_feats=hid_feats, out_feats=out_feats, aggregator_type='mean')def forward(self, graph, inputs):# 输入是节点的特征h = self.conv1(graph, inputs)h = F.relu(h)h = self.conv2(graph, h)return h

模型训练

全图(使用所有的节点和边的特征)上的训练只需要使用上面定义的模型进行前向传播计算,并通过在训练节点上比较预测和真实标签来计算损失,从而完成后向传播。

使用DGL内置的数据集 dgl.data.CiteseerGraphDataset 来展示模型的训练。 节点特征和标签存储在其图上,训练、验证和测试的分割也以布尔掩码的形式存储在图上。


关于 CiteseerGraphDataset

dgl.data.CiteseerGraphDataset(raw_dir=None, force_reload=False, verbose=True, reverse_edge=True)

节点表示科学出版物(一共3327个节点),edge表示引用关系。 每个节点都有一个具有 3703 个维度的预定义特征。该数据集专为节点分类任务而设计。 任务是预测某个出版物的类别(6个类别)。

参数:

  • raw_dir:指定下载数据的存储目录或已下载数据的存储目录。默认: ~/.dgl/
  • force_reload:是否重新导入数据集;
  • verbose:是否打印进度信息;
  • reverse_edge:是否在图中添加反向边,默认:True

类内的__getitem__(idx)方法,可以获取图对象,参数为idx,它是项目索引,注意CiteseerGraphDataset 只有一个图对象;

该方法返回为一个图对象(包括图结构,mask,节点特征和标签),比如:

ndata['train_mask']:训练集mask
ndata['val_mask']: 验证集mask
ndata['test_mask']: 测试集mask
ndata['feat']: node feature
ndata['label']: ground truth labels

调用实例:

from dgl.data import CiteseerGraphDatasetdataset = CiteseerGraphDataset()
g = dataset[0]
num_class = dataset.num_classes
# 获取节点特征
feat = g.ndata['feat'] # torch.Size([3327, 3703])
# 获取样本划分信息
train_mask = g.ndata['train_mask'] # torch.Size([3327])
val_mask = g.ndata['val_mask']
test_mask = g.ndata['test_mask']
# 获取标签
label = g.ndata['label'] # torch.Size([3327])

训练模型前,先加载数据:

from dgl.data import CiteseerGraphDatasetdataset = CiteseerGraphDataset()
graph = dataset[0]node_features = graph.ndata['feat']
node_labels = graph.ndata['label']
train_mask = graph.ndata['train_mask']
valid_mask = graph.ndata['val_mask']
test_mask = graph.ndata['test_mask']n_features = node_features.shape[1]
n_labels = int(node_labels.max().item() + 1)

定义评估模型的函数:

import torchdef evaluate(model, graph, features, labels, mask):model.eval()with torch.no_grad():logits = model(graph, features) # [num_dst_nodes,dim_out_features=num_class]# 对于训练集,如果train mask有500个节点bool=True,则logits[mask].size()为[500,num_class]logits = logits[mask]labels = labels[mask]"""tensor.max(dim=None)->(Tensor,Tensor)dim用于选择对哪个轴方向进行操作,返回对象元组有两个元素,一个是索引到的最大值结果,一个是最大值所在轴上的位置"""_, indices = torch.max(logits, dim=1)correct = torch.sum(indices == labels)return correct.item() * 1.0 / len(labels)

模型训练过程如下:

model = SAGE(in_feats=n_features, hid_feats=100, out_feats=n_labels)
opt = torch.optim.Adam(model.parameters())for epoch in range(10):model.train()# 使用所有节点(全图)进行前向传播计算logits = model(graph, node_features)# 计算损失值loss = F.cross_entropy(logits[train_mask], node_labels[train_mask])# 计算验证集的准确度acc = evaluate(model, graph, node_features, node_labels, valid_mask)# 进行反向传播计算opt.zero_grad()loss.backward()opt.step()print(loss.item())# 如果需要的话,保存训练好的模型,本例中省略"""
1.7910587787628174
...
1.6314970254898071
"""

异构图上的节点分类模型训练

如果图是异构的,用户可能希望沿着所有边类型从邻居那里收集消息。 用户可以使用 dgl.nn.pytorch.HeteroGraphConv 模块(回顾第二十一课-官方SAGEConv和HeteroGraphConv用法部分)在所有边类型上执行消息传递, 并为每种边类型使用一种图卷积模块。

下面的代码定义了一个异构图卷积模块。模块首先对每种边类型进行单独的图卷积计算,然后将每种边类型上的消息聚合结果再相加, 并作为所有节点类型的最终结果。

# 构建一个2层的GNN模型
import dgl.nn.pytorch as dglnn
import torch.nn as nn
import torch.nn.functional as Fclass RGCN(nn.Module):def __init__(self, in_feats, hid_feats, out_feats, rel_names):super().__init__()# 实例化HeteroGraphConv,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregate是聚合函数的类型self.conv1 = dglnn.HeteroGraphConv(# GraphConv即普通的GCN{rel: dglnn.GraphConv(in_feats, hid_feats) for rel in rel_names},aggregate='sum')self.conv2 = dglnn.HeteroGraphConv({rel: dglnn.GraphConv(hid_feats, out_feats) for rel in rel_names},aggregate='sum')def forward(self, graph, inputs):# 输入是节点的特征字典"""注意如果使用样例的异构图数据时,源节点和目标节点已经被我们定义为双向,所以inputs直接输入所有类型节点的特征即可,forward返回的也是所有类型节点的更新后特征"""h = self.conv1(graph, inputs)h = {k: F.relu(v) for k, v in h.items()}h = self.conv2(graph, h)return h

异构图训练的样例数据中已经有 useritem 的特征,我们可实例化模型并获取数据:

# hetero_graph.etypes ['clicked-by', 'disliked-by', 'click', 'dislike', 'follow', 'followed-by']
model = RGCN(n_hetero_features, 20, n_user_classes, hetero_graph.etypes)user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
labels = hetero_graph.nodes['user'].data['label']
train_mask = hetero_graph.nodes['user'].data['train_mask']

然后按以下方式进行前向计算:

node_features = {'user': user_feats, 'item': item_feats}
h_dict = model(hetero_graph, node_features)
h_user = h_dict['user'] # torch.Size([1000, 5])
h_item = h_dict['item'] # torch.Size([500, 5])

异构图上模型的训练和同构图的模型训练是一样的,只是这里使用了一个包括节点表示的字典来计算预测值。 例如,如果只预测 user 节点的类别,用户可以从返回的字典中提取 user 的节点嵌入。

opt = torch.optim.Adam(model.parameters())for epoch in range(5):model.train()# 使用所有节点的特征进行前向传播计算,并提取输出的user节点嵌入logits = model(hetero_graph, node_features)['user']# 计算损失值loss = F.cross_entropy(logits[train_mask], labels[train_mask])# 计算验证集的准确度,在本例中省略# 进行反向传播计算opt.zero_grad()loss.backward()opt.step()print(loss.item())# 如果需要的话,保存训练好的模型,本例中省略

边分类与回归

概述

有时用户希望预测图中边的属性值,这种情况下,用户需要构建一个边分类/回归的模型。

以下代码生成了一个随机图用于演示边分类/回归:

import numpy as np
import dgl
import torch# 生成源节点和目标节点
src = np.random.randint(0, 100, 500)
dst = np.random.randint(0, 100, 500)
# 同时建立反向边
edge_pred_graph = dgl.graph((np.concatenate([src, dst]), np.concatenate([dst, src]))) # (1000,1000)
print(edge_pred_graph)
"""
Graph(num_nodes=100, num_edges=1000,ndata_schemes={}edata_schemes={})
"""
# 建立点和边特征,以及边的标签
edge_pred_graph.ndata['feature'] = torch.randn(100, 10)
edge_pred_graph.edata['feature'] = torch.randn(1000, 10)
edge_pred_graph.edata['label'] = torch.randn(1000)
# 进行训练、验证和测试集划分
edge_pred_graph.edata['train_mask'] = torch.zeros(1000, dtype=torch.bool).bernoulli(0.6) # 伯努利采样

之前介绍了如何使用多层GNN进行节点分类。同样的方法也可以被用于计算任何节点的隐藏表示。 并从边的两个端点的表示,通过计算得出对边属性的预测。

对一条边计算预测值最常见的情况是将预测表示为一个函数,函数的输入为两个端点的表示, 输入还可以包括边自身的特征。

下面分析与节点分类在模型实现上的差别:

如果用户使用前面的模型计算了节点的表示,那么用户只需要再编写一个用 apply_edges() 计算边预测的组件即可进行边分类/回归任务。

例如,对于边回归任务,如果用户想为每条边计算一个分数,可按下面的代码对每一条边计算它的两端节点隐藏表示的点积来作为分数:

import dgl.function as fn
import torch.nn as nnclass DotProductPredictor(nn.Module):def forward(self, graph, h):# h为节点特征with graph.local_scope():graph.ndata['h'] = hgraph.apply_edges(fn.u_dot_v('h', 'h', 'score'))return graph.edata['score']dotproduct=DotProductPredictor()
score=dotproduct(edge_pred_graph,edge_pred_graph.ndata['feature'])
print(score.size()) # torch.Size([1000, 1])

用户也可以使用MLP(多层感知机)对每条边生成一个向量表示(例如,作为一个未经过归一化的类别的分布), 并在下游任务中使用。

class MLPPredictor(nn.Module):def __init__(self, in_features, out_classes):super().__init__()self.fc = nn.Linear(in_features * 2, out_classes)def apply_edges(self, edges):"""自定义消息函数消息函数接受一个参数edges,这是一个EdgeBatch的实例,在消息传递时,它被DGL在内部生成以表示一批边;edges有src、dst和data共3个成员属性,分别用于访问源节点、目标节点和边的特征;返回一个结果字典"""h_u = edges.src['feature']h_v = edges.dst['feature']score = self.fc(torch.cat([h_u, h_v], 1))return {'score': score}def forward(self, graph, h):# h为节点特征with graph.local_scope():graph.ndata['h'] = h# apply_edges()的参数为消息函数graph.apply_edges(self.apply_edges)return graph.edata['score']edge_wise=MLPPredictor(10,6)
score=edge_wise(edge_pred_graph,edge_pred_graph.ndata['feature'])
print(score.size()) # torch.Size([1000, 6])

模型训练

给定计算节点和边上表示的模型后,用户可以轻松地编写在所有边上进行预测的全图训练代码。

以下代码用了 SAGE 作为节点表示计算模型以及前面定义的 DotPredictor 作为边预测模型。

from dgl.nn.pytorch.conv import SAGEConvclass Model(nn.Module):def __init__(self, in_features, out_features):super().__init__()self.sage = SAGEConv(in_features, out_features, aggregator_type="mean")self.pred = DotProductPredictor()def forward(self, g, x):h = self.sage(g, x)return self.pred(g, h)

在训练模型时可以使用布尔掩码区分训练、验证和测试数据集。该例子里省略了训练早停和模型保存部分的代码,训练模型的过程如下:

node_features = edge_pred_graph.ndata['feature']
edge_label = edge_pred_graph.edata['label']
train_mask = edge_pred_graph.edata['train_mask']
model = Model(10, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):# 回归边的值pred = model(edge_pred_graph, node_features).view(-1)# MSE Lossloss = ((pred[train_mask] - edge_label[train_mask]) ** 2).mean()opt.zero_grad()loss.backward()opt.step()print(loss.item())

异构图上的边预测模型训练(边的回归)

如果想在某一特定类型的边上进行回归任务,用户只需要计算所有边类型上的节点表示, 然后同样通过调用 apply_edges() 方法计算预测值即可。 唯一的区别是在调用 apply_edges 时需要指定边的类型。

import torch.nn as nn
import dgl.function as fnclass HeteroDotProductPredictor(nn.Module):def forward(self, graph, h, etype):# h是异构图的每种类型的边的节点特征with graph.local_scope():graph.ndata['h'] = h   #一次性为所有节点类型的'h'赋值graph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)return graph.edges[etype].data['score']

在某种类型的边上为每一条边预测的端到端模型的定义如下所示:

# 构建一个2层的GNN模型
import dgl.nn.pytorch as dglnn
import torch.nn as nn
import torch.nn.functional as Fclass RGCN(nn.Module):def __init__(self, in_feats, hid_feats, out_feats, rel_names):super().__init__()# 实例化HeteroGraphConv,in_feats是输入特征的维度,out_feats是输出特征的维度,aggregate是聚合函数的类型self.conv1 = dglnn.HeteroGraphConv(# GraphConv即普通的GCN{rel: dglnn.GraphConv(in_feats, hid_feats) for rel in rel_names},aggregate='sum')self.conv2 = dglnn.HeteroGraphConv({rel: dglnn.GraphConv(hid_feats, out_feats) for rel in rel_names},aggregate='sum')def forward(self, graph, inputs):# 输入是节点的特征字典"""注意如果使用样例的异构图数据时,源节点和目标节点已经被我们定义为双向,所以inputs直接输入所有类型节点的特征即可,forward返回的也是所有类型节点的更新后特征"""h = self.conv1(graph, inputs)h = {k: F.relu(v) for k, v in h.items()}h = self.conv2(graph, h)return hclass Model(nn.Module):def __init__(self, in_features, hidden_features, out_features, rel_names):super().__init__()self.sage = RGCN(in_features, hidden_features, out_features, rel_names)self.pred = HeteroDotProductPredictor()def forward(self, g, x, etype):h = self.sage(g, x)return self.pred(g, h, etype)

使用模型时只需要简单地向模型提供一个包含节点类型和特征的字典:

# 实例化模型
model = Model(10, 20, 5, hetero_graph.etypes)user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
label = hetero_graph.edges['click'].data['label']
train_mask = hetero_graph.edges['click'].data['train_mask']node_features = {'user': user_feats, 'item': item_feats}

训练部分和同构图的训练基本一致。例如,如果用户想预测边类型为 click 的边的标签,只需要按下例编写代码。

opt = torch.optim.Adam(model.parameters())
for epoch in range(10):pred = model(hetero_graph, node_features, 'click').view(-1)#print(pred.size()) # 5000#print(label.size()) # 5000loss = ((pred[train_mask] - label[train_mask]) ** 2).mean()opt.zero_grad()loss.backward()opt.step()print(loss.item())

在异构图中预测已有边的类型

分类一个图中已存在的边属于哪个类型是一个非常常见的任务。例如,根据前面的异构图样例数据, 我们的任务是给定一条连接 user 节点和 item 节点的边,预测它的类型是 click 还是其他类别。 这个例子是评分预测的简化版,在推荐场景中很常见。

边类型预测的第一步仍然是计算节点表示。可以通过类似 节点分类的RGCN模型 获得。第二步是计算边上的预测值。 在这里可以复用上述提到的 HeteroDotProductPredictor

这里需要注意的是输入的图数据不能包含边的类型信息, 因此需要将所要预测的边类型(如 click 和 “其他类别”)合并成一种边的图, 并为每条边计算出每种边类型的可能得分。下面的例子使用一个拥有 user 和 item 两种节点类型和一种边类型的图。该边类型是通过合并所有从 user 到 item 的边类型(如 like 和 dislike)得到。 用户可以很方便地用关系切片的方式创建这个图:

dec_graph = hetero_graph['user', :, 'item']
print(dec_graph)
"""
Graph(num_nodes={'user': 1000, 'item': 500},num_edges={('user', 'click+dislike', 'item'): 5500},metagraph=[('user', 'item', 'click+dislike')])
"""

我们可以得到一个异构图,它具有 user 和 item 两种节点类型, 以及把它们之间的所有边的类型进行合并后的单一边类型。

由于上面这行代码将原来的边类型存成边特征 dgl.ETYPE,用户可以将它作为标签使用。

edge_label = dec_graph.edata[dgl.ETYPE]
print(edge_label)
# tensor([2, 2, 2,  ..., 3, 3, 3])print(set(edge_label.numpy()))
# {2, 3} 只有两类边click和dislike

将上述图作为边类型预测模块的输入,用户可以按如下方式编写预测模块:

class HeteroMLPPredictor(nn.Module):def __init__(self, in_dims, n_classes):super().__init__()self.W = nn.Linear(in_dims * 2, n_classes)def apply_edges(self, edges):x = torch.cat([edges.src['h'], edges.dst['h']], 1)y = self.W(x)return {'score': y}def forward(self, graph, h):# h是异构图的每种类型的边的节点表示with graph.local_scope():graph.ndata['h'] = h   #一次性为所有节点类型的'h'赋值graph.apply_edges(self.apply_edges)return graph.edata['score']class Model(nn.Module):def __init__(self, in_features, hidden_features, out_features, rel_names):super().__init__()self.sage = RGCN(in_features, hidden_features, out_features, rel_names)self.pred = HeteroMLPPredictor(out_features, len(rel_names))def forward(self, g, x, dec_graph):"""输入了两个图,一个是双向的完整graph,一个是我们想要预测边类型的dec_graph对于双向graph,由于源节点也是目标节点,所以h即为所有节点的特征dec_graph是单向二分图,获得h这个特征表达后,再进行边分类这些操作是为了帮助我们了解边分类其实这个模型是无意义的,因为g中就已经知道了分类,我们却用g去作为节点特征计算的输入"""h = self.sage(g, x)return self.pred(dec_graph, h)

训练过程如下:

# hetero_graph.etypes:
# ['clicked-by', 'disliked-by', 'click', 'dislike', 'follow', 'followed-by']
model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
node_features = {'user': user_feats, 'item': item_feats}opt = torch.optim.Adam(model.parameters())
for epoch in range(10):logits = model(hetero_graph, node_features, dec_graph)# print(logits.size()) # torch.Size([5500, 6])loss = F.cross_entropy(logits, edge_label)opt.zero_grad()loss.backward()opt.step()print(loss.item())

链接预测

概述

在某些场景中,用户可能希望预测给定节点之间是否存在边,这样的任务称作 链接预测 任务。

基于GNN的链接预测模型的基本思想是通过使用所需预测的节点对 u,vu,vu,v 的节点表示hu(l)h_{u}^{(l)}hu(l)hv(l)h_{v}^{(l)}hv(l),计算它们之间存在链接可能性的得分 yu,vy_{u,v}yu,vyu,v=ϕ(hu(l),hv(l))y_{u,v}=\phi(h_{u}^{(l)},h_{v}^{(l)})yu,v=ϕ(hu(l),hv(l))训练一个链接预测模型涉及到比对两个相连接节点之间的得分与任意一对节点之间的得分的差异。 例如,给定一条连接 uuuvvv 的边,一个好的模型希望 uuuvvv 之间的得分要高于 uuu 和从一个任意的噪声分布 v′∼Pn(v)v'\sim Pn(v)vPn(v) 中所采样的节点 v′v'v 之间的得分。 这样的方法称作 负采样

许多损失函数都可以实现上述目标,包括但不限于:

  • 交叉熵损失:L=−log[σ(yu,v)]−∑vi∼Pn(v)log[1−σ(yu,vi)],i=1,..,kL=-log[\sigma(y_{u,v})]-\sum_{v_{i}\sim Pn(v)}log[1-\sigma(y_{u,v_{i}})],\, i=1,..,kL=log[σ(yu,v)]viPn(v)log[1σ(yu,vi)],i=1,..,k
  • 间隔损失:L=∑vi∼Pn(v)max(0,M−yu,v+yu,vi),i=1,..,kL=\sum_{v_{i}\sim Pn(v)}max(0,M-y_{u,v}+y_{u,v_{i}}),\, i=1,..,kL=viPn(v)max(0,Myu,v+yu,vi),i=1,..,k其中,MMM是一个控制间隔的常数;

计算 uuuvvv 之间分数的神经网络模型与前面的边分类/回归中所述的边回归模型相同。

比如我们可以使用点积计算边的得分:

import torch.nn as nn
import dgl.function as fnclass DotProductPredictor(nn.Module):def forward(self, graph, h):# h是节点的特征with graph.local_scope():graph.ndata['h'] = hgraph.apply_edges(fn.u_dot_v('h', 'h', 'score'))return graph.edata['score']

模型训练

因为上述的得分预测模型在图上进行计算,用户需要将负采样的样本表示为另外一个图, 其中包含所有负采样的节点对作为边。

下面的例子展示了将负采样的样本表示为一个图。每一条边 (u,v)(u,v)(u,v) 都有 kkk 个对应的负采样样本 (u,vi)(u,v_{i})(u,vi),其中 viv_{i}vi 是从均匀分布中采样的:

import dgl
import torchdef construct_negative_graph(graph, k):"""用负采样样本生成一个图,每个源节点连接到k个负样本目标节点"""src, dst = graph.edges()"""torch.repeat_interleave(input, repeats, dim=None) → Tensorinput (类型:torch.Tensor):输入张量repeats(类型:int或torch.Tensor):每个元素的重复次数。repeats参数会被广播来适应输入张量的维度dim(类型:int)需要重复的维度。默认情况下,将把输入张量展平(flatten)为向量,然后将每个元素重复repeats次,并返回重复后的张量>>> x = torch.tensor([1, 2, 3])>>> x.repeat_interleave(2)tensor([1, 1, 2, 2, 3, 3])# 传入多维张量,默认`展平`>>> y = torch.tensor([[1, 2], [3, 4]])>>> torch.repeat_interleave(y, 2)tensor([1, 1, 2, 2, 3, 3, 4, 4])"""neg_src = src.repeat_interleave(k)# 目标节点数还是graph.num_nodes()neg_dst = torch.randint(0, graph.num_nodes(), (len(src) * k,))# 返回的graph节点数不变,但是边的数量变成原来的k倍return dgl.graph((neg_src, neg_dst), num_nodes=graph.num_nodes())

预测边得分的模型和边分类/回归模型中的预测边得分模型类似:

from dgl.nn.pytorch import SAGEConvclass Model(nn.Module):def __init__(self, in_features, out_features):super().__init__()self.sage = SAGEConv(in_features, out_features, aggregator_type='mean')self.pred = DotProductPredictor()def forward(self, g, neg_g, x):h = self.sage(g, x)return self.pred(g, h), self.pred(neg_g, h)

训练的循环部分里会重复构建负采样图并计算损失函数值:

def compute_loss(pos_score, neg_score):# 间隔损失n_edges = pos_score.shape[0]# 广播计算: 1 - pos_score + neg_score.view(n_edges, -1)return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()# 数据加载
from dgl.data import CiteseerGraphDatasetdataset = CiteseerGraphDataset()
graph = dataset[0]node_features = graph.ndata['feat']
node_labels = graph.ndata['label']
train_mask = graph.ndata['train_mask']
valid_mask = graph.ndata['val_mask']
test_mask = graph.ndata['test_mask']n_features = node_features.shape[1]
k = 5
model = Model(n_features, 100)
opt = torch.optim.Adam(model.parameters())
for epoch in range(100):negative_graph = construct_negative_graph(graph, k)pos_score, neg_score = model(graph, negative_graph, node_features)# print(pos_score.size()) [9228, 1]# print(neg_score.size()) [46140=9228*5, 1]# print(neg_score.view(9228, -1).size()) [9228, 5]# x=(1 - pos_score + neg_score.view(9228, -1)).clamp(min=0)# print(x.size()) [9228, 5]loss = compute_loss(pos_score, neg_score)opt.zero_grad()loss.backward()opt.step()print(loss.item())

关于点积前的节点embedding获得,在实际应用中,有着许多计算节点embedding的方法,例如,训练下游任务的分类器。

异构图上的链接预测模型训练

异构图上的链接预测和同构图上的链接预测没有太大区别。下文是在一种边类型上进行预测, 用户可以很容易地将其拓展为对多种边类型上进行预测。

例如,为某一种边类型,用户可以重复使用 异构图上的边预测模型的训练 里的 HeteroDotProductPredictor 来计算节点间存在连接可能性的得分。

import torch.nn as nn
import dgl.function as fnclass HeteroDotProductPredictor(nn.Module):def forward(self, graph, h, etype):# h是异构图的每种类型的边所计算的节点表示with graph.local_scope():graph.ndata['h'] = hgraph.apply_edges(fn.u_dot_v('h', 'h', 'score'), etype=etype)return graph.edges[etype].data['score']

要执行负采样时,用户可以对要进行链接预测的边类型构造一个负采样图:

import torch
import dgldef construct_negative_graph(graph, k, etype):"""针对某个类型的边构建负采样graph"""utype, _, vtype = etypesrc, dst = graph.edges(etype=etype)neg_src = src.repeat_interleave(k)neg_dst = torch.randint(0, graph.num_nodes(vtype), (len(src) * k,))return dgl.heterograph({etype: (neg_src, neg_dst)},num_nodes_dict={ntype: graph.num_nodes(ntype) for ntype in graph.ntypes})

该模型与异构图上边分类的模型有些不同,因为用户需要指定在哪种边类型上进行链接预测:

class Model(nn.Module):def __init__(self, in_features, hidden_features, out_features, rel_names):super().__init__()self.sage = RGCN(in_features, hidden_features, out_features, rel_names)self.pred = HeteroDotProductPredictor()def forward(self, g, neg_g, x, etype):h = self.sage(g, x)return self.pred(g, h, etype), self.pred(neg_g, h, etype)

训练的循环部分和同构图时一致:

def compute_loss(pos_score, neg_score):# 间隔损失n_edges = pos_score.shape[0]return (1 - pos_score + neg_score.view(n_edges, -1)).clamp(min=0).mean()k = 5
model = Model(10, 20, 5, hetero_graph.etypes)
user_feats = hetero_graph.nodes['user'].data['feature']
item_feats = hetero_graph.nodes['item'].data['feature']
node_features = {'user': user_feats, 'item': item_feats}
opt = torch.optim.Adam(model.parameters())
for epoch in range(10):negative_graph = construct_negative_graph(hetero_graph, k, ('user', 'click', 'item'))pos_score, neg_score = model(hetero_graph, negative_graph, node_features, ('user', 'click', 'item'))loss = compute_loss(pos_score, neg_score)opt.zero_grad()loss.backward()opt.step()print(loss.item())

整图分类

概述

许多场景中的图数据是由多个图组成,而不是单个的大图数据。例如不同类型的人群社区。 通过用图刻画同一社区里人与人间的友谊,可以得到多张用于分类的图。 在这个场景里,整图分类模型可以识别社区的类型,即根据结构和整体信息对图进行分类。

整图分类与节点分类或链接预测的主要区别是:预测结果刻画了整个输入图的属性。 与之前的任务类似,用户还是在节点或边上进行消息传递。但不同的是,整图分类任务还需要得到整个图的表示。整图分类的处理流程如下图所示:

从左至右,一般流程是:

  • 准备一个批次的图;
  • 在这个批次的图上进行消息传递以更新节点或边的特征;
  • 将一张图里的节点或边特征聚合成整张图的图表示;
  • 根据任务设计分类层;

整图分类任务通常需要在很多图上进行训练。如果用户在训练模型时一次仅使用一张图,训练效率会很低。 借用深度学习实践中常用的小批次训练方法,用户可将多张图组成一个批次,在整个图批次上进行一次训练迭代。

使用DGL,用户可将一系列的图建立成一个图批次。一个图批次可以被看作是一张大图,图中的每个连通子图对应一张原始小图:

需要注意,DGL里对图进行变换的函数会去掉图上的批次信息。用户可以通过 dgl.DGLGraph.set_batch_num_nodes()dgl.DGLGraph.set_batch_num_edges() 两个函数在变换后的图上重新加入批次信息。

数据集中的每一张图都有它独特的结构和节点与边的特征。为了完成单个图的预测,通常会聚合并汇总单个图尽可能多的信息。 这类操作叫做“Readout”。常见的聚合方法包括:对所有节点或边特征求和、取平均值、逐元素求最大值或最小值。

给定一张图 ggg,对它所有节点特征取平均值的聚合readout公式如下:hg=1∣V∣∑v∈Vhvh_{g}=\frac{1}{|V|}\sum_{v\in V}h_{v}hg=V1vVhv其中,hgh_{g}hg是图ggg的表达,VVVggg中节点的集合,hvh_{v}hv是节点vvv的特征。

DGL内置了常见的readout函数,例如 dgl.readout_nodes() 就实现了上述的平均值读出计算。

在得到 hgh_{g}hg 后,用户可将其传给一个多层感知机(MLP)来获得分类输出。

模型定义

模型的输入是带节点和边特征的批次化图。需要注意的是批次化图中的节点和边属性没有批次大小对应的维度。 模型中应特别注意以下几点:

  • 首先,一个批次中不同的图是完全分开的,即任意两个图之间没有边连接。根据这个良好的性质,所有消息传递函数(的计算)仍然具有相同的结果。
  • 其次,readout函数会分别作用在图批次中的每张图上。假设批次大小为 BBB,聚合后的图特征大小为 DDD, 则readout后的张量形状为 (B,D)(B,D)(B,D)
import dgl
import torchg1 = dgl.graph(([0, 1], [1, 0]))
g1.ndata['h'] = torch.tensor([1., 2.])
g2 = dgl.graph(([0, 1], [1, 2]))
g2.ndata['h'] = torch.tensor([1., 2., 3.])dgl.readout_nodes(g1, 'h')
# tensor([3.])  # 1 + 2bg = dgl.batch([g1, g2])
dgl.readout_nodes(bg, 'h')
# tensor([3., 6.])  # [1 + 2, 1 + 2 + 3]

最后,批次化图中的每个节点或边特征张量均通过将所有图上的相应特征拼接得到:

bg.ndata['h']
# tensor([1., 2., 1., 2., 3.])

了解了上述计算规则后,用户可以定义一个非常简单的模型:

import dgl.nn.pytorch as dglnn
import torch.nn as nn
import torch.nn.functional as Fclass Classifier(nn.Module):def __init__(self, in_dim, hidden_dim, n_classes):super(Classifier, self).__init__()self.conv1 = dglnn.GraphConv(in_dim, hidden_dim)self.conv2 = dglnn.GraphConv(hidden_dim, hidden_dim)self.classify = nn.Linear(hidden_dim, n_classes)def forward(self, g, h):# 应用图卷积和激活函数h = F.relu(self.conv1(g, h))h = F.relu(self.conv2(g, h))with g.local_scope():g.ndata['h'] = h# 使用平均读出计算图表示hg = dgl.mean_nodes(g, 'h')return self.classify(hg)

模型训练

由于整图分类处理的是很多相对较小的图,而不是一个大图, 因此通常可以在随机抽取的小批次图上进行高效的训练,而无需设计复杂的图采样算法。

加载dgl中的整图分类数据集:

import dgl.data
dataset = dgl.data.GINDataset('MUTAG', False)

整图分类数据集里的每个样本是一个图和它对应标签的元组。为提升数据加载速度, 用户可以调用GraphDataLoader,从而以小批次遍历整个图数据集:

from dgl.dataloading import GraphDataLoaderdataloader = GraphDataLoader(dataset,batch_size=32,drop_last=False,shuffle=True)

训练过程包括遍历dataloader和更新模型参数的部分:

model = Classifier(7, 20, 5)
opt = torch.optim.Adam(model.parameters())
for epoch in range(20):for batched_graph, labels in dataloader:feats = batched_graph.ndata['attr']logits = model(batched_graph, feats)loss = F.cross_entropy(logits, labels)opt.zero_grad()loss.backward()opt.step()print(loss.item())

异构图的整图分类模型

在异构图上做整图分类和在同构图上做整图分类略有不同。用户除了需要使用异构图卷积模块,还需要在读出函数中聚合不同类别的节点。

以下代码演示了如何对每种节点类型的节点表达取平均值并求和:

import torch.nn as nn
import dgl.nn.pytorch as dglnn
import torch.nn.functional as Fclass RGCN(nn.Module):def __init__(self, in_feats, hid_feats, out_feats, rel_names):super().__init__()self.conv1 = dglnn.HeteroGraphConv({rel: dglnn.GraphConv(in_feats, hid_feats)for rel in rel_names}, aggregate='sum')self.conv2 = dglnn.HeteroGraphConv({rel: dglnn.GraphConv(hid_feats, out_feats)for rel in rel_names}, aggregate='sum')def forward(self, graph, inputs):# inputs是节点的特征h = self.conv1(graph, inputs)h = {k: F.relu(v) for k, v in h.items()}h = self.conv2(graph, h)return hclass HeteroClassifier(nn.Module):def __init__(self, in_dim, hidden_dim, n_classes, rel_names):super().__init__()self.rgcn = RGCN(in_dim, hidden_dim, hidden_dim, rel_names)self.classify = nn.Linear(hidden_dim, n_classes)def forward(self, g):h = g.ndata['feat']h = self.rgcn(g, h)with g.local_scope():g.ndata['h'] = h# 通过平均readout值求和来计算单图的表征hg = 0for ntype in g.ntypes:hg = hg + dgl.mean_nodes(g, 'h', ntype=ntype)return self.classify(hg)

训练过程与之前一样,下面代码仅作演示(没有异构图整图分类数据,不能运行):

# etypes是一个列表,元素是规范边类型
model = HeteroClassifier(10, 20, 5, etypes)
opt = torch.optim.Adam(model.parameters())
for epoch in range(20):for batched_graph, labels in dataloader:logits = model(batched_graph)loss = F.cross_entropy(logits, labels)opt.zero_grad()loss.backward()opt.step()

第二十二课.DeepGraphLibrary(三)相关推荐

  1. OpenGL教程翻译 第二十二课 使用Assimp加载模型

    第二十二课 使用Assimp加载模型 原文地址:http://ogldev.atspace.co.uk/(源码请从原文主页下载) 背景 到现在为止我们都在使用手动生成的模型.正如你所想的,指明每个顶点 ...

  2. 第二十二课php注入,第二十二课 生命的痛苦及其解脱

    第二十二课:生命的痛苦及其解脱 导师做了两个开示: 一.造成生命痛苦的原因 二.获得幸福的方法 上节课导师开示过,以迷惑和烦恼为基础的人生是痛苦的,而以觉醒和觉性为基础的人生则是幸福快乐的. 所以这节 ...

  3. 第二十二课.XGBoost

    目录 模型公式 优化算法 目标函数 树的生成 预测值的确定 特征空间的划分 使用 XGBoost 实现波士顿房价预测 使用 XGBoost 完成乳腺癌诊断的二分类问题 模型公式 XGBoost 在集成 ...

  4. (转)学习淘淘商城第二十二课(KindEditor富文本编辑器的使用)

    http://blog.csdn.net/u012453843/article/details/70184155 上节课我们一起学习了怎样解决KindEditor富文本编辑器上传图片的浏览器兼容性问题 ...

  5. 【问链财经-区块链基础知识系列】 第二十二课 贸易金融区块链平台的技术机理与现实意义

    简介:贸易金融区块链平台的技术机理.模式.优势与现实意义都有哪些?对湾区贸易金融区块链平台的未来建设有何展望?本文将进行详述. 小微企业贡献了我国60%以上的GDP.50%以上的税收以及80%的城镇就 ...

  6. 新版标准日本语中级_第二十二课

    语法   1. 电话中敬语的使用方法:日语中,在与外人说话时,对话题中涉及到的自己人不用敬语. 对公司内的自己人说话时,用山田課長は出張中です.山田課長は出張されています. 和外人说时,则要用(課長の ...

  7. AGG第二十二课 conv_contour函数auto_detect_orientation的字体应用

    1 提供如下的代码结构渲染字体 agg::conv_transform<...> conv (path,matrix); agg::conv_curve<...> curve ...

  8. 重学java基础第二十二课:IDEA安装

  9. PS教程第二十二课:羽化选区

最新文章

  1. Linux下同步模式、异步模式、阻塞调用、非阻塞调用总结
  2. solidity 汇编语言问题——内存数据mload时为什么从第32位开始
  3. 创建型模式之FactoryMethod
  4. ML_Multiple Linear Regression
  5. keras 香草编码器_完善纯香草javascript中的拖放
  6. 一步一步的写出你自己的makefile文件
  7. Android视图组成View
  8. 【终极办法】windows下安装完MySQL,为什么cmd不识别命令?
  9. NOIP2014洛谷P2296:寻找道路(bfs)
  10. 青岛理工大学c语言软件,青岛理工大学C语言程序打印版.docx
  11. 准考证打印系统关闭怎么办_2021国家公务员考试准考证打印系统关闭了怎么办...
  12. Python教学视频(五)顺序结构练习
  13. 设计模式--状态模式(C语言实现)
  14. Python爬虫理论 | (4) 数据存储
  15. 快手科技2020年总收入人民币588亿元,同比增长50.2%
  16. Springboot启动流程简述
  17. 小菊的语义分割3——数据预处理及像素级分类实现原理
  18. 括弧匹配检验(C语言)
  19. wimlib-imagex.exe、DISM、WIMGAPI三种方式应用WIM的速度对比
  20. Qt编写可视化大屏电子看板系统1-布局方案

热门文章

  1. Java基础篇:面向对象
  2. 万字长文带你全面认识 Kubernetes 中如何实现蓝绿部署、金丝雀发布和滚动更新...
  3. 我又被学弟学妹倒挂了
  4. 阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!
  5. 别再用 kill -9 了,这才是微服务上下线的正确姿势!
  6. 干掉Spring Cloud,这个框架是微服务的未来!
  7. 大厂程序员追求深圳女老师被拉黑!原因你想不到!
  8. 换种监控姿势:基于深度学习+流处理的时序告警系统
  9. 各大厂这个档次分配,大佬们有什么看法?
  10. 是时候取消Sprint评审会议了吗?