• [code] Transformer For Summarization Source Code Reading [2]


    Basic Information

    作者:李丕绩(腾讯AI Lab)

    模型:Transformer + copy mechanism for abstractive summarization

    数据集:CNN/Daily Mail

    Debug

    1. run (main.py)

    1. 指定变量existing_model_name,载入之前训练保存的模型参数

    2. 进入run

    3. 利用函数init_modules,初始化模型参数modules, constants, options。其中具体的超参数,在config.py中的class DeepmindConfigs中已有定义;将已经处理好的字典载入modules

    4. 打印模型信息:函数print_basic_info,主要是modules, constants, options等参数,或者操作(初始化model之后,也可以将model打印一下)

    5. 载入数据集

    6. 函数datar.batched返回batch list(每个子列表中是batch size个样本的索引),总的样本数,以及batch的总数目

    7. 实例化模型:model = Model(modules, consts, options)

    8. 定义优化器:optimizer = torch.optim.Adagrad(model.parameters(), lr=consts["lr"], initial_accumulator_value=0.1)

    9. 如果需要载入之前训练的模型,使用函数load_model载入model和optimizer的参数

    10. 进入epoch循环:每个epoch都会对训练集重新进行打乱,重新生成batch list

    11. 进入batch循环:按照当前batch list中子序列中的索引id,索引出对应的原始数据

    12. 利用函数datar.get_data得到一个批量的数据——返回一个类class BatchData的实例(包含进行了word2id的输入x,目标y,考虑拓展词表的x_ext,y_ext,以及记录补零位的x_mask,y_mask;此时所有的数据都是numpy array

    13. 拓展词表x_ext_word,拓展词表是一个batch中所有样本共享的,拼接到fixed vocab的后面;y_ex区别于y之处:后者的OOV被设置为<unk>,但是前者给出了OOV在extended vocabulary中的索引;y_input是对y进行了shift,头部添加了标志位<bos>,作为解码输入

    14. 根据一个batch的数据进行训练,惯常操作:

      # 梯度置零
      model.zero_grad()
      
      # 前向传播
      y_pred, cost = model()  # 输出y_pred是为了打印预测结果
      
      # 反向传播
      cost.backward()  # 保证反向传播的路径通畅
      
      # 梯度裁剪
      torch.nn.utils.clip_grad_norm_(model.parameters(), consts["norm_clip"])
      
      # 优化参数
      optimizer.step()
      
    15. 打印信息

      1. print_size = num_files // print_time;表示每个epoch打印print_time次信息,也就是每处理print_size个数据打印一次信息
      2. 打印信息的同时保存一次训练好的model
      3. 每个epoch打印一次epoch的平均loss,以及所用的时间
      4. 如果上一个epoch的total error大于当前epoch的total error,则继续优化;否则,优化结束。
      5. 这部分是可以优化的:
        1. 多打印一些信息,少保存些model
        2. 每个epoch进行一次validation,并且打印出来生成的摘要
        3. 这个implementation没有用scheduled sampling可以尝试加入
    16. 结束训练

    17. 进行inference(此时必须载入训练好的model,以及预设的参数)

    18. 载入测试数据,利用datar.batched划分batch,函数datar.get_data获取一个batch的数据

    19. 编码:用模型的encodermodel.encode

    20. 解码(摘要生成):此处使用了beam search method;还分为了copy模式、以及non-copy模式

    21. 进入beam_decoding函数(简述操作,不析代码):

      1. 输入了考虑OOV的x_ext,x_mask(OOV对mask没有影响,mask的是padding位),word_embed(encoder的编码输出);padding_mask与x_mask是互为相反(1,0位置)
      2. x_ext_words是一个列表,子列表中是每个样本的OOV词表;max_ext_len指的是当前batch中,最大的OOV词表容量;
      3. 还输入了ground truth y,以及原始的summary,为了计算生成的摘要的ROUGE
      4. 执行beam search decoding:采样;排序;选择TOP N;迭代进行;得到beam search的结果,并写入文件

    2. model (model.py)

    基本流程:

    forward()
    │
    └─── encode()
    │
    └─── decode()
    │   └───decoding()
    │   └───word_prob_layer()
    │
    └─── label_smoothing_loss() or nll_loss()
    

    2.1 input

    传入model的数据(全部来自于类BatchData),该pytorch代码的实现中,batch_first全部设置为False:

    torch.LongTensor(batch.x).to(options["device"])
    # shape: [max_x_len, batch_size]
    # 不考虑OOV的word2id处理后的source article
    
    torch.LongTensor(batch.y_inp).to(options["device"])
    # shape: [max_y_len, batch_size]
    # 是y的基础上向后shift了一位,作为解码输入
    
    torch.LongTensor(batch.y).to(options["device"])
    # shape: [max_y_len, batch_size]
    # 不考虑OOV的word2id处理后的target summary
    
    torch.LongTensor(batch.x_ext).to(options["device"])
    # shape: 
    # 考虑了OOV的word2id处理后的source article
    
    torch.LongTensor(batch.y_ext).to(options["device"])
    # shape: 
    # 考虑了OOV的word2id处理后的target summary
    
    torch.FloatTensor(batch.x_mask).to(options["device"])
    # shape: [max_x_len, batch_size, 1]
    # 欲将一个batch中的所有序列对其,需要将每个序列补足到最大长度,mask掉padding位
    
    torch.FloatTensor(batch.y_mask).to(options["device"])
    # shape: [max_y_len, batch_size, 1]
    
    batch.max_ext_len
    # scalar
    # 每个sample对应一个OOV list,一个batch中OOV的最大容量
    
    

    2.2 encode

    Args: 
    	x: source article经过word2id的表示,尚未进行embedding
    
    Returns:
    	encoding hidden state:经过Transformer encoder编码的结果
    	source padding mask:padding位置1
    
    1. 预处理

      1. token embedding + positional embedding
      2. 层标准化(对embedding进行normalization
      3. dropout(对于embedding)
    2. 进入N encoder stack逐层进行编码。每一层的输出是隐含状态,每一层输入是上一层的输出

      xs = []
      for layer_id, layer in enumerate(self.enc_layers):
          x, _ ,_ = layer(x, self_padding_mask=padding_mask)  # 进行N个blocks的编码
          xs.append(x)
          
      

      列表 xs 保存了每一层的隐含状态。

      encoder layer的输入输出:

      1. 输入的shape:[max_x_len, batch_size, embedding_dim];

      2. 输出的shape:[max_x_len, batch_size, embedding_dim]

      每一层的输入有二:1. 上一层的输出,2. padding mask;每一层encoder是一个Transformer单元,执行的操作有:

      1. 记录residual
      2. self-attention (query = key = value = x + dropout
      3. add residual + attention normalization
      4. 记录residual
      5. 全连接(从embed_dim映射到d_ff,维度变大
      6. GELU activation function + dropout
      7. 全连接(从d_ff映射回embed_dim)
      8. dropout + add residual + feed forward normalization

      其中,attention normalization和 feed forward normalization使用的使用一个class LayerNorm。所做的操作都是减去均值,除以标准差,并且经过一个全连接层的映射。

    3. final encoder layer output 作为 输入编码返回

    2.3 decode

    训练时用的是Teacher Forcing的decoder,需要输入shifted ground truth作为decoder input。

    Args:
        y_inp		:shifted y
        			[max_y_len, batch_size]
            
        mask_y		:非padding的位置置为1
        			[max_y_len, batch_size, 1]
            
        mask_x		:非padding的位置置为1
        			[max_x_len, batch_size, 1]
            
        hs			:source article经过编码的隐含向量
        			[max_x_len, batch_size, embed_dim]
            
        src_padding_mask	:padding的位置置为1
        			[max_x_len, batch_size]
            
        x_ext		:考虑OOV的source article对应的ids(numpy array)
        			[max_x_len, batch_size]
            
        max_ext_len	:最大的OOV容量
    
    Returns:
        y_dec
        
        attn_dist
    
    1. 预处理(与embedding相同)

      1. token embedding + positional embedding
      2. 层标准化(对embedding进行normalization
      3. dropout(对于embedding)
      4. attention mask,使用了Transformer中定义的类class SelfAttentionMask。此处返回了一个上三角矩阵(非零元素全为1),作为self_attn_mask
    2. 进入N层解码。与encoder不同的时,decoder输入了encoder hidden states作为external memory,及其对应的padding(均为encoder的输出),以及self-attention mask。

      Transformer内部的具体操作流程为:

      1. 记录residual
      2. self-attention (query = key = value = x)+ dropout (注意!encoder和decoder的self-attention中的参数不一样,虽然都是相同的attention结构;model中对encoder、decoder定义的时候各自对Transformer分别进行了实例化)
      3. add residual + attention normalization
      4. 外部attention: y_inp的嵌入表示作为query,编码状态作为key和value,进行Multihead Attention的计算
      5. 记录residual
      6. 全连接(从embed_dim映射到d_ff,维度变大
      7. GELU activation function + dropout
      8. 全连接(从d_ff映射回embed_dim)
      9. dropout + add residual + feed forward normalization

      记录transformer中的形状变化:

      x, residual 			[sequence_length, batch_size, embedding_dim]
      self-attention 			[sequence_length, batch_size, embedding_dim]
      dropout, add & norm 	操作不改变形状
      
      residual 				[sequence_length, batch_size, embedding_dim]
      external-attention 		[sequence_length, batch_size, embedding_dim]
      dropout, add & norm 	操作不改变形状
      
      residual 				[sequence_length, batch_size, embedding_dim]
      fully connected layer	[sequence_length, batch_size, ff_dim]
      gelu, dropout			操作不改变形状
      fully connected layer	[sequence_length, batch_size, embedding_dim]
      dropout, add & norm 	操作不改变形状
      

      最终Transformer的输入输出形状相同

    2.4 vocabulary probability distribution (word_rob_layer.py)

    利用解码结果,映射到单词表维度上进行vocabulary probability distribution的计算

    Args:
        h: 			decoder output (hidden states)
        y_emb: 		decoder input(shifted)
        memory: 	encoder output (hidden states)
        mask_x: 	encoder output (padding_mask)
        xids: 		source article in the id representation, considering OOVs
        max_ext_len: maximum length of OOV lists
    
    Returns:
        pred
        dists		decoder-encoder attention [max_y_len, batch_size, max_x_len]
    

    计算单词表上概率分布的流程:

    1. query = h,key = value = memory 进行一次external attention的计算,返回contexts,以及对应的attention weights,记作dists,shape:[num_query, batch_size, num_kv]

    2. pred的计算:

      1. 基于decoder states h,decoder input y_emb,以及encoder-decoder context(external attention的输出)

      2. 将三者进行concatenation;shape:[max_y_len, batch_size, embed_dim * 3]

      3. 再将concatenation的hidden state维度映射到vocabulary size(fixed vocabulary);shape:[max_y_len, batch_size, vocab_size]

      4. 再在单词表的维度上进行softmax表示成概率分布

      5. 给pred拼接上全零Tensor,使得单词表的维度大小变为vocabulary size + max_ext_len(extended vocabulary),shape: [max_y_len, batch_size, vocab_ext_size]

      6. 利用上述的concatenation,经过全连接层,和sigmoid激活函数,得到一个gate,[max_y_len, batch_size, 1]

      7. xids 的shape原本是 [max_x_len, batch_size], 经过一次转置,在第一个维度上复制max_y_len次,形状变为 [max_y_len, batch_size, max_x_size]

      8. 用下述函数得到最终的概率分布:

        pred = (g * pred).scatter_add(2, xids, (1 - g) * dists)
        
        # selfTensor.scatter_add_(dim, indexTensor, otherTensor)
        # 该函数将otherTensor的所有值加到selfTensor中,加入位置由indexTensor指明
        # 理论上indexTensor和otherTensor的形状应该相同,index_Tensor中的元素取值范围应该小于selfTensor的指定维度dim
        
        # For a 3-D tensor, :attr:`self` is updated as::
        #    self[index[i][j][k]][j][k] += other[i][j][k]  # if dim == 0
        #    self[i][index[i][j][k]][k] += other[i][j][k]  # if dim == 1
        #    self[i][j][index[i][j][k]] += other[i][j][k]  # if dim == 2
        
      
      (g * pred)	  shape: [max_y_len, batch_size, vocab_ext_size]
      
      xids				shape: [max_y_len, batch_size, max_x_len]
      
      (1 - g) * dists shape: [max_y_len, batch_size, max_x_len]
      
      xids 中的元素(id)对应着(g * pred)中的每一个单词,也对应着(1 - g) * dists中的每一个(出现在source article中的)单词的注意力权重;
      
      将**这些单词**对应的(decoder-encoder)**attention weights**,与利用decoder输出计算得到的**pred** 进行加权求和。
      
      9. 返回最终的(拓展)单词表上的概率分布,以及decoder-encoder之间的attention weights
      
      

    2.5 loss function (label_smoothing.py)

    Args:
    	y_inp: 		解码输入(shifted)
    	y_tgt: 		目标序列(not shifted)
    	mask_y:  	非padding位置1,padding位被置0
    
    Returns: 
    	avg_loss:	返回该样本中的平均loss:总loss / 总输出词数(而非 总loss / batch size)
    
    1. 预处理

      y_pred = T.log(y_pred.clamp(min=1e-8))
      

      将y_pred中的概率值小于1e-8的数据,全部设置为1e-8

    2. label smoothing

      def forward(self, output, target):
          # 计算real_size,此处相当与之前的max_ext_len
          if output.size(1) > self.size:
              real_size = output.size(1) - self.size
          else:
              real_size = 0
      
          model_prob = self.one_hot.repeat(target.size(0), 1)
          if real_size > 0:
              ext_zeros = torch.full((model_prob.size(0), real_size), self.smoothing_value).to(self.device)
              model_prob = torch.cat((model_prob, ext_zeros), -1)
              
          model_prob.scatter_(1, target, self.confidence)
          model_prob.masked_fill_((target == self.padding_idx), 0.)
          
          return F.kl_div(output, model_prob, reduction='sum')
      

      流程:

      1. 将y_pred(shape: [max_y_len, batch_size, vocab_ext_size])展开成 [max_y_len * batch_size, vocab_ext_size]

      2. 将y_tgt(shape: [max_y_len, batch_size])展开成 [max_y_len * batch_size, 1]

      3. model_prob由one_hot向量repeat而来,shape: [max_y_len * batch_size, vocab_size]

      4. ext_zeros的shape [max_y_len * batch_size, max_ext_len],其中的元素的值均为smoothing value = 2e-6

      5. 将两者进行concatenation,得到model_prob,shape:[max_y_len * batch_size, vocab_ext_size]

      6. 在model_prob中target对应元素的位置,加上confidence = 0.9

        model_prob.scatter_(1, target, self.confidence)
        
        # torch.Tensor.scatter_(dim, index, src) -> Tensor
        # 该函数将src加到self中,加入位置由index指明
        
        # For a 3-D tensor, :attr:`self` is updated as::
        #    self[index[i][j][k]][j][k] += src  # if dim == 0
        #    self[i][index[i][j][k]][k] += src  # if dim == 1
        #    self[i][j][index[i][j][k]] += src  # if dim == 2
        
      7. 对结果进行mask

        model_prob.masked_fill_((target == self.padding_idx), 0.)
        
        # torch.Tensor.masked_fill_(mask, value)
        #     Args:
        #         mask (ByteTensor): the binary mask
        #         value (float): the value to fill in with
        # mask中元素为1的位置,被替换为value
        # selfTensor应该与maskTensor形状一样
        
      8. 返回model_prob与output之间的KL散度作为 label smoothing loss

  • 相关阅读:
    系统架构技能之设计模式组合模式
    系统架构师基础到企业应用架构单机软件架构
    设计模式系列装饰模式
    设计模式系列命令模式
    设计模式系列外观模式
    设计模式系列原型模式
    设计模式系列代理模式
    设计模式系列桥接模式
    设计模式系列适配器模式
    设计模式系列享元模式
  • 原文地址:https://www.cnblogs.com/lauspectrum/p/11234831.html
Copyright © 2020-2023  润新知