NLP基础
自注意力机制
在NLP方向上,自注意力机制要解决的问题。
在CV方向上,一般我们输入的都是图片,无论这个图片多大,都会resize到一个统一的尺寸。最终经过CNN的提取,变成一个特征向量,那么这个特征向量的维度是一样的。再经过softmax变成一个分类(Class)的概率
那么在NLP方向上就不是这样了,它可能输入的是很多个不同长度,不同大小的向量。例如我们输入的是一个句子,这个句子,我们也不知道它有多长。
我们会将句子中的每一个词汇都描述成一个向量——词向量。那对于一个句子来说,就是一个词向量的集合(a set of vectors)。一般词变词向量的方式使用的是word embedding。
对于输出来说,可以分为三种情况
1、输入多少个词向量,输出多少个label。
2、无论输入多少个词向量,只输出一个label
3、输入输出的长度上没有确定关系,输出不由输出的词向量的长度决定。它其实就是Seq2Seq。
因为有一大段的词向量,如果我们只是把它们的每个词向量简单的丢入到神经网络中,可能我们什么都得不到。
因为对于一个句子来说,我们可能更多关心的是每个词之间的位置关系以及词与词之间的语义关系。故而我们会在自注意力机制中将一个句子中所有的词向量变成一个向量,再由各个全连接层来进行每一个词向量的输出
但一般我们会使用两次自注意力机制再进行输出
在自注意力机制中,每一个输出都是考虑了上一层中所有的词向量的
现在我们来看看对于单个输出(b^1)是怎么产生的,其他的(b^2、b^3、b^4)是类似的。
对于(b^1)来说,我们需要看(a^1)与其他词向量之间的关系,看看哪些词跟(a^1)的关系更密切,哪些词跟(a^1)没太大关系。每一个词跟(a^1)的重要程度,我们会用一个α来表示。一般计算这个α会有几种方法,最常用的就是Dot-product。
首先我们会用 (a^1)分别和两个矩阵(W^q)和(W^k)相乘得到q和k,然后再将它们点乘得到α。
(a^1)跟所有的词向量都做了计算之后,就得到了不同的α(这里需要注意的是(a^1)跟自己本身也要做一个相关性计算),再经过一个Softmax就得到了一组新的α((α_{1.1}',α_{1.2}',α_{1.3}',α_{1.4}'....))。这个α可以理解成权重,因为Softmax本身就是占比。
之后我们会把每一个a((a^1,a^2,a^3,a^4...))乘以一个矩阵(w^v),得到一组新的向量v((v^1,v^2,v^3,v^4...)),每一个v与α相乘再相加就得到了(b^1),所以(b^1)相比与(a^1)来说是考虑了所有词向量与(a^1)的关系得到的新的词向量。公式为
- 矩阵运算
我们知道对于单个的a来说,对于q的计算就是q=(W^qa),对于所有的a来说
就有
(q^i=W^qa^i)
那么对于上图中的4个a来说,就有
我们将(a^1)到(a^4)四个向量合并成一个矩阵I,那么得到的四个向量(q^1)到(q^4)合并成一个矩阵Q。这样就有了
(Q=W^qI)
这里的(W^q)就是词向量集合I的参数。k,v跟q是一样的,以此类推,
(K=W^kI)
(V=W^vI)
所以就是句子矩阵I,分别跟三个不同的矩阵(W^q、W^k、W^v)相乘得到Q、K、V。这样就从单个的向量运算变成了矩阵运算。
然后是每一个q都会跟每一个k去计算α
这个过程同样也可以看成是一个矩阵乘以一个向量得到一个新的向量(这里需要注意矩阵乘向量为向量)
这里不仅仅是(q^1)需要计算,而是所有的q都要计算。
这样就有了
(A=K^TQ)
这个A就是注意力机制的分数。再把这个分数经过Softmax就得到了A',这个A'就是权重矩阵
之前我们计算新的词向量(b^1),就是
但我们仔细看的话,它其实就是一个矩阵与向量相乘
(b^1=Vα_1')
对于其他的(b^2、b^3、b^4),就有了
这里的O就是由所有的新的词向量构成的新的矩阵
(O=VA')
所以对于整个自注意力机制来说就是一连串的矩阵运算,这里的输入就是句子矩阵I,分别乘以三个矩阵(W^q、W^k、W^v)得到Q、K、V。再由(K^TQ)得到A,将A经过Softmax得到A',这个A'又叫Attenntion Matrix(注意力矩阵,权重)。再经过(VA')得到输出O。这一系列的操作中唯一需要学习的参数就是(W^q、W^k、W^v),只有这三个矩阵是未知的,需要通过我们的训练来得到的。而其他的步骤都不需要学习,都是已知的。
多头自注意力机制(Multi-head Self-attention)
上图是一个二头自注意力的机制,之前我们已经知道(q^i=W^qa^i),那么二头自注意力机制中会将(q^i)再分别乘以两个矩阵(如果是多头的话就乘以多个矩阵),分别再得到两个(q^{i,1})和(q^{i,2});同样,k和v也做同样的操作,分别得到(k^{i,1}、k^{i,2})以及(v^{i,1}、v^{i,2}),剩下的步骤就跟之前是一样的,只不过是1跟1的玩,2跟2的玩。即(α^{i,1}=q^{i,1}k^{i,1}),(α^{i,2}=q^{i,2}k^{i,2})。最后算出每个头各自的b。
这里我们得到的(b^{i,1})和(b^{i,2})会再经过一个变换矩阵(W^o),得到下一层的输出(b^i)(这里如果有多个头的话就会得到多个(b^{i,n}))
位置编码(Positionnal Encoding)
在以上的自注意力机制中都没有位置的信息。所有的(a^1)到(a^4),它们都是一样的,一样的操作,一样的运算,没有前后之分。
如果我们觉得在一个句子中,位置是很重要的信息(比如说动词不会出现在首位),那么我们就会对位置进行编码,变成一个唯一的位置向量(e^i)。
而具体,我们只需要将位置编码向量加到词向量中就好了。
最早的位置编码用的就是最简单粗暴的方式,你在哪个位置,就编码为几,称为hand-crafted。
位置编码现在还没有一个公认一致的方法,仍然存在着很多的创新。
上图中有这么几种方式,有人为设定好的方式也有使用数据学习出来的。
Transformer
Transformer是一种Sequence-to-sequence的结构,属于输入的向量数与输出的向量数无关的类型。
- 编码器(Encoder)
这是编码器的大致结构,它输入的就是一个一个的词向量,经过几层网络结构之后,最后输出跟输入相同数量的新的词向量。第一个Block就是之前说的自注意力机制加上全连接层组成的。但上图并不是确切的编码器,在第一个Block之间其实是有一个残差连接的(residual),并且会做一个Layer Norm。
Layer Norm是不同于BatchNorm的,它不用考虑batch的问题。它输入的是一个向量,输出的是另外一个向量。不同于BatchNorm(BatchNorm是针对不同的向量,去计算同一个维度的值的均值和方差),Layer Norm是计算同一个向量的不同维度的值的均值和方差。然后用每一个维度的值去减去均值再除以方差,得到新的向量。而这个新的向量就是全连接层的输入,而这个全连接层也有一个残差连接。残差连接之后再做一次Layer Norm的输出才是一个编码器Block的最终输出。
- 解码器(Decoder)
解码器会将编码器的输出给读取进去,但并不是作为解码器的原始输入。解码器有两种,比较常见的叫做自回归(Autoregressive(AT))。
在解码器中,它的原始输入并不是一次就输入一个完整的句子,最开始会辨别一个特殊的字符,如上图中的BEGIN,该字符是一个one-hot编码。输入了该字符后,解码器会输出一个向量。
这个输出的向量的长度很长,它是一个字典的字符的总数量(如英文可能是26个字母,中文3000多个字)。再输出这个向量之前会先运算一个Softmax,得到该字典内所有的字符的概率(总概率为1),这个向量并不是最终输出结果,我们会根据Softmax的计算结果,取概率最大的字符作为最终的输出。如上图中"机"的概率最高为0.8,则最终输出的就是"机"这个字符。
然后我们会将这个最终的输出"机"作为解码器的新的输入,再重复第一轮的过程,从字典中得到一个概率最高的字符进行输出,例如上图中输出的为"器"。
经过了一系列的过程之后,就有了这样一个结果。
掩码自注意力机制(Masked Self-attention)
解码器中的自注意力机制跟编码器中有所不同,称为掩码自注意力机制
简单说,在只有(a^1)作为输入的时候,就只考虑(a^1)自己的信息,而不能考虑后面的(a^2、a^3、a^4);当有(a^2)输入的时候,会同时考虑(a^1)和(a^2)的信息;以此类推当有一个解码器自回归的输入时,可以考虑前面所有的输入,而后面还未输入的信息是不考虑的。
具体一点说,当我们去计算(b^2)的时候,我们只会用(q^2)去分别乘以(k^1)和(k^2)得到(α_{2,1}')和(α_{2,2}'),而不必理会后面的(k^3、k^4)。再用(α_{2,1}'、α_{2,2}')分别乘以(v^1、v^2),结果再相加就得到了(b^2)。
解码器有一个最大的问题,就是它的输入的数量跟输出的数量是不一致的,虽然我们上面的例子中是输入4个字符,输出4个字符,但事实并非如此。
在解码器的输出向量中的字典里其实会有一个特殊字符END,表示输出需要终结。
例如在上面的例子中,当解码器输出"习"以后,再将"习"作为输入,那么它产生的新的输出就是END,不再作为输入而结束了。
还有一种解码器是非自回归(Non-autoregressive(NAT))
上图中左边的是自回归解码器,右边的就是非自回归解码器。它会同时输入多个BEGIN,然后输出多个字符,只需要一个步骤就可以完成句子的生成。
那我们该如何决定非自回归解码器的输出长度呢?
- 使用另外一个分类器(classifier),它的输入是整个Transformer的输入句子,输出的是一个数字,这个数字就是非自回归解码器的输出长度。
- 使用一个固定比较长的数字,例如300,那么就会输入300个BEGIN,输出300个字符。
如果在解码器的输出中发现了END,那么END后面的输出,我们都当成没有输出就好。
NAT的好处是它是并行的,不像AT需要一个一个的输出再输入。并且它可以控制输出的长度。
- 编码器-解码器(Encoder-Decoder)
在上图的红色区域内就是编解码器的连接处,该处称为交叉注意力机制(Cross attention)。在解码器中,如果屏蔽掉这部分,那么编解码器的结构就大致相同了。在交叉注意力机制中,编码器提供了两个输入,解码器提供了一个输入。
在上图中,编码器这一端会先输入一个句子,经过编码器产生了几个新的词向量,这些词向量再分别乘以两个转换矩阵(假设是(W^k)和(W^v))得到各自的k和v;在解码器这一端,先输入BEGIN,通过掩码自注意力机制,产生一个向量,该向量再乘以一个转换矩阵(假设说是(W^q))得到q,那么q就会和编码器端的各个k((k^1、k^2、k^3))相乘得到各个α'((α_1'、α_2'、α_3')),再分别乘以编码器端的v((v^1、v^2、v^3))再相加,就得到了交叉注意力机制层的输出——新的向量v。新的向量再通过全连接层进行处理。
然后就是解码器自回归得到的第二个输入的字符,上图中为"机"。再经过一遍上面的过程就会得到一个新的输出向量v',再将v'输入到全连接层中。
- 模型训练
现在假设编码器这边输入的是一段语音,这段语音的内容就是"机器学习"。所以我们会给这段语音打上标签(Label)——"机器学习"。但是我们标签的打法是以one-hot来标注的,如"机器学习"的"机"字,我们会标注为"0100",这个1其实也代表了Ground truth,即概率为1,其他的概率都为0。在解码器这一端输入BEGIN,输出的向量字典中的概率分布,我们希望它越接近Ground truth越好。所以我们会去计算Ground truth跟这个向量字典概率分布的交叉熵损失最小化。
所以我们对于每一个自回归的输入,都会做一次交叉熵的最小化,这其中也包括了结束字符END的交叉熵最小(END也是一个one-hot编码)。则我们会将所有的交叉熵的总和最小化。
BERT
BERT是基于Transformer的一种模型结构。
Trannsformer的原始结构是这样子的
它是由6个编码器和6个解码器共同构成了整体结构,但对于BERT来说
它是由12个编码器构成的。
BERT最大的特点就是它输入的句子可以是任意的,并且不需要标签(Label)。
BERT做的事情就是输入一个句子,然后输出一个个的词向量(word embedding)。我们来看一下什么是词向量
词向量就是把有相近语义的词尽可能的具有一定的相似度。而不是简单one-hot编码,完全看不出词与词之间的关系。
当然这个词向量是需要去训练才能得到的,而不是天然就可以编码的。
虽然我们这里的例子用的是中文的词——"潮水"、"退了"、"就"、"知道"。但是一般我们在训练中文的时候会使用字,因为词的数量太大,而常用字也就3000~4000左右,作为字典是比较合适的。
- 模型训练
BERT的训练方式有两种,第一种叫Masked LM
Masked LM会将所有输入的句子有随机15%的词汇会被置换成一个特殊的token——MASK。BERT在训练的时候需要去猜测这些被置换的词汇到底是什么词汇。
BERT会先将输入的句子的每一个词汇变成embedding,包括这个被替换掉的词,然后将被替换掉到词生成的embedding再放入到一个线性多分类器(Linear Multi-class Classifier)中,预测出被替换掉的词汇是哪一个词汇。由于是线性分类器,它的分类能力其实是比较弱的,这就对BERT的层数要求比较高,可能会达到24层,32层,而不再是之前说的12层。
如果两个词汇填在同一个地方没有违和感,那它们就有类似的embedding。比如上图中被替换掉的词其实是"退了",但如果BERT预测出来的词是"落了",同样是可以的,那就说明"退了"跟"落了"具有相似性。
第二种训练方式叫Next Sentence Prediction。
这种方式是判断两个句子是否是衔接的,还是不能衔接。
这里需要引入两个特殊的token——SEP,CLS。SEP是两个句子之间的边界(boundary),BERT要预测两个句子是不是相衔接的,还需要输入一个特殊的token就是CLS,它通常位于句子的开头,表示要做分类。CLS通过BERT输出的embedding会进入一个线性二分类器(Linear Binary Classifier)以对后面的两个句子是应该接在一起还是不应该接在一起做一个二分类。
CLS之所以能够放在开头,而不需要放在两个句子的结尾,是由BERT的网络架构决定的,因为BERT使用的是Transformer的编码器架构,编码器会同时处理输入句子中的所有的词,而不是像RNN一个一个去处理的。
由于我们会输入大量的句子,这些句子天然就有连接的,所以BERT会根据我们输入来训练哪些句子是可以连接在一起,哪些是不能连接在一起。
上面的这两种训练方式同时使用,会训练的最好。
- 使用方式
1、输入一个句子,输出一个分类标签。比如句子的情感分析,预测一个句子是正面的还是负面的。句子的新闻分类,比如说是体育新闻还是财经新闻,或者政治新闻等。
这种方式其实也是在句子的开头加入一个特殊的token——CLS表示分类,CLS通过BERT产生的embedding同样送入一个线性分类器中做分类,不过这里我们是需要对这些句子做标注的,比如情感分析中,我们需要标注这些句子哪一个句子是正面的,哪一个句子是负面的,这样BERT通过不断的学习就可以对其他的句子作出一个情感分类的判断。而BERT的预训练模型是支持中文的,我们只需要做好数据集(带标注),并且使用BERT的预训练模型参数进行微调(find-tune)就可以了。
2、输入一个句子,对句子中的每一个词都做一个分类。
比方说这个位置填充,我们需要知道上面输入的句子的每一个词属于哪一种Slot,上面的分类类别就有other、dest、time等,这些都是每一个词的标签。
我们会将输入的句子的每一个词都通过BERT生成的embedding都送入到线性分类器中去做分类,当然类别是我们自己人为去设定的。
3、输入两个句子,输出一个分类。
这里同第二种训练方式。
4、QA(回答基本问题,Extraction-based Question Answering)
一般这种类型的答案都是在文章中有出现过的。
在上图中,D表示文章中所有的token的集合,Q表示问题中所有的token的集合。而我们的模型(QA Model)会将这两种集合同时输入,输出两个整数s和e。那么答案就是文章中第s个token到第e个token。
比方说gravity是文章中的第17个token,那么我们的第一个问题输出的s,e两个整数就都是17。其他问题以此类推,
。
在上图中,SEP左边的是问题的tokens,右边是文章的tokens,文章的tokens通过BERT会生成各自的embedding。此时,我们会训练出两个不同的向量,如上图中的橙色和蓝色的向量,它们的维度跟BERT输出的黄色的embedding向量的维度是相同的。再使用其中橙色的向量与文章的所有token输出的embedding进行点乘得到一组标量(向量与向量相乘,结果为标量),再将这组标量通过Softmax得到一个组概率值(总和为1)。这组概率值中最大的那个所代表的token的序号就是整数s的值。如上图中s=2。
同理,我们会将蓝色的向量与文章的所有token输出的embedding进行点乘得到一组标量,再将这组标量通过Softmax得到一个组概率值(总和为1)。这组概率值中最大的那个所代表的token的序号就是整数e的值。如上图中e=3。
如果我们得到的s>e,那么就代表问题没有答案,我们需要输出此题无解。
LLaMA
GPT一代
模型堆叠了12个transformer的解码器层。由于这种设置中没有编码器,这些解码器层将不会有普通transformer解码器层所具有的编码器-解码器交叉注意力层。但是它扔具有自注意力层。
上图中的GPT包含了12个右边的Decode结构。它没有层级间的交叉注意力机制,只有掩码自注意力机制。
输入:
U = {(u_1,...,u_n)} 这里的(u_i)是一个一个的词向量,比如"我爱北京天安门",那么(u_1)就是"我",(u_2)就是"爱",(u_3)就是"北京",(u_4)就是"天安门"。
(h_0 = UW_e W_p) 神经网络
(h_l = transformer_block(h_{l-1}) ∀i∈1,n) 上一层的神经网络需要经过一个trannsformer_block抵达下一层神经网络
输出:
(P(u) = softmax(h_nW_e^T))
- 训练过程
我们的训练目标就是让某一个词在某个句子中出现的概率最大化,这其实就是一个完形填空。
北京是中国的_____。 (首都)
那我们的目标函数就为
(L_1(U) = sum_ilogP(u_i|u_{i-k},...,u_{i-1};θ)) θ是神经网络参数
这里的(u_i)就是"首都",条件概率中的条件(u_{i-k},...,u_{i-1})就是"北京"、"是"、"中国"、"的"。我们的目的就是要使得概率P最大化。
如此,我们就可以通过语料对该模型去进行一个训练。训练完了之后所得到的参数就可以去做推理测试。而LLaMA用到的语料包含了数万亿个词。
LLaMA背景介绍
LLaMA是一个基础语言模型的集合,参数范围从7B到65B。
- 这些模型是在来自公开数据集的数万亿个tokens上训练的。
- 它由Meta(前Facebook)于2023年2月发布,作为致力于开放科学和人工智能实践的一部分。
- 它与其他大型语言模型的关联
- LLaMA与GPT、GPT-3、Chinchilla和PaLM等其他大型语言模型类似,因为它使用Transformer architecture来预测给定单词或token序列作为输入的下一个单词或token。
- 然而,LLaMA与其他模型的不同之处在于,它使用在更多token上训练,得到较小模型,这使它更高效,资源密集度更低。
- LLaMA发展史
InstructGPT(基于提示学习的一系列模型) -> GPT3.5时代(大规模预训练语言模型,参数量超过1750亿) -> ChatGPT模型(高质量数据标注以及反馈学习(强化学习) -> LLaMA
- LLaMA的特点
- 参数量和训练语料:LLaMA有四种尺寸,7B、13B、33B和65B参数。最小的模型LLaMA 7B在一万亿个tokens上进行训练,而最大的模型LLaMA 65B在1.4万亿个tokens上训练。
- 语种:LLaMA涵盖了20种使用者最多的语言,重点是那些使用拉丁字母和西里尔字母的语言。这些语言包含英语、西班牙语、法语、俄语、阿拉伯语、印地语、汉语等。
- 生成方式:和GPT一样。
- 所需资源更小:LLaMA比其他模型更高效,资源密集度更低,因为它使用在更多tokens上训练的较小模型。这意味着它需要更少的计算能力和资源来训练和运行这些模型,也需要更少的内存和带宽来存储和传输它们。例如LLaMA 13B在大多数基准测试中都优于GPT-3(175B),而只使用了约7%的参数。
- 它对研究界很重要
- 它能够在人工智能领域实现更多的可访问性和个性化(垂直领域)。
- 通过共享LLaMA的代码和模型,Meta允许其他无法访问大量基础设施的研究人员研究,验证和改进这些模型,并探索新的用例和应用程序。
- 开源!
训练方式与训练数据
LLaMA模型训练方法和GPT-3差不多,都是自回归的方式(依据前/后出现的子词来预测当前时刻的子词)。在大量的语料中,使用标准的transformer优化器进行模型的训练。
- 数据集
LLaMA是用Common Crawl这个大规模的网络文本数据集和其他开源数据集来训练的。Common Crawl是一个公开的网络文本数据集,它包含了从2008年开始收集的数千亿个网页的原始数据、元数据和文本提取。另外进行了一些预处理,来确保数据的质量要求:
使用了fastText线性分类器执行语言识别以删除非英语页面,使用n-gram语言模型过滤低质量内容。
下载地址(42B tokens,300d vectors,1.75G):https://huggingface.co/stanfordnlp/glove/resolve/main/glove.42B.300d.zip
下载地址(840B tokens,300d vectors,2.03G):https://huggingface.co/stanfordnlp/glove/resolve/main/glove.840B.300d.zip
训练数据集是多个来源混合,如下表所示,涵盖了不同的领域
数据集 | 采样比例 | 训练轮数 | 数据集大小 |
---|---|---|---|
CommonCrawl | 67.0% | 1.10 | 3.3T |
C4 | 15.0% | 1.06 | 783G |
Github | 4.5% | 0.64 | 328G |
Wikipedia | 4.5% | 2.45 | 83G |
Books | 4.5% | 2.23 | 85G |
ArXiv | 2.5% | 1.06 | 92G |
StackExchange | 2.0% | 1.03 | 78G |
C4数据集是一个巨大的、清洗过的Common Crawl网络爬取语料库的版本。另外进行了一些不同的预处理,包含去重和语言识别步骤,与CommmonCrawl的主要区别在于质量过滤,它主要依赖于启发式方法,例如对网页中的标点符号的过滤,或者限制单词和句子的数量。
GitHub是使用Google BigQuery上可用的公共GitHub数据集。只保留在Apache、BSD和MIT许可证下分发的项目。根据行长或字母数字字符的比例使用启发式方法过滤了低质量文件。在文件级别对生成的数据集进行重复数据删除。
Wikipedia添加了2022年6月至8月期间的维基百科数据,涵盖20种语言,使用拉丁文或西里尔文脚本。
以上这些数据集的下载地址可以参考https://zhuanlan.zhihu.com/p/612243919?utm_id=0
模型结构
LLaMA的网络也是基于Transformer架构。并且对Trannsformer架构进行了部分改进。
- LLaMA方法——Pre-normalization
为了提高训练稳定性,对每个Transformer子层的输入进行归一化,而不是对输出进行归一化。这个叫RMSNorm归一化函数。
上图是传统的Transformer的编解码器,无论是编码器还是解码器,它们都有一个Add & Norm层,Add代表残差连接,Norm则是Layer Norm。LLaMA有8组编解码器。
而LLaMA则是把Layer Norm放到多头注意力机制之前,而通过Multi-Head Attention之后不再进行归一化处理。因为大模型的参数量很大,要进行稳定的训练是比较困难的。
(RMSNorm(x) = {x over sqrt {{1over n}sum_{i=1}^nx_i^2 ξ}})
其中x是输入的向量,n是向量的长度,ξ是一个很小的常数,用于避免分母为0。
- LLaMA方法——SwiGLU激活函数
SwiGLU激活函数代替ReLU非线性,以提高性能。
好处是:
- SwiGLU激活函数的收敛速度更快,效果更好。
- SwiGLU激活函数和ReLU都拥有线性的通道,可以使梯度很容易通过激活的units,更快收敛。
- SwiGLU激活函数相比ReLU更具有表达能力。
SwiGLU激活函数的收敛速度更快,这是因为它在计算过程中使用了门控机制,可以更好地控制信息的流动。
对于一些不重要的信息,我们就会让门完全关闭,而对于很重要的信息,则可以让门完全打开。SwiGLU是以门线性单元GLU为基础的,它是一个双线性函数,表达式为
这里的 ⊗ 表示矩阵的逐元素相乘,关于门控机制可以参考Tensorflow深度学习算法整理(二) 中的长短期记忆网络以及GRU。
- LLaMA方法——Rotary Embedding
删除了绝对位置嵌入,取而代之的是在网络的每一层中添加了旋转位置嵌入。
旋转位置嵌入的主要思想是将位置信息编码为一个旋转矩阵,然后将该矩阵与输入向量相乘,从而得到一个新的向量表示。这种方法可以更好地捕捉序列中不同位置之间关系,从而提升模型的性能。(有关旋转矩阵的内容可以参考线性代数整理 中的图形变换矩阵 (向量的函数))
虽然Transformer一般是处理文字数据的,但是它跟接近于CNN而不是RNN,对于句子中所有的输入tokens是同时送入网络并行处理的,而RNN是将句子中的tokens一个一个送入循环神经网络的,为了表征句子中词的位置关系,于是就有了位置编码Position Embedding。之前的位置编码又分为绝对位置和相对位置,绝对位置就是按照原句的顺序进行编码,如"我爱北京天安门",那么编码后就是"我"——1、"爱"——2、"北京"——3、“天安门”——4。一般来说Position Embedding会有一个长度限制——512。相对位置是以某一个词为基准来定义位置的,如"我爱北京天安门"中以"北京"为基准,那么"北京"的位置为0,"爱"——-1,"我"——-2,"天安门"——1。
LLaMA使用的是旋转位置嵌入,它可以更好的处理序列中的旋转对称性。在传统的位置编码方法中,位置信息只是简单的编码为一个向量,而没有考虑到序列中的旋转对称性。而旋转位置嵌入则将位置信息编码为一个旋转矩阵,从而更好的处理序列中的旋转对称性。
旋转对称性是指物体在旋转后仍然具有相同的性质。例如,一个正方形在旋转90度后仍然是一个正方形,因此具有旋转对称性。在句子序列中,旋转对称性指的是序列中的某些部分可以通过旋转变换得到其他部分。例如,在机器翻译任务中,源语言句子和目标语言句子之间存在一定的对称性。这意味着我们可以通过将源语言句子旋转一定角度来得到目标语言句子。
源码分析与解读
最核心的当然是Transformer
代码语言:javascript复制class Transformer(nn.Module):
def __init__(self, params: ModelArgs):
super().__init__()
self.params = params # 类实例参数
self.vocab_size = params.vocab_size # 词向量维度
self.n_layers = params.n_layers # 层数
# 获取词向量
self.tok_embeddings = ParallelEmbedding(
params.vocab_size, params.dim, init_method=lambda x: x
)
# 添加所有的TransformerBlock,共8层
self.layers = torch.nn.ModuleList()
for layer_id in range(params.n_layers):
self.layers.append(TransformerBlock(layer_id, params))
# 置前的批归一化
self.norm = RMSNorm(params.dim, eps=params.norm_eps)
# 线性特征提取
self.output = ColumnParallelLinear(
params.dim, params.vocab_size, bias=False, init_method=lambda x: x
)
# 计算旋转位置嵌入Rotary Embedding
self.freqs_cis = precompute_freqs_cis(
self.params.dim // self.params.n_heads, self.params.max_seq_len * 2
)
@torch.inference_mode()
def forward(self, tokens: torch.Tensor, start_pos: int):
# 获取句子输入的batchsize和句子的长度
_bsz, seqlen = tokens.shape
# 将句子变成词向量
h = self.tok_embeddings(tokens)
# 旋转位置嵌入
self.freqs_cis = self.freqs_cis.to(h.device)
freqs_cis = self.freqs_cis[start_pos : start_pos seqlen]
mask = None
if seqlen > 1:
mask = torch.full((1, 1, seqlen, seqlen), float("-inf"), device=tokens.device)
mask = torch.triu(mask, diagonal=start_pos 1).type_as(h)
# 将词向量(或隐层表示)送入到每一层的Transformer block
for layer in self.layers:
h = layer(h, start_pos, freqs_cis, mask)
# 批归一化
h = self.norm(h)
# 线性特征提取
output = self.output(h[:, -1, :]) # only compute last logits
return output.float()
然后是Transformer block,它就是layers中的每一层
代码语言:javascript复制class TransformerBlock(nn.Module):
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
self.n_heads = args.n_heads # 多头
self.dim = args.dim # 句子中词向量的最大数量
self.head_dim = args.dim // args.n_heads # 多头头数
self.attention = Attention(args) # 自注意力机制
# 前馈神经网络
self.feed_forward = FeedForward(
dim=args.dim, hidden_dim=4 * args.dim, multiple_of=args.multiple_of
)
self.layer_id = layer_id
# 批归一化
self.attention_norm = RMSNorm(args.dim, eps=args.norm_eps)
self.ffn_norm = RMSNorm(args.dim, eps=args.norm_eps)
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):
# 这里的第一步就是批归一化,然后经过自注意力机制运算再接一个残差连接
h = x self.attention.forward(self.attention_norm(x), start_pos, freqs_cis, mask)
# 上一步的输出再经过前馈神经网络再接一个残差连接
out = h self.feed_forward.forward(self.ffn_norm(h))
return out
然后是Attention
代码语言:javascript复制class Attention(nn.Module):
def __init__(self, args: ModelArgs):
super().__init__()
# 注意力机制中的头
self.n_local_heads = args.n_heads // fs_init.get_model_parallel_world_size()
# 注意力机制中头的长度
self.head_dim = args.dim // args.n_heads
# 查询向量矩阵
self.wq = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# 键向量矩阵
self.wk = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# 值向量矩阵
self.wv = ColumnParallelLinear(
args.dim,
args.n_heads * self.head_dim,
bias=False,
gather_output=False,
init_method=lambda x: x,
)
# 合并后的矩阵
self.wo = RowParallelLinear(
args.n_heads * self.head_dim,
args.dim,
bias=False,
input_is_parallel=True,
init_method=lambda x: x,
)
# 将键词对进行缓存,以便于推理的时候更加方便
self.cache_k = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
self.cache_v = torch.zeros(
(args.max_batch_size, args.max_seq_len, self.n_local_heads, self.head_dim)
).cuda()
def forward(self, x: torch.Tensor, start_pos: int, freqs_cis: torch.Tensor, mask: Optional[torch.Tensor]):
# 获取词向量的batchsize和长度
bsz, seqlen, _ = x.shape
# 通过使用q、k、v的线性变换矩阵对词向量进行线性变换获得q、k、v
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x)
# resize到各个头的大小
xq = xq.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xk = xk.view(bsz, seqlen, self.n_local_heads, self.head_dim)
xv = xv.view(bsz, seqlen, self.n_local_heads, self.head_dim)
# 进行旋转位置嵌入
xq, xk = apply_rotary_emb(xq, xk, freqs_cis=freqs_cis)
# 缓存这些k、v
self.cache_k = self.cache_k.to(xq)
self.cache_v = self.cache_v.to(xq)
self.cache_k[:bsz, start_pos : start_pos seqlen] = xk
self.cache_v[:bsz, start_pos : start_pos seqlen] = xv
# 从缓存中取出所有的k、v
keys = self.cache_k[:bsz, : start_pos seqlen]
values = self.cache_v[:bsz, : start_pos seqlen]
# 计算Attention的值,计算公式为SoftMax(QK^T/√d B)V
xq = xq.transpose(1, 2)
keys = keys.transpose(1, 2)
values = values.transpose(1, 2)
scores = torch.matmul(xq, keys.transpose(2, 3)) / math.sqrt(self.head_dim)
if mask is not None:
scores = scores mask # (bs, n_local_heads, slen, cache_len slen)
scores = F.softmax(scores.float(), dim=-1).type_as(xq)
output = torch.matmul(scores, values) # (bs, n_local_heads, slen, head_dim)
# 合并
output = output.transpose(
1, 2
).contiguous().view(bsz, seqlen, -1)
return self.wo(output)
然后是前馈神经网络FeedForward
代码语言:javascript复制class FeedForward(nn.Module):
def __init__(
self,
dim: int,
hidden_dim: int,
multiple_of: int,
):
super().__init__()
hidden_dim = int(2 * hidden_dim / 3)
hidden_dim = multiple_of * ((hidden_dim multiple_of - 1) // multiple_of)
self.w1 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
self.w2 = RowParallelLinear(
hidden_dim, dim, bias=False, input_is_parallel=True, init_method=lambda x: x
)
self.w3 = ColumnParallelLinear(
dim, hidden_dim, bias=False, gather_output=False, init_method=lambda x: x
)
def forward(self, x):
return self.w2(F.silu(self.w1(x)) * self.w3(x))
前馈神经网络其实就是一个全连接层,这个就不解释了。
然后是批归一化RMSNorm
代码语言:javascript复制class RMSNorm(torch.nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
# 计算公式中的ξ
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def _norm(self, x):
# RMSNorm公式,torch.rsqrt是张量倒数的平方根
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) self.eps)
def forward(self, x):
output = self._norm(x.float()).type_as(x)
return output * self.weight
实验分析
- 模型参数
模型的参数量有4种。
- 优化训练
- 使用因果多头注意力算子的高效实现。
- 为了进一步提高训练效率,减少了在带有检查点的反响传播过程中重新计算的激活量。通过手动实现变换器层的反向功能来实现,而不是依赖于PyTorch autograd。
- 训练时间
- 在训练65B参数模型时,代码在具有80G显存的2048 A100 GPU上处理大约380个Token/秒/GPU。
- 包含1.4T Token的数据集进行训练大约需要21天。
- 实验结果
由上图我们可以看到,模型的损失和Tokens之间的关系为当Tokens的数量不断增大的时候,模型的损失在不断的降低。该实验体现了在训练大模型时,数据量的重要性。
在20个数据集上对比了开源和闭源模型,主要是zero-shot和few-shot性能,也对比了instruct-tuning之后的效果。
在LLaMA 13B的模型与GPT-3 175B对比,我们会发现LLaMA 13B在各个数据集中都能跟GPT-3持平甚至超过。
模型推理
我们这里使用的是LLaMA 7B的模型去进行推理,在batch-size=2的时候,16G的显卡就够了,当然我这里使用的是24G的3090显卡。如果我们要去做模型微调(Fine tuning),需要四张A100。这里我们推理代码的下载地址为:https://github.com/pengwei-iie/llama_bugs
安装环境(我这里假设你的GPU,Cuda,Pytorch环境都装好了)
代码语言:javascript复制conda activate pytorch
pip install fairscale
pip install fire
pip install sentencepiece
pytorch是我自己的conda环境,需要换成你自己的环境。这里的senntencepiece是我们要用到的分词器组件。
- 预训练参数下载
下载地址:https://huggingface.co/nyanko7/LLaMA-7B/tree/main
进入推理代码主目录
代码语言:javascript复制cd llama_bugs-main
拉取预训练模型(如果git拉取有问题的话可以在网页上单独下载)
代码语言:javascript复制git clone https://huggingface.co/nyanko7/LLaMA-7B
- 开始执行推理
torchrun --nproc_per_node 1 example_small.py --ckpt_dir ./LLaMA-7B --tokenizer_path ./LLaMA-7B/tokenizer.model
运行后可以开始问答模式,不过这里只支持英文。
代码语言:javascript复制> initializing model parallel with size 1
> initializing ddp with size 1
> initializing pipeline with size 1
Loading..
Loaded in 4.94 seconds
Once upon a time, there were three bears. They were called Mama Bear, Papa Bear, and Little Bear. The three bears were very different from each other. Papa Bear was big and strong, and he liked to eat rocks. Mama Bear was not big but was very kind. Little Bear was a little bear, but he was very brave.
The three bears lived together in a house deep in the woods. They had a big garden and a stream for swimming. One day, Little Bear went to play in the garden.
“No!” shouted Little Bear. “Come back! You will get lost!” Then Little Bear began to cry.
Little Bear remembered what his Mama Bear had said, so he went back to the house.
Papa Bear did not answer. He was not there. Little Bear looked all over the house but could not find his Papa Bear.
“Papa Bear?” said Little Bear.
“Yes,” said Mama Bear, “he is right here.” Mama Bear was very happy that her Papa Bear was back home.
“Have I ever told you why I live in the woods?” asked Papa Bear.
“No, why?” asked Little Bear.
Mama Bear and Papa Bear smiled. “We live here to
==================================
what's your name?
what's your name? what's your d.o.b.?
where's your favorite place to go in the city?
how did you first get interested in fashion?
what was your favorite item to wear growing up?
what was your favorite item to wear growing up? how do you think this has influenced your style now?
what is your favorite item to wear now?
what is your favorite item to wear now? how do you think this has influenced your style now?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now?
in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit?
what is your favorite item to wear now? how do you think this has influenced your style now? in your eyes, what makes a "good" outfit? in your eyes, what makes a "good" outfit?
who are your favorite fashion designers
ChatGLM
背景介绍
- Chat-GLM 130B
- GLM-130B是一个双语(英语和汉语)预训练的语言模型,具有1300亿个参数,使用了General Language Model(GLM)的算法。
- ChatGLM参考了ChatGPT的设计思路,在千亿基座模型GLM-130B中注入了代码预训练,通过有监督微调(Supervised Fine-Tuning)等技术实现人类意图对齐。
- GLM-130B可以支持多种自然语言处理任务,如文本生成,文本理解,文本分类,文本摘要等。
- GLM-130B在多个英语和汉语的基准测试中优于其他模型,如GPT-3 175B、OPT-175B、BLOOM-176B、ERNIE TITAN 3.0 260B等。
- 开源应用
- 开源GLM-130B是为了促进双语自然语言处理的研究和应用,提供一个高质量的预训练模型给社区用。
- GLM-130B可以应用于多种场景,如机器翻译、对话系统、知识图谱、搜索引擎、内容生成等。
- GLM-130B可以帮助解决跨语言和跨领域的自然语言处理问题,提高人机交互的效率和体验。
- 贡献和创新
- GLM-130B是目前较大的开源双语预训练模型,而GLM-6B也是可以在单个服务器上单张GPU上支持推理的大模型。
- GLM-130B使用了GLM算法,实现双向密集连接的模型结构,提高了模型的表达能力和泛化能力。
- GLM-130B在训练过程中遇到了多种技术和工程挑战,如损失波动和不收敛等,提出了有效的解决方案,并开源了训练代码和日志。
- GLM-130B利用了一种独特的缩放性质,实现了INT4量化,几乎没有精度损失,并且支持多种硬件平台。
- 时间轨迹
- 2023.4.13,ChatGLM-6B开源30天内,全球下载量达到75万,Github星标数达到1.7万。
- 2023.3.31,ChatGLM-6B推出基于P-Tuning-v2的高效参数微调,最低只需7G显存即可进行微调,
- 2023.3.18,ChatGLM-6B登上Hugging Face Treding榜第一,持续12天.
- 2023.3.16,ChatGLM-6B登上Github Trending榜第一。
- 2023.3.14,千亿对话模型ChatGLM开始内测,60亿参数ChatGLM-6B模型开源。
- 应用
同时开源ChatGLM-6B模型,ChatGLM-6B是一个具有62亿参数的中英双语言模型。通过使用与ChatGLM(chatglm.cn)相同的技术,ChatGLM-6B初具中文问答和对话功能,并支持在单张2080Ti上进行推理使用。具体来说,ChatGLM-6B有如下特点:
- 充分的中英双语预训练:ChatGLM-6B在1:1比例的中英文语料上训练了1T的token量,兼具双语能力。
- 较低的部署门槛:FP16半精度下,ChatGLM-6B需要至少13G的显存进行推理,使得ChatGLM-6B可以部署在消费级显卡上。
- 更长的序列长度:相比GLM-10B(序列长度1024),ChatGLM-6B序列长度达204。
- 人类的意图对齐训练。
GLM模型方法
三种主流的预训练框架:
- autoregressive自回归模型代表是GPT,本质上是一个从左到右的语言模型,常用于无条件生成任务(unconditional generation)。
- autoencoding自编码模型是通过某个降噪目标(如掩码语言模型)训练的语言编码器,如BERT、ALBERT、DeBERTa。自编码模型擅长自然语言理解任务(natural language understanding tasks),常被用来生成句子的上下文提示。
- encoder-decoder则是一个完整的Transformer结构,包括一个编码器和一个解码器,以T5、BART为代表,常用于有条件的生成任务(conditional generation)。