# BERT、GPT-2 的旧式 GELU 实现
def gelu(x):
return x * 0.5 * (1 + tf.tanh(np.sqrt(2/np.pi)*(x+0.044715*tf.pow(x,3))))
# 使用erf函数的 GELU 实现
def gelu(x):
cdf = 0.5 * (1.0 + tf.erf(x / tf.sqrt(2.0)))
return x * cdf
GELU vs Swish
GELU 与 Swish 激活函数(x · σ(βx))的函数形式和性质非常相像,一个是固定系数 1.702,另一个是可变系数 β(可以是可训练的参数,也可以是通过搜索来确定的常数),两者的实际应用表现也相差不大。
GLU(Gated Linear Unit)
圆圈代表哈达玛积,按位乘
在公式中,首先通过中间向量g(x)=xW进行门控操作,使用Sigmoid函数σ将其映射到0到1之间的范围,表示每个元素被保留的概率。然后,将输入向量x与门控后的向量进行逐元素相乘(即 ⊗ 操作),得到最终的输出向量。
GLU通过门控机制对输出进行把控,像Attention一样可看作是对重要特征的选择。其优势是不仅具有通用激活函数的非线性,而且反向传播梯度时具有线性通道,类似ResNet残差网络中的加和操作传递梯度,能够缓解梯度消失问题。
为什么?对比下sigmoid 及 LSTM中使用的 gated tanh unit (GTU) 的梯度:
GEGLU
是GLU的激活函数变体
将GLU中的sigmoid替换为GELU,函数形式如下(忽略bias项的书写):
GLU包含W和V两个可学习的参数
GEGLU也包含W和V两个可学习的参数,用GELU替换SIGMOD
SwiGLU
在PaLM论文中使用了SwiGLU激活函数。
在FFN中,即FC->激活函数->FC中,一般定义如下:
在T5论文中没有使用偏置项,也就是:
同理可得:
结合激活函数+未使用偏置项+GLU就得到:
这就是PaLM中的激活函数了,效果也是不错的:
采用SwiGLU激活函数:用于 MLP 中间激活,采用SwiGLU激活函数:用于 MLP 中间激活,因为与标准 ReLU、GELU 或 Swish 激活相比,《GLU Variants Improve Transformer》论文里提到:SwiGLU 已被证明可以显著提高模型效果
提出Parallel Layers:每个 Transformer 结构中的“并行”公式:与 GPT-J-6B 中一样,使用的是标准“序列化”公式。并行公式使大规模训练速度提高了大约 15%。消融实验显示在 8B 参数量下模型效果下降很小,但在 62B 参数量下没有模型效果下降的现象。
Multi-Query Attention:每个头共享键/值的映射,即“key”和“value”被投影到 [1, h],但“query”仍被投影到形状 [k, h],这种操作对模型质量和训练速度没有影响,但在自回归解码时间上有效节省了成本。
使用RoPE embeddings:使用的不是绝对或相对位置嵌入,而是RoPE,是因为 RoPE 嵌入在长文本上具有更好的性能 ,
采用Shared Input-Output Embeddings:输入和输出embedding矩阵是共享的,这个我理解类似于word2vec的输入W和输出W'
ChatGLM-6B
ChatGLM-6B是清华大学提出的支持中英双语问答的对话语言模型。ChatGLM-6B采用了与GLM-130B[4]相同的模型结构。截止到2022年7月,GLM-130B只训练了400B的tokens,中英文比例为1:1。ChatGLM-6B则使用了更多的训练数据,多达1T的tokens,训练语料只包含中文和英文,中英文比例为1:1。
模型结构上,ChatGLM-6B采用了prefix decoder-only的transformer模型框架,在输入上采用双向的注意力机制,在输出上采用单向注意力机制。在模型细节上,做了以下几点改动:
embedding层梯度缩减: 为了提升训练稳定性,减小了Embedding层的梯度。具体
\(\text{word\_embedding}=\text{word\_embedding}*\alpha+\text{word\_embedding}.detach()*(1-\alpha)\)其中,alpha为0.1,这里detach的作用是返回一个新的tensor,并从计算图中分离出来(不计入梯度)。梯度缩减的效果相当于把Embedding层的梯度缩小了10倍
Layer Normalization的顺序和残差连接被重新排列,用POST Normal,用Deep Normal
(\(deepNorm=LayerNorm(x*\alpha+f(x))\)),f(x)代表attention和FFN,相当于先做残差,再做标准化。
初始化对FFN,V_p,O_p用Xavier(w,gaim=\beta)
对Q_p,k_p用Xavier(w,gain=1)
用于输出标记预测的单个线性层;
用GEGLU作激活函数: 相比于普通的FFN,使用线性门控单元的GLU新增了一个权重矩阵,共有三个权重矩阵,为了保持参数量一致,中间维度采用了\(\frac{8}{3}d\)而非4d.
位置编码:去除了绝对位置编码,采用旋转位置编码RoPE
训练目标:ChatGLM-6B的训练任务是自回归文本填空。相比于采用causal decoder-only结构的大语言模型,采用prefix decoder-only结构的ChatGLM-6B存在一个劣势:训练效率低。causal decoder结构会在所有的token上计算损失,而prefix decoder只会在输出上计算损失,而不计算输入上的损失。在有相同数量的训练tokens的情况下,prefix decoder要比causal decoder的效果差,因为训练过程中实际用到的tokens数量要更少。另外,ChatGPT的成功已经证明了causal decoder结构的大语言模型可以获得非常好的few-shot和zero-shot生成能力,通过指令微调可以进一步激发模型的能力。至于prefix decoder结构的大语言模型能否获得相当的few-shot和zero-shot能力还缺少足够的验证。
训练时对一个完整的单词做Mask,这样可以避免 一个单词被拆分成多个Tokens,然后根据自己推测自己
tokenizer:关于tokenizer,ChatGLM在25GB的中英双语数据上训练了SentencePiece作为tokenizer,词表大小为130528。
下面是一些基于ChatGLM衍生出来的大模型应用:
langchain-ChatGLM:基于 langchain 的 ChatGLM 应用,实现基于可扩展知识库的问答。
闻达:大型语言模型调用平台,基于 ChatGLM-6B 实现了类 ChatPDF 功能。
BLOOM
BLOOM[5]系列模型是由BigScience团队训练的大语言模型。训练数据包含了英语、中文、法语、西班牙语、葡萄牙语等共46种语言,另外还包含13种编程语言。1.5TB经过去重和清洗的文本,转换为350B的tokens。训练数据的语言分布如下图所示,可以看到中文语料占比为16.2%。
按照模型参数量,BLOOM模型有560M、1.1B、1.7B、3B、7.1B和176B这几个不同参数规模的模型。BLOOMZ系列模型是在xP3数据集上微调得到的,推荐用于英语提示的场景。BLOOMZ-MT系列模型是在xP3mt数据集上微调得到的,推荐用于非英语提示的场景。
模型结构上,与GPT相同,BLOOM采用了causal decoder-only的transformer模型结构。在模型细节上,做了以下几点改动:
使用 ALiBi 位置嵌入,它根据键和查询的距离直接衰减注意力分数。与原始的 Transformer 和 Rotary 嵌入相比,它可以带来更流畅的训练和更好的下游性能。ALiBi不会在词嵌入中添加位置嵌入;相反,它会使用与其距离成比例的惩罚来偏向查询键的注意力评分。
Embedding Layer Norm 在第一个嵌入层之后立即使用,以避免训练不稳定。
layer normalization:为了提升训练的稳定性,没有使用传统的post layer norm,而是使用了pre layer Norm。
激活函数:采用了GeLU激活函数。
关于tokenizer,BLOOM在多语种语料上使用Byte Pair Encoding(BPE)算法进行训练得到tokenizer,词表大小为250880。使用了 25 万个标记的词汇表。使用字节级 BPE。这样,标记化永远不会产生未知标记
全连接层:
在训练目标上,BLOOM的训练目标是语言模型,即根据已有的上文去预测下一个词。
下面是一些基于BLOOM衍生出来的大模型应用:
轩辕: 金融领域大模型,度小满在BLOOM-176B的基础上针对中文通用领域和金融领域进行了针对性的预训练与微调。
BELLE: 链家仅使用由ChatGPT生产的数据,对BLOOMZ-7B1-mt进行了指令微调。
tokenizer比较
以上几个基座模型的tokenizer的词表大小不同,对同一个中文文本的分词结果会产生不同的结果。在news_commentary的6.9万条中英文平行语料上进行分词处理,对比分词结果和分词耗时,结果如下。“中文平均token数”表示了tokenizer分词后,每个中文字符对应的平均token数。
中文平均token数
英文平均token数
中文处理时间(s)
英文处理时间(s)
从结果来看,
LLaMA的词表是最小的,LLaMA在中英文上的平均token数都是最多的,这意味着LLaMA对中英文分词都会比较碎,比较细粒度。尤其在中文上平均token数高达1.45,这意味着LLaMA大概率会将中文字符切分为2个以上的token。
Chinese LLaMA扩展词表后,中文平均token数显著降低,会将一个汉字或两个汉字切分为一个token,提高了中文编码效率。
ChatGLM-6B是平衡中英文分词效果最好的tokenizer。由于词表比较大,中文处理时间也有增加。
BLOOM虽然是词表最大的,但由于是多语种的,在中英文上分词效率与ChatGLM-6B基本相当。需要注意的是,BLOOM的tokenizer用了transformers的BloomTokenizerFast实现,分词速度更快。
从两个例子上,来直观对比不同tokenizer的分词结果。“男儿何不带吴钩,收取关山五十州。”共有16字。几个tokenizer的分词结果如下:
LLaMA分词为24个token:(看着像是在Unicode编码级别做的BPE)
[ '男', '<0xE5>', '<0x84>', '<0xBF>', '何', '不', '<0xE5>', '<0xB8>', '<0xA6>', '<0xE5>', '<0x90>', '<0xB4>', '<0xE9>', '<0x92>', '<0xA9>', ',', '收', '取', '关', '山', '五', '十', '州', '。']
Chinese LLaMA分词为14个token:
[ '男', '儿', '何', '不', '带', '吴', '钩', ',', '收取', '关', '山', '五十', '州', '。']
ChatGLM-6B分词为11个token:
[ '男儿', '何不', '带', '吴', '钩', ',', '收取', '关山', '五十', '州', '。']
Bloom分词为13个token:
['男', '儿', '何不', '带', '吴', '钩', ',', '收取', '关', '山', '五十', '州', '。']
“杂申椒与菌桂兮,岂维纫夫蕙茝。”的长度为15字。几个tokenizer的分词结果如下:
LLaMA分词为37个token:
[ '<0xE6>', '<0x9D>', '<0x82>', '<0xE7>', '<0x94>', '<0xB3>', '<0xE6>', '<0xA4>', '<0x92>', '与', '<0xE8>', '<0x8F>', '<0x8C>', '<0xE6>', '<0xA1>', '<0x82>', '<0xE5>', '<0x85>', '<0xAE>', ',', '<0xE5>', '<0xB2>', '<0x82>', '<0xE7>', '<0xBB>', '<0xB4>', '<0xE7>', '<0xBA>', '<0xAB>', '夫', '<0xE8>', '<0x95>', '<0x99>', '<0xE8>', '<0x8C>', '<0x9D>', '。']
Chinese LLaMA分词为17个token:
[ '杂', '申', '椒', '与', '菌', '桂', '兮', ',', '岂', '维', '纫', '夫', '蕙', '<0xE8>', '<0x8C>', '<0x9D>', '。']
ChatGLM-6B分词为17个token:
[ '杂', '申', '椒', '与', '菌', '桂', '兮', ',', '岂', '维', '纫', '夫', '蕙', '<0xE8>', '<0x8C>', '<0x9D>', '。']
Bloom分词为17个token:
['杂', '申', '椒', '与', '菌', '桂', '兮', ',', '岂', '维', '�', '�', '夫', '蕙', '�', '�', '。']
从上面的例子可以看到,LLaMA词表中包含了极少数的中文字符,常见字“儿”也被切分为了3个token。Chinese LLaMA、ChatGLM-6B和Bloom的词表中则覆盖了大部分中文常见字,另外也包含了一些中文常用词,比如都把“收取”这个词切分为了一个token;对于一些生僻词,比如“茝”也会切分为2-3个token。总的来说,LLaMA通常会将一个中文汉字切分为2个以上的token,中文编码效率低;Chinese LLaMA、ChatGLM-6B和Bloom对中文分词的编码效率则更高。
Layer Normalization
如下图所示,按照layer normalization的位置不同,可以分为post layer norm和pre layer norm。
post layer norm。在原始的transformer中,layer normalization是放在残差连接之后的,称为post LN。使用Post LN的深层transformer模型容易出现训练不稳定的问题。如下图所示,post LN随着transformer层数的加深,梯度范数逐渐增大,导致了训练的不稳定性。
pre layer norm。改变layer normalization的位置,将其放在残差连接的过程中,self-attention或FFN块之前,称为“Pre LN”。如下图所示,Pre layer norm在每个transformer层的梯度范数近似相等,有利于提升训练稳定性。相比于post LN,使用pre LN的深层transformer训练更稳定,可以缓解训练不稳定问题。但缺点是pre LN可能会轻微影响transformer模型的性能 大语言模型的一个挑战就是如何提升训练的稳定性。为了提升训练稳定性,GPT3、PaLM、BLOOM、OPT等大语言模型都采用了pre layer norm。
layer normalization重要的两个部分是平移不变性和缩放不变性。 [8]认为layer normalization取得成功重要的是缩放不变性,而不是平移不变性。因此,去除了计算过程中的平移,只保留了缩放,进行了简化,提出了RMS Norm(Root Mean Square Layer Normalization),即均方根norm。
layer normalization的计算过程:
RMS计算过程:
相比于正常的layer normalization,RMS norm去除了计算均值进行平移的部分,计算速度更快,效果基本相当,甚至略有提升。Gopher、LLaMA、T5等大语言模型都采用了RMS norm。
[9]提出了Deep Norm可以缓解爆炸式模型更新的问题,把模型更新限制在常数,使得模型训练过程更稳定。具体地,Deep Norm方法在执行Layer Norm之前,up-scale了残差连接(\(\alpha\)>1);另外,在初始化阶段down-scale了模型参数(\(\beta<1\))。ChatGLM-6B采用了基于Deep Norm的post LN。
每个transformer层分为self attention块和FFN块两部分。FFN通常先将向量从维度d升维到中间维度4d,再从4d降维到d。FFN的计算公式如下:
其中,f()为非线性激活函数。广泛使用的激活函数有gelu(Gaussian Error Linear Unit)函数和swish函数。swish函数是一种自门控激活函数。
gelu也是一种通过门控机制调整输出值的激活函数,与swish函数类似,可以用tanh函数或 \(\sigma\)函数近似。
[10]提出了门控线形单元GLU(Gated Linear Units),相比于正常的FFN只有两个权重矩阵,使用GLU的FFN额外增加了一个权重矩阵,即下式中的V,共有三个权重矩阵,获得了更好的模型性能。
使用gelu激活函数的GLU计算公式为:
使用swish激活函数的GLU计算公式为:
对于transformer模型,位置编码是必不可少的。因为attention模块是无法捕捉输入顺序的,无法区分不同位置的token。位置编码分为绝对位置编码和相对位置编码。
最直接的方式是训练式位置编码,将位置编码当作可训练参数,训练一个位置编码向量矩阵。GPT3就采用了这种方式。训练式位置编码的缺点是没有外推性,即若训练时最大序列长度为2048,在推断时最多只能处理长度为2048的序列,超过这个长度就无法处理了。
苏神[11]提出了旋转位置编码RoPE。训练式的位置编码作用在token embedding上,而旋转位置编码RoPE作用在每个transformer层的self-attention块,在计算完Q/K之后,旋转位置编码作用在Q/K上,再计算attention score。旋转位置编码通过绝对编码的方式实现了相对位置编码,有良好的外推性。值得一提的是,RoPE不包含可训练参数。LLaMA、GLM-130B、PaLM等大语言模型就采用了旋转位置编码RoPE。
ALiBi(Attention with Linear Biases)[12]也是作用在每个transformer层的self-attention块,如下图所示,在计算完attention score后,直接为attention score矩阵加上一个预设好的偏置矩阵。这里的偏置矩阵是预设好的,固定的,不可训练。这个偏置根据q和k的相对距离来惩罚attention score,相对距离越大,惩罚项越大。相当于两个token的距离越远,相互贡献就越小。ALiBi位置编码有良好的外推性。BLOOM就采用了这种位置编码。
高效参数微调方法 PEFT
随着大语言模型的参数量越来越大,进行大模型的全量微调成本很高。高成本主要体现在硬件资源要求高,显存占用多;训练速度慢,耗时长;存储成本高。高效参数微调(parameter-efficient finetuning techniques,PEFT)在微调大模型时只训练一小部分参数,而不是训练全量多模型参数。高效参数微调方法有以下几方面优点:
显存占用少,对硬件资源要求低
训练速度快,耗时更短
更低的存储成本,不同的任务可以共享大部分的权重参数
可能会有更好的模型性能,减轻了过拟合问题
prompt tuning
prompt tuning[13]原本的含义指的是通过修改输入prompt来获得更好的模型效果。这里的提示是“硬提示(hard prompt)”。我们直接修改输入prompt,输入prompt是不可导的。
与“硬提示”相对应,“软提示微调(soft prompt tuning)”将一个可训练张量与输入文本的embeddings拼接起来,这个可训练张量可以通过反向传播来优化,进而提升目标任务的模型效果。这里的可训练张量可以理解为prompt文本对应的embedding,是一个soft prompt。如下图所示,这个可训练张量的形状是[virtal_tokens_sum,embed_size]
prompt tuning冻结大模型原始的参数,只训练这个新增加的prompt张量。prompt tuning随着基座模型参数量的增大效果会变好。
prefix tuning
prefix tuning[14]与prompt tuning相似,将一个特定任务的张量添加到输入,这个张量是可训练的,保持预训练模型的参数不变。主要区别如下:
prefix tuning将prefix参数(可训练张量)添加到所有的transformer层,而prompt tuning只将可训练矩阵添加到输入embedding。具体地,prefix tuning会将prefix张量作为past_key_value添加到所有的transformer层。
用一个独立的FFN来编码和优化prefix参数,而不是直接优化soft prompt,因为它可能造成不稳定并损害性能。在更新完soft prompt后,就不再使用FFN了。
prefix tuning与prompt tuning的作用位置不同,有点类似于可训练式位置编码和旋转位置编码RoPE。前者是直接作用在输入embedding上,后者是作用在所有transformer层的self-attention块,在计算得到K和V后,与可训练的prefix张量拼接起来。
prefix tuning可训练张量的形状是 \([virtual\_tokens\_num,2\times layer\_num \times hidden\_size]\)。下图是LLaMA-7B进行prefix tuning的例子,LLaMA-7B有32个transformer层,隐藏维度为4096,有 \(30,262144=2\times 32 \times 4096\)。30对应虚拟词数量,这里的2对应的是K和V。
Adapter
adapter[16]在某种程度上与prefix tuning是类似的,二者都是把额外的可训练参数添加到每个transformer层。不同之处是:prefix tuning是把prefix添加到输入embedding;而adapter在两个位置插入了adapter 层,如下图所示。
LLaMA-Adapter
LLaMA-adapter[16]结合了prefix tuning和adapter。与prefix tuning类似,LLaMA-adapter在输入embed上添加了可训练的prompt张量。需要注意的是:prefix是以一个embedding矩阵学习和保持的,而不是外部给出的。每个transformer层都有各自不同的可学习prefix,允许不同模型层进行更量身定制的适应。
如上图所示,LLaMA-adapter引进了零初始化的注意力机制和门控机制。动机是adapter和prefix tuning结合了随机初始化的张量(prefix prompts和adapter layers)很大可能会损害预训练语言模型的语义学知识,导致在训练初始阶段的微调不稳定和很高的性能损失。
另一个重要的区别是,LLaMA-adapter只给L个深层transformer层添加了可学习的adaption prompts,而不是给所有的transformer层都添加。作者认为,这种方法可以更有效的微调专注于高级语义信息的语言表示。