Cette page a été traduite par l'API Cloud Translation.
Switch to English

Contraintes de forme avec le treillis Tensorflow

Voir sur TensorFlow.org Exécuter dans Google Colab Afficher la source sur GitHub Télécharger le carnet

Aperçu

Ce didacticiel est une vue d'ensemble des contraintes et des régulariseurs fournis par la bibliothèque TensorFlow Lattice (TFL). Ici, nous utilisons des estimateurs prédéfinis TFL sur des ensembles de données synthétiques, mais notez que tout dans ce tutoriel peut également être fait avec des modèles construits à partir de couches TFL Keras.

Avant de continuer, assurez-vous que tous les packages requis sont installés sur votre environnement d'exécution (tels qu'importés dans les cellules de code ci-dessous).

Installer

Installation du package TF Lattice:

pip install -q tensorflow-lattice

Importation des packages requis:

import tensorflow as tf

from IPython.core.pylabtools import figsize
import itertools
import logging
import matplotlib
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
import sys
import tensorflow_lattice as tfl
logging.disable(sys.maxsize)

Valeurs par défaut utilisées dans ce guide:

NUM_EPOCHS = 500
BATCH_SIZE = 64
LEARNING_RATE=0.001

Ensemble de données de formation pour les restaurants de classement

Imaginez un scénario simplifié dans lequel nous souhaitons déterminer si les utilisateurs cliqueront ou non sur un résultat de recherche de restaurant. La tâche consiste à prédire le taux de clics (CTR) en fonction des fonctionnalités d'entrée:

  • Note moyenne ( avg_rating ): une caractéristique numérique avec des valeurs dans la plage [1,5].
  • Nombre d'avis ( num_reviews ): une fonctionnalité numérique avec des valeurs plafonnées à 200, que nous utilisons comme mesure de la tendance.
  • Evaluation du dollar ( dollar_rating ): une caractéristique catégorielle avec des valeurs de chaîne dans l'ensemble {"D", "DD", "DDD", "DDDD"}.

Ici, nous créons un ensemble de données synthétique où le vrai CTR est donné par la formule:

$$ CTR = 1 / (1 + exp\{\mbox{b(dollar_rating)}-\mbox{avg_rating}\times log(\mbox{num_reviews}) /4 \}) $$

où $ b (\ cdot) $ dollar_rating chaque dollar_rating en une valeur de base:

$$ \mbox{D}\to 3,\ \mbox{DD}\to 2,\ \mbox{DDD}\to 4,\ \mbox{DDDD}\to 4.5. $$

Cette formule reflète les modèles d'utilisateurs typiques. Par exemple, étant donné que tout le reste est corrigé, les utilisateurs préfèrent les restaurants avec des étoiles plus élevées, et les restaurants "\ $ \ $" recevront plus de clics que "\ $", suivis de "\ $ \ $ \ $" et "\ $ \ $ \ $ \ $ ".

def click_through_rate(avg_ratings, num_reviews, dollar_ratings):
  dollar_rating_baseline = {"D": 3, "DD": 2, "DDD": 4, "DDDD": 4.5}
  return 1 / (1 + np.exp(
      np.array([dollar_rating_baseline[d] for d in dollar_ratings]) -
      avg_ratings * np.log1p(num_reviews) / 4))

Jetons un coup d'œil aux tracés de contour de cette fonction CTR.

def color_bar():
  bar = matplotlib.cm.ScalarMappable(
      norm=matplotlib.colors.Normalize(0, 1, True),
      cmap="viridis",
  )
  bar.set_array([0, 1])
  return bar


def plot_fns(fns, split_by_dollar=False, res=25):
  """Generates contour plots for a list of (name, fn) functions."""
  num_reviews, avg_ratings = np.meshgrid(
      np.linspace(0, 200, num=res),
      np.linspace(1, 5, num=res),
  )
  if split_by_dollar:
    dollar_rating_splits = ["D", "DD", "DDD", "DDDD"]
  else:
    dollar_rating_splits = [None]
  if len(fns) == 1:
    fig, axes = plt.subplots(2, 2, sharey=True, tight_layout=False)
  else:
    fig, axes = plt.subplots(
        len(dollar_rating_splits), len(fns), sharey=True, tight_layout=False)
  axes = axes.flatten()
  axes_index = 0
  for dollar_rating_split in dollar_rating_splits:
    for title, fn in fns:
      if dollar_rating_split is not None:
        dollar_ratings = np.repeat(dollar_rating_split, res**2)
        values = fn(avg_ratings.flatten(), num_reviews.flatten(),
                    dollar_ratings)
        title = "{}: dollar_rating={}".format(title, dollar_rating_split)
      else:
        values = fn(avg_ratings.flatten(), num_reviews.flatten())
      subplot = axes[axes_index]
      axes_index += 1
      subplot.contourf(
          avg_ratings,
          num_reviews,
          np.reshape(values, (res, res)),
          vmin=0,
          vmax=1)
      subplot.title.set_text(title)
      subplot.set(xlabel="Average Rating")
      subplot.set(ylabel="Number of Reviews")
      subplot.set(xlim=(1, 5))

  _ = fig.colorbar(color_bar(), cax=fig.add_axes([0.95, 0.2, 0.01, 0.6]))


figsize(11, 11)
plot_fns([("CTR", click_through_rate)], split_by_dollar=True)

png

Préparation des données

Nous devons maintenant créer nos ensembles de données synthétiques. Nous commençons par générer un ensemble de données simulé des restaurants et de leurs caractéristiques.

def sample_restaurants(n):
  avg_ratings = np.random.uniform(1.0, 5.0, n)
  num_reviews = np.round(np.exp(np.random.uniform(0.0, np.log(200), n)))
  dollar_ratings = np.random.choice(["D", "DD", "DDD", "DDDD"], n)
  ctr_labels = click_through_rate(avg_ratings, num_reviews, dollar_ratings)
  return avg_ratings, num_reviews, dollar_ratings, ctr_labels


np.random.seed(42)
avg_ratings, num_reviews, dollar_ratings, ctr_labels = sample_restaurants(2000)

figsize(5, 5)
fig, axs = plt.subplots(1, 1, sharey=False, tight_layout=False)
for rating, marker in [("D", "o"), ("DD", "^"), ("DDD", "+"), ("DDDD", "x")]:
  plt.scatter(
      x=avg_ratings[np.where(dollar_ratings == rating)],
      y=num_reviews[np.where(dollar_ratings == rating)],
      c=ctr_labels[np.where(dollar_ratings == rating)],
      vmin=0,
      vmax=1,
      marker=marker,
      label=rating)
plt.xlabel("Average Rating")
plt.ylabel("Number of Reviews")
plt.legend()
plt.xlim((1, 5))
plt.title("Distribution of restaurants")
_ = fig.colorbar(color_bar(), cax=fig.add_axes([0.95, 0.2, 0.01, 0.6]))

png

Produisons les ensembles de données de formation, de validation et de test. Lorsqu'un restaurant est visualisé dans les résultats de recherche, nous pouvons enregistrer l'engagement de l'utilisateur (clic ou pas de clic) comme exemple de point.

Dans la pratique, les utilisateurs ne parcourent souvent pas tous les résultats de recherche. Cela signifie que les utilisateurs ne verront probablement que les restaurants déjà considérés comme «bons» par le modèle de classement actuellement utilisé. En conséquence, les «bons» restaurants sont plus souvent impressionnés et surreprésentés dans les ensembles de données de formation. Lors de l'utilisation de plus d'entités, le jeu de données d'entraînement peut présenter de grandes lacunes dans les «mauvaises» parties de l'espace d'entités.

Lorsque le modèle est utilisé pour le classement, il est souvent évalué sur tous les résultats pertinents avec une distribution plus uniforme qui n'est pas bien représentée par l'ensemble de données d'apprentissage. Un modèle flexible et compliqué peut échouer dans ce cas en raison du surajustement des points de données surreprésentés et donc manquer de généralisabilité. Nous traitons ce problème en appliquant la connaissance du domaine pour ajouter des contraintes de forme qui guident le modèle pour faire des prédictions raisonnables lorsqu'il ne peut pas les récupérer dans l'ensemble de données d'entraînement.

Dans cet exemple, l'ensemble de données de formation se compose principalement d'interactions des utilisateurs avec de bons restaurants populaires. L'ensemble de données de test a une distribution uniforme pour simuler le paramètre d'évaluation décrit ci-dessus. Notez qu'un tel jeu de données de test ne sera pas disponible en cas de problème réel.

def sample_dataset(n, testing_set):
  (avg_ratings, num_reviews, dollar_ratings, ctr_labels) = sample_restaurants(n)
  if testing_set:
    # Testing has a more uniform distribution over all restaurants.
    num_views = np.random.poisson(lam=3, size=n)
  else:
    # Training/validation datasets have more views on popular restaurants.
    num_views = np.random.poisson(lam=ctr_labels * num_reviews / 50.0, size=n)

  return pd.DataFrame({
      "avg_rating": np.repeat(avg_ratings, num_views),
      "num_reviews": np.repeat(num_reviews, num_views),
      "dollar_rating": np.repeat(dollar_ratings, num_views),
      "clicked": np.random.binomial(n=1, p=np.repeat(ctr_labels, num_views))
  })


# Generate datasets.
np.random.seed(42)
data_train = sample_dataset(2000, testing_set=False)
data_val = sample_dataset(1000, testing_set=False)
data_test = sample_dataset(1000, testing_set=True)

# Plotting dataset densities.
figsize(12, 5)
fig, axs = plt.subplots(1, 2, sharey=False, tight_layout=False)
for ax, data, title in [(axs[0], data_train, "training"),
                        (axs[1], data_test, "testing")]:
  _, _, _, density = ax.hist2d(
      x=data["avg_rating"],
      y=data["num_reviews"],
      bins=(np.linspace(1, 5, num=21), np.linspace(0, 200, num=21)),
      density=True,
      cmap="Blues",
  )
  ax.set(xlim=(1, 5))
  ax.set(ylim=(0, 200))
  ax.set(xlabel="Average Rating")
  ax.set(ylabel="Number of Reviews")
  ax.title.set_text("Density of {} examples".format(title))
  _ = fig.colorbar(density, ax=ax)

png

Définition des input_fns utilisés pour la formation et l'évaluation:

train_input_fn = tf.compat.v1.estimator.inputs.pandas_input_fn(
    x=data_train,
    y=data_train["clicked"],
    batch_size=BATCH_SIZE,
    num_epochs=NUM_EPOCHS,
    shuffle=False,
)

# feature_analysis_input_fn is used for TF Lattice estimators.
feature_analysis_input_fn = tf.compat.v1.estimator.inputs.pandas_input_fn(
    x=data_train,
    y=data_train["clicked"],
    batch_size=BATCH_SIZE,
    num_epochs=1,
    shuffle=False,
)

val_input_fn = tf.compat.v1.estimator.inputs.pandas_input_fn(
    x=data_val,
    y=data_val["clicked"],
    batch_size=BATCH_SIZE,
    num_epochs=1,
    shuffle=False,
)

test_input_fn = tf.compat.v1.estimator.inputs.pandas_input_fn(
    x=data_test,
    y=data_test["clicked"],
    batch_size=BATCH_SIZE,
    num_epochs=1,
    shuffle=False,
)

Adaptation des arbres à dégradé amélioré

Commençons par seulement deux fonctionnalités: avg_rating et num_reviews .

Nous créons quelques fonctions auxiliaires pour tracer et calculer les métriques de validation et de test.

def analyze_two_d_estimator(estimator, name):
  # Extract validation metrics.
  metric = estimator.evaluate(input_fn=val_input_fn)
  print("Validation AUC: {}".format(metric["auc"]))
  metric = estimator.evaluate(input_fn=test_input_fn)
  print("Testing AUC: {}".format(metric["auc"]))

  def two_d_pred(avg_ratings, num_reviews):
    results = estimator.predict(
        tf.compat.v1.estimator.inputs.pandas_input_fn(
            x=pd.DataFrame({
                "avg_rating": avg_ratings,
                "num_reviews": num_reviews,
            }),
            shuffle=False,
        ))
    return [x["logistic"][0] for x in results]

  def two_d_click_through_rate(avg_ratings, num_reviews):
    return np.mean([
        click_through_rate(avg_ratings, num_reviews,
                           np.repeat(d, len(avg_ratings)))
        for d in ["D", "DD", "DDD", "DDDD"]
    ],
                   axis=0)

  figsize(11, 5)
  plot_fns([("{} Estimated CTR".format(name), two_d_pred),
            ("CTR", two_d_click_through_rate)],
           split_by_dollar=False)

Nous pouvons ajuster les arbres de décision boostés par gradient TensorFlow sur l'ensemble de données:

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
gbt_estimator = tf.estimator.BoostedTreesClassifier(
    feature_columns=feature_columns,
    # Hyper-params optimized on validation set.
    n_batches_per_layer=1,
    max_depth=3,
    n_trees=20,
    min_node_weight=0.1,
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
gbt_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(gbt_estimator, "GBT")
Validation AUC: 0.7248634099960327
Testing AUC: 0.6980501413345337

png

Même si le modèle a capturé la forme générale du vrai CTR et possède des métriques de validation décentes, il a un comportement contre-intuitif dans plusieurs parties de l'espace d'entrée: le CTR estimé diminue à mesure que la note moyenne ou le nombre d'avis augmente. Cela est dû à un manque de points d'échantillonnage dans les zones mal couvertes par l'ensemble de données de formation. Le modèle n'a tout simplement aucun moyen de déduire le comportement correct uniquement à partir des données.

Pour résoudre ce problème, nous appliquons la contrainte de forme selon laquelle le modèle doit générer des valeurs augmentant de manière monotone par rapport à la note moyenne et au nombre d'avis. Nous verrons plus tard comment implémenter cela dans TFL.

Montage d'un DNN

Nous pouvons répéter les mêmes étapes avec un classificateur DNN. Nous pouvons observer un schéma similaire: ne pas avoir suffisamment de points d'échantillonnage avec un petit nombre de revues entraîne une extrapolation absurde. Notez que même si la métrique de validation est meilleure que la solution d'arborescence, la métrique de test est bien pire.

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
dnn_estimator = tf.estimator.DNNClassifier(
    feature_columns=feature_columns,
    # Hyper-params optimized on validation set.
    hidden_units=[16, 8, 8],
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
dnn_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(dnn_estimator, "DNN")
Validation AUC: 0.7518489956855774
Testing AUC: 0.745200514793396

png

Contraintes de forme

TensorFlow Lattice (TFL) se concentre sur l'application de contraintes de forme pour protéger le comportement du modèle au-delà des données d'apprentissage. Ces contraintes de forme sont appliquées aux couches TFL Keras. Leurs détails peuvent être trouvés dans notre article JMLR .

Dans ce didacticiel, nous utilisons des estimateurs prédéfinis TF pour couvrir diverses contraintes de forme, mais notez que toutes ces étapes peuvent être effectuées avec des modèles créés à partir de couches TFL Keras.

Comme avec tout autre estimateur TensorFlow, les estimateurs prédéfinis TFL utilisent des colonnes de caractéristiques pour définir le format d'entrée et utilisent un input_fn d'apprentissage pour transmettre les données. L'utilisation d'estimateurs prédéfinis TFL nécessite également:

  • une configuration de modèle : définition de l'architecture du modèle et des contraintes et régulariseurs de forme par entité.
  • une analyse de caractéristiques input_fn : un TF input_fn passant des données pour l'initialisation TFL.

Pour une description plus détaillée, veuillez vous reporter au didacticiel sur les estimateurs prédéfinis ou à la documentation sur l'API.

Monotonicité

Nous abordons d'abord les problèmes de monotonie en ajoutant des contraintes de forme de monotonie aux deux entités.

Pour demander à TFL d'appliquer des contraintes de forme, nous spécifions les contraintes dans les configurations d'entités . Le code suivant montre comment nous pouvons exiger que la sortie augmente de manière monotone par rapport à num_reviews et avg_rating en définissant monotonicity="increasing" .

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    feature_configs=[
        tfl.configs.FeatureConfig(
            name="num_reviews",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
        ),
        tfl.configs.FeatureConfig(
            name="avg_rating",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
        )
    ])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(tfl_estimator, "TF Lattice")
Validation AUC: 0.7402218580245972
Testing AUC: 0.735464870929718

png

L'utilisation d'un CalibratedLatticeConfig crée un classificateur prédéfini qui applique d'abord un calibrateur à chaque entrée (une fonction linéaire par morceaux pour les entités numériques) suivi d'une couche de treillis pour fusionner de manière non linéaire les entités calibrées. Nous pouvons utiliser tfl.visualization pour visualiser le modèle. En particulier, le graphique suivant montre les deux calibrateurs formés inclus dans le classificateur en conserve.

def save_and_visualize_lattice(tfl_estimator):
  saved_model_path = tfl_estimator.export_saved_model(
      "/tmp/TensorFlow_Lattice_101/",
      tf.estimator.export.build_parsing_serving_input_receiver_fn(
          feature_spec=tf.feature_column.make_parse_example_spec(
              feature_columns)))
  model_graph = tfl.estimators.get_model_graph(saved_model_path)
  figsize(8, 8)
  tfl.visualization.draw_model_graph(model_graph)
  return model_graph

_ = save_and_visualize_lattice(tfl_estimator)

png

Avec les contraintes ajoutées, le CTR estimé augmentera toujours à mesure que la note moyenne augmente ou que le nombre d'avis augmente. Cela se fait en s'assurant que les calibrateurs et le réseau sont monotones.

Rendements décroissants

Des rendements décroissants signifient que le gain marginal de l'augmentation d'une certaine valeur de caractéristique diminuera à mesure que nous augmentons la valeur. Dans notre cas, nous nous attendons à ce que la fonction num_reviews suive ce modèle, afin que nous puissions configurer son calibrateur en conséquence. Notez que nous pouvons décomposer les rendements décroissants en deux conditions suffisantes:

  • le calibrateur augmente de façon monotone, et
  • le calibrateur est concave.
feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    feature_configs=[
        tfl.configs.FeatureConfig(
            name="num_reviews",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_convexity="concave",
            pwl_calibration_num_keypoints=20,
        ),
        tfl.configs.FeatureConfig(
            name="avg_rating",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
        )
    ])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(tfl_estimator, "TF Lattice")
_ = save_and_visualize_lattice(tfl_estimator)
Validation AUC: 0.7382229566574097
Testing AUC: 0.739747166633606

png

png

Remarquez comment la métrique de test s'améliore en ajoutant la contrainte de concavité. L'intrigue de prédiction ressemble également mieux à la vérité terrain.

Contrainte de forme 2D: confiance

Une note de 5 étoiles pour un restaurant avec seulement une ou deux critiques est probablement une note peu fiable (le restaurant peut ne pas être vraiment bon), tandis qu'une note de 4 étoiles pour un restaurant avec des centaines de critiques est beaucoup plus fiable (le restaurant est probablement bon dans ce cas). Nous pouvons voir que le nombre d'avis sur un restaurant influe sur la confiance que nous accordons à sa note moyenne.

Nous pouvons exercer des contraintes de confiance TFL pour informer le modèle que la valeur plus grande (ou plus petite) d'une caractéristique indique plus de confiance ou de confiance envers une autre caractéristique. Cela se fait en définissant la configuration reflects_trust_in dans la configuration des fonctionnalités.

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    feature_configs=[
        tfl.configs.FeatureConfig(
            name="num_reviews",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_convexity="concave",
            pwl_calibration_num_keypoints=20,
            # Larger num_reviews indicating more trust in avg_rating.
            reflects_trust_in=[
                tfl.configs.TrustConfig(
                    feature_name="avg_rating", trust_type="edgeworth"),
            ],
        ),
        tfl.configs.FeatureConfig(
            name="avg_rating",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
        )
    ])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(tfl_estimator, "TF Lattice")
model_graph = save_and_visualize_lattice(tfl_estimator)
Validation AUC: 0.7389558553695679
Testing AUC: 0.7397989630699158

png

png

Le graphique suivant présente la fonction de réseau entraînée. En raison de la contrainte de confiance, nous nous attendons à ce que des valeurs plus élevées de num_reviews calibrées num_reviews une pente plus élevée par rapport à avg_rating calibré, ce qui entraînerait un mouvement plus significatif dans la sortie du réseau.

lat_mesh_n = 12
lat_mesh_x, lat_mesh_y = tfl.test_utils.two_dim_mesh_grid(
    lat_mesh_n**2, 0, 0, 1, 1)
lat_mesh_fn = tfl.test_utils.get_hypercube_interpolation_fn(
    model_graph.output_node.weights.flatten())
lat_mesh_z = [
    lat_mesh_fn([lat_mesh_x.flatten()[i],
                 lat_mesh_y.flatten()[i]]) for i in range(lat_mesh_n**2)
]
trust_plt = tfl.visualization.plot_outputs(
    (lat_mesh_x, lat_mesh_y),
    {"Lattice Lookup": lat_mesh_z},
    figsize=(6, 6),
)
trust_plt.title("Trust")
trust_plt.xlabel("Calibrated avg_rating")
trust_plt.ylabel("Calibrated num_reviews")
trust_plt.show()

png

Calibrateurs de lissage

Jetons maintenant un œil au calibrateur de avg_rating . Bien qu'il augmente de façon monotone, les changements de ses pentes sont brusques et difficiles à interpréter. Cela suggère que nous pourrions envisager de lisser ce calibrateur en utilisant une configuration de régulariseur dans regularizer_configs .

Ici, nous appliquons un régulariseur de wrinkle pour réduire les changements de courbure. Vous pouvez également utiliser le régulariseur laplacian pour aplatir le calibrateur et le régulariseur de hessian pour le rendre plus linéaire.

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    feature_configs=[
        tfl.configs.FeatureConfig(
            name="num_reviews",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_convexity="concave",
            pwl_calibration_num_keypoints=20,
            regularizer_configs=[
                tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
            ],
            reflects_trust_in=[
                tfl.configs.TrustConfig(
                    feature_name="avg_rating", trust_type="edgeworth"),
            ],
        ),
        tfl.configs.FeatureConfig(
            name="avg_rating",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
            regularizer_configs=[
                tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
            ],
        )
    ])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_two_d_estimator(tfl_estimator, "TF Lattice")
_ = save_and_visualize_lattice(tfl_estimator)
Validation AUC: 0.7512660026550293
Testing AUC: 0.7544151544570923

png

png

Les calibrateurs sont maintenant lisses et le CTR global estimé correspond mieux à la vérité terrain. Cela se reflète à la fois dans la métrique de test et dans les tracés de contour.

Monotonicité partielle pour l'étalonnage catégoriel

Jusqu'à présent, nous n'avons utilisé que deux des caractéristiques numériques du modèle. Ici, nous allons ajouter une troisième fonctionnalité en utilisant une couche d'étalonnage catégorique. Encore une fois, nous commençons par configurer des fonctions d'assistance pour le traçage et le calcul métrique.

def analyze_three_d_estimator(estimator, name):
  # Extract validation metrics.
  metric = estimator.evaluate(input_fn=val_input_fn)
  print("Validation AUC: {}".format(metric["auc"]))
  metric = estimator.evaluate(input_fn=test_input_fn)
  print("Testing AUC: {}".format(metric["auc"]))

  def three_d_pred(avg_ratings, num_reviews, dollar_rating):
    results = estimator.predict(
        tf.compat.v1.estimator.inputs.pandas_input_fn(
            x=pd.DataFrame({
                "avg_rating": avg_ratings,
                "num_reviews": num_reviews,
                "dollar_rating": dollar_rating,
            }),
            shuffle=False,
        ))
    return [x["logistic"][0] for x in results]

  figsize(11, 22)
  plot_fns([("{} Estimated CTR".format(name), three_d_pred),
            ("CTR", click_through_rate)],
           split_by_dollar=True)

Pour impliquer la troisième fonctionnalité, dollar_rating , nous devons rappeler que les fonctionnalités catégorielles nécessitent un traitement légèrement différent dans TFL, à la fois comme colonne de fonctionnalités et comme configuration de fonctionnalités. Ici, nous appliquons la contrainte de monotonie partielle selon laquelle les sorties des restaurants «DD» doivent être plus grandes que celles des restaurants «D» lorsque toutes les autres entrées sont fixes. Cela se fait en utilisant le paramètre de monotonicity dans la configuration des fonctionnalités.

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
    tf.feature_column.categorical_column_with_vocabulary_list(
        "dollar_rating",
        vocabulary_list=["D", "DD", "DDD", "DDDD"],
        dtype=tf.string,
        default_value=0),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    feature_configs=[
        tfl.configs.FeatureConfig(
            name="num_reviews",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_convexity="concave",
            pwl_calibration_num_keypoints=20,
            regularizer_configs=[
                tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
            ],
            reflects_trust_in=[
                tfl.configs.TrustConfig(
                    feature_name="avg_rating", trust_type="edgeworth"),
            ],
        ),
        tfl.configs.FeatureConfig(
            name="avg_rating",
            lattice_size=2,
            monotonicity="increasing",
            pwl_calibration_num_keypoints=20,
            regularizer_configs=[
                tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
            ],
        ),
        tfl.configs.FeatureConfig(
            name="dollar_rating",
            lattice_size=2,
            pwl_calibration_num_keypoints=4,
            # Here we only specify one monotonicity:
            # `D` resturants has smaller value than `DD` restaurants
            monotonicity=[("D", "DD")],
        ),
    ])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_three_d_estimator(tfl_estimator, "TF Lattice")
_ = save_and_visualize_lattice(tfl_estimator)
Validation AUC: 0.8136826753616333
Testing AUC: 0.8294427394866943

png

png

Ce calibrateur catégoriel montre la préférence de la sortie du modèle: DD> D> DDD> DDDD, ce qui est cohérent avec notre configuration. Notez qu'il existe également une colonne pour les valeurs manquantes. Bien qu'aucune caractéristique ne manque dans nos données de formation et de test, le modèle nous fournit une imputation pour la valeur manquante si cela se produit pendant la diffusion du modèle en aval.

Ici, nous dollar_rating également le CTR prédit de ce modèle conditionné sur dollar_rating . Notez que toutes les contraintes requises sont remplies dans chacune des tranches.

Calibrage de sortie

Pour tous les modèles TFL que nous avons formés jusqu'à présent, la couche de treillis (indiquée par «Lattice» dans le graphe du modèle) produit directement la prédiction du modèle. Parfois, nous ne savons pas si la sortie du réseau doit être redimensionnée pour émettre des sorties de modèle:

  • les caractéristiques sont des comptes $ log $ tandis que les étiquettes sont des comptes.
  • le réseau est configuré pour avoir très peu de sommets mais la distribution des étiquettes est relativement compliquée.

Dans ces cas, nous pouvons ajouter un autre calibrateur entre la sortie du réseau et la sortie du modèle pour augmenter la flexibilité du modèle. Ajoutons ici une couche de calibrateur avec 5 points clés au modèle que nous venons de construire. Nous ajoutons également un régulariseur pour le calibrateur de sortie pour maintenir la fonction fluide.

feature_columns = [
    tf.feature_column.numeric_column("num_reviews"),
    tf.feature_column.numeric_column("avg_rating"),
    tf.feature_column.categorical_column_with_vocabulary_list(
        "dollar_rating",
        vocabulary_list=["D", "DD", "DDD", "DDDD"],
        dtype=tf.string,
        default_value=0),
]
model_config = tfl.configs.CalibratedLatticeConfig(
    output_calibration=True,
    output_calibration_num_keypoints=5,
    regularizer_configs=[
        tfl.configs.RegularizerConfig(name="output_calib_wrinkle", l2=0.1),
    ],
    feature_configs=[
    tfl.configs.FeatureConfig(
        name="num_reviews",
        lattice_size=2,
        monotonicity="increasing",
        pwl_calibration_convexity="concave",
        pwl_calibration_num_keypoints=20,
        regularizer_configs=[
            tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
        ],
        reflects_trust_in=[
            tfl.configs.TrustConfig(
                feature_name="avg_rating", trust_type="edgeworth"),
        ],
    ),
    tfl.configs.FeatureConfig(
        name="avg_rating",
        lattice_size=2,
        monotonicity="increasing",
        pwl_calibration_num_keypoints=20,
        regularizer_configs=[
            tfl.configs.RegularizerConfig(name="calib_wrinkle", l2=1.0),
        ],
    ),
    tfl.configs.FeatureConfig(
        name="dollar_rating",
        lattice_size=2,
        pwl_calibration_num_keypoints=4,
        # Here we only specify one monotonicity:
        # `D` resturants has smaller value than `DD` restaurants
        monotonicity=[("D", "DD")],
    ),
])
tfl_estimator = tfl.estimators.CannedClassifier(
    feature_columns=feature_columns,
    model_config=model_config,
    feature_analysis_input_fn=feature_analysis_input_fn,
    optimizer=tf.keras.optimizers.Adam(learning_rate=LEARNING_RATE),
    config=tf.estimator.RunConfig(tf_random_seed=42),
)
tfl_estimator.train(input_fn=train_input_fn)
analyze_three_d_estimator(tfl_estimator, "TF Lattice")
_ = save_and_visualize_lattice(tfl_estimator)
Validation AUC: 0.8131163716316223
Testing AUC: 0.830264151096344

png

png

La métrique et les graphiques de test finaux montrent comment l'utilisation de contraintes de bon sens peut aider le modèle à éviter un comportement inattendu et à mieux extrapoler à l'ensemble de l'espace d'entrée.