首页 科技正文

秦皇岛特产:广告行业中那些趣事系列8:详解BERT中分类器源码

admin 科技 2020-04-08 34 0

最新最全的文章请关注我的微信民众号:数据拾光者。

 

摘要:BERT是近几年NLP领域中具有里程碑意义的存在。由于效果好和应用局限广以是被普遍应用于科学研究和工程项目中。广告系列中前几篇文章有从理论的方面讲过BERT的原理,也有从实战的方面讲过使用BERT构建分类模子。本篇从源码的角度从整体到局部剖析BERT模子中分类器部门的源码。

目录

01 整体模块划分
02 数据处置模块
03 特征处置模块
04 模子构建模块
05 模子运行模块
06 其他模块
总结

 

 

01 整体模块划分

对于机械学习工程师来说,会调包跑程序应该是万里长征的第一步。这一步主要是辅助我们迅速将模子应用到现实营业中,而且提升自信心,但这还远远不够。要想凭据差别的营业场景更好的使用模子,我们需要深层次的明白模子,读点源码才气走的更远。

本篇解读的是BERT开源项目中分类器部门的源码,从最最先的数据输入到模子运行整个流程主要可以分成数据处置模块、特征处置模块、模子构建模块和模子运行模块。详细如下图所示:

图1 BERT分类器整体模块划分


由于原生态BERT预训练模子动辄几百兆甚至上千兆的巨细,模子训练速率异常慢,对于BERT模子线上化异常不友好,以是使用现在对照火的BERT最新派生产物ALBERT来完成BERT线上化服务。ALBERT使用参数削减手艺来降低内存消耗从而最终到达提高BERT的训练速率,而且在主要基准测试中均压倒一切,可谓跑的快,还跑的好。本篇解读的BERT源码也是基于ALBERT开源项目。

项目开源的github工程:https://github.com/wilsonlsm006/albert_zh

主要解读分类器部门的源码,代码及注释在run_classifier.py文件,迎接小伙伴们fork。

02 数据处置模块


数据处置模块主要卖力数据读入和预处置功效。

数据处置主要由数据处置器DataProcessor来完成。凭据差别的义务会有差别的数据处置器子类,这里的差别显示在数据读入方式和数据预处置方面。

1. 数据读入方式

现实项目中数据读入的方式多种多样,好比csv、tsv、txt等。好比有的项目是需要读取csv文件,而有的则需要tsv或者txt花样。我们可以构建自定义的数据处置器来完成差别的项目需求。

2. 数据预处置

数据预处置是凭据差别的NLP义务来完成差别的操作,好比单句分类义务我们需要的是text_a和label花样。而句子相似关系判断义务需要的是text_a,text_b,label花样。其他义务也是类似的,凭据差别的NLP义务来完成数据预处置操作。


通过一个类图来解说源码中的数据处置器:

图2 数据处置器类图


对应到项目源码中,我们有一个DataProcessor父类。父类中有五个方式,划分是读取tsv文件、获得训练集、获得验证集、获得测试集和获得标签。这里可凭据营业需求增删改获取文件类型的函数,好比读取csv可以添加get_csv(input_file)等等。

 1 class DataProcessor(object):
 2     """Base class for data converters for sequence classification data sets."""
 3     def get_train_examples(self, data_dir):
 4         """Gets a collection of `InputExample`s for the train set."""
 5         raise NotImplementedError()
 6     def get_dev_examples(self, data_dir):
 7         """Gets a collection of `InputExample`s for the dev set."""
 8         raise NotImplementedError()
 9     def get_test_examples(self, data_dir):
10         """Gets a collection of `InputExample`s for prediction."""
11         raise NotImplementedError()
12     def get_labels(self):
13         """Gets the list of labels for this data set."""
14         raise NotImplementedError()
15     @classmethod
16     def _read_tsv(cls, input_file, quotechar=None):
17         """Reads a tab separated value file."""
18         with tf.gfile.Open(input_file, "r") as f:
19             reader = csv.reader(f, delimiter="\t", quotechar=quotechar)
20             lines = []
21             for line in reader:
22                 lines.append(line)
23             return lines

下面两个子类,划分是处置句子关系判断义务的SentencePairClassificationProcessor数据处置器和LCQMCPairClassificationProcessor分类的数据处置器。前面文章有讲过若是需要做单句分类的义务我们可以在这里添加一个SentenceClassifierProcess举行定制化开发。

对应到项目源码中,由于我们是句子关系判断义务,实在就是判断两句话是不是有关系,这里我们获得的最终数据花样是列表类型,详细数据花样如下:

[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....]

其中guid作为唯一识别text_a和text_b句子对的标志,可以明白为该条样例的唯一id;

text_a和text_b是需要判断的两个句子;

label字段就是标签,若是两句话相似则置为1,否则为0。

上面四个字段guid和text_a是必须的。text_b是可选的,若是为空则酿成单句分类义务,不为空则是句子关系判断义务。label在训练集和验证集是必须的,在测试集中可以不提供。

详细代码在SentencePairClassificationProcessor子类的_create_examples函数:

 1 def _create_examples(self, lines, set_type):
 2     """Creates examples for the training and dev sets."""
 3     examples = []
 4     print("length of lines:", len(lines))
 5     for (i, line) in enumerate(lines):
 6         # print('#i:',i,line)
 7         if i == 0:
 8             continue
 9         guid = "%s-%s" % (set_type, i)
10         try:
11             label = tokenization.convert_to_unicode(line[2])
12             text_a = tokenization.convert_to_unicode(line[0])
13             text_b = tokenization.convert_to_unicode(line[1])
14             examples.append(
15                 InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label))
16         except Exception:
17             print('###error.i:', i, line)
18     return examples

 

03 特征处置模块

 

特征处置模块主要的功效是将数据处置模块获得的数据转化成特征并持久化到TFRecord文件中,由file_based_convert_examples_to_features函数完成。

 1 """
 2 将数据处置模块获得的数据转化成TFRecord文件
 3 input:
 4     examples:数据花样为[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....]
 5     label_list:标签列表
 6     max_seq_length:允许的句子最大长度 
 7     tokenizer:分词器
 8     output_file:TFRecord文件存储路径
 9 output:持久化到TFRecord花样文件
10 """
11 def file_based_convert_examples_to_features(
12         examples, 
13         label_list, 
14         max_seq_length, 
15         tokenizer, output_file):

1. 预处置数据转化成特征

数据转化成特征的操作主要由函数convert_single_example完成。传统的机械学习需要从数据中抽取特征,NLP义务是对文本举行分词等操作获取特征。BERT模子中默认每个字字就是一个词。

 1 """
 2 将预处置数据加工成模子需要的特征
 3 input:
 4     ex_index:数据条数索引
 5     example:数据花样为[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....]
 6     label_list:标签列表
 7     max_seq_length:允许的句子最大长度,这里若是输入句子长度不足则补0
 8     tokenizer:分词器
 9 output:  feature = InputFeatures(
10   input_ids=input_ids:token embedding:示意词向量,第一个词是CLS,分开词有SEP,是单词自己
11   input_mask=input_mask:position embedding:为了令transformer感知词与词之间的位置关系
12   segment_ids=segment_ids:segment embedding:text_a与text_b的句子关系
13   label_id=label_id:标签
14   is_real_example=True)
15 """
16 def convert_single_example(ex_index, example, 
17                 label_list, max_seq_length,tokenizer):
18     ....
19     feature = InputFeatures(
20         input_ids=input_ids,
21         input_mask=input_mask,
22         segment_ids=segment_ids,
23         label_id=label_id,
24         is_real_example=True)
25     return feature

论文中BERT模子的输入转化成特征如下图所示:

图3 句子输入转化成三层Embedding

这里需要注重下对text_a和text_b的预处置操作。首先会举行符号化将text_a和text_b转化成tokens_a和tokens_b。若是tokens_b存在,那么tokens_a和tokens_b的长度就不能跨越max_seq_length-3,由于需要加入cls,sep,seq三个符号;若是tokens_b不存在,那么tokens_a的长度不能跨越 max_seq_length -2 ,由于需要加入 cls 和 sep符号。

这里通过一条详细的数据转化成特征说明上述流程。现在我们的example中有一条数据,划分有三个字段:

text_a: 这种图片是用什么软件制作的?

text_b: 这种图片制作是用什么软件呢?

label: 1

经由分词之后,我们会获得:

tokens: [CLS] 这 种 图 片 是 用 什 么 软 件 制 作 的 ? [SEP] 这 种 图 片 制 作 是 用 什 么 软 件 呢 ? [SEP]

其中[CLS]是模子分外增添的最先标志,说明这是句首位置。[SEP]代表分开符,我们会将两句话拼接成一句话,通过分开符来识别。第二句话拼接完成后也会加上一个分开符。这里需要注重的是BERT对于中文分词是以每个字举行切分,并不是我们通常明白的根据中文现实的词举行切分。

经由特征提取之后酿成了:

input_ids:101 6821 4905 1745 4275 3221 4500 784 720 6763 816 1169 868 46388043 102 6821 4905 1745 4275 1169 868 3221 4500 784 720 6763 816 1450 8043 1020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

input_mask:1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0

segment_ids:0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0

label_id:1

这里详细说下我们真正给模子输入的特征是什么。

input_ids代表词向量编码。NLP义务中我们会将文本转化成词向量的表征形式提供给模子。通过BERT源码中的tokenizer将句子拆分成字,而且将字映射成id。好比上面例子中第一句话有14个字,第二句话也有14个字,再加上一个最先标志和两个分开符,一种有31个字。而上面例子中的input_ids列表中前31个位置都有每个字映射的id,而且相同字的映射的id也是一样的。其他则通过添加0举行填充;

input_mask代表位置编码。为了transformer感知词与词之间的位置关系,源码中会将当前位置有字的设置为1,其他用0举行填充;

segment_ids代表句子关系编码。若是是句子关系判断义务则会将text_b位置对应的句子关系编码置为1。这里需要注重,只要是句子关系判断义务,不管两句话到底有没有关系,即标签是否为1都会将text_b位置对应的句子关系编码置为1;

label_id就代表两句话是不是有关系。若是有关系则标签置为1,否则为0。

2. 特征存储在TFRecord花样文件

当我们举行模子训练的时刻,会将所有训练数据加载到内存中。对于小规模数据集来说没有问题,然则遇到大规模数据集时我们的内存并不能加载所有的数据,以是涉及到分批加载数据。Tensorflow给开发者提供了TFRecord花样文件。TFRecord内部接纳二进制编码,加载快,对大型数据转换友好。

小结下,特征处置模块主要将预处置获得的数据转化成特征并存储到TFRecord花样文件。BERT会将句子输入转化成三层Embedding编码,第一层是词编码,主要示意词自己;第二层编码是位置编码,主要为了transformer感知词与词之间的位置关系;第三层编码则示意句与句之间关系。通过这三层编码我们就获得了模子的特征输入。为了利便大数据集下模子训练加载数据,我们将特征持久化到TFRecord花样文件。

04 模子构建模块

模子构建模块主要分成模子构建和模子尺度输入。

1. 模子构建

通过函数model_fn_builder来构建自定义模子估量器。

 1 """
 2 自定义模子估量器(model_fn_builder)
 3 input:bert_config:bert相关的设置
 4       num_labels:标签的数目
 5       init_checkpoint:预训练模子
 6       learning_rate:学习率
 7       num_train_steps:模子训练轮数 = (训练集总数/batch_size)*epochs
 8       num_warmup_steps:线性地增添学习率,num_warmup_steps = num_train_steps * warmup_proportion 
 9       use_tpu:是否使用TPU
10 output:构建好的模子
11 """
12 def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate,
13                      num_train_steps, num_warmup_steps, use_tpu,
14                      use_one_hot_embeddings):
15     """Returns `model_fn` closure for TPUEstimator."""
16     ......
17     return model_fn

这里模子构建主要有create_model函数完成,主要完成两件事:第一是挪用modeling.py中的BertModel类确立模子;第二是盘算交织熵损失loss。交织熵的值越小,两个概率漫衍就越靠近。

 1 """
 2 确立模子,主要完成两件事:第一件事是挪用modeling.py中国的BertModel类确立模子;
 3 第二件事事盘算交织熵损失loss。交织熵的值越小,两个概率漫衍就越靠近。
 4 """
 5 def create_model(bert_config, is_training, input_ids, input_mask, segment_ids,
 6                  labels, num_labels, use_one_hot_embeddings):
 7     """Creates a classification model."""
 8     # 确立一个BERT分类模子(create_model)
 9     model = modeling.BertModel(
10         config=bert_config,
11         is_training=is_training,
12         input_ids=input_ids,
13         input_mask=input_mask,
14         token_type_ids=segment_ids,
15         use_one_hot_embeddings=use_one_hot_embeddings)
16     ......
17     return (loss, per_example_loss, logits, probabilities)
 

2. 模子尺度输入

由于源项目是基于Tensorflow框架开发,以是需要将前面获得的特征转化成尺度的Tensorflow模子输入花样。这块主要由函数file_based_input_fn_builder来完成。通过输入文件的差别可以完成训练集、验证集和测试集的输入。

 1 """
 2 模子尺度输入
 3 从TFRecord花样文件中读取特征并转化成TensorFlow尺度的数据输入花样
 4 input:input_file:
 5     input_file=train_file:输入文件,可以是训练集、验证集和展望集
 6     seq_length=FLAGS.max_seq_length:句子最大长度
 7     is_training=True:是否训练标志
 8     drop_remainder=True:示意在少于batch_size元素的情况下是否应删除最后一批 ; 默认是不删除。
 9 output:TensorFlow尺度的花样输入
10 """
11 def file_based_input_fn_builder(input_file, seq_length, is_training,
12                                 drop_remainder):
13   name_to_features = {
14         "input_ids": tf.FixedLenFeature([seq_length], tf.int64),
15         "input_mask": tf.FixedLenFeature([seq_length], tf.int64),
16         "segment_ids": tf.FixedLenFeature([seq_length], tf.int64),
17         "label_ids": tf.FixedLenFeature([], tf.int64),
18         "is_real_example": tf.FixedLenFeature([], tf.int64),
19     }
20   ......
21   return input_fn
 

这里需要注重的是is_training字段,对于训练数据,需要大量的并行读写和打乱顺序;而对于验证数据,我们不希望打乱数据,是否并行也不关心。

小结下,模子构建模块主要由模子构建和模子尺度输入两部门。模子构建卖力确立和设置BERT模子。模子尺度输入则读取TFRecord花样文件并转化成尺度的模子输入,凭据输入文件的差别完成训练集、验证集和测试集的尺度输入。

05 模子运行模块

上面模子构建好了之后即可运行模子。Tensorflow中模子运行需要构建一个Estimator工具。主要通过源码中tf.contrib.tpu.TPUEstimator()来构建。

 1 """
 2 Estimator工具包装由model_fn指定的模子
 3 input:给定输入和其他一些参数
 4     use_tpu:是否使用TPU
 5     model_fn:前面构建好的模子
 6     config:模子运行相关的设置
 7     train_batch_size:训练batch巨细
 8     eval_batch_size:验证batch巨细
 9     predict_batch_size:展望batch巨细
10 output:需要举行训练、盘算,或展望的操作
11 """
12 estimator = tf.contrib.tpu.TPUEstimator(
13     use_tpu=FLAGS.use_tpu,
14     model_fn=model_fn,
15     config=run_config,
16     train_batch_size=FLAGS.train_batch_size,
17     eval_batch_size=FLAGS.eval_batch_size,
18     predict_batch_size=FLAGS.predict_batch_size)

1. 模子训练

模子训练通过estimator.train即可完成:

1 if FLAGS.do_train:
2     train_input_fn = file_based_input_fn_builder(
3         input_file=train_file,
4         seq_length=FLAGS.max_seq_length,
5         is_training=True,
6         drop_remainder=True)
7     ....
8     estimator.train(input_fn=train_input_fn, max_steps=num_train_steps)

2. 模子验证

模子验证通过estimator.evaluate即可完成:

1 if FLAGS.do_eval:
2     eval_input_fn = file_based_input_fn_builder(
3             input_file=eval_file,
4             seq_length=FLAGS.max_seq_length,
5             is_training=False,
6             drop_remainder=eval_drop_remainder)
7     ....
8     result = estimator.evaluate(input_fn=eval_input_fn, steps=eval_steps, checkpoint_path=filename)

3. 模子展望

模子展望通过estimator.predict即可完成:

1 if FLAGS.do_predict:
2     predict_input_fn = file_based_input_fn_builder(
3             input_file=predict_file,
4             seq_length=FLAGS.max_seq_length,
5             is_training=False,
6             drop_remainder=predict_drop_remainder)
7     ....
8     result = estimator.predict(input_fn=predict_input_fn)

 

06 其他模块

1. tf日志模块

1 import tensorflow as tf
2 # 日志的显示品级
3 tf.logging.set_verbosity(tf.logging.INFO) 
4 # 打印提醒日志
5 tf.logging.info("***** Runningtraining *****")
6 # 打印传参日志
7 tf.logging.info("  Num examples = %d", len(train_examples))

2. 外部传参模块

1 import tensorflow as tf
2 flags = tf.flags
3 FLAGS = flags.FLAGS
4 flags.DEFINE_string(
5    "data_dir", None,
6    "The input data dir. Should contain the .tsv files (or other datafiles) "
7 "for thetask.")
8 # 设置哪些参数是必须要传入的
9 flags.mark_flag_as_required("data_dir")
 

总结

本篇主要解说BERT中分类器部门的源码。整体来看主要分成数据处置模块、特征处置模块、模子构建模块和模子运行模块。数据处置模块主要卖力数据读入和预处置事情;特征处置模块卖力将预处置后的数据转化成特征并持久化到TFRecord花样文件中;模子构建模块主要卖力构建BERT模子和模子尺度输入数据准备;模子运行模块主要卖力模子训练、验证和展望。通过整体到局部的方式我们可以对BERT中的分类器源码有深入的领会。后面可以凭据现实的营业需求对分类器举行二次开发。

 

最新最全的文章请关注我的微信民众号:数据拾光者。

 

,

诚信在线

诚信在线 www.nzg8.com自与农展馆合作以来,拓展了业务战线,深化了服务体系,整合了群体,在未来的2019年,将能更好地为诚信在线娱乐网的会员提供更优质的服务。

版权声明

本文仅代表作者观点,
不代表本站dafa888的立场。
本文系作者授权发表,未经许可,不得转载。

评论