diff --git a/choice_learn/models/conditional_logit.py b/choice_learn/models/conditional_logit.py index eee3ece1..c436c359 100644 --- a/choice_learn/models/conditional_logit.py +++ b/choice_learn/models/conditional_logit.py @@ -1,6 +1,7 @@ """Conditional MNL model.""" import logging +import math import numpy as np import pandas as pd @@ -645,10 +646,12 @@ def compute_report(self, choice_dataset): pandas.DataFrame A DF with estimation, Std Err, z_value and p_value for each coefficient. """ - import tensorflow_probability as tfp + + def phi(x): + """Cumulative distribution function for the standard normal distribution.""" + return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0 weights_std = self.get_weights_std(choice_dataset) - dist = tfp.distributions.Normal(loc=0.0, scale=1.0) names = [] z_values = [] @@ -663,7 +666,7 @@ def compute_report(self, choice_dataset): names.append(f"{weight.name[:-2]}") 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())) + p_z.append(2 * (1 - phi(tf.math.abs(z_values[-1]).numpy()))) i += 1 return pd.DataFrame( diff --git a/choice_learn/models/latent_class_base_model.py b/choice_learn/models/latent_class_base_model.py index 8e29dfc9..1b70f949 100644 --- a/choice_learn/models/latent_class_base_model.py +++ b/choice_learn/models/latent_class_base_model.py @@ -107,6 +107,8 @@ def instantiate(self, **kwargs): for model in self.models: model.instantiate(**kwargs) + self.instantiated = True + # @tf.function def batch_predict( self, diff --git a/choice_learn/models/simple_mnl.py b/choice_learn/models/simple_mnl.py index 554a1b26..efca48e3 100644 --- a/choice_learn/models/simple_mnl.py +++ b/choice_learn/models/simple_mnl.py @@ -4,6 +4,7 @@ """ import logging +import math import pandas as pd import tensorflow as tf @@ -254,10 +255,12 @@ def compute_report(self, choice_dataset): pandas.DataFrame A DF with estimation, Std Err, z_value and p_value for each coefficient. """ - import tensorflow_probability as tfp + + def phi(x): + """Cumulative distribution function for the standard normal distribution.""" + return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0 weights_std = self.get_weights_std(choice_dataset) - dist = tfp.distributions.Normal(loc=0.0, scale=1.0) names = [] z_values = [] @@ -272,7 +275,7 @@ def compute_report(self, choice_dataset): names.append(f"{weight.name[:-2]}") estimations.append(weight.numpy()[j]) z_values.append(weight.numpy()[j] / weights_std[i].numpy()) - p_z.append(2 * (1 - dist.cdf(tf.math.abs(z_values[-1])).numpy())) + p_z.append(2 * (1 - phi(tf.math.abs(z_values[-1]).numpy()))) i += 1 return pd.DataFrame( diff --git a/notebooks/introduction/3_model_clogit.ipynb b/notebooks/introduction/3_model_clogit.ipynb index 711ec7ff..7f1806bb 100644 --- a/notebooks/introduction/3_model_clogit.ipynb +++ b/notebooks/introduction/3_model_clogit.ipynb @@ -1361,7 +1361,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "tf_env", "language": "python", "name": "python3" }, @@ -1375,7 +1375,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.14" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tests/integration_tests/models/test_conditional_logit.py b/tests/integration_tests/models/test_conditional_logit.py index 85dccc87..542c24e2 100644 --- a/tests/integration_tests/models/test_conditional_logit.py +++ b/tests/integration_tests/models/test_conditional_logit.py @@ -55,7 +55,8 @@ def test_mode_canada_fit(): canada_dataset = load_modecanada(as_frame=False, preprocessing="tutorial") model = ConditionalLogit(coefficients=coefficients) - model.fit(canada_dataset, get_report=True) + model.fit(canada_dataset) + model.compute_report(canada_dataset) total_nll = model.evaluate(canada_dataset) * len(canada_dataset) assert total_nll <= 1874.4, f"Got NLL: {total_nll}" diff --git a/tests/integration_tests/models/test_latent_class.py b/tests/integration_tests/models/test_latent_class.py index e27ae8ec..8747ad76 100644 --- a/tests/integration_tests/models/test_latent_class.py +++ b/tests/integration_tests/models/test_latent_class.py @@ -14,7 +14,7 @@ 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 + n_latent_classes=3, fit_method="mle", optimizer="lbfgs", epochs=1000, lbfgs_tolerance=1e-8 ) _, _ = lc_model.fit(elec_dataset) @@ -25,7 +25,7 @@ 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 + n_latent_classes=3, fit_method="mle", optimizer="lbfgs", epochs=40, lbfgs_tolerance=1e-8 ) lc_model.add_shared_coefficient( coefficient_name="pf", feature_name="pf", items_indexes=[0, 1, 2, 3] @@ -58,11 +58,28 @@ def test_manual_lc(): model_parameters={"add_exit_choice": False}, n_latent_classes=3, fit_method="mle", - epochs=1000, + epochs=40, optimizer="lbfgs", - lbfgs_tolerance=1e-12, + lbfgs_tolerance=1e-8, ) 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 + + +def test_manual_lc_gd(): + """Test manual specification of Latent Class Simple MNL model with gradient descent.""" + 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=10, + optimizer="Adam", + ) + manual_lc.instantiate(n_items=4, n_shared_features=0, n_items_features=6) + nll_before = manual_lc.evaluate(elec_dataset) + _ = manual_lc.fit(elec_dataset) + assert manual_lc.evaluate(elec_dataset) < nll_before diff --git a/tests/integration_tests/models/test_simple_mnl.py b/tests/integration_tests/models/test_simple_mnl.py index c94dbcff..a2406457 100644 --- a/tests/integration_tests/models/test_simple_mnl.py +++ b/tests/integration_tests/models/test_simple_mnl.py @@ -8,13 +8,14 @@ dataset = load_swissmetro() -def test_simple_mnl_lbfgs_fit_with_lbfgs(): +def test_simple_mnl_fit_with_lbfgs(): """Tests that SimpleMNL can fit with LBFGS.""" + tf.config.run_functions_eagerly(True) global dataset model = SimpleMNL(epochs=20) - model.fit(dataset) - model.evaluate(dataset) + _ = model.fit(dataset, get_report=True) + _ = model.evaluate(dataset) assert model.evaluate(dataset) < 1.0 @@ -24,7 +25,7 @@ def test_simple_mnl_lbfgs_fit_with_adam(): global dataset model = SimpleMNL(epochs=20, optimizer="adam", batch_size=256) - model.fit(dataset, get_report=True) + model.fit(dataset) model.evaluate(dataset) assert model.evaluate(dataset) < 1.0 @@ -34,10 +35,12 @@ def test_that_endpoints_run(): No verification of results. """ + tf.config.run_functions_eagerly(True) global dataset model = SimpleMNL(epochs=20) - model.fit(dataset) + _ = model.fit(dataset) + _ = model.compute_report(dataset) model.evaluate(dataset) model.predict_probas(dataset) assert True