FairUP / src /aif360 /sklearn /preprocessing /learning_fair_representations.py
erasmopurif's picture
First commit
d2a8669
import warnings
import numpy as np
import pandas as pd
import scipy.optimize as optim
from scipy.spatial.distance import cdist
from scipy.special import softmax
from sklearn.base import BaseEstimator, ClassifierMixin, TransformerMixin
from sklearn.exceptions import ConvergenceWarning
from sklearn.preprocessing import LabelEncoder
from sklearn.utils import check_random_state
try:
import torch
import torch.nn.functional as F
except ImportError as error:
from logging import warning
warning("{}: LearnedFairRepresentations will be unavailable. To install, run:\n"
"pip install 'aif360[LFR]'".format(error))
from aif360.sklearn.utils import check_inputs, check_groups
class LearnedFairRepresentations(BaseEstimator, ClassifierMixin, TransformerMixin):
"""Learned Fair Representations.
Learned fair representations is a pre-processing technique that finds a
latent representation which encodes the data well but obfuscates information
about protected attributes [#zemel13]_. It can also be used as an in-
processing method by utilizing the learned target coefficients.
References:
.. [#zemel13] `R. Zemel, Y. Wu, K. Swersky, T. Pitassi, and C. Dwork,
"Learning Fair Representations." International Conference on Machine
Learning, 2013. <http://proceedings.mlr.press/v28/zemel13.html>`_
# Based on code from https://github.com/zjelveh/learning-fair-representations
Attributes:
prot_attr_ (str or list(str)): Protected attribute(s) used for
reweighing.
groups_ (array, shape (n_groups,)): A list of group labels known to the
transformer.
classes_ (array, shape (n_classes,)): A list of class labels known to
the transformer.
priv_group_ (scalar): The label of the privileged group.
coef_ (array, shape (n_prototypes, 1) or (n_prototypes, n_classes)):
Coefficient of the intermediate representation for classification.
prototypes_ (array, shape (n_prototypes, n_features)): The prototype set
used to form a probabilistic mapping to the intermediate
representation. These act as clusters and are in the same space as
the samples.
n_iter_ (int): Actual number of iterations.
"""
def __init__(self, prot_attr=None, n_prototypes=5, reconstruct_weight=0.01,
target_weight=1., fairness_weight=50., tol=1e-4, max_iter=200,
verbose=0, random_state=None):
"""
Args:
prot_attr (single label or list-like, optional): Protected
attribute(s) to use in the reweighing process. If more than one
attribute, all combinations of values (intersections) are
considered. Default is ``None`` meaning all protected attributes
from the dataset are used.
n_prototypes (int, optional): Size of the set of "prototypes," Z.
reconstruct_weight (float, optional): Weight coefficient on the L_x
loss term, A_x.
target_weight (float, optional): Weight coefficient on the L_y loss
term, A_y.
fairness_weight (float, optional): Weight coefficient on the L_z
loss term, A_z.
tol (float, optional): Tolerance for stopping criteria.
max_iter (int, optional): Maximum number of iterations taken for the
solver to converge.
verbose (int, optional): Verbosity. 0 = silent, 1 = final loss only,
2 = print loss every 50 iterations.
random_state (int or numpy.RandomState, optional): Seed of pseudo-
random number generator for shuffling data and seeding weights.
"""
self.prot_attr = prot_attr
self.n_prototypes = n_prototypes
self.reconstruct_weight = reconstruct_weight
self.target_weight = target_weight
self.fairness_weight = fairness_weight
self.tol = tol
self.max_iter = max_iter
self.verbose = verbose
self.random_state = random_state
def fit(self, X, y, priv_group=1, sample_weight=None):
"""Compute the transformation parameters that lead to fair
representations.
Args:
X (pandas.DataFrame): Training samples.
y (array-like): Training labels.
priv_group (scalar, optional): The label of the privileged group.
sample_weight (array-like, optional): Sample weights.
Returns:
self
"""
X, y, sample_weight = check_inputs(X, y, sample_weight)
rng = check_random_state(self.random_state)
groups, self.prot_attr_ = check_groups(X, self.prot_attr)
priv = (groups == priv_group)
self.priv_group_ = priv_group
self.groups_ = np.unique(groups)
le = LabelEncoder()
y = le.fit_transform(y)
self.classes_ = le.classes_
n_classes = len(self.classes_)
if n_classes == 2:
n_classes = 1 # XXX
n_feat = X.shape[1]
w_size = self.n_prototypes*n_classes
i = 0
eps = np.finfo(np.float64).eps
def LFR_optim_objective(x, X, y, priv):
nonlocal i
x = torch.as_tensor(x).requires_grad_()
w = x[:w_size].view(-1, n_classes)
v = x[w_size:].view(-1, n_feat)
M = torch.softmax(-torch.cdist(X, v), dim=1)
y_pred = M.matmul(w).squeeze(1)
L_x = F.mse_loss(M.matmul(v), X)
L_y = F.cross_entropy(y_pred, y) if n_classes > 1 else \
F.binary_cross_entropy(y_pred.clamp(eps, 1-eps), y.type_as(w))
L_z = F.l1_loss(torch.mean(M[priv], 0), torch.mean(M[~priv], 0))
loss = (self.reconstruct_weight * L_x + self.target_weight * L_y
+ self.fairness_weight * L_z)
loss.backward()
if self.verbose > 1 and i % 50 == 0:
print("iter: {:{}d}, loss: {:7.3f}, A_x*L_x: {:7.3f}, A_y*L_y: "
"{:7.3f}, A_z*L_z: {:7.3f}".format(i,
int(np.log10(self.max_iter)+1), loss,
self.reconstruct_weight*L_x, self.target_weight*L_y,
self.fairness_weight*L_z))
i += 1
return loss.item(), x.grad.numpy()
x0 = rng.random(w_size + self.n_prototypes*n_feat)
bounds = [(0, 1)]*w_size + [(None, None)]*self.n_prototypes*n_feat
res = optim.minimize(LFR_optim_objective, x0=x0, method='L-BFGS-B',
args=(torch.tensor(X.to_numpy()), torch.as_tensor(y), priv),
jac=True, bounds=bounds, options={'gtol': self.tol,
'maxiter': self.max_iter})
self.coef_ = res.x[:w_size].reshape(-1, n_classes)
self.prototypes_ = res.x[w_size:].reshape(-1, n_feat)
self.n_iter_ = res.nit
if res.status == 0 and self.verbose:
print("Converged! iter: {}, loss: {:.3f}".format(res.nit, res.fun))
elif res.status == 1:
warnings.warn('lbfgs failed to converge. Increase the number of '
'iterations.', ConvergenceWarning)
elif res.status == 2:
warnings.warn('lbfgs failed to converge: {}'.format(
res.message.decode()), ConvergenceWarning)
return self
def transform(self, X):
"""Transform the dataset using the learned model parameters.
Args:
X (pandas.DataFrame): Training samples.
Returns:
pandas.DataFrame: Transformed samples.
"""
M = softmax(-cdist(X, self.prototypes_), axis=1)
Xt = M.dot(self.prototypes_)
return pd.DataFrame(Xt, columns=X.columns, index=X.index)
def predict_proba(self, X):
"""Transform the targets using the learned model parameters.
Args:
X (pandas.DataFrame): Training samples.
Returns:
numpy.ndarray: Transformed targets. Returns the probability of the
sample for each class in the model, where classes are ordered as
they are in ``self.classes_``.
"""
M = softmax(-cdist(X, self.prototypes_), axis=1)
yt = M.dot(self.coef_)
if yt.shape[1] == 1:
yt = np.c_[1-yt, yt]
else:
yt = softmax(yt, axis=1)
return yt
def predict(self, X):
"""Transform the targets using the learned model parameters.
Args:
X (pandas.DataFrame): Training samples.
Returns:
numpy.ndarray: Transformed targets.
"""
probas = self.predict_proba(X)
return self.classes_[probas.argmax(axis=1)]