Skip to content

Commit

Permalink
Computation of counterfactuals of sklearn pipelines supports mathemat…
Browse files Browse the repository at this point in the history
…ical programs
  • Loading branch information
andreArtelt committed May 12, 2020
1 parent 7158ef7 commit f5b9a39
Show file tree
Hide file tree
Showing 14 changed files with 250 additions and 52 deletions.
1 change: 1 addition & 0 deletions ceml/backend/jax/preprocessing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
from .normalizer import *
from .pca import *
from .polynomial_features import *
from .affine_preprocessing import *
21 changes: 21 additions & 0 deletions ceml/backend/jax/preprocessing/affine_preprocessing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import numpy as np
from functools import reduce


class AffinePreprocessing():
"""
Wrapper for an affine mapping (preprocessing)
"""
def __init__(self, A, b):
self.A = A
self.b = b


def concatenate_affine_mappings(mappings):
A = reduce(np.matmul, [m.A for m in mappings])
b = mappings[-1].b
if len(mappings) > 1:
b += reduce(np.matmul, [np.dot(mappings[i].A, mappings[i-1].b) for i in range(1, len(mappings))])

return A, b
12 changes: 8 additions & 4 deletions ceml/backend/jax/preprocessing/pca.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# -*- coding: utf-8 -*-
import jax.numpy as npx
import numpy as np
from ....model import Model
from .affine_preprocessing import AffinePreprocessing


class PCA(Model):
class PCA(Model, AffinePreprocessing):
"""
Wrapper for PCA - Principle component analysis.
"""
def __init__(self, w):
Model.__init__(self)

self.w = w
super(PCA, self).__init__()

AffinePreprocessing.__init__(self, self.w, np.zeros(self.w.shape[0]))

def predict(self, x):
"""
Computes the forward pass.
Expand Down
19 changes: 13 additions & 6 deletions ceml/backend/jax/preprocessing/scaler.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# -*- coding: utf-8 -*-
import numpy as np
from ....model import Model
from .affine_preprocessing import AffinePreprocessing


class StandardScaler(Model):
class StandardScaler(Model, AffinePreprocessing):
"""
Wrapper for the standard scaler.
"""
def __init__(self, mu, sigma):
Model.__init__(self)

self.mu = mu
self.sigma = sigma

super(StandardScaler, self).__init__()

A = np.diag(1. / self.sigma)
AffinePreprocessing.__init__(self, A, -1. * A @ self.mu)

def predict(self, x):
"""
Expand All @@ -19,16 +24,18 @@ def predict(self, x):
return (x - self.mu) / self.sigma


class MinMaxScaler(Model):
class MinMaxScaler(Model, AffinePreprocessing):
"""
Wrapper for the min max scaler.
"""
def __init__(self, min_, scale):
Model.__init__(self)

self.min = min_
self.scale = scale

super(MinMaxScaler, self).__init__()

AffinePreprocessing.__init__(self, np.diag(self.scale), self.min)

def predict(self, x):
"""
Computes the forward pass.
Expand Down
4 changes: 2 additions & 2 deletions ceml/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Model(ABC):
The class :class:`Model` can not be instantiated because it contains an abstract method.
"""
def __init__(self):
super().__init__()
pass

def __call__(self, x):
return self.predict(x)
Expand All @@ -36,7 +36,7 @@ class ModelWithLoss(Model):
The class :class:`ModelWithLoss` can not be instantiated because it contains an abstract method.
"""
def __init__(self):
super(ModelWithLoss, self).__init__()
pass

@abstractmethod
def get_loss(self, y_target, pred=None):
Expand Down
12 changes: 12 additions & 0 deletions ceml/optim/cvx.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,21 @@ class ConvexQuadraticProgram(ABC):
"""
def __init__(self):
self.epsilon = 1e-2
self.A = None
self.b = None

super().__init__()

def set_affine_preprocessing(self, A, b):
self.A = A
self.b = b

def _apply_affine_preprocessing(self, var_x):
if self.A is not None and self.b is not None:
return self.A @ var_x + self.b
else:
return var_x

@abstractmethod
def _build_constraints(self, var_x, y):
"""Creates and returns all constraints.
Expand Down
6 changes: 5 additions & 1 deletion ceml/sklearn/counterfactual.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ class SklearnCounterfactual(Counterfactual, ABC):
def __init__(self, model):
self.model = model
self.mymodel = self.rebuild_model(model)
self.model_predict = self.model.predict

super(SklearnCounterfactual, self).__init__()

def _model_predict(self, x):
return self.model_predict(x)

@abstractmethod
def rebuild_model(self, model):
"""Rebuilds a `sklearn` model.
Expand Down Expand Up @@ -82,7 +86,7 @@ def compute_counterfactual_ex(self, x, loss, x0, loss_grad, optimizer, input_wra
solver = prepare_optim(optimizer, loss, x0, loss_grad)

x_cf = input_wrapper(solver())
y_cf = self.model.predict([x_cf])[0]
y_cf = self._model_predict([x_cf])[0]
delta = x - x_cf

if return_as_dict is True:
Expand Down
12 changes: 8 additions & 4 deletions ceml/sklearn/lda.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,30 +129,34 @@ def rebuild_model(self, model):
def _build_constraints(self, var_x, y):
constraints = []

# If set, a apply an affine preprocessing to x
var_x_ = self._apply_affine_preprocessing(var_x)

# Build constraints
i = y
q_i = np.dot(self.mymodel.sigma_inv, self.mymodel.means[i].T)
b_i = np.log(self.mymodel.class_priors[i]) - .5 * np.dot( self.mymodel.means[i], np.dot(self.mymodel.sigma_inv, self.mymodel.means[i].T))

for j in range(len(self.mymodel.means)):
for j in range(len(self.mymodel.means)): # One vs. One
if i == j:
continue

q_j = np.dot(self.mymodel.sigma_inv, self.mymodel.means[j])
b_j = np.log(self.mymodel.class_priors[j]) - .5 * np.dot(self.mymodel.means[j], np.dot(self.mymodel.sigma_inv, self.mymodel.means[j]))

constraints.append(q_i.T @ var_x + b_i >= q_j.T @ var_x + b_j + self.epsilon)
constraints.append(q_i.T @ var_x_ + b_i >= q_j.T @ var_x_ + b_j + self.epsilon)

return constraints

def solve(self, x_orig, y_target, regularization, features_whitelist, return_as_dict):
mad = None
if regularization == "l1":
mad = np.ones(self.mymodel.dim)
mad = np.ones(x_orig.shape[0])

xcf = self.build_solve_opt(x_orig, y_target, features_whitelist, mad=mad)
delta = x_orig - xcf

if self.model.predict([xcf]) != y_target:
if self._model_predict([xcf]) != y_target:
raise Exception("No counterfactual found - Consider changing parameters 'regularization', 'features_whitelist', 'optimizer' and try again")

if return_as_dict is True:
Expand Down
11 changes: 7 additions & 4 deletions ceml/sklearn/linearregression.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,22 @@ def rebuild_model(self, model):
def _build_constraints(self, var_x, y):
constraints = []

constraints.append(self.mymodel.w @ var_x + self.mymodel.b - y <= self.epsilon)
constraints.append(-self.mymodel.w @ var_x - self.mymodel.b + y <= self.epsilon)
# If set, a apply an affine preprocessing to x
var_x_ = self._apply_affine_preprocessing(var_x)

# Build box constraints
constraints.append(self.mymodel.w @ var_x_ + self.mymodel.b - y <= self.epsilon)
constraints.append(-self.mymodel.w @ var_x_ - self.mymodel.b + y <= self.epsilon)

return constraints

def solve(self, x_orig, y_target, regularization, features_whitelist, return_as_dict):
mad = None
if regularization == "l1":
mad = np.ones(self.mymodel.dim)
mad = np.ones(x_orig.shape[0])

xcf = self.build_solve_opt(x_orig, y_target, features_whitelist, mad=mad)
delta = x_orig - xcf
y_cf = self.model.predict([xcf])

if return_as_dict is True:
return self._SklearnCounterfactual__build_result_dict(xcf, y_target, delta)
Expand Down
2 changes: 1 addition & 1 deletion ceml/sklearn/lvq.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def solve(self, x_orig, y_target, regularization, features_whitelist, return_as_
xcf = self._compute_counterfactual_via_convex_quadratic_programming(x_orig, y_target, features_whitelist, regularization)
delta = x_orig - xcf

if self.model.predict([xcf]) != y_target:
if self._model_predict([xcf]) != y_target:
raise Exception("No counterfactual found - Consider changing parameters 'regularization', 'features_whitelist', 'optimizer' and try again")

if return_as_dict is True:
Expand Down
2 changes: 1 addition & 1 deletion ceml/sklearn/naivebayes.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def solve(self, x_orig, y_target, regularization, features_whitelist, return_as_
xcf = self._build_solve_dcqp(x_orig, y_target, regularization, features_whitelist)
delta = x_orig - xcf

if self.model.predict([xcf]) != y_target:
if self._model_predict([xcf]) != y_target:
raise Exception("No counterfactual found - Consider changing parameters 'regularization', 'features_whitelist', 'optimizer' and try again")

if return_as_dict is True:
Expand Down

0 comments on commit f5b9a39

Please sign in to comment.