BERT系列2 – pre-training

BERT:Bidirectional Encoder Representations from Transformers
论文下载地址:https://arxiv.org/abs/1810.04805

BERT如雷贯耳,刷新了很多项NLP记录,极大发挥了transfer learning的效果,google出精品!稍微感受下:它在GLUE数据集上获取7.6%的绝对提升,MultiNLI数据集上5.6%的绝对提升,SQuAD v1.1数据集上1.5%的绝对提升!可以说是万能NLP模型。接下来我们来详细看看BERT pre-training模块,fine-tuning已经在系列1中阐述,不做赘述。

Pre-training就是预训练一个模型,这个模型可以适用于很多NLP任务,譬如文本分类、NER、QA等。Google开源了它的代码,良心的是Google不仅预训练了英文模型,还预训练了中文模型,可以说是节省了我们大量财力(据说在TPU上预训练一把需要花费5w块)。我们可以将模型稍微修改一下就可以应用到很多NLP任务里面,可以说是相当方便,代码地址:https://github.com/google-research/bert

接下来我们进入主题,model architecture。如果我们不管transformer(会在系列3中阐述)是什么,把transformer当做是一个黑盒,那么它的结构就相当简单,一个多层的transformer,层数、宽度等等参数都是可以调节的,详见论文。
第一部分是模型的输入,论文进行了相当多的细节处理,如图所示:

第一个,它用了wordpiece embeddings,第二个它加入了positional embeddings,第三个它还加入了segment embeddings。论文中对后两个embeddings讲的不多,看代码可以知道positional embeddings是个位置编码,通过模型训练出来之后每个位置都会有一个跟word embedding一样维度的向量,加入到word embedding中,如下代码所示:

  if use_position_embeddings:
    assert_op = tf.assert_less_equal(seq_length, max_position_embeddings)
    with tf.control_dependencies([assert_op]):
      full_position_embeddings = tf.get_variable(
          name=position_embedding_name,
          shape=[max_position_embeddings, width],
          initializer=create_initializer(initializer_range))
      # Since the position embedding table is a learned variable, we create it
      # using a (long) sequence length `max_position_embeddings`. The actual
      # sequence length might be shorter than this, for faster training of
      # tasks that do not have long sequences.
      #   
      # So `full_position_embeddings` is effectively an embedding table
      # for position [0, 1, 2, ..., max_position_embeddings-1], and the current
      # sequence has positions [0, 1, 2, ... seq_length-1], so we can just
      # perform a slice.
      position_embeddings = tf.slice(full_position_embeddings, [0, 0], 
                                     [seq_length, -1])
      num_dims = len(output.shape.as_list())

      # Only the last two dimensions are relevant (`seq_length` and `width`), so
      # we broadcast among the first dimensions, which is typically just
      # the batch size.
      position_broadcast_shape = []
      for _ in range(num_dims - 2):
        position_broadcast_shape.append(1)
      position_broadcast_shape.extend([seq_length, width])
      position_embeddings = tf.reshape(position_embeddings,
                                       position_broadcast_shape)
      output += position_embeddings

为什么要加入positional embedding?原因就是多层transformer不像RNN没有包含位置信息。
segment embedding就更简单了,SEP分隔的第一段句子为0,第二段句子为1,还有个二维数组将这个0或者1映射到word embedding的维度,参数可以学习。代码里面还说了为什么有SEP还要segment embedding,它的解释是为了模型更容易学习到序列的概念。

  if use_token_type:
    if token_type_ids is None:
      raise ValueError("`token_type_ids` must be specified if"
                       "`use_token_type` is True.")
    token_type_table = tf.get_variable(
        name=token_type_embedding_name,
        shape=[token_type_vocab_size, width],
        initializer=create_initializer(initializer_range))
    # This vocab will be small so we always do one-hot here, since it is always
    # faster for a small vocabulary.
    flat_token_type_ids = tf.reshape(token_type_ids, [-1])
    one_hot_ids = tf.one_hot(flat_token_type_ids, depth=token_type_vocab_size)
    token_type_embeddings = tf.matmul(one_hot_ids, token_type_table)
    token_type_embeddings = tf.reshape(token_type_embeddings,
                                       [batch_size, seq_length, width])
    output += token_type_embeddings

将以上三个embeddings相加作为多层transformer的输入,整个模型就这样了。

怎么做分类?获取BERT输出第一个label做个LR。怎么做相似性计算?获取BERT输出第一个label做个LR。怎么做QA或者NER?获取所有输出对所有输出做LR。就这么简单。什么?怎么获取输出或者embedding?google给你写好啦:

  def get_pooled_output(self):
    return self.pooled_output

  def get_sequence_output(self):
    """Gets final hidden layer of encoder.

    Returns:
      float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
      to the final hidden of the transformer encoder.
    """
    return self.sequence_output

  def get_all_encoder_layers(self):
    return self.all_encoder_layers

  def get_embedding_output(self):
    """Gets output of the embedding lookup (i.e., input to the transformer).

    Returns:
      float Tensor of shape [batch_size, seq_length, hidden_size] corresponding
      to the output of the embedding layer, after summing the word
      embeddings with the positional embeddings and the token type embeddings,
      then performing layer normalization. This is the input to the transformer.
    """
    return self.embedding_output

  def get_embedding_table(self):
    return self.embedding_table

当然到这里并没有结束,怎么去预训练这个模型?google又花了很多心思在这上面,这可能是这个模型能成功的很大一个原因,也给了我们不少启发。
两个无监督任务做pre-training:Masked LM和Next Sentence Prediction。
第一个任务MLM简单来说就是随机抹去15%的单词去预测这个单词是啥。细节来了,它并不是总是用[MASK]替换那些单词,替换只占了80%,剩余的10%替换成了一个随机单词,最后的10%保留了原句。Transformer并不知道预测哪个单词,所以它需要记住每个单词的上下文分布,并且被替换的单词不多,并没有破坏语义。
第二个任务就是判断句子A是否是B的前一句。这个任务对QA和NLI收益很大。

最后loss的定义就是两个task的loss求和平均。

实验结果这里就不多说了,反正就是牛!

— 2018-12-13 18:41

《BERT系列2 – pre-training》上有1条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注