HealthcareGraphRAG / src /gnn /model.py
minhthien's picture
update README.md
2bcda37
Raw
History Blame Contribute Delete
2.84 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import SAGEConv
class VGAEEncoder(nn.Module):
"""
GraphSAGE-based variational encoder for VGAE.
SAGEConv uses mean aggregation, which is degree-agnostic and more stable
than GCNConv's degree-normalised aggregation on sparse knowledge graphs.
Outputs (mu, log_std) for the reparameterisation trick.
"""
def __init__(self, in_channels: int, hidden_channels: int, out_channels: int, dropout: float = 0.1):
super().__init__()
self.conv1 = SAGEConv(in_channels, hidden_channels)
self.conv_mu = SAGEConv(hidden_channels, out_channels)
self.conv_logstd = SAGEConv(hidden_channels, out_channels)
self.dropout = dropout
self._init_weights()
def _init_weights(self):
# Xavier uniform initialisation to control activation magnitude from epoch 0.
for conv in [self.conv1, self.conv_mu, self.conv_logstd]:
nn.init.xavier_uniform_(conv.lin_l.weight)
nn.init.xavier_uniform_(conv.lin_r.weight)
def forward(self, x, edge_index):
x = self.conv1(x, edge_index)
x = F.relu(x)
x = F.dropout(x, p=self.dropout, training=self.training)
mu = self.conv_mu(x, edge_index)
# Clamp both directions: prevents underflow (exp(-10)≈0) and overflow (exp(10)≈22k).
log_std = self.conv_logstd(x, edge_index).clamp(min=-10, max=10)
return mu, log_std
class VGAEModel(nn.Module):
def __init__(self, in_channels: int, hidden_channels: int, out_channels: int, dropout: float = 0.1):
super().__init__()
self.encoder = VGAEEncoder(in_channels, hidden_channels, out_channels, dropout)
def encode(self, x, edge_index):
mu, log_std = self.encoder(x, edge_index)
# At training time use the reparameterisation trick; at inference use the
# deterministic mean mu for reproducible retrieval scores.
if self.training:
std = torch.exp(log_std)
z = mu + torch.randn_like(std) * std
else:
z = mu
self._mu = mu
self._log_std = log_std
return z
def decode(self, z, edge_index, sigmoid=True):
value = (z[edge_index[0]] * z[edge_index[1]]).sum(dim=1)
return torch.sigmoid(value) if sigmoid else value
def kl_loss(self):
"""KL( N(mu, std) || N(0, 1) ) — call after encode()."""
return -0.5 * torch.mean(
1 + 2 * self._log_std - self._mu.pow(2) - (2 * self._log_std).exp()
)
def forward(self, x, edge_index):
return self.encode(x, edge_index)
def get_model(in_channels: int = 384, hidden_channels: int = 256, out_channels: int = 128) -> VGAEModel:
return VGAEModel(in_channels, hidden_channels, out_channels)