本文来自OPPO互联网基础技术团队,转载请注名做者。同时欢迎关注咱们的公众号:OPPO_tech,与你分享OPPO前沿互联网技术及活动。
BERT是近几年NLP领域中具备里程碑意义的存在。由于效果好和应用范围广因此被普遍应用于科学研究和工程项目中。本篇从源码的角度,从总体到局部分析BERT模型中分类器部分的源码。git
对于机器学习工程师来讲,会调包跑程序应该是万里长征的第一步。这一步主要是帮助咱们迅速将模型应用到实际业务中,而且提高自信心,但这还远远不够。要想根据不一样的业务场景更好的使用模型,咱们须要深层次的理解模型,读点源码才能走的更远。github
本篇解读的是BERT开源项目中分类器部分的源码,从最开始的数据输入到模型运行整个流程主要能够分红数据处理模块、特征处理模块、模型构建模块和模型运行模块。具体以下图所示:app
图1 BERT分类器总体模块划分框架
由于原生态BERT预训练模型动辄几百兆甚至上千兆的大小,模型训练速度很是慢,对于BERT模型线上化很是不友好,因此使用目前比较火的BERT最新派生产品ALBERT来完成BERT线上化服务。ALBERT使用参数减小技术来下降内存消耗从而最终达到提升BERT的训练速度,而且在主要基准测试中均名列前茅,可谓跑的快,还跑的好。本篇解读的BERT源码也是基于ALBERT开源项目。机器学习
项目开源的github工程:github.com/wilsonlsm006函数
主要解读分类器部分的源码,代码及注释在run_classifier.py文件,欢迎小伙伴们fork。学习
数据处理模块主要负责数据读入和预处理功能。测试
数据处理主要由数据处理器DataProcessor来完成。根据不一样的任务会有不一样的数据处理器子类,这里的不一样表如今数据读入方式和数据预处理方面。大数据
实际项目中数据读入的方式多种多样,好比csv、tsv、txt等。好比有的项目是须要读取csv文件,而有的则须要tsv或者txt格式。咱们能够构建自定义的数据处理器来完成不一样的项目需求。ui
数据预处理是根据不一样的NLP任务来完成不一样的操做,好比单句分类任务咱们须要的是text_a和label格式。而句子类似关系判断任务须要的是text_a,text_b,label格式。其余任务也是相似的,根据不一样的NLP任务来完成数据预处理操做。
经过一个类图来说解源码中的数据处理器:
图2 数据处理器类图
对应到项目源码中,咱们有一个DataProcessor父类。父类中有五个方法,分别是读取tsv文件、得到训练集、得到验证集、得到测试集和得到标签。这里可根据业务需求增删改获取文件类型的函数,好比读取csv能够添加get_csv(input_file)等等。
class DataProcessor(object): """Base class for data converters for sequence classification data sets.""" def get_train_examples(self, data_dir): """Gets a collection of `InputExample`s for the train set.""" raise NotImplementedError() def get_dev_examples(self, data_dir): """Gets a collection of `InputExample`s for the dev set.""" raise NotImplementedError() def get_test_examples(self, data_dir): """Gets a collection of `InputExample`s for prediction.""" raise NotImplementedError() def get_labels(self): """Gets the list of labels for this data set.""" raise NotImplementedError() @classmethod def _read_tsv(cls, input_file, quotechar=None): """Reads a tab separated value file.""" with tf.gfile.Open(input_file, "r") as f: reader = csv.reader(f, delimiter="\t", quotechar=quotechar) lines = [] for line in reader: lines.append(line) 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函数:
def _create_examples(self, lines, set_type): """Creates examples for the training and dev sets.""" examples = [] print("length of lines:", len(lines)) for (i, line) in enumerate(lines): # print('#i:',i,line) if i == 0: continue guid = "%s-%s" % (set_type, i) try: label = tokenization.convert_to_unicode(line[2]) text_a = tokenization.convert_to_unicode(line[0]) text_b = tokenization.convert_to_unicode(line[1]) examples.append( InputExample(guid=guid, text_a=text_a, text_b=text_b, label=label)) except Exception: print('###error.i:', i, line) return examples
特征处理模块主要的功能是将数据处理模块获得的数据转化成特征并持久化到TFRecord文件中,由file_based_convert_examples_to_features函数完成。
""" 将数据处理模块获得的数据转化成TFRecord文件 input: examples:数据格式为[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....] label_list:标签列表 max_seq_length:容许的句子最大长度 tokenizer:分词器 output_file:TFRecord文件存储路径 output:持久化到TFRecord格式文件 """ def file_based_convert_examples_to_features( examples, label_list, max_seq_length, tokenizer, output_file):
数据转化成特征的操做主要由函数convert_single_example完成。传统的机器学习须要从数据中抽取特征,NLP任务是对文本进行分词等操做获取特征。BERT模型中默认每一个字字就是一个词。
""" 将预处理数据加工成模型须要的特征 input: ex_index:数据条数索引 example:数据格式为[(guid,text_a,text_b,label),(guid,text_a,text_b,label),....] label_list:标签列表 max_seq_length:容许的句子最大长度,这里若是输入句子长度不足则补0 tokenizer:分词器 output: feature = InputFeatures( input_ids=input_ids:token embedding:表示词向量,第一个词是CLS,分隔词有SEP,是单词自己 input_mask=input_mask:position embedding:为了令transformer感知词与词之间的位置关系 segment_ids=segment_ids:segment embedding:text_a与text_b的句子关系 label_id=label_id:标签 is_real_example=True) """ def convert_single_example(ex_index, example, label_list, max_seq_length,tokenizer): .... feature = InputFeatures( input_ids=input_ids, input_mask=input_mask, segment_ids=segment_ids, label_id=label_id, is_real_example=True) 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中有一条数据,分别有三个字段:
通过分词以后,咱们会获得:
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。
当咱们进行模型训练的时候,会将所有训练数据加载到内存中。对于小规模数据集来讲没有问题,可是遇到大规模数据集时咱们的内存并不能加载所有的数据,因此涉及到分批加载数据。Tensorflow给开发者提供了TFRecord格式文件。TFRecord内部采用二进制编码,加载快,对大型数据转换友好。
小结下,特征处理模块主要将预处理获得的数据转化成特征并存储到TFRecord格式文件。BERT会将句子输入转化成三层Embedding编码,第一层是词编码,主要表示词自己;第二层编码是位置编码,主要为了transformer感知词与词之间的位置关系;第三层编码则表示句与句之间关系。经过这三层编码咱们就获得了模型的特征输入。为了方便大数据集下模型训练加载数据,咱们将特征持久化到TFRecord格式文件。
模型构建模块主要分红模型构建和模型标准输入。
经过函数model_fn_builder来构建自定义模型估计器。
""" 自定义模型估计器(model_fn_builder) input:bert_config:bert相关的配置 num_labels:标签的数量 init_checkpoint:预训练模型 learning_rate:学习率 num_train_steps:模型训练轮数 = (训练集总数/batch_size)*epochs num_warmup_steps:线性地增长学习率,num_warmup_steps = num_train_steps * warmup_proportion use_tpu:是否使用TPU output:构建好的模型 """ def model_fn_builder(bert_config, num_labels, init_checkpoint, learning_rate, num_train_steps, num_warmup_steps, use_tpu, use_one_hot_embeddings): """Returns `model_fn` closure for TPUEstimator.""" ...... return model_fn
这里模型构建主要有create_model函数完成,主要完成两件事:第一是调用modeling.py中的BertModel类建立模型;第二是计算交叉熵损失loss。交叉熵的值越小,两个几率分布就越接近。
""" 建立模型,主要完成两件事:第一件事是调用modeling.py中国的BertModel类建立模型; 第二件事事计算交叉熵损失loss。交叉熵的值越小,两个几率分布就越接近。 """ def create_model(bert_config, is_training, input_ids, input_mask, segment_ids, labels, num_labels, use_one_hot_embeddings): """Creates a classification model.""" # 创建一个BERT分类模型(create_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) ...... return (loss, per_example_loss, logits, probabilities)
由于源项目是基于Tensorflow框架开发,因此须要将前面获得的特征转化成标准的Tensorflow模型输入格式。这块主要由函数file_based_input_fn_builder来完成。经过输入文件的不一样能够完成训练集、验证集和测试集的输入。
""" 模型标准输入 从TFRecord格式文件中读取特征并转化成TensorFlow标准的数据输入格式 input:input_file: input_file=train_file:输入文件,能够是训练集、验证集和预测集 seq_length=FLAGS.max_seq_length:句子最大长度 is_training=True:是否训练标志 drop_remainder=True:表示在少于batch_size元素的状况下是否应删除最后一批 ; 默认是不删除。 output:TensorFlow标准的格式输入 """ def file_based_input_fn_builder(input_file, seq_length, is_training, drop_remainder): name_to_features = { "input_ids": tf.FixedLenFeature([seq_length], tf.int64), "input_mask": tf.FixedLenFeature([seq_length], tf.int64), "segment_ids": tf.FixedLenFeature([seq_length], tf.int64), "label_ids": tf.FixedLenFeature([], tf.int64), "is_real_example": tf.FixedLenFeature([], tf.int64), } ...... return input_fn
这里须要注意的是is_training字段,对于训练数据,须要大量的并行读写和打乱顺序;而对于验证数据,咱们不但愿打乱数据,是否并行也不关心。
小结下,模型构建模块主要由模型构建和模型标准输入两部分。模型构建负责建立和配置BERT模型。模型标准输入则读取TFRecord格式文件并转化成标准的模型输入,根据输入文件的不一样完成训练集、验证集和测试集的标准输入。
上面模型构建好了以后便可运行模型。Tensorflow中模型运行须要构建一个Estimator对象。主要经过源码中tf.contrib.tpu.TPUEstimator()来构建。
""" Estimator对象包装由model_fn指定的模型 input:给定输入和其余一些参数 use_tpu:是否使用TPU model_fn:前面构建好的模型 config:模型运行相关的配置 train_batch_size:训练batch大小 eval_batch_size:验证batch大小 predict_batch_size:预测batch大小 output:须要进行训练、计算,或预测的操做 """ 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)
模型训练经过estimator.train便可完成:
if FLAGS.do_train: 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)
模型验证经过estimator.evaluate便可完成:
if FLAGS.do_eval: 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, checkpoint_path=filename)
模型预测经过estimator.predict便可完成:
if FLAGS.do_predict: 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)
import tensorflow as tf # 日志的显示等级 tf.logging.set_verbosity(tf.logging.INFO) # 打印提示日志 tf.logging.info("***** Runningtraining *****") # 打印传参日志 tf.logging.info(" Num examples = %d", len(train_examples))
import tensorflow as tf flags = tf.flags FLAGS = flags.FLAGS flags.DEFINE_string( "data_dir", None, "The input data dir. Should contain the .tsv files (or other datafiles) " "for thetask.") # 设置哪些参数是必需要传入的 flags.mark_flag_as_required("data_dir")
本篇主要讲解BERT中分类器部分的源码。总体来看主要分红数据处理模块、特征处理模块、模型构建模块和模型运行模块。
数据处理模块主要负责数据读入和预处理工做;特征处理模块负责将预处理后的数据转化成特征并持久化到TFRecord格式文件中;模型构建模块主要负责构建BERT模型和模型标准输入数据准备;模型运行模块主要负责模型训练、验证和预测。
经过总体到局部的方式咱们能够对BERT中的分类器源码有深刻的了解。后面能够根据实际的业务需求对分类器进行二次开发。