{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# AI Automation for AI Fairness\n", "\n", "When AI models contribute to high-impact decisions such as whether or\n", "not someone gets a loan, we want them to be fair.\n", "Unfortunately, in current practice, AI models are often optimized\n", "primarily for accuracy, with little consideration for fairness. This\n", "notebook gives a hands-on example for how AI Automation can help build AI\n", "models that are both accurate and fair.\n", "This notebook is written for data scientists who have some familiarity\n", "with Python. No prior knowledge of AI Automation or AI Fairness is\n", "required, we will introduce the relevant concepts as we get to them.\n", "\n", "Bias in data leads to bias in models. AI models are increasingly\n", "consulted for consequential decisions about people, in domains\n", "including credit loans, hiring and retention, penal justice, medical,\n", "and more. Often, the model is trained from past decisions made by\n", "humans. If the decisions used for training were discriminatory, then\n", "your trained model will be too, unless you are careful. Being careful\n", "about bias is something you should do as a data scientist.\n", "Fortunately, you do not have to grapple with this issue alone. You\n", "can consult others about ethics. You can also ask yourself how your AI\n", "model may affect your (or your institution's) reputation. And\n", "ultimately, you must follow applicable laws and regulations.\n", "\n", "_AI Fairness_ can be measured via several metrics, and you need to\n", "select the appropriate metrics based on the circumstances. For\n", "illustration purposes, this notebook uses one particular fairness\n", "metric called _disparate impact_. Disparate impact is defined as the\n", "ratio of the rate of favorable outcome for the unprivileged group to\n", "that of the privileged group. To make this definition more concrete,\n", "consider the case where a favorable outcome means getting a loan, the\n", "unprivileged group is women, and the privileged group is men. Then if\n", "your AI model were to let women get a loan in 30% of the cases and men\n", "in 60% of the cases, the disparate impact would be 30% / 60% = 0.5,\n", "indicating a gender bias towards men. The ideal value for disparate\n", "impact is 1, and you could define fairness for this metric as a band\n", "around 1, e.g., from 0.8 to 1.25.\n", "\n", "To get the best performance out of your AI model, you must experiment\n", "with its configuration. This means searching a high-dimensional space\n", "where some options are categorical, some are continuous, and some are\n", "even conditional. No configuration is optimal for all domains let\n", "alone all metrics, and searching them all by hand is impossible. In\n", "fact, in a high-dimensional space, even exhaustively enumerating all\n", "the valid combinations soon becomes impractical. Fortunately, you can\n", "use tools to automate the search, thus making you more productive at\n", "finding good models quickly. These productivity and quality\n", "improvements become compounded when you have to do the search over.\n", "\n", "_AI Automation_ is a technology that assists data scientists in\n", "building AI models by automating some of the tedious steps. One AI\n", "automation technique is _algorithm selection_ , which automatically\n", "chooses among alternative algorithms for a particular task. Another AI\n", "automation technique is _hyperparameter tuning_ , which automatically\n", "configures the arguments of AI algorithms. You can use AI automation\n", "to optimize for a variety of metrics. This notebook shows you how to use AI\n", "automation to optimize for both accuracy and for fairness as measured\n", "by disparate impact.\n", "\n", "This [Jupyter](https://jupyter.org/)\n", "notebook uses the following open-source Python libraries. \n", "[AIF360](https://aif360.mybluemix.net/) \n", "is a collection of fairness metrics and bias mitigation algorithms.\n", "The [pandas](https://pandas.pydata.org/) and\n", "[scikit-learn](https://scikit-learn.org/) libraries support\n", "data analysis and machine learning with data structures and a\n", "comprehensive collection of AI algorithms.\n", "The [hyperopt](http://hyperopt.github.io/hyperopt/) library\n", "implements both algorithm selection and hyperparameter tuning for\n", "AI automation.\n", "And [Lale](https://github.com/IBM/lale) is a library for\n", "semi-automated data science; this notebook uses Lale as the backbone\n", "for putting the other libraries together.\n", "\n", "Our starting point is a dataset and a task. For illustration\n", "purposes, we picked [credit-g](https://www.openml.org/d/31), also\n", "known as the German Credit dataset. Each row describes a person\n", "using several features that may help evaluate them as a potential\n", "loan applicant. The task is to classify people into either\n", "good or bad credit risks. We load the version of the dataset from\n", "OpenML along with some fairness metadata." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "Possible set intersection at position 3\n" ] } ], "source": [ "from lale.lib.aif360 import fetch_creditg_df\n", "all_X, all_y, fairness_info = fetch_creditg_df()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To see what the dataset looks like, we can use off-the-shelf\n", "functionality from pandas for inspecting a few\n", "rows. The creditg dataset has a single label column, `class`, to be\n", "predicted as the outcome, which can be `good` or `bad`. Some of the\n", "feature columns are numbers, others are categoricals." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
classchecking_statusdurationcredit_historypurposecredit_amountsavings_statusemploymentinstallment_commitmentpersonal_statusother_partiesresidence_sinceproperty_magnitudeageother_payment_planshousingexisting_creditsjobnum_dependentsown_telephoneforeign_worker
0good<06.0critical/other existing creditradio/tv1169.0no known savings>=74.0male singlenone4.0real estate67.0noneown2.0skilled1.0yesyes
1bad0<=X<20048.0existing paidradio/tv5951.0<1001<=X<42.0female div/dep/marnone2.0real estate22.0noneown1.0skilled1.0noneyes
2goodno checking12.0critical/other existing crediteducation2096.0<1004<=X<72.0male singlenone3.0real estate49.0noneown1.0unskilled resident2.0noneyes
3good<042.0existing paidfurniture/equipment7882.0<1004<=X<72.0male singleguarantor4.0life insurance45.0nonefor free1.0skilled2.0noneyes
4bad<024.0delayed previouslynew car4870.0<1001<=X<43.0male singlenone4.0no known property53.0nonefor free2.0skilled2.0noneyes
..................................................................
995goodno checking12.0existing paidfurniture/equipment1736.0<1004<=X<73.0female div/dep/marnone4.0real estate31.0noneown1.0unskilled resident1.0noneyes
996good<030.0existing paidused car3857.0<1001<=X<44.0male div/sepnone4.0life insurance40.0noneown1.0high qualif/self emp/mgmt1.0yesyes
997goodno checking12.0existing paidradio/tv804.0<100>=74.0male singlenone4.0car38.0noneown1.0skilled1.0noneyes
998bad<045.0existing paidradio/tv1845.0<1001<=X<44.0male singlenone4.0no known property23.0nonefor free1.0skilled1.0yesyes
999good0<=X<20045.0critical/other existing creditused car4576.0100<=X<500unemployed3.0male singlenone4.0car27.0noneown1.0skilled1.0noneyes
\n", "

1000 rows × 21 columns

\n", "
" ], "text/plain": [ " class checking_status duration credit_history \\\n", "0 good <0 6.0 critical/other existing credit \n", "1 bad 0<=X<200 48.0 existing paid \n", "2 good no checking 12.0 critical/other existing credit \n", "3 good <0 42.0 existing paid \n", "4 bad <0 24.0 delayed previously \n", ".. ... ... ... ... \n", "995 good no checking 12.0 existing paid \n", "996 good <0 30.0 existing paid \n", "997 good no checking 12.0 existing paid \n", "998 bad <0 45.0 existing paid \n", "999 good 0<=X<200 45.0 critical/other existing credit \n", "\n", " purpose credit_amount savings_status employment \\\n", "0 radio/tv 1169.0 no known savings >=7 \n", "1 radio/tv 5951.0 <100 1<=X<4 \n", "2 education 2096.0 <100 4<=X<7 \n", "3 furniture/equipment 7882.0 <100 4<=X<7 \n", "4 new car 4870.0 <100 1<=X<4 \n", ".. ... ... ... ... \n", "995 furniture/equipment 1736.0 <100 4<=X<7 \n", "996 used car 3857.0 <100 1<=X<4 \n", "997 radio/tv 804.0 <100 >=7 \n", "998 radio/tv 1845.0 <100 1<=X<4 \n", "999 used car 4576.0 100<=X<500 unemployed \n", "\n", " installment_commitment personal_status other_parties \\\n", "0 4.0 male single none \n", "1 2.0 female div/dep/mar none \n", "2 2.0 male single none \n", "3 2.0 male single guarantor \n", "4 3.0 male single none \n", ".. ... ... ... \n", "995 3.0 female div/dep/mar none \n", "996 4.0 male div/sep none \n", "997 4.0 male single none \n", "998 4.0 male single none \n", "999 3.0 male single none \n", "\n", " residence_since property_magnitude age other_payment_plans housing \\\n", "0 4.0 real estate 67.0 none own \n", "1 2.0 real estate 22.0 none own \n", "2 3.0 real estate 49.0 none own \n", "3 4.0 life insurance 45.0 none for free \n", "4 4.0 no known property 53.0 none for free \n", ".. ... ... ... ... ... \n", "995 4.0 real estate 31.0 none own \n", "996 4.0 life insurance 40.0 none own \n", "997 4.0 car 38.0 none own \n", "998 4.0 no known property 23.0 none for free \n", "999 4.0 car 27.0 none own \n", "\n", " existing_credits job num_dependents \\\n", "0 2.0 skilled 1.0 \n", "1 1.0 skilled 1.0 \n", "2 1.0 unskilled resident 2.0 \n", "3 1.0 skilled 2.0 \n", "4 2.0 skilled 2.0 \n", ".. ... ... ... \n", "995 1.0 unskilled resident 1.0 \n", "996 1.0 high qualif/self emp/mgmt 1.0 \n", "997 1.0 skilled 1.0 \n", "998 1.0 skilled 1.0 \n", "999 1.0 skilled 1.0 \n", "\n", " own_telephone foreign_worker \n", "0 yes yes \n", "1 none yes \n", "2 none yes \n", "3 none yes \n", "4 none yes \n", ".. ... ... \n", "995 none yes \n", "996 yes yes \n", "997 none yes \n", "998 yes yes \n", "999 none yes \n", "\n", "[1000 rows x 21 columns]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import pandas as pd\n", "pd.options.display.max_columns = None\n", "pd.concat([all_y, all_X], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `fairness_info` is a JSON object that specifies metadata you\n", "need for measuring and mitigating fairness. The `favorable_labels`\n", "attribute indicates that when the `class` column contains the value\n", "`good`, that is considered a positive outcome.\n", "A _protected attribute_ is a feature that partitions the population\n", "into groups whose outcome should have parity.\n", "Values in the `personal_status` column that indicate that the indidual\n", "is `male` are considered privileged, and so are values in the\n", "`age` column that indicate that the individual is between 26 and 1000\n", "years old." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```python\n", "{\n", " \"favorable_labels\": [\"good\"],\n", " \"protected_attributes\": [\n", " {\n", " \"feature\": \"personal_status\",\n", " \"reference_group\": [\n", " \"male div/sep\", \"male mar/wid\", \"male single\",\n", " ],\n", " },\n", " {\"feature\": \"age\", \"reference_group\": [[26, 1000]]},\n", " ],\n", "}\n", "```" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import lale.pretty_print\n", "lale.pretty_print.ipython_display(fairness_info)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A best practice for any machine-learning experiments is to split\n", "the data into a training set and a hold-out set. Doing so helps \n", "detect and prevent over-fitting. The fairness information induces\n", "groups in the dataset by outcomes and by privileged groups. We\n", "want the distribution of these groups to be similar for the training\n", "set and the holdout set. Therefore, we split the data in a\n", "stratified way." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "from lale.lib.aif360 import fair_stratified_train_test_split\n", "train_X, test_X, train_y, test_y = fair_stratified_train_test_split(\n", " all_X, all_y, **fairness_info, test_size=0.33, random_state=42)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's use the `disparate_impact` metric to measure how biased the\n", "training data and the test data are. At 0.75 and 0.73, they are far\n", "from the ideal value of 1.0." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "disparate impact of training data 0.75, test data 0.73\n" ] } ], "source": [ "from lale.lib.aif360 import disparate_impact\n", "disparate_impact_scorer = disparate_impact(**fairness_info)\n", "print(\"disparate impact of training data {:.2f}, test data {:.2f}\".format(\n", " disparate_impact_scorer.score_data(X=train_X, y_pred=train_y),\n", " disparate_impact_scorer.score_data(X=test_X, y_pred=test_y)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before we look at how to train a classifier that is optimized for both\n", "accuracy and disparate impact, we will set a baseline, by training a\n", "pipeline that is only optimized for accuracy. For this purpose, we\n", "import a few algorithms from scikit-learn and Lale:\n", "`Project` picks a subset of the feature columns,\n", "`OneHotEncoder` turns categoricals into numbers,\n", "`ConcatFeatures` combines sets of feature columns,\n", "and the three interpretable classifiers `LR`, `Tree`, and `KNN`\n", "make predictions." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from lale.lib.lale import Project\n", "from sklearn.preprocessing import OneHotEncoder\n", "from lale.lib.lale import ConcatFeatures\n", "from sklearn.linear_model import LogisticRegression as LR\n", "from sklearn.tree import DecisionTreeClassifier as Tree\n", "from sklearn.neighbors import KNeighborsClassifier as KNN" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To use AI Automation, we need to define a _search space_ ,\n", "which is a set of possible machine learning pipelines and\n", "their associated hyperparameters. The following code\n", "uses Lale to define a search space." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "cluster:(root)\n", "\n", "\n", "\n", "\n", "cluster:choice\n", "\n", "\n", "Choice\n", "\n", "\n", "\n", "\n", "project_0\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "one_hot_encoder\n", "\n", "\n", "One-\n", "Hot-\n", "Encoder\n", "\n", "\n", "\n", "\n", "project_0->one_hot_encoder\n", "\n", "\n", "\n", "\n", "concat_features\n", "\n", "\n", "Concat-\n", "Features\n", "\n", "\n", "\n", "\n", "one_hot_encoder->concat_features\n", "\n", "\n", "\n", "\n", "project_1\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "project_1->concat_features\n", "\n", "\n", "\n", "\n", "lr\n", "\n", "\n", "LR\n", "\n", "\n", "\n", "\n", "concat_features->lr\n", "\n", "\n", "\n", "\n", "tree\n", "\n", "\n", "Tree\n", "\n", "\n", "\n", "\n", "knn\n", "\n", "\n", "KNN\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import lale\n", "lale.wrap_imported_operators()\n", "prep_to_numbers = (\n", " (Project(columns={\"type\": \"string\"}) >> OneHotEncoder(handle_unknown=\"ignore\"))\n", " & Project(columns={\"type\": \"number\"})\n", " ) >> ConcatFeatures\n", "planned_orig = prep_to_numbers >> (LR | Tree | KNN)\n", "planned_orig.visualize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The call to `wrap_imported_operators` augments the algorithms\n", "that were imported from scikit-learn with metadata about\n", "their hyperparameters.\n", "The Lale combinator `>>` pipes the output from one operator to\n", "the next one, creating a dataflow edge in the pipeline.\n", "The Lale combinator `&` enables multiple sub-pipelines to run\n", "on the same data.\n", "Here, `prep_to_numbers` projects string columns and one-hot encodes them;\n", "projects numeric columns and leaves them unmodified; and\n", "finally concatenates both sets of columns back together.\n", "The Lale combinator `|` indicates\n", "algorithmic choice: `(LR | Tree | KNN)` indicates that\n", "it is up to the AI Automation to decide which of the three different\n", "classifiers to use. Note that the classifiers are\n", "not configured\n", "with concrete hyperparameters, since those will be left for the\n", "AI automation to choose instead.\n", "The search space is encapsulated in the object `planned_orig`.\n", "\n", "We will use hyperopt to select the algorithms and to tune their\n", "hyperparameters. Lale provides a `Hyperopt` operator that\n", "turns a search space such as the one specified above into an\n", "optimization problem for the hyperopt tool. After 10 trials, we get back\n", "the model that performed best for the default optimization\n", "objective, which is accuracy." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "100%|██████████| 10/10 [00:32<00:00, 3.25s/trial, best loss: -0.7492859812086269]\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "cluster:(root)\n", "\n", "\n", "\n", "\n", "\n", "project_0\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "one_hot_encoder\n", "\n", "\n", "One-\n", "Hot-\n", "Encoder\n", "\n", "\n", "\n", "\n", "project_0->one_hot_encoder\n", "\n", "\n", "\n", "\n", "concat_features\n", "\n", "\n", "Concat-\n", "Features\n", "\n", "\n", "\n", "\n", "one_hot_encoder->concat_features\n", "\n", "\n", "\n", "\n", "project_1\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "project_1->concat_features\n", "\n", "\n", "\n", "\n", "lr\n", "\n", "\n", "LR\n", "\n", "\n", "\n", "\n", "concat_features->lr\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from lale.lib.lale import Hyperopt\n", "best_estimator = planned_orig.auto_configure(\n", " train_X, train_y, optimizer=Hyperopt, cv=3, max_evals=10)\n", "best_estimator.visualize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As shown by the visualization, the search found a pipeline\n", "with an LR classifier.\n", "Inspecting the hyperparameters reveals which values\n", "worked best for the 10 trials on the dataset at hand." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```python\n", "project_0 = Project(columns={\"type\": \"string\"})\n", "one_hot_encoder = OneHotEncoder(handle_unknown=\"ignore\")\n", "project_1 = Project(columns={\"type\": \"number\"})\n", "lr = LR(\n", " fit_intercept=False,\n", " intercept_scaling=0.3240599822843736,\n", " max_iter=839,\n", " solver=\"newton-cg\",\n", " tol=0.009200093064280898,\n", ")\n", "pipeline = (\n", " ((project_0 >> one_hot_encoder) & project_1) >> ConcatFeatures() >> lr\n", ")\n", "```" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "best_estimator.pretty_print(ipython_display=True, show_imports=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use the accuracy score metric from scikit-learn to measure\n", "how well the pipeline accomplishes the objective for which it\n", "was trained." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "accuracy 72.7%\n" ] } ], "source": [ "import sklearn.metrics\n", "accuracy_scorer = sklearn.metrics.make_scorer(sklearn.metrics.accuracy_score)\n", "print(f'accuracy {accuracy_scorer(best_estimator, test_X, test_y):.1%}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, we would like our model to be not just accurate but also fair.\n", "We can use the same `disparate_impact_scorer` from before to evaluate\n", "the fairness of `best_estimator`." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "disparate impact 0.76\n" ] } ], "source": [ "print(f'disparate impact {disparate_impact_scorer(best_estimator, test_X, test_y):.2f}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The model is biased, which is no surprise, since it was trained\n", "from biased data. We would prefer a\n", "model that is much more fair. The AIF360 toolkit provides several\n", "algorithms for mitigating fairness problems. One of them is\n", "`DisparateImpactRemover`, which modifies the features that are\n", "not the protected attribute in such a way that it is hard to\n", "predict the protected attribute from them. We use a Lale version\n", "of `DisparateImpactRemover` that wraps the corresponding AIF360\n", "algorithm for AI Automation. This algorithm has a hyperparameter\n", "`repair_level` that we will tune with hyperparameter optimization." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```python\n", "{\n", " \"description\": \"Repair amount from 0 = none to 1 = full.\",\n", " \"type\": \"number\",\n", " \"minimum\": 0,\n", " \"maximum\": 1,\n", " \"default\": 1,\n", "}\n", "```" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from lale.lib.aif360 import DisparateImpactRemover\n", "lale.pretty_print.ipython_display(\n", " DisparateImpactRemover.hyperparam_schema('repair_level'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We compose the bias mitigation algorithm in a pipeline with\n", "a choice of classifiers as before.\n", "In the visualization, light blue indicates trainable operators\n", "and dark blue indicates that automation must make a choice before\n", "the operators can be trained. Compared to the earlier pipeline,\n", "we pass the data preparation sub-pipeline as an argument to `DisparateImpactRemover`,\n", "since that fairness mitigator needs numerical data to work on." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "cluster:(root)\n", "\n", "\n", "\n", "\n", "cluster:disparate_impact_remover\n", "\n", "\n", "DisparateImpactRemover\n", "\n", "\n", "\n", "cluster:pipeline_0\n", "\n", "\n", "\n", "\n", "\n", "cluster:choice\n", "\n", "\n", "Choice\n", "\n", "\n", "\n", "\n", "project\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "one_hot_encoder\n", "\n", "\n", "One-\n", "Hot-\n", "Encoder\n", "\n", "\n", "\n", "\n", "project->one_hot_encoder\n", "\n", "\n", "\n", "\n", "concat_features\n", "\n", "\n", "Concat-\n", "Features\n", "\n", "\n", "\n", "\n", "one_hot_encoder->concat_features\n", "\n", "\n", "\n", "\n", "project_0\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "project_0->concat_features\n", "\n", "\n", "\n", "\n", "lr\n", "\n", "\n", "LR\n", "\n", "\n", "\n", "\n", "concat_features->lr\n", "\n", "\n", "\n", "\n", "tree\n", "\n", "\n", "Tree\n", "\n", "\n", "\n", "\n", "knn\n", "\n", "\n", "KNN\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "di_remover = DisparateImpactRemover(\n", " **fairness_info, preparation=prep_to_numbers)\n", "planned_fairer = di_remover >> (LR | Tree | KNN)\n", "planned_fairer.visualize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Besides changing the planned pipeline to use a fairness mitigation\n", "operator, we should also change the optimization objective. We need\n", "a scoring function that blends accuracy with disparate impact.\n", "While you could define this scorer yourself, Lale also provides a\n", "pre-defined version." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "from lale.lib.aif360 import accuracy_and_disparate_impact\n", "combined_scorer = accuracy_and_disparate_impact(**fairness_info)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Fairness metrics can be more unstable than accuracy, because they depend\n", "not just on the distribution of labels, but also on the distribution of\n", "privileged and unprivileged groups as defined by the protected attributes.\n", "In AI Automation, k-fold cross validation helps reduce overfitting.\n", "To get more stable results, we will stratify these k folds by both labels\n", "and groups." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "from lale.lib.aif360 import FairStratifiedKFold\n", "fair_cv = FairStratifiedKFold(**fairness_info, n_splits=3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we have all the pieces in place to use AI Automation\n", "on our `planned_fairer` pipeline for both accuracy and\n", "disparate impact." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "100%|██████████| 10/10 [01:23<00:00, 8.39s/trial, best loss: 0.15000066730728168]\n" ] } ], "source": [ "trained_fairer = planned_fairer.auto_configure(\n", " train_X, train_y, optimizer=Hyperopt, cv=fair_cv,\n", " max_evals=10, scoring=combined_scorer, best_score=1.0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As with any trained model, we can evaluate and visualize the result." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "accuracy 71.2%\n", "disparate impact 1.00\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", "\n", "\n", "\n", "cluster:(root)\n", "\n", "\n", "\n", "\n", "cluster:disparate_impact_remover\n", "\n", "\n", "DisparateImpactRemover\n", "\n", "\n", "\n", "cluster:pipeline_0\n", "\n", "\n", "\n", "\n", "\n", "\n", "project\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "one_hot_encoder\n", "\n", "\n", "One-\n", "Hot-\n", "Encoder\n", "\n", "\n", "\n", "\n", "project->one_hot_encoder\n", "\n", "\n", "\n", "\n", "concat_features\n", "\n", "\n", "Concat-\n", "Features\n", "\n", "\n", "\n", "\n", "one_hot_encoder->concat_features\n", "\n", "\n", "\n", "\n", "project_0\n", "\n", "\n", "Project\n", "\n", "\n", "\n", "\n", "project_0->concat_features\n", "\n", "\n", "\n", "\n", "knn\n", "\n", "\n", "KNN\n", "\n", "\n", "\n", "\n", "concat_features->knn\n", "\n", "\n", "\n", "\n", "\n" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "print(f'accuracy {accuracy_scorer(trained_fairer, test_X, test_y):.1%}')\n", "print(f'disparate impact {disparate_impact_scorer(trained_fairer, test_X, test_y):.2f}')\n", "trained_fairer.visualize()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As the result demonstrates, the best model found by AI Automation\n", "has similar accuracy and better disparate impact than the one we saw\n", "before. Also, it has tuned the repair level and\n", "has picked and tuned a classifier." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/markdown": [ "```python\n", "project = Project(columns={\"type\": \"string\"})\n", "one_hot_encoder = OneHotEncoder(handle_unknown=\"ignore\")\n", "project_0 = Project(columns={\"type\": \"number\"})\n", "disparate_impact_remover = DisparateImpactRemover(\n", " favorable_labels=[\"good\"],\n", " protected_attributes=[\n", " {\n", " \"reference_group\": [\n", " \"male div/sep\", \"male mar/wid\", \"male single\",\n", " ],\n", " \"name\": \"lale.lib.aif360.disparate_impact_remover.DisparateImpactRemover\",\n", " \"feature\": \"personal_status\",\n", " },\n", " {\n", " \"feature\": \"age\",\n", " \"reference_group\": [[26, 1000]],\n", " \"name\": \"lale.lib.aif360.disparate_impact_remover.DisparateImpactRemover\",\n", " },\n", " ],\n", " preparation=((project >> one_hot_encoder) & project_0)\n", " >> ConcatFeatures(),\n", " repair_level=0.6701479482689345,\n", ")\n", "knn = KNN(algorithm=\"ball_tree\", metric=\"manhattan\", n_neighbors=93)\n", "pipeline = disparate_impact_remover >> knn\n", "```" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "trained_fairer.pretty_print(ipython_display=True, show_imports=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These results may vary by dataset and search space.\n", "\n", "In summary, this blog post showed you how to use AI Automation\n", "from Lale, while incorporating a fairness mitigation technique\n", "into the pipeline and a fairness metric into the objective.\n", "Of course, this blog post only scratches the surface of what can\n", "be done with AI Automation and AI Fairness. We encourage you to\n", "check out the open-source projects Lale and AIF360 and use them\n", "to build your own fair and accurate models!\n", "\n", "- Lale: https://github.com/IBM/lale\n", "- AIF360: https://aif360.mybluemix.net/\n", "- API documentation: [lale.lib.aif360](https://lale.readthedocs.io/en/latest/modules/lale.lib.aif360.html#module-lale.lib.aif360)" ] } ], "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.3" } }, "nbformat": 4, "nbformat_minor": 4 }