Transformer学习笔记
摘要
最近学完了transformer框架,现在只要搞nlp基本上都是这玩意儿了。也打算改一改之前写博客的习惯,之前是恨不得只要学一个东西, 都写一个博客,这其实很没必要,很多东西一是没网上别的博客写的详细,二是学的那些东西后续可能用不上,记录太花时间。所以后续准备只记录需要记录的,比较重要的东西。
文章主要参考三个up主:
水导:Transformer、GPT、BERT,预训练语言模型的前世今生(目录) - 水论文的程序猿 - 博客园 (cnblogs.com)
DASOU:DASOU讲AI的个人空间_哔哩哔哩_bilibili
我整个深度学习的入门,是先快速过了一遍李航的统计学习方法,然后都是看的李沐,李沐的课程讲的比较全,包含了cv和nlp,所以nlp相关的入门是看的李沐。
但是可能李沐是绝对大佬的原因吧,有些地方其实讲的有点过于简洁,导致很多东西需要自己去调一遍代码,才能完全搞清楚,但是看代码是比较费时间的,并且transformer前面的那一堆nlp的东西,了解就好,真要去看代码有点得不偿失。
为了提高效率,翻了一堆博客,最终还是觉得水导讲的挺好好,比较通俗易懂,能力肯定不如李沐,但是讲课的能力还是不错的,有时候把自己会的讲给别人听也是一种能力,这可能和他干过培训机构的老师有关。但可惜水导后期不讲代码和框架了,都去讲如何水论文了,没办法,水论文看得人多。
此外,水导的那一版本的代码其实有点问题,并且代码讲的不如理论,很多东西含糊其辞的就跳过去了,说不重要。transformer那一整套代码,其实在了解理论之后,都不难,每一块都有对应,稍微复杂的就是矩阵的维度变换那里,为什么变换,怎么变换,水导都跳过去了。所以就找了另外一个版本,相对简介,且没有问题,讲的也比较好的upDASOU
1.个人学习路线
我还是不太认同一些博客中说的,现在的nlp直接学transformer就行了,以前的那些老框架没有学的意义了。其实还是有必要的,知道整个技术的发展路线,“旧框架有什么问题,为了解决这个问题,这才有了新框架”,这一整个发展过程的了解,对于后续的transformer学习,个人感觉很有帮助。
1.1 编码器-解码器架构
这是一切自然语言模型的基础架构,也很好理解,也不太重要,就是将输入进入编码器,得到一个有信息的词向量,再和解码器的输入做相关操作,得到输出。
编码器:将一个可变长的序列作为输入,并转换为具有固定形状的编码状态。
解码器:将固定形状的编码状态映射到长度可变的序列。

1.2 seq2seq
学seq2seq,到不必要学的那么深入,去看代码什么的,需要了解其中的原理,并且知道他是典型的编码器解码器架构就行。


- 编码器:
每个词元经过Embedding,然后传入RNN(2层双向的GRU),然后将隐状态集通过选定的函数(直接拿的最后一个隐变量),转换成上下文变量,然后传入后续解码器。 - 解码器
将编码器输出的隐变量进行reshape,和解码器的输入对应的大小保持一致,然后concat起来,传入RNN。对于RNN的输出做Mask-softmax(对于padding的处理),得到结果。 - 评估函数
标签序列A、B、C、D、E、F,预测序列A、B、B、C、D,精确度:p1=4/5,p2=3/4,p3=1/3,p4=0。
评估函数中包含对于过短预测序列和过长预测序列的处理。减小过短序列的权重,增大过长序列精确度。
1.3 seq2seq问题
- seq2seq中,最后输入到解码器中的是编码器中RNN的最后一个隐变量。拿文本翻译来举例,对于一个词的翻译,应该是更注重那个词及其周围一些词,而不是更注重最后一个(虽然文章中是说最后一个隐变量包含了前面的信息)
- seq2seq中运用的是RNN系列的模型进行编码,这就导致长距离依赖问题,即两个元素之间距离较长,且存在一定的关系的情况下,则RNN不一定可以很好的处理
- 并行处理问题,RNN系列的模型存在很强的顺序特性,需要一个一个的处理,这种形式并行性能较差
为了解决这三个问题,引出了注意力机制。
1.4 注意力机制

流程
- 意志搜索(q):即主动的操作,类似于搜索中的查询。
- 非意志搜索(k):类似于搜索中的数据库中数据集的key。如果不添加注意力机制,更偏向于关注到环境中比较容易被关注的东西。
- 值(v):一般情况下和k保持一致,transformer中运用的是自注意力,k是和v保持一致的。
- 即意志搜索(q)和非意志搜索(k)之间的交互形成了注意力汇聚。注意力汇聚有选择的聚合了值(v),以生成最终的输出。
那么问题来了,qk的汇聚是怎么汇聚的呢?其实就是计算qk两个向量的相似度。
传统的计算相似度也就是算两个向量的距离。一下即是演变流程,从\(x\)和\(x_i\)相减算距离,然后经过一个核矩阵,转化为概率,这里用的是高斯核,所以相当于转化成了softmax,然后又由于需要经过模型去学习,所以不能全是标量,这就引入了w。

上述是两个标量之间进行相减的距离计算,后续需要切换到向量角度,并且计算相似度不止是相减算距离这一种方式,transformer中用的就是两个向量做矩阵乘法,然后再算softmax来得到概率的。也即是下面的公式
算完之后,再把概率和v相乘,\(Attention = \sum_{i=1}^m \alpha_i V_i\)得到输出的词向量
1.4.1 编码机制
这里问题在于当时再学的时候,对于编码的理解还停留在独热编码,但是独热编码任何两个编码之间都是正交的,这就导致qk相乘得到的永远是0。后续看代码,看到的是直接把输入塞到nn.EMbedding()里面,点进源码看了一下,大概了解后,又去看了一遍编码的历史。

大体流程就是,NNLM在训练任务的时候,为了能将文字输入模型,对文字进行了编码,方法是将独热编码经过一个矩阵Q,然后得到一个新的向量,这样就保证了编码之间不再是正交的,这样就可以去算相似度了。并且这个矩阵Q是可以学习的。
由于这个副产品的出现,使得有人想专门搞个模型,来训练这个矩阵Q,以便得到更加优秀的词向量,然后这个词向量就可以供迁移学习使用了。所以就引申出了word2vec,这个模型的特点是预训练的结果不重要,重要的是副产品矩阵Q好不好,所以这个思想适合传统的语言学习模型不同的。
然后又引申出了新的问题,这个word2vec无法解决单词多意的问题,“我吃了一个苹果”和“我用苹果手机”,这两个“苹果”单词,在通过word2vec的时候,编码是一样的,这显然是不合理的,所以引出了ELMo。
elmo使用了两块lstm模块,且方向相反,这样就在原有词向量的基础上,又加入了上下文的信息,此时“吃苹果”和“苹果手机”就可以分辨了,这个elmo模型和bert很像,只不过bert用的是transformer的编码块。
总结:
这一套组合拳打下来,就使得原有的词向量变成了更加优秀的词向量。不仅可以计算相似度,还可以含有上下文的信息去解决多义词问题
1.4.2 seq2seq引入注意力机制

这样在seq2seq中引入注意力机制之后,经过qk相乘得相似度,再和v相乘的词向量的操作,就使得编码器传入解码器,不再是一股脑的全部塞进去,而是有选择的,挑选那些相似度最高的参数传进解码器。
1.4.3 自注意力机制
一句话是q、k、v同源。同源但是不一样,需要经过一个全连接。即得到三个表示方式不同,但表示内容全是输入x的三个词向量

流程:
先将词向量x分为三个 词向量q、k、v,然后\(\alpha_i = softmax(\frac{f(Q,K_i)}{\sqrt d_k})\),得到概率,再乘以v,得到z1。
总的来说,即是通过一通操作,在原始的词向量x中,包装进去了更多的信息,得到了一个更优秀的词向量
1.4.4 位置编码

由于注意力机制和rnn系列的模型不同,虽然通过一对多的计算相似度,解决了有选择的输入,长距离依赖,并行问题,但是丢失了rnn系列所拥有的时序,这在nlp领域是不允许的,因为失去了前后顺序,文字丢失了大部分的信息。于是就引出了位置编码。
这一部分,在transformer底层源码中,设计了一些矩阵维度变换的操作,需要一点时间去理解。
下面这三个东西,看了论文基本上都知道,这里主要文字总结一下,到底干了什么。



这里i代表一个句子中的一个文字或者单词,j代表的是一个文字或单词编码的位置,一般单词的编码维度是512维,这里分奇偶性,i=256。
所以分别有了256个周期不同的sin函数和cos函数,由于三角函数之间是正交的,这些函数叠加在一起,然后x轴代表i(也就是文字、单词在句中的位置),画x轴的垂线,得到一组交点,交点的值也就是编码值。这有点类似于计算机的二进制编码,0,1,2三位的01交叉频率就可以看做是函数的频率,所以就是三个频率不同的方波函数来表示的。

之所以用三角函数,是因为这么做,可以用三角函数和差化积公式,让绝对的位置信息中,包含了相对位置信息。不得不感叹想出这个算法的人还是很牛逼,虽然现在的bert已经不用这套编码了,位置编码的矩阵是自己学的。人都变懒了,我只管搞一个模型,然后往里面丢数据,其他的啥也不管。
1.5 transformer框架

然后正式进入transformer框架,经过前面的铺垫,这里其实比较好理解了。

这个多头注意力机制,其实就是将q,k,v拆分成多头,论文的解释是这么做,使得向量分到了不同的维度,有更多的信息,能更好的拟合。反正不懂,唯一的理由,即是这么做work,效果好。
多头注意力,理解很简单,但是代码里面还有点小操作,主要是一些矩阵维度转换的问题。
此外,这个拆分多头,不是把q,k,v又复制了几份,拆分的dim是编码,也就是512维度的编码,被拆成了多份。拆成多份,然后传入注意力机制,然后将结果再concat,经过全连接进行维度变换。这里详细的操作还是得看代码,或者去看水导的图。
1.5.1 模块解释
-
编码器:
- 嵌入层:就是1.4.1小节的那一套东西
- 位置编码:1.4.4小节
- 多头注意力:上面说了。编码器的注意力模块,用的是自注意力。
- 加&规范化:也就是残差和批量归一化那一套东西。防止梯度消失和梯度爆炸。
- 逐位前馈网络:两层mlp,加一些激活函数,让模型获得更好的非线性特性,使模型能够更好的拟合。
-
解码器
-
掩蔽多头注意力:因为解码器端,后续测试是不可能直接输入一整个句子的,而是输入一部分,然后让模型去预测后面一部分。但是为了方便,以及更好的训练效果,训练的时候,不是拿解码器输出,再循环回来作为解码器的输入的。而是直接拿标签来训练,为了模仿测试的环境,标签的句子后面需要预测的部分被mask掉了。
代码中,这个模块用的是一个上三角矩阵,并且需要结合编码器输出中的masked矩阵,以及解码器输入编码后的masked矩阵。
-
多头注意力:这里的区别就是,这个不是自注意力机制。
-
最后的全连接层:就是维度转换,转换到词典大小的维度。
-
1.6 BERT
后续的任务主要和BERT相关,所以就顺带学了一下BERT的内容,其实transformer会了之后,BERT还是很简单的。并且之前在编码那一小节,提到了elmo,elmo和BERT的框架结构非常的像,就是把lstm换成了transformer。

1.6.1 目的
BERT用的是transformer的编码器模块,gpt用的是解码器模块。两者的区别就是,解码器模块由于输入被masked了,所以是可以做传统的语言模型任务的,而bert用的编码器模块,由于编码器是双向的,所以相当于后续的东西已经知道了,这样再去做预测就没有意义了。但是编码器的好处是,下文没有被mask,所以词向量拥有了更丰富的信息,非常适合作为迁移学习。
1.6.2 预训练任务
BERT的预训练弄了两个,一个是做完形填空(因为无法做后续预测),并且用了一个mask操作,具体见下面。
然后又因为,一些nlp的任务是做句子相关的操作,而完形填空完全是基于词来做的,为了便于句子之间的任务,又搞了一个预测下文的任务。
-
MLM(完形填空)
由于BERT用的是Trm,得到的是完全的上下文信息,即后续词的信息已知,所以不适于传统的语言模型。
将随机选择15%的词元作为预测的掩蔽词元,用一个特殊的“”替换输入序列中的词元。但是微调或者测试的时候,不存在“ ”,为了弥补这个gap。 -
80%时间为特殊的“
“词元(例如,“this movie is great”变为“this movie is ”; -
10%时间为随机词元(例如,“this movie is great”变为“this movie is drink”);
-
10%时间内为不变的标签词元(例如,“this movie is great”变为“this movie is great”)
这就使得不再只关注于“
”的预测,模型会关注于其他的词,因为可能是随机的,也可能是正确的。毕竟模型的主要目的还是为了提取特征,供后续迁移学习,而不是单纯为了训练完形填空能力。
-
-
NSP(预测下文)
-
问答和自然语言推断,都基于两个句子做逻辑推理,而单词预测粒度的训练到不了句子关系这个层级。为了学习句子之间的关系,引入下句预测任务。
-
连续句对:[CLS]今天下午有公开演讲[SEP]下午的组会取消了[SEP]
随机句对:[CLS]今天下午有公开演讲[SEP]现在西瓜好便宜[SEP]
随机和连续各占50%,如果句子之间存在关系,[CLS]为1,否则为0。但是这不是主要目的,主要目的还是cls在训练过 程中包含了整个句子的信息,从而可以用于后续的迁移学习。
-
1.6.3 微调
这里是四个基本的微调,具体的还是看hugging face博客的bert微调实战。

2. 总结
这么点内容看了小五天,还是有点慢的。但感觉掌握的还算扎实,还去看了transformer底层的源码。我还是觉得不应该直接跳到transformer,前面的rnn,gru,lstm一点不了解的话,整个知识点串不起来,就像是Java直接跳到Springboot,完全不看ssm,这会导致只会用,不知为什么要这样。
道理是这么个道理,目前网上的自学资料也很多。但如果真的要从ssm看起,或者说从rnn看起,很多的教学视频、博客、书籍内容还是有点多的,从头到尾来一遍,非常耗时间,所以比较佩服那些搞全栈开发的,不知道哪来的时间和精力,估计每天除了吃饭睡觉,就是在学技术。

浙公网安备 33010602011771号