【动手学深度学习-Pytorch版】序列到序列的学习(包含NLP常用的Mask技巧)

序言

这一节是对于“编码器-解码器”模型的实际应用,编码器和解码器架构可以使用长度可变的序列作为输入,并将其转换为固定形状的隐状态(编码器实现)。本小节将使用“fra-eng”数据集(这也是《动手学习深度学习-Pytorch版》提供的数据集)进行序列到序列的学习。在d2l官方文档中有很多的内容是根据英文版直译过来的,其中有很多空乏的句子,特别是对于每个模块的描述中,下面我提供一种全新的思路来理解整个代码(不得不说沐神团队的代码绝对值得推敲~)。
这里也是按照官方给的目录架构对于整个项目复现,在复现的过程中详细理解每一行代码的作用(去除无关内容~)同时关注数据的变化,特别是在源和目标的shape变化方面。当然需要注明的是源指的是数据集中所有的英语短语,其按照batch_size的大小装入模型,同时增加了num_steps维度,也就是“时间步”【那对于区分时间步和batch_size的概念有个类似的方式便于理解:将它们映射到图像中,batch_size是每一次取出多少个样本图像,而num_steps可以理解为图像本身的维度问题】。下面将会按着官方给出的步骤进行代码复现:导包、设计编码器、设计解码器、修改交叉熵损失函数、模型训练、模型预测、使用BLEU进行模型的评估。
在这里插入图片描述

模型复现

导包【无脑导包】

# 无脑导包
import torch
import collections # 这个包还是需要注意一下
import math
from torch import nn
from d2l import torch as d2l

设计编码器

根据“编码器-解码器”的模型架构,梳理出编码器的主要任务,它的主要任务包括:

  1. 将某一个时刻t的输入特征向量 x t x_t xt和上一个时刻的隐状态 h t − 1 h_{t-1} ht1转变为 h t h_t ht h t = f ( x t , h t − 1 ) h_t = f(x_t , h_{t-1}) ht=f(xt,ht1)
  2. 编码器需要通过函数q实现把所有的隐状态转变为上下文变量:
    c = q ( h 1 , . . . . , h T ) c = q( h_1,....,h_T ) c=q(h1,....,hT)
  3. 使用嵌入层获取输入序列的每个词元的特征向量[嵌入层权重矩阵行数为vocab_size,列数是特征向量的维度]

明确了编码器的主要任务后下面来看具体的代码复现:

#@save
class Seq2SeqEncoder(d2l.Encoder):"""用于序列到序列学习的循环神经网络编码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):super(Seq2SeqEncoder,self).__init__(**kwargs)# 实现嵌入层Embedding 将每一个词元转变成一个词向量self.embedding = nn.Embedding(vocab_size,embed_size)# print('Encoder中 self.embedding的size为:',self.embedding.size())# print('Encoder中 embed_size:   ',embed_size)with  open('D://pythonProject//Encoder_embed_pervir_size.txt', 'w') as f:f.write(str(embed_size))"""----------embed_size为32----------""""""这里的embed_size为每一个词元对应的特征向量的长度"""self.rnn = nn.GRU(embed_size,num_hiddens,num_layers,dropout=dropout)def forward(self, X, *args):with  open('D://pythonProject//Encoder_Not_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------未进行embedding的X: torch.Size([64, 10]) batch_size * num_steps----------"""# print('Encoder中 未进行embedding前的X的size',X.size())# embedding 的形状 (vocab_size,embed_size)# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X)# print('Encoder中 进行embedding后的X的size',X.size())with  open('D://pythonProject//Encoder_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了embedding的X: torch.Size([64, 10, 32])----------"""#torch要求在循环神经网络模型中,第一个轴对应的必须是时间步X = X.permute(1,0,2)# print('Encoder中 permute后的X的size',X.size())with  open('D://pythonProject//Encoder_permute_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了permute的X: torch.Size([10, 64, 32]) 10为时间步----------"""output,state = self.rnn(X)# output的输出形状: (num_steps,batch_size,num_hiddens)# state的输出形状: (num_layers,batch_size,num_hiddens)return output,state

在上述的编码器中,forward()完成了
1、将输入值【形状为:batch_size*num_steps】输入到嵌入层Embedding,将输入的每个词元转成一个代表该词元的一个特征向量。【之所以用Embedding而不用One-Hot的原因在于:虽然One-Hot可将tokens转成稀疏矩阵便于运算,但是不适用于大批量数据的情况,容易导致运算过慢或者占用内存的情况,详细参考:一文读懂Embedding的概念,以及它和深度学习的关系】;
1-1 注意:原来X的输入形状是
torch.Size[64,10]
—>torch.Size(bach_size,num_steps]
经过Embedding后的X的形状为
torch.Size([64, 10, 32]
—>torch.Size(batch_size,num_steps,embedding_size)
即在输入的X后增加一个维度,用来作为每一个takens的特征向量

        with  open('D://pythonProject//Encoder_Not_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------未进行embedding的X: torch.Size([64, 10]) batch_size * num_steps----------"""# print('Encoder中 未进行embedding前的X的size',X.size())# embedding 的形状 (vocab_size,embed_size)# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X)# print('Encoder中 进行embedding后的X的size',X.size())with  open('D://pythonProject//Encoder_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了embedding的X: torch.Size([64, 10, 32])----------"""

2、为了适应torch要求的循环神经网络模型中第一个维度需要为时间步的需求,这里做了一下permute操作,把第0个维度和第1个维度互换了一下,关于permute的详细操作可以参考:【PyTorch 两大转置函数 transpose() 和 permute()

permute后的矩阵形状就变成了:
torch.Size([10, 64, 32])
—>torch.size([num_steps,batch_size,embedding_size])

        #torch要求在循环神经网络模型中,第一个轴对应的必须是时间步X = X.permute(1,0,2)# print('Encoder中 permute后的X的size',X.size())with  open('D://pythonProject//Encoder_permute_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了permute的X: torch.Size([10, 64, 32]) 10为时间步----------"""

3、最后,编码器需要返回最后一个时间步的state隐状态和最后一个时间步的outputs。

        output,state = self.rnn(X)# output的输出形状: (num_steps,batch_size,num_hiddens)# state的输出形状: (num_layers,batch_size,num_hiddens)return output,state

实例化编码器

下面通过设计一个两层门控循环单元编码器,其隐藏单元是16,给定一个小批量的输入序列X(批量大小为4,时间步为7)。同时,在完成所有时间步后,最后一层的隐状态的输出是一个张量【output由编码器的循环层返回】,形状为(时间步数,批量大小,隐藏单元数)

encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape

注:这里使用的是门控循环单元GRU,最后一个时间步的多层隐状态的形状是(num_layers,batch_size,num_hiddens),如果使用LSTM 则state中还应该包含记忆单元信息。

设计解码器

编码器输出的整个上下文信息变量C需要作用于整个输入序列 x 1 , . . . , x r x_1,...,x_r x1,...,xr,对输入序列进行编码。解码器的输出 y t ′ y_t' yt与上下文变量C输出子序列 y 1 , . . . , ( y t ′ − 1 ) y_1,...,(yt'-1) y1,...,(yt1)的关系:
在这里插入图片描述
且隐状态与上一步的隐状态、上下文变量和上一个时间步的输出有关。在获得解码器的隐状态后,可以使用输出层+softmax操作来计算时间步 t ′ t' t时输出 y t ′ y_t' yt的概率分布:
在这里插入图片描述
解码器的主要任务包括:

  1. 直接使用编码器的最后一个时间步的隐状态来初始化解码器的隐状态及两者具有相同的隐藏层和隐藏单元
  2. 为了让上下文信息更好包含更多的信息,可以用上下文变量C在所有的时间步与解码器的输入进行拼接
  3. 为了输出预测词元的概率分布,在最后一层采用全连接层来变换隐状态
class Seq2SeqDecoder(d2l.Decoder):"""用于序列到序列学习的循环神经网络解码器"""def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0,**kwargs):super(Seq2SeqDecoder, self).__init__(**kwargs)self.embedding = nn.Embedding(vocab_size,embed_size)with  open('D://pythonProject//Decoder_vocab_size.txt', 'w') as f:f.write(str(vocab_size))"""----------decoder的vocab_size为201----------"""with  open('D://pythonProject//Decoder_embed_size.txt', 'w') as f:f.write(str(embed_size))"""----------decoder的embed_size为32----------"""self.rnn = nn.GRU(embed_size+num_hiddens,num_hiddens,num_layers,dropout=dropout)self.dense = nn.Linear(num_hiddens,vocab_size)def init_state(self,enc_outputs,*args):# enc_outputs[0]为编码器的输出# enc_outputs[1]为编码器最后一层输出的隐变量return enc_outputs[1]def forward(self, X, state):# print('Decoder中 未进行embedding的X的形状:',X.size())with  open('D://pythonProject//Decoder_X_size.txt', 'w') as f:f.write(str(X.size()))"""Decoder的X的大小:torch.Size([25, 10])"""# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X).permute(1,0,2)with  open('D://pythonProject//Decoder_X_embed_permute.txt', 'w') as f:f.write(str(X.size()))"""Decoder的X_embed_permute的大小:torch.Size([10, 25, 32])"""# 广播context,使其具有与X相同的num_steps 即X.shape[0]context = state[-1].repeat(X.shape[0], 1, 1)X_and_Context = torch.cat((X,context),2)output,state = self.rnn(X_and_Context,state)output = self.dense(output).permute(1,0,2)# output的形状:(batch_size,num_steps,vocab_size)# state的形状:(num_layers,batch_size,num_hiddens)return output, state

在初始化__init__()函数中完成了将输入维度(batch_size,num_steps)进行Embedding操作,其输出维度变为了(batch_size,num_steps,num_embedding)
同时,将embed+hiddens的大小同时送入GRU的输入层,同时不使用dropout操作。最后,初始化输出层要放入的Linear全连接层。
forward()函数——前向传播中,首先对X进行embedding操作,并进行了permulate()将第一个维度变为了num_steps。将编码器得到的state隐状态通过repeat成与X第一维度num_steps相同后利用广播机制形成最终含有上下文信息的Context并最终通过torch,cat连接到X中【维度选用2】。最后利用了rnn输出output和最后的隐状态state。

实例化解码器

decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape

有关于model.train()和model.eval()的区别可以参考:torch 中的 model.eval() 是什么?

修改损失函数

# 修改损失函数:将填充词元的预测排除在损失函数的计算之外
"""下面的sequence_mask函数 通过零值化屏蔽不相关的项"""
#@save
def sequence_mask(X,valid_len,value=0):# print('mask X的形状:',X.size())with  open('D://pythonProject//Mask_X_size.txt', 'w') as f:f.write(str(X.size()))"""损失函数中的Mask_X_size的大小:torch.Size([25, 10]) 显然是没有进行Embedding的""""""在序列中屏蔽不相干的项"""maxlen = X.size(1)mask = torch.arange((maxlen),dtype=torch.float32,device=X.device)[None,:]<valid_len[:,None]X[~mask] = valuereturn X
X = torch.tensor([[1,2,3],[4,5,6]])
res = sequence_mask(X,torch.tensor([1,2]))
print('valid_len 分别为 1 和 2: ',res)

同时可以使用非0值替换要屏蔽的项

X = torch.ones(2,3,4)
res = sequence_mask(X,torch.tensor([1,2]),value=-1)

我们可以通过扩展softmax交叉熵损失函数来遮蔽不相关的预测。
最初,所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充
词元对应的掩码将被设置为0。 最后,将所有词元的损失乘以掩码,以
过滤掉损失中填充词元产生的不相关预测。

#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):"""带遮蔽的softmax交叉熵损失函数"""# pred的形状:(batch_size,num_steps,vocab_size)# label的形状:(batch_size,num_steps)# valid_len的形状:(batch_size,)def forward(self, pred, label, valid_len):# 预测词元的掩码都设置为1weights = torch.ones_like(label)# 一旦给定了有效长度,与填充# 词元对应的掩码将被设置为0。weights = sequence_mask(weights, valid_len)self.reduction='none'unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)weighted_loss = (unweighted_loss * weights).mean(dim=1)return weighted_loss

训练

在训练部分,需要在原始的编码器输出序列前加入特定的序列开始词元 同时作为解码器的输入—>这种操作被称为强制教学

"""在训练部分,需要在原始的编码器输出序列前加入特定的序列开始词元<bos> 同时作为解码器的输入--->这种操作被称为强制教学"""
#@save
def train_seq2seq(net,data_iter,lr,num_epochs,tgt_vocab,device):"""训练序列到序列模型"""def xavier_init_weights(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)if type(m) == nn.GRU:for param in m._flat_weights_names:if "weight" in param:nn.init.xavier_uniform_(m._parameters[param])net.apply(xavier_init_weights)net.to(device)optimizer = torch.optim.Adam(net.parameters(), lr=lr)loss = MaskedSoftmaxCELoss()"""注意:这里使用的是net.train()"""net.train()animator = d2l.Animator(xlabel='epoch', ylabel='loss',xlim=[10, num_epochs])for epoch in range(num_epochs):timer = d2l.Timer()metric = d2l.Accumulator(2)  # 训练损失总和,词元数量for batch in data_iter:optimizer.zero_grad()X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]# print('train-X:',X,'train-X_valid_len:',X_valid_len)# print('train-Y:',Y,'train-Y_valid_len:',Y_valid_len)with  open('D://pythonProject//X_valid_len.txt', 'w') as f:f.write(str(X_valid_len))"""tensor([4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,5])---------->它的总长度为batch_size=25(最后一个batch_size)  之前的都是64"""with  open('D://pythonProject//Y_valid_len.txt', 'w') as f:f.write(str(Y_valid_len))"""tensor([4, 4, 3, 5, 5, 4, 5, 3, 4, 4, 5, 4, 4, 4, 7, 5, 5, 4, 4, 3, 4, 4, 3, 3,5])---------->它的总长度为batch_size=25(最后一个batch_size)  之前的都是64"""bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],device=device).reshape(-1, 1)dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学Y_hat, _ = net(X, dec_input, X_valid_len)l = loss(Y_hat, Y, Y_valid_len)l.sum().backward()      # 损失函数的标量进行“反向传播”d2l.grad_clipping(net, 1)num_tokens = Y_valid_len.sum()optimizer.step()with torch.no_grad():metric.add(l.sum(), num_tokens)if (epoch + 1) % 10 == 0:animator.add(epoch + 1, (metric[0] / metric[1],))print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} 'f'tokens/sec on {str(device)}')

在机器翻译数据集上创建和训练一个循环神经网络‘编码器-解码器‘模型用于序列到序列的学习

这里需要注意的是在decoder训练的时候丢进去的数据直接是真实的label值。

embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,                      dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)

预测

为了采用一个接着一个词元的方式预测输出序列, 每个解码器当前时间步的输入都将来自于前一时间步的预测词元。
在这里插入图片描述
预测阶段的主要任务是:

  1. 将net设置为评估模式
  2. 在tokens后面加入< eos >;如果长度不够num_steps时在句子后填充< pad >拉长句子
  3. 将源tokens增加维度0,使得它变成一个二维向量
  4. 将编码器的输出(该输出包括outputs和state两个部分)传入解码器的初始化隐状态函数中初始化解码器的隐状态
  5. 将编码器的输入特征X转变成二维特征向量
  6. 预测过程:①利用预测最高可能性的词元作为解码器在下一个时间步的输入;②将解码器的输出转变成二维向量,如果预测的词元为< eos >则停止这个短句的预测;③最后利用join函数形成最终的预测短句
# 预测
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,device, save_attention_weights=False):"""序列到序列模型的预测"""# 在预测时将net设置为评估模式net.eval()src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]enc_valid_len = torch.tensor([len(src_tokens)], device=device)# 增加<pad>if len(src_tokens) > num_steps:with  open('D://pythonProject//predict_seq2seq-truncate.txt', 'w') as f:f.write(str('截断'))else:with  open('D://pythonProject//predict_seq2seq-pad.txt', 'w') as f:f.write(str('拉长'))src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])# 添加批量轴--->增肌维度,将"""input是一维,则dim=0时数据为行方向扩,dim=1时为列方向扩""""""这里的src_tokens是一个list对象"""print('len(src_tokens): ',len(src_tokens))  # len of src_tokens == 10enc_X = torch.unsqueeze(torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)# enc_X的大小为 torch.Size([1, 10])"""这里将src_tokens从list对象转成了一个tensor,增加了维度0"""with  open('D://pythonProject//predict_seq2seq-enc_X-enc_X.txt', 'w') as f:f.write(str(enc_X.size()))enc_outputs = net.encoder(enc_X, enc_valid_len)dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)# 添加批量轴"""这里将tgt_vocab从list对象转成了一个tensor,增加了维度0"""dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)output_seq, attention_weight_seq = [], []for _ in range(num_steps):Y, dec_state = net.decoder(dec_X, dec_state)# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入dec_X = Y.argmax(dim=2)  #返回可能性最大词元的索引位置pred = dec_X.squeeze(dim=0).type(torch.int32).item()print('pred:--->', pred)# 保存注意力权重(稍后讨论)if save_attention_weights:attention_weight_seq.append(net.decoder.attention_weights)# 一旦序列结束词元被预测,输出序列的生成就完成了if pred == tgt_vocab['<eos>']:print('pred:--->eos',pred)breakoutput_seq.append(pred)return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq

利用BLEU函数进行预测序列的评估

BLEU函数:
在这里插入图片描述

正如上述式子所列,当预测的长度 l e n p r e d len_{pred} lenpred小于真实的label长度 l e n l a b e l len_{label} lenlabel时说明预测成功的可能性很低,此时整个分式就变得很大,最后出来的值就会很小,这就在一定程度上加强了短句子的权重惩罚。同时,如果后面的连乘加重了长句子的权重惩罚。

# 预测序列的评估
def bleu(pred_seq, label_seq, k):  #@save"""计算BLEU"""pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')len_pred, len_label = len(pred_tokens), len(label_tokens)score = math.exp(min(0, 1 - len_label / len_pred))for n in range(1, k + 1):num_matches, label_subs = 0, collections.defaultdict(int)for i in range(len_label - n + 1):label_subs[' '.join(label_tokens[i: i + n])] += 1for i in range(len_pred - n + 1):if label_subs[' '.join(pred_tokens[i: i + n])] > 0:num_matches += 1label_subs[' '.join(pred_tokens[i: i + n])] -= 1score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))return score

在“fra-eng”数据集上做预测

"""最后,利用训练好的循环神经网络“编码器-解码器”模型, 将几个英语句子翻译成法语,并计算BLEU的最终结果。"""
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

省流—全部代码

注意:这里为了debug,我增加了很多写文件的操作,主要是观察每一个向量的形状变化,具体的结果已经通过注释的方式写到了下面代码中,仅做参考~

"""模块torch已被修改
def read_data_nmt():# Load the English-French dataset.data_dir = d2l.download_extract('fra-eng')with open(os.path.join(data_dir, 'fra.txt'), 'r',encoding='UTF-8') as f:return f.read()
"""
""" 代码中出现的torch.Size([25, 10, 32])是因为将原始的数据按照batch_size进行划分最后一个batch的大小就是25
"""
# 无脑导包
import torch
import collections # 这个包还是需要注意一下
import math
from torch import nn
from d2l import torch as d2l# 实现Encoder编码器部分
"""
内容部分:
编码器的任务主要包括:将某一个时刻t的输入特征向量x_t和上一个时刻的隐状态h_(t-1)转变为h_t即h_t = f(x_t , h_(t-1))编码器需要通过函数q实现把所有的隐状态转变为上下文变量:c  = q( h_1,....,h_T )使用嵌入层获取输入序列的每个词元的特征向量[嵌入层权重矩阵行数为vocab_size,列数是特征向量的维度]采用GRU实现编码器
"""#@save
class Seq2SeqEncoder(d2l.Encoder):"""用于序列到序列学习的循环神经网络编码器"""def __init__(self, vocab_size, embed_size, num_hiddens, num_layers, dropout=0, **kwargs):super(Seq2SeqEncoder,self).__init__(**kwargs)# 实现嵌入层Embedding 将每一个词元转变成一个词向量self.embedding = nn.Embedding(vocab_size,embed_size)# print('Encoder中 self.embedding的size为:',self.embedding.size())# print('Encoder中 embed_size:   ',embed_size)with  open('D://pythonProject//Encoder_embed_pervir_size.txt', 'w') as f:f.write(str(embed_size))"""----------embed_size为32----------""""""这里的embed_size为每一个词元对应的特征向量的长度"""self.rnn = nn.GRU(embed_size,num_hiddens,num_layers,dropout=dropout)def forward(self, X, *args):with  open('D://pythonProject//Encoder_Not_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------未进行embedding的X: torch.Size([64, 10]) batch_size * num_steps----------"""# print('Encoder中 未进行embedding前的X的size',X.size())# embedding 的形状 (vocab_size,embed_size)# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X)# print('Encoder中 进行embedding后的X的size',X.size())with  open('D://pythonProject//Encoder_embed_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了embedding的X: torch.Size([64, 10, 32])----------"""#torch要求在循环神经网络模型中,第一个轴对应的必须是时间步X = X.permute(1,0,2)# print('Encoder中 permute后的X的size',X.size())with  open('D://pythonProject//Encoder_permute_size.txt', 'w') as f:f.write(str(X.size()))"""----------进行了permute的X: torch.Size([10, 64, 32]) 10为时间步----------"""output,state = self.rnn(X)# output的输出形状: (num_steps,batch_size,num_hiddens)# state的输出形状: (num_layers,batch_size,num_hiddens)return output,state# 编码器实例化
"""
输入:
layer: 2层
hiddens: 16个
batch: 4
steps: 7
输出:
tensor[时间步数,批量大小,隐藏单元数]
"""
encoder = Seq2SeqEncoder(vocab_size=10,embed_size=8,num_hiddens=16,num_layers=2,dropout=0)
X = torch.zeros((4,7),dtype=torch.long)
output,state = encoder(X) # X的维度对应于forwoard中的X的维度
# print('output.shape: ',output.shape)
with  open('D://pythonProject//Encoder_output_size.txt', 'w') as f:f.write(str(output.shape))
"""----------output的形状: torch.Size([7, 4, 16]) 10为时间步----------""""""这里使用的是门控循环单元GRU,最后一个时间步的多层隐状态的形状是(num_layers,batch_size,num_hiddens)"""
"""如果使用LSTM 则state中还应该包含记忆单元信息"""
# 实现Decoder部分
"""编码器输出的整个上下文信息变量C需要作用于整个输入序列x_1,...,x_r,对输入序列进行编码"""
"""解码器输出(star)y取决于输出子序列y1,...,(star)y_(t-1),C"""
"""P((star)y|y1,...,(star)y_(t-1),C)"""
"""
·使用解码器时,我们直接使用编码器的最后一个时间步的隐状态来初始化解码器的隐状态--->两者应该具有相同的隐藏层和隐藏单元
·为了让上下文信息更好包含更多的信息,可以用上下文变量C在所有的时间步与解码器的输入进行拼接
·为了输出预测词元的概率分布,在最后一层采用全连接层来变换隐状态
"""
class Seq2SeqDecoder(d2l.Decoder):"""用于序列到序列学习的循环神经网络解码器"""def __init__(self,vocab_size,embed_size,num_hiddens,num_layers,dropout=0,**kwargs):super(Seq2SeqDecoder, self).__init__(**kwargs)self.embedding = nn.Embedding(vocab_size,embed_size)with  open('D://pythonProject//Decoder_vocab_size.txt', 'w') as f:f.write(str(vocab_size))"""----------decoder的vocab_size为201----------"""with  open('D://pythonProject//Decoder_embed_size.txt', 'w') as f:f.write(str(embed_size))"""----------decoder的embed_size为32----------"""self.rnn = nn.GRU(embed_size+num_hiddens,num_hiddens,num_layers,dropout=dropout)self.dense = nn.Linear(num_hiddens,vocab_size)def init_state(self,enc_outputs,*args):# enc_outputs[0]为编码器的输出# enc_outputs[1]为编码器最后一层输出的隐变量return enc_outputs[1]def forward(self, X, state):# print('Decoder中 未进行embedding的X的形状:',X.size())with  open('D://pythonProject//Decoder_X_size.txt', 'w') as f:f.write(str(X.size()))"""Decoder的X的大小:torch.Size([25, 10])"""# 输出'X'的形状:(batch_size,num_steps,embed_size)X = self.embedding(X).permute(1,0,2)with  open('D://pythonProject//Decoder_X_embed_permute.txt', 'w') as f:f.write(str(X.size()))"""Decoder的X_embed_permute的大小:torch.Size([10, 25, 32])"""# 广播context,使其具有与X相同的num_steps 即X.shape[0]context = state[-1].repeat(X.shape[0], 1, 1)X_and_Context = torch.cat((X,context),2)output,state = self.rnn(X_and_Context,state)output = self.dense(output).permute(1,0,2)# output的形状:(batch_size,num_steps,vocab_size)# state的形状:(num_layers,batch_size,num_hiddens)return output, state# 实例化解码器
decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape# 修改损失函数:将填充词元的预测排除在损失函数的计算之外
"""下面的sequence_mask函数 通过零值化屏蔽不相关的项"""
#@save
def sequence_mask(X,valid_len,value=0):# print('mask X的形状:',X.size())with  open('D://pythonProject//Mask_X_size.txt', 'w') as f:f.write(str(X.size()))"""损失函数中的Mask_X_size的大小:torch.Size([25, 10]) 显然是没有进行Embedding的""""""在序列中屏蔽不相干的项"""maxlen = X.size(1)mask = torch.arange((maxlen),dtype=torch.float32,device=X.device)[None,:]<valid_len[:,None]X[~mask] = valuereturn X
X = torch.tensor([[1,2,3],[4,5,6]])
res = sequence_mask(X,torch.tensor([1,2]))
print('valid_len 分别为 1 和 2: ',res)# 同时可以使用非0值替换要屏蔽的项
X = torch.ones(2,3,4)
res = sequence_mask(X,torch.tensor([1,2]),value=-1)
"""
我们可以通过扩展softmax交叉熵损失函数来遮蔽不相关的预测。 
最初,所有预测词元的掩码都设置为1。 一旦给定了有效长度,与填充
词元对应的掩码将被设置为0。 最后,将所有词元的损失乘以掩码,以
过滤掉损失中填充词元产生的不相关预测。
"""
#@save
class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):"""带遮蔽的softmax交叉熵损失函数"""# pred的形状:(batch_size,num_steps,vocab_size)# label的形状:(batch_size,num_steps)# valid_len的形状:(batch_size,)def forward(self, pred, label, valid_len):# 预测词元的掩码都设置为1weights = torch.ones_like(label)# 一旦给定了有效长度,与填充# 词元对应的掩码将被设置为0。weights = sequence_mask(weights, valid_len)self.reduction='none'unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(pred.permute(0, 2, 1), label)weighted_loss = (unweighted_loss * weights).mean(dim=1)return weighted_loss
# 使用三个相同的序列 来进行代码健全性检查   分别指定这些序列的有效长度是4,2,0
# 得出的损失结果为 第一个序列是第二个序列的两倍,第三个序列的损失直接为0# 训练
"""在训练部分,需要在原始的编码器输出序列前加入特定的序列开始词元<bos> 同时作为解码器的输入--->这种操作被称为强制教学"""
#@save
def train_seq2seq(net,data_iter,lr,num_epochs,tgt_vocab,device):"""训练序列到序列模型"""def xavier_init_weights(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)if type(m) == nn.GRU:for param in m._flat_weights_names:if "weight" in param:nn.init.xavier_uniform_(m._parameters[param])net.apply(xavier_init_weights)net.to(device)optimizer = torch.optim.Adam(net.parameters(), lr=lr)loss = MaskedSoftmaxCELoss()"""注意:这里使用的是net.train()"""net.train()animator = d2l.Animator(xlabel='epoch', ylabel='loss',xlim=[10, num_epochs])for epoch in range(num_epochs):timer = d2l.Timer()metric = d2l.Accumulator(2)  # 训练损失总和,词元数量for batch in data_iter:optimizer.zero_grad()X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]# print('train-X:',X,'train-X_valid_len:',X_valid_len)# print('train-Y:',Y,'train-Y_valid_len:',Y_valid_len)with  open('D://pythonProject//X_valid_len.txt', 'w') as f:f.write(str(X_valid_len))"""tensor([4, 4, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,5])---------->它的总长度为batch_size=25(最后一个batch_size)  之前的都是64"""with  open('D://pythonProject//Y_valid_len.txt', 'w') as f:f.write(str(Y_valid_len))"""tensor([4, 4, 3, 5, 5, 4, 5, 3, 4, 4, 5, 4, 4, 4, 7, 5, 5, 4, 4, 3, 4, 4, 3, 3,5])---------->它的总长度为batch_size=25(最后一个batch_size)  之前的都是64"""bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],device=device).reshape(-1, 1)dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学Y_hat, _ = net(X, dec_input, X_valid_len)l = loss(Y_hat, Y, Y_valid_len)l.sum().backward()      # 损失函数的标量进行“反向传播”d2l.grad_clipping(net, 1)num_tokens = Y_valid_len.sum()optimizer.step()with torch.no_grad():metric.add(l.sum(), num_tokens)if (epoch + 1) % 10 == 0:animator.add(epoch + 1, (metric[0] / metric[1],))print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} 'f'tokens/sec on {str(device)}')
"""在机器翻译数据集上创建和训练一个循环神经网络‘编码器-解码器‘模型用于序列到序列的学习"""
embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
batch_size, num_steps = 64, 10
lr, num_epochs, device = 0.005, 300, d2l.try_gpu()
train_iter, src_vocab, tgt_vocab = d2l.load_data_nmt(batch_size, num_steps)
encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,                      dropout)
decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,dropout)
net = d2l.EncoderDecoder(encoder, decoder)
train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)# 预测
#@save
def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,device, save_attention_weights=False):"""序列到序列模型的预测"""# 在预测时将net设置为评估模式net.eval()src_tokens = src_vocab[src_sentence.lower().split(' ')] + [src_vocab['<eos>']]enc_valid_len = torch.tensor([len(src_tokens)], device=device)# 增加<pad>if len(src_tokens) > num_steps:with  open('D://pythonProject//predict_seq2seq-truncate.txt', 'w') as f:f.write(str('截断'))else:with  open('D://pythonProject//predict_seq2seq-pad.txt', 'w') as f:f.write(str('拉长'))src_tokens = d2l.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])# 添加批量轴--->增肌维度,将"""input是一维,则dim=0时数据为行方向扩,dim=1时为列方向扩""""""这里的src_tokens是一个list对象"""print('len(src_tokens): ',len(src_tokens))  # len of src_tokens == 10enc_X = torch.unsqueeze(torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)# enc_X的大小为 torch.Size([1, 10])"""这里将src_tokens从list对象转成了一个tensor,增加了维度0"""with  open('D://pythonProject//predict_seq2seq-enc_X-enc_X.txt', 'w') as f:f.write(str(enc_X.size()))enc_outputs = net.encoder(enc_X, enc_valid_len)dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)# 添加批量轴"""这里将tgt_vocab从list对象转成了一个tensor,增加了维度0"""dec_X = torch.unsqueeze(torch.tensor([tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)output_seq, attention_weight_seq = [], []for _ in range(num_steps):Y, dec_state = net.decoder(dec_X, dec_state)# 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入dec_X = Y.argmax(dim=2)  #返回可能性最大词元的索引位置pred = dec_X.squeeze(dim=0).type(torch.int32).item()print('pred:--->', pred)# 保存注意力权重(稍后讨论)if save_attention_weights:attention_weight_seq.append(net.decoder.attention_weights)# 一旦序列结束词元被预测,输出序列的生成就完成了if pred == tgt_vocab['<eos>']:print('pred:--->eos',pred)breakoutput_seq.append(pred)return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
# 预测序列的评估
def bleu(pred_seq, label_seq, k):  #@save"""计算BLEU"""pred_tokens, label_tokens = pred_seq.split(' '), label_seq.split(' ')len_pred, len_label = len(pred_tokens), len(label_tokens)score = math.exp(min(0, 1 - len_label / len_pred))for n in range(1, k + 1):num_matches, label_subs = 0, collections.defaultdict(int)for i in range(len_label - n + 1):label_subs[' '.join(label_tokens[i: i + n])] += 1for i in range(len_pred - n + 1):if label_subs[' '.join(pred_tokens[i: i + n])] > 0:num_matches += 1label_subs[' '.join(pred_tokens[i: i + n])] -= 1score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))return score
"""最后,利用训练好的循环神经网络“编码器-解码器”模型, 将几个英语句子翻译成法语,并计算BLEU的最终结果。"""
engs = ['go .', "i lost .", 'he\'s calm .', 'i\'m home .']
fras = ['va !', 'j\'ai perdu .', 'il est calme .', 'je suis chez moi .']
for eng, fra in zip(engs, fras):translation, attention_weight_seq = predict_seq2seq(net, eng, src_vocab, tgt_vocab, num_steps, device)print(f'{eng} => {translation}, bleu {bleu(translation, fra, k=2):.3f}')

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/140675.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux 安装 wordpress

文章目录 linux 安装 wordpress1. wordpress 简介2. wordpress功能和特点3. 部署要求4. 环境搭建4.1 部署 nginx4.1.1 新增配置文件 4.2 部署 PHP74.2.1 查看当前版本4.2.2 YUM 安装 PHP74.2.3 查看 PHP 版本4.2.4 启动PHP-FPM4.2.5 修改配置文件4.2.6 重启服务 4.3 部署 mysql…

IDEA下使用Spring MVC

<?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/POM/4.0.0 http://ma…

【Git】轻松学会 Git:深入理解 Git 的基本操作

文章目录 前言一、创建 Git 本地仓库1.1 什么是仓库1.2 创建本地仓库1.3 .git 目录结构 二、配置 Git三、认识 Git 的工作区、暂存区和版本库3.1 什么是 Git 的工作区、暂存区和版本库3.2 工作区、暂存区和版本库之间的关系 四、添加文件4.1 添加文件到暂存区和版本库中的命令4…

设计模式之备忘录模式

文章目录 游戏角色状态恢复问题传统方案解决游戏角色恢复传统的方式的问题分析备忘录模式基本介绍游戏角色恢复状态实例备忘录模式的注意事项和细节 游戏角色状态恢复问题 游戏角色有攻击力和防御力&#xff0c;在大战 Boss 前保存自身的状态(攻击力和防御力)&#xff0c;当大…

操作系统权限提升(二十八)之数据库提权-SQL Server 数据库安装

SQL Server 数据库安装 SQL Server介绍 SQL Server 是Microsoft 公司推出的关系型数据库管理系统。具有使用方便可伸缩性好与相关软件集成程度高等优点,可跨越从运行Microsoft Windows 98 的膝上型电脑到运行Microsoft Windows 2012 的大型多处理器的服务器等多种平台使用。…

ultraEdit正则匹配多行(xml用)

在ultraEdit中&#xff0c;我想选取<channel到</channel>之间的多行&#xff08;进行删除&#xff09;。在perl模式下&#xff0c;命令为“<channel[\s\S]?</channel>”。下面是xml文件&#xff1a; <!--This XML file does not appear to have any sty…

分享40个Python源代码总有一个是你想要的

分享40个Python源代码总有一个是你想要的 源码下载链接&#xff1a;https://pan.baidu.com/s/1PNR3_RqVWLPzSBUVAo2rnA?pwd8888 提取码&#xff1a;8888 下面是文件的名字。 dailyfresh-天天生鲜 Django-Quick-Start freenom-自动续期域名的脚本 Full Stack Python简体中…

虚拟机安装 centos

title: 虚拟机安装 centos createTime: 2020-12-13 12:00:27 updateTime: 2020-12-13 12:00:27 categories: linux tags: 虚拟机安装 centos 路线图 主机(宿主机) —> centos --> docker --> docker 镜像 --> docker 容器 — docker 服务 1.前期准备 一台 主机 或…

【kubernetes】使用virtual-kubelet扩展k8s

1 何为virtual-kubelet&#xff1f; kubelet是k8s的agent&#xff0c;负责监听Pod的调度情况&#xff0c;并运行Pod。而virtual-kubelet不是真实跑在宿主机上的&#xff0c;而是一个可以跑在任何地方的进程&#xff0c;该进程向k8s伪装成一个真实的Node&#xff0c;但是实际的…

CSS 浮动布局

浮动的设计初衷 float: left/right/both;浮动是网页布局最古老的方式。 浮动一开始并不是为了网页布局而设计&#xff0c;它的初衷是将一个元素拉到一侧&#xff0c;这样文档流就能够包围它。 常见的用途是文本环绕图片&#xff1a; 浮动元素会被移出正常文档流&#xff0c;…

《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(AlexNet)

7.1.1 学习表征 深度卷积神经网络的突破出现在2012年。突破可归因于以下两个关键因素&#xff1a; 缺少的成分&#xff1a;数据 数据集紧缺的情况在 2010 年前后兴起的大数据浪潮中得到改善。ImageNet 挑战赛中&#xff0c;ImageNet数据集由斯坦福大学教授李飞飞小组的研究人…

Golang 协程池 Ants 实现原理,附详细的图文说明和代码

Golang 协程池 Ants 实现原理&#xff0c;附详细的图文说明和代码。 1 前置知识点 1.1 sync.Locker sync.Locker 是 go 标准库 sync 下定义的锁接口&#xff1a; // A Locker represents an object that can be locked and unlocked. type Locker interface {Lock()Unlock() …

stm32之串口/蓝牙控制led灯

该文章记录学习stm32串口遇到的一些问题&#xff0c;完整代码地址。 一、项目描述 通过串口或蓝牙发送指令来控制led灯。 open ------> led 亮close ------> led 灭其它 -------> 反馈给串口或蓝牙错误指令 二、项目用到的模块 stm32 串口1,PA9(TX), PA10(RX)HC…

udp的简单整理

最近思考udp处理的一些细节&#xff0c;根据公开课&#xff0c;反复思考&#xff0c;终于有所理解&#xff0c;做整理备用。 0&#xff1a;简单汇总 1&#xff1a;udp是基于报文传输的&#xff0c;接收方收取数据时要一次性读完。 2&#xff1a;借助udp进行发包&#xff0c;…

JavaWeb-JavaScript

JavaWeb-JavaScript 什么是JavaScript Web标准 Web标准也称为网页标准&#xff0c;由一系列的标准组成&#xff0c;大部分由W3C ( World Wide Web Consortium&#xff0c;万维网联盟&#xff09;负责制定。三个组成部分&#xff1a; HTML&#xff1a;负责网页的结构&#xf…

32 随机链表的复制

随机链表的复制 题解1 哈希表题解2 回溯哈希哈希思路精简 题解3 优化迭代 给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点…

OR54 字符串中找出连续最长的数字串

目录 一、题目 二、解答 &#xff08;一&#xff09;问题一&#xff1a;在记录完一组连续字符串后&#xff0c;没有注意判别紧随其后的非数字字符 &#xff08;二&#xff09;问题二&#xff1a;越界访问 &#xff08;三&#xff09;正确 一、题目 字符串中找出连续最长的…

设计模式再探——原型模式

目录 一、背景介绍二、思路&方案三、过程1.原型模式简介2.原型模式的类图3.原型模式代码4.原型模式深度剖析5.原型模式与spring 四、总结五、升华 一、背景介绍 最近在做业务实现的时候&#xff0c;为了通过提升机器来降低开发人员的难度和要求&#xff0c;于是在架构设计…

数据标准化

1、均值方差标准化(Z-Score标准化) 计算过程&#xff1a; 对每个属性/每列分别进行一下操作&#xff0c;将数据按属性/按列减去其均值&#xff0c;并除以其方差&#xff0c;最终使每个属性/每列的所有数据都聚集在均值为0&#xff0c;方差为1附近。 公式&#xff1a;(x-mean(x…

电子信息工程专业课复习知识点总结:(五)通信原理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 第一章通信系统概述——通信系统的构成、各部分性质、性能指标1.通信系统的组成&#xff1f;2.通信系统的分类&#xff1f;3.调制、解调是什么&#xff1f;有什么用…