| """
|
| Graph Neural Network models for retention time prediction.
|
| Includes GCN, GIN, GAT, and ensemble models.
|
| """
|
|
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| from torch_geometric.nn import (
|
| GCNConv, GINConv, GATConv, GINEConv, GlobalAttention,
|
| global_mean_pool, global_max_pool, global_add_pool
|
| )
|
| from torch_geometric.nn.norm import BatchNorm, GraphNorm, LayerNorm
|
| from typing import Optional, List, Dict, Sequence, Any
|
| import numpy as np
|
|
|
|
|
| class GraphConvModel(nn.Module):
|
| """Base Graph Convolutional Network model."""
|
|
|
| def __init__(self, input_dim: int, hidden_dim: int = 128, output_dim: int = 1,
|
| num_layers: int = 3, dropout: float = 0.2,
|
| num_labs: int = 23, lab_embed_dim: int = 16):
|
| super().__init__()
|
|
|
| self.input_dim = input_dim
|
| self.hidden_dim = hidden_dim
|
| self.num_layers = num_layers
|
| self.dropout = dropout
|
|
|
|
|
| self.lab_embedding = nn.Embedding(num_labs, lab_embed_dim)
|
|
|
|
|
| self.convs = nn.ModuleList()
|
| self.batch_norms = nn.ModuleList()
|
|
|
|
|
| self.convs.append(GCNConv(input_dim, hidden_dim))
|
| self.batch_norms.append(GraphNorm(hidden_dim))
|
|
|
|
|
| for _ in range(num_layers - 1):
|
| self.convs.append(GCNConv(hidden_dim, hidden_dim))
|
| self.batch_norms.append(GraphNorm(hidden_dim))
|
|
|
|
|
| self.global_pool = global_mean_pool
|
|
|
|
|
| final_input_dim = hidden_dim + lab_embed_dim
|
| self.predictor = nn.Sequential(
|
| nn.Linear(final_input_dim, hidden_dim // 2),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim // 2, hidden_dim // 4),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim // 4, output_dim)
|
| )
|
|
|
| def forward(self, x, edge_index, batch, lab_feature, edge_attr=None):
|
|
|
| for conv, norm in zip(self.convs, self.batch_norms):
|
|
|
| edge_weight = None
|
| if edge_attr is not None and edge_attr.size(1) == 1:
|
| edge_weight = edge_attr.squeeze(-1)
|
|
|
| x = conv(x, edge_index, edge_weight=edge_weight)
|
| x = norm(x, batch)
|
| x = F.relu(x)
|
| x = F.dropout(x, p=self.dropout, training=self.training)
|
|
|
|
|
| graph_repr = self.global_pool(x, batch)
|
|
|
|
|
| if lab_feature.dim() == 1:
|
| lab_embed = self.lab_embedding(lab_feature)
|
| else:
|
| lab_embed = self.lab_embedding(lab_feature.squeeze(-1))
|
|
|
|
|
| combined = torch.cat([graph_repr, lab_embed], dim=1)
|
|
|
|
|
| output = self.predictor(combined)
|
| return output.squeeze()
|
|
|
|
|
| class GATModel(nn.Module):
|
| """Graph Attention Network (GAT) model."""
|
|
|
| def __init__(self, input_dim: int, hidden_dim: int = 128, output_dim: int = 1,
|
| num_layers: int = 3, dropout: float = 0.2, num_heads: int = 4,
|
| num_labs: int = 23, lab_embed_dim: int = 16):
|
| super().__init__()
|
|
|
| self.input_dim = input_dim
|
| self.hidden_dim = hidden_dim
|
| self.num_layers = num_layers
|
| self.dropout = dropout
|
| self.num_heads = num_heads
|
|
|
|
|
| self.lab_embedding = nn.Embedding(num_labs, lab_embed_dim)
|
|
|
|
|
| self.convs = nn.ModuleList()
|
| self.batch_norms = nn.ModuleList()
|
|
|
|
|
| head_dim = hidden_dim // num_heads
|
|
|
|
|
| self.convs.append(GATConv(input_dim, head_dim, heads=num_heads, dropout=dropout, concat=True))
|
| self.batch_norms.append(GraphNorm(hidden_dim))
|
|
|
|
|
| for _ in range(num_layers - 2):
|
| self.convs.append(GATConv(hidden_dim, head_dim, heads=num_heads, dropout=dropout, concat=True))
|
| self.batch_norms.append(GraphNorm(hidden_dim))
|
|
|
|
|
| if num_layers > 1:
|
| self.convs.append(GATConv(hidden_dim, hidden_dim, heads=1, dropout=dropout, concat=False))
|
| self.batch_norms.append(GraphNorm(hidden_dim))
|
|
|
|
|
| self.global_pool = global_mean_pool
|
|
|
|
|
| final_input_dim = hidden_dim + lab_embed_dim
|
| self.predictor = nn.Sequential(
|
| nn.Linear(final_input_dim, hidden_dim // 2),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim // 2, hidden_dim // 4),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim // 4, output_dim)
|
| )
|
|
|
| def forward(self, x, edge_index, batch, lab_feature, edge_attr=None):
|
|
|
| for i, (conv, norm) in enumerate(zip(self.convs, self.batch_norms)):
|
| x = conv(x, edge_index)
|
| x = norm(x, batch)
|
| if i < len(self.convs) - 1:
|
| x = F.relu(x)
|
| x = F.dropout(x, p=self.dropout, training=self.training)
|
|
|
|
|
| graph_repr = self.global_pool(x, batch)
|
|
|
|
|
| if lab_feature.dim() == 1:
|
| lab_embed = self.lab_embedding(lab_feature)
|
| else:
|
| lab_embed = self.lab_embedding(lab_feature.squeeze(-1))
|
|
|
|
|
| combined = torch.cat([graph_repr, lab_embed], dim=1)
|
|
|
|
|
| output = self.predictor(combined)
|
| return output.squeeze()
|
|
|
|
|
| class MoleculeMPNNModel(nn.Module):
|
| """Edge-aware message passing network tailored for molecular graphs."""
|
|
|
| def __init__(
|
| self,
|
| input_dim: int,
|
| hidden_dim: int = 256,
|
| output_dim: int = 1,
|
| num_layers: int = 4,
|
| dropout: float = 0.2,
|
| edge_dim: int = 4,
|
| num_labs: int = 23,
|
| lab_embed_dim: int = 16,
|
| use_batch_norm: bool = True,
|
| ):
|
| super().__init__()
|
|
|
| self.hidden_dim = hidden_dim
|
| self.num_layers = num_layers
|
| self.dropout = dropout
|
| self.use_batch_norm = use_batch_norm
|
|
|
| self.input_proj = nn.Linear(input_dim, hidden_dim)
|
| self.edge_encoder = nn.Linear(edge_dim, hidden_dim) if edge_dim > 0 else None
|
| self.lab_embedding = nn.Embedding(num_labs, lab_embed_dim)
|
|
|
| self.convs = nn.ModuleList()
|
| self.norms = nn.ModuleList()
|
|
|
| for _ in range(num_layers):
|
| mlp = nn.Sequential(
|
| nn.Linear(hidden_dim, hidden_dim),
|
| nn.ReLU(),
|
| nn.Linear(hidden_dim, hidden_dim),
|
| )
|
| self.convs.append(GINEConv(mlp, train_eps=True))
|
| if use_batch_norm:
|
| self.norms.append(BatchNorm(hidden_dim))
|
| else:
|
| self.norms.append(GraphNorm(hidden_dim))
|
|
|
| pooled_dim = hidden_dim * 2
|
| final_input_dim = pooled_dim + lab_embed_dim
|
|
|
| self.predictor = nn.Sequential(
|
| nn.Linear(final_input_dim, hidden_dim),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim, hidden_dim // 2),
|
| nn.ReLU(),
|
| nn.Dropout(dropout),
|
| nn.Linear(hidden_dim // 2, output_dim),
|
| )
|
|
|
| def forward(self, x, edge_index, batch, lab_feature, edge_attr=None):
|
| x = self.input_proj(x)
|
| encoded_edge_attr = edge_attr
|
| if edge_attr is not None and self.edge_encoder is not None:
|
| encoded_edge_attr = self.edge_encoder(edge_attr)
|
|
|
| for conv, norm in zip(self.convs, self.norms):
|
| x = conv(x, edge_index, encoded_edge_attr)
|
| if isinstance(norm, BatchNorm):
|
| x = norm(x)
|
| else:
|
| x = norm(x, batch)
|
| x = F.relu(x)
|
| x = F.dropout(x, p=self.dropout, training=self.training)
|
|
|
| mean_pool = global_mean_pool(x, batch)
|
| max_pool = global_max_pool(x, batch)
|
| graph_repr = torch.cat([mean_pool, max_pool], dim=1)
|
|
|
| if lab_feature.dim() == 1:
|
| lab_embed = self.lab_embedding(lab_feature)
|
| else:
|
| lab_embed = self.lab_embedding(lab_feature.squeeze(-1))
|
|
|
| combined = torch.cat([graph_repr, lab_embed], dim=1)
|
| output = self.predictor(combined)
|
| return output.squeeze()
|
|
|
|
|
|
|
| GINEModel = MoleculeMPNNModel
|
|
|
|
|
| class HybridModel(nn.Module):
|
| """Hybrid model combining graph features with molecular descriptors."""
|
|
|
| def __init__(
|
| self,
|
| *,
|
| graph_model_class,
|
| descriptor_dim: int,
|
| graph_model_kwargs: Optional[Dict[str, Any]] = None,
|
| graph_feature_dim: Optional[int] = None,
|
| descriptor_hidden_dims: Optional[Sequence[int]] = None,
|
| final_hidden_dims: Optional[Sequence[int]] = None,
|
| dropout: float = 0.2,
|
| use_batch_norm: bool = True,
|
| output_dim: int = 1,
|
| ) -> None:
|
| super().__init__()
|
|
|
| graph_model_kwargs = dict(graph_model_kwargs or {})
|
|
|
| if graph_feature_dim is None:
|
| graph_feature_dim = graph_model_kwargs.get("hidden_dim")
|
| if graph_feature_dim is None:
|
| graph_feature_dim = 128
|
|
|
| graph_model_kwargs.setdefault("hidden_dim", graph_feature_dim)
|
| graph_model_kwargs.setdefault("output_dim", graph_feature_dim)
|
|
|
| self.graph_model = graph_model_class(**graph_model_kwargs)
|
| self.graph_feature_dim = graph_feature_dim
|
| self.dropout = dropout
|
| self.use_batch_norm = use_batch_norm
|
|
|
| if descriptor_hidden_dims is None or len(descriptor_hidden_dims) == 0:
|
| self.descriptor_net = nn.Identity()
|
| self.descriptor_output_dim = descriptor_dim
|
| else:
|
| descriptor_layers: List[nn.Module] = []
|
| in_dim = descriptor_dim
|
| for hidden_dim in descriptor_hidden_dims:
|
| descriptor_layers.append(nn.Linear(in_dim, hidden_dim))
|
| if use_batch_norm:
|
| descriptor_layers.append(nn.BatchNorm1d(hidden_dim))
|
| descriptor_layers.append(nn.ReLU())
|
| if dropout > 0:
|
| descriptor_layers.append(nn.Dropout(dropout))
|
| in_dim = hidden_dim
|
| self.descriptor_net = nn.Sequential(*descriptor_layers)
|
| self.descriptor_output_dim = in_dim
|
|
|
| if final_hidden_dims is None or len(final_hidden_dims) == 0:
|
| final_hidden_dims = [max(graph_feature_dim // 2, 1)]
|
|
|
| final_layers: List[nn.Module] = []
|
| in_dim = self.graph_feature_dim + self.descriptor_output_dim
|
| for hidden_dim in final_hidden_dims:
|
| final_layers.append(nn.Linear(in_dim, hidden_dim))
|
| if use_batch_norm:
|
| final_layers.append(nn.BatchNorm1d(hidden_dim))
|
| final_layers.append(nn.ReLU())
|
| if dropout > 0:
|
| final_layers.append(nn.Dropout(dropout))
|
| in_dim = hidden_dim
|
| final_layers.append(nn.Linear(in_dim, output_dim))
|
|
|
| self.final_predictor = nn.Sequential(*final_layers)
|
|
|
| def forward(self, x, edge_index, batch, lab_feature, descriptors, edge_attr=None):
|
| graph_features = self.graph_model(
|
| x,
|
| edge_index,
|
| batch,
|
| lab_feature,
|
| edge_attr,
|
| )
|
| if graph_features.dim() == 1:
|
| graph_features = graph_features.unsqueeze(0)
|
|
|
| if descriptors.dim() == 1:
|
| descriptors = descriptors.unsqueeze(0)
|
|
|
| desc_features = self.descriptor_net(descriptors)
|
| if isinstance(self.descriptor_net, nn.Identity):
|
| desc_features = descriptors
|
|
|
| combined = torch.cat([graph_features, desc_features], dim=1)
|
| output = self.final_predictor(combined)
|
|
|
| return output.squeeze(-1)
|
|
|
|
|
| def create_model(model_type: str, input_dim: int, num_labs: int = 23, **kwargs):
|
| """Factory function to create models."""
|
|
|
| models = {
|
| 'gcn': GraphConvModel,
|
| 'gin': GINEModel,
|
| 'gat': GATModel,
|
| 'mpnn': MoleculeMPNNModel,
|
| }
|
|
|
| if model_type not in models:
|
| raise ValueError(f"Unknown model type: {model_type}")
|
|
|
| return models[model_type](input_dim=input_dim, num_labs=num_labs, **kwargs)
|
|
|
|
|
| class EarlyStopping:
|
| """Early stopping utility."""
|
|
|
| def __init__(self, patience: int = 10, min_delta: float = 0.0, restore_best_weights: bool = True):
|
| self.patience = patience
|
| self.min_delta = min_delta
|
| self.restore_best_weights = restore_best_weights
|
| self.best_loss = None
|
| self.counter = 0
|
| self.best_weights = None
|
|
|
| def __call__(self, val_loss: float, model: nn.Module) -> bool:
|
| if self.best_loss is None:
|
| self.best_loss = val_loss
|
| self.save_checkpoint(model)
|
| elif val_loss < self.best_loss - self.min_delta:
|
| self.best_loss = val_loss
|
| self.counter = 0
|
| self.save_checkpoint(model)
|
| else:
|
| self.counter += 1
|
|
|
| if self.counter >= self.patience:
|
| if self.restore_best_weights and self.best_weights is not None:
|
| model.load_state_dict(self.best_weights)
|
| return True
|
| return False
|
|
|
| def save_checkpoint(self, model: nn.Module):
|
| """Save model weights."""
|
| self.best_weights = model.state_dict().copy()
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
|
|
|
|
| batch_size = 32
|
| input_dim = 7
|
| num_nodes = 20
|
| num_edges = 40
|
|
|
| x = torch.randn(num_nodes, input_dim)
|
| edge_index = torch.randint(0, num_nodes, (2, num_edges))
|
| batch = torch.zeros(num_nodes, dtype=torch.long)
|
| lab_feature = torch.randint(0, 23, (1,))
|
|
|
|
|
| models = ['gcn', 'gin', 'gat']
|
|
|
| for model_type in models:
|
| print(f"\nTesting {model_type.upper()} model:")
|
| model = create_model(model_type, input_dim=input_dim, hidden_dim=64)
|
| model.eval()
|
|
|
| with torch.no_grad():
|
| output = model(x, edge_index, batch, lab_feature)
|
| print(f"Output shape: {output.shape}")
|
| print(f"Output value: {output.item():.4f}") |