bert模型简介、transformers中bert模型源码阅读、分类任务实战和难点总结

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模型难点总结

写在最前面,这篇博客篇幅有点长,原因是贴的代码和图有点多,感兴趣的可以坚持读下去!


一、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任务。

bert与训练的流程:

这个过程包括两个任务,一个是Masked Language Model(遮掩语言模型),另外一个是Next Sentence Prediction(下一句预测)。

Masked Language Model(遮掩语言模型)可以理解为是做完型填空,把语料中15%的词遮掩掉,来学习词和词之间的一些规律;

Next Sentence Prediction就是学习语料中上下文中2个句子之间的关系规律。

通过这2个阶段任务的学习,bert就会把文本的语法和语义信息学习到。bert模型中的self-attention机制可以使用文本其他的词来增强目标词的语义表示,这也是bert模型吊打其他模型的一个关键原因。

bert模型的输入

bert模型的输入可以是一个句子或者句子对,代码层面来说,就是输入了句子或者句子对对应的3个向量。它们分别是token embedding,segment embedding和position embedding,具体的含义:

token embedding:句子的词向量

segment embedding:是那个句子的0和1

position embedding:位置向量,指明每个字在句中的位置。

关于position embedding这里有两种求法,一种是有相应的三角函数公式得出的,这种是绝对向量;还有一种是学习得到的,这种是相对向量。具体形式如下:

二、huggingface的bert源码浅析

关于bert模型的使用,我主要是使用huggingface的transformer库来调用bert和使用——一般是直接用来bert来获取词向量。这里就bert的使用和huggingface中的源码进行一些解读。

bert提取文本词向量

首先看一段简单的代码,使用huggingface的transformers(其实就是实现的bert)来提取句——我爱武汉!我爱中国!——的向量。代码如下:


   
    
    
  1. from transformers import BertModel,BertTokenizer,BertConfig
  2. import torch
  3. config = BertConfig.from_pretrained( 'pretrain_model/chinese-bert-wwm') #第一步加载模型配置文件
  4. bertmodel = BertModel.from_pretrained( 'pretrain_model/chinese-bert-wwm',config=config) #第二步初始化模型,并加载权重
  5. # print('***************************bertmodel***************************')
  6. tokenizer = BertTokenizer.from_pretrained( 'pretrain_model/chinese-bert-wwm') #第三步加载tokenizer
  7. text1 = '我爱武汉!我爱中国!'
  8. tokeniz_text1 = tokenizer.tokenize(text1)
  9. # print(tokeniz_text1)
  10. # print('tokeniz_text1:',len(tokeniz_text1))
  11. indexed_tokens_1 = tokenizer.convert_tokens_to_ids(tokeniz_text1)
  12. print( 'len(indexed_tokens_1):',len(indexed_tokens_1))
  13. print(indexed_tokens_1)
  14. input_ids_1 = indexed_tokens_1
  15. # print(indexed_tokens_1)
  16. # print('indexed_tokens_1:',len(indexed_tokens_1))
  17. segments_ids_1 = [ 0]*len(input_ids_1) #其实这个输入可以不用的,因为是单句的原因
  18. input_masks_1 = [ 1]*len(input_ids_1) #其实这个输入可以不用的,因为是单句的原因
  19. input_ids_1_tensor = torch.tensor([input_ids_1])
  20. vector1,pooler1 = bertmodel(input_ids_1_tensor) #应该是输入3个向量的,但是单句情况下,它自会自己做判断,然后自动生成对应的segments_ids和input_masks向量
  21. #这里的输出最后一层的last_hidden_state和最后一层首个token的hidden-state
  22. text2 = '[CLS]我爱武汉!我爱中国![SEP]'
  23. tokeniz_text2 = tokenizer.tokenize(text2)
  24. indexed_tokens_2 = tokenizer.convert_tokens_to_ids(tokeniz_text2)
  25. input_ids_2 = indexed_tokens_2
  26. segments_ids_2 = [ 0]*len(input_ids_2) #其实这个输入可以不用的,因为是单句的原因
  27. input_masks_2 = [ 1]*len(input_ids_2) #其实这个输入可以不用的,因为是单句的原因
  28. input_ids_2_tensor = torch.tensor([input_ids_2])
  29. vector2,pooler2 = bertmodel(input_ids_2_tensor)
  30. print( 'pooler2:',pooler2)
  31. print( 'vector2[:,0:1,:]:',vector2[:, 0: 1,:])
  32. text1_encode = tokenizer.encode(text1,add_special_tokens= True)
  33. print( 'len(text1_encode):',len(text1_encode))
  34. print( 'text1_encode:',text1_encode)
  35. #这里的text1_encode和indexed_tokens_2是一模一样的,encode()函数会自动为文本添加特殊字符[UNK][CLS][SEP][MASK]等

以上代码是基于pytorch来实现的,同时应用到了transoformers库!可以看到bert模型的使用非常简单!

第一步,初始化bert模型和加载权重。这个步骤中,首先加载配置文件、然后加载bert模型和载入权重。

第二步,对输入文本做词表映射,形成初始词向量。

第三步,输入喂入bert模型中得到输入文本的结果向量。

文中是bert模型的输入我这里只给出了一个那就是input_ids,另外的2个没有给出。这里的原因就是这里是单个句子,模型内部可以对另外2个输入做自动添加的处理——并不是没有,这点要注意到。

这里有个疑问因为bert的输入文本得添加一个[cls]特殊字符,我认为最后的输出lsat_hidden_state中的lsat_hidden_state[:,0:1,:]应该和pooler结果是一样的,可是这里是不一样的,有点理解的偏差,不知道为什么。

BertModel代码阅读

通过上文中的代码,大致可以知道怎么调用一些API来创建bert模型和应用它。那么huggingface中是怎么实现BertModel的这个也是比较重要的,这里我们就好好阅读以下其中关于BertModel实现的代码。看一张transformers项目文件结构图:

这么面封装了很多模型的构建,我们主要是阅读modeling_bert.py文件,它在里面详细的展示了如何构建一个Bert模型的:


   
    
    
  1. class BertModel(BertPreTrainedModel):
  2. """
  3. .......
  4. """
  5. def __init__(self, config):
  6. super().__init__(config)
  7. self.config = config
  8. self.embeddings = BertEmbeddings(config)
  9. self.encoder = BertEncoder(config)
  10. self.pooler = BertPooler(config)
  11. self.init_weights()
  12. def get_input_embeddings(self):
  13. return self.embeddings.word_embeddings
  14. def set_input_embeddings(self, value):
  15. self.embeddings.word_embeddings = value
  16. def _prune_heads(self, heads_to_prune):
  17. """ Prunes heads of the model.
  18. heads_to_prune: dict of {layer_num: list of heads to prune in this layer}
  19. See base class PreTrainedModel
  20. """
  21. for layer, heads in heads_to_prune.items():
  22. self.encoder.layer[layer].attention.prune_heads(heads)
  23. @add_start_docstrings_to_callable(BERT_INPUTS_DOCSTRING)
  24. def forward(
  25. self,
  26. input_ids=None,
  27. attention_mask=None,
  28. token_type_ids=None,
  29. position_ids=None,
  30. head_mask=None,
  31. inputs_embeds=None,
  32. encoder_hidden_states=None,
  33. encoder_attention_mask=None,
  34. ):
  35. r""".......
  36. """
  37. if input_ids is not None and inputs_embeds is not None:
  38. raise ValueError( "You cannot specify both input_ids and inputs_embeds at the same time")
  39. elif input_ids is not None:
  40. input_shape = input_ids.size()
  41. elif inputs_embeds is not None:
  42. input_shape = inputs_embeds.size()[: -1]
  43. else:
  44. raise ValueError( "You have to specify either input_ids or inputs_embeds")
  45. device = input_ids.device if input_ids is not None else inputs_embeds.device
  46. if attention_mask is None:
  47. attention_mask = torch.ones(input_shape, device=device)
  48. if token_type_ids is None:
  49. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  50. # We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
  51. # ourselves in which case we just need to make it broadcastable to all heads.
  52. extended_attention_mask: torch.Tensor = self.get_extended_attention_mask(
  53. attention_mask, input_shape, self.device
  54. )
  55. # If a 2D ou 3D attention mask is provided for the cross-attention
  56. # we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
  57. if self.config.is_decoder and encoder_hidden_states is not None:
  58. encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
  59. encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
  60. if encoder_attention_mask is None:
  61. encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
  62. encoder_extended_attention_mask = self.invert_attention_mask(encoder_attention_mask)
  63. else:
  64. encoder_extended_attention_mask = None
  65. # Prepare head mask if needed
  66. # 1.0 in head_mask indicate we keep the head
  67. # attention_probs has shape bsz x n_heads x N x N
  68. # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
  69. # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
  70. head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers)
  71. embedding_output = self.embeddings(
  72. input_ids=input_ids, position_ids=position_ids, token_type_ids=token_type_ids, inputs_embeds=inputs_embeds
  73. )
  74. encoder_outputs = self.encoder(
  75. embedding_output,
  76. attention_mask=extended_attention_mask,
  77. head_mask=head_mask,
  78. encoder_hidden_states=encoder_hidden_states,
  79. encoder_attention_mask=encoder_extended_attention_mask,
  80. )
  81. sequence_output = encoder_outputs[ 0]
  82. pooled_output = self.pooler(sequence_output)
  83. outputs = (sequence_output, pooled_output,) + encoder_outputs[
  84. 1:
  85. ] # add hidden_states and attentions if they are here
  86. return outputs # sequence_output, pooled_output, (hidden_states), (attentions)

以上就是BertModel的全部代码,可以看到在BertModel类中,首先__init__()函数中定义了模型的基本模块,然后在forward()函数里面使用这些结构模块具体实现了Bert的逻辑。


   
    
    
  1. def __init__(self, config):
  2. super().__init__(config)
  3. self.config = config
  4. self.embeddings = BertEmbeddings(config)
  5. self.encoder = BertEncoder(config)
  6. self.pooler = BertPooler(config)
  7. self.init_weights()

__init__()函数中定义的模型模块主要是3个,分别是BertEmbedding、BertEncoder和BertPooler。然后在forward(),输入顺序的经过这3个模块的处理就得到了我们要的结果——对应文本的bert向量。

下面来阅读forward():


   
    
    
  1. if input_ids is not None and inputs_embeds is not None:
  2. raise ValueError( "You cannot specify both input_ids and inputs_embeds at the same time")
  3. elif input_ids is not None:
  4. input_shape = input_ids.size()
  5. elif inputs_embeds is not None:
  6. input_shape = inputs_embeds.size()[: -1]
  7. else:
  8. raise ValueError( "You have to specify either input_ids or inputs_embeds")
  9. device = input_ids.device if input_ids is not None else inputs_embeds.device
  10. if attention_mask is None:
  11. attention_mask = torch.ones(input_shape, device=device)
  12. if token_type_ids is None:
  13. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  14. # We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
  15. # ourselves in which case we just need to make it broadcastable to all heads.
  16. if attention_mask.dim() == 3:
  17. extended_attention_mask = attention_mask[:, None, :, :]
  18. elif attention_mask.dim() == 2:
  19. # Provided a padding mask of dimensions [batch_size, seq_length]
  20. # - if the model is a decoder, apply a causal mask in addition to the padding mask
  21. # - if the model is an encoder, make the mask broadcastable to [batch_size, num_heads, seq_length, seq_length]
  22. if self.config.is_decoder:
  23. batch_size, seq_length = input_shape
  24. seq_ids = torch.arange(seq_length, device=device)
  25. causal_mask = seq_ids[ None, None, :].repeat(batch_size, seq_length, 1) <= seq_ids[ None, :, None]
  26. causal_mask = causal_mask.to(
  27. attention_mask.dtype
  28. ) # causal and attention masks must have same type with pytorch version < 1.3
  29. extended_attention_mask = causal_mask[:, None, :, :] * attention_mask[:, None, None, :]
  30. else:
  31. extended_attention_mask = attention_mask[:, None, None, :]
  32. else:
  33. raise ValueError(
  34. "Wrong shape for input_ids (shape {}) or attention_mask (shape {})".format(
  35. input_shape, attention_mask.shape
  36. )
  37. )
  38. # Since attention_mask is 1.0 for positions we want to attend and 0.0 for
  39. # masked positions, this operation will create a tensor which is 0.0 for
  40. # positions we want to attend and -10000.0 for masked positions.
  41. # Since we are adding it to the raw scores before the softmax, this is
  42. # effectively the same as removing these entirely.
  43. extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype) # fp16 compatibility
  44. extended_attention_mask = ( 1.0 - extended_attention_mask) * -10000.0
  45. # If a 2D ou 3D attention mask is provided for the cross-attention
  46. # we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
  47. if self.config.is_decoder and encoder_hidden_states is not None:
  48. encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
  49. encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
  50. if encoder_attention_mask is None:
  51. encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
  52. if encoder_attention_mask.dim() == 3:
  53. encoder_extended_attention_mask = encoder_attention_mask[:, None, :, :]
  54. elif encoder_attention_mask.dim() == 2:
  55. encoder_extended_attention_mask = encoder_attention_mask[:, None, None, :]
  56. else:
  57. raise ValueError(
  58. "Wrong shape for encoder_hidden_shape (shape {}) or encoder_attention_mask (shape {})".format(
  59. encoder_hidden_shape, encoder_attention_mask.shape
  60. )
  61. )
  62. encoder_extended_attention_mask = encoder_extended_attention_mask.to(
  63. dtype=next(self.parameters()).dtype
  64. ) # fp16 compatibility
  65. encoder_extended_attention_mask = ( 1.0 - encoder_extended_attention_mask) * -10000.0
  66. else:
  67. encoder_extended_attention_mask = None
  68. # Prepare head mask if needed
  69. # 1.0 in head_mask indicate we keep the head
  70. # attention_probs has shape bsz x n_heads x N x N
  71. # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
  72. # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
  73. if head_mask is not None:
  74. if head_mask.dim() == 1:
  75. head_mask = head_mask.unsqueeze( 0).unsqueeze( 0).unsqueeze( -1).unsqueeze( -1)
  76. head_mask = head_mask.expand(self.config.num_hidden_layers, -1, -1, -1, -1)
  77. elif head_mask.dim() == 2:
  78. head_mask = (
  79. head_mask.unsqueeze( 1).unsqueeze( -1).unsqueeze( -1)
  80. ) # We can specify head_mask for each layer
  81. head_mask = head_mask.to(
  82. dtype=next(self.parameters()).dtype
  83. ) # switch to fload if need + fp16 compatibility
  84. else:
  85. 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。

下面具体看核心的部分。

上面把输入做一些预处理后,使得输入都合法,然后就可以喂入模型的功能模块中。第一个就是


   
    
    
  1. embedding_output = self.embeddings(
  2. input_ids=input_ids, position_ids=position_ids, token_type_ids=token_type_ids, inputs_embeds=inputs_embeds
  3. )

BertEmbedding子模型

其中的self.embeddings()就是__inti__()的BertEmbeddings(config)模块,它可以看做是一个起embedding功能作用的子模型,具体代码:


   
    
    
  1. class BertEmbeddings(nn.Module):
  2. """Construct the embeddings from word, position and token_type embeddings.
  3. """
  4. def __init__(self, config):
  5. super().__init__()
  6. self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx= 0)
  7. self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
  8. self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
  9. # self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load
  10. # any TensorFlow checkpoint file
  11. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  12. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  13. def forward(self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None):
  14. if input_ids is not None:
  15. input_shape = input_ids.size()
  16. else:
  17. input_shape = inputs_embeds.size()[: -1]
  18. seq_length = input_shape[ 1]
  19. device = input_ids.device if input_ids is not None else inputs_embeds.device
  20. if position_ids is None:
  21. position_ids = torch.arange(seq_length, dtype=torch.long, device=device)
  22. position_ids = position_ids.unsqueeze( 0).expand(input_shape)
  23. if token_type_ids is None:
  24. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  25. if inputs_embeds is None:
  26. inputs_embeds = self.word_embeddings(input_ids)
  27. position_embeddings = self.position_embeddings(position_ids)
  28. token_type_embeddings = self.token_type_embeddings(token_type_ids)
  29. embeddings = inputs_embeds + position_embeddings + token_type_embeddings
  30. embeddings = self.LayerNorm(embeddings)
  31. embeddings = self.dropout(embeddings)
  32. 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中,代码如下,参数都很清晰明确:


   
    
    
  1. encoder_outputs = self.encoder(
  2. embedding_output,
  3. attention_mask=extended_attention_mask,
  4. head_mask=head_mask,
  5. encoder_hidden_states=encoder_hidden_states,
  6. encoder_attention_mask=encoder_extended_attention_mask,
  7. )

这里的self.encoder同样是__init__()中的BertEncoder(config)模型,全部代码如下:


   
    
    
  1. class BertEncoder(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.output_attentions = config.output_attentions
  5. self.output_hidden_states = config.output_hidden_states
  6. self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
  7. def forward(
  8. self,
  9. hidden_states,
  10. attention_mask=None,
  11. head_mask=None,
  12. encoder_hidden_states=None,
  13. encoder_attention_mask=None,
  14. ):
  15. all_hidden_states = ()
  16. all_attentions = ()
  17. for i, layer_module in enumerate(self.layer):
  18. if self.output_hidden_states:
  19. all_hidden_states = all_hidden_states + (hidden_states,)
  20. layer_outputs = layer_module(
  21. hidden_states, attention_mask, head_mask[i], encoder_hidden_states, encoder_attention_mask
  22. )
  23. hidden_states = layer_outputs[ 0]
  24. if self.output_attentions:
  25. all_attentions = all_attentions + (layer_outputs[ 1],)
  26. # Add last layer
  27. if self.output_hidden_states:
  28. all_hidden_states = all_hidden_states + (hidden_states,)
  29. outputs = (hidden_states,)
  30. if self.output_hidden_states:
  31. outputs = outputs + (all_hidden_states,)
  32. if self.output_attentions:
  33. outputs = outputs + (all_attentions,)
  34. 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循环做如下操作:


   
    
    
  1. for i, layer_module in enumerate(self.layer):
  2. if self.output_hidden_states:
  3. all_hidden_states = all_hidden_states + (hidden_states,)
  4. layer_outputs = layer_module(
  5. hidden_states, attention_mask, head_mask[i], encoder_hidden_states, encoder_attention_mask
  6. )
  7. hidden_states = layer_outputs[ 0]
  8. if self.output_attentions:
  9. 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的实现:


   
    
    
  1. class BertLayer(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.attention = BertAttention(config)
  5. self.is_decoder = config.is_decoder
  6. if self.is_decoder:
  7. self.crossattention = BertAttention(config)
  8. self.intermediate = BertIntermediate(config)
  9. self.output = BertOutput(config)

可以看到BertLayer是由BertAttention()、BertIntermediate()和BertOutput()构成。它的forward()是比较简单的,没有什么奇特的操作,都是顺序的把输入经过BertAttention()、BertIntermediate()和BertOutput()这些子模型。这里主要来看看这些子模型的实现:

BertAttention

这里它又嵌套了一层,由BertSelfAttention()和BertSelfOutput()子模型组成!

这里马上就看到self-attention机制的实现了!感觉好激动!——Self-Attention则利用了Attention机制,计算每个单词与其他所有单词之间的关联(说实话理解的不是很透彻!)


   
    
    
  1. class BertSelfAttention(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"):
  5. raise ValueError(
  6. "The hidden size (%d) is not a multiple of the number of attention "
  7. "heads (%d)" % (config.hidden_size, config.num_attention_heads)
  8. )
  9. self.output_attentions = config.output_attentions
  10. self.num_attention_heads = config.num_attention_heads
  11. self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
  12. self.all_head_size = self.num_attention_heads * self.attention_head_size
  13. self.query = nn.Linear(config.hidden_size, self.all_head_size)
  14. self.key = nn.Linear(config.hidden_size, self.all_head_size)
  15. self.value = nn.Linear(config.hidden_size, self.all_head_size)
  16. self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
  17. def transpose_for_scores(self, x):
  18. new_x_shape = x.size()[: -1] + (self.num_attention_heads, self.attention_head_size)
  19. x = x.view(*new_x_shape)
  20. return x.permute( 0, 2, 1, 3)
  21. def forward(
  22. self,
  23. hidden_states,
  24. attention_mask=None,
  25. head_mask=None,
  26. encoder_hidden_states=None,
  27. encoder_attention_mask=None,
  28. ):
  29. mixed_query_layer = self.query(hidden_states)
  30. # If this is instantiated as a cross-attention module, the keys
  31. # and values come from an encoder; the attention mask needs to be
  32. # such that the encoder's padding tokens are not attended to.
  33. if encoder_hidden_states is not None:
  34. mixed_key_layer = self.key(encoder_hidden_states)
  35. mixed_value_layer = self.value(encoder_hidden_states)
  36. attention_mask = encoder_attention_mask
  37. else:
  38. mixed_key_layer = self.key(hidden_states)
  39. mixed_value_layer = self.value(hidden_states)
  40. query_layer = self.transpose_for_scores(mixed_query_layer)
  41. key_layer = self.transpose_for_scores(mixed_key_layer)
  42. value_layer = self.transpose_for_scores(mixed_value_layer)
  43. # Take the dot product between "query" and "key" to get the raw attention scores.
  44. attention_scores = torch.matmul(query_layer, key_layer.transpose( -1, -2))
  45. attention_scores = attention_scores / math.sqrt(self.attention_head_size)
  46. if attention_mask is not None:
  47. # Apply the attention mask is (precomputed for all layers in BertModel forward() function)
  48. attention_scores = attention_scores + attention_mask
  49. # Normalize the attention scores to probabilities.
  50. attention_probs = nn.Softmax(dim= -1)(attention_scores)
  51. # This is actually dropping out entire tokens to attend to, which might
  52. # seem a bit unusual, but is taken from the original Transformer paper.
  53. attention_probs = self.dropout(attention_probs)
  54. # Mask heads if we want to
  55. if head_mask is not None:
  56. attention_probs = attention_probs * head_mask
  57. context_layer = torch.matmul(attention_probs, value_layer)
  58. context_layer = context_layer.permute( 0, 2, 1, 3).contiguous()
  59. new_context_layer_shape = context_layer.size()[: -2] + (self.all_head_size,)
  60. context_layer = context_layer.view(*new_context_layer_shape)
  61. outputs = (context_layer, attention_probs) if self.output_attentions else (context_layer,)
  62. return outputs

阅读代码之前先回顾一下,self-attention的公式是什么样的,公式编辑比较麻烦直接上2个图,都是来自Attention机制详解(二)——Self-Attention与Transformer文章中:

首先定义Q、K、V

然后应用到公式中:

以上就是单个头的self-attention的公式,多头的话就可以计算多次,然后在合并起来。这里就可以应用到矩阵运算了,还要注意的点就是Q、K、V的学习参数都是共享的——(要去验证),代码对应的就是:


   
    
    
  1. self.query = nn.Linear(config.hidden_size, self.all_head_size)
  2. self.key = nn.Linear(config.hidden_size, self.all_head_size)
  3. self.value = nn.Linear(config.hidden_size, self.all_head_size)
  4. #注意这里的nn.Linear包含的学习参数一个是权重参数weights一个是偏置参数bias
  5. #而且这里的query、key以及value它们的参数不一样,也就是并不共享参数

参数都包含在nn.Linear中了,这里的self.query对应的是12个头的self-attention机制对应的Q的学习参数模型,当然query、key以及value它们的参数不一样,也就是并不共享参数。

那么在forward()中是如何实现的呢?


   
    
    
  1. mixed_query_layer = self.query(hidden_states) #计算Q
  2. if encoder_hidden_states is not None:
  3. mixed_key_layer = self.key(encoder_hidden_states)
  4. mixed_value_layer = self.value(encoder_hidden_states)
  5. attention_mask = encoder_attention_mask
  6. else:
  7. mixed_key_layer = self.key(hidden_states) #计算K
  8. mixed_value_layer = self.value(hidden_states) #计算V
  9. #做转置操作——这有点特殊:mixed_query_layer[batch_size,sequence_length,hidden_states]
  10. #query_layer的维度:[batch_size,num_attention_heads,sequence_length,attention_head_size]
  11. query_layer = self.transpose_for_scores(mixed_query_layer)
  12. key_layer = self.transpose_for_scores(mixed_key_layer)
  13. value_layer = self.transpose_for_scores(mixed_value_layer)
  14. #Q和K做点积
  15. attention_scores = torch.matmul(query_layer, key_layer.transpose( -1, -2))
  16. #Q和K做点积后然后除以根号下多头主力的尺寸
  17. attention_scores = attention_scores / math.sqrt(self.attention_head_size)
  18. if attention_mask is not None:
  19. # Apply the attention mask is (precomputed for all layers in BertModel forward() function)
  20. attention_scores = attention_scores + attention_mask
  21. # Normalize the attention scores to probabilities.
  22. #做softmax操作,归一化
  23. attention_probs = nn.Softmax(dim= -1)(attention_scores)
  24. # This is actually dropping out entire tokens to attend to, which might
  25. # seem a bit unusual, but is taken from the original Transformer paper.
  26. attention_probs = self.dropout(attention_probs)
  27. # Mask heads if we want to
  28. if head_mask is not None:
  29. attention_probs = attention_probs * head_mask
  30. #中间结果和V做点积,得到最终结果——注意力得分也就是公式中的Z
  31. 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():


   
    
    
  1. class BertSelfOutput(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.hidden_size)
  5. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. def forward(self, hidden_states, input_tensor):
  8. hidden_states = self.dense(hidden_states)
  9. hidden_states = self.dropout(hidden_states)
  10. hidden_states = self.LayerNorm(hidden_states + input_tensor)
  11. return hidden_states

以上BertSelfOutput()代码很简单,把self-attention输出的结果经过线性模型和dropout操作,最后做层归一化。到这里就跳出了BertAttention()模型,然后就进入中间层BertIntermediate()。

BertIntermediate

BertIntermediate()作为中间层代码很简单:


   
    
    
  1. class BertIntermediate(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
  5. if isinstance(config.hidden_act, str):
  6. self.intermediate_act_fn = ACT2FN[config.hidden_act]
  7. else:
  8. self.intermediate_act_fn = config.hidden_act
  9. def forward(self, hidden_states):
  10. hidden_states = self.dense(hidden_states)
  11. hidden_states = self.intermediate_act_fn(hidden_states)
  12. return hidden_states

经过一个全连接层,由于config.hidden_size<config.intermediate_size,这里的Linear把特征空间变大了,然后进过了gelu激活函数,增加了特征的非线性性。

BertOutput(config)

跳出BertIntermediate()作为中间层后,就进入了BertOutput(config)模型,这个是BertLayer()模型的最后一个子模型。


   
    
    
  1. class BertOutput(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.intermediate_size, config.hidden_size)
  5. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. def forward(self, hidden_states, input_tensor):
  8. hidden_states = self.dense(hidden_states)
  9. hidden_states = self.dropout(hidden_states)
  10. hidden_states = self.LayerNorm(hidden_states + input_tensor)
  11. return hidden_states

经过线性模型和dropout操作,最后做层归一化,把特征空间又缩小回来了。最后输出一个hidden_states,这里就是一个BertLayer()的输出了。

BertPooler()

然后经历了12个BertLayer()的操作,一层一层的变换,最后得出的outputs进入BertPooler():


   
    
    
  1. sequence_output = encoder_outputs[ 0]
  2. pooled_output = self.pooler(sequence_output)

pooler代码如下:


   
    
    
  1. class BertPooler(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.hidden_size)
  5. self.activation = nn.Tanh()
  6. def forward(self, hidden_states):
  7. # We "pool" the model by simply taking the hidden state corresponding
  8. # to the first token.
  9. first_token_tensor = hidden_states[:, 0]
  10. pooled_output = self.dense(first_token_tensor)
  11. pooled_output = self.activation(pooled_output)
  12. return pooled_output
  13. #以上的pooler作用要具体的去调试hidden_states的shape。

由代码可知这个pooler的功能就是把last_hidden_states的第二维的第一维也就是文本对应的第一个;。。。、。。

以上差不多就是BertModel的具体实现,由于这个模型的代码嵌套调用过多,可能理解起来有一定的困惑,那么接下来就需要一个图片来可视化理解。上图:

上图是huggingface中的BertModel的结构流程图(简图,有很多疏漏的地方勿怪!),bertModel的输入和基本的子模型以及数据的流向都显示出来了,对应着代码理解起来更加方便。黄色的图形就是torch中的基本函数模块(这里的Q、K和V不是),其他颜色的矩形就是模型,平行四边形就是数据。

以上就是对BertModel实现代码的简单解析,里面涉及到很多的细节:不同模型模块的参数以及它们的维度信息,还有就是变量的维度变化,以及每个模型模块的具体作用和意义,没有去深究,读者有精力的话可以自己去深究。

三、Bert文本分类任务实战

        这里我们要写一个使用transformers项目中的分类器来实现一个简单的文本分类任务,这里我们没有自己取重写Dataloader以及模型的训练,就是直接把transformers项目中的bert分类器拿过来进行fine-tune,工作量少,结果也比较好!当然也可以完全自己实现(前面也自己实现过一个基于bert的句子分类的任务——使用bert模型做句子分类,有兴趣的可以移步),后续有时间的话可以写一个各个模型文本分类任务的比较博客,更加熟练文本分类的一些代码coding和知识——增加熟练度,也可以给大家分享一下。

来看本文的transformers项目中的bert分类器进行fine-tune作文本分类的任务,在这个项目里面已经把全部的代码写好了,我们只需要把我们的文本处理成项目能够识别和读取的形式。简单的分析一下,分类任务的代码:

主要的分类任务的代码是在run_glue.py文件中,这里面定义了main函数,命令行参数接收器,模型的加载和调用,模型的训练以及验证,和数据读取以及处理的功能模块调用。

我们看一下这里调用的分类模型,代码是这样的:


   
    
    
  1. model = AutoModelForSequenceClassification.from_pretrained(
  2. args.model_name_or_path,
  3. from_tf=bool( ".ckpt" in args.model_name_or_path),
  4. config=config,
  5. cache_dir=args.cache_dir,
  6. )

其实最终这里的AutoModelForSequenceClassification.from_pretrained()调用的是modeling_bert.py中的BertForSequenceClassification类,它就是具体的分类器实现:


   
    
    
  1. class BertForSequenceClassification(BertPreTrainedModel):
  2. def __init__(self, config):
  3. super().__init__(config)
  4. self.num_labels = config.num_labels
  5. self.bert = BertModel(config)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. self.classifier = nn.Linear(config.hidden_size, self.config.num_labels)
  8. self.init_weights()
  9. def forward(
  10. self,
  11. input_ids=None,
  12. attention_mask=None,
  13. token_type_ids=None,
  14. position_ids=None,
  15. head_mask=None,
  16. inputs_embeds=None,
  17. labels=None,
  18. ):
  19. outputs = self.bert(
  20. input_ids,
  21. attention_mask=attention_mask,
  22. token_type_ids=token_type_ids,
  23. position_ids=position_ids,
  24. head_mask=head_mask,
  25. inputs_embeds=inputs_embeds,
  26. )
  27. pooled_output = outputs[ 1]
  28. pooled_output = self.dropout(pooled_output)
  29. logits = self.classifier(pooled_output)
  30. outputs = (logits,) + outputs[ 2:] # add hidden states and attention if they are here
  31. if labels is not None:
  32. if self.num_labels == 1:
  33. # We are doing regression
  34. loss_fct = MSELoss()
  35. loss = loss_fct(logits.view( -1), labels.view( -1))
  36. else:
  37. loss_fct = CrossEntropyLoss()
  38. loss = loss_fct(logits.view( -1, self.num_labels), labels.view( -1))
  39. outputs = (loss,) + outputs
  40. return outputs

模型调用了BertModel,然后做使用nn.Linear(config.hidden_size, self.config.num_labels)做分类,loss函数是常用的交叉熵损失函数。以上就是分类器的一些简单的分析。 我们要做的工作就是仿照项目里的代码写一个任务处理器:

项目目录结构:transformerer_local/data/glue.py,注意这里的transformerer_local原本应该是transformerer,我这里已经做了修改。在glue.py添加上我们的分类任务代码——添加一个读取文件中的文本然后,然后把每条数据序列化成Example,注意get_labels()函数,把自己的类别数目实现过来,代码如下:


   
    
    
  1. class MyownProcessor(DataProcessor):
  2. """Processor for the CoLA data set (GLUE version)."""
  3. def get_example_from_tensor_dict(self, tensor_dict):
  4. """See base class."""
  5. return InputExample(
  6. tensor_dict[ "idx"].numpy(),
  7. tensor_dict[ "sentence"].numpy().decode( "utf-8"),
  8. None,
  9. str(tensor_dict[ "label"].numpy()),
  10. )
  11. def get_train_examples(self, data_dir):
  12. """See base class."""
  13. return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
  14. def get_dev_examples(self, data_dir):
  15. """See base class."""
  16. return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
  17. def get_predict_examples(self, data_dir):
  18. return self._create_examples(self._read_tsv(os.path.join(data_dir, "test.tsv")), "predict")
  19. def get_labels(self):
  20. """See base class."""
  21. return [ "0", "1", "2", "3", "4", "5", "6", "7"]
  22. def _create_examples(self, lines, set_type):
  23. """Creates examples for the training and dev sets."""
  24. examples = []
  25. for (i, line) in enumerate(lines):
  26. guid = "%s-%s" % (set_type, i)
  27. if len(line)== 2:
  28. text_a = line[ 0]
  29. label = line[ 1]
  30. examples.append(InputExample(guid=guid, text_a=text_a, text_b= None, label=label))
  31. else:
  32. print(line)
  33. return examples

同时在验证的时候,对应评价指标函数,我们这里不是binary,计算f1_score的时候要采用其他的策略:

transformerer_local/data/metrics/__init__.py,注意这里的transformerer_local原本应该是transformerer,添加内容:


   
    
    
  1. #添加多分类评价函数
  2. def acc_and_f1_multi(preds, labels):
  3. acc = simple_accuracy(preds, labels)
  4. f1 = f1_score(y_true=labels, y_pred=preds,average= 'micro')
  5. return {
  6. "acc": acc,
  7. "f1": f1,
  8. "acc_and_f1": (acc + f1) / 2,
  9. }
  10. def glue_compute_metrics(task_name, preds, labels):
  11. assert len(preds) == len(labels)
  12. if task_name == "cola":
  13. return { "mcc": matthews_corrcoef(labels, preds)}
  14. elif task_name == "sst-2":
  15. return { "acc": simple_accuracy(preds, labels)}
  16. elif task_name == "mrpc":
  17. return acc_and_f1(preds, labels)
  18. elif task_name == "sts-b":
  19. return pearson_and_spearman(preds, labels)
  20. elif task_name == "qqp":
  21. return acc_and_f1(preds, labels)
  22. elif task_name == "mnli":
  23. return { "acc": simple_accuracy(preds, labels)}
  24. elif task_name == "mnli-mm":
  25. return { "acc": simple_accuracy(preds, labels)}
  26. elif task_name == "qnli":
  27. return { "acc": simple_accuracy(preds, labels)}
  28. elif task_name == "rte":
  29. return { "acc": simple_accuracy(preds, labels)}
  30. elif task_name == "wnli":
  31. return { "acc": simple_accuracy(preds, labels)}
  32. elif task_name == "hans":
  33. return { "acc": simple_accuracy(preds, labels)}
  34. #添加我们的多分类任务调用函数
  35. elif task_name == "myown":
  36. return acc_and_f1_multi(preds, labels)
  37. else:
  38. raise KeyError(task_name)

添加内容就在注释部分。

OK,现在代码部分已经做好了,接下来就是数据部分了。直接上数据:

数据截图部分就是上面这样的,把pat_summary和ipc_class属性提取出来,这里的数据质量比较好,然后只需要把超级长的文本去掉(长度大于510的):

数据长度分布直方图,发现几乎全部都是小于510的长度,只有少部分比较长,只有128条,这里数据集总规模是24.8W条,可以把这少部分的直接去掉。然后把数据分割成训练集和测试集比例(8:2),保存为tsv格式。

接下来就是直接进行训练了,编写如下命令行,在train_glue_classification.sh文件中:


   
    
    
  1. export TASK_NAME=myown
  2. python -W ignore ./examples/run_glue.py \
  3. --model_type bert \
  4. --model_name_or_path ./pretrain_model/Chinese-BERT-wwm/ \
  5. --task_name $TASK_NAME \
  6. __do_train \
  7. --do_eval \
  8. --data_dir ./data_set/patent/ \
  9. --max_seq_length 510 \
  10. --per_gpu_eval_batch_size= 8 \
  11. --per_gpu_train_batch_size= 8 \
  12. --per_gpu_predict_batch_size= 48 \
  13. --learning_rate 2e-5 \
  14. --num_train_epochs 5.0 \
  15. --output_dir ./output/

直接在终端上运行这个sh文件,bash train_glue_classification.sh。注意这里的训练显卡显存得11G以上,不然跑步起来,batch_size不能太大。训练过程中,一个epoch大概时间3.5小时,所以时间还要蛮久的。最后给出结果:

可以看到acc=0.8508,一个8分类的任务准确率85%粗略一看还能接受。如果要详细的分析,可以把每一类的准确率和召回率给弄出来,或者分析一下ROC,对模型的性能做详细的分析,这里不做过多讨论。另外关于这个模型的优化,怎么提高准确率,也不做考虑。

小结:以上就是直接使用transformers项目中的bert分类器拿过来进行fine-tune,做文本分类,其实代码都写好了,我们只需要简单的修改一下代码和配置,就能很快的训练好自己的分类器。

四、Bert模型难点总结

其实关于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知识点

目录

一、bert模型简介

bert与训练的流程:

bert模型的输入

二、huggingface的bert源码浅析

bert提取文本词向量

BertModel代码阅读

BertEmbedding子模型

BertEncoder

BertAttention

BertIntermediate

BertOutput(config)

BertPooler()

三、Bert文本分类任务实战

四、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任务。

bert与训练的流程:

这个过程包括两个任务,一个是Masked Language Model(遮掩语言模型),另外一个是Next Sentence Prediction(下一句预测)。

Masked Language Model(遮掩语言模型)可以理解为是做完型填空,把语料中15%的词遮掩掉,来学习词和词之间的一些规律;

Next Sentence Prediction就是学习语料中上下文中2个句子之间的关系规律。

通过这2个阶段任务的学习,bert就会把文本的语法和语义信息学习到。bert模型中的self-attention机制可以使用文本其他的词来增强目标词的语义表示,这也是bert模型吊打其他模型的一个关键原因。

bert模型的输入

bert模型的输入可以是一个句子或者句子对,代码层面来说,就是输入了句子或者句子对对应的3个向量。它们分别是token embedding,segment embedding和position embedding,具体的含义:

token embedding:句子的词向量

segment embedding:是那个句子的0和1

position embedding:位置向量,指明每个字在句中的位置。

关于position embedding这里有两种求法,一种是有相应的三角函数公式得出的,这种是绝对向量;还有一种是学习得到的,这种是相对向量。具体形式如下:

二、huggingface的bert源码浅析

关于bert模型的使用,我主要是使用huggingface的transformer库来调用bert和使用——一般是直接用来bert来获取词向量。这里就bert的使用和huggingface中的源码进行一些解读。

bert提取文本词向量

首先看一段简单的代码,使用huggingface的transformers(其实就是实现的bert)来提取句——我爱武汉!我爱中国!——的向量。代码如下:


   
  
  
  1. from transformers import BertModel,BertTokenizer,BertConfig
  2. import torch
  3. config = BertConfig.from_pretrained( 'pretrain_model/chinese-bert-wwm') #第一步加载模型配置文件
  4. bertmodel = BertModel.from_pretrained( 'pretrain_model/chinese-bert-wwm',config=config) #第二步初始化模型,并加载权重
  5. # print('***************************bertmodel***************************')
  6. tokenizer = BertTokenizer.from_pretrained( 'pretrain_model/chinese-bert-wwm') #第三步加载tokenizer
  7. text1 = '我爱武汉!我爱中国!'
  8. tokeniz_text1 = tokenizer.tokenize(text1)
  9. # print(tokeniz_text1)
  10. # print('tokeniz_text1:',len(tokeniz_text1))
  11. indexed_tokens_1 = tokenizer.convert_tokens_to_ids(tokeniz_text1)
  12. print( 'len(indexed_tokens_1):',len(indexed_tokens_1))
  13. print(indexed_tokens_1)
  14. input_ids_1 = indexed_tokens_1
  15. # print(indexed_tokens_1)
  16. # print('indexed_tokens_1:',len(indexed_tokens_1))
  17. segments_ids_1 = [ 0]*len(input_ids_1) #其实这个输入可以不用的,因为是单句的原因
  18. input_masks_1 = [ 1]*len(input_ids_1) #其实这个输入可以不用的,因为是单句的原因
  19. input_ids_1_tensor = torch.tensor([input_ids_1])
  20. vector1,pooler1 = bertmodel(input_ids_1_tensor) #应该是输入3个向量的,但是单句情况下,它自会自己做判断,然后自动生成对应的segments_ids和input_masks向量
  21. #这里的输出最后一层的last_hidden_state和最后一层首个token的hidden-state
  22. text2 = '[CLS]我爱武汉!我爱中国![SEP]'
  23. tokeniz_text2 = tokenizer.tokenize(text2)
  24. indexed_tokens_2 = tokenizer.convert_tokens_to_ids(tokeniz_text2)
  25. input_ids_2 = indexed_tokens_2
  26. segments_ids_2 = [ 0]*len(input_ids_2) #其实这个输入可以不用的,因为是单句的原因
  27. input_masks_2 = [ 1]*len(input_ids_2) #其实这个输入可以不用的,因为是单句的原因
  28. input_ids_2_tensor = torch.tensor([input_ids_2])
  29. vector2,pooler2 = bertmodel(input_ids_2_tensor)
  30. print( 'pooler2:',pooler2)
  31. print( 'vector2[:,0:1,:]:',vector2[:, 0: 1,:])
  32. text1_encode = tokenizer.encode(text1,add_special_tokens= True)
  33. print( 'len(text1_encode):',len(text1_encode))
  34. print( 'text1_encode:',text1_encode)
  35. #这里的text1_encode和indexed_tokens_2是一模一样的,encode()函数会自动为文本添加特殊字符[UNK][CLS][SEP][MASK]等

以上代码是基于pytorch来实现的,同时应用到了transoformers库!可以看到bert模型的使用非常简单!

第一步,初始化bert模型和加载权重。这个步骤中,首先加载配置文件、然后加载bert模型和载入权重。

第二步,对输入文本做词表映射,形成初始词向量。

第三步,输入喂入bert模型中得到输入文本的结果向量。

文中是bert模型的输入我这里只给出了一个那就是input_ids,另外的2个没有给出。这里的原因就是这里是单个句子,模型内部可以对另外2个输入做自动添加的处理——并不是没有,这点要注意到。

这里有个疑问因为bert的输入文本得添加一个[cls]特殊字符,我认为最后的输出lsat_hidden_state中的lsat_hidden_state[:,0:1,:]应该和pooler结果是一样的,可是这里是不一样的,有点理解的偏差,不知道为什么。

BertModel代码阅读

通过上文中的代码,大致可以知道怎么调用一些API来创建bert模型和应用它。那么huggingface中是怎么实现BertModel的这个也是比较重要的,这里我们就好好阅读以下其中关于BertModel实现的代码。看一张transformers项目文件结构图:

这么面封装了很多模型的构建,我们主要是阅读modeling_bert.py文件,它在里面详细的展示了如何构建一个Bert模型的:


   
  
  
  1. class BertModel(BertPreTrainedModel):
  2. """
  3. .......
  4. """
  5. def __init__(self, config):
  6. super().__init__(config)
  7. self.config = config
  8. self.embeddings = BertEmbeddings(config)
  9. self.encoder = BertEncoder(config)
  10. self.pooler = BertPooler(config)
  11. self.init_weights()
  12. def get_input_embeddings(self):
  13. return self.embeddings.word_embeddings
  14. def set_input_embeddings(self, value):
  15. self.embeddings.word_embeddings = value
  16. def _prune_heads(self, heads_to_prune):
  17. """ Prunes heads of the model.
  18. heads_to_prune: dict of {layer_num: list of heads to prune in this layer}
  19. See base class PreTrainedModel
  20. """
  21. for layer, heads in heads_to_prune.items():
  22. self.encoder.layer[layer].attention.prune_heads(heads)
  23. @add_start_docstrings_to_callable(BERT_INPUTS_DOCSTRING)
  24. def forward(
  25. self,
  26. input_ids=None,
  27. attention_mask=None,
  28. token_type_ids=None,
  29. position_ids=None,
  30. head_mask=None,
  31. inputs_embeds=None,
  32. encoder_hidden_states=None,
  33. encoder_attention_mask=None,
  34. ):
  35. r""".......
  36. """
  37. if input_ids is not None and inputs_embeds is not None:
  38. raise ValueError( "You cannot specify both input_ids and inputs_embeds at the same time")
  39. elif input_ids is not None:
  40. input_shape = input_ids.size()
  41. elif inputs_embeds is not None:
  42. input_shape = inputs_embeds.size()[: -1]
  43. else:
  44. raise ValueError( "You have to specify either input_ids or inputs_embeds")
  45. device = input_ids.device if input_ids is not None else inputs_embeds.device
  46. if attention_mask is None:
  47. attention_mask = torch.ones(input_shape, device=device)
  48. if token_type_ids is None:
  49. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  50. # We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
  51. # ourselves in which case we just need to make it broadcastable to all heads.
  52. extended_attention_mask: torch.Tensor = self.get_extended_attention_mask(
  53. attention_mask, input_shape, self.device
  54. )
  55. # If a 2D ou 3D attention mask is provided for the cross-attention
  56. # we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
  57. if self.config.is_decoder and encoder_hidden_states is not None:
  58. encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
  59. encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
  60. if encoder_attention_mask is None:
  61. encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
  62. encoder_extended_attention_mask = self.invert_attention_mask(encoder_attention_mask)
  63. else:
  64. encoder_extended_attention_mask = None
  65. # Prepare head mask if needed
  66. # 1.0 in head_mask indicate we keep the head
  67. # attention_probs has shape bsz x n_heads x N x N
  68. # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
  69. # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
  70. head_mask = self.get_head_mask(head_mask, self.config.num_hidden_layers)
  71. embedding_output = self.embeddings(
  72. input_ids=input_ids, position_ids=position_ids, token_type_ids=token_type_ids, inputs_embeds=inputs_embeds
  73. )
  74. encoder_outputs = self.encoder(
  75. embedding_output,
  76. attention_mask=extended_attention_mask,
  77. head_mask=head_mask,
  78. encoder_hidden_states=encoder_hidden_states,
  79. encoder_attention_mask=encoder_extended_attention_mask,
  80. )
  81. sequence_output = encoder_outputs[ 0]
  82. pooled_output = self.pooler(sequence_output)
  83. outputs = (sequence_output, pooled_output,) + encoder_outputs[
  84. 1:
  85. ] # add hidden_states and attentions if they are here
  86. return outputs # sequence_output, pooled_output, (hidden_states), (attentions)

以上就是BertModel的全部代码,可以看到在BertModel类中,首先__init__()函数中定义了模型的基本模块,然后在forward()函数里面使用这些结构模块具体实现了Bert的逻辑。


   
  
  
  1. def __init__(self, config):
  2. super().__init__(config)
  3. self.config = config
  4. self.embeddings = BertEmbeddings(config)
  5. self.encoder = BertEncoder(config)
  6. self.pooler = BertPooler(config)
  7. self.init_weights()

__init__()函数中定义的模型模块主要是3个,分别是BertEmbedding、BertEncoder和BertPooler。然后在forward(),输入顺序的经过这3个模块的处理就得到了我们要的结果——对应文本的bert向量。

下面来阅读forward():


   
  
  
  1. if input_ids is not None and inputs_embeds is not None:
  2. raise ValueError( "You cannot specify both input_ids and inputs_embeds at the same time")
  3. elif input_ids is not None:
  4. input_shape = input_ids.size()
  5. elif inputs_embeds is not None:
  6. input_shape = inputs_embeds.size()[: -1]
  7. else:
  8. raise ValueError( "You have to specify either input_ids or inputs_embeds")
  9. device = input_ids.device if input_ids is not None else inputs_embeds.device
  10. if attention_mask is None:
  11. attention_mask = torch.ones(input_shape, device=device)
  12. if token_type_ids is None:
  13. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  14. # We can provide a self-attention mask of dimensions [batch_size, from_seq_length, to_seq_length]
  15. # ourselves in which case we just need to make it broadcastable to all heads.
  16. if attention_mask.dim() == 3:
  17. extended_attention_mask = attention_mask[:, None, :, :]
  18. elif attention_mask.dim() == 2:
  19. # Provided a padding mask of dimensions [batch_size, seq_length]
  20. # - if the model is a decoder, apply a causal mask in addition to the padding mask
  21. # - if the model is an encoder, make the mask broadcastable to [batch_size, num_heads, seq_length, seq_length]
  22. if self.config.is_decoder:
  23. batch_size, seq_length = input_shape
  24. seq_ids = torch.arange(seq_length, device=device)
  25. causal_mask = seq_ids[ None, None, :].repeat(batch_size, seq_length, 1) <= seq_ids[ None, :, None]
  26. causal_mask = causal_mask.to(
  27. attention_mask.dtype
  28. ) # causal and attention masks must have same type with pytorch version < 1.3
  29. extended_attention_mask = causal_mask[:, None, :, :] * attention_mask[:, None, None, :]
  30. else:
  31. extended_attention_mask = attention_mask[:, None, None, :]
  32. else:
  33. raise ValueError(
  34. "Wrong shape for input_ids (shape {}) or attention_mask (shape {})".format(
  35. input_shape, attention_mask.shape
  36. )
  37. )
  38. # Since attention_mask is 1.0 for positions we want to attend and 0.0 for
  39. # masked positions, this operation will create a tensor which is 0.0 for
  40. # positions we want to attend and -10000.0 for masked positions.
  41. # Since we are adding it to the raw scores before the softmax, this is
  42. # effectively the same as removing these entirely.
  43. extended_attention_mask = extended_attention_mask.to(dtype=next(self.parameters()).dtype) # fp16 compatibility
  44. extended_attention_mask = ( 1.0 - extended_attention_mask) * -10000.0
  45. # If a 2D ou 3D attention mask is provided for the cross-attention
  46. # we need to make broadcastabe to [batch_size, num_heads, seq_length, seq_length]
  47. if self.config.is_decoder and encoder_hidden_states is not None:
  48. encoder_batch_size, encoder_sequence_length, _ = encoder_hidden_states.size()
  49. encoder_hidden_shape = (encoder_batch_size, encoder_sequence_length)
  50. if encoder_attention_mask is None:
  51. encoder_attention_mask = torch.ones(encoder_hidden_shape, device=device)
  52. if encoder_attention_mask.dim() == 3:
  53. encoder_extended_attention_mask = encoder_attention_mask[:, None, :, :]
  54. elif encoder_attention_mask.dim() == 2:
  55. encoder_extended_attention_mask = encoder_attention_mask[:, None, None, :]
  56. else:
  57. raise ValueError(
  58. "Wrong shape for encoder_hidden_shape (shape {}) or encoder_attention_mask (shape {})".format(
  59. encoder_hidden_shape, encoder_attention_mask.shape
  60. )
  61. )
  62. encoder_extended_attention_mask = encoder_extended_attention_mask.to(
  63. dtype=next(self.parameters()).dtype
  64. ) # fp16 compatibility
  65. encoder_extended_attention_mask = ( 1.0 - encoder_extended_attention_mask) * -10000.0
  66. else:
  67. encoder_extended_attention_mask = None
  68. # Prepare head mask if needed
  69. # 1.0 in head_mask indicate we keep the head
  70. # attention_probs has shape bsz x n_heads x N x N
  71. # input head_mask has shape [num_heads] or [num_hidden_layers x num_heads]
  72. # and head_mask is converted to shape [num_hidden_layers x batch x num_heads x seq_length x seq_length]
  73. if head_mask is not None:
  74. if head_mask.dim() == 1:
  75. head_mask = head_mask.unsqueeze( 0).unsqueeze( 0).unsqueeze( -1).unsqueeze( -1)
  76. head_mask = head_mask.expand(self.config.num_hidden_layers, -1, -1, -1, -1)
  77. elif head_mask.dim() == 2:
  78. head_mask = (
  79. head_mask.unsqueeze( 1).unsqueeze( -1).unsqueeze( -1)
  80. ) # We can specify head_mask for each layer
  81. head_mask = head_mask.to(
  82. dtype=next(self.parameters()).dtype
  83. ) # switch to fload if need + fp16 compatibility
  84. else:
  85. 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。

下面具体看核心的部分。

上面把输入做一些预处理后,使得输入都合法,然后就可以喂入模型的功能模块中。第一个就是


   
  
  
  1. embedding_output = self.embeddings(
  2. input_ids=input_ids, position_ids=position_ids, token_type_ids=token_type_ids, inputs_embeds=inputs_embeds
  3. )

BertEmbedding子模型

其中的self.embeddings()就是__inti__()的BertEmbeddings(config)模块,它可以看做是一个起embedding功能作用的子模型,具体代码:


   
  
  
  1. class BertEmbeddings(nn.Module):
  2. """Construct the embeddings from word, position and token_type embeddings.
  3. """
  4. def __init__(self, config):
  5. super().__init__()
  6. self.word_embeddings = nn.Embedding(config.vocab_size, config.hidden_size, padding_idx= 0)
  7. self.position_embeddings = nn.Embedding(config.max_position_embeddings, config.hidden_size)
  8. self.token_type_embeddings = nn.Embedding(config.type_vocab_size, config.hidden_size)
  9. # self.LayerNorm is not snake-cased to stick with TensorFlow model variable name and be able to load
  10. # any TensorFlow checkpoint file
  11. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  12. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  13. def forward(self, input_ids=None, token_type_ids=None, position_ids=None, inputs_embeds=None):
  14. if input_ids is not None:
  15. input_shape = input_ids.size()
  16. else:
  17. input_shape = inputs_embeds.size()[: -1]
  18. seq_length = input_shape[ 1]
  19. device = input_ids.device if input_ids is not None else inputs_embeds.device
  20. if position_ids is None:
  21. position_ids = torch.arange(seq_length, dtype=torch.long, device=device)
  22. position_ids = position_ids.unsqueeze( 0).expand(input_shape)
  23. if token_type_ids is None:
  24. token_type_ids = torch.zeros(input_shape, dtype=torch.long, device=device)
  25. if inputs_embeds is None:
  26. inputs_embeds = self.word_embeddings(input_ids)
  27. position_embeddings = self.position_embeddings(position_ids)
  28. token_type_embeddings = self.token_type_embeddings(token_type_ids)
  29. embeddings = inputs_embeds + position_embeddings + token_type_embeddings
  30. embeddings = self.LayerNorm(embeddings)
  31. embeddings = self.dropout(embeddings)
  32. 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中,代码如下,参数都很清晰明确:


   
  
  
  1. encoder_outputs = self.encoder(
  2. embedding_output,
  3. attention_mask=extended_attention_mask,
  4. head_mask=head_mask,
  5. encoder_hidden_states=encoder_hidden_states,
  6. encoder_attention_mask=encoder_extended_attention_mask,
  7. )

这里的self.encoder同样是__init__()中的BertEncoder(config)模型,全部代码如下:


   
  
  
  1. class BertEncoder(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.output_attentions = config.output_attentions
  5. self.output_hidden_states = config.output_hidden_states
  6. self.layer = nn.ModuleList([BertLayer(config) for _ in range(config.num_hidden_layers)])
  7. def forward(
  8. self,
  9. hidden_states,
  10. attention_mask=None,
  11. head_mask=None,
  12. encoder_hidden_states=None,
  13. encoder_attention_mask=None,
  14. ):
  15. all_hidden_states = ()
  16. all_attentions = ()
  17. for i, layer_module in enumerate(self.layer):
  18. if self.output_hidden_states:
  19. all_hidden_states = all_hidden_states + (hidden_states,)
  20. layer_outputs = layer_module(
  21. hidden_states, attention_mask, head_mask[i], encoder_hidden_states, encoder_attention_mask
  22. )
  23. hidden_states = layer_outputs[ 0]
  24. if self.output_attentions:
  25. all_attentions = all_attentions + (layer_outputs[ 1],)
  26. # Add last layer
  27. if self.output_hidden_states:
  28. all_hidden_states = all_hidden_states + (hidden_states,)
  29. outputs = (hidden_states,)
  30. if self.output_hidden_states:
  31. outputs = outputs + (all_hidden_states,)
  32. if self.output_attentions:
  33. outputs = outputs + (all_attentions,)
  34. 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循环做如下操作:


   
  
  
  1. for i, layer_module in enumerate(self.layer):
  2. if self.output_hidden_states:
  3. all_hidden_states = all_hidden_states + (hidden_states,)
  4. layer_outputs = layer_module(
  5. hidden_states, attention_mask, head_mask[i], encoder_hidden_states, encoder_attention_mask
  6. )
  7. hidden_states = layer_outputs[ 0]
  8. if self.output_attentions:
  9. 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的实现:


   
  
  
  1. class BertLayer(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.attention = BertAttention(config)
  5. self.is_decoder = config.is_decoder
  6. if self.is_decoder:
  7. self.crossattention = BertAttention(config)
  8. self.intermediate = BertIntermediate(config)
  9. self.output = BertOutput(config)

可以看到BertLayer是由BertAttention()、BertIntermediate()和BertOutput()构成。它的forward()是比较简单的,没有什么奇特的操作,都是顺序的把输入经过BertAttention()、BertIntermediate()和BertOutput()这些子模型。这里主要来看看这些子模型的实现:

BertAttention

这里它又嵌套了一层,由BertSelfAttention()和BertSelfOutput()子模型组成!

这里马上就看到self-attention机制的实现了!感觉好激动!——Self-Attention则利用了Attention机制,计算每个单词与其他所有单词之间的关联(说实话理解的不是很透彻!)


   
  
  
  1. class BertSelfAttention(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. if config.hidden_size % config.num_attention_heads != 0 and not hasattr(config, "embedding_size"):
  5. raise ValueError(
  6. "The hidden size (%d) is not a multiple of the number of attention "
  7. "heads (%d)" % (config.hidden_size, config.num_attention_heads)
  8. )
  9. self.output_attentions = config.output_attentions
  10. self.num_attention_heads = config.num_attention_heads
  11. self.attention_head_size = int(config.hidden_size / config.num_attention_heads)
  12. self.all_head_size = self.num_attention_heads * self.attention_head_size
  13. self.query = nn.Linear(config.hidden_size, self.all_head_size)
  14. self.key = nn.Linear(config.hidden_size, self.all_head_size)
  15. self.value = nn.Linear(config.hidden_size, self.all_head_size)
  16. self.dropout = nn.Dropout(config.attention_probs_dropout_prob)
  17. def transpose_for_scores(self, x):
  18. new_x_shape = x.size()[: -1] + (self.num_attention_heads, self.attention_head_size)
  19. x = x.view(*new_x_shape)
  20. return x.permute( 0, 2, 1, 3)
  21. def forward(
  22. self,
  23. hidden_states,
  24. attention_mask=None,
  25. head_mask=None,
  26. encoder_hidden_states=None,
  27. encoder_attention_mask=None,
  28. ):
  29. mixed_query_layer = self.query(hidden_states)
  30. # If this is instantiated as a cross-attention module, the keys
  31. # and values come from an encoder; the attention mask needs to be
  32. # such that the encoder's padding tokens are not attended to.
  33. if encoder_hidden_states is not None:
  34. mixed_key_layer = self.key(encoder_hidden_states)
  35. mixed_value_layer = self.value(encoder_hidden_states)
  36. attention_mask = encoder_attention_mask
  37. else:
  38. mixed_key_layer = self.key(hidden_states)
  39. mixed_value_layer = self.value(hidden_states)
  40. query_layer = self.transpose_for_scores(mixed_query_layer)
  41. key_layer = self.transpose_for_scores(mixed_key_layer)
  42. value_layer = self.transpose_for_scores(mixed_value_layer)
  43. # Take the dot product between "query" and "key" to get the raw attention scores.
  44. attention_scores = torch.matmul(query_layer, key_layer.transpose( -1, -2))
  45. attention_scores = attention_scores / math.sqrt(self.attention_head_size)
  46. if attention_mask is not None:
  47. # Apply the attention mask is (precomputed for all layers in BertModel forward() function)
  48. attention_scores = attention_scores + attention_mask
  49. # Normalize the attention scores to probabilities.
  50. attention_probs = nn.Softmax(dim= -1)(attention_scores)
  51. # This is actually dropping out entire tokens to attend to, which might
  52. # seem a bit unusual, but is taken from the original Transformer paper.
  53. attention_probs = self.dropout(attention_probs)
  54. # Mask heads if we want to
  55. if head_mask is not None:
  56. attention_probs = attention_probs * head_mask
  57. context_layer = torch.matmul(attention_probs, value_layer)
  58. context_layer = context_layer.permute( 0, 2, 1, 3).contiguous()
  59. new_context_layer_shape = context_layer.size()[: -2] + (self.all_head_size,)
  60. context_layer = context_layer.view(*new_context_layer_shape)
  61. outputs = (context_layer, attention_probs) if self.output_attentions else (context_layer,)
  62. return outputs

阅读代码之前先回顾一下,self-attention的公式是什么样的,公式编辑比较麻烦直接上2个图,都是来自Attention机制详解(二)——Self-Attention与Transformer文章中:

首先定义Q、K、V

然后应用到公式中:

以上就是单个头的self-attention的公式,多头的话就可以计算多次,然后在合并起来。这里就可以应用到矩阵运算了,还要注意的点就是Q、K、V的学习参数都是共享的——(要去验证),代码对应的就是:


   
  
  
  1. self.query = nn.Linear(config.hidden_size, self.all_head_size)
  2. self.key = nn.Linear(config.hidden_size, self.all_head_size)
  3. self.value = nn.Linear(config.hidden_size, self.all_head_size)
  4. #注意这里的nn.Linear包含的学习参数一个是权重参数weights一个是偏置参数bias
  5. #而且这里的query、key以及value它们的参数不一样,也就是并不共享参数

参数都包含在nn.Linear中了,这里的self.query对应的是12个头的self-attention机制对应的Q的学习参数模型,当然query、key以及value它们的参数不一样,也就是并不共享参数。

那么在forward()中是如何实现的呢?


   
  
  
  1. mixed_query_layer = self.query(hidden_states) #计算Q
  2. if encoder_hidden_states is not None:
  3. mixed_key_layer = self.key(encoder_hidden_states)
  4. mixed_value_layer = self.value(encoder_hidden_states)
  5. attention_mask = encoder_attention_mask
  6. else:
  7. mixed_key_layer = self.key(hidden_states) #计算K
  8. mixed_value_layer = self.value(hidden_states) #计算V
  9. #做转置操作——这有点特殊:mixed_query_layer[batch_size,sequence_length,hidden_states]
  10. #query_layer的维度:[batch_size,num_attention_heads,sequence_length,attention_head_size]
  11. query_layer = self.transpose_for_scores(mixed_query_layer)
  12. key_layer = self.transpose_for_scores(mixed_key_layer)
  13. value_layer = self.transpose_for_scores(mixed_value_layer)
  14. #Q和K做点积
  15. attention_scores = torch.matmul(query_layer, key_layer.transpose( -1, -2))
  16. #Q和K做点积后然后除以根号下多头主力的尺寸
  17. attention_scores = attention_scores / math.sqrt(self.attention_head_size)
  18. if attention_mask is not None:
  19. # Apply the attention mask is (precomputed for all layers in BertModel forward() function)
  20. attention_scores = attention_scores + attention_mask
  21. # Normalize the attention scores to probabilities.
  22. #做softmax操作,归一化
  23. attention_probs = nn.Softmax(dim= -1)(attention_scores)
  24. # This is actually dropping out entire tokens to attend to, which might
  25. # seem a bit unusual, but is taken from the original Transformer paper.
  26. attention_probs = self.dropout(attention_probs)
  27. # Mask heads if we want to
  28. if head_mask is not None:
  29. attention_probs = attention_probs * head_mask
  30. #中间结果和V做点积,得到最终结果——注意力得分也就是公式中的Z
  31. 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():


   
  
  
  1. class BertSelfOutput(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.hidden_size)
  5. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. def forward(self, hidden_states, input_tensor):
  8. hidden_states = self.dense(hidden_states)
  9. hidden_states = self.dropout(hidden_states)
  10. hidden_states = self.LayerNorm(hidden_states + input_tensor)
  11. return hidden_states

以上BertSelfOutput()代码很简单,把self-attention输出的结果经过线性模型和dropout操作,最后做层归一化。到这里就跳出了BertAttention()模型,然后就进入中间层BertIntermediate()。

BertIntermediate

BertIntermediate()作为中间层代码很简单:


   
  
  
  1. class BertIntermediate(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.intermediate_size)
  5. if isinstance(config.hidden_act, str):
  6. self.intermediate_act_fn = ACT2FN[config.hidden_act]
  7. else:
  8. self.intermediate_act_fn = config.hidden_act
  9. def forward(self, hidden_states):
  10. hidden_states = self.dense(hidden_states)
  11. hidden_states = self.intermediate_act_fn(hidden_states)
  12. return hidden_states

经过一个全连接层,由于config.hidden_size<config.intermediate_size,这里的Linear把特征空间变大了,然后进过了gelu激活函数,增加了特征的非线性性。

BertOutput(config)

跳出BertIntermediate()作为中间层后,就进入了BertOutput(config)模型,这个是BertLayer()模型的最后一个子模型。


   
  
  
  1. class BertOutput(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.intermediate_size, config.hidden_size)
  5. self.LayerNorm = BertLayerNorm(config.hidden_size, eps=config.layer_norm_eps)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. def forward(self, hidden_states, input_tensor):
  8. hidden_states = self.dense(hidden_states)
  9. hidden_states = self.dropout(hidden_states)
  10. hidden_states = self.LayerNorm(hidden_states + input_tensor)
  11. return hidden_states

经过线性模型和dropout操作,最后做层归一化,把特征空间又缩小回来了。最后输出一个hidden_states,这里就是一个BertLayer()的输出了。

BertPooler()

然后经历了12个BertLayer()的操作,一层一层的变换,最后得出的outputs进入BertPooler():


   
  
  
  1. sequence_output = encoder_outputs[ 0]
  2. pooled_output = self.pooler(sequence_output)

pooler代码如下:


   
  
  
  1. class BertPooler(nn.Module):
  2. def __init__(self, config):
  3. super().__init__()
  4. self.dense = nn.Linear(config.hidden_size, config.hidden_size)
  5. self.activation = nn.Tanh()
  6. def forward(self, hidden_states):
  7. # We "pool" the model by simply taking the hidden state corresponding
  8. # to the first token.
  9. first_token_tensor = hidden_states[:, 0]
  10. pooled_output = self.dense(first_token_tensor)
  11. pooled_output = self.activation(pooled_output)
  12. return pooled_output
  13. #以上的pooler作用要具体的去调试hidden_states的shape。

由代码可知这个pooler的功能就是把last_hidden_states的第二维的第一维也就是文本对应的第一个;。。。、。。

以上差不多就是BertModel的具体实现,由于这个模型的代码嵌套调用过多,可能理解起来有一定的困惑,那么接下来就需要一个图片来可视化理解。上图:

上图是huggingface中的BertModel的结构流程图(简图,有很多疏漏的地方勿怪!),bertModel的输入和基本的子模型以及数据的流向都显示出来了,对应着代码理解起来更加方便。黄色的图形就是torch中的基本函数模块(这里的Q、K和V不是),其他颜色的矩形就是模型,平行四边形就是数据。

以上就是对BertModel实现代码的简单解析,里面涉及到很多的细节:不同模型模块的参数以及它们的维度信息,还有就是变量的维度变化,以及每个模型模块的具体作用和意义,没有去深究,读者有精力的话可以自己去深究。

三、Bert文本分类任务实战

        这里我们要写一个使用transformers项目中的分类器来实现一个简单的文本分类任务,这里我们没有自己取重写Dataloader以及模型的训练,就是直接把transformers项目中的bert分类器拿过来进行fine-tune,工作量少,结果也比较好!当然也可以完全自己实现(前面也自己实现过一个基于bert的句子分类的任务——使用bert模型做句子分类,有兴趣的可以移步),后续有时间的话可以写一个各个模型文本分类任务的比较博客,更加熟练文本分类的一些代码coding和知识——增加熟练度,也可以给大家分享一下。

来看本文的transformers项目中的bert分类器进行fine-tune作文本分类的任务,在这个项目里面已经把全部的代码写好了,我们只需要把我们的文本处理成项目能够识别和读取的形式。简单的分析一下,分类任务的代码:

主要的分类任务的代码是在run_glue.py文件中,这里面定义了main函数,命令行参数接收器,模型的加载和调用,模型的训练以及验证,和数据读取以及处理的功能模块调用。

我们看一下这里调用的分类模型,代码是这样的:


   
  
  
  1. model = AutoModelForSequenceClassification.from_pretrained(
  2. args.model_name_or_path,
  3. from_tf=bool( ".ckpt" in args.model_name_or_path),
  4. config=config,
  5. cache_dir=args.cache_dir,
  6. )

其实最终这里的AutoModelForSequenceClassification.from_pretrained()调用的是modeling_bert.py中的BertForSequenceClassification类,它就是具体的分类器实现:


   
  
  
  1. class BertForSequenceClassification(BertPreTrainedModel):
  2. def __init__(self, config):
  3. super().__init__(config)
  4. self.num_labels = config.num_labels
  5. self.bert = BertModel(config)
  6. self.dropout = nn.Dropout(config.hidden_dropout_prob)
  7. self.classifier = nn.Linear(config.hidden_size, self.config.num_labels)
  8. self.init_weights()
  9. def forward(
  10. self,
  11. input_ids=None,
  12. attention_mask=None,
  13. token_type_ids=None,
  14. position_ids=None,
  15. head_mask=None,
  16. inputs_embeds=None,
  17. labels=None,
  18. ):
  19. outputs = self.bert(
  20. input_ids,
  21. attention_mask=attention_mask,
  22. token_type_ids=token_type_ids,
  23. position_ids=position_ids,
  24. head_mask=head_mask,
  25. inputs_embeds=inputs_embeds,
  26. )
  27. pooled_output = outputs[ 1]
  28. pooled_output = self.dropout(pooled_output)
  29. logits = self.classifier(pooled_output)
  30. outputs = (logits,) + outputs[ 2:] # add hidden states and attention if they are here
  31. if labels is not None:
  32. if self.num_labels == 1:
  33. # We are doing regression
  34. loss_fct = MSELoss()
  35. loss = loss_fct(logits.view( -1), labels.view( -1))
  36. else:
  37. loss_fct = CrossEntropyLoss()
  38. loss = loss_fct(logits.view( -1, self.num_labels), labels.view( -1))
  39. outputs = (loss,) + outputs
  40. return outputs

模型调用了BertModel,然后做使用nn.Linear(config.hidden_size, self.config.num_labels)做分类,loss函数是常用的交叉熵损失函数。以上就是分类器的一些简单的分析。 我们要做的工作就是仿照项目里的代码写一个任务处理器:

项目目录结构:transformerer_local/data/glue.py,注意这里的transformerer_local原本应该是transformerer,我这里已经做了修改。在glue.py添加上我们的分类任务代码——添加一个读取文件中的文本然后,然后把每条数据序列化成Example,注意get_labels()函数,把自己的类别数目实现过来,代码如下:


   
  
  
  1. class MyownProcessor(DataProcessor):
  2. """Processor for the CoLA data set (GLUE version)."""
  3. def get_example_from_tensor_dict(self, tensor_dict):
  4. """See base class."""
  5. return InputExample(
  6. tensor_dict[ "idx"].numpy(),
  7. tensor_dict[ "sentence"].numpy().decode( "utf-8"),
  8. None,
  9. str(tensor_dict[ "label"].numpy()),
  10. )
  11. def get_train_examples(self, data_dir):
  12. """See base class."""
  13. return self._create_examples(self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")
  14. def get_dev_examples(self, data_dir):
  15. """See base class."""
  16. return self._create_examples(self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")
  17. def get_predict_examples(self, data_dir):
  18. return self._create_examples(self._read_tsv(os.path.join(data_dir, "test.tsv")), "predict")
  19. def get_labels(self):
  20. """See base class."""
  21. return [ "0", "1", "2", "3", "4", "5", "6", "7"]
  22. def _create_examples(self, lines, set_type):
  23. """Creates examples for the training and dev sets."""
  24. examples = []
  25. for (i, line) in enumerate(lines):
  26. guid = "%s-%s" % (set_type, i)
  27. if len(line)== 2:
  28. text_a = line[ 0]
  29. label = line[ 1]
  30. examples.append(InputExample(guid=guid, text_a=text_a, text_b= None, label=label))
  31. else:
  32. print(line)
  33. return examples

同时在验证的时候,对应评价指标函数,我们这里不是binary,计算f1_score的时候要采用其他的策略:

transformerer_local/data/metrics/__init__.py,注意这里的transformerer_local原本应该是transformerer,添加内容:


   
  
  
  1. #添加多分类评价函数
  2. def acc_and_f1_multi(preds, labels):
  3. acc = simple_accuracy(preds, labels)
  4. f1 = f1_score(y_true=labels, y_pred=preds,average= 'micro')
  5. return {
  6. "acc": acc,
  7. "f1": f1,
  8. "acc_and_f1": (acc + f1) / 2,
  9. }
  10. def glue_compute_metrics(task_name, preds, labels):
  11. assert len(preds) == len(labels)
  12. if task_name == "cola":
  13. return { "mcc": matthews_corrcoef(labels, preds)}
  14. elif task_name == "sst-2":
  15. return { "acc": simple_accuracy(preds, labels)}
  16. elif task_name == "mrpc":
  17. return acc_and_f1(preds, labels)
  18. elif task_name == "sts-b":
  19. return pearson_and_spearman(preds, labels)
  20. elif task_name == "qqp":
  21. return acc_and_f1(preds, labels)
  22. elif task_name == "mnli":
  23. return { "acc": simple_accuracy(preds, labels)}
  24. elif task_name == "mnli-mm":
  25. return { "acc": simple_accuracy(preds, labels)}
  26. elif task_name == "qnli":
  27. return { "acc": simple_accuracy(preds, labels)}
  28. elif task_name == "rte":
  29. return { "acc": simple_accuracy(preds, labels)}
  30. elif task_name == "wnli":
  31. return { "acc": simple_accuracy(preds, labels)}
  32. elif task_name == "hans":
  33. return { "acc": simple_accuracy(preds, labels)}
  34. #添加我们的多分类任务调用函数
  35. elif task_name == "myown":
  36. return acc_and_f1_multi(preds, labels)
  37. else:
  38. raise KeyError(task_name)

添加内容就在注释部分。

OK,现在代码部分已经做好了,接下来就是数据部分了。直接上数据:

数据截图部分就是上面这样的,把pat_summary和ipc_class属性提取出来,这里的数据质量比较好,然后只需要把超级长的文本去掉(长度大于510的):

数据长度分布直方图,发现几乎全部都是小于510的长度,只有少部分比较长,只有128条,这里数据集总规模是24.8W条,可以把这少部分的直接去掉。然后把数据分割成训练集和测试集比例(8:2),保存为tsv格式。

接下来就是直接进行训练了,编写如下命令行,在train_glue_classification.sh文件中:


   
  
  
  1. export TASK_NAME=myown
  2. python -W ignore ./examples/run_glue.py \
  3. --model_type bert \
  4. --model_name_or_path ./pretrain_model/Chinese-BERT-wwm/ \
  5. --task_name $TASK_NAME \
  6. __do_train \
  7. --do_eval \
  8. --data_dir ./data_set/patent/ \
  9. --max_seq_length 510 \
  10. --per_gpu_eval_batch_size= 8 \
  11. --per_gpu_train_batch_size= 8 \
  12. --per_gpu_predict_batch_size= 48 \
  13. --learning_rate 2e-5 \
  14. --num_train_epochs 5.0 \
  15. --output_dir ./output/

直接在终端上运行这个sh文件,bash train_glue_classification.sh。注意这里的训练显卡显存得11G以上,不然跑步起来,batch_size不能太大。训练过程中,一个epoch大概时间3.5小时,所以时间还要蛮久的。最后给出结果:

可以看到acc=0.8508,一个8分类的任务准确率85%粗略一看还能接受。如果要详细的分析,可以把每一类的准确率和召回率给弄出来,或者分析一下ROC,对模型的性能做详细的分析,这里不做过多讨论。另外关于这个模型的优化,怎么提高准确率,也不做考虑。

小结:以上就是直接使用transformers项目中的bert分类器拿过来进行fine-tune,做文本分类,其实代码都写好了,我们只需要简单的修改一下代码和配置,就能很快的训练好自己的分类器。

四、Bert模型难点总结

其实关于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知识点

猜你喜欢

转载自blog.csdn.net/stay_foolish12/article/details/112366097
今日推荐