BERT系列1 – fine-tuning

代码地址:https://github.com/google-research/bert

学习BERT,我们从最简单的使用开始,Google大牛们已经把代码开源了,而且注释相当详细,很容易把模型跑起来(包括CPU)。但是仅仅会跑代码demo是不够的,还是需要学会定制代码,适配到自己的问题上做一些尝试。
这里简单介绍下fine-tuning的run_classifier.py脚本,因为它最简单、应用最广。
run_pretraining.py这里没有强大的机器,暂时跑不了,好在Google已经给我们预训练了一个中文模型chinese_L-12_H-768_A-12.zip,可以直接使用。run_squad.py应用面有限,理解了run_classifier.py看这个也不是问题。
这里简单的代码就不在赘述,主要集中在一些比较难理解的代码。

代码开头设置了几个数据集的Processor,都有一个共同基类,主要是获取训练集、测试集、labels等功能。对于想适配自己的应用来说,这里是一个很重要的修改点。拷贝一个Processor直接在Processor上面根据自己的数据集的排版进行代码适配就可以了,不是很难,主要是赋值guid、text_a、text_b还有label(为什么会有text_a、text_b?BERT后续系列会有解释)。
接下来就是tokenizer,这里看下代码注释就可以大概理解它是干啥的了

    """Tokenizes a piece of text into its word pieces.

    This uses a greedy longest-match-first algorithm to perform tokenization
    using the given vocabulary.

    For example:
      input = "unaffable"
      output = ["un", "##aff", "##able"]

    Args:
      text: A single token or whitespace separated tokens. This should have
        already been passed through `BasicTokenizer.

    Returns:
      A list of wordpiece tokens.
    """

然后就是BERT的一些配置,我们这里把它当做一个黑盒用,用兴趣的可以看下tensorflow官方文档。
然后就是BERT的定义啦model_fn_builder,这里还要往深层看一看,因为它里面会有定义fine-tuning的代码:

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 labels, num_labels, use_one_hot_embeddings):
  """Creates a classification model."""
  model = modeling.BertModel(
      config=bert_config,
      is_training=is_training,
      input_ids=input_ids,
      input_mask=input_mask,
      token_type_ids=segment_ids,
      use_one_hot_embeddings=use_one_hot_embeddings)

  # In the demo, we are doing a simple classification task on the entire
  # segment.
  #
  # If you want to use the token-level output, use model.get_sequence_output()
  # instead.
  output_layer = model.get_pooled_output()

  hidden_size = output_layer.shape[-1].value

  output_weights = tf.get_variable(
      "output_weights", [num_labels, hidden_size],
      initializer=tf.truncated_normal_initializer(stddev=0.02))

  output_bias = tf.get_variable(
      "output_bias", [num_labels], initializer=tf.zeros_initializer())

  with tf.variable_scope("loss"):
    if is_training:
      # I.e., 0.1 dropout
      output_layer = tf.nn.dropout(output_layer, keep_prob=0.9)

    logits = tf.matmul(output_layer, output_weights, transpose_b=True)
    logits = tf.nn.bias_add(logits, output_bias)
    probabilities = tf.nn.softmax(logits, axis=-1)
    log_probs = tf.nn.log_softmax(logits, axis=-1)

    one_hot_labels = tf.one_hot(labels, depth=num_labels, dtype=tf.float32)

    per_example_loss = -tf.reduce_sum(one_hot_labels * log_probs, axis=-1)
    loss = tf.reduce_mean(per_example_loss)

    return (loss, per_example_loss, logits, probabilities)

可以看到这里的输出层代码就是一层LR。
接下来看是判断你的任务是训练还是评估还是预测,代码大同小异,我们看下预测的代码:

    predict_examples = processor.get_test_examples(FLAGS.data_dir)
    predict_file = os.path.join(FLAGS.output_dir, "predict.tf_record")
    file_based_convert_examples_to_features(predict_examples, label_list,
                                            FLAGS.max_seq_length, tokenizer,
                                            predict_file)

    tf.logging.info("***** Running prediction*****")
    tf.logging.info("  Num examples = %d", len(predict_examples))
    tf.logging.info("  Batch size = %d", FLAGS.predict_batch_size)

    if FLAGS.use_tpu:
      # Warning: According to tpu_estimator.py Prediction on TPU is an
      # experimental feature and hence not supported here
      raise ValueError("Prediction in TPU not supported")

    predict_drop_remainder = True if FLAGS.use_tpu else False
    predict_input_fn = file_based_input_fn_builder(
        input_file=predict_file,
        seq_length=FLAGS.max_seq_length,
        is_training=False,
        drop_remainder=predict_drop_remainder)

    result = estimator.predict(input_fn=predict_input_fn)

    output_predict_file = os.path.join(FLAGS.output_dir, "test_results.tsv")
    with tf.gfile.GFile(output_predict_file, "w") as writer:
      tf.logging.info("***** Predict results *****")
      for prediction in result:
        output_line = "\t".join(
            str(class_probability) for class_probability in prediction) + "\n"
        writer.write(output_line)

代码先把数据获取到,然后将数据写到tfrecord里面,然后再从tfrecord里面获取接口(file_based_input_fn_builder)输送到estimator里面,最后将结果写到output_predict_file里面,一气呵成。

总的来看,代码写的确实6,既显得高大上又易于理解,膜拜。膜拜之余,需要注意定制的时候Processor的处理、BERT output的的获取、BERT embedding的获取、fine-tuning代码的编写等细节。

Q:warmup参数是干啥的?
A:我的理解就是为了解决分布式环境训练时收敛慢的问题。

Q:fine-tuning的时候BERT模型的参数会更新吗?
A:这里没有看到任何将trainable设置为False的代码,应该是会更新的。

Q:有没有什么其他需要注意的?
A:当然有,需要了解BERT的这些接口,才能根据需求进行定制,例如NER的话就不能按照run_classifier.py来了:

  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

Q:为什么要写到tfrecord里面再读出来?
A:这里要看下tfrecord的优越性了,它高效、集成、支持分布式环境、方便等,否则你还要自己实现batch读之类的东西。

— 2018-12-11 17:08

发表评论

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