| |
|
|
| import pandas as pd |
| import numpy as np |
| from tqdm import tqdm |
| import math |
|
|
| import torch |
| import torch.nn as nn |
| from torch.optim import AdamW |
| from torch import Tensor |
| import torch.nn.functional as F |
| from torch.nn import Parameter |
| from torch.nn import Sequential, Linear, ReLU, MultiheadAttention, Dropout, LayerNorm, AvgPool1d |
| from torchmetrics.functional import mean_absolute_error |
| import torch.optim as optim |
| from torch.utils.data import Dataset, DataLoader |
| from torch.optim.lr_scheduler import LambdaLR |
| from tqdm import tqdm |
| import csv |
|
|
| |
| from torch.nn.init import zeros_,xavier_normal_ |
|
|
| import os |
| |
|
|
| import networkx as nx |
| import torch_geometric.data as Data |
| from torch_geometric.loader import DataLoader |
| |
| from torch_geometric.nn import GCNConv, global_mean_pool, GATConv |
| from torch_geometric.transforms import NormalizeScale |
| from torch_geometric.data import Batch |
| from torchmetrics.functional import mean_absolute_error |
| from torch.utils.data import random_split |
|
|
| import matplotlib.pyplot as plt |
|
|
| from biopandas.pdb import PandasPdb |
| import periodictable |
| from Bio import SeqIO |
| from Bio.PDB import PDBParser |
| from Bio.SeqUtils import seq1 |
|
|
| from sklearn.model_selection import train_test_split |
|
|
| import warnings |
| warnings.filterwarnings("ignore") |
|
|
| import logging, sys |
|
|
| logging.basicConfig(filename='./conf/P2PXML_Structure/log_XthY.log', level=logging.DEBUG) |
| logger = logging.getLogger() |
| sys.stderr.write = logger.error |
| sys.stdout.write = logger.info |
| print = lambda *tup : logger.info(str(" ".join([str(x) for x in tup]))) |
|
|
| """List of target values""" |
|
|
| df = pd.read_csv('./conf/P2PXML_Structure/P2PXML_structure.csv') |
|
|
| """#Preprocessing functions""" |
|
|
| |
| folder_1 = "./conf/P2PXML_Structure/antibodies" |
| folder_2 = "./conf/P2PXML_Structure/antigens" |
|
|
| |
| pdb_files_1 = sorted(os.listdir(folder_1)) |
| pdb_files_2 = sorted(os.listdir(folder_2)) |
|
|
| def pdb_to_seq(name, path): |
| pdbparser = PDBParser() |
| structure = pdbparser.get_structure(name, path) |
| chains = {chain.id:seq1(''.join(residue.resname for residue in chain)) for chain in structure.get_chains()} |
| full_sequence = '' |
| for value in chains.values(): |
| full_sequence+=value |
| full_sequence = full_sequence.replace("X","") |
| print(len(full_sequence)) |
| return full_sequence |
|
|
| def pdb_to_graph(pdb_file): |
| |
| ppdb = PandasPdb() |
| ppdb.read_pdb(pdb_file) |
| coords = ppdb.df['ATOM'][['x_coord', 'y_coord', 'z_coord']].values |
| atomic_nums = ppdb.df['ATOM']['element_symbol'].apply(lambda symbol: periodictable.elements.symbol(symbol).number).values |
|
|
| |
| graph = nx.Graph() |
| num_atoms = len(coords) |
| for i in range(num_atoms): |
| graph.add_node(i, x=coords[i][0], y=coords[i][1], z=coords[i][2], atomic_number=atomic_nums[i]) |
| for i in range(num_atoms): |
| for j in range(i+1, num_atoms): |
| dist = ((coords[i]-coords[j])**2).sum()**0.5 |
| if dist < 5: |
| bond_strength = 1 / dist |
| graph.add_edge(i, j, distance=dist, bond_strength=bond_strength) |
|
|
| edge_attrs = {} |
|
|
| for u, v, data in graph.edges(data=True): |
| edge_attrs[(u, v)] = [data['distance'], data['bond_strength']] |
| edge_attrs[(v, u)] = [data['distance'], data['bond_strength']] |
|
|
| |
| data = Data.Data( |
| x=torch.tensor(list(nx.get_node_attributes(graph, 'x').values())).to(torch.float64), |
| y_coord = torch.tensor(list(nx.get_node_attributes(graph, 'y').values())).to(torch.float64), |
| edge_index=torch.tensor(list(graph.edges)).to(torch.float64).t().contiguous(), |
| y=torch.tensor([0.0]).to(torch.float64), |
| z_coord = torch.tensor(list(nx.get_node_attributes(graph, 'z').values())).to(torch.float64), |
| pos=torch.tensor(coords).to(torch.float64), |
| |
| edge_attr=torch.tensor([edge_attrs[e] for e in graph.edges()]).to(torch.float64), |
| z=torch.tensor(list(nx.get_node_attributes(graph, 'atomic_number').values())).to(torch.float64) |
| ) |
| return data |
|
|
| def get_interaction_energy_abCov(data_1, data_2): |
| interaction_energies = np.load('./labels.npy',allow_pickle=True).item() |
| energy = 0 |
| for key in interaction_energies.keys(): |
| if (key == data_1[:-4]): |
| energy = interaction_energies[key] |
| return energy |
|
|
| def pdb_to_graph_residue(pdb_file): |
| |
| ppdb = PandasPdb() |
| ppdb.read_pdb(pdb_file) |
| hetatms = ppdb.df['HETATM'] |
| residues = hetatms.groupby(['residue_number', 'residue_name']).first().reset_index() |
|
|
| |
| graph = nx.Graph() |
| num_residues = len(residues) |
| print(residues) |
| for i in range(num_residues): |
| residue_number = residues.iloc[i]['residue_number'] |
| residue_name = residues.iloc[i]['residue_name'] |
| graph.add_node(i, residue_number=residue_number, residue_name=residue_name) |
|
|
| |
| for i in range(num_residues - 1): |
| graph.add_edge(i, i+1) |
|
|
| return graph |
|
|
| def pdb_to_graph_res(pdb_file): |
| |
| ppdb = PandasPdb() |
| ppdb.read_pdb(pdb_file) |
| atoms = ppdb.df['ATOM'] |
| residues = atoms.groupby(['residue_number', 'residue_name']).first().reset_index() |
|
|
| |
| graph = nx.Graph() |
| num_residues = len(residues) |
| print(num_residues) |
| for i in range(num_residues): |
| residue_number = residues.iloc[i]['residue_number'] |
| residue_name = residues.iloc[i]['residue_name'] |
| graph.add_node(i, residue_number=residue_number, residue_name=residue_name) |
|
|
| |
| for i in range(num_residues - 1): |
| graph.add_edge(i, i+1) |
|
|
| return graph |
|
|
| |
|
|
| df = pd.read_csv('./conf/P2PXML_Structure/P2PXML_structure.csv') |
|
|
| |
| folder_1 = "./conf/P2PXML_Structure/antibodies" |
| folder_2 = "./conf/P2PXML_Structure/antigens" |
|
|
| |
| pdb_files_1 = sorted(os.listdir(folder_1)) |
| pdb_files_2 = sorted(os.listdir(folder_2)) |
|
|
| max_antibody_sequence_length = 250 |
| max_antigen_sequence_length = 1300 |
|
|
| |
| all_index = 0 |
| max_index = 0 |
| for pdb_file in os.listdir('./conf/P2PXML_Structure/antibodies/'): |
| all_index += 1 |
| try: |
| if pdb_file.endswith('.pdb'): |
| pdbparser = PDBParser() |
| structure = pdbparser.get_structure(pdb_file, os.path.join('./conf/P2PXML_Structure/antibodies',pdb_file)) |
| chains = {chain.id:seq1(''.join(residue.resname for residue in chain)) for chain in structure.get_chains()} |
| full_sequence = '' |
| for value in chains.values(): |
| full_sequence+=value |
| full_sequence = full_sequence.replace("X","") |
|
|
| |
| max_antibody_sequence_length = max(max_antibody_sequence_length, len(full_sequence)) |
|
|
| except Exception as e: |
| max_index += 1 |
| print(f"Error: {e} at {pdb_file}") |
| continue |
| |
| |
|
|
| all_index = 0 |
| max_index = 0 |
| |
| for pdb_file in os.listdir('./conf/P2PXML_Structure/antigens/'): |
| all_index +=1 |
| try: |
| if pdb_file.endswith('.pdb'): |
| pdbparser = PDBParser() |
| structure = pdbparser.get_structure(pdb_file, os.path.join('./conf/P2PXML_Structure/antigens/',pdb_file)) |
| chains = {chain.id:seq1(''.join(residue.resname for residue in chain)) for chain in structure.get_chains()} |
| full_sequence = '' |
| for value in chains.values(): |
| full_sequence+=value |
| full_sequence = full_sequence.replace("X","") |
|
|
| |
| max_antigen_sequence_length = max(max_antigen_sequence_length, len(full_sequence)) |
|
|
| except Exception as e: |
| max_index += 1 |
| print(f"Error: {e} at {pdb_file}") |
| continue |
|
|
| |
| |
|
|
| def pdb_to_seq(name, path): |
| pdbparser = PDBParser() |
| structure = pdbparser.get_structure(name, path) |
| chains = {chain.id:seq1(''.join(residue.resname for residue in chain)) for chain in structure.get_chains()} |
| full_sequence = '' |
| for value in chains.values(): |
| full_sequence+=value |
| full_sequence = full_sequence.replace("X","") |
| return full_sequence |
|
|
| seq_length = 250 |
| virus_length = 1300 |
| amino_acids = list("ACDEFGHIKLMNPQRSTVWY") |
|
|
| |
| aa_to_index = {aa: i for i, aa in enumerate(amino_acids)} |
|
|
| |
| def encode_sequence(seq, length): |
| indices = [aa_to_index[aa] for aa in seq] |
| encoded = F.one_hot(torch.tensor(indices), num_classes=len(amino_acids)).float() |
| padded_encoded = F.pad(encoded.flatten(), (0, max(length * len(amino_acids) - encoded.flatten().shape[0], 0))) |
| return padded_encoded |
|
|
| def pdb_to_graph(seq, pdb_file, length): |
|
|
| sequence = encode_sequence(pdb_to_seq(seq, pdb_file), length) |
|
|
| |
| ppdb = PandasPdb() |
| ppdb.read_pdb(pdb_file) |
| coords = ppdb.df['ATOM'][['x_coord', 'y_coord', 'z_coord']].values |
| atomic_nums = ppdb.df['ATOM']['element_symbol'].apply(lambda symbol: periodictable.elements.symbol(symbol).number).values |
|
|
| |
| graph = nx.Graph() |
| num_atoms = len(coords) |
| for i in range(num_atoms): |
| graph.add_node(i, x=coords[i][0], y=coords[i][1], z=coords[i][2], atomic_number=atomic_nums[i]) |
| for i in range(num_atoms): |
| for j in range(i+1, num_atoms): |
| dist = ((coords[i]-coords[j])**2).sum()**0.5 |
| if dist < 5: |
| bond_strength = 1 / dist |
| graph.add_edge(i, j, distance=dist, bond_strength=bond_strength) |
|
|
| edge_attrs = {} |
|
|
| for u, v, data in graph.edges(data=True): |
| edge_attrs[(u, v)] = [data['distance'], data['bond_strength']] |
| edge_attrs[(v, u)] = [data['distance'], data['bond_strength']] |
|
|
| |
| data = Data.Data( |
| x=torch.tensor(list(nx.get_node_attributes(graph, 'x').values())).to(torch.float64), |
| y_coord = torch.tensor(list(nx.get_node_attributes(graph, 'y').values())).to(torch.float64), |
| edge_index=torch.tensor(list(graph.edges)).to(torch.float64).t().contiguous(), |
| y=torch.tensor([0.0]).to(torch.float64), |
| z_coord = torch.tensor(list(nx.get_node_attributes(graph, 'z').values())).to(torch.float64), |
| pos=torch.tensor(coords).to(torch.float64), |
| |
| edge_attr=torch.tensor([edge_attrs[e] for e in graph.edges()]).to(torch.float64), |
| z=torch.tensor(list(nx.get_node_attributes(graph, 'atomic_number').values())).to(torch.float64), |
| seq = sequence |
| |
| ) |
| return data |
|
|
| def get_interaction_energy_abCov(data_1, data_2): |
| interaction_energies = np.load('./labels.npy',allow_pickle=True).item() |
| print(interaction_energies) |
|
|
| energy = 0 |
| for key in interaction_energies.keys(): |
| if (key == data_1[:-4]): |
| energy = interaction_energies[key] |
| return energy |
|
|
| |
| def preprocess_batch(batch): |
| seq1_batch, seq2_batch, label_batch = zip(*batch) |
|
|
| seq1_graphs = torch.stack([pdb_to_graph(seq, os.path.join('./conf/P2PXML_Structure/antibodies/'+seq+'.pdb'),seq_length) for seq in seq1_batch]) |
| seq2_graphs = torch.stack([pdb_to_graph(seq, os.path.join('./conf/P2PXML_Structure/antigens/'+seq+'.pdb'), virus_length) for seq in seq2_batch]) |
|
|
| labels = torch.tensor(label_batch).unsqueeze(1) |
|
|
| return seq1_graphs, seq2_graphs, labels |
|
|
| class ProteinDataset(Dataset): |
| def __init__(self, df, max_antibody_sequence_length, max_antigen_sequence_length, save_dir='./conf/P2PXML_Structure/graph_data'): |
| self.sequences = df['Ab'].values |
| self.viruses = df['Ag'].values |
| self.labels = df['log(IC50)'].values |
| self.max_antibody_sequence_length = max_antibody_sequence_length |
| self.max_antigen_sequence_length = max_antigen_sequence_length |
| self.save_dir = save_dir |
| os.makedirs(save_dir, exist_ok=True) |
|
|
| def __len__(self): |
| return len(self.sequences) |
|
|
| def __getitem__(self, idx): |
| sequence = self.sequences[idx] |
| virus = self.viruses[idx] |
| label = self.labels[idx] |
| label = torch.tensor(label, dtype=torch.float64) |
|
|
| antibody = self.load_or_generate_graph(sequence, self.max_antibody_sequence_length, 'antibodies') |
| antigen = self.load_or_generate_graph(virus, self.max_antigen_sequence_length, 'antigens') |
|
|
| if antibody is not None and antigen is not None: |
| return antibody, antigen, label |
| else: |
| return self.__getitem__((idx + 1) % len(self)) |
|
|
| def load_or_generate_graph(self, pdb_file, max_sequence_length, graph_type): |
| graph_path = os.path.join(self.save_dir, f'{graph_type}_{pdb_file}.pt') |
|
|
| if os.path.exists(graph_path): |
| return torch.load(graph_path) |
|
|
| if graph_type == 'antigens': |
| graph_constructed = self.pdb_to_graph_virus(pdb_file, max_sequence_length) |
| else: |
| graph_constructed = self.pdb_to_graph_antibody(pdb_file, max_sequence_length) |
|
|
| if graph_constructed is not None: |
| torch.save(graph_constructed, graph_path) |
| return graph_constructed |
|
|
| def pdb_to_graph_virus(self, pdb_file, max_antigen_sequence_length): |
| return self.pdb_to_graph(pdb_file, max_antigen_sequence_length, 'antigens') |
|
|
| def pdb_to_graph_antibody(self, pdb_file, max_antibody_sequence_length): |
| return self.pdb_to_graph(pdb_file, max_antibody_sequence_length, 'antibodies') |
|
|
| def pdb_to_graph(self, pdb_file, max_sequence_length, graph_type): |
| seq_length = max_sequence_length |
| amino_acids = list("ACDEFGHIKLMNPQRSTVWY") |
| aa_to_index = {aa: i for i, aa in enumerate(amino_acids)} |
| pdbparser = PDBParser() |
|
|
| try: |
| structure = pdbparser.get_structure(pdb_file, os.path.join(f'./conf/P2PXML_Structure/{graph_type}/'+pdb_file+'.pdb')) |
| chains = {chain.id: seq1(''.join(residue.resname for residue in chain)) for chain in structure.get_chains()} |
| full_sequence = '' |
| for value in chains.values(): |
| full_sequence += value |
| full_sequence = full_sequence.replace("X", "") |
|
|
| if len(full_sequence) > seq_length: |
| print(f"Exceeds max length {graph_type}") |
| return None |
|
|
| indices = [aa_to_index[aa] for aa in full_sequence] |
| encoded = F.one_hot(torch.tensor(indices), num_classes=len(amino_acids)).float() |
| padded_encoded = F.pad(encoded.flatten(), (0, max(seq_length * len(amino_acids) - encoded.flatten().shape[0], 0))) |
|
|
| ppdb = PandasPdb() |
| ppdb.read_pdb(os.path.join(f'./conf/P2PXML_Structure/{graph_type}/'+pdb_file+'.pdb')) |
| coords = ppdb.df['ATOM'][['x_coord', 'y_coord', 'z_coord']].values |
| atomic_nums = ppdb.df['ATOM']['element_symbol'].apply(lambda symbol: periodictable.elements.symbol(symbol).number).values |
|
|
| graph = nx.Graph() |
| num_atoms = len(coords) |
| for i in range(num_atoms): |
| graph.add_node(i, x=coords[i][0], y=coords[i][1], z=coords[i][2], atomic_number=atomic_nums[i]) |
| for i in range(num_atoms): |
| for j in range(i + 1, num_atoms): |
| dist = ((coords[i] - coords[j]) ** 2).sum() ** 0.5 |
| if dist < 5: |
| bond_strength = 1 / dist |
| graph.add_edge(i, j, distance=dist, bond_strength=bond_strength) |
|
|
| edge_attrs = {} |
|
|
| for u, v, data in graph.edges(data=True): |
| edge_attrs[(u, v)] = [data['distance'], data['bond_strength']] |
| edge_attrs[(v, u)] = [data['distance'], data['bond_strength']] |
|
|
| data = Data.Data( |
| x=torch.tensor(list(nx.get_node_attributes(graph, 'x').values())).to(torch.float64), |
| y_coord=torch.tensor(list(nx.get_node_attributes(graph, 'y').values())).to(torch.float64), |
| z_coord=torch.tensor(list(nx.get_node_attributes(graph, 'z').values())).to(torch.float64), |
| pos=torch.tensor(coords).to(torch.float64), |
| edge_index=torch.tensor(list(graph.edges)).to(torch.float64).t().contiguous(), |
| edge_attr=torch.tensor([edge_attrs[e] for e in graph.edges()]).to(torch.float64), |
| z=torch.tensor(list(nx.get_node_attributes(graph, 'atomic_number').values())).to(torch.float64), |
| seq=padded_encoded.to(torch.float64), |
| y = torch.tensor([0.0]).to(torch.float64) |
| ) |
| return data |
|
|
| except Exception as e: |
| print(e) |
| return None |
|
|
| train_df, test_df = train_test_split(df, test_size=0.15, random_state=42) |
|
|
| train_ds = ProteinDataset(train_df, max_antibody_sequence_length, max_antigen_sequence_length) |
| train_loader = DataLoader(train_ds, batch_size=1, shuffle=True) |
|
|
| test_ds = ProteinDataset(test_df, max_antibody_sequence_length, max_antigen_sequence_length) |
| test_loader = DataLoader(test_ds, batch_size=1, shuffle=False) |
|
|
|
|
| |
|
|
| class SelfAttention(nn.Module): |
| def __init__(self, embed_dim, num_heads=16): |
| super(SelfAttention, self).__init__() |
| self.embed_dim = embed_dim |
| self.num_heads = num_heads |
| if embed_dim % num_heads != 0: |
| raise ValueError(f"embedding dimension = {embed_dim} should be divisible by number of heads = {num_heads}") |
| self.head_dim = embed_dim // num_heads |
| self.query_dense = nn.Linear(embed_dim, embed_dim) |
| self.key_dense = nn.Linear(embed_dim, embed_dim) |
| self.value_dense = nn.Linear(embed_dim, embed_dim) |
| self.combine_heads = nn.Linear(embed_dim, embed_dim) |
|
|
| def forward(self, inputs): |
| query = self.query_dense(inputs) |
| key = self.key_dense(inputs) |
| value = self.value_dense(inputs) |
| query = query.view(-1, self.num_heads, self.head_dim) |
| key = key.view(-1, self.num_heads, self.head_dim) |
| value = value.view(-1, self.num_heads, self.head_dim) |
| query = query.permute(1, 0, 2) |
| key = key.permute(1, 0, 2) |
| value = value.permute(1, 0, 2) |
| dot_product = torch.matmul(query, key.permute(0, 2, 1)) |
| scaled_dot_product = dot_product / torch.sqrt(torch.tensor(self.head_dim, dtype=torch.float32)) |
| attention_weights = torch.softmax(scaled_dot_product, dim=-1) |
| output = torch.matmul(attention_weights, value) |
| output = output.permute(1, 0, 2) |
| output = output.view(-1, self.embed_dim) |
| output = self.combine_heads(output) |
| return output |
|
|
| class TransformerBlock(nn.Module): |
| def __init__(self, embed_dim, num_heads, dense_dim=1024, dropout_rate=0.1): |
| super(TransformerBlock, self).__init__() |
| self.attention = SelfAttention(embed_dim, num_heads) |
| self.dropout1 = nn.Dropout(dropout_rate) |
| self.norm1 = nn.LayerNorm(embed_dim, eps=1e-6) |
| self.dense1 = nn.Linear(embed_dim, dense_dim) |
| self.dropout2 = nn.Dropout(dropout_rate) |
| self.norm2 = nn.LayerNorm(embed_dim, eps=1e-6) |
| self.dense2 = nn.Linear(dense_dim, embed_dim) |
|
|
| def forward(self, inputs): |
| attention_output = self.attention(inputs) |
| attention_output = self.dropout1(attention_output) |
| output1 = self.norm1(inputs + attention_output) |
| dense_output = self.dense1(output1) |
| dense_output = self.dropout2(dense_output) |
| output2 = self.norm2(output1 + dense_output) |
| output = self.dense2(output2) |
| return output |
|
|
| class CrossAttention(nn.Module): |
| def __init__(self, dim, input_shape): |
| super(CrossAttention, self).__init__() |
| self.dim = dim |
| self.input_shape = input_shape |
| self.Wq = nn.Parameter(torch.Tensor(input_shape[0][-1], self.dim)) |
| self.Wk = nn.Parameter(torch.Tensor(input_shape[1][-1], self.dim)) |
| self.Wv = nn.Parameter(torch.Tensor(input_shape[1][-1], self.dim)) |
| nn.init.xavier_uniform_(self.Wq) |
| nn.init.xavier_uniform_(self.Wk) |
| nn.init.xavier_uniform_(self.Wv) |
|
|
| def forward(self, inputs): |
| x, y = inputs |
| Q = torch.matmul(x, self.Wq) |
| K = torch.matmul(y, self.Wk) |
| V = torch.matmul(y, self.Wv) |
| attn_weights = torch.matmul(Q, K.t()) / torch.sqrt(torch.tensor(self.dim, dtype=torch.float64)) |
| attn_weights = F.softmax(attn_weights, dim=-1) |
| attn_output = attn_weights * V |
| output = torch.cat([x, attn_output], dim=-1) |
|
|
| return output |
|
|
| class CombinedModel(nn.Module): |
| def __init__(self, hidden_channels=128, num_layers=16): |
| super(CombinedModel, self).__init__() |
|
|
| self.cross_attn_1 = CrossAttention(128, [(1024,),(1024,)]) |
| self.cross_attn_2 = CrossAttention(128,[(1024,),(1024,)]) |
| self.cross_attn = CrossAttention(128,[(1152,),(1152,)]) |
| self.self_atten_1 = nn.AdaptiveAvgPool1d(1) |
| self.self_atten_2 = nn.AdaptiveAvgPool1d(1) |
| self.cross_pooling = nn.AdaptiveAvgPool1d(1) |
| self.dense = nn.Linear(1280, 256) |
| self.output_layer1 = nn.Linear(2816, 128) |
| self.output_layer = nn.Linear(128, 1) |
|
|
| self.input_1 = nn.Linear(input_shape_1[0], 1024) |
| self.self_attn_1 = SelfAttention(1024) |
| self.transformer_1 = TransformerBlock(1024, 4) |
| self.pooling_1 = nn.AdaptiveAvgPool1d(1) |
| self.dense_1 = nn.Linear(1024, 1024) |
| self.dropout_1 = nn.Dropout(p=0.05) |
|
|
| self.input_2 = nn.Linear(input_shape_2[0], 1024) |
| self.self_attn_2 = SelfAttention(1024) |
| self.transformer_2 = TransformerBlock(1024, 4) |
| self.pooling_2 = nn.AdaptiveAvgPool1d(1) |
| self.dense_2 = nn.Linear(1024, 1024) |
| self.dropout_2 = nn.Dropout(p=0.05) |
|
|
| self.num_layers = num_layers |
|
|
| self.convs1 = nn.ModuleList() |
| self.convs1.append(GCNConv(4, hidden_channels)) |
| for _ in range(num_layers - 1): |
| self.convs1.append(GCNConv(hidden_channels, hidden_channels)) |
|
|
| self.convs2 = nn.ModuleList() |
| self.convs2.append(GCNConv(4, hidden_channels)) |
| for _ in range(num_layers - 1): |
| self.convs2.append(GCNConv(hidden_channels, hidden_channels)) |
|
|
| self.cross_att = GATConv(hidden_channels, hidden_channels, heads=2) |
|
|
| self.lin1 = nn.Linear(2816, hidden_channels) |
| self.lin2 = nn.Linear(hidden_channels, 1) |
|
|
| self.transform = NormalizeScale() |
|
|
| def forward(self, data_batch_1, data_batch_2): |
|
|
| x1 = data_batch_1.x.double() |
| edge_index_1 = data_batch_1.edge_index |
| z1 = data_batch_1.z |
| y1_coord = data_batch_1.y_coord |
| z1_coord = data_batch_1.z_coord |
|
|
| concatenated_x1 = torch.stack([x1, z1, y1_coord, z1_coord], dim=1) |
|
|
| data_batch_1 = self.transform(data_batch_1) |
| x1 = data_batch_1.x |
|
|
| for i in range(self.num_layers): |
| concatenated_x1 = self.convs1[i](concatenated_x1, edge_index_1.to(torch.int64)) |
| concatenated_x1 = F.relu(concatenated_x1.double()) |
|
|
| x2 = data_batch_2.x |
| edge_index_2 = data_batch_2.edge_index |
| z2 = data_batch_2.z |
| y2_coord = data_batch_2.y_coord |
| z2_coord = data_batch_2.z_coord |
|
|
| concatenated_x2 = torch.stack([x2, z2, y2_coord, z2_coord], dim=1) |
|
|
| data_batch_2 = self.transform(data_batch_2) |
| x2 = data_batch_2.x |
|
|
| for i in range(self.num_layers): |
| concatenated_x2 = self.convs2[i](concatenated_x2, edge_index_2.to(torch.int64)) |
| concatenated_x2 = F.relu(concatenated_x2) |
|
|
| |
| x1 = self.cross_att(concatenated_x1, edge_index_1.to(torch.int64)) |
| x2 = self.cross_att(concatenated_x2, edge_index_2.to(torch.int64)) |
|
|
| x = torch.cat([ |
| global_mean_pool(x1, data_batch_1.batch), |
| global_mean_pool(x2, data_batch_2.batch)], dim=1) |
|
|
| input_11 = self.input_1(data_batch_1.seq) |
| self_attn_1 = self.self_attn_1(input_11) |
| transformer_1 = self.transformer_1(self_attn_1) |
| pooling_1 = self.pooling_1(transformer_1.transpose(0, 1)).squeeze(dim=1) |
| dense_1 = self.dense_1(pooling_1) |
| dropout_1 = self.dropout_1(dense_1) |
|
|
| input_22 = self.input_2(data_batch_2.seq) |
| self_attn_2 = self.self_attn_2(input_22) |
| transformer_2 = self.transformer_2(self_attn_2) |
| pooling_2 = self.pooling_2(transformer_2.transpose(0, 1)).squeeze(dim=1) |
| dense_2 = self.dense_2(pooling_2) |
| dropout_2 = self.dropout_2(dense_2) |
|
|
| input_shape = [(dropout_1.shape[-1],), (dropout_2.shape[-1],)] |
| cross_attn_1 = self.cross_attn_1([dropout_1, dropout_2]) |
| cross_attn_2 = self.cross_attn_2([self.self_attn_1(input_11), self.self_attn_2(input_22)]) |
| cross_pooling = self.cross_pooling(cross_attn_2.transpose(0, 1)).squeeze(dim=1) |
| cross_attn = self.cross_attn([cross_attn_1, cross_pooling]) |
| cross_atten = F.tanh(self.dense(cross_attn)) |
|
|
| self_atten_1 = self.self_atten_1(self_attn_1.transpose(0, 1)).squeeze(dim=1) |
| self_atten_2 = self.self_atten_2(self_attn_2.transpose(0, 1)).squeeze(dim=1) |
| attention_scores = torch.cat([self_atten_1, self_atten_2, cross_atten], dim=-1) |
|
|
| x_2 = torch.cat([self.pooling_2(x.transpose(0, 1)).squeeze(dim=1), attention_scores], dim = -1) |
|
|
| attention_scores = torch.cat([attention_scores, self.pooling_2(x.transpose(0, 1)).squeeze(dim=1)], dim = -1) |
|
|
| output_layer1 = F.tanh(self.output_layer1(attention_scores)) |
| output_layer = self.output_layer(output_layer1) |
|
|
| x = F.relu(self.lin1(x_2)) |
| x = self.lin2(x) |
|
|
| return x, output_layer |
|
|
| class SaveBestModel: |
| """ |
| Class to save the best model while training. If the current epoch's |
| validation loss is less than the previous least loss, then save the |
| model state. |
| """ |
| def __init__(self, best_valid_loss=float('inf')): |
| self.best_valid_loss = best_valid_loss |
| self.best_model_path = None |
|
|
| def __call__(self, current_valid_loss, epoch, model, optimizer, criterion): |
| if current_valid_loss < self.best_valid_loss: |
| self.best_valid_loss = current_valid_loss |
| print(f"\nBest validation loss: {self.best_valid_loss}") |
| print(f"\nSaving best model for epoch: {epoch+1}\n") |
|
|
| if self.best_model_path: |
| try: |
| os.remove(self.best_model_path) |
| print(f"Deleted previous best model: {self.best_model_path}") |
| except OSError as e: |
| print(f"Error deleting file {self.best_model_path}: {e}") |
|
|
| self.best_model_path = f'./weight/{epoch}_best_model.pth' |
| torch.save({ |
| 'epoch': epoch+1, |
| 'model_state_dict': model.state_dict(), |
| 'optimizer_state_dict': optimizer.state_dict(), |
| 'loss': criterion, |
| }, self.best_model_path) |
| print(f"Saved new best model: {self.best_model_path}") |
|
|
| save_best_model = SaveBestModel() |
|
|
| input_shape_1 = (int(max_antibody_sequence_length*20),) |
| input_shape_2 = (int(max_antigen_sequence_length*20),) |
|
|
| BATCH_SIZE = 16 |
| NUM_EPOCHS = 30 |
| LEARNING_RATE = 0.0001 |
|
|
| def count_parameters(model): |
| return sum(p.numel() for p in model.parameters() if p.requires_grad) |
|
|
| |
|
|
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| com_model = CombinedModel().to(device) |
| com_model = com_model.to(torch.float64) |
|
|
| loss_fn = nn.MSELoss() |
| optimizer = optim.Adam(com_model.parameters(), lr=LEARNING_RATE) |
|
|
| def lr_schedule(epoch, lr = LEARNING_RATE): |
| if epoch < 100: |
| return lr |
| else: |
| return lr * torch.exp(torch.tensor(-0.01)) |
|
|
| lr_scheduler = LambdaLR(optimizer, lr_schedule) |
|
|
| filepath = './weight/combined_XthY.tar' |
|
|
| print(f"Number of parameters: {count_parameters(com_model)}") |
|
|
| com_model_cp = torch.load('./weight/N_best_model.pth') |
| com_model_epoch = com_model_cp['epoch'] |
| print(f"Best model was saved at {com_model_epoch} epochs\n") |
|
|
| com_model.load_state_dict(com_model_cp['model_state_dict']) |
|
|
| alpha = 0.45 |
| beta = 0.55 |
| gamma = 0.05 |
|
|
| for epoch in range(NUM_EPOCHS): |
| com_model.train() |
| for batch_idx, input in enumerate(tqdm(train_loader)): |
| input_1 = input[0].to(device) |
| input_2 = input[1].to(device) |
| target = input[2].to(device) |
|
|
| if input_1.pos is not None: |
| input_1 = NormalizeScale()(input_1) |
| else: |
| print("Data does not have position information, skipping normalization.") |
|
|
| if input_2.pos is not None: |
| input_2 = NormalizeScale()(input_2) |
| else: |
| print("Data does not have position information, skipping normalization.") |
|
|
| optimizer.zero_grad() |
| output_gnn, output_tranf = com_model(input_1, input_2) |
| loss_gnn = loss_fn(output_gnn, target) |
| loss_tranf = loss_fn(output_tranf, target) |
| loss_inBetween = loss_fn(output_gnn, output_tranf) |
| loss = alpha*loss_gnn + beta*loss_tranf + gamma*loss_inBetween |
| loss.backward() |
| optimizer.step() |
|
|
| com_model.eval() |
| with torch.no_grad(): |
| for batch_idx, input in enumerate(test_loader): |
| input_1 = input[0].to(device) |
| input_2 = input[1].to(device) |
| target = input[2].to(device) |
|
|
| if input_1.pos is not None: |
| input_1 = NormalizeScale()(input_1) |
| else: |
| print("Data does not have position information, skipping normalization.") |
|
|
| if input_2.pos is not None: |
| input_2 = NormalizeScale()(input_2) |
| else: |
| print("Data does not have position information, skipping normalization.") |
|
|
| output_gnn, output_tranf = com_model(input_1, input_2) |
| loss_gnn = loss_fn(output_gnn, target) |
| loss_tranf = loss_fn(output_tranf, target) |
| loss_inBetween = loss_fn(output_gnn, output_tranf) |
| loss = alpha*loss_gnn + beta*loss_tranf + gamma*loss_inBetween |
|
|
| print(f"Epoch {epoch+1}/{NUM_EPOCHS}, Loss: {loss.item()}") |
|
|
| save_best_model(loss, epoch, com_model, optimizer, loss_fn) |
|
|
| |
| |
| |
|
|
| lr_scheduler.step() |
|
|
| |
|
|
| csv_file_path = './scripts/training_runs/evaluation_results.csv' |
|
|
| |
| |
| |
|
|
| |
|
|
| with open(csv_file_path, mode='a', newline='') as csv_file: |
| csv_writer = csv.writer(csv_file) |
| csv_writer.writerow(['Loss_gnn', 'Loss_traf', 'Loss_between', 'Loss', 'MAE', 'Output1', 'Output2', 'Target']) |
|
|
| com_model.eval() |
| test_loss = 0.0 |
| test_mae = 0.0 |
| total_samples = 0 |
|
|
| with torch.no_grad(): |
| for input in test_loader: |
| input_1 = input[0].to(device) |
| input_2 = input[1].to(device) |
| target = input[2].to(device) |
| batch_size = input_1.size(0) |
|
|
| output_gnn, output_tranf = com_model(input_1, input_2) |
| loss_gnn = loss_fn(output_gnn, target) |
| loss_tranf = loss_fn(output_tranf, target) |
| loss_inBetween = loss_fn(output_gnn, output_tranf) |
| loss = alpha * loss_gnn + beta * loss_tranf + gamma * loss_inBetween |
| test_loss += loss.item() * batch_size |
|
|
| mae = torch.abs(output_tranf - target).sum().item() |
| test_mae += mae |
| total_samples += batch_size |
|
|
| csv_writer.writerow([loss_gnn.item(), loss_tranf.item(), loss_inBetween.item(), loss.item(), mae, output_gnn.item(), output_tranf.item(), target.item()]) |
|
|
| test_loss /= total_samples |
| test_mae /= total_samples |
|
|
| print('Test loss:', test_loss) |
| print('Test MAE:', test_mae) |
|
|