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%替换成其他”使模型的输入词汇不一定正确,更多地学习上下文信息

BERT中是怎么做到只计算[MASK]token的CrossEntropyLoss的?及torch.nn.CrossEntropyLoss()参数__illusion_的博客-CSDN博客_mask token

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的即为
    # .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翻译任务

图解Bert系列之Transformer实战 (附代码)

从Word Embedding到Bert模型—自然语言处理中的预训练技术发展史 - 知乎

bert 源码解读(基于gluonnlp finetune-classifier)_sinat_34022298的博客-CSDN博客_bert源码

BERT源码分析PART I - 知乎

你可能感兴趣的:(bert,自然语言处理,小程序)