Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
tensorflow
GitHub Repository: tensorflow/docs-l10n
Path: blob/master/site/pt-br/lattice/tutorials/shape_constraints.ipynb
25118 views
Kernel: Python 3
#@title Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License.

Restrições de formato com o TensorFlow Lattice

Visão geral

Este tutorial é uma visão geral das restrições e dos regularizadores fornecidos pela biblioteca do TensorFlow Lattice (TFL). Vamos usar os estimadores predefinidos do TFL em datasets sintéticos, mas observe que o tutorial inteiro também pode ser realizado com modelos construídos a partir de camadas Keras do TFL.

Antes de continuar, confira se o runtime tem todos os pacotes necessários instalados (conforme importados nas células de código abaixo).

Configuração

Instale o pacote do TF Lattice:

#@test {"skip": true} !pip install tensorflow-lattice tensorflow_decision_forests

Importe os pacotes necessários:

import tensorflow as tf import tensorflow_lattice as tfl import tensorflow_decision_forests as tfdf 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 tempfile logging.disable(sys.maxsize)

Valores padrão usados neste guia:

NUM_EPOCHS = 1000 BATCH_SIZE = 64 LEARNING_RATE=0.01

Treinamento de dataset para classificar restaurantes

Imagine um cenário simples em que queremos determinar se os usuários clicarão ou não no resultado de pesquisa de um restaurante. A tarefa é prever a taxa de clique (CTR) a partir das características de entrada:

  • Nota média (avg_rating): característica numérica com valores no intervalo [1,5].

  • Número de avaliações (num_reviews): característica numérica com valores de até 200, usada como medida de popularidade.

  • Classificação de dólar (dollar_rating): característica numérica com valores de string no conjunto {"D", "DD", "DDD", "DDDD"}.

Aqui, usamos um dataset sintético onde a CTR real é obtida com a seguinte fórmula: ParseError: KaTeX parse error: Expected 'EOF', got '_' at position 35: …{\mbox{b(dollar_̲rating)}-\mbox{… em que b()b(\cdot) traduz cada dollar_rating em um valor de linha de base: D3, DD2, DDD4, DDDD4.5. \mbox{D}\to 3,\ \mbox{DD}\to 2,\ \mbox{DDD}\to 4,\ \mbox{DDDD}\to 4.5.

Essa fórmula reflete padrões típicos de usuários, por exemplo, com o resto fixo, os usuários preferem restaurantes com notas de mais estrelas, e restaurantes "$$" recebem mais cliques que "$", seguidos por "$$$" e "$$$$".

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

Vamos conferir os plots de contorno dessa função de 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)

Preparando os dados

Agora precisamos criar nossos datasets sintéticos. Vamos começar com a geração de um dataset simulado de restaurantes e 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]))

Vamos produzir os datasets de treinamento, validação e teste. Quando um restaurante é visualizado nos resultados da pesquisa, podemos registrar o engajamento do usuário (clique ou sem clique) como um ponto de amostragem.

Na prática, os usuários geralmente não navegam por todos os resultados da pesquisa. Portanto, é provável que os usuários só vejam restaurantes já considerados "bons" pelo modelo de classificação atualmente em uso. Como resultado, os restaurantes "bons" terão impressões mais frequentes e maior representação nos datasets de treinamento. Ao usar mais características, o dataset de treinamento pode ter grandes lacunas nas partes "ruins" do espaço de características.

Quando o modelo é usado para classificação, é geralmente avaliado em todos os resultados relevantes com uma distribuição mais uniforme que não é bem representada pelo dataset de treinamento. Nesse caso, um modelo flexível e complicado pode falhar devido ao overfitting dos pontos de dados mais representados e, por isso, não ter capacidade de generalização. Resolvemos esse problema com a aplicação do conhecimento de domínio para adicionar restrições de formato que orientam o modelo a fazer previsões razoáveis quando ele não consegue determinar a partir do dataset de treinamento.

Neste exemplo, o dataset de treinamento consiste na maior parte em interações de usuários com restaurantes bons e populares. O dataset de teste tem uma distribuição uniforme para simular o cenário de avaliação discutido acima. Observe que esse dataset de teste não ficará disponível em um cenário 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)), 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)

Defina as input_fns usadas para treinamento e avaliação:

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

Fitting de árvores impulsionadas por gradientes

Vamos começar com duas características: avg_rating e num_reviews.

Criamos algumas funções auxiliares para o plot e cálculo das métricas de validação e teste.

def analyze_two_d_estimator(estimator, name): # Extract validation metrics. if isinstance(estimator, tf.estimator.Estimator): metric = estimator.evaluate(input_fn=val_input_fn) else: metric = estimator.evaluate( tfdf.keras.pd_dataframe_to_tf_dataset(data_val, label="clicked"), return_dict=True, verbose=0) print("Validation AUC: {}".format(metric["auc"])) if isinstance(estimator, tf.estimator.Estimator): metric = estimator.evaluate(input_fn=test_input_fn) else: metric = estimator.evaluate( tfdf.keras.pd_dataframe_to_tf_dataset(data_test, label="clicked"), return_dict=True, verbose=0) print("Testing AUC: {}".format(metric["auc"])) def two_d_pred(avg_ratings, num_reviews): if isinstance(estimator, tf.estimator.Estimator): 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] else: return estimator.predict( tfdf.keras.pd_dataframe_to_tf_dataset( pd.DataFrame({ "avg_rating": avg_ratings, "num_reviews": num_reviews, })), verbose=0) 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 as árvores de decisão impulsionadas por gradientes do TensorFlow no dataset:

gbt_model = tfdf.keras.GradientBoostedTreesModel( features=[ tfdf.keras.FeatureUsage(name="num_reviews"), tfdf.keras.FeatureUsage(name="avg_rating") ], exclude_non_specified_features=True, num_threads=1, num_trees=32, max_depth=6, min_examples=10, growing_strategy="BEST_FIRST_GLOBAL", random_seed=42, temp_directory=tempfile.mkdtemp(), ) gbt_model.compile(metrics=[tf.keras.metrics.AUC(name="auc")]) gbt_model.fit( tfdf.keras.pd_dataframe_to_tf_dataset(data_train, label="clicked"), validation_data=tfdf.keras.pd_dataframe_to_tf_dataset( data_val, label="clicked"), verbose=0) analyze_two_d_estimator(gbt_model, "GBT")

Mesmo que o modelo tenha capturado o formato geral da CTR real e tenha métricas de validação decentes, ele apresenta um comportamento contraditório em várias partes do espaço de entrada: a CTR estimada diminui conforme a nota média ou o número de avaliações aumenta. Isso se deve à ausência de pontos de amostragem em áreas que não são bem cobertas pelo dataset de treinamento. O modelo simplesmente não tem como deduzir o comportamento correto usando apenas os dados.

Para resolver esse problema, aplicamos a restrição de formato para o modelo gerar valores que aumentem monotonicamente em relação à nota média e ao número de avaliações. Mais tarde, vamos ver como implementar isso no TFL.

Fitting de uma DNN

Podemos repetir os mesmos passos com um classificador de DNN. Podemos observar um padrão semelhante: não ter pontos de amostragem suficientes com um número pequeno de avaliações resulta na extrapolação sem sentido. Observe que, embora a métrica de validação seja melhor do que a solução de árvore, a métrica de teste é muito pior.

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.legacy.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")

Restrições de formato

O foco do TensorFlow Lattice (TFL) é impor restrições de formato para proteger o comportamento do modelo além dos dados de treinamento. Essas restrições de formato são aplicadas às camadas Keras do TFL. Confira detalhes em nosso artigo do JMLR{/a}.

Neste tutorial, usamos estimadores predefinidos do TF para cobrir várias restrições de formato, mas todas essas etapas podem ser seguidas com modelos criados a partir de camadas Keras do TFL.

Assim como qualquer outro estimador do TensorFlow, os estimadores predefinidos do TFL usam colunas de características para definir o formato de entrada e utilizam uma input_fn de treinamento para passar os dados. O uso dos estimadores predefinidos do TFL também exige o seguinte:

  • uma configuração de modelo: define a arquitetura do modelo e as restrições de formato e os regularizadores por característica.

  • uma input_fn de análise de características: uma input_fn do TF que passa dados para a inicialização do TFL.

Para uma descrição mais completa, consulte o tutorial de estimadores predefinidos ou a documentação da API.

Monotonicidade

Primeiro, resolvemos as questões de monotonicidade ao adicionar restrições de formato de monotonicidade a ambas as características.

Para instruir o TFL a impor restrições de formato, especificamos as restrições nas configurações das características. O código a seguir mostra como podemos exigir que a saída aumente de maneira monotônica em relação a num_reviews e a avg_rating definindo 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.legacy.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")

O uso de uma CalibratedLatticeConfig cria um classificador predefinido que, primeiro, aplica um calibrador a cada entrada (uma função linear por partes para características numéricas) e, depois, uma camada lattice para mesclar de maneira não linear as características calibradas. Podemos usar tfl.visualization para visualizar o modelo. Em especial, o plot a seguir mostra os dois calibradores treinados incluídos no classificador predefinido.

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)

Com as restrições adicionadas, a CTR estimada sempre aumentará conforme a nota média ou o número de avaliações aumentar. Isso é possível ao garantir que os calibradores e o lattice sejam monotônicos.

Retornos decrescentes

Retornos decrescentes significa que o ganho mínimo com o aumento do valor de uma determinada característica diminuirá à medida que o valor aumentar. Em nosso caso, esperamos que a característica num_reviews siga esse padrão, para configurar o calibrador de acordo com isso. Observe que podemos decompor os retornos decrescentes em duas condições suficientes:

  • o calibrador aumenta de maneira monotônica, e

  • o calibrador é 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.legacy.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)

Veja como a métrica de teste melhora ao adicionar a restrição de concavidade. O plot de previsão também se parece mais com a verdade.

Restrição de formato 2D: confiança

Uma nota de 5 estrelas para um restaurante com só uma ou duas avaliações é provavelmente uma nota não confiável (o restaurante pode não ser realmente bom), enquanto uma nota de 4 estrelas para um restaurante com centenas de avaliações é muito mais confiável (nesse caso, o restaurante provavelmente é bom). Podemos ver que o número de avaliações de um restaurante afeta o nível de confiança que temos na nota média.

Podemos aplicar as restrições de confiança do TFL para informar o modelo que um valor maior (ou menor) de uma característica indica uma maior confiança ou confiabilidade de outra característica. Isso é possível ao definir reflects_trust_in nas configurações das características.

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

O plot a seguir apresenta a função lattice treinada. Devido à restrição de confiança, esperamos que valores maiores de num_reviews calibrado forcem uma inclinação superior em relação ao avg_rating calibrado, resultando em uma mudança significativa na saída do lattice.

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

Suavizando calibradores

Agora vamos observar o calibrador de avg_rating. Embora aumente monotonicamente, as mudanças na inclinação são abruptas e difíceis de interpretar. Isso sugere que devemos considerar suavizar esse calibrador usando uma configuração de regularizador em regularizer_configs.

Aqui aplicamos um regularizador wrinkle para reduzir as mudanças na curvatura. Você também pode usar o regularizador laplacian para achatar o calibrador, e o regularizador hessian para deixá-lo mais linear.

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

Os calibradores foram suavizados e a CTR estimada geral corresponde melhor à verdade. Isso é refletido na métrica de teste e nos plots de contorno.

Monotonicidade parcial para calibração categórica

Até agora, usamos apenas duas das características numéricas no modelo. Vamos adicionar uma terceira característica usando uma camada de calibração categórica. Novamente, começamos com a configuração das funções helper para o plot e cálculo das 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 envolver a terceira característica, dollar_rating, devemos lembrar que as características categóricas precisam de um tratamento ligeiramente diferente no TFL, tanto como coluna e configuração de característica. Aplicamos a restrição de monotonicidade parcial que faz com que restaurantes "DD" sejam maiores do que restaurantes "D" quando todas as outras entradas estiverem fixas. Isso é possível ao usar monotonicity nas configurações das 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.legacy.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)

Esse calibrador categórico mostra a preferência da saída do modelo: DD > D > DDD > DDDD, que é consistente com nossa configuração. Observe que também há uma coluna para valores ausentes. Apesar de não haver nenhuma característica ausente nos dados de treinamento e teste, o modelo oferece uma imputação para o valor ausente que pode ocorrer durante o serving de modelo downstream.

Aqui também plotamos a CTR prevista desse modelo condicionado em dollar_rating. Note que todas as restrições exigidas são atendidas em cada uma das fatias.

Calibração da saída

Para todos os modelos do TFL que treinamos até agora, a camada lattice (indicada como "Lattice" no grafo do modelo) gera diretamente a previsão do modelo. Às vezes, não temos certeza se a saída do lattice deve ser reescalada para emitir saídas do modelo:

  • as características são contagens loglog, enquanto os rótulos são contagens.

  • o lattice é configurado para ter poucos vértices, mas a distribuição de rótulos é relativamente complicada.

Nesses casos, podemos adicionar outro calibrador entre a saída do lattice e a saída do modelo para aumentar a flexibilidade do modelo. Aqui adicionamos uma camada de calibrador com 5 keypoints ao modelo que acabamos de criar. Também acrescentamos um regularizador para o calibrador de saída deixar a função suave.

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

A métrica de teste final e os plots mostram como o uso das restrições de senso comum pode ajudar o modelo a evitar comportamentos inesperados e extrapolar melhor todo o espaço de entrada.