Google开源的BERT的确很良心,代码写得非常好,是一个不错的学习案例,这里我从实战的角度从预训练到下游任务实战做一个全面的梳理。原理部分的讲解请参考我上篇博客。
这里简单说下环境:
tensorflow 1.11.0(用1.4版本的朋友建议还是装个CUDA9.0做个升级吧,我想google之后的开源应该都是基于这个版本的,所以为了能看到原汁原味的代码,还是妥协下吧!)
我也尝试过pytorch的版本,但毕竟不是官方的代码,从学习的角度建议还是用tensorflow的。
Part1:模型预训练
1.数据准备
官方给的数据形式是这样的:
这里也贴一份中文样本数据:
段和段之间用空格隔开,如果你的语料不是这种格式,需要事先进行处理,英文的官方使用的是NLP的 spaCy工具包进行的句子切割,中文的话,根据标点符号切就行啦。
2.生成Vocab
根据语料生成词典,这里注意加上以下字符,如果是中文就分个词
3.运行create_pretraining_data.py,关于这个代码,我在这做个梳理吧,虽然没啥原理性的东西,但万事开头难,NLP的数据处理永远是至关重要的一步。
这个程序是用来生成tfrecord的,另外论文中提到的所有数据处理过程都在这个程序中。
- 创建tokenizer,很多人也许会困惑这个啥,这是Google AI Language Team写的一个字符处理的工具,按照代码里的使用就行。
-逐行读入数据:
官方代码这里是这么处理每一行英文数据的,实际上可以简单理解为做了个分词操作吧。
接下来就是将数据转换成任务需要的形式了,也就是文章中的两个任务masked lm,next sentences prediction。
- 对于一份数据,可以每次将masked 设定的位置都不一样,也就是可以做个数据扩充,代码中的dupe_factor就是将数据重复多次进行处理。
最终的数据的结果,我们可以打印几条看看:
至于处理规则这里就不讲了,论文里对如何做mask和生成next也讲的比较详细 。
3.模型预训练
这个部分主要梳理下模型预训练的代码,即run_pretraining.py。
首先准备一个config文件夹,里面放bert_config.json文件,这个文件最好在刚才进行数据预处理的时候就生成以下,内容可以参考官方提供的预训练模型的内容。
- (1)设置run config
tf.contrib.learn.RunConfig用于管理Estimator运行的控制信息,代码中用的*.tpu.RunConfig,主要是还有些tpu的设置,这里主要设置好checkpoints_steps和output_dir就行。
- (2)建立模型model_fn_builder,主要是建立好里面的model_fn的回掉函数,里面的内容我后续会说明。
- (3)建立estimator API
- (4)建立input_fn_builder回掉函数,核心是里面的input_fn(params)回掉函数,params是固定参数,是一个词典,里面有batch size(创建estimator 传入)等参数。
上面这5步几乎是靠回调函数完成的,可读性并不是很好,我下面会具体说一下细节。
注:train的第一步就是调用input_fn,读取record,并产生一个batch的数据。
然后就进到model_fn中创建模型,传入的fearure数据如下:
接下来就是创建模型:
模型里面的内容如下:
- embedding:word,position,token type embedding(将这三个embedding相加),得到最后的embedding_output
- encoder :
- 根据input创建mask 函数:create_attention_mask_from_input_mask
- transformer_model 这个部分应该算整个代码的核心了,代码并不难,主要看看是不是论文里讲的那样:
- self-attention
- 残差+layer-norm(黄色框)
- Feed forward layer(intermediate)
- 残差+layer-norm
- 计算loss
先来看第一个get_masked_lm_output
- get_masked_lm_output中的gather_indexes函数就根据positions(masked的位置)从transformer的输出层里把相应的step给挑出来,例如这个句子masked了20个词,那么输出的唯独就是[64,20,96],再接着做一个非线性变化+layer_norm(貌似论文里这块没有讲)
接下来就是全连接层了,这块做法和CBOW一样,直接乘最开始初始化的embedding矩阵:
最后的输出的概率值(64*20,749):
注意这里20是指mask的最大个数,不一定20个词全mask了,所以要配合label_weights一起算
再看next_sentence_example_loss
这里注意BERT做句子层面的分类,都是用的0step的[CLS]标签,这里的get_pooled_output是0_step隐藏层接了一个非线性变化的结果:
句子层面的分类比较简单,借一个线性层就行啦。
最后把两个部分的loss加起来就行了
至此BERT的预训练就梳理到这里。