学习课程:gitee_Datawhale_GNN
学习论坛:Datawhale CLUB
公众号:Datawhale
1.node_classfication
对于节点分类的任务,GAT的得分是0.765,GCN的得分是0.779。
不同的层数和不同的out_channels通过更改hidden_channels_list的数值。
2.edge_classification
由于之前的学习已经保存了"Cora"数据集,所以将Planetoild中的路径修改。
参考代码
<code: node_classification.ipynb>
import os.path as osp import torch import torch.nn.functional as F from torch_geometric.data import (InMemoryDataset, download_url) from torch_geometric.nn import GATConv, Sequential from torch_geometric.transforms import NormalizeFeatures from torch_geometric.io import read_planetoid_data from torch.nn import Linear, ReLU class PlanetoidPubMed(InMemoryDataset): r"""The citation network datasets "PubMed" from the `"Revisiting Semi-Supervised Learning with Graph Embeddings" <https://arxiv.org/abs/1603.08861>`_ paper. Nodes represent documents and edges represent citation links. Training, validation and test splits are given by binary masks. Args: root (string): Root directory where the dataset should be saved. split (string): The type of dataset split (:obj:`"public"`, :obj:`"full"`, :obj:`"random"`). If set to :obj:`"public"`, the split will be the public fixed split from the `"Revisiting Semi-Supervised Learning with Graph Embeddings" <https://arxiv.org/abs/1603.08861>`_ paper. If set to :obj:`"full"`, all nodes except those in the validation and test sets will be used for training (as in the `"FastGCN: Fast Learning with Graph Convolutional Networks via Importance Sampling" <https://arxiv.org/abs/1801.10247>`_ paper). If set to :obj:`"random"`, train, validation, and test sets will be randomly generated, according to :obj:`num_train_per_class`, :obj:`num_val` and :obj:`num_test`. (default: :obj:`"public"`) num_train_per_class (int, optional): The number of training samples per class in case of :obj:`"random"` split. (default: :obj:`20`) num_val (int, optional): The number of validation samples in case of :obj:`"random"` split. (default: :obj:`500`) num_test (int, optional): The number of test samples in case of :obj:`"random"` split. (default: :obj:`1000`) transform (callable, optional): A function/transform that takes in an :obj:`torch_geometric.data.Data` object and returns a transformed version. The data object will be transformed before every access. (default: :obj:`None`) pre_transform (callable, optional): A function/transform that takes in an :obj:`torch_geometric.data.Data` object and returns a transformed version. The data object will be transformed before being saved to disk. (default: :obj:`None`) """ url = 'https://github.com/kimiyoung/planetoid/raw/master/data' def __init__(self, root, split="public", num_train_per_class=20, num_val=500, num_test=1000, transform=None, pre_transform=None): super(PlanetoidPubMed, self).__init__(root, transform, pre_transform) self.data, self.slices = torch.load(self.processed_paths[0]) self.split = split assert self.split in ['public', 'full', 'random'] if split == 'full': data = self.get(0) data.train_mask.fill_(True) data.train_mask[data.val_mask | data.test_mask] = False self.data, self.slices = self.collate([data]) elif split == 'random': data = self.get(0) data.train_mask.fill_(False) for c in range(self.num_classes): idx = (data.y == c).nonzero(as_tuple=False).view(-1) idx = idx[torch.randperm(idx.size(0))[:num_train_per_class]] data.train_mask[idx] = True remaining = (~data.train_mask).nonzero(as_tuple=False).view(-1) remaining = remaining[torch.randperm(remaining.size(0))] data.val_mask.fill_(False) data.val_mask[remaining[:num_val]] = True data.test_mask.fill_(False) data.test_mask[remaining[num_val:num_val + num_test]] = True self.data, self.slices = self.collate([data]) @property def raw_dir(self): return osp.join(self.root, 'raw') @property def processed_dir(self): return osp.join(self.root, 'processed') @property def raw_file_names(self): names = ['x', 'tx', 'allx', 'y', 'ty', 'ally', 'graph', 'test.index'] return ['ind.pubmed.{}'.format(name) for name in names] @property def processed_file_names(self): return 'data.pt' def download(self): for name in self.raw_file_names: download_url('{}/{}'.format(self.url, name), self.raw_dir) def process(self): data = read_planetoid_data(self.raw_dir, 'pubmed') data = data if self.pre_transform is None else self.pre_transform(data) torch.save(self.collate([data]), self.processed_paths[0]) def __repr__(self): return '{}()'.format(self.name) dataset = PlanetoidPubMed(root='data/PlanetoidPubMed/', transform=NormalizeFeatures()) print('dataset.num_features:', dataset.num_features) device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') data = dataset[0].to(device) def train(): model.train() optimizer.zero_grad() # Clear gradients. out = model(data.x, data.edge_index) # Perform a single forward pass. # Compute the loss solely based on the training nodes. loss = criterion(out[data.train_mask], data.y[data.train_mask]) loss.backward() # Derive gradients. optimizer.step() # Update parameters based on gradients. return loss 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 class GAT(torch.nn.Module): def __init__(self, num_features, hidden_channels_list, num_classes): super(GAT, self).__init__() torch.manual_seed(12345) hns = [num_features] + hidden_channels_list conv_list = [] for idx in range(len(hidden_channels_list)): conv_list.append((GATConv(hns[idx], hns[idx+1]), 'x, edge_index -> x')) conv_list.append(ReLU(inplace=True),) self.convseq = Sequential('x, edge_index', conv_list) self.linear = Linear(hidden_channels_list[-1], num_classes) def forward(self, x, edge_index): x = self.convseq(x, edge_index) x = F.dropout(x, p=0.5, training=self.training) x = self.linear(x) return x model = GAT(num_features=dataset.num_features, hidden_channels_list=[200, 100], num_classes=dataset.num_classes).to(device) print(model) optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4) criterion = torch.nn.CrossEntropyLoss() for epoch in range(1, 201): loss = train() print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}') test_acc = test() print(f'Test Accuracy: {test_acc:.4f}')
code:edge_classification.ipynb
import os.path as osp import torch import torch.nn.functional as F import torch_geometric.transforms as T from sklearn.metrics import roc_auc_score from torch_geometric.datasets import Planetoid from torch_geometric.nn import GCNConv from torch_geometric.utils import negative_sampling, train_test_split_edges class Net(torch.nn.Module): def __init__(self, in_channels, out_channels): super(Net, self).__init__() self.conv1 = GCNConv(in_channels, 128) self.conv2 = GCNConv(128, out_channels) def encode(self, x, edge_index): x = self.conv1(x, edge_index) x = x.relu() return self.conv2(x, edge_index) def decode(self, z, pos_edge_index, neg_edge_index): edge_index = torch.cat([pos_edge_index, neg_edge_index], dim=-1) return (z[edge_index[0]] * z[edge_index[1]]).sum(dim=-1) def decode_all(self, z): prob_adj = z @ z.t() return (prob_adj > 0).nonzero(as_tuple=False).t() def get_link_labels(pos_edge_index, neg_edge_index): num_links = pos_edge_index.size(1) + neg_edge_index.size(1) link_labels = torch.zeros(num_links, dtype=torch.float) link_labels[:pos_edge_index.size(1)] = 1. return link_labels def train(data, model, optimizer): model.train() neg_edge_index = negative_sampling( edge_index=data.train_pos_edge_index, num_nodes=data.num_nodes, num_neg_samples=data.train_pos_edge_index.size(1)) train_neg_edge_set = set(map(tuple, neg_edge_index.T.tolist())) val_pos_edge_set = set(map(tuple, data.val_pos_edge_index.T.tolist())) test_pos_edge_set = set(map(tuple, data.test_pos_edge_index.T.tolist())) if (len(train_neg_edge_set & val_pos_edge_set) > 0) or (len(train_neg_edge_set & test_pos_edge_set) > 0): print('wrong!') optimizer.zero_grad() z = model.encode(data.x, data.train_pos_edge_index) link_logits = model.decode(z, data.train_pos_edge_index, neg_edge_index) link_labels = get_link_labels(data.train_pos_edge_index, neg_edge_index).to(data.x.device) loss = F.binary_cross_entropy_with_logits(link_logits, link_labels) loss.backward() optimizer.step() return loss @torch.no_grad() def test(data, model): model.eval() z = model.encode(data.x, data.train_pos_edge_index) results = [] for prefix in ['val', 'test']: pos_edge_index = data[f'{prefix}_pos_edge_index'] neg_edge_index = data[f'{prefix}_neg_edge_index'] link_logits = model.decode(z, pos_edge_index, neg_edge_index) link_probs = link_logits.sigmoid() link_labels = get_link_labels(pos_edge_index, neg_edge_index) results.append(roc_auc_score(link_labels.cpu(), link_probs.cpu())) return results def main(): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') dataset = 'Cora' # path = osp.join(osp.dirname(osp.realpath(__file__)), '..', 'data', dataset) # dataset = Planetoid(path, dataset, transform=T.NormalizeFeatures()) dataset = Planetoid('data/Planetoid', dataset, transform=T.NormalizeFeatures()) data = dataset[0] ground_truth_edge_index = data.edge_index.to(device) data.train_mask = data.val_mask = data.test_mask = data.y = None data = train_test_split_edges(data) data = data.to(device) model = Net(dataset.num_features, 64).to(device) optimizer = torch.optim.Adam(params=model.parameters(), lr=0.01) best_val_auc = test_auc = 0 for epoch in range(1, 101): loss = train(data, model, optimizer) val_auc, tmp_test_auc = test(data, model) if val_auc > best_val_auc: best_val_auc = val_auc test_auc = tmp_test_auc print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}, Val: {val_auc:.4f}, ' f'Test: {test_auc:.4f}') z = model.encode(data.x, data.train_pos_edge_index) final_edge_index = model.decode_all(z) if __name__ == "__main__": main()