Introduction
This page demonstrates the core functionality of CounterfactualTraining.jl: training models that produce more plausible and actionable counterfactual explanations by holding them directly accountable during training.
Mutability Protection
When generating counterfactual explanations, some features may be immutable (e.g., age, race) while others are mutable (e.g., debt, income). Without protection, the contrastive divergence penalty can inadvertently make models more sensitive to immutable features, which is undesirable.
The key insight is that the contrastive divergence penalty gradient with respect to coefficient θ_{y⁺,d} is:
∂/∂θ_{y⁺,d} div(x⁺, x′, y; θ) = x′_d − x⁺_d
Since immutable features tend to differ between classes, the penalty would exacerbate the model’s existing sensitivity to those features. By protecting (zeroing gradients on) immutable features during counterfactual generation, the model learns to be relatively more sensitive to mutable features — producing counterfactuals that are actionable rather than relying on changes to features that cannot actually be changed.
Domain constraints are also applied to keep counterfactuals within plausible ranges for each feature.
Setup
using CounterfactualTraining
using CounterfactualTraining.Native
using CounterfactualExplanations
using Flux
using Plots
using Random
Random.seed!(42)
gr()Data
We create a 2D synthetic dataset with two Gaussian clusters. Feature 1 (x-axis) is mutable (:both), while feature 2 (y-axis) is immutable (:none).
N = 150
X1 = randn(Float32, 2, N) # Class 1: centered at origin
X2 = randn(Float32, 2, N) .+ 3.0f0 # Class 2: shifted by +3
X = hcat(X1, X2)
y = vcat(fill(1, N), fill(2, N))
domain = [(-3.0f0, 6.0f0), (-3.0f0, 6.0f0)]
mutability = [:both, :none]
data = CounterfactualData(X, y; domain=domain, mutability=mutability)
y_onehot = Flux.onehotbatch(y, 1:2)
train_set = Flux.DataLoader((X, y_onehot); batchsize=32, shuffle=true)Training
We train two identical models with different objectives:
- Vanilla objective (
VanillaObjective): standard cross-entropy training, no counterfactual penalty. - Full objective (
FullObjective): counterfactual training with energy differential, regularization, and adversarial loss.
Both use NativeGenerator for fast, batched counterfactual generation, and pass mutability=[:both, :none] to protect the immutable feature.
generator = NativeGenerator()
# Model A: Vanilla (no CF penalty)
model_vanilla = Chain(Dense(2, 8, relu), Dense(8, 2))
opt_vanilla = Flux.setup(Flux.Adam(1e-2), model_vanilla)
model_vanilla, log_vanilla = counterfactual_training(
VanillaObjective(needs_ce=true), model_vanilla, generator, train_set, opt_vanilla;
nepochs=20, maxiter=10, burnin=0.0f0, mutability=mutability, domain=domain, verbose=0
)
# Model B: Full (with CF penalty)
model_full = Chain(Dense(2, 8, relu), Dense(8, 2))
opt_full = Flux.setup(Flux.Adam(1e-2), model_full)
model_full, log_full = counterfactual_training(
FullObjective(), model_full, generator, train_set, opt_full;
nepochs=20, maxiter=10, burnin=0.0f0, mutability=mutability, domain=domain, verbose=0
)Counterfactual Generation
We generate counterfactuals for 20 samples from class 1, targeting class 2, using each trained model.
X_test = X[:, 1:20]
targets = fill(2, 20)
cfs_vanilla, _, _, _ = generate_counterfactuals!(
model_vanilla, X_test, targets, data, generator
)
cfs_full, _, _, _ = generate_counterfactuals!(
model_full, X_test, targets, data, generator
)Results
scatter(X[1, 1:N], X[2, 1:N], label="Class 1", color=:blue, ms=3)
scatter!(X[1, N+1:end], X[2, N+1:end], label="Class 2", color=:red, ms=3)
scatter!(cfs_vanilla[1, :], cfs_vanilla[2, :], label="CF (Vanilla)",
color=:green, markershape=:utriangle, ms=5)
scatter!(cfs_full[1, :], cfs_full[2, :], label="CF (Full)",
color=:purple, markershape=:star5, ms=5)
plot!(xlabel="Feature 1 (mutable)", ylabel="Feature 2 (immutable)")With the FullObjective, counterfactuals should move primarily along the mutable feature (x-axis), leaving the immutable feature (y-axis) relatively unchanged. The model has learned to be less sensitive to the immutable feature, producing counterfactuals that are more actionable.