"""Classifier models for VCNet.
A VCNet architecture is made of a counterfactual generator and a classifier.
This module implements the classes that define the classifiers of the architecture.
Two classes are provided:
* :py:class:`Classifier`: the implementation of a default classifier architecture for the
joint model of VCNet.
It is implemented as a Lightning module. The same architecture can be used in a post-hoc model.
* :py:class:`SKLearnClassifier`: a wrapper for a shallow classifier to be explained with a
post-hoc model.
.. note::
You can define your own classifier by inheriting from one of these two classes.
.. warning::
:py:class:`SKLearnClassifier` cannot be used in a joint-learning fashion.
"""
import warnings
import torch
import torch.nn as nn
import torch.optim as optim
import lightning as L
import pandas as pd
# pylint: disable=W0611
# pylint: disable=W0123
# pylint: disable=C0103
from sklearn.ensemble import AdaBoostClassifier, RandomForestClassifier
from sklearn.gaussian_process import GaussianProcessClassifier
from sklearn.gaussian_process.kernels import RBF
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
from xgboost import XGBClassifier
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
[docs]
class SKLearnClassifier:
"""Wrapper to use a sklearn classifier in the VCNet pipeline.
Args:
hp (Dict): configuration of the classifier (hyperparameters) and of the dataset
Example of minimal configuration
--------------------------------
Example of a minimal dictionary to set up a :py:class:`SKLearnClassifier` in VCNet.
.. code-block:: python
hp = {
"dataset": {
"target":"income",
},
"classifier_params" : {
"skname": "RandomForestClassifier",
"kwargs": {
"n_estimators" : 50,
}
}
}
classifier = SKLearnClassifier(hp)
classifier.fit(dataset.df_train)
.. note::
This class can also be used with an XGBoost classifier (`skname` set to
`"XGBClassifier"`).
.. note::
We refer the user to the `sklearn` API for the list of the classifier parameters.
When using XGBoost, the parameters are described in the
`XGBoost API <https://xgboost.readthedocs.io/en/stable/python/sklearn_estimator.html>`_.
"""
def __init__(self, hp):
if "classifier_params" in hp and "skname" in hp["classifier_params"]:
if "kwargs" in hp["classifier_params"]:
self.clf = eval(hp["classifier_params"]["skname"])(
**hp["classifier_params"]["kwargs"]
)
else:
self.clf = eval(hp["classifier_params"]["skname"])()
self.hp = hp
else:
raise RuntimeError(
"invalid parameters: the ['classifier_params']['skname'] parameter is missing."
)
def __call__(self, x: torch.tensor) -> torch.tensor:
"""Applies the classifier to a tensor.
Args:
x (torch.tensor): tensor to classify
Returns:
torch.tensor: the class probabilities of the examples
"""
p = self.clf.predict_proba(x.detach().cpu())
return torch.tensor(p, dtype=torch.float32, requires_grad=False, device=device)
[docs]
def fit(self, X: pd.DataFrame):
"""Fits the model.
Args:
X (pd.DataFrame): dataset on which the model is trained (including the target
attribute)
"""
self.clf.fit(
X.drop(self.hp["dataset"]["target"], axis=1).to_numpy(),
X[self.hp["dataset"]["target"]],
)
[docs]
class Classifier(L.LightningModule):
"""Simple fully connected classifier that is used by default for the classification
of numerical tabular data.
The network architecture is made of three hidden fully connected layers and an output
layer, with ReLU activation functions between layers and a sigmoid at the end.
The sizes of the layers are defined in the hyperparameters provided when the model
is instantiated.
The mandatory parameters to define in the dictionary are:
* the input size (in `dataset` > `feature_size`)
* the layer sizes (in `classifier_params`: `l1_size`, `l2_size` and `l3_size`)
* the output size (in `dataset` > `class_size`)
The learning rate (defined by `lr` in `classifier_params`) is also recommended. Its
default value is 0.01.
Args:
hp (Dict): configuration of the classifier (hyperparameters) and of the dataset.
Raises:
KeyError: if the settings do not contain all the required parameters
"""
def __init__(self, hp):
super().__init__()
self._hp = hp
try:
self.model = nn.Sequential(
nn.Linear(
hp["dataset"]["feature_size"], hp["classifier_params"]["l1_size"]
),
nn.ReLU(),
# nn.BatchNorm1d(hp['classifier_params']["l1_size"]),
# nn.Dropout(p=0.1),
nn.Linear(
hp["classifier_params"]["l1_size"], hp["classifier_params"]["l2_size"]
),
nn.ReLU(),
# nn.BatchNorm1d(hp['classifier_params']["l2_size"]),
# nn.Dropout(p=0.1),
nn.Linear(
hp["classifier_params"]["l2_size"], hp["classifier_params"]["l3_size"]
),
nn.ReLU(),
nn.Linear(
hp["classifier_params"]["l3_size"], hp["dataset"]["class_size"]
),
nn.Sigmoid(),
)
except KeyError:
print("VCNet classifier: missing mandatory attribute in the \
definition of the classifier.\n")
if "lr" not in hp["classifier_params"]:
self._hp["classifier_params"]["lr"]=0.01
warnings.warn("VCNet classifier: default learning rate is set to 0.01")
[docs]
def forward(self, x: torch.tensor) -> torch.tensor: # pylint: disable=W0221
"""Applies the classification model to the input `x`.
Args:
x (torch.tensor): tensor of examples to classify.
Returns:
torch.tensor: the probabilistic classification vector
"""
return self.model(x)
[docs]
def training_step(self, batch, batch_idx) -> float: # pylint: disable=W0221,W0613
"""Implementation of a training step."""
x, y = batch
output_class = self.forward(x).squeeze()
loss = nn.BCELoss(reduction="sum")(output_class, y)
return loss