Restricciones de forma con Tensorflow Lattice

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno

Descripción general

Este instructivo es una descripción general de las restricciones y los regularizadores proporcionados por la biblioteca TensorFlow Lattice (TFL). Aquí usamos estimadores enlatados de TFL en conjuntos de datos sintéticos, pero tenga en cuenta que todo en este tutorial también se puede hacer con modelos construidos a partir de capas de TFL Keras.

Antes de continuar, asegúrese de que su tiempo de ejecución tenga todos los paquetes requeridos instalados (como se importaron en las celdas de código a continuación).

Configuración

Instalación del paquete TF Lattice:

pip install -q tensorflow-lattice

Importación de paquetes requeridos:

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)

Valores predeterminados utilizados en esta guía:

NUM_EPOCHS = 1000
BATCH_SIZE = 64
LEARNING_RATE=0.01

Conjunto de datos de formación para la clasificación de restaurantes

Imagine un escenario simplificado en el que queremos determinar si los usuarios harán clic en el resultado de la búsqueda de un restaurante o no. La tarea es predecir la tasa de clics (CTR) dadas las características de entrada:

  • Calificación promedio ( avg_rating ): una característica numérica con valores en el rango [1,5].
  • Número de reseñas ( num_reviews ): Una función numérica con valores limitados a 200, que usamos como medida de tendencia.
  • Calificación en dólares ( dollar_rating ): una característica categórica con valores de cadena en el conjunto {"D", "DD", "DDD", "DDDD"}.

Aquí creamos un conjunto de datos sintéticos donde el verdadero CTR viene dado por la fórmula:

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

donde $ b (\ cdot) $ traduce cada dollar_rating de dollar_rating a un valor de referencia:

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

Esta fórmula refleja los patrones típicos de los usuarios. Por ejemplo, dado que todo lo demás se solucionó, los usuarios prefieren restaurantes con calificaciones de estrellas más altas y los restaurantes "\ $ \ $" recibirán más clics que "\ $", seguidos de "\ $ \ $ \ $" y "\ $ \ $ \ $ PS

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))

Echemos un vistazo a los gráficos de contorno de esta función 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

Preparando datos

Ahora necesitamos crear nuestros conjuntos de datos sintéticos. Comenzamos generando un conjunto de datos simulado de restaurantes y sus características.

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

Produzcamos los conjuntos de datos de entrenamiento, validación y prueba. Cuando se ve un restaurante en los resultados de búsqueda, podemos registrar la participación del usuario (clic o no) como un punto de muestra.

En la práctica, los usuarios a menudo no revisan todos los resultados de búsqueda. Esto significa que es probable que los usuarios solo vean los restaurantes que ya se consideran "buenos" por el modelo de clasificación actual en uso. Como resultado, los "buenos" restaurantes quedan impresionados y sobrerrepresentados con más frecuencia en los conjuntos de datos de formación. Cuando se utilizan más funciones, el conjunto de datos de entrenamiento puede tener grandes lagunas en partes "malas" del espacio de funciones.

Cuando se usa el modelo para la clasificación, a menudo se evalúa en todos los resultados relevantes con una distribución más uniforme que no está bien representada por el conjunto de datos de entrenamiento. Un modelo flexible y complicado puede fallar en este caso debido a un ajuste excesivo de los puntos de datos sobrerrepresentados y, por lo tanto, a la falta de generalización. Manejamos este problema aplicando el conocimiento del dominio para agregar restricciones de forma que guían al modelo para hacer predicciones razonables cuando no puede recogerlas del conjunto de datos de entrenamiento.

En este ejemplo, el conjunto de datos de entrenamiento consiste principalmente en interacciones del usuario con restaurantes buenos y populares. El conjunto de datos de prueba tiene una distribución uniforme para simular la configuración de evaluación discutida anteriormente. Tenga en cuenta que dicho conjunto de datos de prueba no estará disponible en un entorno de problema real.

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(500, testing_set=False)
data_val = sample_dataset(500, testing_set=False)
data_test = sample_dataset(500, 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

Definición de input_fns usado para entrenamiento y evaluación:

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,
)

Ajuste de árboles reforzados con degradado

Comencemos con solo dos funciones: avg_rating y num_reviews .

Creamos algunas funciones auxiliares para trazar y calcular métricas de validación y prueba.

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)

Podemos ajustar los árboles de decisión potenciados por gradiente de TensorFlow en el conjunto de datos:

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=2,
    n_trees=50,
    learning_rate=0.05,
    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.6333084106445312
Testing AUC: 0.774649977684021

png

Aunque el modelo ha capturado la forma general del verdadero CTR y tiene métricas de validación decentes, tiene un comportamiento contrario a la intuición en varias partes del espacio de entrada: el CTR estimado disminuye a medida que aumenta la calificación promedio o el número de reseñas. Esto se debe a la falta de puntos de muestra en áreas que no están bien cubiertas por el conjunto de datos de entrenamiento. El modelo simplemente no tiene forma de deducir el comportamiento correcto únicamente a partir de los datos.

Para resolver este problema, aplicamos la restricción de forma de que el modelo debe generar valores que aumenten monótonamente con respecto tanto a la calificación promedio como al número de revisiones. Más adelante veremos cómo implementar esto en TFL.

Colocación de un DNN

Podemos repetir los mismos pasos con un clasificador DNN. Podemos observar un patrón similar: no tener suficientes puntos de muestra con un número pequeño de revisiones da como resultado una extrapolación sin sentido. Tenga en cuenta que aunque la métrica de validación es mejor que la solución de árbol, la métrica de prueba es mucho peor.

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.6696228981018066
Testing AUC: 0.750156044960022

png

Restricciones de forma

TensorFlow Lattice (TFL) se centra en imponer restricciones de forma para salvaguardar el comportamiento del modelo más allá de los datos de entrenamiento. Estas restricciones de forma se aplican a las capas de TFL Keras. Sus detalles se pueden encontrar en nuestro artículo JMLR .

En este tutorial usamos estimadores enlatados TF para cubrir varias restricciones de forma, pero tenga en cuenta que todos estos pasos se pueden realizar con modelos creados a partir de capas de TFL Keras.

Al igual que con cualquier otro estimador de TensorFlow, los estimadores enlatados de TFL usan columnas de características para definir el formato de entrada y usan un input_fn de entrenamiento para pasar los datos. El uso de estimadores enlatados de TFL también requiere:

  • una configuración de modelo : definir la arquitectura del modelo y las restricciones y regularizadores de forma por característica.
  • un análisis de características input_fn : un TF input_fn que pasa datos para la inicialización de TFL.

Para obtener una descripción más detallada, consulte el tutorial de estimadores predefinidos o los documentos de la API.

Monotonicidad

Primero abordamos las preocupaciones de monotonicidad agregando restricciones de forma de monotonicidad a ambas características.

Para instruir a TFL para hacer cumplir las restricciones de forma, especificamos las restricciones en las configuraciones de características . El siguiente código muestra cómo podemos exigir que la salida aumente de forma monótona con respecto tanto al num_reviews avg_rating al valor avg_rating estableciendo 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.6565533876419067
Testing AUC: 0.7784258723258972

png

El uso de CalibratedLatticeConfig crea un clasificador enlatado que primero aplica un calibrador a cada entrada (una función lineal por partes para características numéricas) seguido de una capa de celosía para fusionar de forma no lineal las características calibradas. Podemos usar tfl.visualization para visualizar el modelo. En particular, la siguiente gráfica muestra los dos calibradores entrenados incluidos en el clasificador enlatado.

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

Con las restricciones agregadas, el CTR estimado siempre aumentará a medida que aumenta la calificación promedio o aumenta el número de reseñas. Esto se hace asegurándose de que los calibradores y el enrejado sean monótonos.

Rendimientos decrecientes

Los rendimientos decrecientes significa que la ganancia marginal de aumentar un determinado valor de característica disminuirá a medida que aumentamos el valor. En nuestro caso, esperamos que la función num_reviews siga este patrón, por lo que podemos configurar su calibrador en consecuencia. Observe que podemos descomponer los rendimientos decrecientes en dos condiciones suficientes:

  • el calibrador aumenta monótonamente, y
  • el calibrador es cóncavo.
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.6409633755683899
Testing AUC: 0.7891247272491455

png

png

Observe cómo mejora la métrica de prueba al agregar la restricción de concavidad. La trama de predicción también se parece mejor a la verdad básica.

Restricción de forma 2D: confianza

Una calificación de 5 estrellas para un restaurante con solo una o dos reseñas es probablemente una calificación poco confiable (el restaurante puede no ser bueno), mientras que una calificación de 4 estrellas para un restaurante con cientos de reseñas es mucho más confiable probablemente bueno en este caso). Podemos ver que la cantidad de reseñas de un restaurante afecta la confianza que depositamos en su calificación promedio.

Podemos ejercer restricciones de confianza de TFL para informar al modelo que el valor mayor (o menor) de una característica indica más dependencia o confianza de otra característica. Esto se hace estableciendo la configuración reflects_trust_in en la configuración de la función.

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.6409633755683899
Testing AUC: 0.7891043424606323

png

png

La siguiente gráfica presenta la función de celosía entrenada. Debido a la restricción de confianza, esperamos que valores más grandes de calibrados num_reviews obligarían pendiente superior con respecto al calibrado avg_rating , lo que resulta en un movimiento más significativo en la salida de celosía.

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

Calibradores de suavizado

Echemos ahora un vistazo al calibrador de avg_rating . Aunque aumenta monótonamente, los cambios en sus pendientes son abruptos y difíciles de interpretar. Eso sugiere que podríamos querer considerar suavizar este calibrador usando una configuración de regularizador en regularizer_configs .

Aquí aplicamos un regularizador de wrinkle para reducir los cambios en la curvatura. También puede utilizar el regularizador laplacian para aplanar el calibrador y el regularizador de hessian para hacerlo más lineal.

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.6465646028518677
Testing AUC: 0.7948372960090637

png

png

Los calibradores ahora son suaves y el CTR estimado general coincide mejor con la verdad del terreno. Esto se refleja tanto en la métrica de prueba como en los gráficos de contorno.

Monotonicidad parcial para calibración categórica

Hasta ahora hemos estado usando solo dos de las características numéricas del modelo. Aquí agregaremos una tercera característica usando una capa de calibración categórica. Nuevamente, comenzamos configurando funciones auxiliares para trazar y calcular métricas.

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)

Para involucrar la tercera característica, dollar_rating , debemos recordar que las características categóricas requieren un tratamiento ligeramente diferente en TFL, tanto como columna de características como como configuración de características. Aquí aplicamos la restricción de monotonicidad parcial de que las salidas para los restaurantes "DD" deben ser mayores que los restaurantes "D" cuando todas las demás entradas son fijas. Esto se hace usando la configuración de monotonicity en la configuración de características.

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.7699775695800781
Testing AUC: 0.8594040274620056

png

png

Este calibrador categórico muestra la preferencia de la salida del modelo: DD> D> DDD> DDDD, que es consistente con nuestra configuración. Observe que también hay una columna para los valores perdidos. Aunque no hay ninguna característica que falte en nuestros datos de entrenamiento y prueba, el modelo nos proporciona una imputación del valor faltante en caso de que ocurra durante la entrega del modelo en sentido descendente.

Aquí también dollar_rating el CTR predicho de este modelo condicionado a dollar_rating . Observe que todas las restricciones que requerimos se cumplen en cada uno de los cortes.

Calibración de salida

Para todos los modelos TFL que hemos entrenado hasta ahora, la capa de celosía (indicada como "Lattice" en el gráfico del modelo) genera directamente la predicción del modelo. A veces no estamos seguros de si la salida de celosía debe reescalarse para emitir salidas de modelo:

  • las características son recuentos de $ log $ mientras que las etiquetas son recuentos.
  • la celosía está configurada para tener muy pocos vértices, pero la distribución de etiquetas es relativamente complicada.

En esos casos, podemos agregar otro calibrador entre la salida de celosía y la salida del modelo para aumentar la flexibilidad del modelo. Aquí agreguemos una capa de calibrador con 5 puntos clave al modelo que acabamos de construir. También agregamos un regularizador para el calibrador de salida para mantener la función sin problemas.

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.7697908878326416
Testing AUC: 0.861327052116394

png

png

La métrica de prueba final y los gráficos muestran cómo el uso de restricciones de sentido común puede ayudar al modelo a evitar comportamientos inesperados y extrapolar mejor a todo el espacio de entrada.