直观理解并使用Tensorflow实现Seq2Seq模型的注意机制(下)

本文涉及的产品
模型在线服务 PAI-EAS,A10/V100等 500元 1个月
模型训练 PAI-DLC,5000CU*H 3个月
交互式建模 PAI-DSW,5000CU*H 3个月
简介: 直观理解并使用Tensorflow实现Seq2Seq模型的注意机制

添加注意力机制

注意力不仅为瓶颈问题提供了解决方案,还为句子中的每个单词赋予了权重(相当字面意义)。源序列在编码器输出中有它自己的的信息,在解码器中被预测的字在相应的解码器隐藏状态中有它自己的的信息。我们需要知道哪个编码器的输出拥有类似的信息,我们需要知道在解码器隐藏状态下,哪个编码器输出的信息与解码器隐藏状态下的相似。

因此,这些编码器输出和解码器的隐藏状态被用作一个数学函数的输入,从而得到一个注意力向量分数。当一个单词被预测时(在解码器中的每个GRU单元),这个注意力分数向量在每一步都被计算出来。该向量确定每个编码器输出的权重,以找到加权和。

注意力的一般定义:给定一组向量“值”和一个向量“查询”,注意力是一种计算基于查询的加权值和的技术。

在我们的seq2seq架构上下文中,每个解码器隐藏状态(查询)处理所有编码器输出(值),以获得依赖于解码器隐藏状态(查询)的编码器输出(值)的加权和。

加权和是值中包含的信息的选择性摘要,查询将确定关注哪些值。这个过程类似于将查询投射到值空间中,以便在值空间中查找查询(score)的上下文。较高的分数表示对应的值更类似于查询。

根据注意力机制的原始论文,解码器决定源句要注意的部分。通过让解码器有一个注意机制,我们将编码器从必须将源句中的所有信息编码为固定长度的向量的负担中解脱出来。使用这种新方法,信息可以分布在整个序列中,解码器可以相应地有选择地检索这些信息。

还记得我们刚才讨论的数学函数吗?有几种方法可以找到注意力得分(相似度)。主要有以下几点:

基本点积注意力(Dot Product Attention)

乘法注意力(multiplicative attention)

加性注意力(additive attention)

我们不会在这里逐一深入讲解。我们会在后续的文章中详细接好。现在我们将考虑基本的点积注意,因为它是最容易掌握。你已经猜到了这类注意力的作用。从名称判断,它是输入矩阵的点积。

注意,基本的点积注意有一个假设。它假设两个输入矩阵的维数在轴上要做点积的地方必须是相同的,这样才能做点积。在我们的实现中,这个维度是由超参数hidden_units给出的,对于编码器和解码器都是一样的。

640.png

上面讲了太多的理论。现在让我们回到代码!我们将定义我们的Attention类。

将编码器输出张量与解码器隐藏状态进行点积,得到注意值。这是通过Tensorflow的matmul()函数实现的。我们取上一步得到的注意力分数的softmax。这样做是为了规范化分数并在区间[0,1]内获取值。编码器输出与相应的注意分数相乘,然后相加得到一个张量。这基本上是编码器输出的加权和,通过reduce_sum()函数实现。

class BasicDotProductAttention(tf.keras.layers.Layer):
def __init__(self):
 super(BasicDotProductAttention, self).__init__()
def call(self, decoder_hidden_state, encoder_outputs):
 #Dimensions of decoder_hidden_state => (BATCH_SIZE, hidden_units)
 #Dimensions of encoder_outputs => (BATCH_SIZE, MAX_WORDS_IN_A_SENTENCE, hidden_units)
 decoder_hidden_state_with_time_axis = tf.expand_dims(decoder_hidden_state, 2)
 #Dimensions of decoder_hidden_state_with_time_axis => (BATCH_SIZE, hidden_units, 1)
 attention_scores = tf.matmul(encoder_outputs, decoder_hidden_state_with_time_axis)
 #Dimensions of attention_scores => (BATCH_SIZE, MAX_WORDS_IN_A_SENTENCE, 1)
 attention_scores = tf.nn.softmax(attention_scores, axis = 1)
 weighted_sum_of_encoder_outputs = tf.reduce_sum(encoder_outputs * attention_scores, axis = 1)
 #Dimensions of weighted_sum_of_encoder_outputs => (BATCH_SIZE, hidden_units)
 return weighted_sum_of_encoder_outputs, attention_scores

解码器Decoder ( 增加注意力机制)

代码在decoder类中增加了以下步骤。

就像编码器一样,我们在这里也有一个嵌入层用于目标语言中的序列。序列中的每一个单词都在具有相似意义的相似单词的嵌入空间中表示。

我们也得到的加权和编码器输出通过使用当前解码隐藏状态和编码器输出。这是通过调用我们的注意力层来实现的。

我们将以上两步得到的结果(嵌入空间序列的表示和编码器输出的加权和)串联起来。这个串联张量被发送到我们的解码器的GRU层。

这个GRU层的输出被发送到一个稠密层,这个稠密层给出所有hindi_vocab_size的单词出现的概率。具有高概率的单词意味着模型认为这个单词应该是下一个单词。

class Decoder(tf.keras.Model):
def __init__(self, hindi_vocab_size, embedding_dim, hidden_units):
 super(Decoder, self).__init__()
 self.embedding = tf.keras.layers.Embedding(hindi_vocab_size, embedding_dim)
 self.gru = tf.keras.layers.GRU(hidden_units, return_state = True)
 self.word_probability_layer = tf.keras.layers.Dense(hindi_vocab_size, activation = 'softmax')
 self.attention_layer = BasicDotProductAttention()
def call(self, decoder_input, decoder_hidden, encoder_sequence_output):
 x = self.embedding(decoder_input)
 #Dimensions of x => (BATCH_SIZE, embedding_dim)
 weighted_sum_of_encoder_outputs, attention_scores = self.attention_layer(decoder_hidden, encoder_sequence_output)
 #Dimensions of weighted_sum_of_encoder_outputs => (BATCH_SIZE, hidden_units)
 x = tf.concat([weighted_sum_of_encoder_outputs, x], axis = -1)
 x = tf.expand_dims(x, 1)
 #Dimensions of x => (BATCH_SIZE, 1, hidden_units + embedding_dim)
 decoder_output, decoder_state = self.gru(x)
 #Dimensions of decoder_output => (BATCH_SIZE, hidden_units)
 word_probability = self.word_probability_layer(decoder_output)
 #Dimensions of word_probability => (BATCH_SIZE, hindi_vocab_size)
 return word_probability, decoder_state, attention_scores
# initialize our decoder
decoder = Decoder(hindi_vocab_size, embedding_dim, hidden_units)

640.png

训练

我们定义我们的损失函数和优化器。选择了稀疏分类交叉熵和Adam优化器。每个训练步骤如下:

  1. 从编码器对象获取编码器序列输出和编码器最终隐藏状态。编码器序列输出用于查找注意力分数,编码器最终隐藏状态将成为解码器的初始隐藏状态。
  2. 对于目标语言中预测的每个单词,我们将输入单词、前一个解码器隐藏状态和编码器序列输出作为解码器对象的参数。返回单词预测概率和当前解码器隐藏状态。
  3. 将概率最大的字作为下一个解码器GRU单元(解码器对象)的输入,当前解码器隐藏状态成为下一个解码器GRU单元的输入隐藏状态。
  4. 损失通过单词预测概率和目标句中的实际单词计算,并向后传播

在每个epoch中,每批调用上述训练步骤,最后存储并绘制每个epoch对应的损失。

附注:在第1步,为什么我们仍然使用编码器的最终隐藏状态作为我们的解码器的第一个隐藏状态?

这是因为,如果我们这样做,seq2seq模型将被优化为一个单一系统。反向传播是端到端进行的。我们不想分别优化编码器和解码器。并且,没有必要通过这个隐藏状态来获取源序列信息,因为我们已经有注意力机制了:)

optimizer = tf.keras.optimizers.Adam(learning_rate = learning_rate)
loss_object = tf.keras.losses.SparseCategoricalCrossentropy(reduction='none')
def loss_function(actual_words, predicted_words_probability):
loss = loss_object(actual_words, predicted_words_probability)
mask = tf.where(actual_words > 0, 1.0, 0.0)
return tf.reduce_mean(mask * loss)
def train_step(english_sequences, hindi_sequences):
loss = 0
with tf.GradientTape() as tape:
 encoder_sequence_output, encoder_hidden = encoder(english_sequences)
 decoder_hidden = encoder_hidden
 decoder_input = hindi_sequences[:, 0]
 for i in range(1, hindi_sequences.shape[1]):
 predicted_words_probability, decoder_hidden, _ = decoder(decoder_input, decoder_hidden, encoder_sequence_output)
 actual_words = hindi_sequences[:, i]
 # if all the sentences in batch are completed
 if np.count_nonzero(actual_words) == 0:
 break
 loss += loss_function(actual_words, predicted_words_probability)
 decoder_input = actual_words
variables = encoder.trainable_variables + decoder.trainable_variables
gradients = tape.gradient(loss, variables)
optimizer.apply_gradients(zip(gradients, variables))
return loss.numpy()
all_epoch_losses = []
training_start_time = time.time()
for epoch in range(epochs):
epoch_loss = []
start_time = time.time()
for(batch, (english_sequences, hindi_sequences)) in enumerate(dataset):
 batch_loss = train_step(english_sequences, hindi_sequences)
 epoch_loss.append(batch_loss)
all_epoch_losses.append(sum(epoch_loss)/len(epoch_loss))
print("Epoch No.: " + str(epoch) + " Time: " + str(time.time()-start_time))
print("All Epoch Losses: " + str(all_epoch_losses))
print("Total time in training: " + str(time.time() - training_start_time))
plt.plot(all_epoch_losses)
plt.xlabel("Epochs")
plt.ylabel("Epoch Loss")
plt.show()

测试

为了测试我们的模型在经过训练后的执行情况,我们定义了一个函数,该函数接受一个英语句子,并按照模型的预测返回一个印地语句子。让我们实现这个函数,我们将在下一节中看到结果的好坏。

  1. 我们接受英语句子,对其进行预处理,并将其转换为长度为MAX_WORDS_IN_A_SENTENCE的序列或向量,如开头的“预处理数据”部分所述。
  2. 这个序列被输入到我们训练好的编码器,编码器返回编码器序列输出和编码器的最终隐藏状态。
  3. 编码器的最终隐藏状态是译码器的第一个隐藏状态,译码器的第一个输入字是一个开始标记“sentencestart”。
  4. 解码器返回预测的字概率。概率最大的单词成为我们预测的单词,并被附加到最后的印地语句子中。这个字作为输入进入下一个解码器层。
  5. 预测单词的循环将继续下去,直到解码器预测结束标记“sentenceend”或单词数量超过某个限制(我们将这个限制保持为MAX_WORDS_IN_A_SENTENCE的两倍)。
def get_sentence_from_sequences(sequences, tokenizer):
return tokenizer.sequences_to_texts(sequences)
# Testing
def translate_sentence(sentence):
sentence = preprocess_sentence(sentence, True)
sequence = english_tokenizer.texts_to_sequences([sentence])[0]
sequence = tf.keras.preprocessing.sequence.pad_sequences([sequence], maxlen = MAX_WORDS_IN_A_SENTENCE, padding = 'post')
encoder_input = tf.convert_to_tensor(sequence)
encoder_sequence_output, encoder_hidden = encoder(encoder_input)
decoder_input = tf.convert_to_tensor([hindi_word_index['sentencestart']])
decoder_hidden = encoder_hidden
sentence_end_word_id = hindi_word_index['sentenceend']
hindi_sequence = []
for i in range(MAX_WORDS_IN_A_SENTENCE*2):
 predicted_words_probability, decoder_hidden, _ = decoder(decoder_input, decoder_hidden, encoder_sequence_output)
 # taking the word with maximum probability
 predicted_word_id = tf.argmax(predicted_words_probability[0]).numpy()
 hindi_sequence.append(predicted_word_id)
 # if the word 'sentenceend' is predicted, exit the loop
 if predicted_word_id == sentence_end_word_id:
 break
 decoder_input = tf.convert_to_tensor([predicted_word_id])
print(sentence)
return get_sentence_from_sequences([hindi_sequence], hindi_tokenizer)
# print translated sentence
print(translate_sentence("Try multiple sentences here to check how good model is working!"))

结果

我们来看看结果。我运行的代码与NVidia K80 GPU Kaggle,在上面的代码。100个epoch,需要70分钟的训练。损失与epoch图如下所示。

640.png

经过35个epoch的训练后,我尝试向我们的translate_sentence()函数中添加随机的英语句子,结果有些令人满意,但也有一定的问题。显然,可以对超参数进行更多的优化。

640.png

但是在这里,超参数并不是与实际翻译有一些偏差的唯一原因。让我们对更多可以实现以使我们的模型运行得更好的点进行小讨论。

可能的改进

在实现我们的模型时,我们已经对编码器、解码器和注意力机制有了非常基本的了解。根据可用的时间和计算能力,以下是一些点,可以尝试和测试,以知道如果他们工作时,实施良好:

使用堆叠GRU编码器和解码器

使用不同形式的注意力机制

使用不同的优化器

增加数据集的大小

采用Beam Search代替Greedy decoding

我们现在所使用的的解码器被称作贪婪解码(Greedy decoding)。我们假设概率最高的单词是最终预测的单词,并输入到下一个解码器状态。这种方法的问题是没有办法撤销这个动作。Beam Search解码从单词概率分布中考虑最高k个可能的单词,并检查所有的可能性,我们会在明天的文章中介绍Beam Search。


目录
相关文章
|
20天前
|
机器学习/深度学习 TensorFlow 算法框架/工具
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
将Keras训练好的.hdf5模型转换为TensorFlow的.pb模型,然后再转换为TensorRT支持的.uff格式,并提供了转换代码和测试步骤。
53 3
深度学习之格式转换笔记(三):keras(.hdf5)模型转TensorFlow(.pb) 转TensorRT(.uff)格式
|
4天前
|
机器学习/深度学习 人工智能 算法
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
车辆车型识别,使用Python作为主要编程语言,通过收集多种车辆车型图像数据集,然后基于TensorFlow搭建卷积网络算法模型,并对数据集进行训练,最后得到一个识别精度较高的模型文件。再基于Django搭建web网页端操作界面,实现用户上传一张车辆图片识别其类型。
12 0
【车辆车型识别】Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+算法模型
|
2月前
|
机器学习/深度学习 人工智能 算法
鸟类识别系统Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+ResNet50算法模型+图像识别
鸟类识别系统。本系统采用Python作为主要开发语言,通过使用加利福利亚大学开源的200种鸟类图像作为数据集。使用TensorFlow搭建ResNet50卷积神经网络算法模型,然后进行模型的迭代训练,得到一个识别精度较高的模型,然后在保存为本地的H5格式文件。在使用Django开发Web网页端操作界面,实现用户上传一张鸟类图像,识别其名称。
92 12
鸟类识别系统Python+卷积神经网络算法+深度学习+人工智能+TensorFlow+ResNet50算法模型+图像识别
|
20天前
|
机器学习/深度学习 移动开发 TensorFlow
深度学习之格式转换笔记(四):Keras(.h5)模型转化为TensorFlow(.pb)模型
本文介绍了如何使用Python脚本将Keras模型转换为TensorFlow的.pb格式模型,包括加载模型、重命名输出节点和量化等步骤,以便在TensorFlow中进行部署和推理。
55 0
|
3月前
|
API UED 开发者
如何在Uno Platform中轻松实现流畅动画效果——从基础到优化,全方位打造用户友好的动态交互体验!
【8月更文挑战第31天】在开发跨平台应用时,确保用户界面流畅且具吸引力至关重要。Uno Platform 作为多端统一的开发框架,不仅支持跨系统应用开发,还能通过优化实现流畅动画,增强用户体验。本文探讨了Uno Platform中实现流畅动画的多个方面,包括动画基础、性能优化、实践技巧及问题排查,帮助开发者掌握具体优化策略,提升应用质量与用户满意度。通过合理利用故事板、减少布局复杂性、使用硬件加速等技术,结合异步方法与预设缓存技巧,开发者能够创建美观且流畅的动画效果。
74 0
|
3月前
|
C# 开发者 前端开发
揭秘混合开发新趋势:Uno Platform携手Blazor,教你一步到位实现跨平台应用,代码复用不再是梦!
【8月更文挑战第31天】随着前端技术的发展,混合开发日益受到开发者青睐。本文详述了如何结合.NET生态下的两大框架——Uno Platform与Blazor,进行高效混合开发。Uno Platform基于WebAssembly和WebGL技术,支持跨平台应用构建;Blazor则让C#成为可能的前端开发语言,实现了客户端与服务器端逻辑共享。二者结合不仅提升了代码复用率与跨平台能力,还简化了项目维护并增强了Web应用性能。文中提供了从环境搭建到示例代码的具体步骤,并展示了如何创建一个简单的计数器应用,帮助读者快速上手混合开发。
70 0
|
3月前
|
开发者 算法 虚拟化
惊爆!Uno Platform 调试与性能分析终极攻略,从工具运用到代码优化,带你攻克开发难题成就完美应用
【8月更文挑战第31天】在 Uno Platform 中,调试可通过 Visual Studio 设置断点和逐步执行代码实现,同时浏览器开发者工具有助于 Web 版本调试。性能分析则利用 Visual Studio 的性能分析器检查 CPU 和内存使用情况,还可通过记录时间戳进行简单分析。优化性能涉及代码逻辑优化、资源管理和用户界面简化,综合利用平台提供的工具和技术,确保应用高效稳定运行。
67 0
|
3月前
|
前端开发 开发者 设计模式
揭秘Uno Platform状态管理之道:INotifyPropertyChanged、依赖注入、MVVM大对决,帮你找到最佳策略!
【8月更文挑战第31天】本文对比分析了 Uno Platform 中的关键状态管理策略,包括内置的 INotifyPropertyChanged、依赖注入及 MVVM 框架。INotifyPropertyChanged 方案简单易用,适合小型项目;依赖注入则更灵活,支持状态共享与持久化,适用于复杂场景;MVVM 框架通过分离视图、视图模型和模型,使状态管理更清晰,适合大型项目。开发者可根据项目需求和技术栈选择合适的状态管理方案,以实现高效管理。
40 0
|
3月前
|
Apache 开发者 Java
Apache Wicket揭秘:如何巧妙利用模型与表单机制,实现Web应用高效开发?
【8月更文挑战第31天】本文深入探讨了Apache Wicket的模型与表单处理机制。Wicket作为一个组件化的Java Web框架,提供了多种模型实现,如CompoundPropertyModel等,充当组件与数据间的桥梁。文章通过示例介绍了模型创建及使用方法,并详细讲解了表单组件、提交处理及验证机制,帮助开发者更好地理解如何利用Wicket构建高效、易维护的Web应用程序。
42 0
|
3月前
|
机器学习/深度学习 API TensorFlow
深入解析TensorFlow 2.x中的Keras API:快速搭建深度学习模型的实战指南
【8月更文挑战第31天】本文通过搭建手写数字识别模型的实例,详细介绍了如何利用TensorFlow 2.x中的Keras API简化深度学习模型构建流程。从环境搭建到数据准备,再到模型训练与评估,展示了Keras API的强大功能与易用性,适合初学者快速上手。通过简单的代码,即可完成卷积神经网络的构建与训练,显著降低了深度学习的技术门槛。无论是新手还是专业人士,都能从中受益,高效实现模型开发。
24 0