基于图神经网络的节点表征学习

基于图神经网络的节点表征学习

此文为学习笔记,完整开源内容见DataWhale

引言

基于图神经网络的节点表征学习可以理解为对图神经网络进行基于监督学习的训练,使得图神经网络学会产生高质量的节点表征。

本节中主要学习实现多层图神经网络的方法,以及训练图神经网络产生高质量节点表征并实现高准确性节点分类的一般过程。

在节点预测任务中,一个图上有很多节点,部分节点的标签已知,剩余节点的标签未知。将节点的属性(x)、边的端点信息(edge_index)、边的属性(edge_attr,如果有的话)输入到多层图神经网络,经过图神经网络每一层的一次节点间信息传递,图神经网络为节点生成节点表征。

任务是根据节点的属性(可以是类别型、也可以是数值型)、边的信息、边的属性(如果有的话)、已知的节点预测标签,对未知标签的节点做预测

下面以Cora数据集为例子进行说明,(Cora是一个论文引用网络,节点代表论文,如果两篇论文存在引用关系,那么认为对应的两个节点之间存在边,每个节点由一个1433维的词包特征向量描述。我们的任务是推断每个文档的类别(共7类)。)

主要内容为:

  1. 准备工作:获取并分析数据集构建一个方法用于分析节点表征的分布
  2. 考察MLP用于节点分类的表现,并分析基于MLP学习到的节点表征的分布
  3. 逐一介绍GCN, GAT这两个图神经网络的理论、他们在节点分类任务中的表现以及它们学习到的节点表征的分布
  4. 比较三者在节点表征学习能力上的差异

准备工作

获取并分析数据集

Cora图拥有2,708个节点和10,556条边,平均节点度为3.9。有标签的节点的比例只占到5%。进一步我们可以看到,这个图是无向图,不存在孤立的节点(即每个文档至少有一个引文)。数据转换在将数据输入到神经网络之前修改数据,这一功能可用于实现数据规范化或数据增强

MLP在图节点分类中的应用

理论上,我们应该能够仅根据文件的内容,即它的词包特征表示来推断文件的类别,而无需考虑文件之间的任何关系信息。让我们通过构建一个简单的MLP来验证这一点,该网络只对输入节点的特征进行操作,它在所有节点之间共享权重。

MLP图节点分类器:

import torch
from torch.nn import Linear
import torch.nn.functional as F

class MLP(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(MLP, self).__init__()
        torch.manual_seed(12345)
        self.lin1 = Linear(dataset.num_features, hidden_channels)
        self.lin2 = Linear(hidden_channels, dataset.num_classes)

    def forward(self, x):
        x = self.lin1(x)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.lin2(x)
        return x

model = MLP(hidden_channels=16)
print(model)

我们利用交叉熵损失Adam优化器来训练这个简单的MLP网络。

model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss()  # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)  # Define optimizer.

def train():
    model.train()
    optimizer.zero_grad()  # Clear gradients.
    out = model(data.x)  # Perform a single forward pass.
    loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
    loss.backward()  # Derive gradients.
    optimizer.step()  # Update parameters based on gradients.
    return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

训练完模型后,我们可以进行测试,以检验这个简单的MLP模型在未知标签的节点上表现如何。

def test():
    model.eval()
    out = model(data.x)
    pred = out.argmax(dim=1)  # Use the class with highest probability.
    test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
    test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
    return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

MLP表现相当糟糕,只有大约59%的测试准确性。
MLP表现不好的原因:用于训练此神经网络的有标签节点数量过少,此神经网络被过拟合,它对未见过的节点泛化性很差。

GCN及其在图节点分类任务中的应用

GCN的定义

GCN 神经网络层来源于论文“Semi-supervised Classification with Graph Convolutional Network”,其数学定义为,
X ′ = D ^ − 1 / 2 A ^ D ^ − 1 / 2 X Θ , \mathbf{X}^{\prime} = \mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}} \mathbf{\hat{D}}^{-1/2} \mathbf{X} \mathbf{\Theta}, X=D^1/2A^D^1/2XΘ,
其中 A ^ = A + I \mathbf{\hat{A}} = \mathbf{A} + \mathbf{I} A^=A+I表示插入自环的邻接矩阵, D ^ i i = ∑ j = 0 A ^ i j \hat{D}_{ii} = \sum_{j=0} \hat{A}_{ij} D^ii=j=0A^ij表示其对角线度矩阵。邻接矩阵可以包括不为 1 1 1的值,当邻接矩阵不为{0,1}值时,表示邻接矩阵存储的是边的权重。 D ^ − 1 / 2 A ^ D ^ − 1 / 2 \mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}} \mathbf{\hat{D}}^{-1/2} D^1/2A^D^1/2对称归一化矩阵。

它的节点式表述为:
x i ′ = Θ ∑ j ∈ N ( v ) ∪ { i } e j , i d ^ j d ^ i x j \mathbf{x}^{\prime}_i = \mathbf{\Theta} \sum_{j \in \mathcal{N}(v) \cup \{ i \}} \frac{e_{j,i}}{\sqrt{\hat{d}_j \hat{d}_i}} \mathbf{x}_j xi=ΘjN(v){i}d^jd^i ej,ixj
其中, d ^ i = 1 + ∑ j ∈ N ( i ) e j , i \hat{d}_i = 1 + \sum_{j \in \mathcal{N}(i)} e_{j,i} d^i=1+jN(i)ej,i e j , i e_{j,i} ej,i表示从源节点 j j j到目标节点 i i i的边的对称归一化系数(默认值为1.0)。

PyG中GCNConv 模块说明

GCNConv构造函数接口:

GCNConv(in_channels: int, out_channels: int, improved: bool = False, cached: bool = False, add_self_loops: bool = True, normalize: bool = True, bias: bool = True, **kwargs)

其中:

  • in_channels:输入数据维度;
  • out_channels:输出数据维度;
  • improved:如果为true A ^ = A + 2 I \mathbf{\hat{A}} = \mathbf{A} + 2\mathbf{I} A^=A+2I,其目的在于增强中心节点自身信息;
  • cached:是否存储 D ^ − 1 / 2 A ^ D ^ − 1 / 2 \mathbf{\hat{D}}^{-1/2} \mathbf{\hat{A}} \mathbf{\hat{D}}^{-1/2} D^1/2A^D^1/2的计算结果以便后续使用,这个参数只应在归纳学习(transductive learning)的景中设置为true
  • add_self_loops:是否在邻接矩阵中增加自环边;
  • normalize:是否添加自环边并在运行中计算对称归一化系数;
  • bias:是否包含偏置项。

详细内容参阅GCNConv官方文档

基于GCN图神经网络的图节点分类

通过将torch.nn.Linear layers 替换为PyG的GNN Conv Layers,我们可以轻松地将MLP模型转化为GNN模型。在下方的例子中,我们将MLP例子中的linear层替换为GCNConv层。

from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GCN, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GCNConv(dataset.num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

model = GCN(hidden_channels=16)
print(model)

GAT及其在图节点分类任务中的应用

GAT的定义

图注意网络(GAT)来源于论文 Graph Attention Networks。其数学定义为,
x i ′ = α i , i Θ x i + ∑ j ∈ N ( i ) α i , j Θ x j , \mathbf{x}^{\prime}_i = \alpha_{i,i}\mathbf{\Theta}\mathbf{x}_{i} + \sum_{j \in \mathcal{N}(i)} \alpha_{i,j}\mathbf{\Theta}\mathbf{x}_{j}, xi=αi,iΘxi+jN(i)αi,jΘxj,
其中注意力系数 α i , j \alpha_{i,j} αi,j的计算方法为,
α i , j = exp ⁡ ( L e a k y R e L U ( a ⊤ [ Θ x i   ∥   Θ x j ] ) ) ∑ k ∈ N ( i ) ∪ { i } exp ⁡ ( L e a k y R e L U ( a ⊤ [ Θ x i   ∥   Θ x k ] ) ) . \alpha_{i,j} = \frac{ \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_j] \right)\right)} {\sum_{k \in \mathcal{N}(i) \cup \{ i \}} \exp\left(\mathrm{LeakyReLU}\left(\mathbf{a}^{\top} [\mathbf{\Theta}\mathbf{x}_i \, \Vert \, \mathbf{\Theta}\mathbf{x}_k] \right)\right)}. αi,j=kN(i){i}exp(LeakyReLU(a[ΘxiΘxk]))exp(LeakyReLU(a[ΘxiΘxj])).

PyG中GATConv 模块说明

//GAT部分还没有理解~

结语

在节点表征的学习中,MLP节点分类器只考虑了节点自身属性,忽略了节点之间的连接关系,它的结果是最差的;而GCN与GAT节点分类器,同时考虑了节点自身属性与周围邻居节点的属性,它们的结果优于MLP节点分类器。从中可以看出邻居节点的信息对于节点分类任务的重要性

基于图神经网络的节点表征的学习遵循消息传递范式

  • 在邻居节点信息变换阶段,GCN与GAT都对邻居节点做归一化和线性变换(两个操作不分前后);
  • 在邻居节点信息聚合阶段都将变换后的邻居节点信息做求和聚合;
  • 在中心节点信息变换阶段只是简单返回邻居节点信息聚合阶段的聚合结果。

GCN与GAT的区别在于邻居节点信息聚合过程中的归一化方法不同

  • 前者根据中心节点与邻居节点的度计算归一化系数,后者根据中心节点与邻居节点的相似度计算归一化系数。
  • 前者的归一化方式依赖于图的拓扑结构,不同节点其自身的度不同、其邻居的度也不同,在一些应用中可能会影响泛化能力。
  • 后者的归一化方式依赖于中心节点与邻居节点的相似度,相似度是训练得到的,因此不受图的拓扑结构的影响,在不同的任务中都会有较好的泛化表现。

作业

  • 此篇文章涉及的代码可见于codes/learn_node_representation.ipynb,请参照这份代码使用PyG中不同的图卷积层在PyG的不同数据上实现节点分类或回归任务。
# 获取并分析数据集
from torch_geometric.datasets import Planetoid
from torch_geometric.transforms import NormalizeFeatures

dataset = Planetoid(root='./codes/data/Planetoid/', name='Cora', transform=NormalizeFeatures())

print()
print(f'Dataset: {dataset}:')
print('======================')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')

data = dataset[0]  # Get the first graph object.

print()
print(data)
print('======================')

# Gather some statistics about the graph.
print(f'Number of nodes: {data.num_nodes}')
print(f'Number of edges: {data.num_edges}')
print(f'Average node degree: {data.num_edges / data.num_nodes:.2f}')
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()}')
# 可视化节点表征分布的方法
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

def visualize(h, color):
    z = TSNE(n_components=2).fit_transform(out.detach().cpu().numpy())
    plt.figure(figsize=(10,10))
    plt.xticks([])
    plt.yticks([])

    plt.scatter(z[:, 0], z[:, 1], s=70, c=color, cmap="Set2")
    plt.show()
# MLP图节点分类神经网络
import torch
from torch.nn import Linear
import torch.nn.functional as F


class MLP(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(MLP, self).__init__()
        torch.manual_seed(12345)
        self.lin1 = Linear(dataset.num_features, hidden_channels)
        self.lin2 = Linear(hidden_channels, dataset.num_classes)

    def forward(self, x):
        x = self.lin1(x)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.lin2(x)
        return x

model = MLP(hidden_channels=16)
print(model)
model = MLP(hidden_channels=16)
criterion = torch.nn.CrossEntropyLoss()  # Define loss criterion.
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)  # Define optimizer.

def train():
      model.train()
      optimizer.zero_grad()  # Clear gradients.
      out = model(data.x)  # Perform a single forward pass.
      loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
      loss.backward()  # Derive gradients.
      optimizer.step()  # Update parameters based on gradients.
      return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

Epoch: 001, Loss: 1.9615
Epoch: 002, Loss: 1.9557
Epoch: 003, Loss: 1.9505
Epoch: 004, Loss: 1.9423
Epoch: 005, Loss: 1.9327
Epoch: 006, Loss: 1.9279
Epoch: 007, Loss: 1.9144
Epoch: 008, Loss: 1.9087
Epoch: 009, Loss: 1.9023
Epoch: 010, Loss: 1.8893
Epoch: 011, Loss: 1.8776
Epoch: 012, Loss: 1.8594
Epoch: 013, Loss: 1.8457
Epoch: 014, Loss: 1.8365
Epoch: 015, Loss: 1.8280
Epoch: 016, Loss: 1.7965
Epoch: 017, Loss: 1.7984
Epoch: 018, Loss: 1.7832
Epoch: 019, Loss: 1.7495
Epoch: 020, Loss: 1.7441
Epoch: 021, Loss: 1.7188
Epoch: 022, Loss: 1.7124
Epoch: 023, Loss: 1.6785
Epoch: 024, Loss: 1.6660
Epoch: 025, Loss: 1.6119
Epoch: 026, Loss: 1.6236
Epoch: 027, Loss: 1.5827
Epoch: 028, Loss: 1.5784
Epoch: 029, Loss: 1.5524
Epoch: 030, Loss: 1.5020
Epoch: 031, Loss: 1.5065
Epoch: 032, Loss: 1.4742
Epoch: 033, Loss: 1.4581
Epoch: 034, Loss: 1.4246
Epoch: 035, Loss: 1.4131
Epoch: 036, Loss: 1.4112
Epoch: 037, Loss: 1.3923
Epoch: 038, Loss: 1.3055
Epoch: 039, Loss: 1.2982
Epoch: 040, Loss: 1.2543
Epoch: 041, Loss: 1.2244
Epoch: 042, Loss: 1.2331
Epoch: 043, Loss: 1.1984
Epoch: 044, Loss: 1.1796
Epoch: 045, Loss: 1.1093
Epoch: 046, Loss: 1.1284
Epoch: 047, Loss: 1.1229
Epoch: 048, Loss: 1.0383
Epoch: 049, Loss: 1.0439
Epoch: 050, Loss: 1.0563
Epoch: 051, Loss: 0.9893
Epoch: 052, Loss: 1.0508
Epoch: 053, Loss: 0.9343
Epoch: 054, Loss: 0.9639
Epoch: 055, Loss: 0.8929
Epoch: 056, Loss: 0.8705
Epoch: 057, Loss: 0.9176
Epoch: 058, Loss: 0.9239
Epoch: 059, Loss: 0.8641
Epoch: 060, Loss: 0.8578
Epoch: 061, Loss: 0.7908
Epoch: 062, Loss: 0.7856
Epoch: 063, Loss: 0.7683
Epoch: 064, Loss: 0.7816
Epoch: 065, Loss: 0.7356
Epoch: 066, Loss: 0.6951
Epoch: 067, Loss: 0.7300
Epoch: 068, Loss: 0.6939
Epoch: 069, Loss: 0.7550
Epoch: 070, Loss: 0.6864
Epoch: 071, Loss: 0.7094
Epoch: 072, Loss: 0.7238
Epoch: 073, Loss: 0.7150
Epoch: 074, Loss: 0.6191
Epoch: 075, Loss: 0.6770
Epoch: 076, Loss: 0.6487
Epoch: 077, Loss: 0.6258
Epoch: 078, Loss: 0.5821
Epoch: 079, Loss: 0.5637
Epoch: 080, Loss: 0.6368
Epoch: 081, Loss: 0.6333
Epoch: 082, Loss: 0.6434
Epoch: 083, Loss: 0.5974
Epoch: 084, Loss: 0.6176
Epoch: 085, Loss: 0.5972
Epoch: 086, Loss: 0.4690
Epoch: 087, Loss: 0.6362
Epoch: 088, Loss: 0.6118
Epoch: 089, Loss: 0.5248
Epoch: 090, Loss: 0.5520
Epoch: 091, Loss: 0.6130
Epoch: 092, Loss: 0.5361
Epoch: 093, Loss: 0.5594
Epoch: 094, Loss: 0.5049
Epoch: 095, Loss: 0.5043
Epoch: 096, Loss: 0.5235
Epoch: 097, Loss: 0.5451
Epoch: 098, Loss: 0.5329
Epoch: 099, Loss: 0.5008
Epoch: 100, Loss: 0.5350
Epoch: 101, Loss: 0.5343
Epoch: 102, Loss: 0.5138
Epoch: 103, Loss: 0.5377
Epoch: 104, Loss: 0.5353
Epoch: 105, Loss: 0.5176
Epoch: 106, Loss: 0.5229
Epoch: 107, Loss: 0.4558
Epoch: 108, Loss: 0.4883
Epoch: 109, Loss: 0.4659
Epoch: 110, Loss: 0.4908
Epoch: 111, Loss: 0.4966
Epoch: 112, Loss: 0.4725
Epoch: 113, Loss: 0.4787
Epoch: 114, Loss: 0.4390
Epoch: 115, Loss: 0.4199
Epoch: 116, Loss: 0.4810
Epoch: 117, Loss: 0.4484
Epoch: 118, Loss: 0.5080
Epoch: 119, Loss: 0.4241
Epoch: 120, Loss: 0.4745
Epoch: 121, Loss: 0.4651
Epoch: 122, Loss: 0.4652
Epoch: 123, Loss: 0.5580
Epoch: 124, Loss: 0.4861
Epoch: 125, Loss: 0.4405
Epoch: 126, Loss: 0.4292
Epoch: 127, Loss: 0.4409
Epoch: 128, Loss: 0.3575
Epoch: 129, Loss: 0.4468
Epoch: 130, Loss: 0.4603
Epoch: 131, Loss: 0.4108
Epoch: 132, Loss: 0.4601
Epoch: 133, Loss: 0.4258
Epoch: 134, Loss: 0.3852
Epoch: 135, Loss: 0.4028
Epoch: 136, Loss: 0.4245
Epoch: 137, Loss: 0.4300
Epoch: 138, Loss: 0.4693
Epoch: 139, Loss: 0.4314
Epoch: 140, Loss: 0.4031
Epoch: 141, Loss: 0.4290
Epoch: 142, Loss: 0.4110
Epoch: 143, Loss: 0.3863
Epoch: 144, Loss: 0.4215
Epoch: 145, Loss: 0.4519
Epoch: 146, Loss: 0.3940
Epoch: 147, Loss: 0.4429
Epoch: 148, Loss: 0.3527
Epoch: 149, Loss: 0.4390
Epoch: 150, Loss: 0.4212
Epoch: 151, Loss: 0.4128
Epoch: 152, Loss: 0.3779
Epoch: 153, Loss: 0.4801
Epoch: 154, Loss: 0.4130
Epoch: 155, Loss: 0.3962
Epoch: 156, Loss: 0.4262
Epoch: 157, Loss: 0.4210
Epoch: 158, Loss: 0.4081
Epoch: 159, Loss: 0.4066
Epoch: 160, Loss: 0.3782
Epoch: 161, Loss: 0.3836
Epoch: 162, Loss: 0.4172
Epoch: 163, Loss: 0.3993
Epoch: 164, Loss: 0.4477
Epoch: 165, Loss: 0.3714
Epoch: 166, Loss: 0.3610
Epoch: 167, Loss: 0.4546
Epoch: 168, Loss: 0.4387
Epoch: 169, Loss: 0.3793
Epoch: 170, Loss: 0.3704
Epoch: 171, Loss: 0.4286
Epoch: 172, Loss: 0.4131
Epoch: 173, Loss: 0.3795
Epoch: 174, Loss: 0.4230
Epoch: 175, Loss: 0.4139
Epoch: 176, Loss: 0.3586
Epoch: 177, Loss: 0.3588
Epoch: 178, Loss: 0.3911
Epoch: 179, Loss: 0.3810
Epoch: 180, Loss: 0.4203
Epoch: 181, Loss: 0.3583
Epoch: 182, Loss: 0.3690
Epoch: 183, Loss: 0.4025
Epoch: 184, Loss: 0.3920
Epoch: 185, Loss: 0.4369
Epoch: 186, Loss: 0.4317
Epoch: 187, Loss: 0.4911
Epoch: 188, Loss: 0.3369
Epoch: 189, Loss: 0.4945
Epoch: 190, Loss: 0.3912
Epoch: 191, Loss: 0.3824
Epoch: 192, Loss: 0.3479
Epoch: 193, Loss: 0.3798
Epoch: 194, Loss: 0.3799
Epoch: 195, Loss: 0.4015
Epoch: 196, Loss: 0.3615
Epoch: 197, Loss: 0.3985
Epoch: 198, Loss: 0.4664
Epoch: 199, Loss: 0.3714
Epoch: 200, Loss: 0.3810

def test():
      model.eval()
      out = model(data.x)
      pred = out.argmax(dim=1)  # Use the class with highest probability.
      test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
      test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
      return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

Test Accuracy: 0.5900

# GCN图节点分类神经网络
from torch_geometric.nn import GCNConv

class GCN(torch.nn.Module):
    def __init__(self, hidden_channels):
        super(GCN, self).__init__()
        torch.manual_seed(12345)
        self.conv1 = GCNConv(dataset.num_features, hidden_channels)
        self.conv2 = GCNConv(hidden_channels, dataset.num_classes)

    def forward(self, x, edge_index):
        x = self.conv1(x, edge_index)
        x = x.relu()
        x = F.dropout(x, p=0.5, training=self.training)
        x = self.conv2(x, edge_index)
        return x

model = GCN(hidden_channels=16)
print(model)
# 可视化未训练过的模型输出的节点表征
model = GCN(hidden_channels=16)
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

在这里插入图片描述

# 训练GCN图节点分类器
model = GCN(hidden_channels=16)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
criterion = torch.nn.CrossEntropyLoss()

def train():
      model.train()
      optimizer.zero_grad()  # Clear gradients.
      out = model(data.x, data.edge_index)  # Perform a single forward pass.
      loss = criterion(out[data.train_mask], data.y[data.train_mask])  # Compute the loss solely based on the training nodes.
      loss.backward()  # Derive gradients.
      optimizer.step()  # Update parameters based on gradients.
      return loss

for epoch in range(1, 201):
    loss = train()
    print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}')

Epoch: 001, Loss: 1.9451
Epoch: 002, Loss: 1.9384
Epoch: 003, Loss: 1.9307
Epoch: 004, Loss: 1.9227
Epoch: 005, Loss: 1.9126
Epoch: 006, Loss: 1.9076
Epoch: 007, Loss: 1.8917
Epoch: 008, Loss: 1.8809
Epoch: 009, Loss: 1.8728
Epoch: 010, Loss: 1.8616
Epoch: 011, Loss: 1.8453
Epoch: 012, Loss: 1.8397
Epoch: 013, Loss: 1.8237
Epoch: 014, Loss: 1.8057
Epoch: 015, Loss: 1.7979
Epoch: 016, Loss: 1.7808
Epoch: 017, Loss: 1.7667
Epoch: 018, Loss: 1.7555
Epoch: 019, Loss: 1.7436
Epoch: 020, Loss: 1.7184
Epoch: 021, Loss: 1.7126
Epoch: 022, Loss: 1.6845
Epoch: 023, Loss: 1.6717
Epoch: 024, Loss: 1.6673
Epoch: 025, Loss: 1.6353
Epoch: 026, Loss: 1.6181
Epoch: 027, Loss: 1.5962
Epoch: 028, Loss: 1.5723
Epoch: 029, Loss: 1.5708
Epoch: 030, Loss: 1.5370
Epoch: 031, Loss: 1.5162
Epoch: 032, Loss: 1.4948
Epoch: 033, Loss: 1.4713
Epoch: 034, Loss: 1.4589
Epoch: 035, Loss: 1.4479
Epoch: 036, Loss: 1.4377
Epoch: 037, Loss: 1.4271
Epoch: 038, Loss: 1.3644
Epoch: 039, Loss: 1.3658
Epoch: 040, Loss: 1.3363
Epoch: 041, Loss: 1.3619
Epoch: 042, Loss: 1.2872
Epoch: 043, Loss: 1.2831
Epoch: 044, Loss: 1.2806
Epoch: 045, Loss: 1.2330
Epoch: 046, Loss: 1.2266
Epoch: 047, Loss: 1.2149
Epoch: 048, Loss: 1.1631
Epoch: 049, Loss: 1.1756
Epoch: 050, Loss: 1.1714
Epoch: 051, Loss: 1.1658
Epoch: 052, Loss: 1.0823
Epoch: 053, Loss: 1.1171
Epoch: 054, Loss: 1.0874
Epoch: 055, Loss: 1.0589
Epoch: 056, Loss: 1.0713
Epoch: 057, Loss: 1.0307
Epoch: 058, Loss: 1.0351
Epoch: 059, Loss: 1.0053
Epoch: 060, Loss: 1.0066
Epoch: 061, Loss: 0.9699
Epoch: 062, Loss: 0.9561
Epoch: 063, Loss: 0.9575
Epoch: 064, Loss: 0.9595
Epoch: 065, Loss: 0.8761
Epoch: 066, Loss: 0.8847
Epoch: 067, Loss: 0.8745
Epoch: 068, Loss: 0.8546
Epoch: 069, Loss: 0.8648
Epoch: 070, Loss: 0.8705
Epoch: 071, Loss: 0.8250
Epoch: 072, Loss: 0.8250
Epoch: 073, Loss: 0.8049
Epoch: 074, Loss: 0.8138
Epoch: 075, Loss: 0.7888
Epoch: 076, Loss: 0.7765
Epoch: 077, Loss: 0.7684
Epoch: 078, Loss: 0.7603
Epoch: 079, Loss: 0.6908
Epoch: 080, Loss: 0.7248
Epoch: 081, Loss: 0.7079
Epoch: 082, Loss: 0.6923
Epoch: 083, Loss: 0.7547
Epoch: 084, Loss: 0.6685
Epoch: 085, Loss: 0.6872
Epoch: 086, Loss: 0.6594
Epoch: 087, Loss: 0.6840
Epoch: 088, Loss: 0.6607
Epoch: 089, Loss: 0.6342
Epoch: 090, Loss: 0.6769
Epoch: 091, Loss: 0.6251
Epoch: 092, Loss: 0.6337
Epoch: 093, Loss: 0.6107
Epoch: 094, Loss: 0.5817
Epoch: 095, Loss: 0.6512
Epoch: 096, Loss: 0.6235
Epoch: 097, Loss: 0.5965
Epoch: 098, Loss: 0.6201
Epoch: 099, Loss: 0.5853
Epoch: 100, Loss: 0.5833
Epoch: 101, Loss: 0.6229
Epoch: 102, Loss: 0.5639
Epoch: 103, Loss: 0.5921
Epoch: 104, Loss: 0.5574
Epoch: 105, Loss: 0.5569
Epoch: 106, Loss: 0.5734
Epoch: 107, Loss: 0.5680
Epoch: 108, Loss: 0.5513
Epoch: 109, Loss: 0.5056
Epoch: 110, Loss: 0.5209
Epoch: 111, Loss: 0.5175
Epoch: 112, Loss: 0.5241
Epoch: 113, Loss: 0.5048
Epoch: 114, Loss: 0.5128
Epoch: 115, Loss: 0.5082
Epoch: 116, Loss: 0.4959
Epoch: 117, Loss: 0.4848
Epoch: 118, Loss: 0.4838
Epoch: 119, Loss: 0.4637
Epoch: 120, Loss: 0.5064
Epoch: 121, Loss: 0.5116
Epoch: 122, Loss: 0.4636
Epoch: 123, Loss: 0.4973
Epoch: 124, Loss: 0.4674
Epoch: 125, Loss: 0.4414
Epoch: 126, Loss: 0.4838
Epoch: 127, Loss: 0.4660
Epoch: 128, Loss: 0.4833
Epoch: 129, Loss: 0.4685
Epoch: 130, Loss: 0.4466
Epoch: 131, Loss: 0.4195
Epoch: 132, Loss: 0.4214
Epoch: 133, Loss: 0.4527
Epoch: 134, Loss: 0.4596
Epoch: 135, Loss: 0.4525
Epoch: 136, Loss: 0.4161
Epoch: 137, Loss: 0.4440
Epoch: 138, Loss: 0.4447
Epoch: 139, Loss: 0.4247
Epoch: 140, Loss: 0.4131
Epoch: 141, Loss: 0.4214
Epoch: 142, Loss: 0.3919
Epoch: 143, Loss: 0.4157
Epoch: 144, Loss: 0.4049
Epoch: 145, Loss: 0.4243
Epoch: 146, Loss: 0.3939
Epoch: 147, Loss: 0.3970
Epoch: 148, Loss: 0.3923
Epoch: 149, Loss: 0.3733
Epoch: 150, Loss: 0.4119
Epoch: 151, Loss: 0.3705
Epoch: 152, Loss: 0.3900
Epoch: 153, Loss: 0.3823
Epoch: 154, Loss: 0.4077
Epoch: 155, Loss: 0.3594
Epoch: 156, Loss: 0.3735
Epoch: 157, Loss: 0.3842
Epoch: 158, Loss: 0.3631
Epoch: 159, Loss: 0.3652
Epoch: 160, Loss: 0.3799
Epoch: 161, Loss: 0.3805
Epoch: 162, Loss: 0.3732
Epoch: 163, Loss: 0.3690
Epoch: 164, Loss: 0.3561
Epoch: 165, Loss: 0.3632
Epoch: 166, Loss: 0.3587
Epoch: 167, Loss: 0.4112
Epoch: 168, Loss: 0.3574
Epoch: 169, Loss: 0.3532
Epoch: 170, Loss: 0.3272
Epoch: 171, Loss: 0.3453
Epoch: 172, Loss: 0.3532
Epoch: 173, Loss: 0.3466
Epoch: 174, Loss: 0.3781
Epoch: 175, Loss: 0.3074
Epoch: 176, Loss: 0.3837
Epoch: 177, Loss: 0.3564
Epoch: 178, Loss: 0.3349
Epoch: 179, Loss: 0.2988
Epoch: 180, Loss: 0.3186
Epoch: 181, Loss: 0.3338
Epoch: 182, Loss: 0.3423
Epoch: 183, Loss: 0.3396
Epoch: 184, Loss: 0.3255
Epoch: 185, Loss: 0.3329
Epoch: 186, Loss: 0.3331
Epoch: 187, Loss: 0.3146
Epoch: 188, Loss: 0.3400
Epoch: 189, Loss: 0.3318
Epoch: 190, Loss: 0.3081
Epoch: 191, Loss: 0.3221
Epoch: 192, Loss: 0.3582
Epoch: 193, Loss: 0.3107
Epoch: 194, Loss: 0.3137
Epoch: 195, Loss: 0.3119
Epoch: 196, Loss: 0.2973
Epoch: 197, Loss: 0.3297
Epoch: 198, Loss: 0.3363
Epoch: 199, Loss: 0.3016
Epoch: 200, Loss: 0.3006

# 测试GCN图节点分类器
def test():
      model.eval()
      out = model(data.x, data.edge_index)
      pred = out.argmax(dim=1)  # Use the class with highest probability.
      test_correct = pred[data.test_mask] == data.y[data.test_mask]  # Check against ground-truth labels.
      test_acc = int(test_correct.sum()) / int(data.test_mask.sum())  # Derive ratio of correct predictions.
      return test_acc

test_acc = test()
print(f'Test Accuracy: {test_acc:.4f}')

Test Accuracy: 0.8140

# 可视化训练过的GCN模型输出的节点表征
model.eval()

out = model(data.x, data.edge_index)
visualize(out, color=data.y)

在这里插入图片描述

参考文献

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值