Skip to content

CEGP

Counterfactual Explanations via Genetic Programming

CEGP uses evolutionary algorithms to search for counterfactual explanations.

Overview

CEGP applies genetic programming to evolve counterfactual candidates through mutation and crossover operations.

Usage

from counterfactuals.cf_methods.local_methods import CEGP

method = CEGP(
    gen_model=gen_model,
    disc_model=classifier,
    disc_model_criterion=criterion,
    device="cuda"
)

result = method.explain(
    X=instance,
    y_origin=0,
    y_target=1,
    X_train=X_train,
    y_train=y_train
)

API Reference

CEGP

CEGP(disc_model, beta=0.01, c_init=1.0, c_steps=5, max_iterations=500, device=None, **kwargs)

Bases: BaseCounterfactualMethod, LocalCounterfactualMixin

Parameters:

Name Type Description Default
disc_model PytorchBase

Discriminative model to use for counterfactual generation

required
beta float

Trade-off parameter for distance computation

0.01
c_init float

Initial value of c for the attack loss term

1.0
c_steps int

Number of steps to adjust c

5
max_iterations int

Maximum number of iterations to run optimization

500
Source code in counterfactuals/cf_methods/local_methods/cegp/cegp.py
def __init__(
    self,
    disc_model: PytorchBase,
    beta: float = 0.01,
    c_init: float = 1.0,
    c_steps: int = 5,
    max_iterations: int = 500,
    device: str | None = None,
    **kwargs,  # ignore other arguments
) -> None:
    """Initialize CEGP counterfactual method.

    Args:
        disc_model: Discriminative model to use for counterfactual generation
        beta: Trade-off parameter for distance computation
        c_init: Initial value of c for the attack loss term
        c_steps: Number of steps to adjust c
        max_iterations: Maximum number of iterations to run optimization
    """
    # Initialize base/mixin (moves model to device if applicable)
    super().__init__(disc_model=disc_model, device=device)

    tf.compat.v1.disable_eager_execution()
    predict_proba = lambda x: disc_model.predict_proba(x).numpy()  # noqa: E731
    num_features = disc_model.input_size
    shape = (1, num_features)

    # Get feature ranges from training data
    feature_range = (0, 1)  # Default range, should be adjusted based on data

    self.cf = CounterFactualProto(
        predict_proba,
        shape,
        beta=beta,
        max_iterations=max_iterations,
        feature_range=feature_range,
        c_init=c_init,
        c_steps=c_steps,
    )

    self.is_fitted = False

fit

fit(X_train)

Fit the CEGP model on training data.

Parameters:

Name Type Description Default
X_train ndarray

Training data to fit the model on

required
Source code in counterfactuals/cf_methods/local_methods/cegp/cegp.py
def fit(self, X_train: np.ndarray) -> None:
    """Fit the CEGP model on training data.

    Args:
        X_train: Training data to fit the model on
    """
    self.cf.fit(X_train.astype(np.float32), d_type="abdm", disc_perc=[25, 50, 75])
    self.is_fitted = True

explain

explain(X, y_origin, y_target, X_train=None, y_train=None, **kwargs)

Generate counterfactual explanations for given samples.

Parameters:

Name Type Description Default
X ndarray

Samples to explain

required
y_origin ndarray

Original labels

required
y_target ndarray

Target labels

required
X_train ndarray | None

Training data (used for fitting if not already fitted)

None
y_train ndarray | None

Training labels

None
Source code in counterfactuals/cf_methods/local_methods/cegp/cegp.py
def explain(
    self,
    X: np.ndarray,
    y_origin: np.ndarray,
    y_target: np.ndarray,
    X_train: np.ndarray | None = None,
    y_train: np.ndarray | None = None,
    **kwargs,
) -> ExplanationResult:
    """Generate counterfactual explanations for given samples.

    Args:
        X: Samples to explain
        y_origin: Original labels
        y_target: Target labels
        X_train: Training data (used for fitting if not already fitted)
        y_train: Training labels
    """
    if X_train is not None and not self.is_fitted:
        self.fit(X_train)

    try:
        X_in = X.copy()
        X_proc = X.reshape((1,) + X.shape)
        explanation = self.cf.explain(X_proc).cf
        if explanation is None:
            raise ValueError("No counterfactual found")
        x_cfs = np.array(explanation.get("X"))
    except Exception as e:
        print(f"Error in CEGP explanation: {e}")
        x_cfs = np.full_like(X.reshape(1, -1), np.nan).squeeze()

    # Wrap results in ExplanationResult
    return ExplanationResult(
        x_cfs=np.array(x_cfs),
        y_cf_targets=np.array(y_target),
        x_origs=np.array(X_in),
        y_origs=np.array(y_origin),
        logs=None,
    )

explain_dataloader

explain_dataloader(dataloader, target_class, *args, **kwargs)

Generate counterfactual explanations for all samples in dataloader.

Parameters:

Name Type Description Default
dataloader DataLoader

DataLoader containing samples to explain

required
target_class int

Target class for counterfactuals

required
Source code in counterfactuals/cf_methods/local_methods/cegp/cegp.py
def explain_dataloader(
    self, dataloader: DataLoader, target_class: int, *args, **kwargs
) -> ExplanationResult:
    """Generate counterfactual explanations for all samples in dataloader.

    Args:
        dataloader: DataLoader containing samples to explain
        target_class: Target class for counterfactuals
    """
    Xs, ys = dataloader.dataset.tensors

    # Fit on first batch if not already fitted
    if not self.is_fitted:
        self.fit(Xs.numpy())

    # create ys_target numpy array same shape as ys but with target class
    ys_target = np.full(ys.shape, target_class)
    Xs_cfs = []
    model_returned = []

    for X, y in tqdm(zip(Xs, ys), total=len(Xs)):
        try:
            X = X.reshape((1,) + X.shape)
            explanation = self.cf.explain(X).cf
            if explanation is None:
                raise ValueError("No counterfactual found")
            Xs_cfs.append(explanation["X"])
            model_returned.append(True)
        except Exception as e:
            print(f"Error in CEGP explanation: {e}")
            explanation = np.empty_like(X.reshape(1, -1))
            explanation[:] = np.nan
            Xs_cfs.append(explanation)
            model_returned.append(False)

    Xs_cfs = np.array(Xs_cfs).squeeze()
    Xs = np.array(Xs)
    ys = np.array(ys)
    ys_target = np.array(ys_target)

    return ExplanationResult(
        x_cfs=Xs_cfs,
        y_cf_targets=ys_target,
        x_origs=Xs,
        y_origs=ys,
        logs={"model_returned": model_returned},
    )