bert模型简介、transformers中bert模型源码阅读、分类任务实战和难点总结:https://blog.csdn.net/HUSTHY/article/details/105882989
目录
一、bert模型简介
bert与训练的流程:
bert模型的输入
二、huggingface的bert源码浅析
bert提取文本词向量
BertModel代码阅读
BertEmbedding子模型
BertEncoder
BertAttention
BertIntermediate
BertOutput(config)
BertPooler()
三、Bert文本分类任务实战
四、Bert模型难点总结
写在最前面,这篇博客篇幅有点长,原因是贴的代码和图有点多,感兴趣的可以坚持读下去!
2018年bert模型被谷歌提出,它在NLP的11项任务中取得了state of the art 的结果。bert模型是由很多层transformer结构堆叠而成,这里简单看看一下transformer的结构,上一张经典的图片,如下:
可以看到transformer是由encoder和decoder模块构成,而bert模型则是利用了transformer的encoder模块。最轻量的bert买模型是由12层transformer,12头注意力,768维的hidden state,在论文中的结构简图如下:
这样的双向transformer的结构,在NLP的大部分任务中取得了很好的效果,具备较强的泛化能力。由于使用了海量的语料进行了训练,bert模型可以使用pretrain——fine-tune这种方式来进行各类NLP任务。
这个过程包括两个任务,一个是Masked Language Model(遮掩语言模型),另外一个是Next Sentence Prediction(下一句预测)。
Masked Language Model(遮掩语言模型)可以理解为是做完型填空,把语料中15%的词遮掩掉,来学习词和词之间的一些规律;
Next Sentence Prediction就是学习语料中上下文中2个句子之间的关系规律。
通过这2个阶段任务的学习,bert就会把文本的语法和语义信息学习到。bert模型中的self-attention机制可以使用文本其他的词来增强目标词的语义表示,这也是bert模型吊打其他模型的一个关键原因。
bert模型的输入可以是一个句子或者句子对,代码层面来说,就是输入了句子或者句子对对应的3个向量。它们分别是token embedding,segment embedding和position embedding,具体的含义:
token embedding:句子的词向量
segment embedding:是那个句子的0和1
position embedding:位置向量,指明每个字在句中的位置。
关于position embedding这里有两种求法,一种是有相应的三角函数公式得出的,这种是绝对向量;还有一种是学习得到的,这种是相对向量。具体形式如下:
关于bert模型的使用,我主要是使用huggingface的transformer库来调用bert和使用——一般是直接用来bert来获取词向量。这里就bert的使用和huggingface中的源码进行一些解读。
首先看一段简单的代码,使用huggingface的transformers(其实就是实现的bert)来提取句——我爱武汉!我爱中国!——的向量。代码如下:bert提取文本词向量 首先看一段简单的代码,使用huggingface的transformers(其实就是实现的bert)来提取句——我爱武汉!我爱中国!——的向量。代码如下:
from transformers import BertModel,BertTokenizer,BertConfig
import torch
config = BertConfig.from_pretrained('pretrain_model/chinese-bert-wwm')#第一步加载模型配置文件
bertmodel = BertModel.from_pretrained('pretrain_model/chinese-bert-wwm',config=config)#第二步初始化模型,并加载权重
# print('***************************bertmodel***************************')
tokenizer = BertTokenizer.from_pretrained('pretrain_model/chinese-bert-wwm')#第三步加载tokenizer
text1 = '我爱武汉!我爱中国!'
tokeniz_text1 = tokenizer.tokenize(text1)
# print(tokeniz_text1)
# print('tokeniz_text1:',len(tokeniz_text1))
indexed_tokens_1 = tokenizer.convert_tokens_to_ids(tokeniz_text1)
print('len(indexed_tokens_1):',len(indexed_tokens_1))
print(indexed_tokens_1)
input_ids_1 = indexed_tokens_1
# print(indexed_tokens_1)
# print('indexed_tokens_1:',len(indexed_tokens_1))
segments_ids_1 = [0]*len(input_ids_1)#其实这个输入可以不用的,因为是单句的原因
input_masks_1 = [1]*len(input_ids_1)#其实这个输入可以不用的,因为是单句的原因
input_ids_1_tensor = torch.tensor([input_ids_1])
vector1,pooler1 = bertmodel(input_ids_1_tensor)#应该是输入3个向量的,但是单句情况下,它自会自己做判断,然后自动生成对应的segments_ids和input_masks向量
#这里的输出最后一层的last_hidden_state和最后一层首个token的hidden-state
text2 = '[CLS]我爱武汉!我爱中国![SEP]'
tokeniz_text2 = tokenizer.tokenize(text2)
indexed_tokens_2 = tokenizer.convert_tokens_to_ids(tokeniz_text2)
input_ids_2 = indexed_tokens_2
segments_ids_2 = [0]*len(input_ids_2)#其实这个输入可以不用的,因为是单句的原因
input_masks_2 = [1]*len(input_ids_2)#其实这个输入可以不用的,因为是单句的原因
input_ids_2_tensor = torch.tensor([input_ids_2])
vector2,pooler2 = bertmodel(input_ids_2_tensor)
print('pooler2:',pooler2)
print('vector2[:,0:1,:]:',vector2[:,0:1,:])
text1_encode = tokenizer.encode(text1,add_special_tokens=True)
print('len(text1_encode):',len(text1_encode))
print('text1_encode:',text1_encode)
以上代码是基于pytorch来实现的,同时应用到了transoformers库!可以看到bert模型的使用非常简单!
第一步,初始化bert模型和加载权重。这个步骤中,首先加载配置文件、然后加载bert模型和载入权重。
第二步,对输入文本做词表映射,形成初始词向量。
第三步,输入喂入bert模型中得到输入文本的结果向量。
文中是bert模型的输入我这里只给出了一个那就是input_ids,另外的2个没有给出。这里的原因就是这里是单个句子,模型内部可以对另外2个输入做自动添加的处理——并不是没有,这点要注意到。
这里有个疑问因为bert的输入文本得添加一个[cls]特殊字符,我认为最后的输出lsat_hidden_state中的lsat_hidden_state[:,0:1,:]应该和pooler结果是一样的,可是这里是不一样的,有点理解的偏差,不知道为什么。
通过上文中的代码,大致可以知道怎么调用一些API来创建bert模型和应用它。那么huggingface中是怎么实现BertModel的这个也是比较重要的,这里我们就好好阅读以下其中关于BertModel实现的代码。看一张transformers项目文件结构图:
这么面封装了很多模型的构建,我们主要是阅读modeling_bert.py文件,它在里面详细的展示了如何构建一个Bert模型的:
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 get_input_embeddings(self):
return self.embeddings.word_embeddings
def set_input_embeddings(self, value):
self.embeddings.word_embeddings = value
def _prune_heads(self, heads_to_prune):
""" Prunes heads of the model.
heads_to_prune: dict of {layer_num: list of heads to prune in this layer}
See base class PreTrainedModel
"""
for layer, heads in heads_to_prune.items():
self.encoder.layer[layer].attention.prune_heads(heads)
@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,
encoder_hidden_states=None,
encoder_attention_mask=None,
):
r""".......
"""
if input_ids is not None and inputs_embeds is not None:
raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time")
elif input_ids is not None:
input_shape = input_ids.size()
elif inputs_embeds is not None:
input_shape = inputs_embeds.size()[:-1]
else:
raise ValueError("You have to specify either input_ids or inputs_embeds")
device = input_ids.device if input_ids is not None else inputs_embeds.device
if attention_mask is None:
attention_mask = torch.ones(input_shape, device=device)
if token_type_ids is None:
token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
# We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
# ourselves in which case we just need to make it broadcastable to all heads.
extended_attention_mask: torch.Tensor = self.get_extended_attention_mask(
attention_mask, input_shape, self.device
)
# If a 2D ou 3D attention mask is provided for the cross-attention
# we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
if self.config.is_decoder and encoder_hidden_states is not None:
encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
if encoder_attention_mask is None:
encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
encoder_extended_attention_mask = self.invert_attention_mask(encoder_attention_mask)
else:
encoder_extended_attention_mask = None
# Prepare head mask if needed
# 1.0 in head_mask indicate we keep the head
# attention_probs has shape bsz x n_heads x N x N
# input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
# and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers)
embedding_output = self.embeddings(
input_ids=input_ids, position_ids=position_ids, token_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的全部代码,可以看到在BertModel类中,首先__init__()函数中定义了模型的基本模块,然后在forward()函数里面使用这些结构模块具体实现了Bert的逻辑。
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()
init()函数中定义的模型模块主要是3个,分别是BertEmbedding、BertEncoder和BertPooler。然后在forward(),输入顺序的经过这3个模块的处理就得到了我们要的结果——对应文本的bert向量。
下面来阅读forward():
if input_ids is not None and inputs_embeds is not None:
raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time")
elif input_ids is not None:
input_shape = input_ids.size()
elif inputs_embeds is not None:
input_shape = inputs_embeds.size()[:-1]
else:
raise ValueError("You have to specify either input_ids or inputs_embeds")
device = input_ids.device if input_ids is not None else inputs_embeds.device
if attention_mask is None:
attention_mask = torch.ones(input_shape, device=device)
if token_type_ids is None:
token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
# We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
# ourselves in which case we just need to make it broadcastable to all heads.
if attention_mask.dim() == 3:
extended_attention_mask = attention_mask[:, None, :, :]
elif attention_mask.dim() == 2:
# Provided a padding mask of dimensions [batch_size, seq_length]
# - if the model is a decoder, apply a causal mask in addition to the padding mask
# - if the model is an encoder, make the mask broadcastable to [batch_size, num_heads, seq_length, seq_length]
if self.config.is_decoder:
batch_size, seq_length = input_shape
seq_ids = torch.arange(seq_length, device=device)
causal_mask = seq_ids[None, None, :].repeat(batch_size, seq_length, 1) <= seq_ids[None, :, None]
causal_mask = causal_mask.to(
attention_mask.dtype
) # causal and attention masks must have same type with pytorch version < 1.3
extended_attention_mask = causal_mask[:, None, :, :] * attention_mask[:, None, None, :]
else:
extended_attention_mask = attention_mask[:, None, None, :]
else:
raise ValueError(
"Wrong shape for input_ids (shape {}) or attention_mask (shape {})".format(
input_shape, attention_mask.shape
)
)
# Since attention_mask is 1.0 for positions we want to attend and 0.0 for
# masked positions, this operation will create a tensor which is 0.0 for
# positions we want to attend and -10000.0 for masked positions.
# Since we are adding it to the raw scores before the softmax, this is
# effectively the same as removing these entirely.
extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype) # fp16 compatibility
extended_attention_mask = (1.0 - extended_attention_mask) * -10000.0
# If a 2D ou 3D attention mask is provided for the cross-attention
# we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
if self.config.is_decoder and encoder_hidden_states is not None:
encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
if encoder_attention_mask is None:
encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
if encoder_attention_mask.dim() == 3:
encoder_extended_attention_mask = encoder_attention_mask[:, None, :, :]
elif encoder_attention_mask.dim() == 2:
encoder_extended_attention_mask = encoder_attention_mask[:, None, None, :]
else:
raise ValueError(
"Wrong shape for encoder_hidden_shape (shape {}) or encoder_attention_mask (shape {})".format(
encoder_hidden_shape, encoder_attention_mask.shape
)
)
encoder_extended_attention_mask = encoder_extended_attention_mask.to(
dtype=next(self.parameters()).dtype
) # fp16 compatibility
encoder_extended_attention_mask = (1.0 - encoder_extended_attention_mask) * -10000.0
else:
encoder_extended_attention_mask = None
# Prepare head mask if needed
# 1.0 in head_mask indicate we keep the head
# attention_probs has shape bsz x n_heads x N x N
# input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
# and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
if head_mask is not None:
if head_mask.dim() == 1:
head_mask = head_mask.unsqueeze(0).unsqueeze(0).unsqueeze(-1).unsqueeze(-1)
head_mask = head_mask.expand(self.config.num_hidden_layers, -1, -1, -1, -1)
elif head_mask.dim() == 2:
head_mask = (
head_mask.unsqueeze(1).unsqueeze(-1).unsqueeze(-1)
) # We can specify head_mask for each layer
head_mask = head_mask.to(
dtype=next(self.parameters()).dtype
) # switch to fload if need + fp16 compatibility
else:
head_mask = [None] * self.config.num_hidden_layers
以上是一些预处理的代码。判定input_ids的合法性,不能为空不能和inputs_embeds同时输入;接着就获取使用的设备是CPU还是GPU;判定attention_mask和token_type_ids的合法性,为None的话就新建一个;处理attention_mask得到encoder_extended_attention_mask,把它传播给所有的注意力头;最后就是判定是否启用decoder——bert模型是基于encoder的,我认为这里就不必要做这个判定,bert的encoder的结果只是传递给下一层encoder,并没有传递到decoder。
下面具体看核心的部分。
上面把输入做一些预处理后,使得输入都合法,然后就可以喂入模型的功能模块中。第一个就是
embedding_output = self.embeddings(
input_ids=input_ids, position_ids=position_ids, token_type_ids=token_type_ids, inputs_embeds=inputs_embeds
)
BertEmbedding子模型
其中的self.embeddings()就是__inti__()的BertEmbeddings(config)模块,它可以看做是一个起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=0)
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)
def forward(self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None):
if input_ids is not None:
input_shape = input_ids.size()
else:
input_shape = inputs_embeds.size()[:-1]
seq_length = input_shape[1]
device = input_ids.device if input_ids is not None else inputs_embeds.device
if position_ids is None:
position_ids = torch.arange(seq_length, dtype=torch.long, device=device)
position_ids = position_ids.unsqueeze(0).expand(input_shape)
if token_type_ids is None:
token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
if inputs_embeds is 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
它的具体作用就是:首先把我们输入的input_ids、token_type_ids和position_ids——(这里输入的是对应元素在词典中的index集合)经过torch.nn.Embedding()在各自的词典中得到词嵌入。然后把这3个向量直接做加法运算,接着做层归一化以及dropout()操作。这里为何可以直接相加是可以做一个专门的问题来讨论的,这里的归一化的作用应该就是避免一些数值问题、梯度问题和模型收敛问题以及分布改变问题,dropout操作随机丢弃掉一部分特征,可以增加模型的泛化性能。
BertEncoder
经过上述的处理后,我们就得到了一个维度是[batch_size,sequence_length,hidden_states]的向量embeddings。然后再把这个embeddings输入到Encoder中,代码如下,参数都很清晰明确:
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,
)
这里的self.encoder同样是__init__()中的BertEncoder(config)模型,全部代码如下:
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]
if self.output_attentions:
all_attentions = all_attentions + (layer_outputs[1],)
# Add last layer
if self.output_hidden_states:
all_hidden_states = all_hidden_states + (hidden_states,)
outputs = (hidden_states,)
if self.output_hidden_states:
outputs = outputs + (all_hidden_states,)
if self.output_attentions:
outputs = outputs + (all_attentions,)
return outputs
其中模型定义部分的核心代码如下:
self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
通过这句代码和config中的参数——“num_hidden_layers”: 12——可以得出BertEncoder使用12个(层)BertLayer组成的。对每一层的bertlayer在forward()中的for循环做如下操作:
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]
if self.output_attentions:
all_attentions = all_attentions + (layer_outputs[1],)
更新hidden_states(也就是layer_outputs[0]),然后把更新后的hidden_states传入到下一层BertLayer中,同时把每一层的hidden_states和attentions(也就是layer_outputs[1])记录下来,然后作为一个整体输出。所有最后的输出里包含的有最后一层BertLayer的hidden_states和12层所有的hidden_states以及attentions。
BertLayer具体又是什么样的呢?这里就需要看看具体的BertLayer的实现:
class BertLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.attention = BertAttention(config)
self.is_decoder = config.is_decoder
if self.is_decoder:
self.crossattention = BertAttention(config)
self.intermediate = BertIntermediate(config)
self.output = BertOutput(config)
可以看到BertLayer是由BertAttention()、BertIntermediate()和BertOutput()构成。它的forward()是比较简单的,没有什么奇特的操作,都是顺序的把输入经过BertAttention()、BertIntermediate()和BertOutput()这些子模型。这里主要来看看这些子模型的实现:
BertAttention
这里它又嵌套了一层,由BertSelfAttention()和BertSelfOutput()子模型组成!
这里马上就看到self-attention机制的实现了!感觉好激动!——Self-Attention则利用了Attention机制,计算每个单词与其他所有单词之间的关联(说实话理解的不是很透彻!)
class BertSelfAttention(nn.Module):
def __init__(self, config):
super().__init__()
if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"):
raise ValueError(
"The hidden size (%d) is not a multiple of the number of attention "
"heads (%d)" % (config.hidden_size, config.num_attention_heads)
)
self.output_attentions = config.output_attentions
self.num_attention_heads = config.num_attention_heads
self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
self.all_head_size = self.num_attention_heads * self.attention_head_size
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)
self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
def transpose_for_scores(self, x):
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)
# If this is instantiated as a cross-attention module, the keys
# and values come from an encoder; the attention mask needs to be
# such that the encoder's padding tokens are not attended to.
if encoder_hidden_states is not None:
mixed_key_layer = self.key(encoder_hidden_states)
mixed_value_layer = self.value(encoder_hidden_states)
attention_mask = encoder_attention_mask
else:
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)
# Take the dot product between "query" and "key" to get the raw attention scores.
attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2))
attention_scores = attention_scores / math.sqrt(self.attention_head_size)
if attention_mask is not None:
# Apply the attention mask is (precomputed for all layers in BertModel forward() function)
attention_scores = attention_scores + attention_mask
# Normalize the attention scores to probabilities.
attention_probs = nn.Softmax(dim=-1)(attention_scores)
# This is actually dropping out entire tokens to attend to, which might
# seem a bit unusual, but is taken from the original Transformer paper.
attention_probs = self.dropout(attention_probs)
# Mask heads if we want to
if head_mask is not None:
attention_probs = attention_probs * head_mask
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)
outputs = (context_layer, attention_probs) if self.output_attentions else (context_layer,)
return outputs
阅读代码之前先回顾一下,self-attention的公式是什么样的,公式编辑比较麻烦直接上2个图,都是来自Attention机制详解(二)——Self-Attention与Transformer文章中:
首先定义Q、K、V
然后应用到公式中:
以上就是单个头的self-attention的公式,多头的话就可以计算多次,然后在合并起来。这里就可以应用到矩阵运算了,还要注意的点就是Q、K、V的学习参数都是共享的——(要去验证),代码对应的就是:
-
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)
-
#注意这里的nn.Linear包含的学习参数一个是权重参数weights一个是偏置参数bias
-
#而且这里的query、key以及value它们的参数不一样,也就是并不共享参数
参数都包含在nn.Linear中了,这里的self.query对应的是12个头的self-attention机制对应的Q的学习参数模型,当然query、key以及value它们的参数不一样,也就是并不共享参数。
那么在forward()中是如何实现的呢?
-
mixed_query_layer = self.query(hidden_states)
#计算Q
-
if encoder_hidden_states
is
not
None:
-
mixed_key_layer = self.key(encoder_hidden_states)
-
mixed_value_layer = self.value(encoder_hidden_states)
-
attention_mask = encoder_attention_mask
-
else:
-
mixed_key_layer = self.key(hidden_states)
#计算K
-
mixed_value_layer = self.value(hidden_states)
#计算V
-
-
#做转置操作——这有点特殊:mixed_query_layer[batch_size,sequence_length,hidden_states]
-
#query_layer的维度:[batch_size,num_attention_heads,sequence_length,attention_head_size]
-
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)
-
-
#Q和K做点积
-
attention_scores = torch.matmul(query_layer, key_layer.transpose(
-1,
-2))
-
#Q和K做点积后然后除以根号下多头主力的尺寸
-
attention_scores = attention_scores / math.sqrt(self.attention_head_size)
-
if attention_mask
is
not
None:
-
# Apply the attention mask is (precomputed for all layers in BertModel forward() function)
-
attention_scores = attention_scores + attention_mask
-
-
# Normalize the attention scores to probabilities.
-
#做softmax操作,归一化
-
attention_probs = nn.Softmax(dim=
-1)(attention_scores)
-
-
# This is actually dropping out entire tokens to attend to, which might
-
# seem a bit unusual, but is taken from the original Transformer paper.
-
attention_probs = self.dropout(attention_probs)
-
-
# Mask heads if we want to
-
if head_mask
is
not
None:
-
attention_probs = attention_probs * head_mask
-
#中间结果和V做点积,得到最终结果——注意力得分也就是公式中的Z
-
context_layer = torch.matmul(attention_probs, value_layer)
以上代码的中文注释就把计算过程分析清楚了,计算mixed_query_layer、mixed_key_layer和mixed_value_layer,然后做转置(说是维度变换更贴切一点);接着mixed_query_layer、mixed_key_layer做点积操作,然后除以注意力头的尺寸的开方,做softmax操作;最后和mixed_value_layer相乘,得到注意力得分————矩阵计算代码就很好的实现了self-attention。
以上就是完成了self-attention,然后接下来就进入BertSelfOutput():
-
class BertSelfOutput(nn.Module):
-
def __init__(self, config):
-
super().__init__()
-
self.dense = nn.Linear(config.hidden_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, hidden_states, input_tensor):
-
hidden_states = self.dense(hidden_states)
-
hidden_states = self.dropout(hidden_states)
-
hidden_states = self.LayerNorm(hidden_states + input_tensor)
-
return hidden_states
以上BertSelfOutput()代码很简单,把self-attention输出的结果经过线性模型和dropout操作,最后做层归一化。到这里就跳出了BertAttention()模型,然后就进入中间层BertIntermediate()。
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
经过一个全连接层,由于config.hidden_size 跳出BertIntermediate()作为中间层后,就进入了BertOutput(config)模型,这个是BertLayer()模型的最后一个子模型。 经过线性模型和dropout操作,最后做层归一化,把特征空间又缩小回来了。最后输出一个hidden_states,这里就是一个BertLayer()的输出了。 然后经历了12个BertLayer()的操作,一层一层的变换,最后得出的outputs进入BertPooler(): pooler代码如下: 由代码可知这个pooler的功能就是把last_hidden_states的第二维的第一维也就是文本对应的第一个;。。。、。。 以上差不多就是BertModel的具体实现,由于这个模型的代码嵌套调用过多,可能理解起来有一定的困惑,那么接下来就需要一个图片来可视化理解。上图: 上图是huggingface中的BertModel的结构流程图(简图,有很多疏漏的地方勿怪!),bertModel的输入和基本的子模型以及数据的流向都显示出来了,对应着代码理解起来更加方便。黄色的图形就是torch中的基本函数模块(这里的Q、K和V不是),其他颜色的矩形就是模型,平行四边形就是数据。 以上就是对BertModel实现代码的简单解析,里面涉及到很多的细节:不同模型模块的参数以及它们的维度信息,还有就是变量的维度变化,以及每个模型模块的具体作用和意义,没有去深究,读者有精力的话可以自己去深究。 这里我们要写一个使用transformers项目中的分类器来实现一个简单的文本分类任务,这里我们没有自己取重写Dataloader以及模型的训练,就是直接把transformers项目中的bert分类器拿过来进行fine-tune,工作量少,结果也比较好!当然也可以完全自己实现(前面也自己实现过一个基于bert的句子分类的任务——使用bert模型做句子分类,有兴趣的可以移步),后续有时间的话可以写一个各个模型文本分类任务的比较博客,更加熟练文本分类的一些代码coding和知识——增加熟练度,也可以给大家分享一下。 来看本文的transformers项目中的bert分类器进行fine-tune作文本分类的任务,在这个项目里面已经把全部的代码写好了,我们只需要把我们的文本处理成项目能够识别和读取的形式。简单的分析一下,分类任务的代码: 主要的分类任务的代码是在run_glue.py文件中,这里面定义了main函数,命令行参数接收器,模型的加载和调用,模型的训练以及验证,和数据读取以及处理的功能模块调用。 我们看一下这里调用的分类模型,代码是这样的: 其实最终这里的AutoModelForSequenceClassification.from_pretrained()调用的是modeling_bert.py中的BertForSequenceClassification类,它就是具体的分类器实现: 模型调用了BertModel,然后做使用nn.Linear(config.hidden_size, self.config.num_labels)做分类,loss函数是常用的交叉熵损失函数。以上就是分类器的一些简单的分析。 我们要做的工作就是仿照项目里的代码写一个任务处理器: 项目目录结构:transformerer_local/data/glue.py,注意这里的transformerer_local原本应该是transformerer,我这里已经做了修改。在glue.py添加上我们的分类任务代码——添加一个读取文件中的文本然后,然后把每条数据序列化成Example,注意get_labels()函数,把自己的类别数目实现过来,代码如下: 同时在验证的时候,对应评价指标函数,我们这里不是binary,计算f1_score的时候要采用其他的策略: transformerer_local/data/metrics/__init__.py,注意这里的transformerer_local原本应该是transformerer,添加内容: 添加内容就在注释部分。 OK,现在代码部分已经做好了,接下来就是数据部分了。直接上数据: 数据截图部分就是上面这样的,把pat_summary和ipc_class属性提取出来,这里的数据质量比较好,然后只需要把超级长的文本去掉(长度大于510的): 数据长度分布直方图,发现几乎全部都是小于510的长度,只有少部分比较长,只有128条,这里数据集总规模是24.8W条,可以把这少部分的直接去掉。然后把数据分割成训练集和测试集比例(8:2),保存为tsv格式。 接下来就是直接进行训练了,编写如下命令行,在train_glue_classification.sh文件中: 直接在终端上运行这个sh文件,bash train_glue_classification.sh。注意这里的训练显卡显存得11G以上,不然跑步起来,batch_size不能太大。训练过程中,一个epoch大概时间3.5小时,所以时间还要蛮久的。最后给出结果: 可以看到acc=0.8508,一个8分类的任务准确率85%粗略一看还能接受。如果要详细的分析,可以把每一类的准确率和召回率给弄出来,或者分析一下ROC,对模型的性能做详细的分析,这里不做过多讨论。另外关于这个模型的优化,怎么提高准确率,也不做考虑。 小结:以上就是直接使用transformers项目中的bert分类器拿过来进行fine-tune,做文本分类,其实代码都写好了,我们只需要简单的修改一下代码和配置,就能很快的训练好自己的分类器。 其实关于Bert模型还有很多细节可以去探究,这里推荐知乎上的一些文章:超细节的BERT/Transformer知识点。 1、Bert模型怎么解决长文本问题? 如果文本的长度不是特别长,511-600左右,可以直接把大于510的部分直接去掉,这是一种最粗暴的处理办法。 如果文本内容很长,而且内容也比较重要,那么就不能够这么直接粗暴的处理了。主要思路是global norm + passage rank + sliding window——来自Amazon EMNLP的这篇文章:Multi-passage BERT。简单的说一下sliding window,滑窗法就是把文档分割成有部分重叠的短文本段落,然后把这些文本得出的向量拼接起来或者做mean pooling操作。具体的效果,要去做实验。 2、Bert的输入向量Token Embedding、Segment Embedding、Position Embedding,它们都有自己的物理含义,为什么可以相加后输入到模型中取呢? 这个问题在知乎上已经有人提问了,回答的大佬很多。我个人倾向接受这个解释:one hot向量concat后经过一个全连接等价于向量embedding后直接相加。 Token Embedding、Segment Embedding、Position Embedding分别代表了文本的具体语义,段落含义和位置含义,现在要把这3个不同的向量一起放到模型中去训练,我认为concat的操作就能完整的保留文本的含义。[input_ids] 、[token_type_ids] 和[position_ids]这3个向量,concat以后形成一个[input_ids token_type_ids position_ids]新的向量,这样丢入模型中取训练就应该是我们初始要的结果。但是在丢入模型之前这个向量[input_ids token_type_ids position_ids]是需要经过Embedding的,而[input_ids] 、[token_type_ids] 和[position_ids]先经过Embedding然后相加和上面的效果是等价的。这样的好处是降低了参数的维度的同时达到了同样的效果,所以就采用了直接相加。 3、BERT在第一句前会加一个[CLS]标志,为什么?作用是什么? 最后一层的transformer的输出该位置的向量,由于本身并不具有任何意义,就能很公平的融合整个句子的含义,然后做下游任务的时候就很好了。 其实在huggingface实现的bert代码中,作者认为这个向量并不是很好,要想做下有任务,还是得靠自己取把最后一层的hidden_states[B,S,D]去做一些操作,比如mean pool操作。我这里没有实验过,只是拿来使用,在使用bert模型做句子分类一文中使用了这样的思想。 4、Bert模型的非线性来自什么地方? 主要是来子前馈层的gelu激活函数和self-attention。 5、Bert模型为何要使用多头注意力机制? 谷歌bert作者在论文中提到的是模型有多头的话,就可以形成多个子空间,那么模型就可以去关注不同方面的信息。 可以这样理解,多头attention机制确实有点类似多个卷积核的作用,可以捕捉到文本更多更丰富的信息。 当然知乎有人专门研究这个问题,列举了头和头直接的异同关系,作了一个比较综合全面的回答,可以去阅读!为什么Transformer 需要进行 Multi-head Attention? 写在最后: 我个人理解的Bert模型就只有 这么多,其实Bert模型就是一个提取词向量的语言模型,由于提取的词向量能很好的包含文本的语义信息,它能够做很多任务并且取得不错的效果。NER、关系抽取、文本相似度计算、文本分类、阅读理解等等任务Bert都能做。 这个博客个人算是花了一定的精力了的(五一到现在,差不多10天时间吧),作为这段时间以来学习NLP的一个总结还是很有收获感的。加油!继续努力!当然博客可能写的不是干货,也许还有错误的地方,作者水平有限,望大家提出改正! 参考文章 一文读懂bert模型 超细节的BERT/Transformer知识点BertOutput(config)
BertPooler()
三、Bert文本分类任务实战
四、Bert模型难点总结