(九)再谈embedding——bert详解(实战)下

    前面两篇分别梳理了下BERT的原理和BERT的训练,接着前面的内容,梳理下BERT是如何在下游任务上运用的。

(九)再谈embedding——bert详解(实战)下_第1张图片

原理就是上面这个图了。四种任务,实际上从他的训练模型的代码和我之前的NER的代码就已经对(b)(d)两个部分进行了梳理。那接下来我首先梳理下BERT是如何在SQUAD2.0阅读理解这个任务上使用的。

Part2:SQUAD项目实战

关于阅读理解部分,我会专门做个专题梳理,这里只针对bert进行讲解。

一.数据下载与模型准备

        这里bert开源项目上就有,自行下载,然后根据说明配置一下目录就行。注:模型我下载的是BERT-Large, Uncased,readme以及说的很清楚了,cased的版本是考虑了字母大小写的,适合NER任务。

        模型参数:

(九)再谈embedding——bert详解(实战)下_第2张图片

二.数据处理

        之前看过QA-net的代码,SQUAD部分的数据处理还比较熟悉,这里还是简单的说明,拿SQUAD2.0训练数据为例:

(九)再谈embedding——bert详解(实战)下_第3张图片

    训练集中包含了442段话题,然后每个话题有一个title,对于每个话题下都有几十段的paragraph,每个paragraph是一个json字典文件,包含了context和至少一组问答对:

注:

(九)再谈embedding——bert详解(实战)下_第4张图片

这里的text就是答案,answer_start:是指答案在文章开始的地方,根据这两个算出answer_start,answer_end,然后在到文章中去验证是不是有这个答案,这是数据清洗的部分,这里就大致过一下。

最后将数据处理成逐条的形式,每条数据内容如下:

(九)再谈embedding——bert详解(实战)下_第5张图片

注意:1.SQUAD1.0和SQUAD2.0任务的区别就是后者的数据更多,并且加入了无法回答的问题数据。

            2.代码中的tokenization是一个字符处理和数据清洗的工具,这里补充说明下,主要有BasicTokenizer和WordpieceTokenizer两个类,先使用前者,主要是分离标点,转换大小写,和判断中文字符;后者的作用是对部分字符切片,避免OOV过多的问题,这样做的好处在这里举两个例子:

            Question: What year was John Smith born?

            Context: The leader was John Smith (1895-1943).

             Answer: 1895

像这种问题(1895-1943).是连接在一起的,没有空格,需要把这个部分分开,因此要用到WordpieceTokenizer部分。

三.代码讲解

        代码部分的讲解我选择pytorch的版本,其实都一样,我主要比较了下两个版本的实验结果是不是一样的,因为自己在做调试的时候感觉pytorch的版本梳理起来稍微方便点,后续我还会提到GPU的使用问题。

         关于bert的部分我就不再说明,我们从输出的部分开始看,这里要明确建模任务,我们显然是要找出答案在原文中的位置,也就是start和end两个类别在原文中每个位置上的概率。搞清楚了这个,我们来看bert的最后一层输出就是[bz,max_length,dim],对于不同任务我们需要接不同的全连接层,对于这个任务只需要接一个这样的线性层就行:

当然如果你想在“更厉害”的网络上嵌入bert做微调也是可以的,后续我会找个例子实现一下。

接下来主要说一下GPU的使用问题。

在这个问题上,假如你的手头GPU的计算资源不够,你可能会感觉寸步难行,动不动就报OFM的错误,我大概计算了一下,如果要用32batch size任何trick不加,你至少需要4块TITAN吧,也就是40G的显存,必将large model就是1G+,再乘上batch,大概就需要这么大,那么怎么办?官方代码给了解决方案,我们来看一下可行性:

(九)再谈embedding——bert详解(实战)下_第6张图片

分布式训练也就是第三点我就不说了,主要是还没用过,不是很会,剩余的四个我们来看一下:

1.Gradient Accumulation:所谓梯度累计,就是假如你设置的32batch size,太大了,跑不通,那么就将batch size除以Gradient Accumulation系数,如果是2,那么batch size就变成了16,但是在反向传播的时候,会将mean_loss也除以2,然后进行累计,直到2次后在进行梯度清零,就相当于将32size分成两个16,但是是进行的连续的反向转播,如果看不懂就看看代码:

(九)再谈embedding——bert详解(实战)下_第7张图片

这样的做法并不能提速,只是为了跑32batch_size的数据分两次,然后累积清空梯度,

2.Multi-GPU

这里主要就是model = torch.nn.DataParallel(model).cuda()官方代码里不是这么写的,上来就检测你所有的显卡,然后把空闲的都用了,这里建议自己设定卡的id比较好,把代码稍稍修改一下,原理就是首先将模型加载到主 GPU 上,然后再将模型复制到各个指定的从 GPU 中,然后将输入数据按 batch 维度进行划分,具体来说就是每个 GPU 分配到的数据 batch 数量是总输入数据的 batch 除以指定 GPU 个数。每个 GPU 将针对各自的输入数据独立进行 forward 计算,最后将各个 GPU 的 loss 进行求和,再用反向传播更新单个 GPU 上的模型参数,再将更新后的模型参数复制到剩余指定的 GPU 中,这样就完成了一次迭代计算。

(九)再谈embedding——bert详解(实战)下_第8张图片

3.Optimize on

关于Adam优化,这里有份教程讲的很清楚,这里大致是说Adam优化需要优化的有初始化参数向量、一阶矩向量、二阶矩向量:

(九)再谈embedding——bert详解(实战)下_第9张图片

这样一来,第一块GPU就要存储3倍模型的大小,这样就限制了batch_size的大小,所以我们将这部分参数存在CPU上:

(九)再谈embedding——bert详解(实战)下_第10张图片

接下来在训练的过程中:set_optimizer_params_grad和copy_optimizer_params_to_model是两个过程,当要开始训练时set_optimizer_params_grad将需要训练的参数从模型中取出param_opti.grad.data.copy_(param_model.grad.data),然后optimizer.step(),完成后再将新的参数送回模型param_model.data.copy_(param_opti.data)。

4.16-bits training

这部分代码也是基于CPU优化上完成的,训练过程中,每一层的权重存储成FP32数据类型(Mater-Weights),每次训练时都会将FP32的权重降精度至FP16( a master copy),前向推理和后向梯度都使用FP16进行计算,更新时将FP16的梯度累加到FP32的Mater-Weight上,样例图如下,

(九)再谈embedding——bert详解(实战)下_第11张图片

混合精度训练可以解决权重更新量很小的问题,但无法解决梯度本身很小的问题。在一些网络中(比如SSD),梯度大部分都在FP16的表示范围之外,因此需要将梯度平移到FP16的表示范围内 。所以用到了loss_scale参数,一种简单高效的方法是直接在前向时就将loss乘以scale,这样在后向传导时所有的梯度都会被乘以相同的scale。权重更新时需要将移位后的梯度除以scale后,再更新到权重上。

(九)再谈embedding——bert详解(实战)下_第12张图片

上面就是我对pytorch代码的在squad任务上的解读,pytorch版本的代码只适合1.0任务,2.0任务我后续会进行修改,

(九)再谈embedding——bert详解(实战)下_第13张图片

上面是我的参数设置,下面来看下实验结果:

(九)再谈embedding——bert详解(实战)下_第14张图片

这个结果比官方给的差一点点,毕竟不是最佳参数,在GPU的使用上也有限制。再贴这个结果的时候实验大概跑了三天吧,现在pytorch版本又出了在fp16中自动调节loss_scale的代码,下面是新的实验结果,新的代码里面用了APEX的包,训练速度有很大的提升,缩短了好几倍,结果仍旧差不多:

(上面两个实验都是在一块泰坦上搞定的)

你可能感兴趣的:((九)再谈embedding——bert详解(实战)下)