Bert(Bidirectional Encoder Representations from Transformers)
一、简介
Bert即基于Transformer的双向编码器表示,2018年由google提出。基于多个Transformer的编码器堆叠而成,输入输出不改变形状。
Bert的双向不是常规的RNN式的正向反向后连接,指的能根据上下文表示,推测[mask]处的内容。区别可参考这篇博客:解释BERT为什么是双向表示_B站:阿里武的博客-CSDN博客_bert的双向
二、2种无监督预训练任务
1、MLM(Masked Language Model 遮罩式语言模型)
mask策略:随机mask15%,其中,10%替换成其他,10%原地不动,80%替换成mask。取输出的特征进行交叉熵损失计算,并设置ignore_index参数只计算这15%位置的损失,注意不是只根据替换的位置计算损失。“10%替换成其他”使模型的输入词汇不一定正确,更多地学习上下文信息
2、NSP(Next Sentence Prediction 下一句预测)
判断第二个句子是不是第一个句子的下一句,标签为IsNext和NotNext,取第一个cls的特征表示进行交叉熵二分类损失计算。由于包含主体预测及连贯性预测两个信息,如果是同一主题的文本,易被误判为IsNext,这个任务太简单不是很完美,对模型的训练作用比MLM小。
三、token embedding 词嵌入
input = token embedding(词嵌入) + segment embedding(划分两个句子)+position embedding(0、1、2...初始化,后让模型进行学习,而非transformer的正余弦函数)
Bert的tokenizer是先根据符号及空格分割,后根据词表分词。在英文场景中,一般会转为小写处理,do_lower_case。
BertTokenizer = BasicTokenizer + WordPieceTokenizer
BasicTokenizer 基于符号及空格分割,可指定某些词不分割
WordPieceTokenizer将词根据词根、时态等分割为子词(subword)
下例将一个句子转index为[2,3,4,5]后pad了4个0,组成一个长度为8的token,每个token都用一个7维的信息表示
import torch
import torch.nn as nn
max_len = 8
t= torch.tensor([[2,3,4,5,0,0,0,0]])
embed = nn.Embedding(6, 7) # 随机初始化embedding,词表大小为6,每个词用一个7维向量表示
print(embed(t)) # 8*7维度
四、特殊标志位
这些特殊标志位会出现在词表中
[CLS] 标志放在第一个句子的首位,经过 BERT 得到的的表征向量 C 可以用于后续的分类任务。
[SEP] 标志用于分开两个输入句子,例如输入句子 A 和 B,要在句子 A,B 后面增加 [SEP] 标志。
[UNK]标志指的是未知字符
[MASK] 标志用于遮盖句子中的一些单词,将单词 [MASK] 之后,再利用 BERT 输出的 [MASK] 向量预测单词是什么。
[PAD]句子经过tokenizer后转为索引ids,由于transformer要求输入是固定大小的,以此索引列表又会后面补0并pad到固定长度,补0是因为特殊标志位[PAD]索引一般设置为0。PAD解决了输入的不定长问题
五、PAD MASK
为避免pad的地方对注意力机制产生影响,需要获取这些pad的位置,在实际运用中用MASK遮住补0的地方。获取这些pad位置的方法称为get_attn_pad_mask。这个mask会作用于q与k做点积后的矩阵上,因此要保持维度的一致。此外,get_attn_pad_mask是针对key的,当不是自注意力机制时,q与k不同,mask中1的位置(即pad位置)以k为准。注意这个函数的输入是embedding之前,mask是作用在embedding之后
为什么需要让pad_attn_mask的形状为(batch_size, len_q, len_k)呢?众所周知,做注意力的时候是query去与key做点积运算,做embedding之后q和k的形状为(batch_size, q_len, embed_size)和(batch_size, k_len, embed_size),于是两者做点积后的shape变为(batch_size, q_len, k_len),MASK需要与attn_mask形状一致。
import torch
import torch.nn as nn
def get_attn_pad_mask(seq_q, seq_k):
# 在自注意力中,seq_q == seq_k
batch_size, len_q = seq_q.size()
batch_size, len_k = seq_k.size()
# 等于0的即为<PAD>
# .data意思是不在计算图中储存它的梯度
# eq意思是equal,是否相等
pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) # 等于0的地方赋1,其余地方赋0
print('pad_attn_mask=', pad_attn_mask) # pad_attn_mask= tensor([[[0, 0, 0, 1, 1, 1]]], dtype=torch.uint8)
print('pad_attn_mask.size()=', pad_attn_mask.size()) # [batch_size, 1, seq_k_length]= torch.Size([1, 1, 6])
# tensor 中的expand可以理解为重复n次
return pad_attn_mask.expand(batch_size, len_q, len_k) # [batch_size, seq_q_length, seq_k_length]
q = torch.tensor([[2, 3, 4, 1, 0, 0]])
k = torch.tensor([[3, 4, 1, 0, 0, 0]])
mask = get_attn_pad_mask(q, k)
print(mask)
print(mask.size()) # torch.Size([1, 6, 6])
"""
tensor([[[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1],
[0, 0, 0, 1, 1, 1]]], dtype=torch.uint8)
"""
六、attn_pad_mask的两种用法
attn_pad_mask指向的是key中pad的位置,点积前获取,作用在点积后的矩阵上,将这些位置变为-inf,之后再进行softmax,这些位置就是0。
有填充和相加两种方法,都是将pad处变为非常小的数。
先介绍pytorch的2个函数:bmm、masked_fill。
a = torch.tensor([[[1,2,3]],[[1,2,3]]]) # (b,h,w)
b = torch.tensor([[[1],[2],[3]],[[1],[2],[3]]]) # (b,w,h)
print(a.size(),b.size()) # torch.Size([2, 1, 3]) torch.Size([2, 3, 1])
# bmm做矩阵乘法,对输入的2个矩阵尺寸有要求
c = torch.bmm(a,b)
print(c) # tensor([[[14]],[[14]]])
print(c.size()) # (b,h,h)=torch.Size([2, 1, 1])
#masked_fill将tensor的指定位置填充指定值
# 方法一:直接赋负无穷
# attn为q和k的embedding点积之后的矩阵
attn = attn.masked_fill(get_attn_pad_mask, float("-inf"))
# 方法二:加上负无穷
mask = mask.float().masked_fill(mask == 1, float("-inf")).masked_fill(mask == 0, float(0.0))
attn += mask
七、Position Mask
Bert中有2种mask,一个是pad mask,使注意力不关心k中pad的位置;另一个是解码时的Position Mask,预测下一个词时只看到前面的和本身,看不到后面的。
Decoder中的attention与encoder中的attention有所不同。Decoder中的attention中当前单词只受当前单词之前内容的影响,而encoder中的每个单词会受到前后内容的影响。因为编码是并行输入的,解码会用到当前的输出。
实现方法为先用triu做一个上三角矩阵,转置,分别赋予-inf和0
def generate_square_subsequent_mask(sz: int):
print(torch.ones(sz, sz))
print((torch.triu(torch.ones(sz, sz)) == 1))
mask = (torch.triu(torch.ones(sz, sz)) == 1).transpose(0, 1)
print(mask)
mask = mask.float().masked_fill(mask == 0, float('-inf')).masked_fill(mask == 1, float(0.0))
return mask
q= torch.tensor([[2, 3, 0, 0]])
position_mask = generate_square_subsequent_mask(q.size()[-1])
print(position_mask)
# tensor([[1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.],
# [1., 1., 1., 1.]])
# tensor([[1, 1, 1, 1],
# [0, 1, 1, 1],
# [0, 0, 1, 1],
# [0, 0, 0, 1]], dtype=torch.uint8)
# tensor([[1, 0, 0, 0],
# [1, 1, 0, 0],
# [1, 1, 1, 0],
# [1, 1, 1, 1]], dtype=torch.uint8)
# tensor([[0., -inf, -inf, -inf],
# [0., 0., -inf, -inf],
# [0., 0., 0., -inf],
# [0., 0., 0., 0.]])
八、整合PAD MASK和位置遮挡
解码时这2种mask要叠加,即将两种mask相加,再和点乘后的矩阵相加
attn_mask = mask + position_mask
attn += attn_mask
attn = softmax(attn)
九、位置编码
相对位置编码有学习和正余弦函数两种方式
- 正余弦函数法:在正余弦表示中,位置编码对token在句子中的位置POS及词向量的位置(单词含义)2i/2i+1都是敏感的。形状同word embedding,是一个二维矩阵,行为token长度,列为词向量长度。
PE(pos,2i)=sin(pos/100002i/dmodel)
PE(pos,2i+1)=cos(pos/100002i/dmodel)
import torch
import torch.nn as nn
Tensor = torch.Tensor
def positional_encoding(X, num_features, dropout_p=0.1, max_len=512) -> Tensor:
r'''
给输入加入位置编码
参数:
- num_features: 输入进来的维度,word embedding时表示词向量的维度
- dropout_p: dropout的概率,当其为非零时执行dropout
- max_len: 句子的最大长度,默认512
形状:
输出=输入+pisitional encoding,因此输入输出维度保持一致
- 输入: [batch_size, seq_length, num_features]
- 输出: [batch_size, seq_length, num_features]
'''
dropout = nn.Dropout(dropout_p)
P = torch.zeros((1, max_len, num_features)) # P为位置编码矩阵
X_ = torch.arange(max_len, dtype=torch.float32).reshape(-1, 1) / torch.pow(
10000,
torch.arange(0, num_features, 2, dtype=torch.float32) / num_features)
P[:, :, 0::2] = torch.sin(X_) # 偶数位置
P[:, :, 1::2] = torch.cos(X_) # 奇数位置
X = X + P[:, :X.shape[1], :].to(
X.device) # X为输入,P为位置编码矩阵,相加后作为多头attention的输入。由于这个例子中X为(2,4,10),而不是(2,512,10),所以还用X.shape[1]做了一下截断
return dropout(X)
X = torch.randn((2, 4, 10)) # (batch, seq_len, num_features) = (2,4,10),这个例子里还没做pad
X = positional_encoding(X, 10)
print(X.shape) # torch.Size([2, 4, 10])
十、下游任务
1、句子对分类/文本匹配:取出cls信息做n分类下游任务,如nsp
2、单个句子分类
3、QA问答:取出一个句子中间的start及end作为答案
4、序列标注:序列标注,对每个词进行bio分类从而进行ner
十一、优化的Bert
这部分主要讲解原生bert存在的问题,以及后人在此基础上的改进。
BERT、ALBERT、RoBerta、ERNIE模型对比和改进点总结 - 知乎
ALBERT(A Lite BERT 一个精简的 BERT)
通过因式分解及跨层参数共享减小参数量,提出Sentence-order prediction (SOP序列顺序预测)来取代NSP
RoBERTa(A Robustly Optimized BERT 一个强力优化的Bert)
主要是训练技巧(动态mask技巧、更大batch_size、训练任务、更大的词汇表(更大的Byte-Pair Encoding))、更大数据集大小等细节的优化
ERNIE百度
MLM直接对单个token进行随机mask,丢失了短语和实体信息,这一点对中文尤其明显。利用短语和实体级别的mask方式,更多的中文语料
参考链接:
BERT 的 PyTorch 实现(超详细)_数学家是我理想的博客-CSDN博客_bert pytorch
一篇看懂所有关于Transformer在翻译任务中的细节_sherlock31415931的博客-CSDN博客_transformer翻译任务
从Word Embedding到Bert模型—自然语言处理中的预训练技术发展史 - 知乎
bert 源码解读(基于gluonnlp finetune-classifier)_sinat_34022298的博客-CSDN博客_bert源码
更多推荐
所有评论(0)