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 lc endpoint: predict_modelwise_probas #206

Merged
merged 22 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
92fd73d
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau May 17, 2024
8e8bca4
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau May 23, 2024
b3455e2
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau May 28, 2024
aa35f2f
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau May 30, 2024
5fbbaca
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Jun 25, 2024
c8ec7c2
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Jul 2, 2024
18d6c25
ENH: logos in ReadMe
VincentAuriau Jul 3, 2024
19f04af
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Jul 4, 2024
03c5b35
merge
VincentAuriau Jul 8, 2024
57bdf4b
poetry lock [--no-update]Merge branch 'main' of github.com:artefactor…
VincentAuriau Jul 31, 2024
1266ad9
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Aug 21, 2024
053977d
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Sep 11, 2024
52b2356
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Oct 21, 2024
78517ec
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Oct 22, 2024
3ff9d54
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Dec 23, 2024
1597ca3
Merge branch 'main' of github.com:artefactory/choice-learn-private
VincentAuriau Dec 23, 2024
2eaf427
ENH: efficiency of EM algorithm
VincentAuriau Dec 26, 2024
2ede973
ADD: updated LC notebook
VincentAuriau Dec 26, 2024
e15d9ef
ADD: modelwise proba
VincentAuriau Dec 26, 2024
f8c8297
ADD: EM tests & more
VincentAuriau Dec 27, 2024
f1e980a
FIX: val_dataset for fit_with_gd
VincentAuriau Dec 27, 2024
0f0aa77
ADD: better tests specs
VincentAuriau Dec 27, 2024
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
3 changes: 1 addition & 2 deletions choice_learn/models/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def fit(
self.callbacks.on_train_end(logs=temps_logs)
return losses_history

@tf.function
@tf.function(reduce_retracing=True)
def batch_predict(
self,
shared_features_by_choice,
Expand Down Expand Up @@ -731,7 +731,6 @@ def f(params_1d):
# calculate gradients and convert to 1D tf.Tensor
grads = tape.gradient(loss_value, self.trainable_weights)
grads = tf.dynamic_stitch(idx, grads)
# print out iteration & loss
f.iter.assign_add(1)

# store loss value so we can retrieve later
Expand Down
117 changes: 67 additions & 50 deletions choice_learn/models/latent_class_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,18 @@ def instantiate(self, **kwargs):
name="Latent-Logits",
)
self.latent_logits = init_logit
self.models = [self.model_class(**mp) for mp in self.model_parameters]
for model in self.models:
model.instantiate(**kwargs)

self.models = self.instantiate_latent_models(**kwargs)
self.instantiated = True

def instantiate_latent_models(self, **kwargs):
"""Instantiate latent models."""
models = [self.model_class(**mp) for mp in self.model_parameters]
for model in models:
model.instantiate(**kwargs)

return models

# @tf.function
def batch_predict(
self,
Expand Down Expand Up @@ -230,7 +236,7 @@ def compute_batch_utility(
utilities.append(model_utilities)
return utilities

def fit(self, choice_dataset, sample_weight=None, verbose=0):
def fit(self, choice_dataset, sample_weight=None, val_dataset=None, verbose=0):
"""Fit the model on a ChoiceDataset.

Parameters
Expand All @@ -239,6 +245,8 @@ def fit(self, choice_dataset, sample_weight=None, verbose=0):
Dataset to be used for coefficients estimations
sample_weight : np.ndarray, optional
sample weights to apply, by default None
val_dataset: ChoiceDataset
Validation dataset for MLE Gradient Descent Optimization
verbose : int, optional
print level, for debugging, by default 0

Expand All @@ -249,7 +257,6 @@ def fit(self, choice_dataset, sample_weight=None, verbose=0):
"""
if self.fit_method.lower() == "em":
self.minf = np.log(1e-3)
print("Expectation-Maximization estimation algorithm not well implemented yet.")
return self._em_fit(
choice_dataset=choice_dataset, sample_weight=sample_weight, verbose=verbose
)
Expand All @@ -272,7 +279,10 @@ def fit(self, choice_dataset, sample_weight=None, verbose=0):
self.optimizer = tf.keras.optimizers.Adam(self.lr)

return self._fit_with_gd(
choice_dataset=choice_dataset, sample_weight=sample_weight, verbose=verbose
choice_dataset=choice_dataset,
sample_weight=sample_weight,
verbose=verbose,
val_dataset=val_dataset,
)

raise ValueError(f"Fit method not implemented: {self.fit_method}")
Expand Down Expand Up @@ -757,45 +767,6 @@ def _fit_with_gd(
# self.callbacks.on_train_end(logs=temps_logs)
return losses_history

def _nothing(self, inputs):
"""_summary_.

Parameters
----------
inputs : _type_
_description_

Returns
-------
_type_
_description_
"""
latent_probas = tf.clip_by_value(
self.latent_logits - tf.reduce_max(self.latent_logits), self.minf, 0
)
latent_probas = tf.math.exp(latent_probas)
# latent_probas = tf.math.abs(self.logit_latent_probas) # alternative implementation
latent_probas = latent_probas / tf.reduce_sum(latent_probas)
proba_list = []
avail = inputs[4]
for q in range(self.n_latent_classes):
combined = self.models[q].compute_batch_utility(*inputs)
combined = tf.clip_by_value(
combined - tf.reduce_max(combined, axis=1, keepdims=True), self.minf, 0
)
combined = tf.keras.layers.Activation(activation=tf.nn.softmax)(combined)
# combined = tf.keras.layers.Softmax()(combined)
combined = combined * avail
combined = latent_probas[q] * tf.math.divide(
combined, tf.reduce_sum(combined, axis=1, keepdims=True)
)
combined = tf.expand_dims(combined, -1)
proba_list.append(combined)
# print(combined.get_shape()) # it is useful to print the shape of tensors for debugging

proba_final = tf.keras.layers.Concatenate(axis=2)(proba_list)
return tf.math.reduce_sum(proba_final, axis=2, keepdims=False)

def _expectation(self, choice_dataset):
predicted_probas = [model.predict_probas(choice_dataset) for model in self.models]
latent_probabilities = self.get_latent_classes_weights()
Expand Down Expand Up @@ -824,7 +795,7 @@ def _expectation(self, choice_dataset):
)

return tf.clip_by_value(
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 1e-10, 1
predicted_probas / np.sum(predicted_probas, axis=1, keepdims=True), 1e-6, 1
), loss

def _maximization(self, choice_dataset, verbose=0):
Expand All @@ -842,10 +813,17 @@ def _maximization(self, choice_dataset, verbose=0):
np.ndarray
latent probabilities resulting of maximization step
"""
self.models = [self.model_class(**mp) for mp in self.model_parameters]
# models = [self.model_class(**mp) for mp in self.model_parameters]
# for i in range(len(models)):
# for j, var in enumerate(self.models[i].trainable_weights):
# models[i]._trainable_weights[j] = var
# self.instantiate_latent_models(choice_dataset)

# M-step: MNL estimation
for q in range(self.n_latent_classes):
self.models[q].fit(choice_dataset, sample_weight=self.weights[:, q], verbose=verbose)
self.models[q].fit(
choice_dataset, sample_weight=self.weights[:, q].numpy(), verbose=verbose
)

# M-step: latent probability estimation
latent_probas = np.sum(self.weights, axis=0)
Expand Down Expand Up @@ -876,7 +854,9 @@ def _em_fit(self, choice_dataset, sample_weight=None, verbose=0):

# Initialization
init_sample_weight = np.random.rand(self.n_latent_classes, len(choice_dataset))
init_sample_weight = init_sample_weight / np.sum(init_sample_weight, axis=0, keepdims=True)
init_sample_weight = np.clip(
init_sample_weight / np.sum(init_sample_weight, axis=0, keepdims=True), 1e-6, 1
)
for i, model in enumerate(self.models):
# model.instantiate()
model.fit(choice_dataset, sample_weight=init_sample_weight[i], verbose=verbose)
Expand All @@ -888,7 +868,7 @@ def _em_fit(self, choice_dataset, sample_weight=None, verbose=0):
if np.sum(np.isnan(self.latent_logits)) > 0:
print("Nan in logits")
break
return hist_logits, hist_loss
return hist_loss, hist_logits

def predict_probas(self, choice_dataset, batch_size=-1):
"""Predicts the choice probabilities for each choice and each product of a ChoiceDataset.
Expand Down Expand Up @@ -922,6 +902,43 @@ def predict_probas(self, choice_dataset, batch_size=-1):

return tf.concat(stacked_probabilities, axis=0)

def predict_modelwise_probas(self, choice_dataset, batch_size=-1):
"""Predicts the choice probabilities for each choice and each product of a ChoiceDataset.

Stacks each model probability.

Parameters
----------
choice_dataset : ChoiceDataset
Dataset on which to apply to prediction
batch_size : int, optional
Batch size to use for the prediction, by default -1

Returns
-------
np.ndarray (n_choices, n_items)
Choice probabilties for each choice and each product
"""
modelwise_probabilities = []
for model in self.models:
stacked_probabilities = []
for (
shared_features,
items_features,
available_items,
choices,
) in choice_dataset.iter_batch(batch_size=batch_size):
_, probabilities = model.batch_predict(
shared_features_by_choice=shared_features,
items_features_by_choice=items_features,
available_items_by_choice=available_items,
choices=choices,
)
stacked_probabilities.append(probabilities)
modelwise_probabilities.append(tf.concat(stacked_probabilities, axis=0))

return tf.stack(modelwise_probabilities, axis=0)

def get_latent_classes_weights(self):
"""Return the latent classes weights / probabilities from logits.

Expand Down
14 changes: 13 additions & 1 deletion choice_learn/models/latent_class_mnl.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import tensorflow as tf

import choice_learn.tf_ops as tf_ops

from .conditional_logit import ConditionalLogit, MNLCoefficients
from .latent_class_base_model import BaseLatentClassModel
from .simple_mnl import SimpleMNL
Expand All @@ -23,6 +25,7 @@ def __init__(
intercept=None,
optimizer="Adam",
lr=0.001,
epochs_maximization=1000,
**kwargs,
):
"""Initialize model.
Expand Down Expand Up @@ -56,7 +59,7 @@ def __init__(
"batch_size": batch_size,
"lbfgs_tolerance": lbfgs_tolerance,
"lr": lr,
"epochs": 1000,
"epochs": epochs_maximization,
}

super().__init__(
Expand Down Expand Up @@ -88,6 +91,15 @@ def instantiate_latent_models(self, n_items, n_shared_features, n_items_features
model.indexes, model.weights = model.instantiate(
n_items, n_shared_features, n_items_features
)
model.exact_nll = tf_ops.CustomCategoricalCrossEntropy(
from_logits=False,
label_smoothing=0.0,
sparse=False,
axis=-1,
epsilon=1e-25,
name="exact_categorical_crossentropy",
reduction="sum_over_batch_size",
)
model.instantiated = True

def instantiate(self, n_items, n_shared_features, n_items_features):
Expand Down
Loading
Loading