From 7c7890eec244db3d4757317c8ff7120d7ebda121 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 00:12:09 +0200 Subject: [PATCH 001/136] debug test run --- models/purple_alien/configs/config_hyperparameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index d3afb65b..a67683a1 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 600, # 10 just for debug + 'samples': 10, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, @@ -24,7 +24,7 @@ def get_hp_config(): 'loss_reg': 'b', 'loss_reg_a' : 258, 'loss_reg_c' : 0.001, # 0.05 works... - 'test_samples': 128, + 'test_samples': 10, # 128 for actual testing, 10 for debug 'np_seed' : 4, 'torch_seed' : 4, 'window_dim' : 32, From 19ac02108742aadb8a4d3d9204e5a558f6a1058b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 00:24:20 +0200 Subject: [PATCH 002/136] added setup_artifact_path --- models/purple_alien/src/training/train_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index 1fc31ae5..ffc36313 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -15,8 +15,9 @@ PATH = Path(__file__) sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths +from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) +setup_artifacts_paths(PATH) from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data #from config_sweep import get_swep_config From 5348675d5483545fabf86dc65c559ff3edc170fb Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 00:28:58 +0200 Subject: [PATCH 003/136] new path_art.. --- models/purple_alien/src/training/train_model.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index ffc36313..50d9f724 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -17,7 +17,6 @@ sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) -setup_artifacts_paths(PATH) from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data #from config_sweep import get_swep_config @@ -191,8 +190,10 @@ def model_pipeline(config = None, project = None): model = model_pipeline(config = hyperparameters, project = project) # this works because the specfic artifacts path is added to sys.path in set_path.py at the start of the script - PATH_ARTIFACTS = [i for i in sys.path if "artifacts" in i][0] # this is a list with one element (a str), so I can just index it with 0 - + # PATH_ARTIFACTS = [i for i in sys.path if "artifacts" in i][0] # this is a list with one element (a str), so I can just index it with 0 + + PATH_ARTIFACTS = setup_artifacts_paths(PATH) + # create the artifacts folder if it does not exist os.makedirs(PATH_ARTIFACTS, exist_ok=True) From d840fc7da12a1ccd18219c076b99a82828f85e3f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 00:53:26 +0200 Subject: [PATCH 004/136] removed comments --- models/purple_alien/src/training/train_model.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index 50d9f724..a433b45b 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -189,9 +189,6 @@ def model_pipeline(config = None, project = None): model = model_pipeline(config = hyperparameters, project = project) - # this works because the specfic artifacts path is added to sys.path in set_path.py at the start of the script - # PATH_ARTIFACTS = [i for i in sys.path if "artifacts" in i][0] # this is a list with one element (a str), so I can just index it with 0 - PATH_ARTIFACTS = setup_artifacts_paths(PATH) # create the artifacts folder if it does not exist From e8c55c2c1cc4b0aa2eb1f9fa7cb2ab7bdcd1e083 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:04:43 +0200 Subject: [PATCH 005/136] first main for P-A --- models/purple_alien/main.py | 93 +++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 models/purple_alien/main.py diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py new file mode 100644 index 00000000..f1e2de34 --- /dev/null +++ b/models/purple_alien/main.py @@ -0,0 +1,93 @@ +import numpy as np +import pickle +import time +import os +import functools + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import wandb + +import sys +from pathlib import Path + +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths, setup_artifacts_paths +setup_project_paths(PATH) + +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +#from config_sweep import get_swep_config +from config_hyperparameters import get_hp_config +from train_model import make, training_loop + + +print('Imports done...') + + +def model_pipeline(config = None, project = None): + + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(device) + + # tell wandb to get started + with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep + + wandb.define_metric("monthly/out_sample_month") + wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + + # access all HPs through wandb.config, so logging matches execution! + config = wandb.config + + views_vol = get_data(config) + + # make the model, data, and optimization problem + model, criterion, optimizer, scheduler = make(config, device) + + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') + + return(model) + + +if __name__ == "__main__": + + wandb.login() + + # model type is still a vary bad name here - it should be something like run_type... Change later! + # Also, can you even choose testing and forecasting here? + model_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} + model_type = model_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] + print(f'Run type: {model_type}\n') + + project = f"imp_new_structure_{model_type}" # temp. also a bad name. Change later! + + hyperparameters = get_hp_config() + + hyperparameters['model_type'] = model_type # bad name... ! Change later! + hyperparameters['sweep'] = False + + start_t = time.time() + + model = model_pipeline(config = hyperparameters, project = project) + + PATH_ARTIFACTS = setup_artifacts_paths(PATH) + + # create the artifacts folder if it does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) + + # save the model + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{model_type}_model.pt") + torch.save(model, PATH_MODEL_ARTIFACT) + + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + + end_t = time.time() + minutes = (end_t - start_t)/60 + print(f'Done. Runtime: {minutes:.3f} minutes') + + + + From 6392cefe891671837f49787f3a7d426024900989 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:19:44 +0200 Subject: [PATCH 006/136] model_type to run_type --- models/purple_alien/main.py | 12 +- .../src/offline_evaluation/evaluate_model.py | 16 +-- .../purple_alien/src/training/train_model.py | 121 +++++++++--------- models/purple_alien/src/utils/utils.py | 4 +- 4 files changed, 77 insertions(+), 76 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index f1e2de34..5388b209 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -58,15 +58,15 @@ def model_pipeline(config = None, project = None): # model type is still a vary bad name here - it should be something like run_type... Change later! # Also, can you even choose testing and forecasting here? - model_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} - model_type = model_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] - print(f'Run type: {model_type}\n') + run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} + run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] + print(f'Run type: {run_type}\n') - project = f"imp_new_structure_{model_type}" # temp. also a bad name. Change later! + project = f"imp_new_structure_{run_type}" # temp. also a bad name. Change later! hyperparameters = get_hp_config() - hyperparameters['model_type'] = model_type # bad name... ! Change later! + hyperparameters['run_type'] = run_type # bad name... ! Change later! hyperparameters['sweep'] = False start_t = time.time() @@ -79,7 +79,7 @@ def model_pipeline(config = None, project = None): os.makedirs(PATH_ARTIFACTS, exist_ok=True) # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{model_type}_model.pt") + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") torch.save(model, PATH_MODEL_ARTIFACT) print(f"Model saved as: {PATH_MODEL_ARTIFACT}") diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 90bcf0ae..ef1cb2bc 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -175,13 +175,13 @@ def get_posterior(model, views_vol, config, device): metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.model_type}.pkl', 'wb') as file: + with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{dump_location}metric_dict_{config.time_steps}_{config.model_type}.pkl', 'wb') as file: + with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(metric_dict, file) - with open(f'{dump_location}test_vol_{config.time_steps}_{config.model_type}.pkl', 'wb') as file: # make it numpy + with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy pickle.dump(test_tensor.cpu().numpy(), file) print('Posterior dict, metric dict and test vol pickled and dumped!') @@ -233,16 +233,16 @@ def model_pipeline(config = None, project = None): time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] - model_type_dict = {'a' : 'calibration', 'b' : 'testing'} - model_type = model_type_dict[input("a) Calibration\nb) Testing\n")] - print(f'Run type: {model_type}\n') + run_type_dict = {'a' : 'calibration', 'b' : 'testing'} + run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] + print(f'Run type: {run_type}\n') - project = f"imp_new_structure_{model_type}" # temp. + project = f"imp_new_structure_{run_type}" # temp. hyperparameters = get_hp_config() hyperparameters['time_steps'] = time_steps - hyperparameters['model_type'] = model_type + hyperparameters['run_type'] = run_type hyperparameters['sweep'] = False start_t = time.time() diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index a433b45b..dd2caba9 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -143,63 +143,64 @@ def training_loop(config, model, criterion, optimizer, scheduler, views_vol, dev print('training done...') - -def model_pipeline(config = None, project = None): - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(device) - - # tell wandb to get started - with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep - - wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - - # access all HPs through wandb.config, so logging matches execution! - config = wandb.config - - views_vol = get_data(config) - - # make the model, data, and optimization problem - model, criterion, optimizer, scheduler = make(config, device) - - training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - print('Done training') - - return(model) - - -if __name__ == "__main__": - - wandb.login() - - # model type is still a vary bad name here - it should be something like run_type... Change later! - model_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} - model_type = model_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] - print(f'Run type: {model_type}\n') - - project = f"imp_new_structure_{model_type}" # temp. also a bad name. Change later! - - hyperparameters = get_hp_config() - - hyperparameters['model_type'] = model_type # bad name... ! Change later! - hyperparameters['sweep'] = False - - start_t = time.time() - - model = model_pipeline(config = hyperparameters, project = project) - - PATH_ARTIFACTS = setup_artifacts_paths(PATH) - - # create the artifacts folder if it does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) - - # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{model_type}_model.pt") - torch.save(model, PATH_MODEL_ARTIFACT) - - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") - - end_t = time.time() - minutes = (end_t - start_t)/60 - print(f'Done. Runtime: {minutes:.3f} minutes') +# MOVE TO NEW main.py IN purple_alien root. +# def model_pipeline(config = None, project = None): +# +# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +# print(device) +# +# # tell wandb to get started +# with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep +# +# wandb.define_metric("monthly/out_sample_month") +# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") +# +# # access all HPs through wandb.config, so logging matches execution! +# config = wandb.config +# +# views_vol = get_data(config) +# +# # make the model, data, and optimization problem +# model, criterion, optimizer, scheduler = make(config, device) +# +# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) +# print('Done training') +# +# return(model) +# +# +# if __name__ == "__main__": +# +# wandb.login() +# +# # model type is still a vary bad name here - it should be something like run_type... Change later! +# model_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} +# model_type = model_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] +# print(f'Run type: {model_type}\n') +# +# project = f"imp_new_structure_{model_type}" # temp. also a bad name. Change later! +# +# hyperparameters = get_hp_config() +# +# hyperparameters['model_type'] = model_type # bad name... ! Change later! +# hyperparameters['sweep'] = False +# +# start_t = time.time() +# +# model = model_pipeline(config = hyperparameters, project = project) +# +# PATH_ARTIFACTS = setup_artifacts_paths(PATH) +# +# # create the artifacts folder if it does not exist +# os.makedirs(PATH_ARTIFACTS, exist_ok=True) +# +# # save the model +# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{model_type}_model.pt") +# torch.save(model, PATH_MODEL_ARTIFACT) +# +# print(f"Model saved as: {PATH_MODEL_ARTIFACT}") +# +# end_t = time.time() +# minutes = (end_t - start_t)/60 +# print(f'Done. Runtime: {minutes:.3f} minutes') +# \ No newline at end of file diff --git a/models/purple_alien/src/utils/utils.py b/models/purple_alien/src/utils/utils.py index 950e8c32..fc5da826 100644 --- a/models/purple_alien/src/utils/utils.py +++ b/models/purple_alien/src/utils/utils.py @@ -204,10 +204,10 @@ def get_data(config): _, PATH_PROCESSED, _ = setup_data_paths(PATH) - model_type = config.model_type # 'calibration', 'testing' or 'forecasting' + run_type = config.run_type # 'calibration', 'testing' or 'forecasting' try: - file_name = f'/{model_type}_vol.npy' # NOT WINDOWS FRIENDLY + file_name = f'/{run_type}_vol.npy' # NOT WINDOWS FRIENDLY views_vol = np.load(str(PATH_PROCESSED) + file_name) except FileNotFoundError as e: From 0e14f93031f3e63ff376005fa7b7066d76f38e76 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:26:33 +0200 Subject: [PATCH 007/136] nornir to viewspipeline --- models/purple_alien/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 5388b209..588bc911 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -33,8 +33,9 @@ def model_pipeline(config = None, project = None): print(device) # tell wandb to get started - with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep + with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep + # for the monthly metrics wandb.define_metric("monthly/out_sample_month") wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") From c582bdcef727c5dfd1e6b638b3105c5d290ed2b5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:27:56 +0200 Subject: [PATCH 008/136] nornir to views_pipeline --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 2 +- models/purple_alien/src/offline_evaluation/evaluate_sweep.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index ef1cb2bc..3ee463cd 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -198,7 +198,7 @@ def model_pipeline(config = None, project = None): print(device) # tell wandb to get started - with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep + with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep wandb.define_metric("monthly/out_sample_month") wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index 0f0f8ca9..977b8de9 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -329,7 +329,7 @@ def model_pipeline(config = None, project = None): print(device) # tell wandb to get started - with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep + with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep wandb.define_metric("monthly/out_sample_month") wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") From 9bd3c25e0fe0009d20d6b29b1887411ed9131489 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:37:02 +0200 Subject: [PATCH 009/136] for debug --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 3b57d2d3..dcc2f6f3 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 600}, # should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' : 128}, + 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From eb700e1bc8f54a60833c789dc512045ccf79be27 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:39:38 +0200 Subject: [PATCH 010/136] added get_data to import --- models/purple_alien/src/offline_evaluation/evaluate_sweep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index 977b8de9..f1d935a7 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -24,7 +24,7 @@ from set_path import setup_project_paths setup_project_paths(PATH) -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from config_sweep import get_swep_config from config_hyperparameters import get_hp_config From 41c8c4b092b3d1deb8251d4fab3caa3ef64e5630 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 01:45:24 +0200 Subject: [PATCH 011/136] new run_type_dict --- models/purple_alien/src/offline_evaluation/evaluate_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index f1d935a7..15f00408 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -364,8 +364,8 @@ def model_pipeline(config = None, project = None): time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] - runtype_dict = {'a' : 'calib', 'b' : 'test'} - run_type = runtype_dict[input("a) Calibration\nb) Testing\n")] + run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} + run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] print(f'Run type: {run_type}\n') do_sweep = input(f'a) Do sweep \nb) Do one run and pickle results \n') From e3b2e2277bc15ddfba93bb8f25dd944137980713 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:16:35 +0200 Subject: [PATCH 012/136] argparse solution --- models/purple_alien/main.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 588bc911..f8eb0518 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -53,21 +53,43 @@ def model_pipeline(config = None, project = None): return(model) + +import argparse + +def parse_args(): + parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') + + parser.add_argument('--run_type', + choices=['calibration', 'testing', 'forecasting'], + type=str, + default='calibration', + help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration.') + + return parser.parse_args() + + if __name__ == "__main__": wandb.login() - # model type is still a vary bad name here - it should be something like run_type... Change later! - # Also, can you even choose testing and forecasting here? - run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} - run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] + # can you even choose testing and forecasting here? + #run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} + #run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] + #print(f'Run type: {run_type}\n') + + # new argpars solution. + args = parse_args() + + # Extract run_type from parsed arguments + run_type = args.run_type print(f'Run type: {run_type}\n') - project = f"imp_new_structure_{run_type}" # temp. also a bad name. Change later! + + project = f"imp_new_structure_{run_type}" hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type # bad name... ! Change later! + hyperparameters['run_type'] = run_type hyperparameters['sweep'] = False start_t = time.time() From 9348e4b4eb9c2cb55a1329af2deaf16b745f6b7b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:26:41 +0200 Subject: [PATCH 013/136] starting on sweep --- models/purple_alien/main.py | 49 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index f8eb0518..ed5c0845 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -65,6 +65,12 @@ def parse_args(): default='calibration', help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration.') + parser.add_argument('--sweep', + choices=[True, False], + type=bool, + default=False, + help='Choose whether to run the model pipeline as part of a sweep. Default is False.') + return parser.parse_args() @@ -80,32 +86,43 @@ def parse_args(): # new argpars solution. args = parse_args() - # Extract run_type from parsed arguments - run_type = args.run_type - print(f'Run type: {run_type}\n') + if args.sweep: + + print('not implemented yet') + os.exit() + + #sweep_config = get_swep_config() + #wandb.agent(sweep_config, function = model_pipeline) + #sys.exit() + + else: + + # Extract run_type from parsed arguments + run_type = args.run_type + print(f'Run type: {run_type}\n') - project = f"imp_new_structure_{run_type}" + project = f"imp_new_structure_{run_type}" - hyperparameters = get_hp_config() + hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type - hyperparameters['sweep'] = False + hyperparameters['run_type'] = run_type + hyperparameters['sweep'] = False - start_t = time.time() + start_t = time.time() - model = model_pipeline(config = hyperparameters, project = project) + model = model_pipeline(config = hyperparameters, project = project) - PATH_ARTIFACTS = setup_artifacts_paths(PATH) + PATH_ARTIFACTS = setup_artifacts_paths(PATH) - # create the artifacts folder if it does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) + # create the artifacts folder if it does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) - # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") - torch.save(model, PATH_MODEL_ARTIFACT) + # save the model + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") + torch.save(model, PATH_MODEL_ARTIFACT) - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") end_t = time.time() minutes = (end_t - start_t)/60 From 321ee62f08b7a01724dabfbcc7cadb1ab1e57223 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:36:12 +0200 Subject: [PATCH 014/136] sweep back in --- models/purple_alien/main.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index ed5c0845..5e30d590 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -19,7 +19,7 @@ setup_project_paths(PATH) from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data -#from config_sweep import get_swep_config +from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop @@ -88,21 +88,29 @@ def parse_args(): if args.sweep: - print('not implemented yet') - os.exit() - - #sweep_config = get_swep_config() - #wandb.agent(sweep_config, function = model_pipeline) - #sys.exit() + print('Running sweep...') + + project = f"purple_alien_sweep" # check naming convention + + sweep_config = get_swep_config() + sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep + sweep_config['parameters']['sweep'] = {'value' : True} + sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name + + start_t = time.time() + wandb.agent(sweep_id, model_pipeline) + else: + print('Train one model and save it as an artifact...') + # Extract run_type from parsed arguments run_type = args.run_type print(f'Run type: {run_type}\n') - project = f"imp_new_structure_{run_type}" + project = f"purple_alien_{run_type}" # check naming convention hyperparameters = get_hp_config() From 7825fe7f2edd2f00942a87aabc688ed64c2cfe0b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:38:22 +0200 Subject: [PATCH 015/136] added time_steps here --- models/purple_alien/configs/config_sweep.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index dcc2f6f3..40974fb8 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -43,6 +43,7 @@ def get_swep_config(): 'first_feature_idx' : {'value' : 5}, 'norm_target' : {'value' : False}, 'freeze_h' : {'value' : "hl"}, + 'time_steps' : {'value' : 36} } sweep_config['parameters'] = parameters_dict From 76673b09c169753e4f5583402641afb6dd6aba2c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:52:22 +0200 Subject: [PATCH 016/136] added get_posterior --- models/purple_alien/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 5e30d590..7444f719 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -50,9 +50,12 @@ def model_pipeline(config = None, project = None): training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) print('Done training') - return(model) - + if config.sweep: + get_posterior(unet, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, + print('Done testing') + else: + return(model) import argparse From ed2e07035ccd399a4a45d120e0fce43d9ade1b76 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:55:56 +0200 Subject: [PATCH 017/136] posterior... --- models/purple_alien/main.py | 4 +- .../src/offline_evaluation/evaluate_sweep.py | 175 +++--------------- 2 files changed, 25 insertions(+), 154 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 7444f719..352f982b 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -22,7 +22,7 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop - +from offline_evaluation import get_posterior print('Imports done...') @@ -55,7 +55,7 @@ def model_pipeline(config = None, project = None): print('Done testing') else: - return(model) + return(model) import argparse diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index 15f00408..c3c036c1 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -29,133 +29,6 @@ from config_hyperparameters import get_hp_config -# SHOULD BE TRAIN SCRIPT ------------------------------------------------------------------ - -def make(config, device): - - unet = choose_model(config, device) - - # Create a partial function with the initialization function and the config parameter - init_fn = functools.partial(init_weights, config=config) - - # Apply the initialization function to the modeli - unet.apply(init_fn) - - # choose loss function - criterion = choose_loss(config, device) # this is a touple of the reg and the class criteria - - # choose sheduler - the optimizer is always AdamW right now - optimizer, scheduler = choose_sheduler(config, unet) - - return(unet, criterion, optimizer, scheduler) #, dataloaders, dataset_sizes) - - -def train(model, optimizer, scheduler, criterion_reg, criterion_class, multitaskloss_instance, views_vol, sample, config, device): # views vol and sample - - wandb.watch(model, [criterion_reg, criterion_class], log= None, log_freq=2048) - - avg_loss_reg_list = [] - avg_loss_class_list = [] - avg_loss_list = [] - total_loss = 0 - - model.train() # train mode - multitaskloss_instance.train() # meybe another place... - - - # Batch loops: - for batch in range(config.batch_size): - - # Getting the train_tensor - train_tensor = get_train_tensors(views_vol, sample, config, device) - seq_len = train_tensor.shape[1] - window_dim = train_tensor.shape[-1] # the last dim should always be a spatial dim (H or W) - - # initialize a hidden state - h = model.init_h(hidden_channels = model.base, dim = window_dim).float().to(device) - - # Sequens loop rnn style - for i in range(seq_len-1): # so your sequnce is the full time len - last month. - print(f'\t\t month: {i+1}/{seq_len}...', end='\r') - - t0 = train_tensor[:, i, :, :, :] - - t1 = train_tensor[:, i+1, :, :, :] - t1_binary = (t1.clone().detach().requires_grad_(True) > 0) * 1.0 # 1.0 to ensure float. Should avoid cloning warning now. - - # forward-pass - t1_pred, t1_pred_class, h = model(t0, h.detach()) - - losses_list = [] - - for j in range(t1_pred.shape[1]): # first each reggression loss. Should be 1 channel, as I conccat the reg heads on dim = 1 - - losses_list.append(criterion_reg(t1_pred[:,j,:,:], t1[:,j,:,:])) # index 0 is batch dim, 1 is channel dim (here pred), 2 is H dim, 3 is W dim - - for j in range(t1_pred_class.shape[1]): # then each classification loss. Should be 1 channel, as I conccat the class heads on dim = 1 - - losses_list.append(criterion_class(t1_pred_class[:,j,:,:], t1_binary[:,j,:,:])) # index 0 is batch dim, 1 is channel dim (here pred), 2 is H dim, 3 is W dim - - losses = torch.stack(losses_list) - loss = multitaskloss_instance(losses) - - total_loss += loss - - # traning output - loss_reg = losses[:t1_pred.shape[1]].sum() # sum the reg losses - loss_class = losses[-t1_pred.shape[1]:].sum() # assuming - - avg_loss_reg_list.append(loss_reg.detach().cpu().numpy().item()) - avg_loss_class_list.append(loss_class.detach().cpu().numpy().item()) - avg_loss_list.append(loss.detach().cpu().numpy().item()) - - - # log each sequence/timeline/batch - train_log(avg_loss_list, avg_loss_reg_list, avg_loss_class_list) # FIX!!! - - # Backpropagation and optimization - after a full sequence... - optimizer.zero_grad() - total_loss.backward() - - # Gradient Clipping - if config.clip_grad_norm == True: - clip_value = 1.0 - torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=clip_value) - - else: - pass - - # optimize - optimizer.step() - - # Adjust learning rate based on the loss - scheduler.step() - - -def training_loop(config, model, criterion, optimizer, scheduler, views_vol, device): - - # # add spatail transformer - - criterion_reg, criterion_class, multitaskloss_instance = criterion - - np.random.seed(config.np_seed) - torch.manual_seed(config.torch_seed) - print(f'Training initiated...') - - for sample in range(config.samples): - - print(f'Sample: {sample+1}/{config.samples}', end = '\r') - - train(model, optimizer, scheduler , criterion_reg, criterion_class, multitaskloss_instance, views_vol, sample, config, device) - - print('training done...') - - - - -# SHOULD BE TEST SCRIPT ------------------------------------------------------------------ - - def test(model, test_tensor, time_steps, config, device): # should be called eval/validation """ @@ -199,7 +72,6 @@ def test(model, test_tensor, time_steps, config, device): # should be called eva return pred_np_list, pred_class_np_list - def sample_posterior(model, views_vol, config, device): """ @@ -237,7 +109,6 @@ def sample_posterior(model, views_vol, config, device): return posterior_list, posterior_list_class, out_of_sample_vol, test_tensor - def get_posterior(model, views_vol, config, device): """ @@ -287,29 +158,29 @@ def get_posterior(model, views_vol, config, device): auc_list.append(auc) brier_list.append(brier) - if not config.sweep: - - # DUMP 2 - dump_location = '/home/projects/ku_00017/data/generated/conflictNet/' # should be in config - - posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - - metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, - 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - - with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(posterior_dict, file) - - with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(metric_dict, file) - - with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy - pickle.dump(test_tensor.cpu().numpy(), file) - - print('Posterior dict, metric dict and test vol pickled and dumped!') - - else: - print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') +# if not config.sweep: +# +# # DUMP 2 +# dump_location = '/home/projects/ku_00017/data/generated/conflictNet/' # should be in config +# +# posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} +# +# metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, +# 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} +# +# with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: +# pickle.dump(posterior_dict, file) +# +# with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: +# pickle.dump(metric_dict, file) +# +# with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy +# pickle.dump(test_tensor.cpu().numpy(), file) +# +# print('Posterior dict, metric dict and test vol pickled and dumped!') + +# else: + print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') # ------------------------------------------------------------------------------------ wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) From c012f18b85dbb3b3cfdcbe3904f6c3dd6ba51d18 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:58:57 +0200 Subject: [PATCH 018/136] fix? --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 352f982b..b36a6f4a 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -22,7 +22,7 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop -from offline_evaluation import get_posterior +from evaluate_swep import get_posterior # see if it can be more genrel to a single model as well... print('Imports done...') From 1114d816b39a74c22e11e506e9da3b7f55a502f9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 03:59:42 +0200 Subject: [PATCH 019/136] fix?? --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index b36a6f4a..796b6744 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -22,7 +22,7 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop -from evaluate_swep import get_posterior # see if it can be more genrel to a single model as well... +from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... print('Imports done...') From e0bd460fdb7ab75d5f564eaed27103b8da6e75a6 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:03:21 +0200 Subject: [PATCH 020/136] unet -> model --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 796b6744..9ad4cc29 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -51,7 +51,7 @@ def model_pipeline(config = None, project = None): print('Done training') if config.sweep: - get_posterior(unet, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, + get_posterior(model, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, print('Done testing') else: From 1a63be0faa57bf5a727146044754571b54a57801 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:31:46 +0200 Subject: [PATCH 021/136] validate args --- models/purple_alien/main.py | 60 +++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 9ad4cc29..9b93598d 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -24,8 +24,6 @@ from train_model import make, training_loop from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... -print('Imports done...') - def model_pipeline(config = None, project = None): @@ -57,28 +55,79 @@ def model_pipeline(config = None, project = None): else: return(model) + + +# --------------------------- + import argparse def parse_args(): parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') - parser.add_argument('--run_type', + parser.add_argument('-r', '--run_type', choices=['calibration', 'testing', 'forecasting'], type=str, default='calibration', help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration.') - parser.add_argument('--sweep', + parser.add_argument('-s', '--sweep', choices=[True, False], type=bool, default=False, help='Choose whether to run the model pipeline as part of a sweep. Default is False.') + + parser.add_argument('-t', '--train', + choices=[True, False], + type=bool, + default=False, + help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact.') + + parser.add_argument('-e', '--evaluate', + choices=[True, False], + type=bool, + default=False, + help='Flag to indicate if the model should be evaluated.') + + return parser.parse_args() +def validate_arguments(args): + + if args.sweep: + if args.run_type != 'calibration': + print("Sweep runs must have run_type set to 'calibration'. Exiting.") + sys.exit(1) + + print("Sweep runs must train and evaluate the model. Setting train and evaluate flags to True.") + args.train = True + args.evaluate = True + + if args.run_type in ['testing', 'forecasting'] and args.sweep: + print("Sweep cannot be performed with testing or forecasting run types. Exiting.") + sys.exit(1) + + if args.run_type == 'forecasting' and args.evaluate: + print("Forecasting runs cannot be evaluated. Exiting.") + sys.exit(1) + + if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: + print(f"Run type is {args.run_type} but neither train nor evaluate flag is set. Nothing to do... Exiting.") + sys.exit(1) + +# --------------------------- + + if __name__ == "__main__": + # new argpars solution. + args = parse_args() + + # validate arguments to ensure that only correct combinations of flags are set + validate_arguments(args) + + # wandb login wandb.login() # can you even choose testing and forecasting here? @@ -86,8 +135,7 @@ def parse_args(): #run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] #print(f'Run type: {run_type}\n') - # new argpars solution. - args = parse_args() + if args.sweep: From 5046b369886b2e9c8d5bd51f695c9be23a99a4a2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:45:34 +0200 Subject: [PATCH 022/136] better help and warnings --- models/purple_alien/main.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 9b93598d..ed159b9b 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -68,52 +68,56 @@ def parse_args(): choices=['calibration', 'testing', 'forecasting'], type=str, default='calibration', - help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration.') + help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' + 'Note: If --sweep is True, --run_type must be calibration.') parser.add_argument('-s', '--sweep', choices=[True, False], type=bool, default=False, - help='Choose whether to run the model pipeline as part of a sweep. Default is False.') - + help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' + 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True.') parser.add_argument('-t', '--train', choices=[True, False], type=bool, default=False, - help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact.') + help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' + 'Note: If --sweep is True, --train will be set to True automatically.') parser.add_argument('-e', '--evaluate', choices=[True, False], type=bool, default=False, - help='Flag to indicate if the model should be evaluated.') - + help='Flag to indicate if the model should be evaluated. ' + 'Note: If --sweep is True, --evaluate will be set to True automatically.' + 'Cannot be used with --run_type forecasting.') return parser.parse_args() def validate_arguments(args): - if args.sweep: if args.run_type != 'calibration': print("Sweep runs must have run_type set to 'calibration'. Exiting.") + print("To fix: Use --run_type calibration when --sweep is specified.") sys.exit(1) - - print("Sweep runs must train and evaluate the model. Setting train and evaluate flags to True.") args.train = True args.evaluate = True if args.run_type in ['testing', 'forecasting'] and args.sweep: print("Sweep cannot be performed with testing or forecasting run types. Exiting.") + print("To fix: Use --sweep False or set --run_type to 'calibration'.") sys.exit(1) if args.run_type == 'forecasting' and args.evaluate: - print("Forecasting runs cannot be evaluated. Exiting.") + print("Forecasting runs cannot evaluate. Exiting.") + print("To fix: Use --evaluate False when --run_type is 'forecasting'.") sys.exit(1) if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: - print(f"Run type is {args.run_type} but neither train nor evaluate flag is set. Nothing to do... Exiting.") + print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") + print("To fix: Use --train True and/or --evaluate True.") sys.exit(1) # --------------------------- From 11e45fec281fb7279032ee7c8e4d98a5b355625f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:51:18 +0200 Subject: [PATCH 023/136] new parser script --- common_utils/cli_parser_utils.py | 62 +++++++++++++++ models/purple_alien/main.py | 125 ++++++++++++++++--------------- 2 files changed, 125 insertions(+), 62 deletions(-) create mode 100644 common_utils/cli_parser_utils.py diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py new file mode 100644 index 00000000..18e22ff9 --- /dev/null +++ b/common_utils/cli_parser_utils.py @@ -0,0 +1,62 @@ +import argparse + +def parse_args(): + parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') + + parser.add_argument('-r', '--run_type', + choices=['calibration', 'testing', 'forecasting'], + type=str, + default='calibration', + help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' + 'Note: If --sweep is True, --run_type must be calibration.') + + parser.add_argument('-s', '--sweep', + choices=[True, False], + type=bool, + default=False, + help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' + 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True automatically.') + + parser.add_argument('-t', '--train', + choices=[True, False], + type=bool, + default=False, + help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' + 'Note: If --sweep is True, --train will be set to True automatically.') + + parser.add_argument('-e', '--evaluate', + choices=[True, False], + type=bool, + default=False, + help='Flag to indicate if the model should be evaluated. ' + 'Note: If --sweep is True, --evaluate will be set to True automatically.' + 'Cannot be used with --run_type forecasting.') + + return parser.parse_args() + + +def validate_arguments(args): + if args.sweep: + if args.run_type != 'calibration': + print("Sweep runs must have run_type set to 'calibration'. Exiting.") + print("To fix: Use --run_type calibration when --sweep True.") + + sys.exit(1) + args.train = True + args.evaluate = True + + if args.run_type in ['testing', 'forecasting'] and args.sweep: + print("Sweep cannot be performed with testing or forecasting run types. Exiting.") + print("To fix: Use --sweep False or set --run_type to 'calibration'.") + sys.exit(1) + + if args.run_type == 'forecasting' and args.evaluate: + print("Forecasting runs cannot evaluate. Exiting.") + print("To fix: Use --evaluate False when --run_type is 'forecasting'.") + sys.exit(1) + + if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: + print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") + print("To fix: Use --train True and/or --evaluate True.") + sys.exit(1) + diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index ed159b9b..9829ecbd 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -23,7 +23,7 @@ from config_hyperparameters import get_hp_config from train_model import make, training_loop from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... - +from cli_parser import parse_args, validate_arguments def model_pipeline(config = None, project = None): @@ -59,67 +59,68 @@ def model_pipeline(config = None, project = None): # --------------------------- -import argparse - -def parse_args(): - parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') - - parser.add_argument('-r', '--run_type', - choices=['calibration', 'testing', 'forecasting'], - type=str, - default='calibration', - help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' - 'Note: If --sweep is True, --run_type must be calibration.') - - parser.add_argument('-s', '--sweep', - choices=[True, False], - type=bool, - default=False, - help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' - 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True.') - - parser.add_argument('-t', '--train', - choices=[True, False], - type=bool, - default=False, - help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' - 'Note: If --sweep is True, --train will be set to True automatically.') - - parser.add_argument('-e', '--evaluate', - choices=[True, False], - type=bool, - default=False, - help='Flag to indicate if the model should be evaluated. ' - 'Note: If --sweep is True, --evaluate will be set to True automatically.' - 'Cannot be used with --run_type forecasting.') - - return parser.parse_args() - - -def validate_arguments(args): - if args.sweep: - if args.run_type != 'calibration': - print("Sweep runs must have run_type set to 'calibration'. Exiting.") - print("To fix: Use --run_type calibration when --sweep is specified.") - sys.exit(1) - args.train = True - args.evaluate = True - - if args.run_type in ['testing', 'forecasting'] and args.sweep: - print("Sweep cannot be performed with testing or forecasting run types. Exiting.") - print("To fix: Use --sweep False or set --run_type to 'calibration'.") - sys.exit(1) - - if args.run_type == 'forecasting' and args.evaluate: - print("Forecasting runs cannot evaluate. Exiting.") - print("To fix: Use --evaluate False when --run_type is 'forecasting'.") - sys.exit(1) - - if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: - print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") - print("To fix: Use --train True and/or --evaluate True.") - sys.exit(1) - +#import argparse +# +#def parse_args(): +# parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') +# +# parser.add_argument('-r', '--run_type', +# choices=['calibration', 'testing', 'forecasting'], +# type=str, +# default='calibration', +# help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' +# 'Note: If --sweep is True, --run_type must be calibration.') +# +# parser.add_argument('-s', '--sweep', +# choices=[True, False], +# type=bool, +# default=False, +# help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' +# 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True automatically.') +# +# parser.add_argument('-t', '--train', +# choices=[True, False], +# type=bool, +# default=False, +# help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' +# 'Note: If --sweep is True, --train will be set to True automatically.') +# +# parser.add_argument('-e', '--evaluate', +# choices=[True, False], +# type=bool, +# default=False, +# help='Flag to indicate if the model should be evaluated. ' +# 'Note: If --sweep is True, --evaluate will be set to True automatically.' +# 'Cannot be used with --run_type forecasting.') +# +# return parser.parse_args() +# +# +#def validate_arguments(args): +# if args.sweep: +# if args.run_type != 'calibration': +# print("Sweep runs must have run_type set to 'calibration'. Exiting.") +# print("To fix: Use --run_type calibration when --sweep True.") +# +# sys.exit(1) +# args.train = True +# args.evaluate = True +# +# if args.run_type in ['testing', 'forecasting'] and args.sweep: +# print("Sweep cannot be performed with testing or forecasting run types. Exiting.") +# print("To fix: Use --sweep False or set --run_type to 'calibration'.") +# sys.exit(1) +# +# if args.run_type == 'forecasting' and args.evaluate: +# print("Forecasting runs cannot evaluate. Exiting.") +# print("To fix: Use --evaluate False when --run_type is 'forecasting'.") +# sys.exit(1) +# +# if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: +# print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") +# print("To fix: Use --train True and/or --evaluate True.") +# sys.exit(1) +# # --------------------------- From 6db4f8cd3365aa1e37cd886c8278eced77b1e9fe Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:52:08 +0200 Subject: [PATCH 024/136] fix? --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 9829ecbd..680aa4a5 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -23,7 +23,7 @@ from config_hyperparameters import get_hp_config from train_model import make, training_loop from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... -from cli_parser import parse_args, validate_arguments +from cli_parser_utils import parse_args, validate_arguments def model_pipeline(config = None, project = None): From ae4bbe4255e34ef8a9cddfbe6b5420e25906c333 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:52:55 +0200 Subject: [PATCH 025/136] import sys --- common_utils/cli_parser_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index 18e22ff9..4723eccb 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -1,3 +1,4 @@ +import sys import argparse def parse_args(): From 8ab6d8139a0e9756c9816c325ffe2aca5d8e94d7 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 04:59:07 +0200 Subject: [PATCH 026/136] removed comments... --- models/purple_alien/main.py | 66 ------------------------------------- 1 file changed, 66 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 680aa4a5..faedcb36 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -57,72 +57,6 @@ def model_pipeline(config = None, project = None): -# --------------------------- - -#import argparse -# -#def parse_args(): -# parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') -# -# parser.add_argument('-r', '--run_type', -# choices=['calibration', 'testing', 'forecasting'], -# type=str, -# default='calibration', -# help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' -# 'Note: If --sweep is True, --run_type must be calibration.') -# -# parser.add_argument('-s', '--sweep', -# choices=[True, False], -# type=bool, -# default=False, -# help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' -# 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True automatically.') -# -# parser.add_argument('-t', '--train', -# choices=[True, False], -# type=bool, -# default=False, -# help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' -# 'Note: If --sweep is True, --train will be set to True automatically.') -# -# parser.add_argument('-e', '--evaluate', -# choices=[True, False], -# type=bool, -# default=False, -# help='Flag to indicate if the model should be evaluated. ' -# 'Note: If --sweep is True, --evaluate will be set to True automatically.' -# 'Cannot be used with --run_type forecasting.') -# -# return parser.parse_args() -# -# -#def validate_arguments(args): -# if args.sweep: -# if args.run_type != 'calibration': -# print("Sweep runs must have run_type set to 'calibration'. Exiting.") -# print("To fix: Use --run_type calibration when --sweep True.") -# -# sys.exit(1) -# args.train = True -# args.evaluate = True -# -# if args.run_type in ['testing', 'forecasting'] and args.sweep: -# print("Sweep cannot be performed with testing or forecasting run types. Exiting.") -# print("To fix: Use --sweep False or set --run_type to 'calibration'.") -# sys.exit(1) -# -# if args.run_type == 'forecasting' and args.evaluate: -# print("Forecasting runs cannot evaluate. Exiting.") -# print("To fix: Use --evaluate False when --run_type is 'forecasting'.") -# sys.exit(1) -# -# if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: -# print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") -# print("To fix: Use --train True and/or --evaluate True.") -# sys.exit(1) -# -# --------------------------- - if __name__ == "__main__": From 78ad92c76b7570465b4c30dc68aa3bc32fc4a3ea Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:11:34 +0200 Subject: [PATCH 027/136] extended logic --- models/purple_alien/main.py | 49 +++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index faedcb36..49481628 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -75,7 +75,7 @@ def model_pipeline(config = None, project = None): #print(f'Run type: {run_type}\n') - + # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags if args.sweep: print('Running sweep...') @@ -90,37 +90,38 @@ def model_pipeline(config = None, project = None): start_t = time.time() wandb.agent(sweep_id, model_pipeline) - - else: - - print('Train one model and save it as an artifact...') - # Extract run_type from parsed arguments + + else: + print('Running single model operation...') run_type = args.run_type - print(f'Run type: {run_type}\n') - - - project = f"purple_alien_{run_type}" # check naming convention - + project = f"purple_alien_{run_type}" hyperparameters = get_hp_config() - - hyperparameters['run_type'] = run_type + hyperparameters['run_type'] = run_type hyperparameters['sweep'] = False + + if args.train: + print(f"Training one model for run type: {run_type} and saving it as an artifact...") + start_t = time.time() + model = model_pipeline(config = hyperparameters, project = project) + PATH_ARTIFACTS = setup_artifacts_paths(PATH) - start_t = time.time() - - model = model_pipeline(config = hyperparameters, project = project) - - PATH_ARTIFACTS = setup_artifacts_paths(PATH) + # create the artifacts folder if it does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) - # create the artifacts folder if it does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) + # save the model + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") + torch.save(model, PATH_MODEL_ARTIFACT) - # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") - torch.save(model, PATH_MODEL_ARTIFACT) + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + if args.evaluate: + print(f"Evaluating model for run type: {run_type}...") + print('not implemented yet...') # you need to implement this part. + + #model = torch.load(PATH_MODEL_ARTIFACT) + #model.eval() + #get_posterior(model, views_vol, config, device) end_t = time.time() minutes = (end_t - start_t)/60 From 02cae7c3b4536251aad88c77646f8f6404e44f34 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:23:19 +0200 Subject: [PATCH 028/136] sweep right now? --- models/purple_alien/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 49481628..c81ca7d4 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -76,7 +76,7 @@ def model_pipeline(config = None, project = None): # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags - if args.sweep: + if args.sweep == True: print('Running sweep...') @@ -92,7 +92,7 @@ def model_pipeline(config = None, project = None): wandb.agent(sweep_id, model_pipeline) - else: + elif args.sweep == False: print('Running single model operation...') run_type = args.run_type project = f"purple_alien_{run_type}" From 3a66151378d579923a4021157801870d718effbf Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:26:11 +0200 Subject: [PATCH 029/136] debug... --- models/purple_alien/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index c81ca7d4..4c1935bb 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -62,6 +62,7 @@ def model_pipeline(config = None, project = None): # new argpars solution. args = parse_args() + print(args) # validate arguments to ensure that only correct combinations of flags are set validate_arguments(args) From 8db0a55449c335b3abf3f037bdbe4898e7c9b854 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:39:05 +0200 Subject: [PATCH 030/136] now with action --- common_utils/cli_parser_utils.py | 48 +++++++++++++++----------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index 4723eccb..de3c24f4 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -2,6 +2,11 @@ import argparse def parse_args(): + + """ + CLI parser for model specific main.py scripts. + """ + parser = argparse.ArgumentParser(description='Run model pipeline with specified run type.') parser.add_argument('-r', '--run_type', @@ -9,55 +14,46 @@ def parse_args(): type=str, default='calibration', help='Choose the run type for the model: calibration, testing, or forecasting. Default is calibration. ' - 'Note: If --sweep is True, --run_type must be calibration.') + 'Note: If --sweep is flagged, --run_type must be calibration.') parser.add_argument('-s', '--sweep', - choices=[True, False], - type=bool, - default=False, - help='Choose whether to run the model pipeline as part of a sweep. Default is False. ' - 'Note: If --sweep is True, --run_type must be calibration, and both --train and --evaluate will be set to True automatically.') + action='store_true', + help='Set flag to run the model pipeline as part of a sweep. No explicit flag means no sweep.' + 'Note: If --sweep is flagged, --run_type must be calibration, and both the --train and --evaluate flag will be activated automatically.') parser.add_argument('-t', '--train', - choices=[True, False], - type=bool, - default=False, - help='Flag to indicate if a new model should be trained - if not, a model will be loaded from an artifact. ' - 'Note: If --sweep is True, --train will be set to True automatically.') + action='store_true', + help='Flag to indicate if a new model should be trained. ' + 'Note: If --sweep is flagged, --train will also automatically be flagged.') parser.add_argument('-e', '--evaluate', - choices=[True, False], - type=bool, - default=False, + action='store_true', help='Flag to indicate if the model should be evaluated. ' - 'Note: If --sweep is True, --evaluate will be set to True automatically.' + 'Note: If --sweep is specified, --evaluate will also automatically be flagged. ' 'Cannot be used with --run_type forecasting.') return parser.parse_args() - def validate_arguments(args): if args.sweep: if args.run_type != 'calibration': - print("Sweep runs must have run_type set to 'calibration'. Exiting.") - print("To fix: Use --run_type calibration when --sweep True.") - + print("Error: Sweep runs must have --run_type set to 'calibration'. Exiting.") + print("To fix: Use --run_type calibration when --sweep is flagged.") sys.exit(1) args.train = True args.evaluate = True if args.run_type in ['testing', 'forecasting'] and args.sweep: - print("Sweep cannot be performed with testing or forecasting run types. Exiting.") - print("To fix: Use --sweep False or set --run_type to 'calibration'.") + print("Error: Sweep cannot be performed with testing or forecasting run types. Exiting.") + print("To fix: Remove --sweep flag or set --run_type to 'calibration'.") sys.exit(1) if args.run_type == 'forecasting' and args.evaluate: - print("Forecasting runs cannot evaluate. Exiting.") - print("To fix: Use --evaluate False when --run_type is 'forecasting'.") + print("Error: Forecasting runs cannot evaluate. Exiting.") + print("To fix: Remove --evaluate flag when --run_type is 'forecasting'.") sys.exit(1) if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: - print(f"Run type is {args.run_type} but neither --train nor --evaluate flag is set. Exiting.") - print("To fix: Use --train True and/or --evaluate True.") + print(f"Error: Run type is {args.run_type} but neither --train nor --evaluate flag is set. Nothing to do... Exiting.") + print("To fix: Add --train and/or --evaluate flag.") sys.exit(1) - From a28dbd26fbfa7f7869c0358037fbbf806b2bc2a8 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:51:13 +0200 Subject: [PATCH 031/136] move start time --- models/purple_alien/main.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 4c1935bb..6727b718 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -56,8 +56,6 @@ def model_pipeline(config = None, project = None): return(model) - - if __name__ == "__main__": # new argpars solution. @@ -70,6 +68,7 @@ def model_pipeline(config = None, project = None): # wandb login wandb.login() + start_t = time.time() # can you even choose testing and forecasting here? #run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} #run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] @@ -89,7 +88,6 @@ def model_pipeline(config = None, project = None): sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - start_t = time.time() wandb.agent(sweep_id, model_pipeline) @@ -103,7 +101,6 @@ def model_pipeline(config = None, project = None): if args.train: print(f"Training one model for run type: {run_type} and saving it as an artifact...") - start_t = time.time() model = model_pipeline(config = hyperparameters, project = project) PATH_ARTIFACTS = setup_artifacts_paths(PATH) From f6f748c494435fb6a966f3931dda1e8ee0e4114a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:55:26 +0200 Subject: [PATCH 032/136] forecastin place holder --- models/purple_alien/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 6727b718..df5f29b2 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -120,6 +120,12 @@ def model_pipeline(config = None, project = None): #model = torch.load(PATH_MODEL_ARTIFACT) #model.eval() #get_posterior(model, views_vol, config, device) + + + # I guess you also need some kind of forecasting here... + if run_type == 'forecasting': + print('Forecasting...') + print('not implemented yet...') end_t = time.time() minutes = (end_t - start_t)/60 From a4f1be4dbd46fd5cb7ebd3c0691e16720a884118 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 24 May 2024 05:56:25 +0200 Subject: [PATCH 033/136] full sweeps test --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 40974fb8..92b7b854 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 600}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug + 'test_samples': { 'value' :128}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From 6ef160721cad42289d50d34f0484cfe8389642cf Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 28 May 2024 22:45:04 +0200 Subject: [PATCH 034/136] now with eval --- models/purple_alien/main.py | 83 +++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index df5f29b2..fe681ada 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -25,55 +25,65 @@ from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... from cli_parser_utils import parse_args, validate_arguments -def model_pipeline(config = None, project = None): +def model_pipeline(config = None, project = None, train = None, eval = None): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(device) + print(f"Using device: {device}") - # tell wandb to get started - with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep - - # for the monthly metrics + # Initialize WandB + with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep + + # Define "new" monthly metrics for WandB logging wandb.define_metric("monthly/out_sample_month") wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - - # access all HPs through wandb.config, so logging matches execution! + + # Access all HPs through wandb.config, so logging matches execution config = wandb.config + # Retrieve data (pertition) based on the configuration views_vol = get_data(config) - # make the model, data, and optimization problem - model, criterion, optimizer, scheduler = make(config, device) - - training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - print('Done training') + if config.sweep: # If we are running a sweep, always train and evaluate + model, criterion, optimizer, scheduler = make(config, device) + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') - if config.sweep: - get_posterior(model, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, + get_posterior(model, views_vol, config, device) print('Done testing') - else: - return(model) + if train: + model, criterion, optimizer, scheduler = make(config, device) + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') + return model + + if eval: + # Ensure the model path is correctly handled and the model exists + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{config.run_type}_model.pt") # Replace with the correct path handling + + if not os.path.exists(PATH_MODEL_ARTIFACT): + raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + + model = torch.load(PATH_MODEL_ARTIFACT) + #model.eval() # this is done in the get_posterior function + + get_posterior(model, views_vol, config, device) + print('Done testing') if __name__ == "__main__": # new argpars solution. args = parse_args() - print(args) + #print(args) - # validate arguments to ensure that only correct combinations of flags are set + # Validate the parsed arguments to ensure they conform to the required logic and combinations. validate_arguments(args) # wandb login wandb.login() start_t = time.time() - # can you even choose testing and forecasting here? - #run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} - #run_type = run_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] - #print(f'Run type: {run_type}\n') - # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags if args.sweep == True: @@ -98,17 +108,19 @@ def model_pipeline(config = None, project = None): hyperparameters = get_hp_config() hyperparameters['run_type'] = run_type hyperparameters['sweep'] = False - + + # setup the paths for the artifacts (but should you not timestamp the artifacts as well?) + PATH_ARTIFACTS = setup_artifacts_paths(PATH) + if args.train: print(f"Training one model for run type: {run_type} and saving it as an artifact...") - model = model_pipeline(config = hyperparameters, project = project) - PATH_ARTIFACTS = setup_artifacts_paths(PATH) + model = model_pipeline(config = hyperparameters, project = project, train=True) # create the artifacts folder if it does not exist os.makedirs(PATH_ARTIFACTS, exist_ok=True) # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION torch.save(model, PATH_MODEL_ARTIFACT) print(f"Model saved as: {PATH_MODEL_ARTIFACT}") @@ -116,10 +128,17 @@ def model_pipeline(config = None, project = None): if args.evaluate: print(f"Evaluating model for run type: {run_type}...") print('not implemented yet...') # you need to implement this part. - - #model = torch.load(PATH_MODEL_ARTIFACT) - #model.eval() - #get_posterior(model, views_vol, config, device) + + # get the artifact path + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION + + # load the model + model = torch.load(PATH_MODEL_ARTIFACT) + + #model.eval() # this is done in the get_posterior function + model_pipeline(config = hyperparameters, project = project, eval=True) + + print('Done testing') # I guess you also need some kind of forecasting here... From 7f8278fe8da7f5ac39fd722eb0ccd66ab5b778f5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 28 May 2024 23:59:22 +0200 Subject: [PATCH 035/136] no forcing of t or e for s --- common_utils/cli_parser_utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index de3c24f4..3d505584 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -19,7 +19,9 @@ def parse_args(): parser.add_argument('-s', '--sweep', action='store_true', help='Set flag to run the model pipeline as part of a sweep. No explicit flag means no sweep.' - 'Note: If --sweep is flagged, --run_type must be calibration, and both the --train and --evaluate flag will be activated automatically.') + 'Note: If --sweep is flagged, --run_type must be calibration') #, and both the --train and --evaluate flag will be activated automatically.') + + # well, perhaps not, since sweeps handle trianing and evaluation in a different way... parser.add_argument('-t', '--train', action='store_true', @@ -40,8 +42,8 @@ def validate_arguments(args): print("Error: Sweep runs must have --run_type set to 'calibration'. Exiting.") print("To fix: Use --run_type calibration when --sweep is flagged.") sys.exit(1) - args.train = True - args.evaluate = True + #args.train = True + #args.evaluate = True if args.run_type in ['testing', 'forecasting'] and args.sweep: print("Error: Sweep cannot be performed with testing or forecasting run types. Exiting.") From 77c1252fd96685713efb8c209ced27a023ea0b96 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 00:19:15 +0200 Subject: [PATCH 036/136] utils to find the last art --- common_utils/artifacts_utils.py | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 common_utils/artifacts_utils.py diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py new file mode 100644 index 00000000..7f3e334f --- /dev/null +++ b/common_utils/artifacts_utils.py @@ -0,0 +1,38 @@ +import os + +def get_latest_model_artifact(path, run_type): + """ + Retrieve the latest model artifact for a given run type based on the modification time. + + Args: + path (str): The model specifc directory path where artifacts are stored. + Where PATH_ARTIFACTS = setup_artifacts_paths(PATH) executed in the model specifc main.py script. + and PATH = Path(__file__) + + run_type (str): The type of run (e.g., calibration, testing, forecasting). + + Returns: + str: The path to the latest model artifact given the run type. + + Raises: + FileNotFoundError: If no model artifacts are found for the given run type. + """ + + # List all model files for the given specific run_type with the expected filename pattern + model_files = [f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and f.endswith('.pt')] + + if not model_files: + raise FileNotFoundError(f"No model artifacts found for run type '{run_type}' in path '{path}'") + + # Sort the files based on the timestamp embedded in the filename. With format %Y%m%d_%H%M%S For example, '20210831_123456.pt' + model_files.sort(reverse=True) + + #print statements for debugging + print(model_files) + print(model_files[0]) + + # Return the latest model file + return os.path.join(path, model_files[0]) + + + From 76bbd541e85df46a6f5860809da0813889f11772 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 00:21:44 +0200 Subject: [PATCH 037/136] time stamped arts in mp --- models/purple_alien/main.py | 79 ++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index fe681ada..e1449dae 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -3,6 +3,7 @@ import time import os import functools +from datetime import datetime import torch import torch.nn as nn @@ -24,9 +25,14 @@ from train_model import make, training_loop from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... from cli_parser_utils import parse_args, validate_arguments +from artifacts_utils import get_latest_model_artifact -def model_pipeline(config = None, project = None, train = None, eval = None): +def model_pipeline(config = None, project = None, train = None, eval = None, artifact_name = None): + # Define the path for the artifacts + PATH_ARTIFACTS = setup_artifacts_paths(PATH) + + # Set the device device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"Using device: {device}") @@ -37,12 +43,13 @@ def model_pipeline(config = None, project = None, train = None, eval = None): wandb.define_metric("monthly/out_sample_month") wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - # Access all HPs through wandb.config, so logging matches execution + # Update config from WandB initialization above config = wandb.config - # Retrieve data (pertition) based on the configuration + # Retrieve data (partition) based on the configuration views_vol = get_data(config) + # Handle the sweep runs if config.sweep: # If we are running a sweep, always train and evaluate model, criterion, optimizer, scheduler = make(config, device) training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) @@ -51,19 +58,52 @@ def model_pipeline(config = None, project = None, train = None, eval = None): get_posterior(model, views_vol, config, device) print('Done testing') + # Handle the single model runs: train and save the model as an artifact if train: + + # Create the model, criterion, optimizer and scheduler model, criterion, optimizer, scheduler = make(config, device) training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) print('Done training') - return model + # create the artifacts folder if it does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) + + # Define the path for the artifacts with a timestamp and a run type + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + model_filename = f"{config.run_type}_model_{timestamp}.pt" + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) + + # save the model + torch.save(model, PATH_MODEL_ARTIFACT) + + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + #return model # dont return anything, the model is saved as an artifact + + # Handle the single model runs: evaluate a trained model (artifact) if eval: - # Ensure the model path is correctly handled and the model exists - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{config.run_type}_model.pt") # Replace with the correct path handling + + # Determine the artifact path: + # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type + if artifact_name is not None: + + # Check if the artifact name has the correct file extension + if not artifact_name.endswith('.pt'): + artifact_name += '.pt' + + # Define the full (model specific) path for the artifact + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + else: + # Get the latest model artifact based on the run type and the (models specific) artifacts path + PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) + + # Check if the model artifact exists - if not, raise an error if not os.path.exists(PATH_MODEL_ARTIFACT): raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + + # load the model model = torch.load(PATH_MODEL_ARTIFACT) #model.eval() # this is done in the get_posterior function @@ -110,40 +150,49 @@ def model_pipeline(config = None, project = None, train = None, eval = None): hyperparameters['sweep'] = False # setup the paths for the artifacts (but should you not timestamp the artifacts as well?) - PATH_ARTIFACTS = setup_artifacts_paths(PATH) if args.train: print(f"Training one model for run type: {run_type} and saving it as an artifact...") model = model_pipeline(config = hyperparameters, project = project, train=True) # create the artifacts folder if it does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) + #os.makedirs(PATH_ARTIFACTS, exist_ok=True) # save the model - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION - torch.save(model, PATH_MODEL_ARTIFACT) + #PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION + #torch.save(model, PATH_MODEL_ARTIFACT) - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + #print(f"Model saved as: {PATH_MODEL_ARTIFACT}") if args.evaluate: print(f"Evaluating model for run type: {run_type}...") - print('not implemented yet...') # you need to implement this part. + + # alright, but then the argspars should be able to take in an artifact name as well and pass it to the model_pipeline function here. + + #print('not implemented yet...') # you need to implement this part. # get the artifact path - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION + #PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION # load the model - model = torch.load(PATH_MODEL_ARTIFACT) + #model = torch.load(PATH_MODEL_ARTIFACT) #model.eval() # this is done in the get_posterior function model_pipeline(config = hyperparameters, project = project, eval=True) - print('Done testing') + #print('Done testing') # I guess you also need some kind of forecasting here... if run_type == 'forecasting': print('Forecasting...') + + + # notes: + # should always be a trained artifact? + # should always de the last artifact? + + print('not implemented yet...') end_t = time.time() From f4aa7b4d319f9798066084643032691a5092d946 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 00:26:39 +0200 Subject: [PATCH 038/136] better print for debug --- common_utils/artifacts_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py index 7f3e334f..8b1a582f 100644 --- a/common_utils/artifacts_utils.py +++ b/common_utils/artifacts_utils.py @@ -28,8 +28,8 @@ def get_latest_model_artifact(path, run_type): model_files.sort(reverse=True) #print statements for debugging - print(model_files) - print(model_files[0]) + print(f"artifacts availible: {model_files}") + print(f"artifact used: {model_files[0]}") # Return the latest model file return os.path.join(path, model_files[0]) From 523a43f0eebc3aaa93dd14af14816c01b0093e23 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 00:47:46 +0200 Subject: [PATCH 039/136] artifact name can now be passed --- common_utils/cli_parser_utils.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index 3d505584..b49613f6 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -19,10 +19,8 @@ def parse_args(): parser.add_argument('-s', '--sweep', action='store_true', help='Set flag to run the model pipeline as part of a sweep. No explicit flag means no sweep.' - 'Note: If --sweep is flagged, --run_type must be calibration') #, and both the --train and --evaluate flag will be activated automatically.') + 'Note: If --sweep is flagged, --run_type must be calibration, and both training and evaluation is automatically implied.') - # well, perhaps not, since sweeps handle trianing and evaluation in a different way... - parser.add_argument('-t', '--train', action='store_true', help='Flag to indicate if a new model should be trained. ' @@ -34,6 +32,14 @@ def parse_args(): 'Note: If --sweep is specified, --evaluate will also automatically be flagged. ' 'Cannot be used with --run_type forecasting.') + parser.add_argument('-a', '--artifact_name', + type=str, + help='Specify the name of the model artifact to be used for evaluation. ' + 'The file extension will be added in the main and fit with the specific model algorithm.' + 'The artifact name should be in the format: _model_.pt.' + 'where is calibration, testing, or forecasting, and is in the format %Y%m%d_%H%M%S.' + 'If not provided, the latest artifact will be used by default.') + return parser.parse_args() def validate_arguments(args): @@ -42,8 +48,6 @@ def validate_arguments(args): print("Error: Sweep runs must have --run_type set to 'calibration'. Exiting.") print("To fix: Use --run_type calibration when --sweep is flagged.") sys.exit(1) - #args.train = True - #args.evaluate = True if args.run_type in ['testing', 'forecasting'] and args.sweep: print("Error: Sweep cannot be performed with testing or forecasting run types. Exiting.") From 7ea89b79b63ba7e2b53a238ed7a157930e663fdc Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 00:58:23 +0200 Subject: [PATCH 040/136] can now pass art name --- models/purple_alien/main.py | 39 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index e1449dae..1e540b40 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -146,42 +146,25 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art run_type = args.run_type project = f"purple_alien_{run_type}" hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type + hyperparameters['run_type'] = run_type # this is also how the forecast if statement is informed below hyperparameters['sweep'] = False - # setup the paths for the artifacts (but should you not timestamp the artifacts as well?) - + # if train is flagged, train the model and save it as an artifact if args.train: print(f"Training one model for run type: {run_type} and saving it as an artifact...") - model = model_pipeline(config = hyperparameters, project = project, train=True) - - # create the artifacts folder if it does not exist - #os.makedirs(PATH_ARTIFACTS, exist_ok=True) - - # save the model - #PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION - #torch.save(model, PATH_MODEL_ARTIFACT) - - #print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + model_pipeline(config = hyperparameters, project = project, train=True) + # if evaluate is flagged, evaluate the model if args.evaluate: print(f"Evaluating model for run type: {run_type}...") - # alright, but then the argspars should be able to take in an artifact name as well and pass it to the model_pipeline function here. - - #print('not implemented yet...') # you need to implement this part. - - # get the artifact path - #PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{run_type}_model.pt") # THIS NEEDS TO BE CHANGED TO A TIMESTAMPED VERSION - - # load the model - #model = torch.load(PATH_MODEL_ARTIFACT) - - #model.eval() # this is done in the get_posterior function - model_pipeline(config = hyperparameters, project = project, eval=True) - - #print('Done testing') - + # if an artifact name is provided, use it. + if args.artifact_name is not None: + model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) + + # Otherwise, get the default - I.e. latest model artifact give the specific run type + else: + model_pipeline(config = hyperparameters, project = project, eval=True) # I guess you also need some kind of forecasting here... if run_type == 'forecasting': From f8c425fc8fd7087ee95422f57acc1f5422313d2e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:05:33 +0200 Subject: [PATCH 041/136] notes on stepshifted models --- common_utils/artifacts_utils.py | 8 +++++++- common_utils/cli_parser_utils.py | 11 +++++++++++ models/purple_alien/main.py | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py index 8b1a582f..55d98174 100644 --- a/common_utils/artifacts_utils.py +++ b/common_utils/artifacts_utils.py @@ -34,5 +34,11 @@ def get_latest_model_artifact(path, run_type): # Return the latest model file return os.path.join(path, model_files[0]) - + # notes on stepshifted models: + # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # It is not a big issue, but it is something to consider os we don't do something headless. + # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. + # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. + # Alternatively, we could store the model artifacts in a subfolder for each stepshifted model. This would make it easier to handle the artifacts, but it would also make it harder to retrieve the latest artifact for a given run type. + # Lastly, the solution Xiaolong is working on might allow us the store multiple models (steps) in one artifact, which would make this whole discussion obsolete and be the best solution. diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index b49613f6..e2b750e1 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -63,3 +63,14 @@ def validate_arguments(args): print(f"Error: Run type is {args.run_type} but neither --train nor --evaluate flag is set. Nothing to do... Exiting.") print("To fix: Add --train and/or --evaluate flag.") sys.exit(1) + + + # notes on stepshifted models: + # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # It is not a big issue, but it is something to consider os we don't do something headless. + # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. + # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. + # Alternatively, we could store the model artifacts in a subfolder for each stepshifted model. This would make it easier to handle the artifacts, but it would also make it harder to retrieve the latest artifact for a given run type. + # Lastly, the solution Xiaolong is working on might allow us the store multiple models (steps) in one artifact, which would make this whole discussion obsolete and be the best solution. + + diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 1e540b40..36857157 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -185,3 +185,12 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art + # notes on stepshifted models: + # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # It is not a big issue, but it is something to consider os we don't do something headless. + # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. + # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. + # Alternatively, we could store the model artifacts in a subfolder for each stepshifted model. This would make it easier to handle the artifacts, but it would also make it harder to retrieve the latest artifact for a given run type. + # Lastly, the solution Xiaolong is working on might allow us the store multiple models (steps) in one artifact, which would make this whole discussion obsolete and be the best solution. + + From d5798704752124e5d41061667d6f8b58cd60d5d2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:06:29 +0200 Subject: [PATCH 042/136] fix typo... --- common_utils/artifacts_utils.py | 2 +- common_utils/cli_parser_utils.py | 2 +- models/purple_alien/main.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py index 55d98174..a540da18 100644 --- a/common_utils/artifacts_utils.py +++ b/common_utils/artifacts_utils.py @@ -35,7 +35,7 @@ def get_latest_model_artifact(path, run_type): return os.path.join(path, model_files[0]) # notes on stepshifted models: - # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. # It is not a big issue, but it is something to consider os we don't do something headless. # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index e2b750e1..d27597bb 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -66,7 +66,7 @@ def validate_arguments(args): # notes on stepshifted models: - # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. # It is not a big issue, but it is something to consider os we don't do something headless. # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 36857157..55747101 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -186,7 +186,7 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # notes on stepshifted models: - # There will be some thinnking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. + # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. # It is not a big issue, but it is something to consider os we don't do something headless. # A possible format could be: _model_s_.pt example: calibration_model_s00_20210831_123456.pt, calibration_model_s01_20210831_123456.pt, etc. # And the rest of the code maded in a way to handle this naming convention without any issues. Could be a simple fix. From 9b8a4a1b083e009770a262465665023cd8d9e858 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:15:21 +0200 Subject: [PATCH 043/136] debug prints --- models/purple_alien/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 55747101..787e21d6 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -86,6 +86,9 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # Determine the artifact path: # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type if artifact_name is not None: + + # pritn statement for debugging + print(f"Using (non default) artifact: {artifact_name}") # Check if the artifact name has the correct file extension if not artifact_name.endswith('.pt'): @@ -95,6 +98,9 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) else: + # print statement for debugging + print(f"Using lastest (default) run type ({config.run_type}) specific artifact") + # Get the latest model artifact based on the run type and the (models specific) artifacts path PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) From 01b652eb4286ba30db2ade367e94847e40276ed5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:37:36 +0200 Subject: [PATCH 044/136] if/if not sweep --- .../src/offline_evaluation/evaluate_sweep.py | 236 +++++++++--------- 1 file changed, 123 insertions(+), 113 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index c3c036c1..b47eb27a 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -118,6 +118,8 @@ def get_posterior(model, views_vol, config, device): posterior_list, posterior_list_class, out_of_sample_vol, test_tensor = sample_posterior(model, views_vol, config, device) # YOU ARE MISSING SOMETHING ABOUT FEATURES HERE WHICH IS WHY YOU REPORTED AP ON WandB IS BIASED DOWNWARDS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!RYRYRYRYERYERYR + # need to check you "offline" evaluation script which is correctlly implemented before you use this function for forecasting. + # Get mean and std mean_array = np.array(posterior_list).mean(axis = 0) # get mean for each month! std_array = np.array(posterior_list).std(axis = 0) @@ -158,130 +160,138 @@ def get_posterior(model, views_vol, config, device): auc_list.append(auc) brier_list.append(brier) -# if not config.sweep: -# -# # DUMP 2 -# dump_location = '/home/projects/ku_00017/data/generated/conflictNet/' # should be in config -# -# posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} -# -# metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, -# 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} -# -# with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: -# pickle.dump(posterior_dict, file) -# -# with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: -# pickle.dump(metric_dict, file) -# -# with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy -# pickle.dump(test_tensor.cpu().numpy(), file) -# -# print('Posterior dict, metric dict and test vol pickled and dumped!') - -# else: - print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - - # ------------------------------------------------------------------------------------ - wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) - wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) - wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) - wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) - - - -# SHOULD BE MAIN SCRIPT ------------------------------------------------------------------ - - - -def model_pipeline(config = None, project = None): - - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(device) - - # tell wandb to get started - with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep - - wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - - # access all HPs through wandb.config, so logging matches execution! - config = wandb.config - - views_vol = get_data(config) + if not config.sweep: + + # fimbulthul dump location + dump_location = config.path_generated_data #'/home/simmaa/HydraNet_001/data/generated/' # should be in config <--------------------------------------------------------------------------------------------------- - # make the model, data, and optimization problem - unet, criterion, optimizer, scheduler = make(config, device) - training_loop(config, unet, criterion, optimizer, scheduler, views_vol, device) - print('Done training') + posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - get_posterior(unet, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, - print('Done testing') + metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, + 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - if config.sweep == False: # if it is not a sweep, return the model for pickling (not pickled right now...), pth - return(unet) + with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + pickle.dump(posterior_dict, file) + with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + pickle.dump(metric_dict, file) -if __name__ == "__main__": + with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy + pickle.dump(test_tensor.cpu().numpy(), file) - wandb.login() + print('Posterior dict, metric dict and test vol pickled and dumped!') - time_steps_dict = {'a':12, - 'b':24, - 'c':36, - 'd':48,} - - time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] - - - run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} - run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] - print(f'Run type: {run_type}\n') - - do_sweep = input(f'a) Do sweep \nb) Do one run and pickle results \n') - - if do_sweep == 'a': - - print('Doing a sweep!') - - project = f"RUNET_VIEWSER_{time_steps}_{run_type}_experiments_016_sbnsos" # 4 is without h freeze... See if you have all the outputs now??? - - sweep_config = get_swep_config() - sweep_config['parameters']['time_steps'] = {'value' : time_steps} - sweep_config['parameters']['run_type'] = {'value' : run_type} - sweep_config['parameters']['sweep'] = {'value' : True} - - sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - - #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - #print(device) - - start_t = time.time() - wandb.agent(sweep_id, model_pipeline) - - elif do_sweep == 'b': - - print(f'One run and pickle!') - - project = f"RUNET_VIEWS_{time_steps}_{run_type}_pickled_sbnsos" - - hyperparameters = get_hp_config() - hyperparameters['time_steps'] = time_steps - hyperparameters['run_type'] = run_type - hyperparameters['sweep'] = False - - print(f"using: {hyperparameters['model']}") +# wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) +# wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) +# wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) +# wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) +# - #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - #print(device) + else: + print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - start_t = time.time() + # ------------------------------------------------------------------------------------ + wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) + wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) + wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) + wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) - unet = model_pipeline(config = hyperparameters, project = project) - end_t = time.time() - minutes = (end_t - start_t)/60 - print(f'Done. Runtime: {minutes:.3f} minutes') +# SHOULD BE MAIN SCRIPT ------------------------------------------------------------------ +# +# +# def model_pipeline(config = None, project = None): +# +# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +# print(device) +# +# # tell wandb to get started +# with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep +# +# wandb.define_metric("monthly/out_sample_month") +# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") +# +# # access all HPs through wandb.config, so logging matches execution! +# config = wandb.config +# +# views_vol = get_data(config) +# +# # make the model, data, and optimization problem +# unet, criterion, optimizer, scheduler = make(config, device) +# +# training_loop(config, unet, criterion, optimizer, scheduler, views_vol, device) +# print('Done training') +# +# get_posterior(unet, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, +# print('Done testing') +# +# if config.sweep == False: # if it is not a sweep, return the model for pickling (not pickled right now...), pth +# return(unet) +# +# +# if __name__ == "__main__": +# +# wandb.login() +# +# time_steps_dict = {'a':12, +# 'b':24, +# 'c':36, +# 'd':48,} +# +# time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] +# +# +# run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} +# run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] +# print(f'Run type: {run_type}\n') +# +# do_sweep = input(f'a) Do sweep \nb) Do one run and pickle results \n') +# +# if do_sweep == 'a': +# +# print('Doing a sweep!') +# +# project = f"RUNET_VIEWSER_{time_steps}_{run_type}_experiments_016_sbnsos" # 4 is without h freeze... See if you have all the outputs now??? +# +# sweep_config = get_swep_config() +# sweep_config['parameters']['time_steps'] = {'value' : time_steps} +# sweep_config['parameters']['run_type'] = {'value' : run_type} +# sweep_config['parameters']['sweep'] = {'value' : True} +# +# sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name +# +# #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +# #print(device) +# +# start_t = time.time() +# wandb.agent(sweep_id, model_pipeline) +# +# elif do_sweep == 'b': +# +# print(f'One run and pickle!') +# +# project = f"RUNET_VIEWS_{time_steps}_{run_type}_pickled_sbnsos" +# +# hyperparameters = get_hp_config() +# hyperparameters['time_steps'] = time_steps +# hyperparameters['run_type'] = run_type +# hyperparameters['sweep'] = False +# +# print(f"using: {hyperparameters['model']}") +# +# #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +# #print(device) +# +# start_t = time.time() +# +# unet = model_pipeline(config = hyperparameters, project = project) +# +# end_t = time.time() +# minutes = (end_t - start_t)/60 +# print(f'Done. Runtime: {minutes:.3f} minutes') +# +# +# \ No newline at end of file From 7ab48e6403be40c44eef329eed03d66157e06623 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:38:16 +0200 Subject: [PATCH 045/136] test sweep --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 92b7b854..40974fb8 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 600}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' :128}, # 128 for actual testing, 10 for debug + 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From 6077bea67d8368f3ebe689f53851e1a608ab331c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:53:16 +0200 Subject: [PATCH 046/136] correct path now? --- .../src/offline_evaluation/evaluate_sweep.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index b47eb27a..9b5b817f 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pickle import time @@ -21,9 +23,10 @@ PATH = Path(__file__) sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths +from set_path import setup_project_paths, setup_data_paths setup_project_paths(PATH) + from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from config_sweep import get_swep_config from config_hyperparameters import get_hp_config @@ -162,31 +165,34 @@ def get_posterior(model, views_vol, config, device): if not config.sweep: - # fimbulthul dump location - dump_location = config.path_generated_data #'/home/simmaa/HydraNet_001/data/generated/' # should be in config <--------------------------------------------------------------------------------------------------- + _ , _, PATH_GENERATED = setup_data_paths(PATH) + + # if the path does not exist, create it + if not os.path.exists(PATH_GENERATED): + os.makedirs(PATH_GENERATED) + + # print for debugging + print(f'PATH to generated data: {PATH_GENERATED}') + # pickle the posterior dict, metric dict, and test vol + # Should be time_steps and run_type in the name.... posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(metric_dict, file) - with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy + with open(f'{PATH_GENERATED}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy pickle.dump(test_tensor.cpu().numpy(), file) print('Posterior dict, metric dict and test vol pickled and dumped!') -# wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) -# wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) -# wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) -# wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) -# else: print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') From 1c478c9debc7c78451af410d810706a2e5d7c6f5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 01:57:25 +0200 Subject: [PATCH 047/136] fixed loop? --- .../src/offline_evaluation/evaluate_sweep.py | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index 9b5b817f..3b055a3c 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -163,45 +163,46 @@ def get_posterior(model, views_vol, config, device): auc_list.append(auc) brier_list.append(brier) - if not config.sweep: + + if not config.sweep: - _ , _, PATH_GENERATED = setup_data_paths(PATH) + _ , _, PATH_GENERATED = setup_data_paths(PATH) + + # if the path does not exist, create it + if not os.path.exists(PATH_GENERATED): + os.makedirs(PATH_GENERATED) - # if the path does not exist, create it - if not os.path.exists(PATH_GENERATED): - os.makedirs(PATH_GENERATED) + # print for debugging + print(f'PATH to generated data: {PATH_GENERATED}') - # print for debugging - print(f'PATH to generated data: {PATH_GENERATED}') + # pickle the posterior dict, metric dict, and test vol + # Should be time_steps and run_type in the name.... - # pickle the posterior dict, metric dict, and test vol - # Should be time_steps and run_type in the name.... + posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} + metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, + 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, - 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} + with open(f'{PATH_GENERATED}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + pickle.dump(posterior_dict, file) - with open(f'{PATH_GENERATED}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(posterior_dict, file) + with open(f'{PATH_GENERATED}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + pickle.dump(metric_dict, file) - with open(f'{PATH_GENERATED}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(metric_dict, file) + with open(f'{PATH_GENERATED}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy + pickle.dump(test_tensor.cpu().numpy(), file) - with open(f'{PATH_GENERATED}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy - pickle.dump(test_tensor.cpu().numpy(), file) + print('Posterior dict, metric dict and test vol pickled and dumped!') - print('Posterior dict, metric dict and test vol pickled and dumped!') + else: + print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - else: - print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - # ------------------------------------------------------------------------------------ - wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) - wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) - wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) - wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) + wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) + wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) + wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) + wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) From f1f9938c7442e29b6a3e05e6ef260b506d0772f2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:08:28 +0200 Subject: [PATCH 048/136] added / --- .../purple_alien/src/offline_evaluation/evaluate_sweep.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py index 3b055a3c..33c541ce 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_sweep.py @@ -183,13 +183,13 @@ def get_posterior(model, views_vol, config, device): metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - with open(f'{PATH_GENERATED}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{PATH_GENERATED}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: pickle.dump(metric_dict, file) - with open(f'{PATH_GENERATED}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy + with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy pickle.dump(test_tensor.cpu().numpy(), file) print('Posterior dict, metric dict and test vol pickled and dumped!') From bf109f8ddfb23523d0fecf60017d8458fb60b170 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:14:30 +0200 Subject: [PATCH 049/136] more generel for single and sweep --- .../{evaluate_sweep.py => evaluation.py} | 98 ------------------- 1 file changed, 98 deletions(-) rename models/purple_alien/src/offline_evaluation/{evaluate_sweep.py => evaluation.py} (71%) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py b/models/purple_alien/src/offline_evaluation/evaluation.py similarity index 71% rename from models/purple_alien/src/offline_evaluation/evaluate_sweep.py rename to models/purple_alien/src/offline_evaluation/evaluation.py index 33c541ce..8959107f 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_sweep.py +++ b/models/purple_alien/src/offline_evaluation/evaluation.py @@ -204,101 +204,3 @@ def get_posterior(model, views_vol, config, device): wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) - - -# SHOULD BE MAIN SCRIPT ------------------------------------------------------------------ - -# -# -# def model_pipeline(config = None, project = None): -# -# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# print(device) -# -# # tell wandb to get started -# with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep -# -# wandb.define_metric("monthly/out_sample_month") -# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") -# -# # access all HPs through wandb.config, so logging matches execution! -# config = wandb.config -# -# views_vol = get_data(config) -# -# # make the model, data, and optimization problem -# unet, criterion, optimizer, scheduler = make(config, device) -# -# training_loop(config, unet, criterion, optimizer, scheduler, views_vol, device) -# print('Done training') -# -# get_posterior(unet, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, -# print('Done testing') -# -# if config.sweep == False: # if it is not a sweep, return the model for pickling (not pickled right now...), pth -# return(unet) -# -# -# if __name__ == "__main__": -# -# wandb.login() -# -# time_steps_dict = {'a':12, -# 'b':24, -# 'c':36, -# 'd':48,} -# -# time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] -# -# -# run_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} -# run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] -# print(f'Run type: {run_type}\n') -# -# do_sweep = input(f'a) Do sweep \nb) Do one run and pickle results \n') -# -# if do_sweep == 'a': -# -# print('Doing a sweep!') -# -# project = f"RUNET_VIEWSER_{time_steps}_{run_type}_experiments_016_sbnsos" # 4 is without h freeze... See if you have all the outputs now??? -# -# sweep_config = get_swep_config() -# sweep_config['parameters']['time_steps'] = {'value' : time_steps} -# sweep_config['parameters']['run_type'] = {'value' : run_type} -# sweep_config['parameters']['sweep'] = {'value' : True} -# -# sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name -# -# #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# #print(device) -# -# start_t = time.time() -# wandb.agent(sweep_id, model_pipeline) -# -# elif do_sweep == 'b': -# -# print(f'One run and pickle!') -# -# project = f"RUNET_VIEWS_{time_steps}_{run_type}_pickled_sbnsos" -# -# hyperparameters = get_hp_config() -# hyperparameters['time_steps'] = time_steps -# hyperparameters['run_type'] = run_type -# hyperparameters['sweep'] = False -# -# print(f"using: {hyperparameters['model']}") -# -# #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# #print(device) -# -# start_t = time.time() -# -# unet = model_pipeline(config = hyperparameters, project = project) -# -# end_t = time.time() -# minutes = (end_t - start_t)/60 -# print(f'Done. Runtime: {minutes:.3f} minutes') -# -# -# \ No newline at end of file From 22ade30edb718b93f7d6f0e6ef9960ea23bfcbc5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:14:53 +0200 Subject: [PATCH 050/136] use evalution.py --- models/purple_alien/main.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 787e21d6..b632f7b5 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -23,7 +23,8 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop -from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... +# from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... +from evaluation import get_posterior from cli_parser_utils import parse_args, validate_arguments from artifacts_utils import get_latest_model_artifact @@ -174,7 +175,16 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # I guess you also need some kind of forecasting here... if run_type == 'forecasting': - print('Forecasting...') + print('True forecasting ->->->->') + + # if an artifact name is provided, use it. + if args.artifact_name is not None: + model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) + + # Otherwise, get the default - I.e. latest model artifact give the specific run type + else: + model_pipeline(config = hyperparameters, project = project) + # notes: From 357df2b955fd7a41b2252f658897a21d812c3d93 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:37:16 +0200 Subject: [PATCH 051/136] timedtapm to pickle --- models/purple_alien/main.py | 9 +++++++++ models/purple_alien/src/offline_evaluation/evaluation.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index b632f7b5..07bb9f71 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -113,6 +113,15 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # load the model model = torch.load(PATH_MODEL_ARTIFACT) #model.eval() # this is done in the get_posterior function + + # Get the excact model date_time stamp for the pkl files made in the get_posterior from evaluation.py + model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] + + # debug print statement + print(f"model_time_stamp: {model_time_stamp}") + + # save to config for logging and concisness + config.model_time_stamp = model_time_stamp get_posterior(model, views_vol, config, device) print('Done testing') diff --git a/models/purple_alien/src/offline_evaluation/evaluation.py b/models/purple_alien/src/offline_evaluation/evaluation.py index 8959107f..db6afe68 100644 --- a/models/purple_alien/src/offline_evaluation/evaluation.py +++ b/models/purple_alien/src/offline_evaluation/evaluation.py @@ -183,13 +183,13 @@ def get_posterior(model, views_vol, config, device): metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type_}{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(metric_dict, file) - with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy + with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy pickle.dump(test_tensor.cpu().numpy(), file) print('Posterior dict, metric dict and test vol pickled and dumped!') From 17c57bbca158af35399fe3e86e2f0be477334501 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:38:21 +0200 Subject: [PATCH 052/136] fixed? --- models/purple_alien/src/offline_evaluation/evaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluation.py b/models/purple_alien/src/offline_evaluation/evaluation.py index db6afe68..fd8dd52a 100644 --- a/models/purple_alien/src/offline_evaluation/evaluation.py +++ b/models/purple_alien/src/offline_evaluation/evaluation.py @@ -186,7 +186,7 @@ def get_posterior(model, views_vol, config, device): with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type_}{config.model_time_stamp}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(metric_dict, file) with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy From 3b3bc95aa37ad6b7e2e926f4dc362693b402111c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 02:40:34 +0200 Subject: [PATCH 053/136] full run single model --- models/purple_alien/configs/config_hyperparameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index a67683a1..d2c7ec90 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 10, # 600 for actual trainnig, 10 for debug + 'samples': 600, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, @@ -24,7 +24,7 @@ def get_hp_config(): 'loss_reg': 'b', 'loss_reg_a' : 258, 'loss_reg_c' : 0.001, # 0.05 works... - 'test_samples': 10, # 128 for actual testing, 10 for debug + 'test_samples': 128, # 128 for actual testing, 10 for debug 'np_seed' : 4, 'torch_seed' : 4, 'window_dim' : 32, From e3ec2abdc88b9f98b98252e15f5fcfba2203784f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 16:29:16 +0200 Subject: [PATCH 054/136] note on one script --- models/purple_alien/src/offline_evaluation/evaluation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/purple_alien/src/offline_evaluation/evaluation.py b/models/purple_alien/src/offline_evaluation/evaluation.py index fd8dd52a..6206f7e1 100644 --- a/models/purple_alien/src/offline_evaluation/evaluation.py +++ b/models/purple_alien/src/offline_evaluation/evaluation.py @@ -204,3 +204,6 @@ def get_posterior(model, views_vol, config, device): wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) +# note: +# Going with the argparser, there is less of a clear reason to have to separate .py files for evaluation sweeps and single models. I think. Let me know if you disagree. +# naturally its a question of generalization and reusability, and i could see I had a lot of copy paste code between the two scripts. \ No newline at end of file From 51579ae9535522bcae4770544e074ff71386a766 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 16:34:08 +0200 Subject: [PATCH 055/136] some comments --- models/purple_alien/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 07bb9f71..be1f8444 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -62,6 +62,8 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # Handle the single model runs: train and save the model as an artifact if train: + # All wandb logging is done in the training loop. + # Create the model, criterion, optimizer and scheduler model, criterion, optimizer, scheduler = make(config, device) training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) @@ -78,6 +80,8 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # save the model torch.save(model, PATH_MODEL_ARTIFACT) + # Currently the artifacts are only sotred locally. Putting them on WandB is a good idea, but I need to understand thier model storage better first. + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") #return model # dont return anything, the model is saved as an artifact From 3831e497840600c39257a4ab56d385f97dae3956 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 16:36:02 +0200 Subject: [PATCH 056/136] new (old) name --- models/purple_alien/main.py | 2 +- .../src/offline_evaluation/evaluate_model.py | 121 ++++------ .../src/offline_evaluation/evaluation.py | 209 ------------------ 3 files changed, 38 insertions(+), 294 deletions(-) delete mode 100644 models/purple_alien/src/offline_evaluation/evaluation.py diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index be1f8444..1d293578 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -24,7 +24,7 @@ from config_hyperparameters import get_hp_config from train_model import make, training_loop # from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... -from evaluation import get_posterior +from evaluate_model import get_posterior from cli_parser_utils import parse_args, validate_arguments from artifacts_utils import get_latest_model_artifact diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 3ee463cd..6206f7e1 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -1,6 +1,9 @@ +import os + import numpy as np import pickle import time +import functools import torch import torch.nn as nn @@ -20,13 +23,12 @@ PATH = Path(__file__) sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths +from set_path import setup_project_paths, setup_data_paths setup_project_paths(PATH) -from config_hyperparameters import get_hp_config from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data -#from config_sweep import get_swep_config +from config_sweep import get_swep_config from config_hyperparameters import get_hp_config @@ -46,7 +48,7 @@ def test(model, test_tensor, time_steps, config, device): # should be called eva pred_np_list = [] pred_class_np_list = [] - h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # should infere the dim... + h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... seq_len = test_tensor.shape[1] # og nu køre eden bare helt til roden print(f'\t\t\t\t sequence length: {seq_len}', end= '\r') @@ -73,7 +75,6 @@ def test(model, test_tensor, time_steps, config, device): # should be called eva return pred_np_list, pred_class_np_list - def sample_posterior(model, views_vol, config, device): """ @@ -111,7 +112,6 @@ def sample_posterior(model, views_vol, config, device): return posterior_list, posterior_list_class, out_of_sample_vol, test_tensor - def get_posterior(model, views_vol, config, device): """ @@ -121,6 +121,8 @@ def get_posterior(model, views_vol, config, device): posterior_list, posterior_list_class, out_of_sample_vol, test_tensor = sample_posterior(model, views_vol, config, device) # YOU ARE MISSING SOMETHING ABOUT FEATURES HERE WHICH IS WHY YOU REPORTED AP ON WandB IS BIASED DOWNWARDS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!RYRYRYRYERYERYR + # need to check you "offline" evaluation script which is correctlly implemented before you use this function for forecasting. + # Get mean and std mean_array = np.array(posterior_list).mean(axis = 0) # get mean for each month! std_array = np.array(posterior_list).std(axis = 0) @@ -161,96 +163,47 @@ def get_posterior(model, views_vol, config, device): auc_list.append(auc) brier_list.append(brier) - # DUMP - - # computerome dump location - #dump_location = '/home/projects/ku_00017/data/generated/conflictNet/' # should be in config - - # fimbulthul dump location - dump_location = config.path_generated_data #'/home/simmaa/HydraNet_001/data/generated/' # should be in config <--------------------------------------------------------------------------------------------------- - - - posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - - metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, - 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - - with open(f'{dump_location}posterior_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(posterior_dict, file) - - with open(f'{dump_location}metric_dict_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: - pickle.dump(metric_dict, file) - - with open(f'{dump_location}test_vol_{config.time_steps}_{config.run_type}.pkl', 'wb') as file: # make it numpy - pickle.dump(test_tensor.cpu().numpy(), file) - - print('Posterior dict, metric dict and test vol pickled and dumped!') - - wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) - wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) - wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) - wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) + if not config.sweep: + + _ , _, PATH_GENERATED = setup_data_paths(PATH) -def model_pipeline(config = None, project = None): + # if the path does not exist, create it + if not os.path.exists(PATH_GENERATED): + os.makedirs(PATH_GENERATED) - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(device) + # print for debugging + print(f'PATH to generated data: {PATH_GENERATED}') - # tell wandb to get started - with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when runnig a sweep + # pickle the posterior dict, metric dict, and test vol + # Should be time_steps and run_type in the name.... - wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - # access all HPs through wandb.config, so logging matches execution! - config = wandb.config + metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, + 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - views_vol = get_data(config) + with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: + pickle.dump(posterior_dict, file) - # computerome artifacts path - #artifacts_path = f"/home/projects/ku_00017/people/simpol/scripts/conflictNet/artifacts" - - # fimbulthul artifacts path - artifacts_path = config.path_artifacts # f"/home/simmaa/HydraNet_001/artifacts" # should be in config <--------------------------------------------------------------------------------------------------- + with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}{config.model_time_stamp}.pkl', 'wb') as file: + pickle.dump(metric_dict, file) - model = torch.load(f"{artifacts_path}/calibration_model.pt") # you rpolly need configs for both train and test... + with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy + pickle.dump(test_tensor.cpu().numpy(), file) - get_posterior(model, views_vol, config, device) # actually since you give config now you do not need: time_steps, run_type, is_sweep, - print('Done testing') + print('Posterior dict, metric dict and test vol pickled and dumped!') - return(model) + else: + print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') -if __name__ == "__main__": - - wandb.login() - - time_steps_dict = {'a':12, - 'b':24, - 'c':36, - 'd':48,} - - time_steps = time_steps_dict[input('a) 12 months\nb) 24 months\nc) 36 months\nd) 48 months\nNote: 48 is the current VIEWS standard.\n')] - - run_type_dict = {'a' : 'calibration', 'b' : 'testing'} - run_type = run_type_dict[input("a) Calibration\nb) Testing\n")] - print(f'Run type: {run_type}\n') - - project = f"imp_new_structure_{run_type}" # temp. - - hyperparameters = get_hp_config() - - hyperparameters['time_steps'] = time_steps - hyperparameters['run_type'] = run_type - hyperparameters['sweep'] = False - - start_t = time.time() - - model = model_pipeline(config = hyperparameters, project = project) - - end_t = time.time() - minutes = (end_t - start_t)/60 - print(f'Done. Runtime: {minutes:.3f} minutes') + wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) + wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) + wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) + wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) +# note: +# Going with the argparser, there is less of a clear reason to have to separate .py files for evaluation sweeps and single models. I think. Let me know if you disagree. +# naturally its a question of generalization and reusability, and i could see I had a lot of copy paste code between the two scripts. \ No newline at end of file diff --git a/models/purple_alien/src/offline_evaluation/evaluation.py b/models/purple_alien/src/offline_evaluation/evaluation.py deleted file mode 100644 index 6206f7e1..00000000 --- a/models/purple_alien/src/offline_evaluation/evaluation.py +++ /dev/null @@ -1,209 +0,0 @@ -import os - -import numpy as np -import pickle -import time -import functools - -import torch -import torch.nn as nn -import torch.nn.functional as F - - -#from sklearn.preprocessing import MinMaxScaler -from sklearn.metrics import average_precision_score -from sklearn.metrics import roc_auc_score -from sklearn.metrics import mean_squared_error -from sklearn.metrics import brier_score_loss - -import wandb - -import sys -from pathlib import Path - -PATH = Path(__file__) -sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths, setup_data_paths -setup_project_paths(PATH) - - -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data -from config_sweep import get_swep_config -from config_hyperparameters import get_hp_config - - -def test(model, test_tensor, time_steps, config, device): # should be called eval/validation - - """ - Function to test the model on the hold-out test set. - The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. - The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. - Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). - """ - - model.eval() # remove to allow dropout to do its thing as a poor mans ensamble. but you need a high dropout.. - model.apply(apply_dropout) - - # wait until you know if this work as usually - pred_np_list = [] - pred_class_np_list = [] - - h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... - seq_len = test_tensor.shape[1] # og nu køre eden bare helt til roden - print(f'\t\t\t\t sequence length: {seq_len}', end= '\r') - - - for i in range(seq_len-1): # need to get hidden state... You are predicting one step ahead so the -1 - - if i < seq_len-1-time_steps: # take form the test set - - print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') - - t0 = test_tensor[:, i, :, :, :].to(device) # THIS IS ALL YOU NEED TO PUT ON DEVICE!!!!!!!!! - t1_pred, t1_pred_class, h_tt = model(t0, h_tt) - - else: # take the last t1_pred - print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') - t0 = t1_pred.detach() - - t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) - - t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. - pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays - pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays - - return pred_np_list, pred_class_np_list - - -def sample_posterior(model, views_vol, config, device): - - """ - Samples from the posterior distribution of Hydranet. - - Args: - - model: HydraNet - - views_vol (torch.Tensor): Input views data. - - config: Configuration file - - device: Device for computations. - - Returns: - - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) - """ - - print(f'Drawing {config.test_samples} posterior samples...') - - # Why do you put this test tensor on device here??!? - test_tensor = get_test_tensor(views_vol, config, device) # better cal thiis evel tensor - out_of_sample_vol = test_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. - - posterior_list = [] - posterior_list_class = [] - - for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? - - # test_tensor is need on device here, but maybe just do it inside the test function? - pred_np_list, pred_class_np_list = test(model, test_tensor, config.time_steps, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. - posterior_list.append(pred_np_list) - posterior_list_class.append(pred_class_np_list) - - #if i % 10 == 0: # print steps 10 - print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') - - return posterior_list, posterior_list_class, out_of_sample_vol, test_tensor - - -def get_posterior(model, views_vol, config, device): - - """ - Function to get the posterior distribution of Hydranet. - """ - - posterior_list, posterior_list_class, out_of_sample_vol, test_tensor = sample_posterior(model, views_vol, config, device) - - # YOU ARE MISSING SOMETHING ABOUT FEATURES HERE WHICH IS WHY YOU REPORTED AP ON WandB IS BIASED DOWNWARDS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!RYRYRYRYERYERYR - # need to check you "offline" evaluation script which is correctlly implemented before you use this function for forecasting. - - # Get mean and std - mean_array = np.array(posterior_list).mean(axis = 0) # get mean for each month! - std_array = np.array(posterior_list).std(axis = 0) - - mean_class_array = np.array(posterior_list_class).mean(axis = 0) # get mean for each month! - std_class_array = np.array(posterior_list_class).std(axis = 0) - - out_sample_month_list = [] # only used for pickle... - ap_list = [] - mse_list = [] - auc_list = [] - brier_list = [] - - for i in range(mean_array.shape[0]): # 0 of mean array is the temporal dim - - y_score = mean_array[i].reshape(-1) # make it 1d # nu 180x180 - y_score_prob = mean_class_array[i].reshape(-1) # nu 180x180 - - # do not really know what to do with these yet. - y_var = std_array[i].reshape(-1) # nu 180x180 - y_var_prob = std_class_array[i].reshape(-1) # nu 180x180 - - y_true = out_of_sample_vol[:,i].reshape(-1) # nu 180x180 . dim 0 is time - y_true_binary = (y_true > 0) * 1 - - mse = mean_squared_error(y_true, y_score) - ap = average_precision_score(y_true_binary, y_score_prob) - auc = roc_auc_score(y_true_binary, y_score_prob) - brier = brier_score_loss(y_true_binary, y_score_prob) - - log_dict = get_log_dict(i, mean_array, mean_class_array, std_array, std_class_array, out_of_sample_vol, config)# so at least it gets reported sep. - - wandb.log(log_dict) - - out_sample_month_list.append(i) # only used for pickle... - mse_list.append(mse) - ap_list.append(ap) # add to list. - auc_list.append(auc) - brier_list.append(brier) - - - if not config.sweep: - - _ , _, PATH_GENERATED = setup_data_paths(PATH) - - # if the path does not exist, create it - if not os.path.exists(PATH_GENERATED): - os.makedirs(PATH_GENERATED) - - # print for debugging - print(f'PATH to generated data: {PATH_GENERATED}') - - # pickle the posterior dict, metric dict, and test vol - # Should be time_steps and run_type in the name.... - - posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} - - metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, - 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} - - with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: - pickle.dump(posterior_dict, file) - - with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}{config.model_time_stamp}.pkl', 'wb') as file: - pickle.dump(metric_dict, file) - - with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy - pickle.dump(test_tensor.cpu().numpy(), file) - - print('Posterior dict, metric dict and test vol pickled and dumped!') - - - else: - print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - - - wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) - wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) - wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) - wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) - -# note: -# Going with the argparser, there is less of a clear reason to have to separate .py files for evaluation sweeps and single models. I think. Let me know if you disagree. -# naturally its a question of generalization and reusability, and i could see I had a lot of copy paste code between the two scripts. \ No newline at end of file From 8fca69971be8f03b9103403357e00784812eafe2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 16:45:34 +0200 Subject: [PATCH 057/136] sweep enabled again --- common_utils/cli_parser_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index d27597bb..55c3b858 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -59,9 +59,9 @@ def validate_arguments(args): print("To fix: Remove --evaluate flag when --run_type is 'forecasting'.") sys.exit(1) - if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate: - print(f"Error: Run type is {args.run_type} but neither --train nor --evaluate flag is set. Nothing to do... Exiting.") - print("To fix: Add --train and/or --evaluate flag.") + if args.run_type in ['calibration', 'testing'] and not args.train and not args.evaluate and not args.sweep: + print(f"Error: Run type is {args.run_type} but neither --train, --evaluate, nor --sweep flag is set. Nothing to do... Exiting.") + print("To fix: Add --train and/or --evaluate flag. Or use --sweep to run both training and evaluation in a WadnB sweep loop.") sys.exit(1) From 02db210ba9f096c3e7da1188fddc14506a444fc9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 17:16:35 +0200 Subject: [PATCH 058/136] fixed? --- common_utils/cli_parser_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common_utils/cli_parser_utils.py b/common_utils/cli_parser_utils.py index 55c3b858..cbc8f261 100644 --- a/common_utils/cli_parser_utils.py +++ b/common_utils/cli_parser_utils.py @@ -37,7 +37,7 @@ def parse_args(): help='Specify the name of the model artifact to be used for evaluation. ' 'The file extension will be added in the main and fit with the specific model algorithm.' 'The artifact name should be in the format: _model_.pt.' - 'where is calibration, testing, or forecasting, and is in the format %Y%m%d_%H%M%S.' + 'where is calibration, testing, or forecasting, and is in the format YMD_HMS.' 'If not provided, the latest artifact will be used by default.') return parser.parse_args() From d54df88a8e1d43092e3550cfe71009417780bdbf Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 29 May 2024 17:18:42 +0200 Subject: [PATCH 059/136] test run --- models/purple_alien/configs/config_hyperparameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index d2c7ec90..a67683a1 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 600, # 600 for actual trainnig, 10 for debug + 'samples': 10, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, @@ -24,7 +24,7 @@ def get_hp_config(): 'loss_reg': 'b', 'loss_reg_a' : 258, 'loss_reg_c' : 0.001, # 0.05 works... - 'test_samples': 128, # 128 for actual testing, 10 for debug + 'test_samples': 10, # 128 for actual testing, 10 for debug 'np_seed' : 4, 'torch_seed' : 4, 'window_dim' : 32, From 5b7105064ed493cb5f843b10e3bad6a1369581e6 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 01:52:31 +0200 Subject: [PATCH 060/136] renamed test_tensor to full --- models/purple_alien/src/utils/utils.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/models/purple_alien/src/utils/utils.py b/models/purple_alien/src/utils/utils.py index fc5da826..50e5b740 100644 --- a/models/purple_alien/src/utils/utils.py +++ b/models/purple_alien/src/utils/utils.py @@ -350,6 +350,7 @@ def train_log(avg_loss_list, avg_loss_reg_list, avg_loss_class_list): wandb.log({"avg_loss": avg_loss, "avg_loss_reg": avg_loss_reg, "avg_loss_class": avg_loss_class}) +# Should rename to sub_tensor or something like that... But it is used for training.. def get_train_tensors(views_vol, sample, config, device): """Uses the get_window_index and get_window_coords functions to sample a window from the training tensor. @@ -387,31 +388,25 @@ def get_train_tensors(views_vol, sample, config, device): train_tensor = train_tensor.reshape(N, C, D, H, W) - return(train_tensor) + return train_tensor +def get_full_tensor(views_vol, config, device): - - -def get_test_tensor(views_vol, config, device): - - """Uses to get the features for the test tensor. The test tensor is of size 1 x config.time_steps x config.input_channels x 180 x 180.""" + """Uses to get the features for the full tensor + Used for out-of-sample predictions for both evaluation and forecasting, depending on the run_type (partition). + The test tensor is of size 1 x config.time_steps x config.input_channels x 180 x 180.""" ln_best_sb_idx = config.first_feature_idx # 5 = ln_best_sb last_feature_idx = ln_best_sb_idx + config.input_channels - # !!!!!!!!!!!!!! why is this test tensor put on device here? !!!!!!!!!!!!!!!!!! - #test_tensor = torch.tensor(views_vol).float().to(device).unsqueeze(dim=0).permute(0,1,4,2,3)[:, :, ln_best_sb_idx:last_feature_idx, :, :] - print(f'views_vol shape {views_vol.shape}') - test_tensor = torch.tensor(views_vol).float().unsqueeze(dim=0).permute(0,1,4,2,3)[:, :, ln_best_sb_idx:last_feature_idx, :, :] - - print(f'test_tensor shape {test_tensor.shape}') - - return test_tensor + full_tensor = torch.tensor(views_vol).float().unsqueeze(dim=0).permute(0,1,4,2,3)[:, :, ln_best_sb_idx:last_feature_idx, :, :] + print(f'test_tensor shape {full_tensor.shape}') + return full_tensor @@ -447,7 +442,7 @@ def get_log_dict(i, mean_array, mean_class_array, std_array, std_class_array, ou log_dict[f"monthly/roc_auc_score{j}"] = auc log_dict[f"monthly/brier_score_loss{j}"] = brier - return (log_dict) + return log_dict def execute_freeze_h_option(config, model, t0, h_tt): From 0ee00c7b6644788cabbb6b22e05db53df994f905 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 01:55:35 +0200 Subject: [PATCH 061/136] test_tensor to full tensor --- .../src/offline_evaluation/evaluate_model.py | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 6206f7e1..2ab6837d 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -27,54 +27,72 @@ setup_project_paths(PATH) -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from config_sweep import get_swep_config from config_hyperparameters import get_hp_config -def test(model, test_tensor, time_steps, config, device): # should be called eval/validation +def predict(model, full_tensor, config, device, forecast = False): """ - Function to test the model on the hold-out test set. + Function to create predictions for the Hydranet model. The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). """ - model.eval() # remove to allow dropout to do its thing as a poor mans ensamble. but you need a high dropout.. + # Set the model to evaluation mode + model.eval() + + # Apply dropout which is otherwise not applied during eval mode model.apply(apply_dropout) - # wait until you know if this work as usually + # create empty lists to store the predictions both counts and probabilities pred_np_list = [] pred_class_np_list = [] + # initialize the hidden state h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... - seq_len = test_tensor.shape[1] # og nu køre eden bare helt til roden + + # get the sequence length + seq_len = full_tensor.shape[1] # get the sequence length + + # print the sequence length four tabs out to leave room for the sample prints print(f'\t\t\t\t sequence length: {seq_len}', end= '\r') + for i in range(seq_len-1): # You are predicting one step ahead so the -1 - for i in range(seq_len-1): # need to get hidden state... You are predicting one step ahead so the -1 - if i < seq_len-1-time_steps: # take form the test set + if i < seq_len-1-config.time_steps: # take form the test set. This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') - t0 = test_tensor[:, i, :, :, :].to(device) # THIS IS ALL YOU NEED TO PUT ON DEVICE!!!!!!!!! + # get the tensor for the current month + t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. + + # predict the next month, both the magnitudes and the probabilities and get the updated hidden state (which both cell and hidden state concatenated) t1_pred, t1_pred_class, h_tt = model(t0, h_tt) - else: # take the last t1_pred + + else: # take the last t1_pred. This is the out-of-sample part. print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') t0 = t1_pred.detach() + # Execute whatever freeze option you have set in the config out of sample t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) + # Only save the out-of-sample predictions t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays + # return the lists of predictions return pred_np_list, pred_class_np_list + + + def sample_posterior(model, views_vol, config, device): """ @@ -92,24 +110,25 @@ def sample_posterior(model, views_vol, config, device): print(f'Drawing {config.test_samples} posterior samples...') + # REALLY BAD NAME!!!! # Why do you put this test tensor on device here??!? - test_tensor = get_test_tensor(views_vol, config, device) # better cal thiis evel tensor - out_of_sample_vol = test_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. + full_tensor = get_full_tensor(views_vol, config, device) # better cal this evel tensor + out_of_sample_vol = full_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. posterior_list = [] posterior_list_class = [] for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? - # test_tensor is need on device here, but maybe just do it inside the test function? - pred_np_list, pred_class_np_list = test(model, test_tensor, config.time_steps, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. + # full_tensor is need on device here, but maybe just do it inside the test function? + pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. posterior_list.append(pred_np_list) posterior_list_class.append(pred_class_np_list) #if i % 10 == 0: # print steps 10 print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') - return posterior_list, posterior_list_class, out_of_sample_vol, test_tensor + return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor def get_posterior(model, views_vol, config, device): @@ -118,7 +137,7 @@ def get_posterior(model, views_vol, config, device): Function to get the posterior distribution of Hydranet. """ - posterior_list, posterior_list_class, out_of_sample_vol, test_tensor = sample_posterior(model, views_vol, config, device) + posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) # YOU ARE MISSING SOMETHING ABOUT FEATURES HERE WHICH IS WHY YOU REPORTED AP ON WandB IS BIASED DOWNWARDS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!RYRYRYRYERYERYR # need to check you "offline" evaluation script which is correctlly implemented before you use this function for forecasting. @@ -190,7 +209,7 @@ def get_posterior(model, views_vol, config, device): pickle.dump(metric_dict, file) with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy - pickle.dump(test_tensor.cpu().numpy(), file) + pickle.dump(full_tensor.cpu().numpy(), file) print('Posterior dict, metric dict and test vol pickled and dumped!') From 514412b5966ec6aa11dc9aad2ec9be40c2fd4d2a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 01:57:12 +0200 Subject: [PATCH 062/136] test_tensoer to full_tensor --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 1d293578..2c01a51c 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -19,7 +19,7 @@ from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop From 2646ab0b514b8c5ab6053f7dc943e93c73420f4a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 01:58:20 +0200 Subject: [PATCH 063/136] test_tensor to full --- models/purple_alien/src/training/train_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index dd2caba9..401aa61d 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -18,7 +18,7 @@ from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_test_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data #from config_sweep import get_swep_config from config_hyperparameters import get_hp_config From c9989594fbd16acdb6164e3f1880a63afdaed921 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 01:59:47 +0200 Subject: [PATCH 064/136] changed print --- models/purple_alien/src/utils/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/utils/utils.py b/models/purple_alien/src/utils/utils.py index 50e5b740..f70cdeba 100644 --- a/models/purple_alien/src/utils/utils.py +++ b/models/purple_alien/src/utils/utils.py @@ -404,7 +404,7 @@ def get_full_tensor(views_vol, config, device): full_tensor = torch.tensor(views_vol).float().unsqueeze(dim=0).permute(0,1,4,2,3)[:, :, ln_best_sb_idx:last_feature_idx, :, :] - print(f'test_tensor shape {full_tensor.shape}') + print(f'full_tensor shape {full_tensor.shape}') return full_tensor From 20422b5f27cdfe7a17c240c2e1831908574ef8b3 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:12:59 +0200 Subject: [PATCH 065/136] hold_out setting --- .../src/offline_evaluation/evaluate_model.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 2ab6837d..0d9016b6 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, forecast = False): +def predict(model, full_tensor, config, device, is_evalutaion = True): """ Function to create predictions for the Hydranet model. @@ -60,10 +60,17 @@ def predict(model, full_tensor, config, device, forecast = False): # print the sequence length four tabs out to leave room for the sample prints print(f'\t\t\t\t sequence length: {seq_len}', end= '\r') + # define the hold out set + hold_out = config.time_steps * is_evalutaion # if for_evel is True, hold_out is is the time_steps, else it is 0 + + # print for debugging + print(f'\t\t\t\t hold out size for evaluation: {hold_out}', end= '\r') + + for i in range(seq_len-1): # You are predicting one step ahead so the -1 - if i < seq_len-1-config.time_steps: # take form the test set. This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) + if i < seq_len-1-hold_out: # take form the test set. This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') From b09b963b777406b56bf6efde93f35871f681b8e0 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:14:25 +0200 Subject: [PATCH 066/136] debugging print --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 0d9016b6..9e9d30ca 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -64,7 +64,7 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): hold_out = config.time_steps * is_evalutaion # if for_evel is True, hold_out is is the time_steps, else it is 0 # print for debugging - print(f'\t\t\t\t hold out size for evaluation: {hold_out}', end= '\r') + print(f'\t\t\t\t\t\t\t\t\t\t\t\t hold out size for evaluation: {hold_out}', end= '\r') for i in range(seq_len-1): # You are predicting one step ahead so the -1 From 064e2996eee87a58edc69397a4fd017997b3c12a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:15:24 +0200 Subject: [PATCH 067/136] checking --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 9e9d30ca..f1e0e1b8 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, is_evalutaion = True): +def predict(model, full_tensor, config, device, is_evalutaion = False): """ Function to create predictions for the Hydranet model. From d57500128d3d2ee2dc91fee9358eea9bba165ed2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:30:40 +0200 Subject: [PATCH 068/136] test the new solution --- .../src/offline_evaluation/evaluate_model.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index f1e0e1b8..fb718bcf 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, is_evalutaion = False): +def predict(model, full_tensor, config, device, is_evalutaion = True): """ Function to create predictions for the Hydranet model. @@ -57,20 +57,27 @@ def predict(model, full_tensor, config, device, is_evalutaion = False): # get the sequence length seq_len = full_tensor.shape[1] # get the sequence length - # print the sequence length four tabs out to leave room for the sample prints - print(f'\t\t\t\t sequence length: {seq_len}', end= '\r') + if is_evalutaion: + + print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') + + full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. + in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation - # define the hold out set - hold_out = config.time_steps * is_evalutaion # if for_evel is True, hold_out is is the time_steps, else it is 0 + else: + + print(f'\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') - # print for debugging - print(f'\t\t\t\t\t\t\t\t\t\t\t\t hold out size for evaluation: {hold_out}', end= '\r') + full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting + in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence - for i in range(seq_len-1): # You are predicting one step ahead so the -1 + # print the sequence length four tabs out to leave room for the sample prints + print(f'\t\t\t\t full sequence length: {full_seq_len}', end= '\r') + for i in range(full_seq_len): - if i < seq_len-1-hold_out: # take form the test set. This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) + if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') From 5882ff2a56d152500cef07612a22d0e9500ea3bc Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:34:10 +0200 Subject: [PATCH 069/136] dump print shit --- .../purple_alien/src/offline_evaluation/evaluate_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index fb718bcf..e58f14c5 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -59,21 +59,21 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): if is_evalutaion: - print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') + print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation else: - print(f'\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') + print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence # print the sequence length four tabs out to leave room for the sample prints - print(f'\t\t\t\t full sequence length: {full_seq_len}', end= '\r') + print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') for i in range(full_seq_len): From 032f719b426fe98c8be417d9bc2c25946255b441 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:35:24 +0200 Subject: [PATCH 070/136] just a test:w --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index e58f14c5..0978b864 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, is_evalutaion = True): +def predict(model, full_tensor, config, device, is_evalutaion = False): """ Function to create predictions for the Hydranet model. From 75a06f32ba04a09efabbc393779bb5f574d717cc Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:40:51 +0200 Subject: [PATCH 071/136] moved pred stuf to new utils --- .../src/offline_evaluation/evaluate_model.py | 226 +++++++++--------- .../src/utils/utils_prediction.py | 143 +++++++++++ 2 files changed, 256 insertions(+), 113 deletions(-) create mode 100644 models/purple_alien/src/utils/utils_prediction.py diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 0978b864..c35ff467 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -28,123 +28,123 @@ from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils_prediction import predict, sample_posterior from config_sweep import get_swep_config from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, is_evalutaion = False): - - """ - Function to create predictions for the Hydranet model. - The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. - The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. - Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). - """ - - # Set the model to evaluation mode - model.eval() - - # Apply dropout which is otherwise not applied during eval mode - model.apply(apply_dropout) - - # create empty lists to store the predictions both counts and probabilities - pred_np_list = [] - pred_class_np_list = [] - - # initialize the hidden state - h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... - - # get the sequence length - seq_len = full_tensor.shape[1] # get the sequence length - - if is_evalutaion: - - print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') - - full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. - in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation - - else: - - print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') - - full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting - in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence - - - # print the sequence length four tabs out to leave room for the sample prints - print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') - - for i in range(full_seq_len): - - if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) - - print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') - - # get the tensor for the current month - t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. - - # predict the next month, both the magnitudes and the probabilities and get the updated hidden state (which both cell and hidden state concatenated) - t1_pred, t1_pred_class, h_tt = model(t0, h_tt) - - - else: # take the last t1_pred. This is the out-of-sample part. - print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') - t0 = t1_pred.detach() - - # Execute whatever freeze option you have set in the config out of sample - t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) - - # Only save the out-of-sample predictions - t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. - pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays - pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays - - # return the lists of predictions - return pred_np_list, pred_class_np_list - - - - - -def sample_posterior(model, views_vol, config, device): - - """ - Samples from the posterior distribution of Hydranet. - - Args: - - model: HydraNet - - views_vol (torch.Tensor): Input views data. - - config: Configuration file - - device: Device for computations. - - Returns: - - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) - """ - - print(f'Drawing {config.test_samples} posterior samples...') - - # REALLY BAD NAME!!!! - # Why do you put this test tensor on device here??!? - full_tensor = get_full_tensor(views_vol, config, device) # better cal this evel tensor - out_of_sample_vol = full_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. - - posterior_list = [] - posterior_list_class = [] - - for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? - - # full_tensor is need on device here, but maybe just do it inside the test function? - pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. - posterior_list.append(pred_np_list) - posterior_list_class.append(pred_class_np_list) - - #if i % 10 == 0: # print steps 10 - print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') - - return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor - - +# +#def predict(model, full_tensor, config, device, is_evalutaion = True): +# +# """ +# Function to create predictions for the Hydranet model. +# The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. +# The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. +# Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). +# """ +# +# # Set the model to evaluation mode +# model.eval() +# +# # Apply dropout which is otherwise not applied during eval mode +# model.apply(apply_dropout) +# +# # create empty lists to store the predictions both counts and probabilities +# pred_np_list = [] +# pred_class_np_list = [] +# +# # initialize the hidden state +# h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... +# +# # get the sequence length +# seq_len = full_tensor.shape[1] # get the sequence length +# +# if is_evalutaion: +# +# print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') +# +# full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. +# in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation +# +# else: +# +# print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') +# +# full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting +# in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence +# +# +# # print the sequence length four tabs out to leave room for the sample prints +# print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') +# +# for i in range(full_seq_len): +# +# if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) +# +# print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') +# +# # get the tensor for the current month +# t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. +# +# # predict the next month, both the magnitudes and the probabilities and get the updated hidden state (which both cell and hidden state concatenated) +# t1_pred, t1_pred_class, h_tt = model(t0, h_tt) +# +# +# else: # take the last t1_pred. This is the out-of-sample part. +# print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') +# t0 = t1_pred.detach() +# +# # Execute whatever freeze option you have set in the config out of sample +# t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) +# +# # Only save the out-of-sample predictions +# t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. +# pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays +# pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays +# +# # return the lists of predictions +# return pred_np_list, pred_class_np_list +# +# +#def sample_posterior(model, views_vol, config, device): +# +# """ +# Samples from the posterior distribution of Hydranet. +# +# Args: +# - model: HydraNet +# - views_vol (torch.Tensor): Input views data. +# - config: Configuration file +# - device: Device for computations. +# +# Returns: +# - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) +# """ +# +# print(f'Drawing {config.test_samples} posterior samples...') +# +# # REALLY BAD NAME!!!! +# # Why do you put this test tensor on device here??!? +# full_tensor = get_full_tensor(views_vol, config, device) # better cal this evel tensor +# out_of_sample_vol = full_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. +# +# posterior_list = [] +# posterior_list_class = [] +# +# for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? +# +# # full_tensor is need on device here, but maybe just do it inside the test function? +# pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. +# posterior_list.append(pred_np_list) +# posterior_list_class.append(pred_class_np_list) +# +# #if i % 10 == 0: # print steps 10 +# print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') +# +# return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor +# + +# should be called evaluate_posterior.... def get_posterior(model, views_vol, config, device): """ diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py new file mode 100644 index 00000000..5de1afe2 --- /dev/null +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -0,0 +1,143 @@ +import os + +import numpy as np +import pickle +import time +import functools + +import torch +import torch.nn as nn +import torch.nn.functional as F + + +#from sklearn.preprocessing import MinMaxScaler +from sklearn.metrics import average_precision_score +from sklearn.metrics import roc_auc_score +from sklearn.metrics import mean_squared_error +from sklearn.metrics import brier_score_loss + +import wandb + +import sys +from pathlib import Path + +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths, setup_data_paths +setup_project_paths(PATH) + + +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from config_sweep import get_swep_config +from config_hyperparameters import get_hp_config + + +def predict(model, full_tensor, config, device, is_evalutaion = True): + + """ + Function to create predictions for the Hydranet model. + The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. + The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. + Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). + """ + + # Set the model to evaluation mode + model.eval() + + # Apply dropout which is otherwise not applied during eval mode + model.apply(apply_dropout) + + # create empty lists to store the predictions both counts and probabilities + pred_np_list = [] + pred_class_np_list = [] + + # initialize the hidden state + h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... + + # get the sequence length + seq_len = full_tensor.shape[1] # get the sequence length + + if is_evalutaion: + + print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') + + full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. + in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation + + else: + + print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') + + full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting + in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence + + + # print the sequence length four tabs out to leave room for the sample prints + print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') + + for i in range(full_seq_len): + + if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) + + print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') + + # get the tensor for the current month + t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. + + # predict the next month, both the magnitudes and the probabilities and get the updated hidden state (which both cell and hidden state concatenated) + t1_pred, t1_pred_class, h_tt = model(t0, h_tt) + + + else: # take the last t1_pred. This is the out-of-sample part. + print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') + t0 = t1_pred.detach() + + # Execute whatever freeze option you have set in the config out of sample + t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) + + # Only save the out-of-sample predictions + t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. + pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays + pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays + + # return the lists of predictions + return pred_np_list, pred_class_np_list + + +def sample_posterior(model, views_vol, config, device): + + """ + Samples from the posterior distribution of Hydranet. + + Args: + - model: HydraNet + - views_vol (torch.Tensor): Input views data. + - config: Configuration file + - device: Device for computations. + + Returns: + - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) + """ + + print(f'Drawing {config.test_samples} posterior samples...') + + # REALLY BAD NAME!!!! + # Why do you put this test tensor on device here??!? + full_tensor = get_full_tensor(views_vol, config, device) # better cal this evel tensor + out_of_sample_vol = full_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. + + posterior_list = [] + posterior_list_class = [] + + for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? + + # full_tensor is need on device here, but maybe just do it inside the test function? + pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. + posterior_list.append(pred_np_list) + posterior_list_class.append(pred_class_np_list) + + #if i % 10 == 0: # print steps 10 + print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') + + return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor + From a2760bc778dd3bc3f93203a38b2cfdc1fc153de9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:44:14 +0200 Subject: [PATCH 072/136] better printing? --- models/purple_alien/src/utils/utils_prediction.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 5de1afe2..9d3edffa 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -59,27 +59,23 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): if is_evalutaion: - print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') - full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation + print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set. Full sequence length: {full_seq_len}', end= '\r') + else: - print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') - full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence - - # print the sequence length four tabs out to leave room for the sample prints - print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') + print(f'\t\t\t\t\t\t\t Forecasting mode. No hold out set. Full sequence length: {full_seq_len}', end= '\r') for i in range(full_seq_len): if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) - print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') + print(f'\t\t\t\t in sample. month: {i+1}', end= '\r') # get the tensor for the current month t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. @@ -89,7 +85,7 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): else: # take the last t1_pred. This is the out-of-sample part. - print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') + print(f'\t\t\t\t Out of sample. month: {i+1}', end= '\r') t0 = t1_pred.detach() # Execute whatever freeze option you have set in the config out of sample From 1afaebbc7f594d6fe1fac0adb410a7006ac656cb Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:45:09 +0200 Subject: [PATCH 073/136] better printing --- models/purple_alien/src/utils/utils_prediction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 9d3edffa..49f19ce0 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -75,7 +75,7 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) - print(f'\t\t\t\t in sample. month: {i+1}', end= '\r') + print(f'\t\t\t in sample. month: {i+1}', end= '\r') # get the tensor for the current month t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. @@ -85,7 +85,7 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): else: # take the last t1_pred. This is the out-of-sample part. - print(f'\t\t\t\t Out of sample. month: {i+1}', end= '\r') + print(f'\t\t\t Out of sample. month: {i+1}', end= '\r') t0 = t1_pred.detach() # Execute whatever freeze option you have set in the config out of sample From 525b2c23588a42891e72cef5bb8b195d7fc05b7e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 02:48:38 +0200 Subject: [PATCH 074/136] get_posterior to evaluate_posterior --- models/purple_alien/main.py | 12 +- .../src/offline_evaluation/evaluate_model.py | 114 +----------------- 2 files changed, 8 insertions(+), 118 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 2c01a51c..6494eac7 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -23,8 +23,8 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config from train_model import make, training_loop -# from evaluate_sweep import get_posterior # see if it can be more genrel to a single model as well... -from evaluate_model import get_posterior +# from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... +from evaluate_model import evaluate_posterior from cli_parser_utils import parse_args, validate_arguments from artifacts_utils import get_latest_model_artifact @@ -56,7 +56,7 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) print('Done training') - get_posterior(model, views_vol, config, device) + evaluate_posterior(model, views_vol, config, device) print('Done testing') # Handle the single model runs: train and save the model as an artifact @@ -116,9 +116,9 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # load the model model = torch.load(PATH_MODEL_ARTIFACT) - #model.eval() # this is done in the get_posterior function + #model.eval() # this is done in the evaluate_posterior function - # Get the excact model date_time stamp for the pkl files made in the get_posterior from evaluation.py + # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] # debug print statement @@ -127,7 +127,7 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # save to config for logging and concisness config.model_time_stamp = model_time_stamp - get_posterior(model, views_vol, config, device) + evaluate_posterior(model, views_vol, config, device) print('Done testing') diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index c35ff467..6a59e5f2 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -33,122 +33,12 @@ from config_hyperparameters import get_hp_config -# -#def predict(model, full_tensor, config, device, is_evalutaion = True): -# -# """ -# Function to create predictions for the Hydranet model. -# The function takes the model, the test tensor, the number of time steps to predict, the config, and the device as input. -# The function returns **two lists of numpy arrays**. One list of the predicted magnitudes and one list of the predicted probabilities. -# Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). -# """ -# -# # Set the model to evaluation mode -# model.eval() -# -# # Apply dropout which is otherwise not applied during eval mode -# model.apply(apply_dropout) -# -# # create empty lists to store the predictions both counts and probabilities -# pred_np_list = [] -# pred_class_np_list = [] -# -# # initialize the hidden state -# h_tt = model.init_hTtime(hidden_channels = model.base, H = 180, W = 180).float().to(device) # coul auto the... -# -# # get the sequence length -# seq_len = full_tensor.shape[1] # get the sequence length -# -# if is_evalutaion: -# -# print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Evaluation mode. retaining hold out set', end= '\r') -# -# full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. -# in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation -# -# else: -# -# print(f'\t\t\t\t\t\t\t \t\t\t\t\t\t\t\t Forecasting mode. No hold out set', end= '\r') -# -# full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting -# in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence -# -# -# # print the sequence length four tabs out to leave room for the sample prints -# print(f'\t\t\t\t\t\t\t\t\t\t\t\t\t full sequence length: {full_seq_len}', end= '\r') -# -# for i in range(full_seq_len): -# -# if i < in_sample_seq_len: # This is the in-sample part and where the out sample part is defined (seq_len-1-time_steps) -# -# print(f'\t\t\t\t\t\t\t in sample. month: {i+1}', end= '\r') -# -# # get the tensor for the current month -# t0 = full_tensor[:, i, :, :, :].to(device) # This is all you need to put on device. -# -# # predict the next month, both the magnitudes and the probabilities and get the updated hidden state (which both cell and hidden state concatenated) -# t1_pred, t1_pred_class, h_tt = model(t0, h_tt) -# -# -# else: # take the last t1_pred. This is the out-of-sample part. -# print(f'\t\t\t\t\t\t\t Out of sample. month: {i+1}', end= '\r') -# t0 = t1_pred.detach() -# -# # Execute whatever freeze option you have set in the config out of sample -# t1_pred, t1_pred_class, h_tt = execute_freeze_h_option(config, model, t0, h_tt) -# -# # Only save the out-of-sample predictions -# t1_pred_class = torch.sigmoid(t1_pred_class) # there is no sigmoid in the model (the loss takes logits) so you need to do it here. -# pred_np_list.append(t1_pred.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays -# pred_class_np_list.append(t1_pred_class.cpu().detach().numpy().squeeze()) # squeeze to remove the batch dim. So this is a list of 3x180x180 arrays -# -# # return the lists of predictions -# return pred_np_list, pred_class_np_list -# -# -#def sample_posterior(model, views_vol, config, device): -# -# """ -# Samples from the posterior distribution of Hydranet. -# -# Args: -# - model: HydraNet -# - views_vol (torch.Tensor): Input views data. -# - config: Configuration file -# - device: Device for computations. -# -# Returns: -# - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) -# """ -# -# print(f'Drawing {config.test_samples} posterior samples...') -# -# # REALLY BAD NAME!!!! -# # Why do you put this test tensor on device here??!? -# full_tensor = get_full_tensor(views_vol, config, device) # better cal this evel tensor -# out_of_sample_vol = full_tensor[:,-config.time_steps:,:,:,:].cpu().numpy() # From the test tensor get the out-of-sample time_steps. -# -# posterior_list = [] -# posterior_list_class = [] -# -# for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? -# -# # full_tensor is need on device here, but maybe just do it inside the test function? -# pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. -# posterior_list.append(pred_np_list) -# posterior_list_class.append(pred_class_np_list) -# -# #if i % 10 == 0: # print steps 10 -# print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') -# -# return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor -# # should be called evaluate_posterior.... -def get_posterior(model, views_vol, config, device): +def evaluate_posterior(model, views_vol, config, device): """ - Function to get the posterior distribution of Hydranet. + Function to sample from and evaluate the posterior distribution of Hydranet. """ posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) From 93880bebcb57f34220ce59d523cd8986b143c1c8 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 03:03:03 +0200 Subject: [PATCH 075/136] first commit --- .../src/forecasting/generate_forcast.py | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/models/purple_alien/src/forecasting/generate_forcast.py b/models/purple_alien/src/forecasting/generate_forcast.py index e69de29b..951620bb 100644 --- a/models/purple_alien/src/forecasting/generate_forcast.py +++ b/models/purple_alien/src/forecasting/generate_forcast.py @@ -0,0 +1,136 @@ +import os + +import numpy as np +import pickle +import time +import functools + +import torch +import torch.nn as nn +import torch.nn.functional as F + +import wandb + +import sys +from pathlib import Path + +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths, setup_data_paths +setup_project_paths(PATH) + + +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils_prediction import predict, sample_posterior +from config_hyperparameters import get_hp_config + + +def generate_forecast(model, views_vol, config, device, PATH): + """ + Function to generate forecast using the provided model and views_vol. + It saves the generated posterior distributions and out-of-sample volumes. + + Args: + model: The trained model used for forecasting. + views_vol: The input data tensor for forecasting. + config: Configuration object containing settings. + device: The device (CPU or GPU) to run the predictions on. + PATH: The base path where generated data will be saved. + + Returns: + None + """ + # Ensure the model is in evaluation mode + model.eval() + model.apply(apply_dropout) + + # Generate posterior samples and out-of-sample volumes + posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) + + # Set up paths for storing generated data + _, _, PATH_GENERATED = setup_data_paths(PATH) + + # Create the directory if it does not exist + os.makedirs(PATH_GENERATED, exist_ok=True) + + # Print the path for debugging + print(f'PATH to generated data: {PATH_GENERATED}') + + # Create a dictionary to store posterior data + posterior_dict = { + 'posterior_list': posterior_list, + 'posterior_list_class': posterior_list_class, + 'out_of_sample_vol': out_of_sample_vol + } + + # Save the posterior data to a pickle file + filename = f'posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl' + with open(os.path.join(PATH_GENERATED, filename), 'wb') as file: + pickle.dump(posterior_dict, file) + + print('Posterior dict and test vol pickled and dumped!') + +# Ensure utils_prediction.py and any other dependencies are imported correctly +# from utils_prediction import sample_posterior, apply_dropout +# from utils_data import setup_data_paths + + + + + + + + + + + + + + + + + + +## you always load an artifact for forecasting - like with the evaluate you take the latest artifact unless you specify another one +## But that is done in main.py - just passed to here as an argument +# +## Then the load the offical forescasting partition +## And the first steps must be usign the function from utils_prediction.py to get the predictions and the posetrior +# +## model, views_vol, config, device should be passed as arguments to this function +# +#def generate_forecast(model, views_vol, config, device): +# +# +# # THIS IS ALL PURE MESS RIGHT NOW!!! +# +# +# posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) +# +## then to prediction store I guess? Or perhaps just the generated data for now... +# +# _ , _, PATH_GENERATED = setup_data_paths(PATH) +# +# # if the path does not exist, create it +# +# if not os.path.exists(PATH_GENERATED): +# +# os.makedirs(PATH_GENERATED) +# +# # print for debugging +# print(f'PATH to generated data: {PATH_GENERATED}') +# +# # pickle the posterior dict, metric dict, and test vol +# +# # Should be time_steps and run_type in the name.... +# posterior_dict = {'posterior_list' : posterior_list, 'posterior_list_class': posterior_list_class, 'out_of_sample_vol' : out_of_sample_vol} +# +# +# with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: +# +# pickle.dump(posterior_dict, file) +# +# +# print('Posterior dict, metric dict and test vol pickled and dumped!') +# +# \ No newline at end of file From 8882742eee2eca82834ecaf3c28ddce55b3d7ba4 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 03:06:53 +0200 Subject: [PATCH 076/136] thinking about forecastng --- models/purple_alien/main.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 6494eac7..9a55e264 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -130,6 +130,50 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art evaluate_posterior(model, views_vol, config, device) print('Done testing') +# GENERATED CODE. I NEED TO THINK ABOUT HOW MASSIVE THE MODEL FUNCTION IS BECOMING. +# if forecast: +# +# # Determine the artifact path: +# # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type +# if artifact_name is not None: +# +# # pritn statement for debugging +# print(f"Using (non default) artifact: {artifact_name}") +# +# # Check if the artifact name has the correct file extension +# if not artifact_name.endswith('.pt'): +# artifact_name += '.pt' +# +# # Define the full (model specific) path for the artifact +# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) +# +# else: +# # print statement for debugging +# print(f"Using lastest (default) run type ({config.run_type}) specific artifact") +# +# # Get the latest model artifact based on the run type and the (models specific) artifacts path +# PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) +# +# # Check if the model artifact exists - if not, raise an error +# if not os.path.exists(PATH_MODEL_ARTIFACT): +# raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") +# +# # load the model +# model = torch.load(PATH_MODEL_ARTIFACT) +# #model.eval() # this is done in the evaluate_posterior function +# +# # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py +# model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] +# +# # debug print statement +# print(f"model_time_stamp: {model_time_stamp}") +# +# # save to config for logging and concisness +# config.model_time_stamp = model_time_stamp +# +# evaluate_posterior(model, views_vol, config, device) +# print('Done testing') +# if __name__ == "__main__": From 34a22193d1dad3926400e678bba368dc6a2df9fd Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 03:07:54 +0200 Subject: [PATCH 077/136] full sweep test --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 40974fb8..92b7b854 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 600}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug + 'test_samples': { 'value' :128}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From 28d4e4c79e5973b271b034e1c0d27963e7a6170a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:32:03 +0200 Subject: [PATCH 078/136] much improved modularity - see if works --- models/purple_alien/main.py | 407 +++++++++++++++++++++++------------- 1 file changed, 266 insertions(+), 141 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 9a55e264..4daeb74a 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -28,22 +28,177 @@ from cli_parser_utils import parse_args, validate_arguments from artifacts_utils import get_latest_model_artifact -def model_pipeline(config = None, project = None, train = None, eval = None, artifact_name = None): + +def setup_device(): + # set the device + evice = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"Using device: {device}") + +def add_wandb_monthly_metrics(): + + # Define "new" monthly metrics for WandB logging + wandb.define_metric("monthly/out_sample_month") + wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + + +def handle_sweep_run(args): + print('Running sweep...') + + project = f"purple_alien_sweep" # check naming convention + sweep_config = get_swep_config() + sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep + sweep_config['parameters']['sweep'] = {'value' : True} + + sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name + + wandb.agent(sweep_id, model_pipeline) + + +def handle_single_run(args): + + # get run type and denoting project name - check convention! + run_type = args.run_type + project = f"purple_alien_{run_type}" + + # get hyperparameters + hyperparameters = get_hp_config() + hyperparameters['run_type'] = run_type + hyperparameters['sweep'] = False + + if args.train: + print(f"Training one model for run type: {run_type} and saving it as an artifact...") + model_pipeline(args, config = hyperparameters, project = project, train=True) + + if args.evaluate: + print(f"Evaluating model for run type: {run_type}...") + model_pipeline(args, config = hyperparameters, project = project, eval=True) + + #if args.artifact_name is not None: + # model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) + + #else: +# model_pipeline(config = hyperparameters, project = project, eval=True) + + if args.run_type == 'forecasting': + print('True forecasting ->->->->') + model_pipeline(args, config = hyperparameters, project = project, forecast=True) + + + +def handle_training(config, device, views_vol, PATH_ARTIFACTS): + + # Create the model, criterion, optimizer and scheduler + model, criterion, optimizer, scheduler = make(config, device) + + # Train the model + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') + + # just in case the artifacts folder does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) + + # Define the path for the artifacts with a timestamp and a run type + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + model_filename = f"{config.run_type}_model_{timestamp}.pt" + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) + + # save the model + torch.save(model, PATH_MODEL_ARTIFACT) + + # done + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + + +def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): + + # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type + if artifact_name: + print(f"Using (non-default) artifact: {artifact_name}") + + # If it lacks the file extension, add it + if not artifact_name.endswith('.pt'): + artifact_name += '.pt' + + # Define the full (model specific) path for the artifact + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + + else: + # use the latest model artifact based on the run type + print(f"Using latest (default) run type ({config.run_type}) specific artifact") + + # Get the latest model artifact based on the run type and the (models specific) artifacts path + PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) + + # Check if the model artifact exists - if not, raise an error + if not os.path.exists(PATH_MODEL_ARTIFACT): + raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + + # load the model + model = torch.load(PATH_MODEL_ARTIFACT) + + # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py + model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] + + # print for debugging + print(f"model_time_stamp: {model_time_stamp}") + + # add to config for logging and conciseness + config.model_time_stamp = model_time_stamp + + # evaluate the model posterior distribution + evaluate_posterior(model, views_vol, config, device) + + # done. + print('Done testing') + + +def handle_forecasting(args): + + run_type = args.run_type + project = f"purple_alien_{run_type}" + hyperparameters = get_hp_config() + hyperparameters['run_type'] = run_type + hyperparameters['sweep'] = False + + if args.artifact_name is not None: + model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) + + else: + model_pipeline(config = hyperparameters, project = project) + + raise NotImplementedError('Forecasting not implemented yet') + + #print('Done forecasting') + + + # but right now there is no forecasting implemented in model pipeline..... + + + + + +# ----------------- Model Pipeline ----------------- NOW THIS IS TOO BIG... + +def model_pipeline(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): # Define the path for the artifacts PATH_ARTIFACTS = setup_artifacts_paths(PATH) # Set the device - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(f"Using device: {device}") + #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + #print(f"Using device: {device}") + + device = setup_device() # Initialize WandB with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep # Define "new" monthly metrics for WandB logging - wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + #wandb.define_metric("monthly/out_sample_month") + #wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + add_wandb_monthly_metrics() # add the monthly metrics to WandB + # Update config from WandB initialization above config = wandb.config @@ -52,6 +207,7 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # Handle the sweep runs if config.sweep: # If we are running a sweep, always train and evaluate + model, criterion, optimizer, scheduler = make(config, device) training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) print('Done training') @@ -61,119 +217,82 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # Handle the single model runs: train and save the model as an artifact if train: + handle_training(config, device, views_vol, PATH_ARTIFACTS) + # # All wandb logging is done in the training loop. + # + # # Create the model, criterion, optimizer and scheduler + # model, criterion, optimizer, scheduler = make(config, device) + # training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + # print('Done training') - # All wandb logging is done in the training loop. - - # Create the model, criterion, optimizer and scheduler - model, criterion, optimizer, scheduler = make(config, device) - training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - print('Done training') - - # create the artifacts folder if it does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) + # # create the artifacts folder if it does not exist + # os.makedirs(PATH_ARTIFACTS, exist_ok=True) - # Define the path for the artifacts with a timestamp and a run type - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - model_filename = f"{config.run_type}_model_{timestamp}.pt" - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) + # # Define the path for the artifacts with a timestamp and a run type + # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + # model_filename = f"{config.run_type}_model_{timestamp}.pt" + # PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) - # save the model - torch.save(model, PATH_MODEL_ARTIFACT) + # # save the model + # torch.save(model, PATH_MODEL_ARTIFACT) - # Currently the artifacts are only sotred locally. Putting them on WandB is a good idea, but I need to understand thier model storage better first. + # # Currently the artifacts are only sotred locally. Putting them on WandB is a good idea, but I need to understand thier model storage better first. - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") - #return model # dont return anything, the model is saved as an artifact + # print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + # #return model # dont return anything, the model is saved as an artifact # Handle the single model runs: evaluate a trained model (artifact) if eval: + handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + # # Determine the artifact path: + # # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type + # if artifact_name is not None: - # Determine the artifact path: - # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type - if artifact_name is not None: + # # pritn statement for debugging + # print(f"Using (non default) artifact: {artifact_name}") + # + # # Check if the artifact name has the correct file extension + # if not artifact_name.endswith('.pt'): + # artifact_name += '.pt' - # pritn statement for debugging - print(f"Using (non default) artifact: {artifact_name}") - - # Check if the artifact name has the correct file extension - if not artifact_name.endswith('.pt'): - artifact_name += '.pt' + # # Define the full (model specific) path for the artifact + # PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + # + # else: + # # print statement for debugging + # print(f"Using lastest (default) run type ({config.run_type}) specific artifact") - # Define the full (model specific) path for the artifact - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) - - else: - # print statement for debugging - print(f"Using lastest (default) run type ({config.run_type}) specific artifact") + # # Get the latest model artifact based on the run type and the (models specific) artifacts path + # PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) - # Get the latest model artifact based on the run type and the (models specific) artifacts path - PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) + # # Check if the model artifact exists - if not, raise an error + # if not os.path.exists(PATH_MODEL_ARTIFACT): + # raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") - # Check if the model artifact exists - if not, raise an error - if not os.path.exists(PATH_MODEL_ARTIFACT): - raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + # # load the model + # model = torch.load(PATH_MODEL_ARTIFACT) + # #model.eval() # this is done in the evaluate_posterior function + # + # # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py + # model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] - # load the model - model = torch.load(PATH_MODEL_ARTIFACT) - #model.eval() # this is done in the evaluate_posterior function - - # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py - model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] + # # debug print statement + # print(f"model_time_stamp: {model_time_stamp}") - # debug print statement - print(f"model_time_stamp: {model_time_stamp}") + # # save to config for logging and concisness + # config.model_time_stamp = model_time_stamp - # save to config for logging and concisness - config.model_time_stamp = model_time_stamp + # evaluate_posterior(model, views_vol, config, device) + # print('Done testing') + +# --------------------------------------------------------------------- + if forecast: + handle_forecasting(config, device, views_vol, PATH_ARTIFACTS) + #raise NotImplementedError('Forecasting not implemented yet') + #print('Done forecasting') - evaluate_posterior(model, views_vol, config, device) - print('Done testing') -# GENERATED CODE. I NEED TO THINK ABOUT HOW MASSIVE THE MODEL FUNCTION IS BECOMING. -# if forecast: -# -# # Determine the artifact path: -# # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type -# if artifact_name is not None: -# -# # pritn statement for debugging -# print(f"Using (non default) artifact: {artifact_name}") -# -# # Check if the artifact name has the correct file extension -# if not artifact_name.endswith('.pt'): -# artifact_name += '.pt' -# -# # Define the full (model specific) path for the artifact -# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) -# -# else: -# # print statement for debugging -# print(f"Using lastest (default) run type ({config.run_type}) specific artifact") -# -# # Get the latest model artifact based on the run type and the (models specific) artifacts path -# PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) -# -# # Check if the model artifact exists - if not, raise an error -# if not os.path.exists(PATH_MODEL_ARTIFACT): -# raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") -# -# # load the model -# model = torch.load(PATH_MODEL_ARTIFACT) -# #model.eval() # this is done in the evaluate_posterior function -# -# # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py -# model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] -# -# # debug print statement -# print(f"model_time_stamp: {model_time_stamp}") -# -# # save to config for logging and concisness -# config.model_time_stamp = model_time_stamp -# -# evaluate_posterior(model, views_vol, config, device) -# print('Done testing') -# if __name__ == "__main__": @@ -191,65 +310,71 @@ def model_pipeline(config = None, project = None, train = None, eval = None, art # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags if args.sweep == True: - - print('Running sweep...') - project = f"purple_alien_sweep" # check naming convention + handle_sweep_run(args) + + # print('Running sweep...') + + # project = f"purple_alien_sweep" # check naming convention - sweep_config = get_swep_config() - sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep - sweep_config['parameters']['sweep'] = {'value' : True} + # sweep_config = get_swep_config() + # sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep + # sweep_config['parameters']['sweep'] = {'value' : True} - sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name + # sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - wandb.agent(sweep_id, model_pipeline) + # wandb.agent(sweep_id, model_pipeline) elif args.sweep == False: - print('Running single model operation...') - run_type = args.run_type - project = f"purple_alien_{run_type}" - hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type # this is also how the forecast if statement is informed below - hyperparameters['sweep'] = False - - # if train is flagged, train the model and save it as an artifact - if args.train: - print(f"Training one model for run type: {run_type} and saving it as an artifact...") - model_pipeline(config = hyperparameters, project = project, train=True) - - # if evaluate is flagged, evaluate the model - if args.evaluate: - print(f"Evaluating model for run type: {run_type}...") - - # if an artifact name is provided, use it. - if args.artifact_name is not None: - model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) - - # Otherwise, get the default - I.e. latest model artifact give the specific run type - else: - model_pipeline(config = hyperparameters, project = project, eval=True) + + handle_single_run(args) + +# print('Running single model operation...') +# run_type = args.run_type +# project = f"purple_alien_{run_type}" +# hyperparameters = get_hp_config() +# hyperparameters['run_type'] = run_type # this is also how the forecast if statement is informed below +# hyperparameters['sweep'] = False +# +# # if train is flagged, train the model and save it as an artifact +# if args.train: +# print(f"Training one model for run type: {run_type} and saving it as an artifact...") +# model_pipeline(config = hyperparameters, project = project, train=True) +# +# # if evaluate is flagged, evaluate the model +# if args.evaluate: +# print(f"Evaluating model for run type: {run_type}...") +# +# # if an artifact name is provided, use it. +# if args.artifact_name is not None: +# model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) +# +# # Otherwise, get the default - I.e. latest model artifact give the specific run type +# else: +# model_pipeline(config = hyperparameters, project = project, eval=True) # I guess you also need some kind of forecasting here... - if run_type == 'forecasting': - print('True forecasting ->->->->') - - # if an artifact name is provided, use it. - if args.artifact_name is not None: - model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) + #if args.run_type == 'forecasting': - # Otherwise, get the default - I.e. latest model artifact give the specific run type - else: - model_pipeline(config = hyperparameters, project = project) + # handle_single_run(args) +# print('True forecasting ->->->->') +# +# # if an artifact name is provided, use it. +# if args.artifact_name is not None: +# model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) +# +# # Otherwise, get the default - I.e. latest model artifact give the specific run type +# else: +# model_pipeline(config = hyperparameters, project = project) +# # notes: # should always be a trained artifact? # should always de the last artifact? - - print('not implemented yet...') end_t = time.time() minutes = (end_t - start_t)/60 From a54c6368924873951261d13dd9597b6c0d700375 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:35:08 +0200 Subject: [PATCH 079/136] fixed a typo... --- models/purple_alien/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 4daeb74a..a4826b83 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -31,7 +31,7 @@ def setup_device(): # set the device - evice = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"Using device: {device}") def add_wandb_monthly_metrics(): From d20b8574e5e7caa12671120f9aafc778b7bae3f2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:45:56 +0200 Subject: [PATCH 080/136] Better now? --- models/purple_alien/main.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index a4826b83..979cfdf8 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -65,13 +65,17 @@ def handle_single_run(args): hyperparameters['run_type'] = run_type hyperparameters['sweep'] = False - if args.train: - print(f"Training one model for run type: {run_type} and saving it as an artifact...") - model_pipeline(args, config = hyperparameters, project = project, train=True) + if args.run_type == 'calibration' or args.run_type == 'testing': - if args.evaluate: - print(f"Evaluating model for run type: {run_type}...") - model_pipeline(args, config = hyperparameters, project = project, eval=True) + model_pipeline(args, config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) + +# if args.train: +# print(f"Training one model for run type: {run_type} and saving it as an artifact...") +# model_pipeline(args, config = hyperparameters, project = project, train=True) +# +# if args.evaluate: +# print(f"Evaluating model for run type: {run_type}...") +# model_pipeline(args, config = hyperparameters, project = project, eval=True) #if args.artifact_name is not None: # model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) @@ -79,10 +83,13 @@ def handle_single_run(args): #else: # model_pipeline(config = hyperparameters, project = project, eval=True) - if args.run_type == 'forecasting': - print('True forecasting ->->->->') - model_pipeline(args, config = hyperparameters, project = project, forecast=True) + elif args.run_type == 'forecasting': + #print('True forecasting ->->->->') + model_pipeline(args, config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) + + else: + raise ValueError(f"Invalid run type: {args.run_type}") def handle_training(config, device, views_vol, PATH_ARTIFACTS): From abeb7d99c00948d8e672f32253a4c636abf4d320 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:48:00 +0200 Subject: [PATCH 081/136] now mayhaps? --- models/purple_alien/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 979cfdf8..bed81cfa 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -67,7 +67,7 @@ def handle_single_run(args): if args.run_type == 'calibration' or args.run_type == 'testing': - model_pipeline(args, config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) + model_pipeline(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) # if args.train: # print(f"Training one model for run type: {run_type} and saving it as an artifact...") @@ -86,7 +86,7 @@ def handle_single_run(args): elif args.run_type == 'forecasting': #print('True forecasting ->->->->') - model_pipeline(args, config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) + model_pipeline(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) else: raise ValueError(f"Invalid run type: {args.run_type}") From a1e1ba4574211093dd7d73b68df3f1f4dab1ba6b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:55:33 +0200 Subject: [PATCH 082/136] now? --- models/purple_alien/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index bed81cfa..45ae777e 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -56,15 +56,14 @@ def handle_sweep_run(args): def handle_single_run(args): - # get run type and denoting project name - check convention! - run_type = args.run_type - project = f"purple_alien_{run_type}" - # get hyperparameters hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type + hyperparameters['run_type'] = args.run_type hyperparameters['sweep'] = False + # get run type and denoting project name - check convention! + project = f"purple_alien_{args.run_type}" + if args.run_type == 'calibration' or args.run_type == 'testing': model_pipeline(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) From 55a8b6725e7de491aa4cce76686bd9c8258b4376 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 14:59:47 +0200 Subject: [PATCH 083/136] forecastin error --- models/purple_alien/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 45ae777e..a2e415a8 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -158,9 +158,10 @@ def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=N print('Done testing') -def handle_forecasting(args): +# could be better... +def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): - run_type = args.run_type + run_type = "forecasting" project = f"purple_alien_{run_type}" hyperparameters = get_hp_config() hyperparameters['run_type'] = run_type @@ -294,7 +295,7 @@ def model_pipeline(config = None, project = None, train = None, eval = None, for # --------------------------------------------------------------------- if forecast: - handle_forecasting(config, device, views_vol, PATH_ARTIFACTS) + handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) #raise NotImplementedError('Forecasting not implemented yet') #print('Done forecasting') From a3d24c376b17964d1b8d6127cdad9ac5c4180c8e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 15:00:46 +0200 Subject: [PATCH 084/136] sweep to see if error also there... --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 92b7b854..40974fb8 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 600}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' :128}, # 128 for actual testing, 10 for debug + 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From 7b27e6eeb1021094aec9d8528e43fd9b9845e911 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 15:29:35 +0200 Subject: [PATCH 085/136] added debug print --- models/purple_alien/src/utils/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/models/purple_alien/src/utils/utils.py b/models/purple_alien/src/utils/utils.py index f70cdeba..d10f3ef5 100644 --- a/models/purple_alien/src/utils/utils.py +++ b/models/purple_alien/src/utils/utils.py @@ -208,6 +208,8 @@ def get_data(config): try: file_name = f'/{run_type}_vol.npy' # NOT WINDOWS FRIENDLY + # debug print + print(f'Loading {run_type} data from {file_name}...') views_vol = np.load(str(PATH_PROCESSED) + file_name) except FileNotFoundError as e: From f01855580d14389558967841453fc156f0c474f0 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Thu, 30 May 2024 16:49:41 +0200 Subject: [PATCH 086/136] larger test... --- models/purple_alien/configs/config_hyperparameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index a67683a1..0c59cbdb 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 10, # 600 for actual trainnig, 10 for debug + 'samples': 100, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, From 8b325eda17d98f982e385dc002f22f87cc719649 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 03:23:06 +0200 Subject: [PATCH 087/136] full run --- models/purple_alien/configs/config_sweep.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 40974fb8..92b7b854 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -17,7 +17,7 @@ def get_swep_config(): 'scheduler' : {'value': 'WarmupDecay'}, #CosineAnnealingLR004 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels': {'value': 32}, # you like need 32, it seems from qualitative results 'min_events': {'value': 5}, - 'samples': {'value': 10}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... + 'samples': {'value': 600}, # 600 for run 10 for debug. should be a function of batches becaus batch 3 and sample 1000 = 3000.... 'batch_size': {'value': 3}, # just speed running here.. "dropout_rate" : {'value' : 0.125}, 'learning_rate': {'value' : 0.001}, #0.001 default, but 0.005 might be better @@ -33,7 +33,7 @@ def get_swep_config(): 'loss_reg' : { 'value' : 'b'}, 'loss_reg_a' : { 'value' : 256}, 'loss_reg_c' : { 'value' : 0.001}, - 'test_samples': { 'value' :10}, # 128 for actual testing, 10 for debug + 'test_samples': { 'value' :128}, # 128 for actual testing, 10 for debug 'np_seed' : {'values' : [4,8]}, 'torch_seed' : {'values' : [4,8]}, 'window_dim' : {'value' : 32}, From 496351ae29de1af0a8806e8588956b21554ace81 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 14:39:22 +0200 Subject: [PATCH 088/136] added handle_training.py --- .../purple_alien/src/training/train_model.py | 87 ++++++------------- 1 file changed, 25 insertions(+), 62 deletions(-) diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index 401aa61d..9ffd3551 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -3,7 +3,7 @@ import time import os import functools - +from datetime import datetime import torch import torch.nn as nn import torch.nn.functional as F @@ -143,64 +143,27 @@ def training_loop(config, model, criterion, optimizer, scheduler, views_vol, dev print('training done...') -# MOVE TO NEW main.py IN purple_alien root. -# def model_pipeline(config = None, project = None): -# -# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# print(device) -# -# # tell wandb to get started -# with wandb.init(project=project, entity="nornir", config=config): # project and config ignored when runnig a sweep -# -# wandb.define_metric("monthly/out_sample_month") -# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") -# -# # access all HPs through wandb.config, so logging matches execution! -# config = wandb.config -# -# views_vol = get_data(config) -# -# # make the model, data, and optimization problem -# model, criterion, optimizer, scheduler = make(config, device) -# -# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) -# print('Done training') -# -# return(model) -# -# -# if __name__ == "__main__": -# -# wandb.login() -# -# # model type is still a vary bad name here - it should be something like run_type... Change later! -# model_type_dict = {'a' : 'calibration', 'b' : 'testing', 'c' : 'forecasting'} -# model_type = model_type_dict[input("a) Calibration\nb) Testing\nc) Forecasting\n")] -# print(f'Run type: {model_type}\n') -# -# project = f"imp_new_structure_{model_type}" # temp. also a bad name. Change later! -# -# hyperparameters = get_hp_config() -# -# hyperparameters['model_type'] = model_type # bad name... ! Change later! -# hyperparameters['sweep'] = False -# -# start_t = time.time() -# -# model = model_pipeline(config = hyperparameters, project = project) -# -# PATH_ARTIFACTS = setup_artifacts_paths(PATH) -# -# # create the artifacts folder if it does not exist -# os.makedirs(PATH_ARTIFACTS, exist_ok=True) -# -# # save the model -# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, f"{model_type}_model.pt") -# torch.save(model, PATH_MODEL_ARTIFACT) -# -# print(f"Model saved as: {PATH_MODEL_ARTIFACT}") -# -# end_t = time.time() -# minutes = (end_t - start_t)/60 -# print(f'Done. Runtime: {minutes:.3f} minutes') -# \ No newline at end of file + +def handle_training(config, device, views_vol, PATH_ARTIFACTS): + + # Create the model, criterion, optimizer and scheduler + model, criterion, optimizer, scheduler = make(config, device) + + # Train the model + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') + + # just in case the artifacts folder does not exist + os.makedirs(PATH_ARTIFACTS, exist_ok=True) + + # Define the path for the artifacts with a timestamp and a run type + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + model_filename = f"{config.run_type}_model_{timestamp}.pt" + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) + + # save the model + torch.save(model, PATH_MODEL_ARTIFACT) + + # done + print(f"Model saved as: {PATH_MODEL_ARTIFACT}") + From fcd44f950f1facfbe769af938d0deff1b00c1a8c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 14:40:58 +0200 Subject: [PATCH 089/136] added handle_evaluation --- .../src/offline_evaluation/evaluate_model.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 6a59e5f2..bcee4fc1 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -121,12 +121,57 @@ def evaluate_posterior(model, views_vol, config, device): else: print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') - + # could be a function in utils_wandb.... wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) + +def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): + + # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type + if artifact_name: + print(f"Using (non-default) artifact: {artifact_name}") + + # If it lacks the file extension, add it + if not artifact_name.endswith('.pt'): + artifact_name += '.pt' + + # Define the full (model specific) path for the artifact + PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + + else: + # use the latest model artifact based on the run type + print(f"Using latest (default) run type ({config.run_type}) specific artifact") + + # Get the latest model artifact based on the run type and the (models specific) artifacts path + PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) + + # Check if the model artifact exists - if not, raise an error + if not os.path.exists(PATH_MODEL_ARTIFACT): + raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + + # load the model + model = torch.load(PATH_MODEL_ARTIFACT) + + # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py + model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] + + # print for debugging + print(f"model_time_stamp: {model_time_stamp}") + + # add to config for logging and conciseness + config.model_time_stamp = model_time_stamp + + # evaluate the model posterior distribution + evaluate_posterior(model, views_vol, config, device) + + # done. + print('Done testing') + + + # note: # Going with the argparser, there is less of a clear reason to have to separate .py files for evaluation sweeps and single models. I think. Let me know if you disagree. # naturally its a question of generalization and reusability, and i could see I had a lot of copy paste code between the two scripts. \ No newline at end of file From 1b7c3f14bba4294359dc326a4cf0bf35e1f0d0ca Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 14:42:23 +0200 Subject: [PATCH 090/136] moved handle_forecast here --- .../src/forecasting/generate_forcast.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/forecasting/generate_forcast.py b/models/purple_alien/src/forecasting/generate_forcast.py index 951620bb..910977fe 100644 --- a/models/purple_alien/src/forecasting/generate_forcast.py +++ b/models/purple_alien/src/forecasting/generate_forcast.py @@ -45,7 +45,11 @@ def generate_forecast(model, views_vol, config, device, PATH): model.apply(apply_dropout) # Generate posterior samples and out-of-sample volumes - posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) + posterior_list, posterior_list_class, out_of_sample_vol, _ = sample_posterior(model, views_vol, config, device) # the _ is the full tensor. + + # I suspect you'll need the out_of_sample_vol to create the df (it has pg and ocean info) + # However, I see in the test_prediction_store notebook in "conflictnet" repo that I load the "calibration_vol" from the pickle file.... Investigate... + # Set up paths for storing generated data _, _, PATH_GENERATED = setup_data_paths(PATH) @@ -60,7 +64,7 @@ def generate_forecast(model, views_vol, config, device, PATH): posterior_dict = { 'posterior_list': posterior_list, 'posterior_list_class': posterior_list_class, - 'out_of_sample_vol': out_of_sample_vol + 'out_of_sample_vol': out_of_sample_vol # you might need this for the df creation before predstore. Experiments in notebook test_to_prediction_store.ipynb } # Save the posterior data to a pickle file @@ -70,6 +74,15 @@ def generate_forecast(model, views_vol, config, device, PATH): print('Posterior dict and test vol pickled and dumped!') + +def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): + + # the thing above might work, but it needs to be tested thoroughly.... + raise NotImplementedError('Forecasting not implemented yet') + + + + # Ensure utils_prediction.py and any other dependencies are imported correctly # from utils_prediction import sample_posterior, apply_dropout # from utils_data import setup_data_paths From 8608b31330fcf8255efbe627c0ea1b49b8df5c77 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 14:43:04 +0200 Subject: [PATCH 091/136] moved handle functions --- models/purple_alien/main.py | 337 +++++++++--------------------------- 1 file changed, 85 insertions(+), 252 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index a2e415a8..d0007756 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -20,25 +20,27 @@ setup_project_paths(PATH) from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils_wandb import add_wandb_monthly_metrics +from utils_device import setup_device from config_sweep import get_swep_config from config_hyperparameters import get_hp_config -from train_model import make, training_loop +from train_model import make, training_loop, handle_training # from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... from evaluate_model import evaluate_posterior from cli_parser_utils import parse_args, validate_arguments from artifacts_utils import get_latest_model_artifact -def setup_device(): - # set the device - device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - print(f"Using device: {device}") - -def add_wandb_monthly_metrics(): - - # Define "new" monthly metrics for WandB logging - wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") +#def setup_device(): +# # set the device +# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') +# print(f"Using device: {device}") +# +#def add_wandb_monthly_metrics(): +# +# # Define "new" monthly metrics for WandB logging +# wandb.define_metric("monthly/out_sample_month") +# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") def handle_sweep_run(args): @@ -56,7 +58,7 @@ def handle_sweep_run(args): def handle_single_run(args): - # get hyperparameters + # get hyperparameters. IS THE ISSUE UP HERE? hyperparameters = get_hp_config() hyperparameters['run_type'] = args.run_type hyperparameters['sweep'] = False @@ -68,20 +70,6 @@ def handle_single_run(args): model_pipeline(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) -# if args.train: -# print(f"Training one model for run type: {run_type} and saving it as an artifact...") -# model_pipeline(args, config = hyperparameters, project = project, train=True) -# -# if args.evaluate: -# print(f"Evaluating model for run type: {run_type}...") -# model_pipeline(args, config = hyperparameters, project = project, eval=True) - - #if args.artifact_name is not None: - # model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) - - #else: -# model_pipeline(config = hyperparameters, project = project, eval=True) - elif args.run_type == 'forecasting': #print('True forecasting ->->->->') @@ -91,120 +79,92 @@ def handle_single_run(args): raise ValueError(f"Invalid run type: {args.run_type}") -def handle_training(config, device, views_vol, PATH_ARTIFACTS): - - # Create the model, criterion, optimizer and scheduler - model, criterion, optimizer, scheduler = make(config, device) - - # Train the model - training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - print('Done training') - - # just in case the artifacts folder does not exist - os.makedirs(PATH_ARTIFACTS, exist_ok=True) - - # Define the path for the artifacts with a timestamp and a run type - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - model_filename = f"{config.run_type}_model_{timestamp}.pt" - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) - - # save the model - torch.save(model, PATH_MODEL_ARTIFACT) - - # done - print(f"Model saved as: {PATH_MODEL_ARTIFACT}") - - -def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): - - # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type - if artifact_name: - print(f"Using (non-default) artifact: {artifact_name}") - - # If it lacks the file extension, add it - if not artifact_name.endswith('.pt'): - artifact_name += '.pt' - - # Define the full (model specific) path for the artifact - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) - - else: - # use the latest model artifact based on the run type - print(f"Using latest (default) run type ({config.run_type}) specific artifact") - - # Get the latest model artifact based on the run type and the (models specific) artifacts path - PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) - - # Check if the model artifact exists - if not, raise an error - if not os.path.exists(PATH_MODEL_ARTIFACT): - raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") - - # load the model - model = torch.load(PATH_MODEL_ARTIFACT) - - # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py - model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] - - # print for debugging - print(f"model_time_stamp: {model_time_stamp}") - - # add to config for logging and conciseness - config.model_time_stamp = model_time_stamp - - # evaluate the model posterior distribution - evaluate_posterior(model, views_vol, config, device) - - # done. - print('Done testing') +#def handle_training(config, device, views_vol, PATH_ARTIFACTS): +# +# # Create the model, criterion, optimizer and scheduler +# model, criterion, optimizer, scheduler = make(config, device) +# +# # Train the model +# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) +# print('Done training') +# +# # just in case the artifacts folder does not exist +# os.makedirs(PATH_ARTIFACTS, exist_ok=True) +# +# # Define the path for the artifacts with a timestamp and a run type +# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") +# model_filename = f"{config.run_type}_model_{timestamp}.pt" +# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) +# +# # save the model +# torch.save(model, PATH_MODEL_ARTIFACT) +# +# # done +# print(f"Model saved as: {PATH_MODEL_ARTIFACT}") +# +#def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +# +# # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type +# if artifact_name: +# print(f"Using (non-default) artifact: {artifact_name}") +# +# # If it lacks the file extension, add it +# if not artifact_name.endswith('.pt'): +# artifact_name += '.pt' +# +# # Define the full (model specific) path for the artifact +# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) +# +# else: +# # use the latest model artifact based on the run type +# print(f"Using latest (default) run type ({config.run_type}) specific artifact") +# +# # Get the latest model artifact based on the run type and the (models specific) artifacts path +# PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) +# +# # Check if the model artifact exists - if not, raise an error +# if not os.path.exists(PATH_MODEL_ARTIFACT): +# raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") +# +# # load the model +# model = torch.load(PATH_MODEL_ARTIFACT) +# +# # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py +# model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] +# +# # print for debugging +# print(f"model_time_stamp: {model_time_stamp}") +# +# # add to config for logging and conciseness +# config.model_time_stamp = model_time_stamp +# +# # evaluate the model posterior distribution +# evaluate_posterior(model, views_vol, config, device) +# +# # done. +# print('Done testing') +# # could be better... -def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): - - run_type = "forecasting" - project = f"purple_alien_{run_type}" - hyperparameters = get_hp_config() - hyperparameters['run_type'] = run_type - hyperparameters['sweep'] = False - - if args.artifact_name is not None: - model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) - - else: - model_pipeline(config = hyperparameters, project = project) - - raise NotImplementedError('Forecasting not implemented yet') - - #print('Done forecasting') - - - # but right now there is no forecasting implemented in model pipeline..... - - - - +#def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +# +# raise NotImplementedError('Forecasting not implemented yet') +# -# ----------------- Model Pipeline ----------------- NOW THIS IS TOO BIG... def model_pipeline(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): # Define the path for the artifacts PATH_ARTIFACTS = setup_artifacts_paths(PATH) - # Set the device - #device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') - #print(f"Using device: {device}") - device = setup_device() # Initialize WandB with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep - # Define "new" monthly metrics for WandB logging - #wandb.define_metric("monthly/out_sample_month") - #wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - - add_wandb_monthly_metrics() # add the monthly metrics to WandB + # add the monthly metrics to WandB + add_wandb_monthly_metrics() # Update config from WandB initialization above config = wandb.config @@ -225,80 +185,13 @@ def model_pipeline(config = None, project = None, train = None, eval = None, for # Handle the single model runs: train and save the model as an artifact if train: handle_training(config, device, views_vol, PATH_ARTIFACTS) - # # All wandb logging is done in the training loop. - # - # # Create the model, criterion, optimizer and scheduler - # model, criterion, optimizer, scheduler = make(config, device) - # training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - # print('Done training') - - # # create the artifacts folder if it does not exist - # os.makedirs(PATH_ARTIFACTS, exist_ok=True) - - # # Define the path for the artifacts with a timestamp and a run type - # timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - # model_filename = f"{config.run_type}_model_{timestamp}.pt" - # PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) - - # # save the model - # torch.save(model, PATH_MODEL_ARTIFACT) - - # # Currently the artifacts are only sotred locally. Putting them on WandB is a good idea, but I need to understand thier model storage better first. - - # print(f"Model saved as: {PATH_MODEL_ARTIFACT}") - # #return model # dont return anything, the model is saved as an artifact # Handle the single model runs: evaluate a trained model (artifact) if eval: handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) - # # Determine the artifact path: - # # If an artifact name is provided, use it. Otherwise, get the latest model artifact based on the run type - # if artifact_name is not None: - - # # pritn statement for debugging - # print(f"Using (non default) artifact: {artifact_name}") - # - # # Check if the artifact name has the correct file extension - # if not artifact_name.endswith('.pt'): - # artifact_name += '.pt' - - # # Define the full (model specific) path for the artifact - # PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) - # - # else: - # # print statement for debugging - # print(f"Using lastest (default) run type ({config.run_type}) specific artifact") - - # # Get the latest model artifact based on the run type and the (models specific) artifacts path - # PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) - - # # Check if the model artifact exists - if not, raise an error - # if not os.path.exists(PATH_MODEL_ARTIFACT): - # raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") - - # # load the model - # model = torch.load(PATH_MODEL_ARTIFACT) - # #model.eval() # this is done in the evaluate_posterior function - # - # # Get the excact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py - # model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] - - # # debug print statement - # print(f"model_time_stamp: {model_time_stamp}") - - # # save to config for logging and concisness - # config.model_time_stamp = model_time_stamp - - # evaluate_posterior(model, views_vol, config, device) - # print('Done testing') - -# --------------------------------------------------------------------- if forecast: handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) - #raise NotImplementedError('Forecasting not implemented yet') - #print('Done forecasting') - if __name__ == "__main__": @@ -320,76 +213,16 @@ def model_pipeline(config = None, project = None, train = None, eval = None, for handle_sweep_run(args) - # print('Running sweep...') - - # project = f"purple_alien_sweep" # check naming convention - # sweep_config = get_swep_config() - # sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep - # sweep_config['parameters']['sweep'] = {'value' : True} - - # sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - - # wandb.agent(sweep_id, model_pipeline) - - elif args.sweep == False: handle_single_run(args) - -# print('Running single model operation...') -# run_type = args.run_type -# project = f"purple_alien_{run_type}" -# hyperparameters = get_hp_config() -# hyperparameters['run_type'] = run_type # this is also how the forecast if statement is informed below -# hyperparameters['sweep'] = False -# -# # if train is flagged, train the model and save it as an artifact -# if args.train: -# print(f"Training one model for run type: {run_type} and saving it as an artifact...") -# model_pipeline(config = hyperparameters, project = project, train=True) -# -# # if evaluate is flagged, evaluate the model -# if args.evaluate: -# print(f"Evaluating model for run type: {run_type}...") -# -# # if an artifact name is provided, use it. -# if args.artifact_name is not None: -# model_pipeline(config = hyperparameters, project = project, eval=True, artifact_name=args.artifact_name) -# -# # Otherwise, get the default - I.e. latest model artifact give the specific run type -# else: -# model_pipeline(config = hyperparameters, project = project, eval=True) - - # I guess you also need some kind of forecasting here... - #if args.run_type == 'forecasting': - - # handle_single_run(args) - -# print('True forecasting ->->->->') -# -# # if an artifact name is provided, use it. -# if args.artifact_name is not None: -# model_pipeline(config = hyperparameters, project = project, artifact_name=args.artifact_name) -# -# # Otherwise, get the default - I.e. latest model artifact give the specific run type -# else: -# model_pipeline(config = hyperparameters, project = project) -# - - - # notes: - # should always be a trained artifact? - # should always de the last artifact? - end_t = time.time() minutes = (end_t - start_t)/60 print(f'Done. Runtime: {minutes:.3f} minutes') - - # notes on stepshifted models: # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. # It is not a big issue, but it is something to consider os we don't do something headless. From bf04fdb781785dd5663ab6c29b5b282042409897 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 15:10:41 +0200 Subject: [PATCH 092/136] migrate code co modular scripts --- models/purple_alien/main.py | 190 +++++++++--------- .../src/utils/model_run_handlers.py | 50 +++++ .../src/utils/model_run_manager.py | 60 ++++++ models/purple_alien/src/utils/utils_device.py | 7 + models/purple_alien/src/utils/utils_wandb.py | 9 + 5 files changed, 221 insertions(+), 95 deletions(-) create mode 100644 models/purple_alien/src/utils/model_run_handlers.py create mode 100644 models/purple_alien/src/utils/model_run_manager.py create mode 100644 models/purple_alien/src/utils/utils_device.py create mode 100644 models/purple_alien/src/utils/utils_wandb.py diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index d0007756..20b2594f 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -1,13 +1,13 @@ -import numpy as np -import pickle +#import numpy as np +#import pickle import time -import os -import functools -from datetime import datetime +#import os +#import functools +#from datetime import datetime -import torch -import torch.nn as nn -import torch.nn.functional as F +#import torch +#import torch.nn as nn +#import torch.nn.functional as F import wandb @@ -19,17 +19,19 @@ from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) -from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data -from utils_wandb import add_wandb_monthly_metrics -from utils_device import setup_device -from config_sweep import get_swep_config -from config_hyperparameters import get_hp_config -from train_model import make, training_loop, handle_training +#from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +#from utils_wandb import add_wandb_monthly_metrics +#from utils_device import setup_device +#from config_sweep import get_swep_config +#from config_hyperparameters import get_hp_config +#from train_model import make, training_loop, handle_training # from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... -from evaluate_model import evaluate_posterior +#from evaluate_model import evaluate_posterior, handle_evaluation +#from forecast_model import handle_forecasting from cli_parser_utils import parse_args, validate_arguments -from artifacts_utils import get_latest_model_artifact +#from artifacts_utils import get_latest_model_artifact +#from mode_run_manager import model_run_manager #def setup_device(): # # set the device @@ -43,42 +45,42 @@ # wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") -def handle_sweep_run(args): - print('Running sweep...') - - project = f"purple_alien_sweep" # check naming convention - sweep_config = get_swep_config() - sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep - sweep_config['parameters']['sweep'] = {'value' : True} - - sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - - wandb.agent(sweep_id, model_pipeline) - - -def handle_single_run(args): - - # get hyperparameters. IS THE ISSUE UP HERE? - hyperparameters = get_hp_config() - hyperparameters['run_type'] = args.run_type - hyperparameters['sweep'] = False - - # get run type and denoting project name - check convention! - project = f"purple_alien_{args.run_type}" - - if args.run_type == 'calibration' or args.run_type == 'testing': - - model_pipeline(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) - - elif args.run_type == 'forecasting': - - #print('True forecasting ->->->->') - model_pipeline(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) - - else: - raise ValueError(f"Invalid run type: {args.run_type}") - - +# def handle_sweep_run(args): +# print('Running sweep...') +# +# project = f"purple_alien_sweep" # check naming convention +# sweep_config = get_swep_config() +# sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep +# sweep_config['parameters']['sweep'] = {'value' : True} +# +# sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name +# +# wandb.agent(sweep_id, model_run_manager) +# +# +# def handle_single_run(args): +# +# # get hyperparameters. IS THE ISSUE UP HERE? +# hyperparameters = get_hp_config() +# hyperparameters['run_type'] = args.run_type +# hyperparameters['sweep'] = False +# +# # get run type and denoting project name - check convention! +# project = f"purple_alien_{args.run_type}" +# +# if args.run_type == 'calibration' or args.run_type == 'testing': +# +# model_run_manager(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) +# +# elif args.run_type == 'forecasting': +# +# #print('True forecasting ->->->->') +# model_run_manager(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) +# +# else: +# raise ValueError(f"Invalid run type: {args.run_type}") +# +# #def handle_training(config, device, views_vol, PATH_ARTIFACTS): # # # Create the model, criterion, optimizer and scheduler @@ -150,49 +152,48 @@ def handle_single_run(args): #def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): # # raise NotImplementedError('Forecasting not implemented yet') -# - - -def model_pipeline(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): - - # Define the path for the artifacts - PATH_ARTIFACTS = setup_artifacts_paths(PATH) - - device = setup_device() - - # Initialize WandB - with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep - - # add the monthly metrics to WandB - add_wandb_monthly_metrics() - - # Update config from WandB initialization above - config = wandb.config - - # Retrieve data (partition) based on the configuration - views_vol = get_data(config) - - # Handle the sweep runs - if config.sweep: # If we are running a sweep, always train and evaluate - - model, criterion, optimizer, scheduler = make(config, device) - training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) - print('Done training') - - evaluate_posterior(model, views_vol, config, device) - print('Done testing') - - # Handle the single model runs: train and save the model as an artifact - if train: - handle_training(config, device, views_vol, PATH_ARTIFACTS) - - # Handle the single model runs: evaluate a trained model (artifact) - if eval: - handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) - - if forecast: - handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) +# +#def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): +# +# # Define the path for the artifacts +# PATH_ARTIFACTS = setup_artifacts_paths(PATH) +# +# device = setup_device() +# +# # Initialize WandB +# with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep +# +# # add the monthly metrics to WandB +# add_wandb_monthly_metrics() +# +# # Update config from WandB initialization above +# config = wandb.config +# +# # Retrieve data (partition) based on the configuration +# views_vol = get_data(config) +# +# # Handle the sweep runs +# if config.sweep: # If we are running a sweep, always train and evaluate +# +# model, criterion, optimizer, scheduler = make(config, device) +# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) +# print('Done training') +# +# evaluate_posterior(model, views_vol, config, device) +# print('Done testing') +# +# # Handle the single model runs: train and save the model as an artifact +# if train: +# handle_training(config, device, views_vol, PATH_ARTIFACTS) +# +# # Handle the single model runs: evaluate a trained model (artifact) +# if eval: +# handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) +# +# if forecast: +# handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) +# if __name__ == "__main__": @@ -212,7 +213,6 @@ def model_pipeline(config = None, project = None, train = None, eval = None, for if args.sweep == True: handle_sweep_run(args) - elif args.sweep == False: diff --git a/models/purple_alien/src/utils/model_run_handlers.py b/models/purple_alien/src/utils/model_run_handlers.py new file mode 100644 index 00000000..8238f801 --- /dev/null +++ b/models/purple_alien/src/utils/model_run_handlers.py @@ -0,0 +1,50 @@ +import wandb + +import sys +from pathlib import Path + +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths, setup_artifacts_paths +setup_project_paths(PATH) + +from config_sweep import get_swep_config +from config_hyperparameters import get_hp_config +from mode_run_manager import model_run_manager + + +def handle_sweep_run(args): + print('Running sweep...') + + project = f"purple_alien_sweep" # check naming convention + sweep_config = get_swep_config() + sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep + sweep_config['parameters']['sweep'] = {'value' : True} + + sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name + + wandb.agent(sweep_id, model_run_manager) + + +def handle_single_run(args): + + # get hyperparameters. IS THE ISSUE UP HERE? + hyperparameters = get_hp_config() + hyperparameters['run_type'] = args.run_type + hyperparameters['sweep'] = False + + # get run type and denoting project name - check convention! + project = f"purple_alien_{args.run_type}" + + if args.run_type == 'calibration' or args.run_type == 'testing': + + model_run_manager(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) + + elif args.run_type == 'forecasting': + + #print('True forecasting ->->->->') + model_run_manager(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) + + else: + raise ValueError(f"Invalid run type: {args.run_type}") + diff --git a/models/purple_alien/src/utils/model_run_manager.py b/models/purple_alien/src/utils/model_run_manager.py new file mode 100644 index 00000000..16973dde --- /dev/null +++ b/models/purple_alien/src/utils/model_run_manager.py @@ -0,0 +1,60 @@ + +import wandb + +import sys +from pathlib import Path + +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths, setup_artifacts_paths +setup_project_paths(PATH) + +from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data +from utils_wandb import add_wandb_monthly_metrics +from utils_device import setup_device +from train_model import make, training_loop, handle_training +# from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... +from evaluate_model import evaluate_posterior, handle_evaluation +from forecast_model import handle_forecasting + + +def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): + + # Define the path for the artifacts + PATH_ARTIFACTS = setup_artifacts_paths(PATH) + + device = setup_device() + + # Initialize WandB + with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep + + # add the monthly metrics to WandB + add_wandb_monthly_metrics() + + # Update config from WandB initialization above + config = wandb.config + + # Retrieve data (partition) based on the configuration + views_vol = get_data(config) + + # Handle the sweep runs + if config.sweep: # If we are running a sweep, always train and evaluate + + model, criterion, optimizer, scheduler = make(config, device) + training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) + print('Done training') + + evaluate_posterior(model, views_vol, config, device) + print('Done testing') + + # Handle the single model runs: train and save the model as an artifact + if train: + handle_training(config, device, views_vol, PATH_ARTIFACTS) + + # Handle the single model runs: evaluate a trained model (artifact) + if eval: + handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + + if forecast: + handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + diff --git a/models/purple_alien/src/utils/utils_device.py b/models/purple_alien/src/utils/utils_device.py new file mode 100644 index 00000000..26f6a9f5 --- /dev/null +++ b/models/purple_alien/src/utils/utils_device.py @@ -0,0 +1,7 @@ +import torch + +def setup_device(): + # Set the device + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + print(f"Using device: {device}") + return device # not sure you need to return it, but it might be useful for debugging diff --git a/models/purple_alien/src/utils/utils_wandb.py b/models/purple_alien/src/utils/utils_wandb.py new file mode 100644 index 00000000..8859f098 --- /dev/null +++ b/models/purple_alien/src/utils/utils_wandb.py @@ -0,0 +1,9 @@ +import wandb + +# there are things in other utils that should be here... + +def add_wandb_monthly_metrics(): + + # Define "new" monthly metrics for WandB logging + wandb.define_metric("monthly/out_sample_month") + wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") \ No newline at end of file From 9a09b0affa322fecb1d55eacba54e5d7e0f55870 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 15:13:19 +0200 Subject: [PATCH 093/136] imported handlers --- models/purple_alien/main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 20b2594f..1bca6699 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -31,6 +31,9 @@ from cli_parser_utils import parse_args, validate_arguments #from artifacts_utils import get_latest_model_artifact +from model_run_handlers import handle_sweep_run, handle_single_run + + #from mode_run_manager import model_run_manager #def setup_device(): From 3068ce308b35225680a7cc839530c3bf41a725f2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 15:14:40 +0200 Subject: [PATCH 094/136] corrected imprt --- models/purple_alien/src/utils/model_run_handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/utils/model_run_handlers.py b/models/purple_alien/src/utils/model_run_handlers.py index 8238f801..455c77ca 100644 --- a/models/purple_alien/src/utils/model_run_handlers.py +++ b/models/purple_alien/src/utils/model_run_handlers.py @@ -10,7 +10,7 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config -from mode_run_manager import model_run_manager +from model_run_manager import model_run_manager def handle_sweep_run(args): From f9dd01d2e7f153f0c68ce9cc1baa2c2fd91edfdc Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 15:15:44 +0200 Subject: [PATCH 095/136] corrected import --- models/purple_alien/src/utils/model_run_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/utils/model_run_manager.py b/models/purple_alien/src/utils/model_run_manager.py index 16973dde..17ed2e5c 100644 --- a/models/purple_alien/src/utils/model_run_manager.py +++ b/models/purple_alien/src/utils/model_run_manager.py @@ -15,7 +15,7 @@ from train_model import make, training_loop, handle_training # from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... from evaluate_model import evaluate_posterior, handle_evaluation -from forecast_model import handle_forecasting +from generate_forecast import handle_forecasting def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): From 5a9fc94e1879200e49fce92aa190fc37cb5d97c4 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 31 May 2024 15:16:30 +0200 Subject: [PATCH 096/136] corrected script name --- .../src/forecasting/{generate_forcast.py => generate_forecast.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/purple_alien/src/forecasting/{generate_forcast.py => generate_forecast.py} (100%) diff --git a/models/purple_alien/src/forecasting/generate_forcast.py b/models/purple_alien/src/forecasting/generate_forecast.py similarity index 100% rename from models/purple_alien/src/forecasting/generate_forcast.py rename to models/purple_alien/src/forecasting/generate_forecast.py From 1bd47fa244fa4b9affae68403ab311c83cc80ec1 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Sat, 1 Jun 2024 15:49:06 +0200 Subject: [PATCH 097/136] full run --- models/purple_alien/configs/config_hyperparameters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index 0c59cbdb..d2c7ec90 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 100, # 600 for actual trainnig, 10 for debug + 'samples': 600, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, @@ -24,7 +24,7 @@ def get_hp_config(): 'loss_reg': 'b', 'loss_reg_a' : 258, 'loss_reg_c' : 0.001, # 0.05 works... - 'test_samples': 10, # 128 for actual testing, 10 for debug + 'test_samples': 128, # 128 for actual testing, 10 for debug 'np_seed' : 4, 'torch_seed' : 4, 'window_dim' : 32, From bf6d241c15ecfbbbf0bbc7bafdae696c97b72cd9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Sat, 1 Jun 2024 19:30:49 +0200 Subject: [PATCH 098/136] added function --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index bcee4fc1..d6b36e73 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -29,6 +29,7 @@ from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from utils_prediction import predict, sample_posterior +from artifacts_utils import get_latest_model_artifact from config_sweep import get_swep_config from config_hyperparameters import get_hp_config From 9b3d51ca942b0335bee0644f7e8a7f74770f1208 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 10:13:34 +0200 Subject: [PATCH 099/136] removed comments --- models/purple_alien/main.py | 184 +----------------------------------- 1 file changed, 2 insertions(+), 182 deletions(-) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 1bca6699..2f3594fe 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -1,13 +1,4 @@ -#import numpy as np -#import pickle import time -#import os -#import functools -#from datetime import datetime - -#import torch -#import torch.nn as nn -#import torch.nn.functional as F import wandb @@ -19,185 +10,13 @@ from set_path import setup_project_paths, setup_artifacts_paths setup_project_paths(PATH) -#from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data -#from utils_wandb import add_wandb_monthly_metrics -#from utils_device import setup_device -#from config_sweep import get_swep_config -#from config_hyperparameters import get_hp_config -#from train_model import make, training_loop, handle_training -# from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... -#from evaluate_model import evaluate_posterior, handle_evaluation -#from forecast_model import handle_forecasting from cli_parser_utils import parse_args, validate_arguments #from artifacts_utils import get_latest_model_artifact from model_run_handlers import handle_sweep_run, handle_single_run - #from mode_run_manager import model_run_manager -#def setup_device(): -# # set the device -# device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -# print(f"Using device: {device}") -# -#def add_wandb_monthly_metrics(): -# -# # Define "new" monthly metrics for WandB logging -# wandb.define_metric("monthly/out_sample_month") -# wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") - - -# def handle_sweep_run(args): -# print('Running sweep...') -# -# project = f"purple_alien_sweep" # check naming convention -# sweep_config = get_swep_config() -# sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep -# sweep_config['parameters']['sweep'] = {'value' : True} -# -# sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name -# -# wandb.agent(sweep_id, model_run_manager) -# -# -# def handle_single_run(args): -# -# # get hyperparameters. IS THE ISSUE UP HERE? -# hyperparameters = get_hp_config() -# hyperparameters['run_type'] = args.run_type -# hyperparameters['sweep'] = False -# -# # get run type and denoting project name - check convention! -# project = f"purple_alien_{args.run_type}" -# -# if args.run_type == 'calibration' or args.run_type == 'testing': -# -# model_run_manager(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) -# -# elif args.run_type == 'forecasting': -# -# #print('True forecasting ->->->->') -# model_run_manager(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) -# -# else: -# raise ValueError(f"Invalid run type: {args.run_type}") -# -# -#def handle_training(config, device, views_vol, PATH_ARTIFACTS): -# -# # Create the model, criterion, optimizer and scheduler -# model, criterion, optimizer, scheduler = make(config, device) -# -# # Train the model -# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) -# print('Done training') -# -# # just in case the artifacts folder does not exist -# os.makedirs(PATH_ARTIFACTS, exist_ok=True) -# -# # Define the path for the artifacts with a timestamp and a run type -# timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") -# model_filename = f"{config.run_type}_model_{timestamp}.pt" -# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, model_filename) -# -# # save the model -# torch.save(model, PATH_MODEL_ARTIFACT) -# -# # done -# print(f"Model saved as: {PATH_MODEL_ARTIFACT}") -# - -#def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): -# -# # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type -# if artifact_name: -# print(f"Using (non-default) artifact: {artifact_name}") -# -# # If it lacks the file extension, add it -# if not artifact_name.endswith('.pt'): -# artifact_name += '.pt' -# -# # Define the full (model specific) path for the artifact -# PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) -# -# else: -# # use the latest model artifact based on the run type -# print(f"Using latest (default) run type ({config.run_type}) specific artifact") -# -# # Get the latest model artifact based on the run type and the (models specific) artifacts path -# PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) -# -# # Check if the model artifact exists - if not, raise an error -# if not os.path.exists(PATH_MODEL_ARTIFACT): -# raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") -# -# # load the model -# model = torch.load(PATH_MODEL_ARTIFACT) -# -# # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py -# model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] -# -# # print for debugging -# print(f"model_time_stamp: {model_time_stamp}") -# -# # add to config for logging and conciseness -# config.model_time_stamp = model_time_stamp -# -# # evaluate the model posterior distribution -# evaluate_posterior(model, views_vol, config, device) -# -# # done. -# print('Done testing') -# - -# could be better... -#def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): -# -# raise NotImplementedError('Forecasting not implemented yet') - -# -#def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): -# -# # Define the path for the artifacts -# PATH_ARTIFACTS = setup_artifacts_paths(PATH) -# -# device = setup_device() -# -# # Initialize WandB -# with wandb.init(project=project, entity="views_pipeline", config=config): # project and config ignored when running a sweep -# -# # add the monthly metrics to WandB -# add_wandb_monthly_metrics() -# -# # Update config from WandB initialization above -# config = wandb.config -# -# # Retrieve data (partition) based on the configuration -# views_vol = get_data(config) -# -# # Handle the sweep runs -# if config.sweep: # If we are running a sweep, always train and evaluate -# -# model, criterion, optimizer, scheduler = make(config, device) -# training_loop(config, model, criterion, optimizer, scheduler, views_vol, device) -# print('Done training') -# -# evaluate_posterior(model, views_vol, config, device) -# print('Done testing') -# -# # Handle the single model runs: train and save the model as an artifact -# if train: -# handle_training(config, device, views_vol, PATH_ARTIFACTS) -# -# # Handle the single model runs: evaluate a trained model (artifact) -# if eval: -# handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) -# -# if forecast: -# handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) -# - if __name__ == "__main__": # new argpars solution. @@ -212,6 +31,8 @@ start_t = time.time() + # Test if and why a model_metadata_dict.py was saved in the artifacts folder.. + # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags if args.sweep == True: @@ -225,7 +46,6 @@ minutes = (end_t - start_t)/60 print(f'Done. Runtime: {minutes:.3f} minutes') - # notes on stepshifted models: # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. # It is not a big issue, but it is something to consider os we don't do something headless. From a5b94a5eec283eb4426cc4a9182a0b6ebeb0e48c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 10:22:13 +0200 Subject: [PATCH 100/136] get_data comment? --- models/purple_alien/src/utils/model_run_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/utils/model_run_manager.py b/models/purple_alien/src/utils/model_run_manager.py index 17ed2e5c..1b44dd2c 100644 --- a/models/purple_alien/src/utils/model_run_manager.py +++ b/models/purple_alien/src/utils/model_run_manager.py @@ -35,7 +35,7 @@ def model_run_manager(config = None, project = None, train = None, eval = None, config = wandb.config # Retrieve data (partition) based on the configuration - views_vol = get_data(config) + views_vol = get_data(config) # a bit HydraNet specific, but it is fine for now. If statment or move to handle_training, handle_evaluation, and handle_forecasting? # Handle the sweep runs if config.sweep: # If we are running a sweep, always train and evaluate From 0eb9afb72f882ffa440225456903356b8675b30f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 10:42:06 +0200 Subject: [PATCH 101/136] added comment --- models/purple_alien/src/utils/utils_dataloaders.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/purple_alien/src/utils/utils_dataloaders.py b/models/purple_alien/src/utils/utils_dataloaders.py index abd7f490..a6630f34 100644 --- a/models/purple_alien/src/utils/utils_dataloaders.py +++ b/models/purple_alien/src/utils/utils_dataloaders.py @@ -155,3 +155,6 @@ def process_partition_data(partition, get_views_date, df_to_vol, PATH): print('Done') return df, vol + + +# Should this be more general? \ No newline at end of file From 0b2c6b4d3f4940c6dfa8a265b1b0e65dbc9dfa16 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 13:17:06 +0200 Subject: [PATCH 102/136] fixed time_stamp? --- models/purple_alien/src/offline_evaluation/evaluate_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index d6b36e73..1554bfe4 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -110,7 +110,7 @@ def evaluate_posterior(model, views_vol, config, device): with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) - with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}{config.model_time_stamp}.pkl', 'wb') as file: + with open(f'{PATH_GENERATED}/metric_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(metric_dict, file) with open(f'{PATH_GENERATED}/test_vol_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: # make it numpy @@ -157,7 +157,7 @@ def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=N model = torch.load(PATH_MODEL_ARTIFACT) # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py - model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT).split('.')[0] + model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT)[-18:-3] # 18 is the length of the timestamp string, and -3 is to remove the .pt file extension. a bit hardcoded, but very simple and should not change. # print for debugging print(f"model_time_stamp: {model_time_stamp}") From 3abd39ba3406c933e65de5bac57311e750cb0b1c Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 13:22:42 +0200 Subject: [PATCH 103/136] added not on pickled files being overwritten... --- .../purple_alien/src/offline_evaluation/evaluate_model.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 1554bfe4..c711626e 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -107,6 +107,12 @@ def evaluate_posterior(model, views_vol, config, device): metric_dict = {'out_sample_month_list' : out_sample_month_list, 'mse_list': mse_list, 'ap_list' : ap_list, 'auc_list': auc_list, 'brier_list' : brier_list} + + # Note: we are using the model_time_stamp from the model artifact to denote the time stamp for the pkl files + # This is to ensure that the pkl files are easily identifiable and associated with the correct model artifact + # But it also means that running evaluation on the same model artifact multiple times will overwrite the pkl files + # I think this is fine, but we should think about cases where we might want to evaluate the same model artifact multiple times - maybe for robustiness checks or something for publication. + with open(f'{PATH_GENERATED}/posterior_dict_{config.time_steps}_{config.run_type}_{config.model_time_stamp}.pkl', 'wb') as file: pickle.dump(posterior_dict, file) From 835c6d75671b9668a3b40e96c272eb9f7ed7359b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 3 Jun 2024 13:27:08 +0200 Subject: [PATCH 104/136] note on print statement --- models/purple_alien/src/utils/utils_prediction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 49f19ce0..744c2bf6 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -62,6 +62,7 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): full_seq_len = seq_len -1 # we loop over the full sequence. you need -1 because you are predicting the next month. in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation + # These print staments are informative while the model is running, but the implementation is not optimal.... print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set. Full sequence length: {full_seq_len}', end= '\r') else: From bd8a362c1e9009019662b7321d0ced4fe576cb33 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 08:43:54 +0200 Subject: [PATCH 105/136] model file extensions for model --- common_utils/artifacts_utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py index a540da18..ffb1c22d 100644 --- a/common_utils/artifacts_utils.py +++ b/common_utils/artifacts_utils.py @@ -1,5 +1,26 @@ import os + +def get_model_files(path, run_type): + """ + Retrieve model files from a directory that match the given run type and common extensions. + + Args: + path (str): The directory path where model files are stored. + run_type (str): The type of run (e.g., calibration, testing). + + Returns: + list: List of matching model file paths. + """ + # Define the common model file extensions - more can be added as needed + common_extensions = ['.pt', '.pth', '.h5', '.hdf5', '.pkl', '.json', '.bst', '.txt', '.bin', '.cbm', '.onnx'] + + # Retrieve files that start with run_type and end with any of the common extensions + model_files = [f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and any(f.endswith(ext) for ext in common_extensions)] + + return model_files + + def get_latest_model_artifact(path, run_type): """ Retrieve the latest model artifact for a given run type based on the modification time. @@ -19,7 +40,7 @@ def get_latest_model_artifact(path, run_type): """ # List all model files for the given specific run_type with the expected filename pattern - model_files = [f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and f.endswith('.pt')] + model_files = get_model_files(path, run_type) #[f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and f.endswith('.pt')] if not model_files: raise FileNotFoundError(f"No model artifacts found for run type '{run_type}' in path '{path}'") From 91f1a214acd84931e868936b347211f70a1f609b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 14:12:59 +0200 Subject: [PATCH 106/136] abstracted out model and root path --- common_utils/set_path.py | 60 +++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/common_utils/set_path.py b/common_utils/set_path.py index 6ba2957d..ad352555 100644 --- a/common_utils/set_path.py +++ b/common_utils/set_path.py @@ -1,6 +1,41 @@ import sys from pathlib import Path +def setup_root_paths(PATH) -> Path: + + """ + Extracts and returns the root path up to and including the "views_pipeline" directory from any given path. + This function identifies the "views_pipeline" directory within the provided path and constructs a new path up to and including this directory. + This is useful for setting up root paths for project-wide resources and utilities. + + Args: + PATH (Path): The base path, typically the path of the script invoking this function (e.g., `PATH = Path(__file__)`). + + Returns: + PATH_ROOT: The root path including the "views_pipeline" directory. + """ + + PATH_ROOT = Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) # The +1 is to include the "views_pipeline" part in the path + return PATH_ROOT + +def setup_model_paths(PATH): + + """ + Extracts and returns the model-specific path including the "models" directory and its immediate subdirectory. + This function identifies the "models" (e.g. purple_alien or orange_pasta) directory within the provided path and constructs a new path up to and including the next subdirectory after "models". + This is useful for setting up paths specific to a model within the project. + + Args: + PATH (Path): The base path, typically the path of the script invoking this function (e.g., `PATH = Path(__file__)`). + + Returns: + PATH_model: The path including the "models" directory and its immediate subdirectory. + """ + + PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path + return PATH_MODEL + + def setup_project_paths(PATH) -> None: """ @@ -30,9 +65,12 @@ def setup_project_paths(PATH) -> None: Disclaimer: A solution that avoids the insertion of the code above would be preferred. """ - PATH_ROOT = Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) # The +1 is to include the "views_pipeline" part in the path - PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path - +# PATH_ROOT = Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) # The +1 is to include the "views_pipeline" part in the path +# PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path + + PATH_ROOT = setup_root_paths(PATH) + PATH_MODEL = setup_model_paths(PATH) + # print(f"Root path: {PATH_ROOT}") # debug # print(f"Model path: {PATH_MODEL}") # debug @@ -62,7 +100,7 @@ def setup_project_paths(PATH) -> None: sys.path.insert(0, path_str) -def setup_data_paths(PATH) -> None: +def setup_data_paths(PATH) -> Path: """ Returns the raw, processed, and generated data paths for the specified model. @@ -73,17 +111,18 @@ def setup_data_paths(PATH) -> None: """ - PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path - + #PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path + PATH_MODEL = setup_model_paths(PATH) + PATH_DATA = PATH_MODEL / "data" PATH_RAW = PATH_DATA / "raw" PATH_PROCCEDS = PATH_DATA / "processed" PATH_GENERATED = PATH_DATA / "generated" - return PATH_RAW, PATH_PROCCEDS, PATH_GENERATED + return PATH_RAW, PATH_PROCCEDS, PATH_GENERATED # added in accordance with Sara's escwa branch -def setup_artifacts_paths(PATH) -> None: +def setup_artifacts_paths(PATH) -> Path: """ Returns the paths for the artifacts for the specified model. @@ -94,8 +133,9 @@ def setup_artifacts_paths(PATH) -> None: """ - PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path - + #PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path + PATH_MODEL = setup_model_paths(PATH) + PATH_ARTIFACTS = PATH_MODEL / "artifacts" # print(f"Artifacts path: {PATH_ARTIFACTS}") return PATH_ARTIFACTS From bc9d5994b9a06ab54a328545faf22b1d98b45c73 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 14:45:10 +0200 Subject: [PATCH 107/136] better naming 01 --- .../src/forecasting/generate_forecast.py | 7 ++++++- .../src/offline_evaluation/evaluate_model.py | 7 ++++++- models/purple_alien/src/training/train_model.py | 8 ++++++-- .../purple_alien/src/utils/model_run_manager.py | 16 ++++++++++------ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/models/purple_alien/src/forecasting/generate_forecast.py b/models/purple_alien/src/forecasting/generate_forecast.py index 910977fe..b6af5b42 100644 --- a/models/purple_alien/src/forecasting/generate_forecast.py +++ b/models/purple_alien/src/forecasting/generate_forecast.py @@ -75,7 +75,12 @@ def generate_forecast(model, views_vol, config, device, PATH): print('Posterior dict and test vol pickled and dumped!') -def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +def forecast_with_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +#def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): + + """ + ... + """ # the thing above might work, but it needs to be tested thoroughly.... raise NotImplementedError('Forecasting not implemented yet') diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index c711626e..8673630d 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -135,7 +135,12 @@ def evaluate_posterior(model, views_vol, config, device): wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) -def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +def evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): +#def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): + + """ + ... + """ # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type if artifact_name: diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index 9ffd3551..66a7a084 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -143,9 +143,13 @@ def training_loop(config, model, criterion, optimizer, scheduler, views_vol, dev print('training done...') - -def handle_training(config, device, views_vol, PATH_ARTIFACTS): +def train_model_artifact(config, device, views_vol, PATH_ARTIFACTS): +#def handle_training(config, device, views_vol, PATH_ARTIFACTS): + """ + ... + """ + # Create the model, criterion, optimizer and scheduler model, criterion, optimizer, scheduler = make(config, device) diff --git a/models/purple_alien/src/utils/model_run_manager.py b/models/purple_alien/src/utils/model_run_manager.py index 1b44dd2c..43bfff26 100644 --- a/models/purple_alien/src/utils/model_run_manager.py +++ b/models/purple_alien/src/utils/model_run_manager.py @@ -12,10 +12,10 @@ from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from utils_wandb import add_wandb_monthly_metrics from utils_device import setup_device -from train_model import make, training_loop, handle_training +from train_model import make, training_loop, train_model_artifact #handle_training # from evaluate_sweep import evaluate_posterior # see if it can be more genrel to a single model as well... -from evaluate_model import evaluate_posterior, handle_evaluation -from generate_forecast import handle_forecasting +from evaluate_model import evaluate_posterior, evaluate_model_artifact #handle_evaluation +from generate_forecast import forecast_with_model_artifact #handle_forecasting def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): @@ -49,12 +49,16 @@ def model_run_manager(config = None, project = None, train = None, eval = None, # Handle the single model runs: train and save the model as an artifact if train: - handle_training(config, device, views_vol, PATH_ARTIFACTS) + #handle_training(config, device, views_vol, PATH_ARTIFACTS) + train_model_artifact(config, device, views_vol, PATH_ARTIFACTS) # Handle the single model runs: evaluate a trained model (artifact) if eval: - handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + #handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_name) if forecast: - handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + #handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + forecast_with_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_name) + From 05cd666a5c8b3b6c97a0a674a10f75ef4f857a89 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 15:01:06 +0200 Subject: [PATCH 108/136] doc strings --- .../src/forecasting/generate_forecast.py | 17 ++++++++++++++++- .../src/offline_evaluation/evaluate_model.py | 17 ++++++++++++++++- models/purple_alien/src/training/train_model.py | 12 +++++++++++- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/src/forecasting/generate_forecast.py b/models/purple_alien/src/forecasting/generate_forecast.py index b6af5b42..5f306158 100644 --- a/models/purple_alien/src/forecasting/generate_forecast.py +++ b/models/purple_alien/src/forecasting/generate_forecast.py @@ -79,7 +79,22 @@ def forecast_with_model_artifact(config, device, views_vol, PATH_ARTIFACTS, arti #def handle_forecasting(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): """ - ... + Loads a model artifact and performs true forecasting. + + This function handles loading a model artifact either by using a specified artifact name + or by selecting the latest model artifact based on the run type (default). It then performs forecasting + using the model and the current forecasting partition. + + Args: + config: Configuration object containing parameters and settings. + device: The (torch) device to run the model on (CPU or GPU). + views_vol: The tensor containing the input data for forecasting. + PATH_ARTIFACTS: The path where model artifacts are stored. + artifact_name(optional): The specific name of the model artifact to load. Defaults to None which will lead to the latest runtype-specific artifact being loaded. + + Raises: + FileNotFoundError: If the specified or default model artifact cannot be found. + NotImplementedError: Indicates that forecasting is not yet implemented. """ # the thing above might work, but it needs to be tested thoroughly.... diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 8673630d..8b4c3f62 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -139,7 +139,22 @@ def evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_ #def handle_evaluation(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): """ - ... + Loads a model artifact and evaluates it given the respective trian and eval set within each data partition (Calibration, Testing). + + This function handles the loading of a model artifact either by using a specified artifact name + or by selecting the latest model artifact based on the run type (default). It then evaluates the model's + posterior distribution and prints the result. + + Args: + config: Configuration object containing parameters and settings. + device: The device to run the model on (CPU or GPU). + views_vol: The tensor containing the input data for evaluation. + PATH_ARTIFACTS: The path where model artifacts are stored. + artifact_name (optional): The specific name of the model artifact to load. Defaults to None. + + Raises: + FileNotFoundError: If the specified or default model artifact cannot be found. + """ # if an artifact name is provided through the CLI, use it. Otherwise, get the latest model artifact based on the run type diff --git a/models/purple_alien/src/training/train_model.py b/models/purple_alien/src/training/train_model.py index 66a7a084..ef42039f 100644 --- a/models/purple_alien/src/training/train_model.py +++ b/models/purple_alien/src/training/train_model.py @@ -147,7 +147,17 @@ def train_model_artifact(config, device, views_vol, PATH_ARTIFACTS): #def handle_training(config, device, views_vol, PATH_ARTIFACTS): """ - ... + Creates, trains, and saves a model artifact. + + This function creates the model, criterion, optimizer, and scheduler. It then trains the model + using the provided training loop and saves the trained model with a timestamp and run type as an artifact + in the specified artifacts path. + + Args: + config: Configuration object containing parameters and settings. + device: The device (torch.device) to run the model on (CPU or GPU). + views_vol: The tensor containing the input data for training. + PATH_ARTIFACTS: The path where model artifacts are stored. """ # Create the model, criterion, optimizer and scheduler From dcee74f1604e9b71eb68f3cce8b6ccc636b04bc6 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 21:11:09 +0200 Subject: [PATCH 109/136] add management to paths --- common_utils/set_path.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common_utils/set_path.py b/common_utils/set_path.py index ad352555..4c15f755 100644 --- a/common_utils/set_path.py +++ b/common_utils/set_path.py @@ -18,6 +18,7 @@ def setup_root_paths(PATH) -> Path: PATH_ROOT = Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) # The +1 is to include the "views_pipeline" part in the path return PATH_ROOT + def setup_model_paths(PATH): """ @@ -85,13 +86,14 @@ def setup_project_paths(PATH) -> None: PATH_CONFIGS = PATH_MODEL / "configs" PATH_SRC = PATH_MODEL / "src" PATH_UTILS = PATH_SRC / "utils" + PATH_MANAGEMENT = PATH_SRC / "management" # added to keep the management scripts in a separate folder the utils according to Sara's point PATH_ARCHITECTURES = PATH_SRC / "architectures" PATH_TRAINING = PATH_SRC / "training" PATH_FORECASTING = PATH_SRC / "forecasting" PATH_OFFLINE_EVALUATION = PATH_SRC / "offline_evaluation" PATH_DATALOADERS = PATH_SRC / "dataloaders" - paths_to_add = [PATH_ROOT, PATH_COMMON_UTILS, PATH_COMMON_CONFIGS, PATH_CONFIGS, PATH_UTILS, PATH_ARCHITECTURES, PATH_TRAINING, PATH_FORECASTING, PATH_OFFLINE_EVALUATION, PATH_DATALOADERS] + paths_to_add = [PATH_ROOT, PATH_COMMON_UTILS, PATH_COMMON_CONFIGS, PATH_CONFIGS, PATH_UTILS, PATH_MANAGEMENT, PATH_ARCHITECTURES, PATH_TRAINING, PATH_FORECASTING, PATH_OFFLINE_EVALUATION, PATH_DATALOADERS] for path in paths_to_add: path_str = str(path) From 9e6cdd1a1724de7ec364f166389c6280cb2568ac Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 21:12:52 +0200 Subject: [PATCH 110/136] change name 02 and location --- .../execute_model_tasks.py} | 18 +++++++++++++++++- .../model_run_handlers.py | 9 ++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) rename models/purple_alien/src/{utils/model_run_manager.py => management/execute_model_tasks.py} (76%) rename models/purple_alien/src/{utils => management}/model_run_handlers.py (65%) diff --git a/models/purple_alien/src/utils/model_run_manager.py b/models/purple_alien/src/management/execute_model_tasks.py similarity index 76% rename from models/purple_alien/src/utils/model_run_manager.py rename to models/purple_alien/src/management/execute_model_tasks.py index 43bfff26..9b60dc93 100644 --- a/models/purple_alien/src/utils/model_run_manager.py +++ b/models/purple_alien/src/management/execute_model_tasks.py @@ -18,7 +18,23 @@ from generate_forecast import forecast_with_model_artifact #handle_forecasting -def model_run_manager(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): +def execute_model_tasks(config = None, project = None, train = None, eval = None, forecast = None, artifact_name = None): + + """ + Executes various model-related tasks including training, evaluation, and forecasting. + + This function manages the execution of different tasks such as training the model, + evaluating an existing model, or performing forecasting. + It also initializes the WandB project. + + Args: + config: Configuration object containing parameters and settings. + project: The WandB project name. + train: Flag to indicate if the model should be trained. + eval: Flag to indicate if the model should be evaluated. + forecast: Flag to indicate if forecasting should be performed. + artifact_name (optional): Specific name of the model artifact to load for evaluation or forecasting. + """ # Define the path for the artifacts PATH_ARTIFACTS = setup_artifacts_paths(PATH) diff --git a/models/purple_alien/src/utils/model_run_handlers.py b/models/purple_alien/src/management/model_run_handlers.py similarity index 65% rename from models/purple_alien/src/utils/model_run_handlers.py rename to models/purple_alien/src/management/model_run_handlers.py index 455c77ca..e91b6cd1 100644 --- a/models/purple_alien/src/utils/model_run_handlers.py +++ b/models/purple_alien/src/management/model_run_handlers.py @@ -10,7 +10,8 @@ from config_sweep import get_swep_config from config_hyperparameters import get_hp_config -from model_run_manager import model_run_manager +#from model_run_manager import model_run_manager +from execute_model_tasks import execute_model_tasks def handle_sweep_run(args): @@ -38,12 +39,14 @@ def handle_single_run(args): if args.run_type == 'calibration' or args.run_type == 'testing': - model_run_manager(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) + #model_run_manager(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) + execute_model_tasks(config = hyperparameters, project = project, train = args.train, eval = args.evaluate, forecast = False, artifact_name = args.artifact_name) elif args.run_type == 'forecasting': #print('True forecasting ->->->->') - model_run_manager(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) + #model_run_manager(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) + execute_model_tasks(config = hyperparameters, project = project, train = False, eval = False, forecast=True, artifact_name = args.artifact_name) else: raise ValueError(f"Invalid run type: {args.run_type}") From 4bab662eb44e705f3d68e73a016c9b158d78517e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 21:26:55 +0200 Subject: [PATCH 111/136] more renaming 03 --- models/purple_alien/main.py | 12 ++++++++---- .../{model_run_handlers.py => execute_model_runs.py} | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) rename models/purple_alien/src/management/{model_run_handlers.py => execute_model_runs.py} (97%) diff --git a/models/purple_alien/main.py b/models/purple_alien/main.py index 2f3594fe..11a8b62a 100644 --- a/models/purple_alien/main.py +++ b/models/purple_alien/main.py @@ -13,7 +13,8 @@ from cli_parser_utils import parse_args, validate_arguments #from artifacts_utils import get_latest_model_artifact -from model_run_handlers import handle_sweep_run, handle_single_run +#from model_run_handlers import handle_sweep_run, handle_single_run +from execute_model_runs import execute_sweep_run, execute_single_run #from mode_run_manager import model_run_manager @@ -36,12 +37,15 @@ # first you need to check if you are running a sweep or not, because the sweep will overwrite the train and evaluate flags if args.sweep == True: - handle_sweep_run(args) + #handle_sweep_run(args) + execute_sweep_run(args) elif args.sweep == False: - handle_single_run(args) - + #handle_single_run(args) + execute_single_run(args) + + end_t = time.time() minutes = (end_t - start_t)/60 print(f'Done. Runtime: {minutes:.3f} minutes') diff --git a/models/purple_alien/src/management/model_run_handlers.py b/models/purple_alien/src/management/execute_model_runs.py similarity index 97% rename from models/purple_alien/src/management/model_run_handlers.py rename to models/purple_alien/src/management/execute_model_runs.py index e91b6cd1..12ca7b1a 100644 --- a/models/purple_alien/src/management/model_run_handlers.py +++ b/models/purple_alien/src/management/execute_model_runs.py @@ -14,7 +14,7 @@ from execute_model_tasks import execute_model_tasks -def handle_sweep_run(args): +def execute_sweep_run(args): print('Running sweep...') project = f"purple_alien_sweep" # check naming convention @@ -27,7 +27,7 @@ def handle_sweep_run(args): wandb.agent(sweep_id, model_run_manager) -def handle_single_run(args): +def execute_single_run(args): # get hyperparameters. IS THE ISSUE UP HERE? hyperparameters = get_hp_config() From 2ed7b537e3794b94d72e45ac0401b1229bb67fe1 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 21:48:13 +0200 Subject: [PATCH 112/136] fixed a thing... --- models/purple_alien/src/management/execute_model_runs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/management/execute_model_runs.py b/models/purple_alien/src/management/execute_model_runs.py index 12ca7b1a..bf0f6484 100644 --- a/models/purple_alien/src/management/execute_model_runs.py +++ b/models/purple_alien/src/management/execute_model_runs.py @@ -24,7 +24,7 @@ def execute_sweep_run(args): sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name - wandb.agent(sweep_id, model_run_manager) + wandb.agent(sweep_id, execute_model_tasks) def execute_single_run(args): From f6e7c9915122af3cc04f28d80ab5cdb8b3461298 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 22:57:20 +0200 Subject: [PATCH 113/136] log_monthly_metric in w&b utils --- .../src/offline_evaluation/evaluate_model.py | 34 ++++++++++++++----- models/purple_alien/src/utils/utils_wandb.py | 27 ++++++++++++++- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 8b4c3f62..0461ed98 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -30,6 +30,7 @@ from utils import choose_model, choose_loss, choose_sheduler, get_train_tensors, get_full_tensor, apply_dropout, execute_freeze_h_option, get_log_dict, train_log, init_weights, get_data from utils_prediction import predict, sample_posterior from artifacts_utils import get_latest_model_artifact +from utils_wandb import log_wandb_monthly_metrics from config_sweep import get_swep_config from config_hyperparameters import get_hp_config @@ -39,7 +40,17 @@ def evaluate_posterior(model, views_vol, config, device): """ - Function to sample from and evaluate the posterior distribution of Hydranet. + Samples from and evaluates the posterior distribution of the model. + + This function evaluates the posterior distribution of the model, computes metrics + such as mean squared error, average precision, AUC, and Brier score, and logs the results. + If not running a sweep, it also pickles and saves the posterior, metrics, and test volumes. + + Args: + model: The trained model to evaluate. + views_vol: The input data volume. + config: Configuration object containing parameters and settings. + device: The device (CPU or GPU) on which to run the evaluation. """ posterior_list, posterior_list_class, out_of_sample_vol, full_tensor = sample_posterior(model, views_vol, config, device) @@ -72,15 +83,17 @@ def evaluate_posterior(model, views_vol, config, device): y_true = out_of_sample_vol[:,i].reshape(-1) # nu 180x180 . dim 0 is time y_true_binary = (y_true > 0) * 1 + # log the metrics to WandB - but why here? + log_dict = get_log_dict(i, mean_array, mean_class_array, std_array, std_class_array, out_of_sample_vol, config)# so at least it gets reported sep. + + wandb.log(log_dict) + + # this could be a function in utils_wandb or in common_utils... mse = mean_squared_error(y_true, y_score) ap = average_precision_score(y_true_binary, y_score_prob) auc = roc_auc_score(y_true_binary, y_score_prob) brier = brier_score_loss(y_true_binary, y_score_prob) - log_dict = get_log_dict(i, mean_array, mean_class_array, std_array, std_class_array, out_of_sample_vol, config)# so at least it gets reported sep. - - wandb.log(log_dict) - out_sample_month_list.append(i) # only used for pickle... mse_list.append(mse) ap_list.append(ap) # add to list. @@ -129,10 +142,13 @@ def evaluate_posterior(model, views_vol, config, device): print('Running sweep. NO posterior dict, metric dict, or test vol pickled+dumped') # could be a function in utils_wandb.... - wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) - wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) - wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) - wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) + #wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) + #wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) + #wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) + #wandb.log({f"{config.time_steps}month_brier_score_loss":np.mean(brier_list)}) + + log_wandb_monthly_metrics(config, mse_list, ap_list, auc_list, brier_list) + def evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_name=None): diff --git a/models/purple_alien/src/utils/utils_wandb.py b/models/purple_alien/src/utils/utils_wandb.py index 8859f098..df82c326 100644 --- a/models/purple_alien/src/utils/utils_wandb.py +++ b/models/purple_alien/src/utils/utils_wandb.py @@ -1,3 +1,5 @@ +import numpy as np +from sklearn.metrics import mean_squared_error, average_precision_score, roc_auc_score, brier_score_loss import wandb # there are things in other utils that should be here... @@ -6,4 +8,27 @@ def add_wandb_monthly_metrics(): # Define "new" monthly metrics for WandB logging wandb.define_metric("monthly/out_sample_month") - wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") \ No newline at end of file + wandb.define_metric("monthly/*", step_metric="monthly/out_sample_month") + + +def log_wandb_monthly_metrics(config, mse_list, ap_list, auc_list, brier_list): + + """ + Logs evaluation metrics to WandB. + + This function computes the mean of provided metrics and logs them to WandB. + The metrics include mean squared error, average precision score, ROC AUC score, and Brier score loss. + + Args: + config : Configuration object containing parameters and settings. + mse_list : List of monthly mean squared errors. + ap_list : List of monthly average precision scores. + auc_list : List of monthly ROC AUC scores. + brier_list : List of monthly Brier scores. + + """ + + wandb.log({f"{config.time_steps}month_mean_squared_error": np.mean(mse_list)}) + wandb.log({f"{config.time_steps}month_average_precision_score": np.mean(ap_list)}) + wandb.log({f"{config.time_steps}month_roc_auc_score": np.mean(auc_list)}) + wandb.log({f"{config.time_steps}month_brier_score_loss": np.mean(brier_list)}) \ No newline at end of file From 800c9387480b7b23736be0b57aa7340060456733 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 23:02:08 +0200 Subject: [PATCH 114/136] fixed print? --- models/purple_alien/src/utils/utils_prediction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 744c2bf6..20c09d62 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -116,7 +116,7 @@ def sample_posterior(model, views_vol, config, device): - tuple: (posterior_magnitudes, posterior_probabilities, out_of_sample_data) """ - print(f'Drawing {config.test_samples} posterior samples...') + print(f'Drawing {config.test_samples} posterior samples...', end = '\r') # REALLY BAD NAME!!!! # Why do you put this test tensor on device here??!? From d4617abe408c4c46d0af9ff1023eabb77e569a5a Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 23:42:03 +0200 Subject: [PATCH 115/136] os to pathlib --- .../src/offline_evaluation/evaluate_model.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/models/purple_alien/src/offline_evaluation/evaluate_model.py b/models/purple_alien/src/offline_evaluation/evaluate_model.py index 0461ed98..9e702807 100644 --- a/models/purple_alien/src/offline_evaluation/evaluate_model.py +++ b/models/purple_alien/src/offline_evaluation/evaluate_model.py @@ -105,9 +105,12 @@ def evaluate_posterior(model, views_vol, config, device): _ , _, PATH_GENERATED = setup_data_paths(PATH) - # if the path does not exist, create it - if not os.path.exists(PATH_GENERATED): - os.makedirs(PATH_GENERATED) + # if the path does not exist, create it - maybe doable with Pathlib, but this is a well recognized way of doing it. + #if not os.path.exists(PATH_GENERATED): + # os.makedirs(PATH_GENERATED) + + # Pathlib alternative + Path(PATH_GENERATED).mkdir(parents=True, exist_ok=True) # print for debugging print(f'PATH to generated data: {PATH_GENERATED}') @@ -177,12 +180,15 @@ def evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_ if artifact_name: print(f"Using (non-default) artifact: {artifact_name}") - # If it lacks the file extension, add it + # If the pytorch artifact lacks the file extension, add it. This is obviously specific to pytorch artifacts, but we are deep in the model code here, so it is fine. if not artifact_name.endswith('.pt'): artifact_name += '.pt' # Define the full (model specific) path for the artifact - PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + #PATH_MODEL_ARTIFACT = os.path.join(PATH_ARTIFACTS, artifact_name) + + # pathlib alternative as per sara's comment + PATH_MODEL_ARTIFACT = PATH_ARTIFACTS / artifact_name # PATH_ARTIFACTS is already a Path object else: # use the latest model artifact based on the run type @@ -192,14 +198,22 @@ def evaluate_model_artifact(config, device, views_vol, PATH_ARTIFACTS, artifact_ PATH_MODEL_ARTIFACT = get_latest_model_artifact(PATH_ARTIFACTS, config.run_type) # Check if the model artifact exists - if not, raise an error - if not os.path.exists(PATH_MODEL_ARTIFACT): + #if not os.path.exists(PATH_MODEL_ARTIFACT): + # raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") + + # Pathlib alternative as per sara's comment + if not PATH_MODEL_ARTIFACT.exists(): # PATH_MODEL_ARTIFACT is already a Path object raise FileNotFoundError(f"Model artifact not found at {PATH_MODEL_ARTIFACT}") # load the model model = torch.load(PATH_MODEL_ARTIFACT) # get the exact model date_time stamp for the pkl files made in the evaluate_posterior from evaluation.py - model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT)[-18:-3] # 18 is the length of the timestamp string, and -3 is to remove the .pt file extension. a bit hardcoded, but very simple and should not change. + #model_time_stamp = os.path.basename(PATH_MODEL_ARTIFACT)[-18:-3] # 18 is the length of the timestamp string + ".pt", and -3 is to remove the .pt file extension. a bit hardcoded, but very simple and should not change. + + + # Pathlib alternative as per sara's comment + model_time_stamp = PATH_MODEL_ARTIFACT.stem[-15:] # 15 is the length of the timestamp string. This is more robust than the os.path.basename solution above since it does not rely on the file extension. # print for debugging print(f"model_time_stamp: {model_time_stamp}") From 81f258c4d3c541e1d9688fb511753335eaed56c5 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 23:43:40 +0200 Subject: [PATCH 116/136] better docstrings --- common_utils/set_path.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common_utils/set_path.py b/common_utils/set_path.py index 4c15f755..9713d0c3 100644 --- a/common_utils/set_path.py +++ b/common_utils/set_path.py @@ -4,7 +4,7 @@ def setup_root_paths(PATH) -> Path: """ - Extracts and returns the root path up to and including the "views_pipeline" directory from any given path. + Extracts and returns the root path (pathlib path object) up to and including the "views_pipeline" directory from any given path. This function identifies the "views_pipeline" directory within the provided path and constructs a new path up to and including this directory. This is useful for setting up root paths for project-wide resources and utilities. @@ -12,7 +12,7 @@ def setup_root_paths(PATH) -> Path: PATH (Path): The base path, typically the path of the script invoking this function (e.g., `PATH = Path(__file__)`). Returns: - PATH_ROOT: The root path including the "views_pipeline" directory. + PATH_ROOT: The root path (pathlib path object) including the "views_pipeline" directory. """ PATH_ROOT = Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) # The +1 is to include the "views_pipeline" part in the path @@ -22,7 +22,7 @@ def setup_root_paths(PATH) -> Path: def setup_model_paths(PATH): """ - Extracts and returns the model-specific path including the "models" directory and its immediate subdirectory. + Extracts and returns the model-specific path (pathlib path object) including the "models" directory and its immediate subdirectory. This function identifies the "models" (e.g. purple_alien or orange_pasta) directory within the provided path and constructs a new path up to and including the next subdirectory after "models". This is useful for setting up paths specific to a model within the project. @@ -30,7 +30,7 @@ def setup_model_paths(PATH): PATH (Path): The base path, typically the path of the script invoking this function (e.g., `PATH = Path(__file__)`). Returns: - PATH_model: The path including the "models" directory and its immediate subdirectory. + PATH_model: The path (pathlib path object) including the "models" directory and its immediate subdirectory. """ PATH_MODEL = Path(*[i for i in PATH.parts[:PATH.parts.index("models")+2]]) # The +2 is to include the "models" and the individual model name in the path @@ -105,7 +105,7 @@ def setup_project_paths(PATH) -> None: def setup_data_paths(PATH) -> Path: """ - Returns the raw, processed, and generated data paths for the specified model. + Returns the raw, processed, and generated data paths (pathlib path object) for the specified model. Args: PATH (Path): The base path, typically the path of the script invoking this function (i.e., `Path(__file__)`). @@ -127,7 +127,7 @@ def setup_data_paths(PATH) -> Path: def setup_artifacts_paths(PATH) -> Path: """ - Returns the paths for the artifacts for the specified model. + Returns the paths (pathlib path object) for the artifacts for the specified model. Args: PATH (Path): The base path, typically the path of the script invoking this function (i.e., `Path(__file__)`). From d58701a7376c197daf18bb3aaf3b33555a976b83 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Mon, 10 Jun 2024 23:55:08 +0200 Subject: [PATCH 117/136] os -> pathlib --- common_utils/artifacts_utils.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/common_utils/artifacts_utils.py b/common_utils/artifacts_utils.py index ffb1c22d..8699466d 100644 --- a/common_utils/artifacts_utils.py +++ b/common_utils/artifacts_utils.py @@ -1,9 +1,9 @@ import os +from pathlib import Path - -def get_model_files(path, run_type): +def get_artifact_files(PATH, run_type): """ - Retrieve model files from a directory that match the given run type and common extensions. + Retrieve artifact files from a directory that match the given run type and common extensions. Args: path (str): The directory path where model files are stored. @@ -16,14 +16,18 @@ def get_model_files(path, run_type): common_extensions = ['.pt', '.pth', '.h5', '.hdf5', '.pkl', '.json', '.bst', '.txt', '.bin', '.cbm', '.onnx'] # Retrieve files that start with run_type and end with any of the common extensions - model_files = [f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and any(f.endswith(ext) for ext in common_extensions)] + # artifact_files = [f for f in os.listdir(PATH) if f.startswith(f"{run_type}_model_") and any(f.endswith(ext) for ext in common_extensions)] - return model_files + # pathlib alternative + artifact_files = [f for f in PATH.iterdir() if f.is_file() and f.stem.startswith(f"{run_type}_model_") and f.suffix in common_extensions] + + + return artifact_files -def get_latest_model_artifact(path, run_type): +def get_latest_model_artifact(PATH, run_type): """ - Retrieve the latest model artifact for a given run type based on the modification time. + Retrieve the path (pathlib path object) latest model artifact for a given run type based on the modification time. Args: path (str): The model specifc directory path where artifacts are stored. @@ -33,17 +37,17 @@ def get_latest_model_artifact(path, run_type): run_type (str): The type of run (e.g., calibration, testing, forecasting). Returns: - str: The path to the latest model artifact given the run type. + The path (pathlib path objsect) to the latest model artifact given the run type. Raises: FileNotFoundError: If no model artifacts are found for the given run type. """ # List all model files for the given specific run_type with the expected filename pattern - model_files = get_model_files(path, run_type) #[f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and f.endswith('.pt')] + model_files = get_artifact_files(PATH, run_type) #[f for f in os.listdir(path) if f.startswith(f"{run_type}_model_") and f.endswith('.pt')] if not model_files: - raise FileNotFoundError(f"No model artifacts found for run type '{run_type}' in path '{path}'") + raise FileNotFoundError(f"No model artifacts found for run type '{run_type}' in path '{PATH}'") # Sort the files based on the timestamp embedded in the filename. With format %Y%m%d_%H%M%S For example, '20210831_123456.pt' model_files.sort(reverse=True) @@ -53,7 +57,12 @@ def get_latest_model_artifact(path, run_type): print(f"artifact used: {model_files[0]}") # Return the latest model file - return os.path.join(path, model_files[0]) + #PATH_MODEL_ARTIFACT = os.path.join(path, model_files[0]) + + # pathlib alternative + PATH_MODEL_ARTIFACT = Path(PATH) / model_files[0] + + return PATH_MODEL_ARTIFACT # notes on stepshifted models: # There will be some thinking here in regards to how we store, denote (naming convention), and retrieve the model artifacts from stepshifted models. From 8c799a8000e1ed137c59b7daa877a282f6dc946e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 00:08:35 +0200 Subject: [PATCH 118/136] fixed print? --- models/purple_alien/src/utils/utils_prediction.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 20c09d62..3983354a 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, is_evalutaion = True): +def predict(model, full_tensor, config, device, sample, is_evalutaion = True): """ Function to create predictions for the Hydranet model. @@ -41,6 +41,9 @@ def predict(model, full_tensor, config, device, is_evalutaion = True): Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). """ + print(f'Posterior sample: {sample}/{config.test_samples}', end = '\r') # could and should put this in the predict function above. + + # Set the model to evaluation mode model.eval() @@ -126,15 +129,15 @@ def sample_posterior(model, views_vol, config, device): posterior_list = [] posterior_list_class = [] - for i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? + for sample in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? # full_tensor is need on device here, but maybe just do it inside the test function? - pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. + pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device, sample) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. posterior_list.append(pred_np_list) posterior_list_class.append(pred_class_np_list) #if i % 10 == 0: # print steps 10 - print(f'Posterior sample: {i}/{config.test_samples}', end = '\r') + #print(f'Posterior sample: {sample}/{config.test_samples}', end = '\r') # could and should put this in the predict function above. return posterior_list, posterior_list_class, out_of_sample_vol, full_tensor From 09462b75d5dfc3f0a6f8f92f47432a718c029c0f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 00:11:55 +0200 Subject: [PATCH 119/136] print better now? --- models/purple_alien/src/utils/utils_prediction.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/models/purple_alien/src/utils/utils_prediction.py b/models/purple_alien/src/utils/utils_prediction.py index 3983354a..784af4d7 100644 --- a/models/purple_alien/src/utils/utils_prediction.py +++ b/models/purple_alien/src/utils/utils_prediction.py @@ -32,7 +32,7 @@ from config_hyperparameters import get_hp_config -def predict(model, full_tensor, config, device, sample, is_evalutaion = True): +def predict(model, full_tensor, config, device, sample_i, is_evalutaion = True): """ Function to create predictions for the Hydranet model. @@ -41,7 +41,7 @@ def predict(model, full_tensor, config, device, sample, is_evalutaion = True): Each array is of the shap **fx180x180**, where f is the number of features (currently 3 types of violence). """ - print(f'Posterior sample: {sample}/{config.test_samples}', end = '\r') # could and should put this in the predict function above. + print(f'Posterior sample: {sample_i}/{config.test_samples}', end = '\r') # could and should put this in the predict function above. # Set the model to evaluation mode @@ -66,14 +66,14 @@ def predict(model, full_tensor, config, device, sample, is_evalutaion = True): in_sample_seq_len = seq_len - 1 - config.time_steps # but retain the last time_steps for hold-out evaluation # These print staments are informative while the model is running, but the implementation is not optimal.... - print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set. Full sequence length: {full_seq_len}', end= '\r') + #print(f'\t\t\t\t\t\t\t Evaluation mode. retaining hold out set. Full sequence length: {full_seq_len}', end= '\r') else: full_seq_len = seq_len - 1 + config.time_steps # we loop over the entire sequence plus the additional time_steps for forecasting in_sample_seq_len = seq_len - 1 # the in-sample part is now the entire sequence - print(f'\t\t\t\t\t\t\t Forecasting mode. No hold out set. Full sequence length: {full_seq_len}', end= '\r') + #print(f'\t\t\t\t\t\t\t Forecasting mode. No hold out set. Full sequence length: {full_seq_len}', end= '\r') for i in range(full_seq_len): @@ -129,10 +129,10 @@ def sample_posterior(model, views_vol, config, device): posterior_list = [] posterior_list_class = [] - for sample in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? + for sample_i in range(config.test_samples): # number of posterior samples to draw - just set config.test_samples, no? # full_tensor is need on device here, but maybe just do it inside the test function? - pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device, sample) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. + pred_np_list, pred_class_np_list = predict(model, full_tensor, config, device, sample_i) # Returns two lists of numpy arrays (shape 3/180/180). One list of the predicted magnitudes and one list of the predicted probabilities. posterior_list.append(pred_np_list) posterior_list_class.append(pred_class_np_list) From 6014f716723b0e27f020aeaecd7786ad8525b62f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 16:43:41 +0200 Subject: [PATCH 120/136] 300 run --- models/purple_alien/configs/config_hyperparameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index d2c7ec90..e41ea09c 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -8,7 +8,7 @@ def get_hp_config(): 'scheduler' : 'WarmupDecay', # 'CosineAnnealingLR' 'OneCycleLR' 'total_hidden_channels' : 32, 'min_events' : 5, - 'samples': 600, # 600 for actual trainnig, 10 for debug + 'samples': 300, # 600 for actual trainnig, 10 for debug 'batch_size': 3, 'dropout_rate' : 0.125, 'learning_rate' : 0.001, From 5b717b9b64a79103e039d94c866a7f5530432976 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 17:18:43 +0200 Subject: [PATCH 121/136] new combined dataloader --- .../src/dataloaders/get_partioned_data.py | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 models/purple_alien/src/dataloaders/get_partioned_data.py diff --git a/models/purple_alien/src/dataloaders/get_partioned_data.py b/models/purple_alien/src/dataloaders/get_partioned_data.py new file mode 100644 index 00000000..28f40f80 --- /dev/null +++ b/models/purple_alien/src/dataloaders/get_partioned_data.py @@ -0,0 +1,66 @@ +import sys +import argparse +from pathlib import Path + +# Set up the path +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths +setup_project_paths(PATH) + +# Import necessary functions +from utils_dataloaders import get_views_date, df_to_vol, process_partition_data, process_data, parse_args + +import sys +import argparse +from pathlib import Path + +# Set up the path +PATH = Path(__file__) +sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS +from set_path import setup_project_paths +setup_project_paths(PATH) + +# Import necessary functions +from config_hyperparameters import get_hp_config +from utils_dataloaders import get_views_date, df_to_vol, process_partition_data + +if __name__ == "__main__": + # Parse CLI arguments + args = parse_args() + + # Immediate feedback on partitions to be processed + partitions_to_process = [] + if args.calibration: + partitions_to_process.append('calibration') + if args.testing: + partitions_to_process.append('testing') + if args.forecasting: + partitions_to_process.append('forecasting') + + if not partitions_to_process: + print("Error: No partition flag provided. Use -c, -t, and/or -f.") + sys.exit(1) + + print(f"Partitions to be processed: {', '.join(partitions_to_process)}") + + # Process calibration data if flag is set + if args.calibration: + df_cal, vol_cal = process_data('calibration', PATH) + print(f"Processed calibration data:") + print(f"DataFrame shape: {df_cal.shape if df_cal is not None else 'None'}") + print(f"Volume shape: {vol_cal.shape if vol_cal is not None else 'None'}") + + # Process testing data if flag is set + if args.testing: + df_test, vol_test = process_data('testing', PATH) + print(f"Processed testing data:") + print(f"DataFrame shape: {df_test.shape if df_test is not None else 'None'}") + print(f"Volume shape: {vol_test.shape if vol_test is not None else 'None'}") + + # Process forecasting data if flag is set + if args.forecasting: + df_forecast, vol_forecast = process_data('forecasting', PATH) + print(f"Processed forecasting data:") + print(f"DataFrame shape: {df_forecast.shape if df_forecast is not None else 'None'}") + print(f"Volume shape: {vol_forecast.shape if vol_forecast is not None else 'None'}") From 8a7af6dc4b9685827894f561f136bd0134857626 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 17:19:26 +0200 Subject: [PATCH 122/136] updated for the new single dataloader --- .../src/utils/utils_dataloaders.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/models/purple_alien/src/utils/utils_dataloaders.py b/models/purple_alien/src/utils/utils_dataloaders.py index a6630f34..5ba5ffad 100644 --- a/models/purple_alien/src/utils/utils_dataloaders.py +++ b/models/purple_alien/src/utils/utils_dataloaders.py @@ -2,6 +2,7 @@ import sys from pathlib import Path +import argparse PATH = Path(__file__) sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS @@ -15,6 +16,7 @@ import numpy as np import pandas as pd + #from config_partitioner import get_partitioner_dict from set_partition import get_partitioner_dict @@ -107,7 +109,7 @@ def df_to_vol(df): return vol -def process_partition_data(partition, get_views_date, df_to_vol, PATH): +def process_partition_data(partition, PATH): """ Processes data for a given partition by ensuring the existence of necessary directories, @@ -115,8 +117,6 @@ def process_partition_data(partition, get_views_date, df_to_vol, PATH): Args: partition (str): The partition to process, e.g., 'calibration', 'forecasting', 'testing'. - get_views_date (function): Function to download the VIEWSER data. - df_to_vol (function): Function to convert a DataFrame to a volume. Returns: tuple: A tuple containing the DataFrame `df` and the volume `vol`. @@ -156,5 +156,26 @@ def process_partition_data(partition, get_views_date, df_to_vol, PATH): return df, vol +def parse_args(): + parser = argparse.ArgumentParser(description='Process data for different partitions') + + # Add binary flags for each partition + parser.add_argument('-c', '--calibration', action='store_true', help='Process calibration data') + parser.add_argument('-t', '--testing', action='store_true', help='Process testing data') + parser.add_argument('-f', '--forecasting', action='store_true', help='Process forecasting data') + + return parser.parse_args() + +def process_data(partition, PATH): + """ + Processes the data for the given partition. + + Args: + partition (str): The partition type (e.g., 'calibration', 'testing', 'forecasting'). + PTAH (Path): The base path for data. -# Should this be more general? \ No newline at end of file + Returns: + tuple: DataFrame and volume array for the partition. + """ + df, vol = process_partition_data(partition, PATH) + return df, vol From 707313cd27da88ef486a42fbbbfde51ed8937063 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 18:44:01 +0200 Subject: [PATCH 123/136] new config_setup --- .../purple_alien/configs/config_deployment.py | 16 ++++++++++++++++ .../configs/config_hyperparameters.py | 10 +++++++++- models/purple_alien/configs/config_meta.py | 17 +++++++++++++++++ models/purple_alien/configs/config_sweep.py | 9 +++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 models/purple_alien/configs/config_deployment.py create mode 100644 models/purple_alien/configs/config_meta.py diff --git a/models/purple_alien/configs/config_deployment.py b/models/purple_alien/configs/config_deployment.py new file mode 100644 index 00000000..e1d56586 --- /dev/null +++ b/models/purple_alien/configs/config_deployment.py @@ -0,0 +1,16 @@ +def get_deployment_config(): + + """ + Contains the configuration for deploying the model into different environments. + This configuration is "behavioral" so modifying it will affect the model's runtime behavior and integration into the deployment system. + + Returns: + - deployment_config (dict): A dictionary containing deployment settings, determining how the model is deployed, including status, endpoints, and resource allocation. + """ + + # More deployment settings can/will be added here + deployment_config = { + "deployment_status": "shadow", # shadow, deployed, baseline, or deprecated + } + + return deployment_config \ No newline at end of file diff --git a/models/purple_alien/configs/config_hyperparameters.py b/models/purple_alien/configs/config_hyperparameters.py index e41ea09c..5883b7f8 100644 --- a/models/purple_alien/configs/config_hyperparameters.py +++ b/models/purple_alien/configs/config_hyperparameters.py @@ -1,6 +1,14 @@ def get_hp_config(): - + + """ + Contains the hyperparameter configurations for model training. + This configuration is "operational" so modifying these settings will impact the model's behavior during training. + + Returns: + - hyperparameters (dict): A dictionary containing hyperparameters for training the model, which determine the model's behavior during the training phase. + """ + hyperparameters = { 'model' : 'HydraBNUNet06_LSTM4', #'BNUNet', 'weight_init' : 'xavier_norm', diff --git a/models/purple_alien/configs/config_meta.py b/models/purple_alien/configs/config_meta.py new file mode 100644 index 00000000..c2eef0af --- /dev/null +++ b/models/purple_alien/configs/config_meta.py @@ -0,0 +1,17 @@ +def get_meta_config(): + """ + Contains the meta data for the model (model architecture, name, target variable, and level of analysis). + This config is for documentation purposes only, and modifying it will not affect the model, the training, or the evaluation. + + Returns: + - meta_config (dict): A dictionary containing model meta configuration. + """ + meta_config = { + "name": "purple_alien", + "algorithm": "HydraNet", + "target(S)": ["ln_sb_best", "ln_ns_best", "ln_os_best", "ln_sb_best_binarized", "ln_ns_best_binarized", "ln_os_best_binarized"], + "queryset": "escwa001_cflong", + "level": "cm", + "creator": "Simon" + } + return meta_config \ No newline at end of file diff --git a/models/purple_alien/configs/config_sweep.py b/models/purple_alien/configs/config_sweep.py index 92b7b854..4d5b6f86 100644 --- a/models/purple_alien/configs/config_sweep.py +++ b/models/purple_alien/configs/config_sweep.py @@ -1,4 +1,13 @@ def get_swep_config(): + + """ + Contains the configuration for hyperparameter sweeps using WandB. + This configuration is "operational" so modifying it will change the search strategy, parameter ranges, and other settings for hyperparameter tuning aimed at optimizing model performance. + + Returns: + - sweep_config (dict): A dictionary containing the configuration for hyperparameter sweeps, defining the methods and parameter ranges used to search for optimal hyperparameters. + """ + sweep_config = { 'method': 'grid' } From a05d118dd4e4fddfb70fb78e317539b1a1739a83 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 18:45:42 +0200 Subject: [PATCH 124/136] removed double stuff --- models/purple_alien/src/dataloaders/get_partioned_data.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/models/purple_alien/src/dataloaders/get_partioned_data.py b/models/purple_alien/src/dataloaders/get_partioned_data.py index 28f40f80..b2b8025f 100644 --- a/models/purple_alien/src/dataloaders/get_partioned_data.py +++ b/models/purple_alien/src/dataloaders/get_partioned_data.py @@ -21,10 +21,6 @@ from set_path import setup_project_paths setup_project_paths(PATH) -# Import necessary functions -from config_hyperparameters import get_hp_config -from utils_dataloaders import get_views_date, df_to_vol, process_partition_data - if __name__ == "__main__": # Parse CLI arguments args = parse_args() From 2fe4a343b904723cf4704b5409dba67da6c34c1e Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 19:10:51 +0200 Subject: [PATCH 125/136] added comment regarding stuff --- common_utils/set_partition.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/common_utils/set_partition.py b/common_utils/set_partition.py index bc71a8ae..0548f18c 100644 --- a/common_utils/set_partition.py +++ b/common_utils/set_partition.py @@ -20,4 +20,13 @@ def get_partitioner_dict(partion, step=36): print('partitioner_dict', partitioner_dict) - return partitioner_dict \ No newline at end of file + return partitioner_dict + +# currently these differ from the ones in the config_data_partitions.py file for the stepshifted models (see below). This needs to be sorted out asap. + +# data_partitions = { +# 'calib_partitioner_dict': {"train": (121, 396), "predict": (409, 456)}, # Does not make sense that the eval set starts at 409, it should start at 397, no? +# 'test_partitioner_dict': {"train": (121, 456), "predict": (457, 504)}, +# 'future_partitioner_dict': {"train": (121, 504), "predict": (529, 529)}, # NO HARD CODIGN THE FUTURE START DATE +# 'FutureStart': 529, #Jan 24 # THIS SHOULD NOT BE HARD CODED!!!! Whatever the right partitions are for calibration and testing, the forecasting should be automatically infered from the current date. +# } \ No newline at end of file From d07d4c7f38577b97699314cd2a91ec581e24b437 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 19:39:39 +0200 Subject: [PATCH 126/136] config_input_data added --- .../purple_alien/configs/config_input_data.py | 24 +++++++++++++++++++ .../src/utils/utils_dataloaders.py | 23 ++++++++++-------- 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 models/purple_alien/configs/config_input_data.py diff --git a/models/purple_alien/configs/config_input_data.py b/models/purple_alien/configs/config_input_data.py new file mode 100644 index 00000000..0ac89f04 --- /dev/null +++ b/models/purple_alien/configs/config_input_data.py @@ -0,0 +1,24 @@ +from viewser import Queryset, Column + +def get_input_data_config(): + + """ + Contains the configuration for the input data in the form of a viewser queryset. That is the data from viewser that is used to train the model. + This configuration is "behavioral" so modifying it will affect the model's runtime behavior and integration into the deployment system. + There is no guarantee that the model will work if the input data configuration is changed here without changing the model settings and architecture accordingly. + + Returns: + queryset_base (Queryset): A queryset containing the base data for the model training. + """ + + queryset_base = (Queryset("purple_alien", "priogrid_month") + .with_column(Column("ln_sb_best", from_table = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_ns_best", from_table = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_os_best", from_table = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("month", from_table = "month", from_column = "month")) + .with_column(Column("year_id", from_table = "country_year", from_column = "year_id")) + .with_column(Column("c_id", from_table = "country_year", from_column = "country_id")) + .with_column(Column("col", from_table = "priogrid", from_column = "col")) + .with_column(Column("row", from_table = "priogrid", from_column = "row"))) + + return queryset_base \ No newline at end of file diff --git a/models/purple_alien/src/utils/utils_dataloaders.py b/models/purple_alien/src/utils/utils_dataloaders.py index 5ba5ffad..076d9f80 100644 --- a/models/purple_alien/src/utils/utils_dataloaders.py +++ b/models/purple_alien/src/utils/utils_dataloaders.py @@ -19,6 +19,7 @@ #from config_partitioner import get_partitioner_dict from set_partition import get_partitioner_dict +from config_input_data import get_input_data_config def get_views_date(partition): @@ -26,16 +27,18 @@ def get_views_date(partition): print('Beginning file download through viewser...') - queryset_base = (Queryset("simon_tests", "priogrid_month") - .with_column(Column("ln_sb_best", from_table = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_ns_best", from_table = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_os_best", from_table = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("month", from_table = "month", from_column = "month")) - .with_column(Column("year_id", from_table = "country_year", from_column = "year_id")) - .with_column(Column("c_id", from_table = "country_year", from_column = "country_id")) - .with_column(Column("col", from_table = "priogrid", from_column = "col")) - .with_column(Column("row", from_table = "priogrid", from_column = "row"))) - + queryset_base = get_input_data_config() + +# queryset_base = (Queryset("simon_tests", "priogrid_month") +# .with_column(Column("ln_sb_best", from_table = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) +# .with_column(Column("ln_ns_best", from_table = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) +# .with_column(Column("ln_os_best", from_table = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) +# .with_column(Column("month", from_table = "month", from_column = "month")) +# .with_column(Column("year_id", from_table = "country_year", from_column = "year_id")) +# .with_column(Column("c_id", from_table = "country_year", from_column = "country_id")) +# .with_column(Column("col", from_table = "priogrid", from_column = "col")) +# .with_column(Column("row", from_table = "priogrid", from_column = "row"))) +# df = queryset_base.publish().fetch() df.reset_index(inplace = True) From 481a1c417af7a0f625a0a65cb64bdb19ba7be6ae Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 22:53:39 +0200 Subject: [PATCH 127/136] naive first viewers 6 test... --- .../purple_alien/configs/config_input_data.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/models/purple_alien/configs/config_input_data.py b/models/purple_alien/configs/config_input_data.py index 0ac89f04..e061f856 100644 --- a/models/purple_alien/configs/config_input_data.py +++ b/models/purple_alien/configs/config_input_data.py @@ -11,14 +11,15 @@ def get_input_data_config(): queryset_base (Queryset): A queryset containing the base data for the model training. """ + # VIEWSER 6 queryset_base = (Queryset("purple_alien", "priogrid_month") - .with_column(Column("ln_sb_best", from_table = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_ns_best", from_table = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_os_best", from_table = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("month", from_table = "month", from_column = "month")) - .with_column(Column("year_id", from_table = "country_year", from_column = "year_id")) - .with_column(Column("c_id", from_table = "country_year", from_column = "country_id")) - .with_column(Column("col", from_table = "priogrid", from_column = "col")) - .with_column(Column("row", from_table = "priogrid", from_column = "row"))) + .with_column(Column("ln_sb_best", from_lao = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_ns_best", from_lao = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_os_best", from_lao = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("month", from_lao = "month", from_column = "month")) + .with_column(Column("year_id", from_lao = "country_year", from_column = "year_id")) + .with_column(Column("c_id", from_lao = "country_year", from_column = "country_id")) + .with_column(Column("col", from_lao = "priogrid", from_column = "col")) + .with_column(Column("row", from_lao = "priogrid", from_column = "row"))) return queryset_base \ No newline at end of file From 93ebc19411d726005226159b5ce216f2d24b6add Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 22:59:36 +0200 Subject: [PATCH 128/136] better help --- models/purple_alien/src/dataloaders/get_partioned_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/src/dataloaders/get_partioned_data.py b/models/purple_alien/src/dataloaders/get_partioned_data.py index b2b8025f..d7e5b1c7 100644 --- a/models/purple_alien/src/dataloaders/get_partioned_data.py +++ b/models/purple_alien/src/dataloaders/get_partioned_data.py @@ -43,20 +43,20 @@ # Process calibration data if flag is set if args.calibration: df_cal, vol_cal = process_data('calibration', PATH) - print(f"Processed calibration data:") + print(f"Fetch calibration data from viewser:") print(f"DataFrame shape: {df_cal.shape if df_cal is not None else 'None'}") print(f"Volume shape: {vol_cal.shape if vol_cal is not None else 'None'}") # Process testing data if flag is set if args.testing: df_test, vol_test = process_data('testing', PATH) - print(f"Processed testing data:") + print(f"Fetch testing data from viewser:") print(f"DataFrame shape: {df_test.shape if df_test is not None else 'None'}") print(f"Volume shape: {vol_test.shape if vol_test is not None else 'None'}") # Process forecasting data if flag is set if args.forecasting: df_forecast, vol_forecast = process_data('forecasting', PATH) - print(f"Processed forecasting data:") + print(f"Fetch forecasting data from viewser:") print(f"DataFrame shape: {df_forecast.shape if df_forecast is not None else 'None'}") print(f"Volume shape: {vol_forecast.shape if vol_forecast is not None else 'None'}") From fc8f71f24fc8b8e173e75b0ba0eb7ff0d1a2865f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 23:00:59 +0200 Subject: [PATCH 129/136] better help --- models/purple_alien/src/dataloaders/get_partioned_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/purple_alien/src/dataloaders/get_partioned_data.py b/models/purple_alien/src/dataloaders/get_partioned_data.py index d7e5b1c7..357d8d46 100644 --- a/models/purple_alien/src/dataloaders/get_partioned_data.py +++ b/models/purple_alien/src/dataloaders/get_partioned_data.py @@ -38,7 +38,7 @@ print("Error: No partition flag provided. Use -c, -t, and/or -f.") sys.exit(1) - print(f"Partitions to be processed: {', '.join(partitions_to_process)}") + print(f"Partitions to be fetched from viewser: {', '.join(partitions_to_process)}") # Process calibration data if flag is set if args.calibration: From 05b0346085d4ae76e45cadbe37e06c6c498b502b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 23:03:46 +0200 Subject: [PATCH 130/136] better help --- models/purple_alien/src/utils/utils_dataloaders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/models/purple_alien/src/utils/utils_dataloaders.py b/models/purple_alien/src/utils/utils_dataloaders.py index 076d9f80..e0549660 100644 --- a/models/purple_alien/src/utils/utils_dataloaders.py +++ b/models/purple_alien/src/utils/utils_dataloaders.py @@ -115,7 +115,7 @@ def df_to_vol(df): def process_partition_data(partition, PATH): """ - Processes data for a given partition by ensuring the existence of necessary directories, + Fetches data for a given partition by ensuring the existence of necessary directories, downloading or loading existing data, and creating or loading a volume. Args: @@ -160,18 +160,18 @@ def process_partition_data(partition, PATH): return df, vol def parse_args(): - parser = argparse.ArgumentParser(description='Process data for different partitions') + parser = argparse.ArgumentParser(description='Fetch data for different partitions') # Add binary flags for each partition - parser.add_argument('-c', '--calibration', action='store_true', help='Process calibration data') - parser.add_argument('-t', '--testing', action='store_true', help='Process testing data') - parser.add_argument('-f', '--forecasting', action='store_true', help='Process forecasting data') + parser.add_argument('-c', '--calibration', action='store_true', help='Fetch calibration data from viewser') + parser.add_argument('-t', '--testing', action='store_true', help='Fetch testing data from viewser') + parser.add_argument('-f', '--forecasting', action='store_true', help='Fetch forecasting data from viewser') return parser.parse_args() def process_data(partition, PATH): """ - Processes the data for the given partition. + Fetch the data for the given partition from viewser. Args: partition (str): The partition type (e.g., 'calibration', 'testing', 'forecasting'). From ef6890385756b9c63eae39f6b7d9095266cae92d Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 23:06:18 +0200 Subject: [PATCH 131/136] fixe typo --- models/purple_alien/configs/config_input_data.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/models/purple_alien/configs/config_input_data.py b/models/purple_alien/configs/config_input_data.py index e061f856..78df58f6 100644 --- a/models/purple_alien/configs/config_input_data.py +++ b/models/purple_alien/configs/config_input_data.py @@ -13,13 +13,13 @@ def get_input_data_config(): # VIEWSER 6 queryset_base = (Queryset("purple_alien", "priogrid_month") - .with_column(Column("ln_sb_best", from_lao = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_ns_best", from_lao = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_os_best", from_lao = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("month", from_lao = "month", from_column = "month")) - .with_column(Column("year_id", from_lao = "country_year", from_column = "year_id")) - .with_column(Column("c_id", from_lao = "country_year", from_column = "country_id")) - .with_column(Column("col", from_lao = "priogrid", from_column = "col")) - .with_column(Column("row", from_lao = "priogrid", from_column = "row"))) + .with_column(Column("ln_sb_best", from_loa = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_ns_best", from_loa = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_os_best", from_loa = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("month", from_loa = "month", from_column = "month")) + .with_column(Column("year_id", from_loa = "country_year", from_column = "year_id")) + .with_column(Column("c_id", from_loa = "country_year", from_column = "country_id")) + .with_column(Column("col", from_loa = "priogrid", from_column = "col")) + .with_column(Column("row", from_loa = "priogrid", from_column = "row"))) return queryset_base \ No newline at end of file From ee8b818b4fcdd5e864c4c454c0d0c6d01f78829f Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 23:10:09 +0200 Subject: [PATCH 132/136] right loa now? --- models/purple_alien/configs/config_input_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/purple_alien/configs/config_input_data.py b/models/purple_alien/configs/config_input_data.py index 78df58f6..13cf56d5 100644 --- a/models/purple_alien/configs/config_input_data.py +++ b/models/purple_alien/configs/config_input_data.py @@ -13,9 +13,9 @@ def get_input_data_config(): # VIEWSER 6 queryset_base = (Queryset("purple_alien", "priogrid_month") - .with_column(Column("ln_sb_best", from_loa = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_ns_best", from_loa = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) - .with_column(Column("ln_os_best", from_loa = "ged2_pgm", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_sb_best", from_loa = "priogrid_month", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_ns_best", from_loa = "priogrid_month", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) + .with_column(Column("ln_os_best", from_loa = "priogrid_month", from_column = "ged_os_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) .with_column(Column("month", from_loa = "month", from_column = "month")) .with_column(Column("year_id", from_loa = "country_year", from_column = "year_id")) .with_column(Column("c_id", from_loa = "country_year", from_column = "country_id")) From ca2a714c67cf8a2134e73729c640ffa2ba34ab5b Mon Sep 17 00:00:00 2001 From: Polichinl Date: Tue, 11 Jun 2024 23:57:31 +0200 Subject: [PATCH 133/136] seems correct --- .../purple_alien/notebooks/check_data.ipynb | 479 ++++++++++++++++-- 1 file changed, 448 insertions(+), 31 deletions(-) diff --git a/models/purple_alien/notebooks/check_data.ipynb b/models/purple_alien/notebooks/check_data.ipynb index 4d905656..458a0d34 100644 --- a/models/purple_alien/notebooks/check_data.ipynb +++ b/models/purple_alien/notebooks/check_data.ipynb @@ -2,26 +2,9 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Root path: /home/simon/Documents/scripts/views_pipeline\n", - "Common utils path: /home/simon/Documents/scripts/views_pipeline/common_utils\n", - "Common configs path: /home/simon/Documents/scripts/views_pipeline/common_configs\n", - "Adding /home/simon/Documents/scripts/views_pipeline/common_configs to sys.path\n", - "Adding /home/simon/Documents/scripts/views_pipeline/models/purple_alien/configs to sys.path\n", - "Adding /home/simon/Documents/scripts/views_pipeline/models/purple_alien/src/utils to sys.path\n", - "Adding /home/simon/Documents/scripts/views_pipeline/models/purple_alien/src/architectures to sys.path\n", - "Root path: /home/simon/Documents/scripts/views_pipeline\n", - "Common utils path: /home/simon/Documents/scripts/views_pipeline/common_utils\n", - "Common configs path: /home/simon/Documents/scripts/views_pipeline/common_configs\n" - ] - } - ], + "outputs": [], "source": [ "# on SIMON local, use conda env pytroch_2023\n", "\n", @@ -56,21 +39,96 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 8, "metadata": {}, "outputs": [ + { + "data": { + "text/html": [ + "Finishing last run (ID:yccnhqao) before initializing another..." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "name": "stderr", "output_type": "stream", "text": [ - "Failed to detect the name of this notebook, you can set it manually with the WANDB_NOTEBOOK_NAME environment variable to enable code saving.\n", - "\u001b[34m\u001b[1mwandb\u001b[0m: Currently logged in as: \u001b[33msimpol\u001b[0m (\u001b[33mnornir\u001b[0m). Use \u001b[1m`wandb login --relogin`\u001b[0m to force relogin\n" + "wandb: WARNING Source type is set to 'repo' but some required information is missing from the environment. A job will not be created from this run. See https://docs.wandb.ai/guides/launch/create-job\n" ] }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f7760cd8d0ef429ebc16071e258f6664", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Label(value='0.018 MB of 0.028 MB uploaded\\r'), FloatProgress(value=0.6680237372343362, max=1.0…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "data": { "text/html": [ - "wandb version 0.16.4 is available! To upgrade, please run:\n", + " View run eager-frog-33 at: https://wandb.ai/nornir/views_pipeline-models_purple_alien_notebooks/runs/yccnhqao
Synced 6 W&B file(s), 0 media file(s), 0 artifact file(s) and 0 other file(s)" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Find logs at: ./wandb/run-20240611_234518-yccnhqao/logs" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "Successfully finished last run (ID:yccnhqao). Initializing new run:
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ddcb25a750d44f99bcab39bfd898161d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Label(value='Waiting for wandb.init()...\\r'), FloatProgress(value=0.011112510433304124, max=1.0…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "wandb version 0.17.1 is available! To upgrade, please run:\n", " $ pip install wandb --upgrade" ], "text/plain": [ @@ -95,7 +153,7 @@ { "data": { "text/html": [ - "Run data is saved locally in /home/simon/Documents/scripts/views_pipeline/models/purple_alien/notebooks/wandb/run-20240313_133931-woepx4u9" + "Run data is saved locally in /home/simon/Documents/scripts/views_pipeline/models/purple_alien/notebooks/wandb/run-20240611_234606-5xa0te9b" ], "text/plain": [ "" @@ -107,7 +165,7 @@ { "data": { "text/html": [ - "Syncing run mild-plasma-27 to Weights & Biases (docs)
" + "Syncing run radiant-wildflower-34 to Weights & Biases (docs)
" ], "text/plain": [ "" @@ -131,7 +189,7 @@ { "data": { "text/html": [ - " View run at https://wandb.ai/nornir/views_pipeline-models_purple_alien_notebooks/runs/woepx4u9" + " View run at https://wandb.ai/nornir/views_pipeline-models_purple_alien_notebooks/runs/5xa0te9b" ], "text/plain": [ "" @@ -145,12 +203,254 @@ "# this jazz is just to emulate the behavior of the scripts which all uses the waandb.init() to get the config\n", "\n", "config_dict = get_hp_config()\n", - "config_dict['model_type'] = 'calibration'\n", + "config_dict['run_type'] = 'calibration'\n", "\n", "wandb.init(config=config_dict)\n", "config = wandb.config" ] }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import pickle" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Index(['month_id', 'pg_id', 'month', 'year_id', 'c_id', 'col', 'row',\n", + " 'ln_sb_best', 'ln_ns_best', 'ln_os_best', 'in_viewser', 'abs_row',\n", + " 'abs_col', 'abs_month'],\n", + " dtype='object')\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
month_idpg_idmonthyear_idc_idcolrowln_sb_bestln_ns_bestln_os_bestabs_rowabs_colabs_month
count4.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+064.247640e+06
mean2.825000e+021.447941e+056.500000e+002.003000e+031.404556e+024.076765e+022.015367e+024.476676e-031.078898e-031.940727e-031.145367e+029.767651e+011.615000e+02
std9.353031e+012.670816e+043.452053e+007.788882e+006.559149e+013.667092e+013.709581e+018.413804e-023.748633e-024.695119e-023.709581e+013.667092e+019.353031e+01
min1.210000e+026.235600e+041.000000e+001.990000e+034.000000e+013.100000e+028.700000e+010.000000e+000.000000e+000.000000e+000.000000e+000.000000e+000.000000e+00
25%2.017500e+021.264360e+053.750000e+001.996000e+037.000000e+013.830000e+021.760000e+020.000000e+000.000000e+000.000000e+008.900000e+017.300000e+018.075000e+01
50%2.825000e+021.494575e+056.500000e+002.003000e+031.540000e+024.100000e+022.080000e+020.000000e+000.000000e+000.000000e+001.210000e+021.000000e+021.615000e+02
75%3.632500e+021.660120e+059.250000e+002.010000e+031.910000e+024.350000e+022.310000e+020.000000e+000.000000e+000.000000e+001.440000e+021.250000e+022.422500e+02
max4.440000e+021.905110e+051.200000e+012.016000e+032.540000e+024.870000e+022.650000e+025.986452e+004.564348e+006.336826e+001.780000e+021.770000e+023.230000e+02
\n", + "
" + ], + "text/plain": [ + " month_id pg_id month year_id c_id \\\n", + "count 4.247640e+06 4.247640e+06 4.247640e+06 4.247640e+06 4.247640e+06 \n", + "mean 2.825000e+02 1.447941e+05 6.500000e+00 2.003000e+03 1.404556e+02 \n", + "std 9.353031e+01 2.670816e+04 3.452053e+00 7.788882e+00 6.559149e+01 \n", + "min 1.210000e+02 6.235600e+04 1.000000e+00 1.990000e+03 4.000000e+01 \n", + "25% 2.017500e+02 1.264360e+05 3.750000e+00 1.996000e+03 7.000000e+01 \n", + "50% 2.825000e+02 1.494575e+05 6.500000e+00 2.003000e+03 1.540000e+02 \n", + "75% 3.632500e+02 1.660120e+05 9.250000e+00 2.010000e+03 1.910000e+02 \n", + "max 4.440000e+02 1.905110e+05 1.200000e+01 2.016000e+03 2.540000e+02 \n", + "\n", + " col row ln_sb_best ln_ns_best ln_os_best \\\n", + "count 4.247640e+06 4.247640e+06 4.247640e+06 4.247640e+06 4.247640e+06 \n", + "mean 4.076765e+02 2.015367e+02 4.476676e-03 1.078898e-03 1.940727e-03 \n", + "std 3.667092e+01 3.709581e+01 8.413804e-02 3.748633e-02 4.695119e-02 \n", + "min 3.100000e+02 8.700000e+01 0.000000e+00 0.000000e+00 0.000000e+00 \n", + "25% 3.830000e+02 1.760000e+02 0.000000e+00 0.000000e+00 0.000000e+00 \n", + "50% 4.100000e+02 2.080000e+02 0.000000e+00 0.000000e+00 0.000000e+00 \n", + "75% 4.350000e+02 2.310000e+02 0.000000e+00 0.000000e+00 0.000000e+00 \n", + "max 4.870000e+02 2.650000e+02 5.986452e+00 4.564348e+00 6.336826e+00 \n", + "\n", + " abs_row abs_col abs_month \n", + "count 4.247640e+06 4.247640e+06 4.247640e+06 \n", + "mean 1.145367e+02 9.767651e+01 1.615000e+02 \n", + "std 3.709581e+01 3.667092e+01 9.353031e+01 \n", + "min 0.000000e+00 0.000000e+00 0.000000e+00 \n", + "25% 8.900000e+01 7.300000e+01 8.075000e+01 \n", + "50% 1.210000e+02 1.000000e+02 1.615000e+02 \n", + "75% 1.440000e+02 1.250000e+02 2.422500e+02 \n", + "max 1.780000e+02 1.770000e+02 3.230000e+02 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# new output viewser 6 - seems to fit\n", + "\n", + "# Loading a the pkl df to check columsn\n", + "with open('/home/simon/Documents/scripts/views_pipeline/models/purple_alien/data/raw/calibration_viewser_data.pkl', 'rb') as file: # not machine agnostic\n", + " views_df = pickle.load(file)\n", + "\n", + "print(views_df.columns)\n", + "views_df.describe()" + ] + }, { "cell_type": "code", "execution_count": 3, @@ -373,6 +673,8 @@ } ], "source": [ + "# OLD OUTPUT viewser 5\n", + "\n", "# Loading a the pkl df to check columsn\n", "with open('/home/simon/Documents/scripts/views_pipeline/models/purple_alien/data/raw/calibration_viewser_data.pkl', 'rb') as file: # not machine agnostic\n", " views_df = pickle.load(file)\n", @@ -576,13 +878,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "Loading calibration data from /calibration_vol.npy...\n", "(324, 180, 180, 8)\n", "[ 0. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. 132. 133.\n", " 134. 135. 136. 137. 138. 139. 140. 141. 142. 143. 144. 145. 146. 147.\n", @@ -620,12 +923,12 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] @@ -640,6 +943,120 @@ "plt.show()" ] }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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CBC7du3qsr5f7ep1acnnn38uAIiysjJH25gxY8TTTz/t3s51QEvr09b7SuZtM23aNHHXXXc5tam1baRLXDabDUVFRUhNTXVqT01NRUFBgYd61TFmsxkAEBUV5dS+b98+9OrVCwMGDMBjjz2GyspKT3SvTSdPnoTRaERCQgJmz56N7777DgBQWloKk8nktI20Wi3GjBkjxTay2WzYunUrHn30USiK4miXZbv8XHu2RVFREerr653mMRqNSEpK8vrtZTaboSgKunfv7tT+X//1X4iJicGQIUOwdOlSr036wLXfV7Jum/Pnz+P999/HvHnzmk1TY9tId3f4H3/8EXa7HXq93qldr9fDZDJ5qFeuE0Jg8eLFuP3225GUlORonzRpEu6//3706dMHpaWlWLVqFe666y4UFRV51e1gRo0ahbfffhsDBgzA+fPn8cILL2D06NEoKSlxbIeWtlFZWZknuuuSnTt34uLFi5g7d66jTZbtcrX2bAuTyQSNRoMePXo0m8ebP1OXL1/GihUrMGfOHKc7kD/44INISEiAwWDAsWPHkJGRgS+++MJx+NebtPW+knXb5OTkICIiAjNmzHBqV2vbSFe4mvx8Txi4UgiubvNmCxcuxJdffomDBw86tc+aNcvx76SkJNx0003o06cP3n///WZvAk+aNGmS499Dhw7Frbfein79+iEnJ8cxuCzrNtq0aRMmTZoEo9HoaJNlu7SmI9vCm7dXfX09Zs+ejcbGRrzyyitO0x577DHHv5OSkpCYmIibbroJxcXFGDFiRFd39Zo6+r7y5m0DAG+++SYefPBBhISEOLWrtW2kO1QYExODwMDAZnsblZWVzfYqvdWiRYvw7rvvYu/evYiLi7vmvLGxsejTpw9OnjzZRb3rmPDwcAwdOhQnT550nF0o4zYqKyvDnj178Otf//qa88myXdqzLQwGA2w2G6qqqlqdx5vU19dj5syZKC0tRV5eXpu/XzVixAgEBwd7/bYCmr+vZNs2AHDgwAEcP368zc8Q0PFtI13h0mg0SElJaRYt8/LyMHr0aA/1qn2EEFi4cCF27NiBjz/+GAkJCW0+58KFCygvL0dsbGwX9LDjrFYrvv76a8TGxjoOBfx8G9lsNuTn53v9NnrrrbfQq1cv3HPPPdecT5bt0p5tkZKSguDgYKd5KioqcOzYMa/bXk1F6+TJk9izZw+io6PbfE5JSQnq6+u9flsBzd9XMm2bJps2bUJKSgqSk5PbnLfD26bTp3d4wPbt20VwcLDYtGmT+Oqrr0R6eroIDw8X33//vae7dk1PPvmk0Ol0Yt++faKiosLxqK2tFUIIUV1dLZYsWSIKCgpEaWmp2Lt3r7j11lvFddddJywWi4d772zJkiVi37594rvvvhOffvqpmDx5soiIiHBsgxdffFHodDqxY8cOcfToUfHAAw+I2NhYr1uPn7Pb7aJ3795i+fLlTu3evl2qq6vF4cOHxeHDhwUAsX79enH48GHHmXbt2RZPPPGEiIuLE3v27BHFxcXirrvuEsnJyaKhocFr1qW+vl5MnTpVxMXFiSNHjjh9hqxWqxBCiFOnTok1a9aIwsJCUVpaKt5//30xcOBAceONN3b5urS1Pu19X8mwbZqYzWYRFhYmXn311WbPV3PbSFm4hBDij3/8o+jTp4/QaDRixIgRTqeUeysALT7eeustIYQQtbW1IjU1VfTs2VMEBweL3r17i7S0NHH69GnPdrwFs2bNErGxsSI4OFgYjUYxY8YMUVJS4pje2NgoVq9eLQwGg9BqteKOO+4QR48e9WCP2/bRRx8JAOL48eNO7d6+Xfbu3dvi+yotLU0I0b5tUVdXJxYuXCiioqJEaGiomDx5skfW71rrUlpa2upnaO/evUIIIU6fPi3uuOMOERUVJTQajejXr5946qmnxIULF7p8Xdpan/a+r2TYNk1ef/11ERoaKi5evNjs+WpuG/4eFxERSUW6MS4iIvJvLFxERCQVFi4iIpIKCxcREUmFhYuIiKTCwkVERFJh4SIiIqmwcBERkVRYuIiISCosXEREJBUWLiIiksr/BzA/jmPKJsNWAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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IL84P/vsM99jixYt0uLFi7VkyRJt375dTqdTY8aM8S2QHG62b9/uty+FhYWSpNtuu83XJ1zH5cSJExoyZIiWLFnS5OOtGYvc3FytXbtWa9as0datW1VVVaXx48errq6us3ZD0rn35eTJk9q5c6eefPJJ7dy5U2+++aa++OILTZw4sVHfe++912+sXnnllc4Iv5GWxkZq+XVlhbGR5LcP5eXl+sMf/iCbzaZ//Md/9OsXkLExFnT11Veb+++/369t4MCB5vHHHw9RRO1TWVlpJJmioiJf2/Tp083NN98cuqBa6emnnzZDhgxp8rH6+nrjdDrNr3/9a1/b6dOnjcPhMC+//HInRdgxDz/8sLnoootMfX29McY64yLJrF271ne/NWNx7NgxExcXZ9asWePr8/XXX5suXbqY999/v9NiP9vZ+9KUjz/+2EgypaWlvrYRI0aYhx9+OLjBtUNT+9PS68rKY3PzzTebG2+80a8tUGNjuYrL6/WquLhYOTk5fu05OTnatm1biKJqH7fbLUlKSUnxa9+8ebN69eqlAQMG6N5771VlZWUowmvR/v375XK5lJGRoalTp+qrr76SJJWUlKiiosJvjOx2u0aMGGGJMfJ6vVq1apV++tOfymaz+dqtMi7f15qxKC4uVk1NjV8fl8ulrKyssB8vt9stm82m8847z6/9X//1X9WzZ08NGjRIjzzySNhW+tK5X1dWHZtvvvlG77zzju65555GjwVibCy3Ovy3336ruro6paWl+bWnpaWpoqIiRFG1nTFGc+bM0XXXXaesrCxf+7hx43TbbbepX79+Kikp0ZNPPqkbb7xRxcXFYbUczLBhw/Taa69pwIAB+uabb/TLX/5Sw4cP1969e33j0NQYlZaWhiLcNlm3bp2OHTumGTNm+NqsMi5na81YVFRUKD4+Xj169GjUJ5zfU6dPn9bjjz+uadOm+a1AfscddygjI0NOp1N79uzR/Pnz9cknn/hO/4aTll5XVh2bFStWKCkpSZMnT/ZrD9TYWC5xNfj+kbB0JhGc3RbOZs2apU8//VRbt271a7/99tt9/87KytJVV12lfv366Z133mn0IgilcePG+f49ePBgXXvttbrooou0YsUK3+SyVcdo6dKlGjdunFwul6/NKuPSnPaMRTiPV01NjaZOnar6+nq9+OKLfo/de++9vn9nZWUpMzNTV111lXbu3Kkrr7yys0M9p/a+rsJ5bCTpD3/4g+644w517drVrz1QY2O5U4U9e/ZUTExMo6ONysrKRkeV4Wr27Nl66623tGnTJvXp0+ecfXv37q1+/fpp//79nRRd+yQmJmrw4MHav3+/7+pCK45RaWmpNmzYoJ/97Gfn7GeVcWnNWDidTnm9Xh09erTZPuGkpqZGU6ZMUUlJiQoLC1v8/aorr7xScXFxYT9WUuPXldXGRpI+/PBD7du3r8X3kNT+sbFc4oqPj1d2dnaj0rKwsFDDhw8PUVStY4zRrFmz9Oabb2rjxo3KyMho8TlHjhxRWVmZevfu3QkRtl91dbU+//xz9e7d23cq4Ptj5PV6VVRUFPZjtGzZMvXq1Us33XTTOftZZVxaMxbZ2dmKi4vz61NeXq49e/aE3Xg1JK39+/drw4YNSk1NbfE5e/fuVU1NTdiPldT4dWWlsWmwdOlSZWdna8iQIS32bffYdPjyjhBYs2aNiYuLM0uXLjX/9V//ZXJzc01iYqL561//GurQzumBBx4wDofDbN682ZSXl/tuJ0+eNMYYc/z4cTN37lyzbds2U1JSYjZt2mSuvfZac8EFFxiPxxPi6P3NnTvXbN682Xz11Vfmo48+MuPHjzdJSUm+Mfj1r39tHA6HefPNN81nn31mfvKTn5jevXuH3X58X11dnenbt6+ZN2+eX3u4j8vx48fNrl27zK5du4wks3jxYrNr1y7flXatGYv777/f9OnTx2zYsMHs3LnT3HjjjWbIkCGmtrY2bPalpqbGTJw40fTp08fs3r3b7z1UXV1tjDHmwIED5plnnjHbt283JSUl5p133jEDBw40V1xxRafvS0v709rXlRXGpoHb7TbdunUzL730UqPnB3JsLJm4jDHmd7/7nenXr5+Jj483V155pd8l5eFKUpO3ZcuWGWOMOXnypMnJyTHnn3++iYuLM3379jXTp083Bw8eDG3gTbj99ttN7969TVxcnHG5XGby5Mlm7969vsfr6+vN008/bZxOp7Hb7eb66683n332WQgjbtkHH3xgJJl9+/b5tYf7uGzatKnJ19X06dONMa0bi1OnTplZs2aZlJQUk5CQYMaPHx+S/TvXvpSUlDT7Htq0aZMxxpiDBw+a66+/3qSkpJj4+Hhz0UUXmYceesgcOXKk0/elpf1p7evKCmPT4JVXXjEJCQnm2LFjjZ4fyLHh97gAAJZiuTkuAEB0I3EBACyFxAUAsBQSFwDAUkhcAABLIXEBACyFxAUAsBQSFwDAUkhcAABLIXEBACyFxAUAsJT/HyAd0niJI3DlAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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ydY3APDFF1/g+PHj9X6GgKb3jXSJKygoCElJSbVKy5ycHAwYMMBNUTWMEAKzZs3C5s2b8fnnnyMuLq7e11y4cAFFRUWIjo5ugQibzmKx4LvvvkN0dLTjVMCv+8hqtSI3N9fj++jdd99Fu3btcNddd91wPVn6pSF9kZSUhMDAQKd1iouLcfToUY/rr5qkdfLkSezYsQORkZH1vubYsWOoqqry+L4Car+vZOqbGmvXrkVSUhISExPrXbfJfdPs6R1usGnTJhEYGCjWrl0r/vvf/4rU1FQRGhoqfvzxR3eHdkNPPvmk0Gq1YteuXaK4uNjxqKioEEIIUVZWJubOnSvy8vJEQUGB2Llzp7j99tvFzTffLMxms5ujdzZ37lyxa9cucfr0abF3714xevRoERYW5uiDV155RWi1WrF582bx7bffivvuu09ER0d73H78ms1mEx06dBALFixwavf0fikrKxMHDx4UBw8eFADEihUrxMGDBx0z7RrSF0888YRo37692LFjhzhw4IC48847RWJioqiurvaYfamqqhIpKSmiffv24tChQ06fIYvFIoQQ4tSpU2Lp0qVi3759oqCgQGzbtk107dpV9O7du8X3pb79aej7Soa+qWEymUSrVq3Em2++Wev1avaNlIlLCCHeeOMNERsbK4KCgkSfPn2cppR7KgB1Pt59910hhBAVFRUiOTlZ3HTTTSIwMFB06NBBTJkyRZw5c8a9gddh4sSJIjo6WgQGBgqDwSDGjx8vjh075lhut9vFkiVLhF6vFxqNRgwePFh8++23boy4fp988okAII4fP+7U7un9snPnzjrfV1OmTBFCNKwvKisrxaxZs0RERIQICQkRo0ePdsv+3WhfCgoKrvsZ2rlzpxBCiDNnzojBgweLiIgIERQUJG655Rbx1FNPiQsXLrT4vtS3Pw19X8nQNzXefvttERISIi5dulTr9Wr2DX+Pi4iIpCLdGBcREfk2Ji4iIpIKExcREUmFiYuIiKTCxEVERFJh4iIiIqkwcRERkVSYuIiISCpMXEREJBUmLiIikgoTFxERSeX/AzWwvC5+xOxgAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAa4AAAGxCAYAAAA6dVLUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA3MUlEQVR4nO3de3hTZb4+/Hv1kPRAG2ihDYECBcogtAIWRBkVECh2LAcZBcStoIwvykErIIeXUYpbWsH5IfMOW9TZDHRgEGdmU8YRFcoGigyi0ILSjsNBalukoQolaWlp2vT5/cHbjKHHtCuHJ7k/15XrImutpN/FSvJd93pWVhQhhAAREZEk/NxdABERkSPYuIiISCpsXEREJBU2LiIikgobFxERSYWNi4iIpMLGRUREUmHjIiIiqbBxERGRVNi4yGfNmTMHiqJAURTEx8e3+3kqKyuRmpoKg8GAoKAgDB06FLt27epQbb/+9a+RkpKCHj16QFEUzJkzp8nl/vu//xtTp05Fnz59EBwcjP79++P5559HaWlpi89/5coVREZGQlEU/PWvf221nuvXr9v+rxRFwW9+85v2rBaRKti4yKfp9Xp8/vnn2LlzZ7ufY9q0acjMzMTq1avxySefYMSIEXj88cc79JxvvfUWrl69ismTJ0Oj0TS73OrVq9GpUyekp6fj008/xbJly/DRRx8hMTERV65cafZxCxYsQFBQUJvrCQsLw+eff47du3c7tB5ETiGIfNTs2bNF7969O/Qce/fuFQDEzp077aZPmDBBGAwGUVdX167ntVqttn+HhoaK2bNnN7nclStXGk07ceKEACD+8z//s8nH/PWvfxWdOnUSmZmZAoD4y1/+0ua6CgsLBQDx5ptvtvkxRGpj4iLqgKysLHTq1AmPPfaY3fSnn34aly9fxhdffNGu5/Xza9tbMyoqqtG0xMRE+Pv7o6SkpNG8a9euYcGCBVi7di169erVrtqI3I2Ni6gD8vPzcccddyAgIMBu+p133mmb72o5OTmwWq0YPHhwo3kvvPACYmNjsXDhQpfXRaSWgNYXIaLmXL16FX379m00PSIiwjbflSoqKjB//nzExMTgmWeesZu3d+9e/PnPf0ZeXl6bEx2RJ2LjIuogRVHaNU9tN2/exLRp01BUVISDBw+iU6dOtnkmkwnz5s3D8uXLO3QGJZEnYOMi6oDIyMgmU9W1a9cA/Dt5OVtNTQ0eeeQRHD16FB999BFGjhxpN3/VqlUIDAzEwoULcf36dQC3TuMHgKqqKly/fh06nc6ljZaovdi4iDogISEB77//Purq6uzGuc6cOQMALkk3NTU1mDp1Kg4dOoS//e1vGDduXKNl8vPz8d1330Gv1zeaN3v2bABAeXk5Onfu7OxyiTqMjYuoAx555BH8/ve/x//8z/9gxowZtumZmZkwGAyNko/aGpLWwYMHsXv3bkycOLHJ5TZu3GhLWg1Onz6Nl156CWlpaRg9erTdoUUiT8bGRdQBycnJmDBhAp5//nmYzWb0798f77//Pj799FPs2LED/v7+tmXnzp2LzMxMfPvtt+jdu3eLz5uTk4MffvgBAGC1WlFUVGS7wsXo0aPRrVs3AMCjjz6KTz75BKtWrUJkZCSOHz9ue47w8HAMGjQIADB06NBm/9bgwYMxZswYu789btw4vPrqq3j11Vcd+v8gcgU2LqIO2r17N1atWoVXX30V165dw8CBA/H+++9j5syZdstZrVZYrVYIIVp9ztWrVyMnJ8d2//Dhwzh8+DAA4NChQ7ZG89FHHwEA1q5di7Vr19o9x+jRo22PcYQQAlarFfX19Q4/lsgVFNGWdxGRF5ozZw4OHz6MCxcuQFEUu3REjdXV1aGoqAj9+/fHm2++iaVLl7q7JPJR/DIH+bSioiIEBgZiyJAh7i7Fo12/fh2BgYHo37+/u0shYuIi3/Xdd9/hxx9/BAAEBwc3eaUJusVqteLUqVO2+zExMYiOjnZjReTL2LiIiEgqPFRIRERScWvjevvttxEbG4ugoCAkJibis88+c2c5REQkAbc1rg8++ACpqalYtWoVTp06hfvvvx/JyckoLi52V0lERCQBt41xjRw5EnfddRc2b95sm3bHHXdg6tSpyMjIaPGx9fX1uHz5MsLCwnhtNSIiCQkhUFFRAYPB4PCvFbjlC8gWiwW5ublYsWKF3fSkpCQcO3as0fI1NTWoqamx3f/+++9tVwQgIiJ5lZSUoGfPng49xi2N68cff4TVam10Om10dDSMRmOj5TMyMrBmzZpG00tKShAeHu60OomIyDnMZjNiYmIQFhbm8GPdesmn2w/zCSGaPPS3cuVKLF682Ha/YYXDw8PZuIhUlnbbWzDNiYMJDX/r9r/R3HTyPu0Z7nFL4+ratSv8/f0bpauysrImv9So1Wqh1WpdVR4REXkwtzQujUaDxMREZGdn45FHHrFNz87OxpQpU9xREpHPuz1p3T69vemnued15G/b7jspgTX195n2PJfbDhUuXrwYTz75JIYPH457770X7733HoqLi/Hcc8+5qyQiIpKA2xrXjBkzcPXqVbz22msoLS1FfHw8Pv7441Z/p4iIXKu58Sd3aDaZOaFGV471kWPcenLG/PnzMX/+fHeWQEREkuEPSRKR9NyZAsn1eJFdIiKSChMXEbWIaQao0wi8GgD4WRWOdXkAJi4iohacnFqH//NJGXaurUKdhl3LEzBxEZFXqve/1WT8rB2LjMWDa5FybzEOBPVEvX+Iw99r49mJ6mPjIiKvU6UT+PPqG7jcx4JH/09nDPhH+w8uDd2vxZdBgzHin4EIsKhYJLUbGxcReR1LCOD/0I9IjrmGy39P6FDj6nvCD31PhDSa3lry4tig87BxEZHXCbkORL7XHQX6bhjzJT/mvA23KBF5HU21gl9s1AJw/sW5HU1WvPJ9x/GsQiIikgobFxERSYWHCokIgGddTNcd8ibV4eRD1bb78UeDMOr9QDdWpJ7WtqVshy3ZuIiIAByddgPJj39ju/9+TBzu+XNEh78HRupj4yIiAnDnkRC8HxmHOwZdx10xZU7/e644SaOtqVm2RMYxLiIiAGO2BuLXj0SgfF8U6gVTlidj4iLpqf3jgp62d0mu42dVMPDLIOweGIuhn4W65DBhW1+/HXmujmrqed35PmHjIiL6ift2BGDU+1Hws7q7EmqOIoSQbv/SbDZDp9PBZDIhPDzc3eWQAzz5TDUmLcc4mhTasu1lunzStZ4CeQ/XolO5H+76uz801R5YpAs5+v7pyOc4x7iIiNrhwkgrgjP+hW/XXII5yt3V+BYeKiSn8sQ9ZVKHo3vY3pZow3/wwycF3VBj1EJT5e5qfAsbFxFROwz4h4JeKXr4WYGgSu6huRIblw9yxg/beUOy4sVPyRF+VgUhJndX4Zs4xkVERFJh4iKvSEtE5DuYuIiISCpMXD6EyappHNMiaj93vH+YuIiISCpsXETk8S4PFKj3ZzSmW9i4iMjj/WlzGap07q6CPAXHuLwYx7TaxhnfayN1PDSjDuVvXUBAALsW/ZvqiSsjIwMjRoxAWFgYoqKiMHXqVJw9e9ZuGSEE0tLSYDAYEBwcjDFjxqCgoEDtUohIcnfu80e/6QPw2HN6ftmXbFRvXDk5OViwYAGOHz+O7Oxs1NXVISkpCTdu3LAts379emzYsAGbNm3CiRMnoNfrMWHCBFRUVKhdjk9JU+xv1D78//McISYFA/7hh54Fikt+G4vkoPqhwk8//dTu/tatWxEVFYXc3Fw88MADEEJg48aNWLVqFaZNmwYAyMzMRHR0NHbu3Il58+apXRIREXkRp49xmUy38n1ERAQAoLCwEEajEUlJSbZltFotRo8ejWPHjjXZuGpqalBTU2O7bzabnVy1XJgOnMPR/1dnjo15Ui2uwNc0tcSpZxUKIbB48WLcd999iI+PBwAYjUYAQHR0tN2y0dHRtnm3y8jIgE6ns91iYmKcWTYREXkwpyauhQsX4uuvv8bRo0cbzVMU+10qIUSjaQ1WrlyJxYsX2+6bzWY2L/I4P00JaiWe9iaPlh4nexojclrjWrRoET788EMcOXIEPXv2tE3X6/UAbiWv7t2726aXlZU1SmENtFottFqts0olIiKJqN64hBBYtGgRsrKycPjwYcTGxtrNj42NhV6vR3Z2NoYNGwYAsFgsyMnJwbp169Qux6txHMBzefJvezX3uulorWo876+1AgEWvrCpZaqPcS1YsAA7duzAzp07ERYWBqPRCKPRiOrqagC3DhGmpqYiPT0dWVlZyM/Px5w5cxASEoJZs2apXQ4RSWTXa1W8tBO1SvXEtXnzZgDAmDFj7KZv3boVc+bMAQAsW7YM1dXVmD9/PsrLyzFy5Ejs378fYWFhapdD5FatpWJPSmRtTfC319zqOrbheXemV2FIagGKhvVBvX8I/Kxtq4Xczx1XnnHKocLWKIqCtLQ0pKWlqf3niUhC92QFIb88Acn/CuChQmoVr1VI5EYyXifRGWOrfU/4oe+JIPWfmFzGla9dXh2eiIikwsRF5EF4pqjvaC6h8DXQOiYuIiKSChOXhLhHRiSv1saCGubzfd48Ji4iIpIKGxcREUmFhwolwkMHROSpXHmZMyYuIiKSChOXBJi0iHzPT5OLTJ8BrkheTFxERCQVJi4PIdMeFZGzOHoBXxl58k/eqMmZ68nERUREUmHicjNv3KMkclSzlz/y4i/jOpJIeHkoe0xcREQkFSYuIiI36shYkKOJ1B1jiM4Y62LiIiIiqSiiLT9Z7GHMZjN0Oh1MJhPCw8PdXU67+OqxaaKWtHoBWh9437jjbENn/L+2th4d+Rxn4iIiIqlwjMvFfGGPkchZvPkswwZNrZtM3/nitQqJiIhuwzEuF/HmPUQitbV1r93X3lcdTTOOnuHXnv/ftj43x7iIiMhncIyLiMjLtTeZeurYGhMXERFJhYnLyXztGDyRGnzlCurO0trnjuz/v0xcREQkFZ5V6CRMWkTq4VmGzuWO5MWzComIyGdwjIuIvIYvXFnDGZr7//LUMTCnJ66MjAwoioLU1FTbNCEE0tLSYDAYEBwcjDFjxqCgoMDZpRARkRdwauM6ceIE3nvvPdx5551209evX48NGzZg06ZNOHHiBPR6PSZMmICKigpnluMSaQr39ojIO3jq55nTGldlZSWeeOIJ/P73v0eXLl1s04UQ2LhxI1atWoVp06YhPj4emZmZqKqqws6dO51VDhEReQmnNa4FCxbg4Ycfxvjx4+2mFxYWwmg0IikpyTZNq9Vi9OjROHbsWJPPVVNTA7PZbHcjIt/h6J5/mvDc8RkZeVrycsrJGbt27UJeXh5OnDjRaJ7RaAQAREdH202Pjo5GUVFRk8+XkZGBNWvWqF8oERFJR/XEVVJSghdffBE7duxAUFBQs8spin37FkI0mtZg5cqVMJlMtltJSYmqNavB0/ZIiLwR32cEOCFx5ebmoqysDImJibZpVqsVR44cwaZNm3D27FkAt5JX9+7dbcuUlZU1SmENtFottFqt2qUSEZGEVG9c48aNw5kzZ+ymPf300xg4cCCWL1+Ovn37Qq/XIzs7G8OGDQMAWCwW5OTkYN26dWqXQ0REHeRp44WqN66wsDDEx8fbTQsNDUVkZKRtempqKtLT0xEXF4e4uDikp6cjJCQEs2bNUrscIiLyMm65csayZctQXV2N+fPno7y8HCNHjsT+/fsRFhbmjnI6hMfbiVxP9quby8JT/395kd0OYuMicp/WPlj5/uwYZzaujnyO81qFHXT7huUbhchz8NqF3olXhyciIqmwcRERkVR4qJCIvB4PGbaNp56McTsmLiIikgobFxH5DF581zuwcRERkVTYuIjI5zB5yY2Ni4iIpMKzColIWrz0U8fI+v/GxEVERFJh4iIi6d3+/ay2Jglf/X6XrEmrARMXERFJhYmLiMjLyZ6wbsfERUREUpE6cWXoAC28b2+CiDqGZxve4q3rz8RFRERSkTpxrTQBbv4BZCLyIJZgga8nWlGlE7hzfwCAtp0u2FQykeFMQ29NVK2RunEREf2UMU4g7n9yMKjeiHdXPIpbgwnkbdi4VCLD3hl5t7K+ApWRAl2/UxD+g2++IP2sCirrNbAoAcgbXQlvbVy+mrQacIyLyAvUaQS2ZVzH+b+dw5GnLO4ux23054GKcT/H1uVTsT3pQ3eXQ07CxEXkJSIHVuIBfTH+1KcnvDVptCbAomDgEQWa6kD0v3oF2JkM5Y7tWJ3SFYD8SUX2+tXCxEXkBeo0wJyEM1j3wTZox/3o7nKInMqrEld7r1em5t8kcoY6jcCFewRudhLof9wPna41fuHVww8I8Ed9ve++KOv9BcxRwLUe9ajSaoGeXRBokWv/nKmqdV7VuIi81YV7BJ7+23/hnvxzGHw6A79a1MluvqYaOPbUOPQdcj/u3RjspirdzxwFbP7jD7jzjnJ8HBGPgzN+hvjlIe4ui1TmVY2LeyrkrW52Ergn/xx0e07h2t21jeb7WRXctyMQ9+0IdEN1nqNOA+ijbyIqrBr/rOiGyppAdL3kb5vvjqMyrfGEGmTjVY2LyFv1P+6HhNwMXBtei+lv6NxdjsfqXAqMfLEHKiMMAACdBeh/3L+VR5FsFCGEdP3ebDZDp9PBZDIh3EMuncGxLiJ5ufLKGUxYt3Tkc1yuUUsiIvJ5PFSoEkuwgKaasYvI1SzBtyJMR95/zkhXTFbO45TE9f333+M//uM/EBkZiZCQEAwdOhS5ubm2+UIIpKWlwWAwIDg4GGPGjEFBQYEzSnGZ9e9fQ1lfvlKJXKmsr8AbH1xDxl+u4vJAvv98heqJq7y8HD//+c8xduxYfPLJJ4iKisK3336Lzp0725ZZv349NmzYgG3btmHAgAF4/fXXMWHCBJw9exZhYWFql+QSw4b/iCpdF7T1atRE1HFVOoERiT8gwF+gMjIC7nj/MVm5nuqNa926dYiJicHWrVtt0/r06WP7txACGzduxKpVqzBt2jQAQGZmJqKjo7Fz507MmzdP7ZJcQvufsehaxKZF5EpdixQEvtYX9f6A/hzff75C9bMKBw0ahIkTJ+LSpUvIyclBjx49MH/+fDz77LMAgIsXL6Jfv37Iy8vDsGHDbI+bMmUKOnfujMzMzEbPWVNTg5qaGtt9s9mMmJgYnlVIRE4nQ6LyxO+ntcajziq8ePEiNm/ejLi4OOzbtw/PPfccXnjhBfzxj38EABiNRgBAdHS03eOio6Nt826XkZEBnU5nu8XExKhdNhERSUL1Q4X19fUYPnw40tPTAQDDhg1DQUEBNm/ejKeeesq2nKLY7yIIIRpNa7By5UosXrzYdr8hcXmShj0cJi8icjUZEpaaVE9c3bt3x6BBg+ym3XHHHSguLgYA6PV6AGiUrsrKyhqlsAZarRbh4eF2NyIi8k2qJ66f//znOHv2rN20c+fOoXfv3gCA2NhY6PV6ZGdn28a4LBYLcnJysG7dOrXLISKSSlNHbXwtUbVG9cb10ksvYdSoUUhPT8f06dPx5Zdf4r333sN7770H4NYhwtTUVKSnpyMuLg5xcXFIT09HSEgIZs2apXY5RETkZVRvXCNGjEBWVhZWrlyJ1157DbGxsdi4cSOeeOIJ2zLLli1DdXU15s+fj/LycowcORL79++X9jtcRETkOrzIrpPxZA0i96nTCByYZ8HlfrV4MDMEfU61f1i/vYfr1PgM8MZDhR51OjwRkae42QkoeaYMI579F/51X+PfMSM58SK7TsbT5Incp94fGNjrOu4OvoQt3Qe1/oAWNLyHb08/fG+7HhMXEXmtOi0wvEspkkryYexlcXc5pBImLhdh8iJyPU0V8MdTg3DuZ5EY9EVwh54rf7wVu1+4jh3rgjB9TYjTf8bIG8e11MLGRUReq9M1BXPv7YF6/x5I6GDg2j+nAl/c9R52Pnw3qn77IDTV6tRIjuOhQhdLE9yTInIWS7DAscdrcWCeBdd63nqjBVgUaKoV+Fk7lpD6fxWETd1G48Oz/aCpUqNaai8mLiLyGuYooCjtEnp1q8CFHwfh7kvqfcSlbNDi5nv34jErEFTJY/7uxMblJhzzIlKfpgoo+loHoz4IDxvVPaDkZ1UQYlL1Kamd2LiIyGuE/6Ag9ekuqPcHgirdXQ05CxsXEXkV2Q/jcQy8dTw5g4iIpMLE5WYc6yIigEnLEUxcREQkFSYuL1ClE7gUL6CpAnoWKAiwML4Rkfdi4vIC/3rAirPvX8An75XCHOXuaoiInIuJy0M0dXy7reNeARYFljp/AEDxnfWo0vlBfx5MXkQS4NiW49i4vMCAf/ih89RYXLi7Dt9u+g5flGvxyGM9EHXR3ZUREamPjcuDtfV3f4IqFfT6WkFlpD/KAfj5cReOiLwXG5cX6XvCD5pZsQioASIuubsaIiLnYOOSSGvf+QqqVND/uGePa7X3eD6/50behmNb7cezComISCpMXBKS4Wobau9NyrDOROQaTFxERCQVJi6JdTSFtCcV3f63eJyeyDF8z3QcExcREUlFEUJI1//NZjN0Oh1MJhPCw8PdXY7HakhH3riH15aU2dbvwRG5gje+DzuiI5/jTFxERCQVjnF5MW/ew/PmdSOiljFxERGRVJi4yGdwzIvcgUcH1Kd64qqrq8Ovf/1rxMbGIjg4GH379sVrr72G+vp62zJCCKSlpcFgMCA4OBhjxoxBQUGB2qUQEZEXUj1xrVu3Du+88w4yMzMxePBgnDx5Ek8//TR0Oh1efPFFAMD69euxYcMGbNu2DQMGDMDrr7+OCRMm4OzZswgLC1O7JCIil2PSch7VE9fnn3+OKVOm4OGHH0afPn3w6KOPIikpCSdPngRwK21t3LgRq1atwrRp0xAfH4/MzExUVVVh586dapdDRB5g/3wL3vjLNXz5yzp3l0JeQPXGdd999+F///d/ce7cOQDAV199haNHj+IXv/gFAKCwsBBGoxFJSUm2x2i1WowePRrHjh1r8jlrampgNpvtbkQkh3p/gS8mmfDLSeeQf/9Nd5fjdGmCacvZVD9UuHz5cphMJgwcOBD+/v6wWq1Yu3YtHn/8cQCA0WgEAERHR9s9Ljo6GkVFRU0+Z0ZGBtasWaN2qUTkAn5WBff/uTP2XR6E+/YHt7jsP8dacfA/bqD/aS2S3tbAz8ozaKgx1RPXBx98gB07dmDnzp3Iy8tDZmYmfvOb3yAzM9NuOUWxf0EKIRpNa7By5UqYTCbbraSkRO2yiciJxmwNxMK54Rj6sX+Ly/1rlAUTZn2D87Ouok7jouJIOqonrpdffhkrVqzAzJkzAQAJCQkoKipCRkYGZs+eDb1eD+BW8urevbvtcWVlZY1SWAOtVgutVqt2qUTkYQZ8ocHf9wzAgLxgBFjcXU3b8LCg66meuKqqquDnZ/+0/v7+ttPhY2NjodfrkZ2dbZtvsViQk5ODUaNGqV0OEUkk/oA/lj7eBZPfDOJhQmqW6olr0qRJWLt2LXr16oXBgwfj1KlT2LBhA5555hkAtw4RpqamIj09HXFxcYiLi0N6ejpCQkIwa9YstcshIlIVE5b7qd64fve73+GVV17B/PnzUVZWBoPBgHnz5uHVV1+1LbNs2TJUV1dj/vz5KC8vx8iRI7F//35+h4uIiFrFnzUhn8NLPVF7MGmpiz9rQkREPoMX2SUiagGTludh4iIiIqkwcRERNYFJy3MxcRERkVSYuIiIfoJJy/MxcRERkVSYuMhn8Ptb1BymLLkwcRERkVTYuMhn8Af+iLwDGxcREUmFY1xE5LOYwOXExEVERFJh4iKf07CXzbMMfReTltyYuIiISCpMXOSzmLx8D5OWd2DiIiIiqTBxkc9j8vJ+TFrehYmLiIikwsZF9P/jlTWI5MDGRUREUmHjIroNkxeRZ2PjIiIiqbBxERGRVNi4iIhIKvweF1Ez+P0u+XGs0jsxcRERkVTYuIiISCpsXEREJBU2LiLyWmkKxyi9kcON68iRI5g0aRIMBgMURcGePXvs5gshkJaWBoPBgODgYIwZMwYFBQV2y9TU1GDRokXo2rUrQkNDMXnyZFy6dKlDK0JERL7B4cZ148YNDBkyBJs2bWpy/vr167FhwwZs2rQJJ06cgF6vx4QJE1BRUWFbJjU1FVlZWdi1axeOHj2KyspKpKSkwGq1tn9NiIjIJyhCiHafMKooCrKysjB16lQAt9KWwWBAamoqli9fDuBWuoqOjsa6deswb948mEwmdOvWDdu3b8eMGTMAAJcvX0ZMTAw+/vhjTJw4sdW/azabodPpYDKZEB4e3t7yiRzCQ07y4mnxnqcjn+OqjnEVFhbCaDQiKSnJNk2r1WL06NE4duwYACA3Nxe1tbV2yxgMBsTHx9uWuV1NTQ3MZrPdjYiIfJOqjctoNAIAoqOj7aZHR0fb5hmNRmg0GnTp0qXZZW6XkZEBnU5nu8XExKhZNpFqvp5oxY43buD0L3jYm8hZnHJWoaLYH1MRQjSadruWllm5ciVMJpPtVlJSolqtRGo6PLMSI1PP4MCTPCpA5CyqXvJJr9cDuJWqunfvbpteVlZmS2F6vR4WiwXl5eV2qausrAyjRo1q8nm1Wi20Wq2apRI5rC2XgBp0PBi7e8ViyLEQ1xRFLeLYlndSNXHFxsZCr9cjOzvbNs1isSAnJ8fWlBITExEYGGi3TGlpKfLz85ttXESyePC/A7EkOQpJb2vcXQqR13I4cVVWVuLChQu2+4WFhTh9+jQiIiLQq1cvpKamIj09HXFxcYiLi0N6ejpCQkIwa9YsAIBOp8PcuXOxZMkSREZGIiIiAkuXLkVCQgLGjx+v3poRuYGfVYEfh7eInMrhxnXy5EmMHTvWdn/x4sUAgNmzZ2Pbtm1YtmwZqqurMX/+fJSXl2PkyJHYv38/wsLCbI956623EBAQgOnTp6O6uhrjxo3Dtm3b4O/vr8IqERGRN+vQ97jchd/jIndqy/e5iu+sx8VEK3p+44/+x5s/In+9u0D+uDqEmBTEH/CHpppfFnMGjnV5Ho/5HhcR3XJwThW6/O5r/HXJddT7N/+peW6UFeK3/0LBukuojHRhgUQS4w9JEjlBVHEgvi6OhOE7LfyszaeoTtcU5F6MwA+lQQiocWGBRBJj4yJygvHvanDzT72hqW55uQH/8EPPXxjgZ73VxIiodWxcRE6gqVZabVoAEGBREP6D8+sh8iYc4yIiIqkwcRE56KdX0DDGCVzrWQ/9eT9EXOKhPk91+5mgPMtQbkxcRO1U7y+wc7UJP/zlGxz41U13l0PkM9i4iDpAU+OHm5YABNS2bfkqncDFEfUwxnGXn6i9eKiQqJ38rApmrQ6DedOANh8mPDm1DhVrLyLvTARefqwrgip5eJHIUWxcRB0QcUlxaGyr3g8I8BMICHAscdX7C/zYG7AE37rvZwUiLoGNj3wSGxeRCw3/WwAunxuAwdfbdrp8g+vdge1br2BgPxMAoOpmAGKX9sbwPXwLk+/hq57IhTpdUzDgH46npHp/IDy8Fp1Dbl1eQxNQjzr+cgr5KDYuIgl0LgXGPt8DVToDAEBnBfrk8dwq8k1sXEQOasvV4S3BAlWdgaAKdcahAiwK+h/neJY34XfL2o+7bEROsH++BXsPFmHXazfcXQqR12HiImqjtiStBj/2qMWQntewLyYc9f6ixSvEE5Fj2LiInGD8llBc/CoBj571Z9MiUhkbF5ET9CxQ0LMg0N1lkAdqLrk3TOdYV+s4xkVERFJh4iJqhSNjW0TN4etIPUxcREQkFTYuIiKSChsXERFJhWNcREROxLEt9TFxERGRVJi4iIhUxITlfGxcRB7M3E3go5eqURVWj5T/LxT68/xUJOKhQiIPZo4CdE+V4I4nLqKsb727yyHyCExcRM1w5iGf4jvrcXBOFaKKAzH+XQ001U3/sfAy4Ic/90BRp3pMu/jv/cw6jcCBeRZc7leLBzND0OcU90HbwhmXU1LrdcJLPbUdGxeRG1xMtGLY/3MWXxdH4uafekNT3fRy4T8omLO4U6PpNzsBJc+U4Z4BV/CvbxPQ55TWyRUTeQ6Hd9OOHDmCSZMmwWAwQFEU7NmzxzavtrYWy5cvR0JCAkJDQ2EwGPDUU0/h8uXLds9RU1ODRYsWoWvXrggNDcXkyZNx6dKlDq8MkSx6fuOPTz6Jhdjbrdmm1RJNNRD6SST+vq8veuVz/7M1aUK9RJOm2N/I9RxuXDdu3MCQIUOwadOmRvOqqqqQl5eHV155BXl5edi9ezfOnTuHyZMn2y2XmpqKrKws7Nq1C0ePHkVlZSVSUlJgtVrbvyZEEul/3A/LZnbBUy+HtusXkjXVCmb9vyFY8VgXDDrk74QKiTyXw7tqycnJSE5ObnKeTqdDdna23bTf/e53uPvuu1FcXIxevXrBZDJhy5Yt2L59O8aPHw8A2LFjB2JiYnDgwAFMnDixHatBJB81fqeLv/XVMo4beSenj+iaTCYoioLOnTsDAHJzc1FbW4ukpCTbMgaDAfHx8Th27FiTz1FTUwOz2Wx3IyIi3+TUg+M3b97EihUrMGvWLISHhwMAjEYjNBoNunTpYrdsdHQ0jEZjk8+TkZGBNWvWOLNUIpKYK5IVx7M8h9MSV21tLWbOnIn6+nq8/fbbrS4vhICiNP3KWLlyJUwmk+1WUlKidrlERCQJpySu2tpaTJ8+HYWFhTh48KAtbQGAXq+HxWJBeXm5XeoqKyvDqFGjmnw+rVYLrZan+xLRLe4Yu2r4m2onL47DOU71xNXQtM6fP48DBw4gMjLSbn5iYiICAwPtTuIoLS1Ffn5+s42LiIiogcOJq7KyEhcuXLDdLywsxOnTpxEREQGDwYBHH30UeXl5+Oijj2C1Wm3jVhEREdBoNNDpdJg7dy6WLFmCyMhIREREYOnSpUhISLCdZUjkCZy1h01EHeNw4zp58iTGjh1ru7948WIAwOzZs5GWloYPP/wQADB06FC7xx06dAhjxowBALz11lsICAjA9OnTUV1djXHjxmHbtm3w9+f3UYiIqGWKEEK6I6xmsxk6nQ4mk8lu/IzImZi83MeTxoE4xqWOjnyO88qcREQkFV7kjIg8ljenEW9eN2dj4iIiIqkwcRG1Ec8ydB2mEWoJExcREUmFjYuIyMfJ9ttibFxERCQVjnERkcdp2Pv3pLEutRKJJ61TA0+sqSVMXEREJBUmLiIH8exC38Lt7HmYuIiISCpsXETtlCbkGxsg8gZsXEREJBWOcRGRx/LEswvbyxvWwVMwcRERkVTYuIiISCpsXEREJBU2LiKiFvDsUc/DxkVERFLhWYVE5LGYdKgpTFxERCQVJi6iDuK1C6klTI3qY+IiIiKpsHEREZFUeKiQiDyGJx9W4yFhz8HERUREUmHiIiKX8uRU1Ra31+9NFwKWBRMXERFJhYmLiKgDmLRcj4mLiIik4nDjOnLkCCZNmgSDwQBFUbBnz55ml503bx4URcHGjRvtptfU1GDRokXo2rUrQkNDMXnyZFy6dMnRUog8SsPFWLkHTuRcDjeuGzduYMiQIdi0aVOLy+3ZswdffPEFDAZDo3mpqanIysrCrl27cPToUVRWViIlJQVWq9XRcoiIyMc4PMaVnJyM5OTkFpf5/vvvsXDhQuzbtw8PP/yw3TyTyYQtW7Zg+/btGD9+PABgx44diImJwYEDBzBx4kRHSyIiIh+i+hhXfX09nnzySbz88ssYPHhwo/m5ubmora1FUlKSbZrBYEB8fDyOHTvW5HPW1NTAbDbb3YiIyDep3rjWrVuHgIAAvPDCC03ONxqN0Gg06NKli9306OhoGI3GJh+TkZEBnU5nu8XExKhdNhERSULVxpWbm4vf/va32LZtGxTFseuiCCGafczKlSthMplst5KSEjXKJSIiCanauD777DOUlZWhV69eCAgIQEBAAIqKirBkyRL06dMHAKDX62GxWFBeXm732LKyMkRHRzf5vFqtFuHh4XY3Ik/GswuJnEfVxvXkk0/i66+/xunTp203g8GAl19+Gfv27QMAJCYmIjAwENnZ2bbHlZaWIj8/H6NGjVKzHCIi8kIOn1VYWVmJCxcu2O4XFhbi9OnTiIiIQK9evRAZGWm3fGBgIPR6PX72s58BAHQ6HebOnYslS5YgMjISERERWLp0KRISEmxnGRJ5C15R/N+YQEktDjeukydPYuzYsbb7ixcvBgDMnj0b27Zta9NzvPXWWwgICMD06dNRXV2NcePGYdu2bfD393e0HCIi8jGKEEK6/SCz2QydTgeTycTxLpKKLyYvJi1qSkc+x3mtQiIikgqvDk9ETsGkRc7CxEVERFJh4iJyIV84y5BJi5yNiYuIiKTCxkVERFJh4yIiIqlwjIvIDbxxrItjW+QqTFxERCQVJi4iN/KG5MWkRa7GxEVERFJh4iKidmHSIndh4iIiIqkwcRGRQ5i0yN2YuIiISCpMXERuJMPZhExY5GmYuIiISCpMXERu5Mnf42LSIk/FxEVERFJh4yIiIqnwUCGRj+MhQZINExcREUmFiYvIA9yeem4/WYOpiOjfmLiIiEgqTFxEHogJi6h5TFxERCQVNi4iIpIKGxcREUmFjYuIiKTCxkVERFJxuHEdOXIEkyZNgsFggKIo2LNnT6NlvvnmG0yePBk6nQ5hYWG45557UFxcbJtfU1ODRYsWoWvXrggNDcXkyZNx6dKlDq0IERH5Bocb140bNzBkyBBs2rSpyfnffvst7rvvPgwcOBCHDx/GV199hVdeeQVBQUG2ZVJTU5GVlYVdu3bh6NGjqKysREpKCqxWa/vXhIiIfIIihGj3N0YURUFWVhamTp1qmzZz5kwEBgZi+/btTT7GZDKhW7du2L59O2bMmAEAuHz5MmJiYvDxxx9j4sSJrf5ds9kMnU4Hk8mE8PDw9pZPRERu0pHPcVXHuOrr67F3714MGDAAEydORFRUFEaOHGl3ODE3Nxe1tbVISkqyTTMYDIiPj8exY8eafN6amhqYzWa7GxER+SZVG1dZWRkqKyvxxhtv4KGHHsL+/fvxyCOPYNq0acjJyQEAGI1GaDQadOnSxe6x0dHRMBqNTT5vRkYGdDqd7RYTE6Nm2UREJBHVExcATJkyBS+99BKGDh2KFStWICUlBe+8806LjxVCQFGa/hnYlStXwmQy2W4lJSVqlk1ERBJRtXF17doVAQEBGDRokN30O+64w3ZWoV6vh8ViQXl5ud0yZWVliI6ObvJ5tVotwsPD7W5EROSbVG1cGo0GI0aMwNmzZ+2mnzt3Dr179wYAJCYmIjAwENnZ2bb5paWlyM/Px6hRo9Qsh4iIvJDDV4evrKzEhQsXbPcLCwtx+vRpREREoFevXnj55ZcxY8YMPPDAAxg7diw+/fRT/P3vf8fhw4cBADqdDnPnzsWSJUsQGRmJiIgILF26FAkJCRg/frxqK0ZERF5KOOjQoUMCQKPb7Nmzbcts2bJF9O/fXwQFBYkhQ4aIPXv22D1HdXW1WLhwoYiIiBDBwcEiJSVFFBcXt7kGk8kkAAiTyeRo+URE5AE68jneoe9xuQu/x0VEJDeP+R4XERGRs7FxERGRVNi4iIhIKmxcREQkFTYuIiKSChsXERFJhY2LiIikwsZFRERSYeMiIiKpsHEREZFU2LiIiEgqbFxERCQVNi4iIpIKGxcREUmFjYuIiKTCxkVERFJh4yIiIqmwcRERkVTYuIiISCpsXEREJBU2LiIikgobFxERSYWNi4iIpMLGRUREUmHjIiIiqbBxERGRVNi4iIhIKmxcREQkFTYuIiKSChsXERFJhY2LiIikwsZFRERSCXB3Ae0hhAAAmM1mN1dCRETt0fD53fB57ggpG1dFRQUAICYmxs2VEBFRR1RUVECn0zn0GEW0p925WX19Pc6ePYtBgwahpKQE4eHh7i6pw8xmM2JiYrxifbgunsub1ofr4rnasj5CCFRUVMBgMMDPz7FRKykTl5+fH3r06AEACA8P94oN3cCb1ofr4rm8aX24Lp6rtfVxNGk14MkZREQkFTYuIiKSirSNS6vVYvXq1dBqte4uRRXetD5cF8/lTevDdfFczl4fKU/OICIi3yVt4iIiIt/ExkVERFJh4yIiIqmwcRERkVTYuIiISCrSNq63334bsbGxCAoKQmJiIj777DN3l9SqjIwMjBgxAmFhYYiKisLUqVNx9uxZu2XmzJkDRVHsbvfcc4+bKm5eWlpaozr1er1tvhACaWlpMBgMCA4OxpgxY1BQUODGilvWp0+fRuujKAoWLFgAwLO3y5EjRzBp0iQYDAYoioI9e/bYzW/LtqipqcGiRYvQtWtXhIaGYvLkybh06ZIL1+KWltaltrYWy5cvR0JCAkJDQ2EwGPDUU0/h8uXLds8xZsyYRttq5syZLl6TW1rbNm15XcmwbQA0+f5RFAVvvvmmbRm1to2UjeuDDz5AamoqVq1ahVOnTuH+++9HcnIyiouL3V1ai3JycrBgwQIcP34c2dnZqKurQ1JSEm7cuGG33EMPPYTS0lLb7eOPP3ZTxS0bPHiwXZ1nzpyxzVu/fj02bNiATZs24cSJE9Dr9ZgwYYLtAsme5sSJE3brkp2dDQB47LHHbMt46na5ceMGhgwZgk2bNjU5vy3bIjU1FVlZWdi1axeOHj2KyspKpKSkwGq1umo1ALS8LlVVVcjLy8Mrr7yCvLw87N69G+fOncPkyZMbLfvss8/abat3333XFeU30tq2AVp/XcmwbQDYrUNpaSn+8Ic/QFEU/PKXv7RbTpVtIyR09913i+eee85u2sCBA8WKFSvcVFH7lJWVCQAiJyfHNm327NliypQp7iuqjVavXi2GDBnS5Lz6+nqh1+vFG2+8YZt28+ZNodPpxDvvvOOiCjvmxRdfFP369RP19fVCCHm2CwCRlZVlu9+WbXH9+nURGBgodu3aZVvm+++/F35+fuLTTz91We23u31dmvLll18KAKKoqMg2bfTo0eLFF190bnHt0NT6tPa6knnbTJkyRTz44IN209TaNtIlLovFgtzcXCQlJdlNT0pKwrFjx9xUVfuYTCYAQEREhN30w4cPIyoqCgMGDMCzzz6LsrIyd5TXqvPnz8NgMCA2NhYzZ87ExYsXAQCFhYUwGo1220ir1WL06NFSbCOLxYIdO3bgmWeegaIotumybJefasu2yM3NRW1trd0yBoMB8fHxHr+9TCYTFEVB586d7ab/6U9/QteuXTF48GAsXbrUY5M+0PLrStZtc+XKFezduxdz585tNE+NbSPd1eF//PFHWK1WREdH202Pjo6G0Wh0U1WOE0Jg8eLFuO+++xAfH2+bnpycjMceewy9e/dGYWEhXnnlFTz44IPIzc31qMvBjBw5En/84x8xYMAAXLlyBa+//jpGjRqFgoIC23ZoahsVFRW5o1yH7NmzB9evX8ecOXNs02TZLrdry7YwGo3QaDTo0qVLo2U8+T118+ZNrFixArNmzbK7AvkTTzyB2NhY6PV65OfnY+XKlfjqq69sh389SWuvK1m3TWZmJsLCwjBt2jS76WptG+kaV4Of7gkDtxrB7dM82cKFC/H111/j6NGjdtNnzJhh+3d8fDyGDx+O3r17Y+/evY1eBO6UnJxs+3dCQgLuvfde9OvXD5mZmbbBZVm30ZYtW5CcnAyDwWCbJst2aU57toUnb6/a2lrMnDkT9fX1ePvtt+3mPfvss7Z/x8fHIy4uDsOHD0deXh7uuusuV5faova+rjx52wDAH/7wBzzxxBMICgqym67WtpHuUGHXrl3h7+/faG+jrKys0V6lp1q0aBE+/PBDHDp0CD179mxx2e7du6N37944f/68i6prn9DQUCQkJOD8+fO2swtl3EZFRUU4cOAAfvWrX7W4nCzbpS3bQq/Xw2KxoLy8vNllPEltbS2mT5+OwsJCZGdnt/r7VXfddRcCAwM9flsBjV9Xsm0bAPjss89w9uzZVt9DQPu3jXSNS6PRIDExsVG0zM7OxqhRo9xUVdsIIbBw4ULs3r0bBw8eRGxsbKuPuXr1KkpKStC9e3cXVNh+NTU1+Oabb9C9e3fboYCfbiOLxYKcnByP30Zbt25FVFQUHn744RaXk2W7tGVbJCYmIjAw0G6Z0tJS5Ofne9z2amha58+fx4EDBxAZGdnqYwoKClBbW+vx2wpo/LqSads02LJlCxITEzFkyJBWl233tunw6R1usGvXLhEYGCi2bNki/vnPf4rU1FQRGhoqvvvuO3eX1qLnn39e6HQ6cfjwYVFaWmq7VVVVCSGEqKioEEuWLBHHjh0ThYWF4tChQ+Lee+8VPXr0EGaz2c3V21uyZIk4fPiwuHjxojh+/LhISUkRYWFhtm3wxhtvCJ1OJ3bv3i3OnDkjHn/8cdG9e3ePW4+fslqtolevXmL58uV20z19u1RUVIhTp06JU6dOCQBiw4YN4tSpU7Yz7dqyLZ577jnRs2dPceDAAZGXlycefPBBMWTIEFFXV+cx61JbWysmT54sevbsKU6fPm33HqqpqRFCCHHhwgWxZs0aceLECVFYWCj27t0rBg4cKIYNG+bydWltfdr6upJh2zQwmUwiJCREbN68udHj1dw2UjYuIYT4r//6L9G7d2+h0WjEXXfdZXdKuacC0ORt69atQgghqqqqRFJSkujWrZsIDAwUvXr1ErNnzxbFxcXuLbwJM2bMEN27dxeBgYHCYDCIadOmiYKCAtv8+vp6sXr1aqHX64VWqxUPPPCAOHPmjBsrbt2+ffsEAHH27Fm76Z6+XQ4dOtTk62r27NlCiLZti+rqarFw4UIREREhgoODRUpKilvWr6V1KSwsbPY9dOjQISGEEMXFxeKBBx4QERERQqPRiH79+okXXnhBXL161eXr0tr6tPV1JcO2afDuu++K4OBgcf369UaPV3Pb8Pe4iIhIKtKNcRERkW9j4yIiIqmwcRERkVTYuIiISCpsXEREJBU2LiIikgobFxERSYWNi4iIpMLGRUREUmHjIiIiqbBxERGRVP4v1dt+jpwJFYEAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAa4AAAGxCAYAAAA6dVLUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA0GklEQVR4nO3df3yT9b3//2f6K5TaRlugIVKgjnJQiqjoUOYERco4AiJHgeEmKPOGIswOUOTG1OpmK7gPcj5j/jqHCcoYfm5nwjzq1HKEIl90gwJTOgeiXSnSrhNLQqH0R/r+/sEhM7T0Z9LkSh732y23W3PlSvK6eiV5Xc/rfeWKzRhjBACARcSEugAAADqCxgUAsBQaFwDAUmhcAABLoXEBACyFxgUAsBQaFwDAUmhcAABLoXEBACyFxoWoNXv2bNlsNtlsNmVnZ3f6cWpqapSbmyuXy6UePXroiiuu0MaNG7tU209/+lNNnDhRF198sWw2m2bPnt3ifP/5n/+pKVOmaODAgUpMTNSgQYN0//33q6Kiwm++bdu2+Za1pct9993Xaj3Hjx/3m/8Xv/hFl5YP6Iq4UBcAhJLT6dSmTZvUs2fPTj/G1KlTtWvXLj399NMaPHiwNmzYoO9///tqamrSzJkzO/WYzz77rC6//HJNnjxZv/71r8873+OPP64bb7xR+fn5uvjii3XgwAH97Gc/0+9//3vt3btX6enpkqSrrrpKH374YbP7P//883rllVd02223tVpPcnKyPvzwQ1VUVGjq1KmdWiYgYAwQpWbNmmUGDBjQpcd46623jCSzYcMGv+njxo0zLpfLNDY2dupxvV6v7++kpCQza9asFuf7+9//3mzarl27jCTzs5/9rNXnaGpqMpdccokZMGCA3/O1prS01EgyzzzzTLvmB4KBXYVAF2zatEkXXHCB7rjjDr/pd999t44ePao//vGPnXrcmJj2vTX79OnTbNqIESMUGxur8vLyVu+7detWffHFF7r77rvb/XxAOODVCnTB/v37demllyouzn+v++WXX+67vbsVFRXJ6/Vq6NChrc63Zs0axcTE6O677+6myoDAoHEBXXDs2DGlpqY2m3522rFjx7q1nhMnTmjevHnKyMjQPffcc975jh8/rtdff13jxo1T//79u7FCoOs4OAPoIpvN1qnbAu306dOaOnWqysrK9P777+uCCy4477y/+c1vdPr0af3oRz/qtvqAQKFxAV2QlpbWYqr6+uuvJanFNBYMdXV1uu2227Rjxw69+eabGjlyZKvzr1mzRr1799att97aLfUBgcSuQqALhg0bpk8//VSNjY1+0z/55BNJ6tL3w9qrrq5OU6ZM0datW7V582aNHTu21fn37t2rvXv36q677lJ8fHzQ6wMCjcYFdMFtt92mmpoa/e53v/Obvm7dOrlcrjaTT1edTVrvv/++fve732n8+PFt3mfNmjWSpDlz5gS1NiBY2FUIdMGECRM0btw43X///fJ4PBo0aJB++9vf6p133tH69esVGxvrm3fOnDlat26dPv/8cw0YMKDVxy0qKtI//vEPSZLX61VZWZn+67/+S5I0evRo9e7dW5J0++236w9/+IOWLVumtLQ0ffTRR77HSElJ0WWXXeb3uKdPn9aGDRs0atQoXXrpped97rFjx+qxxx7TY4891vF/ChBkNC6gi15//XUtW7ZMjz32mL7++msNGTJEv/3tbzVjxgy/+bxer7xer4wxbT7m448/rqKiIt/1bdu2adu2bZLOfP9qzJgxkqQ333xTkvTUU0/pqaee8nuM0aNH++7zzVqrq6tbPSjDGCOv16umpqY26wRCwWba8y4CItDs2bO1bds2HTp0SDabzS8dobnGxkaVlZVp0KBBeuaZZ7R48eJQl4QoxRgXolpZWZni4+M1fPjwUJcS1o4fP674+HgNGjQo1KUAJC5Er7/97W/66quvJEmJiYltnmkimnm9Xu3du9d3PSMjw3cCX6C70bgAAJbCrkIAgKWEtHE999xzyszMVI8ePTRixAh98MEHoSwHAGABIWtcr732mnJzc7Vs2TLt3btX3/3udzVhwgQdPnw4VCUBACwgZGNcI0eO1FVXXaXnn3/eN+3SSy/VlClTVFBQ0Op9m5qadPToUSUnJ3frSUwBAIFhjNGJEyfkcrk6/HtwIfkCcn19vYqLi/XII4/4Tc/JydHOnTubzV9XV6e6ujrf9S+//LLZGQEAANZTXl6ufv36deg+IWlcX331lbxeb7PDadPT01VZWdls/oKCAj3xxBPNppeXlyslJSVodQIAgsPj8SgjI0PJyckdvm9IT/l07m4+Y0yLu/6WLl2qhQsX+q6fXeCUlBQaFxBgeee8BfOCOJhw9rnOfY7zTUfk6cxwT0gaV69e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5MmTm8177733+q2rF198sTvKb6atdSO1/bqywrqR5LcMFRUV+vWvfy2bzaZ/+7d/85svIOvGWNC3v/1tc9999/lNGzJkiHnkkUdCVFHnVFVVGUmmqKjIN23WrFnm1ltvDV1R7fT444+b4cOHt3hbU1OTcTqd5umnn/ZNO336tHE4HOaFF17opgq75sEHHzTf+ta3TFNTkzHGOutFktm0aZPvenvWxfHjx018fLzZuHGjb54vv/zSxMTEmHfeeafbaj/XucvSkj/96U9GkikrK/NNGz16tHnwwQeDW1wntLQ8bb2urLxubr31VnPTTTf5TQvUurFc4qqvr1dxcbFycnL8pufk5Gjnzp0hqqpz3G63JCk1NdVv+rZt29SnTx8NHjxY9957r6qqqkJRXps+++wzuVwuZWZmasaMGfriiy8kSaWlpaqsrPRbR3a7XaNHj7bEOqqvr9f69et1zz33yGaz+aZbZb18U3vWRXFxsRoaGvzmcblcys7ODvv15Xa7ZbPZdOGFF/pN/81vfqNevXpp6NChWrx4cdgmfan115VV183f//53vfXWW5ozZ06z2wKxbix3dvivvvpKXq9X6enpftPT09NVWVkZoqo6zhijhQsX6vrrr1d2drZv+oQJE3THHXdowIABKi0t1aOPPqqbbrpJxcXFYXU6mJEjR+qVV17R4MGD9fe//10///nPNWrUKJWUlPjWQ0vrqKysLBTldsjmzZt1/PhxzZ492zfNKuvlXO1ZF5WVlUpISNBFF13UbJ5wfk+dPn1ajzzyiGbOnOl3BvI777xTmZmZcjqd2r9/v5YuXao///nPvt2/4aSt15VV1826deuUnJysqVOn+k0P1LqxXOM665tbwtKZRnDutHA2f/58ffzxx9qxY4ff9OnTp/v+zs7O1tVXX60BAwborbfeavYiCKUJEyb4/h42bJiuu+46fetb39K6det8g8tWXUdr1qzRhAkT5HK5fNOssl7OpzPrIpzXV0NDg2bMmKGmpiY999xzfrfde++9vr+zs7OVlZWlq6++Wnv27NFVV13V3aW2qrOvq3BeN5L061//Wnfeead69OjhNz1Q68Zyuwp79eql2NjYZlsbVVVVzbYqw9WCBQv0xhtvaOvWrerXr1+r8/bt21cDBgzQZ5991k3VdU5SUpKGDRumzz77zHd0oRXXUVlZmbZs2aIf/ehHrc5nlfXSnnXhdDpVX1+v6urq884TThoaGjRt2jSVlpaqsLCwzd+vuuqqqxQfHx/260pq/rqy2rqRpA8++EAHDhxo8z0kdX7dWK5xJSQkaMSIEc2iZWFhoUaNGhWiqtrHGKP58+fr9ddf1/vvv6/MzMw273Ps2DGVl5erb9++3VBh59XV1enTTz9V3759fbsCvrmO6uvrVVRUFPbr6OWXX1afPn10yy23tDqfVdZLe9bFiBEjFB8f7zdPRUWF9u/fH3br62zT+uyzz7RlyxalpaW1eZ+SkhI1NDSE/bqSmr+urLRuzlqzZo1GjBih4cOHtzlvp9dNlw/vCIGNGzea+Ph4s2bNGvOXv/zF5ObmmqSkJPO3v/0t1KW16v777zcOh8Ns27bNVFRU+C6nTp0yxhhz4sQJs2jRIrNz505TWlpqtm7daq677jpz8cUXG4/HE+Lq/S1atMhs27bNfPHFF+ajjz4yEydONMnJyb518PTTTxuHw2Fef/1188knn5jvf//7pm/fvmG3HN/k9XpN//79zZIlS/ymh/t6OXHihNm7d6/Zu3evkWRWrlxp9u7d6zvSrj3r4r777jP9+vUzW7ZsMXv27DE33XSTGT58uGlsbAybZWloaDCTJ082/fr1M/v27fN7D9XV1RljjDl06JB54oknzK5du0xpaal56623zJAhQ8yVV17Z7cvS1vK093VlhXVzltvtNj179jTPP/98s/sHct1YsnEZY8yvfvUrM2DAAJOQkGCuuuoqv0PKw5WkFi8vv/yyMcaYU6dOmZycHNO7d28THx9v+vfvb2bNmmUOHz4c2sJbMH36dNO3b18THx9vXC6XmTp1qikpKfHd3tTUZB5//HHjdDqN3W43N9xwg/nkk09CWHHb3n33XSPJHDhwwG96uK+XrVu3tvi6mjVrljGmfeuitrbWzJ8/36SmpprExEQzceLEkCxfa8tSWlp63vfQ1q1bjTHGHD582Nxwww0mNTXVJCQkmG9961vmxz/+sTl27Fi3L0tby9Pe15UV1s1ZL774oklMTDTHjx9vdv9Arht+jwsAYCmWG+MCAEQ3GhcAwFJoXAAAS6FxAQAshcYFALAUGhcAwFJoXAAAS6FxAQAshcYFALAUGhcAwFJoXAAAS/n/AfW0Z4Gq9o9CAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAa4AAAGxCAYAAAA6dVLUAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy88F64QAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA2a0lEQVR4nO3dfVxUZd4/8M/hYQZEGAVyhlFQSlwfIEs0yx7wEddENLfUbEvL9WeZD6SW+jIT2xZS91bve92edl0lXdfuNmG7syy8U8wlNwQttfKhCDCZKMUZEBhguH5/eDPbCMrTGWaumc/79ZrXi7nONWe+h2tmvud7rjNnFCGEABERkSR8XB0AERFRWzBxERGRVJi4iIhIKkxcREQkFSYuIiKSChMXERFJhYmLiIikwsRFRERSYeIiIiKpMHGR15o9ezYURYGiKIiNjW33eiorK5GSkgKj0YiAgADcdttt2L17d4die/7555GUlISePXtCURTMnj272X5//vOfMWXKFPTp0weBgYHo27cvnnrqKZSWljbpa7FYsGrVKvTr1w9dunRBz5498dBDD+HUqVMtxnP58mX7/0pRFPz+97/v0PYRdQQTF3k1g8GATz/9FLt27Wr3OqZOnYqMjAysWbMGH3zwAYYNG4aHH364Q+vctGkTLl68iOTkZGg0muv2W7NmDbp27Yq0tDTs27cPzz33HN577z3Ex8fjhx9+cOg7adIkbN68GXPnzsXevXvx8ssv4/jx47jrrrtQVFR0w3iCg4Px6aefYs+ePe3eJiLVCCIvNWvWLNG7d+8OrWPv3r0CgNi1a5dD+7hx44TRaBT19fXtWq/NZrP/HRQUJGbNmtVsvx9++KFJW15engAgfvvb39rbzp49KwCI559/3qFvbm6uACA2btzYqrgKCwsFALFhw4ZW9SdyBlZcRB2QmZmJrl274qGHHnJof/zxx3HhwgX861//atd6fXxa99bs0aNHk7b4+Hj4+vqipKTE3ubv7w8A0Ol0Dn27desGAAgICGhXnESuwMRF1AEnT57EgAED4Ofn59B+66232pd3tpycHNhsNgwaNMje1rt3b0yePBmbNm3CgQMHUFlZia+//hqLFi1CVFQUZsyY0elxErUXExdRB1y8eBGhoaFN2hvbLl682KnxVFRUYP78+YiMjMQTTzzhsOztt9/GxIkTMXr0aAQHB2PAgAEoKytDTk4Ounfv3qlxEnUEExdRBymK0q5laqupqcHUqVNRVFSEt99+G127dnVY/tRTT+Gdd97Bpk2bkJOTg7feegsajQajR49u8eQMInfi13IXIrqesLCwZquqS5cuAUCz1ZgzWK1WPPDAAzh8+DDee+89DB8+3GH5vn37sHXrVrz99tt48MEH7e2JiYno06cPUlNTsW3btk6JlaijWHERdUBcXBy++uor1NfXO7SfOHECADr0/bDWslqtmDJlCg4cOICsrCyMGTOmSZ/jx48DAIYNG+bQ3q1bN/Tt29clc3FE7cXERdQBDzzwACorK/HOO+84tGdkZMBoNDapfNTWWGl9/PHHeOeddzB+/Phm+xmNRgDAkSNHHNovXryIM2fOoFevXk6Nk0hNPFRI1AETJkzAuHHj8NRTT8FisaBv377429/+hn379mHnzp3w9fW1950zZw4yMjLwzTffoHfv3jdcb05ODn788UcAgM1mQ1FREf7+978DABISEnDTTTcBAB588EF88MEHWLVqFcLCwhwSU0hICAYOHAjg6pekX3jhBTz11FM4f/48hgwZgtLSUmzYsAFVVVVYvHixw3OPGTMGL7zwAl544QV1/lFEanL1F8mIXEWNLyALIURFRYVYtGiRMBgMQqPRiFtvvVX87W9/a/b5AIjCwsIW15mQkCAANHs7cOCAvd/1+gAQCQkJDussLS0VCxYsEH379hUBAQHCaDSKiRMnik8//dSh34EDBwQAsWbNmiZx8QvI5A4UIYTo/HRJ5HqzZ8/GwYMHce7cOSiK4lAdUVP19fUoKipC3759sWHDBixbtszVIZGX4hwXebWioiL4+/tj8ODBrg7FrV2+fBn+/v7o27evq0MhAisu8lrfffcdfvrpJwBAYGCgw5UmyJHNZsOxY8fs9yMjI6HX610YEXkzJi4iIpIKDxUSEZFUXJq4XnnlFURHRyMgIADx8fH45JNPXBkOERFJwGWJ66233kJKSgpWrVqFY8eO4d5778WECRNQXFzsqpCIiEgCLpvjGj58OIYMGYJXX33V3jZgwABMmTIF6enpN3xsQ0MDLly4gODg4E69iCkREalDCIGKigoYjcZW//5cI5dcOaO2thb5+flYsWKFQ3tiYiJyc3Ob9LdarbBarfb733//vf2KAEREJK+SkpI2X3LMJYnrp59+gs1m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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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xkVERFJh4yIin8PkJTc2LiIikgrPKiQiafHST86R9d+NiYuIiKTCxEVE0rv++1kdTRK++v0uWZNWCyYuIiKSChMXEZGXkz1hXY+Ji4iIpCJ14srRAVp4394EETmHZxte463rz8RFRERSkTpxZZoBN/8AMhF5gbaSiQxnGnpromoPExcREUlF6sTlSWTYOyPyVs3+AudigCYNEFkBaOq8+w3pq0mrBRMXEUnvQh/grXUX8OH6apwd6OOf6j6AiYuIvILwEwgIaEbzTz7VOntFDU8le/1qYeMiIun1qAKeXhqBZn+g90l3V0Ou5lWNyx17VxzbInK/gAYFxu/cXYU6mKraxzEuIiKSinclLu6pENFNeOKYlyfUIBsmLiIikopXJS538NXf8yHyBu64piETlvOYuIiISCpMXETk81xxxITJynVckrh++OEHPPHEE4iIiEC3bt0wdOhQlJaW2uYLIZCVlQWj0Yjg4GCMGTMGR48edUUpRETkZVRPXDU1NbjnnnswduxYfPbZZ+jduze+//579OjRw7bMmjVrsHbtWuTl5WHAgAF45ZVXMH78eJSVlSE0NFTtkoiIXIbJquup3rhWr16N6OhovPfee7Zp/fr1s/2/EALr1q3DihUrMHnyZABAfn4+9Ho9Nm3ahLlz56pdEhEReRHVG9cnn3yCCRMm4LHHHkNxcTFuueUWzJs3D08//TQAoLy8HCaTCSkpKbbHaLVajB49GiUlJW02rvr6etTX19vuWywWtcsmImqTDInKE7+f5kqqj3GdPHkS69evR1xcHD7//HM888wzeO655/D+++8DAEwmEwBAr9fbPU6v19vmXS8nJwc6nc52i46OVrtsIiKShOqJq7m5GcOHD0d2djYA4I477sDRo0exfv16PPnkk7blFMV+F0EI0Wpai8zMTCxevNh232KxeFzz4ve5iMhdvD1hXU/1xBUVFYXBgwfbTRs0aBBOnz4NADAYDADQKl1VV1e3SmEttFotwsLC7G5EROSbVE9c99xzD8rKyuymHTt2DDExMQCA2NhYGAwGFBUV4Y477gAANDQ0oLi4GKtXr1a7HCIiqbR11MbXElV7VG9cv/zlL5GcnIzs7GxMmTIFf/3rX/HOO+/gnXfeAXDtEGFGRgays7MRFxeHuLg4ZGdno1u3bpgxY4ba5RARkZdRvXGNGDEChYWFyMzM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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for i in range(10): #(vol.shape[0]):\n", + " true_obs_masked = np.ma.masked_where((views_vol[0,:,:,4] == 0), views_vol[i,:,:,5])\n", + " plt.imshow(true_obs_masked, cmap = 'rainbow')\n", + " plt.title(str(np.unique(views_vol[i,:,:,3]))) # mean wrong since lots of zeros (oceans etc.) Parhaps the zeros should just get a month_id anyway?\n", + " plt.show()" + ] + }, { "cell_type": "code", "execution_count": 8, @@ -2445,7 +2862,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.16" + "version": "3.11.7" } }, "nbformat": 4, From 1c66fa3f1201f4c3227c4dae8149241b86f53f19 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 12 Jun 2024 00:33:15 +0200 Subject: [PATCH 134/136] small comment --- models/purple_alien/src/utils/utils_dataloaders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/models/purple_alien/src/utils/utils_dataloaders.py b/models/purple_alien/src/utils/utils_dataloaders.py index e0549660..9cf0ef1a 100644 --- a/models/purple_alien/src/utils/utils_dataloaders.py +++ b/models/purple_alien/src/utils/utils_dataloaders.py @@ -29,6 +29,7 @@ def get_views_date(partition): queryset_base = get_input_data_config() +# old viewser 5 code # queryset_base = (Queryset("simon_tests", "priogrid_month") # .with_column(Column("ln_sb_best", from_table = "ged2_pgm", from_column = "ged_sb_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) # .with_column(Column("ln_ns_best", from_table = "ged2_pgm", from_column = "ged_ns_best_count_nokgi").transform.ops.ln().transform.missing.replace_na()) From 2252551e42d2546ffc192db70cb43ae7aec663d2 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 12 Jun 2024 00:35:23 +0200 Subject: [PATCH 135/136] one dataloader to rule them all --- .../src/dataloaders/get_calibration_data.py | 18 ------------------ .../src/dataloaders/get_forecasting_data.py | 18 ------------------ ...rtioned_data.py => get_partitioned_data.py} | 0 .../src/dataloaders/get_test_data.py | 18 ------------------ 4 files changed, 54 deletions(-) delete mode 100644 models/purple_alien/src/dataloaders/get_calibration_data.py delete mode 100644 models/purple_alien/src/dataloaders/get_forecasting_data.py rename models/purple_alien/src/dataloaders/{get_partioned_data.py => get_partitioned_data.py} (100%) delete mode 100644 models/purple_alien/src/dataloaders/get_test_data.py diff --git a/models/purple_alien/src/dataloaders/get_calibration_data.py b/models/purple_alien/src/dataloaders/get_calibration_data.py deleted file mode 100644 index 9de7edeb..00000000 --- a/models/purple_alien/src/dataloaders/get_calibration_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Use viewser env - -import sys -from pathlib import Path - -PATH = Path(__file__) -sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths -setup_project_paths(PATH) - -from config_hyperparameters import get_hp_config -from utils_dataloaders import get_views_date, df_to_vol, process_partition_data - -if __name__ == "__main__": - - partition = 'calibration' # 'calibration', 'forecasting', 'testing' - - df, vol = process_partition_data(partition, get_views_date, df_to_vol, PATH) \ No newline at end of file diff --git a/models/purple_alien/src/dataloaders/get_forecasting_data.py b/models/purple_alien/src/dataloaders/get_forecasting_data.py deleted file mode 100644 index 27429d52..00000000 --- a/models/purple_alien/src/dataloaders/get_forecasting_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Use viewser env - -import sys -from pathlib import Path - -PATH = Path(__file__) -sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths -setup_project_paths(PATH) - -from config_hyperparameters import get_hp_config -from utils_dataloaders import get_views_date, df_to_vol, process_partition_data - -if __name__ == "__main__": - - partition = 'forecasting' # 'calibration', 'forecasting', 'testing' - - df, vol = process_partition_data(partition, get_views_date, df_to_vol, PATH) \ No newline at end of file diff --git a/models/purple_alien/src/dataloaders/get_partioned_data.py b/models/purple_alien/src/dataloaders/get_partitioned_data.py similarity index 100% rename from models/purple_alien/src/dataloaders/get_partioned_data.py rename to models/purple_alien/src/dataloaders/get_partitioned_data.py diff --git a/models/purple_alien/src/dataloaders/get_test_data.py b/models/purple_alien/src/dataloaders/get_test_data.py deleted file mode 100644 index 14b7a08b..00000000 --- a/models/purple_alien/src/dataloaders/get_test_data.py +++ /dev/null @@ -1,18 +0,0 @@ -# Use viewser env - -import sys -from pathlib import Path - -PATH = Path(__file__) -sys.path.insert(0, str(Path(*[i for i in PATH.parts[:PATH.parts.index("views_pipeline")+1]]) / "common_utils")) # PATH_COMMON_UTILS -from set_path import setup_project_paths -setup_project_paths(PATH) - -from config_hyperparameters import get_hp_config -from utils_dataloaders import get_views_date, df_to_vol, process_partition_data - -if __name__ == "__main__": - - partition = 'testing' # 'calibration', 'forecasting', 'testing' - - df, vol = process_partition_data(partition, get_views_date, df_to_vol, PATH) \ No newline at end of file From 1b9b9cd6c164fe52e9894420f14c48bfce32c758 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Wed, 12 Jun 2024 11:15:00 +0200 Subject: [PATCH 136/136] set entity for sweep - I think --- models/purple_alien/src/management/execute_model_runs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/purple_alien/src/management/execute_model_runs.py b/models/purple_alien/src/management/execute_model_runs.py index bf0f6484..e5387161 100644 --- a/models/purple_alien/src/management/execute_model_runs.py +++ b/models/purple_alien/src/management/execute_model_runs.py @@ -22,9 +22,9 @@ def execute_sweep_run(args): sweep_config['parameters']['run_type'] = {'value' : "calibration"} # I see no reason to run the other types in the sweep sweep_config['parameters']['sweep'] = {'value' : True} - sweep_id = wandb.sweep(sweep_config, project=project) # and then you put in the right project name + sweep_id = wandb.sweep(sweep_config, project=project, entity='views_pipeline') # entity is the team name - wandb.agent(sweep_id, execute_model_tasks) + wandb.agent(sweep_id, execute_model_tasks, entity='views_pipeline') # entity is the team name - Seem like it needs to be botb in sweep_id and agent def execute_single_run(args):