首页 > 其他 > 详细

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

时间:2019-07-23 22:23:54      阅读:124      评论:0      收藏:0      [点我收藏+]

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 进行加权求和。

    1. 返回最终的(拓展)单词表上的概率分布,以及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

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

原文:https://www.cnblogs.com/lauspectrum/p/11234831.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!