0%

UniLMv2

微软提出的另一个尝试统一autoencoder和autoregressive的一个预训练大模型

Code & Paper

Methodology

论文提出了以下三个方法尝试在autoencoder的框架下把autoregressive融合进来

  1. Pseudo Mask
  2. blockwise factorization
  3. PAR

UniLM提出增加一种新的Mask(i.e Pseudo Mask)使LM能够以autoeocode的方式训练,这样在预训练时就能同时进行autoencoder和autoregressive式的训练,由于这仍是cloze式的预训练方式,所以能直接使用诸如Bert这类纯encoder模型。

Pseudo Mask

存在的问题

  • 传统的Mask在输入中随机遮盖几个词,用未遮盖的词去预测Mask中的词语,但这忽略了Mask之间的关系(被遮盖住了不知道是什么)。
  • 在autoregresive的预训练目标中,我们往往能够利用上文中的信息,但无法使用下文的信息(单方向预测),所以autoregresive和autoencoder其实是一种互补的预训练方式
  • AR前边预测出的mask能够被attempt但AE不行,AE中拥有的下文信息却但AR attmpt不到

为了解决以上问题,Pseudo Mask被提出

截屏2022-04-02 下午4.40.47

  1. Pseudo Mask可以看做是和传统的Mask同样的东西,只不过这是在AR下的Mask
  2. Pseudo Mask与XL-net中的处理相类似,做了因子排序的处理,每次sample一种排序方式比如(4,5 –> 2)用前边的因子去预测后边的因子,这样就能补齐AR中无法attmpt下文的缺陷了
  3. 值得注意的是P Mask拥有相同的Position Embedding,因此即使被打乱了顺序模型仍然知道被mask的词之间的顺序
  4. 实际Input变成了两段,前边是传统的Mask式,后边是Pseudo Mask式,但后边的输入序列只有Masked Token+Pseudo Mask

PAR & Blockwise factorization

与传统AR不同,AR往往是一个token一个token的预测,PAR采用了一种span的方式,具体来说:

  • 随机sample一种因子序列
  • 前边的因子必须一次性预测下个因子中内部所有的token
  • PAR采用的Mask(40%span,60%single token)

截屏2022-04-02 下午4.52.49

Finetuning

  • 对于NLU任务,和Bert类似把[CLS]当作Input embedding,把[CLS]输出的向量直接输入到task-specific layer
  • 对于NLG任务,输入是src+target (i.e [SOS] SRC [EOS] TGT [EOS]),但是target是被mask的,所以finetune的目标是恢复target,复现的代码中是为用pseudo mask替换了所有的target token,然后用bert预测所有的pseudo mask原本的token

Experiment

截屏2022-04-02 下午5.02.45

image-20220402171023121

image-20220402171532109

  • UniLMv2效果无论是在NLU还是NLG上效果都是很好的,在NLG上甚至比肩了一些large模型

transformers源码阅读计划 - BertGeneration

transformers是一个基于Pytorch的NLP框架,集成了模型,训练器,tokenizer一系列工具,而且都封装得非常好。更重要的是这个库也复现了许多非常经典的模型比如Bert,XLNet,所以开始一个transformers的源码阅读计划,一方面提高自己的代码复现能力,一边学习transformers内部的结构。

BertGeneration

Papers

BertGeneration提出用Bert同时充当decoder角色来充分利用预训练成本,具体来说非常简单,按照transformer的思想,encoder和decoder的差别不过多了一层cross attention layer。

![image-20220409193520425](/Users/noah/Library/Application Support/typora-user-images/image-20220409193520425.png)

image-20220409193920098

  • 论文还对比了不同情况下这种以Bert为原型的Seq2Seq架构在机器翻译上的效果,发现共享参数的效果也不错

源码

源码路径(models/bert_generation/modeling_bert_generation.py)

模型由五个类构成:

1
2
3
4
5
6
7
8
9
10
class BertGenerationEmbeddings(nn.Module):
# bert的word_embedding
class BertGenerationPreTrainedModel(PreTrainedModel):
# 一个用来保存和恢复checkpoint的抽象类,继承了transformers内部的独有类PreTrainedModel
class BertGenerationEncoder(BertGenerationPreTrainedModel):
# 最重要的类,可以根据args 变成 encoder or decoder
class BertGenerationOnlyLMHead(nn.Module):
# decoder的分类头,用来做vocab的分类的
class BertGenerationDecoder(BertGenerationPreTrainedModel):
# encoder的行为和decoder的行为不同,用一个decoder类定义decoder的行为

BertGenerationEncoder

transformers内部的模型其实都是nn.Module的子类,重点关注___init___和forward两个函数

  • init
1
2
3
4
5
6
7
8
9
10
11
def __init__(self, config):
super().__init__(config)
# config是模型的参数类,定义了模型的一些超参
self.config = config
# embeddings 使用了自己定义的embeddings,encoder直接使用了bert内部的encoder
self.embeddings = BertGenerationEmbeddings(config)
# is_decoder在config被传入BertEncoder
self.encoder = BertEncoder(config)

# Initialize weights and apply final processing
self.post_init()
  • BertEncoder & BertLayer

Encoder这个类主要是判断一下是否传入了is_decoder & add_crossattention这两参数,True时行为差异会比较不同,具体来说

  • Bert Generation这个类复用了transforemrs内部的bert实现BertEncoder&BertLayer

    1
    2
    3
    4
    5
    6
    class BertEncoder(nn.Module):
    def __init__(self, config):
    super().__init__()
    self.config = config
    self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
    self.gradient_checkpointing = False

    layer使用ModuleList容器包装起来,num_hidden_layers决定了这样的BertLayer有几层,在forward中会用for循环展开进行前向计算

  • BertLayer也就是Bert-base的12层layer中的layer实现,当is_decoder传入时会在最末端增加一层cross-attention

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BertLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.chunk_size_feed_forward = config.chunk_size_feed_forward
self.seq_len_dim = 1
# self-attention
self.attention = BertAttention(config)
self.is_decoder = config.is_decoder
self.add_cross_attention = config.add_cross_attention
# 增加了cross-attention
if self.add_cross_attention:
if not self.is_decoder:
raise ValueError(f"{self} should be used as a decoder model if cross attention is added")
# 发现cross-attention和self-attention使用的是同一个attention类说明进行了复用
self.crossattention = BertAttention(config, position_embedding_type="absolute")
# layer中的全连接层
self.intermediate = BertIntermediate(config)
self.output = BertOutput(config)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if self.is_decoder and encoder_hidden_states is not None:
if not hasattr(self, "crossattention"):
raise ValueError(
f"If `encoder_hidden_states` are passed, {self} has to be instantiated with cross-attention layers by setting `config.add_cross_attention=True`"
)

# cross_attn cached key/values tuple is at positions 3,4 of past_key_value tuple
cross_attn_past_key_value = past_key_value[-2:] if past_key_value is not None else None
cross_attention_outputs = self.crossattention(
attention_output, # self-attn的输出做cross-attn的Q输入
attention_mask,
head_mask,
encoder_hidden_states,# 来自encoder的隐藏层做K与V的输入
encoder_attention_mask,
cross_attn_past_key_value,
output_attentions,
)
attention_output = cross_attention_outputs[0]
outputs = outputs + cross_attention_outputs[1:-1] # add cross attentions if we output attention weights
  • forward的输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def forward(
self,
input_ids=None,
attention_mask=None,
position_ids=None,
head_mask=None,# 前四个都是tokenizer的输出
inputs_embeds=None,# 可以直接输入embeding而不是ids,input_ids,inputs_embeds只能输入一个
encoder_hidden_states=None,# 作为decoder时会使用,Cross-Attn的KV
encoder_attention_mask=None,# 作为decoder时会使用,用来屏蔽[PAD]
past_key_values=None,# 作为decoder时会使用,预先算好的所有BertLayer的KV
use_cache=None,# 作为encoder时,True会保留所有的BertLayer的KV然后供decoder使用也就是上面的arg
output_attentions=None,
output_hidden_states=None,
return_dict=None,# 输出时调整输出格式的一些bool参数,详细可以去看doc
):

forward函数也只是简单对encoder进行计算一下然后返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
embedding_output = self.embeddings(
input_ids=input_ids,
position_ids=position_ids,
inputs_embeds=inputs_embeds,
past_key_values_length=past_key_values_length,
)

encoder_outputs = self.encoder(
embedding_output,
attention_mask=extended_attention_mask,
head_mask=head_mask,
encoder_hidden_states=encoder_hidden_states,# 当is_decoder=True时以下3个参数会派上用场
encoder_attention_mask=encoder_extended_attention_mask,
past_key_values=past_key_values,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)

BertGenerationDecoder

BertGenerationDecoder只是对BertGenerationEncoder更高一层的封装

  • init
1
2
3
4
5
6
7
8
def __init__(self, config):
super().__init__(config)

if not config.is_decoder:
logger.warning("If you want to use `BertGenerationDecoder` as a standalone, add `is_decoder=True.`")
# 复用了BertGenerationEncoder,通过config.is_decoder调整BertGenerationEncoder的行为
self.bert = BertGenerationEncoder(config)
self.lm_head = BertGenerationOnlyLMHead(config)
  • forward
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
outputs = self.bert(
input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
head_mask=head_mask,
inputs_embeds=inputs_embeds,
encoder_hidden_states=encoder_hidden_states,
encoder_attention_mask=encoder_attention_mask,
past_key_values=past_key_values,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=return_dict,
)
# 输入的每个token的last hidden layer
sequence_output = outputs[0]
# hidden layer输入vocab的分类头计算每个token的预测值然后存在logits输出
prediction_scores = self.lm_head(sequence_output)

模型测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from transformers import BertGenerationTokenizer, BertGenerationDecoder, BertGenerationConfig
import torch

tokenizer = BertGenerationTokenizer.from_pretrained('google/bert_for_seq_generation_L-24_bbc_encoder')
config = BertGenerationConfig.from_pretrained("google/bert_for_seq_generation_L-24_bbc_encoder")
config.is_decoder = True
model = BertGenerationDecoder.from_pretrained('google/bert_for_seq_generation_L-24_bbc_encoder', config=config)

inputs = tokenizer("How are you", return_token_type_ids=False, return_tensors="pt")
tokenizer.pad_token
outputs = model(**inputs)

prediction_logits = outputs.logits
prediction_logits.shape
1
2
3
4
>>> normalizer.cc(51) LOG(INFO) precompiled_charsmap is empty. use identity normalization.
>>> Some weights of BertGenerationDecoder were not initialized from the model checkpoint at google/bert_for_seq_generation_L-24_bbc_encoder and are newly initialized: ['lm_head.bias', 'lm_head.decoder.weight', 'lm_head.decoder.bias']
>>>You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
>>> torch.Size([1, 3, 50358])

最终得到的就是”How are you”对每个vocab的概率

总结

  • transformers的模型封装的很好,但是因为复用性很强,一个attention模块可以同时充当self-attention和cross-attention使用,这样就导致了阅读源码时会有很多细节需要处理比如每个入参出参的含义,这就增加了阅读源码时的工作量
  • 优势
    • 模型多
    • 复用性强
    • 封装完善
    • 几乎是原生Pytorch实现
  • 劣势
    • 只是实现了比较经典的模型
    • 阅读源码需要对每个模块很熟悉
    • 封装的过于完善也会导致拓展性差

JMeter 测试接口指南

JMeter下载地址

编写测试脚本

  1. 添加线程组,配置对应的并发要求

    img

    img

线程组主要参数详解:
  • 线程数:虚拟用户数。一个虚拟用户占用一个进程或线程。模拟多少用户访问也就填写多少个线程数量。

  • Ramp-Up时间(秒):设置的虚拟用户数需要多长时间全部启动。如果线程数为100,准备时长为5,那么需要5秒钟启动100个线程,也就是每秒钟启动20个线程。 相当于每秒模拟20个用户进行访问,设置为零我理解为并发访问。

  • 循环次数:如果线程数为100,循环次数为100。那么总请求数为100*100=10000 。如果勾选了“永远”,那么所有线程会一直发送请求,直到选择停止运行脚本。

  1. 添加HTTP请求

    • 右键点击 “你的线程组” → “添加” → “取样器” → “HTTP请求”

      img

    • 配置你需要测试的IP,端口,headers,data等

      img

  2. 添加监视器

    • 监视器是用来返回每次请求返回的结果并进行统计的工具

    • 一般聚合统计、查看结果数较为实用,其他的也可以尝试

      img

    img

  3. 为请求添加变量

    • 有些情况下我们的请求中需要一些变量(比如每次请求时都需要更改data中的某一个值),这时就可以增加变量

    • 右键点击 “你的线程组” → “添加” → “配置元件” → “用户定义的变量”:img

    • 新增一个用户名参数

      img

    • 在Http请求中使用该参数,格式为:${key}

      img

    • 改变参数后,每次请求时username将是一个变量,每次请求不同的值(如果你不是设置为常量的话)

      img

  4. 开始测试

    • 点击绿色按钮则JMeter会按照测试计划由上至下(如果你有多个任务需要执行)执行,右边的按钮是清除结果

    img

  5. 查看测试结果

    • 一般聚合报告中有着我们需要的测试参数,如P95,P99等

      img

Python 中的装饰器

最近正在看transformers中的一些源码,transfomers中使用了Python语言中许多特性,比如装饰器等,结合Python3-Cookbook一边看源码一边学习一些特性

装饰器

为了增加代码的复用,经常使用装饰器来修改或者增加函数或者类的行为,下面从最基础的使用开始介绍

无参装饰器

一个最简单的装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
def log(func):
def someprocess():
print("before call")
func()
return someprocess

@log
def test():
print("it's a test func")

before call
it's a test func

使用@等价于

1
test = log(test)

带参装饰器

有的情况下我们需要装饰器带参,但是又想保留不带参数的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import partial

def log(func=None,*,post_message=None):
def someprocess():
print("before call")
func()
if post_message:
print(post_message)
# 如果以log(post_messag=..)调用,此处返回一个固定了post_message的参数的log装饰器
if func is None:
return partial(log,post_message=post_message)
# 如果以log方式调用则直接返回被装饰的函数
return someprocess

@log
def test():
print("it's a test func")

test()

@log(post_message="after call")
def test2():
print("it's a test func")

test2()
  • *代表了后边的参数必须用关键字参数传参,同理还有**\**这个符号表示之前的参数必须使用位置参数传参数
  • partial函数的用法,partial(func,arg1,arg2…)固定func函数的参数,返回一个缩减了参数的*可调用函数

Pytorch 中的 ModuleList & Sequential

ModuleList & Sequential 都是 torch.nn中重要的容器类,是为了方便定义结构化的可复用的网络结构而产生,但是两者的功能又有略微不同。

不同点

  1. 场景不同,ModuleList 可拓展性更强,sequential更方便。

  2. ModuleList需要可以自定义运算顺序,Sequential必须按照定义的顺序依次计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class SeqNet(nn.Module):
def __init__(self):
super().__init__()
# 注意这里Sequential的参数是多个对象,而ModList的是一个数组
self.encoder = nn.Sequential(
nn.Linear(10,128),
nn.ReLU(),
nn.Linear(128,50)
)
def forward(self,x):
# Sequential直接输入X则会按照定义的顺序依次运算
return self.encoder(x)
class ModNet(nn.Module):
def __init__(self):
super().__init__()
self.encoder = nn.ModuleList([
nn.Linear(10,128),
nn.ReLU(),
nn.Linear(128,50)
])
def forward(self,x):
# 如果按照以下方式输入则会报Not Implement error 说明如何运算需要自己定义
# return self.encoder(x)
# 需要我们自己定义整个运算过程
for block in self.encoder:
x = block(x)
return x

  • 可以看到ModuleList拓展性更强,如何运算完全取决于我们如何定义。
  • 在一些结构化很强的模型中使用Sequential更方便,但是对于一些灵活的模型ModuleList更好
  • 两者初始化的参数也有略微不同,Sequential是多个module对象,ModList是一个数组

torch常用函数

  • torch.squeeze <->torch.unsqueeze para:dim= 制定维度降维/升维,[3,1,2]<->[3,2]

  • Torch.scatter 使用索引数组index对原tensor定位修改

  • torch.gather 根据索引数组index取出原tensor中的值

  • torch.split <-> torch.stack 把tensor分开[3,1,2]->[tensor[1,2],tensor[1,2],tensor[1,2]] stack是逆操作

    • stack 用法1:list([tensor1,tensor1]) –> tensor([tensor1,tensor2]),tensor1.size()==tensor2.size()
  • Torch.cat 在指定dim上合并两个tensor [3,1],[3,1] ->[3,2] or [6,1]

  • torch.reshape<-> torch.views 重新调整tensor的维度,但是view要求tensor的必须是连续的

  • permute

  • torch.einsum 爱因斯坦计数法

  • TORCH.NN.UTILS.RNN.PACK_PADDED_SEQUENCE

  • expand 可以指定定维度进行重复 (1,3)->(10,3) (3,1)->(3,10) (3,2)->(2,3,2)

    • repeat创建新tensor,expand仍是原tensor
  • F.pad(tensor,p1d,…) tensor(3,1,2) p1d(1,1,2,0,0,2) ->tensor(0+3+2,2+1+0,1+2+1) ->(5,3,4)

  • Tensor.detach() 返回一个从一个图中分离出的新tensor,对这个新tensor任何求导都不会传播到原tensor

    • Tensor.copy()返回的新tensor求导会传播到原来的图
  • Tril 返回一个2D(或者带Batch的2D) tensor的下三角矩阵,diagonal 是斜方向上的偏移量,如果为n则保留下三角线+上三角线n的区域

  • Tensor.new() 返回一个与原tensor相同type、device的无内容无大小的新tensor

  • torch.arg*

    • tensor.nonzero(as_tuple=False) 返回tensor中非零元素的索引数组

      • as_tuple的用法,转置

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        import torch
        a = torch.arange(4).view((2,2))
        print(a)
        print(a.nonzero(as_tuple=True))
        print(a.nonzero())

        tensor([[0, 1],
        [2, 3]])
        (tensor([0, 1, 1]), tensor([1, 0, 1]))
        tensor([[0, 1],
        [1, 0],
        [1, 1]])
      • nonzero()配合bool mask可以返回某一条件的索引

        1
        2
        3
        4
        import torch
        a = torch.arange(12).view((3,4))
        print(a)
        (a>6).nonzero()
        1
        2
        3
        4
        5
        6
        7
        8
        tensor([[ 0,  1,  2,  3],
        [ 4, 5, 6, 7],
        [ 8, 9, 10, 11]])
        tensor([[1, 3],
        [2, 0],
        [2, 1],
        [2, 2],
        [2, 3]])
      • 配合as_tuple也可以直接返回某一条件的值

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        import torch
        a = torch.arange(16).view((4,4))
        print(a)
        index = (a>8).nonzero(as_tuple=True)
        print(index)
        a[index]

        tensor([[ 0, 1, 2, 3],
        [ 4, 5, 6, 7],
        [ 8, 9, 10, 11],
        [12, 13, 14, 15]])
        (tensor([2, 2, 2, 3, 3, 3, 3]), tensor([1, 2, 3, 0, 1, 2, 3]))
        tensor([ 9, 10, 11, 12, 13, 14, 15])
    • argsort 返回tensor排序后的索引顺序,a.argsort()[i] : 第i小的元素的索引

  • out = tensor.where(condition,x,y) 如果 满足condition out 设为x,不满足为y 三元运算符

Tensor中的索引和筛选

  • 索引:根据下标选出元素,Tensor类型为int

  • 筛选:根据True/False筛选元素,Tensor类型为Bool

    索引保持原维度不变,筛选可能会使维度变小

筛选

1
2
3
4
5
6
import torch
a = torch.randn((3,5))
b = torch.randint(0,3,(5,)).bool()
print(a)
print(b)
a[:,b]
1
2
3
4
5
6
7
tensor([[ 1.2588,  0.1620, -0.3095,  2.0669, -0.3158],
[-1.6065, -0.7930, -0.5658, 1.8601, -0.2592],
[ 1.3226, 0.3040, -1.0924, 0.7583, -1.5444]])
tensor([ True, False, False, True, True])
tensor([[ 1.2588, 2.0669, -0.3158],
[-1.6065, 1.8601, -0.2592],
[ 1.3226, 0.7583, -1.5444]])

可以看出这是在shape[1]上进行了筛选,False的元素会原地删除

索引

1
2
3
4
5
6
import torch
a = torch.randn((3,5))
b = torch.randint(0,3,(5,))
print(a)
print(b)
a[:,b]
1
2
3
4
5
6
7
tensor([[-1.0592,  0.9445, -0.6265,  0.2607, -0.2166],
[ 1.8283, 0.9678, 0.6175, 2.0904, 0.2356],
[ 0.1864, 1.0110, 0.0425, -1.3611, 1.1043]])
tensor([1, 1, 1, 2, 1])
tensor([[ 0.9445, 0.9445, 0.9445, -0.6265, 0.9445],
[ 0.9678, 0.9678, 0.9678, 0.6175, 0.9678],
[ 1.0110, 1.0110, 1.0110, 0.0425, 1.0110]])

可以看出这是在shape[1]上进行了索引,原维度不变,根据下标取出对应的元素

CELoss 和 BCELoss

CE 和BCE都是深度学习里常用来衡量prediction和label之间的差距的损失函数,但是这两者在用法和表达式上又有些差别

表达式

  • BCE虽然形式上是CE中的特例(分类数n=2),但是实际上BCELoss也可以处理多分类的问题
  • BCE的激活函数是sigmoid
  • 当N是batch_size时,这只是一个用BCE解决二分类的问题,当N是n_labels时,则是用BCE解决多分类问题
    • 用BCE解决多分类的问题是对每个labels回答YES or No,即执行n_labels次二分类就变成了多分类
  • BCE的输入一般是 [ n_labels,1 ],需自己把一个batch中的BCELoss相加

image-20220406125736981

  • CE的激活函数是softmax

  • CE可以直接应用在多分类或二分类问题上

  • CE的输入是 [ batch_size, n_labels ]

image-20220406130832519

区别

  1. 为什么激活函数不同?
    • 因为对负样本的处理不同,CE中的Loss看似只提高了正例的概率,没有对负例的概率做降低,但是softmax是一个容易使大者越大,小者越小的激活函数,当正例概率提高,负例的概率自然就会降低。
    • BCE中由于没有softmax,所以需要在做二分类时关注负例,表现在(1-yi)*log(1-xi)上,这部分的损失会被记入,通过这种方式降低负例的概率
  2. 为什么要用BCE处理多分类?
    • softmax计算成本问题