BERT (Bidirectional Encoder Representations from Transformers) 官方代码库 https://github.com/google-research/bert 包含了 BERT 的实现代码与使用 BERT 进行文本分类和问题回答两个 demo. 本文对官方代码库的结构进行整理和分析, 并在此基础上介绍本地数据集使用 BERT 进行 finetune 的操作流程. BERT 的原理介绍见参考文献[3].
BERT 是一种能够生成句子中词向量表示以及句子向量表示的深度学习模型, 其生成的向量表示可以用于词级别的自然语言处理任务 (如序列标注) 和句子级别的任务(如文本分类).
从头开始训练 BERT 模型所需要的计算量很大, 但 Google 公开了在多种语言 (包括中文) 上预训练好的 BERT 模型参数, 因此可以在此基础上, 对自定义的任务进行 finetune. 相比于从头训练 BERT 模型的参数, 对自定义任务进 行 finetune 所需的计算量要小得多.
本文的第一部分对 BERT 的官方代码结构进行介绍. 第二部分以文本分类任务为例, 介绍在自己的数据集上对 BERT 模型进行 finetune 的操作流程.
1. BERT 实现代码
BERT 官方项目的目录结构如下图所示:
下文中将分别介绍项目中各模块的结构和功能.
1.1 modeling.py
如下图所示, modeling.py 定义了 BERT 模型的主体结构, 即从 input_ids(句子中词语 id 组成的 tensor)到 sequence_output(句子中每个词语的向量表示)以及 pooled_output(句子的向量表示)的计算过程, 是其它所有后续的任务的基础. 如文本分类任务就是得到输入的 input_ids 后, 用 BertModel 得到句子的向量表示, 并将其作为分类层的输入, 得到分类结果.
modeling.py 的 31-106 行定义了一个 BertConfig 类, 即 BertModel 的配置, 在新建一个 BertModel 类时, 必须配置其对应的 BertConfig.BertConfig 类包含了一个 BertModel 所需的超参数, 除词表大小 vocab_size 外, 均定义了其默认取值. BertConfig 类中还定义了从 python dict 和 JSON 中生成 BertConfig 的方法以及将 BertConfig 转换为 python dict 或者 JSON 字符串的方法.
107-263 行定义了一个 BertModel 类. BertModel 类初始化时, 需要填写三个没有默认值的参数:
config: 即 31-106 行定义的 BertConfig 类的一个对象;
is_training: 如果训练则填 true, 否则填 false, 该参数会决定是否执行 dropout.
input_ids: 一个
[batch_size, seq_length]
的 tensor, 包含了一个 batch 的输入句子中的词语 id.
另外还有 input_mask,token_type_ids 和 use_one_hot_embeddings,scope 四个可选参数, scope 参数会影响计算图中 tensor 的名字前缀, 如不填写, 则前缀为 "bert". 在下文中, 其余参数会在使用时进行说明.
BertModel 的计算都在__init__函数中完成. 计算流程如下:
为了不影响原 config 对象, 对 config 进行 deepcopy, 然后对 is_training 进行判断, 如果为 False, 则将 config 中 dropout 的概率均设为 0.
定义 input_mask 和 token_type_ids 的默认取值(前者为全 1, 后者为全 0),shape 均和 input_ids 相同. 二者的用途会在下文中提及.
使用 embedding_lookup 函数, 将 input_ids 转化为向量, 形状为
[batch_size, seq_length, embedding_size]
, 这里的 embedding_table 使用 tf.get_variable, 因此第一次调用时会生成, 后续都是直接获取现有的. 此处 use_one_hot_embedding 的取值只影响 embedding_lookup 函数的内部实现, 不影响结果.
调用 embedding_postprocessor 对输入句子的向量进行处理. 这个函数分为两部分, 先按照 token_type_id(即输入的句子中各个词语的 type, 如对两个句子的分类任务, 用 type_id 区分第一个句子还是第二个句子),lookup 出各个词语的 type 向量, 然后加到各个词语的向量表示中. 如果 token_type_id 不存在(即不使用额外的 type 信息), 则跳过这一步. 其次, 这个函数计算 position_embedding: 即初始化一个 shape 为
[max_positition_embeddings, width]
的 position_embedding 矩阵, 再按照对应的 position 加到输入句子的向量表示中. 如果不使用 position_embedding, 则跳过这一步. 最后对输入句子的向量进行 layer_norm 和 dropout, 如果不是训练阶段, 此处 dropout 概率为 0.0, 相当于跳过这一步.
根据输入的 input_mask(即与句子真实长度匹配的 mask, 如 batch_size 为 2, 句子实际长度分别为 2,3, 则 mask 为
[[1, 1, 0], [1, 1, 1]]
), 计算 shape 为
[batch_size, seq_length, seq_length]
的 mask, 并将输入句子的向量表示和 mask 共同传给 transformer_model 函数, 即 encoder 部分.
transformer_model 函数的行为是先将输入的句子向量表示 reshape 成
[batch_size * seq_length, width]
的矩阵, 然后循环调用 transformer 的前向过程, 次数为隐藏层个数. 每次前向过程都包含 self_attention_layer,add_and_norm,feed_forward 和 add_and_norm 四个步骤, 具体信息可参考 transformer 的论文.
获取 transformer_model 最后一层的输出, 此时 shape 为
[batch_size, seq_length, hidden_size]
. 如果要进行句子级别的任务, 如句子分类, 需要将其转化为
[batch_size, hidden_size]
的 tensor, 这一步通过取第一个 token 的向量表示完成. 这一层在代码中称为 pooling 层.
BertModel 类提供了接口来获取不同层的输出, 包括:
embedding 层的输出, shape 为
[batch_size, seq_length, embedding_size]
pooling 层的输出, shape 为
[batch_size, hidden_size]
sequence 层的输出, shape 为
[batch_size, seq_length, hidden_size]
encoder 各层的输出
embedding_table
modeling.py 的其余部分定义了上面的步骤用到的函数, 以及激活函数等.
1.2 run_classifier.py
这个模块可以用于配置和启动基于 BERT 的文本分类任务, 包括输入样本为句子对的 (如 MRPC) 和输入样本为单个句子的(如 CoLA).
模块中的内容包括:
InputExample 类. 一个输入样本包含 id,text_a,text_b 和 label 四个属性, text_a 和 text_b 分别表示第一个句子和第二个句子, 因此 text_b 是可选的.
PaddingInputExample 类. 定义这个类是因为 TPU 只支持固定大小的 batch, 在 eval 和 predict 的时候需要对 batch 做 padding. 如不使用 TPU, 则无需使用这个类.
InputFeatures 类, 定义了输入到 estimator 的 model_fn 中的 feature, 包括 input_ids,input_mask,segment_ids(即 0 或 1, 表明词语属于第一个句子还是第二个句子, 在 BertModel 中被看作 token_type_id),label_id 以及 is_real_example.
DataProcessor 类以及四个公开数据集对应的子类. 一个数据集对应一个 DataProcessor 子类, 需要继承四个函数: 分别从文件目录中获得 train,eval 和 predict 样本的三个函数以及一个获取 label 集合的函数. 如果需要在自己的数据集上进行 finetune, 则需要实现一个 DataProcessor 的子类, 按照自己数据集的格式从目录中获取样本. 注意! 在这一步骤中, 对没有 label 的 predict 样本, 要指定一个 label 的默认值供统一的 model_fn 使用.
convert_single_example 函数. 可以对一个 InputExample 转换为 InputFeatures, 里面调用了 tokenizer 进行一些句子清洗和预处理工作, 同时截断了长度超过最大值的句子.
file_based_convert_example_to_features 函数: 将一批 InputExample 转换为 InputFeatures, 并写入到 tfrecord 文件中, 相当于实现了从原始数据集文件到 tfrecord 文件的转换.
file_based_input_fn_builder 函数: 这个函数用于根据 tfrecord 文件, 构建 estimator 的 input_fn, 即先建立一个 TFRecordDataset, 然后进行 shuffle,repeat,decode 和 batch 操作.
create_model 函数: 用于构建从 input_ids 到 prediction 和 loss 的计算过程, 包括建立 BertModel, 获取 BertModel 的 pooled_output, 即句子向量表示, 然后构建隐藏层和 bias, 并计算 logits 和 softmax, 最终用 cross_entropy 计算出 loss.
model_fn_builder: 根据 create_model 函数, 构建 estimator 的 model_fn. 由于 model_fn 需要 labels 输入, 为简化代码减少判断, 当要进行 predict 时也要求传入 label, 因此 DataProcessor 中为每个 predict 样本生成了一个默认 label(其取值并无意义). 这里构建的是 TPUEstimator, 但没有 TPU 时, 它也可以像普通 estimator 一样工作.
input_fn_builder 和 convert_examples_to_features 目前并没有被使用, 应为开放供开发者使用的功能.
main 函数:
首先定义任务名称和 processor 的对应关系, 因此如果定义了自己的 processor, 需要将其加入到 processors 字典中.
其次从 FLAGS 中, 即启动命令中读取相关参数, 构建 model_fn 和 estimator, 并根据参数中的 do_train,do_eval 和 do_predict 的取值决定要进行 estimator 的哪些操作.
1.3 run_pretraining.py
这个模块用于 BERT 模型的预训练, 即使用 masked language model 和 next sentence 的方法, 对 BERT 模型本身的参数进行训练. 如果使用现有的预训练 BERT 模型在文本分类 / 问题回答等任务上进行 fine_tune, 则无需使用 run_pretraining.py.
1.4 create_pretraining_data.py
此处定义了如何将普通文本转换成可用于预训练 BERT 模型的 tfrecord 文件的方法. 如果使用现有的预训练 BERT 模型在文本分类 / 问题回答等任务上进行 fine_tune, 则无需使用 create_pretraining_data.py.
1.5 tokenization.py
此处定义了对输入的句子进行预处理的操作, 预处理的内容包括:
转换为 Unicode
切分成数组
去除控制字符
统一空格格式
切分中文字符(即给连续的中文字符之间加上空格)
将英文单词切分成小片段 (如["unaffable"] 切分为["un", "##aff", "##able"])
大小写和特殊形式字母转换
分离标点符号 (如 ["hello?"] 转换为 ["hello", "?"])
1.6 run_squad.py
这个模块可以配置和启动基于 BERT 在 squad 数据集上的问题回答任务.
1.7 extract_features.py
这个模块可以使用预训练的 BERT 模型, 生成输入句子的向量表示和输入句子中各个词语的向量表示(类似 ELMo). 这个模块不包含训练的过程, 只是执行 BERT 的前向过程, 使用固定的参数对输入句子进行转换.
1.8 optimization.py
这个模块配置了用于 BERT 的 optimizer, 即加入 weight decay 功能和 learning_rate warmup 功能的 AdamOptimizer.
2. 在自己的数据集上 finetune
BERT 官方项目搭建了文本分类模型的 model_fn, 因此只需定义自己的 DataProcessor, 即可在自己的文本分类数据集上进行训练.
训练自己的文本分类数据集所需步骤如下:
下载预训练的 BERT 模型参数文件, 如( ), 解压后的目录应包含 bert_config.JSON,
- bert_model.ckpt.data-00000-of-00001
- ,
- bert_model.ckpt.index
- ,
- bert_model_ckpt.meta
和 vocab.txt 五个文件.
将自己的数据集统一放到一个目录下. 为简便起见, 事先将其划分成 train.txt,eval.txt 和 predict.txt 三个文件, 每个文件中每行为一个样本, 格式如下(可以使用任何自定义格式, 只需要编写符合要求的 DataProcessor 子类即可):
simplistic , silly and tedious . __label__0
即句子和标签之间用__label__划分, 句子中的词语之间用空格划分.
修改 run_classifier.py, 或者复制一个副本, 命名为
run_custom_classifier.py
或类似文件名后进行修改.
新建一个 DataProcessor 的子类, 并继承三个 get_examples 方法和一个 get_labels 方法. 三个 get_examples 方法需要从数据集目录中获得各自对应的 InputExample 列表. 以 get_train_examples 方法为例, 该方法需要传入唯一的一个参数 data_dir, 即数据集所在目录, 然后根据该目录读取训练数据, 将所有用于训练的句子转换为 InputExample, 并返回所有 InputExample 组成的列表. get_dev_examples 和 get_test_examples 方法同理. get_labels 方法仅需返回一个所有 label 的集合组成的列表即可. 本例中 get_train_examples 方法和 get_labels 方法的实现如下(此处省略 get_dev_examples 和 get_test_examples):
class RtPolarityProcessor(DataProcessor): """Processor of the rt-polarity data set""" @staticmethod def read_raw_text(input_file): with tf.gfile.Open(input_file, "r") as f: lines = f.readlines() return lines def get_train_examples(self, data_dir): """See base class""" lines = self.read_raw_text(os.path.join(data_dir, "train.txt")) examples = [] for i, line in enumerate(lines): guid = "train-%d" % (i + 1) line = line.strip().split("__label__") text_a = tokenization.convert_to_unicode(line[0]) label = line[1] examples.append( InputExample(guid=guid, text_a=text_a, label=label) ) return examples def get_labels(self): return ["0", "1"]
在 main 函数中, 向 main 函数开头的 processors 字典增加一项, key 为自己的数据集的名称, value 为上一步中定义的 DataProcessor 的类名:
processors = { "cola": ColaProcessor, "mnli": MnliProcessor, "mrpc": MrpcProcessor, "xnli": XnliProcessor, "rt_polarity": RtPolarityProcessor, }
执行 python run_custom_classifier.py, 启动命令中包含必填参数 data_dir,task_name,vocab_file,bert_config_file,output_dir. 参数 do_train,do_eval 和 do_predict 分别控制了是否进行训练, 评估和预测, 可以按需将其设置为 True 或者 False, 但至少要有一项设为 True.
为了从预训练的 checkpoint 开始 finetune, 启动命令中还需要配置 init_checkpoint 参数. 假设 BERT 模型参数文件解压后的路径为
/uncased_L-12_H-768_A-12
, 则将 init_checkpoint 参数配置为
/uncased_L-12_H-768_A-12/bert_model.ckpt
. 其它可选参数, 如 learning_rate 等, 可参考文件中 FLAGS 的定义自行配置或使用默认值.
在没有 TPU 的情况下, 即使使用了 GPU, 这一步有可能会在日志中看到
Running train on CPU
字样. 对此, 官方项目的 readme 中做出了解释:"Note: You might see a message
Running train on CPU
. This really just means that it's running on something other than a Cloud TPU, which includes a GPU.", 因此无需在意.
如果需要训练文本分类之外的模型, 如命名实体识别, BERT 的官方项目中没有完整的 demo, 因此需要设计和实现自己的 model_fn 和 input_fn. 以命名实体识别为例, model_fn 的基本思路是, 根据输入句子的 input_ids 生成一个 BertModel, 获得 BertModel 的 sequence_output(shape 为[batch_size,max_length,hidden_size]), 再结合全连接层和 crf 等函数进行序列标注.
这是 BERT 介绍的第一篇文章. 后续我们会将 BERT 整合进智能钛机器学习平台 https://tio.cloud.tencent.com/ , 并基于智能钛机器学习平台, 讲解 BERT 用于文本分类, 序列化标注, 问答等任务的细节, 并对比其他方法, 给出 benchmark.
3. 参考文献
[1] BERT 论文: BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding https://arxiv.org/pdf/1810.04805.pdf
[2] 官方代码库 https://github.com/google-research/bert
[3] BERT 原理简介
来源: https://www.qcloud.com/developer/article/1454853