From 7d0826f5e88b1ac6b408a1e144c1e9520a2c07e6 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Mon, 23 Sep 2024 15:44:28 +0000 Subject: [PATCH 01/14] Report Handler --- config/audit.yaml | 11 +- config/dev_config/cifar10.yaml | 8 +- leakpro/attacks/mia_attacks/lira.py | 3 +- leakpro/metrics/attack_result.py | 54 ++++++++- leakpro/reporting/audit_report.py | 2 + leakpro/reporting/report_handler.py | 177 ++++++++++++++++++++++++++++ leakpro/reporting/utils.py | 2 +- leakpro_main.py | 22 +++- 8 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 leakpro/reporting/report_handler.py diff --git a/config/audit.yaml b/config/audit.yaml index f9ea4a4d..e64658a6 100644 --- a/config/audit.yaml +++ b/config/audit.yaml @@ -16,11 +16,10 @@ audit: # Configurations for auditing # population: # attack_data_fraction: 0.1 # Fraction of the auxilary dataset to use for this attack # lira: - # training_data_fraction: 0.1 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) - # num_shadow_models: 8 # Number of shadow models to train - # online: False # perform online or offline attack - # fixed_variance: True # Use a fixed variance for the whole audit - # boosting: True + # training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) + # num_shadow_models: 4 # Number of shadow models to train + # online: True # perform online or offline attack + # boosting: False # loss_traj: # training_distill_data_fraction : 0.2 # Fraction of the auxilary dataset to use for training the distillation models D_s = (1-D_KD)/2 # number_of_traj: 1 # Number of epochs (number of points in the loss trajectory) @@ -79,7 +78,7 @@ shadow_model: # Name of the class to instantiate from the specified file model_class: "ResNet18" #"ConvNet" batch_size: 256 - epochs: 1 + epochs: 3 optimizer: name: sgd #adam, sgd, rmsprop diff --git a/config/dev_config/cifar10.yaml b/config/dev_config/cifar10.yaml index efa586d2..d8c71e26 100644 --- a/config/dev_config/cifar10.yaml +++ b/config/dev_config/cifar10.yaml @@ -5,8 +5,8 @@ run: # Configurations for a specific run train: # Configuration for training type: pytorch # Training framework (we only support pytorch now). num_target_model: 1 #Integer number for indicating how many target models we want to audit for the privacy game - epochs: 10 # Integer number for indicating the epochs for training target model. For speedyresnet, it uses its own number of epochs. - batch_size: 128 # Integer number for indicating batch size for training the target model. For speedyresnet, it uses its own batch size. + epochs: 3 # Integer number for indicating the epochs for training target model. For speedyresnet, it uses its own number of epochs. + batch_size: 256 # Integer number for indicating batch size for training the target model. For speedyresnet, it uses its own batch size. optimizer: SGD # String which indicates the optimizer. We support Adam and SGD. For speedyresnet, it uses its own optimizer. learning_rate: 0.01 # Float number for indicating learning rate for training the target model. For speedyresnet, it uses its own learning_rate. momentum: 0.9 @@ -16,8 +16,8 @@ train: # Configuration for training data: # Configuration for data dataset: cifar10 # String indicates the name of the dataset - f_train: 0.1 # Float number from 0 to 1 indicating the fraction of the train dataset - f_test: 0.1 # Float number from 0 to 1 indicating the size of the test set + f_train: 0.4999 # Float number from 0 to 1 indicating the fraction of the train dataset + f_test: 0.4999 # Float number from 0 to 1 indicating the size of the test set data_dir: ./target/data # String about where to save the data. diff --git a/leakpro/attacks/mia_attacks/lira.py b/leakpro/attacks/mia_attacks/lira.py index 1204d3f1..0b61141d 100644 --- a/leakpro/attacks/mia_attacks/lira.py +++ b/leakpro/attacks/mia_attacks/lira.py @@ -290,7 +290,7 @@ def run_attack(self:Self) -> CombinedMetricResult: score[i] = (pr_in - pr_out) # Append the calculated probability density value to the score list # Generate thresholds based on the range of computed scores for decision boundaries - self.thresholds = np.linspace(np.min(score), np.max(score), 2000) + self.thresholds = np.linspace(np.min(score), np.max(score), 1000) # Split the score array into two parts based on membership: in (training) and out (non-training) self.in_member_signals = score[self.in_members].reshape(-1,1) # Scores for known training data members @@ -317,4 +317,5 @@ def run_attack(self:Self) -> CombinedMetricResult: true_labels=true_labels, predictions_proba=None, # Note: Direct probability predictions are not computed here signal_values=signal_values, + # masks = masks ) diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index c13f223a..d1d42088 100644 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -1,7 +1,7 @@ """Contains the AttackResult class, which stores the results of an attack.""" import os - +import json import numpy as np from sklearn.metrics import ( accuracy_score, @@ -100,11 +100,14 @@ def __init__( # noqa: PLR0913 threshold: Threshold computed by the metric. """ + # TODO REDIFINE THE CLASS SO IT DOSE NOT STORE MATRICIES BUT VECTORS + self.predicted_labels = predicted_labels self.true_labels = true_labels self.predictions_proba = predictions_proba self.signal_values = signal_values self.threshold = threshold + self.id = None self.accuracy = np.mean(predicted_labels == true_labels, axis=1) self.tn = np.sum(true_labels == 0) - np.sum( @@ -124,6 +127,39 @@ def __init__( # noqa: PLR0913 self.roc_auc = auc(self.fpr, self.tpr) + def _get_primitives(self:Self): + """Return the primitives of the CombinedMetricResult class.""" + return {"predicted_labels": self.predicted_labels.tolist(), + "true_labels": self.true_labels.tolist(), + "predictions_proba": self.predictions_proba.tolist() if isinstance(self.predictions_proba, np.ndarray) else None, + "signal_values": self.signal_values.tolist() if isinstance(self.signal_values, np.ndarray) else None, + "threshold": self.threshold.tolist() if isinstance(self.threshold, np.ndarray) else None, + } + + def save(self:Self, path: str, name: str, config:dict): + """Save the CombinedMetricResult class to disk.""" + + # Primitives are the set of variables to re-create the class from scratch + primitives = self._get_primitives() + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "primitives": primitives, + "config": config + } + + # Get the name for the attack configuration + config_name = get_config_name(config["attack_list"][name]) + + # Check if path exists, otherwise create it. + if not os.path.exists(f'{path}/{name}/{name}{config_name}'): + os.makedirs(f'{path}/{name}/{name}{config_name}') + + # Save the results to a file + with open(f'{path}/{name}/{name}{config_name}/data.json', 'w') as f: + json.dump(data, f) + def __str__(self:Self) -> str: """Return a string describing the metric result.""" txt_list = [] @@ -174,3 +210,19 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: save_image(gt_denormalized, os.path.join(save_path, "original_image.png")) return attack_name + +def get_config_name(config): + config = dict(sorted(config.items())) + + exclude = ["attack_data_dir"] + + config_name = "" + for key, value in zip(list(config.keys()), list(config.values())): + if key in exclude: + pass + else: + if type(value) is bool: + config_name += f"-{key}" + else: + config_name += f"-{key}={value}" + return config_name \ No newline at end of file diff --git a/leakpro/reporting/audit_report.py b/leakpro/reporting/audit_report.py index f9c7644c..09974877 100644 --- a/leakpro/reporting/audit_report.py +++ b/leakpro/reporting/audit_report.py @@ -224,6 +224,8 @@ def generate_report( verticalalignment="center", bbox={"facecolor": "white", "alpha": 0.5}, ) + plt.xlim(left=1e-5) + plt.ylim(bottom=1e-5) if save: plt.savefig(fname=filename, dpi=1000) if show: diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py new file mode 100644 index 00000000..1a0dfef0 --- /dev/null +++ b/leakpro/reporting/report_handler.py @@ -0,0 +1,177 @@ +import json +import logging +import numpy as np +import os +import subprocess + +# from leakpro.reporting.utils import get_config_name +from leakpro.metrics.attack_result import CombinedMetricResult + +import matplotlib.pyplot as plt + +def load_mia_results(path: str, name: str): + with open(f'{path}/{name}_data') as f: + result_data = json.load(f) + return result_data + +# Report Handler +class report_handler(): + """Implementation of the report handler.""" + + def __init__(self, report_dir: str, logger:logging.Logger) -> None: + self.logger = logger + self.report_dir = report_dir + self.image_paths = [] + + def save_results(self, attack_name: str, result_data: dict, config: dict) -> None: + """Save attack results. """ + + self.logger.info(f'Saving results for {attack_name}') + result_data.save(self.report_dir, attack_name, config) + + def load_results(self): + self.results = [] + for parentdir in os.scandir(f"{self.report_dir}"): + if parentdir.is_dir(): + for subdir in os.scandir(f"{self.report_dir}/{parentdir.name}"): + if subdir.is_dir(): + try: + with open(f"{self.report_dir}/{parentdir.name}/{subdir.name}/data.json") as f: + data = json.load(f) + + # Extract class name and data + resulttype = data["resulttype"] + primitives = data["primitives"] + config = data["config"] + + # Dynamically get the class from its name (resulttype) + # This assumes that the class is already defined in the current module or imported + if resulttype in globals() and callable(globals()[resulttype]): + cls = globals()[resulttype] + else: + raise ValueError(f"Class '{resulttype}' not found.") + + # Initialize the class using the saved primitives + instance = cls( + predicted_labels=np.array(primitives["predicted_labels"]), + true_labels=np.array(primitives["true_labels"]), + predictions_proba=np.array(primitives["predictions_proba"]) if primitives["predictions_proba"] is not None else None, + signal_values=np.array(primitives["signal_values"]) if primitives["signal_values"] is not None else None, + threshold=np.array(primitives["threshold"]) if primitives["threshold"] is not None else None, + ) + instance.config = config + instance.id = subdir.name + instance.resultname = parentdir.name + self.results.append(instance) + + except Exception as e: + self.logger.info(f"Not able to load data, Error: {e}") + + def _plot_merged_results( + self, + merged_results, + title = "ROC curve", + save_name = "", + ): + + filename = f"{self.report_dir}/{save_name}" + + for res in merged_results: + + fpr = res.fp / (res.fp + res.tn) + tpr = res.tp / (res.tp + res.fn) + + range01 = np.linspace(0, 1) + plt.fill_between(fpr, tpr, alpha=0.15) + plt.plot(fpr, tpr, label=res.id) + + plt.plot(range01, range01, "--", label="Random guess") + plt.yscale("log") + plt.xscale("log") + plt.tight_layout() + plt.grid() + plt.legend() + plt.xlabel("False positive rate (FPR)") + plt.ylabel("True positive rate (TPR)") + plt.title(title) + plt.xlim(left=1e-5) + plt.ylim(bottom=1e-5) + plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches='tight') + plt.clf() + return filename + + def _get_results_of_name(self, results, resultname_value) -> list: + indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] + return [results[idx] for idx in indices] + + def _get_all_attacknames(self): + attack_name_list = [] + for result in self.results: + if result.resultname not in attack_name_list: + attack_name_list.append(result.resultname) + return attack_name_list + + def create_results_all(self) -> None: + names = self._plot_merged_results(merged_results=self.results, save_name="all_results") + self.image_paths.append(names) + pass + + def get_strongest(self, results) -> list: + return max((res for res in results), key=lambda d: d.roc_auc) + + def create_results_strong(self): + attack_name_grouped_results = [self._get_results_of_name(self.results, name) for name in self._get_all_attacknames()] + strongest_results = [self.get_strongest(attack_name) for attack_name in attack_name_grouped_results] + names = self._plot_merged_results(merged_results=strongest_results, save_name="strongest_attacks") + self.image_paths.append(names) + pass + + def create_results_attackname_grouped(self): + all_attack_names = self._get_all_attacknames() + print(all_attack_names) + for name in all_attack_names: + attack_results = self._get_results_of_name(self.results, name) + names = self._plot_merged_results(merged_results=attack_results, save_name="all_"+name) + self.image_paths.append(names) + pass + + # TODO: Make other useful groupings of results + def create_results_numshadowmodels(self): + pass + + def create_report(self): + self._init_pdf() + + for image in self.image_paths: + self._append_to_pdf(image_path=image) + + self._compile_pdf() + pass + + def _init_pdf(self,): + self.latex_content = f""" + \\documentclass{{article}} + \\usepackage{{graphicx}} + + \\begin{{document}} + """ + pass + + def _append_to_pdf(self, image_path=None, table=None): + self.latex_content += f""" + \\begin{{figure}}[ht] + \\includegraphics[width=0.9\\textwidth]{{{image_path}.png}} + \\end{{figure}} + """ + pass + + def _compile_pdf(self): + self.latex_content += f""" + \\end{{document}} + """ + with open(f'{self.report_dir}/LeakPro_output.tex', 'w') as f: + f.write(self.latex_content) + + cmd = ['pdflatex', '-interaction', 'nonstopmode', f'{self.report_dir}/LeakPro_output.tex'] + proc = subprocess.Popen(cmd) + pass \ No newline at end of file diff --git a/leakpro/reporting/utils.py b/leakpro/reporting/utils.py index 9573d272..b078c000 100644 --- a/leakpro/reporting/utils.py +++ b/leakpro/reporting/utils.py @@ -42,4 +42,4 @@ def prepare_privacy_risk_report( metric_result=audit_results, save=True, filename=f"{save_path}/Histogram.png", - ) + ) \ No newline at end of file diff --git a/leakpro_main.py b/leakpro_main.py index 3023798a..753f6652 100644 --- a/leakpro_main.py +++ b/leakpro_main.py @@ -23,7 +23,7 @@ ) from leakpro.attacks.attack_scheduler import AttackScheduler from leakpro.dataset import get_dataloader -from leakpro.reporting.utils import prepare_privacy_risk_report +from leakpro.reporting.report_handler import report_handler from leakpro.utils.handler_logger import setup_log @@ -100,14 +100,26 @@ def generate_user_input(configs: dict, retrain: bool = False, logger: logging.Lo attack_scheduler = AttackScheduler(handler) audit_results = attack_scheduler.run_attacks() + # Initiate report handler + ReportHandler = report_handler(report_dir=report_dir, logger=logger) + for attack_name in audit_results: logger.info(f"Preparing results for attack: {attack_name}") - prepare_privacy_risk_report( - audit_results[attack_name]["result_object"], - configs["audit"], - save_path=f"{report_dir}/{attack_name}", + # Save results to be used later + ReportHandler.save_results( + attack_name=attack_name, + result_data=audit_results[attack_name]["result_object"], + config=configs["audit"] ) + + # Create report from the saved results + ReportHandler.load_results() + ReportHandler.create_results_all() + ReportHandler.create_results_strong() + ReportHandler.create_results_attackname_grouped() + ReportHandler.create_report() + # ------------------------------------------------ # Save the configs and user_configs config_log_path = configs["audit"]["config_log"] From 7e5dc2c67e2f833651158814c0be51b185736e83 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Tue, 22 Oct 2024 20:49:51 +0000 Subject: [PATCH 02/14] Adding report handler --- leakpro/attacks/mia_attacks/lira.py | 35 +- leakpro/metrics/attack_result.py | 506 +++++++++++++++++- leakpro/reporting/audit_report.py | 2 +- leakpro/reporting/report_handler.py | 206 +++---- tests/test_attack_result.py/__init__.py | 1 + .../test_attack_result.py | 149 ++++++ tests/test_report_handler/__init__.py | 1 + .../test_report_handler.py | 77 +++ 8 files changed, 862 insertions(+), 115 deletions(-) create mode 100644 tests/test_attack_result.py/__init__.py create mode 100644 tests/test_attack_result.py/test_attack_result.py create mode 100644 tests/test_report_handler/__init__.py create mode 100644 tests/test_report_handler/test_report_handler.py diff --git a/leakpro/attacks/mia_attacks/lira.py b/leakpro/attacks/mia_attacks/lira.py index 0b61141d..5a243863 100644 --- a/leakpro/attacks/mia_attacks/lira.py +++ b/leakpro/attacks/mia_attacks/lira.py @@ -7,8 +7,8 @@ from leakpro.attacks.mia_attacks.abstract_mia import AbstractMIA from leakpro.attacks.utils.boosting import Memorization from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler -from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.import_helper import Self +from leakpro.metrics.attack_result import CombinedMetricResult, MIAResult from leakpro.signals.signal import ModelRescaledLogits from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -132,7 +132,7 @@ def prepare_attack(self:Self)->None: mask = (num_shadow_models_seen_points > 0) & (num_shadow_models_seen_points < self.num_shadow_models) # Filter the audit data - audit_data_indices = self.audit_dataset["data"][mask] + self.audit_dataset["data"] = self.audit_dataset["data"][mask] self.in_indices_masks = self.in_indices_masks[mask, :] # Filter IN and OUT members @@ -140,15 +140,16 @@ def prepare_attack(self:Self)->None: num_out_members = np.sum(mask[self.audit_dataset["out_members"]]) self.out_members = np.arange(len(self.in_members), len(self.in_members) + num_out_members) - assert len(audit_data_indices) == len(self.in_members) + len(self.out_members) + assert len(self.audit_dataset["data"]) == len(self.in_members) + len(self.out_members) - if len(audit_data_indices) == 0: + if len(self.audit_dataset["data"]) == 0: raise ValueError("No points in the audit dataset are used for the shadow models") else: - audit_data_indices = self.audit_dataset["data"] + self.audit_dataset["data"] = self.audit_dataset["data"] self.in_members = self.audit_dataset["in_members"] self.out_members = self.audit_dataset["out_members"] + # mask = [True if indice in self.in_members else False for indice in self.audit_dataset["data"]] # Check offline attack for possible IN- sample(s) if not self.online: @@ -157,14 +158,14 @@ def prepare_attack(self:Self)->None: logger.info(f"Some shadow model(s) contains {count_in_samples} IN samples in total for the model(s)") logger.info("This is not an offline attack!") - self.batch_size = len(audit_data_indices) - logger.info(f"Calculating the logits for all {self.num_shadow_models} shadow models") - self.shadow_models_logits = np.swapaxes(self.signal(self.shadow_models, self.handler, audit_data_indices,\ + self.batch_size = 20000 #int(len(self.audit_dataset["data"])/2) + self.logger.info(f"Calculating the logits for all {self.num_shadow_models} shadow models") + self.shadow_models_logits = np.swapaxes(self.signal(self.shadow_models, self.handler, self.audit_dataset["data"],\ self.batch_size), 0, 1) # Calculate logits for the target model - logger.info("Calculating the logits for the target model") - self.target_logits = np.swapaxes(self.signal([self.target_model], self.handler, audit_data_indices, self.batch_size),\ + self.logger.info("Calculating the logits for the target model") + self.target_logits = np.swapaxes(self.signal([self.target_model], self.handler, self.audit_dataset["data"], self.batch_size),\ 0, 1).squeeze() # Using Memorizationg boosting @@ -172,8 +173,8 @@ def prepare_attack(self:Self)->None: # Prepare for memorization org_audit_data_length = self.audit_dataset["data"].size - audit_data_indices = self.audit_dataset["data"][mask] if self.online else self.audit_dataset["data"] - audit_data_labels = self.handler.get_labels(audit_data_indices) + self.audit_dataset["data"] = self.audit_dataset["data"][mask] if self.online else self.audit_dataset["data"] + audit_data_labels = self.handler.get_labels(self.audit_dataset["data"]) logger.info("Running memorization") memorization = Memorization( @@ -184,7 +185,7 @@ def prepare_attack(self:Self)->None: self.in_indices_masks, self.shadow_models, self.target_model, - audit_data_indices, + self.audit_dataset["data"], audit_data_labels, org_audit_data_length, self.handler, @@ -312,10 +313,10 @@ def run_attack(self:Self) -> CombinedMetricResult: signal_values = np.concatenate([self.in_member_signals, self.out_member_signals]) # Return a result object containing predictions, true labels, and the signal values for further evaluation - return CombinedMetricResult( + return MIAResult( predicted_labels=predictions, true_labels=true_labels, - predictions_proba=None, # Note: Direct probability predictions are not computed here + predictions_proba=None, signal_values=signal_values, - # masks = masks + audit_indices=self.audit_dataset["data"], ) diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index d1d42088..8d20da2e 100644 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -1,8 +1,12 @@ """Contains the AttackResult class, which stores the results of an attack.""" +from collections import defaultdict import os import json import numpy as np +import matplotlib.pyplot as plt +import pandas as pd +import seaborn as sn from sklearn.metrics import ( accuracy_score, auc, @@ -175,19 +179,281 @@ def __str__(self:Self) -> str: txt_list.append("\n".join(txt)) return "\n\n".join(txt_list) +class MIAResult: + """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" + + def __init__( # noqa: PLR0913 + self:Self, + predicted_labels: list=None, + true_labels: list=None, + predictions_proba:list=None, + signal_values:list=None, + threshold: list = None, + audit_indices: list = None, + metadata: dict = None, + resultname: str = None, + id: str = None, + load: bool = False, + )-> None: + """Compute and store the accuracy, ROC AUC score, and the confusion matrix for a metric. + + Args: + ---- + predicted_labels: Membership predictions of the metric. + true_labels: True membership labels used to evaluate the metric. + predictions_proba: Continuous version of the predicted_labels. + signal_values: Values of the signal used by the metric. + threshold: Threshold computed by the metric. + + """ + + self.predicted_labels = predicted_labels + self.true_labels = true_labels + self.predictions_proba = predictions_proba + self.signal_values = signal_values + self.threshold = threshold + self.audit_indices = audit_indices + self.metadata = metadata + self.resultname = resultname + self.id = id + + if load: + return + + self.tn = np.sum(true_labels == 0) - np.sum( + predicted_labels[:, true_labels == 0], axis=1 + ) + self.tp = np.sum(predicted_labels[:, true_labels == 1], axis=1) + self.fp = np.sum(predicted_labels[:, true_labels == 0], axis=1) + self.fn = np.sum(true_labels == 1) - np.sum( + predicted_labels[:, true_labels == 1], axis=1 + ) + + self.fpr = self.fp / (self.fp + self.tn) + self.tpr = self.tp / (self.tp + self.fn) + self.roc_auc = auc(self.fpr, self.tpr) + + def load(self, data): + self.resultname = data["resultname"] + self.resulttype = data["resulttype"] + self.tpr = data["tpr"] + self.fpr = data["fpr"] + self.roc_auc = data["roc_auc"] + self.config = data["config"] + self.fixed_fpr_table = data["fixed_fpr"] + self.audit_indices = data["audit_indices"] + self.signal_values = data["signal_values"] + self.true_labels = data["true_labels"] + self.threshold = data["threshold"] + + def save(self:Self, path: str, name: str, config:dict = None): + """Save the MIAResults to disk.""" + + print(config) + + result_config = config["attack_list"][name] + fixed_fpr_table = get_result_fixed_fpr(self.fpr, self.tpr) + + # Get the name for the attack configuration + config_name = get_config_name(result_config) + + self.id = f"{name}{config_name}" + save_path = f"{path}/{name}/{self.id}" + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "resultname": name, + "tpr": self.tpr.tolist(), + "fpr": self.fpr.tolist(), + "roc_auc": self.roc_auc, + "config": config, + "fixed_fpr": fixed_fpr_table, + "audit_indices": self.audit_indices.tolist(), + "signal_values": self.signal_values.tolist(), + "true_labels": self.true_labels.tolist(), + "threshold": self.threshold.tolist() if self.threshold is not None else None, + "id": name, + } + + # Check if path exists, otherwise create it. + if not os.path.exists(save_path): + os.makedirs(save_path) + + # Save the results to a file + with open(f'{save_path}/data.json', 'w') as f: + json.dump(data, f) + + # Create ROC plot for MIAResult + filename = f'{save_path}/ROC' + temp_res = MIAResult(load=True) + temp_res.tpr = self.tpr + temp_res.fpr = self.fpr + temp_res.id = self.id + self.create_plot(results = [temp_res], + filename = filename + ) + + # Create SignalHistogram plot for MIAResult + filename = f'{save_path}/SignalHistogram.png' + self.create_signal_histogram(filename = filename, + signal_values = self.signal_values, + true_labels = self.true_labels, + threshold = self.threshold + ) + + def get_strongest(self, results) -> list: + """Method for selecting the strongest attack.""" + return max((res for res in results), key=lambda d: d.roc_auc) + + def create_signal_histogram(self, filename, signal_values, true_labels, threshold) -> None: + + values = np.array(signal_values).ravel() + labels = np.array(true_labels).ravel() + threshold = threshold + + data = pd.DataFrame( + { + "Signal": values, + "Membership": ["Member" if y == 1 else "Non-member" for y in labels], + } + ) + + bin_edges = np.histogram_bin_edges(values, bins=1000) + + histogram = sn.histplot( + data=data, + x="Signal", + hue="Membership", + element="step", + kde=True, + bins = bin_edges + ) + + if threshold is not None and isinstance(threshold, float): + histogram.axvline(x=threshold, linestyle="--", color="C{}".format(2)) + histogram.text( + x=threshold - (np.max(values) - np.min(values)) / 30, + y=0.8, + s="Threshold", + rotation=90, + color="C{}".format(2), + transform=histogram.get_xaxis_transform(), + ) + + plt.grid() + plt.xlabel("Signal value") + plt.ylabel("Number of samples") + plt.title("Signal histogram") + plt.savefig(fname=filename, dpi=1000) + plt.clf() + + def create_plot(self, results, filename = "", save_name = "") -> None: + + # Create plot for results + reduced_labels = reduce_to_unique_labels(results) + for res, label in zip(results, reduced_labels): + + plt.fill_between(res.fpr, res.tpr, alpha=0.15) + plt.plot(res.fpr, res.tpr, label=label) + + # Plot random guesses + range01 = np.linspace(0, 1) + plt.plot(range01, range01, "--", label="Random guess") + + # Set plot parameters + plt.yscale("log") + plt.xscale("log") + plt.xlim(left=1e-5) + plt.ylim(bottom=1e-5) + plt.tight_layout() + plt.grid() + plt.legend(bbox_to_anchor =(0.5,-0.27), loc='lower center') + + plt.xlabel("False positive rate (FPR)") + plt.ylabel("True positive rate (TPR)") + plt.title(save_name+"ROC Curve") + plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches='tight') + plt.clf() + + def create_results( + self: Self, + results: list, + save_dir: str = "./", + save_name: str = "foo", + ): + + filename = f"{save_dir}/{save_name}" + + self.create_plot(results, filename, save_name) + + return self._latex(results, save_name, filename) + + def _latex(self, results, subsection, filename): + """Latex method for MIAResult.""" + + latex_content = "" + latex_content += f""" + \\subsection{{{" ".join(subsection.split("_"))}}} + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{filename}.png}} + \\end{{figure}} + """ + + latex_content += f''' + \\resizebox{{\\linewidth}}{{!}}{{% + \\begin{{tabularx}}{{\\textwidth}}{{l c l l l l}} + Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR \\\\ + \\hline + ''' + + def config_latex_style(config): + config = " \\\\ ".join(config.split("-")[1:]) + config = "-".join(config.split("_")) + return f"""\\shortstack{{{config}}}""" + + for res in results: + config = config_latex_style(res.id) + latex_content += f'''{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & {res.fixed_fpr_table["TPR@0.1%FPR"]} & {res.fixed_fpr_table["TPR@0.01%FPR"]} & {res.fixed_fpr_table["TPR@0.0%FPR"]} \\\\ \\hline + ''' + latex_content += f""" + \\end{{tabularx}} + }} + \\newline + """ + return latex_content + + + class GIAResults: """Contains results for a GIA attack.""" def __init__(self: Self, original_data: DataLoader, recreated_data: DataLoader, - psnr_score: float, data_mean: float, data_std: float) -> None: + psnr_score: float, data_mean: float, data_std: float, load: bool) -> None: self.original_data = original_data self.recreated_data = recreated_data self.PSNR_score = psnr_score self.data_mean = data_mean self.data_std = data_std - def prepare_privacy_risk_report(self: Self, attack_name: str, save_path: str) -> None: - """Risk report for GIA. WIP.""" + if load: + return + + def load(self, data): + self.original = data["original"] + self.resulttype = data["resulttype"] + self.recreated = data["recreated"] + self.id = data["id"] + + def save(self: Self, save_path: str, name: str, config: dict): + """Save the GIAResults to disk.""" + + result_config = config["attack_list"][name] + + # Get the name for the attack configuration + config_name = get_config_name(result_config) + self.id = f"{name}{config_name}" + save_path = f"{save_path}/{name}/{self.id}" def extract_tensors_from_subset(dataset: Dataset) -> Tensor: all_tensors = [] @@ -204,12 +470,181 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: original_data = extract_tensors_from_subset(self.original_data.dataset) output_denormalized = clamp(recreated_data * self.data_std + self.data_mean, 0, 1) - save_image(output_denormalized, os.path.join(save_path, "recreated_image.png")) + recreated = os.path.join(save_path, "recreated_image.png") + save_image(output_denormalized, recreated) gt_denormalized = clamp(original_data * self.data_std + self.data_mean, 0, 1) - save_image(gt_denormalized, os.path.join(save_path, "original_image.png")) + original = os.path.join(save_path, "original_image.png") + save_image(gt_denormalized, original) + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "original": original, + "recreated": recreated, + "id": self.id, + } + + # Check if path exists, otherwise create it. + if not os.path.exists(f'{save_path}'): + os.makedirs(f'{save_path}') - return attack_name + # Save the results to a file + with open(f'{save_path}/data.json', 'w') as f: + json.dump(data, f) + + pass + + def create_result(self: Self, attack_name: str, save_path: str) -> None: + """Result method for GIA.""" + + def _latex(attack_name, original, recreated): + latex_content = f""" + \\subsection{{{" ".join(attack_name.split("_"))}}} + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{original}}} + \\caption{{Original}} + \\end{{figure}} + + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{recreated}}} + \\caption{{Original}} + \\end{{figure}} + """ + return latex_content + + return _latex(attack_name=attack_name, original=save_path+"recreated_image.png", recreated=save_path+"original_image.png") + +class SyntheticResult: + """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" + + def __init__( # noqa: PLR0913 + self:Self, + load: bool = False, + )-> None: + """Initalze Result method + + Args: + ---- + """ + # Initialize values to result object + # self.values = values + + # Have a method to return if the results are to be loaded + if load: + return + + # Create some result + # self.result_values = some_result + + def load(self, data: dict): + """Load the TEMPLATEResult class to disk.""" + # self.result_values = data["some_result"] + pass + + def save(self:Self, path: str, name: str, config:dict = None): + """Save the TEMPLATEResult class to disk.""" + + result_config = config["attack_list"][name] + + # Data to be saved + data = { + "some_result": self.result_values + } + + # Get the name for the attack configuration + config_name = get_config_name(result_config) + self.id = f"{name}{config_name}" + save_path = f'{path}/{name}/{self.id}' + + # Check if path exists, otherwise create it. + if not os.path.exists(f'{save_path}'): + os.makedirs(f'{save_path}') + + # Save the results to a file + with open(f'{save_path}/data.json', 'w') as f: + json.dump(data, f) + +class TEMPLATEResult: + """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" + + def __init__( # noqa: PLR0913 + self:Self, + load: bool = False, + )-> None: + """Initalze Result method + + Args: + ---- + """ + # Initialize values to result object + # self.values = values + + # Have a method to return if the results are to be loaded + if load: + return + + # Create some result + # self.result_values = some_result + + def load(self, data: dict): + """Load the TEMPLATEResult class to disk.""" + # self.result_values = data["some_result"] + pass + + def save(self:Self, path: str, name: str, config:dict = None): + """Save the TEMPLATEResult class to disk.""" + + result_config = config["attack_list"][name] + + # Data to be saved + data = { + "some_result": self.result_values + } + + # Get the name for the attack configuration + config_name = get_config_name(result_config) + self.id = f"{name}{config_name}" + + # Check if path exists, otherwise create it. + if not os.path.exists(f'{path}/{name}/{self.id}'): + os.makedirs(f'{path}/{name}/{self.id}') + + # Save the results to a file + with open(f'{path}/{name}/{self.id}/data.json', 'w') as f: + json.dump(data, f) + + def create_result(self, results): + """Method for results.""" + def _latex(results): + """Latex method for TEMPLATEResult""" + pass + pass + + def create_result(self, results): + """Method for results.""" + def _latex(results): + """Latex method for TEMPLATEResult""" + pass + pass + +def get_result_fixed_fpr(fpr, tpr): + + # Function to find TPR at given FPR thresholds + def find_tpr_at_fpr(fpr_array:np.ndarray, tpr_array:np.ndarray, threshold:float): #-> Optional[str]: + try: + # Find the last index where FPR is less than the threshold + valid_index = np.where(fpr_array < threshold)[0][-1] + return float(f"{tpr_array[valid_index] * 100:.4f}") + except IndexError: + # Return None or some default value if no valid index found + return "N/A" + + # Compute TPR values at various FPR thresholds + return {"TPR@1.0%FPR": find_tpr_at_fpr(fpr, tpr, 0.01), + "TPR@0.1%FPR": find_tpr_at_fpr(fpr, tpr, 0.001), + "TPR@0.01%FPR": find_tpr_at_fpr(fpr, tpr, 0.0001), + "TPR@0.0%FPR": find_tpr_at_fpr(fpr, tpr, 0.0)} def get_config_name(config): config = dict(sorted(config.items())) @@ -225,4 +660,61 @@ def get_config_name(config): config_name += f"-{key}" else: config_name += f"-{key}={value}" - return config_name \ No newline at end of file + return config_name + +def reduce_to_unique_labels(results): + """Reduce very long labels to unique and distinct ones.""" + strings = [res.id for res in results] + + # Dictionary to store name as key and a list of configurations as value + name_configs = defaultdict(list) + + # Parse each string and store configurations + for s in strings: + parts = s.split('-') + name = parts[0] # The first part is the name + config = '-'.join(parts[1:]) if len(parts) > 1 else '' # The rest is the configuration + name_configs[name].append(config) # Store the configuration under the name + + def find_common_suffix(configs): + """Helper function to find the common suffix among multiple configurations""" + if not configs: + return '' + + # Split each configuration by "-" and zip them in reverse to compare backwards + reversed_configs = [config.split('-')[::-1] for config in configs] + common_suffix = [] + + for elements in zip(*reversed_configs): + if all(e == elements[0] for e in elements): + common_suffix.append(elements[0]) + else: + break + + # Return the common suffix as a string, reversed back to normal order + return '-'.join(common_suffix[::-1]) + + result = [] + + # Process each name and its configurations + for name, configs in name_configs.items(): + if len(configs) > 1: + # Find the common suffix for the configurations + common_suffix = find_common_suffix(configs) + + # Remove the common suffix from each configuration + trimmed_configs = [config[:-(len(common_suffix) + 1)] if common_suffix and config.endswith(common_suffix) else config for config in configs] + + # Process configurations based on whether they share the same pattern + for config in trimmed_configs: + if config: + result.append(f"{name}-{config}") + else: + result.append(name) + else: + # If only one configuration, just return the string as is + result.append(f"{name}") + + return result + + diff --git a/leakpro/reporting/audit_report.py b/leakpro/reporting/audit_report.py index 09974877..0a3b9e82 100644 --- a/leakpro/reporting/audit_report.py +++ b/leakpro/reporting/audit_report.py @@ -648,7 +648,7 @@ def read_and_parse_data(filename:str) -> dict: return data # Main logic to process and save results -def fixed_fpr_results(fpr:np.ndarray, tpr:np.ndarray, configs:dict, filename:str) -> None: +def fixed_fpr_results(fpr:np.ndarray, tpr:np.ndarray, configs:dict, filename:str = None) -> None: """Compute and save fixed FPR results. Args: diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index 1a0dfef0..067c3202 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -4,24 +4,26 @@ import os import subprocess -# from leakpro.reporting.utils import get_config_name -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import CombinedMetricResult, MIAResult import matplotlib.pyplot as plt -def load_mia_results(path: str, name: str): - with open(f'{path}/{name}_data') as f: - result_data = json.load(f) - return result_data - # Report Handler -class report_handler(): +class ReportHandler(): """Implementation of the report handler.""" def __init__(self, report_dir: str, logger:logging.Logger) -> None: self.logger = logger self.report_dir = report_dir - self.image_paths = [] + self.pdf_results = {} + self.leakpro_types = ["MIAResult", + "GIAResults", + "SyntheticResult" + ] + + # Initiate empty lists for the different types of LeakPro attack types + for key in self.leakpro_types: + self.pdf_results[key] = [] def save_results(self, attack_name: str, result_data: dict, config: dict) -> None: """Save attack results. """ @@ -41,8 +43,6 @@ def load_results(self): # Extract class name and data resulttype = data["resulttype"] - primitives = data["primitives"] - config = data["config"] # Dynamically get the class from its name (resulttype) # This assumes that the class is already defined in the current module or imported @@ -52,54 +52,20 @@ def load_results(self): raise ValueError(f"Class '{resulttype}' not found.") # Initialize the class using the saved primitives - instance = cls( - predicted_labels=np.array(primitives["predicted_labels"]), - true_labels=np.array(primitives["true_labels"]), - predictions_proba=np.array(primitives["predictions_proba"]) if primitives["predictions_proba"] is not None else None, - signal_values=np.array(primitives["signal_values"]) if primitives["signal_values"] is not None else None, - threshold=np.array(primitives["threshold"]) if primitives["threshold"] is not None else None, - ) - instance.config = config - instance.id = subdir.name - instance.resultname = parentdir.name + instance = cls(load=True) + instance.load(data) + + if instance.id is None: + instance.id = subdir.name + + if instance.resultname is None: + instance.resultname = parentdir.name + self.results.append(instance) except Exception as e: self.logger.info(f"Not able to load data, Error: {e}") - def _plot_merged_results( - self, - merged_results, - title = "ROC curve", - save_name = "", - ): - - filename = f"{self.report_dir}/{save_name}" - - for res in merged_results: - - fpr = res.fp / (res.fp + res.tn) - tpr = res.tp / (res.tp + res.fn) - - range01 = np.linspace(0, 1) - plt.fill_between(fpr, tpr, alpha=0.15) - plt.plot(fpr, tpr, label=res.id) - - plt.plot(range01, range01, "--", label="Random guess") - plt.yscale("log") - plt.xscale("log") - plt.tight_layout() - plt.grid() - plt.legend() - plt.xlabel("False positive rate (FPR)") - plt.ylabel("True positive rate (TPR)") - plt.title(title) - plt.xlim(left=1e-5) - plt.ylim(bottom=1e-5) - plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches='tight') - plt.clf() - return filename - def _get_results_of_name(self, results, resultname_value) -> list: indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] return [results[idx] for idx in indices] @@ -112,66 +78,126 @@ def _get_all_attacknames(self): return attack_name_list def create_results_all(self) -> None: - names = self._plot_merged_results(merged_results=self.results, save_name="all_results") - self.image_paths.append(names) - pass - - def get_strongest(self, results) -> list: - return max((res for res in results), key=lambda d: d.roc_auc) + for result_type in self.leakpro_types: + try: + # Get all results of type "Result" + results = [res for res in self.results if res.resulttype == result_type] + + # If no results of type "result_type" is found, skip to next result_type + if len(results) == 0: + self.logger.info(f"No results of type {result_type} found.") + continue + + # Create all results + merged_result = results[0].create_results(results=results, save_dir=self.report_dir, save_name="all_results") + self.pdf_results[result_type].append(merged_result) + + except Exception as e: + print("all", e) def create_results_strong(self): - attack_name_grouped_results = [self._get_results_of_name(self.results, name) for name in self._get_all_attacknames()] - strongest_results = [self.get_strongest(attack_name) for attack_name in attack_name_grouped_results] - names = self._plot_merged_results(merged_results=strongest_results, save_name="strongest_attacks") - self.image_paths.append(names) - pass + for result_type in self.leakpro_types: + try: + # Get all results of type "Result" + results = [res for res in self.results if res.resulttype == result_type] + + # If no results of type "result_type" is found, skip to next result_type + if len(results) == 0: + self.logger.info(f"No \'strong\' results of type {result_type} found.") + continue + + # Get all attack names + attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] + + # Get the strongest result for each attack name + strongest_results = [result[0].get_strongest(result) for result in attack_name_grouped_results] + + # Create the strongest results + merged_result = results[0].create_results(results=strongest_results, save_dir=self.report_dir, save_name="strong_results") + self.pdf_results[result_type].append(merged_result) + + except Exception as e: + print("results_strong", e) def create_results_attackname_grouped(self): + # Get all attack names all_attack_names = self._get_all_attacknames() - print(all_attack_names) - for name in all_attack_names: - attack_results = self._get_results_of_name(self.results, name) - names = self._plot_merged_results(merged_results=attack_results, save_name="all_"+name) - self.image_paths.append(names) - pass - # TODO: Make other useful groupings of results - def create_results_numshadowmodels(self): - pass + for result_type in self.leakpro_types: + + # Get all results of type "Result" + results = [res for res in self.results if res.resulttype == result_type] + + # If no results of type "result_type" is found, skip to next result_type + if len(results) == 0: + self.logger.info(f"No results of type {result_type} found.") + continue + + for name in all_attack_names: + + try: + # Get result for each attack names + attack_results = self._get_results_of_name(results, name) + + # Create results + merged_result = attack_results[0].create_results(results=attack_results, save_dir=self.report_dir, save_name="grouped_"+name) + self.pdf_results[result_type].append(merged_result) + + except Exception as e: + print("create_results_attackname_grouped", e) def create_report(self): + """Method to create PDF report""" + + # Create initial part of the document. self._init_pdf() - for image in self.image_paths: - self._append_to_pdf(image_path=image) + # Append all results to the document + for result_type in self.leakpro_types: + if len(self.pdf_results[result_type]) > 0: + self.latex_content += f"""\\section{{{result_type}}}""" + for res in self.pdf_results[result_type]: + self.latex_content += res + # Compile the PDF self._compile_pdf() - pass def _init_pdf(self,): self.latex_content = f""" \\documentclass{{article}} + \\usepackage{{tabularx}} \\usepackage{{graphicx}} - + \\usepackage{{graphics}} \\begin{{document}} """ - pass - def _append_to_pdf(self, image_path=None, table=None): - self.latex_content += f""" - \\begin{{figure}}[ht] - \\includegraphics[width=0.9\\textwidth]{{{image_path}.png}} - \\end{{figure}} - """ - pass + def _compile_pdf(self, install_flag: bool = False): + """Method to compile PDF.""" - def _compile_pdf(self): self.latex_content += f""" \\end{{document}} """ with open(f'{self.report_dir}/LeakPro_output.tex', 'w') as f: f.write(self.latex_content) - cmd = ['pdflatex', '-interaction', 'nonstopmode', f'{self.report_dir}/LeakPro_output.tex'] - proc = subprocess.Popen(cmd) - pass \ No newline at end of file + # Check if pdflatex is installed + try: + check = subprocess.check_output(["which", "pdflatex"], universal_newlines=True) + assert "pdflatex" in check + except: + # Option to install pdflatex + self.logger.info("Could not find pdflatex installed\nPlease install pdflatex with \"apt install texlive-latex-base\"") + choice = input("Do you want to install pdflatex? (Y/n): ").lower() + if (choice in {"y", "yes"} or install_flag==True): + proc = subprocess.Popen(["apt", "install", "-y", "texlive-latex-base"], stdout=subprocess.DEVNULL) + proc.communicate() + + # Compile PDF if possible + try: + cmd = ['pdflatex', '-interaction', 'nonstopmode', f'{self.report_dir}/LeakPro_output.tex'] + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) + proc.communicate() + self.logger.info("PDF compiled") + except Exception as e: + print(e) + self.logger.info("Could not compile PDF") \ No newline at end of file diff --git a/tests/test_attack_result.py/__init__.py b/tests/test_attack_result.py/__init__.py new file mode 100644 index 00000000..c4bfdc70 --- /dev/null +++ b/tests/test_attack_result.py/__init__.py @@ -0,0 +1 @@ +"""Init file for attack result tests""" \ No newline at end of file diff --git a/tests/test_attack_result.py/test_attack_result.py b/tests/test_attack_result.py/test_attack_result.py new file mode 100644 index 00000000..dab9ce27 --- /dev/null +++ b/tests/test_attack_result.py/test_attack_result.py @@ -0,0 +1,149 @@ +import unittest +import os +import json +import logging +import subprocess +import tempfile +from unittest.mock import MagicMock, patch, mock_open, call +from leakpro.metrics.attack_result import * + +class TestMIAResult(unittest.TestCase): + + def setUp(self) -> None: + """Set up temporary directory and logger for MIAResult.""" + self.temp_dir = tempfile.TemporaryDirectory() + + predicted_labels = np.array([[False, False, False, False, False, False], + [False, False, False, False, False, False], + [False, False, True, False, False, False], + [False, True, True, False, True, False], + [ True, True, True, True, True, True], + [ True, True, True, True, True, True], + [ True, True, True, True, True, True], + [ True, True, True, True, True, True], + [ True, True, True, True, True, True], + [ True, True, True, True, True, True]]) + true_labels = np.ones((6)) + signal_values = np.array([[-0.00614866], + [-0.45619705], + [-2.30781003], + [ 0.46973035], + [-0.1584589 ], + [ 0.14289466]]) + + + predictions_proba = None + threshold = None + audit_indices = np.array([0, 1, 2, 3]) + resultname = None + id = None + + self.miaresult = MIAResult(predicted_labels = predicted_labels, + true_labels = true_labels, + signal_values = signal_values, + predictions_proba = predictions_proba, + threshold = threshold, + audit_indices = audit_indices, + resultname = resultname, + id = id) + + + + self.config = {'random_seed': 1234, 'attack_list': + {'lira': + {'training_data_fraction': 0.5, + 'num_shadow_models': 3, + 'online': True} + }, + 'report_log': + './leakpro_output/results', + 'config_log': + './leakpro_output/config', + 'target_model_folder': + './target', + 'attack_folder': + 'attack_objects', + 'attack_type': + 'mia', + 'split_method': + 'no_overlapping' + } + + def tearDown(self) -> None: + """Clean up temporary directory.""" + self.temp_dir.cleanup() + + def test_MIAResult_init(self) -> None: + """Test the initialization of MIAResult.""" + assert self.miaresult.id == None + + def test_check_tpr_fpr(self): + assert np.allclose(self.miaresult.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) + assert self.miaresult.fp.all() == 0. + assert self.miaresult.tn.all() == 0. + + def test_save_load_MIAResult(self) -> None: + + name = "lira" + config_name = get_config_name(self.config['attack_list'][name]) + save_path = f"{self.temp_dir}/{name}/{name}{config_name}" + + # Test saving + self.miaresult.save(self.temp_dir, name, self.config) + + assert os.path.isdir(save_path) + assert os.path.exists(f"{save_path}/data.json") + assert os.path.exists(f"{save_path}/ROC.png") + assert os.path.exists(f"{save_path}/SignalHistogram.png") + + # Test loading + with open(f"{save_path}/data.json") as f: + data = json.load(f) + + self.miaresult_new = MIAResult(load=True) + assert self.miaresult_new.predicted_labels == None + assert self.miaresult_new.true_labels == None + assert self.miaresult_new.signal_values == None + + self.miaresult_new.load(data) + assert np.allclose(self.miaresult_new.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) + + def test_get_strongest_MIAResult(self) -> None: + """Test selecting the strongest attack based on ROC AUC.""" + result_1 = MagicMock(roc_auc=0.75) + result_2 = MagicMock(roc_auc=0.85) + result_3 = MagicMock(roc_auc=0.65) + + mia_result = MIAResult(load=True) + strongest = mia_result.get_strongest([result_1, result_2, result_3]) + + # The strongest attack should be the one with the highest ROC AUC + assert strongest == result_2 + + def test_latex(self): + """Test if the LaTeX content is generated correctly.""" + + result = [MagicMock(id="attack-config-1", resultname="test_attack_1", fixed_fpr_table={"TPR@1.0%FPR": 0.90, "TPR@0.1%FPR": 0.80, "TPR@0.01%FPR": 0.70, "TPR@0.0%FPR": 0.60})] + subsection = "attack_comparison" + filename = f"{self.temp_dir}/test.png" + + latex_content = MIAResult(load=True)._latex(result, subsection, filename) + + # Check that the subsection is correctly included + self.assertIn("\\subsection{attack comparison}", latex_content) + + # Check that the figure is correctly included + self.assertIn(f"\\includegraphics[width=0.8\\textwidth]{{{filename}.png}}", latex_content) + + # Check that the table header is correct + self.assertIn("Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR", latex_content) + + # Check if the results for mock_result are included correctly + self.assertIn("test-attack-1", latex_content) + self.assertIn("0.9", latex_content) + self.assertIn("0.8", latex_content) + self.assertIn("0.7", latex_content) + self.assertIn("0.6", latex_content) + + # Ensure the LaTeX content ends properly + self.assertIn("\\newline\n", latex_content) \ No newline at end of file diff --git a/tests/test_report_handler/__init__.py b/tests/test_report_handler/__init__.py new file mode 100644 index 00000000..796d15a0 --- /dev/null +++ b/tests/test_report_handler/__init__.py @@ -0,0 +1 @@ +"""Init file for report handler tests""" \ No newline at end of file diff --git a/tests/test_report_handler/test_report_handler.py b/tests/test_report_handler/test_report_handler.py new file mode 100644 index 00000000..9ed6b952 --- /dev/null +++ b/tests/test_report_handler/test_report_handler.py @@ -0,0 +1,77 @@ +import unittest +import os +import json +import logging +import subprocess +import tempfile +from unittest.mock import MagicMock, patch, mock_open, call +from leakpro.reporting.report_handler import ReportHandler +from leakpro.metrics.attack_result import * + +class TestReportHandler(unittest.TestCase): + + def setUp(self) -> None: + """Set up temporary directory and logger for ReportHandler.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.logger = logging.getLogger('test_logger') + self.logger.setLevel(logging.INFO) + self.report_handler = ReportHandler(report_dir=self.temp_dir.name, logger=self.logger) + + def tearDown(self) -> None: + """Clean up temporary directory.""" + self.temp_dir.cleanup() + + def test_report_handler_initialization(self) -> None: + """Test the initialization of ReportHandler.""" + assert self.report_handler is not None + assert self.report_handler.report_dir == self.temp_dir.name + assert isinstance(self.report_handler.logger, logging.Logger) + + types = ["MIAResult", "GIAResults", "SyntheticResult"] + assert False not in [_type in types for _type in self.report_handler.leakpro_types] + assert True not in [True if self.report_handler.pdf_results[key] else False for key in self.report_handler.leakpro_types] + assert False not in [_type in globals() for _type in types] + + def test_init_pdf(self) -> None: + assert hasattr(self.report_handler, 'latex_content') == False + + self.report_handler._init_pdf() + assert "documentclass" in self.report_handler.latex_content + assert "begin" in self.report_handler.latex_content + + def test_compile_pdf(self) -> None: + """Test PDF compilation.""" + + self.report_handler._init_pdf() + self.report_handler._compile_pdf(install_flag=True) + + assert "end" in self.report_handler.latex_content + assert os.path.isfile(f'{self.report_handler.report_dir}/LeakPro_output.tex') + assert os.path.isfile(f'./LeakPro_output.pdf') + + def test_get_all_attacknames(self) -> None: + """Test retrieval of all attack names.""" + result_mock_1 = MagicMock(resultname="Attack1") + result_mock_2 = MagicMock(resultname="Attack2") + self.report_handler.results = [result_mock_1, result_mock_2, result_mock_1] + + attack_names = self.report_handler._get_all_attacknames() + + assert attack_names == ["Attack1", "Attack2"] + + def test_get_results_of_name(self): + + """Test retrieval of all attack names.""" + result_mock_1 = MagicMock(resultname="Attack1") + result_mock_2 = MagicMock(resultname="Attack2") + result_mock_3 = MagicMock(resultname="Attack2") + result_mock_4 = MagicMock(resultname="Attack3") + result_mock_5 = MagicMock(resultname="Attack3") + result_mock_6 = MagicMock(resultname="Attack3") + + self.report_handler.results = [result_mock_1, result_mock_2, result_mock_3, + result_mock_4, result_mock_5, result_mock_6] + + assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack1")) == 1 + assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack2")) == 2 + assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack3")) == 3 \ No newline at end of file From cf3a3433a461a4dd953efcf0f6760a32b174dda4 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Tue, 22 Oct 2024 22:29:15 +0000 Subject: [PATCH 03/14] ruff check --fix --- examples/mia/tabular_mia/adult_handler.py | 9 +- examples/mia/tabular_mia/main.ipynb | 35 ++--- .../utils/adult_data_preparation.py | 45 +++--- .../utils/adult_model_preparation.py | 27 ++-- .../synthetic_data/anomalies_example.ipynb | 13 +- .../synthetic_data/inference_example.ipynb | 1 + .../synthetic_data/linkability_example.ipynb | 1 + .../synthetic_data/singling_out_example.ipynb | 1 + leakpro/attacks/mia_attacks/lira.py | 2 +- leakpro/metrics/attack_result.py | 130 +++++++++--------- leakpro/reporting/report_handler.py | 48 +++---- leakpro/reporting/utils.py | 2 +- leakpro/tests/conftest.py | 29 ++-- .../input_handler/image_input_handler.py | 3 +- leakpro/tests/input_handler/image_utils.py | 6 +- .../input_handler/tabular_input_handler.py | 9 +- leakpro/tests/input_handler/tabular_utils.py | 40 +++--- .../input_handler/test_tabular_handler.py | 14 +- .../tests/mia_attacks/attacks/test_lira.py | 49 +++---- .../utils/test_shadow_model_handler.py | 36 ++--- .../tests/test_attack_result}/__init__.py | 0 .../test_attack_result}/test_attack_result.py | 0 .../tests}/test_report_handler/__init__.py | 0 .../test_report_handler.py | 0 24 files changed, 255 insertions(+), 245 deletions(-) rename {tests/test_attack_result.py => leakpro/tests/test_attack_result}/__init__.py (100%) rename {tests/test_attack_result.py => leakpro/tests/test_attack_result}/test_attack_result.py (100%) rename {tests => leakpro/tests}/test_report_handler/__init__.py (100%) rename {tests => leakpro/tests}/test_report_handler/test_report_handler.py (100%) diff --git a/examples/mia/tabular_mia/adult_handler.py b/examples/mia/tabular_mia/adult_handler.py index cf03361c..5ee74387 100644 --- a/examples/mia/tabular_mia/adult_handler.py +++ b/examples/mia/tabular_mia/adult_handler.py @@ -8,6 +8,7 @@ from leakpro import AbstractInputHandler + class AdultInputHandler(AbstractInputHandler): """Class to handle the user input for the CIFAR10 dataset.""" @@ -41,11 +42,11 @@ def train( criterion = self.get_criterion() optimizer = self.get_optimizer(model) - + for e in tqdm(range(epochs), desc="Training Progress"): model.train() train_acc, train_loss = 0.0, 0.0 - + for data, target in dataloader: target = target.float().unsqueeze(1) data, target = data.to(dev, non_blocking=True), target.to(dev, non_blocking=True) @@ -55,11 +56,11 @@ def train( loss = criterion(output, target) pred = sigmoid(output) >= 0.5 train_acc += pred.eq(target).sum().item() - + loss.backward() optimizer.step() train_loss += loss.item() - + train_acc = train_acc/len(dataloader.dataset) train_loss = train_loss/len(dataloader) diff --git a/examples/mia/tabular_mia/main.ipynb b/examples/mia/tabular_mia/main.ipynb index 5ca090cf..96dbfb87 100644 --- a/examples/mia/tabular_mia/main.ipynb +++ b/examples/mia/tabular_mia/main.ipynb @@ -34,10 +34,13 @@ "project_root = os.path.abspath(os.path.join(os.getcwd(), \"../../..\"))\n", "sys.path.append(project_root)\n", "\n", - "from examples.mia.tabular_mia.utils.adult_data_preparation import preprocess_adult_dataset, get_adult_dataloaders, download_adult_dataset\n", + "from examples.mia.tabular_mia.utils.adult_data_preparation import (\n", + " download_adult_dataset,\n", + " get_adult_dataloaders,\n", + " preprocess_adult_dataset,\n", + ")\n", "from examples.mia.tabular_mia.utils.adult_model_preparation import AdultNet, create_trained_model_and_metadata\n", "\n", - "\n", "# Generate the dataset and dataloaders\n", "path = os.path.join(os.getcwd(), \"data/\")\n", "\n", @@ -52,9 +55,9 @@ "if not os.path.exists(\"target\"):\n", " os.makedirs(\"target\")\n", "model = AdultNet(input_size=n_features, hidden_size=64, num_classes=n_classes)\n", - "train_acc, train_loss, test_acc, test_loss = create_trained_model_and_metadata(model, \n", - " train_loader, \n", - " test_loader, \n", + "train_acc, train_loss, test_acc, test_loss = create_trained_model_and_metadata(model,\n", + " train_loader,\n", + " test_loader,\n", " epochs=10)" ] }, @@ -81,20 +84,20 @@ "plt.figure(figsize=(5, 4))\n", "\n", "plt.subplot(1, 2, 1)\n", - "plt.plot(train_acc, label='Train Accuracy')\n", - "plt.plot(test_acc, label='Test Accuracy')\n", - "plt.xlabel('Epoch')\n", - "plt.ylabel('Accuracy')\n", - "plt.title('Accuracy over Epochs')\n", + "plt.plot(train_acc, label=\"Train Accuracy\")\n", + "plt.plot(test_acc, label=\"Test Accuracy\")\n", + "plt.xlabel(\"Epoch\")\n", + "plt.ylabel(\"Accuracy\")\n", + "plt.title(\"Accuracy over Epochs\")\n", "plt.legend()\n", "\n", "# Plot training and test loss\n", "plt.subplot(1, 2, 2)\n", - "plt.plot(train_loss, label='Train Loss')\n", - "plt.plot(test_loss, label='Test Loss')\n", - "plt.xlabel('Epoch')\n", - "plt.ylabel('Loss')\n", - "plt.title('Loss over Epochs')\n", + "plt.plot(train_loss, label=\"Train Loss\")\n", + "plt.plot(test_loss, label=\"Test Loss\")\n", + "plt.xlabel(\"Epoch\")\n", + "plt.ylabel(\"Loss\")\n", + "plt.title(\"Loss over Epochs\")\n", "plt.legend()\n", "\n", "plt.tight_layout()\n", @@ -142,7 +145,7 @@ "# Prepare leakpro object\n", "leakpro = LeakPro(AdultInputHandler, config_path)\n", "\n", - "# Run the audit \n", + "# Run the audit\n", "leakpro.run_audit()" ] }, diff --git a/examples/mia/tabular_mia/utils/adult_data_preparation.py b/examples/mia/tabular_mia/utils/adult_data_preparation.py index 4cb92c1e..d553d034 100644 --- a/examples/mia/tabular_mia/utils/adult_data_preparation.py +++ b/examples/mia/tabular_mia/utils/adult_data_preparation.py @@ -1,25 +1,26 @@ import os +import pickle +import urllib.request + +import joblib import numpy as np import pandas as pd -import joblib -import pickle -from sklearn.preprocessing import LabelEncoder, StandardScaler, OneHotEncoder from sklearn.model_selection import train_test_split -import urllib.request -from torch.utils.data import Dataset, Subset, DataLoader -from torch import tensor, float32 +from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler +from torch import float32, tensor +from torch.utils.data import DataLoader, Dataset, Subset class AdultDataset(Dataset): def __init__(self, x:tensor, y:tensor, dec_to_onehot:dict, one_hot_encoded:bool=True): self.x = x self.y = y - + # create dictionary to map between indices in categorical representation and one-hot encoded representation # For example: cols 1,2 continuous and col 3 categorical with 3 categories will be mapped to {1:1,2:2,3:[3,4,5]} self.dec_to_onehot = dec_to_onehot self.one_hot_encoded = one_hot_encoded - + def __len__(self): return len(self.y) @@ -28,8 +29,8 @@ def __getitem__(self, idx): def subset(self, indices): return AdultDataset(self.x[indices], self.y[indices], self.dec_to_onehot, self.one_hot_encoded) - - + + def download_adult_dataset(data_dir): """Download the Adult Dataset if it's not present.""" # URLs for the dataset @@ -54,22 +55,22 @@ def download_adult_dataset(data_dir): def preprocess_adult_dataset(path): """Get the dataset, download it if necessary, and store it.""" - + if os.path.exists(os.path.join(path, "adult_data.pkl")): with open(os.path.join(path, "adult_data.pkl"), "rb") as f: dataset = joblib.load(f) - else: + else: column_names = [ - "age", "workclass", "fnlwgt", "education", "education-num", + "age", "workclass", "fnlwgt", "education", "education-num", "marital-status", "occupation", "relationship", "race", "sex", "capital-gain", "capital-loss", "hours-per-week", "native-country", "income", ] - + # Load and clean data df_train = pd.read_csv(os.path.join(path, "adult.data"), names=column_names) df_test = pd.read_csv(os.path.join(path, "adult.test"), names=column_names, header=0) df_test["income"] = df_test["income"].str.replace(".", "", regex=False) - + df_concatenated = pd.concat([df_train, df_test], axis=0) df_clean = df_concatenated.replace(" ?", np.nan).dropna() @@ -83,19 +84,19 @@ def preprocess_adult_dataset(path): # Scaling numerical features scaler = StandardScaler() x_numerical = pd.DataFrame(scaler.fit_transform(x[numerical_features]), columns=numerical_features, index=x.index) - + # Label encode the categories one_hot_encoder = OneHotEncoder(sparse_output=False) x_categorical_one_hot = one_hot_encoder.fit_transform(x[categorical_features]) one_hot_feature_names = one_hot_encoder.get_feature_names_out(categorical_features) x_categorical_one_hot_df = pd.DataFrame(x_categorical_one_hot, columns=one_hot_feature_names, index=x.index) - + # Concatenate the numerical and one-hot encoded categorical features x_final = pd.concat([x_numerical, x_categorical_one_hot_df], axis=1) # Label encode the target variable y = pd.Series(LabelEncoder().fit_transform(y)) - + # Add numerical features to the dictionary dec_to_onehot_mapping = {} for i, feature in enumerate(numerical_features): @@ -115,11 +116,11 @@ def preprocess_adult_dataset(path): with open(f"{path}/adult_data.pkl", "wb") as file: pickle.dump(dataset, file) print(f"Save data to {path}.pkl") - + return dataset def get_adult_dataloaders(dataset, train_fraction=0.3, test_fraction=0.3): - + dataset_size = len(dataset) train_size = int(train_fraction * dataset_size) test_size = int(test_fraction * dataset_size) @@ -127,10 +128,10 @@ def get_adult_dataloaders(dataset, train_fraction=0.3, test_fraction=0.3): # Use sklearn's train_test_split to split into train and test indices selected_index = np.random.choice(np.arange(dataset_size), train_size + test_size, replace=False) train_indices, test_indices = train_test_split(selected_index, test_size=test_size) - + train_subset = Subset(dataset, train_indices) test_subset = Subset(dataset, test_indices) - + train_loader = DataLoader(train_subset, batch_size=128, shuffle=True) test_loader = DataLoader(test_subset, batch_size=128, shuffle=False) diff --git a/examples/mia/tabular_mia/utils/adult_model_preparation.py b/examples/mia/tabular_mia/utils/adult_model_preparation.py index a7af914f..3f58f2ac 100644 --- a/examples/mia/tabular_mia/utils/adult_model_preparation.py +++ b/examples/mia/tabular_mia/utils/adult_model_preparation.py @@ -1,8 +1,9 @@ -import torch.nn as nn -from torch import device, optim, cuda, no_grad, save, sigmoid import pickle + +from torch import cuda, device, nn, no_grad, optim, save, sigmoid from tqdm import tqdm + class AdultNet(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super(AdultNet, self).__init__() @@ -13,7 +14,7 @@ def __init__(self, input_size, hidden_size, num_classes): self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_size, hidden_size) self.fc3 = nn.Linear(hidden_size, num_classes) - + def forward(self, x): out = self.fc1(x) out = self.relu(out) @@ -47,11 +48,11 @@ def create_trained_model_and_metadata(model, train_loader, test_loader, epochs = optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.8) train_losses, train_accuracies = [], [] test_losses, test_accuracies = [], [] - + for e in tqdm(range(epochs), desc="Training Progress"): model.train() train_acc, train_loss = 0.0, 0.0 - + for data, target in train_loader: target = target.float().unsqueeze(1) data, target = data.to(device_name, non_blocking=True), target.to(device_name, non_blocking=True) @@ -61,17 +62,17 @@ def create_trained_model_and_metadata(model, train_loader, test_loader, epochs = loss = criterion(output, target) pred = sigmoid(output) >= 0.5 train_acc += pred.eq(target).sum().item() - + loss.backward() optimizer.step() train_loss += loss.item() - + train_loss /= len(train_loader) train_acc /= len(train_loader.dataset) - + train_losses.append(train_loss) train_accuracies.append(train_acc) - + test_loss, test_acc = evaluate(model, test_loader, criterion, device_name) test_losses.append(test_loss) test_accuracies.append(test_acc) @@ -86,12 +87,12 @@ def create_trained_model_and_metadata(model, train_loader, test_loader, epochs = meta_data["train_indices"] = train_loader.dataset.indices meta_data["test_indices"] = test_loader.dataset.indices meta_data["num_train"] = len(meta_data["train_indices"]) - + # Write init params meta_data["init_params"] = {} for key, value in model.init_params.items(): meta_data["init_params"][key] = value - + # read out optimizer parameters meta_data["optimizer"] = {} meta_data["optimizer"]["name"] = optimizer.__class__.__name__.lower() @@ -112,8 +113,8 @@ def create_trained_model_and_metadata(model, train_loader, test_loader, epochs = meta_data["train_loss"] = train_loss meta_data["test_loss"] = test_loss meta_data["dataset"] = "adult" - + with open("target/model_metadata.pkl", "wb") as f: pickle.dump(meta_data, f) - + return train_accuracies, train_losses, test_accuracies, test_losses diff --git a/examples/synthetic_data/anomalies_example.ipynb b/examples/synthetic_data/anomalies_example.ipynb index 5e435ce0..249958b5 100644 --- a/examples/synthetic_data/anomalies_example.ipynb +++ b/examples/synthetic_data/anomalies_example.ipynb @@ -24,16 +24,17 @@ "outputs": [], "source": [ "import os\n", + "import sys\n", + "\n", "import pandas as pd\n", "\n", - "import sys\n", "sys.path.append(\"../..\")\n", "\n", + "from leakpro.synthetic_data_attacks import plots\n", "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", - "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", - "from leakpro.synthetic_data_attacks import plots\n", "\n", "#Get ori and syn\n", "n_samples = 100\n", @@ -98,7 +99,7 @@ } ], "source": [ - "print('Syn anom shape',syn_anom.shape)" + "print(\"Syn anom shape\",syn_anom.shape)" ] }, { @@ -128,7 +129,7 @@ ], "source": [ "sin_out_res = singling_out_risk_evaluation(\n", - " dataset = 'adults',\n", + " dataset = \"adults\",\n", " ori = ori,\n", " syn = syn_anom,\n", " n_attacks = syn_anom.shape[0]\n", @@ -199,7 +200,7 @@ ], "source": [ "inf_res = inference_risk_evaluation(\n", - " dataset = 'adults',\n", + " dataset = \"adults\",\n", " ori = ori,\n", " syn = syn_anom,\n", " worst_case_flag = True,\n", diff --git a/examples/synthetic_data/inference_example.ipynb b/examples/synthetic_data/inference_example.ipynb index 2904789e..bb7f0498 100644 --- a/examples/synthetic_data/inference_example.ipynb +++ b/examples/synthetic_data/inference_example.ipynb @@ -17,6 +17,7 @@ "source": [ "import os\n", "import sys\n", + "\n", "import pandas as pd\n", "\n", "sys.path.append(\"../..\")\n", diff --git a/examples/synthetic_data/linkability_example.ipynb b/examples/synthetic_data/linkability_example.ipynb index 48337a1c..2d2bf576 100644 --- a/examples/synthetic_data/linkability_example.ipynb +++ b/examples/synthetic_data/linkability_example.ipynb @@ -17,6 +17,7 @@ "source": [ "import os\n", "import sys\n", + "\n", "import pandas as pd\n", "\n", "sys.path.append(\"../..\")\n", diff --git a/examples/synthetic_data/singling_out_example.ipynb b/examples/synthetic_data/singling_out_example.ipynb index b5337016..7413df53 100644 --- a/examples/synthetic_data/singling_out_example.ipynb +++ b/examples/synthetic_data/singling_out_example.ipynb @@ -17,6 +17,7 @@ "source": [ "import os\n", "import sys\n", + "\n", "import pandas as pd\n", "\n", "sys.path.append(\"../..\")\n", diff --git a/leakpro/attacks/mia_attacks/lira.py b/leakpro/attacks/mia_attacks/lira.py index 5a243863..de1edd7c 100644 --- a/leakpro/attacks/mia_attacks/lira.py +++ b/leakpro/attacks/mia_attacks/lira.py @@ -316,7 +316,7 @@ def run_attack(self:Self) -> CombinedMetricResult: return MIAResult( predicted_labels=predictions, true_labels=true_labels, - predictions_proba=None, + predictions_proba=None, signal_values=signal_values, audit_indices=self.audit_dataset["data"], ) diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index 8d20da2e..dfa8ad90 100644 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -1,10 +1,11 @@ """Contains the AttackResult class, which stores the results of an attack.""" -from collections import defaultdict -import os import json -import numpy as np +import os +from collections import defaultdict + import matplotlib.pyplot as plt +import numpy as np import pandas as pd import seaborn as sn from sklearn.metrics import ( @@ -104,7 +105,7 @@ def __init__( # noqa: PLR0913 threshold: Threshold computed by the metric. """ - # TODO REDIFINE THE CLASS SO IT DOSE NOT STORE MATRICIES BUT VECTORS + # TODO REDIFINE THE CLASS SO IT DOSE NOT STORE MATRICIES BUT VECTORS self.predicted_labels = predicted_labels self.true_labels = true_labels @@ -133,13 +134,13 @@ def __init__( # noqa: PLR0913 def _get_primitives(self:Self): """Return the primitives of the CombinedMetricResult class.""" - return {"predicted_labels": self.predicted_labels.tolist(), + return {"predicted_labels": self.predicted_labels.tolist(), "true_labels": self.true_labels.tolist(), - "predictions_proba": self.predictions_proba.tolist() if isinstance(self.predictions_proba, np.ndarray) else None, + "predictions_proba": self.predictions_proba.tolist() if isinstance(self.predictions_proba, np.ndarray) else None, "signal_values": self.signal_values.tolist() if isinstance(self.signal_values, np.ndarray) else None, "threshold": self.threshold.tolist() if isinstance(self.threshold, np.ndarray) else None, } - + def save(self:Self, path: str, name: str, config:dict): """Save the CombinedMetricResult class to disk.""" @@ -157,11 +158,11 @@ def save(self:Self, path: str, name: str, config:dict): config_name = get_config_name(config["attack_list"][name]) # Check if path exists, otherwise create it. - if not os.path.exists(f'{path}/{name}/{name}{config_name}'): - os.makedirs(f'{path}/{name}/{name}{config_name}') + if not os.path.exists(f"{path}/{name}/{name}{config_name}"): + os.makedirs(f"{path}/{name}/{name}{config_name}") # Save the results to a file - with open(f'{path}/{name}/{name}{config_name}/data.json', 'w') as f: + with open(f"{path}/{name}/{name}{config_name}/data.json", "w") as f: json.dump(data, f) def __str__(self:Self) -> str: @@ -216,7 +217,7 @@ def __init__( # noqa: PLR0913 self.metadata = metadata self.resultname = resultname self.id = id - + if load: return @@ -248,7 +249,7 @@ def load(self, data): def save(self:Self, path: str, name: str, config:dict = None): """Save the MIAResults to disk.""" - + print(config) result_config = config["attack_list"][name] @@ -281,11 +282,11 @@ def save(self:Self, path: str, name: str, config:dict = None): os.makedirs(save_path) # Save the results to a file - with open(f'{save_path}/data.json', 'w') as f: + with open(f"{save_path}/data.json", "w") as f: json.dump(data, f) # Create ROC plot for MIAResult - filename = f'{save_path}/ROC' + filename = f"{save_path}/ROC" temp_res = MIAResult(load=True) temp_res.tpr = self.tpr temp_res.fpr = self.fpr @@ -295,17 +296,17 @@ def save(self:Self, path: str, name: str, config:dict = None): ) # Create SignalHistogram plot for MIAResult - filename = f'{save_path}/SignalHistogram.png' + filename = f"{save_path}/SignalHistogram.png" self.create_signal_histogram(filename = filename, signal_values = self.signal_values, true_labels = self.true_labels, threshold = self.threshold ) - + def get_strongest(self, results) -> list: """Method for selecting the strongest attack.""" return max((res for res in results), key=lambda d: d.roc_auc) - + def create_signal_histogram(self, filename, signal_values, true_labels, threshold) -> None: values = np.array(signal_values).ravel() @@ -347,7 +348,7 @@ def create_signal_histogram(self, filename, signal_values, true_labels, threshol plt.title("Signal histogram") plt.savefig(fname=filename, dpi=1000) plt.clf() - + def create_plot(self, results, filename = "", save_name = "") -> None: # Create plot for results @@ -356,11 +357,11 @@ def create_plot(self, results, filename = "", save_name = "") -> None: plt.fill_between(res.fpr, res.tpr, alpha=0.15) plt.plot(res.fpr, res.tpr, label=label) - + # Plot random guesses range01 = np.linspace(0, 1) plt.plot(range01, range01, "--", label="Random guess") - + # Set plot parameters plt.yscale("log") plt.xscale("log") @@ -368,12 +369,12 @@ def create_plot(self, results, filename = "", save_name = "") -> None: plt.ylim(bottom=1e-5) plt.tight_layout() plt.grid() - plt.legend(bbox_to_anchor =(0.5,-0.27), loc='lower center') + plt.legend(bbox_to_anchor =(0.5,-0.27), loc="lower center") plt.xlabel("False positive rate (FPR)") plt.ylabel("True positive rate (TPR)") plt.title(save_name+"ROC Curve") - plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches='tight') + plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches="tight") plt.clf() def create_results( @@ -388,7 +389,7 @@ def create_results( self.create_plot(results, filename, save_name) return self._latex(results, save_name, filename) - + def _latex(self, results, subsection, filename): """Latex method for MIAResult.""" @@ -400,12 +401,12 @@ def _latex(self, results, subsection, filename): \\end{{figure}} """ - latex_content += f''' - \\resizebox{{\\linewidth}}{{!}}{{% - \\begin{{tabularx}}{{\\textwidth}}{{l c l l l l}} + latex_content += """ + \\resizebox{\\linewidth}{!}{% + \\begin{tabularx}{\\textwidth}{l c l l l l} Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR \\\\ \\hline - ''' + """ def config_latex_style(config): config = " \\\\ ".join(config.split("-")[1:]) @@ -414,11 +415,11 @@ def config_latex_style(config): for res in results: config = config_latex_style(res.id) - latex_content += f'''{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & {res.fixed_fpr_table["TPR@0.1%FPR"]} & {res.fixed_fpr_table["TPR@0.01%FPR"]} & {res.fixed_fpr_table["TPR@0.0%FPR"]} \\\\ \\hline - ''' - latex_content += f""" - \\end{{tabularx}} - }} + latex_content += f"""{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & {res.fixed_fpr_table["TPR@0.1%FPR"]} & {res.fixed_fpr_table["TPR@0.01%FPR"]} & {res.fixed_fpr_table["TPR@0.0%FPR"]} \\\\ \\hline + """ + latex_content += """ + \\end{tabularx} + } \\newline """ return latex_content @@ -486,11 +487,11 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: } # Check if path exists, otherwise create it. - if not os.path.exists(f'{save_path}'): - os.makedirs(f'{save_path}') + if not os.path.exists(f"{save_path}"): + os.makedirs(f"{save_path}") # Save the results to a file - with open(f'{save_path}/data.json', 'w') as f: + with open(f"{save_path}/data.json", "w") as f: json.dump(data, f) pass @@ -526,6 +527,7 @@ def __init__( # noqa: PLR0913 Args: ---- + """ # Initialize values to result object # self.values = values @@ -555,16 +557,16 @@ def save(self:Self, path: str, name: str, config:dict = None): # Get the name for the attack configuration config_name = get_config_name(result_config) self.id = f"{name}{config_name}" - save_path = f'{path}/{name}/{self.id}' + save_path = f"{path}/{name}/{self.id}" # Check if path exists, otherwise create it. - if not os.path.exists(f'{save_path}'): - os.makedirs(f'{save_path}') + if not os.path.exists(f"{save_path}"): + os.makedirs(f"{save_path}") # Save the results to a file - with open(f'{save_path}/data.json', 'w') as f: + with open(f"{save_path}/data.json", "w") as f: json.dump(data, f) - + class TEMPLATEResult: """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" @@ -576,6 +578,7 @@ def __init__( # noqa: PLR0913 Args: ---- + """ # Initialize values to result object # self.values = values @@ -607,13 +610,13 @@ def save(self:Self, path: str, name: str, config:dict = None): self.id = f"{name}{config_name}" # Check if path exists, otherwise create it. - if not os.path.exists(f'{path}/{name}/{self.id}'): - os.makedirs(f'{path}/{name}/{self.id}') + if not os.path.exists(f"{path}/{name}/{self.id}"): + os.makedirs(f"{path}/{name}/{self.id}") # Save the results to a file - with open(f'{path}/{name}/{self.id}/data.json', 'w') as f: + with open(f"{path}/{name}/{self.id}/data.json", "w") as f: json.dump(data, f) - + def create_result(self, results): """Method for results.""" def _latex(results): @@ -629,7 +632,7 @@ def _latex(results): pass def get_result_fixed_fpr(fpr, tpr): - + # Function to find TPR at given FPR thresholds def find_tpr_at_fpr(fpr_array:np.ndarray, tpr_array:np.ndarray, threshold:float): #-> Optional[str]: try: @@ -655,11 +658,10 @@ def get_config_name(config): for key, value in zip(list(config.keys()), list(config.values())): if key in exclude: pass + elif type(value) is bool: + config_name += f"-{key}" else: - if type(value) is bool: - config_name += f"-{key}" - else: - config_name += f"-{key}={value}" + config_name += f"-{key}={value}" return config_name def reduce_to_unique_labels(results): @@ -668,43 +670,43 @@ def reduce_to_unique_labels(results): # Dictionary to store name as key and a list of configurations as value name_configs = defaultdict(list) - + # Parse each string and store configurations for s in strings: - parts = s.split('-') + parts = s.split("-") name = parts[0] # The first part is the name - config = '-'.join(parts[1:]) if len(parts) > 1 else '' # The rest is the configuration + config = "-".join(parts[1:]) if len(parts) > 1 else "" # The rest is the configuration name_configs[name].append(config) # Store the configuration under the name - + def find_common_suffix(configs): """Helper function to find the common suffix among multiple configurations""" if not configs: - return '' - + return "" + # Split each configuration by "-" and zip them in reverse to compare backwards - reversed_configs = [config.split('-')[::-1] for config in configs] + reversed_configs = [config.split("-")[::-1] for config in configs] common_suffix = [] - + for elements in zip(*reversed_configs): if all(e == elements[0] for e in elements): common_suffix.append(elements[0]) else: break - + # Return the common suffix as a string, reversed back to normal order - return '-'.join(common_suffix[::-1]) - + return "-".join(common_suffix[::-1]) + result = [] - + # Process each name and its configurations for name, configs in name_configs.items(): if len(configs) > 1: # Find the common suffix for the configurations common_suffix = find_common_suffix(configs) - + # Remove the common suffix from each configuration trimmed_configs = [config[:-(len(common_suffix) + 1)] if common_suffix and config.endswith(common_suffix) else config for config in configs] - + # Process configurations based on whether they share the same pattern for config in trimmed_configs: if config: @@ -714,7 +716,7 @@ def find_common_suffix(configs): else: # If only one configuration, just return the string as is result.append(f"{name}") - + return result diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index 067c3202..340d57f3 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -1,12 +1,8 @@ import json import logging -import numpy as np import os import subprocess -from leakpro.metrics.attack_result import CombinedMetricResult, MIAResult - -import matplotlib.pyplot as plt # Report Handler class ReportHandler(): @@ -20,15 +16,15 @@ def __init__(self, report_dir: str, logger:logging.Logger) -> None: "GIAResults", "SyntheticResult" ] - + # Initiate empty lists for the different types of LeakPro attack types for key in self.leakpro_types: self.pdf_results[key] = [] def save_results(self, attack_name: str, result_data: dict, config: dict) -> None: - """Save attack results. """ - - self.logger.info(f'Saving results for {attack_name}') + """Save attack results.""" + + self.logger.info(f"Saving results for {attack_name}") result_data.save(self.report_dir, attack_name, config) def load_results(self): @@ -50,7 +46,7 @@ def load_results(self): cls = globals()[resulttype] else: raise ValueError(f"Class '{resulttype}' not found.") - + # Initialize the class using the saved primitives instance = cls(load=True) instance.load(data) @@ -69,7 +65,7 @@ def load_results(self): def _get_results_of_name(self, results, resultname_value) -> list: indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] return [results[idx] for idx in indices] - + def _get_all_attacknames(self): attack_name_list = [] for result in self.results: @@ -103,11 +99,11 @@ def create_results_strong(self): # If no results of type "result_type" is found, skip to next result_type if len(results) == 0: - self.logger.info(f"No \'strong\' results of type {result_type} found.") + self.logger.info(f"No 'strong' results of type {result_type} found.") continue # Get all attack names - attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] + attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] # Get the strongest result for each attack name strongest_results = [result[0].get_strongest(result) for result in attack_name_grouped_results] @@ -132,9 +128,9 @@ def create_results_attackname_grouped(self): if len(results) == 0: self.logger.info(f"No results of type {result_type} found.") continue - + for name in all_attack_names: - + try: # Get result for each attack names attack_results = self._get_results_of_name(results, name) @@ -163,21 +159,21 @@ def create_report(self): self._compile_pdf() def _init_pdf(self,): - self.latex_content = f""" - \\documentclass{{article}} - \\usepackage{{tabularx}} - \\usepackage{{graphicx}} - \\usepackage{{graphics}} - \\begin{{document}} + self.latex_content = """ + \\documentclass{article} + \\usepackage{tabularx} + \\usepackage{graphicx} + \\usepackage{graphics} + \\begin{document} """ def _compile_pdf(self, install_flag: bool = False): """Method to compile PDF.""" - self.latex_content += f""" - \\end{{document}} + self.latex_content += """ + \\end{document} """ - with open(f'{self.report_dir}/LeakPro_output.tex', 'w') as f: + with open(f"{self.report_dir}/LeakPro_output.tex", "w") as f: f.write(self.latex_content) # Check if pdflatex is installed @@ -186,7 +182,7 @@ def _compile_pdf(self, install_flag: bool = False): assert "pdflatex" in check except: # Option to install pdflatex - self.logger.info("Could not find pdflatex installed\nPlease install pdflatex with \"apt install texlive-latex-base\"") + self.logger.info('Could not find pdflatex installed\nPlease install pdflatex with "apt install texlive-latex-base"') choice = input("Do you want to install pdflatex? (Y/n): ").lower() if (choice in {"y", "yes"} or install_flag==True): proc = subprocess.Popen(["apt", "install", "-y", "texlive-latex-base"], stdout=subprocess.DEVNULL) @@ -194,10 +190,10 @@ def _compile_pdf(self, install_flag: bool = False): # Compile PDF if possible try: - cmd = ['pdflatex', '-interaction', 'nonstopmode', f'{self.report_dir}/LeakPro_output.tex'] + cmd = ["pdflatex", "-interaction", "nonstopmode", f"{self.report_dir}/LeakPro_output.tex"] proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) proc.communicate() self.logger.info("PDF compiled") except Exception as e: print(e) - self.logger.info("Could not compile PDF") \ No newline at end of file + self.logger.info("Could not compile PDF") diff --git a/leakpro/reporting/utils.py b/leakpro/reporting/utils.py index b078c000..9573d272 100644 --- a/leakpro/reporting/utils.py +++ b/leakpro/reporting/utils.py @@ -42,4 +42,4 @@ def prepare_privacy_risk_report( metric_result=audit_results, save=True, filename=f"{save_path}/Histogram.png", - ) \ No newline at end of file + ) diff --git a/leakpro/tests/conftest.py b/leakpro/tests/conftest.py index cf2c7168..d70b2f71 100644 --- a/leakpro/tests/conftest.py +++ b/leakpro/tests/conftest.py @@ -2,35 +2,34 @@ import os import shutil -from typing import Generator import pytest import yaml from dotmap import DotMap from leakpro import LeakPro -from leakpro.tests.input_handler.image_utils import setup_image_test +from leakpro.tests.constants import STORAGE_PATH, get_audit_config from leakpro.tests.input_handler.image_input_handler import ImageInputHandler +from leakpro.tests.input_handler.image_utils import setup_image_test from leakpro.tests.input_handler.tabular_input_handler import TabularInputHandler from leakpro.tests.input_handler.tabular_utils import setup_tabular_test -from leakpro.tests.constants import STORAGE_PATH, get_audit_config @pytest.fixture(scope="session") def manage_storage_directory(): """Fixture to create and remove the storage directory.""" - + # Setup: Create the folder at the start of the test session os.makedirs(STORAGE_PATH, exist_ok=True) - + # Yield control back to the test session yield - + # Teardown: Remove the folder and its contents at the end of the session if os.path.exists(STORAGE_PATH): shutil.rmtree(STORAGE_PATH) -@pytest.fixture() -def image_handler(manage_storage_directory) -> Generator[ImageInputHandler, None, None]: +@pytest.fixture +def image_handler(manage_storage_directory) -> ImageInputHandler: """Fixture for the image input handler to be shared between many tests.""" config = DotMap() @@ -41,16 +40,16 @@ def image_handler(manage_storage_directory) -> Generator[ImageInputHandler, None config_path = f"{STORAGE_PATH}/image_test_config.yaml" with open(config_path, "w") as f: yaml.dump(config.toDict(), f) - + leakpro = LeakPro(ImageInputHandler, config_path) handler = leakpro.handler handler.configs = DotMap(handler.configs) # Yield control back to the test session - yield handler - -@pytest.fixture() -def tabular_handler(manage_storage_directory) -> Generator[TabularInputHandler, None, None]: + return handler + +@pytest.fixture +def tabular_handler(manage_storage_directory) -> TabularInputHandler: """Fixture for the image input handler to be shared between many tests.""" config = DotMap() @@ -61,10 +60,10 @@ def tabular_handler(manage_storage_directory) -> Generator[TabularInputHandler, config_path = f"{STORAGE_PATH}/tabular_test_config.yaml" with open(config_path, "w") as f: yaml.dump(config.toDict(), f) - + leakpro = LeakPro(TabularInputHandler, config_path) handler = leakpro.handler handler.configs = DotMap(handler.configs) # Yield control back to the test session - yield handler + return handler diff --git a/leakpro/tests/input_handler/image_input_handler.py b/leakpro/tests/input_handler/image_input_handler.py index a2fe2f32..3df37d1c 100644 --- a/leakpro/tests/input_handler/image_input_handler.py +++ b/leakpro/tests/input_handler/image_input_handler.py @@ -5,10 +5,11 @@ from torch.utils.data import DataLoader from tqdm import tqdm -from leakpro.utils.import_helper import Self from leakpro.input_handler.abstract_input_handler import AbstractInputHandler +from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger + class ImageInputHandler(AbstractInputHandler): """Class to handle the user input for the CIFAR10 dataset.""" diff --git a/leakpro/tests/input_handler/image_utils.py b/leakpro/tests/input_handler/image_utils.py index 9c1c3859..9beb14c1 100644 --- a/leakpro/tests/input_handler/image_utils.py +++ b/leakpro/tests/input_handler/image_utils.py @@ -10,9 +10,9 @@ from torch.utils.data import TensorDataset from torchvision import transforms +from leakpro.tests.constants import STORAGE_PATH, get_image_handler_config from leakpro.utils.import_helper import Self -from leakpro.tests.constants import STORAGE_PATH, get_image_handler_config class ConvNet(Module): """Convolutional Neural Network model.""" @@ -128,10 +128,10 @@ def create_mock_image_dataset() -> str: def create_mock_model_and_metadata() -> str: """Creates a mock model and saves it to a file.""" parameters = get_image_handler_config() - + if not os.path.exists(parameters.target_folder): os.makedirs(parameters.target_folder) - + # Create a mock model model = ConvNet() model_path = parameters.target_folder + "/target_model.pkl" diff --git a/leakpro/tests/input_handler/tabular_input_handler.py b/leakpro/tests/input_handler/tabular_input_handler.py index 7caaa288..3853a925 100644 --- a/leakpro/tests/input_handler/tabular_input_handler.py +++ b/leakpro/tests/input_handler/tabular_input_handler.py @@ -9,6 +9,7 @@ from leakpro import AbstractInputHandler from leakpro.input_handler.abstract_input_handler import AbstractInputHandler + class TabularInputHandler(AbstractInputHandler): """Class to handle the user input for the CIFAR10 dataset.""" @@ -42,11 +43,11 @@ def train( criterion = self.get_criterion() optimizer = self.get_optimizer(model) - + for e in tqdm(range(epochs), desc="Training Progress"): model.train() train_acc, train_loss = 0.0, 0.0 - + for data, target in dataloader: target = target.float().unsqueeze(1) data, target = data.to(dev, non_blocking=True), target.to(dev, non_blocking=True) @@ -56,11 +57,11 @@ def train( loss = criterion(output, target) pred = sigmoid(output) >= 0.5 train_acc += pred.eq(target).sum().item() - + loss.backward() optimizer.step() train_loss += loss.item() - + train_acc = train_acc/len(dataloader.dataset) train_loss = train_loss/len(dataloader) diff --git a/leakpro/tests/input_handler/tabular_utils.py b/leakpro/tests/input_handler/tabular_utils.py index 862ae8ba..f820f4c7 100644 --- a/leakpro/tests/input_handler/tabular_utils.py +++ b/leakpro/tests/input_handler/tabular_utils.py @@ -1,19 +1,17 @@ import os import pickle +import random import numpy as np -import torch.nn.functional as F # noqa: N812 -import pandas as pd -import random from dotmap import DotMap from sklearn.preprocessing import OneHotEncoder - -from torch import from_numpy, tensor, save -from torch.nn import Module, Linear, ReLU +from torch import from_numpy, save, tensor +from torch.nn import Linear, Module, ReLU from torch.utils.data import TensorDataset from leakpro.tests.constants import STORAGE_PATH, get_tabular_handler_config + class MLP(Module): def __init__(self, input_size, hidden_size, num_classes): super(MLP, self).__init__() @@ -23,7 +21,7 @@ def __init__(self, input_size, hidden_size, num_classes): self.fc1 = Linear(input_size, hidden_size) self.relu = ReLU() self.fc2 = Linear(hidden_size, num_classes) - + def forward(self, x): out = self.fc1(x) out = self.relu(out) @@ -36,15 +34,15 @@ class DatasetWithSubset(TensorDataset): def __init__(self, x:tensor, y:tensor, dec_to_onehot:dict, one_hot_encoded:bool=True): self.x = x self.y = y - + # create dictionary to map categorical columns to number of classes self.dec_to_onehot = dec_to_onehot self.one_hot_encoded = one_hot_encoded - + def subset(self, indices): - return DatasetWithSubset(self.x[indices], - self.y[indices], - self.dec_to_onehot, + return DatasetWithSubset(self.x[indices], + self.y[indices], + self.dec_to_onehot, self.one_hot_encoded) def __len__(self): return len(self.y) @@ -88,7 +86,7 @@ def setup_tabular_test()->None: def create_mock_tabular_dataset() -> str: """Creates a mock tabular dataset with random images.""" parameters = get_tabular_handler_config() - + # Constants to create a mock tabular dataset n_points = parameters.data_points n_continuous = parameters.n_continuous @@ -97,31 +95,31 @@ def create_mock_tabular_dataset() -> str: dataset_name = "tabular_handler_dataset.pkl" continuous_data = np.random.randn(n_points, n_continuous) - + categorical_data = [] for _ in range(n_categorical): classes = np.random.randint(2, 10) categorical_data.append([random.choice(range(classes)) for _ in range(n_points)]) categorical_data = np.array(categorical_data).T # Transpose to align rows with n_points - + one_hot_encoder = OneHotEncoder(sparse_output=False) categorical_one_hot = one_hot_encoder.fit_transform(categorical_data) combined_data = np.hstack([continuous_data, categorical_one_hot]) - + dec_to_onehot = {} for i in range(n_continuous): dec_to_onehot[i] = [i] # Continuous features are identity-mapped - + n_cols = n_continuous for i in range(n_continuous, n_continuous + n_categorical): dec_to_onehot[i] = list(one_hot_encoder.categories_[i - n_continuous] + n_cols) n_cols += len(dec_to_onehot[i]) one_hot_encoded = True - + data = from_numpy(combined_data).float() label = from_numpy(np.random.randint(0, num_classes, n_points)).float() - + dataset = DatasetWithSubset(data, label, dec_to_onehot, one_hot_encoded) # Save the dataset to a .pkg file @@ -134,10 +132,10 @@ def create_mock_tabular_dataset() -> str: def create_mock_model_and_metadata(input_size:int) -> str: """Creates a mock model and saves it to a file.""" parameters = get_tabular_handler_config() - + if not os.path.exists(parameters.target_folder): os.makedirs(parameters.target_folder) - + # Create a mock model model = MLP(input_size=input_size, hidden_size=64, num_classes=parameters.num_classes) model_path = parameters.target_folder + "/target_model.pkl" diff --git a/leakpro/tests/input_handler/test_tabular_handler.py b/leakpro/tests/input_handler/test_tabular_handler.py index 5b15b213..29cb0f25 100644 --- a/leakpro/tests/input_handler/test_tabular_handler.py +++ b/leakpro/tests/input_handler/test_tabular_handler.py @@ -47,24 +47,24 @@ def test_abstract_handler_setup_tabular(tabular_handler:TabularInputHandler) -> assert len(labels) == parameters.data_points assert np.all(labels <= parameters.num_classes) assert np.all(labels >= 0) - + def test_tabular_extension_class(tabular_handler:TabularInputHandler) -> None: """Test the extension methods of the tabular handler.""" data, _ = next(iter(tabular_handler.get_dataloader(np.arange(10)))) - + assert data is not None - + if not tabular_handler.one_hot_encoded: data = tabular_handler.one_hot_encode(data) - + data2 = tabular_handler.one_hot_to_categorical(data) assert data2 is not None data3 = tabular_handler.one_hot_encode(data2) assert data3 is not None - + assert equal(data, data3) assert data2.shape[1] <= data.shape[1] - + def test_tabular_input_handler(tabular_handler:TabularInputHandler) -> None: """Test the CIFAR10 input handler.""" @@ -84,7 +84,7 @@ def test_tabular_input_handler(tabular_handler:TabularInputHandler) -> None: tabular_handler.get_criterion(), tabular_handler.get_optimizer(tabular_handler.target_model), parameters.epochs) - # move back to cpu + # move back to cpu after_weights = train_dict["model"].to("cpu").state_dict() weights_changed = [equal(before_weights[key], after_weights[key]) for key in before_weights] assert any(weights_changed) is False diff --git a/leakpro/tests/mia_attacks/attacks/test_lira.py b/leakpro/tests/mia_attacks/attacks/test_lira.py index 641eed0d..a66669e0 100644 --- a/leakpro/tests/mia_attacks/attacks/test_lira.py +++ b/leakpro/tests/mia_attacks/attacks/test_lira.py @@ -1,33 +1,34 @@ -from pytest import raises from math import isnan +from pytest import raises -from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler from leakpro.attacks.mia_attacks.lira import AttackLiRA +from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler +from leakpro.tests.constants import get_audit_config, get_shadow_model_config from leakpro.tests.input_handler.image_input_handler import ImageInputHandler -from leakpro.tests.constants import get_shadow_model_config, get_audit_config + def test_lira_setup(image_handler:ImageInputHandler) -> None: """Test the initialization of LiRA.""" audit_config = get_audit_config() lira_params = audit_config.attack_list.lira lira_obj = AttackLiRA(image_handler, lira_params) - + assert lira_obj is not None assert lira_obj.target_model is not None assert lira_obj.online == lira_params.online assert lira_obj.num_shadow_models == lira_params.num_shadow_models assert lira_obj.training_data_fraction == lira_params.training_data_fraction assert lira_obj.memorization == False - + lira_params.num_shadow_models = -1 with raises(ValueError) as excinfo: lira_obj = AttackLiRA(image_handler, lira_params) assert str(excinfo.value) == "num_shadow_models must be between 1 and None" - + lira_params.num_shadow_models = 3 - + description = lira_obj.description() assert len(description) == 4 @@ -35,48 +36,48 @@ def test_lira_prepare_online_attack(image_handler:ImageInputHandler) -> None: audit_config = get_audit_config() lira_params = audit_config.attack_list.lira lira_params.online = True - + image_handler.configs.shadow_model = get_shadow_model_config() lira_obj = AttackLiRA(image_handler, lira_params) - + if ShadowModelHandler.is_created() == False: ShadowModelHandler(image_handler) - + lira_obj.prepare_attack() - + # ensure correct number of shadow models are read assert len(lira_obj.shadow_models) == lira_params.num_shadow_models # ensure the attack data indices correspond to the correct pool assert sorted(lira_obj.attack_data_indices) == list(range(image_handler.population_size)) # memorization is tested in a different module assert lira_obj.memorization == False - + # Check that the filtering of the attack data is correct (this is done after shadow models are created) n_attack_points = len(lira_obj.in_members) + len(lira_obj.out_members) assert n_attack_points > 0 assert lira_obj.shadow_models_logits.shape == (n_attack_points, lira_params.num_shadow_models) assert lira_obj.target_logits.shape == (n_attack_points, ) - + def test_lira_prepare_offline_attack(image_handler:ImageInputHandler) -> None: audit_config = get_audit_config() lira_params = audit_config.attack_list.lira lira_params.online = False - + image_handler.configs.shadow_model = get_shadow_model_config() lira_obj = AttackLiRA(image_handler, lira_params) - + if ShadowModelHandler.is_created() == False: ShadowModelHandler(image_handler) - + lira_obj.prepare_attack() - + # ensure correct number of shadow models are read assert len(lira_obj.shadow_models) == lira_params.num_shadow_models # ensure the attack data indices correspond to the correct pool assert sorted(lira_obj.attack_data_indices) == sorted(set(range(image_handler.population_size)) - set(image_handler.test_indices) - set(image_handler.train_indices)) # memorization is tested in a different module assert lira_obj.memorization == False - + # Check that the filtering of the attack data is correct (this is done after shadow models are created) n_attack_points = len(lira_obj.in_members) + len(lira_obj.out_members) assert n_attack_points > 0 @@ -94,7 +95,7 @@ def test_lira_online_attack(image_handler:ImageInputHandler): if ShadowModelHandler.is_created() == False: ShadowModelHandler(image_handler) lira_obj.prepare_attack() - + # Test standard deviation calculation std_fixed = lira_obj.get_std(lira_obj.shadow_models_logits.flatten(), lira_obj.in_indices_masks.flatten(), @@ -107,14 +108,14 @@ def test_lira_online_attack(image_handler:ImageInputHandler): lira_obj.in_indices_masks.flatten(), True, "carlini") - + std_individual = lira_obj.get_std(lira_obj.shadow_models_logits.flatten(), lira_obj.in_indices_masks.flatten(), True, "individual_carlini") assert std_fixed == std_carlini assert std_fixed == std_individual - + # Test attack lira_obj.run_attack() assert lira_obj.fixed_in_std != lira_obj.fixed_out_std @@ -122,7 +123,7 @@ def test_lira_online_attack(image_handler:ImageInputHandler): assert len(lira_obj.in_member_signals)+len(lira_obj.out_member_signals) == n_attack_points assert any(isnan(x) for x in lira_obj.in_member_signals) == False assert any(isnan(x) for x in lira_obj.out_member_signals) == False - + def test_lira_online_attack(image_handler:ImageInputHandler): # Set up for testing audit_config = get_audit_config() @@ -134,11 +135,11 @@ def test_lira_online_attack(image_handler:ImageInputHandler): ShadowModelHandler(image_handler) lira_obj.prepare_attack() lira_obj.fix_var_threshold = 0.0 - + # Test attack lira_obj.run_attack() assert lira_obj.fixed_in_std != lira_obj.fixed_out_std n_attack_points = len(lira_obj.in_members) + len(lira_obj.out_members) assert len(lira_obj.in_member_signals)+len(lira_obj.out_member_signals) == n_attack_points assert any(isnan(x) for x in lira_obj.in_member_signals) == False - assert any(isnan(x) for x in lira_obj.out_member_signals) == False \ No newline at end of file + assert any(isnan(x) for x in lira_obj.out_member_signals) == False diff --git a/leakpro/tests/mia_attacks/utils/test_shadow_model_handler.py b/leakpro/tests/mia_attacks/utils/test_shadow_model_handler.py index 7f767689..07d4a528 100644 --- a/leakpro/tests/mia_attacks/utils/test_shadow_model_handler.py +++ b/leakpro/tests/mia_attacks/utils/test_shadow_model_handler.py @@ -1,20 +1,22 @@ """Test the shadow model handler module.""" import os -import numpy as np +import numpy as np from pytest import raises + from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler -from leakpro.tests.input_handler.image_input_handler import ImageInputHandler from leakpro.tests.constants import get_shadow_model_config +from leakpro.tests.input_handler.image_input_handler import ImageInputHandler + def test_shadow_model_handler_singleton(image_handler:ImageInputHandler) -> None: """Test that only one instance gets created.""" - + image_handler.configs.shadow_model = get_shadow_model_config() if ShadowModelHandler.is_created() == False: sm = ShadowModelHandler(image_handler) assert ShadowModelHandler.is_created() == True - + with raises(ValueError) as excinfo: ShadowModelHandler(image_handler) assert str(excinfo.value) == "Singleton already created with specific parameters." @@ -26,12 +28,12 @@ def test_shadow_model_handler_creation_from_target(image_handler:ImageInputHandl if ShadowModelHandler.is_created() == True: ShadowModelHandler.delete_instance() sm = ShadowModelHandler(image_handler) - + assert sm.batch_size == image_handler.target_model_metadata["batch_size"] assert sm.epochs == image_handler.target_model_metadata["epochs"] assert sm.init_params == image_handler.target_model_metadata["init_params"] assert sm.model_blueprint == image_handler.target_model.__class__ - + image_handler.target_model_metadata["optimizer"].pop("name") assert sm.optimizer_config == image_handler.target_model_metadata["optimizer"] image_handler.target_model_metadata["loss"].pop("name") @@ -39,34 +41,34 @@ def test_shadow_model_handler_creation_from_target(image_handler:ImageInputHandl def test_shadow_model_creation_and_loading(image_handler:ImageInputHandler) -> None: image_handler.configs.shadow_model = get_shadow_model_config() - + # Test initialization if ShadowModelHandler.is_created() == True: ShadowModelHandler.delete_instance() sm = ShadowModelHandler(image_handler) - + assert sm.batch_size == image_handler.configs.shadow_model.batch_size assert sm.epochs == image_handler.configs.shadow_model.epochs assert sm.init_params == {} assert sm.model_blueprint is not None assert sm.optimizer_config is not None assert sm.loss_config is not None - + # Test creation n_models = 1 training_fraction = 0.5 online = False - + entries_start = os.listdir(sm.storage_path) n_entries_start = len(entries_start) - + indx = sm.create_shadow_models(n_models, image_handler.test_indices, training_fraction, online)[0] entries = os.listdir(sm.storage_path) n_entries_phase1 = len(entries) assert n_entries_phase1 - n_entries_start == 2*n_models assert f"metadata_{indx}.pkl" in entries assert f"shadow_model_{indx}.pkl" in entries - + indx2 = sm.create_shadow_models(n_models, image_handler.test_indices, training_fraction, ~online)[0] entries = os.listdir(sm.storage_path) @@ -75,25 +77,25 @@ def test_shadow_model_creation_and_loading(image_handler:ImageInputHandler) -> N assert f"shadow_model_{indx}.pkl" in entries assert f"metadata_{indx2}.pkl" in entries assert f"shadow_model_{indx2}.pkl" in entries - + # Test loading meta_0 = sm._load_metadata(sm.storage_path + f"/metadata_{indx}.pkl") meta_1 = sm._load_metadata(sm.storage_path + f"/metadata_{indx2}.pkl") assert meta_0["online"] == online assert meta_1["online"] == ~online assert meta_1["num_train"] == meta_0["num_train"] - + shadow_model_indices = [indx, indx2] models, indices = sm.get_shadow_models(shadow_model_indices) for model in models: assert model.model_obj.__class__.__name__ == "ConvNet" - + # Test index mask # check what test data is included in the training data of the shadow models mask = sm.get_in_indices_mask(shadow_model_indices, np.array(image_handler.test_indices)) - + true_mask_0 = np.array([True if item in meta_0["train_indices"] else False for item in image_handler.test_indices]) assert np.array_equal(mask[:, 0], true_mask_0) - + true_mask_1 = np.array([True if item in meta_1["train_indices"] else False for item in image_handler.test_indices]) assert np.array_equal(mask[:, 1], true_mask_1) diff --git a/tests/test_attack_result.py/__init__.py b/leakpro/tests/test_attack_result/__init__.py similarity index 100% rename from tests/test_attack_result.py/__init__.py rename to leakpro/tests/test_attack_result/__init__.py diff --git a/tests/test_attack_result.py/test_attack_result.py b/leakpro/tests/test_attack_result/test_attack_result.py similarity index 100% rename from tests/test_attack_result.py/test_attack_result.py rename to leakpro/tests/test_attack_result/test_attack_result.py diff --git a/tests/test_report_handler/__init__.py b/leakpro/tests/test_report_handler/__init__.py similarity index 100% rename from tests/test_report_handler/__init__.py rename to leakpro/tests/test_report_handler/__init__.py diff --git a/tests/test_report_handler/test_report_handler.py b/leakpro/tests/test_report_handler/test_report_handler.py similarity index 100% rename from tests/test_report_handler/test_report_handler.py rename to leakpro/tests/test_report_handler/test_report_handler.py From 133220882565abc585cd7456cade50c51ab537c1 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Mon, 28 Oct 2024 12:24:50 +0000 Subject: [PATCH 04/14] updated raport handler --- leakpro/metrics/attack_result.py | 11 ++--- leakpro/reporting/report_handler.py | 63 ++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index dfa8ad90..3ae62af1 100644 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -303,6 +303,7 @@ def save(self:Self, path: str, name: str, config:dict = None): threshold = self.threshold ) + @classmethod def get_strongest(self, results) -> list: """Method for selecting the strongest attack.""" return max((res for res in results), key=lambda d: d.roc_auc) @@ -377,6 +378,7 @@ def create_plot(self, results, filename = "", save_name = "") -> None: plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches="tight") plt.clf() + @classmethod def create_results( self: Self, results: list, @@ -496,6 +498,7 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: pass + @classmethod def create_result(self: Self, attack_name: str, save_path: str) -> None: """Result method for GIA.""" @@ -617,13 +620,7 @@ def save(self:Self, path: str, name: str, config:dict = None): with open(f"{path}/{name}/{self.id}/data.json", "w") as f: json.dump(data, f) - def create_result(self, results): - """Method for results.""" - def _latex(results): - """Latex method for TEMPLATEResult""" - pass - pass - + @classmethod def create_result(self, results): """Method for results.""" def _latex(results): diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index 340d57f3..5744fd75 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -80,13 +80,22 @@ def create_results_all(self) -> None: results = [res for res in self.results if res.resulttype == result_type] # If no results of type "result_type" is found, skip to next result_type - if len(results) == 0: + if not results: self.logger.info(f"No results of type {result_type} found.") continue - # Create all results - merged_result = results[0].create_results(results=results, save_dir=self.report_dir, save_name="all_results") - self.pdf_results[result_type].append(merged_result) + # Check if the result type has a 'create_results' method + try: + result_class = globals().get(result_type) + except: + self.logger.info(f"No {result_type} class could be found or exists") + continue + + if hasattr(result_class, 'create_results') and callable(getattr(result_class, 'create_results')): + + # Create all results + merged_result = result_class.create_results(results=results, save_dir=self.report_dir, save_name="all_results") + self.pdf_results[result_type].append(merged_result) except Exception as e: print("all", e) @@ -98,19 +107,26 @@ def create_results_strong(self): results = [res for res in self.results if res.resulttype == result_type] # If no results of type "result_type" is found, skip to next result_type - if len(results) == 0: + if not results: self.logger.info(f"No 'strong' results of type {result_type} found.") continue + try: + result_class = globals().get(result_type) + except: + self.logger.info(f"No {result_type} class could be found or exists") + continue + # Get all attack names attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] # Get the strongest result for each attack name - strongest_results = [result[0].get_strongest(result) for result in attack_name_grouped_results] + if hasattr(result_class, 'get_strongest') and callable(getattr(result_class, 'get_strongest')): + strongest_results = [result_class.get_strongest(result) for result in attack_name_grouped_results] - # Create the strongest results - merged_result = results[0].create_results(results=strongest_results, save_dir=self.report_dir, save_name="strong_results") - self.pdf_results[result_type].append(merged_result) + # Create the strongest results + merged_result = result_class.create_results(results=strongest_results, save_dir=self.report_dir, save_name="strong_results") + self.pdf_results[result_type].append(merged_result) except Exception as e: print("results_strong", e) @@ -125,22 +141,29 @@ def create_results_attackname_grouped(self): results = [res for res in self.results if res.resulttype == result_type] # If no results of type "result_type" is found, skip to next result_type - if len(results) == 0: - self.logger.info(f"No results of type {result_type} found.") + if not results: + self.logger.info(f"No results of type {result_type} to group.") + continue + + # Check if the result type has a 'create_results' method + try: + result_class = globals().get(result_type) + except: + self.logger.info(f"No {result_type} class could be found or exists") continue for name in all_attack_names: + if hasattr(result_class, 'create_results') and callable(getattr(result_class, 'create_results')): + try: + # Get result for each attack names + attack_results = self._get_results_of_name(results, name) - try: - # Get result for each attack names - attack_results = self._get_results_of_name(results, name) - - # Create results - merged_result = attack_results[0].create_results(results=attack_results, save_dir=self.report_dir, save_name="grouped_"+name) - self.pdf_results[result_type].append(merged_result) + # Create results + merged_result = result_class.create_results(results=attack_results, save_dir=self.report_dir, save_name="grouped_"+name) + self.pdf_results[result_type].append(merged_result) - except Exception as e: - print("create_results_attackname_grouped", e) + except Exception as e: + print("create_results_attackname_grouped", e) def create_report(self): """Method to create PDF report""" From c416dbbb58bf961cd300924fcab628c7e8cb4873 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Tue, 29 Oct 2024 13:47:09 +0000 Subject: [PATCH 05/14] update report_handler and tests --- leakpro/reporting/report_handler.py | 40 +++++----- leakpro/tests/test_attack_result/__init__.py | 2 +- .../test_attack_result/test_attack_result.py | 79 ++++++++++--------- leakpro/tests/test_report_handler/__init__.py | 2 +- .../test_report_handler.py | 45 ++++++----- 5 files changed, 87 insertions(+), 81 deletions(-) diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index 5744fd75..ae6fcc67 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -3,12 +3,14 @@ import os import subprocess +from leakpro.utils.import_helper import Self + # Report Handler class ReportHandler(): """Implementation of the report handler.""" - def __init__(self, report_dir: str, logger:logging.Logger) -> None: + def __init__(self:Self, report_dir: str, logger:logging.Logger) -> None: self.logger = logger self.report_dir = report_dir self.pdf_results = {} @@ -21,13 +23,13 @@ def __init__(self, report_dir: str, logger:logging.Logger) -> None: for key in self.leakpro_types: self.pdf_results[key] = [] - def save_results(self, attack_name: str, result_data: dict, config: dict) -> None: + def save_results(self:Self, attack_name: str, result_data: dict, config: dict) -> None: """Save attack results.""" self.logger.info(f"Saving results for {attack_name}") result_data.save(self.report_dir, attack_name, config) - def load_results(self): + def load_results(self:Self): self.results = [] for parentdir in os.scandir(f"{self.report_dir}"): if parentdir.is_dir(): @@ -62,18 +64,18 @@ def load_results(self): except Exception as e: self.logger.info(f"Not able to load data, Error: {e}") - def _get_results_of_name(self, results, resultname_value) -> list: + def _get_results_of_name(self:Self, results, resultname_value) -> list: indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] return [results[idx] for idx in indices] - def _get_all_attacknames(self): + def _get_all_attacknames(self:Self): attack_name_list = [] for result in self.results: if result.resultname not in attack_name_list: attack_name_list.append(result.resultname) return attack_name_list - def create_results_all(self) -> None: + def create_results_all(self:Self) -> None: for result_type in self.leakpro_types: try: # Get all results of type "Result" @@ -87,11 +89,11 @@ def create_results_all(self) -> None: # Check if the result type has a 'create_results' method try: result_class = globals().get(result_type) - except: + except: self.logger.info(f"No {result_type} class could be found or exists") continue - - if hasattr(result_class, 'create_results') and callable(getattr(result_class, 'create_results')): + + if hasattr(result_class, "create_results") and callable(result_class.create_results): # Create all results merged_result = result_class.create_results(results=results, save_dir=self.report_dir, save_name="all_results") @@ -100,7 +102,7 @@ def create_results_all(self) -> None: except Exception as e: print("all", e) - def create_results_strong(self): + def create_results_strong(self:Self): for result_type in self.leakpro_types: try: # Get all results of type "Result" @@ -113,7 +115,7 @@ def create_results_strong(self): try: result_class = globals().get(result_type) - except: + except: self.logger.info(f"No {result_type} class could be found or exists") continue @@ -121,7 +123,7 @@ def create_results_strong(self): attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] # Get the strongest result for each attack name - if hasattr(result_class, 'get_strongest') and callable(getattr(result_class, 'get_strongest')): + if hasattr(result_class, "get_strongest") and callable(result_class.get_strongest): strongest_results = [result_class.get_strongest(result) for result in attack_name_grouped_results] # Create the strongest results @@ -131,7 +133,7 @@ def create_results_strong(self): except Exception as e: print("results_strong", e) - def create_results_attackname_grouped(self): + def create_results_attackname_grouped(self:Self): # Get all attack names all_attack_names = self._get_all_attacknames() @@ -144,16 +146,16 @@ def create_results_attackname_grouped(self): if not results: self.logger.info(f"No results of type {result_type} to group.") continue - + # Check if the result type has a 'create_results' method try: result_class = globals().get(result_type) - except: + except: self.logger.info(f"No {result_type} class could be found or exists") continue for name in all_attack_names: - if hasattr(result_class, 'create_results') and callable(getattr(result_class, 'create_results')): + if hasattr(result_class, "create_results") and callable(result_class.create_results): try: # Get result for each attack names attack_results = self._get_results_of_name(results, name) @@ -165,7 +167,7 @@ def create_results_attackname_grouped(self): except Exception as e: print("create_results_attackname_grouped", e) - def create_report(self): + def create_report(self:Self): """Method to create PDF report""" # Create initial part of the document. @@ -181,7 +183,7 @@ def create_report(self): # Compile the PDF self._compile_pdf() - def _init_pdf(self,): + def _init_pdf(self:Self): self.latex_content = """ \\documentclass{article} \\usepackage{tabularx} @@ -190,7 +192,7 @@ def _init_pdf(self,): \\begin{document} """ - def _compile_pdf(self, install_flag: bool = False): + def _compile_pdf(self:Self, install_flag: bool = False): """Method to compile PDF.""" self.latex_content += """ diff --git a/leakpro/tests/test_attack_result/__init__.py b/leakpro/tests/test_attack_result/__init__.py index c4bfdc70..bba53df7 100644 --- a/leakpro/tests/test_attack_result/__init__.py +++ b/leakpro/tests/test_attack_result/__init__.py @@ -1 +1 @@ -"""Init file for attack result tests""" \ No newline at end of file +"""Init file for attack result tests.""" diff --git a/leakpro/tests/test_attack_result/test_attack_result.py b/leakpro/tests/test_attack_result/test_attack_result.py index dab9ce27..56d3f2ae 100644 --- a/leakpro/tests/test_attack_result/test_attack_result.py +++ b/leakpro/tests/test_attack_result/test_attack_result.py @@ -1,15 +1,16 @@ -import unittest -import os import json -import logging -import subprocess +import os import tempfile -from unittest.mock import MagicMock, patch, mock_open, call +import unittest +from unittest.mock import MagicMock + from leakpro.metrics.attack_result import * +from leakpro.utils.import_helper import Self + class TestMIAResult(unittest.TestCase): - def setUp(self) -> None: + def setUp(self:Self) -> None: """Set up temporary directory and logger for MIAResult.""" self.temp_dir = tempfile.TemporaryDirectory() @@ -30,7 +31,7 @@ def setUp(self) -> None: [ 0.46973035], [-0.1584589 ], [ 0.14289466]]) - + predictions_proba = None threshold = None @@ -39,53 +40,53 @@ def setUp(self) -> None: id = None self.miaresult = MIAResult(predicted_labels = predicted_labels, - true_labels = true_labels, + true_labels = true_labels, signal_values = signal_values, predictions_proba = predictions_proba, threshold = threshold, audit_indices = audit_indices, resultname = resultname, id = id) - - - - self.config = {'random_seed': 1234, 'attack_list': - {'lira': - {'training_data_fraction': 0.5, - 'num_shadow_models': 3, - 'online': True} + + + + self.config = {"random_seed": 1234, "attack_list": + {"lira": + {"training_data_fraction": 0.5, + "num_shadow_models": 3, + "online": True} }, - 'report_log': - './leakpro_output/results', - 'config_log': - './leakpro_output/config', - 'target_model_folder': - './target', - 'attack_folder': - 'attack_objects', - 'attack_type': - 'mia', - 'split_method': - 'no_overlapping' + "report_log": + "./leakpro_output/results", + "config_log": + "./leakpro_output/config", + "target_model_folder": + "./target", + "attack_folder": + "attack_objects", + "attack_type": + "mia", + "split_method": + "no_overlapping" } - def tearDown(self) -> None: + def tearDown(self:Self) -> None: """Clean up temporary directory.""" self.temp_dir.cleanup() - def test_MIAResult_init(self) -> None: + def test_MIAResult_init(self:Self) -> None: """Test the initialization of MIAResult.""" assert self.miaresult.id == None - def test_check_tpr_fpr(self): + def test_check_tpr_fpr(self:Self) -> None: assert np.allclose(self.miaresult.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) assert self.miaresult.fp.all() == 0. assert self.miaresult.tn.all() == 0. - def test_save_load_MIAResult(self) -> None: - + def test_save_load_MIAResult(self:Self) -> None: + name = "lira" - config_name = get_config_name(self.config['attack_list'][name]) + config_name = get_config_name(self.config["attack_list"][name]) save_path = f"{self.temp_dir}/{name}/{name}{config_name}" # Test saving @@ -99,16 +100,16 @@ def test_save_load_MIAResult(self) -> None: # Test loading with open(f"{save_path}/data.json") as f: data = json.load(f) - + self.miaresult_new = MIAResult(load=True) assert self.miaresult_new.predicted_labels == None assert self.miaresult_new.true_labels == None assert self.miaresult_new.signal_values == None - + self.miaresult_new.load(data) assert np.allclose(self.miaresult_new.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) - def test_get_strongest_MIAResult(self) -> None: + def test_get_strongest_MIAResult(self:Self) -> None: """Test selecting the strongest attack based on ROC AUC.""" result_1 = MagicMock(roc_auc=0.75) result_2 = MagicMock(roc_auc=0.85) @@ -120,7 +121,7 @@ def test_get_strongest_MIAResult(self) -> None: # The strongest attack should be the one with the highest ROC AUC assert strongest == result_2 - def test_latex(self): + def test_latex(self:Self) -> None: """Test if the LaTeX content is generated correctly.""" result = [MagicMock(id="attack-config-1", resultname="test_attack_1", fixed_fpr_table={"TPR@1.0%FPR": 0.90, "TPR@0.1%FPR": 0.80, "TPR@0.01%FPR": 0.70, "TPR@0.0%FPR": 0.60})] @@ -146,4 +147,4 @@ def test_latex(self): self.assertIn("0.6", latex_content) # Ensure the LaTeX content ends properly - self.assertIn("\\newline\n", latex_content) \ No newline at end of file + self.assertIn("\\newline\n", latex_content) diff --git a/leakpro/tests/test_report_handler/__init__.py b/leakpro/tests/test_report_handler/__init__.py index 796d15a0..7018163f 100644 --- a/leakpro/tests/test_report_handler/__init__.py +++ b/leakpro/tests/test_report_handler/__init__.py @@ -1 +1 @@ -"""Init file for report handler tests""" \ No newline at end of file +"""Init file for report handler tests.""" diff --git a/leakpro/tests/test_report_handler/test_report_handler.py b/leakpro/tests/test_report_handler/test_report_handler.py index 9ed6b952..c0775fca 100644 --- a/leakpro/tests/test_report_handler/test_report_handler.py +++ b/leakpro/tests/test_report_handler/test_report_handler.py @@ -1,27 +1,28 @@ -import unittest -import os -import json import logging -import subprocess +import os import tempfile -from unittest.mock import MagicMock, patch, mock_open, call -from leakpro.reporting.report_handler import ReportHandler +from unittest.mock import MagicMock + from leakpro.metrics.attack_result import * +from leakpro.reporting.report_handler import ReportHandler +from leakpro.utils.import_helper import Self + -class TestReportHandler(unittest.TestCase): +class TestReportHandler(): + """Test class of the ReportHandler.""" - def setUp(self) -> None: + def setUp(self:Self) -> None: """Set up temporary directory and logger for ReportHandler.""" self.temp_dir = tempfile.TemporaryDirectory() - self.logger = logging.getLogger('test_logger') + self.logger = logging.getLogger("test_logger") self.logger.setLevel(logging.INFO) self.report_handler = ReportHandler(report_dir=self.temp_dir.name, logger=self.logger) - def tearDown(self) -> None: + def tearDown(self:Self) -> None: """Clean up temporary directory.""" self.temp_dir.cleanup() - def test_report_handler_initialization(self) -> None: + def test_report_handler_initialization(self:Self) -> None: """Test the initialization of ReportHandler.""" assert self.report_handler is not None assert self.report_handler.report_dir == self.temp_dir.name @@ -29,27 +30,30 @@ def test_report_handler_initialization(self) -> None: types = ["MIAResult", "GIAResults", "SyntheticResult"] assert False not in [_type in types for _type in self.report_handler.leakpro_types] - assert True not in [True if self.report_handler.pdf_results[key] else False for key in self.report_handler.leakpro_types] + assert True not in [bool(self.report_handler.pdf_results[key]) for key in self.report_handler.leakpro_types] assert False not in [_type in globals() for _type in types] - def test_init_pdf(self) -> None: - assert hasattr(self.report_handler, 'latex_content') == False + def test_init_pdf(self:Self) -> None: + """Test the initialization method of the ReportHandler.""" + + if hasattr(self.report_handler, "latex_content"): + raise AssertionError self.report_handler._init_pdf() assert "documentclass" in self.report_handler.latex_content assert "begin" in self.report_handler.latex_content - def test_compile_pdf(self) -> None: + def test_compile_pdf(self:Self) -> None: """Test PDF compilation.""" self.report_handler._init_pdf() self.report_handler._compile_pdf(install_flag=True) assert "end" in self.report_handler.latex_content - assert os.path.isfile(f'{self.report_handler.report_dir}/LeakPro_output.tex') - assert os.path.isfile(f'./LeakPro_output.pdf') + assert os.path.isfile(f"{self.report_handler.report_dir}/LeakPro_output.tex") + assert os.path.isfile("./LeakPro_output.pdf") - def test_get_all_attacknames(self) -> None: + def test_get_all_attacknames(self:Self) -> None: """Test retrieval of all attack names.""" result_mock_1 = MagicMock(resultname="Attack1") result_mock_2 = MagicMock(resultname="Attack2") @@ -59,8 +63,7 @@ def test_get_all_attacknames(self) -> None: assert attack_names == ["Attack1", "Attack2"] - def test_get_results_of_name(self): - + def test_get_results_of_name(self:Self) -> None: """Test retrieval of all attack names.""" result_mock_1 = MagicMock(resultname="Attack1") result_mock_2 = MagicMock(resultname="Attack2") @@ -74,4 +77,4 @@ def test_get_results_of_name(self): assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack1")) == 1 assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack2")) == 2 - assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack3")) == 3 \ No newline at end of file + assert len(self.report_handler._get_results_of_name(self.report_handler.results, "Attack3")) == 3 From 7df0ddcb8eabadea0944bab8906e221c8f1125ac Mon Sep 17 00:00:00 2001 From: henrikfo Date: Tue, 5 Nov 2024 11:34:50 +0000 Subject: [PATCH 06/14] check fixed --- leakpro/attacks/mia_attacks/lira.py | 16 +- leakpro/metrics/attack_result.py | 149 +++++++++--------- leakpro/reporting/report_handler.py | 93 ++++++----- .../test_attack_result/test_attack_result.py | 46 +++--- .../test_report_handler.py | 3 +- pyproject.toml | 2 + 6 files changed, 169 insertions(+), 140 deletions(-) diff --git a/leakpro/attacks/mia_attacks/lira.py b/leakpro/attacks/mia_attacks/lira.py index de1edd7c..c366302e 100755 --- a/leakpro/attacks/mia_attacks/lira.py +++ b/leakpro/attacks/mia_attacks/lira.py @@ -7,7 +7,7 @@ from leakpro.attacks.mia_attacks.abstract_mia import AbstractMIA from leakpro.attacks.utils.boosting import Memorization from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler -from leakpro.import_helper import Self +from leakpro.input_handler.abstract_input_handler import AbstractInputHandler from leakpro.metrics.attack_result import CombinedMetricResult, MIAResult from leakpro.signals.signal import ModelRescaledLogits from leakpro.utils.import_helper import Self @@ -49,6 +49,7 @@ def _configure_attack(self:Self, configs: dict) -> None: self.training_data_fraction = configs.get("training_data_fraction", 0.5) self.include_train_data = configs.get("include_train_data", self.online) self.include_test_data = configs.get("include_test_data", self.online) + self.eval_batch_size = configs.get("eval_batch_size", 32) # Memorization config # Activate memorization @@ -70,6 +71,7 @@ def _configure_attack(self:Self, configs: dict) -> None: validation_dict = { "num_shadow_models": (self.num_shadow_models, 1, None), "training_data_fraction": (self.training_data_fraction, 0, 1), + "eval_batch_size": (self.eval_batch_size, 1, 1_000_000), "memorization_threshold": (self.memorization_threshold, 0, 1), "min_num_memorization_audit_points": (self.min_num_memorization_audit_points, 1, 1_000_000), "num_memorization_audit_points": (self.num_memorization_audit_points, 0, 1_000_000), @@ -149,7 +151,6 @@ def prepare_attack(self:Self)->None: self.audit_dataset["data"] = self.audit_dataset["data"] self.in_members = self.audit_dataset["in_members"] self.out_members = self.audit_dataset["out_members"] - # mask = [True if indice in self.in_members else False for indice in self.audit_dataset["data"]] # Check offline attack for possible IN- sample(s) if not self.online: @@ -158,15 +159,14 @@ def prepare_attack(self:Self)->None: logger.info(f"Some shadow model(s) contains {count_in_samples} IN samples in total for the model(s)") logger.info("This is not an offline attack!") - self.batch_size = 20000 #int(len(self.audit_dataset["data"])/2) self.logger.info(f"Calculating the logits for all {self.num_shadow_models} shadow models") - self.shadow_models_logits = np.swapaxes(self.signal(self.shadow_models, self.handler, self.audit_dataset["data"],\ - self.batch_size), 0, 1) + self.shadow_models_logits = np.swapaxes(self.signal(self.shadow_models, self.handler, self.audit_dataset["data"], + self.eval_batch_size), 0, 1) # Calculate logits for the target model self.logger.info("Calculating the logits for the target model") - self.target_logits = np.swapaxes(self.signal([self.target_model], self.handler, self.audit_dataset["data"], self.batch_size),\ - 0, 1).squeeze() + self.target_logits = np.swapaxes(self.signal([self.target_model], self.handler, self.audit_dataset["data"], + self.eval_batch_size), 0, 1).squeeze() # Using Memorizationg boosting if self.memorization: @@ -190,7 +190,7 @@ def prepare_attack(self:Self)->None: org_audit_data_length, self.handler, self.online, - self.batch_size, + self.eval_batch_size, ) memorization_mask, _, _ = memorization.run() diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index 3ae62af1..ac1ae0b0 100755 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -132,7 +132,7 @@ def __init__( # noqa: PLR0913 self.roc_auc = auc(self.fpr, self.tpr) - def _get_primitives(self:Self): + def _get_primitives(self:Self) -> dict: """Return the primitives of the CombinedMetricResult class.""" return {"predicted_labels": self.predicted_labels.tolist(), "true_labels": self.true_labels.tolist(), @@ -141,7 +141,7 @@ def _get_primitives(self:Self): "threshold": self.threshold.tolist() if isinstance(self.threshold, np.ndarray) else None, } - def save(self:Self, path: str, name: str, config:dict): + def save(self:Self, path: str, name: str, config:dict) -> None: """Save the CombinedMetricResult class to disk.""" # Primitives are the set of variables to re-create the class from scratch @@ -205,6 +205,11 @@ def __init__( # noqa: PLR0913 predictions_proba: Continuous version of the predicted_labels. signal_values: Values of the signal used by the metric. threshold: Threshold computed by the metric. + audit_indices: The connesponding dataset indices for the results + id: The identity of the attack + load: If the data should be loaded + metadata: Metadata about the results + resultname: The name of the attack and result """ @@ -234,7 +239,9 @@ def __init__( # noqa: PLR0913 self.tpr = self.tp / (self.tp + self.fn) self.roc_auc = auc(self.fpr, self.tpr) - def load(self, data): + def load(self:Self, data: dict) -> None: + """Load the MIAResults to disk.""" + self.resultname = data["resultname"] self.resulttype = data["resulttype"] self.tpr = data["tpr"] @@ -247,11 +254,9 @@ def load(self, data): self.true_labels = data["true_labels"] self.threshold = data["threshold"] - def save(self:Self, path: str, name: str, config:dict = None): + def save(self:Self, path: str, name: str, config:dict = None) -> None: """Save the MIAResults to disk.""" - print(config) - result_config = config["attack_list"][name] fixed_fpr_table = get_result_fixed_fpr(self.fpr, self.tpr) @@ -303,16 +308,16 @@ def save(self:Self, path: str, name: str, config:dict = None): threshold = self.threshold ) - @classmethod - def get_strongest(self, results) -> list: + @staticmethod + def get_strongest(results: list) -> list: """Method for selecting the strongest attack.""" return max((res for res in results), key=lambda d: d.roc_auc) - def create_signal_histogram(self, filename, signal_values, true_labels, threshold) -> None: + def create_signal_histogram(self:Self, filename: str, signal_values: list, true_labels: list, threshold: float) -> None: + """Method to create Signal Histogram.""" values = np.array(signal_values).ravel() labels = np.array(true_labels).ravel() - threshold = threshold data = pd.DataFrame( { @@ -350,7 +355,11 @@ def create_signal_histogram(self, filename, signal_values, true_labels, threshol plt.savefig(fname=filename, dpi=1000) plt.clf() - def create_plot(self, results, filename = "", save_name = "") -> None: + @staticmethod + def create_plot(results: list, save_dir: str = "", save_name: str = "") -> None: + """Plot method for MIAResult.""" + + filename = f"{save_dir}/{save_name}" # Create plot for results reduced_labels = reduce_to_unique_labels(results) @@ -378,26 +387,27 @@ def create_plot(self, results, filename = "", save_name = "") -> None: plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches="tight") plt.clf() - @classmethod + @staticmethod def create_results( - self: Self, results: list, save_dir: str = "./", save_name: str = "foo", - ): + ) -> str: + """Result method for MIAResult.""" - filename = f"{save_dir}/{save_name}" - - self.create_plot(results, filename, save_name) + MIAResult.create_plot(results, save_dir, save_name) - return self._latex(results, save_name, filename) + return MIAResult._latex(results, save_dir, save_name) - def _latex(self, results, subsection, filename): + @staticmethod + def _latex(results: list, save_dir: str, save_name: str) -> str: """Latex method for MIAResult.""" + filename = f"{save_dir}/{save_name}" + latex_content = "" latex_content += f""" - \\subsection{{{" ".join(subsection.split("_"))}}} + \\subsection{{{" ".join(save_name.split("_"))}}} \\begin{{figure}}[ht] \\includegraphics[width=0.8\\textwidth]{{{filename}.png}} \\end{{figure}} @@ -407,18 +417,19 @@ def _latex(self, results, subsection, filename): \\resizebox{\\linewidth}{!}{% \\begin{tabularx}{\\textwidth}{l c l l l l} Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR \\\\ - \\hline + \\hline """ - def config_latex_style(config): + def config_latex_style(config: str) -> str: config = " \\\\ ".join(config.split("-")[1:]) config = "-".join(config.split("_")) return f"""\\shortstack{{{config}}}""" for res in results: config = config_latex_style(res.id) - latex_content += f"""{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & {res.fixed_fpr_table["TPR@0.1%FPR"]} & {res.fixed_fpr_table["TPR@0.01%FPR"]} & {res.fixed_fpr_table["TPR@0.0%FPR"]} \\\\ \\hline - """ + latex_content += f"""{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & + {res.fixed_fpr_table["TPR@0.1%FPR"]} & {res.fixed_fpr_table["TPR@0.01%FPR"]} & + {res.fixed_fpr_table["TPR@0.0%FPR"]} \\\\ \\hline""" latex_content += """ \\end{tabularx} } @@ -442,13 +453,15 @@ def __init__(self: Self, original_data: DataLoader, recreated_data: DataLoader, if load: return - def load(self, data): + def load(self:Self, data: dict) -> None: + """Load the GIAResults from disk.""" + self.original = data["original"] self.resulttype = data["resulttype"] self.recreated = data["recreated"] self.id = data["id"] - def save(self: Self, save_path: str, name: str, config: dict): + def save(self: Self, save_path: str, name: str, config: dict) -> None: """Save the GIAResults to disk.""" result_config = config["attack_list"][name] @@ -496,14 +509,13 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: with open(f"{save_path}/data.json", "w") as f: json.dump(data, f) - pass - - @classmethod - def create_result(self: Self, attack_name: str, save_path: str) -> None: + @staticmethod + def create_result(attack_name: str, save_path: str) -> None: """Result method for GIA.""" - def _latex(attack_name, original, recreated): - latex_content = f""" + def _latex(attack_name: str, original: str, recreated: str) -> str: + """Latex method for GIAResults.""" + return f""" \\subsection{{{" ".join(attack_name.split("_"))}}} \\begin{{figure}}[ht] \\includegraphics[width=0.8\\textwidth]{{{original}}} @@ -515,8 +527,6 @@ def _latex(attack_name, original, recreated): \\caption{{Original}} \\end{{figure}} """ - return latex_content - return _latex(attack_name=attack_name, original=save_path+"recreated_image.png", recreated=save_path+"original_image.png") class SyntheticResult: @@ -524,30 +534,26 @@ class SyntheticResult: def __init__( # noqa: PLR0913 self:Self, + values: list, load: bool = False, - )-> None: - """Initalze Result method - - Args: - ---- + ) -> None: + """Initalze Result method.""" - """ # Initialize values to result object - # self.values = values + self.values = values # Have a method to return if the results are to be loaded if load: return # Create some result - # self.result_values = some_result + self.result_values = self.create_result(self.values) - def load(self, data: dict): + def load(self:Self, data: dict) -> None: """Load the TEMPLATEResult class to disk.""" - # self.result_values = data["some_result"] - pass + self.result_values = data["some_result"] - def save(self:Self, path: str, name: str, config:dict = None): + def save(self:Self, path: str, name: str, config:dict = None) -> None: """Save the TEMPLATEResult class to disk.""" result_config = config["attack_list"][name] @@ -575,30 +581,26 @@ class TEMPLATEResult: def __init__( # noqa: PLR0913 self:Self, + values: list, load: bool = False, - )-> None: - """Initalze Result method - - Args: - ---- + ) -> None: + """Initalze Result method.""" - """ # Initialize values to result object - # self.values = values + self.values = values # Have a method to return if the results are to be loaded if load: return # Create some result - # self.result_values = some_result + self.result_values = self.create_result(self.values) - def load(self, data: dict): + def load(self:Self, data: dict) -> None: """Load the TEMPLATEResult class to disk.""" - # self.result_values = data["some_result"] - pass + self.result_values = data["some_result"] - def save(self:Self, path: str, name: str, config:dict = None): + def save(self:Self, path: str, name: str, config:dict = None) -> None: """Save the TEMPLATEResult class to disk.""" result_config = config["attack_list"][name] @@ -620,18 +622,19 @@ def save(self:Self, path: str, name: str, config:dict = None): with open(f"{path}/{name}/{self.id}/data.json", "w") as f: json.dump(data, f) - @classmethod - def create_result(self, results): + @staticmethod + def create_result(results: list) -> str: """Method for results.""" - def _latex(results): - """Latex method for TEMPLATEResult""" - pass - pass - -def get_result_fixed_fpr(fpr, tpr): + def _latex(results: list) -> str: + """Latex method for TEMPLATEResult.""" + return results + return _latex(results) +def get_result_fixed_fpr(fpr: list, tpr: list) -> dict: + """Find TPR values for fixed TPRs.""" # Function to find TPR at given FPR thresholds - def find_tpr_at_fpr(fpr_array:np.ndarray, tpr_array:np.ndarray, threshold:float): #-> Optional[str]: + def find_tpr_at_fpr(fpr_array:np.ndarray, tpr_array:np.ndarray, threshold:float) -> float: + """Find tpr for a given fpr.""" try: # Find the last index where FPR is less than the threshold valid_index = np.where(fpr_array < threshold)[0][-1] @@ -646,7 +649,8 @@ def find_tpr_at_fpr(fpr_array:np.ndarray, tpr_array:np.ndarray, threshold:float) "TPR@0.01%FPR": find_tpr_at_fpr(fpr, tpr, 0.0001), "TPR@0.0%FPR": find_tpr_at_fpr(fpr, tpr, 0.0)} -def get_config_name(config): +def get_config_name(config: dict) -> str: + """Create id from the attack config.""" config = dict(sorted(config.items())) exclude = ["attack_data_dir"] @@ -661,7 +665,7 @@ def get_config_name(config): config_name += f"-{key}={value}" return config_name -def reduce_to_unique_labels(results): +def reduce_to_unique_labels(results: list) -> list: """Reduce very long labels to unique and distinct ones.""" strings = [res.id for res in results] @@ -675,8 +679,8 @@ def reduce_to_unique_labels(results): config = "-".join(parts[1:]) if len(parts) > 1 else "" # The rest is the configuration name_configs[name].append(config) # Store the configuration under the name - def find_common_suffix(configs): - """Helper function to find the common suffix among multiple configurations""" + def find_common_suffix(configs: list) -> str: + """Helper function to find the common suffix among multiple configurations.""" if not configs: return "" @@ -702,7 +706,8 @@ def find_common_suffix(configs): common_suffix = find_common_suffix(configs) # Remove the common suffix from each configuration - trimmed_configs = [config[:-(len(common_suffix) + 1)] if common_suffix and config.endswith(common_suffix) else config for config in configs] + trimmed_configs = [config[:-(len(common_suffix) + 1)] if common_suffix and config.endswith(common_suffix) + else config for config in configs] # Process configurations based on whether they share the same pattern for config in trimmed_configs: diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index ae6fcc67..9dd4f18f 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -1,3 +1,5 @@ +"""Implementation of the Report module.""" + import json import logging import os @@ -24,12 +26,14 @@ def __init__(self:Self, report_dir: str, logger:logging.Logger) -> None: self.pdf_results[key] = [] def save_results(self:Self, attack_name: str, result_data: dict, config: dict) -> None: - """Save attack results.""" + """Save method for results.""" self.logger.info(f"Saving results for {attack_name}") result_data.save(self.report_dir, attack_name, config) - def load_results(self:Self): + def load_results(self:Self) -> None: + """Load method for results.""" + self.results = [] for parentdir in os.scandir(f"{self.report_dir}"): if parentdir.is_dir(): @@ -64,11 +68,11 @@ def load_results(self:Self): except Exception as e: self.logger.info(f"Not able to load data, Error: {e}") - def _get_results_of_name(self:Self, results, resultname_value) -> list: + def _get_results_of_name(self:Self, results: list, resultname_value: str) -> list: indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] return [results[idx] for idx in indices] - def _get_all_attacknames(self:Self): + def _get_all_attacknames(self:Self) -> list: attack_name_list = [] for result in self.results: if result.resultname not in attack_name_list: @@ -76,6 +80,8 @@ def _get_all_attacknames(self:Self): return attack_name_list def create_results_all(self:Self) -> None: + """Result method to group all attacks.""" + for result_type in self.leakpro_types: try: # Get all results of type "Result" @@ -89,20 +95,24 @@ def create_results_all(self:Self) -> None: # Check if the result type has a 'create_results' method try: result_class = globals().get(result_type) - except: - self.logger.info(f"No {result_type} class could be found or exists") + except Exception as e: + self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") continue if hasattr(result_class, "create_results") and callable(result_class.create_results): # Create all results - merged_result = result_class.create_results(results=results, save_dir=self.report_dir, save_name="all_results") + merged_result = result_class.create_results(results=results, + save_dir=self.report_dir, + save_name="all_results") self.pdf_results[result_type].append(merged_result) except Exception as e: - print("all", e) + self.logger.info(f"Error in results all: {e}") + + def create_results_strong(self:Self) -> None: + """Result method for grouping the strongest attacks.""" - def create_results_strong(self:Self): for result_type in self.leakpro_types: try: # Get all results of type "Result" @@ -115,25 +125,31 @@ def create_results_strong(self:Self): try: result_class = globals().get(result_type) - except: - self.logger.info(f"No {result_type} class could be found or exists") + except Exception as e: + self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") continue # Get all attack names - attack_name_grouped_results = [self._get_results_of_name(results, name) for name in self._get_all_attacknames()] + attack_name_grouped_results = [self._get_results_of_name(results, name) for\ + name in self._get_all_attacknames()] # Get the strongest result for each attack name if hasattr(result_class, "get_strongest") and callable(result_class.get_strongest): - strongest_results = [result_class.get_strongest(result) for result in attack_name_grouped_results] + strongest_results = [result_class.get_strongest(result) for result in \ + attack_name_grouped_results] # Create the strongest results - merged_result = result_class.create_results(results=strongest_results, save_dir=self.report_dir, save_name="strong_results") + merged_result = result_class.create_results(results=strongest_results, + save_dir=self.report_dir, + save_name="strong_results") self.pdf_results[result_type].append(merged_result) except Exception as e: - print("results_strong", e) + self.logger.info(f"Error in results strong: {e}") + + def create_results_attackname_grouped(self:Self) -> None: + """Result method for grouping attacks by name.""" - def create_results_attackname_grouped(self:Self): # Get all attack names all_attack_names = self._get_all_attacknames() @@ -150,8 +166,8 @@ def create_results_attackname_grouped(self:Self): # Check if the result type has a 'create_results' method try: result_class = globals().get(result_type) - except: - self.logger.info(f"No {result_type} class could be found or exists") + except Exception as e: + self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") continue for name in all_attack_names: @@ -161,14 +177,16 @@ def create_results_attackname_grouped(self:Self): attack_results = self._get_results_of_name(results, name) # Create results - merged_result = result_class.create_results(results=attack_results, save_dir=self.report_dir, save_name="grouped_"+name) + merged_result = result_class.create_results(results=attack_results, + save_dir=self.report_dir, + save_name="grouped_"+name) self.pdf_results[result_type].append(merged_result) except Exception as e: - print("create_results_attackname_grouped", e) + self.logger.info(f"Error in results grouped: {e}") - def create_report(self:Self): - """Method to create PDF report""" + def create_report(self:Self) -> None: + """Method to create PDF report.""" # Create initial part of the document. self._init_pdf() @@ -183,16 +201,16 @@ def create_report(self:Self): # Compile the PDF self._compile_pdf() - def _init_pdf(self:Self): + def _init_pdf(self:Self) -> None: self.latex_content = """ \\documentclass{article} \\usepackage{tabularx} \\usepackage{graphicx} - \\usepackage{graphics} + \\usepackage{graphics} \\begin{document} """ - def _compile_pdf(self:Self, install_flag: bool = False): + def _compile_pdf(self:Self) -> None: """Method to compile PDF.""" self.latex_content += """ @@ -201,24 +219,17 @@ def _compile_pdf(self:Self, install_flag: bool = False): with open(f"{self.report_dir}/LeakPro_output.tex", "w") as f: f.write(self.latex_content) - # Check if pdflatex is installed - try: - check = subprocess.check_output(["which", "pdflatex"], universal_newlines=True) - assert "pdflatex" in check - except: - # Option to install pdflatex - self.logger.info('Could not find pdflatex installed\nPlease install pdflatex with "apt install texlive-latex-base"') - choice = input("Do you want to install pdflatex? (Y/n): ").lower() - if (choice in {"y", "yes"} or install_flag==True): - proc = subprocess.Popen(["apt", "install", "-y", "texlive-latex-base"], stdout=subprocess.DEVNULL) - proc.communicate() - - # Compile PDF if possible try: + # Check if pdflatex is installed + check = subprocess.check_output(["which", "pdflatex"], universal_newlines=True) # noqa: S607 S603 + if "pdflatex" not in check: + self.logger.info("Could not find pdflatex installed\ + \nPlease install pdflatex with apt install texlive-latex-base") + cmd = ["pdflatex", "-interaction", "nonstopmode", f"{self.report_dir}/LeakPro_output.tex"] - proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) # noqa: S603 proc.communicate() self.logger.info("PDF compiled") + except Exception as e: - print(e) - self.logger.info("Could not compile PDF") + self.logger.info(f"Could not compile PDF: {e}") diff --git a/leakpro/tests/test_attack_result/test_attack_result.py b/leakpro/tests/test_attack_result/test_attack_result.py index 56d3f2ae..58390cbb 100644 --- a/leakpro/tests/test_attack_result/test_attack_result.py +++ b/leakpro/tests/test_attack_result/test_attack_result.py @@ -1,14 +1,19 @@ +"""Tests for the attack_result module.""" + import json import os import tempfile import unittest from unittest.mock import MagicMock -from leakpro.metrics.attack_result import * +import numpy as np + +from leakpro.metrics.attack_result import MIAResult, get_config_name from leakpro.utils.import_helper import Self class TestMIAResult(unittest.TestCase): + """Test class for MIAResult.""" def setUp(self:Self) -> None: """Set up temporary directory and logger for MIAResult.""" @@ -74,16 +79,19 @@ def tearDown(self:Self) -> None: """Clean up temporary directory.""" self.temp_dir.cleanup() - def test_MIAResult_init(self:Self) -> None: + def test_miaresult_init(self:Self) -> None: """Test the initialization of MIAResult.""" - assert self.miaresult.id == None + assert self.miaresult.id is None def test_check_tpr_fpr(self:Self) -> None: + """Test fpr and tpr.""" + assert np.allclose(self.miaresult.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) assert self.miaresult.fp.all() == 0. assert self.miaresult.tn.all() == 0. - def test_save_load_MIAResult(self:Self) -> None: + def test_save_load_miaresult(self:Self) -> None: + """Test load and save functionality.""" name = "lira" config_name = get_config_name(self.config["attack_list"][name]) @@ -102,14 +110,14 @@ def test_save_load_MIAResult(self:Self) -> None: data = json.load(f) self.miaresult_new = MIAResult(load=True) - assert self.miaresult_new.predicted_labels == None - assert self.miaresult_new.true_labels == None - assert self.miaresult_new.signal_values == None + assert self.miaresult_new.predicted_labels is None + assert self.miaresult_new.true_labels is None + assert self.miaresult_new.signal_values is None self.miaresult_new.load(data) assert np.allclose(self.miaresult_new.tpr, np.array([0., 0., 0.16666667, 0.5, 1., 1., 1., 1., 1., 1.])) - def test_get_strongest_MIAResult(self:Self) -> None: + def test_get_strongest_miaresult(self:Self) -> None: """Test selecting the strongest attack based on ROC AUC.""" result_1 = MagicMock(roc_auc=0.75) result_2 = MagicMock(roc_auc=0.85) @@ -124,27 +132,29 @@ def test_get_strongest_MIAResult(self:Self) -> None: def test_latex(self:Self) -> None: """Test if the LaTeX content is generated correctly.""" - result = [MagicMock(id="attack-config-1", resultname="test_attack_1", fixed_fpr_table={"TPR@1.0%FPR": 0.90, "TPR@0.1%FPR": 0.80, "TPR@0.01%FPR": 0.70, "TPR@0.0%FPR": 0.60})] + result = [MagicMock(id="attack-config-1", resultname="test_attack_1",\ + fixed_fpr_table={"TPR@1.0%FPR": 0.90, "TPR@0.1%FPR": 0.80, "TPR@0.01%FPR": 0.70, "TPR@0.0%FPR": 0.60})] + subsection = "attack_comparison" filename = f"{self.temp_dir}/test.png" latex_content = MIAResult(load=True)._latex(result, subsection, filename) # Check that the subsection is correctly included - self.assertIn("\\subsection{attack comparison}", latex_content) + assert "\\subsection{attack comparison}" in latex_content # Check that the figure is correctly included - self.assertIn(f"\\includegraphics[width=0.8\\textwidth]{{{filename}.png}}", latex_content) + assert f"\\includegraphics[width=0.8\\textwidth]{{{filename}.png}}" in latex_content # Check that the table header is correct - self.assertIn("Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR", latex_content) + assert "Attack name & attack config & TPR: 1.0\\%FPR & 0.1\\%FPR & 0.01\\%FPR & 0.0\\%FPR" in latex_content # Check if the results for mock_result are included correctly - self.assertIn("test-attack-1", latex_content) - self.assertIn("0.9", latex_content) - self.assertIn("0.8", latex_content) - self.assertIn("0.7", latex_content) - self.assertIn("0.6", latex_content) + assert "test-attack-1" in latex_content + assert "0.9" in latex_content + assert "0.8" in latex_content + assert "0.7" in latex_content + assert "0.6" in latex_content # Ensure the LaTeX content ends properly - self.assertIn("\\newline\n", latex_content) + assert "\\newline\n" in latex_content diff --git a/leakpro/tests/test_report_handler/test_report_handler.py b/leakpro/tests/test_report_handler/test_report_handler.py index c0775fca..28f273f8 100644 --- a/leakpro/tests/test_report_handler/test_report_handler.py +++ b/leakpro/tests/test_report_handler/test_report_handler.py @@ -1,9 +1,10 @@ +"""Tests for the report_handler module.""" + import logging import os import tempfile from unittest.mock import MagicMock -from leakpro.metrics.attack_result import * from leakpro.reporting.report_handler import ReportHandler from leakpro.utils.import_helper import Self diff --git a/pyproject.toml b/pyproject.toml index edffb97b..f679d7ef 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,8 @@ lint.select = [ exclude = [ ".venv", "./tests", + "./leakpro/tests", + "./examples", ] lint.ignore = [ From b15b8a749b85e9bbfb9ae55c64f6415131d58837 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Wed, 20 Nov 2024 00:37:18 +0000 Subject: [PATCH 07/14] Added notebook example for report handler --- leakpro/metrics/attack_result.py | 567 +++++++++++++++--- leakpro/reporting/report_handler.py | 220 +++---- leakpro/synthetic_data_attacks/plots.py | 16 +- .../singling_out_utils.py | 107 +++- 4 files changed, 694 insertions(+), 216 deletions(-) diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index ac1ae0b0..5bb72131 100755 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -239,22 +239,30 @@ def __init__( # noqa: PLR0913 self.tpr = self.tp / (self.tp + self.fn) self.roc_auc = auc(self.fpr, self.tpr) - def load(self:Self, data: dict) -> None: + + @staticmethod + def load(data: dict) -> None: """Load the MIAResults to disk.""" - self.resultname = data["resultname"] - self.resulttype = data["resulttype"] - self.tpr = data["tpr"] - self.fpr = data["fpr"] - self.roc_auc = data["roc_auc"] - self.config = data["config"] - self.fixed_fpr_table = data["fixed_fpr"] - self.audit_indices = data["audit_indices"] - self.signal_values = data["signal_values"] - self.true_labels = data["true_labels"] - self.threshold = data["threshold"] - - def save(self:Self, path: str, name: str, config:dict = None) -> None: + miaresult = MIAResult(load=True) + + miaresult.resultname = data["resultname"] + miaresult.resulttype = data["resulttype"] + miaresult.tpr = data["tpr"] + miaresult.fpr = data["fpr"] + miaresult.roc_auc = data["roc_auc"] + miaresult.config = data["config"] + miaresult.fixed_fpr_table = data["fixed_fpr"] + miaresult.audit_indices = data["audit_indices"] + miaresult.signal_values = data["signal_values"] + miaresult.true_labels = data["true_labels"] + miaresult.threshold = data["threshold"] + + miaresult.id = data["id"] + + return miaresult + + def save(self:Self, path: str, name: str, config:dict = None, show_plot:bool = False) -> None: """Save the MIAResults to disk.""" result_config = config["attack_list"][name] @@ -297,7 +305,8 @@ def save(self:Self, path: str, name: str, config:dict = None) -> None: temp_res.fpr = self.fpr temp_res.id = self.id self.create_plot(results = [temp_res], - filename = filename + filename = filename, + show_plot = show_plot ) # Create SignalHistogram plot for MIAResult @@ -305,7 +314,8 @@ def save(self:Self, path: str, name: str, config:dict = None) -> None: self.create_signal_histogram(filename = filename, signal_values = self.signal_values, true_labels = self.true_labels, - threshold = self.threshold + threshold = self.threshold, + show_plot = show_plot, ) @staticmethod @@ -313,7 +323,13 @@ def get_strongest(results: list) -> list: """Method for selecting the strongest attack.""" return max((res for res in results), key=lambda d: d.roc_auc) - def create_signal_histogram(self:Self, filename: str, signal_values: list, true_labels: list, threshold: float) -> None: + def create_signal_histogram( + self:Self, filename: str, + signal_values: list, + true_labels: list, + threshold: float, + show_plot: bool = False, + ) -> None: """Method to create Signal Histogram.""" values = np.array(signal_values).ravel() @@ -353,10 +369,18 @@ def create_signal_histogram(self:Self, filename: str, signal_values: list, true_ plt.ylabel("Number of samples") plt.title("Signal histogram") plt.savefig(fname=filename, dpi=1000) - plt.clf() + if show_plot: + plt.show() + else: + plt.clf() @staticmethod - def create_plot(results: list, save_dir: str = "", save_name: str = "") -> None: + def create_plot( + results: list, + save_dir: str = "", + save_name: str = "", + show_plot: bool = False + ) -> None: """Plot method for MIAResult.""" filename = f"{save_dir}/{save_name}" @@ -385,34 +409,79 @@ def create_plot(results: list, save_dir: str = "", save_name: str = "") -> None: plt.ylabel("True positive rate (TPR)") plt.title(save_name+"ROC Curve") plt.savefig(fname=f"{filename}.png", dpi=1000, bbox_inches="tight") - plt.clf() + + if show_plot: + plt.show() + else: + plt.clf() + + @staticmethod + def _get_all_attacknames( + results: list + ) -> list: + attack_name_list = [] + for result in results: + if result.resultname not in attack_name_list: + attack_name_list.append(result.resultname) + return attack_name_list + + @staticmethod + def _get_results_of_name( + results: list, + resultname_value: str + ) -> list: + indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] + return [results[idx] for idx in indices] @staticmethod def create_results( results: list, save_dir: str = "./", save_name: str = "foo", + show_plot: bool = False, ) -> str: """Result method for MIAResult.""" + latex = "" + + # Create plot for all results + MIAResult.create_plot(results, save_dir, save_name="all_results", show_plot=show_plot) + latex += MIAResult._latex(results, save_dir, save_name="all_results") + + # Create plot for results grouped by name + all_attack_names = MIAResult._get_all_attacknames(results) + for name in all_attack_names: + results_name_grouped = MIAResult._get_results_of_name(results, name) + MIAResult.create_plot(results_name_grouped, save_dir, save_name=name, show_plot=show_plot) + latex += MIAResult._latex(results_name_grouped, save_dir, save_name=name) - MIAResult.create_plot(results, save_dir, save_name) + # Create plot for results grouped by name + grouped_results = [MIAResult._get_results_of_name(results, name) for name + in all_attack_names] + strongest_results = [MIAResult.get_strongest(result) for result in grouped_results] + MIAResult.create_plot(strongest_results, save_dir, save_name="strongest", show_plot=show_plot) + latex += MIAResult._latex(strongest_results, save_dir, save_name="strongest") - return MIAResult._latex(results, save_dir, save_name) + return latex @staticmethod - def _latex(results: list, save_dir: str, save_name: str) -> str: + def _latex( + results: list, + save_dir: str, + save_name: str + ) -> str: """Latex method for MIAResult.""" filename = f"{save_dir}/{save_name}" - latex_content = "" - latex_content += f""" + # Input mia results image + latex_content = f""" \\subsection{{{" ".join(save_name.split("_"))}}} \\begin{{figure}}[ht] \\includegraphics[width=0.8\\textwidth]{{{filename}.png}} \\end{{figure}} """ + # Initialize latex table latex_content += """ \\resizebox{\\linewidth}{!}{% \\begin{tabularx}{\\textwidth}{l c l l l l} @@ -420,11 +489,13 @@ def _latex(results: list, save_dir: str, save_name: str) -> str: \\hline """ + # Convert config to latex table input def config_latex_style(config: str) -> str: config = " \\\\ ".join(config.split("-")[1:]) config = "-".join(config.split("_")) return f"""\\shortstack{{{config}}}""" + # Append all mia results to table for res in results: config = config_latex_style(res.id) latex_content += f"""{"-".join(res.resultname.split("_"))} & {config} & {res.fixed_fpr_table["TPR@1.0%FPR"]} & @@ -442,8 +513,16 @@ def config_latex_style(config: str) -> str: class GIAResults: """Contains results for a GIA attack.""" - def __init__(self: Self, original_data: DataLoader, recreated_data: DataLoader, - psnr_score: float, data_mean: float, data_std: float, load: bool) -> None: + def __init__( + self: Self, + original_data: DataLoader, + recreated_data: DataLoader, + psnr_score: float, + data_mean: float, + data_std: float, + load: bool + ) -> None: + self.original_data = original_data self.recreated_data = recreated_data self.PSNR_score = psnr_score @@ -453,7 +532,10 @@ def __init__(self: Self, original_data: DataLoader, recreated_data: DataLoader, if load: return - def load(self:Self, data: dict) -> None: + def load( + self:Self, + data: dict + ) -> None: """Load the GIAResults from disk.""" self.original = data["original"] @@ -461,7 +543,13 @@ def load(self:Self, data: dict) -> None: self.recreated = data["recreated"] self.id = data["id"] - def save(self: Self, save_path: str, name: str, config: dict) -> None: + def save( + self: Self, + save_path: str, + name: str, + config: dict, + show_plot: bool = False + ) -> None: """Save the GIAResults to disk.""" result_config = config["attack_list"][name] @@ -493,6 +581,15 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: original = os.path.join(save_path, "original_image.png") save_image(gt_denormalized, original) + if show_plot: + # Plot output + plt.plot(output_denormalized) + plt.show() + + # Plot ground truth + plt.plot(gt_denormalized) + plt.show() + # Data to be saved data = { "resulttype": self.__class__.__name__, @@ -510,13 +607,21 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: json.dump(data, f) @staticmethod - def create_result(attack_name: str, save_path: str) -> None: + def create_results( + results: list, + save_dir: str = "./", + save_name: str = "foo", + ) -> str: """Result method for GIA.""" - def _latex(attack_name: str, original: str, recreated: str) -> str: + def _latex( + save_name: str, + original: str, + recreated: str + ) -> str: """Latex method for GIAResults.""" return f""" - \\subsection{{{" ".join(attack_name.split("_"))}}} + \\subsection{{{" ".join(save_name.split("_"))}}} \\begin{{figure}}[ht] \\includegraphics[width=0.8\\textwidth]{{{original}}} \\caption{{Original}} @@ -527,63 +632,347 @@ def _latex(attack_name: str, original: str, recreated: str) -> str: \\caption{{Original}} \\end{{figure}} """ - return _latex(attack_name=attack_name, original=save_path+"recreated_image.png", recreated=save_path+"original_image.png") - -class SyntheticResult: - """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" - - def __init__( # noqa: PLR0913 - self:Self, - values: list, - load: bool = False, - ) -> None: - """Initalze Result method.""" + return _latex(save_name=save_name, original=save_dir+"recreated_image.png", recreated=save_dir+"original_image.png") + +# class SyntheticResult: +# """Contains results for SyntheticResult.""" + +# def __init__( # noqa: PLR0913 +# self:Self, +# SynRes: Union[SinglingOutResults, LinkabilityResults, InferenceResults], +# load: bool = False, +# ) -> None: +# """Initalze SyntheticResult method.""" + +# # Initialize values to result object +# self.SynRes = SynRes + +# # Have a method to return if the results are to be loaded +# if load: +# return + +# # Create some result +# self.result_values = self.create_result(self.values) + +# def load( +# self:Self, +# data: dict +# ) -> None: +# """Load the SyntheticResult class to disk.""" +# self.result_values = data["some_result"] + +# def save( +# self:Self, +# save_path: str, +# save_name: str, +# config:dict = None +# ) -> None: +# """Save the SyntheticResult class to disk.""" + +# result_config = config["attack_list"][name] + +# # Get the name for the attack configuration +# config_name = get_config_name(result_config) +# self.id = f"{name}{config_name}" +# save_path = f"{path}/{name}/{self.id}" + +# save_name = os.path.join(save_path, f"synthetic.png") + +# SyntheticResult.plot( +# res=self.SynRes, +# show=False, +# save=True, +# save_path=save_path, +# save_name=save_name, +# ) + +# # Data to be saved +# data = { +# "synthetic_result_name": self.SynRes.__class__.__name__, +# "synthetic_result": self.SynRes, +# "image_path": save_name, +# "id": self.id +# } + +# # Check if path exists, otherwise create it. +# if not os.path.exists(f"{save_path}"): +# os.makedirs(f"{save_path}") + +# # Save the results to a file +# with open(f"{save_path}/data.json", "w") as f: +# json.dump(data, f) + +# @staticmethod +# def create_results( +# results: list, +# save_dir: str = "./", +# save_name: str = "foo", +# ) -> str: +# """Result method for SyntheticResult.""" + +# def _latex(save_name: str, result_file: str) -> str: +# """Latex method for SyntheticResult.""" +# return f""" +# \\subsection{{{" ".join(save_name.split("_"))}}} +# \\begin{{figure}}[ht] +# \\includegraphics[width=0.8\\textwidth]{{{result_file}}} +# \\caption{{Original}} +# \\end{{figure}} +# """ +# return _latex(results=results, save_name=save_name, result_file=save_dir+"synthetic.png") + +# @staticmethod +# def plot( +# res: Union[SinglingOutResults, LinkabilityResults, InferenceResults], +# high_res_flag: bool = True, +# case_flag: str = "base", +# show:bool = True, +# save:bool = False, +# save_path:str = "./", +# save_name:str = "fig.png", +# ) -> None: + +# save_name = os.path.join(save_path, save_name) + +# SyntheticResultName = res.__class__.__name__ +# if SyntheticResultName == "SinglingOutResults": +# SyntheticResult.plot_singling_out(sin_out_res=res, +# high_res_flag = high_res_flag, +# show=show, +# save=save, +# save_name=save_name) + +# elif SyntheticResultName == "LinkabilityResults": +# SyntheticResult.plot_linkability(link_res=res, +# high_res_flag = high_res_flag, +# show=show, +# save=save, +# save_name=save_name) + +# elif SyntheticResultName == "InferenceResults": +# if case_flag == "base": +# SyntheticResult.plot_ir_base_case(inf_res=res, +# high_res_flag = high_res_flag, +# show=show, +# save=save, +# save_name=save_name) +# elif case_flag == "worst": +# SyntheticResult.plot_ir_worst_case(inf_res=res, +# high_res_flag = high_res_flag, +# show=show, +# save=save, +# save_name=save_name) +# else: +# print("No such case") + +# def plot_ir_base_case( +# *, +# inf_res: InferenceResults, +# high_res_flag: bool = True, +# show: bool = True, +# save: bool = False, +# save_name: str = None, +# ) -> None: +# """Function to plot inference results base case given results. + +# Note: function is not tested and is used in examples. +# """ +# #Set res, secrets, set_secrets and set_nr_aux_cols +# res = np.array(inf_res.res) +# secrets = np.array(inf_res.secrets) +# set_secrets = sorted(set(secrets)) +# set_nr_aux_cols = np.unique(res[:,-1].astype(int)) +# # High res flag +# if high_res_flag: +# plot_save_high_res() +# # Set up the figure and get axes +# fig_title = f"Inference risk, base case scenario, {conf_level} confidence, total attacks: {int(res[:,0].sum())}" +# axs = get_figure_axes(two_axes_flag=True, fig_title=fig_title) +# # Set plot variables +# titles = ["Risk per column", "Risk per Nr aux cols"] +# xlabels = ["Secret col", "Nr aux cols"] +# sets_values = [set_secrets, set_nr_aux_cols] +# valueses = [secrets, res[:,-1]] +# assert len(axs) == len(titles) +# assert len(axs) == len(xlabels) +# assert len(axs) == len(sets_values) +# assert len(axs) == len(valueses) +# #Plotting +# for ax, title, xlabel, set_values, values in zip(axs, titles, xlabels, sets_values, valueses): +# set_labels_and_title( +# ax = ax, +# xlabel = xlabel, +# ylabel = "Risk", +# title = title +# ) +# # Iterate through values and plot bar charts +# iterate_values_plot_bar_charts(ax=ax, res=res, set_values=set_values, values=values) +# # Adding ticks +# set_ticks(ax=ax, xlabels=set_values) +# # Adding legend +# set_legend(ax=ax) +# # Save plot +# if save: +# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") +# # Show plot +# if show: +# plt.show() +# else: +# plt.clf() + +# def plot_ir_worst_case( +# *, +# inf_res: InferenceResults, +# high_res_flag: bool = True, +# show: bool = True, +# save: bool = False, +# save_name: str = None, +# ) -> None: +# """Function to plot inference results worst case given results. + +# Note: function is not tested and is used in examples. +# """ +# #Set res, secrets and set_secrets +# res = np.array(inf_res.res) +# secrets = np.array(inf_res.secrets) +# set_secrets = sorted(set(secrets)) +# # High res flag +# if high_res_flag: +# plot_save_high_res() +# # Set up the figure and get axes +# ax = get_figure_axes() +# # Iterate through secrets and plot bar charts +# iterate_values_plot_bar_charts( +# ax = ax, +# res = res, +# set_values = set_secrets, +# values = secrets, +# max_value_flag = True +# ) +# # Adding labels and title +# set_labels_and_title( +# ax = ax, +# xlabel = "Secret col", +# ylabel = "Risk", +# title = f"Inference risk, worst case scenario, total attacks: {int(res[:,0].sum())}" +# ) +# # Adding ticks +# set_ticks(ax=ax, xlabels=set_secrets) +# # Adding legend +# set_legend(ax=ax) +# # Save plot +# if save: +# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") +# # Show plot +# if show: +# plt.show() +# else: +# plt.clf() + +# @staticmethod +# def plot_linkability( +# *, +# link_res:LinkabilityResults, +# high_res_flag: bool = False, +# show: bool = True, +# save: bool = False, +# save_name: str = None, +# ) -> None: +# """Function to plot linkability results from given res. + +# Note: function is not tested and is used in examples. +# """ +# # Get res and aux_cols_nr +# res = np.array(link_res.res) +# set_nr_aux_cols = np.unique(res[:,-1].astype(int)) +# # High res flag +# if high_res_flag: +# plot_save_high_res() +# # Set up the figure and get axes +# ax = get_figure_axes() +# # Iterate through nr of columns and plot bar charts +# iterate_values_plot_bar_charts(ax=ax, res=res, set_values=set_nr_aux_cols, values=res[:, -1]) +# # Adding labels and title +# set_labels_and_title( +# ax = ax, +# xlabel = "Nr aux cols", +# ylabel = "Risk", +# title = f"Linkability risk {conf_level} confidence, total attacks: {int(res[:,0].sum())}" +# ) +# # Adding ticks +# set_ticks(ax=ax, xlabels=set_nr_aux_cols) +# # Adding legend +# set_legend(ax=ax) +# # Save plot +# if save: +# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") +# # Show plot +# if show: +# plt.show() +# else: +# plt.clf() + +# @staticmethod +# def plot_singling_out( +# *, +# sin_out_res: SinglingOutResults, +# high_res_flag: bool = True, +# show: bool = True, +# save: bool = False, +# save_name: str = None, +# ) -> None: +# """Function to plot singling out given results. + +# Note: function is not tested and is used in examples. +# """ +# #Set res, n_cols and set_n_cols +# res = np.array(sin_out_res.res) +# n_cols = res[:,-1].astype(int).tolist() +# set_n_cols = np.unique(n_cols) +# # High res flag +# if high_res_flag: +# plot_save_high_res() +# # Set up the figure and get axes +# ax = get_figure_axes() +# # Iterate through values and plot bar charts +# iterate_values_plot_bar_charts( +# ax = ax, +# res = res, +# set_values = set_n_cols, +# values = n_cols, +# max_value_flag = True +# ) +# # Adding labels and title +# fig_title = f"Singling out risk total attacks: {int(res[:,0].sum())}" +# if res.shape[0]==1: +# fig_title += f", n_cols={int(res[0,-1])}" +# set_labels_and_title( +# ax = ax, +# xlabel = "n_cols for predicates", +# ylabel = "Risk", +# title = fig_title +# ) +# # Adding ticks +# set_ticks(ax=ax, xlabels=set_n_cols) +# # Adding legend +# if save: +# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") +# # Show plot +# if show: +# plt.show() +# else: +# plt.clf() - # Initialize values to result object - self.values = values - - # Have a method to return if the results are to be loaded - if load: - return - - # Create some result - self.result_values = self.create_result(self.values) - - def load(self:Self, data: dict) -> None: - """Load the TEMPLATEResult class to disk.""" - self.result_values = data["some_result"] - - def save(self:Self, path: str, name: str, config:dict = None) -> None: - """Save the TEMPLATEResult class to disk.""" - - result_config = config["attack_list"][name] - - # Data to be saved - data = { - "some_result": self.result_values - } - - # Get the name for the attack configuration - config_name = get_config_name(result_config) - self.id = f"{name}{config_name}" - save_path = f"{path}/{name}/{self.id}" - - # Check if path exists, otherwise create it. - if not os.path.exists(f"{save_path}"): - os.makedirs(f"{save_path}") - # Save the results to a file - with open(f"{save_path}/data.json", "w") as f: - json.dump(data, f) class TEMPLATEResult: """Contains results related to the performance of the metric. It contains the results for multiple fpr.""" def __init__( # noqa: PLR0913 - self:Self, - values: list, - load: bool = False, - ) -> None: + self:Self, + values: list, + load: bool = False, + ) -> None: """Initalze Result method.""" # Initialize values to result object @@ -596,11 +985,19 @@ def __init__( # noqa: PLR0913 # Create some result self.result_values = self.create_result(self.values) - def load(self:Self, data: dict) -> None: + def load( + self:Self, + data: dict + ) -> None: """Load the TEMPLATEResult class to disk.""" self.result_values = data["some_result"] - def save(self:Self, path: str, name: str, config:dict = None) -> None: + def save( + self:Self, + path: str, + name: str, + config:dict = None + ) -> None: """Save the TEMPLATEResult class to disk.""" result_config = config["attack_list"][name] @@ -623,7 +1020,7 @@ def save(self:Self, path: str, name: str, config:dict = None) -> None: json.dump(data, f) @staticmethod - def create_result(results: list) -> str: + def create_results(results: list) -> str: """Method for results.""" def _latex(results: list) -> str: """Latex method for TEMPLATEResult.""" diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index 9dd4f18f..ec6e3621 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -5,31 +5,53 @@ import os import subprocess -from leakpro.utils.import_helper import Self +from leakpro.metrics.attack_result import GIAResults, MIAResult +from leakpro.synthetic_data_attacks.singling_out_utils import SinglingOutResults +from leakpro.utils.import_helper import Self, Union +from leakpro.utils.logger import setup_logger # Report Handler class ReportHandler(): """Implementation of the report handler.""" - def __init__(self:Self, report_dir: str, logger:logging.Logger) -> None: - self.logger = logger - self.report_dir = report_dir + def __init__(self:Self, report_dir: str = None, logger:logging.Logger = None) -> None: + self.logger = setup_logger() if logger is None else logger + self.logger.info("Initializing report handler...") + + self.report_dir = self._try_find_rep_dir() if report_dir is None else report_dir + self.logger.info(f"report_dir set to: {self.report_dir}") + self.pdf_results = {} self.leakpro_types = ["MIAResult", "GIAResults", - "SyntheticResult" + "SinglingOutResults", ] # Initiate empty lists for the different types of LeakPro attack types for key in self.leakpro_types: self.pdf_results[key] = [] - def save_results(self:Self, attack_name: str, result_data: dict, config: dict) -> None: + def _try_find_rep_dir(self): + save_path = "../leakpro_output/results" + # Check if path exists, otherwise create it. + for _ in range(3): + if os.path.exists(save_path): + return save_path + save_path = "../"+save_path + + # If no result folder can be found + if not os.path.exists(save_path): + save_path = "../../leakpro_output/results" + os.makedirs(save_path) + return save_path + + + def save_results(self:Self, attack_name: str = None, result_data: Union[MIAResult, GIAResults, SinglingOutResults] = None, config: dict = None) -> None: """Save method for results.""" self.logger.info(f"Saving results for {attack_name}") - result_data.save(self.report_dir, attack_name, config) + result_data.save(path=self.report_dir, name=attack_name, config=config) def load_results(self:Self) -> None: """Load method for results.""" @@ -54,136 +76,81 @@ def load_results(self:Self) -> None: raise ValueError(f"Class '{resulttype}' not found.") # Initialize the class using the saved primitives - instance = cls(load=True) - instance.load(data) + # instance = cls(load=True) + data["id"] = subdir.name + instance = cls.load(data) - if instance.id is None: - instance.id = subdir.name + # if instance.id is None: + # instance.id = subdir.name - if instance.resultname is None: - instance.resultname = parentdir.name + # if instance.resultname is None: + # instance.resultname = parentdir.name self.results.append(instance) except Exception as e: self.logger.info(f"Not able to load data, Error: {e}") - def _get_results_of_name(self:Self, results: list, resultname_value: str) -> list: - indices = [idx for (idx, result) in enumerate(results) if result.resultname == resultname_value] - return [results[idx] for idx in indices] - - def _get_all_attacknames(self:Self) -> list: - attack_name_list = [] - for result in self.results: - if result.resultname not in attack_name_list: - attack_name_list.append(result.resultname) - return attack_name_list - - def create_results_all(self:Self) -> None: + def create_results( + self:Self, + types: list = [], + ) -> None: """Result method to group all attacks.""" - for result_type in self.leakpro_types: - try: - # Get all results of type "Result" - results = [res for res in self.results if res.resulttype == result_type] - - # If no results of type "result_type" is found, skip to next result_type - if not results: - self.logger.info(f"No results of type {result_type} found.") - continue - - # Check if the result type has a 'create_results' method - try: - result_class = globals().get(result_type) - except Exception as e: - self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") - continue - - if hasattr(result_class, "create_results") and callable(result_class.create_results): - - # Create all results - merged_result = result_class.create_results(results=results, - save_dir=self.report_dir, - save_name="all_results") - self.pdf_results[result_type].append(merged_result) - - except Exception as e: - self.logger.info(f"Error in results all: {e}") + for result_type in types: + # try: + # Get all results of type "Result" + # results = [res for res in self.results if res.resulttype == result_type] + results = [res for res in self.results if res.__class__.__name__ == result_type] - def create_results_strong(self:Self) -> None: - """Result method for grouping the strongest attacks.""" + # If no results of type "result_type" is found, skip to next result_type + if not results: + self.logger.info(f"No results of type {result_type} found.") + continue - for result_type in self.leakpro_types: + # Check if the result type has a 'create_results' method try: - # Get all results of type "Result" - results = [res for res in self.results if res.resulttype == result_type] - - # If no results of type "result_type" is found, skip to next result_type - if not results: - self.logger.info(f"No 'strong' results of type {result_type} found.") - continue - - try: - result_class = globals().get(result_type) - except Exception as e: - self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") - continue - - # Get all attack names - attack_name_grouped_results = [self._get_results_of_name(results, name) for\ - name in self._get_all_attacknames()] - - # Get the strongest result for each attack name - if hasattr(result_class, "get_strongest") and callable(result_class.get_strongest): - strongest_results = [result_class.get_strongest(result) for result in \ - attack_name_grouped_results] - - # Create the strongest results - merged_result = result_class.create_results(results=strongest_results, - save_dir=self.report_dir, - save_name="strong_results") - self.pdf_results[result_type].append(merged_result) - + result_class = globals().get(result_type) except Exception as e: - self.logger.info(f"Error in results strong: {e}") - - def create_results_attackname_grouped(self:Self) -> None: - """Result method for grouping attacks by name.""" - - # Get all attack names - all_attack_names = self._get_all_attacknames() - - for result_type in self.leakpro_types: - - # Get all results of type "Result" - results = [res for res in self.results if res.resulttype == result_type] - - # If no results of type "result_type" is found, skip to next result_type - if not results: - self.logger.info(f"No results of type {result_type} to group.") - continue - - # Check if the result type has a 'create_results' method - try: - result_class = globals().get(result_type) - except Exception as e: - self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") - continue - - for name in all_attack_names: - if hasattr(result_class, "create_results") and callable(result_class.create_results): - try: - # Get result for each attack names - attack_results = self._get_results_of_name(results, name) - - # Create results - merged_result = result_class.create_results(results=attack_results, - save_dir=self.report_dir, - save_name="grouped_"+name) - self.pdf_results[result_type].append(merged_result) - - except Exception as e: - self.logger.info(f"Error in results grouped: {e}") + self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") + continue + + if hasattr(result_class, "create_results") and callable(result_class.create_results): + + # Create all results + latex_results = result_class.create_results(results=results, + save_dir=self.report_dir, + ) + self.pdf_results[result_type].append(latex_results) + + # except Exception as e: + # self.logger.info(f"Error in results all: {result_class}, {e}") + + def create_results_all( + self:Self, + ) -> None: + """Method to create all types of results.""" + self.create_results(types=self.leakpro_types) + + def create_results_mia( + self:Self, + ) -> None: + """Method to create MIAResult results.""" + self.create_results(types=["MIAResult"]) + + def create_results_gia( + self:Self, + ) -> None: + """Method to create GIAResults results.""" + self.create_results(types=["GIAResults"]) + + def create_results_syn( + self:Self, + ) -> None: + """Method to create Synthetic results.""" + self.create_results(types=["SinglingOutResults", + "InferenceResults", + "LinkabilityResults"]) def create_report(self:Self) -> None: """Method to create PDF report.""" @@ -196,7 +163,7 @@ def create_report(self:Self) -> None: if len(self.pdf_results[result_type]) > 0: self.latex_content += f"""\\section{{{result_type}}}""" for res in self.pdf_results[result_type]: - self.latex_content += res + self.latex_content += res # Compile the PDF self._compile_pdf() @@ -226,10 +193,11 @@ def _compile_pdf(self:Self) -> None: self.logger.info("Could not find pdflatex installed\ \nPlease install pdflatex with apt install texlive-latex-base") - cmd = ["pdflatex", "-interaction", "nonstopmode", f"{self.report_dir}/LeakPro_output.tex"] - proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL) # noqa: S603 + cmd = ["pdflatex", "-interaction", "nonstopmode", "LeakPro_output.tex"] + proc = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, cwd=f"{self.report_dir}") # noqa: S603 proc.communicate() self.logger.info("PDF compiled") except Exception as e: self.logger.info(f"Could not compile PDF: {e}") + self.logger.info("Make sure to install pdflatex with apt install texlive-latex-base") diff --git a/leakpro/synthetic_data_attacks/plots.py b/leakpro/synthetic_data_attacks/plots.py index 8e7305f4..4540f474 100755 --- a/leakpro/synthetic_data_attacks/plots.py +++ b/leakpro/synthetic_data_attacks/plots.py @@ -189,7 +189,13 @@ def plot_ir_base_case(*, inf_res: InferenceResults, high_res_flag: bool = True) plt.tight_layout() plt.show() -def plot_singling_out(*, sin_out_res: SinglingOutResults, high_res_flag: bool = True) -> None: +def plot_singling_out(*, + sin_out_res: SinglingOutResults, + high_res_flag: bool = True, + show: bool = True, + save: bool = False, + save_name: str = None + ) -> None: """Function to plot singling out given results. Note: function is not tested and is used in examples. @@ -225,5 +231,11 @@ def plot_singling_out(*, sin_out_res: SinglingOutResults, high_res_flag: bool = set_ticks(ax=ax, xlabels=set_n_cols) # Adding legend set_legend(ax=ax) + + if save: + plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") # Show plot - plt.show() + if show: + plt.show() + else: + plt.clf() diff --git a/leakpro/synthetic_data_attacks/singling_out_utils.py b/leakpro/synthetic_data_attacks/singling_out_utils.py index 6db9ffa0..f3c302ef 100755 --- a/leakpro/synthetic_data_attacks/singling_out_utils.py +++ b/leakpro/synthetic_data_attacks/singling_out_utils.py @@ -1,7 +1,9 @@ """Singling-out risk util functions.""" +import json import multiprocessing as mp +import os from itertools import repeat -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Self, Tuple, Union from pandas import DataFrame from pydantic import BaseModel @@ -24,6 +26,103 @@ class SinglingOutResults(BaseModel): res_cols: List[str] res: List[List[Union[int,float]]] + prefix: str + dataset: str + + def save(self:Self, path:str = "../leakpro_output/results/", name: str = "singling_out", config:dict = None) -> None: # noqa: ARG002 + """Save method for SinglingOutResults.""" + + id = f"{self.prefix}"+f"_{self.dataset}" + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "resultname": name, + "res": self.model_dump(), + "id": id, + } + + # Check if path exists, otherwise create it. + for _ in range(3): + if os.path.exists(path): + break + path = "../"+path + + # If no result folder can be found + if not os.path.exists(path): + os.makedirs("../../leakpro_output/results/") + + # Save the results to a file + if not os.path.exists(f"{path}/{name}/{id}"): + os.makedirs(f"{path}/{name}/{id}") + + with open(f"{path}/{name}/{id}/data.json", "w") as f: + json.dump(data, f) + + from leakpro.synthetic_data_attacks.plots import plot_singling_out + plot_singling_out(sin_out_res=SinglingOutResults(res=self.res, + res_cols=self.res_cols, + prefix=self.prefix, + dataset=self.dataset), + show=False, + save=True, + save_name=f"{path}/{name}/{id}/{self.prefix}", + ) + + @staticmethod + def load(data: dict) -> None: + """Load method for SinglingOutResults.""" + return SinglingOutResults(res=data["res"]["res"], + res_cols=data["res"]["res_cols"], + dataset=data["res"]["dataset"], + prefix=data["res"]["prefix"] + ) + + def plot(self:Self, + high_res_flag:bool = False, + show:bool = True, + save:bool = False, + save_path:str = "./", + save_name:str = "fig.png", + ) -> None: + """Plot method for SinglingOutResults.""" + from leakpro.synthetic_data_attacks.plots import plot_singling_out + plot_singling_out(sin_out_res=SinglingOutResults(res=self.res, + res_cols=self.res_cols, + prefix=self.prefix, + dataset=self.dataset), + high_res_flag=high_res_flag, + show = show, + save = save, + save_name = f"{save_path}/{save_name}", + ) + + @staticmethod + def create_results( + results: list, + save_dir: str = "./", + ) -> str: + """Result method for SinglingOutResults.""" + latex = "" + + def _latex( + save_dir: str, + save_name: str, + ) -> str: + """Latex method for SinglingOutResults.""" + + filename = f"{save_dir}/{save_name}.png" + return f""" + \\subsection{{{" ".join(save_name.split("_"))}}} + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{filename}}} + \\caption{{Original}} + \\end{{figure}} + """ + for res in results: + res.plot(show=False, save=True, save_path=save_dir, save_name=res.prefix) + latex += _latex(save_dir=save_dir, save_name=res.prefix) + return latex def check_for_int_value(*, x: int) -> None: """Auxiliary function to check a given integer value.""" @@ -48,7 +147,7 @@ def aux_singling_out_risk_evaluation(**kwargs: Any) -> Tuple[Optional[Union[int, verbose = kwargs.pop("verbose") #Get n_cols n_cols = kwargs["n_cols"] - #Return non if n_cols==2 + #Return non if n'_cols==2 #Note: this is because n_cols==2 takes A LOT of time. Seems algorithm is not good for predicates with len==2 if n_cols == 2: return None, None @@ -155,7 +254,9 @@ def singling_out_risk_evaluation( #Instantiate SinglingOutResults sin_out_res = SinglingOutResults( res_cols = res_cols, - res = res + res = res, + prefix = get_singling_out_prefix(n_cols=n_cols), + dataset = dataset, ) #Save results to json if save_results_json: From b9117633f5853ab0704c8b92096e736af799630c Mon Sep 17 00:00:00 2001 From: henrikfo Date: Wed, 20 Nov 2024 00:39:09 +0000 Subject: [PATCH 08/14] added example ... --- .../___report_handler_anomalies.ipynb | 418 ++++++++++++++++++ .../report_handler_anomalies.ipynb | 203 +++++++++ 2 files changed, 621 insertions(+) create mode 100644 examples/report_handler/___report_handler_anomalies.ipynb create mode 100644 examples/report_handler/report_handler_anomalies.ipynb diff --git a/examples/report_handler/___report_handler_anomalies.ipynb b/examples/report_handler/___report_handler_anomalies.ipynb new file mode 100644 index 00000000..3411063a --- /dev/null +++ b/examples/report_handler/___report_handler_anomalies.ipynb @@ -0,0 +1,418 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "95d5acad-514e-4950-94a0-c80d789d9364", + "metadata": {}, + "source": [ + "# Report handler examples" + ] + }, + { + "cell_type": "markdown", + "id": "71f5dbe9", + "metadata": {}, + "source": [ + "Install leakpro as ``` pip install -e /path/to/leakpro ```" + ] + }, + { + "cell_type": "markdown", + "id": "68b48ce8", + "metadata": {}, + "source": [ + "### Synthetic examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "import pandas as pd\n", + "\n", + "sys.path.append(\"../..\")\n", + "\n", + "from leakpro.synthetic_data_attacks import plots\n", + "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", + "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", + "# from leakpro.metrics.attack_result import SyntheticResult\n", + "\n", + "#Get ori and syn\n", + "n_samples = 100\n", + "DATA_PATH = \"../synthetic_data/datasets/\"\n", + "ori = pd.read_csv(os.path.join(DATA_PATH, \"adults_ori.csv\"), nrows=n_samples)\n", + "syn = pd.read_csv(os.path.join(DATA_PATH, \"adults_syn.csv\"), nrows=n_samples)" + ] + }, + { + "cell_type": "markdown", + "id": "62c44504-a5fa-4846-8132-53877f369825", + "metadata": {}, + "source": [ + "### Get anomalies of synthetic data" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "0d25b9e7-03a7-4320-a456-6153774cb82c", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[Parallel(n_jobs=64)]: Using backend ThreadingBackend with 64 concurrent workers.\n", + "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 0.9s remaining: 28.1s\n", + "[Parallel(n_jobs=64)]: Done 64 out of 64 | elapsed: 4.1s finished\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unique predictions (array([-1, 1]), array([ 2, 98]))\n", + "Syn anom shape (2, 14)\n" + ] + } + ], + "source": [ + "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", + "print(\"Syn anom shape\",syn_anom.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ad69ece9", + "metadata": {}, + "outputs": [], + "source": [ + "sin_out_res = singling_out_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_attacks = syn_anom.shape[0]\n", + ")\n", + "# save_path = sin_out_res.save()\n", + "# print(save_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5e8fe664", + "metadata": {}, + "outputs": [], + "source": [ + "# from leakpro.synthetic_data_attacks.singling_out_utils import SinglingOutResults\n", + "# import json\n", + "\n", + "# with open(\"../../leakpro_output/results/singling_out/singling_out_n_cols_all_adults/data.json\") as f:\n", + "# data = json.load(f)\n", + "# syn_loaded = SinglingOutResults.load(data=data)\n", + " \n", + "# syn_loaded.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "373dcc8a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-20 00:09:42,566 INFO Initializing report handler...\n", + "2024-11-20 00:09:42,567 INFO report_dir set to: ../../leakpro_output/results\n", + "2024-11-20 00:09:42,569 INFO Saving results for singling_out\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../../leakpro_output/results\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from leakpro.reporting.report_handler import ReportHandler\n", + "report_handler = ReportHandler()\n", + "\n", + "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "1d91c7e0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-19 23:38:04,520 INFO No results of type GIAResults found.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../../leakpro_output/results\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-19 23:38:14,966 INFO PDF compiled\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "report_handler.load_results()\n", + "report_handler.create_results_all()\n", + "\n", + "report_handler.create_report()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89fcbe8a", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "markdown", + "id": "f0f7288c-a380-43df-97cb-fb0152e410a2", + "metadata": {}, + "source": [ + "### Singling-out risk analysis, Linkability riks analysis with anomalies and Inference risk, worst and base case" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "49ff9bb3-e7f4-4d3a-8759-382eed697893", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA5QAAAGHCAYAAADP6x1UAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6TUlEQVR4nO3deVyVZf7/8fdBhFA2RVM4IHxTwsQFpbDFMkt/hKnomGvrNKNptsxok3u22OaSkzo1KuUy6Tgqg2MkLqk1pbnnFC22jCiBGuICuLBevz98eMaToHgHHji+no/H/Xhw3/d1X/fnOoftfa773MdmjDECAAAAAOAyebi6AAAAAABA7USgBAAAAABYQqAEAAAAAFhCoAQAAAAAWEKgBAAAAABYQqAEAAAAAFhCoAQAAAAAWEKgBAAAAABYQqAEAAAAAFhCoAQAN2Sz2bRy5coq7fP5559XTEyMY/2RRx5R7969q/QcNVVERIT+/Oc/V3lbV/joo49ks9l0/PhxV5dSKRkZGbLZbNqzZ4+rSwEAlINACQC1TE5OjoYPH65mzZrJ29tbTZs2VXx8vDZv3uxoc/DgQSUkJFRrHW+++aYWLFhQreeoKr8Mw5drx44dGjp0aNUVdJ4FCxYoMDDwih1XFSo6d00P0+UpLi7W6NGj1aZNG9WvX18hISF66KGHlJ2dfUHbDz74QB07dpSPj48aNGhw1bygAgAX4+nqAgAAl6dv374qKirSwoULdd111+nw4cPasGGDcnNzHW2aNm1a7XUEBARU+zlcraioSF5eXmrcuLGrS0E1OXXqlHbv3q2JEyeqXbt2OnbsmJ5++mn16tVLO3fudLRLTk7WkCFD9Morr+iuu+5SSUmJ0tPTXVg5ANQQBgBQaxw7dsxIMh999NFF20kyKSkpxhhj9u3bZySZ5ORkc+eddxofHx/Ttm1bs2XLFqdj5s6da0JDQ42Pj4/p3bu3mT59ugkICHDsnzRpkmnXrp1j/eGHHzaJiYmO9c6dO5snn3zS/OlPfzINGjQwTZo0MZMmTXI6xzfffGNuu+024+3tbW644Qazfv16p1rLc+bMGfPkk0+axo0bG29vb3PbbbeZ7du3O/bPnz/fqU5jjElJSTHn/sTNnz/fSHJa5s+fX+65zo1p8uTJJjg42ERERBhjjAkPDzczZswwxhhTVlZmJk2aZMLCwoyXl5cJDg42Tz75pKOP89saY8y8efNMQECA+fDDDy8436ZNmy6o7dxjdvToUfPggw+awMBA4+PjY+655x7z3XffXfK4RYsWmdjYWOPr62uaNGliBg0aZA4fPnzBOY8dO1bhYz59+nTTunVrU69ePRMaGmqGDx9u8vPzL3ruzp07X7DdGGOOHDliBg4caEJCQoyPj49p3bq1WbJkidP5SktLzeuvv26aN29uvLy8TFhYmJk8ebIx5n/fv59//rkxxpiSkhLz29/+1kRFRZn9+/df8vmwYvv27UaS2b9/vzHGmOLiYmO3201SUtKv6hcA3BGXvAJALeLr6ytfX1+tXLlShYWFl3Xs+PHj9cwzz2jPnj26/vrrNWjQIJWUlEiSNm/erGHDhunpp5/Wnj171K1bN7388suXXd/ChQtVv359bdu2TVOmTNGLL76o9evXS5JKS0vVu3dv1atXT9u2bdPcuXM1fvz4S/b57LPPKjk5WQsXLtTu3bvVokULxcfH6+jRo5WqacCAARo1apSio6N18OBBHTx4UAMGDKiw/YYNG7R3716tX79eqampF+xPTk7WjBkzNGfOHH3//fdauXKl2rRpU25fU6ZM0ZgxY7Ru3TrdfffdF+y/9dZb9ec//1n+/v6O2p555hlJZ9+junPnTq1atUqfffaZjDHq3r27iouLL3pccXGxXnrpJf3nP//RypUrlZGRoUceeaRSj9U5Hh4emjlzpr766istXLhQGzdu1LPPPnvRmv/5z38qNDRUL774omO7JJ05c0axsbH64IMPlJ6erqFDh+rBBx/U9u3bHecbO3asXnvtNU2cOFFff/21lixZoiZNmlxQV2Fhofr166c9e/bok08+UbNmzS75fDz//POKiIi4rPGfOHFCNpvNcVnv7t27lZWVJQ8PD7Vv317BwcFKSEhghhIAJGYoAaC2WbFihWnQoIG55pprzK233mrGjh1r/vOf/zi1UTkzlOfPrnz11VdGkvnmm2+MMcYMGDDA3HvvvU593H///Zc9Q9mpUyenPm666SYzevRoY4wxaWlpxtPT0xw8eNCx/1IzlAUFBaZu3bpm8eLFjm1FRUUmJCTETJkyxRhz6RnK8mqvyMMPP2yaNGliCgsLnbafP+s4ffp0c/3115uioqJy+zjX9tlnnzXBwcEmPT39oucsr/7vvvvOSDKbN292bDty5Ijx8fExy5Ytq/C48uzYscNIumCG8WIzlL+0fPlyExQUdNGajblwdrYi9957rxk1apQxxpi8vDzj7e1t5s2bV27bc9+/n3zyibn77rtNp06dzPHjxx37L/V8zJo1y9x1112XrOmc06dPmw4dOpjBgwc7tv397383kkyzZs3MihUrzM6dO82gQYNMUFCQyc3NrXTfAOCOmKEEgFqmb9++ys7O1qpVq3TPPffoo48+UocOHS55g5y2bds6vg4ODpYk/fzzz5KkvXv3Ki4uzqn9L9cr4/xznDvP+ecICwtzen/npc7x448/qri4WLfddptjW926dRUXF6dvvvnmsuurjDZt2sjLy6vC/f369dPp06d13XXXaciQIUpJSXHM9J4zffp0zZs3T59++qmio6Mvu4ZvvvlGnp6e6tixo2NbUFCQoqKiLjnuXbt2qWfPnmrWrJn8/PzUuXNnSdKBAwcqff4PP/xQd999t+x2u/z8/PTggw8qNzdXp06duuyxlJaW6qWXXlKbNm3UsGFD+fr6au3atY56vvnmGxUWFpY7g3u+QYMG6eTJk1q3bp3T+3cv9Xw88cQT2rBhQ6VqLS4uVv/+/WWM0dtvv+3YXlZWJunsLH/fvn0VGxur+fPny2azafny5ZV+LADAHREoAaAWuuaaa9StWzdNnDhRW7Zs0SOPPKJJkyZd9Ji6des6vrbZbJL+949yVTn/HOfOU9Xn+CUPDw8ZY5y2FRcXW+6vfv36F90fFhamvXv36q233pKPj48ef/xx3XHHHU7nvP3221VaWqply5ZZrsOKkydPKj4+Xv7+/lq8eLF27NihlJQUSWdvMFQZGRkZ6tGjh9q2bavk5GTt2rVLf/nLXy6rj/NNnTpVb775pkaPHq1NmzZpz549io+Pd/Tl4+NTqX66d++uL774Qp999pnT9so8H5VxLkzu379f69evl7+/v2PfuRdgWrVq5djm7e2t66677rKCOgC4IwIlALiBVq1a6eTJk5aPj4qK0o4dO5y2/XL914qKilJmZqYOHz5c6XM0b95cXl5eTh+JUlxcrB07djj+uW/cuLHy8/Odxv/Lzyz08vJSaWlpFYziLB8fH/Xs2VMzZ87URx99pM8++0xffvmlY39cXJzS0tL0yiuvaNq0aRftq7zabrjhBpWUlGjbtm2Obbm5udq7d69j3OUd9+233yo3N1evvfaabr/9drVs2dIxQ1xZu3btUllZmaZPn66bb75Z119//QUfoVHR41ne9s2bNysxMVEPPPCA2rVrp+uuu07fffedY39kZKR8fHwuOYs4fPhwvfbaa+rVq5c+/vhjp32Xej4u5VyY/P777/Xhhx8qKCjIaX9sbKy8vb21d+9ep2MyMjIUHh5e6fMAgDviY0MAoBbJzc1Vv3799Oijj6pt27by8/PTzp07NWXKFCUmJlru98knn9Qdd9yhN954Qz179tTGjRuVlpbmmMmsCt26dVPz5s318MMPa8qUKcrPz9eECRMkqcLz1K9fX8OHD9ef/vQnNWzYUM2aNdOUKVN06tQp/e53v5MkdezYUfXq1dO4ceP01FNPadu2bRdc/hsREaF9+/Zpz549Cg0NlZ+fn7y9vS2NY8GCBSotLXWc97333pOPj88FweLWW2/V6tWrlZCQIE9PT/3hD38ot7+IiAgVFBRow4YNateunerVq6fIyEglJiZqyJAhmjNnjvz8/DRmzBjZ7XbH81zecc2aNZOXl5dmzZqlYcOGKT09XS+99NJlja9FixYqLi7WrFmz1LNnT23evFl//etfL1lzvXr1FBERoX//+98aOHCgvL291ahRI0VGRmrFihXasmWLGjRooDfeeEOHDx92BONrrrlGo0eP1rPPPisvLy/ddtttysnJ0VdffeV4js958sknVVpaqh49eigtLU2dOnW65PMxe/ZspaSkVBhYi4uLdd9992n37t1KTU1VaWmpDh06JElq2LChvLy85O/vr2HDhmnSpEkKCwtTeHi4pk6dKunsJbcAcFVz9Zs4AQCVd+bMGTNmzBjToUMHExAQYOrVq2eioqLMhAkTzKlTpxztVM5Nec597IIx//v4kU2bNjm2zZ0719jtdsfHhkyePNk0bdrUsb8yN+V5+umnnepNTEw0Dz/8sGP93MeGeHl5mZYtW5r333/fSDJr1qypcMynT582Tz75pGnUqFG5HxtizNmb8LRo0cL4+PiYHj16mLlz5zrdlOfMmTOmb9++JjAwsFIfG/JL599sJiUlxXTs2NH4+/ub+vXrm5tvvtnpI0F+eWOajz/+2NSvX9/MnDmzwjEOGzbMBAUFlfuxIQEBAcbHx8fEx8c7PjbkYsctWbLEREREGG9vb3PLLbeYVatWOT3/lbkpzxtvvGGCg4Md5120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P//8sw4fPuzqMq6YAwcOKDMz86r4H1I6+0Lf119/7eoycAkEyivo/fffV7du3fTcc89Jcv9QuWnTJv3xj3/UnDlztH79euXn5+vzzz93dVnVwsPDQ4GBgRo0aJBiY2P16aef6p///KckyWazubi6qrdlyxY99NBDevfdd3XXXXcpLy9Phw4dcut/YDZt2qSJEyfqN7/5jXJzc/X1118rLy/P1WVViw8//FCPP/643nvvPX377bfat2+fvvnmG1eXVW2OHDmit956S0uXLtWqVatUVlamLVu26PTp064urVrk5OTo7bff1pIlS/Svf/1LQUFBGj9+vFJSUtwyVCYnJ+u+++7TwIEDNXnyZK1atcrVJVWbNWvWaMSIEZKkOnXquPXvZOnsVSP9+/d3vJjr7pfop6amasCAAfrd736nV199VSUlJa4uqdoYY3TgwAElJibqmWee0Z49e5z2oWYhUF4hBQUFeumllzR48GCdOHFCzz//vCT3DpWBgYH661//qo4dO+rgwYP6+uuvNW3aND3xxBPaunWrq8urFk2aNNHRo0cVFRWlbdu2acaMGUpKSpLkXr8Ag4KC5OnpqcOHDys/P1+/+c1v9Pvf/14PPfSQNmzY4OryqkVBQYGKi4tVUlKiPn36aPTo0RoxYoSWLVvmVs+tdHZmY9GiRWrdurXOnDkjSY4/5u42VkmqW7euCgsL9e233yo/P1+7du3S7Nmz9dhjj7nl81u3bl2dOXNGR44ckSSNGTNGfn5+2rZtm9u96FdQUKAZM2Zo5syZWrp0qZo2baq1a9dqwYIFri6tym3fvl1Dhw5Venq6Bg0aJMm9Q+XHH3+sMWPGaObMmZo3b542btyoNWvWuLqsarN27VqNGzdOs2fP1vz587V27VrHVW/uyGazqVmzZkpMTFSHDh308ssva9u2bY59qFkIlFeIr6+vVq5cqccff1z333+/fvrpJ6dQ6W7/sEhS+/bt1aVLFxljHJeRLV68WKGhoZo1a5ZbvbJ27vmLj49XZGSkHn30URljNG7cOMf7sdzpF2BUVJQ++OADPf7442rVqpUGDBiglJQUdezYUQsXLnTLmZ1BgwZp6dKleuCBBzRkyBC9//77SkhI0Lp16xz/mLuL+Ph43XrrrSorK5Pdble/fv00btw4fffdd271fXxOQECARo0apddee0333HOPfve73yk5OVkJCQlKS0tzu/dUBgYGavDgwVq0aJGWLl2ql156SU2bNlX9+vX1zjvvuLq8KmWM0bXXXqtGjRopNDRUffr0Ubdu3bRz506tXr3a1eVVqaKiIj3//PPasGGD6tWrp4EDB0py31B58OBBDR8+XG3btlWbNm00dOhQrV+/XmVlZW75P9Xx48f12muvKTY2Vt7e3jp8+LDGjx+vGTNmuOULuUVFRSotLVWdOnUUHByshIQE/eUvf9F7772n5ORkSe75AmdtRaC8gkJCQtSoUSPdeOONeuyxx5xC5Y8//qiCggLXFlhNbDabJk6c6LjUd8yYMTpy5IgyMjJcW1gVOvdPdsOGDbVnzx797W9/U3Jysn7/+9/r4MGD+uCDD1xcYdVr166dUlNTNXr0aA0ZMkR169bVU089pZ9//tmtnttz4uLi9Jvf/Ebp6emO99cNHjxYOTk5+vbbb11cXfXw8Dj7J6J3794aPHiwPv30U0nu+d6snj17av369br99tsVFxcn6eyLCDk5OW55ue+AAQPUpUsXrV27VidOnNDChQv13HPPKScnR8XFxa4u71c7N3Pj5+enmJgYPfbYY8rJyVFQUJA6deqkG264QV988YWLq6wa58baqVMn9e3bV56enpoxY4Z8fX01YMAAGWNUp04dHT161MWVVo1z4x04cKAGDx4s6ezvJH9/f2VnZ8tms8lms+nkyZOuLLPKHDhwQNLZn9nu3burpKRETz/9tB588EG9+OKLKi0t1Zo1axxXk9R2555fLy8v1alTR/369dM111yjRx99VAEBARo2bJjjRVx3fIGztiJQuoCHh4fatWunxx57THl5eercubO6d+/uNr8MylOnTh3H1ytWrNDx48fVoEEDF1ZUPex2u4KDgzVx4kTNmjVLs2bN0k033aTY2FhXl1YtWrVqpSeeeMKxnpycrCNHjqhRo0YurKp6eHp6ql+/fho8eLCWLVumFStWaMWKFfrpp5903XXXubq8ahcZGem4TPD8n2d3EhgYqC5dumjlypXauHGj3n//fWVnZ6tFixauLq3KBQYG6v7771dSUpJmzpwpSZo/f75ycnJq/QsG5+5XMGHCBEnSc889p44dO+pPf/qTcnJy1KhRI/Xo0UNr1qyp9bPP58Y6ceJESWdn28vKyuTv769p06bJz89Pjz32mObNm6eZM2eqqKjI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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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Tp06qWXLltq9e7e2bduml19+OcvR0t27d6tZs2bq0aOHZsyYkWWdXbt26fr163r88cfNx6pVq6aKFStq27ZtWcaXkpIiSRajJ3Z2dnJ2dtbmzZst6n777bcqU6aMatWqpREjRujKlSs59v25557TwoULNW3aNO3fv1+zZs2Sq6urJCk+Pl5hYWFq1KiR/vzzT82cOVNffPGF/vOf/1i0MX/+fJUqVUrbt2/XhAkT9O6775pf299//11t27ZVly5dlJiYqKlTp2aKITk5We3atVONGjW0a9cujR07VoMHD7aoc+HCBT366KOqV6+edu7cqbVr1+rkyZPq0qVLrmNJT09XaGiotmzZom+++Ub79u3Thx9+KHt7e0k3R63atm2rzp07a/fu3fruu++0efNm9e/fP8ef4a0yplb++OOPSkxM1LJlyyRJU6dO1eTJkzVp0iTt3r1bISEh6tChg3nacnbfd+nSJfXu3VubN2/Wb7/9psDAQIWFhenSpUu5juny5csaOHCgdu7cqY0bN8rOzk7h4eFKT0/P9tyDBw9Wly5d1LZtW/P7rEmTJpIkNzc3zZs3T/v27dPUqVM1e/ZsTZkyxXy+1atXKzw8XGFhYYqOjtbGjRsVHBycZWwTJkzQ8OHDtX79ej322GNKTExU9+7dze/nqKgoPfXUUzIMQ9L/v5/62LFjue7/lStXdP36dZUuXVrSzetg9erVevjhhxUSEqJy5cqpcePGFveh5uZ9um3bNtWuXVsPPPCAuU5ISIiSkpL0119/5To+AEWYrTNaALCGNSOUt45I/fXXX4YkY//+/YZh/P8Ryr///tvw9/c33njjjSxH3m5Vs2ZNY/r06eb9SpUqGW3btrWo07VrVyM0NDTHuLIaobw1ptuFhoYar776qnn/9ddfN1q1apVtnBntrlixwuL4raOOZ8+eNSQZUVFRWbaRUXfLli2Gl5eXMWnSpGzPZxiG8e233xpOTk6Zjjdq1MgYOnRolt+TmppqVKxY0XjmmWeMc+fOGSkpKcaHH35oSDKeeOIJc71Zs2YZa9euNXbv3m188803hp+fnxEeHp5tLBkjn7ePKGd4++23japVq1q83p988onh6upq3LhxwzCMm6OCzZo1y9SXYcOGmfc7duxoHpnMcOvrPWvWLMPb29u4evWquXzmzJkW18B7771n0VfDMIy4uDhDknHgwIFcxbJu3TrDzs7OXP92L774ovHyyy9bHPv1118NOzs7i9hycvu1m8HX19d4//33M8X22muv5fh9t7tx44bh5uZmMaqqO4xQ3u706dOGJGPPnj05nvtOv0syTJw40WjQoIF5/5FHHjF69uyZbf2MEcqhQ4ca5cuXtxh537VrlyHJPAPgdtu3bzeqVq1q/PPPP3eMK8Orr75qPPjgg+bXMDEx0ZBkuLi4GP/973+N6OhoY/z48YbJZDK/z3PzPu3bt2+ma/Ly5cvmWQ8AwAglgGLj1lG58uXLS5JOnTplPnb16lU1b95cTz31lKZOnWox8pacnKzBgwerevXq8vT0lKurq/bv359phPKRRx7JtL9///487Uffvn21cOFCXbt2TampqVqwYIFeeOGFO35fw4YNsy0rXbq0+vTpo5CQELVv315Tp05VYmKiRZ3Y2Fi1adNG77zzjgYNGnTP/bido6Ojli1bpoMHD6p06dJycXHRpk2bFBoaKju7///n6uWXX1ZISIhq166tnj176quvvtLy5ct15MiRLNuNiYmRvb29WrZsmWX5/v379cgjj1i83k2bNlVycrL++ecf87HbR3XLly9vcf3cScbI8K0jsLdfL3/++ac2bdokV1dX81atWjVJsuhfTrHExMSoQoUKevjhh7OM488//9S8efMszhESEqL09HQdPXo01/25XVJSkhISEtS0aVOL402bNr3je+DkyZPq27evAgMD5eHhIXd3dyUnJ2d6f+Xk0KFD6t69ux588EG5u7srICBAku6qjVt99913atq0qXx8fOTq6qpRo0ZZtBUTE6PHHnssxzYmT56s2bNna/PmzapZs6b5eN26dfXYY4+pdu3aeuaZZzR79mydP3/eXB4cHKy///5bfn5+uYr1ww8/1KJFi7R8+XLz9ZUxMtuxY0cNGDBAQUFBGj58uNq1a6fPPvss1z8HALgTEkoAxYajo6P564zkIeOfLklydnbW448/roiICIuFLaSbK6IuX75cH3zwgX799VfFxMSodu3aSk1NLZjgb9G+fXs5Oztr+fLlWrVqla5fv66nn376jt9XqlSpHMvnzp2rbdu2qUmTJvruu+/08MMP67fffjOXly1bVsHBwVq4cKGSkpJybMvHx0epqamZHoFy8uRJ+fj4ZPt9DRo0UExMjC5cuKDExEStXbtWZ8+e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bR1OvklPT9eFCxfUsWNHTZkyRatWrTKXFdWFlNevX6+3335bgYGBtg6lQKxfv14jRoxQ1apVbR1Kgcjo7x9//KH33nvP1uHkux9++EHPPvusZs2apddee63IJ9ErVqxQhw4dNHToUL355pvatGlTkf1dJUlxcXH6559/bB1Ggbpx44atQygQp06d0smTJ20dRoGJjY1VXFyc0tPTbR1KgYiJidG+fftsHQbugISyAG3dulVDhgzRrFmz9Msvv+jChQsaOnSorcPKN5s2bdKAAQM0a9YsbdiwQZcuXVJ0dLStw8oXdnZ28vT0VPfu3dWgQQNt3rxZy5YtkySZTCYbR5f3tm7dqueee05ffvmlHn30USUlJenEiRNF+h+YTZs2afTo0Xrqqad09uxZ7du3T0lJSbYOK1/8+OOPeu211/TNN9/o77//1tGjR7V//35bh5Vvzpw5o08//VSLFi3SypUrlZ6erq1bt+rq1au2Di1fnD59WjNnztSCBQv0ww8/yNvbWyNHjtTy5cuLZFK5dOlSPf300+rWrZv+85//aOXKlbYOKd+sXbtW/fr1kyTZ29sX6d/J0s1ZI126dDF/mFvUp+hHRESoa9euevHFFzV+/PgiPShhGIZiY2PVsWNHDR48WDExMRZlKFxIKAvY8OHD1ahRI0nS5MmTdeHCBdsGlI88PT312WefqXHjxkpMTNS+ffs0adIk9e/fX7/99putw8sXDzzwgM6dO6eqVatq+/btmjJliubMmSOpaP0C9Pb2loODg06ePKlLly7pqaee0ksvvaTnnntOGzdutHV4+SI5OVnXr19XWlqawsPDNWzYMPXr10+LFy8uUq+tdHNk46uvvlKtWrV07do1STL/MS9qfZUkR0dHpaSk6O+//9alS5e0a9cuzZgxQ6+88kqRfH0dHR117do1nTlzRtLNv0tubm7avn17kfvQLzk5WVOmTNG0adO0aNEi+fj4aN26dZo3b56tQ8tzO3bs0Msvv6y9e/eqe/fukop2Uvnzzz9r+PDhmjZtmmbPnq2ffvpJa9eutXVY+WbdunV6++23NWPGDM2dO1fr1q1TXFycrcPKNyaTSRUrVlTHjh1Vv359vf/++9q+fbu5DIULCWUBaty4scLDwyXd/KfMMAz9+eef5lGOovZpeL169dS6dWsZhmGeRvbtt9+qQoUKmj59epH6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Fi1ayNHRUW+99Za8vLy0evXqTO04Ojrq7NmzOnTokOzt7RUUFKTSpUvnKl4AALJCQgkAKJZKliypMWPG6O2339aNGzcylVesWDHH7z99+rR69Oghf39/ubu7q0WLFkpJSdGlS5dyHcOLL76oY8eOqWXLlqpfv3629Y4fP67AwMAsy/755x8FBARYHKtcubL++eefTHWHDBmi5s2bq0uXLvLx8dGbb76pq1ev5jpeAABuR0IJACi2XnzxRaWnp2v+/PmZyuzscv4TOWLECF25ckV//PGHkpKSzPdjGoYhSXJ1ddWVK1fM9RMTE7M8f/v27bV9+3atXLky23NVqlRJhw8fzrKsQoUKmVanPXbsmCpUqJCprqurqz766CMdOHBA27Zt08aNG/Xpp5/m2E8AAHJCQgkAKLbs7e31/vvv64MPPrjr701KSpKLi4s8PT119uxZjRs3zqK8fv36WrZsmS5evKhTp05pwoQJFuXTpk3TwYMHNX/+fH355Zd68cUXlZCQkOW5+vbtq4ULF2r58uVKS0vTxYsX9dtvv0mSevXqpW+//VZbtmxRWlqapk+frrNnzyosLCxTOxERETp48KDS09Pl7u4uR0dHOTg43HXfAQDIQEIJACjWOnfurCpVqtz1940bN06HDx+Wl5eXmjZtqtDQUIvyAQMGqHz58vL399ejjz6qrl27mst2796tUaNGme+bbNeunXr06KFnn302y3sw69evr6VLl+r9999X6dKlVb16df3888+SpJYtW2r69Ol68cUX5e3trUWLFikyMlKenp6Z2jl8+LDatm0rNzc31ahRQ4888oheffXVu+47AAAZTEbG3BwAAAAAAO4CI5QAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAqJJQAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAqJJQAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAq/w8PPyP3AXXc+gAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA5QAAAG0CAYAAABNBWhXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACR40lEQVR4nOzdd1gUV9sG8HsFARVBxQoasSB2scaOHbGg2GPviTX2EntJbEGNLcaGvQsWBLvYGyp2EREFARsqCEp/vj98dz5XRWVFl8X7d1176c6cnX0OU3aeOWfOqEREQERERERERJRCGXQdABEREREREeknJpRERERERESkFSaUREREREREpBUmlERERERERKQVJpRERERERESkFSaUREREREREpBUmlERERERERKQVJpRERERERESkFSaUREREREREpBUmlESklx49eoSGDRsiS5YsyJYtm67D+W4mT54MOzu7VC9LpLZ69Wq936fu378PlUoFX19fXYfyxVQqFXbu3KnrMIiIUowJJRHpXPfu3dGyZcsUfWbevHkICwuDr68v7ty5820CS4NGjBiBw4cP6zqM78ra2hrz58/XdRg/jPbt26f6PuXt7Q2VSoWXL19+l8+lhuS+u06dOhgyZMh3j+drvXr1CkOGDEHBggWRKVMmVK9eHRcuXNAo4+bmhkaNGsHCwiLZhLxOnTpQqVQar99+++2DcqtXr0bZsmVhYmKC3LlzY8CAAd+qakSkY4a6DoCISBsBAQGoWLEibGxstF5GXFwcjIyMUjGqb0dEkJiYCFNTU5iamuo6nFSjT+vgRxAfH49MmTIhU6ZMug6FUlnv3r1x/fp1rFu3DpaWlli/fj0aNGiAmzdvwsrKCgAQHR2NmjVrol27dujTp0+yy+rTpw+mTp2qvM+cObPG/Llz58LFxQVz5szBzz//jOjoaNy/f/+b1IuI0gAhItKxbt26SYsWLZT39vb2MmjQIBk5cqRkz55d8uTJI5MmTVLmFyxYUAAor27duomIyIsXL6RXr16SM2dOyZo1q9StW1d8fX2Vz02aNEnKlSsny5cvF2tra1GpVCn63Nq1a6VgwYJiZmYm7du3l8jISKVMYmKizJo1S4oUKSJGRkZSoEABmT59ujI/KChI2rZtK+bm5pI9e3ZxcnKSwMDAZP8mR48eFQDi6ekpFSpUkIwZM8rRo0eV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Rlw8rqQsn+Ior0REpBtZftK7BI+IiIg0scsrERERERERaYUJJREREREREWmFCSURERERERFphQklERF9Fz/y4Ab0edw+iIj0ExNKIiL6pjJmzAgAeP36tY4jobRMvX2otxciItIPHOWViIi+KQMDA2TLlg1PnjwBAGTOnFkZkp2+TExMTJpaTmp68+YNXr9+jSdPniBbtmwwMDDQdUhERJQCTCiJiOiby5s3LwAoSSV9mWfP3j6nMzAwME0sJ7U9e/YM9+/fBwBky5ZN2U6IiEh/MKEkIqJvTqVSIV++fMidOzfi4+N1HY7ecHR0BADcvn07TSwntTk6OuL27dvImDEjWyaJiPQUE0oiIvpuDAwMmDikwIMHDwAAJiYmaWI5qe3BgwdpLiYiIkoZDspDREREREREWmFCSURERERERFphQklERERERERaYUJJREREREREWmFCSURERERERFphQklERERERERaYUJJREREREREWmFCSURERERERFphQklERERERERa0VlC6eHhAVtbW9jY2GDFihUfzK9Tpw6KFy8OOzs72NnZ4c2bNzqIkoiIiIiIiJJjqIsvTUhIwLBhw3D06FGYm5ujYsWKcHZ2hoWFhUa57du3o3Tp0roIkYiIiIiIiD5DJy2U58+fR6lSpWBlZQVTU1M4OjriwIEDWi8vNjYWkZGRGi8iIiIiIiL6tnTSQhkaGgorKyvlvZWVFUJCQj4o17FjRxgYGKBLly4YNmxYssubMWMGpkyZ8k1i/d5UKhUAQETSxHJSXyrFMzl1FpNS//uzfl2ZyV+wEACqKZ8uJ5PS2rrVbyqVKt3ud6l5XElrdQP0e919yTHli8qlwnGFx5TUld5/z9PzcUXf1933PK7o4lyF52JpT5odlGfDhg24evUqvL29sWvXLuzduzfZsmPHjkVERITyCg4O/o6REhERERER/Zh0klBaWlpqtEiGhITA0tJSo4y6BdPc3Bzt2rXDhQsXkl2esbExzMzMNF5ERERERET0bekkoaxSpQquX7+OkJAQREVFwcvLCw4ODsr8hIQEPHv2DAAQFxcHLy8vlCpVShehEhERERERUTJ0cg+loaEhXFxcULduXSQlJWHUqFGwsLBAkyZNsGLFCpibm8PBwQHx8fFITExE8+bN0aZNG12ESkRERERERMnQSUIJAE5OTnByctKY5unp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D9ja2sLGxgYrVqzQmPf69Ws4OjqiePHiKFWqFBYuXKijKImIiIiIiCg5hrr40oSEBAwbNgxHjx6Fubk5KlasCGdnZ1hYWChlxowZA3t7e0RFRaFSpUpwdHRE0aJFdREuERERERERfYROWijPnz+PUqVKwcrKCqampnB0dMSBAweU+ZkzZ4a9vT0AwNTUFLa2tggLC0t2ebGxsYiMjNR4ERERERER0belk4QyNDQUVlZWynsrKyuEhIR8tGxwcDCuXr2KChUqJLu8GTNmwNzcXHkVKFAg1WMmIiIiIiIiTWl6UJ7Y2Fi0b98ec+bMQZYsWZItN3bsWERERCiv4ODg7xglERERERHRj0kn91BaWlpqtEiGhISgSpUqGmVEBF27dkWTJk3Qpk2bTy7P2NgYxsbG3yRWIiIiIiIi+jidtFBWqVIF169fR0hICKKiouDl5QUHBweNMmPHjkXmzJkxfvx4XYRIREREREREn6GThNLQ0BAuLi6oW7cu7OzsMHz4cFhYWKBJkyYIDQ3Fw4cPMWvWLJw/fx52dnaws7PD/v37dREqERERERERJUMnXV4BwMnJCU5OThrTPD09lf+LyPcOiYiIiIiIiFIgTQ/KQ0RERERERGkXE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0r6JJVKpesQvhmVSpWu60f6Kz1vl+l9v0vP9UvPdfsRpOd1l963zfRcN0ofmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWdJZQenh4wNbWFjY2NlixYsUH8wcMGIA8efKgUqVKOoiOiIiIiIiIPkcnCWVCQgKGDRuGI0eO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adOUlgV98O462LVrl5ibm0uPHj2Ue7QSExPF1dVV+vbtK1u3btXLdaZ27NgxcXd3l8uXL0tSUpLMnTtXRo0aJTt27JAzZ87I77//LqdOndJ1mF/txIkT4ubmJt7e3lKpUiW5d++eiOh3q+v8+fPl559/lnbt2kmnTp3kwoULcuHCBbGxsdHre+4SExMlLCxMnJ2dpXr16nLp0iVl3n///SetW7fW61Y7dctHjx49xNLSUhYvXiwi/3/cmTlzplSuXFlevXqlt60k27dvl3bt2klgYKDcv39f4767IkWKSN++ffX2ItXhw4elUKFCGsfF4OBgadasmUyfPl3vEq1PefPmjZw/f16ePXsmIm9Hem3YsKHSGqsv1PvW4cOHpV+/fjJv3jzx9fWVpKQkuXz5svzzzz9StGhR6dmzp95epPpRMKH8BjZt2iSOjo4yb948qVWrlsZw1Pr2I/To0SOpVq2a8ry32bNnS/369eXs2bPy+PFj8fb21uvhtuPi4uTEiRPSqVMnWbFihbx580YaNWokvXv3lokTJ0q5cuX0KuF614sXL6R169bSqFEj5flTr169kjp16kiPHj00yupD4vWxGO/fvy+LFy+WQYMGydmzZ0Xk7WNsli9fLk+ePPneIX61d3sEFChQQAYNGiRNmjSRlStXiojIypUrpWvXrlKkSBHx8PDQZahf7cKFC3L48GG5ffu25MmTR4oXL64MinHo0CEZNGiQxr00+iApKUn8/f2lUqVK8vTpUwkLC5O9e/dK8+bNJSQkRPbu3SvlypWTV69e6cU+J/Lx/c7Pz0+6dOkiixYt0rivdeXKlRIaGvo9w0sV6jqGhoYqra7h4eFibW0t06dPF5G395rfuXNHgoODdRbn1woJCZHChQtL7969RUTkyZMn0rhxY9m6dats3bpVOnbsKIGBgboNUguJiYkSHx8vI0eOlMWLF0tCQoKsXbtWOnToIMuXL5dHjx5JvXr1lItV6UliYqKsWLFCSpUqpXeNFWonTpyQggULiqurq4wcOVL69+8vq1at0pjfqlUrvXuk0o+GCeU38PLlS1m7dq20atVK46RPX04g3pWUlCQdOnTQuHewX79+YmNjo/fPpFKLjY2VEydOyC+//CKrV6+W+Ph42bFjhyxatEh5DqO+rbsbN27Is2fP5MyZM9K6dWtZv3690r0uMjJSqlevLlevXtW7eomIzJs3T3r37i3dunWTgIAAefHihcyYMUMGDRokQ4YMkZ49e2oMLqRvzp8/LyNGjFC6D549e1YZGVRNH0/6RDT3o02bNsmvv/4qkZGRMn/+fKlbt674+vrKvn37pEyZMhr3jKZl79YpMTFRHj16JI6Ojsq0ly9fyogRI2Tr1q0iIno3kqTa0qVL5ddff5W5c+fKgwcP5Pbt29KlSxdZvHhxujhRd3d3F3t7e2nevLksWrRIREQePHgg+fLlk379+kmZMmWUi1b6bMeOHWJlZaVsj+vWrZM2bdpImTJl9C4hef/3a9++fZI1a1apW7eu/PHHH7J582axt7eX2NhYZWTe9CY6OlpWrVqlV+djjx49kgMHDijHwo0bNyoD7zx//lz27t0rAwYM0Hhubbt27ZRReSltYkL5DanvsdDHk3aRt3HHx8crI9Wqf2xOnz4tFStWlCpVqmiMiKfP1Ellly5dlMFq9FV0dLRMmTJFunbtKuHh4XLs2DHp1KmTbNy4UTlA69vgSWoLFiyQevXqSUhIiNSrV09sbGzkxo0bEhUVJWvXrpXmzZvr9QA8Im+7SxYvXlwZnVDk7X2GtWrVkjlz5oiI/h5TRN4OXubv7y9Pnz6V0aNHK93TFi1aJPXq1ZP27dvrzYO4341v3bp1snz5chF5O0ptly5dlHmjR49W7tXWx31vwYIFUqdOHfH29pYGDRpIy5Yt5fr16+Lv7y8tW7aUZcuW6fU9hSdOnJAqVarIs2fPZPTo0VKgQAHlNoiQkBCZNWuWMlhbeuDp6SmVK1dWLnjr88PhDx48KAMHDpSDBw9KYmKi3LlzR4KCgkTk7S0fP//8s162mqdEWj9Ovm/NmjXSpk0b8fT0lJiYGHFzc5PixYsr6y0qKkoaNWqkjFIbFRUljo6Oejni8I+ECeU3pG87eXLu3LkjgwYNkl69eknfvn2lVKlScvPmTenZs6fePF/s3r17Gi08HxMXFyfe3t7SsWNHvamX2vvb2o0bN2T69Ony66+/yvPnz+XYsWPi7Ows69atk7i4OL3ZNt+NMy4uTmbMmCHPnj2TOXPmSPv27WXOnDlSuHBhJYmMi4vTVaipatGiRdKkSRONe9TOnj2rl4/kedfr16+lSZMmUrJkSTlz5owMGDBAmjZtqtz38/r1a728ELdgwQKpWLGictHtzZs34uzsLA0aNJDp06dLqVKllFGU9cG7Se/z589l6tSpEhkZKXPnzpX69evLvHnzxNnZWW7fvi13797V22e8qrcxT09POXv2rOzZs0eqVKkibm5uYmdnJ6NGjVLuUUtvvLy8pGjRohoDmOmbCxcuSPXq1WXkyJHSokULmT9/vtIFe/fu3VK8eHFxd3fXbZD0UcuWLZPu3buLh4eHxMXFyZw5c6Rdu3Zy584duXHjhlSpUkVj8CR9va/3R8KEkr4o2QoLC5NTp07JggUL5Pr163Ls2DGxtbXV6JKQlt24cUMsLCzk33///WS52NhYvb3x+9SpU8ooriIit2/flmnTpsnAgQMlIiJCjhw5olcj+L2bUBw/flxE3l6p9PPzk1q1ain3OpUvX15KlSolMTExetn686534589e7Y4OTkp9y/rO3U33T179oidnZ1s3LhRlixZItmyZZPBgwfr7cWAV69eSZcuXZRun++e+KxYsUJWr16tt1fW9+3bJxcvXpRz587Jo0ePlMdM3L59WypXriydOnXSyxM99bHl/ed//vbbb8ptDuoRh9WtJunRwYMH9ba78q1bt6Rs2bLK6J8HDx6UwYMHy+LFi8XPz09OnDihN6ND/yjeXw/r1q2TLl26iIeHh9y7d09mzpwpVatWlbp16yojYpP+YEJJX5xsqZ08eVKKFy+uN10L1SfpV69eFRsbG+X+mE+VVdOnH6JHjx6Jra2txii1Bw4ckJ9//ln69Omjt13SFi9eLCVLllQGwwgNDZXevXuLj4+PrFq1SqZMmaIMOpQevLsNTp8+XRwcHPT6nlCRt8+wHTx4sLRu3VpiY2Nl4MCBMnr0aAkLCxMHBwepXr263rRyvX9MSEpKkmbNmikje6tdunRJ7wZhe78Lb+7cuWXIkCHSpk0b+ffff6V///4iIrJhwwb57bff9HrgKw8PD2natKl0795dtm/fLi9fvpSZM2dK/fr1xcPDQ2rWrJkuRlFOr8LDw6VWrVpSu3ZtZdqRI0ekT58+Mm/evHRzO056s3//fvnrr7/E1dVVXr9+Le7u7tKtWzdl8MPo6GhlMDZ9Ov8iJpQ/vJQkW+8mJO9f2U2r1AckdevHtWvXpFixYh+c/In8/wi8z58/l5EjR36/ILWkrtu1a9fk/PnzEhERIa9evZJq1aopLZXnzp2THj166N1gC2rnz5+XihUraowkGR0dLSNHjpRff/1VChUqpJd1+9gP5buJ5Lv/Vz/3Tp8lJSVJQkKCDB48WHr16iWrV6+W9u3bS3h4uLx69Upv1uG76+XKlSty8eJFERFxc3OTyZMnKydFmzZtkubNm8vTp091Eqc23t0mg4KCZOXKlXL//n1JSEiQlStXipOTk1hYWMjAgQOlYMGCenNB8WPOnj0r9erVk2PHjsn8+fNl/PjxMmvWLAkMDJTx48dLgwYNNJ57R7qn3j7v3r0rFy9elPj4eHn27Jn07NlTOnfurJQ7ePCgcu8dpQ3qdXf16lUpWbKkTJs2Tfr16ydVqlSRyMhIcXNzkzZt2siePXv07iIc/T8mlD8wbZKtJ0+eyOjRo79fkF9BXb8jR47IuHHjlIfC+/n5ia2trfKMMZH/T5ZfvHghDRo0kMOHD3//gLWwe/duqVChgrRt21Z++eUXWbFihbx+/Vrq1asn3bp1E0tLS6Xe+ujmzZvy66+/isjb7VS9rUZFRUlMTIxetpCo7d+/X/7880/ZvXu3REZGisiHSaV6G9bnrrzvJ8+bNm2SSZMmiUql0ptjyfsWLFggZcuWlcKFC8uMGTPkwYMHsmDBAmnUqJG0atVKSpUqJdeuXdN1mF/s3XX0zz//SL169aR06dLKCd7Lly9l69atYm9vL1u2bJHw8HAdRvt1Hj9+LM2aNZNWrVop0w4dOiRdu3ZVLlypL5iyhSRtcXd3l/Lly0udOnWkZ8+esmHDBgkJCZFevXpJmzZtdB0efcKJEyekRYsWsmHDBmXapEmTlP3w33//lStXrugqPEoFTCh/UD9CsiXyduCBEiVKyNq1a6V8+fIyatQoefLkifj7+4uVlZX8888/Stnnz59Lw4YNlfv10rqYmBhp3LixnDx5UhITE+XKlSvSvn172bdvn7x+/VquXbumNy0/7zt16pQcPHhQXrx4IcWLF9d4JtXSpUvlr7/+0mF02lPvd9evX5fSpUvL77//LgMHDpThw4cr3XzUyaP6Is7Lly9l4cKFyj2jaZ26ju8/RPzdpDgmJka2bNmiN894PXnypBw6dEhE3j6AW/1YkLCwMLG3t5f58+fLmzdvJDw8XC5duiRhYWG6DFdrbm5u0qFDB7l165b07dtXhgwZohxDXr58KWvWrNHrewrVifCqVaukcOHCsnHjRmVe69atlUdpMJFMG9495kVEREjTpk2VCzXr16+XMWPGyMWLFyU8PFzatWsnvr6+ugqVPuPy5ctSoEAB6du3r4i83ceioqKkZ8+eens7DmliQvkDS4/J1rsnAi9fvpTu3bvLnTt35PDhw2Jrayv9+vWTESNGSEREhNy5c0cZCj4uLk5q166tDMiQVqnrd/r0adm5c6e0bNlS+YF9/fq1LF26VHk8gT5RJxuJiYkSEREhM2bMkIEDBypJcenSpeWPP/6QWbNmSYUKFfS6u92JEyekbdu2yqMxLl68KGPGjJGRI0cqLSPqH9iXL19K9erV5eTJk7oKVysHDx6URYsWfZBUqr27n+rDyfupU6ckODhYLl68KPPmzRMbGxtl1NZ79+5J3bp1ZezYsTqO8uuEhIRI0aJFlR4Bb968kd9//10GDx6snKjrw7r6mISEBHn8+LHkyJFDeb7rli1bpFmzZjJnzhy5evWqFC9ePN0MgJUeREREKKOUi7wd/Kpq1arKhZ3Xr19L//79Zdy4cSIiTErSqCtXroifn5+IvH1igI2NjSxevFgiIiLkxIkTUrRoUbl//77eHlvo/2UA/TBERPl/REQEtmzZgl27dsHKygqvX7/Gq1evMHv2bOTOnRtHjx5FuXLlAADx8fFo2bIl/vjjD9SqVUtX4X/W69evcfPmTQDAiRMnICKYNWsWMmbMiPHjx+P8+fP49ddfsXnzZsyePRsFCxaEvb09RAQZM2bEtm3bUKdOHd1W4jNUKhX27NmDAQMGoEiRIqhYsSJ69eqFR48eIVOmTMiWLRtu376NuLg4jfWd1mXI8PZQlJiYCDMzM7Ru3RpFixaFq6sr4uLisGfPHhgZGSE2Nhbr169HmTJldByx9kxNTXHmzBkcOnQIAGBnZ4d27dohLi4OkydPRlxcHAwNDfHy5Uu0atUKs2bNQo0aNXQc9Ze7ffs2/v33X9SoUQMmJiYfLZOUlKT8X6VSfa/QtFa9enUYGxujc+fOePLkCXr37g1XV1f4+/ujUKFCWL58Oa5evYpnz57pOlStWVpaYvbs2fD09MTWrVthYmKCOXPmIDo6Ghs3bkRcXJxerKuPUalUyJ07N5YuXYpevXrh4MGDaNeuHTp06IC5c+di0KBBcHV1RZUqVTS2TdIdMzMzTJo0CS9evICnpydMTU3Ru3dv7Nq1C5cuXUKmTJng6OiIyMhI5ZhJacvRo0fRokULDBo0CFOnTkXWrFmxe/duLFy4EC1atMD27dsxd+5cFCxYUG+PLfQO3eaz9L1ER0crXZeOHz8uL168kMePH0tgYKBUq1ZNIiIixNfXV/Lnzy/jxo1ThoJXXzVK6w89jo+Pl/DwcOnVq5cMGjRIrK2txcfHR0Tejrb4888/i8jbLr2Ojo5y8+ZNXYartYiICHFycpLTp08r06ZMmSJFixaV2bNnS+HChWXfvn06jDBlbt68Kd7e3vLmzRs5c+aM5M+fX7mf0N/fX2bNmiU9e/bUeB6jvlHvQ+pn9iUmJkpgYKAUK1ZMGVk5KSlJLl68qDx3KyYmRtq3b69XD1NPTEyUJ0+eSLVq1aRJkybJPlJI3ZU3IiJCduzYoVePC/Hy8pKaNWvKiBEjZOnSpTJ27FjlkSDppYVE/dD7LVu2iMjbeunL46E+5tatW7Jhwwal++TOnTslW7ZsynFyx44d0qlTJ9m5c6cuw6T/effe+BcvXsjWrVulUqVKcvDgQbl9+7bMnz9fKlasKBMmTJCffvpJaXGmtEH9e/fq1SuZMmWK+Pj4SEBAgEyZMkX++OMPCQ0NlXv37kmJEiVk5syZymfYQqn/eEnnB5CQkICYmBjMmzcPmTNnxp49e7B9+3ZUrFgRly9fRlJSEszMzJApUyaUKVMGnTp1gpGREYD/bz3InTu3LqvwSU+ePMG+ffvQtWtXNGzYEL1798aAAQNQsWJFAED58uWRN29eVKtWDeHh4XBxcUGJEiV0HLV2MmTIgIiICMTGxgJ429IzceJEmJmZoUyZMli7dq3etGaJCNzc3HD37l0YGhqiRo0aaNmyJWrWrImTJ0+iaNGicHR0xI4dO7Bjxw4UK1YMWbJk0XXYKSIiUKlU8PDwwNSpU1GgQAFkzpwZXbp0wf79+9G0aVPExcVh8ODBqFChgvK5uLg4zJo1CwULFtRh9F9GXUeVSoVcuXLh77//xtixY3H8+HG0bt1aaX0G3rZAGxgYICIiAg4ODpg3bx4yZsyow+hTpnHjxjAwMMCkSZNgaGiIvHnzYvPmzRg3bly6aSFxdHREhgwZMGjQIBgaGqJVq1bIkyePrsNKEfU2CQAXLlzAmTNnkCFDBjRv3hwtWrTA5MmT4ejoCG9vbzRv3hxxcXFwd3dH3bp1YWZmpuPof2ynT59GUFAQoqKi4OrqCh8fH8TGxmLWrFn4448/0LdvX5QvXx5BQUHYvHkzqlWrpuuQ6R0qlQq7d++Gp6cnLl68iObNm6Nw4cJwcnLC7t274eLigiFDhmDLli2wt7dH3rx50a1bN12HTalBt/ksfWuPHz+WNWvWiIjI5s2bxdTU9IORFVu0aCFVq1YVGxsbvRwq3dfXV+7cuSNPnjyRAwcOyP79+8XBwUFWrFghr1690iiXHm7aX7BggcycOVO5L+H06dPStWtXefbsmY4j+3LvDtAyaNAg6dGjh9KiPGzYMCldurRERUWJu7u7tGnTJs23kL8vIiJCeQ7a8+fPpWrVqnLu3DkJDw+Xo0ePipOTk1y5ckV8fX2lYMGCEhQUpFyh1acrtepYPT09pWfPnjJ27Fi5fPmynDx5UurUqSPu7u5Ki+S7D5OvV6+enDhxQmdxf61Dhw5JiRIlZOLEiXrzCKWU0teH3qu3s71798qcOXMkMTFRVqxYIcOHD1cG4bl8+bI0a9ZMuR8vKipK47eCdOfVq1fSpEkTyZYtm6xYsUKZvm7dOmnUqJFenqP8SC5cuCA1atQQT09P6dChg1SuXFnZJy9evCgTJkxQeoj5+vqKv7+/LsOlVMSEMp37UZKtqKgoGTFihEyePFliY2Pl4sWLUqdOHdm4caNs27ZNWrRooVdd6z4lODhYJk6cKA0bNpTx48dLoUKF9PbRIEuWLBEHBwepWLGi1KtXT86cOSMiIkOGDJFGjRpJ2bJl9W4AnpcvX8rcuXPl8ePHkpSUJPHx8dK0aVMl8YiKipKZM2fK/PnzlfL67ODBg1K5cmU5c+aMdOrUSZo1ayYibx8cX7lyZdm+fbtSNjo6Wpo0aSLHjh3TVbip5ujRoxIYGKjrMOgjvLy8pFy5ckryER8fLytWrJABAwZI+/btxdbWVhmAR58u4PwotmzZIu3bt5dZs2bJ5cuXlQuQa9askVq1aun146LSs4cPH0rXrl2la9euyrQuXbpItWrVlAuL6lta+LzJ9Eclokcjd5BWoqOjMXnyZJiammLs2LG4fv06hg8fjr59+yJjxoxYv349tm3bplddzwDNbk0AcPXqVaxduxY5c+ZE//79cffuXSxYsAAhISHo06cP2rVrp8NoU1d0dDQuXLiAhw8fomjRoqhataquQ/oi6nUmIrh//z46duyIw4cPI3PmzJg2bRr8/f0xZMgQVKhQAeHh4TAwMEC2bNl0HXaKJCUl4fHjx8iQIQMOHjyIzp07Y9CgQbh79y68vLwAAMuWLcPly5exePFiJCUl6XV3yZUrV6Jy5coIDg7G9OnTsWnTJlhbWwMADh48iKxZsyrbZ3h4OJ4+fYrixYvrMGJK70aNGoXatWujWbNmiI2NhbGxMRITE+Hv748zZ87A2toadevW1XWY9D/q34ULFy7AyMgIWbNmRYECBTB8+HBkzZoV/fr1Q3BwMExNTZE7d26964Kdnr17HhYREYENGzZg+/bt+PXXX9G+fXsAQNu2bREQEIBLly4hKSlJ4zYISj+YUKZTP0qytWfPHuzcuRNZsmRBv379kDlzZsyfPx/58uVDjx49YGFhgYiICGTPnv2Dvwl9X+/+/V+8eAERQePGjeHi4qKMHtyyZUuEhoZi/vz5qF69ui7DTbGYmBg8f/4clpaWePToEbZv347r16/DwcEBjo6OGDVqFM6cOYNevXph3rx5WLx4MRo0aKDrsFPs/f1owYIFWL16NczNzbFx40bky5cPe/fuxc2bNzFy5EilHE8k6FtRb5Ph4eGwsLBA165dUahQIUyZMkUpc+PGDZQqVUqHUdLHqI8Le/bswcSJE9GqVStcuXIF/fr1Q7Vq1TBmzBgkJiZiw4YN2LhxI5o0aaLrkOl/1Pvd0aNHcfv2bRQsWBCVK1fGnj174OvrC3t7e7Ru3RoAcOXKFeXJAZQ+MaFMx9J7snXr1i1069YNw4YNQ2hoKObNm4e9e/fC3Nwcs2fPRv78+TFkyBBkypRJ16HSOxYtWoQjR46gaNGiSEpKgpWVFezt7VGhQgWsXLkSe/bswfLly5ErVy5dh/rFRASXLl3CyZMnkZCQgGvXrmHWrFnw8vLClStXUL16dbRt2xaurq5QqVQoUKAA6tevr+uwtXbmzBk8ffoUP/30E0qXLo3WrVsjU6ZM2Lx5Mw4fPoz+/ftj4cKFaNSoka5DpR/Evn37sHv3bvz9998IDAzEH3/8gVatWqFbt244c+YMevfujS1btqB06dK6DpXwtrcCAFhYWMDPz09ZP7t27cLSpUtRqFAh/Prrr3BwcMDt27cRHx/PhCQNOnToEAYOHIg5c+agQ4cOWLhwIRwdHbF//36cPHkSjRo1Qrt27fTyHJNSRn/7WdEn3bp1C9OmTVOSrUaNGmHv3r0YMmQIZs+ejRUrVmDIkCHInj07AP14Fty7rly5gvHjx6NRo0bo0KEDACBPnjxo27Ytzpw5g06dOiFHjhxMJtMYd3d3bN68GXv27EHjxo1RuXJlREdHY/To0ShUqBBOnTqFnTt36lUyCbzdf6ysrHDu3DkcOHAAU6dORZ48edClSxckJibizJkziI+Pxy+//JLssxnTOvUJwblz59C1a1fUqlULMTExyJcvH9atW4cuXbqgXbt2CAkJwbx585hM0ndz9uxZDBw4EK6ursicOTOKFy+O33//HUOHDsWRI0fg4+OD2bNnM5lMI2JiYrB06VK8fPkSY8eORb58+bB06VIEBARg2bJl2Lp1K7Zv345x48bh2bNn6NKli/JZJiZpg4ggNjYWbm5uWL9+PUQExYoVQ+PGjZEvXz60aNECCQkJSq8ArrP0jwllOvQjJFs//fQTTExMcOXKFYSFhSFnzpzo1KkTTp48ifDwcL3rLvmjiIyMxMiRI7F7926Ym5tj0aJFiImJQdWqVRESEoIxY8agcOHCug4zRdQnOOpH02TOnBkhISE4c+YMqlWrhl69emHx4sW4dOkS6tSpA0tLS12HrBV116bFixdjzZo1qF69Oh49eoQJEyZg7dq12LVrF169eoU3b96k6ccMUfrj6+uLDh06oFatWkhKSoKBgQHq1auHEydO4OnTp0hMTESxYsWYjKQRJiYmqFatGo4cOYKFCxdi8ODBKFWqFFavXo1+/frB1tYWVlZWqFWr1gfdlLn+0gaVSgUTExNUqFABCxYsgJ+fH9zd3WFpaYlVq1ahePHi6N27t67DpO+IN7SkQ+8nW/Hx8ejUqRPq1aunJFv6NiiGumf25cuXcfHiRRgZGWHt2rXIkiULXFxccPz4cZw6dQp79uzB69evdRwtJadw4cIYNWoUVq5ciQMHDgAAFi9eDB8fH3Tu3Flvk8krV67g7t276NSpE5YsWYIsWbJg8+bNCAgIwOPHj1G0aFEMGjRIb5NJtfDwcLi5ueHGjRsAgBw5cqBdu3YICAgAAGTNmpXJJH1z6t+D4OBgBAcHo0SJEnj48CGePn2q3Kd76tQp3L59G0WKFEGxYsUAMBlJC5KSkgAA9erVQ7NmzfD69WvMnz8fz58/R9asWbFw4UIsW7YMkydPRvv27TWez0tpT6ZMmXDz5k3MmDED1tbWuHbtGv7++2+8efNG16HRd8Z7KNMB9Unt5cuXkZSUhOLFiyNDhgzo0aMH8ufPD0dHR5iYmKB9+/bYu3ev3t6HsGvXLkybNg01atRAdHQ0+vbtizJlyuDXX3/FjRs3UKNGDTRp0gSNGzfmleg0KioqChMnTkTmzJnh6OiIwMBAzJs3D2vWrNHb7mheXl4YNmwYunXrhq1bt2LHjh3IlSsXFi1ahLNnzyrdeGvUqKHrUFPFmjVrMHDgQBw6dAg///wzvLy88Oeff8LDwwPm5ubc7+i72LlzJ6ZMmYJs2bIhPj4esbGx+O2332BnZwcDAwP06NEDK1asQMWKFXUdKv2P+nf5+vXriImJQcmSJXH16lV4eHjAxMQE48ePx5YtWxAYGIiyZctyAB49MW7cODx69AhPnz7F48ePMW7cODg5Oek6LPrOmFCmE+k92bpz5w769euHjRs3Ytu2bVi8eDFq1KiB7t27o3Llyujbty9y5MiBOXPm6PUjGH4EoaGh8PDwwO7du5E9e3aMGjUKZcqU0XVYWrl+/To6deoENzc3XLhwAaNGjUJSUhIOHz4MW1tbnD9/HomJiahWrZquQ/1q7x43Vq1ahT59+qB79+6IjY1F69at4ezsrOMI6Ufx5MkTdO7cGXPnzkXp0qWxdOlSuLq64ueff0ZgYCDevHmDAQMGcJtMg/bs2YPRo0ejYcOGOH78OFatWoVXr17By8sLBgYGGDFihPKoKH08V0nv3l0n747cfffuXURHR8PY2BjFixfnuvsBsctrOnDnzh0sWLAAe/fuhY2NDU6dOoVly5bh4sWLWL58OUqXLg0DAwPlEQX6spOrr3VcuHABBw8exKJFi3Dr1i2sWrUKGzZsQJYsWTBx4kQcP34cCxcuxLVr1/Dnn38iMTFRx5HTp1haWqJv375wc3PDypUr9S6ZfPcaXMGCBbFt2zaEhIRgzpw5CAoKwi+//IIaNWrA19cXVapU0ctkUl3Hd7stqZ8fCgA9e/bEunXr4O7uDkdHRzg7OyMhIUEnsdKPx8jICDExMYiMjAQA9O7dG7a2tsiSJQu2bduGDRs2wNnZGbxenrZcvXoVu3fvxpEjR9CsWTPExMSgSJEiyjND37x5g6dPnyrl9eVcJT1T70OvXr0CoLlOMmTIoMwvWrQoypUrp9xOxX3vx8OEUk/9CMmWSqXC7t27lW5MJUqUwO3btzFs2DBUqFABpUqVQtGiRZEnTx6YmZlh+/bt6NWrFwwMDHQdOn0BIyMjGBkZ6TqMFFFfdfXw8MD06dOROXNmFCtWDFeuXFFaQypVqoTq1avj+fPnOo5WeyqVCvv378fChQsRExOjMV197OnYsSOWLFmC7t274/z58+wZQN9NtmzZ0LJlS5w8eRK3bt2CoaEh2rRpg4wZM8LExER58D0TEt1THy9OnjyJtm3bIk+ePFiwYAEmTJiAvXv3wszMDAcPHkTVqlUxefJk2NjY6DhiepdKpcKePXvQr18/DB48GAcPHtQYp0K9j6kvKKrPMfnM4R8P17ie+hGSrcjISKxcuRJLlixR7j8zNDTE4MGDsXr1asycORNdunRB2bJlkZCQgGzZsiF//vw6jprSM5VKhYMHD2L8+PGoXLmysj9ly5YNwcHBmDVrFv7880/MnDkT9erV09urtJcuXYKHhweqVav2wWNO1EllfHw8OnTogHXr1ild1Ii+l3b/196dB0Vd/3Ecf64cCnIoHiAoCILiMWNTeWWWJo6mkqh5pKjlEWmo6XiRJmAmMnikpjNFaqZmqeRtmklSKjpIMWWlg8YhGgmCIOu1HL8/yi3N+vkja3/rvh4zzLB8v7vf9y47+/2+9nMNHkxZWRkzZsxg3rx5TJ482Sp7AzzoDAYDaWlpzJ07l/j4eNzd3dmzZw9r1qwhICCAY8eOMXHiRL7//ntcXV0tXa7cIS0tjTlz5hAfH29utLgzLFZUVGBvb8/ly5cZNWoUJSUlFqpWLElfKVup34etDh06AL+FrfLychYuXMi6detuC1vWdtFXo0YNSkpKzN3uKisrGTt2LFlZWVy4cIFVq1bRpUsXALWOyL8mOTmZ6dOn07NnT0wmEw4ODvTt25ebN29y+vRp5s+fT6tWrQDraiGpqKjAzs6OyspKxo0bh6OjI3PmzDGH4t8/l8rKShwcHCgqKjKvPybyb2rcuDHTpk0jNTWVU6dOsX79eh5//HFLlyV3UVJSwpdffsmoUaMIDw/n0KFDrF27lqqqKvbv309CQoLVDX2wFWfOnGHSpElkZmZy5coVEhMTqVWrFpcuXaJevXqUl5ebw+TAgQOJjo7G3d3d0mWLBegq3ErZQthycXFh4MCBHD9+HG9vb5o3b86xY8e4ePEiMTEx+Pj4aOC3/GtunUCNRiOZmZnAb916ioqKGDNmjHlfa3pfXrlyBVdXV+zs7Dhy5Ag3btxg5cqVREREsHHjRqZOnQr89pxuBc+SkhJCQ0NZsGCBhZ+B2Co3Nzd69uxJz549LV2K/IWQkBC2bNlCdHQ0/v7+vP/++xw+fJgLFy7wzjvv0KlTJ6v6zHyQ3fl/CAoKYubMmRQUFLBnzx6aNGnCli1bSElJYcmSJTg6OlJcXMzgwYOJiYkxX3eK7bHOpCE2E7b69+9PYmIikZGRdOjQgQ0bNrBy5Up8fHwA62oBEuuVk5PDsmXLeO6555gyZQr9+vWjSZMmjBkzhqNHjzJy5Ei2b99uXvrEWt6XV69eZcCAAURGRtK8eXPGjh2Lt7c3HTp0YPTo0SxfvhwHBwcmTpyIwWAwLxp/69vo+Ph4tQqJyH8VFhaGo6MjM2fOZNq0aQwcOPC27dbymfkgu3XN+MUXX/Djjz9Sv359Hn30UVq0aMFTTz1Fbm4u58+f5/XXX2fBggU4OjpSUVHBzJkziYqKUpi0cVo2xIrl5eWRmJhIamrqbWHrQVu7yWg0kpaWRl5eHoGBgXTs2NHSJYkNuHNK9KSkJAoKChg2bBi1atXi2WefpXPnzhw/fpy4uDj69Olj4YqrZ8eOHSxevBhnZ2cWLVpEmzZtSEhIAKBWrVrMnz+fWbNmMWXKFOCXmV9DQ0OJjo7WBYSI/E927dpFdHQ0e/bsoWHDhlY1r4MtSElJ4fnnnyciIoL169czefJkGjVqRGZmJsnJyTg5OTFixAjzOpNVVVWUlZVp/KsoUFo7hS2R++v69evY2dnh4ODA4cOHadu2La6urpw9e5bdu3eTk5PDhAkT8PT0pLS0FKPRSPPmza26R8DBgwcZPHgwr732Gq+88gomk4nly5dz7do1nJ2defjhh+natat5/zNnzhAYGGi5gkXEahUWFlK/fn1LlyF3yMzMJDY2lr59+zJ06FBOnTpFXFwcnTt35sUXX6S8vByj0Yi7u/tdx9aLbdMsr1audu3adO3alfDwcIVJkb+pqKiIuXPnkpKSAkBSUhJt2rThypUrNGvWjN69e3Px4kVmz55Neno6Pj4+5glprPnE2r17d9asWcP69evZvHkzDg4OTJ48GQcHB8LCwujatStVVVXmKeEVJkWkuhQm//+UlpZy4sQJzp07x4EDB7h06RLBwcFMmzaNxMRECgoKsLe3N0+4YzAYrPqcJ/efxlCKiPzKw8MDV1dX9u7di4uLC0uXLsXe3p5OnTqRmppKUFAQjz32GGlpaXh5eVm63PuqX79+1KxZk3nz5nHz5k3Cw8OZOXOmebvBYFD3NBGRB0hVVRVZWVnMmDGDtWvX0rBhQ3bs2MG2bdsYNmyYeb1oa53cUf49eoeIiIB5+vMnn3yS6OhoUlNTWbFiBQkJCVRVVdG+fXtmzJjBihUrePfddwkODrZ0yfddr169MJlMREdH0717dzw9PbVAtYjIA+T33VUNBgMBAQE4OTmxdOlS5s6dS35+Ph9//DHr16/Hzs6OqKgo6tata+Gq5f+dxlCKiPwqJSWF8ePHk5CQwNq1a3FzcyMiIoIOHTqwatUqCgoKaNeu3QM38dWdNMZJROTBc2vtZIDs7GycnJzw9PTk5MmTrFu3zjwhW1JSEgcPHqRly5ZMnDjRkiWLlVCgFBH51cqVKzl37hwLFy4EICYmhkOHDhEXF0fHjh3NY0aseQIeERGxPeXl5WzduhV/f39MJhPdunVj1KhRNG3alAkTJtCrVy+mTp3K0KFDAfjggw9ISUmha9euDBkyRL1V5C+py6uI2KxbwTA9PR2j0YjBYODbb78lPT2dRx55hJiYGNq0acO6deto0aIFHh4egHVPwCMiIrbH3t6eVq1aERoairOzM8nJybRs2ZLw8HCcnJyws7Pjo48+olu3bnh6ejJs2DAcHBx44oknFCblv1KgFBGbZTAY2LFjB7GxsfTo0YOTJ0/SpUsXDhw4QGlpKd7e3nh6ejJ69GhzmBQREbFGzZo1w8/Pj8LCQkwmE/Xr12fXrl1kZmYC8OGHH3L+/Hk8PT0BGDRokCXLFSuirxxExGYVFxezadMmDh06RPv27cnPz6djx464uroyb948xo0bx6RJk2jfvr2lSxUREflbateuzf79+1m9ejWzZs1i06ZNODg44OTkxNSpU3nppZdYunSpeYkokXulFkoRsVmOjo40aNCA2NhYjh07xpYtWwgICMDZ2ZmQkBDc3d3x8vLSmEkREXkgODk50alTJ2JjY5k+fTrfffcdKSkpbNiwgbp161JcXExlZaWWiZL/iVooRcRm1a5dm+DgYD799FNef/11AgICSElJYfjw4ZSXl5vXmlSYFBGRB8nTTz/N6tWr+emnn5gzZw5+fn7Y2dkRHx9vnglW5F5pllcRsWk///wzK1asICMjg9atW7Nz504WLVpEnz59LF2aiIjIP+rWGswAlZWVmoBHqkWBUkRsntFo5MSJExQUFODr60v79u3VzVVERETkHihQioiIiIiISLWoXVtERERERESqRYFSREREREREqkWBUkRERERERKpFgVJERERERESqRYFSREREREREqkWBUkRERERERKpFgVJERERERESqRYFSRETERhgMBjIyMixdhoiIPEAUKEVExKadPn2a0NBQ6tevj5ubG8HBwcTHx1uklpiYGMLCwixybBERkepQoBQREZvWp08f2rZtS25uLsXFxSQlJREQEHDfj2Myme77Y4qIiFiaAqWIiNiswsJCzp49S0REBM7OztjZ2dG6dWsGDRpk3qesrIzIyEh8fX1p2LAhI0eOpKSkxLw9MzOTZ555hgYNGuDh4cGAAQMAyM7OxmAwsHbtWgIDA2ncuDEAX331Fd26dcPDw4PAwEASExMB2L59OwsWLGD37t24uLjg4uJy15orKytZvnw5wcHBuLq6EhQUxL59+4BfQmtUVBS+vr40aNCAIUOGUFBQ8I+8diIiIqBAKSIiNqxevXq0aNGCF154gc2bN5OTk/OHfUaPHk1RURHffPMNWVlZmEwmIiMjATAajYSEhNCmTRuys7PJz89n4sSJt91/586dnDhxgqysLPLz8+nRowfjx4+noKCA7du3Ex0dzcGDBwkLC+PVV1+lb9++lJWVUVZWdtea33rrLd588002btxIaWkpBw8exM/PD4C4uDh2797N4cOHycrKwmAwMHz48Pv8qomIiPzGUFVVVWXpIkRERCwlPz+fhIQE9u3bx6lTp2jRogXLli2jR48eFBQU4OXlRWFhIXXr1gV+aZFs3bo1165dY+vWrcyePZvMzEwMBsNtj5udnY2/vz9ff/01Dz30EAAJCQkcPXqUbdu2mfebPXs2+fn5rF69mpiYGDIyMti+ffuf1tuyZUuioqIYOXLkH7YFBQUxf/58hgwZAsCFCxfw8fHh/PnzeHt7YzAYbqtHRETk77K3dAEiIiKW5OXlxeLFi1m8eDFFRUW88cYb9O/fn9zcXLKzs6msrMTf3/+2+9SoUYP8/HxycnJo1qzZH8Lk7/n6+pp/z87OZu/evdSpU8f8t4qKCrp06XLP9ebk5BAUFHTXbXl5eTRt2tR829vbm5o1a5KXl4e3t/c9H0NEROReqcuriIjIrzw8PIiJicFoNJKVlUWTJk2oUaMGFy5c4PLly+af69ev4+Pjg5+fH2fPnuWvOvvUqPHbqbZJkyb079//tse6cuUKe/fu/cO+f8bPz48zZ87cdVvjxo3Jzs42387Pz+fGjRvm8ZsiIiL3mwKliIjYrOLiYubMmcOpU6eoqKjg6tWrLFmyBA8PD4KDg/Hy8iIsLIzIyEgKCwuBX0LarS6rffr04caNG8ydOxej0cjNmzf5/PPP//R4I0aMIDk5maSkJEwmEyaTiYyMDNLS0gDw9PQkJyeH8vLyP32MiIgIYmNjycjIoKqqitzcXH744QcAwsPDWbBgAefOnaOsrIypU6cSEhKi1kkREfnHKFCKiIjNcnR05Pz58/Tu3Rt3d3d8fX05cuQIn3zyCbVr1wbgvffeo06dOrRr1w43Nze6dOlCeno6AC4uLnz22Wekp6fj6+tLo0aNWLly5Z8ez8fHh/379/P222/TqFEjPD09efnllyktLQVg0KBBuLm50aBBg9u6xf7epEmTGD9+PIMHD8bV1ZWQkBByc3MBiIqKomfPnnTq1ImmTZtiMpnYsGHDfXzFREREbqdJeURERERERKRa1EIpIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItXyHwNqNg+VqtyCAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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0t/fH3p6ekrC+zENGjSAg4MDNm/eDD09PY1iSQvs8kpEREREROlOt27dsGrVKqxevfqjXVKbNWuGZ8+eYdmyZThz5gzOnDkDAEhISPjksjNnzqz8X/X/H1PwsW6yp06dQocOHdC4cWN4eXnh0qVLGD169GfX8TFDhw6Fp6cn/v77bxw7dgz+/v4oU6aMsrzUls3PadKkCY4ePYobN25oFEdaYQslERERERGlO40aNUJCQgJUKhWcnJzemx8ZGYmAgAAsW7YMNWvWBAAcP348zeM4efIkChUqhNGjRyvTUrvipsqSJQuSk5Pf+2zmzJnfm37ixAl07doVrq6uAN60WN67d0+ZX6ZMGaSkpODIkSNKl9cPmT59OoyNjVGvXj34+fmhVKlSmlTvmzGhJCIiIiKidEdPTw83b95U/v+u7Nmzw8LCAkuXLkW+fPkQEhKCkSNHpnkcNjY2CAkJwaZNm1C5cmXs2bMHnp6eamWsra0RHBwMf39/5M+fHyYmJjAwMIC1tTUOHjyI6tWrw8DAANmzZ4eNjQ08PDzQrFkzqFQqjB07Vq111NraGl26dEG3bt0wf/58lCtXDvfv38fjx4/Rpk0btfX+888/SE5ORt26deHn54cSJUqkef0/h11eiYiIiIh+AjlzAh8Zc+a7MTR8s15NmZqafvRexUyZMmHTpk24cOECSpcujUGDBmHWrFmar+wjXFxcMGjQIPTr1w/29vY4efIkxo4dq1amVatWaNSoEerUqYNcuXJh48aNAAA3Nzf4+vqiQIECyiNPZs+ejezZs6NatWpo1qwZnJycUKFCBbXl/fvvv2jdujX++OMPlChRAj179kRsbOwH45szZw7atGmDunXr4vbt22le/89RiYj88LV+Z9HR0TAzM0NUVJTGN8uSZv5/F/RvN+HbFyTjM9yuTUT0QaqJPGYS0f+Ji4tDcHAwChcu/N6opSEhwNOnPy6WnDmBj4xzQ1r2qf3ka/IpdnklIiIiIvpJFCzIBI/SFru8EhERERERkUaYUBIREREREZFGmFASERERERGRRphQEhERERERkUaYUBIREREREZFGmFASERERERGRRphQEhERERERkUb4HEoiIiIiop9ESFQInr56+sPWlzNrThQ044MvMzImlEREREREP4GQqBDYLrRFXFLcD1unob4hAvoFaCWprF27Nuzt7TF3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fvnjUr6Gqakp2rZti2XLlmHz5s3Yvn07nj17psx3cXHBhg0b0KNHD2zatOm7xPA2JpRERERERJRu/frrr9DT08OiRYtgYmKCoUOHYtCgQVi9ejWCgoJw8eJFLFiwAKtXrwYAjBs3Djt37sSdO3dw/fp1eHl5oWTJkh9c9v/+9z8EBgZi2LBhCAgIwIYNG7Bq1Sq1MrVr18aTJ08wc+ZMBAUFYdGiRfDx8VErY2Njg7Vr1+LmzZs4c+YMOnToACMjozT/W8yePRsbN27ErVu3cPv2bWzduhV58+aFubm5WjlXV1esXbsWv//+u9oIuN8DE0oiIiIiop+BQU4gk+GPXWcmwzfr/Qb6+vro168fZs6cidjYWEyePBljx47FtGnTULJkSTRq1Ah79uxB4cKFAQBZsmTBqFGjULZsWdSqVQt6enofbakrWLAgtm/fjh07dqBcuXJYsmQJ/v77b7UyJUuWxOLFi7Fo0SKUK1cOZ8+efa8L7YoVK/D8+XNUqFABnTp1woABA5A7d+5vqveHmJiYYObMmahUqRIqV66Me/fuwdvbG5kyvZ/WtW7dGqtXr0anTp3g4eGR5rGkUsm7HYIzgOjoaJiZmSEqKgqmpqbaDuenolKl0YImfPuCZHyG27WJiD5INZHHTCL6P3FxcQgODkbhwoVhaPhOAhkbAsR/n66YH2SQE8hW8Metj77Yp/aTr8mnOMorEREREdHPIltBJniUptjllYiIiIiIiDTChJKIiIiIiIg0woSSiIiIiIiINMKEkoiIiIgoA8qAY29SGkqr/YMJJRERERFRBpI5c2YAwKtXr7QcCaVnqftH6v6iKY7ySkRERESUgejp6cHc3ByPHz8GAGTNmhWqNHu2G+k6EcGrV6/w+PFjmJubQ09P75uWx4SSiIiIiCiDyZs3LwAoSSXRu8zNzZX95FswoSQiIiIiymBUKhXy5cuH3LlzIzExUdvhUDqTOXPmb26ZTMWEkoiIiIgog9LT00uzxIHoQzgoDxEREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaURrCaWXlxdsbW1hY2OD5cuXvze/du3aKFGiBOzt7WFvb4/Xr19rIUoiIiIiIiL6GH1trDQpKQmDBw/G4cOHYWZmhooVK8LV1RUWFhZq5bZt24bSpUtrI0QiIiIiIiL6DK20UJ49exZ2dnawsrKCsbExnJ2dsX//fo2XFx8fj+joaLUXERERERERfV9aaaEMDw+HlZWV8t7KygphYWHvlWvfvj309PTQqVMnDB48+KPLmzZtGiZOnPhdYk1rKlUaLWhC2ixIxkuaLOdnkSbbj9uO0inVxG/fN7lfEtHPIi2OmQCPm1+L52LpT7odlGf9+vW4cuUK/Pz8sHPnTuzZs+ejZUeNGoWoqCjlFRoa+gMjJSIiIiIi+jlpJaG0tLRUa5EMCwuDpaWlWpnUFkwzMzO0adMG586d++jyDAwMYGpqqvYiIiIiIiKi70srCaWDgwOuXbuGsLAwxMTEwMfHB05OTsr8pKQkPH36FACQkJAAHx8f2NnZaSNUIiIiIiIi+git3EOpr68PNzc31KlTBykpKRg+fDgsLCzQuHFjLF++HGZmZnByckJiYiKSk5PRrFkztG7dWhuhEhERERER0UdoJaEEABcXF7i4uKhN8/b2Vv5/4cKFHx0SERERERERfYV0OygPERERERERpW9MKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjWksovby8YGtrCxsbGyxfvvyDZVJSUvDLL7+gdevWPzg6IiIiIiIi+hx9baw0KSkJgwcPxuHDh2FmZoaKFSvC1dUVFhYWauVWrFgBa2trJCcnayNMIiIiIiIi+gSttFCePXsWdnZ2sLKygrGxMZydnbF//361Ms+ePcOmTZvQq1evzy4vPj4e0dHRai8iIiIiIiL6vrSSUIaHh8PKykp5b2VlhbCwMLUyo0ePxtixY6Gnp/fZ5U2bNg1mZmbKq0CBAmkeMxEREREREalLl4PyXLp0Cc+fP0ft2rW/qPyoUaMQFRWlvEJDQ79vgERERERERKSdeygtLS3VWiTDwsLg4OCgvD99+jSOHTsGa2trxMXF4eXLl+jVqxeWLl36weUZGBjAwMDgu8dNRERERERE/0crLZQODg64du0awsLCEBMTAx8fHzg5OSnz+/Tpg7CwMNy7dw+bNm2Cs7PzR5NJIiIiIiIi0g6tJJT6+vpwc3NDnTp1YG9vjyFDhsDCwgKNGzdGeHi4NkIiIiIiIiKir6SVLq8A4OLiAhcXF7Vp3t7e75WrXbv2F99LSURERERERD9OuhyUh4iIiIiIiNI/JpRERERERESkESaUREREREREpBEmlERERERERKQRJpRERERERESkEY0Syh07dnxw+tSpU78lFiIiIiIiItIhGiWU/fr1w/Hjx9WmTZ8+HatXr06ToIiIiIiIiCj90yih3LZtG9q1a4fr168DAP755x8sW7YMhw4dStPgiIiIiIiIKP3S1+RDVapUwX///YcmTZqgY8eOWL9+PY4cOYL8+fOndXxERERERESUTn1xQhkdHa32vmbNmhg4cCBmzpwJHx8fmJubIzo6GqampmkeJBEREREREaU/X5xQmpubQ6VSqU0TEQBAhQoVICJQqVRITk5O2wiJiIiIiIgoXfrihDI4OPh7xkFEREREREQ65osTykKFCn103pMnT6Cvr4/s2bOnSVBERERERESU/mk0ymvfvn1x+vRpAMDWrVthaWmJPHnyYPv27WkaHBEREREREaVfGiWUHh4eKFeuHIA3z5/csmUL9u7diwkTJqRlbERERERERJSOafTYkNjYWBgZGeHp06e4d+8eXF1dAQAhISFpGhwRERERERGlXxollIULF8aGDRsQGBiIOnXqAABevHiBLFmypGlwRERERERElH5plFD+888/6Nq1K7JkyQJPT08AgJeXFypXrpymwREREREREVH6pVFC2aBBA4SFhalNa9u2Ldq2bZsmQREREREREVH698UJ5cuXL2FiYgIAiI6O/mi5zJkzf3tURERERERElO59cUJpZWWlJJLm5uZQqVRq80UEKpUKycnJaRshERERERERpUtfnFBev35d+X9wcPAHy4SGhn57RERERERERKQTvjihLFCggPJ/Y2NjZM+eHZkyvXmM5cOHDzF16lSsWLECr169SvsoiYiIiIiIKN3J9DWFL1y4gEKFCiF37tzIly8fTpw4gX///Rc2Nja4f/8+Dh069L3iJCIiIiIionTmq0Z5HTp0KNq1a4cuXbpg+fLl+PXXX2FlZYWjR4+ifPny3ytGIiIiIiIiSoe+KqG8evUqfH19oa+vj6lTp2LevHm4cOEC8uXL973iI6If5J1xtjQ34dsXJOMlDQJRlyb1S4O6AWlfv7Tbdmm0nDTGbfcFJqTRctIYt90XYP0+K/3+JqTBMr6DjL7tKP35qi6vCQkJ0Nd/k4MaGRnBzMyMySQREREREdFP6qtaKBMSEjB//nzlfXx8vNp7ABgwYEDaREZERERERETp2lcllFWqVIGnp6fy3sHBQe29SqViQklERERERPST+KqE0s/P7zuFQURERERERLrmq+6hJCIiIiIiIkrFhJKIiIiIiIg0woSSiIiIiIiINMKEkoiIiIiIiDSitYTSy8sLtra2sLGxwfLly9+bX6tWLZQrVw6lSpXCpEmTtBAhERERERERfcpXjfKaVpKSkjB48GAcPnwYZmZmqFixIlxdXWFhYaGU8fLygqmpKZKSklCjRg00a9YM5cuX10a4RERERERE9AFaaaE8e/Ys7OzsYGVlBWNjYzg7O2P//v1qZUxNTQEAiYmJSExMhEql0kaoRERERERE9BFaSSjDw8NhZWWlvLeyskJYWNh75apVq4bcuXOjfv36sLe3/+jy4uPjER0drfYiIiIiIiKi7ytdD8pz8uRJhIeHw9/fH9euXftouWnTpsHMzEx5FShQ4AdGSURERERE9HPSSkJpaWmp1iIZFhYGS0vLD5Y1MTFBvXr1sHfv3o8ub9SoUYiKilJeoaGhaR4zERERERERqdNKQung4IBr164hLCwMMTEx8PHxgZOTkzI/KioKT548AfCmO+u+fftQokSJjy7PwMAApqamai8iIiIiIiL6vrQyyqu+vj7c3NxQp04dpKSkYPjw4bCwsEDjxo2xfPlyJCYmolWrVkhISEBKSgratGmDpk2baiNUIiIiIiIi+gitJJQA4OLiAhcXF7Vp3t7eyv/Pnz//o0MiIiIiIiKir5CuB+UhIiIiIiKi9IsJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWlEawmll5cXbG1tYWNjg+XLl6vNe/XqFZydnVGiRAnY2dlhwYIFWoqSiIiIiIiIPkZfGytNSkrC4MGDcfjwYZiZmaFixYpwdXWFhYWFUmbkyJFwdHRETEwMKlWqBGdnZxQrVkwb4RIREREREdEHaKWF8uzZs7Czs4OVlRWMjY3h7OyM/fv3K/OzZs0KR0dHAICxsTFsbW0RERHx0eXFx8cjOjpa7UVERERERETfl1YSyvDwcFhZWSnvraysEBYW9sGyoaGhuHLlCipUqPDR5U2bNg1mZmbKq0CBAmkeMxEREREREalL14PyxMfHo23btpg1axayZcv20XKjRo1CVFSU8goNDf2BURIREREREf2ctHIPpaWlpVqLZFhYGBwcHNTKiAg6d+6Mxo0bo3Xr1p9cnoGBAQwMDL5LrERERERERPRhWmmhdHBwwLVr1xAWFoaYmBj4+PjAyclJrcyoUaOQNWtWjBkzRhshEhERERER0WdoJaHU19eHm5sb6tSpA3t7ewwZMgQWFhZo3LgxwsPD8eDBA8yYMQNnz56Fvb097O3tsW/fPm2ESkRERERERB+hlS6vAODi4gIXFxe1ad7e3sr/ReRHh0RERERERERfIV0PykNERERERETpFxNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ojWEkovLy/Y2trCxsYGy5cvf29+3759kSdPHlSqVEkL0REREREREdHnaCWhTEpKwuDBg3Ho0CFcunQJs2bNQmRkpFqZ9u3bw9vbWxvhERERERER0RfQSkJ59uxZ2NnZwcrKCsbGxnB2dsb+/fvVylSvXh0WFhZftLz4+HhER0ervYiIiIiIiOj70kpCGR4eDisrK+W9lZUVwsLCNF7etGnTYGZmprwKFCiQFmESERERERHRJ2SIQXlGjRqFqKgo5RUaGqrtkIiIiIiIiDI8fW2s1NLSUq1FMiwsDA4ODhovz8DAAAYGBmkRGhEREREREX0hrbRQOjg44Nq1awgLC0NMTAx8fHzg5OSkjVCIiIiIiIhIQ1pJKPX19eHm5oY6derA3t4eQ4YMgYWFBRo3bozw8HAAQNeuXVG1alVcuXIF+fPnx9atW7URKhEREREREX2EVrq8AoCLiwtcXFzUpr39mJBVq1b94IiIiIiIiIjoa2SIQXmIiIiIiIjox2NCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQa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JwcPHqSoqAiDwcCECRNu86MmIiLyC0NTU1NTcxchIiLSXCoqKsjIyGDPnj2cOnWK4OBgVqxYwaBBg6isrMTLy4uqqirat28P/HRGsmfPnly+fJktW7Ywb948CgsLMRgMN91ucXEx/v7+fPHFF9x9990AZGRkcPjwYbZu3Wreb968eVRUVLB69WpSUlIoKCggJyfnT+vt0aMHSUlJTJo06XfbgoKCeOmllxg7diwA586dw8fHh/Lycry9vTEYDDfVIyIi8ne1bu4CREREmpOXlxdLly5l6dKlVFdX8/LLLzNixAhKS0spLi6msbERf3//m67TqlUrKioqKCkpoVu3br8Lk7/m6+tr/r24uJjdu3fTrl0789+uX79O//79b7nekpISgoKC/nBbWVkZXbt2NV/29vamTZs2lJWV4e3tfcvHEBERuVUa8ioiIvIzNzc3UlJSMBqNFBUV0aVLF1q1asW5c+f48ccfzT9XrlzBx8cHPz8/zpw5w18N9mnV6pe32i5dujBixIibbquuro7du3f/bt8/4+fnx+nTp/9wW+fOnSkuLjZfrqio4OrVq+b5myIiIrebAqWIiFitmpoa5s+fz6lTp7h+/TqXLl1i2bJluLm5ERISgpeXFzExMcTHx1NVVQX8FNJuDFmNiori6tWrLFiwAKPRyLVr1/j000//9HgTJ05k//79ZGdnYzKZMJlMFBQUcPz4cQA8PT0pKSmhoaHhT28jLi6O1NRUCgoKaGpqorS0lG+++QaA2NhY0tLSOHv2LPX19SQkJBAZGamzkyIi8o9RoBQREatlZ2dHeXk5Q4cOxdXVFV9fXw4dOsRHH32Eo6MjAO+88w7t2rWjd+/euLi40L9/f/Lz8wFwcnLik08+IT8/H19fXzp16kRmZuafHs/Hx4e9e/fyxhtv0KlTJzw9PXn22Wepra0FYPTo0bi4uODh4XHTsNhfmzFjBlOnTmXMmDE4OzsTGRlJaWkpAElJSQwePJjw8HC6du2KyWRi/fr1t/ERExERuZma8oiIiIiIiIhFdIZSRERERERELKJAKSIiIiIiIhZRoBQRERERERGLKFCKiIiIiIiIRRQoRURERERExCIKlCIiIiIiImIRBUoRERERERGxiAKliIiIiIiIWESBUkRERERERCyiQCkiIiIiIiIWUaAUERERERERi/wbP7LGLa/KxhIAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Singling-out\n", + "sin_out_res = singling_out_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_attacks = syn_anom.shape[0]\n", + ")\n", + "SyntheticResult.plot(res=sin_out_res,\n", + " high_res_flag=False,\n", + " save=True,\n", + " save_path=\"./outputs\",\n", + " save_name=\"Singling-out\"\n", + " )\n", + "\n", + "# Linkability\n", + "link_res = linkability_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_samples = syn_anom.shape[0],\n", + " n_attacks = 100\n", + ")\n", + "SyntheticResult.plot(res=link_res,\n", + " high_res_flag=False,\n", + " save=True,\n", + " save_path=\"./outputs\",\n", + " save_name=\"Linkability\"\n", + " )\n", + "\n", + "# Inference risk, base case\n", + "inf_res = inference_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " worst_case_flag = False,\n", + " n_attacks = syn_anom.shape[0]\n", + ")\n", + "SyntheticResult.plot(res=inf_res,\n", + " high_res_flag=False,\n", + " case_flag=\"base\",\n", + " save=True,\n", + " save_path=\"./outputs\",\n", + " save_name=\"Inference_base_case\"\n", + " )\n", + "\n", + "# Inference risk, worst case\n", + "inf_res_worst = inference_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " worst_case_flag = True,\n", + " n_attacks = syn_anom.shape[0]\n", + ")\n", + "SyntheticResult.plot(res=inf_res_worst,\n", + " high_res_flag=False,\n", + " case_flag=\"worst\",\n", + " save=True,\n", + " save_path=\"./outputs\",\n", + " save_name=\"Inference_worst_case\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "9f9e01d4", + "metadata": {}, + "source": [ + "## Save and store results" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "0e0c6c09", + "metadata": {}, + "outputs": [ + { + "ename": "TypeError", + "evalue": "ReportHandler.__init__() missing 1 required positional argument: 'logger'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[11], line 7\u001b[0m\n\u001b[1;32m 3\u001b[0m os\u001b[38;5;241m.\u001b[39mmakedirs(path)\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mleakpro\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreporting\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreport_handler\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ReportHandler\n\u001b[0;32m----> 7\u001b[0m reporthandler \u001b[38;5;241m=\u001b[39m \u001b[43mReportHandler\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[43m \u001b[49m\u001b[43mreport_dir\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11\u001b[0m reporthandler\u001b[38;5;241m.\u001b[39msave_results(\n\u001b[1;32m 12\u001b[0m attack_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msynthetic_inference\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 13\u001b[0m result_data\u001b[38;5;241m=\u001b[39minf_res,\n\u001b[1;32m 14\u001b[0m config\u001b[38;5;241m=\u001b[39m{\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcase\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbase\u001b[39m\u001b[38;5;124m\"\u001b[39m}\n\u001b[1;32m 15\u001b[0m )\n", + "\u001b[0;31mTypeError\u001b[0m: ReportHandler.__init__() missing 1 required positional argument: 'logger'" + ] + } + ], + "source": [ + "path = \"../../leakpro_output\"\n", + "if not os.path.exists(path):\n", + " os.makedirs(path)\n", + " \n", + "from leakpro.reporting.report_handler import ReportHandler\n", + "\n", + "reporthandler = ReportHandler(\n", + " report_dir=path,\n", + " )\n", + "\n", + "reporthandler.save_results(\n", + " attack_name=\"synthetic_inference\",\n", + " result_data=inf_res,\n", + " config={\"case\": \"base\"}\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f9c3942", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/report_handler/report_handler_anomalies.ipynb b/examples/report_handler/report_handler_anomalies.ipynb new file mode 100644 index 00000000..a32fbdda --- /dev/null +++ b/examples/report_handler/report_handler_anomalies.ipynb @@ -0,0 +1,203 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "95d5acad-514e-4950-94a0-c80d789d9364", + "metadata": {}, + "source": [ + "# Report handler examples" + ] + }, + { + "cell_type": "markdown", + "id": "71f5dbe9", + "metadata": {}, + "source": [ + "Install leakpro as ``` pip install -e /path/to/leakpro ```" + ] + }, + { + "cell_type": "markdown", + "id": "68b48ce8", + "metadata": {}, + "source": [ + "### Synthetic examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "import pandas as pd\n", + "\n", + "sys.path.append(\"../..\")\n", + "\n", + "from leakpro.synthetic_data_attacks import plots\n", + "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", + "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", + "# from leakpro.metrics.attack_result import SyntheticResult\n", + "\n", + "#Get ori and syn\n", + "n_samples = 100\n", + "DATA_PATH = \"../synthetic_data/datasets/\"\n", + "ori = pd.read_csv(os.path.join(DATA_PATH, \"adults_ori.csv\"), nrows=n_samples)\n", + "syn = pd.read_csv(os.path.join(DATA_PATH, \"adults_syn.csv\"), nrows=n_samples)" + ] + }, + { + "cell_type": "markdown", + "id": "62c44504-a5fa-4846-8132-53877f369825", + "metadata": {}, + "source": [ + "### Get anomalies of synthetic data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ad69ece9", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a result\n", + "\n", + "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", + "print(\"Syn anom shape\",syn_anom.shape)\n", + "\n", + "sin_out_res = singling_out_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_attacks = syn_anom.shape[0]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "373dcc8a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-20 00:09:42,566 INFO Initializing report handler...\n", + "2024-11-20 00:09:42,567 INFO report_dir set to: ../../leakpro_output/results\n", + "2024-11-20 00:09:42,569 INFO Saving results for singling_out\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../../leakpro_output/results\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Import and initialize ReportHandler\n", + "from leakpro.reporting.report_handler import ReportHandler\n", + "report_handler = ReportHandler()\n", + "\n", + "# Save the result using the ReportHandler\n", + "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d91c7e0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-19 23:38:04,520 INFO No results of type GIAResults found.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "../../leakpro_output/results\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-19 23:38:14,966 INFO PDF compiled\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Use the ReportHandler and load all the saved results\n", + "report_handler.load_results()\n", + "\n", + "# Create results and collect corresponding latex texts\n", + "report_handler.create_results_all()\n", + "\n", + "# Create the report by compiling the latex text\n", + "report_handler.create_report()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 91882483701c950855a57e781a2bd0362a3e4e95 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Fri, 22 Nov 2024 11:59:03 +0000 Subject: [PATCH 09/14] Added MIAResults to all mia attacks --- leakpro/attacks/mia_attacks/HSJ.py | 8 ++++---- leakpro/attacks/mia_attacks/attack_p.py | 4 ++-- leakpro/attacks/mia_attacks/loss_trajectory.py | 10 +++++----- leakpro/attacks/mia_attacks/qmia.py | 6 +++--- leakpro/attacks/mia_attacks/rmia.py | 6 +++--- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/leakpro/attacks/mia_attacks/HSJ.py b/leakpro/attacks/mia_attacks/HSJ.py index 166572f1..f8a946d8 100755 --- a/leakpro/attacks/mia_attacks/HSJ.py +++ b/leakpro/attacks/mia_attacks/HSJ.py @@ -4,7 +4,7 @@ from leakpro.attacks.mia_attacks.abstract_mia import AbstractMIA from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import HopSkipJumpDistance from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -169,12 +169,12 @@ def prepare_attack(self:Self) -> None: - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Run the attack and return the combined metric result. Returns ------- - CombinedMetricResult: The combined metric result containing predicted labels, true labels, + MIAResult: The Result containing predicted labels, true labels, predictions probabilities, and signal values. """ @@ -217,7 +217,7 @@ def run_attack(self:Self) -> CombinedMetricResult: ) # compute ROC, TP, TN etc - return CombinedMetricResult( + return MIAResult( predicted_labels=member_preds, true_labels=true_labels, predictions_proba=None, diff --git a/leakpro/attacks/mia_attacks/attack_p.py b/leakpro/attacks/mia_attacks/attack_p.py index baa0bc6a..7ff4d3c9 100755 --- a/leakpro/attacks/mia_attacks/attack_p.py +++ b/leakpro/attacks/mia_attacks/attack_p.py @@ -5,7 +5,7 @@ from leakpro.attacks.mia_attacks.abstract_mia import AbstractMIA from leakpro.attacks.utils.threshold_computation import linear_itp_threshold_func from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import ModelLoss from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -136,7 +136,7 @@ def run_attack(self:Self) -> CombinedMetricResult: ) # compute ROC, TP, TN etc - return CombinedMetricResult( + return MIAResult( predicted_labels=predictions, true_labels=true_labels, predictions_proba=None, diff --git a/leakpro/attacks/mia_attacks/loss_trajectory.py b/leakpro/attacks/mia_attacks/loss_trajectory.py index 5e78ee0e..072b5aa9 100755 --- a/leakpro/attacks/mia_attacks/loss_trajectory.py +++ b/leakpro/attacks/mia_attacks/loss_trajectory.py @@ -13,7 +13,7 @@ from leakpro.attacks.utils.distillation_model_handler import DistillationModelHandler from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import ModelLogits from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -402,7 +402,7 @@ def mia_attack(self:Self, attack_model:nn.Module) -> tuple: return auc_ground_truth, member_preds - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Run the attack and return the combined metric result. Returns @@ -418,9 +418,9 @@ def run_attack(self:Self) -> CombinedMetricResult: signals = np.random.rand(*true_labels.shape) # compute ROC, TP, TN etc - return CombinedMetricResult( - predicted_labels= predictions, + return MIAResult( + predicted_labels=predictions, true_labels=true_labels, predictions_proba=None, signal_values=signals, - ) + ) \ No newline at end of file diff --git a/leakpro/attacks/mia_attacks/qmia.py b/leakpro/attacks/mia_attacks/qmia.py index aa297a1c..9628a1d9 100755 --- a/leakpro/attacks/mia_attacks/qmia.py +++ b/leakpro/attacks/mia_attacks/qmia.py @@ -10,7 +10,7 @@ from leakpro.attacks.mia_attacks.abstract_mia import AbstractMIA from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import ModelRescaledLogits from leakpro.utils.import_helper import Any, Self, Tuple from leakpro.utils.logger import logger @@ -285,7 +285,7 @@ def _train_quantile_regressor( # Move the model back to the CPU self.quantile_regressor.to("cpu") - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Run the attack on the target model and dataset. Args: @@ -329,7 +329,7 @@ def run_attack(self:Self) -> CombinedMetricResult: ) # compute ROC, TP, TN etc - return CombinedMetricResult( + return MIAResult( predicted_labels=predictions, true_labels=true_labels, predictions_proba=None, diff --git a/leakpro/attacks/mia_attacks/rmia.py b/leakpro/attacks/mia_attacks/rmia.py index 9c78d98e..f46b6fea 100755 --- a/leakpro/attacks/mia_attacks/rmia.py +++ b/leakpro/attacks/mia_attacks/rmia.py @@ -6,7 +6,7 @@ from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler from leakpro.attacks.utils.utils import softmax_logits from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import ModelLogits from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -279,7 +279,7 @@ def _offline_attack(self:Self) -> None: self.in_member_signals = score[in_members].reshape(-1,1) self.out_member_signals = score[out_members].reshape(-1,1) - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Run the attack on the target model and dataset. Returns @@ -315,7 +315,7 @@ def run_attack(self:Self) -> CombinedMetricResult: ) # compute ROC, TP, TN etc - return CombinedMetricResult( + return MIAResult( predicted_labels=predictions, true_labels=true_labels, predictions_proba=None, From 2baec49174bc1cb150d5f07f53cb8ae9eae4a381 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Mon, 2 Dec 2024 01:03:40 +0000 Subject: [PATCH 10/14] pre-merge --- .../___report_handler_anomalies.ipynb | 418 ------------- examples/report_handler/gia_utils/cifar.py | 26 + examples/report_handler/gia_utils/model.py | 81 +++ examples/report_handler/mia_utils/audit.yaml | 56 ++ .../report_handler/mia_utils/cifar_handler.py | 67 +++ .../mia_utils/train_config.yaml | 17 + .../mia_utils/utils/cifar_data_preparation.py | 111 ++++ .../utils/cifar_model_preparation.py | 117 ++++ examples/report_handler/report_handler.ipynb | 562 ++++++++++++++++++ .../report_handler_anomalies.ipynb | 203 ------- .../attacks/gia_attacks/invertinggradients.py | 4 +- leakpro/attacks/mia_attacks/attack_p.py | 2 +- leakpro/attacks/mia_attacks/lira.py | 8 +- .../attacks/mia_attacks/loss_trajectory.py | 2 +- leakpro/leakpro.py | 21 +- leakpro/metrics/attack_result.py | 495 +++------------ leakpro/reporting/report_handler.py | 79 +-- leakpro/run.py | 3 +- .../synthetic_data_attacks/inference_utils.py | 102 +++- .../linkability_utils.py | 79 ++- leakpro/synthetic_data_attacks/plots.py | 61 +- .../singling_out_utils.py | 2 +- leakpro/synthetic_data_attacks/utils.py | 22 +- 23 files changed, 1429 insertions(+), 1109 deletions(-) delete mode 100644 examples/report_handler/___report_handler_anomalies.ipynb create mode 100644 examples/report_handler/gia_utils/cifar.py create mode 100644 examples/report_handler/gia_utils/model.py create mode 100644 examples/report_handler/mia_utils/audit.yaml create mode 100644 examples/report_handler/mia_utils/cifar_handler.py create mode 100644 examples/report_handler/mia_utils/train_config.yaml create mode 100644 examples/report_handler/mia_utils/utils/cifar_data_preparation.py create mode 100644 examples/report_handler/mia_utils/utils/cifar_model_preparation.py create mode 100644 examples/report_handler/report_handler.ipynb delete mode 100644 examples/report_handler/report_handler_anomalies.ipynb diff --git a/examples/report_handler/___report_handler_anomalies.ipynb b/examples/report_handler/___report_handler_anomalies.ipynb deleted file mode 100644 index 3411063a..00000000 --- a/examples/report_handler/___report_handler_anomalies.ipynb +++ /dev/null @@ -1,418 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "95d5acad-514e-4950-94a0-c80d789d9364", - "metadata": {}, - "source": [ - "# Report handler examples" - ] - }, - { - "cell_type": "markdown", - "id": "71f5dbe9", - "metadata": {}, - "source": [ - "Install leakpro as ``` pip install -e /path/to/leakpro ```" - ] - }, - { - "cell_type": "markdown", - "id": "68b48ce8", - "metadata": {}, - "source": [ - "### Synthetic examples" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import sys\n", - "\n", - "import pandas as pd\n", - "\n", - "sys.path.append(\"../..\")\n", - "\n", - "from leakpro.synthetic_data_attacks import plots\n", - "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", - "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", - "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", - "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", - "# from leakpro.metrics.attack_result import SyntheticResult\n", - "\n", - "#Get ori and syn\n", - "n_samples = 100\n", - "DATA_PATH = \"../synthetic_data/datasets/\"\n", - "ori = pd.read_csv(os.path.join(DATA_PATH, \"adults_ori.csv\"), nrows=n_samples)\n", - "syn = pd.read_csv(os.path.join(DATA_PATH, \"adults_syn.csv\"), nrows=n_samples)" - ] - }, - { - "cell_type": "markdown", - "id": "62c44504-a5fa-4846-8132-53877f369825", - "metadata": {}, - "source": [ - "### Get anomalies of synthetic data" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "0d25b9e7-03a7-4320-a456-6153774cb82c", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[Parallel(n_jobs=64)]: Using backend ThreadingBackend with 64 concurrent workers.\n", - "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 0.9s remaining: 28.1s\n", - "[Parallel(n_jobs=64)]: Done 64 out of 64 | elapsed: 4.1s finished\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unique predictions (array([-1, 1]), array([ 2, 98]))\n", - "Syn anom shape (2, 14)\n" - ] - } - ], - "source": [ - "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", - "print(\"Syn anom shape\",syn_anom.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ad69ece9", - "metadata": {}, - "outputs": [], - "source": [ - "sin_out_res = singling_out_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " n_attacks = syn_anom.shape[0]\n", - ")\n", - "# save_path = sin_out_res.save()\n", - "# print(save_path)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "5e8fe664", - "metadata": {}, - "outputs": [], - "source": [ - "# from leakpro.synthetic_data_attacks.singling_out_utils import SinglingOutResults\n", - "# import json\n", - "\n", - "# with open(\"../../leakpro_output/results/singling_out/singling_out_n_cols_all_adults/data.json\") as f:\n", - "# data = json.load(f)\n", - "# syn_loaded = SinglingOutResults.load(data=data)\n", - " \n", - "# syn_loaded.plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "373dcc8a", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-20 00:09:42,566 INFO Initializing report handler...\n", - "2024-11-20 00:09:42,567 INFO report_dir set to: ../../leakpro_output/results\n", - "2024-11-20 00:09:42,569 INFO Saving results for singling_out\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../../leakpro_output/results\n" - ] - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from leakpro.reporting.report_handler import ReportHandler\n", - "report_handler = ReportHandler()\n", - "\n", - "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "1d91c7e0", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-19 23:38:04,520 INFO No results of type GIAResults found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../../leakpro_output/results\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-19 23:38:14,966 INFO PDF compiled\n" - ] - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "report_handler.load_results()\n", - "report_handler.create_results_all()\n", - "\n", - "report_handler.create_report()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "89fcbe8a", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "markdown", - "id": "f0f7288c-a380-43df-97cb-fb0152e410a2", - "metadata": {}, - "source": [ - "### Singling-out risk analysis, Linkability riks analysis with anomalies and Inference risk, worst and base case" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "49ff9bb3-e7f4-4d3a-8759-382eed697893", - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA5QAAAGHCAYAAADP6x1UAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6TUlEQVR4nO3deVyVZf7/8fdBhFA2RVM4IHxTwsQFpbDFMkt/hKnomGvrNKNptsxok3u22OaSkzo1KuUy6Tgqg2MkLqk1pbnnFC22jCiBGuICuLBevz98eMaToHgHHji+no/H/Xhw3/d1X/fnOoftfa773MdmjDECAAAAAOAyebi6AAAAAABA7USgBAAAAABYQqAEAAAAAFhCoAQAAAAAWEKgBAAAAABYQqAEAAAAAFhCoAQAAAAAWEKgBAAAAABYQqAEAAAAAFhCoAQAN2Sz2bRy5coq7fP5559XTEyMY/2RRx5R7969q/QcNVVERIT+/Oc/V3lbV/joo49ks9l0/PhxV5dSKRkZGbLZbNqzZ4+rSwEAlINACQC1TE5OjoYPH65mzZrJ29tbTZs2VXx8vDZv3uxoc/DgQSUkJFRrHW+++aYWLFhQreeoKr8Mw5drx44dGjp0aNUVdJ4FCxYoMDDwih1XFSo6d00P0+UpLi7W6NGj1aZNG9WvX18hISF66KGHlJ2dfUHbDz74QB07dpSPj48aNGhw1bygAgAX4+nqAgAAl6dv374qKirSwoULdd111+nw4cPasGGDcnNzHW2aNm1a7XUEBARU+zlcraioSF5eXmrcuLGrS0E1OXXqlHbv3q2JEyeqXbt2OnbsmJ5++mn16tVLO3fudLRLTk7WkCFD9Morr+iuu+5SSUmJ0tPTXVg5ANQQBgBQaxw7dsxIMh999NFF20kyKSkpxhhj9u3bZySZ5ORkc+eddxofHx/Ttm1bs2XLFqdj5s6da0JDQ42Pj4/p3bu3mT59ugkICHDsnzRpkmnXrp1j/eGHHzaJiYmO9c6dO5snn3zS/OlPfzINGjQwTZo0MZMmTXI6xzfffGNuu+024+3tbW644Qazfv16p1rLc+bMGfPkk0+axo0bG29vb3PbbbeZ7du3O/bPnz/fqU5jjElJSTHn/sTNnz/fSHJa5s+fX+65zo1p8uTJJjg42ERERBhjjAkPDzczZswwxhhTVlZmJk2aZMLCwoyXl5cJDg42Tz75pKOP89saY8y8efNMQECA+fDDDy8436ZNmy6o7dxjdvToUfPggw+awMBA4+PjY+655x7z3XffXfK4RYsWmdjYWOPr62uaNGliBg0aZA4fPnzBOY8dO1bhYz59+nTTunVrU69ePRMaGmqGDx9u8vPzL3ruzp07X7DdGGOOHDliBg4caEJCQoyPj49p3bq1WbJkidP5SktLzeuvv26aN29uvLy8TFhYmJk8ebIx5n/fv59//rkxxpiSkhLz29/+1kRFRZn9+/df8vmwYvv27UaS2b9/vzHGmOLiYmO3201SUtKv6hcA3BGXvAJALeLr6ytfX1+tXLlShYWFl3Xs+PHj9cwzz2jPnj26/vrrNWjQIJWUlEiSNm/erGHDhunpp5/Wnj171K1bN7388suXXd/ChQtVv359bdu2TVOmTNGLL76o9evXS5JKS0vVu3dv1atXT9u2bdPcuXM1fvz4S/b57LPPKjk5WQsXLtTu3bvVokULxcfH6+jRo5WqacCAARo1apSio6N18OBBHTx4UAMGDKiw/YYNG7R3716tX79eqampF+xPTk7WjBkzNGfOHH3//fdauXKl2rRpU25fU6ZM0ZgxY7Ru3TrdfffdF+y/9dZb9ec//1n+/v6O2p555hlJZ9+junPnTq1atUqfffaZjDHq3r27iouLL3pccXGxXnrpJf3nP//RypUrlZGRoUceeaRSj9U5Hh4emjlzpr766istXLhQGzdu1LPPPnvRmv/5z38qNDRUL774omO7JJ05c0axsbH64IMPlJ6erqFDh+rBBx/U9u3bHecbO3asXnvtNU2cOFFff/21lixZoiZNmlxQV2Fhofr166c9e/bok08+UbNmzS75fDz//POKiIi4rPGfOHFCNpvNcVnv7t27lZWVJQ8PD7Vv317BwcFKSEhghhIAJGYoAaC2WbFihWnQoIG55pprzK233mrGjh1r/vOf/zi1UTkzlOfPrnz11VdGkvnmm2+MMcYMGDDA3HvvvU593H///Zc9Q9mpUyenPm666SYzevRoY4wxaWlpxtPT0xw8eNCx/1IzlAUFBaZu3bpm8eLFjm1FRUUmJCTETJkyxRhz6RnK8mqvyMMPP2yaNGliCgsLnbafP+s4ffp0c/3115uioqJy+zjX9tlnnzXBwcEmPT39oucsr/7vvvvOSDKbN292bDty5Ijx8fExy5Ytq/C48uzYscNIumCG8WIzlL+0fPlyExQUdNGajblwdrYi9957rxk1apQxxpi8vDzj7e1t5s2bV27bc9+/n3zyibn77rtNp06dzPHjxx37L/V8zJo1y9x1112XrOmc06dPmw4dOpjBgwc7tv397383kkyzZs3MihUrzM6dO82gQYNMUFCQyc3NrXTfAOCOmKEEgFqmb9++ys7O1qpVq3TPPffoo48+UocOHS55g5y2bds6vg4ODpYk/fzzz5KkvXv3Ki4uzqn9L9cr4/xznDvP+ecICwtzen/npc7x448/qri4WLfddptjW926dRUXF6dvvvnmsuurjDZt2sjLy6vC/f369dPp06d13XXXaciQIUpJSXHM9J4zffp0zZs3T59++qmio6Mvu4ZvvvlGnp6e6tixo2NbUFCQoqKiLjnuXbt2qWfPnmrWrJn8/PzUuXNnSdKBAwcqff4PP/xQd999t+x2u/z8/PTggw8qNzdXp06duuyxlJaW6qWXXlKbNm3UsGFD+fr6au3atY56vvnmGxUWFpY7g3u+QYMG6eTJk1q3bp3T+3cv9Xw88cQT2rBhQ6VqLS4uVv/+/WWM0dtvv+3YXlZWJunsLH/fvn0VGxur+fPny2azafny5ZV+LADAHREoAaAWuuaaa9StWzdNnDhRW7Zs0SOPPKJJkyZd9Ji6des6vrbZbJL+949yVTn/HOfOU9Xn+CUPDw8ZY5y2FRcXW+6vfv36F90fFhamvXv36q233pKPj48ef/xx3XHHHU7nvP3221VaWqply5ZZrsOKkydPKj4+Xv7+/lq8eLF27NihlJQUSWdvMFQZGRkZ6tGjh9q2bavk5GTt2rVLf/nLXy6rj/NNnTpVb775pkaPHq1NmzZpz549io+Pd/Tl4+NTqX66d++uL774Qp999pnT9so8H5VxLkzu379f69evl7+/v2PfuRdgWrVq5djm7e2t66677rKCOgC4IwIlALiBVq1a6eTJk5aPj4qK0o4dO5y2/XL914qKilJmZqYOHz5c6XM0b95cXl5eTh+JUlxcrB07djj+uW/cuLHy8/Odxv/Lzyz08vJSaWlpFYziLB8fH/Xs2VMzZ87URx99pM8++0xffvmlY39cXJzS0tL0yiuvaNq0aRftq7zabrjhBpWUlGjbtm2Obbm5udq7d69j3OUd9+233yo3N1evvfaabr/9drVs2dIxQ1xZu3btUllZmaZPn66bb75Z119//QUfoVHR41ne9s2bNysxMVEPPPCA2rVrp+uuu07fffedY39kZKR8fHwuOYs4fPhwvfbaa+rVq5c+/vhjp32Xej4u5VyY/P777/Xhhx8qKCjIaX9sbKy8vb21d+9ep2MyMjIUHh5e6fMAgDviY0MAoBbJzc1Vv3799Oijj6pt27by8/PTzp07NWXKFCUmJlru98knn9Qdd9yhN954Qz179tTGjRuVlpbmmMmsCt26dVPz5s318MMPa8qUKcrPz9eECRMkqcLz1K9fX8OHD9ef/vQnNWzYUM2aNdOUKVN06tQp/e53v5MkdezYUfXq1dO4ceP01FNPadu2bRdc/hsREaF9+/Zpz549Cg0NlZ+fn7y9vS2NY8GCBSotLXWc97333pOPj88FweLWW2/V6tWrlZCQIE9PT/3hD38ot7+IiAgVFBRow4YNateunerVq6fIyEglJiZqyJAhmjNnjvz8/DRmzBjZ7XbH81zecc2aNZOXl5dmzZqlYcOGKT09XS+99NJlja9FixYqLi7WrFmz1LNnT23evFl//etfL1lzvXr1FBERoX//+98aOHCgvL291ahRI0VGRmrFihXasmWLGjRooDfeeEOHDx92BONrrrlGo0eP1rPPPisvLy/ddtttysnJ0VdffeV4js958sknVVpaqh49eigtLU2dOnW65PMxe/ZspaSkVBhYi4uLdd9992n37t1KTU1VaWmpDh06JElq2LChvLy85O/vr2HDhmnSpEkKCwtTeHi4pk6dKunsJbcAcFVz9Zs4AQCVd+bMGTNmzBjToUMHExAQYOrVq2eioqLMhAkTzKlTpxztVM5Nec597IIx//v4kU2bNjm2zZ0719jtdsfHhkyePNk0bdrUsb8yN+V5+umnnepNTEw0Dz/8sGP93MeGeHl5mZYtW5r333/fSDJr1qypcMynT582Tz75pGnUqFG5HxtizNmb8LRo0cL4+PiYHj16mLlz5zrdlOfMmTOmb9++JjAwsFIfG/JL599sJiUlxXTs2NH4+/ub+vXrm5tvvtnpI0F+eWOajz/+2NSvX9/MnDmzwjEOGzbMBAUFlfuxIQEBAcbHx8fEx8c7PjbkYsctWbLEREREGG9vb3PLLbeYVatWOT3/lbkpzxtvvGGCg4Md5120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P//8sw4fPuzqMq6YAwcOKDMz86r4H1I6+0Lf119/7eoycAkEyivo/fffV7du3fTcc89Jcv9QuWnTJv3xj3/UnDlztH79euXn5+vzzz93dVnVwsPDQ4GBgRo0aJBiY2P16aef6p///KckyWazubi6qrdlyxY99NBDevfdd3XXXXcpLy9Phw4dcut/YDZt2qSJEyfqN7/5jXJzc/X1118rLy/P1WVViw8//FCPP/643nvvPX377bfat2+fvvnmG1eXVW2OHDmit956S0uXLtWqVatUVlamLVu26PTp064urVrk5OTo7bff1pIlS/Svf/1LQUFBGj9+vFJSUtwyVCYnJ+u+++7TwIEDNXnyZK1atcrVJVWbNWvWaMSIEZKkOnXquPXvZOnsVSP9+/d3vJjr7pfop6amasCAAfrd736nV199VSUlJa4uqdoYY3TgwAElJibqmWee0Z49e5z2oWYhUF4hBQUFeumllzR48GCdOHFCzz//vCT3DpWBgYH661//qo4dO+rgwYP6+uuvNW3aND3xxBPaunWrq8urFk2aNNHRo0cVFRWlbdu2acaMGUpKSpLkXr8Ag4KC5OnpqcOHDys/P1+/+c1v9Pvf/14PPfSQNmzY4OryqkVBQYGKi4tVUlKiPn36aPTo0RoxYoSWLVvmVs+tdHZmY9GiRWrdurXOnDkjSY4/5u42VkmqW7euCgsL9e233yo/P1+7du3S7Nmz9dhjj7nl81u3bl2dOXNGR44ckSSNGTNGfn5+2rZtm9u96FdQUKAZM2Zo5syZWrp0qZo2baq1a9dqwYIFri6tym3fvl1Dhw5Venq6Bg0aJMm9Q+XHH3+sMWPGaObMmZo3b542btyoNWvWuLqsarN27VqNGzdOs2fP1vz587V27VrHVW/uyGazqVmzZkpMTFSHDh308ssva9u2bY59qFkIlFeIr6+vVq5cqccff1z333+/fvrpJ6dQ6W7/sEhS+/bt1aVLFxljHJeRLV68WKGhoZo1a5ZbvbJ27vmLj49XZGSkHn30URljNG7cOMf7sdzpF2BUVJQ++OADPf7442rVqpUGDBiglJQUdezYUQsXLnTLmZ1BgwZp6dKleuCBBzRkyBC9//77SkhI0Lp16xz/mLuL+Ph43XrrrSorK5Pdble/fv00btw4fffdd271fXxOQECARo0apddee0333HOPfve73yk5OVkJCQlKS0tzu/dUBgYGavDgwVq0aJGWLl2ql156SU2bNlX9+vX1zjvvuLq8KmWM0bXXXqtGjRopNDRUffr0Ubdu3bRz506tXr3a1eVVqaKiIj3//PPasGGD6tWrp4EDB0py31B58OBBDR8+XG3btlWbNm00dOhQrV+/XmVlZW75P9Xx48f12muvKTY2Vt7e3jp8+LDGjx+vGTNmuOULuUVFRSotLVWdOnUUHByshIQE/eUvf9F7772n5ORkSe75AmdtRaC8gkJCQtSoUSPdeOONeuyxx5xC5Y8//qiCggLXFlhNbDabJk6c6LjUd8yYMTpy5IgyMjJcW1gVOvdPdsOGDbVnzx797W9/U3Jysn7/+9/r4MGD+uCDD1xcYdVr166dUlNTNXr0aA0ZMkR169bVU089pZ9//tmtnttz4uLi9Jvf/Ebp6emO99cNHjxYOTk5+vbbb11cXfXw8Dj7J6J3794aPHiwPv30U0nu+d6snj17av369br99tsVFxcn6eyLCDk5OW55ue+AAQPUpUsXrV27VidOnNDChQv13HPPKScnR8XFxa4u71c7N3Pj5+enmJgYPfbYY8rJyVFQUJA6deqkG264QV988YWLq6wa58baqVMn9e3bV56enpoxY4Z8fX01YMAAGWNUp04dHT161MWVVo1z4x04cKAGDx4s6ezvJH9/f2VnZ8tms8lms+nkyZOuLLPKHDhwQNLZn9nu3burpKRETz/9tB588EG9+OKLKi0t1Zo1axxXk9R2555fLy8v1alTR/369dM111yjRx99VAEBARo2bJjjRVx3fIGztiJQuoCHh4fatWunxx57THl5eercubO6d+/uNr8MylOnTh3H1ytWrNDx48fVoEEDF1ZUPex2u4KDgzVx4kTNmjVLs2bN0k033aTY2FhXl1YtWrVqpSeeeMKxnpycrCNHjqhRo0YurKp6eHp6ql+/fho8eLCWLVumFStWaMWKFfrpp5903XXXubq8ahcZGem4TPD8n2d3EhgYqC5dumjlypXauHGj3n//fWVnZ6tFixauLq3KBQYG6v7771dSUpJmzpwpSZo/f75ycnJq/QsG5+5XMGHCBEnSc889p44dO+pPf/qTcnJy1KhRI/Xo0UNr1qyp9bPP58Y6ceJESWdn28vKyuTv769p06bJz89Pjz32mObNm6eZM2eqqKjI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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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Tp06qWXLltq9e7e2bduml19+OcvR0t27d6tZs2bq0aOHZsyYkWWdXbt26fr163r88cfNx6pVq6aKFStq27ZtWcaXkpIiSRajJ3Z2dnJ2dtbmzZst6n777bcqU6aMatWqpREjRujKlSs59v25557TwoULNW3aNO3fv1+zZs2Sq6urJCk+Pl5hYWFq1KiR/vzzT82cOVNffPGF/vOf/1i0MX/+fJUqVUrbt2/XhAkT9O6775pf299//11t27ZVly5dlJiYqKlTp2aKITk5We3atVONGjW0a9cujR07VoMHD7aoc+HCBT366KOqV6+edu7cqbVr1+rkyZPq0qVLrmNJT09XaGiotmzZom+++Ub79u3Thx9+KHt7e0k3R63atm2rzp07a/fu3fruu++0efNm9e/fP8ef4a0yplb++OOPSkxM1LJlyyRJU6dO1eTJkzVp0iTt3r1bISEh6tChg3nacnbfd+nSJfXu3VubN2/Wb7/9psDAQIWFhenSpUu5juny5csaOHCgdu7cqY0bN8rOzk7h4eFKT0/P9tyDBw9Wly5d1LZtW/P7rEmTJpIkNzc3zZs3T/v27dPUqVM1e/ZsTZkyxXy+1atXKzw8XGFhYYqOjtbGjRsVHBycZWwTJkzQ8OHDtX79ej322GNKTExU9+7dze/nqKgoPfXUUzIMQ9L/v5/62LFjue7/lStXdP36dZUuXVrSzetg9erVevjhhxUSEqJy5cqpcePGFveh5uZ9um3bNtWuXVsPPPCAuU5ISIiSkpL0119/5To+AEWYrTNaALCGNSOUt45I/fXXX4YkY//+/YZh/P8Ryr///tvw9/c33njjjSxH3m5Vs2ZNY/r06eb9SpUqGW3btrWo07VrVyM0NDTHuLIaobw1ptuFhoYar776qnn/9ddfN1q1apVtnBntrlixwuL4raOOZ8+eNSQZUVFRWbaRUXfLli2Gl5eXMWnSpGzPZxiG8e233xpOTk6Zjjdq1MgYOnRolt+TmppqVKxY0XjmmWeMc+fOGSkpKcaHH35oSDKeeOIJc71Zs2YZa9euNXbv3m188803hp+fnxEeHp5tLBkjn7ePKGd4++23japVq1q83p988onh6upq3LhxwzCMm6OCzZo1y9SXYcOGmfc7duxoHpnMcOvrPWvWLMPb29u4evWquXzmzJkW18B7771n0VfDMIy4uDhDknHgwIFcxbJu3TrDzs7OXP92L774ovHyyy9bHPv1118NOzs7i9hycvu1m8HX19d4//33M8X22muv5fh9t7tx44bh5uZmMaqqO4xQ3u706dOGJGPPnj05nvtOv0syTJw40WjQoIF5/5FHHjF69uyZbf2MEcqhQ4ca5cuXtxh537VrlyHJPAPgdtu3bzeqVq1q/PPPP3eMK8Orr75qPPjgg+bXMDEx0ZBkuLi4GP/973+N6OhoY/z48YbJZDK/z3PzPu3bt2+ma/Ly5cvmWQ8AwAglgGLj1lG58uXLS5JOnTplPnb16lU1b95cTz31lKZOnWox8pacnKzBgwerevXq8vT0lKurq/bv359phPKRRx7JtL9///487Uffvn21cOFCXbt2TampqVqwYIFeeOGFO35fw4YNsy0rXbq0+vTpo5CQELVv315Tp05VYmKiRZ3Y2Fi1adNG77zzjgYNGnTP/bido6Ojli1bpoMHD6p06dJycXHRpk2bFBoaKju7///n6uWXX1ZISIhq166tnj176quvvtLy5ct15MiRLNuNiYmRvb29WrZsmWX5/v379cgjj1i83k2bNlVycrL++ecf87HbR3XLly9vcf3cScbI8K0jsLdfL3/++ac2bdokV1dX81atWjVJsuhfTrHExMSoQoUKevjhh7OM488//9S8efMszhESEqL09HQdPXo01/25XVJSkhISEtS0aVOL402bNr3je+DkyZPq27evAgMD5eHhIXd3dyUnJ2d6f+Xk0KFD6t69ux588EG5u7srICBAku6qjVt99913atq0qXx8fOTq6qpRo0ZZtBUTE6PHHnssxzYmT56s2bNna/PmzapZs6b5eN26dfXYY4+pdu3aeuaZZzR79mydP3/eXB4cHKy///5bfn5+uYr1ww8/1KJFi7R8+XLz9ZUxMtuxY0cNGDBAQUFBGj58uNq1a6fPPvss1z8HALgTEkoAxYajo6P564zkIeOfLklydnbW448/roiICIuFLaSbK6IuX75cH3zwgX799VfFxMSodu3aSk1NLZjgb9G+fXs5Oztr+fLlWrVqla5fv66nn376jt9XqlSpHMvnzp2rbdu2qUmTJvruu+/08MMP67fffjOXly1bVsHBwVq4cKGSkpJybMvHx0epqamZHoFy8uRJ+fj4ZPt9DRo0UExMjC5cuKDExEStXbtWZ8+e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bR1OvklPT9eFCxfUsWNHTZkyRatWrTKXFdWFlNevX6+3335bgYGBtg6lQKxfv14jRoxQ1apVbR1Kgcjo7x9//KH33nvP1uHkux9++EHPPvusZs2apddee63IJ9ErVqxQhw4dNHToUL355pvatGlTkf1dJUlxcXH6559/bB1Ggbpx44atQygQp06d0smTJ20dRoGJjY1VXFyc0tPTbR1KgYiJidG+fftsHQbugISyAG3dulVDhgzRrFmz9Msvv+jChQsaOnSorcPKN5s2bdKAAQM0a9YsbdiwQZcuXVJ0dLStw8oXdnZ28vT0VPfu3dWgQQNt3rxZy5YtkySZTCYbR5f3tm7dqueee05ffvmlHn30USUlJenEiRNF+h+YTZs2afTo0Xrqqad09uxZ7du3T0lJSbYOK1/8+OOPeu211/TNN9/o77//1tGjR7V//35bh5Vvzpw5o08//VSLFi3SypUrlZ6erq1bt+rq1au2Di1fnD59WjNnztSCBQv0ww8/yNvbWyNHjtTy5cuLZFK5dOlSPf300+rWrZv+85//aOXKlbYOKd+sXbtW/fr1kyTZ29sX6d/J0s1ZI126dDF/mFvUp+hHRESoa9euevHFFzV+/PgiPShhGIZiY2PVsWNHDR48WDExMRZlKFxIKAvY8OHD1ahRI0nS5MmTdeHCBdsGlI88PT312WefqXHjxkpMTNS+ffs0adIk9e/fX7/99putw8sXDzzwgM6dO6eqVatq+/btmjJliubMmSOpaP0C9Pb2loODg06ePKlLly7pqaee0ksvvaTnnntOGzdutHV4+SI5OVnXr19XWlqawsPDNWzYMPXr10+LFy8uUq+tdHNk46uvvlKtWrV07do1STL/MS9qfZUkR0dHpaSk6O+//9alS5e0a9cuzZgxQ6+88kqRfH0dHR117do1nTlzRtLNv0tubm7avn17kfvQLzk5WVOmTNG0adO0aNEi+fj4aN26dZo3b56tQ8tzO3bs0Msvv6y9e/eqe/fukop2Uvnzzz9r+PDhmjZtmmbPnq2ffvpJa9eutXVY+WbdunV6++23NWPGDM2dO1fr1q1TXFycrcPKNyaTSRUrVlTHjh1Vv359vf/++9q+fbu5DIULCWUBaty4scLDwyXd/KfMMAz9+eef5lGOovZpeL169dS6dWsZhmGeRvbtt9+qQoUKmj59epH6ZC3jH86QkBAFBgbqhRdekGEYevvtt833YxWlX4BVq1bV6tWr9dprr6lGjRrq2rWrli9frsaNG2v+/PlF7lqWpO7du2vRokXq1auX+vbtq1WrVik0NFTr1683/2NeVISEhKhJkyZKT0+Xn5+fnnnmGb399ts6ePBgkbqOM3h4eGjQoEH68MMP1bZtW7344otaunSpQkNDFRkZWeTuqfT09FSPHj301VdfadGiRXrvvffk4+OjUqVK6YsvvrB1eHnKMAyVK1dOZcqUUYUKFRQeHq42bdpo586dWrNmja3Dy1OpqakaO3asNm7cKBcXF3Xr1k1S0U0qExMT9eqrr6pOnTqqXbu2Xn75ZW3YsEHp6elF7kMgSbpw4YI+/PBDNWjQQM7Ozjp58qRGjhypKVOmFMkPclNTU3Xjxg3Z29urfPnyCg0N1SeffKJvvvlGS5culVQ0P+C8X5FQFiB7e3u5u7tLujkCULZsWXl5ecnd3V1ff/213n333SL5S99kMmn06NF65513JN38NPzMmTM6duyYbQPLQxn/ZJcuXVoxMTH6+uuvtXTpUr300ktKTEzU6tWrbRxh3qtbt64iIiI0bNgw9e3bV46OjnrjjTd06tSpIvXaZggODtZTTz2lvXv3mu+v69Gjh06fPq2///7bxtHlDzu7m38iOnXqpB49emjz5s2Siua9We3bt9eGDRvUvHlzBQcHS7r5IcLp06eL5HTfrl27qnXr1lq3bp0uXryo+fPn65133tHp06d1/fp1W4d3zzJGbtzc3BQUFKRXXnlFp0+flre3t5o1a6bq1atr9+7dNo4yb2T0tVmzZurcubMcHBw0ZcoUubq6qmvXrjIMQ/b29jp37pyNI80bGf3t1q2bevToIenm7yR3d3clJCTIZDLJZDLp8uXLtgwzz8TGxkq6+Z4NCwtTWlqa3nzzTT377LPm/xvXrl1rnk1yv8t4fZ2cnGRvb69nnnlGJUqU0AsvvCAPDw/9+9//Nn+IWxQ/4LxfkVDaiIODg0qXLq2AgAD95z//0ccff6zu3bvL3t7e1qHli1v79f333+vChQvy8vKyYUT5w8/PT+XLl9fo0aM1ffp0TZ8+XY0aNVKDBg1sHVq+qFGjhvr372/eX7p0qc6cOaMyZcrYMKr84eDgoGeeeUY9evTQ4sWL9f333+v777/XP//8owcffNDW4eW7wMBA8zTBovp7ytPTU61bt9aKFSv0008/adWqVUpISFCVKlVsHVqe8/T0VM+ePTVnzhxNmzZNkjR37lydPn36vv/AYNWqVWrTpo1GjRolSXrnnXfUuHFjDRkyRKdPn1aZMmXUrl07rV279r4ffc7o6+jRoyXdHG1PT0+Xu7u7Jk2aJDc3N73yyiuaPXu2pk2bptTUVBtHfG9WrVqlxx9/3PwBtbe3tzlhDgwMlJubm0wmk77++mvNnj37vv9wZNWqVXriiSfMr69082/RrFmzNGrUKFWpUkVdunTRnj17dOnSJRtGmjcyrueM11e6OVIZExOjtWvXKjIyUr169VJkZKT++OMPG0aK25kMxottwjAMpaSkmFeM/PHHH4v8apkpKSn69ttv9d///leLFi1SrVq1bB1SvoiLi9OpU6fMSWRaWpocHBxsHFX+MgxDc+fO1aRJk7R48eIi+9pKN/+4bd++XV9++aXs7Oz0xhtvqG7durYOq0B06dJFkyZNUsWKFW0dSr65cOGCvvrqKy1fvlyOjo6aOHFisXh9v/jiC02cOFFLlixR7dq1bR2O1ZKTk/Xoo4+qXbt2Onv2rLy8vDR27FidP39ekyZN0pYtWzRz5kzt3LlTn3/+uVauXHnffriZXV8lmacKSlJQUJBiY2O1adOm+/pazqm/knT27Fm98cYbCgoK0vz587V48WLVqFHDdgHfo5z6e+v/FcuXL9f06dO1dOnS+/ZalnLub79+/bRo0SJ9+eWX6tixo+bMmaO2bduqQoUKtg0aZiSUNjZv3jwFBwff17/0cislJUVr1qxR9erVVa1aNVuHk+8y3lrFYUqGYRj6+eef5ePjUyxeW+n/T/ssqqN1tzIMo1hcx7e6dOmS0tPT5eHhYetQCsSxY8d0/fr1IvHBZkJCgpycnPS///1Ps2bNUqVKlcwjHh988IH+/vtvJSYmasKECapXr56No703t/b1888/V4UKFSySrIiICL311ltasWJFkfigL6f+njhxQg0bNlTp0qW1ZMmSIvGIp5z6m5qaqlmzZunLL7/UV199dV9/EJTh9v76+flp3Lhx2rJlixwcHNS4cWNJxeOD+vsNCaWNFcd/1AAAyG/p6enatWuXPvvsM1WoUEHjxo3T2bNnZTKZ5OrqKicnJ1uHmGcy+jpr1ixz0hEbG6vdu3erWrVqRW7adlb9vXDhgqZOnapOnTrd1yOxWcmqv4cOHdLGjRvVvHlz1axZ09Yh5qmM/s6cOVMPPfSQRo4cqfj4eLm6uhabD/nuNySUAACgSEpNTdWff/6phQsX6vfff9fJkye1devWInmf96193blzp06ePKnffvvtvp4GmZNb+7tjxw6dOXNGv/32mzw9PW0dWr64tb+7du3SiRMntH379iLf3wULFuiPP/5QYmJikX3vFgUsygMAAIokJycnNWrUSI6Ojjp69Ki+//77IvsP6a19/d///qclS5YU2WRSsuzvsWPH9N133xXZ5Eqy7O+RI0e0ePHiYtFfJycnHTlypEi/d4sCJiADAIAi69y5c1q/fr3WrFmjOnXq2DqcfFWc+irR36KuuPX3fsaUVwAAUKRdvXpVJUuWtHUYBaI49VWiv0Vdcevv/YqEEgAAAABgFe6hBAAAAABYhYQSAAAAAGAVEkoAAAAAgFVIKAEAAAAAViGhBAAAAABYhYQSAIAirFWrVvr4449tHQYAoIgioQQAFCutWrWSvb29du/ebT524cIFmUwmHTt2zHaBAQBwHyKhBAAUO15eXhoxYkSu61+/fj0fowEA4P5FQgkAKHZee+01bdmyRb/88kuW5WPHjlW7du306quvqnTp0ho+fHimOrGxsWrTpo3Kli0rLy8vPfnkkxYjnLdPNY2JiZHJZJIkJSUl6aGHHtKcOXPM5e3bt9fzzz+fbcwbNmxQ48aN5enpqfLly2v8+PHmsm+++UbVq1eXp6enmjVrpj/++CPLNs6dO6fw8HB5eXnJ09NTDRo00PHjx7M9JwAAd0JCCQAodkqXLq1hw4ZlmShmWLt2rRo3bqxTp07pvffey1Senp6ugQMHKi4uTsePH5eLi4v69u2bq/O7u7trwYIFGjx4sP7++29NnTpVBw8e1IwZM7KsHx0drY4dO2ro0KE6ffq0/v77b7Vu3VqS9Msvv+jVV1/VrFmzdPr0aT399NNq27atLl68mKmdSZMmKS0tTfHx8Tp79qy++OILubm55SpmAACyQkIJACiW3nrrLR0/flwrVqzIsrxWrVrq06ePHBwc5OLikqk8ICBAoaGhKlGihNzd3TVy5Ej9+uuvSk9Pz9X5GzdurGHDhqljx44aPXq0Fi5cqFKlSmVZ9/PPP1e3bt3UuXNnOTo6ysPDQ//6178kSV9//bV69eqlFi1ayNHRUW+99Za8vLy0evXqTO04Ojrq7NmzOnTokOzt7RUUFKTSpUvnKl4AALJCQgkAKJZKliypMWPG6O2339aNGzcylVesWDHH7z99+rR69Oghf39/ubu7q0WLFkpJSdGlS5dyHcOLL76oY8eOqWXLlqpfv3629Y4fP67AwMAsy/755x8FBARYHKtcubL++eefTHWHDBmi5s2bq0uXLvLx8dGbb76pq1ev5jpeAABuR0IJACi2XnzxRaWnp2v+/PmZyuzscv4TOWLECF25ckV//PGHkpKSzPdjGoYhSXJ1ddWVK1fM9RMTE7M8f/v27bV9+3atXLky23NVqlRJhw8fzrKsQoUKmVanPXbsmCpUqJCprqurqz766CMdOHBA27Zt08aNG/Xpp5/m2E8AAHJCQgkAKLbs7e31/vvv64MPPrjr701KSpKLi4s8PT119uxZjRs3zqK8fv36WrZsmS5evKhTp05pwoQJFuXTpk3TwYMHNX/+fH355Zd68cUXlZCQkOW5+vbtq4ULF2r58uVKS0vTxYsX9dtvv0mSevXqpW+//VZbtmxRWlqapk+frrNnzyosLCxTOxERETp48KDS09Pl7u4uR0dHOTg43HXfAQDIQEIJACjWOnfurCpVqtz1940bN06HDx+Wl5eXmjZtqtDQUIvyAQMGqHz58vL399ejjz6qrl27mst2796tUaNGme+bbNeunXr06KFnn302y3sw69evr6VLl+r9999X6dKlVb16df3888+SpJYtW2r69Ol68cUX5e3trUWLFikyMlKenp6Z2jl8+LDatm0rNzc31ahRQ4888oheffXVu+47AAAZTEbG3BwAAAAAAO4CI5QAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAqJJQAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAqJJQAAAAAAKuQUAIAAAAArEJCCQAAAACwCgklAAAAAMAq/w8PPyP3AXXc+gAAAABJRU5ErkJggg==", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA5QAAAG0CAYAAABNBWhXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAACR40lEQVR4nOzdd1gUV9sG8HsFARVBxQoasSB2scaOHbGg2GPviTX2EntJbEGNLcaGvQsWBLvYGyp2EREFARsqCEp/vj98dz5XRWVFl8X7d1176c6cnX0OU3aeOWfOqEREQERERERERJRCGXQdABEREREREeknJpRERERERESkFSaUREREREREpBUmlERERERERKQVJpRERERERESkFSaUREREREREpBUmlERERERERKQVJpRERERERESkFSaUREREREREpBUmlESklx49eoSGDRsiS5YsyJYtm67D+W4mT54MOzu7VC9LpLZ69Wq936fu378PlUoFX19fXYfyxVQqFXbu3KnrMIiIUowJJRHpXPfu3dGyZcsUfWbevHkICwuDr68v7ty5820CS4NGjBiBw4cP6zqM78ra2hrz58/XdRg/jPbt26f6PuXt7Q2VSoWXL19+l8+lhuS+u06dOhgyZMh3j+drvXr1CkOGDEHBggWRKVMmVK9eHRcuXNAo4+bmhkaNGsHCwiLZhLxOnTpQqVQar99+++2DcqtXr0bZsmVhYmKC3LlzY8CAAd+qakSkY4a6DoCISBsBAQGoWLEibGxstF5GXFwcjIyMUjGqb0dEkJiYCFNTU5iamuo6nFSjT+vgRxAfH49MmTIhU6ZMug6FUlnv3r1x/fp1rFu3DpaWlli/fj0aNGiAmzdvwsrKCgAQHR2NmjVrol27dujTp0+yy+rTpw+mTp2qvM+cObPG/Llz58LFxQVz5szBzz//jOjoaNy/f/+b1IuI0gAhItKxbt26SYsWLZT39vb2MmjQIBk5cqRkz55d8uTJI5MmTVLmFyxYUAAor27duomIyIsXL6RXr16SM2dOyZo1q9StW1d8fX2Vz02aNEnKlSsny5cvF2tra1GpVCn63Nq1a6VgwYJiZmYm7du3l8jISKVMYmKizJo1S4oUKSJGRkZSoEABmT59ujI/KChI2rZtK+bm5pI9e3ZxcnKSwMDAZP8mR48eFQDi6ekpFSpUkIwZM8rRo0eV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Rlw8rqQsn+Ior0REpBtZftK7BI+IiIg0scsrERERERERaYUJJREREREREWmFCSURERERERFphQklERF9Fz/y4Ab0edw+iIj0ExNKIiL6pjJmzAgAeP36tY4jobRMvX2otxciItIPHOWViIi+KQMDA2TLlg1PnjwBAGTOnFkZkp2+TExMTJpaTmp68+YNXr9+jSdPniBbtmwwMDDQdUhERJQCTCiJiOiby5s3LwAoSSV9mWfP3j6nMzAwME0sJ7U9e/YM9+/fBwBky5ZN2U6IiEh/MKEkIqJvTqVSIV++fMidOzfi4+N1HY7ecHR0BADcvn07TSwntTk6OuL27dvImDEjWyaJiPQUE0oiIvpuDAwMmDikwIMHDwAAJiYmaWI5qe3BgwdpLiYiIkoZDspDREREREREWmFCSURERERERFphQklERERERERaYUJJREREREREWmFCSURERERERFphQklERERERERaYUJJREREREREWmFCSURERERERFphQklERERERERa0VlC6eHhAVtbW9jY2GDFihUfzK9Tpw6KFy8OOzs72NnZ4c2bNzqIkoiIiIiIiJJjqIsvTUhIwLBhw3D06FGYm5ujYsWKcHZ2hoWFhUa57du3o3Tp0roIkYiIiIiIiD5DJy2U58+fR6lSpWBlZQVTU1M4OjriwIEDWi8vNjYWkZGRGi8iIiIiIiL6tnTSQhkaGgorKyvlvZWVFUJCQj4o17FjRxgYGKBLly4YNmxYssubMWMGpkyZ8k1i/d5UKhUAQETSxHJSXyrFMzl1FpNS//uzfl2ZyV+wEACqKZ8uJ5PS2rrVbyqVKt3ud6l5XElrdQP0e919yTHli8qlwnGFx5TUld5/z9PzcUXf1933PK7o4lyF52JpT5odlGfDhg24evUqvL29sWvXLuzduzfZsmPHjkVERITyCg4O/o6REhERERER/Zh0klBaWlpqtEiGhITA0tJSo4y6BdPc3Bzt2rXDhQsXkl2esbExzMzMNF5ERERERET0bekkoaxSpQquX7+OkJAQREVFwcvLCw4ODsr8hIQEPHv2DAAQFxcHLy8vlCpVShehEhERERERUTJ0cg+loaEhXFxcULduXSQlJWHUqFGwsLBAkyZNsGLFCpibm8PBwQHx8fFITExE8+bN0aZNG12ESkRERERERMnQSUIJAE5OTnByctKY5unpqfz/4sWL3zskIiIiIiIiSoE0OygPERERERERpW1MKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrTCiJiIiIiIhIK0woiYiIiIiISCtMKImIiIiIiEgrOksoPTw8YGtrCxsbG6xYseKjZZKSkvDzzz+jTZs23zk6IiIiIiIi+hxDXXxpQkIChg0bhqNHj8Lc3BwVK1aEs7MzLCwsNMqtXLkS1tbWSExM1EWYRERERERE9Ak6aaE8f/48SpUqBSsrK5iamsLR0REHDhzQKPP8+XNs3rwZffv2/ezyYmNjERkZqfEiIiIiIiKib0snCWVoaCisrKyU91ZWVggJCdEoM27cOEyYMAEGBgafXd6MGTNgbm6uvAoUKJDqMRMREREREZGmNDkoz+XLl/HixQvUqVPni8qPHTsWERERyis4OPjbBkhERERERES6uYfS0tJSo0UyJCQEVapUUd6fPXsWJ06cgLW1NWJiYvDq1Sv07dsXy5Yt++jyjI2NYWxs/M3jJiIiIiIiov+nkxbKKlWq4Pr16wgJCUFUVBS8vLzg4OCgzO/Xrx9CQkJw//59bN68GY6Ojskmk0RERERERKQbOkkoDQ0N4eLigrp168LOzg7Dhw+HhYUFmjRpgtDQUF2ERERERERERCmkky6vAODk5AQnJyeNaZ6enh+Uq1OnzhffS0lERERERETfT5oclIeIiIiIiIjSPiaUREREREREpBUmlERERERERKQVJpRERERERESkFSaUREREREREpBWtEsqdO3d+dPqff/75NbEQERERERGRHtEqoRw4cCBOnjypMW3mzJlYs2ZNqgRFREREREREaZ9WCeX27dvRoUMH3LhxAwDw999/Y/ny5Thy5EiqBkdERERERERpl6E2H6patSr+++8/NG3aFJ07d8aGDRtw7Ngx5M+fP7XjIyIiIiIiojTqixPKyMhIjfe1atXCkCFDMHv2bHh5eSFbtmyIjIyEmZlZqgdJREREREREac8XJ5TZsmWDSqXSmCYiAIAKFSpARKBSqZCYmJi6ERIREREREVGa9MUJZWBg4LeMg4iIiIiIiPTMFyeUBQsWTHbe06dPYWhoiOzZs6dKUERERERERJT2aTXK64ABA3D27FkAwLZt22BpaYk8efJgx44dqRocERERERERpV1aJZRubm4oV64cgLfPn9y6dSv27duHyZMnp2ZsRERERERElIZp9diQ6OhoZMqUCc+ePcP9+/fh7OwMAAgKCkrV4IiIiIiIiCjt0iqhLFSoEDZu3Ah/f3/UrVsXAPDy5UsYGRmlanBERERERESUdmmVUP7999/o3r07jIyM4O7uDgDw8PBA5cqVUzU4IiIiIiIiSru0SigbNmyIkJAQjWnt27dH+/btUyUoIiIiIiIiSvu+OKF89eoVsmbNCgCIjIxMtlzGjBm/Pioi+moqlQoiouswiIj0gkqlAgAeN4m+o/S+3/0o52JfnFBaWVkpiWS2bNmUDUBNRKBSqZCYmJi6ERIREREREVGa9MUJ5Y0bN5T/BwYGfrRMcHDw10dEREREREREeuGLE8oCBQoo/zc1NUX27NmRIcPbx1g+evQIf/75J1auXInXr1+nfpRERERERESU5mRISeGLFy+iYMGCyJ07N/Lly4dTp07h33//hY2NDR48eIAjR458qziJiIiIiIgojUnRKK8jRoxAhw4d0K1bN6xYsQJt27aFlZUVjh8/jvLly3+rGImIiIiIiCgNUkkKhh7KmTMnHj16BENDQ7x58wampqZ4+PAh8uXL9y1jTLHIyEiYm5sjIiICZmZmug5Hw3tjGWlvcuosSCal7shTaal+qV03IJXq953WXUpHFuO6+wLc7z6L6y5luO6+wHc8ZgJfPtpkWlp3ALfNlNKnbTOl9GndaTPKqz6tO30e5TUl+VSKurzGxcXB0PBto2amTJlgbm6e5pJJIiIiIiIi+j5S1OU1Li4OCxYsUN7HxsZqvAeAwYMHp05kRERERERElKalKKGsWrUq3N3dlfdVqlTReK9SqZhQEhERERER/SBSlFB6e3t/ozCIiIiIiIhI36ToHkoiIiIiIiIiNSaUREREREREpBUmlERERERERKQVJpRERERERESkFZ0llB4eHrC1tYWNjQ1WrFjxwfzatWujXLlyKFmyJKZOnaqDCImIiIiIiOhTUjTKa2pJSEjAsGHDcPToUZibm6NixYpwdnaGhYWFUsbDwwNmZmZISEhAzZo10bx5c5QvX14X4RIREREREdFH6KSF8vz58yhVqhSsrKxgamoKR0dHHDhwQKOMmZkZACA+Ph7x8fFQqVS6CJWIiIiIiIiSoZOEMjQ0FFZWVsp7KysrhISEfFCuevXqyJ07Nxo0aAA7O7tklxcbG4vIyEiNFxEREREREX1baXpQntOnTyM0NBS+vr64fv16suVmzJgBc3Nz5VWgQIHvGCUREREREdGPSScJpaWlpUaLZEhICCwtLT9aNmvWrKhfvz727duX7PLGjh2LiIgI5RUcHJzqMRMREREREZEmnSSUVapUwfXr1xESEoKoqCh4eXnBwcFBmR8REYGnT58CeNuddf/+/ShevHiyyzM2NoaZmZnGi4iIiIiIiL4tnYzyamhoCBcXF9StWxdJSUkYNWoULCws0KRJE6xYsQLx8fFo3bo14uLikJSUhHbt2qFZs2a6CJWIiIiIiIiSoZOEEgCcnJzg5OSkMc3T01P5v4+Pz/cOiYiIiIiIiFIgTQ/KQ0RERERERGkXE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSis4SSg8PD9ja2sLGxgYrVqzQmPf69Ws4OjqiePHiKFWqFBYuXKijKImIiIiIiCg5hrr40oSEBAwbNgxHjx6Fubk5KlasCGdnZ1hYWChlxowZA3t7e0RFRaFSpUpwdHRE0aJFdREuERERERERfYROWijPnz+PUqVKwcrKCqampnB0dMSBAweU+ZkzZ4a9vT0AwNTUFLa2tggLC0t2ebGxsYiMjNR4ERERERER0belk4QyNDQUVlZWynsrKyuEhIR8tGxwcDCuXr2KChUqJLu8GTNmwNzcXHkVKFAg1WMmIiIiIiIiTWl6UJ7Y2Fi0b98ec+bMQZYsWZItN3bsWERERCiv4ODg7xglERERERHRj0kn91BaWlpqtEiGhISgSpUqGmVEBF27dkWTJk3Qpk2bTy7P2NgYxsbG3yRWIiIiIiIi+jidtFBWqVIF169fR0hICKKiouDl5QUHBweNMmPHjkXmzJkxfvx4XYRIREREREREn6GThNLQ0BAuLi6oW7cu7OzsMHz4cFhYWKBJkyYIDQ3Fw4cPMWvWLJw/fx52dnaws7PD/v37dREqERERERERJUMnXV4BwMnJCU5OThrTPD09lf+LyPcOiYiIiIiIiFIgTQ/KQ0RERERERGkXE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0oiIiIiIiLSChNKIiIiIiIi0goTSiIiIiIiItIKE0r6JJVKpesQvhmVSpWu60f6Kz1vl+l9v0vP9UvPdfsRpOd1l963zfRcN0ofmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWmFASERERERGRVphQEhERERERkVaYUBIREREREZFWdJZQenh4wNbWFjY2NlixYsUH8wcMGIA8efKgUqVKOoiOiIiIiIiIPkcnCWVCQgKGDRuGI0eO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adOUlgV98O462LVrl5ibm0uPHj2Ue7QSExPF1dVV+vbtK1u3btXLdaZ27NgxcXd3l8uXL0tSUpLMnTtXRo0aJTt27JAzZ87I77//LqdOndJ1mF/txIkT4ubmJt7e3lKpUiW5d++eiOh3q+v8+fPl559/lnbt2kmnTp3kwoULcuHCBbGxsdHre+4SExMlLCxMnJ2dpXr16nLp0iVl3n///SetW7fW61Y7dctHjx49xNLSUhYvXiwi/3/cmTlzplSuXFlevXqlt60k27dvl3bt2klgYKDcv39f4767IkWKSN++ffX2ItXhw4elUKFCGsfF4OBgadasmUyfPl3vEq1PefPmjZw/f16ePXsmIm9Hem3YsKHSGqsv1PvW4cOHpV+/fjJv3jzx9fWVpKQkuXz5svzzzz9StGhR6dmzp95epPpRMKH8BjZt2iSOjo4yb948qVWrlsZw1Pr2I/To0SOpVq2a8ry32bNnS/369eXs2bPy+PFj8fb21uvhtuPi4uTEiRPSqVMnWbFihbx580YaNWokvXv3lokTJ0q5cuX0KuF614sXL6R169bSqFEj5flTr169kjp16kiPHj00yupD4vWxGO/fvy+LFy+WQYMGydmzZ0Xk7WNsli9fLk+ePPneIX61d3sEFChQQAYNGiRNmjSRlStXiojIypUrpWvXrlKkSBHx8PDQZahf7cKFC3L48GG5ffu25MmTR4oXL64MinHo0CEZNGiQxr00+iApKUn8/f2lUqVK8vTpUwkLC5O9e/dK8+bNJSQkRPbu3SvlypWTV69e6cU+J/Lx/c7Pz0+6dOkiixYt0rivdeXKlRIaGvo9w0sV6jqGhoYqra7h4eFibW0t06dPF5G395rfuXNHgoODdRbn1woJCZHChQtL7969RUTkyZMn0rhxY9m6dats3bpVOnbsKIGBgboNUguJiYkSHx8vI0eOlMWLF0tCQoKsXbtWOnToIMuXL5dHjx5JvXr1lItV6UliYqKsWLFCSpUqpXeNFWonTpyQggULiqurq4wcOVL69+8vq1at0pjfqlUrvXuk0o+GCeU38PLlS1m7dq20atVK46RPX04g3pWUlCQdOnTQuHewX79+YmNjo/fPpFKLjY2VEydOyC+//CKrV6+W+Ph42bFjhyxatEh5DqO+rbsbN27Is2fP5MyZM9K6dWtZv3690r0uMjJSqlev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K1asICMjg9atW7Nz504WLVpEnz59LF2aiIjIP+rWGswAlZWVmoBHqkWBUkRsntFo5MSJExQUFODr60v79u3VzVVERETkHihQioiIiIiISLWoXVtERERERESqRYFSREREREREqkWBUkRERERERKpFgVJERERERESqRYFSREREREREqkWBUkRERERERKpFgVJERERERESqRYFSRETERhgMBjIyMixdhoiIPEAUKEVExKadPn2a0NBQ6tevj5ubG8HBwcTHx1uklpiYGMLCwixybBERkepQoBQREZvWp08f2rZtS25uLsXFxSQlJREQEHDfj2Myme77Y4qIiFiaAqWIiNiswsJCzp49S0REBM7OztjZ2dG6dWsGDRpk3qesrIzIyEh8fX1p2LAhI0eOpKSkxLw9MzOTZ555hgYNGuDh4cGAAQMAyM7OxmAwsHbtWgIDA2ncuDEAX331Fd26dcPDw4PAwEASExMB2L59OwsWLGD37t24uLjg4uJy15orKytZvnw5wcHBuLq6EhQUxL59+4BfQmtUVBS+vr40aNCAIUOGUFBQ8I+8diIiIqBAKSIiNqxevXq0aNGCF154gc2bN5OTk/OHfUaPHk1RURHffPMNWVlZmEwmIiMjATAajYSEhNCmTRuys7PJz89n4sSJt91/586dnDhxgqysLPLz8+nRowfjx4+noKCA7du3Ex0dzcGDBwkLC+PVV1+lb9++lJWVUVZWdtea33rrLd588002btxIaWkpBw8exM/PD4C4uDh2797N4cOHycrKwmAwMHz48Pv8qomIiPzGUFVVVWXpIkRERCwlPz+fhIQE9u3bx6lTp2jRogXLli2jR48eFBQU4OXlRWFhIXXr1gV+aZFs3bo1165dY+vWrcyePZvMzEwMBsNtj5udnY2/vz9ff/01Dz30EAAJCQkcPXqUbdu2mfebPXs2+fn5rF69mpiYGDIyMti+ffuf1tuyZUuioqIYOXLkH7YFBQUxf/58hgwZAsCFCxfw8fHh/PnzeHt7YzAYbqtHRETk77K3dAEiIiKW5OXlxeLFi1m8eDFFRUW88cYb9O/fn9zcXLKzs6msrMTf3/+2+9SoUYP8/HxycnJo1qzZH8Lk7/n6+pp/z87OZu/evdSpU8f8t4qKCrp06XLP9ebk5BAUFHTXbXl5eTRt2tR829vbm5o1a5KXl4e3t/c9H0NEROReqcuriIjIrzw8PIiJicFoNJKVlUWTJk2oUaMGFy5c4PLly+af69ev4+Pjg5+fH2fPnuWvOvvUqPHbqbZJkyb079//tse6cuUKe/fu/cO+f8bPz48zZ87cdVvjxo3Jzs42387Pz+fGjRvm8ZsiIiL3mwKliIjYrOLiYubMmcOpU6eoqKjg6tWrLFmyBA8PD4KDg/Hy8iIsLIzIyEgKCwuBX0LarS6rffr04caNG8ydOxej0cjNmzf5/PPP//R4I0aMIDk5maSkJEwmEyaTiYyMDNLS0gDw9PQkJyeH8vLyP32MiIgIYmNjycjIoKqqitzcXH744QcAwsPDWbBgAefOnaOsrIypU6cSEhKi1kkREfnHKFCKiIjNcnR05Pz58/Tu3Rt3d3d8fX05cuQIn3zyCbVr1wbgvffeo06dOrRr1w43Nze6dOlCeno6AC4uLnz22Wekp6fj6+tLo0aNWLly5Z8ez8fHh/379/P222/TqFEjPD09efnllyktLQVg0KBBuLm50aBBg9u6xf7epEmTGD9+PIMHD8bV1ZWQkBByc3MBiIqKomfPnnTq1ImmTZtiMpnYsGHDfXzFREREbqdJeURERERERKRa1EIpIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItWiQCkiIiIiIiLVokApIiIiIiIi1aJAKSIiIiIiItXyHwNqNg+VqtyCAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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0t/fH3p6ekrC+zENGjSAg4MDNm/eDD09PY1iSQvs8kpEREREROlOt27dsGrVKqxevfqjXVKbNWuGZ8+eYdmyZThz5gzOnDkDAEhISPjksjNnzqz8X/X/H1PwsW6yp06dQocOHdC4cWN4eXnh0qVLGD169GfX8TFDhw6Fp6cn/v77bxw7dgz+/v4oU6aMsrzUls3PadKkCY4ePYobN25oFEdaYQslERERERGlO40aNUJCQgJUKhWcnJzemx8ZGYmAgAAsW7YMNWvWBAAcP348zeM4efIkChUqhNGjRyvTUrvipsqSJQuSk5Pf+2zmzJnfm37ixAl07doVrq6uAN60WN67d0+ZX6ZMGaSkpODIkSNKl9cPmT59OoyNjVGvXj34+fmhVKlSmlTvmzGhJCIiIiKidEdPTw83b95U/v+u7Nmzw8LCAkuXLkW+fPkQEhKCkSNHpnkcNjY2CAkJwaZNm1C5cmXs2bMHnp6eamWsra0RHBwMf39/5M+fHyYmJjAwMIC1tTUOHjyI6tWrw8DAANmzZ4eNjQ08PDzQrFkzqFQqjB07Vq111NraGl26dEG3bt0wf/58lCtXDvfv38fjx4/Rpk0btfX+888/SE5ORt26deHn54cSJUqkef0/h11eiYiIiIh+AjlzAh8Zc+a7MTR8s15NmZqafvRexUyZMmHTpk24cOECSpcujUGDBmHWrFmar+wjXFxcMGjQIPTr1w/29vY4efIkxo4dq1amVatWaNSoEerUqYNcuXJh48aNAAA3Nzf4+vqiQIECyiNPZs+ejezZs6NatWpo1qwZnJycUKFCBbXl/fvvv2jdujX++OMPlChRAj179kRsbOwH45szZw7atGmDunXr4vbt22le/89RiYj88LV+Z9HR0TAzM0NUVJTGN8uSZv5/F/RvN+HbFyTjM9yuTUT0QaqJPGYS0f+Ji4tDcHAwChcu/N6opSEhwNOnPy6WnDmBj4xzQ1r2qf3ka/IpdnklIiIiIvpJFCzIBI/SFru8EhERERERkUaYUBIREREREZFGmFASERERERGRRphQEhERERERkUaYUBIREREREZFGmFASERERERGRRphQEhERERERkUb4HEoiIiIiop9ESFQInr56+sPWlzNrThQ044MvMzImlEREREREP4GQqBDYLrRFXFLcD1unob4hAvoFaCWprF27Nuzt7TF3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fvnjUr6Gqakp2rZti2XLlmHz5s3Yvn07nj17psx3cXHBhg0b0KNHD2zatOm7xPA2JpRERERERJRu/frrr9DT08OiRYtgYmKCoUOHYtCgQVi9ejWCgoJw8eJFLFiwAKtXrwYAjBs3Djt37sSdO3dw/fp1eHl5oWTJkh9c9v/+9z8EBgZi2LBhCAgIwIYNG7Bq1Sq1MrVr18aTJ08wc+ZMBAUFYdGiRfDx8VErY2Njg7Vr1+LmzZs4c+YMOnToACMjozT/W8yePRsbN27ErVu3cPv2bWzduhV58+aFubm5WjlXV1esXbsWv//+u9oIuN8DE0oiIiIiop+BQU4gk+GPXWcmwzfr/Qb6+vro168fZs6cidjYWEyePBljx47FtGnTULJkSTRq1Ah79uxB4cKFAQBZsmTBqFGjULZsWdSqVQt6enofbakrWLAgtm/fjh07dqBcuXJYsmQJ/v77b7UyJUuWxOLFi7Fo0SKUK1cOZ8+efa8L7YoVK/D8+XNUqFABnTp1woABA5A7d+5vqveHmJiYYObMmahUqRIqV66Me/fuwdvbG5kyvZ/WtW7dGqtXr0anTp3g4eGR5rGkUsm7HYIzgOjoaJiZmSEqKgqmpqbaDuenolKl0YImfPuCZHyG27WJiD5INZHHTCL6P3FxcQgODkbhwoVhaPhOAhkbAsR/n66YH2SQE8hW8Metj77Yp/aTr8mnOMorEREREdHPIltBJniUptjllYiIiIiIiDTChJKIiIiIiIg0woSSiIiIiIiINMKEkoiIiIgoA8qAY29SGkqr/YMJJRERERFRBpI5c2YAwKtXr7QcCaVnqftH6v6iKY7ySkRERESUgejp6cHc3ByPHz8GAGTNmhWqNHu2G+k6EcGrV6/w+PFjmJubQ09P75uWx4SSiIiIiCiDyZs3LwAoSSXRu8zNzZX95FswoSQiIiIiymBUKhXy5cuH3LlzIzExUdvhUDqTOXPmb26ZTMWEkoiIiIgog9LT00uzxIHoQzgoDxEREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaURrCaWXlxdsbW1hY2OD5cuXvze/du3aKFGiBOzt7WFvb4/Xr19rIUoiIiIiIiL6GH1trDQpKQmDBw/G4cOHYWZmhooVK8LV1RUWFhZq5bZt24bSpUtrI0QiIiIiIiL6DK20UJ49exZ2dnawsrKCsbExnJ2dsX//fo2XFx8fj+joaLUXERERERERfV9aaaEMDw+HlZWV8t7KygphYWHvlWvfvj309PTQqVMnDB48+KPLmzZtGiZOnPhdYk1rKlUaLWhC2ixIxkuaLOdnkSbbj9uO0inVxG/fN7lfEtHPIi2OmQCPm1+L52LpT7odlGf9+vW4cuUK/Pz8sHPnTuzZs+ejZUeNGoWoqCjlFRoa+gMjJSIiIiIi+jlpJaG0tLRUa5EMCwuDpaWlWpnUFkwzMzO0adMG586d++jyDAwMYGpqqvYiIiIiIiKi70srCaWDgwOuXbuGsLAwxMTEwMfHB05OTsr8pKQkPH36FACQkJAAHx8f2NnZaSNUIiIiIiIi+git3EOpr68PNzc31KlTBykpKRg+fDgsLCzQuHFjLF++HGZmZnByckJiYiKSk5PRrFkztG7dWhuhEhERERER0UdoJaEEABcXF7i4uKhN8/b2Vv5/4cKFHx0SERERERERfYV0OygPERERERERpW9MKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjTCiJiIiIiIhII0woiYiIiIiISCNMKImIiIiIiEgjWksovby8YGtrCxsbGyxfvvyDZVJSUvDLL7+gdevWPzg6IiIiIiIi+hx9baw0KSkJgwcPxuHDh2FmZoaKFSvC1dUVFhYWauVWrFgBa2trJCcnayNMIiIiIiIi+gSttFCePXsWdnZ2sLKygrGxMZydnbF//361Ms+ePcOmTZvQq1evzy4vPj4e0dHRai8iIiIiIiL6vrSSUIaHh8PKykp5b2VlhbCwMLUyo0ePxtixY6Gnp/fZ5U2bNg1mZmbKq0CBAmkeMxEREREREalLl4PyXLp0Cc+fP0ft2rW/qPyoUaMQFRWlvEJDQ79vgERERERERKSdeygtLS3VWiTDwsLg4OCgvD99+jSOHTsGa2trxMXF4eXLl+jVqxeWLl36weUZGBjAwMDgu8dNRERERERE/0crLZQODg64du0awsLCEBMTAx8fHzg5OSnz+/Tpg7CwMNy7dw+bNm2Cs7PzR5NJIiIiIiIi0g6tJJT6+vpwc3NDnTp1YG9vjyFDhsDCwgKNGzdGeHi4NkIiIiIiIiKir6SVLq8A4OLiAhcXF7Vp3t7e75WrXbv2F99LSURERERERD9OuhyUh4iIiIiIiNI/JpRERERERESkESaUREREREREpBEmlERERERERKQRJpRERERERESkEY0Syh07dnxw+tSpU78lFiIiIiIiItIhGiWU/fr1w/Hjx9WmTZ8+HatXr06ToIiIiIiIiCj90yih3LZtG9q1a4fr168DAP755x8sW7YMhw4dStPgiIiIiIiIKP3S1+RDVapUwX///YcmTZqgY8eOWL9+PY4cOYL8+fOndXxERERERESUTn1xQhkdHa32vmbNmhg4cCBmzpwJHx8fmJubIzo6GqampmkeJBEREREREaU/X5xQmpubQ6VSqU0TEQBAhQoVICJQqVRITk5O2wiJiIiIiIgoXfrihDI4OPh7xkFEREREREQ65osTykKFCn103pMnT6Cvr4/s2bOnSVBERERERESU/mk0ymvfvn1x+vRpAMDWrVthaWmJPHnyYPv27WkaHBEREREREaVfGiWUHh4eKFeuHIA3z5/csmUL9u7diwkTJqRlbERERERERJSOafTYkNjYWBgZGeHp06e4d+8eXF1dAQAhISFpGhwRERERERGlXxollIULF8aGDRsQGBiIOnXqAABevHiBLFmypGlwRERERERElH5plFD+888/6Nq1K7JkyQJPT08AgJeXFypXrpymwREREREREVH6pVFC2aBBA4SFhalNa9u2Ldq2bZsmQREREREREVH698UJ5cuXL2FiYgIAiI6O/mi5zJkzf3tURERERERElO59cUJpZWWlJJLm5uZQqVRq80UEKpUKycnJaRshERERERERpUtfnFBev35d+X9wcPAHy4SGhn57RERERERERKQTvjihLFCggPJ/Y2NjZM+eHZkyvXmM5cOHDzF16lSsWLECr169SvsoiYiIiIiIKN3J9DWFL1y4gEKFCiF37tzIly8fTpw4gX///Rc2Nja4f/8+Dh069L3iJCIiIiIionTmq0Z5HTp0KNq1a4cuXbpg+fLl+PXXX2FlZYWjR4+ifPny3ytGIiIiIiIiSoe+KqG8evUqfH19oa+vj6lTp2LevHm4cOEC8uXL973iI6If5J1xtjQ34dsXJOMlDQJRlyb1S4O6AWlfv7Tbdmm0nDTGbfcFJqTRctIYt90XYP0+K/3+JqTBMr6DjL7tKP35qi6vCQkJ0Nd/k4MaGRnBzMyMySQREREREdFP6qtaKBMSEjB//nzlfXx8vNp7ABgwYEDaREZERERERETp2lcllFWqVIGnp6fy3sHBQe29SqViQklERERERPST+KqE0s/P7zuFQURERERERLrmq+6hJCIiIiIiIkrFhJKIiIiIiIg0woSSiIiIiIiINMKEkoiIiIiIiDSitYTSy8sLtra2sLGxwfLly9+bX6tWLZQrVw6lSpXCpEmTtBAhERERERERfcpXjfKaVpKSkjB48GAcPnwYZmZmqFixIlxdXWFhYaGU8fLygqmpKZKSklCjRg00a9YM5cuX10a4RERERERE9AFaaaE8e/Ys7OzsYGVlBWNjYzg7O2P//v1qZUxNTQEAiYmJSExMhEql0kaoRERERERE9BFaSSjDw8NhZWWlvLeyskJYWNh75apVq4bcuXOjfv36sLe3/+jy4uPjER0drfYiIiIiIiKi7ytdD8pz8uRJhIeHw9/fH9euXftouWnTpsHMzEx5FShQ4AdGSURERERE9HPSSkJpaWmp1iIZFhYGS0vLD5Y1MTFBvXr1sHfv3o8ub9SoUYiKilJeoaGhaR4zERERERERqdNKQung4IBr164hLCwMMTEx8PHxgZOTkzI/KioKT548AfCmO+u+fftQokSJjy7PwMAApqamai8iIiIiIiL6vrQyyqu+vj7c3NxQp04dpKSkYPjw4bCwsEDjxo2xfPlyJCYmolWrVkhISEBKSgratGmDpk2baiNUIiIiIiIi+gitJJQA4OLiAhcXF7Vp3t7eyv/Pnz//o0MiIiIiIiKir5CuB+UhIiIiIiKi9IsJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWmECSURERERERFphAklERERERERaYQJJREREREREWlEawmll5cXbG1tYWNjg+XLl6vNe/XqFZydnVGiRAnY2dlhwYIFWoqSiIiIiIiIPkZfGytNSkrC4MGDcfjwYZiZmaFixYpwdXWFhYWFUmbkyJFwdHRETEwMKlWqBGdnZxQrVkwb4RIREREREdEHaKWF8uzZs7Czs4OVlRWMjY3h7OyM/fv3K/OzZs0KR0dHAICxsTFsbW0RERHx0eXFx8cjOjpa7UVERERERETfl1YSyvDwcFhZWSnvraysEBYW9sGyoaGhuHLlCipUqPDR5U2bNg1mZmbKq0CBAmkeMxEREREREalL14PyxMfHo23btpg1axayZcv20XKjRo1CVFSU8goNDf2BURIREREREf2ctHIPpaWlpVqLZFhYGBwcHNTKiAg6d+6Mxo0bo3Xr1p9cnoGBAQwMDL5LrERERERERPRhWmmhdHBwwLVr1xAWFoaYmBj4+PjAyclJrcyoUaOQNWtWjBkzRhshEhERERER0WdoJaHU19eHm5sb6tSpA3t7ewwZMgQWFhZo3LgxwsPD8eDBA8yYMQNnz56Fvb097O3tsW/fPm2ESkRERERERB+hlS6vAODi4gIXFxe1ad7e3sr/ReRHh0RERERERERfIV0PykNERERERETpFxNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ggTSiIiIiIiItIIE0oiIiIiIiLSCBNKIiIiIiIi0ojWEkovLy/Y2trCxsYGy5cvf29+3759kSdPHlSqVEkL0REREREREdHnaCWhTEpKwuDBg3Ho0CFcunQJs2bNQmRkpFqZ9u3bw9vbWxvhERERERER0RfQSkJ59uxZ2NnZwcrKCsbGxnB2dsb+/fvVylSvXh0WFhZftLz4+HhER0ervYiIiIiIiOj70kpCGR4eDisrK+W9lZUVwsLCNF7etGnTYGZmprwKFCiQFmESERERERHRJ2SIQXlGjRqFqKgo5RUaGqrtkIiIiIiIiDI8fW2s1NLSUq1FMiwsDA4ODhovz8DAAAYGBmkRGhEREREREX0hrbRQOjg44Nq1awgLC0NMTAx8fHzg5OSkjVCIiIiIiIhIQ1pJKPX19eHm5oY6derA3t4eQ4YMgYWFBRo3bozw8HAAQNeuXVG1alVcuXIF+fPnx9atW7URKhEREREREX2EVrq8AoCLiwtcXFzUpr39mJBVq1b94IiIiIiIiIjoa2SIQXmIiIiIiIjox2NCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQaYUJJREREREREGmFCSURERERERBphQklEREREREQa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JwcPHqSoqAiDwcCECRNu86MmIiLyC0NTU1NTcxchIiLSXCoqKsjIyGDPnj2cOnWK4OBgVqxYwaBBg6isrMTLy4uqqirat28P/HRGsmfPnly+fJktW7Ywb948CgsLMRgMN91ucXEx/v7+fPHFF9x9990AZGRkcPjwYbZu3Wreb968eVRUVLB69WpSUlIoKCggJyfnT+vt0aMHSUlJTJo06XfbgoKCeOmllxg7diwA586dw8fHh/Lycry9vTEYDDfVIyIi8ne1bu4CREREmpOXlxdLly5l6dKlVFdX8/LLLzNixAhKS0spLi6msbERf3//m67TqlUrKioqKCkpoVu3br8Lk7/m6+tr/r24uJjdu3fTrl0789+uX79O//79b7nekpISgoKC/nBbWVkZXbt2NV/29vamTZs2lJWV4e3tfcvHEBERuVUa8ioiIvIzNzc3UlJSMBqNFBUV0aVLF1q1asW5c+f48ccfzT9XrlzBx8cHPz8/zpw5w18N9mnV6pe32i5dujBixIibbquuro7du3f/bt8/4+fnx+nTp/9wW+fOnSkuLjZfrqio4OrVq+b5myIiIrebAqWIiFitmpoa5s+fz6lTp7h+/TqXLl1i2bJluLm5ERISgpeXFzExMcTHx1NVVQX8FNJuDFmNiori6tWrLFiwAKPRyLVr1/j000//9HgTJ05k//79ZGdnYzKZMJlMFBQUcPz4cQA8PT0pKSmhoaHhT28jLi6O1NRUCgoKaGpqorS0lG+++QaA2NhY0tLSOHv2LPX19SQkJBAZGamzkyIi8o9RoBQREatlZ2dHeXk5Q4cOxdXVFV9fXw4dOsRHH32Eo6MjAO+88w7t2rWjd+/euLi40L9/f/Lz8wFwcnLik08+IT8/H19fXzp16kRmZuafHs/Hx4e9e/fyxhtv0KlTJzw9PXn22Wepra0FYPTo0bi4uODh4XHTsNhfmzFjBlOnTmXMmDE4OzsTGRlJaWkpAElJSQwePJjw8HC6du2KyWRi/fr1t/ERExERuZma8oiIiIiIiIhFdIZSRERERERELKJAKSIiIiIiIhZRoBQRERERERGLKFCKiIiIiIiIRRQoRURERERExCIKlCIiIiIiImIRBUoRERERERGxiAKliIiIiIiIWESBUkRERERERCyiQCkiIiIiIiIWUaAUERERERERi/wbP7LGLa/KxhIAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Singling-out\n", - "sin_out_res = singling_out_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " n_attacks = syn_anom.shape[0]\n", - ")\n", - "SyntheticResult.plot(res=sin_out_res,\n", - " high_res_flag=False,\n", - " save=True,\n", - " save_path=\"./outputs\",\n", - " save_name=\"Singling-out\"\n", - " )\n", - "\n", - "# Linkability\n", - "link_res = linkability_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " n_samples = syn_anom.shape[0],\n", - " n_attacks = 100\n", - ")\n", - "SyntheticResult.plot(res=link_res,\n", - " high_res_flag=False,\n", - " save=True,\n", - " save_path=\"./outputs\",\n", - " save_name=\"Linkability\"\n", - " )\n", - "\n", - "# Inference risk, base case\n", - "inf_res = inference_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " worst_case_flag = False,\n", - " n_attacks = syn_anom.shape[0]\n", - ")\n", - "SyntheticResult.plot(res=inf_res,\n", - " high_res_flag=False,\n", - " case_flag=\"base\",\n", - " save=True,\n", - " save_path=\"./outputs\",\n", - " save_name=\"Inference_base_case\"\n", - " )\n", - "\n", - "# Inference risk, worst case\n", - "inf_res_worst = inference_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " worst_case_flag = True,\n", - " n_attacks = syn_anom.shape[0]\n", - ")\n", - "SyntheticResult.plot(res=inf_res_worst,\n", - " high_res_flag=False,\n", - " case_flag=\"worst\",\n", - " save=True,\n", - " save_path=\"./outputs\",\n", - " save_name=\"Inference_worst_case\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "9f9e01d4", - "metadata": {}, - "source": [ - "## Save and store results" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "0e0c6c09", - "metadata": {}, - "outputs": [ - { - "ename": "TypeError", - "evalue": "ReportHandler.__init__() missing 1 required positional argument: 'logger'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[11], line 7\u001b[0m\n\u001b[1;32m 3\u001b[0m os\u001b[38;5;241m.\u001b[39mmakedirs(path)\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mleakpro\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreporting\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mreport_handler\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ReportHandler\n\u001b[0;32m----> 7\u001b[0m reporthandler \u001b[38;5;241m=\u001b[39m \u001b[43mReportHandler\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 8\u001b[0m \u001b[43m \u001b[49m\u001b[43mreport_dir\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 9\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 11\u001b[0m reporthandler\u001b[38;5;241m.\u001b[39msave_results(\n\u001b[1;32m 12\u001b[0m attack_name\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124msynthetic_inference\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 13\u001b[0m result_data\u001b[38;5;241m=\u001b[39minf_res,\n\u001b[1;32m 14\u001b[0m config\u001b[38;5;241m=\u001b[39m{\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcase\u001b[39m\u001b[38;5;124m\"\u001b[39m: \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mbase\u001b[39m\u001b[38;5;124m\"\u001b[39m}\n\u001b[1;32m 15\u001b[0m )\n", - "\u001b[0;31mTypeError\u001b[0m: ReportHandler.__init__() missing 1 required positional argument: 'logger'" - ] - } - ], - "source": [ - "path = \"../../leakpro_output\"\n", - "if not os.path.exists(path):\n", - " os.makedirs(path)\n", - " \n", - "from leakpro.reporting.report_handler import ReportHandler\n", - "\n", - "reporthandler = ReportHandler(\n", - " report_dir=path,\n", - " )\n", - "\n", - "reporthandler.save_results(\n", - " attack_name=\"synthetic_inference\",\n", - " result_data=inf_res,\n", - " config={\"case\": \"base\"}\n", - " )\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1f9c3942", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/report_handler/gia_utils/cifar.py b/examples/report_handler/gia_utils/cifar.py new file mode 100644 index 00000000..98ee9490 --- /dev/null +++ b/examples/report_handler/gia_utils/cifar.py @@ -0,0 +1,26 @@ +"""Module with functions for preparing the dataset for training the target models.""" +import torchvision +from torch import as_tensor, randperm +from torch.utils.data import DataLoader, Subset, TensorDataset +from torchvision import transforms + +from leakpro.fl_utils.data_utils import get_meanstd + + +def get_cifar10_loader(num_images:int =1, batch_size:int = 1, num_workers:int = 2 ) -> TensorDataset: + """Get the full dataset for CIFAR10.""" + trainset = torchvision.datasets.CIFAR10(root="./data", train=True, download=True, transform=transforms.ToTensor()) + data_mean, data_std = get_meanstd(trainset) + transform = transforms.Compose([ + transforms.ToTensor(), + transforms.Normalize(data_mean, data_std)]) + trainset.transform = transform + + total_examples = len(trainset) + random_indices = randperm(total_examples)[:num_images] + subset_trainset = Subset(trainset, random_indices) + trainloader = DataLoader(subset_trainset, batch_size=batch_size, + shuffle=False, drop_last=True, num_workers=num_workers) + data_mean = as_tensor(data_mean)[:, None, None] + data_std = as_tensor(data_std)[:, None, None] + return trainloader, data_mean, data_std diff --git a/examples/report_handler/gia_utils/model.py b/examples/report_handler/gia_utils/model.py new file mode 100644 index 00000000..403ff164 --- /dev/null +++ b/examples/report_handler/gia_utils/model.py @@ -0,0 +1,81 @@ +"""ResNet model.""" +from typing import Optional + +import torch +import torchvision +from torch import nn +from torchvision.models.resnet import BasicBlock, Bottleneck + +from leakpro.utils.import_helper import Self + + +class ResNet(torchvision.models.ResNet): + """ResNet generalization for CIFAR thingies.""" + + def __init__(self: Self, block: BasicBlock, layers: list, num_classes: int=10, zero_init_residual: bool=False, # noqa: C901 + groups: int=1, base_width: int=64, replace_stride_with_dilation: list=None, + norm_layer: Optional[nn.Module]=None, strides: list=[1, 2, 2, 2], pool: str="avg") -> None: # noqa: B006 + """Initialize as usual. Layers and strides are scriptable.""" + super(torchvision.models.ResNet, self).__init__() # nn.Module + if norm_layer is None: + norm_layer = nn.BatchNorm2d + self._norm_layer = norm_layer + + + self.dilation = 1 + if replace_stride_with_dilation is None: + # each element in the tuple indicates if we should replace + # the 2x2 stride with a dilated convolution instead + replace_stride_with_dilation = [False, False, False, False] + if len(replace_stride_with_dilation) != 4: + raise ValueError("replace_stride_with_dilation should be None " + "or a 4-element tuple, got {}".format(replace_stride_with_dilation)) + self.groups = groups + + self.inplanes = base_width + self.base_width = 64 # Do this to circumvent BasicBlock errors. The value is not actually used. + self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False) + self.bn1 = norm_layer(self.inplanes) + self.relu = nn.ReLU(inplace=True) + + self.layers = torch.nn.ModuleList() + width = self.inplanes + for idx, layer in enumerate(layers): + self.layers.append(self._make_layer(block, width, layer, stride=strides[idx], dilate=replace_stride_with_dilation[idx])) + width *= 2 + + self.pool = nn.AdaptiveAvgPool2d((1, 1)) if pool == "avg" else nn.AdaptiveMaxPool2d((1, 1)) + self.fc = nn.Linear(width // 2 * block.expansion, num_classes) + + for m in self.modules(): + if isinstance(m, nn.Conv2d): + nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") + elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)): + nn.init.constant_(m.weight, 1) + nn.init.constant_(m.bias, 0) + + # Zero-initialize the last BN in each residual branch, + # so that the residual branch starts with zeros, and each residual block behaves like an identity. + # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 + if zero_init_residual: + for m in self.modules(): + if isinstance(m, Bottleneck): + nn.init.constant_(m.bn3.weight, 0) + elif isinstance(m, torchvision.models.resnet.BasicBlock): + nn.init.constant_(m.bn2.weight, 0) + + + def _forward_impl(self: Self, x: torch.Tensor) -> None: + # See note [TorchScript super()] + x = self.conv1(x) + x = self.bn1(x) + x = self.relu(x) + + for layer in self.layers: + x = layer(x) + + x = self.pool(x) + x = torch.flatten(x, 1) + x = self.fc(x) + + return x diff --git a/examples/report_handler/mia_utils/audit.yaml b/examples/report_handler/mia_utils/audit.yaml new file mode 100644 index 00000000..073fe7f1 --- /dev/null +++ b/examples/report_handler/mia_utils/audit.yaml @@ -0,0 +1,56 @@ +audit: # Configurations for auditing + random_seed: 1234 # Integer specifying the random seed + attack_list: + # rmia: + # training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) + # attack_data_fraction: 0.5 # Fraction of auxiliary dataset to sample from during attack + # num_shadow_models: 3 # Number of shadow models to train + # online: True # perform online or offline attack + # temperature: 2 + # gamma: 2.0 + # offline_a: 0.33 # parameter from which we compute p(x) from p_OUT(x) such that p_IN(x) = a p_OUT(x) + b. + # offline_b: 0.66 + # qmia: + # training_data_fraction: 1.0 # Fraction of the auxilary dataset (data without train and test indices) to use for training the quantile regressor + # epochs: 5 # Number of training epochs for quantile regression + population: + attack_data_fraction: 1.0 # Fraction of the auxilary dataset to use for this attack + # lira: + # training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) + # num_shadow_models: 3 # Number of shadow models to train + # online: False # perform online or offline attack + # fixed_variance: True # Use a fixed variance for the whole audit + # boosting: True + # loss_traj: + # training_distill_data_fraction : 0.7 # Fraction of the auxilary dataset to use for training the distillation models D_s = (1-D_KD)/2 + # number_of_traj: 10 # Number of epochs (number of points in the loss trajectory) + # label_only: False # True or False + # mia_classifier_epochs: 100 + # HSJ: + # attack_data_fraction: 0.01 # Fraction of the auxilary dataset to use for this attack + # target_metadata_path: "./target/model_metadata.pkl" + # num_iterations: 2 # Number of iterations for the optimization + # initial_num_evals: 100 # Number of evaluations for number of random vecotr to estimate the gradient + # max_num_evals: 10000 # Maximum number of evaluations + # stepsize_search: "geometric_progression" # Step size search method + # gamma: 1.0 # Gamma for the optimization + # constraint: 2 + # batch_size: 50 + # verbose: True + # epsilon_threshold: 1e-6 + + output_dir: "./leakpro_output" + attack_type: "mia" #mia, gia + modality: "image" #image, tabular + +target: + # Target model path + module_path: "./mia_utils/utils/cifar_model_preparation.py" + model_class: "ResNet18" + # Data paths + target_folder: "./target" + data_path: "./data/cifar10.pkl" + +shadow_model: + +distillation_model: diff --git a/examples/report_handler/mia_utils/cifar_handler.py b/examples/report_handler/mia_utils/cifar_handler.py new file mode 100644 index 00000000..97755604 --- /dev/null +++ b/examples/report_handler/mia_utils/cifar_handler.py @@ -0,0 +1,67 @@ +"""Module containing the class to handle the user input for the CIFAR100 dataset.""" + +import torch +from torch import cuda, device, optim, sigmoid +from torch.nn import CrossEntropyLoss +from torch.utils.data import DataLoader +from tqdm import tqdm + +from leakpro import AbstractInputHandler + +class CifarInputHandler(AbstractInputHandler): + """Class to handle the user input for the CIFAR100 dataset.""" + + def __init__(self, configs: dict) -> None: + super().__init__(configs = configs) + print(configs) + + + def get_criterion(self)->None: + """Set the CrossEntropyLoss for the model.""" + return CrossEntropyLoss() + + def get_optimizer(self, model:torch.nn.Module) -> None: + """Set the optimizer for the model.""" + learning_rate = 0.1 + momentum = 0.8 + return optim.SGD(model.parameters(), lr=learning_rate, momentum=momentum) + + def train( + self, + dataloader: DataLoader, + model: torch.nn.Module = None, + criterion: torch.nn.Module = None, + optimizer: optim.Optimizer = None, + epochs: int = None, + ) -> dict: + """Model training procedure.""" + + # read hyperparams for training (the parameters for the dataloader are defined in get_dataloader): + if epochs is None: + raise ValueError("epochs not found in configs") + + # prepare training + gpu_or_cpu = device("cuda" if cuda.is_available() else "cpu") + model.to(gpu_or_cpu) + + # training loop + for epoch in range(epochs): + train_loss, train_acc = 0, 0 + model.train() + for inputs, labels in tqdm(dataloader, desc=f"Epoch {epoch+1}/{epochs}"): + labels = labels.long() + inputs, labels = inputs.to(gpu_or_cpu, non_blocking=True), labels.to(gpu_or_cpu, non_blocking=True) + optimizer.zero_grad() + outputs = model(inputs) + loss = criterion(outputs, labels) + pred = outputs.data.max(1, keepdim=True)[1] + loss.backward() + optimizer.step() + + # Accumulate performance of shadow model + train_acc += pred.eq(labels.data.view_as(pred)).sum() + train_loss += loss.item() + + model.to("cpu") + + return {"model": model, "metrics": {"accuracy": train_acc, "loss": train_loss}} diff --git a/examples/report_handler/mia_utils/train_config.yaml b/examples/report_handler/mia_utils/train_config.yaml new file mode 100644 index 00000000..4ea31131 --- /dev/null +++ b/examples/report_handler/mia_utils/train_config.yaml @@ -0,0 +1,17 @@ +run: # Configurations for a specific run + random_seed: 1234 # Integer number of specifying random seed + log_dir: target # String for indicating where to save all the information, including models and computed signals. We can reuse the models saved in the same log_dir. + +train: # Configuration for training + epochs: 3 # Integer number for indicating the epochs for training target model. For speedyresnet, it uses its own number of epochs. + batch_size: 128 # Integer number for indicating batch size for training the target model. For speedyresnet, it uses its own batch size. + optimizer: SGD # String which indicates the optimizer. We support Adam and SGD. For speedyresnet, it uses its own optimizer. + learning_rate: 0.01 # Float number for indicating learning rate for training the target model. For speedyresnet, it uses its own learning_rate. + momentum: 0.9 + weight_decay: 0.0 # Float number for indicating weight decay for training the target model. For speedyresnet, it uses its own weight_decay. + +data: # Configuration for data + dataset: cifar10 # String indicates the name of the dataset + f_train: 0.3 # Float number from 0 to 1 indicating the fraction of the train dataset + f_test: 0.3 # Float number from 0 to 1 indicating the size of the test set + data_dir: ./data # String about where to save the data. \ No newline at end of file diff --git a/examples/report_handler/mia_utils/utils/cifar_data_preparation.py b/examples/report_handler/mia_utils/utils/cifar_data_preparation.py new file mode 100644 index 00000000..90d48987 --- /dev/null +++ b/examples/report_handler/mia_utils/utils/cifar_data_preparation.py @@ -0,0 +1,111 @@ +import os +import numpy as np +import pickle +from sklearn.model_selection import train_test_split +from torchvision import transforms +from torchvision.datasets import CIFAR10, CIFAR100 +from torch.utils.data import Dataset, Subset, DataLoader +from torch import tensor, float32, cat + + + +class CifarDataset(Dataset): + def __init__(self, x, y, transform=None, indices=None): + """ + Custom dataset for CIFAR data. + + Args: + x (torch.Tensor): Tensor of input images. + y (torch.Tensor): Tensor of labels. + transform (callable, optional): Optional transform to be applied on the image tensors. + """ + self.x = x + self.y = y + self.transform = transform + self.indices = indices + + def __len__(self): + """Return the total number of samples.""" + return len(self.y) + + def __getitem__(self, idx): + """Retrieve the image and its corresponding label at index 'idx'.""" + image = self.x[idx] + label = self.y[idx] + + # Apply transformations to the image if any + if self.transform: + image = self.transform(image) + + return image, label + + @classmethod + def from_cifar(cls, config, download=True, transform=None): + + root = config["data"]["data_dir"] + # Load the CIFAR train and test datasets + if config["data"]["dataset"] == "cifar10": + trainset = CIFAR10(root=root, train=True, download=download, transform=transforms.ToTensor()) + testset = CIFAR10(root=root, train=False, download=download, transform=transforms.ToTensor()) + elif config["data"]["dataset"] == "cifar100": + trainset = CIFAR100(root=root, train=True, download=download, transform=transforms.ToTensor()) + testset = CIFAR100(root=root, train=False, download=download, transform=transforms.ToTensor()) + else: + raise ValueError("Unknown dataset type") + + # Concatenate both datasets' data and labels + data = cat([tensor(trainset.data, dtype=float32), + tensor(testset.data, dtype=float32)], + dim=0) + # Rescale data from [0, 255] to [0, 1] + data /= 255.0 + normalize = transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) + data = data.permute(0, 3, 1, 2) + data = normalize(data) + + targets = cat([tensor(trainset.targets), tensor(testset.targets)], dim=0) + + return cls(data, targets) + + + def subset(self, indices): + """Return a subset of the dataset based on the given indices.""" + return CifarDataset(self.x[indices], self.y[indices], transform=self.transform) + + +def get_cifar_dataloader(data_path, train_config): + # Create the combined CIFAR-10 dataset + train_fraction = train_config["data"]["f_train"] + test_fraction = train_config["data"]["f_test"] + cifar_type = train_config["data"]["dataset"] + batch_size = train_config["train"]["batch_size"] + + transform = transforms.Compose([transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) + + population_dataset = CifarDataset.from_cifar(config=train_config, download=True, transform=transform) + + file_path = "data/"+ cifar_type + ".pkl" + if not os.path.exists(file_path): + with open(file_path, "wb") as file: + pickle.dump(population_dataset, file) + print(f"Save data to {file_path}.pkl") + + dataset_size = len(population_dataset) + train_size = int(train_fraction * dataset_size) + test_size = int(test_fraction * dataset_size) + + # Use sklearn's train_test_split to split into train and test indices + selected_index = np.random.choice(np.arange(dataset_size), train_size + test_size, replace=False) + train_indices, test_indices = train_test_split(selected_index, test_size=test_size) + + train_subset = Subset(population_dataset, train_indices) + test_subset = Subset(population_dataset, test_indices) + + train_loader = DataLoader(train_subset, batch_size =batch_size, shuffle=True) + test_loader = DataLoader(test_subset, batch_size= batch_size, shuffle=False) + + return train_loader, test_loader + + + diff --git a/examples/report_handler/mia_utils/utils/cifar_model_preparation.py b/examples/report_handler/mia_utils/utils/cifar_model_preparation.py new file mode 100644 index 00000000..4021da04 --- /dev/null +++ b/examples/report_handler/mia_utils/utils/cifar_model_preparation.py @@ -0,0 +1,117 @@ +import torch.nn as nn +from torch import device, optim, cuda, no_grad, save, sigmoid +import torchvision.models as models +import pickle +from tqdm import tqdm + +class ResNet18(nn.Module): + def __init__(self, num_classes): + super(ResNet18, self).__init__() + self.model = models.resnet18(pretrained=False) + self.model.fc = nn.Linear(self.model.fc.in_features, num_classes) + self.init_params = {"num_classes": num_classes} + + def forward(self, x): + return self.model(x) + +def evaluate(model, loader, criterion, device): + model.eval() + loss, acc = 0, 0 + with no_grad(): + for data, target in loader: + data, target = data.to(device), target.to(device) + target = target.view(-1) + output = model(data) + loss += criterion(output, target).item() + pred = output.argmax(dim=1) + acc += pred.eq(target).sum().item() + loss /= len(loader) + acc = float(acc) / len(loader.dataset) + return loss, acc + +def create_trained_model_and_metadata(model, + train_loader, + test_loader, + train_config): + lr = train_config["train"]["learning_rate"] + momentum = train_config["train"]["momentum"] + epochs = train_config["train"]["epochs"] + + device_name = device("cuda" if cuda.is_available() else "cpu") + model.to(device_name) + model.train() + + criterion = nn.CrossEntropyLoss() + optimizer = optim.SGD(model.parameters(), lr=lr, momentum=momentum) + train_losses, train_accuracies = [], [] + test_losses, test_accuracies = [], [] + + for e in tqdm(range(epochs), desc="Training Progress"): + model.train() + train_acc, train_loss = 0.0, 0.0 + + for data, target in train_loader: + data, target = data.to(device_name, non_blocking=True), target.to(device_name, non_blocking=True) + target = target.view(-1) + optimizer.zero_grad() + output = model(data) + + loss = criterion(output, target) + pred = output.argmax(dim=1) # for multi-class classification + train_acc += pred.eq(target).sum().item() + + loss.backward() + optimizer.step() + train_loss += loss.item() + + train_loss /= len(train_loader) + train_acc /= len(train_loader.dataset) + + train_losses.append(train_loss) + train_accuracies.append(train_acc) + + test_loss, test_acc = evaluate(model, test_loader, criterion, device_name) + test_losses.append(test_loss) + test_accuracies.append(test_acc) + + # Move the model back to the CPU + model.to("cpu") + with open( train_config["run"]["log_dir"]+"/target_model.pkl", "wb") as f: + save(model.state_dict(), f) + + # Create metadata and store it + meta_data = {} + meta_data["train_indices"] = train_loader.dataset.indices + meta_data["test_indices"] = test_loader.dataset.indices + meta_data["num_train"] = len(meta_data["train_indices"]) + + # Write init params + meta_data["init_params"] = {} + for key, value in model.init_params.items(): + meta_data["init_params"][key] = value + + # read out optimizer parameters + meta_data["optimizer"] = {} + meta_data["optimizer"]["name"] = optimizer.__class__.__name__.lower() + meta_data["optimizer"]["lr"] = optimizer.param_groups[0].get("lr", 0) + meta_data["optimizer"]["weight_decay"] = optimizer.param_groups[0].get("weight_decay", 0) + meta_data["optimizer"]["momentum"] = optimizer.param_groups[0].get("momentum", 0) + meta_data["optimizer"]["dampening"] = optimizer.param_groups[0].get("dampening", 0) + meta_data["optimizer"]["nesterov"] = optimizer.param_groups[0].get("nesterov", False) + + # read out criterion parameters + meta_data["loss"] = {} + meta_data["loss"]["name"] = criterion.__class__.__name__.lower() + + meta_data["batch_size"] = train_loader.batch_size + meta_data["epochs"] = epochs + meta_data["train_acc"] = train_acc + meta_data["test_acc"] = test_acc + meta_data["train_loss"] = train_loss + meta_data["test_loss"] = test_loss + meta_data["dataset"] = train_config["data"]["dataset"] + + with open("target/model_metadata.pkl", "wb") as f: + pickle.dump(meta_data, f) + + return train_accuracies, train_losses, test_accuracies, test_losses diff --git a/examples/report_handler/report_handler.ipynb b/examples/report_handler/report_handler.ipynb new file mode 100644 index 00000000..5e713e1d --- /dev/null +++ b/examples/report_handler/report_handler.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "95d5acad-514e-4950-94a0-c80d789d9364", + "metadata": {}, + "source": [ + "# Report handler examples" + ] + }, + { + "cell_type": "markdown", + "id": "71f5dbe9", + "metadata": {}, + "source": [ + "Install leakpro as ``` pip install -e /path/to/leakpro ```" + ] + }, + { + "cell_type": "markdown", + "id": "68b48ce8", + "metadata": {}, + "source": [ + "### Synthetic examples" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "import pandas as pd\n", + "\n", + "sys.path.append(\"../..\")\n", + "\n", + "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", + "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", + "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", + "\n", + "#Get ori and syn\n", + "n_samples = 100\n", + "DATA_PATH = \"../synthetic_data/datasets/\"\n", + "ori = pd.read_csv(os.path.join(DATA_PATH, \"adults_ori.csv\"), nrows=n_samples)\n", + "syn = pd.read_csv(os.path.join(DATA_PATH, \"adults_syn.csv\"), nrows=n_samples)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c89f3738", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[Parallel(n_jobs=64)]: Using backend ThreadingBackend with 64 concurrent workers.\n", + "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 1.2s remaining: 36.4s\n", + "[Parallel(n_jobs=64)]: Done 64 out of 64 | elapsed: 4.4s finished\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Unique predictions (array([-1, 1]), array([ 3, 97]))\n", + "Syn anom shape (3, 14)\n" + ] + } + ], + "source": [ + "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", + "print(\"Syn anom shape\",syn_anom.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "ad69ece9", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a singling-out result\n", + "sin_out_res = singling_out_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_attacks = syn_anom.shape[0]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7d7ffb5a", + "metadata": {}, + "outputs": [], + "source": [ + "# Create linkability result\n", + "link_res = linkability_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " n_samples = syn_anom.shape[0],\n", + " n_attacks = 100\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0a5c20e2", + "metadata": {}, + "outputs": [], + "source": [ + "# # Create base-case inference result\n", + "inf_res = inference_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " worst_case_flag = False,\n", + " n_attacks = syn_anom.shape[0]\n", + ")\n", + "\n", + "# # Create worst-case inference result\n", + "inf_res_worst = inference_risk_evaluation(\n", + " dataset = \"adults\",\n", + " ori = ori,\n", + " syn = syn_anom,\n", + " worst_case_flag = True,\n", + " n_attacks = syn_anom.shape[0]\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0b3be474", + "metadata": {}, + "source": [ + "### Gradient inversion example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35aee5a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Files already downloaded and verified\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-27 08:38:00,656 INFO Inverting gradient initialized.\n", + "2024-11-27 08:38:02,796 INFO Iteration 0, loss 0.0003550234832800925\n", + "2024-11-27 08:38:02,803 INFO New best loss: 0.0003550234832800925 on round: 0\n", + "2024-11-27 08:38:02,946 INFO New best loss: 0.00033073360100388527 on round: 1\n", + "2024-11-27 08:38:03,193 INFO New best loss: 0.0003084556374233216 on round: 3\n", + "2024-11-27 08:38:03,552 INFO New best loss: 0.0002839812950696796 on round: 6\n", + "2024-11-27 08:38:03,674 INFO New best loss: 0.00028284190921112895 on round: 7\n", + "2024-11-27 08:38:03,802 INFO New best loss: 0.00027706200489774346 on round: 8\n", + "2024-11-27 08:38:03,924 INFO New best loss: 0.00026298483135178685 on round: 9\n", + "2024-11-27 08:38:04,303 INFO New best loss: 0.00025501404888927937 on round: 12\n", + "2024-11-27 08:38:04,427 INFO New best loss: 0.00025296382955275476 on round: 13\n", + "2024-11-27 08:38:04,677 INFO New best loss: 0.00024899665731936693 on round: 15\n", + "2024-11-27 08:38:04,807 INFO New best loss: 0.0002483891148585826 on round: 16\n", + "2024-11-27 08:38:04,936 INFO New best loss: 0.0002449913590680808 on round: 17\n", + "2024-11-27 08:38:05,063 INFO New best loss: 0.0002429946616757661 on round: 18\n", + "2024-11-27 08:38:05,185 INFO New best loss: 0.00023749274259898812 on round: 19\n", + "2024-11-27 08:38:05,890 INFO New best loss: 0.0002372416784055531 on round: 25\n", + "2024-11-27 08:38:06,015 INFO New best loss: 0.00023295756545849144 on round: 26\n", + "2024-11-27 08:38:06,139 INFO New best loss: 0.00023274944396689534 on round: 27\n", + "2024-11-27 08:38:06,266 INFO New best loss: 0.00023147281899582595 on round: 28\n", + "2024-11-27 08:38:06,394 INFO New best loss: 0.00022923419601283967 on round: 29\n", + "2024-11-27 08:38:06,646 INFO New best loss: 0.00022827980865258723 on round: 31\n", + "2024-11-27 08:38:06,775 INFO New best loss: 0.00022587741841562092 on round: 32\n", + "2024-11-27 08:38:06,907 INFO New best loss: 0.00022527067631017417 on round: 33\n", + "2024-11-27 08:38:08,442 INFO New best loss: 0.00022461664048023522 on round: 45\n", + "2024-11-27 08:38:08,569 INFO New best loss: 0.00022421970788855106 on round: 46\n", + "2024-11-27 08:38:08,696 INFO New best loss: 0.0002231039834441617 on round: 47\n", + "2024-11-27 08:38:08,819 INFO New best loss: 0.00022097492183092982 on round: 48\n", + "2024-11-27 08:38:10,106 INFO New best loss: 0.00022094276209827513 on round: 59\n", + "2024-11-27 08:38:10,216 INFO New best loss: 0.00022092672588769346 on round: 60\n", + "2024-11-27 08:38:10,328 INFO New best loss: 0.00022070117120165378 on round: 61\n", + "2024-11-27 08:38:10,434 INFO New best loss: 0.00022059425828047097 on round: 62\n", + "2024-11-27 08:38:10,539 INFO New best loss: 0.00022042241471353918 on round: 63\n", + "2024-11-27 08:38:11,368 INFO New best loss: 0.00022039355826564133 on round: 71\n", + "2024-11-27 08:38:11,472 INFO New best loss: 0.00022023300698492676 on round: 72\n", + "2024-11-27 08:38:11,680 INFO New best loss: 0.0002201125753344968 on round: 74\n", + "2024-11-27 08:38:12,092 INFO New best loss: 0.00021973479306325316 on round: 78\n", + "2024-11-27 08:38:12,195 INFO New best loss: 0.00021972827380523086 on round: 79\n", + "/opt/conda/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:70: FutureWarning: Importing `peak_signal_noise_ratio` from `torchmetrics.functional` was deprecated and will be removed in 2.0. Import `peak_signal_noise_ratio` from `torchmetrics.image` instead.\n", + " _future_warning(\n" + ] + } + ], + "source": [ + "from gia_utils.cifar import get_cifar10_loader\n", + "from gia_utils.model import ResNet\n", + "from torchvision.models.resnet import BasicBlock\n", + "\n", + "from leakpro.attacks.gia_attacks.invertinggradients import InvertingConfig\n", + "from leakpro.fl_utils.gia_train import train\n", + "from leakpro.run import run_inverting\n", + "\n", + "model = ResNet(BasicBlock, [5, 5, 5], num_classes=10, base_width=16 * 10)\n", + "client_dataloader, data_mean, data_std = get_cifar10_loader(num_images=1, batch_size=1, num_workers=2)\n", + "\n", + "# Meta train function designed to work with GIA\n", + "train_fn = train\n", + "\n", + "# Baseline config\n", + "configs = InvertingConfig()\n", + "configs.at_iterations = 80 # Decreased from 8000 to avoid GPU memory crash\n", + "\n", + "GIA_result = run_inverting(model, client_dataloader, train_fn, data_mean, data_std, configs)" + ] + }, + { + "cell_type": "markdown", + "id": "645e8caa", + "metadata": {}, + "source": [ + "### Membership Inference Attack, CIFAR example" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d38d6aa1", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "import yaml\n", + "\n", + "project_root = os.path.abspath(os.path.join(os.getcwd(), \"../../..\"))\n", + "sys.path.append(project_root)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a45a0d6b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Files already downloaded and verified\n", + "Files already downloaded and verified\n" + ] + } + ], + "source": [ + "from mia_utils.utils.cifar_data_preparation import get_cifar_dataloader\n", + "from mia_utils.utils.cifar_model_preparation import ResNet18, create_trained_model_and_metadata\n", + "\n", + "\n", + "# Load the config.yaml file\n", + "with open('mia_utils/train_config.yaml', 'r') as file:\n", + " train_config = yaml.safe_load(file)\n", + "\n", + "# Generate the dataset and dataloaders\n", + "path = os.path.join(os.getcwd(), train_config[\"data\"][\"data_dir\"])\n", + "\n", + "train_loader, test_loader = get_cifar_dataloader(path, train_config)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "4cda80cf", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n", + "Training Progress: 100%|██████████| 3/3 [00:13<00:00, 4.39s/it]\n" + ] + } + ], + "source": [ + "# Train the model\n", + "if not os.path.exists(\"target\"):\n", + " os.makedirs(\"target\")\n", + "if train_config[\"data\"][\"dataset\"] == \"cifar10\":\n", + " num_classes = 10\n", + "elif train_config[\"data\"][\"dataset\"] == \"cifar100\":\n", + " num_classes = 100\n", + "else:\n", + " raise ValueError(\"Invalid dataset name\")\n", + "\n", + "model = ResNet18(num_classes = num_classes)\n", + "train_acc, train_loss, test_acc, test_loss = create_trained_model_and_metadata(model, \n", + " train_loader, \n", + " test_loader, \n", + " train_config)" + ] + }, + { + "cell_type": "markdown", + "id": "0872bf51", + "metadata": {}, + "source": [ + "##### Run the MIA attack" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f28eb14f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-27 08:38:27,995 INFO Target model blueprint created from ResNet18 in ./mia_utils/utils/cifar_model_preparation.py.\n", + "2024-11-27 08:38:27,997 INFO Loaded target model metadata from ./target/model_metadata.pkl\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'audit': {'random_seed': 1234, 'attack_list': {'population': {'attack_data_fraction': 1.0}}, 'output_dir': './leakpro_output', 'attack_type': 'mia', 'modality': 'image'}, 'target': {'module_path': './mia_utils/utils/cifar_model_preparation.py', 'model_class': 'ResNet18', 'target_folder': './target', 'data_path': './data/cifar10.pkl'}, 'shadow_model': None, 'distillation_model': None}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-27 08:38:28,210 INFO Loaded target model from ./target\n", + "2024-11-27 08:38:29,306 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-11-27 08:38:29,306 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-11-27 08:38:29,307 INFO Creating shadow model handler singleton\n", + "2024-11-27 08:38:29,308 INFO Creating distillation model handler singleton\n", + "2024-11-27 08:38:29,310 INFO Configuring the Population attack\n", + "2024-11-27 08:38:29,310 INFO Added attack: population\n", + "2024-11-27 08:38:29,311 INFO Preparing attack: population\n", + "2024-11-27 08:38:29,312 INFO Preparing attack data for training the Population attack\n", + "2024-11-27 08:38:29,316 INFO Subsampling attack data from 24000 points\n", + "2024-11-27 08:38:29,317 INFO Number of attack data points after subsampling: 24000\n", + "2024-11-27 08:38:29,318 INFO Computing signals for the Population attack\n", + "Getting loss for model 1/ 1: 100%|██████████| 750/750 [00:12<00:00, 62.27it/s]\n", + "2024-11-27 08:38:41,410 INFO Running attack: population\n", + "2024-11-27 08:38:41,415 INFO Running the Population attack on the target model\n", + "Getting loss for model 1/ 1: 100%|██████████| 1125/1125 [00:18<00:00, 62.04it/s]\n", + "2024-11-27 08:38:59,662 INFO Attack completed\n", + "2024-11-27 08:38:59,674 INFO Finished attack: population\n", + "2024-11-27 08:38:59,675 INFO Preparing results for attack: population\n", + "2024-11-27 08:38:59,675 INFO Auditing completed\n" + ] + } + ], + "source": [ + "from mia_utils.cifar_handler import CifarInputHandler\n", + "\n", + "from leakpro import LeakPro\n", + "\n", + "# Read the config file\n", + "config_path = \"mia_utils/audit.yaml\"\n", + "\n", + "# Prepare leakpro object\n", + "leakpro = LeakPro(CifarInputHandler, config_path)\n", + "\n", + "# Run the audit \n", + "mia_results = leakpro.run_audit(return_results=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "373dcc8a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-27 08:38:59,693 INFO Initializing report handler...\n", + "2024-11-27 08:38:59,693 INFO report_dir set to: ../../leakpro_output/results\n", + "2024-11-27 08:38:59,694 INFO Saving results for singling_out\n", + "2024-11-27 08:39:01,779 INFO Saving results for linkability_risk\n", + "2024-11-27 08:39:03,748 INFO Saving results for inference_risk_base\n", + "2024-11-27 08:39:08,623 INFO Saving results for inference_risk_worst\n", + "2024-11-27 08:39:10,925 INFO Saving results for gia\n", + "2024-11-27 08:39:10,939 INFO Saving results for population\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import sys\n", + "sys.path.append(\"../..\")\n", + "\n", + "# Import and initialize ReportHandler\n", + "from leakpro.reporting.report_handler import ReportHandler\n", + "report_handler = ReportHandler()\n", + "\n", + "# # Save Synthetic results using the ReportHandler\n", + "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)\n", + "report_handler.save_results(attack_name=\"linkability_risk\", result_data=link_res)\n", + "report_handler.save_results(attack_name=\"inference_risk_base\", result_data=inf_res)\n", + "report_handler.save_results(attack_name=\"inference_risk_worst\", result_data=inf_res_worst)\n", + "\n", + "# # Save GIA results using report handler\n", + "report_handler.save_results(attack_name=\"gia\", result_data=GIA_result)\n", + "\n", + "# Save MIA resuls using report handler\n", + "for res in mia_results:\n", + " report_handler.save_results(attack_name=res.attack_name, result_data=res, config=res.configs)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "1d91c7e0", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-11-27 08:40:04,818 INFO PDF compiled\n" + ] + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Use the ReportHandler and load all the saved results\n", + "report_handler.load_results()\n", + "\n", + "# Create results and collect corresponding latex texts\n", + "report_handler.create_results_all()\n", + "\n", + "# Create the report by compiling the latex text\n", + "report_handler.create_report()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/report_handler/report_handler_anomalies.ipynb b/examples/report_handler/report_handler_anomalies.ipynb deleted file mode 100644 index a32fbdda..00000000 --- a/examples/report_handler/report_handler_anomalies.ipynb +++ /dev/null @@ -1,203 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "95d5acad-514e-4950-94a0-c80d789d9364", - "metadata": {}, - "source": [ - "# Report handler examples" - ] - }, - { - "cell_type": "markdown", - "id": "71f5dbe9", - "metadata": {}, - "source": [ - "Install leakpro as ``` pip install -e /path/to/leakpro ```" - ] - }, - { - "cell_type": "markdown", - "id": "68b48ce8", - "metadata": {}, - "source": [ - "### Synthetic examples" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import sys\n", - "\n", - "import pandas as pd\n", - "\n", - "sys.path.append(\"../..\")\n", - "\n", - "from leakpro.synthetic_data_attacks import plots\n", - "from leakpro.synthetic_data_attacks.anomalies import return_anomalies\n", - "from leakpro.synthetic_data_attacks.inference_utils import inference_risk_evaluation\n", - "from leakpro.synthetic_data_attacks.linkability_utils import linkability_risk_evaluation\n", - "from leakpro.synthetic_data_attacks.singling_out_utils import singling_out_risk_evaluation\n", - "# from leakpro.metrics.attack_result import SyntheticResult\n", - "\n", - "#Get ori and syn\n", - "n_samples = 100\n", - "DATA_PATH = \"../synthetic_data/datasets/\"\n", - "ori = pd.read_csv(os.path.join(DATA_PATH, \"adults_ori.csv\"), nrows=n_samples)\n", - "syn = pd.read_csv(os.path.join(DATA_PATH, \"adults_syn.csv\"), nrows=n_samples)" - ] - }, - { - "cell_type": "markdown", - "id": "62c44504-a5fa-4846-8132-53877f369825", - "metadata": {}, - "source": [ - "### Get anomalies of synthetic data" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ad69ece9", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a result\n", - "\n", - "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", - "print(\"Syn anom shape\",syn_anom.shape)\n", - "\n", - "sin_out_res = singling_out_risk_evaluation(\n", - " dataset = \"adults\",\n", - " ori = ori,\n", - " syn = syn_anom,\n", - " n_attacks = syn_anom.shape[0]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "373dcc8a", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-20 00:09:42,566 INFO Initializing report handler...\n", - "2024-11-20 00:09:42,567 INFO report_dir set to: ../../leakpro_output/results\n", - "2024-11-20 00:09:42,569 INFO Saving results for singling_out\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../../leakpro_output/results\n" - ] - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Import and initialize ReportHandler\n", - "from leakpro.reporting.report_handler import ReportHandler\n", - "report_handler = ReportHandler()\n", - "\n", - "# Save the result using the ReportHandler\n", - "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1d91c7e0", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-19 23:38:04,520 INFO No results of type GIAResults found.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "../../leakpro_output/results\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-11-19 23:38:14,966 INFO PDF compiled\n" - ] - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Use the ReportHandler and load all the saved results\n", - "report_handler.load_results()\n", - "\n", - "# Create results and collect corresponding latex texts\n", - "report_handler.create_results_all()\n", - "\n", - "# Create the report by compiling the latex text\n", - "report_handler.create_report()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/leakpro/attacks/gia_attacks/invertinggradients.py b/leakpro/attacks/gia_attacks/invertinggradients.py index 4e2fc369..4b583e7d 100755 --- a/leakpro/attacks/gia_attacks/invertinggradients.py +++ b/leakpro/attacks/gia_attacks/invertinggradients.py @@ -44,6 +44,7 @@ def __init__(self: Self, model: Module, client_loader: DataLoader, train_fn: Cal self.train_fn = train_fn self.data_mean = data_mean self.data_std = data_std + self.configs = configs self.t_v_scale = configs.total_variation self.attack_lr = configs.attack_lr self.iterations = configs.at_iterations @@ -123,7 +124,8 @@ def run_attack(self:Self) -> GIAResults: pass return GIAResults(self.client_loader, self.best_reconstruction, - dataloaders_psnr(self.client_loader, self.reconstruction_loader), self.data_mean, self.data_std) + dataloaders_psnr(self.client_loader, self.reconstruction_loader), self.data_mean, self.data_std, + self.configs) def gradient_closure(self: Self, optimizer: torch.optim.Optimizer) -> Callable: diff --git a/leakpro/attacks/mia_attacks/attack_p.py b/leakpro/attacks/mia_attacks/attack_p.py index 7ff4d3c9..44509546 100755 --- a/leakpro/attacks/mia_attacks/attack_p.py +++ b/leakpro/attacks/mia_attacks/attack_p.py @@ -86,7 +86,7 @@ def prepare_attack(self:Self) -> None: logger.info("Computing signals for the Population attack") self.attack_signal = np.array(self.signal([self.target_model], self.handler, attack_data_indices)).squeeze() - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Run the attack on the target model and dataset. Args: diff --git a/leakpro/attacks/mia_attacks/lira.py b/leakpro/attacks/mia_attacks/lira.py index c366302e..d52a347b 100755 --- a/leakpro/attacks/mia_attacks/lira.py +++ b/leakpro/attacks/mia_attacks/lira.py @@ -8,7 +8,7 @@ from leakpro.attacks.utils.boosting import Memorization from leakpro.attacks.utils.shadow_model_handler import ShadowModelHandler from leakpro.input_handler.abstract_input_handler import AbstractInputHandler -from leakpro.metrics.attack_result import CombinedMetricResult, MIAResult +from leakpro.metrics.attack_result import MIAResult from leakpro.signals.signal import ModelRescaledLogits from leakpro.utils.import_helper import Self from leakpro.utils.logger import logger @@ -159,12 +159,12 @@ def prepare_attack(self:Self)->None: logger.info(f"Some shadow model(s) contains {count_in_samples} IN samples in total for the model(s)") logger.info("This is not an offline attack!") - self.logger.info(f"Calculating the logits for all {self.num_shadow_models} shadow models") + logger.info(f"Calculating the logits for all {self.num_shadow_models} shadow models") self.shadow_models_logits = np.swapaxes(self.signal(self.shadow_models, self.handler, self.audit_dataset["data"], self.eval_batch_size), 0, 1) # Calculate logits for the target model - self.logger.info("Calculating the logits for the target model") + logger.info("Calculating the logits for the target model") self.target_logits = np.swapaxes(self.signal([self.target_model], self.handler, self.audit_dataset["data"], self.eval_batch_size), 0, 1).squeeze() @@ -246,7 +246,7 @@ def _individual_carlini(self:Self, logits: list, mask: list, is_in: bool) -> np. return self.fixed_in_std return self.fixed_out_std - def run_attack(self:Self) -> CombinedMetricResult: + def run_attack(self:Self) -> MIAResult: """Runs the attack on the target model and dataset and assess privacy risks or data leakage. This method evaluates how the target model's output (logits) for a specific dataset diff --git a/leakpro/attacks/mia_attacks/loss_trajectory.py b/leakpro/attacks/mia_attacks/loss_trajectory.py index 072b5aa9..a51c2390 100755 --- a/leakpro/attacks/mia_attacks/loss_trajectory.py +++ b/leakpro/attacks/mia_attacks/loss_trajectory.py @@ -423,4 +423,4 @@ def run_attack(self:Self) -> MIAResult: true_labels=true_labels, predictions_proba=None, signal_values=signals, - ) \ No newline at end of file + ) diff --git a/leakpro/leakpro.py b/leakpro/leakpro.py index 9b24d506..7b060809 100755 --- a/leakpro/leakpro.py +++ b/leakpro/leakpro.py @@ -32,7 +32,6 @@ setup, ) from leakpro.input_handler.modality_extensions.tabular_extension import TabularExtension -from leakpro.reporting.utils import prepare_privacy_risk_report from leakpro.utils.import_helper import Self from leakpro.utils.logger import add_file_handler, logger @@ -126,17 +125,25 @@ def setup_handler(self:Self, handler_class:AbstractInputHandler, configs:dict) - return handler - def run_audit(self:Self) -> None: + def run_audit(self:Self, return_results: bool = False) -> None: """Run the audit.""" audit_results = self.attack_scheduler.run_attacks() + results = [] if return_results else None for attack_name in audit_results: logger.info(f"Preparing results for attack: {attack_name}") - prepare_privacy_risk_report( - audit_results[attack_name]["result_object"], - self.handler.configs["audit"], - save_path=f"{self.report_dir}/{attack_name}", - ) + if return_results: + + result = audit_results[attack_name]["result_object"] + result.attack_name = attack_name + result.configs = self.handler.configs["audit"] + + # Append + results.append(result) + else: + result = audit_results[attack_name]["result_object"] + result.save(name=attack_name, path=self.report_dir, config=self.handler.configs["audit"]) logger.info("Auditing completed") + return results diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index 5bb72131..cf5a7cd6 100755 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -105,7 +105,6 @@ def __init__( # noqa: PLR0913 threshold: Threshold computed by the metric. """ - # TODO REDIFINE THE CLASS SO IT DOSE NOT STORE MATRICIES BUT VECTORS self.predicted_labels = predicted_labels self.true_labels = true_labels @@ -194,7 +193,6 @@ def __init__( # noqa: PLR0913 metadata: dict = None, resultname: str = None, id: str = None, - load: bool = False, )-> None: """Compute and store the accuracy, ROC AUC score, and the confusion matrix for a metric. @@ -205,7 +203,7 @@ def __init__( # noqa: PLR0913 predictions_proba: Continuous version of the predicted_labels. signal_values: Values of the signal used by the metric. threshold: Threshold computed by the metric. - audit_indices: The connesponding dataset indices for the results + audit_indices: The corresponding dataset indices for the results id: The identity of the attack load: If the data should be loaded metadata: Metadata about the results @@ -223,7 +221,9 @@ def __init__( # noqa: PLR0913 self.resultname = resultname self.id = id - if load: + if true_labels is None or predicted_labels is None: + self.tn, self.tp, self.fn, self.fp = 0.0, 0.0, 0.0, 0.0 + self.fpr, self.tpr, self.roc_auc = 0.0, 0.0, 0.0 return self.tn = np.sum(true_labels == 0) - np.sum( @@ -235,8 +235,13 @@ def __init__( # noqa: PLR0913 predicted_labels[:, true_labels == 1], axis=1 ) - self.fpr = self.fp / (self.fp + self.tn) - self.tpr = self.tp / (self.tp + self.fn) + self.fpr = np.divide(self.fp.astype(float), (self.fp + self.tn).astype(float), + out=np.zeros_like(self.fp, dtype=float), + where=(self.fp + self.tn) != 0.0) + self.tpr = np.divide(self.tp.astype(float), (self.tp + self.fn).astype(float), + out=np.zeros_like(self.tp, dtype=float), + where=(self.tp + self.fn) != 0.0) + self.roc_auc = auc(self.fpr, self.tpr) @@ -244,7 +249,7 @@ def __init__( # noqa: PLR0913 def load(data: dict) -> None: """Load the MIAResults to disk.""" - miaresult = MIAResult(load=True) + miaresult = MIAResult() miaresult.resultname = data["resultname"] miaresult.resulttype = data["resulttype"] @@ -283,9 +288,9 @@ def save(self:Self, path: str, name: str, config:dict = None, show_plot:bool = F "roc_auc": self.roc_auc, "config": config, "fixed_fpr": fixed_fpr_table, - "audit_indices": self.audit_indices.tolist(), - "signal_values": self.signal_values.tolist(), - "true_labels": self.true_labels.tolist(), + "audit_indices": self.audit_indices.tolist() if self.audit_indices is not None else None, + "signal_values": self.signal_values.tolist() if self.signal_values is not None else None, + "true_labels": self.true_labels.tolist() if self.true_labels is not None else None, "threshold": self.threshold.tolist() if self.threshold is not None else None, "id": name, } @@ -299,19 +304,19 @@ def save(self:Self, path: str, name: str, config:dict = None, show_plot:bool = F json.dump(data, f) # Create ROC plot for MIAResult - filename = f"{save_path}/ROC" - temp_res = MIAResult(load=True) + temp_res = MIAResult() temp_res.tpr = self.tpr temp_res.fpr = self.fpr temp_res.id = self.id self.create_plot(results = [temp_res], - filename = filename, + save_dir = save_path, + save_name = "ROC", show_plot = show_plot ) # Create SignalHistogram plot for MIAResult - filename = f"{save_path}/SignalHistogram.png" - self.create_signal_histogram(filename = filename, + self.create_signal_histogram(save_path = save_path, + save_name = "SignalHistogram", signal_values = self.signal_values, true_labels = self.true_labels, threshold = self.threshold, @@ -324,7 +329,9 @@ def get_strongest(results: list) -> list: return max((res for res in results), key=lambda d: d.roc_auc) def create_signal_histogram( - self:Self, filename: str, + self:Self, + save_path: str, + save_name: str, signal_values: list, true_labels: list, threshold: float, @@ -332,6 +339,7 @@ def create_signal_histogram( ) -> None: """Method to create Signal Histogram.""" + filename = f"{save_path}/{save_name}" values = np.array(signal_values).ravel() labels = np.array(true_labels).ravel() @@ -368,7 +376,7 @@ def create_signal_histogram( plt.xlabel("Signal value") plt.ylabel("Number of samples") plt.title("Signal histogram") - plt.savefig(fname=filename, dpi=1000) + plt.savefig(fname=filename+".png", dpi=1000) if show_plot: plt.show() else: @@ -437,7 +445,7 @@ def _get_results_of_name( def create_results( results: list, save_dir: str = "./", - save_name: str = "foo", + save_name: str = "foo", # noqa: ARG004 show_plot: bool = False, ) -> str: """Result method for MIAResult.""" @@ -508,19 +516,17 @@ def config_latex_style(config: str) -> str: """ return latex_content - - class GIAResults: """Contains results for a GIA attack.""" def __init__( self: Self, - original_data: DataLoader, - recreated_data: DataLoader, - psnr_score: float, - data_mean: float, - data_std: float, - load: bool + original_data: DataLoader = None, + recreated_data: DataLoader = None, + psnr_score: float = None, + data_mean: float = None, + data_std: float = None, + config: dict = None, ) -> None: self.original_data = original_data @@ -528,36 +534,55 @@ def __init__( self.PSNR_score = psnr_score self.data_mean = data_mean self.data_std = data_std + self.config = config - if load: - return - + @staticmethod def load( - self:Self, - data: dict + data:dict ) -> None: """Load the GIAResults from disk.""" - self.original = data["original"] - self.resulttype = data["resulttype"] - self.recreated = data["recreated"] - self.id = data["id"] + giaresult = GIAResults() + + giaresult.original = data["original"] + giaresult.resulttype = data["resulttype"] + giaresult.recreated = data["recreated"] + giaresult.id = data["id"] + giaresult.result_config = data["result_config"] + + return giaresult def save( self: Self, - save_path: str, name: str, - config: dict, - show_plot: bool = False + path: str, + config: dict, # noqa: ARG002 + show_plot: bool = False # noqa: ARG002 ) -> None: """Save the GIAResults to disk.""" - result_config = config["attack_list"][name] + def get_gia_config(instance: Any, skip_keys: List[str] = None) -> dict: + """Extract manually typed variables and their values from a class instance with options to skip keys.""" + if skip_keys is None: + skip_keys = [] + + cls_annotations = instance.__class__.__annotations__ # Get typed attributes + return { + var: getattr(instance, var) + for var in cls_annotations + if var not in skip_keys # Exclude skipped keys + } + + result_config = get_gia_config(self.config, skip_keys=["optimizer", "criterion"]) # Get the name for the attack configuration config_name = get_config_name(result_config) self.id = f"{name}{config_name}" - save_path = f"{save_path}/{name}/{self.id}" + path = f"{path}/gradient_inversion/{self.id}" + + # Check if path exists, otherwise create it. + if not os.path.exists(f"{path}"): + os.makedirs(f"{path}") def extract_tensors_from_subset(dataset: Dataset) -> Tensor: all_tensors = [] @@ -574,46 +599,34 @@ def extract_tensors_from_subset(dataset: Dataset) -> Tensor: original_data = extract_tensors_from_subset(self.original_data.dataset) output_denormalized = clamp(recreated_data * self.data_std + self.data_mean, 0, 1) - recreated = os.path.join(save_path, "recreated_image.png") + recreated = os.path.join(path, "recreated_image.png") save_image(output_denormalized, recreated) gt_denormalized = clamp(original_data * self.data_std + self.data_mean, 0, 1) - original = os.path.join(save_path, "original_image.png") + original = os.path.join(path, "original_image.png") save_image(gt_denormalized, original) - if show_plot: - # Plot output - plt.plot(output_denormalized) - plt.show() - - # Plot ground truth - plt.plot(gt_denormalized) - plt.show() - # Data to be saved data = { "resulttype": self.__class__.__name__, "original": original, "recreated": recreated, + "result_config": result_config, "id": self.id, } - # Check if path exists, otherwise create it. - if not os.path.exists(f"{save_path}"): - os.makedirs(f"{save_path}") - # Save the results to a file - with open(f"{save_path}/data.json", "w") as f: + with open(f"{path}/data.json", "w") as f: json.dump(data, f) @staticmethod def create_results( results: list, - save_dir: str = "./", - save_name: str = "foo", + save_dir: str = "./", # noqa: ARG004 + save_name: str = "foo", # noqa: ARG004 ) -> str: """Result method for GIA.""" - + latex = "" def _latex( save_name: str, original: str, @@ -623,346 +636,19 @@ def _latex( return f""" \\subsection{{{" ".join(save_name.split("_"))}}} \\begin{{figure}}[ht] - \\includegraphics[width=0.8\\textwidth]{{{original}}} + \\includegraphics[width=0.6\\textwidth]{{{original}}} \\caption{{Original}} \\end{{figure}} \\begin{{figure}}[ht] - \\includegraphics[width=0.8\\textwidth]{{{recreated}}} - \\caption{{Original}} + \\includegraphics[width=0.6\\textwidth]{{{recreated}}} + \\caption{{Recreated}} \\end{{figure}} """ - return _latex(save_name=save_name, original=save_dir+"recreated_image.png", recreated=save_dir+"original_image.png") - -# class SyntheticResult: -# """Contains results for SyntheticResult.""" - -# def __init__( # noqa: PLR0913 -# self:Self, -# SynRes: Union[SinglingOutResults, LinkabilityResults, InferenceResults], -# load: bool = False, -# ) -> None: -# """Initalze SyntheticResult method.""" - -# # Initialize values to result object -# self.SynRes = SynRes - -# # Have a method to return if the results are to be loaded -# if load: -# return - -# # Create some result -# self.result_values = self.create_result(self.values) - -# def load( -# self:Self, -# data: dict -# ) -> None: -# """Load the SyntheticResult class to disk.""" -# self.result_values = data["some_result"] - -# def save( -# self:Self, -# save_path: str, -# save_name: str, -# config:dict = None -# ) -> None: -# """Save the SyntheticResult class to disk.""" - -# result_config = config["attack_list"][name] - -# # Get the name for the attack configuration -# config_name = get_config_name(result_config) -# self.id = f"{name}{config_name}" -# save_path = f"{path}/{name}/{self.id}" - -# save_name = os.path.join(save_path, f"synthetic.png") - -# SyntheticResult.plot( -# res=self.SynRes, -# show=False, -# save=True, -# save_path=save_path, -# save_name=save_name, -# ) - -# # Data to be saved -# data = { -# "synthetic_result_name": self.SynRes.__class__.__name__, -# "synthetic_result": self.SynRes, -# "image_path": save_name, -# "id": self.id -# } - -# # Check if path exists, otherwise create it. -# if not os.path.exists(f"{save_path}"): -# os.makedirs(f"{save_path}") - -# # Save the results to a file -# with open(f"{save_path}/data.json", "w") as f: -# json.dump(data, f) - -# @staticmethod -# def create_results( -# results: list, -# save_dir: str = "./", -# save_name: str = "foo", -# ) -> str: -# """Result method for SyntheticResult.""" - -# def _latex(save_name: str, result_file: str) -> str: -# """Latex method for SyntheticResult.""" -# return f""" -# \\subsection{{{" ".join(save_name.split("_"))}}} -# \\begin{{figure}}[ht] -# \\includegraphics[width=0.8\\textwidth]{{{result_file}}} -# \\caption{{Original}} -# \\end{{figure}} -# """ -# return _latex(results=results, save_name=save_name, result_file=save_dir+"synthetic.png") - -# @staticmethod -# def plot( -# res: Union[SinglingOutResults, LinkabilityResults, InferenceResults], -# high_res_flag: bool = True, -# case_flag: str = "base", -# show:bool = True, -# save:bool = False, -# save_path:str = "./", -# save_name:str = "fig.png", -# ) -> None: - -# save_name = os.path.join(save_path, save_name) - -# SyntheticResultName = res.__class__.__name__ -# if SyntheticResultName == "SinglingOutResults": -# SyntheticResult.plot_singling_out(sin_out_res=res, -# high_res_flag = high_res_flag, -# show=show, -# save=save, -# save_name=save_name) - -# elif SyntheticResultName == "LinkabilityResults": -# SyntheticResult.plot_linkability(link_res=res, -# high_res_flag = high_res_flag, -# show=show, -# save=save, -# save_name=save_name) - -# elif SyntheticResultName == "InferenceResults": -# if case_flag == "base": -# SyntheticResult.plot_ir_base_case(inf_res=res, -# high_res_flag = high_res_flag, -# show=show, -# save=save, -# save_name=save_name) -# elif case_flag == "worst": -# SyntheticResult.plot_ir_worst_case(inf_res=res, -# high_res_flag = high_res_flag, -# show=show, -# save=save, -# save_name=save_name) -# else: -# print("No such case") - -# def plot_ir_base_case( -# *, -# inf_res: InferenceResults, -# high_res_flag: bool = True, -# show: bool = True, -# save: bool = False, -# save_name: str = None, -# ) -> None: -# """Function to plot inference results base case given results. - -# Note: function is not tested and is used in examples. -# """ -# #Set res, secrets, set_secrets and set_nr_aux_cols -# res = np.array(inf_res.res) -# secrets = np.array(inf_res.secrets) -# set_secrets = sorted(set(secrets)) -# set_nr_aux_cols = np.unique(res[:,-1].astype(int)) -# # High res flag -# if high_res_flag: -# plot_save_high_res() -# # Set up the figure and get axes -# fig_title = f"Inference risk, base case scenario, {conf_level} confidence, total attacks: {int(res[:,0].sum())}" -# axs = get_figure_axes(two_axes_flag=True, fig_title=fig_title) -# # Set plot variables -# titles = ["Risk per column", "Risk per Nr aux cols"] -# xlabels = ["Secret col", "Nr aux cols"] -# sets_values = [set_secrets, set_nr_aux_cols] -# valueses = [secrets, res[:,-1]] -# assert len(axs) == len(titles) -# assert len(axs) == len(xlabels) -# assert len(axs) == len(sets_values) -# assert len(axs) == len(valueses) -# #Plotting -# for ax, title, xlabel, set_values, values in zip(axs, titles, xlabels, sets_values, valueses): -# set_labels_and_title( -# ax = ax, -# xlabel = xlabel, -# ylabel = "Risk", -# title = title -# ) -# # Iterate through values and plot bar charts -# iterate_values_plot_bar_charts(ax=ax, res=res, set_values=set_values, values=values) -# # Adding ticks -# set_ticks(ax=ax, xlabels=set_values) -# # Adding legend -# set_legend(ax=ax) -# # Save plot -# if save: -# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") -# # Show plot -# if show: -# plt.show() -# else: -# plt.clf() - -# def plot_ir_worst_case( -# *, -# inf_res: InferenceResults, -# high_res_flag: bool = True, -# show: bool = True, -# save: bool = False, -# save_name: str = None, -# ) -> None: -# """Function to plot inference results worst case given results. - -# Note: function is not tested and is used in examples. -# """ -# #Set res, secrets and set_secrets -# res = np.array(inf_res.res) -# secrets = np.array(inf_res.secrets) -# set_secrets = sorted(set(secrets)) -# # High res flag -# if high_res_flag: -# plot_save_high_res() -# # Set up the figure and get axes -# ax = get_figure_axes() -# # Iterate through secrets and plot bar charts -# iterate_values_plot_bar_charts( -# ax = ax, -# res = res, -# set_values = set_secrets, -# values = secrets, -# max_value_flag = True -# ) -# # Adding labels and title -# set_labels_and_title( -# ax = ax, -# xlabel = "Secret col", -# ylabel = "Risk", -# title = f"Inference risk, worst case scenario, total attacks: {int(res[:,0].sum())}" -# ) -# # Adding ticks -# set_ticks(ax=ax, xlabels=set_secrets) -# # Adding legend -# set_legend(ax=ax) -# # Save plot -# if save: -# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") -# # Show plot -# if show: -# plt.show() -# else: -# plt.clf() - -# @staticmethod -# def plot_linkability( -# *, -# link_res:LinkabilityResults, -# high_res_flag: bool = False, -# show: bool = True, -# save: bool = False, -# save_name: str = None, -# ) -> None: -# """Function to plot linkability results from given res. - -# Note: function is not tested and is used in examples. -# """ -# # Get res and aux_cols_nr -# res = np.array(link_res.res) -# set_nr_aux_cols = np.unique(res[:,-1].astype(int)) -# # High res flag -# if high_res_flag: -# plot_save_high_res() -# # Set up the figure and get axes -# ax = get_figure_axes() -# # Iterate through nr of columns and plot bar charts -# iterate_values_plot_bar_charts(ax=ax, res=res, set_values=set_nr_aux_cols, values=res[:, -1]) -# # Adding labels and title -# set_labels_and_title( -# ax = ax, -# xlabel = "Nr aux cols", -# ylabel = "Risk", -# title = f"Linkability risk {conf_level} confidence, total attacks: {int(res[:,0].sum())}" -# ) -# # Adding ticks -# set_ticks(ax=ax, xlabels=set_nr_aux_cols) -# # Adding legend -# set_legend(ax=ax) -# # Save plot -# if save: -# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") -# # Show plot -# if show: -# plt.show() -# else: -# plt.clf() - -# @staticmethod -# def plot_singling_out( -# *, -# sin_out_res: SinglingOutResults, -# high_res_flag: bool = True, -# show: bool = True, -# save: bool = False, -# save_name: str = None, -# ) -> None: -# """Function to plot singling out given results. - -# Note: function is not tested and is used in examples. -# """ -# #Set res, n_cols and set_n_cols -# res = np.array(sin_out_res.res) -# n_cols = res[:,-1].astype(int).tolist() -# set_n_cols = np.unique(n_cols) -# # High res flag -# if high_res_flag: -# plot_save_high_res() -# # Set up the figure and get axes -# ax = get_figure_axes() -# # Iterate through values and plot bar charts -# iterate_values_plot_bar_charts( -# ax = ax, -# res = res, -# set_values = set_n_cols, -# values = n_cols, -# max_value_flag = True -# ) -# # Adding labels and title -# fig_title = f"Singling out risk total attacks: {int(res[:,0].sum())}" -# if res.shape[0]==1: -# fig_title += f", n_cols={int(res[0,-1])}" -# set_labels_and_title( -# ax = ax, -# xlabel = "n_cols for predicates", -# ylabel = "Risk", -# title = fig_title -# ) -# # Adding ticks -# set_ticks(ax=ax, xlabels=set_n_cols) -# # Adding legend -# if save: -# plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") -# # Show plot -# if show: -# plt.show() -# else: -# plt.clf() - + unique_names = reduce_to_unique_labels(results) + for res, name in zip(results, unique_names): + latex += _latex(save_name=name, original=res.original, recreated=res.recreated) + return latex class TEMPLATEResult: @@ -970,27 +656,24 @@ class TEMPLATEResult: def __init__( # noqa: PLR0913 self:Self, - values: list, - load: bool = False, + values: list = None, ) -> None: - """Initalze Result method.""" + """Initialize the result method.""" # Initialize values to result object self.values = values - # Have a method to return if the results are to be loaded - if load: - return - - # Create some result + # Create some latex result self.result_values = self.create_result(self.values) + @staticmethod def load( - self:Self, data: dict ) -> None: """Load the TEMPLATEResult class to disk.""" - self.result_values = data["some_result"] + template_res = TEMPLATEResult() + template_res.values = data["some_values"] + return template_res def save( self:Self, @@ -1000,15 +683,13 @@ def save( ) -> None: """Save the TEMPLATEResult class to disk.""" - result_config = config["attack_list"][name] - # Data to be saved data = { - "some_result": self.result_values + "some_values": self.values } # Get the name for the attack configuration - config_name = get_config_name(result_config) + config_name = get_config_name(config) self.id = f"{name}{config_name}" # Check if path exists, otherwise create it. diff --git a/leakpro/reporting/report_handler.py b/leakpro/reporting/report_handler.py index ec6e3621..2b9f39e7 100644 --- a/leakpro/reporting/report_handler.py +++ b/leakpro/reporting/report_handler.py @@ -6,6 +6,8 @@ import subprocess from leakpro.metrics.attack_result import GIAResults, MIAResult +from leakpro.synthetic_data_attacks.inference_utils import InferenceResults +from leakpro.synthetic_data_attacks.linkability_utils import LinkabilityResults from leakpro.synthetic_data_attacks.singling_out_utils import SinglingOutResults from leakpro.utils.import_helper import Self, Union from leakpro.utils.logger import setup_logger @@ -26,13 +28,15 @@ def __init__(self:Self, report_dir: str = None, logger:logging.Logger = None) -> self.leakpro_types = ["MIAResult", "GIAResults", "SinglingOutResults", + "InferenceResults", + "LinkabilityResults" ] # Initiate empty lists for the different types of LeakPro attack types for key in self.leakpro_types: self.pdf_results[key] = [] - def _try_find_rep_dir(self): + def _try_find_rep_dir(self:Self) -> str: save_path = "../leakpro_output/results" # Check if path exists, otherwise create it. for _ in range(3): @@ -44,10 +48,17 @@ def _try_find_rep_dir(self): if not os.path.exists(save_path): save_path = "../../leakpro_output/results" os.makedirs(save_path) - return save_path + return save_path - def save_results(self:Self, attack_name: str = None, result_data: Union[MIAResult, GIAResults, SinglingOutResults] = None, config: dict = None) -> None: + def save_results(self:Self, + attack_name: str = None, + result_data: Union[MIAResult, + GIAResults, + InferenceResults, + LinkabilityResults, + SinglingOutResults] = None, + config: dict = None) -> None: """Save method for results.""" self.logger.info(f"Saving results for {attack_name}") @@ -69,62 +80,51 @@ def load_results(self:Self) -> None: resulttype = data["resulttype"] # Dynamically get the class from its name (resulttype) - # This assumes that the class is already defined in the current module or imported + # This assumes that the class is already defined in the current module or imported to context if resulttype in globals() and callable(globals()[resulttype]): cls = globals()[resulttype] else: raise ValueError(f"Class '{resulttype}' not found.") - # Initialize the class using the saved primitives - # instance = cls(load=True) - data["id"] = subdir.name instance = cls.load(data) - - # if instance.id is None: - # instance.id = subdir.name - - # if instance.resultname is None: - # instance.resultname = parentdir.name - self.results.append(instance) except Exception as e: - self.logger.info(f"Not able to load data, Error: {e}") + self.logger.info(f"In ReportHandler.load_results(), Not able to load data, Error: {e}") def create_results( self:Self, - types: list = [], + types: list = None, ) -> None: """Result method to group all attacks.""" for result_type in types: - # try: - # Get all results of type "Result" - # results = [res for res in self.results if res.resulttype == result_type] - results = [res for res in self.results if res.__class__.__name__ == result_type] + try: + # Get all results of type result_type + results = [res for res in self.results if res.__class__.__name__ == result_type] - # If no results of type "result_type" is found, skip to next result_type - if not results: - self.logger.info(f"No results of type {result_type} found.") - continue + # If no results of type "result_type" is found, skip to next result_type + if not results: + self.logger.info(f"No results of type {result_type} found.") + continue - # Check if the result type has a 'create_results' method - try: - result_class = globals().get(result_type) - except Exception as e: - self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") - continue + # Check if the result type has a 'create_results' method + try: + result_class = globals().get(result_type) + except Exception as e: + self.logger.info(f"No {result_type} class could be found or exists. Error: {e}") + continue - if hasattr(result_class, "create_results") and callable(result_class.create_results): + if hasattr(result_class, "create_results") and callable(result_class.create_results): - # Create all results - latex_results = result_class.create_results(results=results, - save_dir=self.report_dir, - ) - self.pdf_results[result_type].append(latex_results) + # Create all results + latex_results = result_class.create_results(results=results, + save_dir=self.report_dir, + ) + self.pdf_results[result_type].append(latex_results) - # except Exception as e: - # self.logger.info(f"Error in results all: {result_class}, {e}") + except Exception as e: + self.logger.info(f"Error in results all: {result_class}, {e}") def create_results_all( self:Self, @@ -148,7 +148,8 @@ def create_results_syn( self:Self, ) -> None: """Method to create Synthetic results.""" - self.create_results(types=["SinglingOutResults", + + self.create_results(types=["SinglingOutResults", "InferenceResults", "LinkabilityResults"]) diff --git a/leakpro/run.py b/leakpro/run.py index d8f97da4..80d9551b 100644 --- a/leakpro/run.py +++ b/leakpro/run.py @@ -15,7 +15,8 @@ def run_inverting(model: Module, client_data: DataLoader, train_fn: Callable, """Runs InvertingGradients.""" attack = InvertingGradients(model, client_data, train_fn, data_mean, data_std, config) result = attack.run_attack() - result.prepare_privacy_risk_report(experiment_name, "./leakpro_output/results") + result.save(name=experiment_name, path="./leakpro_output/results") + return result def run_inverting_audit(model: Module, dataset: Dataset, train_fn: Callable, data_mean: torch.Tensor, data_std: torch.Tensor diff --git a/leakpro/synthetic_data_attacks/inference_utils.py b/leakpro/synthetic_data_attacks/inference_utils.py index d5915350..df35d694 100755 --- a/leakpro/synthetic_data_attacks/inference_utils.py +++ b/leakpro/synthetic_data_attacks/inference_utils.py @@ -1,14 +1,16 @@ """Inference risk util functions.""" import itertools +import json import math +import os import random -from typing import List, Union from pandas import DataFrame from pydantic import BaseModel from leakpro.synthetic_data_attacks.anonymeter.evaluators.inference_evaluator import InferenceEvaluator from leakpro.synthetic_data_attacks.utils import load_res_json_file, save_res_json_file +from leakpro.utils.import_helper import List, Self, Union class InferenceResults(BaseModel): @@ -31,6 +33,101 @@ class InferenceResults(BaseModel): res: List[List[Union[int,float]]] aux_cols: List[List[str]] secrets: List[str] + worst_case_flag: bool + + def save(self:Self, + path: str = "../leakpro_output/results/", + name: str = "inference", + case_flag:str = "base", # noqa: ARG002 + config: dict = None # noqa: ARG002 + ) -> None: + """Save method for InferenceResults.""" + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "resultname": name, + "res": self.model_dump(), + } + + # Check if path exists, otherwise create it. + for _ in range(3): + if os.path.exists(path): + break + path = "../" + path + + # If no result folder can be found + if not os.path.exists(path): + os.makedirs("../../leakpro_output/results/") + + # Save the results to a file + if not os.path.exists(f"{path}/inference_risk/{name}"): + os.makedirs(f"{path}/inference_risk/{name}") + + with open(f"{path}/inference_risk/{name}/data.json", "w") as f: + json.dump(data, f) + + + self.plot(worst_case_flag=self.worst_case_flag, + show=False, + save=True, + save_path=f"{path}/inference_risk/{name}", + save_name=name + ) + + @staticmethod + def load(data: dict) -> "InferenceResults": + """Load method for InferenceResults.""" + return InferenceResults(res=data["res"]["res"], + res_cols=data["res"]["res_cols"], + aux_cols=data["res"]["aux_cols"], + secrets=data["res"]["secrets"], + worst_case_flag=data["res"]["worst_case_flag"]) + + def plot(self:Self, + high_res_flag: bool = False, + worst_case_flag: bool = False, + show: bool = True, + save: bool = False, + save_path: str = "./", + save_name: str = "fig.png" + ) -> None: + """Plot method for InferenceResults.""" + from leakpro.synthetic_data_attacks.plots import plot_ir_base_case, plot_ir_worst_case + + plot_inference = plot_ir_worst_case if worst_case_flag else plot_ir_base_case + plot_inference(inf_res=InferenceResults(res=self.res, + res_cols=self.res_cols, + aux_cols=self.aux_cols, + secrets=self.secrets, + worst_case_flag=self.worst_case_flag), + high_res_flag=high_res_flag, + show=show, + save=save, + save_name=f"{save_path}/{save_name}") + + @staticmethod + def create_results(results: list, save_dir: str = "./") -> str: + """Result method for InferenceResults.""" + latex = "" + + def _latex(save_dir: str, save_name: str) -> str: + """Latex method for InferenceResults.""" + filename = f"{save_dir}/{save_name}.png" + return f""" + \\subsection{{{" ".join(save_name.split("_"))}}} + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{filename}}} + \\caption{{Original}} + \\end{{figure}} + """ + + for res in results: + name = "inference_"+("worst_case" if res.worst_case_flag else "base_case") + + res.plot(show=False, save=True, save_path=save_dir, save_name=name) + latex += _latex(save_dir=save_dir, save_name=name) + return latex def get_inference_prefix(*, worst_case_flag: bool) -> str: """Auxiliary function to get inference prefix used in json results filename.""" @@ -166,7 +263,8 @@ def inference_risk_evaluation( res_cols = res_cols, res = res, aux_cols = aux_cols_all, - secrets = secrets + secrets = secrets, + worst_case_flag = worst_case_flag ) #Save results to json if save_results_json: diff --git a/leakpro/synthetic_data_attacks/linkability_utils.py b/leakpro/synthetic_data_attacks/linkability_utils.py index 27c33bca..da482c35 100755 --- a/leakpro/synthetic_data_attacks/linkability_utils.py +++ b/leakpro/synthetic_data_attacks/linkability_utils.py @@ -1,7 +1,8 @@ """Linkability risk util functions.""" import itertools +import json import math -from typing import List, Union +import os import numpy as np from pandas import DataFrame @@ -9,6 +10,7 @@ from leakpro.synthetic_data_attacks.anonymeter.evaluators.linkability_evaluator import LinkabilityEvaluator from leakpro.synthetic_data_attacks.utils import load_res_json_file, save_res_json_file +from leakpro.utils.import_helper import List, Self, Union def aux_assert_input_values_get_combs_2_buckets(*, cols: List, buck1_nr: int, buck2_nr: int) -> None: @@ -193,6 +195,81 @@ class LinkabilityResults(BaseModel): res: List[List[Union[int,float]]] aux_cols: List[List[List[str]]] + def save(self:Self, path: str = "../leakpro_output/results/", name: str = "linkability", config: dict = None) -> None: # noqa: ARG002 + """Save method for LinkabilityResults.""" + + id = "linkability" + + # Data to be saved + data = { + "resulttype": self.__class__.__name__, + "resultname": name, + "res": self.model_dump(), + "id": id, + } + + # Check if path exists, otherwise create it. + for _ in range(3): + if os.path.exists(path): + break + path = "../" + path + + # If no result folder can be found + if not os.path.exists(path): + os.makedirs("../../leakpro_output/results/") + + # Save the results to a file + if not os.path.exists(f"{path}/{name}/{id}"): + os.makedirs(f"{path}/{name}/{id}") + + with open(f"{path}/{name}/{id}/data.json", "w") as f: + json.dump(data, f) + + self.plot(show=False, + save=True, + save_path=f"{path}", + save_name=f"{name}/{id}/{name}") + + @staticmethod + def load(data: dict) -> "LinkabilityResults": + """Load method for LinkabilityResults.""" + return LinkabilityResults(res=data["res"]["res"], + res_cols=data["res"]["res_cols"], + aux_cols=data["res"]["aux_cols"]) + + def plot(self:Self, high_res_flag: bool = False, show: bool = True, save: bool = False, + save_path: str = "./", save_name: str = "fig.png") -> None: + """Plot method for LinkabilityResults.""" + from leakpro.synthetic_data_attacks.plots import plot_linkability + plot_linkability(link_res=LinkabilityResults(res=self.res, + res_cols=self.res_cols, + aux_cols=self.aux_cols), + high_res_flag=high_res_flag, + show=show, + save=save, + save_name=f"{save_path}/{save_name}") + + @staticmethod + def create_results(results: list, save_dir: str = "./") -> str: + """Result method for LinkabilityResults.""" + latex = "" + + def _latex(save_dir: str, save_name: str) -> str: + """Latex method for LinkabilityResults.""" + filename = f"{save_dir}/{save_name}.png" + return f""" + \\subsection{{{" ".join(save_name.split("_"))}}} + \\begin{{figure}}[ht] + \\includegraphics[width=0.8\\textwidth]{{{filename}}} + \\caption{{Original}} + \\end{{figure}} + """ + + for res in results: + res.plot(show=False, save=True, save_path=save_dir, save_name="linkability") + latex += _latex(save_dir=save_dir, save_name="linkability") + return latex + def linkability_risk_evaluation( ori: DataFrame, syn: DataFrame, diff --git a/leakpro/synthetic_data_attacks/plots.py b/leakpro/synthetic_data_attacks/plots.py index 4540f474..6293c0b9 100755 --- a/leakpro/synthetic_data_attacks/plots.py +++ b/leakpro/synthetic_data_attacks/plots.py @@ -82,7 +82,13 @@ def iterate_values_plot_bar_charts(*, max_value = res[:, 4:7].max() ax.set_ylim(0, max_value * 1.05) -def plot_linkability(*, link_res: LinkabilityResults, high_res_flag: bool = True) -> None: +def plot_linkability(*, + link_res: LinkabilityResults, + high_res_flag: bool = True, + show: bool = True, + save: bool = False, + save_name: str = None + ) -> None: """Function to plot linkability results from given res. Note: function is not tested and is used in examples. @@ -108,10 +114,22 @@ def plot_linkability(*, link_res: LinkabilityResults, high_res_flag: bool = True set_ticks(ax=ax, xlabels=set_nr_aux_cols) # Adding legend set_legend(ax=ax) + # Save and or show figure + if save: + plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") # Show plot - plt.show() + if show: + plt.show() + else: + plt.clf() -def plot_ir_worst_case(*, inf_res: InferenceResults, high_res_flag: bool = True) -> None: +def plot_ir_worst_case(*, + inf_res: InferenceResults, + high_res_flag: bool = True, + show: bool = True, + save: bool = False, + save_name: str = None + ) -> None: """Function to plot inference results worst case given results. Note: function is not tested and is used in examples. @@ -144,10 +162,22 @@ def plot_ir_worst_case(*, inf_res: InferenceResults, high_res_flag: bool = True) set_ticks(ax=ax, xlabels=set_secrets) # Adding legend set_legend(ax=ax) + # Save and or show figure + if save: + plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") # Show plot - plt.show() + if show: + plt.show() + else: + plt.clf() -def plot_ir_base_case(*, inf_res: InferenceResults, high_res_flag: bool = True) -> None: +def plot_ir_base_case(*, + inf_res: InferenceResults, + high_res_flag: bool = True, + show: bool = True, + save: bool = False, + save_name: str = None + ) -> None: """Function to plot inference results base case given results. Note: function is not tested and is used in examples. @@ -187,14 +217,21 @@ def plot_ir_base_case(*, inf_res: InferenceResults, high_res_flag: bool = True) # Adding legend set_legend(ax=ax) plt.tight_layout() - plt.show() + # Save and or show figure + if save: + plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") + # Show plot + if show: + plt.show() + else: + plt.clf() def plot_singling_out(*, - sin_out_res: SinglingOutResults, - high_res_flag: bool = True, - show: bool = True, - save: bool = False, - save_name: str = None + sin_out_res: SinglingOutResults, + high_res_flag: bool = True, + show: bool = True, + save: bool = False, + save_name: str = None ) -> None: """Function to plot singling out given results. @@ -231,7 +268,7 @@ def plot_singling_out(*, set_ticks(ax=ax, xlabels=set_n_cols) # Adding legend set_legend(ax=ax) - + # Save and or show figure if save: plt.savefig(fname=f"{save_name}.png", dpi=1000, bbox_inches="tight") # Show plot diff --git a/leakpro/synthetic_data_attacks/singling_out_utils.py b/leakpro/synthetic_data_attacks/singling_out_utils.py index f3c302ef..b2c7b13a 100755 --- a/leakpro/synthetic_data_attacks/singling_out_utils.py +++ b/leakpro/synthetic_data_attacks/singling_out_utils.py @@ -3,13 +3,13 @@ import multiprocessing as mp import os from itertools import repeat -from typing import Any, Callable, Dict, List, Optional, Self, Tuple, Union from pandas import DataFrame from pydantic import BaseModel from leakpro.synthetic_data_attacks.anonymeter.evaluators.singling_out_evaluator import SinglingOutEvaluator from leakpro.synthetic_data_attacks.utils import load_res_json_file, save_res_json_file +from leakpro.utils.import_helper import Any, Callable, Dict, List, Optional, Self, Tuple, Union class SinglingOutResults(BaseModel): diff --git a/leakpro/synthetic_data_attacks/utils.py b/leakpro/synthetic_data_attacks/utils.py index 80046150..f3ea7509 100755 --- a/leakpro/synthetic_data_attacks/utils.py +++ b/leakpro/synthetic_data_attacks/utils.py @@ -3,28 +3,28 @@ import os from typing import Tuple -#Path to save results -PATH_RESULTS = os.path.dirname(os.path.dirname(__file__)) + "/synthetic_data_attacks/results/" +# Default path to save results +DEFAULT_PATH_RESULTS = os.path.dirname(os.path.dirname(__file__)) + "/synthetic_data_attacks/results/" -def aux_file_path(*, prefix: str, dataset: str) -> Tuple[str,str]: +def aux_file_path(*, path: str = None, prefix: str, dataset: str) -> Tuple[str, str]: """Util function that returns file and file_path for given prefix and dataset.""" if prefix: prefix += "_" file = "res_" + prefix + dataset + ".json" - file_path = PATH_RESULTS + file + file_path = os.path.join(path or DEFAULT_PATH_RESULTS, file) return file, file_path -def save_res_json_file(*, prefix: str, dataset: str, res: dict) -> None: +def save_res_json_file(*, path: str = None, prefix: str, dataset: str, res: dict) -> None: """Util function that saves results dictionary into a json file with given prefix and dataset name.""" - file, file_path = aux_file_path(prefix=prefix, dataset=dataset) - #Create directory if does not exist - os.makedirs(os.path.dirname(PATH_RESULTS), exist_ok=True) + file, file_path = aux_file_path(path=path, prefix=prefix, dataset=dataset) + # Create directory if it does not exist + os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, "w") as f: json.dump(res, f, indent=4) - print("\n### Results saved!", file) # noqa: T201 + print("\n### Results saved!", file) # noqa: T201 -def load_res_json_file(*, prefix: str, dataset: str) -> dict: +def load_res_json_file(*, path: str = None, prefix: str, dataset: str) -> dict: """Util function that loads and returns results from json file with given prefix and dataset name.""" - _, file_path = aux_file_path(prefix=prefix, dataset=dataset) + _, file_path = aux_file_path(path=path, prefix=prefix, dataset=dataset) with open(file_path, "r") as f: return json.load(f) From 54815abfcde4afa2a458e5af9c9088b484e55f01 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Thu, 5 Dec 2024 10:26:13 +0000 Subject: [PATCH 11/14] fixed gia report_handler --- examples/report_handler/report_handler.ipynb | 75 ++++++++++---------- leakpro/run.py | 5 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/examples/report_handler/report_handler.ipynb b/examples/report_handler/report_handler.ipynb index 5e713e1d..a5f785a9 100644 --- a/examples/report_handler/report_handler.ipynb +++ b/examples/report_handler/report_handler.ipynb @@ -163,44 +163,41 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-11-27 08:38:00,656 INFO Inverting gradient initialized.\n", - "2024-11-27 08:38:02,796 INFO Iteration 0, loss 0.0003550234832800925\n", - "2024-11-27 08:38:02,803 INFO New best loss: 0.0003550234832800925 on round: 0\n", - "2024-11-27 08:38:02,946 INFO New best loss: 0.00033073360100388527 on round: 1\n", - "2024-11-27 08:38:03,193 INFO New best loss: 0.0003084556374233216 on round: 3\n", - "2024-11-27 08:38:03,552 INFO New best loss: 0.0002839812950696796 on round: 6\n", - "2024-11-27 08:38:03,674 INFO New best loss: 0.00028284190921112895 on round: 7\n", - "2024-11-27 08:38:03,802 INFO New best loss: 0.00027706200489774346 on round: 8\n", - "2024-11-27 08:38:03,924 INFO New best loss: 0.00026298483135178685 on round: 9\n", - "2024-11-27 08:38:04,303 INFO New best loss: 0.00025501404888927937 on round: 12\n", - "2024-11-27 08:38:04,427 INFO New best loss: 0.00025296382955275476 on round: 13\n", - "2024-11-27 08:38:04,677 INFO New best loss: 0.00024899665731936693 on round: 15\n", - "2024-11-27 08:38:04,807 INFO New best loss: 0.0002483891148585826 on round: 16\n", - "2024-11-27 08:38:04,936 INFO New best loss: 0.0002449913590680808 on round: 17\n", - "2024-11-27 08:38:05,063 INFO New best loss: 0.0002429946616757661 on round: 18\n", - "2024-11-27 08:38:05,185 INFO New best loss: 0.00023749274259898812 on round: 19\n", - "2024-11-27 08:38:05,890 INFO New best loss: 0.0002372416784055531 on round: 25\n", - "2024-11-27 08:38:06,015 INFO New best loss: 0.00023295756545849144 on round: 26\n", - "2024-11-27 08:38:06,139 INFO New best loss: 0.00023274944396689534 on round: 27\n", - "2024-11-27 08:38:06,266 INFO New best loss: 0.00023147281899582595 on round: 28\n", - "2024-11-27 08:38:06,394 INFO New best loss: 0.00022923419601283967 on round: 29\n", - "2024-11-27 08:38:06,646 INFO New best loss: 0.00022827980865258723 on round: 31\n", - "2024-11-27 08:38:06,775 INFO New best loss: 0.00022587741841562092 on round: 32\n", - "2024-11-27 08:38:06,907 INFO New best loss: 0.00022527067631017417 on round: 33\n", - "2024-11-27 08:38:08,442 INFO New best loss: 0.00022461664048023522 on round: 45\n", - "2024-11-27 08:38:08,569 INFO New best loss: 0.00022421970788855106 on round: 46\n", - "2024-11-27 08:38:08,696 INFO New best loss: 0.0002231039834441617 on round: 47\n", - "2024-11-27 08:38:08,819 INFO New best loss: 0.00022097492183092982 on round: 48\n", - "2024-11-27 08:38:10,106 INFO New best loss: 0.00022094276209827513 on round: 59\n", - "2024-11-27 08:38:10,216 INFO New best loss: 0.00022092672588769346 on round: 60\n", - "2024-11-27 08:38:10,328 INFO New best loss: 0.00022070117120165378 on round: 61\n", - "2024-11-27 08:38:10,434 INFO New best loss: 0.00022059425828047097 on round: 62\n", - "2024-11-27 08:38:10,539 INFO New best loss: 0.00022042241471353918 on round: 63\n", - "2024-11-27 08:38:11,368 INFO New best loss: 0.00022039355826564133 on round: 71\n", - "2024-11-27 08:38:11,472 INFO New best loss: 0.00022023300698492676 on round: 72\n", - "2024-11-27 08:38:11,680 INFO New best loss: 0.0002201125753344968 on round: 74\n", - "2024-11-27 08:38:12,092 INFO New best loss: 0.00021973479306325316 on round: 78\n", - "2024-11-27 08:38:12,195 INFO New best loss: 0.00021972827380523086 on round: 79\n", + "2024-12-05 10:20:59,883 INFO Inverting gradient initialized.\n", + "2024-12-05 10:21:01,974 INFO Iteration 0, loss 0.00037895439891144633\n", + "2024-12-05 10:21:01,981 INFO New best loss: 0.00037895439891144633 on round: 0\n", + "2024-12-05 10:21:02,261 INFO New best loss: 0.00036255631130188704 on round: 2\n", + "2024-12-05 10:21:02,527 INFO New best loss: 0.0003450227959547192 on round: 4\n", + "2024-12-05 10:21:02,669 INFO New best loss: 0.00032889979775063694 on round: 5\n", + "2024-12-05 10:21:02,807 INFO New best loss: 0.00031025282805785537 on round: 6\n", + "2024-12-05 10:21:03,075 INFO New best loss: 0.00029653109959326684 on round: 8\n", + "2024-12-05 10:21:03,212 INFO New best loss: 0.00029343905043788254 on round: 9\n", + "2024-12-05 10:21:03,357 INFO New best loss: 0.0002867175207938999 on round: 10\n", + "2024-12-05 10:21:03,482 INFO New best loss: 0.0002853123296517879 on round: 11\n", + "2024-12-05 10:21:03,607 INFO New best loss: 0.00027869627228938043 on round: 12\n", + "2024-12-05 10:21:03,735 INFO New best loss: 0.0002784693206194788 on round: 13\n", + "2024-12-05 10:21:04,092 INFO New best loss: 0.00027562331524677575 on round: 14\n", + "2024-12-05 10:21:05,240 INFO New best loss: 0.0002711182169150561 on round: 23\n", + "2024-12-05 10:21:05,367 INFO New best loss: 0.00026883225655183196 on round: 24\n", + "2024-12-05 10:21:05,622 INFO New best loss: 0.0002568941272329539 on round: 26\n", + "2024-12-05 10:21:05,748 INFO New best loss: 0.0002561017172411084 on round: 27\n", + "2024-12-05 10:21:05,876 INFO New best loss: 0.0002496801607776433 on round: 28\n", + "2024-12-05 10:21:06,517 INFO New best loss: 0.0002473248168826103 on round: 33\n", + "2024-12-05 10:21:06,648 INFO New best loss: 0.0002469118044245988 on round: 34\n", + "2024-12-05 10:21:07,403 INFO New best loss: 0.00024639995535835624 on round: 40\n", + "2024-12-05 10:21:07,532 INFO New best loss: 0.000243801434407942 on round: 41\n", + "2024-12-05 10:21:07,670 INFO New best loss: 0.0002417033538222313 on round: 42\n", + "2024-12-05 10:21:07,803 INFO New best loss: 0.0002388415450695902 on round: 43\n", + "2024-12-05 10:21:07,924 INFO New best loss: 0.00023807109391782433 on round: 44\n", + "2024-12-05 10:21:08,178 INFO New best loss: 0.00023767446691635996 on round: 46\n", + "2024-12-05 10:21:08,302 INFO New best loss: 0.0002361331571592018 on round: 47\n", + "2024-12-05 10:21:08,427 INFO New best loss: 0.00023581282584927976 on round: 48\n", + "2024-12-05 10:21:09,189 INFO New best loss: 0.0002355617325520143 on round: 54\n", + "2024-12-05 10:21:09,321 INFO New best loss: 0.00023450757726095617 on round: 55\n", + "2024-12-05 10:21:09,447 INFO New best loss: 0.0002343545202165842 on round: 56\n", + "2024-12-05 10:21:09,702 INFO New best loss: 0.0002338749181944877 on round: 58\n", + "2024-12-05 10:21:10,466 INFO New best loss: 0.00023371708812192082 on round: 64\n", + "2024-12-05 10:21:10,595 INFO New best loss: 0.00023358875478152186 on round: 65\n", "/opt/conda/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:70: FutureWarning: Importing `peak_signal_noise_ratio` from `torchmetrics.functional` was deprecated and will be removed in 2.0. Import `peak_signal_noise_ratio` from `torchmetrics.image` instead.\n", " _future_warning(\n" ] @@ -225,7 +222,7 @@ "configs = InvertingConfig()\n", "configs.at_iterations = 80 # Decreased from 8000 to avoid GPU memory crash\n", "\n", - "GIA_result = run_inverting(model, client_dataloader, train_fn, data_mean, data_std, configs)" + "GIA_result = run_inverting(model, client_dataloader, train_fn, data_mean, data_std, configs, save=False)" ] }, { diff --git a/leakpro/run.py b/leakpro/run.py index 80d9551b..797ad7da 100644 --- a/leakpro/run.py +++ b/leakpro/run.py @@ -11,11 +11,12 @@ def run_inverting(model: Module, client_data: DataLoader, train_fn: Callable, - data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients") -> None: + data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients", save:bool = False) -> None: """Runs InvertingGradients.""" attack = InvertingGradients(model, client_data, train_fn, data_mean, data_std, config) result = attack.run_attack() - result.save(name=experiment_name, path="./leakpro_output/results") + if save: + result.save(name=experiment_name, path="./leakpro_output/results", config=config) return result def run_inverting_audit(model: Module, dataset: Dataset, From 0f3cbf759b174fa749e6b407a85cbf2daf13035c Mon Sep 17 00:00:00 2001 From: henrikfo Date: Thu, 5 Dec 2024 10:26:36 +0000 Subject: [PATCH 12/14] fixed gia report_handler --- leakpro/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leakpro/run.py b/leakpro/run.py index 797ad7da..a060f78b 100644 --- a/leakpro/run.py +++ b/leakpro/run.py @@ -11,7 +11,7 @@ def run_inverting(model: Module, client_data: DataLoader, train_fn: Callable, - data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients", save:bool = False) -> None: + data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients", save:bool = True) -> None: """Runs InvertingGradients.""" attack = InvertingGradients(model, client_data, train_fn, data_mean, data_std, config) result = attack.run_attack() From 59bcec592394a542a767c7cb367964f29523f49b Mon Sep 17 00:00:00 2001 From: henrikfo Date: Tue, 10 Dec 2024 15:41:06 +0000 Subject: [PATCH 13/14] updated notebook with local directory for reporthandler example --- examples/report_handler/report_handler.ipynb | 137 +++++++++---------- leakpro/run.py | 5 +- 2 files changed, 69 insertions(+), 73 deletions(-) diff --git a/examples/report_handler/report_handler.ipynb b/examples/report_handler/report_handler.ipynb index a5f785a9..725e673e 100644 --- a/examples/report_handler/report_handler.ipynb +++ b/examples/report_handler/report_handler.ipynb @@ -61,7 +61,7 @@ "output_type": "stream", "text": [ "[Parallel(n_jobs=64)]: Using backend ThreadingBackend with 64 concurrent workers.\n", - "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 1.2s remaining: 36.4s\n", + "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 1.2s remaining: 38.1s\n", "[Parallel(n_jobs=64)]: Done 64 out of 64 | elapsed: 4.4s finished\n" ] }, @@ -148,7 +148,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "35aee5a3", "metadata": {}, "outputs": [ @@ -163,41 +163,31 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-12-05 10:20:59,883 INFO Inverting gradient initialized.\n", - "2024-12-05 10:21:01,974 INFO Iteration 0, loss 0.00037895439891144633\n", - "2024-12-05 10:21:01,981 INFO New best loss: 0.00037895439891144633 on round: 0\n", - "2024-12-05 10:21:02,261 INFO New best loss: 0.00036255631130188704 on round: 2\n", - "2024-12-05 10:21:02,527 INFO New best loss: 0.0003450227959547192 on round: 4\n", - "2024-12-05 10:21:02,669 INFO New best loss: 0.00032889979775063694 on round: 5\n", - "2024-12-05 10:21:02,807 INFO New best loss: 0.00031025282805785537 on round: 6\n", - "2024-12-05 10:21:03,075 INFO New best loss: 0.00029653109959326684 on round: 8\n", - "2024-12-05 10:21:03,212 INFO New best loss: 0.00029343905043788254 on round: 9\n", - "2024-12-05 10:21:03,357 INFO New best loss: 0.0002867175207938999 on round: 10\n", - "2024-12-05 10:21:03,482 INFO New best loss: 0.0002853123296517879 on round: 11\n", - "2024-12-05 10:21:03,607 INFO New best loss: 0.00027869627228938043 on round: 12\n", - "2024-12-05 10:21:03,735 INFO New best loss: 0.0002784693206194788 on round: 13\n", - "2024-12-05 10:21:04,092 INFO New best loss: 0.00027562331524677575 on round: 14\n", - "2024-12-05 10:21:05,240 INFO New best loss: 0.0002711182169150561 on round: 23\n", - "2024-12-05 10:21:05,367 INFO New best loss: 0.00026883225655183196 on round: 24\n", - "2024-12-05 10:21:05,622 INFO New best loss: 0.0002568941272329539 on round: 26\n", - "2024-12-05 10:21:05,748 INFO New best loss: 0.0002561017172411084 on round: 27\n", - "2024-12-05 10:21:05,876 INFO New best loss: 0.0002496801607776433 on round: 28\n", - "2024-12-05 10:21:06,517 INFO New best loss: 0.0002473248168826103 on round: 33\n", - "2024-12-05 10:21:06,648 INFO New best loss: 0.0002469118044245988 on round: 34\n", - "2024-12-05 10:21:07,403 INFO New best loss: 0.00024639995535835624 on round: 40\n", - "2024-12-05 10:21:07,532 INFO New best loss: 0.000243801434407942 on round: 41\n", - "2024-12-05 10:21:07,670 INFO New best loss: 0.0002417033538222313 on round: 42\n", - "2024-12-05 10:21:07,803 INFO New best loss: 0.0002388415450695902 on round: 43\n", - "2024-12-05 10:21:07,924 INFO New best loss: 0.00023807109391782433 on round: 44\n", - "2024-12-05 10:21:08,178 INFO New best loss: 0.00023767446691635996 on round: 46\n", - "2024-12-05 10:21:08,302 INFO New best loss: 0.0002361331571592018 on round: 47\n", - "2024-12-05 10:21:08,427 INFO New best loss: 0.00023581282584927976 on round: 48\n", - "2024-12-05 10:21:09,189 INFO New best loss: 0.0002355617325520143 on round: 54\n", - "2024-12-05 10:21:09,321 INFO New best loss: 0.00023450757726095617 on round: 55\n", - "2024-12-05 10:21:09,447 INFO New best loss: 0.0002343545202165842 on round: 56\n", - "2024-12-05 10:21:09,702 INFO New best loss: 0.0002338749181944877 on round: 58\n", - "2024-12-05 10:21:10,466 INFO New best loss: 0.00023371708812192082 on round: 64\n", - "2024-12-05 10:21:10,595 INFO New best loss: 0.00023358875478152186 on round: 65\n", + "2024-12-10 15:37:19,407 INFO Inverting gradient initialized.\n", + "2024-12-10 15:37:21,549 INFO Iteration 0, loss 0.00017312286945525557\n", + "2024-12-10 15:37:21,558 INFO New best loss: 0.00017312286945525557 on round: 0\n", + "2024-12-10 15:37:21,716 INFO New best loss: 0.00015130748215597123 on round: 1\n", + "2024-12-10 15:37:21,865 INFO New best loss: 0.00014282172196544707 on round: 2\n", + "2024-12-10 15:37:22,004 INFO New best loss: 0.0001371056423522532 on round: 3\n", + "2024-12-10 15:37:22,146 INFO New best loss: 0.00013112256419844925 on round: 4\n", + "2024-12-10 15:37:22,404 INFO New best loss: 0.00012842075375374407 on round: 6\n", + "2024-12-10 15:37:22,531 INFO New best loss: 0.00012392438657116145 on round: 7\n", + "2024-12-10 15:37:22,661 INFO New best loss: 0.00011872354662045836 on round: 8\n", + "2024-12-10 15:37:22,915 INFO New best loss: 0.00011686021025525406 on round: 10\n", + "2024-12-10 15:37:23,045 INFO New best loss: 0.00011540275590959936 on round: 11\n", + "2024-12-10 15:37:23,176 INFO New best loss: 0.00010958370694424957 on round: 12\n", + "2024-12-10 15:37:23,306 INFO New best loss: 0.00010606442810967565 on round: 13\n", + "2024-12-10 15:37:23,432 INFO New best loss: 0.00010551762534305453 on round: 14\n", + "2024-12-10 15:37:23,558 INFO New best loss: 0.00010321227455278859 on round: 15\n", + "2024-12-10 15:37:23,688 INFO New best loss: 0.00010049617412732914 on round: 16\n", + "2024-12-10 15:37:23,819 INFO New best loss: 9.98983159661293e-05 on round: 17\n", + "2024-12-10 15:37:24,073 INFO New best loss: 9.980545291909948e-05 on round: 19\n", + "2024-12-10 15:37:24,330 INFO New best loss: 9.797140228329226e-05 on round: 21\n", + "2024-12-10 15:37:24,457 INFO New best loss: 9.713656618259847e-05 on round: 22\n", + "2024-12-10 15:37:29,775 INFO New best loss: 9.707298158900812e-05 on round: 63\n", + "2024-12-10 15:37:31,176 INFO New best loss: 9.695763583295047e-05 on round: 75\n", + "2024-12-10 15:37:31,299 INFO New best loss: 9.695166954770684e-05 on round: 76\n", + "2024-12-10 15:37:31,425 INFO New best loss: 9.689731814432889e-05 on round: 77\n", "/opt/conda/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:70: FutureWarning: Importing `peak_signal_noise_ratio` from `torchmetrics.functional` was deprecated and will be removed in 2.0. Import `peak_signal_noise_ratio` from `torchmetrics.image` instead.\n", " _future_warning(\n" ] @@ -222,7 +212,8 @@ "configs = InvertingConfig()\n", "configs.at_iterations = 80 # Decreased from 8000 to avoid GPU memory crash\n", "\n", - "GIA_result = run_inverting(model, client_dataloader, train_fn, data_mean, data_std, configs, save=False)" + "name = \"my_gia_results\"\n", + "GIA_result = run_inverting(model, client_dataloader, train_fn, data_mean, data_std, configs, experiment_name=name, save=True)" ] }, { @@ -292,7 +283,7 @@ " warnings.warn(\n", "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", " warnings.warn(msg)\n", - "Training Progress: 100%|██████████| 3/3 [00:13<00:00, 4.39s/it]\n" + "Training Progress: 100%|██████████| 3/3 [00:13<00:00, 4.60s/it]\n" ] } ], @@ -332,8 +323,8 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-11-27 08:38:27,995 INFO Target model blueprint created from ResNet18 in ./mia_utils/utils/cifar_model_preparation.py.\n", - "2024-11-27 08:38:27,997 INFO Loaded target model metadata from ./target/model_metadata.pkl\n" + "2024-12-10 15:37:48,180 INFO Target model blueprint created from ResNet18 in ./mia_utils/utils/cifar_model_preparation.py.\n", + "2024-12-10 15:37:48,183 INFO Loaded target model metadata from ./target/model_metadata.pkl\n" ] }, { @@ -347,26 +338,26 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-11-27 08:38:28,210 INFO Loaded target model from ./target\n", - "2024-11-27 08:38:29,306 INFO Loaded population dataset from ./data/cifar10.pkl\n", - "2024-11-27 08:38:29,306 INFO Loaded population dataset from ./data/cifar10.pkl\n", - "2024-11-27 08:38:29,307 INFO Creating shadow model handler singleton\n", - "2024-11-27 08:38:29,308 INFO Creating distillation model handler singleton\n", - "2024-11-27 08:38:29,310 INFO Configuring the Population attack\n", - "2024-11-27 08:38:29,310 INFO Added attack: population\n", - "2024-11-27 08:38:29,311 INFO Preparing attack: population\n", - "2024-11-27 08:38:29,312 INFO Preparing attack data for training the Population attack\n", - "2024-11-27 08:38:29,316 INFO Subsampling attack data from 24000 points\n", - "2024-11-27 08:38:29,317 INFO Number of attack data points after subsampling: 24000\n", - "2024-11-27 08:38:29,318 INFO Computing signals for the Population attack\n", - "Getting loss for model 1/ 1: 100%|██████████| 750/750 [00:12<00:00, 62.27it/s]\n", - "2024-11-27 08:38:41,410 INFO Running attack: population\n", - "2024-11-27 08:38:41,415 INFO Running the Population attack on the target model\n", - "Getting loss for model 1/ 1: 100%|██████████| 1125/1125 [00:18<00:00, 62.04it/s]\n", - "2024-11-27 08:38:59,662 INFO Attack completed\n", - "2024-11-27 08:38:59,674 INFO Finished attack: population\n", - "2024-11-27 08:38:59,675 INFO Preparing results for attack: population\n", - "2024-11-27 08:38:59,675 INFO Auditing completed\n" + "2024-12-10 15:37:48,394 INFO Loaded target model from ./target\n", + "2024-12-10 15:37:49,289 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-12-10 15:37:49,290 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-12-10 15:37:49,291 INFO Creating shadow model handler singleton\n", + "2024-12-10 15:37:49,294 INFO Creating distillation model handler singleton\n", + "2024-12-10 15:37:49,296 INFO Configuring the Population attack\n", + "2024-12-10 15:37:49,297 INFO Added attack: population\n", + "2024-12-10 15:37:49,298 INFO Preparing attack: population\n", + "2024-12-10 15:37:49,299 INFO Preparing attack data for training the Population attack\n", + "2024-12-10 15:37:49,306 INFO Subsampling attack data from 24000 points\n", + "2024-12-10 15:37:49,307 INFO Number of attack data points after subsampling: 24000\n", + "2024-12-10 15:37:49,308 INFO Computing signals for the Population attack\n", + "Getting loss for model 1/ 1: 100%|██████████| 750/750 [00:12<00:00, 60.59it/s]\n", + "2024-12-10 15:38:01,752 INFO Running attack: population\n", + "2024-12-10 15:38:01,758 INFO Running the Population attack on the target model\n", + "Getting loss for model 1/ 1: 100%|██████████| 1125/1125 [00:17<00:00, 63.04it/s]\n", + "2024-12-10 15:38:19,692 INFO Attack completed\n", + "2024-12-10 15:38:19,703 INFO Finished attack: population\n", + "2024-12-10 15:38:19,703 INFO Preparing results for attack: population\n", + "2024-12-10 15:38:19,704 INFO Auditing completed\n" ] } ], @@ -395,14 +386,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-11-27 08:38:59,693 INFO Initializing report handler...\n", - "2024-11-27 08:38:59,693 INFO report_dir set to: ../../leakpro_output/results\n", - "2024-11-27 08:38:59,694 INFO Saving results for singling_out\n", - "2024-11-27 08:39:01,779 INFO Saving results for linkability_risk\n", - "2024-11-27 08:39:03,748 INFO Saving results for inference_risk_base\n", - "2024-11-27 08:39:08,623 INFO Saving results for inference_risk_worst\n", - "2024-11-27 08:39:10,925 INFO Saving results for gia\n", - "2024-11-27 08:39:10,939 INFO Saving results for population\n" + "2024-12-10 15:38:19,719 INFO Initializing report handler...\n", + "2024-12-10 15:38:19,719 INFO report_dir set to: ./leakpro_output/results\n", + "2024-12-10 15:38:19,720 INFO Saving results for singling_out\n", + "2024-12-10 15:38:21,860 INFO Saving results for linkability_risk\n", + "2024-12-10 15:38:23,763 INFO Saving results for inference_risk_base\n", + "2024-12-10 15:38:28,190 INFO Saving results for inference_risk_worst\n", + "2024-12-10 15:38:30,418 INFO Saving results for gia\n", + "2024-12-10 15:38:30,431 INFO Saving results for population\n" ] }, { @@ -448,7 +439,11 @@ "\n", "# Import and initialize ReportHandler\n", "from leakpro.reporting.report_handler import ReportHandler\n", - "report_handler = ReportHandler()\n", + "\n", + "# Set report_dir to \"./leakpro_output/results\" to the results to a local results folder\n", + "# or don't use the report_dir argument to let the ReportHandler find an already\n", + "# existing results folder\n", + "report_handler = ReportHandler(report_dir=\"./leakpro_output/results\")\n", "\n", "# # Save Synthetic results using the ReportHandler\n", "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)\n", @@ -474,7 +469,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-11-27 08:40:04,818 INFO PDF compiled\n" + "2024-12-10 15:38:59,171 INFO PDF compiled\n" ] }, { diff --git a/leakpro/run.py b/leakpro/run.py index a060f78b..8abb771d 100644 --- a/leakpro/run.py +++ b/leakpro/run.py @@ -11,12 +11,13 @@ def run_inverting(model: Module, client_data: DataLoader, train_fn: Callable, - data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients", save:bool = True) -> None: + data_mean:Tensor, data_std: Tensor, config: dict, experiment_name: str = "InvertingGradients", + path:str = "./leakpro_output/results", save:bool = True) -> None: """Runs InvertingGradients.""" attack = InvertingGradients(model, client_data, train_fn, data_mean, data_std, config) result = attack.run_attack() if save: - result.save(name=experiment_name, path="./leakpro_output/results", config=config) + result.save(name=experiment_name, path=path, config=config) return result def run_inverting_audit(model: Module, dataset: Dataset, From 0468bc85440e49d304f928f5315b9f04d57b0419 Mon Sep 17 00:00:00 2001 From: henrikfo Date: Wed, 11 Dec 2024 13:48:09 +0000 Subject: [PATCH 14/14] Removed duplicate gia files --- examples/report_handler/gia_utils/cifar.py | 26 - examples/report_handler/gia_utils/model.py | 81 --- examples/report_handler/mia_utils/audit.yaml | 65 ++- examples/report_handler/report_handler.ipynb | 496 ++++++++++++------- leakpro/metrics/attack_result.py | 2 +- 5 files changed, 359 insertions(+), 311 deletions(-) delete mode 100644 examples/report_handler/gia_utils/cifar.py delete mode 100644 examples/report_handler/gia_utils/model.py diff --git a/examples/report_handler/gia_utils/cifar.py b/examples/report_handler/gia_utils/cifar.py deleted file mode 100644 index 98ee9490..00000000 --- a/examples/report_handler/gia_utils/cifar.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Module with functions for preparing the dataset for training the target models.""" -import torchvision -from torch import as_tensor, randperm -from torch.utils.data import DataLoader, Subset, TensorDataset -from torchvision import transforms - -from leakpro.fl_utils.data_utils import get_meanstd - - -def get_cifar10_loader(num_images:int =1, batch_size:int = 1, num_workers:int = 2 ) -> TensorDataset: - """Get the full dataset for CIFAR10.""" - trainset = torchvision.datasets.CIFAR10(root="./data", train=True, download=True, transform=transforms.ToTensor()) - data_mean, data_std = get_meanstd(trainset) - transform = transforms.Compose([ - transforms.ToTensor(), - transforms.Normalize(data_mean, data_std)]) - trainset.transform = transform - - total_examples = len(trainset) - random_indices = randperm(total_examples)[:num_images] - subset_trainset = Subset(trainset, random_indices) - trainloader = DataLoader(subset_trainset, batch_size=batch_size, - shuffle=False, drop_last=True, num_workers=num_workers) - data_mean = as_tensor(data_mean)[:, None, None] - data_std = as_tensor(data_std)[:, None, None] - return trainloader, data_mean, data_std diff --git a/examples/report_handler/gia_utils/model.py b/examples/report_handler/gia_utils/model.py deleted file mode 100644 index 403ff164..00000000 --- a/examples/report_handler/gia_utils/model.py +++ /dev/null @@ -1,81 +0,0 @@ -"""ResNet model.""" -from typing import Optional - -import torch -import torchvision -from torch import nn -from torchvision.models.resnet import BasicBlock, Bottleneck - -from leakpro.utils.import_helper import Self - - -class ResNet(torchvision.models.ResNet): - """ResNet generalization for CIFAR thingies.""" - - def __init__(self: Self, block: BasicBlock, layers: list, num_classes: int=10, zero_init_residual: bool=False, # noqa: C901 - groups: int=1, base_width: int=64, replace_stride_with_dilation: list=None, - norm_layer: Optional[nn.Module]=None, strides: list=[1, 2, 2, 2], pool: str="avg") -> None: # noqa: B006 - """Initialize as usual. Layers and strides are scriptable.""" - super(torchvision.models.ResNet, self).__init__() # nn.Module - if norm_layer is None: - norm_layer = nn.BatchNorm2d - self._norm_layer = norm_layer - - - self.dilation = 1 - if replace_stride_with_dilation is None: - # each element in the tuple indicates if we should replace - # the 2x2 stride with a dilated convolution instead - replace_stride_with_dilation = [False, False, False, False] - if len(replace_stride_with_dilation) != 4: - raise ValueError("replace_stride_with_dilation should be None " - "or a 4-element tuple, got {}".format(replace_stride_with_dilation)) - self.groups = groups - - self.inplanes = base_width - self.base_width = 64 # Do this to circumvent BasicBlock errors. The value is not actually used. - self.conv1 = nn.Conv2d(3, self.inplanes, kernel_size=3, stride=1, padding=1, bias=False) - self.bn1 = norm_layer(self.inplanes) - self.relu = nn.ReLU(inplace=True) - - self.layers = torch.nn.ModuleList() - width = self.inplanes - for idx, layer in enumerate(layers): - self.layers.append(self._make_layer(block, width, layer, stride=strides[idx], dilate=replace_stride_with_dilation[idx])) - width *= 2 - - self.pool = nn.AdaptiveAvgPool2d((1, 1)) if pool == "avg" else nn.AdaptiveMaxPool2d((1, 1)) - self.fc = nn.Linear(width // 2 * block.expansion, num_classes) - - for m in self.modules(): - if isinstance(m, nn.Conv2d): - nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu") - elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)): - nn.init.constant_(m.weight, 1) - nn.init.constant_(m.bias, 0) - - # Zero-initialize the last BN in each residual branch, - # so that the residual branch starts with zeros, and each residual block behaves like an identity. - # This improves the model by 0.2~0.3% according to https://arxiv.org/abs/1706.02677 - if zero_init_residual: - for m in self.modules(): - if isinstance(m, Bottleneck): - nn.init.constant_(m.bn3.weight, 0) - elif isinstance(m, torchvision.models.resnet.BasicBlock): - nn.init.constant_(m.bn2.weight, 0) - - - def _forward_impl(self: Self, x: torch.Tensor) -> None: - # See note [TorchScript super()] - x = self.conv1(x) - x = self.bn1(x) - x = self.relu(x) - - for layer in self.layers: - x = layer(x) - - x = self.pool(x) - x = torch.flatten(x, 1) - x = self.fc(x) - - return x diff --git a/examples/report_handler/mia_utils/audit.yaml b/examples/report_handler/mia_utils/audit.yaml index 073fe7f1..0fcd5558 100644 --- a/examples/report_handler/mia_utils/audit.yaml +++ b/examples/report_handler/mia_utils/audit.yaml @@ -1,43 +1,38 @@ audit: # Configurations for auditing random_seed: 1234 # Integer specifying the random seed attack_list: - # rmia: - # training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) - # attack_data_fraction: 0.5 # Fraction of auxiliary dataset to sample from during attack - # num_shadow_models: 3 # Number of shadow models to train - # online: True # perform online or offline attack - # temperature: 2 - # gamma: 2.0 - # offline_a: 0.33 # parameter from which we compute p(x) from p_OUT(x) such that p_IN(x) = a p_OUT(x) + b. - # offline_b: 0.66 - # qmia: - # training_data_fraction: 1.0 # Fraction of the auxilary dataset (data without train and test indices) to use for training the quantile regressor - # epochs: 5 # Number of training epochs for quantile regression + rmia: + training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) + attack_data_fraction: 0.5 # Fraction of auxiliary dataset to sample from during attack + num_shadow_models: 3 # Number of shadow models to train + online: True # perform online or offline attack + temperature: 2 + gamma: 2.0 + offline_a: 0.33 # parameter from which we compute p(x) from p_OUT(x) such that p_IN(x) = a p_OUT(x) + b. + offline_b: 0.66 population: attack_data_fraction: 1.0 # Fraction of the auxilary dataset to use for this attack - # lira: - # training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) - # num_shadow_models: 3 # Number of shadow models to train - # online: False # perform online or offline attack - # fixed_variance: True # Use a fixed variance for the whole audit - # boosting: True - # loss_traj: - # training_distill_data_fraction : 0.7 # Fraction of the auxilary dataset to use for training the distillation models D_s = (1-D_KD)/2 - # number_of_traj: 10 # Number of epochs (number of points in the loss trajectory) - # label_only: False # True or False - # mia_classifier_epochs: 100 - # HSJ: - # attack_data_fraction: 0.01 # Fraction of the auxilary dataset to use for this attack - # target_metadata_path: "./target/model_metadata.pkl" - # num_iterations: 2 # Number of iterations for the optimization - # initial_num_evals: 100 # Number of evaluations for number of random vecotr to estimate the gradient - # max_num_evals: 10000 # Maximum number of evaluations - # stepsize_search: "geometric_progression" # Step size search method - # gamma: 1.0 # Gamma for the optimization - # constraint: 2 - # batch_size: 50 - # verbose: True - # epsilon_threshold: 1e-6 + lira: + training_data_fraction: 0.5 # Fraction of the auxilary dataset to use for this attack (in each shadow model training) + num_shadow_models: 3 # Number of shadow models to train + online: True # perform online or offline attack + loss_traj: + training_distill_data_fraction : 0.7 # Fraction of the auxilary dataset to use for training the distillation models D_s = (1-D_KD)/2 + number_of_traj: 10 # Number of epochs (number of points in the loss trajectory) + label_only: False # True or False + mia_classifier_epochs: 100 + HSJ: + attack_data_fraction: 0.01 # Fraction of the auxilary dataset to use for this attack + target_metadata_path: "./target/model_metadata.pkl" + num_iterations: 2 # Number of iterations for the optimization + initial_num_evals: 100 # Number of evaluations for number of random vecotr to estimate the gradient + max_num_evals: 10000 # Maximum number of evaluations + stepsize_search: "geometric_progression" # Step size search method + gamma: 1.0 # Gamma for the optimization + constraint: 2 + batch_size: 50 + verbose: True + epsilon_threshold: 1e-6 output_dir: "./leakpro_output" attack_type: "mia" #mia, gia diff --git a/examples/report_handler/report_handler.ipynb b/examples/report_handler/report_handler.ipynb index 725e673e..94cae90a 100644 --- a/examples/report_handler/report_handler.ipynb +++ b/examples/report_handler/report_handler.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "bcf529c7-8bfe-49da-9889-59111ec2cd73", "metadata": {}, "outputs": [], @@ -52,28 +52,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c89f3738", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[Parallel(n_jobs=64)]: Using backend ThreadingBackend with 64 concurrent workers.\n", - "[Parallel(n_jobs=64)]: Done 2 out of 64 | elapsed: 1.2s remaining: 38.1s\n", - "[Parallel(n_jobs=64)]: Done 64 out of 64 | elapsed: 4.4s finished\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Unique predictions (array([-1, 1]), array([ 3, 97]))\n", - "Syn anom shape (3, 14)\n" - ] - } - ], + "outputs": [], "source": [ "syn_anom = return_anomalies(df=syn, n_estimators=1000, n_jobs=-1, verbose=True)\n", "print(\"Syn anom shape\",syn_anom.shape)" @@ -81,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "ad69ece9", "metadata": {}, "outputs": [], @@ -97,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "7d7ffb5a", "metadata": {}, "outputs": [], @@ -114,7 +96,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "0a5c20e2", "metadata": {}, "outputs": [], @@ -148,54 +130,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "35aee5a3", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Files already downloaded and verified\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2024-12-10 15:37:19,407 INFO Inverting gradient initialized.\n", - "2024-12-10 15:37:21,549 INFO Iteration 0, loss 0.00017312286945525557\n", - "2024-12-10 15:37:21,558 INFO New best loss: 0.00017312286945525557 on round: 0\n", - "2024-12-10 15:37:21,716 INFO New best loss: 0.00015130748215597123 on round: 1\n", - "2024-12-10 15:37:21,865 INFO New best loss: 0.00014282172196544707 on round: 2\n", - "2024-12-10 15:37:22,004 INFO New best loss: 0.0001371056423522532 on round: 3\n", - "2024-12-10 15:37:22,146 INFO New best loss: 0.00013112256419844925 on round: 4\n", - "2024-12-10 15:37:22,404 INFO New best loss: 0.00012842075375374407 on round: 6\n", - "2024-12-10 15:37:22,531 INFO New best loss: 0.00012392438657116145 on round: 7\n", - "2024-12-10 15:37:22,661 INFO New best loss: 0.00011872354662045836 on round: 8\n", - "2024-12-10 15:37:22,915 INFO New best loss: 0.00011686021025525406 on round: 10\n", - "2024-12-10 15:37:23,045 INFO New best loss: 0.00011540275590959936 on round: 11\n", - "2024-12-10 15:37:23,176 INFO New best loss: 0.00010958370694424957 on round: 12\n", - "2024-12-10 15:37:23,306 INFO New best loss: 0.00010606442810967565 on round: 13\n", - "2024-12-10 15:37:23,432 INFO New best loss: 0.00010551762534305453 on round: 14\n", - "2024-12-10 15:37:23,558 INFO New best loss: 0.00010321227455278859 on round: 15\n", - "2024-12-10 15:37:23,688 INFO New best loss: 0.00010049617412732914 on round: 16\n", - "2024-12-10 15:37:23,819 INFO New best loss: 9.98983159661293e-05 on round: 17\n", - "2024-12-10 15:37:24,073 INFO New best loss: 9.980545291909948e-05 on round: 19\n", - "2024-12-10 15:37:24,330 INFO New best loss: 9.797140228329226e-05 on round: 21\n", - "2024-12-10 15:37:24,457 INFO New best loss: 9.713656618259847e-05 on round: 22\n", - "2024-12-10 15:37:29,775 INFO New best loss: 9.707298158900812e-05 on round: 63\n", - "2024-12-10 15:37:31,176 INFO New best loss: 9.695763583295047e-05 on round: 75\n", - "2024-12-10 15:37:31,299 INFO New best loss: 9.695166954770684e-05 on round: 76\n", - "2024-12-10 15:37:31,425 INFO New best loss: 9.689731814432889e-05 on round: 77\n", - "/opt/conda/lib/python3.10/site-packages/torchmetrics/utilities/prints.py:70: FutureWarning: Importing `peak_signal_noise_ratio` from `torchmetrics.functional` was deprecated and will be removed in 2.0. Import `peak_signal_noise_ratio` from `torchmetrics.image` instead.\n", - " _future_warning(\n" - ] - } - ], + "outputs": [], "source": [ - "from gia_utils.cifar import get_cifar10_loader\n", - "from gia_utils.model import ResNet\n", + "sys.path.append(\"../gia/cifar10_inverting_1_image/\")\n", + "from cifar import get_cifar10_loader\n", + "from model import ResNet\n", "from torchvision.models.resnet import BasicBlock\n", "\n", "from leakpro.attacks.gia_attacks.invertinggradients import InvertingConfig\n", @@ -226,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "d38d6aa1", "metadata": {}, "outputs": [], @@ -241,19 +183,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "a45a0d6b", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Files already downloaded and verified\n", - "Files already downloaded and verified\n" - ] - } - ], + "outputs": [], "source": [ "from mia_utils.utils.cifar_data_preparation import get_cifar_dataloader\n", "from mia_utils.utils.cifar_model_preparation import ResNet18, create_trained_model_and_metadata\n", @@ -271,22 +204,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "4cda80cf", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", - " warnings.warn(\n", - "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", - " warnings.warn(msg)\n", - "Training Progress: 100%|██████████| 3/3 [00:13<00:00, 4.60s/it]\n" - ] - } - ], + "outputs": [], "source": [ "# Train the model\n", "if not os.path.exists(\"target\"):\n", @@ -315,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "f28eb14f", "metadata": {}, "outputs": [ @@ -323,41 +244,311 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-12-10 15:37:48,180 INFO Target model blueprint created from ResNet18 in ./mia_utils/utils/cifar_model_preparation.py.\n", - "2024-12-10 15:37:48,183 INFO Loaded target model metadata from ./target/model_metadata.pkl\n" + "2024-12-11 13:30:05,252 INFO Target model blueprint created from ResNet18 in ./mia_utils/utils/cifar_model_preparation.py.\n", + "2024-12-11 13:30:05,254 INFO Loaded target model metadata from ./target/model_metadata.pkl\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ - "{'audit': {'random_seed': 1234, 'attack_list': {'population': {'attack_data_fraction': 1.0}}, 'output_dir': './leakpro_output', 'attack_type': 'mia', 'modality': 'image'}, 'target': {'module_path': './mia_utils/utils/cifar_model_preparation.py', 'model_class': 'ResNet18', 'target_folder': './target', 'data_path': './data/cifar10.pkl'}, 'shadow_model': None, 'distillation_model': None}\n" + "{'audit': {'random_seed': 1234, 'attack_list': {'rmia': {'training_data_fraction': 0.5, 'attack_data_fraction': 0.5, 'num_shadow_models': 3, 'online': True, 'temperature': 2, 'gamma': 2.0, 'offline_a': 0.33, 'offline_b': 0.66}, 'population': {'attack_data_fraction': 1.0}, 'lira': {'training_data_fraction': 0.5, 'num_shadow_models': 3, 'online': True}, 'loss_traj': {'training_distill_data_fraction': 0.7, 'number_of_traj': 10, 'label_only': False, 'mia_classifier_epochs': 100}, 'HSJ': {'attack_data_fraction': 0.01, 'target_metadata_path': './target/model_metadata.pkl', 'num_iterations': 2, 'initial_num_evals': 100, 'max_num_evals': 10000, 'stepsize_search': 'geometric_progression', 'gamma': 1.0, 'constraint': 2, 'batch_size': 50, 'verbose': True, 'epsilon_threshold': '1e-6'}}, 'output_dir': './leakpro_output', 'attack_type': 'mia', 'modality': 'image'}, 'target': {'module_path': './mia_utils/utils/cifar_model_preparation.py', 'model_class': 'ResNet18', 'target_folder': './target', 'data_path': './data/cifar10.pkl'}, 'shadow_model': None, 'distillation_model': None}\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "2024-12-10 15:37:48,394 INFO Loaded target model from ./target\n", - "2024-12-10 15:37:49,289 INFO Loaded population dataset from ./data/cifar10.pkl\n", - "2024-12-10 15:37:49,290 INFO Loaded population dataset from ./data/cifar10.pkl\n", - "2024-12-10 15:37:49,291 INFO Creating shadow model handler singleton\n", - "2024-12-10 15:37:49,294 INFO Creating distillation model handler singleton\n", - "2024-12-10 15:37:49,296 INFO Configuring the Population attack\n", - "2024-12-10 15:37:49,297 INFO Added attack: population\n", - "2024-12-10 15:37:49,298 INFO Preparing attack: population\n", - "2024-12-10 15:37:49,299 INFO Preparing attack data for training the Population attack\n", - "2024-12-10 15:37:49,306 INFO Subsampling attack data from 24000 points\n", - "2024-12-10 15:37:49,307 INFO Number of attack data points after subsampling: 24000\n", - "2024-12-10 15:37:49,308 INFO Computing signals for the Population attack\n", - "Getting loss for model 1/ 1: 100%|██████████| 750/750 [00:12<00:00, 60.59it/s]\n", - "2024-12-10 15:38:01,752 INFO Running attack: population\n", - "2024-12-10 15:38:01,758 INFO Running the Population attack on the target model\n", - "Getting loss for model 1/ 1: 100%|██████████| 1125/1125 [00:17<00:00, 63.04it/s]\n", - "2024-12-10 15:38:19,692 INFO Attack completed\n", - "2024-12-10 15:38:19,703 INFO Finished attack: population\n", - "2024-12-10 15:38:19,703 INFO Preparing results for attack: population\n", - "2024-12-10 15:38:19,704 INFO Auditing completed\n" + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n", + "2024-12-11 13:30:05,518 INFO Loaded target model from ./target\n", + "2024-12-11 13:30:06,417 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-12-11 13:30:06,419 INFO Loaded population dataset from ./data/cifar10.pkl\n", + "2024-12-11 13:30:06,420 INFO Creating shadow model handler singleton\n", + "2024-12-11 13:30:06,423 INFO Creating distillation model handler singleton\n", + "2024-12-11 13:30:06,425 INFO Configuring RMIA attack\n", + "2024-12-11 13:30:06,426 INFO Added attack: rmia\n", + "2024-12-11 13:30:06,427 INFO Configuring the Population attack\n", + "2024-12-11 13:30:06,428 INFO Added attack: population\n", + "2024-12-11 13:30:06,429 INFO Added attack: lira\n", + "2024-12-11 13:30:06,430 INFO Configuring Loss trajecatory attack\n", + "2024-12-11 13:30:06,434 INFO Added attack: loss_traj\n", + "2024-12-11 13:30:06,436 INFO Configuring label only attack\n", + "2024-12-11 13:30:06,438 INFO Added attack: HSJ\n", + "2024-12-11 13:30:06,439 INFO Preparing attack: rmia\n", + "2024-12-11 13:30:06,440 INFO Preparing shadow models for RMIA attack\n", + "2024-12-11 13:30:06,441 INFO Preparing attack data for training the RMIA attack\n", + "2024-12-11 13:30:06,447 INFO Check for 3 shadow models (dataset: 60000 points)\n", + "2024-12-11 13:30:07,605 INFO Number of existing models exceeds or equals the number of models to create\n", + "2024-12-11 13:30:07,607 INFO Loading shadow model 2\n", + "2024-12-11 13:30:07,832 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_2.pkl\n", + "2024-12-11 13:30:07,833 INFO Loading shadow model 1\n", + "2024-12-11 13:30:08,059 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_1.pkl\n", + "2024-12-11 13:30:08,060 INFO Loading shadow model 0\n", + "2024-12-11 13:30:08,274 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_0.pkl\n", + "2024-12-11 13:30:08,279 INFO Running attack: rmia\n", + "2024-12-11 13:30:08,280 INFO Running RMIA online attack\n", + "2024-12-11 13:30:08,281 INFO Loading metadata 2\n", + "2024-12-11 13:30:08,285 INFO Loading metadata 1\n", + "2024-12-11 13:30:08,287 INFO Loading metadata 0\n", + "2024-12-11 13:30:08,881 INFO Number of points in the audit dataset that are used for online attack: 26907\n", + "2024-12-11 13:31:51,245 INFO Subsampling attack data from 24000 points \n", + "2024-12-11 13:31:51,249 INFO Number of attack data points after subsampling: 12000\n", + "2024-12-11 13:32:41,414 INFO Finished attack: rmia \n", + "2024-12-11 13:32:41,416 INFO Preparing attack: population\n", + "2024-12-11 13:32:41,417 INFO Preparing attack data for training the Population attack\n", + "2024-12-11 13:32:41,423 INFO Subsampling attack data from 24000 points\n", + "2024-12-11 13:32:41,425 INFO Number of attack data points after subsampling: 24000\n", + "2024-12-11 13:32:41,426 INFO Computing signals for the Population attack\n", + "Getting loss for model 1/ 1: 100%|██████████| 750/750 [00:13<00:00, 57.54it/s]\n", + "2024-12-11 13:32:54,560 INFO Running attack: population\n", + "2024-12-11 13:32:54,565 INFO Running the Population attack on the target model\n", + "Getting loss for model 1/ 1: 100%|██████████| 1125/1125 [00:17<00:00, 64.57it/s]\n", + "2024-12-11 13:33:12,093 INFO Attack completed\n", + "2024-12-11 13:33:12,105 INFO Finished attack: population\n", + "2024-12-11 13:33:12,106 INFO Preparing attack: lira\n", + "2024-12-11 13:33:12,131 INFO Number of existing models exceeds or equals the number of models to create\n", + "2024-12-11 13:33:12,132 INFO Loading shadow model 2\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n", + "2024-12-11 13:33:12,340 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_2.pkl\n", + "2024-12-11 13:33:12,341 INFO Loading shadow model 1\n", + "2024-12-11 13:33:12,543 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_1.pkl\n", + "2024-12-11 13:33:12,544 INFO Loading shadow model 0\n", + "2024-12-11 13:33:12,745 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_0.pkl\n", + "2024-12-11 13:33:12,748 INFO Create masks for all IN and OUT samples\n", + "2024-12-11 13:33:12,749 INFO Loading metadata 2\n", + "2024-12-11 13:33:12,750 INFO Loading metadata 1\n", + "2024-12-11 13:33:12,752 INFO Loading metadata 0\n", + "2024-12-11 13:33:12,761 INFO Calculating the logits for all 3 shadow models\n", + "2024-12-11 13:34:25,365 INFO Calculating the logits for the target model \n", + "2024-12-11 13:34:51,520 INFO Running attack: lira \n", + "Processing audit samples: 100%|██████████| 26907/26907 [00:06<00:00, 4483.96it/s]\n", + "2024-12-11 13:34:57,628 INFO Finished attack: lira\n", + "2024-12-11 13:34:57,630 INFO Preparing attack: loss_traj\n", + "2024-12-11 13:34:57,631 INFO Preparing the data for loss trajectory attack\n", + "2024-12-11 13:34:57,640 INFO Training shadow models on 3600 points\n", + "2024-12-11 13:34:57,647 INFO Number of existing models exceeds or equals the number of models to create\n", + "2024-12-11 13:34:57,648 INFO Loading shadow model 6\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:208: UserWarning: The parameter 'pretrained' is deprecated since 0.13 and may be removed in the future, please use 'weights' instead.\n", + " warnings.warn(\n", + "/opt/conda/lib/python3.10/site-packages/torchvision/models/_utils.py:223: UserWarning: Arguments other than a weight enum or `None` for 'weights' are deprecated since 0.13 and may be removed in the future. The current behavior is equivalent to passing `weights=None`.\n", + " warnings.warn(msg)\n", + "2024-12-11 13:34:57,858 INFO Loaded model from ./leakpro_output/attack_objects/shadow_model/shadow_model_6.pkl\n", + "2024-12-11 13:34:58,208 INFO Training distillation of the shadow model on 21147 points\n", + "2024-12-11 13:34:58,276 INFO Created distillation dataset with size 21147\n", + "Epoch 1/10: 100%|██████████| 166/166 [00:07<00:00, 21.51it/s]\n", + "2024-12-11 13:35:05,996 INFO Epoch 1/10 | Loss: 63.67065370082855\n", + "2024-12-11 13:35:06,119 INFO Saved distillation model for epoch 0 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:06,121 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:06,126 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 2/10: 100%|██████████| 166/166 [00:07<00:00, 21.17it/s]\n", + "2024-12-11 13:35:13,972 INFO Epoch 2/10 | Loss: 9.804248925298452\n", + "2024-12-11 13:35:14,085 INFO Saved distillation model for epoch 1 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:14,087 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:14,091 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 3/10: 100%|██████████| 166/166 [00:07<00:00, 20.81it/s]\n", + "2024-12-11 13:35:22,074 INFO Epoch 3/10 | Loss: 5.905309362336993\n", + "2024-12-11 13:35:22,189 INFO Saved distillation model for epoch 2 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:22,191 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:22,194 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 4/10: 100%|██████████| 166/166 [00:08<00:00, 19.90it/s]\n", + "2024-12-11 13:35:30,539 INFO Epoch 4/10 | Loss: 4.59359712805599\n", + "2024-12-11 13:35:30,672 INFO Saved distillation model for epoch 3 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:30,674 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:30,678 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 5/10: 100%|██████████| 166/166 [00:08<00:00, 19.77it/s]\n", + "2024-12-11 13:35:39,080 INFO Epoch 5/10 | Loss: 4.146416590549052\n", + "2024-12-11 13:35:39,197 INFO Saved distillation model for epoch 4 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:39,198 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:39,202 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 6/10: 100%|██████████| 166/166 [00:08<00:00, 19.94it/s]\n", + "2024-12-11 13:35:47,532 INFO Epoch 6/10 | Loss: 3.597899131476879\n", + "2024-12-11 13:35:47,654 INFO Saved distillation model for epoch 5 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:47,656 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:47,660 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 7/10: 100%|██████████| 166/166 [00:07<00:00, 20.79it/s]\n", + "2024-12-11 13:35:55,650 INFO Epoch 7/10 | Loss: 3.298955911770463\n", + "2024-12-11 13:35:55,765 INFO Saved distillation model for epoch 6 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:35:55,767 INFO Storing metadata for distillation model\n", + "2024-12-11 13:35:55,771 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 8/10: 100%|██████████| 166/166 [00:07<00:00, 21.17it/s]\n", + "2024-12-11 13:36:03,616 INFO Epoch 8/10 | Loss: 3.090168266557157\n", + "2024-12-11 13:36:03,730 INFO Saved distillation model for epoch 7 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:03,731 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:03,736 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 9/10: 100%|██████████| 166/166 [00:07<00:00, 21.07it/s]\n", + "2024-12-11 13:36:11,618 INFO Epoch 9/10 | Loss: 2.8429356180131435\n", + "2024-12-11 13:36:11,731 INFO Saved distillation model for epoch 8 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:11,732 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:11,736 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 10/10: 100%|██████████| 166/166 [00:07<00:00, 21.02it/s]\n", + "2024-12-11 13:36:19,639 INFO Epoch 10/10 | Loss: 2.593701013363898\n", + "2024-12-11 13:36:19,759 INFO Saved distillation model for epoch 9 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:19,760 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:19,764 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:19,914 INFO Created distillation dataset with size 21147\n", + "Epoch 1/10: 100%|██████████| 166/166 [00:07<00:00, 21.29it/s]\n", + "2024-12-11 13:36:27,717 INFO Epoch 1/10 | Loss: 114.02811643481255\n", + "2024-12-11 13:36:27,829 INFO Saved distillation model for epoch 0 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:27,831 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:27,837 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 2/10: 100%|██████████| 166/166 [00:07<00:00, 20.76it/s]\n", + "2024-12-11 13:36:35,838 INFO Epoch 2/10 | Loss: 44.576862797141075\n", + "2024-12-11 13:36:35,950 INFO Saved distillation model for epoch 1 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:35,951 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:35,956 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 3/10: 100%|██████████| 166/166 [00:07<00:00, 20.76it/s]\n", + "2024-12-11 13:36:43,958 INFO Epoch 3/10 | Loss: 28.83185875415802\n", + "2024-12-11 13:36:44,077 INFO Saved distillation model for epoch 2 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:44,079 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:44,083 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 4/10: 100%|██████████| 166/166 [00:08<00:00, 20.59it/s]\n", + "2024-12-11 13:36:52,150 INFO Epoch 4/10 | Loss: 21.28936092555523\n", + "2024-12-11 13:36:52,270 INFO Saved distillation model for epoch 3 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:36:52,272 INFO Storing metadata for distillation model\n", + "2024-12-11 13:36:52,277 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 5/10: 100%|██████████| 166/166 [00:08<00:00, 20.58it/s]\n", + "2024-12-11 13:37:00,346 INFO Epoch 5/10 | Loss: 16.496566824615\n", + "2024-12-11 13:37:00,457 INFO Saved distillation model for epoch 4 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:00,459 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:00,463 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 6/10: 100%|██████████| 166/166 [00:08<00:00, 20.54it/s]\n", + "2024-12-11 13:37:08,548 INFO Epoch 6/10 | Loss: 13.477496419101954\n", + "2024-12-11 13:37:08,660 INFO Saved distillation model for epoch 5 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:08,662 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:08,666 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 7/10: 100%|██████████| 166/166 [00:08<00:00, 20.69it/s]\n", + "2024-12-11 13:37:16,693 INFO Epoch 7/10 | Loss: 11.912189535796642\n", + "2024-12-11 13:37:16,812 INFO Saved distillation model for epoch 6 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:16,814 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:16,818 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 8/10: 100%|██████████| 166/166 [00:08<00:00, 20.62it/s]\n", + "2024-12-11 13:37:24,872 INFO Epoch 8/10 | Loss: 11.11345386132598\n", + "2024-12-11 13:37:24,991 INFO Saved distillation model for epoch 7 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:24,992 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:24,997 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 9/10: 100%|██████████| 166/166 [00:08<00:00, 20.44it/s]\n", + "2024-12-11 13:37:33,121 INFO Epoch 9/10 | Loss: 10.197872441262007\n", + "2024-12-11 13:37:33,241 INFO Saved distillation model for epoch 8 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:33,242 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:33,247 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "Epoch 10/10: 100%|██████████| 166/166 [00:08<00:00, 20.26it/s]\n", + "2024-12-11 13:37:41,445 INFO Epoch 10/10 | Loss: 9.438245516270399\n", + "2024-12-11 13:37:41,565 INFO Saved distillation model for epoch 9 to ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:41,566 INFO Storing metadata for distillation model\n", + "2024-12-11 13:37:41,570 INFO Metadata for distillation model stored in ./leakpro_output/attack_objects/distillation_model\n", + "2024-12-11 13:37:41,595 INFO Loading MIA trajectory_train_data.pkl: 7200 points\n", + "2024-12-11 13:37:41,602 INFO Loading MIA trajectory_test_data.pkl: 36000 points\n", + "2024-12-11 13:37:41,610 INFO Running attack: loss_traj\n", + "2024-12-11 13:37:41,614 INFO Loading Loss Trajectory classifier\n", + "2024-12-11 13:37:41,615 INFO Running the MIA attack\n", + "100%|██████████| 563/563 [00:00<00:00, 823.90it/s]\n", + "2024-12-11 13:37:42,932 INFO Finished attack: loss_traj\n", + "2024-12-11 13:37:42,933 INFO Preparing attack: HSJ\n", + "2024-12-11 13:37:42,934 INFO Preparing the data for Hop Skip Jump attack\n", + "2024-12-11 13:37:42,938 INFO Running attack: HSJ\n", + "2024-12-11 13:37:42,939 INFO Running Hop Skip Jump distance attack\n", + "Epoch: 100%|██████████| 8/8 [00:00<00:00, 131.66it/s]\n", + "2024-12-11 13:37:44,287 INFO All data points in the batch have been successfully perturbed by random noise after 20 evaluations.\n", + "Batch: 0%| | 0/8 [00:00" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -434,9 +597,6 @@ } ], "source": [ - "import sys\n", - "sys.path.append(\"../..\")\n", - "\n", "# Import and initialize ReportHandler\n", "from leakpro.reporting.report_handler import ReportHandler\n", "\n", @@ -445,13 +605,13 @@ "# existing results folder\n", "report_handler = ReportHandler(report_dir=\"./leakpro_output/results\")\n", "\n", - "# # Save Synthetic results using the ReportHandler\n", + "# Save Synthetic results using the ReportHandler\n", "report_handler.save_results(attack_name=\"singling_out\", result_data=sin_out_res)\n", "report_handler.save_results(attack_name=\"linkability_risk\", result_data=link_res)\n", "report_handler.save_results(attack_name=\"inference_risk_base\", result_data=inf_res)\n", "report_handler.save_results(attack_name=\"inference_risk_worst\", result_data=inf_res_worst)\n", "\n", - "# # Save GIA results using report handler\n", + "# Save GIA results using report handler\n", "report_handler.save_results(attack_name=\"gia\", result_data=GIA_result)\n", "\n", "# Save MIA resuls using report handler\n", @@ -461,7 +621,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 3, "id": "1d91c7e0", "metadata": {}, "outputs": [ @@ -469,13 +629,13 @@ "name": "stderr", "output_type": "stream", "text": [ - "2024-12-10 15:38:59,171 INFO PDF compiled\n" + "2024-12-11 13:45:59,879 INFO PDF compiled\n" ] }, { "data": { "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -484,7 +644,7 @@ { "data": { "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -493,7 +653,7 @@ { "data": { "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -502,7 +662,7 @@ { "data": { "text/plain": [ - "
" + "
" ] }, "metadata": {}, @@ -511,7 +671,7 @@ { "data": { "text/plain": [ - "
" + "
" ] }, "metadata": {}, diff --git a/leakpro/metrics/attack_result.py b/leakpro/metrics/attack_result.py index cf5a7cd6..27816388 100755 --- a/leakpro/metrics/attack_result.py +++ b/leakpro/metrics/attack_result.py @@ -276,7 +276,7 @@ def save(self:Self, path: str, name: str, config:dict = None, show_plot:bool = F # Get the name for the attack configuration config_name = get_config_name(result_config) - self.id = f"{name}{config_name}" + self.id = f"{name}{config_name}".replace("/", "__") save_path = f"{path}/{name}/{self.id}" # Data to be saved