BERT

views 630 words

1. 简介

BERT的全称是Bidirectional Encoder Representation from Transformers, 即双向Transformer的Encoder, 因为decoder是不能获要预测的信息的. 模型的主要创新点都在pre-train方法上, 即用了 Masked Language Model(MLM) 和 Next Sentence Prediction(NSP) 两种方法分别捕捉词语和句子级别的representation.

BERT模型的结构如下图最左: 注: Trm指的是transformer block. 这里用的是没有encoder的Transformer.

The decoder is a good choice because it’s a natural choice for language modeling (predicting the next word) since it’s built to mask future tokens – a valuable feature when it’s generating a translation word by word

对比OpenAI GPT (Generative Pre-Training Transformer), 两者均是采用的transformer的结构. BERT是双向的Transformer block连接, 而GPT是单向的.

对比ELMo, 虽然都是’双向’, 但目标函数其实是不同的. ELMo是分别以 $P(w_i|w_1,...,w_{i-1})$$P(w_i|w_{i+1},...,w_n)$ 作为目标函数, 独立训练处两个representation然后拼接, 而BERT则是以 $P(w_i|w_1,...,w_{i-1},w_{i+1},...,w_n)$ 作为目标函数训练LM.

BERT对比这两个算法的优点是只有BERT表征会基于所有层中的左右两侧语境. BERT能做到这一点得益于Transformer中Attention机制将任意位置的两个单词的距离转换成了1.

其次BERT在多方面的NLP任务变现来看效果都较好, 具备较强的泛化能力, 对于特定的任务只需要添加一个输出层来进行fine-tuning即可.

2. 结构

先看下BERT的内部结构, 官网提供了两个版本, L表示的是transformer的层数, H表示输出的维度, A表示mutil-head attention的个数

  • BERT_BASE: L=12, H=768, A=12, TotalParameters=110M
  • BERT_LARGE: L=24, H=1024, A=16, TotalParameters=340M

从模型的层数来说其实已经很大了, 但是由于transformer的residual模块, 层数并不会引起梯度消失等问题, 但是并不代表层数越多效果越好, 有论点认为低层偏向于语法特征学习, 高层偏向于语义特征学习.

3. 输入

BERT的输入可以是单一的一个句子或者是句子对, 实际的输入值是segment embedding与position embedding相加 BERT的输入的编码向量(长度是512)是3个嵌入特征的单位和, 如上图, 这三个词嵌入特征是:

  1. WordPiece 嵌入: WordPiece是指将单词划分成一组有限的公共子词单元, 能在单词的有效性和字符的灵活性之间取得一个折中的平衡. 例如上图的示例中‘playing’被拆分成了‘play’和‘ing’
  2. 位置嵌入(Position Embedding): 位置嵌入是指将单词的位置信息编码成特征向量, 位置嵌入是向模型中引入单词位置关系的至关重要的一环.
  3. 分割嵌入(Segment Embedding): 用于区分两个句子, 例如B是否是A的下文(对话场景, 问答场景等). 对于句子对, 第一个句子的特征值是0, 第二个句子的特征值是1.

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

4. 预训练模型

什么是预训练模型? 举个例子, 假设有大量的维基百科数据, 那么可以用这部分巨大的数据来训练一个泛化能力很强的模型, 当需要在特定场景使用时, 例如做文本相似度计算, 那么, 只需要简单的修改一些输出层, 再用自己的数据进行一个增量训练, 对权重进行一个轻微的调整.

预训练的好处在于在特定场景使用时不需要用大量的语料来进行训练, 节约时间效率高效, BERT就是这样的一个泛化能力较强的预训练模型.

5. BERT的预训练过程

BERT的预训练阶段包括两个任务, 一个是Masked Language Model, 还有一个是Next Sentence Prediction.

Masked Language Model

MML可以理解为完形填空, 随机mask每一个句子中15%的词(WordPiece Token), 用其上下文来做预测, 例如:my dog is hairy → my dog is [MASK]

此处将hairy进行了mask处理, 然后采用非监督学习的方法预测mask位置的词是什么. 但是该方法有一个问题, 因为是mask 15%的词, 其数量已经很高了, 这样就会导致某些词在fine-tuning阶段从未见过, 为了解决这个问题, 要做如下的处理:

  • 80%的时间是采用[mask], my dog is hairy → my dog is [MASK]
  • 10%的时间是随机取一个词来代替mask的词, my dog is hairy -> my dog is apple
  • 10%的时间保持不变, my dog is hairy -> my dog is hairy

这么做的原因是如果句子中的某个Token 100%都会被mask掉, 那么在fine-tuning的时候模型就会有一些没有见过的单词. 加入随机Token的原因是因为Transformer要保持对每个输入token的分布式表征, 否则模型就会记住这个[mask]是token ‘hairy’. 至于单词带来的负面影响, 因为一个单词被随机替换掉的概率只有15%*10% =1.5%, 这个负面影响其实是可以忽略不计的.

语言模型会根据前面单词来预测下一个单词,但是self-attention的注意力只会放在自己身上,那么这样100%预测到自己,毫无意义,所以用Mask,把需要预测的词给挡住

Two-sentence Tasks

Next Sentence Prediction

选择一些句子对A与B, 其中50%的数据B是A的下一条句子, 剩余50%的数据B是语料库中随机选择的, 学习其中的相关性, 添加这样的预训练的目的是目前很多NLP的任务比如QA和NLI都需要理解两个句子之间的关系, 从而能让预训练的模型更好的适应这样的任务.

如果句子B是句子A的下文, 输出’IsNext’, 否则输出’NotNext’. 这个关系保存在[CLS]符号中.

预训练的时候可以达到97-98%的准确度.

6. fine-tuning(微调)

在海量语料上训练完BERT之后, 便可以将其应用到NLP的各个任务中了. 对于NSP任务来说, 其条件概率表示为 $P=softmax(CW^T)$, 其中 $C$ 是BERT输出中的[CLS]符号, $W$ 是可学习的权值矩阵.

对于其它任务来说, 也可以根据BERT的输出信息作出对应的预测. 下图展示了BERT在11个不同任务中的模型, 它们只需要在BERT的基础上再添加一个输出层便可以完成对特定任务的微调. 这些任务类似于文科试卷, 其中有选择题, 简答题等等. 下图中其中Tok表示不同的Token, $E$表示嵌入向量, $T_i$表示第 $i$ 个Token在经过BERT处理之后得到的特征向量.

微调的任务包括:

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

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

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

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

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

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

$P_i = \frac{e^{V \cdot C_i}}{\sum^4_{j=1}e^{V \cdot C_i}}$

(c)问答任务

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

$P_i = \frac{e^{S \cdot T_i}}{\sum_{j}e^{S \cdot T_i}}$

(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

7. 总结

优点

  • BERT是截至2018年10月的最新state of the art (SOTA)模型, 通过预训练和精调横扫了11项NLP任务, 这首先就是最大的优点了. 而且它还用的是Transformer, 也就是相对RNN更加高效、能捕捉更长距离的依赖. 对比起之前的预训练模型, 它捕捉到的是真正意义上的bidirectional context信息

缺点

作者在论文中主要提到的就是MLM预训练时的mask问题:

  1. [MASK]标记在实际预测中不会出现, 训练时用过多[MASK]影响模型表现
  2. 每个batch只有15%的token被预测, 所以BERT收敛得比left-to-right模型要慢(它们会预测每个token)

但总的来说, BERT开启了NLP领域的预训练学习, 大大提升了模型的训练效率.

BERT源代码结构(pytorch)

代码参考链接:https://github.com/huggingface/pytorch-pretrained-BERT

文件定位在 pytorch-transformers/pytorch_transformers/modeling_bert.py

包含的结构

  • class类:
    • BertModel
      • forward 函数
        • 接收 参数 :inputs,segment,mask’(符号’是可以为None的意思),position_ids’,head_mask’
        • 输出 :元组 (最后一层的隐变量,最后一层第一个token的隐变量,最后一层的隐变量或每一层attentions 权重参数)
        • 方法过程:embedding(关联类BertEmbeddings)->encoder(关联类BertEncoder)->pooler(关联类BertPooler)
  • BertEmbeddings
    • forword 函数
      • 接收 参数 :inputs,segment’,position_ids’
      • 输出 :words+position+segment的embedding
      • 方法过程 :调用nn.Embedding构造words、position、segment的embedding -> 三个embedding相加 -> 规范化 LayerNorm(关联类BertLayerNorm)-> dropout
  • BertLayerNorm
    • forword 函数
      • 方法过程:规范化,不多说
  • BertEncoder
    • forword 函数
      • 接收 参数 :hidden_states(由BetEmbeddings输出),attention_mask,head_mask’
      • 输出 :元组 (最后一层隐变量+每层隐变量) 或者 (最后一层attention+每一层attention)
      • 方法过程 :调用modulelist类实例layer使得每一层输出(关联类BertLayer)-> 保存所有层的attention输出 和 隐变量 -> 返回元组,元组第一个是最后一层的attention或hidden,再往后是每层的。
  • BertLayer
    • forword 函数
      • 接收 参数 :hidden_states(由上层BertLayer输出),attention_mask,head_mask
      • 输出:元组,(本层输出的隐变量,本层输出的attention)
      • 方法过程:调用attention(关联类BertAttention)得到attention_outputs -> 取第一维attention_output[0]作为intermediate的参数 ->调用intermediate(关联类BertIntermediate)-> 调用output(关联类BertOutput)得到layer_output -> layer_output 和 attention_outputs[1:]合并成元组返回
  • BertAttention
    • forword 函数
      • 接收 参数 :input_tensor(就是BertLayer的hidden_states),mask,head_mask’
      • 输出:
      • 方法过程:selfattention(关联类BertSelfAttention)得到 self_outputs-> 以self_outputs[0]作为参数调用selfoutput(关联类BertSelfOutput)得到 attention_output-> 返回元组(attention_output,self_outputs[1:] )第一个是语义向量,第二个是概率
  • BertSelfAttention
    • forword 函数
      • 接收 参数 :hidden_states(由BertLayer输出), mask,head_mask’
      • 输出:(context_layer语义向量,attention_prob概率)
      • 方法过程:selfattention方法,不多说
  • BertSelfOutput
    • forword 函数
      • 接收 参数 :hidden_states(由BertSelfAttention输出), input_tensor(就是BertAttention的input_tensor,也就是BertSelfAttention的输入)
      • 输出:hidden_states
      • 方法过程:对hidden_states加一层dense -> dropout -> 得到的hidden_states与input_tensor相加做LayerNorm(关联BertLayerNorm类) #这种做法说是为了避免梯度消失,也就是曾经的残差网络解决办法:output=output+Q
  • BertIntermediate
    • forword 函数
      • 接收 参数 :hidden_states(由BertSelfAttention输出), input_tensor(就是BertAttention的input_tensor,也就是BertSelfAttention的输入)
      • 输出:hidden_states
      • 方法过程:对hidden_states加一层dense,向量输出大小为intermedia_size -> 调用intermediate_act_fn,这个函数是由config.hidden_act得来,是gelu、relu、swish方法中的一个 #中间层存在的意义:我翻了翻文献,推测是能够使模型从低至高学习到多层级信息,从表面信息到句法到语义。还有人研究说中间层的可迁移性更好。
  • BertOutput
    • forword 函数
      • 接收 参数 :hidden_states(由BertSelfAttention输出), input_tensor(就是BertAttention的input_tensor,也就是BertSelfAttention的输入)
      • 输出:hidden_states
      • 方法过程:对hidden_states加一层dense ,由intermedia_size 又变回hidden_size -> dropout -> 得到的hidden_states与input_tensor相加做LayerNorm(关联BertLayerNorm类) #这种做法说是为了避免梯度消失,也就是曾经的残差网络解决办法:output=output+Q
  • BertPooler
    • forword 函数
      • 接收 参数 :hidden_states(由BertSelfAttention输出), input_tensor(就是BertAttention的input_tensor,也就是BertSelfAttention的输入)
      • 输出:pooled_output 一个pool后的output
    • 方法过程:简单取第一个token -> 加一层dense -> Tanh激活函数输出

——————————上面是整个encoding过程———————————

  • BertConfig
    • 保存BERT的各种参数配置
  • BertOnlyMLMHead
    • 使用mask 方法训练语言模型时用的,返回预测值
    • 过程:调用BertLMPredictionHead,返回的就是prediction_scores
  • BertLMPredictionHead
    • decode功能
    • 过程:调用BertPredictionHeadTransform -> linear层,输出维度是vocab_size
  • BertPredictionHeadTransform
    • 过程:dense -> 激活(gelu or relu or swish) -> LayerNorm
  • BertOnlyNSPHead
    • NSP策略训练模型用的,返回0或1
    • 过程:添加linear层输出size是2,返回seq_relationship_score
  • BertPreTrainingHeads
    • MLM和NSP策略都写在里面,输入的是Encoder的输出sequence_output, pooled_output
    • 返回(prediction_scores, seq_relationship_score)分别是MLM和NSP下的分值
  • BertPreTrainedModel
    • 从全局变量BERT_PRETRAINED_MODEL_ARCHIVE_MAP加载BERT模型的权重
  • BertForPreTraining
    • 计算score和loss
    • 通过BertPreTrainingHeads,得到prediction后计算loss,然后反向传播。
  • BertForMaskedLM
    • 只有MLM策略的loss
  • BertForNextSentencePrediction
    • 只有NSP策略的loss
  • BertForSequenceClassification
    • 计算句子分类任务的loss
  • BertForMultipleChoice
    • 计算句子选择任务的loss
  • BertForTokenClassification
    • 计算对token分类or标注任务的loss
  • BertForQuestionAnswering
    • 计算问答任务的loss
  • 全局变量
    • BERT_START_DOCSTRING
    • BERT_INPUTS_DOCSTRING
    • BERT_PRETRAINED_CONFIG_ARCHIVE_MAP
    • BERT_PRETRAINED_MODEL_ARCHIVE_MAP
  • 装饰器

  • ref: https://zhuanlan.zhihu.com/p/75558363

8. Reference

https://zhuanlan.zhihu.com/p/48612853

https://zhuanlan.zhihu.com/p/46652512

The Illustrated BERT, ELMo, and co. (How NLP Cracked Transfer Learning)

BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding