Latent Class MNL, another example
# Install necessary requirements
# If you run this notebook on Google Colab, or in standalone mode, you need to install the required packages.
# Uncomment the following lines:
# !pip install choice-learn
# If you run the notebook within the GitHub repository, you need to run the following lines, that can skipped otherwise:
import os
import sys
sys.path.append("../../")
import os
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
Let's use the cars preferences dataset in this example.
from choice_learn.datasets import load_car_preferences
dataset = load_car_preferences(as_frame=False, one_hot_features=True)
Load the right classes from choice-learn
from choice_learn.models.simple_mnl import SimpleMNL
from choice_learn.models.latent_class_mnl import LatentClassSimpleMNL
First, we estimate a two classes model with a simple 'Maximum Likelihood Estimation' (MLE) that is performed with a gradient descent (adam optimizer).
lc_model = LatentClassSimpleMNL(
n_latent_classes=2,
fit_method="mle",
optimizer="adam",
epochs=2_000,
batch_size=-1,
lr=5e-3
)
hist = lc_model.fit(dataset, verbose=1)
Let's check that the optimization went well.
plt.plot(hist["train_loss"])
plt.xlabel("Epochs")
plt.ylabel("Training Loss")
plt.title("Negative Log-Likelihood over Epochs")
plt.show()
We observe the difference between the two classes's estimated models.
colors = ["#E69F00", "#56B4E9", "#009E73", "#0072B2", "#D55E00", "#CC79A7", "#F0E442"]
plt.figure(figsize=(12, 12))
plt.subplot(3, 1, 1)
for i in range(lc_model.n_latent_classes):
plt.bar(i, lc_model.get_latent_classes_weights()[i], color=colors[i % len(colors)])
plt.ylabel("Class Probability")
plt.xticks(range(lc_model.n_latent_classes), [f"Class {i}" for i in range(lc_model.n_latent_classes)])
plt.title("Class Probabilities")
plt.subplot(3, 1, 2)
div = 1 / (lc_model.n_latent_classes + 1)
for i in range(lc_model.n_latent_classes):
for j in range(dataset.get_n_shared_features()):
plt.bar(j + i * div, lc_model.models[i].trainable_weights[0][j], color=colors[i % len(colors)], width=div)
plt.ylabel("Feature Weight")
plt.xticks(range(dataset.get_n_shared_features()), [f"{j}" for j in dataset.shared_features_by_choice_names[0]])
plt.title("Shared Feature Weights")
plt.subplot(3, 1, 3)
div = 1 / (lc_model.n_latent_classes + 1)
for i in range(lc_model.n_latent_classes):
for j in range(dataset.get_n_items_features()):
plt.bar(j + i * div, lc_model.models[i].trainable_weights[1][j], color=colors[i % len(colors)], width=div)
plt.ylabel("Feature Weight")
plt.xticks(range(dataset.get_n_items_features()), [f"{j}" for j in dataset.items_features_by_choice_names[0]])
plt.title("Item Feature Weights")
plt.tight_layout()
plt.show()
Let's do the same with a training that is performed with the 'Expectation-Maximization' (EM) algorithm. For the maximization step, the weights are computed using the lbfgs algorithm.
em_model = LatentClassSimpleMNL(n_latent_classes=2, fit_method="em", optimizer="lbfgs", epochs=400, lbfgs_tolerance=1e-6)
em_model.instantiate(
n_items=dataset.get_n_items(),
n_shared_features=dataset.get_n_shared_features(),
n_items_features=dataset.get_n_items_features(),
base_seed=1024
)
em_hist = em_model.fit(dataset, verbose=0)
colors = ["#E69F00", "#56B4E9", "#009E73", "#0072B2", "#D55E00", "#CC79A7", "#F0E442"]
plt.figure(figsize=(12, 12))
plt.subplot(3, 1, 1)
for i in range(em_model.n_latent_classes):
plt.bar(i, em_model.get_latent_classes_weights()[i], color=colors[i % len(colors)])
plt.ylabel("Class Probability")
plt.xticks(range(em_model.n_latent_classes), [f"Class {i}" for i in range(em_model.n_latent_classes)])
plt.title("Class Probabilities")
plt.subplot(3, 1, 2)
div = 1 / (em_model.n_latent_classes + 1)
for i in range(em_model.n_latent_classes):
for j in range(dataset.get_n_shared_features()):
plt.bar(j + i * div, em_model.models[i].trainable_weights[0][j], color=colors[i % len(colors)], width=div)
plt.ylabel("Feature Weight")
plt.xticks(range(dataset.get_n_shared_features()), [f"{j}" for j in dataset.shared_features_by_choice_names[0]])
plt.title("Shared Feature Weights")
plt.subplot(3, 1, 3)
div = 1 / (em_model.n_latent_classes + 1)
for i in range(em_model.n_latent_classes):
for j in range(dataset.get_n_items_features()):
plt.bar(j + i * div, em_model.models[i].trainable_weights[1][j], color=colors[i % len(colors)], width=div)
plt.ylabel("Feature Weight")
plt.xticks(range(dataset.get_n_items_features()), [f"{j}" for j in dataset.items_features_by_choice_names[0]])
plt.title("Item Feature Weights")
plt.tight_layout()
plt.show()
Now let's estimate a latent class model for different number of classes. We use the MLE and lbfgs to get faster results (that are slightly less precise here).
nlls = []
for n_classes in range(2, 8):
lc_model = LatentClassSimpleMNL(
n_latent_classes=n_classes,
fit_method="mle",
optimizer="lbfgs",
epochs=2_000,
lbfgs_tolerance=1e-20
)
hist = lc_model.fit(dataset, verbose=0)
nll = lc_model.evaluate(dataset)
nlls.append(nll)