深度学习(自然语言处理)Seq2Seq学习笔记(动手实践)

目录

0. 前言

1. Seq2Seq模型简介

2. 代码复现

2.1 Introduction:

2.2 准备数据:

2.3 训练、验证和测试数据集

2.4 创建Seq2Seq Model

2.4.1 编码器Encoder:

2.4.2 Decoder

2.5 实现Seq2Seq模型

2.6 训练模型

2.7 评估:


from 恒心

研一上时的学习笔记,确实是一个序列到序列学习的好方法,建议初学者可以尝试以下欧

修改~2021.8.17

文章有些公式无效,重新更正了一下。

0. 前言

首先这部分的学习还是看代码比较直观,代码看完后,在重新看完论文图片以及公式推导,更容易理解,考虑到Pytorch 与Tensorflow 如今框架比较新 所以不建议用旧的框架实现,因此在github找到了一个不错的仓库项目

个人的学习笔记仓库:

https://github.com/leandon/Postgraduate_Study_Notes

运行环境:

google colabratory

1. Seq2Seq模型简介

对这个模型需要有一定的理解,具体可以解读多图解读

2. 代码复现

该笔记原文是英文笔记,因此复现的时候,汉化了一部分做记录,建议还是看原文会比较好。

Sequence to Sequence(Seq2seq) Learning with Neural Networks

2.1 Introduction:

实例中演示了德语英语的翻译,但其实这个模型可以应用在任何涉及从一个序列到另一个序列,例如汇总

最常见的Seq2Seq模型是解码器-编码器模型

  • 通常使用递归神经网络(RNN)将源(输入)语句编码为单个向量。在本笔记本中,我们将将此单个向量称为上下文向量。我们可以将上下文向量视为整个输入句子的抽象表示。
  • 然后,该向量由第二个RNN解码,该第二个RNN通过一次生成一个单词来学习输出目标(输出)语句

这个图,基本上就是整个实验的核心了

编码器:

h_t = \text{EncoderRNN}(e(x_t), h_{t-1})

解码器

h_t = \text{EncoderRNN}(e(x_t), h_{t-1})

2.2 准备数据:

我们使用PyTorch对模型进行编码,并使用TorchText版主我们进行所需的所有预处理,使用spacy协助数据标记化

接下来:tokenizers是一个分词器,使用分词器将包含句子的字符串转换为组成该字符串的单个令牌的列表。句子是一系列标记。而不是一系列单词。[“ good”,“ morning”,“!”],“好”和“早上”都是单词和记号,但是“!”是一个象征,而不是一个单词

spact 具有每种语言的模型(德语为“ de”,英语为“ en”),因此我们可以访问每种模型的标记器。

使用之前需要在命令行里输入:

python -m spacy download en
python -m spacy download de

我们创建令牌生成器(分词器)函数,这些可以传递给TorchText,并将句子作为字符串接收,并将句子作为标记列表返回。

在我们正在实施的论文中,他们发现反转输入顺序是有益的,他们认为输入顺序“在数据中引入了许多短期依赖性,这使得优化问题更加容易”。在将德语句子转换为标记列表之后,我们通过反转德语句子来复制该句子。

注意翻转:

str='Runoob'

print(str[::-1])

2.3 训练、验证和测试数据集

我们将使用的数据集是Multi30k数据集。这是一个具有约30,000个并行英语,德语和法语句子的数据集,每个句子每个句子含〜12个单词。

词汇表用于将每个唯一标记与索引(整数)相关联。源语言和目标语言的词汇是不同的。

使用min_freq参数,我们只允许出现至少2次的标记出现在我们的词汇表中。仅出现一次令牌转换成<UNK>(未知)令牌。

重要的是要注意,我们的词汇表应该仅基于训练集而不是验证/测试集。这可以防止“信息泄漏”进入我们的模型,从而使我们夸大了验证/测试分数。

准备数据的最后一步是创建迭代器,可以重复这些操作以返回一批数据,这些数据是一个Pytroch张量,可以说是使用词汇表将他们从一系列可读标记转换成一系列相应的索引。

我们获得一批数据后,我们需要确保所有语句的填充长度都相同,目标语句也是如此。幸运的是,TorchText迭代器为我们的处理了此问题。

使用BucketIterator 它以最小化源句和目标句中的填充量的方式创建批处理

2.4 创建Seq2Seq Model

三部分构建模型

  • 编码器
  • 解码器
  • seq2seq模型

同时提供一种相互连接的方式。

2.4.1 编码器Encoder:

两层的LSTM 网络,隐藏状态的第一层的输出(建议参照第一张图进行思考)

​​​​​​​

隐藏状态的第二层的输出(建议参照第一张图进行思考)

使用多层RNN还意味着我们还需要一个初始隐藏状态作为每层hl0的输入,并且我们还将每层输出一个上下文向量

我们采用LSTM ,是因为我们需要返回的不单单是新的隐藏状态而是要返回一个单元格状态Ct以及每个时间步长

 

我们可以将Ct作为另一种隐藏的状态,初始为全零的张量。我们的上下文向量现在将同时是最终的隐藏状态和最终的单元格状态

将我们的多层(multi-layer)扩展到LSTM,我们得到

 

如何将第一层的隐藏状态作为输入传递给第二层,而不将其作为单元状态传递给第二层

参数解释如下:

  • input_dim  输入(源)词汇量

  • emb_dim    嵌入层的尺寸

  • hid_dim    是隐藏状态和单元状态的维数。

  • n_layers 是RNN的层数

  • dropout Dropout层,用于正则化,在多层RNN的各层之间应用。

在将单词(从技术上讲,单词的索引)传递到RNN之前,有一个步骤,在那里将单词转换为向量

这个RNN 返回的数值

outputs (每个时间步的顶层隐藏状态), 

hidden (the final hidden state for each layer, hT, stacked on top of each other)

cell (the final cell state for each layer, cT, stacked on top of each other).

每个张量的大小在代码中留为注释。在此实现中,n_directions始终为1,但是请注意,双向RNN(在教程3中介绍)的n_directions为2。

2.4.2 Decoder

采用2层LSTM

其实现原理和编码器类似

请记住,解码器的初始隐藏和单元状态是我们的上下文向量,它们是来自同一层的编码器的最终隐藏和单元状态

参数和初始化与Encoder类类似,除了我们现在有一个output_dim,它是输出/目标的词汇量。还添加了线性层,用于根据顶层隐藏状态进行预测

实现过程注意:

当序列长度始终为1的时候,采用nn.LSTMCell

但是当序列长度比较大的时候,采用nn.LSTM代码比较简洁

2.5 实现Seq2Seq模型

  • 接受输入/源句
  • 使用编码器产生上下文向量
  • 使用编码器产生预测的输出/目标句子

在前向方法中要做的第一件事是创建一个输出张量,该张量将存储我们的所有预测Y

在模型训练过程中,我们加入了一个teaching force的阈值,表示使用teaching force的概率。当随机生成的数字大于这个阈值时,使用teaching force;否则不使用。teacher force就是在翻译的过程中“抄答案”,将正确的单词作为后面decoder的输入。

“我们可以看到,如果使用了teacher force,不管翻译的结果是否正确,我们都使用正确的答案参与后面的decoder的计算中。简单来说,就是 “师傅带进门,修行靠个人” 。----关于teacher force 比较通俗的讲解

代码实现

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()
        
        self.encoder = encoder
        self.decoder = decoder
        self.device = device
        
        assert encoder.hid_dim == decoder.hid_dim, \
            "Hidden dimensions of encoder and decoder must be equal!"
        assert encoder.n_layers == decoder.n_layers, \
            "Encoder and decoder must have equal number of layers!"
        
    def forward(self, src, trg, teacher_forcing_ratio = 0.5):
        
        #src = [src len, batch size]
        #trg = [trg len, batch size]
        #teacher_forcing_ratio is probability to use teacher forcing
        #e.g. if teacher_forcing_ratio is 0.75 we use ground-truth inputs 75% of the time
        
        batch_size = trg.shape[1]
        trg_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim
        
        #tensor to store decoder outputs
        outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
        
        #last hidden state of the encoder is used as the initial hidden state of the decoder
        hidden, cell = self.encoder(src)
        
        #first input to the decoder is the <sos> tokens
        input = trg[0,:]
        
        for t in range(1, trg_len):
            
            #insert input token embedding, previous hidden and previous cell states
            #receive output tensor (predictions) and new hidden and cell states
            output, hidden, cell = self.decoder(input, hidden, cell)
            
            #place predictions in a tensor holding predictions for each token
            outputs[t] = output
            
            #decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio
            
            #get the highest predicted token from our predictions
            top1 = output.argmax(1) 
            
            #if teacher forcing, use actual next token as next input
            #if not, use predicted token
            input = trg[t] if teacher_force else top1
        
        return outputs

编码器和解码器的嵌入(embedding)维数和丢失(Dropout)量可以不同,但​​是层数和隐藏/单元状态的大小必须相同

我们初始权重(从-0.08到+0.08之间的均匀分布),并使用nn.init.uniform_从均匀分布中采样它们

def init_weights(m):
    for name, param in m.named_parameters():
        nn.init.uniform_(param.data, -0.08, 0.08)
        
model.apply(init_weights)

就算可训练参数的数量

def count_parameters(model):
    return sum(p.numel() for p in model.parameters() if p.requires_grad)

print(f'The model has {count_parameters(model):,} trainable parameters')

2.6 训练模型

  • 从批处理中获取源句子和目标句子X和Y

  • 将最后一批计算出的梯度归零

  • 将源和目标馈入模型以获取输出Y ^
  • 由于损失函数仅适用于具有1d目标的2d输入,因此我们需要使用.view展平它们
  • 将输出张量和目标张量的第一列切开
  • 用loss.backward()计算梯度
  • 裁剪渐变以防止其爆炸(RNN中的常见问题)
  • 通过执行优化程序步骤来更新模型的参数
  • 损失值加总
  • 最后返回所有批次的平均损失
def train(model, iterator, optimizer, criterion, clip):
    
    model.train()
    
    epoch_loss = 0
    
    for i, batch in enumerate(iterator):
        # 从批处理中后去源句子和目标句子X,Y
        src = batch.src
        trg = batch.trg
        # 将最后一批计算出的梯度归零
        optimizer.zero_grad()
        # 将源和目标放入模型中输出下一个y
        output = model(src, trg)
        
        #trg = [trg len, batch size]
        #output = [trg len, batch size, output dim]
        
        output_dim = output.shape[-1]
        # 由于损失函数是仅适用于具有1d目标的2d输入,因此我们用view将其展平输入
        # 将输出张量和目标张量的第一列切开
        output = output[1:].view(-1, output_dim)
        trg = trg[1:].view(-1)
        
        #trg = [(trg len - 1) * batch size]
        #output = [(trg len - 1) * batch size, output dim]
        
        loss = criterion(output, trg)
        # 用此函数计算梯度
        loss.backward()
        # clip the gradients 防止其梯度爆炸
        torch.nn.utils.clip_grad_norm_(model.parameters(), clip)
        # 执行优化程序步骤来更新模型的参数
        optimizer.step()
        # 损失值求和
        epoch_loss += loss.item()
    # 返回所有批次的平均损失    
    return epoch_loss / len(iterator)

2.7 评估:

def evaluate(model, iterator, criterion):
    # 设置为评估模式,关闭Dropout(弱使用批处理规范化,则也关闭)
    model.eval()
    
    epoch_loss = 0
    
    with torch.no_grad():
        # 确保该模块内不计算梯度
        for i, batch in enumerate(iterator):

            src = batch.src
            trg = batch.trg
            # 必须确保关闭teacher_forcing 参数
            output = model(src, trg, 0) #turn off teacher forcing

            #trg = [trg len, batch size]
            #output = [trg len, batch size, output dim]

            output_dim = output.shape[-1]
            
            output = output[1:].view(-1, output_dim)
            trg = trg[1:].view(-1)

            #trg = [(trg len - 1) * batch size]
            #output = [(trg len - 1) * batch size, output dim]

            loss = criterion(output, trg)
            
            epoch_loss += loss.item()
        
    return epoch_loss / len(iterator)

计算运行时间 

def epoch_time(start_time, end_time):
    elapsed_time = end_time - start_time
    elapsed_mins = int(elapsed_time / 60)
    elapsed_secs = int(elapsed_time - (elapsed_mins * 60))
    return elapsed_mins, elapsed_secs

N_EPOCHS = 10
CLIP = 1

best_valid_loss = float('inf')

for epoch in range(N_EPOCHS):
    
    start_time = time.time()
    
    train_loss = train(model, train_iterator, optimizer, criterion, CLIP)
    valid_loss = evaluate(model, valid_iterator, criterion)
    
    end_time = time.time()
    
    epoch_mins, epoch_secs = epoch_time(start_time, end_time)
    
    if valid_loss < best_valid_loss:
        best_valid_loss = valid_loss
        torch.save(model.state_dict(), 'tut1-model.pt')
    
    print(f'Epoch: {epoch+1:02} | Time: {epoch_mins}m {epoch_secs}s')
    print(f'\tTrain Loss: {train_loss:.3f} | Train PPL: {math.exp(train_loss):7.3f}')
    print(f'\t Val. Loss: {valid_loss:.3f} |  Val. PPL: {math.exp(valid_loss):7.3f}')


# 我们将加载为模型提供最佳验证损失的参数(state_dict),然后在测试集上运行模型
model.load_state_dict(torch.load('tut1-model.pt'))

test_loss = evaluate(model, test_iterator, criterion)

print(f'| Test Loss: {test_loss:.3f} | Test PPL: {math.exp(test_loss):7.3f} |')

运行效果:

参考文献:

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

忆_恒心

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

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

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

打赏作者

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

抵扣说明:

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

余额充值