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

news2024/12/28 18:25:29

序言

这一节是对于“编码器-解码器”模型的实际应用,编码器和解码器架构可以使用长度可变的序列作为输入,并将其转换为固定形状的隐状态(编码器实现)。本小节将使用“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] = value
    return 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):
        # 预测词元的掩码都设置为1
        weights = 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 == 10
    enc_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)
            break
        output_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])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= 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] = value
    return 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):
        # 预测词元的掩码都设置为1
        weights = 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 == 10
    enc_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)
            break
        output_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])] += 1
        for i in range(len_pred - n + 1):
            if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
                num_matches += 1
                label_subs[' '.join(pred_tokens[i: i + n])] -= 1
        score *= 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.coloradmin.cn/o/1038039.html

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

相关文章

[论文分享] How to Better Utilize Code Graphs in Semantic Code Search?

How to Better Utilize Code Graphs in Semantic Code Search? [ESEC/FSE 2022] 语义代码搜索极大地促进了软件的重用&#xff0c;使用户能够找到与用户指定的自然语言查询高度匹配的代码片段。由于代码图(如控制流图和程序依赖图)丰富的表达能力&#xff0c;两种主流的研究工…

【Gradle-9】Gradle插件发布指南

1、前言 不管是在公司内部&#xff0c;还是开源&#xff0c;Gradle插件发布都是一项必备的技能&#xff0c;本文主要介绍本地发布和远端发布两种方式。 2、本地发布 2.1、添加依赖 在plugin>build.gradle文件中&#xff08;插件的项目&#xff09;先依赖一个maven发布的…

分布式搜索引擎Elasticsearch

一、Elasticsearch介绍 1.Elasticsearch产生背景 大数据量的检索NoSql: not only sql,泛指非关系型的数据库Nginx的7层负载均衡和4层负载均衡2.Elasticsearch是什么 一个基于Lucene的分布式搜索和分析引擎,一个开源的高扩展的分布式全文检索引擎 Elasticsearch使用Java开发…

零基础也能制作小说推文视频,输入文案就能制作推文短视频

小说推文视频一直是各类写手们追捧的创作方式之一&#xff0c;而如何制作出优质、吸引人的小说推文视频成了许多人关注的焦点。幸运的是&#xff0c;现在有了一款名为推文视频制作神器&#xff0c;让制作小说推文视频变得轻松简单。 这款小说推文视频神器的功能十分强大&#…

山西电力市场日前价格预测【2023-09-25】

日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-09-25&#xff09;山西电力市场全天平均日前电价为442.30元/MWh。其中&#xff0c;最高日前电价为720.46元/MWh&#xff0c;预计出现在19: 00。最低日前电价为276.06元/MWh&#xff0c;预计…

AUTOSAR 多核操作系统时序监控系统设计

AUTOSAR 多核操作系统时序监控系统设计 0 引言1 AUTOSAR 介绍1.1 AUTOSAR 诞生1.3 AUTOSAR 架构 2 时序监控系统软硬件介绍2.1 硬件部分2.2 软件部分 3 时序监控系统设计3.1 监控系统整体设计3.2 监控数据获取3.3 监控数据存储3.4 监控数据处理 3.5 还原运行时序5 推动 5G工业互…

3D点云目标检测:Centerformer训练waymo数据集

一、环境准备 项目地址:centerformer 1.0、基础环境 python 3.8.0 torch 1.9.1cu111 waymo-open-dataset-tf-2-6-0 1.4.9 spconv 1.2.1 其余按照requirement.txt里安装就行 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt由于我本人是在…

uniapp、vue实现滑动拼图验证码

uniapp、vue实现滑动拼图验证码 实际开发工作中&#xff0c;在登陆的时候需要短信验证码&#xff0c;但容易引起爬虫行为&#xff0c;需要用到反爬虫验证码&#xff0c;今天介绍一下拼图验证码&#xff0c;解决验证码反爬虫中的滑动验证码反爬虫。滑动拼图验证码是在滑块验证码…

QLineEdit设置数据的输入范围QIntValidator和QDoubleValidator

在日常开发过程中QLineEdit作为输入框&#xff0c;有时要限制输入的内容&#xff0c;比哪&#xff0c;考试分数为0-100&#xff0c;这个时候就使用QIntValidator作为限制范围&#xff0c;而如何输入的是带小数的呢&#xff0c;那么使用QDoubleValidator可以吗&#xff0c;下面请…

ipad触控笔有必要买原装吗?ipad2023手写笔推荐

目前&#xff0c;在无纸教学、无纸办公的大背景下&#xff0c;电容笔得到了广泛的关注。只是&#xff0c;对于这两支电容笔的不同之处&#xff0c;不少人并不是很清楚。其实这两种电容笔都很好区分&#xff0c;第一种是主动电容笔&#xff0c;也就是我们常用的电容式屏幕&#…

安全生产一张图 安全生产三维地理信息平台

一、 建设目标 易图讯科技是一家专业从事大数据、移动互联网、物联网、三维GIS、AI系统研发&#xff0c;开发了三维电子沙盘、AI三维电子沙盘、WEB三维地球、移动端三维地球、数字武装三维电子沙盘、智慧动员三维电子沙盘、智慧公安三维电子沙盘、智慧安监三维电子沙盘、森林防…

vue重修003

文章目录 版权声明day03一、今日目标1.生命周期2.综合案例-小黑记账清单3.工程化开发入门4.综合案例-小兔仙首页 二、Vue生命周期三、Vue生命周期钩子四、生命周期钩子小案例1.在created中发送数据2.在mounted中获取焦点 五、案例-小黑记账清单1.需求图示&#xff1a;2.需求分析…

Maven项目在pom.xml里配置远程仓库

如图:作用 在项目的 pom.xml 文件中配置了 <repositories> 元素&#xff0c;Maven会优先使用项目级别的仓库配置&#xff0c;而不会查找全局设置文件中的仓库配置。换句话说&#xff0c;项目级别的配置会覆盖全局设置文件中的仓库配置。 这意味着当在项目的 pom.xml 文…

AUTOSAR 面试知识回顾

如果答不上来&#xff0c;就讲当时做了什么 1. Ethernet基础: 硬件接口&#xff1a; ECU到PHY&#xff1a; data 是MII总线&#xff0c; 寄存器控制是SMI总线【MDCMDIO两根线, half duplex】PHY输出(100BASE-T1)&#xff1a; MDI总线&#xff0c;2 wire 【T1: twisted 1 pair …

C++项目:仿muduo库实现高性能高并发服务器

文章目录 一、实现目标二、前置知识&#xff08;一&#xff09;HTTP服务器1.概念 &#xff08;二&#xff09;Reactor模型&#xff1a;1.概念2.分类&#xff08;1&#xff09;单Reactor单线程&#xff1a;单I/O多路复用业务处理。&#xff08;2&#xff09;单Reactor多线程&…

腾讯mini项目-【指标监控服务重构-会议记录】2023-07-26

2023-07-26组长会议纪要 A组 项目对齐和问题 分配需求&#xff0c;SLI指标上报&#xff0c;暂时没有实际效果 每个人负责一条指标&#xff0c;同步代码&#xff0c;时间问题还是难题跟B组同学请教&#xff0c;答疑 问题&#xff1a;启动 Tracer 【已解决】 环境问题&#xf…

21.redo日志(下)

title: “redo日志&#xff08;下&#xff09;” createTime: 2022-03-06T15:52:4108:00 updateTime: 2022-03-06T15:52:4108:00 draft: false author: “ggball” tags: [“mysql”] categories: [“db”] description: “” redo log的刷盘时机 log buffer 空间不足时&…

【linux】性能优化

这张图谱出自倪朋飞&#xff1b; 1、什么是性能指标 这里一定会想到 “高并发” 和 “响应快”&#xff0c;这里词正对应的就是 “吞吐” 和 “延时”。我们知道随着应用负载的体系&#xff0c;系统资源的使用就会提高&#xff0c;甚至达到极限。而性能问题的本质&#xff0c…

【CNN-FPGA开源项目解析】卷积层01--floatMult16模块

文章目录 (基础)半精度浮点数的表示和乘运算16位半精度浮点数浮点数的乘运算 floatMult16完整代码floatMult16代码逐步解析符号位sign判断指数exponent计算尾数fraction计算尾数fraction的标准化和舍位整合为最后的16位浮点数结果[sign,exponent,fraction] 其他变量宽度表alway…

Matplotlib 是一个广泛用于 Python 数据可视化的库

Matplotlib 是一个广泛用于 Python 数据可视化的库&#xff0c;它提供了丰富的绘图功能&#xff0c;允许用户创建各种类型的图表&#xff0c;从简单的折线图到复杂的三维图表&#xff0c;以及定制图形的各个方面。以下是Matplotlib的一些重要特点和常见用法&#xff1a; Matpl…