Trabajar con capas de preprocesamiento

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

Preprocesamiento de Keras

La API de capas de preprocesamiento de Keras permite a los desarrolladores crear tuberías de procesamiento de entrada nativas de Keras. Estas canalizaciones de procesamiento de entrada se pueden utilizar como código de preprocesamiento independiente en flujos de trabajo que no son de Keras, se pueden combinar directamente con modelos de Keras y exportar como parte de un modelo guardado de Keras.

Con las capas de preprocesamiento de Keras, puede crear y exportar modelos que son verdaderamente de un extremo a otro: modelos que aceptan imágenes sin procesar o datos estructurados sin procesar como entrada; modelos que manejan la normalización de características o la indexación de valores de características por sí mismos.

Preprocesamiento disponible

Preprocesamiento de texto

Preprocesamiento de características numéricas

Preprocesamiento de características categóricas

  • tf.keras.layers.CategoryEncoding : turnos enteros características categóricas en representaciones densas uno en caliente, multi-calientes, o contar.
  • tf.keras.layers.Hashing : Realiza la función de hash categórica, también conocido como el "truco hash".
  • tf.keras.layers.StringLookup : vueltas cadena categórica valores de una representación codificada que puede ser leído por una Embedding capa o Dense capa.
  • tf.keras.layers.IntegerLookup : vueltas enteros valores categóricos en una representación codificada que puede ser leído por una Embedding capa o Dense capa.

Preprocesamiento de imágenes

Estas capas sirven para estandarizar las entradas de un modelo de imagen.

Aumento de datos de imagen

Estas capas aplican transformaciones de aumento aleatorio a un lote de imágenes. Solo están activos durante el entrenamiento.

La adapt() método

Algunas capas de preprocesamiento tienen un estado interno que se puede calcular en función de una muestra de los datos de entrenamiento. La lista de capas de preprocesamiento con estado es:

  • TextVectorization : tiene una asignación entre los tokens de cadena y los índices enteros
  • StringLookup y IntegerLookup : tienen un mapeo entre valores de entrada y índices enteros.
  • Normalization : mantiene la media y la desviación estándar de las características.
  • Discretization : contiene información acerca de los límites de cubo de valor.

Fundamentalmente, estas capas son no entrenable. Su estado no se establece durante el entrenamiento; se debe establecer antes de la formación, ya sea mediante la inicialización desde una constante precalculada, o por "adaptar" ellos en los datos.

Se establece el estado de una capa de pre-procesamiento mediante su exposición a los datos de entrenamiento, a través de la adapt() método:

import numpy as np
import tensorflow as tf
from tensorflow.keras import layers

data = np.array([[0.1, 0.2, 0.3], [0.8, 0.9, 1.0], [1.5, 1.6, 1.7],])
layer = layers.Normalization()
layer.adapt(data)
normalized_data = layer(data)

print("Features mean: %.2f" % (normalized_data.numpy().mean()))
print("Features std: %.2f" % (normalized_data.numpy().std()))
Features mean: -0.00
Features std: 1.00

La adapt() método toma ya sea una matriz Numpy o una tf.data.Dataset objeto. En el caso de StringLookup y TextVectorization , también puede pasar una lista de cadenas:

data = [
    "ξεῖν᾽, ἦ τοι μὲν ὄνειροι ἀμήχανοι ἀκριτόμυθοι",
    "γίγνοντ᾽, οὐδέ τι πάντα τελείεται ἀνθρώποισι.",
    "δοιαὶ γάρ τε πύλαι ἀμενηνῶν εἰσὶν ὀνείρων:",
    "αἱ μὲν γὰρ κεράεσσι τετεύχαται, αἱ δ᾽ ἐλέφαντι:",
    "τῶν οἳ μέν κ᾽ ἔλθωσι διὰ πριστοῦ ἐλέφαντος,",
    "οἵ ῥ᾽ ἐλεφαίρονται, ἔπε᾽ ἀκράαντα φέροντες:",
    "οἱ δὲ διὰ ξεστῶν κεράων ἔλθωσι θύραζε,",
    "οἵ ῥ᾽ ἔτυμα κραίνουσι, βροτῶν ὅτε κέν τις ἴδηται.",
]
layer = layers.TextVectorization()
layer.adapt(data)
vectorized_text = layer(data)
print(vectorized_text)
tf.Tensor(
[[37 12 25  5  9 20 21  0  0]
 [51 34 27 33 29 18  0  0  0]
 [49 52 30 31 19 46 10  0  0]
 [ 7  5 50 43 28  7 47 17  0]
 [24 35 39 40  3  6 32 16  0]
 [ 4  2 15 14 22 23  0  0  0]
 [36 48  6 38 42  3 45  0  0]
 [ 4  2 13 41 53  8 44 26 11]], shape=(8, 9), dtype=int64)

Además, las capas adaptables siempre exponen una opción para establecer el estado directamente a través de los argumentos del constructor o la asignación de peso. Si los valores de estado destinados son conocidos en el momento de construcción de la capa, o se calculan fuera de la adapt() de llamadas, que se pueden ajustar sin depender de cálculo interno de la capa. Por ejemplo, si los archivos de vocabulario externo para los TextVectorization , StringLookup , o IntegerLookup ya existen capas, los que se pueden cargar directamente en las tablas de búsqueda haciendo pasar una ruta de acceso al archivo de vocabulario en argumentos de constructor de la capa.

He aquí un ejemplo en el que una instancia de un StringLookup capa con el vocabulario de cálculo previo:

vocab = ["a", "b", "c", "d"]
data = tf.constant([["a", "c", "d"], ["d", "z", "b"]])
layer = layers.StringLookup(vocabulary=vocab)
vectorized_data = layer(data)
print(vectorized_data)
tf.Tensor(
[[1 3 4]
 [4 0 2]], shape=(2, 3), dtype=int64)

Procesamiento previo de datos antes del modelo o dentro del modelo

Hay dos formas de utilizar capas de preprocesamiento:

Opción 1: Haz que parte del modelo, de esta manera:

inputs = keras.Input(shape=input_shape)
x = preprocessing_layer(inputs)
outputs = rest_of_the_model(x)
model = keras.Model(inputs, outputs)

Con esta opción, el preprocesamiento ocurrirá en el dispositivo, de forma sincrónica con el resto de la ejecución del modelo, lo que significa que se beneficiará de la aceleración de la GPU. Si usted está entrenando en la GPU, esta es la mejor opción para la Normalization capa, y para toda la imagen y el procesamiento previo de aumento de capas de datos.

Opción 2: aplicarlo a su tf.data.Dataset , a fin de obtener un conjunto de datos que los rendimientos de los lotes de los datos que se procesan, como este:

dataset = dataset.map(lambda x, y: (preprocessing_layer(x), y))

Con esta opción, su preprocesamiento ocurrirá en la CPU, de forma asincrónica, y se almacenará en búfer antes de ingresar al modelo. Además, si se llama a dataset.prefetch(tf.data.AUTOTUNE) en el conjunto de datos, el procesamiento previo sucederá de manera eficiente en paralelo con la formación:

dataset = dataset.map(lambda x, y: (preprocessing_layer(x), y))
dataset = dataset.prefetch(tf.data.AUTOTUNE)
model.fit(dataset, ...)

Esta es la mejor opción para TextVectorization , y todos los datos estructurados preprocesamiento capas. También puede ser una buena opción si estás entrenando en CPU y usas capas de preprocesamiento de imágenes.

Cuando se ejecuta en TPU, siempre se debe colocar el procesamiento previo capas en el tf.data tubería (con la excepción de Normalization y Rescaling , que funcionan muy bien en TPU y se utilizan comúnmente como la primera capa es un modelo de imagen).

Beneficios de realizar preprocesamiento dentro del modelo en el momento de la inferencia

Incluso si opta por la opción 2, es posible que más adelante desee exportar un modelo de extremo a extremo solo de inferencia que incluirá las capas de preprocesamiento. El beneficio clave para hacerlo es que hace que su modelo de portátil y que ayuda a reducir la formación / porción oblicua .

Cuando todo el preprocesamiento de datos es parte del modelo, otras personas pueden cargar y usar su modelo sin tener que saber cómo se espera que se codifique y normalice cada característica. Su modelo de inferencia podrá procesar imágenes sin procesar o datos estructurados sin procesar, y no requerirá que los usuarios del modelo conozcan los detalles de, por ejemplo, el esquema de tokenización utilizado para el texto, el esquema de indexación utilizado para las características categóricas, si los valores de píxeles de la imagen se normalizan a [-1, +1] o [0, 1] , etc. Esto es especialmente poderosa si se va a exportar su modelo a otro en tiempo de ejecución, tales como TensorFlow.js: usted no tendrá que volver a implementar el procesamiento previo canalización en JavaScript.

Si en un inicio se pone sus capas de pre-procesamiento en su tf.data tubería, puede exportar un modelo de inferencia de que los paquetes de la pre-procesamiento. Simplemente crea una instancia de un nuevo modelo que encadena tus capas de preprocesamiento y tu modelo de entrenamiento:

inputs = keras.Input(shape=input_shape)
x = preprocessing_layer(inputs)
outputs = training_model(x)
inference_model = keras.Model(inputs, outputs)

Recetas rapidas

Aumento de datos de imagen

Tenga en cuenta que las capas de aumento de los datos de imagen sólo están activos durante el entrenamiento (de manera similar a la Dropout de la capa).

from tensorflow import keras
from tensorflow.keras import layers

# Create a data augmentation stage with horizontal flipping, rotations, zooms
data_augmentation = keras.Sequential(
    [
        layers.RandomFlip("horizontal"),
        layers.RandomRotation(0.1),
        layers.RandomZoom(0.1),
    ]
)

# Load some data
(x_train, y_train), _ = keras.datasets.cifar10.load_data()
input_shape = x_train.shape[1:]
classes = 10

# Create a tf.data pipeline of augmented images (and their labels)
train_dataset = tf.data.Dataset.from_tensor_slices((x_train, y_train))
train_dataset = train_dataset.batch(16).map(lambda x, y: (data_augmentation(x), y))


# Create a model and train it on the augmented image data
inputs = keras.Input(shape=input_shape)
x = layers.Rescaling(1.0 / 255)(inputs)  # Rescale inputs
outputs = keras.applications.ResNet50(  # Add the rest of the model
    weights=None, input_shape=input_shape, classes=classes
)(x)
model = keras.Model(inputs, outputs)
model.compile(optimizer="rmsprop", loss="sparse_categorical_crossentropy")
model.fit(train_dataset, steps_per_epoch=5)
Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170500096/170498071 [==============================] - 6s 0us/step
170508288/170498071 [==============================] - 6s 0us/step
5/5 [==============================] - 11s 44ms/step - loss: 8.8927
<keras.callbacks.History at 0x7f1c0c3f16d0>

Se puede ver una configuración similar a la acción en el ejemplo de clasificación de imágenes a partir de cero .

Normalización de características numéricas

# Load some data
(x_train, y_train), _ = keras.datasets.cifar10.load_data()
x_train = x_train.reshape((len(x_train), -1))
input_shape = x_train.shape[1:]
classes = 10

# Create a Normalization layer and set its internal state using the training data
normalizer = layers.Normalization()
normalizer.adapt(x_train)

# Create a model that include the normalization layer
inputs = keras.Input(shape=input_shape)
x = normalizer(inputs)
outputs = layers.Dense(classes, activation="softmax")(x)
model = keras.Model(inputs, outputs)

# Train the model
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy")
model.fit(x_train, y_train)
1563/1563 [==============================] - 3s 2ms/step - loss: 2.1304
<keras.callbacks.History at 0x7f1bc43f40d0>

Codificación de características categóricas de cadenas mediante codificación one-hot

# Define some toy data
data = tf.constant([["a"], ["b"], ["c"], ["b"], ["c"], ["a"]])

# Use StringLookup to build an index of the feature values and encode output.
lookup = layers.StringLookup(output_mode="one_hot")
lookup.adapt(data)

# Convert new test data (which includes unknown feature values)
test_data = tf.constant([["a"], ["b"], ["c"], ["d"], ["e"], [""]])
encoded_data = lookup(test_data)
print(encoded_data)
tf.Tensor(
[[0. 0. 0. 1.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]], shape=(6, 4), dtype=float32)

Tenga en cuenta que, aquí, el índice 0 se reserva para fuera de vocabulario valores (valores que no fueron vistos durante la adapt() ).

Se puede ver la StringLookup en acción en la clasificación de los datos estructurados a partir de cero ejemplo.

Codificación de características categóricas enteras mediante codificación one-hot

# Define some toy data
data = tf.constant([[10], [20], [20], [10], [30], [0]])

# Use IntegerLookup to build an index of the feature values and encode output.
lookup = layers.IntegerLookup(output_mode="one_hot")
lookup.adapt(data)

# Convert new test data (which includes unknown feature values)
test_data = tf.constant([[10], [10], [20], [50], [60], [0]])
encoded_data = lookup(test_data)
print(encoded_data)
tf.Tensor(
[[0. 0. 1. 0. 0.]
 [0. 0. 1. 0. 0.]
 [0. 1. 0. 0. 0.]
 [1. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0.]
 [0. 0. 0. 0. 1.]], shape=(6, 5), dtype=float32)

Tenga en cuenta que el índice 0 se reserva para los valores que faltan (que se debe especificar como el valor 0), y el índice 1 está reservado para fuera de vocabulario valores (valores que no fueron vistos durante la adapt() ). Esto se puede configurar mediante el uso de la mask_token y oov_token argumentos de constructor de IntegerLookup .

Se puede ver la IntegerLookup en acción en el ejemplo estructurado clasificación de datos desde cero .

Aplicar el truco de hash a una característica categórica de números enteros

Si tiene una característica categórica que puede tomar muchos valores diferentes (del orden de 10e3 o más), donde cada valor solo aparece unas pocas veces en los datos, se vuelve impráctico e ineficaz indexar y codificar en un solo uso los valores de la característica. En cambio, puede ser una buena idea aplicar el "truco de hash": hash de los valores en un vector de tamaño fijo. Esto mantiene manejable el tamaño del espacio de características y elimina la necesidad de indexación explícita.

# Sample data: 10,000 random integers with values between 0 and 100,000
data = np.random.randint(0, 100000, size=(10000, 1))

# Use the Hashing layer to hash the values to the range [0, 64]
hasher = layers.Hashing(num_bins=64, salt=1337)

# Use the CategoryEncoding layer to multi-hot encode the hashed values
encoder = layers.CategoryEncoding(num_tokens=64, output_mode="multi_hot")
encoded_data = encoder(hasher(data))
print(encoded_data.shape)
(10000, 64)

Codificación de texto como una secuencia de índices de token

Esta es la forma en que debe preprocesar el texto que se pasa a una Embedding capa.

# Define some text data to adapt the layer
adapt_data = tf.constant(
    [
        "The Brain is wider than the Sky",
        "For put them side by side",
        "The one the other will contain",
        "With ease and You beside",
    ]
)

# Create a TextVectorization layer
text_vectorizer = layers.TextVectorization(output_mode="int")
# Index the vocabulary via `adapt()`
text_vectorizer.adapt(adapt_data)

# Try out the layer
print(
    "Encoded text:\n", text_vectorizer(["The Brain is deeper than the sea"]).numpy(),
)

# Create a simple model
inputs = keras.Input(shape=(None,), dtype="int64")
x = layers.Embedding(input_dim=text_vectorizer.vocabulary_size(), output_dim=16)(inputs)
x = layers.GRU(8)(x)
outputs = layers.Dense(1)(x)
model = keras.Model(inputs, outputs)

# Create a labeled dataset (which includes unknown tokens)
train_dataset = tf.data.Dataset.from_tensor_slices(
    (["The Brain is deeper than the sea", "for if they are held Blue to Blue"], [1, 0])
)

# Preprocess the string inputs, turning them into int sequences
train_dataset = train_dataset.batch(2).map(lambda x, y: (text_vectorizer(x), y))
# Train the model on the int sequences
print("\nTraining model...")
model.compile(optimizer="rmsprop", loss="mse")
model.fit(train_dataset)

# For inference, you can export a model that accepts strings as input
inputs = keras.Input(shape=(1,), dtype="string")
x = text_vectorizer(inputs)
outputs = model(x)
end_to_end_model = keras.Model(inputs, outputs)

# Call the end-to-end model on test data (which includes unknown tokens)
print("\nCalling end-to-end model on test string...")
test_data = tf.constant(["The one the other will absorb"])
test_output = end_to_end_model(test_data)
print("Model output:", test_output)
Encoded text:
 [[ 2 19 14  1  9  2  1]]

Training model...
1/1 [==============================] - 3s 3s/step - loss: 0.4776

Calling end-to-end model on test string...
Model output: tf.Tensor([[0.04233753]], shape=(1, 1), dtype=float32)

Se puede ver la TextVectorization capa en acción, combinada con una Embedding modo, en el ejemplo de clasificación de texto a partir de cero .

Tenga en cuenta que al entrenar a un modelo de este tipo, para un mejor rendimiento, siempre debe utilizar el TextVectorization capa como parte de la tubería de entrada.

Codificación de texto como una matriz densa de ngrams con codificación multi-hot

Esta es la forma en que debe preprocesar el texto que se pasa a una Dense capa.

# Define some text data to adapt the layer
adapt_data = tf.constant(
    [
        "The Brain is wider than the Sky",
        "For put them side by side",
        "The one the other will contain",
        "With ease and You beside",
    ]
)
# Instantiate TextVectorization with "multi_hot" output_mode
# and ngrams=2 (index all bigrams)
text_vectorizer = layers.TextVectorization(output_mode="multi_hot", ngrams=2)
# Index the bigrams via `adapt()`
text_vectorizer.adapt(adapt_data)

# Try out the layer
print(
    "Encoded text:\n", text_vectorizer(["The Brain is deeper than the sea"]).numpy(),
)

# Create a simple model
inputs = keras.Input(shape=(text_vectorizer.vocabulary_size(),))
outputs = layers.Dense(1)(inputs)
model = keras.Model(inputs, outputs)

# Create a labeled dataset (which includes unknown tokens)
train_dataset = tf.data.Dataset.from_tensor_slices(
    (["The Brain is deeper than the sea", "for if they are held Blue to Blue"], [1, 0])
)

# Preprocess the string inputs, turning them into int sequences
train_dataset = train_dataset.batch(2).map(lambda x, y: (text_vectorizer(x), y))
# Train the model on the int sequences
print("\nTraining model...")
model.compile(optimizer="rmsprop", loss="mse")
model.fit(train_dataset)

# For inference, you can export a model that accepts strings as input
inputs = keras.Input(shape=(1,), dtype="string")
x = text_vectorizer(inputs)
outputs = model(x)
end_to_end_model = keras.Model(inputs, outputs)

# Call the end-to-end model on test data (which includes unknown tokens)
print("\nCalling end-to-end model on test string...")
test_data = tf.constant(["The one the other will absorb"])
test_output = end_to_end_model(test_data)
print("Model output:", test_output)
WARNING:tensorflow:5 out of the last 1567 calls to <function PreprocessingLayer.make_adapt_function.<locals>.adapt_step at 0x7f1b9c5c5290> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
Encoded text:
 [[1. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 1. 1. 0. 0. 0. 0. 0.

  0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 1. 1. 0. 0. 0.]]

Training model...
1/1 [==============================] - 0s 231ms/step - loss: 1.0046

Calling end-to-end model on test string...
Model output: tf.Tensor([[-0.54753447]], shape=(1, 1), dtype=float32)

Codificación de texto como una matriz densa de ngrams con ponderación TF-IDF

Esta es una forma alternativa de pre-procesamiento de texto antes de pasarlo a una Dense capa.

# Define some text data to adapt the layer
adapt_data = tf.constant(
    [
        "The Brain is wider than the Sky",
        "For put them side by side",
        "The one the other will contain",
        "With ease and You beside",
    ]
)
# Instantiate TextVectorization with "tf-idf" output_mode
# (multi-hot with TF-IDF weighting) and ngrams=2 (index all bigrams)
text_vectorizer = layers.TextVectorization(output_mode="tf-idf", ngrams=2)
# Index the bigrams and learn the TF-IDF weights via `adapt()`

with tf.device("CPU"):
    # A bug that prevents this from running on GPU for now.
    text_vectorizer.adapt(adapt_data)

# Try out the layer
print(
    "Encoded text:\n", text_vectorizer(["The Brain is deeper than the sea"]).numpy(),
)

# Create a simple model
inputs = keras.Input(shape=(text_vectorizer.vocabulary_size(),))
outputs = layers.Dense(1)(inputs)
model = keras.Model(inputs, outputs)

# Create a labeled dataset (which includes unknown tokens)
train_dataset = tf.data.Dataset.from_tensor_slices(
    (["The Brain is deeper than the sea", "for if they are held Blue to Blue"], [1, 0])
)

# Preprocess the string inputs, turning them into int sequences
train_dataset = train_dataset.batch(2).map(lambda x, y: (text_vectorizer(x), y))
# Train the model on the int sequences
print("\nTraining model...")
model.compile(optimizer="rmsprop", loss="mse")
model.fit(train_dataset)

# For inference, you can export a model that accepts strings as input
inputs = keras.Input(shape=(1,), dtype="string")
x = text_vectorizer(inputs)
outputs = model(x)
end_to_end_model = keras.Model(inputs, outputs)

# Call the end-to-end model on test data (which includes unknown tokens)
print("\nCalling end-to-end model on test string...")
test_data = tf.constant(["The one the other will absorb"])
test_output = end_to_end_model(test_data)
print("Model output:", test_output)
WARNING:tensorflow:6 out of the last 1568 calls to <function PreprocessingLayer.make_adapt_function.<locals>.adapt_step at 0x7f1b9f6eae60> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
Encoded text:
 [[5.461647  1.6945957 0.        0.        0.        0.        0.

  0.        0.        0.        0.        0.        0.        0.
  0.        0.        1.0986123 1.0986123 1.0986123 0.        0.
  0.        0.        0.        0.        0.        0.        0.
  1.0986123 0.        0.        0.        0.        0.        0.
  0.        1.0986123 1.0986123 0.        0.        0.       ]]

Training model...
1/1 [==============================] - 0s 239ms/step - loss: 4.4868

Calling end-to-end model on test string...
Model output: tf.Tensor([[0.25670475]], shape=(1, 1), dtype=float32)

Errores importantes

Trabajar con capas de búsqueda con vocabularios muy extensos

Es posible que se encuentre trabajando con un vocabulario muy grande en un TextVectorization , un StringLookup capa, o un IntegerLookup capa. Normalmente, un vocabulario superior a 500 MB se consideraría "muy extenso".

En tal caso, para un mejor rendimiento, se debe evitar el uso de adapt() . En su lugar, calcule previamente su vocabulario con anticipación (podría usar Apache Beam o TF Transform para esto) y guárdelo en un archivo. A continuación, cargue el vocabulario en la capa en el tiempo de construcción haciendo pasar la ruta de archivo como el vocabulary argumento.

El uso de capas de búsqueda en una vaina de TPU o con ParameterServerStrategy .

No es una cuestión pendiente que hace que el rendimiento se degrade cuando se utiliza un TextVectorization , StringLookup o IntegerLookup capa mientras que la formación de una vaina de TPU o en varias máquinas a través de ParameterServerStrategy . Esto está programado para solucionarse en TensorFlow 2.7.