BERT源码解析
Google提供的BERT代码在这里,我们可以直接git clone下来。注意运行它需要Tensorflow 1.11及其以上的版本,低版本的Tensorflow不能运行。
由于从头开始(from scratch)训练需要巨大的计算资源,因此Google提供了预训练的模型(的checkpoint),目前包括英语、汉语和多语言3类模型,而英语又包括4个版本:
BERT-Base, Uncased 12层,768个隐单元,12个Attention head,110M参数
BERT-Large, Uncased 24层,1024个隐单元,16个head,340M参数
BERT-Base, Cased 12层,768个隐单元,12个Attention head,110M参数
BERT-Large, Uncased 24层,1024个隐单元,16个head,340M参数。
Uncased的意思是保留大小写,而cased是在预处理的时候都变成了小写。
对于汉语只有一个版本:BERT-Base, Chinese: 包括简体和繁体汉字,共12层,768个隐单元,12个Attention head,110M参数。另外一个多语言的版本是BERT-Base, Multilingual Cased (New, recommended),它包括104种不同语言,12层,768个隐单元,12个Attention head,110M参数。它是用所有这104中语言的维基百科文章混在一起训练出来的模型。所有这些模型的下载地址都在这里。
1. BERT实现代码
读取数据
DataProcessor类是一个抽象基类,定义了get_train_examples、get_dev_examples、get_test_examples和get_labels等4个需要子类实现的方法,另外提供了一个_read_tsv函数用于读取tsv文件。
对于不同的数据集,有不同的读取数据类,比如实现类MrpcProcessor。
get_train_examples函数首先使用
_read_tsv读入训练文件train.tsv,然后使用_create_examples函数把每一行变成一个InputExample对象。InputExample对象有4个属性:guid、text_a、text_b和label,guid只是个唯一的id而已。text_a代表第一个句子,text_b代表第二个句子,第二个句子可以为None,label代表分类标签。
分词
BERT里分词主要是由FullTokenizer类来实现的。
class FullTokenizer(object):
def __init__(self, vocab_file, do_lower_case=True):
self.vocab = load_vocab(vocab_file)
self.basic_tokenizer = BasicTokenizer(do_lower_case=do_lower_case)
self.wordpiece_tokenizer = WordpieceTokenizer(vocab=self.vocab)
def tokenize(self, text):
split_tokens = []
for token in self.basic_tokenizer.tokenize(text):
for sub_token in self.wordpiece_tokenizer.tokenize(token):
split_tokens.append(sub_token)
return split_tokens
def convert_tokens_to_ids(self, tokens):
return convert_tokens_to_ids(self.vocab, tokens)FullTokenizer的构造函数需要传入参数词典vocab_file和do_lower_case。如果我们自己从头开始训练模型(后面会介绍),那么do_lower_case决定了我们的某些是否区分大小写。如果我们只是Fine-Tuning,那么这个参数需要与模型一致,比如模型是uncased_L-12_H-768_A-12,那么do_lower_case就必须为True。
函数首先调用load_vocab加载词典,建立词到id的映射关系。接下来是构造BasicTokenizer和WordpieceTokenizer。前者是根据空格等进行普通的分词,而后者会把前者的结果再细粒度的切分为WordPiece。
BasicTokenizer的tokenize方法。
首先是用convert_to_unicode把输入变成unicode,这是为了兼容Python2和Python3,因为Python3的str就是unicode,而Python2的str其实是bytearray,Python2却有一个专门的unicode类型。
接下来是_clean_text函数,它的作用是去除一些无意义的字符。
codepoint为0的是无意义的字符,0xfffd(U+FFFD)显示为�,通常用于替换未知的字符。_is_control用于判断一个字符是否是控制字符(control character),所谓的控制字符就是用于控制屏幕的显示,比如\n告诉(控制)屏幕把光标移到下一行的开始。读者可以参考这里。
这里使用了unicodedata.category这个函数,它返回这个Unicode字符的Category,这里C开头的都被认为是控制字符,读者可以参考这里。
接下来是调用_is_whitespace函数,把whitespace变成空格。
这里把category为Zs的字符以及空格、tab、换行和回车当成whitespace。然后是_tokenize_chinese_chars,用于切分中文,这里的中文分词很简单,就是切分成一个一个的汉字。也就是在中文字符的前后加上空格,这样后续的分词流程会把每一个字符当成一个词。
很多网上的判断汉字的正则表达式都只包括4E00-9FA5,但这是不全的,比如 㐈 就不再这个范围内。读者可以参考这里。
接下来是使用whitespace进行分词,这是通过函数whitespace_tokenize来实现的。它直接调用split函数来实现分词。Python里whitespace包括’\t\n\x0b\x0c\r ‘。然后遍历每一个词,如果需要变成小写,那么先用lower()函数变成小写,接着调用_run_strip_accents函数去除accent。
它首先调用unicodedata.normalize(“NFD”, text)对text进行归一化。
我们”看到”的é其实可以有两种表示方法,一是用一个codepoint直接表示”é”,另外一种是用”e”再加上特殊的codepoint U+0301两个字符来表示。U+0301是COMBINING ACUTE ACCENT,它跟在e之后就变成了”é”。类似的”a\u0301”显示出来就是”á”。注意:这只是打印出来一模一样而已,但是在计算机内部的表示它们完全不同的,前者é是一个codepoint,值为0xe9,而后者是两个codepoint,分别是0x65和0x301。unicodedata.normalize(“NFD”, text)就会把0xe9变成0x65和0x301
接下来遍历每一个codepoint,把category为Mn的去掉,比如前面的U+0301,COMBINING ACUTE ACCENT就会被去掉。category为Mn的所有Unicode字符完整列表在这里。
处理完大小写和accent之后得到的Token通过函数_run_split_on_punc再次用标点切分。这个函数会对输入字符串用标点进行切分,返回一个list,list的每一个元素都是一个char。比如输入he’s,则输出是[[h,e], [’],[s]]。代码很简单,这里就不赘述。里面它会调用函数_is_punctuation来判断一个字符是否标点。
WordpieceTokenizer
对于中文来说,WordpieceTokenizer什么也不干,因为之前的分词已经是基于字符的了。有兴趣的读者可以参考这个开源项目。一般情况我们不需要自己重新生成WordPiece,使用BERT模型里自带的就行。
代码有点长,但是很简单,就是贪心的最大正向匹配。其实为了加速,是可以把词典加载到一个Double Array Trie里的。我们用一个例子来看代码的执行过程。比如假设输入是”unaffable”。我们跳到while循环部分,这是start=0,end=len(chars)=9,也就是先看看unaffable在不在词典里,如果在,那么直接作为一个WordPiece,如果不再,那么end-=1,也就是看unaffabl在不在词典里,最终发现”un”在词典里,把un加到结果里。
接着start=2,看affable在不在,不在再看affabl,…,最后发现 ##aff 在词典里。注意:##表示这个词是接着前面的,这样使得WordPiece切分是可逆的——我们可以恢复出“真正”的词。
run_classifier.py的main函数
这里使用的是Tensorflow的Estimator API,在本书的Tensorflow部分我们已经介绍过了。训练、验证和预测的代码都很类似,我们这里只介绍训练部分的代码。
首先是通过file_based_convert_examples_to_features函数把输入的tsv文件变成TFRecord文件,便于Tensorflow处理。
file_based_convert_examples_to_features函数遍历每一个example(InputExample类的对象)。然后使用convert_single_example函数把每个InputExample对象变成InputFeature。InputFeature就是一个存放特征的对象,它包括input_ids、input_mask、segment_ids和label_id,这4个属性除了label_id是一个int之外,其它都是int的列表,因此使用create_int_feature函数把它变成tf.train.Feature,而label_id需要构造一个只有一个元素的列表,最后构造tf.train.Example对象,然后写到TFRecord文件里。后面Estimator的input_fn会用到它。
这里的最关键是convert_single_example函数,读懂了它就真正明白BERT把输入表示成向量的过程,所以请读者仔细阅读代码和其中的注释。
如果两个Token序列的长度太长,那么需要去掉一些,这会用到_truncate_seq_pair函数:
这个函数很简单,如果两个序列的长度小于max_length,那么不用truncate,否则在tokens_a和tokens_b中选择长的那个序列来pop掉最后面的那个Token,这样的结果是使得两个Token序列一样长(或者最多a比b多一个Token)。对于Estimator API来说,最重要的是实现model_fn和input_fn。我们先看input_fn,它是由file_based_input_fn_builder构造出来的。代码如下:
这个函数返回一个函数input_fn。这个input_fn函数首先从文件得到TFRecordDataset,然后根据是否训练来shuffle和重复读取。然后用applay函数对每一个TFRecord进行map_and_batch,调用_decode_record函数对record进行parsing。从而把TFRecord的一条Record变成tf.Example对象,这个对象包括了input_ids等4个用于训练的Tensor。
接下来是model_fn_builder,它用于构造Estimator使用的model_fn。下面是它的主要代码(一些无关的log和TPU相关代码去掉了):
这里的代码都是一些boilerplate代码,没什么可说的,最重要的是调用create_model”真正”的创建Transformer模型。下面我们来看这个函数的代码:
上面代码调用modeling.BertModel得到BERT模型,然后使用它的get_pooled_output方法得到[CLS]最后一层的输出,这是一个768(默认参数下)的向量,然后就是常规的接一个全连接层得到logits,然后softmax得到概率,之后就可以根据真实的分类标签计算loss。我们这时候发现关键的代码是modeling.BertModel。
BertModel类
我们首先来看这个类的用法,把它当成黑盒。前面的create_model也用到了BertModel,这里我们在详细的介绍一下。下面的代码演示了BertModel的使用方法:
接下来我们看一下BertModel的构造函数:
代码很长,但是其实很简单。首先是对config(BertConfig对象)深度拷贝一份,如果不是训练,那么把dropout都置为零。如果输入的input_mask为None,那么构造一个shape合适值全为1的input_mask,这表示输入都是”真实”的输入,没有padding的内容。如果token_type_ids为None,那么构造一个shape合适并且值全为0的tensor,表示所有Token都属于第一个句子。
然后使用embedding_lookup函数构造词的Embedding,用embedding_postprocessor函数增加位置embeddings和token type的embeddings,然后是layer normalize和dropout。
接着用transformer_model函数构造多个Transformer SubLayer然后stack在一起。得到的all_encoder_layers是一个list,长度为num_hidden_layers(默认12),每一层对应一个值。 每一个值都是一个shape为[batch_size, seq_length, hidden_size]的tensor。
self.sequence_output是最后一层的输出,shape是[batch_size, seq_length, hidden_size]。first_token_tensor是第一个Token([CLS])最后一层的输出,shape是[batch_size, hidden_size]。最后对self.sequence_output再加一个线性变换,得到的tensor仍然是[batch_size, hidden_size]。
embedding_lookup函数用于实现Embedding,它有两种方式:使用tf.nn.embedding_lookup和矩阵乘法(one_hot_embedding=True)。前者适合于CPU与GPU,后者适合于TPU。所谓的one-hot方法是把输入id表示成one-hot的向量,当然输入id序列就变成了one-hot的矩阵,然后乘以Embedding矩阵。而tf.nn.embedding_lookup是直接用id当下标提取Embedding矩阵对应的向量。一般认为tf.nn.embedding_lookup更快一点,但是TPU上似乎不是这样,作者也不太了解原因是什么,猜测可能是TPU的没有快捷的办法提取矩阵的某一行/列?
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_postprocessor的代码如下,需要注意的部分都有注释。
create_attention_mask_from_input_mask函数用于构造Mask矩阵。我们先了解一下它的作用然后再阅读其代码。比如调用它时的两个参数是是:
表示这个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]的矩阵。接下来就是transformer_model函数了,它就是构造Transformer的核心代码。
如果对照Transformer的论文,非常容易阅读,里面实现Self-Attention的函数就是attention_layer。
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的其余部分定义了上面的步骤用到的函数,以及激活函数等。
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的哪些操作。
run_pretraining.py
这个模块用于BERT模型的预训练,即使用masked language model和next sentence的方法,对BERT模型本身的参数进行训练。如果使用现有的预训练BERT模型在文本分类/问题回答等任务上进行fine_tune,则无需使用run_pretraining.py。
用法:
run_pretraining.py的代码和run_classifier.py很类似,都是用BertModel构建Transformer模型,唯一的区别在于损失函数不同
get_masked_lm_output函数用于计算语言模型的Loss(Mask位置预测的词和真实的词是否相同)。
get_next_sentence_output函数用于计算预测下一个句子的loss.
create_pretraining_data.py
此处定义了如何将普通文本转换成可用于预训练BERT模型的tfrecord文件的方法。如果使用现有的预训练BERT模型在文本分类/问题回答等任务上进行fine_tune,则无需使用create_pretraining_data.py。
用法:
max_seq_length Token序列的最大长度
max_predictions_per_seq 最多生成多少个MASK
masked_lm_prob 多少比例的Token变成MASK
dupe_factor 一个文档重复多少次
首先说一下参数dupe_factor,比如一个句子”it is a good day”,为了充分利用数据,我们可以多次随机的生成MASK,比如第一次可能生成”it is a [MASK] day”,第二次可能生成”it [MASK] a good day”。这个参数控制重复的次数。
masked_lm_prob就是论文里的参数15%。max_predictions_per_seq是一个序列最多MASK多少个Token,它通常等于max_seq_length * masked_lm_prob。这么看起来这个参数没有必要提供,但是后面的脚本也需要用到这个同样的值,而后面的脚本并没有这两个参数。
main函数很简单,输入文本文件列表是input_files,通过函数create_training_instances构建训练的instances,然后调用write_instance_to_example_files以TFRecord格式写到output_files。
训练样本的格式,这是用类TrainingInstance来表示的:
is_random_next表示这两句话是有关联的,预测句子关系的分类器应该把这个输入判断为1。masked_lm_positions记录哪些位置被Mask了,而masked_lm_labels记录被Mask之前的词。
create_training_instances函数会调用create_instances_from_document来从一个文档里抽取多个训练数据(TrainingInstance)。普通的语言模型只要求连续的字符串就行,通常是把所有的文本(比如维基百科的内容)拼接成一个很大很大的文本文件,然后训练的时候随机的从里面抽取固定长度的字符串作为一个”句子”。但是BERT要求我们的输入是一个一个的Document,每个Document有很多句子,这些句子是连贯的真实的句子,需要正确的分句,而不能随机的(比如按照固定长度)切分句子。
代码有点长,但是逻辑很简单,比如有一篇文档有n个句子:
那么算法首先找到一个chunk,它会不断往chunk加入一个句子的所有Token,使得chunk里的token数量大于等于target_seq_length。通常我们期望target_seq_length为max_num_tokens(128-3),这样padding的尽量少,训练的效率高。但是有时候我们也需要生成一些短的序列,否则会出现训练与实际使用不匹配的问题。
找到一个chunk之后,比如这个chunk有5个句子,那么我们随机的选择一个切分点,比如3。把前3个句子当成句子A,后两个句子当成句子B。这是两个句子A和B有关系的样本(is_random_next=False)。为了生成无关系的样本,我们还以50%的概率把B用随机从其它文档抽取的句子替换掉,这样就得到无关系的样本(is_random_next=True)。如果是这种情况,后面两个句子需要放回去,以便在下一层循环中能够被再次利用。
有了句子A和B之后,我们就可以填充tokens和segment_ids,这里会加入特殊的[CLS]和[SEP]。接下来使用create_masked_lm_predictions来随机的选择某些Token,把它变成[MASK]。
最后是使用函数write_instance_to_example_files把前面得到的TrainingInstance用TFRecord的个数写到文件里。
tokenization.py
此处定义了对输入的句子进行预处理的操作,预处理的内容包括:
转换为Unicode
切分成数组
去除控制字符
统一空格格式
切分中文字符(即给连续的中文字符之间加上空格)
将英文单词切分成小片段(如[“unaffable”]切分为[“un”, “##aff”, “##able”])
大小写和特殊形式字母转换
分离标点符号(如 [“hello?”]转换为 [“hello”, “?”])
run_squad.py
这个模块可以配置和启动基于BERT在squad数据集上的问题回答任务。
extract_features.py
这个模块可以使用预训练的BERT模型,生成输入句子的向量表示和输入句子中各个词语的向量表示(类似ELMo)。这个模块不包含训练的过程,只是执行BERT的前向过程,使用固定的参数对输入句子进行转换。
optimization.py
这个模块配置了用于BERT的optimizer,即加入weight decay功能和learning_rate warmup功能的AdamOptimizer。
Self-Attention(torch)
BERT 模型对 Self-Attention 的实现代码片段:
参照Transformer 的结构,在 Multi-Head Attention 之后是 Add & Norm,将经过注意力机制计算后的向量和原输入相加并归一化,进入 Feed Forward Neural Network,然后再进行一次和输入的相加并完成归一化。
2. 在自己的数据集上finetune
BERT官方项目搭建了文本分类模型的model_fn,因此只需定义自己的DataProcessor,即可在自己的文本分类数据集上进行训练。
训练自己的文本分类数据集所需步骤如下:
1.下载预训练的BERT模型参数文件,如(https://storage.googleapis.com/bert_models/2018_10_18/uncased_L-12_H-768_A-12.zip ),解压后的目录应包含bert_config.json,bert_model.ckpt.data-00000-of-00001,bert_model.ckpt.index,bert_model_ckpt.meta和vocab.txt五个文件。
2.将自己的数据集统一放到一个目录下。为简便起见,事先将其划分成train.txt,eval.txt和predict.txt三个文件,每个文件中每行为一个样本,格式如下(可以使用任何自定义格式,只需要编写符合要求的DataProcessor子类即可):
即句子和标签之间用__label__划分,句子中的词语之间用空格划分。
3.修改run_classifier.py,或者复制一个副本,命名为run_custom_classifier.py或类似文件名后进行修改。
4.新建一个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):
5.在main函数中,向main函数开头的processors字典增加一项,key为自己的数据集的名称,value为上一步中定义的DataProcessor的类名:
6.执行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。
7.为了从预训练的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的定义自行配置或使用默认值。
8.在没有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等函数进行序列标注。
参考资料
http://fancyerii.github.io/2019/03/09/bert-codes/
Last updated