##### Copyright 2020 The TensorFlow Authors.
#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

使用预处理层#

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 GitHub 上查看源代码 下载笔记本

Keras 预处理#

Keras 预处理层 API 可供开发者构建 Keras 原生输入处理流水线。这些输入处理流水线可在非 Keras 工作流中用作独立预处理代码,直接与 Keras 模型结合,并作为 Keras SavedModel 的一部分导出。

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

可用预处理#

文本预处理#

  • tf.keras.layers.TextVectorization:将原始字符串转换为可由 Embedding 层或 Dense 层读取的编码表示法。

数值特征预处理#

  • tf.keras.layers.Normalization:对输入特征执行逐特征归一化。

  • tf.keras.layers.Discretization:将连续数值特征转换为整数分类特征。

分类特征预处理#

  • tf.keras.layers.CategoryEncoding:将整数分类特征转换为独热、多热或计数密集表示法。

  • tf.keras.layers.Hashing:执行分类特征哈希,也称为“哈希技巧”(hashing trick)。

  • tf.keras.layers.StringLookup:将字符串分类值转换为可由 Embedding 层或 Dense 层读取的编码表示法。

  • tf.keras.layers.IntegerLookup:将整数分类值转换为可由 Embedding 层或 Dense 层读取的编码表示法。

图像预处理#

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

  • tf.keras.layers.Resizing:将一批图像的大小调整为目标大小。

  • tf.keras.layers.Rescaling:重新缩放和偏移一批图像的值(例如,从 [0, 255] 范围内的输入变为 [0, 1] 范围内的输入)。

  • tf.keras.layers.CenterCrop:返回一批图像的中心裁剪。

图像数据增强#

以下层会对一批图像应用随机增强转换。它们仅在训练期间有效。

  • tf.keras.layers.RandomCrop

  • tf.keras.layers.RandomFlip

  • tf.keras.layers.RandomTranslation

  • tf.keras.layers.RandomRotation

  • tf.keras.layers.RandomZoom

  • tf.keras.layers.RandomHeight

  • tf.keras.layers.RandomWidth

  • tf.keras.layers.RandomContrast

adapt() 方法#

一些预处理层具有可基于训练数据样本计算的内部状态。以下是有状态预处理层的列表:

  • TextVectorization:保存字符串词例和整数索引之间的映射。

  • StringLookupIntegerLookup:保存输入值和整数索引之间的映射。

  • Normalization:保存特征的平均值和标准差。

  • Discretization:保存值桶边界相关信息。

至关重要的是,这些层不可训练。它们的状态不是在训练期间设定的;必须在训练之前设置状态,方法是通过预先计算的常量对其进行初始化,或者基于数据对其进行“调整”。

您可以通过如下 adapt() 方法将预处理层公开给训练数据以设置其状态:

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

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

data = [
    "ξεῖν᾽, ἦ τοι μὲν ὄνειροι ἀμήχανοι ἀκριτόμυθοι",
    "γίγνοντ᾽, οὐδέ τι πάντα τελείεται ἀνθρώποισι.",
    "δοιαὶ γάρ τε πύλαι ἀμενηνῶν εἰσὶν ὀνείρων:",
    "αἱ μὲν γὰρ κεράεσσι τετεύχαται, αἱ δ᾽ ἐλέφαντι:",
    "τῶν οἳ μέν κ᾽ ἔλθωσι διὰ πριστοῦ ἐλέφαντος,",
    "οἵ ῥ᾽ ἐλεφαίρονται, ἔπε᾽ ἀκράαντα φέροντες:",
    "οἱ δὲ διὰ ξεστῶν κεράων ἔλθωσι θύραζε,",
    "οἵ ῥ᾽ ἔτυμα κραίνουσι, βροτῶν ὅτε κέν τις ἴδηται.",
]
layer = layers.TextVectorization()
layer.adapt(data)
vectorized_text = layer(data)
print(vectorized_text)

此外,自适应层始终公开一个可以通过构造函数参数或权重赋值直接设置状态的选项。如果预期的状态值在构造层时已知,或者是在 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)

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

您可以通过以下两种方式使用预处理层:

选项 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.map(lambda x, y: (preprocessing_layer(x), y))

使用此选项,您的预处理将在 CPU 上异步进行,并在进入模型之前进行缓存。此外,如果您对数据集调用 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 上进行训练并且使用图像预处理层,那么这同样是一个不错的选择。

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

快速秘诀#

图像数据增强#

请注意,图像数据增强层仅在训练期间有效(类似于 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)

您可以在从零开始进行图像分类示例中查看类似设置。

归一化数值特征#

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

通过独热编码进行字符串分类特征编码#

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

请注意,此处的索引 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)

请注意,索引 0 为缺失值(您应将其指定为值 0)保留,索引 1 为词汇之外的值(adapt() 期间未出现的值)保留。您可以使用 IntegerLookupmask_tokenoov_token 构造函数参数进行配置。

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

对整数分类特征应用哈希技巧#

如果您拥有可以接受许多不同值(约 10e3 或更高次方)的分类特征,其中每个值仅在数据中出现几次,那么对特征值进行索引和独热编码就变得不切实际且低效。相反,应用“哈希技巧”可能是一个好主意:将值散列到固定大小的向量。这使得特征空间的大小易于管理,并且无需显式索引。

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

将文本编码为词例索引序列#

这是预处理要传递到 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)

您可以在示例从头开始进行文本分类中查看 TextVectorization 层与 Embedding 模式组合的实际使用情况。

请注意,在训练此类模型时,为了获得最佳性能,您应始终使用 TextVectorization 层作为输入流水线的一部分。

通过多热编码将文本编码为 ngram 的密集矩阵#

这是预处理要传递到 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)

通过 TF-IDF 加权将文本编码为 ngram 的密集矩阵#

这是在将文本传递到 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()`

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)

重要问题#

处理包含非常大的词汇的查找层#

您可能会在 TextVectorizationStringLookup 层或 IntegerLookup 层中处理非常大的词汇。通常,大于 500MB 的词汇就会被视为“非常大”。

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

在 TPU pod 上或与 ParameterServerStrategy 一起使用查找层。#

有一个未解决的问题,它会导致在 TPU pod 上或通过 ParameterServerStrategy 在多台计算机上进行训练时,使用 TextVectorizationStringLookupIntegerLookup 层时出现性能下降。该问题预计将在 TensorFlow 2.7 中得到修正。