https://github.com/ZJU-LLMs/Foundations-of-LLMs
3Blue1Brown - Transformers (how LLMs work) explained visually | DL5
毛玉仁
语言之于智能:在认知层面,语言与智能紧密相连,语言是智能的载体。
如何建模语言:将语言建模为一系列词元(Token)组成的序列数据。其中,词元是不可再拆分的最小语义单位。
语言模型:语言模型旨在预测一个词元或词元序列出现的概率。现有语言模型通常基于规则、统计或学习来构建。
{我,为,什么,要,选,这,门,课} → 语言模型 → 0.66666

语言模型的概率预测与上下文和语料库息息相关。
上下文
{这,课,好,难,我,为,什么,要,选,这,门,课} → 语言模型 → 0.9
{这,课,好,简,单,我,为,什么,要,选,这,门,课} → 语言模型 → 0.2
语料库
普通话语料库:{我,为,什么,要,选,这,门,课} → 语言模型 → 0.6
四川话语料库:
{我,为,什么,要,选,这,门,课} → 语言模型 → 0.2
{我,为,啥子,要,选,这,门,课} → 语言模型 → 0.6
综合以上两点,我们可以用条件概率的链式法则对语言模型的概率进行建模。
条件概率链式法则
设词元序列为$\{w_1, w_2, ..., w_N\}$,其概率可由条件概率的链式法则进行计算。
$P(\{w_1, w_2, ..., w_N\}) = P(w_1) \cdot P(w_2|w_1) \cdot P(w_3|w_1,w_2) ... P(w_N|w_1,w_2, ... , w_{N-2}, w_{N-1})$n-阶马尔科夫假设
当前状态只与前面n个状态有关。
对序列$\{w_1, w_2, ..., w_N\}$,当前状态$w_N$出现的概率只与前n个状态$\{w_{N-n},... ,w_{N-1}\}$有关,即:
$P(w_N|w_1,w_2, ... , w_{N-1}) \approx P(w_N|w_{N-n},... ,w_{N-1})$
n-grams 语言模型中的n-gram 指的是长度为n 的词序列。n-grams 语言模型通过依次统计文本中的n-gram 及其对应的(n-1)-gram 在语料库中出现的相对频率来
计算文本$w_{1:N}$ 出现的概率。
经典的n-grams语言模型,被工业界沿用至今。

n-grams语言模型中,n为变量,当n=1时,称之为unigram,其不考虑文本的上下文关系。当n=2时,称之为bigrams,其对前一个词进行考虑。当n=3时,称之为trigrams,其对前两个词进行考虑。以此类推。
bigrams的例子:

虽然“长颈鹿脖子长”并没有直接出现在语料库中,但是bigrams 语言模型仍可以预测出“长颈鹿脖子长”出现的概率有 2/15。由此可见,n-grams具备对未知文本的泛化能力。
n的选择会影响n-grams模型的泛化性能和计算复杂度。实际中n通常小于等于5。
泛化性:在n-grams 语言模型中,n 代表了拟合语料库的能力与对未知文本的泛化能力之间的权衡。当n 过大时,语料库中难以找到与n-gram 一模一样的词序列,可能出现大量“零概率”现象;在n 过小时,n-gram 难以承载足够的语言信息,不足以反应语料库的特性。
计算量:随着n的增大,n-gram模型的参数呈指数级增长。假设语料库中包含1000个词汇,则unigram的参数量为1000,而bigrams的参数量则为1000*1000。
n-grams中的统计学原理
n-grams语言模型是在n阶马尔可夫假设下,对语料库中出现的长度为n的词序列出现概率的极大似然估计。
n-gram的效果与语料库息息相关。Google在2005年开始Google Books Library Project项目,试图囊括自现代印刷术发明以来的全世界所有的书刊。其提供了unigram到5-gram的数据。
n-grams的应用
n-gram不仅在输入法、拼写纠错、机器翻译等任务上得到广泛应用。其还推动了Culturomics(文化组学)的诞生。
n-grams的缺点
n-gram因为观测长度有限,无法捕捉长程依赖。此外,其是逐字匹配的,不能很好地适应语言的复杂性。

统计:设计模型,描摹已知。
学习:找到模型,预测未知。

机器学习的过程:在某种学习范式下,基于训练数据,利用学习算法,从受归纳偏置限制的假设类中选取可以达到学习目标的假设,该假设可以泛化到未知数据上。
假设类:

归纳偏置:

学习范式:

学习目标:

损失函数:

学习算法:
1阶优化:目前最常用的梯度下降。
0阶优化:对梯度进行模拟,用估计出来的梯度来对模型进行优化。

泛化误差:

泛化误差界的公式来自概率近似正确(PAC,Probably Approximately Correct)理论。
PAC Learning为机器学习提供了对机器学习方法进行定量分析的理论框架,可以为设计机器学习方法提供理论指导。
Leslie Valiant由该理论,获得2010年图灵奖。



RNN 是一类网络连接中包含环路的神经网络的总称。

RNN 在串行输入的过程中,前面的元素会被循环编码成隐状态,并叠加到当前的输入上面。是在时间维度上嵌套的复合函数。
在训练RNN时,涉及大量的矩阵联乘操作,容易引发梯度衰减或梯度爆炸问题。
LSTM
为解决经典RNN的梯度衰减/爆炸问题,带有门控机制的LSTM被提出。
LSTM将经典RNN中的通过复合函数传递隐藏状态的方式,解耦为状态累加。隐藏状态通过遗忘门、输入门来实现合理的状态累加,通过输出门实现合理整合。
GRU为降低LSTM的计算成本,GRU将遗忘门与输入门进行合并。

左边是Encoder模块,右边是Decoder模块
典型的支持并行输入的模型是Transformer,其是一类基于注意力机制的模块化构建的神经网络结构。
论文最重要作者之一的 沙耶尔,后面创建了Character.AI。
两种主要模块
(1) 注意力模块
注意力模块负责对上下文进行通盘考虑。
注意力模块由自注意力层、残差连接和层正则化组成。

(2) 全连接前馈模块
全连接前馈模块占据了Transformer近三分之二的参数,掌管着Transformer模型的记忆。

说白了是加权输出的机制,而权重是通过$W_q$、$W_k$两个矩阵学出来的。
加权平均:原值是$v$,权重是当前位置的$q$和上下文的$k$的相似度。
袁粒老师的比喻
—— 想在京东买一件女式的红色大衣
Q、K、V的解释:
Q:输入的查询词:“女式”、“红色”、“大衣”;K:搜索引擎根据输入Q提供K(颜色、种类等),根据Q与K的相似程度匹配到最终搜索到的商品V。
层正则化用以加速神经网络训练过程并取得更好的泛化性能;引入残差连接可以有效解决梯度消失问题。
残差连接加在LN前叫pre-LN,加在LN后叫post-LN。不同的模型里,两种加法性能各有优劣。
自回归 vs. Teacher Forcing
自回归面临着错误级联放大和串行效率低两个主要问题。为了解决上述两个问题,Teacher Forcing在语言模型预训练过程中被广泛应用。



但Teacher Forcing的训练方式将导致曝光偏差(Exposure Bias):训练模型的过程和模型在推理过程存在差异。其易导致模型幻觉问题。
语言模型每轮预测输出的是一个概率向量。我们需要根据概率值从词表中选出本轮输出的词元。选择词元的过程被称为采样。
两类主流的采样方法可以总结为
(1). 概率最大化方法
最大化$P(w_{N+1:N+M}) = \prod_{i=N}^{N+M-1} P(w_{i+1}|w_{1:i}) = \prod_{i=N}^{N+M-1}o_i(w_{i+1})$
假设生成M个词元,概率最大化方法的搜索空间为$M^D$,是NP-Hard问题。
贪心搜索在每轮预测中都选择概率最大的词
波束搜索在每轮预测中都先保留b个可能性最高的词,在结束搜索事,得到M个集合。找出最优组合使得联合概率最大。

但概率最大的文本通常是最为常见的文本。这些文本会略显平庸。用于生成代码还行。但在开放式文本生成中,贪心搜索和波束搜索都容易生成一些“废话文学”——重复且平庸的文本。
(2). 随机采样方法
在每轮预测时,其先选出一组可能性高的候选词,然后按照其概率分布进行随机采样。
在每轮预测中都选取K个概率最高的词作为本轮的候选词集合。
缺点:受候选词分布的方差的影响,方差大时可能“胡言乱语”,方差小时,候选集不够丰富。
Top-P设定阈值p来对候选集进行选取。
Temperature机制
Top-K采样和Top-P采样的随机性由语言模型输出的概率决定,不可自由调整。但在不同场景中,我们对于随机性的要求可能不同。引入Temperature机制可以对解码随机性进行调节。

Temperature越高随机性越高。可以看到T无穷大时,会变成概率为$\frac{1}{K} $或$\frac{1}{|S_p|} $的均匀分布。反之,T趋近于0时,也会将概率值大的输出通过指数放缩得更大,再归一化。
(1)内在评测:测试文本通常由与预训练中所用的文本独立同分布的文本构成,不依赖于具体任务。
困惑度(Perplexity),$PPL(s_{test})=P(w_{1:N})^{-\frac{1}{N}}=\sqrt[N]{\prod_{i=1}^{N}\frac{1}{P(w_i|w_{
困惑度减小也意味着熵减,意味着模型“胡言乱语”的可能性降低。
(2)外在评测:测试文本通常包括该任务上的问题和对应的标准答案,其依赖于具体任务。
BLEU (BiLingual Evaluation Understudy):计算n-gram匹配精度的一种指标
示例:“大语言模型”翻译成英文,生成的翻译为“big language models”,而参考文本为“large language models”。
当n=1时,$Pr(g_1)=\frac{2}{3}$。
当n=2时,$Pr(g_2)=\frac{1}{2}$。
当N=2时,$BLEU = \sqrt{\frac{1}{2}\cdot\frac{2}{3}}=\sqrt{\frac{1}{3}}$
从语义理解的层面进行评测
基于上下文词嵌入:上下文词嵌入(Contextual Embeddings)向量的相似度。
基于生成模型:直接利用提示词工程引导LLM输出评测分数。属于无参评价。

语言模型输出的概率值可以直接应用于输入法、机器翻译、对话等任务。
语言模型中间产出的文本嵌入可以应用于实体识别、实体检测、文本检索等任务。
涌现能力:实验发现新能力随着模型规模提升凭空自然涌现出来,因此将其称为涌现能力(Emergent Abilities),例如上下文学习、逻辑推理和常识推理等能力。
扩展法则:GPT系列模型的性能提升,有着一系列关于模型能力与参数/数据规模之间的定量关系作为理论支撑,即扩展法则(Scaling Law)。其中以OpenAI提出的Kaplan-McCandlish法则以及DeepMind提出的Chinchilla法则最为著名。
模型基础:Transformer灵活的并行架构为训练数据和参数的扩展提供了模型基础,推动了本轮大语言模型的法则。
[!NOTE]
如上一章所述Transformer是模块化的模型
在Transformer的基础上衍生出了三种主流模型架构。
[!TIP]
- 纯 Encoder 模型(例如 BERT),又称自编码 (auto-encoding) Transformer 模型;
- 纯 Decoder 模型(例如 GPT),又称自回归 (auto-regressive) Transformer 模型;
- Encoder-Decoder 模型(例如 BART、T5),又称 Seq2Seq (sequence-to-sequence) Transformer 模型。
只选用Transformer中的Encoder部分,代表模型为BERT系列。

同时选用Transformer中的Encoder和Decoder部分,代表模型为T5、BART等。

只选用Transformer中的Decoder部分,代表模型为GPT和LLaMA系列。

https://transformers.run/c1/transformer/
标准的 Transformer 模型主要由两个模块构成:
Encoder(左边):负责理解输入文本,为每个输入构造对应的语义表示(语义特征);
Decoder(右边):负责生成输出,使用 Encoder 输出的语义表示结合其他输入来生成目标序列。

这两个模块可以根据任务的需求而单独使用:
原始结构
Transformer 模型本来是为了翻译任务而设计的。在训练过程中,Encoder 接受源语言的句子作为输入,而 Decoder 则接受目标语言的翻译作为输入。在 Encoder 中,由于翻译一个词语需要依赖于上下文,因此注意力层可以访问句子中的所有词语;而 Decoder 是顺序地进行解码,在生成每个词语时,注意力层只能访问前面已经生成的单词。
例如,假设翻译模型当前已经预测出了三个词语,我们会把这三个词语作为输入送入 Decoder,然后 Decoder 结合 Encoder 所有的源语言输入来预测第四个词语。
实际训练中为了加快速度,会将整个目标序列都送入 Decoder,然后在注意力层中通过 Mask 遮盖掉未来的词语来防止信息泄露。例如我们在预测第三个词语时,应该只能访问到已生成的前两个词语,如果 Decoder 能够访问到序列中的第三个(甚至后面的)词语,就相当于作弊了。
其中,Decoder 中的第一个注意力层关注 Decoder 过去所有的输入,而第二个注意力层则是使用 Encoder 的输出,因此 Decoder 可以基于整个输入句子来预测当前词语。这对于翻译任务非常有用,因为同一句话在不同语言下的词语顺序可能并不一致(不能逐词翻译),所以出现在源语言句子后部的词语反而可能对目标语言句子前部词语的预测非常重要。
在 Encoder/Decoder 的注意力层中,我们还会使用 Attention Mask 遮盖掉某些词语来防止模型关注它们,例如为了将数据处理为相同长度而向序列中添加的填充 (padding) 字符。
Transformer家族

Encoder 分支
纯 Encoder 模型只使用 Transformer 模型中的 Encoder 模块,也被称为自编码 (auto-encoding) 模型。在每个阶段,注意力层都可以访问到原始输入句子中的所有词语,即具有“双向 (Bi-directional)”注意力。
纯 Encoder 模型通常通过破坏给定的句子(例如随机遮盖其中的词语),然后让模型进行重构来进行预训练,最适合处理那些需要理解整个句子语义的任务,例如句子分类、命名实体识别(词语分类)、抽取式问答。
BERT 是第一个基于 Transformer 结构的纯 Encoder 模型。
Decoder 分支
纯 Decoder 模型只使用 Transformer 模型中的 Decoder 模块。在每个阶段,对于给定的词语,注意力层只能访问句子中位于它之前的词语,即只能迭代地基于已经生成的词语来逐个预测后面的词语,因此也被称为自回归 (auto-regressive) 模型。
纯 Decoder 模型的预训练通常围绕着预测句子中下一个单词展开。纯 Decoder 模型适合处理那些只涉及文本生成的任务。
对 Transformer Decoder 模型的探索在在很大程度上是由 OpenAI 带头进行的。
Encoder-Decoder 分支
Encoder-Decoder 模型(又称 Seq2Seq 模型)同时使用 Transformer 架构的两个模块。在每个阶段,Encoder 的注意力层都可以访问初始输入句子中的所有单词,而 Decoder 的注意力层则只能访问输入中给定词语之前的词语(即已经解码生成的词语)。
Encoder-Decoder 模型可以使用 Encoder 或 Decoder 模型的目标来完成预训练,但通常会包含一些更复杂的任务。例如,T5 通过随机遮盖掉输入中的文本片段进行预训练,训练目标则是预测出被遮盖掉的文本。Encoder-Decoder 模型适合处理那些需要根据给定输入来生成新文本的任务,例如自动摘要、翻译、生成式问答。


形式化表示为:
$Attention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d_k}})V$'''
手工实现 Scaled Dot-product Attention
'''
# 文本分词, 并转换为词向量:
from torch import nn
from transformers import AutoConfig
from transformers import AutoTokenizer
model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
text = "I really like eating McDonald"
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
print(inputs.keys())
print(inputs.input_ids)
config = AutoConfig.from_pretrained(model_ckpt)
token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
print(token_emb)
inputs_embeds = token_emb(inputs.input_ids)
print(inputs_embeds.size())
# 创建 query、key、value 向量序列, 并且使用点积作为相似度函数来计算注意力分数:
import torch
from math import sqrt
Q = K = V = inputs_embeds # Self-Attention
dim_k = K.size(-1)
scores = torch.bmm(Q, K.transpose(1,2)) / sqrt(dim_k)
print(scores.size())
# 这里Q、K的序列长度都为5,因此生成了一个5x5的注意力分数矩阵,接下来就是应用 Softmax 标准化注意力权重:
import torch.nn.functional as F
weights = F.softmax(scores, dim=-1)
print(weights.sum(dim=-1))
# 最后将注意力权重与V序列相乘:
attn_outputs = torch.bmm(weights, V)
print(attn_outputs.shape)
打印输出:
dict_keys(['input_ids', 'token_type_ids', 'attention_mask'])
tensor([[1045, 2428, 2066, 5983, 9383]])
Embedding(30522, 768)
torch.Size([1, 5, 768])
torch.Size([1, 5, 5])
tensor([[1., 1., 1., 1., 1.]], grad_fn=<SumBackward1>)
torch.Size([1, 5, 768])
'''
至此实现了一个简化版的 Scaled Dot-product Attention。可以将上面这些操作封装为函数
'''
def scaled_dot_product_attention(query, key, value, query_mask=None, key_mask=None, mask=None):
dim_k = query.size(-1)
scores = torch.bmm(query, key.transpose(1, 2)) / sqrt(dim_k)
if query_mask is not None and key_mask is not None:
mask = torch.bmm(query_mask.unsqueeze(-1), key_mask.unsqueeze(1))
if mask is not None:
# Fills elements of self tensor with value where mask is True
scores = scores.masked_fill(mask == 0, -float("inf"))
weights = F.softmax(scores, dim=-1)
return torch.bmm(weights, value)
[!NOTE]
上面的代码还考虑了 $Q,K,V$ 序列的 Mask。填充 (padding) 字符不应该参与计算,因此将对应的注意力分数设置为 −∞,这样 softmax 之后其对应的注意力权重就为 0 了(e−∞=0)。
注意!上面的做法会带来一个问题:当 Q 和 K 序列相同时,注意力机制会为上下文中的相同单词分配非常大的分数(点积为 1),而在实践中,相关词往往比相同词更重要。例如对于上面的例子,只有关注“eating”才能够确认“McDonald”的含义。
因此,多头注意力 (Multi-head Attention) 出现了!
多头注意力首先通过线性映射将 $Q,K,V$ 序列映射到特征空间,每一组线性投影后的向量表示称为一个头,然后在每组映射后的序列上再应用 Scaled Dot-product Attention:

每个注意力头负责关注某一方面的语义相似性,多个头就可以让模型同时关注多个方面。因此与简单的 Scaled Dot-product Attention 相比,Multi-head Attention 可以捕获到更加复杂的特征信息。
形式化表示为:
$head_i = Attention(QW^Q_i, KW^K_i, VW^V_i) $ $MultiHead(Q, K, V) = Concat(head_1, ..., head_h)$其中 $W_i^Q∈R^{d_k×\tilde{d}_k},W_i^K∈R^{d_k×\tilde{d}_k},W_i^V∈R^{d_v×\tilde{d}_v}$是映射矩阵,$h$ 是注意力头的数量。最后,将多头的结果拼接起来就得到最终 m×hd~v 的结果序列。所谓的“多头” (Multi-head),其实就是多做几次 Scaled Dot-product Attention,然后把结果拼接。
'''
下面我们首先实现一个注意力头:
'''
from torch import nn
class AttentionHead(nn.Module):
def __init__(self, embed_dim, head_dim):
super().__init__()
self.q = nn.Linear(embed_dim, head_dim)
self.k = nn.Linear(embed_dim, head_dim)
self.v = nn.Linear(embed_dim, head_dim)
def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
attn_outputs = scaled_dot_product_attention(
self.q(query), self.k(key), self.v(value), query_mask, key_mask, mask)
return attn_outputs
每个头都会初始化三个独立的线性层,负责将 Q,K,V 序列映射到尺寸为 [batch_size, seq_len, head_dim] 的张量,其中 head_dim 是映射到的向量维度。
[!NOTE]
实践中一般将
head_dim设置为embed_dim的因数,这样 token 嵌入式表示的维度就可以保持不变,例如 BERT 有 12 个注意力头,因此每个头的维度被设置为 768/12=64。
最后只需要拼接多个注意力头的输出就可以构建出 Multi-head Attention 层了(这里在拼接后还通过一个线性变换来生成最终的输出张量):
class MultiHeadAttention(nn.Module):
def __init__(self, config):
super().__init__()
embed_dim = config.hidden_size
num_heads = config.num_attention_heads
head_dim = embed_dim // num_heads
self.heads = nn.ModuleList(
[AttentionHead(embed_dim, head_dim) for _ in range(num_heads)]
)
self.output_linear = nn.Linear(embed_dim, embed_dim)
def forward(self, query, key, value, query_mask=None, key_mask=None, mask=None):
x = torch.cat([
h(query, key, value, query_mask, key_mask, mask) for h in self.heads
], dim=-1)
x = self.output_linear(x)
return x
这里使用 BERT-base-uncased 模型的参数初始化 Multi-head Attention 层,并且将之前构建的输入送入模型以验证是否工作正常:
from transformers import AutoConfig
from transformers import AutoTokenizer
model_ckpt = "bert-base-uncased"
tokenizer = AutoTokenizer.from_pretrained(model_ckpt)
text = "time flies like an arrow"
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False)
config = AutoConfig.from_pretrained(model_ckpt)
token_emb = nn.Embedding(config.vocab_size, config.hidden_size)
inputs_embeds = token_emb(inputs.input_ids)
multihead_attn = MultiHeadAttention(config)
query = key = value = inputs_embeds
attn_output = multihead_attn(query, key, value)
print(attn_output.size())
回忆一下上一章中介绍过的标准 Transformer 结构,Encoder 负责将输入的词语序列转换为词向量序列,Decoder 则基于 Encoder 的隐状态来迭代地生成词语序列作为输出,每次生成一个词语。
其中,Encoder 和 Decoder 都各自包含有多个 building blocks。下图展示了一个翻译任务的例子:

可以看到:
Transformer Encoder/Decoder 中的前馈子层实际上就是两层全连接神经网络,它单独地处理序列中的每一个词向量,也被称为 position-wise feed-forward layer。常见做法是让第一层的维度是词向量大小的 4 倍,然后以 GELU 作为激活函数。
class FeedForward(nn.Module):
def __init__(self, config):
super().__init__()
self.linear_1 = nn.Linear(config.hidden_size, config.intermediate_size)
self.linear_2 = nn.Linear(config.intermediate_size, config.hidden_size)
self.gelu = nn.GELU()
self.dropout = nn.Dropout(config.hidden_dropout_prob)
def forward(self, x):
x = self.linear_1(x)
x = self.gelu(x)
x = self.linear_2(x)
x = self.dropout(x)
return x
将前面注意力层的输出送入到该层中以测试是否符合我们的预期:
feed_forward = FeedForward(config)
ff_outputs = feed_forward(attn_output)
print(ff_outputs.size())
至此创建完整 Transformer Encoder 的所有要素都已齐备,只需要再加上 Skip Connections 和 Layer Normalization 就大功告成了。
Layer Normalization 负责将一批 (batch) 输入中的每一个都标准化为均值为零且具有单位方差;Skip Connections 则是将张量直接传递给模型的下一层而不进行处理,并将其添加到处理后的张量中。
向 Transformer Encoder/Decoder 中添加 Layer Normalization 目前共有两种做法:

本章采用第二种方式来构建 Transformer Encoder 层:
class TransformerEncoderLayer(nn.Module):
def __init__(self, config):
super().__init__()
self.layer_norm_1 = nn.LayerNorm(config.hidden_size)
self.layer_norm_2 = nn.LayerNorm(config.hidden_size)
self.attention = MultiHeadAttention(config)
self.feed_forward = FeedForward(config)
def forward(self, x, mask=None):
# Apply layer normalization and then copy input into query, key, value
hidden_state = self.layer_norm_1(x)
# Apply attention with a skip connection
x = x + self.attention(hidden_state, hidden_state, hidden_state, mask=mask)
# Apply feed-forward layer with a skip connection
x = x + self.feed_forward(self.layer_norm_2(x))
return x
同样地,这里将之前构建的输入送入到该层中进行测试:
encoder_layer = TransformerEncoderLayer(config)
print(inputs_embeds.shape)
print(encoder_layer(inputs_embeds).size())
结果符合预期!至此,本章就构建出了一个几乎完整的 Transformer Encoder 层。
前面讲过,由于注意力机制无法捕获词语之间的位置信息,因此 Transformer 模型还使用 Positional Embeddings 添加了词语的位置信息。
Positional Embeddings 基于一个简单但有效的想法:使用与位置相关的值模式来增强词向量。
下面将所有这些层结合起来构建完整的 Transformer Encoder:
class TransformerEncoder(nn.Module):
def __init__(self, config):
super().__init__()
self.embeddings = Embeddings(config)
self.layers = nn.ModuleList([TransformerEncoderLayer(config)
for _ in range(config.num_hidden_layers)])
def forward(self, x, mask=None):
x = self.embeddings(x)
for layer in self.layers:
x = layer(x, mask=mask)
return x
Transformer Decoder 与 Encoder 最大的不同在于 Decoder 有两个注意力子层。
Masked multi-head self-attention layer(自注意力模块):确保在每个时间步生成的词语仅基于过去的输出和当前预测的词,否则 Decoder 相当于作弊了;
Encoder-decoder attention layer(交叉注意力模块):以解码器的中间表示作为 queries,对 encoder stack 的输出 key 和 value 向量执行 Multi-head Attention。通过这种方式,Encoder-Decoder Attention Layer 就可以学习到如何关联来自两个不同序列的词语,例如两种不同的语言。 解码器可以访问每个 block 中 Encoder 的 keys 和 values。
与 Encoder 中的 Mask 不同,Decoder 的 Mask 是一个下三角矩阵:
seq_len = inputs.input_ids.size(-1)
mask = torch.tril(torch.ones(seq_len, seq_len)).unsqueeze(0)
print(mask[0])
tensor([[1., 0., 0., 0., 0.],
[1., 1., 0., 0., 0.],
[1., 1., 1., 0., 0.],
[1., 1., 1., 1., 0.],
[1., 1., 1., 1., 1.]])
这里使用 PyTorch 自带的 tril() 函数来创建下三角矩阵,然后同样地,通过 Tensor.masked_fill() 将所有零替换为负无穷大来防止注意力头看到未来的词语而造成信息泄露:
scores.masked_fill(mask == 0, -float("inf"))
本章对 Decoder 只做简单的介绍,如果你想更深入的了解可以参考 Andrej Karpathy 实现的 minGPT。
[!TIP]
本章的所有代码已经整理于 Github:
https://gist.github.com/jsksxs360/3ae3b176352fa78a4fca39fff0ffe648

由于各自独特的模型设计以及注意力矩阵上的差异,在同等参数规模下,这三种架构的模型在适用任务上也都各有倾向。
Encoder-only架构中的双向注意力机制允许模型充分考虑序列中的前后文信息,捕捉丰富的语义和依赖关系。但由于缺少解码器组件,无法直接输出序列。
适合:判别任务,如情感识别。
Encoder-Decoder架构通过添加解码器来基于编码器输出的上下文表示逐步生成输出序列。但解码器的加入也造成的训练和推理成本的增加。
适合:既适合判别任务,也适合生成任务。但计算量大幅提升,不利于参数扩展。
Encoder-Decoder架构主要包含编码器和解码器两部分:


大规模预训练数据的加持使得Decoder-only架构的模型能够生成高质量、连贯的文本。但是缺乏编码器提供的双向上下文信息,这一架构在理解复杂输入数据时存在一定局限性。
适合:生成任务,如对话问答。

Decoder-only架构去除了Transformer中的编码器部分,其简单的架构设计和强大的可扩展性,使得Decoder-only架构被广泛应用于大规模语言模型。
不仅去除了编码器,由此解码器中也不需要交叉注意力模块了。
CloseAI的GPT闭源,Meta的LLaMA开源。
GPT系列:
GPT-1:《Improving Language Understanding by Generative Pre-Training》
GPT-2:《Language Models are Unsupervised Multitask Learners》
GPT-3:《Language Models are Few-Shot Learners》,GPT-3涌现出良好的上下文学习(In-Context Learning, ICL)能力。
GPT-3的一系列衍生模型,其中最具启发意义的是有良好指令跟随能力的InstructGPT模型:《Training language models to follow instructions with human feedback》
往后就闭源了。
ChatGPT(GPT-3.5):标志着一种新的服务模式LLMaaS(LLM as a Service)的出现。GPT模型也开始走向闭源。
GPT-4:更好理解复杂语境、生成连贯文本。还引入对图文双模态的支持。
GPT-4o:继续提升模型性能和用户体验。比GPT4便宜很多,但成本、响应速度、延迟、多模态处理和多语言支持能力都比较好。

LLaMA系列:
推动力大语言模型的“共创”。
GPT系列的升级主线聚焦于模型规模与预训练预料的同步提升(KM法则),而LLaMA则在模型规模上保持相对稳定,更专注于提升预训练数据的规模与质量(Chinchilla法则)。


Mamba的原理

选择状态空间模型(Selective State Space Model) 所用的是控制论里面的知识。
第一部分:状态空间模型(State Space Model,SSM)
n阶系统用n个1阶系统进行矩阵表达。
阅读[1]:https://blog.csdn.net/v_JULY_v/article/details/134923301
阅读[2]:https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-mamba-and-state
状态方程:$h(t) = Ah(t-1) + Bx(t)$
输出方程:$y(t) = Ch(t) + Dx(t)$
[!NOTE]
$Dx(t)$ 可以视为跳跃连接,可被简化掉。

第二部分:从SSM到S4、S4D的升级之路
零阶保持 能够令连续SSM转变为离散SSM,使得不再是函数$x(t)$到$y(t)$,而是序列到序列$x_k$到$y_k$。

循环结构表示:方便快速推理
$y2 = Ch2 = C(\bar{A}h_1+\bar{B}x2) = ... = C\bar{A}^2\bar{B}x_0 + C\bar{A}\bar{B}x_1 + C\bar{B}x_2$有没有眼前一亮?如此,便可以RNN的结构来处理。

卷积结构表示:方便并行训练
$y2 = C\bar{A}^2\bar{B}x_0 + C\bar{A}\bar{B}x_1 + C\bar{B}x_2$可以视为:

由于其中三个离散参数A、B、C都是常数,因此我们可以预先计算左侧向量并将其保存为卷积核,这为我们提供了一种使用卷积超高速计算$y$的简单方法:
这儿A、B、C都是 时不变。但这样

所以两全其美的办法是,推理用RNN结构,训练用CNN结构:

总之,这类模型可以非常高效地计算为递归或卷积,在序列长度上具有线性或近线性缩放。
code is cheap, show me the prompt
任务大一统:LLM处理下游任务时,从“预训练-微调-预测”范式,转向灵活的“预训练-提示(prompt)预测”范式。
Prompt:用于指导生成式AI模型执行特定任务的输入指令。
Prompt的基本元素:

Prompt工程:设计和优化prompt。常见的Prompt工程技术包括 上下文学习、思维链等。
上下文学习(In-Context Learning,ICL)是大语言模型一种新的学习范式,它通过构造特定的Prompt,来使得语言模型理解并学习下游任务。相比于传统的监督微调,其不需要更新模型参数,可以快速适应下游任务。
[!NOTE]
上下文学习 通过任务说明,演示示例等信息引导模型输出,快速适应新任务,使语言模型即服务成为可能。
按照示例数量的不同,上下文学习可以分为三类:零样本(Zero-shot)、单样本(One-shot)和少样本(Few-shot)

通常情况下,少样本性能>单样本性能>零样本性能;模型越大,差异越明显。
演示示例选择的两个主要依据是相似性和多样性。
[!TIP]
在心理学中,System-1任务和System-2任务分别代表两种不同的思维方式所处理的任务类型。
System-1(Intuition & instinct):快速、自动且无意识。
System-2(Rational thinking):缓慢,需要集中注意力和耗费精力。
在标准提示下的现象:
思维链 (Chain-of-Thought, CoT) 通过在提示中嵌入一系列中间推理步骤,引导大语言模型模拟人类解决问题时的思考过程,以提升模型处理System2任务的能力。
CoT可以归纳为三种模式:按部就班、三思后行和集思广益

按部就班模式
强调逻辑的连贯性和步骤的顺序性。该模式下,模型像是遵循一条预设的路径,每一步都紧密依赖于前一步的结论。
代表性方法:
三思后行模式
为了解决按部就班模式的不足,三思后行模式在决策过程中融入审慎和灵活性。该模式下,模型在每一步会停下来评估当前的情况,判断是否需要调整方向。
集思广益模式
通过汇集多种不同的观点和方法来优化决策过程。
[!TIP]
三种思维链模式都是作用于模型推理侧,OpenAI尝试在训练和推理时深度融合思维链技术,并提出了GPT-o1。它在回答问题前会花费更多时间来思考,擅长处理科学、编程、数学等领域的复杂问题。
GPT-o1相比GPT-4o,常识(System-1)能力持平,但推理(System-2)能力更胜一筹。但是GPT-o1的推理运行开销显著高于GPT-4o。
GPT-o1没有公开技术细节。我们猜测GPT-o1训练的关键在于从“结果监督”转为“过程监督”。训练时,利用大规模强化学习增强模型生成优质思维片段能力;测试时,利用大规模搜索采样可能的思维片段,并利用奖励模型指导生成。
结合Prompt技术(上面两节所介绍) 和 Prompt技巧,能够引导模型生成更加精准、符合预期的内容,进一步提升大语言模型在实际应用中的表现。
一个标准规范的Prompt通常由任务说明,上下文,问题,输出格式这几个部分中的一个或几个组成。

合理归纳提问。提问的质量直接影响到信息触达的效率和深度。
复杂问题拆解:将问题分解为更小、更易于理解的子问题,并引导模型逐一回答。
追问:深入追问 或 扩展追问 或 反馈追问
善用CoT:
适时使用CoT:思维链能显著增强模型的推理能力,但在决定何时使用CoT时,需要对任务类别、模型规模以及模型能力三方面因素进行考虑。
灵活使用CoT:按部就班、三思后行、集思广益
善用心理暗示:
角色扮演:为大语言模型设定一个详尽的角色
情景带入:示例:想象你在90年代的街头,放学的小朋友们围成一圈,手中玩着弹珠和卡片,欢声笑语中流露出纯真的快乐。小浣熊干脆面给我们带来的快乐有什么?
Prompt长度压缩问题:LLMLingua 是一个经过充分训练的小型语言模型,能迭代地检测并提出Prompt中的非必要Token,最小化损失的同时,实现最高20倍的压缩率。
我们可以通过精心设计的Prompt,激活大语言模型的内在潜力,而不需要对模型进行微调。因此Prompt工程已经在垂域任务、数据增强、智能代理等多个领域发挥出卓越的性能。
例如:Text-to-SQL(如C3)、代码生成(如AlphaCodium)
例如:Self-Instruct、Evol-Instruct
例如:Microsoft Copilot
例如:GPT researcher(单智能体)、HuggingGPT(单智能体),MetaGPT(多智能体 )、斯坦福小镇(多智能体 )

例如:VoxPoser(给定自然语言指令,LLM生成控制机械臂的代码)、Alter3(生成动作代码 + 人类反馈与记忆训练动作)
下游任务适配:预训练模型难以直接适配到下游任务。
比如,经过续写任务预训练的 Qwen2.5-1.5B 倾向于续写或复读一句话。
虽然,利用上下文学习可以让Qwen2.5-1.5B回答得更好,但是上下文学习也存在几点不足:性能有限、人力成本高、推理效率低。
指令微调:为了保证下有任务性能,语言模型需要定制化调整以完成下游任务适配。
比如,经过指令微调的Qwen2.5-1.5B-Instruct就能够听从人类指令并回答问题。


回顾上一章节的Self-Instruct、Evol-Instruct
监督微调(SFT):基于构造的指令数据集,对大模型进行监督微调。对于现有大语言模型,通常以自回归(+Teacher Forcing)方式进行训练。
全量微调需要更新所有模型参数。面临:
为了解决全量微调的问题,参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)避免更新全部参数,在保证微调性能的同时,减少更新的参数数量和计算开销。
PEFT技术主要有三方面优势:计算效率、存储效率以及适应性强
适应性强,是指在大部分任务上性能匹配或超过全量微调基线。这是由于全量微调可能有过拟合的,而PEFT对假设空间做了一定限制,泛化好一点。
主流PEFT方法可分为三类:参数附加方法、参数选择方法、低秩适配方法。

参数附加方法通过增加并训练新的附加参数或模块对大语言模型进行微调。参数附加方法按照附加位置可以分为三类:加在输入、加在模型 以及 加在输出。
加在输入的方法将额外参数附加到模型的输入嵌入(Embedding)中,其中最经典的方法是Prompt-tuning。

这些额外参数通常称之为软提示(Soft Prompt)。 不同于手工编写的硬提示(Hard Prompt),软提示会在训练过程中被动态调整,其本质是可训练的、连续的嵌入。

Prompt-tuning:
加在输入的优势:内存效率高、多任务能力、缩放特性
加在模型的方法将额外的参数或模型添加到预训练模型的隐藏层中,其中经典的方法有Prefix-tuning、Adapter-tuning等。

Prefix-tuning(发表于ACL 2021)和Prompt-tuning(发表于EMNLP 2021)十分类似,但对软提示的处理有所不同:
Prompt-tuning仅将软提示添加到输入嵌入中。
Prefix-tuning将可训练前缀插入到输入嵌入以及注意力模块中。
加在注意力的KV上。
[!TIP]
Prompt-tuning可以理解为改善该怎么描述这个问题、提示。
Prefix-tuning可以理解为也改善该怎么理解这个问题。
但Prefix-tuning从结构上是不美的,因为有点违反transformer模块化的思想,它要拆开KV把参数加到其中。本身是搭积木的过程,成了在积木上雕花。而Adapter-tuning 很好地沿用了堆积木的思想。
Adapter-tuning 通过在预训练语音模型的每个多头注意力层和全连接层后插入新的可学习神经网络模块(称为适配器,Adapter)来实现扩展。


最后仅对适配器、层正则化以及最后的分类层参数进行微调。可以逼近甚至超过全量微调的效果。
加在模型的优势:参数效率高、任务适应性强、保持预训练知识
在微调LLM时,仍会面临以下问题:
Proxy-tuning :为了应对上述问题,代理微调(Proxy-tuning)提供了一种轻量级的解码时(Decoding-time)算法,让我们只需要微调较小的专家模型,且只需要访问大规模语言模型的输出词汇表预测分布,来实现对大规模语言模型的定制化调整。

代理模型$M$:大规模模型或黑箱模型
专家模型$M+$:(微调后的专家模型)和代理模型共词表,如果是同系列更好,比如代理模型是Llama70B,专家模型用Llama1B。
反专家模型$M-$:(微调前的专家模型)和专家模型来比较微调前后的logits差异。用该差异来纠正代理模型的输出。
[!TIP]
核心idea:小模型和大模型之间存在知识的迁移性。所以小模型微调训练前后输出的改变,在大模型很多也是需要对应改变的。
加在输出的优势:非常适合用于微调不动,但推理得动的大规模模型(代理模型)、可用于云边端协同、可用于黑盒模型

参数选择的方法选择模型中的部分参数进行微调。参数选择方法分为两类:基于规则的方法 和 基于学习的方法。

BitFit方法(基于规则):仅优化神经网络中的每一层的偏置项(Biases)以及任务特定的分类头来实现参数高效微调。
BitFit有参数效率、性能表现和过程稳定(相比全量微调允许使用更大学习率)几方面的优势。但该方法仅在小规模模型上进行验证性能,在更大规模模型上的性能表现如何尚且未知。
其他基于规则的方法:微调最后四分之一层、微调具有最小绝对值的模型参数(当前没有发挥重要作用的参数),等等
Child-tuning(基于学习):在模型训练过程中自动地选择可训练的参数子集。其中,最为典型的方法是Child-tuning。该方法通过梯度掩码矩阵策略实现仅对选中的子网络进行梯度更新。
假设$\omega_t$是第$t$轮迭代的参数矩阵,我们引入一个与$\omega_t$同维度的0-1掩码矩阵$M_t$。用于选择第$t$轮迭代的子网络$C_t$,仅更新该子网络的参数。
总结:其实参数选择方法目前用得不多。尤其是在大规模模型上。
优缺点:微调成本低、适应性较好,但选多少、怎么选难抉择。
这是现在应用最为广泛的PEFT方法。
低秩适配方法(Low-rank Adaptation Methods)通过低秩矩阵近似原始权重更新矩阵,并仅微调低秩矩阵,以大幅降低模型参数量。
本征维度假设:在预训练语言模型微调时,仅在一个低维子空间中进行参数更新,也能实现与完整参数更新类似的性能。
本征维度(intrinsic dimension):可以使模型达到预期效果的最小参数子空间的维度。因为数据或任务通常位于一个低维的流形上,模型只需关注与任务相关的特征,而不是整个高维空间。
低秩适配(Low-rank Adaptation,LoRA)将$d \times k$参数更新矩阵低秩分解为一个$r \times k$的矩阵 和 一个$d \times r$的矩阵($r \ll min(d,k)$),冻结原模型参数,仅微调这两个小矩阵。
$r$ 称为秩

不仅微调的参数量大大降低,而且我们发现微调的投影矩阵本身和我们的模型是解耦的,有很好的参数隔离的效果。
效果展示:LoRA使用更少的参数量达到匹配甚至超过全量微调的性能。
权重初始化方式、LoRA的秩、施加位置。






后面提到的QLoRA在权重内存和激活内存上下了功夫,很实用。

插件化,甚至插件组合(DNI)。
许多LoRA变体被提出,从以下几个角度对LoRA进行改进:
(1)性能改进:
将参数更新矩阵参数化为奇异值分解(SVD)的形式,再通过奇异值剪枝来调整不同模块中LoRA的秩。
(2)任务泛化
可以通过组合在不同任务上训练的LoRA模块来提高模型对未见任务的适应性。
LoRAHub提供了一个可用的多LoRA组合的方法框架。
$A=w_1A_1+w_2A_2+...+w_NA_N$,
(3)训练改进
将原始模型权重从16-bit量化为4-bit来存储,只将LoRA参数保持为16-bit。
(4)推理改进
将输入与LLM和LoRA参数的运算分离,两者分别计算完成之后再求和得到最终结果。使得不同LoRA的请求可以放在同一个batch中,从而增加了请求执行的并行度,提升了GPU的利用率。
原先 $h = xW' = x(W+AB)$
S-LoRA是 $h = xW' = x(W+AB) = xW + xAB$
但要求运算是线性的,所以只支持在attention里用,没法在FFN里使用。
继续预训练(continue pre-training)。比如有个ReLoRA技术,通过多次重启,每隔一定训练步数将低秩矩阵与模型权重合并,并重新初始化生成新的低秩矩阵。
也叫life-long learning。LoRA权重可以以插件形式管理与使用,天然具备连续学习的优势,插件化、任务特定、参数隔离的特点。 因而不用担心连续学习出现灾难性遗忘的问题。
预训练大语言模型中可能存在大量混乱信息,包括偏见、毒性和知识错误等问题。

直接输出偏见、嵌入空间偏见(比如在词嵌入空间,man和doctor的距离很小,而woman则和nurse的距离很小)
例如在“越狱模式”下,被问出Windows系统的激活码、序列号。
重新预训练和微调是最朴素的模型更新手段,然而存在一些缺陷。
是否存在更优的解决方案,精准、高效地修正大语言模型中的特定知识点,避免对大模型进行大范围的调整?
有的,模型编辑
模型编辑的定义
模型编辑技术针对特定知识点对模型进行编辑,其旨在修正大语言模型使其输出期望结果,同时不影响其他无关输出。

模型编辑的思想
类比1:模型编辑可看作大语言模型中的“思想钢印”。
类比2:模型学习过程可与人类学习过程相对应。
预训练 —— 体验世界,接触海量知识,形成自身的知识体系
微调 —— 在学校,针对不同学科进行专门的学习,提升在特定领域的能力
模型编辑 —— 在与他人交流中,人类会针对特定的知识进行探讨,从而纠正自己在该知识点上的错误认知
模型编辑的挑战
知识内部紧密关联,“如何精确控制模型编辑的范围”是模型编辑的一个关键挑战。
不可错误编辑(准确性)、不可只顾自己(泛化性,同义的表达)、不可顾此失彼(可迁移性,相关的表达)、不可伤及无辜(局部性),并且需要高效性

[!TIP]
高效性:高效性主要考虑模型编辑的时间成本和资源消耗,直接影响到模型编辑的可行性和实用性。
在实际应用中,模型可能需要频繁地进行更新和纠错,这就要求编辑过程必须快速且资源友好。
2023年12月,网友发现用中文询问“你是谁”这种问题时,谷歌的Gemini Pro会回答“我是百度文心大模型”。 然而仅仅一天之后,Gemini Pro便不再回答类似的内容。
模型编辑的评测
需要构建 泛化性数据集、可迁移性数据集、局部性数据集 用于评估效果。
在评估模型编辑方法时。需要在多个性质之间寻找平衡——六边形战士。
现有一些模型编辑常用的评估数据集。
模型编辑——类比游戏
冒险游戏中的主角需要升级时,可以从内和外两个方面进行改造。
将LLM比作游戏中的主角,那么模型编辑可被看作一种满足“升级”需求的方法,可分为外部拓展法 和 内部修改法。

核心思想是将新知识存储在附加的外部参数或外部知识库中,将其和原始模型一起作为编辑后模型。

如CALINET、T-Patcher
核心思想是通过更新原始模型的内部参数来为模型注入新知识,在不增加物理存储负担的情况下直接优化自身,提高其在特定任务上的表现。
通过“学习如何编辑”来获取元知识,再基于元知识实现模型编辑。



对原始模型的局部参数进行编辑,先定位到需要修改的参数的位置,然后修改关键参数。其定位过程基于对大模型中知识的存储机制的理解。
有点类似目前脑科学家,研究人脑哪一块区域对应哪种功能、感觉。
如,ROME通过因果跟踪实验来定位与编辑知识最相关的一个全连接前馈层。
