import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import SAGEConv, JumpingKnowledge NUM_LAYERS = 3 DROPOUT = 0.3 class ProteinGNN(nn.Module): """ GraphSAGE + Jumping Knowledge — architecture matches training exactly. input_dim : ESM2 embedding size (1280) hidden_dim : SAGEConv hidden channels (512) output_dim : number of GO terms (4201) """ def __init__(self, input_dim: int, hidden_dim: int, output_dim: int): super().__init__() self.convs = nn.ModuleList() self.norms = nn.ModuleList() for i in range(NUM_LAYERS): in_c = input_dim if i == 0 else hidden_dim self.convs.append(SAGEConv(in_c, hidden_dim)) self.norms.append(nn.LayerNorm(hidden_dim)) self.jk = JumpingKnowledge("cat") jk_dim = hidden_dim * NUM_LAYERS # 512 * 3 = 1536 self.classifier = nn.Sequential( nn.Linear(jk_dim, hidden_dim), nn.ReLU(), nn.Dropout(DROPOUT), nn.Linear(hidden_dim, output_dim), ) def forward(self, x: torch.Tensor, edge_index: torch.Tensor) -> torch.Tensor: layer_outs = [] for conv, norm in zip(self.convs, self.norms): x = conv(x, edge_index) x = norm(x) x = F.relu(x) x = F.dropout(x, p=DROPOUT, training=self.training) layer_outs.append(x) x = self.jk(layer_outs) return self.classifier(x) # ── Inductive inference (no graph needed) ───────────────────────────── def forward_inductive(self, x: torch.Tensor) -> torch.Tensor: """ Skip graph convolutions — run classifier on raw ESM2 embedding. Input : (1, 1280) Output : (1, 4201) logits → apply sigmoid for probabilities. """ # Simulate 3 JK layers with zeros so the jk_dim (1536) still matches # We pass the embedding through the classifier directly by padding batch = x.shape[0] # Repeat x three times to match jk_dim = hidden_dim * 3 = 1536 # First project from 1280 → 512 (same as a single SAGEConv would give) # then concat 3 times # We use only the final Linear layers of the classifier # Project: 1280 → 512 using first Linear weight (reuse layer 0 of norms as proxy) # Simpler: just pad with zeros to match 1536 pad = torch.zeros(batch, 1536 - x.shape[1], device=x.device, dtype=x.dtype) x_padded = torch.cat([x, pad], dim=1) # (B, 1536) return self.classifier(x_padded)