Trabalhar com camadas de pré-processamento

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

Pré-processamento Keras

A API de camadas de pré-processamento Keras permite que os desenvolvedores criem pipelines de processamento de entrada nativos Keras. Esses pipelines de processamento de entrada podem ser usados ​​como código de pré-processamento independente em fluxos de trabalho não Keras, combinados diretamente com modelos Keras e exportados como parte de um Keras SavedModel.

Com as camadas de pré-processamento Keras, você pode construir e exportar modelos que são verdadeiramente ponta a ponta: modelos que aceitam imagens brutas ou dados estruturados brutos como entrada; modelos que lidam com normalização de recurso ou indexação de valor de recurso por conta própria.

Pré-processamento disponível

Pré-processamento de texto

Pré-processamento de recursos numéricos

Pré-processamento de recursos categóricos

  • tf.keras.layers.CategoryEncoding : turnos inteiros características categóricas em representações densas one-quentes, multi-quentes, ou contam.
  • tf.keras.layers.Hashing : Realiza hashing característica categórica, também conhecido como o "truque hashing".
  • tf.keras.layers.StringLookup : cadeia voltas categórica valoriza uma representação codificada que pode ser lido por uma Embedding camada ou Dense camada.
  • tf.keras.layers.IntegerLookup : voltas inteiras valores categóricas em uma representação codificada que pode ser lido por uma Embedding camada ou Dense camada.

Pré-processamento de imagem

Essas camadas são para padronizar as entradas de um modelo de imagem.

Aumento de dados de imagem

Essas camadas aplicam transformações de aumento aleatórias a um lote de imagens. Eles estão ativos apenas durante o treinamento.

A adapt() Método

Algumas camadas de pré-processamento têm um estado interno que pode ser calculado com base em uma amostra dos dados de treinamento. A lista de camadas de pré-processamento com estado é:

  • TextVectorization : mantém um mapeamento entre fichas de cordas e índices inteiros
  • StringLookup e IntegerLookup : segurar um mapeamento entre os valores de entrada e de índices de números inteiros.
  • Normalization : detém a média e desvio padrão das características.
  • Discretization : contém informações sobre limites de balde de valor.

Fundamentalmente, essas camadas não são orientáveis. Seu estado não é definido durante o treinamento; ele deve ser definido antes do treino, ou por inicializando-los de uma constante pré-computadas, ou "adaptação"-los em dados.

Você define o estado de uma camada pré-processamento, expondo-o aos dados de treinamento, através da 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

A adapt() método leva qualquer uma matriz Numpy ou um tf.data.Dataset objecto. No caso de StringLookup e TextVectorization , você também pode passar uma lista de strings:

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)

Além disso, as camadas adaptáveis ​​sempre expõem uma opção para definir o estado diretamente por meio de argumentos do construtor ou atribuição de peso. Se os valores de estado pretendidos são conhecidos no tempo de construção da camada, ou são calculadas fora da adapt() chamada, que pode ser ajustado, sem depender de computação interna da camada. Por exemplo, se arquivos de vocabulário externo para os TextVectorization , StringLookup , ou IntegerLookup camadas já existem, aqueles que podem ser carregados diretamente nas tabelas de pesquisa por meio de um caminho para o arquivo de vocabulário em argumentos do construtor da camada.

Aqui está um exemplo em que instanciar um StringLookup camada com vocabulário pré-computados:

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)

Dados de pré-processamento antes do modelo ou dentro do modelo

Existem duas maneiras de usar camadas de pré-processamento:

Opção 1: Faça-os parte do modelo, como este:

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

Com esta opção, o pré-processamento acontecerá no dispositivo, de forma síncrona com o resto da execução do modelo, o que significa que ele se beneficiará da aceleração da GPU. Se você está treinando em GPU, esta é a melhor opção para a Normalization camada, e para toda imagem de pré-processamento e camadas de aumento de dados.

Opção 2: aplicá-lo ao seu tf.data.Dataset , de modo a obter um conjunto de dados que produz lotes de dados pré-processados, como este:

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

Com esta opção, seu pré-processamento acontecerá na CPU, de forma assíncrona, e será armazenado em buffer antes de entrar no modelo. Além disso, se você chamar dataset.prefetch(tf.data.AUTOTUNE) em seu conjunto de dados, o pré-processamento vai acontecer de forma eficiente em paralelo com a formação:

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

Esta é a melhor opção para TextVectorization , e todos os dados estruturados pré-processamento de camadas. Também pode ser uma boa opção se você estiver treinando na CPU e usar camadas de pré-processamento de imagem.

Quando rodando em TPU, você deve sempre colocar o pré-processamento camadas na tf.data pipeline (com exceção de Normalization e Rescaling , que funcionam muito bem em TPU e são comumente usados como a primeira camada é um modelo de imagem).

Benefícios de fazer o pré-processamento dentro do modelo no momento da inferência

Mesmo se você escolher a opção 2, pode querer exportar posteriormente um modelo de ponta a ponta somente para inferência que incluirá as camadas de pré-processamento. O benefício chave para fazer isso é que faz o seu modelo de portátil e ajuda a reduzir a formação / servindo de inclinação .

Quando todo o pré-processamento de dados faz parte do modelo, outras pessoas podem carregar e usar seu modelo sem precisar saber como cada recurso deve ser codificado e normalizado. Seu modelo de inferência será capaz de processar imagens brutas ou dados estruturados brutos e não exigirá que os usuários do modelo conheçam os detalhes, por exemplo, o esquema de tokenização usado para texto, o esquema de indexação usado para recursos categóricos, sejam valores de pixel de imagem são normalizados para [-1, +1] ou [0, 1] , etc. Isto é especialmente poderoso se você está exportando o seu modelo para outro tempo de execução, tais como TensorFlow.js: você não terá que reimplementar o seu pré-processamento pipeline em JavaScript.

Se você inicialmente colocou suas camadas de pré-processamento em sua tf.data gasoduto, você pode exportar um modelo de inferência de que os pacotes de pré-processamento. Basta instanciar um novo modelo que encadeia suas camadas de pré-processamento e seu modelo de treinamento:

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

Receitas rápidas

Aumento de dados de imagem

Note-se que as camadas de aumento de dados de imagem são apenas activo durante o treino (semelhante ao Dropout camada).

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>

Você pode ver uma configuração semelhante em ação no exemplo classificação de imagens a partir do zero .

Normalizando recursos numéricos

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

Codificação de recursos categóricos de string por meio de codificação 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)

Note-se que, aqui, índice 0 é reservado para fora-de-vocabulário valores (valores que não foram observados durante adapt() ).

Você pode ver o StringLookup em ação na classificação de dados estruturados a partir do zero exemplo.

Codificação de recursos categóricos inteiros por meio de codificação 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)

Note-se que o índice 0 é reservado para valores em falta (que você deve especificar como o valor 0), e índice 1 é reservado para fora de vocabulário valores (valores que não foram vistos durante a adapt() ). Você pode configurar isso usando o mask_token e oov_token argumentos do construtor de IntegerLookup .

Você pode ver o IntegerLookup em ação no exemplo estruturado classificação de dados a partir do zero .

Aplicando o truque de hashing a um recurso categórico de número inteiro

Se você tiver um recurso categórico que pode assumir muitos valores diferentes (na ordem de 10e3 ou mais), em que cada valor aparece apenas algumas vezes nos dados, torna-se impraticável e ineficaz indexar e codificar os valores do recurso em um único momento. Em vez disso, pode ser uma boa ideia aplicar o "truque de hashing": hash os valores para um vetor de tamanho fixo. Isso mantém o tamanho do espaço do recurso gerenciável e elimina a necessidade de indexação 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)

Codificando texto como uma sequência de índices de token

Isto é como você deve pré-processar o texto a ser passado para um Embedding camada.

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

Você pode ver o TextVectorization camada em ação, combinada com uma Embedding modo, no exemplo de classificação de texto a partir do zero .

Note-se que quando o treinamento tal modelo um, para um melhor desempenho, você deve sempre usar o TextVectorization camada como parte do pipeline de entrada.

Codificando texto como uma matriz densa de ngrams com codificação multi-hot

Isto é como você deve pré-processar o texto a ser passado para uma Dense camada.

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

Codificando texto como uma matriz densa de ngrams com ponderação TF-IDF

Esta é uma forma alternativa de pré-processamento de texto antes de passá-la para uma Dense camada.

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

Dicas importantes

Trabalhar com camadas de pesquisa com vocabulários muito grandes

Você pode encontrar-se trabalhando com um grande vocabulário em um TextVectorization , um StringLookup camada, ou um IntegerLookup camada. Normalmente, um vocabulário maior que 500 MB seria considerado "muito grande".

Nesse caso, para um melhor desempenho, você deve evitar o uso de adapt() . Em vez disso, pré-calcule seu vocabulário com antecedência (você pode usar o Apache Beam ou TF Transform para isso) e armazene-o em um arquivo. Em seguida, carregar o vocabulário para dentro da camada pelo tempo de construção, passando o caminhoficheiro como o vocabulary argumento.

Utilizando as camadas de pesquisa sobre uma vagem ou TPU com ParameterServerStrategy .

Não é uma questão pendente que faz com que o desempenho se degradar ao usar um TextVectorization , StringLookup , ou IntegerLookup camada durante o treinamento em um pod TPU ou em várias máquinas através ParameterServerStrategy . Isso está programado para ser corrigido no TensorFlow 2.7.