Exemplo de otimização restrita do TensorFlow usando o conjunto de dados CelebA

Ver no TensorFlow.org Executar no Google Colab Ver no GitHub Baixar caderno

Este bloco de notas demonstra uma maneira fácil de criar e otimizar problemas restritos usando a biblioteca TFCO. Este método pode ser útil para melhorar os modelos quando descobrimos que eles não têm um desempenho igualmente bom em diferentes fatias de nossos dados, que podemos identificar usando indicadores de imparcialidade . O segundo princípio de IA do Google afirma que nossa tecnologia deve evitar criar ou reforçar preconceitos injustos e acreditamos que essa técnica pode ajudar a melhorar a justiça do modelo em algumas situações. Em particular, este notebook irá:

  • Treine um modelo de rede neural simples e irrestrito para detectar o sorriso de uma pessoa em imagens usando tf.keras e o conjunto de dados CelebFaces Attributes ( CelebA ) em grande escala.
  • Avalie o desempenho do modelo em relação a uma métrica de justiça comumente usada em todas as faixas etárias, usando indicadores de justiça.
  • Configure um problema simples de otimização restrita para obter um desempenho mais justo em todas as faixas etárias.
  • Treine novamente o modelo agora restrito e avalie o desempenho novamente, garantindo que nossa métrica de justiça escolhida tenha melhorado.

Última atualização: 3/11 de fevereiro de 2020

Instalação

Este notebook foi criado no Colaboratory , conectado ao back-end Python 3 do Google Compute Engine. Se você deseja hospedar este notebook em um ambiente diferente, não deverá enfrentar nenhum problema grave, desde que inclua todos os pacotes necessários nas células abaixo.

Observe que na primeira vez que você executa as instalações do pip, pode ser solicitado que você reinicie o tempo de execução devido a pacotes desatualizados pré-instalados. Depois de fazer isso, os pacotes corretos serão usados.

Instalações pip

Dependendo de quando você executa a célula abaixo, você pode receber um aviso sobre a versão padrão do TensorFlow no Colab mudando para o TensorFlow 2.X em breve. Você pode ignorar esse aviso com segurança, pois este notebook foi projetado para ser compatível com o TensorFlow 1.X e 2.X.

Módulos de importação

Além disso, adicionamos algumas importações que são específicas para Indicadores de Equidade que usaremos para avaliar e visualizar o desempenho do modelo.

Embora o TFCO seja compatível com a execução rápida e gráfica, este notebook assume que a execução antecipada está ativada por padrão, pois está no TensorFlow 2.x. Para garantir que nada seja interrompido, a execução rápida será habilitada na célula abaixo.

Habilite a execução rápida e versões de impressão

Eager execution enabled by default.
TensorFlow 2.4.1
TFMA 0.29.0
TFDS 4.2.0
FI 0.29.0

Conjunto de dados CelebA

CelebA é um conjunto de dados de atributos de rosto em grande escala com mais de 200.000 imagens de celebridades, cada uma com 40 anotações de atributos (como tipo de cabelo, acessórios de moda, características faciais, etc.) e 5 locais de referência (posições de olhos, boca e nariz). Para mais detalhes, dê uma olhada no jornal . Com a permissão dos proprietários, armazenamos esse conjunto de dados no Google Cloud Storage e o acessamos principalmente por meio do TensorFlow Datasets ( tfds ) .

Neste caderno:

  • Nosso modelo tentará classificar se o sujeito da imagem está sorrindo, conforme representado pelo atributo "Sorrindo" * .
  • As imagens serão redimensionadas de 218x178 a 28x28 para reduzir o tempo de execução e a memória durante o treinamento.
  • O desempenho do nosso modelo será avaliado em grupos de idade, usando o atributo binário "Young". Chamaremos isso de "faixa etária" neste caderno.

* Embora haja poucas informações disponíveis sobre a metodologia de rotulagem para este conjunto de dados, assumiremos que o atributo "Sorrir" foi determinado por uma expressão de satisfação, gentileza ou diversão no rosto do sujeito. Para o propósito deste estudo de caso, consideraremos esses rótulos como verdades fundamentais.

gcs_base_dir = "gs://celeb_a_dataset/"
celeb_a_builder = tfds.builder("celeb_a", data_dir=gcs_base_dir, version='2.0.0')

celeb_a_builder.download_and_prepare()

num_test_shards_dict = {'0.3.0': 4, '2.0.0': 2} # Used because we download the test dataset separately
version = str(celeb_a_builder.info.version)
print('Celeb_A dataset version: %s' % version)
Celeb_A dataset version: 2.0.0

Testar funções auxiliares do conjunto de dados

Ressalvas

Antes de prosseguir, há várias considerações a ter em mente ao usar o CelebA:

  • Embora, em princípio, este bloco de notas pudesse usar qualquer conjunto de dados de imagens de rosto, o CelebA foi escolhido porque contém imagens de domínio público de figuras públicas.
  • Todas as anotações de atributo no CelebA são operacionalizadas como categorias binárias. Por exemplo, o atributo "Jovem" (conforme determinado pelos rotuladores do conjunto de dados) é indicado como presente ou ausente na imagem.
  • As categorizações do CelebA não refletem a diversidade humana real de atributos.
  • Para os fins deste bloco de notas, o recurso que contém o atributo "Jovem" é referido como "faixa etária", onde a presença do atributo "Jovem" em uma imagem é rotulada como um membro do grupo de idade "Jovem" e o a ausência do atributo "Jovem" é rotulada como membro do grupo de idade "Não é jovem". Essas são suposições feitas porque essas informações não são mencionadas no artigo original .
  • Assim, o desempenho nos modelos treinados neste notebook está vinculado às formas como os atributos foram operacionalizados e anotados pelos autores do CelebA.
  • Este modelo não deve ser usado para fins comerciais, pois isso violaria o contrato de pesquisa não comercial da CelebA .

Configurando funções de entrada

As células subsequentes ajudarão a otimizar o pipeline de entrada, bem como visualizar o desempenho.

Primeiro, definimos algumas variáveis ​​relacionadas aos dados e definimos uma função de pré-processamento de requisito.

Definir Variáveis

Definir funções de pré-processamento

Em seguida, construímos as funções de dados de que precisamos no restante do colab.

# Train data returning either 2 or 3 elements (the third element being the group)
def celeb_a_train_data_wo_group(batch_size):
  celeb_a_train_data = celeb_a_builder.as_dataset(split='train').shuffle(1024).repeat().batch(batch_size).map(preprocess_input_dict)
  return celeb_a_train_data.map(get_image_and_label)
def celeb_a_train_data_w_group(batch_size):
  celeb_a_train_data = celeb_a_builder.as_dataset(split='train').shuffle(1024).repeat().batch(batch_size).map(preprocess_input_dict)
  return celeb_a_train_data.map(get_image_label_and_group)

# Test data for the overall evaluation
celeb_a_test_data = celeb_a_builder.as_dataset(split='test').batch(1).map(preprocess_input_dict).map(get_image_label_and_group)
# Copy test data locally to be able to read it into tfma
copy_test_files_to_local()

Construir um modelo DNN simples

Como este bloco de notas se concentra no TFCO, montaremos um modelo tf.keras.Sequential simples e irrestrito.

Podemos melhorar muito o desempenho do modelo adicionando alguma complexidade (por exemplo, camadas mais densamente conectadas, explorando diferentes funções de ativação, aumentando o tamanho da imagem), mas isso pode nos desviar do objetivo de demonstrar como é fácil aplicar a biblioteca TFCO ao trabalhar com Keras. Por esse motivo, o modelo será mantido simples - mas sinta-se encorajado a explorar este espaço.

def create_model():
  # For this notebook, accuracy will be used to evaluate performance.
  METRICS = [
    tf.keras.metrics.BinaryAccuracy(name='accuracy')
  ]

  # The model consists of:
  # 1. An input layer that represents the 28x28x3 image flatten.
  # 2. A fully connected layer with 64 units activated by a ReLU function.
  # 3. A single-unit readout layer to output real-scores instead of probabilities.
  model = keras.Sequential([
      keras.layers.Flatten(input_shape=(IMAGE_SIZE, IMAGE_SIZE, 3), name='image'),
      keras.layers.Dense(64, activation='relu'),
      keras.layers.Dense(1, activation=None)
  ])

  # TFCO by default uses hinge loss — and that will also be used in the model.
  model.compile(
      optimizer=tf.keras.optimizers.Adam(0.001),
      loss='hinge',
      metrics=METRICS)
  return model

Também definimos uma função para definir sementes para garantir resultados reproduzíveis. Observe que este colab é uma ferramenta educacional e não tem a estabilidade de um pipeline de produção bem ajustado. Correr sem colocar uma semente pode levar a resultados variados.

def set_seeds():
  np.random.seed(121212)
  tf.compat.v1.set_random_seed(212121)

Funções auxiliares dos indicadores de justiça

Antes de treinar nosso modelo, definimos uma série de funções auxiliares que nos permitirão avaliar o desempenho do modelo por meio de Indicadores de Equidade.

Primeiro, criamos uma função auxiliar para salvar nosso modelo depois de treiná-lo.

def save_model(model, subdir):
  base_dir = tempfile.mkdtemp(prefix='saved_models')
  model_location = os.path.join(base_dir, subdir)
  model.save(model_location, save_format='tf')
  return model_location

A seguir, definimos as funções usadas para pré-processar os dados a fim de passá-los corretamente para o TFMA.

Funções de pré-processamento de dados para

Por fim, definimos uma função que avalia os resultados em TFMA.

def get_eval_results(model_location, eval_subdir):
  base_dir = tempfile.mkdtemp(prefix='saved_eval_results')
  tfma_eval_result_path = os.path.join(base_dir, eval_subdir)

  eval_config_pbtxt = """
        model_specs {
          label_key: "%s"
        }
        metrics_specs {
          metrics {
            class_name: "FairnessIndicators"
            config: '{ "thresholds": [0.22, 0.5, 0.75] }'
          }
          metrics {
            class_name: "ExampleCount"
          }
        }
        slicing_specs {}
        slicing_specs { feature_keys: "%s" }
        options {
          compute_confidence_intervals { value: False }
          disabled_outputs{values: "analysis"}
        }
      """ % (LABEL_KEY, GROUP_KEY)

  eval_config = text_format.Parse(eval_config_pbtxt, tfma.EvalConfig())

  eval_shared_model = tfma.default_eval_shared_model(
        eval_saved_model_path=model_location, tags=[tf.saved_model.SERVING])

  schema_pbtxt = """
        tensor_representation_group {
          key: ""
          value {
            tensor_representation {
              key: "%s"
              value {
                dense_tensor {
                  column_name: "%s"
                  shape {
                    dim { size: 28 }
                    dim { size: 28 }
                    dim { size: 3 }
                  }
                }
              }
            }
          }
        }
        feature {
          name: "%s"
          type: FLOAT
        }
        feature {
          name: "%s"
          type: FLOAT
        }
        feature {
          name: "%s"
          type: BYTES
        }
        """ % (IMAGE_KEY, IMAGE_KEY, IMAGE_KEY, LABEL_KEY, GROUP_KEY)
  schema = text_format.Parse(schema_pbtxt, schema_pb2.Schema())
  coder = tf_example_record.TFExampleBeamRecord(
      physical_format='inmem', schema=schema,
      raw_record_column_name=tfma.ARROW_INPUT_COLUMN)
  tensor_adapter_config = tensor_adapter.TensorAdapterConfig(
    arrow_schema=coder.ArrowSchema(),
    tensor_representations=coder.TensorRepresentations())
  # Run the fairness evaluation.
  with beam.Pipeline() as pipeline:
    _ = (
          tfds_as_pcollection(pipeline, 'celeb_a', 'test')
          | 'ExamplesToRecordBatch' >> coder.BeamSource()
          | 'ExtractEvaluateAndWriteResults' >>
          tfma.ExtractEvaluateAndWriteResults(
              eval_config=eval_config,
              eval_shared_model=eval_shared_model,
              output_path=tfma_eval_result_path,
              tensor_adapter_config=tensor_adapter_config)
    )
  return tfma.load_eval_result(output_path=tfma_eval_result_path)

Treinar e avaliar o modelo irrestrito

Com o modelo agora definido e o pipeline de entrada no lugar, estamos prontos para treinar nosso modelo. Para reduzir a quantidade de tempo de execução e memória, treinaremos o modelo dividindo os dados em pequenos lotes com apenas algumas iterações repetidas.

Observe que executar este notebook no TensorFlow <2.0.0 pode resultar em um aviso de np.where uso para np.where . Ignore com segurança esse aviso, pois o TensorFlow aborda isso em 2.X usando tf.where em vez de np.where .

BATCH_SIZE = 32

# Set seeds to get reproducible results
set_seeds()

model_unconstrained = create_model()
model_unconstrained.fit(celeb_a_train_data_wo_group(BATCH_SIZE), epochs=5, steps_per_epoch=1000)
Epoch 1/5
1000/1000 [==============================] - 17s 11ms/step - loss: 0.6219 - accuracy: 0.7189
Epoch 2/5
1000/1000 [==============================] - 10s 10ms/step - loss: 0.4061 - accuracy: 0.8187
Epoch 3/5
1000/1000 [==============================] - 10s 10ms/step - loss: 0.3649 - accuracy: 0.8391
Epoch 4/5
1000/1000 [==============================] - 16s 16ms/step - loss: 0.3427 - accuracy: 0.8485
Epoch 5/5
1000/1000 [==============================] - 10s 10ms/step - loss: 0.3390 - accuracy: 0.8482
<tensorflow.python.keras.callbacks.History at 0x7f47c01a8550>

Avaliar o modelo nos dados de teste deve resultar em uma pontuação de precisão final de pouco mais de 85%. Nada mal para um modelo simples sem ajustes finos.

print('Overall Results, Unconstrained')
celeb_a_test_data = celeb_a_builder.as_dataset(split='test').batch(1).map(preprocess_input_dict).map(get_image_label_and_group)
results = model_unconstrained.evaluate(celeb_a_test_data)
Overall Results, Unconstrained
19962/19962 [==============================] - 40s 2ms/step - loss: 0.2125 - accuracy: 0.8636

No entanto, o desempenho avaliado em grupos de idade pode revelar algumas deficiências.

Para explorar isso mais a fundo, avaliamos o modelo com Indicadores de Equidade (via TFMA). Em particular, estamos interessados ​​em ver se há uma lacuna significativa no desempenho entre as categorias "Jovem" e "Não jovem" quando avaliadas na taxa de falso positivo.

Um erro falso positivo ocorre quando o modelo prediz incorretamente a classe positiva. Nesse contexto, um resultado falso positivo ocorre quando a verdade fundamental é a imagem de uma celebridade 'Não Sorrindo' e a modelo prevê 'Sorrindo'. Por extensão, a taxa de falsos positivos, que é usada na visualização acima, é uma medida de precisão para um teste. Embora seja um erro relativamente mundano de se cometer neste contexto, erros de falso positivo às vezes podem causar comportamentos mais problemáticos. Por exemplo, um erro de falso positivo em um classificador de spam pode fazer com que o usuário perca um e-mail importante.

model_location = save_model(model_unconstrained, 'model_export_unconstrained')
eval_results_unconstrained = get_eval_results(model_location, 'eval_results_unconstrained')
INFO:tensorflow:Assets written to: /tmp/saved_modelseqklzviu/model_export_unconstrained/assets
INFO:tensorflow:Assets written to: /tmp/saved_modelseqklzviu/model_export_unconstrained/assets
WARNING:apache_beam.runners.interactive.interactive_environment:Dependencies required for Interactive Beam PCollection visualization are not available, please use: `pip install apache-beam[interactive]` to install necessary dependencies to enable all data visualization features.
WARNING:apache_beam.io.tfrecordio:Couldn't find python-snappy so the implementation of _TFRecordUtil._masked_crc32c is not as fast as it could be.
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow_model_analysis/writers/metrics_plots_and_validations_writer.py:113: tf_record_iterator (from tensorflow.python.lib.io.tf_record) is deprecated and will be removed in a future version.
Instructions for updating:
Use eager execution and: 
`tf.data.TFRecordDataset(path)`
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow_model_analysis/writers/metrics_plots_and_validations_writer.py:113: tf_record_iterator (from tensorflow.python.lib.io.tf_record) is deprecated and will be removed in a future version.
Instructions for updating:
Use eager execution and: 
`tf.data.TFRecordDataset(path)`

Conforme mencionado acima, estamos nos concentrando na taxa de falsos positivos. A versão atual do Fairness Indicators (0.1.2) seleciona a taxa de falsos negativos por padrão. Depois de executar a linha abaixo, desmarque false_negative_rate e selecione false_positive_rate para ver a métrica na qual estamos interessados.

tfma.addons.fairness.view.widget_view.render_fairness_indicator(eval_results_unconstrained)
FairnessIndicatorViewer(slicingMetrics=[{'sliceValue': 'Overall', 'slice': 'Overall', 'metrics': {'example_cou…

Como os resultados mostram acima, vemos uma lacuna desproporcional entre as categorias "Jovem" e "Não jovem" .

É aqui que o TFCO pode ajudar, restringindo a taxa de falsos positivos para um critério mais aceitável.

Configuração de modelo restrito

Conforme documentado na biblioteca do TFCO , existem vários auxiliares que tornarão mais fácil restringir o problema:

  1. tfco.rate_context() - Isso é o que será usado na construção de uma restrição para cada categoria de grupo de idade.
  2. tfco.RateMinimizationProblem() - A expressão de taxa a ser minimizada aqui será a taxa de falsos positivos sujeita à faixa etária. Em outras palavras, o desempenho agora será avaliado com base na diferença entre as taxas de falsos positivos da faixa etária e do conjunto de dados geral. Para esta demonstração, uma taxa de falsos positivos menor ou igual a 5% será definida como a restrição.
  3. tfco.ProxyLagrangianOptimizerV2() - Este é o auxiliar que realmente resolverá o problema de restrição de taxa.

A célula abaixo chamará esses ajudantes para configurar o treinamento do modelo com a restrição de justiça.

# The batch size is needed to create the input, labels and group tensors.
# These tensors are initialized with all 0's. They will eventually be assigned
# the batch content to them. A large batch size is chosen so that there are
# enough number of "Young" and "Not Young" examples in each batch.
set_seeds()
model_constrained = create_model()
BATCH_SIZE = 32

# Create input tensor.
input_tensor = tf.Variable(
    np.zeros((BATCH_SIZE, IMAGE_SIZE, IMAGE_SIZE, 3), dtype="float32"),
    name="input")

# Create labels and group tensors (assuming both labels and groups are binary).
labels_tensor = tf.Variable(
    np.zeros(BATCH_SIZE, dtype="float32"), name="labels")
groups_tensor = tf.Variable(
    np.zeros(BATCH_SIZE, dtype="float32"), name="groups")

# Create a function that returns the applied 'model' to the input tensor
# and generates constrained predictions.
def predictions():
  return model_constrained(input_tensor)

# Create overall context and subsetted context.
# The subsetted context contains subset of examples where group attribute < 1
# (i.e. the subset of "Not Young" celebrity images).
# "groups_tensor < 1" is used instead of "groups_tensor == 0" as the former
# would be a comparison on the tensor value, while the latter would be a
# comparison on the Tensor object.
context = tfco.rate_context(predictions, labels=lambda:labels_tensor)
context_subset = context.subset(lambda:groups_tensor < 1)

# Setup list of constraints.
# In this notebook, the constraint will just be: FPR to less or equal to 5%.
constraints = [tfco.false_positive_rate(context_subset) <= 0.05]

# Setup rate minimization problem: minimize overall error rate s.t. constraints.
problem = tfco.RateMinimizationProblem(tfco.error_rate(context), constraints)

# Create constrained optimizer and obtain train_op.
# Separate optimizers are specified for the objective and constraints
optimizer = tfco.ProxyLagrangianOptimizerV2(
      optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
      constraint_optimizer=tf.keras.optimizers.Adam(learning_rate=0.001),
      num_constraints=problem.num_constraints)

# A list of all trainable variables is also needed to use TFCO.
var_list = (model_constrained.trainable_weights + list(problem.trainable_variables) +
            optimizer.trainable_variables())

O modelo agora está configurado e pronto para ser treinado com a restrição de taxa de falsos positivos em toda a faixa etária.

Agora, como a última iteração do modelo restrito pode não ser necessariamente o modelo de melhor desempenho em termos da restrição definida, a biblioteca TFCO vem equipada com tfco.find_best_candidate_index() que pode ajudar a escolher a melhor iteração dentre as encontradas após cada época. Pense em tfco.find_best_candidate_index() como uma heurística adicionada que classifica cada um dos resultados com base na precisão e restrição de justiça (neste caso, taxa de falsos positivos em toda a faixa etária) separadamente com relação aos dados de treinamento. Dessa forma, ele pode buscar uma melhor compensação entre a precisão geral e a restrição de justiça.

As células a seguir iniciarão o treinamento com restrições enquanto também encontram o modelo de melhor desempenho por iteração.

# Obtain train set batches.

NUM_ITERATIONS = 100  # Number of training iterations.
SKIP_ITERATIONS = 10  # Print training stats once in this many iterations.

# Create temp directory for saving snapshots of models.
temp_directory = tempfile.mktemp()
os.mkdir(temp_directory)

# List of objective and constraints across iterations.
objective_list = []
violations_list = []

# Training iterations.
iteration_count = 0
for (image, label, group) in celeb_a_train_data_w_group(BATCH_SIZE):
  # Assign current batch to input, labels and groups tensors.
  input_tensor.assign(image)
  labels_tensor.assign(label)
  groups_tensor.assign(group)

  # Run gradient update.
  optimizer.minimize(problem, var_list=var_list)

  # Record objective and violations.
  objective = problem.objective()
  violations = problem.constraints()

  sys.stdout.write(
      "\r Iteration %d: Hinge Loss = %.3f, Max. Constraint Violation = %.3f"
      % (iteration_count + 1, objective, max(violations)))

  # Snapshot model once in SKIP_ITERATIONS iterations.
  if iteration_count % SKIP_ITERATIONS == 0:
    objective_list.append(objective)
    violations_list.append(violations)

    # Save snapshot of model weights.
    model_constrained.save_weights(
        temp_directory + "/celeb_a_constrained_" +
        str(iteration_count / SKIP_ITERATIONS) + ".h5")

  iteration_count += 1
  if iteration_count >= NUM_ITERATIONS:
    break

# Choose best model from recorded iterates and load that model.
best_index = tfco.find_best_candidate_index(
    np.array(objective_list), np.array(violations_list))

model_constrained.load_weights(
    temp_directory + "/celeb_a_constrained_" + str(best_index) + ".0.h5")

# Remove temp directory.
os.system("rm -r " + temp_directory)
Iteration 100: Hinge Loss = 0.614, Max. Constraint Violation = 0.268
0

Depois de aplicar a restrição, avaliamos os resultados mais uma vez usando Indicadores de Equidade.

model_location = save_model(model_constrained, 'model_export_constrained')
eval_result_constrained = get_eval_results(model_location, 'eval_results_constrained')
INFO:tensorflow:Assets written to: /tmp/saved_modelsrnjadh_e/model_export_constrained/assets
INFO:tensorflow:Assets written to: /tmp/saved_modelsrnjadh_e/model_export_constrained/assets

Como na vez anterior, usamos indicadores de imparcialidade, desmarque false_negative_rate e selecione false_positive_rate para ver a métrica na qual estamos interessados.

Observe que, para comparar de forma justa as duas versões de nosso modelo, é importante usar limites que definem a taxa geral de falsos positivos como aproximadamente igual. Isso garante que estamos olhando para a mudança real em oposição a apenas uma mudança no modelo equivalente a simplesmente mover o limite do limite. Em nosso caso, comparar o modelo irrestrito em 0,5 e o modelo restrito em 0,22 fornece uma comparação justa para os modelos.

eval_results_dict = {
    'constrained': eval_result_constrained,
    'unconstrained': eval_results_unconstrained,
}
tfma.addons.fairness.view.widget_view.render_fairness_indicator(multi_eval_results=eval_results_dict)
FairnessIndicatorViewer(evalName='constrained', evalNameCompare='unconstrained', slicingMetrics=[{'sliceValue'…

Com a capacidade do TFCO de expressar um requisito mais complexo como uma restrição de taxa, ajudamos este modelo a atingir um resultado mais desejável com pouco impacto no desempenho geral. É claro que ainda há espaço para melhorias, mas pelo menos o TFCO foi capaz de encontrar um modelo que chega perto de satisfazer a restrição e reduz a disparidade entre os grupos tanto quanto possível.