图结构数据
一、图的表示
定义一(图):
- 一个图被记为 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其中 V = { v 1 , … , v N } \mathcal{V}=\left\{v_{1}, \ldots, v_{N}\right\} V={v1,…,vN}是数量为 N = ∣ V ∣ N=|\mathcal{V}| N=∣V∣ 的结点的集合, E = { e 1 , … , e M } \mathcal{E}=\left\{e_{1}, \ldots, e_{M}\right\} E={e1,…,eM} 是数量为 M M M 的边的集合。
- 图用节点表示实体(entities ),用边表示实体间的关系(relations)。
- 节点和边的信息可以是类别型的(categorical),类别型数据的取值只能是哪一类别。一般称类别型的信息为标签(label)。
- 节点和边的信息可以是数值型的(numeric),类别型数据的取值范围为实数。一般称类别型的信息为属性(attribute)。
- 大部分情况中,节点含有信息,边可能含有信息。
定义二(图的邻接矩阵):
-
给定一个图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其对应的邻接矩阵被记为 A ∈ { 0 , 1 } N × N \mathbf{A} \in\{0,1\}^{N \times N} A∈{0,1}N×N。 A i , j = 1 \mathbf{A}_{i, j}=1 Ai,j=1表示存在从结点 v i v_i vi到 v j v_j vj的边,反之表示不存在从结点 v i v_i vi到 v j v_j vj的边。
-
在无向图中,从结点 v i v_i vi到 v j v_j vj的边存在,意味着从结点 v j v_j vj到 v i v_i vi的边也存在。因而无向图的邻接矩阵是对称的。
-
在无权图中,各条边的权重被认为是等价的,即认为各条边的权重为 1 1 1。
-
对于有权图,其对应的邻接矩阵通常被记为 W ∈ { 0 , 1 } N × N \mathbf{W} \in\{0,1\}^{N \times N} W∈{0,1}N×N,其中 W i , j = w i j \mathbf{W}_{i, j}=w_{ij} Wi,j=wij表示从结点 v i v_i vi到 v j v_j vj的边的权重。若边不存在时,边的权重为 0 0 0。
一个无向无权图的例子:
其邻接矩阵为:
A = ( 0 1 0 1 1 1 0 1 0 0 0 1 0 0 1 1 0 0 0 1 1 0 1 1 0 ) \mathbf{A}=\left(\begin{array}{lllll} 0 & 1 & 0 & 1 & 1 \\ 1 & 0 & 1 & 0 & 0 \\ 0 & 1 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 & 1 \\ 1 & 0 & 1 & 1 & 0 \end{array}\right) A=⎝⎜⎜⎜⎜⎛0101110100010011000110110⎠⎟⎟⎟⎟⎞
二、图的属性
定义三(结点的度,degree):
- 对于有向有权图,结点 v i v_i vi的出度(out degree)等于从 v i v_i vi出发的边的权重之和,结点 v i v_i vi的入度(in degree)等于从连向 v i v_i vi的边的权重之和。
- 无向图是有向图的特殊情况,结点的出度与入度相等。
- 无权图是有权图的特殊情况,各边的权重为 1 1 1,那么结点 v i v_i vi的出度(out degree)等于从 v i v_i vi出发的边的数量,结点 v i v_i vi的入度(in degree)等于从连向 v i v_i vi的边的数量。
- 结点 v i v_i vi的度记为 d ( v i ) d(v_i) d(vi),入度记为 d i n ( v i ) d_{in}(v_i) din(vi),出度记为 d o u t ( v i ) d_{out}(v_i) dout(vi)。
定义四(邻接结点,neighbors):
- 结点 v i v_i vi的邻接结点为与结点 v i v_i vi直接相连的结点,其被记为** N ( v i ) \mathcal{N(v_i)} N(vi)**。
- **结点 v i v_i vi的 k k k跳远的邻接节点(neighbors with k k k-hop)**指的是到结点 v i v_i vi要走 k k k步的节点(一个节点的 2 2 2跳远的邻接节点包含了自身)。
定义五(行走,walk):
- w a l k ( v 1 , v 2 ) = ( v 1 , e 6 , e 5 , e 4 , e 1 , v 2 ) walk(v_1, v_2) = (v_1, e_6,e_5,e_4,e_1,v_2) walk(v1,v2)=(v1,e6,e5,e4,e1,v2),这是一次“行走”,它是一次从节点 v 1 v_1 v1出发,依次经过边 e 6 , e 5 , e 4 , e 1 e_6,e_5,e_4,e_1 e6,e5,e4,e1,最终到达节点 v 2 v_2 v2的“行走”。
- 下图所示为 w a l k ( v 1 , v 2 ) = ( v 1 , e 6 , e 5 , e 4 , e 1 , v 2 ) walk(v_1, v_2) = (v_1, e_6,e_5,e_4,e_1,v_2) walk(v1,v2)=(v1,e6,e5,e4,e1,v2),其中红色数字标识了边的访问序号。
- 在“行走”中,节点是运行重复的。
定理六:
- 有一图,其邻接矩阵为 A \mathbf{A} A, A n \mathbf{A}^{n} An为邻接矩阵的 n n n次方,那么 A n [ i , j ] \mathbf{A}^{n}[i,j] An[i,j]等于从结点 v i v_i vi到结点 v j v_j vj的长度为 n n n的行走的个数。
定义七(路径,path):
- “路径”是结点不可重复的“行走”。
定义八(子图,subgraph):
- 有一图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},另有一图 G ′ = { V ′ , E ′ } \mathcal{G}^{\prime}=\{\mathcal{V}^{\prime}, \mathcal{E}^{\prime}\} G′={V′,E′},其中 V ′ ∈ V \mathcal{V}^{\prime} \in \mathcal{V} V′∈V, E ′ ∈ E \mathcal{E}^{\prime} \in \mathcal{E} E′∈E并且 V ′ \mathcal{V}^{\prime} V′不包含 E ′ \mathcal{E}^{\prime} E′中未出现过的结点,那么 G ′ \mathcal{G}^{\prime} G′是 G \mathcal{G} G的子图。
定义九(连通分量,connected component):
-
给定图 G ′ = { V ′ , E ′ } \mathcal{G}^{\prime}=\{\mathcal{V}^{\prime}, \mathcal{E}^{\prime}\} G′={V′,E′}是图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E}的子图。记属于图 G \mathcal{G} G但不属于 G ′ \mathcal{G}^{\prime} G′图的结点集合记为 V / V ′ \mathcal{V}/\mathcal{V}^{\prime} V/V′ 。如果属于 V ′ \mathcal{V}^{\prime} V′的任意结点对之间存在至少一条路径,但不存在一条边连接属于 V ′ \mathcal{V}^{\prime} V′的结点与属于 V / V ′ \mathcal{V}/\mathcal{V}^{\prime} V/V′的结点,那么图 G ′ \mathcal{G}^{\prime} G′是图 G \mathcal{G} G的连通分量。
左右两边子图都是整图的连通分量。
定义十(连通图,connected graph):
- 当一个图只包含一个连通分量,即其自身,那么该图是一个连通图。
定义十一(最短路径,shortest path):
-
v
s
,
v
t
∈
V
v_{s}, v_{t} \in \mathcal{V}
vs,vt∈V 是图
G
=
{
V
,
E
}
\mathcal{G}=\{\mathcal{V}, \mathcal{E}\}
G={V,E}上的一对结点,结点对
v
s
,
v
t
∈
V
v_{s}, v_{t} \in \mathcal{V}
vs,vt∈V之间所有路径的集合记为
P
s
t
\mathcal{P}_{\mathrm{st}}
Pst。结点对
v
s
,
v
t
v_{s}, v_{t}
vs,vt之间的最短路径
p
s
t
s
p
p_{\mathrm{s} t}^{\mathrm{sp}}
pstsp为
P
s
t
\mathcal{P}_{\mathrm{st}}
Pst中长度最短的一条路径,其形式化定义为
p s t s p = arg min p ∈ P s t ∣ p ∣ p_{\mathrm{s} t}^{\mathrm{sp}}=\arg \min _{p \in \mathcal{P}_{\mathrm{st}}}|p| pstsp=argp∈Pstmin∣p∣
其中, p p p表示 P s t \mathcal{P}_{\mathrm{st}} Pst中的一条路径, ∣ p ∣ |p| ∣p∣是路径 p p p的长度。
定义十二(直径,diameter):
- 给定一个连通图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其直径为其所有结点对之间的最短路径的最小值,形式化定义为
diameter ( G ) = max v s , v t ∈ V min p ∈ P s t ∣ p ∣ \operatorname{diameter}(\mathcal{G})=\max _{v_{s}, v_{t} \in \mathcal{V}} \min _{p \in \mathcal{P}_{s t}}|p| diameter(G)=vs,vt∈Vmaxp∈Pstmin∣p∣
定义十三(拉普拉斯矩阵,Laplacian Matrix):
- 给定一个图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其邻接矩阵为 A A A,其拉普拉斯矩阵定义为 L = D − A \mathbf{L=D-A} L=D−A,其中 D = d i a g ( d ( v 1 ) , ⋯ , d ( v N ) ) \mathbf{D=diag(d(v_1), \cdots, d(v_N))} D=diag(d(v1),⋯,d(vN))。
定义十四(对称归一化的拉普拉斯矩阵,Symmetric normalized Laplacian):
- 给定一个图 G = { V , E } \mathcal{G}=\{\mathcal{V}, \mathcal{E}\} G={V,E},其邻接矩阵为 A A A,其规范化的拉普拉斯矩阵定义为
L = D − 1 2 ( D − A ) D − 1 2 = I − D − 1 2 A D − 1 2 \mathbf{L=D^{-\frac{1}{2}}(D-A)D^{-\frac{1}{2}}=I-D^{-\frac{1}{2}}AD^{-\frac{1}{2}}} L=D−21(D−A)D−21=I−D−21AD−21
三、图的种类
- 同质图(Homogeneous Graph):只有一种类型的节点和一种类型的边的图。
- 异质图(Heterogeneous Graph):存在多种类型的节点和多种类型的边的图。
- 二部图(Bipartite Graphs):节点分为两类,只有不同类的节点之间存在边。
四、图结构数据上的机器学习
- 节点预测:预测节点的类别或某类属性的取值
- 例子:对是否是潜在客户分类、对游戏玩家的消费能力做预测
- 边预测:预测两个节点间是否存在链接
- 例子:Knowledge graph completion、好友推荐、商品推荐
- 图的预测:对不同的图进行分类或预测图的属性
- 例子:分子属性预测
- 节点聚类:检测节点是否形成一个社区
- 例子:社交圈检测
- 其他任务
- 图生成:例如药物发现
- 图演变:例如物理模拟
- ……
五、应用神经网络于图面临的挑战
在学习了简单的图论知识,我们再来回顾应用神经网络于图面临的挑战。
过去的深度学习应用中,我们主要接触的数据形式主要是这四种:矩阵、张量、序列(sequence)和时间序列(time series),它们都是规则的结构化的数据。然而图数据是非规则的非结构化的,它具有以下的特点:
- 任意的大小和复杂的拓扑结构;
- 没有固定的结点排序或参考点;
- 通常是动态的,并具有多模态的特征;
- 图的信息并非只蕴含在节点信息和边的信息中,图的信息还包括了图的拓扑结构。
以往的深度学习技术是为规则且结构化的数据设计的,无法直接用于图数据。应用于图数据的神经网络,要求
- 适用于不同度的节点;
- 节点表征的计算与邻接节点的排序无关;
- 不但能够根据节点信息、邻接节点的信息和边的信息计算节点表征,还能根据图拓扑结构计算节点表征。下面的图片展示了一个需要根据图拓扑结构计算节点表征的例子。图片中展示了两个图,它们同样有俩黄、俩蓝、俩绿,共6个节点,因此它们的节点信息相同;假设边两端节点的信息为边的信息,那么这两个图有一样的边,即它们的边信息相同。但这两个图是不一样的图,它们的拓扑结构不一样。
六、结语
在此篇文章中,我们学习了简单的图论知识。更多的图论知识可以参阅参考文献“Chapter 2 - Foundations of Graphs, Deep Learning on Graphs”。
参考资料
环境配置与PyG中图与图数据集的表示和使用
一、引言
PyTorch Geometric (PyG)是面向几何深度学习的PyTorch的扩展库,几何深度学习指的是应用于图和其他不规则、非结构化数据的深度学习。基于PyG库,我们可以轻松地根据数据生成一个图对象,然后很方便的使用它;我们也可以容易地为一个图数据集构造一个数据集类,然后很方便的将它用于神经网络。
通过此节的实践内容,我们将
- 首先学习程序运行环境的配置。
- 接着学习PyG中图数据的表示及其使用,即学习PyG中
Data
类。 - 最后学习PyG中图数据集的表示及其使用,即学习PyG中
Dataset
类。
二、环境配置
-
使用
nvidia-smi
命令查询显卡驱动是否正确安装
-
安装正确版本的pytorch和cudatoolkit,此处安装1.8.1版本的pytorch和11.1版本的cudatoolkit
conda install pytorch torchvision torchaudio cudatoolkit=11.1 -c pytorch -c nvidia
或
pip3 install torch==1.8.1+cu111 torchvision==0.9.1+cu111 torchaudio===0.8.1 -f https://download.pytorch.org/whl/torch_stable.html
- 确认是否正确安装,正确的安装应出现下方的结果
$ python -c "import torch; print(torch.__version__)" # 1.8.1 $ python -c "import torch; print(torch.version.cuda)" # 11.1
-
安装正确版本的PyG
pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html pip install torch-cluster -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html pip install torch-spline-conv -f https://pytorch-geometric.com/whl/torch-1.8.0+cu111.html pip install torch-geometric
其他版本的安装方法以及安装过程中出现的大部分问题的解决方案可以在Installation of of PyTorch Geometric 页面找到。
三、Data
类——PyG中图的表示及其使用
Data
对象的创建
Data
类的官方文档为torch_geometric.data.Data。
通过构造函数
Data
类的构造函数:
class Data(object):
def __init__(self, x=None, edge_index=None, edge_attr=None, y=None, **kwargs):
r"""
Args:
x (Tensor, optional): 节点属性矩阵,大小为`[num_nodes, num_node_features]`
edge_index (LongTensor, optional): 边索引矩阵,大小为`[2, num_edges]`,第0行为尾节点,第1行为头节点,头指向尾
edge_attr (Tensor, optional): 边属性矩阵,大小为`[num_edges, num_edge_features]`
y (Tensor, optional): 节点或图的标签,任意大小(,其实也可以是边的标签)
"""
self.x = x
self.edge_index = edge_index
self.edge_attr = edge_attr
self.y = y
for key, item in kwargs.items():
if key == 'num_nodes':
self.__num_nodes__ = item
else:
self[key] = item
edge_index
的每一列定义一条边,其中第一行为边起始节点的索引,第二行为边结束节点的索引。这种表示方法被称为COO格式(coordinate format),通常用于表示稀疏矩阵。PyG不是用稠密矩阵
A
∈
{
0
,
1
}
∣
V
∣
×
∣
V
∣
\mathbf{A} \in \{ 0, 1 \}^{|\mathcal{V}| \times |\mathcal{V}|}
A∈{0,1}∣V∣×∣V∣来持有邻接矩阵的信息,而是用仅存储邻接矩阵
A
\mathbf{A}
A中非
0
0
0元素的稀疏矩阵来表示图。
通常,一个图至少包含x, edge_index, edge_attr, y, num_nodes
5个属性,当图包含其他属性时,我们可以通过指定额外的参数使Data
对象包含其他的属性:
graph = Data(x=x, edge_index=edge_index, edge_attr=edge_attr, y=y, num_nodes=num_nodes, other_attr=other_attr)
转dict
对象为Data
对象
我们也可以将一个dict
对象转换为一个Data
对象:
graph_dict = {
'x': x,
'edge_index': edge_index,
'edge_attr': edge_attr,
'y': y,
'num_nodes': num_nodes,
'other_attr': other_attr
}
graph_data = Data.from_dict(graph_dict)
from_dict
是一个类方法:
@classmethod
def from_dict(cls, dictionary):
r"""Creates a data object from a python dictionary."""
data = cls()
for key, item in dictionary.items():
data[key] = item
return data
注意:graph_dict
中属性值的类型与大小的要求与Data
类的构造函数的要求相同。
Data
对象转换成其他类型数据
我们可以将Data
对象转换为dict
对象:
def to_dict(self):
return {key: item for key, item in self}
或转换为namedtuple
:
def to_namedtuple(self):
keys = self.keys
DataTuple = collections.namedtuple('DataTuple', keys)
return DataTuple(*[self[key] for key in keys])
获取Data
对象属性
x = graph_data['x']
设置Data
对象属性
graph_data['x'] = x
获取Data
对象包含的属性的关键字
graph_data.keys()
对边排序并移除重复的边
graph_data.coalesce()
Data
对象的其他性质
我们通过观察PyG中内置的一个图来查看Data
对象的性质:
from torch_geometric.datasets import KarateClub
dataset = KarateClub()
data = dataset[0] # Get the first graph object.
print(data)
print('==============================================================')
# 获取图的一些信息
print(f'Number of nodes: {data.num_nodes}') # 节点数量
print(f'Number of edges: {data.num_edges}') # 边数量
print(f'Number of node features: {data.num_node_features}') # 节点属性的维度
print(f'Number of node features: {data.num_features}') # 同样是节点属性的维度
print(f'Number of edge features: {data.num_edge_features}') # 边属性的维度
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}') # 平均节点度
print(f'if edge indices are ordered and do not contain duplicate entries.: {data.is_coalesced()}') # 是否边是有序的同时不含有重复的边
print(f'Number of training nodes: {data.train_mask.sum()}') # 用作训练集的节点
print(f'Training node label rate: {int(data.train_mask.sum()) / data.num_nodes:.2f}') # 用作训练集的节点的数量
print(f'Contains isolated nodes: {data.contains_isolated_nodes()}') # 此图是否包含孤立的节点
print(f'Contains self-loops: {data.contains_self_loops()}') # 此图是否包含自环的边
print(f'Is undirected: {data.is_undirected()}') # 此图是否是无向图
四、Dataset
类——PyG中图数据集的表示及其使用
PyG内置了大量常用的基准数据集,接下来我们以PyG内置的Planetoid
数据集为例,来学习PyG中图数据集的表示及使用。
Planetoid
数据集类的官方文档为torch_geometric.datasets.Planetoid。
生成数据集对象并分析数据集
如下方代码所示,在PyG中生成一个数据集是简单直接的。在第一次生成PyG内置的数据集时,程序首先下载原始文件,然后将原始文件处理成包含Data
对象的Dataset
对象并保存到文件。
from torch_geometric.datasets import Planetoid
dataset = Planetoid(root='/dataset/Cora', name='Cora')
# Cora()
len(dataset)
# 1
dataset.num_classes
# 7
dataset.num_node_features
# 1433
分析数据集中样本
可以看到该数据集只有一个图,包含7个分类任务,节点的属性为1433维度。
data = dataset[0]# Data(edge_index=[2, 10556], test_mask=[2708],# train_mask=[2708], val_mask=[2708], x=[2708, 1433], y=[2708])
data.is_undirected()
# True
data.train_mask.sum().item()
# 140
data.val_mask.sum().item()
# 500
data.test_mask.sum().item()
# 1000
现在我们看到该数据集包含的唯一的图,有2708个节点,节点特征为1433维,有10556条边,有140个用作训练集的节点,有500个用作验证集的节点,有1000个用作测试集的节点。PyG内置的其他数据集,请小伙伴一一试验,以观察不同数据集的不同。
数据集的使用
假设我们定义好了一个图神经网络模型,其名为Net
。在下方的代码中,我们展示了节点分类图数据集在训练过程中的使用。
model = Net().to(device)
data = dataset[0].to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
model.train()
for epoch in range(200):
optimizer.zero_grad()
out = model(data)
loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
loss.backward()
optimizer.step()
结语
通过此实践环节,我们学习了程序运行环境的配置、PyG中Data
对象的生成与使用、以及PyG中Dataset
对象的表示和使用。此节内容是图神经网络实践的基础,所涉及的内容是最常用、最基础的,在后面的内容中我们还将学到复杂Data
类的构建,和复杂Dataset
类的构建。
作业
- 请通过继承
Data
类实现一个类,专门用于表示“机构-作者-论文”的网络。该网络包含“机构“、”作者“和”论文”三类节点,以及“作者-机构“和“作者-论文“两类边。对要实现的类的要求:1)用不同的属性存储不同节点的属性;2)用不同的属性存储不同的边(边没有属性);3)逐一实现获取不同节点数量的方法。
from torch_geometric.data import Data
@unique
class MyLabel(Enum):
dept = 0
paper = 1
author = 2
class MyData(Data):
def __init__(self, input_data, **kwargs):
self.data = input_data
super(MyData, self).__init__()
x, edge_index, y, author_dept_edge_index, author_paper_edge_index = self.__create_datasets()
self.author_dept_edge_index = author_paper_edge_index
self.author_paper_edge_index = author_paper_edge_index
super().__init__(x=x, edge_index=edge_index, y=y, **kwargs)
def __add_node(self, node_label_list, node, label):
if node_label_list.count((node, label)) == 0:
# 添加节点
node_label_list.append((node, label))
node_index = node_label_list.index((node, label))
# 返回节点集,节点索引
return node_label_list, node_index
def __create_datasets(self):
node_label_list = []
edge_index = None
author_dept_edge_index = None
author_paper_edge_index = None
for row in self.data.values.tolist():
# 取出三个节点数据
dept = row[0]
paper = row[1]
author = row[2]
# 添加节点,得到节点索引
node_label_list, dept_index = self.__add_node(
node_label_list, dept, MyLabel.dept.value)
node_label_list, paper_index = self.__add_node(
node_label_list, paper, MyLabel.paper.value)
node_label_list, author_index = self.__add_node(
node_label_list, author, MyLabel.author.value)
# 构建作者与机构的关系
author_dept_index = np.array([[author_index, dept_index],
[dept_index, author_index]])
author_dept_edge_index = np.hstack((author_dept_edge_index, author_dept_index)) \
if author_dept_edge_index is not None else author_dept_index
# 构建作者与论文的关系
author_paper_index = np.array([[author_index, paper_index],
[paper_index, author_index]])
author_paper_edge_index = np.hstack((author_paper_edge_index, author_paper_index)) \
if author_paper_edge_index is not None else author_paper_index
# 添加边的索引
edge_index = np.hstack(
(edge_index, author_dept_index)) if edge_index is not None else author_dept_index
edge_index = np.hstack((edge_index, author_paper_index))
nodes = [[node] for node, label in node_label_list]
labels = [[label] for node, label in node_label_list]
x = torch.tensor(nodes)
edge_index = torch.tensor(edge_index, dtype=torch.long)
y = torch.tensor(labels)
return x, edge_index, y, author_dept_edge_index, author_paper_edge_index
@property
def dept_nums(self):
return self.data['dept'].nunique()
@property
def author_nums(self):
return self.data['author'].nunique()
@property
def paper_nums(self):
return self.data['paper'].nunique()
class IPAGNN(Data):
"""
I:Institute;
P:Papers;
A:Authors;
"""
def __init__(self, x_institute=None, x_article=None, x_author=None,
edge_index_author_to_institute=None, edge_index_author_to_article=None,
y=None):
super(IPAGNN, self).__init__(
x=torch.cat((node_attr_institute, node_attr_article, node_attr_author)),
edge_index=torch.cat((edge_index_author_to_institute, edge_index_author_to_article), 1))
# node attritude
self.x_institute = x_institute
self.x_article = x_article
self.x_author = x_author
# edge index
self.edge_index_author_to_institute = edge_index_author_to_institute
self.edge_index_author_to_article = edge_index_author_to_article
# label
self.y = y
@property
def num_nodes_of_institute(self):
return self.x_institute.size(0)
@property
def num_nodes_of_article(self):
return self.x_article.size(0)
@property
def num_nodes_of_author(self):
return self.x_author.size(0)