← Back to Course
Practical Work 6

Bias Audit & Final Project

Auditing CV systems for fairness and planning your capstone project

Duration 3 hours
Difficulty Intermediate
Session 6 - Ethics & Governance

Objectives

By the end of this practical work, you will be able to:

  • Conduct a systematic bias audit on a computer vision model
  • Create test datasets representing diverse demographic groups
  • Measure and document performance disparities across groups
  • Propose actionable mitigations for identified biases
  • Write a professional bias audit report
  • Define the scope and requirements for your final project

Prerequisites

  • Completed Practical Works 1-5
  • Python 3.8+ with required packages
  • Understanding of model evaluation metrics
  • Access to a trained image classification model (from PW4/PW5 or provided)

Install required packages:

pip install torch torchvision pillow pandas matplotlib seaborn scikit-learn

Part A: Bias Audit

Step 1: Understanding the Audit Framework

A comprehensive bias audit examines model performance across different demographic groups and conditions. We will use the following framework:

Dimension What to Test Metrics
Demographic Performance by age, gender, skin tone Accuracy, FPR, FNR per group
Environmental Lighting, background, angle variations Accuracy degradation %
Technical Image quality, resolution, compression Performance at each quality level
Intersectional Combined factors (e.g., older + darker skin) Worst-case group performance

Ethical Note: When collecting or using demographic data for testing, ensure you have proper consent and handle data responsibly. Consider using synthetic datasets or properly licensed benchmark datasets.

Step 2: Prepare the Audit Dataset

Create a structured test dataset with labeled demographic information:

# audit_dataset.py
import os
import json
import pandas as pd
from pathlib import Path
from PIL import Image
import torch
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms

class AuditDataset(Dataset):
    """Dataset for bias auditing with demographic labels."""

    def __init__(self, data_dir: str, metadata_file: str, transform=None):
        """
        Args:
            data_dir: Directory containing test images
            metadata_file: JSON file with demographic labels
            transform: Image transforms
        """
        self.data_dir = Path(data_dir)
        self.transform = transform or self._default_transform()

        # Load metadata
        with open(metadata_file, "r") as f:
            self.metadata = json.load(f)

        self.samples = list(self.metadata.keys())

    def _default_transform(self):
        return transforms.Compose([
            transforms.Resize(256),
            transforms.CenterCrop(224),
            transforms.ToTensor(),
            transforms.Normalize(
                mean=[0.485, 0.456, 0.406],
                std=[0.229, 0.224, 0.225]
            )
        ])

    def __len__(self):
        return len(self.samples)

    def __getitem__(self, idx):
        filename = self.samples[idx]
        image_path = self.data_dir / filename
        meta = self.metadata[filename]

        image = Image.open(image_path).convert("RGB")
        if self.transform:
            image = self.transform(image)

        return {
            "image": image,
            "filename": filename,
            "true_label": meta["label"],
            "age_group": meta.get("age_group", "unknown"),
            "gender": meta.get("gender", "unknown"),
            "skin_tone": meta.get("skin_tone", "unknown"),
            "lighting": meta.get("lighting", "normal"),
            "image_quality": meta.get("quality", "high")
        }


def create_sample_metadata():
    """Create example metadata structure."""
    metadata = {
        "image_001.jpg": {
            "label": "cat",
            "age_group": "n/a",
            "gender": "n/a",
            "skin_tone": "n/a",
            "lighting": "bright",
            "quality": "high"
        },
        "image_002.jpg": {
            "label": "dog",
            "age_group": "n/a",
            "gender": "n/a",
            "skin_tone": "n/a",
            "lighting": "low",
            "quality": "medium"
        }
        # Add more entries...
    }

    with open("audit_metadata.json", "w") as f:
        json.dump(metadata, f, indent=2)

    print("Sample metadata created: audit_metadata.json")


if __name__ == "__main__":
    create_sample_metadata()

Step 3: Implement the Bias Auditor

Create the main auditing class to evaluate model performance:

# bias_auditor.py
import torch
import numpy as np
import pandas as pd
from collections import defaultdict
from typing import Dict, List, Tuple
from sklearn.metrics import accuracy_score, confusion_matrix
import matplotlib.pyplot as plt
import seaborn as sns

class BiasAuditor:
    """Audit a model for performance disparities across groups."""

    def __init__(self, model, device: str = None):
        self.model = model
        self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
        self.model.to(self.device)
        self.model.eval()

        self.results = defaultdict(list)

    def run_inference(self, dataloader) -> pd.DataFrame:
        """Run inference on all samples and collect results."""
        records = []

        with torch.no_grad():
            for batch in dataloader:
                images = batch["image"].to(self.device)
                outputs = self.model(images)
                probs = torch.softmax(outputs, dim=1)
                confidences, predictions = probs.max(1)

                for i in range(len(images)):
                    records.append({
                        "filename": batch["filename"][i],
                        "true_label": batch["true_label"][i],
                        "predicted": predictions[i].item(),
                        "confidence": confidences[i].item(),
                        "age_group": batch["age_group"][i],
                        "gender": batch["gender"][i],
                        "skin_tone": batch["skin_tone"][i],
                        "lighting": batch["lighting"][i],
                        "image_quality": batch["image_quality"][i]
                    })

        return pd.DataFrame(records)

    def calculate_group_metrics(
        self,
        df: pd.DataFrame,
        group_column: str
    ) -> pd.DataFrame:
        """Calculate metrics for each group."""
        metrics = []

        for group in df[group_column].unique():
            group_df = df[df[group_column] == group]

            if len(group_df) < 5:  # Skip groups with too few samples
                continue

            correct = (group_df["true_label"] == group_df["predicted"]).sum()
            total = len(group_df)
            accuracy = correct / total if total > 0 else 0

            metrics.append({
                "group": group,
                "attribute": group_column,
                "accuracy": accuracy,
                "sample_count": total,
                "avg_confidence": group_df["confidence"].mean(),
                "min_confidence": group_df["confidence"].min()
            })

        return pd.DataFrame(metrics)

    def audit(self, dataloader) -> Dict:
        """Run full audit and return results."""
        print("Running inference...")
        df = self.run_inference(dataloader)

        # Overall metrics
        overall_accuracy = (df["true_label"] == df["predicted"]).mean()
        print(f"Overall Accuracy: {overall_accuracy:.2%}")

        # Group-wise metrics for each attribute
        attributes = ["age_group", "gender", "skin_tone", "lighting", "image_quality"]
        group_results = {}

        for attr in attributes:
            if df[attr].nunique() > 1:  # Only if multiple groups exist
                group_metrics = self.calculate_group_metrics(df, attr)
                if not group_metrics.empty:
                    group_results[attr] = group_metrics
                    print(f"\n{attr.upper()} Performance:")
                    print(group_metrics.to_string(index=False))

        # Calculate disparity metrics
        disparities = self._calculate_disparities(group_results)

        return {
            "overall_accuracy": overall_accuracy,
            "raw_results": df,
            "group_metrics": group_results,
            "disparities": disparities
        }

    def _calculate_disparities(self, group_results: Dict) -> Dict:
        """Calculate disparity metrics between groups."""
        disparities = {}

        for attr, metrics_df in group_results.items():
            if len(metrics_df) < 2:
                continue

            best_accuracy = metrics_df["accuracy"].max()
            worst_accuracy = metrics_df["accuracy"].min()
            disparity = best_accuracy - worst_accuracy

            best_group = metrics_df.loc[metrics_df["accuracy"].idxmax(), "group"]
            worst_group = metrics_df.loc[metrics_df["accuracy"].idxmin(), "group"]

            disparities[attr] = {
                "disparity": disparity,
                "best_group": best_group,
                "best_accuracy": best_accuracy,
                "worst_group": worst_group,
                "worst_accuracy": worst_accuracy,
                "ratio": worst_accuracy / best_accuracy if best_accuracy > 0 else 0
            }

        return disparities

    def generate_report(self, results: Dict, output_path: str = "audit_report"):
        """Generate audit report with visualizations."""
        # Create visualizations
        self._plot_group_comparison(results, output_path)
        self._plot_disparity_summary(results, output_path)

        # Generate text report
        self._write_text_report(results, output_path)

        print(f"\nReport generated: {output_path}/")

    def _plot_group_comparison(self, results: Dict, output_path: str):
        """Create bar charts comparing group performance."""
        os.makedirs(output_path, exist_ok=True)

        for attr, metrics_df in results["group_metrics"].items():
            fig, ax = plt.subplots(figsize=(10, 6))

            colors = ["#e74c3c" if acc < results["overall_accuracy"] * 0.9
                      else "#27ae60" for acc in metrics_df["accuracy"]]

            bars = ax.bar(metrics_df["group"], metrics_df["accuracy"], color=colors)

            # Add overall accuracy line
            ax.axhline(y=results["overall_accuracy"], color="#3498db",
                       linestyle="--", label=f'Overall: {results["overall_accuracy"]:.1%}')

            ax.set_xlabel("Group")
            ax.set_ylabel("Accuracy")
            ax.set_title(f"Model Performance by {attr.replace('_', ' ').title()}")
            ax.legend()
            ax.set_ylim(0, 1)

            # Add value labels
            for bar, acc in zip(bars, metrics_df["accuracy"]):
                ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
                        f"{acc:.1%}", ha="center", va="bottom")

            plt.tight_layout()
            plt.savefig(f"{output_path}/{attr}_comparison.png", dpi=150)
            plt.close()

    def _plot_disparity_summary(self, results: Dict, output_path: str):
        """Create summary visualization of disparities."""
        if not results["disparities"]:
            return

        fig, ax = plt.subplots(figsize=(10, 6))

        attrs = list(results["disparities"].keys())
        disparities = [results["disparities"][a]["disparity"] for a in attrs]

        colors = ["#e74c3c" if d > 0.1 else "#f39c12" if d > 0.05
                  else "#27ae60" for d in disparities]

        bars = ax.barh(attrs, disparities, color=colors)

        ax.axvline(x=0.05, color="#f39c12", linestyle="--",
                   label="Warning threshold (5%)")
        ax.axvline(x=0.1, color="#e74c3c", linestyle="--",
                   label="Critical threshold (10%)")

        ax.set_xlabel("Accuracy Disparity (Best - Worst Group)")
        ax.set_title("Performance Disparity Summary")
        ax.legend()

        plt.tight_layout()
        plt.savefig(f"{output_path}/disparity_summary.png", dpi=150)
        plt.close()

    def _write_text_report(self, results: Dict, output_path: str):
        """Write detailed text report."""
        report_lines = [
            "# Bias Audit Report",
            "",
            "## Executive Summary",
            f"- Overall Model Accuracy: {results['overall_accuracy']:.2%}",
            f"- Groups Analyzed: {len(results['group_metrics'])} attributes",
            "",
            "## Findings",
            ""
        ]

        for attr, disp in results["disparities"].items():
            severity = "CRITICAL" if disp["disparity"] > 0.1 else \
                       "WARNING" if disp["disparity"] > 0.05 else "OK"

            report_lines.extend([
                f"### {attr.replace('_', ' ').title()}",
                f"- Status: **{severity}**",
                f"- Disparity: {disp['disparity']:.2%}",
                f"- Best performing: {disp['best_group']} ({disp['best_accuracy']:.2%})",
                f"- Worst performing: {disp['worst_group']} ({disp['worst_accuracy']:.2%})",
                f"- Equity ratio: {disp['ratio']:.2%}",
                ""
            ])

        report_lines.extend([
            "## Recommendations",
            "",
            "1. **Data Collection**: Increase representation for underperforming groups",
            "2. **Data Augmentation**: Apply targeted augmentation for affected conditions",
            "3. **Model Retraining**: Consider rebalancing training data",
            "4. **Monitoring**: Implement ongoing fairness monitoring in production",
            ""
        ])

        with open(f"{output_path}/audit_report.md", "w") as f:
            f.write("\n".join(report_lines))


# Import os at the top (needed for makedirs)
import os

Step 4: Run the Audit

Execute the bias audit on your model:

# run_audit.py
import torch
from torch.utils.data import DataLoader
from torchvision import models
import torch.nn as nn

from audit_dataset import AuditDataset
from bias_auditor import BiasAuditor

def load_model(model_path: str, num_classes: int, device: str):
    """Load the trained model."""
    model = models.resnet50(weights=None)
    num_features = model.fc.in_features
    model.fc = nn.Sequential(
        nn.Dropout(0.5),
        nn.Linear(num_features, num_classes)
    )

    checkpoint = torch.load(model_path, map_location=device)
    model.load_state_dict(checkpoint["model_state_dict"])

    return model


def main():
    # Configuration
    MODEL_PATH = "models/best_model.pth"
    AUDIT_DATA_DIR = "audit_data/images"
    METADATA_FILE = "audit_data/metadata.json"
    NUM_CLASSES = 2  # Adjust based on your model

    device = "cuda" if torch.cuda.is_available() else "cpu"
    print(f"Using device: {device}")

    # Load model
    print("Loading model...")
    model = load_model(MODEL_PATH, NUM_CLASSES, device)

    # Create dataset and dataloader
    print("Loading audit dataset...")
    dataset = AuditDataset(AUDIT_DATA_DIR, METADATA_FILE)
    dataloader = DataLoader(dataset, batch_size=32, shuffle=False)

    print(f"Audit dataset size: {len(dataset)} images")

    # Run audit
    auditor = BiasAuditor(model, device)
    results = auditor.audit(dataloader)

    # Generate report
    auditor.generate_report(results, "audit_report")

    # Print summary
    print("\n" + "="*50)
    print("AUDIT SUMMARY")
    print("="*50)

    for attr, disp in results["disparities"].items():
        status = "PASS" if disp["disparity"] < 0.05 else \
                 "WARN" if disp["disparity"] < 0.1 else "FAIL"
        print(f"{attr}: {status} (disparity: {disp['disparity']:.1%})")


if __name__ == "__main__":
    main()

Step 5: Analyze and Document Findings

Based on your audit results, complete this analysis template:

Question Your Answer
Which group had the highest accuracy? [Group name and accuracy]
Which group had the lowest accuracy? [Group name and accuracy]
What is the largest disparity found? [Percentage gap and attribute]
Are any disparities above 10% (critical)? [Yes/No - list if yes]
What patterns do you observe? [Your observations]

Step 6: Propose Mitigations

For each identified bias, propose specific mitigations:

Bias Found Root Cause Hypothesis Mitigation Strategy Priority
[e.g., Low accuracy for dark images] [e.g., Training data had mostly bright images] [e.g., Add low-light augmentation, collect more samples] [High/Medium/Low]
[Your finding] [Your hypothesis] [Your strategy] [Priority]
[Your finding] [Your hypothesis] [Your strategy] [Priority]

Common mitigation strategies:

  • Data-level: Collect more representative data, use data augmentation
  • Algorithm-level: Use fairness-aware loss functions, ensemble models
  • Post-processing: Adjust thresholds per group, implement confidence filtering
  • Process-level: Add human review for borderline cases, implement feedback loops

Part B: Final Project Planning

Step 7: Define Your Project

Use this template to plan your capstone project:

PROJECT PROPOSAL TEMPLATE
========================

Project Title: _________________________________

1. PROBLEM STATEMENT
   What business problem does this solve?
   _____________________________________________
   _____________________________________________

2. TARGET USER
   Who will use this system?
   _____________________________________________

3. CV CAPABILITY REQUIRED
   [ ] Image Classification
   [ ] Object Detection
   [ ] OCR / Text Extraction
   [ ] Face Analysis
   [ ] Image Segmentation
   [ ] Visual Q&A
   [ ] Other: ______________

4. DATA REQUIREMENTS
   - What images are needed? ___________________
   - How will you collect/obtain them? __________
   - Estimated dataset size: ___________________
   - Any privacy considerations? ________________

5. TECHNICAL APPROACH
   [ ] Cloud API (Google, AWS, Azure, Claude)
   [ ] Pre-trained Model (fine-tuning)
   [ ] Custom Model (training from scratch)

   Justification: _____________________________

6. DEPLOYMENT PLAN
   [ ] Web Application
   [ ] Mobile App
   [ ] API Service
   [ ] Batch Processing
   [ ] Edge/Embedded

7. SUCCESS METRICS
   - Accuracy target: _________________________
   - Latency requirement: _____________________
   - Business KPI: ____________________________

8. ETHICAL CONSIDERATIONS
   - Potential biases: ________________________
   - Privacy concerns: ________________________
   - Mitigation plan: _________________________

9. TIMELINE (High-level phases)
   Phase 1: _________________________________
   Phase 2: _________________________________
   Phase 3: _________________________________

10. RISKS AND DEPENDENCIES
    - Key risk: _______________________________
    - Mitigation: _____________________________

Step 8: Create Project Roadmap

Break down your project into deliverables:

Deliverable Description Dependencies
D1: Data Collection [What data, how much, from where] [None / External access]
D2: Data Preparation [Cleaning, labeling, splitting] D1
D3: Model Development [Training, validation, selection] D2
D4: API/Integration [Building the interface] D3
D5: Testing & Bias Audit [Quality assurance] D4
D6: Documentation [User guide, technical docs] D5

Expected Output

After completing this practical work, you should have:

Part A - Bias Audit:

  • Audit dataset with demographic metadata
  • Complete audit results with metrics per group
  • Visualization charts (group comparison, disparity summary)
  • Written audit report (Markdown format)
  • Mitigation recommendations document

Part B - Final Project:

  • Completed project proposal template
  • Project roadmap with deliverables
  • Initial risk assessment

Deliverables

  • Audit Code: Python scripts for running the bias audit
  • Audit Report: PDF or Markdown document with findings and visualizations
  • Mitigation Plan: Prioritized list of bias mitigations
  • Project Proposal: Completed template for your final project
  • Project Roadmap: Deliverable breakdown with dependencies

Bonus Challenges

  • Challenge 1: Implement a fairness-aware loss function and compare results with standard training
  • Challenge 2: Create an automated bias monitoring dashboard using Streamlit or Gradio
  • Challenge 3: Research and implement the "Equal Opportunity" fairness constraint in your model
  • Challenge 4: Write a model card following the standard template for your trained model

Resources