diff --git a/.codecov.yml b/.codecov.yml index 247dbfbb..a2387d52 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,9 +1,9 @@ ---- # configuration for https://codecov.io ignore: - - "setup.py" - - "aisdc/safemodel/classifiers/new_model_template.py" + - "aisdc/config" + - "aisdc/main.py" - "aisdc/preprocessing" - - "user_stories" + - "aisdc/safemodel/classifiers/new_model_template.py" - "examples" -... + - "setup.py" + - "user_stories" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index db96210b..cfbf4db3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,11 @@ jobs: with: python-version: ${{ matrix.python-version }} + # xgboost requires libomp on macOS + - name: Install dependencies on macOS + if: runner.os == 'macOS' + run: brew install libomp + - name: Install run: pip install .[test] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2505706f..f20f9c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## Version 1.2.0 (under development) + Changes: * Add support for scikit-learn MLPClassifier ([#276](https://github.com/AI-SDC/AI-SDC/pull/276)) * Use default XGBoost params if not defined in structural attacks ([#277](https://github.com/AI-SDC/AI-SDC/pull/277)) @@ -7,6 +9,7 @@ Changes: * Clean up repository and update packaging ([#283](https://github.com/AI-SDC/AI-SDC/pull/283)) * Format docstrings ([#286](https://github.com/AI-SDC/AI-SDC/pull/286)) * Refactor ([#284](https://github.com/AI-SDC/AI-SDC/pull/284), [#285](https://github.com/AI-SDC/AI-SDC/pull/285), [#287](https://github.com/AI-SDC/AI-SDC/pull/287)) +* Add CLI and tools for generating configs; significant refactor ([#291](https://github.com/AI-SDC/AI-SDC/pull/291)) ## Version 1.1.3 (Apr 26, 2024) diff --git a/README.md b/README.md index effc4fc7..6455fc32 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ The `aisdc` package provides: * A variety of privacy attacks for assessing machine learning models. * The safemodel package: a suite of open source wrappers for common machine learning frameworks, including [scikit-learn](https://scikit-learn.org) and [Keras](https://keras.io). It is designed for use by researchers in Trusted Research Environments (TREs) where disclosure control methods must be implemented. Safemodel aims to give researchers greater confidence that their models are more compliant with disclosure control. -A collection of user guides can be found in the [`user_stories`](user_stories) folder of this repository. These guides include configurable examples from the perspective of both a researcher and a TRE, with separate scripts for each. Instructions on how to use each of these scripts and which scripts to use are included in the README located in the folder. - ## Installation [![PyPI package](https://img.shields.io/pypi/v/aisdc.svg)](https://pypi.org/project/aisdc) @@ -32,14 +30,15 @@ To additionally install the safemodel package: $ pip install aisdc[safemodel] ``` -## Running - -To run an example, simply execute the desired script. For example, to run LiRA: - +Note: macOS users may need to install libomp due to a dependency on XGBoost: ``` -$ python -m lira_attack_example +$ brew install libomp ``` +## Running + +See the [`examples`](examples/). + ## Acknowledgement This work was funded by UK Research and Innovation under Grant Numbers MC_PC_21033 and MC_PC_23006 as part of Phase 1 of the [DARE UK](https://dareuk.org.uk) (Data and Analytics Research Environments UK) programme, delivered in partnership with Health Data Research UK (HDR UK) and Administrative Data Research UK (ADR UK). The specific projects were Semi-Automatic checking of Research Outputs (SACRO; MC_PC_23006) and Guidelines and Resources for AI Model Access from TrusTEd Research environments (GRAIMATTER; MC_PC_21033).­This project has also been supported by MRC and EPSRC [grant number MR/S010351/1]: PICTURES. diff --git a/aisdc/attacks/attack.py b/aisdc/attacks/attack.py index ece859ba..720f4e3a 100644 --- a/aisdc/attacks/attack.py +++ b/aisdc/attacks/attack.py @@ -1,32 +1,90 @@ """Base class for an attack object.""" +from __future__ import annotations + +import importlib import inspect -import json +import logging +import os +import uuid +from datetime import datetime + +from fpdf import FPDF +from aisdc.attacks import report from aisdc.attacks.target import Target +logger = logging.getLogger(__name__) + class Attack: - """Base (abstract) class to represent an attack.""" + """Base class to represent an attack.""" - def __init__(self) -> None: - self.attack_config_json_file_name = None + def __init__(self, output_dir: str = "outputs", write_report: bool = True) -> None: + """Instantiate an attack. - def attack(self, target: Target) -> None: + Parameters + ---------- + output_dir : str + name of the directory where outputs are stored + write_report : bool + Whether to generate a JSON and PDF report. + """ + self.output_dir: str = output_dir + self.write_report: bool = write_report + self.attack_metrics: dict | list = {} + self.metadata: dict = {} + if not os.path.exists(self.output_dir): + os.makedirs(self.output_dir) + + def attack(self, target: Target) -> dict: """Run an attack.""" raise NotImplementedError + def _construct_metadata(self) -> None: + """Generate attack metadata.""" + self.metadata = { + "attack_name": str(self), + "attack_params": self.get_params(), + "global_metrics": {}, + } + + def _get_attack_metrics_instances(self) -> dict: + """Get metrics for each individual repetition of an attack.""" + raise NotImplementedError # pragma: no cover + + def _make_pdf(self, output: dict) -> FPDF | None: + """Create PDF report.""" + raise NotImplementedError # pragma: no cover + + def _make_report(self, target: Target) -> dict: + """Create attack report.""" + logger.info("Generating report") + self._construct_metadata() + self.metadata["target_model"] = target.model_name + self.metadata["target_model_params"] = target.model_params + output: dict = { + "log_id": str(uuid.uuid4()), + "log_time": datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + "metadata": self.metadata, + "attack_experiment_logger": self._get_attack_metrics_instances(), + } + return output + + def _write_report(self, output: dict) -> None: + """Write report as JSON and PDF.""" + dest: str = os.path.join(self.output_dir, "report") + if self.write_report: + logger.info("Writing report: %s.json %s.pdf", dest, dest) + report.write_json(output, dest) + pdf_report = self._make_pdf(output) + if pdf_report is not None: + report.write_pdf(dest, pdf_report) + def __str__(self) -> str: """Return the string representation of an attack.""" raise NotImplementedError - def _update_params_from_config_file(self) -> None: - """Read a configuration file and load it into a dictionary object.""" - with open(self.attack_config_json_file_name, encoding="utf-8") as f: - config = json.loads(f.read()) - for key, value in config.items(): - setattr(self, key, value) - @classmethod def _get_param_names(cls) -> list[str]: """Get parameter names.""" @@ -49,3 +107,10 @@ def get_params(self) -> dict: for key in self._get_param_names(): out[key] = getattr(self, key) return out + + +def get_class_by_name(class_path: str): + """Return a class given its name.""" + module_path, class_name = class_path.rsplit(".", 1) + module = importlib.import_module(module_path) + return getattr(module, class_name) diff --git a/aisdc/attacks/attack_report_formatter.py b/aisdc/attacks/attack_report_formatter.py index 64f359a2..859dadc5 100644 --- a/aisdc/attacks/attack_report_formatter.py +++ b/aisdc/attacks/attack_report_formatter.py @@ -11,6 +11,7 @@ import matplotlib.pyplot as plt import numpy as np +import yaml def cleanup_files_for_release( @@ -73,7 +74,7 @@ def add_attack_output(self, incoming_json: dict, class_name: str) -> None: class_name = class_name + "_" + str(incoming_json["log_id"]) file_data[class_name] = incoming_json - json.dump(file_data, f) + json.dump(file_data, f, indent=4) def get_output_filename(self) -> str: """Return the filename of the JSON file which has been created.""" @@ -447,7 +448,7 @@ class GenerateTextReport: def __init__(self) -> None: self.text_out = [] - self.target_json_filename = None + self.target_yaml_filename = None self.attack_json_filename = None self.model_name_from_target = None @@ -455,8 +456,8 @@ def __init__(self) -> None: self.support_rejection = [] self.support_release = [] - def _process_target_json(self) -> None: - """Create a summary of a target model JSON file.""" + def _process_target_yaml(self) -> None: + """Create a summary of a target model YAML file.""" model_params_of_interest = [ "C", "kernel", @@ -470,33 +471,33 @@ def _process_target_json(self) -> None: "learning_rate", ] - with open(self.target_json_filename, encoding="utf-8") as f: - json_report = json.loads(f.read()) + with open(self.target_yaml_filename, encoding="utf-8") as f: + yaml_report = yaml.safe_load(f) output_string = "TARGET MODEL SUMMARY\n" - if "model_name" in json_report: + if "model_name" in yaml_report: output_string = ( - output_string + "model_name: " + json_report["model_name"] + "\n" + output_string + "model_name: " + yaml_report["model_name"] + "\n" ) - if "n_samples" in json_report: + if "n_samples" in yaml_report: output_string = output_string + "number of samples used to train: " - output_string = output_string + str(json_report["n_samples"]) + "\n" + output_string = output_string + str(yaml_report["n_samples"]) + "\n" - if "model_params" in json_report: + if "model_params" in yaml_report: for param in model_params_of_interest: - if param in json_report["model_params"]: + if param in yaml_report["model_params"]: output_string = output_string + param + ": " output_string = output_string + str( - json_report["model_params"][param] + yaml_report["model_params"][param] ) output_string = output_string + "\n" - if "model_path" in json_report: - filepath = os.path.split(os.path.abspath(self.target_json_filename))[0] + if "model_path" in yaml_report: + filepath = os.path.split(os.path.abspath(self.target_yaml_filename))[0] self.model_name_from_target = os.path.join( - filepath, json_report["model_path"] + filepath, yaml_report["model_path"] ) self.text_out.append(output_string) @@ -519,10 +520,10 @@ def process_attack_target_json( json_report = json.loads(f.read()) if target_filename is not None: - self.target_json_filename = target_filename + self.target_yaml_filename = target_filename with open(target_filename, encoding="utf-8") as f: - target_file = json.loads(f.read()) + target_file = yaml.safe_load(f) json_report = {**json_report, **target_file} modules = [ @@ -576,8 +577,8 @@ def export_to_file( # pylint: disable=too-many-arguments copy_of_text_out = self.text_out self.text_out = [] - if self.target_json_filename is not None: - self._process_target_json() + if self.target_yaml_filename is not None: + self._process_target_yaml() self.text_out += copy_of_text_out @@ -594,7 +595,7 @@ def export_to_file( # pylint: disable=too-many-arguments copy_into_release = [ output_filename, self.attack_json_filename, - self.target_json_filename, + self.target_yaml_filename, ] if model_filename is None: diff --git a/aisdc/attacks/attribute_attack.py b/aisdc/attacks/attribute_attack.py index a74b05a1..3f4ec6e3 100644 --- a/aisdc/attacks/attribute_attack.py +++ b/aisdc/attacks/attribute_attack.py @@ -2,12 +2,8 @@ from __future__ import annotations -import argparse -import json import logging import os -import uuid -from datetime import datetime import matplotlib.pyplot as plt import multiprocess as mp @@ -18,11 +14,10 @@ from aisdc.attacks import report from aisdc.attacks.attack import Attack -from aisdc.attacks.attack_report_formatter import GenerateJSONModule from aisdc.attacks.target import Target logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("aia") +logger = logging.getLogger(__name__) COLOR_A: str = "#86bf91" # training set plot colour COLOR_B: str = "steelblue" # testing set plot colour @@ -31,13 +26,11 @@ class AttributeAttack(Attack): """Attribute inference attack.""" - def __init__( # pylint: disable = too-many-arguments + def __init__( self, - output_dir: str = "output_attribute", - report_name: str = "aia_report", + output_dir: str = "outputs", + write_report: bool = True, n_cpu: int = max(1, mp.cpu_count() - 1), - attack_config_json_file_name: str = None, - target_path: str = None, ) -> None: """Construct an object to execute an attribute inference attack. @@ -47,31 +40,17 @@ def __init__( # pylint: disable = too-many-arguments number of CPUs used to run the attack output_dir : str name of the directory where outputs are stored - report_name : str - name of the pdf and json output reports - attack_config_json_file_name : str - name of the configuration file to load parameters - target_path : str - path to the saved trained target model and target data + write_report : bool + Whether to generate a JSON and PDF report. """ - super().__init__() + super().__init__(output_dir=output_dir, write_report=write_report) self.n_cpu = n_cpu - self.output_dir = output_dir - self.report_name = report_name - self.attack_config_json_file_name = attack_config_json_file_name - self.target_path = target_path - if self.attack_config_json_file_name is not None: - self._update_params_from_config_file() - if not os.path.exists(self.output_dir): - os.makedirs(self.output_dir) - self.attack_metrics: dict = {} - self.metadata: dict = {} def __str__(self) -> str: """Return the name of the attack.""" return "Attribute inference attack" - def attack(self, target: Target) -> None: + def attack(self, target: Target) -> dict: """Run attribute inference attack. To be used when code has access to Target class and trained target model. @@ -80,52 +59,21 @@ def attack(self, target: Target) -> None: ---------- target : attacks.target.Target target is a Target class object - """ - self.attack_metrics = _attribute_inference(target, self.n_cpu) - - def _construct_metadata(self) -> None: - """Construct the metadata object.""" - self.metadata = {} - self.metadata["experiment_details"] = {} - self.metadata["experiment_details"] = self.get_params() - self.metadata["attack"] = str(self) - - def make_report(self) -> dict: - """Create the report. - - Creates the output report. If self.report_name is not None, it will - also save the information in json and pdf formats. Returns ------- - output : dict - Dictionary containing all attack output. + dict + Attack report. """ - output = {} - report_dest = os.path.join(self.output_dir, self.report_name) - logger.info( - "Starting reports, pdf report name = %s, json report name = %s", - report_dest + ".pdf", - report_dest + ".json", - ) - output["log_id"] = str(uuid.uuid4()) - output["log_time"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - self._construct_metadata() - output["metadata"] = self.metadata - output["attack_experiment_logger"] = self._get_attack_metrics_instances() - - json_attack_formatter = GenerateJSONModule(report_dest + ".json") - json_report = json.dumps(output, cls=report.NumpyArrayEncoder) - json_attack_formatter.add_attack_output(json_report, "AttributeAttack") - - pdf_report = create_aia_report(output, report_dest) - report.add_output_to_pdf(report_dest, pdf_report, "AttributeAttack") - logger.info( - "Wrote pdf report to %s and json report to %s", - report_dest + ".pdf", - report_dest + ".json", - ) - return output + if target.n_features < 1: + logger.info("Can't run attribute inference unless features are defined.") + else: + logger.info("Running attribute inference attack") + self.attack_metrics = _attribute_inference(target, self.n_cpu) + output = self._make_report(target) + self._write_report(output) + return output + return {} def _get_attack_metrics_instances(self) -> dict: """Construct the instances metric calculated, during attacks.""" @@ -135,6 +83,51 @@ def _get_attack_metrics_instances(self) -> dict: attack_metrics_experiment["attack_instance_logger"] = attack_metrics_instances return attack_metrics_experiment + def _make_pdf(self, output: dict) -> FPDF: + """Create PDF report.""" + metadata: dict = output["metadata"] + metrics: dict = output["attack_experiment_logger"]["attack_instance_logger"][ + "instance_0" + ] + path: str = metadata["attack_params"]["output_dir"] + # Create PDF + pdf = FPDF() + pdf.add_page() + pdf.set_xy(0, 0) + report.title(pdf, "Attribute Inference Attack Report") + report.subtitle(pdf, "Introduction") + # Add attack parameters + report.subtitle(pdf, "Metadata") + for key, value in metadata["attack_params"].items(): + report.line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") + # Add attack results + report.subtitle(pdf, "Metrics") + # Categorical + categ_rep: list[str] = report_categorical(metrics).split("\n") + if len(categ_rep) > 1: + report.line(pdf, "Categorical Features:", font="courier") + for line in categ_rep: + report.line(pdf, line, font="courier") + # Quantatitive + quant_rep: list[str] = report_quantitative(metrics).split("\n") + if len(quant_rep) > 1: + report.line(pdf, "Quantitative Features:", font="courier") + for line in quant_rep: + report.line(pdf, line, font="courier") + # Add plots + pdf.add_page() + report.subtitle(pdf, "Plots") + plot_categorical_risk(metrics, path) # Create pngs + plot_categorical_fraction(metrics, path) + plot_quantitative_risk(metrics, path) + graphs = ["cat_risk.png", "cat_frac.png", "quant_risk.png"] + for graph in graphs: + filename = os.path.join(path, graph) + if os.path.exists(filename): + pdf.image(filename, x=None, y=None, w=150, h=0, type="", link="") + os.remove(filename) + return pdf + def _unique_max(confidences: list[float], threshold: float) -> bool: """Return if there is a unique maximum confidence value above threshold.""" @@ -157,13 +150,10 @@ def _get_inference_data( # pylint: disable=too-many-locals indices: list[int] = attack_feature["indices"] unique = np.unique(target.X_orig[:, feature_id]) n_unique: int = len(unique) + values = unique if attack_feature["encoding"] == "onehot": onehot_enc = OneHotEncoder() values = onehot_enc.fit_transform(unique.reshape(-1, 1)).toarray() - else: # pragma: no cover - # catch all, but can't be reached because this func only called via _infer - # which is only called for categorical data - values = unique # samples after encoding (e.g. one-hot) samples: np.ndarray = target.X_train # samples before encoding (e.g. str) @@ -200,7 +190,7 @@ def _infer( # pylint: disable=too-many-locals for the original sample, and the highest confidence score is unique, infer that attribute if the confidence score is greater than a threshold. """ - logger.debug("Commencing attack on feature %d set %d", feature_id, int(memberset)) + logger.debug("Attacking feature %d set %d", feature_id, int(memberset)) correct: int = 0 # number of correct inferences made total: int = 0 # total number of inferences made x_values, y_values, baseline = _get_inference_data(target, feature_id, memberset) @@ -246,7 +236,6 @@ def report_categorical(results: dict) -> str: f"baseline: {baseline:.2f}%\n" ) else: # pragma: no cover - # no examples with test dataset where this doesn't happen msg += f"Unable to make any inferences of the {tranche} set\n" return msg @@ -264,12 +253,20 @@ def report_quantitative(results: dict) -> str: return msg -def plot_quantitative_risk(res: dict, savefile: str = "") -> None: - """Generate a bar chart showing quantitative value risk scores.""" - logger.debug("Plotting quantitative feature risk scores") +def plot_quantitative_risk(res: dict, path: str = "") -> None: + """Generate a bar chart showing quantitative value risk scores. + + Parameters + ---------- + res : dict + Dictionary containing attribute inference attack results. + path : str + Directory to write plots. + """ results = res["quantitative"] if len(results) < 1: # pragma: no cover return + logger.debug("Plotting quantitative feature risk scores") x = np.arange(len(results)) ya = [] yb = [] @@ -292,21 +289,27 @@ def plot_quantitative_risk(res: dict, savefile: str = "") -> None: ax.legend(loc="best") plt.margins(y=0) plt.tight_layout() - if savefile != "": - fig.savefig(savefile + "_quant_risk.png", pad_inches=0, bbox_inches="tight") - logger.debug("Saved quantitative risk plot: %s", savefile) - else: # pragma: no cover - plt.show() + filename = os.path.join(path, "quant_risk.png") + fig.savefig(filename, pad_inches=0, bbox_inches="tight") + logger.debug("Saved quantitative risk plot: %s", filename) def plot_categorical_risk( # pylint: disable=too-many-locals - res: dict, savefile: str = "" + res: dict, path: str = "" ) -> None: - """Generate a bar chart showing categorical risk scores.""" - logger.debug("Plotting categorical feature risk scores") + """Generate a bar chart showing categorical risk scores. + + Parameters + ---------- + res : dict + Dictionary containing attribute inference attack results. + path : str + Directory to write plots. + """ results: list[dict] = res["categorical"] if len(results) < 1: # pragma: no cover return + logger.debug("Plotting categorical feature risk scores") x: np.ndarray = np.arange(len(results)) ya: list[float] = [] yb: list[float] = [] @@ -333,21 +336,27 @@ def plot_categorical_risk( # pylint: disable=too-many-locals ax.legend(loc="best") plt.margins(y=0) plt.tight_layout() - if savefile != "": - fig.savefig(savefile + "_cat_risk.png", pad_inches=0, bbox_inches="tight") - logger.debug("Saved categorical risk plot: %s", savefile) - else: # pragma: no cover - plt.show() + filename = os.path.join(path, "cat_risk.png") + fig.savefig(filename, pad_inches=0, bbox_inches="tight") + logger.debug("Saved categorical risk plot: %s", filename) def plot_categorical_fraction( # pylint: disable=too-many-locals - res: dict, savefile: str = "" + res: dict, path: str = "" ) -> None: - """Generate a bar chart showing fraction of dataset inferred.""" - logger.debug("Plotting categorical feature tranche sizes") + """Generate a bar chart showing fraction of dataset inferred. + + Parameters + ---------- + res : dict + Dictionary containing attribute inference attack results. + path : str + Directory to write plots. + """ results: list[dict] = res["categorical"] if len(results) < 1: # pragma: no cover return + logger.debug("Plotting categorical feature tranche sizes") x: np.ndarray = np.arange(len(results)) ya: list[float] = [] yb: list[float] = [] @@ -374,21 +383,18 @@ def plot_categorical_fraction( # pylint: disable=too-many-locals ax.legend(loc="best") plt.margins(y=0) plt.tight_layout() - if savefile != "": - fig.savefig(savefile + "_cat_frac.png", pad_inches=0, bbox_inches="tight") - logger.debug("Saved categorical fraction plot: %s", savefile) - else: # pragma: no cover - plt.show() + filename = os.path.join(path, "cat_frac.png") + fig.savefig(filename, pad_inches=0, bbox_inches="tight") + logger.debug("Saved categorical fraction plot: %s", filename) def _infer_categorical(target: Target, feature_id: int, threshold: float) -> dict: """Return the training and test set risks of a categorical feature.""" - result: dict = { + return { "name": target.features[feature_id]["name"], "train": _infer(target, feature_id, threshold, True), "test": _infer(target, feature_id, threshold, False), } - return result def _is_categorical(target: Target, feature_id: int) -> bool: @@ -457,7 +463,8 @@ def _get_bounds_risk_for_sample( # pylint: disable=too-many-locals,too-many-arg Returns ------- - A bool representing whether the quantitative feature is at risk for the sample. + bool + Whether the quantitative feature is at risk for the sample. """ # attribute values to test - linearly sampled x_feat = np.linspace(feat_min, feat_max, feat_n, endpoint=True) @@ -515,10 +522,7 @@ def _get_bounds_risk_for_feature( # testing uses nursery with dummy cont. feature # which is not predictive feature_risk += 1 - if n_samples < 1: # pragma: no cover - # is unreachable because of how it is called - return 0 - return feature_risk / n_samples + return feature_risk / n_samples if n_samples > 0 else 0 def _get_bounds_risk( @@ -529,12 +533,11 @@ def _get_bounds_risk( X_test: np.ndarray, ) -> dict: """Return a dict containing the dataset risks of a quantitative feature.""" - risk: dict = { + return { "name": feature_name, "train": _get_bounds_risk_for_feature(target_model, feature_id, X_train), "test": _get_bounds_risk_for_feature(target_model, feature_id, X_test), } - return risk def _get_bounds_risks(target: Target, features: list[int], n_cpu: int) -> list[dict]: @@ -557,120 +560,23 @@ def _get_bounds_risks(target: Target, features: list[int], n_cpu: int) -> list[d def _attribute_inference(target: Target, n_cpu: int) -> dict: """Execute attribute inference attacks on a target given a trained model.""" # brute force attack categorical attributes using dataset unique values - logger.debug("Attacking dataset: %s", target.name) - logger.debug("Attacking categorical attributes...") + logger.info("Attacking dataset: %s", target.dataset_name) + logger.info("Attacking categorical attributes...") feature_list: list[int] = [] for feature in range(target.n_features): if _is_categorical(target, feature): feature_list.append(feature) results_a: list[dict] = _attack_brute_force(target, feature_list, n_cpu) # compute risk scores for quantitative attributes - logger.debug("Attacking quantitative attributes...") + logger.info("Attacking quantitative attributes...") feature_list = [] for feature in range(target.n_features): if not _is_categorical(target, feature): feature_list.append(feature) results_b: list[dict] = _get_bounds_risks(target, feature_list, n_cpu) # combine results into single object - results: dict = { - "name": target.name, + return { + "name": target.dataset_name, "categorical": results_a, "quantitative": results_b, } - return results - - -def create_aia_report(output: dict, name: str = "aia_report") -> FPDF: - """Create PDF report.""" - metadata = output["metadata"] - aia_metrics = output["attack_experiment_logger"]["attack_instance_logger"][ - "instance_0" - ] - plot_categorical_risk(aia_metrics, name) - plot_categorical_fraction(aia_metrics, name) - plot_quantitative_risk(aia_metrics, name) - pdf = FPDF() - pdf.add_page() - pdf.set_xy(0, 0) - report.title(pdf, "Attribute Inference Attack Report") - report.subtitle(pdf, "Introduction") - report.subtitle(pdf, "Metadata") - for key, value in metadata["experiment_details"].items(): - report.line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") - report.subtitle(pdf, "Metrics") - categ_rep = report_categorical(aia_metrics).split("\n") - quant_rep = report_quantitative(aia_metrics).split("\n") - report.line(pdf, "Categorical Features:", font="courier") - for line in categ_rep: - report.line(pdf, line, font="courier") - report.line(pdf, "Quantitative Features:", font="courier") - for line in quant_rep: - report.line(pdf, line, font="courier") - pdf.add_page() - report.subtitle(pdf, "Plots") - if len(aia_metrics["categorical"]) > 0: - pdf.image(name + "_cat_risk.png", x=None, y=None, w=150, h=0, type="", link="") - pdf.image(name + "_cat_frac.png", x=None, y=None, w=150, h=0, type="", link="") - if len(aia_metrics["quantitative"]) > 0: - pdf.image( - name + "_quant_risk.png", x=None, y=None, w=150, h=0, type="", link="" - ) - return pdf - - -def _run_attack_from_configfile(args: dict) -> None: - """Run a command line attack based on saved files described in .json file.""" - attack_obj = AttributeAttack( - attack_config_json_file_name=str(args.attack_config_json_file_name), - target_path=str(args.target_path), - ) - target = Target() - target.load(attack_obj.target_path) - attack_obj.attack(target) - attack_obj.make_report() - - -def main() -> None: - """Parse args and invoke relevant code.""" - parser = argparse.ArgumentParser(add_help=False) - - subparsers = parser.add_subparsers() - attack_parser_config = subparsers.add_parser("run-attack-from-configfile") - attack_parser_config.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="attack_config_json_file_name", - type=str, - default="config_aia_cmd.json", - help=( - "Name of the .json file containing details for the run. Default = %(default)s" - ), - ) - - attack_parser_config.add_argument( - "-t", - "--attack-target-folder-path", - action="store", - required=True, - dest="target_path", - type=str, - default="aia_target", - help=( - """Name of the target directory to load the trained target model and the target data. - Default = %(default)s""" - ), - ) - - attack_parser_config.set_defaults(func=_run_attack_from_configfile) - args = parser.parse_args() - try: - args.func(args) - except AttributeError as e: # pragma:no cover - print(e) - print("Invalid command. Try --help to get more details") - - -if __name__ == "__main__": # pragma:no cover - main() diff --git a/aisdc/attacks/factory.py b/aisdc/attacks/factory.py new file mode 100644 index 00000000..74852a7b --- /dev/null +++ b/aisdc/attacks/factory.py @@ -0,0 +1,62 @@ +"""Factory for running attacks.""" + +import logging + +import yaml + +from aisdc.attacks.attribute_attack import AttributeAttack +from aisdc.attacks.likelihood_attack import LIRAAttack +from aisdc.attacks.structural_attack import StructuralAttack +from aisdc.attacks.target import Target +from aisdc.attacks.worst_case_attack import WorstCaseAttack + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +registry: dict = { + "attribute": AttributeAttack, + "lira": LIRAAttack, + "structural": StructuralAttack, + "worstcase": WorstCaseAttack, +} + + +def create_attack(attack_name: str, **kwargs: dict) -> None: + """Instantiate an attack.""" + if attack_name in registry: + return registry[attack_name](**kwargs) + raise ValueError(f"Unknown Attack: {attack_name}") + + +def attack(target: Target, attack_name: str, **kwargs: dict) -> dict: + """Create and execute an attack on a target.""" + attack_obj = create_attack(attack_name, **kwargs) + return attack_obj.attack(target) + + +def run_attacks(target_dir: str, attack_filename: str) -> None: + """Run attacks given a target and attack configuration. + + Parameters + ---------- + target_dir : str + Name of a directory containing target.yaml. + attack_filename : str + Name of a YAML file containing an attack configuration. + """ + logger.info("Preparing Target") + target = Target() + target.load(target_dir) + + logger.info("Preparing Attacks") + with open(attack_filename, encoding="utf-8") as f: + config = yaml.safe_load(f) + + logger.info("Running Attacks") + + for attack_cfg in config["attacks"]: + name = attack_cfg["name"] + params = attack_cfg["params"] + attack(target=target, attack_name=name, **params) + + logger.info("Finished running attacks") diff --git a/aisdc/attacks/failfast.py b/aisdc/attacks/failfast.py deleted file mode 100644 index ac072f2a..00000000 --- a/aisdc/attacks/failfast.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Class to evaluate metric for fail fast option.""" - -from __future__ import annotations - -from typing import Any - - -class FailFast: - """Class to check attack being successful or not for a given metric. - - Note: An object of a FailFast is stateful and instance members - (success_count and fail_count) will preserve values across repetitions for - a test. For the new test a new object will require to be instantiated. - """ - - def __init__(self, attack_obj: Any) -> None: - self.metric_name = attack_obj.attack_metric_success_name - self.metric_success_thresh = attack_obj.attack_metric_success_thresh - self.comp_type = attack_obj.attack_metric_success_comp_type - self.success_count = 0 - self.fail_count = 0 - - def check_attack_success(self, metric_dict: dict) -> bool: - """Check if attack was successful for a given metric. - - Parameters - ---------- - metric_dict : dict - Dictionary with all computed metric values. - - Returns - ------- - success_status : bool - Boolean value based on the comparison for a given threshold. - - Notes - ----- - If value of a given metric value has a value meeting the threshold - based on the comparison type returns true otherwise it returns false. - This function also counts how many times the attack was successful - (i.e. true) and how many times it was not successful (i.e. false). - """ - metric_value = metric_dict[self.metric_name] - success_status = False - if self.comp_type == "lt": - success_status = bool(metric_value < self.metric_success_thresh) - elif self.comp_type == "lte": - success_status = bool(metric_value <= self.metric_success_thresh) - elif self.comp_type == "gt": - success_status = bool(metric_value > self.metric_success_thresh) - elif self.comp_type == "gte": - success_status = bool(metric_value >= self.metric_success_thresh) - elif self.comp_type == "eq": - success_status = bool(metric_value == self.metric_success_thresh) - elif self.comp_type == "not_eq": - success_status = bool(metric_value != self.metric_success_thresh) - if success_status: - self.success_count += 1 - else: - self.fail_count += 1 - return success_status - - def get_success_count(self) -> int: - """Return a count of attack being successful.""" - return self.success_count - - def get_fail_count(self) -> int: - """Return a count of attack being not successful.""" - return self.fail_count - - def get_attack_summary(self) -> dict: - """Return a dict of counts of attack being successful and not successful.""" - summary = {} - summary["success_count"] = self.success_count - summary["fail_count"] = self.fail_count - return summary - - def check_overall_attack_success(self, attack_obj: Any) -> bool: - """Return true if attack is successful for a given success count threshold.""" - overall_success_status = False - if self.success_count >= attack_obj.attack_metric_success_count_thresh: - overall_success_status = True - return overall_success_status diff --git a/aisdc/attacks/likelihood_attack.py b/aisdc/attacks/likelihood_attack.py index 23e07a43..d0bf4d43 100644 --- a/aisdc/attacks/likelihood_attack.py +++ b/aisdc/attacks/likelihood_attack.py @@ -4,33 +4,23 @@ from __future__ import annotations -import argparse -import importlib -import json import logging -import os -import uuid from collections.abc import Iterable -from datetime import datetime import numpy as np import sklearn +from fpdf import FPDF from scipy.stats import norm -from sklearn.datasets import load_breast_cancer -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split from aisdc import metrics from aisdc.attacks import report from aisdc.attacks.attack import Attack -from aisdc.attacks.attack_report_formatter import GenerateJSONModule from aisdc.attacks.target import Target logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -N_SHADOW_MODELS = 100 # Number of shadow models that should be trained EPS = 1e-16 # Used to avoid numerical issues in logit function -P_THRESH = 0.05 # default significance threshold class DummyClassifier: @@ -73,90 +63,36 @@ def _logit(p: float) -> float: class LIRAAttack(Attack): """The main LiRA Attack class.""" - # pylint: disable=too-many-instance-attributes - - def __init__( # pylint: disable = too-many-arguments + def __init__( self, + output_dir: str = "outputs", + write_report: bool = True, n_shadow_models: int = 100, p_thresh: float = 0.05, - output_dir: str = "outputs_lira", - report_name: str = "report_lira", - training_data_filename: str = None, - test_data_filename: str = None, - training_preds_filename: str = None, - test_preds_filename: str = None, - target_model: list = None, - target_model_hyp: dict = None, - attack_config_json_file_name: str = None, - n_shadow_rows_confidences_min: int = 10, - shadow_models_fail_fast: bool = False, - target_path: str = None, ) -> None: """Construct an object to execute a LiRA attack. Parameters ---------- + output_dir : str + Name of the directory where outputs are stored. + write_report : bool + Whether to generate a JSON and PDF report. n_shadow_models : int - number of shadow models to be trained + Number of shadow models to be trained. p_thresh : float - threshold to determine significance of things. For instance auc_p_value and pdif_vals - output_dir : str - name of the directory where outputs are stored - report_name : str - name of the pdf and json output reports - training_data_filename : str - name of the data file for the training data (in-sample) - test_data_filename : str - name of the file for the test data (out-of-sample) - training_preds_filename : str - name of the file to keep predictions of the training data (in-sample) - test_preds_filename : str - name of the file to keep predictions of the test data (out-of-sample) - target_model : list - name of the module (i.e. classification module name such as 'sklearn.ensemble') and - attack model name (i.e. classification model name such as 'RandomForestClassifier') - target_model_hyp : dict - dictionary of hyper parameters for the target_model - such as min_sample_split, min_samples_leaf etc - attack_config_json_file_name : str - name of the configuration file to load parameters - n_shadow_rows_confidences_min : int - number of minimum number of confidences calculated for - each row in test data (out-of-sample) - shadow_models_fail_fast : bool - If true it stops repetitions earlier based on the given minimum - number of confidences for each row in the test data - target_path : str - path to the saved trained target model and target data + Threshold to determine significance of things. For instance + auc_p_value and pdif_vals. """ - super().__init__() + super().__init__(output_dir=output_dir, write_report=write_report) self.n_shadow_models = n_shadow_models self.p_thresh = p_thresh - self.output_dir = output_dir - self.report_name = report_name - self.training_data_filename = training_data_filename - self.test_data_filename = test_data_filename - self.training_preds_filename = training_preds_filename - self.test_preds_filename = test_preds_filename - self.target_model = target_model - self.target_model_hyp = target_model_hyp - self.attack_config_json_file_name = attack_config_json_file_name - self.n_shadow_rows_confidences_min = n_shadow_rows_confidences_min - self.shadow_models_fail_fast = shadow_models_fail_fast - self.target_path = target_path - if self.attack_config_json_file_name is not None: - self._update_params_from_config_file() - if not os.path.exists(self.output_dir): - os.makedirs(self.output_dir) - self.attack_metrics = None - self.attack_failfast_shadow_models_trained = None - self.metadata = None def __str__(self): """Return the name of the attack.""" return "LiRA Attack" - def attack(self, target: Target) -> None: + def attack(self, target: Target) -> dict: """Run a LiRA attack from a Target object and a target model. Needs to have X_train, X_test, y_train and y_test set. @@ -165,11 +101,15 @@ def attack(self, target: Target) -> None: ---------- target : attacks.target.Target target as an instance of the Target class. + + Returns + ------- + dict + Attack report. """ shadow_clf = sklearn.base.clone(target.model) - target = self._check_and_update_dataset(target) - + # execute attack self.run_scenario_from_preds( shadow_clf, target.X_train, @@ -179,6 +119,12 @@ def attack(self, target: Target) -> None: target.y_test, target.model.predict_proba(target.X_test), ) + # create the report + output = self._make_report(target) + # write the report + self._write_report(output) + # return the report + return output def _check_and_update_dataset(self, target: Target) -> Target: """Check that it is safe to use class variables to index prediction arrays. @@ -189,7 +135,6 @@ def _check_and_update_dataset(self, target: Target) -> Target: 2. Removing from the test set any rows corresponding to classes that are not in the training set. """ - logger = logging.getLogger("_check_and_update_dataset") y_train_new = [] classes = list(target.model.classes_) for y in target.y_train: @@ -227,7 +172,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements, too-many- X_shadow_train: Iterable[float], y_shadow_train: Iterable[float], shadow_train_preds: Iterable[float], - ) -> tuple[np.ndarray, np.ndarray, sklearn.base.BaseEstimator]: + ) -> None: """Run the likelihood test, using the "offline" version. See p.6 (top of second column) for details. @@ -249,38 +194,7 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements, too-many- Labels that will be used to train the shadow model shadow_train_preds : np.ndarray Array of predictions produced by the target model on the shadow data - - Returns - ------- - mia_scores : np.ndarray - Attack probabilities of belonging to the training set or not - mia_labels : np.ndarray - True labels of belonging to the training set or not - mia_cls : DummyClassifier - A DummyClassifier that directly returns the scores for compatibility with code - in metrics.py - - Examples - -------- - >>> X, y = load_breast_cancer(return_X_y=True, as_frame=False) - >>> X_train, X_test, y_train, y_test = train_test_split( - >>> X, y, test_size=0.5, stratify=y - >>> ) - >>> rf = RandomForestClassifier(min_samples_leaf=1, min_samples_split=2) - >>> rf.fit(X_train, y_train) - >>> mia_test_probs, mia_test_labels, mia_clf = likelihood_scenario( - >>> RandomForestClassifier(min_samples_leaf=1, min_samples_split=2, max_depth=10), - >>> X_train, - >>> y_train, - >>> rf.predict_proba(X_train), - >>> X_test, - >>> y_test, - >>> rf.predict_proba(X_test), - >>> n_shadow_models=100 - >>> ) """ - logger = logging.getLogger("lr-scenario") - n_train_rows, _ = X_target_train.shape n_shadow_rows, _ = X_shadow_train.shape indices = np.arange(0, n_train_rows + n_shadow_rows, 1) @@ -342,23 +256,6 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements, too-many- # catch-all shadow_row_to_confidence[i].append(_logit(0)) - # Compute number of confidences for each row - lengths_shadow_row_to_confidence = { - key: len(value) for key, value in shadow_row_to_confidence.items() - } - n_shadow_confidences = self.n_shadow_rows_confidences_min - # Stop training of shadow models when shadow_model_fail_fast is True - # and a minimum number of confidences specified by parameter - # (n_shadow_rows_confidences_min) are computed for each row - if ( - not any( - value < n_shadow_confidences - for value in lengths_shadow_row_to_confidence.values() - ) - and self.shadow_models_fail_fast - ): - break - self.attack_failfast_shadow_models_trained = model_idx + 1 # Do the test described in the paper in each case mia_scores = [] mia_labels = [] @@ -394,34 +291,9 @@ def run_scenario_from_preds( # pylint: disable = too-many-statements, too-many- y_pred_proba = mia_clf.predict_proba(mia_scores) self.attack_metrics = [metrics.get_metrics(y_pred_proba, mia_labels)] - def example(self) -> None: - """Run an example attack using data from sklearn. - - Generates example data, trains a classifier and tuns the attack - """ - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.5, stratify=y - ) - rf = RandomForestClassifier(min_samples_leaf=1, min_samples_split=2) - rf.fit(X_train, y_train) - self.run_scenario_from_preds( - sklearn.base.clone(rf), - X_train, - y_train, - rf.predict_proba(X_train), - X_test, - y_test, - rf.predict_proba(X_test), - ) - def _construct_metadata(self) -> None: """Construct the metadata object.""" - self.metadata = {} - self.metadata["experiment_details"] = {} - self.metadata["experiment_details"] = self.get_params() - - self.metadata["global_metrics"] = {} + super()._construct_metadata() pdif = np.exp(-self.attack_metrics[0]["PDIF01"]) @@ -445,46 +317,9 @@ def _construct_metadata(self) -> None: f"{0.5 - 3 * auc_std} -> {0.5 + 3 * auc_std}" ) - self.metadata["attack"] = str(self) - - def make_report(self) -> dict: - """Create the report. - - Creates the output report. If self.args.report_name is not None, it - will also save the information in json and pdf formats. - - Returns - ------- - output : Dict - Dictionary containing all attack output - """ - logger = logging.getLogger("reporting") - report_dest = os.path.join(self.output_dir, self.report_name) - logger.info( - "Starting reports, pdf report name = %s, json report name = %s", - report_dest + ".pdf", - report_dest + ".json", - ) - output = {} - output["log_id"] = str(uuid.uuid4()) - output["log_time"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - self._construct_metadata() - output["metadata"] = self.metadata - output["attack_experiment_logger"] = self._get_attack_metrics_instances() - - json_attack_formatter = GenerateJSONModule(report_dest + ".json") - json_report = report.create_json_report(output) - json_attack_formatter.add_attack_output(json_report, "LikelihoodAttack") - - pdf_report = report.create_lr_report(output) - report.add_output_to_pdf(report_dest, pdf_report, "LikelihoodAttack") - logger.info( - "Wrote pdf report to %s and json report to %s", - report_dest + ".pdf", - report_dest + ".json", - ) - - return output + def _make_pdf(self, output: dict) -> FPDF: + """Create PDF report.""" + return report.create_lr_report(output) def _get_attack_metrics_instances(self) -> dict: """Construct the metadata object after attacks.""" @@ -492,276 +327,7 @@ def _get_attack_metrics_instances(self) -> dict: attack_metrics_instances = {} for rep, _ in enumerate(self.attack_metrics): - self.attack_metrics[rep]["n_shadow_models_trained"] = ( - self.attack_failfast_shadow_models_trained - ) attack_metrics_instances["instance_" + str(rep)] = self.attack_metrics[rep] attack_metrics_experiment["attack_instance_logger"] = attack_metrics_instances return attack_metrics_experiment - - def setup_example_data(self) -> None: - """Create example data and save (including config). - - Intended to allow users to see how they would need to setup their own - data. Generates train and test data .csv files, train and test - predictions .csv files and a config.json file that can be used to run - the attack from the command line. - """ - X, y = load_breast_cancer(return_X_y=True) - X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.5, stratify=y - ) - rf = RandomForestClassifier(min_samples_split=2, min_samples_leaf=1) - rf.fit(X_train, y_train) - train_data = np.hstack((X_train, y_train[:, None])) - np.savetxt("train_data.csv", train_data, delimiter=",") - - test_data = np.hstack((X_test, y_test[:, None])) - np.savetxt("test_data.csv", test_data, delimiter=",") - - train_preds = rf.predict_proba(X_train) - test_preds = rf.predict_proba(X_test) - np.savetxt("train_preds.csv", train_preds, delimiter=",") - np.savetxt("test_preds.csv", test_preds, delimiter=",") - - config = { - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - - with open("config.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - - def attack_from_config(self) -> None: # pylint: disable = too-many-locals - """Run an attack based on the args parsed from the command line.""" - logger = logging.getLogger("run-attack") - logger.info("Loading training data csv from %s", self.training_data_filename) - training_data = np.loadtxt(self.training_data_filename, delimiter=",") - X_train = training_data[:, :-1] - y_train = training_data[:, -1].flatten().astype(int) - logger.info("Loaded %d rows", len(X_train)) - - logger.info("Loading test data csv from %s", self.test_data_filename) - test_data = np.loadtxt(self.test_data_filename, delimiter=",") - X_test = test_data[:, :-1] - y_test = test_data[:, -1].flatten().astype(int) - logger.info("Loaded %d rows", len(X_test)) - - logger.info("Loading train predictions form %s", self.training_preds_filename) - train_preds = np.loadtxt(self.training_preds_filename, delimiter=",") - assert len(train_preds) == len(X_train) - - logger.info("Loading test predictions form %s", self.test_preds_filename) - test_preds = np.loadtxt(self.test_preds_filename, delimiter=",") - assert len(test_preds) == len(X_test) - if self.target_model is None: - raise ValueError("Target model cannot be None") - if self.target_model_hyp is None: - raise ValueError("Target model hyperparameters cannot be None") - clf_module_name, clf_class_name = self.target_model - module = importlib.import_module(clf_module_name) - clf_class = getattr(module, clf_class_name) - clf_params = self.target_model_hyp - clf = clf_class(**clf_params) - logger.info("Created model: %s", str(clf)) - self.run_scenario_from_preds( - clf, X_train, y_train, train_preds, X_test, y_test, test_preds - ) - logger.info("Computing metrics") - - -# Methods invoked by command line script -def _setup_example_data(args): - """Call the methods to setup some example data.""" - attack_obj = LIRAAttack( - n_shadow_models=args.n_shadow_models, - n_shadow_rows_confidences_min=args.n_shadow_rows_confidences_min, - output_dir=args.output_dir, - report_name=args.report_name, - p_thresh=args.p_thresh, - shadow_models_fail_fast=args.shadow_models_fail_fast, - ) - attack_obj.setup_example_data() - - -def _example(args): - """Call the methods to run an example.""" - attack_obj = LIRAAttack( - n_shadow_models=args.n_shadow_models, - n_shadow_rows_confidences_min=args.n_shadow_rows_confidences_min, - output_dir=args.output_dir, - report_name=args.report_name, - p_thresh=args.p_thresh, - shadow_models_fail_fast=args.shadow_models_fail_fast, - ) - attack_obj.example() - attack_obj.make_report() - - -def _run_attack(args): - """Run a command line attack based on saved files described in .json file.""" - attack_obj = LIRAAttack( - n_shadow_models=args.n_shadow_models, - n_shadow_rows_confidences_min=args.n_shadow_rows_confidences_min, - p_thresh=args.p_thresh, - output_dir=args.output_dir, - report_name=args.report_name, - shadow_models_fail_fast=args.shadow_models_fail_fast, - attack_config_json_file_name=args.attack_config_json_file_name, - ) - attack_obj.attack_from_config() - attack_obj.make_report() - - -def _run_attack_from_configfile(args): - """Run a command line attack based on saved files described in .json file.""" - attack_obj = LIRAAttack( - attack_config_json_file_name=args.attack_config_json_file_name, - target_path=str(args.target_path), - ) - print(args.attack_config_json_file_name) - target = Target() - target.load(attack_obj.target_path) - attack_obj.attack(target) - attack_obj.make_report() - - -def main(): - """Parse args and invoke relevant code.""" - parser = argparse.ArgumentParser(add_help=False) - parser.add_argument( - "-s", - "--n-shadow-models", - type=int, - required=False, - default=N_SHADOW_MODELS, - action="store", - dest="n_shadow_models", - help=("The number of shadow models to train (default = %(default)d)"), - ) - - parser.add_argument( - "--n-shadow-rows-confidences-min", - type=int, - action="store", - dest="n_shadow_rows_confidences_min", - default=10, - required=False, - help=( - """Number of confidences against rows in shadow data from the shadow models - and works when --shadow-models-fail-fast = True. Default = %(default)d""" - ), - ) - - parser.add_argument( - "--output-dir", - type=str, - action="store", - dest="output_dir", - default="output_lira", - required=False, - help=("Directory name where output files are stored. Default = %(default)s."), - ) - - parser.add_argument( - "--report-name", - type=str, - action="store", - dest="report_name", - default="report_lira", - required=False, - help=( - """Filename for the pdf and json output reports. Default = %(default)s. - Code will append .pdf and .json""" - ), - ) - - parser.add_argument( - "-p", - "--p-thresh", - type=float, - action="store", - dest="p_thresh", - required=False, - default=P_THRESH, - help=("Significance threshold for p-value comparisons. Default = %(default)f"), - ) - - parser.add_argument( - "--shadow-models-fail-fast", - action="store_true", - required=False, - dest="shadow_models_fail_fast", - help=( - """To stop training shadow models early based on minimum number of - confidences across all rows (--n-shadow-rows-confidences-min) - in the shadow data. Default = %(default)s""" - ), - ) - - subparsers = parser.add_subparsers() - example_parser = subparsers.add_parser("run-example", parents=[parser]) - example_parser.set_defaults(func=_example) - - attack_parser = subparsers.add_parser("run-attack", parents=[parser]) - attack_parser.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="attack_config_json_file_name", - type=str, - help=( - "Name of the .json file containing details for the run. Default = %(default)s" - ), - ) - attack_parser.set_defaults(func=_run_attack) - - attack_parser_config = subparsers.add_parser("run-attack-from-configfile") - attack_parser_config.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="attack_config_json_file_name", - type=str, - default="config_lira_cmd.json", - help=( - "Name of the .json file containing details for the run. Default = %(default)s" - ), - ) - - attack_parser_config.add_argument( - "-t", - "--attack-target-folder-path", - action="store", - required=True, - dest="target_path", - type=str, - default="lira_target", - help=( - """Name of the target directory to load the trained target model and the target data. - Default = %(default)s""" - ), - ) - - attack_parser_config.set_defaults(func=_run_attack_from_configfile) - - example_data_parser = subparsers.add_parser("setup-example-data") - example_data_parser.set_defaults(func=_setup_example_data) - - args = parser.parse_args() - try: - args.func(args) - except AttributeError as e: # pragma:no cover - print(e) - print("Invalid command. Try --help to get more details") - - -if __name__ == "__main__": # pragma:no cover - main() diff --git a/aisdc/attacks/multiple_attacks.py b/aisdc/attacks/multiple_attacks.py deleted file mode 100644 index c192fd5b..00000000 --- a/aisdc/attacks/multiple_attacks.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Run multiple attacks including MIA and AIA using a single configuration file.""" - -from __future__ import annotations - -import argparse -import json -import logging -import os -import uuid -from typing import Any - -from aisdc.attacks.attack import Attack -from aisdc.attacks.attribute_attack import AttributeAttack -from aisdc.attacks.likelihood_attack import LIRAAttack -from aisdc.attacks.target import Target -from aisdc.attacks.worst_case_attack import WorstCaseAttack - - -class MultipleAttacks(Attack): - """Wrap the MIA and AIA attack codes.""" - - def __init__(self, config_filename: str = None) -> None: - """Construct an object to execute multiple attacks. - - Parameters - ---------- - config_filename : str - Name of a JSON file containing attack configurations. - """ - super().__init__() - self.config_filename = config_filename - - def __str__(self) -> None: - """Return the name of the attack.""" - return "Multiple Attacks (MIA and AIA) given configurations" - - def attack(self, target: Target) -> None: - """Run attacks from a Target object and a target model. - - Parameters - ---------- - target : attacks.target.Target - Target as an instance of the Target class. Needs to have X_train, - X_test, y_train and y_test set. - """ - logger = logging.getLogger("attack-multiple attacks") - logger.info("Running attacks") - file_contents = "" - with open(self.config_filename, "r+", encoding="utf-8") as f: - file_contents = f.read() - - if file_contents != "": - config_file_data = json.loads(file_contents) - for config_obj in config_file_data: - params = config_file_data[config_obj] - attack_name = config_obj.split("-")[0] - attack_obj = None - if attack_name == "worst_case": - attack_obj = WorstCaseAttack(**params) - elif attack_name == "lira": - attack_obj = LIRAAttack(**params) - elif attack_name == "attribute": - attack_obj = AttributeAttack(**params) - else: - attack_names = "'worst_case', 'lira' and 'attribute'" - logger.error( - """attack name is %s whereas supported attack names are %s: """, - attack_name, - attack_names, - ) - - if attack_obj is not None: - attack_obj.attack(target) - - if attack_obj is not None: - _ = attack_obj.make_report() - logger.info("Finished running attacks") - - -class ConfigFile: - """Create a single JSON configuration file.""" - - def __init__(self, filename: str = None) -> None: - self.filename = filename - - dirname = os.path.normpath(os.path.dirname(self.filename)) - os.makedirs(dirname, exist_ok=True) - # if file doesn't exist, create it - with open(self.filename, "w", encoding="utf-8") as f: - f.write("") - - def add_config(self, config_obj: Any, config_attack_type: str) -> None: - """Add a section of JSON to the file which is already open.""" - # Read the contents of the file and then clear the file - config_file_data = self.read_config_file() - - # Add the new JSON to the JSON that was in the file, and re-write - with open(self.filename, "w", encoding="utf-8") as f: - class_name = config_attack_type + "-" + str(uuid.uuid4()) - - if isinstance(config_obj, dict): - config_file_data[class_name] = config_obj - elif isinstance(config_obj, str): - with open(str(config_obj), encoding="utf-8") as fr: - config_file_data[class_name] = json.loads(fr.read()) - - f.write(json.dumps(config_file_data)) - - def read_config_file(self) -> dict: - """Read a JSON config file and return dict with configuration objects.""" - with open(self.filename, encoding="utf-8") as f: - file_contents = f.read() - return json.loads(file_contents) if file_contents != "" else {} - - -def _run_attack_from_configfile(args) -> None: - """Run a command line attack based on saved files described in a JSON file.""" - attack_obj = MultipleAttacks( - config_filename=str(args.config_filename), - ) - target = Target() - target.load(args.target_path) - attack_obj.attack(target) - - -def main() -> None: - """Parse args and invoke relevant code.""" - parser = argparse.ArgumentParser(add_help=False) - - subparsers = parser.add_subparsers() - attack_parser_config = subparsers.add_parser("run-attack-from-configfile") - attack_parser_config.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="config_filename", - type=str, - default="singleconfig.json", - help=( - """Name of the .json file containing details for running - multiple attacks run. Default = %(default)s""" - ), - ) - - attack_parser_config.add_argument( - "-t", - "--attack-target-folder-path", - action="store", - required=True, - dest="target_path", - type=str, - default="target", - help=( - """Name of the target directory to load the trained target model and the target data. - Default = %(default)s""" - ), - ) - - attack_parser_config.set_defaults(func=_run_attack_from_configfile) - args = parser.parse_args() - try: - args.func(args) - except AttributeError as e: # pragma:no cover - print(e) - print("Invalid command. Try --help to get more details") - - -if __name__ == "__main__": # pragma:no cover - main() diff --git a/aisdc/attacks/report.py b/aisdc/attacks/report.py index 3712fdc5..abe47d83 100644 --- a/aisdc/attacks/report.py +++ b/aisdc/attacks/report.py @@ -1,6 +1,5 @@ """Code for automatic report generation.""" -import abc import json import os from typing import Any @@ -10,6 +9,8 @@ from fpdf import FPDF from pypdf import PdfWriter +from aisdc.attacks.attack_report_formatter import GenerateJSONModule + # Adds a border to all pdf cells of set to 1 -- useful for debugging BORDER = 0 @@ -28,81 +29,91 @@ MAPPINGS = {"PDIF01": lambda x: np.exp(-x)} INTRODUCTION = ( - "This report provides a summary of a series of simulated attack experiments performed " - "on the model outputs provided. An attack model is trained to attempt to distinguish " - "between outputs from training (in-sample) and testing (out-of-sample) data. The metrics " - "below describe the success of this classifier. A successful classifier indicates that the " - "original model is unsafe and should not be allowed to be released from the TRE.\n" - "In particular, the simulation splits the data provided into test and train sets (each will " - "in- and out-of-sample examples). The classifier is trained on the train set and evaluated " - "on the test set. This is repeated with different train/test splits a user-specified number " - "of times.\n" - "To help place the results in context, the code may also have run a series of baseline " - "experiments. In these, random model outputs for hypothetical in- and out-of-sample data are " - "generated with identical statistical properties. In these baseline cases, there is no signal " - "that an attacker could leverage and therefore these values provide a baseline against " - "which the actual values can be compared.\n" - "For some metrics (FDIF and AUC), we are able to compute p-values. In each case, shown below " - "(in the Global metrics sections) is the number of repetitions that exceeded the p-value " - "threshold both without, and with correction for multiple testing (Benjamini-Hochberg " - "procedure).\n" - "ROC curves for all real (red) and dummy (blue) repetitions are provided. These are shown in " - "log space (as reommended here [ADD URL]) to emphasise the region in which risk is highest -- " - "the bottom left (are high true positive rates possible with low false positive rates).\n" - "A description of the metrics and how to interpret them within the context of an attack is " + "This report provides a summary of a series of simulated attack experiments " + "performed on the model outputs provided. An attack model is trained to " + "attempt to distinguish between outputs from training (in-sample) and " + "testing (out-of-sample) data. The metrics below describe the success of " + "this classifier. A successful classifier indicates that the original model " + "is unsafe and should not be allowed to be released from the TRE.\n In " + "particular, the simulation splits the data provided into test and train " + "sets (each will in- and out-of-sample examples). The classifier is trained " + "on the train set and evaluated on the test set. This is repeated with " + "different train/test splits a user-specified number of times.\n To help " + "place the results in context, the code may also have run a series of " + "baseline experiments. In these, random model outputs for hypothetical in- " + "and out-of-sample data are generated with identical statistical properties. " + "In these baseline cases, there is no signal that an attacker could leverage " + "and therefore these values provide a baseline against which the actual " + "values can be compared.\n For some metrics (FDIF and AUC), we are able to " + "compute p-values. In each case, shown below (in the Global metrics " + "sections) is the number of repetitions that exceeded the p-value threshold " + "both without, and with correction for multiple testing (Benjamini-Hochberg " + "procedure).\n ROC curves for all real (red) and dummy (blue) repetitions are " + "provided. These are shown in log space (as reommended here [ADD URL]) to " + "emphasise the region in which risk is highest -- the bottom left (are high " + "true positive rates possible with low false positive rates).\n A description " + "of the metrics and how to interpret them within the context of an attack is " "given below." ) LOGROC_CAPTION = ( - "This plot shows the False Positive Rate (x) versus the True Positive Rate (y). " - "The axes are in log space enabling us to focus on areas where the False Positive Rate is low " - "(left hand area). Curves above the y = x line (black dashes) in this region represent a " - "disclosure risk as an attacker can obtain many more true than false positives. " - "The solid coloured lines show the curves for the attack simulations with the true model " - "outputs. The lighter grey lines show the curves for randomly generated outputs with " - "no structure (i.e. in- and out-of- sample predictions are generated from the same " - "distributions. Solid curves consistently higher than the grey curves in the left hand " - "part of the plot are a sign of concern." + "This plot shows the False Positive Rate (x) versus the True Positive Rate " + "(y). The axes are in log space enabling us to focus on areas where the " + "False Positive Rate is low (left hand area). Curves above the y = x line " + "(black dashes) in this region represent a disclosure risk as an attacker " + "can obtain many more true than false positives. The solid coloured lines " + "show the curves for the attack simulations with the true model outputs. The " + "lighter grey lines show the curves for randomly generated outputs with no " + "structure (i.e. in- and out-of- sample predictions are generated from the " + "same distributions. Solid curves consistently higher than the " + "grey curves in the left hand part of the plot are a sign of concern. " ) GLOSSARY = { "AUC": "Area Under the ROC curve", "True Positive Rate (TPR)": ( - "The true positive rate is the number of True Positives that are predicted as positive as " - "a proportion of the total number of positives. If an attacker has N examples that were " - "actually in the training set, the TPR is the proportion of these that they predict as " - "being in the training set." + "The true positive rate is the number of True Positives that are " + "predicted as positive as a proportion of the total number of positives. " + "If an attacker has N examples that were actually in the training set, " + "the TPR is the proportion of these that they predict as being in the " + "training set." ), "ACC": "The proportion of predictions that the attacker makes that are correct.", } -class NumpyArrayEncoder(json.JSONEncoder): - """Json encoder that can cope with numpy arrays.""" +def write_json(output: dict, dest: str) -> None: + """Write attack report to JSON.""" + attack_formatter = GenerateJSONModule(dest + ".json") + attack_report: str = json.dumps(output, cls=CustomJSONEncoder) + attack_name: str = output["metadata"]["attack_name"] + attack_formatter.add_attack_output(attack_report, attack_name) + + +class CustomJSONEncoder(json.JSONEncoder): + """JSON encoder that can cope with numpy arrays, etc.""" def default(self, o: Any): """If an object is an np.ndarray, convert to list.""" if isinstance(o, np.ndarray): return o.tolist() - if isinstance(o, np.int64): + if isinstance(o, (np.int64, np.int32)): return int(o) - if isinstance(o, np.int32): - return int(o) - if isinstance(o, abc.ABCMeta): - return str(o) - return json.JSONEncoder.default(self, o) + if isinstance(o, np.bool_): + return bool(o) + try: # Try the default method first + return super().default(o) + except TypeError: + return str(o) # If object is not serializable, convert it to a string -def _write_dict( - pdf: FPDF, input_dict: dict, indent: int = 0, border: int = BORDER -) -> None: +def _write_dict(pdf: FPDF, data: dict, border: int = BORDER) -> None: """Write a dictionary to the pdf.""" - for key, value in input_dict.items(): + for key, value in data.items(): pdf.set_font("arial", "B", 14) - pdf.cell(75, 5, key, border, 1, "L") - pdf.cell(indent, 0) + pdf.cell(0, 5, key, border, 1, "L") pdf.set_font("arial", "", 12) - pdf.multi_cell(150, 5, value, border, "L") + pdf.multi_cell(0, 5, str(value), 0, 1) pdf.ln(h=5) @@ -164,11 +175,10 @@ def _roc_plot_single(metrics: dict, save_name: str) -> None: plt.savefig(save_name) -def _roc_plot(metrics: dict, dummy_metrics: list, save_name: str) -> None: +def _roc_plot(metrics: dict, save_name: str) -> None: """Create a roc plot for multiple repetitions.""" plt.figure() plt.plot([0, 1], [0, 1], "k--") - do_dummy = bool(dummy_metrics) # Compute average ROC base_fpr = np.linspace(0, 1, 1000) @@ -176,22 +186,6 @@ def _roc_plot(metrics: dict, dummy_metrics: list, save_name: str) -> None: for i, metric_set in enumerate(metrics): all_tpr[i, :] = np.interp(base_fpr, metric_set["fpr"], metric_set["tpr"]) - if do_dummy: - all_tpr_dummy = np.zeros((len(dummy_metrics), len(base_fpr)), float) - for i, metric_set in enumerate(dummy_metrics): - all_tpr_dummy[i, :] = np.interp( - base_fpr, metric_set["fpr"], metric_set["tpr"] - ) - - for _, metric_set in enumerate(dummy_metrics): - plt.plot( - metric_set["fpr"], - metric_set["tpr"], - color="lightsteelblue", - linewidth=0.5, - alpha=0.5, - ) - for _, metric_set in enumerate(metrics): plt.plot( metric_set["fpr"], metric_set["tpr"], color="lightsalmon", linewidth=0.5 @@ -199,11 +193,6 @@ def _roc_plot(metrics: dict, dummy_metrics: list, save_name: str) -> None: tpr_mu = all_tpr.mean(axis=0) plt.plot(base_fpr, tpr_mu, "r") - - if do_dummy: - dummy_mu = all_tpr_dummy.mean(axis=0) - plt.plot(base_fpr, dummy_mu, "b") - plt.xscale("log") plt.yscale("log") plt.xlabel("False Positive Rate") @@ -233,8 +222,6 @@ def create_mia_report(attack_output: dict) -> FPDF: pdf : fpdf.FPDF fpdf document object """ - do_dummy = False - dummy_metrics = [] mia_metrics = [ v for _, v in attack_output["attack_experiment_logger"][ @@ -243,14 +230,9 @@ def create_mia_report(attack_output: dict) -> FPDF: ] metadata = attack_output["metadata"] - dest_log_roc = ( - os.path.join( - metadata["experiment_details"]["output_dir"], - metadata["experiment_details"]["report_name"], - ) - + "_log_roc.png" - ) - _roc_plot(mia_metrics, dummy_metrics, dest_log_roc) + path: str = metadata["attack_params"]["output_dir"] + dest_log_roc = os.path.join(path, "log_roc.png") + _roc_plot(mia_metrics, dest_log_roc) pdf = FPDF() pdf.add_page() @@ -259,15 +241,11 @@ def create_mia_report(attack_output: dict) -> FPDF: subtitle(pdf, "Introduction") line(pdf, INTRODUCTION) subtitle(pdf, "Experiment summary") - for key, value in metadata["experiment_details"].items(): + for key, value in metadata["attack_params"].items(): line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") subtitle(pdf, "Global metrics") for key, value in metadata["global_metrics"].items(): line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") - if do_dummy: - subtitle(pdf, "Baseline global metrics") - for key, value in metadata["baseline_global_metrics"].items(): - line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") subtitle(pdf, "Metrics") line( @@ -285,28 +263,6 @@ def create_mia_report(attack_output: dict) -> FPDF: ) line(pdf, text, font="courier") - if do_dummy: - subtitle(pdf, "Baseline metrics") - line( - pdf, - ( - "The following show summaries of the attack metrics over the " - "repetitions where there is no statistical difference between " - "predictions in the training and test sets. Simulation was done " - "with training and test set sizes equal to the real ones" - ), - font="arial", - ) - for metric in DISPLAY_METRICS: - vals = np.array([m[metric] for m in dummy_metrics]) - if metric in MAPPINGS: - vals = np.array([MAPPINGS[metric](v) for v in vals]) - text = ( - f"{metric:>12} mean = {vals.mean():.2f}, var = {vals.var():.4f}, " - f"min = {vals.min():.2f}, max = {vals.max():.2f}" - ) - line(pdf, text, font="courier") - _add_log_roc_to_page(dest_log_roc, pdf) line(pdf, LOGROC_CAPTION) @@ -314,10 +270,12 @@ def create_mia_report(attack_output: dict) -> FPDF: title(pdf, "Glossary") _write_dict(pdf, GLOSSARY) + if os.path.exists(dest_log_roc): + os.remove(dest_log_roc) return pdf -def add_output_to_pdf(report_dest: str, pdf_report: FPDF, attack_type: str) -> None: +def write_pdf(report_dest: str, pdf_report: FPDF) -> None: """Create pdf and append contents if it already exists.""" if os.path.exists(report_dest + ".pdf"): old_pdf = report_dest + ".pdf" @@ -331,18 +289,6 @@ def add_output_to_pdf(report_dest: str, pdf_report: FPDF, attack_type: str) -> N os.remove(new_pdf) else: pdf_report.output(report_dest + ".pdf") - if attack_type in ("WorstCaseAttack", "LikelihoodAttack"): - path = report_dest + "_log_roc.png" - if os.path.exists(path): - os.remove(path) - elif attack_type == "AttributeAttack": - path = report_dest + "_cat_frac.png" - if os.path.exists(path): - os.remove(path) - - path = report_dest + "_cat_risk.png" - if os.path.exists(path): - os.remove(path) def _add_log_roc_to_page(log_roc: str = None, pdf_obj: FPDF = None) -> None: @@ -353,12 +299,6 @@ def _add_log_roc_to_page(log_roc: str = None, pdf_obj: FPDF = None) -> None: pdf_obj.set_font("arial", "", 12) -def create_json_report(output: dict) -> None: - """Create a report in json format for injestion by other tools.""" - # Initial work, just dump mia_metrics and dummy_metrics into a json structure - return json.dumps(output, cls=NumpyArrayEncoder) - - def create_lr_report(output: dict) -> FPDF: """Make a lira membership inference report. @@ -385,13 +325,9 @@ def create_lr_report(output: dict) -> FPDF: for _, v in output["attack_experiment_logger"]["attack_instance_logger"].items() ][0] metadata = output["metadata"] - dest_log_roc = ( - os.path.join( - metadata["experiment_details"]["output_dir"], - metadata["experiment_details"]["report_name"], - ) - + "_log_roc.png" - ) + + path: str = metadata["attack_params"]["output_dir"] + dest_log_roc = os.path.join(path, "log_roc.png") _roc_plot_single(mia_metrics, dest_log_roc) pdf = FPDF() pdf.add_page() @@ -399,7 +335,7 @@ def create_lr_report(output: dict) -> FPDF: title(pdf, "Likelihood Ratio Attack Report") subtitle(pdf, "Introduction") subtitle(pdf, "Metadata") - for key, value in metadata["experiment_details"].items(): + for key, value in metadata["attack_params"].items(): line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") for key, value in metadata["global_metrics"].items(): line(pdf, f"{key:>30s}: {str(value):30s}", font="courier") @@ -414,4 +350,7 @@ def create_lr_report(output: dict) -> FPDF: pdf.add_page() subtitle(pdf, "ROC Curve") pdf.image(dest_log_roc, x=None, y=None, w=0, h=140, type="", link="") + # clean up + if os.path.exists(dest_log_roc): + os.remove(dest_log_roc) return pdf diff --git a/aisdc/attacks/structural_attack.py b/aisdc/attacks/structural_attack.py index f44ec5f0..62bc08a5 100644 --- a/aisdc/attacks/structural_attack.py +++ b/aisdc/attacks/structural_attack.py @@ -3,32 +3,29 @@ Runs a number of 'static' structural attacks based on: (i) the target model's properties; (ii) the TREs risk appetite as applied to tables and standard regressions. + +Tree-based model types currently supported. """ from __future__ import annotations -import argparse import logging -import os -import uuid -from datetime import datetime import numpy as np from acro import ACRO - -# tree-based model types currently supported from sklearn.base import BaseEstimator from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier from sklearn.neural_network import MLPClassifier from sklearn.tree import DecisionTreeClassifier from xgboost.sklearn import XGBClassifier -from aisdc.attacks import report from aisdc.attacks.attack import Attack -from aisdc.attacks.attack_report_formatter import GenerateJSONModule from aisdc.attacks.target import Target logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# pylint: disable=chained-comparison def get_unnecessary_risk(model: BaseEstimator) -> bool: @@ -49,92 +46,102 @@ def get_unnecessary_risk(model: BaseEstimator) -> bool: were in the 20% most risky. The rules below were extracted from that tree for the 'least risky' nodes. - Notes - ----- - Returns True if high risk, otherwise False. + Parameters + ---------- + model : BaseEstimator + Model to check for risk. + + Returns + ------- + bool + True if high risk, otherwise False. """ - if not isinstance( - model, (DecisionTreeClassifier, RandomForestClassifier, XGBClassifier) - ): - return 0 # no experimental evidence to support rejection + unnecessary_risk: bool = False + if isinstance(model, DecisionTreeClassifier): + unnecessary_risk = _get_unnecessary_risk_dt(model) + elif isinstance(model, RandomForestClassifier): + unnecessary_risk = _get_unnecessary_risk_rf(model) + elif isinstance(model, XGBClassifier): + unnecessary_risk = _get_unnecessary_risk_xgb(model) + return unnecessary_risk + - unnecessary_risk = 0 +def _get_unnecessary_risk_dt(model: DecisionTreeClassifier) -> bool: + """Return whether DecisionTreeClassifier parameters are high risk.""" max_depth = float(model.max_depth) if model.max_depth else 500 + max_features = model.max_features + min_samples_leaf = model.min_samples_leaf + min_samples_split = model.min_samples_split + splitter = model.splitter + return ( + (max_depth > 7.5 and min_samples_leaf <= 7.5 and min_samples_split <= 15) + or ( + splitter == "best" + and max_depth > 7.5 + and min_samples_leaf <= 7.5 + and min_samples_split > 15 + ) + or ( + splitter == "best" + and max_depth > 7.5 + and 7.5 < min_samples_leaf <= 15 + and max_features is None + ) + or ( + splitter == "best" + and 3.5 < max_depth <= 7.5 + and max_features is None + and min_samples_leaf <= 7.5 + ) + or ( + splitter == "random" + and max_depth > 7.5 + and min_samples_leaf <= 7.5 + and max_features is None + ) + ) - # pylint:disable=chained-comparison,too-many-boolean-expressions - if isinstance(model, DecisionTreeClassifier): - max_features = model.max_features - min_samples_leaf = model.min_samples_leaf - min_samples_split = model.min_samples_split - splitter = model.splitter - if ( - (max_depth > 7.5 and min_samples_leaf <= 7.5 and min_samples_split <= 15) - or ( - splitter == "best" - and max_depth > 7.5 - and min_samples_leaf <= 7.5 - and min_samples_split > 15 - ) - or ( - splitter == "best" - and max_depth > 7.5 - and 7.5 < min_samples_leaf <= 15 - and max_features is None - ) - or ( - splitter == "best" - and 3.5 < max_depth <= 7.5 - and max_features is None - and min_samples_leaf <= 7.5 - ) - or ( - splitter == "random" - and max_depth > 7.5 - and min_samples_leaf <= 7.5 - and max_features is None - ) - ): - unnecessary_risk = 1 - elif isinstance(model, RandomForestClassifier): - n_estimators = model.n_estimators - max_features = model.max_features - min_samples_leaf = model.min_samples_leaf - min_samples_split = model.min_samples_split - if ( - (max_depth > 3.5 and n_estimators > 35 and max_features is not None) - or ( - max_depth > 3.5 - and n_estimators > 35 - and min_samples_split <= 15 - and max_features is None - and model.bootstrap - ) - or ( - max_depth > 7.5 - and 15 < n_estimators <= 35 - and min_samples_leaf <= 15 - and not model.bootstrap - ) - ): - unnecessary_risk = 1 - elif isinstance(model, XGBClassifier): - # check whether params exist and using xgboost defaults if not using defaults - # from https://github.com/dmlc/xgboost/blob/master/python-package/xgboost/sklearn.py - # and here: https://xgboost.readthedocs.io/en/stable/parameter.html - n_estimators = int(model.n_estimators) if model.n_estimators else 100 - max_depth = float(model.max_depth) if model.max_depth else 6 - min_child_weight = ( - float(model.min_child_weight) if model.min_child_weight else 1.0 +def _get_unnecessary_risk_rf(model: RandomForestClassifier) -> bool: + """Return whether RandomForestClassifier parameters are high risk.""" + max_depth = float(model.max_depth) if model.max_depth else 500 + n_estimators = model.n_estimators + max_features = model.max_features + min_samples_leaf = model.min_samples_leaf + min_samples_split = model.min_samples_split + return ( + (max_depth > 3.5 and n_estimators > 35 and max_features is not None) + or ( + max_depth > 3.5 + and n_estimators > 35 + and min_samples_split <= 15 + and max_features is None + and model.bootstrap + ) + or ( + max_depth > 7.5 + and 15 < n_estimators <= 35 + and min_samples_leaf <= 15 + and not model.bootstrap ) + ) - if ( - (max_depth > 3.5 and 3.5 < n_estimators <= 12.5 and min_child_weight <= 1.5) - or (max_depth > 3.5 and n_estimators > 12.5 and min_child_weight <= 3) - or (max_depth > 3.5 and n_estimators > 62.5 and 3 < min_child_weight <= 6) - ): - unnecessary_risk = 1 - return unnecessary_risk + +def _get_unnecessary_risk_xgb(model: XGBClassifier) -> bool: + """Return whether XGBClassifier parameters are high risk. + + Check whether params exist and using xgboost defaults if not using defaults + from https://github.com/dmlc/xgboost/blob/master/python-package/xgboost/sklearn.py + and here: https://xgboost.readthedocs.io/en/stable/parameter.html + """ + n_estimators = int(model.n_estimators) if model.n_estimators else 100 + max_depth = float(model.max_depth) if model.max_depth else 6 + min_child_weight = float(model.min_child_weight) if model.min_child_weight else 1.0 + return ( + (max_depth > 3.5 and 3.5 < n_estimators <= 12.5 and min_child_weight <= 1.5) + or (max_depth > 3.5 and n_estimators > 12.5 and min_child_weight <= 3) + or (max_depth > 3.5 and n_estimators > 62.5 and 3 < min_child_weight <= 6) + ) def get_tree_parameter_count(dtree: DecisionTreeClassifier) -> int: @@ -157,84 +164,93 @@ def get_tree_parameter_count(dtree: DecisionTreeClassifier) -> int: def get_model_param_count(model: BaseEstimator) -> int: """Return the number of trained parameters in a model.""" - n_params = 0 - + n_params: int = 0 if isinstance(model, DecisionTreeClassifier): - n_params = get_tree_parameter_count(model) - + n_params = _get_model_param_count_dt(model) elif isinstance(model, RandomForestClassifier): - for member in model.estimators_: - n_params += get_tree_parameter_count(member) - + n_params = _get_model_param_count_rf(model) elif isinstance(model, AdaBoostClassifier): - try: # sklearn v1.2+ - base = model.estimator - except AttributeError: # sklearn version <1.2 - base = model.base_estimator - if isinstance(base, DecisionTreeClassifier): - for member in model.estimators_: - n_params += get_tree_parameter_count(member) - - # TO-DO define these for xgb, logistic regression, SVC and others + n_params = _get_model_param_count_ada(model) elif isinstance(model, XGBClassifier): - df = model.get_booster().trees_to_dataframe() - n_trees = df["Tree"].max() - total = len(df) - n_leaves = len(df[df.Feature == "Leaf"]) - # 2 per internal node, one per clas in leaves, one weight per tree - n_params = 2 * (total - n_leaves) + (model.n_classes_ - 1) * n_leaves + n_trees - + n_params = _get_model_param_count_xgb(model) elif isinstance(model, MLPClassifier): - weights = model.coefs_ # dtype is list of numpy.ndarrays - biasses = model.intercepts_ # dtype is list of numpy.ndarrays - n_params = sum(a.size for a in weights) + sum(a.size for a in biasses) + n_params = _get_model_param_count_mlp(model) + return n_params + + +def _get_model_param_count_dt(model: DecisionTreeClassifier) -> int: + """Return the number of trained DecisionTreeClassifier parameters.""" + return get_tree_parameter_count(model) - else: - pass +def _get_model_param_count_rf(model: RandomForestClassifier) -> int: + """Return the number of trained RandomForestClassifier parameters.""" + n_params: int = 0 + for member in model.estimators_: + n_params += get_tree_parameter_count(member) + return n_params + + +def _get_model_param_count_ada(model: AdaBoostClassifier) -> int: + """Return the number of trained AdaBoostClassifier parameters.""" + n_params: int = 0 + try: # sklearn v1.2+ + base = model.estimator + except AttributeError: # sklearn version <1.2 + base = model.base_estimator + if isinstance(base, DecisionTreeClassifier): + for member in model.estimators_: + n_params += get_tree_parameter_count(member) return n_params +def _get_model_param_count_xgb(model: XGBClassifier) -> int: + """Return the number of trained XGBClassifier parameters.""" + df = model.get_booster().trees_to_dataframe() + n_trees = df["Tree"].max() + total = len(df) + n_leaves = len(df[df.Feature == "Leaf"]) + # 2 per internal node, one per clas in leaves, one weight per tree + return 2 * (total - n_leaves) + (model.n_classes_ - 1) * n_leaves + n_trees + + +def _get_model_param_count_mlp(model: MLPClassifier) -> int: + """Return the number of trained MLPClassifier parameters.""" + weights = model.coefs_ # dtype is list of numpy.ndarrays + biasses = model.intercepts_ # dtype is list of numpy.ndarrays + return sum(a.size for a in weights) + sum(a.size for a in biasses) + + class StructuralAttack(Attack): """Structural attacks based on the static structure of a model.""" # pylint: disable=too-many-instance-attributes - def __init__( # pylint: disable = too-many-arguments + def __init__( self, - attack_config_json_file_name: str = None, + output_dir: str = "outputs", + write_report: bool = True, risk_appetite_config: str = "default", - target_path: str = None, - output_dir: str = "outputs_structural", - report_name: str = "report_structural", ) -> None: """Construct an object to execute a structural attack. Parameters ---------- - attack_config_json_file_name : str - Name of a JSON file containing an attack configuration. - risk_appetite_config : str - Path to yaml file specifying TRE risk appetite. - target_path : str - Path to the saved trained target model and target data. output_dir : str Name of a directory to write outputs. - report_name : str - Name of the pdf and json output reports. + write_report : bool + Whether to generate a JSON and PDF report. + risk_appetite_config : str + Path to yaml file specifying TRE risk appetite. """ - super().__init__() - logger = logging.getLogger("structural_attack") + super().__init__(output_dir=output_dir, write_report=write_report) self.target: Target = None - self.target_path = target_path - self.attack_config_json_file_name = attack_config_json_file_name # disclosure risk - self.k_anonymity_risk = 0 - self.DoF_risk = 0 - self.unnecessary_risk = 0 - self.class_disclosure_risk = 0 - self.lowvals_cd_risk = 0 - self.metadata = {} + self.k_anonymity_risk: bool = False + self.dof_risk: bool = False + self.unnecessary_risk: bool = False + self.class_disclosure_risk: bool = False + self.lowvals_cd_risk: bool = False # make dummy acro object and use it to extract risk appetite myacro = ACRO(risk_appetite_config) self.risk_appetite_config = risk_appetite_config @@ -244,12 +260,10 @@ def __init__( # pylint: disable = too-many-arguments "Thresholds for count %i and Dof %i", self.THRESHOLD, self.DOF_THRESHOLD ) del myacro - if self.attack_config_json_file_name is not None: - self._update_params_from_config_file() # metrics self.attack_metrics = [ - "DoF_risk", + "dof_risk", "k_anonymity_risk", "class_disclosure_risk", "lowvals_cd_risk", @@ -257,15 +271,11 @@ def __init__( # pylint: disable = too-many-arguments ] self.yprobs = [] - # paths for reporting - self.output_dir = output_dir - self.report_name = report_name - def __str__(self) -> str: """Return the name of the attack.""" return "Structural attack" - def attack(self, target: Target) -> None: + def attack(self, target: Target) -> dict: """Run structural attack. To be used when code has access to Target class and trained target model. @@ -274,6 +284,11 @@ def attack(self, target: Target) -> None: ---------- target : attacks.target.Target target as a Target class object + + Returns + ------- + dict + Attack report. """ self.target = target if target.model is None: @@ -305,11 +320,11 @@ def attack(self, target: Target) -> None: # Degrees of Freedom n_params = get_model_param_count(target.model) residual_dof = self.target.X_train.shape[0] - n_params - self.DoF_risk = 1 if residual_dof < self.DOF_THRESHOLD else 0 + self.dof_risk = residual_dof < self.DOF_THRESHOLD # k-anonymity mink = np.min(np.array(equiv_counts)) - self.k_anonymity_risk = 1 if mink < self.THRESHOLD else 0 + self.k_anonymity_risk = mink < self.THRESHOLD # unnecessary risk arising from poor hyper-parameter combination. self.unnecessary_risk = get_unnecessary_risk(self.target.model) @@ -318,9 +333,16 @@ def attack(self, target: Target) -> None: freqs = np.zeros(equiv_classes.shape) for group in range(freqs.shape[0]): freqs = equiv_classes[group] * equiv_counts[group] - self.class_disclosure_risk = np.any(freqs < self.THRESHOLD).astype(int) + self.class_disclosure_risk = np.any(freqs < self.THRESHOLD) freqs[freqs == 0] = 100 - self.lowvals_cd_risk = np.any(freqs < self.THRESHOLD).astype(int) + self.lowvals_cd_risk = np.any(freqs < self.THRESHOLD) + + # create the report + output = self._make_report(target) + # write the report + self._write_report(output) + # return the report + return output def dt_get_equivalence_classes(self) -> tuple: """Get details of equivalence classes based on white box inspection.""" @@ -357,32 +379,26 @@ def _get_global_metrics(self, attack_metrics: list) -> dict: Parameters ---------- - attack_metrics : List - list of attack metrics to be reported. + attack_metrics : list + List of attack metrics to be reported. Returns ------- - global_metrics : Dict - Dictionary of summary metrics + global_metrics : dict + Dictionary of summary metrics. """ global_metrics = {} if attack_metrics is not None and len(attack_metrics) != 0: - global_metrics["DoF_risk"] = self.DoF_risk + global_metrics["dof_risk"] = self.dof_risk global_metrics["k_anonymity_risk"] = self.k_anonymity_risk global_metrics["class_disclosure_risk"] = self.class_disclosure_risk global_metrics["unnecessary_risk"] = self.unnecessary_risk global_metrics["lowvals_cd_risk"] = self.lowvals_cd_risk - return global_metrics def _construct_metadata(self): """Construct the metadata object, after attacks.""" - self.metadata = {} - # Store all args - self.metadata["experiment_details"] = {} - self.metadata["experiment_details"] = self.get_params() - self.metadata["attack"] = str(self) - # Global metrics + super()._construct_metadata() self.metadata["global_metrics"] = self._get_global_metrics(self.attack_metrics) def _get_attack_metrics_instances(self) -> dict: @@ -390,153 +406,13 @@ def _get_attack_metrics_instances(self) -> dict: attack_metrics_experiment = {} attack_metrics_instances = {} attack_metrics_experiment["attack_instance_logger"] = attack_metrics_instances - attack_metrics_experiment["DoF_risk"] = self.DoF_risk + attack_metrics_experiment["dof_risk"] = self.dof_risk attack_metrics_experiment["k_anonymity_risk"] = self.k_anonymity_risk attack_metrics_experiment["class_disclosure_risk"] = self.class_disclosure_risk attack_metrics_experiment["unnecessary_risk"] = self.unnecessary_risk attack_metrics_experiment["lowvals_cd_risk"] = self.lowvals_cd_risk return attack_metrics_experiment - def make_report(self) -> dict: - """Create output dict and generate pdf and json if filenames are given.""" - output = {} - output["log_id"] = str(uuid.uuid4()) - output["log_time"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - self._construct_metadata() - output["metadata"] = self.metadata - output["attack_experiment_logger"] = self._get_attack_metrics_instances() - report_dest = os.path.join(self.output_dir, self.report_name) - json_attack_formatter = GenerateJSONModule(report_dest + ".json") - json_report = report.create_json_report(output) - json_attack_formatter.add_attack_output(json_report, "StructuralAttack") - return output - - -def _run_attack(args) -> None: - """Initialise class and run attack.""" - attack_obj = StructuralAttack( - risk_appetite_config=args.risk_appetite_config, - target_path=args.target_path, - output_dir=args.output_dir, - report_name=args.report_name, - ) - - target = Target() - target.load(attack_obj.target_path) - attack_obj.attack(target) - _ = attack_obj.make_report() - - -def _run_attack_from_configfile(args) -> None: - """Initialise class and run attack using config file.""" - attack_obj = StructuralAttack( - attack_config_json_file_name=str(args.attack_config_json_file_name), - target_path=str(args.target_path), - ) - target = Target() - target.load(attack_obj.target_path) - attack_obj.attack(target) - _ = attack_obj.make_report() - - -def main() -> None: - """Parse arguments and invoke relevant method.""" - logger = logging.getLogger("main") - parser = argparse.ArgumentParser(description="Perform a structural attack") - - subparsers = parser.add_subparsers() - - attack_parser = subparsers.add_parser("run-attack") - - attack_parser.add_argument( - "--output-dir", - type=str, - action="store", - dest="output_dir", - default="output_structural", - required=False, - help=("Directory name where output files are stored. Default = %(default)s."), - ) - - attack_parser.add_argument( - "--report-name", - type=str, - action="store", - dest="report_name", - default="report_structural", - required=False, - help=( - """Filename for the pdf and json report outputs. Default = %(default)s. - Code will append .pdf and .json""" - ), - ) - - attack_parser.add_argument( - "--risk-appetite-filename", - action="store", - type=str, - default="default", - required=False, - dest="risk_appetite_config", - help=( - """provide the name of the dataset-specific risk appetite filename - using --risk-appetite-filename Default = %(default)s""" - ), - ) - - attack_parser.add_argument( - "--target-path", - action="store", - type=str, - default=None, - required=False, - dest="target_path", - help=( - """Provide the path to the stored target usinmg - --target-path option. Default = %(default)f""" - ), - ) - - attack_parser.set_defaults(func=_run_attack) - - attack_parser_config = subparsers.add_parser("run-attack-from-configfile") - attack_parser_config.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="attack_config_json_file_name", - type=str, - default="config_structural_cmd.json", - help=( - "Name of the .json file containing details for the run. Default = %(default)s" - ), - ) - - attack_parser_config.add_argument( - "-t", - "--attack-target-folder-path", - action="store", - required=True, - dest="target_path", - type=str, - default="structural_target", - help=( - """Name of the target directory to load the trained target model and the target data. - Default = %(default)s""" - ), - ) - - attack_parser_config.set_defaults(func=_run_attack_from_configfile) - - args = parser.parse_args() - - try: - args.func(args) - except AttributeError as e: # pragma:no cover - logger.error("Invalid command. Try --help to get more details") - logger.error(e) - - -if __name__ == "__main__": # pragma:no cover - main() + def _make_pdf(self, output: dict) -> None: + attack_name: str = output["metadata"]["attack_name"] + logger.info("PDF report not yet implemented for %s", attack_name) diff --git a/aisdc/attacks/target.py b/aisdc/attacks/target.py index 0f3e977e..afc31311 100644 --- a/aisdc/attacks/target.py +++ b/aisdc/attacks/target.py @@ -2,86 +2,109 @@ from __future__ import annotations -import json import logging import os import pickle import numpy as np import sklearn - -from aisdc.attacks.report import NumpyArrayEncoder +import yaml logging.basicConfig(level=logging.INFO) -logger = logging.getLogger("target") +logger = logging.getLogger(__name__) class Target: # pylint: disable=too-many-instance-attributes """Store information about the target model and data.""" - def __init__(self, model: sklearn.base.BaseEstimator | None = None) -> None: + def __init__( # pylint: disable=too-many-arguments, too-many-locals + self, + model: sklearn.base.BaseEstimator | None = None, + dataset_name: str = "", + features: dict | None = None, + X_train: np.ndarray | None = None, + y_train: np.ndarray | None = None, + X_test: np.ndarray | None = None, + y_test: np.ndarray | None = None, + X_orig: np.ndarray | None = None, + y_orig: np.ndarray | None = None, + X_train_orig: np.ndarray | None = None, + y_train_orig: np.ndarray | None = None, + X_test_orig: np.ndarray | None = None, + y_test_orig: np.ndarray | None = None, + proba_train: np.ndarray | None = None, + proba_test: np.ndarray | None = None, + ) -> None: """Store information about a target model and associated data. Parameters ---------- - model : sklearn.base.BaseEstimator | None + model : sklearn.base.BaseEstimator | None, optional Trained target model. Any class that implements the sklearn.base.BaseEstimator interface (i.e. has fit, predict and predict_proba methods) - - Attributes - ---------- - name : str + dataset_name : str The name of the dataset. - n_samples : int - The total number of samples in the dataset. - X_train : np.ndarray + features : dict + Dictionary describing the dataset features. + X_train : np.ndarray | None The (processed) training inputs. - y_train : np.ndarray + y_train : np.ndarray | None The (processed) training outputs. - X_test : np.ndarray + X_test : np.ndarray | None The (processed) testing inputs. - y_test : np.ndarray + y_test : np.ndarray | None The (processed) testing outputs. - features : dict - Dictionary describing the dataset features. - n_features : int - The total number of features. - X_orig : np.ndarray + X_orig : np.ndarray | None The original (unprocessed) dataset inputs. - y_orig : np.ndarray + y_orig : np.ndarray | None The original (unprocessed) dataset outputs. - X_train_orig : np.ndarray + X_train_orig : np.ndarray | None The original (unprocessed) training inputs. - y_train_orig : np.ndarray + y_train_orig : np.ndarray | None The original (unprocessed) training outputs. - X_test_orig : np.ndarray + X_test_orig : np.ndarray | None The original (unprocessed) testing inputs. - y_test_orig : np.ndarray + y_test_orig : np.ndarray | None The original (unprocessed) testing outputs. - n_samples_orig : int - The total number of samples in the original dataset. - model : sklearn.base.BaseEstimator | None - The trained model. - safemodel : list - The results of safemodel disclosure checks. + proba_train : np.ndarray | None + The model predicted training probabilities. + proba_test : np.ndarray | None + The model predicted testing probabilities. """ - self.name: str = "" + # Model - details + self.model: sklearn.base.BaseEstimator | None = model + self.model_name: str = "unknown" + self.model_params: dict = {} + if self.model is not None: + self.model_name = type(self.model).__name__ + self.model_params = self.model.get_params() + # Model - predicted probabilities + self.proba_train: np.ndarray | None = proba_train + self.proba_test: np.ndarray | None = proba_test + # Dataset - details + self.dataset_name: str = dataset_name + # Dataset - processed + self.X_train: np.ndarray | None = X_train + self.y_train: np.ndarray | None = y_train + self.X_test: np.ndarray | None = X_test + self.y_test: np.ndarray | None = y_test self.n_samples: int = 0 - self.X_train: np.ndarray - self.y_train: np.ndarray - self.X_test: np.ndarray - self.y_test: np.ndarray - self.features: dict = {} - self.n_features: int = 0 - self.X_orig: np.ndarray - self.y_orig: np.ndarray - self.X_train_orig: np.ndarray - self.y_train_orig: np.ndarray - self.X_test_orig: np.ndarray - self.y_test_orig: np.ndarray + if X_train is not None and X_test is not None: + self.n_samples = len(X_train) + len(X_test) + # Dataset - unprocessed + self.X_orig: np.ndarray | None = X_orig + self.y_orig: np.ndarray | None = y_orig + self.X_train_orig: np.ndarray | None = X_train_orig + self.y_train_orig: np.ndarray | None = y_train_orig + self.X_test_orig: np.ndarray | None = X_test_orig + self.y_test_orig: np.ndarray | None = y_test_orig self.n_samples_orig: int = 0 - self.model: sklearn.base.BaseEstimator | None = model + if X_train_orig is not None and X_test_orig is not None: + self.n_samples_orig = len(X_train_orig) + len(X_test_orig) + self.features: dict = features if features is not None else {} + self.n_features: int = len(self.features) + # Safemodel report self.safemodel: list = [] def add_processed_data( @@ -126,7 +149,7 @@ def add_raw_data( # pylint: disable=too-many-arguments self.y_test_orig = y_test_orig self.n_samples_orig = len(X_orig) - def __save_model(self, path: str, ext: str, target: dict) -> None: + def _save_model(self, path: str, ext: str, target: dict) -> None: """Save the target model. Parameters @@ -136,7 +159,7 @@ def __save_model(self, path: str, ext: str, target: dict) -> None: ext : str File extension defining the model saved format, e.g., "pkl" or "sav". target : dict - Target class as a dictionary for writing JSON. + Target class as a dictionary for writing yaml. """ # write model filename: str = os.path.normpath(f"{path}/model.{ext}") @@ -149,31 +172,28 @@ def __save_model(self, path: str, ext: str, target: dict) -> None: raise ValueError(f"Unsupported file format for saving a model: {ext}") target["model_path"] = f"model.{ext}" # write hyperparameters - try: - target["model_name"] = type(self.model).__name__ - target["model_params"] = self.model.get_params() - except Exception: # pragma: no cover pylint: disable=broad-exception-caught - pass + target["model_name"] = self.model_name + target["model_params"] = self.model_params - def __load_model(self, path: str, target: dict) -> None: + def load_model(self, model_path: str) -> None: """Load the target model. Parameters ---------- - path : str + model_path : str Path to load the model. - target : dict - Target class as a dictionary read from JSON. """ - model_path = os.path.normpath(f"{path}/{target['model_path']}") - _, ext = os.path.splitext(model_path) + path = os.path.normpath(model_path) + _, ext = os.path.splitext(path) if ext == ".pkl": - with open(model_path, "rb") as fp: + with open(path, "rb") as fp: self.model = pickle.load(fp) + model_type = type(self.model) + logger.info("Loaded: %s", model_type.__name__) else: # pragma: no cover raise ValueError(f"Unsupported file format for loading a model: {ext}") - def __save_numpy(self, path: str, target: dict, name: str) -> None: + def _save_numpy(self, path: str, target: dict, name: str) -> None: """Save a numpy array variable as pickle. Parameters @@ -181,36 +201,56 @@ def __save_numpy(self, path: str, target: dict, name: str) -> None: path : str Path to save the data. target : dict - Target class as a dictionary for writing JSON. + Target class as a dictionary for writing yaml. name : str Name of the numpy array to save. """ - if hasattr(self, name): + if getattr(self, name) is not None: np_path: str = os.path.normpath(f"{path}/{name}.pkl") target[f"{name}_path"] = f"{name}.pkl" with open(np_path, "wb") as fp: pickle.dump(getattr(self, name), fp, protocol=pickle.HIGHEST_PROTOCOL) + else: + target[f"{name}_path"] = "" - def __load_numpy(self, path: str, target: dict, name: str) -> None: - """Load a numpy array variable from pickle. + def load_array(self, arr_path: str, name: str) -> None: + """Load a data array variable from file. Parameters ---------- - path : str - Path to load the data. - target : dict - Target class as a dictionary read from JSON. + arr_path : str + Filename of a data array. name : str - Name of the numpy array to load. + Name of the data array to load. """ - key: str = f"{name}_path" - if key in target: - np_path: str = os.path.normpath(f"{path}/{target[key]}") - with open(np_path, "rb") as fp: + path = os.path.normpath(arr_path) + with open(path, "rb") as fp: + _, ext = os.path.splitext(path) + if ext == ".pkl": arr = pickle.load(fp) setattr(self, name, arr) + logger.info("%s shape: %s", name, arr.shape) + else: + raise ValueError(f"Target cannot load {ext} files.") + + def _load_array(self, arr_path: str, target: dict, name: str) -> None: + """Load a data array variable contained in a yaml config. + + Parameters + ---------- + arr_path : str + Filename of a data array. + target : dict + Target class as a dictionary read from yaml. + name : str + Name of the data array to load. + """ + key = f"{name}_path" + if key in target and target[key] != "": + path = f"{arr_path}/{target[key]}" + self.load_array(path, name) - def __save_data(self, path: str, target: dict) -> None: + def _save_data(self, path: str, target: dict) -> None: """Save the target model data. Parameters @@ -218,20 +258,20 @@ def __save_data(self, path: str, target: dict) -> None: path : str Path to save the data. target : dict - Target class as a dictionary for writing JSON. + Target class as a dictionary for writing yaml. """ - self.__save_numpy(path, target, "X_train") - self.__save_numpy(path, target, "y_train") - self.__save_numpy(path, target, "X_test") - self.__save_numpy(path, target, "y_test") - self.__save_numpy(path, target, "X_orig") - self.__save_numpy(path, target, "y_orig") - self.__save_numpy(path, target, "X_train_orig") - self.__save_numpy(path, target, "y_train_orig") - self.__save_numpy(path, target, "X_test_orig") - self.__save_numpy(path, target, "y_test_orig") - - def __load_data(self, path: str, target: dict) -> None: + self._save_numpy(path, target, "X_train") + self._save_numpy(path, target, "y_train") + self._save_numpy(path, target, "X_test") + self._save_numpy(path, target, "y_test") + self._save_numpy(path, target, "X_orig") + self._save_numpy(path, target, "y_orig") + self._save_numpy(path, target, "X_train_orig") + self._save_numpy(path, target, "y_train_orig") + self._save_numpy(path, target, "X_test_orig") + self._save_numpy(path, target, "y_test_orig") + + def _load_data(self, path: str, target: dict) -> None: """Load the target model data. Parameters @@ -239,41 +279,41 @@ def __load_data(self, path: str, target: dict) -> None: path : str Path to load the data. target : dict - Target class as a dictionary read from JSON. + Target class as a dictionary read from yaml. """ - self.__load_numpy(path, target, "X_train") - self.__load_numpy(path, target, "y_train") - self.__load_numpy(path, target, "X_test") - self.__load_numpy(path, target, "y_test") - self.__load_numpy(path, target, "X_orig") - self.__load_numpy(path, target, "y_orig") - self.__load_numpy(path, target, "X_train_orig") - self.__load_numpy(path, target, "y_train_orig") - self.__load_numpy(path, target, "X_test_orig") - self.__load_numpy(path, target, "y_test_orig") - - def __ge(self) -> str: + self._load_array(path, target, "X_train") + self._load_array(path, target, "y_train") + self._load_array(path, target, "X_test") + self._load_array(path, target, "y_test") + self._load_array(path, target, "X_orig") + self._load_array(path, target, "y_orig") + self._load_array(path, target, "X_train_orig") + self._load_array(path, target, "y_train_orig") + self._load_array(path, target, "X_test_orig") + self._load_array(path, target, "y_test_orig") + + def _ge(self) -> float: """Return the model generalisation error. Returns ------- - str + float Generalisation error. """ if ( hasattr(self.model, "score") - and hasattr(self, "X_train") - and hasattr(self, "y_train") - and hasattr(self, "X_test") - and hasattr(self, "y_test") + and self.X_train is not None + and self.y_train is not None + and self.X_test is not None + and self.y_test is not None ): try: train = self.model.score(self.X_train, self.y_train) test = self.model.score(self.X_test, self.y_test) - return str(test - train) + return test - train except sklearn.exceptions.NotFittedError: - return "not fitted" - return "unknown" + return np.NaN + return np.NaN def save(self, path: str = "target", ext: str = "pkl") -> None: """Save the target class to persistent storage. @@ -286,26 +326,26 @@ def save(self, path: str = "target", ext: str = "pkl") -> None: File extension defining the model saved format, e.g., "pkl" or "sav". """ path: str = os.path.normpath(path) - filename: str = os.path.normpath(f"{path}/target.json") + filename: str = os.path.normpath(f"{path}/target.yaml") os.makedirs(os.path.dirname(filename), exist_ok=True) - # convert Target to JSON + # convert Target to dict target: dict = { - "data_name": self.name, + "dataset_name": self.dataset_name, "n_samples": self.n_samples, "features": self.features, "n_features": self.n_features, "n_samples_orig": self.n_samples_orig, - "generalisation_error": self.__ge(), + "generalisation_error": self._ge(), "safemodel": self.safemodel, } - # write model and add path to JSON + # write model and add path if self.model is not None: - self.__save_model(path, ext, target) - # write data arrays and add paths to JSON - self.__save_data(path, target) - # write JSON - with open(filename, "w", newline="", encoding="utf-8") as fp: - json.dump(target, fp, indent=4, cls=NumpyArrayEncoder) + self._save_model(path, ext, target) + # write data arrays and add paths + self._save_data(path, target) + # write yaml + with open(filename, "w", encoding="utf-8") as fp: + yaml.dump(target, fp, default_flow_style=False, sort_keys=False) def load(self, path: str = "target") -> None: """Load the target class from persistent storage. @@ -313,16 +353,17 @@ def load(self, path: str = "target") -> None: Parameters ---------- path : str - Name of the output folder containing a target JSON file. + Name of the output folder containing a target yaml file. """ target: dict = {} - # load JSON - filename: str = os.path.normpath(f"{path}/target.json") + # load yaml + filename: str = os.path.normpath(f"{path}/target.yaml") with open(filename, encoding="utf-8") as fp: - target = json.load(fp) + target = yaml.safe_load(fp) # load parameters - if "data_name" in target: - self.name = target["data_name"] + if "dataset_name" in target: + self.dataset_name = target["dataset_name"] + logger.info("dataset_name: %s", self.dataset_name) if "n_samples" in target: self.n_samples = target["n_samples"] if "features" in target: @@ -331,15 +372,21 @@ def load(self, path: str = "target") -> None: self.features = {int(key): value for key, value in features.items()} if "n_features" in target: self.n_features = target["n_features"] + logger.info("n_features: %d", self.n_features) if "n_samples_orig" in target: self.n_samples_orig = target["n_samples_orig"] if "safemodel" in target: self.safemodel = target["safemodel"] # load model + if "model_name" in target: + self.model_name = target["model_name"] + if "model_params" in target: + self.model_params = target["model_params"] if "model_path" in target: - self.__load_model(path, target) + model_path = os.path.normpath(f"{path}/{target['model_path']}") + self.load_model(model_path) # load data - self.__load_data(path, target) + self._load_data(path, target) def add_safemodel_results(self, data: list) -> None: """Add the results of safemodel disclosure checking. @@ -353,4 +400,4 @@ def add_safemodel_results(self, data: list) -> None: def __str__(self) -> str: """Return the name of the dataset used.""" - return self.name + return self.dataset_name diff --git a/aisdc/attacks/worst_case_attack.py b/aisdc/attacks/worst_case_attack.py index 17a6f866..c5114841 100644 --- a/aisdc/attacks/worst_case_attack.py +++ b/aisdc/attacks/worst_case_attack.py @@ -2,38 +2,32 @@ from __future__ import annotations -import argparse import logging -import os -import uuid from collections.abc import Iterable -from datetime import datetime -from typing import Any import numpy as np -from sklearn.ensemble import RandomForestClassifier +from fpdf import FPDF from sklearn.metrics import confusion_matrix from sklearn.model_selection import train_test_split from aisdc import metrics from aisdc.attacks import report -from aisdc.attacks.attack import Attack -from aisdc.attacks.attack_report_formatter import GenerateJSONModule -from aisdc.attacks.failfast import FailFast +from aisdc.attacks.attack import Attack, get_class_by_name from aisdc.attacks.target import Target logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) P_THRESH = 0.05 -class WorstCaseAttack(Attack): +class WorstCaseAttack(Attack): # pylint: disable=too-many-instance-attributes """Worst case attack.""" - # pylint: disable=too-many-instance-attributes - - def __init__( # pylint: disable = too-many-arguments, too-many-locals, too-many-statements + def __init__( # pylint: disable = too-many-arguments self, + output_dir: str = "outputs", + write_report: bool = True, n_reps: int = 10, reproduce_split: int | Iterable[int] | None = 5, p_thresh: float = 0.05, @@ -41,236 +35,155 @@ def __init__( # pylint: disable = too-many-arguments, too-many-locals, too-many train_beta: int = 1, test_beta: int = 1, test_prop: float = 0.2, - n_rows_in: int = 1000, - n_rows_out: int = 1000, - training_preds_filename: str = None, - test_preds_filename: str = None, - output_dir: str = "output_worstcase", - report_name: str = "report_worstcase", include_model_correct_feature: bool = False, sort_probs: bool = True, - mia_attack_model: Any = RandomForestClassifier, - mia_attack_model_hyp: dict = None, - attack_metric_success_name: str = "P_HIGHER_AUC", - attack_metric_success_thresh: float = 0.05, - attack_metric_success_comp_type: str = "lte", - attack_metric_success_count_thresh: int = 5, - attack_fail_fast: bool = False, - attack_config_json_file_name: str = None, - target_path: str = None, + attack_model: str = "sklearn.ensemble.RandomForestClassifier", + attack_model_params: dict | None = None, ) -> None: """Construct an object to execute a worst case attack. Parameters ---------- + output_dir : str + Name of the directory where outputs are stored. + write_report : bool + Whether to generate a JSON and PDF report. n_reps : int - number of attacks to run -- in each iteration an attack model - is trained on a different subset of the data - reproduce_split : int - variable that controls the reproducibility of the data split. - It can be an integer or a list of integers of length n_reps. Default : 5. + Number of attacks to run -- in each iteration an attack model + is trained on a different subset of the data. + reproduce_split : int or Iterable[int] or None + Variable that controls the reproducibility of the data split. + It can be an integer or a list of integers of length `n_reps`. + Default : 5. p_thresh : float - threshold to determine significance of things. For instance auc_p_value and pdif_vals + Threshold to determine significance of things. For instance + `auc_p_value` and `pdif_vals`. n_dummy_reps : int - number of baseline (dummy) experiments to do + Number of baseline (dummy) experiments to do. train_beta : int - value of b for beta distribution used to sample the in-sample (training) probabilities + Value of b for beta distribution used to sample the in-sample + (training) probabilities. test_beta : int - value of b for beta distribution used to sample the out-of-sample (test) probabilities + Value of b for beta distribution used to sample the out-of-sample + (test) probabilities. test_prop : float - proportion of data to use as a test set for the attack model - n_rows_in : int - number of rows for in-sample (training data) - n_rows_out : int - number of rows for out-of-sample (test data) - training_preds_filename : str - name of the file to keep predictions of the training data (in-sample) - test_preds_filename : str - name of the file to keep predictions of the test data (out-of-sample) - output_dir : str - name of the directory where outputs are stored - report_name : str - name of the pdf and json output reports + Proportion of data to use as a test set for the attack model. include_model_correct_feature : bool - inclusion of additional feature to hold whether or not the target model - made a correct prediction for each example + Inclusion of additional feature to hold whether or not the target model + made a correct prediction for each example. sort_probs : bool - true in case require to sort combine preds (from training and test) - to have highest probabilities in the first column - mia_attack_model : Any - name of the attack model such as RandomForestClassifier - mia_attack_model_hyp : dict - dictionary of hyper parameters for the mia_attack_model - such as min_sample_split, min_samples_leaf etc - attack_metric_success_name : str - name of metric to compute for the attack being successful - attack_metric_success_thresh : float - threshold for a given metric to measure attack being successful or not - attack_metric_success_comp_type : str - threshold comparison operator (i.e., gte: greater than or equal to, gt: - greater than, lte: less than or equal to, lt: less than, - eq: equal to and not_eq: not equal to) - attack_metric_success_count_thresh : int - a counter to record how many times an attack was successful - given that the threshold has fulfilled criteria for a given comparison type - attack_fail_fast : bool - If true it stops repetitions earlier based on the given attack metric - (i.e., attack_metric_success_name) considering the comparison type - (attack_metric_success_comp_type) satisfying a threshold - (i.e., attack_metric_success_thresh) for n - (attack_metric_success_count_thresh) number of times - attack_config_json_file_name : str - name of the configuration file to load parameters - target_path : str - path to the saved trained target model and target data + Whether to sort combined preds (from training and test) + to have highest probabilities in the first column. + attack_model : str + Class name of the attack model. + attack_model_params : dict or None + Dictionary of hyperparameters for the `attack_model` + such as `min_sample_split`, `min_samples_leaf`, etc. """ - super().__init__() - self.n_reps = n_reps - self.reproduce_split = reproduce_split - if isinstance(reproduce_split, int): - reproduce_split = [reproduce_split] + [ - x**2 for x in range(reproduce_split, reproduce_split + n_reps - 1) - ] - else: - # remove potential duplicates - reproduce_split = list(dict.fromkeys(reproduce_split)) - if len(reproduce_split) == n_reps: - pass - elif len(reproduce_split) > n_reps: - print("split", reproduce_split, "nreps", n_reps) - reproduce_split = list(reproduce_split)[0:n_reps] - print( - "WARNING: the length of the parameter 'reproduce_split'\ - is longer than n_reps. Values have been removed." - ) - else: - # assign values to match length of n_reps - reproduce_split += [ - reproduce_split[-1] * x - for x in range(2, (n_reps - len(reproduce_split) + 2)) - ] - print( - "WARNING: the length of the parameter 'reproduce_split'\ - is shorter than n_reps. Vales have been added." - ) - print("reproduce split now", reproduce_split) - self.reproduce_split = reproduce_split - self.p_thresh = p_thresh - self.n_dummy_reps = n_dummy_reps - self.train_beta = train_beta - self.test_beta = test_beta - self.test_prop = test_prop - self.n_rows_in = n_rows_in - self.n_rows_out = n_rows_out - self.training_preds_filename = training_preds_filename - self.test_preds_filename = test_preds_filename - self.output_dir = output_dir - self.report_name = report_name - self.include_model_correct_feature = include_model_correct_feature - self.sort_probs = sort_probs - self.mia_attack_model = mia_attack_model - if mia_attack_model_hyp is None: - self.mia_attack_model_hyp = { - "min_samples_split": 20, - "min_samples_leaf": 10, - "max_depth": 5, - } - else: - self.mia_attack_model_hyp = mia_attack_model_hyp - self.attack_metric_success_name = attack_metric_success_name - self.attack_metric_success_thresh = attack_metric_success_thresh - self.attack_metric_success_comp_type = attack_metric_success_comp_type - self.attack_metric_success_count_thresh = attack_metric_success_count_thresh - self.attack_fail_fast = attack_fail_fast - self.attack_config_json_file_name = attack_config_json_file_name - self.target_path = target_path - # Updating parameters from a configuration json file - if self.attack_config_json_file_name is not None: - self._update_params_from_config_file() - if not os.path.exists(self.output_dir): - os.makedirs(self.output_dir) - self.attack_metrics = None - self.attack_metric_failfast_summary = None - self.dummy_attack_metrics = None - self.dummy_attack_metric_failfast_summary = None - self.metadata = None + super().__init__(output_dir=output_dir, write_report=write_report) + self.n_reps: int = n_reps + self.reproduce_split: int | Iterable[int] | None = reproduce_split + self.p_thresh: float = p_thresh + self.n_dummy_reps: int = n_dummy_reps + self.train_beta: int = train_beta + self.test_beta: int = test_beta + self.test_prop: float = test_prop + self.include_model_correct_feature: bool = include_model_correct_feature + self.sort_probs: bool = sort_probs + self.attack_model: str = attack_model + self.attack_model_params: dict | None = attack_model_params + self.dummy_attack_metrics: list = [] def __str__(self) -> str: """Return name of attack.""" return "WorstCase attack" - def attack(self, target: Target) -> None: + def attack(self, target: Target) -> dict: """Run worst case attack. - To be used when code has access to Target class and trained target model. - Parameters ---------- target : attacks.target.Target target as a Target class object - """ - train_preds = target.model.predict_proba(target.X_train) - test_preds = target.model.predict_proba(target.X_test) - train_correct = None - test_correct = None - if self.include_model_correct_feature: - train_correct = 1 * (target.y_train == target.model.predict(target.X_train)) - test_correct = 1 * (target.y_test == target.model.predict(target.X_test)) + Returns + ------- + dict + Attack report. + """ + train_c = None + test_c = None + # compute target model probas if possible + if ( + target.model is not None + and target.X_train is not None + and target.y_train is not None + ): + proba_train = target.model.predict_proba(target.X_train) + proba_test = target.model.predict_proba(target.X_test) + if self.include_model_correct_feature: + train_c = 1 * (target.y_train == target.model.predict(target.X_train)) + test_c = 1 * (target.y_test == target.model.predict(target.X_test)) + # use supplied target model probas if unable to compute + elif target.proba_train is not None and target.proba_test is not None: + proba_train = target.proba_train + proba_test = target.proba_test + # cannot proceed + else: + logger.info("Insufficient Target details to run worst case attack.") + return {} + # execute attack self.attack_from_preds( - train_preds, - test_preds, - train_correct=train_correct, - test_correct=test_correct, + proba_train, + proba_test, + train_correct=train_c, + test_correct=test_c, ) + # create the report + output = self._make_report(target) + # write the report + self._write_report(output) + # return the report + return output - def attack_from_prediction_files(self) -> None: - """Run attack from saved prediction files. - - To be used when only saved predictions are available. - - Filenames for the saved prediction files to be specified in the - arguments provided in the constructor. - """ - train_preds = np.loadtxt(self.training_preds_filename, delimiter=",") - test_preds = np.loadtxt(self.test_preds_filename, delimiter=",") - self.attack_from_preds(train_preds, test_preds) + def _make_report(self, target: Target) -> dict: + """Create attack report.""" + output = super()._make_report(target) + output["dummy_attack_experiments_logger"] = ( + self._get_dummy_attack_metrics_experiments_instances() + ) + return output def attack_from_preds( self, - train_preds: np.ndarray, - test_preds: np.ndarray, - train_correct: np.ndarray = None, - test_correct: np.ndarray = None, + proba_train: np.ndarray, + proba_test: np.ndarray, + train_correct: np.ndarray | None = None, + test_correct: np.ndarray | None = None, ) -> None: - """Run attack based upon the predictions in train_preds and test_preds. + """Run attack based upon the predictions in proba_train and proba_test. Parameters ---------- - train_preds : np.ndarray + proba_train : np.ndarray Array of train predictions. One row per example, one column per class. - test_preds : np.ndarray + proba_test : np.ndarray Array of test predictions. One row per example, one column per class. """ - logger = logging.getLogger("attack-from-preds") logger.info("Running main attack repetitions") attack_metric_dict = self.run_attack_reps( - train_preds, - test_preds, + proba_train, + proba_test, train_correct=train_correct, test_correct=test_correct, ) self.attack_metrics = attack_metric_dict["mia_metrics"] - self.attack_metric_failfast_summary = attack_metric_dict[ - "failfast_metric_summary" - ] self.dummy_attack_metrics = [] - self.dummy_attack_metric_failfast_summary = [] if self.n_dummy_reps > 0: logger.info("Running dummy attack reps") - n_train_rows = len(train_preds) - n_test_rows = len(test_preds) + n_train_rows = len(proba_train) + n_test_rows = len(proba_test) for _ in range(self.n_dummy_reps): d_train_preds, d_test_preds = self.generate_arrays( n_train_rows, @@ -282,21 +195,14 @@ def attack_from_preds( d_train_preds, d_test_preds ) temp_metrics = temp_attack_metric_dict["mia_metrics"] - temp_metric_failfast_summary = temp_attack_metric_dict[ - "failfast_metric_summary" - ] - self.dummy_attack_metrics.append(temp_metrics) - self.dummy_attack_metric_failfast_summary.append( - temp_metric_failfast_summary - ) logger.info("Finished running attacks") def _prepare_attack_data( self, - train_preds: np.ndarray, - test_preds: np.ndarray, + proba_train: np.ndarray, + proba_test: np.ndarray, train_correct: np.ndarray = None, test_correct: np.ndarray = None, ) -> tuple[np.ndarray, np.ndarray]: @@ -307,27 +213,70 @@ def _prepare_attack_data( first column. Constructs a label array that has ones corresponding to training rows and zeros to testing rows. """ - logger = logging.getLogger("prep-attack-data") if self.sort_probs: logger.info("Sorting probabilities to leave highest value in first column") - train_preds = -np.sort(-train_preds, axis=1) - test_preds = -np.sort(-test_preds, axis=1) + proba_train = -np.sort(-proba_train, axis=1) + proba_test = -np.sort(-proba_test, axis=1) logger.info("Creating MIA data") if self.include_model_correct_feature and train_correct is not None: - train_preds = np.hstack((train_preds, train_correct[:, None])) - test_preds = np.hstack((test_preds, test_correct[:, None])) - - mi_x = np.vstack((train_preds, test_preds)) - mi_y = np.hstack((np.ones(len(train_preds)), np.zeros(len(test_preds)))) + proba_train = np.hstack((proba_train, train_correct[:, None])) + proba_test = np.hstack((proba_test, test_correct[:, None])) + mi_x = np.vstack((proba_train, proba_test)) + mi_y = np.hstack((np.ones(len(proba_train)), np.zeros(len(proba_test)))) return (mi_x, mi_y) + def _get_attack_model(self): + """Return an instantiated attack model.""" + # load attack model module and get class + model = get_class_by_name(self.attack_model) + params = self.attack_model_params + if ( # set custom default parameters for RF attack model + self.attack_model == "sklearn.ensemble.RandomForestClassifier" + and self.attack_model_params is None + ): + params = { + "min_samples_split": 20, + "min_samples_leaf": 10, + "max_depth": 5, + } + # instantiate attack model + return model(**params) if params is not None else model() + + def _get_reproducible_split(self) -> list: + """Return a list of splits.""" + split = self.reproduce_split + n_reps = self.n_reps + if isinstance(split, int): + split = [split] + [x**2 for x in range(split, split + n_reps - 1)] + else: + # remove potential duplicates + split = list(dict.fromkeys(split)) + if len(split) == n_reps: + pass + elif len(split) > n_reps: + print("split", split, "nreps", n_reps) + split = list(split)[0:n_reps] + print( + "WARNING: the length of the parameter 'reproduce_split' " + "is longer than n_reps. Values have been removed." + ) + else: + # assign values to match length of n_reps + split += [split[-1] * x for x in range(2, (n_reps - len(split) + 2))] + print( + "WARNING: the length of the parameter 'reproduce_split' " + "is shorter than n_reps. Values have been added." + ) + print("reproduce split now", split) + return split + def run_attack_reps( # pylint: disable = too-many-locals self, - train_preds: np.ndarray, - test_preds: np.ndarray, + proba_train: np.ndarray, + proba_test: np.ndarray, train_correct: np.ndarray = None, test_correct: np.ndarray = None, ) -> dict: @@ -335,43 +284,36 @@ def run_attack_reps( # pylint: disable = too-many-locals Parameters ---------- - train_preds : np.ndarray - predictions from the model on training (in-sample) data - test_preds : np.ndarray - predictions from the model on testing (out-of-sample) data + proba_train : np.ndarray + Predictions from the model on training (in-sample) data. + proba_test : np.ndarray + Predictions from the model on testing (out-of-sample) data. Returns ------- - mia_metrics_dict : dict - a dictionary with two items including mia_metrics - (a list of metric across repetitions) and failfast_metric_summary object - (an object of FailFast class) to maintain summary of - fail/success of attacks for a given metric of failfast option + dict + Dictionary of mia_metrics (a list of metric across repetitions). """ - self.n_rows_in = len(train_preds) - self.n_rows_out = len(test_preds) - logger = logging.getLogger("attack-reps") mi_x, mi_y = self._prepare_attack_data( - train_preds, test_preds, train_correct, test_correct + proba_train, proba_test, train_correct, test_correct ) mia_metrics = [] - - failfast_metric_summary = FailFast(self) + split = self._get_reproducible_split() for rep in range(self.n_reps): - logger.info( - "Rep %d of %d split %d", rep + 1, self.n_reps, self.reproduce_split[rep] - ) + logger.info("Rep %d of %d split %d", rep + 1, self.n_reps, split[rep]) + mi_train_x, mi_test_x, mi_train_y, mi_test_y = train_test_split( mi_x, mi_y, test_size=self.test_prop, stratify=mi_y, - random_state=self.reproduce_split[rep], + random_state=split[rep], shuffle=True, ) - attack_classifier = self.mia_attack_model(**self.mia_attack_model_hyp) + + attack_classifier = self._get_attack_model() attack_classifier.fit(mi_train_x, mi_train_y) y_pred_proba = attack_classifier.predict_proba(mi_test_x) @@ -387,21 +329,8 @@ def run_attack_reps( # pylint: disable = too-many-locals mia_metrics[-1]["yeom_tpr"] - mia_metrics[-1]["yeom_fpr"] ) - failfast_metric_summary.check_attack_success(mia_metrics[rep]) - - if ( - failfast_metric_summary.check_overall_attack_success(self) - and self.attack_fail_fast - ): - break - logger.info("Finished simulating attacks") - - mia_metrics_dict = {} - mia_metrics_dict["mia_metrics"] = mia_metrics - mia_metrics_dict["failfast_metric_summary"] = failfast_metric_summary - - return mia_metrics_dict + return {"mia_metrics": mia_metrics} def _get_global_metrics(self, attack_metrics: list) -> dict: """Summarise metrics from a metric list. @@ -453,7 +382,7 @@ def _get_global_metrics(self, attack_metrics: list) -> dict: def _get_n_significant( self, p_val_list: list[float], p_thresh: float, bh_fdr_correction: bool = False ) -> int: - """Return number of p-values significant at p_thresh. + """Return number of p-values significant at `p_thresh`. Can perform multiple testing correction. """ @@ -471,14 +400,14 @@ def _generate_array(self, n_rows: int, beta: float) -> np.ndarray: Parameters ---------- n_rows : int - the number of rows worth of data to generate + The number of rows worth of data to generate. beta : float - the beta parameter for sampling probabilities + The beta parameter for sampling probabilities. Returns ------- preds : np.ndarray - Array of predictions. Two columns, n_rows rows + Array of predictions. Two columns, `n_rows` rows. """ preds = np.zeros((n_rows, 2), float) for row_idx in range(n_rows): @@ -500,59 +429,29 @@ def generate_arrays( Parameters ---------- n_rows_in : int - number of rows of in-sample (training) probabilities + Number of rows of in-sample (training) probabilities. n_rows_out : int - number of rows of out-of-sample (testing) probabilities + Number of rows of out-of-sample (testing) probabilities. train_beta : float - beta value for generating train probabilities + Beta value for generating train probabilities. test_beta : float: - beta_value for generating test probabilities + Beta value for generating test probabilities. Returns ------- - train_preds : np.ndarray - Array of train predictions (n_rows x 2 columns) - test_preds : np.ndarray - Array of test predictions (n_rows x 2 columns) - """ - train_preds = self._generate_array(n_rows_in, train_beta) - test_preds = self._generate_array(n_rows_out, test_beta) - return train_preds, test_preds - - def make_dummy_data(self) -> None: - """Make dummy data for testing functionality. - - Notes - ----- - Returns nothing but saves two .csv files. + proba_train : np.ndarray + Array of train predictions (n_rows x 2 columns). + proba_test : np.ndarray + Array of test predictions (n_rows x 2 columns). """ - logger = logging.getLogger("dummy-data") - logger.info( - "Making dummy data with %d rows in and %d out", - self.n_rows_in, - self.n_rows_out, - ) - logger.info("Generating rows") - train_preds, test_preds = self.generate_arrays( - self.n_rows_in, - self.n_rows_out, - train_beta=self.train_beta, - test_beta=self.test_beta, - ) - logger.info("Saving files") - np.savetxt(self.training_preds_filename, train_preds, delimiter=",") - np.savetxt(self.test_preds_filename, test_preds, delimiter=",") + proba_train = self._generate_array(n_rows_in, train_beta) + proba_test = self._generate_array(n_rows_out, test_beta) + return proba_train, proba_test def _construct_metadata(self) -> None: """Construct the metadata object after attacks.""" - self.metadata = {} - # Store all args - self.metadata["experiment_details"] = {} - self.metadata["experiment_details"] = self.get_params() - - self.metadata["attack"] = str(self) + super()._construct_metadata() - # Global metrics self.metadata["global_metrics"] = self._get_global_metrics(self.attack_metrics) self.metadata["baseline_global_metrics"] = self._get_global_metrics( self._unpack_dummy_attack_metrics_experiments_instances() @@ -561,32 +460,23 @@ def _construct_metadata(self) -> None: def _unpack_dummy_attack_metrics_experiments_instances(self) -> list: """Construct the metadata object after attacks.""" dummy_attack_metrics_instances = [] - for exp_rep, _ in enumerate(self.dummy_attack_metrics): temp_dummy_attack_metrics = self.dummy_attack_metrics[exp_rep] dummy_attack_metrics_instances += temp_dummy_attack_metrics - return dummy_attack_metrics_instances def _get_attack_metrics_instances(self) -> dict: """Construct the metadata object after attacks.""" attack_metrics_experiment = {} attack_metrics_instances = {} - for rep, _ in enumerate(self.attack_metrics): attack_metrics_instances["instance_" + str(rep)] = self.attack_metrics[rep] - attack_metrics_experiment["attack_instance_logger"] = attack_metrics_instances - attack_metrics_experiment["attack_metric_failfast_summary"] = ( - self.attack_metric_failfast_summary.get_attack_summary() - ) - return attack_metrics_experiment def _get_dummy_attack_metrics_experiments_instances(self) -> dict: """Construct the metadata object after attacks.""" dummy_attack_metrics_experiments = {} - for exp_rep, _ in enumerate(self.dummy_attack_metrics): temp_dummy_attack_metrics = self.dummy_attack_metrics[exp_rep] dummy_attack_metric_instances = {} @@ -596,403 +486,11 @@ def _get_dummy_attack_metrics_experiments_instances(self) -> dict: ) temp = {} temp["attack_instance_logger"] = dummy_attack_metric_instances - temp["attack_metric_failfast_summary"] = ( - self.dummy_attack_metric_failfast_summary[exp_rep].get_attack_summary() - ) dummy_attack_metrics_experiments[ "dummy_attack_metrics_experiment_" + str(exp_rep) ] = temp - return dummy_attack_metrics_experiments - def make_report(self) -> dict: - """Create output dict and generate pdf and json if filenames are given.""" - output = {} - output["log_id"] = str(uuid.uuid4()) - output["log_time"] = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - - self._construct_metadata() - output["metadata"] = self.metadata - - output["attack_experiment_logger"] = self._get_attack_metrics_instances() - output["dummy_attack_experiments_logger"] = ( - self._get_dummy_attack_metrics_experiments_instances() - ) - - report_dest = os.path.join(self.output_dir, self.report_name) - json_attack_formatter = GenerateJSONModule(report_dest + ".json") - json_report = report.create_json_report(output) - json_attack_formatter.add_attack_output(json_report, "WorstCaseAttack") - - pdf_report = report.create_mia_report(output) - report.add_output_to_pdf(report_dest, pdf_report, "WorstCaseAttack") - return output - - -def _make_dummy_data(args) -> None: - """Initialise class and run dummy data creation.""" - args.__dict__["training_preds_filename"] = "train_preds.csv" - args.__dict__["test_preds_filename"] = "test_preds.csv" - attack_obj = WorstCaseAttack( - train_beta=args.train_beta, - test_beta=args.test_beta, - n_rows_in=args.n_rows_in, - n_rows_out=args.n_rows_out, - training_preds_filename=args.training_preds_filename, - test_preds_filename=args.test_preds_filename, - ) - attack_obj.make_dummy_data() - - -def _run_attack(args) -> None: - """Initialise class and run attack from prediction files.""" - attack_obj = WorstCaseAttack( - n_reps=args.n_reps, - p_thresh=args.p_thresh, - n_dummy_reps=args.n_dummy_reps, - train_beta=args.train_beta, - test_beta=args.test_beta, - test_prop=args.test_prop, - training_preds_filename=args.training_preds_filename, - test_preds_filename=args.test_preds_filename, - output_dir=args.output_dir, - report_name=args.report_name, - sort_probs=args.sort_probs, - attack_metric_success_name=args.attack_metric_success_name, - attack_metric_success_thresh=args.attack_metric_success_thresh, - attack_metric_success_comp_type=args.attack_metric_success_comp_type, - attack_metric_success_count_thresh=args.attack_metric_success_count_thresh, - attack_fail_fast=args.attack_fail_fast, - ) - print(attack_obj.training_preds_filename) - attack_obj.attack_from_prediction_files() - _ = attack_obj.make_report() - - -def _run_attack_from_configfile(args) -> None: - """Initialise class and run attack from prediction files using config file.""" - attack_obj = WorstCaseAttack( - attack_config_json_file_name=str(args.attack_config_json_file_name), - target_path=str(args.target_path), - ) - target = Target() - target.load(attack_obj.target_path) - attack_obj.attack(target) - _ = attack_obj.make_report() - - -def main() -> None: - """Parse arguments and invoke relevant method.""" - logger = logging.getLogger("main") - parser = argparse.ArgumentParser( - description=("Perform a worst case attack from saved model predictions") - ) - - subparsers = parser.add_subparsers() - dummy_parser = subparsers.add_parser("make-dummy-data") - dummy_parser.add_argument( - "--num-rows-in", - action="store", - dest="n_rows_in", - type=int, - required=False, - default=1000, - help=("How many rows to generate in the in-sample file. Default = %(default)d"), - ) - - dummy_parser.add_argument( - "--num-rows-out", - action="store", - dest="n_rows_out", - type=int, - required=False, - default=1000, - help=( - "How many rows to generate in the out-of-sample file. Default = %(default)d" - ), - ) - - dummy_parser.add_argument( - "--train-beta", - action="store", - type=float, - required=False, - default=5, - dest="train_beta", - help=( - """Value of b parameter for beta distribution used to sample the in-sample - probabilities. High values will give more extreme probabilities. Set this - value higher than --test-beta to see successful attacks. Default = %(default)f""" - ), - ) - - dummy_parser.add_argument( - "--test-beta", - action="store", - type=float, - required=False, - default=2, - dest="test_beta", - help=( - "Value of b parameter for beta distribution used to sample the out-of-sample " - "probabilities. High values will give more extreme probabilities. Set this value " - "lower than --train-beta to see successful attacks. Default = %(default)f" - ), - ) - - dummy_parser.set_defaults(func=_make_dummy_data) - - attack_parser = subparsers.add_parser("run-attack") - attack_parser.add_argument( - "-i", - "--training-preds-filename", - action="store", - dest="training_preds_filename", - required=False, - type=str, - default="train_preds.csv", - help=( - "csv file containing the predictive probabilities (one column per class) for the " - "training data (one row per training example). Default = %(default)s" - ), - ) - - attack_parser.add_argument( - "-o", - "--test-preds-filename", - action="store", - dest="test_preds_filename", - required=False, - type=str, - default="test_preds.csv", - help=( - "csv file containing the predictive probabilities (one column per class) for the " - "non-training data (one row per training example). Default = %(default)s" - ), - ) - - attack_parser.add_argument( - "-r", - "--n-reps", - type=int, - required=False, - default=5, - action="store", - dest="n_reps", - help=( - "Number of repetitions (splitting data into attack model training and testing " - "partitions to perform. Default = %(default)d" - ), - ) - - attack_parser.add_argument( - "-t", - "--test-prop", - type=float, - required=False, - default=0.3, - action="store", - dest="test_prop", - help=( - "Proportion of examples to be used for testing when fiting the attack model. " - "Default = %(default)f" - ), - ) - - attack_parser.add_argument( - "--output-dir", - type=str, - action="store", - dest="output_dir", - default="output_worstcase", - required=False, - help=("Directory name where output files are stored. Default = %(default)s."), - ) - - attack_parser.add_argument( - "--report-name", - type=str, - action="store", - dest="report_name", - default="report_worstcase", - required=False, - help=( - """Filename for the pdf and json report outputs. Default = %(default)s. - Code will append .pdf and .json""" - ), - ) - - attack_parser.add_argument( - "--n-dummy-reps", - type=int, - action="store", - dest="n_dummy_reps", - default=1, - required=False, - help=( - "Number of dummy datasets to sample. Each will be assessed with --n-reps train and " - "test splits. Set to 0 to do no baseline calculations. Default = %(default)d" - ), - ) - - attack_parser.add_argument( - "--p-thresh", - action="store", - type=float, - default=P_THRESH, - required=False, - dest="p_thresh", - help=("P-value threshold for significance testing. Default = %(default)f"), - ) - - attack_parser.add_argument( - "--train-beta", - action="store", - type=float, - required=False, - default=5, - dest="train_beta", - help=( - "Value of b parameter for beta distribution used to sample the in-sample probabilities." - "High values will give more extreme probabilities. Set this value higher than " - "--test-beta to see successful attacks. Default = %(default)f" - ), - ) - - attack_parser.add_argument( - "--test-beta", - action="store", - type=float, - required=False, - default=2, - dest="test_beta", - help=( - "Value of b parameter for beta distribution used to sample the out-of-sample " - "probabilities. High values will give more extreme probabilities. Set this value " - "lower than --train-beta to see successful attacks. Default = %(default)f" - ), - ) - - # --include-correct feature not supported as not currently possible from the command line - # as we cannot compute the correctness of predictions. - - attack_parser.add_argument( - "--sort-probs", - action="store", - type=bool, - default=True, - required=False, - dest="sort_probs", - help=( - "Whether or not to sort the output probabilities (per row) before " - "using them to train the attack model. Default = %(default)f" - ), - ) - - attack_parser.add_argument( - "--attack-metric-success-name", - action="store", - type=str, - default="P_HIGHER_AUC", - required=False, - dest="attack_metric_success_name", - help=( - """for computing attack success/failure based on - --attack-metric-success-thresh option. Default = %(default)s""" - ), - ) - - attack_parser.add_argument( - "--attack-metric-success-thresh", - action="store", - type=float, - default=0.05, - required=False, - dest="attack_metric_success_thresh", - help=( - """for defining threshold value to measure attack success - for the metric defined by argument --fail-metric-name option. Default = %(default)f""" - ), - ) - - attack_parser.add_argument( - "--attack-metric-success-comp-type", - action="store", - type=str, - default="lte", - required=False, - dest="attack_metric_success_comp_type", - help=( - """for computing attack success/failure based on - --attack-metric-success-thresh option. Default = %(default)s""" - ), - ) - - attack_parser.add_argument( - "--attack-metric-success-count-thresh", - action="store", - type=int, - default=2, - required=False, - dest="attack_metric_success_count_thresh", - help=( - """for setting counter limit to stop further repetitions given the attack is - successful and the --attack-fail-fast is true. Default = %(default)d""" - ), - ) - - attack_parser.add_argument( - "--attack-fail-fast", - action="store_true", - required=False, - dest="attack_fail_fast", - help=( - """to stop further repetitions when the given metric has fulfilled - a criteria for a specified number of times (--attack-metric-success-count-thresh) - and this has a true status. Default = %(default)s""" - ), - ) - - attack_parser.set_defaults(func=_run_attack) - - attack_parser_config = subparsers.add_parser("run-attack-from-configfile") - attack_parser_config.add_argument( - "-j", - "--attack-config-json-file-name", - action="store", - required=True, - dest="attack_config_json_file_name", - type=str, - default="config_worstcase_cmd.json", - help=( - "Name of the .json file containing details for the run. Default = %(default)s" - ), - ) - - attack_parser_config.add_argument( - "-t", - "--attack-target-folder-path", - action="store", - required=True, - dest="target_path", - type=str, - default="worstcase_target", - help=( - """Name of the target directory to load the trained target model and the target data. - Default = %(default)s""" - ), - ) - - attack_parser_config.set_defaults(func=_run_attack_from_configfile) - - args = parser.parse_args() - - try: - args.func(args) - except AttributeError as e: # pragma:no cover - logger.error("Invalid command. Try --help to get more details") - logger.error(e) - - -if __name__ == "__main__": # pragma:no cover - main() + def _make_pdf(self, output: dict) -> FPDF: + """Create PDF report.""" + return report.create_mia_report(output) diff --git a/aisdc/config/__init__.py b/aisdc/config/__init__.py new file mode 100644 index 00000000..4be195d9 --- /dev/null +++ b/aisdc/config/__init__.py @@ -0,0 +1 @@ +"""Configuration file generation via prompt.""" diff --git a/aisdc/config/attack.py b/aisdc/config/attack.py new file mode 100644 index 00000000..6127f7b9 --- /dev/null +++ b/aisdc/config/attack.py @@ -0,0 +1,83 @@ +"""Prompt to generate valid attack config.""" + +from __future__ import annotations + +import logging + +import yaml + +from aisdc.attacks import factory +from aisdc.config import utils + + +def _get_defaults(name: str) -> dict: + """Return an attack parameters and their defaults.""" + attack = factory.create_attack(name) + return attack.get_params() + + +def _prompt_for_params(params: dict) -> None: + """Prompt user to change parameter values.""" + for key, val in params.items(): + print(f"The current value for '{key}' is {val}.") + if utils.get_bool("Do you want to change it?"): + while True: + new_val = input(f"Enter new value for '{key}': ").strip() + try: + params[key] = type(val)(new_val) + break + except ValueError: + print(f"Please enter a value of type {type(val).__name__}.") + + +def _get_attack(name: str) -> dict: + """Get an attack configuration.""" + params: dict = _get_defaults(name) + if not utils.get_bool("Use all defaults?"): + _prompt_for_params(params) + return {"name": name, "params": params} + + +def _prompt_for_attacks() -> list[dict]: + """Prompt user for individual attack configurations.""" + attacks: list[dict] = [] + names: list[str] = list(factory.registry.keys()) + while utils.get_bool("Would you like to add an attack?"): + while True: + print(f"Attacks available: {', '.join(names)}") + name: str = input("Which attack?: ") + if name in names: + attack = _get_attack(name) + attacks.append(attack) + print(f"{name} attack added.") + break + print("Please enter one of the available attacks.") + return attacks + + +def _default_config() -> list[dict]: + """Return a default configuration with all attacks.""" + attacks: list[dict] = [] + names: list[str] = list(factory.registry.keys()) + for name in names: + params: dict = _get_defaults(name) + attack: dict = {"name": name, "params": params} + attacks.append(attack) + return attacks + + +def prompt_for_attack() -> None: + """Prompt user for information to generate attack config.""" + logging.disable(logging.ERROR) # suppress info/warnings + + # get attack configuration + if utils.get_bool("Generate default config with all attacks?"): + attacks = _default_config() + else: + attacks = _prompt_for_attacks() + + # write to file + filename: str = "attack.yaml" + with open(filename, "w", encoding="utf-8") as fp: + yaml.dump({"attacks": attacks}, fp) + print(f"{filename} has been generated.") diff --git a/aisdc/config/target.py b/aisdc/config/target.py new file mode 100644 index 00000000..5a58066d --- /dev/null +++ b/aisdc/config/target.py @@ -0,0 +1,124 @@ +"""Prompt to generate valid target config.""" + +from __future__ import annotations + +import ast +import os +import sys + +from aisdc.attacks.target import Target +from aisdc.config import utils + +arrays_pro = ["X_train", "y_train", "X_test", "y_test"] +arrays_raw = ["X", "y", "X_train_orig", "y_train_orig", "X_test_orig", "y_test_orig"] +arrays_proba = ["proba_train", "proba_test"] +encodings = ["onehot", "str", "int", "float"] + +MAX_FEATURES = 64 # maximum features to prompt + + +def _get_arrays(target: Target, arrays: list[str]) -> None: + """Prompt user for the paths to array data.""" + for arr in arrays: + while True: + path = input(f"What is the path to {arr}? ") + try: + target.load_array(path, arr) + break + except FileNotFoundError: + print("File does not exist. Please try again.") + except ValueError as e: + print(f"WARNING: {e}") + break + + +def _get_dataset_name(target: Target) -> None: + """Prompt user for the name of a dataset.""" + target.dataset_name = input("What is the name of the dataset? ") + + +def _get_feature_encoding(feat: int) -> str: + """Prompt user for feature encoding.""" + while True: + encoding = input(f"What is the encoding of feature {feat}? ") + if encoding in encodings: + return encoding + print("Invalid encoding. Please try again.") + + +def _get_feature_indices(feat: int) -> list[int]: + """Prompt user for feature indices.""" + while True: + indices = input(f"What are the indices for feature {feat}, e.g., [1,2,3]? ") + try: + indices = ast.literal_eval(indices) + if isinstance(indices, list) and all(isinstance(i, int) for i in indices): + return indices + print("Invalid input. Please enter a list of integers like [1,2,3].") + except (ValueError, SyntaxError): + print("Invalid input. Please enter a valid list of integers.") + + +def _get_features(target: Target) -> None: + """Prompt user for dataset features.""" + print("To run attribute inference attacks the features must be described.") + n_features = input("How many features does this dataset have? ") + n_features = int(n_features) + if n_features > MAX_FEATURES: + print("There are too many features to add via prompt.") + print("You can edit the 'target.yaml' to add features later.") + print("Note: this is only necessary for attribute inference.") + return + print("The name, index, and encoding are needed for each feature.") + print("For example: feature 0 = 'parents', '[0, 1, 2]', 'onehot'") + if utils.get_bool("Do you want to add this information?"): + print(f"Valid encodings: {', '.join(encodings)}") + for i in range(n_features): + name = input(f"What is the name of feature {i}? ") + indices = _get_feature_indices(i) + encoding = _get_feature_encoding(i) + target.add_feature(name, indices, encoding) + + +def _get_model_path() -> str: + """Prompt user for path to a saved fitted model.""" + if not utils.get_bool("Do you have a saved fitted model?"): + print("Cannot generate a target config without a fitted model.") + sys.exit() + + while True: + path = input("Enter path including the full filename: ") + if os.path.isfile(path): + break + print("File does not exist. Please try again.") + return path + + +def _get_proba(target: Target) -> None: + """Prompt user for model predicted probabilities.""" + if not utils.get_bool("Do you have the model predicted probabilities?"): + print("Cannot run any attacks without a supported model or probabilities.") + sys.exit() + _get_arrays(target, arrays_proba) + + +def prompt_for_target() -> None: + """Prompt user for information to generate target config.""" + target = Target() + path: str = utils.check_dir("target") + + model_path: str = _get_model_path() + try: # attempt to load saved model + target.load_model(model_path) + except ValueError: # unsupported model, require probas + print("Unable to load model.") + _get_proba(target) + + _get_dataset_name(target) + if utils.get_bool("Do you know the paths to processed data?"): + _get_arrays(target, arrays_pro) + _get_features(target) + if utils.get_bool("Do you know the paths to original raw data?"): + _get_arrays(target, arrays_raw) + target.save(path) + print(f"Target generated in directory: '{path}'") diff --git a/aisdc/config/utils.py b/aisdc/config/utils.py new file mode 100644 index 00000000..3dbd3074 --- /dev/null +++ b/aisdc/config/utils.py @@ -0,0 +1,57 @@ +"""Utilities for config prompt generation.""" + +from __future__ import annotations + +import os +import shutil + +yes: list[str] = ["yes", "y", "yeah", "yep", "sure", "ok"] +no: list[str] = ["no", "n", "nope", "nah"] + + +def get_bool(prompt: str) -> bool: + """Get a Boolean response to a prompt. + + Parameters + ---------- + prompt : str + Message to display to user. + + Returns + ------- + bool + Whether the user responded yes or no. + """ + while True: + response = input(prompt + " (yes/no): ").strip().lower() + if response in yes: + return True + if response in no: + return False + print("Invalid input. Please enter 'yes' or 'no'.") + + +def check_dir(path: str) -> str: + """Check directory exists and create as necessary. + + Parameters + ---------- + path : str + Directory to check exists. + + Returns + ------- + str + Directory, possibly changed by user if pre-existing. + """ + if os.path.isdir(path): + print(f"Directory '{path}' already exists.") + resp = get_bool( + "Continue using this directory and delete its current contents?" + ) + if resp: + shutil.rmtree(path) + else: + path = input("Specify an alternative directory: ") + return check_dir(path) + return path diff --git a/aisdc/main.py b/aisdc/main.py new file mode 100644 index 00000000..989ca9d5 --- /dev/null +++ b/aisdc/main.py @@ -0,0 +1,43 @@ +"""Main entry point to aisdc.""" + +from __future__ import annotations + +import argparse +import os + +from aisdc.attacks.factory import run_attacks +from aisdc.config.attack import prompt_for_attack +from aisdc.config.target import prompt_for_target + + +def main() -> None: + """Load target and attack configurations and run attacks.""" + parser = argparse.ArgumentParser( + description="CLI for running attacks and generating config files" + ) + + subparsers = parser.add_subparsers(dest="cmd", required=True) + + run = subparsers.add_parser("run", help="Run attacks from YAML config files") + + run.add_argument("target_dir", type=str, help="Directory containing target.yaml") + run.add_argument("attack_yaml", type=str, help="Attack YAML config") + + subparsers.add_parser("gen-target", help="Generate Target YAML config") + subparsers.add_parser("gen-attack", help="Generate Attack YAML config") + + args = parser.parse_args() + + if args.cmd == "run": + if os.path.isdir(args.target_dir) and os.path.isfile(args.attack_yaml): + run_attacks(args.target_dir, args.attack_yaml) + else: + print("Both files must exist to run attacks.") + elif args.cmd == "gen-target": + prompt_for_target() + elif args.cmd == "gen-attack": + prompt_for_attack() + + +if __name__ == "__main__": # pragma:no cover + main() diff --git a/aisdc/safemodel/safemodel.py b/aisdc/safemodel/safemodel.py index cfeb1f81..f81e72ff 100644 --- a/aisdc/safemodel/safemodel.py +++ b/aisdc/safemodel/safemodel.py @@ -15,15 +15,13 @@ import joblib from dictdiffer import diff -from aisdc.attacks.attribute_attack import AttributeAttack -from aisdc.attacks.likelihood_attack import LIRAAttack +from aisdc.attacks.factory import attack from aisdc.attacks.target import Target -from aisdc.attacks.worst_case_attack import WorstCaseAttack # pylint : disable=too-many-branches from .reporting import get_reporting_string -logger = logging.getLogger(__file__) +logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -609,11 +607,10 @@ def request_release(self, path: str, ext: str, target: Target = None) -> None: output["recommendation"] = "Do not allow release" output["reason"] = msg_prel + msg_post # Run attacks programmatically if possible - attack_results_filename = "attack_results" if target is not None: - for attack_name in ["worst_case", "lira", "attribute"]: + for attack_name in ["worstcase", "lira", "attribute"]: output[f"{attack_name}_results"] = self.run_attack( - target, attack_name, path, attack_results_filename + target, attack_name, path ) # add timestamp now = datetime.datetime.now() @@ -628,10 +625,9 @@ def request_release(self, path: str, ext: str, target: Target = None) -> None: def run_attack( self, - target: Target = None, - attack_name: str = None, - output_dir: str = "RES", - report_name: str = "undefined", + target: Target, + attack_name: str, + output_dir: str = "outputs_safemodel", ) -> dict: """Run a specified attack on the trained model and save report to file. @@ -642,57 +638,21 @@ def run_attack( attack_name : str Name of the attack to run. output_dir : str - Name of the directory to store .json and .pdf output reports - report_name : str - Name of a .json file to save report. + Name of the directory to store JSON and PDF reports. Returns ------- dict Metadata results. - - Notes - ----- - Currently implemented attack types are: - - Likelihood Ratio: lira. - - Worst_Case Membership inference: worst_case. - - Single Attribute Inference: attribute. """ - if attack_name == "worst_case": - attack_obj = WorstCaseAttack( - n_reps=10, - n_dummy_reps=1, - p_thresh=0.05, - training_preds_filename=None, - test_preds_filename=None, - test_prop=0.5, - output_dir=output_dir, - report_name=report_name, - ) - attack_obj.attack(target) - output = attack_obj.make_report() - metadata = output["metadata"] - elif attack_name == "lira": - attack_obj = LIRAAttack( - n_shadow_models=100, - output_dir=output_dir, - report_name=report_name, - ) - attack_obj.attack(target) - output = attack_obj.make_report() - metadata = output["metadata"] - elif attack_name == "attribute": - attack_obj = AttributeAttack( - output_dir=output_dir, - report_name=report_name, - ) - attack_obj.attack(target) - output = attack_obj.make_report() + try: + params = {"output_dir": output_dir} + output = attack(target=target, attack_name=attack_name, **params) metadata = output["metadata"] - else: + except ValueError: metadata = {} metadata["outcome"] = "unrecognised attack type requested" - print(f"attack {attack_name}, metadata {metadata}") + logger.info("attack %s, metadata %s", attack_name, metadata) return metadata def __str__(self) -> str: # pragma: no cover diff --git a/docs/source/attacks/lira_example1_report.json b/docs/source/attacks/lira_example1_report.json deleted file mode 100644 index 21cf5b66..00000000 --- a/docs/source/attacks/lira_example1_report.json +++ /dev/null @@ -1,659 +0,0 @@ -{ - "log_id": "71ad36cf-0158-4924-9a09-f046f9f67da5", - "log_time": "05/06/2023 15:00:22", - "metadata": { - "experiment_details": { - "n_shadow_models": 100, - "n_shadow_rows_confidences_min": 10, - "p_thresh": 0.05, - "report_name": "lira_example1_report", - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": [ - "sklearn.ensemble", - "RandomForestClassifier" - ], - "target_model_hyp": { - "min_samples_split": 2, - "min_samples_leaf": 1 - }, - "shadow_models_fail_fast": false - }, - "global_metrics": { - "PDIF_sig": "Significant at p=0.05", - "AUC_sig": "Significant at p=0.05", - "null_auc_3sd_range": "0.4207446718814698 -> 0.5792553281185302" - }, - "attack": "LIRA Attack" - }, - "attack_experiment_logger": { - "attack_instance_logger": { - "instance_0": { - "TPR": 0.8040201, - "FPR": 0.52631579, - "FAR": 0.2195122, - "TNR": 0.47368421, - "PPV": 0.7804878, - "NPV": 0.50943396, - "FNR": 0.1959799, - "ACC": 0.70474517, - "F1score": 0.79207921, - "Advantage": 0.27770431000000007, - "AUC": 0.74110318, - "P_HIGHER_AUC": 0.0, - "FMAX01": 0.9824561403508771, - "FMIN01": 0.40350877192982454, - "FDIF01": 0.5789473684210527, - "PDIF01": 25.569652500349367, - "FMAX02": 0.9385964912280702, - "FMIN02": 0.41228070175438597, - "FDIF02": 0.5263157894736843, - "PDIF02": 115.13, - "FMAX001": 1.0, - "FMIN001": 0.3333333333333333, - "FDIF001": 0.6666666666666667, - "PDIF001": 5.134026454098573, - "pred_prob_var": 0.0699193384552378, - "TPR@0.5": 0.7964824120603015, - "TPR@0.2": 0.5477386934673367, - "TPR@0.1": 0.3969849246231156, - "TPR@0.01": 0.1658291457286432, - "TPR@0.001": 0.052763819095477386, - "TPR@1e-05": 0.052763819095477386, - "fpr": [ - 0.0, - 0.0, - 0.0, - 0.005847953216374269, - 0.005847953216374269, - 0.011695906432748537, - 0.011695906432748537, - 0.017543859649122806, - 0.017543859649122806, - 0.023391812865497075, - 0.023391812865497075, - 0.029239766081871343, - 0.029239766081871343, - 0.03508771929824561, - 0.03508771929824561, - 0.04093567251461988, - 0.04093567251461988, - 0.05263157894736842, - 0.05263157894736842, - 0.07017543859649122, - 0.07017543859649122, - 0.07602339181286549, - 0.07602339181286549, - 0.08187134502923976, - 0.08187134502923976, - 0.0935672514619883, - 0.0935672514619883, - 0.09941520467836257, - 0.09941520467836257, - 0.10526315789473684, - 0.10526315789473684, - 0.11695906432748537, - 0.11695906432748537, - 0.13450292397660818, - 0.13450292397660818, - 0.14035087719298245, - 0.14035087719298245, - 0.14619883040935672, - 0.14619883040935672, - 0.15789473684210525, - 0.15789473684210525, - 0.1695906432748538, - 0.1695906432748538, - 0.17543859649122806, - 0.17543859649122806, - 0.18128654970760233, - 0.18128654970760233, - 0.1871345029239766, - 0.19298245614035087, - 0.19298245614035087, - 0.19883040935672514, - 0.2046783625730994, - 0.2046783625730994, - 0.21052631578947367, - 0.21052631578947367, - 0.21637426900584794, - 0.21637426900584794, - 0.2222222222222222, - 0.2222222222222222, - 0.22807017543859648, - 0.23391812865497075, - 0.23391812865497075, - 0.25146198830409355, - 0.25146198830409355, - 0.25146198830409355, - 0.2573099415204678, - 0.2573099415204678, - 0.2631578947368421, - 0.2631578947368421, - 0.26900584795321636, - 0.27485380116959063, - 0.27485380116959063, - 0.27485380116959063, - 0.27485380116959063, - 0.2807017543859649, - 0.2807017543859649, - 0.28654970760233917, - 0.28654970760233917, - 0.29239766081871343, - 0.29239766081871343, - 0.30409356725146197, - 0.30409356725146197, - 0.30994152046783624, - 0.30994152046783624, - 0.30994152046783624, - 0.3157894736842105, - 0.3157894736842105, - 0.32748538011695905, - 0.32748538011695905, - 0.3391812865497076, - 0.3391812865497076, - 0.3391812865497076, - 0.3684210526315789, - 0.3684210526315789, - 0.3742690058479532, - 0.3742690058479532, - 0.38011695906432746, - 0.38011695906432746, - 0.38596491228070173, - 0.391812865497076, - 0.391812865497076, - 0.391812865497076, - 0.391812865497076, - 0.39766081871345027, - 0.39766081871345027, - 0.40350877192982454, - 0.40350877192982454, - 0.40350877192982454, - 0.4093567251461988, - 0.4152046783625731, - 0.4327485380116959, - 0.4327485380116959, - 0.4327485380116959, - 0.43859649122807015, - 0.43859649122807015, - 0.4444444444444444, - 0.4444444444444444, - 0.4502923976608187, - 0.4502923976608187, - 0.45614035087719296, - 0.4678362573099415, - 0.4678362573099415, - 0.4678362573099415, - 0.47953216374269003, - 0.49707602339181284, - 0.49707602339181284, - 0.5029239766081871, - 0.5029239766081871, - 0.5146198830409356, - 0.5146198830409356, - 0.5263157894736842, - 0.5263157894736842, - 0.5614035087719298, - 0.5906432748538012, - 0.5906432748538012, - 0.5906432748538012, - 0.5964912280701754, - 0.5964912280701754, - 0.6023391812865497, - 0.6023391812865497, - 0.6081871345029239, - 0.6081871345029239, - 0.6257309941520468, - 0.6257309941520468, - 0.631578947368421, - 0.631578947368421, - 0.6374269005847953, - 0.6374269005847953, - 0.6549707602339181, - 0.6549707602339181, - 0.6608187134502924, - 0.6608187134502924, - 0.6900584795321637, - 0.6900584795321637, - 0.695906432748538, - 0.695906432748538, - 0.7251461988304093, - 0.7251461988304093, - 0.7368421052631579, - 0.7368421052631579, - 0.7543859649122807, - 0.7543859649122807, - 0.7719298245614035, - 0.7719298245614035, - 0.7894736842105263, - 0.7894736842105263, - 0.7953216374269005, - 0.7953216374269005, - 0.8011695906432749, - 0.8011695906432749, - 0.8245614035087719, - 0.8245614035087719, - 0.847953216374269, - 0.847953216374269, - 0.8538011695906432, - 0.8538011695906432, - 0.8596491228070176, - 0.8596491228070176, - 0.8830409356725146, - 0.8830409356725146, - 0.8888888888888888, - 0.8888888888888888, - 0.9064327485380117, - 0.9064327485380117, - 0.9181286549707602, - 0.9181286549707602, - 0.9649122807017544, - 0.9649122807017544, - 0.9707602339181286, - 0.9707602339181286, - 0.9766081871345029, - 0.9766081871345029, - 1.0, - 1.0 - ], - "tpr": [ - 0.0, - 0.010050251256281407, - 0.052763819095477386, - 0.052763819095477386, - 0.1658291457286432, - 0.1658291457286432, - 0.18341708542713567, - 0.18341708542713567, - 0.20854271356783918, - 0.20854271356783918, - 0.2236180904522613, - 0.2236180904522613, - 0.23869346733668342, - 0.23869346733668342, - 0.2613065326633166, - 0.2613065326633166, - 0.31155778894472363, - 0.31155778894472363, - 0.31909547738693467, - 0.31909547738693467, - 0.36432160804020103, - 0.36432160804020103, - 0.3693467336683417, - 0.3693467336683417, - 0.3768844221105528, - 0.3768844221105528, - 0.3869346733668342, - 0.3869346733668342, - 0.3969849246231156, - 0.3969849246231156, - 0.39949748743718594, - 0.39949748743718594, - 0.4045226130653266, - 0.4045226130653266, - 0.45979899497487436, - 0.45979899497487436, - 0.4623115577889447, - 0.4623115577889447, - 0.4899497487437186, - 0.4899497487437186, - 0.49748743718592964, - 0.49748743718592964, - 0.5100502512562815, - 0.5100502512562815, - 0.5175879396984925, - 0.5175879396984925, - 0.5326633165829145, - 0.5326633165829145, - 0.535175879396985, - 0.5452261306532663, - 0.5477386934673367, - 0.5477386934673367, - 0.5628140703517588, - 0.5628140703517588, - 0.5653266331658291, - 0.5653266331658291, - 0.5678391959798995, - 0.5678391959798995, - 0.5703517587939698, - 0.5703517587939698, - 0.5728643216080402, - 0.5804020100502513, - 0.5804020100502513, - 0.585427135678392, - 0.5879396984924623, - 0.5879396984924623, - 0.5954773869346733, - 0.5954773869346733, - 0.6030150753768844, - 0.6055276381909548, - 0.6055276381909548, - 0.6130653266331658, - 0.6180904522613065, - 0.6206030150753769, - 0.6206030150753769, - 0.628140703517588, - 0.628140703517588, - 0.6381909547738693, - 0.6381909547738693, - 0.6557788944723618, - 0.6557788944723618, - 0.6582914572864321, - 0.6582914572864321, - 0.6683417085427136, - 0.6708542713567839, - 0.6733668341708543, - 0.678391959798995, - 0.678391959798995, - 0.6809045226130653, - 0.6809045226130653, - 0.6909547738693468, - 0.6959798994974874, - 0.6959798994974874, - 0.7035175879396985, - 0.7035175879396985, - 0.7060301507537688, - 0.7085427135678392, - 0.7110552763819096, - 0.7160804020100503, - 0.7160804020100503, - 0.7185929648241206, - 0.7236180904522613, - 0.7261306532663316, - 0.7261306532663316, - 0.7286432160804021, - 0.7286432160804021, - 0.7336683417085427, - 0.7412060301507538, - 0.7412060301507538, - 0.7462311557788944, - 0.7462311557788944, - 0.7512562814070352, - 0.7537688442211056, - 0.7537688442211056, - 0.7562814070351759, - 0.7613065326633166, - 0.7688442211055276, - 0.7688442211055276, - 0.7763819095477387, - 0.7788944723618091, - 0.7788944723618091, - 0.7839195979899497, - 0.7889447236180904, - 0.7939698492462312, - 0.7939698492462312, - 0.7964824120603015, - 0.7964824120603015, - 0.7989949748743719, - 0.7989949748743719, - 0.8015075376884422, - 0.8015075376884422, - 0.8040201005025126, - 0.8391959798994975, - 0.8567839195979899, - 0.864321608040201, - 0.8693467336683417, - 0.8693467336683417, - 0.8768844221105527, - 0.8768844221105527, - 0.8793969849246231, - 0.8793969849246231, - 0.8819095477386935, - 0.8819095477386935, - 0.8894472361809045, - 0.8894472361809045, - 0.8969849246231156, - 0.8969849246231156, - 0.907035175879397, - 0.907035175879397, - 0.9095477386934674, - 0.9095477386934674, - 0.9120603015075377, - 0.9120603015075377, - 0.914572864321608, - 0.914572864321608, - 0.9170854271356784, - 0.9170854271356784, - 0.9221105527638191, - 0.9221105527638191, - 0.9246231155778895, - 0.9246231155778895, - 0.9271356783919598, - 0.9271356783919598, - 0.9321608040201005, - 0.9321608040201005, - 0.9346733668341709, - 0.9346733668341709, - 0.9371859296482412, - 0.9371859296482412, - 0.9422110552763819, - 0.9422110552763819, - 0.9522613065326633, - 0.9522613065326633, - 0.9597989949748744, - 0.9597989949748744, - 0.9623115577889447, - 0.9623115577889447, - 0.9698492462311558, - 0.9698492462311558, - 0.9748743718592965, - 0.9748743718592965, - 0.9798994974874372, - 0.9798994974874372, - 0.9849246231155779, - 0.9849246231155779, - 0.9899497487437185, - 0.9899497487437185, - 0.992462311557789, - 0.992462311557789, - 0.9949748743718593, - 0.9949748743718593, - 0.9974874371859297, - 0.9974874371859297, - 1.0 - ], - "roc_thresh": [ - 2.0, - 1.0, - 0.9999990662847387, - 0.9999990563120845, - 0.9992407564315533, - 0.999224241635539, - 0.9987631752771089, - 0.9980316778642635, - 0.9961183960546937, - 0.9960338424770212, - 0.9945475601694589, - 0.9937406390440989, - 0.9900510370913693, - 0.9898650696617601, - 0.9772009003916872, - 0.9771911569948254, - 0.9512593135427929, - 0.947309640166169, - 0.9352413109907762, - 0.930089313181121, - 0.8910261284505214, - 0.8873867562145326, - 0.8860602246950124, - 0.8860457517017866, - 0.8802810739987481, - 0.8775620913300239, - 0.8564127780244211, - 0.8554096958276379, - 0.84131095340667, - 0.8413018880675517, - 0.841272361855808, - 0.8351494425100894, - 0.8339952923406713, - 0.8309888110716661, - 0.7995533634202967, - 0.796460901745287, - 0.7928817814378666, - 0.7928726109294209, - 0.7806854639230842, - 0.7782023526700991, - 0.7686662791260738, - 0.7602499389065233, - 0.7553944354791697, - 0.7543324746501248, - 0.7474667776297698, - 0.7429937324857152, - 0.7324005602995822, - 0.731436553515783, - 0.7267410594281718, - 0.7237646761332568, - 0.7229305304125133, - 0.7218507693626326, - 0.7136488201108449, - 0.7136414501325502, - 0.7129171494807268, - 0.710443668039783, - 0.7104391135850816, - 0.7080487715945838, - 0.7053924092429098, - 0.7035099509912865, - 0.7035021063591089, - 0.699238296496945, - 0.6952383098245853, - 0.6914551985793077, - 0.6914498007516868, - 0.6883878810122829, - 0.6878898482006836, - 0.6872073842378373, - 0.6852806597823655, - 0.6845229794079146, - 0.6832153521428725, - 0.6813120122334345, - 0.6794825785008081, - 0.6794711109832587, - 0.6782855782181897, - 0.6726339339469332, - 0.6694985771880104, - 0.6650923212002915, - 0.6641481488626844, - 0.6559979297710586, - 0.6536836079790197, - 0.6536772053830657, - 0.6525566988137631, - 0.6525566988137629, - 0.6507323208483302, - 0.6498443135315546, - 0.6494511976293276, - 0.6472715069443634, - 0.6472715069443632, - 0.6448267155022167, - 0.6381631950841185, - 0.6361382266652248, - 0.6305586598182359, - 0.6221321912131363, - 0.6211896381583009, - 0.6202685673100334, - 0.6202685673100331, - 0.6184876997235027, - 0.6184876997235024, - 0.6170768023402461, - 0.6167836417729271, - 0.6135850036577761, - 0.611351294605239, - 0.6106354977063289, - 0.609244352500643, - 0.6073469489655614, - 0.6072526264408226, - 0.6053659869328589, - 0.6035755086858976, - 0.6035755086858973, - 0.5987063256829234, - 0.5931681421166038, - 0.5884683631209386, - 0.5865456447788606, - 0.5825863354073804, - 0.5808717568070867, - 0.579259709439103, - 0.5792597094391029, - 0.5777403662635295, - 0.5763051701566428, - 0.5763051701566424, - 0.5701581024006669, - 0.5701581024006666, - 0.568084128572636, - 0.5606031410203505, - 0.5541549195243072, - 0.5525616881720951, - 0.5492950164102707, - 0.5261416909390764, - 0.5259310415049618, - 0.5149014886651603, - 0.5089712249587206, - 0.5, - 0.4999997165344607, - 0.49999943306892136, - 0.4858404599004833, - 0.4539808276256947, - 0.4494271609318672, - 0.44714935377576753, - 0.44604762280405796, - 0.4456229984516941, - 0.43800647230340034, - 0.419779504614214, - 0.41423339257204883, - 0.4048081590495849, - 0.3964263198325637, - 0.3932713655976186, - 0.3759070164808108, - 0.36503777600451476, - 0.360585315683724, - 0.3596855874496766, - 0.358420934916232, - 0.34081222022837443, - 0.3390327083784013, - 0.33772909022235353, - 0.33677335130152064, - 0.3262934416718155, - 0.32386492620493396, - 0.323349644108635, - 0.32270315899423774, - 0.3154855475743056, - 0.31284326956762315, - 0.3110887162968101, - 0.3083474320203914, - 0.3023725821981934, - 0.298999506292999, - 0.29630548980183935, - 0.29493486843427447, - 0.2864171441614538, - 0.2819890818640831, - 0.27503401421306617, - 0.27383240235209616, - 0.26436854303648893, - 0.25775640021971535, - 0.25118009171471767, - 0.24922177025980308, - 0.24351726878323254, - 0.23428629602327095, - 0.21494318583032235, - 0.20216014066708476, - 0.19769469683818142, - 0.1874480570809287, - 0.16679632165325153, - 0.15865525393145707, - 0.11852553125421672, - 0.11339359492729129, - 0.08686169877965144, - 0.07469181693355917, - 0.048345070890205254, - 0.046163272978155104, - 0.030818672452908664, - 0.02939086067767943, - 0.002338867490523633, - 6.065772541830307e-08 - ], - "n_pos_test_examples": 398, - "n_neg_test_examples": 171, - "n_shadow_models_trained": 100 - } - } - } -} diff --git a/docs/source/attacks/output_format.rst b/docs/source/attacks/output_format.rst index 228938b8..64f819f8 100644 --- a/docs/source/attacks/output_format.rst +++ b/docs/source/attacks/output_format.rst @@ -1,7 +1,7 @@ -JSON Output for MIA attacks -=========================== +JSON Output for Attacks +======================= -We standaridised the JSON output both for worst_case and LIRA attacks where possible. A generic JSON output structure is presented as under: +JSON output has been standardised where possible. A generic JSON output structure is presented as under: General Structure ----------------- @@ -20,49 +20,29 @@ A worst case attack will have the following components in a metadata component o metadata:: - experiment_details: this will have attack type parameters - n_reps: number of attacks to run -- in each iteration an attack model is trained on a different subset of the data - p_thresh: threshold to determine significance of things. For instance auc_p_value and pdif_vals - n_dummy_reps: number of baseline (dummy) experiments to do - train_beta: value of b for beta distribution used to sample the in-sample (training) probabilities - test_beta: value of b for beta distribution used to sample the out-of-sample (test) probabilities - test_prop: proportion of data to use as a test set for the attack model - n_rows_in: number of rows for in-sample (training data) - n_rows_out: number of rows for out-of-sample (test data) - training_preds_filename: name of the file to keep predictions of the training data (in-sample) - test_preds_filename: name of the file to keep predictions of the test data (out-of-sample) - report_name: name of the JSON report - include_model_correct_feature: inclusion of additional feature to hold whether or not the target model made a correct prediction for each example - sort_probs: true in case require to sort combine preds (from training and test) to have highest probabilities in the first column - mia_attack_model: name of the attack model suchas RandomForestClassifier - mia_attack_model_hyp: list of hyper parameters for the mia_attack_model such as min_sample_split, min_samples_leaf, max_depth etc - attack_metric_success_name: the name of metric to compute for the attack being successful - attack_metric_success_thresh: threshold for a given metric to measure attack being successful or not - attack_metric_success_comp_type: threshold comparison operator (i.e., gte: greater than or equal to, gt: greater than, lte: less than or equal to, lt: less than, eq: equal to and not_eq: not equal to) - attack_metric_success_count_thresh: a counter to record how many times an attack was successful given that the threshold has fulfilled criteria for a given comparison type - attack_fail_fast: If true it stops repetitions earlier based on the given attack metric (i.e., attack_metric_success_name) considering the comparison type (attack_metric_success_comp_type) satisfying a threshold (i.e., attack_metric_success_thresh) for n (attack_metric_success_count_thresh) number of times - - attack: name of the attack type ('WorstCase attack') - - global_metric: the following global metrics are computed for attack repetitions - null_auc_3sd_range: a three standard deviation range from the mean for the observed p_value - n_sig_auc_p_vals: number of significant p values given a p_thresh value - n_sig_auc_p_vals_corrected: number of significant p values given a p_thresh value given applying testing corrections - n_sig_pdif_vals: number of significant pdif given a p_thresh value - n_sig_pdif_vals_corrected: number of significant p values given a p_thresh value given applying testing corrections - - baseline_global_metric: the following global metrics are computed for attack repetitions across all experiments of baseline (dummy) experiments - null_auc_3sd_range: a three standard deviation range from the mean for the observed p_value - n_sig_auc_p_vals: number of significant p values given a p_thresh value - n_sig_auc_p_vals_corrected: number of significant p values given a p_thresh value given applying testing corrections - n_sig_pdif_vals: number of significant pdif given a p_thresh value - n_sig_pdif_vals_corrected: number of significant p values given a p_thresh value given applying testing corrections + attack_name: Name of the attack + attack_params: Attack parameters + target_model: Name of the target model + target_model_params: Target model parameters + global_metrics: The following global metrics are computed for attack repetitions + null_auc_3sd_range: A three standard deviation range from the mean for the observed p_value + n_sig_auc_p_vals: Number of significant p values given a p_thresh value + n_sig_auc_p_vals_corrected: Number of significant p values given a p_thresh value given applying testing corrections + n_sig_pdif_vals: Number of significant pdif given a p_thresh value + n_sig_pdif_vals_corrected: Number of significant p values given a p_thresh value given applying testing corrections + + baseline_global_metric: The following global metrics are computed for attack repetitions across all experiments of baseline (dummy) experiments + null_auc_3sd_range: A three standard deviation range from the mean for the observed p_value + n_sig_auc_p_vals: Number of significant p values given a p_thresh value + n_sig_auc_p_vals_corrected: Number of significant p values given a p_thresh value given applying testing corrections + n_sig_pdif_vals: Number of significant pdif given a p_thresh value + n_sig_pdif_vals_corrected: Number of significant p values given a p_thresh value given applying testing corrections A worst case attack will have experiment logger and baseline (dummy) experiments logger which is unique to worst case attack only. attack_experiment_logger:: - attack_instance_logger: stores metrics computed across all iteration of attacks (i.e. n_reps) + attack_instance_logger: Stores metrics computed across all iteration of attacks (i.e. n_reps) instance_0: TPR: value of true positive rate FPR: value of false positive rate @@ -76,9 +56,6 @@ attack_experiment_logger:: instance_n: ... n will be n_reps-1 representing iterations of attacks - attack_metric_failfast_summary: - succcess_count: number of attacks being successful given the attack success criteria demonstrated in metadata - fail_count: number of attacks being not successful dummy_attack_experiments_logger:: @@ -97,45 +74,31 @@ dummy_attack_experiments_logger:: instance_n: n will be n_reps-1 representing iterations of attacks ... - attack_metric_failfast_summary: - succcess_count: number of attacks being successful given the attack success criteria demonstrated in metadata - fail_count: number of attacks being not successful dummy_attack_metrics_experiment_1: ... ... dummy_attack_metrics_experiment_n: n will be n_dummy_reps-1 representing iterations of attacks ... -Example JSON output for worst case attack is accessible from :download:`link ` +Example JSON output for worst case attack is accessible from :download:`link ` -LIRA Attack +LiRA Attack ----------- -A LIRA attack will have the following components in a metadata component of JSON output. +A LiRA attack will have the following components in a metadata component of JSON output. metadata:: - experiment_details: this will have attack type parameters - n_shadow_models: number of shadow models to be trained - p_thresh: threshold to determine significance of things. For instance auc_p_value and pdif_vals - report_name: name of the JSON report - training_data_filename: name of the data file for the training data (in-sample) - test_data_filename: name of the file for the test data (out-of-sample) - training_preds_filename: name of the file to keep predictions of the training data (in-sample) - test_preds_filename: name of the file to keep predictions of the test data (out-of-sample) - target_model: name of the attack model suchas RandomForestClassifier - target_model_hyp: list of hyper parameters for the mia_attack_model such as min_sample_split, min_samples_leaf etc - n_shadow_rows_confidences_min: number of minimum number of confidences calculated for each row in test data (out-of-sample) - attack_fail_fast: If true it stops repetitions earlier based on the given minimum number of confidences for each row in the test data - - attack: name of the attack type ('WorstCase attack') - - global_metric: the following global metrics are computed for attack repetitions - null_auc_3sd_range: a three standard deviation range from the mean for the observed p_value - AUC_sig: significant AUC at given p value - PDIF_sig: significant PDIF at given p value + attack_name: Name of the attack + attack_params: Attack parameters + target_model: Name of the target model + target_model_params: Target model parameters + global_metric: The following global metrics are computed for attack repetitions + null_auc_3sd_range: A three standard deviation range from the mean for the observed p_value + AUC_sig: Significant AUC at given p value + PDIF_sig: Significant PDIF at given p value -A LIRA attack will have experiment logger with only one instance. +A LiRA attack will have experiment logger with only one instance. attack_experiment_logger:: @@ -147,12 +110,5 @@ attack_experiment_logger:: ... n_pos_test_examples: n_neg_test_examples: - n_shadow_models_trained: this represent number of actual models trained. For a case where attack_fail_fast is true and minimum number of confidences computed for each row in the test data, there is likely to be a chance to have less number of shadow models trained satisfying the given criteria - -Example JSON output for LIRA attack is accessible from :download:`link ` - -Running MIA Attacks from Config File -==================================== -Both for worst case and LIRA attacks, examples presented `worst_case_attack_example `_ -and `lira_attack_example `_ in the AI-SDC explains most of the possible use of configuration files. +Example JSON output for LiRA attack is accessible from :download:`link ` diff --git a/docs/source/attacks/programmatically_worstcase_example1_report.json b/docs/source/attacks/programmatically_worstcase_example1_report.json deleted file mode 100644 index 1168c876..00000000 --- a/docs/source/attacks/programmatically_worstcase_example1_report.json +++ /dev/null @@ -1,928 +0,0 @@ -{ - "log_id": "e29a98da-9806-4f0b-8934-68324cf2f25a", - "log_time": "05/06/2023 12:01:54", - "metadata": { - "experiment_details": { - "n_reps": 10, - "p_thresh": 0.05, - "n_dummy_reps": 1, - "train_beta": 5, - "test_beta": 2, - "test_prop": 0.5, - "n_rows_in": 398, - "n_rows_out": 171, - "training_preds_filename": null, - "test_preds_filename": null, - "report_name": "programmatically_worstcase_example1_report", - "include_model_correct_feature": false, - "sort_probs": true, - "mia_attack_model": "", - "mia_attack_model_hyp": { - "min_samples_split": 20, - "min_samples_leaf": 10, - "max_depth": 5 - }, - "attack_metric_success_name": "P_HIGHER_AUC", - "attack_metric_success_thresh": 0.05, - "attack_metric_success_comp_type": "lte", - "attack_metric_success_count_thresh": 2, - "attack_fail_fast": true - }, - "attack": "WorstCase attack", - "global_metrics": { - "null_auc_3sd_range": "0.3880 -> 0.6120", - "n_sig_auc_p_vals": 2, - "n_sig_auc_p_vals_corrected": 2, - "n_sig_pdif_vals": 2, - "n_sig_pdif_vals_corrected": 2 - }, - "baseline_global_metrics": { - "null_auc_3sd_range": "0.3880 -> 0.6120", - "n_sig_auc_p_vals": 2, - "n_sig_auc_p_vals_corrected": 2, - "n_sig_pdif_vals": 2, - "n_sig_pdif_vals_corrected": 2 - } - }, - "attack_experiment_logger": { - "attack_instance_logger": { - "instance_0": { - "TPR": 1.0, - "FPR": 0.0, - "FAR": 0.0, - "TNR": 1.0, - "PPV": 1.0, - "NPV": 1.0, - "FNR": 0.0, - "ACC": 1.0, - "F1score": 1.0, - "Advantage": 1.0, - "AUC": 1.0, - "P_HIGHER_AUC": 0.0, - "FMAX01": 1.0, - "FMIN01": 0.0, - "FDIF01": 1.0, - "PDIF01": 115.13, - "FMAX02": 1.0, - "FMIN02": 0.0, - "FDIF02": 1.0, - "PDIF02": 115.13, - "FMAX001": 1.0, - "FMIN001": 0.0, - "FDIF001": 1.0, - "PDIF001": 5.569287279660488, - "pred_prob_var": 0.210698676515851, - "TPR@0.5": 1.0, - "TPR@0.2": 1.0, - "TPR@0.1": 1.0, - "TPR@0.01": 1.0, - "TPR@0.001": 1.0, - "TPR@1e-05": 1.0, - "fpr": [ - 0.0, - 0.0, - 1.0 - ], - "tpr": [ - 0.0, - 1.0, - 1.0 - ], - "roc_thresh": [ - 2.0, - 1.0, - 0.0 - ], - "n_pos_test_examples": 199.0, - "n_neg_test_examples": 86.0 - }, - "instance_1": { - "TPR": 1.0, - "FPR": 0.0, - "FAR": 0.0, - "TNR": 1.0, - "PPV": 1.0, - "NPV": 1.0, - "FNR": 0.0, - "ACC": 1.0, - "F1score": 1.0, - "Advantage": 1.0, - "AUC": 1.0, - "P_HIGHER_AUC": 0.0, - "FMAX01": 1.0, - "FMIN01": 0.0, - "FDIF01": 1.0, - "PDIF01": 115.13, - "FMAX02": 1.0, - "FMIN02": 0.0, - "FDIF02": 1.0, - "PDIF02": 115.13, - "FMAX001": 1.0, - "FMIN001": 0.0, - "FDIF001": 1.0, - "PDIF001": 5.569287279660488, - "pred_prob_var": 0.210698676515851, - "TPR@0.5": 1.0, - "TPR@0.2": 1.0, - "TPR@0.1": 1.0, - "TPR@0.01": 1.0, - "TPR@0.001": 1.0, - "TPR@1e-05": 1.0, - "fpr": [ - 0.0, - 0.0, - 1.0 - ], - "tpr": [ - 0.0, - 1.0, - 1.0 - ], - "roc_thresh": [ - 2.0, - 1.0, - 0.0 - ], - "n_pos_test_examples": 199.0, - "n_neg_test_examples": 86.0 - } - }, - "attack_metric_failfast_summary": { - "success_count": 2, - "fail_count": 0 - } - }, - "dummy_attack_experiments_logger": { - "dummy_attack_metrics_experiment_0": { - "attack_instance_logger": { - "instance_0": { - "TPR": 0.84924623, - "FPR": 0.70930233, - "FAR": 0.26521739, - "TNR": 0.29069767, - "PPV": 0.73478261, - "NPV": 0.45454545, - "FNR": 0.15075377, - "ACC": 0.68070175, - "F1score": 0.78787879, - "Advantage": 0.1399439, - "AUC": 0.5969674, - "P_HIGHER_AUC": 0.004682711940895645, - "FMAX01": 0.7931034482758621, - "FMIN01": 0.5517241379310345, - "FDIF01": 0.24137931034482762, - "PDIF01": 3.7889078224645787, - "FMAX02": 0.7543859649122807, - "FMIN02": 0.5614035087719298, - "FDIF02": 0.19298245614035092, - "PDIF02": 4.389911438971763, - "FMAX001": 1.0, - "FMIN001": 0.6666666666666666, - "FDIF001": 0.33333333333333337, - "PDIF001": 1.677202524252041, - "pred_prob_var": 0.05412407324215461, - "TPR@0.5": 0.7135678391959799, - "TPR@0.2": 0.2613065326633166, - "TPR@0.1": 0.14723618090452262, - "TPR@0.01": 0.03663316582914604, - "TPR@0.001": 0.030798994974873803, - "TPR@1e-05": 0.030157236180904625, - "fpr": [ - 0.0, - 0.0, - 0.023255813953488372, - 0.023255813953488372, - 0.046511627906976744, - 0.046511627906976744, - 0.06976744186046512, - 0.06976744186046512, - 0.06976744186046512, - 0.08139534883720931, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.11627906976744186, - 0.11627906976744186, - 0.13953488372093023, - 0.13953488372093023, - 0.1511627906976744, - 0.16279069767441862, - 0.16279069767441862, - 0.16279069767441862, - 0.18604651162790697, - 0.19767441860465115, - 0.19767441860465115, - 0.20930232558139536, - 0.20930232558139536, - 0.22093023255813954, - 0.2441860465116279, - 0.2441860465116279, - 0.2441860465116279, - 0.2558139534883721, - 0.2558139534883721, - 0.27906976744186046, - 0.29069767441860467, - 0.3023255813953488, - 0.37209302325581395, - 0.37209302325581395, - 0.38372093023255816, - 0.38372093023255816, - 0.38372093023255816, - 0.4069767441860465, - 0.4069767441860465, - 0.4069767441860465, - 0.4186046511627907, - 0.4186046511627907, - 0.4186046511627907, - 0.4186046511627907, - 0.43023255813953487, - 0.43023255813953487, - 0.45348837209302323, - 0.45348837209302323, - 0.45348837209302323, - 0.45348837209302323, - 0.45348837209302323, - 0.45348837209302323, - 0.45348837209302323, - 0.46511627906976744, - 0.46511627906976744, - 0.46511627906976744, - 0.46511627906976744, - 0.46511627906976744, - 0.46511627906976744, - 0.47674418604651164, - 0.47674418604651164, - 0.47674418604651164, - 0.47674418604651164, - 0.5, - 0.5116279069767442, - 0.5232558139534884, - 0.5232558139534884, - 0.5232558139534884, - 0.5232558139534884, - 0.5348837209302325, - 0.5348837209302325, - 0.5697674418604651, - 0.5813953488372093, - 0.6046511627906976, - 0.627906976744186, - 0.6627906976744186, - 0.6627906976744186, - 0.6744186046511628, - 0.6744186046511628, - 0.7093023255813954, - 0.7093023255813954, - 0.7325581395348837, - 0.7325581395348837, - 0.7441860465116279, - 0.7558139534883721, - 0.7674418604651163, - 0.7674418604651163, - 0.7790697674418605, - 0.8604651162790697, - 0.872093023255814, - 0.872093023255814, - 0.8837209302325582, - 0.8953488372093024, - 0.9186046511627907, - 0.9186046511627907, - 0.9186046511627907, - 0.9418604651162791, - 0.9418604651162791, - 0.9534883720930233, - 0.9534883720930233, - 0.9651162790697675, - 0.9883720930232558, - 1.0 - ], - "tpr": [ - 0.0, - 0.03015075376884422, - 0.04522613065326633, - 0.06030150753768844, - 0.08542713567839195, - 0.09045226130653267, - 0.10552763819095477, - 0.11557788944723618, - 0.12060301507537688, - 0.12060301507537688, - 0.1306532663316583, - 0.1407035175879397, - 0.1457286432160804, - 0.1507537688442211, - 0.16080402010050251, - 0.1658291457286432, - 0.18090452261306533, - 0.18090452261306533, - 0.20100502512562815, - 0.21105527638190955, - 0.22110552763819097, - 0.24120603015075376, - 0.24120603015075376, - 0.2613065326633166, - 0.2613065326633166, - 0.271356783919598, - 0.3417085427135678, - 0.3417085427135678, - 0.35175879396984927, - 0.36180904522613067, - 0.36180904522613067, - 0.37185929648241206, - 0.3768844221105528, - 0.3768844221105528, - 0.38190954773869346, - 0.4221105527638191, - 0.4321608040201005, - 0.4371859296482412, - 0.44221105527638194, - 0.45226130653266333, - 0.46733668341708545, - 0.4824120603015075, - 0.49246231155778897, - 0.49246231155778897, - 0.507537688442211, - 0.5276381909547738, - 0.5326633165829145, - 0.5326633165829145, - 0.542713567839196, - 0.542713567839196, - 0.5477386934673367, - 0.5778894472361809, - 0.6080402010050251, - 0.6130653266331658, - 0.6331658291457286, - 0.6432160804020101, - 0.6432160804020101, - 0.6482412060301508, - 0.6633165829145728, - 0.6683417085427136, - 0.678391959798995, - 0.6884422110552764, - 0.6884422110552764, - 0.6934673366834171, - 0.7035175879396985, - 0.7135678391959799, - 0.7135678391959799, - 0.7185929648241206, - 0.7286432160804021, - 0.7336683417085427, - 0.7487437185929648, - 0.7587939698492462, - 0.7688442211055276, - 0.7738693467336684, - 0.7889447236180904, - 0.7889447236180904, - 0.7989949748743719, - 0.7989949748743719, - 0.7989949748743719, - 0.8090452261306532, - 0.8190954773869347, - 0.8291457286432161, - 0.8291457286432161, - 0.8542713567839196, - 0.8542713567839196, - 0.864321608040201, - 0.864321608040201, - 0.8743718592964824, - 0.8743718592964824, - 0.8793969849246231, - 0.8793969849246231, - 0.9195979899497487, - 0.9246231155778895, - 0.9296482412060302, - 0.9296482412060302, - 0.9346733668341709, - 0.9396984924623115, - 0.949748743718593, - 0.9547738693467337, - 0.9597989949748744, - 0.9748743718592965, - 0.9748743718592965, - 0.9849246231155779, - 0.9899497487437185, - 0.9899497487437185, - 1.0 - ], - "roc_thresh": [ - 1.9703474182058294, - 0.9703474182058293, - 0.9689585293169404, - 0.966787027794429, - 0.9661987925003114, - 0.9660727420801433, - 0.965365459166978, - 0.9628427000633366, - 0.9622041433520281, - 0.9592315889522253, - 0.9493641364156552, - 0.9466037147831534, - 0.9459787147831534, - 0.9430574352269917, - 0.932056687492705, - 0.9164119335897968, - 0.9106005490672604, - 0.908734673740569, - 0.90596025261011, - 0.9046331849409371, - 0.9031883227855486, - 0.9026229320359473, - 0.9025776972586342, - 0.9023424031409871, - 0.9023014113819647, - 0.9008697236200562, - 0.8999194973757124, - 0.8970438012440881, - 0.8948220858012951, - 0.8829854174955591, - 0.8815868160969578, - 0.8806754759750913, - 0.8792469045465198, - 0.879117771030129, - 0.8757410499941582, - 0.870538405950764, - 0.8566823257576256, - 0.8551438642191641, - 0.8528236027812557, - 0.8507402694479222, - 0.8503025447999861, - 0.8489416031835385, - 0.8455993571942337, - 0.8365010599806115, - 0.8046812088211075, - 0.7984708913607902, - 0.7782601785714824, - 0.7781897579374748, - 0.7753190021008942, - 0.7747975749966872, - 0.7740004735474117, - 0.7494302219845922, - 0.7465874331387258, - 0.741598996487313, - 0.7252551991213627, - 0.7200785017062361, - 0.7109350971802931, - 0.7092630192582152, - 0.7033198174744064, - 0.7025416073454943, - 0.7023198174744063, - 0.6982115079103128, - 0.6966752805161294, - 0.6915043824247913, - 0.686340365456477, - 0.6797773182849517, - 0.6795903654564771, - 0.6656357046733571, - 0.64915339646728, - 0.6171530353184493, - 0.6143143326678614, - 0.6115202150208026, - 0.6114091039096915, - 0.6083535483541359, - 0.6045446884403999, - 0.601914751856516, - 0.5985323989153395, - 0.5983018312975428, - 0.5955410680240086, - 0.5944617029446435, - 0.5932758436956661, - 0.5888379497769849, - 0.5714692514733993, - 0.47910201427091864, - 0.40626063816048785, - 0.3909802324614022, - 0.3840585080680949, - 0.38133140596599285, - 0.3350066457346368, - 0.33083891069190163, - 0.3205286821197588, - 0.3173857334832004, - 0.3152834607559277, - 0.31409298456545154, - 0.30928152151869437, - 0.3081990524901292, - 0.3044385482884485, - 0.2990952950504584, - 0.2975785291929218, - 0.2948848764970493, - 0.24370259941237177, - 0.24226730529472465, - 0.20309776503451885, - 0.20230559869484313, - 0.19397587140527223, - 0.18503257929819295 - ], - "n_pos_test_examples": 199.0, - "n_neg_test_examples": 86.0 - }, - "instance_1": { - "TPR": 0.84422111, - "FPR": 0.70930233, - "FAR": 0.26637555, - "TNR": 0.29069767, - "PPV": 0.73362445, - "NPV": 0.44642857, - "FNR": 0.15577889, - "ACC": 0.67719298, - "F1score": 0.78504673, - "Advantage": 0.13491878000000002, - "AUC": 0.67523665, - "P_HIGHER_AUC": 1.327916290039255e-06, - "FMAX01": 0.8275862068965517, - "FMIN01": 0.5862068965517241, - "FDIF01": 0.24137931034482762, - "PDIF01": 3.7889078224645787, - "FMAX02": 0.8596491228070176, - "FMIN02": 0.543859649122807, - "FDIF02": 0.3157894736842105, - "PDIF02": 9.028137067022724, - "FMAX001": 0.6666666666666666, - "FMIN001": 0.0, - "FDIF001": 0.6666666666666666, - "PDIF001": 3.2797542278361775, - "pred_prob_var": 0.030572333939640803, - "TPR@0.5": 0.7738693467336684, - "TPR@0.2": 0.4723618090452261, - "TPR@0.1": 0.29748743718593007, - "TPR@0.01": 0.008643216080402012, - "TPR@0.001": 0.000864321608040201, - "TPR@1e-05": 8.643216080402721e-06, - "fpr": [ - 0.0, - 0.011627906976744186, - 0.023255813953488372, - 0.023255813953488372, - 0.023255813953488372, - 0.023255813953488372, - 0.03488372093023256, - 0.03488372093023256, - 0.05813953488372093, - 0.05813953488372093, - 0.05813953488372093, - 0.06976744186046512, - 0.08139534883720931, - 0.08139534883720931, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.09302325581395349, - 0.10465116279069768, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.11627906976744186, - 0.13953488372093023, - 0.1511627906976744, - 0.1511627906976744, - 0.1511627906976744, - 0.16279069767441862, - 0.1744186046511628, - 0.1744186046511628, - 0.19767441860465115, - 0.19767441860465115, - 0.20930232558139536, - 0.22093023255813954, - 0.22093023255813954, - 0.23255813953488372, - 0.2558139534883721, - 0.2558139534883721, - 0.26744186046511625, - 0.26744186046511625, - 0.27906976744186046, - 0.29069767441860467, - 0.29069767441860467, - 0.3023255813953488, - 0.3023255813953488, - 0.3023255813953488, - 0.3023255813953488, - 0.313953488372093, - 0.32558139534883723, - 0.3488372093023256, - 0.3488372093023256, - 0.36046511627906974, - 0.36046511627906974, - 0.37209302325581395, - 0.37209302325581395, - 0.37209302325581395, - 0.37209302325581395, - 0.37209302325581395, - 0.37209302325581395, - 0.38372093023255816, - 0.38372093023255816, - 0.4186046511627907, - 0.4186046511627907, - 0.4186046511627907, - 0.4418604651162791, - 0.4418604651162791, - 0.4883720930232558, - 0.4883720930232558, - 0.5116279069767442, - 0.5116279069767442, - 0.5232558139534884, - 0.5232558139534884, - 0.5232558139534884, - 0.5232558139534884, - 0.5465116279069767, - 0.5581395348837209, - 0.5581395348837209, - 0.5697674418604651, - 0.5697674418604651, - 0.6046511627906976, - 0.627906976744186, - 0.6395348837209303, - 0.6627906976744186, - 0.6627906976744186, - 0.6744186046511628, - 0.6744186046511628, - 0.686046511627907, - 0.6976744186046512, - 0.7093023255813954, - 0.7093023255813954, - 0.7325581395348837, - 0.7325581395348837, - 0.7441860465116279, - 0.7441860465116279, - 0.7441860465116279, - 0.7906976744186046, - 0.8023255813953488, - 0.8255813953488372, - 0.8255813953488372, - 0.8372093023255814, - 0.8372093023255814, - 0.8488372093023255, - 0.8604651162790697, - 0.872093023255814, - 0.872093023255814, - 0.9069767441860465, - 0.9069767441860465, - 0.9302325581395349, - 0.9302325581395349, - 0.9534883720930233, - 0.9534883720930233, - 1.0 - ], - "tpr": [ - 0.0, - 0.010050251256281407, - 0.035175879396984924, - 0.04020100502512563, - 0.05025125628140704, - 0.06532663316582915, - 0.06532663316582915, - 0.07035175879396985, - 0.09547738693467336, - 0.1306532663316583, - 0.1457286432160804, - 0.1507537688442211, - 0.1507537688442211, - 0.16080402010050251, - 0.16080402010050251, - 0.17587939698492464, - 0.22110552763819097, - 0.22613065326633167, - 0.23618090452261306, - 0.2562814070351759, - 0.271356783919598, - 0.27638190954773867, - 0.2914572864321608, - 0.3015075376884422, - 0.3065326633165829, - 0.32663316582914576, - 0.33668341708542715, - 0.36180904522613067, - 0.37185929648241206, - 0.38190954773869346, - 0.39195979899497485, - 0.3969849246231156, - 0.4221105527638191, - 0.4271356783919598, - 0.4371859296482412, - 0.4472361809045226, - 0.4472361809045226, - 0.457286432160804, - 0.46733668341708545, - 0.46733668341708545, - 0.4723618090452261, - 0.4723618090452261, - 0.47738693467336685, - 0.48743718592964824, - 0.49246231155778897, - 0.49246231155778897, - 0.49748743718592964, - 0.5025125628140703, - 0.507537688442211, - 0.507537688442211, - 0.5125628140703518, - 0.5175879396984925, - 0.5175879396984925, - 0.5226130653266332, - 0.5326633165829145, - 0.5577889447236181, - 0.5678391959798995, - 0.5728643216080402, - 0.5829145728643216, - 0.5879396984924623, - 0.5879396984924623, - 0.592964824120603, - 0.592964824120603, - 0.6080402010050251, - 0.628140703517588, - 0.6381909547738693, - 0.6482412060301508, - 0.6683417085427136, - 0.6733668341708543, - 0.678391959798995, - 0.6984924623115578, - 0.7035175879396985, - 0.7135678391959799, - 0.7236180904522613, - 0.7487437185929648, - 0.7688442211055276, - 0.7738693467336684, - 0.7738693467336684, - 0.7788944723618091, - 0.7839195979899497, - 0.7889447236180904, - 0.7989949748743719, - 0.8040201005025126, - 0.8040201005025126, - 0.8040201005025126, - 0.8090452261306532, - 0.8090452261306532, - 0.8140703517587939, - 0.8140703517587939, - 0.8241206030150754, - 0.8241206030150754, - 0.8291457286432161, - 0.8341708542713567, - 0.8341708542713567, - 0.8391959798994975, - 0.8391959798994975, - 0.8442211055276382, - 0.8442211055276382, - 0.8592964824120602, - 0.8592964824120602, - 0.8693467336683417, - 0.8743718592964824, - 0.8894472361809045, - 0.8944723618090452, - 0.8994974874371859, - 0.8994974874371859, - 0.8994974874371859, - 0.9045226130653267, - 0.9045226130653267, - 0.9095477386934674, - 0.9095477386934674, - 0.914572864321608, - 0.914572864321608, - 0.9195979899497487, - 0.9195979899497487, - 0.9346733668341709, - 0.9447236180904522, - 0.9597989949748744, - 0.9748743718592965, - 0.9899497487437185, - 1.0 - ], - "roc_thresh": [ - 1.9749060433637637, - 0.9749060433637637, - 0.9740131862209067, - 0.9737130609076234, - 0.9734576306653513, - 0.9726883998961204, - 0.971355066562787, - 0.9679822182208982, - 0.9265289582849917, - 0.925862291618325, - 0.9199021971891903, - 0.9153325860180164, - 0.9143066119920422, - 0.9079765952346063, - 0.9004849173218757, - 0.8834469090141688, - 0.8794487194944586, - 0.8705633833551015, - 0.8653919319994395, - 0.8301691622081709, - 0.8284555793562037, - 0.826959222041348, - 0.8250808988363826, - 0.82410903182871, - 0.8238646269838371, - 0.822251852656347, - 0.821670900618445, - 0.8213005244197347, - 0.8209513340820851, - 0.8182236013428114, - 0.8178010072846996, - 0.8167411296467683, - 0.8152872093037132, - 0.8147404086965736, - 0.8141842681272426, - 0.8134122093037132, - 0.8111361934313677, - 0.8110250828446246, - 0.8094122093037132, - 0.808867753963609, - 0.8074536424128249, - 0.8072695553890006, - 0.807252541646386, - 0.8046163583852028, - 0.8035858749797193, - 0.8007182279208959, - 0.7998767108193826, - 0.799259077590256, - 0.7987509626997494, - 0.7972819971251269, - 0.7972285014261077, - 0.7971380594739429, - 0.7953194183970862, - 0.7934986458313453, - 0.7915940043184113, - 0.7674217328663105, - 0.7657433408468528, - 0.7562477823234733, - 0.7515485953316035, - 0.7498167946451307, - 0.7495457962497929, - 0.7493564365888649, - 0.748406176366752, - 0.7464473056870868, - 0.7460789177713382, - 0.746019544860841, - 0.7449445448608409, - 0.7420952403404967, - 0.7413612115275076, - 0.7409178939223808, - 0.7388860572900614, - 0.7358528258215283, - 0.7358498176754178, - 0.7173962960518204, - 0.7036513911933255, - 0.6935685510466983, - 0.6749874591091501, - 0.6718091005991366, - 0.6705098641430415, - 0.6698027934359709, - 0.6626941515898644, - 0.6613649373062891, - 0.6510350353792775, - 0.5905159089915215, - 0.5903592486440716, - 0.589206526635932, - 0.5874732842227582, - 0.5871874563822125, - 0.5814908743897292, - 0.5811071299541823, - 0.5556041728520532, - 0.5508835379314183, - 0.5251784306704205, - 0.5227705568521348, - 0.52086637251546, - 0.5155391633069253, - 0.5096154179346505, - 0.5087536582724826, - 0.49903654334664455, - 0.49881163876473444, - 0.4986183592904944, - 0.49767803552586803, - 0.4959607779527677, - 0.4880557369684706, - 0.48248632913718664, - 0.48218216059939983, - 0.4785109544402106, - 0.4741112890363157, - 0.4733300515427852, - 0.46946626869920316, - 0.46466938440671757, - 0.4582942544389536, - 0.45681133688979914, - 0.45489721397101335, - 0.44982756134092516, - 0.4442773221064754, - 0.39449291205116177, - 0.36615224085912246, - 0.3402065749022259, - 0.3277278470465626, - 0.325911990269069 - ], - "n_pos_test_examples": 199.0, - "n_neg_test_examples": 86.0 - } - }, - "attack_metric_failfast_summary": { - "success_count": 2, - "fail_count": 0 - } - } - } -} diff --git a/docs/source/attacks/report_example_lira.json b/docs/source/attacks/report_example_lira.json new file mode 100644 index 00000000..c623d470 --- /dev/null +++ b/docs/source/attacks/report_example_lira.json @@ -0,0 +1,699 @@ +{ + "LiRA Attack_0d8cf41e-cb4a-4bf7-8a21-74a1998fb8b7": { + "log_id": "0d8cf41e-cb4a-4bf7-8a21-74a1998fb8b7", + "log_time": "30/06/2024 18:34:53", + "metadata": { + "attack_name": "LiRA Attack", + "attack_params": { + "output_dir": "outputs_lira", + "write_report": true, + "n_shadow_models": 100, + "p_thresh": 0.05 + }, + "global_metrics": { + "PDIF_sig": "Significant at p=0.05", + "AUC_sig": "Significant at p=0.05", + "null_auc_3sd_range": "0.4207446718814698 -> 0.5792553281185302" + }, + "target_model": "RandomForestClassifier", + "target_model_params": { + "bootstrap": true, + "ccp_alpha": 0.0, + "class_weight": null, + "criterion": "gini", + "max_depth": null, + "max_features": "sqrt", + "max_leaf_nodes": null, + "max_samples": null, + "min_impurity_decrease": 0.0, + "min_samples_leaf": 1, + "min_samples_split": 2, + "min_weight_fraction_leaf": 0.0, + "monotonic_cst": null, + "n_estimators": 100, + "n_jobs": null, + "oob_score": false, + "random_state": null, + "verbose": 0, + "warm_start": false + } + }, + "attack_experiment_logger": { + "attack_instance_logger": { + "instance_0": { + "TPR": 0.76884422, + "FPR": 0.49122807, + "FAR": 0.21538462, + "TNR": 0.50877193, + "PPV": 0.78461538, + "NPV": 0.48603352, + "FNR": 0.23115578, + "ACC": 0.69068541, + "F1score": 0.77664974, + "Advantage": 0.27761614999999995, + "AUC": 0.75869112, + "P_HIGHER_AUC": 0.0, + "FMAX01": 0.9824561403508771, + "FMIN01": 0.3333333333333333, + "FDIF01": 0.6491228070175439, + "PDIF01": 31.521864812068117, + "FMAX02": 0.956140350877193, + "FMIN02": 0.38596491228070173, + "FDIF02": 0.5701754385964912, + "PDIF02": 115.13, + "FMAX001": 1.0, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.6666666666666667, + "PDIF001": 5.134026454098573, + "pred_prob_var": 0.07234395378484082, + "TPR@0.5": 0.7776381909547895, + "TPR@0.2": 0.5703517587939698, + "TPR@0.1": 0.4321608040201005, + "TPR@0.01": 0.23618090452261306, + "TPR@0.001": 0.12814070351758794, + "TPR@1e-05": 0.12814070351758794, + "fpr": [ + 0.0, + 0.0, + 0.0, + 0.005847953216374269, + 0.005847953216374269, + 0.011695906432748537, + 0.011695906432748537, + 0.017543859649122806, + 0.017543859649122806, + 0.023391812865497075, + 0.023391812865497075, + 0.029239766081871343, + 0.029239766081871343, + 0.03508771929824561, + 0.03508771929824561, + 0.04093567251461988, + 0.04093567251461988, + 0.05263157894736842, + 0.05263157894736842, + 0.05847953216374269, + 0.05847953216374269, + 0.06432748538011696, + 0.06432748538011696, + 0.07017543859649122, + 0.07017543859649122, + 0.07602339181286549, + 0.07602339181286549, + 0.08187134502923976, + 0.08187134502923976, + 0.08771929824561403, + 0.08771929824561403, + 0.0935672514619883, + 0.0935672514619883, + 0.09941520467836257, + 0.09941520467836257, + 0.10526315789473684, + 0.10526315789473684, + 0.1111111111111111, + 0.1111111111111111, + 0.1286549707602339, + 0.1286549707602339, + 0.13450292397660818, + 0.13450292397660818, + 0.14035087719298245, + 0.14035087719298245, + 0.14619883040935672, + 0.14619883040935672, + 0.15204678362573099, + 0.15204678362573099, + 0.15789473684210525, + 0.15789473684210525, + 0.1695906432748538, + 0.1695906432748538, + 0.17543859649122806, + 0.17543859649122806, + 0.1871345029239766, + 0.1871345029239766, + 0.1871345029239766, + 0.1871345029239766, + 0.19298245614035087, + 0.19298245614035087, + 0.2046783625730994, + 0.2046783625730994, + 0.21052631578947367, + 0.21052631578947367, + 0.21637426900584794, + 0.21637426900584794, + 0.22807017543859648, + 0.22807017543859648, + 0.23976608187134502, + 0.23976608187134502, + 0.24561403508771928, + 0.24561403508771928, + 0.2573099415204678, + 0.2573099415204678, + 0.2631578947368421, + 0.2631578947368421, + 0.26900584795321636, + 0.26900584795321636, + 0.26900584795321636, + 0.26900584795321636, + 0.27485380116959063, + 0.27485380116959063, + 0.2807017543859649, + 0.2807017543859649, + 0.28654970760233917, + 0.28654970760233917, + 0.2982456140350877, + 0.2982456140350877, + 0.30994152046783624, + 0.3157894736842105, + 0.3157894736842105, + 0.3157894736842105, + 0.32748538011695905, + 0.32748538011695905, + 0.3333333333333333, + 0.3333333333333333, + 0.3333333333333333, + 0.3333333333333333, + 0.3391812865497076, + 0.3391812865497076, + 0.34502923976608185, + 0.34502923976608185, + 0.3508771929824561, + 0.3567251461988304, + 0.36257309941520466, + 0.36257309941520466, + 0.3684210526315789, + 0.3684210526315789, + 0.3684210526315789, + 0.3684210526315789, + 0.3742690058479532, + 0.39766081871345027, + 0.39766081871345027, + 0.4093567251461988, + 0.4152046783625731, + 0.4152046783625731, + 0.42105263157894735, + 0.4444444444444444, + 0.4502923976608187, + 0.4619883040935672, + 0.4619883040935672, + 0.47953216374269003, + 0.47953216374269003, + 0.4853801169590643, + 0.4853801169590643, + 0.49122807017543857, + 0.5087719298245614, + 0.5321637426900585, + 0.5321637426900585, + 0.5321637426900585, + 0.543859649122807, + 0.543859649122807, + 0.5497076023391813, + 0.5497076023391813, + 0.5555555555555556, + 0.5555555555555556, + 0.5614035087719298, + 0.5614035087719298, + 0.5672514619883041, + 0.5672514619883041, + 0.5730994152046783, + 0.5730994152046783, + 0.5847953216374269, + 0.5847953216374269, + 0.5906432748538012, + 0.5906432748538012, + 0.5964912280701754, + 0.5964912280701754, + 0.6023391812865497, + 0.6023391812865497, + 0.6081871345029239, + 0.6081871345029239, + 0.6257309941520468, + 0.6257309941520468, + 0.6374269005847953, + 0.6374269005847953, + 0.6432748538011696, + 0.6432748538011696, + 0.6491228070175439, + 0.6491228070175439, + 0.6608187134502924, + 0.6608187134502924, + 0.672514619883041, + 0.672514619883041, + 0.7017543859649122, + 0.7017543859649122, + 0.7426900584795322, + 0.7426900584795322, + 0.7485380116959064, + 0.7485380116959064, + 0.7543859649122807, + 0.7543859649122807, + 0.7602339181286549, + 0.7602339181286549, + 0.7660818713450293, + 0.7660818713450293, + 0.7953216374269005, + 0.7953216374269005, + 0.8011695906432749, + 0.8011695906432749, + 0.8070175438596491, + 0.8070175438596491, + 0.8245614035087719, + 0.8245614035087719, + 0.8304093567251462, + 0.8304093567251462, + 0.8362573099415205, + 0.8362573099415205, + 0.8538011695906432, + 0.8538011695906432, + 0.8596491228070176, + 0.8596491228070176, + 0.8654970760233918, + 0.8654970760233918, + 0.9122807017543859, + 0.9122807017543859, + 0.9532163742690059, + 0.9590643274853801, + 0.9766081871345029, + 0.9766081871345029, + 0.9824561403508771, + 0.9883040935672515, + 1.0 + ], + "tpr": [ + 0.0, + 0.01256281407035176, + 0.12814070351758794, + 0.12814070351758794, + 0.23618090452261306, + 0.23618090452261306, + 0.24874371859296482, + 0.24874371859296482, + 0.2562814070351759, + 0.2562814070351759, + 0.2613065326633166, + 0.2613065326633166, + 0.2914572864321608, + 0.2914572864321608, + 0.2989949748743719, + 0.2989949748743719, + 0.3341708542713568, + 0.3341708542713568, + 0.3391959798994975, + 0.3391959798994975, + 0.36683417085427134, + 0.36683417085427134, + 0.37185929648241206, + 0.37185929648241206, + 0.3844221105527638, + 0.3844221105527638, + 0.38944723618090454, + 0.38944723618090454, + 0.3969849246231156, + 0.3969849246231156, + 0.4045226130653266, + 0.4045226130653266, + 0.4221105527638191, + 0.4221105527638191, + 0.4321608040201005, + 0.4321608040201005, + 0.43467336683417085, + 0.43467336683417085, + 0.45226130653266333, + 0.45226130653266333, + 0.4899497487437186, + 0.4899497487437186, + 0.5125628140703518, + 0.5125628140703518, + 0.5150753768844221, + 0.5150753768844221, + 0.5201005025125628, + 0.5201005025125628, + 0.5276381909547738, + 0.5276381909547738, + 0.5452261306532663, + 0.5452261306532663, + 0.5477386934673367, + 0.5477386934673367, + 0.550251256281407, + 0.550251256281407, + 0.5603015075376885, + 0.5653266331658291, + 0.5678391959798995, + 0.5678391959798995, + 0.5703517587939698, + 0.5703517587939698, + 0.5778894472361809, + 0.5778894472361809, + 0.6005025125628141, + 0.6005025125628141, + 0.6030150753768844, + 0.6030150753768844, + 0.6080402010050251, + 0.6080402010050251, + 0.6105527638190955, + 0.6105527638190955, + 0.6180904522613065, + 0.6180904522613065, + 0.6231155778894473, + 0.6231155778894473, + 0.628140703517588, + 0.6306532663316583, + 0.635678391959799, + 0.6407035175879398, + 0.6457286432160804, + 0.6457286432160804, + 0.6532663316582915, + 0.6532663316582915, + 0.6582914572864321, + 0.6582914572864321, + 0.6608040201005025, + 0.6608040201005025, + 0.6633165829145728, + 0.6683417085427136, + 0.6708542713567839, + 0.6758793969849246, + 0.6809045226130653, + 0.6809045226130653, + 0.6859296482412061, + 0.6859296482412061, + 0.6884422110552764, + 0.6934673366834171, + 0.6959798994974874, + 0.6984924623115578, + 0.7085427135678392, + 0.7085427135678392, + 0.7160804020100503, + 0.7185929648241206, + 0.7286432160804021, + 0.7286432160804021, + 0.7311557788944724, + 0.7311557788944724, + 0.7361809045226131, + 0.7386934673366834, + 0.7437185929648241, + 0.7437185929648241, + 0.7487437185929648, + 0.7512562814070352, + 0.7512562814070352, + 0.7512562814070352, + 0.7537688442211056, + 0.7587939698492462, + 0.7587939698492462, + 0.7613065326633166, + 0.7613065326633166, + 0.7638190954773869, + 0.7638190954773869, + 0.7663316582914573, + 0.7663316582914573, + 0.7688442211055276, + 0.7688442211055276, + 0.7864321608040201, + 0.8190954773869347, + 0.8241206030150754, + 0.8291457286432161, + 0.8291457286432161, + 0.8316582914572864, + 0.8316582914572864, + 0.8341708542713567, + 0.8341708542713567, + 0.8366834170854272, + 0.8366834170854272, + 0.8467336683417085, + 0.8467336683417085, + 0.8492462311557789, + 0.8492462311557789, + 0.8542713567839196, + 0.8542713567839196, + 0.8768844221105527, + 0.8768844221105527, + 0.8944723618090452, + 0.8944723618090452, + 0.8969849246231156, + 0.8969849246231156, + 0.8994974874371859, + 0.8994974874371859, + 0.9020100502512562, + 0.9020100502512562, + 0.9045226130653267, + 0.9045226130653267, + 0.9095477386934674, + 0.9095477386934674, + 0.9120603015075377, + 0.9120603015075377, + 0.9195979899497487, + 0.9195979899497487, + 0.9221105527638191, + 0.9221105527638191, + 0.9271356783919598, + 0.9271356783919598, + 0.9296482412060302, + 0.9296482412060302, + 0.9321608040201005, + 0.9321608040201005, + 0.9346733668341709, + 0.9346733668341709, + 0.9396984924623115, + 0.9396984924623115, + 0.9422110552763819, + 0.9422110552763819, + 0.9522613065326633, + 0.9522613065326633, + 0.9547738693467337, + 0.9547738693467337, + 0.957286432160804, + 0.957286432160804, + 0.9623115577889447, + 0.9623115577889447, + 0.964824120603015, + 0.964824120603015, + 0.9673366834170855, + 0.9673366834170855, + 0.9773869346733668, + 0.9773869346733668, + 0.9798994974874372, + 0.9798994974874372, + 0.9824120603015075, + 0.9824120603015075, + 0.9899497487437185, + 0.9899497487437185, + 0.992462311557789, + 0.992462311557789, + 0.9949748743718593, + 0.9949748743718593, + 0.9974874371859297, + 0.9974874371859297, + 1.0, + 1.0 + ], + "roc_thresh": [ + Infinity, + 1.0, + 0.9997295737148277, + 0.9996517559988691, + 0.9782042095498769, + 0.9771588382176053, + 0.9666195732425267, + 0.964227517959063, + 0.9635392621950645, + 0.9622977418643489, + 0.9582915000745373, + 0.9582825877584221, + 0.9449855142906414, + 0.9430645292415549, + 0.9275078419531647, + 0.9268056529555889, + 0.8945514082889757, + 0.8896136037358711, + 0.879445154907128, + 0.8784824066769135, + 0.8523326827727193, + 0.8496432712851155, + 0.847546317322327, + 0.8469690055422092, + 0.8413000328822121, + 0.8412952813825597, + 0.8407377882099488, + 0.8375297162377666, + 0.8329741530774297, + 0.8329597413134081, + 0.8211336774769791, + 0.8162850263942258, + 0.8062954393224391, + 0.8040894857024858, + 0.7964614077991111, + 0.7956274370672685, + 0.7894033915630831, + 0.7893928397719702, + 0.7766024307805219, + 0.7751277280961939, + 0.7543514378920535, + 0.7543439252965495, + 0.7364384373488638, + 0.7341859940288324, + 0.7341690242050011, + 0.7335442611711884, + 0.7267532022967089, + 0.7267460800266611, + 0.7181336118941577, + 0.7141849077791924, + 0.7080587896148174, + 0.7061938992637833, + 0.7061938992637832, + 0.7035037620292894, + 0.7016493916353219, + 0.700910395815921, + 0.6980626640623229, + 0.6952383098245855, + 0.6952308669752427, + 0.6942735065232478, + 0.6883878810122829, + 0.6813240558830315, + 0.6794748664810916, + 0.6788890599726357, + 0.6650923212002915, + 0.6650862623063054, + 0.6636911919179302, + 0.661012278544606, + 0.6584543008451956, + 0.6525566988137632, + 0.6525566988137631, + 0.6525566988137629, + 0.6472715069443632, + 0.6453059424928868, + 0.6448267155022165, + 0.6448175011143846, + 0.6381631950841185, + 0.6381631950841182, + 0.6361382266652248, + 0.6354827552305977, + 0.6341997055200492, + 0.634199705520049, + 0.6305513320618845, + 0.6305504755424713, + 0.6282896511523737, + 0.6282824668957872, + 0.6269564920113957, + 0.6202685673100331, + 0.6184876997235024, + 0.6159587190495285, + 0.6135850036577761, + 0.6120811628910351, + 0.6113512946052388, + 0.6092376384957937, + 0.6072526264408226, + 0.6069859896265537, + 0.6066125339962983, + 0.6053659869328589, + 0.6048148078288289, + 0.6035755086858973, + 0.5958174220427449, + 0.5946178036750199, + 0.5907270958089782, + 0.584414795229118, + 0.5808717568070867, + 0.579259709439103, + 0.5792597094391029, + 0.57774036626353, + 0.5763051701566424, + 0.5749466304307371, + 0.5749466304307367, + 0.5736581578326786, + 0.5724339297076463, + 0.5712687790203796, + 0.5712687790203791, + 0.5701581024006671, + 0.5701581024006666, + 0.5690977834754943, + 0.568084128572636, + 0.5671138125036891, + 0.5518014512593604, + 0.5341217277257556, + 0.5271383817340841, + 0.5256559633367658, + 0.5224742578228889, + 0.5163823713841876, + 0.5000623847643585, + 0.5, + 0.4999997165344607, + 0.49999943306892136, + 0.48495099276413856, + 0.483809135017406, + 0.4822776605087658, + 0.47638300515361925, + 0.4742893993145948, + 0.47289081799488486, + 0.4661135962752618, + 0.45221952319437086, + 0.4375702213490566, + 0.4360754036979878, + 0.4262445138253022, + 0.425807595163003, + 0.41715956614052513, + 0.41262335483936324, + 0.39892671895036774, + 0.39879112421004426, + 0.38530837664442086, + 0.38520741264412706, + 0.3848165437623386, + 0.38362946261861863, + 0.3753501518897933, + 0.37501879493755574, + 0.3686965664328564, + 0.36180936886211246, + 0.36027203922070156, + 0.35983267810331687, + 0.3569214303954568, + 0.35620698398675726, + 0.3552743002037074, + 0.35522832361746515, + 0.35197780985557686, + 0.34139326054066144, + 0.34057112094255004, + 0.3393087179499329, + 0.338861411483949, + 0.33204409463920725, + 0.3254350757639142, + 0.30171247939092805, + 0.30030328215863367, + 0.29905022939455006, + 0.2969637716323285, + 0.29305116123633645, + 0.29180514955198805, + 0.2904091656058932, + 0.2899815008767104, + 0.28736993459777055, + 0.2818633748045013, + 0.24573843064913353, + 0.24567442118883975, + 0.2385744433127177, + 0.23688469364742532, + 0.22883846831156074, + 0.22716113249371161, + 0.22061032257707974, + 0.21898684106372823, + 0.2186573914320013, + 0.21519546306466997, + 0.2120257760895205, + 0.18258249873718746, + 0.1693146506540406, + 0.16327520868808776, + 0.16297846641229813, + 0.1626618708364604, + 0.1577408584926066, + 0.15044242321073076, + 0.11950158840601832, + 0.11774676613830187, + 0.0906831938788113, + 0.07530915900053184, + 0.022508288203730666, + 0.01575274152085758, + 0.007152939217714809, + 0.006209665325776123, + 0.0004555594385768564 + ], + "n_pos_test_examples": 398, + "n_neg_test_examples": 171 + } + } + } + } +} diff --git a/docs/source/attacks/report_example_worstcase.json b/docs/source/attacks/report_example_worstcase.json new file mode 100644 index 00000000..450f70f2 --- /dev/null +++ b/docs/source/attacks/report_example_worstcase.json @@ -0,0 +1,1191 @@ +{ + "WorstCase attack_ccd2def8-54f1-4d1f-8158-98c65d687db6": { + "log_id": "ccd2def8-54f1-4d1f-8158-98c65d687db6", + "log_time": "30/06/2024 18:34:39", + "metadata": { + "attack_name": "WorstCase attack", + "attack_params": { + "output_dir": "outputs_worstcase", + "write_report": true, + "n_reps": 10, + "reproduce_split": 5, + "p_thresh": 0.05, + "n_dummy_reps": 1, + "train_beta": 5, + "test_beta": 2, + "test_prop": 0.5, + "include_model_correct_feature": false, + "sort_probs": true, + "attack_model": "sklearn.ensemble.RandomForestClassifier", + "attack_model_params": null + }, + "global_metrics": { + "null_auc_3sd_range": "0.3880 -> 0.6120", + "n_sig_auc_p_vals": 4, + "n_sig_auc_p_vals_corrected": 0, + "n_sig_pdif_vals": 3, + "n_sig_pdif_vals_corrected": 0 + }, + "baseline_global_metrics": { + "null_auc_3sd_range": "0.3880 -> 0.6120", + "n_sig_auc_p_vals": 10, + "n_sig_auc_p_vals_corrected": 10, + "n_sig_pdif_vals": 8, + "n_sig_pdif_vals_corrected": 8 + }, + "target_model": "RandomForestClassifier", + "target_model_params": { + "bootstrap": true, + "ccp_alpha": 0.0, + "class_weight": null, + "criterion": "gini", + "max_depth": null, + "max_features": "sqrt", + "max_leaf_nodes": null, + "max_samples": null, + "min_impurity_decrease": 0.0, + "min_samples_leaf": 1, + "min_samples_split": 2, + "min_weight_fraction_leaf": 0.0, + "monotonic_cst": null, + "n_estimators": 100, + "n_jobs": null, + "oob_score": false, + "random_state": null, + "verbose": 0, + "warm_start": false + } + }, + "attack_experiment_logger": { + "attack_instance_logger": { + "instance_0": { + "TPR": 0.96482412, + "FPR": 0.88372093, + "FAR": 0.28358209, + "TNR": 0.11627907, + "PPV": 0.71641791, + "NPV": 0.58823529, + "FNR": 0.03517588, + "ACC": 0.70877193, + "F1score": 0.82226981, + "Advantage": 0.08110318999999999, + "AUC": 0.56909548, + "P_HIGHER_AUC": 0.032045980556885345, + "FMAX01": 0.6896551724137931, + "FMIN01": 0.5172413793103449, + "FDIF01": 0.1724137931034483, + "PDIF01": 2.5728670463946393, + "FMAX02": 0.7543859649122807, + "FMIN02": 0.543859649122807, + "FDIF02": 0.21052631578947367, + "PDIF02": 4.937449769004868, + "FMAX001": 0.6666666666666666, + "FMIN001": 0.0, + "FDIF001": 0.6666666666666666, + "PDIF001": 3.2797542278361775, + "pred_prob_var": 0.01129902576974124, + "TPR@0.5": 0.5998284103444051, + "TPR@0.2": 0.2361809045226127, + "TPR@0.1": 0.0914572864321608, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.023255813953488372, + 0.046511627906976744, + 0.046511627906976744, + 0.046511627906976744, + 0.06976744186046512, + 0.08139534883720931, + 0.08139534883720931, + 0.10465116279069768, + 0.10465116279069768, + 0.5813953488372093, + 0.6046511627906976, + 0.6046511627906976, + 0.6511627906976745, + 0.6511627906976745, + 0.7209302325581395, + 0.7674418604651163, + 0.8604651162790697, + 0.8604651162790697, + 0.8837209302325582, + 0.8837209302325582, + 0.9069767441860465, + 1.0 + ], + "tpr": [ + 0.0, + 0.020100502512562814, + 0.02512562814070352, + 0.04522613065326633, + 0.05527638190954774, + 0.06532663316582915, + 0.06532663316582915, + 0.07537688442211055, + 0.09547738693467336, + 0.12060301507537688, + 0.6984924623115578, + 0.7085427135678392, + 0.7185929648241206, + 0.7487437185929648, + 0.7537688442211056, + 0.8592964824120602, + 0.8894472361809045, + 0.9296482412060302, + 0.9346733668341709, + 0.964824120603015, + 0.9748743718592965, + 0.9798994974874372, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.825851798523959, + 0.8218557850396795, + 0.8104747175329332, + 0.8089833296917734, + 0.8001330648589553, + 0.7943774744719808, + 0.794355938857314, + 0.7931958099381825, + 0.7896072464207436, + 0.7515441159999184, + 0.7101152733748299, + 0.7059705365327247, + 0.6866424923999992, + 0.6821582446450714, + 0.6646040031837572, + 0.6619913740467628, + 0.6546474044736891, + 0.6190034766424983, + 0.6148670207690615, + 0.3662630297192441, + 0.3455145448707592, + 0.3171915330573382 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_1": { + "TPR": 0.98492462, + "FPR": 0.94186047, + "FAR": 0.29241877, + "TNR": 0.05813953, + "PPV": 0.70758123, + "NPV": 0.625, + "FNR": 0.01507538, + "ACC": 0.70526316, + "F1score": 0.82352941, + "Advantage": 0.04306415000000008, + "AUC": 0.5831483, + "P_HIGHER_AUC": 0.012936570328542674, + "FMAX01": 0.7586206896551724, + "FMIN01": 0.5862068965517241, + "FDIF01": 0.1724137931034483, + "PDIF01": 2.5728670463946393, + "FMAX02": 0.7719298245614035, + "FMIN02": 0.5614035087719298, + "FDIF02": 0.21052631578947367, + "PDIF02": 4.937449769004868, + "FMAX001": 0.3333333333333333, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.0, + "PDIF001": 0.6931471805599453, + "pred_prob_var": 0.00945597915192779, + "TPR@0.5": 0.6180904522613065, + "TPR@0.2": 0.27115062491946856, + "TPR@0.1": 0.15258342997036556, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.023255813953488372, + 0.03488372093023256, + 0.03488372093023256, + 0.03488372093023256, + 0.03488372093023256, + 0.4883720930232558, + 0.5116279069767442, + 0.5232558139534884, + 0.5232558139534884, + 0.5232558139534884, + 0.5465116279069767, + 0.5465116279069767, + 0.6627906976744186, + 0.6627906976744186, + 0.686046511627907, + 0.7093023255813954, + 0.7093023255813954, + 0.7674418604651163, + 0.7674418604651163, + 0.8837209302325582, + 0.9186046511627907, + 0.9186046511627907, + 0.9418604651162791, + 0.9418604651162791, + 0.9651162790697675, + 0.9767441860465116, + 1.0 + ], + "tpr": [ + 0.0, + 0.03015075376884422, + 0.04020100502512563, + 0.05025125628140704, + 0.07035175879396985, + 0.07537688442211055, + 0.6130653266331658, + 0.6231155778894473, + 0.6432160804020101, + 0.6482412060301508, + 0.6582914572864321, + 0.6683417085427136, + 0.6733668341708543, + 0.7839195979899497, + 0.7889447236180904, + 0.7989949748743719, + 0.8241206030150754, + 0.8291457286432161, + 0.8542713567839196, + 0.8592964824120602, + 0.9296482412060302, + 0.9798994974874372, + 0.9849246231155779, + 0.9849246231155779, + 0.9949748743718593, + 0.9949748743718593, + 0.9949748743718593, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8513212822109211, + 0.8304469787143121, + 0.8084817214429054, + 0.7839161177296797, + 0.7831477224396126, + 0.7512690543855228, + 0.7390341311655853, + 0.738690796330375, + 0.734489115658106, + 0.7313727748695517, + 0.7279848017315785, + 0.7277774939464844, + 0.7183390146869284, + 0.7155252864555163, + 0.7131856536289293, + 0.6816649267929662, + 0.6813765953973088, + 0.6771586502379751, + 0.6684955274283696, + 0.5981414432513413, + 0.5649437325008086, + 0.5480123238328595, + 0.5259297563029234, + 0.30409427990074767, + 0.2985387243451921, + 0.25462932672526817, + 0.2149051725899298 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_2": { + "TPR": 0.96482412, + "FPR": 0.88372093, + "FAR": 0.28358209, + "TNR": 0.11627907, + "PPV": 0.71641791, + "NPV": 0.58823529, + "FNR": 0.03517588, + "ACC": 0.70877193, + "F1score": 0.82226981, + "Advantage": 0.08110318999999999, + "AUC": 0.55296833, + "P_HIGHER_AUC": 0.07789366841416012, + "FMAX01": 0.8275862068965517, + "FMIN01": 0.5862068965517241, + "FDIF01": 0.24137931034482762, + "PDIF01": 3.7889078224645787, + "FMAX02": 0.7719298245614035, + "FMIN02": 0.6491228070175439, + "FDIF02": 0.12280701754385959, + "PDIF02": 2.5690929345644054, + "FMAX001": 0.6666666666666666, + "FMIN001": 0.0, + "FDIF001": 0.6666666666666666, + "PDIF001": 3.2797542278361775, + "pred_prob_var": 0.015035666511345275, + "TPR@0.5": 0.5755607304816734, + "TPR@0.2": 0.25618335580341034, + "TPR@0.1": 0.14972423091065262, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.011627906976744186, + 0.011627906976744186, + 0.023255813953488372, + 0.023255813953488372, + 0.023255813953488372, + 0.03488372093023256, + 0.5116279069767442, + 0.5232558139534884, + 0.5465116279069767, + 0.5581395348837209, + 0.5581395348837209, + 0.6627906976744186, + 0.7674418604651163, + 0.813953488372093, + 0.8255813953488372, + 0.8255813953488372, + 0.8255813953488372, + 0.8604651162790697, + 0.8604651162790697, + 0.8604651162790697, + 0.8837209302325582, + 0.8837209302325582, + 0.8953488372093024, + 1.0 + ], + "tpr": [ + 0.0, + 0.005025125628140704, + 0.010050251256281407, + 0.010050251256281407, + 0.03015075376884422, + 0.04522613065326633, + 0.08040201005025126, + 0.5879396984924623, + 0.5879396984924623, + 0.592964824120603, + 0.5979899497487438, + 0.6030150753768844, + 0.7386934673366834, + 0.8040201005025126, + 0.8341708542713567, + 0.8442211055276382, + 0.8542713567839196, + 0.8592964824120602, + 0.8844221105527639, + 0.8944723618090452, + 0.9045226130653267, + 0.964824120603015, + 0.9698492462311558, + 0.9698492462311558, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8202899572689847, + 0.8172998168513756, + 0.7887314516909629, + 0.7880171659766773, + 0.7828617988900791, + 0.778614673307755, + 0.7729828848639824, + 0.7675174883016952, + 0.7670355543000243, + 0.7378649061549046, + 0.7282783819801503, + 0.654930507739666, + 0.6395024771274006, + 0.6181759566956554, + 0.6071831054132137, + 0.587008016286654, + 0.5847746829533206, + 0.5780714975983569, + 0.5639588198690365, + 0.5177793130140738, + 0.5024347218829593, + 0.4645809947344861, + 0.38991798781872655, + 0.3191636524698799 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_3": { + "TPR": 0.93969849, + "FPR": 0.88372093, + "FAR": 0.28897338, + "TNR": 0.11627907, + "PPV": 0.71102662, + "NPV": 0.45454545, + "FNR": 0.06030151, + "ACC": 0.69122807, + "F1score": 0.80952381, + "Advantage": 0.05597756000000009, + "AUC": 0.5593666, + "P_HIGHER_AUC": 0.05582318792662222, + "FMAX01": 0.8275862068965517, + "FMIN01": 0.6206896551724138, + "FDIF01": 0.2068965517241379, + "PDIF01": 3.145420412944275, + "FMAX02": 0.7894736842105263, + "FMIN02": 0.6140350877192983, + "FDIF02": 0.17543859649122806, + "PDIF02": 3.8798010518537023, + "FMAX001": 1.0, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.6666666666666667, + "PDIF001": 3.2797542278361775, + "pred_prob_var": 0.012798467308000003, + "TPR@0.5": 0.6211055276381906, + "TPR@0.2": 0.23301120989563145, + "TPR@0.1": 0.11998453807499099, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.011627906976744186, + 0.46511627906976744, + 0.47674418604651164, + 0.47674418604651164, + 0.47674418604651164, + 0.47674418604651164, + 0.5930232558139535, + 0.627906976744186, + 0.6395348837209303, + 0.7093023255813954, + 0.7209302325581395, + 0.7325581395348837, + 0.7441860465116279, + 0.7906976744186046, + 0.8488372093023255, + 0.872093023255814, + 0.8837209302325582, + 0.8837209302325582, + 0.8837209302325582, + 0.9069767441860465, + 0.9069767441860465, + 1.0 + ], + "tpr": [ + 0.0, + 0.020100502512562814, + 0.5326633165829145, + 0.5527638190954773, + 0.5678391959798995, + 0.5879396984924623, + 0.5979899497487438, + 0.7135678391959799, + 0.7336683417085427, + 0.7386934673366834, + 0.7788944723618091, + 0.7788944723618091, + 0.7939698492462312, + 0.8190954773869347, + 0.864321608040201, + 0.9095477386934674, + 0.9095477386934674, + 0.9095477386934674, + 0.9296482412060302, + 0.9597989949748744, + 0.9748743718592965, + 0.9798994974874372, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8055012041737802, + 0.765249795558943, + 0.7640488764917515, + 0.7572252806664393, + 0.7276671968761054, + 0.7275088797030821, + 0.7150461527091959, + 0.6657425860853895, + 0.6654670073500106, + 0.6329528082691607, + 0.625760846716422, + 0.6225269059409951, + 0.6168391719000104, + 0.6145166041582106, + 0.6109521669782105, + 0.6042482894299627, + 0.6015293460170537, + 0.5936661522688782, + 0.4576087240067097, + 0.3529239309277856, + 0.34442125713099414, + 0.341096448946851 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_4": { + "TPR": 1.0, + "FPR": 1.0, + "FAR": 0.30175439, + "TNR": 0.0, + "PPV": 0.69824561, + "NPV": 0.0, + "FNR": 0.0, + "ACC": 0.69824561, + "F1score": 0.82231405, + "Advantage": 0.0, + "AUC": 0.52629426, + "P_HIGHER_AUC": 0.2405287486867479, + "FMAX01": 0.6551724137931034, + "FMIN01": 0.6896551724137931, + "FDIF01": -0.034482758620689724, + "PDIF01": 0.490070177552447, + "FMAX02": 0.6842105263157895, + "FMIN02": 0.5964912280701754, + "FDIF02": 0.08771929824561409, + "PDIF02": 1.8719978462658604, + "FMAX001": 0.6666666666666666, + "FMIN001": 1.0, + "FDIF001": -0.33333333333333337, + "PDIF001": 0.20689635800507752, + "pred_prob_var": 0.009054854614813016, + "TPR@0.5": 0.542713567839196, + "TPR@0.2": 0.18368766911479584, + "TPR@0.1": 0.08693467336683408, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.011627906976744186, + 0.011627906976744186, + 0.011627906976744186, + 0.023255813953488372, + 0.05813953488372093, + 0.19767441860465115, + 0.19767441860465115, + 0.6511627906976745, + 0.6627906976744186, + 0.6627906976744186, + 0.6744186046511628, + 0.686046511627907, + 0.7093023255813954, + 0.7093023255813954, + 0.7441860465116279, + 0.7674418604651163, + 0.7674418604651163, + 0.7674418604651163, + 0.9302325581395349, + 1.0 + ], + "tpr": [ + 0.0, + 0.010050251256281407, + 0.01507537688442211, + 0.03015075376884422, + 0.035175879396984924, + 0.05527638190954774, + 0.16080402010050251, + 0.18090452261306533, + 0.7236180904522613, + 0.7587939698492462, + 0.7638190954773869, + 0.7788944723618091, + 0.7989949748743719, + 0.7989949748743719, + 0.8140703517587939, + 0.8391959798994975, + 0.8542713567839196, + 0.8592964824120602, + 0.8693467336683417, + 0.8994974874371859, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8434638635738719, + 0.8269245445997703, + 0.823121311025639, + 0.8186546443589724, + 0.806795431108455, + 0.778863545381092, + 0.7549608357378551, + 0.7528922098766166, + 0.7426499857261537, + 0.6455311332252057, + 0.645471085216948, + 0.6429100484276503, + 0.6152715216511607, + 0.6120117892816794, + 0.6111892357729977, + 0.5979672587313205, + 0.5928020488919485, + 0.5752023651746547, + 0.540510144237697, + 0.5007350287434608 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_5": { + "TPR": 0.83919598, + "FPR": 0.75581395, + "FAR": 0.28017241, + "TNR": 0.24418605, + "PPV": 0.71982759, + "NPV": 0.39622642, + "FNR": 0.16080402, + "ACC": 0.65964912, + "F1score": 0.774942, + "Advantage": 0.08338203, + "AUC": 0.56643684, + "P_HIGHER_AUC": 0.03751372489763849, + "FMAX01": 0.7586206896551724, + "FMIN01": 0.5517241379310345, + "FDIF01": 0.2068965517241379, + "PDIF01": 3.145420412944275, + "FMAX02": 0.7017543859649122, + "FMIN02": 0.5964912280701754, + "FDIF02": 0.10526315789473684, + "PDIF02": 2.203372254115738, + "FMAX001": 1.0, + "FMIN001": 0.6666666666666666, + "FDIF001": 0.33333333333333337, + "PDIF001": 1.677202524252041, + "pred_prob_var": 0.018153151102488482, + "TPR@0.5": 0.5807084201495276, + "TPR@0.2": 0.2644931976957961, + "TPR@0.1": 0.1590881235445527, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.023255813953488372, + 0.023255813953488372, + 0.03488372093023256, + 0.03488372093023256, + 0.5116279069767442, + 0.5348837209302325, + 0.5581395348837209, + 0.5581395348837209, + 0.5697674418604651, + 0.5697674418604651, + 0.5930232558139535, + 0.6976744186046512, + 0.6976744186046512, + 0.7209302325581395, + 0.7558139534883721, + 0.7790697674418605, + 0.7906976744186046, + 0.8837209302325582, + 1.0 + ], + "tpr": [ + 0.0, + 0.03015075376884422, + 0.04020100502512563, + 0.05025125628140704, + 0.09045226130653267, + 0.592964824120603, + 0.6030150753768844, + 0.6180904522613065, + 0.628140703517588, + 0.6331658291457286, + 0.6381909547738693, + 0.6532663316582915, + 0.7638190954773869, + 0.7688442211055276, + 0.7889447236180904, + 0.8391959798994975, + 0.8793969849246231, + 0.8844221105527639, + 0.9748743718592965, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8261715153209457, + 0.8218861877979522, + 0.8198406461227399, + 0.8158393550161854, + 0.7757303561753066, + 0.733928845882359, + 0.7313889685129985, + 0.7173737626034292, + 0.7099260127941234, + 0.707556353885556, + 0.7024452637903563, + 0.7010235363774677, + 0.6742364343972932, + 0.5989839441072702, + 0.5098201669510588, + 0.4629847527733464, + 0.4596417534513049, + 0.45865954509412676, + 0.3996597251024035 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_6": { + "TPR": 0.9798995, + "FPR": 0.90697674, + "FAR": 0.28571429, + "TNR": 0.09302326, + "PPV": 0.71428571, + "NPV": 0.66666667, + "FNR": 0.0201005, + "ACC": 0.7122807, + "F1score": 0.82627118, + "Advantage": 0.07292276000000009, + "AUC": 0.51846442, + "P_HIGHER_AUC": 0.310374935251162, + "FMAX01": 0.6896551724137931, + "FMIN01": 0.6551724137931034, + "FDIF01": 0.034482758620689724, + "PDIF01": 0.9482546870891132, + "FMAX02": 0.6666666666666666, + "FMIN02": 0.631578947368421, + "FDIF02": 0.03508771929824561, + "PDIF02": 1.0740943143454584, + "FMAX001": 0.6666666666666666, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.3333333333333333, + "PDIF001": 1.677202524252041, + "pred_prob_var": 0.009679543591272365, + "TPR@0.5": 0.5183493223694167, + "TPR@0.2": 0.1825125628140704, + "TPR@0.1": 0.1108040201005026, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.023255813953488372, + 0.03488372093023256, + 0.046511627906976744, + 0.046511627906976744, + 0.046511627906976744, + 0.18604651162790697, + 0.3023255813953488, + 0.3023255813953488, + 0.686046511627907, + 0.7325581395348837, + 0.7441860465116279, + 0.7674418604651163, + 0.8255813953488372, + 0.8372093023255814, + 0.8837209302325582, + 0.8837209302325582, + 0.8837209302325582, + 0.9069767441860465, + 0.9069767441860465, + 0.9069767441860465, + 0.9069767441860465, + 0.9302325581395349, + 0.9302325581395349, + 0.9418604651162791, + 1.0 + ], + "tpr": [ + 0.0, + 0.02512562814070352, + 0.03015075376884422, + 0.04522613065326633, + 0.05527638190954774, + 0.07035175879396985, + 0.17587939698492464, + 0.23115577889447236, + 0.23618090452261306, + 0.7839195979899497, + 0.8090452261306532, + 0.8140703517587939, + 0.8241206030150754, + 0.864321608040201, + 0.8693467336683417, + 0.9095477386934674, + 0.9195979899497487, + 0.9447236180904522, + 0.964824120603015, + 0.9748743718592965, + 0.9798994974874372, + 0.9899497487437185, + 0.9899497487437185, + 0.9949748743718593, + 0.9949748743718593, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.9097858406012286, + 0.9035828821639248, + 0.901809083934956, + 0.8238809617847177, + 0.8205387348207869, + 0.7744871240903618, + 0.7262561651636881, + 0.7211688292140535, + 0.7170275001718116, + 0.7117373724415502, + 0.6963231920643329, + 0.6885089356131451, + 0.6802355988607814, + 0.6759068463832812, + 0.6495040382865638, + 0.6422862535567814, + 0.6250097943332138, + 0.617792371153308, + 0.6024439050253889, + 0.5634070234293815, + 0.446211863187721, + 0.430940604179133, + 0.29757684611919305, + 0.28774680948915643, + 0.26232445127461557 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_7": { + "TPR": 0.96984925, + "FPR": 0.94186047, + "FAR": 0.29562044, + "TNR": 0.05813953, + "PPV": 0.70437956, + "NPV": 0.45454545, + "FNR": 0.03015075, + "ACC": 0.69473684, + "F1score": 0.81606765, + "Advantage": 0.027988780000000046, + "AUC": 0.51098516, + "P_HIGHER_AUC": 0.3842385757720995, + "FMAX01": 0.6896551724137931, + "FMIN01": 0.6896551724137931, + "FDIF01": 0.0, + "PDIF01": 0.6931471805599453, + "FMAX02": 0.6666666666666666, + "FMIN02": 0.631578947368421, + "FDIF02": 0.03508771929824561, + "PDIF02": 1.0740943143454584, + "FMAX001": 0.3333333333333333, + "FMIN001": 0.0, + "FDIF001": 0.3333333333333333, + "PDIF001": 1.677202524252041, + "pred_prob_var": 0.010094474450982082, + "TPR@0.5": 0.5175879396984759, + "TPR@0.2": 0.17432617633622682, + "TPR@0.1": 0.09547738693467336, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.023255813953488372, + 0.09302325581395349, + 0.09302325581395349, + 0.09302325581395349, + 0.10465116279069768, + 0.10465116279069768, + 0.16279069767441862, + 0.29069767441860467, + 0.7093023255813954, + 0.7325581395348837, + 0.7906976744186046, + 0.8023255813953488, + 0.813953488372093, + 0.813953488372093, + 0.8488372093023255, + 0.8488372093023255, + 0.8488372093023255, + 0.9418604651162791, + 0.9418604651162791, + 0.9534883720930233, + 0.9534883720930233, + 1.0 + ], + "tpr": [ + 0.0, + 0.010050251256281407, + 0.035175879396984924, + 0.06532663316582915, + 0.09547738693467336, + 0.09547738693467336, + 0.10552763819095477, + 0.1407035175879397, + 0.2562814070351759, + 0.7788944723618091, + 0.7989949748743719, + 0.8291457286432161, + 0.8291457286432161, + 0.8391959798994975, + 0.8592964824120602, + 0.8693467336683417, + 0.8844221105527639, + 0.8944723618090452, + 0.9547738693467337, + 0.9698492462311558, + 0.9899497487437185, + 0.9949748743718593, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8771667228069415, + 0.8598245489544017, + 0.8441576576870246, + 0.7941789725331843, + 0.7900203181500716, + 0.7895597588933019, + 0.7599307930442774, + 0.7402892971324565, + 0.7385359534097491, + 0.7233906050379314, + 0.7116962084842894, + 0.6714305415970125, + 0.6701156060572443, + 0.6576117859775603, + 0.6520188483104864, + 0.643909675130725, + 0.6350865701311492, + 0.6197880205488494, + 0.6003118113442221, + 0.4525020316378479, + 0.21013072326223456, + 0.1936574631889745 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_8": { + "TPR": 0.96482412, + "FPR": 0.88372093, + "FAR": 0.28358209, + "TNR": 0.11627907, + "PPV": 0.71641791, + "NPV": 0.58823529, + "FNR": 0.03517588, + "ACC": 0.70877193, + "F1score": 0.82226981, + "Advantage": 0.08110318999999999, + "AUC": 0.58043123, + "P_HIGHER_AUC": 0.0155691146749396, + "FMAX01": 0.7241379310344828, + "FMIN01": 0.5517241379310345, + "FDIF01": 0.1724137931034483, + "PDIF01": 2.5728670463946393, + "FMAX02": 0.6666666666666666, + "FMIN02": 0.6491228070175439, + "FDIF02": 0.01754385964912275, + "PDIF02": 0.8695004910111879, + "FMAX001": 0.6666666666666666, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.3333333333333333, + "PDIF001": 1.677202524252041, + "pred_prob_var": 0.011564092255346407, + "TPR@0.5": 0.6231155778894473, + "TPR@0.2": 0.26982412060291355, + "TPR@0.1": 0.1520603015074023, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.011627906976744186, + 0.011627906976744186, + 0.023255813953488372, + 0.03488372093023256, + 0.5, + 0.6162790697674418, + 0.627906976744186, + 0.627906976744186, + 0.6395348837209303, + 0.6511627906976745, + 0.686046511627907, + 0.7209302325581395, + 0.813953488372093, + 0.8488372093023255, + 0.8488372093023255, + 0.8488372093023255, + 0.8488372093023255, + 0.8837209302325582, + 0.8837209302325582, + 0.8837209302325582, + 0.8953488372093024, + 0.8953488372093024, + 0.9186046511627907, + 0.9302325581395349, + 1.0 + ], + "tpr": [ + 0.0, + 0.005025125628140704, + 0.05025125628140704, + 0.07035175879396985, + 0.07537688442211055, + 0.6231155778894473, + 0.7336683417085427, + 0.7537688442211056, + 0.7587939698492462, + 0.7587939698492462, + 0.7788944723618091, + 0.7989949748743719, + 0.8040201005025126, + 0.8542713567839196, + 0.8844221105527639, + 0.8944723618090452, + 0.9045226130653267, + 0.9095477386934674, + 0.9547738693467337, + 0.9748743718592965, + 0.9798994974874372, + 0.9849246231155779, + 0.9899497487437185, + 0.9949748743718593, + 0.9949748743718593, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.8205138082234891, + 0.7982093484334317, + 0.790949774988966, + 0.7562673481083729, + 0.7561235980138686, + 0.7337630210370137, + 0.6847111810995622, + 0.6747689128450278, + 0.6728599665937673, + 0.6669166982678902, + 0.6465284467618866, + 0.6464860997370003, + 0.639238051725342, + 0.6205824675626717, + 0.6056147611256161, + 0.603989761125616, + 0.5779583338736679, + 0.5752205730379312, + 0.40442486101613645, + 0.40060907154245223, + 0.35755113108868886, + 0.350943988231546, + 0.3342500884494109, + 0.32883342178274433, + 0.3220932257043129 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + }, + "instance_9": { + "TPR": 0.9798995, + "FPR": 0.87209302, + "FAR": 0.27777778, + "TNR": 0.12790698, + "PPV": 0.72222222, + "NPV": 0.73333333, + "FNR": 0.0201005, + "ACC": 0.72280702, + "F1score": 0.8315565, + "Advantage": 0.10780648000000004, + "AUC": 0.5411067, + "P_HIGHER_AUC": 0.1353332183240572, + "FMAX01": 0.5862068965517241, + "FMIN01": 0.4827586206896552, + "FDIF01": 0.1034482758620689, + "PDIF01": 1.6327185713531083, + "FMAX02": 0.7192982456140351, + "FMIN02": 0.631578947368421, + "FDIF02": 0.08771929824561409, + "PDIF02": 1.8719978462658604, + "FMAX001": 0.6666666666666666, + "FMIN001": 0.3333333333333333, + "FDIF001": 0.3333333333333333, + "PDIF001": 1.677202524252041, + "pred_prob_var": 0.007887712252135689, + "TPR@0.5": 0.5310764348056368, + "TPR@0.2": 0.1864850568632887, + "TPR@0.1": 0.07162126421579207, + "TPR@0.01": 0.0, + "TPR@0.001": 0.0, + "TPR@1e-05": 0.0, + "fpr": [ + 0.0, + 0.011627906976744186, + 0.05813953488372093, + 0.08139534883720931, + 0.08139534883720931, + 0.5232558139534884, + 0.5348837209302325, + 0.5348837209302325, + 0.5348837209302325, + 0.5465116279069767, + 0.5581395348837209, + 0.5697674418604651, + 0.5813953488372093, + 0.5813953488372093, + 0.6511627906976745, + 0.6511627906976745, + 0.6744186046511628, + 0.6744186046511628, + 0.7674418604651163, + 0.7906976744186046, + 0.872093023255814, + 0.872093023255814, + 0.8837209302325582, + 1.0 + ], + "tpr": [ + 0.0, + 0.010050251256281407, + 0.01507537688442211, + 0.035175879396984924, + 0.05025125628140704, + 0.5577889447236181, + 0.5829145728643216, + 0.592964824120603, + 0.6030150753768844, + 0.6080402010050251, + 0.628140703517588, + 0.6733668341708543, + 0.6934673366834171, + 0.6984924623115578, + 0.7336683417085427, + 0.7386934673366834, + 0.7487437185929648, + 0.7587939698492462, + 0.8894472361809045, + 0.9246231155778895, + 0.9748743718592965, + 0.9849246231155779, + 0.9849246231155779, + 1.0 + ], + "roc_thresh": [ + Infinity, + 0.7783395039054761, + 0.7761026618002128, + 0.76706011235259, + 0.754159623207495, + 0.7533654056104558, + 0.7524162676774053, + 0.7521296223539515, + 0.7451262852415619, + 0.7346109631271165, + 0.7225058059697499, + 0.7217423920793745, + 0.7176616795675388, + 0.7138756156680564, + 0.7121850329229279, + 0.7116090389605252, + 0.7030902798286865, + 0.6647350490735255, + 0.6406580287365936, + 0.6019440210478921, + 0.5938086673704709, + 0.4474812035703419, + 0.40615749289437986, + 0.40080035003723696 + ], + "n_pos_test_examples": 199.0, + "n_neg_test_examples": 86.0 + } + } + } + } +} diff --git a/docs/source/conf.py b/docs/source/conf.py index 0ba94786..2f34946a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,7 +13,7 @@ project = "AI-SDC" copyright = "2024, GRAIMATTER and SACRO Project Team" author = "GRAIMATTER and SACRO Project Team" -release = "1.1.3" +release = "1.2.0" # -- General configuration --------------------------------------------------- diff --git a/examples/MIAandAIA_attacks_example.py b/examples/MIAandAIA_attacks_example.py deleted file mode 100644 index f9f4c532..00000000 --- a/examples/MIAandAIA_attacks_example.py +++ /dev/null @@ -1,206 +0,0 @@ -"""Examples for running multiple attacks. - -Includes the Membership Inference Attack and the Attribute Inference Attack -with a single configuration file have multiple configurations. - -Below, [Researcher] and [TRE] are used to denote which task is performed by whom. -""" - -import json -import os -import sys - -import numpy as np -from sklearn.datasets import fetch_openml -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import LabelEncoder, OneHotEncoder - -from aisdc.attacks.multiple_attacks import ConfigFile, MultipleAttacks -from aisdc.attacks.target import Target - -sys.path.append(os.path.dirname(os.path.dirname(__file__))) - -if __name__ == "__main__": - # [Researcher] Access a dataset - nursery_data = fetch_openml(data_id=26, as_frame=True) - X = np.asarray(nursery_data.data, dtype=str) - y = np.asarray(nursery_data.target, dtype=str) - n_features = np.shape(X)[1] - indices: list[list[int]] = [ - [0, 1, 2], # parents - [3, 4, 5, 6, 7], # has_nurs - [8, 9, 10, 11], # form - [12, 13, 14, 15], # children - [16, 17, 18], # housing - [19, 20], # finance - [21, 22, 23], # social - [24, 25, 26], # health - ] - - # [Researcher] Split into training and test sets - # target model train / test split - these are strings - ( - X_train_orig, - X_test_orig, - y_train_orig, - y_test_orig, - ) = train_test_split( - X, - y, - test_size=0.5, - stratify=y, - shuffle=True, - ) - - # [Researcher] Preprocess dataset - # one-hot encoding of features and integer encoding of labels - label_enc = LabelEncoder() - feature_enc = OneHotEncoder() - X_train = feature_enc.fit_transform(X_train_orig).toarray() - y_train = label_enc.fit_transform(y_train_orig) - X_test = feature_enc.transform(X_test_orig).toarray() - y_test = label_enc.transform(y_test_orig) - - # [Researcher] Define the classifier - model = RandomForestClassifier(bootstrap=False) - - # [Researcher] Train the classifier - model.fit(X_train, y_train) - - # [TRE / Researcher] Wrap the model and data in a Target object - target = Target(model=model) - target.name = "nursery" - target.add_processed_data(X_train, y_train, X_test, y_test) - target.add_raw_data(X, y, X_train_orig, y_train_orig, X_test_orig, y_test_orig) - for i in range(n_features): - target.add_feature(nursery_data.feature_names[i], indices[i], "onehot") - - # [Researcher] Dump the target model and target data - target.save(path="target") - - # [TRE / Researcher] Wrap the model and data in a Target object - # Instantiating a ConfigFile instance to add configurations - # (i.e., configuration dictionaries or a configuration file) - # to a single configuration file and then running attacks - configfile_obj = ConfigFile( - filename="single_config.json", - ) - - # Adding three worst-case attack configuration dictionaries to the JSON file - config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "worst_case") - - config = { - "n_reps": 20, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "worst_case") - - config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "attack_metric_success_name": "P_HIGHER_AUC", - "attack_metric_success_thresh": 0.05, - "attack_metric_success_comp_type": "lte", - "attack_metric_success_count_thresh": 2, - "attack_fail_fast": True, - } - configfile_obj.add_config(config, "worst_case") - - # Adding two lira attack configuration dictionaries to the JSON file - config = { - "n_shadow_models": 100, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - configfile_obj.add_config(config, "lira") - - config = { - "n_shadow_models": 150, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "shadow_models_fail_fast": True, - "n_shadow_rows_confidences_min": 10, - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - configfile_obj.add_config(config, "lira") - - # Adding a lira JSON configuration file to a configuration file - # having multiple attack configurations - config = { - "n_shadow_models": 120, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "shadow_models_fail_fast": True, - "n_shadow_rows_confidences_min": 10, - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - with open("lira_config.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - configfile_obj.add_config("lira_config.json", "lira") - - # Adding an attribute inference attack configuration dictionary to the JSON file - config = { - "n_cpu": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "attribute") - - # Instantiating MultipleAttacks object specifying a single configuration file - # (with multiple attacks configurations) and a single JSON output file - attack_obj = MultipleAttacks(config_filename="single_config.json") - attack_obj.attack(target) - - # [TRE] Runs the attack. This would be done on the command line, here we do that with os.system - # [TRE] First they access the help to work out which parameters they need to set - os.system( - f"{sys.executable} -m aisdc.attacks.multiple_attacks run-attack-from-configfile --help" - ) - - # # [TRE] Then they run the attack - os.system( - f"{sys.executable} -m aisdc.attacks.multiple_attacks run-attack-from-configfile " - "--attack-config-json-file-name single_config.json " - "--attack-target-folder-path target " - ) diff --git a/examples/README.md b/examples/README.md index cf4acab4..58056f9c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,16 +4,57 @@ This folder contains examples of how to run the code contained in this repositor ## Scripts -* How to simulate attribute inference attacks: `attribute_inference_example.py`. -* How to simulate membership inference attacks: - - Worst case scenario attack: `worst_case_attack_example.py`. - - LIRA scenario attack: `lira_attack_example.py`. -* Integration of attacks into safemodel classes `safemodel_attack_integration_bothcalls.py`. +### Contents + +* Examples training a target model: + - `train_rf_breast_cancer.py` - Trains RF on breast cancer dataset. + - `train_rf_nursery.py` - Trains RF on nursery dataset with one-hot encoding. +* Examples programmatically running attacks: + - `attack_lira.py` - Simulated LiRA membership inference attack on breast cancer RF. + - `attack_worstcase.py` - Simulated worst case membership inference attack on breast cancer RF. + - `attack_attribute.py` - Simulated attribute inference attack on nursery RF. +* Examples of attack integration within safemodel classes: + - `safemodel.py` - Simulated attacks on a safe RF trained on the nursery dataset. + +### Programmatic Execution + +To run a programmatic example: +1. Run the relevant training script. +2. Run the desired attack script. + +For example: +``` +$ python -m examples.train_rf_breast_cancer +$ python -m examples.attack_lira +``` + +### Command Line Interface (CLI) Execution + +1. Run the relevant training script. +2. Generate an `attack.yaml` config. +3. Run the attack CLI tool. + +For example: +``` +$ python -m examples.train_rf_nursery +$ aisdc gen-attack +$ aisdc run target_rf_nursery attack.yaml +``` + +If you are unable to use the Python `Target` class to generate the `target_dir/` containing the `target.yaml` you can generate one using the CLI tool: + +``` +$ aisdc gen-target +``` + +## User Stories + +A collection of user guides can be found in the [`user_stories`](user_stories) folder of this repository. These guides include configurable examples from the perspective of both a researcher and a TRE, with separate scripts for each. Instructions on how to use each of these scripts and which scripts to use are included in the README located in the folder. ## Notebooks -The `notebooks` folder contains short tutorials on the basic concept of "safe_XX" versions of machine learning algorithms, and examples of some specific algorithms. +The `notebooks` folder contains short tutorials on the basic concept of "safe" versions of machine learning algorithms, and examples of some specific algorithms. ## Risk Examples -The `risk_examples` folder contains hypothetical examples of data leakage through machine learning models as described by [Jefferson et al. (2022)](https://doi.org/10.5281/zenodo.6896214). +The `risk_examples` contains hypothetical examples of data leakage through ML models as described by [Jefferson et al. (2022)](https://doi.org/10.5281/zenodo.6896214). diff --git a/examples/attack_attribute.py b/examples/attack_attribute.py new file mode 100644 index 00000000..b3d9bb39 --- /dev/null +++ b/examples/attack_attribute.py @@ -0,0 +1,35 @@ +"""Example running an attribute inference attack. + +The steps are as follows: + +1. Researcher trains their model, e.g., `train_rf_nursery.py` +2. Researcher and/or TRE runs the attacks + 1. The TRE calls the attack code. + 2. The TRE computes and inspects attack metrics. +""" + +import logging + +from aisdc.attacks import attribute_attack +from aisdc.attacks.target import Target + +output_dir = "outputs_aia" +target_dir = "target_rf_nursery" + +if __name__ == "__main__": + logging.info("Loading Target object from '%s'", target_dir) + target = Target() + target.load(target_dir) + + logging.info("Creating attribute inference attack") + attack_obj = attribute_attack.AttributeAttack(n_cpu=2, output_dir=output_dir) + + logging.info("Running attribute inference attack") + output = attack_obj.attack(target) + + logging.info("Accessing attack metrics and metadata") + output = output["attack_experiment_logger"]["attack_instance_logger"]["instance_0"] + logging.info(attribute_attack.report_categorical(output)) + logging.info(attribute_attack.report_quantitative(output)) + + logging.info("Report available in directory: '%s'", output_dir) diff --git a/examples/attack_lira.py b/examples/attack_lira.py new file mode 100644 index 00000000..06869902 --- /dev/null +++ b/examples/attack_lira.py @@ -0,0 +1,57 @@ +"""Example running a LiRA membership inference attack programmatically. + +This code simulates a MIA attack providing the attacker with as much +information as possible. That is, they have a subset of rows that they _know_ +were used for training. And a subset that they know were not. They also have +query access to the target model. + +The attack proceeds as described in this paper: +https://arxiv.org/pdf/2112.03570.pdf + +The steps are as follows: + +1. Researcher trains their model, e.g., `train_rf_breast_cancer.py` +2. Researcher and/or TRE runs the attacks + 1. The TRE calls the attack code. + 2. The TRE computes and inspects attack metrics. +""" + +import logging + +from aisdc.attacks.likelihood_attack import LIRAAttack +from aisdc.attacks.target import Target + +output_dir = "outputs_lira" +target_dir = "target_rf_breast_cancer" + +if __name__ == "__main__": + logging.info("Loading Target object from '%s'", target_dir) + target = Target() + target.load(target_dir) + + logging.info("Creating LiRA attack") + attack_obj = LIRAAttack(n_shadow_models=100, output_dir=output_dir) + + logging.info("Running LiRA attack") + output = attack_obj.attack(target) + + logging.info("Accessing attack metrics and metadata") + metrics = output["attack_experiment_logger"]["attack_instance_logger"]["instance_0"] + metadata = output["metadata"] + + logging.info("*******************") + logging.info("Attack metrics:") + logging.info("*******************") + for key, value in metrics.items(): + try: + logging.info("%s: %s", key, str(value)) + except TypeError: + logging.info("Cannot print %s", key) + + logging.info("*******************") + logging.info("Global metrics") + logging.info("*******************") + for key, value in metadata["global_metrics"].items(): + logging.info("%s: %s", key, str(value)) + + logging.info("Report available in directory: '%s'", output_dir) diff --git a/examples/attack_worstcase.py b/examples/attack_worstcase.py new file mode 100644 index 00000000..3afa5f6e --- /dev/null +++ b/examples/attack_worstcase.py @@ -0,0 +1,102 @@ +"""Example running a worst case membership inference attack programmatically. + +This code simulates a MIA attack providing the attacker with as much +information as possible. That is, they have a subset of rows that they _know_ +were used for training. And a subset that they know were not. They also have +query access to the target model. + +They pass the training and non-training rows through the target model to get +the predictive probabilities. These are then used to train an _attack model_. +And the attack model is evaluated to see how well it can predict whether or not +other examples were in the training set or not. + +To compare the results obtained with those expected by chance, the attack runs +some baseline experiments too. + +The steps are as follows: + +1. Researcher trains their model, e.g., `train_rf_breast_cancer.py` +2. Researcher and/or TRE runs the attacks + 1. The TRE calls the attack code. + 2. The TRE computes and inspects attack metrics. +""" + +import logging + +from aisdc.attacks import worst_case_attack +from aisdc.attacks.target import Target + +output_dir = "outputs_worstcase" +target_dir = "target_rf_breast_cancer" + +if __name__ == "__main__": + logging.info("Loading Target object from '%s'", target_dir) + target = Target() + target.load(target_dir) + + logging.info("Creating worst case attack") + attack_obj = worst_case_attack.WorstCaseAttack( + n_reps=10, + n_dummy_reps=1, + train_beta=5, + test_beta=2, + p_thresh=0.05, + test_prop=0.5, + output_dir=output_dir, + ) + + logging.info("Running worst case attack") + output = attack_obj.attack(target) + + logging.info("Accessing attack metrics and metadata") + metadata = output["metadata"] + + logging.info( + "Number of significant AUC values (raw): %d/%d", + metadata["global_metrics"]["n_sig_auc_p_vals"], + attack_obj.n_reps, + ) + + logging.info( + "Number of significant AUC values (FDR corrected): %d/%d", + metadata["global_metrics"]["n_sig_auc_p_vals_corrected"], + attack_obj.n_reps, + ) + + logging.info( + "Number of significant PDIF values (proportion of 0.1), raw: %d/%d", + metadata["global_metrics"]["n_sig_pdif_vals"], + attack_obj.n_reps, + ) + + logging.info( + "Number of significant PDIF values (proportion of 0.1), FDR corrected: %d/%d", + metadata["global_metrics"]["n_sig_pdif_vals_corrected"], + attack_obj.n_reps, + ) + + logging.info( + "(dummy) Number of significant AUC values (raw): %d/%d", + metadata["baseline_global_metrics"]["n_sig_auc_p_vals"], + attack_obj.n_reps, + ) + + logging.info( + "(dummy) Number of significant AUC values (FDR corrected): %d/%d", + metadata["baseline_global_metrics"]["n_sig_auc_p_vals_corrected"], + attack_obj.n_reps, + ) + + logging.info( + "(dummy) Number of significant PDIF values (proportion of 0.1), raw: %d/%d", + metadata["baseline_global_metrics"]["n_sig_pdif_vals"], + attack_obj.n_reps, + ) + + logging.info( + "(dummy) Number of significant PDIF values (proportion of 0.1) FDR corrected: %d/%d", + metadata["baseline_global_metrics"]["n_sig_pdif_vals_corrected"], + attack_obj.n_reps, + ) + + logging.info("Report available in directory: '%s'", output_dir) diff --git a/examples/attribute_inference_example.py b/examples/attribute_inference_example.py deleted file mode 100644 index 02e09b2e..00000000 --- a/examples/attribute_inference_example.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Example demonstrating the attribute inference attacks.""" - -import json -import os -import sys - -import numpy as np -from sklearn.datasets import fetch_openml -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import LabelEncoder, OneHotEncoder - -from aisdc.attacks import attribute_attack -from aisdc.attacks.target import Target - -if __name__ == "__main__": - # [Researcher] Access a dataset - nursery_data = fetch_openml(data_id=26, as_frame=True) - X = np.asarray(nursery_data.data, dtype=str) - y = np.asarray(nursery_data.target, dtype=str) - n_features = np.shape(X)[1] - indices: list[list[int]] = [ - [0, 1, 2], # parents - [3, 4, 5, 6, 7], # has_nurs - [8, 9, 10, 11], # form - [12, 13, 14, 15], # children - [16, 17, 18], # housing - [19, 20], # finance - [21, 22, 23], # social - [24, 25, 26], # health - ] - - # [Researcher] Split into training and test sets - # target model train / test split - these are strings - ( - X_train_orig, - X_test_orig, - y_train_orig, - y_test_orig, - ) = train_test_split( - X, - y, - test_size=0.5, - stratify=y, - shuffle=True, - ) - - # [Researcher] Preprocess dataset - # one-hot encoding of features and integer encoding of labels - label_enc = LabelEncoder() - feature_enc = OneHotEncoder() - X_train = feature_enc.fit_transform(X_train_orig).toarray() - y_train = label_enc.fit_transform(y_train_orig) - X_test = feature_enc.transform(X_test_orig).toarray() - y_test = label_enc.transform(y_test_orig) - - # [Researcher] Define the classifier - model = RandomForestClassifier(bootstrap=False) - - # [Researcher] Train the classifier - model.fit(X_train, y_train) - acc_train = model.score(X_train, y_train) - acc_test = model.score(X_test, y_test) - print(f"Base model train accuracy: {acc_train}") - print(f"Base model test accuracy: {acc_test}") - - # [TRE / Researcher] Wrap the model and data in a Target object - target = Target(model=model) - target.name = "nursery" - target.add_processed_data(X_train, y_train, X_test, y_test) - target.add_raw_data(X, y, X_train_orig, y_train_orig, X_test_orig, y_test_orig) - for i in range(n_features): - target.add_feature(nursery_data.feature_names[i], indices[i], "onehot") - - print(f"Dataset: {target.name}") - print(f"Features: {target.features}") - print(f"X_train shape = {np.shape(target.X_train)}") - print(f"y_train shape = {np.shape(target.y_train)}") - print(f"X_test shape = {np.shape(target.X_test)}") - print(f"y_test shape = {np.shape(target.y_test)}") - - # [TRE] Create the attack object with attack parameters - attack_obj = attribute_attack.AttributeAttack(n_cpu=2, output_dir="outputs_aia") - - # [TRE] Run the attack - attack_obj.attack(target) - - # [TRE] Grab the output - output = attack_obj.make_report() # also makes .pdf and .json files - output = output["attack_experiment_logger"]["attack_instance_logger"]["instance_0"] - - # [TRE] explore the metrics - print(attribute_attack.report_categorical(output)) - print(attribute_attack.report_quantitative(output)) - - print("Programmatic example finished") - print("****************************") - - print() - print() - print("Command line example starting") - print("*****************************") - - # [Researcher] Dump the training and test predictions to .csv files - target.save(path="aia_target") - - # [TRE] Runs the attack. This would be done on the command line, here we do that with os.system - # [TRE] First they access the help to work out which parameters they need to set - os.system( - f"{sys.executable} -m aisdc.attacks.attribute_attack run-attack-from-configfile --help" - ) - - # [TRE] Then they run the attack - - # Example 1 to demonstrate running attack from configuration and target files - config = { - "n_cpu": 2, - "output_dir": "outputs_aia", - } - - with open("config_aia_cmd.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - - os.system( - f"{sys.executable} -m aisdc.attacks.attribute_attack run-attack-from-configfile " - "--attack-config-json-file-name config_aia_cmd.json " - "--attack-target-folder-path aia_target " - ) diff --git a/examples/lira_attack_example.py b/examples/lira_attack_example.py deleted file mode 100644 index dfccb7e2..00000000 --- a/examples/lira_attack_example.py +++ /dev/null @@ -1,235 +0,0 @@ -"""Examples for using the likelihood ratio attack code. - -This code simulates a MIA attack providing the attacker with as much -information as possible. That is, they have a subset of rows that they _know_ -were used for training. And a subset that they know were not. They also have -query access to the target model. - -The attack proceeds as described in this paper: -https://arxiv.org/pdf/2112.03570.pdf - -The steps are as follows: - -1. The researcher partitions their data into training and testing subsets -2. The researcher trains their model -3. The TRE runs the attacks - *Programmatically* - 1. The TRE calls the attack code. - 2. The TRE computes and inspects attack metrics. - *Command line* - 3. The researcher writes out their training and testing data, as well as the predictions - that their target model makes on this data. - 4. The TRE create a config file for the attack, specifying the file names for the files created - in the previous two steps, as well as specifications for the shadow models. - 5. The attack is run with a command line command, creating a report. - -Below, [Researcher] and [TRE] are used to denote which task is performed by whom. -""" - -import json -import os -import sys - -import numpy as np -from sklearn.datasets import load_breast_cancer -from sklearn.ensemble import RandomForestClassifier -from sklearn.model_selection import train_test_split - -from aisdc.attacks.likelihood_attack import LIRAAttack -from aisdc.attacks.target import Target - -# [Researcher] Access a dataset -X, y = load_breast_cancer(return_X_y=True, as_frame=False) - -# [Researcher] Split into training and test sets -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) - -# [Researcher] Define the classifier -target_model = RandomForestClassifier(min_samples_split=2, min_samples_leaf=1) -# [Researcher] Train the classifier -target_model.fit(X_train, y_train) - -# [Researcher] Provide the model and the train and test data to the TRE -target = Target(model=target_model) -target.add_processed_data(X_train, y_train, X_test, y_test) - -# [TRE] Creates a config file for the likelihood attack -config = { - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, -} - -with open("lira_config.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -# [TRE] Example 1: sets up the attack -attack_obj = LIRAAttack( - n_shadow_models=100, - output_dir="outputs_lira", - attack_config_json_file_name="lira_config.json", -) - -# [TRE] runs the attack -attack_obj.attack(target) - -# [TRE] Get the output -output = attack_obj.make_report() # also makes .pdf and .json files - -# [TRE] Accesses attack metrics and metadata -attack_metrics = output["attack_experiment_logger"]["attack_instance_logger"][ - "instance_0" -] -metadata = output["metadata"] - -# [TRE] Looks at the metric values -print("Attack metrics:") -for key, value in attack_metrics.items(): - try: - print(key, f"{value:.3f}") - except TypeError: - print(f"Cannot print {key}") - -print("Global metrics") -for key, value in metadata["global_metrics"].items(): - print(key, value) - - -print("Programmatic example finished") -print("****************************") - -# [TRE] Example 2: sets up the attack with fail-fast option -attack_obj = LIRAAttack( - n_shadow_models=100, - output_dir="outputs_lira", - attack_config_json_file_name="lira_config.json", - shadow_models_fail_fast=True, - n_shadow_rows_confidences_min=10, -) - -# [TRE] runs the attack -attack_obj.attack(target) - -# [TRE] Get the output -output = attack_obj.make_report() # also makes .pdf and .json files - -# [TRE] Accesses attack metrics and metadata -attack_metrics = output["attack_experiment_logger"]["attack_instance_logger"][ - "instance_0" -] -metadata = output["metadata"] - -# [TRE] Looks at the metric values -print("Attack metrics:") -for key, value in attack_metrics.items(): - try: - print(key, f"{value:.3f}") - except TypeError: - print(f"Cannot print {key}") - -print("Global metrics") -for key, value in metadata["global_metrics"].items(): - print(key, value) - - -print("Programmatic example with fail-fast option finished") -print("****************************") - -print() -print() -print("Command line example starting") -print("*****************************") -# Command line version. The same functionality as above, but the attack is run from -# the command line rather than programmatically - -# [Researcher] Dump the training and test predictions to .csv files -np.savetxt("train_preds.csv", target_model.predict_proba(X_train), delimiter=",") -np.savetxt("test_preds.csv", target_model.predict_proba(X_test), delimiter=",") - -# [Researcher] Dump the training and test data to a .csv file -np.savetxt("train_data.csv", np.hstack((X_train, y_train[:, None])), delimiter=",") -np.savetxt("test_data.csv", np.hstack((X_test, y_test[:, None])), delimiter=",") - -# [Researcher] Dump the target model and target data -target.save(path="target_model_for_lira") - -# [TRE] Runs the attack. This would be done on the command line, here we do that with os.system -# [TRE] First they access the help to work out which parameters they need to set -os.system(f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack --help") - -# [TRE] Then they run the attack -# Example 1 to demonstrate all given shadow models trained -os.system( - f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack " - "--attack-config-json-file-name lira_config.json " - "--output-dir outputs_lira " - # "report-name report_lira " - "--n-shadow-models 100 " -) - -# Example 2 to demonstrate fail fast of shadow models trained -os.system( - f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack " - "--attack-config-json-file-name lira_config.json " - "--output-dir outputs_lira " - # "--report-name report_lira " - "--n-shadow-models 100 " - "--shadow-models-fail-fast " - "--n-shadow-rows-confidences-min 10 " -) - -# [TRE] Runs the attack. This would be done on the command line, here we do that with os.system -# [TRE] First they access the help to work out which parameters they need to set -os.system( - f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack-from-configfile --help" -) - -# Example 3 to demonstrate running attack from configuration file only -config = { - "n_shadow_models": 150, - "output_dir": "outputs_lira", - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, -} - -with open("config_lira_cmd1.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -os.system( - f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack-from-configfile " - "--attack-config-json-file-name config_lira_cmd1.json " - "--attack-target-folder-path target_model_for_lira " -) - -# Example 4 to demonstrate running attack from configuration file only with fail fail fast option -config = { - "n_shadow_models": 150, - "output_dir": "outputs_lira", - "shadow_models_fail_fast": True, - "n_shadow_rows_confidences_min": 10, - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, -} - -with open("config_lira_cmd2.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -os.system( - f"{sys.executable} -m aisdc.attacks.likelihood_attack run-attack-from-configfile " - "--attack-config-json-file-name config_lira_cmd2.json " - "--attack-target-folder-path target_model_for_lira " -) - - -# [TRE] The code produces a .pdf report (example_lira_report.pdf) diff --git a/examples/safemodel.py b/examples/safemodel.py new file mode 100644 index 00000000..784b92c6 --- /dev/null +++ b/examples/safemodel.py @@ -0,0 +1,77 @@ +"""Example showing how to integrate attacks into safemodel classes.""" + +import logging + +import numpy as np +from sklearn.datasets import fetch_openml +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import LabelEncoder, OneHotEncoder + +from aisdc.attacks.target import Target +from aisdc.safemodel.classifiers import SafeDecisionTreeClassifier + +output_dir = "outputs_safemodel" + +if __name__ == "__main__": + logging.info("Loading dataset") + nursery_data = fetch_openml(data_id=26, as_frame=True) + X = np.asarray(nursery_data.data, dtype=str) + y = np.asarray(nursery_data.target, dtype=str) + + n_features = np.shape(X)[1] + indices = [ + [0, 1, 2], # parents + [3, 4, 5, 6, 7], # has_nurs + [8, 9, 10, 11], # form + [12, 13, 14, 15], # children + [16, 17, 18], # housing + [19, 20], # finance + [21, 22, 23], # social + [24, 25, 26], # health + ] + + logging.info("Splitting data into training and test sets") + X_train_orig, X_test_orig, y_train_orig, y_test_orig = train_test_split( + X, y, test_size=0.5, stratify=y, shuffle=True + ) + + logging.info("Preprocessing dataset") + label_enc = LabelEncoder() + feature_enc = OneHotEncoder() + X_train = feature_enc.fit_transform(X_train_orig).toarray() + y_train = label_enc.fit_transform(y_train_orig) + X_test = feature_enc.transform(X_test_orig).toarray() + y_test = label_enc.transform(y_test_orig) + + logging.info("Defining the (safe) model") + model = SafeDecisionTreeClassifier(random_state=1) + + logging.info("Training the model") + model.fit(X_train, y_train) + acc_train = model.score(X_train, y_train) + acc_test = model.score(X_test, y_test) + logging.info("Base model train accuracy: %.4f", acc_train) + logging.info("Base model test accuracy: %.4f", acc_test) + + logging.info("Performing a preliminary check") + msg, disclosive = model.preliminary_check() + + logging.info("Wrapping the model and data in a Target object") + target = Target(model=model) + target.dataset_name = "nursery" + target.add_processed_data(X_train, y_train, X_test, y_test) + target.add_raw_data(X, y, X_train_orig, y_train_orig, X_test_orig, y_test_orig) + for i in range(n_features): + target.add_feature(nursery_data.feature_names[i], indices[i], "onehot") + + logging.info("Dataset: %s", target.dataset_name) + logging.info("Features: %s", target.features) + logging.info("X_train shape: %s", str(target.X_train.shape)) + logging.info("y_train shape: %s", str(target.y_train.shape)) + logging.info("X_test shape: %s", str(target.X_test.shape)) + logging.info("y_test shape: %s", str(target.y_test.shape)) + + logging.info("Performing disclosure checks") + model.request_release(path=output_dir, ext="pkl", target=target) + + logging.info("Please see the files generated in: %s", output_dir) diff --git a/examples/safemodel_attack_integration_bothcalls.py b/examples/safemodel_attack_integration_bothcalls.py deleted file mode 100644 index 64f86bfa..00000000 --- a/examples/safemodel_attack_integration_bothcalls.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Example showing how to integrate attacks into safemodel classes.""" - -import logging - -import numpy as np -from sklearn.datasets import fetch_openml -from sklearn.model_selection import train_test_split -from sklearn.preprocessing import LabelEncoder, OneHotEncoder - -from aisdc.attacks.target import Target -from aisdc.safemodel.classifiers import SafeDecisionTreeClassifier - -if __name__ == "__main__": - # [Researcher] Access a dataset - nursery_data = fetch_openml(data_id=26, as_frame=True) - X = np.asarray(nursery_data.data, dtype=str) - y = np.asarray(nursery_data.target, dtype=str) - - n_features = np.shape(X)[1] - indices: list[list[int]] = [ - [0, 1, 2], # parents - [3, 4, 5, 6, 7], # has_nurs - [8, 9, 10, 11], # form - [12, 13, 14, 15], # children - [16, 17, 18], # housing - [19, 20], # finance - [21, 22, 23], # social - [24, 25, 26], # health - ] - - # [Researcher] Split into training and test sets - # target model train / test split - these are strings - ( - X_train_orig, - X_test_orig, - y_train_orig, - y_test_orig, - ) = train_test_split( - X, - y, - test_size=0.5, - stratify=y, - shuffle=True, - ) - - # [Researcher] Preprocess dataset - # one-hot encoding of features and integer encoding of labels - label_enc = LabelEncoder() - feature_enc = OneHotEncoder() - X_train = feature_enc.fit_transform(X_train_orig).toarray() - y_train = label_enc.fit_transform(y_train_orig) - X_test = feature_enc.transform(X_test_orig).toarray() - y_test = label_enc.transform(y_test_orig) - - # [Researcher] Build a model - model = SafeDecisionTreeClassifier(random_state=1) - model.fit(X_train, y_train) - msg, disclosive = model.preliminary_check() - - # [TRE / Researcher] Wrap the model and data in a Target object - target = Target(model=model) - target.name = "nursery" - target.add_processed_data(X_train, y_train, X_test, y_test) - target.add_raw_data(X, y, X_train_orig, y_train_orig, X_test_orig, y_test_orig) - for i in range(n_features): - target.add_feature(nursery_data.feature_names[i], indices[i], "onehot") - - logging.info("Dataset: %s", target.name) - logging.info("Features: %s", target.features) - logging.info("X_train shape = %s", np.shape(target.X_train)) - logging.info("y_train shape = %s", np.shape(target.y_train)) - logging.info("X_test shape = %s", np.shape(target.X_test)) - logging.info("y_test shape = %s", np.shape(target.y_test)) - - # [TRE / Researcher] Perform disclosure checks - SAVE_DIR = "mytest" - SAVE_FILENAME = "direct_results" - - # check direct method - print("==========> first running attacks explicitly via run_attack()") - for attack_name in ["worst_case", "attribute", "lira"]: - print(f"===> running {attack_name} attack directly") - metadata = model.run_attack(target, attack_name, SAVE_DIR, SAVE_FILENAME) - logging.info("metadata is:") - for key, val in metadata.items(): - if isinstance(val, dict): - logging.info(" %s ", key) - for key1, val2 in val.items(): - logging.info(" %s : %s", key1, val2) - else: - logging.info(" %s : %s", key, val) - - # now via request_release() - print("===> now running attacks implicitly via request_release()") - model.request_release(path=SAVE_DIR, ext="pkl", target=target) - - print(f"Please see the files generated in: {SAVE_DIR}") diff --git a/examples/train_rf_breast_cancer.py b/examples/train_rf_breast_cancer.py new file mode 100644 index 00000000..f5afad5d --- /dev/null +++ b/examples/train_rf_breast_cancer.py @@ -0,0 +1,32 @@ +"""Train a Random Forest classifier on the breast cancer data.""" + +import logging + +from sklearn.datasets import load_breast_cancer +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split + +from aisdc.attacks.target import Target + +output_dir = "target_rf_breast_cancer" + +if __name__ == "__main__": + logging.info("Loading dataset") + X, y = load_breast_cancer(return_X_y=True, as_frame=False) + + logging.info("Splitting data into training and test sets") + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) + + logging.info("Defining the model") + model = RandomForestClassifier(min_samples_split=2, min_samples_leaf=1) + + logging.info("Training the model") + model.fit(X_train, y_train) + + logging.info("Wrapping the model and data in a Target object") + target = Target(model=model) + target.dataset_name = "breast cancer" + target.add_processed_data(X_train, y_train, X_test, y_test) + + logging.info("Writing Target object to directory: '%s'", output_dir) + target.save(output_dir) diff --git a/examples/train_rf_nursery.py b/examples/train_rf_nursery.py new file mode 100644 index 00000000..980ea876 --- /dev/null +++ b/examples/train_rf_nursery.py @@ -0,0 +1,65 @@ +"""Train a Random Forest classifier on the nursery dataset.""" + +import logging + +import numpy as np +from sklearn.datasets import fetch_openml +from sklearn.ensemble import RandomForestClassifier +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import LabelEncoder, OneHotEncoder + +from aisdc.attacks.target import Target + +output_dir = "target_rf_nursery" + +if __name__ == "__main__": + logging.info("Loading dataset") + nursery_data = fetch_openml(data_id=26, as_frame=True) + X = np.asarray(nursery_data.data, dtype=str) + y = np.asarray(nursery_data.target, dtype=str) + + logging.info("Splitting data into training and test sets") + X_train_orig, X_test_orig, y_train_orig, y_test_orig = train_test_split( + X, y, test_size=0.5, stratify=y, shuffle=True + ) + + logging.info("Preprocessing dataset") + label_enc = LabelEncoder() + feature_enc = OneHotEncoder() + X_train = feature_enc.fit_transform(X_train_orig).toarray() + y_train = label_enc.fit_transform(y_train_orig) + X_test = feature_enc.transform(X_test_orig).toarray() + y_test = label_enc.transform(y_test_orig) + + logging.info("Defining the model") + model = RandomForestClassifier(bootstrap=False) + + logging.info("Training the model") + model.fit(X_train, y_train) + acc_train = model.score(X_train, y_train) + acc_test = model.score(X_test, y_test) + logging.info("Base model train accuracy: %.4f", acc_train) + logging.info("Base model test accuracy: %.4f", acc_test) + + logging.info("Wrapping the model and data in a Target object") + target = Target(model=model) + target.dataset_name = "nursery" + target.add_processed_data(X_train, y_train, X_test, y_test) + target.add_raw_data(X, y, X_train_orig, y_train_orig, X_test_orig, y_test_orig) + + logging.info("Wrapping feature details and encoding for attribute inference") + feature_indices = [ + [0, 1, 2], # parents + [3, 4, 5, 6, 7], # has_nurs + [8, 9, 10, 11], # form + [12, 13, 14, 15], # children + [16, 17, 18], # housing + [19, 20], # finance + [21, 22, 23], # social + [24, 25, 26], # health + ] + for i, index in enumerate(feature_indices): + target.add_feature(nursery_data.feature_names[i], index, "onehot") + + logging.info("Writing Target object to directory: '%s'", output_dir) + target.save(output_dir) diff --git a/user_stories/README.md b/examples/user_stories/README.md similarity index 98% rename from user_stories/README.md rename to examples/user_stories/README.md index 9a8f840e..bea0f780 100644 --- a/user_stories/README.md +++ b/examples/user_stories/README.md @@ -1,11 +1,22 @@ # User Stories + In this section there are code examples of how the AI-SDC tools can be used by both a researchers in Trusted Research Environment (TRE) and a TRE output checkers. Each project is unique and therefore how AI-SDC tools are applied may vary from case to case. The user guides have been split into 8 'user stories', each designed to fit a different use-case. The following diagram is intended to identify the closest use-case match to projects: ![User Stories](user_stories_flow_chart.drawio.png) +## Note + +These code examples were designed for an older version of `aisdc`. + +This can still be installed: +``` +$ pip install aisdc==1.1.3.post1 +``` + ## General description + The user stories are coding examples intended to maximise the chances of successfully and smoothly egressing a Machine Learning (ML) model from the TRE. These guides are useful to create appropriate ML models and the metadata files necessary for output checking of the ML model prior to the egress. Saving time and effort, and ultimately optimising costs. Each user story consists of at least 2 files: diff --git a/user_stories/default_config.yaml b/examples/user_stories/default_config.yaml similarity index 100% rename from user_stories/default_config.yaml rename to examples/user_stories/default_config.yaml diff --git a/user_stories/generate_disclosure_risk_report.py b/examples/user_stories/generate_disclosure_risk_report.py similarity index 100% rename from user_stories/generate_disclosure_risk_report.py rename to examples/user_stories/generate_disclosure_risk_report.py diff --git a/user_stories/user_stories_example_output/summary_example_output.txt b/examples/user_stories/user_stories_example_output/summary_example_output.txt similarity index 100% rename from user_stories/user_stories_example_output/summary_example_output.txt rename to examples/user_stories/user_stories_example_output/summary_example_output.txt diff --git a/user_stories/user_stories_example_output/summary_example_output_description.png b/examples/user_stories/user_stories_example_output/summary_example_output_description.png similarity index 100% rename from user_stories/user_stories_example_output/summary_example_output_description.png rename to examples/user_stories/user_stories_example_output/summary_example_output_description.png diff --git a/user_stories/user_stories_flow_chart.drawio.png b/examples/user_stories/user_stories_flow_chart.drawio.png similarity index 100% rename from user_stories/user_stories_flow_chart.drawio.png rename to examples/user_stories/user_stories_flow_chart.drawio.png diff --git a/user_stories/user_story_1/README.MD b/examples/user_stories/user_story_1/README.MD similarity index 100% rename from user_stories/user_story_1/README.MD rename to examples/user_stories/user_story_1/README.MD diff --git a/user_stories/user_story_1/user_story_1_researcher_template.py b/examples/user_stories/user_story_1/user_story_1_researcher_template.py similarity index 100% rename from user_stories/user_story_1/user_story_1_researcher_template.py rename to examples/user_stories/user_story_1/user_story_1_researcher_template.py diff --git a/user_stories/user_story_1/user_story_1_tre.py b/examples/user_stories/user_story_1/user_story_1_tre.py similarity index 100% rename from user_stories/user_story_1/user_story_1_tre.py rename to examples/user_stories/user_story_1/user_story_1_tre.py diff --git a/user_stories/user_story_2/README.MD b/examples/user_stories/user_story_2/README.MD similarity index 100% rename from user_stories/user_story_2/README.MD rename to examples/user_stories/user_story_2/README.MD diff --git a/user_stories/user_story_2/data_processing_researcher.py b/examples/user_stories/user_story_2/data_processing_researcher.py similarity index 100% rename from user_stories/user_story_2/data_processing_researcher.py rename to examples/user_stories/user_story_2/data_processing_researcher.py diff --git a/user_stories/user_story_2/user_story_2_researcher_template.py b/examples/user_stories/user_story_2/user_story_2_researcher_template.py similarity index 100% rename from user_stories/user_story_2/user_story_2_researcher_template.py rename to examples/user_stories/user_story_2/user_story_2_researcher_template.py diff --git a/user_stories/user_story_2/user_story_2_tre.py b/examples/user_stories/user_story_2/user_story_2_tre.py similarity index 100% rename from user_stories/user_story_2/user_story_2_tre.py rename to examples/user_stories/user_story_2/user_story_2_tre.py diff --git a/user_stories/user_story_3/README.MD b/examples/user_stories/user_story_3/README.MD similarity index 100% rename from user_stories/user_story_3/README.MD rename to examples/user_stories/user_story_3/README.MD diff --git a/user_stories/user_story_3/user_story_3_researcher_template.py b/examples/user_stories/user_story_3/user_story_3_researcher_template.py similarity index 100% rename from user_stories/user_story_3/user_story_3_researcher_template.py rename to examples/user_stories/user_story_3/user_story_3_researcher_template.py diff --git a/user_stories/user_story_3/user_story_3_tre.py b/examples/user_stories/user_story_3/user_story_3_tre.py similarity index 100% rename from user_stories/user_story_3/user_story_3_tre.py rename to examples/user_stories/user_story_3/user_story_3_tre.py diff --git a/user_stories/user_story_4/README.MD b/examples/user_stories/user_story_4/README.MD similarity index 100% rename from user_stories/user_story_4/README.MD rename to examples/user_stories/user_story_4/README.MD diff --git a/user_stories/user_story_4/user_story_4_researcher_template.R b/examples/user_stories/user_story_4/user_story_4_researcher_template.R similarity index 100% rename from user_stories/user_story_4/user_story_4_researcher_template.R rename to examples/user_stories/user_story_4/user_story_4_researcher_template.R diff --git a/user_stories/user_story_4/user_story_4_tre.py b/examples/user_stories/user_story_4/user_story_4_tre.py similarity index 100% rename from user_stories/user_story_4/user_story_4_tre.py rename to examples/user_stories/user_story_4/user_story_4_tre.py diff --git a/user_stories/user_story_7/REAME.MD b/examples/user_stories/user_story_7/REAME.MD similarity index 100% rename from user_stories/user_story_7/REAME.MD rename to examples/user_stories/user_story_7/REAME.MD diff --git a/user_stories/user_story_7/user_story_7_researcher_template.py b/examples/user_stories/user_story_7/user_story_7_researcher_template.py similarity index 100% rename from user_stories/user_story_7/user_story_7_researcher_template.py rename to examples/user_stories/user_story_7/user_story_7_researcher_template.py diff --git a/user_stories/user_story_7/user_story_7_tre.py b/examples/user_stories/user_story_7/user_story_7_tre.py similarity index 100% rename from user_stories/user_story_7/user_story_7_tre.py rename to examples/user_stories/user_story_7/user_story_7_tre.py diff --git a/user_stories/user_story_8/README.MD b/examples/user_stories/user_story_8/README.MD similarity index 100% rename from user_stories/user_story_8/README.MD rename to examples/user_stories/user_story_8/README.MD diff --git a/user_stories/user_story_8/data_processing_researcher.py b/examples/user_stories/user_story_8/data_processing_researcher.py similarity index 100% rename from user_stories/user_story_8/data_processing_researcher.py rename to examples/user_stories/user_story_8/data_processing_researcher.py diff --git a/user_stories/user_story_8/user_story_8_researcher_template.py b/examples/user_stories/user_story_8/user_story_8_researcher_template.py similarity index 100% rename from user_stories/user_story_8/user_story_8_researcher_template.py rename to examples/user_stories/user_story_8/user_story_8_researcher_template.py diff --git a/user_stories/user_story_8/user_story_8_tre.py b/examples/user_stories/user_story_8/user_story_8_tre.py similarity index 100% rename from user_stories/user_story_8/user_story_8_tre.py rename to examples/user_stories/user_story_8/user_story_8_tre.py diff --git a/examples/worst_case_attack_example.py b/examples/worst_case_attack_example.py deleted file mode 100644 index e23e4574..00000000 --- a/examples/worst_case_attack_example.py +++ /dev/null @@ -1,329 +0,0 @@ -"""Examples for using the 'worst case' attack code. - -This code simulates a MIA attack providing the attacker with as much -information as possible. That is, they have a subset of rows that they _know_ -were used for training. And a subset that they know were not. They also have -query access to the target model. - -They pass the training and non-training rows through the target model to get -the predictive probabilities. These are then used to train an _attack model_. -And the attack model is evaluated to see how well it can predict whether or not -other examples were in the training set or not. - -The code can be called from the command line, or accessed programmatically. -Examples of both are shown below. - -Below, [Researcher] and [TRE] are used to denote which task is performed by whom. -""" - -import json -import os -import sys - -import numpy as np -from sklearn.datasets import load_breast_cancer -from sklearn.model_selection import train_test_split -from sklearn.svm import SVC - -from aisdc.attacks import worst_case_attack -from aisdc.attacks.target import Target - -sys.path.append(os.path.dirname(os.path.dirname(__file__))) - -# [Researcher] Access a dataset -X, y = load_breast_cancer(return_X_y=True, as_frame=False) - -# [Researcher] Split into training and test sets -X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3) - -# [Researcher] Define the classifier -target_model = SVC(gamma=0.1, probability=True) - -# [Researcher] Train the classifier -target_model.fit(X_train, y_train) - -# [Researcher] Provide the model and the train and test data to the TRE - -# [TRE] Compute the predictions on the training and test sets -train_preds = target_model.predict_proba(X_train) -test_preds = target_model.predict_proba(X_test) - -# [TRE / Researcher] Wrap the model and data in a Target object -target = Target(model=target_model) -target.add_processed_data(X_train, y_train, X_test, y_test) - -# [TRE] Create the attack object -attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data - n_reps=10, - # number of baseline (dummy) experiments to do - n_dummy_reps=1, - # value of b for beta distribution used to sample the in-sample probabilities - train_beta=5, - # value of b for beta distribution used to sample the out-of-sample probabilities - test_beta=2, - # Threshold to determine significance of things - p_thresh=0.05, - # Filename arguments needed by the code, meaningless if run programmatically - training_preds_filename=None, - test_preds_filename=None, - # Proportion of data to use as a test set for the attack model; - test_prop=0.5, - # name of the output directory - output_dir="outputs_worstcase", - # # If report_name is given, it creates pdf and json files with the specified name; - # # otherwise it create output files with default name 'report_worstcase' - # e.g., report_name="programmatically_worstcase_example1_report", - attack_metric_success_name="P_HIGHER_AUC", - # threshold for a given metric for failure/success counters - attack_metric_success_thresh=0.05, - # threshold comparison operator (i.e., gte: greater than or equal to, gt: greater than, lte: - # less than or equal to, lt: less than, eq: equal to and not_eq: not equal to) - attack_metric_success_comp_type="lte", - # fail fast counter to stop further repetitions of the test - attack_metric_success_count_thresh=2, - # If true it stop repetitions earlier based on the given attack metric - # (i.e., attack_metric_success_name) considering the comparison type - # (attack_metric_success_comp_type) satisfying a threshold (i.e., attack_metric_success_thresh) - # for n (attack_metric_success_count_thresh) number of times - attack_fail_fast=True, -) - -# [TRE] Run the attack -attack_obj.attack(target) - -# [TRE] Grab the output -output = attack_obj.make_report() -metadata = output["metadata"] -# [TRE] explore the metrics -# For how many of the reps is the AUC p-value significant, with and without FDR correction. A -# significant P-value means that the attack was statistically successful at predicting rows at -# belonging in the training set - -print( - "Number of significant AUC values (raw):", - f"{metadata['global_metrics']['n_sig_auc_p_vals']}/{attack_obj.n_reps}", -) - -print( - "Number of significant AUC values (FDR corrected):", - f"{metadata['global_metrics']['n_sig_auc_p_vals_corrected']}/{attack_obj.n_reps}", -) - -# Or the number of repetitions in which the PDIF (0.1) was significant -print( - "Number of significant PDIF values (proportion of 0.1), raw:", - f"{metadata['global_metrics']['n_sig_pdif_vals']}/{attack_obj.n_reps}", -) - -print( - "Number of significant PDIF values (proportion of 0.1), FDR corrected:", - f"{metadata['global_metrics']['n_sig_pdif_vals_corrected']}/{attack_obj.n_reps}", -) - -# [TRE] to compare the results obtained with those expected by chance, the attack runs some -# Baseline experiments too - -# [TRE] looks at the metric values to compare with those for the model -print( - "(dummy) Number of significant AUC values (raw):", - f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals']}/{attack_obj.n_reps}", -) - -print( - "(dummy) Number of significant AUC values (FDR corrected):", - f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals_corrected']}/{attack_obj.n_reps}", -) - -# Or the number of repetitions in which the PDIF (0.1) was significant -print( - "(dummy) Number of significant PDIF values (proportion of 0.1), raw:", - f"{metadata['baseline_global_metrics']['n_sig_pdif_vals']}/{attack_obj.n_reps}", -) - -print( - "(dummy) Number of significant PDIF values (proportion of 0.1), FDR corrected:", - f"{metadata['baseline_global_metrics']['n_sig_pdif_vals_corrected']}/{attack_obj.n_reps}", -) - -print("Programmatic example1 finished") -print("****************************") - -# Example 2: Use of configuration file name to pass through and load parameters -# and running attack programmatically -config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_worstcase", - # "report_name": "report_worstcase" -} - -with open("config_worstcase.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -# [TRE] Create the attack object -attack_obj = worst_case_attack.WorstCaseAttack( - # name of the configuration file in JSON format to load parameters - attack_config_json_file_name="config_worstcase.json", -) - -# [TRE] Run the attack -attack_obj.attack(target) - -# [TRE] Grab the output -output = attack_obj.make_report() -metadata = output["metadata"] -# [TRE] explore the metrics -# For how many of the reps is the AUC p-value significant, with and without FDR correction. A -# significant P-value means that the attack was statistically successful at predicting rows at -# belonging in the training set - -print( - "Number of significant AUC values (raw):", - f"{metadata['global_metrics']['n_sig_auc_p_vals']}/{attack_obj.n_reps}", -) - -print( - "Number of significant AUC values (FDR corrected):", - f"{metadata['global_metrics']['n_sig_auc_p_vals_corrected']}/{attack_obj.n_reps}", -) - -# Or the number of repetitions in which the PDIF (0.1) was significant -print( - "Number of significant PDIF values (proportion of 0.1), raw:", - f"{metadata['global_metrics']['n_sig_pdif_vals']}/{attack_obj.n_reps}", -) - -print( - "Number of significant PDIF values (proportion of 0.1), FDR corrected:", - f"{metadata['global_metrics']['n_sig_pdif_vals_corrected']}/{attack_obj.n_reps}", -) - -# [TRE] to compare the results obtained with those expected by chance, the attack runs some -# Baseline experiments too - -# [TRE] looks at the metric values to compare with those for the model -print( - "(dummy) Number of significant AUC values (raw):", - f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals']}/{attack_obj.n_reps}", -) - -print( - "(dummy) Number of significant AUC values (FDR corrected):", - f"{metadata['baseline_global_metrics']['n_sig_auc_p_vals_corrected']}/{attack_obj.n_reps}", -) - -# Or the number of repetitions in which the PDIF (0.1) was significant -print( - "(dummy) Number of significant PDIF values (proportion of 0.1), raw:", - f"{metadata['baseline_global_metrics']['n_sig_pdif_vals']}/{attack_obj.n_reps}", -) - -print( - "(dummy) Number of significant PDIF values (proportion of 0.1), FDR corrected:", - f"{metadata['baseline_global_metrics']['n_sig_pdif_vals_corrected']}/{attack_obj.n_reps}", -) - -print("Programmatic example2 finished") -print("****************************") - -print() -print() -print("Command line example starting") -print("*****************************") -# Command line version. The same functionality as above, but the attack is run from -# the command line rather than programmatically - -# [Researcher] Dump the training and test predictions to .csv files -np.savetxt("train_preds.csv", train_preds, delimiter=",") -np.savetxt("test_preds.csv", test_preds, delimiter=",") - -# [Researcher] Dump the target model and target data -target.save(path="target_model_worstcase") - -# [TRE] Runs the attack. This would be done on the command line, here we do that with os.system -# [TRE] First they access the help to work out which parameters they need to set -os.system(f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack --help") - -# [TRE] Then they run the attack -# Example 1: Worstcase attack through commandline by passing parameters -os.system( - f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack " - "--training-preds-filename train_preds.csv " - "--test-preds-filename test_preds.csv " - "--n-reps 10 " - "--output-dir outputs_worstcase " - # "--report-name commandline_report_worstcase " - "--n-dummy-reps 1 " - "--test-prop 0.1 " - "--train-beta 5 " - "--test-beta 2 " - "--attack-metric-success-name P_HIGHER_AUC " - "--attack-metric-success-thresh 0.05 " - "--attack-metric-success-comp-type lte " - "--attack-metric-success-count-thresh 2 " - "--attack-fail-fast " -) - -# [TRE] Runs the attack. This would be done on the command line, here we do that with os.system -# [TRE] First they access the help to work out which parameters they need to set -os.system( - f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack-from-configfile --help" -) - -# Example 2: Worstcase attack by passing a configuratation file name for loading parameters -config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_worstcase", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "attack_metric_success_name": "P_HIGHER_AUC", - "attack_metric_success_thresh": 0.05, - "attack_metric_success_comp_type": "lte", - "attack_metric_success_count_thresh": 2, - "attack_fail_fast": True, -} - -with open("config_worstcase_cmd.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -os.system( - f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack-from-configfile " - "--attack-config-json-file-name config_worstcase_cmd.json " - "--attack-target-folder-path target_model_worstcase " -) - -# Example 3: Worstcase attack by passing a configuratation file name for loading parameters -config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_worstcase", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", -} - -with open("config_worstcase_cmd.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - -os.system( - f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack-from-configfile " - "--attack-config-json-file-name config_worstcase_cmd.json " - "--attack-target-folder-path target_model_worstcase " -) - -# [TRE] The code produces a .pdf report (example_report.pdf) and a .json file (example_report.json) -# that can be injesetd by the shiny app diff --git a/setup.cfg b/setup.cfg index c2c5a4f2..c16d12af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = aisdc -version = 1.1.3 +version = 1.2.0 description = Tools for the statistical disclosure control of machine learning models long_description = file: README.md long_description_content_type = text/markdown @@ -49,6 +49,10 @@ install_requires = scikit-learn xgboost +[options.entry_points] +console_scripts = + aisdc = aisdc.main:main + [options.package_data] aisdc.safemodel = rules.json diff --git a/tests/attacks/lrconfig.json b/tests/attacks/lrconfig.json deleted file mode 100644 index eb68bc82..00000000 --- a/tests/attacks/lrconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": [ - "sklearn.ensemble", - "RandomForestClassifier" - ], - "target_model_hyp": { - "min_samples_split": 2, - "min_samples_leaf": 1 - } -} diff --git a/tests/attacks/lrconfig_cmd.json b/tests/attacks/lrconfig_cmd.json deleted file mode 100644 index 672b7d9f..00000000 --- a/tests/attacks/lrconfig_cmd.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "n_shadow_models": 150, - "output_dir": "test_output_lira", - "pdf_report_name": "commandline_lira_example3_report", - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": [ - "sklearn.ensemble", - "RandomForestClassifier" - ], - "target_model_hyp": { - "min_samples_split": 2, - "min_samples_leaf": 1 - } -} diff --git a/tests/attacks/test_attacks_target.py b/tests/attacks/test_attacks_target.py index fa021aae..9d044c6c 100644 --- a/tests/attacks/test_attacks_target.py +++ b/tests/attacks/test_attacks_target.py @@ -13,18 +13,19 @@ @pytest.mark.parametrize("get_target", [RandomForestClassifier()], indirect=True) def test_target(get_target): - """Test a randomly sampled 10% of the nursery dataset as a Target object.""" + """Test Target object creation, saving, and loading.""" + # create target target = get_target - # [Researcher] Saves the target model and data + # save target target.save(RES_DIR) - # [TRE] Loads the target model and data + # test loading target tre_target = Target() tre_target.load(RES_DIR) assert tre_target.model.get_params() == target.model.get_params() - assert tre_target.name == target.name + assert tre_target.dataset_name == target.dataset_name assert tre_target.features == target.features assert tre_target.n_samples == target.n_samples assert tre_target.n_samples_orig == target.n_samples_orig @@ -39,3 +40,18 @@ def test_target(get_target): assert np.array_equal(tre_target.y_train_orig, target.y_train_orig) assert np.array_equal(tre_target.X_test_orig, target.X_test_orig) assert np.array_equal(tre_target.y_test_orig, target.y_test_orig) + + # test creating target with data added via constructor + new_target = Target( + model=target.model, + X_train=target.X_train, + y_train=target.y_train, + X_test=target.X_test, + y_test=target.y_test, + X_train_orig=target.X_train_orig, + y_train_orig=target.y_train_orig, + X_test_orig=target.X_test_orig, + y_test_orig=target.y_test_orig, + ) + assert new_target.n_samples == target.n_samples + assert new_target.n_samples_orig == target.n_samples_orig diff --git a/tests/attacks/test_attribute_inference_attack.py b/tests/attacks/test_attribute_inference_attack.py index 1a7bd6bb..59ef08e0 100644 --- a/tests/attacks/test_attribute_inference_attack.py +++ b/tests/attacks/test_attribute_inference_attack.py @@ -2,10 +2,6 @@ from __future__ import annotations -import json -import os -import sys - import pytest from sklearn.ensemble import RandomForestClassifier @@ -30,16 +26,17 @@ def fixture_common_setup(get_target): """Get ready to test some code.""" target = get_target target.model.fit(target.X_train, target.y_train) - attack_obj = attribute_attack.AttributeAttack(n_cpu=7, report_name="aia_report") + attack_obj = attribute_attack.AttributeAttack(n_cpu=7) return target, attack_obj -def test_attack_args(common_setup): - """Test methods in the attack_args class.""" - _, attack_obj = common_setup - attack_obj.__dict__["newkey"] = True - thedict = attack_obj.__dict__ - assert thedict["newkey"] +def test_attack_undefined_feats(common_setup): + """Test attack when features have not been defined.""" + target, attack_obj = common_setup + target.n_features = 0 + target.features = {} + output = attack_obj.attack(target) + assert output == {} def test_unique_max(): @@ -52,7 +49,7 @@ def test_unique_max(): def test_categorical_via_modified_attack_brute_force(common_setup): - """Test lcategoricals using code from brute_force.""" + """Test categoricals using code from brute_force.""" target, _ = common_setup threshold = 0 @@ -89,34 +86,10 @@ def test_continuous_via_modified_bounds_risk(common_setup): def test_aia_on_nursery(common_setup): - """Test AIA on the nursery data with an added continuous feature.""" + """Test attribute inference attack.""" target, attack_obj = common_setup - attack_obj.attack(target) - output = attack_obj.make_report() + output = attack_obj.attack(target) keys = output["attack_experiment_logger"]["attack_instance_logger"][ "instance_0" ].keys() assert "categorical" in keys - - -def test_aia_on_nursery_from_cmd(common_setup): - """Test AIA on the nursery data with an added continuous feature.""" - target, _ = common_setup - target.save(path="tests/test_aia_target") - - config = { - "n_cpu": 7, - "report_name": "commandline_aia_exampl1_report", - } - with open( - os.path.join("tests", "test_config_aia_cmd.json"), "w", encoding="utf-8" - ) as f: - f.write(json.dumps(config)) - - cmd_json = os.path.join("tests", "test_config_aia_cmd.json") - aia_target = os.path.join("tests", "test_aia_target") - os.system( - f"{sys.executable} -m aisdc.attacks.attribute_attack run-attack-from-configfile " - f"--attack-config-json-file-name {cmd_json} " - f"--attack-target-folder-path {aia_target} " - ) diff --git a/tests/attacks/test_factory.py b/tests/attacks/test_factory.py new file mode 100644 index 00000000..619607a4 --- /dev/null +++ b/tests/attacks/test_factory.py @@ -0,0 +1,53 @@ +"""Test attack factory.""" + +from __future__ import annotations + +import json +import os + +import pytest +import yaml +from sklearn.ensemble import RandomForestClassifier + +from aisdc.attacks.factory import run_attacks +from aisdc.config.attack import _get_attack + + +@pytest.mark.parametrize( + "get_target", [RandomForestClassifier(random_state=1)], indirect=True +) +def test_factory(monkeypatch, get_target): + """Test Target object creation, saving, and loading.""" + # create target_dir + target = get_target + target.save("target_factory") + + model = target.model + assert model.score(target.X_test, target.y_test) == pytest.approx(0.92, 0.01) + + # create LiRA config with default params + mock_input = "yes" + monkeypatch.setattr("builtins.input", lambda _: mock_input) + attacks = [_get_attack("lira")] + attacks[0]["params"]["output_dir"] = "outputs_factory" + + # create attack.yaml + filename: str = "attack.yaml" + with open(filename, "w", encoding="utf-8") as fp: + yaml.dump({"attacks": attacks}, fp) + + # run attacks + run_attacks("target_factory", "attack.yaml") + + # load JSON report + path = os.path.normpath("outputs_factory/report.json") + with open(path, encoding="utf-8") as fp: + report = json.load(fp) + + # check report output + nr = list(report.keys())[0] + metrics = report[nr]["attack_experiment_logger"]["attack_instance_logger"][ + "instance_0" + ] + assert metrics["TPR"] == pytest.approx(0.89, abs=0.01) + assert metrics["FPR"] == pytest.approx(0.43, abs=0.01) diff --git a/tests/attacks/test_failfast.py b/tests/attacks/test_failfast.py deleted file mode 100644 index df888632..00000000 --- a/tests/attacks/test_failfast.py +++ /dev/null @@ -1,164 +0,0 @@ -"""Test fail fast.""" - -from __future__ import annotations - -import unittest - -from aisdc.attacks import failfast, worst_case_attack - - -class TestFailFast(unittest.TestCase): - """Tests the fail fast functionality of the WortCaseAttack module.""" - - def test_parse_boolean_argument(self): - """Test all comparison operators and both options. - - Tests for attack being successful and not successful given a metric and - comparison operator with a threshold value. - """ - metrics = {} - metrics["ACC"] = 0.9 - metrics["AUC"] = 0.8 - metrics["P_HIGHER_AUC"] = 0.05 - - # Option 1 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.04, - attack_metric_success_comp_type="lte", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 2 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="lte", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - - # Option 3 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.04, - attack_metric_success_comp_type="lt", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 4 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="lt", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - - # Option 5 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.04, - attack_metric_success_comp_type="gte", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - - # Option 6 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="gte", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 7 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.04, - attack_metric_success_comp_type="gt", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - - # Option 8 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="gt", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 9 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.05, - attack_metric_success_comp_type="eq", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - - # Option 10 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="eq", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 11 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.05, - attack_metric_success_comp_type="not_eq", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert not failfast_obj.check_attack_success(metrics) - - # Option 12 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.06, - attack_metric_success_comp_type="not_eq", - ) - failfast_obj = failfast.FailFast(attack_obj) - assert failfast_obj.check_attack_success(metrics) - assert failfast_obj.get_fail_count() == 0 - - def test_attack_success_fail_counts_and_overall_attack_success(self): - """Test success and fail counts of attacks. - - Tests for a given threshold of a given metric based on a given - comparison operation and also test overall attack successes using count - threshold of attack being successful or not successful. - """ - metrics = {} - metrics["ACC"] = 0.9 - metrics["AUC"] = 0.8 - metrics["P_HIGHER_AUC"] = 0.08 - attack_obj = worst_case_attack.WorstCaseAttack( - attack_metric_success_name="P_HIGHER_AUC", - attack_metric_success_thresh=0.05, - attack_metric_success_comp_type="lte", - attack_metric_success_count_thresh=3, - ) - failfast_obj = failfast.FailFast(attack_obj) - _ = failfast_obj.check_attack_success(metrics) - metrics["P_HIGHER_AUC"] = 0.07 - _ = failfast_obj.check_attack_success(metrics) - metrics["P_HIGHER_AUC"] = 0.03 - _ = failfast_obj.check_attack_success(metrics) - assert not failfast_obj.check_overall_attack_success(attack_obj) - - metrics["P_HIGHER_AUC"] = 0.02 - _ = failfast_obj.check_attack_success(metrics) - metrics["P_HIGHER_AUC"] = 0.01 - _ = failfast_obj.check_attack_success(metrics) - assert failfast_obj.get_success_count() == 3 - assert failfast_obj.get_fail_count() == 2 - assert failfast_obj.check_overall_attack_success(attack_obj) diff --git a/tests/attacks/test_lira_attack.py b/tests/attacks/test_lira_attack.py index 4732f97d..2ddd346f 100644 --- a/tests/attacks/test_lira_attack.py +++ b/tests/attacks/test_lira_attack.py @@ -2,24 +2,15 @@ from __future__ import annotations -import os -import sys -from unittest.mock import patch - import numpy as np import pytest from sklearn.datasets import load_breast_cancer from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split -from aisdc.attacks import likelihood_attack from aisdc.attacks.likelihood_attack import DummyClassifier, LIRAAttack from aisdc.attacks.target import Target -N_SHADOW_MODELS = 20 -LR_CONFIG = os.path.normpath("tests/attacks/lrconfig.json") -LR_CMD_CONFIG = os.path.normpath("tests/attacks/lrconfig_cmd.json") - @pytest.fixture(name="dummy_classifier_setup") def fixture_dummy_classifier_setup(): @@ -56,171 +47,11 @@ def fixture_lira_classifier_setup(): target = Target(target_model) target.add_processed_data(X_train, y_train, X_test, y_test) target.save(path="test_lira_target") - # Dump training and test data to csv - np.savetxt( - "train_data.csv", - np.hstack((X_train, y_train[:, None])), - delimiter=",", - ) - np.savetxt("test_data.csv", np.hstack((X_test, y_test[:, None])), delimiter=",") - # dump the training and test predictions into files - np.savetxt( - "train_preds.csv", - target_model.predict_proba(X_train), - delimiter=",", - ) - np.savetxt("test_preds.csv", target_model.predict_proba(X_test), delimiter=",") return target def test_lira_attack(lira_classifier_setup): - """Tests the lira code two ways.""" - target = lira_classifier_setup - attack_obj = LIRAAttack( - n_shadow_models=N_SHADOW_MODELS, - output_dir="test_output_lira", - attack_config_json_file_name=LR_CONFIG, - ) - attack_obj.setup_example_data() - attack_obj.attack_from_config() - attack_obj.example() - - attack_obj2 = LIRAAttack( - n_shadow_models=N_SHADOW_MODELS, - output_dir="test_output_lira", - report_name="lira_example1_report", - ) - attack_obj2.attack(target) - output2 = attack_obj2.make_report() - n_shadow_models_trained = output2["attack_experiment_logger"][ - "attack_instance_logger" - ]["instance_0"]["n_shadow_models_trained"] - n_shadow_models = output2["metadata"]["experiment_details"]["n_shadow_models"] - assert n_shadow_models_trained == n_shadow_models - - -def test_check_and_update_dataset(lira_classifier_setup): - """Test removal from test set with classes not present in training set.""" - target = lira_classifier_setup - attack_obj = LIRAAttack(n_shadow_models=N_SHADOW_MODELS) - - # now make test[0] have a class not present in training set# - local_y_test = np.copy(target.y_test) - local_y_test[0] = 5 - local_target = Target(target.model) - local_target.add_processed_data( - target.X_train, target.y_train, target.X_test, local_y_test - ) - unique_classes_pre = set(local_y_test) - n_test_examples_pre = len(local_y_test) - local_target = attack_obj._check_and_update_dataset( # pylint: disable=protected-access - local_target - ) - - unique_classes_post = set(local_target.y_test) - n_test_examples_post = len(local_target.y_test) - - assert local_target.y_test[0] != 5 - assert (n_test_examples_pre - n_test_examples_post) == 1 - class_diff = unique_classes_pre - unique_classes_post - assert class_diff == {5} - - # Test command line example. - testargs = [ - "prog", - "run-example", - "--output-dir", - "test_output_lira", - "--report-name", - "commandline_lira_example2_report", - ] - with patch.object(sys, "argv", testargs): - likelihood_attack.main() - - # Test command line with a config file. - testargs = [ - "prog", - "run-attack", - "-j", - LR_CONFIG, - "--output-dir", - "test_output_lira", - "--report-name", - "commandline_lira_example1_report", - ] - with patch.object(sys, "argv", testargs): - likelihood_attack.main() - - # Test command line with a config file. - testargs = [ - "prog", - "run-attack-from-configfile", - "-j", - LR_CMD_CONFIG, - "-t", - "test_lira_target", - ] - with patch.object(sys, "argv", testargs): - likelihood_attack.main() - - # Test command line example data creation. - testargs = [ - "prog", - "setup-example-data", - ] - with patch.object(sys, "argv", testargs): - likelihood_attack.main() - - -def test_lira_attack_failfast_example(): - """Tests the lira code two ways.""" - attack_obj = LIRAAttack( - n_shadow_models=N_SHADOW_MODELS, - output_dir="test_output_lira", - attack_config_json_file_name=LR_CONFIG, - shadow_models_fail_fast=True, - n_shadow_rows_confidences_min=10, - ) - attack_obj.setup_example_data() - attack_obj.attack_from_config() - attack_obj.example() - - -def test_lira_attack_failfast_from_scratch1(lira_classifier_setup): - """Test by training a model from scratch.""" + """Test LiRA attack.""" target = lira_classifier_setup - attack_obj = LIRAAttack( - n_shadow_models=N_SHADOW_MODELS, - output_dir="test_output_lira", - report_name="lira_example2_failfast_report", - attack_config_json_file_name=LR_CONFIG, - shadow_models_fail_fast=True, - n_shadow_rows_confidences_min=10, - ) - attack_obj.attack(target) - output = attack_obj.make_report() - n_shadow_models_trained = output["attack_experiment_logger"][ - "attack_instance_logger" - ]["instance_0"]["n_shadow_models_trained"] - n_shadow_models = output["metadata"]["experiment_details"]["n_shadow_models"] - assert n_shadow_models_trained == n_shadow_models - - -def test_lira_attack_failfast_from_scratch2(lira_classifier_setup): - """Test by training a model from scratch.""" - target = lira_classifier_setup - attack_obj = LIRAAttack( - n_shadow_models=150, - output_dir="test_output_lira", - report_name="lira_example3_failfast_report", - attack_config_json_file_name=LR_CONFIG, - shadow_models_fail_fast=True, - n_shadow_rows_confidences_min=10, - ) - attack_obj.attack(target) - output = attack_obj.make_report() - n_shadow_models_trained = output["attack_experiment_logger"][ - "attack_instance_logger" - ]["instance_0"]["n_shadow_models_trained"] - n_shadow_models = output["metadata"]["experiment_details"]["n_shadow_models"] - assert n_shadow_models_trained < n_shadow_models + lira = LIRAAttack(n_shadow_models=20, output_dir="test_output_lira") + lira.attack(target) diff --git a/tests/attacks/test_multiple_attacks.py b/tests/attacks/test_multiple_attacks.py deleted file mode 100644 index 1437e178..00000000 --- a/tests/attacks/test_multiple_attacks.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Test multiple attacks (MIA and AIA) using a single configuration file.""" - -from __future__ import annotations - -import json -import os -import sys - -import pytest -from sklearn.ensemble import RandomForestClassifier - -from aisdc.attacks.multiple_attacks import ConfigFile, MultipleAttacks - - -def pytest_generate_tests(metafunc): - """Generate target model for testing.""" - if "get_target" in metafunc.fixturenames: - metafunc.parametrize( - "get_target", [RandomForestClassifier(bootstrap=False)], indirect=True - ) - - -@pytest.fixture(name="common_setup") -def fixture_common_setup(get_target): - """Get ready to test some code.""" - target = get_target - target.model.fit(target.X_train, target.y_train) - attack_obj = MultipleAttacks(config_filename="test_single_config.json") - return target, attack_obj - - -def create_single_config_file(): - """Create single config file using multiple attack configuration.""" - configfile_obj = ConfigFile(filename="test_single_config.json") - - # Example 1: Add 3 different worst case configuration dictionaries to JSON - config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "worst_case") - - config = { - "n_reps": 20, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "worst_case") - - config = { - "n_reps": 10, - "n_dummy_reps": 1, - "p_thresh": 0.05, - "test_prop": 0.5, - "train_beta": 5, - "test_beta": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "attack_metric_success_name": "P_HIGHER_AUC", - "attack_metric_success_thresh": 0.05, - "attack_metric_success_comp_type": "lte", - "attack_metric_success_count_thresh": 2, - "attack_fail_fast": True, - } - configfile_obj.add_config(config, "worst_case") - - # Add 2 different lira attack configuration dictionaries to JSON - config = { - "n_shadow_models": 100, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - configfile_obj.add_config(config, "lira") - - config = { - "n_shadow_models": 150, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "shadow_models_fail_fast": True, - "n_shadow_rows_confidences_min": 10, - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - configfile_obj.add_config(config, "lira") - # add explicitly wrong attack name to cover codecov test - configfile_obj.add_config(config, "lirrra") - - # Example 3: Add a lira JSON configuration file to a configuration file - # having multiple attack configurations - config = { - "n_shadow_models": 120, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - "shadow_models_fail_fast": True, - "n_shadow_rows_confidences_min": 10, - "training_data_filename": "train_data.csv", - "test_data_filename": "test_data.csv", - "training_preds_filename": "train_preds.csv", - "test_preds_filename": "test_preds.csv", - "target_model": ["sklearn.ensemble", "RandomForestClassifier"], - "target_model_hyp": {"min_samples_split": 2, "min_samples_leaf": 1}, - } - with open("test_lira_config.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - configfile_obj.add_config("test_lira_config.json", "lira") - - # Example 4: Add an attribute configuration dictionary - # from an existing configuration file to JSON - config = { - "n_cpu": 2, - "output_dir": "outputs_multiple_attacks", - "report_name": "report_multiple_attacks", - } - configfile_obj.add_config(config, "attribute") - os.remove("test_lira_config.json") - return configfile_obj - - -def test_configfile_number(): - """Test attack configurations in a configuration file.""" - configfile_obj = create_single_config_file() - configfile_data = configfile_obj.read_config_file() - assert len(configfile_data) == 8 - os.remove("test_single_config.json") - - -def test_multiple_attacks_programmatic(common_setup): - """Test programmatically running attacks using a single config file.""" - target, attack_obj = common_setup - _ = create_single_config_file() - attack_obj.attack(target) - print(attack_obj) - os.remove("test_single_config.json") - - -def test_multiple_attacks_cmd(common_setup): - """Test multiple attacks (MIA and AIA) with a continuous feature.""" - target, _ = common_setup - target.save(path=os.path.join("tests", "test_multiple_target")) - _ = create_single_config_file() - - multiple_target = os.path.join("tests", "test_multiple_target") - os.system( - f"{sys.executable} -m aisdc.attacks.multiple_attacks run-attack-from-configfile " - "--attack-config-json-file-name test_single_config.json " - f"--attack-target-folder-path {multiple_target} " - ) diff --git a/tests/attacks/test_report.py b/tests/attacks/test_report.py new file mode 100644 index 00000000..3bcb7bda --- /dev/null +++ b/tests/attacks/test_report.py @@ -0,0 +1,46 @@ +"""Test attack report module.""" + +from __future__ import annotations + +import numpy as np +from fpdf import FPDF + +from aisdc.attacks import report + +BORDER = 0 + + +def test_custom_json_encoder(): + """Test custom JSON encoder.""" + i32 = np.int32(2) + i64 = np.int64(2) + array_2d = np.zeros((2, 2)) + my_encoder = report.CustomJSONEncoder() + + retval = my_encoder.default(i32) + assert isinstance(retval, int) + + retval = my_encoder.default(i64) + assert isinstance(retval, int) + + retval = my_encoder.default(array_2d) + assert isinstance(retval, list) + + +def test_line(): + """Test add line to PDF.""" + pdf = FPDF() + pdf.add_page() + report.line(pdf, "foo") + pdf.close() + + +def test_dict(): + """Test write dictionary to PDF.""" + pdf = FPDF() + pdf.add_page() + mydict = {"a": "hello", "b": "world"} + report._write_dict( # pylint:disable=protected-access + pdf, mydict, border=BORDER + ) + pdf.close() diff --git a/tests/attacks/test_structural_attack.py b/tests/attacks/test_structural_attack.py index 86b71f80..17fc66f6 100644 --- a/tests/attacks/test_structural_attack.py +++ b/tests/attacks/test_structural_attack.py @@ -2,10 +2,6 @@ from __future__ import annotations -import json -import sys -from unittest.mock import patch - import pytest from sklearn.datasets import load_breast_cancer from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier @@ -55,7 +51,7 @@ def test_unnecessary_risk(): """Check the unnecessary rules.""" # non-tree we have no evidence yet model = SVC() - assert sa.get_unnecessary_risk(model) == 0, "no risk without evidence" + assert not sa.get_unnecessary_risk(model), "no risk without evidence" # decision tree next risky_param_dicts = [ { @@ -93,10 +89,10 @@ def test_unnecessary_risk(): for idx, paramdict in enumerate(risky_param_dicts): model = DecisionTreeClassifier(**paramdict) errstr = f" unnecessary risk with rule {idx}" f"params are {model.get_params()}" - assert sa.get_unnecessary_risk(model) == 1, errstr + assert sa.get_unnecessary_risk(model), errstr model = DecisionTreeClassifier(max_depth=1, min_samples_leaf=150) - assert ( - sa.get_unnecessary_risk(model) == 0 + assert not sa.get_unnecessary_risk( + model ), f"should be non-disclosive with {model.get_params}" # now random forest @@ -119,10 +115,10 @@ def test_unnecessary_risk(): for idx, paramdict in enumerate(risky_param_dicts): model = RandomForestClassifier(**paramdict) errstr = f" unnecessary risk with rule {idx}" f"params are {model.get_params()}" - assert sa.get_unnecessary_risk(model) == 1, errstr + assert sa.get_unnecessary_risk(model), errstr model = RandomForestClassifier(max_depth=1, n_estimators=25, min_samples_leaf=150) - assert ( - sa.get_unnecessary_risk(model) == 0 + assert not sa.get_unnecessary_risk( + model ), f"should be non-disclosive with {model.get_params}" # finally xgboost @@ -146,10 +142,10 @@ def test_unnecessary_risk(): for idx, paramdict in enumerate(risky_param_dicts): model = XGBClassifier(**paramdict) errstr = f" unnecessary risk with rule {idx}" f"params are {model.get_params()}" - assert sa.get_unnecessary_risk(model) == 1, errstr + assert sa.get_unnecessary_risk(model), errstr model = XGBClassifier(min_child_weight=10) - assert ( - sa.get_unnecessary_risk(model) == 0 + assert not sa.get_unnecessary_risk( + model ), f"should be non-disclosive with {model.get_params}" @@ -173,29 +169,29 @@ def test_dt(): target = get_target("dt", **param_dict) myattack = sa.StructuralAttack() myattack.attack(target) - assert myattack.DoF_risk == 0, "should be no DoF risk with decision stump" + assert not myattack.dof_risk, "should be no DoF risk with decision stump" assert ( - myattack.k_anonymity_risk == 0 + not myattack.k_anonymity_risk ), "should be no k-anonymity risk with min_samples_leaf 150" assert ( - myattack.class_disclosure_risk == 0 + not myattack.class_disclosure_risk ), "no class disclosure risk for stump with min samples leaf 150" - assert myattack.unnecessary_risk == 0, "not unnecessary risk if max_depth < 3.5" + assert not myattack.unnecessary_risk, "not unnecessary risk if max_depth < 3.5" # highly disclosive param_dict2 = {"max_depth": None, "min_samples_leaf": 1, "min_samples_split": 2} target = get_target("dt", **param_dict2) myattack = sa.StructuralAttack() myattack.attack(target) - assert myattack.DoF_risk == 0, "should be no DoF risk with decision stump" + assert not myattack.dof_risk, "should be no DoF risk with decision stump" assert ( - myattack.k_anonymity_risk == 1 + myattack.k_anonymity_risk ), "should be k-anonymity risk with unlimited depth and min_samples_leaf 5" assert ( - myattack.class_disclosure_risk == 1 + myattack.class_disclosure_risk ), "should be class disclosure risk with unlimited depth and min_samples_leaf 5" assert ( - myattack.unnecessary_risk == 1 + myattack.unnecessary_risk ), " unnecessary risk with unlimited depth and min_samples_leaf 5" @@ -209,12 +205,12 @@ def test_adaboost(): myattack = sa.StructuralAttack() myattack.THRESHOLD = 2 myattack.attack(target) - assert myattack.DoF_risk == 0, "should be no DoF risk with just 2 decision stumps" + assert not myattack.dof_risk, "should be no DoF risk with just 2 decision stumps" assert ( - myattack.k_anonymity_risk == 0 + not myattack.k_anonymity_risk ), "should be no k-anonymity risk with only 2 stumps" - assert myattack.class_disclosure_risk == 0, "no class disclosure risk for 2 stumps" - assert myattack.unnecessary_risk == 0, " unnecessary risk not defined for adaboost" + assert not myattack.class_disclosure_risk, "no class disclosure risk for 2 stumps" + assert not myattack.unnecessary_risk, " unnecessary risk not defined for adaboost" # highly disclosive kwargs = {"max_depth": None, "min_samples_leaf": 2} @@ -225,14 +221,14 @@ def test_adaboost(): target = get_target("adaboost", **param_dict2) myattack2 = sa.StructuralAttack() myattack2.attack(target) - assert myattack2.DoF_risk == 1, "should be DoF risk with adaboost of deep trees" + assert myattack2.dof_risk, "should be DoF risk with adaboost of deep trees" assert ( - myattack2.k_anonymity_risk == 1 - ), "should be k-anonymity risk with adaboost unlimited depth and min_samples_leaf 2" + myattack2.k_anonymity_risk + ), "should be k-anonymity risk with adaboost unlimited depth and min_samples_leaf 2" assert ( - myattack2.class_disclosure_risk == 1 - ), "should be class disclosure risk with adaboost unlimited depth and min_samples_leaf 2" - assert myattack2.unnecessary_risk == 0, " unnecessary risk not define for adaboost" + myattack2.class_disclosure_risk + ), "should be class risk with adaboost unlimited depth and min_samples_leaf 2" + assert not myattack2.unnecessary_risk, " unnecessary risk not define for adaboost" def test_rf(): @@ -243,15 +239,15 @@ def test_rf(): myattack = sa.StructuralAttack() myattack.attack(target) assert ( - myattack.DoF_risk == 0 + not myattack.dof_risk ), "should be no DoF risk with small forest of decision stumps" assert ( - myattack.k_anonymity_risk == 0 + not myattack.k_anonymity_risk ), "should be no k-anonymity risk with min_samples_leaf 150" assert ( - myattack.class_disclosure_risk == 0 + not myattack.class_disclosure_risk ), "no class disclosure risk for stumps with min samples leaf 150" - assert myattack.unnecessary_risk == 0, "not unnecessary risk if max_depth < 3.5" + assert not myattack.unnecessary_risk, "not unnecessary risk if max_depth < 3.5" # highly disclosive param_dict2 = { @@ -263,15 +259,15 @@ def test_rf(): target = get_target("rf", **param_dict2) myattack = sa.StructuralAttack() myattack.attack(target) - assert myattack.DoF_risk == 1, "should be DoF risk with forest of deep trees" + assert myattack.dof_risk, "should be DoF risk with forest of deep trees" assert ( - myattack.k_anonymity_risk == 1 + myattack.k_anonymity_risk ), "should be k-anonymity risk with unlimited depth and min_samples_leaf 5" assert ( - myattack.class_disclosure_risk == 1 + myattack.class_disclosure_risk ), "should be class disclsoure risk with unlimited depth and min_samples_leaf 5" assert ( - myattack.unnecessary_risk == 1 + myattack.unnecessary_risk ), " unnecessary risk with unlimited depth and min_samples_leaf 5" @@ -283,13 +279,13 @@ def test_xgb(): myattack = sa.StructuralAttack() myattack.attack(target) assert ( - myattack.DoF_risk == 0 + not myattack.dof_risk ), "should be no DoF risk with small xgb of decision stumps" assert ( - myattack.k_anonymity_risk == 0 + not myattack.k_anonymity_risk ), "should be no k-anonymity risk with min_samples_leaf 150" assert ( - myattack.class_disclosure_risk == 0 + not myattack.class_disclosure_risk ), "no class disclosure risk for stumps with min child weight 50" assert myattack.unnecessary_risk == 0, "not unnecessary risk if max_depth < 3.5" @@ -298,14 +294,14 @@ def test_xgb(): target2 = get_target("xgb", **param_dict2) myattack2 = sa.StructuralAttack() myattack2.attack(target2) - assert myattack2.DoF_risk == 1, "should be DoF risk with xgb of deep trees" + assert myattack2.dof_risk, "should be DoF risk with xgb of deep trees" assert ( - myattack2.k_anonymity_risk == 1 + myattack2.k_anonymity_risk ), "should be k-anonymity risk with depth 50 and min_child_weight 1" assert ( - myattack2.class_disclosure_risk == 1 + myattack2.class_disclosure_risk ), "should be class disclosure risk with xgb lots of deep trees" - assert myattack2.unnecessary_risk == 1, " unnecessary risk with these xgb params" + assert myattack2.unnecessary_risk, " unnecessary risk with these xgb params" def test_sklearnmlp(): @@ -324,15 +320,15 @@ def test_sklearnmlp(): for key, val in safeparams.items(): paramstr += f"{key}:{val}\n" assert ( - myattack.DoF_risk == 0 + not myattack.dof_risk ), f"should be no DoF risk with small mlp with params {paramstr}" assert ( - myattack.k_anonymity_risk == 0 + not myattack.k_anonymity_risk ), f"should be no k-anonymity risk with params {paramstr}" assert ( - myattack.class_disclosure_risk == 1 + myattack.class_disclosure_risk ), f"should be class disclosure risk with params {paramstr}" - assert myattack.unnecessary_risk == 0, "not unnecessary risk for mlps at present" + assert not myattack.unnecessary_risk, "not unnecessary risk for mlps at present" # highly disclosive unsafeparams = { @@ -347,16 +343,14 @@ def test_sklearnmlp(): target2 = get_target("mlpclassifier", **unsafeparams) myattack2 = sa.StructuralAttack() myattack2.attack(target2) - assert myattack2.DoF_risk == 1, f"should be DoF risk with this MLP:\n{uparamstr}" + assert myattack2.dof_risk, f"should be DoF risk with this MLP:\n{uparamstr}" assert ( - myattack2.k_anonymity_risk == 1 - ), "559/560 records should have should be k-anonymity 1 with this MLP:\n{uparamstr}" + myattack2.k_anonymity_risk + ), "559/560 records should be k-anonymity risk with this MLP:\n{uparamstr}" assert ( - myattack2.class_disclosure_risk == 1 + myattack2.class_disclosure_risk ), "should be class disclosure risk with this MLP:\n{uparamstr}" - assert ( - myattack2.unnecessary_risk == 0 - ), " no unnecessary risk yet for MLPClassifiers" + assert not myattack2.unnecessary_risk, "no unnecessary risk yet for MLPClassifiers" def test_reporting(): @@ -365,40 +359,3 @@ def test_reporting(): target = get_target("dt", **param_dict) myattack = sa.StructuralAttack() myattack.attack(target) - myattack.make_report() - - -def test_main_example(): - """Test command line example.""" - param_dict = {"max_depth": 1, "min_samples_leaf": 150} - target = get_target("dt", **param_dict) - target_path = "dt.sav" - target.save(target_path) - testargs = [ - "prog", - "run-attack", - "--target-path", - target_path, - "--output-dir", - "test_output_sa", - "--report-name", - "commandline_structural_report", - ] - with patch.object(sys, "argv", testargs): - sa.main() - config = { - "output_dir": "test_output_structural2", - "report_name": "structural_test", - } - with open("config_structural_test.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - testargs = [ - "prog", - "run-attack-from-configfile", - "--attack-config-json-file-name", - "config_structural_test.json", - "--attack-target-folder-path", - "dt.sav", - ] - with patch.object(sys, "argv", testargs): - sa.main() diff --git a/tests/attacks/test_worst_case_attack.py b/tests/attacks/test_worst_case_attack.py index 6860a606..91ce8787 100644 --- a/tests/attacks/test_worst_case_attack.py +++ b/tests/attacks/test_worst_case_attack.py @@ -2,116 +2,57 @@ from __future__ import annotations -import json -import os -import sys -from unittest.mock import patch - import numpy as np import pytest -from sklearn.datasets import load_breast_cancer -from sklearn.model_selection import train_test_split from sklearn.svm import SVC from aisdc.attacks import worst_case_attack from aisdc.attacks.target import Target -def test_config_file_arguments_parsin(): - """Tests reading parameters from the configuration file.""" - config = { - "n_reps": 12, - "n_dummy_reps": 2, - "p_thresh": 0.06, - "test_prop": 0.4, - "output_dir": "test_output_worstcase", - "report_name": "programmatically_worstcase_example1_test", - } - with open("config_worstcase_test.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - attack_obj = worst_case_attack.WorstCaseAttack( - attack_config_json_file_name="config_worstcase_test.json", - ) - assert attack_obj.n_reps == config["n_reps"] - assert attack_obj.n_dummy_reps == config["n_dummy_reps"] - assert attack_obj.p_thresh == config["p_thresh"] - assert attack_obj.test_prop == config["test_prop"] - assert attack_obj.report_name == config["report_name"] - os.remove("config_worstcase_test.json") - - -def test_attack_from_predictions_cmd(): - """Running attack using configuration file and prediction files.""" - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - - ytr_pred = model.predict_proba(X_train) - yte_pred = model.predict_proba(X_test) - np.savetxt("ypred_train.csv", ytr_pred, delimiter=",") - np.savetxt("ypred_test.csv", yte_pred, delimiter=",") - - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) - - target.save(path="test_worstcase_target") - - config = { - "n_reps": 30, - "n_dummy_reps": 2, - "p_thresh": 0.05, - "test_prop": 0.5, - "output_dir": "test_output_worstcase", - "report_name": "commandline_worstcase_example1_report", - "training_preds_filename": "ypred_train.csv", - "test_preds_filename": "ypred_test.csv", - "attack_metric_success_name": "P_HIGHER_AUC", - "attack_metric_success_thresh": 0.05, - "attack_metric_success_comp_type": "lte", - "attack_metric_success_count_thresh": 2, - "attack_fail_fast": True, - } - - with open("config_worstcase_cmd.json", "w", encoding="utf-8") as f: - f.write(json.dumps(config)) - os.system( - f"{sys.executable} -m aisdc.attacks.worst_case_attack run-attack-from-configfile " - "--attack-config-json-file-name config_worstcase_cmd.json " - "--attack-target-folder-path test_worstcase_target " - ) - os.remove("config_worstcase_cmd.json") - os.remove("ypred_train.csv") - os.remove("ypred_test.csv") +def pytest_generate_tests(metafunc): + """Generate target model for testing.""" + if "get_target" in metafunc.fixturenames: + metafunc.parametrize( + "get_target", [SVC(gamma=0.1, probability=True)], indirect=True + ) -def test_report_worstcase(): - """Tests worst case attack directly.""" - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) +@pytest.fixture(name="common_setup") +def fixture_common_setup(get_target): + """Get ready to test some code.""" + target = get_target + target.model.fit(target.X_train, target.y_train) + return target - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - _ = model.predict_proba(X_train) - _ = model.predict_proba(X_test) - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) +def test_insufficient_target(): + """Test insufficient target details to run.""" + target = Target() + attack_obj = worst_case_attack.WorstCaseAttack( + n_reps=10, + n_dummy_reps=1, + p_thresh=0.05, + test_prop=0.5, + output_dir="test_output_worstcase", + ) + output = attack_obj.attack(target) + assert not output + + +def test_report_worstcase(common_setup): + """Test worst case attack directly.""" + target = common_setup # with multiple reps attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data n_reps=10, n_dummy_reps=1, p_thresh=0.05, - training_preds_filename=None, - test_preds_filename=None, test_prop=0.5, output_dir="test_output_worstcase", ) attack_obj.attack(target) - _ = attack_obj.make_report() # with one rep attack_obj = worst_case_attack.WorstCaseAttack( @@ -119,37 +60,22 @@ def test_report_worstcase(): n_reps=1, n_dummy_reps=1, p_thresh=0.05, - training_preds_filename=None, - test_preds_filename=None, test_prop=0.5, output_dir="test_output_worstcase", ) attack_obj.attack(target) - _ = attack_obj.make_report() -def test_attack_with_correct_feature(): +def test_attack_with_correct_feature(common_setup): """Test the attack when the model correctness feature is used.""" - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) - - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) + target = common_setup # with multiple reps attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data n_reps=1, n_dummy_reps=1, p_thresh=0.05, - training_preds_filename=None, - test_preds_filename=None, test_prop=0.5, - report_name="test-1rep-programmatically_worstcase_example4_test", include_model_correct_feature=True, ) attack_obj.attack(target) @@ -160,90 +86,42 @@ def test_attack_with_correct_feature(): assert "yeom_advantage" in attack_obj.attack_metrics[0] -def test_attack_from_predictions(): +def test_attack_from_predictions(common_setup): """Checks code that runs attacks from predictions.""" - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) + target = common_setup - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - ytr_pred = model.predict_proba(X_train) - yte_pred = model.predict_proba(X_test) - np.savetxt("ypred_train.csv", ytr_pred, delimiter=",") - np.savetxt("ypred_test.csv", yte_pred, delimiter=",") + ytr_pred = target.model.predict_proba(target.X_train) + yte_pred = target.model.predict_proba(target.X_test) - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) + new_target = Target(model=target.model, proba_train=ytr_pred, proba_test=yte_pred) attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data n_reps=10, n_dummy_reps=1, p_thresh=0.05, - training_preds_filename="ypred_train.csv", - test_preds_filename="ypred_test.csv", test_prop=0.5, output_dir="test_output_worstcase", - report_name="test-10reps-programmatically_worstcase_example5_test", ) - - assert attack_obj.training_preds_filename == "ypred_train.csv" - - # with multiple reps - attack_obj.attack_from_prediction_files() + attack_obj.attack(new_target) -def test_attack_from_predictions_no_dummy(): +def test_attack_from_predictions_no_dummy(common_setup): """Checks code that runs attacks from predictions.""" - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) + target = common_setup - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - ytr_pred = model.predict_proba(X_train) - yte_pred = model.predict_proba(X_test) - np.savetxt("ypred_train.csv", ytr_pred, delimiter=",") - np.savetxt("ypred_test.csv", yte_pred, delimiter=",") + ytr_pred = target.model.predict_proba(target.X_train) + yte_pred = target.model.predict_proba(target.X_test) - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) + new_target = Target(model=target.model, proba_train=ytr_pred, proba_test=yte_pred) attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data n_reps=10, n_dummy_reps=0, p_thresh=0.05, - training_preds_filename="ypred_train.csv", - test_preds_filename="ypred_test.csv", - test_prop=0.5, - output_dir="test_output_worstcase", - report_name="test-10reps-programmatically_worstcase_example6_test", - ) - - assert attack_obj.training_preds_filename == "ypred_train.csv" - print(attack_obj) - # with multiple reps - attack_obj.attack_from_prediction_files() - - -def test_dummy_data(): - """Test functionality around creating dummy data.""" - attack_obj = worst_case_attack.WorstCaseAttack( - # How many attacks to run -- in each the attack model is trained on a different - # subset of the data - n_reps=10, - n_dummy_reps=1, - p_thresh=0.05, - training_preds_filename="ypred_train.csv", - test_preds_filename="ypred_test.csv", test_prop=0.5, output_dir="test_output_worstcase", - report_name="test-10reps-programmatically_worstcase_example7_test", ) - - attack_obj.make_dummy_data() + attack_obj.attack(new_target) def test_attack_data_prep(): @@ -299,43 +177,3 @@ def test_attack_data_prep_with_correct_feature(): np.testing.assert_array_equal( mi_x, np.array([[1, 0, 1], [0, 1, 0], [2, 0, 0], [0, 2, 1]]) ) - - -def test_non_rf_mia(): - """Test that it is possible to set the attack model via the args. - - In this case, we set as a SVC. But we set probability to false. If the code does - indeed try and use the SVC (as we want) it will fail as it will try and access - the predict_proba which won't work if probability=False. Hence, if the code throws - an AttributeError we now it *is* trying to use the SVC. - """ - X, y = load_breast_cancer(return_X_y=True, as_frame=False) - X_train, X_test, train_y, test_y = train_test_split(X, y, test_size=0.3) - - model = SVC(gamma=0.1, probability=True) - model.fit(X_train, train_y) - ytr_pred = model.predict_proba(X_train) - yte_pred = model.predict_proba(X_test) - - target = Target(model=model) - target.add_processed_data(X_train, train_y, X_test, test_y) - - attack_obj = worst_case_attack.WorstCaseAttack( - mia_attack_model=SVC, - mia_attack_model_hyp={"kernel": "rbf", "probability": False}, - ) - with pytest.raises(AttributeError): - attack_obj.attack_from_preds(ytr_pred, yte_pred) - - -def test_main(): - """Test invocation via command line.""" - # option 1 - testargs = ["prog", "make-dummy-data"] - with patch.object(sys, "argv", testargs): - worst_case_attack.main() - - # option 2 - testargs = ["prog", "run-attack"] - with patch.object(sys, "argv", testargs): - worst_case_attack.main() diff --git a/tests/conftest.py b/tests/conftest.py index f7c29180..83714afc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,15 +14,19 @@ from aisdc.attacks.target import Target +np.random.seed(1) + folders = [ "RES", "dt.sav", "fit.tf", "fit2.tf", "keras_save.tf", + "outputs", "output_attribute", "output_lira", "output_worstcase", + "outputs_factory", "outputs_lira", "outputs_multiple_attacks", "outputs_structural", @@ -30,6 +34,8 @@ "release_dir", "safekeras.tf", "save_test", + "target", + "target_factory", "test_lira_target", "test_output_lira", "test_output_sa", @@ -45,8 +51,7 @@ "1024-WorstCase.png", "2048-WorstCase.png", "attack.txt", - "config.json", - "config_structural_test.json", + "attack.yaml", "dummy.pkl", "dummy.sav", "dummy_model.txt", @@ -61,8 +66,6 @@ "test.json", "test_data.csv", "test_preds.csv", - "test_single_config.json", - "tests/test_config_aia_cmd.json", "train_data.csv", "train_preds.csv", "unpicklable.pkl", @@ -92,7 +95,7 @@ def _cleanup(): @pytest.fixture() def get_target(request) -> Target: # pylint: disable=too-many-locals - """Wrap the model and data in a Target object. + """Return a target object with test data and fitted model. Uses a randomly sampled 10+10% of the nursery data set. """ @@ -121,17 +124,8 @@ def get_target(request) -> Target: # pylint: disable=too-many-locals # [Researcher] Split into training and test sets # target model train / test split - these are strings - ( - X_train_orig, - X_test_orig, - y_train_orig, - y_test_orig, - ) = train_test_split( - x, - y, - test_size=0.05, - stratify=y, - shuffle=True, + X_train_orig, X_test_orig, y_train_orig, y_test_orig = train_test_split( + x, y, test_size=0.05, stratify=y, shuffle=True, random_state=1 ) # now resample the training data reduce number of examples @@ -141,6 +135,7 @@ def get_target(request) -> Target: # pylint: disable=too-many-locals test_size=0.05, stratify=y_train_orig, shuffle=True, + random_state=1, ) # [Researcher] Preprocess dataset @@ -165,9 +160,12 @@ def get_target(request) -> Target: # pylint: disable=too-many-locals xmore = np.concatenate((X_train_orig, X_test_orig)) n_features = np.shape(X_train_orig)[1] + # fit model + model.fit(X_train, y_train) + # wrap target = Target(model=model) - target.name = "nursery" + target.dataset_name = "nursery" target.add_processed_data(X_train, y_train, X_test, y_test) for i in range(n_features - 1): target.add_feature(nursery_data.feature_names[i], indices[i], "onehot") diff --git a/tests/safemodel/test_attacks.py b/tests/safemodel/test_attacks.py deleted file mode 100644 index 1027cf30..00000000 --- a/tests/safemodel/test_attacks.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Tests to pick up odd cases not otherwise covered in code in the attacks folder.""" - -from __future__ import annotations - -import numpy as np -import pytest -from fpdf import FPDF - -from aisdc.attacks import attack, report -from aisdc.attacks.target import Target -from aisdc.safemodel.classifiers import SafeDecisionTreeClassifier - -BORDER = 0 - - -def test_superclass(): - """Test that the exceptions are raised if the superclass is called in error.""" - model = SafeDecisionTreeClassifier() - target = Target(model=model) - my_attack = attack.Attack() - with pytest.raises(NotImplementedError): - my_attack.attack(target) - with pytest.raises(NotImplementedError): - print(str(my_attack)) - - -def test_numpy_array_encoder(): - """Conversion routine from reports.py.""" - i32 = np.int32(2) - i64 = np.int64(2) - array_2d = np.zeros((2, 2)) - my_encoder = report.NumpyArrayEncoder() - - retval = my_encoder.default(i32) - assert isinstance(retval, int) - - retval = my_encoder.default(i64) - assert isinstance(retval, int) - - retval = my_encoder.default(array_2d) - assert isinstance(retval, list) - - with pytest.raises(TypeError): - retval = my_encoder.default("a string") - - -def test_line(): - """Code from report.py.""" - pdf = FPDF() - pdf.add_page() - report.line(pdf, "foo") - pdf.close() - - -def test_dict(): - """Code from report.py.""" - pdf = FPDF() - pdf.add_page() - mydict = {"a": "hello", "b": "world"} - report._write_dict( # pylint:disable=protected-access - pdf, mydict, indent=0, border=BORDER - ) - pdf.close() diff --git a/tests/safemodel/test_attacks_via_safemodel.py b/tests/safemodel/test_attacks_via_safemodel.py index 7cb25431..bf9d360f 100644 --- a/tests/safemodel/test_attacks_via_safemodel.py +++ b/tests/safemodel/test_attacks_via_safemodel.py @@ -38,7 +38,7 @@ def test_run_attack_lira(get_target): assert not disclosive print(np.unique(target.y_test, return_counts=True)) print(np.unique(target.model.predict(target.X_test), return_counts=True)) - metadata = target.model.run_attack(target, "lira", RES_DIR, "lira_res") + metadata = target.model.run_attack(target, "lira", RES_DIR) assert len(metadata) > 0 # something has been added @@ -53,7 +53,7 @@ def test_run_attack_worstcase(get_target): target.model.fit(target.X_train, target.y_train) _, disclosive = target.model.preliminary_check() assert not disclosive - metadata = target.model.run_attack(target, "worst_case", RES_DIR, "wc_res") + metadata = target.model.run_attack(target, "worstcase", RES_DIR) assert len(metadata) > 0 # something has been added @@ -68,35 +68,23 @@ def test_run_attack_attribute(get_target): target.model.fit(target.X_train, target.y_train) _, disclosive = target.model.preliminary_check() assert not disclosive - metadata = target.model.run_attack(target, "attribute", RES_DIR, "attr_res") + metadata = target.model.run_attack(target, "attribute", RES_DIR) assert len(metadata) > 0 # something has been added def test_attack_args(): """Test the attack arguments class.""" - fname = "aia_example" - attack_obj = attribute_attack.AttributeAttack( - output_dir="output_attribute", report_name=fname - ) + attack_obj = attribute_attack.AttributeAttack(output_dir="output_attribute") attack_obj.__dict__["foo"] = "boo" assert attack_obj.__dict__["foo"] == "boo" - assert fname == attack_obj.report_name - fname = "liraa" - attack_obj = likelihood_attack.LIRAAttack( - output_dir="output_lira", report_name=fname - ) + attack_obj = likelihood_attack.LIRAAttack(output_dir="output_lira") attack_obj.__dict__["foo"] = "boo" assert attack_obj.__dict__["foo"] == "boo" - assert fname == attack_obj.report_name - fname = "wca" - attack_obj = worst_case_attack.WorstCaseAttack( - output_dir="output_worstcase", report_name=fname - ) + attack_obj = worst_case_attack.WorstCaseAttack(output_dir="output_worstcase") attack_obj.__dict__["foo"] = "boo" assert attack_obj.__dict__["foo"] == "boo" - assert fname == attack_obj.report_name @pytest.mark.parametrize( @@ -108,5 +96,5 @@ def test_run_attack_unknown(get_target): """Test an unknown attack via safemodel.""" target = get_target target.model.fit(target.X_train, target.y_train) - metadata = target.model.run_attack(target, "unknown", RES_DIR, "unk") + metadata = target.model.run_attack(target, "unknown", RES_DIR) assert metadata["outcome"] == "unrecognised attack type requested" diff --git a/tests/safemodel/test_safedecisiontreeclassifier.py b/tests/safemodel/test_safedecisiontreeclassifier.py index f0302ce9..06485866 100644 --- a/tests/safemodel/test_safedecisiontreeclassifier.py +++ b/tests/safemodel/test_safedecisiontreeclassifier.py @@ -7,8 +7,11 @@ import joblib import numpy as np +import pytest from sklearn import datasets +from aisdc.attacks import attack +from aisdc.attacks.target import Target from aisdc.safemodel import reporting from aisdc.safemodel.classifiers import SafeDecisionTreeClassifier from aisdc.safemodel.classifiers.safedecisiontreeclassifier import ( @@ -17,6 +20,17 @@ ) +def test_superclass(): + """Test that the exceptions are raised if the superclass is called in error.""" + model = SafeDecisionTreeClassifier() + target = Target(model=model) + my_attack = attack.Attack() + with pytest.raises(NotImplementedError): + my_attack.attack(target) + with pytest.raises(NotImplementedError): + print(str(my_attack)) + + def get_data(): """Return data for testing.""" iris = datasets.load_iris() diff --git a/tests/safemodel/test_safekeras2.py b/tests/safemodel/test_safekeras2.py index 63d602c4..40d12a26 100644 --- a/tests/safemodel/test_safekeras2.py +++ b/tests/safemodel/test_safekeras2.py @@ -347,7 +347,8 @@ def test_keras_model_created(): assert ( model.model_type == rightname ), "failed check for model type being set in init()" - # noise multiplier should have been reset from default to one that matches rules.json + # noise multiplier should have been reset from default to one that matches + # rules.json assert model.noise_multiplier == 0.7 @@ -379,7 +380,8 @@ def test_second_keras_model_created(): assert ( model2.model_type == rightname ), "failed check for second model type being set in init()" - # noise multiplier should have been reset from default to one that matches rules.json + # noise multiplier should have been reset from default to one that matches + # rules.json assert model2.noise_multiplier == 0.7 @@ -711,8 +713,8 @@ def test_create_checkfile(): # check release model.request_release(path=RES_DIR, ext=ext) assert os.path.exists(name), f"Failed test to save model as {name}" - name = os.path.normpath(f"{RES_DIR}/target.json") - assert os.path.exists(name), "Failed test to save target.json" + name = os.path.normpath(f"{RES_DIR}/target.yaml") + assert os.path.exists(name), "Failed test to save target.yaml" # now other versions which should not exts = ("sav", "pkl", "undefined") diff --git a/tests/safemodel/test_safemodel.py b/tests/safemodel/test_safemodel.py index 02991758..c594a6bc 100644 --- a/tests/safemodel/test_safemodel.py +++ b/tests/safemodel/test_safemodel.py @@ -3,12 +3,12 @@ from __future__ import annotations import copy -import json import os import pickle import joblib import numpy as np +import yaml from sklearn import datasets from aisdc.safemodel.reporting import get_reporting_string @@ -499,7 +499,7 @@ def test_generic_additional_tests(): def test_request_release_without_attacks(): - """Test request release works and check the content of the json file.""" + """Test request release works and check the content of the yaml file.""" model = SafeDummyClassifier() x, y = get_data() model.fit(x, y) @@ -509,18 +509,18 @@ def test_request_release_without_attacks(): # no file provided, has k_anonymity res_dir = "RES" - json_filename = os.path.normpath(os.path.join(f"{res_dir}", "target.json")) + yaml_filename = os.path.normpath(os.path.join(f"{res_dir}", "target.yaml")) model_filename = os.path.normpath(os.path.join(f"{res_dir}", "model.pkl")) model.request_release(path=res_dir, ext="pkl") - # check that pikle and the json files have been created + # check that pikle and the yaml files have been created assert os.path.isfile(model_filename) - assert os.path.isfile(json_filename) + assert os.path.isfile(yaml_filename) - # check the content of the json file - with open(f"{json_filename}", encoding="utf-8") as file: - json_data = json.load(file) + # check the content of the yaml file + with open(f"{yaml_filename}", encoding="utf-8") as fp: + yaml_data = yaml.safe_load(fp) details, _ = model.preliminary_check(verbose=False) msg_post, _ = model.posthoc_check() @@ -536,4 +536,4 @@ def test_request_release_without_attacks(): "recommendation": recommendation, "reason": reason, "timestamp": model.timestamp, - } in json_data["safemodel"] + } in yaml_data["safemodel"]