深度学习处理文本(12)
接下来,我们准备两个单独的TextVectorization层:一个用于英语,一个用于西班牙语。我们需要自定义字符串的预处理方式。我们需要保留插入的词元"[start]“和”[end]"。默认情况下,字符[和]将被删除,但我们希望保留它们,以便区分单词“start”与开始词元"[start]"。不同语言的标点符号是不同的。在西班牙语的TextVectorization层中,如果要删除标点符号,也需要删除字符¿。请注意,对于真实的翻译模型,我们会将标点符号作为单独的词元,而不会将其删除,因为我们希望能够生成带有正确标点符号的句子。在这个示例中,为简单起见,我们会去掉所有的标点符号,如代码清单11-26所示。
代码清单11-26 将英语和西班牙语的文本对向量化
import tensorflow as tf
import string
import re
strip_chars = string.punctuation + "¿" ←---- (本行及以下6行)为西班牙语的TextVectorization层准备一个自定义的字符串标准化函数:保留[和],但去掉¿(同时去掉string.punctuation中的其他所有字符)
strip_chars = strip_chars.replace("[", "")
strip_chars = strip_chars.replace("]", "")
def custom_standardization(input_string):
lowercase = tf.strings.lower(input_string)
return tf.strings.regex_replace(
lowercase, f"[{re.escape(strip_chars)}]", "")
vocab_size = 15000 ←---- (本行及以下1行)为简单起见,只查看每种语言前15 000个最常见的单词,并将句子长度限制为20个单词
sequence_length = 20
source_vectorization = layers.TextVectorization( ←----英语层
max_tokens=vocab_size,
output_mode="int",
output_sequence_length=sequence_length,
)
target_vectorization = layers.TextVectorization( ←----西班牙语层
max_tokens=vocab_size,
output_mode="int",
output_sequence_length=sequence_length + 1, ←----生成的西班牙语句子多了一个词元,因为在训练过程中需要将句子偏移一个时间步
standardize=custom_standardization,
)
train_english_texts = [pair[0] for pair in train_pairs]
train_spanish_texts = [pair[1] for pair in train_pairs]
source_vectorization.adapt(train_english_texts) ←---- (本行及以下1行)学习每种语言的词表
target_vectorization.adapt(train_spanish_texts)
最后,我们可以将数据转换为tf.data管道,如代码清单11-27所示。我们希望它能够返回一个元组(inputs, target),其中inputs是一个字典,包含两个键,分别是“编码器输入”(英语句子)和“解码器输入”(西班牙语句子),target则是向后偏移一个时间步的西班牙语句子。
代码清单11-27 准备翻译任务的数据集
batch_size = 64
def format_dataset(eng, spa):
eng = source_vectorization(eng)
spa = target_vectorization(spa)
return ({
"english": eng,
"spanish": spa[:, :-1], ←----输入西班牙语句子不包含最后一个词元,以保证输入和目标具有相同的长度
}, spa[:, 1:]) ←----目标西班牙语句子向后偏移一个时间步。二者长度相同,都是20个单词
def make_dataset(pairs):
eng_texts, spa_texts = zip(*pairs)
eng_texts = list(eng_texts)
spa_texts = list(spa_texts)
dataset = tf.data.Dataset.from_tensor_slices((eng_texts, spa_texts))
dataset = dataset.batch(batch_size)
dataset = dataset.map(format_dataset, num_parallel_calls=4)
return dataset.shuffle(2048).prefetch(16).cache() ←----利用内存缓存来加快预处理速度
train_ds = make_dataset(train_pairs)
val_ds = make_dataset(val_pairs)
这个数据集如下所示。
>>> for inputs, targets in train_ds.take(1):
>>> print(f"inputs['english'].shape: {inputs['english'].shape}")
>>> print(f"inputs['spanish'].shape: {inputs['spanish'].shape}")
>>> print(f"targets.shape: {targets.shape}")
inputs["english"].shape: (64, 20)
inputs["spanish"].shape: (64, 20)
targets.shape: (64, 20)
数据已准备就绪,我们可以开始构建模型了。我们首先构建一个序列到序列的循环模型,然后再继续构建Transformer。
RNN的序列到序列学习
2015年~2017年,RNN主宰了序列到序列学习,不过随后被Transformer超越。RNN是现实世界中的许多机器翻译系统的基础。2017年前后的谷歌翻译模型由7个大型LSTM层堆叠而成。这种方法在今天仍然值得一学,因为它为理解序列到序列模型提供了一个简单的切入点。使用RNN将一个序列转换为另一个序列,最简单的方法是在每个时间步都保存RNN的输出。这在Keras中的实现如下所示。
inputs = keras.Input(shape=(sequence_length,), dtype="int64")
x = layers.Embedding(input_dim=vocab_size, output_dim=128)(inputs)
x = layers.LSTM(32, return_sequences=True)(x)
outputs = layers.Dense(vocab_size, activation="softmax")(x)
model = keras.Model(inputs, outputs)
但是,这种方法有两个主要问题。目标序列必须始终与源序列的长度相同。在实践中,这种情况很少见。从技术上来说,这一点并不重要,因为可以对源序列或目标序列进行填充,使二者长度相同。由于RNN逐步处理的性质,模型将仅通过查看源序列第0~N个词元来预测目标序列的第N个词元。这种限制不适用于大多数任务,特别是翻译。比如将“The weather is nice today”(今天天气不错)翻译成法语,应该是“Il fait beau aujourd’hui”。模型需要能够仅从“The”预测出“Il”,仅从“The weather”预测出“Il fait”,以此类推,这根本不可能。如果你是一名译员,你会先阅读整个源句子,然后再开始翻译。如果你要处理的两种语言具有非常不同的词序,比如英语和日语,那么这一点就尤为重要。这正是标准的序列到序列模型所做的。
在一个正确的序列到序列模型中(如图11-13所示),首先使用一个RNN(编码器)将整个源序列转换为单一向量(或向量集)。它既可以是RNN的最后一个输出,也可以是最终的内部状态向量。然后,使用这个向量(或向量集)作为另一个RNN(解码器)的初始状态。解码器会查看目标序列的第0~N个元素,并尝试预测目标序列中的第N+1个时间步