BERT介绍
1 前言
在NLP语言的模型中,ELMo可以对上下文进行双向编码,显著改进了自然语言处理任务的解决方案,但使用的是特定的任务架构;GPT是任务无关的,不必为特定的任务设置特定的架构,但是它只能从左到右编码上下文。那么,有没有一个模型,将二者的优点结合起来呢?答,有的,就是这里要提到的BERT。
当前,BERT被应用在文本分类、文本相似等任务中,并取得了良好的效果。
2 概念与业务相关
2.1 文本嵌入任务
文本嵌入,即文本向量化,将输入文本进行向量化。文本向量化后,可以做到使用余弦相似度等方法计算向量计算文本的相似度,也可以利用向量搜索引擎进行文本的向量检索。
在传统的NLP中,我们将文本数据视作离散符号,可以使用one-hot向量表示。但这时,文本数据可以使用向量来表示,但他们之间没有上下文的概念。
文本嵌入,通俗的说,是为每个文本数据建立密集向量,使得相近的文本数据有相近的向量表示。
通过文本嵌入,可以让模型更好地理解数据中存在的特定领域语言、语义关系和上下文线索,当前,文本嵌入在自然语言处理、情感分析、文本分类、机器翻译等任务上有着广泛的应用。
而BERT能够在NLP中有着广泛的应用,很大一部分归功于BERT能够将单词的意思嵌入到密集向量的能力。
2.2 文本相似
在NLP所涉及的模型中,大部分只能输入一个句子,如果想要比较“A woman is dancing."和"A man is talking."这两句话是否相似,仅有一个输入是很难做到的。而BERT中存在【CLS】和【SEP】,可以同时输入两个句子,在计算文本相似方面有着很大的优势。
使用BERT,可以将输入的两个句子转为向量,在BERT中,相近的文本数据对应的向量也是相近的,此时可以计算两个向量的相似度(如余弦相似度),来衡量二者是否相近。
2.3 文本分类
文本分类是NLP任务之一,是指计算机将载有信息的一篇文本映射到预先给定的某一类别或某几类别主题的过程。
这里,以文本分类中的情感分类为例,我们希望机器能够像人类一样“思考”这句话的情感是怎样的。
但有的时候,一个句子中文字的顺序不一样,或者有的词存在一词多义的情况,对于传统的NLP模型,很难解决这一问题。
可以阅读下面两个句子:
- 这部剧虽然瑕疵太多,但很有趣。
- 这部剧虽然很有趣,但瑕疵太多。
两句话,词语的顺序不一样,表达的情感也不一样。
而BERT能够根据上下文,来“理解”句子中表达的情感,非常适合解决文本分类的问题。
3 简单技术与应用
一般来说,要想让BERT更好地应用与NLP问题,一般会经历两个步骤:预训练和微调。
3.1 预训练
不妨回想一下我们学习语言的过程。基本都是先学习笔画,再结合笔画学习写字,学会了字以后再学习词语相关的知识(比如组词,近义词,反义词),之后会学习连词成句。通过这么一个漫长的过程,我们才能够掌握一门语言。而在实际面对一个问题时,往往只需要调用一部分词语、句子就能够完成。比如,如果现在需要考虑今天中午吃什么饭,或许你只需要考虑“米饭”、“馒头”、“牛肉面”、“肝膋”、“青菜炒扁豆”等,这个时候,一般也不会使用“计算机”、“挖掘机”、“体育西路”等词汇。但如果你没有语言基础,面对这个问题,会很难想到这些词汇。可以说,在实际解决这个问题时,你已经把以前学到的语言知识都带进去了。
但如果把这个问题应用到机器上,会有一个很明显的特点:实际的训练数据并不是很多。比如你需要一个智能推荐今天中午吃啥外卖的模型,一般训练的时候会常用:“开封菜测评,吮指原味鸡真的好吃”,“这家的菜真不行,尤其是群英荟萃,就跟萝卜开会,就这还要你八十不嫌贵”,“推荐这家苏格兰打卤面,他家只要卤是免费的”等有关数据。但要想让机器从0开始学习语言,往往这个代价都非常巨大。
而预训练可以很好的解决这个问题,使用尽可能多的训练数据,从中提取出尽可能多的共性特征,从而让模型对特定任务的学习负担变轻。
当前,很多的BERT预训练模型都是开源的,并且支持不同的语言。在官网中,提供了以下的预训练模型。
这里,BERT-Based, Chinese是常用的中文模型。
3.2 微调
对于广泛的自然语言处理应用,BERT只需要最少的架构更改。 然而,这一好处是以微调下游应用的大量BERT参数为代价的。 当空间或时间有限时,基于多层感知机、卷积神经网络、循环神经网络 和注意力的精心构建的模型更具可行性。
微调的具体步骤如下:
Step1:读取数据,并处理成BERT输入格式数据
首先读取训练集、测试集数据(也可以使用sklearn划分),之后,使用tokenizer
将其处理为BERT的输入格式。
Step2:封装成迭代器
使用Dataset、DataLoader将其封装为迭代器。
Step3:封装微调模型
下面所示是一个对bert模型进行微调的代码,在这里,使用的是一个线性层作为微调使用,当然,根据具体情况,也可以选择其它的架构(如CNN等)
from transformers import BertModel, AdamW
class myFinrtuneModel(torch.nn.Module):
def __init__(self,model_name='bert-base-chinese',freeze_bert=False):
super(myFinrtuneModel,self).__init__()
# bert模型
self.bert = BertModel.from_pretrained(model_name)
if freeze_bert:
for p in self.bert.parameters():
p.requires_grad=False
# 定义bert后面要接的网络
self.class_net = torch.nn.Linear(768,1)
# 微调的具体操作
def forward(self,input_ids,attention_masks):
# 输入bert
outputs = self.bert(input_ids, attention_mask=attention_masks)
# 获取bert输出的隐藏层特征
last_hidden_state=outputs.last_hidden_state
# 把token embedding平均得到sentences_embedding
sentences_embeddings=torch.mean(last_hidden_state,dim=1)
sentences_embeddings=sentences_embeddings.squeeze(1)
# 把sentences_embedding输入分类网络
out=self.class_net(sentences_embeddings).squeeze(-1)
return out
Step4:初始化
在这里,主要将模型调整为训练模式,并指定优化器optim,最大迭代次数max_epoch,损失函数loss_function。
Step5:训练、测试、保存
初始化之后,首先对其进行训练
def train(model,train_loader,test_loader,optim,loss_function,max_epoch):
print('-------------- start training ---------------','\n')
step=0
for epoch in range(max_epoch):
print("========= epoch:",epoch,'==============')
for batch in train_loader:
step+=1
# 清空优化器
optim.zero_grad()
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
# 将用例输入模型,计算loss
out=model(input_ids=input_ids,attention_masks=attention_mask)
loss=loss_function(out,labels)
if step%100==0:
print('step ',step,"loss:",format(loss.item(),'.3f'))
# 反向传播
loss.backward()
optim.step()
# 每一次epoch进行一次测试
eval(model=model,test_loader=test_loader)
之后,对模型进行测试
def eval(model,test_loader):
right=0
total=0
for batch in test_loader:
total+=1
input_ids = batch['input_ids'].to(device)
attention_mask = batch['attention_mask'].to(device)
labels = batch['labels'].to(device)
out=torch.sigmoid(model(input_ids=input_ids,attention_masks=attention_mask))
# 二分类
pred_label=0 if out.item()<=0.5 else 1
if pred_label == labels.item():
right+=1
accurcy=format(right/total, '.3f')
print("= accurcy:",accurcy)
print("\n")
此时,如果准确率计算合适,则保存模型,否则继续训练,直到其能够满足我们的需求。
4 基本原理和探讨
4.1 输入层
对于BERT,输入层结构如图所示。
在输入层方面,思路和GPT基本类似,如果输入只有一个句子的话,则直接在句子的前后添加句子的起始标记位和句子的结束符号,在BERT中,起始标记都用“[CLS]”来表示,结束标记符用"[SEP]“表示,对于两个句子的输入情况,除了起始标记和结束标记之外,两个句子间通过”[SEP]"来进行区分。
除了这些之外,BERT还用了两个表示当前是句子A还是句子B的向量来进行表示,对于句子A来说,每一词都会添加一个同样的表示当前句子为句子A的向量,相应的,如果有句子B的话,句子B中的每一个词也都会添加一个表示当前句子为句子B的向量。
当然,和Transformer中一样,为了引入序列中词的位置信息,也用了position embedding。
4.2 Encoder、Decoder
在BERT中,Encoder、Decoder部分完全采用的Transformer中的结构。
4.3 输出层
除了输入层外,输出层也需要进行设计。BERT主要针对四类任务考虑和设计了一些非常易于移植的输出层,这四类任务分别是:单个序列文本分类任务(SST-2, CoLA)、两个序列文本分类任务(MNLI, QQP, QNLI, STS-B, MRPC, RTE)、阅读理解任务(SQuAD)和序列标注任务(CoNLL-2003 NER)。这里面,对于句子或答案选择任务(SWAG等),虽然具体做法和普通的文本分类任务不完全一致,但在BERT中它和其他分类任务都只需要利用"[CLS]"位置的向量,所以才把它一起归入到句子对分类任务中。
对于单序列文本分类任务和序列对的文本分类任务使用框架基本一致,只要输入层按照上面提到的方法做好表示即可,然后这两个分类任务都是利用BERT的Encoder最后一层的第一个时刻“[CLS]”对应的输出作为分类器的输入,文中的解释是这个时刻可以得到输入序列的全局表征,并且因为监督信号从这个位置反馈给模型,因而实际上在fine-tune阶段也可以使得这一表征尽量倾向于全局的表征。当然,即便如此,同样也是可以通过一些很简单易行的办法将其他时刻甚至其他层内的表征拿来用的,个人认为并不需要局限在这一个时刻上的表征。另外,fine-tune阶段,在BERT Encoder基础上,这些分类任务因为只需要接一个全连接层,因此增加的参数只有 H × K H\times K H×K ,其中H是Encoder输出层中隐状态的维度,K是分类类别个数。
对于SQuAD 1.1任务来说,需要 在给定的段落中找到正确答案所在的区间,这段区间通过一个起始符与终止符来进行标记,因此只需要预测输入序列中哪个token所在位置是起始符或终止符即可,因此这个过程只需要维护两个向量,分别是起始符的向量和终止符的向量。
BERT也考虑了如何在序列标注任务上进行finetune,对于序列中的每一个token而言,实际上就是一个分类任务,只不过和前面提到的普通分类任务不一样的地方在于,这里的分类是需要针对序列中的每一个词做分类,参数增加在 H × K H\times K H×K,这里的K是序列标注中标注的种类个数。
4.4 BERT特点
4.4.1 概述
为了更好的了解BERT,不妨先从字面上了解:
不妨首先从字面意思上来理解BERT:
-
Bidirectional:双向的。说明它是一个双向神经网络。这使得输出的结果不仅涉及了过去的信息、现在的信息,甚至还有未来的信息。
-
Encoder:编码。说明它是一个编码器。
-
Representations:表现。它是完成词的表征任务的模型。
-
Transformers:说明了BERT采用了transformer结构。
4.4.2 双向Transformer
如何理解这个双向,不妨结合下面图片来进行理解:
这时,如果想要获取 T 2 T_2 T2下的信息,对于BERT,信息既可以从 E 1 E_1 E1, E 2 E_2 E2过来,走图中的红色路径,也可以从 E n E_n En过来,走图中的蓝色路径。这时,它的输出既包含了过去的信息和现在的信息,也包含了未来的信息。而OpenAI GPT模型中,要获取 T 2 T_2 T2下的信息,信息只能从 E 1 E_1 E1, E 2 E_2 E2过来,并不是双向的。
BERT每个层都是互相连接的。在上图中,ELMo也是一个双向的,但它仅仅是两个模型的简单连接,没有做到BERT中每一层都互相连接。
4.4.3 Mask-LM
在使用双向Transformer时会有一个问题,序列中每一个位置上的信息,已经融合了输入序列上的所有词信息,这使得普通语言模型的目标函数无法直接套用。于是提出了类似于完形填空的方式,即将原来要预测整个句子的输出,改为只预测这个句子中的某个词,并把输入中这个词挖空(使用[MASK]替代)。
不过,BERT针对如何做[MASK],做了以下处理:
-
选取语料中所有词的15%进行随机mask
-
选中的词在80%的概率下被真实mask
-
选中的词在10%的概率下不做mask,而被随机替换成其他一个词
-
选中的词在10%的概率下不做mask,仍然保留原来真实的词
Jacob在论文中并没有做更细致的实验,来证明这些言论的正确性,因而有可能存在其它更好的比例。
4.4.4 Skip-thoughts
BERT利用和借鉴了Skip-thoughts方法中的句子预测问题,来学习句子级别的语义关系,具体做法则是将两个句子组合成一个序列,然后让模型预测这两个句子是否是先后近邻的两个句子,也就是会把"Next Sentence Prediction"问题建模成为一个二分类问题。训练的时候,数据中有50%的情况这两个句子是先后关系,而另外50%的情况下,这两个句子是随机从语料中凑到一起的,也就是不具备先后关系,以此来构造训练数据。
性,因而有可能存在其它更好的比例。