7.2.2 下载并准备数据集
本实例我们将使用http://www.manythings.org/anki/提供的语言数据集,该数据集包含如下格式的语言翻译对:
May I borrow this book? ¿Puedo tomar prestado este libro?
在这个数据集中有多种语言可用,在本实例中将只使用“英语-西班牙”语数据集。
为方便起见,在 Google Cloud上托管了语言数据集的副本,大家也可以下载自己的副本。下载数据集后,将按照以下步骤准备数据:
- 为每个句子添加一个开始和结束标记。
- 通过删除特殊字符来清理句子。
- 创建单词索引和反向单词索引(从单词 → id 和 id → 单词映射的字典)。
- 将每个句子填充到最大长度。
(1)编写如下代码下载数据集:
import pathlib
path_to_zip = tf.keras.utils.get_file(
'spa-eng.zip', origin='http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip',
extract=True)
path_to_file = pathlib.Path(path_to_zip).parent/'spa-eng/spa.txt'
执行后会显示下载过程:
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip
2646016/2638744 [==============================] - 0s 0us/step
2654208/2638744 [==============================] - 0s 0us/step
(2)通过如下代码加载数据集中的数据:
def load_data(path):
text = path.read_text(encoding='utf-8')
lines = text.splitlines()
pairs = [line.split('\t') for line in lines]
inp = [inp for targ, inp in pairs]
targ = [targ for targ, inp in pairs]
return targ, inp
targ, inp = load_data(path_to_file)
print(inp[-1])
print(targ[-1])
(3)创建tf.data数据集
从这些字符串数组中,可以创建一个tf.data.Dataset字符串来有效地对它们进行混洗和批处理操作。代码如下:
BUFFER_SIZE = len(inp)
BATCH_SIZE = 64
dataset = tf.data.Dataset.from_tensor_slices((inp, targ)).shuffle(BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE)
for example_input_batch, example_target_batch in dataset.take(1):
print(example_input_batch[:5])
print()
print(example_target_batch[:5])
break
执行后会输出:
tf.Tensor(
[b'La temperatura descendi\xc3\xb3 a cinco grados bajo cero.'
b'Tom dijo que \xc3\xa9l nunca dejar\xc3\xada a su esposa.'
b'\xc2\xbfEsto es legal?' b'Tom se cay\xc3\xb3.'
b'Se est\xc3\xa1 haciendo tarde, as\xc3\xad que es mejor que nos vayamos.'], shape=(5,), dtype=string)
tf.Tensor(
[b'The temperature fell to five degrees below zero.'
b"Tom said he'd never leave his wife." b'Is this legal?'
b'Tom fell down.' b"It's getting late, so we'd better get going."], shape=(5,), dtype=string)
7.2.3 文本预处理
本实例的目标之一是,通过数据集构建一个可以导出为tf.saved_model格式的模型。为了使被导出的模型有用,这个模型应该接受tf.string类型的输入,并重新运行tf.string输出。并且所有文本处理工作都在模型内部进行。
(1)标准化处理
因为我们创建的模型需要处理词汇量有限的多语言文本,所以对输入文本进行标准化非常重要。我们首先实现Unicode 规范化处理以拆分重音字符,并将兼容字符替换为和其ASCII等效的字符。该tensroflow_text包包含一个 unicode 规范化操作:
example_text = tf.constant('¿Todavía está en casa?')
print(example_text.numpy())
print(tf_text.normalize_utf8(example_text, 'NFKD').numpy())
执行后会输出:
b'\xc2\xbfTodav\xc3\xada est\xc3\xa1 en casa?'
b'\xc2\xbfTodavi\xcc\x81a esta\xcc\x81 en casa?'
编写如下代码实现Unicode 标准化操作,这是实现文本标准化功能的第一步。
def tf_lower_and_split_punct(text):
# 拆分重音字符
text = tf_text.normalize_utf8(text, 'NFKD')
text = tf.strings.lower(text)
# 保留空格,从a到z,然后选择标点符号.
text = tf.strings.regex_replace(text, '[^ a-z.?!,¿]', '')
#在标点符号周围添加空格
text = tf.strings.regex_replace(text, '[.?!,¿]', r' \0 ')
#删除空白
text = tf.strings.strip(text)
text = tf.strings.join(['[START]', text, '[END]'], separator=' ')
return text
print(example_text.numpy().decode())
print(tf_lower_and_split_punct(example_text).numpy().decode())
执行后会输出:
¿Todavía está en casa?
[START] ¿ todavia esta en casa ? [END]
(2)文本矢量化处理
本实例的标准化功能将被包裹在preprocessing.TextVectorization层中,该层将实现词汇提取和输入文本到标记序列的转换功能。
max_vocab_size = 5000
input_text_processor = preprocessing.TextVectorization(
standardize=tf_lower_and_split_punct,
max_tokens=max_vocab_size)
该TextVectorization层和许多其他的experimental.preprocessing层都有一个adapt()方法。此方法读取训练数据的一个时期,其工作方式与Model.fix()相似。这个adapt()方法会根据数据初始化图层。通过如下代码设置了词汇表的内容:
input_text_processor.adapt(inp)
#以下是词汇表中的前10个单词:
input_text_processor.get_vocabulary()[:10]
执行后会输出:
['', '[UNK]', '[START]', '[END]', '.', 'que', 'de', 'el', 'a', 'no']
上述输出是西班牙语TextVectorization层,接下来构建adapt()英语层:
output_text_processor = preprocessing.TextVectorization(
standardize=tf_lower_and_split_punct,
max_tokens=max_vocab_size)
output_text_processor.adapt(targ)
output_text_processor.get_vocabulary()[:10]
执行后会输出:
['', '[UNK]', '[START]', '[END]', '.', 'the', 'i', 'to', 'you', 'tom']
现在这些层可以将一批字符串转换成一批令牌 ID:
example_tokens = input_text_processor(example_input_batch)
example_tokens[:3, :10]
执行后会输出:
<tf.Tensor: shape=(3, 10), dtype=int64, numpy=
array([[ 2, 11, 1593, 1, 8, 313, 2658, 353, 2800, 4],
[ 2, 10, 92, 5, 7, 82, 2677, 8, 25, 437],
[ 2, 13, 58, 15, 1, 12, 3, 0, 0, 0]])>
通过使用上述该函数get_vocabulary(),可以将令牌 ID 转换回文本:
input_vocab = np.array(input_text_processor.get_vocabulary())
tokens = input_vocab[example_tokens[0].numpy()]
' '.join(tokens)
执行后会输出:
'[START] la temperatura [UNK] a cinco grados bajo cero . [END] '
返回的Token ID 以零填充,这样可以很容易地变成一个Mask:
plt.subplot(1, 2, 1)
plt.pcolormesh(example_tokens)
plt.title('Token IDs')
plt.subplot(1, 2, 2)
plt.pcolormesh(example_tokens != 0)
plt.title('Mask')
执行后会输出下面的内容,并绘制如图7-4所示的可视化图。
Text(0.5, 1.0, 'Mask')
图7-4 可视化图
7.2.4 编码器模型
下图7-5展示了该模型的概述,在每个时间段内,解码器的输出与编码输入的加权和相结合,以预测下一个单词。
图7-5 模型概述图
在创建编码器模型之前,先为模型定义一些常量:
embedding_dim = 256
units = 1024
编写类Encoder实现编码器,对应上图7-5中的蓝色部分,具体运行流程如下:
- 获取令牌 ID 列表(来自input_text_processor)。
- 查找每个标记的嵌入向量(使用 a layers.Embedding)。
- 将嵌入处理为新序列(使用 a layers.GRU)。
- 分别返回:
- 处理后的序列:这将传递给注意力头。
- 内部状态:这将用于初始化解码器。
class Encoder(tf.keras.layers.Layer):
def __init__(self, input_vocab_size, embedding_dim, enc_units):
super(Encoder, self).__init__()
self.enc_units = enc_units
self.input_vocab_size = input_vocab_size
#嵌入层将令牌转换为向量
self.embedding = tf.keras.layers.Embedding(self.input_vocab_size,
embedding_dim)
#GRU RNN层按顺序处理这些向量.
self.gru = tf.keras.layers.GRU(self.enc_units,
# Return the sequence and state
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
def call(self, tokens, state=None):
shape_checker = ShapeChecker()
shape_checker(tokens, ('batch', 's'))
# 嵌入层查找每个令牌的嵌入
vectors = self.embedding(tokens)
shape_checker(vectors, ('batch', 's', 'embed_dim'))
# GRU处理嵌入序列。
#输出形状: (batch, s, enc_units)
#状态形状: (batch, enc_units)
output, state = self.gru(vectors, initial_state=state)
shape_checker(output, ('batch', 's', 'enc_units'))
shape_checker(state, ('batch', 'enc_units'))
# 返回新序列及其状态.
return output, state
#将输入文本转换为标记
example_tokens = input_text_processor(example_input_batch)
#对输入序列进行编码.
encoder = Encoder(input_text_processor.vocabulary_size(),
embedding_dim, units)
example_enc_output, example_enc_state = encoder(example_tokens)
print(f'Input batch, shape (batch): {example_input_batch.shape}')
print(f'Input batch tokens, shape (batch, s): {example_tokens.shape}')
print(f'Encoder output, shape (batch, s, units): {example_enc_output.shape}')
print(f'Encoder state, shape (batch, units): {example_enc_state.shape}')
编码器将返回其内部状态,以便其状态可用于初始化解码器。RNN 返回其状态,以便可以通过多次调用处理序列。
7.2.5 绘制可视化注意力图
本实例中的解码器使用注意力机制来选择性地关注输入序列的一部分,将一系列向量作为每个示例的输入,并为每个示例返回一个“注意力”向量。这个注意力层类似于 a,但是layers.GlobalAveragePoling1D注意力层执行加权平均。
(1)本实例使用Bahdanau注意力机制实现,TensorFlow包括 aslayers.Attention和 layers.AdditiveAttention。编写类BahdanauAttention,功能是处理一对layers.Dense层中的权重矩阵,并调用内置实现。
class BahdanauAttention(tf.keras.layers.Layer):
def __init__(self, units):
super().__init__()
# For Eqn. (4), the Bahdanau attention
self.W1 = tf.keras.layers.Dense(units, use_bias=False)
self.W2 = tf.keras.layers.Dense(units, use_bias=False)
self.attention = tf.keras.layers.AdditiveAttention()
def call(self, query, value, mask):
shape_checker = ShapeChecker()
shape_checker(query, ('batch', 't', 'query_units'))
shape_checker(value, ('batch', 's', 'value_units'))
shape_checker(mask, ('batch', 's'))
# 来自Eqn. (4), `W1@ht`.
w1_query = self.W1(query)
shape_checker(w1_query, ('batch', 't', 'attn_units'))
# 来自Eqn. (4), `W2@hs`.
w2_key = self.W2(value)
shape_checker(w2_key, ('batch', 's', 'attn_units'))
query_mask = tf.ones(tf.shape(query)[:-1], dtype=bool)
value_mask = mask
context_vector, attention_weights = self.attention(
inputs = [w1_query, value, w2_key],
mask=[query_mask, value_mask],
return_attention_scores = True,
)
shape_checker(context_vector, ('batch', 't', 'value_units'))
shape_checker(attention_weights, ('batch', 't', 's'))
return context_vector, attention_weights
(2)测试注意力层
创建一个BahdanauAttention图层:
attention_layer = BahdanauAttention(units)
这一层需要获得3 个输入:
- query:这将被解码器所产生,更高版本。
- value: 这将是编码器的输出。
- mask:要排除填充,example_tokens != 0。
首先编写如下代码:
#稍后,解码器将生成该注意查询
example_attention_query = tf.random.normal(shape=[len(example_tokens), 2, 10])
#注意编码的令牌
context_vector, attention_weights = attention_layer(
query=example_attention_query,
value=example_enc_output,
mask=(example_tokens != 0))
print(f'Attention result shape: (batch_size, query_seq_length, units): {context_vector.shape}')
print(f'Attention weights shape: (batch_size, query_seq_length, value_seq_length): {attention_weights.shape}')
执行后会输出:
Attention result shape: (batch_size, query_seq_length, units): (64, 2, 1024)
Attention weights shape: (batch_size, query_seq_length, value_seq_length): (64, 2, 24)
每个序列的注意力权重总和应该为1.0,以下是整个序列的注意力权重t=0:
plt.subplot(1, 2, 1)
plt.pcolormesh(attention_weights[:, 0, :])
plt.title('Attention weights')
plt.subplot(1, 2, 2)
plt.pcolormesh(example_tokens != 0)
plt.title('Mask')
执行后会绘制一个可视化注意力图,如图7-6所示。
图7-6 注意力图
如果放大单个序列的权重,会发现模型可以学习扩展和利用一些小的变化。代码如下:
plt.suptitle('Attention weights for one sequence')
plt.figure(figsize=(12, 6))
a1 = plt.subplot(1, 2, 1)
plt.bar(range(len(attention_slice)), attention_slice)
#释放xlim
plt.xlim(plt.xlim())
plt.xlabel('Attention weights')
a2 = plt.subplot(1, 2, 2)
plt.bar(range(len(attention_slice)), attention_slice)
plt.xlabel('Attention weights, zoomed')
# 缩小
top = max(a1.get_ylim())
zoom = 0.85*top
a2.set_ylim([0.90*top, top])
a1.plot(a1.get_xlim(), [zoom, zoom], color='k')
执行后会绘制对应的缩小版的注意力图,如图7-7所示。
图7-7 缩小版注意力图