Keras 预处理层简介
2021 年 11 月 24 日

作者:Matthew Watson,Keras 开发者

确定数据正确的特征表示可能是构建模型最棘手的部分之一。想象一下,您正在处理诸如颜色名称之类的分类输入特征。您可以对特征进行独热编码,以便每个颜色在特定索引处获得一个 1('red' = [0, 0, 1, 0, 0]),或者您可以嵌入特征,以便每个颜色映射到一个独特的可训练向量('red' = [0.1, 0.2, 0.5, -0.2])。更大的类别空间可能更适合嵌入,而较小的空间则更适合独热编码,但答案并不明确。这需要您在特定数据集上进行实验。

理想情况下,我们希望对特征表示的更新和对模型架构的更新在紧密的迭代循环中发生,将新的变换应用于我们的数据,同时更改我们的模型架构。在实践中,特征预处理模型构建通常由完全不同的库、框架或语言处理。这可能会减慢实验过程。

在 Keras 团队中,我们最近发布了Keras 预处理层,这是一组Keras 层,旨在使预处理数据更自然地融入模型开发工作流程。在本文中,我们将使用这些层来构建一个简单的带有IMDb 电影评论数据集的情感分类模型。目标是展示如何灵活地开发和应用预处理。首先,我们可以导入 tensorflow 并下载训练数据。

import tensorflow as tf
import tensorflow_datasets as tfds

train_ds = tfds.load('imdb_reviews', split='train', as_supervised=True).batch(32)

Keras 预处理层可以处理各种输入,包括结构化数据、图像和文本。在本例中,我们将处理原始文本,因此我们将使用TextVectorization 层。

默认情况下,TextVectorization 层将文本处理分为三个阶段。

  • 首先,删除标点符号并小写输入。
  • 接下来,将文本拆分为单个字符串词的列表。
  • 最后,使用已知词的词汇表将字符串映射到数字输出。

我们可以尝试的一个简单方法是多热编码,我们只考虑评论中是否存在术语。例如,假设一个层的词汇表为 ['movie', 'good', 'bad'],而评论为 'This movie was bad.'。我们将将其编码为 [1, 0, 1],其中 movie(第一个词汇表项)和 bad(最后一个词汇表项)存在。

text_vectorizer = tf.keras.layers.TextVectorization(
     output_mode='multi_hot', max_tokens=2500)
features = train_ds.map(lambda x, y: x)
text_vectorizer.adapt(features)

在上面,我们创建了一个具有多热输出的 TextVectorization 层,并执行两件事来设置层的狀態。首先,我们遍历训练数据集并丢弃表示正面或负面评论的整数标签。这给了我们一个仅包含评论文本的数据集。接下来,我们adapt()该层遍历此数据集,这会导致该层学习所有文档中最常见项的词汇表,限制为最多 2500 个项。

Adapt 是所有有状态预处理层上的一个实用程序函数,它允许层从输入数据设置其内部状态。调用 adapt 始终是可选的。对于 TextVectorization,我们可以改为在层构造时提供一个预先计算的词汇表,并跳过 adapt 步骤。

我们现在可以在此多热编码的基础上训练一个简单的线性模型。我们将定义两个函数:preprocess,它将原始输入数据转换为我们想要用于模型的表示形式;以及 forward_pass,它应用可训练层。

def preprocess(x):
  return text_vectorizer(x)

def forward_pass(x):
  return tf.keras.layers.Dense(1)(x)  # Linear model

inputs = tf.keras.Input(shape=(1,), dtype='string')
outputs = forward_pass(preprocess(inputs))
model = tf.keras.Model(inputs, outputs)
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True))
model.fit(train_ds, epochs=5)

这就是端到端训练示例的全部内容,这已经足够达到 85% 的准确率。您可以在本文末尾找到此示例的完整代码。

让我们尝试一个新特征。我们的多热编码不包含任何有关评论长度的概念,因此我们可以尝试添加一个用于规范化字符串长度的特征。预处理层可以与 TensorFlow 操作和自定义层混合使用,按需组合。在这里,我们可以将 tf.strings.length 函数与Normalization 层结合起来,该层将使输入的均值为 0 且方差为 1。我们只更新了到下面 preprocess 函数的代码,但为了清晰起见,我们将展示训练的其余部分。

# This layer will scale our review length feature to mean 0 variance 1.
normalizer = tf.keras.layers.Normalization(axis=None)
normalizer.adapt(features.map(lambda x: tf.strings.length(x)))

def preprocess(x):
  multi_hot_terms = text_vectorizer(x)
  normalized_length = normalizer(tf.strings.length(x))
  # Combine the multi-hot encoding with review length.
  return tf.keras.layers.concatenate((multi_hot_terms, normalized_length))

def forward_pass(x):
  return tf.keras.layers.Dense(1)(x)  # Linear model.

inputs = tf.keras.Input(shape=(1,), dtype='string')
outputs = forward_pass(preprocess(inputs))
model = tf.keras.Model(inputs, outputs)
model.compile(loss=tf.keras.losses.BinaryCrossentropy(from_logits=True))
model.fit(train_ds, epochs=5)

在上面,我们创建了规范化层并将其适配到我们的输入。在 preprocess 函数中,我们只需将我们的多热编码和长度特征连接在一起。我们在两个特征表示的并集上学习一个线性模型。

我们可以进行的最后一个更改是加速训练。我们有一个主要的机会来提高我们的训练吞吐量。现在,在每个训练步骤中,我们在 CPU 上花费一些时间执行字符串操作(这些操作不能在加速器上运行),然后在 GPU 上计算损失函数和梯度。

With all computation in a single model, we will first preprocess each batch on the CPU and then update parameter weights on the GPU. This leaves gaps in our GPU usage.
在单个模型中进行所有计算时,我们首先在 CPU 上预处理每个批次,然后更新 GPU 上的参数权重。这会导致我们的 GPU 使用率出现间隙。

这种加速器使用率的差距是完全没有必要的!预处理与模型的实际前向传播是不同的。预处理不使用任何正在训练的参数。它是一个静态的变换,我们可以预先计算。

为了加快速度,我们希望预取我们预处理的批次,这样每次我们在一个批次上进行训练时,我们都在预处理下一个批次。这很容易使用tf.data 库来实现,该库专门为这种用途而构建。我们需要做的唯一重大更改是将我们单一的 keras.Model 拆分为两个:一个用于预处理,另一个用于训练。这很容易使用 Keras 的函数式 API 来实现。

inputs = tf.keras.Input(shape=(1,), dtype="string")
preprocessed_inputs = preprocess(inputs)
outputs = forward_pass(preprocessed_inputs)

# The first model will only apply preprocessing.
preprocessing_model = tf.keras.Model(inputs, preprocessed_inputs)
# The second model will only apply the forward pass.
training_model = tf.keras.Model(preprocessed_inputs, outputs)
training_model.compile(
    loss=tf.keras.losses.BinaryCrossentropy(from_logits=True))

# Apply preprocessing asynchronously with tf.data.
# It is important to call prefetch and remember the AUTOTUNE options.
preprocessed_ds = train_ds.map(
    lambda x, y: (preprocessing_model(x), y),
    num_parallel_calls=tf.data.AUTOTUNE).prefetch(tf.data.AUTOTUNE)

# Now the GPU can focus on the training part of the model.
training_model.fit(preprocessed_ds, epochs=5)

在上面的示例中,我们通过 preprocessforward_pass 函数传递了一个 keras.Input,但我们在变换后的输入上定义了两个单独的模型。这将我们单个操作图切片为两个。另一个有效的选项是只创建一个训练模型,并在遍历数据集时直接调用 preprocess 函数。在这种情况下,keras.Input 需要反映预处理特征的类型和形状,而不是原始字符串。

使用 tf.data 预取批次将我们的训练步骤时间缩短了 30% 以上!我们现在的计算时间看起来更像是这样。

With tf.data, we are now precomputing each preprocessed batch before the GPU needs it. This significantly speeds up training.
使用 tf.data,我们现在在 GPU 需要之前预先计算每个预处理批次。这大大加快了训练速度。

我们甚至可以更进一步,使用 tf.data 将我们预处理的数据集缓存在内存或磁盘中。我们只需在调用 prefetch 之前直接添加一个 .cache() 调用。通过这种方式,我们可以完全跳过在训练的第一个 epoch 之后计算预处理批次。

在训练之后,我们可以将拆分的模型在推理期间重新组合成一个单一模型。这使我们能够保存一个能够直接处理原始输入数据的模型。

inputs = preprocessing_model.input
outputs = training_model(preprocessing_model(inputs))
inference_model = tf.keras.Model(inputs, outputs)
inference_model.predict(
    tf.constant(["Terrible, no good, trash.", "I loved this movie!"]))

Keras 预处理层旨在提供一种灵活而富有表现力的方式来构建数据预处理管道。预建层可以与自定义层和其他 tensorflow 函数混合搭配使用。预处理可以与训练分离,并使用 tf.data 高效地应用,并在推理期间重新组合。我们希望它们能够在您的模型中更自然、更有效地迭代特征表示。

要在 Colab 中尝试本文中的代码,您可以遵循此链接。要查看可以使用预处理层完成的各种任务,请查看我们的预处理指南中的快速配方部分。您还可以查看我们关于基本文本分类图像数据增强结构化数据分类的完整教程。

下一篇文章
An Introduction to Keras Preprocessing Layers

作者:Matthew Watson,Keras 开发者 确定数据正确的特征表示可能是构建模型最棘手的部分之一。想象一下,您正在处理诸如颜色名称之类的分类输入特征。您可以对特征进行独热编码,以便每个颜色在特定索引处获得一个 1('red' = [0, 0, 1, 0, 0]),或者您可以嵌入特征,以便每个颜色映射到一个独特的可训练向量('red' = [0.1, 0.2, 0.5, -0.2])。更大的类别空间可能更适合嵌入,而较小的空间则更适合独热编码,但答案并不明确。这需要您在特定数据集上进行实验。