bubbleeee

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

bert

BERT 可以用于问答系统,情感分析,垃圾邮件过滤,命名实体识别,文档聚类等任务中,作为这些任务的基础设施,语言模型

使用了双向Transformer作为算法的主要框架,但只利用了 Transformer 的 encoder 部分。因为BERT 的目标是生成语言模型,所以只需要 encoder 机制。

使用了Mask Language Model(MLM)和 Next Sentence Prediction(NSP) 的多任务训练目标

输入表示包含了3个组成部分: 词嵌入张量: word embeddings;语句分块张量: segmentation embeddings;位置编码张量: position embeddings。最终的embedding向量是将上述的3个向量直接做加和的结果。

Bert_BASE:Layer = 12, Hidden = 768, Head = 12, Total Parameters = 110M

Bert_LARGE:Layer = 24, Hidden = 1024, Head = 16, Total Parameters = 340M

bert模型的输入输出

模型输入

3个embedding:token embedding字向量,position embedding位置向量,segement embedding文本向量

  • 关于token embedding

token embedding要将各个词转换成固定维度的向量。在BERT中,每个词会被转换成768维的向量表示。输入文本在送入token embeddings 层之前要先进行tokenization处理。

tokenization使用的方法是WordPiece tokenization。两个特殊的token会被插入到tokenization的结果的开头 ([CLS])和结尾 ([SEP]) 

  • 关于position embedding
    BERT中的Position Embedding和Transformer不一样,transormer中式直接利用公式,计算出对应维度的值。在BERT中是要学习的。比如说d_model的大小为512,那么每个句子就会生成一个[0,1,2,...511]的一维数组,然后重复batch次,因此实际的输入为[batch,d_model],将其送到one_hot中进行编码,具体的编码过程和Token Embedding一样,然后最后的输出为[batch,seq_len,d_model]。和Token Embedding输出的维度一样
  • 关于segement embedding
  • 分词

(参考原文链接:https://blog.csdn.net/u010099080/article/details/102587954)

BERT 源码中 tokenization.py 就是预处理进行分词的程序,主要有两个分词器:BasicTokenizerWordpieceTokenizer,另外一个 FullTokenizer 是这两个的结合:先进行BasicTokenizer 得到一个分得比较粗的 token 列表,然后再对每个 token 进行一次 WordpieceTokenizer,得到最终的分词结果。

BasicTokenizer是一个初步的分词器。对于一个待分词字符串,流程大致就是转成 unicode-->去除各种奇怪字符-->处理中文(判断是否为中文,中文按字符分隔)-->空格分词-->去除多余字符和标点分词-->再次空格分词

WordpieceTokenizer是在BT结果的基础上进行再一次切分,得到子词(subword,以 ## 开头),词汇表就是在此时引入的。该类只有两个方法:一个初始化方法__init__(self, vocab, unk_token="[UNK]", max_input_chars_per_word=200),一个分词方法tokenize(self, text)。tokenize(self, text):该方法就是主要的分词方法了,大致分词思路是按照从左到右的顺序,将一个词拆分成多个子词,每个子词尽可能长。 按照源码中的说法,该方法称之为 greedy longest-match-first algorithm,贪婪最长优先匹配算法。

样本方面

def convert_single_example( max_seq_length, tokenizer,text_a, text_b=None):
    tokens_a = tokenizer.tokenize(text_a)
    tokens_b = None
    if text_b:
        tokens_b = tokenizer.tokenize(text_b)# 这里主要是将中文分字
    if tokens_b:
        # 如果有第二个句子,那么两个句子的总长度要小于 max_seq_length - 3
        # 因为要为句子补上[CLS], [SEP], [SEP]
        _truncate_seq_pair(tokens_a, tokens_b, max_seq_length - 3)
    else:
        # 如果只有一个句子,只用在前后加上[CLS], [SEP] 所以句子长度要小于 max_seq_length - 3
        if len(tokens_a) > max_seq_length - 2:
            tokens_a = tokens_a[0:(max_seq_length - 2)]

    # 转换成bert的输入,注意下面的type_ids 在源码中对应的是 segment_ids
    # (a) 两个句子:
    #  tokens:   [CLS] is this jack ##son ##ville ? [SEP] no it is not . [SEP]
    #  type_ids: 0     0  0    0    0     0       0 0     1  1  1  1   1 1
    # (b) 单个句子:
    #  tokens:   [CLS] the dog is hairy . [SEP]
    #  type_ids: 0     0   0   0  0     0 0
    #
    # 这里 "type_ids" 主要用于区分第一个第二个句子。
    # 第一个句子为0,第二个句子是1。在预训练的时候会添加到单词的的向量中,但这个不是必须的
    # 因为[SEP] 已经区分了第一个句子和第二个句子。但type_ids 会让学习变的简单

    tokens = []
    segment_ids = []
    tokens.append("[CLS]")
    segment_ids.append(0)
    for token in tokens_a:
        tokens.append(token)
        segment_ids.append(0)
    tokens.append("[SEP]")
    segment_ids.append(0)
    if tokens_b:
        for token in tokens_b:
            tokens.append(token)
            segment_ids.append(1)
        tokens.append("[SEP]")
        segment_ids.append(1)
    input_ids = tokenizer.convert_tokens_to_ids(tokens)# 将中文转换成ids
    # 创建mask
    input_mask = [1] * len(input_ids)
    # 对于输入进行补0
    while len(input_ids) < max_seq_length:
        input_ids.append(0)
        input_mask.append(0)
        segment_ids.append(0)
    assert len(input_ids) == max_seq_length
    assert len(input_mask) == max_seq_length
    assert len(segment_ids) == max_seq_length
    return input_ids,input_mask,segment_ids # 对应的就是创建bert模型时候的input_ids,input_mask,segment_ids 参数
  • 对于文本分类任务,BERT模型在文本前插入一个[CLS]符号,并将该符号对应的输出向量作为整篇文本的语义表示

  • 语句对分类任务:该任务的实际应用场景包括:问答(判断一个问题与一个答案是否匹配)、语句匹配(两句话是否表达同一个意思)等。对于该任务,BERT模型除了添加[CLS]符号并将对应的输出作为文本的语义表示,还对输入的两句话用一个[SEP]符号作分割,并分别对两句话附加两个不同的文本向量以作区分

  • 序列标注任务:该任务的实际应用场景包括:中文分词&新词发现(标注每个字是词的首字、中间字或末字)、答案抽取(答案的起止位置)等。对于该任务,BERT模型利用文本中每个字对应的输出向量对该字进行标注(分类),如下图所示(B、I、E分别表示一个词的第一个字、中间字和最后一个字)。

 模型输出

 获取bert模型的输出,使用 model.get_sequence_output()model.get_pooled_output() 两个方法。

output_layer = model.get_sequence_output()# 这个获取每个token的output 输出[batch_size, seq_length, embedding_size] 
"""Gets final hidden layer of encoder.

Returns:
float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
to the final hidden of the transformer encoder.
"""
 output_layer = model.get_pooled_output() # 这个获取句子的output

mask问题

在 BERT 的 Masked LM 训练任务中, 会用 [MASK] token 去替换语料中 15% 的词,然后在最后一层预测。但是下游任务中不会出现 [MASK] token,导致预训练和 fine-tune 出现了不一致,为了减弱不一致性给模型带来的影响,在这被替换的 15% 语料中:

1. 80% 的 tokens 会被替换为 [MASK] token

2. 10% 的 tokens 会称替换为随机的 token

3. 10% 的 tokens 会保持不变但需要被预测

第一点中的替换:是 Masked LM 中的主要部分,可以在不泄露 label 的情况下融合真双向语义信息;

第二点的随机替换:因为需要在最后一层随机替换的这个 token 位去预测它真实的词,而模型并不知道这个 token 位是被随机替换的,就迫使模型尽量在每一个词上都学习到一个 全局语境下的表征,因而也能够让 BERT 获得更好的语境相关的词向量(这正是解决一词多义的最重要特性);

第三点的保持不变:也就是真的有 10% 的情况下是 泄密的(占所有词的比例为15% * 10% = 1.5%),这样能够给模型一定的bias,相当于是额外的奖励,将模型对于词的表征能够拉向词的真实表征(此时输入层是待预测词的真实 embedding,在输出层中的该词位置得到的embedding,是经过层层 Self-attention 后得到的,这部分 embedding 里多少依然保留有部分输入embedding 的信息,而这部分就是通过输入一定比例的真实词所带来的额外奖励,最终会使得模型的输出向量朝输入层的真实 embedding 有一个偏移)。 而如果全用 mask 的话,模型只需要保证输出层的分类准确,对于输出层的向量表征并不关心,因此可能会导致最终的向量输出效果并不好。

被随机选择15%的词当中以10%的概率用任意词替换去预测正确的词,相当于文本纠错任务,为BERT模型赋予了一定的文本纠错能力;被随机选择15%的词当中以10%的概率保持不变,缓解了finetune时候与预训练时候输入不匹配的问题(预训练时候输入句子当中有mask,而finetune时候输入是完整无缺的句子,即为输入不匹配问题)

模型结构

 关于attention机制

Attention机制将目标字上下文各个字的语义向量表示作为输入,首先通过线性变换获得目标字Query向量表示、上下文各个字Key向量表示以及目标字与上下文各个字原始Value表示,然后计算Query向量与各个Key向量的相似度作为权重,加权融合目标字的Value向量和各个上下文字的Value向量,作为Attention的输出,即:目标字的增强语义向量表示

Self-Attention:对于输入文本,我们需要对其中的每个字分别增强语义向量表示,因此,我们分别将每个字作为Query,加权融合文本中所有字的语义信息,得到各个字的增强语义向量,如下图所示。在这种情况下,QueryKeyValue的向量表示均来自于同一输入文本,因此,该Attention机制也叫Self-Attention

attention和self-attention 区别描述(面试被问了无数遍还不长记性ε=(´ο`*)))):

  • 一般在自然语言处理应用里会把Attention模型看作是输出Target句子中某个单词和输入Source句子每个单词的对齐模型。
  • 目标句子生成的每个单词对应输入句子单词的概率分布可以理解为输入句子单词和这个目标生成单词的对齐概率,这在机器翻译语境下是非常直观的:传统的统计机器翻译一般在做的过程中会专门有一个短语对齐的步骤,而注意力模型其实起的是相同的作用。
  • 在一般任务的Encoder-Decoder框架中,输入Source和输出Target内容是不一样的,比如对于英-中机器翻译来说,Source是英文句子,Target是对应的翻译出的中文句子,Attention机制发生在Target的元素Query和Source中的所有元素之间。而Self Attention顾名思义,指的不是Target和Source之间的Attention机制,而是Source内部元素之间或者Target内部元素之间发生的Attention机制,也可以理解为Target=Source这种特殊情况下的注意力计算机制。其具体计算过程是一样的,只是计算对象发生了变化而已

 

 Multi-head Self-Attention:为了增强Attention的多样性,文章作者进一步利用不同的Self-Attention模块获得文本中每个字在不同语义空间下的增强语义向量,并将每个字的多个增强语义向量进行线性组合,从而获得一个最终的与原始字向量长度相同的增强语义向量

使用多头注意力,能够从不同角度提取信息,提高信息提取的全面性

 【Multi-head Self-Attention的输入和输出在形式上完全相同,输入为文本中各个字的原始向量表示,输出为各个字融合了全文语义信息后的增强向量表示】

残差连接(ResidualConnection)

将模块的输入与输出直接相加,作为最后的输出。这种操作背后的一个基本考虑是:修改输入比重构整个输出更容易(“锦上添花”比“雪中送炭”容易多了!)。这样一来,可以使网络更容易训练。解决网络层数比较多时产生的梯度消失的问题

Layer Normalization

对某一层神经网络节点作0均值1方差的标准化。

batch normalization 和 layer normalization

batch normalization是对一批样本的同一维度进行归一化。因为统计意义,在batch_size较大时才表现较好;不易用于RNN

【bert采用】layer normalization是对一个样本的所有维度进行归一化。比较适合用于RNN和单条样本的训练和预测。但是在batch_size较大时性能时比不过batch normalization

经过归一化再输入激活函数,得到的值大部分会落入非线性函数的线性区,导数远离导数饱和区,避免了梯度消失,这样来加速训练收敛过程。

BatchNorm这类归一化技术,目的就是让每一层的分布稳定下来,让后面的层可以在前面层的基础上安心学习知识。

BatchNorm就是通过对batch size这个维度归一化来让分布稳定下来。LayerNorm则是通过对Hidden size这个维度归一。

batch normalization是对一批样本的同一维度进行归一化

layer normalization是对一个样本的所有维度进行归一化

BatchNorm就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的。BN就是通过一定的规范化手段,把每层神经网络任意神经元这个输入值的分布强行拉回到均值为0方差为1的标准正态分布,其实就是把越来越偏的分布强制拉回比较标准的分布,这样使得激活输入值落在非线性函数对输入比较敏感的区域,这样输入的小变化就会导致损失函数较大的变化,意思是这样让梯度变大,避免梯度消失问题产生,而且梯度变大意味着学习收敛速度快,能大大加快训练速度。

LN常被用于小mini-batch场景、动态网络场景和 RNN,特别是自然语言处理领域,就bert来说就是对每层输出的隐层向量(768维)做规范化,图像领域用BN比较多的原因是因为每一个卷积核的参数在不同位置的神经元当中是共享的,因此也应该被一起规范化。

而NLP中不同batch样本的信息关联性不大,而且由于不同的句子长度不同,强行归一化会损失不同样本间的差异信息,所以就没在batch维度进行归一化,而是选择LN,只考虑的句子内部维度的归一化。 可以认为NLP应用场景中一个样本内部维度间是有关联的,所以在信息归一化时,对样本内部差异信息进行一些损失,反而能降低方差。选择什么样的归一化方式,取决于你关注数据的哪部分信息。如果某个维度信息的差异性很重要,需要被拟合,那就别在那个维度进行归一化。

线性转换

  • 对每个字的增强语义向量再做两次线性变换,以增强整个模型的表达能力。这里,变换后的向量与原向量保持长度相同。

【Transformer Encoder的输入和输出在形式上还是完全相同,因此,Transformer Encoder同样可以表示为将输入文本中各个字的语义向量转换为相同长度的增强语义向量】

BERT使用Transformer-encoder来编码输入,encoder中的Self-attention机制在编码一个token的时候同时利用了其上下文的token,其中‘同时利用上下文’即为双向的体现

 激活函数GELU

ReLU和Dropout都会返回一个神经元的输出,其中,ReLU会确定性的将输入乘上一个0或者1,Dropout则是随机乘上0。而GELU也是通过将输入乘上0或1来实现这个功能,但是输入是乘以0还是1,是在同时取决于输入自身分布的情况下随机选择的。换句话说,是0还是1取决于当前的输入有多大的概率大于其余的输入。

 因为erf无解析表达式,因此原论文给出了两种近似表达:

Sigmoid近似:

[公式]

tanh逼近方式:

[公式]

源码中的GELU

在google-research/bert/modeling.py中的GELU,是采用近似的方式计算:

def gelu():
    cdf = 0.5 * (1.0 + tf.tanh((np.sqrt(2 / np.pi) * (x + 0.044715 * tf.pow(x, 3)))))
    return x * cdf

而在pretrained-BERT-pytorch/modeling.py中,已经有了精确的计算方式:

def gelu(x):
    """Implementation of the gelu activation function.
        For information: OpenAI GPT's gelu is slightly different (and gives slightly different results):
        0.5 * x * (1 + torch.tanh(math.sqrt(2 / math.pi) * (x + 0.044715 * torch.pow(x, 3))))
        Also see https://arxiv.org/abs/1606.08415
    """
    return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))

其注释也说明了GPT也是近似的计算方式。

bert损失函数

bert的训练包含两个任务,损失函数也由两个任务的损失函数组成,一部分是来自 Mask-LM 的单词级别分类任务;另一部分是句子级别的分类任务;

优点:通过这两个任务的联合学习,可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。

在第一部分的损失函数中,如果被 mask 的词集合为 M,因为它是一个词典大小 |V| 上的多分类问题,所用的损失函数为负对数似然函数(且是最小化,等价于最大化对数似然函数),

第二部分的损失函数中,在句子预测任务中,也是一个分类问题的损失函数

联合起来

 

transformer

Transformer由论文《Attention is all you need》中提出,其最大的特点是抛弃了传统的RNN和CNN,通过Attention机制将任意位置的两个单词的距离转换成1,有效的解决了NLP中棘手的长期依赖问题。Transformer的结构在NLP领域中已经得到了广泛应用,并且作者已经发布在TensorFlow的tensor2tensor库中。 Transformer是一个encoder-decoder的结构,由若干个编码器,和解码器堆叠形成。 如下图所示,左侧部分为编码器,负责接收文本作为输入,由Multi-Head Attention和一个全连接组成,用于将输入语料转化成特征向量; 右侧部分是解码器,其输入为编码器的输出以及已经预测的结果,由Masked Multi-Head Attention, Multi-Head Attention以及一个全连接组成,用于输出最后结果的条件概率。

transformer的结构

 transformer由6个encoder和6个decoder构成

encoder结构:

input经过embedding后,进行position encoding,multi-head attention,feed forward神经网络,每个子层间有残差链接层

position encoding

位置编码是一个包含每个频率的正弦余弦对,频率沿向量维度逐渐减小

 

 

 好处:能够适应比训练集里面所有句子更长的句子;可以让模型容易地计算出相对位置

 参考:https://zhuanlan.zhihu.com/p/106644634

multi-head attention

self-attention过程

为每个单词创建3个向量:query,key,value(由embedding和3个矩阵相乘得到

Q:查询向量
K:表示被查询信息与其他信息的相关性的向量
V:表示被查询信息的向量

每一个单词都有QKV这三个向量,也有是会去求其他单词和该单词的匹配度,Q表示的就是与我这个单词相匹配的单词的属性,K就表示我这个单词的本身的属性,V表示的是我这个单词的包含的信息本身。

 除以维度的根号:防止内积过大,稳定梯度

为什么是除以维度的根号,而不是维度

解答:Q和K的点积结果会很大,导致进入softmax后梯度会很小,需要通过scaled(缩放)来缓解。

为什么较大的输入会导致softmax梯度很小?

对于一个输入向量 [公式] ,softmax函数将其映射/归一化到一个分布 [公式] 。在这个过程中,softmax先用一个自然底数e 将输入中的元素间差距先“拉大”,然后归一化为一个分布。假设某个输入X中最大的的元素下标是k,如果输入的数量级变大(每个元素都很大),那么[公式]会非常接近1。

用一个小例子来看看X的数量级对输入最大元素对应的预测概率[公式]的影响。假定输入 [公式] ),我们来看不同量级的a产生的[公式]有什么区别。

import numpy as np
def softmax(x):
    # x为一维数据时
    if x.ndim == 1:
        return np.exp(x - np.max(x)) / np.sum(np.exp(x - np.max(x)))
    # x为二维数据时
    elif x.ndim == 2:
        val_num = np.zeros_like(x)
        for i in range(len(x)):
            part_x = x[i]
            val_num[i] = np.exp(part_x - np.max(part_x)) / np.sum(np.exp(part_x - np.max(part_x)))
        return val_num
print(softmax(np.array([1, 1, 2])))
print(softmax(np.array([10, 10, 20])))
print(softmax(np.array([100, 100, 200])))

输出

[0.21194156 0.21194156 0.57611688]
[4.53958078e-05 4.53958078e-05 9.99909208e-01]
[3.72007598e-44 3.72007598e-44 1.00000000e+00]

当a=100时,[公式]达到了接近1(计算机精度限制,展示为1)

因此在数量级较大时,softmax将几乎全部的概率分布都分配给了最大值对应的标签。
在计算softmax梯度时,记

分两种情况:对角单元(i=j)和非对角单元(i≠j)。

若为对角单元(i=j)

 若为非对角单元(i≠j)

 整理可得

 展开为矩阵形式

根据前面的结论,当输入X的元素均较大时,softmax会把大部分概率分布分配给最大的元素,假设我们的输入数量级很大,最大的元素是x1,那么就将产生一个接近one-hot的向量 [公式] ,此时上面的矩阵变为如下形式:

也就是说,在输入的数量级很大时,梯度消失为0,造成参数更新困难。

为什么缩放是维度的根号?

针对为什么维度会影响点积的大小,在论文的脚注中其实给出了一点解释:

 假设向量q和k的各个分量是互相独立的随机变量,均值是0,方差是1,那么点积q*k的均值是0,方差是dk

对 [公式],qi和ki都是随机变量,记X=qi,Y=ki。这样有:D(X)=D(Y)=1, E(X)=E(Y)=0。

有E(XY)=E(X)E(Y)=0, 有D(XY)=E(X2*Y2)-[E(XY)]2=1

这样[公式] , qi*ki的均值是0,方差是1,又由期望和方差的性质, 对相互独立的分量Zi,有    

所以有q*k的均值E(q*k)=0,方差D(q*k)=dk。方差越大也就说明,点积的数量级越大(以越大的概率取大值)。那么一个自然的做法就是把方差稳定到1,做法是将点积除以 [公式] ,这样有:[公式]

将方差控制为1,也就有效地控制了前面提到的梯度消失的问题。

multi-headed 机制

为了进一步提升注意力层的性能,使模型关注不同位置

论文用了8个头,会得到8个不同的矩阵,先concat起来,再与一个权重矩阵(线性变换)相乘

feed forward神经网络

Feed Forward 层是一个两层的全连接层,第一层的激活函数为 Relu,第二层不使用激活函数

残差链接层

Add & Norm 层由 Add 和 Norm 两部分组成

Add指 X+MultiHeadAttention(X),是一种残差连接,通常用于解决多层网络训练的问题,可以让网络只关注当前差异的部分

Norm指 Layer Normalization,通常用于 RNN 结构,Layer Normalization 会将每一层神经元的输入都转成均值方差都一样的,这样可以加快收敛

decoder结构:

 decoder结构整体与encoder结构相似,只是多了一个用于与encoder输出进行交互的多头注意力,

masked multi-head attention将未来的信息mask掉,因为在生成的时候是无法知道未来的信息的

encoder-decoder attention:k和v来自encoder的输出,q来自decoder

decoder部分的输入包括:

初始输入:前一时刻的decoder输入+前一时刻预测结果+position encoding

中间输入:encoder的embeding

transformer的mask

mask 表示掩码,它对某些值进行掩盖,使其在参数更新时不产生效果。Transformer 模型里面涉及两种 mask,分别是 padding mask 和 sequence mask。padding mask 在所有的 scaled dot-product attention 里面都需要用到,而 sequence mask 只有在 decoder 的 self-attention 里面用到。

1. Padding Mask:因为每个批次输入序列长度是不一样的,要对输入序列进行对齐。给在较短的序列后面填充 0。但是如果输入的序列太长,则是截取左边的内容,把多余的直接舍弃。因为这些填充的位置,其实是没什么意义的,所以我们的attention机制不应该把注意力放在这些位置上,所以需要进行一些处理。把这些位置的值加上一个非常大的负数(负无穷),这样的话,经过 softmax,这些位置的概率就会接近0!而 padding mask 实际上是一个张量,每个值都是一个Boolean,值为 false 的地方就是我们要进行处理的地方。

2. Sequence mask:是为了使得 decoder 不能看见未来的信息。也就是对于一个序列,在 time_step 为 t 的时刻,我们的解码输出应该只能依赖于 t 时刻之前的输出,而不能依赖 t 之后的输出。因此需要把 t 之后的信息给隐藏起来。产生一个下三角矩阵。把这个矩阵作用在每一个序列上,就可以达到我们的目的。

对于 decoder 的 self-attention,里面使用到的 scaled dot-product attention,同时需要padding mask 和 sequence mask 作为 attn_mask,具体实现就是两个mask相加作为attn_mask。其他情况,attn_mask 一律等于 padding mask。

transformer的并行化

Encoder支持并行化

Encoder部分能支持并行化训练的关键就在于理解自注意力机制。在计算每一个 [公式] 时,需要的是全体的 [公式] ,而并不依赖 [公式] ,所以Encoder可以并行化计算所有的 [公式] ,而无需像RNN那样严格按照时序依次迭代计算。

Decoder支持并行化

Decoder部分并行化训练,是基于以下两个关键点:

  1. teacher force
  2. masked self attention

对于teacher force,在其他seq2seq模型中也有应用。它是指在每一轮预测时,不使用上一轮预测的输出,而强制使用正确的单词。通过这样的方法可以有效的避免因中间预测错误而对后续序列的预测,从而加快训练速度。而Transformer采用这个方法,为并行化训练提供了可能,因为每个时刻的输入不再依赖上一时刻的输出,而是依赖正确的样本,而正确的样本在训练集中已经全量提供了。但是Decoder的并行化仅在训练阶段,测试阶段因为没有正确的目标语句,t时刻的输入必然依赖t-1时刻的输出。

对于masked self attention,它本质就是一种自注意力,只是加了一个mask,也正是这个mask为Decoder提供了并行化训练的可能。Decoder是在做解码,解码时要求第t时刻的输出,是看不到t时刻以后的样本的。mask就是用一个0,1构成的矩阵与普通的自注意力机制得到的权重做一下合并。mask矩阵是 -inf 和 0,这里是0是保留,-inf是不要。因为注意力机制计算权重时,还要将这个score矩阵交给softmax,-inf经softmax运算后就是0,相当于将神经网络的权重参数置为0。
Transformer实现Decoder部分训练并行化,就是一次性将整个目标句子(假设长度为4)输入给decoder,然后利用masked自注意力算法计算出 [公式] ,往后面网络继续传递,最后计算出4个预测值,分别对应4个时刻的输出。

假如:

第一轮:给自注意力模块输入x1,计算得到了z1

第二轮:给自注意力模块输入x1,x2,计算得到了z1和z2

这个第二轮的z1和第一轮的z1显然是不同的,因为第二轮的z1是综合考虑了x1和x2之后的加权,而第一轮的z1仅考虑了x1。如果按照上述的并行化训练方法,一次性得到了 z1和z2。根据前面的描述,我们可以说:这个z1和第一轮的z1是相同的,这个z2和第二轮的z2也是相同的。但与时序方法相比,第二轮时,我们给的z1是不同的,这样不会影响最后结果吗?

简单起见,我们可以考虑Decoder侧只有一个单元,如上图右侧所示。从下往上看,在self-Attention阶段,我们将输入 [公式] 通过masked计算出了对应的 [公式] ,这一步发生了在目标序列上的局部特征提取,正如我们前面所说,只提取与前置样本的局部特征。下一步送给了Encoder-Decoder Attention,在这里我们是以Encoder侧的输出作为K和V,上一步的输出 [公式] 作为Q,来计算注意力。换句话说,他是提取在原始序列上的局部特征,而并没有在目标序列上提取特征,所以这一步对 [公式] 之间互相不影响。再下来送进Feed Forward,图中画的很清晰,对每一个 [公式] 其实都是一个单独的(也是相同参数的)全连接网络进行映射。这一点在原文的3.3节也有相关描述:“the linear transformations are the same across different positions”。所以这一步也不会对不同 [公式] 之间产生关联。整个过程下来,可以发现,只有在最初一步的masked self-attention上,发生了对目标序列的局部特征提取,而这一步通过mask屏蔽了后面序列对当前时刻的影响。这一步以后, [公式] 之间就是互相独立的。最后每个 [公式] 分别经历Linear和SoftMax得到了对应位置i的输出预测。

对于上面这段冗述,我们可以通过一个实验来证明。还以之前目标语句为:“<start> I love China” 为例,当我们给解码器输入 “<start>”时,解码器应该输出一个预测单词记作 [公式] 。同样的解码器,如果我们给它输入“<start> I”,它应该输出两个预测单词 [公式] 。如果我们上一段的分析成立,两次输出中的 [公式] 必然是相等的。推广到输入完整的目标句子,输出的序列 [公式] 必然和时序处理的每个时刻产生的输出是一一对应的。

=================================================================================

待看

 bert的并行化体现

 

BERT和Transformer Encoder的差异

与Transformer本身的Encoder端相比,BERT的Transformer Encoder端输入的向量表示,多了Segment Embeddings。 加入Segment Embeddings的原因:Bert会处理句对分类、问答等任务,这里会出现句对关系,而两个句子是有先后顺序关系的,如果不考虑,就会出现词袋子之类的问题(如:武松打虎 和 虎打武松 是一个意思了~),因此Bert加入了句子向量。

 

bert的优化器

Adagard 自适应梯度优化

针对不同的参数调整不同的学习率,频繁变化的参数步长越小,稀疏的参数则以较大的步长进行更新

在计算时增加了分母,为梯度平方累计和的平方根

优势:在数据分布稀疏的场景,能更好利用稀疏梯度的信息,比标准的SGD算法更有效地收敛。

缺点:主要缺陷来自分母项的对梯度平方不断累积,随之时间步地增加,分母项越来越大,最终导致学习率收缩到太小无法进行有效更新

RMSProp
通过梯度平方的指数移动平均数(Exponential Moving Average)来调节学习率,γ是遗忘因子(或称为指数衰减率),依据经验,默认设置为0.9

 梯度更新时候,与AdaGrad类似,只是更新的梯度平方的期望(指数移动均值),其中ε=10^-8,避免除数为0。默认学习率α=0.001

优势:能够克服AdaGrad梯度急剧减小的问题,在很多应用中都展示出优秀的学习率自适应能力。尤其在不稳定(Non-Stationary)的目标函数下,比基本的SGD、Momentum、AdaGrad表现更良好。

Adam优化器
综合考虑梯度的一阶矩估计和二阶矩估计来更新梯度
β1 ,β2 系数为指数衰减率,默认为0.9和0.999

 其中,

      

      

 由表达式可以看出,对更新的步长计算,能够从梯度均值及梯度平方两个角度进行自适应地调节,而不是直接由当前梯度决定。

BERT中的优化器AdamW(AdamWeightDecayOptimizer)

在adam和L2正则化的基础上改进

没有直接将L2约束添加到loss函数中去,因为这样会影响Adam中梯度的一阶和二阶矩估计,所以使用了权重衰减的方式来处理,类似于在SGD中直接加上L2正则化,具体地,就是在每次权重更新时,进行一定比例的衰减

 bert区分一词多义:

同一个字在转换为bert的输入之后(id),embedding的向量是一样,但是通过bert中的多层transformer encoder之后,attention关注不同的上下文,就会导致不同句子输入到bert之后,相同字输出的字向量是不同的,这样就解决了一词多义的问题。

BERT变种

XLM(跨语言模型)

1. 多语言分类任务:每个样本包含两种语言的相同文本,使用一种语言的上下文预测另外一种语言的单词

2. bert作为初始化模型用到无监督机器翻译:transformer的encoder和decoder进行随机初始化,MLM初始化和CLM初始化来得到9种不同的结构

MASS(端到端问题)

 1. 在encoder端和decoder端训练语言模型,mask掉连续的k个词,decoder只输入被mask掉的前k-1个词,并预测被mask掉的k个词
 

XLNET(mask问题)

1. 使用自回归语言模型,解决mask的影响,通过对句子中的单词的排列组合,把一部分预测单词后面的词排列到前面来

2. 相对位置编码

3. 长文本友好

ALBERT(预训练模型参数量过大)

1. 多层之间参数共享,默认共享全部参数,只共享attention参数,只共享feed-forward层参数

2. 预训练任务NSP改进--->SOP:sentence order prediction,负例构造方法为同一篇文档中连续的两个句子交换顺序,能够更多的学习句子间的连贯性

RoBERTa

1. 原始bert的adam优化器超参数β1=0.9,β2=0.999,RoBerta模型因为采用了更大的batch,β2改为了0.98。

2. 原始bert的mask机制,每个样本只会进行一次随机的mask,RoBERTa采用了动态mask,在每次输入时动态生成mask,是变化的。

3. 调整batch-size为8k

4. 训练数据集提升为160G

5. 使用BPE(Byte-pair encoding)编码来处理数据,BPE不依赖完整的单词,而是依赖于子词

6. 取消了NSP任务

 

 

 

 

 

posted on 2021-11-07 15:54  bubbleeee  阅读(1732)  评论(0编辑  收藏  举报