大模型笔记

Monday, July 8, 2024
本文共4779字
10分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/%E8%AE%BA%E6%96%87%E7%AC%94%E8%AE%B0/%E5%A4%A7%E6%A8%A1%E5%9E%8B/%E5%A4%A7%E6%A8%A1%E5%9E%8B%E7%AC%94%E8%AE%B0/。商业转载请联系作者获得授权,非商业转载请注明出处!

Transformer

方法

自注意力机制(self-attention)

$$ \text{Attention}(Q,K,V) = \text{Softmax}(\frac{QK^T}{\sqrt{d_K}})V $$

其中,$Q$代表查询,$K$代表键,$V$代表值 文内图片

对于多头注意力来说,其实就是对$Q,K,V$做了不同方向的线性变换

$$ \begin{align*} \text{MultiheadAttention}(Q,K,V) &= \text{Concat}(h_1, h_2, h_3, \ldots, h_i)W^O \newline \text{where } h_i &= \text{Attention}(QW_i^Q, KW_i^K, VW_i^V) \end{align*} $$

文内图片

Position-wise Feed-Forward Networks(FFN)

FFN实际上就是一个双层的感知机,用于对语句逐位置进行变换(transformer用于在语句内部交换信息)

$$ \mathrm{FFN}(x)=\max(0, xW_1 + b_1) W_2 + b_2 $$

位置编码(Position Encoding)

transformer使用周期函数进行位置编码的原因是:

  1. 如果使用归一化,对于不同长度的语句,处于相同位置的字词会有不同的值
  2. 如果直接使用索引,那么编码是没有上界的

因此,transformer使用sin和cos函数进行位置编码,以保证:

  1. 一个位置(pos)编码后可以是另一个位置的线性变换
  2. 对于每个维度($i$)都有一个不同的sin波形与之对应

$$PE_{(pos,2i)} = \sin(pos / 10000^{2i/d_{\text{model}}})$$ $$PE_{(pos,2i+1)} = \cos(pos / 10000^{2i/d_{\text{model}}})$$

在实际使用中, $1/10000^{2i/d_{\text{model}}}$ 可以被优化为

$$ \exp(-\log{10000} \cdot 2i/d_{\text{model}}) = e^{-{\log{10000}}^{2i/d_{\text{model}}}} = (1/10000)^{2i/d_{\text{model}}} $$

实验

总的来说是Encoder-Decoder模型,下游任务会接一个Generator

对于Encoder而言,总的流程是:

Encoder(SourceEmbedding(source), SourceMask)
# 具体而言,分为两次SublayerConnection
Original_X = X = SourceEmbedding(source)
# 第一次
# 1. 对嵌入向量转换后的语句进行层归一化
X = LayerNormalization(X)
# 2. 对语句计算Attention(可以实现为multihead的)
X = Attention(X,X,X,SourceMask) # 其中Q,K,V全是X
# 3. 对语句进行dropout
X = Dropout(X)
# 4. 将语句输入残差层
X = Original_X + X

# 第二次
# 不同的只有第二步
# 2. 对语句计算FFN
X = FFN(X)

对于Decoder而言:

# target是需要补全的语句,比如"你好,我叫"这个语句
# memory是Encoder的输出
# SourceMask是对target的Attention使用的
# TargetMask是对memory的Attention使用的
Decoder(TargetEmbedding(target), memory, SourceMask, TargetMask)

# 具体而言,分为三次SublayerConnection
Original_X = X = TargetEmbedding(source)
# 第一次
# 1. 对嵌入向量转换后的语句进行层归一化
X = LayerNormalization(X)
# 2. 对语句计算Attention(可以实现为multihead的)
X = Attention(X,X,X,SourceMask) # 其中Q,K,V全是X
# 3. 对语句进行dropout
X = Dropout(X)
# 4. 将语句输入残差层
X = Original_X + X

# 第二次
# 不同的只有第二步
# 2. 对语句计算Attention(可以实现为multihead的)
# 这里使用了memory参数,用于结合Encoder的信息
X = Attention(X,memory,memory,SourceMask)

# 第三次
# 不同的只有第二步
# 2. 对语句计算FFN
X = FFN(X)

对于Decoder而言,可以使用以下mask来让Attention注意到特定的列 文内图片

对于Generator,它使用Decoder的最后一个输出用于预测

由于Attention机制会在整个语句中传播信息,所以最后一个token已经包含了之前生成的所有上下文信息,并且为了保证后续生成的token的连续性及计算的精简,就只需要取最后一个token

# 假设嵌入向量的维度是k,输入的语句长度为n
# 那么X的维度就是nxk,这里就只取最后一个
# 也就是输入Generator的维度为1xk
y = Generator(X[-1, :])
# 之后,这个y会拼接到Decoder的target后面,从而进行连续的预测

BERT(Bidirectional Encoder Representations from Transformers)

BERT是基于transformer的NLP模型,不同的是:

  1. BERT只使用了transformer的Encoder部分
  2. BERT增加了segment embedding

Segment Embedding

Segment Embedding用于分析两个句子之间是否具有语义相似性,对一个句子对分别编码为0和1

文内图片

具体架构

# 首先对句子进行三次嵌入向量转换并dropout
X = TokenEmbedding(sequence) + PositionEmbedding(sequence) + SegmentEmbedding(segment_label) # 其中如果需要进行语义分析,就要额外提供segment_label,或者全设为0
X = Dropout(X)

# 然后输入多层Transformer层
# 其中Transformer层就是前文所述的Encoder
for _ in range(n):
	X = Transformer(X)
	X = Dropout(X)

# 下游任务有两个:
# 1. Masked LM 就是完形填空
# 2. Next Sentence Prediction (NSP) 就是预测某一句是否是另一句话的下一句
MaskedLM_Results = Softmax(Linear(X))
NSP_Results = Softmax(Linear(X))

Albert(A lite BERT)

Large Language Models, ALBERT — A Lite BERT for Self-supervised Learning | by Vyacheslav Efimov | Towards Data Science

看名字就可以知道,这是轻量级的BERT,但是实现了比BERT-large还要好的性能

Factorized Parameter Embedding 因式分解参数嵌入

假设我们的单词表一共有V个token,而每个单词被编码成H维度的向量,那我们的嵌入矩阵就会是VxH维的,这个矩阵实在是太大了。比如对于一个具有30k个token的单词表,每个token被编码成768维的向量,最终的矩阵就会是23M大小的

Albert的一个主要简化就是将VxH的矩阵分解为VxE的矩阵和ExH的矩阵相乘(注意这个分解可能会有精度损失,因此这个简化可能是有损的),当E « H的时候,这个简化就会非常有效

文内图片

Cross-layer Parameter Sharing 跨层参数共享

简而言之,就是共享同类层的参数从而加快计算,如下图所示,Albert进行了all parameters sharing

也就是说,一共只更新一层transformer的参数,但是进行多次计算(性能下降但是压缩率最高)

文内图片

参见初探ALBERT:参数共享的改进bert_albert 参数共享-CSDN博客

Sentence Order Prediction 语句顺序预测

BERT中有两个任务,前面已经说过,但是大量的研究证明NSP任务因为比较简单,导致优化效率不高

因此Albert使用了SOP任务

简而言之,就是对一对句子,判断他们的顺序是否正确。这一对句子从同一篇文章中取得,positive的样本就是顺序正确的,negative的样本就是顺序倒反的

下图对比了BERT的NSP任务和Albert的SOP任务

文内图片

性能对比

文内图片

可以看到,虽然Albert进行了参数压缩,而且取得了较好的结果,但是这是以时间成本为代价的,因此实际使用时仍需斟酌

RAG(Retrieval Augmented Generation)

RAG就是大模型的检索增强技术,意在为大模型的输出增加数据库支持

参见zhuanlan.zhihu.com/p/675509396

使用RAG的原因

  1. 大模型知识的局限性
  2. 大模型的幻觉问题
  3. 大模型回答的数据安全性

原理

文内图片

大致的原理就是基于用户的问题,在数据库中查找相关度较高的资料,注入到prompt中发送给大模型

文内图片

这是基本的原理,除了这些之外还有很多相关的技术

LLaMA

The Annotated LLaMA. Dissecting the code for the LLaMA… | by Nishant Bhansali | Medium

主要改变

使用RMSNorm代替LayerNorm

RMSNorm认为re-centering invariance property是不必要的,只用保留re-scaling invariance property,这一点从公式上也能看出来

LayerNorm的公式:

$$ \begin{align*} \mu &= \frac{1}{H}∑_{i=1}^{H}x_i \newline \sigma^2 &= \frac{1}{H}∑_{i=1}^{H}(x_i-μ)^2 \newline \hat{x}_i &= \frac{x_i - μ}{\sqrt{σ^2+ϵ}}\newline y_i &= \gamma \hat{x}_i + \beta \end{align*} $$

RMSNorm的公式:

$$ \begin{align*} \text{RMS}(x) &= \sqrt{\frac{1}{H}∑_{i=1}^{H}x_i^2} \newline \hat{x}_i &= \frac{x_i}{\text{RMS}(x)+ϵ}\newline y_i &= \gamma \hat{x}_i + \beta \end{align*} $$

RMSNorm去除了均值的使用,也就是不会将整个数据中心化

RMSNorm的好处

  1. 提高训练稳定性
  2. 无需均值计算
  3. 适用于变长序列
  4. 在训练和推理阶段的计算是一致的(不用使用训练时的均值和方差)
  5. 更好的梯度流动(标准化让数据处于有效范围内)

其他Norm

文内图片

  • BatchNorm:batch方向做归一化,算NHW的均值,对小batchsize效果不好;BN主要缺点是对batchsize的大小比较敏感,由于每次计算均值和方差是在一个batch上,所以如果batchsize太小,则计算的均值、方差不足以代表整个数据分布
  • LayerNorm:channel方向做归一化,算CHW的均值,主要对RNN作用明显;
  • InstanceNorm:一个channel内做归一化,算HW的均值,用在风格化迁移;因为在图像风格化中,生成结果主要依赖于某个图像实例,所以对整个batch归一化不适合图像风格化中,因而对HW做归一化。可以加速模型收敛,并且保持每个图像实例之间的独立。
  • GroupNorm:将channel方向分group,然后每个group内做归一化,算(C//G)HW的均值;这样与batchsize无关,不受其约束。
  • SwitchableNorm是将BN、LN、IN结合,赋予权重,让网络自己去学习归一化层应该使用什么方法。
  • 参见RMSNorm论文阅读

旋转位置编码Rotary Postional Embedding

使用这个编码的一个核心idea是基于attention公式的——我们知道attention中涉及$Q$与$K$的内积,此时又如何导入语句的位置信息呢?于是,llama设想存在一个函数$g$,它以$Q_m$,$K_n$和两者之间的位置差$m-n$为输入,恒等于$<f(Q_m, m), f(K_n, n)>$,其中$f(\cdot)$是旋转位置编码。

而论文中提供了一个符合的函数$g$,即

$$ \begin{align*} f(Q_m, m) &= Q_m e^{im\theta} \newline f(K_n, n) &= K_m e^{in\theta} \newline g(Q_m, K_n, m-n) &= Re[Q_mK_n^Te^{i(m-n)\theta}] \newline &= <f(Q_m, m), f(K_n, n)> \end{align*} $$

其中$i$是虚数,$Re[\cdot]$代表取实部

具体证明见zhuanlan.zhihu.com/p/642884818

简要证明如下:

左乘$e^{im\theta}$可以看作右乘一个旋转矩阵(由欧拉公式$e^{ix}=\cos{x} + i\sin{x}$得,具体证明略),则$f(Q_m, m) = R_aQ_m$,其中$R_a$是角度为$a$的旋转矩阵,同理$f(K_n, n) = R_bK_n$。

因为旋转矩阵有如下性质:

  1. $R_a^T = R_{-a}$
  2. $R_aR_b = R_{a+b}$

所以

$$ \begin{align*} <f(Q_m, m), f(K_n, n)> &= R_aQ_mR_b^TK_n^T \newline &= Q_mR_aR_{-b}K_n^T \newline &= Q_mR_{a-b}K_n^T \newline &= Q_mK_n^Te^{i(m-n)\theta} \newline &= <Q_m, R_{b-a}K_n> \end{align*} $$

只取实部是为了方便运算

使用了旋转矩阵也是它被称为旋转位置编码的原因

SwiGLU的使用

GLU 和 SwiGLU

GLU(Gated Linear Unit)

GLU公式如下:

$$ \begin{align*} GLU(x) &= A \circ \sigma(B) \newline &=(W_1x+b_1) \circ \sigma(W_2x+b_2) \end{align*} $$

其中$\circ$代表逐元素相乘(哈达玛积),$\sigma(\cdot)$是sigmoid函数

$A,B$可以是两个独立的MLP后的结果,也可以是卷积后的结果

SwiGLU

换个内部的激活函数而已

$$ \begin{align*} GLU(x) &= A \circ \text{swish}(B) \newline &=(W_1x+b_1) \circ \text{swish}(W_2x+b_2) \end{align*} $$

其中,

$$ \text{swish}(x) = x *\sigma(x) $$

K & V caching 键与值的缓存

在LLaMA中,除了第一次计算,之后的每一次计算都只需要输入一个token

比如,一个句子"I have several",将这个句子输入LLaMA后,预测得到"question",如果还需要接着预测,就只需要输入"question"就可以了,因为模型已经缓存了之前的句子的信息

这是如何做到的呢?

假设每个嵌入向量的维度为k,那么一个token的维度就是nxk,假设attention中Q,K,V的线性变换的权重w是kxz的,那么Q,K,V就会是nxz的

而因为这n个token的计算(也就是Q的每行的计算)是独立的(Attention机制和后面的FFN),并且最后预测下一个token的时候只取处理后的最后一个token(原因在transformer那一节讲过),所以其实在K,V保存了之前的上下文信息之后,Q不需要缓存,只取最后一项就好

那么,每次输入的语句的维度就会是1xk的,计算大幅缩减

每次计算时,K,V的过去值会被保存,下一次迭代的时候,K = [K_old, K_new]V = [V_old, V_new]

Introduction to Llama2 : Part-1 Architectural Analysis | by Utsavtiwari | Medium

整体架构

Llama模型结构解析(源码阅读)

文内图片

LLaMa2

和LLaMA的区别在于

  1. 增加了40%的训练数据
  2. 更长的上下文
  3. 分组查询注意力机制(GQA, Grouped-Query Attention)

分组查询注意力机制(GQA, Grouped-Query Attention)

文内图片

可以看到,GQA是一种折中,减少了K和V的数量由不至于过于极端

注意,这里是对于multihead的优化,与K V caching是可以共存的

LLaMA3

LLaMA3是一个比较小的模型(相对于GPT-4),但是使用了巨量的数据集进行训练,即便如此,Meta仍称LLaMA3还没有在标准意义上收敛,性能还待改善

变化如下:

  1. 更大的数据集
  2. 更长的上下文
  3. 词汇量更大的tokenizer(sentencepiece –> tiktoken)
  4. 更长的序列长度
  5. 更好的并行化
  6. 监督微调(SFT)拒绝采样近端策略优化(PPO)直接策略优化 (DPO)结合的微调方法