https://mydoc.myencyclopedia.top/pub-html/03_gcn.html
https://www.bilibili.com/video/BV1f3411i7MQ?p=3
https://blog.csdn.net/StarfishCu/article/details/109132437GCN学习:用PyG实现自定义layers的GCN网络及训练(五)

使用

  1. import numpy as np
  2. import torch
  3. import torch.nn as nn
  4. import torch.nn.functional as F
  1. from torch_geometric.datasets import Planetoid
  2. import torch_geometric.transforms as T
  3. name_data = 'Cora'
  4. dataset = Planetoid(root='data', name=name_data)
  5. # dataset.transform = T.NormalizeFeatures()
  6. print(f"Number of Classes in {name_data}:", dataset.num_classes)
  7. print(f"Number of Node Features in {name_data}:", dataset.num_node_features)
  8. device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
  9. data = dataset[0].to(device)
  10. >>>
  11. Number of Classes in Cora: 7
  12. Number of Node Features in Cora: 1433
  13. dataset.data
  14. #Data(x=[2708, 1433], edge_index=[2, 10556], y=[2708], train_mask=[2708], val_mask=[2708], test_mask=[2708])

因为len(dataset) =1,也就是说数据集里只有一张图,所以只需 data = dataset[0].to(device)
image.png可以看到数据已经转移到了cuda中了
image.png

封装函数

train

  1. def train(model, data, optimizer):
  2. model.train()
  3. optimizer.zero_grad()
  4. out = model(data)
  5. loss = F.nll_loss(out[data.train_mask], data.y[data.train_mask])
  6. loss.backward()
  7. optimizer.step()
  8. return loss.item()

Test

  1. @torch.no_grad()
  2. def test(model, data):
  3. model.eval()
  4. _, pred = model(data).max(dim=1)
  5. correct_train = pred[data.train_mask].eq(data.y[data.train_mask]).sum().item()
  6. acc_train = correct_train / data.train_mask.sum().item()
  7. correct_val = pred[data.val_mask].eq(data.y[data.val_mask]).sum().item()
  8. acc_val = correct_val / data.val_mask.sum().item()
  9. correct_test = pred[data.test_mask].eq(data.y[data.test_mask]).sum().item()
  10. acc_test = correct_test / data.test_mask.sum().item()
  11. return acc_train, acc_val, acc_test

这里的@torch.no_grad() 不需要计算梯度,也不会进行反向传播

  1. model.eval() # 测试模式
  2. with torch.no_grad():
  3. pass
  4. #等价于
  5. @torch.no_grad()
  6. def test():
  7. ...

绘图工具

  1. def live_plot(model, epoch_num=100):
  2. from livelossplot import PlotLosses
  3. liveloss = PlotLosses()
  4. model = model.to(device)
  5. optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
  6. for epoch in range(epoch_num):
  7. loss = train(model, dataset[0], optimizer)
  8. acc_train, acc_val, acc_test = test(model, dataset[0])
  9. logs = {
  10. 'loss': loss,
  11. 'acc_val': acc_val,
  12. 'acc_test': acc_test,
  13. }
  14. liveloss.update(logs)
  15. liveloss.send()

Kipf GCN

https://github.com/tkipf/pygcn
最原始的GCN。借用PyG实现Kipf GCN,由于Kipf GCN使用的是稠密矩阵,而PyG里使用的是稀疏矩阵,所以需要做转化。

  1. #稀疏矩阵→稠密矩阵
  2. def convert_data_to_adj(data):
  3. from torch_geometric.utils import to_dense_adj
  4. feature, edge_index = data.x, data.edge_index
  5. adj = torch.squeeze(to_dense_adj(edge_index))
  6. return feature, adj

函数测试
使用PyG的例子
image.png

  1. import torch
  2. from torch_geometric.data import Data
  3. edge_index = torch.tensor([[0, 1, 1, 2],
  4. [1, 0, 2, 1]], dtype=torch.long)
  5. x = torch.tensor([[-1], [0], [1]], dtype=torch.float)
  6. data = Data(x=x, edge_index=edge_index)
  1. f, adj = convert_data_to_adj(data)
  2. print(adj)
  3. >>>
  4. tensor([[0., 1., 0.],
  5. [1., 0., 1.],
  6. [0., 1., 0.]])

测试成功,开始编写Kipf GCN

  1. # https://github.com/tkipf/pygcn/blob/master/pygcn/layers.py
  2. import torch
  3. from torch.nn.parameter import Parameter
  4. from torch.nn.modules.module import Module
  5. class GraphConvolution(Module):
  6. """
  7. Simple GCN layer, similar to https://arxiv.org/abs/1609.02907
  8. """
  9. def __init__(self, in_features, out_features, bias=True):
  10. super(GraphConvolution, self).__init__()
  11. self.in_features = in_features
  12. self.out_features = out_features
  13. self.weight = Parameter(torch.FloatTensor(in_features, out_features))
  14. if bias:
  15. self.bias = Parameter(torch.FloatTensor(out_features))
  16. else:
  17. self.register_parameter('bias', None)
  18. self.reset_parameters()
  19. def reset_parameters(self):
  20. stdv = 1. / np.sqrt(self.weight.size(1))
  21. self.weight.data.uniform_(-stdv, stdv)
  22. if self.bias is not None:
  23. self.bias.data.uniform_(-stdv, stdv)
  24. def forward(self, data):
  25. # note in the orig impl @ https://github.com/tkipf/pygcn/blob/master/pygcn/layers.py
  26. # forward is defined as
  27. # def forward(self, input, adj)
  28. # where adj is sparse adj matrix
  29. #增加了这一个语句
  30. input, adj = convert_data_to_adj(data)
  31. # now adj fits orig impl
  32. support = torch.mm(input, self.weight)
  33. output = torch.spmm(adj, support)
  34. if self.bias is not None:
  35. return output + self.bias
  36. else:
  37. return output
  38. def __repr__(self):
  39. return self.__class__.__name__ + ' (' \
  40. + str(self.in_features) + ' -> ' \
  41. + str(self.out_features) + ')'
  1. model_gcn_kipf = GraphConvolution(dataset.num_features, dataset.num_classes)
  2. live_plot(model_gcn_kipf, epoch_num=120)

image.png
精度还是比较低的

torch_geometric.nn.GCNConv

定义了一个包含2层GCN的网络

  1. from torch_geometric.nn import GCNConv
  2. class GCNConv_Demo(nn.Module):
  3. def __init__(self, in_channels, hidden_channels, out_channels):
  4. super().__init__()
  5. self.conv1 = GCNConv(in_channels, hidden_channels)
  6. self.conv2 = GCNConv(hidden_channels, out_channels)
  7. def forward(self, data):
  8. x, edge_index = data.x, data.edge_index
  9. x = self.conv1(x, edge_index).relu()
  10. x = self.conv2(x, edge_index)
  11. return F.log_softmax(x, dim=1)
  1. net=GCNConv_Demo(dataset.num_features,16,dataset.num_classes)
  2. live_plot(net,50)

image.png

Impl GCNConv based on MessagePassing

https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html
PyG的消息传递机制

  1. from torch_geometric.utils import add_self_loops, degree
  2. import torch_geometric
  3. class GCNConvImpl(torch_geometric.nn.MessagePassing):
  4. def __init__(self, in_channels, out_channels):
  5. super().__init__(aggr='add') # "Add" aggregation (Step 5).
  6. self.lin = torch.nn.Linear(in_channels, out_channels)
  7. def forward(self, x, edge_index):
  8. # x has shape [N, in_channels]
  9. # edge_index has shape [2, E]
  10. # Step 1: Add self-loops to the adjacency matrix.
  11. edge_index, _ = add_self_loops(edge_index, num_nodes=x.size(0))
  12. # Step 2: Linearly transform node feature matrix.
  13. x = self.lin(x)
  14. # Step 3: Compute normalization.
  15. row, col = edge_index
  16. deg = degree(col, x.size(0), dtype=x.dtype)
  17. deg_inv_sqrt = deg.pow(-0.5)
  18. deg_inv_sqrt[deg_inv_sqrt == float('inf')] = 0
  19. norm = deg_inv_sqrt[row] * deg_inv_sqrt[col]
  20. # Step 4-5: Start propagating messages.
  21. return self.propagate(edge_index, x=x, norm=norm)
  22. def message(self, x_j, norm):
  23. # x_j has shape [E, out_channels]
  24. # Step 4: Normalize node features.
  25. return norm.view(-1, 1) * x_j
  1. class GCNConvMessagePassingDemo(torch.nn.Module):
  2. def __init__(self, in_channels, hidden_channels, out_channels):
  3. super().__init__()
  4. self.conv1 = GCNConvImpl(in_channels, hidden_channels)
  5. self.conv2 = GCNConvImpl(hidden_channels, out_channels)
  6. def forward(self, data):
  7. x, edge_index = data.x, data.edge_index
  8. x = self.conv1(x, edge_index).relu()
  9. x = self.conv2(x, edge_index)
  10. return F.log_softmax(x, dim=1)
  1. model_gcn_impl_demo = GCNConvMessagePassingDemo(dataset.num_features, 16, dataset.num_classes)
  2. live_plot(model_gcn_impl_demo)

image.png