From a9db338c7c63252cb05674c7bc4acda7cfc67cac Mon Sep 17 00:00:00 2001 From: Vincent Auriau Date: Fri, 29 Nov 2024 14:24:36 -0800 Subject: [PATCH] Fix tolerance argument in LatentClass Model (#190) * FIX: forgotten remainings of renaming of tolerance argument into lbfgs_tolerance * ADD: updates notebook * ADD: basic LC model tests * ADD: tolerance * ADD: eagerly exec for SimpleMNL --- .../models/latent_class_base_model.py | 8 +-- choice_learn/models/latent_class_mnl.py | 22 +++--- notebooks/models/latent_class_model.ipynb | 13 ++-- .../models/test_latent_class.py | 68 +++++++++++++++++++ .../models/test_simple_mnl.py | 5 +- 5 files changed, 94 insertions(+), 22 deletions(-) create mode 100644 tests/integration_tests/models/test_latent_class.py diff --git a/choice_learn/models/latent_class_base_model.py b/choice_learn/models/latent_class_base_model.py index 9dd27617..8e29dfc9 100644 --- a/choice_learn/models/latent_class_base_model.py +++ b/choice_learn/models/latent_class_base_model.py @@ -22,7 +22,7 @@ def __init__( batch_size=128, optimizer=None, add_exit_choice=False, - tolerance=1e-6, + lbfgs_tolerance=1e-6, lr=0.001, ): """Instantiate of the model mixture. @@ -44,7 +44,7 @@ class of models to get a mixture of Name of the tf.keras.optimizers to be used if one is used, by default None add_exit_choice : bool, optional Whether or not to add an exit choice, by default False - tolerance: float, optional + lbfgs_tolerance: float, optional Tolerance for the L-BFGS optimizer if applied, by default 1e-6 lr: float, optional Learning rate for the optimizer if applied, by default 0.001 @@ -65,7 +65,7 @@ class of models to get a mixture of self.epochs = epochs self.add_exit_choice = add_exit_choice - self.tolerance = tolerance + self.lbfgs_tolerance = lbfgs_tolerance self.optimizer = optimizer self.lr = lr self.batch_size = batch_size @@ -472,7 +472,7 @@ def _fit_with_lbfgs(self, choice_dataset, sample_weight=None, verbose=0): initial_position=init_params, max_iterations=epochs, tolerance=-1, - f_absolute_tolerance=self.tolerance, + f_absolute_tolerance=self.lbfgs_tolerance, f_relative_tolerance=-1, x_tolerance=-1, ) diff --git a/choice_learn/models/latent_class_mnl.py b/choice_learn/models/latent_class_mnl.py index 4f6e1b17..1c5b6b83 100644 --- a/choice_learn/models/latent_class_mnl.py +++ b/choice_learn/models/latent_class_mnl.py @@ -19,7 +19,7 @@ def __init__( epochs=100, batch_size=128, add_exit_choice=False, - tolerance=1e-6, + lbfgs_tolerance=1e-6, intercept=None, optimizer="Adam", lr=0.001, @@ -37,7 +37,7 @@ def __init__( Number of epochs add_exit_choice : bool, optional Whether to normalize probabilities with exit choice, by default False - tolerance : float, optional + lbfgs_tolerance : float, optional LBFG-S tolerance, by default 1e-6 intercept : str, optional Type of intercept to include in the SimpleMNL. @@ -49,24 +49,24 @@ def __init__( """ self.n_latent_classes = n_latent_classes self.intercept = intercept - model_coefficients = { + model_parameters = { "add_exit_choice": add_exit_choice, "intercept": intercept, "optimizer": optimizer, "batch_size": batch_size, - "tolerance": tolerance, + "lbfgs_tolerance": lbfgs_tolerance, "lr": lr, "epochs": 1000, } super().__init__( model_class=SimpleMNL, - model_parameters=model_coefficients, + model_parameters=model_parameters, n_latent_classes=n_latent_classes, fit_method=fit_method, epochs=epochs, add_exit_choice=add_exit_choice, - tolerance=tolerance, + lbfgs_tolerance=lbfgs_tolerance, optimizer=optimizer, lr=lr, **kwargs, @@ -133,7 +133,7 @@ def __init__( coefficients=None, epochs=100, add_exit_choice=False, - tolerance=1e-6, + lbfgs_tolerance=1e-6, optimizer="Adam", lr=0.001, **kwargs, @@ -156,7 +156,7 @@ def __init__( Number of epochs add_exit_choice : bool, optional Whether to normalize probabilities with exit choice, by default False - tolerance : float, optional + lbfgs_tolerance : float, optional LBFG-S tolerance, by default 1e-6 optimizer : str, optional tf.keras.optimizers to be used, by default "Adam" @@ -168,7 +168,7 @@ def __init__( self.coefficients = coefficients self.epochs = epochs self.add_exit_choice = add_exit_choice - self.tolerance = tolerance + self.lbfgs_tolerance = lbfgs_tolerance self.optimizer = optimizer self.lr = lr @@ -176,7 +176,7 @@ def __init__( "coefficients": self.coefficients, "add_exit_choice": self.add_exit_choice, "optimizer": self.optimizer, - "tolerance": self.tolerance, + "lbfgs_tolerance": self.lbfgs_tolerance, "lr": self.lr, "epochs": self.epochs, } @@ -188,7 +188,7 @@ def __init__( fit_method=fit_method, epochs=epochs, add_exit_choice=add_exit_choice, - tolerance=tolerance, + lbfgs_tolerance=lbfgs_tolerance, optimizer=optimizer, lr=lr, **kwargs, diff --git a/notebooks/models/latent_class_model.ipynb b/notebooks/models/latent_class_model.ipynb index 5feea829..62ad7d60 100644 --- a/notebooks/models/latent_class_model.ipynb +++ b/notebooks/models/latent_class_model.ipynb @@ -79,7 +79,7 @@ "metadata": {}, "outputs": [], "source": [ - "lc_model = LatentClassSimpleMNL(n_latent_classes=3, fit_method=\"mle\", optimizer=\"lbfgs\", epochs=1000, tolerance=1e-20)\n", + "lc_model = LatentClassSimpleMNL(n_latent_classes=3, fit_method=\"mle\", optimizer=\"lbfgs\", epochs=1000, lbfgs_tolerance=1e-20)\n", "hist, results = lc_model.fit(elec_dataset, verbose=1)" ] }, @@ -134,7 +134,7 @@ " fit_method=\"mle\",\n", " optimizer=\"lbfgs\",\n", " epochs=1000,\n", - " tolerance=1e-12)" + " lbfgs_tolerance=1e-12)" ] }, { @@ -246,7 +246,8 @@ " n_latent_classes=3,\n", " fit_method=\"mle\",\n", " epochs=1000,\n", - " optimizer=\"lbfgs\"\n", + " optimizer=\"lbfgs\",\n", + " lbfgs_tolerance=1e-12\n", " )\n", "manual_lc.instantiate(n_items=4,\n", " n_shared_features=0,\n", @@ -260,7 +261,7 @@ "metadata": {}, "outputs": [], "source": [ - "manual_lc.evaluate(elec_dataset) * len(elec_dataset)" + "print(manual_lc.evaluate(elec_dataset) * len(elec_dataset))" ] }, { @@ -280,7 +281,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "tf_env", "language": "python", "name": "python3" }, @@ -294,7 +295,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.18" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tests/integration_tests/models/test_latent_class.py b/tests/integration_tests/models/test_latent_class.py new file mode 100644 index 00000000..e27ae8ec --- /dev/null +++ b/tests/integration_tests/models/test_latent_class.py @@ -0,0 +1,68 @@ +"""Tests basic stuff for the latent class models.""" + +import tensorflow as tf + +from choice_learn.datasets import load_electricity +from choice_learn.models.latent_class_base_model import BaseLatentClassModel +from choice_learn.models.latent_class_mnl import LatentClassConditionalLogit, LatentClassSimpleMNL +from choice_learn.models.simple_mnl import SimpleMNL + +elec_dataset = load_electricity(as_frame=False) + + +def test_latent_simple_mnl(): + """Test the simple latent class model fit() method.""" + tf.config.run_functions_eagerly(True) + lc_model = LatentClassSimpleMNL( + n_latent_classes=3, fit_method="mle", optimizer="lbfgs", epochs=1000, lbfgs_tolerance=1e-20 + ) + _, _ = lc_model.fit(elec_dataset) + + assert lc_model.evaluate(elec_dataset).numpy() < 1.15 + + +def test_latent_clogit(): + """Test the conditional logit latent class model fit() method.""" + tf.config.run_functions_eagerly(True) + lc_model = LatentClassConditionalLogit( + n_latent_classes=3, fit_method="mle", optimizer="lbfgs", epochs=1000, lbfgs_tolerance=1e-12 + ) + lc_model.add_shared_coefficient( + coefficient_name="pf", feature_name="pf", items_indexes=[0, 1, 2, 3] + ) + lc_model.add_shared_coefficient( + coefficient_name="cl", feature_name="cl", items_indexes=[0, 1, 2, 3] + ) + lc_model.add_shared_coefficient( + coefficient_name="loc", feature_name="loc", items_indexes=[0, 1, 2, 3] + ) + lc_model.add_shared_coefficient( + coefficient_name="wk", feature_name="wk", items_indexes=[0, 1, 2, 3] + ) + lc_model.add_shared_coefficient( + coefficient_name="tod", feature_name="tod", items_indexes=[0, 1, 2, 3] + ) + lc_model.add_shared_coefficient( + coefficient_name="seas", feature_name="seas", items_indexes=[0, 1, 2, 3] + ) + _, _ = lc_model.fit(elec_dataset) + + assert lc_model.evaluate(elec_dataset).numpy() < 1.15 + + +def test_manual_lc(): + """Test manual specification of Latent Class Simple MNL model.""" + tf.config.run_functions_eagerly(True) + manual_lc = BaseLatentClassModel( + model_class=SimpleMNL, + model_parameters={"add_exit_choice": False}, + n_latent_classes=3, + fit_method="mle", + epochs=1000, + optimizer="lbfgs", + lbfgs_tolerance=1e-12, + ) + + manual_lc.instantiate(n_items=4, n_shared_features=0, n_items_features=6) + _ = manual_lc.fit(elec_dataset) + assert manual_lc.evaluate(elec_dataset) < 1.15 diff --git a/tests/integration_tests/models/test_simple_mnl.py b/tests/integration_tests/models/test_simple_mnl.py index c84d901d..c94dbcff 100644 --- a/tests/integration_tests/models/test_simple_mnl.py +++ b/tests/integration_tests/models/test_simple_mnl.py @@ -1,5 +1,7 @@ """Tests SimpleMNL.""" +import tensorflow as tf + from choice_learn.datasets import load_swissmetro from choice_learn.models import SimpleMNL @@ -18,10 +20,11 @@ def test_simple_mnl_lbfgs_fit_with_lbfgs(): def test_simple_mnl_lbfgs_fit_with_adam(): """Tests that SimpleMNL can fit with Adam.""" + tf.config.run_functions_eagerly(True) global dataset model = SimpleMNL(epochs=20, optimizer="adam", batch_size=256) - model.fit(dataset) + model.fit(dataset, get_report=True) model.evaluate(dataset) assert model.evaluate(dataset) < 1.0