Keras预处理层的介绍

72 阅读7分钟

发布者: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 tfimport tensorflow_datasets as tfdstrain_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] ,其中电影(第一个词汇表术语)和坏(最后一个词汇表术语)都存在。

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 层,并做了两件事来设置该层的状态。首先,我们映射我们的训练数据集,丢弃表示正面或负面评论的整数标签。这给了我们一个只包含评论文本的数据集。接下来,我们在这个数据集上适应()该层,这使得该层学习所有文档中最频繁的词汇,上限为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 modelinputs = 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 ops和自定义层混合。在这里,我们可以将tf.strings.length 函数与归一化层结合起来,这将使输入的平均值为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() 。这样,我们就可以在训练的第一个历时之后完全跳过计算预处理批次。

训练结束后,我们可以在推理过程中把我们的分裂模型重新连接成一个单一的模型。这使我们能够保存一个能够直接处理原始输入数据的模型。

inputs = preprocessing_model.inputoutputs = 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中玩转这篇文章中的代码,你可以点击这个链接。要看你可以用预处理层做的各种任务,请看我们的预处理指南的快速食谱部分。你还可以查看我们的基本文本分类图像数据增量结构化数据分类的完整教程。