Começando.
Vamos escolher um conjunto de dados de gráfico simples, como o Zachary Karate Club . Aqui, os nós representam 34 alunos que estiveram envolvidos no clube e os links representam 78 interações diferentes entre pares de sócios fora do clube. Existem dois tipos diferentes de rótulos , ou seja, as duas facções. Podemos usar essas informações para formular uma tarefa de classificação de nós.
Dividimos o gráfico em conjuntos de treinamento e de teste, onde usamos o conjunto de treinamento para construir um modelo de rede neural de gráfico e usamos o modelo para prever os rótulos de nós ausentes no conjunto de teste.
Aqui, usamos a biblioteca python PyTorch Geometric (PyG) para modelar a rede neural de gráfico. Como alternativa, a Deep Graph Library (DGL) também pode ser usada para o mesmo propósito.
PyTorch Geometric é uma biblioteca de aprendizado geométrico profundo construída sobre PyTorch. Vários métodos populares de rede neural de gráfico foram implementados usando PyG e você pode brincar com o código usando conjuntos de dados integrados ou criar seu próprio conjunto de dados. PyG usa uma implementação bacana onde fornece uma classe InMemoryDataset que pode ser usada para criar o conjunto de dados personalizado ( Nota: InMemoryDataset deve ser usado para conjuntos de dados pequenos o suficiente para carregar na memória ).
Uma visualização simples do conjunto de dados do gráfico do Zachary Karate Club é a seguinte:Visualização dos dados gráficos do Zachary Karate Club (Fonte: mim)
Formule o problema.
Para formular o problema, precisamos:
- O próprio gráfico e os rótulos de cada nó
- Os dados de borda no formato de coordenada (COO)
- Embeddings ou representações numéricas para os nós
Vamos entrar na parte de codificação.
Preparações.
import networkx as nx | |
import numpy as np | |
import torch | |
from sklearn.preprocessing import StandardScaler | |
# load graph from networkx library | |
G = nx.karate_club_graph() | |
# retrieve the labels for each node | |
labels = np.asarray([G.nodes[i][‘club’] != ‘Mr. Hi’ for i in G.nodes]).astype(np.int64) | |
# create edge index from | |
adj = nx.to_scipy_sparse_matrix(G).tocoo() | |
row = torch.from_numpy(adj.row.astype(np.int64)).to(torch.long) | |
col = torch.from_numpy(adj.col.astype(np.int64)).to(torch.long) | |
edge_index = torch.stack([row, col], dim=0) | |
# using degree as embedding | |
embeddings = np.array(list(dict(G.degree()).values())) | |
# normalizing degree values | |
scale = StandardScaler() | |
embeddings = scale.fit_transform(embeddings.reshape(-1,1)) |
O conjunto de dados do karate club pode ser carregado diretamente da biblioteca NetworkX. Recuperamos os rótulos do gráfico e criamos um índice de borda no formato de coordenadas. O grau de nó foi usado como embeddings / representações numéricas para os nós (no caso de um gráfico direcionado, em grau pode ser usado para o mesmo propósito). Como os valores de graus tendem a ser diversos, nós os normalizamos antes de usar os valores como entrada para o modelo GNN.
Com isso, preparamos todas as peças necessárias para construir o conjunto de dados personalizado do Pytorch Geometric.
O conjunto de dados personalizado.
import torch | |
import pandas as pd | |
from torch_geometric.data import InMemoryDataset, Data | |
from sklearn.model_selection import train_test_split | |
import torch_geometric.transforms as T | |
# custom dataset | |
class KarateDataset(InMemoryDataset): | |
def __init__(self, transform=None): | |
super(KarateDataset, self).__init__(‘.’, transform, None, None) | |
data = Data(edge_index=edge_index) | |
data.num_nodes = G.number_of_nodes() | |
# embedding | |
data.x = torch.from_numpy(embeddings).type(torch.float32) | |
# labels | |
y = torch.from_numpy(labels).type(torch.long) | |
data.y = y.clone().detach() | |
data.num_classes = 2 | |
# splitting the data into train, validation and test | |
X_train, X_test, y_train, y_test = train_test_split(pd.Series(G.nodes()), | |
pd.Series(labels), | |
test_size=0.30, | |
random_state=42) | |
n_nodes = G.number_of_nodes() | |
# create train and test masks for data | |
train_mask = torch.zeros(n_nodes, dtype=torch.bool) | |
test_mask = torch.zeros(n_nodes, dtype=torch.bool) | |
train_mask[X_train.index] = True | |
test_mask[X_test.index] = True | |
data[‘train_mask’] = train_mask | |
data[‘test_mask’] = test_mask | |
self.data, self.slices = self.collate([data]) | |
def _download(self): | |
return | |
def _process(self): | |
return | |
def __repr__(self): | |
return ‘{}()’.format(self.__class__.__name__) | |
dataset = KarateDataset() | |
data = dataset[0] |
A classe KarateDataset herda da classe InMemoryDataset e usa um objeto Data para reunir todas as informações relacionadas ao conjunto de dados do clube de caratê. Os dados do gráfico são então divididos em conjuntos de treinamento e teste, criando assim as máscaras de treinamento e teste usando as divisões.
O objeto de dados contém as seguintes variáveis:
Dados (edge_index = [2, 156], num_classes = [1], test_mask = [34], train_mask = [34], x = [34, 1], y = [34])
Este conjunto de dados personalizado agora pode ser usado com vários modelos de rede neural de gráfico da biblioteca Pytorch Geometric. Vamos escolher um modelo de rede convolucional de gráfico e usá-lo para prever os rótulos ausentes no conjunto de teste.
Observação: a biblioteca PyG se concentra mais na tarefa de classificação de nós, mas também pode ser usada para previsão de link.
Rede convolucional do gráfico.
import torch.nn as nn | |
import torch.nn.functional as F | |
from torch_geometric.nn import GCNConv | |
# GCN model with 2 layers | |
class Net(torch.nn.Module): | |
def __init__(self): | |
super(Net, self).__init__() | |
self.conv1 = GCNConv(data.num_features, 16) | |
self.conv2 = GCNConv(16, int(data.num_classes)) | |
def forward(self): | |
x, edge_index = data.x, data.edge_index | |
x = F.relu(self.conv1(x, edge_index)) | |
x = F.dropout(x, training=self.training) | |
x = self.conv2(x, edge_index) | |
return F.log_softmax(x, dim=1) | |
device = torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’) | |
data = data.to(device) | |
model = Net().to(device) |
view rawgcn-karateclub.py hosted with ❤ by GitHub
O modelo GCN é construído com 2 camadas ocultas e cada camada oculta contém 16 neurônios. Vamos treinar o modelo!
Treine o modelo GCN.
torch.manual_seed(42) | |
optimizer_name = “Adam” | |
lr = 1e-1 | |
optimizer = getattr(torch.optim, optimizer_name)(model.parameters(), lr=lr) | |
epochs = 200 | |
def train(): | |
model.train() | |
optimizer.zero_grad() | |
F.nll_loss(model()[data.train_mask], data.y[data.train_mask]).backward() | |
optimizer.step() | |
@torch.no_grad() | |
def test(): | |
model.eval() | |
logits = model() | |
mask1 = data[‘train_mask’] | |
pred1 = logits[mask1].max(1)[1] | |
acc1 = pred1.eq(data.y[mask1]).sum().item() / mask1.sum().item() | |
mask = data[‘test_mask’] | |
pred = logits[mask].max(1)[1] | |
acc = pred.eq(data.y[mask]).sum().item() / mask.sum().item() | |
return acc1,acc | |
for epoch in range(1, epochs): | |
train() | |
train_acc,test_acc = test() | |
print(‘#’ * 70) | |
print(‘Train Accuracy: %s’ %train_acc ) | |
print(‘Test Accuracy: %s’ % test_acc) | |
print(‘#’ * 70) |
Experimentos iniciais com hiperparâmetros aleatórios deram os seguintes resultados:
Precisão do trem: 0,913
Precisão do teste: 0,727
Isso não é impressionante e certamente podemos fazer melhor. Em meu próximo post, discutirei como podemos usar Optuna (biblioteca python sobre ajuste de hiperparâmetros) para ajustar os hiperparâmetros facilmente e encontrar o melhor modelo.