diff --git a/examples/case_study/Readme.md b/examples/case_study/Readme.md index c08d5a3..70aeebd 100644 --- a/examples/case_study/Readme.md +++ b/examples/case_study/Readme.md @@ -10,8 +10,8 @@ Additionally install *tqdm*: ``pip install tqdm`` ## Data -Download monthly data *2024-01.csv* from -https://files.stlouisfed.org/files/htdocs/fred-md/monthly/2024-01.csv +Download monthly data *2024-07.csv* from +https://files.stlouisfed.org/files/htdocs/fred-md/monthly/2024-07.csv and place it in the current directory. ## Files diff --git a/examples/case_study/analyse_losses.py b/examples/case_study/analyse_losses.py new file mode 100644 index 0000000..59e9b59 --- /dev/null +++ b/examples/case_study/analyse_losses.py @@ -0,0 +1,34 @@ +# %% +import pandas as pd +from pathlib import Path +import matplotlib.pyplot as plt + +# %% +dir = Path(__file__).parent +areas = ["output_and_income", "consumption_and_orders", "prices"] +df_collection = [] +for area in areas: + df = pd.read_csv(dir / f"overall_losses_{area}.csv", index_col=[0, 1, 2]) + df.index.set_names(["var", "model", "rolling_origin"], inplace=True) + df.columns = range(1, len(df.columns) + 1) + df.columns.name = "Forecast Step" + df_collection.append(df) + +# %% Overall results +dfs = pd.concat(df_collection, keys=areas, axis=0) +dfs.groupby(level=(2)).mean().round(3).to_latex(dir / f"overall_results.tex") + +# %% Results for first step over three variable groups +forecast_step = 1 + +dfs = pd.concat([df[forecast_step] for df in df_collection], keys=areas, axis=1) +mean_error_per_group = dfs.groupby(level=1).mean() + +# Plot +ax = mean_error_per_group.rank().plot(kind='bar', figsize=(10, 4)) +ax.spines[['right', 'top']].set_visible(False) +plt.ylabel('Rank') +plt.xlabel('Model') +plt.legend(bbox_to_anchor=(1,1)) +plt.tight_layout() +plt.savefig(dir / 'model_ranking_in_groups.pdf') \ No newline at end of file diff --git a/examples/case_study/benchmark.py b/examples/case_study/benchmark.py index e6aead8..3d14199 100644 --- a/examples/case_study/benchmark.py +++ b/examples/case_study/benchmark.py @@ -3,21 +3,21 @@ import os sys.path.append(os.path.abspath("../..")) +sys.path.append(os.path.abspath("../../..")) +sys.path.append(os.path.abspath("....")) +sys.path.append(os.path.abspath("...")) sys.path.append(os.path.abspath("..")) sys.path.append(os.path.abspath(".")) - # %% import torch -import torch.nn as nn import pandas as pd -from tqdm import tqdm from pathlib import Path -from prosper_nn.models.ecnn import ECNN -from prosper_nn.models.ensemble import Ensemble -from models import RNN_direct, RNN_recursive, RNN_S2S, Naive -from fredmd import Dataset_Fredmd +from training import EarlyStopping, Trainer +from evaluation import Evaluator +from models import init_models, Naive +from fredmd import init_datasets from config import ( past_horizon, @@ -29,179 +29,60 @@ n_evaluate_targets, n_features_Y, n_models, + area, ) # %% torch.manual_seed(0) -# %% Training -def train_model( - model: nn.Module, - dataloader: torch.utils.data.DataLoader, - dataset_val: torch.utils.data.Dataset, - n_epochs: int, - patience: int, -): - optimizer = torch.optim.Adam(model.parameters()) - smallest_val_loss = torch.inf - epoch_smallest_val = 0 - val_features_past, val_target_past, val_target_future = ( - dataset_val.get_all_rolling_origins() - ) - epochs = tqdm(range(n_epochs)) - - for epoch in epochs: - train_loss = 0 - for features_past, target_past, target_future in dataloader: - target_past = target_past.transpose(1, 0) - target_future = target_future.transpose(1, 0) - features_past = features_past.transpose(1, 0) - - model.zero_grad() - - forecasts = get_forecast(model, features_past, target_past) - - assert forecasts.shape == target_future.shape - loss = nn.functional.mse_loss(forecasts, target_future) - loss.backward() - train_loss += loss.detach() - optimizer.step() - - # Validation loss - forecasts_val = get_forecast(model, val_features_past, val_target_past) - val_loss = nn.functional.mse_loss(forecasts_val[0], val_target_future[0]).item() - epochs.set_postfix( - {"val_loss": round(val_loss, 3), "train_loss": round(train_loss.item(), 3)} - ) - - # Save and later use model with best validation loss - if val_loss < smallest_val_loss: - print(f"Save model_state at epoch {epoch}") - best_model_state = model.state_dict() - smallest_val_loss = val_loss - epoch_smallest_val = epoch - - # Early Stopping - if epoch >= epoch_smallest_val + patience: - print(f"No validation improvement since {patience} epochs -> Stop Training") - model.load_state_dict(best_model_state) - return - - model.load_state_dict(best_model_state) - - -def get_forecast( - model: nn.Module, features_past: torch.Tensor, target_past: torch.Tensor -) -> torch.Tensor: - model_type = model.models[0] - - # Select input - if isinstance(model_type, ECNN): - input = (features_past, target_past) - else: - input = (features_past,) - - ensemble_output = model(*input) - mean = ensemble_output[-1] - - # Extract forecasts - if isinstance(model_type, ECNN): - _, forecasts = torch.split(mean, past_horizon) - else: - forecasts = mean - return forecasts - - -def evaluate_model(model: nn.Module, dataset: torch.utils.data.Dataset) -> pd.DataFrame: - model.eval() - losses = [] - - for features_past, target_past, target_future in dataset: - features_past = features_past.unsqueeze(1) - target_past = target_past.unsqueeze(1) - - with torch.no_grad(): - forecasts = get_forecast(model, features_past, target_past) - forecasts = forecasts.squeeze(1) - assert forecasts.shape == target_future.shape - losses.append( - [ - nn.functional.mse_loss(forecasts[i], target_future[i]).item() - for i in range(forecast_horizon) - ] - ) - return pd.DataFrame(losses) - - # %% Get Data - -fredmd = Dataset_Fredmd( - past_horizon, - forecast_horizon, - split_date=train_test_split_period, - data_type="train", -) -fredmd_val = Dataset_Fredmd( - past_horizon, - forecast_horizon, - split_date=train_test_split_period, - data_type="val", -) -fredmd_test = Dataset_Fredmd( - past_horizon, - forecast_horizon, - split_date=train_test_split_period, - data_type="test", +fredmd_train, fredmd_val, fredmd_test = init_datasets( + past_horizon, forecast_horizon, train_test_split_period, area ) # %% Run benchmark -n_features_U = len(fredmd.features) -n_state_neurons = n_features_U + n_features_Y +n_features_U = len(fredmd_train.features) +n_state_neurons = 2 * (n_features_U + n_features_Y) overall_losses = {} -for target in fredmd.features[:n_evaluate_targets]: - fredmd.target = target +if n_evaluate_targets == None: + n_evaluate_targets = len(fredmd_train.features) + +benchmark_models = {} +for index_target, target in enumerate(fredmd_train.features[:n_evaluate_targets]): + fredmd_train.target = target fredmd_val.target = target fredmd_test.target = target - # Error Correction Neural Network (ECNN) - ecnn = ECNN( - n_state_neurons=n_state_neurons, - n_features_U=n_features_U, - n_features_Y=n_features_Y, - past_horizon=past_horizon, - forecast_horizon=forecast_horizon, - ) - - # Define an Ensemble for better forecasts, heatmap visualization and sensitivity analysis - ecnn_ensemble = Ensemble(model=ecnn, n_models=n_models).double() - benchmark_models = {"ECNN": ecnn_ensemble} - - # Compare to further Recurrent Neural Networks - for forecast_module in [RNN_direct, RNN_recursive, RNN_S2S]: - for recurrent_cell_type in ["elman", "gru", "lstm"]: - model = forecast_module( - n_features_U, - n_state_neurons, - n_features_Y, - forecast_horizon, - recurrent_cell_type, - ) - ensemble = Ensemble(model=model, n_models=n_models).double() - benchmark_models[f"{recurrent_cell_type}_{model.forecast_method}"] = ( - ensemble - ) - - # Train models - dataloader = torch.utils.data.DataLoader( - fredmd, batch_size=batch_size, shuffle=True + benchmark_models = init_models( + benchmark_models, + n_features_U, + n_state_neurons, + n_features_Y, + past_horizon, + forecast_horizon, + n_models, ) for name, model in benchmark_models.items(): - print(f"### Train {name} ###") - train_model(model, dataloader, fredmd_val, n_epochs, patience) + is_untrained_multivariate_model = model.multivariate and (index_target == 0) + not_naive_model = not isinstance(model, Naive) + + needs_training = ( + not model.multivariate or is_untrained_multivariate_model + ) and not_naive_model + if needs_training: + fredmd_train.set_target_future_format(multivariate=model.multivariate) + fredmd_val.set_target_future_format(multivariate=model.multivariate) + + dataloader = torch.utils.data.DataLoader( + fredmd_train, batch_size=batch_size, shuffle=True, drop_last=True + ) + trainer = Trainer(model, EarlyStopping(patience), n_epochs) + print(f"### Train {name} ###") + trainer.train(dataloader, fredmd_val) if target == "DNDGRG3M086SBEA": torch.save( @@ -209,19 +90,17 @@ def evaluate_model(model: nn.Module, dataset: torch.utils.data.Dataset) -> pd.Da ) # Test - # Additionally, compare with the naive no-change forecast - benchmark_models["Naive"] = Ensemble( - Naive(past_horizon, forecast_horizon, n_features_Y), n_models - ) + losses_one_target = {} + for name, model in benchmark_models.items(): + fredmd_test.set_target_future_format(multivariate=False) + evaluator = Evaluator(model, forecast_horizon) + loss_one_target_one_model = evaluator.evaluate(fredmd_test, index_target) + losses_one_target[name] = loss_one_target_one_model - all_losses = { - name: evaluate_model(model, fredmd_test) - for name, model in benchmark_models.items() - } - overall_losses[target] = pd.concat(all_losses) + overall_losses[target] = pd.concat(losses_one_target) overall_losses = pd.concat(overall_losses) -overall_losses.to_csv(Path(__file__).parent / f"overall_losses.csv") +overall_losses.to_csv(Path(__file__).parent / f"overall_losses_{area}.csv") mean_overall_losses = overall_losses.groupby(level=1).mean() -mean_overall_losses.to_csv(Path(__file__).parent / f"mean_overall_losses.csv") +mean_overall_losses.to_csv(Path(__file__).parent / f"mean_overall_losses_{area}.csv") print(mean_overall_losses) diff --git a/examples/case_study/config.py b/examples/case_study/config.py index a00b7a5..bbc5d2f 100644 --- a/examples/case_study/config.py +++ b/examples/case_study/config.py @@ -1,5 +1,6 @@ import pandas as pd +area = "output_and_income" # "consumption_and_orders", "prices", "output_and_income" n_evaluate_targets = 19 past_horizon = 24 diff --git a/examples/case_study/evaluation.py b/examples/case_study/evaluation.py new file mode 100644 index 0000000..5c12bcd --- /dev/null +++ b/examples/case_study/evaluation.py @@ -0,0 +1,37 @@ +from torch import nn +import torch +import pandas as pd + + +class Evaluator: + def __init__(self, model: nn.Module, forecast_horizon: int): + self.losses = [] + self.model = model + self.forecast_horizon = forecast_horizon + self.loss_metric = nn.functional.mse_loss + + def evaluate( + self, dataset: torch.utils.data.Dataset, index: int = None + ) -> pd.DataFrame: + self.model.eval() + + for features_past, target_past, target_future in dataset: + features_past = features_past.unsqueeze(1) + target_past = target_past.unsqueeze(1) + + with torch.no_grad(): + input = self.model.get_input(features_past, target_past) + output = self.model(*input) + forecasts = self.model.extract_forecasts(output) + forecasts = forecasts.squeeze(1) + if forecasts.size(-1) > 1: + forecasts = forecasts[..., [index]] + + assert forecasts.shape == target_future.shape + + self.losses.append( + self.loss_metric( + forecasts, target_future, reduction="none" + ).flatten().tolist() + ) + return pd.DataFrame(self.losses) diff --git a/examples/case_study/fredmd.py b/examples/case_study/fredmd.py index 582408c..cfa99b0 100644 --- a/examples/case_study/fredmd.py +++ b/examples/case_study/fredmd.py @@ -18,38 +18,77 @@ def __init__( forecast_horizon: int, split_date: pd.Period, data_type: str = "test", - target: str = "CPIAUCSL", + area: str = "prices", ): assert data_type in ["train", "val", "test"] self.past_horizon = past_horizon self.forecast_horizon = forecast_horizon self.window_size = past_horizon + forecast_horizon self.split_date = split_date + self.area = area - # Select variables from "prices" group without 'OILPRICEx' - self.features = [ - "WPSFD49207", - "WPSFD49502", - "WPSID61", - "WPSID62", - # "OILPRICEx", - "PPICMM", - "CPIAUCSL", - "CPIAPPSL", - "CPITRNSL", - "CPIMEDSL", - "CUSR0000SAC", - "CUSR0000SAD", - "CUSR0000SAS", - "CPIULFSL", - "CUSR0000SA0L2", - "CUSR0000SA0L5", - "PCEPI", - "DDURRG3M086SBEA", - "DNDGRG3M086SBEA", - "DSERRG3M086SBEA", - ] - self.target = target + if area == "prices": + # Select variables from "prices" group without 'OILPRICEx' + self.features = [ + "WPSFD49207", + "WPSFD49502", + "WPSID61", + "WPSID62", + # "OILPRICEx", + "PPICMM", + "CPIAUCSL", + "CPIAPPSL", + "CPITRNSL", + "CPIMEDSL", + "CUSR0000SAC", + "CUSR0000SAD", + "CUSR0000SAS", + "CPIULFSL", + "CUSR0000SA0L2", + "CUSR0000SA0L5", + "PCEPI", + "DDURRG3M086SBEA", + "DNDGRG3M086SBEA", + "DSERRG3M086SBEA", + ] + self.target = "CPIAUCSL" + elif area == "output_and_income": + self.features = [ + "RPI", + "W875RX1", + "INDPRO", + "IPFPNSS", + "IPFINAL", + "IPCONGD", + "IPDCONGD", + "IPNCONGD", + "IPBUSEQ", + "IPMAT", + "IPDMAT", + "IPNMAT", + "IPMANSICS", + "IPB51222S", + "IPFUELS", + "CUMFNS", + ] + self.target = "RPI" + elif area == "consumption_and_orders": + self.features = [ + "HOUST", + "HOUSTNE", + "HOUSTMW", + "HOUSTS", + "HOUSTW", + "PERMIT", + "PERMITNE", + "PERMITMW", + "PERMITS", + "PERMITW", + ] + self.target = "HOUST" + else: + raise ValueError(f"area {area} unknown") + self.target_future_format = [self.target] self.original_data = self.get_data() self.n_rolling_origins = len(self.original_data) - self.window_size @@ -123,10 +162,16 @@ def get_one_rolling_origin( assert future.notnull().all().all() features_past = torch.tensor(past[self.features].values) - target_past = torch.tensor(past[self.target].values).unsqueeze(1) - target_future = torch.tensor(future[self.target].values).unsqueeze(1) + target_past = torch.tensor(past[[self.target]].values) + target_future = torch.tensor(future[self.target_future_format].values) return features_past, target_past, target_future + def set_target_future_format(self, multivariate=False): + if multivariate: + self.target_future_format = self.features + else: + self.target_future_format = [self.target] + def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: rolling_origin_start_date = self.df.index.get_level_values(0).unique()[idx] features_past, target_past, target_future = self.get_one_rolling_origin( @@ -137,13 +182,17 @@ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tenso def get_data(self) -> pd.DataFrame: path = Path(__file__).parent df = pd.read_csv( - path / "2024-01.csv", + path / "2024-07.csv", parse_dates=["sasdate"], index_col="sasdate", usecols=["sasdate", self.target] + self.features, ) df = df.drop("Transform:") df.index = pd.PeriodIndex(df.index, freq="M") + + # Some variables are only available after 1959 + if self.area == "consumption_and_orders": + df = df.iloc[12:] return df def get_rolling_origins(self, df: pd.DataFrame) -> pd.DataFrame: @@ -200,3 +249,34 @@ def get_all_rolling_origins( all_targets_future = torch.stack(all_targets_future, dim=1) all_features_past = torch.stack(all_features_past, dim=1) return all_features_past, all_targets_past, all_targets_future + + +def init_datasets( + past_horizon: int, + forecast_horizon: int, + train_test_split_period: pd.Period, + area: str, +) -> Tuple[torch.utils.data.Dataset]: + fredmd_train = Dataset_Fredmd( + past_horizon, + forecast_horizon, + split_date=train_test_split_period, + data_type="train", + area=area, + ) + fredmd_val = Dataset_Fredmd( + past_horizon, + forecast_horizon, + split_date=train_test_split_period, + data_type="val", + area=area, + ) + fredmd_test = Dataset_Fredmd( + past_horizon, + forecast_horizon, + split_date=train_test_split_period, + data_type="test", + area=area, + ) + + return fredmd_train, fredmd_val, fredmd_test diff --git a/examples/case_study/fuzzy_nn.py b/examples/case_study/fuzzy_nn.py new file mode 100644 index 0000000..0cb4b55 --- /dev/null +++ b/examples/case_study/fuzzy_nn.py @@ -0,0 +1,67 @@ +from pathlib import Path +import torch + +from prosper_nn.models.fuzzy.rule_manager import RuleManager + +from prosper_nn.models.fuzzy.membership_functions import ( + NormlogMembership, + GaussianMembership, +) +from prosper_nn.models.fuzzy.fuzzification import Fuzzification +from prosper_nn.models.fuzzy.fuzzy_inference import FuzzyInference +from prosper_nn.models.fuzzy.defuzzification import Defuzzification + + +class Benchmark_Fuzzy_NN(torch.nn.Module): + """ + Construct Fuzzy Neural Network and add methods to run it in unified benchmark pipeline. + """ + + multivariate = False + + def __init__(self, n_features_input, past_horizon, n_rules=3, n_output_classes=9): + super(Benchmark_Fuzzy_NN, self).__init__() + self.past_horizon = past_horizon + membership_fcts = { + "decrease": NormlogMembership(negative=True), + "constant": GaussianMembership(sigma=1.0), + "increase": NormlogMembership(), + } + n_membership_fcts = len(membership_fcts) + + fuzzification = Fuzzification( + n_features_input=n_features_input, membership_fcts=membership_fcts + ) + + rule_manager = RuleManager( + path=Path(__file__).parent / "fuzzy_rules.json", + rule_matrix_shape=(n_rules, n_features_input, n_membership_fcts), + classification_matrix_shape=(n_rules, n_output_classes), + ) + fuzzy_inference = FuzzyInference( + n_features_input=n_features_input, + n_rules=n_rules, + n_output_classes=n_output_classes, + n_membership_fcts=n_membership_fcts, + rule_matrix=rule_manager.rule_matrix, + prune_weights=False, + learn_conditions=True, + classification_matrix=rule_manager.classification_matrix, + ) + + defuzzification = Defuzzification(n_output_classes, n_features_output=3) + + self.fuzzy = torch.nn.Sequential( + fuzzification, fuzzy_inference, defuzzification + ) + + def forward(self, X: torch.Tensor) -> torch.Tensor: + output = self.fuzzy(X) + return output + + def get_input(self, features_past, target_past): + return (features_past[-1],) + + def extract_forecasts(self, ensemble_output): + mean = ensemble_output[-1] + return mean.T.unsqueeze(2) diff --git a/examples/case_study/fuzzy_rules.json b/examples/case_study/fuzzy_rules.json new file mode 100644 index 0000000..8f23a10 --- /dev/null +++ b/examples/case_study/fuzzy_rules.json @@ -0,0 +1,39 @@ +{ + "rules": { + "continue_trend_DNDGRG3M086SBEA": {"DNDGRG3M086SBEA": "increase"}, + "relation_of_durable_nondurable_goods": {"DDURRG3M086SBEA": "decrease"}, + "relation_of_durable_nondurable_goods:2": {"DDURRG3M086SBEA": "constant"}, + }, + "classification_rules": { + "continue_trend_DNDGRG3M086SBEA_forecast_step_1_2_3": [0, 3, 6], + "relation_of_durable_nondurable_goods": [2, 5, 7], + "relation_of_durable_nondurable_goods_2": [1, 4, 6], + }, + "input_names": { + "WPSFD49207": 0, + "WPSFD49502": 1, + "WPSID61": 2, + "WPSID62": 3, + "PPICMM": 4, + "CPIAUCSL": 5, + "CPIAPPSL": 6, + "CPITRNSL": 7, + "CPIMEDSL": 8, + "CUSR0000SAC": 9, + "CUSR0000SAD": 10, + "CUSR0000SAS": 11, + "CPIULFSL": 12, + "CUSR0000SA0L2": 13, + "CUSR0000SA0L5": 14, + "PCEPI": 15, + "DDURRG3M086SBEA": 16, + "DNDGRG3M086SBEA": 17, + "DSERRG3M086SBEA": 18, + }, + "member_activations": { + "decrease": [1, 0, 0], + "constant": [0, 1, 0], + "increase": [0, 0, 1], + "none": [0, 0, 0], + }, +} diff --git a/examples/case_study/models.py b/examples/case_study/models.py index 84f10a5..421d116 100644 --- a/examples/case_study/models.py +++ b/examples/case_study/models.py @@ -2,6 +2,11 @@ import torch import torch.nn as nn +from prosper_nn.models.ecnn import ECNN +from prosper_nn.models.hcnn import HCNN +from prosper_nn.models.crcnn import CRCNN +from prosper_nn.models.ensemble import Ensemble +from fuzzy_nn import Benchmark_Fuzzy_NN class Benchmark_RNN(nn.Module): @@ -9,7 +14,7 @@ class Benchmark_RNN(nn.Module): Parent class to create various RNNs based on Elman, GRU and LSTM cells. Additionally, the forecast methods direct, recursive and sequence to sequence (S2S) are possible. - For all approaches the past_target is merged to the past_features to enable + For all approaches the past_target is merged to the features_past to enable an autoregressive part in the models. """ @@ -22,6 +27,8 @@ def __init__( recurrent_cell_type: str, ): super(Benchmark_RNN, self).__init__() + self.multivariate = False + self.n_features_Y = n_features_Y self.forecast_horizon = forecast_horizon self.n_state_neurons = n_state_neurons @@ -34,11 +41,11 @@ def __init__( ) self.init_state = self.set_init_state() - def forward(self, past_features: torch.Tensor) -> torch.Tensor: - batchsize = past_features.size(1) + def forward(self, features_past: torch.Tensor) -> torch.Tensor: + batchsize = features_past.size(1) init_state = self.repeat_init_state(batchsize) - output_rnn = self.rnn(past_features, init_state) + output_rnn = self.rnn(features_past, init_state) return output_rnn def set_init_state(self) -> Union[nn.Parameter, Tuple[nn.Parameter, nn.Parameter]]: @@ -81,6 +88,14 @@ def repeat_init_state( else: return self.init_state.repeat(batchsize, 1).unsqueeze(0) + def get_input(self, features_past, target_past): + return (features_past,) + + def extract_forecasts(self, ensemble_output): + mean = ensemble_output[-1] + + return mean + class RNN_direct(Benchmark_RNN): """ @@ -114,8 +129,8 @@ def __init__( recurrent_cell_type, ) - def forward(self, past_features: torch.Tensor) -> torch.Tensor: - output_rnn = super(RNN_direct, self).forward(past_features) + def forward(self, features_past: torch.Tensor) -> torch.Tensor: + output_rnn = super(RNN_direct, self).forward(features_past) context_vector = output_rnn[0][-1] forecast = self.state_output(context_vector) @@ -158,10 +173,10 @@ def __init__( recurrent_cell_type, ) - def forward(self, past_features: torch.Tensor) -> torch.Tensor: + def forward(self, features_past: torch.Tensor) -> torch.Tensor: # add zeros as RNN input for forecast horizon - future_zeros_features = torch.zeros_like(past_features)[: self.forecast_horizon] - features = torch.cat([past_features, future_zeros_features], dim=0) + future_zeros_features = torch.zeros_like(features_past)[: self.forecast_horizon] + features = torch.cat([features_past, future_zeros_features], dim=0) output_rnn = super(RNN_recursive, self).forward(features) future_states = output_rnn[0][-self.forecast_horizon :] @@ -205,11 +220,11 @@ def __init__( ) self.decoder = self.cell(input_size=n_features_U, hidden_size=n_state_neurons) - def forward(self, past_features: torch.Tensor) -> torch.Tensor: - output_rnn = super(RNN_S2S, self).forward(past_features) + def forward(self, features_past: torch.Tensor) -> torch.Tensor: + output_rnn = super(RNN_S2S, self).forward(features_past) # add dummy zeros as RNN input for forecast horizon - future_zeros_features = torch.zeros_like(past_features)[: self.forecast_horizon] + future_zeros_features = torch.zeros_like(features_past)[: self.forecast_horizon] context_vector = output_rnn[1] states_decoder = self.decoder(future_zeros_features, context_vector)[0] @@ -227,6 +242,8 @@ class Naive(nn.Module): \hat{y}_{T+i} = 0 for i=1,...,\tau """ + multivariate = False + def __init__( self, past_horizon: int, forecast_horizon: int, n_features_Y: int ) -> None: @@ -235,9 +252,144 @@ def __init__( self.forecast_horizon = forecast_horizon self.n_features_Y = n_features_Y - def forward(self, past_features: torch.Tensor) -> torch.Tensor: + def forward(self, features_past: torch.Tensor) -> torch.Tensor: return torch.zeros( self.forecast_horizon, - past_features.size(1), + features_past.size(1), self.n_features_Y, ) + + def get_input(self, features_past, target_past): + return (features_past,) + + def extract_forecasts(self, output): + return output + + +class Benchmark_ECNN(ECNN): + """ + Adds methods to run ECNN in unified benchmark pipeline. + """ + + multivariate = False + + def get_input(self, features_past, target_past): + return features_past, target_past + + def extract_forecasts(self, ensemble_output): + mean = ensemble_output[-1] + _, forecasts = torch.split(mean, self.past_horizon) + return forecasts + + +class Benchmark_HCNN(HCNN): + """ + Adds methods to run HCNN in unified benchmark pipeline. + """ + + multivariate = True + + def get_input(self, features_past, target_past): + return (features_past,) + + def extract_forecasts(self, ensemble_output): + mean = ensemble_output[-1] + _, forecasts = torch.split(mean, self.past_horizon) + return forecasts + + +class Benchmark_CRCNN(CRCNN): + """ + Adds methods to run CRCNN in unified benchmark pipeline. + """ + + multivariate = True + + def get_input(self, features_past, target_past): + return (features_past,) + + def extract_forecasts(self, ensemble_output): + mean = ensemble_output[-1, -1] + _, forecasts = torch.split(mean, self.past_horizon) + return forecasts + + +def init_models( + benchmark_models, + n_features_U, + n_state_neurons, + n_features_Y, + past_horizon, + forecast_horizon, + n_models, +): + # Error Correction Neural Network (ECNN) + ecnn = Benchmark_ECNN( + n_state_neurons=n_state_neurons, + n_features_U=n_features_U, + n_features_Y=n_features_Y, + past_horizon=past_horizon, + forecast_horizon=forecast_horizon, + ).double() + + # Define an Ensemble for better forecasts, heatmap visualization and sensitivity analysis + ecnn_ensemble = init_ensemble(ecnn, n_models) + + benchmark_models["ECNN"] = ecnn_ensemble + + # # Fuzzy Neural Network + # fuzzy_nn = Benchmark_Fuzzy_NN(n_features_input=n_features_U, past_horizon=past_horizon,) + # fuzzy_nn_ensemble = init_ensemble(fuzzy_nn, n_models, keep_pruning_mask=True) + # benchmark_models["Fuzzy_NN"] = fuzzy_nn_ensemble + + if not 'HCNN' in benchmark_models: # Reuse trained multivariate model + # Historical Consistent Neural Network (HCNN) + hcnn = Benchmark_HCNN( + n_state_neurons=n_state_neurons, + n_features_Y=n_features_U, + past_horizon=past_horizon, + forecast_horizon=forecast_horizon, + ) + hcnn_ensemble = init_ensemble(hcnn, n_models) + benchmark_models["HCNN"] = hcnn_ensemble + + if not 'CRCNN' in benchmark_models: # Reuse trained multivariate model + # Causal Retro Causal Neural Network (CRCNN) + crcnn = Benchmark_CRCNN( + n_state_neurons=n_state_neurons, + n_features_Y=n_features_U, + past_horizon=past_horizon, + forecast_horizon=forecast_horizon, + ) + crcnn_ensemble = init_ensemble(crcnn, n_models) + benchmark_models["CRCNN"] = crcnn_ensemble + + # Compare to further Recurrent Neural Networks + for forecast_module in [RNN_direct, RNN_recursive, RNN_S2S]: + for recurrent_cell_type in ["elman", "gru", "lstm"]: + model = forecast_module( + n_features_U, + n_state_neurons, + n_features_Y, + forecast_horizon, + recurrent_cell_type, + ) + ensemble = init_ensemble(model, n_models) + benchmark_models[f"{recurrent_cell_type}_{model.forecast_method}"] = ( + ensemble + ) + + # Additionally, compare with the naive no-change forecast + benchmark_models["Naive"] = Naive(past_horizon, forecast_horizon, n_features_Y) + + return benchmark_models + + +def init_ensemble(model, n_models, keep_pruning_mask=False): + ensemble = Ensemble( + model=model, n_models=n_models, keep_pruning_mask=keep_pruning_mask + ).double() + setattr(ensemble, "get_input", model.get_input) + setattr(ensemble, "extract_forecasts", model.extract_forecasts) + setattr(ensemble, "multivariate", model.multivariate) + return ensemble diff --git a/examples/case_study/training.py b/examples/case_study/training.py new file mode 100644 index 0000000..a6becbe --- /dev/null +++ b/examples/case_study/training.py @@ -0,0 +1,90 @@ +import torch +from torch import nn +from tqdm import tqdm +from copy import deepcopy + + +class EarlyStopping: + def __init__(self, patience): + self.patience = patience + self.smallest_val_loss = torch.inf + self.epoch_smallest_val = 0 + self.best_model_state = None + + def early_stop(self, epoch): + stop = epoch >= self.epoch_smallest_val + self.patience + if stop: + print( + f"No validation improvement since {self.patience} epochs -> Stop Training" + ) + return stop + + def set_best_model_state(self, model, epoch, val_loss): + if val_loss < self.smallest_val_loss: + print(f"Save model_state at epoch {epoch}") + self.best_model_state = deepcopy(model.state_dict()) + self.smallest_val_loss = val_loss + self.epoch_smallest_val = epoch + + +class Trainer: + def __init__(self, model: nn.Module, early_stopping: EarlyStopping, n_epochs: int): + self.model = model + self.early_stopping = early_stopping + self.train_loss = 0 + self.epochs = tqdm(range(n_epochs)) + self.optimizer = torch.optim.Adam(self.model.parameters()) + + def train( + self, + dataloader: torch.utils.data.DataLoader, + dataset_val: torch.utils.data.Dataset, + ): + val_features_past, val_target_past, val_target_future = ( + dataset_val.get_all_rolling_origins() + ) + + for epoch in self.epochs: + self.train_loss = 0 + for features_past, target_past, target_future in dataloader: + target_past = target_past.transpose(1, 0) + target_future = target_future.transpose(1, 0) + features_past = features_past.transpose(1, 0) + + self.model.zero_grad() + + input = self.model.get_input(features_past, target_past) + output = self.model(*input) + forecasts = self.model.extract_forecasts(output) + assert forecasts.shape == target_future.shape + loss = nn.functional.mse_loss(forecasts, target_future) + loss.backward() + self.train_loss += loss.detach() + self.optimizer.step() + + # Validation loss + val_loss = self.get_validation_loss( + val_features_past, val_target_past, val_target_future + ) + self.epochs.set_postfix( + { + "val_loss": round(val_loss, 3), + "train_loss": round(self.train_loss.item(), 3), + } + ) + + self.early_stopping.set_best_model_state(self.model, epoch, val_loss) + if self.early_stopping.early_stop(epoch): + self.model.load_state_dict(self.early_stopping.best_model_state) + return + + self.model.load_state_dict(self.early_stopping.best_model_state) + + def get_validation_loss( + self, val_features_past, val_target_past, val_target_future + ): + input = self.model.get_input(val_features_past, val_target_past) + output = self.model(*input) + forecasts_val = self.model.extract_forecasts(output) + val_loss = nn.functional.mse_loss(forecasts_val[0], val_target_future[0]).item() + return val_loss diff --git a/prosper_nn/models/crcnn/crcnn.py b/prosper_nn/models/crcnn/crcnn.py index 652c8b1..f2b26cf 100644 --- a/prosper_nn/models/crcnn/crcnn.py +++ b/prosper_nn/models/crcnn/crcnn.py @@ -1,4 +1,3 @@ -"""""" """ Prosper_nn provides implementations for specialized time series forecasting neural networks and related utility functions. @@ -24,7 +23,7 @@ import torch.nn as nn import torch -from typing import Optional, Type +from typing import Optional, Type, TypeVar from ..hcnn.hcnn_cell import HCNNCell @@ -43,8 +42,8 @@ def __init__( n_features_Y: int, past_horizon: int, forecast_horizon: int, - n_branches: int, - batchsize: int, + n_branches: int = 3, + batchsize: Optional[int] = None, sparsity: float = 0.0, activation: Type[torch.autograd.Function] = torch.tanh, init_state_causal: Optional[torch.Tensor] = None, @@ -152,10 +151,11 @@ def __init__( self.teacher_forcing, ) - self.future_bias = nn.Parameter( - torch.zeros((self.forecast_horizon, self.batchsize, self.n_features_Y)), - requires_grad=True, - ) + if self.mirroring: + self.future_bias = nn.Parameter( + torch.zeros((self.forecast_horizon, self.batchsize, self.n_features_Y)), + requires_grad=True, + ) def forward(self, Y: torch.Tensor) -> torch.Tensor: """ @@ -174,33 +174,41 @@ def forward(self, Y: torch.Tensor) -> torch.Tensor: self._check_sizes(Y) - device = self.CRCNNCell_causal.A.weight.device + device = self.init_state_causal.device + dtype = self.init_state_causal.dtype + batchsize = Y.size(1) if self.mirroring: future_bias = self.future_bias + assert batchsize == self.batchsize, f"batchsize {batchsize} does not match initialized batchsize {self.batchsize} for mirroring" else: future_bias = [None] * self.forecast_horizon + # reset saved cell outputs past_error = torch.zeros( - (self.n_branches - 1, self.past_horizon, self.batchsize, self.n_features_Y), device=device + (self.n_branches - 1, self.past_horizon, batchsize, self.n_features_Y), + device=device, dtype=dtype, ) forecast = torch.zeros( - (self.n_branches - 1, - self.forecast_horizon, - self.batchsize, - self.n_features_Y,), - device=device + ( + self.n_branches - 1, + self.forecast_horizon, + batchsize, + self.n_features_Y, + ), + device=device, + dtype=dtype ) # initialize causal and retro-causal branches for i in range(self.n_causal_branches): self.state_causal[i][0], _ = self.CRCNNCell_causal( - self.init_state_causal.repeat(self.batchsize, 1) + self.init_state_causal.repeat(batchsize, 1) ) for i in range(self.n_causal_branches - 1): self.state_retro_causal[i][-1], _ = self.CRCNNCell_retro_causal( - self.init_state_retro_causal.repeat(self.batchsize, 1) + self.init_state_retro_causal.repeat(batchsize, 1) ) # First (causal) branch (no teacher-forcing) @@ -285,15 +293,13 @@ def _check_sizes(self, Y: torch.Tensor) -> None: None """ - if len(Y.shape) != 3: + if Y.dim() != 3: raise ValueError( "The shape for a batch of observations should be " "shape = (past_horizon, batchsize, n_features_Y)" ) - if Y.size() != torch.Size( - (self.past_horizon, self.batchsize, self.n_features_Y) - ): + if (Y.size(0) != self.past_horizon) | (Y.size(2) != self.n_features_Y): raise ValueError( "Y must be of the dimensions" " shape = (past_horizon, batchsize, n_features_Y)." @@ -304,7 +310,7 @@ def _check_sizes(self, Y: torch.Tensor) -> None: def _check_variables(self) -> None: """ Checks if self.n_state_neurons, self.n_features_Y, self.past_horizon, - self.forecast_horizon, self.batchsize, self.sparsity have valid inputs. + self.forecast_horizon, self.sparsity have valid inputs. Parameters ---------- None @@ -346,11 +352,14 @@ def _check_variables(self) -> None: "{} is not a valid number for n_branches. " "It must be an integer equal or greater than 3.".format(self.n_branches) ) - if (self.batchsize < 1) or (type(self.batchsize) != int): - raise ValueError( - "{} is not a valid number for batchsize. " - "It must be an integer greater than 0.".format(self.batchsize) - ) + if self.batchsize != None: + if (self.batchsize < 1) or (type(self.batchsize) != int): + raise ValueError( + "{} is not a valid number for batchsize. " + "It must be an integer greater than 0.".format(self.batchsize) + ) + if (self.batchsize == None) and self.mirroring: + raise ValueError('Parameter batchsize has to be set if mirroring is True.') if (self.sparsity < 0) or (self.sparsity > 1): raise ValueError( "{} is not a valid number for sparsity. " diff --git a/prosper_nn/models/ensemble/ensemble.py b/prosper_nn/models/ensemble/ensemble.py index b319541..eef4155 100644 --- a/prosper_nn/models/ensemble/ensemble.py +++ b/prosper_nn/models/ensemble/ensemble.py @@ -1,4 +1,3 @@ -"""""" """ Prosper_nn provides implementations for specialized time series forecasting neural networks and related utility functions. @@ -23,10 +22,10 @@ """ import warnings -import torch from copy import deepcopy from typing import Callable, Tuple, Union import operator +import torch import torch.nn.utils.prune as prune @@ -55,11 +54,18 @@ def init_model(model: torch.nn.Module, init_func: Callable, *params, **kwargs) - try: init_func(p, *params, **kwargs) except ValueError: - warnings.warn( - "Bias could not be initialized with wished init function." - "Instead torch.nn.init.normal_ is chosen." - ) - torch.nn.init.zeros_(p, *params, **kwargs) + if p.size(0) > 1: + torch.nn.init.zeros_(p, *params, **kwargs) + warnings.warn( + "Bias could not be initialized with wished init function." + "Instead torch.nn.init.zeros_ is chosen." + ) + else: + warnings.warn( + "Parameter could not be initialized with wished init function." + "Instead keep parameter of original model." + ) + else: init_func(p, *params, **kwargs) @@ -132,9 +138,7 @@ def __init__( self.models = torch.nn.ModuleList() if list(orig_model.named_buffers()): pruning = True - assert ( - self.sparsity > 0.0 - ) != self.keep_pruning_mask, "It is either possible that sparsity > 0 or keep_pruning = True, but not both." + assert not (self.sparsity > 0.0 and self.keep_pruning_mask), "It is either possible that sparsity > 0 or keep_pruning = True, but not both." else: pruning = False @@ -158,7 +162,9 @@ def __init__( # Apply pruning to the models if the original model had pruned weights if pruning: for name, mask in pruned_weights: - object_to_prune = operator.attrgetter(name[:10])(self.models[i]) + object_to_prune = operator.attrgetter( + name[: name.index("weight") - 1] + )(self.models[i]) if self.keep_pruning_mask: prune.custom_from_mask(object_to_prune, "weight", mask) elif self.sparsity > 0: @@ -166,13 +172,12 @@ def __init__( object_to_prune, "weight", self.sparsity ) else: - ValueError( + raise ValueError( "If the orig_model has pruned weights, sparsity should be greater than 0 " "or keep_pruning_mask should be True." ) def forward(self, *input: Union[torch.Tensor, Tuple[torch.Tensor]]) -> torch.Tensor: - """ Forward passing function of the module. Passes input through all instances of the given model and returns a torch.Tensor with an additional dimension. @@ -196,6 +201,15 @@ def forward(self, *input: Union[torch.Tensor, Tuple[torch.Tensor]]) -> torch.Ten else: outs = [model(*input) for model in self.models] + if isinstance(outs[0], tuple): + return tuple( + self.get_final_output([out[j] for out in outs]) + for j in range(len(outs[0])) + ) + else: + return self.get_final_output(outs) + + def get_final_output(self, outs): outs = torch.stack(outs) if self.combination_type == "mean": combined_output = torch.mean(outs, dim=0, keepdim=True) @@ -216,8 +230,8 @@ def _check_variables(self) -> None: """ if self.combination_type not in ["mean", "median"]: raise ValueError( - '"{}" is not a valid type for combination. ' - 'It must be either "mean" or "median".'.format(self.combination_type) + f"{self.combination_type} is not a valid type for combination. " + "It must be either 'mean' or 'median'." ) def set(self, variable: str, value) -> None: diff --git a/prosper_nn/models/fuzzy/fuzzy_inference.py b/prosper_nn/models/fuzzy/fuzzy_inference.py index c5cc59d..201cc69 100644 --- a/prosper_nn/models/fuzzy/fuzzy_inference.py +++ b/prosper_nn/models/fuzzy/fuzzy_inference.py @@ -1,4 +1,3 @@ -"""""" """ Prosper_nn provides implementations for specialized time series forecasting neural networks and related utility functions. @@ -130,7 +129,7 @@ def __init__( prune.custom_from_mask( self.consequences, "weight", self.classification_matrix ) - self.consequences.register_backward_hook( + self.consequences.register_full_backward_hook( self.clamp_and_normalize_consequences_backward ) self.consequences.register_forward_pre_hook( diff --git a/prosper_nn/models/fuzzy/membership_functions.py b/prosper_nn/models/fuzzy/membership_functions.py index 1eee000..a7240e0 100644 --- a/prosper_nn/models/fuzzy/membership_functions.py +++ b/prosper_nn/models/fuzzy/membership_functions.py @@ -1,4 +1,3 @@ -"""""" """ Prosper_nn provides implementations for specialized time series forecasting neural networks and related utility functions. @@ -98,7 +97,7 @@ class GaussianMembership(torch.nn.Module): * deviation of the curve as learnable parameter sigma """ - def __init__(self, sigma_initializer: Callable = torch.nn.init.constant_) -> None: + def __init__(self, sigma: float = 1.) -> None: """ Parameters @@ -106,8 +105,7 @@ def __init__(self, sigma_initializer: Callable = torch.nn.init.constant_) -> Non sigma_initializer : torch.nn.Initializer """ super(GaussianMembership, self).__init__() - self.sigma = torch.nn.Parameter(torch.Tensor(1)) - sigma_initializer(self.sigma, 1) + self.sigma = torch.nn.Parameter(torch.tensor([sigma])) def forward(self, inputs: torch.Tensor) -> torch.Tensor: """ diff --git a/prosper_nn/models/hcnn/hcnn.py b/prosper_nn/models/hcnn/hcnn.py index 5641066..718bddd 100644 --- a/prosper_nn/models/hcnn/hcnn.py +++ b/prosper_nn/models/hcnn/hcnn.py @@ -1,4 +1,3 @@ -"""""" """ Prosper_nn provides implementations for specialized time series forecasting neural networks and related utility functions. @@ -24,9 +23,11 @@ import torch.nn as nn import torch -from typing import Optional, Type +from typing import Optional, Type, Literal from . import hcnn_cell, hcnn_gru_cell +allowed_update_frequencies = Literal["forward", "time_step"] + class HCNN(nn.Module): """ @@ -92,8 +93,7 @@ def __init__( teacher_forcing: float The probability that teacher forcing is applied for a single state neuron. In each time step this is repeated and therefore enforces stochastic learning - if the value is smaller than 1. Since not all nodes are corrected then, it is - partial teacher forcing (ptf). + if the value is smaller than 1. decrease_teacher_forcing: float The amount by which teacher_forcing is decreased each epoch. @@ -131,13 +131,13 @@ def __init__( raise ValueError("Cell type is not found") self.HCNNCell = self.HCNNCell( - self.n_state_neurons, - self.n_features_Y, - self.sparsity, - self.activation, - self.teacher_forcing, - self.backward_full_Y, - self.ptf_in_backward, + n_state_neurons=self.n_state_neurons, + n_features_Y=self.n_features_Y, + sparsity=self.sparsity, + activation=self.activation, + teacher_forcing=self.teacher_forcing, + backward_full_Y=self.backward_full_Y, + ptf_in_backward=self.ptf_in_backward, ) def forward(self, Y: torch.Tensor): @@ -158,30 +158,51 @@ def forward(self, Y: torch.Tensor): shape=(past_horizon+forecast_horizon, batchsize, n_features_Y) """ device = self.init_state.device - self.state[0] = self.init_state + dtype = self.init_state.dtype self._check_sizes(Y) + batchsize = Y.shape[1] - # reset saved cell outputs - past_error = torch.zeros((self.past_horizon, batchsize, self.n_features_Y), device=device) - forecast = torch.zeros((self.forecast_horizon, batchsize, self.n_features_Y), device=device) + # reset + self.state[0] = self.init_state + past_error, forecast = self.reset_cell_outputs(batchsize, device, dtype) # past for t in range(self.past_horizon): if t == 0: self.state[t + 1], past_error[t] = self.HCNNCell( - self.state[t].repeat(batchsize, 1), Y[t] + self.state[t].repeat(batchsize, 1), + Y[t], ) else: self.state[t + 1], past_error[t] = self.HCNNCell(self.state[t], Y[t]) + # future - for t in range(self.past_horizon, self.past_horizon + self.forecast_horizon): - self.state[t + 1], forecast[t - self.past_horizon] = self.HCNNCell( - self.state[t] - ) + if self.calculate_forecast: + for t in range( + self.past_horizon, self.past_horizon + self.forecast_horizon + ): + self.state[t + 1], forecast[t - self.past_horizon] = self.HCNNCell( + self.state[t] + ) return torch.cat([past_error, forecast], dim=0) + def enable_calculate_forecast(self): + self.calculate_forecast = True + + def disable_calculate_forecast(self): + self.calculate_forecast = False + + def reset_cell_outputs(self, batchsize, device, dtype): + past_error = torch.zeros( + (self.past_horizon, batchsize, self.n_features_Y), device=device, dtype=dtype + ) + forecast = torch.zeros( + (self.forecast_horizon, batchsize, self.n_features_Y), device=device, dtype=dtype + ) + return past_error, forecast + def adjust_teacher_forcing(self): """ Decrease teacher_forcing each epoch by decrease_teacher_forcing until it reaches zero. diff --git a/prosper_nn/utils/sensitivity_analysis.py b/prosper_nn/utils/sensitivity_analysis.py index b3e71b4..d9f0444 100644 --- a/prosper_nn/utils/sensitivity_analysis.py +++ b/prosper_nn/utils/sensitivity_analysis.py @@ -255,6 +255,111 @@ def analyse_temporal_sensitivity( return torch.stack(total_heat) +def plot_analyse_temporal_sensitivity( + sensis: torch.Tensor, + target_var: List[str], + features: List[str], + n_future_steps: int, + path: Optional[str] = None, + title: Optional[Union[dict, str]] = None, + xticks: Optional[Union[dict, str]] = None, + yticks: Optional[Union[dict, str]] = None, + xlabel: Optional[Union[dict, str]] = None, + ylabel: Optional[Union[dict, str]] = None, + figsize: List[float] = [12.4, 5.8], +) -> None: + """ + Plots a sensitivity analysis and creates a table with monotonie and total heat on the right side + for each task variable. + """ + # Calculate total heat and monotony + total_heat = torch.sum(torch.abs(sensis), dim=2) + total_heat = (total_heat * 100).round() / 100 + monotonie = torch.sum(sensis, dim=2) / total_heat + monotonie = (monotonie * 100).round() / 100 + + plt.rcParams["figure.figsize"] = figsize + ### Temporal Sensitivity Heatmap ### + # plot a sensitivity matrix for every feature/target variable to be investigated + for i, node in enumerate(target_var): + # Set description + if not title: + title = "Influence of auxiliary variables on {}" + if not xlabel: + xlabel = "Weeks into future" + if not ylabel: + ylabel = "Auxiliary variables" + if not xticks: + xticks = { + "ticks": range(1, n_future_steps + 1), + "labels": [ + str(i) if i % 2 == 1 else None for i in range(1, n_future_steps + 1) + ], + "horizontalalignment": "right", + } + if not yticks: + yticks = { + "ticks": range(len(features)), + "labels": [feature.replace("_", " ") for feature in features], + "rotation": 0, + "va": "top", + "size": "large", + } + + sns.heatmap(sensis[i], + center=0, + cmap='coolwarm', + robust=True, + cbar_kws={'location':'right', 'pad': 0.22}, + ) + plt.ylabel(ylabel) + plt.xlabel(xlabel) + plt.xticks(**xticks) + plt.yticks(**yticks), + plt.title(title.format(node.replace("_", " ")), pad=25) + + # Fade out row name if total heat is not that strong + for j, ticklabel in enumerate(plt.gca().get_yticklabels()): + if j >= len(target_var): + alpha = float(0.5 + (total_heat[i][j] / torch.max(total_heat)) / 2) + ticklabel.set_color(color=[0, 0, 0, alpha]) + else: + ticklabel.set_color(color="C0") + plt.tight_layout() + + ### Table with total heat and monotonie ### + table_values = torch.stack((total_heat[i], monotonie[i])).T + + # Colour of cells + cell_colours = [ + ["#E1E3E3" for _ in range(table_values.shape[1])] + for _ in range(table_values.shape[0]) + ] + cell_colours[torch.argmax(table_values, dim=0)[0]][0] = "#179C7D" + cell_colours[torch.argmax(torch.abs(table_values), dim=0)[1]][1] = "#179C7D" + + # Plot table + plt.table( + table_values.numpy(), + loc='right', + colLabels=['Absolute', 'Monotony'], + colWidths=[0.2,0.2], + bbox=[1, 0, 0.3, 1.042], #[1, 0, 0.4, 1.042], + cellColours=cell_colours, + edges='BRT', + ) + plt.subplots_adjust(left=0.05, right=1.0) # creates space for table + + # Save and close + if path: + plt.savefig( + path + "sensi_analysis_{}.png".format(node), bbox_inches="tight" + ) + else: + plt.show() + plt.close() + + # %% Sensitivity for feed-forward models and other not-recurrent models diff --git a/setup.py b/setup.py index 6342554..2d0e6f0 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setuptools.setup( name="prosper_nn", # Replace with your own username - version="0.3.0", + version="0.3.1", author="Nico Beck, Julia Schemm", author_email="nico.beck@iis.fraunhofer.de", description="Package contains, in PyTorch implemented, neural networks with problem specific pre-structuring architectures and utils that help building and understanding models.",