NLP新星:BERT的优雅解读

2019-03-01 13:32发布

恰逢春节假期,研究了一下BERT。作为2018年自然语言处理领域的新星,BERT做到了过去几年NLP重大进展的集大成,一出场就技惊四座碾压竞争对手,刷新了11项NLP测试的最高纪录,甚至超越了人类的表现,相信会是未来NLP研究和工业应用最主流的语言模型之一。本文尝试由浅入深,为各位看客带来优雅的BERT解读。

NLP背景

BERT的应用舞台


NLP

Natural Language Process,自然语言处理,是计算机科学、信息工程以及人工智能的子领域,专注于人机交互,特别是大规模自然语言数据的处理和分析。

除了OCR、语音识别,自然语言处理有四大类常见的任务。第一类任务:序列标注,譬如命名实体识别、语义标注、词性标注、分词等;第二类任务:分类任务,譬如文本分类、情感分析等;第三类任务:句对关系判断,譬如自然语言推理、问答QA、文本语义相似性等;第四类任务:生成式任务,譬如机器翻译、文本摘要、写诗造句等。

GLUE benchmark

General Language Understanding Evaluation benchmark,通用语言理解评估基准,用于测试模型在广泛自然语言理解任务中的鲁棒性。

BERT刷新了GLUE benchmark的11项测试任务最高记录,这11项测试任务可以简单分为3类。序列标注类:命名实体识别CoNNL 2003 NER;单句分类类:单句情感分类SST-2、单句语法正确性分析CoLA;句对关系判断类:句对entailment关系识别MNLI和RTE、自然语言推理WNLI、问答对是否包含正确答案QNLI、句对文本语义相似STS-B、句对语义相等分析QQP和MRPC、问答任务SQuAD v1.1。虽然论文中没有提及生成式任务,BERT核心的特征提取器源于谷歌针对机器翻译问题所提出的新网络框架Transformer,本身就适用于生成式任务。

语言模型的更迭

BERT之集大成

LM

Language Model,语言模型,一串词序列的概率分布,通过概率模型来表示文本语义。

语言模型有什么作用?通过语言模型,可以量化地衡量一段文本存在的可能性。对于一段长度为n的文本,文本里每个单词都有上文预测该单词的过程,所有单词的概率乘积便可以用来评估文本。在实践中,如果文本很长,P(wi|context(wi))的估算会很困难,因此有了简化版:N元模型。在N元模型中,通过对当前词的前N个词进行计算来估算该词的条件概率。对于N元模型,常用的有unigram、bigram和trigram,N越大,越容易出现数据稀疏问题,估算结果越不准。此外,N元模型没法解决一词多义和一义多词问题。

为了解决N元模型估算概率时的数据稀疏问题,研究者提出了神经网络语言模型,代表作有2003年Bengio等提出了的NNLM,但效果并不吸引人,足足沉寂了十年。在另一计算机科学领域机器视觉,深度学习混得风生水起,特别值得一提的是预训练处理,典型代表:基于ImageNet预训练的Fine-Tuning模型。图像领域的预处理跟现在NLP领域的预训练处理思路相似,基于大规模图像训练数据集,利用神经网络预先训练,将训练好的网络参数保存。当有新的任务时,采用相同的网络结构,加载预训练的网络参数初始化,基于新任务的数据训练模型,Frozen或者Fine-Tuning。Frozen指底层加载的预训练网络参数在新任务训练过程中不变,Fine-Tuning指底层加载的预训练网络参数会随着新任务训练过程不断调整以适应当前任务。深度学习是适用于大规模数据,数据量少训练出来的神经网络模型效果并没有那么好。所以,预训练带来的好处非常明显,新任务即使训练数据集很小,基于预训练结果,也能训练出不错的效果。

深度学习预训练在图像领域的成果,吸引研究者探索预训练在NLP领域的应用,譬如Word Embedding。2013年开始大火的Word Embedding工具Word2Vec,Glove跟随其后。Word2Vec有2种训练方式:CBOW和Skip-gram。CBOW指抠掉一个词,通过上下文预测该词;Skip-gram则与CBOW相反,通过一个词预测其上下文。不得不说,Word2Vec的CBOW训练方式,跟BERT“完形填空”的学习思路有异曲同工之妙。



一个单词通过Word Embedding表示,很容易找到语义相近的单词,但单一词向量表示,不可避免一词多义问题。于是有了基于上下文表示的ELMo和OpenAI GPT。

ELMo,Embedding from Language Models,基于上下文对Word Embedding动态调整的双向神经网络语言模型。ELMo采用的是一种“Feature-based Approaches”的预训练模式,分两个阶段:第一阶段采用双层双向的LSTM模型进行预训练;第二阶段处理下游任务时,预训练网络中提取出的Word Embedding作为新特征添加到下游任务中,通过双层双向LSTM模型补充语法语义特征。相比Word2Vec,ELMo很好地解决了一词多义问题,在6个NLP测试任务中取得SOTA。



Transformer

谷歌提出的新网络结构,这里指Encoder特征提取器。LSTM提取长距离特征有长度限制,而Transformer基于self-Attention机制,任意单元都会交互,没有长度限制问题,能够更好捕获长距离特征。

GPT,Generative Pre-Training,OpenAI提出的基于生成式预训练的单向神经网络语言模型。GPT采用的是一种“Fine-Tuning Approaches”的预训练模式,同样分两个阶段: 第一阶段采用Transformer模型通过上文预测的方式进行预训练;第二阶段采用Fine-Tuning的模式应用到下游任务。GPT的效果同样不错,在9个NLP测试任务中取得SOTA。不过,GPT这种单向训练模式,会丢失下文很多信息,在阅读理解这类任务场景就没有双向训练模式那么优秀。



BERT,Bidirectional Encoder Representations from Transformers,基于Transformer的双向语言模型。同样,BERT采用跟GPT一样的“Fine-Tuning Approaches”预训练模式,分两个阶段:第一阶段采用双层双向Transformer模型通过MLM和NSP两种策略进行预训练;第二阶段采用Fine-Tuning的模式应用到下游任务。有人戏称:Word2Vec + ELMo + GPT = BERT,不过也并无道理,BERT吸收了这些模型的优点:“完形填空”的学习模式迫使模型更多依赖上下文信息预测单词,赋予了模型一定的纠错能力;Transformer模型相比LSTM模型没有长度限制问题,具备更好的能力捕获上下文信息特征;相比单向训练模式,双向训练模型捕获上下文信息会更加全面;等等。当然,效果才是王道,集大成者BERT拿了11项SOTA。



论文解读

BERT原理

相关论文

2017年,谷歌发表《Attention Is All You Need》,提出Transformer模型;

2018年,谷歌发表《BERT:Pre-training of Deep Bidirectional Transformers for Language Understanding》,提出基于Transformer的语言模型BERT。

在未来NLP领域的研究和应用,BERT有两点值得被借鉴:其一,基于Transformer编码器作特征提取,结合MLM&NSP策略预训练;其二,超大数据规模预训练Pre-Training+具体任务微调训练Fine-Tuning的两阶段模式。

1.特征提取器

____________

Transformer Encoder,特征提取器,由Nx个完全一样的layer组成,每个layer有2个sub-layer,分别是:Multi-Head Self-Attention机制、Position-Wise全连接前向神经网络。对于每个sub-layer,都添加了2个操作:残差连接Residual Connection和归一化Normalization,用公式来表示sub-layer的输出结果就是LayerNorm(x+Sublayer(x))。



Attention Mechanism。为什么要有注意力机制?换句话说,注意力机制有什么好处?类比人类世界,当我们看到一个人走过来,为了识别这个人的身份,眼睛注意力会关注在脸上,除了脸之后的其他区域信息会被暂时无视或不怎么重视。对于语言模型,为了模型能够更加准确地判断,需要对输入的文本提取出关键且重要的信息。怎么做?对输入文本的每个单词赋予不同的权重,携带关键重要信息的单词偏向性地赋予更高的权重。抽象来说,即是:对于输入Input,有相应的向量query和key-value对,通过计算query和key关系的function,赋予每个value不同的权重,最终得到一个正确的向量输出Output。在Transformer编码器里,应用了两个Attention单元:Scaled Dot-Product Attention和Multi-Head Attention。

Scaled Dot-Product Attention。Self-Attention机制是在该单元实现的。对于输入Input,通过线性变换得到Q、K、V,然后将Q和K通过Dot-Product相乘计算,得到输入Input中词与词之间的依赖关系,再通过尺度变换Scale、掩码Mask和Softmax操作,得到Self-Attention矩阵,最后跟V进行Dot-Product相乘计算。



Multi-Head Attention。通过h个不同线性变换,将d_model维的Q、K、V分别映射成d_k、d_k、d_v维,并行应用Self-Attention机制,得到h个d_v维的输出,进行拼接计算Concat、线性变换Linear操作。



2.输入特征处理

______________

BERT的输入是一个线性序列,支持单句文本和句对文本,句首用符号[CLS]表示,句尾用符号[SEP]表示,如果是句对,句子之间添加符号[SEP]。输入特征,由Token向量、Segment向量和Position向量三个共同组成,分别代表单词信息、句子信息、位置信息。

3.预训练

________

BERT采用了MLM和NSP两种策略用于模型预训练。为了证明这两种策略的效果,谷歌额外增加了两组对照实验。对照组一:No NSP,保留MLM,但没有NSP;对照组二:LTR & No NSP,没有MLM和NSP,替换成一个Left-to-Right(LTR)模型,甚至为了增强可信性,在对照组二的基础上增加一个随机初始化的BiLSTM。实验数据表明,BERT采用MLM&NSP策略完胜其他。



LM,Masked LM。对输入的单词序列,随机地掩盖15%的单词,然后对掩盖的单词做预测任务。相比传统标准条件语言模型只能left-to-right或right-to-left单向预测目标函数,MLM可以从任意方向预测被掩盖的单词。不过这种做法会带来两个缺点:1.预训练阶段随机用符号[MASK]替换掩盖的单词,而下游任务微调阶段并没有Mask操作,会造成预训练跟微调阶段的不匹配;2.预训练阶段只对15%被掩盖的单词进行预测,而不是整个句子,模型收敛需要花更多时间。对于第二点,作者们觉得效果提升明显还是值得;而对于第一点,为了缓和,15%随机掩盖的单词并不是都用符号[MASK]替换,掩盖单词操作进行了以下改进,同时举例“my dog is hairy”挑中单词“hairy”。

80%用符号[MASK]替换:my dog is hairy -> my dog is [MASK]

10%用其他单词替换:my dog is hairy -> my dog is apple

10%不做替换操作:my dog is hairy -> my dog is hairy

NSP,Next Sentence Prediction。许多重要的下游任务譬如QA、NLI需要语言模型理解两个句子之间的关系,而传统的语言模型在训练的过程没有考虑句对关系的学习。NSP,预测下一句模型,增加对句子A和B关系的预测任务,50%的时间里B是A的下一句,分类标签为IsNext,另外50%的时间里B是随机挑选的句子,并不是A的下一句,分类标签为NotNext。

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

he brought a gallon [MASK] milk [SEP]

Label = IsNext

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

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

Label = NotNext

4.任务微调

__________

BERT提供了4种不同下游任务的微调方案:

(a)句对关系判断,第一个起始符号[CLS]经过Transformer编码器后,增加简单的Softmax层,即可用于分类;

(b)单句分类任务,具体实现同(a)一样;

(c)问答类任务,譬如SQuAD v1.1,问答系统输入文本序列的question和包含answer的段落,并在序列中标记answer,让BERT模型学习标记answer开始和结束的向量来训练模型;

(d)序列标准任务,譬如命名实体标注NER,识别系统输入标记好实体类别(人、组织、位置、其他无名实体)的文本序列进行微调训练,识别实体类别时,将序列的每个Token向量送到预测NER标签的分类层进行识别。



源码分析

BERT实现

PyTorch

Torch的python包,而Torch是一个用于机器学习和科学计算的模块化开源库。Anaconda Python针对Interl架构调整了MKL,使得PyTorch在Interl CPU上的性能达到最佳;此外,PyTorch支持NVIDIA GPU,能够利用GPU加速训练。

网上已经有各个版本的BERT源码,譬如谷歌开源的基于TensorFlow的BERT源码,谷歌推荐的能够加载谷歌预训练模型的PyTorch-PreTrain-BERT版本。我用来源码分析学习的是另一个PyTorch版本:Google AI 2018 BERT PyTorch Implementation,对照谷歌开源的tf-BERT版本开发实践。

以下代码如需使用,请用网页浏览器查看。

MLM模型实现:

class MaskedLanguageModel(nn.Module):

def __init__(self, hidden, vocab_size):

super(MaskedLanguageModel, self).__init__()

self.linear = nn.Linear(hidden, vocab_size)

self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, x):

return self.softmax(self.linear(x))

NSP模型实现(分类模型,同样可以适用于单句分类或句对关系判断任务。):

class NextSentencePrediction(nn.Module):

def __init__(self, hidden):

super(NextSentencePrediction, self).__init__()

self.linear = nn.Linear(hidden, 2)

self.softmax = nn.LogSoftmax(dim=-1)

def forward(self, x):

return self.softmax(self.linear(x[:, 0]))

BERT-Encoder实现:

class BERT(PreTrainedBERTModel):

def __init__(self, vocab_size, hidden=768, n_layers=12, attn_heads=12, dropout=0.1):

config = BertConfig(vocab_size, hidden_size=hidden, num_hidden_layers=n_layers,num_attention_heads=attn_heads, hidden_dropout_prob=dropout)

super(BERT, self).__init__(config)

self.hidden = hidden

self.n_layers = n_layers

self.attn_heads = attn_heads

self.feed_forward_hidden = hidden * 4

self.embedding = BERTEmbedding(vocab_size=vocab_size, embed_size=hidden)

self.transformer_blocks = nn.ModuleList([TransformerBlock(hidden, attn_heads, hidden * 4, dropout) for _ in range(n_layers)])

def forward(self, x, segment_info):

mask = (x > 0).unsqueeze(1).repeat(1, x.size(1), 1).unsqueeze(1)

x = self.embedding(x, segment_info)

for transformer in self.transformer_blocks:

x = transformer.forward(x, mask)

return x

Transformer实现:

class TransformerBlock(nn.Module):

def __init__(self, hidden, attn_heads, feed_forward_hidden, dropout):

super(TransformerBlock, self).__init__()

self.attention = MultiHeadedAttention(h=attn_heads, d_model=hidden)

self.feed_forward = PositionwiseFeedForward(d_model=hidden, d_ff=feed_forward_hidden, dropout=dropout)

self.input_sublayer = SublayerConnection(size=hidden, dropout=dropout)

self.output_sublayer = SublayerConnection(size=hidden, dropout=dropout)

self.dropout = nn.Dropout(p=dropout)

def forward(self, x, mask):

x = self.input_sublayer(x, lambda _x: self.attention.forward(_x, _x, _x, mask=mask))

x = self.output_sublayer(x, self.feed_forward)

return self.dropout(x)

Multi-Head Attention实现:

class MultiHeadedAttention(nn.Module):

def __init__(self, h, d_model, dropout=0.1):

super().__init__()

assert d_model % h == 0

self.d_k = d_model // h

self.h = h

self.linear_layers = nn.ModuleList([nn.Linear(d_model, d_model) for _ in range(3)])

self.output_linear = nn.Linear(d_model, d_model)

self.attention = Attention()

self.dropout = nn.Dropout(p=dropout)

def forward(self, query, key, value, mask=None):

batch_size = query.size(0)

query, key, value = [l(x).view(batch_size, -1, self.h, self.d_k).transpose(1, 2)

for l, x in zip(self.linear_layers, (query, key, value))]

x, attn = self.attention(query, key, value, mask=mask, dropout=self.dropout)

x = x.transpose(1, 2).contiguous().view(batch_size, -1, self.h * self.d_k)

return self.output_linear(x)

Self-Attention实现:

class Attention(nn.Module):

def forward(self, query, key, value, mask=None, dropout=None):

scores = torch.matmul(query, key.transpose(-2, -1))/math.sqrt(query.size(-1))

if mask is not None:

scores = scores.masked_fill(mask == 0, -1e9)

p_attn = F.softmax(scores, dim=-1)

if dropout is not None:

p_attn = dropout(p_attn)

return torch.matmul(p_attn, value), p_attn

SubLayerConnection实现:

class SublayerConnection(nn.Module):

def __init__(self, size, dropout):

super(SublayerConnection, self).__init__()

self.norm = LayerNorm(size)

self.dropout = nn.Dropout(dropout)

def forward(self, x, sublayer):

return x + self.dropout(sublayer(self.norm(x)))

参考资料:

1. Jacob Devlin, Ming-Wei Chang, Kenton Lee, and Krisina Toutanova.2018.BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.

2. Matthew Peters, Mark Neumann, Mohit Iyyer, Matt Gardner, Christopher Clark, Kenton Lee, and Luke Zettlemoyer. 2018. Deep contextualized word rep- resentations. In NAACL.

3. Alec Radford, Karthik Narasimhan, Tim Salimans, and Ilya Sutskever. 2018. Improving language under- standing by Generative Pre-Training. Technical re- port, OpenAI.

4. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N Gomez, Lukasz Kaiser, and Illia Polosukhin. 2017. Attention is all you need. In Advances in Neural Information Pro- cessing Systems, pages 6000–6010.

5. J. Deng, W. Dong, R. Socher, L.-J. Li, K. Li, and L. Fei- Fei. 2009. ImageNet: A Large-Scale Hierarchical Image Database. In CVPR09.

6. Tomas Mikolov, Kai Chen, Greg Corrado, and Jeffrey Dean. 2013.Efficient Estimation of Word Representations in Vector Space.

文章来源: https://www.toutiao.com/group/6663248635641201164/