BERT模型总结
说明:这几天整理了BERT模型相关资料,并总结如下,本文所提及BERT代码均来自 transformers。
BERT模型简介
Bert的基本原理和框架
BERT是Bidirectional Encoder Representations from Transformers的首字母缩写,整体是自编码语言模型,使用Maksed LM任务和Next Sentence Prediction任务进行联合。
- 任务一:Masked LM
即在输入一句话的时候随机的选取一些词汇抹去,然后根据剩余的词汇来预测被抹去的几个词分别是什么。
具体来说,BERT会随机选择(抹去)15%的词汇用于预测,对于原句中被抹去的词80%的采用[MASK]替换,10%的用任意词替换,剩下的10%保持不变。 - 任务二:Next Sentence Prediction
即给定一篇文章的两句话判断第二句话在文本中是否紧跟在第一句话后,在实际训练过程中从语料中随机选择50%正确句对和50%的错误句对。
BERT基本框架简图如下:
在这个框架简图中对于每一个输入的[Token]在每一层都会有一个Transformer的Encode与之对应,非常容易让人误解为BERT中每一层都会有512个transformer(BERT限制句子最大长度为512),以及BERT在训练时会将每个句子padding到512。但其实对于Transformer的Attention模块来说是,在代码中表现为和句子的长度无关,并不会将所有的句子padding到512。(关于这块内容在后续Transformer在BERT中的应用里会有详细说明)
BERT的输入和输出
BERT模型的输入有三种,字向量、文本向量和位置向量
- 字向量:该向量可以随机初始化,也可以利用word2vector等算法进行预训练以作为初始值
- 文本向量:该向量的取值在模型训练过程中自动学习,用于刻画全局语义信息,并与单词/字的语义信息相互融合。
- 位置向量:由于出现在文本不同位置的字/词所携带的语义信息存在差异,因此,BERT模型对于不同位置的字/词分别附加一个不同的向量以作区分,但是与Transformer不同之处在于这里的位置向量不是由三角函数给出,而是由模型在训练过程中自动学习。
BERT模型Embedding层如下:
class BertEmbeddings(nn.Module):
"""Construct the embeddings from word, position and token_type embeddings.
"""
def __init__(self, config):
super().__init__()
self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
# self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load
# any TensorFlow checkpoint file
self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
值得注意的是Bert模型限制了输入句子的最大长度(512),所以 self.position_embeddings
的输入为从1到N的递增序列,N为句子长度且小于512.
self.token_type_embeddings
的输入为句子标记,是一个取值为0或者1的向量
Transformer在BERT中的应用
BERT模型使用了Transformer的Encoder模块,原论文中作者分别用了12层和24层Transformer Encoder组装了两套BERT模型,分别是:
BERT_base: L=12, H=768, A=12, Total Parameters=110M
BERT_large: L=24, H=1024, A=16, Total Parameters=340M
其中层的数量(即Transformer Encoder模块的数量)为L,隐藏层的维度为H,自注意力头的个数为A,过滤器/前馈网络(Transformer Encoder端的feed-forward层)的维度为4H
Transformer的Encoder端结构如下图所示:
以下将详细介绍Transformer Encode端的各个子模块
-
多头self-attention模块
self-attention模块如下图所示:
Attention的输出为
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
其中K,Q,V是由X通过三个参数矩阵 W Q , W K , W V W^Q,W^K,W^V WQ,WK,WV相乘得到的。
而多头的self-attention则由多组 W i Q , W i K , W i V W{^Q_i},W{^K_i},W{^V_i} WiQ,WiK,WiV相乘得到在讲结果拼接在一起送入一个全连接层。具体结构如下:
对应公式如下:
M u l t i H e a d ( Q , K , V ) = C o n c a t ( h e a d 1 , h e a d 2 , … … h e a d n ) W O w h e r e h e a d i = A t t e n t i o n ( X W i Q , X W i K , X W i V ) MultiHead(Q,K,V)=Concat(head_1,head_2,……head_n)W^O\\ where\quad head_i=Attention(XW{^Q_i},XW{^K_i},XW{^V_i}) MultiHead(Q,K,V)=Concat(head1,head2,……headn)WOwhereheadi=Attention(XWiQ,XWiK,XWiV)
其中 W O W^O WO是全连接层的参数矩阵。 -
前馈神经网络模块
前馈神经网络模块由两个线性变换组成,中间有一个relu
激活函数,即:
F F N ( X ) = m a x ( 0 , X W 1 + b 1 ) W 2 + b 2 FFN(X)=max(0,XW_1+b_1)W_2+b_2 FFN(X)=max(0,XW1+b1)W2+b2
值得注意的是在BERT中激活函数使用的是gelu
而不是relu
-
多头自注意力机制的并行运算在代码中如何体现
在代码中自然不会对于每一个子空间设置参数矩阵 W i Q , W i K , W i V W{^Q_i},W{^K_i},W{^V_i} WiQ,WiK,WiV,而是使用三个参数矩阵 W Q , W K , W V W^Q,W^K,W^V WQ,WK,WV,然后在子空间运算时对于参数矩阵进行切分,从而达到并行运算的目的,具体代码如下(为了不显得累赘我对代码做了部分删除):class BertSelfAttention(nn.Module): def __init__(self, config): self.query = nn.Linear(config.hidden_size, self.all_head_size) self.key = nn.Linear(config.hidden_size, self.all_head_size) self.value = nn.Linear(config.hidden_size, self.all_head_size) def transpose_for_scores(self, x): # 模型在这里将x的最后一个维度(x的前两个维度大小为batch_size和sentence_length) # 按照num_attention_heads和attention_head_size进行了切分, # 然后交换第二个维度和第三个维度,这样就可以在子空间sentence_length*attention_head_size #上进行attention运算,等到运算结束后再将其换回来,并合并最后两个维度 new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) x = x.view(*new_x_shape) return x.permute(0, 2, 1, 3) def forward(self, hidden_states, attention_mask=None, head_mask=None, encoder_hidden_states=None, encoder_attention_mask=None,): mixed_query_layer = self.query(hidden_states) mixed_key_layer = self.key(hidden_states) mixed_value_layer = self.value(hidden_states) query_layer = self.transpose_for_scores(mixed_query_layer) key_layer = self.transpose_for_scores(mixed_key_layer) value_layer = self.transpose_for_scores(mixed_value_layer) attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) attention_scores = attention_scores / math.sqrt(self.attention_head_size) attention_probs = nn.Softmax(dim=-1)(attention_scores) context_layer = torch.matmul(attention_probs, value_layer) context_layer = context_layer.permute(0, 2, 1, 3).contiguous() new_context_layer_shape = context_layer.size()[:-2] + (self.all_head_size,) context_layer = context_layer.view(*new_context_layer_shape) return context_layer
-
BERT模型代码表现为和句子无关的论证
在使用BERT模型时不难发现,我们似乎不必关心是否要将句子padding,或者说padding本身只是针对一个batch而言,而这样做的目的则是为了能将其转化为一个张量。是因为BERT模型内部会对输入的batch进行padding吗?其实并不是,BERT模型在代码上表现为和句子长度无关,准确来说,对于Attention机制,在整个运算过程中在代码上表现为和句子长度无关。输入的所有向量合并为矩阵形式,则所有query, key, value向量也可以合并为矩阵形式表示则可以得到下图关于 Q , K , V Q,K,V Q,K,V的计算
很明显的一点在于,在计算 K , Q , V K,Q,V K,Q,V三个矩阵时我们并不关心句子长度( X X X的一行表示一个词/字),每一个 X i X_i Xi都与同一个参数矩阵相乘,在整个乘积过程中并不需要关心句子的长度,无非每次生成的 K , Q , V K,Q,V K,Q,V的维度不同而已,但这并不影响我对于参数矩阵的定义。因为同样的原因,在代码对于 W K , W Q , W V W^K,W^Q,W^V WK,WQ,WV的定义只需要使用Linear
即可:class BertSelfAttention(nn.Module): def __init__(self, config): self.query = nn.Linear(config.hidden_size, self.all_head_size) self.key = nn.Linear(config.hidden_size, self.all_head_size) self.value = nn.Linear(config.hidden_size, self.all_head_size)
但这和Attention机制能够长距离抽取句子/词特征并不矛盾。因为就Attention机制而言重要不在于如何生成 K , Q , V K,Q,V K,Q,V三个矩阵,而是如何计算词之间的Attention。
A t t e n t i o n ( Q , K , V ) = s o f t m a x ( Q K T d k ) V Attention(Q,K,V)=softmax(\frac{QK^T}{\sqrt{d_k}})V Attention(Q,K,V)=softmax(dkQKT)V
上述公式中很显然的一点在于句子中每一个单词所对应的 Q Q Q都和其它单词(包括自己)所对应的 K K K进行了充分的乘积运算。
同时需要说明的是,BERT模型的每一层只有一个Transformer Encoder,只是在Encoder层会对句子中所有的词汇都进行运算,Encoder的个数并不会随着句子长度的变化而改变,从这一点上来说BERT并没有对Transformer Encoder的代码本身做过多的改变(比较好奇BERT和OpenAI GPT这一部分的区别在代码上的具体表现,之后有时间再来探究)。
BERT代码浅析
概述
以下部分主要针对huggingface/transformers中的源码进行一些解读
先来看一段简单的BERT预训练模型调用的代码
import torch
from transformers import BertModel, BertTokenizer
# 这里我们调用bert-base模型,同时模型的词典经过小写处理
model_name = 'bert-base-uncased'
# 读取模型对应的tokenizer
tokenizer = BertTokenizer.from_pretrained(model_name)
# 载入模型
model = BertModel.from_pretrained(model_name)
# 输入文本
input_text = "Here is some text to encode"
# 通过tokenizer把文本变成 token_id
input_ids = tokenizer.encode(input_text, add_special_tokens=True)
# input_ids: [101, 2182, 2003, 2070, 3793, 2000, 4372, 16044, 102]
input_ids = torch.tensor([input_ids])
# 获得BERT模型最后一个隐层结果
with torch.no_grad():
last_hidden_states = model(input_ids)[0]
BertModel.from_pretrained
会自动加载已经训练好的BERT模型,同时在保存模型时只保存参数,所以在加载模型参数之前应当先实例化模型,所以BertModel.from_pretrained
中代码如下(为了凸显代码结构删除了大量细节):
def from_pretrained(cls, pretrained_model_name_or_path, *model_args, **kwargs):
# Load model
# Instantiate model.
model = cls(config, *model_args, **model_kwargs)
try:
state_dict = torch.load(resolved_archive_file, map_location="cpu")
except Exception:
pass
# PyTorch's `_load_from_state_dict` does not copy parameters in a module's descendants
# so we need to apply the function recursively.
def load(module: nn.Module, prefix=""):
local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {})
module._load_from_state_dict(
state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs,
)
for name, child in module._modules.items():
if child is not None:
load(child, prefix + name + ".")
load(model_to_load, prefix=start_prefix)
model.tie_weights() # make sure token embedding weights are still tied if needed
# Set model in evaluation mode to deactivate DropOut modules by default
model.eval()
return model
不过关于以下代码及其所给注释尚不能完全理解,需要后续再深入研究
# PyTorch's `_load_from_state_dict` does not copy parameters in a module's descendants
# so we need to apply the function recursively.
def load(module: nn.Module, prefix=""):
local_metadata = {} if metadata is None else metadata.get(prefix[:-1], {})
module._load_from_state_dict(
state_dict, prefix, local_metadata, True, missing_keys, unexpected_keys, error_msgs,
)
for name, child in module._modules.items():
if child is not None:
load(child, prefix + name + ".")
而对于BERT模型的实例化部分以及BERT模型本身我们应当关注BertModel
这个类,位于src/transformer/modeling_bert.py
(为了凸显代码结构删除了大量细节)
class BertModel(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.config = config
self.embeddings = BertEmbeddings(config)
self.encoder = BertEncoder(config)
self.pooler = BertPooler(config)
self.init_weights()
def forward():
embedding_output = self.embeddings(
input_ids=input_ids, position_ids=position_ids, ken_type_ids=token_type_ids, inputs_embeds=inputs_embeds
)
encoder_outputs = self.encoder(
embedding_output, attention_mask=extended_attention_mask,
head_mask=head_mask, encoder_hidden_states=encoder_hidden_states,
encoder_attention_mask=encoder_extended_attention_mask,
)
sequence_output = encoder_outputs[0]
pooled_output = self.pooler(sequence_output)
outputs = (sequence_output, pooled_output,) + encoder_outputs[
1:
] # add hidden_states and attentions if they are here
return outputs # sequence_output, pooled_output, (hidden_states), (attentions)
由上面BertModel
类的定义可以看出整个BERT模型分为三个模块,BertEmbedding
, BertEncoder
和BertPooler
,forward时顺序经过三个模块,然后输出output。
下面依次介绍这三个模块:
BertEmbedding模块
模块定义如下(为了凸显代码结构删除了大量细节):
class BertEmbeddings(nn.Module):
def __init__(self, config):
super().__init__()
self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx=config.pad_token_id)
self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None):
inputs_embeds = self.word_embeddings(input_ids)
position_embeddings = self.position_embeddings(position_ids)
token_type_embeddings = self.token_type_embeddings(token_type_ids)
embeddings = inputs_embeds + position_embeddings + token_type_embeddings
embeddings = self.LayerNorm(embeddings)
embeddings = self.dropout(embeddings)
return embeddings
在BertEmbedding
类中可以看到embedding由三种embedding相加得到,然后经过LayerNorm
和dropout
得到。
BertEncoder模块
模块定义如下(为了凸显代码结构删除了大量细节):
class BertEncoder(nn.Module):
def __init__(self, config):
super().__init__()
self.output_attentions = config.output_attentions
self.output_hidden_states = config.output_hidden_states
self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
def forward(self, hidden_states, attention_mask=None, head_mask=None,encoder_hidden_states=None, encoder_attention_mask=None,):
all_hidden_states = ()
all_attentions = ()
for i, layer_module in enumerate(self.layer):
if self.output_hidden_states:
all_hidden_states = all_hidden_states + (hidden_states,)
layer_outputs = layer_module(
hidden_states, attention_mask, head_mask[i], encoder_hidden_states, encoder_attention_mask
)
hidden_states = layer_outputs[0]
outputs = (hidden_states,)
return outputs
这一层的主体可以看到是一个for循环,每一层for循环构造一层encoder,所以在bade模型中self.layer
=12,整段代码的核心点在于
python self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
而BertLayer
的forward顺序将输入经过BertAttention、BertIntermediate、BertOutput
最核心的是BertAttention
模块
class BertLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.attention = BertAttention(config)
self.intermediate = BertIntermediate(config)
self.output = BertOutput(config)
BertAttention
模块定义如下:
class BertAttention(nn.Module):
def __init__(self, config):
super().__init__()
self.self = BertSelfAttention(config)
self.output = BertSelfOutput(config)
def forward(self, hidden_states, attention_mask=None, head_mask=None, encoder_hidden_states=None, encoder_attention_mask=None,):
self_outputs = self.self(
hidden_states, attention_mask, head_mask, encoder_hidden_states, encoder_attention_mask
)
attention_output = self.output(self_outputs[0], hidden_states)
outputs = (attention_output,) + self_outputs[1:] # add attentions if we output them
return outputs
而这里就正式到了BERT最核心的基础构架,也就是Transformer的Encoder模块,BertSelfAttention
是多头自注意力模块,BertSelfOutput
对应的是Add&Norm。关于多头自注意力模块请回顾之前多头自注意力机制的并行运算在代码中如何体现
这一小结,这里不做过多赘述,Add&Norm在编码上相对简单不做过多描述
BertIntermediate
模块定义入下:
class BertIntermediate(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
if isinstance(config.hidden_act, str):
self.intermediate_act_fn = ACT2FN[config.hidden_act]
else:
self.intermediate_act_fn = config.hidden_act
def forward(self, hidden_states):
hidden_states = self.dense(hidden_states)
hidden_states = self.intermediate_act_fn(hidden_states)
return hidden_states
这里的中间层就是Transformer Encoder模块里面的Feed Forward层,值得注意的是这里的激活函数在默认设置中就是gelu.
BertOutput
和BertSelfOutput
相同,都是Add&Norm。
BertPooler模块
模块定义如下:
class BertPooler(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.activation = nn.Tanh()
def forward(self, hidden_states):
# We "pool" the model by simply taking the hidden state corresponding
# to the first token.
first_token_tensor = hidden_states[:, 0]
pooled_output = self.dense(first_token_tensor)
pooled_output = self.activation(pooled_output)
return pooled_output
再结合BertModel
中对于BertPooler
的使用
sequence_output = encoder_outputs[0]
pooled_output = self.pooler(sequence_output)
可以看出pooler功能取到的是最后一层layer输出的第一个[Token]的embedding,对应到输入就是[CLS]即句子向量。一般说来,如果需要用BERT做句子级的任务,可以使用pooled_output结果做baseline;进一步的微调可以使用last_hidden_state的结果。
上述便为BertModel的基本实现,最后给出一张结构图来进一步说明(该结构图来自bert模型简介、transformers中bert模型源码阅读、分类任务实战和难点总结)