TensorFlow 实战(五)(1)https://developer.aliyun.com/article/1522830
11.2.1 文本矢量化层
文本矢量化层接受一个字符串,对其进行标记化,并通过词汇表(或字典)查找将标记转换为 ID。它以字符串列表(或字符串数组)作为输入,其中每个字符串可以是单词/短语/句子(等等)。然后它从语料库中学习词汇。最后,该层可以用于将字符串列表转换为包含该列表中每个字符串的标记 ID 序列的张量。让我们看看这个层的作用。首先,导入该层:
from tensorflow.keras.layers.experimental.preprocessing import ➥ TextVectorization
然后,按以下方式定义该层。在这里,我们为英语定义了该层。请记住,我们的模型中需要两个 TextVectorization 层,一个用于英语,一个用于德语:
en_vectorize_layer = TextVectorization( max_tokens=en_vocab, output_mode='int', output_sequence_length=None )
值得停下来看看我们提供的不同参数:
- max_tokens—指定词汇表中的单词数。如果词汇表中没有某个单词(即,超出词汇表的单词),则将其转换为[UNK]。
- output_mode—指定最终输出的类型。可以是"int"、“binary”、"count"和"tf-idf"之一。"int"表示层将为每个标记输出一个标记 ID。"binary"意味着输出将是一个[<批量大小>, <词汇大小>]张量,在这个例子中,如果该标记所指示的索引在这个示例中存在,则给定值为 1。"count"给出了与"binary"类似的输出,但其中包含了该示例中标记出现的次数。"tf-id"给出了与"binary"类似的输出,但每个位置处的 TF-IDF 值。
- output_sequence_length—指定转换为标记 ID 后的批量输入序列的长度。如果设置为 None,则意味着序列长度将设置为批量中最长序列的长度。较短的序列将使用特殊标记(特殊标记默认为"")进行填充。
要充分利用这个层,我们必须在文本语料库上适应它,以便它可以学习词汇表。调用 adapt()函数并向其传递字符串列表(或字符串数组)可以实现这一目的。换句话说,adapt()产生了与 scikit-learn 模型的 fit()方法相同的结果(mng.bz/aJmB
)。它接受一些数据并根据数据训练(或适应)模型。对于标记器来说,除其他外,它构建了一个词典(从单词到 ID 的映射):
en_vectorize_layer.adapt(np.array(train_df["EN"].tolist()).astype('str'))
安装该层后,可以获得词汇表
print(en_vectorize_layer.get_vocabulary()[:10])
which prints
['', '[UNK]', 'tom', 'to', 'you', 'the', 'i', 'a', 'is', 'that']
换句话说,词汇表是一个标记列表,其中 ID 对应于它们在列表中的索引。你可以通过计算词汇表的大小来得到:
print(len(en_vectorize_layer.get_vocabulary()))
which returns
2238
接下来,要使用这个层将字符串转换为数字 ID,我们必须将其封装在一个模型中。为此,首先让我们定义一个 Keras 顺序模型。让我们将模型命名为 toy_model,因为这只用于学习文本向量化器的行为:
toy_model = tf.keras.models.Sequential()
定义一个输入层,将其大小设置为接受单列张量(即,一个字符串列表),并将数据类型设置为 tf.string:
toy_model.add(tf.keras.Input(shape=(1,), dtype=tf.string))
然后添加我们定义的文本向量化层:
toy_model.add(en_vectorize_layer)
你可以像使用其他 Keras 模型一样使用它,并将任意文本转换为数字 ID 序列。具体地,你可以在一些输入数据上使用 model.predict()函数,该函数接受输入并根据模型中使用的层进行相应转换:
input_data = [["run"], ["how are you"],["ectoplasmic residue"]] pred = toy_model.predict(input_data)
最后,按以下方式打印输入和结果
print("Input data: \n{}\n".format(input_data)) print("\nToken IDs: \n{}".format(pred))
which gives
Input data: [['run'], ['how are you'], ['ectoplasmic residue']] Token IDs: [[427 0 0] [ 40 23 4] [ 1 1 0]]
该层按照一切都进行。 首先让我们看一下输出的形状。 由于我们设置了 output_sequence_length=None,所以将所有输入示例填充到输入中最长输入的长度。 在这里,“how are you”是最长的,其中有三个单词。 因此,所有行都用零填充,以便每个示例都有三列。 通常,该层返回一个大小为 [, sequence_length] 的输出。
如果单词在词汇表中找到,它将转换为某个数字(例如,“run”转换为 427)。 如果单词未在词汇表中找到(例如,“ectoplasmic”),则会用表示词汇表外单词的特殊 ID(1)替换。
11.2.2 为 seq2seq 模型定义 TextVectorization 层
通过对 TextVectorization 层有很好的理解,让我们定义一个函数,返回一个包装在 Keras Model 对象中的文本向量化层。 此函数名为 get_vectorizer(),接受以下参数:
- 语料库—接受字符串列表(或数组)(即要构建词汇表的语料库)。
- n_vocab—词汇量大小。 保留最常见的 n_vocab 个词以构建词汇表。
- max_length(可选)—结果标记序列的长度。 默认为 None,此时序列长度将为最长文本序列的长度。
- return_vocabulary(可选)—是否返回词汇表(即字符串标记列表)。
- name(可选)—用于设置模型名称的字符串
它定义了一个接受字符串批次(总共形状为 [None, 1])的输入层。 接下来,函数定义了一个文本向量化层。 请注意,该层的词汇量为 n_vocab + 2。额外的 2 是为了容纳特殊标记" “和”[UNK]"。 该层适应了传递给函数的文本语料库。 最后,我们使用输入层(inp)和文本向量化层的输出(vectorize_out)定义了一个 Keras 模型。 如果 return_vocabulary 设置为 True,则还会返回 vectorize_layer 的词汇表,如下一列表所示。
列表 11.3 为编码器-解码器模型定义文本向量化器
def get_vectorizer( corpus, n_vocab, max_length=None, return_vocabulary=True, name=None ): """ Return a text vectorization layer or a model """ inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='encoder_input')❶ vectorize_layer = ➥ tf.keras.layers.experimental.preprocessing.TextVectorization( max_tokens=n_vocab+2, ❷ output_mode='int', output_sequence_length=max_length, ) vectorize_layer.adapt(corpus) ❸ vectorized_out = vectorize_layer(inp) ❹ if not return_vocabulary: return tf.keras.models.Model( inputs=inp, outputs=vectorized_out, name=name ) ❺ else: return tf.keras.models.Model( inputs=inp, outputs=vectorized_out, name=name ❻ ), vectorize_layer.get_vocabulary()
❶ 定义一个接受字符串列表(或字符串数组)的输入层。
❷ 在定义词汇量大小时,我们使用 n_vocab + 2,因为自动添加了两个特殊标记“(填充)”和“[UNK]”。
❸ 在数据上拟合向量化层。
❹ 获取输入数据的标记 ID。
❺ 仅返回模型。 该模型接受字符串数组并输出标记 ID 的张量。
❻ 除了模型外,返回词汇表。
既然我们已经定义了该函数,让我们使用它并定义两个向量化器,一个用于英文输入,一个用于德文输入:
# Get the English vectorizer/vocabulary en_vectorizer, en_vocabulary = get_vectorizer( corpus=np.array(train_df[“EN”].tolist()), n_vocab=en_vocab, max_length=en_seq_length, name=’en_vectorizer’ ) # Get the German vectorizer/vocabulary de_vectorizer, de_vocabulary = get_vectorizer( corpus=np.array(train_df[“DE”].tolist()), n_vocab=de_vocab, max_length=de_seq_length-1, name=’de_vectorizer’ )
在这里,语料库接受一个文本列表或数组。每个文本都是一个包含英语或德语短语/句子的字符串。n_vocab 定义词汇表的大小,max_length 定义了我们应该对数据进行填充的序列长度。请注意,我们在解码器中使用 de_seq_length-1。这里减 1 的操作是由于在模型训练期间数据呈现给解码器的方式所决定的。当我们到达模型训练时,我们将讨论具体细节。最后,我们可以定义一个名称来跟踪不同的层。
11.2.3 定义编码器
接下来我们来到编码器,我们将在编码器的核心使用一个 GRU 模型。编码器负责处理源输入序列。它的责任是处理源输入并生成一个上下文向量(有时称为思考向量)。该向量以紧凑的、向量化的形式捕获输入序列的本质。通常情况下,这个上下文向量将是 GRU 单元在处理完整输入序列后的最后输出状态。
让我们看看准备编码器所涉及的步骤。为此,我们将使用 Keras Functional 层。序列到序列模型不是顺序的,并且在编码器和解码器之间涉及非线性连接。因此,我们不能使用 Keras Sequential API。首先,我们定义输入层:
# The input is (None,1) shaped and accepts an array of strings inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='e_input')
输入接受一个字符串列表。请注意,我们将形状设置为(1,)以确保模型接受具有一个列的张量,并将 dtype 设置为 tf.string。接下来,我们将由 inp 前向传递的文本输入进行向量化。
# Vectorize the data (assign token IDs) vectorized_out = en_vectorizer(inp)
这里,向量化器是由我们之前定义的 get_vectorizer()函数输出的执行文本向量化的模型。
接下来,我们定义一个嵌入层,它将由向量化器返回的标记 ID 转换为单词向量。这是一个具有可训练权重的层。因此,在训练期间,模型将调整单词嵌入以反映解决手头任务的有用表示:
# Define an embedding layer to convert IDs to word vectors emb_layer = tf.keras.layers.Embedding( input_dim=n_vocab+2, output_dim=128, mask_zero=True, name=’e_embedding’ ) # Get the embeddings of the token IDs emb_out = emb_layer(vectorized_out)
在定义嵌入层时,您需要始终传递词汇表大小(input_dim)和 output_dim。请注意,词汇表大小已经增加了 2 以容纳引入的两个特殊标记(即 UNK 和 PAD)。我们将 output_dim 设置为 128。我们还希望屏蔽过多的零,因此设置 mask_zero=True。最后,我们还将传递一个名称以便于识别该层。
现在我们要来到我们模型的核心:循环神经网络(RNN)。正如前面提到的,我们将使用一个 GRU 模型,但是带有一个额外的特点!我们将使我们的 GRU 模型成为双向的!双向 RNN 是一种特殊类型的 RNN,它同时处理序列的前向和后向。这与标准 RNN 相反,后者只处理序列的前向:
gru_layer = tf.keras.layers.Bidirectional(tf.keras.layers.GRU(128))
双向 RNN:前向和后向阅读文本
标准的循环神经网络逐步阅读文本,一次处理一个时间步,然后输出一系列输出。双向循环神经网络正如其名称所示,不仅向前阅读文本,而且向后阅读文本。这意味着双向循环神经网络有两个输出序列。然后,这两个序列使用组合策略(例如连接)进行组合,以产生最终输出。通常双向循环神经网络的性能优于标准循环神经网络,因为它们可以理解文本前后的关系,如下图所示。
标准 RNN 和双向 RNN 之间的比较
阅读文本倒置为什么有帮助呢?有一些语言是倒序阅读的(例如阿拉伯语、希伯来语)。除非文本经过特殊处理以考虑这种书写风格,否则标准的循环神经网络将很难理解这种语言。通过使用双向循环神经网络,您可以消除模型对语言始终从左到右或从右到左的依赖。
如果考虑英语,可能存在只从前面推断关系是不可能的情况。考虑以下两个句子
约翰走向了克拉伦斯街上的银行。
约翰朝河边的银行走去。
由于这两个句子在“bank”一词之前是相同的,因此在阅读其余部分之前,不可能知道“bank”是指金融机构还是河岸。对于双向循环神经网络来说,这是微不足道的。
然后我们获得 gru_layer 的输出,并将其分配给 gru_out:
gru_out = gru_layer(emb_out)
最后,我们将编码器模型定义为 tf.keras.models.Model 对象。它接受 inp(即 tf.string 类型的单列张量)并输出 gru_out(即双向 GRU 模型的最终状态)。这个 GRU 模型的最终状态被认为是上下文向量,为解码器提供有关源语言句子/短语输入的信息:
encoder = tf.keras.models.Model(inputs=inp, outputs=gru_out)
可以观察在以下清单中显示的逐步构建编码器模型的函数封装方式。
清单 11.4 返回编码器的函数
def get_encoder(n_vocab, vectorizer): """ Define the encoder of the seq2seq model""" inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='e_input') ❶ vectorized_out = vectorizer(inp) ❷ emb_layer = tf.keras.layers.Embedding( n_vocab+2, 128, mask_zero=True, name='e_embedding' ❸ ) emb_out = emb_layer(vectorized_out) ❹ gru_layer = tf.keras.layers.Bidirectional( tf.keras.layers.GRU(128, name='e_gru'), ❺ name='e_bidirectional_gru' ) gru_out = gru_layer(emb_out) ❻ encoder = tf.keras.models.Model( inputs=inp, outputs=gru_out, name='encoder' ) ❼ return encoder
❶ 输入形状为(None,1),接受一个字符串数组。
❷ 对数据进行向量化(分配令牌 ID)
❸ 定义一个嵌入层,将 ID 转换为词向量。
❹ 获取令牌 ID 的嵌入。
❺ 定义一个双向 GRU 层。编码器同时查看英文文本(即输入)的前向和后向。
❻ 获取 gru 的最后一个输出(模型返回的最后一个输出状态向量)。
❼ 定义编码器模型;它接受一个字符串列表/数组并返回 GRU 模型的最后输出状态。
定义函数后,您可以简单地调用它来构建编码器模型:
encoder = get_encoder(en_vocab, en_vectorizer)
11.2.4 定义解码器和最终模型
编码器已经完成,现在是看看解码器的时候了。解码器看起来会比编码器稍微复杂一些。解码器的核心模型再次是一个 GRU 模型。然后是一个完全连接的隐藏层和一个完全连接的预测层。预测层对每个时间步输出来自德语词汇表的一个单词(通过计算整个词汇表上的概率)。
在模型训练期间,解码器预测给定目标序列中的下一个单词。例如,给定目标序列 [A, B, C, D],解码器将根据以下输入-输出元组对模型进行三个时间步的训练:(A, B),(B, C) 和 (C, D)。换句话说,给定标记 A,预测标记 B;给定标记 B,预测标记 C;依此类推。如果你考虑编码器-解码器模型的端到端过程,将会发生以下步骤:
- 编码器处理源输入序列(即英文)并生成上下文向量(即 GRU 模型的最后输出状态)。
- 解码器使用编码器产生的上下文向量作为其循环组件的初始状态。
- 解码器接收目标输入序列(即德语)并根据前一个标记预测下一个标记。对于每个时间步,它使用完全连接的层和 softmax 层在完整的目标词汇表上预测一个标记。
这种模型训练的方式被称为教师强制,因为你正在用目标序列(即老师)引导解码器。与在编码器-解码器类型模型的训练中不使用教师强制相比,使用教师强制很快会导致更好的性能。让我们深入了解解码器,并看看编码器和解码器如何相互联系以创建更深入的最终模型,如图 11.3 所示。
图 11.3 最终的序列到序列模型的实现,重点关注各种层和输出
现在是讨论构建解码器和最终模型的具体细节的时候了。我们首先必须做的事情是通过传递一个输入来获得编码器的输出。我们定义了一个与编码器的输入相同的输入层,并将其传递给我们之前定义的编码器模型:
e_inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='e_input_final')
然后我们将 e_inp 传递给编码器模型,它将给出 GRU 模型的最后输出状态作为输出。这是解码器的重要输入:
d_init_state = encoder(e_inp)
作为解码器的起点,我们定义了一个具有与编码器输入相同规格的输入层:
d_inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='d_input')
然后我们将输入传递给一个文本向量化模型,给定 get_vectorizer() 函数:
vectorized_out = de_vectorizer(inp)
我们像对编码器一样定义一个嵌入层,以便文本向量化层生成的标记 ID 转换为单词向量。请注意,我们为编码器和解码器分别定义了两个单独的嵌入层,因为它们使用来自两种不同语言的序列:
emb_layer = tf.keras.layers.Embedding( input_dim=n_vocab+2, output_dim=128, mask_zero=True, name='d_embedding' ) emb_out = emb_layer(vectorized_out)
现在是时候实现解码器的循环组件了。与编码器类似,我们使用 GRU 模型来处理序列:
gru_layer = tf.keras.layers.GRU(256, return_sequences=True)
但请注意,与编码器相比,在解码器中,我们没有在 GRU 模型上使用双向包装器。解码器不能依赖于向后阅读能力,因为它应该仅根据前一个和当前输入生成下一个输出。还要注意,我们设置了 return_sequences=True:
gru_out = gru_layer(emb_out, initial_state=d_init_state)
最后,将嵌入层的输出传递给 gru_layer,我们得到输出。之前我们声明过 d_init_state(即编码器的输出)是解码器的重要输入之一。在这里,我们将 d_init_state 作为初始状态传递给解码器的 GRU 层。这意味着,解码器不是从零初始化状态向量开始,而是使用编码器的上下文向量作为初始状态。由于我们设置了 return_sequences=True,因此输出将包含所有时间步的输出状态向量,而不仅仅是最后一个时间步。这意味着输出的大小为[<批大小>,<时间步长>,256]。
清单 11.5 定义解码器和最终模型
def get_final_seq2seq_model(n_vocab, encoder, vectorizer): """ Define the final encoder-decoder model """ e_inp = tf.keras.Input( shape=(1,), dtype=tf.string, name='e_input_final' ) ❶ d_init_state = encoder(e_inp) ❶ d_inp = tf.keras.Input(shape=(1,), dtype=tf.string, name='d_input') ❷ d_vectorized_out = vectorizer(d_inp) ❸ d_emb_layer = tf.keras.layers.Embedding( n_vocab+2, 128, mask_zero=True, name='d_embedding' ❹ ) d_emb_out = d_emb_layer(d_vectorized_out) d_gru_layer = tf.keras.layers.GRU( 256, return_sequences=True, name='d_gru' ) ❺ d_gru_out = d_gru_layer(d_emb_out, initial_state=d_init_state) ❻ d_dense_layer_1 = tf.keras.layers.Dense( 512, activation='relu', name='d_dense_1' ) ❼ d_dense1_out = d_dense_layer_1(d_gru_out) ❼ d_dense_layer_final = tf.keras.layers.Dense( n_vocab+2, activation='softmax', name='d_dense_final' ❽ ) d_final_out = d_dense_layer_final(d_dense1_out) ❽ seq2seq = tf.keras.models.Model( inputs=[e_inp, d_inp], outputs=d_final_out, name='final_seq2seq' ❾ ) return seq2seq
❶定义编码器输入层并获取编码器输出(即上下文向量)。
❷输入的形状为(None,1),并接受字符串数组作为输入。我们将德语序列作为输入并要求模型使用偏移一个(即下一个)单词的单词预测它。
❸获取解码器的向量化输出。
❹定义嵌入层,将 ID 转换为单词向量。这是与编码器嵌入层不同的层。
❺定义 GRU 层。与编码器不同,我们不能为解码器定义双向 GRU。
❻获取解码器的 GRU 层输出。
❼定义中间的 Dense 层并获取输出。
❽最终预测层使用 softmax
❾定义完整模型。
我们现在可以定义所需的所有内容,以制定我们的最终模型
# Get the English vectorizer/vocabulary en_vectorizer, en_vocabulary = get_vectorizer( corpus=np.array(train_df["EN"].tolist()), n_vocab=en_vocab, max_length=en_seq_length, name='e_vectorizer' ) # Get the German vectorizer/vocabulary de_vectorizer, de_vocabulary = get_vectorizer( corpus=np.array(train_df["DE"].tolist()), n_vocab=de_vocab, max_length=de_seq_length-1, name='d_vectorizer' ) # Define the final model encoder = get_encoder(n_vocab=en_vocab, vectorizer=en_vectorizer) final_model = get_final_seq2seq_model( n_vocab=de_vocab, encoder=encoder, vectorizer=de_vectorizer )
在这里,我们定义英语和德语的向量器(分别为 en_vectorizer 和 de_vectorizer)。然后使用英语词汇量和英语向量器定义编码器。最后,使用德语词汇量(de_vocab)编码器和德语向量器(de_vectorizer)定义最终的编码器-解码器模型。
TensorFlow 实战(五)(3)https://developer.aliyun.com/article/1522832