A tese desta página: uma GNN não busca e não percorre ela treina. A vizinhança de cada nó entra na conta como entrada, e o que sai é uma representação aprendida. É o degrau seguinte da página de grafos: lá a estrutura era percorrida, aqui ela é dado de treino. E a página inteira gira em torno de uma pergunta que quase nunca é feita antes de montar o pipeline: a sua estrutura ajuda ou atrapalha? A etapa 5 mostra a resposta mudando de sinal.
O que um GNN não é
não é travessia: não existe consulta, nem caminho, nem salto contado
não é busca: nada é recuperado de lugar nenhum
não é um LLM sobre grafo: não há token, nem geração, nem prompt
O que ele é
uma rede neural comum, com uma multiplicação a mais por camada
treinada por gradiente, igual a qualquer outra
onde a entrada de um nó inclui os vizinhos dele
Explore cada etapa →
A matemática, num só lugar
As 4 contas desta página. Clique em qualquer uma para pular direto para a etapa que a explica.
Honestidade, antes de começar leia esta parte: o conjunto de dados é sintético: 30 artigos, 3 temas e citações geradas com uma homofilia que você controla. É uma rede de citações em miniatura, no espírito da Cora, e é pequena de propósito. O que NÃO é simulado: a normalização Â, a propagação, o treino por descida de gradiente, as três arquiteturas e a previsão de aresta rodam de verdade, com forward e retropropagação escritos na mão inclusive o jacobiano do softmax sobre a vizinhança, no GAT. O gradiente foi conferido contra diferença finita (erro relativo da ordem de 1e-8) antes de esta página existir; sem isso, "treina de verdade" seria só uma frase. O que é pequeno demais para virar número seu: 30 nós e 6 rótulos dão acurácias altas e instáveis o GCN chega a 100% aqui, e num benchmark real como a Cora o estado da arte fica na casa dos 81 a 84%. Leve daqui a forma das curvas, não os valores.
Todo GNN come exatamente duas coisas: uma matriz X com um vetor por nó, e uma matriz A dizendo quem se liga a quem. Nada mais. O que muda de arquitetura para arquitetura é só o que se faz com elas e a etapa 5 mostra que dá para simplesmente ignorar a segunda.
clique num nó do mapa o contorno mais grosso marca os 6 que têm rótulo
O detalhe que faz a página inteira: os três temas compartilham vocabulário de propósito. Um artigo de redes e um de teoria podem os dois falar de "grafo"; todos falam de "método" e "dados". Por isso a feature sozinha leva a rede a menos da metade de acerto e sobra espaço para a estrutura ajudar. Se a feature já resolvesse, um GNN seria custo puro, e essa é uma situação bem mais comum do que a literatura sugere.
A operação inteira cabe em uma linha: cada nó vira a média ponderada dos vizinhos e de si mesmo. Repita com uma matriz de pesos e uma não linearidade no meio, e você tem uma camada. Repita a camada, e tem a rede. Não há mais nada.
Fórmula · a normalização Â
 = D−½ (A + I) D−½
Leia assim: some a identidade à adjacência, para que cada nó continue enxergando a própria feature sem isso ele se esquece de si a cada camada. Depois divida por √(dᵢ·dⱼ) nos dois lados, o que impede o nó popular de dominar a média de todo mundo. As duas correções são de 2017 e continuam sendo o padrão.
Fórmula · uma camada de GCN
H(l+1) = σ( Â · H(l) · W(l) )
Três operações: ·H agrega a vizinhança, ·W mistura as dimensões é a única parte com peso aprendido e σ aplica a não linearidade. Tire o  e sobra exatamente uma camada densa comum, que é a observação da qual a etapa 5 vive.
Repare que aqui não há peso nenhum: a propagação acima é só Â multiplicando as features, sem nada aprendido. Mesmo assim os nós da mesma classe começam a se parecer nas primeiras rodadas é o sinal da estrutura aparecendo sozinho. Isso tem nome e é um resultado publicado: GNN sem treino já é uma linha de base decente, e a etapa 7 mostra isso virando número.
Uma camada enxerga os vizinhos. Duas enxergam os vizinhos dos vizinhos. A tentação óbvia é empilhar e é aí que a intuição de redes profundas não se transfere: em grafo, empilhar demais faz todo nó convergir para a mesma coisa.
dispersão média entre pares de nós, contra o número de propagações
Alcance, dispersão e acurácia por profundidade
Over-smoothing é o nome disso, e a explicação é de álgebra linear, não de otimização: aplicar  muitas vezes é multiplicar repetidamente pela mesma matriz, e o resultado converge para o autovetor dominante que é o mesmo para todos os nós. Não é um problema de gradiente que se resolve com mais dados ou mais épocas; é a operação fazendo o que ela faz. As saídas conhecidas são conexões residuais, concatenar as camadas em vez de empilhá-las, ou simplesmente usar duas camadas, que é o que quase todo trabalho publicado faz.
Seis nós têm rótulo. Vinte e quatro não têm. Este é o cenário semi-supervisionado, e é o cenário real: rotular é caro, e o grafo é o que espalha o pouco que existe. O treino abaixo roda no seu navegador forward, entropia cruzada, gradiente e atualização de pesos.
Fórmula · a perda
L = −(1/|T|) · Σi∈T log p(yi | xi)
A soma é só sobre T, o punhado de nós rotulados. Os outros 24 não entram na perda mas entram no forward, porque a representação dos rotulados depende deles. É essa a diferença entre semi-supervisionado de verdade e treinar num subconjunto: o nó sem rótulo influencia o gradiente mesmo sem ter alvo.
perdaacerto no teste
rosa: perda de treino · verde: acerto nos nós sem rótulo
a camada inteira em uma linha
# A biblioteca esconde o Â, mas é exatamente ele que está ali dentro.# Duas camadas, porque a etapa 3 mostrou o que acontece com oito.import torch.nn.functional as F
from torch_geometric.nn import GCNConv
classGCN(torch.nn.Module):
def__init__(self, d_in, hidden, n_classes):
super().__init__()
self.c1 = GCNConv(d_in, hidden)
self.c2 = GCNConv(hidden, n_classes)
defforward(self, x, edge_index):
x = F.relu(self.c1(x, edge_index))
# dropout entre as camadas: com 6 rótulos, decorar é fácil demais
x = F.dropout(x, p=0.5, training=self.training)
return self.c2(x, edge_index)
# A perda só olha os nós rotulados. O resto entra no forward mesmo assim,# e é dessa assimetria que sai o "semi" de semi-supervisionado.
loss = F.cross_entropy(model(x, edge_index)[train_mask], y[train_mask])
// O que esta página realmente executa, sem biblioteca nenhuma.// S é Â guardada em forma esparsa: num grafo de grau 3, multiplicar// denso desperdiçaria 85% das operações.const forward = () => {
const Z0 = matmul(SX, W0); // SX = ·X, constante no treinoconst H1 = relu(Z0);
const SH1 = spmm(SP, H1); // ·H1const Z1 = matmul(SH1, W1);
return { Z0, H1, SH1, Z1, P: softmaxRows(Z1) };
};
// dZ1 só é não-nulo nos 6 nós rotulados daí o laço sobre trainIdx// em vez da matriz inteira.
trainIdx.forEach((i) => {
for (let c = 0; c < NCLS; c++)
dZ1[i][c] = (f.P[i][c] - (y[i] === c ? 1 : 0)) / trainIdx.length;
});
const SdZ1 = spmm(SP, dZ1); // Â é simétrica: Âᵀ = Âconst dH1 = matmul(SdZ1, transpose(W1));
const dZ0 = dH1.map((row, i) =>
Float64Array.from(row, (v, j) => (f.Z0[i][j] > 0 ? v : 0))); // ReLU'const dW0 = matmul(SXT, dZ0);
As duas redes abaixo são o mesmo código, com a mesma inicialização, as mesmas features e o mesmo número de épocas. A única diferença é o valor de uma matriz: em um lado ela é Â, no outro é a identidade. Trocar  pela identidade transforma o GCN num MLP comum literalmente, no código desta página.
âmbar: MLP · roxo: GCN · a linha vertical é onde você está
os nós ficam parados: só as arestas se religam quando a homofilia muda
Este é o resultado que quase nunca aparece no material introdutório: com homofilia baixa, o GNN perde do MLP. E perde porque está funcionando ele foi construído para misturar o nó com a vizinhança, e se a vizinhança é de outra classe, misturar destrói informação. A literatura levou até por volta de 2020 para tratar isso a sério, e hoje existe uma linha inteira de arquiteturas para heterofilia. A lição prática vem antes de qualquer arquitetura: meça a homofilia do seu grafo antes de decidir usar um GNN. É uma linha de código a fração de arestas que liga nós do mesmo rótulo e ela decide se o projeto faz sentido.
As três arquiteturas mais citadas da área respondem à mesma pergunta: quanto vale cada vizinho? Peso fixo pelo grau, peso igual com o próprio vetor separado, ou peso aprendido por aresta. As três abaixo estão treinadas de verdade, com a mesma perda e as mesmas épocas.
clique num nó para ver os pesos que ele dá aos vizinhos
Fórmula · a atenção do GAT
αij = softmaxj∈N(i)( LeakyReLU( aT[ gi ‖ gj ] ) )
Compare com a página do Transformer e a semelhança é exata: uma pontuação por par, um softmax normalizando, uma média ponderada saindo. A única diferença é sobre o que o softmax normaliza: lá, sobre todos os tokens anteriores; aqui, só sobre os vizinhos. A máscara vem do grafo em vez de vir da causalidade e é por isso que se costuma dizer que um Transformer é um GNN sobre o grafo completo.
Tudo até aqui dependeu de rótulo, mesmo que pouco. Mas o grafo traz supervisão de graça: as arestas que existem contra os pares que não existem. Treinar para distinguir os dois grupos produz representação útil sem ninguém rotular nada e é assim que a maior parte dos embeddings de grafo em produção é treinada.
verde: AUC nas arestas escondidas · a linha cinza é 0,5, o acaso
Alguns pares e a pontuação que a rede deu
Repare no ponto de partida, não só na chegada: a AUC já começa bem acima do acaso antes de qualquer época de treino. Isso não é um bug da demonstração é a propagação fazendo o trabalho, e é um resultado conhecido: em vários benchmarks, uma GNN com pesos aleatórios chega perto de uma treinada. Vale como aviso de método: se você não comparou o seu modelo treinado contra a versão não treinada dele, você não sabe quanto o treino contribuiu. É a linha de base mais barata que existe e a mais esquecida.
A matemática é simples e escala mal. O problema não é o número de parâmetros uma GNN é minúscula perto de qualquer LLM. O problema é que atualizar um nó exige carregar a vizinhança dele, e a vizinhança de dois saltos de um grafo real é metade do grafo.
As seis coisas que quebram fora do artigo
O conselho impopular: antes de montar um pipeline de GNN, meça duas coisas. A homofilia do seu grafo, que a etapa 5 mostrou decidir o sinal do ganho. E quanto uma tabela com features de grafo grau, número de triângulos, tamanho da componente, PageRank adiciona ao modelo tabular que você já tem. Essas colunas custam uma consulta e entram em qualquer gradient boosting. Numa quantidade desconfortável de casos elas capturam a maior parte do ganho, sem nenhum dos problemas desta etapa. Um GNN se paga quando a estrutura é rica, homofílica e não cabe em três colunas o que acontece, só não acontece sempre.
O caminho inteiro
No fim: um GNN é uma rede neural comum com uma multiplicação de matriz a mais por camada. Essa multiplicação é a estrutura entrando na conta, e ela não é um bônus é uma aposta. A aposta é que vizinho se parece com vizinho, e quando ela está certa o ganho é grande. Quando está errada, a mesma operação destrói informação e você teria ido melhor ignorando as arestas. Nenhuma arquitetura resolve isso por você: é uma propriedade dos seus dados, mede-se em uma linha, e é a primeira coisa que vale olhar.