使用预处理层

作者:Francois Chollet,Mark Omernick

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 在 keras.io 上查看

Keras 预处理

Keras 预处理层 API 允许开发人员构建 Keras 原生的输入处理管道。这些输入处理管道可以用作非 Keras 工作流程中的独立预处理代码,直接与 Keras 模型结合使用,并作为 Keras SavedModel 的一部分导出。

使用 Keras 预处理层,您可以构建和导出真正端到端的模型:接受原始图像或原始结构化数据作为输入的模型;能够自行处理特征规范化或特征值索引的模型。

可用的预处理

文本预处理

数值特征预处理

类别特征预处理

图像预处理

这些层用于标准化图像模型的输入。

图像数据增强

这些层将随机增强变换应用于一批图像。它们仅在训练期间处于活动状态。

adapt() 方法

一些预处理层具有内部状态,该状态可以根据训练数据的样本进行计算。有状态预处理层的列表如下:

  • TextVectorization: 保存字符串标记和整数索引之间的映射
  • StringLookupIntegerLookup: 保存输入值和整数索引之间的映射。
  • Normalization: 保存特征的均值和标准差。
  • Discretization: 保存有关值桶边界的信息。

至关重要的是,这些层是 **不可训练的**。它们的 state 不会在训练期间设置;它必须在 **训练之前** 设置,要么通过从预先计算的常量初始化它们,要么通过在数据上“适应”它们。

通过使用 adapt() 方法将预处理层暴露给训练数据,来设置预处理层的 state。

import numpy as np
import tensorflow as tf
from tensorflow import keras
from 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

adapt() 方法接受 Numpy 数组或 tf.data.Dataset 对象。对于 StringLookupTextVectorization,还可以传递字符串列表。

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)

此外,可适应层始终提供通过构造函数参数或权重分配直接设置 state 的选项。如果预期的 state 值在层构造时已知,或者是在 adapt() 调用之外计算的,则可以设置它们,而无需依赖层的内部计算。例如,如果 TextVectorizationStringLookupIntegerLookup 层的外部词汇表文件已存在,则可以通过在层的构造函数参数中传递词汇表文件的路径,将它们直接加载到查找表中。

以下是一个使用预先计算的词汇表实例化 StringLookup 层的示例。

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)

在模型之前或模型内部预处理数据

可以使用两种方法使用预处理层。

**选项 1:** 将它们作为模型的一部分,如下所示。

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

使用此选项,预处理将在设备上同步进行,与模型执行的其余部分同步,这意味着它将受益于 GPU 加速。如果您在 GPU 上进行训练,这是 Normalization 层以及所有图像预处理和数据增强层的最佳选择。

**选项 2:** 将其应用于您的 tf.data.Dataset,以便获得一个产生预处理数据批次的 dataset,如下所示。

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

使用此选项,您的预处理将在 CPU 上异步进行,并在进入模型之前进行缓冲。此外,如果您在 dataset 上调用 dataset.prefetch(tf.data.AUTOTUNE),则预处理将与训练并行高效地进行。

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

这是 TextVectorization 以及所有结构化数据预处理层的最佳选择。如果您在 CPU 上进行训练并使用图像预处理层,这也是一个不错的选择。

请注意,TextVectorization 层只能在 CPU 上执行,因为它主要是一个字典查找操作。因此,如果您在 GPU 或 TPU 上训练模型,则应将 TextVectorization 层放在 tf.data 管道中以获得最佳性能。

**在 TPU 上运行时,应始终将预处理层放在 tf.data 管道中**(NormalizationRescaling 除外,它们可以在 TPU 上正常运行,并且通常用作图像模型中的第一层)。

在推理时在模型内部进行预处理的好处

即使您选择了选项 2,您可能仍希望稍后导出一个仅用于推理的端到端模型,该模型将包含预处理层。这样做主要的好处是 **它使您的模型可移植**,并且 **有助于减少 训练/服务偏差**。

当所有数据预处理都是模型的一部分时,其他人可以加载和使用您的模型,而无需了解每个特征的预期编码和规范化方式。您的推理模型将能够处理原始图像或原始结构化数据,并且不需要模型用户了解例如文本使用的标记化方案、类别特征使用的索引方案、图像像素值是否规范化为 [-1, +1][0, 1] 等细节。如果您将模型导出到另一个运行时(例如 TensorFlow.js),这将特别有用:您无需在 JavaScript 中重新实现预处理管道。

如果您最初将预处理层放在 tf.data 管道中,则可以导出一个将预处理打包在一起的推理模型。只需实例化一个新的模型,该模型将您的预处理层和训练模型链接在一起即可。

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

多工作器训练期间的预处理

预处理层与 tf.distribute API 兼容,该 API 用于在多台机器上运行训练。

通常,预处理层应放在 tf.distribute.Strategy.scope() 中,并在模型内部或模型之前调用,如上所述。

with strategy.scope():
    inputs = keras.Input(shape=input_shape)
    preprocessing_layer = tf.keras.layers.Hashing(10)
    dense_layer = tf.keras.layers.Dense(16)

有关更多详细信息,请参阅 分布式输入 教程的“数据预处理”部分。

快速食谱

图像数据增强

请注意,图像数据增强层仅在训练期间处于活动状态(类似于 Dropout 层)。

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
170498071/170498071 [==============================] - 5s 0us/step
5/5 [==============================] - 25s 31ms/step - loss: 9.0505
<keras.src.callbacks.History at 0x7fdb34287820>

您可以在 从头开始的图像分类 示例中看到类似的设置。

规范化数值特征

# 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.1271
<keras.src.callbacks.History at 0x7fda8c6f0730>

通过独热编码对字符串类别特征进行编码

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

请注意,此处索引 0 保留给词汇表外值(在 adapt() 期间未见过的值)。

您可以在 从头开始的结构化数据分类 示例中看到 StringLookup 的实际应用。

通过独热编码对整数类别特征进行编码

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

请注意,索引 0 保留给缺失值(应将其指定为值 0),索引 1 保留给词汇表外值(在 adapt() 期间未见过的值)。您可以通过使用 IntegerLookupmask_tokenoov_token 构造函数参数来配置此设置。

您可以在 从头开始的结构化数据分类 示例中看到 IntegerLookup 的实际应用。

将哈希技巧应用于整数类别特征

如果您有一个可以取许多不同值的类别特征(大约 1e4 或更高),其中每个值在数据中只出现几次,那么对特征值进行索引和独热编码变得不切实际且效率低下。相反,应用“哈希技巧”可能是一个好主意:将值哈希到固定大小的向量中。这可以使特征空间的大小保持可控,并且无需显式索引。

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

将文本编码为标记索引序列

这是将文本预处理以传递给 Embedding 层的方式。

# 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 [==============================] - 2s 2s/step - loss: 0.5296

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

您可以在 从头开始的文本分类 示例中看到 TextVectorization 层与 Embedding 模式结合使用的实际应用。

请注意,在训练此类模型时,为了获得最佳性能,应始终将 TextVectorization 层作为输入管道的一部分。

将文本编码为具有多热编码的 N 元组密集矩阵

这是将文本预处理以传递给 Dense 层的方式。

# 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 0x7fda8c3463a0> 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 reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://tensorflowcn.cn/guide/function#controlling_retracing and https://tensorflowcn.cn/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 392ms/step - loss: 0.0805

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

将文本编码为具有 TF-IDF 权重的 N 元组密集矩阵

这是将文本预处理以传递给 Dense 层的另一种方法。

# 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()`
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 0x7fda8c0569d0> 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 reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://tensorflowcn.cn/guide/function#controlling_retracing and https://tensorflowcn.cn/api_docs/python/tf/function for  more details.
Encoded text:
 [[5.4616475 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 363ms/step - loss: 6.8945

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

重要注意事项

使用具有非常大词汇表的查找层

您可能会发现自己正在 TextVectorizationStringLookup 层或 IntegerLookup 层中使用非常大的词汇表。通常,大于 500MB 的词汇表将被视为“非常大”。

在这种情况下,为了获得最佳性能,应避免使用 adapt()。相反,应预先计算词汇表(可以使用 Apache Beam 或 TF Transform 来完成此操作),并将其存储在文件中。然后,通过将文件路径作为 vocabulary 参数传递,在构造时将词汇表加载到层中。

在 TPU pod 或使用 ParameterServerStrategy 上使用查找层。

存在一个未解决的问题,会导致在 TPU pod 上或通过 ParameterServerStrategy 在多台机器上进行训练时,使用 TextVectorizationStringLookupIntegerLookup 层时性能下降。这将在 TensorFlow 2.7 中得到修复。