{ "cells": [ { "cell_type": "code", "source": [ "#@title Copyright 2024 Google LLC. Double-click for license information.\n", "# Licensed under the Apache License, Version 2.0 (the \"License\");\n", "# you may not use this file except in compliance with the License.\n", "# You may obtain a copy of the License at\n", "#\n", "# https://www.apache.org/licenses/LICENSE-2.0\n", "#\n", "# Unless required by applicable law or agreed to in writing, software\n", "# distributed under the License is distributed on an \"AS IS\" BASIS,\n", "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", "# See the License for the specific language governing permissions and\n", "# limitations under the License." ], "metadata": { "cellView": "form", "id": "4zAh2rJSuao0" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "*Note: For compatibility with the installed libraries (see [Setup](#scrollTo=2aALNeO9Rvk7)), this exercise uses a past version of the Colab runtime.*" ], "metadata": { "id": "MMUNt0Gbfbvr" } }, { "cell_type": "markdown", "metadata": { "id": "T4r2z30vJSbA" }, "source": [ "# Colabs\n", "\n", "Machine Learning Crash Course uses Colaboratory (Colab) notebooks for all programming exercises. Colab is Google's implementation of [Jupyter Notebook](https://jupyter.org/). For more information about Colabs and how to use them, go to [Welcome to Colaboratory](https://research.google.com/colaboratory)." ] }, { "cell_type": "markdown", "metadata": { "id": "TL5y5fY9Jy_x" }, "source": [ "# Addressing Bias and Fairness Issues in ML Models\n", "\n", "This notebook uses an updated and reconstructed version of the [UCI Adult](https://archive.ics.uci.edu/dataset/2/adult) dataset called [ACSIncome](https://github.com/socialfoundations/folktables) to:\n", "\n", "* Train a model that predicts whether an individual's income is above $50,000 USD based on education, employment, and marital status, as well as *sensitive* attributes, such as age, sex, and race.\n", "* Compute commonly-identified fairness metrics to evaluate how the model performs across demographic groups.\n", "* Apply a model remediation technique to minimize the difference in error rates between demographic groups.\n", "\n", "\n", "\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "h8wtceyJj2uX" }, "source": [ "## Learning Objectives\n", "\n", " * Learn how to evaluate the performance of a trained model for fairness using [TensorFlow Model Analysis](https://www.tensorflow.org/tfx/model_analysis/get_started) and [Fairness Indicators](https://www.tensorflow.org/tfx/guide/fairness_indicators).\n", " * Address bias and fairness issues in a trained model with [TensorFlow Model Remediation](https://www.tensorflow.org/responsible_ai/model_remediation)." ] }, { "cell_type": "markdown", "metadata": { "id": "mW9OHUMHr7Jy" }, "source": [ "## Intended Use & Considerations\n", "\n", "This notebook demonstrates how to mitigate undesirable biases in a trained model. In practice, the model and the prediction task in this notebook is not realistic. Rather, the emphasis here is the approach to evaluating the performance of a trained model and minimizing error rates between demographics groups.\n", "\n", "With regard to the prediction task, there may circumstances where an individual's income may be used as a determining factor, such as obtaining a loan, acquiring insurance, applying for assistance programs, or possibly to advertise products. But in those cases, such businesses, institutions or organizations would not be looking to infer one's income using a machine learning model; they would instead obtain that information directly from the applicant, if possible, or rely on other signals to decide on the outcome.\n", "\n", "With that said, **this notebook is for educational purposes only and it is not advisable to use this model in any context outside of this exercise**." ] }, { "cell_type": "markdown", "metadata": { "id": "2aALNeO9Rvk7" }, "source": [ "## Setup\n", "To run this exercise, TensorFlow Model Analysis, TensorFlow Model Remediation and Fairness Indicators will need to be installed." ] }, { "metadata": { "id": "GSqrJLhhklhR" }, "cell_type": "markdown", "source": [ "**Note:** The following cell may take 10-15 minutes to run. If you encounter a prompt to restart the runtime after installing the necessary packages, please do so. Once the runtime has restarted, you can continue working with the subsequent cells without needing to reinstall the packages again." ] }, { "metadata": { "id": "Rqy7tAHmW6Pu" }, "cell_type": "code", "source": [ "!pip install \\\n", " tensorflow==2.15 \\\n", " tensorflow-model-remediation \\\n", " fairness-indicators==0.46.0 \\\n", " tensorflow-model-analysis==0.46.0 \\\n", " tensorflow-data-validation==1.15.1" ], "outputs": [], "execution_count": null }, { "metadata": { "id": "V1hu8K637Sio" }, "cell_type": "markdown", "source": [ "With the libraries installed, all necessary components can now be imported — including MinDiff for addressing unfair bias in models and Fairness Indicators for evaluating and improving models for fairness concerns." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "2_64QdLt2Fwo" }, "outputs": [], "source": [ "import pandas as pd\n", "import tensorflow as tf\n", "\n", "import tensorflow_model_analysis as tfma\n", "from google.protobuf import text_format\n", "\n", "from tensorflow_model_remediation import min_diff" ] }, { "cell_type": "markdown", "metadata": { "id": "JJZEgJQSjyK4" }, "source": [ "## About the ACSIncome Dataset\n", " \n", "ACSIncome is one of several datasets created by [Ding et al.](https://proceedings.neurips.cc/paper_files/paper/2021/file/32e54441e6382a7fbacbbbaf3c450059-Paper.pdf) as an alternative to [UCI Adult](https://archive.ics.uci.edu/dataset/2/adult). A few key details about ACSIncome:\n", "* The dataset contains 1,664,500 datapoints pulled from the 2018 United States–wide [American Community Survey](https://www.census.gov/programs-surveys/acs) (ACS) [Public Use Microdata Sample](https://www.census.gov/programs-surveys/acs/microdata.html) (PUMS) data sample.\n", "* All fifty US states and Puerto Rico are represented in this dataset.\n", "* Each row represents a person described by various features, including age, race, and sex, which correspond to protected categories in different domains under US anti-discrimination laws.\n", "* The dataset only includes individuals above 16 years old who worked at least 1 hour per week in the past year and had an income of at least $100 USD.\n", "\n", "For more information on the dataset and how it was created to reconstruct UCI Adult, check out the following citations:\n", "\n", "> Ding, Frances, Moritz Hardt, John Miller, and Ludwig Schmidt. \"[Retiring adult: New datasets for fair machine learning.](https://proceedings.neurips.cc/paper_files/paper/2021/hash/32e54441e6382a7fbacbbbaf3c450059-Abstract.html)\" Advances in neural information processing systems 34 (2021): 6478-6490.\n", "\n", "> Sarah Flood, Miriam King, Renae Rodgers, Steven Ruggles, and J. Robert Warren (2020). Integrated Public Use Microdata Series, Current Population Survey: Version 8.0 [dataset]. Minneapolis, MN: IPUMS. https://doi.org/10.18128/D030.V8.0\n", "\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8pWWcKN7Lmv7" }, "outputs": [], "source": [ "# Import the dataset\n", "acs_df = pd.read_csv(filepath_or_buffer=\"https://download.mlcc.google.com/mledu-datasets/acsincome_raw_2018.csv\")\n", "\n", "# Print five random rows of the pandas DataFrame.\n", "acs_df.sample(5)" ] }, { "cell_type": "markdown", "metadata": { "id": "AQ1aYynYXS3g" }, "source": [ "## Features\n", "\n", "After importing the dataset, five random samples appear in a table in the output cell. Each sample represents an individual, with each column representing an aspect of the invidiual, such as their age, occupation, place of birth, and so forth.\n", "\n", "The following table describes each feature column:\n", "\n", "| Feature | Description |\n", "| -------- | ------- |\n", "| AGEP | Age |\n", "| COW | Class of worker (government employee, self-employed, private employee) |\n", "| SCHL | Educational attainment (high school diploma, bachelor's degree, doctorate degree) |\n", "| MAR | Marital status |\n", "| OCCP | Occuptation |\n", "| POBP | Place of birth |\n", "| RELP | Relationship to householder (husband or wife, housemate or roommate, nursing home, group home, etc.) |\n", "| WKHP | Usual hours worked per week in the past 12 months |\n", "| SEX | Male or female |\n", "| RAC1P | Recorded detailed race code |\n", "| ST | US state code that represents the individual's location |\n", "| PINCP | Total person's yearly income |\n", "\n", "All of these features are represented numerically, though some of them correspond to a coded value. For example, for the `COW` (Class of worker) feature, `1.0` represents *an employee of a private for-profit company or business, or of an individual, for wages, salary, or commissions* and `2.0` represents *an employee of a private not-for-profit, tax-exempt, or charitable organization*. See [the supplemental section](https://proceedings.neurips.cc/paper_files/paper/2021/file/32e54441e6382a7fbacbbbaf3c450059-Supplemental.pdf) of [Ding et al.](https://proceedings.neurips.cc/paper_files/paper/2021/file/32e54441e6382a7fbacbbbaf3c450059-Paper.pdf) and the [ACS PUMS 2018 Data Dictionary](https://www2.census.gov/programs-surveys/acs/tech_docs/pums/data_dict/PUMS_Data_Dictionary_2018.pdf) for the full mapping of codes." ] }, { "cell_type": "markdown", "metadata": { "id": "hRd-tmUPa4tB" }, "source": [ "## Change Target Value to Binary\n", "[As stated earlier](#scrollTo=TL5y5fY9Jy_x), the task is to predict whether the annual income of a US working adult is more than $50,000. The `PINCP` (total person's yearly income) column in the dataset represents the target variable; however, the value will need to be convereted into a binary. For each sample, an individual’s target label will be `1` if `PINCP` > `50000.0`, otherwise `0`.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "N9MgdxLtDLpk" }, "outputs": [], "source": [ "LABEL_KEY = 'PINCP'\n", "LABEL_THRESHOLD = 50000.0\n", "\n", "acs_df[LABEL_KEY] = acs_df[LABEL_KEY].apply(\n", " lambda income: 1 if income > LABEL_THRESHOLD else 0)\n", "\n", "acs_df.sample(10)" ] }, { "cell_type": "markdown", "metadata": { "id": "eP3Gj4ntYqWb" }, "source": [ "## Defining Base Model\n", "For the purposes of this exercise, a simple, lightly-tuned [`keras.Model`](https://www.tensorflow.org/api_docs/python/tf/keras/Model) (using the [Functional API](https://www.tensorflow.org/guide/keras/functional) for preprocessing) will be created and serve as the base model for this exercise. Note that while the focus for this exercise is the technique involved in addressing fairness concerns, the model architecture — as taught throughout MLCC — would be thoughfully chosen and hyperparameter tuning would be performed before attempting to address any fairness concerns that might arise in the model." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "SPnLdxApoux2" }, "outputs": [], "source": [ "inputs = {}\n", "features = acs_df.copy()\n", "features.pop(LABEL_KEY)\n", "\n", "# Instantiate a Keras input node for each column in the dataset.\n", "for name, column in features.items():\n", " if name != LABEL_KEY:\n", " inputs[name] = tf.keras.Input(\n", " shape=(1,), name=name, dtype=tf.float64)\n", "\n", "# Stack the inputs as a dictionary and preprocess them.\n", "def stack_dict(inputs, fun=tf.stack):\n", " values = []\n", " for key in sorted(inputs.keys()):\n", " values.append(tf.cast(inputs[key], tf.float64))\n", "\n", " return fun(values, axis=-1)\n", "\n", "x = stack_dict(inputs, fun=tf.concat)\n", "\n", "# Collect the features from the DataFrame, stack them together and normalize\n", "# their values by passing them to the normalization layer.\n", "normalizer = tf.keras.layers.Normalization(axis=-1)\n", "normalizer.adapt(stack_dict(dict(features)))\n", "\n", "# Build the main body of the model using a normalization layer, two dense\n", "# rectified-linear layers, and a single output node for classification.\n", "x = normalizer(x)\n", "x = tf.keras.layers.Dense(64, activation='relu')(x)\n", "x = tf.keras.layers.Dense(32, activation='relu')(x)\n", "outputs = tf.keras.layers.Dense(1, activation='sigmoid')(x)\n", "\n", "# Put it all together using the Keras Functional API\n", "base_model = tf.keras.Model(inputs, outputs)" ] }, { "cell_type": "markdown", "metadata": { "id": "tWi8a1O5OnG8" }, "source": [ "## Configuring Base Model\n", "Since this is a binary classification task, computing the cross-entropy loss between true labels and predicted labels will be sufficient for this exercise." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MrFw_DQtFG7y" }, "outputs": [], "source": [ "# Define the metrics used to monitor model performance while training.\n", "METRICS = [\n", " tf.keras.metrics.BinaryAccuracy(name='accuracy'),\n", " tf.keras.metrics.AUC(name='auc'),\n", "]\n", "\n", "# Configure the model for training using a stochastic gradient descent\n", "# optimizer, cross-entropy loss between true labels and predicted labels, and\n", "# the metrics defined above to evaluate the base model during training.\n", "base_model.compile(\n", " optimizer='adam',\n", " loss=tf.keras.losses.BinaryCrossentropy(),\n", " metrics=METRICS)" ] }, { "cell_type": "markdown", "metadata": { "id": "6FyuAZ-BYRXU" }, "source": [ "## Convert to tf.data.Dataset\n", "Most of the exercises throughout MLCC use a [pandas DataFrame](https://developers.google.com/machine-learning/glossary/#pandas) directly as an input argument for [`Model.fit`](https://www.tensorflow.org/api_docs/python/tf/keras/Model#fit). But in this exercise, the dataset must be converted to [`tf.data.Dataset`](https://www.tensorflow.org/api_docs/python/tf/data/Dataset) because that's the requirement for [`MinDiffModel`](https://www.tensorflow.org/responsible_ai/model_remediation/api_docs/python/model_remediation/min_diff/keras/MinDiffModel), which will be introducted later. Fortunately, this conversion is simple to do, thanks to [`tf.convert_to_tensor`](https://www.tensorflow.org/api_docs/python/tf/convert_to_tensor).\n", "\n", "The following helper function will be useful for preparing the dataset in subsequent code cells:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jJ6CWSMJc59H" }, "outputs": [], "source": [ "# Helper function to convert a pandas DataFrame into a tf.Data.dataset object\n", "# necessary for the purposes of this exercise.\n", "def dataframe_to_dataset(dataframe):\n", " dataframe = dataframe.copy()\n", " labels = dataframe.pop(LABEL_KEY)\n", " dataset = tf.data.Dataset.from_tensor_slices(\n", " ((dict(dataframe), labels)))\n", " return dataset" ] }, { "cell_type": "markdown", "metadata": { "id": "meL30tAbcZHM" }, "source": [ "## Finalize Training Set & Train Base Model\n", "At this point, all that remains is splitting the dataset before training the base model.\n", "\n", "**NOTE:** *The following cell may take approximately 10—15 minutes to run.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "zoHosWJ1gZjd" }, "outputs": [], "source": [ "RANDOM_STATE = 200\n", "BATCH_SIZE = 100\n", "EPOCHS = 10\n", "\n", "# Use the sample() method in pandas to split the dataset into a training set\n", "# that represents 80% of the original dataset, then convert it to a\n", "# tf.data.Dataset object, and finally train the model using the\n", "# converted training set.\n", "acs_train_df = acs_df.sample(frac=0.8, random_state=RANDOM_STATE)\n", "acs_train_ds = dataframe_to_dataset(acs_train_df)\n", "acs_train_batches = acs_train_ds.batch(BATCH_SIZE)\n", "\n", "base_model.fit(acs_train_batches, epochs=EPOCHS)" ] }, { "cell_type": "markdown", "metadata": { "id": "Ksj1-ycTe9qW" }, "source": [ "## Evaluate Base Model\n", "Consistent with [Ding et al.](https://proceedings.neurips.cc/paper_files/paper/2021/file/32e54441e6382a7fbacbbbaf3c450059-Paper.pdf), the overall accuracy for the base model should be at around 80% with minimal tuning and a basic model architecture. The following code cell uses the test set to evaluate the performance of the base model:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "jmcHhuTb1bG5" }, "outputs": [], "source": [ "# Use the indices from the training set to create the test set, which represents\n", "# 20% of the original dataset; then convert it to a tf.data.Dataset object, and\n", "# evaluate the base model using the converted test set.\n", "acs_test_df = acs_df.drop(acs_train_df.index).sample(frac=1.0)\n", "acs_test_ds = dataframe_to_dataset(acs_test_df)\n", "acs_test_batches = acs_test_ds.batch(BATCH_SIZE)\n", "\n", "base_model.evaluate(acs_test_batches, batch_size=BATCH_SIZE)" ] }, { "cell_type": "markdown", "metadata": { "id": "JPqttzkuOY0g" }, "source": [ "## Evaluating for Fairness\n", "With the base model trained, now would be a good opportunity to evaluate performance across demographic groups. For ease of analysis, [Fairness Indicators](https://www.tensorflow.org/responsible_ai/fairness_indicators/guide) will be used to compute fairness metrics across demographic groups and visualize results.\n", "\n", "To begin, a column containing all the base model's predictions from the test set will be needed in order to configure Fairness Indicators. [`model.predict(test_set)`](https://www.tensorflow.org/api_docs/python/tf/keras/Model#predict) will be used to generate the output predictions." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "XwJ257tXhL8k" }, "outputs": [], "source": [ "# Generate output predictions using the test set.\n", "base_model_predictions = base_model.predict(\n", " acs_test_batches, batch_size=BATCH_SIZE)" ] }, { "cell_type": "markdown", "metadata": { "id": "vGG9x0aqnPtE" }, "source": [ "## A Note on Sensitive Attributes\n", "There are several features in ACSIncome that can be used to evaluate for fairness. For this exercise, the `SEX` attribute was chosen. This is in part to keep the config terse.\n", "\n", "The `SEX` attribute used by the [US Census Bureau surveys](https://www.census.gov/glossary/?term=Sex) to construct ACSIncome was specifically intended to capture an individual's biological sex and not gender. As such, possible ambiguity of these concepts could have tampered with their intended data collection, which could result in some misrepresentation. Ideally, there would be a separate category in such surveys that allow for an individual to express their gender identity (male, female, non-binary, agender, and so forth). But for the purposes of this exercise, the `SEX` attribute available in ACSIncome will suffice for demonstrating how to perform fairness evaluations.\n", "\n", "In practice, the recommended approach is to evaluate across any group that may be negatively impacted by the trained model and is accessible in the dataset, which can include race, ethnicity, working status, educational background, and even the US state that the individual is in.\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "g0qTAaFKnOV-" }, "outputs": [], "source": [ "SENSITIVE_ATTRIBUTE_VALUES = {1.0: \"Male\", 2.0: \"Female\"}\n", "SENSITIVE_ATTRIBUTE_KEY = 'SEX'\n", "PREDICTION_KEY = 'PRED'\n", "\n", "# Make a copy of the test set, replace sensitive attribute values with\n", "# categorial strings (for ease of visualization), and add predictions\n", "# from the test set to the copied DataFrame as a separate column.\n", "base_model_analysis = acs_test_df.copy()\n", "base_model_analysis[SENSITIVE_ATTRIBUTE_KEY].replace(\n", " SENSITIVE_ATTRIBUTE_VALUES, inplace=True)\n", "base_model_analysis[PREDICTION_KEY] = base_model_predictions\n", "\n", "# Show five random examples to ensure that it looks correct.\n", "base_model_analysis.sample(5)" ] }, { "cell_type": "markdown", "metadata": { "id": "w1mUUFJoyx9V" }, "source": [ "## Configure Fairness Indicators\n", "With a column of predictions now included in the test set, an [`eval_config`](https://www.tensorflow.org/tfx/model_analysis/api_docs/python/tfma/EvalConfig) must be created to use Fairness Indicators. This config must include: the names of the prediction and target label columns in the test set, a list of metrics to compute, and the sensitive attribute to designate how the metrics should be computed.\n", "\n", "As far as metrics goes, [there are several to choose from](https://www.tensorflow.org/tfx/model_analysis/metrics#binary_classification_metrics). For now, `ConfusionMatrixPlot` will provide everything needed to evaluate for fairness.\n", "\n", "**NOTE:** *The following cell may take 5—10 minutes to run.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "x00hquzW0wyi" }, "outputs": [], "source": [ "# Specify Fairness Indicators using eval_config.\n", "eval_config_pbtxt = \"\"\"\n", " model_specs {\n", " prediction_key: \"%s\"\n", " label_key: \"%s\" }\n", " metrics_specs {\n", " metrics { class_name: \"ExampleCount\" }\n", " metrics { class_name: \"BinaryAccuracy\" }\n", " metrics { class_name: \"AUC\" }\n", " metrics { class_name: \"ConfusionMatrixPlot\" }\n", " metrics {\n", " class_name: \"FairnessIndicators\"\n", " config: '{\"thresholds\": [0.50]}'\n", " }\n", " }\n", " slicing_specs {\n", " feature_keys: \"%s\"\n", " }\n", " slicing_specs {}\n", "\"\"\" % (PREDICTION_KEY, LABEL_KEY, SENSITIVE_ATTRIBUTE_KEY)\n", "eval_config = text_format.Parse(eval_config_pbtxt, tfma.EvalConfig())\n", "\n", "# Run TensorFlow Model Analysis.\n", "base_model_eval_result = tfma.analyze_raw_data(base_model_analysis, eval_config)" ] }, { "cell_type": "markdown", "metadata": { "id": "AbEyFfAdhmeI" }, "source": [ "### Task 1: Identify Fairness Concerns\n", "Run the code cell below and take a moment to explore the results by selecting several metrics to display on the left pane. Individual graphs for each of the metrics selected will appear in the widget to the right.\n", "\n", "For each individual graph, you should see a bar that represents the overall performance, followed by bars that correspond to a demographic group based on the sensitive attribute defined in the configuration.\n", "\n", "After looking at performance across different metrics,\n", "\n", "1. Is there a metric that performed equally well across demographic groups?\n", "2. Is there a metric that was disproportionate across demographic groups, even though its overall performance seemed promising?" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "ij4xK5xoDkV0" }, "outputs": [], "source": [ "# Render Fairness Indicators.\n", "tfma.addons.fairness.view.widget_view.render_fairness_indicator(\n", " base_model_eval_result)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "x8_LU8tw394S" }, "outputs": [], "source": [ "#@title Double-Click to View a Possible Answer { display-mode: \"form\" }\n", "\n", "# 1. The overall AUC for the base model was around 0.88, with male and female\n", "# groups performing just as well with 0.87 and 0.88, respectfully. A performance\n", "# metric like the AUC would lead one to believe that the model performs well\n", "# across groups.\n", "#\n", "# 2. However, when evaluting with respect to the false negative rate, the\n", "# results show that performance is disappropriately favoring males, with female\n", "# performance is nearly 27% worse than overall baseline. In fact, what the\n", "# graphs reveal is that males perform better than the baseline by around 16%." ] }, { "cell_type": "markdown", "metadata": { "id": "XvNTD2vx9EOn" }, "source": [ "## The Fairness Issue\n", "Using Fairness Indicators to evaluate the base model performance across `SEX`, the false negative rates (FNR) between `Male` and `Female` groups revealed disproportionate outcomes. For context, the FNR represents the percentage of positive examples (individuals earning \\$50,000 USD or more annually) that are incorrectly predicted as negative (earning less than \\$50,000 USD annually). This relates to [equality of opportunity](https://developers.google.com/machine-learning/glossary#equality-of-opportunity).\n", "\n", "In order for opportunities to be equal within a group, the goal when training the model should be to reduce the gap in the FNR between `Male` and `Female` groups. A tool like [TensorFlow Model Remediation](https://www.tensorflow.org/responsible_ai/model_remediation) can be used at training time to intervene and minimize the error rates between the groups.\n", "\n", "To set this up, a subset of the dataset must first be created with only positively labeled `Female` examples (which is referred to as the sensitive group, or the protected class to improve model performance on) and another with only positively labeled `Male` examples (which is referred to as the non-sensitive group, or any group that is not the protected class)." ] }, { "cell_type": "markdown", "metadata": { "id": "ZUcX6jv0OroC" }, "source": [ "### Task 2: Create Positively-Labeled Subsets\n", "Using the training set:\n", "\n", "\n", "1. Add a line of code that creates a subset of the training set only containing positive `Female` examples.\n", "2. Add another line of code that creates a subset of the training set but only containing positive `Male ` examples.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "DhwUgAxcDO_x" }, "outputs": [], "source": [ "sensitive_group_pos = acs_train_df[ ? ] # Replace the ? with a way to filter\n", " # positively labeled Female examples.\n", "\n", "non_sensitive_group_pos = acs_train_df[ ? ] # Replace the ? with a way to filter\n", " # positively labeled Male examples." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "Vdu6s_s_yGcI" }, "outputs": [], "source": [ "#@title Double-Click to View a Possible Answer { display-mode: \"form\" }\n", "\n", "# A pandas DataFrame offers many approaches when it comes to indexing and\n", "# selecting rows. One approach is by using boolean indexing\n", "# (e.g., df[df['col'] == value]) as demonstrated in the following code:\n", "sensitive_group_pos = acs_train_df[\n", " (acs_train_df[SENSITIVE_ATTRIBUTE_KEY] == 2.0) & (acs_train_df[LABEL_KEY] == 1)]\n", "non_sensitive_group_pos = acs_train_df[\n", " (acs_train_df[SENSITIVE_ATTRIBUTE_KEY] == 1.0) & (acs_train_df[LABEL_KEY] == 1)]\n", "\n", "# To learn more, visit: https://pandas.pydata.org/docs/user_guide/indexing.html\n", "\n", "print(len(sensitive_group_pos),\n", " 'positively labeled sensitive group examples')\n", "print(len(non_sensitive_group_pos),\n", " 'positively labeled non-sensitive group examples')" ] }, { "cell_type": "markdown", "metadata": { "id": "JAGQtLVyHsq8" }, "source": [ "## Preparing MinDiff Data\n", "With the sensitive and non-sensitive subsets defined, attempts can now be made at equalizing the distributions between them using [MinDiff](https://www.tensorflow.org/responsible_ai/model_remediation/min_diff/guide/mindiff_overview). MinDiff is one of the [TensorFlow Model Remediation](https://www.tensorflow.org/responsible_ai/model_remediation) techniques used to balance error rates (or, in this exercise, the FNR) between demographic groups (`Male` and `Female`) by penalizing distributional differences during training; hence, MinDiff, or *minimizing the differences*.\n", "\n", "As [specified earlier](#scrollTo=6FyuAZ-BYRXU) in this exercise, [`MinDiffModel`](https://www.tensorflow.org/responsible_ai/model_remediation/api_docs/python/model_remediation/min_diff/keras/MinDiffModel) requires [`tf.data.Dataset`](https://www.tensorflow.org/api_docs/python/tf/data/Dataset) as the input. The following code cell will use that same helper function to convert the subsets into `tf.data.Dataset`." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "qumF1dmJVd_3" }, "outputs": [], "source": [ "# Convert sensitive and non-sensitive subsets into tf.data.Dataset.\n", "MIN_DIFF_BATCH_SIZE = 50\n", "sensitive_group_ds = dataframe_to_dataset(sensitive_group_pos)\n", "non_sensitive_group_ds = dataframe_to_dataset(non_sensitive_group_pos)\n", "\n", "# Batch the subsets.\n", "sensitive_group_batches = sensitive_group_ds.batch(\n", " MIN_DIFF_BATCH_SIZE, drop_remainder=True)\n", "non_sensitive_group_batches = non_sensitive_group_ds.batch(\n", " MIN_DIFF_BATCH_SIZE, drop_remainder=True)" ] }, { "cell_type": "markdown", "metadata": { "id": "lg-7EXgOK52M" }, "source": [ "### Task 3: Packing the Datasets for MinDiff Model\n", "Now that the subsets are prepared, they must be packed into a single dataset along with the original training set, which will then be passed along to the `MinDiffModel` for training.\n", "\n", "To advance, add a line of code that uses [`min_diff.keras.utils.pack_min_diff_data()`](https://www.tensorflow.org/responsible_ai/model_remediation/api_docs/python/model_remediation/min_diff/keras/utils/pack_min_diff_data) to pack the original dataset, the sensitive subset, and the nonsensitive subset.\n", "\n", "**Note:** All datasets must be batched before packing them together to prevent errors downstream as a consequence of unintended input tensor shapes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "agxgBOtyLfSb" }, "outputs": [], "source": [ "acs_train_min_diff_ds = min_diff.keras.utils.pack_min_diff_data(\n", " original_dataset = ?, # Replace ? with the original training set\n", " sensitive_group_dataset = ?, # Replace ? with the sensitive subset\n", " nonsensitive_group_dataset = ?) # Replace ? with the non-sensitive subset" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "dT4b6Xn-RPdn" }, "outputs": [], "source": [ "#@title Double-Click to View a Possible Answer { display-mode: \"form\" }\n", "\n", "# All you have to do is include each of the three batched tf.data.Datasets into\n", "# the arguments: the training set, the sensitive subset (female) and\n", "# the non-sensitive subset (male).\n", "acs_train_min_diff_ds = min_diff.keras.utils.pack_min_diff_data(\n", " original_dataset = acs_train_batches,\n", " sensitive_group_dataset = sensitive_group_batches,\n", " nonsensitive_group_dataset = non_sensitive_group_batches)" ] }, { "cell_type": "markdown", "metadata": { "id": "H0I9MOIBNtsc" }, "source": [ "## Train MinDiff Model\n", "With the data preparation complete, the base model can now be wrapped in the [`MinDiffModel`](https://www.tensorflow.org/responsible_ai/model_remediation/api_docs/python/model_remediation/min_diff/keras/MinDiffModel) and compiled. Configuring the MinDiff model is no different from how the base model was configured [earlier](#scrollTo=tWi8a1O5OnG8)." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "5K0N0QcNWDyS" }, "outputs": [], "source": [ "# Wrap the original model in a MinDiffModel.\n", "min_diff_model = min_diff.keras.MinDiffModel(\n", " original_model=base_model,\n", " loss=min_diff.losses.MMDLoss(),\n", " loss_weight=1)\n", "\n", "# Compile the model after wrapping the original model.\n", "min_diff_model.compile(\n", " optimizer='adam',\n", " loss=tf.keras.losses.BinaryCrossentropy(from_logits=False),\n", " metrics=METRICS)" ] }, { "cell_type": "markdown", "metadata": { "id": "Mb1oByIMP32S" }, "source": [ "As for training, just remember to pass in the packed dataset.\n", "\n", "**NOTE:** *The following cell may take approximately 5 minutes to run.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "LAPWTBtpWEb_" }, "outputs": [], "source": [ "# Train MinDiff model using the packed dataset instead of the training set.\n", "min_diff_model.fit(acs_train_min_diff_ds, epochs=EPOCHS)" ] }, { "cell_type": "markdown", "metadata": { "id": "cjKFEMtvQKY8" }, "source": [ "## Evaluate MinDiff Model\n", "\n", "When it comes to overall performance, the results of the MinDiff model should be somewhat similar to the base model. Of course, the accuracy or AUC might slightly vary when compared to the base model." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "gnBXH4qZWV76" }, "outputs": [], "source": [ "min_diff_model.evaluate(acs_test_batches, batch_size=BATCH_SIZE)" ] }, { "cell_type": "markdown", "metadata": { "id": "sRJM9Jt4RxHe" }, "source": [ "## Evaluating for Fairness Using Remediated Model\n", "With the MinDiff model now trained, the same steps used to evaluate the base model with Fairness Indicators can be applied to the MinDiff model as well.\n", "\n", "Begin by generating the predictions — this time passing the test set as an input argument to the MinDiff model.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "avtyLfTEomzR" }, "outputs": [], "source": [ "# Generate MinDiff output predictions using the test set.\n", "min_diff_model_predictions = min_diff_model.predict(\n", " acs_test_batches, batch_size=BATCH_SIZE)" ] }, { "cell_type": "markdown", "metadata": { "id": "S_Kk52L2S7Jr" }, "source": [ "Same as before, append the predictions as a column onto the DataFrame, then pass it onto Fairness Indicators for evaluation. Note that the configuration used for the base model remains the same for the MinDiff model, which is why it is not being redefined below.\n", "\n", "**NOTE:** *The following cell may take 5—10 minutes to run.*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "8hnRYjw4F1sy" }, "outputs": [], "source": [ "# Make a copy of the test set, replace attribute with categorical values,\n", "# and add the MinDiff test set predictions to the copied DataFrame as a separate\n", "# column.\n", "min_diff_model_analysis = acs_test_df.copy()\n", "min_diff_model_analysis[SENSITIVE_ATTRIBUTE_KEY].replace(\n", " SENSITIVE_ATTRIBUTE_VALUES, inplace=True)\n", "min_diff_model_analysis[PREDICTION_KEY] = min_diff_model_predictions\n", "\n", "# Run TensorFlow Model Analysis on the MinDiff model.\n", "min_diff_model_eval_result = tfma.analyze_raw_data(\n", " min_diff_model_analysis, eval_config)" ] }, { "cell_type": "markdown", "metadata": { "id": "jwXkfG1HT-7Y" }, "source": [ "### Task 4: Reviewing MinDiff Results\n", "Run the cell below to visualize the MinDiff results. Then click on AUC and False Negative Rate in the left pane to reveal their respective graphs in the widget to the right.\n", "\n", "1. Looking at the AUC, and compared to the base model, did model performance increase or decrease as a result of penalizing the model during training for differences in error rates?\n", "2. Looking at the false negative rate, are there any noticeable differences between the MinDiff model and the base model?\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "MGUTnhHVMj4I" }, "outputs": [], "source": [ "tfma.addons.fairness.view.widget_view.render_fairness_indicator(\n", " min_diff_model_eval_result)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "id": "zr1ibvxoVTCu" }, "outputs": [], "source": [ "#@title Double-Click to View a Possible Answer\n", "# 1. Though applying MinDiff may come with some performance tradeoffs in\n", "# comparison to the original task, in this exercise, the MinDiff model performed\n", "# nearly equally as well as the base model, at least in terms of AUC. What this\n", "# is suggesting is that, in this context, MinDiff can be effective while not\n", "# worsening overall performance.\n", "#\n", "# 2. Here is where we see the MinDiff performing better than the base model. Not\n", "# only is the gap in error rates between male and female smaller, but the FNR\n", "# for female went down drastically from nearly 35% all the way to 30%, while the\n", "# overall performance still remains relatively the same (29% in MinDiff vs. 28%)" ] }, { "cell_type": "markdown", "metadata": { "id": "ACnCUYH4Xks7" }, "source": [ "## Considerations\n", "This exercise demonstrated an approachable way to train and evalute a model for fairness by expanding on key concepts taught throughout MLCC. The tasks in this notebook provided an example of how a gap in a performance metric between two demographic groups could be a signal that the model may have unfair skews.\n", "\n", "However, as discussed in other sections in MLCC, real-world production ML systems are large ecosystems — and the model is just a component of it. That means there are a lot of contributing factors (or confounds) that could affect model performance. Furthermore, there are numerous social and technical processes, known and unknown, that underpin and surround ML and AI technologies. Achieving equality on a particular metric alone does not ensure that the model itself is overall fair.\n", "\n", "In practice, there is an assumption that the features used to compare performance across sensitive attributes are readily available, or can be accurately inferred. In actuality, datasets do not often include sensitive attributes, and it is generally not a good idea to impute them.\n", "\n", "\n", "\n" ] } ], "metadata": { "colab": { "private_outputs": true, "provenance": [], "toc_visible": true, "runtime_attributes": { "runtime_version": "2025.07" }, "gpuType": "T4" }, "kernelspec": { "display_name": "Python 3", "name": "python3" }, "language_info": { "name": "python" }, "accelerator": "GPU" }, "nbformat": 4, "nbformat_minor": 0 }