【NLP】NLP实战篇之bert源码阅读(run_classifier)

本文主要会阅读bert源码

(https://github.com/google-research/bert )中run_classifier.py文件,已完成modeling.py、optimization.py、run_pretraining.py、tokenization.py、create_pretraining_data.py、extract_feature.py文件的源码阅读,后续会陆续阅读bert的理解任务训练等源码。本文介绍了run_classifier.py中的主要内容,包括不同分类任务的数据读取,用于分类的bert模型结构,和整体的训练流程。代码中还涉及很多其他内容,如运行参数,特征转为tfrecord文件等等,由于在之前的阅读中,出现过非常相似的内容,所以这里不再重复。

run_classifier.py的全部代码以及中文注释可参考

https://github.com/wellinxu/nlp_store/blob/master/read_source/bert/run_classifier.py。

实战系列篇章中主要会分享,解决实际问题时的过程、遇到的问题或者使用的工具等等。如问题分解、bug排查、模型部署等等。相关代码实现开源在:https://github.com/wellinxu/nlp_store ,。

  • 分类任务

    • 句对分类

    • 单句分类

  • 模型结构

    • model_fn

  • main函数

  • 其他

分类任务

源码中,bert能处理的文本分类任务可简单分为两种:句对分类任务(比如文本匹配/文本蕴含等)和单句分类任务(比如情感分类/长文本分类等)。代码中涉及好几个任务的数据读取,不过因为大同小异,本文只分别讲述一个示例,其他任务的相关代码请参考原代码。
不管是哪种分类任务,都是将每个样本转化为一个InputExample类,如下面代码所示,其中包含样本的id,第一句文本,第二句文本(单句分类时为空)以及标签文本。

class InputExample(object):
  """分类用的单个训练、测试样本"""

  def __init__(self, guid, text_a, text_b=None, label=None):
    """构建一个输入样本

    Args:
      guid: 样本的唯一id.
      text_a: string. 第一句原文本,对于单句分类任务,只有该参数是必须的.
      text_b: (可选参数) string. 第二句原文本,对于句子对任务,该参数才是需要的.
      label: (可选参数) string. 样本的标签,对于训练和验证集都是必须的,测试集则不是.
    """
    self.guid = guid
    self.text_a = text_a
    self.text_b = text_b
    self.label = label

句对分类

关于句对分类任务,我们主要分析MultiNLI数据集,这是一个文本蕴含任务,需要判断前后两句文本是对立、蕴含还是中立关系。下面的这段代码则是读取该任务的代码,逻辑很简单,主要就是将每个样本的第一句、第二句和标签分别取出,然后构建InputExample。

class MnliProcessor(DataProcessor):
  """处理MultiNLI数据集(GLUE版本)."""

  def get_train_examples(self, data_dir):
    """See base class."""
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "dev_matched.tsv")),
        "dev_matched")

  def get_test_examples(self, data_dir):
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "test_matched.tsv")), "test")

  def get_labels(self):
    return ["contradiction", "entailment", "neutral"]

  def _create_examples(self, lines, set_type):
    """生成样本数据"""
    examples = []
    for (i, line) in enumerate(lines):
      if i == 0:
        continue
      guid = "%s-%s" % (set_type, tokenization.convert_to_unicode(line[0]))
      text_a = tokenization.convert_to_unicode(line[8])
      text_b = tokenization.convert_to_unicode(line[9])
      if set_type == "test":
        label = "contradiction"
      else:
        label = tokenization.convert_to_unicode(line[-1])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
    return examples

单句分类

关于单句分类任务,我们分析CoLA数据集,其是一个文本二分类问题。如下面代码所示,其逻辑更加简单,主要就是将单句文本与标签提取出来,然后构建InputExample。

class ColaProcessor(DataProcessor):
  """处理CoLA数据集(GLUE版本)."""

  def get_train_examples(self, data_dir):
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "train.tsv")), "train")

  def get_dev_examples(self, data_dir):
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "dev.tsv")), "dev")

  def get_test_examples(self, data_dir):
    return self._create_examples(
        self._read_tsv(os.path.join(data_dir, "test.tsv")), "test")

  def get_labels(self):
    return ["0", "1"]

  def _create_examples(self, lines, set_type):
    """生成样本数据"""
    examples = []
    for (i, line) in enumerate(lines):
      # 测试集有header行,所以要跳过
      if set_type == "test" and i == 0:
        continue
      guid = "%s-%s" % (set_type, i)
      if set_type == "test":
        text_a = tokenization.convert_to_unicode(line[1])
        label = "0"
      else:
        text_a = tokenization.convert_to_unicode(line[3])
        label = tokenization.convert_to_unicode(line[1])
      examples.append(
          InputExample(guid=guid, text_a=text_a, text_b=None, label=label))
    return examples

模型结构

bert在做文本分类时的模型结构比较简单,直接用pooled层的结果接一层全连接层+softmax。如下图所示,句对分类任务与单句分类任务的模型结构基本一致,只是开始的输入略有差异。
【NLP】NLP实战篇之bert源码阅读(run_classifier)_第1张图片
模型的代码构建如下所示,建立bert模型之后,获取pooled层输出(bertd 模型结构和pooled层输出可参考modeling.py),然后接上全连接层计算softmax,最后计算交叉熵loss,用来训练。

def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
                 labels, num_labels, use_one_hot_embeddings):
  """创建一个分类模型."""
  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)

  # 获取pooled层结果,如果想获取token级别的输出,可以用model.get_sequence_output()
  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:
      # dropout 丢弃0.1
      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)
    # 计算交叉熵loss
    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)

model_fn

上面已经得到了模型结构,但在任务运行过程中,一般会分为训练阶段、验证阶段和预测阶段,每个阶段需要计算的算子和返回的结果略有不同,所以就有了以下代码。其主要逻辑是,将样本数据输入给bert模型之后,根据阶段的不同,分别获取训练阶段的优化操作、验证阶段的评估操作和预测阶段的直接结果。

def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
                     num_train_steps, num_warmup_steps, use_tpu,
                     use_one_hot_embeddings):
  """
  返回给TPUEstimator使用的模型函数-model_fn
  可以参考run_pretraining.py中的model_fn_builder方法
  """

  def model_fn(features, labels, mode, params):  # pylint: disable=unused-argument
    """待返回的模型函数,model_fn"""

    tf.logging.info("*** Features ***")
    for name in sorted(features.keys()):
      tf.logging.info("  name = %s, shape = %s" % (name, features[name].shape))

    # 样本输入数据
    input_ids = features["input_ids"]
    input_mask = features["input_mask"]
    segment_ids = features["segment_ids"]
    label_ids = features["label_ids"]
    is_real_example = None
    if "is_real_example" in features:
      is_real_example = tf.cast(features["is_real_example"], dtype=tf.float32)
    else:    # tpu训练需要固定尺寸,所以在某些step中样本不够的时候需要构建假样本
      is_real_example = tf.ones(tf.shape(label_ids), dtype=tf.float32)

    is_training = (mode == tf.estimator.ModeKeys.TRAIN)

    # 将样本输入模型得到结果
    (total_loss, per_example_loss, logits, probabilities) = create_model(
        bert_config, is_training, input_ids, input_mask, segment_ids, label_ids,
        num_labels, use_one_hot_embeddings)

    tvars = tf.trainable_variables()
    initialized_variable_names = {}
    scaffold_fn = None
    if init_checkpoint:   # 是否只是初始化模型参数
      (assignment_map, initialized_variable_names
      ) = modeling.get_assignment_map_from_checkpoint(tvars, init_checkpoint)
      if use_tpu:

        def tpu_scaffold():
          tf.train.init_from_checkpoint(init_checkpoint, assignment_map)
          return tf.train.Scaffold()

        scaffold_fn = tpu_scaffold
      else:
        tf.train.init_from_checkpoint(init_checkpoint, assignment_map)

    tf.logging.info("**** Trainable Variables ****")
    for var in tvars:
      init_string = ""
      if var.name in initialized_variable_names:
        init_string = ", *INIT_FROM_CKPT*"
      tf.logging.info("  name = %s, shape = %s%s", var.name, var.shape,
                      init_string)

    output_spec = None
    if mode == tf.estimator.ModeKeys.TRAIN:    # 训练模式

      train_op = optimization.create_optimizer(
          total_loss, learning_rate, num_train_steps, num_warmup_steps, use_tpu)

      output_spec = tf.contrib.tpu.TPUEstimatorSpec(
          mode=mode,
          loss=total_loss,
          train_op=train_op,
          scaffold_fn=scaffold_fn)
    elif mode == tf.estimator.ModeKeys.EVAL:   # 评估模式
      # 评价函数,会计算评估数据集结果的准确性和loss
      def metric_fn(per_example_loss, label_ids, logits, is_real_example):
        predictions = tf.argmax(logits, axis=-1, output_type=tf.int32)
        accuracy = tf.metrics.accuracy(
            labels=label_ids, predictions=predictions, weights=is_real_example)
        loss = tf.metrics.mean(values=per_example_loss, weights=is_real_example)
        return {
            "eval_accuracy": accuracy,
            "eval_loss": loss,
        }

      # 评估模式会返回评估结果
      eval_metrics = (metric_fn,
                      [per_example_loss, label_ids, logits, is_real_example])
      output_spec = tf.contrib.tpu.TPUEstimatorSpec(
          mode=mode,
          loss=total_loss,
          eval_metrics=eval_metrics,
          scaffold_fn=scaffold_fn)
    else:   # 预测模式,只要返回预测值
      output_spec = tf.contrib.tpu.TPUEstimatorSpec(
          mode=mode,
          predictions={"probabilities": probabilities},
          scaffold_fn=scaffold_fn)
    return output_spec

  return model_fn

main函数

在知道输入读取与模型结构之后,我们来看下分类任务的主体结构main函数。其主要逻辑如下:

  1. 检查并测试bert相关参数

  2. 根据任务名称获取数据处理类

  3. 设置训练参数,构建bert模型与estimator

  4. 如果执行训练阶段:

    1. 将训练样本保存为tfrecord格式

    2. 将训练样本转换为训练输入函数

    3. 训练模型

  5. 如果执行验证阶段:

    1. 将验证样本保存为tfrecord格式

    2. 将验证样本转换为验证输入函数

    3. 验证模型

    4. 将评估结果写入文件

  6. 如果执行预测阶段:

    1. 将预测样本保存为tfrecord格式

    2. 将预测样本转化为预测输入函数

    3. 模型预测

    4. 将预测结果写入文件

其中将数据转化为tfrecord格式,是file_based_convert_examples_to_features函数实现的,可参考create_pretraining_data.py中的write_instance_to_example_files方法,不再赘述;而转为输入函数,则是file_based_input_fn_builder函数实现的,可以参考run_pretrainin.py中的input_fn_builder方法,也不赘述。整体的main代码如下:

def main(_):
  tf.logging.set_verbosity(tf.logging.INFO)    # 设置日志等级

  # 数据处理类的映射
  processors = {
      "cola": ColaProcessor,
      "mnli": MnliProcessor,
      "mrpc": MrpcProcessor,
      "xnli": XnliProcessor,
  }
  # 校验模型参数
  tokenization.validate_case_matches_checkpoint(FLAGS.do_lower_case,
                                                FLAGS.init_checkpoint)

  if not FLAGS.do_train and not FLAGS.do_eval and not FLAGS.do_predict:
    raise ValueError(
        "At least one of `do_train`, `do_eval` or `do_predict' must be True.")

  bert_config = modeling.BertConfig.from_json_file(FLAGS.bert_config_file)    # 获取bert配置

  if FLAGS.max_seq_length > bert_config.max_position_embeddings:
    raise ValueError(
        "Cannot use sequence length %d because the BERT model "
        "was only trained up to sequence length %d" %
        (FLAGS.max_seq_length, bert_config.max_position_embeddings))

  tf.gfile.MakeDirs(FLAGS.output_dir)

  task_name = FLAGS.task_name.lower()    # 获取任务名称

  if task_name not in processors:
    raise ValueError("Task not found: %s" % (task_name))

  processor = processors[task_name]()    # 获取数据处理类

  label_list = processor.get_labels()    # 获取标签集合
  # 初始化token切分器
  tokenizer = tokenization.FullTokenizer(
      vocab_file=FLAGS.vocab_file, do_lower_case=FLAGS.do_lower_case)

  # tpu相关
  tpu_cluster_resolver = None
  if FLAGS.use_tpu and FLAGS.tpu_name:
    tpu_cluster_resolver = tf.contrib.cluster_resolver.TPUClusterResolver(
        FLAGS.tpu_name, zone=FLAGS.tpu_zone, project=FLAGS.gcp_project)
  # tpu相关
  is_per_host = tf.contrib.tpu.InputPipelineConfig.PER_HOST_V2
  run_config = tf.contrib.tpu.RunConfig(
      cluster=tpu_cluster_resolver,
      master=FLAGS.master,
      model_dir=FLAGS.output_dir,
      save_checkpoints_steps=FLAGS.save_checkpoints_steps,
      tpu_config=tf.contrib.tpu.TPUConfig(
          iterations_per_loop=FLAGS.iterations_per_loop,
          num_shards=FLAGS.num_tpu_cores,
          per_host_input_for_training=is_per_host))

  train_examples = None    # 训练样本
  num_train_steps = None    # 训练步数
  num_warmup_steps = None    # warmup步数
  if FLAGS.do_train:
    train_examples = processor.get_train_examples(FLAGS.data_dir)
    num_train_steps = int(
        len(train_examples) / FLAGS.train_batch_size * FLAGS.num_train_epochs)
    num_warmup_steps = int(num_train_steps * FLAGS.warmup_proportion)
  # 模型函数,输入到输出中间的结构定义
  model_fn = model_fn_builder(
      bert_config=bert_config,
      num_labels=len(label_list),
      init_checkpoint=FLAGS.init_checkpoint,
      learning_rate=FLAGS.learning_rate,
      num_train_steps=num_train_steps,
      num_warmup_steps=num_warmup_steps,
      use_tpu=FLAGS.use_tpu,
      use_one_hot_embeddings=FLAGS.use_tpu)

  # 如果tpu不可用,则会退化成cpu或者gpu版本
  estimator = tf.contrib.tpu.TPUEstimator(
      use_tpu=FLAGS.use_tpu,
      model_fn=model_fn,
      config=run_config,
      train_batch_size=FLAGS.train_batch_size,
      eval_batch_size=FLAGS.eval_batch_size,
      predict_batch_size=FLAGS.predict_batch_size)

  # 进行训练
  if FLAGS.do_train:
    train_file = os.path.join(FLAGS.output_dir, "train.tf_record")
    # 将输入数据转换为tfrecord格式,并保存
    file_based_convert_examples_to_features(
        train_examples, label_list, FLAGS.max_seq_length, tokenizer, train_file)
    tf.logging.info("***** Running training *****")
    tf.logging.info("  Num examples = %d", len(train_examples))
    tf.logging.info("  Batch size = %d", FLAGS.train_batch_size)
    tf.logging.info("  Num steps = %d", num_train_steps)
    # 训练的输入函数,产生训练输入样本
    train_input_fn = file_based_input_fn_builder(
        input_file=train_file,
        seq_length=FLAGS.max_seq_length,
        is_training=True,
        drop_remainder=True)
    # 根据输入函数与模型函数进行训练模型
    estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)

  # 进行评估
  if FLAGS.do_eval:
    eval_examples = processor.get_dev_examples(FLAGS.data_dir)
    num_actual_eval_examples = len(eval_examples)
    if FLAGS.use_tpu:
      # TPU需要固定大小的batch,添加加样本补足
      while len(eval_examples) % FLAGS.eval_batch_size != 0:
        eval_examples.append(PaddingInputExample())

    eval_file = os.path.join(FLAGS.output_dir, "eval.tf_record")
    # 将输入数据转换为tfrecord格式,并保存
    file_based_convert_examples_to_features(
        eval_examples, label_list, FLAGS.max_seq_length, tokenizer, eval_file)

    tf.logging.info("***** Running evaluation *****")
    tf.logging.info("  Num examples = %d (%d actual, %d padding)",
                    len(eval_examples), num_actual_eval_examples,
                    len(eval_examples) - num_actual_eval_examples)
    tf.logging.info("  Batch size = %d", FLAGS.eval_batch_size)

    eval_steps = None
    # 使用TPU时,需要知道具体运行步数
    if FLAGS.use_tpu:
      assert len(eval_examples) % FLAGS.eval_batch_size == 0
      eval_steps = int(len(eval_examples) // FLAGS.eval_batch_size)

    eval_drop_remainder = True if FLAGS.use_tpu else False
    # 评估的输入函数,产生评估输入样本
    eval_input_fn = file_based_input_fn_builder(
        input_file=eval_file,
        seq_length=FLAGS.max_seq_length,
        is_training=False,
        drop_remainder=eval_drop_remainder)
    # 根据输入函数与模型函数进行模型评估
    result = estimator.evaluate(input_fn=eval_input_fn, steps=eval_steps)

    # 评估结果写入文件
    output_eval_file = os.path.join(FLAGS.output_dir, "eval_results.txt")
    with tf.gfile.GFile(output_eval_file, "w") as writer:
      tf.logging.info("***** Eval results *****")
      for key in sorted(result.keys()):
        tf.logging.info("  %s = %s", key, str(result[key]))
        writer.write("%s = %s\n" % (key, str(result[key])))

  # 进行预测
  if FLAGS.do_predict:
    predict_examples = processor.get_test_examples(FLAGS.data_dir)
    num_actual_predict_examples = len(predict_examples)
    if FLAGS.use_tpu:
      #  TPU需要固定大小的batch,添加加样本补足
      while len(predict_examples) % FLAGS.predict_batch_size != 0:
        predict_examples.append(PaddingInputExample())

    predict_file = os.path.join(FLAGS.output_dir, "predict.tf_record")
    # 将输入数据转换为tfrecord格式,并保存
    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 (%d actual, %d padding)",
                    len(predict_examples), num_actual_predict_examples,
                    len(predict_examples) - num_actual_predict_examples)
    tf.logging.info("  Batch size = %d", FLAGS.predict_batch_size)

    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:
      num_written_lines = 0
      tf.logging.info("***** Predict results *****")
      for (i, prediction) in enumerate(result):
        probabilities = prediction["probabilities"]
        if i >= num_actual_predict_examples:
          break
        output_line = "\t".join(
            str(class_probability)
            for class_probability in probabilities) + "\n"
        writer.write(output_line)
        num_written_lines += 1
    assert num_written_lines == num_actual_predict_examples

其他

文本分类代码与预训练和create_data代码有很多相似代码,这边都不再赘述,比如convert_single_example、_truncate_seq_pair、file_based_convert_examples_to_features、file_based_input_fn_builder、input_fn_builder等函数都可以找到非常相似的代码。而模型的运行参数与之前也是大同小异,具体参数以及整体代码及中文注释,都可以参考https://github.com/wellinxu/nlp_store/blob/master/read_source/bert/run_classifier.py。

【NLP】NLP实战篇之bert源码阅读(run_classifier)_第2张图片

往期精彩回顾



适合初学者入门人工智能的路线及资料下载机器学习及深度学习笔记等资料打印机器学习在线手册深度学习笔记专辑《统计学习方法》的代码复现专辑
AI基础下载机器学习的数学基础专辑温州大学《机器学习课程》视频
本站qq群851320808,加入微信群请扫码:

你可能感兴趣的:(人工智能,python,机器学习,深度学习,tensorflow)