无监督关键短语的生成问题博客06--model.py的分析

2021SC@SDUSC

在上一篇博客中,我们分析了model.py中的Decoder类,并对LSTM作了简要的介绍,以实例来说明了构建模型时的各个参数,以及引入了嵌入层。本篇博客我们将从RNN模型的分类入手,分析Seq2Seq模型的框架并分析本篇论文中的Seq2Seq代码。

Encoder-Decoder框架是一个End-to-End学习的算法。简单来说,Seq2Seq(即Sequence to Sequence)以一个Encoder来编码输入的Sequence,再以一个Decoder来输出Sequence。其大致框架是一个序列经过Encoderr得到一个隐状态,再通过这个隐状态使用Decoder得到最终需要的序列。

 图1:Seq2Seq模型示例

一、 RNN的分类

接下来我们将由RNN的分类引出Seq2Seq模型。按照输入和输出的结构,可以将RNN分为:

  • N vs N - RNN
  • N vs 1 - RNN
  • 1 vs N - RNN
  • N vs M - RNN 

 1. N vs N - RNN

 图2:N vs N - RNN图示

这是RNN最基础的结构形式,其特点是输出和输入序列是等长的,但也由于这个限制的存在,其适用范围比较小,可以用于生成等长的诗句。

2. N vs 1 - RNN

  图3:N vs 1 - RNN图示

有时候我们需要处理的问题输入是一个序列,但输出的是一个单独的值而不是序列,我们只需要在隐藏层输出h上进行线性变换就可以了,大部分情况下,为了更好地明确结果,还需要sigmoid或者softmax进行处理,这种结构通常被用在文本分类上。注意,这里的h_4本身已经包含之前上一隐藏层h_3的信息和当前时间步输入x_4的信息,而h_3由依赖于之前隐藏层的信息和输入x_3,所以可以利用所有输入与隐藏层信息。

3. 1 vs N - RNN

  图4:1 vs N - RNN图示 

当输入的不是序列而输出的是序列时,我们需要将输入作用在每次输出之上,这种结构可以用于将图片生成文字任务。

4. N vs M - RNN

 图5:N vs M - RNN图示 

这是一种不限制输入输出长度的RNN结构,它由编码器和解码器两部分组成,可以理解为编码器部分是N vs 1 - RNN,解码部分为1 vs N - RNN,这类RNN就被理解为Seq2Seq架构,输入数据首先经过编码器,最终输出一个隐含变量c,然后将c作用在解码器解码的每一步,以保证输入信息被有效利用。

二、Seq2Seq问题描述

该模型接受句子作为输入,使用Encoder来生成向量,再用Decoder来生成目标语句。将上面两个模块结合起来就得到了整体的模型。在序列到序列处理不定长序列的过程中,采用了序列的起始标志<sos>和终止标志<eos>来“告诉”编码器的编码过程何时开始与结束,也就是间接反映了当前序列的长度信息。这里将结合到我们后面分析的vocabulary构建字典,对于语料库中的所有词,构建一个字典,实现word2idx和idx2word方法,对于语句的开始和结尾,用<sos>和<eos>填充,对于没有出现在字典中的词,用<unk>填充,然后转为idx,再用模型处理。

三、代码分析

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!"
        
    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
        
        #存储decoder输出的张量
        outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
        
        #encoder最后的隐藏层
        context = self.encoder(src)
        
        #encoder最后的隐藏层作为decoder初始的隐藏层
        hidden = context
        
        #首先输入decoder的是<sos>
        input = trg[0,:]
        
        for t in range(1, trg_len):
            
            #插入嵌入的输入标记,之前的隐藏层状态和context
            #接收输出张量和新的隐藏层状态
            output, hidden = self.decoder(input, hidden, context)
            
            #将预测放在一个张量中,该张量有每个token的预测
            outputs[t] = output
            
            #是否使用teaching_force
            teacher_force = random.random() < teacher_forcing_ratio
            
            #从预测中获得最高的预测token
            top1 = output.argmax(1) 
            
            #使用teaching_force,用实际的下一个token作为输入
            #未使用teaching_force,用预测的token
            input = trg[t] if teacher_force else top1
 
        return outputs

将源序列x馈入编码器以接收上下文张量,创建输出张量保留所有预测。使用一批<sos>令牌作为第一个输入𝑦1,然后,我们在一个循环中解码,将输入令牌𝑦𝑡,先前的隐藏状态𝑠𝑡−1和上下文向量插入解码器,接收预测𝑡+ 1以及新的隐藏状态𝑠𝑡。

训练模型:

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)

从批处理中后去源句子和目标句子X,Y,将最后一批计算出的梯度归零,将源和目标放入模型中输出下一个y,计算梯度同时要防止梯度爆炸,损失值求和最后返回所有批次的平均损失。

评估部分代码:

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} |')

四、Attention模型

传统的Encoder-Decoder是有很多弊端的,后来又提出了Attention模型,这种模型在产生输出的时候,还会产生一个“注意力范围”表示接下来输出的时候要重点关注输入序列中的哪些部分,然后根据关注的区域来产生下一个输出,如此往复。模型的大概示意图如下所示。

目前的attention模型可大致分为两类:

  • 聚焦式(focus)注意力:自上而下的有意识的注意力,主动注意——是指有预定目的、依赖任务的、主动有意识地聚焦于某一对象的注意力;
  • 显著性(saliency-based)注意力:自下而上的有意识的注意力,被动注意——基于显著性的注意力是由外界刺激驱动的注意,不需要主动干预,也和任务无关;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值