【动手学深度学习-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.mzph.cn/news/86882.shtml

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

相关文章

linux用户和权限命令学习记录

文章目录 版权声明root用户&#xff08;超级管理员&#xff09;su和exit命令sudo命令为普通用户配置sudo认证 用户、用户组管理用户组管理getent命令 查看权限控制认知权限信息 修改权限控制chmod修改文件、文件夹的权限权限的数字序号chown修改所属用户、用户组 版权声明 本博…

华为NFC设置教程(门禁卡/公交卡/校园卡等)

今天把华为NFC设置教程分享给大家 出门带门禁卡、校园卡、银行卡、身份证……东西又多&#xff0c;携带又麻烦&#xff0c;还容易搞丢&#xff0c;有没有一种方法可以把它们都装下&#xff1f;有&#xff01;只要一部手机&#xff0c;出门不带卡包&#xff0c;各种证件&#x…

机器学习第十课--提升树

一.Bagging与Boosting的区别 在上一章里我们学习了一个集成模型叫作随机森林&#xff0c;而且也了解到随机森林属于Bagging的成员。本节我们重点来学习一下另外一种集成模型叫作Boosting。首先回顾一下什么叫Bagging? 比如在随机森林里&#xff0c;针对于样本数据&#xff0c;…

使用命令行快速创建Vite项目

一、构建项目 在终端中使用如下命令行&#xff1a; npm create vite 二、定义项目名称 三、选择项目类型 Vanilla是我们常用的JavaScript&#xff0c;Vue和React是常用前端框架&#xff0c;可以根据自己的需要进行选择 通过上下键进行选择&#xff0c;按下回车进行确认 创建…

docker容器安装MongoDB数据库

一&#xff1a;MongoDB数据库 1.1 简介 MongoDB是一个开源、高性能、无模式的文档型数据库&#xff0c;当初的设计就是用于简化开发和方便扩展&#xff0c;是NoSQL数据库产品中的一种。是最 像关系型数据库&#xff08;MySQL&#xff09;的非关系型数据库。 它支持的数据结构…

Neo4j图数据库_web页面关闭登录实现免登陆访问_常用的cypher语句_删除_查询_创建关系图谱---Neo4j图数据库工作笔记0013

由于除了安装,那么真实使用的时候,就是导入数据了,有了关系和节点的csv文件以后如果用 cypher进行导入数据和创建关系图谱,还有进行查询,以及如果导入错误如何清空,大概是这些 用的最多的,单独把这些拿进来,总结一下,用的会比较方便. 1.实现免登陆访问: /data/module/neo4j-…

Spring学习笔记6 Bean的实例化方式

Spring学习笔记5 GoF之工厂模式_biubiubiu0706的博客-CSDN博客 Spring为Bean提供了多种实例化方式,通常包括4中(目的:更加灵活) 1.通过构造方法实例化 2.通过简单工厂模式实例化 3.通过factory-bean实例化 4.通过FactoryBean接口实例化 新建模块 spring-005 依赖 <!--S…

结构体,联合体与位段

1.结构体的内存对齐(计算结构体的大小) 1.1 为什么需要结构体内存对齐? 原因1:平台原因 不是所有的硬件平台都能访问任意地址上的任意数据的&#xff1b;某些平台只能在某些地址处取得某些特定类型的数据&#xff0c;否则抛出硬件异常。 比如&#xff0c;当一个平台要取一个…

Qt5开发及实例V2.0-第二十二章-Qt.Quick Controls 2新颖界面开发

Qt5开发及实例V2.0-第二十二章-Qt.Quick Controls 2新颖界面开发 第22章 Qt Quick Controls 2新颖界面开发22.1 Qt Quick Controls 2简介22.1.1 第一个Qt Quick Controls 2程序22.1.2 Qt Quick Controls 2程序的构成 22.2 Qt Quick Controls 2与1的比较22.2.1 ApplicationWindo…

紫光展锐6nm国产5G处理器T820_国产手机芯片5G方案

紫光展锐T820是一款采用先进6nm EUV工艺的芯片&#xff0c;采用134三丛集八核心CPU架构&#xff0c;由1个主频为 2.7GHz 的 Arm Cortex-A76 大核和 3个主频为2.3GHz 的Arm Cortex-A76大核以及4个主频为2.1GHz的 Arm Cortex-A55组成 &#xff0c;支持高达3MB 三级缓存&#xff0…

MySQL 篇

目录 1、数据库三范式 2、数据库事务的特性 3、MySQL数据库引擎 4、说说 InnoDB 与 MyISAM 的区别 5、索引是什么&#xff1f; 6、索引数据结构 7、MySQL 索引类型有哪些&#xff1f; 8、索引有什么优缺点&#xff1f; 9、索引设计原则 9、使用索引应该注意些什…

ubuntu 开启笔记本摄像头并修复画面颠倒问题

文章目录 基本环境状况&#xff1a; 没找到摄像头检查 opencv检查系统应用 键盘右侧&#xff0c;硬件层面开启摄像头画面镜像问题 基本环境 笔记本&#xff1a; 联想拯救者 系统&#xff1a; ubuntu 22.04 状况&#xff1a; 没找到摄像头 检查 opencv 使用 cv::VideoCaptu…

实验室安全教育与考试

目录 我的错题&#xff08;2个&#xff09;新知识题目&#xff08;10个&#xff09;刚开始不太理解的题目&#xff08;10个&#xff09;写在最后&#xff08;免责声明&#xff09; 我的错题&#xff08;2个&#xff09; 18.发生电气火灾时可以使用的灭火设备包括&#xff1a;&…

【100天精通Python】Day65:Python可视化_Matplotlib3D绘图mplot3d,绘制3D散点图、3D线图和3D条形图,示例+代码

1 mpl_toolkits.mplot3d 功能介绍 mpl_toolkits.mplot3d 是 Matplotlib 库中的一个子模块&#xff0c;用于绘制和可视化三维图形&#xff0c;包括三维散点图、曲面图、线图等。它提供了丰富的功能来创建和定制三维图形。以下是 mpl_toolkits.mplot3d 的主要功能和功能简介&am…

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

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

js对象属性

在面向对象的语言中有一个标志&#xff0c;那就是都有类&#xff0c;通过类可以创建任意多个相同属性、方法的对象。在js中没有类的存在&#xff0c;所以js中的对象&#xff0c;相对于类语言中对象有所不同。 js中定义对象为&#xff1a;“无序属性的集合&#xff0c;其属性可…

虹科案例 | LIN/CAN总线汽车零部件测试方案

文章来源&#xff1a;虹科汽车电子 点此阅读原文 虹科的LIN/CAN总线汽车零部件测试方案是一款优秀的集成套装&#xff0c;基于Baby-LIN系列产品&#xff0c;帮助客户高效完成在测试、生产阶段车辆零部件质量、功能、控制等方面的检测工作。 1、汽车零部件测试的重要性&#xf…

乐鑫科技全球首批支持蓝牙 Mesh Protocol 1.1 协议

乐鑫科技 (688018.SH) 非常高兴地宣布&#xff0c;其自研的蓝牙 Mesh 协议栈 ESP-BLE-MESH 现已支持最新蓝牙 Mesh Protocol 1.1 协议的全部功能&#xff0c;成为全球首批在蓝牙技术联盟 (Bluetooth SIG) 正式发布该协议之前支持该更新的公司之一。这意味着乐鑫在低功耗蓝牙无线…

【Java 基础篇】Java Stream 流详解

Java Stream&#xff08;流&#xff09;是Java 8引入的一个强大的新特性&#xff0c;用于处理集合数据。它提供了一种更简洁、更灵活的方式来操作数据&#xff0c;可以大大提高代码的可读性和可维护性。本文将详细介绍Java Stream流的概念、用法和一些常见操作。 什么是Stream…

API网关是如何提升API接口安全管控能力的

API安全的重要性 近几年&#xff0c;越来越多的企业开始数字化转型之路。数字化转型的核心是将企业的服务、资产和能力打包成服务&#xff08;服务的形式通常为API&#xff0c;API又称接口&#xff0c;下文中提到的API和接口意思相同&#xff09;&#xff0c;从而让资源之间形…