| """
|
| Module: model.py
|
| Description:
|
| This module defines the neural network architecture for the REDDA framework used in drug repositioning.
|
| It includes several components such as a link prediction decoder, heterogeneous graph convolutional layers,
|
| semantic attention mechanisms, and graph attention modules. Adapted for the new knowledge graph dataset.
|
| """
|
|
|
| import torch
|
| import torch.nn as nn
|
| import dgl.nn as dglnn
|
| import dgl
|
|
|
|
|
| class InnerProductDecoder(nn.Module):
|
| """
|
| Decoder layer that computes the inner product between drug and disease embeddings for link prediction.
|
| Optionally applies a linear transformation to the disease embeddings.
|
| """
|
|
|
| def __init__(self, input_dim=None, dropout=0.4):
|
| """
|
| Parameters:
|
| input_dim (int, optional): Dimension of input features. If provided, a linear layer is used.
|
| dropout (float): Dropout rate.
|
| """
|
| super(InnerProductDecoder, self).__init__()
|
| self.dropout = nn.Dropout(dropout)
|
| if input_dim:
|
| self.weights = nn.Linear(input_dim, input_dim, bias=False)
|
| nn.init.xavier_uniform_(self.weights.weight)
|
|
|
| def forward(self, feature):
|
| """
|
| Forward pass to compute the similarity score matrix.
|
|
|
| Parameters:
|
| feature (dict): Dictionary containing node features for 'drug' and 'disease'.
|
|
|
| Returns:
|
| torch.Tensor: The computed score matrix with shape [num_drugs, num_diseases].
|
| """
|
|
|
| feature["drug"] = self.dropout(feature["drug"])
|
| feature["disease"] = self.dropout(feature["disease"])
|
| R = feature["drug"]
|
| D = self.weights(feature["disease"]) if hasattr(self, "weights") else feature["disease"]
|
| outputs = R @ D.T
|
| return outputs
|
|
|
|
|
| class Node_Embedding(nn.Module):
|
| """
|
| Basic heterogeneous graph convolution layer to generate node embeddings for each relation type.
|
| """
|
|
|
| def __init__(self, in_feats, out_feats, dropout, rel_names):
|
| """
|
| Parameters:
|
| in_feats (int): Input feature dimension.
|
| out_feats (int): Output feature dimension.
|
| dropout (float): Dropout rate.
|
| rel_names (list): List of relation names to construct individual GraphConv layers.
|
| """
|
| super().__init__()
|
|
|
| hetero_conv_dict = {}
|
| for rel in rel_names:
|
| graphconv = dglnn.GraphConv(in_feats, out_feats)
|
| nn.init.xavier_normal_(graphconv.weight)
|
| hetero_conv_dict[rel] = graphconv
|
| self.dropout = nn.Dropout(p=dropout)
|
| self.embedding = dglnn.HeteroGraphConv(hetero_conv_dict, aggregate="sum")
|
| self.bn_layer = nn.BatchNorm1d(out_feats)
|
| self.prelu = nn.PReLU()
|
|
|
| def forward(self, graph, inputs, bn=False, dp=False):
|
| """
|
| Forward pass for the node embedding.
|
|
|
| Parameters:
|
| graph (dgl.DGLHeteroGraph): Input heterogeneous graph.
|
| inputs (dict): Dictionary of input node features keyed by node type.
|
| bn (bool): Whether to apply batch normalization.
|
| dp (bool): Whether to apply dropout.
|
|
|
| Returns:
|
| dict: Updated node features keyed by node type.
|
| """
|
| h = self.embedding(graph, inputs)
|
|
|
| if bn and dp:
|
| h = {k: self.prelu(self.dropout(self.bn_layer(v))) for k, v in h.items()}
|
| elif dp:
|
| h = {k: self.prelu(self.dropout(v)) for k, v in h.items()}
|
| elif bn:
|
| h = {k: self.prelu(self.bn_layer(v)) for k, v in h.items()}
|
| else:
|
| h = {k: self.prelu(v) for k, v in h.items()}
|
| return h
|
|
|
|
|
| class SemanticAttention(nn.Module):
|
| """
|
| Attention mechanism to aggregate multiple embeddings.
|
| """
|
|
|
| def __init__(self, in_feats, hidden_size=128):
|
| """
|
| Parameters:
|
| in_feats (int): Dimension of input features.
|
| hidden_size (int): Hidden layer size for attention projection.
|
| """
|
| super(SemanticAttention, self).__init__()
|
| self.project = nn.Sequential(
|
| nn.Linear(in_feats, hidden_size),
|
| nn.Tanh(),
|
| nn.Linear(hidden_size, 1, bias=False),
|
| )
|
|
|
| def forward(self, z, is_print=False):
|
| """
|
| Apply semantic attention to a stack of embeddings.
|
|
|
| Parameters:
|
| z (torch.Tensor): Tensor of shape [batch, num_layers, in_feats].
|
| is_print (bool): If True, prints the attention weights.
|
|
|
| Returns:
|
| torch.Tensor: Aggregated embedding of shape [batch, in_feats].
|
| """
|
| w = self.project(z).mean(0)
|
| beta = torch.softmax(w, dim=0)
|
| beta = beta.expand((z.shape[0],) + beta.shape)
|
| if is_print:
|
| print(beta)
|
| return (beta * z).sum(1)
|
|
|
|
|
| class SubnetworkEncoder(nn.Module):
|
| """
|
| Module to compute topological subnetwork embeddings using multiple heterogeneous graph convolution blocks.
|
| Adapted for the new knowledge graph with multiple node types.
|
| """
|
|
|
| def __init__(self, ntypes, in_feats, out_feats, dropout):
|
| """
|
| Parameters:
|
| ntypes (list): List of node types present in the graph.
|
| in_feats (int): Input feature dimension.
|
| out_feats (int): Output feature dimension.
|
| dropout (float): Dropout rate.
|
| """
|
| super(SubnetworkEncoder, self).__init__()
|
| self.ntypes = ntypes
|
|
|
|
|
|
|
| self.drug_disease = Node_Embedding(in_feats, out_feats, dropout,
|
| ['drug_drug', 'drug_disease_indication', 'disease_disease'])
|
|
|
| self.drug_protein = Node_Embedding(in_feats, out_feats, dropout,
|
| ['drug_drug', 'drug_protein', 'protein_protein'])
|
|
|
| self.drug_effect = Node_Embedding(in_feats, out_feats, dropout,
|
| ['drug_drug', 'drug_effect'])
|
|
|
|
|
| self.protein_bioprocess = Node_Embedding(in_feats, out_feats, dropout,
|
| ['protein_protein', 'protein_bioprocess', 'bioprocess_bioprocess'])
|
|
|
| self.protein_cellcomp = Node_Embedding(in_feats, out_feats, dropout,
|
| ['protein_protein', 'protein_cellcomp', 'cellcomp_cellcomp'])
|
|
|
| self.protein_molfunc = Node_Embedding(in_feats, out_feats, dropout,
|
| ['protein_protein', 'protein_molfunc', 'molfunc_molfunc'])
|
|
|
| self.protein_pathway = Node_Embedding(in_feats, out_feats, dropout,
|
| ['protein_protein', 'protein_pathway', 'pathway_pathway'])
|
|
|
| self.protein_disease = Node_Embedding(in_feats, out_feats, dropout,
|
| ['protein_protein', 'protein_disease', 'disease_disease'])
|
|
|
|
|
| self.disease_phenotype = Node_Embedding(in_feats, out_feats, dropout,
|
| ['disease_disease', 'disease_phenotype_positive', 'phenotype_phenotype'])
|
|
|
| self.disease_exposure = Node_Embedding(in_feats, out_feats, dropout,
|
| ['disease_disease', 'disease_exposure'])
|
|
|
|
|
| self.bioprocess_network = Node_Embedding(in_feats, out_feats, dropout,
|
| ['bioprocess_bioprocess'])
|
|
|
| self.cellcomp_network = Node_Embedding(in_feats, out_feats, dropout,
|
| ['cellcomp_cellcomp'])
|
|
|
| self.molfunc_network = Node_Embedding(in_feats, out_feats, dropout,
|
| ['molfunc_molfunc'])
|
|
|
| self.pathway_network = Node_Embedding(in_feats, out_feats, dropout,
|
| ['pathway_pathway'])
|
|
|
| self.phenotype_network = Node_Embedding(in_feats, out_feats, dropout,
|
| ['phenotype_phenotype'])
|
|
|
| self.semantic_attention = SemanticAttention(in_feats=out_feats)
|
|
|
| def forward(self, g, h, bn=False, dp=False):
|
| """
|
| Compute subnetwork embeddings and aggregate them using semantic attention.
|
|
|
| Parameters:
|
| g (dgl.DGLHeteroGraph): Input heterogeneous graph.
|
| h (dict): Dictionary of node features.
|
| bn (bool): Whether to apply batch normalization.
|
| dp (bool): Whether to apply dropout.
|
|
|
| Returns:
|
| dict: Updated node features for each node type.
|
| """
|
| new_h = {ntype: [] for ntype in self.ntypes}
|
|
|
|
|
| if 'drug' in self.ntypes and 'disease' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['drug_drug', 'drug_disease_indication', 'disease_disease'])
|
| h_sub = self.drug_disease(subgraph, {'drug': h['drug'], 'disease': h['disease']}, bn, dp)
|
| new_h['drug'].append(h_sub['drug'])
|
| new_h['disease'].append(h_sub['disease'])
|
| except:
|
| pass
|
|
|
|
|
| if 'drug' in self.ntypes and 'protein' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['drug_drug', 'drug_protein', 'protein_protein'])
|
| h_sub = self.drug_protein(subgraph, {'drug': h['drug'], 'protein': h['protein']}, bn, dp)
|
| new_h['drug'].append(h_sub['drug'])
|
| new_h['protein'].append(h_sub['protein'])
|
| except:
|
| pass
|
|
|
|
|
| if 'drug' in self.ntypes and 'effect' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['drug_drug', 'drug_effect'])
|
| h_sub = self.drug_effect(subgraph, {'drug': h['drug'], 'effect': h['effect']}, bn, dp)
|
| new_h['drug'].append(h_sub['drug'])
|
| new_h['effect'].append(h_sub['effect'])
|
| except:
|
| pass
|
|
|
|
|
| if 'protein' in self.ntypes and 'bioprocess' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['protein_protein', 'protein_bioprocess', 'bioprocess_bioprocess'])
|
| h_sub = self.protein_bioprocess(subgraph, {'protein': h['protein'], 'bioprocess': h['bioprocess']}, bn, dp)
|
| new_h['protein'].append(h_sub['protein'])
|
| new_h['bioprocess'].append(h_sub['bioprocess'])
|
| except:
|
| pass
|
|
|
|
|
| if 'protein' in self.ntypes and 'cellcomp' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['protein_protein', 'protein_cellcomp', 'cellcomp_cellcomp'])
|
| h_sub = self.protein_cellcomp(subgraph, {'protein': h['protein'], 'cellcomp': h['cellcomp']}, bn, dp)
|
| new_h['protein'].append(h_sub['protein'])
|
| new_h['cellcomp'].append(h_sub['cellcomp'])
|
| except:
|
| pass
|
|
|
|
|
| if 'protein' in self.ntypes and 'molfunc' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['protein_protein', 'protein_molfunc', 'molfunc_molfunc'])
|
| h_sub = self.protein_molfunc(subgraph, {'protein': h['protein'], 'molfunc': h['molfunc']}, bn, dp)
|
| new_h['protein'].append(h_sub['protein'])
|
| new_h['molfunc'].append(h_sub['molfunc'])
|
| except:
|
| pass
|
|
|
|
|
| if 'protein' in self.ntypes and 'pathway' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['protein_protein', 'protein_pathway', 'pathway_pathway'])
|
| h_sub = self.protein_pathway(subgraph, {'protein': h['protein'], 'pathway': h['pathway']}, bn, dp)
|
| new_h['protein'].append(h_sub['protein'])
|
| new_h['pathway'].append(h_sub['pathway'])
|
| except:
|
| pass
|
|
|
|
|
| if 'protein' in self.ntypes and 'disease' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['protein_protein', 'protein_disease', 'disease_disease'])
|
| h_sub = self.protein_disease(subgraph, {'protein': h['protein'], 'disease': h['disease']}, bn, dp)
|
| new_h['protein'].append(h_sub['protein'])
|
| new_h['disease'].append(h_sub['disease'])
|
| except:
|
| pass
|
|
|
|
|
| if 'disease' in self.ntypes and 'phenotype' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['disease_disease', 'disease_phenotype_positive', 'phenotype_phenotype'])
|
| h_sub = self.disease_phenotype(subgraph, {'disease': h['disease'], 'phenotype': h['phenotype']}, bn, dp)
|
| new_h['disease'].append(h_sub['disease'])
|
| new_h['phenotype'].append(h_sub['phenotype'])
|
| except:
|
| pass
|
|
|
|
|
| if 'disease' in self.ntypes and 'exposure' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['disease_disease', 'disease_exposure'])
|
| h_sub = self.disease_exposure(subgraph, {'disease': h['disease'], 'exposure': h['exposure']}, bn, dp)
|
| new_h['disease'].append(h_sub['disease'])
|
| new_h['exposure'].append(h_sub['exposure'])
|
| except:
|
| pass
|
|
|
|
|
| if 'bioprocess' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['bioprocess_bioprocess'])
|
| h_sub = self.bioprocess_network(subgraph, {'bioprocess': h['bioprocess']}, bn, dp)
|
| new_h['bioprocess'].append(h_sub['bioprocess'])
|
| except:
|
| pass
|
|
|
| if 'cellcomp' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['cellcomp_cellcomp'])
|
| h_sub = self.cellcomp_network(subgraph, {'cellcomp': h['cellcomp']}, bn, dp)
|
| new_h['cellcomp'].append(h_sub['cellcomp'])
|
| except:
|
| pass
|
|
|
| if 'molfunc' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['molfunc_molfunc'])
|
| h_sub = self.molfunc_network(subgraph, {'molfunc': h['molfunc']}, bn, dp)
|
| new_h['molfunc'].append(h_sub['molfunc'])
|
| except:
|
| pass
|
|
|
| if 'pathway' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['pathway_pathway'])
|
| h_sub = self.pathway_network(subgraph, {'pathway': h['pathway']}, bn, dp)
|
| new_h['pathway'].append(h_sub['pathway'])
|
| except:
|
| pass
|
|
|
| if 'phenotype' in self.ntypes:
|
| try:
|
| subgraph = g.edge_type_subgraph(['phenotype_phenotype'])
|
| h_sub = self.phenotype_network(subgraph, {'phenotype': h['phenotype']}, bn, dp)
|
| new_h['phenotype'].append(h_sub['phenotype'])
|
| except:
|
| pass
|
|
|
|
|
| for ntype in self.ntypes:
|
| if new_h[ntype]:
|
| h[ntype] = torch.stack(new_h[ntype], dim=1)
|
| h[ntype] = self.semantic_attention(h[ntype])
|
| return h
|
|
|
|
|
| class Graph_attention(nn.Module):
|
| """
|
| Multi-omics graph attention block that aggregates information from all node types.
|
| """
|
|
|
| def __init__(self, in_feats, out_feats, num_heads, dropout):
|
| """
|
| Parameters:
|
| in_feats (int): Input feature dimension.
|
| out_feats (int): Output feature dimension.
|
| num_heads (int): Number of attention heads.
|
| dropout (float): Dropout rate.
|
| """
|
| super().__init__()
|
| self.gat = dglnn.GATConv(in_feats, out_feats, num_heads,
|
| dropout, dropout,
|
| activation=nn.PReLU(),
|
| allow_zero_in_degree=True)
|
| self.gat.reset_parameters()
|
| self.linear = nn.Linear(in_feats * num_heads, out_feats)
|
| self.prelu = nn.PReLU()
|
| self.bn_layer = nn.BatchNorm1d(out_feats)
|
|
|
| def forward(self, graph, inputs, bn=False):
|
| """
|
| Forward pass for graph attention.
|
|
|
| Parameters:
|
| graph (dgl.DGLHeteroGraph): Input heterogeneous graph.
|
| inputs (dict): Dictionary of node features.
|
| bn (bool): Whether to apply batch normalization.
|
|
|
| Returns:
|
| tuple: Aggregated disease and drug embeddings.
|
| """
|
|
|
| num_dis = graph.num_nodes("disease")
|
| num_drug = graph.num_nodes("drug")
|
| new_g = dgl.to_homogeneous(graph)
|
| new_h = torch.cat([feat for feat in inputs.values()], dim=0)
|
| new_h = self.gat(new_g, new_h)
|
| new_h = self.prelu(torch.mean(new_h, dim=1))
|
| if bn:
|
| return self.bn_layer(new_h[:num_dis]), self.bn_layer(new_h[num_dis:num_drug + num_dis])
|
| return new_h[:num_dis], new_h[num_dis:num_drug + num_dis]
|
|
|
|
|
| class Model(nn.Module):
|
| """
|
| Overall REDDA architecture for drug repositioning, adapted for the new knowledge graph.
|
| """
|
|
|
| def __init__(self, etypes, ntypes, in_feats, hidden_feats, num_heads, dropout):
|
| """
|
| Parameters:
|
| etypes (list): List of edge types.
|
| ntypes (list): List of node types.
|
| in_feats (int): Input feature dimension.
|
| hidden_feats (int): Hidden layer dimension.
|
| num_heads (int): Number of attention heads.
|
| dropout (float): Dropout rate.
|
| """
|
| super(Model, self).__init__()
|
| self.ntypes = ntypes
|
|
|
|
|
| self.node_projections = nn.ModuleDict()
|
| for ntype in ntypes:
|
| self.node_projections[ntype] = nn.Linear(in_feats, hidden_feats)
|
| nn.init.xavier_normal_(self.node_projections[ntype].weight)
|
|
|
|
|
| self.feat_generate_layer1 = Node_Embedding(hidden_feats, hidden_feats, dropout, etypes)
|
| self.feat_generate_layer2 = Node_Embedding(hidden_feats, hidden_feats, dropout, etypes)
|
|
|
| self.subnet_layer = SubnetworkEncoder(ntypes, hidden_feats, hidden_feats, dropout)
|
|
|
| self.totalnet_layer = Graph_attention(hidden_feats, hidden_feats, num_heads, dropout)
|
|
|
| self.layer_attention_layer_drug = SemanticAttention(hidden_feats)
|
| self.layer_attention_layer_dis = SemanticAttention(hidden_feats)
|
|
|
| self.predict = InnerProductDecoder(hidden_feats)
|
|
|
| def forward(self, g, x):
|
| """
|
| Forward pass of the REDDA model.
|
|
|
| Parameters:
|
| g (dgl.DGLHeteroGraph): Input heterogeneous graph.
|
| x (dict): Dictionary of initial node features for each node type.
|
|
|
| Returns:
|
| torch.Tensor: Predicted score matrix between drugs and diseases.
|
| """
|
| drug_emb_list, dis_emb_list = [], []
|
| h = {ntype: x[ntype] for ntype in self.ntypes}
|
|
|
|
|
| for ntype in self.ntypes:
|
| h[ntype] = self.node_projections[ntype](h[ntype])
|
|
|
| drug_emb_list.append(h["drug"])
|
| dis_emb_list.append(h["disease"])
|
|
|
|
|
| h = self.feat_generate_layer1(g, h, bn=True, dp=True)
|
| h = self.feat_generate_layer2(g, h, bn=True, dp=True)
|
| drug_emb_list.append(h["drug"])
|
| dis_emb_list.append(h["disease"])
|
|
|
|
|
| h = self.subnet_layer(g, h, bn=False, dp=True)
|
| drug_emb_list.append(h["drug"])
|
| dis_emb_list.append(h["disease"])
|
|
|
|
|
| h["disease"], h["drug"] = self.totalnet_layer(g, h, bn=False)
|
| drug_emb_list.append(h["drug"])
|
| dis_emb_list.append(h["disease"])
|
|
|
|
|
| h["drug"] = self.layer_attention_layer_drug(torch.stack(drug_emb_list, dim=1))
|
| h["disease"] = self.layer_attention_layer_dis(torch.stack(dis_emb_list, dim=1))
|
|
|
| return self.predict(h) |