本文主要会阅读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 = guidself.text_a = text_aself.text_b = text_bself.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:continueguid = "%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:continueguid = "%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。如下图所示,句对分类任务与单句分类任务的模型结构基本一致,只是开始的输入略有差异。

模型的代码构建如下所示,建立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.1output_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)# 计算交叉熵lossper_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 = Noneif "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 = Noneif 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_scaffoldelse: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 = Noneif 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:   # 评估模式# 评价函数,会计算评估数据集结果的准确性和lossdef 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_specreturn 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 = Noneif 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_V2run_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 == 0eval_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 = 0tf.logging.info("***** Predict results *****")for (i, prediction) in enumerate(result):probabilities = prediction["probabilities"]if i >= num_actual_predict_examples:breakoutput_line = "\t".join(str(class_probability)for class_probability in probabilities) + "\n"writer.write(output_line)num_written_lines += 1assert 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。

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

【NLP】NLP实战篇之bert源码阅读(run_classifier)相关推荐

  1. Spark修炼之道(高级篇)——Spark源码阅读:第六节 Task提交

    Task提交 在上一节中的 Stage提交中我们提到,最终stage被封装成TaskSet,使用taskScheduler.submitTasks提交,具体代码如下: taskScheduler.su ...

  2. Spark修炼之道(高级篇)——Spark源码阅读:第九节 Task执行成功时的结果处理...

    Task执行成功时的结果处理 在上一节中,给出了Task在Executor上的运行代码演示,我们知道代码的最终运行通过的是TaskRunner方法 class TaskRunner(execBacke ...

  3. BERT源码分析(PART III)

    写在前面 继续之前没有介绍完的 Pre-training 部分,在上一篇中(BERT源码分析(PART II))我们已经完成了对输入数据的处理,接下来看看 BERT 是怎么完成「Masked LM」和 ...

  4. php微框架 flight源码阅读

    Flight(https://github.com/mikecao/fl... 是一个可扩展的PHP微框架,快速.简单,能够快速轻松地构建RESTful web应用程序,在github上有2k sta ...

  5. logrus 输出多个文件_Logrus源码阅读(1)基本用法

    选择golang日志库时, 使用logrus的主要原因就是因为star比较多, 而且社区活跃度非常高. 在项目使用过程中, 发现logrus的调用入口, 性能, 插件, 自定义插件, 输出格式等都非常 ...

  6. redis源码阅读-持久化之aof与aof重写详解

    aof相关配置 aof-rewrite-incremental-fsync yes # aof 开关,默认是关闭的,改为yes表示开启 appendonly no # aof的文件名,默认 appen ...

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

    bert模型简介.transformers中bert模型源码阅读.分类任务实战和难点总结:https://blog.csdn.net/HUSTHY/article/details/105882989 ...

  8. 【NLP】Transformers 源码阅读和实践

    本文主要针对HuggingFace开源的 transformers,以BERT为例介绍其源码并进行一些实践.主要以pytorch为例 (tf 2.0 代码风格几乎和pytorch一致),介绍BERT使 ...

  9. jieba tfidf_【NLP】【三】jieba源码分析之关键字提取(TF-IDF/TextRank)

    [一]综述 利用jieba进行关键字提取时,有两种接口.一个基于TF-IDF算法,一个基于TextRank算法.TF-IDF算法,完全基于词频统计来计算词的权重,然后排序,在返回TopK个词作为关键字 ...

最新文章

  1. 【紧急】支付宝全面停止微信合作
  2. 深度摄影风格转换--Deep Photo Style Transfer
  3. 明年就翻身系列:AMD 2017统治PC、服务器市场?
  4. nltk 文本预处理
  5. socket如连接linux上的端口号_Linux| |对于UDP的学习
  6. qt开发环境 - c++之输入,输出,名字空间定义,名字空间指令,名字空间声明
  7. java typereference_记录一下jackson中TypeReference的使用
  8. python中for语句的使用_python中for in的用法
  9. 转】.NET强名称工具(Sn.exe)使用详解
  10. PHP在不同页面间传递Json数据示例代码
  11. C语言 · 9-1九宫格
  12. 【裂缝识别】基于matlab组合BCOSFIRE过滤器墙体裂缝识别【含Matlab源码 321期】
  13. ARM介绍1:发展史
  14. 圣诞素材网站推荐 这几个网站超多免费可商用素材
  15. java 创建一个类_java创建一个类
  16. 土地资源管理就业怎么这么难_我所理解的土地资源管理专业
  17. CONTRASTIVE REPRESENTATION DISTILLATION
  18. 计算机科学学现代,现代计算机科学的鼻祖
  19. pandas选取excel特定两列,上下行数值相减,降采样,缺失值处理,按数值范围去除行的处理,在pycharm画图实例
  20. mysql 关联更新_MySQL UPDATE多表关联更新

热门文章

  1. 我的iOS学习历程 - UISlider(简单的设置一组图片动画)
  2. XML和JSON两种数据交换格式的比较
  3. 很多优秀人感觉是:让他实际工作,他可能会考虑得很全面,而答题的时候,他就显得比较马虎,完成要求就算结束...
  4. TP框架(接口文档模板框架)
  5. php每天一题:strlen()与mb_strlen()的作用分别是什么
  6. 转贴 jQuery Datepicker by Example
  7. 如何读取FoxPro(dbf)打删除标记的记录
  8. matlab z变换离散化_MATLAB作图从入门到熟练
  9. 医学生如何选专业选科室?全网最全最详细分析
  10. GraphPad Prism 统计教程:简单线性回归原理