人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part1
Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part2
Pytorch:解码器端的Attention注意力机制、seq2seq模型架构实现英译法任务
BahdanauAttention注意力机制、LuongAttention注意力机制
BahdanauAttention注意力机制:基于seq2seq的西班牙语到英语的机器翻译任务、解码器端的Attention注意力机制、seq2seq模型架构
图片的描述生成任务、使用迁移学习实现图片的描述生成过程、CNN编码器+RNN解码器(GRU)的模型架构、BahdanauAttention注意力机制、解码器端的Attention注意力机制
注意力机制、bmm运算
注意力机制 SENet、CBAM
机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)
基于Seq2Seq的中文聊天机器人编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制)
基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)
注意:这一文章“基于Transformer的文本情感分析编程实践(Encoder编码器-Decoder解码器框架 + Attention注意力机制 + Positional Encoding位置编码)”
该文章实现的Transformer的Model类型模型,实际是改造过的特别版的Transformer,因为Transformer的Model类型模型中只实现了Encoder编码器,
而没有对应实现的Decoder解码器,并且因为当前Transformer的Model类型模型处理的是分类任务,
所以我们此处只用了Encoder编码器来提取特征,最后通过全连接层网络来拟合分类。
Go. Ve.
Go. Vete.
Go. Vaya.
Go. Váyase.
Hi. Hola.
Run! ¡Corre!
Run. Corred.
Who? ¿Quién?
Fire! ¡Fuego!
Fire! ¡Incendio!
Fire! ¡Disparad!
第一步:下载和准备训练数据集
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
# 打印tensorflow版本
print("Tensorflow Version:", tf.__version__)
# 导入之后绘制Attention效果图的工具包
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# 导入sklearn中的相关工具以便进行训练集与验证集划分
from sklearn.model_selection import train_test_split
# 导入一些必备的文本清洗工具包
import unicodedata
import re
import numpy as np
import os
import io
import time
# 使用tf.keras工具中get_file方法下载文件
# 'spa-eng.zip'是下载的文件名
# origin表示文件下载地址, extract表示是否对文件进行解压缩
# 进行解压缩后, 获得压缩包的地址path_to_zip
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 = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"
Tensorflow Version: 2.1.0-rc2
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/spa-eng.zip
2646016/2638744 [==============================] - 0s 0us/step
第二步:对数据进行预处理并创建一个tf.data数据集
# 将 unicode 文件转换为 ascii
# 我们可以认为是去掉一些语言中的重音标记:如Ślusàrski--->Slusarski
def unicode_to_ascii(s):
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')
def preprocess_sentence(w):
"""句子处理函数, 输入为原始语料的一句话"""
# 字母小写并去除两侧空白符,调用unicode_to_ascii
w = unicode_to_ascii(w.lower().strip())
# 在单词与跟在其后的标点符号之间插入一个空格
# 例如: "he is a boy." => "he is a boy ."
w = re.sub(r"([?.!,¿])", r" \1 ", w)
w = re.sub(r'[" "]+', " ", w)
# 除了 (a-z, A-Z, ".", "?", "!", ","),将所有字符替换为空格
w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)
# 替换后再次去除两侧空白符
w = w.rstrip().strip()
# 给句子加上开始和结束标记
# 以便模型知道何时开始和结束预测
w = ' ' + w + ' '
return w
en_sentence = u"May I borrow this book?"
sp_sentence = u"¿Puedo tomar prestado este libro?"
print(preprocess_sentence(en_sentence))
print(preprocess_sentence(sp_sentence).encode('utf-8'))
may i borrow this book ?
b' \xc2\xbf puedo tomar prestado este libro ? '
def create_dataset(path, num_examples):
"""
description: 创建对应的翻译文本集:共包含两个元组,第一个元组中都是英文,
第二个元组中都是对应的西班牙文
:param path: 数据集路径
:param num_examples: 取数据集中的文本行数
"""
# 读取持久化文件中的全部行
lines = io.open(path, encoding='UTF-8').read().strip().split('\n')
# 取指定行数的数据并调用preprocess_sentence函数处理
pairs = [[preprocess_sentence(w) for w in l.split('\t')] for l in lines[:num_examples]]
# 将两组文本整合在一个zip对象中
return zip(*pairs)
path = path_to_file
num_examples = 5
en, sp = create_dataset(path, num_examples)
print(en)
print(sp)
# 第一个元组都是英文句子(因为我们取的是数据集前面的句子,一个单词就是一个句子)
(' go . ', ' go . ', ' go . ', ' go . ', ' hi . ')
# 第二个元组都是对应西班牙文句子
(' ve . ', ' vete . ', ' vaya . ', ' vayase . ', ' hola . ')
def max_length(tensor):
"""获取tensor中的最大长度函数"""
return max(len(t) for t in tensor)
def tokenize(lang):
"""对文本进行数值映射函数"""
# 实例化一个Tokenizer数值映射器
lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(
filters='')
# 在输入文本上拟合映射器
lang_tokenizer.fit_on_texts(lang)
# 将映射器作用在当前文本上
tensor = lang_tokenizer.texts_to_sequences(lang)
# 使用pad_sequences对文本进行最大长度补齐
tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor,
padding='post')
# 返回处理后的结果和映射器(因为在之后的预测中还会使用该映射器处理文本)
return tensor, lang_tokenizer
def load_dataset(path, num_examples=None):
"""对两种语言文本进行进行数值映射并进行最大长度补齐"""
# 获得文本清洗之后的结果
targ_lang, inp_lang = create_dataset(path, num_examples)
# 分别调用tokenize函数
input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
target_tensor, targ_lang_tokenizer = tokenize(targ_lang)
# 返回对应的结果
return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer
path = path_to_file
num_examples = 5
input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer = load_dataset(path, num_examples)
print("input_tensor:", input_tensor)
print("target_tensor:", target_tensor)
print(inp_lang_tokenizer)
print(targ_lang_tokenizer)
input_tensor: [[1 4 2 3]
[1 5 2 3]
[1 6 2 3]
[1 7 2 3]
[1 8 2 3]]
target_tensor: [[1 4 2 3]
[1 4 2 3]
[1 4 2 3]
[1 4 2 3]
[1 5 2 3]]
# 尝试实验不同大小的数据集
# 这里使用30000个样本
num_examples = 30000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)
# 计算目标张量的最大长度 (max_length)
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)
# 采用 8:2 的比例切分训练集和验证集
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)
# 训练集和验证集的样本数量
print(len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val))
24000 24000 6000 6000
def convert(lang, tensor):
# 遍历张量中的每一个数值
for t in tensor:
# 数值映射从1开始,不包括0
if t!=0:
# 使用传入的数值映射器的index_word方法寻找数值对应的单词
print ("%d ----> %s" % (t, lang.index_word[t]))
print ("输入文本的对应情况:")
convert(inp_lang, input_tensor_train[0])
print ()
print ("输出文本的对应情况:")
convert(targ_lang, target_tensor_train[0])
Input Language; index to word mapping
1 ---->
2605 ----> mande
19 ----> mi
280 ----> reloj
10 ----> a
2342 ----> arreglar
3 ----> .
2 ---->
Target Language; index to word mapping
1 ---->
4 ----> i
99 ----> had
21 ----> my
177 ----> watch
1002 ----> fixed
3 ----> .
2 ---->
# 为了方便之后的训练,都需要将数据集转化成tf.data数据集对象,这已经成为了使用tf.keras进行模型训练前的标准步骤!
# 设置超参数
BUFFER_SIZE = len(input_tensor_train)
BATCH_SIZE = 64
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE
embedding_dim = 256
units = 1024
vocab_inp_size = len(inp_lang.word_index)+1
vocab_tar_size = len(targ_lang.word_index)+1
# 转化成tf.data的dataset形式
dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
# 进行批次化,drop_remainder=True代表舍弃最后一个批次可能不满足batch_size大小的数据
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
# 取出一组样例数据查看一下,现将dataset封装成迭代器,再用next方法取出一个
example_input_batch, example_target_batch = next(iter(dataset))
# 打印结果
print(example_input_batch.shape, example_target_batch.shape)
TensorShape([64, 16]), TensorShape([64, 11])
第三步:构建模型并选择优化器和损失函数¶
class Encoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
"""
:param vocab_size: 非重复的词汇总数
:param embedding_dim: 词嵌入的维度
:enc_units: 编码器中GRU层的隐含节点数
:batch_sz: 数据批次大小(每次参数更新用到的数据量)
"""
super(Encoder, self).__init__()
# 将变量传入类中
self.batch_sz = batch_sz
self.enc_units = enc_units
# 实例化embedding层
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
# 实例化gru层
# return_sequences=True代表返回GRU序列模型的每个时间步的输出(每个输出做连接操作)
# return_state=True代表除了返回输出外,还需要返回最后一个隐层状态
# recurrent_initializer='glorot_uniform'即循环状态张量的初始化方式为均匀分布
self.gru = tf.keras.layers.GRU(self.enc_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
def call(self, x, hidden):
# 对输入进行embedding操作
x = self.embedding(x)
# 通过gru层获得最后一个时间步的输出和隐含状态
output, state = self.gru(x, initial_state = hidden)
return output, state
def initialize_hidden_state(self):
# gru层的隐含节点对应的参数张量以零张量初始化
return tf.zeros((self.batch_sz, self.enc_units))
# 实例化encoder
encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)
# 样本输入
sample_hidden = encoder.initialize_hidden_state()
sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
print ('Encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape))
print ('Encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
Encoder output shape: (batch size, sequence length, units) (64, 16, 1024)
Encoder Hidden state shape: (batch size, units) (64, 1024)
# 这里使用Bahdanau 注意力机制
1, score = FC(tanh(FC(EO) + FC(H)))
2, attention weights = softmax(score, axis = 1).
# 解释: Softmax 默认被应用于最后一个轴,但是这里我们想将它应用于第一个轴,
# 因为分数 (score) 的形状是 (批大小,最大长度,隐层大小),最大长度 (max_length) 是输入的长度。
# 因为我们想为每个输入长度分配一个权重,所以softmax应该用在这个轴上。
3, context vector = sum(attention weights * EO, axis = 1)
# 解释: 选择第一个轴的原因同上.
4, embedding output = 解码器输入 X 通过一个嵌入层
5, merged vector = concat(embedding output, context vector)
符号代表:
FC: 全连接层
EO: 编码器输出
H: 隐藏层状态
X: 解码器输入
class BahdanauAttention(tf.keras.Model):
def __init__(self, units):
"""初始化三个必要的全连接层"""
super(BahdanauAttention, self).__init__()
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
def call(self, features, hidden):
"""
description: 具体计算函数
:param features: 编码器的输出
:param hidden: 解码器的隐层输出状态
return: 通过注意力机制处理后的结果和注意力权重attention_weights
"""
# 为hidden扩展一个维度(batch_size, hidden_size) --> (batch_size, 1, hidden_size)
hidden_with_time_axis = tf.expand_dims(hidden, 1)
# 根据公式计算注意力得分, 输出score的形状为: (batch_size, 64, hidden_size)
score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))
# 根据公式计算注意力权重, 输出attention_weights形状为: (batch_size, 64, 1)
attention_weights = tf.nn.softmax(self.V(score), axis=1)
# 最后根据公式获得注意力机制处理后的结果context_vector
# context_vector的形状为: (batch_size, hidden_size)
context_vector = attention_weights * features
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
attention_layer = BahdanauAttention(10)
attention_result, attention_weights = attention_layer(sample_hidden, sample_output)
print("Attention result shape: (batch size, units) {}".format(attention_result.shape))
print("Attention weights shape: (batch_size, sequence_length, 1) {}".format(attention_weights.shape))
Attention result shape: (batch size, units) (64, 1024)
Attention weights shape: (batch_size, sequence_length, 1) (64, 16, 1)
class Decoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
super(Decoder, self).__init__()
# 所有的参数传递和实例化方法与编码器相同
self.batch_sz = batch_sz
self.dec_units = dec_units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
# 实例化一个Dense层作为输出层
self.fc = tf.keras.layers.Dense(vocab_size)
# 在解码器阶段我们将使用注意力机制,这里实例化注意力的类
self.attention = BahdanauAttention(self.dec_units)
def call(self, x, hidden, enc_output):
"""
:param x: 每个时间步上解码器的输入
:param hidden: 每次解码器的隐层输出
:param enc_output: 编码器的输出
"""
# 输入通过embedding层
x = self.embedding(x)
# 使用注意力规则计算hidden与enc_output的'相互影响程度'
context_vector, attention_weights = self.attention(enc_output, hidden)
# 将这种'影响程度'与输入x拼接(这个操作也是注意力计算规则的一部分)
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
# 将新的x输入到gru层中得到输出
output, state = self.gru(x)
# 改变输出形状使其适应全连接层的输入形式
output = tf.reshape(output, (-1, output.shape[2]))
# 使用全连接层作为输出层
# 输出的形状 == (批大小,vocab)
x = self.fc(output)
return x, state, attention_weights
decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)
sample_decoder_output, _, _ = decoder(tf.random.uniform((64, 1)),
sample_output, sample_hidden)
print ('Decoder output shape: (batch_size, vocab size) {}'.format(sample_decoder_output.shape))
Decoder output shape: (batch_size, vocab size) (64, 4935)
# 选取Adam优化方法
optimizer = tf.keras.optimizers.Adam()
# 损失基本计算方法为稀疏类别交叉熵损失
# from_logits=True代表是否将预测结果预期为非 0/1 的值进行保留
# 理论来讲二分类最终的结果应该只有0/1,函数将自动将其变为0/1,from_logits=True后,值不会被改变
# reduction='none',接下来我们将自定义损失函数,reduction必须设置为None,
# 我们可以将它看作是自定义损失函数的识别属性
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=True, reduction='none')
# 因为每次生成的结果都是局部结果,要和真实结果进行比较需要对真实结果进行遮掩
# 等效于对损失计算结果进行掩码
def loss_function(real, pred):
"""自定义损失函数,参数为预测结果pred和真实结果real"""
# 使用tf.math.equal方法对real和0进行对比
# 对结果再进行逻辑非操作生成掩码张量mask
mask = tf.math.logical_not(tf.math.equal(real, 0))
# 使用基本计算方法计算损失
loss_ = loss_object(real, pred)
# 将mask进行类型转换,使其能够进行后续操作
mask = tf.cast(mask, dtype=loss_.dtype)
# 将loss_与mask相乘即对loss_进行掩码
loss_ *= mask
# 计算loss_张量所有元素的均值
return tf.reduce_mean(loss_)
# 定义检测点(每个阶段训练的模型)保存路径
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
# 使用tf.train.Checkpoint创建检测点保存对象
# 我们会在之后的训练中调用它来保存模型
checkpoint = tf.train.Checkpoint(optimizer=optimizer,
encoder=encoder,
decoder=decoder)
第四步: 构建训练函数并进行训练
@tf.function # 该装饰器使该函数自动编译张量图, 使其可以直接执行
def train_step(inp, targ, enc_hidden):
# 设定初始损失为0
loss = 0
# 开启一个用于梯度记录的上下文管理器
with tf.GradientTape() as tape:
# 调用编码器部分得到编码器输出和编码器隐层输出
enc_output, enc_hidden = encoder(inp, enc_hidden)
# 将编码器隐层输出设定为解码器的初始隐层状态
dec_hidden = enc_hidden
# 以'起始符'作为解码器的第一个输入字符
dec_input = tf.expand_dims([targ_lang.word_index['']] * BATCH_SIZE, 1)
# 开始循环解码过程
for t in range(1, targ.shape[1]):
# 使用解码器获得新的解码器隐层输出(状态),以及预测结果
predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)
# 使用损失函数计算本次训练过程的损失
loss += loss_function(targ[:, t], predictions)
# 使用teacher-forcing矫正可能的错误结果
# 直接将正确结果作为下一次循环的输入
dec_input = tf.expand_dims(targ[:, t], 1)
# 计算每次batch的平均损失
batch_loss = (loss / int(targ.shape[1]))
# 获得整个模型训练的参数变量
variables = encoder.trainable_variables + decoder.trainable_variables
# 使用梯度记录管理器求解整个网络的梯度,参数是loss和全部参数变量
gradients = tape.gradient(loss, variables)
# 根据梯度更新参数
optimizer.apply_gradients(zip(gradients, variables))
# 返回每次batch的平均损失
return batch_loss
什么是teacher_forcing?
teacher_forcing的作用:
# 设置训练轮数
EPOCHS = 10
for epoch in range(EPOCHS):
# 获得每轮训练的开始时间
start = time.time()
# 初始化编码器隐含状态
enc_hidden = encoder.initialize_hidden_state()
# 初始化总损失为0
total_loss = 0
# 循环数据集中的每个批次进行训练
for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
# 调用train_step函数获得批次平均损失
batch_loss = train_step(inp, targ, enc_hidden)
# 将批次平均损失相加获得轮数总损失
total_loss += batch_loss
# 每100个批次打印一次批次平均损失
if batch % 100 == 0:
print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1,
batch,
batch_loss.numpy()))
# 每两轮(epoch),保存一次模型
if (epoch + 1) % 2 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
# 打印轮数平均损失
print('Epoch {} Loss {:.4f}'.format(epoch + 1,
total_loss / steps_per_epoch))
# 打印模型训练耗时
print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
Epoch 1 Batch 0 Loss 4.6268
Epoch 1 Batch 100 Loss 2.1385
Epoch 1 Batch 200 Loss 1.8945
Epoch 1 Batch 300 Loss 1.7187
Epoch 1 Loss 2.0145
Time taken for 1 epoch 34.930274963378906 sec
Epoch 2 Batch 0 Loss 1.5522
Epoch 2 Batch 100 Loss 1.3454
Epoch 2 Batch 200 Loss 1.3791
Epoch 2 Batch 300 Loss 1.3919
Epoch 2 Loss 1.3569
Time taken for 1 epoch 17.682456016540527 sec
Epoch 3 Batch 0 Loss 1.0861
Epoch 3 Batch 100 Loss 1.0815
Epoch 3 Batch 200 Loss 0.9158
Epoch 3 Batch 300 Loss 0.8474
Epoch 3 Loss 0.9312
Time taken for 1 epoch 17.20573592185974 sec
Epoch 4 Batch 0 Loss 0.6689
Epoch 4 Batch 100 Loss 0.5476
Epoch 4 Batch 200 Loss 0.5617
Epoch 4 Batch 300 Loss 0.5462
Epoch 4 Loss 0.6193
Time taken for 1 epoch 17.651796579360962 sec
Epoch 5 Batch 0 Loss 0.3442
Epoch 5 Batch 100 Loss 0.3203
Epoch 5 Batch 200 Loss 0.4267
Epoch 5 Batch 300 Loss 0.4384
Epoch 5 Loss 0.4171
Time taken for 1 epoch 17.20345664024353 sec
Epoch 6 Batch 0 Loss 0.2950
Epoch 6 Batch 100 Loss 0.3294
Epoch 6 Batch 200 Loss 0.3228
Epoch 6 Batch 300 Loss 0.2724
Epoch 6 Loss 0.2861
Time taken for 1 epoch 17.587135076522827 sec
Epoch 7 Batch 0 Loss 0.1584
Epoch 7 Batch 100 Loss 0.1876
Epoch 7 Batch 200 Loss 0.1844
Epoch 7 Batch 300 Loss 0.1919
Epoch 7 Loss 0.2025
Time taken for 1 epoch 17.285163402557373 sec
Epoch 8 Batch 0 Loss 0.1315
Epoch 8 Batch 100 Loss 0.1640
Epoch 8 Batch 200 Loss 0.1424
Epoch 8 Batch 300 Loss 0.1343
Epoch 8 Loss 0.1482
Time taken for 1 epoch 17.918259382247925 sec
Epoch 9 Batch 0 Loss 0.1118
Epoch 9 Batch 100 Loss 0.1030
Epoch 9 Batch 200 Loss 0.0644
Epoch 9 Batch 300 Loss 0.1106
Epoch 9 Loss 0.1172
Time taken for 1 epoch 17.48120665550232 sec
Epoch 10 Batch 0 Loss 0.0853
Epoch 10 Batch 100 Loss 0.0803
Epoch 10 Batch 200 Loss 0.0681
Epoch 10 Batch 300 Loss 0.0957
Epoch 10 Loss 0.0951
Time taken for 1 epoch 17.945307970046997 sec
第五步:构建评估函数并进行预测分析
def evaluate(sentence):
"""
description: 评估函数
:param sentence: 待翻译的句子
"""
# 初始化用于绘制注意力效果图的张量
attention_plot = np.zeros((max_length_targ, max_length_inp))
# 下面将对输入文本做与训练语料同样的操作
# 对输入的句子进行文本预处理
sentence = preprocess_sentence(sentence)
# 使用输入数值映射器对文本进行数值化映射
inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
# 对文本进行最大长度补齐
inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs],
maxlen=max_length_inp,
padding='post')
# 转换成张量
inputs = tf.convert_to_tensor(inputs)
# 定义翻译后的结果变量
result = ''
# 初始化编码器的隐层状态
hidden = [tf.zeros((1, units))]
# 使用编码器进行编码
enc_out, enc_hidden = encoder(inputs, hidden)
# 将编码器隐层输出设定为解码器的初始隐层状态
dec_hidden = enc_hidden
# 以'起始符'作为解码器的第一个输入字符
dec_input = tf.expand_dims([targ_lang.word_index['']], 0)
# 开始循环解码,过程和训练时十分类似
for t in range(max_length_targ):
predictions, dec_hidden, attention_weights = decoder(dec_input,
dec_hidden,
enc_out)
# 存储每次产生的注意力权重以便后面制图
attention_weights = tf.reshape(attention_weights, (-1, ))
attention_plot[t] = attention_weights.numpy()
# 从预测分布中获得概率最大预测id
predicted_id = tf.argmax(predictions[0]).numpy()
# 使用目标数值映射器将其还原为对应的文本(单词/标识)
result += targ_lang.index_word[predicted_id] + ' '
# 如果解码还原后为'终止符',则返回结果
if targ_lang.index_word[predicted_id] == '':
return result, sentence, attention_plot
# 否则,预测id被输送回模型,作为下一次预测输入
dec_input = tf.expand_dims([predicted_id], 0)
# 返回预测结果,原始输入文本,以及整个过程的注意力张量组合(仍然是一个张量)
return result, sentence, attention_plot
def plot_attention(attention, sentence, predicted_sentence):
"""
description: 注意力张量制图函数
:param attention: 整个过程的注意力张量组合
:param sentence: 原始输入文本
:param predicted_sentence: 预测结果
"""
# 打开一个10x10的画布
fig = plt.figure(figsize=(10,10))
# 在画布上创建1x1的子画布
ax = fig.add_subplot(1, 1, 1)
# 绘制子画布上绘制矩形,根据给出输入数值不同,颜色也不相同
# cmap='viridis'是一种矩形的色彩填充方案‘绿藻’
ax.matshow(attention, cmap='viridis')
# 定义字体规范字典,这里我们只要求字体大小即可
fontdict = {'fontsize': 14}
# 使用x轴设置方法使输入文本在x轴上显示
# rotation=90代表有90度的倾斜角,便于观看
ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
# 同样,使预测文本在y轴上显示
ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)
# 设置x和y轴的刻度,与绘制时add_subplot的参数对应即可
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
# 绘图
plt.show()
def translate(sentence):
"""预测并绘图"""
# 调用评估函数获得结果
result, sentence, attention_plot = evaluate(sentence)
# 打印输入文本和预测文本
print('Input: %s' % (sentence))
print('Predicted translation: {}'.format(result))
# 根据文本长度对注意力张量进行剪裁(剪裁掉的都是0部分)
attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
# 进行注意力效果图绘制
plot_attention(attention_plot, sentence.split(' '), result.split(' '))
# 使用检测点对象恢复最近一次保存的模型
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
# 翻译西班牙语‘这里很冷。’
translate(u'hace mucho frio aqui.')
# 翻译西班牙语‘这就是我的生活。’
translate(u'esta es mi vida.')
# 翻译西班牙语‘他们还在家里吗?’
translate(u'¿todavia estan en casa?')
# 翻译西班牙语‘尝试找出答案。’
translate(u'trata de averiguarlo.')
Input: hace mucho frio aqui .
Predicted translation: it s very cold here .
Input: esta es mi vida .
Predicted translation: this is my life .
Input: ¿ todavia estan en casa ?
Predicted translation: are you still at home ?
Input: trata de averiguarlo .
Predicted translation: try to figure it out .
"""
使用基于GRU的seq2seq模型架构实现翻译的过程
第一步:下载和准备训练数据集
第二步:对数据进行预处理并创建一个tf.data数据集
第三步:构建模型并选择优化器和损失函数
第四步:构建训练函数并进行训练
第五步:构建评估函数并进行预测分析
"""
#---------------------------------第一步:下载和准备训练数据集-----------------------------------#
from __future__ import absolute_import, division, print_function, unicode_literals
import tensorflow as tf
# 打印tensorflow版本
# print("Tensorflow Version:", tf.__version__)
# 导入之后绘制Attention效果图的工具包
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
# 导入sklearn中的相关工具以便进行训练集与验证集划分
from sklearn.model_selection import train_test_split
# 导入一些必备的文本清洗工具包
import unicodedata
import re
import numpy as np
import os
import io
import time
# 使用tf.keras工具中get_file方法下载文件
# 'spa-eng.zip'是下载的文件名
# origin表示文件下载地址, extract表示是否对文件进行解压缩
# 进行解压缩后, 获得压缩包的地址path_to_zip
# 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 = os.path.dirname(path_to_zip)+"/spa-eng/spa.txt"
path_to_file = "./spa.txt"
#---------------------------------第二步:对数据进行预处理并创建一个tf.data数据集-----------------------------------#
#对文本进行清洗并给每个句子添加一个“开始标记” 和一个“结束标记”:
# 将 unicode 文件转换为 ascii
# 我们可以认为是去掉一些语言中的重音标记:如Ślusàrski--->Slusarski
def unicode_to_ascii(s):
return ''.join(c for c in unicodedata.normalize('NFD', s)
if unicodedata.category(c) != 'Mn')
def preprocess_sentence(w):
"""句子处理函数, 输入为原始语料的一句话"""
# 字母小写并去除两侧空白符,调用unicode_to_ascii
w = unicode_to_ascii(w.lower().strip())
# 在单词与跟在其后的标点符号之间插入一个空格
# 例如: "he is a boy." => "he is a boy ."
w = re.sub(r"([?.!,¿])", r" \1 ", w)
w = re.sub(r'[" "]+', " ", w)
# 除了 (a-z, A-Z, ".", "?", "!", ","),将所有字符替换为空格
w = re.sub(r"[^a-zA-Z?.!,¿]+", " ", w)
# 替换后再次去除两侧空白符
w = w.rstrip().strip()
# 给句子加上开始和结束标记
# 以便模型知道何时开始和结束预测
w = ' ' + w + ' '
return w
#调用:
# en_sentence = u"May I borrow this book?"
# sp_sentence = u"¿Puedo tomar prestado este libro?"
# print(preprocess_sentence(en_sentence)) # may i borrow this book ?
# print(preprocess_sentence(sp_sentence).encode('utf-8')) #b' \xc2\xbf puedo tomar prestado este libro ? '
#创建对应的翻译文本集
def create_dataset(path, num_examples):
"""
description: 创建对应的翻译文本集:共包含两个元组,第一个元组中都是英文,
第二个元组中都是对应的西班牙文
:param path: 数据集路径
:param num_examples: 取数据集中的文本行数
"""
# 读取持久化文件中的全部行
lines = io.open(path, encoding='UTF-8').read().strip().split('\n')
# 取指定行数的数据并调用preprocess_sentence函数处理
pairs = [[preprocess_sentence(w) for w in l.split('\t')] for l in lines[:num_examples]]
#[[' go . ', ' ve . '], [' go . ', ' vete . '], ......]
# print(pairs)
#[' go . ', ' ve . '] [' go . ', ' vete . '] ......
# print(*pairs)
"""pairs:[[' 输入序列 ', ' 目标序列 '], ......]
*pairs:[' 输入序列 ', ' 目标序列 '] ......
zip(*pairs)作为返回值赋值给en, sp两个变量,即可把每个一维列表中的第一个元素封装为元祖赋值给en,把每个一维列表中的第二个元素封装为元祖赋值给sp
"""
# 将两组文本整合在一个zip对象中
return zip(*pairs)
#调用:
# path = path_to_file
# num_examples = 25
# en, sp = create_dataset(path, num_examples)
# print(en) #(' go . ', ' go . ', ' go . ', ' go . ', ......)
# print(sp) #(' ve . ', ' vete . ', ' vaya . ', ......)
"""
限制训练集的大小以保证在可控时间内完成训练
为了加快训练速度,将使用30,000个训练子集来训练模型。
如果你的硬件资源足够充分,也可以选择使用更多数据来提高模型质量。
如果在一个P100 GPU 上运行10万条数据大约花费10分钟左右。
"""
#对文本进行数值映射并进行最大长度补齐
def max_length(tensor):
"""获取tensor中的最大长度函数"""
return max(len(t) for t in tensor)
def tokenize(lang):
"""对文本进行数值映射函数"""
# 实例化一个Tokenizer数值映射器
lang_tokenizer = tf.keras.preprocessing.text.Tokenizer(filters='')
# 在输入文本上拟合映射器
lang_tokenizer.fit_on_texts(lang)
# 将映射器作用在当前文本上
tensor = lang_tokenizer.texts_to_sequences(lang)
"""
tf.keras.preprocessing.sequence.pad_sequences(sequences, maxlen=None, dtype='int32', padding='pre', truncating='pre',value=0.0)
此函数将num_samples序列列表(整数列表)转换为2D Numpy形状的数组(num_samples, num_timesteps)。
num_timesteps是maxlen参数(如果提供),或者是最长序列的长度。
小于num_timesteps的序列在末尾填充值。
长度大于num_timesteps的序列将被截断,以使其适合所需的长度。填充或截断发生的位置分别由参数padding和截断确定。
预填充是默认设置。
maxlen最大长度:Int,所有序列的最大长度。
padding填充:
字符串,'pre'前置 或 'post'后置
在每个序列之前或之后填充。
value=0.0:浮点数或字符串,填充值。
"""
#maxlen=None:默认使用所有句子中最长句子的长度作为maxlen
# 使用pad_sequences对文本进行最大长度补齐
tensor = tf.keras.preprocessing.sequence.pad_sequences(tensor, padding='post')
# 返回处理后的结果和映射器(因为在之后的预测中还会使用该映射器处理文本)
return tensor, lang_tokenizer
def load_dataset(path, num_examples=None):
"""对两种语言文本进行进行数值映射并进行最大长度补齐"""
# 获得文本清洗之后的结果
targ_lang, inp_lang = create_dataset(path, num_examples)
# 分别调用tokenize函数
input_tensor, inp_lang_tokenizer = tokenize(inp_lang)
target_tensor, targ_lang_tokenizer = tokenize(targ_lang)
# 返回对应的结果
return input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer
# path = path_to_file
# num_examples = 5
# input_tensor, target_tensor, inp_lang_tokenizer, targ_lang_tokenizer = load_dataset(path, num_examples)
# print("input_tensor:", input_tensor)
# print("target_tensor:", target_tensor)
# print(inp_lang_tokenizer)
# print(targ_lang_tokenizer)
# input_tensor:
# [[1 4 2 3]
# [1 5 2 3]
# [1 6 2 3]
# [1 7 2 3]
# [1 8 2 3]]
# target_tensor:
# [[1 4 2 3]
# [1 4 2 3]
# [1 4 2 3]
# [1 4 2 3]
# [1 5 2 3]]
#
#
# 尝试实验不同大小的数据集
# 这里使用30000个样本
num_examples = 30000
input_tensor, target_tensor, inp_lang, targ_lang = load_dataset(path_to_file, num_examples)
# 计算目标张量的最大长度 (max_length)
#max_length_targ:目标序列所有语句中的最大长度句子的长度(单词数)
#max_length_inp:输入序列所有语句中的最大长度句子的长度(单词数)
max_length_targ, max_length_inp = max_length(target_tensor), max_length(input_tensor)
print(max_length_targ, max_length_inp) #11 16
# 采用 8:2 的比例切分训练集和验证集
input_tensor_train, input_tensor_val, target_tensor_train, target_tensor_val = train_test_split(input_tensor, target_tensor, test_size=0.2)
# 训练集和验证集的样本数量
print(len(input_tensor_train), len(target_tensor_train), len(input_tensor_val), len(target_tensor_val)) #24000 24000 6000 6000
#查看一下数值映射后样本的对应情况:
def convert(lang, tensor):
# 遍历张量中的每一个数值
for t in tensor:
# 数值映射从1开始,不包括0
if t!=0:
# 使用传入的数值映射器的index_word方法寻找数值对应的单词
print ("%d ----> %s" % (t, lang.index_word[t]))
# print ("输入文本的对应情况:")
# convert(inp_lang, input_tensor_train[0])
# print ()
# print ("输出文本的对应情况:")
# convert(targ_lang, target_tensor_train[0])
# 输入文本的对应情况:
# 1 ---->
# 33 ----> los
# 1923 ----> fantasmas
# 1924 ----> existen
# 3 ----> .
# 2 ---->
# 输出文本的对应情况:
# 1 ---->
# 1508 ----> ghosts
# 1115 ----> exist
# 3 ----> .
# 2 ---->
#创建一个tf.data数据集对象
# 为了方便之后的训练,都需要将数据集转化成tf.data数据集对象,这已经成为了使用tf.keras进行模型训练前的标准步骤!
# 设置超参数
BUFFER_SIZE = len(input_tensor_train) #24000个用于训练阶段的样本
BATCH_SIZE = 64 #数据批次大小(每次参数更新用到的数据量)
steps_per_epoch = len(input_tensor_train)//BATCH_SIZE #steps_per_epoch:批量次数,即一个epoch中遍历BATCH_SIZE批量大小数据的次数
embedding_dim = 256 #词嵌入的维度
units = 1024 #编码器中GRU层的隐含节点数,即隐藏层中的隐藏神经元数量
# 非重复的词汇总数
vocab_inp_size = len(inp_lang.word_index)+1 #获取输入序列的不重复单词的总数作为输入序列的字典大小
vocab_tar_size = len(targ_lang.word_index)+1 #获取目标序列的不重复单词的总数作为目标序列的字典大小
print(vocab_inp_size) #9414 输入序列的不重复单词的总数作为输入序列的字典大小
print(vocab_tar_size) #4935 目标序列的不重复单词的总数作为目标序列的字典大小
"""
dataset中输入序列tensor的形状为(64, 16),即(BATCH_SIZE, 输入序列最大长度句子的长度)
dataset中目标序列tensor的形状为(64, 11),即(BATCH_SIZE, 目标序列最大长度句子的长度)
"""
# 转化成tf.data的dataset形式
dataset = tf.data.Dataset.from_tensor_slices((input_tensor_train, target_tensor_train)).shuffle(BUFFER_SIZE)
# 进行批次化,drop_remainder=True代表舍弃最后一个批次可能不满足batch_size大小的数据
dataset = dataset.batch(BATCH_SIZE, drop_remainder=True)
# 取出一组样例数据查看一下,现将dataset封装成迭代器,再用next方法取出一个
example_input_batch, example_target_batch = next(iter(dataset))
# 打印结果
print(example_input_batch.shape, example_target_batch.shape) #(64, 16) (64, 11)
#---------------------------------第三步:构建模型并选择优化器和损失函数-----------------------------------#
#构建模型的编码器部分:
class Encoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, enc_units, batch_sz):
"""
:param vocab_size: 非重复的词汇总数
:param embedding_dim: 词嵌入的维度
:enc_units: 编码器中GRU层的隐含节点数
:batch_sz: 数据批次大小(每次参数更新用到的数据量)
"""
super(Encoder, self).__init__()
# 将变量传入类中
self.batch_sz = batch_sz
self.enc_units = enc_units
# 实例化embedding层
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
"""
return_sequences:
布尔值。是返回输出序列中的最后一个输出还是完整序列。 默认值:False。
True代表返回GRU序列模型的每个时间步的输出(每个输出做连接操作)
return_state:
布尔值。 除输出外,是否返回最后一个状态。 默认值:False。
True代表除了返回输出外,还需要返回最后一个隐层状态。
recurrent_initializer:
recurrent_kernel权重矩阵的初始化程序,用于对递归状态进行线性转换。 默认值:正交。
'glorot_uniform'即循环状态张量的初始化方式为均匀分布。
"""
# 实例化gru层
# return_sequences=True代表返回GRU序列模型的每个时间步的输出(每个输出做连接操作)
# return_state=True代表除了返回输出外,还需要返回最后一个隐层状态
# recurrent_initializer='glorot_uniform'即循环状态张量的初始化方式为均匀分布
self.gru = tf.keras.layers.GRU(self.enc_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
def call(self, x, hidden):
# 对输入进行embedding操作
x = self.embedding(x)
"""initial_state:要传递给单元格的第一个调用的初始状态张量的列表(可选,默认为None,这将导致创建零填充的初始状态张量)。"""
# 通过gru层获得最后一个时间步的输出和隐含状态
output, state = self.gru(x, initial_state = hidden)
return output, state
def initialize_hidden_state(self):
""" (BATCH_SIZE, 隐藏层中的隐藏神经元数量) """
# gru层的隐含节点对应的参数张量以零张量初始化
return tf.zeros((self.batch_sz, self.enc_units))
#调用:
# 实例化encoder
encoder = Encoder(vocab_inp_size, embedding_dim, units, BATCH_SIZE)
# 样本输入
# sample_hidden = encoder.initialize_hidden_state()
# sample_output, sample_hidden = encoder(example_input_batch, sample_hidden)
# #(64, 16, 1024) 即 (BATCH_SIZE, 输入序列最大长度句子的长度, 隐藏层中的隐藏神经元数量)
# print ('Encoder output shape: (batch size, sequence length, units) {}'.format(sample_output.shape))
# #(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
# print ('Encoder Hidden state shape: (batch size, units) {}'.format(sample_hidden.shape))
"""
tensorflow提供了两种Attention Mechanisms(注意力机制)
1.BahdanauAttention注意力机制:tfa.seq2seq.BahdanauAttention
实现BahdanauAttention风格的(additive累积)注意力机制。
2.LuongAttention注意力机制:tfa.seq2seq.LuongAttention
实现LuongAttention风格的(multiplicative乘法)注意力机制
构建BahdanauAttention注意力机制类的伪代码:
# 这里实现 BahdanauAttention注意力机制
1, score = FC(tanh(FC(EO) + FC(H)))
2, attention weights = softmax(score, axis = 1).
解释: Softmax 默认被应用于最后一个轴,但是这里我们想将它应用于第一个轴,
因为分数 (score) 的形状是 (批大小,最大长度,隐层大小),最大长度 (max_length) 是输入的长度。
因为我们想为每个输入长度分配一个权重,所以softmax应该用在这个轴上。
3, context vector = sum(attention weights * EO, axis = 1)
解释: 选择第一个轴的原因同上.
4, embedding output = 解码器输入 X 通过一个嵌入层
5, merged vector = concat(embedding output, context vector)
符号代表:
FC: 全连接层
EO: 编码器输出
H: 隐藏层状态
X: 解码器输入
"""
#构建注意力机制类:
class BahdanauAttention(tf.keras.Model):
def __init__(self, units):
"""初始化三个必要的全连接层"""
super(BahdanauAttention, self).__init__()
self.W1 = tf.keras.layers.Dense(units)
self.W2 = tf.keras.layers.Dense(units)
self.V = tf.keras.layers.Dense(1)
"""
传入值:
features:编码器的输出,(64, 16, 1024) 即 (BATCH_SIZE, 输入序列最大长度句子的长度, 隐藏层中的隐藏神经元数量)
hidden:解码器的隐层输出状态,(64, 1024) 即 (batch_size, hidden_size) (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
返回值:
attention_result:(64, 1024) 即 (batch size, units) (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
attention_weights:(64, 16, 1) 即 (batch_size, sequence_length, 1) (BATCH_SIZE, 输入序列最大长度句子的长度, 1)
"""
def call(self, features, hidden):
"""
description: 具体计算函数
:param features: 编码器的输出
:param hidden: 解码器的隐层输出状态
return: 通过注意力机制处理后的结果和注意力权重attention_weights
"""
"""
1.hidden_with_time_axis = tf.expand_dims(hidden, 1)
解码器的隐层输出状态hidden,(64, 1024) 即 (batch_size, hidden_size) (BATCH_SIZE, 隐藏层中的隐藏神经元数量)。
hidden扩展一个维度从(64, 1024)变成(64, 1,1024)。
2.score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))
计算注意力得分score。
features:编码器的输出,(64, 16, 1024)。
hidden_with_time_axis:解码器的隐层输出状态,(64, 1,1024)
W1和W2:Dense(隐藏层中的隐藏神经元数量1024)
tanh(W1(features) + W2(hidden_with_time_axis)):
---> tanh(W1((64, 16, 1024)) + W2((64, 1,1024)))
---> tanh((64, 16, 1024))
---> (64, 16, 1024) 即 (BATCH_SIZE, 输入序列最大长度句子的长度, 隐藏层中的隐藏神经元数量)
3.attention_weights = tf.nn.softmax(self.V(score), axis=1)
计算注意力权重attention_weights。
V:Dense(隐藏层中的隐藏神经元数量1)
softmax(V(score), axis=1)
---> softmax(V((64, 16, 1024)), axis=1)
---> softmax((64, 16, 1), axis=1)
---> (64, 16, 1) 即 (BATCH_SIZE, 输入序列最大长度句子的长度, 1)
因为注意力得分score的形状是(BATCH_SIZE, 输入序列最大长度句子的长度, 隐藏层中的隐藏神经元数量),
输入序列最大长度句子的长度(max_length)是输入的长度。
因为我们想为每个输入长度分配一个权重,所以softmax应该用在第一个轴(max_length)上axis=1,
而softmax默认被应用于最后一个轴axis=-1。
4.context_vector = tf.reduce_sum(attention_weights * features, axis=1)
获得注意力机制处理后的结果context_vector。
reduce_sum(attention_weights * features, axis=1)
---> reduce_sum((64, 16, 1) * (64, 16, 1024), axis=1)
---> reduce_sum((64, 16, 1024), axis=1)
---> (64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
"""
# 为hidden扩展一个维度(batch_size, hidden_size) --> (batch_size, 1, hidden_size)
hidden_with_time_axis = tf.expand_dims(hidden, 1)
# 根据公式计算注意力得分, 输出score的形状为: (batch_size, 16, hidden_size)
score = tf.nn.tanh(self.W1(features) + self.W2(hidden_with_time_axis))
# 根据公式计算注意力权重, 输出attention_weights形状为: (batch_size, 16, 1)
attention_weights = tf.nn.softmax(self.V(score), axis=1)
# 最后根据公式获得注意力机制处理后的结果context_vector
# context_vector的形状为: (batch_size, hidden_size)
context_vector = attention_weights * features
context_vector = tf.reduce_sum(context_vector, axis=1)
return context_vector, attention_weights
#调用:
# attention_layer = BahdanauAttention(1024)
# attention_result, attention_weights = attention_layer(sample_output, sample_hidden)
# print("Attention result shape: (batch size, units) {}".format(attention_result.shape)) #(64, 1024)
# print("Attention weights shape: (batch_size, sequence_length, 1) {}".format(attention_weights.shape)) #(64, 16, 1)
"""
构建RNN解码器:这里RNN是指GRU, 同时在解码器中使用注意力机制.
"""
class Decoder(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim, dec_units, batch_sz):
super(Decoder, self).__init__()
# 所有的参数传递和实例化方法与编码器相同
self.batch_sz = batch_sz
self.dec_units = dec_units
self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
self.gru = tf.keras.layers.GRU(self.dec_units,
return_sequences=True,
return_state=True,
recurrent_initializer='glorot_uniform')
# 实例化一个Dense层作为输出层
self.fc = tf.keras.layers.Dense(vocab_size)
# 在解码器阶段我们将使用注意力机制,这里实例化注意力的类
self.attention = BahdanauAttention(self.dec_units)
"""
1.x = self.embedding(x)
输入:(64, 1) 64行1列,批量大小句子数为64,1列为该行句子的第N列的单词
输出:(64, 1, 256) (BATCH_SIZE, 输入序列最大长度句子的长度, 嵌入维度)
2.context_vector, attention_weights = self.attention(enc_output, hidden)
attention_weights注意力权重:(64, 16, 1) 即 (BATCH_SIZE, 输入序列最大长度句子的长度, 1)
context_vector注意力机制处理后的结果:(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
3.x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
tf.expand_dims(context_vector, 1):(64, 1, 1024) 即 (BATCH_SIZE, 1, 隐藏层中的隐藏神经元数量)
concat([(64, 1, 1024),(64, 1, 256)], axis=-1):1024+256=1280,最终输出 (64, 1, 1280)
4.GRU
1.tf.keras.layers.GRU(self.dec_units, return_sequences=True, return_state=True, recurrent_initializer='glorot_uniform')
return_sequences:
布尔值。是返回输出序列中的最后一个输出还是完整序列。 默认值:False。
True代表返回GRU序列模型的每个时间步的输出(每个输出做连接操作)
return_state:
布尔值。 除输出外,是否返回最后一个状态。 默认值:False。
True代表除了返回输出外,还需要返回最后一个隐层状态。
recurrent_initializer:
recurrent_kernel权重矩阵的初始化程序,用于对递归状态进行线性转换。 默认值:正交。
'glorot_uniform'即循环状态张量的初始化方式为均匀分布。
2.output, state = gru(x)
output:
(64, 1, 1024) 即 (BATCH_SIZE, 1, 隐藏层中的隐藏神经元数量)
(当前批次的样本个数, 当前样本的序列长度(单词个数), 隐藏层中神经元数量 * 1)
state:
(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
5.output = tf.reshape(output, (-1, output.shape[2]))
(-1, output.shape[2]):表示把(64, 1, 1024)转换为(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
6.x = self.fc(output)
x:(64, 4935) 即 (BATCH_SIZE, 目标序列的不重复单词的总数作为目标序列的字典大小)
"""
def call(self, x, hidden, enc_output):
"""
:param x: 每个时间步上解码器的输入
:param hidden: 每次解码器的隐层输出
:param enc_output: 编码器的输出
"""
# print("x.shape",x.shape) #(64, 1)。64行1列,批量大小句子数为64,1列为该行句子的第N列的单词
# 输入通过embedding层
x = self.embedding(x)
# print("x1.shape",x.shape) #(64, 1, 256)。(BATCH_SIZE, 输入序列最大长度句子的长度, 嵌入维度)
# 使用注意力规则计算hidden与enc_output的'相互影响程度'
context_vector, attention_weights = self.attention(enc_output, hidden)
# print("tf.expand_dims(context_vector, 1).shape",tf.expand_dims(context_vector, 1).shape) #(64, 1, 1024)
# 将这种'影响程度'与输入x拼接(这个操作也是注意力计算规则的一部分)
x = tf.concat([tf.expand_dims(context_vector, 1), x], axis=-1)
# print("x2.shape",x.shape) #(64, 1, 1280)
# 将新的x输入到gru层中得到输出
output, state = self.gru(x)
# print("output1.shape",output.shape) #(64, 1, 1024) 即 (BATCH_SIZE, 1, 隐藏层中的隐藏神经元数量)
# print("state.shape",state.shape) #(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
# 改变输出形状使其适应全连接层的输入形式
output = tf.reshape(output, (-1, output.shape[2]))
# print("output2.shape",output.shape) #(64, 1024) 即 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
# 使用全连接层作为输出层
# 输出的形状 == (批大小,vocab)
x = self.fc(output)
# print("x3.shape",x.shape) #(64, 4935) 即 (BATCH_SIZE, 目标序列的不重复单词的总数作为目标序列的字典大小)
return x, state, attention_weights
#调用:
decoder = Decoder(vocab_tar_size, embedding_dim, units, BATCH_SIZE)
# sample_decoder_output, _, _ = decoder(tf.random.uniform((64, 1)), sample_output, sample_hidden)
# print ('Decoder output shape: (batch_size, vocab size) {}'.format(sample_decoder_output.shape)) #(64, 4935)
"""
选取优化方法和损失函数
"""
# 选取Adam优化方法
optimizer = tf.keras.optimizers.Adam()
# 损失基本计算方法为稀疏类别交叉熵损失
# from_logits=True代表是否将预测结果预期为非 0/1 的值进行保留
# 理论来讲二分类最终的结果应该只有0/1,函数将自动将其变为0/1,from_logits=True后,值不会被改变
# reduction='none',接下来我们将自定义损失函数,reduction必须设置为None,
# 我们可以将它看作是自定义损失函数的识别属性
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
"""
tf.keras.losses.SparseCategoricalCrossentropy(
from_logits=False, reduction=losses_utils.ReductionV2.AUTO,
name='sparse_categorical_crossentropy'
)
from_logits:
y_pred是否预期为对数张量。 默认情况下,我们假设y_pred编码概率分布。
注意:使用from_logits = True可能在数值上更稳定。
import tensorflow as tf
help(tf.losses.categorical_crossentropy)
查看categorical_crossentropy函数的默认参数列表和使用方法介绍
其中形参默认为from_logits=False,网络预测值y_pred 表示必须为经过了 Softmax函数的输出值。
当 from_logits 设置为 True 时,网络预测值y_pred 表示必须为还没经过 Softmax 函数的变量 z。
from_logits=True 标志位将softmax激活函数实现在损失函数中,便不需要手动添加softmax损失函数,提升数值计算稳定性。
from_logits 指的就是是否有经过Logistic函数,常见的Logistic函数包括Sigmoid、Softmax函数。
reduction:
(可选的)
tf.keras.losses.reduction的类型,适用于损失。 默认值为自动。 AUTO表示减少选项将由使用情况决定。
在几乎所有情况下,该默认值均为SUM_OVER_BATCH_SIZE。
与tf.distribute.Strategy一起使用时,在诸如tf.keras之类的内置训练循环之外,使用AUTO或SUM_OVER_BATCH_SIZE会引发错误。
1.reduction='none':
默认值为losses_utils.ReductionV2.AUTO,即默认使用SUM_OVER_BATCH_SIZE的方式计算批量样本个数的loss的sum总和返回一个总和值。
设置'none'代表将自定义损失函数,即自定义损失函数的识别属性,代表每个样本的loss均保留返回。
2.reduction='none'
打印loss为 Tensor("sparse_categorical_crossentropy_4/weighted_loss/Mul:0", shape=(64,), dtype=float32)
shape=(64,) 代表每个样本的loss均保留返回。
3.reduction=默认值losses_utils.ReductionV2.AUTO
打印loss为 Tensor("sparse_categorical_crossentropy_7/weighted_loss/value:0", shape=(), dtype=float32)
shape=() 代表默认使用SUM_OVER_BATCH_SIZE的方式计算批量样本个数的loss的sum总和,那么返回的loss只有一个总和值。
"""
# 因为每次生成的结果都是局部结果,要和真实结果进行比较需要对真实结果进行遮掩
# 等效于对损失计算结果进行掩码
def loss_function(real, pred):
"""自定义损失函数,参数为预测结果pred和真实结果real"""
"""
1.创建mask掩码,用于对填充的字符进行掩盖,不计算进loss中,等效于对损失计算结果进行掩码
1.bool_result = tf.math.equal(real, 0):返回 False/True
tf.math.logical_not([False,True]):返回 tf.Tensor([True False], shape=(2,), dtype=bool)
2.pad_sequences填充值默认为0.0,因此只需要对填充的字符进行掩盖,只要输入是0 那么tf.math.equal(real, 0)就会返回True,
再通过logical_not即能把True的变换为False,因此便能不计算进loss中,等效于对损失计算结果进行掩码。
2.SparseCategoricalCrossentropy
1.loss_object = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction='none')
SparseCategoricalCrossentropy稀疏类别交叉熵损失,自动将其中一方不是one-hot表示的数据转换为one-hot表示。
from_logits=True:softmax激活函数实现在损失函数中,便不需要手动添加softmax损失函数
reduction='none':
默认值为losses_utils.ReductionV2.AUTO,即默认使用SUM_OVER_BATCH_SIZE的方式计算批量样本个数的loss的sum总和返回一个总和值。
设置'none'代表将自定义损失函数,即自定义损失函数的识别属性,代表每个样本的loss均保留返回。
2.reduction='none'
打印loss为 Tensor("sparse_categorical_crossentropy_4/weighted_loss/Mul:0", shape=(64,), dtype=float32)
shape=(64,) 代表每个样本的loss均保留返回。
3.reduction=默认值losses_utils.ReductionV2.AUTO
打印loss为 Tensor("sparse_categorical_crossentropy_7/weighted_loss/value:0", shape=(), dtype=float32)
shape=() 代表默认使用SUM_OVER_BATCH_SIZE的方式计算批量样本个数的loss的sum总和,那么返回的loss只有一个总和值。
"""
# 使用tf.math.equal方法对real和0进行对比
# 对结果再进行逻辑非操作生成掩码张量mask
bool_result = tf.math.equal(real, 0) #tf.math.logical_not([False,True]) 结果 tf.Tensor([ True False], shape=(2,), dtype=bool)
mask = tf.math.logical_not(bool_result)
# print("mask", mask) #Tensor("LogicalNot_4:0", shape=(64,), dtype=bool)
# print("mask.shape",mask.shape) #(64,)
# 使用基本计算方法计算损失
loss_ = loss_object(real, pred)
# print("loss_",loss_) #Tensor("sparse_categorical_crossentropy_4/weighted_loss/Mul:0", shape=(64,), dtype=float32)
# 将mask进行类型转换,使其能够进行后续操作
mask = tf.cast(mask, dtype=loss_.dtype)
# 将loss_与mask相乘即对loss_进行掩码
loss_ *= mask
# 计算loss_张量所有元素的均值
return tf.reduce_mean(loss_)
#创建检测点保存对象:
# 定义检测点(每个阶段训练的模型)保存路径
checkpoint_dir = './training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
# 使用tf.train.Checkpoint创建检测点保存对象
# 我们会在之后的训练中调用它来保存模型
checkpoint = tf.train.Checkpoint(optimizer=optimizer,
encoder=encoder,
decoder=decoder)
#---------------------------------第四步: 构建训练函数并进行训练-----------------------------------#
#构建训练函数:
@tf.function # 该装饰器使该函数自动编译张量图, 使其可以直接执行
def train_step(inp, targ, enc_hidden):
# 设定初始损失为0
loss = 0
# 开启一个用于梯度记录的上下文管理器
with tf.GradientTape() as tape:
# 调用编码器部分得到编码器输出和编码器隐层输出
enc_output, enc_hidden = encoder(inp, enc_hidden)
# print("enc_output.shape",enc_output.shape) #(64, 16, 1024) 。编码器输出 (BATCH_SIZE, 输入序列最大长度句子的长度, 隐藏层中的隐藏神经元数量)
# print("enc_hidden.shape",enc_hidden.shape) #(64, 1024)。 编码器输出的隐藏状态 (BATCH_SIZE, 隐藏层中的隐藏神经元数量)
# 将编码器隐层输出设定为解码器的初始隐层状态
dec_hidden = enc_hidden
# 以'起始符'作为解码器的第一个输入字符
dec_input = tf.expand_dims([targ_lang.word_index['']] * BATCH_SIZE, 1)
# print("dec_input.shape",dec_input.shape) #(64, 1) 64行1列,批量大小句子数为64,1列为该行句子的第1列的单词''
"""
targ.shape:(64, 11) 。dataset中目标序列tensor的形状为 (BATCH_SIZE, 目标序列最大长度句子的长度)
targ.shape[1]:11 即 目标序列最大长度句子的长度
for t in range(1, targ.shape[1]):即遍历 目标序列最大长度句子的长度
"""
# 开始循环解码过程
for t in range(1, targ.shape[1]):
# 使用解码器获得新的解码器隐层输出(状态),以及预测结果
predictions, dec_hidden, _ = decoder(dec_input, dec_hidden, enc_output)
# print("predictions.shape", predictions.shape) #(64, 4935)
# print("dec_hidden.shape", dec_hidden.shape) #(64, 1024)
"""
loss_function(targ[:, t], predictions):取所有行的第t列,即取目标序列最大长度句子的长度11中的每一列单词
"""
# 使用损失函数计算本次训练过程的损失
loss += loss_function(targ[:, t], predictions)
# 使用teacher-forcing矫正可能的错误结果
# 直接将正确结果作为下一次循环的输入
dec_input = tf.expand_dims(targ[:, t], 1)
# print("dec_input.shape", dec_input.shape) #(64, 1)
# 计算每次batch的平均损失
batch_loss = (loss / int(targ.shape[1]))
# 获得整个模型训练的参数变量
variables = encoder.trainable_variables + decoder.trainable_variables
# 使用梯度记录管理器求解整个网络的梯度,参数是loss和全部参数变量
gradients = tape.gradient(loss, variables)
# 根据梯度更新参数
optimizer.apply_gradients(zip(gradients, variables))
# 返回每次batch的平均损失
return batch_loss
"""
1.什么是teacher_forcing?
它是一种用于序列生成任务的训练技巧, 在seq2seq架构中, 根据循环神经网络理论,
解码器每次应该使用上一步的结果作为输入的一部分, 但是训练过程中,一旦上一步的结果是错误的,
就会导致这种错误被累积,无法达到训练效果, 因此,我们需要一种机制改变上一步出错的情况,
因为训练时我们是已知正确的输出应该是什么,因此可以强制将上一步结果设置成正确的输出,
这种方式就叫做teacher_forcing.
2.teacher_forcing的作用:
能够在训练的时候矫正模型的预测,避免在序列生成的过程中误差进一步放大.
teacher_forcing能够极大的加快模型的收敛速度,令模型训练过程更快更平稳.
"""
""" 进行训练并打印日志 """
# 设置训练轮数
EPOCHS = 20
for epoch in range(EPOCHS):
# 获得每轮训练的开始时间
start = time.time()
# 初始化编码器隐含状态
enc_hidden = encoder.initialize_hidden_state()
#编码器输入的隐藏状态:(BATCH_SIZE, 隐藏层中的隐藏神经元数量)
print("enc_hidden",enc_hidden.shape) #(64, 1024)
# 初始化总损失为0
total_loss = 0
"""
steps_per_epoch:批量次数,即一个epoch中遍历BATCH_SIZE批量大小数据的次数
for (遍历次数, (输入数据, 目标标签数据)) in enumerate(dataset.take(批量次数))
"""
print("steps_per_epoch",steps_per_epoch) #375
# 循环数据集中的每个批次进行训练
for (batch, (inp, targ)) in enumerate(dataset.take(steps_per_epoch)):
# print("inp.shape",inp.shape) #(64, 16) 。dataset中输入序列tensor的形状为 (BATCH_SIZE, 输入序列最大长度句子的长度)
# print("targ.shape",targ.shape) #(64, 11) 。dataset中目标序列tensor的形状为 (BATCH_SIZE, 目标序列最大长度句子的长度)
# 调用train_step函数获得批次平均损失
batch_loss = train_step(inp, targ, enc_hidden)
# 将批次平均损失相加获得轮数总损失
total_loss += batch_loss
# 每100个批次打印一次批次平均损失
if batch % 100 == 0:
print('Epoch {} Batch {} Loss {:.4f}'.format(epoch + 1, batch, batch_loss.numpy()))
# 每两轮(epoch),保存一次模型
if (epoch + 1) % 2 == 0:
checkpoint.save(file_prefix = checkpoint_prefix)
# 打印轮数平均损失
print('Epoch {} Loss {:.4f}'.format(epoch + 1, total_loss / steps_per_epoch))
# 打印模型训练耗时
print('Time taken for 1 epoch {} sec\n'.format(time.time() - start))
#---------------------------------第五步:构建评估函数并进行预测分析-----------------------------------#
#构建评估函数:
def evaluate(sentence):
"""
description: 评估函数
:param sentence: 待翻译的句子
"""
# 初始化用于绘制注意力效果图的张量
attention_plot = np.zeros((max_length_targ, max_length_inp))
# 下面将对输入文本做与训练语料同样的操作
# 对输入的句子进行文本预处理
sentence = preprocess_sentence(sentence)
# 使用输入数值映射器对文本进行数值化映射
inputs = [inp_lang.word_index[i] for i in sentence.split(' ')]
# 对文本进行最大长度补齐
inputs = tf.keras.preprocessing.sequence.pad_sequences([inputs], maxlen=max_length_inp, padding='post')
# 转换成张量
inputs = tf.convert_to_tensor(inputs)
# 定义翻译后的结果变量
result = ''
# 初始化编码器的隐层状态
hidden = [tf.zeros((1, units))]
# 使用编码器进行编码
enc_out, enc_hidden = encoder(inputs, hidden)
# 将编码器隐层输出设定为解码器的初始隐层状态
dec_hidden = enc_hidden
# 以'起始符'作为解码器的第一个输入字符
dec_input = tf.expand_dims([targ_lang.word_index['']], 0)
# 开始循环解码,过程和训练时十分类似
for t in range(max_length_targ):
predictions, dec_hidden, attention_weights = decoder(dec_input, dec_hidden, enc_out)
# 存储每次产生的注意力权重以便后面制图
attention_weights = tf.reshape(attention_weights, (-1, ))
attention_plot[t] = attention_weights.numpy()
# 从预测分布中获得概率最大预测id
predicted_id = tf.argmax(predictions[0]).numpy()
# 使用目标数值映射器将其还原为对应的文本(单词/标识)
result += targ_lang.index_word[predicted_id] + ' '
# 如果解码还原后为'终止符',则返回结果
if targ_lang.index_word[predicted_id] == '':
return result, sentence, attention_plot
# 否则,预测id被输送回模型,作为下一次预测输入
dec_input = tf.expand_dims([predicted_id], 0)
# 返回预测结果,原始输入文本,以及整个过程的注意力张量组合(仍然是一个张量)
return result, sentence, attention_plot
def plot_attention(attention, sentence, predicted_sentence):
"""
description: 注意力张量制图函数
:param attention: 整个过程的注意力张量组合
:param sentence: 原始输入文本
:param predicted_sentence: 预测结果
"""
# 打开一个10x10的画布
fig = plt.figure(figsize=(10,10))
# 在画布上创建1x1的子画布
ax = fig.add_subplot(1, 1, 1)
# 绘制子画布上绘制矩形,根据给出输入数值不同,颜色也不相同
# cmap='viridis'是一种矩形的色彩填充方案‘绿藻’
ax.matshow(attention, cmap='viridis')
# 定义字体规范字典,这里我们只要求字体大小即可
fontdict = {'fontsize': 14}
# 使用x轴设置方法使输入文本在x轴上显示
# rotation=90代表有90度的倾斜角,便于观看
ax.set_xticklabels([''] + sentence, fontdict=fontdict, rotation=90)
# 同样,使预测文本在y轴上显示
ax.set_yticklabels([''] + predicted_sentence, fontdict=fontdict)
# 设置x和y轴的刻度,与绘制时add_subplot的参数对应即可
ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
# 绘图
plt.show()
#调用:
def translate(sentence):
"""预测并绘图"""
# 调用评估函数获得结果
result, sentence, attention_plot = evaluate(sentence)
# 打印输入文本和预测文本
print('Input: %s' % (sentence))
print('Predicted translation: {}'.format(result))
# 根据文本长度对注意力张量进行剪裁(剪裁掉的都是0部分)
attention_plot = attention_plot[:len(result.split(' ')), :len(sentence.split(' '))]
# 进行注意力效果图绘制
plot_attention(attention_plot, sentence.split(' '), result.split(' '))
# 使用检测点对象恢复最近一次保存的模型
checkpoint.restore(tf.train.latest_checkpoint(checkpoint_dir))
# 翻译西班牙语‘这里很冷。’
translate(u'hace mucho frio aqui.')
# 翻译西班牙语‘这就是我的生活。’
translate(u'esta es mi vida.')
# 翻译西班牙语‘他们还在家里吗?’
translate(u'¿todavia estan en casa?')
# 翻译西班牙语‘尝试找出答案。’
translate(u'trata de averiguarlo.')