首先给大家引入一个github博客,这份代码是我在看了4份transformer的源码后选出来的,这位作者的写法非常易懂,代码质量比较高。https://github.com/Separius/BERT-keras
这篇文章主要跟大家分享四个点:多头机制(multi-head)、LN和GELU、位置编码。
在这再给大家安利几篇博客,便于大家更具体的理解自注意力的内在原理。
https://zhuanlan.zhihu.com/p/44121378
https://zhuanlan.zhihu.com/p/47282410(精华)
https://www.cnblogs.com/robert-dlut/p/8638283.html
transformer是self-attention的落地或者说扩展,多头机制把自注意力机制发挥得淋漓尽致。transformer最亮眼的地方就是完全抛弃了常规的链式RNN结构(包括LSTM等其他变体),即:并行计算能力特别弱的计算方法。它应该会是早期NLP训练技术跟当期技术的一个里程碑,毕竟人家BERT是吧,刷新了不造几个记录,虽然XLNET又刷新了BERT的记录,但是这也正证实了这种设计理念的优秀!优秀啊。。。[斜眼笑]。。。
言归正传!
一、自注意力机制(self-attention)和多头机制(mutil-head)
常规的语言生成模型长这样
下一个字的生成,依靠且只依靠上一个字的输出状态和当前输入的输入状态,也就是说,预测值在某一程度上说只跟上一个字关系大一些,而自注意力模型,差不多长下面这个样子。
这个图的意思是,每一个字的生成,会跟所有的字(encode的时候)都有关系,这就是所谓的“注意力”机制。整个文字的生成过程中,每一个字都可能会跟所有的字做加权,为什么是“可能”呢,因为有mask嘛,随机给屏蔽掉一些词,屏蔽掉的那就没办法顾及了。这样的好处有两个:
一是能“照顾”所有的词,也就是我们理解的“语境”,
比如,句子1:“优秀!这就很优秀了!我做梦都没想到他这么175的个子能在中场投篮投进了!”;
和句子2:“优秀!这就很优秀了!他这185的个子在篮下那么好的位置还是没进球!”。
同样位置的一个词“优秀!”,一模一样的字,它的意思的完全相反的(中华文化博大精深),自注意力机制就需要在即使后面说了一堆废话的基础上,还是能学出这个词是褒义还是贬义。换句话说,它在判断优秀是褒义还是贬义时,甚至需要看到最后几个关键语气的词,才能做出判断,而这个功能正是RNN系列模型做不到的!数学意义上可以叫做“贡献度”。
二是,这样所以词就能并行计算了,至少这一步是可以并行计算了!
OK,自注意力就是大致这样个流程,多头又是什么鬼!很简单,经过嵌入层后,每个词有多个维度(代码嵌入为768列),把这些维度均分成n_head(12)份,每一份都去做这么一件事,就是多头机制。简而言之,就是自注意力的模式,复制了几次,这个“几次”就是“多头”,12次就是12头。。。只不过,不是做复制,二是做拆分,均分成12次来进行注意力的计算。
原理懂了哈,咱们看下人家是怎么实现的。
(1)funcs的multihead_attention函数
_q, _k, _v = x[:, :, :n_state], x[:, :, n_state:2 * n_state], x[:, :, -n_state:]
# [B, n_head, max_len, 768 // n_head]
q = split_heads(_q, n_head) # B, H, L, C//H
# [B, n_head, 768 // n_head, max_len]
k = split_heads(_k, n_head, k=True) # B, H, C//H, L
# [B, n_head, max_len, 768 // n_head]
v = split_heads(_v, n_head)
x是embedding后的输入,经过3*768个1x1卷积,变成[B, max_len, 3*768]的特征矩阵,q、k和v各占1/3,即:[B, max_len, 768]
(2)funcs的split_heads函数
# [-1, max_len, 768]
x_shape = shape_list(x)
# 768
m = x_shape[-1]
# [-1, max_len, n_head, 768 // n_head]
new_x_shape = x_shape[:-1] + [n, m // n]
# [B, max_len, n_head, 768 // n_head]
new_x = K.reshape(x, new_x_shape)
# return [B, n_head, max_len, 768 // n_head] False
# return [B, n_head, 768 // n_head, max_len] True
return K.permute_dimensions(new_x, [0, 2, 3, 1] if k else [0, 2, 1, 3])
这里对q和v拆分成[B, 12, max_len, 768 // 12] = [B, 12, max_len, 64]的长度,k拆成[B, 12, 64, max_len]
也就是说,每个词拆成了12等分,每一等分特征由之前的768变成了64。整个分成了B个batch,12的小batch,每个小batch的句子是max_len个长度的词组成,每个词有64的特征。
(3)这里咱们细讲一下这个permute,permute是自注意力计算逻辑最核心最抽象的地方之一。先上一张图
permute跟numpy的transpose是异曲同工的,都可以理解为转置。只是我们在对张量做转置的时候,用常规的二维矩阵的思维去理解比较难。但是!解释还是得用二维矩阵的方法去解释!
假设我们的q是一个2X6的矩阵,每一个字被嵌入成6个特征。k跟q一毛一样,但是我把k做一下转置,变成6X2的矩阵。这样的形式,我们用矩阵相乘的方法,把2x6的矩阵跟6x2的矩阵相乘,是不是变成了2x2,这个2x2是不是就可以理解为2个字对自己的排列组合。换句话说,所有的字与字之间的加权值得出来了。如图:优跟优的加权值是50(1x1+2x2+3x3+4x4+5x5),优跟秀的加权值是70(1x2+2x3+3x4+4x5+5x6),秀跟优的加权值是70(1x2+2x3+3x4+4x5+5x6),秀跟秀的加权值是90(2x2+3x3+4x4+5x5+6x6)。
(4)funcs的scaled_dot_product_attention_tf函数
先看这个函数:w = K.batch_dot(q, k)
这一步就是上一步所说的矩阵乘法。算法中,对每一组嵌入数据
[B, max_len, 768*3]通过均分,拆成3份[B, max_len, 768],分别作为q、k和v的前身;通过reshape,都变成[B, max_len, 12, 64];在通过permute变成q=[B, 12, max_len, 64]、k=[B, 12, 64, max_len]和v=[B, 12, max_len, 64]。
其中,q和k进行矩阵乘法,把max_len个字彼此求加权值(q=[B, 12, max_len, 64] * k=[B, 12, max_len, 64]),变成w=[B, 12, max_len, max_len]。这里的max_len放在“优秀”一组词里面就是2,即:当前句子的长度。
接着,继续对w和v做矩阵乘法,再求一次加权值(至于这一步有什么实际的原理,我不太确定),这一次矩阵乘法,在数学上把维度还原到[B, 12, max_len, 64],以方便后期继续还原成输入的shape。我揣测,这一步w*v的意义跟全连接层的意义是接近的。只不过在设计的出发点和可解释性上,要稍强于全连接层的意义。
所以,我用我自己组织的方式,给大家解释一下整个自注意力的过程
a、首先是split_heads,就是切分出qkv
b、矩阵乘法求权值,即:scaled_dot_product_attention_tf
c、merge_heads
经过上述两步,O(output)=[B, 12, max_len, 64],在这个函数里面,还原成[B, max_len, 64]
到此,多头+自注意力机制暂告一段落!
接下来你想在这一步重复多少次就重复多少次,因为输入输出都是一个shape。
二、LN(Layer Normalization)
https://blog.csdn.net/weixin_42078618/article/details/90730488
往这看!!!
三、GELU
一个公式说明一切(这个是近似函数,不是本征函数)
然后看看图像
当我看到这个图像的时候,我第一反应想起了Swish激活函数
他俩真的是异曲同工,几乎长得都一毛一样。从论文来看,GELU函数的收敛性会比RELU和ELU都有略好。
四、位置编码
位置信息对于理解一句话来说,也是很重要的。比如:
a、难受!滑板坏了,水还洒身上了!
b、滑板坏了,难受!水还洒身上了!
对于句子b,其实表达出来的意思,难受的重心是滑板,水是附加的负面影响。对于a,可能整体对心情造成的负面影响是差不多的。这就是词在不同位置可能对语境带了影响的一种情况。
常用的位置编码一般无外乎两种:一种是词嵌入,相当于先加一步全连接层,并且该层参数可学;另一种是自己设计位置编码方法。比如我印象中bert是用正余弦函数做编码的,以后看到再跟大家分享;或者,做一个递进的简单累加也不是不行哇,哈哈。
这里的Transformer阶段的位置编码只是使用了简单的词嵌入的方式,你也可以理解其实就是全连接层的一种应用方式。
结语:transformer以及后来的bert模型最核心的地方就是自注意力机制,大家能把自注意力的实现原理看懂,其核心思想也就一通百通了。