Transformers 和语言模型¶
Transformers 用 self-attention 取代了递归,并成为语言理解和生成的主导架构。该文件涵盖 BERT、GPT、T5、位置编码(正弦、RoPE)、预训练目标(MLM、CLM)、微调、prompt 工程和 scaling laws(现代 LLMs 背后的蓝图)。
-
在第06章中,我们介绍了Transformer架构:self-attention、multi-head attention、位置编码和encoder-decoder结构。在这里,我们重点关注 Transformer 如何适应特定的 NLP 范式、定义现代 NLP 的模型(BERT、GPT、T5)以及使其大规模实用的技术。
-
回想一下核心操作:缩放点积 attention 计算 \(\text{softmax}(QK^T / \sqrt{d_k}) V\),其中查询、键和值是输入的线性投影。 多头 attention 运行 \(h\) 并行 attention 头,每个头具有不同的学习投影,并将结果连接起来。 Transformer 块用残差连接、层归一化和位置前馈网络(第 06 章)来包装它。
-
一个微妙但重要的架构选择是层标准化的放置。原始 Transformer 使用 post-norm:残差和归一化位于子层之后,如 \(\text{LayerNorm}(x + \text{Sublayer}(x))\)。
-
大多数现代模型使用 pre-norm:在子层之前进行标准化,如 \(x + \text{Sublayer}(\text{LayerNorm}(x))\)。预规范在训练过程中更加稳定,因为残差连接直接通过恒等路径传递梯度,而不会受到规范化的影响。这使得训练非常深的模型变得更容易,而无需仔细的学习率预热。
-
每个 Transformer 块中的 前馈子层 是独立应用于每个 token 位置的两层 MLP:
-
内部尺寸通常是模型尺寸的 4 倍(例如 \(d_{\text{model}} = 768\)、\(d_{\text{ff}} = 3072\))。这个 FFN 约占每个块中参数的三分之二,被认为充当键值存储器,用于存储在训练期间学到的事实知识。
-
位置编码 提供有关 token 顺序的模型信息,因为 attention 本身是排列等变的。原始的正弦编码(第06章)在不同频率下使用固定的正弦和余弦函数。 学习位置 embeddings 只需为每个位置添加一个可训练向量(在 BERT 和 GPT-2 中使用)。两者都是绝对编码:无论上下文如何,位置 5 都会获得相同的向量。
-
旋转位置嵌入 (RoPE) 通过在 2D 子空间中旋转查询和键向量来编码位置。对于一对尺寸 \((q_{2i}, q_{2i+1})\),旋转角度 \(m\theta_i\)(其中 \(m\) 是位置,\(\theta_i = 10000^{-2i/d}\))适用:
-
RoPE 的优点在于,旋转查询和键之间的点积 \(q'^T k'\) 仅取决于相对位置 \(m - n\),而不是绝对位置。
-
要了解原因,请将旋转写为 \(q' = R_m q\) 和 \(k' = R_n k\),其中 \(R_m\) 是块对角旋转矩阵。 attention 分数变为:
-
最后一步来自旋转组属性:\(R_m^T R_n = R_{n-m}\)(向后旋转 \(m\),然后向前旋转 \(n\) 等于旋转 \(n - m\))。
-
这意味着 attention 分数仅取决于相对距离 \(n - m\),而不是单独的绝对位置 \(m\) 和 \(n\)。
-
该模型无需学习任何位置参数即可获得自然的距离概念,并且可以推广到训练期间未见过的序列长度。
-
ALiBi(具有线性偏差的注意力)采用更简单的方法:它根据距离向 attention 分数添加固定的线性惩罚,如 \(\text{score}_{ij} = q_i^T k_j - m \cdot |i - j|\),其中 \(m\) 是特定于头部的斜率。不同的头使用不同的斜度,使一些头能够局部聚焦,而另一些则全局聚焦。 ALiBi 不需要学习位置参数,并且可以很好地推广到比训练期间看到的序列更长的序列。
-
基于 Transformer 的语言模型的三个主要范式是 encoder-only、decoder-only 和 encoder-decoder。它们的不同之处在于模型可以看到的内容(attention 掩模)以及它们的训练方式。
-
BERT(来自 Transformers 的双向编码器表示,Devlin 等人,2019)是规范的仅 encoder 模型。它使用完全双向 attention 处理文本:每个 token 都可以处理其他所有 token,无论是左还是右。这为 BERT 提供了丰富的上下文表示,但意味着它无法自动生成文本。
-
BERT 经过预训练,有两个目标。 屏蔽语言建模 (MLM) 随机屏蔽 15% 的输入 tokens 并训练模型来预测它们。在选定的 tokens 中,80% 被替换为 [MASK] token,10% 被替换为随机单词,10% 保持不变(以防止模型学习仅在看到 [MASK] 时进行预测)。培训目标是:
- 其中 \(\mathcal{M}\) 是屏蔽位置的集合,\(w_{\backslash \mathcal{M}}\) 是屏蔽了这些位置的句子。这是一个去噪目标:模型学习重建损坏的输入。
-
下一句预测(NSP) 训练 BERT 来预测原始文本中两个句子是否连续。输入开头的特殊 [CLS] token 用于此二元分类。 NSP 的加入是为了帮助完成诸如回答问题之类需要理解句子关系的任务,尽管后来的工作 (RoBERTa) 表明它贡献甚微并且可以被删除。
-
BERT 的预训练表示通过在顶部添加特定于任务的头(简单的线性层)并微调整个模型来适应下游任务。对于分类任务,使用 [CLS] token 表示。对于 token 级任务(NER、POS 标记),使用每个 token 的表示。这种“微调”方法将预训练期间学到的语言知识转移到标记数据相对较少的新任务中。
-
GPT (生成式预训练 Transformer,Radford 等人,2018)是规范的仅 decoder 模型。它使用 因果(自回归)attention:每个 token 只能在较早的位置(及其自身)关注 tokens。这是通过屏蔽 attention 矩阵中的未来位置(在 softmax 之前将它们的分数设置为 \(-\infty\))来强制执行的。训练目标很简单因果语言建模:根据所有先前的 tokens 预测下一个 token。
-
这与文件 02 中的 n-gram 语言模型目标相同,但具有 Transformer 参数化,可以以整个前面的上下文为条件,而不仅仅是最后一个 \(k-1\) tokens。
-
GPT-2 将其扩展到 15 亿个参数,并展示了强大的零样本性能:无需任何微调,它就可以通过自然语言 prompt(“将英语翻译成法语:...”)来执行任务。
-
GPT-3(1750 亿个参数)表明,仅靠规模就可以实现 in-context learning:通过在 prompt 中提供一些输入输出示例,模型可以在没有任何梯度更新的情况下执行新任务。
-
编码器-decoder模型如T5(文本到文本传输Transformer,Raffel等人,2020)将每个NLP任务构建为文本到文本:输入是文本字符串(可能带有“将英语翻译成德语:”等任务前缀),输出是文本字符串。 encoder 使用双向 attention 处理输入,decoder 通过交叉 attention 到 encoder 自回归生成输出。
-
T5 经过 跨度损坏 进行预训练:tokens 的随机连续跨度被替换为哨兵 tokens,并且模型必须生成原始 tokens。例如,“The cat sat on the mat”可能会变成“The [X] on [Y]”作为输入,而目标是“[X] cat sat [Y] the mat”。这是 BERT 的 MLM 到跨度而不是单个 tokens 的概括。
-
BART(Lewis 等人,2020)是另一个以去噪为目标进行预训练的 encoder-decoder 模型,但它应用了更广泛的腐败策略:token 屏蔽、token 删除、跨度屏蔽、句子排列和文档轮换。腐败的多样性迫使模型学习更稳健的表示。
-
随着语言模型变得越来越大,完全微调(更新所有参数)变得不切实际:175B 参数模型仅需要数百 GB 来存储优化器状态。 参数高效微调 (PEFT) 方法仅适应一小部分参数。
-
适配器在现有 Transformer 层之间插入小瓶颈层(通常是两个具有非线性的线性层:向下投影到小尺寸,然后向上投影回来)。仅训练 adapter 权重;原始模型权重被冻结。这增加了不到 5% 的新参数,同时匹配大多数任务的全面微调性能。
-
LoRA(低阶适应)修改权重矩阵本身而不添加新层。 LoRA 不是更新完整的权重矩阵 \(W\),而是学习更新的低阶分解:\(W' = W + BA\),其中 \(B\) 是 \(d \times r\),\(A\) 是 \(r \times d\) 和 \(r \ll d\)(通常是 \(r = 4\) 到 \(r = 64\))。原来的 \(W\) 被冻结;仅 \(A\) 和 \(B\) 接受过培训。在推理时,更新可以合并到原始权重中,没有额外的延迟:
-
前缀调整 将一系列可学习的“虚拟 tokens”添加到每个 attention 层的键和值矩阵中。该模型将这些前缀向量视为真实的 tokens ,并且仅训练前缀参数。这类似于 prompt 调整,但在激活空间而不是 embedding 空间中操作。
-
prompt engineering是设计输入文本的艺术,它可以从预先训练的模型中得出所需的行为,而无需任何参数更新。
-
零样本 prompting 用自然语言描述任务(“对以下评论的情绪进行分类:”)。
-
Few-shot prompting 在实际查询之前提供输入输出示例。
-
思想链 (CoT) prompting 在示例中添加“让我们一步一步思考”或包含推理痕迹,通过指导模型分解问题,显着提高算术和逻辑推理任务的性能。
-
-
上下文学习 (ICL) 是大型语言模型可以从 prompt 中提供的示例中学习执行任务的现象,而无需任何梯度更新。模型的权重不变;它使用示例作为一种隐式规范。
-
ICL 的机械工作原理仍然是一个活跃的研究问题;一个假设是 attention 层在其前向传递中实现了一种梯度下降形式,有效地“训练”上下文中的示例。
-
缩放定律描述了模型大小、数据大小、计算预算和性能(通过损失衡量)之间的可预测关系。卡普兰等人。 (2020)发现每个变量的损失都遵循幂律:
- 其中 \(N\) 是参数数量,\(D\) 是数据集大小,\(C\) 是计算预算。这些幂律适用于许多数量级,并且表明简单地扩大规模就会产生可预测的改进。
- Chinchilla scaling laws(Hoffmann 等人,2022)通过表明大多数大型模型都缺乏训练来修正这一点。对于固定的计算预算 \(C\),最佳分配同等地缩放模型大小和训练数据:
-
这意味着,如果您的计算预算加倍,则应该将模型大小和数据集大小增加 \(\sqrt{2}\) 倍,而不仅仅是使模型更大。
-
卡普兰等人。建议比 \(D\) 更快地扩展 \(N\),这导致模型非常大但训练不足。 Chinchilla(70B 参数,1.4T tokens)在相同的计算预算下与 Gopher(280B 参数,300B tokens)的性能相匹配,这表明早期模型严重缺乏数据。
-
实用的经验法则:每个参数训练大约 20 个 tokens。
-
Mixture of Experts (MoE) 是一种无需按比例缩放计算即可缩放模型容量的架构。 MoE 使用多个 专家 FFN 层和一个 门控网络(路由器),而不是一个大型前馈层,该网络选择为每个 token 激活哪些专家。
-
门控函数计算每个专家的路由分数并选择顶部的 \(k\) (通常为 \(k = 1\) 或 \(k = 2\)):
- 只有选定的专家处理 token,因此计算成本与 \(k\)(活跃专家数量)成比例,而不是与专家总数 \(E\) 成比例。具有 8 位专家和 top-2 路由的模型的参数是密集模型的 4 倍,但计算量仅为 2 倍。
- MoE 中的一个关键挑战是负载平衡:如果路由器将大部分 tokens 发送给少数受欢迎的专家,那么其他的就会被浪费。训练增加了辅助负载平衡损失,鼓励统一的专家利用:
-
其中 \(f_i\) 是分配给专家 \(i\) 的 tokens 的分数,\(p_i\) 是专家 \(i\) 的平均路由概率。当 token 分数和概率均等(均等于 \(1/E\))时,该乘积最小。
-
专家并行性将不同的专家分布在不同的加速器上。在前向传递期间,全对全通信步骤将 tokens 路由到托管其指定专家的设备,然后将结果路由回。这种通信成本是大规模 MoE 的主要工程挑战。 Switch Transformer、Mixtral 和 GShard 等模型使用 MoE 以实际的推理成本实现强大的性能。
-
建立模型只是工作的一半;衡量它们是否有效是另一半。 NLP 评估特别困难,因为语言是模糊的、主观的和开放式的。
-
翻译可以在很多方面都是正确的。即使摘要与参考文献没有完全相同的文字,也可以是很好的摘要。
-
聊天机器人的反应可能是有帮助的、无害的、诚实的,但理性的人类可能会不同意。
-
精确匹配 (EM) 是最简单的指标:模型的输出是否与黄金答案完全匹配?它用于具有简短、明确答案的任务,例如提取式问答 (SQuAD) 或封闭式数学。
-
EM 很严酷;除非应用标准化,否则“New York City”和“new york city”将无法匹配,但其简单性使其明确无误。
-
标记级指标将 NLP 视为 token 级别的分类问题,使用第 06 章中的精度、召回率和 F1。
-
精度衡量模型预测的 tokens 正确的比例:\(P = \text{TP} / (\text{TP} + \text{FP})\)。预测很少实体但能正确预测实体的模型具有很高的精度。
-
回忆测量模型发现的黄金 tokens 的比例:\(R = \text{TP} / (\text{TP} + \text{FN})\)。将每个 token 作为实体进行预测的模型具有完美的召回率,但精度却很差。
-
F1 是精度和召回率的调和平均值:
-
调和平均值(而不是算术平均值)会惩罚不平衡:如果 \(P\) 或 \(R\) 较低,则 \(F_1\) 也较低。对于 NER(文件 02),F1 是按实体类型计算的,然后跨类型进行宏观平均。对于词性标记,token 级准确度更为常见,因为每个 token 都会获得一个标记。
-
跨度级别 F1(在 SQuAD 中使用)将预测跨度中的 tokens 集合与黄金跨度中的集合进行比较。这比精确匹配更宽容:如果黄金答案是“埃菲尔铁塔”并且模型预测“埃菲尔铁塔”,则跨度 F1 很高(5 个中 4 个重叠 tokens),即使 EM 为零。
-
BLEU(双语评估研究,Papineni 等人,2002 年)是机器翻译的经典指标。它测量候选翻译和一个或多个参考翻译之间的 n 元语法重叠。该分数将多个 n 元语法级别(一元语法到 4 元语法)的精度与简洁性惩罚相结合:
-
其中 \(p_n\) 是修改的 n-gram 精度:候选中每个 n-gram 的计数被剪切到任何引用中的最大计数,从而防止像“the the the the”这样的退化候选得分高。权重 \(w_n\) 通常是均匀的(\(w_n = 1/N\),带有 \(N = 4\))。
-
简洁惩罚 \(\text{BP} = \min(1, \exp(1 - r/c))\) 惩罚短于参考的候选内容(\(c\) 是候选长度,\(r\) 是参考长度)。如果没有这个,模型可以通过输出很少、非常安全的单词来实现高精度。
-
BLEU 在语料库级别(对许多句子进行平均)与人类判断具有合理的相关性,但在句子级别的相关性较差。
-
它奖励精确的 n 元语法匹配,并错过有效的释义:“the cat is on the mat”和“a feline sits atop the rug”尽管含义相同,但二元语法重叠为零。
-
BLEU 还完全忽略了召回率——仅产生最常见单词的候选者在精确度上得分很高。
-
ROUGE(面向回忆的 Gisting 评估的研究,Lin,2004 年)是摘要的标准度量。与强调精确度的 BLEU 不同,ROUGE 强调召回率:候选中出现的参考 n 元语法的比例是多少?
-
ROUGE-N 计算 n 元语法的召回率:\(\text{ROUGE-N} = \frac{|\text{n-grams}_{\text{ref}} \cap \text{n-grams}_{\text{cand}}|}{|\text{n-grams}_{\text{ref}}|}\)。 ROUGE-1(一元组)和 ROUGE-2(二元组)最常见。
-
ROUGE-L 在候选和参考之间使用最长公共子序列 (LCS),它可以捕获句子级别的词序,而不需要连续匹配。
-
通过参考长度归一化的 LCS 长度提供召回率,通过候选长度归一化提供精度,F 度量将它们结合起来。
-
LCS 通过动态规划在 \(O(mn)\) 时间内计算(类似于文件 02 的编辑距离):
-
其中 \(m\) 和 \(n\) 是参考和候选的长度,\(\beta\) 通常设置为有利于召回(\(\beta \to \infty\) 提供纯粹的召回)。
-
METEOR(使用显式排序进行翻译评估的指标,Banerjee 和 Lavie,2005 年)通过合并同义词、词干和词序来解决 BLEU 的弱点。
-
它首先使用精确匹配、词干匹配(通过文件 02 中的 Porter 词干)和同义词匹配(通过文件 01 中的 WordNet)来对齐候选词和参考词之间的单词。
-
然后,它计算一元语法精度和针对召回率加权的召回率的调和平均值,并应用碎片惩罚,对匹配单词以与参考单词不同的顺序出现的候选者进行惩罚。
-
ChrF(字符 n 元语法 F 分数)计算字符 n 元语法而不是单词 n 元语法的 F 分数。这使得它对形态变化具有鲁棒性(对于文件 01 中的粘着语言至关重要),并部分处理标记化差异。 ChrF++ 将单词二元组添加到字符 n 元组中。
-
它已与 BLEU 一起成为机器翻译的推荐指标,特别是对于形态丰富的语言。
-
困惑度(文件 02)衡量语言模型预测保留测试集的效果。它是语言模型的标准内在度量:\(\text{PPL} = \exp(-\frac{1}{N} \sum_{i} \log P(w_i \mid w_{<i}))\)。越低越好。
-
困惑度仅在使用相同标记化的模型之间具有可比性,因为不同的标记化器为相同文本生成不同的序列长度 \(N\) 。
-
词汇量较大的模型往往每个 token 的困惑度较低,但每个句子处理的 tokens 较少。
-
每字节位数 (BPB) 按文本中的 UTF-8 字节数而不是 tokens 数进行标准化,使其独立于标记化:
- BERTScore(Zhang 等人,2020)通过计算 embedding 空间中的相似性,超越了表面级 n-gram 匹配。使用上下文 embeddings 的余弦相似度(通常来自预训练的 BERT 模型),将候选中的每个 token 与参考中最相似的 token 进行匹配。分数汇总为精确率、召回率和 F1:
-
其中 \(r_i\) 和 \(c_j\) 是参考和候选 tokens 的上下文 embeddings。这捕获了 n-gram 指标遗漏的语义相似性:“automobile”和“car”得分很高,因为它们的 BERT embeddings 相似,即使它们没有共享字符。
-
BLEURT(Sellam 等人,2020)通过直接根据人类质量判断微调 BERT 模型进一步推进了这一点。给定参考和候选对,它输出标量质量分数。 BLEURT 在合成数据(通过 BLEU 和 METEOR 等指标评级的参考翻译的随机扰动)上进行训练,然后根据人类评级进行微调。与任何表面指标相比,它与人类判断的相关性更好。
-
COMET(翻译评估的跨语言优化指标,Rei 等人,2020)是一种机器翻译学习指标,以源句子、参考文献和候选语句为条件,而不仅仅是参考文献和候选语句。它使用多语言 encoder (XLM-R) 嵌入所有三个并预测质量分数。通过查看来源,COMET 可以检测仅参考指标遗漏的含义错误(例如,流畅但实际上错误的翻译)。
-
LLM-as-judge 是大规模评估的现代方法。系统不会根据引用计算指标,而是会提示使用强大的语言模型(GPT-4,Claude)来评估模型输出的质量。法官接收输入、模型的响应以及可选的参考答案,并产生评级(例如 1-5)或成对偏好(响应 A 优于响应 B)。
-
成对比较(用于 Chatbot Arena)是最可靠的 LLM-as-judge 格式。法官看到两个答案并选择更好的一个,而不是分配绝对分数。这避免了校准问题(不同的法官可能对“五分之三”有不同的基线)。结果被汇总到 Elo 评级(来自国际象棋),其中每个模型都从基本评级开始,并根据与其他模型的胜负来获得或失去分数。模型 \(A\) 相对于模型 \(B\) 的预期获胜概率为:
-
其中 \(R_A, R_B\) 是 Elo 评级。每次比较后,评级都会更新:\(R_A' = R_A + K(S - P(A \succ B))\),其中 \(S \in \{0, 1\}\) 是实际结果,\(K\) 控制更新幅度。持续击败强敌的车型迅速崛起;输给弱对手的模型就会下降。
-
立场偏见是 LLM 法官的一个已知问题:他们倾向于更喜欢首先提出的答案(或者在某些模型中,第二个提出的答案)。 交换(对每对进行两次评估,并按两个顺序进行响应)并对结果求平均值可以缓解这种情况。
-
冗长偏见是另一个原因:法官往往更喜欢更长、更详细的回答,即使简洁的答案更好。
-
自我一致性检查法官是否对同一输入的多次评估给出相同的评分。高方差表明评估信号有噪声。
-
注释者间一致性(Cohen 的 kappa 或 Krippendorff 的 alpha)衡量多个法官是否同意,从而提供评估可靠性的上限。
-
污染是一个关键问题:如果评估数据出现在模型的训练集中,基准分数就会被夸大并且毫无意义。
-
这对于接受网络抓取数据训练的 LLMs 来说尤其成问题,其中可能存在流行的基准。缓解策略包括:使用未公开发布的保留测试集、创建定期重新生成问题的动态基准、金丝雀字符串(嵌入基准数据中以检测泄漏的唯一标识符)以及比较受污染子集与干净子集的性能。
-
标准 NLU 基准 评估跨不同任务的语言理解。
-
GLUE(通用语言理解评估)和 SuperGLUE 是多任务基准,涵盖情感 (SST-2)、文本相似性 (STS-B)、自然语言推理(MNLI、RTE)、共指 (WSC) 和问答 (BoolQ)。
-
模型针对每项任务分别进行评估,并通过聚合指标进行评分。 GLUE 现在被认为已经饱和(模型在大多数任务上都超过了人类的表现); SuperGLUE 仍然更具挑战性。
-
MMLU(大规模多任务语言理解)使用多项选择题评估 57 个学科(数学、历史、法律、医学、计算机科学等)的知识和推理。
-
它测试模型在预训练期间是否吸收了广泛的知识。报告每个科目的分数并作为宏观平均值。
-
MMLU-Pro 添加了更难的多步骤推理问题,有 10 个答案选择,而不是 4 个。
-
HellaSwag 通过要求模型选择最合理的场景延续来测试常识推理。错误的答案是敌对地生成的(使用模型),表面上看似合理,但语义上是错误的。
-
WinoGrande 使用相差一个词的最少对来测试常识共指解析。
-
ARC(AI2 推理挑战)在简单和挑战集中使用小学科学问题,测试事实和推理能力。
-
推理和数学基准评估区分强 LLMs 和弱者的解决问题能力。
-
GSM8K(小学数学 8K)包含 8,500 个需要多步算术推理的初等数学应用题。它是基本数学推理和评估思想链 prompting(文件 04)的标准基准。
-
MATH 是一个更难的竞赛级数学问题数据集,涉及代数、数论、几何、计数和概率。问题需要多步骤符号推理,而 MATH-500 是常见的包含 500 个问题的子集。
-
AIME(美国数学邀请赛)问题属于竞赛级别:正确解决这些问题需要通过多个步骤进行深入的数学推理。 DeepSeek-R1 在 AIME 2024 上得分为 79.8%,证明 RL 训练的推理模型(文件 05)可以接近强大的人类竞争对手。
-
HumanEval 和 MBPP(主要是基本编程问题)通过检查模型的代码是否通过单元测试来评估代码生成。 HumanEval 包含 164 个带有函数签名和文档字符串的 Python 问题;模型必须生成函数体。
-
指标为 pass@k:至少 \(k\) 生成的解决方案之一通过所有测试的概率。对于单个样本:
-
其中 \(n\) 是生成的样本总数,\(c\) 是通过的样本数。此公式纠正了简单地选取 \(k\) 样本中最好的样本时出现的偏差。
-
SWE-bench 更进一步,评估模型是否可以通过修改现有代码库来解决真正的 GitHub 问题——这是对实际软件工程能力的更难测试。
-
GPQA(研究生级 Google 验证 QA)包含生物学、物理和化学领域的专家级问题,即使对于领域专家来说也很难。它测试模型是否具有真正的理解而不是模式匹配。 “钻石”子集是最难的。
-
安全和一致性基准评估模型是否有用、无害和诚实。
-
TruthfulQA 测试模型是否重现常见的误解。问题的设计使得最常见的互联网答案都是错误的(例如,“如果你吞下口香糖会发生什么?”,常见的误区是它会保留 7 年,但真实的答案是它会正常通过)。记住流行但不正确的主张的模型得分很低。
-
BBQ(QA 偏见基准)测试跨年龄、性别、种族和宗教等类别的社会偏见。问题是结构化的,以便有偏见的模型会系统地选择刻板的答案。 Toxigen 评估模型生成有关特定人口群体的有毒内容的趋势。
-
MT-Bench 使用 80 个精心设计的问题来评估多回合对话能力,涉及写作、角色扮演、推理、数学、编码、提取、STEM 和人文学科。 LLM 法官 (GPT-4) 按 1-10 分等级对回答进行评分。多回合格式测试模型是否可以跟进、维护上下文并处理澄清请求。
-
Chatbot Arena (LMSYS) 使用真实用户在匿名模型之间进行盲目的成对比较。用户提交 prompts 并投票选出更好的响应,而不知道是哪个模型产生的。由此产生的 Elo 排行榜被认为是对通用 LLM 质量最生态有效的评估,因为它反映了用户对多样化、未经策划的 prompts 的真实偏好。
-
AlpacaEval 通过在一组固定指令上将模型输出与参考模型 (GPT-4) 进行比较,自动进行成对评估。判断模型决定胜率。
-
AlpacaEval 2.0 使用长度控制的获胜率来纠正冗长偏差。
-
特定任务的评估需要针对专门领域量身定制的指标。
-
语音识别的单词错误率 (WER):\(\text{WER} = (S + D + I) / N\),其中 \(S\)、\(D\)、\(I\) 是替换、删除和插入错误,\(N\) 是参考单词的数量。这是按参考长度标准化的编辑距离(文件 02),应用于单词级别。
-
用于面向任务的对话系统的槽位 F1 衡量模型是否正确从用户话语中提取结构化信息(例如,从“为我明天预订飞往巴黎的航班”中提取“目的地:巴黎”和“日期:明天”)。
-
RAG 系统(文件 05)的引用准确性 检查模型生成的引用是否真正支持所提出的主张。根据检索到的段落验证声明,并且该指标计算完全、部分或不支持的声明的比例。
-
评估陷阱很常见,并且可能会使整个基准比较无效。
-
应试教学:优化基准性能而不是真正的能力。在 MMLU 式多项选择上进行微调的模型将在 MMLU 上得分较高,但在以开放式格式提出的相同问题上可能会失败。
-
度量游戏:可以优化模型以产生在自动度量上得分良好的输出(高 BLEU、低困惑度),但并不是真正的好。 BLEU 最佳翻译通常是一种安全、通用的释义,而不是自然、流畅的释义。
-
基准饱和:当模型在基准上接近或超过人类表现时,基准就不再提供信息。 GLUE、SQuAD 1.1 和其他几个版本现已饱和。
-
该领域不断创造更难的基准,但创建、饱和和替换的循环使得纵向比较变得困难。
-
人工评估仍然是黄金标准,但成本昂贵、缓慢且难以重现。不同的注释者池(众包工作者与领域专家、不同的文化、不同的语言)会产生不同的判断。报告注释者间协议和注释者人口统计数据对于可重复性至关重要。
编码任务(使用 CoLab 或笔记本)¶
-
从头开始实现完整的 Transformer encoder 块(multi-head attention、前馈、残差连接、层范数)。将其应用于简单的序列分类任务。
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def layer_norm(x, gamma, beta, eps=1e-5): mean = x.mean(axis=-1, keepdims=True) var = x.var(axis=-1, keepdims=True) return gamma * (x - mean) / jnp.sqrt(var + eps) + beta def multi_head_attention(Q, K, V, W_q, W_k, W_v, W_o, n_heads): B, T, D = Q.shape head_dim = D // n_heads q = Q @ W_q # (B, T, D) k = K @ W_k v = V @ W_v # Reshape to (B, n_heads, T, head_dim) q = q.reshape(B, T, n_heads, head_dim).transpose(0, 2, 1, 3) k = k.reshape(B, T, n_heads, head_dim).transpose(0, 2, 1, 3) v = v.reshape(B, T, n_heads, head_dim).transpose(0, 2, 1, 3) scores = q @ k.transpose(0, 1, 3, 2) / jnp.sqrt(head_dim) weights = jax.nn.softmax(scores, axis=-1) out = (weights @ v).transpose(0, 2, 1, 3).reshape(B, T, D) return out @ W_o, weights def transformer_block(x, params): # Pre-norm multi-head self-attention normed = layer_norm(x, params['ln1_g'], params['ln1_b']) attn_out, weights = multi_head_attention( normed, normed, normed, params['W_q'], params['W_k'], params['W_v'], params['W_o'], n_heads=4 ) x = x + attn_out # Pre-norm feed-forward normed = layer_norm(x, params['ln2_g'], params['ln2_b']) ff = jax.nn.gelu(normed @ params['W1'] + params['b1']) ff = ff @ params['W2'] + params['b2'] x = x + ff return x, weights # Initialise parameters d_model, d_ff, n_heads = 32, 128, 4 key = jax.random.PRNGKey(42) keys = jax.random.split(key, 10) params = { 'W_q': jax.random.normal(keys[0], (d_model, d_model)) * 0.05, 'W_k': jax.random.normal(keys[1], (d_model, d_model)) * 0.05, 'W_v': jax.random.normal(keys[2], (d_model, d_model)) * 0.05, 'W_o': jax.random.normal(keys[3], (d_model, d_model)) * 0.05, 'ln1_g': jnp.ones(d_model), 'ln1_b': jnp.zeros(d_model), 'ln2_g': jnp.ones(d_model), 'ln2_b': jnp.zeros(d_model), 'W1': jax.random.normal(keys[4], (d_model, d_ff)) * 0.05, 'b1': jnp.zeros(d_ff), 'W2': jax.random.normal(keys[5], (d_ff, d_model)) * 0.05, 'b2': jnp.zeros(d_model), } # Test with random input x = jax.random.normal(keys[6], (2, 8, d_model)) # batch=2, seq_len=8 out, attn_weights = transformer_block(x, params) print(f"Input shape: {x.shape}") print(f"Output shape: {out.shape}") print(f"Attention weights shape: {attn_weights.shape}") # (B, n_heads, T, T) # Visualise attention patterns for each head fig, axes = plt.subplots(1, 4, figsize=(16, 3.5)) for h in range(4): im = axes[h].imshow(attn_weights[0, h], cmap='Blues', vmin=0) axes[h].set_title(f"Head {h}") axes[h].set_xlabel("Key pos"); axes[h].set_ylabel("Query pos") plt.suptitle("Multi-Head Attention Patterns") plt.tight_layout(); plt.show() -
实现因果(自回归)attention 屏蔽并将其与双向 attention 进行比较。展示掩码如何阻止信息从未来流向过去 tokens。
import jax import jax.numpy as jnp import matplotlib.pyplot as plt def attention(Q, K, V, mask=None): d_k = Q.shape[-1] scores = Q @ K.T / jnp.sqrt(d_k) if mask is not None: scores = jnp.where(mask, scores, -1e9) weights = jax.nn.softmax(scores, axis=-1) return weights @ V, weights seq_len, d_model = 6, 8 key = jax.random.PRNGKey(0) k1, k2, k3 = jax.random.split(key, 3) Q = jax.random.normal(k1, (seq_len, d_model)) K = jax.random.normal(k2, (seq_len, d_model)) V = jax.random.normal(k3, (seq_len, d_model)) # Bidirectional (encoder-style): all positions visible bidir_mask = jnp.ones((seq_len, seq_len), dtype=bool) bidir_out, bidir_weights = attention(Q, K, V, bidir_mask) # Causal (decoder-style): only past and current positions visible causal_mask = jnp.tril(jnp.ones((seq_len, seq_len), dtype=bool)) causal_out, causal_weights = attention(Q, K, V, causal_mask) fig, axes = plt.subplots(1, 3, figsize=(14, 4)) tokens = [f"t{i}" for i in range(seq_len)] axes[0].imshow(bidir_weights, cmap='Blues', vmin=0, vmax=0.5) axes[0].set_title("Bidirectional Attention\n(BERT-style)") axes[0].set_xticks(range(seq_len)); axes[0].set_xticklabels(tokens) axes[0].set_yticks(range(seq_len)); axes[0].set_yticklabels(tokens) axes[1].imshow(causal_mask.astype(float), cmap='Greys', vmin=0, vmax=1) axes[1].set_title("Causal Mask\n(1 = allowed, 0 = blocked)") axes[1].set_xticks(range(seq_len)); axes[1].set_xticklabels(tokens) axes[1].set_yticks(range(seq_len)); axes[1].set_yticklabels(tokens) axes[2].imshow(causal_weights, cmap='Blues', vmin=0, vmax=0.5) axes[2].set_title("Causal Attention\n(GPT-style)") axes[2].set_xticks(range(seq_len)); axes[2].set_xticklabels(tokens) axes[2].set_yticks(range(seq_len)); axes[2].set_yticklabels(tokens) for ax in axes: ax.set_xlabel("Key"); ax.set_ylabel("Query") plt.tight_layout(); plt.show() # Verify: in causal attention, output at position i depends only on positions <= i print("Causal attention weight at position 2 (should only attend to 0, 1, 2):") print(f" Weights: {causal_weights[2]}") print(f" Sum of future weights (should be ~0): {causal_weights[2, 3:].sum():.6f}") -
实现 LoRA (低阶适应)并展示它如何使用比完全微调少得多的可训练参数来修改权重矩阵。
import jax import jax.numpy as jnp d_model = 256 rank = 4 # LoRA rank (much smaller than d_model) key = jax.random.PRNGKey(42) k1, k2, k3 = jax.random.split(key, 3) # Original frozen weight matrix W_frozen = jax.random.normal(k1, (d_model, d_model)) * 0.02 # LoRA matrices (only these are trainable) B = jnp.zeros((d_model, rank)) # initialised to zero A = jax.random.normal(k2, (rank, d_model)) * 0.01 # random init # Forward pass: W_effective = W_frozen + B @ A x = jax.random.normal(k3, (8, d_model)) # Without LoRA y_original = x @ W_frozen.T # With LoRA W_effective = W_frozen + B @ A y_lora = x @ W_effective.T # Parameter counts full_params = d_model * d_model lora_params = d_model * rank + rank * d_model # B + A print(f"Model dimension: {d_model}") print(f"LoRA rank: {rank}") print(f"Full fine-tuning parameters: {full_params:,}") print(f"LoRA parameters: {lora_params:,}") print(f"Parameter reduction: {full_params / lora_params:.1f}x") print(f"\nSince B is initialised to zeros, initial LoRA output matches original:") print(f" Max difference: {jnp.abs(y_original - y_lora).max():.2e}") # Simulate training: only update A and B def lora_forward(A, B, W_frozen, x): return x @ (W_frozen + B @ A).T def dummy_loss(A, B, W_frozen, x, target): pred = lora_forward(A, B, W_frozen, x) return jnp.mean((pred - target) ** 2) # Target: some transformation of x target = x @ jax.random.normal(jax.random.PRNGKey(99), (d_model, d_model)).T * 0.02 grad_fn = jax.jit(jax.grad(dummy_loss, argnums=(0, 1))) lr = 0.01 for step in range(200): gA, gB = grad_fn(A, B, W_frozen, x, target) A = A - lr * gA B = B - lr * gB loss_before = dummy_loss(jnp.zeros_like(A), jnp.zeros_like(B), W_frozen, x, target) loss_after = dummy_loss(A, B, W_frozen, x, target) print(f"\nLoss before LoRA: {loss_before:.6f}") print(f"Loss after LoRA: {loss_after:.6f}") print(f"Effective weight change rank: {jnp.linalg.matrix_rank(B @ A)}")