使用預先處理層

作者: 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:保留關於值儲存區邊界的資訊。

至關重要的是,這些層是不可訓練的。它們的狀態並非在訓練期間設定;必須在訓練前設定,方法是從預先計算的常數初始化它們,或在資料上「調整」它們。

您可以透過 adapt() 方法將預先處理層公開給訓練資料,藉此設定預先處理層的狀態

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)

此外,可調整層一律會公開選項,以透過建構函式引數或權重指派直接設定狀態。如果預期的狀態值在層建構時已知,或是在 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.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 上進行訓練並使用圖片預先處理層,這也可能是一個不錯的選項。

請注意,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 相容,適用於跨多部機器執行訓練。

一般而言,預先處理層應放置在 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://tensorflow.dev.org.tw/guide/function#controlling_retracing and https://tensorflow.dev.org.tw/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://tensorflow.dev.org.tw/guide/function#controlling_retracing and https://tensorflow.dev.org.tw/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 中修正。