引入
BERT是谷歌在2018年10月发布的自然语言处理模型,它在十一项自然语言任务中打破记录,在有些任务中有显著提高,并超越了人类水平,被誉为开启了NLP的新时代。虽然,在之后又出现了大量新算法,这两年BERT仍然是各大比赛以及产品中的主流算法。论文地址:https://arxiv.org/pdf/1810.04805.pdf。
BERT全称为Bidirectional Encoder Representations from Transformers,从名字可以看出,它基于Transformer基础模型。在BERT之前,ELMo模型已经开始用预测训练方法从无监督数据中提取与上下文相关的词义;而GPT模型用Pretrain/Fine-tune方法,延用了预训练模型的结构和参数,但由于它是单向模型,主要用于据前文估计后文。而BERT使用了双向模型,遮蔽句中部分单词,训练句间关系等方法,提出了一套完整的解决方案,在模型结构不变的情况下,适配各种各样的NLP任务。
模型规模
BERT通过前期对大量的无标签数据的预训练pretain,显著地提高了后期在少量数据有标签任务上的表现fine-tune。在33亿单词(BooksCorpus 800M words,English Wikipedia 2,500M words)的无标注语料库上做预训练,BERT最终发布了BASE和LARGE两个版本的模型,官方也发布了中文模型。
BERT_BASE (L=12, H=768, A=12, Total Param-eters=110M)
BERT_LARGE (L=24, H=1024,A=16, Total Parameters=340M)
BERT_CHINESE(L=12, H=768, A=12, Total Param-eters=110M)
其中L为Transformer layer+ feed forward层数,H为隐藏层的维度,A为注意力头数。
原理
BERT是一个多层、双向,且只有Encoding编码部分的Transformer模型,先使用大量无标签数据训练,然后针对具体任务,加入最后一层,然后微调模型fine-tune。从而解决分类、推理、问答、命名实体识别等多种问题。
前篇讲到迁移学习的两种主流方法:第一种方法是用训练好的模型作为特征提取器;第二种方法是延用之前训练出的模型整体结构。因此,在预训练时,就要把接口给留出来,比如怎么支持分类,怎么判断前后关系……,设计模型时难度较高,这也是BERT模型的关键技术。
BERT的底层使用Transformer模型,改进了训练方法,更好地利用无监督数据,把一段话中词的之间关系(Attention)用参数描述出来。其中包含了词义、位置的先后关系、句间关系(Segment)。
预训练包括两个任务:第一个任务是屏蔽语言模型(后面详述);第二个任务是将上下句作为训练样本,用模型判断两句是否相关。两个任务各有一个损失函数值loss,将两个损失加起来作为总的损失进行优化。
遮蔽语言模型(训练句中词的关系)
屏蔽语言模型masked language model(Masked LM),它随机抠掉句中15%的单词,其中80%替换成[MASK],10%替换成随机词,另外10%只做替换标记,但不替换内容,让模型根据上下文猜测该单词。由于BERT是双向模型,它不仅能从前文中寻找线索,也能从后文中寻找线索,MLM极大地扩展了模型的适用场景,如解决完型填空之类的问题。
下一句预测(训练句间关系)
下一句预测next Sentence Prediction (NSP),用于训练模型识别句子之间的关系。将训练样本作为上下句,有50%样本,下句和上句存在真实的连续关系的,另外50%样本,下句和上句无关,用模型训练判断两句是否相关,从而将无标签数据变为有标签数据。
具体实现
BERT设计同一结构解决不同问题,pretain与fine-tune时模型结构几乎不变,从而利用少量数据fine-tune增量训练,生成高质量的模型。
首先,BERT定义了几种特殊字符: '[PAD]' : 0, 句子不够长时填补的空白字符 '[CLS]' : 1, 位于句首,为分类任务预留,可存储两句间的关系 '[SEP]' : 2, 标记句尾和句间位置 '[MASK]' : 3,随机遮蔽 例如随机取两个句子,组装在一起: [CLS]+句1+[SEP]+句2+[SEP];句中15%的词被替换;不够长的句子补[PAD]。如下图所示:
图片来自论文
输入数据由三部分组成:词的具体含义(token),分段信息(segment),位置信息(position)。这一结构在fine-tune时即可支持双句输入,也可支持单句输入。Pretain训练好的模型参数和结构,用于初始化针对特定目的训练fine-tune。
代码分析
论文中官方发布的代码地址https://github.com/google-research/bert,由Tensorflow实现。
如果使用pytorch,推荐https://github.com/graykode/nlp-tutorial,它是一个自然语言处理教程,由Pytorch实现。其中包括从Word2Vec、TextCNN到Transformer多个模型的演进。其主要优点是代码非常简单。比如BERT实现在nlp-tutorial/5-2.BERT/BERT_Torch.py文件中,只有200多行代码,其中一半以上和前篇Transformer相同,同时比Transformer翻译任务减少了Decoder部分,因此只需要考虑不到一半的基础逻辑。
也可参考https://github.com/huggingface/transformers,它的下载量仅次于google官方发布的TensorFlow版本。其中除了BERT还包括GPT2、CTRL、ROBERTA等多个基于transformer模型NLP工具的实现,它同时提供BERT和Pytorch代码。其中BERT的Pytorch实现包括1500行代码,例程相对完整,对于问答、分类、句间关系等问题均有具体实现的类及调用方法。
下面列出了解决问答的实例(在程序的最后部分):
class BertForQuestionAnswering(BertPreTrainedModel):
def __init__(self, config):
super(BertForQuestionAnswering, self).__init__(config)
self.num_labels = config.num_labels
self.bert = BertModel(config)
self.qa_outputs = nn.Linear(config.hidden_size, config.num_labels)
self.init_weights()
@add_start_docstrings_to_callable(BERT_INPUTS_DOCSTRING)
def forward(self, input_ids=None,
attention_mask=None, token_type_ids=None,
position_ids=None, head_mask=None,
inputs_embeds=None, start_positions=None,
end_positions=None,):
outputs = self.bert(input_ids,
attention_mask=attention_mask, token_type_ids=token_type_ids,
position_ids=position_ids, head_mask=head_mask,
inputs_embeds=inputs_embeds,)
sequence_output = outputs[0]
logits = self.qa_outputs(sequence_output)
start_logits, end_logits = logits.split(1, dim=-1)
start_logits = start_logits.squeeze(-1)
end_logits = end_logits.squeeze(-1)
outputs = (start_logits, end_logits,) + outputs[2:]
if start_positions is not None and end_positions is not None:
# If we are on multi-GPU, split add a dimension
if len(start_positions.size()) > 1:
start_positions = start_positions.squeeze(-1)
if len(end_positions.size()) > 1:
end_positions = end_positions.squeeze(-1)
# sometimes the start/end positions are outside our model inputs, we ignore these terms
ignored_index = start_logits.size(1)
start_positions.clamp_(0, ignored_index)
end_positions.clamp_(0, ignored_index)
loss_fct = CrossEntropyLoss(ignore_index=ignored_index)
start_loss = loss_fct(start_logits, start_positions)
end_loss = loss_fct(end_logits, end_positions)
total_loss = (start_loss + end_loss) / 2
outputs = (total_loss,) + outputs
return outputs # (loss), start_logits, end_logits, (hidden_states), (attentions)
问答给出两部分数据,第一部分是问题,第二部分是包含答案的段落,目标是找到答案在段落中的开始和结束位置。上例重写了其父类的初始化init和前向传播forward两个函数,在整个网络结构的最后加入了一个全连接层来计算位置;其核心是用预测的位置与实际位置的差异计算误差函数。
程序中也示例了该类的调用方法:
import torch
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertForQuestionAnswering.from_pretrained('bert-large-uncased-whole-word-masking-finetuned-squad')
question, text = "Who was Jim Henson?", "Jim Henson was a nice puppet"
input_ids = tokenizer.encode(question, text)
token_type_ids = [0 if i <= input_ids.index(102) else 1 for i in range(len(input_ids))]
start_scores, end_scores = model(torch.tensor([input_ids]), token_type_ids=torch.tensor([token_type_ids]))
all_tokens = tokenizer.convert_ids_to_tokens(input_ids)
answer = ' '.join(all_tokens[torch.argmax(start_scores) : torch.argmax(end_scores)+1])
assert answer == "a nice puppet"
由于Pytorch,TensorFlow已经提供了大量的工具,很多“高深”的模型,站在工具的基础上看并不困难。