Bias Audit & Final Project
Auditing CV systems for fairness and planning your capstone project
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