diff --git a/examples/images/images:optim_preproc.png b/examples/images/images:optim_preproc.png new file mode 100644 index 00000000..d5941984 Binary files /dev/null and b/examples/images/images:optim_preproc.png differ diff --git a/examples/images/images:optim_preproc2.png b/examples/images/images:optim_preproc2.png new file mode 100644 index 00000000..77379c7b Binary files /dev/null and b/examples/images/images:optim_preproc2.png differ diff --git a/examples/tutorial_optim_preproc.ipynb b/examples/tutorial_optim_preproc.ipynb new file mode 100644 index 00000000..46c70f6c --- /dev/null +++ b/examples/tutorial_optim_preproc.ipynb @@ -0,0 +1,490 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tutorial for Optimized Pre-Processing for Discrimination Prevention" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The goal of this tutorial is to introduce the functionality and principles of the optimized preprocessing technique of AI Fairness 360 to an interested data scientist who have a background in bias detection and mitigation but is not familiar with the technique.\n", + "\n", + "Note: For background introduction to bias and mitigation, please look up this file: tutorial_credit_scoring.ipynb" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "This tutorial consists of two parts: \n", + "**An showcase of basic functionalities of the optimized preprocessing technique** and **a brief introduction of the principles of the technique**\n", + "\n", + "We will use the adult dataset as an example and analyze how the technique could mitigate the discrimination in Race for the data. We will introduce the theoritical basis and main concepts of the technique as we walk through the example.\n", + "\n", + "## Reference\n", + "\n", + "[adult dataset from UCI](http://archive.ics.uci.edu/ml/datasets/Adult) \n", + "[Optimized Pre-Processing for Discrimination Prevention](https://papers.nips.cc/paper/6988-optimized-pre-processing-for-discrimination-prevention.pdf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Main Steps\n", + "\n", + "We will have the following steps for the tutorial:\n", + "\n", + "Step 1: Write import statements \n", + "Step 2: Set bias detection options, load dataset, and split between train and test \n", + "Step 3: Compute fairness metric on original training dataset \n", + "Step 4: Mitigate bias by transforming the original dataset \n", + "Step 5: Compute fairness metric on transformed training and testing dataset¶ \n", + "Step 6: Compare the accuracy of prediction" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step1: Write import statements\n", + "\n", + "We import several components from the aif360 package as well as other python packages. We import a custom version of the AdultDataset with certain features binned, metrics to check for bias, and classes related to the algorithm we will use to mitigate bias." + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(\"../\")\n", + "\n", + "import numpy as np\n", + "\n", + "from aif360.metrics import BinaryLabelDatasetMetric\n", + "\n", + "from aif360.algorithms.preprocessing.optim_preproc import OptimPreproc\n", + "from aif360.algorithms.preprocessing.optim_preproc_helpers.data_preproc_functions\\\n", + " import load_preproc_data_adult\n", + "from aif360.algorithms.preprocessing.optim_preproc_helpers.distortion_functions\\\n", + " import get_distortion_adult\n", + "from aif360.algorithms.preprocessing.optim_preproc_helpers.opt_tools import OptTools\n", + "\n", + "from IPython.display import Markdown, display\n", + "from tqdm import tqdm\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from sklearn.linear_model import LogisticRegression\n", + "from sklearn.preprocessing import StandardScaler\n", + "from sklearn.metrics import accuracy_score\n", + "from common_utils import compute_metrics\n", + "from aif360.metrics import ClassificationMetric" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(42)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 2: Set bias detection options, load dataset, and split between train and test\n", + "\n", + "In Step 2 we load the initial dataset, setting the protected attribute to be race. We then splits the original dataset into training and testing datasets. Finally, we set two variables (to be used in Step 3) for the privileged (1) and unprivileged (0) values for the race attribute. These are key inputs for detecting and mitigating bias." + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [], + "source": [ + "dataset_orig = load_preproc_data_adult(['race'])\n", + "\n", + "dataset_orig_train, dataset_orig_test = dataset_orig.split([0.7], shuffle=True)\n", + "\n", + "privileged_groups = [{'race': 1}] # White\n", + "unprivileged_groups = [{'race': 0}] # Not white" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 3: Compute fairness metric on original training dataset\n", + "\n", + "We will use aif360 to detect bias in the dataset. Our test is to compare the percentage of favorable results for the privileged and unprivileged groups, subtracting the former percentage from the latter. A negative value indicates less favorable outcomes for the unprivileged groups. This is implemented in the method called mean_difference on the BinaryLabelDatasetMetric class. The code below performs this check and displays the output:" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Original training dataset" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Difference in mean outcomes between unprivileged and privileged groups = -0.105374\n" + ] + } + ], + "source": [ + "metric_orig_train = BinaryLabelDatasetMetric(dataset_orig_train, \n", + " unprivileged_groups=unprivileged_groups,\n", + " privileged_groups=privileged_groups)\n", + "display(Markdown(\"#### Original training dataset\"))\n", + "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_orig_train.mean_difference())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 4: Mitigate bias by transforming the original dataset\n", + "\n", + "The previous step showed that the privileged group was getting 9.8% more positive outcomes in the training dataset. Since this is not desirable, we are going to try to mitigate this bias in the training dataset by implementing optimized preprocessing algorithms. This algorithm will transform the dataset to have more equity in positive outcomes on the protected attribute for the privileged and unprivileged groups.\n", + "\n", + "The algorithm requires some tuning parameters, which are set in the optim_options variable and passed as an argument along with some other parameters, including the 2 variables containg the unprivileged and privileged groups defined in Step 3.\n", + "\n", + "We then call the fit and transform methods to perform the transformation, producing a newly transformed training dataset (dataset_transf_train). Finally, we ensure alignment of features between the transformed and the original dataset to enable comparisons.\n", + "\n", + "**We will talk about the theoritical basis for this algorithm later.**" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimized Preprocessing: Objective converged to 0.000000\n" + ] + } + ], + "source": [ + "optim_options = {\n", + " \"distortion_fun\": get_distortion_adult,\n", + " \"epsilon\": 0.05,\n", + " \"clist\": [0.99, 1.99, 2.99],\n", + " \"dlist\": [.1, 0.05, 0]\n", + "}\n", + " \n", + "OP = OptimPreproc(OptTools, optim_options)\n", + "\n", + "OP = OP.fit(dataset_orig_train)\n", + "dataset_transf_train = OP.transform(dataset_orig_train, transform_Y=True)\n", + "\n", + "dataset_transf_train = dataset_orig_train.align_datasets(dataset_transf_train)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Basic Ideas for the optimized preprocessing algorithm\n", + "\n", + "The main idea of the optimized preprocessing algorithm is to work on a constrained optimization problem as below.\n", + "\n", + "![](images/optim_preproc.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The optimization problem has three main focuses:\n", + "\n", + "**I. Utility Preservation**\n", + "\n", + "As is represented by the objective function, we require that the distribution of $(\\hat X, \\hat Y)$ be statistically close to the original distribution of $(X, Y)$, where D is the protected variables, X is the input variables(other than D) and Y is the output and $\\hat X, \\hat Y$ implicate the corresponding transformed data. This is to ensure that a\n", + "model learned from the transformed dataset (when averaged over the protected variables D) is not\n", + "too different from one learned from the original dataset. For a given dissimilarity measure $\\Delta$ between probability distributions (e.g. KL-divergence), we require that the dissimilarity be small.\n", + "\n", + "**II. Discrimination Control**\n", + "\n", + "As is represented by the first constraint, $J(·, ·)$ denotes some distance function. Here we ask for a similar conditional probability of the target variable Y given the protected variable D as the original one.\n", + "\n", + "**III. Distortion Control**\n", + "\n", + "The mapping should satisfy distortion constraints with respect to the domain $X \\times Y$. We assume that $\\delta(x, y, x, y) = 0$ for all $(x, y) \\in X \\times Y$, else 1. These constraints restrict the mapping to reduce or avoid altogether certain large changes." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step 5: Compute fairness metric on transformed training and testing dataset¶\n", + "\n", + "Now that we have a transformed dataset, we can check how effective it was in removing bias by using the same metric we used for the original training and testing dataset in Step 3. Once again, we use the function mean_difference in the BinaryLabelDatasetMetric class:" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Transformed training dataset" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Difference in mean outcomes between unprivileged and privileged groups = -0.049406\n" + ] + } + ], + "source": [ + "metric_transf_train = BinaryLabelDatasetMetric(dataset_transf_train, \n", + " unprivileged_groups=unprivileged_groups,\n", + " privileged_groups=privileged_groups)\n", + "display(Markdown(\"#### Transformed training dataset\"))\n", + "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_transf_train.mean_difference())" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "#### Testing Dataset shape" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(14653, 18)\n" + ] + }, + { + "data": { + "text/markdown": [ + "#### Original test dataset" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Difference in mean outcomes between unprivileged and privileged groups = -0.092213\n" + ] + }, + { + "data": { + "text/markdown": [ + "#### Transformed test dataset" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Difference in mean outcomes between unprivileged and privileged groups = -0.038322\n" + ] + } + ], + "source": [ + "# Load, clean up original test data and compute metric\n", + "dataset_orig_test = dataset_transf_train.align_datasets(dataset_orig_test)\n", + "display(Markdown(\"#### Testing Dataset shape\"))\n", + "print(dataset_orig_test.features.shape)\n", + "\n", + "metric_orig_test = BinaryLabelDatasetMetric(dataset_orig_test, \n", + " unprivileged_groups=unprivileged_groups,\n", + " privileged_groups=privileged_groups)\n", + "display(Markdown(\"#### Original test dataset\"))\n", + "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_orig_test.mean_difference())\n", + "#Transform test data and compute metric\n", + "dataset_transf_test = OP.transform(dataset_orig_test, transform_Y = True)\n", + "dataset_transf_test = dataset_orig_test.align_datasets(dataset_transf_test)\n", + "\n", + "metric_transf_test = BinaryLabelDatasetMetric(dataset_transf_test, \n", + " unprivileged_groups=unprivileged_groups,\n", + " privileged_groups=privileged_groups)\n", + "display(Markdown(\"#### Transformed test dataset\"))\n", + "print(\"Difference in mean outcomes between unprivileged and privileged groups = %f\" % metric_transf_test.mean_difference())\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Step6: Compare accuracy of the prediction\n", + "\n", + "We would like to see if the preprocssing of data would hurt the performance of the model. So we trained a simple logistic model on both original and transformed training data, then compare the prediction accuracy on testing data." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accuracy on test data by logistic regression is = 0.799017\n" + ] + } + ], + "source": [ + "# Logistic regression classifier and predictions\n", + "scale_orig = StandardScaler()\n", + "X_train = scale_orig.fit_transform(dataset_orig_train.features)\n", + "y_train = dataset_orig_train.labels.ravel()\n", + "\n", + "lmod = LogisticRegression(solver='lbfgs')\n", + "lmod.fit(X_train, y_train)\n", + "\n", + "X_test = scale_orig.transform(dataset_orig_test.features)\n", + "y_test = dataset_orig_test.labels.ravel()\n", + "y_test_pred = lmod.predict(X_test)\n", + "acc_orig = np.mean(y_test_pred == y_test)\n", + "\n", + "print(\"Accuracy on original test data by logistic regression is = %f\" % acc_orig)" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Accuracy on transformed test data by logistic regression is = 0.797652\n" + ] + } + ], + "source": [ + "# Logistic regression classifier and predictions\n", + "scale_transf = StandardScaler()\n", + "X_train = scale_transf.fit_transform(dataset_transf_train.features)\n", + "y_train = dataset_transf_train.labels.ravel()\n", + "\n", + "lmod = LogisticRegression(solver='lbfgs')\n", + "lmod.fit(X_train, y_train)\n", + "\n", + "X_test = scale_transf.transform(dataset_transf_test.features)\n", + "y_test = dataset_transf_test.labels.ravel()\n", + "y_test_pred = lmod.predict(X_test)\n", + "acc_transf = np.mean(y_test_pred == y_test)\n", + "\n", + "print(\"Accuracy on transformed test data by logistic regression is = %f\" % acc_transf)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We may observe that the accuracy has decreased by approximately 0.2\\%, which is acceptable. We have applied similar workflow on different datasets and use AUC to evaluate the model performance.\n", + "\n", + "The figure below presents the relationship between discrimination and model performance.\n", + "![](images/optim_preproc2.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusions\n", + "\n", + "The optimized preprocessing provides an algorithm to mitigate potential bias of the training data and as well maintain the effectiveness of data. The algorithm is straightforward by seeing its optimization function, which consists of an objective function with two constrains, which corresponds to the three focuses of the algorithm: **Utility Preservation, Discrimination Control, and Distortion Control**. \n", + "\n", + "The algorithm works good to reduce bias on the protected variables as a preprocessing technique. However, we may still have to sacrifice some model performance as we reduce discrimination. We would suggest that users should carefully choose a proper parameter to control the level of reduce of bias. For example, in some field where human is involved along with ethical issues, we may rather sacrifice model efficiency to get an unbiased model, while in other cases we may focus on the performance of the model instead.\n", + "\n", + "Also, optimized preprocessing is just one technique that we may use to mitigate bias. Other techniques for inprocessing and postprocessing should also be considered while building the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}