大模型笔记
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/。商业转载请联系作者获得授权,非商业转载请注明出处!
方法
自注意力机制(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使用周期函数进行位置编码的原因是:
- 如果使用归一化,对于不同长度的语句,处于相同位置的字词会有不同的值
- 如果直接使用索引,那么编码是没有上界的
因此,transformer使用sin和cos函数进行位置编码,以保证:
- 一个位置(pos)编码后可以是另一个位置的线性变换
- 对于每个维度($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是基于transformer的NLP模型,不同的是:
- BERT只使用了transformer的Encoder部分
- 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的原因
- 大模型知识的局限性
- 大模型的幻觉问题
- 大模型回答的数据安全性
原理
大致的原理就是基于用户的问题,在数据库中查找相关度较高的资料,注入到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的好处
- 提高训练稳定性
- 无需均值计算
- 适用于变长序列
- 在训练和推理阶段的计算是一致的(不用使用训练时的均值和方差)
- 更好的梯度流动(标准化让数据处于有效范围内)
其他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$。
因为旋转矩阵有如下性质:
- $R_a^T = R_{-a}$
- $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的区别在于
- 增加了40%的训练数据
- 更长的上下文
- 分组查询注意力机制(GQA, Grouped-Query Attention)
分组查询注意力机制(GQA, Grouped-Query Attention)
可以看到,GQA是一种折中,减少了K和V的数量由不至于过于极端
注意,这里是对于multihead的优化,与K V caching是可以共存的
LLaMA3
LLaMA3是一个比较小的模型(相对于GPT-4),但是使用了巨量的数据集进行训练,即便如此,Meta仍称LLaMA3还没有在标准意义上收敛,性能还待改善
变化如下:
- 更大的数据集
- 更长的上下文
- 词汇量更大的tokenizer(sentencepiece –> tiktoken)
- 更长的序列长度
- 更好的并行化
- 监督微调(SFT)、拒绝采样、近端策略优化(PPO)和直接策略优化 (DPO)结合的微调方法
扫码阅读此文章
点击按钮复制分享信息
点击订阅