NLP 模型:从 transformer 到 albert
文章目录
1 Transformer
1.1 transformer 整体架构
1.2 transformer 结构原理
1 | <1> Inputs是经过padding的输入数据,大小是[batch size, max seq length]。 |
1.3 transformer 的技术细节
transformer 中的 self-attention 是从普通的点积 attention 中演化出来的
1.3.1 为什么 <2> 要乘以 embedding size 的开方?
论文并没有讲为什么这么做,我看了代码,猜测是因为 embedding matrix 的初始化方式是 xavier init,这种方式的方差是 1/embedding size,因此乘以 embedding size 的开方使得 embedding matrix 的方差是 1,在这个 scale 下可能更有利于 embedding matrix 的收敛。
1.3.2 为什么 inputs embedding 要加入 positional encoding?
因为 self-attention 是位置无关的,无论句子的顺序是什么样的,通过 self-attention 计算的 token 的 hidden embedding 都是一样的,这显然不符合人类的思维。因此要有一个办法能够在模型中表达出一个 token 的位置信息,transformer 使用了固定的 positional encoding 来表示 token 在句子中的绝对位置信息。positional encoding 的公式如下:
1.3.3 为什么 <4.2> 的结果要 scale?
以数组为例,2 个长度是 len,均值是 0,方差是 1 的数组点积会生成长度是 len,均值是 0,方差是 len 的数组。而方差变大会导致 softmax 的输入推向正无穷或负无穷,这时的梯度会无限趋近于 0,不利于训练的收敛。因此除以 len 的开方,可以是数组的方差重新回归到 1,有利于训练的收敛。
1.3.4 为什么 <5> 要将 multi-head attention 的输入和输出相加?
类似于 resnet 中的残差学习单元,有 ensemble 的思想在里面,解决网络退化问题。
1.3.5 为什么 attention 需要 multi-head,一个大 head 行不行?
multi-head 相当于把一个大空间划分成多个互斥的小空间,然后在小空间内分别计算 attention,虽然单个小空间的 attention 计算结果没有大空间计算得精确,但是多个小空间并行然后 concat 有助于网络捕捉到更丰富的信息,类比 cnn 网络中的 channel。
1.3.6 为什么 multi-head attention 后面要加一个 ffn?
类比 cnn 网络中,cnn block 和 fc 交替连接,效果更好。相比于单独的 multi-head attention,在后面加一个 ffn,可以提高整个 block 的非线性变换的能力。
1.3.7 为什么 <11> 要 mask 当前时刻的 token 与后续 token 的点积结果?
自然语言生成 (例如机器翻译,文本摘要) 是 auto-regressive 的,在推理的时候只能依据之前的 token 生成当前时刻的 token,正因为生成当前时刻的 token 的时候并不知道后续的 token 长什么样,所以为了保持训练和推理的一致性,训练的时候也不能利用后续的 token 来生成当前时刻的 token。这种方式也符合人类在自然语言生成中的思维方式。
1.4 transformer 的总结
transformer 刚发表的时候,我刚好在百度 nlp 部实习,当时觉得 transformer 噱头更多一些,在小模型上 self-attention 并不比 rnn,lstm 好。直到大力出奇迹的 bert 出现,深深地打了我的脸,当模型变得越来越大,样本数越来越多的时候,self-attention 无论是并行化带来的训练提速,还是在长距离上的建模,都是要比传统的 rnn,lstm 好很多。transformer 现在已经各种具有代表性的 nlp 预训练模型的基础,bert 系列使用了 transformer 的 encoder,gpt 系列 transformer 的 decoder。在推荐领域,transformer 的 multi-head attention 也应用得很广泛。
2 bert
2.1 bert 的背景
在 bert 之前,将预训练的 embedding 应用到下游任务的方式大致可以分为 2 种,一种是 feature-based,例如 ELMo 这种将经过预训练的 embedding 作为特征引入到下游任务的网络中;一种是 fine-tuning,例如 GPT 这种将下游任务接到预训练模型上,然后一起训练。然而这 2 种方式都会面临同一个问题,就是无法直接学习到上下文信息,像 ELMo 只是分别学习上文和下文信息,然后 concat 起来表示上下文信息,抑或是 GPT 只能学习上文信息。因此,作者提出一种基于 transformer encoder 的预训练模型,可以直接学习到上下文信息,叫做 bert。bert 使用了 12 个 transformer encoder block,在 13G 的数据上进行了预训练,可谓是 nlp 领域大力出奇迹的代表。
2.2 bert 的流程
bert 是在 transformer encoder 的基础之上进行改进的,因此在整个流程上与 transformer encoder 没有大的差别,只是在 embedding,multi-head attention,loss 上有所差别。
2.2.1 bert 和 transformer 在 embedding 上的差异
bert 预训练 and fine-tune
bert 和 transformer 在 embedding 上的差异主要有 3 点:
<1> transformer 的 embedding 由 2 部分构成,一个是 token embedding,通过 embedding matrix lookup 到 token_ids 上生成表示 token 的向量;一个是 position embedding,是通过 sin 和 cos 函数创建的定值向量。而 bert 的 embedding 由 3 部分构成,第一个同样是 token embedding,通过 embedding matrix lookup 到 token_ids 上生成表示 token 的向量;第二个是 segment embedding,用来表达当前 token 是来自于第一个 segment,还是第二个 segment,因此 segment vocab size 是 2;第三个是 position embedding,与 transformer 不同的是,bert 创建了一个 position embedding matrix,通过 position embedding matrix lookup 到 token_ids 的位置上生成表示 token 位置的位置向量。
<2> transformer 在 embedding 之后跟了一个 dropout,但是 bert 在 embedding 之后先跟了一个 layer normalization,再跟了一个 dropout。
<3> bert 在 token 序列之前加了一个特定的 token“[cls]”,这个 token 对应的向量后续会用在分类任务上;如果是句子对的任务,那么两个句子间使用特定的 token“[seq]” 来分割。
2.2.2 bert 和 transformer 在 multi-head attention 上的差异
bert 和 transformer 在 multi-head attention 上的差异主要有 1 点:
<1> transformer 在 < 4.7 > 之后没有 linear 的操作 (也可能是因为我看的 transformer 代码不是官方 transformer 的缘故),而 bert 在 transformer 的 < 4.7 > 之后有一个 linear 的操作。
2.2.3 bert 和 transformer 在 loss 上的差异
bert 和 transformer 在 loss 上的差异主要有 2 点:
<1> transformer 的 loss 是在 decoder 阶段计算的,loss 的计算方式是 transformer 的 < 19>。bert 预训练的 loss 由 2 部分构成,一部分是 NSP 的 loss,就是 token“[cls]” 经过 1 层 Dense,然后接一个二分类的 loss,其中 0 表示 segment B 是 segment A 的下一句,1 表示 segment A 和 segment B 来自 2 篇不同的文本;另一部分是 MLM 的 loss,segment 中每个 token 都有 15% 的概率被 mask,而被 mask 的 token 有 80% 的概率用 “
<2> bert 在 encoder 之后,在计算 NSP 和 MLM 的 loss 之前,分别对 NSP 和 MLM 的输入加了一个 Dense 操作,这部分参数只对预训练有用,对 fine-tune 没用。而 transformer 在 decoder 之后就直接计算 loss 了,中间没有 Dense 操作。
2.3 bert 的技术细节
2.3.1 为什么 bert 需要额外的 segment embedding?
因为 bert 预训练的其中一个任务是判断 segment A 和 segment B 之间的关系,这就需要 embedding 中能包含当前 token 属于哪个 segment 的信息,然而无论是 token embedding,还是 position embedding 都无法表示出这种信息,因此额外创建一个 segment embedding matrix 用来表示当前 token 属于哪个 segment 的信息,segment vocab size 就是 2,其中 index=0 表示 token 属于 segment A,index=1 表示 token 属于 segment B。
2.3.2 为什么 transformer 的 embedding 后面接了一个 dropout,而 bert 是先接了一个 layer normalization,再接 dropout?
LN 是为了解决梯度消失的问题,dropout 是为了解决过拟合的问题。在 embedding 后面加 LN 有利于 embedding matrix 的收敛。
2.3.3 为什么 token 被 mask 的概率是 15%?为什么被 mask 后,还要分 3 种情况?
15% 的概率是通过实验得到的最好的概率,xlnet 也是在这个概率附近,说明在这个概率下,既能有充分的 mask 样本可以学习,又不至于让 segment 的信息损失太多,以至于影响 mask 样本上下文信息的表达。然而因为在下游任务中不会出现 token“
2.4 bert 的总结
相比于那些说自己很好,但是在实际场景中然并软的论文,bert 是真正地影响了学术界和工业界。无论是 GLUE,还是 SQUAD,现在榜单上的高分方法都是在 bert 的基础之上进行了改进。在我的工作中,用 bert 落地的业务效果也比我预想的要好一些。bert 在 nlp 领域的地位可以类比 cv 领域的 inception 或者 resnet,cv 领域的算法效果在几年前就已经超过了人类的标注准确率,而 nlp 领域直到 bert 的出现才做到这一点。不过 bert 也并不是万能的,bert 的框架决定了这个模型适合解决自然语言理解的问题,因为没有解码的过程,所以 bert 不适合解决自然语言生成的问题。因此如何将 bert 改造成适用于解决机器翻译,文本摘要问题的框架,是今后值得研究的一个点。
3 xlnet
3.1 xlnet 的背景
目前语言预训练模型的模式主要有 2 种,第一种是像 gpt 这种的 auto-regressive 模型,每个时刻都依据之前所有时刻的 token 来预测下一个 token,auto-regressive 的 loss 的定义如下:
auto-regressive 的 loss
第二种是像 bert 这种的 auto-encoder 模型,随机 mask 掉句子中若干个 token,然后依据上下文预测被 mask 掉的 token,auto-encoder 的 loss 的定义如下:
auto-encoder 的 loss
auto-regressive 模型在训练的过程中只能用到上文的信息,但是不会出现训练和推理的 gap;auto-encoder 模型在训练的过程中能利用到上下文信息,但是会出现训练和推理的 gap,训练过程中的
3.2 xlnet 的流程
3.2.1 因子分解序
一个句子的因子分解序就是这个句子的 token 的一种随机排列。为了能融合 auto-regressive 模型和 auto-encoder 模型的优势,xlnet 使用因子分解序将上下文信息引入 auto-regressive 的 loss 中。例如句子 1->2->3->4->5,在 auto-regressive 的 loss 中,预测 token 2 可以利用 token 1 的信息,但是不能利用 token 2/3/4/5 的信息;在引入了因子分解序之后,假设使用了 1->4->2->3->5 的因子分解序,那么预测 token 2 可以利用 token 1/4 的信息,但是不能利用 token 3/5 的信息。在使用因子分解序之后,并不会影响句子的输入顺序,只是在 transformer-xl 的 multi-head attention 中计算每一个 token 的 attention 结果时会有所改变,原先的方式是 mask 掉当前 token 以及句子中的后续 token,而现在是 mask 掉当前 token 以及因子分解序中的后续 token。这种方式可以在计算当前 token 的 attention 结果时利用到当前 token 的上下文信息,例如上面这个因子分解序,计算 token 2 的 attention 结果时就是用到了 token 1/4 的信息,在原始句子中,token 1 在 token 2 之前,token 4 在 token 2 之后。
因子分解序的实现方式是在计算 multi-head attention 的时候进行了 proper mask。例如 1->4->2->3->5 的因子分解序,在输入 token 2 时,由于在因子分解序中 token 2 排在 token 1/4 的后面,所以在计算 token 2 的 attention 结果时将 token 2/3/5 进行了 mask,只计算 token 2 和 token 1/4 的点积结果,然后 softmax 以及加权求和当作 attention 的结果。
3.2.2 双流自注意力机制
xlnet 使用了 transformer-xl 的框架,并在 transformer 的基础之上使用了双流自注意力机制。
双流自注意力机制
相比于普通的 transformer,xlnet 多加了一个 multi-head attention+ffn 的计算。双流自注意力机制分为查询流 g 和内容流 h 2 个流。h 就是和 transformer 一样的 multi-head attention,计算第 t 个时刻的 attention 的结果时用到了因子分解序中前 t 个位置的位置信息和 token 信息,而 g 在 transformer 的 multi-head attention 的基础之上做了修改,计算第 t 个时刻的 attention 的结果时只用到了因子分解序中前 t 个位置的位置信息和前 t-1 个位置的 token 信息。在预训练的过程当中,为了降低优化的难度,xlnet 只会计算因子分解序最后的 1/6 或者 1/7 的 token 的 g,然后把 g 融合到 auto-regressive 的 loss 当中进行训练,顺带着训练 h。在预训练结束之后,放弃 g,使用 h 做下游任务的 fine-tune,fine-tune 的过程就和普通的 transfomer 的 fine-tune 一模一样了。
3.3 xlnet 的技术细节
3.3.1 因子分解序的优势
因子分解序创新地将上下文信息融入到 auto-regressive 的 loss 中,理论上,只要模型的预训练将一个句子的所有因子分解序都训练一遍,那么模型就能准确地 get 到句子中每一个 token 和上下文之间的联系。然而实际情况下,一个句子的因子分解序的数量是随着句子长度指数增长的,因此在实际训练中只是用到了句子的某个因子分解序或者某几个因子分解序而已。即便如此,相比于只能 get 到上文信息的 auto-regressive,加了因子分解序之后可以同时 get 到上下文信息,能够提高模型的推理能力。
3.3.2 为什么自注意力要用双流?
因为普通的 transformer 无法融合因子分解序和 auto-regressive 的 loss,例如 2 个不同的因子分解序 1->3->2->4->5 和 1->3->2->5->4,第 1 个句子的 4 和第 2 个句子的 5 在 auto-regressive 的 loss 下的 attention 结果是一样的,因此第 1 个句子的 4 和第 2 个句子的 5 在 vocab 上的预测概率分布也是一样的,这就不符合常理了。造成这种现象的原因在于,auto-regressive 的 loss 是利用前 t-1 个 token 的 token 信息和位置信息预测第 t 个 token,然而因子分解序的第 t 个 token 在原始句子中的位置是不确定的,因此需要额外的信息表示因子分解序中需要预测的 token 在原始句子中的位置。为了达到目的,xlnet 使用双流的 multi-head attention+ffn,查询流 g 利用因子分解序中前 t 个位置的位置信息和前 t-1 个位置的 token 信息计算第 t 个位置的输出信息,而内容流 h 利用因子分解序中前 t 个位置的位置信息和 token 信息计算第 t 个位置的输出信息。在预训练的过程中,使用 g 计算 auto-regressive 的 loss,然后最小化的 loss 的值,顺带着训练 h。预训练完成之后,放弃 g,使用 h 无缝切换到普通 transformer 的 fine-tune。
3.4 xlnet 的总结
由于我也是只看过论文,并没有在实际工作中用过 xlnet,因此我也只能讲讲 xlnet 的理论。在 bert 之后,有很多论文都对 bert 进行了改进,但是创新点都很有限,xlnet 是在我看过的论文中唯一一篇在 transformer 的框架之下将上下文信息和 auto-regressive 的 loss 融合在一起的论文。但是 xlnet 是否真的比 bert 优秀,这还是一个疑问,xlnet 使用了 126G 的数据进行预训练,相比于 bert 的 13G 数据大了一个数量级,在 xlnet 发布之后不久,bert 的改进版 roberta 使用了 160G 的数据进行预训练,又打败了 xlnet。
4 albert
4.1 albert 的背景
增大预训练模型的大小通常能够提高预训练模型的推理能力,但是当预训练模型增大到一定程度之后,会碰到 GPU/TPU memory 的限制。因此,作者在 bert 中加入了 2 项减少参数的技术,能够缩小 bert 的大小,并且修改了 bert NSP 的 loss,在和 bert 有相同参数量的前提之下,有更强的推理能力。
4.2 albert 的流程
4.2.1 词向量矩阵的分解
在 bert 以及诸多 bert 的改进版中,embedding size 都是等于 hidden size 的,这不一定是最优的。因为 bert 的 token embedding 是上下文无关的,而经过 multi-head attention+ffn 后的 hidden embedding 是上下文相关的,bert 预训练的目的是提供更准确的 hidden embedding,而不是 token embedding,因此 token embedding 没有必要和 hidden embedding 一样大。albert 将 token embedding 进行了分解,首先降低 embedding size 的大小,然后用一个 Dense 操作将低维的 token embedding 映射回 hidden size 的大小。bert 的 embedding size=hidden size,因此词向量的参数量是 vocab size * hidden size,进行分解后的参数量是 vocab size * embedding size + embedding size * hidden size,只要 embedding size << hidden size,就能起到减少参数的效果。
4.2.2 参数共享
bert 的 12 层 transformer encoder block 是串行在一起的,每个 block 虽然长得一模一样,但是参数是不共享的。albert 将 transformer encoder block 进行了参数共享,这样可以极大地减少整个模型的参数量。
4.2.3 sentence order prediction(SOP)
在 auto-encoder 的 loss 之外,bert 使用了 NSP 的 loss,用来提高 bert 在句对关系推理任务上的推理能力。而 albert 放弃了 NSP 的 loss,使用了 SOP 的 loss。NSP 的 loss 是判断 segment A 和 segment B 之间的关系,其中 0 表示 segment B 是 segment A 的下一句,1 表示 segment A 和 segment B 来自 2 篇不同的文本。SOP 的 loss 是判断 segment A 和 segment B 的的顺序关系,0 表示 segment B 是 segment A 的下一句,1 表示 segment A 是 segment B 的下一句。
4.3 albert 的技术细节
4.3.1 参数减少技术
albert 使用了 2 项参数减少的技术,但是 2 项技术对于参数减少的贡献是不一样的,第 1 项是词向量矩阵的分解,当 embedding size 从 768 降到 64 时,可以节省 21M 的参数量,但是模型的推理能力也会随之下降。第 2 项是 multi-head attention+ffn 的参数共享,在 embedding size=128 时,可以节省 77M 的参数量,模型的推理能力同样会随之下降。虽然参数减少会导致了模型推理能力的下降,但是可以通过增大模型使得参数量变回和 bert 一个量级,这时模型的推理能力就超过了 bert。
现在学术界发论文有 2 种常见的套路,第 1 种是往死里加参数加数据量,然后提高模型的推理能力;第 2 种是减参数,然后使模型的推理能力不怎么降。albert 使用的参数减少技术看似是第 2 种,实则是第 1 种。当 bert 从 large 变到 xlarge 时,虽然模型变大到了 1270M,但是模型出现了退化现象,推理能力下跌了一大截,说明在 bert 的框架下,large 已经是模型推理能力的极限了。albert 使用了参数减少技术,相比于 bert 的 large 是 334M,albert 的 large 只有 18M,虽然推理能力比 bert 差,但是参数减少后的 albert 还有成长空间,将 albert 从 large 变到 xlarge,甚至是 xxlarge 时,模型的推理能力又得到了提高,并且超过了 bert 最好的模型。
4.3.2 loss
在 albert 之前,很多 bert 的改进版都对 NSP 的 loss 提出了质疑。structbert 在 NSP 的 loss 上进行了修改,有 1/3 的概率是 segment B 是 segment A 的下一句,有 1/3 的概率是 segment A 是 segment B 的下一句,有 1/3 的概率是 segment A 和 segment B 来自 2 篇不同的文本。roberta 则是直接放弃了 NSP 的 loss,修改了样本的构造方式,将输入 2 个 segment 修改为从一个文本中连续 sample 句子直到塞满 512 的长度。当到达文本的末尾且未塞满 512 的长度时,先增加一个 “[sep]”,再从另一个文本接着 sample,直到塞满 512 的长度。
albert 在 structbert 的基础之上又抛弃了 segment A 和 segment B 来自 2 篇不同的文本的做法,只剩下 1/2 的概率是 segment B 是 segment A 的下一句,1/2 的概率是 segment A 是 segment B 的下一句。论文中给出了这么做的解释,NSP 的 loss 包含了 2 部分功能:topic prediction 和 coherence prediction,其中 topic prediction 要比 coherence prediction 更容易学习,而 MLM 的 loss 也包含了 topic prediction 的功能,因此 bert 难以学到 coherence prediction 的能力。albert 的 SOP loss 抛弃了 segment A 和 segment B 来自 2 篇不同的文本的做法,让 loss 更关注于 coherence prediction,这样就能提高模型在句对关系推理上的能力。
4.4 albert 的总结
albert 虽然减少参数量,但是并不会减少推理时间,推理的过程只不过是从串行计算 12 个 transformer encoder block 变成了循环计算 transformer encoder block 12 次。albert 最大的贡献在于使模型具备了比原始的 bert 更强的成长性,在模型变向更大的时候,推理能力还能够得到提高。
5. 其他论文
5.1 gpt
gpt 在 bert 之前就发表了,使用了 transformer decoder 作为预训练的框架。在看到了 decoder 只能 get 上文信息,不能 get 下文信息的缺点之后,bert 改用了 transformer encoder 作为预训练的框架,能够同时 get 上下文信息,获得了巨大的成功。
5.2 structbert
structbert 的创新点主要在 loss 上,除了 MLM 的 loss 外,还有一个重构 token 顺序的 loss 和一个判断 2 个 segment 关系的 loss。重构 token 顺序的 loss 是以一定的概率挑选 segment 中的 token 三元组,然后随机打乱顺序,最后经过 encoder 之后能够纠正被打乱顺序的 token 三元组的顺序。判断 2 个 segment 关系的 loss 是 1/3 的概率是 segment B 是 segment A 的下一句,有 1/3 的概率是 segment A 是 segment B 的下一句,有 1/3 的概率是 segment A 和 segment B 来自 2 篇不同的文本,通过 “[cls]” 预测样本属于这 3 种的某一种。
5.3 roberta
在 xlnet 使用 126G 的数据登顶 GLUE 之后不久,roberta 使用 160G 的数据又打败了 xlnet。roberta 的创新点主要有 4 点:第 1 点是动态 mask,之前 bert 使用的是静态 mask,就是数据预处理的时候完成 mask 操作,之后训练的时候同一个样本都是相同的 mask 结果,动态 mask 就是在训练的时候每输入一个样本都要重新 mask,动态 mask 相比静态 mask 有更多不同 mask 结果的数据用于训练,效果很好。第 2 点是样本的构造方式,roberta 放弃了 NSP 的 loss,修改了样本的构造方式,将输入 2 个 segment 修改为从一个文本中连续 sample 句子直到塞满 512 的长度。当到达文本的末尾且未塞满 512 的长度时,先增加一个 “[sep]”,再从另一个文本接着 sample,直到塞满 512 的长度。第 3 点是增大了 batch size,在训练相同数据量的前提之下,增大 batch size 能够提高模型的推理能力。第 4 点是使用了 subword 的分词方法,类比于中文的字,相比于 full word 的分词方法,subword 的分词方法使得词表的大小从 30k 变成了 50k,虽然实验效果上 subword 的分词方法比 full word 差,但是作者坚信 subword 具备了理论优越性,今后肯定会比 full word 好 (手动黑脸)。
6. 总结
nlp 和 cv 的不同点在于 nlp 是认识学习,而 cv 是感知学习,nlp 在 cv 的基础之上多了一个符号映射的过程,正因如此,nlp 领域发展得比 cv 慢很多,cv 领域有很多比较成功的创业公司,有很多能够达到商用程度的子领域,而 nlp 领域就比较少。不过 nlp 领域在 17 年的 transformer 发布之后开始进入快速迭代的时期,bert 的发表使得 nlp 领域的 benchmark 提高了一大截,产生了不少可以达到商用程度的子领域。到了 19 年,nlp 领域的发展可以说是越来越快了,我在国庆的时候开始执笔写这个技术分享,当时 albert 刚发表 1 个星期,等我写完这个技术分享已经到 11 月了,前几天谷歌又发表了一篇 T5,又把 albert 打败了。T5 的论文据说有 50 页,是 nlp 预训练模型的一个综述,值得花时间一看。
最后更新: 2021年07月14日 21:24