BERT

文章目录

  • 词向量模型
    • word2vec --> ELMo
    • ELMo --> BERT
  • BERT的三个亮点
    • 1.Masked Language Model
    • 2.Transformer —— attention is all you need
    • 3. sentence-level representation
  • 迁移策略
  • BERT的使用
    • 加载BERT
    • 使用模型
  • 参考资料

词向量模型

横向比较word2vec,ELMo,BERT这三个模型:
词向量是一个工具,可以把真实世界抽象存在的文字转化成可以进行数学公式操作的向量,而对这些向量的操作,才是NLP真正要做的任务。因而某种意义上,NLP任务可以分成两个部分:预训练产生词向量和对词向量操作(下游具体NLP任务)。
从word2vec到ELMo到BERT,这个阶段做的其实就是把对下游具体NLP任务的活逐渐移到预训练词向量上。

word2vec --> ELMo

结果:上下文无关的static向量变成上下文相关的dynamic向量,比如苹果在不同语境vector不同。
操作:encoder操作转移到预训练产生词向量过程实现。

ELMo --> BERT

结果:训练出的word-level向量变成sentence-level的向量,下游具体NLP任务调用更方便,修正了ELMo模型的潜在问题。
操作:使用句子级负采样获得句子表示/句对关系,Transformer模型代替LSTM提升表达和时间上的效率,masked LM解决“自己看到自己”的问题。

1.word2vec
线性模型:
很神奇的地方,从而也说明高维空间映射的词向量可以很好体现真实世界中token之间的关系。如:king-man = queen-woman

负采样:
由于训练词向量模型的目标不是为了得到一个多么精准的语言模型,而是为了获得它的副产物——词向量。所以要做到的不是在几万几十万个token中艰难计算softmax获得最优的那个词(就是预测的对于给定词的下一词),而只需能做到在几个词中找到对的那个词就行,这几个词包括一个正例(即直接给定的下一词),和随机产生的噪声词(采样抽取的几个负例),就是说训练一个sigmoid二分类器,只要模型能够从中找出正确的词就认为完成任务。
这种负采样思想也应用到之后的BERT里,只不过从word-level变成sentence-level,这样能获取句子间关联关系。

缺点是上下文无关(static):
因而为了让句子有一个整体含义(context),大家会在下游具体的NLP任务中基与词向量的序列做encoding操作。
下面是一个比较表格,预测目标这里的next word下一个词,是所有传统语言模型都做的事——寻找下一个词填什么。
在这里插入图片描述
2. ELMo
介绍ELMo的两个方面,一个是它的encoder模型Bi-LSTM,另一个是它和下游具体NLP任务的接口(迁移策略)。

Bi-LSTM做encoder实现上下文相关(context):
这里就是之前说的把下游具体NLP任务放到预训练产生词向量里面,从而达到获得一个根据context不同不断变化的dynamic词向量。具体实现方法是使用双向语言模型Bi-LSTM来实现,如下面左图所示。从前到后和后到前分别做一遍LSTM的encoding操作,从而获得两个方向的token联系,进而获得句子的context。
但这里有两个潜在问题,姑且称作“不完全双向”和“自己看见自己”。
首先,“不完全双向”是指模型的前向和后向LSTM两个模型是分别训练的,从图中也可以看出,对于一个序列,前向遍历一遍获得左边的LSTM,后向遍历一遍获得右边的LSTM,最后得到的隐层向量直接拼接得到结果向量(前向的hidden state1 + 后向的hidden state2 = 总的hidden state,+是concat),并且在最后的Loss function中也是前向和后向的loss function直接相加,并非完全同时的双向计算。

另外,“自己看见自己”是指要预测的下一个词在给定的序列中已经出现的情况。传统语言模型的数学原理决定了它的单向性。
从公式 p ( s ) = p ( w 0 ) p ( w 1 ∣ w 0 ) p ( w 2 ∣ w 1 , w 0 ) p ( w 3 ∣ w 2 , w 1 , w 0 ) . . . p ( w n ∣ c o n t e x t ) p(s)=p(w_0)p(w_1|w_0)p(w_2|w_1,w_0)p(w_3|w_2,w_1,w_0)...p(w_n|context) p(s)=p(w0)p(w1w0)p(w2w1,w0)p(w3w2,w1,w0)...p(wncontext)可以看出,传统语言模型的目标是获得在给定序列从头到尾条件概率相乘后概率最大的下一词,而双向模型会导致预测的下一词已经在给定序列中出现了的问题,这就是“自己看见自己”。如下面右图所示(图片从下往上看),最下行是训练数据A B C D,经过两个Bi-lstm操作,需要预测某个词位置的内容。比如第二行第二列A|CD这个结果是第一层Bi-lstm在B位置输出内容,包括正向A和反向CD,直接拼接成A|CD。比如第三行第二例ABCD这个结果是前向BCD和反向AB|D拼接结果,而当前位置需要预测的是B,已经在ABCD中出现了,这就会有问题。因而对于Bi-LSTM,只要层数增加,就是会存在“自己看见自己”的问题。
BERT_第1张图片
与下游具体NLP任务接口:
ELMo模型将context的encoding操作从下游具体NLP任务转换到了预训练词向量这里,但在具体应用时要做出一些调整。当Bilstm有多层时,由于每层会学到不同的特征,而这些特征在具体应用中侧重点不同,每层的关注度也不同。ELMo给原始词向量层和每个RNN隐层都设置了一个可训练参数,通过softmax层归一化后乘到相应的层上并求和起到了加权作用。
比如,原本论文中设定了两个隐层,第一隐层可以学到对词性、句法等信息,对此有明显需求的任务可以对第一隐层参数学到比较大的值;第二隐层更适合对词义消歧有需求的任务,从而分配更高权重。
下面是 ELMo的比较表格。
在这里插入图片描述
3.BERT
BERT_第2张图片
BERT模型进一步增加词向量模型泛化能力,充分描述字符级、词级、句子级甚至句间关系特征。

真正的双向encoding:
Masked LM,类似完形填空,尽管仍旧看到所有位置信息,但需要预测的词已被特殊符号代替,可以放心双向encoding。

Transformer做encoder实现上下文相关(context):
使用Transformer而不是Bi-LSTM做encoder,可以有更深的层数、具有更好并行性。并且线性的Transformer比lstm更易免受mask标记影响,只需要通过self-attention减小mask标记权重即可,而lstm类似黑盒模型,很难确定其内部对于mask标记的处理方式。

提升至句子级别:
学习句子/句对关系表示,句子级负采样。首先给定的一个句子,下一句子正例(正确词),随机采样一句负例(随机采样词),句子级上来做二分类(即判断句子是当前句子的下一句还是噪声),类似word2vec的单词级负采样。
在这里插入图片描述

BERT的三个亮点

1.Masked Language Model

随机mask语料中15%的token,然后将masked token 位置输出的最终隐层向量送入softmax,来预测masked token。
这样输入一个句子,每次只预测句子中大概15%的词,所以BERT训练很慢。
在这里插入图片描述
而对于盖住词的特殊标记,在下游NLP任务中不存在。因此,为了和后续任务保持一致,作者按一定的比例在需要预测的词位置上输入原词或者输入某个随机的词。如:my dog is hairy

  • 有80%的概率用“[mask]”标记来替换——my dog is [MASK]
  • 有10%的概率用随机采样的一个单词来替换——my dog is apple
  • 有10%的概率不做替换——my dog is hairy

2.Transformer —— attention is all you need

下面左图是transformer模型一个结构,分成左边Nx框框的encoder和右边Nx框框的decoder,相较于RNN+attention常见的encoder-decoder之间的attention(上边的一个橙色框),还多出encoder和decoder内部的self-attention(下边的两个橙色框)。每个attention都有multi-head特征。最后,通过position encoding加入没考虑过的位置信息。
BERT_第3张图片
multi-head attention:
将一个词的vector切分成h个维度,求attention相似度时每个h维度计算。由于单词映射在高维空间作为向量形式,每一维空间都可以学到不同的特征,相邻空间所学结果更相似,相较于全体空间放到一起对应更加合理。比如对于vector-size=512的词向量,取h=8,每64个空间做一个attention,学到结果更细化。

self-attention:
每个词位的词都可以无视方向和距离,有机会直接和句子中的每个词encoding。比如上面右图这个句子,每个单词和同句其他单词之间都有一条边作为联系,边的颜色越深表明联系越强,而一般意义模糊的词语所连的边都比较深。比如:law,application,missing,opinion

position encoding:
因为Transformer既没有RNN的recurrence也没有CNN的convolution,但序列顺序信息很重要,比如你欠我100万明天要还和我欠你100万明天要还的含义截然不同。
Transformer计算token的位置信息这里使用正弦波↓,类似模拟信号传播周期性变化。这样的循环函数可以一定程度上增加模型的泛化能力。
在这里插入图片描述
但BERT直接训练一个position embedding来保留位置信息,每个位置随机初始化一个向量,加入模型训练,最后就得到一个包含位置信息的embedding(简单粗暴。。),最后这个position embedding和word embedding的结合方式上,BERT选择直接拼接。

3. sentence-level representation

在很多任务中,仅仅靠encoding是不足以完成任务的(这个只是学到了一堆token级的特征),还需要捕捉一些句子级的模式,来完成SLI、QA、dialogue等需要句子表示、句间交互与匹配的任务。对此,BERT又引入了另一个极其重要却又极其轻量级的任务,来试图把这种模式也学习到。

句子级负采样
句子级别的连续性预测任务,即预测输入BERT的两端文本是否为连续的文本。训练的时候,输入模型的第二个片段会以50%的概率从全部文本中随机选取,剩下50%的概率选取第一个片段的后续的文本。 即首先给定的一个句子(相当于word2vec中给定context),它下一个句子即为正例(相当于word2vec中的正确词),随机采样一个句子作为负例(相当于word2vec中随机采样的词),然后在该sentence-level上来做二分类(即判断句子是当前句子的下一句还是噪声)。

句子级表示
在这里插入图片描述
BERT是一个句子级别的语言模型,不像ELMo模型在与下游具体NLP任务拼接时需要每层加上权重做全局池化,BERT可以直接获得一整个句子的唯一向量表示。它在每个input前面加一个特殊的记号[CLS],然后让Transformer对[CLS]进行深度encoding,由于Transformer是可以无视空间和距离的把全局信息encoding进每个位置的,而[CLS]的最高隐层作为句子/句对的表示直接跟softmax的输出层连接,因此其作为梯度反向传播路径上的“关卡”,可以学到整个input的上层特征。

segment embedding
对于句对来说,EA和EB分别代表左句子和右句子;对于句子来说,只有EA。这个EA和EB也是随模型训练出来的。
如下图所示,最终输入结果会变成下面3个embedding拼接的表示。
BERT_第4张图片

迁移策略

下游具体NLP任务主要分为4大类

  • 序列标注:分词、实体识别、语义标注…
  • 分类任务:文本分类、情感计算…
  • 句子关系判断:entailment、QA、自然语言处理
  • 生成式任务:机器翻译、文本摘要

BERT将传统大量在下游具体NLP任务中做的操作转移到预训练词向量中,在获得使用BERT词向量后,最终只需在词向量上加简单的MLP或线性分类器即可。比如论文中所给的几类任务:
BERT_第5张图片
对于左边两幅文本分类任务和文本匹配任务(文本匹配其实也是一种文本分类任务,只不过输入是文本对)来说,只需要用得到的表示(即encoder在[CLS]词位的顶层输出)加上一层MLP就好。
a——句子关系判断(句对匹配):
MultiNLI文本蕴含识别(M推理出N,蕴含/矛盾/中立),QQP(文本匹配),QNLI(自然语言问题推理),STS-B(语义文本相似度1-5),MRPC(微软研究释义语料库,判断文本对语音信息是否等价)、RTE(同MNLI,小数据),SWAG(113k多项选择问题组成的数据集,涉及丰富的基础情境)
b——分类任务(文本匹配):
SST-2(斯坦福情感分类树),CoLA(语言可接受性预测)
对于左三图抽取式任务,用两个线性分类器分别输出span的起点和终点
c——序列标注(文本抽取):
SQuAD(斯坦福问答数据集,从phrase中选取answer)
对于左四图序列标注任务,就只需要加softmax输出层
d——序列标注:
NER命名实体识别

BERT的使用

加载BERT

TensorFlow模型加载方法

import tensorflow as tf
from bert import modeling
import os

pathname = "chinese_L-12_H-768_A-12/bert_model.ckpt" # 模型地址
bert_config = modeling.BertConfig.from_json_file("chinese_L-12_H-768_A-12/bert_config.json")# 配置文件地址。
configsession = tf.ConfigProto()
configsession.gpu_options.allow_growth = True
sess = tf.Session(config=configsession)
input_ids = tf.placeholder(shape=[64, 128], dtype=tf.int32, name="input_ids")
input_mask = tf.placeholder(shape=[64, 128], dtype=tf.int32, name="input_mask")
segment_ids = tf.placeholder(shape=[64, 128], dtype=tf.int32, name="segment_ids")

with sess.as_default():
    model = modeling.BertModel(
        config=bert_config,
        is_training=True,
        input_ids=input_ids,
        input_mask=input_mask,
        token_type_ids=segment_ids,
        use_one_hot_embeddings=False)
    saver = tf.train.Saver()
    sess.run(tf.global_variables_initializer())# 这里尤其注意,先初始化,在加载参数,否者会把bert的参数重新初始化。这里和demo1是有区别的
    saver.restore(sess, pathname)
    print(1)

使用模型

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

output_layer = model.get_sequence_output()# 这个获取每个token的output 输出[batch_size, seq_length, embedding_size] 如果做seq2seq 或者ner 用这个

output_layer = model.get_pooled_output() # 这个获取句子的output

参考资料

彻底搞懂BERT
BERT使用详解
BERT相关论文、文章和代码资源汇总

你可能感兴趣的:(NLP基础学习)