Bert Code

阅读bert代码

run_classifier.py

主要过程就是:先定义好参数,然后利用数据类读取微调的数据文件,将其每一行转成InputExample对象,然后利用file_based_convert_examples_to_features保存到TFrecord中。然后定义好input_fn和model_fn给Estimator,进行推断。

定义参数

其中data_dir即本次需要微调的新数据;vocab_file为bert模型预训练时候用的词典;uncased表示不保留大小写,都变成小写;cased表示保留大小写。

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import collections
import csv
import os
import modeling
import optimization
import tokenization
import tensorflow as tf

flags = tf.flags

FLAGS = flags.FLAGS

## Required parameters
flags.DEFINE_string(
    "data_dir", None,
    "The input data dir. Should contain the .tsv files (or other data files) "
    "for the task.")

flags.DEFINE_string(
    "bert_config_file", None,
    "The config json file corresponding to the pre-trained BERT model. "
    "This specifies the model architecture.")

flags.DEFINE_string("task_name", None, "The name of the task to train.")

flags.DEFINE_string("vocab_file", None,
                    "The vocabulary file that the BERT model was trained on.")

flags.DEFINE_string(
    "output_dir", None,
    "The output directory where the model checkpoints will be written.")

## Other parameters

flags.DEFINE_string(
    "init_checkpoint", None,
    "Initial checkpoint (usually from a pre-trained BERT model).")

flags.DEFINE_bool(
    "do_lower_case", True,
    "Whether to lower case the input text. Should be True for uncased "
    "models and False for cased models.")

flags.DEFINE_integer(
    "max_seq_length", 128,
    "The maximum total input sequence length after WordPiece tokenization. "
    "Sequences longer than this will be truncated, and sequences shorter "
    "than this will be padded.")

flags.DEFINE_bool("do_train", False, "Whether to run training.")

flags.DEFINE_bool("do_eval", False, "Whether to run eval on the dev set.")

flags.DEFINE_bool("do_predict", False, "Whether to run the model in inference mode on the test set.")

flags.DEFINE_integer("train_batch_size", 32, "Total batch size for training.")

flags.DEFINE_integer("eval_batch_size", 8, "Total batch size for eval.")

flags.DEFINE_integer("predict_batch_size", 8, "Total batch size for predict.")

flags.DEFINE_float("learning_rate", 5e-5, "The initial learning rate for Adam.")

flags.DEFINE_float("num_train_epochs", 3.0,
                   "Total number of training epochs to perform.")

flags.DEFINE_float(
    "warmup_proportion", 0.1,
    "Proportion of training to perform linear learning rate warmup for. "
    "E.g., 0.1 = 10% of training.")

flags.DEFINE_integer("save_checkpoints_steps", 1000,
                     "How often to save the model checkpoint.")

flags.DEFINE_integer("iterations_per_loop", 1000,
                     "How many steps to make in each estimator call.")

flags.DEFINE_bool("use_tpu", False, "Whether to use TPU or GPU/CPU.")

tf.flags.DEFINE_string(
    "tpu_name", None,
    "The Cloud TPU to use for training. This should be either the name "
    "used when creating the Cloud TPU, or a grpc://ip.address.of.tpu:8470 "
    "url.")

tf.flags.DEFINE_string(
    "tpu_zone", None,
    "[Optional] GCE zone where the Cloud TPU is located in. If not "
    "specified, we will attempt to automatically detect the GCE project from "
    "metadata.")

tf.flags.DEFINE_string(
    "gcp_project", None,
    "[Optional] Project name for the Cloud TPU-enabled project. If not "
    "specified, we will attempt to automatically detect the GCE project from "
    "metadata.")

tf.flags.DEFINE_string("master", None, "[Optional] TensorFlow master URL.")

flags.DEFINE_integer(
    "num_tpu_cores", 8,
    "Only used if `use_tpu` is True. Total number of TPU cores to use.")

main函数初始化

数据文件读取的类DataProcessor, 官方自带了4个不同数据集(Xnli, Mnli, Mrpc和Cola)的子类。

从json文件中读取bert配置。

新建分词器。

数据文件读取

定义了DataProcessor这个抽象基类,定义了get_train_examples、get_dev_examples、get_test_examples和get_labels等4个需要子类实现的方法,另外提供了一个_read_tsv函数用于读取tsv文件。

子类(文本中每行变成一个InputExample对象)

对于MRPC任务,这里定义了MrpcProcessor来基础DataProcessor。我们来看其中的get_labels和get_train_examples,其余两个抽象方法是类似的。首先是get_labels,它非常简单,这任务只有两个label。

接下来是get_train_examples:

这个函数首先使用_read_tsv读入训练文件train.tsv,然后使用_create_examples函数把每一行变成一个InputExample对象。

代码非常简单,line是一个list,line[3]和line[4]分别代表两个句子,如果是训练集合和验证集合,那么第一列line[0]就是真正的label,而如果是测试集合,label就没有意义,随便赋值成”0”。然后对于所有的字符串都使用tokenization.convert_to_unicode把字符串变成unicode的字符串。这是为了兼容Python2和Python3,因为Python3的str就是unicode,而Python2的str其实是bytearray,Python2却有一个专门的unicode类型。感兴趣的读者可以参考其实现,不感兴趣的可以忽略。

最终构造出一个InputExample对象来,它有4个属性:guid、text_a、text_b和label,guid只是个唯一的id而已。text_a代表第一个句子,text_b代表第二个句子,第二个句子可以为None,label代表分类标签。

FullTokenizer(初始化分词器,将用于后续的train/dev/test环节,对中文没啥用)

分词是我们需要重点关注的代码,因为如果想要把BERT产品化,我们需要使用Tensorflow Serving,Tensorflow Serving的输入是Tensor,把原始输入变成Tensor一般需要在Client端完成。BERT的分词是Python的代码,如果我们使用其它语言的gRPC Client,那么需要用其它语言实现同样的分词算法,否则预测时会出现问题。

代码在tokenization.py文件中,目的将词变成更细粒度的词,减少词表数量,loves, loved, loving => lov, ed, ing, es.

建立模型函数(用于构造Estimator的model_fn)

闭包就是回调函数中的内部函数用到了外部函数的参数。好处在于,这里固定了model_fn的参数个数,那么我们要传入额外的变量,要么用全局变量访问不安全,要么就可以用此闭包的形式

model_fn_bulider(返回Estimator的闭包model_fn)

create_model()建立真正的transformer模型

调用modeling.BertModel得到BERT模型,然后使用它的get_pooled_output方法得到[CLS]最后一层的输出,这是一个768(默认参数下)的向量,然后就是常规的接一个全连接层得到logits,然后softmax得到概率,之后就可以根据真实的分类标签计算loss。这时候发现关键的代码是modeling.BertModel。

利用model_fn和配置config建立Estimator对象

利用Estimator对象进行train/dev/test

通过file_based_convert_examples_to_features函数把输入的tsv文件变成TFRecord文件,便于Tensorflow处理。

将微调数据变成TFRecord文件,file_based_convert_examples_to_features,其中传入分词器,InputExample->InputFeature->tf.train.Feature->tf.train.Example->TFRecord

file_based_convert_examples_to_features函数遍历每一个example(InputExample类的对象)。然后使用convert_single_example函数把每个InputExample对象变成InputFeature。InputFeature就是一个存放特征的对象,它包括input_ids、input_mask、segment_ids和label_ids,这4个属性除了label_ids是一个int之外,其它都是int的列表,因此使用create_int_feature函数把它变成tf.train.Feature,最后构造tf.train.Example对象,然后写到TFRecord文件里。后面Estimator的input_fn会用到它。

注意file_based_convert_examples_to_features中会传入分词器,这样就意味着,微调数据,基本就是按照bert预训练的词典,进行转换。

将输入InputExample对象变成向量InputFeature对象:convert_single_example

如果两个Token序列的长度太长,那么需要去掉一些,这会用到_truncate_seq_pair函数:

这个函数很简单,如果两个序列的长度小鱼max_length,那么不用truncate,否则在tokens_a和tokens_b中选择长的那个序列来pop掉最后面的那个Token,这样的结果是使得两个Token序列一样长(或者最多a比b多一个Token)。

定义Estimator的数据读取函数input_fn,读取保存的TFRecord:file_based_input_fn_builder

input_fn,它是由file_based_input_fn_builder构造出来的,闭包。

用于让Estimator读取保存好的TFRecord对象,在train_file文件夹中。

这个函数返回一个函数input_fn。这个input_fn函数首先从文件得到TFRecordDataset,然后根据是否训练来shuffle和重复读取。然后用apply函数对每一个TFRecord进行map_and_batch,调用_decode_record函数对record进行parsing。从而把TFRecord的一条Record变成tf.Example对象,这个对象包括了input_ids等4个用于训练的Tensor。

执行操作

modeling.py

Bert构造函数

输入向量的说明:对于一个输入向量,如果是两个句子的任务,则是拼接在一个向量中,如下所示,然后如果有位置不足的,就补充pad0假数据,因此首先需要判断哪些是假数据,然后再对真数据判断,哪两句话。而batch_size就是表示有多少个输入向量一起进入训练,表示并行训练,一个batch内的输入向量不会互相干扰,只是最后算loss更新时候会用到对所有输入向量的结果进行汇总。

其中all_encoder_layers就是把每个transformer block的输出值保存下来。

BertModel.sequence_output 是取最后attenion层的输出。BertModel.pooled_outputsequence_output的第一个token“CLS”的emb,然后加个连接层。

embedding_lookup函数用于实现词的Embedding,即从词变成id

Embedding本来很简单,使用tf.nn.embedding_lookup就行了。但是为了优化TPU,它还支持使用矩阵乘法来提取词向量。另外为了提高效率,输入的shape除了[batch_size, seq_length]外,它还增加了一个维度变成[batch_size, seq_length, num_inputs]。如果不关心细节,我们把这个函数当成黑盒,那么我们只需要知道它的输入input_ids(可能)是[8, 128],输出是[8, 128, 768]就可以了,此外这里还返回了随机初始化的embedding_table

create_attention_mask_from_input_mask函数用于构造Mask矩阵,解决对pad的非真实词进行忽略

用途:在计算Self-Attention的时候每一个样本都需要一个Attention Mask矩阵,表示每一个时刻可以attend to的范围,1表示可以attend,0表示是padding的。

比如调用它时的两个参数是是:

表示这个batch有两个样本,第一个样本长度为3(padding了2个0),第二个样本长度为5。在计算Self-Attention的时候每一个样本都需要一个Attention Mask矩阵,表示每一个时刻可以attend to的范围,1表示可以attend,0表示是padding的(或者在机器翻译的Decoder中不能attend to未来的词)。对于上面的输入,这个函数返回一个shape是[2, 5, 5]的tensor,分别代表两个Attention Mask矩阵。

比如前面举的例子,broadcast_ones的shape是[2, 5, 1],值全是1,而to_mask是

shape是[2, 5],reshape为[2, 1, 5]。然后broadcast_ones to_mask就得到[2, 5, 5],正是我们需要的两个Mask矩阵,读者可以验证。注意**[batch, A, B][batch, B, C]=[batch, A, C]**,我们可以认为是batch个[A, B]的矩阵乘以batch个[B, C]的矩阵。

run_pretraining.py

get_masked_lm_output函数用于计算语言模型的Loss(Mask位置预测的词(即最后一层的输出向量model.get_sequence_output() )和真实的词是否相同)

model.get_sequence_output()的shape是:[batch_size, seq_length, hidden_size]。

其中表示为:batch_size是一个批次内的样本数(向量数),比如8。

而hidden_size则是每一个token被表示成的向量维度(就是一个transformer encoder中一层的维度),比如768。

而seq_length则表示有多少个transformer encoder在一个transformer block中。

model.get_embedding_table()的shape是:[vocab_size, embedding_size]。

表示为这些词典中的词随机初始化的向量,embedding_size表示为768。

masked_lm_positions表示mask词的位置,shape是[batch_size, masked_length]

masked_lm_ids表示mask词的id,shape是[batch_size, masked_length]

masked_lm_weights表示哪些是真mask,哪些是没有mask,shape是[batch_size, masked_length]

Reference

Code

run_classifier.py

Last updated

Was this helpful?