深度学习:BERT模型

BERT模型

        BERT出自https://arxiv.org/pdf/1810.04805.pdf的全称是Bidirectional Encoder Representation from Transformers,即双向Transformer的Encoder。作为一个Word2Vec的替代者,其在NLP领域的11个方向大幅刷新了精度,可以说是近年来自残差网络最优突破性的一项技术了。

      模型的主要创新点都在pre-train方法上,即用了Masked LM和Next Sentence Prediction两种方法分别捕捉词语和句子级别的representation。BERT的本质上是通过在海量的语料的基础上运行自监督学习方法为单词学习一个好的特征表示,所谓自监督学习是指在没有人工标注的数据上运行的监督学习(《A Neural Probabilistic Language Model》这篇论文曾说,每一篇文章,天生是训练语料。)。

        在以后特定的NLP任务中,我们可以直接使用BERT的特征表示作为该任务的词嵌入特征。所以BERT提供的是一个供其它任务迁移学习的模型,该模型可以根据任务微调或者固定之后作为特征提取器。

Bert模型的主要特点:

1 BERT的网络架构使用的是《Attention is all you need》中提出的多层Transformer结构,其最大的特点是抛弃了传统的RNN和CNN,通过Attention机制将任意位置的两个单词的距离转换成1,有效的解决了NLP中棘手的长期依赖问题,能更彻底的捕捉语句中的双向关系;

2 使用了Mask Language Model(MLM) [Wilson L Taylor. 1953. cloze procedure: A new tool for measuring readability.] 和 Next Sentence Prediction(NSP) 的多任务训练目标;

3 使用更强大的机器训练更大规模的数据,使BERT的结果达到了全新的高度,并且Google开源了训练好的多语言BERT模型代码,用户可以直接使用BERT作为Word2Vec的转换矩阵并高效的将其应用到自己的任务中。

Bert的模型结构

Bert 主体模型

包括

BertEmbeddings

+ BertEncoder[12 x BertLayer(BertSelfAttention+BertSelfOutput+BertIntermediate+BertOutput)]

+ BertPooler(应该是可选)

BertEmbeddings

(embeddings): BertEmbeddings(
  (word_embeddings): Embedding(21128, 768, padding_idx=0)
  (position_embeddings): Embedding(512, 768)
  (token_type_embeddings): Embedding(2, 768)
  (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
  (dropout): Dropout(p=0.1, inplace=False)
)

BERT的输入的编码向量(d_model=768)是3个嵌入特征的单位和,这三个词嵌入特征是:

  1. WordPiece 嵌入[Google’s neural machine translation system: Bridging the gap between human and machine translation. ]。
  2. 位置嵌入(Position Embedding):位置嵌入是指将单词的位置信息编码成特征向量,向模型中引入单词位置关系。
  3. 分割嵌入(Segment Embedding)。

                BERT的输入特征。特征是token嵌入,位置嵌入和分割嵌入的单位和

语料的选取很关键,要选用document-level的而不是sentence-level的,这样可以具备抽象连续长序列特征的能力。

WordPiece 嵌入

在BERT和RoBERTa的英文版里面,都采用的是WordPiece。WordPiece是指将单词划分成一组有限的公共子词单元,最小的token切分单位并不是单个英文词,而是更细粒度的切分,如 predict 这个词被切分成pre、##di、##ct三个token(##表示非完整单词,而是某个单词的非开头部分),这种切分方式的好处在于能缓解未见词的问题,也更加丰富了词表的表征能力。

WordPiece模型使用贪心法创建了一个固定大小的词汇表,其中包含单个字符、子单词和最适合我们的语言数据的单词。由于词汇量限制大小为30,000,因此,用WordPiece模型生成一个包含所有英语字符的词汇表,再加上该模型所训练的英语语料库中发现的~30,000个最常见的单词和子单词。这个词汇表包含:
1、整个单词
2、出现在单词前面或单独出现的子单词(“em”(如embeddings中的“em”)与“go get em”中的独立字符序列“em”分配相同的向量)
3、不在单词前面的子单词,在前面加上“##”来表示这种情况
4、单个字符
要在此模型下对单词进行记号化,tokenizer首先检查整个单词是否在词汇表中。如果没有,则尝试将单词分解为词汇表中包含的尽可能大的子单词,最后将单词分解为单个字符。注意,由于这个原因,我们总是可以将一个单词表示为至少是它的单个字符的集合。

因此,不是将词汇表中的没有的单词分配给诸如“OOV”或“UNK”之类的全集令牌,而是将词汇表中没有的单词分解为子单词和字符令牌,然后我们可以为它们生成嵌入。因此,我们没有将“embeddings”和词汇表之外的每个单词分配给一个重载的未知词汇表标记,而是将其拆分为子单词标记[’ em ‘、’ ##bed ‘、’ ##ding ‘、’ ##s '],这些标记将保留原单词的一些上下文含义。我们甚至可以平均这些子单词的嵌入向量来为原始单词生成一个近似的向量。

中文全词mask预训练

对于中文来说,并没有WordPiece的切分法,因为中文最小单位就是字。Whole word masking(wwm):虽然token是最小的单位,但在【MASK】的时候是基于分词的。具体来说就是,使用中文分词工具来决定词的边界,如分词后变成"使用 语言 模型 来 预测 下一个 词 的 概率 。",在【MASK】的时候,是对分词后的结构进行【MASK】的(如不能只【MASK】掉"语"这个token,要不就把“语 言”都【MASK】掉),N-gram Masking的意思是对连续n个词进行【MASK】,如图中把“语 言 模 型”都【MASK】了,就是一个2-gram Masking。虽然【MASK】是对分词后的结果进行,但在输入的时候还是单个的token。比如MacBERT采用基于分词的n-gram masking,1-gram~4gram Masking的概率分别是40%、30%、20%、10%。

这样在中文vocab词表既存在普通中文文本"下",同时还存在"##下"。
中文全词mask预训练核心点
1、词粒度tokens;
2、中文全词mask部分;

关于中文字粒度tokens
[‘[CLS]’, ‘进’, ‘入’, ‘10’, ‘月’, ‘份’, ‘,’, ‘日’, ‘本’, ‘新’, ‘增’, ‘确’, ‘诊’, ‘人’, ‘数’, ‘锐’, ‘减’, ‘。’, ‘[SEP]’, ‘着’, ‘大’, ‘量’, ‘[UNK]’, ‘新’, ‘奇’, ‘特’, ‘[UNK]’, ‘展’, ‘品’, ‘的’, ‘第’, ‘四’, ‘届’, ‘进’, ‘博’, ‘会’, ‘消’, ‘费’, ‘品’, ‘展’, ‘区’, ‘,’, ‘讲’, ‘述’, ‘着’, ‘这’, ‘样’, ‘的’, ‘[UNK]’, ‘美’, ‘美’, ‘与’, ‘共’, ‘[UNK]’, ‘故’, ‘事’, ‘。’, ‘[UNK]’, ‘新’, ‘奇’, ‘特’, ‘[UNK]’, ‘全’, ‘球’, ‘新’, ‘品’, ‘来’, ‘这’, ‘里’, ‘首’, ‘发’, ‘米’, ‘技’, ‘国’, ‘际’, ‘控’, ‘股’, ‘有’, ‘限’, ‘公’, ‘司’, ‘董’, ‘事’, ‘长’, ‘季’, ‘残’, ‘月’, ‘的’, ‘进’, ‘博’, ‘会’, ‘行’, ‘程’, ‘满’, ‘满’, ‘当’, ‘当’, ‘,’, ‘她’, ‘将’, ‘与’, ‘四’, ‘五’, ‘十’, ‘个’, ‘来’, ‘自’, ‘全’, ‘国’, ‘各’, ‘地’, ‘的’, ‘上’, ‘下’, ‘游’, ‘合’, ‘作’, ‘伙’, ‘伴’, ‘见’, ‘面’, ‘,’, ‘进’, ‘一’, ‘步’, ‘加’, ‘深’, ‘[SEP]’]

关于中文词粒度tokens
[‘[CLS]’, ‘进’, ‘入’, ‘10’, ‘月’, ‘份’, ‘,’, ‘日’, ‘##本’, ‘新’, ‘##增’, ‘确’, ‘##诊’, ‘人’, ‘##数’, ‘锐’, ‘减’, ‘。’, ‘[SEP]’, ‘着’, ‘大’, ‘量’, ‘[UNK]’, ‘新’, ‘##奇’, ‘##特’, ‘[UNK]’, ‘展’, ‘##品’, ‘的’, ‘第’, ‘四’, ‘届’, ‘进’, ‘##博’, ‘##会’, ‘消’, ‘##费’, ‘##品’, ‘展’, ‘##区’, ‘,’, ‘讲’, ‘述’, ‘着’, ‘这’, ‘样’, ‘的’, ‘[UNK]’, ‘美’, ‘美’, ‘与’, ‘共’, ‘[UNK]’, ‘故’, ‘##事’, ‘。’, ‘[UNK]’, ‘新’, ‘##奇’, ‘##特’, ‘[UNK]’, ‘全’, ‘##球’, ‘新’, ‘##品’, ‘来’, ‘这’, ‘里’, ‘首’, ‘发’, ‘米’, ‘##技’, ‘##国’, ‘##际’, ‘##控’, ‘##股’, ‘##有’, ‘##限’, ‘##公’, ‘##司’, ‘董’, ‘##事’, ‘##长’, ‘季’, ‘##残’, ‘##月’, ‘的’, ‘进’, ‘##博’, ‘##会’, ‘行’, ‘##程’, ‘满’, ‘满’, ‘当’, ‘##当’, ‘,’, ‘她’, ‘将’, ‘与’, ‘四’, ‘五’, ‘十’, ‘个’, ‘来’, ‘自’, ‘全’, ‘##国’, ‘##各’, ‘##地’, ‘的’, ‘上’, ‘##下’, ‘##游’, ‘合’, ‘##作’, ‘伙’, ‘##伴’, ‘见’, ‘面’, ‘,’, ‘进’, ‘一’, ‘步’, ‘加’, ‘深’, ‘[SEP]’]

中文词粒度mask_tokens
[‘[CLS]’, ‘进’, ‘入’, ‘10’, ‘月’, ‘份’, ‘,’, ‘日’, ‘##本’, ‘新’, ‘##增’, ‘确’, ‘##诊’, ‘人’, ‘##数’, ‘锐’, ‘减’, ‘。’, ‘[SEP]’, ‘着’, ‘大’, ‘量’, ‘[UNK]’, ‘新’, ‘##奇’, ‘##特’, ‘[UNK]’, ‘展’, ‘##品’, ‘的’, ‘第’, ‘四’, ‘届’, ‘进’, ‘##博’, ‘##会’, ‘消’, ‘##费’, ‘##品’, ‘展’, ‘##区’, ‘,’, ‘讲’, ‘述’, ‘着’, ‘这’, ‘样’, ‘的’, ‘[UNK]’, ‘美’, ‘美’, ‘与’, ‘共’, ‘[UNK]’, ‘[MASK]’, ‘[MASK]’, ‘。’, ‘[UNK]’, ‘新’, ‘##奇’, ‘##特’, ‘[UNK]’, ‘全’, ‘##球’, ‘新’, ‘##品’, ‘来’, ‘这’, ‘里’, ‘首’, ‘发’, ‘[MASK]’, ‘[MASK]’, ‘##oh’, ‘[MASK]’, ‘##ㄉ’, ‘[MASK]’, ‘##有’, ‘[MASK]’, ‘[MASK]’, ‘##司’, ‘[MASK]’, ‘[MASK]’, ‘[MASK]’, ‘[MASK]’, ‘[MASK]’, ‘[MASK]’, ‘的’, ‘进’, ‘##博’, ‘##会’, ‘行’, ‘##程’, ‘满’, ‘满’, ‘当’, ‘##当’, ‘,’, ‘她’, ‘将’, ‘与’, ‘四’, ‘五’, ‘十’, ‘个’, ‘来’, ‘自’, ‘全’, ‘##国’, ‘##各’, ‘##地’, ‘的’, ‘上’, ‘##下’, ‘##游’, ‘合’, ‘##作’, ‘伙’, ‘##伴’, ‘见’, ‘面’, ‘,’, ‘进’, ‘一’, ‘步’, ‘加’, ‘深’, ‘[SEP]’]

在这里插入图片描述

 [Bert中文词粒度级别[MASK]预训练总结_fillmask_福将~白鹿的博客-CSDN博客]

位置编码

        相比Transformer是通过正弦函数生成的,BERT的位置编码是学习出来的(learned position embedding),是绝对位置的参数式编码,且和相应位置上的词向量进行相加而不是拼接。

在BERT中,Token,Position,Segment Embeddings 都是通过学习来得到的,pytorch代码中它们是这样的:

self.word_embeddings = Embedding(config.vocab_size, config.hidden_size)

self.position_embeddings = Embedding(config.max_position_embeddings, config.hidden_size) 

self.token_type_embeddings = Embedding(config.type_vocab_size, config.hidden_size)

上述BERT pytorch代码来自:https://github.com/xieyufei1993/Bert-Pytorch-Chinese-TextClassification,结构层次非常清晰。

        Transformer的位置编码是一个固定值,因此只能标记位置,但是不能标记这个位置有什么用。BERT的位置编码是可学习的Embedding,因此不仅可以标记位置,还可以学习到这个位置有什么用。要这么做的一个原因可能是,相比于Transformer,BERT训练所用的数据量充足,完全可以让模型自己学习。

        BERT的位置编码维度为[seq_length, width]。从实现上可以看到,BERT中将位置编码创建为一个tensorflow变量,并将其broadcast到与词嵌入编码同维度后相加。

with tf.control_dependencies([assert_op]):
      full_position_embeddings = tf.get_variable(
          name=position_embedding_name,
          shape=[max_position_embeddings, width],
          initializer=create_initializer(initializer_range))

      # 这里position embedding是可学习的参数,[max_position_embeddings, width]
      # 但是通常实际输入序列没有达到max_position_embeddings
      # 所以为了提高训练速度,使用tf.slice取出句子长度的embedding
      position_embeddings = tf.slice(full_position_embeddings, [0, 0],
                                     [seq_length, -1])
      num_dims = len(output.shape.as_list())

      # word embedding之后的tensor是[batch_size, seq_length, width]
      # 因为位置编码是与输入内容无关,它的shape总是[seq_length, width]
      # 我们无法把位置Embedding加到word embedding上
      # 因此我们需要扩展位置编码为[1, seq_length, width]
      # 然后就能通过broadcasting加上去了。
      position_broadcast_shape = []
      for _ in range(num_dims - 2):
        position_broadcast_shape.append(1)
      position_broadcast_shape.extend([seq_length, width])
      position_embeddings = tf.reshape(position_embeddings,
                                       position_broadcast_shape)
      output += position_embeddings

缺点:BERT模型最多只能处理512个token的文本,其原因在于BERT使用了随机初始化训练出来的绝对位置编码,最大位置设为为512,若是文本长于512便无位置编码可用。

[『Transformer/BERT』Transformer和BERT的位置编码 - 知乎]

特殊符号

图中的两个特殊符号[CLS][SEP],其中[CLS]表示该特征用于分类模型,对非分类模型,该符合可以省去。[SEP]表示分句符号,用于断开输入语料中的两个句子。

分割嵌入[SEP]:

用于区分两个句子,例如B是否是A的下文(对话场景,问答场景等)。对于句子对,第一个句子的特征值是0,第二个句子的特征值是1。(lz:/segment_ids/token_type_ids。如果不使用,可以全默认为0)。

第一句前[CLS]标志:

BERT在第一句前会加一个[CLS]标志,最后一层该位对应向量可以作为整句话的语义表示(即句子embedding),从而用于下游的分类任务等。与文本中已有的其它词相比,这个无明显语义信息的符号会更“公平”地融合文本中各个词的语义信息,从而更好的表示整句话的语义。当然,也可以通过对最后一层所有词的embedding做pooling去表征句子语义。

句子embedding:

说法1:不同方法效果:总体上来看,CNN-BERT > SBERT > Avg pooling > CLS
可参考《Evaluation of BERT and ALBERT Sentence Embedding Performance on Downstream NLP Tasks》,这篇文章在 BERT/ALBERT 提取信息作为句子信息表示应用在下游任务,对其效果进行了评测。[BERT中CLS效果真的好嘛?]

说法2:倒数第二层是汉安定下来的一个合理的靶点(sweet spot)。

[BERT单词嵌入教程_bert词嵌入_居里付任的博客-CSDN博客]

lz建议使用最后一层emb的均值吧,或者某种加权的均值。

另:bert的输出,有两种,在BERT TF源码中对应:
一种是get_pooled_out(),就是上述[CLS]的表示,输出shape是[batch size,hidden size]。
一种是get_sequence_out(),获取的是整个句子每一个token的向量表示,输出shape是[batch_size, seq_length, hidden_size],这里也包括[CLS],因此在做token级别的任务时要注意它。

[为什么BERT在第一句前会加一个[CLS]标志?]

BertEncoder

(encoder): BertEncoder(
  (layer): ModuleList(
    (0-11): 12 x BertLayer(
      (attention): BertAttention(
        (self): BertSelfAttention(
          (query): Linear(in_features=768, out_features=768, bias=True)
          (key): Linear(in_features=768, out_features=768, bias=True)
          (value): Linear(in_features=768, out_features=768, bias=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
        (output): BertSelfOutput(
          (dense): Linear(in_features=768, out_features=768, bias=True)
          (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
          (dropout): Dropout(p=0.1, inplace=False)
        )
      )
      (intermediate): BertIntermediate(
        (dense): Linear(in_features=768, out_features=3072, bias=True)
        (intermediate_act_fn): GELUActivation()
      )
      (output): BertOutput(
        (dense): Linear(in_features=3072, out_features=768, bias=True)
        (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
        (dropout): Dropout(p=0.1, inplace=False)
      )
    )
  )
)

共12层BertLayer,其中“BertLayer”就是Transformer的编码器结构深度学习:transformer模型

BertPooler

BertPooler模块接在bert的堆叠encoder层后,本质上就是一个全连接层网络,主要用于解决序列级的NLP任务(比如nsp任务或者序列分类任务)。
BertPooler模块只接收BertEncoder模块中第一个token([CLS])对应的输出(因为序列级任务只会用到[CLS]位置对应的输出),过全连接层后作为最终输出pooler_output,shape (batch-size, hidden-size)。

(pooler): BertPooler(
  (dense): Linear(in_features=768, out_features=768, bias=True)
  (activation): Tanh()
)

[从源码解析 Bert 的 BertPooler 模块_energy_百分百的博客-CSDN博客]

Note: 对于预训练只有加入NSP任务时,或者对于finetune分类等任务时,才会训练BertPooler层。只有mlm的要小心看下。

这里就有个问题,如果语言模型重新只finetune mlm了,nsp(或者BertPooler层)没变,那后面接下游分类时,BertPooler层还要不要(毕竟已经过时了)?感觉是重新初始化比较好。

Bert的训练

        BERT是一个多任务模型,它的预训练任务是由两个自监督任务组成,即MLM和NSP。

Task #1: Masked Language Model/完形填空(Cloze task)

        Masked Language Model(MLM)的核心思想取自Wilson Taylor在1953年发表的一篇论文[cloze procedure: A new tool for measuring readability. ]。所谓MLM是指在训练的时候随即从输入预料上mask掉一些单词,然后通过的上下文预测该单词,该任务非常像完形填空。正如传统的语言模型算法和RNN匹配那样,MLM的这个性质和Transformer的结构是非常匹配的。

        在BERT的实验中,15%的WordPiece Token会被随机Mask掉(而不是把像cbow一样把每个词都预测一遍),假如有 1 万篇文章(或句子),每篇文章(或句子)平均有 100 个词汇,随机遮盖 15% 的词汇,模型的任务是正确地预测这 15 万个被遮盖的词汇。

        全部mask的小问题:导致pre-train与fine-tuning之间存在差异。假设我们使用了上述方式去对BERT进行训练,而后再应用到迁移学习fine-tuning中。在预训练的时候,语料库里是有[MASK] 这个标志的,但是在实际的迁移学习中,输入的文本是不包含[MASK]标志的。如果句子中的某个Token100%都会被mask掉,那么在fine-tuning的时候模型就会有一些没有见过的单词。

        为了解决这个问题,研究人员制定了80%,10%,10%的规则。即在随机选择了15%的单词被遮挡后,对于这15%的单词,做如下处理:
    80%的概率将被遮挡的单词替换为[MASK]
    10%的概率将被遮挡的单词替换为1个随机单词
    10%的概率不做任何改变
这样做的好处是:
    解决了与fine-tuning的gap问题。80%的遮挡率既应用了[MASK],也有20%的概率不使用[MASK]
    存在10%的概率不做任何改变,保留了原有语义,让模型可以有机会了解原始数据样貌
    存在10%的概率替换随机单词,可以使得模型不仅仅依赖于看到过的原始数据,而是还让数据依赖于句子的上下文来预测目标词,达到“纠错”的目的。加入随机Token的还有因为Transformer要保持对每个输入token的分布式表征,否则模型就会记住这个[mask]是token ’hairy‘,随机词替换会给模型增加一点点噪声,但是因为此时模型不知道哪个词是被随机换了(不像[MASK],给模型[MASK]则模型知道此处词的是被挖了,他需要预测这个位置是啥),所以就迫使他去更好地保留每个词的词义,为下游任务提供方便。至于单词带来的负面影响,因为一个单词被随机替换掉的概率只有15%*10% =1.5%,这个负面影响其实是可以忽略不计的。

另外文章指出每次只预测15%的单词,因此模型收敛的比较慢。

Note:

1 为什么BERT选择mask掉15%这个比例的词? 从CBOW的角度,这里 p=15% 有一个比较好的解释是:在一个大小为 1/p~=7 的窗口中随机选一个词,类似CBOW中滑动窗口的中心词,区别是这里的滑动窗口是非重叠的。那从CBOW的滑动窗口角度,10%~20%都是还ok的比例。

细节

Input:
the man [MASK1] to [MASK2] store
Label:
[MASK1] = went; [MASK2] = a

In particular, we feed the input through a deep Transformer encoder and then use the final hidden states corresponding to the masked positions to predict what word was masked, exactly like we would train a language model.通过一个深层转换Transformer编码器进行输入,随后使用对应于遮盖处的最终隐态对遮盖词进行预测,正如我们训练一个语言模型一样。

为什么要使用MaskLM的方式来训练语言模型?

        传统的语言模型是单向的(数学上已经定义了),而且往往都很浅(想象一下LSTM堆三层就train不动了,就要上各种trick了),比如ELMo。与之前使用的单向语言模型进行预训练不同,BERT使用遮蔽语言模型来实现预训练的深度双向表示。

举个例子:

> 今天 天气 不错, 我们 去 公园 玩 吧。

        这句话,单向语言模型在学习的时候是从左向右进行学习的,先给模型看到“今天 天气”两个词,然后告诉模型下一个要填的词是“不错”。然而单向语言模型有一个欠缺,就是模型学习的时候总是按照句子的一个方向去学的,因此模型学习每个词的时候只看到了上文,并没有看到下文。更加合理的方式应该是让模型同时通过上下文去学习,这个过程有点类似于完形填空题。

例如:

>今天 天气 { }, 我们 去 公园 玩 吧。

        通过这样的学习,模型能够更好地把握“不错”这个词所出现的上下文语境。

it would be much better if we could train a single model that was deeply bidirectional.It's unfortunately impossible to train a deep bidirectional model like a normal LM, because that would create cycles where words can indirectly "see themselves," and the predictions become trivial.   [Reddit上Bert作者Jacob的介绍和解释(原帖链接)]

        Bert在随机mask词汇的时候,为什么非要把mask掉的词用[mask]替代,然后将这个字符给与一个embedding。而不是像transformer一样在attention过程中对于后面的词不可见。

因为“[MASK]起到一个占位符,告诉模型这里有一个term,不要忽略掉了”。

[bert的mask为何不学习transformer在attention处进行屏蔽score的技巧? - 知乎]

Task #2: Next Sentence Prediction

        Intuition: The other thing that's missing from an LM is that it doesn't understand relationships between sentences, which is important for many NLP tasks.(句子级别的任务对于阅读理解,推理等任务提升较大。) To pre-train a sentence relationship model, we use a very simple binary classification task, which is to concatenate two sentences A and B and predict whether B actually comes after A in the original text.

        Next Sentence Prediction(NSP)的任务是判断句子B是否是句子A的下文。如果是的话输出’IsNext‘,否则输出’NotNext‘。训练数据的生成方式是从平行语料中随机抽取的连续两句话,其中50%保留抽取的两句话,它们符合IsNext关系,另外50%的第二句话是随机从预料中提取的,它们的关系是NotNext的。这个关系保存在[CLS]符号中。

        对比:word2vec的一个精髓是引入了一个优雅的负采样任务来学习词向量(word-level representation),BERT使用句子级负采样任务学到句子表示。同时在句子表示上,BERT这里并没有像下游监督任务中的普遍做法一样,在encoding的基础上再搞个全局池化之类的,它首先在每个sequence(对于句子对任务来说是两个拼起来的句子,对于其他任务来说是一个句子)前面加了一个特殊的token[CLS]。然后让encoder对[CLS]进行深度encoding,深度encoding的最高隐层即为整个句子/句对的表示。这个做法乍一看有点费解,不过Transformer是可以无视空间和距离的把全局信息encoding进每个位置的,而[CLS]作为句子/句对的表示是直接跟分类器的输出层连接的,因此其作为梯度反传路径上的“关卡”,当然会想办法学习到分类相关的上层特征啦。为了让模型能够区分里面的每个词是属于“左句子”还是“右句子”,作者这里引入了“segment embedding”的概念来区分句子。

细节

Input:
the man went to the store [SEP] he bought a gallon of milk
Label:
IsNext

Input:
the man went to the store [SEP] penguins are flightless birds
Label:
NotNext

关于NSP任务的必要性

实验:

  1. 真实句子对加之 NSP 炼制
  2. 成对句子段加之 NSP 炼制,乃原始 BERT 所为
  3. 连续长句拼接无 NSP 炼制,句子可跨文档
  4. 同上,句子不可跨文档

却说性能对比,简而言之 1<2<3<4,可观得

  • 真实句子过短,不如拼接成句子段
  • 无 NSP 任务,略好过有 NSP
  • 不跨文档好过跨文档

后两者想来亦与 SpanBERT 中提之噪音问题有关。[RoBERTa 详解]

Bert的损失函数loss

BERT的损失函数由两部分组成,第一部分是来自 Mask-LM 的单词级别分类任务,另一部分是句子级别的分类任务。通过这两个任务的联合学习,可以使得 BERT 学习到的表征既有 token 级别信息,同时也包含了句子级别的语义信息。

在第一部分的损失函数中,如果被 mask 的词集合为 M(即计算BERT的MLM loss时会忽略没被mask的token),因为它是一个词典大小 |V| 上的多分类问题,那么具体说来有:

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

因此,两个任务联合学习的损失函数是:

[NLP 的巨人肩膀(下):从 CoVe 到 BERT]

mlm和nsp任务的代码解析

MLM模型 BertOnlyMLMHead

包含两部分,一部分是 bert 模型主体,另一部分则是用于 mask 训练。即包括BertEmbeddings和BertEncoder(但是不包括BertPooler),再加上BertOnlyMLMHead即MLM Head(注意没有nsp的)。

BertOnlyMLMHead其实就是一个二层神经网络,一层是BertPredictionHeadTransform(包括linear+geluAct+ln),一层是decoder(hidden_size*vocab_size大小的linear)。

Huggingface Transformer中实现的结构:
(cls): BertOnlyMLMHead(
  (predictions): BertLMPredictionHead(
    (transform): BertPredictionHeadTransform(
      (dense): Linear(in_features=768, out_features=768, bias=True)
      (transform_act_fn): GELUActivation()
      (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
    )
    (decoder): Linear(in_features=768, out_features=21128, bias=True)
  )
)

注意MLM任务的输入使用的是last_hidden_state。具体实现参考[transformers/blob/main/src/transformers/models/bert/modeling_bert.py#L704]。 

NSP模型 BertOnlyNSPHead

这部分将 BertPooler层的 输出 pooled_output(batch-size, hidden-size) 经过一个全连接层,得到概率,再利用 next_sentence_label 来计算损失。

注意NSP任务训练时,并不是直接使用[CLS]token的embedding作为句子特征传给后续分类头的,而是使用的是pooler_output,即实际是二层神经网络作为nsp的分类。可能是因为直接使用[CLS]的embedding效果不够好?具体实现参考[transformers/blob/main/src/transformers/models/bert/modeling_bert.py#L714]

BertPreTrainingHeads

如果mlm和nsp都考虑,全部加进来就ok。

结构上就是上面上Bert主模型,再加上BertPreTrainingHeads
(cls): BertPreTrainingHeads(
(predictions): BertLMPredictionHead(
  (transform): BertPredictionHeadTransform(
    (dense): Linear(in_features=768, out_features=768, bias=True)
    (transform_act_fn): GELUActivation()
    (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
  )
  (decoder): Linear(in_features=768, out_features=21128, bias=True)
)
(seq_relationship): Linear(in_features=768, out_features=2, bias=True)
)

代码解析:

class BertPreTrainingHeads(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.predictions = BertLMPredictionHead(config)
        self.seq_relationship = nn.Linear(config.hidden_size, 2)

    def forward(self, sequence_output, pooled_output):
        prediction_scores = self.predictions(sequence_output)
        seq_relationship_score = self.seq_relationship(pooled_output)
        return prediction_scores, seq_relationship_score

[具体实现参考transformers/blob/main/src/transformers/models/bert/modeling_bert.py#L724]

预训练过程

上面两个任务合并起来的样本就是:

Input = [CLS] the man went to [MASK] store [SEP]

he bought a gallon [MASK] milk [SEP]

Label = IsNext

Input = [CLS] the man [MASK] to the store [SEP]

penguin [MASK] are flight ##less birds [SEP]

Label = NotNext

假设采集了以下2条句子:
Beijing is a beautiful city
I love Beijing

对这2条句子先做分词:
Tokens = [ [CLS], Beijing, is, a, beautiful, city, [SEP], I, love, Beijing, [SEP] ]

然后,以15%的概率遮挡单词,并遵循80%-10%-10%的规则。假设遮挡的单词为city,则:
Tokens = [ [CLS], Beijing, is, a, beautiful, [MASK], [SEP], I, love, Beijing, [SEP] ]

接下来将Tokens送入到BERT中,并训练BERT预测被遮挡的单词,同时也要预测这2条句子是否为相邻(句子2是句子1的下一条句子)。

[https://www.cnblogs.com/zackstang/p/15358061.html#_label5]

训练细节

BERT 还利用了一系列策略,使得模型更易于训练。比如

1 优化器和学习率: Adam with learning rate of 1e-4, β1 = 0.9, β2 = 0.999,adam_epsilon=1e-08。 L2 weight decay of 0.01。 

warm-up 策略warmup over the first 10,000 steps, and linear decay of the learning rate.倾斜的三角学习率(STLR),它首先线性地增加学习速率,然后根据时间表线性地衰减。

2 使用的激活函数不再是普通的 ReLu,而是 高斯误差线性单元GeLu。(following OpenAI GPT)

[gelu激活函数]

3  dropout =0.1等常见的训练技巧。

4 batch_size = 256 (256 sequences * 512 tokens = 128,000 tokens/batch);

1,000,000 steps; 40 epochs over the 3.3 billion word corpus;数据量:Books Corpus + English Wikipedia (16GB)。

5 static masking: BERT采用的是static mask的方式,在create pretraining data中,先对数据进行提前的mask,为了充分利用数据,定义了dupe_factor,这样可以将训练数据复制dupe_factor份,然后同一条数据可以有不同的mask。注意这些数据不是全部都喂给同一个epoch,是不同的epoch,例如dupe_factor=10epoch=40, 则每种mask的方式在训练中会被使用4次。 The original BERT implementation performed masking once during data preprocessing, resulting in a single static mask. To avoid using the same mask for each training instance in every epoch, training data was duplicated 10 times so that each sequence is masked in 10 different ways over the 40 epochs of training. Thus, each training sequence was seen with the same mask four times during training.[RoBERTa 详解]

when we go from a big model (12 Transformer blocks, 768-hidden, 110M parameters) to a really big model (24 Transformer blocks, 1024-hidden, 340M parameters), we get huge improvements even on very small datasets (small == less than 5,000 labeled examples).

-柚子皮-

Bert模型的评价

优点

        用的是Transformer,也就是相对rnn更加高效、能捕捉更长距离的依赖,它捕捉到的是真正意义上的bidirectional context信息。

        模型有两个 loss,一个是 Masked Language Model,另一个是 Next Sentence Prediction。前者用于建模更广泛的上下文,通过 mask 来强制模型给每个词记住更多的上下文信息;后者用来建模多个句子之间的关系,强迫 [CLS] token 的顶层状态编码更多的篇章信息。

缺点

        BERT算法还有很大的优化空间,例如我们在Transformer中讲的如何让模型有捕捉Token序列关系的能力,而不是简单依靠位置嵌入。

        每个batch只有15%的token被预测,所以BERT收敛得比left-to-right模型要慢。

        [MASK]标记在实际预测中不会出现,训练时用过多[MASK]影响模型表现。

        BERT的体量以及训练的数据量,训练在目前的计算资源下很难完成。BERT训练数据采用了英文的开源语料BooksCropus 以及英文维基百科数据,一共有33亿个词。同时BERT模型的标准版本有1亿的参数量,与GPT持平,而BERT的大号版本有3亿多参数量,这应该是目前自然语言处理中最大的预训练模型了。当然,这么大的模型和这么多的数据,训练的代价也是不菲的。谷歌用了16个自己的TPU集群(一共64块TPU,一块TPU的速度约是目前主流GPU的7-8倍)来训练大号版本的BERT,一共花了4天的时间。对于是否可以复现预训练,作者在Reddit上有一个大致的回复,指出OpenAI当时训练GPT用了将近1个月的时间,而如果用同等的硬件条件来训练BERT估计需要1年的时间。不过他们会将已经训练好的模型和代码开源,方便大家训练好的模型上进行后续任务。

        “bert模型是针对长文本进行训练的,对短文本效果一般。需要找合适的预训练模型。“

        BERT虽然对上下文有很强的编码能力,却缺乏细粒度语义的表示:

把token编码降维后,可以看到sky和sea明明是天与海的区别,却因为上下文一样而得到了极为相似的编码。

模型对比

        从Word2vec到AllenNLP ELMo,从OpenAI GPT到BERT的不同。

      Word2vec本身是一种浅层结构,而且其训练的词向量所“学习”到的语义信息受制于窗口大小;

      因此后续有学者提出利用可以获取长距离依赖的LSTM语言模型预训练词向量,而此种语言模型也有自身的缺陷,因为此种模型是根据句子的上文信息来预测下文的,或者根据下文来预测上文,传统的LSTM模型只学习到了单向的信息。

      ELMO的出现在一定程度上解决了这个问题,ELMO是一种双层双向的LSTM结构,其训练的语言模型可以学习到句子左右两边的上下文信息,但此处所谓的上下文信息并不是真正意义上的上下文。ELMO的设置其实是最符合直觉的预训练套路,两个方向的语言模型刚好可以用来预训练一个BiLSTM,非常容易理解。但是受限于LSTM的能力,无法变深了。

      OpenAI 的GPT是利用了transform的编码器作为语言模型进行预训练的,之后特定的自然语言处理任务在其基础上进行微调即可,和LSTM相比,此种语言模型的优点是可以获得句子上下文更远距离的语言信息,但也是单向的。

      BERT的出现,似乎融合了它们所有的优点,并摒弃了它们的缺点,因此才可以在诸多后续特定任务上取得最优的效果。BERT对比AI2的 ELMo和OpenAI的fine-tune transformer的优点是只有BERT表征会基于所有层中的左右两侧语境,BERT能做到这一点得益于Transformer中Attention机制将任意位置的两个单词的距离转换成了1。区别是:1 它在训练双向语言模型时以减小的概率把少量的词替成了Mask或者另一个随机的词。我个人感觉这个目的在于使模型被迫增加对上下文的记忆。至于这个概率,可能是Jacob拍脑袋随便设的。2 增加了一个预测下一句的loss。3 对比ELMo,虽然都是“双向”,但目标函数其实是不同的。ELMo是分别以 和  作为目标函数,独立训练处两个representation然后拼接,是一种双向预测 bi-directional;而BERT则是以  作为目标函数训练LM,称之为 deep bi-directional。

BERT用了两个反直觉的手段来找到了一个“更好”的方式。(1) 用比语言模型更简单的任务来做预训练。直觉上要做更深的模型,需要设置一个比语言模型更难的任务,而BERT则选择了两个看起来更简单的任务:完形填空和句对预测。(2) 完形填空任务在直观上很难作为其它任务的预训练任务。

Bert参数量分析

BERT提供了简单和复杂两个模型,对应的超参数分别如下:

BERT-Base, Uncased: 12-layer, 768-hidden, 12-heads, 110M parameters

BERT-Large, Uncased: 24-layer, 1024-hidden, 16-heads, 340M parameters

在上面的超参数中,L表示网络的层数(即Transformer blocks的数量Nx),H表示隐层大小,A表示Multi-Head Attention中self-Attention的数量/heads数量,feed-forward/filter 的尺寸是4H。

加载完模型后,输入model,或者print(model)就可以查看模型的结构。

Base Bert的encoder用了12层,参数大小为108808704.0 ≈ 110M。

主要参数量分布:
word_embeddings 21128*768=1623w
BertSelfAttention 768*768*3*12=2123w
BertSelfOutput 768*768*12=707w
BertIntermediate+BertOutput 768*(768*4)*2*12=5662w

Note: feed-forward networks是BERT将self-attention的结果映射到更高维的语义空间,占整个BERT中参数量的55%,占有极高的权重。Albert中将这个参数共享后,指标也是下降的最厉害。

[BERT模型到底有多少参数]

[Bert/Transformer模型的参数大小计算]

Bert代码:实现code

谷歌24种训练好的小模型

[google-research/bert: TensorFlow code and pre-trained models for BERT]

[pytorch-pretrained-BERT: 📖The Big-&-Extending-Repository-of-Transformers: Pretrained PyTorch models for Google's BERT, OpenAI GPT & GPT-2, Google/CMU Transformer-XL.]

更好的是,使用huggingface的transformer库加载使用

[bert-base-uncased · Hugging Face 实际是varient版本的bert]

Bert模型的fine-turning微调

        在海量单预料上训练完BERT之后,便可以将其应用到NLP的各个任务中了。

        对于NSP任务来说,其条件概率表示为  ,其中 C 是BERT输出中的[CLS]符号, W 是可学习的权值矩阵。

        对于其它任务来说,我们也可以根据BERT的输出信息作出对应的预测。下图展示了BERT在11个不同任务中的模型,它们只需要在BERT的基础上再添加一个输出层便可以完成对特定任务的微调。

Tok表示不同的Token, E 表示嵌入向量, T_i 表示第 i 个Token在经过BERT处理之后得到的特征向量。

                                                              BERT用于模型微调

(a)基于句子对的分类任务

  • MNLI:给定一个前提 (Premise) ,根据这个前提去推断假设 (Hypothesis) 与前提的关系。该任务的关系分为三种,蕴含关系 (Entailment)、矛盾关系 (Contradiction) 以及中立关系 (Neutral)。所以这个问题本质上是一个分类问题,我们需要做的是去发掘前提和假设这两个句子对之间的交互信息。
  • QQP:基于Quora,判断 Quora 上的两个问题句是否表示的是一样的意思。
  • QNLI:用于判断文本是否包含问题的答案,类似于我们做阅读理解定位问题所在的段落。
  • STS-B:预测两个句子的相似性,包括5个级别。
  • MRPC:也是判断两个句子是否是等价的。
  • RTE:类似于MNLI,但是只是对蕴含关系的二分类判断,而且数据集更小。
  • SWAG:从四个句子中选择为可能为前句下文的那个。

(b)基于单个句子的分类任务

分类模型结构和bert下游任务处理一样,就是在[Bert 主体模型]基础上加上了 (dropout): Dropout 和 (classifier): Linear。这里输入是bertpooler层的输出(对比直接cls的输出要好)。

  (dropout): Dropout(p=0.1, inplace=False)
  (classifier): Linear(in_features=768, out_features=2, bias=True)

  • SST-2:电影评价的情感分析。
  • CoLA:句子语义判断,是否是可接受的(Acceptable)。

        对于GLUE数据集的分类任务(MNLI,QQP,QNLI,SST-B,MRPC,RTE,SST-2,CoLA),BERT的微调方法是根据[CLS]标志生成一组特征向量 C ,并通过一层全连接进行微调。损失函数根据任务类型自行设计,例如多分类的softmax或者二分类的sigmoid。

SWAG的微调方法与GLUE数据集类似,只不过其输出是四个可能选项的softmax:

(c)问答任务

  • SQuAD v1.1:给定一个句子(通常是一个问题)和一段描述文本,输出这个问题的答案,类似于做阅读理解的简答题。如图(c)表示的,SQuAD的输入是问题和描述文本的句子对。输出是特征向量,通过在描述文本上接一层激活函数为softmax的全连接来获得输出文本的条件概率,全连接的输出节点个数是语料中Token的个数。

(d)命名实体识别

  • CoNLL-2003 NER:判断一个句子中的单词是不是Person,Organization,Location,Miscellaneous或者other(无命名实体)。微调CoNLL-2003 NER时将整个句子作为输入,在每个时间片输出一个概率,并通过softmax得到这个Token的实体类别。

可以调整的参数和取值范围有:

  • Batch size: 16, 32
  • Learning rate (Adam): 5e-5, 3e-5, 2e-5
  • Number of epochs: 3, 4

因为大部分参数都和预训练时一样,精调会快一些。

from: 皮皮blog_-柚子皮-_CSDN博客-机器学习MachineLearning,Linux,Python领域博主

ref: [如何评价 BERT 模型?]*

[BERT详解]*

[【NLP】Google BERT详解]

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

[自然语言处理中的语言模型预训练方法]

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值