Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ADD: model report, Standard Err, Zvalue, Pvalue #18

Merged
merged 3 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions choice_learn/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(
self.callbacks.set_model(self)

# Was originally in BaseMNL, moved here.
self.optimizer_name = optimizer
if optimizer.lower() == "adam":
self.optimizer = tf.keras.optimizers.Adam(lr)
elif optimizer.lower() == "sgd":
Expand Down
144 changes: 137 additions & 7 deletions choice_learn/models/conditional_mnl.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Conditional MNL model."""

import pandas as pd
import tensorflow as tf
import tensorflow_probability as tfp

from .base_model import ChoiceModel

Expand Down Expand Up @@ -627,6 +629,12 @@ def instantiate(
When a mode is wrongly precised.
"""
# Possibility to stack weights to be faster ????
if items_features_names is None:
items_features_names = []
if contexts_features_names is None:
contexts_features_names = []
if contexts_items_features_names is None:
contexts_items_features_names = []
weights = []
weights_count = 0
self._items_features_names = []
Expand Down Expand Up @@ -853,11 +861,14 @@ def compute_utility_from_dict(
tf.Tensor
Utilities corresponding of shape (n_choices, n_items)
"""
del availabilities_batch, choices_batch
_, _ = availabilities_batch, choices_batch

contexts_items_utilities = []
num_items = items_batch[0].shape[0]
num_choices = contexts_batch[0].shape[0]
if items_batch is not None:
num_items = items_batch[0].shape[0]
else:
num_items = contexts_items_batch[0].shape[1]
num_choices = availabilities_batch.shape[0]

# Items features
for i, feat_tuple in enumerate(self._items_features_names):
Expand Down Expand Up @@ -945,13 +956,15 @@ def compute_utility_from_dict(

return tf.reduce_sum(contexts_items_utilities, axis=0)

def fit(self, choice_dataset, **kwargs):
def fit(self, choice_dataset, get_report=False, **kwargs):
"""Main fit function to estimate the paramters.

Parameters
----------
choice_dataset : ChoiceDataset
Choice dataset to use for the estimation.
get_report: bool, optional
Whether or not to compute a report of the estimation, by default False

Returns:
--------
Expand All @@ -970,9 +983,12 @@ def fit(self, choice_dataset, **kwargs):
contexts_items_features_names=choice_dataset.contexts_items_features_names,
)
self.instantiated = True
return super().fit(choice_dataset=choice_dataset, **kwargs)
fit = super().fit(choice_dataset=choice_dataset, **kwargs)
if get_report:
self.report = self.compute_report(choice_dataset)
return fit

def _fit_with_lbfgs(self, choice_dataset, n_epochs, tolerance=1e-8):
def _fit_with_lbfgs(self, choice_dataset, n_epochs, tolerance=1e-8, get_report=False):
"""Specific fit function to estimate the paramters with LBFGS.

Parameters
Expand All @@ -983,6 +999,8 @@ def _fit_with_lbfgs(self, choice_dataset, n_epochs, tolerance=1e-8):
Number of epochs to run.
tolerance : float, optional
Tolerance in the research of minimum, by default 1e-8
get_report: bool, optional
Whether or not to compute a report of the estimation, by default False

Returns:
--------
Expand All @@ -1001,4 +1019,116 @@ def _fit_with_lbfgs(self, choice_dataset, n_epochs, tolerance=1e-8):
contexts_items_features_names=choice_dataset.contexts_items_features_names,
)
self.instantiated = True
return super()._fit_with_lbfgs(choice_dataset, n_epochs, tolerance)
fit = super()._fit_with_lbfgs(choice_dataset, n_epochs, tolerance)
if get_report:
self.report = self.compute_report(choice_dataset)
return fit

def compute_report(self, dataset):
"""Computes a report of the estimated weights.

Parameters
----------
dataset : ChoiceDataset
ChoiceDataset used for the estimation of the weights that will be
used to compute the Std Err of this estimation.

Returns:
--------
pandas.DataFrame
A DF with estimation, Std Err, z_value and p_value for each coefficient.
"""
weights_std = self.get_weights_std(dataset)
dist = tfp.distributions.Normal(loc=0.0, scale=1.0)

names = []
z_values = []
estimations = []
p_z = []
i = 0
for weight in self.weights:
for j in range(weight.shape[1]):
names.append(f"{weight.name}_{j}")
estimations.append(weight.numpy()[0][j])
z_values.append(weight.numpy()[0][j] / weights_std[i].numpy())
p_z.append(2 * (1 - dist.cdf(tf.math.abs(z_values[-1])).numpy()))
i += 1

return pd.DataFrame(
{
"Coefficient Name": names,
"Coefficient Estimation": estimations,
"Std. Err": weights_std.numpy(),
"z_value": z_values,
"P(.>z)": p_z,
},
)

def get_weights_std(self, dataset):
"""Approximates Std Err with Hessian matrix.

Parameters
----------
dataset : ChoiceDataset
ChoiceDataset used for the estimation of the weights that will be
used to compute the Std Err of this estimation.

Returns:
--------
tf.Tensor
Estimation of the Std Err for the weights.
"""
# Loops of differentiation
with tf.GradientTape() as tape_1:
with tf.GradientTape(persistent=True) as tape_2:
model = self.clone()
w = tf.concat(self.weights, axis=1)
tape_2.watch(w)
tape_1.watch(w)
mw = []
index = 0
for _w in self.weights:
mw.append(w[:, index : index + _w.shape[1]])
index += _w.shape[1]
model.weights = mw
for batch in dataset.iter_batch(batch_size=-1):
utilities = model.compute_utility(*batch)
probabilities = tf.nn.softmax(utilities, axis=-1)
loss = tf.keras.losses.CategoricalCrossentropy(reduction="sum")(
y_pred=probabilities,
y_true=tf.one_hot(dataset.choices, depth=4),
)
# Compute the Jacobian
jacobian = tape_2.jacobian(loss, w)
# Compute the Hessian from the Jacobian
hessian = tape_1.batch_jacobian(jacobian, w)
return tf.sqrt([tf.linalg.inv(tf.squeeze(hessian))[i][i] for i in range(13)])

def clone(self):
"""Returns a clone of the model."""
clone = ConditionalMNL(
parameters=self.params,
add_exit_choice=self.normalize_non_buy,
optimizer=self.optimizer_name,
)
if hasattr(self, "history"):
clone.history = self.history
if hasattr(self, "is_fitted"):
clone.is_fitted = self.is_fitted
if hasattr(self, "instantiated"):
clone.instantiated = self.instantiated
clone.loss = self.loss
clone.label_smoothing = self.label_smoothing
if hasattr(self, "report"):
clone.report = self.report
if hasattr(self, "weights"):
clone.weights = self.weights
if hasattr(self, "lr"):
clone.lr = self.lr
if hasattr(self, "_items_features_names"):
clone._items_features_names = self._items_features_names
if hasattr(self, "_contexts_features_names"):
clone._contexts_features_names = self._contexts_features_names
if hasattr(self, "_contexts_items_features_names"):
clone._contexts_items_features_names = self._contexts_items_features_names
return clone
Loading
Loading