BERT模型解读与简单任务实现(论文复现)
BERT模型解读与简单任务实现(论文复现)
本文所涉及所有资源均在传知代码平台可获取
概述
相关背景
语言模型:语言模型是指对于任意的词序列,它能够计算出这个序列是一句话的概率。
预训练:预训练是一种迁移学习的概念,指的是用海量的数据来训练一个泛化能力很强的模型
微调:微调(Fine-tuning)是指在预训练模型的基础上,针对特定任务或数据领域,对部分或全部模型参数进行进一步的训练和调整
Transformer:
BERT是基于Transformer实现的,BERT中包含很多Transformer模块,其取得成功的一个关键因素是Transformer的强大作用。BERT仅有Encoder部分,因为它并不是生成式模型。Transformer个模块通过自注意力机制实现快速并行,改进了RNN最被人诟病的训练慢的缺点,并且可以增加到非常深的深度,充分发掘DNN模型的特性,提升模型准确率。
Transformer首先对每个句子进行词向量化,进行编码,再添加某个词蕴含的位置信息,生成一个向量。而后通过Attention算法,生成一个新向量,这个新向量不仅包含了词的含义,词中句子中的位置信息,也包含了该词和句子中的每个单词含义之间的关系和价值信息。这种方法突破了时序序列的屏障,使得Transformer得到了广泛的应用。
BERT的优势
- 作为一种预训练模型,在特定场景使用时不需要用大量的语料来进行训练,节约时间效率高效,泛化能力较强。
- Bert是一种端到端(end-to-end)的模型,不需要我们调整网络结构,只需要在最后加上特定于下游任务的输出层。
- 基于Transformer,可以实现快速并行,也可以增加到非常深的深度,充分发掘DNN模型的特性,提升模型准确率。
- 和ELMO,GPT等其他预训练模型相比,BERT是一种双向的模型,结合上下文来进行训练,具有更好的性能。
BERT和传统nlp相比的特点
1.更好的语义理解能力
传统的自然语言处理工具只能从字面意义上进行文本分析,无法理解句子的含义和上下文。而BERT模型是双向的,可以同时考虑句子左右两侧的上下文信息,从而更好地理解句子的含义和语境。因此,在对话系统、文本分类等领域中BERT模型的表现更加优秀
2.更好的文本预训练能力
BERT是基于预训练的模型,使用了大型无标注语料库进行训练。由于BERT训练时使用了大量的语料库。因此具有更好的泛化能力和适应性,可以适应不同的自然语言处理任务。
3.可拓展性强
BERT采用Transformer结构,使得模型可以轻松地进行拓展。可以通过增加层数、增加训练数据等方式来提高模型的性能。因此,BERT模型在对新领域的应用中具有很大的潜力。
4.更好的效果
针对一些自然语言处理领域的任务,BERT模型的表现要优于其他传统的自然语言处理模型。例如,BERT在文本分类任务中表现出的效果比传统的卷积网络和循环神经网络要好,在当前的文本分类领域中有着广泛的应用
BERT的应用领域
BERT作为一个预训练模型,能够通过适当的数据集进行微调,使得它能够胜任自然语言处理领域的多种任务,比如情感分析、摘要、对话等任务
模型架构
BERT的模型架构是基于多层双向Transformer编码器。具体的,Google提供了一大一小两个BERT模型:
BERT_Small(L=12,H=768,A=12,总参数=110M)
BERT_Large(L=24,H=1024,A=16,总参数=340M)
输入输出表示
为了使BERT能够处理各种下游任务,输入表示能够明确表示单个句子和一对句子(例如,⟨question,answer⟩)在一个标记序列中。
BERT的输入由三部分组成:
Token Embeddings:使用具有30,000个标记词汇表的WordPiece嵌入。每个序列的第一个标记始终是一个特殊的分类标记([CLS])。对应于此标记的最终隐藏状态用作分类任务的聚合序列表示。
Segment Embeddings:用于区分两个句子。通过两种方式区分句子:1.用一个特殊标记([SEP])将它们分开。2.为每个标记添加一个学习的嵌入,指示它属于句子A还是句子B。
Position Embeddings:位置编码,transformer没有捕捉位置信息的能力,所以需要额外的位置编码,这里没有使用transformer论文中的正弦位置编码, 而是采用了learned positional embeddings。
将BERT的输入表示可视化如下:
BERT预训练任务
使用两个无监督任务来预训练BERT,包括MLM和NSP。
MLM掩码语言模型
直观来看,深度双向语言模型当然比单向的从左到右或者从右到左模型更有效。但不幸的是,标准条件语言模型只能从左到右或从右到左进行训练,因为双向条件将允许每个单词在多层上下文中间接 “看到自己”。
为了训练一个深度双向表示,Google学者简单地随机掩盖一定比例的输入标记,然后预测这些被掩盖的标记,这个过程称为“掩码语言模型”(MLM),也就是类似于完形填空任务。
但这种办法存在两个问题:
1.在预训练和微调之间导致了不匹配,因为[MASK]标记在微调期间不会出现。为了缓解这一问题,他们并不总是用实际的[MASK]标记替换“被掩盖”的单词,而是在训练时随机选择15%的标记位置进行预测。如果选择第i个标记,将第i个标记以以下方式替换:(1) 80%的概率 用[MASK]标记替换 (2) 10%的概率用随机标 记替换 (3) 10%的概率保持不变。
2.每个batch只预测15%的tokens,需要更多的轮次才能收敛。
NSP下句预测
许多重要的下游任务,例如问答(QA)和自然语言推断(NLI),都是基于理解两个文本句子之间的关系的。这个没有办法直接由语言模型捕捉,所以增加了一个next sentence prediction(NSP)任务。具体的,对于训练语料中一对句子A和B,B有一半的概率是A的下一句,一半的概率是随机的句子。
预训练过程
预训练过程在很大程度上遵循现有关于语言模型预训练的文献。对于预训练语料库,BERT使用了BooksCorpus(800M字)和英文维基百科(2,500M字)。对于维基百科,仅提取文本段落,忽略列表、表格和标题。使用文档级语料库而不是乱序句子级语料库是至关重要的,以便提取长连续序列
微调过程
在不同任务上微调BERT的示意图如图所示。任务特定模型是通过将BERT与一个额外的输出层结合形成的,因此只需要从头开始学习少量参数。在这些任务中,a和b是序列级任务,而c和d是标记级任务。在图中,E代表输入嵌入,Ti代表标记 i的上下文表示,[CLS]是用于分类输出的特殊符号,[SEP]是用于分隔非连续标记序列的特殊符号。
核心逻辑
代码主要分为三部分。
1.dataset,主要负责数据的预处理。比如如何对语料做mask,如何加入CLS、SEP符号等等。
2.model,主要包括bert模型架构,两个预训练任务的实现。
3.trainer,主要实现了预训练的逻辑。
核心内容在model部分。
model部分的核心在于BERT、BERTEmbedding和transformer着三部分
BERT
下面的类实现了BERT的模型,模型的输入通过BERT Embedding层和transformer组建,实现模型架构。
class BERT(nn.Module)
def __init__(self, vocab_size, hidden=768, n_layers=12, attn_heads=12, dropout=0.1):
super().__init__()
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
下面实现的是transformer块,通过多头注意力层、前馈网络层(两层全连接层)、两层残差链接和dropout层来实现
class TransformerBlock(nn.Module):
"""
Bidirectional Encoder = Transformer (self-attention)
Transformer = MultiHead_Attention + Feed_Forward with sublayer connection
"""
def __init__(self, hidden, attn_heads, feed_forward_hidden, dropout):
"""
:param hidden: hidden size of transformer
:param attn_heads: head sizes of multi-head attention
:param feed_forward_hidden: feed_forward_hidden, usually 4*hidden_size
:param dropout: dropout rate
"""
super().__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)
BERT Embedding
下面的类实现了BERT Embedding层,BERT模型的数据输入的三种编码加和,再经过一个dropout层。
class BERTEmbedding(nn.Module):
def __init__(self, vocab_size, embed_size, dropout=0.1):
super().__init__()
self.token = TokenEmbedding(vocab_size=vocab_size, embed_size=embed_size)
self.position = PositionalEmbedding(d_model=self.token.embedding_dim)
self.segment = SegmentEmbedding(embed_size=self.token.embedding_dim)
self.dropout = nn.Dropout(p=dropout)
self.embed_size = embed_size
def forward(self, sequence, segment_label):
x = self.token(sequence) + self.position(sequence) + self.segment(segment_label)
return self.dropout(x)
演示效果
经过训练后的BERT模型,能够根据输入的句子计算他们的编码值。以及,可以区分两个句子的编码,对指定的词进行self-Attention操作,例如:
输入内容:
bert_model = BertModel.from_pretrained(MODEL_PATH, config = model_config)
print(tokenizer.encode('我不喜欢你'))
sen_code = tokenizer.encode_plus('我不喜欢这世界','我只喜欢你')
print(sen_code)
输出内容:
[101, 2769, 679, 1599, 3614, 872, 102]
{'input_ids': [101, 2769, 679, 1599, 3614, 6821, 686, 4518, 102, 2769, 1372, 1599, 3614, 872, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
同时可以实现论文中提到的两个任务。
MLM任务:
import numpy as np
import torch
from transformers import BertTokenizer, BertConfig, BertForMaskedLM, BertForNextSentencePrediction
from transformers import BertModel
model_name = 'xxxxx' #指定需下载的预训练模型参数
#任务一:MLM
import numpy as np
import torch
from transformers import logging
logging.set_verbosity_error()
from transformers import BertTokenizer, BertConfig, BertForMaskedLM, BertForNextSentencePrediction
from transformers import BertModel
model_name = 'xxxx' #指定需下载的预训练模型参数
#任务一:MLM
samples = ['[CLS] Where is the capital of China? [SEP] Beijing is the capital of [MASK] . [SEP]']
tokenizer = BertTokenizer.from_pretrained(model_name)
tokenized_text = [tokenizer.tokenize(i) for i in samples]
input_ids = [tokenizer.convert_tokens_to_ids(i) for i in tokenized_text]
input_ids = torch.LongTensor(input_ids)
model = BertForMaskedLM.from_pretrained(model_name, cache_dir=model_name)
model.eval()
outputs = model(input_ids)
prediction_scores = outputs[0]
sample = prediction_scores[0].detach().numpy()
pred = np.argmax(sample, axis=1)
print(pred)
输出pred代表每个位置最大概率的字符索引:
[1012 2073 2003 1996 3007 1997 2859 1029 1012 7211 2003 1996 3007 1997
2859 1012 1012]
NSP任务:
from transformers import BertTokenizer, BertForSequenceClassification
import torch
path='xxxx'
tokenizer = BertTokenizer.from_pretrained(path)
model = BertForSequenceClassification.from_pretrained(path)
TEMPERATURE = 1
MERGE_RATIO = 0.5
def is_nextsent(sent, next_sent):
encoding = tokenizer(sent, next_sent, return_tensors="pt",truncation=True, padding=False)
with torch.no_grad():
labels = torch.tensor([1]).unsqueeze(0)
outputs = model(**encoding, labels=labels)
logits = outputs.logits
probs = torch.softmax(logits/TEMPERATURE, dim=1)
next_sentence_prob = probs[:, 0].item()
print(next_sentence_prob)
if next_sentence_prob <= MERGE_RATIO:
return False
else:
return True
sen1="今天天气怎么样"
sen2="今天天气很好"
sen3="小明今年多大了"
sen4="小明天天吃饭"
print(is_nextsent(sen1,sen2))
print(is_nextsent(sen3,sen4))
一个输出例子,说明句子1和句子2之间是上下句关系,而句子3和句子4之间不是上下句关系,其中小数表示模型预测的两个句子是上下文关系的概率,这里概率越大,说明两个模型越可能是上下句关系:
0.5294263362884521
True
0.4861791133880615
False
使用方式
下载模型,进入huggingface官网搜索框输入bert-base-chinese,下载需要的文件。在本地新建一个文件夹,把上面文件下载到这个目录下面,注意:不能改变文件名和后缀;如果无法登录huggingface,可以前往附件查看,附件中提供了模型的下载链接。本地加载模型,使用如下代码本地加载模型,即可进行模型加载使用等功能。
path='xxxx'
tokenizer = BertTokenizer.from_pretrained(path)
model = BertForSequenceClassification.from_pretrained(path)
文章代码资源点击附件获取