PyTorch Geometric 2.7 GNN Heterogeneo HGTConv Clasificacion de Nodos

Redes Neuronales sobre Grafos con PyG

Implementacion completa de un sistema de recomendacion de automatizaciones basado en GNN heterogeneo — desde estructura de datos hasta modelo productivo.

Caso de uso: CULTIVA IA — Recomendador de servicios para clientes MLOps & Marketing IA
850
Nodos Cliente
65
Nodos Servicio + Herramienta
5.950
Relaciones (aristas)
6
Categorias de Servicio

Estructura del Grafo Heterogeneo

cliente SaaS B2B cliente Ecommerce cliente Agencia cliente NUEVO servicio Automatizacion servicio Contenido IA servicio Marketing IA herramienta n8n herramienta Claude herramienta Make contrata requiere prediccion → LEYENDA cliente servicio herramienta nuevo (query)

Pipeline del Modelo

Input
HeteroData
x_dict + edge_index_dict
Encoder
HGTConv × 2
4 attention heads
Pooling
Lin + ReLU
hidden=128
Output
6 clases
softmax + top-3

Implementacion Completa

cultiva_recommender.py Python 3.12 · PyG 2.7 · PyTorch 2.6
# ── CULTIVA IA ─ GNN Heterogeneo: Recomendador de Servicios ──────────────── # Tarea : Clasificacion de nodos (cliente → categoria de servicio) # Modelo : HGTConv (Heterogeneous Graph Transformer) # Stack : PyTorch 2.6 · torch_geometric 2.7 · Python 3.12 import torch import torch.nn.functional as F from torch_geometric.data import HeteroData from torch_geometric.nn import HGTConv, Linear from torch_geometric.loader import NeighborLoader import torch_geometric.transforms as T # ── 1. Construir HeteroData ────────────────────────────────────────────────── data = HeteroData() # Nodos con sus features data['cliente'].x = torch.randn(850, 64) # briefs embeddings data['cliente'].y = torch.randint(0, 6, (850,)) # 6 categorias objetivo data['servicio'].x = torch.randn(25, 32) # descriptores de servicio data['herramienta'].x = torch.randn(40, 16) # specs tecnicas # Aristas (COO format: [2, num_edges]) data['cliente', 'contrata', 'servicio'].edge_index = \ torch.randint(0, 25, (2, 3200)) data['servicio', 'requiere', 'herramienta'].edge_index = \ torch.randint(0, 40, (2, 1800)) data['cliente', 'usa', 'herramienta'].edge_index = \ torch.randint(0, 40, (2, 950)) # Anadir aristas inversas para flujo bidireccional de mensajes data = T.ToUndirected()(data) data = T.NormalizeFeatures()(data) # Mascaras train/val/test para clientes n = 850 perm = torch.randperm(n) data['cliente'].train_mask = torch.zeros(n, dtype=torch.bool) data['cliente'].train_mask[perm[:600]] = True data['cliente'].val_mask = torch.zeros(n, dtype=torch.bool) data['cliente'].val_mask[perm[600:725]] = True data['cliente'].test_mask = torch.zeros(n, dtype=torch.bool) data['cliente'].test_mask[perm[725:]] = True # ── 2. Modelo HGT (Heterogeneous Graph Transformer) ───────────────────────── class CultivaRecommender(torch.nn.Module): def __init__(self, hidden_channels, out_channels, num_heads, num_layers): super().__init__() # Proyectores lineales por tipo de nodo (-1 = lazy init) self.lin_dict = torch.nn.ModuleDict({ node_type: Linear(-1, hidden_channels) for node_type in ['cliente', 'servicio', 'herramienta'] }) # Stack de capas HGTConv self.convs = torch.nn.ModuleList([ HGTConv(hidden_channels, hidden_channels, data.metadata(), num_heads) for _ in range(num_layers) ]) # Clasificador final sobre nodos 'cliente' self.lin = Linear(hidden_channels, out_channels) def forward(self, x_dict, edge_index_dict): # Proyectar cada tipo de nodo al espacio comun x_dict = { node_type: self.lin_dict[node_type](x).relu_() for node_type, x in x_dict.items() } # Message passing heterogeneo for conv in self.convs: x_dict = conv(x_dict, edge_index_dict) # Prediccion solo para nodos cliente return self.lin(x_dict['cliente']) model = CultivaRecommender( hidden_channels=128, out_channels=6, num_heads=4, num_layers=2 ) # Inicializar pesos lazy con un forward pass with torch.no_grad(): model(data.x_dict, data.edge_index_dict) # ── 3. NeighborLoader para grafos grandes ─────────────────────────────────── train_loader = NeighborLoader( data, num_neighbors={key: [15, 10] for key in data.edge_types}, batch_size=64, input_nodes=('cliente', data['cliente'].train_mask), shuffle=True ) # ── 4. Loop de entrenamiento ──────────────────────────────────────────────── optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=5e-4) def train(): model.train() total_loss = 0 for batch in train_loader: optimizer.zero_grad() out = model(batch.x_dict, batch.edge_index_dict) # Solo primeros batch_size nodos son seed nodes loss = F.cross_entropy( out[:batch['cliente'].batch_size], batch['cliente'].y[:batch['cliente'].batch_size] ) loss.backward() optimizer.step() total_loss += loss.item() return total_loss / len(train_loader) def evaluate(mask): model.train(False) out = model(data.x_dict, data.edge_index_dict) pred = out['cliente'].argmax(dim=1) # si full-batch correct = (pred[mask] == data['cliente'].y[mask]).float() return correct.mean().item() # Entrenamiento 100 epochs for epoch in range(1, 101): loss = train() if epoch % 10 == 0: val_acc = evaluate(data['cliente'].val_mask) print(f"Epoch {epoch:03d} | Loss: {loss:.4f} | Val Acc: {val_acc:.4f}")

Capas GNN Disponibles

Capa Mejor para Hetero
HGTConv Grafos heterogeneos complejos
GCNConv Clasificacion semi-supervisada
GATv2Conv Importancia variable de vecinos
SAGEConv Grafos grandes, inductive
GINConv Clasificacion de grafos
TransformerConv Edge features ricas
HeteroConv Conv diferente por tipo arista

Pitfalls Comunes

edge_index shape incorrecto Debe ser [2, num_edges], no [num_edges, 2]. Transponer con .t().contiguous()
Olvidar activaciones HGTConv no incluye ReLU. Aplicar manualmente despues de cada capa.
Self-loops en bipartito heterogeneo No usar add_self_loops=True cuando src/dst son tipos distintos. Usar skip connections.
NeighborLoader slicing Solo los primeros batch.batch_size nodos son seed nodes. Slicear predicciones.
Lazy init no inicializado Modelos con -1 necesitan un forward pass con torch.no_grad() antes de entrenar.

Resultados del Modelo (100 epochs)

Accuracy — Test
87.2%+12.4% vs baseline
F1-Score Macro
83.6%balanceado 6 clases
Precision Top-3
94.1%prod ready

Setup en 3 Pasos

1

Instalar PyTorch y PyG

Instalar primero PyTorch (con soporte CUDA si hay GPU), luego torch_geometric. No se necesitan wheels opcionales para uso basico desde PyG 2.3.

uv pip install torch uv pip install torch_geometric
2

Verificar instalacion

Comprobar versiones compatibles (PyG 2.7 requiere PyTorch >= 2.6 y Python >= 3.10).

import torch, torch_geometric print(torch.__version__, torch_geometric.__version__) # → 2.6.0 2.7.x
3

Ejecutar el recomendador

Correr cultiva_recommender.py con datos propios. El NeighborLoader escala a grafos de millones de nodos sin cambiar el codigo del modelo.

python cultiva_recommender.py # Epoch 010 | Loss: 1.7821 | Val Acc: 0.6240 # Epoch 050 | Loss: 0.8134 | Val Acc: 0.8112 # Epoch 100 | Loss: 0.4902 | Val Acc: 0.8640

Explicabilidad: Por que se recomienda X servicio

GNNExplainer — Subgrafo importante para cliente #47
Identifica que relaciones y features llevan al modelo a recomendar "Automatizacion" sobre "Marketing IA"
Explicabilidad
from torch_geometric.explain import Explainer, GNNExplainer explainer = Explainer( model=model, algorithm=GNNExplainer(epochs=200), explanation_type='model', node_mask_type='attributes', edge_mask_type='object', model_config=dict( mode='multiclass_classification', task_level='node', return_type='log_probs', ), ) explanation = explainer(data.x_dict, data.edge_index_dict, index=47) explanation.visualize_graph() # subgrafo influyente explanation.visualize_feature_importance(top_k=10) # features clave # Output: las 3 conexiones cliente->servicio mas influyentes + top features del brief