Se usó la API de Cloud Translation para traducir esta página.
Switch to English

Aprendizaje federado para la generación de texto

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

Este tutorial se basa en los conceptos del tutorial de aprendizaje federado para clasificación de imágenes y muestra varios otros enfoques útiles para el aprendizaje federado.

En particular, cargamos un modelo de Keras previamente entrenado y lo refinamos usando entrenamiento federado en un conjunto de datos descentralizado (simulado). Esto es prácticamente importante por varias razones. La capacidad de utilizar modelos serializados facilita la combinación del aprendizaje federado con otros enfoques de aprendizaje automático. Además, esto permite el uso de una gama cada vez mayor de modelos previamente entrenados --- por ejemplo, entrenar modelos de lenguaje desde cero rara vez es necesario, ya que numerosos modelos previamente entrenados ahora están ampliamente disponibles (ver, por ejemplo, TF Hub ). En cambio, tiene más sentido comenzar desde un modelo previamente entrenado y refinarlo usando el aprendizaje federado, adaptándose a las características particulares de los datos descentralizados para una aplicación en particular.

Para este tutorial, comenzamos con un RNN que genera caracteres ASCII y lo refinamos a través del aprendizaje federado. También mostramos cómo los pesos finales pueden retroalimentarse al modelo original de Keras, lo que permite una fácil evaluación y generación de texto utilizando herramientas estándar.


!pip install --quiet --upgrade tensorflow_federated_nightly
!pip install --quiet --upgrade nest_asyncio

import nest_asyncio
nest_asyncio.apply()
import collections
import functools
import os
import time

import numpy as np
import tensorflow as tf
import tensorflow_federated as tff

np.random.seed(0)

# Test the TFF is working:
tff.federated_computation(lambda: 'Hello, World!')()
b'Hello, World!'

Cargar un modelo previamente entrenado

Cargamos un modelo que fue entrenado previamente siguiendo el tutorial de TensorFlow Generación de texto usando un RNN con ejecución ávida . Sin embargo, en lugar de entrenar sobre Las obras completas de Shakespeare , entrenamos previamente el modelo sobre el texto de A Tale of Two Cities y A Christmas Carol de Charles Dickens.

Aparte de ampliar el vocabulario, no modificamos el tutorial original, por lo que este modelo inicial no es el más moderno, pero produce predicciones razonables y es suficiente para los propósitos de nuestro tutorial. El modelo final se guardó con tf.keras.models.save_model(include_optimizer=False) .

Usaremos el aprendizaje federado para ajustar este modelo para Shakespeare en este tutorial, utilizando una versión federada de los datos proporcionados por TFF.

Genere las tablas de búsqueda de vocabulario

# A fixed vocabularly of ASCII chars that occur in the works of Shakespeare and Dickens:
vocab = list('dhlptx@DHLPTX $(,048cgkoswCGKOSW[_#\'/37;?bfjnrvzBFJNRVZ"&*.26:\naeimquyAEIMQUY]!%)-159\r')

# Creating a mapping from unique characters to indices
char2idx = {u:i for i, u in enumerate(vocab)}
idx2char = np.array(vocab)

Cargue el modelo previamente entrenado y genere algo de texto

def load_model(batch_size):
  urls = {
      1: 'https://storage.googleapis.com/tff-models-public/dickens_rnn.batch1.kerasmodel',
      8: 'https://storage.googleapis.com/tff-models-public/dickens_rnn.batch8.kerasmodel'}
  assert batch_size in urls, 'batch_size must be in ' + str(urls.keys())
  url = urls[batch_size]
  local_file = tf.keras.utils.get_file(os.path.basename(url), origin=url)  
  return tf.keras.models.load_model(local_file, compile=False)
def generate_text(model, start_string):
  # From https://www.tensorflow.org/tutorials/sequences/text_generation
  num_generate = 200
  input_eval = [char2idx[s] for s in start_string]
  input_eval = tf.expand_dims(input_eval, 0)
  text_generated = []
  temperature = 1.0

  model.reset_states()
  for i in range(num_generate):
    predictions = model(input_eval)
    predictions = tf.squeeze(predictions, 0)
    predictions = predictions / temperature
    predicted_id = tf.random.categorical(
        predictions, num_samples=1)[-1, 0].numpy()
    input_eval = tf.expand_dims([predicted_id], 0)
    text_generated.append(idx2char[predicted_id])

  return (start_string + ''.join(text_generated))
# Text generation requires a batch_size=1 model.
keras_model_batch1 = load_model(batch_size=1)
print(generate_text(keras_model_batch1, 'What of TensorFlow Federated, you ask? '))
Downloading data from https://storage.googleapis.com/tff-models-public/dickens_rnn.batch1.kerasmodel
16195584/16193984 [==============================] - 0s 0us/step
16203776/16193984 [==============================] - 0s 0us/step
What of TensorFlow Federated, you ask? Sall
yesterday. Received the Bailey."

"Mr. Lorry, grimmering himself, or low varked thends the winter, and the eyes of Monsieur
Defarge. "Let his mind, hon in his
life and message; four declare 

Cargar y preprocesar los datos federados de Shakespeare

El paquete tff.simulation.datasets proporciona una variedad de conjuntos de datos que se dividen en "clientes", donde cada cliente corresponde a un conjunto de datos en un dispositivo particular que podría participar en el aprendizaje federado.

Estos conjuntos de datos proporcionan distribuciones de datos realistas que no son IID que replican en simulación los desafíos de la capacitación en datos descentralizados reales. Parte del preprocesamiento de estos datos se realizó utilizando herramientas del proyecto Leaf ( github ).

train_data, test_data = tff.simulation.datasets.shakespeare.load_data()

Los conjuntos de datos proporcionados por shakespeare.load_data() consisten en una secuencia de Tensors de cadena, uno por cada línea hablada por un personaje en particular en una obra de Shakespeare. Las claves del cliente consisten en el nombre de la obra junto con el nombre del personaje, por ejemplo, MUCH_ADO_ABOUT_NOTHING_OTHELLO corresponde a las líneas del personaje Othello en la obra Much Ado About Nothing . Tenga en cuenta que en un escenario de aprendizaje federado real, los identificadores nunca identifican o rastrean a los clientes, pero para la simulación es útil trabajar con conjuntos de datos con clave.

Aquí, por ejemplo, podemos ver algunos datos de King Lear:

# Here the play is "The Tragedy of King Lear" and the character is "King".
raw_example_dataset = train_data.create_tf_dataset_for_client(
    'THE_TRAGEDY_OF_KING_LEAR_KING')
# To allow for future extensions, each entry x
# is an OrderedDict with a single key 'snippets' which contains the text.
for x in raw_example_dataset.take(2):
  print(x['snippets'])
tf.Tensor(b'', shape=(), dtype=string)
tf.Tensor(b'What?', shape=(), dtype=string)

Ahora usamos transformaciones tf.data.Dataset para preparar estos datos para entrenar el char RNN cargado arriba.

# Input pre-processing parameters
SEQ_LENGTH = 100
BATCH_SIZE = 8
BUFFER_SIZE = 100  # For dataset shuffling
# Construct a lookup table to map string chars to indexes,
# using the vocab loaded above:
table = tf.lookup.StaticHashTable(
    tf.lookup.KeyValueTensorInitializer(
        keys=vocab, values=tf.constant(list(range(len(vocab))),
                                       dtype=tf.int64)),
    default_value=0)


def to_ids(x):
  s = tf.reshape(x['snippets'], shape=[1])
  chars = tf.strings.bytes_split(s).values
  ids = table.lookup(chars)
  return ids


def split_input_target(chunk):
  input_text = tf.map_fn(lambda x: x[:-1], chunk)
  target_text = tf.map_fn(lambda x: x[1:], chunk)
  return (input_text, target_text)


def preprocess(dataset):
  return (
      # Map ASCII chars to int64 indexes using the vocab
      dataset.map(to_ids)
      # Split into individual chars
      .unbatch()
      # Form example sequences of SEQ_LENGTH +1
      .batch(SEQ_LENGTH + 1, drop_remainder=True)
      # Shuffle and form minibatches
      .shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
      # And finally split into (input, target) tuples,
      # each of length SEQ_LENGTH.
      .map(split_input_target))

Tenga en cuenta que en la formación de las secuencias originales y en la formación de los lotes anteriores, usamos drop_remainder=True para simplificar. Esto significa que cualquier carácter (cliente) que no tenga al menos (SEQ_LENGTH + 1) * BATCH_SIZE caracteres de texto tendrá conjuntos de datos vacíos. Un enfoque típico para abordar esto sería rellenar los lotes con un token especial y luego enmascarar la pérdida para no tener en cuenta los tokens de relleno.

Esto complicaría un poco el ejemplo, por lo que para este tutorial solo usamos lotes completos, como en el tutorial estándar . Sin embargo, en el entorno federado, este problema es más importante, porque muchos usuarios pueden tener pequeños conjuntos de datos.

Ahora podemos preprocesar nuestro raw_example_dataset y verificar los tipos:

example_dataset = preprocess(raw_example_dataset)
print(example_dataset.element_spec)
(TensorSpec(shape=(8, 100), dtype=tf.int64, name=None), TensorSpec(shape=(8, 100), dtype=tf.int64, name=None))

Compile el modelo y pruebe con los datos preprocesados

Cargamos un modelo de keras sin compilar, pero para ejecutar keras_model.evaluate , necesitamos compilarlo con una pérdida y métricas. También compilaremos en un optimizador, que se utilizará como el optimizador en el dispositivo en Federated Learning.

El tutorial original no tenía precisión a nivel de caracteres (la fracción de predicciones donde se colocó la probabilidad más alta en el siguiente carácter correcto). Esta es una métrica útil, así que la agregamos. Sin embargo, necesitamos definir una nueva clase de métrica para esto porque nuestras predicciones tienen rango 3 (un vector de logits para cada una de las predicciones BATCH_SIZE * SEQ_LENGTH ), y SparseCategoricalAccuracy solo espera predicciones de rango 2.

class FlattenedCategoricalAccuracy(tf.keras.metrics.SparseCategoricalAccuracy):

  def __init__(self, name='accuracy', dtype=tf.float32):
    super().__init__(name, dtype=dtype)

  def update_state(self, y_true, y_pred, sample_weight=None):
    y_true = tf.reshape(y_true, [-1, 1])
    y_pred = tf.reshape(y_pred, [-1, len(vocab), 1])
    return super().update_state(y_true, y_pred, sample_weight)

Ahora podemos compilar un modelo y evaluarlo en nuestro example_dataset .

BATCH_SIZE = 8  # The training and eval batch size for the rest of this tutorial.
keras_model = load_model(batch_size=BATCH_SIZE)
keras_model.compile(
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
    metrics=[FlattenedCategoricalAccuracy()])

# Confirm that loss is much lower on Shakespeare than on random data
loss, accuracy = keras_model.evaluate(example_dataset.take(5), verbose=0)
print(
    'Evaluating on an example Shakespeare character: {a:3f}'.format(a=accuracy))

# As a sanity check, we can construct some completely random data, where we expect
# the accuracy to be essentially random:
random_guessed_accuracy = 1.0 / len(vocab)
print('Expected accuracy for random guessing: {a:.3f}'.format(
    a=random_guessed_accuracy))
random_indexes = np.random.randint(
    low=0, high=len(vocab), size=1 * BATCH_SIZE * (SEQ_LENGTH + 1))
data = collections.OrderedDict(
    snippets=tf.constant(
        ''.join(np.array(vocab)[random_indexes]), shape=[1, 1]))
random_dataset = preprocess(tf.data.Dataset.from_tensor_slices(data))
loss, accuracy = keras_model.evaluate(random_dataset, steps=10, verbose=0)
print('Evaluating on completely random data: {a:.3f}'.format(a=accuracy))
Downloading data from https://storage.googleapis.com/tff-models-public/dickens_rnn.batch8.kerasmodel
16195584/16193984 [==============================] - 0s 0us/step
16203776/16193984 [==============================] - 0s 0us/step
Evaluating on an example Shakespeare character: 0.402000
Expected accuracy for random guessing: 0.012
Evaluating on completely random data: 0.011

Ajuste el modelo con el aprendizaje federado

TFF serializa todos los cálculos de TensorFlow para que puedan ejecutarse potencialmente en un entorno que no sea Python (aunque, por el momento, solo está disponible un tiempo de ejecución de simulación implementado en Python). A pesar de que estamos ejecutando en modo ansioso, (TF 2.0), actualmente TFF serializa los cálculos de TensorFlow mediante la construcción de las operaciones necesarias dentro del contexto de una declaración " with tf.Graph.as_default() ". Por lo tanto, necesitamos proporcionar una función que TFF pueda usar para introducir nuestro modelo en un gráfico que controla. Hacemos esto de la siguiente manera:

# Clone the keras_model inside `create_tff_model()`, which TFF will
# call to produce a new copy of the model inside the graph that it will 
# serialize. Note: we want to construct all the necessary objects we'll need 
# _inside_ this method.
def create_tff_model():
  # TFF uses an `input_spec` so it knows the types and shapes
  # that your model expects.
  input_spec = example_dataset.element_spec
  keras_model_clone = tf.keras.models.clone_model(keras_model)
  return tff.learning.from_keras_model(
      keras_model_clone,
      input_spec=input_spec,
      loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
      metrics=[FlattenedCategoricalAccuracy()])

Ahora estamos listos para construir un proceso iterativo de Promedio federado, que usaremos para mejorar el modelo (para obtener detalles sobre el algoritmo de Promedio federado, consulte el artículo Aprendizaje eficiente en comunicación de redes profundas a partir de datos descentralizados ).

Usamos un modelo de Keras compilado para realizar una evaluación estándar (no federada) después de cada ronda de capacitación federada. Esto es útil para fines de investigación cuando se realiza un aprendizaje federado simulado y hay un conjunto de datos de prueba estándar.

En un entorno de producción realista, esta misma técnica podría usarse para tomar modelos entrenados con aprendizaje federado y evaluarlos en un conjunto de datos de referencia centralizado con fines de prueba o garantía de calidad.

# This command builds all the TensorFlow graphs and serializes them: 
fed_avg = tff.learning.build_federated_averaging_process(
    model_fn=create_tff_model,
    client_optimizer_fn=lambda: tf.keras.optimizers.SGD(lr=0.5))

Aquí está el ciclo más simple posible, donde ejecutamos promedios federados para una ronda en un solo cliente en un solo lote:

state = fed_avg.initialize()
state, metrics = fed_avg.next(state, [example_dataset.take(5)])
train_metrics = metrics['train']
print('loss={l:.3f}, accuracy={a:.3f}'.format(
    l=train_metrics['loss'], a=train_metrics['accuracy']))
loss=4.403, accuracy=0.132

Ahora escribamos un ciclo de capacitación y evaluación un poco más interesante.

Para que esta simulación aún se ejecute relativamente rápido, entrenamos con los mismos tres clientes en cada ronda, considerando solo dos minibatches para cada uno.

def data(client, source=train_data):
  return preprocess(source.create_tf_dataset_for_client(client)).take(5)


clients = [
    'ALL_S_WELL_THAT_ENDS_WELL_CELIA', 'MUCH_ADO_ABOUT_NOTHING_OTHELLO',
]

train_datasets = [data(client) for client in clients]

# We concatenate the test datasets for evaluation with Keras by creating a 
# Dataset of Datasets, and then identity flat mapping across all the examples.
test_dataset = tf.data.Dataset.from_tensor_slices(
    [data(client, test_data) for client in clients]).flat_map(lambda x: x)

El estado inicial del modelo producido por fed_avg.initialize() se basa en los inicializadores aleatorios del modelo Keras, no en los pesos que se cargaron, ya que clone_model() no clona los pesos. Para comenzar a entrenar desde un modelo previamente entrenado, establecemos los pesos del modelo en el estado del servidor directamente desde el modelo cargado.

NUM_ROUNDS = 5

# The state of the FL server, containing the model and optimization state.
state = fed_avg.initialize()

# Load our pre-trained Keras model weights into the global model state.
state = tff.learning.state_with_new_model_weights(
    state,
    trainable_weights=[v.numpy() for v in keras_model.trainable_weights],
    non_trainable_weights=[
        v.numpy() for v in keras_model.non_trainable_weights
    ])


def keras_evaluate(state, round_num):
  # Take our global model weights and push them back into a Keras model to
  # use its standard `.evaluate()` method.
  keras_model = load_model(batch_size=BATCH_SIZE)
  keras_model.compile(
      loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
      metrics=[FlattenedCategoricalAccuracy()])
  state.model.assign_weights_to(keras_model)
  loss, accuracy = keras_model.evaluate(example_dataset, steps=2, verbose=0)
  print('\tEval: loss={l:.3f}, accuracy={a:.3f}'.format(l=loss, a=accuracy))


for round_num in range(NUM_ROUNDS):
  print('Round {r}'.format(r=round_num))
  keras_evaluate(state, round_num)
  state, metrics = fed_avg.next(state, train_datasets)
  train_metrics = metrics['train']
  print('\tTrain: loss={l:.3f}, accuracy={a:.3f}'.format(
      l=train_metrics['loss'], a=train_metrics['accuracy']))

print('Final evaluation')
keras_evaluate(state, NUM_ROUNDS + 1)
Round 0
    Eval: loss=3.324, accuracy=0.401
    Train: loss=4.360, accuracy=0.155
Round 1
    Eval: loss=4.361, accuracy=0.049
    Train: loss=4.235, accuracy=0.164
Round 2
    Eval: loss=4.219, accuracy=0.177
    Train: loss=4.081, accuracy=0.221
Round 3
    Eval: loss=4.080, accuracy=0.174
    Train: loss=3.940, accuracy=0.226
Round 4
    Eval: loss=3.991, accuracy=0.176
    Train: loss=3.840, accuracy=0.226
Final evaluation
    Eval: loss=3.909, accuracy=0.171

Con los cambios predeterminados, no hemos realizado suficiente entrenamiento para marcar una gran diferencia, pero si entrena más tiempo con más datos de Shakespeare, debería ver una diferencia en el estilo del texto generado con el modelo actualizado:

# Set our newly trained weights back in the originally created model.
keras_model_batch1.set_weights([v.numpy() for v in keras_model.weights])
# Text generation requires batch_size=1
print(generate_text(keras_model_batch1, 'What of TensorFlow Federated, you ask? '))
What of TensorFlow Federated, you ask? Shalways, I will call your
compet with any city brought their faces uncompany," besumed him. "When he
sticked Madame Defarge pushed the lamps.

"Have I often but no unison. She had probably come, 

Extensiones sugeridas

¡Este tutorial es solo el primer paso! A continuación, se ofrecen algunas ideas sobre cómo podría intentar ampliar este cuaderno:

  • Escriba un ciclo de entrenamiento más realista en el que muestre a los clientes para entrenar al azar.
  • Utilice " .repeat(NUM_EPOCHS) " en los conjuntos de datos del cliente para probar múltiples épocas de entrenamiento local (por ejemplo, como en McMahan et. Al. ). Consulte también Aprendizaje federado para clasificación de imágenes que hace esto.
  • Cambie el comando compile() para experimentar con el uso de diferentes algoritmos de optimización en el cliente.
  • Pruebe el argumento server_optimizer para build_federated_averaging_process para probar diferentes algoritmos para aplicar las actualizaciones del modelo en el servidor.
  • Pruebe el argumento client_weight_fn para build_federated_averaging_process para probar diferentes ponderaciones de los clientes. El valor predeterminado pondera las actualizaciones del cliente por el número de ejemplos en el cliente, pero puede hacerlo, por ejemplo, client_weight_fn=lambda _: tf.constant(1.0) .