From 650ee2408e32a085ffc6d255e8484073b2d50c33 Mon Sep 17 00:00:00 2001 From: Luis Roberto Mercado Diaz Date: Sat, 20 Apr 2024 15:05:31 -0400 Subject: [PATCH 1/2] R drive updating The R drive needs to be defined well, this definition some times works, but the grove method ensures the source connection all the time. --- GP_Original_checkpoint.py | 4 +- main_darren_v1.py | 361 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 main_darren_v1.py diff --git a/GP_Original_checkpoint.py b/GP_Original_checkpoint.py index 0bdceae..777f962 100644 --- a/GP_Original_checkpoint.py +++ b/GP_Original_checkpoint.py @@ -43,8 +43,8 @@ def get_data_paths(data_format, is_linux=False, is_hpc=False): saving_base_path = "/gpfs/scratchfs1/hfp14002/lrm22005/Casseys_case/Project_1_analysis" else: # R:\ENGR_Chon\Dong\MATLAB_generate_results\NIH_PulseWatch - base_path = "R:\ENGR_Chon\Dong\MATLAB_generate_results\\NIH_PulseWatch" - labels_base_path = "R:\ENGR_Chon\\NIH_Pulsewatch_Database\Adjudication_UConn" + base_path = r"\\grove.ad.uconn.edu\\research\\ENGR_Chon\Dong\MATLAB_generate_results\\NIH_PulseWatch" + labels_base_path = r"\\grove.ad.uconn.edu\\research\\ENGR_Chon\\NIH_Pulsewatch_Database\Adjudication_UConn" saving_base_path = r"\\grove.ad.uconn.edu\research\ENGR_Chon\Luis\Research\Casseys_case" if data_format == 'csv': data_path = os.path.join(base_path, "TFS_csv") diff --git a/main_darren_v1.py b/main_darren_v1.py new file mode 100644 index 0000000..2473be0 --- /dev/null +++ b/main_darren_v1.py @@ -0,0 +1,361 @@ +import os +import torch +import gpytorch +from sklearn.metrics import precision_recall_fscore_support, roc_auc_score +from sklearn.preprocessing import label_binarize +from torch.utils.data import Dataset, DataLoader +import numpy as np +import random +import time +import matplotlib.pyplot as plt + +# Seeds +torch.manual_seed(42) +np.random.seed(42) +random.seed(42) + +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +num_latents = 6 # This should match the complexity of your data or the number of tasks +num_tasks = 4 # This should match the number of output classes or tasks +num_inducing_points = 50 # This is independent and should be sufficient for the input space + +class MultitaskGPModel(gpytorch.models.ApproximateGP): + def __init__(self): + # Let's use a different set of inducing points for each latent function + inducing_points = torch.rand(num_latents, num_inducing_points, 128 * 128) # Assuming flattened 128x128 images + + # We have to mark the CholeskyVariationalDistribution as batch + # so that we learn a variational distribution for each task + variational_distribution = gpytorch.variational.CholeskyVariationalDistribution( + inducing_points.size(-2), batch_shape=torch.Size([num_latents]) + ) + + # We have to wrap the VariationalStrategy in a LMCVariationalStrategy + # so that the output will be a MultitaskMultivariateNormal rather than a batch output + variational_strategy = gpytorch.variational.LMCVariationalStrategy( + gpytorch.variational.VariationalStrategy( + self, inducing_points, variational_distribution, learn_inducing_locations=True + ), + num_tasks=num_tasks, + num_latents=num_latents, + latent_dim=-1 + ) + + super().__init__(variational_strategy) + + # The mean and covariance modules should be marked as batch + # so we learn a different set of hyperparameters + self.mean_module = gpytorch.means.ConstantMean(batch_shape=torch.Size([num_latents])) + self.covar_module = gpytorch.kernels.ScaleKernel( + gpytorch.kernels.RBFKernel(batch_shape=torch.Size([num_latents])), + batch_shape=torch.Size([num_latents]) + ) + + def forward(self, x): + mean_x = self.mean_module(x) + covar_x = self.covar_module(x) + latent_pred = gpytorch.distributions.MultivariateNormal(mean_x, covar_x) + return latent_pred + +class CustomDataset(Dataset): + def __init__(self, data_path, labels_path, binary=False, start_idx=0): + self.data_path = data_path + self.labels_path = labels_path + self.binary = binary + self.start_idx = start_idx + self.segment_names, self.labels = self.extract_segment_names_and_labels() + + def __len__(self): + return len(self.segment_names) + + def __getitem__(self, idx): + actual_idx = (idx + self.start_idx) % len(self.segment_names) + segment_name = self.segment_names[actual_idx] + label = self.labels[segment_name] + data_tensor = torch.load(os.path.join(self.data_path, segment_name + '.pt')) + return {'data': data_tensor, 'label': label, 'segment_name': segment_name} + + def extract_segment_names_and_labels(self): + segment_names = [] + labels = {} + + with open(self.labels_path, 'r') as file: + lines = file.readlines() + for line in lines[1:]: # Skip the header line + segment_name, label = line.strip().split(',') + label = int(float(label)) # Convert the label to float first, then to int + if self.binary and label == 2: + label = 0 # Convert PAC/PVC to non-AF (0) for binary classification + segment_names.append(segment_name) + labels[segment_name] = label + + return segment_names, labels + + def set_start_idx(self, index): + self.start_idx = index + + def save_checkpoint(self, checkpoint_path): + checkpoint = { + 'segment_names': self.segment_names, + 'labels': self.labels, + 'start_idx': self.start_idx + } + torch.save(checkpoint, checkpoint_path) + + def load_checkpoint(self, checkpoint_path): + checkpoint = torch.load(checkpoint_path) + self.segment_names = checkpoint['segment_names'] + self.labels = checkpoint['labels'] + self.start_idx = checkpoint['start_idx'] + +def load_data(data_path, labels_path, batch_size, binary=False, start_idx=0): + dataset = CustomDataset(data_path, labels_path, binary, start_idx) + dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False) + return dataloader + +def train_gp_model(train_loader, val_loader, num_iterations=50, n_classes=4, patience=10, + checkpoint_path='model_checkpoint.pt', data_checkpoint_path='data_checkpoint.pt', + resume_training=False, plot_path='training_plot.png'): + model = MultitaskGPModel().to(device) + likelihood = gpytorch.likelihoods.SoftmaxLikelihood(num_features=4, num_classes=4).to(device) + optimizer = torch.optim.Adam(model.parameters(), lr=0.1) + mll = gpytorch.mlls.VariationalELBO(likelihood, model, num_data=len(train_loader.dataset)) + + start_epoch = 0 + if resume_training and os.path.exists(checkpoint_path): + checkpoint = torch.load(checkpoint_path) + model.load_state_dict(checkpoint['model_state_dict']) + likelihood.load_state_dict(checkpoint['likelihood_state_dict']) + optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + start_epoch = checkpoint.get('epoch', 0) + + best_val_loss = float('inf') + epochs_no_improve = 0 + + metrics = { + 'precision': [], + 'recall': [], + 'f1_score': [], + 'auc_roc': [], + 'train_loss': [] + } + + # Initialize lists to store metrics for plotting + train_losses = [] + val_losses = [] + val_precisions = [] + val_recalls = [] + val_f1_scores = [] + val_auc_rocs = [] + + for epoch in range(start_epoch, num_iterations): + model.train() + likelihood.train() + for train_batch in train_loader: + optimizer.zero_grad() + train_x = train_batch['data'].reshape(train_batch['data'].size(0), -1).to(device) + train_y = train_batch['label'].to(device) + output = model(train_x) + loss = -mll(output, train_y) + metrics['train_loss'].append(loss.item()) + loss.backward() + optimizer.step() + + # Append metrics to lists for plotting + train_losses.append(np.mean(metrics['train_loss'])) + val_losses.append(val_loss) + val_precisions.append(precision) + val_recalls.append(recall) + val_f1_scores.append(f1) + val_auc_rocs.append(auc_roc) + + # Stochastic validation + model.eval() + likelihood.eval() + with torch.no_grad(): + val_indices = torch.randperm(len(val_loader.dataset))[:int(0.1 * len(val_loader.dataset))] + val_loss = 0.0 + val_labels = [] + val_predictions = [] + for idx in val_indices: + val_batch = val_loader.dataset[idx] + val_x = val_batch['data'].reshape(-1).unsqueeze(0).to(device) + val_y = torch.tensor([val_batch['label']], device=device) + val_output = model(val_x) + val_loss_batch = -mll(val_output, val_y).sum() + val_loss += val_loss_batch.item() + val_labels.append(val_y.item()) + val_predictions.append(val_output.mean.argmax(dim=-1).item()) + + precision, recall, f1, _ = precision_recall_fscore_support(val_labels, val_predictions, average='macro') + auc_roc = roc_auc_score(label_binarize(val_labels, classes=range(n_classes)), + label_binarize(val_predictions, classes=range(n_classes)), + multi_class='ovr') + + metrics['precision'].append(precision) + metrics['recall'].append(recall) + metrics['f1_score'].append(f1) + metrics['auc_roc'].append(auc_roc) + val_loss /= len(val_indices) + + # Plot metrics + plt.figure(figsize=(12, 8)) + plt.subplot(2, 2, 1) + plt.plot(train_losses, label='Training Loss') + plt.plot(val_losses, label='Validation Loss') + plt.legend() + plt.title('Loss') + plt.xlabel('Epoch') + plt.ylabel('Loss') + + plt.subplot(2, 2, 2) + plt.plot(val_precisions, label='Precision') + plt.plot(val_recalls, label='Recall') + plt.plot(val_f1_scores, label='F1 Score') + plt.legend() + plt.title('Validation Metrics') + plt.xlabel('Epoch') + plt.ylabel('Metric') + + plt.subplot(2, 2, 3) + plt.plot(val_auc_rocs, label='AUC-ROC') + plt.legend() + plt.title('Validation AUC-ROC') + plt.xlabel('Epoch') + plt.ylabel('AUC-ROC') + + plt.tight_layout() + plt.savefig(plot_path) + plt.close() + + if val_loss < best_val_loss: + best_val_loss = val_loss + epochs_no_improve = 0 + torch.save({'model_state_dict': model.state_dict(), + 'likelihood_state_dict': likelihood.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'epoch': epoch}, checkpoint_path) + else: + epochs_no_improve += 1 + if epochs_no_improve >= patience: + print(f"Early stopping triggered at epoch {epoch+1}") + break + + if os.path.exists(checkpoint_path): + checkpoint = torch.load(checkpoint_path) + model.load_state_dict(checkpoint['model_state_dict']) + likelihood.load_state_dict(checkpoint['likelihood_state_dict']) + optimizer.load_state_dict(checkpoint['optimizer_state_dict']) + + # Save final model weights and other information + torch.save({ + 'model_state_dict': model.state_dict(), + 'likelihood_state_dict': likelihood.state_dict(), + 'optimizer_state_dict': optimizer.state_dict(), + 'train_losses': train_losses, + 'val_losses': val_losses, + 'val_precisions': val_precisions, + 'val_recalls': val_recalls, + 'val_f1_scores': val_f1_scores, + 'val_auc_rocs': val_auc_rocs + }, 'final_model_info.pt') + + return model, likelihood, metrics + +def evaluate_gp_model(test_loader, model, likelihood, n_classes=4): + model.eval() + likelihood.eval() + test_labels = [] + test_predictions = [] + + with torch.no_grad(): + for test_batch in test_loader: + test_x = test_batch['data'].reshape(test_batch['data'].size(0), -1).to(device) + test_y = test_batch['label'].to(device) + test_output = model(test_x) + test_labels.extend(test_y.tolist()) + test_predictions.extend(test_output.mean.argmax(dim=-1).tolist()) + + precision, recall, f1, _ = precision_recall_fscore_support(test_labels, test_predictions, average='macro') + auc_roc = roc_auc_score(label_binarize(test_labels, classes=range(n_classes)), + label_binarize(test_predictions, classes=range(n_classes)), + multi_class='ovr') + + metrics = { + 'precision': precision, + 'recall': recall, + 'f1_score': f1, + 'auc_roc': auc_roc + } + + return metrics + +def main(): + print("Step 1: Loading paths and parameters") + # Paths + base_path = r"\\grove.ad.uconn.edu\\research\\ENGR_Chon\Darren\\NIH_Pulsewatch" + smote_type = 'Cassey5k_SMOTE' + split = 'holdout_60_10_30' + data_path_train = os.path.join(base_path, "TFS_pt", smote_type, split, "train") + data_path_val = os.path.join(base_path, "TFS_pt", smote_type, split, "validate") + data_path_test = os.path.join(base_path, "TFS_pt", smote_type, split, "test") + labels_path_train = os.path.join(base_path, "TFS_pt", smote_type, split, "Cassey5k_SMOTE_train_names_labels.csv") + labels_path_val = os.path.join(base_path, "TFS_pt", smote_type, split, "Cassey5k_SMOTE_validate_names_labels.csv") + labels_path_test = os.path.join(base_path, "TFS_pt", smote_type, split, "Cassey5k_SMOTE_test_names_labels.csv") + + # Parameters + binary = False + n_epochs = 100 + if binary: + n_classes = 2 + else: + n_classes = 3 + patience = round(n_epochs / 10) if n_epochs > 50 else 5 + resume_checkpoint_path = None + batch_size = 256 + + print("Step 2: Loading data") + # Data loading + train_loader = load_data(data_path_train, labels_path_train, batch_size, binary) + val_loader = load_data(data_path_val, labels_path_val, batch_size, binary) + test_loader = load_data(data_path_test, labels_path_test, batch_size, binary) + + print("Step 3: Loading data checkpoints") + # Data loading with checkpointing + data_checkpoint_path = 'data_checkpoint.pt' + if os.path.exists(data_checkpoint_path): + train_loader.dataset.load_checkpoint(data_checkpoint_path) + val_loader.dataset.load_checkpoint(data_checkpoint_path) + test_loader.dataset.load_checkpoint(data_checkpoint_path) + + print("Step 4: Training and validation") + # Training and validation with checkpointing and plotting + model_checkpoint_path = 'model_checkpoint.pt' + plot_path = 'training_plot.png' + start_time = time.time() + model, likelihood, metrics = train_gp_model(train_loader, val_loader, n_epochs, + n_classes, patience, + model_checkpoint_path, data_checkpoint_path, + resume_checkpoint_path is not None, plot_path) + end_time = time.time() + time_passed = end_time - start_time + print('\nTraining and validation took %.2f minutes' % (time_passed / 60)) + + print("Step 5: Evaluation") + # Evaluation + start_time = time.time() + test_metrics = evaluate_gp_model(test_loader, model, likelihood, n_classes) + end_time = time.time() + time_passed = end_time - start_time + print('\nTesting took %.2f seconds' % time_passed) + + print("Step 6: Printing test metrics") + print('Test Metrics:') + print('Precision: %.4f' % test_metrics['precision']) + print('Recall: %.4f' % test_metrics['recall']) + print('F1 Score: %.4f' % test_metrics['f1_score']) + print('AUC-ROC: %.4f' % test_metrics['auc_roc']) + +if __name__ == '__main__': + main() \ No newline at end of file From 5f896204925d7cc4453ab29df8e9b5cd24a1d2cc Mon Sep 17 00:00:00 2001 From: Luis Roberto Mercado Diaz Date: Sat, 20 Apr 2024 15:57:39 -0400 Subject: [PATCH 2/2] labels updated on path --- GP_Original_checkpoint.py | 2 +- __pycache__/GP_original_data.cpython-312.pyc | Bin 0 -> 33664 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 __pycache__/GP_original_data.cpython-312.pyc diff --git a/GP_Original_checkpoint.py b/GP_Original_checkpoint.py index 0bdceae..5d90699 100644 --- a/GP_Original_checkpoint.py +++ b/GP_Original_checkpoint.py @@ -279,7 +279,7 @@ def load_checkpoint(self, loader_name): data_path, labels_path, saving_path = get_data_paths(data_format, is_linux=is_linux, is_hpc=is_hpc) # Define batch size for loading data - batch_size = 512 + batch_size = 1024 # Load the training, validation, and test data train_loader = load_data_split_batched(data_path, labels_path, clinical_trial_train, batch_size, standardize=True, data_format='csv', read_all_labels=False, drop_last=True) val_loader = load_data_split_batched(data_path, labels_path, clinical_trial_test, batch_size, standardize=True, data_format='csv', read_all_labels=False, drop_last=True) diff --git a/__pycache__/GP_original_data.cpython-312.pyc b/__pycache__/GP_original_data.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc57c371de8ad29f7612e8816b66bf6fedb39a33 GIT binary patch literal 33664 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 cN)_$7VQ@wC|_?U$=Ij7XSbN literal 0 HcmV?d00001