机器翻译 MXNet(使用含注意力机制的编码器—解码器,即 Encoder编码器-Decoder解码器框架 + Attention注意力机制)

日萌社

人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)


Encoder编码器-Decoder解码器框架 + Attention注意力机制

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part1

Pytorch:Transformer(Encoder编码器-Decoder解码器、多头注意力机制、多头自注意力机制、掩码张量、前馈全连接层、规范化层、子层连接结构、pyitcast) part2

Pytorch:使用Transformer构建语言模型

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编码器来提取特征,最后通过全连接层网络来拟合分类。


one-hot向量化中实现的循环神经网络:
	1.输入数据inputs:
		输入数据数据为List列表,列表的元素数量为时间步数,每个元素(时间步t)为(批量⼤小,词典⼤小)形状的矩阵。
		inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵。
	2.输入隐藏状态state:
		初始化的隐藏状态为元组,元祖中可包含多个(批量大小, 隐藏单元个数)形状的NDArray。
		定义init_rnn_state函数来返回初始化的隐藏状态。由一个形状为(批量大小, 隐藏单元个数)的值为0的NDArray组成的元组。
		使用元组是为了更便于处理隐藏状态含有多个NDArray的情况。
	3.输出数据outputs:
		输入数据和输出数据的形状相同。
		输出数据为List列表,列表的元素数量为时间步数,每个元素(时间步t)为(批量⼤小,词典⼤小)形状的矩阵。
		inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵
	4.输出隐藏状态state:
		输入隐藏状态和输出隐藏状态的形状相同。
		输出隐藏状态为元组,元祖中可包含多个(批量大小, 隐藏单元个数)形状的NDArray。
		由一个形状为(批量大小, 隐藏单元个数)的值为0的NDArray组成的元组。使用元组是为了更便于处理隐藏状态含有多个NDArray的情况。
		对于门控循环单元来说,state列表中只含一个元素,即隐藏状态;如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。

1.最终inputs变量为一个List列表,列表中元素数量即为时间步数。
2.列表中的一个元素,实际是这个批量中所有多个样本的相同时间步t都构建到同一个矩阵(一维数组)中,
  列表中的每个元素代表这个批量中所有多个样本的每个相同时间步t。
3.我们一开始取样的数据的形状为(批量大小,时间步),批量大小即一个批量中的样本数,此处每个样本都有相同的时间步数。
  把(批量大小,时间步)转置为(时间步,批量大小)的形状之后,然后把每个样本的相同的时间步t都构建到同一个矩阵(一维数组)中,
  然后这个矩阵(一维数组)中的每个元素进行one-hot向量化,即一个元素值转换为一个一维数组,这个一维数组的大小为词典大小,
  然后把一维数组中“和这个元素值值相同”的索引值位置上的元素置为1,其他的都置为0。
  最终(批量大小,时间步)形状的批量数据经过one-hot向量化之后,变成了一个List列表,列表中元素总数等于样本中的时间步数,
  每个元素为(批量⼤小,词典⼤小)形状的矩阵。
4.例子:对批量中每个样本的多个时间步进行one-hot向量化
	>>> from mxnet import nd
	>>> X = nd.arange(10).reshape((2, 5))
	>>> X
		[[0. 1. 2. 3. 4.]
		 [5. 6. 7. 8. 9.]]
		<NDArray 2x5 @cpu(0)>
	>>> X.T
		[[0. 5.]
		 [1. 6.]
		 [2. 7.]
		 [3. 8.]
		 [4. 9.]]
		<NDArray 5x2 @cpu(0)>
	>>> for x in X.T:
	...     x
		[0. 5.]
		<NDArray 2 @cpu(0)>
		[1. 6.]
		<NDArray 2 @cpu(0)>
		[2. 7.]
		<NDArray 2 @cpu(0)>
		[3. 8.]
		<NDArray 2 @cpu(0)>
		[4. 9.]
		<NDArray 2 @cpu(0)>
		
	>>> for x in X.T:
	...     nd.one_hot(x, 10)
		[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
		 [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]]
		<NDArray 2x10 @cpu(0)>
		[[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
		 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]]
		<NDArray 2x10 @cpu(0)>
		[[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
		 [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]
		<NDArray 2x10 @cpu(0)>
		[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
		 [0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]]
		<NDArray 2x10 @cpu(0)>
		[[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
		 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]]
		<NDArray 2x10 @cpu(0)>

#含单隐藏层、num_hiddens隐藏单元个数为256
rnn_layer = rnn.RNN(num_hiddens=256) 

#1.batch_size批量大小即是指单个批量中的样本数
#2.成员函数begin_state返回initialize()初始化后的隐藏状态,返回的变量是一个List列表,此时的List列表中只有一个元素。
#3.List列表中第一个元素为初始化的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
#  对于门控循环单元来说,state列表中只含一个元素,即隐藏状态;如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
state = rnn_layer.begin_state(batch_size=2)
#state隐藏状态为列表,state[0]即列表中第一个元素的形状(1, 2, 256) 代表 (隐藏层个数, 批量⼤小, 隐藏单元个数)
#对于门控循环单元来说,state列表中只含一个元素,即隐藏状态;如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
state[0].shape #(1, 2, 256) 

#1.上一节one-hot向量化中实现的循环神经网络中,inputs和outputs皆为num_steps个形状为(batch_size, vocab_size)的矩阵,
#  即inputs和outputs均为List列表,列表的元素数量为时间步数,每个元素(时间步t)为(批量⼤小,词典⼤小)形状的矩阵。
#2.与上一节中实现的循环神经网络不同,这里rnn_layer的输入形状为(时间步数, 批量大小, 输入个数)。其中输入个数即one-hot向量长度(词典大小),
#  即(时间步数, 批量大小, 词典⼤小)。state输入隐藏状态为列表,state[0]即列表中第一个元素的形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)。
#  对于门控循环单元来说,state列表中只含一个元素,即隐藏状态;如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
#3.此外,rnn_layer作为Gluon的rnn.RNN实例,在通过rnn_layer(输入数据, 隐藏状态)前向计算后会分别返回输出outputs和隐藏状态state_new,
#  其中输出outputs指的是隐藏层在各个时间步上计算并输出的隐藏状态,它们通常作为后续输出层的输入。
#  需要强调的是,该“输出”本身并不涉及输出层计算,形状为(时间步数, 批量大小, 隐藏单元个数),该前向计算并不涉及输出层计算。
#  而rnn.RNN实例在前向计算返回的隐藏状态指的是隐藏层在最后时间步的可用于初始化下一时间步的隐藏状态:当隐藏层有多层时,每一层的隐藏状态都会记录在该变量中;
#  对于像长短期记忆这样的循环神经网络,该变量还会包含其他信息。我们会在本章的后面介绍长短期记忆和深度循环神经网络。

num_steps = 35 #时间步数
X = nd.random.uniform(shape=(num_steps, batch_size, vocab_size)) #(时间步数, 批量大小, 词典⼤小)
#state隐藏状态为列表,state[0]即列表中第一个元素的形状(1, 2, 256)代表(隐藏层个数, 批量⼤小, 隐藏单元个数)
#rnn_layer(输入数据, 隐藏状态)前向计算后会分别返回输出outputs和隐藏状态state_new
Y, state_new = rnn_layer(X, state) 
#输出outputs形状为(时间步数, 批量大小, 隐藏单元个数)
#输出隐藏状态state_new为列表,state[0]即列表中第一个元素的形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
#对于门控循环单元来说,state列表中只含一个元素,即隐藏状态;如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
Y.shape, len(state_new), state_new[0].shape #((35, 2, 256), 1, (1, 2, 256))

in_vocab:输入法语词汇表Vocabulary对象
out_vocab:输出英语词汇表Vocabulary对象
dataset:包含每个法语样本中的词索引序列和每个英语样本中的词索引序列
dataset[0]:同时获取第一个法语样本和第一个英语样本中的词索引序列

#嵌入层的权重是一个矩阵,其行数为词典大小vocab_size,列数为每个词向量的维度embed_size
self.embedding = nn.Embedding(vocab_size, embed_size)
#设门控循环单元的隐藏层num_layers个数为2,隐藏单元num_hiddens个数为16
self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=drop_prob)

def forward(self, inputs, state):
	#将法语输入每个样本的词索引通过词嵌入层Embedding得到词的表征
	#inputs输入形状是(批量大小, 时间步数),输出形状为(批量大小, 词数, 词向量维度),批量大小即样本数/样本维,时间步数即词数
	#swapaxes(0, 1)将输出互换样本维和时间步维,输出形状变换为(词数, 批量大小, 词向量维度)
        	embedding = self.embedding(inputs).swapaxes(0, 1)
	#1.将提取的词的表征(词数, 批量大小, 词向量维度)输入到一个多层门控循环单元中
	#  同时还输入初始化后的隐藏状态List列表,第一个元素形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
	#2.编码器对该输入执行前向计算后返回的输出output和最终时间步的多层隐藏状态state。
	#  输出output形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。
	#  输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制中将输出output作为键项和值项。
	#  对于门控循环单元来说,前向计算返回的最终时间步的多层隐藏状态state实际为列表,并且列表中只含一个元素,该元素才是真正的多层隐藏状态;
	#  最终时间步的多层隐藏状态可用作为初始化下一时间步的隐藏状态。如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
        	return self.rnn(embedding, state)

def begin_state(self, *args, **kwargs):
	#传入batch_size批量大小,返回initialize()初始化后的隐藏状态是一个List列表。
	#List列表中第一个元素为初始化的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
	#对于门控循环单元来说,state列表中只含一个元素,即隐藏状态。如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
	return self.rnn.begin_state(*args, **kwargs)

MXNet中的 编码器-解码器-注意力机制
        1.GRU在前向计算后会分别返回输出output(即最后一层的隐藏层在各个时间步上计算并输出的隐藏状态)和最终时间步的多层隐藏状态state,
          其中前向计算后返回的输出output指的是最后一层的隐藏层在各个时间步上计算并输出的隐藏状态,shape为(时间步数, 批量大小, 隐藏单元个数),
          该输出本身通常作为后续输出层的输入,因此该输出本身就不涉及输出层计算,而注意力机制通常将该输出本身同时作为键项K和值项V。
        2.其中GRU前向计算后返回的最终时间步的多层隐藏状态state指的是隐藏层在最后时间步的“可用于初始化下一时间步的”隐藏状态,
          shape为(隐藏层个数, 批量大小, 隐藏单元个数):当隐藏层有多层时,每一层的隐藏状态都会记录到该变量中。
          注意:最终时间步的多层隐藏状态state为一个列表。
          对于门控循环单元:state[0]即能取出“shape为(隐藏层个数, 批量大小, 隐藏单元个数)的最终时间步的”多层隐藏状态。
          对于像长短期记忆这样的循环神经网络:state列表不仅包含多层隐藏状态,还包含记忆细胞。


《走向TensorFlow 2.0:深度学习应用编程快速入门》中chineseChatbotWeb项目的注意力机制应用

    1.MXNet中的 编码器-解码器-注意力机制
        1.GRU在前向计算后会分别返回输出output(即最后一层的隐藏层在各个时间步上计算并输出的隐藏状态)和最终时间步的多层隐藏状态state,
          其中前向计算后返回的输出output指的是最后一层的隐藏层在各个时间步上计算并输出的隐藏状态,shape为(时间步数, 批量大小, 隐藏单元个数),
          该输出本身通常作为后续输出层的输入,因此该输出本身就不涉及输出层计算,而注意力机制通常将该输出本身同时作为键项K和值项V。
        2.其中GRU前向计算后返回的最终时间步的多层隐藏状态state指的是隐藏层在最后时间步的“可用于初始化下一时间步的”隐藏状态,
          shape为(隐藏层个数, 批量大小, 隐藏单元个数):当隐藏层有多层时,每一层的隐藏状态都会记录到该变量中。
          注意:最终时间步的多层隐藏状态state为一个列表。
          对于门控循环单元:state[0]即能取出“shape为(隐藏层个数, 批量大小, 隐藏单元个数)的最终时间步的”多层隐藏状态。
          对于像长短期记忆这样的循环神经网络:state列表不仅包含多层隐藏状态,还包含记忆细胞。
    2.当前BahdanauAttention类中下面的call函数的传入值实际代表意义如下:
        1.query(查询项Q):
                1.第一次计算注意力:编码器Encode的GRU在前向计算后返回的最终时间步的多层隐藏状态state
                2.第一次之后的计算注意力:上一个时间步解码器Decode的隐藏层状态
        2.values(键项K/值项V):编码器Encode的GRU在前向计算后返回的“最后一层的隐藏层在各个时间步上计算并输出的”隐藏状态 


1.c = attention_forward(self.attention, enc_states, state[0][-1])
  在解码器的前向计算attention_forward中,先通过注意⼒机制计算得到当前时间步的背景向量c。
  attention:其中函数a定义⾥向量v的⻓度是⼀个超参数
  enc_states:键项和值项均为编码器在所有时间步的隐藏状态,形状为(时间步数, 批量⼤小, 隐藏单元个数)
  state[0][-1]:查询项为解码器在上⼀时间步的隐藏状态,形状为(批量⼤小, 隐藏单元个数)
2.input_and_c = nd.concat(self.embedding(cur_input), c, dim=1)
  由于解码器的输⼊cur_input来⾃输出语⾔的词索引,我们将输⼊通过词嵌⼊层embedding(cur_input)得到表征,
  然后和背景向量c在特征维dim=1连结得到input_and_c。
3.output, state = self.rnn(input_and_c.expand_dims(0), state)
  为输⼊和背景向量的连结input_and_c增加时间步维expand_dims(0),时间步个数为1。
  我们将连结后的结果input_and_c与上⼀时间步的隐藏状态state,
  通过⻔控循环单元计算出当前时间步的输出output与隐藏状态state。
4.output = self.out(output).squeeze(axis=0)
  将输出output通过全连接层out=nn.Dense变换为有关各个输出词的预测,
  移除时间步维squeeze(axis=0),输出形状为(批量⼤⼩, 输出词典⼤⼩)

import collections
import io
import math
from mxnet import autograd, gluon, init, nd
from mxnet.contrib import text
from mxnet.gluon import data as gdata, loss as gloss, nn, rnn

PAD, BOS, EOS = '<pad>', '<bos>', '<eos>'


# 将一个序列中所有的词记录在all_tokens中以便之后构造词典,然后在该序列后面添加PAD直到序列
# 长度变为max_seq_len,然后将序列保存在all_seqs中
def process_one_seq(seq_tokens, all_tokens, all_seqs, max_seq_len):
    all_tokens.extend(seq_tokens)
    seq_tokens += [EOS] + [PAD] * (max_seq_len - len(seq_tokens) - 1)
    all_seqs.append(seq_tokens)

	
# 使用所有的词来构造词典。并将所有序列中的词变换为词索引后构造NDArray实例
def build_data(all_tokens, all_seqs):
    vocab = text.vocab.Vocabulary(collections.Counter(all_tokens),
                                  reserved_tokens=[PAD, BOS, EOS])
    indices = [vocab.to_indices(seq) for seq in all_seqs]
    return vocab, nd.array(indices)
	
	
def read_data(max_seq_len):
    # in和out分别是input和output的缩写
    in_tokens, out_tokens, in_seqs, out_seqs = [], [], [], []
    with io.open('fr-en-small.txt') as f:
        lines = f.readlines()
    for line in lines:
        in_seq, out_seq = line.rstrip().split('\t')
        in_seq_tokens, out_seq_tokens = in_seq.split(' '), out_seq.split(' ')
        if max(len(in_seq_tokens), len(out_seq_tokens)) > max_seq_len - 1:
            continue  # 如果加上EOS后长于max_seq_len,则忽略掉此样本
        process_one_seq(in_seq_tokens, in_tokens, in_seqs, max_seq_len)
        process_one_seq(out_seq_tokens, out_tokens, out_seqs, max_seq_len)
    in_vocab, in_data = build_data(in_tokens, in_seqs)
    out_vocab, out_data = build_data(out_tokens, out_seqs)
    return in_vocab, out_vocab, gdata.ArrayDataset(in_data, out_data)
	
	
max_seq_len = 7
in_vocab, out_vocab, dataset = read_data(max_seq_len)
#print(out_vocab.token_to_idx[EOS]) #3
#print(out_vocab.token_to_idx[PAD]) #1
dataset[0]
#(
# [ 6.  5. 46.  4.  3.  1.  1.]
# <NDArray 7 @cpu(0)>, 
# [ 9.  5. 28.  4.  3.  1.  1.]
# <NDArray 7 @cpu(0)>)


class Encoder(nn.Block):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 drop_prob=0, **kwargs):
        super(Encoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=drop_prob)

    def forward(self, inputs, state):
        # 输入形状是(批量大小, 时间步数)。将输出互换样本维和时间步维
        #将法语输入每个样本的词索引通过词嵌入层Embedding得到词的表征
        #inputs输入形状是(批量大小, 时间步数),输出形状为(批量大小, 词数, 词向量维度),批量大小即样本数/样本维,时间步数即词数
        #swapaxes(0, 1)将输出互换样本维和时间步维,输出形状变换为(词数, 批量大小, 词向量维度)
        embedding = self.embedding(inputs).swapaxes(0, 1)
        #1.将提取的词的表征(词数, 批量大小, 词向量维度)输入到一个多层门控循环单元中
        #  同时还输入初始化后的隐藏状态List列表,第一个元素形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
        #2.编码器对该输入执行前向计算后返回的输出output和最终时间步的多层隐藏状态state。
        #  输出output形状为(时间步数, 批量大小, 隐藏单元个数)。门控循环单元在最终时间步的多层隐藏状态的形状为(隐藏层个数, 批量大小, 隐藏单元个数)。
        #  输出指的是最后一层的隐藏层在各个时间步的隐藏状态,并不涉及输出层计算。注意力机制中将输出output作为键项和值项。
        #  对于门控循环单元来说,前向计算返回的最终时间步的多层隐藏状态state实际为列表,并且列表中只含一个元素,该元素才是真正的多层隐藏状态;
        #  最终时间步的多层隐藏状态可用作为初始化下一时间步的隐藏状态。如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
        #3.门控循环单元执行前向计算后返回的输出output和最终时间步的多层隐藏状态state
        return self.rnn(embedding, state)

    def begin_state(self, *args, **kwargs):
        #传入batch_size批量大小,返回initialize()初始化后的隐藏状态是一个List列表。
        #List列表中第一个元素为初始化的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
        #对于门控循环单元来说,state列表中只含一个元素,即隐藏状态。如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
        return self.rnn.begin_state(*args, **kwargs)
		
		
encoder = Encoder(vocab_size=10, embed_size=8, num_hiddens=16, num_layers=2)
encoder.initialize()
output, state = encoder(nd.zeros((4, 7)), encoder.begin_state(batch_size=4))
output.shape, state[0].shape #((7, 4, 16), (2, 4, 16))


dense = nn.Dense(2, flatten=False)
dense.initialize()
dense(nd.zeros((3, 5, 7))).shape #(3, 5, 2)
 
 
def attention_model(attention_size):
    model = nn.Sequential()
    model.add(nn.Dense(attention_size, activation='tanh', use_bias=False,flatten=False),
              nn.Dense(1, use_bias=False, flatten=False))
    return model


def attention_forward(model, enc_states, dec_state):
    #将解码器隐藏状态广播到和编码器隐藏状态形状相同后进行连结。
    #把代表查询项的“为解码器在上一时间步的”隐藏状态的形状(批量大小, 隐藏单元个数)变换为(时间步数, 批量大小, 隐藏单元个数)。
    dec_states = nd.broadcast_axis(dec_state.expand_dims(0), axis=0, size=enc_states.shape[0])
    #print("dec_states:",dec_states.shape) #(7, 2, 64)
    
    #键项和值项均为编码器在所有时间步的隐藏状态enc_states,形状为(时间步数, 批量大小, 隐藏单元个数)
    #enc_states和dec_states均为(7, 2, 64),因此concat第三维之后变成(7, 2, 128)
    enc_and_dec_states = nd.concat(enc_states, dec_states, dim=2)
    #print("enc_and_dec_states:",enc_and_dec_states.shape) #(7, 2, 128)
    
    #使用了Dense,设置输出层的输出个数为1,因为flatten=False,所以全连接层只对输入的最后一维做仿射变换,(7, 2, 128)变成(7, 2, 1)
    e = model(enc_and_dec_states)  # 形状为(时间步数, 批量大小, 1)
    #print("e:",e.shape) #(7, 2, 1)
    
    alpha = nd.softmax(e, axis=0)  # 在时间步维度做softmax运算
    #print("alpha:",alpha.shape) #(7, 2, 1)
    #print("enc_states:",enc_states.shape) #(7, 2, 64)
    #print("alpha * enc_states:",(alpha * enc_states).shape) #(7, 2, 64)
    
    #(时间步数, 批量大小, 1) * (时间步数, 批量大小, 隐藏单元个数) = (时间步数, 批量大小, 隐藏单元个数)
    #(时间步数, 批量大小, 隐藏单元个数).sum(axis=0) 变成 (批量大小, 隐藏单元个数)
    return (alpha * enc_states).sum(axis=0)  # 返回背景变量



seq_len, batch_size, num_hiddens = 10, 4, 8
model = attention_model(10)
model.initialize()
enc_states = nd.zeros((seq_len, batch_size, num_hiddens))
dec_state = nd.zeros((batch_size, num_hiddens))
attention_forward(model, enc_states, dec_state).shape #(4, 8)


class Decoder(nn.Block):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 attention_size, drop_prob=0, **kwargs):
        super(Decoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.attention = attention_model(attention_size)
        self.rnn = rnn.GRU(num_hiddens, num_layers, dropout=drop_prob)
        self.out = nn.Dense(vocab_size, flatten=False)

    def forward(self, cur_input, state, enc_states):
        #print("state.class:",type(state)) # <class 'list'>
        #print("state[0].class:",type(state[0])) #<class 'mxnet.ndarray.ndarray.NDArray'>
        #print("state[0]:",state[0].shape) #(2, 2, 64)
        #print("state[0][-1]:",state[0][-1].shape) #(2, 64)。[-1]取三维数组中的后两维。
 
        #1.使用注意力机制计算背景向量。注意力机制返回当前时间步的背景变量c,形状为(批量大小, 隐藏单元个数),即(2, 64)。
        #2.传入 注意力超参数attention、形状为(时间步数, 批量大小, 隐藏单元个数)的编码器在所有时间步的隐藏状态enc_states、
        #  state[0][-1]代表查询项的“为解码器在上一时间步的”隐藏状态,形状为(批量大小, 隐藏单元个数)
        c = attention_forward(self.attention, enc_states, state[0][-1])
        
        #1.将嵌入后的输入和背景向量c在特征维连结。
        #2.输入数据cur_input为真实标签序列的词索引,形状为(2,),即(时间步,)。
        #  由于解码器的输入cur_input来自输出语言的词索引,我们将输入通过词嵌入层embedding得到表征(2, 64),即(批量大小,词向量维度)
        #3.然后形状为(2, 64)的词表征和形状为(2, 64)的背景向量c在dim=1的特征维上连结 得出形状为(2, 128)。
        #print("embedding(cur_input).shape:",self.embedding(cur_input).shape) # (2, 64)
        #print("c:",c) # (2, 64)
        
        input_and_c = nd.concat(self.embedding(cur_input), c, dim=1)
        #print("input_and_c:",input_and_c.shape) #(2, 128)
        #print("input_and_c.expand_dims(0):",input_and_c.expand_dims(0).shape) #(1, 2, 128)
        
        #1.为输入和背景向量的连结input_and_c增加时间步维,时间步个数为1,时间步个数即词数,即变为(词数/时间步数, 批量大小, 词向量维度+隐藏单元个数)
        #  我们将连结后的结果与上一时间步的隐藏状态state 通过门控循环单元 计算出当前时间步的输出output与隐藏状态state。
        #2.解码器的前向计算输出的output形状为(时间步数, 批量大小, 隐藏单元个数),即(1, 2, 64)。
        #3.解码器的前向计算输出的隐藏状态state为List列表,列表中的第一个元素才是真正的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数),即(2, 2, 64)
        #  解码器的当前时间步的的前向计算输出的隐藏状态可作为下一个时间步前向计算的隐藏状态的输入。
        output, state = self.rnn(input_and_c.expand_dims(0), state)
        #print("output:",output.shape) #(1, 2, 64)
        #print("state:",len(state)) #1
        #print("state.class:",type(state)) #<class 'list'>
 
        #移除时间步维,输出形状为(批量大小, 输出词典大小)
        #因为最终还要将输出通过全连接层变换为输出词典中有关各个输出词的预测,形状为(批量大小, 输出词典大小)。
        #所以然后再经过Dense(vocab_size, flatten=False),(时间步数, 批量大小, 隐藏单元个数) 变成了 (时间步数, 批量大小, 输出词典大小),
        #最终再经过squeeze(axis=0)移除了时间步维,因此输出形状变为(批量大小, 输出词典大小)。
        output = self.out(output).squeeze(axis=0)
        return output, state

    def begin_state(self, enc_state):
        # 直接将编码器最终时间步的隐藏状态作为解码器的初始隐藏状态
        return enc_state



def batch_loss(encoder, decoder, X, Y, loss):
    #X和Y的形状都均为(批量大小,时间步数),批量大小为样本数,时间步数为每个样本中的单词数量
    #print("X:",X.shape) #(2, 7)
    #print("Y:",Y.shape) #(2, 7)
    
    batch_size = X.shape[0] #批量大小
    #传入batch_size批量大小,返回initialize()初始化后的隐藏状态是一个List列表
    #List列表中第一个元素为初始化的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数)
    #对于门控循环单元来说,state列表中只含一个元素,即隐藏状态。如果使用长短期记忆,state列表中还将包含另一个元素,即记忆细胞。
    enc_state = encoder.begin_state(batch_size=batch_size)
    #print("enc_state1:",len(enc_state)) #1
    
    #调用forward函数进行前向计算,最终门控循环单元执行前向计算后返回的输出output和最终时间步的多层隐藏状态state
    enc_outputs, enc_state = encoder(X, enc_state)
    #print("enc_state2:",len(enc_state)) #1
    #print("enc_state2.class:",type(enc_state)) #<class 'list'>
    #print("enc_outputs:",enc_outputs.shape) #(7, 2, 64) 即 (时间步数, 批量大小, 隐藏单元个数)

    #直接将编码器在最终时间步的隐藏状态作为解码器的初始隐藏状态,但这编码器和解码器的循环神经网络使用相同的隐藏层个数和隐藏单元个数
    #解码器的初始隐藏状态 同样为List列表,封装到第一个元素才是真正的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数),即(2, 2, 64)
    dec_state = decoder.begin_state(enc_state)
    #print("dec_state1:",len(dec_state)) #1
    
    # 解码器在最初时间步的输入是BOS,形状为(2,)
    dec_input = nd.array([out_vocab.token_to_idx[BOS]] * batch_size)
    # 我们将使用掩码变量mask来忽略掉标签为填充项PAD的损失
    mask, num_not_pad_tokens = nd.ones(shape=(batch_size,)), 0
    l = nd.array([0])
    
    #Y的形状为(2,7),Y.T的形状为(7,2)。2代表批量大小,7代表每个样本的元素值数量,此处每个样本的元素值为单词的索引值
    #Y为真实标签序列的批量样本数据,根据for y in Y.T循环每次同时遍历出2个样本中的相同时间步上的索引值,一共遍历7次。
    for y in Y.T:
        print("y:",y.shape) #(2,) 
        #1.dec_input:输入数据为真实标签序列的词索引,形状为(2,),即(批量大小,)。
        #  因为此处使用的是强制教学,所以每次遍历出的词索引是批量中的每个样本的同一个时间步的词索引。
        #2.注意⼒机制的输⼊包括查询项、键项和值项。设编码器和解码器的隐藏单元个数相同。
        #  注意力机制返回当前时间步的背景变量,形状为(批量大小, 隐藏单元个数)。
        #    1.dec_state:第一次是以编码器在最终时间步的隐藏状态List列表作为解码器的初始隐藏状态,之后每次都是使用解码器在上一时间步的隐藏状态List列表。
        #      但在每次的注意力计算之前,不是直接使用隐藏状态List列表,因此还要执行state[0][-1],state[0]表示获取List列表中第一个隐藏状态元素,
        #      即为(隐藏层个数, 批量⼤小, 隐藏单元个数),即(2, 2, 64),而state[0][-1]表示获取出的隐藏状态形状为(批量⼤小, 隐藏单元个数),
        #      最后再执行state.expand_dims(0)将解码器隐藏状态广播到和编码器隐藏状态形状相同,形状变为(时间步数, 批量大小, 隐藏单元个数)。
        #      这里的查询项为解码器在上一时间步的隐藏状态,形状为(批量大小, 隐藏单元个数);
        #    2.enc_outputs:编码器在所有时间步的隐藏状态,形状为(时间步数, 批量大小, 隐藏单元个数)。
        #      键项和值项均为编码器在所有时间步的隐藏状态,形状都均为(时间步数, 批量大小, 隐藏单元个数)。
        #3.dec_output:
        #  解码器的前向计算输出的output形状为(时间步数, 批量大小, 隐藏单元个数),即(1, 2, 64)。
        #  因为最终还要将输出通过全连接层变换为输出词典中有关各个输出词的预测,形状为(批量大小, 输出词典大小)。
        #  所以然后再经过Dense(vocab_size, flatten=False),(时间步数, 批量大小, 隐藏单元个数) 变成了 (时间步数, 批量大小, 输出词典大小),
        #  最终再经过squeeze(axis=0)移除了时间步维,因此输出形状变为(批量大小, 输出词典大小)。
        #4.dec_state:解码器的前向计算输出的隐藏状态dec_state为List列表,列表中的第一个元素才是真正的隐藏状态,形状为(隐藏层个数, 批量⼤小, 隐藏单元个数),即(2, 2, 64)
        #  解码器的当前时间步的的前向计算输出的隐藏状态可作为下一个时间步前向计算的隐藏状态的输入。
        dec_output, dec_state = decoder(dec_input, dec_state, enc_outputs)
        #print("dec_state2:",len(dec_state))   #1
        #print("dec_output:",dec_output.shape) #(2, 39)

        #输出词典中有关各个输出词的预测dec_output 和 样本输出序列(真实标签序列)中的对应真实时间步的单词y 放到一起通过交叉熵损失函数计算损失值
        l = l + (mask * loss(dec_output, y)).sum()
        #1.在模型训练中,所有输出序列损失的均值通常作为需要最小化的损失函数。
        #  在图10.8所描述的模型预测中,我们需要将解码器在上一个时间步的输出作为当前时间步的输入。
        #  与此不同的是,在训练中我们也可以将标签序列(训练集的真实输出标签序列)在上一个时间步的真实标签作为解码器在当前时间步的输入,
        #  这叫作强制教学(teacher forcing)。
        #2.解码器在某时间步的输入为样本输出序列(真实标签序列)在上一时间步的词,即强制教学。在编码器—解码器的训练中,可以采用强制教学。
        #3.问题:在训练中,将强制教学替换为使用解码器在上一时间步的输出作为解码器在当前时间步的输入,结果有什么变化吗?
        dec_input = y  # 使用强制教学
        num_not_pad_tokens += mask.sum().asscalar()
        #1.当遇到EOS时,序列后面的词将均为PAD,相应位置的掩码设成0
        #2.token_to_idx[EOS]的索引值实际会与矩阵大小为(2,)的y中的两个索引值逐个比较,如果token_to_idx[EOS]的索引值不等于y中的某个索引值的话,
        #  该索引值返回1,如果token_to_idx[EOS]的索引值不等于y中的某个索引值的话,该索引值返回0,即表示此时y中的该索引值代表EOS,
        #  因此该判断表达式实际返回一个大小为(2,)的矩阵,但矩阵中元素只为1或0。
        #3.如果其中包含0的大小为(2,)的矩阵跟大小为(2,)的矩阵mask相乘的话,那么mask对应位置上的元素值即为0。
        #4.只要遇到EOS的话,mask对应位置上值变为0,那么即使遍历到后面为填充项PAD的话,便会开始继续使用带有0的mask来计算,
        #  从而达到使用掩码变量mask来忽略掉标签为填充项PAD的损失。
        mask = mask * (y != out_vocab.token_to_idx[EOS])
        
    return l / num_not_pad_tokens



def train(encoder, decoder, dataset, lr, batch_size, num_epochs):
    #强制Xavier初始化
    encoder.initialize(init.Xavier(), force_reinit=True)
    decoder.initialize(init.Xavier(), force_reinit=True)
    #设置Trainer训练器,通过adam算法进行优化调整权重/偏差等超参数,,学习率为lr
    enc_trainer = gluon.Trainer(encoder.collect_params(), 'adam',{'learning_rate': lr})
    dec_trainer = gluon.Trainer(decoder.collect_params(), 'adam',{'learning_rate': lr})
    #交叉熵损失函数 softmax 和 cross-entropy 放在一起使用, 可以大大减少梯度求解的计算量
    loss = gloss.SoftmaxCrossEntropyLoss()
    data_iter = gdata.DataLoader(dataset, batch_size, shuffle=True)
    for epoch in range(num_epochs):
        l_sum = 0.0
        for X, Y in data_iter:
            with autograd.record():
                #X和Y的形状都均为(批量大小,时间步数),批量大小为样本数,时间步数为每个样本中的单词数量
                l = batch_loss(encoder, decoder, X, Y, loss)
            l.backward()
            enc_trainer.step(1)
            dec_trainer.step(1)
            l_sum += l.asscalar()
        if (epoch + 1) % 10 == 0:
            print("epoch %d, loss %.3f" % (epoch + 1, l_sum / len(data_iter)))



#每个词向量的维度embed_size、隐藏层个数num_layers、隐藏单元个数num_hiddens
embed_size, num_hiddens, num_layers = 64, 64, 2
#注意力超参数attention_size、丢弃率drop_prob、学习率lr、批量大小(单个批量中的样本数)batch_size、训练周期num_epochs
attention_size, drop_prob, lr, batch_size, num_epochs = 10, 0.5, 0.01, 2, 50
#print("in_vocab",len(in_vocab)) #输入词典大小,即47个单词
#print("out_vocab",len(out_vocab))#输出词典大小,即39个单词
#print("BOS",out_vocab.token_to_idx[BOS])#词索引为2
#print("EOS",out_vocab.token_to_idx[EOS])#词索引为3
#print("PAD",out_vocab.token_to_idx[PAD])#词索引为1

#传入 输入词典大小、每个词向量的维度、隐藏单元个数、隐藏层个数、丢弃率
encoder = Encoder(len(in_vocab), embed_size, num_hiddens, num_layers,drop_prob)
#传入 输出词典大小、每个词向量的维度、隐藏单元个数、隐藏层个数、注意力超参数、丢弃率
decoder = Decoder(len(out_vocab), embed_size, num_hiddens, num_layers,attention_size, drop_prob)
train(encoder, decoder, dataset, lr, batch_size, num_epochs)



def translate(encoder, decoder, input_seq, max_seq_len):
    in_tokens = input_seq.split(' ')
    in_tokens += [EOS] + [PAD] * (max_seq_len - len(in_tokens) - 1)
    enc_input = nd.array([in_vocab.to_indices(in_tokens)])
    enc_state = encoder.begin_state(batch_size=1)
    enc_output, enc_state = encoder(enc_input, enc_state)
    dec_input = nd.array([out_vocab.token_to_idx[BOS]])
    dec_state = decoder.begin_state(enc_state)
    output_tokens = []
    for _ in range(max_seq_len):
        #1.len(out_vocab))为39,代表Vocabulary词汇表out_vocab有39个单词,包括PAD/BOS/EOS,索引分别为1/2/3
        #2.dec_output形状为1x39,代表Vocabulary词汇表39个单词每个单词所对应的概率值大小,模型则预测出每个单词的概率值,
        #  获取最大概率值所在的索引,可根据该索引获取Vocabulary词汇表相同索引上的单词。
        dec_output, dec_state = decoder(dec_input, dec_state, enc_output)
        print("dec_output:",dec_output)
        #argmax(axis=1):代表取出形状为1x39的dec_output的列值,argmax获取出最大概率值所在的索引,可根据该索引获取Vocabulary词汇表相同索引上的单词。
        #argmax的方式:代表的是贪婪搜索,而贪婪搜索不是每次都能得到最优输出序列,而本书另外最推荐的是束搜索。
        pred = dec_output.argmax(axis=1)
        print("pred:",pred)
        #根据模型预测出的Vocabulary词汇表39个单词每个单词的概率值输出,根据最大概率值所在的索引获取Vocabulary词汇表相同索引上的单词。
        pred_token = out_vocab.idx_to_token[int(pred.asscalar())]
        if pred_token == EOS:  # 当任一时间步搜索出EOS时,输出序列即完成
            break
        else:
            output_tokens.append(pred_token)
            dec_input = pred
    return output_tokens
	
	
input_seq = 'ils regardent .'
translate(encoder, decoder, input_seq, max_seq_len)
		

BLEU计算的逐步分析

import math
import collections

pred_tokens = 'they are watching .'
label_tokens = 'they are watching .'
k=2

len_pred, len_label = len(pred_tokens), len(label_tokens)
len_pred, len_label #(19, 19)
1 - len_label / len_pred #0.0

#math.exp(0)的值 1.0
score = math.exp(min(0, 1 - len_label / len_pred))
score #1.0


#1.双层for循环
#	for n in range(1, k+1): #k+1=3,遍历的n为1、2
#		for i in range(len_label - n + 1):
#			#n为1时,len_label - n + 1 = 19,遍历的i为0~18;
#			#n为2时,len_label - n + 1 = 18,遍历的i为0~17;
#			label_tokens[i: i + n] 
#			#n为1时,从label_tokens[0:1]到label_tokens[18:19] 
#			#n为2时,从label_tokens[0:2]到label_tokens[17:19] 
#		for i in range(len_pred - n + 1):
#			#n为1时,len_label - n + 1 = 19,遍历的i为0~18;
#			#n为2时,len_label - n + 1 = 18,遍历的i为0~17;


#2.collections.defaultdict(int) 输出的为 defaultdict(<class 'int'>, {})
# 返回一个新的类似字典的对象,类似于Counter字典,两者都可用于统计某值出现的次数。 
# defaultdict 是内置 dict 类的子类。它重载了一个方法并添加了一个可写的实例变量,其余的功能与 dict 类相同。
# >>> s = 'mississippi'
# >>> d = defaultdict(int)
# >>> for k in s:
# ...     d[k] += 1
# >>> sorted(d.items())
# [('i', 4), ('m', 1), ('p', 2), ('s', 4)]


#遍历的n为1、2
for n in range(1, k + 1):
	#defaultdict字典类似于Counter字典,可用于统计某值出现的次数
	num_matches, label_subs = 0, collections.defaultdict(int)
	
	for i in range(len_label - n + 1):
		#计算标签序列中字母组合出现的次数
		label_subs[''.join(label_tokens[i: i + n])] += 1
	sorted(label_subs.items())
	#n为1时,打印[(' ', 3), ('.', 1), ('a', 2), ('c', 1), ('e', 2), ('g', 1), ('h', 2), ('i', 1), ('n', 1), ('r', 1), ('t', 2), ('w', 1), ('y', 1)]
	#n为2时,打印[(' .', 1), (' a', 1), (' w', 1), ('ar', 1), ('at', 1), ('ch', 1), ('e ', 1), ('ey', 1), ('g ', 1), ('he', 1), ('hi', 1), ('in', 1), ('ng', 1), ('re', 1), ('tc', 1), ('th', 1), ('wa', 1), ('y ', 1)]
 
	for i in range(len_pred - n + 1):
		#根据预测序列中字母组合与标签序列中字母组合进行匹配,如果能匹配出字母组合并且出现次数大于0则执行继续执行下面操作
		if label_subs[''.join(pred_tokens[i: i + n])] > 0:
			#预测序列中字母组合能匹配标签序列中字母组合,则给统计相似度num_matches加1
			num_matches += 1
			#给defaultdict字典中统计的标签序列中字母组合的出现次数减1
			label_subs[''.join(pred_tokens[i: i + n])] -= 1
	
	#1.math.pow(x, y) 将返回 x 的 y 次幂。 
	#  pow(1.0, x) 和 pow(x, 0.0) 总是返回 1.0 ,即使 x 是零或NaN。 
	#  如果 x 和 y 都是有限的, x 是负数, y 不是整数那么 pow(x, y) 是未定义的,并且引发 ValueError。
	#  与内置的 ** 运算符不同, math.pow() 将其参数转换为 float类型的。而使用 ** 或内置自带的pow()函数是用来计算精确的整数幂的。
	#2.下面的公式代码实质为Pn**(0.5**n)的推导:Pn为预测序列与标签序列匹配词数为n的子序列的数量与预测序列中词数为n的子序列的数量之比,
	#  可简单理解为“预测序列与标签序列所匹配出来的”词数数量与预测序列中词数的数量之比。
	#3.n为1时,num_matches为19,math.pow(19 / (19 - 1 + 1), math.pow(0.5, 1)),实际为pow(1,0.5),结果为1.0
	#  n为2时,num_matches为18,math.pow(18 / (19 - 2 + 1), math.pow(0.5, 2)),实际为pow(1,0.25),结果为1.0
	#  因为pow(1.0, x) 总是返回 1.0
	score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
	print(score) #n为1或2时,score都为1.0

  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

あずにゃん

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值