| """
|
| Module for Drug Repositioning (Drug-Disease Association Prediction).
|
|
|
| This module implements the training pipeline for a drug-disease association (DDA) prediction model.
|
| It performs K-fold cross-validation on the dataset, constructs training/testing splits, and trains the model
|
| with early stopping. The model predictions are then saved and evaluation plots (ROC and PR curves) are generated.
|
| """
|
|
|
| import os
|
| import numpy as np
|
| import pandas as pd
|
| import torch as th
|
| from warnings import simplefilter
|
| from sklearn.model_selection import KFold
|
| import dgl
|
| import time
|
|
|
| from model import Model
|
| from load_data import load, remove_graph
|
| from utils import (
|
| get_metrics_auc,
|
| set_seed,
|
| plot_result_auc,
|
| plot_result_aupr,
|
| EarlyStopping,
|
| get_metrics,
|
| )
|
| from args import args
|
|
|
|
|
| def run_training():
|
| """
|
| Train the drug-disease association prediction model using K-fold cross-validation.
|
| Full-graph training (no k-hop subgraph), and for KG datasets perform training-time
|
| negative sampling (20x positives) while leaving the test set unchanged.
|
| """
|
|
|
| simplefilter(action="ignore", category=FutureWarning)
|
|
|
|
|
| print(args)
|
| set_seed(args.seed)
|
|
|
|
|
| os.makedirs(args.saved_path, exist_ok=True)
|
|
|
|
|
| if args.device_id:
|
| print("Training on GPU")
|
| device = th.device(f"cuda:{args.device_id}")
|
| else:
|
| print("Training on CPU")
|
| device = th.device("cpu")
|
|
|
|
|
| if args.dataset in ("KGdataset", "KGdataset_tiny"):
|
| print("Processing KGdataset...")
|
| g_tmp = load(args.dataset)
|
| num_drugs = g_tmp.num_nodes("drug")
|
| num_diseases = g_tmp.num_nodes("disease")
|
| drug_disease_matrix = np.zeros((num_drugs, num_diseases), dtype=np.int64)
|
| if "drug_disease" in g_tmp.etypes:
|
| drug_disease_edges = g_tmp.edges(etype="drug_disease")
|
| di = drug_disease_edges[0].cpu().numpy()
|
| dj = drug_disease_edges[1].cpu().numpy()
|
| drug_disease_matrix[di, dj] = 1
|
| print(f"Created drug-disease matrix: {num_drugs} drugs x {num_diseases} diseases")
|
| print(f"Number of positive associations: {int(drug_disease_matrix.sum())}")
|
| df = drug_disease_matrix
|
| else:
|
| df = pd.read_csv(
|
| f"./dataset/{args.dataset}/{args.dataset}_baseline.csv", header=None
|
| ).values
|
|
|
|
|
| data = np.array([[i, j, df[i, j]] for i in range(df.shape[0]) for j in range(df.shape[1])], dtype="int64")
|
| data_pos = data[data[:, -1] == 1]
|
| data_neg = data[data[:, -1] == 0]
|
| assert len(data) == len(data_pos) + len(data_neg)
|
|
|
| print(f"Total samples: {len(data)}")
|
| print(f"Positive samples: {len(data_pos)}")
|
| print(f"Negative samples: {len(data_neg)}")
|
|
|
|
|
| set_seed(args.seed)
|
| kf = KFold(n_splits=args.nfold, shuffle=True, random_state=args.seed)
|
| fold = 1
|
| pred_result = np.zeros(df.shape)
|
|
|
|
|
| for (train_pos_idx, test_pos_idx), (train_neg_idx, test_neg_idx) in zip(
|
| kf.split(data_pos), kf.split(data_neg)
|
| ):
|
| print(f"{args.nfold}-Fold Cross Validation: Fold {fold}")
|
|
|
|
|
| train_pos_id, test_pos_id = data_pos[train_pos_idx], data_pos[test_pos_idx]
|
| train_neg_id, test_neg_id = data_neg[train_neg_idx], data_neg[test_neg_idx]
|
|
|
| assert max(train_pos_id[:, 0]) < df.shape[0] and max(test_pos_id[:, 0]) < df.shape[0]
|
| assert max(train_pos_id[:, 1]) < df.shape[1] and max(test_pos_id[:, 1]) < df.shape[1]
|
| assert max(train_neg_id[:, 0]) < df.shape[0] and max(test_neg_id[:, 0]) < df.shape[0]
|
| assert max(train_neg_id[:, 1]) < df.shape[1] and max(test_neg_id[:, 1]) < df.shape[1]
|
|
|
|
|
| train_pos_indices = [tuple(train_pos_id[:, 0]), tuple(train_pos_id[:, 1])]
|
| test_pos_indices = [tuple(test_pos_id[:, 0]), tuple(test_pos_id[:, 1])]
|
| train_neg_indices = [tuple(train_neg_id[:, 0]), tuple(train_neg_id[:, 1])]
|
| test_neg_indices = [tuple(test_neg_id[:, 0]), tuple(test_neg_id[:, 1])]
|
| assert (
|
| len(train_pos_indices[0])
|
| + len(test_pos_indices[0])
|
| + len(train_neg_indices[0])
|
| + len(test_neg_indices[0])
|
| == len(data)
|
| )
|
|
|
|
|
| g = load(args.dataset)
|
| print(g)
|
| g = remove_graph(g, test_pos_id[:, :-1]).to(device)
|
|
|
|
|
| if args.dataset == "Kdataset":
|
| feature = {
|
| "drug": g.nodes["drug"].data["h"],
|
| "disease": g.nodes["disease"].data["h"],
|
| "protein": g.nodes["protein"].data["h"],
|
| "gene": g.nodes["gene"].data["h"],
|
| "pathway": g.nodes["pathway"].data["h"],
|
| }
|
| elif args.dataset == "Bdataset":
|
| feature = {
|
| "drug": g.nodes["drug"].data["h"],
|
| "disease": g.nodes["disease"].data["h"],
|
| "protein": g.nodes["protein"].data["h"],
|
| }
|
| elif args.dataset in ("KGdataset", "KGdataset_tiny"):
|
| feature = {ntype: g.nodes[ntype].data["h"] for ntype in g.ntypes}
|
| print(f"Node types in graph: {list(feature.keys())}")
|
| else:
|
| raise ValueError("Unsupported dataset type.")
|
|
|
|
|
| mask_label = np.ones(df.shape, dtype=np.int8)
|
| mask_label[test_pos_indices[0], test_pos_indices[1]] = 0
|
| mask_label[test_neg_indices[0], test_neg_indices[1]] = 0
|
|
|
|
|
| mask_test_np = np.where(mask_label == 0)
|
| mask_test = [tuple(mask_test_np[0]), tuple(mask_test_np[1])]
|
|
|
|
|
| if args.dataset in ("KGdataset", "KGdataset_tiny"):
|
|
|
| n_pos = len(train_pos_indices[0])
|
| n_neg_total = len(train_neg_indices[0])
|
| n_neg_sample = min(20 * n_pos, n_neg_total)
|
| rng = np.random.default_rng(args.seed + fold)
|
| sel = rng.choice(n_neg_total, size=n_neg_sample, replace=False)
|
| neg0 = np.array(train_neg_indices[0])[sel]
|
| neg1 = np.array(train_neg_indices[1])[sel]
|
| tr0 = np.concatenate([np.array(train_pos_indices[0]), neg0])
|
| tr1 = np.concatenate([np.array(train_pos_indices[1]), neg1])
|
| mask_train = [tuple(tr0), tuple(tr1)]
|
| pos_weight = th.tensor(n_neg_sample / max(1, n_pos))
|
| else:
|
| mask_train_np = np.where(mask_label == 1)
|
| mask_train = [tuple(mask_train_np[0]), tuple(mask_train_np[1])]
|
| pos_weight = th.tensor(len(train_neg_indices[0]) / max(1, len(train_pos_indices[0])))
|
|
|
| print(
|
| "Number of total training samples: {}, pos samples: {}, neg samples (used): {}".format(
|
| len(mask_train[0]),
|
| len(train_pos_indices[0]),
|
| len(mask_train[0]) - len(train_pos_indices[0]),
|
| )
|
| )
|
| print(
|
| "Number of total testing samples: {}, pos samples: {}, neg samples: {}".format(
|
| len(mask_test[0]),
|
| len(test_pos_indices[0]),
|
| len(test_neg_indices[0]),
|
| )
|
| )
|
|
|
| label = th.tensor(df).float().to(device)
|
|
|
|
|
| model = Model(
|
| etypes=g.etypes,
|
| ntypes=g.ntypes,
|
| in_feats=feature["drug"].shape[1],
|
| hidden_feats=args.hidden_feats,
|
| num_heads=args.num_heads,
|
| dropout=args.dropout,
|
| ).to(device)
|
|
|
| optimizer = th.optim.Adam(
|
| model.parameters(), lr=args.learning_rate, weight_decay=args.weight_decay
|
| )
|
| optim_scheduler = th.optim.lr_scheduler.CyclicLR(
|
| optimizer,
|
| base_lr=0.1 * args.learning_rate,
|
| max_lr=args.learning_rate,
|
| gamma=0.995,
|
| step_size_up=20,
|
| mode="exp_range",
|
| cycle_momentum=False,
|
| )
|
| criterion = th.nn.BCEWithLogitsLoss(pos_weight=pos_weight)
|
| print(f"Loss pos weight: {pos_weight.item():.3f}")
|
|
|
| stopper = EarlyStopping(patience=args.patience, saved_path=args.saved_path)
|
|
|
|
|
| for epoch in range(1, args.epoch + 1):
|
| time_start = time.time()
|
| model.train()
|
| score = model(g, feature)
|
| pred = th.sigmoid(score)
|
| loss = criterion(
|
| score[mask_train].cpu().flatten(), label[mask_train].cpu().flatten()
|
| )
|
| optimizer.zero_grad()
|
| loss.backward()
|
| optimizer.step()
|
| optim_scheduler.step()
|
|
|
|
|
| model.eval()
|
| train_auc, _ = get_metrics_auc(
|
| label[mask_train].cpu().detach().numpy(),
|
| pred[mask_train].cpu().detach().numpy(),
|
| )
|
| early_stop = stopper.step(loss.item(), train_auc, model)
|
|
|
|
|
| if epoch % 50 == 0:
|
| test_auc, test_aupr = get_metrics_auc(
|
| label[mask_test].cpu().detach().numpy(),
|
| pred[mask_test].cpu().detach().numpy(),
|
| )
|
| time_end = time.time()
|
| time_cost = (time_end - time_start) / 3600
|
| time_remaining = (time_cost / (epoch / 50)) * (args.epoch - epoch)
|
| print(
|
| f"Epoch {epoch} Loss: {loss.item():.3f}; Train AUC: {train_auc:.3f}; "
|
| f"Test AUC: {test_auc:.3f}; Test AUPR: {test_aupr:.3f}; Est. Time Remaining: {time_remaining:.2f}h"
|
| )
|
| print("-" * 50)
|
|
|
|
|
|
|
|
|
| stopper.load_checkpoint(model)
|
| model.eval()
|
|
|
| th.save(model.state_dict(), os.path.join(args.saved_path, f"model_{fold}.pth"))
|
| final_pred = th.sigmoid(model(g, feature)).cpu().detach().numpy()
|
|
|
|
|
| pred_result[test_pos_indices[0], test_pos_indices[1]] = final_pred[test_pos_indices[0], test_pos_indices[1]]
|
| pred_result[test_neg_indices[0], test_neg_indices[1]] = final_pred[test_neg_indices[0], test_neg_indices[1]]
|
|
|
|
|
|
|
|
|
|
|
| fold += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| result_path = os.path.join(args.saved_path, "result.csv")
|
| pd.DataFrame(pred_result).to_csv(result_path, index=False, header=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| run_training() |