BERT 作为一个里程碑式的预训练模型,很多时候我们都是直接用训练好的 model 直接 fine-tune,对它的理解只停留在 MLM 和 NSP 上。后续的很多 SOTA 模型都是在 BERT 的基础上发展来,比如 ALBERT、RoBERTa、XLNet 之类。
这里对 BERT 创建预训练数据的源码:create_pretraining_data.py
和run_pretrain.py
进行分析和理解。
原始数据格式
- 每行一句话,每个文档中间用空格分开。
- 可以输入多个文件,也可以输出多个 tfrecord 文件
- 参考样例可以看 bert 中附带的
sample_text.txt
数据生成 tfrecord
主要为create_pretraining_data.py
分析。
生成 tfrecord 命令
1 | python create_pretraining_data.py \ |
这里并不是所有的参数,所有的参数和说明可以看create_pretraining_data.py
中的flags.DEFINE_string
。
参数说明
1 | flags.DEFINE_string("input_file", None, |
其中:
- input_file:是输入文件,就按照上面说的格式。如果有多个文件可以用逗号分开
- output_file:是输出的 tfrecord 文件,多个可以用逗号分开
- vocab_file:是词表,可以直接用 bert 模型里面的 vocab。如果重新训练也可以用自己的词表
- do_lower_case:是表示是否把输入小写
- do_whole_word_mask:表示是否要进行整个单词的 mask,而不是 word piece 的 mask。word piece 会把单词拆分,非单词首部的用##开头
- max_seq_length:表示拼接后的句子对组成的序列中包含 Wordpiece 级别的 token 数的上限,超过部分,需将较长的句子进行首尾截断
- max_predictions_per_seq:表示每个序列中需要预测的 token 的上限
- masked_lm_prob:表示生成的序列中被 masked 的 token 占总 token 数的比例。(这里的 masked 是广义的 mask,即将选中的 token 替换成[mask]或保持原词汇或随机替换成词表中的另一个词),且有如下关系
max_predictions_per_seq = max_seq_length * masked_lm_prob
- random_seed:用于复现结果,每次保持一样
- dupe_factor:对输入使用不同的 mask 的次数,会重复创建 TrainingInstance
- masked_lm_prob:mask LM 的概率,一般按照上面的公式确定
- short_seq_prob:会按照这个概率创建比最大长度短的句子
main of pretraining data
1 | def main(_): |
可以看到,这个模块的流程大概是:
- 创建 tokenizer,使用到了 vocab 和 do_lower_case 这两个参数
- 将 input files 整理到数组中
- 创建 training instances,见create_training_instances
- 将生成的每一个 TrainingInstance 对象依此转成 tf.train.Example 对象后
- 将上述生成的对象序列化到.tfrecord 格式的文件中。最终生成的.tfrecord 格式的文件是 BERT 预训练时的数据源,见write_instance_to_example_files
下面先看如何创建 training instances
create_training_instances
直接在代码上写注释了
1 | def create_training_instances(input_files, tokenizer, max_seq_length, |
这里引申出一个问题,怎么从文档生成 TrainingInstance 对象组成的 instances 列表
见create_instances_from_document
debug 截图如下所示
create_instances_from_document
同样是注释的形式
1 | def create_instances_from_document( |
debug 截图如下所示:
上面主要是解决了 BERT 中的 NSP 问题,然后又引出了两个问题:
- MLM 问题,就是那个create_masked_lm_predictions()
- 生成 TrainingInstance 问题,即TrainingInstance()
create_masked_lm_predictions
1 | def create_masked_lm_predictions(tokens, masked_lm_prob, |
最后返回给create_instances_from_document
中的
1 | (tokens, masked_lm_positions, |
TrainingInstance
在结束 Masked LM 之后就可以来创建 TrainingInstance。
在create_instances_from_document
中调用:
1 | instance = TrainingInstance( |
TrainingInstance 类别源码:
1 | class TrainingInstance(object): |
write_instance_to_example_files
返回到main 函数中可以看到,还需要把 TrainingInstance 对象写入到输出文件中,即write_instance_to_example_files(instances, tokenizer, FLAGS.max_seq_length, FLAGS.max_predictions_per_seq, output_files)
那段。
1 | def write_instance_to_example_files(instances, tokenizer, max_seq_length, |
预训练 run_pretrain
预训练命令
1 | python run_pretraining.py \ |
参数说明
1 | ## Required parameters |
其中:
- bert_config_file: 预训练模型的配置文件,直接使用对应大小 bert model 里的 config,或者自己调
- input_file: tfrecord 格式的文件
- output_dir: 预训练生成的模型文件路径,会自动创建
- init_checkpoint: 预训练模型的初始检查点,从头开始训练就不需要这个参数,fine-tune 的话就加载 bert 预训练的 ckpt
- max_seq_length: 最大序列长度,超过这个的会被阶段,不足的会补齐。要和数据生成 tfrecord 过程的一致。类似于 RNN 中的最大时间步,每次可动态调整。针对某一特定领域的语料,可在通用的语言模型的基础上,每次通过设置不同长度的专业领域的句子对微调语言模型,使最终生成的预训练的语言模型更适合某一特定领域
- train_batch_size: 训练的 mini batch 大小。如果出现内存不够的问题,那么调小 max_seq_length 或者 batch 大小就可以。
- do_train: 如果不训练只是预测 or 验证,可以设置为 false
- do_eval: 是否进行 eval 验证
- eval_batch_size: 验证的时候的 batch 大小
- learning_rate: Adam 的学习率,有论文表明越小越好,一般是 2e-5 级别
- num_train_steps: 训练的步数,如果是自己从头训练,这个步数要根据语料大小看,一般设置 w 级别。
- num_warmup_steps: warmup 步数,学习率从 0 逐渐增加到初始学习率所需的步数,以后的步数保持固定学习率。参考github-issue
- save_checkpoints_steps: 每隔多少步保存一次模型
- iterations_per_loop: 每次调用 estimator 的步数
- max_eval_steps: 最大的 eval 步数
- tup 相关参数看说明
main of pretrain
在源码中写了注释说明。
1 | def main(_): |
这里引出几个问题:
- 模型的创建: model_fn_builder
- 数据的解析: input_fn_builder
- 模型的训练:estimator.train
- 验证集的测试: estimator.evaluate
input_fn_builder
从 tfrecord 中解析出 bert 的输入数据
1 | def input_fn_builder(input_files, |
model_fn_builder
用于构造 Estimator 使用的 model_fn。包含了特征提取、模型创建、计算损失、加载 checkpoint、计算 acc,并返回一个 EstimatorSpec。
定义好了get_masked_lm_output
和get_next_sentence_output
两个训练任务后,就可以写出训练过程,之后将训练集传入自动训练。
1 | def model_fn_builder(bert_config, init_checkpoint, learning_rate, |
基于上述搭建好的模型结构及相应的损失函数,在训练阶段,利用相应的优化器(AdamWeightDecayOptimizer)优化损失函数,使其减小,并保存不同训练步数对应的模型参数,直到跑完所有步数,从而确定最终的模型结构与参数。
从这里引出几个问题:
- Bert 模型的创建,见model = modeling.BertModel(···)
- 计算 MaskedLM 的损失,见(masked_lm_loss, masked_lm_example_loss, masked_lm_log_probs) = get_masked_lm_output()
- 计算 NSP 的损失,见(next_sentence_loss, next_sentence_example_loss, next_sentence_log_probs) = get_next_sentence_output()
- 创建优化器,用来更新模型(权重)参数,见create_optimizer()
文件说明
由于 BERT 在预训练中使用了 estimator 这种高级 API 形式,在训练完成后会自动生成 ckpt 格式的模型文件(结构和数据是分开的) 及可供 tensorboard 查看的事件文件。具体文件说明如下:
checkpoint
: 记录了模型文件的路径信息列表,可以用来迅速查找最近一次的 ckpt 文件。(每个 ckpt 文件对应一个模型)其内容如下所示- model_checkpoint_path: “model.ckpt-20”
- all_model_checkpoint_paths: “model.ckpt-0”
- all_model_checkpoint_paths: “model.ckpt-20”
events.out.tfevents.1570029823.04c93f97d224
:事件文件,tensorboard 可加载显示graph.pbtxt
: 以 Protobuffer 格式描述的模型结构文件(text 格式的图文件(.pbtext),二进制格式的图文件为(.pb)),记录了模型中所有的节点信息,内容大致如下:
1 | node { |
model.ckpt-20.data-00000-of-00001
: 模型文件中的数据(the values of all variables)部分 (二进制文件)model.ckpt-20.index
: 模型文件中的映射表( Each key is a name of a tensor and its value is a serialized BundleEntryProto. Each BundleEntryProto describes the metadata of a tensor: which of the “data” files contains the content of a tensor, the offset into that file, checksum, some auxiliary data, etc.)部分 (二进制文件)model.ckpt-20.meta
: 模型文件中的(图)结构(由 GraphDef, SaverDef, MateInfoDef,SignatureDef,CollectionDef 等组成的 MetaGraphDef)部分 (二进制文件,内容和 graph.pbtxt 基本一样,其是一个序列化的 MetaGraphDef protocol buffer)
在评估阶段,直接加载训练好的模型结构与参数,对预测样本进行预测即可。
BertModel
1 | class BertModel(object): |
这里引出几个部分:
- embedding 表的构建,即embedding_lookup
- 在 word_embeddings 的基础上增加 segment_id 和 position 信息,最后将叠加后 embedding 分别进行 layer_norm,batch_norm 和 dropout 操作。见embedding_postprocessor
- transformer 模型的构建,即transformer_model
- attention 自注意力层的构建,即attention_layer
embedding_lookup
构建一个 embedding lookup 表,用于生成每个 token 的表示,同时返回 input_ids 对应的 embedding。
这里的 embedding 只包括 word_embedding,token embedding 和 position embedding 在 embedding_postprocessor 中处理
1 | def embedding_lookup(input_ids, |
embedding_postprocessor
在 word_embeddings 的基础上增加 segment_id 和 position 信息,最后将叠加后 embedding 分别进行 layer_norm(对每个样本的不同维度进行归一化操作),batch_norm(是对不同样本的同一特征进行归一化操作)和 dropout(一个张量中某几个位置的值变成 0)操作。
token_type_table 与 full_position_embeddings 为模型待学习参数。它们和 word_embedding 是对应位置相加,不改变 shape
1 | def embedding_postprocessor(input_tensor, |
transformer_model
1 | def transformer_model(input_tensor, |
attention_layer
1 | def attention_layer(from_tensor, |
get_masked_lm_output
从 BertModel 部分返回到model_fn_builder。
搞定了modeling.BertModel
,下面开始计算 Masked LM 和 NSP 任务。
两个任务的本质都是分类任务,一个是二分类,即两个 segment 是否是连贯的;一个是多分类,即输入序列中被 mask 的 token 为词表中某个 token 的概率。它们的损失函数都是交叉熵损失。
NSP 问题中 0 是连续的,1 是随机的。
在model_fn_builder
中是通过下面代码进行调用的:
1 | (masked_lm_loss, |
源码:
1 | def get_masked_lm_output(bert_config, input_tensor, output_weights, positions, |
get_next_sentence_output
model_fn_builder
中计算完了get_masked_lm_output
之后,计算get_next_sentence_output
。
调用方式:
1 | (next_sentence_loss, next_sentence_example_loss, |
源码:
1 | def get_next_sentence_output(bert_config, input_tensor, labels): |
create_optimizer
调用部分:
1 | if mode == tf.estimator.ModeKeys.TRAIN: |
实现部分:
1 | def create_optimizer(loss, init_lr, num_train_steps, num_warmup_steps, use_tpu): |
首先是学习率部分,将学习率设置为线性衰减的形式,接着根据global_step是否达到num_warmup_steps,在原来线性衰减的基础上将学习率进一步分成warmup_learning_rate和learning_rate两种方式。然后是优化器的构建。
先是实例化AdamWeightDecayOptimizer(其是梯度下降法的一种变种,也由待更新参数、学习率和参数更新方向三大要素组成),接着通过tvars = tf.trainable_variables()解析出模型中所有待训练的参数变量,并给出loss关于所有参数变量的梯度表示grads = tf.gradients(loss, tvars),同时限制梯度的大小。最后基于上述描述的梯度与变量,进行参数更新操作。更新时,依此遍历每一个待更新的参数,根据标准的Adam更新公式(参考Adam和学习率衰减(learning rate decay)),先确定参数更新方向,接着在方向的基础上增加衰减参数(这个操作叫纠正的L2 weight decay),然后在纠正后的方向上移动一定距离(learning_rate * update)后,更新现有的参数。 以上更新步骤随着训练步数不断进行,直到走完所有训练步数。
Estimator 类
tf 提供了很多预创建的 Estimator,也可以自己定义 Estimator 类。但都是基于tf.estimator.Estimator
。
介绍
Estimator 类,用来训练和验证 TensorFlow 模型:
- Estimator 对象包含了一个模型 model_fn,这个模型给定输入和参数,会返回训练、验证或者预测等所需要的操作节点。
- 所有的输出(检查点、事件文件等)会写入到 model_dir,或者其子文件夹中。如果 model_dir 为空,则默认为临时目录。
- config 参数为 tf.estimator.RunConfig 对象,包含了执行环境的信息。如果没有传递 config,则它会被 Estimator 实例化,使用的是默认配置。
- params 包含了超参数。Estimator 只传递超参数,不会检查超参数,因此 params 的结构完全取决于开发者。
- Estimator 的所有方法都不能被子类覆盖(它的构造方法强制决定的)。子类应该使用 model_fn 来配置母类,或者增添方法来实现特殊的功能。
- Estimator 不支持 Eager Execution(eager execution 能够使用 Python 的 debug 工具、数据结构与控制流。并且无需使用 placeholder、session,计算结果能够立即得出)。
初始化
__init__(self, model_fn, model_dir=None, config=None, params=None, warm_start_from=None)
构造一个 Estimator 的实例。
参数:
- model_fn: 模型函数。
- 参数:
- features: 这是 input_fn 返回的第一项(input_fn 是 train, evaluate 和 predict 的参数)。类型应该是单一的 Tensor 或者 dict。
- labels: 这是 input_fn 返回的第二项。类型应该是单一的 Tensor 或者 dict。如果 mode 为 ModeKeys.PREDICT,则会默认为 labels=None。如果 model_fn 不接受 mode,model_fn 应该仍然可以处理 labels=None。
- mode: 可选。指定是训练、验证还是测试。参见 ModeKeys。
- params: 可选,超参数的 dict。 可以从超参数调整中配置 Estimators。
- config: 可选,配置。如果没有传则为默认值。可以根据 num_ps_replicas 或 model_dir 等配置更新 model_fn。
- 返回:EstimatorSpec
- 参数:
- model_dir:
- 保存模型参数、图等的地址,也可以用来将路径中的检查点加载至 estimator 中来继续训练之前保存的模型。
- 如果是 PathLike, 那么路径就固定为它了。
- 如果是 None,那么 config 中的 model_dir 会被使用(如果设置了的话)
- 如果两个都设置了,那么必须相同;如果两个都是 None,则会使用临时目录。
- config: 配置类。
- params: 超参数的 dict,会被传递到 model_fn。keys 是参数的名称,values 是基本 python 类型。
- warm_start_from:
- 可选,字符串,检查点的文件路径,用来指示从哪里开始热启动。
- 或者是 tf.estimator.WarmStartSettings 类来全部配置热启动。
- 如果是字符串路径,则所有的变量都是热启动,并且需要 Tensor 和词汇的名字都没有变。
- 异常:
- RuntimeError: 开启了 eager execution
- ValueError:model_fn 的参数与 params 不匹配
- ValueError:这个函数被 Estimator 的子类所覆盖
train
train(self, input_fn, hooks=None, steps=None, max_steps=None, saving_listeners=None)
根据所给数据 input_fn, 对模型进行训练。
参数:
- input_fn:一个函数,提供由小 batches 组成的数据, 供训练使用。必须返回以下之一:
- 一个 ‘tf.data.Dataset’对象:Dataset 的输出必须是一个元组 (features, labels),元组要求如下。
- 一个元组 (features, labels):features 是一个 Tensor 或者一个字典(特征名为 Tensor),labels 是一个 Tensor 或者一个字典(特征名为 Tensor)。features 和 labels 都被 model_fn 所使用,应该符合 model_fn 输入的要求。
- hooks:SessionRunHook 子类实例的列表。用于在训练循环内部执行。
- steps:模型训练的步数。
- 如果是 None, 则一直训练,直到 input_fn 抛出了超过界限的异常。
- steps 是递进式进行的。如果执行了两次训练(steps=10),则总共训练了 20 次。如果中途抛出了越界异常,则训练在 20 次之前就会停止。
- 如果你不想递进式进行,请换为设置 max_steps。如果设置了 steps,则 max_steps 必须是 None。
- max_steps:模型训练的最大步数。
- 如果为 None,则一直训练,直到 input_fn 抛出了超过界限的异常。
- 如果设置了 max_steps, 则 steps 必须是 None。
- 如果中途抛出了越界异常,则训练在 max_steps 次之前就会停止。
- 执行两次 train(steps=100) 意味着 200 次训练;但是,执行两次 train(max_steps=100) 意味着第二次执行不会进行任何训练,因为第一次执行已经做完了所有的 100 次。
- saving_listeners:CheckpointSaverListener 对象的列表。用于在保存检查点之前或之后立即执行的回调函数。
返回:self:为了链接下去。
异常:
- ValueError:steps 和 max_steps 都不是 None
- ValueError:steps 或 max_steps <= 0
evaluate
evaluate(self, input_fn, steps=None, hooks=None, checkpoint_path=None, name=None)
根据所给数据 input_fn, 对模型进行验证。
对于每一步,执行 input_fn(返回数据的一个 batch)。一直进行验证,直到:
- steps 个 batches 进行完毕,或者
- input_fn 抛出了越界异常(OutOfRangeError 或 StopIteration)
参数:
- input_fn:一个函数,构造了验证所需的输入数据,必须返回以下之一:
- 一个 ‘tf.data.Dataset’对象:Dataset 的输出必须是一个元组 (features, labels),元组要求如下。
- 一个元组 (features, labels):features 是一个 Tensor 或者一个字典(特征名为 Tensor),labels 是一个 Tensor 或者一个字典(特征名为 Tensor)。features 和 labels 都被 model_fn 所使用,应该符合 model_fn 输入的要求。
- steps:模型验证的步数。如果是 None, 则一直验证,直到 input_fn 抛出了超过界限的异常。
- hooks:SessionRunHook 子类实例的列表。用于在验证内部执行。
- checkpoint_path: 用于验证的检查点路径。如果是 None, 则使用 model_dir 中最新的检查点。
- name:验证的名字。使用者可以针对不同的数据集运行多个验证操作,比如训练集 vs 测试集。不同验证的结果被保存在不同的文件夹中,且分别出现在 tensorboard 中。
返回:
- 返回一个字典,包括 model_fn 中指定的评价指标、global_step(包含验证进行的全局步数)
异常:
- ValueError:如果 step 小于等于 0
- ValueError:如果 model_dir 指定的模型没有被训练,或者指定的 checkpoint_path 为空。
predict
predict(self, input_fn, predict_keys=None, hooks=None, checkpoint_path=None, yield_single_examples=True)
对给出的特征进行预测。
参数:
- input_fn:一个函数,构造特征。预测一直进行下去,直到 input_fn 抛出了越界异常(OutOfRangeError 或 StopIteration)。函数必须返回以下之一:
- 一个 ‘tf.data.Dataset’对象:Dataset 的输出和以下的限制相同。
- features:一个 Tensor 或者一个字典(特征名为 Tensor)。features 被 model_fn 所使用,应该符合 model_fn 输入的要求。
- 一个元组,其中第一项为 features。
- predict_keys:字符串列表,要预测的键值。当 EstimatorSpec.predictions 是一个 dict 时使用。如果使用了 predict_keys, 那么剩下的预测值会从字典中过滤掉。如果是 None,则返回全部。
- hooks:SessionRunHook 子类实例的列表。用于在预测内部回调。
- checkpoint_path: 用于预测的检查点路径。如果是 None, 则使用 model_dir 中最新的检查点。
- yield_single_examples:If False, yield the whole batch as returned by the model_fn instead of decomposing the batch into individual elements. This is useful if model_fn returns some tensors whose first dimension is not equal to the batch size.
返回:
- predictions tensors 的值
异常:
- ValueError:model_dir 中找不到训练好的模型。
- ValueError:预测值的 batch 长度不同,且 yield_single_examples 为 True。
- ValueError:predict_keys 和 predictions 之间有冲突。例如,predict_keys 不是 None,但是 EstimatorSpec.predictions 不是一个 dict。
estimator.train/evaluate
了解了 Estimator 可以理解上面的estimator.train
和estimator.evaluate
了
搞定了上面的部分就可以完成model_fn_builder部分的理解,然后返回main。这下只剩estimator.train
和estimator.evaluate
了。
1 | estimator = tf.contrib.tpu.TPUEstimator( |