bert预训练实战篇-持续更新

导读

使用bert预训练会遇到很多坑,包括但不限于数据预处理的正确姿势、数据预处理的高效实现、bert单机多卡分布式训练的基本实现,以及如何debug并提升使用单机多卡来进行深度学习训练的性能瓶颈。本篇记录bert预训练遇到的坑以及排坑方法。供大家参考。
凡是涉及训练模型,总要涉及算力和数据,算力决定模型训练的速度,数据决定了训练的模型所能达到的高度。对于大多数人来说,实战平台仅仅是一台Intel CORE i7的CPU,数据只能从网上爬一些或者公开的数据集,这些数据标注量有限,因此无法引入监督训练,即使引入,训练效果也差强人意,采用经典的roberta训练方式即去除next sentence prediction任务后的masked_language_model任务。
步骤分为:训练数据处理、模型训练、模型调优。

1、训练数据处理

使用数据工程技术,对爬取的网页数据进行清洗,使用正则表达式过滤掉无用的符号(),一些HTML标签中包含比较复杂的css样式。可使用Beautifulshop中的html格式化方法,将style属性过滤掉,代码如下:

def remove_html_tags(html): soup = BeautifulSoup(html,"html.parser") for s in soup(['script', 'style']): s.decompose() return ' '.join(soup.stripped_strings)

其中,decompose方法递归删除符合条件的所有标签。
此外,还有一些字符无法删除,可以使用规则进行过滤。

2、模型训练和调优

将数据喂给bert后,当数据量超过一定量级(百万级别),模型执行效率需要引起重视,因此,需要根据训练数据优化原始模型。可从以下几点进行:

  1. 原始代码中,对于原始数据格式的要求是每个文章先分句,然后每个句子一行,文章与文章之间使用空行来分隔。而在制作训练数据时,又需要通过循环读取每行文本,将同一篇文章的句子进行聚合,代码如下:
for input_file in input_files: with tf.gfile.GFile(input_file, "r") as reader: while True: line = tokenization.convert_to_unicode(reader.readline().replace("",""))# .replace("”","")) # 将、”替换掉。 if not line: break line = line.strip() # Empty lines are used as document delimiters if not line: all_documents.append([]) tokens = tokenizer.tokenize(line) if tokens: all_documents[-1].append(tokens)

这种方式相当于会对大数据量的列表进行循环,效率比较低。因此,我建议可以在前面做数据清洗的时候就进行分句,然后将一篇文章转换为句子列表,最后整体外面再包一层列表,中间临时保存文件可以保存成json文件。上述代码即可直接用json的load来代替。

  1. 将每个文章句子列表整合后,就会对文档列表循环,执行wwm的核心逻辑。这里还是需要有大量的循环操作,效率还是非常低的。此时,要优化性能的话,可以引入python的多进程,跑并发任务。我的CPU是8核的,因此并发数可以设为8。具体多进程实现可以参考苏神的bert4keras中的实现:
    def parallel_apply( func, iterable, workers, max_queue_size, callback=None, dummy=False, random_seeds=True ): “”“多进程或多线程地将func应用到iterable的每个元素中。 注意这个apply是异步且无序的,也就是说依次输入a,b,c,但是 输出可能是func©, func(a), func(b)。 参数: callback: 处理单个输出的回调函数; dummy: False是多进程/线性,True则是多线程/线性; random_seeds: 每个进程的随机种子。 “””
    大概原理就是使用python的Pool机制,将每个样本的处理操作封装成一个work_step。另外定义两个queue,一个用于存放输入数据,一个用于存放work_step输出的结果。最后可以定义一些后处理操作比如保存到最终结果列表等。
    通过上述多进程改造后,性能提升非常明显,提升率足有200%-300%。同等情况下,处理30万篇文档,现在只要1.5小时。

  2. 另外一个可以使用多进程优化的地方在write_instance_to_example_files。原始代码中,它主要是遍历每个instance,然后生成tf的Example写到tfrecord中。这里的循环也可以使用多进程来改造优化,不过有一点需要注意,即将tf.Example写到tfrecord的操作最好放在后处理的function中,如果放在work_step中,会导致并发执行的时候,写文件紊乱,最后生成tfrecord会格式错误。通过本步骤的优化,同样能够让性能提升200%。结合上述步骤,处理30万篇文档,现在只要50分钟左右

  3. 原始代码使用的tokenization也是一个可以优化性能的点。我使用了huggingface的tokenizer代替了原始的tokenization,它是有ruby开发的一个高性能的切词工具,里面内置了bert的wordpiece分词模型,相对于原始python实现的分词方法,它在性能上能够带来20-30%左右的提升。

  4. 中文分句工具方面也是一个可以关注的点。原本我是准备用百度的lac来做分词,但是发现它的执行效率还是比不上结巴分词。另外,我在github上还发现了结巴分词的性能提升版fast_jieba,它是用C++来重新实现的,因此效率上更加高效。最后该项改造能够为整体性能带来10%左右的提升。

当前开源的中文通用BERT模型,大多是使用谷歌的TPU训练的,这种情况下,一般是不需要考虑性能调优的工作,你尽可以将batch size设为很大的值,然后使用LAMB优化器来加速训练收敛。TPU的显存一般至少得有128G,而且针对深度学习训练有专门的优化。然而,对于我们个人以及公司场景来说,使用TPU不现实。一个是成本太高,另一个是公司的数据一般属于隐私数据,是不能随意外传的,因此只能使用传统的GPU甚至CPU来训练。以上方法目标是希望能尽量缩短bert训练的时间。

[1] 我不太懂BERT系列——BERT预训练实操总结 https://zhuanlan.zhihu.com/p/337212893
[2] BERT实战(源码分析+踩坑) https://zhuanlan.zhihu.com/p/58471554

你可能感兴趣的:(NLP,bert,自然语言处理)