├── Ch03 └── discrim_measure.py ├── Ch04 └── Ch04.ipynb ├── Ch05 ├── Ch05.ipynb └── common_utils.py ├── Ch06 ├── Ch06.ipynb └── common_utils.py ├── Ch07 └── Ch07.ipynb ├── Ch08 ├── Ch08 SHAP.ipynb ├── Ch08_GLRM.ipynb ├── Ch08_LIME.ipynb └── Ch08_ProtoDash.ipynb ├── Ch09 ├── classifier_methods.py ├── cnn_model.py ├── fc_model.py └── run_attack.py ├── Ch10 ├── Ch10.ipynb └── datatraining.txt └── README.md /Ch03/discrim_measure.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | warnings.filterwarnings("ignore") ## indexing issue that does not affect these computations 3 | 4 | ## simple but realistic and legally relevant example: simple ratios of success rates 5 | import pandas as pd 6 | import numpy as np 7 | 8 | ## p 92 9 | data = pd.read_csv("german.data", delim_whitespace = True, header = None) 10 | 11 | success_data = data.groupby(8).agg([lambda x: sum(x == 1), 12 | lambda x: len(x)]).iloc[:, -2:] 13 | success_data.columns = ["num_success", "num_total"] 14 | most_successful = np.max(success_data.num_success / success_data.num_total) 15 | print(success_data.num_success / success_data.num_total / most_successful) 16 | 17 | ## p 94 18 | ## more nuanced example 19 | ## looking at whether a rule is alpha discriminatory 20 | ## note code is updated as compared to book to reflect code deprecations 21 | cond1 = data.iloc[:, 8] == "A91" 22 | num_div_males = data[cond1].shape[0] 23 | 24 | cond2 = data.iloc[:, 5] == "A65" 25 | num_unknown_credit = data[cond2].shape[0] 26 | 27 | cond3 = data.iloc[:, 5] == "A65" 28 | cond4 = data.iloc[:, 8] == "A91" 29 | num_unknown_credit_div_male = data[cond3][cond4].shape[0] 30 | 31 | cond5 = data.iloc[:, 20] == 2 32 | num_bad_outcome = data[cond5].shape[0] 33 | 34 | cond6 = data.iloc[:, 5] == "A65" 35 | cond7 = data.iloc[:, 20] == 2 36 | num_bad_outcome_unknown_credit = data[cond6][cond7].shape[0] 37 | 38 | cond8 = data.iloc[:, 8] == "A91" 39 | cond9 = data.iloc[:, 5] == "A65" 40 | cond10 = data.iloc[:, 20] == 2 41 | num_bad_outcome_unknown_credit_div_male = data[cond8][cond9][cond10].shape[0] 42 | 43 | ### test association rule 1: unknown credit -> bad outcome 44 | rule1_conf = num_bad_outcome_unknown_credit / num_unknown_credit 45 | rule2_conf = num_bad_outcome_unknown_credit_div_male / num_unknown_credit_div_male 46 | 47 | ## compute elift (ratio of confidence rules) 48 | print(rule2_conf / rule1_conf) 49 | 50 | ## now rerun for divorced females 51 | ## extra example 52 | ## not in book 53 | num_div_females = data[data.iloc[:, 8] == "A92"].shape[0] 54 | num_unknown_credit = data[data.iloc[:, 5] == "A65"].shape[0] 55 | num_unknown_credit_div_female = data[data.iloc[:, 5] == "A65"][data.iloc[:, 8] == "A92"].shape[0] 56 | num_bad_outcome = data[data.iloc[:, 20] == 2].shape[0] 57 | num_bad_outcome_unknown_credit = data[data.iloc[:, 5] == "A65"][data.iloc[:, 20] == 2].shape[0] 58 | num_bad_outcome_unknown_credit_div_female = data[data.iloc[:, 8] == "A92"][data.iloc[:, 5] == "A65"][data.iloc[:, 20] == 2].shape[0] 59 | 60 | ### test association rule 1: unknown credit -> bad outcome 61 | rule1_conf = num_bad_outcome_unknown_credit / num_unknown_credit 62 | rule2_conf = num_bad_outcome_unknown_credit_div_female / num_unknown_credit_div_female 63 | 64 | ## compute elift (ratio of confidence rules) 65 | print(rule2_conf / rule1_conf) 66 | -------------------------------------------------------------------------------- /Ch04/Ch04.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib inline\n", 10 | "\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "\n", 13 | "import numpy as np\n", 14 | "np.set_printoptions(suppress=True) \n", 15 | "\n", 16 | "from aif360.datasets import BinaryLabelDataset\n", 17 | "from aif360.datasets import AdultDataset, GermanDataset, CompasDataset\n", 18 | "from aif360.metrics import BinaryLabelDatasetMetric, ClassificationMetric\n", 19 | "from aif360.metrics.utils import compute_boolean_conditioning_vector\n", 20 | "from aif360.algorithms.preprocessing.optim_preproc_helpers.data_preproc_functions\\\n", 21 | " import load_preproc_data_adult\n", 22 | "from common_utils import compute_metrics\n", 23 | "\n", 24 | "from sklearn.linear_model import LogisticRegression\n", 25 | "from sklearn.preprocessing import StandardScaler\n", 26 | "\n", 27 | "## utility functions\n", 28 | "from common_utils import compute_metrics ## taken from AIF360 github repo\n", 29 | "\n", 30 | "## for Reweighting\n", 31 | "from aif360.algorithms.preprocessing.reweighing import Reweighing \n", 32 | "\n", 33 | "## Optimized Preprocessing\n", 34 | "from aif360.algorithms.preprocessing.optim_preproc import OptimPreproc\n", 35 | "from aif360.algorithms.preprocessing.optim_preproc_helpers.distortion_functions\\\n", 36 | " import get_distortion_adult\n", 37 | "from aif360.algorithms.preprocessing.optim_preproc_helpers.opt_tools import OptTools\n", 38 | "\n", 39 | "## Learning Fair Representations\n", 40 | "from aif360.algorithms.preprocessing.lfr import LFR" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "## Load the data" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": { 54 | "scrolled": true 55 | }, 56 | "outputs": [], 57 | "source": [ 58 | "## p 113\n", 59 | "priv_group = [{'sex': 1}]\n", 60 | "unpriv_group = [{'sex': 0}]\n", 61 | "census_data = load_preproc_data_adult(['sex']) ## utility function to collapse categories\n", 62 | " ## according to details of dataset" 63 | ] 64 | }, 65 | { 66 | "cell_type": "markdown", 67 | "metadata": {}, 68 | "source": [ 69 | "## Utility functions: splitting data, building models" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "## p 113\n", 79 | "def split_data_trn_vld_tst(data_raw):\n", 80 | " dset_raw_trn, dset_raw_vt = data_raw.split( [0.7], shuffle = True)\n", 81 | " dset_raw_vld, dset_raw_tst = dset_raw_vt.split([0.5], shuffle = True)\n", 82 | " \n", 83 | " return dset_raw_trn, dset_raw_vld, dset_raw_tst" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "## p 121\n", 93 | "def build_logit_model(dset_trn, dset_tst, privileged_groups, unprivileged_groups):\n", 94 | " \n", 95 | " scaler = StandardScaler()\n", 96 | " X_trn = scaler.fit_transform(dset_trn.features)\n", 97 | " y_trn = dset_trn.labels.ravel()\n", 98 | " w_trn = dset_trn.instance_weights.ravel()\n", 99 | " \n", 100 | " lmod = LogisticRegression()\n", 101 | " lmod.fit(X_trn, y_trn, \n", 102 | " sample_weight = w_trn)\n", 103 | "\n", 104 | " dset_tst_pred = dset_tst.copy(deepcopy=True)\n", 105 | " X_tst = scaler.transform(dset_tst_pred.features)\n", 106 | " dset_tst_pred.labels = lmod.predict(X_tst)\n", 107 | " \n", 108 | " print(\"HOMEMADE METRICS\")\n", 109 | " priv_idx = np.where(dset_tst_pred.protected_attributes.ravel() == 1.0)[0]\n", 110 | " unpriv_idx = np.where(dset_tst_pred.protected_attributes.ravel() == 0.0)[0]\n", 111 | " \n", 112 | " print(np.sum(dset_tst_pred.labels[priv_idx] == 1.0) / \n", 113 | " np.sum(dset_tst_pred.labels[priv_idx] > -1.0))\n", 114 | " print(np.sum(dset_tst_pred.labels[unpriv_idx] == 1.0) / \n", 115 | " np.sum(dset_tst_pred.labels[unpriv_idx] > -1.0))\n", 116 | " print(\"Mean difference: %0.2f\" % \n", 117 | " (np.mean(dset_tst_pred.labels[unpriv_idx]) - np.mean(dset_tst_pred.labels[priv_idx])))\n", 118 | " print(\"Disparate impact: %0.2f\" % \n", 119 | " (np.mean(dset_tst_pred.labels[unpriv_idx]) / np.mean(dset_tst_pred.labels[priv_idx])))\n", 120 | "\n", 121 | " \n", 122 | " metric_tst = BinaryLabelDatasetMetric(dset_tst_pred, unprivileged_groups, privileged_groups)\n", 123 | " print(\"PREROLLED METRICS\")\n", 124 | " print(metric_tst.num_positives(privileged = True) / metric_tst.num_instances(privileged = True))\n", 125 | " print(metric_tst.num_positives(privileged = False) / metric_tst.num_instances(privileged = False))\n", 126 | " print(\"Disparate impact is %0.2f (closer to 1 is better)\" % metric_tst.disparate_impact())\n", 127 | " print(\"Mean difference is %0.2f (closer to 0 is better)\" % metric_tst.mean_difference())\n", 128 | "\n", 129 | " return lmod, dset_tst_pred, metric_tst " 130 | ] 131 | }, 132 | { 133 | "cell_type": "markdown", 134 | "metadata": {}, 135 | "source": [ 136 | "## Examine raw dataset and a logistic regression" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "## p 113\n", 146 | "# reproducibility\n", 147 | "np.random.seed(316)\n", 148 | "\n", 149 | "# split into train, validate, test\n", 150 | "dset_raw_trn, dset_raw_vld, dset_raw_tst = split_data_trn_vld_tst(census_data)" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": null, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "## p 113\n", 160 | "## calculate the metric of interest\n", 161 | "metric_raw_trn = BinaryLabelDatasetMetric(dset_raw_trn, \n", 162 | " unprivileged_groups = unpriv_group,\n", 163 | " privileged_groups = priv_group)\n", 164 | "\n", 165 | "print(\"Disparate impact is %0.2f (closed to 1 is better)\" % metric_raw_trn.disparate_impact())\n", 166 | "print(\"Mean difference is %0.2f (closer to 0 is better)\" % metric_raw_trn.mean_difference())" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "### taking a look at coefficient values" 174 | ] 175 | }, 176 | { 177 | "cell_type": "code", 178 | "execution_count": null, 179 | "metadata": {}, 180 | "outputs": [], 181 | "source": [ 182 | "dset_raw_trn.feature_names" 183 | ] 184 | }, 185 | { 186 | "cell_type": "code", 187 | "execution_count": null, 188 | "metadata": {}, 189 | "outputs": [], 190 | "source": [ 191 | "# reproducibility\n", 192 | "np.random.seed(316)\n", 193 | "\n", 194 | "## raw training data\n", 195 | "raw_lmod, raw_pred, raw_metric = build_logit_model(dset_raw_trn, dset_raw_tst, priv_group, unpriv_group)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "## plot coefficients for a quick visual summary of values\n", 205 | "print(dset_raw_trn.feature_names[:9])\n", 206 | "plt.plot(raw_lmod.coef_.ravel()[:9])" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "metadata": {}, 213 | "outputs": [], 214 | "source": [ 215 | "print(dset_raw_trn.feature_names[9:])\n", 216 | "plt.plot(raw_lmod.coef_.ravel()[9:])" 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "metadata": {}, 222 | "source": [ 223 | "## Suppression\n", 224 | "### p 115" 225 | ] 226 | }, 227 | { 228 | "cell_type": "code", 229 | "execution_count": null, 230 | "metadata": {}, 231 | "outputs": [], 232 | "source": [ 233 | "def build_logit_model_suppression(dset_trn, \n", 234 | " dset_tst, \n", 235 | " privileged_groups, \n", 236 | " unprivileged_groups):\n", 237 | " \n", 238 | " scaler = StandardScaler()\n", 239 | " X_trn = scaler.fit_transform(dset_trn.features[:, 2:])\n", 240 | " y_trn = dset_trn.labels.ravel()\n", 241 | " w_trn = dset_trn.instance_weights.ravel()\n", 242 | " \n", 243 | " lmod = LogisticRegression()\n", 244 | " lmod.fit(X_trn, y_trn, \n", 245 | " sample_weight = w_trn)\n", 246 | "\n", 247 | " dset_tst_pred = dset_tst.copy(deepcopy=True)\n", 248 | " X_tst = scaler.transform(dset_tst_pred.features[:, 2:])\n", 249 | " dset_tst_pred.labels = lmod.predict(X_tst)\n", 250 | "\n", 251 | " metric_tst = BinaryLabelDatasetMetric(dset_tst_pred,\n", 252 | " unprivileged_groups, \n", 253 | " privileged_groups)\n", 254 | " print(\"HOMEMADE METRICS\")\n", 255 | " priv_idx = np.where(dset_tst_pred.protected_attributes.ravel() == 1.0)[0]\n", 256 | " unpriv_idx = np.where(dset_tst_pred.protected_attributes.ravel() == 0.0)[0]\n", 257 | " print(np.sum(dset_tst_pred.labels[priv_idx] == 1.0) / np.sum(dset_tst_pred.labels[priv_idx] > -1.0))\n", 258 | " print(np.sum(dset_tst_pred.labels[unpriv_idx] == 1.0) / np.sum(dset_tst_pred.labels[unpriv_idx] > -1.0))\n", 259 | " print(\"Mean difference: %0.2f\" % (np.mean(dset_tst_pred.labels[unpriv_idx]) - np.mean(dset_tst_pred.labels[priv_idx])))\n", 260 | " print(\"Disparate impact: %0.2f\" % (np.mean(dset_tst_pred.labels[unpriv_idx]) / np.mean(dset_tst_pred.labels[priv_idx])))\n", 261 | "\n", 262 | " \n", 263 | " metric_tst = BinaryLabelDatasetMetric(dset_tst_pred,\n", 264 | " unprivileged_groups, privileged_groups)\n", 265 | " print(\"PREROLLED METRICS\")\n", 266 | " print(metric_tst.num_positives(privileged = True) / metric_tst.num_instances(privileged = True))\n", 267 | " print(metric_tst.num_positives(privileged = False) / metric_tst.num_instances(privileged = False))\n", 268 | " print(\"Disparate impact is %0.2f (closer to 1 is better)\" % metric_tst.disparate_impact())\n", 269 | " print(\"Mean difference is %0.2f (closer to 0 is better)\" % metric_tst.mean_difference())\n", 270 | " \n", 271 | " return lmod, dset_tst_pred, metric_tst " 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "# reproducibility\n", 281 | "np.random.seed(316)\n", 282 | "\n", 283 | "sup_lmod, sup_pred, sup_metric = build_logit_model_suppression(dset_raw_trn, dset_raw_tst, priv_group, unpriv_group)" 284 | ] 285 | }, 286 | { 287 | "cell_type": "markdown", 288 | "metadata": {}, 289 | "source": [ 290 | "### Suppression turns out not to be that bad, in the sense that the mean difference is reduced compared to the baseline model presented above and disparate impact is closer to 1. This result is referenced on p 117 but not adequately discussed there." 291 | ] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "metadata": {}, 296 | "source": [ 297 | "# Preprocessing via reweighting\n", 298 | "### p 117" 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": null, 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "# reproducibility\n", 308 | "np.random.seed(316)" 309 | ] 310 | }, 311 | { 312 | "cell_type": "code", 313 | "execution_count": null, 314 | "metadata": {}, 315 | "outputs": [], 316 | "source": [ 317 | "## p 120\n", 318 | "## transform the data set\n", 319 | "RW = Reweighing(unprivileged_groups = unpriv_group,\n", 320 | " privileged_groups = priv_group)\n", 321 | "RW.fit(dset_raw_trn)\n", 322 | "dset_rewgt_trn = RW.transform(dset_raw_trn)\n", 323 | "\n", 324 | "## calculate the metric of interest\n", 325 | "metric_rewgt_trn = BinaryLabelDatasetMetric(dset_rewgt_trn, \n", 326 | " unprivileged_groups = unpriv_group,\n", 327 | " privileged_groups = priv_group)\n", 328 | "print(\"Difference in mean outcomes = %f\" %\n", 329 | " metric_rewgt_trn.mean_difference())\n", 330 | "print(\"Disparate impact = %f\" %\n", 331 | " metric_rewgt_trn.disparate_impact())" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": null, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "## 4 weights resulte because there are 4 types\n", 341 | "## privileged/unprivileged x positive/negative outcome (2 x 2 = 4)\n", 342 | "set(dset_rewgt_trn.instance_weights)" 343 | ] 344 | }, 345 | { 346 | "cell_type": "markdown", 347 | "metadata": {}, 348 | "source": [ 349 | "### Now that we have reweighted the data, fit a logistic regression with the reweighted dataset" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": null, 355 | "metadata": {}, 356 | "outputs": [], 357 | "source": [ 358 | "# reproducibility\n", 359 | "np.random.seed(316)\n", 360 | "\n", 361 | "## fairness preprocessed data\n", 362 | "rewgt_lmod, rewgt_pred, rewgt_metric = build_logit_model(dset_rewgt_trn, dset_raw_tst, priv_group, unpriv_group)" 363 | ] 364 | }, 365 | { 366 | "cell_type": "markdown", 367 | "metadata": {}, 368 | "source": [ 369 | "### Dpending on your fairness metric, this does slightly better than suppression as indicated by disparate impact. note that the disparate impact however would still meet the criterion of presumptive disparate impact under the EEOC's 4/5 rule." 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "## Quick model comparison" 377 | ] 378 | }, 379 | { 380 | "cell_type": "code", 381 | "execution_count": null, 382 | "metadata": {}, 383 | "outputs": [], 384 | "source": [ 385 | "from scipy.stats import pearsonr\n", 386 | "pearsonr(rewgt_lmod.coef_[0], raw_lmod.coef_[0])" 387 | ] 388 | }, 389 | { 390 | "cell_type": "markdown", 391 | "metadata": {}, 392 | "source": [ 393 | "#### We see a difference in how the gender variable is weighted" 394 | ] 395 | }, 396 | { 397 | "cell_type": "code", 398 | "execution_count": null, 399 | "metadata": {}, 400 | "outputs": [], 401 | "source": [ 402 | "## Gender is the second coefficient and here we plot the difference\n", 403 | "## between the coefficients in the reweighted as compared to raw/naive model\n", 404 | "plt.plot(rewgt_lmod.coef_[0] - raw_lmod.coef_[0])" 405 | ] 406 | }, 407 | { 408 | "cell_type": "code", 409 | "execution_count": null, 410 | "metadata": {}, 411 | "outputs": [], 412 | "source": [ 413 | "## a scatetr plot shows that the coefficients are mostly quite close\n", 414 | "## in value in the 2 models\n", 415 | "plt.scatter(rewgt_lmod.coef_[0], raw_lmod.coef_[0])" 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": null, 421 | "metadata": {}, 422 | "outputs": [], 423 | "source": [ 424 | "## importantly we also see that the reweighting doesn't distort the values of the other coefficients\n", 425 | "## you should be asking whether this would be true if other variables were highly correlated with the\n", 426 | "## protected group. we will study this later in the book" 427 | ] 428 | }, 429 | { 430 | "cell_type": "markdown", 431 | "metadata": {}, 432 | "source": [ 433 | "### Let's look at how the models treat females vs. males in the case of the model trained on the raw data and on the preprocessed data" 434 | ] 435 | }, 436 | { 437 | "cell_type": "code", 438 | "execution_count": null, 439 | "metadata": {}, 440 | "outputs": [], 441 | "source": [ 442 | "fem_idx = np.where(dset_raw_tst.features[:, 1] == 0)[0][5]" 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": null, 448 | "metadata": {}, 449 | "outputs": [], 450 | "source": [ 451 | "fem_test_case = np.copy(dset_raw_tst.features[fem_idx:(fem_idx + 1)]) ## funny slicing to preserve 2d " 452 | ] 453 | }, 454 | { 455 | "cell_type": "code", 456 | "execution_count": null, 457 | "metadata": {}, 458 | "outputs": [], 459 | "source": [ 460 | "fem_test_case" 461 | ] 462 | }, 463 | { 464 | "cell_type": "code", 465 | "execution_count": null, 466 | "metadata": {}, 467 | "outputs": [], 468 | "source": [ 469 | "rewgt_lmod.predict_proba(fem_test_case)" 470 | ] 471 | }, 472 | { 473 | "cell_type": "code", 474 | "execution_count": null, 475 | "metadata": {}, 476 | "outputs": [], 477 | "source": [ 478 | "raw_lmod.predict_proba(fem_test_case)" 479 | ] 480 | }, 481 | { 482 | "cell_type": "code", 483 | "execution_count": null, 484 | "metadata": {}, 485 | "outputs": [], 486 | "source": [ 487 | "fake_male_test_case = np.copy(fem_test_case)\n", 488 | "fake_male_test_case[0, 1] = 1.0\n", 489 | "fake_male_test_case" 490 | ] 491 | }, 492 | { 493 | "cell_type": "code", 494 | "execution_count": null, 495 | "metadata": {}, 496 | "outputs": [], 497 | "source": [ 498 | "rewgt_lmod.predict_proba(fake_male_test_case) - rewgt_lmod.predict_proba(fem_test_case)" 499 | ] 500 | }, 501 | { 502 | "cell_type": "code", 503 | "execution_count": null, 504 | "metadata": {}, 505 | "outputs": [], 506 | "source": [ 507 | "raw_lmod.predict_proba(fake_male_test_case) - raw_lmod.predict_proba(fem_test_case)" 508 | ] 509 | }, 510 | { 511 | "cell_type": "code", 512 | "execution_count": null, 513 | "metadata": {}, 514 | "outputs": [], 515 | "source": [ 516 | "raw_lmod.classes_" 517 | ] 518 | }, 519 | { 520 | "cell_type": "markdown", 521 | "metadata": {}, 522 | "source": [ 523 | "### So probability of being successful goes up by almost 10% just for being male in the raw case even with all else being equal. this looks like an unfair/illegal model (but then again unlikely) such a model would get deployed in a legally regulated area" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "metadata": {}, 529 | "source": [ 530 | "# Learning fair representations\n", 531 | "### p 123" 532 | ] 533 | }, 534 | { 535 | "cell_type": "code", 536 | "execution_count": null, 537 | "metadata": { 538 | "scrolled": true 539 | }, 540 | "outputs": [], 541 | "source": [ 542 | "# reproducibility\n", 543 | "np.random.seed(316)\n", 544 | "\n", 545 | "TR = LFR(unprivileged_groups = unpriv_group, \n", 546 | " privileged_groups = priv_group)\n", 547 | "TR = TR.fit(dset_raw_trn)" 548 | ] 549 | }, 550 | { 551 | "cell_type": "code", 552 | "execution_count": null, 553 | "metadata": {}, 554 | "outputs": [], 555 | "source": [ 556 | "dset_lfr_trn = TR.transform(dset_raw_trn, thresh = 0.5)\n", 557 | "dset_lfr_trn = dset_raw_trn.align_datasets(dset_lfr_trn)\n", 558 | "\n", 559 | "dset_lfr_tst = TR.transform(dset_raw_tst, thresh = 0.5)\n", 560 | "dset_lfr_tst = dset_raw_trn.align_datasets(dset_lfr_tst)" 561 | ] 562 | }, 563 | { 564 | "cell_type": "code", 565 | "execution_count": null, 566 | "metadata": {}, 567 | "outputs": [], 568 | "source": [ 569 | "metric_op = BinaryLabelDatasetMetric(dset_lfr_trn, \n", 570 | " unprivileged_groups = unpriv_group,\n", 571 | " privileged_groups = priv_group)\n", 572 | "print(\"Mean difference: %0.2f\" % metric_op.mean_difference())\n", 573 | "print(\"Disparate impact: %0.2f\" % metric_op.disparate_impact())\n", 574 | "print(\"Size %d\" % dset_lfr_trn.features.shape[0])" 575 | ] 576 | }, 577 | { 578 | "cell_type": "code", 579 | "execution_count": null, 580 | "metadata": {}, 581 | "outputs": [], 582 | "source": [ 583 | "metric_op_tst = BinaryLabelDatasetMetric(dset_lfr_tst, \n", 584 | " unprivileged_groups = unpriv_group,\n", 585 | " privileged_groups = priv_group)\n", 586 | "print(\"Mean difference: %0.2f\" % metric_op_tst.mean_difference())\n", 587 | "print(\"Disparate impact: %0.2f\" % metric_op_tst.disparate_impact())\n", 588 | "print(\"Size %d\" % dset_lfr_tst.features.shape[0])" 589 | ] 590 | }, 591 | { 592 | "cell_type": "code", 593 | "execution_count": null, 594 | "metadata": { 595 | "scrolled": true 596 | }, 597 | "outputs": [], 598 | "source": [ 599 | "# reproducibility\n", 600 | "np.random.seed(316)\n", 601 | "\n", 602 | "## fairness preprocessed data\n", 603 | "lfr_lmod1, lfr_pred, lfr_metric = build_logit_model(dset_lfr_trn, dset_raw_tst, priv_group, unpriv_group)" 604 | ] 605 | }, 606 | { 607 | "cell_type": "markdown", 608 | "metadata": {}, 609 | "source": [ 610 | "## Tuning additional hyperparameters\n", 611 | "#### not covered in book" 612 | ] 613 | }, 614 | { 615 | "cell_type": "code", 616 | "execution_count": null, 617 | "metadata": {}, 618 | "outputs": [], 619 | "source": [ 620 | "thresholds = [0.3, 0.4, 0.5, 0.6, 0.7]\n", 621 | "for thresh in thresholds:\n", 622 | " \n", 623 | " # Transform training data and align features\n", 624 | " dset_lfr_trn = TR.transform(dset_raw_trn, threshold = thresh)\n", 625 | "\n", 626 | " metric_lfr_trn = BinaryLabelDatasetMetric(dset_lfr_trn, \n", 627 | " unprivileged_groups = unpriv_group,\n", 628 | " privileged_groups = priv_group)\n", 629 | "\n", 630 | " unpriv_idx = np.where(dset_lfr_trn.protected_attributes.ravel() == 0.0)[0]\n", 631 | " print(\"Pct of positive outcomes for unpriv group: %0.3f\" % \n", 632 | " (np.where(dset_lfr_trn.labels[unpriv_idx] == 1.0)[0].shape[0] / unpriv_idx.shape[0]))\n", 633 | " \n", 634 | " priv_idx = np.where(dset_lfr_trn.protected_attributes.ravel() == 1.0)[0]\n", 635 | " print(\"Pct of positive outcomes for priv group: %0.3f\\n\" % \n", 636 | " (np.where(dset_lfr_trn.labels[priv_idx] == 1.0)[0].shape[0] / priv_idx.shape[0]))" 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "metadata": {}, 642 | "source": [ 643 | "##### Preprocessing does not remove the potential for in-processing or post-processing interventions. More on this in the next two chapters." 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": null, 649 | "metadata": {}, 650 | "outputs": [], 651 | "source": [ 652 | "#### For example, consider whether to retain the logistic regression classification threshold at 0.5\n", 653 | "#### even after the data has been transformed. Perhaps it should be shifted to increase accuracy \n", 654 | "#### in recognition that some accuracy has been sacrificed in transforming the data, maybe it can be\n", 655 | "#### recovered in optimizing the threshold as a hyperparameter." 656 | ] 657 | }, 658 | { 659 | "cell_type": "code", 660 | "execution_count": null, 661 | "metadata": {}, 662 | "outputs": [], 663 | "source": [ 664 | "#### Also we have only compared along group fairness metrics. We should also consider incorporating measures of\n", 665 | "#### individual fairness. However this would be a poor example to consider individual fairness\n", 666 | "#### given that the categories are so broad (age crossed with education). The less specific the data\n", 667 | "#### the less compelling the interest in individual fairness." 668 | ] 669 | }, 670 | { 671 | "cell_type": "code", 672 | "execution_count": null, 673 | "metadata": {}, 674 | "outputs": [], 675 | "source": [] 676 | }, 677 | { 678 | "cell_type": "markdown", 679 | "metadata": {}, 680 | "source": [ 681 | "# Preprocess by learning an optimal representation\n", 682 | "### p 127" 683 | ] 684 | }, 685 | { 686 | "cell_type": "code", 687 | "execution_count": null, 688 | "metadata": {}, 689 | "outputs": [], 690 | "source": [ 691 | "## p 129\n", 692 | "# reproducibility\n", 693 | "np.random.seed(316)\n", 694 | "\n", 695 | "optim_options = {\n", 696 | " \"distortion_fun\": get_distortion_adult,\n", 697 | " \"epsilon\": 0.05,\n", 698 | " \"clist\": [0.99, 1.99, 2.99],\n", 699 | " \"dlist\": [.1, 0.05, 0]\n", 700 | "}\n", 701 | "\n", 702 | "OP = OptimPreproc(OptTools, optim_options)\n", 703 | "\n", 704 | "OP = OP.fit(dset_raw_trn)\n", 705 | "\n", 706 | "## p 131\n", 707 | "# Transform training data and align features\n", 708 | "dset_op_trn = OP.transform(dset_raw_trn, transform_Y=True)\n", 709 | "dset_op_trn = dset_raw_trn.align_datasets(dset_op_trn )" 710 | ] 711 | }, 712 | { 713 | "cell_type": "code", 714 | "execution_count": null, 715 | "metadata": {}, 716 | "outputs": [], 717 | "source": [ 718 | "metric_op = BinaryLabelDatasetMetric(dset_op_trn, \n", 719 | " unprivileged_groups = unpriv_group,\n", 720 | " privileged_groups = priv_group)\n", 721 | "print(\"Mean difference: %0.2f\" % metric_op.mean_difference())\n", 722 | "print(\"Disparate impact: %0.2f\" % metric_op.disparate_impact())" 723 | ] 724 | }, 725 | { 726 | "cell_type": "code", 727 | "execution_count": null, 728 | "metadata": {}, 729 | "outputs": [], 730 | "source": [ 731 | "## Transform testing data\n", 732 | "dset_op_tst = OP.transform(dset_raw_tst, transform_Y=True)\n", 733 | "dset_op_tst = dset_raw_trn.align_datasets(dset_op_tst)" 734 | ] 735 | }, 736 | { 737 | "cell_type": "code", 738 | "execution_count": null, 739 | "metadata": {}, 740 | "outputs": [], 741 | "source": [ 742 | "# reproducibility\n", 743 | "np.random.seed(316)\n", 744 | "\n", 745 | "## fairness preprocessed data\n", 746 | "op_lmod, op_pred, op_metric = build_logit_model(dset_op_trn, dset_op_tst, priv_group, unpriv_group)" 747 | ] 748 | }, 749 | { 750 | "cell_type": "markdown", 751 | "metadata": {}, 752 | "source": [ 753 | "#### We can define the distortion function/metric differently e.g. based on domain knowledge" 754 | ] 755 | }, 756 | { 757 | "cell_type": "code", 758 | "execution_count": null, 759 | "metadata": {}, 760 | "outputs": [], 761 | "source": [ 762 | "## p 132\n", 763 | "def get_distortion_adult2(vold, vnew):\n", 764 | " # Define local functions to adjust education and age\n", 765 | " def adjustEdu(v):\n", 766 | " if v == '>12':\n", 767 | " return 13\n", 768 | " elif v == '<6':\n", 769 | " return 5\n", 770 | " else:\n", 771 | " return int(v)\n", 772 | "\n", 773 | " def adjustAge(a):\n", 774 | " if a == '>=70':\n", 775 | " return 70.0\n", 776 | " else:\n", 777 | " return float(a)\n", 778 | "\n", 779 | " def adjustInc(a):\n", 780 | " if a == \"<=50K\":\n", 781 | " return 0\n", 782 | " elif a == \">50K\":\n", 783 | " return 1\n", 784 | " else:\n", 785 | " return int(a)\n", 786 | "\n", 787 | " # value that will be returned for events that should not occur\n", 788 | " bad_val = 3.0\n", 789 | "\n", 790 | " # Adjust education years\n", 791 | " eOld = adjustEdu(vold['Education Years'])\n", 792 | " eNew = adjustEdu(vnew['Education Years'])\n", 793 | "\n", 794 | " # Education cannot be lowered or increased in more than 1 year\n", 795 | " #########################################################################\n", 796 | " if (eNew < eOld - 1) | (eNew > eOld+1): ## CHANGED THIS TO LESS STRINGENT\n", 797 | " return bad_val\n", 798 | " #########################################################################\n", 799 | " # adjust age\n", 800 | " aOld = adjustAge(vold['Age (decade)'])\n", 801 | " aNew = adjustAge(vnew['Age (decade)'])\n", 802 | "\n", 803 | " # Age cannot be increased or decreased in more than a decade\n", 804 | " #########################################################################\n", 805 | " if np.abs(aOld-aNew) > 15.0: ## CHANGED THIS TO LESS STRINGENT\n", 806 | " return bad_val\n", 807 | " #########################################################################\n", 808 | "\n", 809 | " # Penalty of 2 if age is decreased or increased\n", 810 | " if np.abs(aOld-aNew) > 0:\n", 811 | " return 2.0\n", 812 | "\n", 813 | " # Adjust income\n", 814 | " incOld = adjustInc(vold['Income Binary'])\n", 815 | " incNew = adjustInc(vnew['Income Binary'])\n", 816 | "\n", 817 | " # final penalty according to income\n", 818 | " if incOld > incNew:\n", 819 | " return 1.0\n", 820 | " else:\n", 821 | " return 0.0\n", 822 | " \n", 823 | "# reproducibility\n", 824 | "np.random.seed(316)\n", 825 | "\n", 826 | "optim_options2 = {\n", 827 | " \"distortion_fun\": get_distortion_adult2,\n", 828 | " \"epsilon\": 0.05,\n", 829 | " \"clist\": [0.99, 1.99, 2.99],\n", 830 | " \"dlist\": [.1, 0.05, 0]\n", 831 | "}\n", 832 | "\n", 833 | "OP2 = OptimPreproc(OptTools, optim_options2)\n", 834 | "OP2 = OP2.fit(dset_raw_trn)\n", 835 | "\n", 836 | "# Transform training data and align features\n", 837 | "dset_op_trn2 = OP2.transform(dset_raw_trn, transform_Y=True)\n", 838 | "dset_op_trn2 = dset_raw_trn.align_datasets(dset_op_trn2)\n", 839 | "\n", 840 | "metric_op2 = BinaryLabelDatasetMetric(dset_op_trn2, \n", 841 | " unprivileged_groups = unpriv_group,\n", 842 | " privileged_groups = priv_group)\n", 843 | "print(\"Mean difference: %0.2f\" % metric_op2.mean_difference())\n", 844 | "print(\"Disparate impact: %0.2f\" % metric_op2.disparate_impact())\n", 845 | "\n", 846 | "# reproducibility\n", 847 | "np.random.seed(316)\n", 848 | "\n", 849 | "## fairness preprocessed data\n", 850 | "op_lmod2, op_pred2, op_metric2 = build_logit_model(dset_op_trn2, dset_raw_tst, priv_group, unpriv_group)" 851 | ] 852 | }, 853 | { 854 | "cell_type": "markdown", 855 | "metadata": {}, 856 | "source": [ 857 | "#### Alternately can adjust the tolerance by upping the probability limits for the distortion" 858 | ] 859 | }, 860 | { 861 | "cell_type": "code", 862 | "execution_count": null, 863 | "metadata": {}, 864 | "outputs": [], 865 | "source": [ 866 | "# reproducibility\n", 867 | "np.random.seed(316)\n", 868 | "\n", 869 | "optim_options3 = {\n", 870 | " \"distortion_fun\": get_distortion_adult,\n", 871 | " \"epsilon\": 0.05,\n", 872 | " \"clist\": [0.99, 1.99, 2.99],\n", 873 | " \"dlist\": [.15, 0.10, 0.05]\n", 874 | "}\n", 875 | "\n", 876 | "OP3 = OptimPreproc(OptTools, optim_options)\n", 877 | "\n", 878 | "OP3 = OP.fit(dset_raw_trn)\n", 879 | "\n", 880 | "# Transform training data and align features\n", 881 | "dset_op_trn3 = OP3.transform(dset_raw_trn, transform_Y=True)\n", 882 | "dset_op_trn3 = dset_raw_trn.align_datasets(dset_op_trn3)\n", 883 | "\n", 884 | "metric_op3 = BinaryLabelDatasetMetric(dset_op_trn3, \n", 885 | " unprivileged_groups = unpriv_group,\n", 886 | " privileged_groups = priv_group)\n", 887 | "print(\"Mean difference: %0.2f\" % metric_op3.mean_difference())\n", 888 | "print(\"Disparate impact: %0.2f\" % metric_op3.disparate_impact())\n", 889 | "\n", 890 | "## Transform testing data\n", 891 | "dset_op_tst3 = OP.transform(dset_raw_tst, transform_Y=True)\n", 892 | "dset_op_tst3 = dset_raw_trn.align_datasets(dset_op_tst3)\n", 893 | "\n", 894 | "# reproducibility\n", 895 | "np.random.seed(316)\n", 896 | "\n", 897 | "## fairness preprocessed data\n", 898 | "op_lmod3, op_pred3, op_metric3 = build_logit_model(dset_op_trn3, dset_op_tst3, priv_group, unpriv_group)" 899 | ] 900 | }, 901 | { 902 | "cell_type": "code", 903 | "execution_count": null, 904 | "metadata": {}, 905 | "outputs": [], 906 | "source": [ 907 | "## note we don't need to transform the test data to enjoy the benefits of the transformation\n", 908 | "op_lmod4, op_pred4, op_metric4 = build_logit_model(dset_op_trn3, dset_raw_tst, priv_group, unpriv_group)" 909 | ] 910 | }, 911 | { 912 | "cell_type": "markdown", 913 | "metadata": {}, 914 | "source": [ 915 | "#### Question for ourselves: When might we find larger deviations/distortions acceptable or less acceptable when considering a potential accuracy/fairness trade off (which need not necessarily exist)?" 916 | ] 917 | }, 918 | { 919 | "cell_type": "code", 920 | "execution_count": null, 921 | "metadata": {}, 922 | "outputs": [], 923 | "source": [ 924 | "## when accuracy is not of prime importance (e.g. low stakes consumer decisions)\n", 925 | "## don't do that on health decisions!\n", 926 | "## or when we think the data is probably noisy anyway so that we may not be adding much\n", 927 | "## noise compared to original data collection" 928 | ] 929 | } 930 | ], 931 | "metadata": { 932 | "kernelspec": { 933 | "display_name": "Python 3", 934 | "language": "python", 935 | "name": "python3" 936 | }, 937 | "language_info": { 938 | "codemirror_mode": { 939 | "name": "ipython", 940 | "version": 3 941 | }, 942 | "file_extension": ".py", 943 | "mimetype": "text/x-python", 944 | "name": "python", 945 | "nbconvert_exporter": "python", 946 | "pygments_lexer": "ipython3", 947 | "version": "3.7.5" 948 | } 949 | }, 950 | "nbformat": 4, 951 | "nbformat_minor": 2 952 | } 953 | -------------------------------------------------------------------------------- /Ch05/Ch05.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "np.set_printoptions(suppress=True) \n", 11 | "\n", 12 | "# Datasets\n", 13 | "from aif360.datasets import MEPSDataset19\n", 14 | "### fyi: there are also alternate MEPSDataset data sets to look into\n", 15 | "\n", 16 | "from sklearn.linear_model import LogisticRegression\n", 17 | "from sklearn.preprocessing import StandardScaler\n", 18 | "\n", 19 | "## utility functions\n", 20 | "from common_utils import compute_metrics ## taken from AIF360 github repo\n", 21 | "from aif360.metrics import BinaryLabelDatasetMetric, ClassificationMetric\n", 22 | "\n", 23 | "## In-processing prejudice remover\n", 24 | "from aif360.algorithms.inprocessing import PrejudiceRemover\n", 25 | "\n", 26 | "## Adversarial debiasing\n", 27 | "from aif360.algorithms.inprocessing.adversarial_debiasing import AdversarialDebiasing\n", 28 | "import tensorflow as tf" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## Load a dataset regarding healthcare allocation\n", 36 | "### p 136" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "np.random.seed(132)\n", 46 | "def split_data_trn_vld_tst(data_raw):\n", 47 | " dset_raw_trn, dset_raw_vt = data_raw.split([0.7], shuffle=True)\n", 48 | " dset_raw_vld, dset_raw_tst = dset_raw_vt.split([0.5], shuffle=True)\n", 49 | " \n", 50 | " return dset_raw_trn, dset_raw_vld, dset_raw_tst" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": null, 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "## p 137\n", 60 | "med_data = MEPSDataset19()" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "dset_raw_trn, dset_raw_vld, dset_raw_tst = split_data_trn_vld_tst(med_data)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "dset_raw_trn.protected_attribute_names" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "priv_group = [{'RACE': 1}]\n", 88 | "unpriv_group = [{'RACE': 0}]" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "dset_raw_trn" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "dset_raw_trn.label_names" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": null, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "metric = BinaryLabelDatasetMetric(dset_raw_trn, \n", 116 | " unprivileged_groups = unpriv_group, \n", 117 | " privileged_groups = priv_group)" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "metric.disparate_impact()" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "metric.consistency()" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "metric.mean_difference()" 145 | ] 146 | }, 147 | { 148 | "cell_type": "markdown", 149 | "metadata": {}, 150 | "source": [ 151 | "## Prejudice Remover" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "## p 143\n", 161 | "def test_eta_bal_acc(ETA, dset_raw_trn, dset_raw_vld, dset_raw_tst):\n", 162 | " pr = PrejudiceRemover(sensitive_attr = 'RACE', eta = ETA)\n", 163 | " scaler = StandardScaler()\n", 164 | "\n", 165 | " dset_scaled_trn = dset_raw_trn.copy()\n", 166 | " dset_scaled_trn.features = scaler.fit_transform(dset_scaled_trn.features)\n", 167 | "\n", 168 | " pr_fitted = pr.fit(dset_scaled_trn)\n", 169 | " \n", 170 | " accs = []\n", 171 | " thresholds = np.linspace(0.01, 0.50, 10)\n", 172 | "\n", 173 | " dset_val = dset_raw_vld.copy()\n", 174 | " dset_val.features = scaler.transform(dset_val.features)\n", 175 | "\n", 176 | " ##################### STEP 1 TRAINING WITH INPROCESSING #####################\n", 177 | " pr_pred_prob = pr_fitted.predict(dset_val).scores\n", 178 | "\n", 179 | " ##################### STEP 2 PICKING THRESHOLD WITH VALIDATION DATA #####################\n", 180 | " for threshold in thresholds:\n", 181 | " dset_val_pred = dset_val.copy()\n", 182 | " dset_val_pred.labels = (pr_pred_prob[:, 0] > threshold).astype(np.float64)\n", 183 | "\n", 184 | " metric = ClassificationMetric(\n", 185 | " dset_val, dset_val_pred,\n", 186 | " unprivileged_groups = unpriv_group,\n", 187 | " privileged_groups=priv_group)\n", 188 | " accs.append((metric.true_positive_rate() + metric.true_negative_rate()) / 2)\n", 189 | "\n", 190 | "\n", 191 | " pr_val_best_idx = np.argmax(accs)\n", 192 | " best_threshold = thresholds[pr_val_best_idx]\n", 193 | " \n", 194 | " ##################### STEP 3 TEST DATA #####################\n", 195 | " dset_tst = dset_raw_tst.copy()\n", 196 | " dset_tst.features = scaler.transform(dset_tst.features)\n", 197 | "\n", 198 | " pr_pred_prob = pr_fitted.predict(dset_tst).scores\n", 199 | "\n", 200 | "\n", 201 | " dset_tst_pred = dset_tst.copy()\n", 202 | " dset_tst_pred.labels = (pr_pred_prob[:, 0] > best_threshold).astype(np.float64)\n", 203 | "\n", 204 | " metric = ClassificationMetric(\n", 205 | " dset_tst, dset_tst_pred,\n", 206 | " unprivileged_groups = unpriv_group,\n", 207 | " privileged_groups = priv_group)\n", 208 | " test_acc = (metric.true_positive_rate() + metric.true_negative_rate()) / 2 ## no built in balanced error rate\n", 209 | " test_disp_impact = metric.disparate_impact()\n", 210 | "\n", 211 | " print(\"Testing accuracy with ETA %0.2f = %0.2f\\n Disparate impact %0.2f\" % (ETA, test_acc, test_disp_impact))\n", 212 | " return (test_acc, test_disp_impact)" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": null, 218 | "metadata": {}, 219 | "outputs": [], 220 | "source": [ 221 | "## p 144\n", 222 | "test_eta_bal_acc(5.0, dset_raw_trn, dset_raw_vld, dset_raw_tst)" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "test_eta_bal_acc(50.0, dset_raw_trn, dset_raw_vld, dset_raw_tst)" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "test_eta_bal_acc(20.0, dset_raw_trn, dset_raw_vld, dset_raw_tst)" 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "metadata": {}, 246 | "source": [ 247 | "## Adversarial debiasing\n", 248 | "### p 145" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "## p 148\n", 258 | "tf.reset_default_graph()\n", 259 | "sess = tf.Session()\n", 260 | " \n", 261 | "kwargs = {'privileged_groups' : priv_group,\n", 262 | " 'unprivileged_groups' : unpriv_group,\n", 263 | " 'scope_name' : 'debiased_classifier', \n", 264 | " 'debias' : True,\n", 265 | " 'sess' : sess,\n", 266 | " 'adversary_loss_weight' : 0.5,\n", 267 | " 'num_epochs' : 2, \n", 268 | " 'batch_size' : 128, \n", 269 | " 'classifier_num_hidden_units': 200, \n", 270 | " 'debias' : False,\n", 271 | " 'seed' : 117\n", 272 | " }\n", 273 | "\n", 274 | "# Learn parameters with debias set to True\n", 275 | "debiased_model = AdversarialDebiasing(**kwargs) \n", 276 | "\n", 277 | "## p 149\n", 278 | "scaler = StandardScaler()\n", 279 | "\n", 280 | "dset_scaled_trn = dset_raw_trn.copy()\n", 281 | "dset_scaled_trn.features = scaler.fit_transform(dset_scaled_trn.features)\n", 282 | "\n", 283 | "debiased_model.fit(dset_scaled_trn)\n", 284 | "\n", 285 | "dset_tst = dset_raw_tst.copy()\n", 286 | "dset_tst.features = scaler.transform(dset_tst.features)\n", 287 | "\n", 288 | "thresholds = np.linspace(0.2, 0.60, 5)\n", 289 | "\n", 290 | "for thresh in thresholds:\n", 291 | " dset_tst_pred = dset_tst.copy()\n", 292 | " dset_tst_pred.labels = debiased_model.predict(dset_tst).scores > thresh\n", 293 | " print(np.bincount(dset_tst_pred.labels[:, 0].astype('int')))\n", 294 | " \n", 295 | " adv_deb_metric = ClassificationMetric(\n", 296 | " dset_tst, dset_tst_pred,\n", 297 | " unprivileged_groups = unpriv_group,\n", 298 | " privileged_groups = priv_group)\n", 299 | "\n", 300 | " test_acc = (adv_deb_metric.true_positive_rate() + adv_deb_metric.true_negative_rate()) / 2\n", 301 | " test_disp_impact = adv_deb_metric.disparate_impact()\n", 302 | "\n", 303 | " print(\"\\n\\nThresh: %0.2f\\nTesting balanced accuracy %0.2f\\nDisparate impact %0.2f\" % \n", 304 | " (thresh, test_acc, test_disp_impact))" 305 | ] 306 | }, 307 | { 308 | "cell_type": "code", 309 | "execution_count": null, 310 | "metadata": {}, 311 | "outputs": [], 312 | "source": [ 313 | "## p 150\n", 314 | "tf.reset_default_graph()\n", 315 | "sess = tf.Session()\n", 316 | " \n", 317 | "kwargs = {\n", 318 | " 'privileged_groups' : priv_group,\n", 319 | " 'unprivileged_groups' : unpriv_group,\n", 320 | " 'scope_name' : 'debiased_classifier', \n", 321 | " 'debias' : True,\n", 322 | " 'sess' : sess,\n", 323 | " 'adversary_loss_weight' : 1.0,\n", 324 | " 'num_epochs' : 25, \n", 325 | " 'batch_size' : 128, \n", 326 | " 'classifier_num_hidden_units': 16, \n", 327 | " 'debias' : True,\n", 328 | " 'seed' : 117\n", 329 | " }\n", 330 | "\n", 331 | "# Learn parameters with debias set to True\n", 332 | "debiased_model = AdversarialDebiasing(**kwargs) \n", 333 | "\n", 334 | "scaler = StandardScaler()\n", 335 | "\n", 336 | "dset_scaled_trn = dset_raw_trn.copy()\n", 337 | "dset_scaled_trn.features = scaler.fit_transform(dset_scaled_trn.features)\n", 338 | "\n", 339 | "debiased_model.fit(dset_scaled_trn)\n", 340 | "\n", 341 | "dset_tst = dset_raw_tst.copy()\n", 342 | "dset_tst.features = scaler.transform(dset_tst.features)\n", 343 | "\n", 344 | "thresholds = np.linspace(0.2, 0.60, 5)\n", 345 | "\n", 346 | "for thresh in thresholds:\n", 347 | " dset_tst_pred = dset_tst.copy()\n", 348 | " dset_tst_pred.labels = debiased_model.predict(dset_tst).scores > thresh\n", 349 | " print(np.bincount(dset_tst_pred.labels[:, 0].astype('int')))\n", 350 | " \n", 351 | " adv_deb_metric = ClassificationMetric(\n", 352 | " dset_tst, dset_tst_pred,\n", 353 | " unprivileged_groups = unpriv_group,\n", 354 | " privileged_groups = priv_group)\n", 355 | "\n", 356 | " test_acc = (adv_deb_metric.true_positive_rate() + adv_deb_metric.true_negative_rate()) / 2\n", 357 | " test_disp_impact = adv_deb_metric.disparate_impact()\n", 358 | "\n", 359 | " print(\"\\n\\nThresh: %0.2f\\nTesting balanced accuracy %0.2f\\nDisparate impact %0.2f\" % (thresh, test_acc, test_disp_impact))" 360 | ] 361 | }, 362 | { 363 | "cell_type": "code", 364 | "execution_count": null, 365 | "metadata": {}, 366 | "outputs": [], 367 | "source": [] 368 | } 369 | ], 370 | "metadata": { 371 | "kernelspec": { 372 | "display_name": "Python 3", 373 | "language": "python", 374 | "name": "python3" 375 | }, 376 | "language_info": { 377 | "codemirror_mode": { 378 | "name": "ipython", 379 | "version": 3 380 | }, 381 | "file_extension": ".py", 382 | "mimetype": "text/x-python", 383 | "name": "python", 384 | "nbconvert_exporter": "python", 385 | "pygments_lexer": "ipython3", 386 | "version": "3.7.5" 387 | } 388 | }, 389 | "nbformat": 4, 390 | "nbformat_minor": 2 391 | } 392 | -------------------------------------------------------------------------------- /Ch05/common_utils.py: -------------------------------------------------------------------------------- 1 | # Metrics function 2 | from collections import OrderedDict 3 | from aif360.metrics import ClassificationMetric 4 | 5 | def compute_metrics(dataset_true, dataset_pred, 6 | unprivileged_groups, privileged_groups, 7 | disp = True): 8 | """ Compute the key metrics """ 9 | classified_metric_pred = ClassificationMetric(dataset_true, 10 | dataset_pred, 11 | unprivileged_groups=unprivileged_groups, 12 | privileged_groups=privileged_groups) 13 | metrics = OrderedDict() 14 | metrics["Balanced accuracy"] = 0.5*(classified_metric_pred.true_positive_rate()+ 15 | classified_metric_pred.true_negative_rate()) 16 | metrics["Statistical parity difference"] = classified_metric_pred.statistical_parity_difference() 17 | metrics["Disparate impact"] = classified_metric_pred.disparate_impact() 18 | metrics["Average odds difference"] = classified_metric_pred.average_odds_difference() 19 | metrics["Equal opportunity difference"] = classified_metric_pred.equal_opportunity_difference() 20 | metrics["Theil index"] = classified_metric_pred.theil_index() 21 | 22 | if disp: 23 | for k in metrics: 24 | print("%s = %.4f" % (k, metrics[k])) 25 | 26 | return metrics 27 | -------------------------------------------------------------------------------- /Ch06/Ch06.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%matplotlib inline\n", 10 | "# Load all necessary packages\n", 11 | "import numpy as np\n", 12 | "\n", 13 | "\n", 14 | "from aif360.datasets import CompasDataset\n", 15 | "from aif360.metrics import BinaryLabelDatasetMetric\n", 16 | "from aif360.metrics import ClassificationMetric\n", 17 | "\n", 18 | "from aif360.algorithms.preprocessing.optim_preproc_helpers.data_preproc_functions import load_preproc_data_compas\n", 19 | "\n", 20 | "from sklearn.preprocessing import scale\n", 21 | "from sklearn.linear_model import LogisticRegression\n", 22 | "\n", 23 | "## Equalized odds post-processing \n", 24 | "# Odds equalizing post-processing algorithm\n", 25 | "from aif360.algorithms.postprocessing import EqOddsPostprocessing\n", 26 | "\n", 27 | "## Calbirating equalized odds post-processing\n", 28 | "from aif360.algorithms.postprocessing import CalibratedEqOddsPostprocessing" 29 | ] 30 | }, 31 | { 32 | "cell_type": "markdown", 33 | "metadata": {}, 34 | "source": [ 35 | "## Load data set\n", 36 | "### p 159" 37 | ] 38 | }, 39 | { 40 | "cell_type": "code", 41 | "execution_count": null, 42 | "metadata": {}, 43 | "outputs": [], 44 | "source": [ 45 | "compas_data = load_preproc_data_compas()\n", 46 | "priv_group = [{'race': 1}]\n", 47 | "unpriv_group = [{'race': 0}] " 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "compas_data = load_preproc_data_compas()\n", 57 | "compas_data" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "#### Divide dataset into train, validation, and test partitions " 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": null, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "np.random.seed(132)\n", 74 | "def split_data_trn_vld_tst(data_raw):\n", 75 | " dset_raw_trn, dset_raw_vt = data_raw.split([0.7], \n", 76 | " shuffle = True)\n", 77 | " dset_raw_vld, dset_raw_tst = dset_raw_vt.split([0.5], \n", 78 | " shuffle = True)\n", 79 | " \n", 80 | " return dset_raw_trn, dset_raw_vld, dset_raw_tst" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": null, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "dset_raw_trn, dset_raw_vld, dset_raw_tst = split_data_trn_vld_tst(compas_data)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "metadata": {}, 95 | "source": [ 96 | "#### Metric for the original datasets (without any classifiers)" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "## p 161\n", 106 | "metric_orig_test = BinaryLabelDatasetMetric(dset_raw_tst, \n", 107 | " unprivileged_groups = unpriv_group,\n", 108 | " privileged_groups = priv_group)\n", 109 | "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % \n", 110 | " metric_orig_test.mean_difference()) " 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "## Train classifier (logistic regression) on original training data\n", 118 | "### not shown in book" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "from sklearn.linear_model import LogisticRegression\n", 128 | "from sklearn.preprocessing import StandardScaler\n", 129 | "from sklearn.metrics import roc_curve\n", 130 | "\n", 131 | "dataset_orig_train_pred = dset_raw_trn.copy(deepcopy = True)\n", 132 | "dataset_orig_valid_pred = dset_raw_vld.copy(deepcopy = True)\n", 133 | "dataset_orig_test_pred = dset_raw_tst.copy(deepcopy = True)\n", 134 | "\n", 135 | "# Logistic regression classifier and predictions for training data\n", 136 | "scale_orig = StandardScaler()\n", 137 | "X_train = scale_orig.fit_transform(dset_raw_trn.features)\n", 138 | "y_train = dset_raw_trn.labels.ravel()\n", 139 | "lmod = LogisticRegression()\n", 140 | "lmod.fit(X_train, y_train)\n", 141 | "\n", 142 | "fav_idx = np.where(lmod.classes_ == dset_raw_trn.favorable_label)[0][0]\n", 143 | "y_train_pred_prob = lmod.predict_proba(X_train)[:,fav_idx]\n", 144 | "\n", 145 | "# Prediction probs for validation and testing data\n", 146 | "X_valid = scale_orig.transform(dset_raw_vld.features)\n", 147 | "y_valid_pred_prob = lmod.predict_proba(X_valid)[:,fav_idx]\n", 148 | "\n", 149 | "X_test = scale_orig.transform(dset_raw_tst.features)\n", 150 | "y_test_pred_prob = lmod.predict_proba(X_test)[:,fav_idx]\n", 151 | "\n", 152 | "class_thresh = 0.5\n", 153 | "dataset_orig_train_pred.scores = y_train_pred_prob.reshape(-1,1)\n", 154 | "dataset_orig_valid_pred.scores = y_valid_pred_prob.reshape(-1,1)\n", 155 | "dataset_orig_test_pred.scores = y_test_pred_prob.reshape(-1,1)\n", 156 | "\n", 157 | "y_train_pred = np.zeros_like(dataset_orig_train_pred.labels)\n", 158 | "y_train_pred[y_train_pred_prob >= class_thresh] = dataset_orig_train_pred.favorable_label\n", 159 | "y_train_pred[~(y_train_pred_prob >= class_thresh)] = dataset_orig_train_pred.unfavorable_label\n", 160 | "dataset_orig_train_pred.labels = y_train_pred\n", 161 | "\n", 162 | "y_valid_pred = np.zeros_like(dataset_orig_valid_pred.labels)\n", 163 | "y_valid_pred[y_valid_pred_prob >= class_thresh] = dataset_orig_valid_pred.favorable_label\n", 164 | "y_valid_pred[~(y_valid_pred_prob >= class_thresh)] = dataset_orig_valid_pred.unfavorable_label\n", 165 | "dataset_orig_valid_pred.labels = y_valid_pred\n", 166 | " \n", 167 | "y_test_pred = np.zeros_like(dataset_orig_test_pred.labels)\n", 168 | "y_test_pred[y_test_pred_prob >= class_thresh] = dataset_orig_test_pred.favorable_label\n", 169 | "y_test_pred[~(y_test_pred_prob >= class_thresh)] = dataset_orig_test_pred.unfavorable_label\n", 170 | "dataset_orig_test_pred.labels = y_test_pred" 171 | ] 172 | }, 173 | { 174 | "cell_type": "markdown", 175 | "metadata": {}, 176 | "source": [ 177 | "#### Results before post-processing" 178 | ] 179 | }, 180 | { 181 | "cell_type": "code", 182 | "execution_count": null, 183 | "metadata": {}, 184 | "outputs": [], 185 | "source": [ 186 | "logit_classifier_metric = ClassificationMetric(dset_raw_tst, \n", 187 | " dataset_orig_test_pred,\n", 188 | " unprivileged_groups = unpriv_group,\n", 189 | " privileged_groups = priv_group)\n", 190 | "\n", 191 | "print(\"Difference in GFPR between unprivileged and privileged groups\")\n", 192 | "print(logit_classifier_metric.difference(logit_classifier_metric.generalized_false_positive_rate))\n", 193 | "print(\"Difference in GFNR between unprivileged and privileged groups\")\n", 194 | "print(logit_classifier_metric.difference(logit_classifier_metric.generalized_false_negative_rate))\n", 195 | "print(\"Mean difference in outcomes\")\n", 196 | "print(logit_classifier_metric.mean_difference()) " 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "### Note that logistic regression produces far more disparate outcome than what is found in raw data outcome!!" 204 | ] 205 | }, 206 | { 207 | "cell_type": "markdown", 208 | "metadata": {}, 209 | "source": [ 210 | "## Equality of Odds post-processing\n", 211 | "### p 161" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": null, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [ 220 | "SEED = 94\n", 221 | "\n", 222 | "# Learn parameters to equalize odds and apply to create a new dataset\n", 223 | "eop = EqOddsPostprocessing(unprivileged_groups = unpriv_group, \n", 224 | " privileged_groups = priv_group, \n", 225 | " seed = SEED)\n", 226 | "eop = eop.fit(dset_raw_vld, dataset_orig_valid_pred)\n", 227 | "\n", 228 | "eop_trn_pred = eop.predict(dataset_orig_train_pred)\n", 229 | "\n", 230 | "eop_trn_metric = ClassificationMetric(dset_raw_trn, eop_trn_pred,\n", 231 | " unprivileged_groups = unpriv_group,\n", 232 | " privileged_groups = priv_group)\n", 233 | "\n", 234 | "print(\"TRAINING DATA\")\n", 235 | "print(\"Difference in GFPR between unprivileged and privileged groups\")\n", 236 | "print(eop_trn_metric.difference(eop_trn_metric.generalized_false_positive_rate))\n", 237 | "print(\"Difference in GFNR between unprivileged and privileged groups\")\n", 238 | "print(eop_trn_metric.difference(eop_trn_metric.generalized_false_negative_rate))\n", 239 | "print(\"Mean difference in outcomes\")\n", 240 | "print(eop_trn_metric.mean_difference())" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "eop_test_pred = eop.predict(dataset_orig_test_pred)\n", 250 | "eop_test_metric = ClassificationMetric(dset_raw_tst, eop_test_pred,\n", 251 | " unprivileged_groups = unpriv_group,\n", 252 | " privileged_groups = priv_group)\n", 253 | "print(\"TEST DATA\")\n", 254 | "print(\"Difference in GFPR between unprivileged and privileged groups\")\n", 255 | "print(eop_test_metric.difference(eop_test_metric.generalized_false_positive_rate))\n", 256 | "print(\"Difference in GFNR between unprivileged and privileged groups\")\n", 257 | "print(eop_test_metric.difference(eop_test_metric.generalized_false_negative_rate))\n", 258 | "print(\"Mean difference in outcomes\")\n", 259 | "print(eop_test_metric.mean_difference())" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": {}, 265 | "source": [ 266 | "## Calibration preserving equalized odds\n", 267 | "### p 166" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "metadata": {}, 274 | "outputs": [], 275 | "source": [ 276 | "## p 166\n", 277 | "\n", 278 | "# Learn parameters to equalize odds and apply to create a new dataset\n", 279 | "cpp = CalibratedEqOddsPostprocessing(privileged_groups = priv_group,\n", 280 | " unprivileged_groups = unpriv_group,\n", 281 | " cost_constraint = 'fpr',\n", 282 | " seed = 76)\n", 283 | "cpp = cpp.fit(dset_raw_vld, dataset_orig_valid_pred)\n", 284 | "\n", 285 | "\n", 286 | "dataset_transf_test_pred = cpp.predict(dataset_orig_test_pred)\n", 287 | "cm_transf_test_metric = ClassificationMetric(dset_raw_tst, dataset_transf_test_pred,\n", 288 | " unprivileged_groups = unpriv_group,\n", 289 | " privileged_groups = priv_group)\n", 290 | "\n", 291 | "\n", 292 | "dataset_transf_train_pred = cpp.predict(dataset_orig_train_pred)\n", 293 | "cm_transf_train_metric = ClassificationMetric(dset_raw_trn, dataset_transf_train_pred,\n", 294 | " unprivileged_groups = unpriv_group,\n", 295 | " privileged_groups = priv_group)\n", 296 | "\n", 297 | "print(\"CALIBRATING EQUALIZED ODDS PERFORMANCE ON TRAIN DATA\")\n", 298 | "print(\"Difference in GFPR between unprivileged and privileged groups\")\n", 299 | "print(cm_transf_train_metric.difference(cm_transf_train_metric.generalized_false_positive_rate))\n", 300 | "print(\"Difference in GFNR between unprivileged and privileged groups\")\n", 301 | "print(cm_transf_train_metric.difference(cm_transf_train_metric.generalized_false_negative_rate))\n", 302 | "print(\"Mean difference in outcomes\")\n", 303 | "print(cm_transf_train_metric.mean_difference())\n", 304 | "print(\"\")\n", 305 | "print(\"CALIBRATING EQUALIZED ODDS PERFORMANCE ON TEST DATA\")\n", 306 | "print(\"Difference in GFPR between unprivileged and privileged groups\")\n", 307 | "print(cm_transf_test_metric.difference(cm_transf_test_metric.generalized_false_positive_rate))\n", 308 | "print(\"Difference in GFNR between unprivileged and privileged groups\")\n", 309 | "print(cm_transf_test_metric.difference(cm_transf_test_metric.generalized_false_negative_rate))\n", 310 | "print(\"Mean difference in outcomes\")\n", 311 | "print(cm_transf_test_metric.mean_difference())" 312 | ] 313 | } 314 | ], 315 | "metadata": { 316 | "kernelspec": { 317 | "display_name": "Python 3", 318 | "language": "python", 319 | "name": "python3" 320 | }, 321 | "language_info": { 322 | "codemirror_mode": { 323 | "name": "ipython", 324 | "version": 3 325 | }, 326 | "file_extension": ".py", 327 | "mimetype": "text/x-python", 328 | "name": "python", 329 | "nbconvert_exporter": "python", 330 | "pygments_lexer": "ipython3", 331 | "version": "3.7.5" 332 | } 333 | }, 334 | "nbformat": 4, 335 | "nbformat_minor": 2 336 | } 337 | -------------------------------------------------------------------------------- /Ch06/common_utils.py: -------------------------------------------------------------------------------- 1 | # Metrics function 2 | from collections import OrderedDict 3 | from aif360.metrics import ClassificationMetric 4 | 5 | def compute_metrics(dataset_true, dataset_pred, 6 | unprivileged_groups, privileged_groups, 7 | disp = True): 8 | """ Compute the key metrics """ 9 | classified_metric_pred = ClassificationMetric(dataset_true, 10 | dataset_pred, 11 | unprivileged_groups=unprivileged_groups, 12 | privileged_groups=privileged_groups) 13 | metrics = OrderedDict() 14 | metrics["Balanced accuracy"] = 0.5*(classified_metric_pred.true_positive_rate()+ 15 | classified_metric_pred.true_negative_rate()) 16 | metrics["Statistical parity difference"] = classified_metric_pred.statistical_parity_difference() 17 | metrics["Disparate impact"] = classified_metric_pred.disparate_impact() 18 | metrics["Average odds difference"] = classified_metric_pred.average_odds_difference() 19 | metrics["Equal opportunity difference"] = classified_metric_pred.equal_opportunity_difference() 20 | metrics["Theil index"] = classified_metric_pred.theil_index() 21 | 22 | if disp: 23 | for k in metrics: 24 | print("%s = %.4f" % (k, metrics[k])) 25 | 26 | return metrics 27 | -------------------------------------------------------------------------------- /Ch07/Ch07.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Example black box audit with BBA library\n", 8 | "### p 190" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "%matplotlib inline\n", 18 | "import matplotlib.pyplot as plt\n", 19 | "import pylab\n", 20 | "\n", 21 | "from sklearn.linear_model import LogisticRegression as LR\n", 22 | "\n", 23 | "from BlackBoxAuditing.model_factories import SVM\n", 24 | "\n", 25 | "from BlackBoxAuditing.data import load_from_file\n", 26 | "from BlackBoxAuditing.model_factories.AbstractModelFactory import AbstractModelFactory\n", 27 | "from BlackBoxAuditing.model_factories.AbstractModelVisitor import AbstractModelVisitor\n", 28 | "\n", 29 | "import pandas as pd\n", 30 | "import numpy as np\n", 31 | "import random\n", 32 | "\n", 33 | "import BlackBoxAuditing as BBA\n", 34 | "\n", 35 | "import pickle" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "## p 190\n", 45 | "ricci_data = BBA.load_data(\"ricci\")" 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "type(ricci_data)" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "len(ricci_data)" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": {}, 70 | "outputs": [], 71 | "source": [ 72 | "for i in range(len(ricci_data)):\n", 73 | " print(i)\n", 74 | " print(type(ricci_data[0]))" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "ricci_data[0]" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": null, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "len(ricci_data[1])" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": {}, 99 | "outputs": [], 100 | "source": [ 101 | "ricci_data[1][:10]" 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": null, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "df = pd.DataFrame(ricci_data[2])\n", 111 | "df.columns = ricci_data[0]" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "df.groupby('Race').count()" 121 | ] 122 | }, 123 | { 124 | "cell_type": "code", 125 | "execution_count": null, 126 | "metadata": {}, 127 | "outputs": [], 128 | "source": [ 129 | "df.groupby('Position').count()" 130 | ] 131 | }, 132 | { 133 | "cell_type": "code", 134 | "execution_count": null, 135 | "metadata": {}, 136 | "outputs": [], 137 | "source": [ 138 | "df.groupby(['Position', 'Race']).count()" 139 | ] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "execution_count": null, 144 | "metadata": {}, 145 | "outputs": [], 146 | "source": [ 147 | "## p 192\n", 148 | "ricci_data[2][:10]" 149 | ] 150 | }, 151 | { 152 | "cell_type": "code", 153 | "execution_count": null, 154 | "metadata": {}, 155 | "outputs": [], 156 | "source": [ 157 | "auditor = BBA.Auditor()\n", 158 | "auditor.ModelFactory = SVM\n", 159 | "auditor(ricci_data, output_dir =\"ricci-audit-output\")" 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "## p 196\n", 169 | "acc_data = pd.read_csv(\"ricci-audit-output/accuracy.png.data\")\n", 170 | "print(acc_data)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "## p 197\n", 180 | "def influence(df):\n", 181 | " return (df.iloc[0][1:] - df.iloc[-1][1:])\n", 182 | "\n", 183 | "influence(acc_data)" 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": null, 189 | "metadata": {}, 190 | "outputs": [], 191 | "source": [ 192 | "def influence_partial_repair(df):\n", 193 | " return (df.iloc[0][1:] - df.iloc[5][1:])\n", 194 | "\n", 195 | "influence_partial_repair(acc_data)" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "## p 197\n", 205 | "deltas = influence(acc_data) - influence_partial_repair(acc_data)\n", 206 | "plt.bar(x = deltas.index, height = deltas.values)" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "## Auditing a proprietary data set + black box model\n", 214 | "### p 197" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### First generate a proprietary data set ( you can also use a real one but I generate synthetic data for convenience)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "code", 226 | "execution_count": null, 227 | "metadata": {}, 228 | "outputs": [], 229 | "source": [ 230 | "## first produce the data\n", 231 | "## not covered in book, just background code needed to run example\n", 232 | "SAMPLE_SIZE = 1000\n", 233 | "\n", 234 | "credit_score = np.array(np.random.randn(SAMPLE_SIZE)) * 100 + 600\n", 235 | "gender = np.array(random.choices([\"female\", \"male\", \"non-binary\", \"prefer not to answer\"], \n", 236 | " weights = [0.48, 0.48, 0.02, 0.02], \n", 237 | " k = SAMPLE_SIZE))\n", 238 | "age = np.array(random.choices(range(18, 80), k = SAMPLE_SIZE))\n", 239 | "length_employment = np.rint((age - 18) * np.random.uniform(size=SAMPLE_SIZE))\n", 240 | "employee_score = credit_score * length_employment + random.choices(range(-1000, 1000), k = SAMPLE_SIZE)\n", 241 | "\n", 242 | "hire = np.logical_or(np.logical_and(employee_score > 9000, np.logical_or(gender == \"male\", age < 50)),\n", 243 | " employee_score > 9500).astype(float) \n", 244 | "\n", 245 | "female = np.where(gender == 'female', 1, 0)\n", 246 | "male = np.where(gender == 'male', 1, 0)\n", 247 | "nonbinary = np.where(gender == 'nonbinary', 1, 0)\n", 248 | "\n", 249 | "df = pd.DataFrame(\n", 250 | " {\n", 251 | " 'credit_score' : credit_score,\n", 252 | " 'gender' : gender,\n", 253 | " 'age' : age,\n", 254 | " 'length_employment': length_employment,\n", 255 | " 'employee_score' : employee_score,\n", 256 | " 'female' : female,\n", 257 | " 'male' : male,\n", 258 | " 'nonbinary' : nonbinary,\n", 259 | " 'hire' : hire\n", 260 | " })\n", 261 | "\n", 262 | "col_names = ['credit_score', 'age', \n", 263 | " 'length_employment', 'employee_score', \n", 264 | " 'female', 'male', 'nonbinary',\n", 265 | " 'hire']\n", 266 | "\n", 267 | "df.to_csv(\"synth_data.csv\", \n", 268 | " index=False, \n", 269 | " columns=col_names)" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "### Example with proprietary data starts here in earnest" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": null, 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "## p 198\n", 286 | "synthetic_data = load_from_file(\"synth_data.csv\", \n", 287 | " correct_types = np.repeat([float], [len(col_names)]), \n", 288 | " response_header = 'hire',\n", 289 | " train_percentage = 0.5)" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "### But then another detour to build the \"proprietary model\" that will be opaque to BBA" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": null, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "train_data = pd.DataFrame(synthetic_data[1])\n", 306 | "test_data = pd.DataFrame(synthetic_data[2])\n", 307 | "train_data.columns = test_data.columns = col_names" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": null, 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "train_data.head()" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "X = train_data.iloc[:, :-1]\n", 326 | "Y = train_data.iloc[:, -1]" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": null, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "lr = LR().fit(X, Y)" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": null, 341 | "metadata": {}, 342 | "outputs": [], 343 | "source": [ 344 | "X_test = test_data.iloc[:, :-1]\n", 345 | "Y_test = test_data.iloc[:, -1]\n", 346 | "lr.score(X_test, Y_test)" 347 | ] 348 | }, 349 | { 350 | "cell_type": "code", 351 | "execution_count": null, 352 | "metadata": {}, 353 | "outputs": [], 354 | "source": [ 355 | "with open( 'lr.pickle', 'wb' ) as f:\n", 356 | " pickle.dump(lr, f )" 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": null, 362 | "metadata": {}, 363 | "outputs": [], 364 | "source": [ 365 | "with open( 'lr.pickle', 'rb' ) as f:\n", 366 | " lr2 = pickle.load(f)" 367 | ] 368 | }, 369 | { 370 | "cell_type": "markdown", 371 | "metadata": {}, 372 | "source": [ 373 | "### Return to on-topic example of auditing a black box model" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [ 382 | "## p 199\n", 383 | "class HirePredictorBuilder(AbstractModelFactory):\n", 384 | " def __init__(self, *args, **kwargs):\n", 385 | " AbstractModelFactory.__init__(self, *args, **kwargs)\n", 386 | " self.verbose_factory_name = \"HirePredictor\" \n", 387 | " def build(self, train_set):\n", 388 | " return HirePredictor() \n", 389 | "\n", 390 | "class HirePredictor(AbstractModelVisitor):\n", 391 | " def __init__(self):\n", 392 | " with open( 'lr.pickle', 'rb' ) as f:\n", 393 | " self.lr = pickle.load(f) \n", 394 | "\n", 395 | " def test(self, test_set, test_name=\"\"):\n", 396 | " return [[v[-1], self.lr.predict(np.expand_dims(np.array(v[:-1]), axis = 0))] for v in test_set] \n" 397 | ] 398 | }, 399 | { 400 | "cell_type": "code", 401 | "execution_count": null, 402 | "metadata": {}, 403 | "outputs": [], 404 | "source": [ 405 | "auditor = BBA.Auditor()\n", 406 | "auditor.ModelFactory = HirePredictorBuilder\n", 407 | "auditor(synthetic_data, output_dir = \"synthetic-audit-output\")" 408 | ] 409 | }, 410 | { 411 | "cell_type": "code", 412 | "execution_count": null, 413 | "metadata": {}, 414 | "outputs": [], 415 | "source": [] 416 | } 417 | ], 418 | "metadata": { 419 | "kernelspec": { 420 | "display_name": "Python 3", 421 | "language": "python", 422 | "name": "python3" 423 | }, 424 | "language_info": { 425 | "codemirror_mode": { 426 | "name": "ipython", 427 | "version": 3 428 | }, 429 | "file_extension": ".py", 430 | "mimetype": "text/x-python", 431 | "name": "python", 432 | "nbconvert_exporter": "python", 433 | "pygments_lexer": "ipython3", 434 | "version": "3.7.5" 435 | } 436 | }, 437 | "nbformat": 4, 438 | "nbformat_minor": 2 439 | } 440 | -------------------------------------------------------------------------------- /Ch08/Ch08 SHAP.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import pandas as pd\n", 10 | "\n", 11 | "from sklearn.metrics import accuracy_score\n", 12 | "from sklearn.model_selection import train_test_split\n", 13 | "from sklearn.ensemble import RandomForestClassifier as RFC\n", 14 | "\n", 15 | "from aif360.datasets import MEPSDataset19\n", 16 | "\n", 17 | "import aix360\n", 18 | "import shap" 19 | ] 20 | }, 21 | { 22 | "cell_type": "code", 23 | "execution_count": null, 24 | "metadata": {}, 25 | "outputs": [], 26 | "source": [ 27 | "med_data19 = MEPSDataset19()" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "## Data" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "X_train, X_test, y_train, y_test = train_test_split(\n", 44 | " med_data19.features, med_data19.labels, \n", 45 | " random_state = 0, stratify = med_data19.labels) " 46 | ] 47 | }, 48 | { 49 | "cell_type": "code", 50 | "execution_count": null, 51 | "metadata": {}, 52 | "outputs": [], 53 | "source": [ 54 | "X_train = pd.DataFrame(X_train)\n", 55 | "X_train.columns = med_data19.feature_names\n", 56 | "X_train.head()" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": null, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "X_test = pd.DataFrame(X_test)\n", 66 | "X_test.columns = med_data19.feature_names" 67 | ] 68 | }, 69 | { 70 | "cell_type": "markdown", 71 | "metadata": {}, 72 | "source": [ 73 | "## Build a model that needs explaining" 74 | ] 75 | }, 76 | { 77 | "cell_type": "code", 78 | "execution_count": null, 79 | "metadata": {}, 80 | "outputs": [], 81 | "source": [ 82 | "## p 225\n", 83 | "rf = RFC(n_estimators=200, max_depth = 4)\n", 84 | "rf.fit(X_train, y_train.ravel())" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": null, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "print('Train accuracy: %0.2f Test accuracy: %0.2f' % \n", 94 | " (accuracy_score(y_train, rf.predict(X_train)),\n", 95 | " accuracy_score(y_test, rf.predict(X_test))))" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "## SHAP\n", 103 | "### p 223" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "## not shown in book\n", 113 | "shap.initjs()" 114 | ] 115 | }, 116 | { 117 | "cell_type": "code", 118 | "execution_count": null, 119 | "metadata": {}, 120 | "outputs": [], 121 | "source": [ 122 | "## p 225\n", 123 | "rf_prob_1 = lambda x: rf.predict_proba(x)[:,1]" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [ 132 | "## p 226\n", 133 | "ke = shap.KernelExplainer(rf_prob_1, shap.sample(X_train, 100), link = 'logit')" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "shap_values = ke.shap_values(X_test[:100], nsamples = 100)" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "metadata": {}, 149 | "outputs": [], 150 | "source": [ 151 | "## p 227\n", 152 | "shap.summary_plot(shap_values, X_test[:100])" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": null, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "## p 228\n", 162 | "shap.dependence_plot('AGE', shap_values, X_test[:100])" 163 | ] 164 | }, 165 | { 166 | "cell_type": "code", 167 | "execution_count": null, 168 | "metadata": {}, 169 | "outputs": [], 170 | "source": [] 171 | } 172 | ], 173 | "metadata": { 174 | "kernelspec": { 175 | "display_name": "Python 3", 176 | "language": "python", 177 | "name": "python3" 178 | }, 179 | "language_info": { 180 | "codemirror_mode": { 181 | "name": "ipython", 182 | "version": 3 183 | }, 184 | "file_extension": ".py", 185 | "mimetype": "text/x-python", 186 | "name": "python", 187 | "nbconvert_exporter": "python", 188 | "pygments_lexer": "ipython3", 189 | "version": "3.7.5" 190 | } 191 | }, 192 | "nbformat": 4, 193 | "nbformat_minor": 2 194 | } 195 | -------------------------------------------------------------------------------- /Ch08/Ch08_GLRM.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "\n", 12 | "from sklearn.metrics import accuracy_score\n", 13 | "from sklearn.model_selection import train_test_split\n", 14 | "\n", 15 | "from aix360.algorithms.rbm import FeatureBinarizer, LogisticRuleRegression\n", 16 | "\n", 17 | "from aif360.datasets import MEPSDataset19\n", 18 | "\n", 19 | "import aix360" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Getting some data\n", 27 | "### p 208" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Bring in the dataset" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "## p 208\n", 44 | "med_data19 = MEPSDataset19()" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "#### some brief exploration not shown in book" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "med_data19.features.shape" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "med_data19.labels.shape" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "med_data19.label_names" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "type(med_data19)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "med_data19.features" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "med_data19.labels" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "#### back to loading data and continuing" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "## p 208\n", 122 | "X_train, X_test, y_train, y_test = train_test_split(\n", 123 | " med_data19.features, med_data19.labels, \n", 124 | " random_state = 0, stratify = med_data19.labels) " 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "X_train.shape" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "X_train = pd.DataFrame(X_train)\n", 143 | "X_train.columns = med_data19.feature_names\n", 144 | "X_train.head()" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "X_test = pd.DataFrame(X_test)\n", 154 | "X_test.columns = med_data19.feature_names" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "X_test.head()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "y_train = y_train[:, 0]" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "y_test = y_test[:, 0]" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "## Building interpretable models" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "## preparation: we need to binarize inputs\n", 198 | "## p 209\n", 199 | "feat_bin = FeatureBinarizer(negations=True, returnOrd=True)" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [ 208 | "X_train, X_train_std = feat_bin.fit_transform(X_train)\n", 209 | "X_test, X_testStd = feat_bin.transform(X_test)" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "X_train['AGE'].head()" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "## GLRM\n", 226 | "### p 207 and onwards" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "## p 210\n", 236 | "lrr = LogisticRuleRegression(lambda0 = 0.005, lambda1 = 0.001, useOrd = True)\n", 237 | "lrr.fit(X_train, y_train, X_train_std)" 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": null, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "print('Train accuracy: %0.2f Test accuracy: %0.2f' % \n", 247 | " (accuracy_score(y_train, lrr.predict(X_train, X_train_std)),\n", 248 | " accuracy_score(y_test, lrr.predict(X_test, X_testStd))))" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": null, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "## p 211\n", 258 | "df = lrr.explain()\n", 259 | "df['rule/numerical feature'][1]\n", 260 | "df.style.set_properties(subset=['rule/numerical feature'], **{'width': '300px'})" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "## p 212\n", 270 | "df = lrr.explain(highDegOnly = True)\n", 271 | "df.style.set_properties(subset=['rule'], **{'width': '300px'})" 272 | ] 273 | }, 274 | { 275 | "cell_type": "code", 276 | "execution_count": null, 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "## from documentation\n", 281 | "## lambda0 (float, optional) – Regularization - fixed cost of each rule\n", 282 | "## lambda1 (float, optional) – Regularization - additional cost of each literal in rule" 283 | ] 284 | }, 285 | { 286 | "cell_type": "code", 287 | "execution_count": null, 288 | "metadata": {}, 289 | "outputs": [], 290 | "source": [ 291 | "## p 215\n", 292 | "## now let's imagine we're willing to allow more complex rules but want fewer rules. let's adjust lambda and see what that does to performance\n", 293 | "lrr_alt = LogisticRuleRegression(lambda0=0.01, lambda1=0.0001, useOrd=True)\n", 294 | "lrr_alt.fit(X_train, y_train, X_train_std)" 295 | ] 296 | }, 297 | { 298 | "cell_type": "code", 299 | "execution_count": null, 300 | "metadata": {}, 301 | "outputs": [], 302 | "source": [ 303 | "print('Train accuracy: %0.2f Test accuracy: %0.2f' % \n", 304 | " (accuracy_score(y_train, lrr_alt.predict(X_train, X_train_std)),\n", 305 | " accuracy_score(y_test, lrr_alt.predict(X_test, X_testStd))))" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": null, 311 | "metadata": {}, 312 | "outputs": [], 313 | "source": [ 314 | "## p 216\n", 315 | "df_alt = lrr_alt.explain()\n", 316 | "df_alt['rule'][1]\n", 317 | "df_alt.style.set_properties(subset=['rule'], **{'width': '300px'})" 318 | ] 319 | } 320 | ], 321 | "metadata": { 322 | "kernelspec": { 323 | "display_name": "Python 3", 324 | "language": "python", 325 | "name": "python3" 326 | }, 327 | "language_info": { 328 | "codemirror_mode": { 329 | "name": "ipython", 330 | "version": 3 331 | }, 332 | "file_extension": ".py", 333 | "mimetype": "text/x-python", 334 | "name": "python", 335 | "nbconvert_exporter": "python", 336 | "pygments_lexer": "ipython3", 337 | "version": "3.7.5" 338 | } 339 | }, 340 | "nbformat": 4, 341 | "nbformat_minor": 2 342 | } 343 | -------------------------------------------------------------------------------- /Ch08/Ch08_LIME.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "\n", 12 | "from sklearn.metrics import accuracy_score\n", 13 | "from sklearn.model_selection import train_test_split\n", 14 | "\n", 15 | "from aix360.algorithms.rbm import FeatureBinarizer, LogisticRuleRegression\n", 16 | "\n", 17 | "from aif360.datasets import MEPSDataset19\n", 18 | "\n", 19 | "import aix360" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Getting some data\n", 27 | "### p 208" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "### Bring in the dataset" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "## p 208\n", 44 | "med_data19 = MEPSDataset19()" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "#### some brief exploration not shown in book" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "med_data19.features.shape" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "med_data19.labels.shape" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "med_data19.label_names" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "type(med_data19)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "code", 92 | "execution_count": null, 93 | "metadata": {}, 94 | "outputs": [], 95 | "source": [ 96 | "med_data19.features" 97 | ] 98 | }, 99 | { 100 | "cell_type": "code", 101 | "execution_count": null, 102 | "metadata": {}, 103 | "outputs": [], 104 | "source": [ 105 | "med_data19.labels" 106 | ] 107 | }, 108 | { 109 | "cell_type": "markdown", 110 | "metadata": {}, 111 | "source": [ 112 | "#### back to loading data and continuing" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": null, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "## p 208\n", 122 | "X_train, X_test, y_train, y_test = train_test_split(\n", 123 | " med_data19.features, med_data19.labels, \n", 124 | " random_state = 0, stratify = med_data19.labels) " 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "X_train.shape" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "X_train = pd.DataFrame(X_train)\n", 143 | "X_train.columns = med_data19.feature_names\n", 144 | "X_train.head()" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": null, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "X_test = pd.DataFrame(X_test)\n", 154 | "X_test.columns = med_data19.feature_names" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "X_test.head()" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "y_train = y_train[:, 0]" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "y_test = y_test[:, 0]" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "## Building interpretable models" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": null, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "## preparation: we need to binarize inputs\n", 198 | "## p 209\n", 199 | "feat_bin = FeatureBinarizer(negations=True, returnOrd=True)" 200 | ] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "execution_count": null, 205 | "metadata": {}, 206 | "outputs": [], 207 | "source": [ 208 | "X_train, X_train_std = feat_bin.fit_transform(X_train)\n", 209 | "X_test, X_testStd = feat_bin.transform(X_test)" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "X_train['AGE'].head()" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "## LIME \n", 226 | "### p 219" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "from sklearn.ensemble import RandomForestClassifier as RFC\n", 236 | "from aix360.algorithms.lime.lime_wrapper import LimeTabularExplainer" 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": null, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "orig_inputs = pd.DataFrame(med_data19.features)\n", 246 | "orig_inputs.columns = med_data19.feature_names" 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": null, 252 | "metadata": {}, 253 | "outputs": [], 254 | "source": [ 255 | "## p 221\n", 256 | "orig_inputs = pd.DataFrame(med_data19.features)\n", 257 | "orig_inputs.columns = med_data19.feature_names\n", 258 | "orig_target = med_data19.labels" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": { 265 | "scrolled": true 266 | }, 267 | "outputs": [], 268 | "source": [ 269 | "rf = RFC(n_estimators=500)\n", 270 | "rf.fit(orig_inputs, orig_target.ravel())" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "metadata": {}, 277 | "outputs": [], 278 | "source": [ 279 | "accuracy_score(orig_target, rf.predict(orig_inputs))" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": null, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "## p 221\n", 289 | "cat_idxs = [1] + list(range(5, 138))\n", 290 | "ltf = LimeTabularExplainer(orig_inputs.values, \n", 291 | " feature_names = orig_inputs.columns,\n", 292 | " class_names = orig_target.ravel(),\n", 293 | " categorical_features = cat_idxs,\n", 294 | " discretize_continuous = True\n", 295 | " )" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": null, 301 | "metadata": {}, 302 | "outputs": [], 303 | "source": [ 304 | "i = np.random.randint(0, orig_inputs.values.shape[0])\n", 305 | "print(\"i = %d\" % i)\n", 306 | "exp = ltf.explain_instance(orig_inputs.values[i], rf.predict_proba, num_features=5, top_labels=1)\n", 307 | "print(exp.as_list(label = 0) )" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": null, 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "rf.predict_proba(orig_inputs.values[i:(i+1), :])" 317 | ] 318 | }, 319 | { 320 | "cell_type": "code", 321 | "execution_count": null, 322 | "metadata": {}, 323 | "outputs": [], 324 | "source": [ 325 | "## p 221\n", 326 | "fig = exp.as_pyplot_figure(0)" 327 | ] 328 | }, 329 | { 330 | "cell_type": "code", 331 | "execution_count": null, 332 | "metadata": {}, 333 | "outputs": [], 334 | "source": [ 335 | "### what about for another data point?\n", 336 | "orig_target[i]" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "## p 222\n", 346 | "i = 1001\n", 347 | "exp2 = ltf.explain_instance(orig_inputs.values[i], rf.predict_proba, num_features=5, top_labels=1)\n", 348 | "exp2.as_list(orig_target[i][0]) " 349 | ] 350 | }, 351 | { 352 | "cell_type": "code", 353 | "execution_count": null, 354 | "metadata": {}, 355 | "outputs": [], 356 | "source": [ 357 | "fig = exp2.as_pyplot_figure()" 358 | ] 359 | }, 360 | { 361 | "cell_type": "code", 362 | "execution_count": null, 363 | "metadata": {}, 364 | "outputs": [], 365 | "source": [] 366 | } 367 | ], 368 | "metadata": { 369 | "kernelspec": { 370 | "display_name": "Python 3", 371 | "language": "python", 372 | "name": "python3" 373 | }, 374 | "language_info": { 375 | "codemirror_mode": { 376 | "name": "ipython", 377 | "version": 3 378 | }, 379 | "file_extension": ".py", 380 | "mimetype": "text/x-python", 381 | "name": "python", 382 | "nbconvert_exporter": "python", 383 | "pygments_lexer": "ipython3", 384 | "version": "3.7.5" 385 | } 386 | }, 387 | "nbformat": 4, 388 | "nbformat_minor": 2 389 | } 390 | -------------------------------------------------------------------------------- /Ch08/Ch08_ProtoDash.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import pandas as pd\n", 11 | "\n", 12 | "\n", 13 | "from sklearn.metrics import accuracy_score\n", 14 | "from sklearn.model_selection import train_test_split\n", 15 | "from sklearn.ensemble import RandomForestClassifier as RFC\n", 16 | "\n", 17 | "from aif360.datasets import MEPSDataset19\n", 18 | "\n", 19 | "import aix360\n", 20 | "from aix360.algorithms.protodash import ProtodashExplainer" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "med_data19 = MEPSDataset19()" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "X_train, X_test, y_train, y_test = train_test_split(\n", 39 | " med_data19.features, med_data19.labels, \n", 40 | " random_state = 0, stratify = med_data19.labels) " 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "med_data19.label_names" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "X_train = pd.DataFrame(X_train)\n", 59 | "X_train.columns = med_data19.feature_names\n", 60 | "X_train.head()" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "X_test = pd.DataFrame(X_test)\n", 70 | "X_test.columns = med_data19.feature_names" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "## ProtoDash\n", 78 | "### p 229" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "metadata": {}, 84 | "source": [ 85 | "### preparing the data p 230" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "## p 230\n", 95 | "X_scale = np.vstack((X_train, X_test))\n", 96 | "Xmax = np.max(X_scale, axis = 0)\n", 97 | "Xmin = np.min(X_scale, axis = 0) " 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "X_scale = (X_scale - Xmin)/(Xmax - Xmin)\n", 107 | "X_scale = X_scale - 0.5" 108 | ] 109 | }, 110 | { 111 | "cell_type": "code", 112 | "execution_count": null, 113 | "metadata": {}, 114 | "outputs": [], 115 | "source": [ 116 | "X_scale_train = X_scale[:X_train.shape[0]]\n", 117 | "X_scale_test = X_scale[X_train.shape[0]:]" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "## Train a model" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": {}, 131 | "outputs": [], 132 | "source": [ 133 | "## p 230\n", 134 | "rf = RFC(n_estimators=200, max_depth = 4)\n", 135 | "rf.fit(X_scale_train, y_train.ravel())" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "metadata": {}, 142 | "outputs": [], 143 | "source": [ 144 | "print('Train accuracy: %0.2f Test accuracy: %0.2f' % \n", 145 | " (accuracy_score(y_train, rf.predict(X_scale_train)),\n", 146 | " accuracy_score(y_test, rf.predict(X_scale_test))))" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": {}, 152 | "source": [ 153 | "## Generate samples similar to a specific data point" 154 | ] 155 | }, 156 | { 157 | "cell_type": "code", 158 | "execution_count": null, 159 | "metadata": {}, 160 | "outputs": [], 161 | "source": [ 162 | "## p 231\n", 163 | "\n", 164 | "## predict values from our model to divide data points according to predicted values\n", 165 | "predicted_vals = rf.predict(X_scale_test)\n", 166 | "results_df = pd.DataFrame(np.hstack([X_scale_test, predicted_vals.reshape(-1, 1)]))\n", 167 | "results_df.columns = list(X_test.columns.values) + [\"Class\"]\n", 168 | "\n", 169 | "## looking at those with UTILIZATION == 0.0 (as a binary variable of 0 or 1)\n", 170 | "base_dataset = results_df[results_df.Class == 0.0].values\n", 171 | "\n", 172 | "## select an example to explain \n", 173 | "selected_example_idx = 5\n", 174 | "data_to_explain = np.expand_dims(base_dataset[selected_example_idx], axis = 1).transpose()\n", 175 | "\n", 176 | "## remove the example of interest from the base dataset\n", 177 | "base_dataset = np.delete(base_dataset, selected_example_idx, 0)\n", 178 | "\n", 179 | "## how many prototypes do we want returned?\n", 180 | "num_prototypes = 5" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "## p 231\n", 190 | "exp = ProtodashExplainer()\n", 191 | "(W, S, _) = exp.explain(data_to_explain, base_dataset, m = num_prototypes)" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "W" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "S" 210 | ] 211 | }, 212 | { 213 | "cell_type": "code", 214 | "execution_count": null, 215 | "metadata": {}, 216 | "outputs": [], 217 | "source": [ 218 | "## p 232\n", 219 | "dfs = pd.DataFrame.from_records(results_df.iloc[S, 0:-1].astype('double'))\n", 220 | "dfs.columns = X_test.columns\n", 221 | "dfs[138] = 0\n", 222 | "dfs.columns.values[138] = \"Utilization\"" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "dfs[\"Weight\"] = np.around(W, 4)/np.sum(np.around(W, 4)) # Calculate normalized importance weights" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": null, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "x_row = pd.DataFrame(data_to_explain)" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [ 249 | "x_row[139] = 100" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": null, 255 | "metadata": {}, 256 | "outputs": [], 257 | "source": [ 258 | "dfs.columns" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": null, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "x_row.columns = dfs.columns" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "metadata": {}, 274 | "outputs": [], 275 | "source": [ 276 | "dfs = dfs.append(x_row)" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": null, 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "## p 232\n", 286 | "## reorder with Weight\n", 287 | "dfs.sort_values(\"Weight\", inplace = True, ascending = False)" 288 | ] 289 | }, 290 | { 291 | "cell_type": "code", 292 | "execution_count": null, 293 | "metadata": {}, 294 | "outputs": [], 295 | "source": [ 296 | "dfs.transpose().to_csv( \"protodash_results.csv\")" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": null, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "dfs.transpose()" 306 | ] 307 | } 308 | ], 309 | "metadata": { 310 | "kernelspec": { 311 | "display_name": "Python 3", 312 | "language": "python", 313 | "name": "python3" 314 | }, 315 | "language_info": { 316 | "codemirror_mode": { 317 | "name": "ipython", 318 | "version": 3 319 | }, 320 | "file_extension": ".py", 321 | "mimetype": "text/x-python", 322 | "name": "python", 323 | "nbconvert_exporter": "python", 324 | "pygments_lexer": "ipython3", 325 | "version": "3.7.5" 326 | } 327 | }, 328 | "nbformat": 4, 329 | "nbformat_minor": 2 330 | } 331 | -------------------------------------------------------------------------------- /Ch09/classifier_methods.py: -------------------------------------------------------------------------------- 1 | ### code patterned from https://github.com/csong27/membership-inference 2 | from fc_model import FCNet 3 | from cnn_model import ConvNet 4 | 5 | import copy 6 | import numpy as np 7 | 8 | import torch 9 | import torch.nn as nn 10 | 11 | torch.multiprocessing.set_sharing_strategy('file_system') 12 | 13 | def train(trainloader, testloader, model = 'cnn', 14 | fc_dim_hidden = 50, fc_dim_in = 10, fc_dim_out = 2, 15 | batch_size = 10, epochs = 10, 16 | learning_rate = 0.001): 17 | 18 | if model == 'fc': 19 | net = FCNet(dim_hidden = fc_dim_hidden, dim_in = fc_dim_in, dim_out = fc_dim_out, 20 | batch_size = batch_size) 21 | elif model == 'cnn': 22 | net = ConvNet() 23 | else: 24 | raise NotImplementedError 25 | 26 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 27 | net.to(device) 28 | 29 | criterion = nn.CrossEntropyLoss(reduction = 'mean') 30 | optimizer = torch.optim.SGD(net.parameters(), lr = learning_rate, momentum = 0.9) 31 | 32 | ## for numpy iteration, need to keep a copy and refresh 33 | bak_trainloader, bak_testloader = copy.deepcopy(trainloader), copy.deepcopy(testloader) 34 | needs_refresh = False 35 | if 'needs_refresh' in dir(trainloader): 36 | trainloader = bak_trainloader() ## for case of using iterate_and_shuffle_numpy 37 | testloader = bak_testloader() 38 | needs_refresh = True 39 | 40 | for epoch in range(epochs): # loop over the dataset multiple times 41 | 42 | if needs_refresh: 43 | trainloader = bak_trainloader() ## for case of using iterate_and_shuffle_numpy 44 | testloader = bak_testloader() 45 | 46 | running_loss = 0.0 47 | n_correct = 0 48 | n_total = 0 49 | 50 | for idx, data in enumerate(trainloader): 51 | # get the inputs; data is a list of [inputs, labels] 52 | try: 53 | inputs, labels = data[0].to(device), data[1].to(device) 54 | except: 55 | inputs = torch.from_numpy(data[0]).to(device) 56 | labels = torch.from_numpy(data[1]).to(device) 57 | 58 | # zero the parameter gradients 59 | optimizer.zero_grad() 60 | 61 | # forward + backward + optimize 62 | outputs = net(inputs) 63 | 64 | _, predicted = torch.max(outputs.data, 1) 65 | n_total += labels.size(0) 66 | n_correct += (predicted == labels).sum().item() 67 | 68 | try: 69 | loss = criterion(outputs, labels) 70 | except: 71 | loss = criterion(outputs, labels.long()) 72 | 73 | loss.backward() 74 | optimizer.step() 75 | 76 | 77 | if epoch == epochs - 1: 78 | print('Epoch: %d Accuracy of the network on the training set: %d %%' % ( 79 | epoch, 100 * n_correct / n_total)) 80 | 81 | n_correct, n_total = 0, 0 82 | y_hat, y_true = [], [] 83 | with torch.no_grad(): 84 | for idx, data in enumerate(testloader): 85 | try: 86 | images, labels = data[0].to(device), data[1].to(device) 87 | except: 88 | images = torch.from_numpy(data[0]).to(device) 89 | labels = torch.from_numpy(data[1]).to(device) 90 | 91 | outputs = net(images) 92 | _, predicted = torch.max(outputs.data, 1) 93 | n_total += labels.size(0) 94 | n_correct += (predicted == labels).sum().item() 95 | 96 | if epoch == epochs - 1: 97 | y_hat.append(predicted.cpu().numpy()) 98 | y_true.append(labels.cpu().numpy()) 99 | 100 | if epoch == epochs - 1: 101 | print('Epoch: %d Accuracy of the network on the test set: %d %%' % ( 102 | epoch, 100 * n_correct / n_total)) 103 | 104 | y_true, y_hat = np.concatenate(y_true), np.concatenate(y_hat) 105 | 106 | return net, y_hat, y_true 107 | 108 | 109 | def iterate_and_shuffle_numpy(inputs, targets, batch_size): 110 | def return_generator(): 111 | assert len(inputs) == len(targets) 112 | indices = np.arange(len(inputs)) 113 | np.random.shuffle(indices) 114 | 115 | for start_idx in range(0, len(inputs) - batch_size + 1, batch_size): 116 | excerpt = indices[start_idx:start_idx + batch_size] 117 | yield inputs[excerpt], targets[excerpt] 118 | 119 | return_generator.needs_refresh = True 120 | return return_generator 121 | -------------------------------------------------------------------------------- /Ch09/cnn_model.py: -------------------------------------------------------------------------------- 1 | import torch.nn as nn 2 | import torch.nn.functional as F 3 | 4 | class ConvNet(nn.Module): 5 | def __init__(self): 6 | super(ConvNet, self).__init__() 7 | self.conv1 = nn.Conv2d(3, 6, 5) 8 | self.pool = nn.MaxPool2d(2, 2) 9 | self.conv2 = nn.Conv2d(6, 16, 5) 10 | self.fc1 = nn.Linear(16 * 5 * 5, 120) 11 | self.fc2 = nn.Linear(120, 84) 12 | self.fc3 = nn.Linear(84, 10) 13 | 14 | def forward(self, x): 15 | x = self.pool(F.relu(self.conv1(x))) 16 | x = self.pool(F.relu(self.conv2(x))) 17 | x = x.view(-1, 16 * 5 * 5) 18 | x = F.relu(self.fc1(x)) 19 | x = F.relu(self.fc2(x)) 20 | x = self.fc3(x) 21 | return x 22 | -------------------------------------------------------------------------------- /Ch09/fc_model.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import torch.nn as nn 3 | import torch.nn.functional as F 4 | 5 | class FCNet(nn.Module): 6 | 7 | def __init__(self, 8 | dim_in = 10, dim_hidden = 20, dim_out = 2, 9 | batch_size = 100, rtn_layer = True): 10 | super(FCNet, self).__init__() 11 | 12 | self.fc1 = nn.Linear(dim_in, dim_hidden) 13 | self.fc2 = nn.Linear(dim_hidden, dim_out) 14 | 15 | 16 | def forward(self, x): 17 | x = F.relu(self.fc1(x)) 18 | x = self.fc2(x) 19 | return x 20 | 21 | -------------------------------------------------------------------------------- /Ch09/run_attack.py: -------------------------------------------------------------------------------- 1 | ### code patterned from https://github.com/csong27/membership-inference 2 | import numpy as np 3 | import argparse 4 | import os 5 | 6 | from classifier_torch import train, iterate_and_shuffle_numpy 7 | from sklearn.metrics import classification_report, accuracy_score 8 | 9 | from torch.utils.data.sampler import SubsetRandomSampler 10 | import torchvision.transforms as transforms 11 | import torchvision 12 | import torch 13 | 14 | torch.multiprocessing.set_sharing_strategy('file_system') 15 | np.random.seed(171717) 16 | 17 | #### CONSTANTS 18 | TRAIN_SIZE = 10000 19 | TEST_SIZE = 500 20 | 21 | TRAIN_EXAMPLES_AVAILABLE = 50000 22 | TEST_EXAMPLES_AVAILABLE = 10000 23 | 24 | ################################################################################ 25 | #### FILES/ARCHIVING 26 | ################################################################################ 27 | MODEL_PATH = './attack_model/' 28 | DATA_PATH = './data/' 29 | 30 | if not os.path.exists(MODEL_PATH): 31 | os.makedirs(MODEL_PATH) 32 | 33 | if not os.path.exists(DATA_PATH): 34 | os.makedirs(DATA_PATH) 35 | 36 | 37 | 38 | ################################################################################ 39 | ### LOADING/SAVING DATA 40 | ################################################################################ 41 | 42 | def generate_data_indices(data_size, target_train_size): 43 | ## Returns indices for data sizing and sampling 44 | train_indices = np.arange(data_size) 45 | target_data_indices = np.random.choice(train_indices, target_train_size, replace = False) 46 | shadow_indices = np.setdiff1d(train_indices, target_data_indices) 47 | return target_data_indices, shadow_indices 48 | 49 | ### taken from https://github.com/csong27/membership-inference/blob/master/attack.py 50 | def load_attack_data(): 51 | ## Loads presaved training and testing datasets 52 | fname = MODEL_PATH + 'attack_train_data.npz' 53 | with np.load(fname) as f: 54 | train_x, train_y, train_classes = [f['arr_%d' % i] 55 | for i in range(len(f.files))] 56 | fname = MODEL_PATH + 'attack_test_data.npz' 57 | with np.load(fname) as f: 58 | test_x, test_y, test_classes = [f['arr_%d' % i] 59 | for i in range(len(f.files))] 60 | 61 | return train_x.astype('float32'),train_y.astype('int32'), train_classes.astype('int32'), 62 | test_x.astype('float32'), test_y.astype('int32'), test_classes.astype('int32'), 63 | 64 | 65 | 66 | ################################################################################ 67 | ### TRAINING PATHWAYS 68 | ################################################################################ 69 | 70 | def full_attack_training(): 71 | 72 | ### divide up dataset between target and shadow models 73 | ## training 74 | train_indices = list(range(TRAIN_EXAMPLES_AVAILABLE)) 75 | train_target_indices = np.random.choice(train_indices, TRAIN_SIZE, replace=False) 76 | train_shadow_indices = np.setdiff1d(train_indices, train_target_indices) 77 | ## testing 78 | test_indices = list(range(TEST_EXAMPLES_AVAILABLE)) 79 | test_target_indices = np.random.choice(test_indices, TEST_SIZE, replace=False) 80 | test_shadow_indices = np.setdiff1d(test_indices, test_target_indices) 81 | 82 | print("training target model...") 83 | attack_test_x, attack_test_y, test_classes = train_target_model( 84 | train_indices = train_target_indices, 85 | test_indices = test_target_indices, 86 | epochs = args.target_epochs, 87 | batch_size = args.target_batch_size, 88 | learning_rate = args.target_learning_rate, 89 | model = args.target_model, 90 | fc_dim_hidden = args.target_fc_dim_hidden, 91 | save = args.save_model) 92 | print("done training target model") 93 | 94 | print("training shadow models...") 95 | attack_train_x, attack_train_y, train_classes = train_shadow_models( 96 | train_indices = train_shadow_indices, 97 | test_indices = test_shadow_indices, 98 | epochs = args.target_epochs, 99 | batch_size = args.target_batch_size, 100 | learning_rate = args.target_learning_rate, 101 | n_shadow = args.n_shadow, 102 | fc_dim_hidden = args.target_fc_dim_hidden, 103 | model = args.target_model, 104 | save = args.save_model) 105 | print("done training shadow models") 106 | 107 | print("training attack model...") 108 | data = (attack_train_x, attack_train_y, train_classes,\ 109 | attack_test_x, attack_test_y, test_classes) 110 | train_attack_model( 111 | data = data, 112 | epochs = args.attack_epochs, 113 | batch_size = args.attack_batch_size, 114 | learning_rate = args.attack_learning_rate, 115 | fc_dim_hidden = args.attack_fc_dim_hidden, 116 | model = args.attack_model) 117 | print("done training attack model") 118 | 119 | 120 | 121 | def only_attack_training(): 122 | 123 | dataset = None 124 | train_attack_model( 125 | dataset = dataset, 126 | epochs = args.attack_epochs, 127 | batch_size = args.attack_batch_size, 128 | learning_rate = args.attack_learning_rate, 129 | fc_dim_hidden = args.attack_fc_dim_hidden, 130 | model = args.attack_model) 131 | 132 | 133 | 134 | ########################################################################## 135 | #### TRAINING TARGET, SHADOW, ATTACK MODELS 136 | ########################################################################## 137 | 138 | def train_target_model(train_indices, test_indices, 139 | epochs = 100, batch_size = 10, learning_rate = 0.01, 140 | fc_dim_hidden = 50, model = 'cnn', save = True): 141 | 142 | ## CIFAR image preparation 143 | transform = transforms.Compose( 144 | [transforms.ToTensor(), 145 | transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) 146 | 147 | trainset = torchvision.datasets.CIFAR10(root='./data', train = True, download = True, 148 | transform = transform) 149 | 150 | trainloader = torch.utils.data.DataLoader(trainset, batch_size = batch_size, num_workers = 2, 151 | sampler = SubsetRandomSampler(train_indices), 152 | drop_last = True) 153 | 154 | testset = torchvision.datasets.CIFAR10(root = './data', train = False, download = True, 155 | transform = transform) 156 | testloader = torch.utils.data.DataLoader(testset, batch_size = batch_size, shuffle = False, n 157 | um_workers = 2, 158 | sampler = SubsetRandomSampler(test_indices), 159 | drop_last = True) 160 | 161 | 162 | output_layer, _1, _2 = train(trainloader, testloader, 163 | fc_dim_hidden = fc_dim_hidden, epochs = epochs, 164 | learning_rate = learning_rate, batch_size = batch_size, 165 | model = model) 166 | 167 | # test data for attack model 168 | attack_x, attack_y, classes = [], [], [] 169 | 170 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 171 | with torch.no_grad(): 172 | for data in trainloader: 173 | images, labels = data[0].to(device), data[1].to(device) 174 | outputs = output_layer(images) 175 | attack_x.append(outputs.cpu()) 176 | attack_y.append(np.ones(batch_size)) 177 | classes.append(labels) 178 | 179 | for data in testloader: 180 | images, labels = data[0].to(device), data[1].to(device) 181 | outputs = output_layer(images) 182 | attack_x.append(outputs.cpu()) 183 | attack_y.append(np.zeros(batch_size)) 184 | classes.append(labels) 185 | 186 | attack_x = np.vstack(attack_x) 187 | attack_y = np.concatenate(attack_y) 188 | classes = np.concatenate([cl.cpu() for cl in classes]) 189 | 190 | attack_x = attack_x.astype('float32') 191 | attack_y = attack_y.astype('int32') 192 | classes = classes.astype('int32') 193 | 194 | if save: 195 | np.savez(MODEL_PATH + 'attack_test_data.npz', attack_x, attack_y, classes) 196 | 197 | return attack_x, attack_y, classes 198 | 199 | def train_shadow_models(train_indices, test_indices, 200 | fc_dim_hidden = 50, n_shadow = 20, model = 'cnn', 201 | epochs = 100, learning_rate = 0.05, batch_size = 10, 202 | save = True): 203 | 204 | ## CIFAR image preparation 205 | transform = transforms.Compose( 206 | [transforms.ToTensor(), 207 | transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]) 208 | 209 | trainset = torchvision.datasets.CIFAR10(root = './data', train = True, download = True, 210 | transform = transform) 211 | testset = torchvision.datasets.CIFAR10(root = './data', train = False, download = True, 212 | transform = transform) 213 | 214 | attack_x, attack_y, classes = [], [], [] 215 | for i in range(n_shadow): 216 | print('Training shadow model %d'%(i)) 217 | trainloader = torch.utils.data.DataLoader(trainset, batch_size = batch_size, num_workers = 2, 218 | sampler = SubsetRandomSampler( 219 | np.random.choice(train_indices, TRAIN_SIZE, 220 | replace = False)), 221 | drop_last = True) 222 | 223 | testloader = torch.utils.data.DataLoader(testset, batch_size = batch_size, 224 | shuffle = False, num_workers = 2, 225 | sampler = SubsetRandomSampler( 226 | np.random.choice(test_indices, 227 | round(TRAIN_SIZE * 0.3), 228 | replace = False)), 229 | drop_last = True) 230 | 231 | 232 | output_layer, _1, _2 = train(trainloader, testloader, 233 | fc_dim_hidden = fc_dim_hidden, model = model, 234 | epochs = epochs, learning_rate = learning_rate, 235 | batch_size = batch_size) 236 | 237 | attack_i_x, attack_i_y, classes_i = [], [], [] 238 | 239 | device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") 240 | with torch.no_grad(): 241 | for data in trainloader: 242 | images, labels = data[0].to(device), data[1].to(device) 243 | outputs = output_layer(images) 244 | attack_i_x.append(outputs.cpu()) 245 | # data used in training, label is 1 246 | attack_i_y.append(np.ones(batch_size)) 247 | classes_i.append(labels) 248 | 249 | 250 | for data in testloader: 251 | images, labels = data[0].to(device), data[1].to(device) 252 | outputs = output_layer(images) 253 | attack_i_x.append(outputs.cpu()) 254 | # data NOT used in training, label is 0 255 | attack_i_y.append(np.zeros(batch_size)) 256 | classes_i.append(labels) 257 | 258 | attack_x += attack_i_x 259 | attack_y += attack_i_y 260 | classes += classes_i 261 | 262 | # train data for attack model 263 | attack_x = np.vstack(attack_x) 264 | attack_y = np.concatenate(attack_y) 265 | classes = np.concatenate([cl.cpu() for cl in classes]) 266 | 267 | attack_x = attack_x.astype('float32') 268 | attack_y = attack_y.astype('int32' ) 269 | classes = classes.astype( 'int32' ) 270 | 271 | if save: 272 | np.savez(MODEL_PATH + 'attack_train_data.npz', attack_x, attack_y, classes) 273 | 274 | return attack_x, attack_y, classes 275 | 276 | def reduce_ones(x, y, classes): 277 | ## assumes more training than testing examples 278 | ## 1 as over-represented class is hardcoded in here 279 | idx_to_keep = np.where(y == 0)[0] 280 | idx_to_reduce = np.where(y == 1)[0] 281 | num_to_reduce = (y.shape[0] - idx_to_reduce.shape[0]) * 2 282 | idx_sample = np.random.choice(idx_to_reduce, num_to_reduce, replace = False) 283 | 284 | x = x[ np.concatenate([idx_to_keep, idx_sample, idx_to_keep])] 285 | y = y[ np.concatenate([idx_to_keep, idx_sample, idx_to_keep])] 286 | classes = classes[ np.concatenate([idx_to_keep, idx_sample, idx_to_keep])] 287 | 288 | return x, y, classes 289 | 290 | def train_attack_model(data = None, 291 | fc_dim_hidden = 50, model = 'fc', 292 | learning_rate = 0.01, batch_size = 10, epochs = 3): 293 | if data is None: 294 | data = load_attack_data() 295 | train_x, train_y, train_classes, test_x, test_y, test_classes = data 296 | 297 | ## balance datasets 298 | train_x, train_y, train_classes = reduce_ones(train_x, train_y, train_classes) 299 | test_x, test_y, test_classes = reduce_ones(test_x, test_y, test_classes) 300 | 301 | train_indices = np.arange(len(train_x)) 302 | test_indices = np.arange(len(test_x)) 303 | unique_classes = np.unique(train_classes) 304 | true_y = [] 305 | pred_y = [] 306 | for c in unique_classes: 307 | print('Training attack model for class %d...'%(c)) 308 | c_train_indices = train_indices[train_classes == c] 309 | c_train_x, c_train_y = train_x[c_train_indices], train_y[c_train_indices] 310 | c_test_indices = test_indices[test_classes == c] 311 | c_test_x, c_test_y = test_x[c_test_indices], test_y[c_test_indices] 312 | print("training number is %d"%c_train_x.shape[0]) 313 | print("testing number is %d"%c_test_x.shape[0]) 314 | 315 | trainloader = iterate_and_shuffle_numpy(c_train_x, c_train_y, batch_size) 316 | testloader = iterate_and_shuffle_numpy(c_test_x, c_test_y, batch_size) 317 | 318 | 319 | _, c_pred_y, c_true_y = train(trainloader, testloader, 320 | fc_dim_in = train_x.shape[1], 321 | fc_dim_out = 2, 322 | fc_dim_hidden = fc_dim_hidden, epochs = epochs, learning_rate = learning_rate, 323 | batch_size = batch_size, model = model) 324 | true_y.append(c_true_y) 325 | pred_y.append(c_pred_y) 326 | print("Accuracy score for class %d:"%c) 327 | print(accuracy_score(c_true_y, c_pred_y)) 328 | 329 | true_y = np.concatenate(true_y) 330 | pred_y = np.concatenate(pred_y) 331 | print('Final full: %0.2f'%(accuracy_score(true_y, pred_y))) 332 | print(classification_report(true_y, pred_y)) 333 | 334 | 335 | if __name__ == '__main__': 336 | parser = argparse.ArgumentParser() 337 | 338 | ## housekeeping 339 | parser.add_argument('--save_model', type = int, default = 1) 340 | parser.add_argument('--save_data', type = int, default = 0) 341 | 342 | ## target/blackbox model 343 | parser.add_argument('--target_model', type = str, default = 'cnn') 344 | parser.add_argument('--target_learning_rate', type = float, default = 0.001) 345 | parser.add_argument('--target_batch_size', type = int, default = 4) 346 | parser.add_argument('--target_fc_dim_hidden', type = int, default = 50) 347 | parser.add_argument('--target_epochs', type = int, default = 10) 348 | 349 | ## shadow models (intermediate step) 350 | parser.add_argument('--n_shadow', type = int, default = 10) 351 | 352 | # attack model 353 | parser.add_argument('--attack_model', type = str, default = 'fc') 354 | parser.add_argument('--attack_learning_rate', type = float, default = 0.001) 355 | parser.add_argument('--attack_batch_size', type = int, default = 50) 356 | parser.add_argument('--attack_fc_dim_hidden', type = int, default = 50) 357 | parser.add_argument('--attack_epochs', type = int, default = 5) 358 | 359 | # parse configuration 360 | args = parser.parse_args() 361 | print(vars(args)) 362 | if args.save_data: 363 | save_data() 364 | else: 365 | full_attack_training() 366 | # only_attack_training() 367 | 368 | -------------------------------------------------------------------------------- /Ch10/Ch10.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import matplotlib.pyplot as plt\n", 10 | "\n", 11 | "import numpy as np\n", 12 | "import pandas as pd\n", 13 | "\n", 14 | "\n", 15 | "from sklearn.svm import SVC\n", 16 | "from sklearn.pipeline import Pipeline\n", 17 | "from sklearn.model_selection import train_test_split, GridSearchCV\n", 18 | "\n", 19 | "from art.estimators.classification import SklearnClassifier\n", 20 | "from art.attacks.evasion import HopSkipJump\n", 21 | "\n", 22 | "import warnings\n", 23 | "warnings.filterwarnings('ignore')" 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Import data" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "## p 269\n", 40 | "df = pd.read_csv(\"datatraining.txt\")\n", 41 | "df[10:20]" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "X = df.iloc[:, 1:6]\n", 51 | "y = df.iloc[:, 6]" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "np.unique(y, return_counts = True)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "## max-min scale\n", 70 | "Xmin = np.min(X, axis = 0)\n", 71 | "Xmax = np.max(X, axis = 0)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "## p 270\n", 81 | "def scale(X_var):\n", 82 | " return (X_var - Xmin) / (Xmax - Xmin)\n", 83 | "\n", 84 | "def unscale(X_var):\n", 85 | " return X_var * (Xmax - Xmin) + Xmin" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": {}, 92 | "outputs": [], 93 | "source": [ 94 | "X_scaled = scale(X)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size = 0.2, random_state = 18)" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "## Build training pipeline" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "## p 271\n", 120 | "svc = SVC(C=1.0, kernel='rbf')\n", 121 | "pipeline = Pipeline(steps=[('svc', svc)])" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [ 130 | "param_grid = {'svc__C': np.logspace(-4, 4, 5)}\n", 131 | "search = GridSearchCV(estimator = pipeline, \n", 132 | " param_grid = param_grid, \n", 133 | " iid = False, \n", 134 | " cv = 5)\n", 135 | "search.fit(X_train, y_train)\n", 136 | "print(\"Best parameter (CV score=%0.3f):\" % search.best_score_)\n", 137 | "print(search.best_params_)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "accuracy_test_benign = search.score(X_test, y_test)\n", 147 | "print('Accuracy on benign test samples %0.2f'%(accuracy_test_benign * 100))" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [ 154 | "## Generate adversarial examples" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "## p 271\n", 164 | "classifier = SklearnClassifier(model=search.best_estimator_)\n", 165 | "attack = HopSkipJump(classifier = classifier, \n", 166 | " targeted = False, \n", 167 | " norm = np.inf, \n", 168 | " max_iter = 100, \n", 169 | " max_eval = 100,\n", 170 | " init_eval = 100, \n", 171 | " init_size = 100)" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": null, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "## p 272\n", 181 | "X_test_adv = attack.generate(X_test[:50])" 182 | ] 183 | }, 184 | { 185 | "cell_type": "code", 186 | "execution_count": null, 187 | "metadata": {}, 188 | "outputs": [], 189 | "source": [ 190 | "accuracy_test_adversarial = search.score(X_test_adv, y_test[:50])\n", 191 | "print('Accuracy on adversarial test samples %0.2f'%(accuracy_test_adversarial * 100))" 192 | ] 193 | }, 194 | { 195 | "cell_type": "code", 196 | "execution_count": null, 197 | "metadata": {}, 198 | "outputs": [], 199 | "source": [ 200 | "X_test_adv.shape" 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": null, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "X_test_adv = unscale(pd.DataFrame(X_test_adv, columns = X_test.columns.values))\n", 210 | "X_test_adv.head()" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": null, 216 | "metadata": {}, 217 | "outputs": [], 218 | "source": [ 219 | "X_test = unscale(X_test)" 220 | ] 221 | }, 222 | { 223 | "cell_type": "code", 224 | "execution_count": null, 225 | "metadata": {}, 226 | "outputs": [], 227 | "source": [ 228 | "## p 273\n", 229 | "plt.hist(X_test[:50].Temperature)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": null, 235 | "metadata": {}, 236 | "outputs": [], 237 | "source": [ 238 | "plt.hist(X_test_adv.Temperature)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": null, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "plt.hist(X_test.CO2)" 248 | ] 249 | }, 250 | { 251 | "cell_type": "code", 252 | "execution_count": null, 253 | "metadata": {}, 254 | "outputs": [], 255 | "source": [ 256 | "plt.hist(X_test_adv.CO2)" 257 | ] 258 | }, 259 | { 260 | "cell_type": "code", 261 | "execution_count": null, 262 | "metadata": { 263 | "scrolled": true 264 | }, 265 | "outputs": [], 266 | "source": [ 267 | "plt.scatter(X_test.Temperature[:50], X_test_adv.Temperature[:50])" 268 | ] 269 | }, 270 | { 271 | "cell_type": "code", 272 | "execution_count": null, 273 | "metadata": {}, 274 | "outputs": [], 275 | "source": [ 276 | "plt.scatter(X_test.CO2[:50], X_test_adv.CO2[:50])" 277 | ] 278 | }, 279 | { 280 | "cell_type": "code", 281 | "execution_count": null, 282 | "metadata": {}, 283 | "outputs": [], 284 | "source": [ 285 | "plt.scatter(X_test_adv.Temperature[:50], X_test_adv.CO2[:50])" 286 | ] 287 | }, 288 | { 289 | "cell_type": "code", 290 | "execution_count": null, 291 | "metadata": {}, 292 | "outputs": [], 293 | "source": [ 294 | "plt.scatter(X_test.Temperature[:50], X_test.CO2[:50])" 295 | ] 296 | } 297 | ], 298 | "metadata": { 299 | "kernelspec": { 300 | "display_name": "Python 3", 301 | "language": "python", 302 | "name": "python3" 303 | }, 304 | "language_info": { 305 | "codemirror_mode": { 306 | "name": "ipython", 307 | "version": 3 308 | }, 309 | "file_extension": ".py", 310 | "mimetype": "text/x-python", 311 | "name": "python", 312 | "nbconvert_exporter": "python", 313 | "pygments_lexer": "ipython3", 314 | "version": "3.7.5" 315 | } 316 | }, 317 | "nbformat": 4, 318 | "nbformat_minor": 2 319 | } 320 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to the code repository for Practical Fairness 2 | 3 | ### This is the code repository that pairs with Practical Fairness (published December 2020). You can contact me at Aileen A Nielsen at my gmail address. My email address is also posted in many places around the web and in the book. 4 | 5 | ### Thank you in advance for comments, corrections, and enlargments you may wish to add to the repo or recommend via direct communication. 6 | 7 | ### This book and code repo are very much a work in progress to reflect a nascent but important field, so I hope this code repo and the book can evolve with feedback from you. 8 | 9 | ### Thank you. 10 | --------------------------------------------------------------------------------