Skip to content

Transformer 与语言模型

Transformer 用自注意力替代了循环,成为语言理解和生成的主导架构。本文件涵盖 BERT、GPT、T5、位置编码(正弦、RoPE)、预训练目标(MLM、CLM)、微调、提示工程和缩放定律——这是现代大语言模型背后的蓝图。

  • 在第 06 章中,我们介绍了 Transformer 架构:自注意力、多头注意力、位置编码和编码器-解码器结构。这里我们将重点讨论 Transformer 如何适配特定的 NLP 范式、定义了现代 NLP 的模型(BERT、GPT、T5),以及使它们在大规模下实用的技术。

  • 回顾核心操作:缩放点积注意力计算 \(\text{softmax}(QK^T / \sqrt{d_k}) V\),其中查询、键、值是输入的线性投影。多头注意力运行 \(h\) 个并行的注意力头,每个头使用不同的学习到的投影,并将结果拼接起来。Transformer 块通过残差连接、层归一化和逐位置前馈网络封装这些操作(第 06 章)。

  • 一个微妙但重要的架构选择是层归一化的位置。原始 Transformer 使用后归一化:残差和归一化放在子层之后,即 \(\text{LayerNorm}(x + \text{Sublayer}(x))\)

  • 大多数现代模型使用前归一化:在子层之前进行归一化,即 \(x + \text{Sublayer}(\text{LayerNorm}(x))\)。前归一化在训练中更加稳定,因为残差连接通过恒等路径直接传递梯度,不受归一化影响。这使得训练非常深的模型变得更容易,无需仔细的学习率预热。

  • 每个 Transformer 块中的前馈子层是一个两层的 MLP,独立应用于每个标记位置:

\[\text{FFN}(x) = W_2 \cdot \text{GELU}(W_1 x + b_1) + b_2\]
  • 内部维度通常是模型维度的 4 倍(例如 \(d_{\text{model}} = 768\), \(d_{\text{ff}} = 3072\))。这个 FFN 约占每个块参数的三分之二,被认为起到键值记忆的作用,存储训练期间学到的事实知识。

  • 位置编码为模型提供关于标记顺序的信息,因为注意力本身是置换等变的。原始的正弦编码(第 06 章)在不同频率上使用固定的正弦和余弦函数。可学习的位置嵌入简单地添加每个位置的可训练向量(用于 BERT 和 GPT-2)。两者都是绝对编码:位置 5 的向量与上下文无关。

  • 旋转位置嵌入(RoPE)通过在二维子空间中旋转查询和键向量来编码位置。对于一对维度 \((q_{2i}, q_{2i+1})\),通过角度 \(m\theta_i\)(其中 \(m\) 是位置,\(\theta_i = 10000^{-2i/d}\))进行旋转:

\[ \begin{bmatrix} q'_{2i} \\ q'_{2i+1} \end{bmatrix} = \begin{bmatrix} \cos m\theta_i & -\sin m\theta_i \\ \sin m\theta_i & \cos m\theta_i \end{bmatrix} \begin{bmatrix} q_{2i} \\ q_{2i+1} \end{bmatrix} \]

RoPE:每个位置在二维子空间中将查询和键向量旋转不同角度,使得注意力分数仅依赖于相对位置

  • RoPE 的妙处在于,旋转后的查询和键的点积 \(q'^T k'\) 仅取决于相对位置 \(m - n\),而不是绝对位置。

  • 为了理解原因,将旋转写为 \(q' = R_m q\)\(k' = R_n k\),其中 \(R_m\) 是块对角旋转矩阵。注意力分数变为:

\[q'^T k' = (R_m q)^T (R_n k) = q^T R_m^T R_n \, k = q^T R_{n-m} \, k\]
  • 最后一步因旋转群性质成立:\(R_m^T R_n = R_{n-m}\)(先反向旋转 \(m\),再正向旋转 \(n\) 等价于旋转 \(n-m\))。

  • 这意味着注意力分数仅取决于相对距离 \(n-m\),而不是绝对位置 \(m\)\(n\)

  • 模型获得了自然的距离概念,无需任何可学习的位置参数,并且可以泛化到训练期间未见过的序列长度。

  • ALiBi(基于线性偏置的注意力)采用更简单的方法:它根据距离向注意力分数添加固定的线性惩罚,即 \(\text{score}_{ij} = q_i^T k_j - m \cdot |i - j|\),其中 \(m\) 是头部特定的斜率。不同的头部使用不同的斜率,允许一些头部聚焦局部,另一些关注全局。ALiBi 不需要学习位置参数,并且能够很好地泛化到比训练时更长的序列。

  • 基于 Transformer 的语言模型的三种主导范式是仅编码器仅解码器编码器-解码器。它们的不同之处在于模型能看到什么(注意力掩码)以及如何训练。

三种 Transformer 范式:仅编码器(BERT)使用双向注意力用于分类,仅解码器(GPT)使用因果注意力用于生成,编码器-解码器(T5)结合两者用于序列到序列任务

  • BERT(来自 Transformer 的双向编码器表示,Devlin 等人,2019)是经典的仅编码器模型。它使用完全双向注意力处理文本:每个标记可以关注所有其他标记,包括左边和右边。这为 BERT 提供了丰富的上下文表示,但意味着它不能自回归地生成文本。

  • BERT 使用两个目标进行预训练。掩码语言建模随机掩码 15% 的输入标记,并训练模型预测它们。在被选中的标记中,80% 被替换为 [MASK] 标记,10% 被替换为随机词,10% 保持不变(以防止模型只在看到 [MASK] 时才学习预测)。训练目标为:

\[\mathcal{L}_{\text{MLM}} = -\sum_{i \in \mathcal{M}} \log P(w_i \mid w_{\backslash \mathcal{M}})\]
  • 其中 \(\mathcal{M}\) 是被掩码的位置集合,\(w_{\backslash \mathcal{M}}\) 是这些位置被掩码后的句子。这是一个去噪目标:模型学习重建损坏的输入。

BERT 掩码语言建模:输入中 15% 的标记被掩码,双向 Transformer 预测掩码位置的原始标记

  • 下一句预测训练 BERT 预测两个句子在原始文本中是否连续。输入开头的特殊 [CLS] 标记用于这个二分类任务。加入 NSP 是为了帮助像问答这样需要理解句子关系的任务,尽管后续工作(RoBERTa)表明它的贡献很小,可以丢弃。

  • BERT 的预训练表示通过添加任务特定的头(一个简单的线性层)并微调整个模型来适应下游任务。对于分类任务,使用 [CLS] 标记的表示。对于标记级任务(NER、词性标注),使用每个标记的表示。这种微调方法将预训练期间学到的语言知识转移到新任务中,只需要相对较少的标注数据。

  • GPT(生成式预训练 Transformer,Radford 等人,2018)是经典的仅解码器模型。它使用因果(自回归)注意力:每个标记只能关注更早位置的标记(以及自身)。这通过在注意力矩阵中掩码未来位置(在 softmax 之前将其分数设为 \(-\infty\))来实现。训练目标是简单的因果语言建模:给定所有之前的标记,预测下一个标记。

\[\mathcal{L}_{\text{CLM}} = -\sum_{i=1}^{n} \log P(w_i \mid w_1, \ldots, w_{i-1})\]
  • 这与文件 02 中的 n-元语言模型目标相同,但使用了 Transformer 参数化,可以基于整个前文(而不仅仅是最后 \(k-1\) 个标记)进行条件化。

  • GPT-2 将其扩展到 15 亿参数,并展示了强大的零样本性能:无需任何微调,它可以通过使用自然语言提示(“将英语翻译成法语:...”)来执行任务。

  • GPT-3(1750 亿参数)表明,仅靠规模就能实现上下文学习:通过在提示中提供几个输入-输出示例,模型可以在没有任何梯度更新的情况下执行新任务。

  • 编码器-解码器模型T5(文本到文本迁移 Transformer,Raffel 等人,2020)将每个 NLP 任务框架化为文本到文本:输入是一个文本字符串(可能带有任务前缀,如“将英语翻译成德语:”),输出是一个文本字符串。编码器使用双向注意力处理输入,解码器自回归地生成输出,并与编码器进行交叉注意力。

  • T5 使用跨度损坏进行预训练:随机的连续标记跨度被替换为哨兵标记,模型必须生成原始标记。例如,“The cat sat on the mat” 可能变成输入“The [X] on [Y]”,目标是“[X] cat sat [Y] the mat”。这是 BERT 的 MLM 从单个标记到跨度的推广。

  • BART(Lewis 等人,2020)是另一个使用去噪目标预训练的编码器-解码器模型,但它应用了更广泛的损坏策略:标记掩码、标记删除、跨度掩码、句子排列和文档旋转。多样化的损坏迫使模型学习更鲁棒的表示。

  • 随着语言模型变得越来越大,全量微调(更新所有参数)变得不切实际:一个 1750 亿参数的模型仅存储优化器状态就需要数百 GB。参数高效微调方法只适配一小部分参数。

  • 适配器在现有的 Transformer 层之间插入小的瓶颈层(通常是两层线性层加一个非线性:下投影到小维度,再上投影回来)。只有适配器权重被训练;原始模型权重被冻结。这增加的新参数不到 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\)。推理时,更新可以合并到原始权重中,没有额外延迟:

\[W' = W + BA\]

LoRA:冻结的权重矩阵 W 被一个通过小矩阵 A 和 B 的低秩路径旁路,将可训练参数减少 32 倍,同时匹配全量微调的性能

  • 前缀调优在每个注意力层的键和值矩阵前面添加一串可学习的“虚拟标记”。模型像对待真实标记一样关注这些前缀向量,并且只训练前缀参数。这类似于提示调优,但在激活空间而非嵌入空间中操作。

  • 提示工程是设计输入文本以从预训练模型中引出所需行为而不进行任何参数更新的艺术。

    • 零样本提示用自然语言描述任务(“对以下评论进行情感分类:”)。

    • 少样本提示在实际查询前提供输入-输出示例。

    • 思维链提示添加“让我们一步步思考”或在示例中包含推理轨迹,通过引导模型分解问题,显著提升了算术和逻辑推理任务的性能。

  • 上下文学习是大语言模型可以仅从提示中提供的示例学习执行任务而无需任何梯度更新的现象。模型的权重没有改变;它使用示例作为一种隐式规范。

  • 上下文学习的机制仍然是一个活跃的研究问题;一个假设是注意力层在前向传播中实现了某种形式的梯度下降,有效地在上下文示例上“训练”。

  • 缩放定律描述了模型大小、数据量、计算预算和性能(以损失衡量)之间的可预测关系。Kaplan 等人(2020)发现损失与每个变量遵循幂律关系:

\[L(N) \propto N^{-\alpha_N}, \quad L(D) \propto D^{-\alpha_D}, \quad L(C) \propto C^{-\alpha_C}\]
  • 其中 \(N\) 是参数量,\(D\) 是数据集大小,\(C\) 是计算预算。这些幂律在多个数量级上成立,表明仅仅扩大规模就能带来可预测的提升。

缩放定律:损失在对数-对数坐标图上呈幂律下降,Kaplan 和 Chinchilla 的研究表明性能随规模可预测地提升

  • Chinchilla 缩放定律(Hoffmann 等人,2022)对此进行了修正,表明大多数大模型训练不足。对于固定的计算预算 \(C\),最优分配是模型大小和训练数据量按同等比例增长:
\[N_{\text{opt}} \propto C^{0.5}, \quad D_{\text{opt}} \propto C^{0.5}\]
  • 这意味着,如果你将计算预算翻倍,你应该同时将模型大小和数据集大小增加 \(\sqrt{2}\) 倍,而不仅仅是让模型更大。

  • Kaplan 等人曾建议将 \(N\) 增加得比 \(D\) 更快,这导致了非常大但训练不足的模型。Chinchilla(700 亿参数,1.4 万亿标记)在相同的计算预算下匹配了 Gopher(2800 亿参数,3000 亿标记)的性能,表明早期模型严重缺乏数据。

  • 实用的经验法则:每个参数大约训练 20 个标记。

  • 混合专家(MoE)是一种在不按比例增加计算的情况下扩展模型容量的架构。MoE 不使用一个大的前馈层,而是使用多个专家 FFN 层和一个门控网络(路由器),为每个标记选择激活哪些专家。

  • 门控函数为每个专家计算路由分数,并选择 top-\(k\)(通常 \(k = 1\)\(k = 2\)):

\[G(x) = \text{TopK}(\text{softmax}(W_g x))\]
  • 只有被选中的专家处理该标记,因此计算成本与 \(k\)(激活专家的数量)成比例,而不是与专家总数 \(E\) 成比例。一个具有 8 个专家和 top-2 路由的模型拥有稠密模型 4 倍的参数量,但只有 2 倍的计算量。

MoE 层:输入标记通过路由器计算每个专家的分数,选择 top-2 专家,其输出按门控分数加权并求和

  • MoE 的一个关键挑战是负载均衡:如果路由器将大多数标记发送给少数受欢迎的专家,其他专家就被浪费了。训练中添加了一个辅助的负载均衡损失,鼓励均匀的专家利用率:
\[\mathcal{L}_{\text{balance}} = E \cdot \sum_{i=1}^{E} f_i \cdot p_i\]
  • 其中 \(f_i\) 是分配给专家 \(i\) 的标记比例,\(p_i\) 是专家 \(i\) 的平均路由概率。当标记比例和概率都均匀(每个等于 \(1/E\))时,该乘积最小化。

  • 专家并行将不同的专家分布到不同的加速器上。在前向传播过程中,一个 all-to-all 通信步骤将标记路由到托管其分配专家的设备,然后将结果路由回来。这种通信成本是大规模 MoE 的主要工程挑战。Switch Transformer、Mixtral 和 GShard 等模型使用 MoE 以实际的推理成本实现强大的性能。

  • 构建模型是工作的一半;衡量它们是否有效是另一半。NLP 评估尤其困难,因为语言是模糊的、主观的和开放式的。

  • 一个翻译可以有很多不同的正确方式。一个摘要即使与参考文本没有完全相同的词,也可能是好的。

  • 一个聊天机器人的响应可以是有帮助、无害和诚实的,但理性的人类仍会有分歧。

  • 精确匹配是最简单的指标:模型的输出是否与标准答案完全一致?它用于具有简短、无歧义答案的任务,如抽取式问答(SQuAD)或封闭式数学题。

  • EM 很严苛;“New York City”和“new york city”如果不进行规范化就无法匹配——但它的简单性使其无歧义。

  • 标记级指标将 NLP 视为标记级的分类问题,使用第 06 章中的精确率、召回率和 F1。

  • 精确率衡量模型预测的标记中有多少是正确的:\(P = \text{TP} / (\text{TP} + \text{FP})\)。一个预测很少实体但全对的模型具有高精确率。

  • 召回率衡量模型找到了多少标准答案中的标记:\(R = \text{TP} / (\text{TP} + \text{FN})\)。一个将每个标记都预测为实体的模型具有完美的召回率,但精确率极差。

  • F1 是精确率和召回率的调和平均值:

\[F_1 = \frac{2PR}{P + R}\]
  • 调和平均值(而非算术平均值)惩罚不均衡:如果 \(P\)\(R\) 中有一个很低,\(F_1\) 就会很低。对于 NER(文件 02),每个实体类型分别计算 F1,然后对类型取宏平均。对于词性标注,更常见的是标记级准确率,因为每个标记都有一个标签。

  • 跨度级 F1(用于 SQuAD)比较预测跨度中的标记集合与标准跨度中的标记集合。这比精确匹配更宽容:如果标准答案是“the Eiffel Tower”,模型预测“Eiffel Tower”,跨度 F1 很高(5 个标记中有 4 个重叠),尽管 EM 为零。

  • BLEU(双语评估替补,Papineni 等人,2002)是机器翻译的经典指标。它衡量候选翻译与一个或多个参考翻译之间的 n-gram 重叠。该分数结合了多个 n-gram 级别(1-gram 到 4-gram)的精确率和一个长度惩罚:

\[\text{BLEU} = \text{BP} \cdot \exp\!\left(\sum_{n=1}^{N} w_n \log p_n\right)\]
  • 其中 \(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-gram 匹配,而错过了有效的释义:“the cat is on the mat”和“a feline sits atop the rug”尽管意思相同,但二元组重叠为零。

  • BLEU 也完全忽略了召回率——一个只输出最常见词的候选在精确率上得分很高。

  • ROUGE(面向召回的评价替补,Lin,2004)是摘要的标准指标。与强调精确率的 BLEU 不同,ROUGE 强调召回率:参考的 n-gram 中有多少出现在候选摘要中?

  • ROUGE-N 计算 n-gram 的召回率:\(\text{ROUGE-N} = \frac{|\text{n-grams}_{\text{ref}} \cap \text{n-grams}_{\text{cand}}|}{|\text{n-grams}_{\text{ref}}|}\)。ROUGE-1(1-gram)和 ROUGE-2(2-gram)最常见。

  • ROUGE-L 使用候选和参考之间的最长公共子序列,它捕获句子级别的词序,而不需要连续的匹配。

  • LCS 长度除以参考长度得到召回率,除以候选长度得到精确率,然后 F 度量将两者结合。

  • LCS 通过动态规划计算,时间复杂度为 \(O(mn)\)(类似于文件 02 中的编辑距离):

\[R_{\text{LCS}} = \frac{\text{LCS}(X, Y)}{m}, \quad P_{\text{LCS}} = \frac{\text{LCS}(X, Y)}{n}, \quad F_{\text{LCS}} = \frac{(1 + \beta^2) R_{\text{LCS}} P_{\text{LCS}}}{R_{\text{LCS}} + \beta^2 P_{\text{LCS}}}\]
  • 其中 \(m\)\(n\) 是参考和候选的长度,\(\beta\) 通常设置为偏向召回率(\(\beta \to \infty\) 给出纯召回率)。

  • METEOR(具有显式排序的翻译评估指标,Banerjee 和 Lavie,2005)通过引入同义词、词干和词序来解决 BLEU 的弱点。

  • 它首先使用精确匹配、词干匹配(通过文件 02 的 Porter 词干提取器)和同义词匹配(通过文件 01 的 WordNet)对齐候选和参考之间的词。

  • 然后计算一元精确率和召回率的调和平均值(偏向召回率),并应用一个碎片惩罚,惩罚匹配词在候选中的出现顺序与参考不同的情况。

  • ChrF(字符 n-gram F 分数)计算字符 n-gram 而非词 n-gram 的 F 分数。这使得它对形态变化鲁棒(对文件 01 中的黏着语言至关重要),并且部分处理了分词差异。ChrF++ 在字符 n-gram 中添加了词二元组。

  • 它已成为与 BLEU 并行的推荐机器翻译指标,特别是对于形态丰富的语言。

  • 困惑度(文件 02)衡量语言模型对保留测试集的预测能力。它是语言模型的标准内在指标:\(\text{PPL} = \exp(-\frac{1}{N} \sum_{i} \log P(w_i \mid w_{<i}))\)。越低越好。

  • 困惑度仅在相同分词的模型之间可比,因为不同的分词器对相同的文本产生不同的序列长度 \(N\)

  • 词汇表越大的模型,每个标记的困惑度通常越低,但每个句子处理的标记数越少。

  • 每字节比特数通过文本中的 UTF-8 字节数而非标记数进行归一化,使其与分词无关:

\[ \text{BPB} = \frac{-\sum_{i} \log_2 P(w_i \mid w_{
  • BERTScore(Zhang 等人,2020)通过在嵌入空间中计算相似度,超越了表面的 n-gram 匹配。候选中的每个标记被匹配到参考中最相似的标记,使用上下文嵌入的余弦相似度(通常来自预训练的 BERT 模型)。分数聚合成精确率、召回率和 F1:
\[R_{\text{BERT}} = \frac{1}{|r|} \sum_{r_i \in r} \max_{c_j \in c} \cos(r_i, c_j), \quad P_{\text{BERT}} = \frac{1}{|c|} \sum_{c_j \in c} \max_{r_i \in r} \cos(c_j, r_i)\]
  • 其中 \(r_i\)\(c_j\) 是参考和候选标记的上下文嵌入。这捕捉了 n-gram 指标遗漏的语义相似度:“automobile”和“car”得分很高,因为它们的 BERT 嵌入相似,尽管它们没有共享任何字符。

  • BLEURT(Sellam 等人,2020)更进一步,直接在人类质量判断上微调 BERT 模型。给定一个参考和候选对,它输出一个标量质量分数。BLEURT 首先在合成数据上训练(用 BLEU 和 METEOR 等指标评分的参考翻译的随机扰动),然后在人类评分上微调。它与人类判断的相关性优于任何表面级指标。

  • COMET(翻译评估的跨语言优化指标,Rei 等人,2020)是一个用于机器翻译的学习指标,其条件不仅包括参考和候选,还包括源句子。它使用多语言编码器(XLM-R)嵌入三者,并预测质量分数。通过看到源句,COMET 可以检测仅基于参考的指标遗漏的意义错误(例如,流畅但事实错误的翻译)。

  • LLM-as-judge 是现代大规模评估的方法。不同于针对参考计算指标,一个强大的语言模型(GPT-4、Claude)被提示评估模型输出的质量。评判者接收输入、模型的响应,以及可选的参考答案,并产生一个评分(例如 1-5)或一个成对偏好(响应 A 优于响应 B)。

  • 成对比较(用于 Chatbot Arena)是最可靠的 LLM-as-judge 格式。评判者看到两个响应并选择更好的一个,而不是分配绝对分数。这避免了校准问题(不同的评判者对“3/5”可能有不同的基准)。结果聚合成Elo 等级分(源自国际象棋),其中每个模型从一个基准分开始,并根据对其他模型的胜负得失分。模型 \(A\) 战胜模型 \(B\) 的期望获胜概率为:

\[P(A \succ B) = \frac{1}{1 + 10^{(R_B - R_A) / 400}}\]
  • 其中 \(R_A, R_B\) 是 Elo 等级分。每次比较后,等级分更新:\(R_A' = R_A + K(S - P(A \succ B))\),其中 \(S \in \{0, 1\}\) 是实际结果,\(K\) 控制更新幅度。持续击败强对手的模型快速上升;输给弱对手的模型下降。

  • 位置偏见是 LLM 评判者已知的问题:它们倾向于偏好先出现的响应(或在某些模型中,后出现的响应)。交换(以两种顺序评估每对响应)并平均结果可以缓解此问题。

  • 冗长偏见是另一个问题:即使简洁的答案更好,评判者也倾向于偏好更长、更详细的响应。

  • 自一致性检查评判者在多次评估同一输入时是否给出相同的评分。高方差表明评估信号有噪声。

  • 标注者间一致性(Cohen's kappa 或 Krippendorff's alpha)衡量多个评判者是否一致,为评估可靠性提供了上限。

  • 数据污染是一个关键问题:如果评估数据出现在模型的训练集中,基准分数会被夸大且毫无意义。

  • 这对于在网络抓取数据上训练的 LLM 尤其成问题,因为流行的基准很可能存在。缓解策略包括:使用未公开发布的保留测试集、创建定期重新生成问题的动态基准、金丝雀字符串(嵌入基准数据中以检测泄露的唯一标识符),以及比较受污染子集和干净子集上的性能。

  • 标准 NLU 基准评估跨不同任务的语言理解能力。

  • GLUE(通用语言理解评估)和 SuperGLUE 是多任务基准,涵盖情感(SST-2)、文本相似度(STS-B)、自然语言推理(MNLI、RTE)、共指(WSC)和问答(BoolQ)。

  • 模型在每个任务上分别评估,并由一个聚合指标打分。GLUE 现在被认为已饱和(模型在大多数任务上超过人类表现);SuperGLUE 仍然更具挑战性。

  • MMLU(大规模多任务语言理解)使用多项选择题评估涵盖 57 个学术科目(数学、历史、法律、医学、计算机科学等)的知识和推理能力。

  • 它测试模型在预训练期间是否吸收了广泛的知识。分数按科目和宏平均值报告。

  • MMLU-Pro 增加了更难的、多步推理的问题,答案选项从 4 个增加到 10 个。

  • HellaSwag 通过要求模型选择一个场景最合理的延续来测试常识推理。错误答案是对抗性生成的(使用模型),表面上合理但语义错误。

  • WinoGrande 使用相差一个词的最小对立对来测试常识共指消解。

  • ARC(AI2 推理挑战)使用小学科学问题,分为简单和挑战集,测试事实和推理能力。

  • 推理和数学基准评估区分强大的 LLM 和弱 LLM 的问题解决能力。

  • GSM8K(小学 8K 数学)包含 8,500 个需要多步算术推理的小学数学应用题。它是基础数学推理和评估思维链提示(文件 04)的标准基准。

  • MATH 是一个更难的竞赛级数学问题数据集,涵盖代数、数论、几何、计数和概率。问题需要多步符号推理,MATH-500 是常报告的 500 题子集。

  • AIME(美国邀请数学考试)问题是竞赛级:正确解决它们需要在许多步骤中进行深度数学推理。DeepSeek-R1 在 AIME 2024 上得分 79.8%,表明经过 RL 训练的推理模型(文件 05)可以接近强大的人类竞争者。

  • HumanEvalMBPP(大部分基本编程问题)通过检查模型的代码是否通过单元测试来评估代码生成能力。HumanEval 包含 164 个带函数签名和文档字符串的 Python 问题;模型必须生成函数体。

  • 指标是 pass@k:生成的 \(k\) 个解中至少有一个通过所有测试的概率。对于单个样本:

\[\text{pass@}k = 1 - \frac{\binom{n-c}{k}}{\binom{n}{k}}\]
  • 其中 \(n\) 是生成的样本总数,\(c\) 是通过的数量。此公式纠正了简单地取 \(k\) 个样本中最好的偏差。

  • SWE-bench 更进一步,评估模型是否能够通过修改现有代码库来解决真实的 GitHub issue——这是一个对实际软件工程能力更难的测试。

  • GPQA(研究生级 Google-proof QA)包含生物学、物理学和化学领域的专家级问题,即使对领域专家来说也很困难。它测试模型是否具有真正的理解而非模式匹配。“Diamond”子集是最难的。

  • 安全性与对齐基准评估模型是否有帮助、无害和诚实。

  • TruthfulQA 测试模型是否复现常见误解。问题的设计使得最常见的互联网答案都是错误的(例如,“如果你吞下口香糖会发生什么?”,常见的误解是它在胃里停留 7 年,但真实的答案是它会正常通过)。记住了流行但错误说法的模型得分低。

  • BBQ(问答中的偏见基准)测试跨类别(如年龄、性别、种族和宗教)的社会偏见。问题的结构使得有偏见的模型会系统性地选择刻板印象的答案。Toxigen 评估模型生成关于特定人口群体的有毒内容的倾向。

  • MT-Bench 使用 80 个精心设计的问题评估多轮对话能力,涵盖写作、角色扮演、推理、数学、编码、信息提取、STEM 和人文学科。一个 LLM 评判者(GPT-4)以 1-10 分对响应打分。多轮格式测试模型是否能跟进、维护上下文和处理澄清请求。

  • Chatbot Arena(LMSYS)使用真实用户在匿名模型之间进行盲成对比较。用户提交提示并投票选择更好的响应,而不知道是哪个模型生成的。最终的 Elo 等级分排行榜被认为是对通用 LLM 质量最具生态效度的评估,因为它反映了真实用户在多样化、未经过滤的提示上的偏好。

  • AlpacaEval 通过在一个固定的指令集上比较模型输出与参考模型(GPT-4)来自动化成对评估。一个评判模型决定胜率。

  • AlpacaEval 2.0 使用长度控制的胜率来纠正冗长偏见。

  • 任务特定评估需要为专业领域定制的指标。

  • 词错误率用于语音识别:\(\text{WER} = (S + D + I) / N\),其中 \(S, D, I\) 分别是替换、删除和插入错误,\(N\) 是参考词数。这是编辑距离(文件 02)除以参考长度,在词级别上应用。

  • 槽位 F1 用于任务导向型对话系统,衡量模型是否正确地从用户话语中提取结构化信息(例如,从“帮我订一张明天去巴黎的机票”中提取“destination: Paris”和“date: tomorrow”)。

  • 引用准确性用于 RAG 系统(文件 05)检查模型生成的引用是否真正支持所提出的主张。针对检索到的段落验证主张,指标统计完全支持、部分支持和不支持的主张的比例。

  • 评估陷阱很常见,会使整个基准比较失效。

  • 应试训练:优化基准性能而非真正的能力。一个在 MMLU 风格的选择题上微调的模型在 MMLU 上得分很高,但在以开放式格式提出的相同问题上可能失败。

  • 指标投机:模型可以被优化以产生在自动指标上得分很高的输出(高 BLEU、低困惑度),而实际上并不好。BLEU 最优的翻译通常是一个安全、通用的释义,而不是自然、流畅的翻译。

  • 基准饱和:当模型在基准上接近或超过人类表现时,该基准就不再具有信息量。GLUE、SQuAD 1.1 和其他几个现在已经饱和。

  • 该领域不断创建更难的新基准,但创建、饱和和替换的循环使得纵向比较变得困难。

  • 人工评估仍然是黄金标准,但昂贵、缓慢且难以复现。不同标注者群体(众包工作者与领域专家、不同文化、不同语言)产生不同的判断。报告标注者间一致性和标注者人口统计信息对于可复现性至关重要。

编码任务(使用 CoLab 或 notebook)

  1. 从头实现一个完整的 Transformer 编码器块(多头注意力、前馈、残差连接、层归一化)。将其应用于一个简单的序列分类任务。

    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
    
        # 重塑为 (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):
        # 前归一化多头自注意力
        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
    
        # 前归一化前馈
        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
    
    # 初始化参数
    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),
    }
    
    # 用随机输入测试
    x = jax.random.normal(keys[6], (2, 8, d_model))  # batch=2, seq_len=8
    out, attn_weights = transformer_block(x, params)
    print(f"输入形状:  {x.shape}")
    print(f"输出形状: {out.shape}")
    print(f"注意力权重形状: {attn_weights.shape}")  # (B, n_heads, T, T)
    
    # 可视化每个头的注意力模式
    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"头 {h}")
        axes[h].set_xlabel("键位置"); axes[h].set_ylabel("查询位置")
    plt.suptitle("多头注意力模式")
    plt.tight_layout(); plt.show()
    

  2. 实现因果(自回归)注意力掩码,并与双向注意力进行比较。展示掩码如何防止信息从未来流向过去的标记。

    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))
    
    # 双向(编码器风格):所有位置可见
    bidir_mask = jnp.ones((seq_len, seq_len), dtype=bool)
    bidir_out, bidir_weights = attention(Q, K, V, bidir_mask)
    
    # 因果(解码器风格):只有过去和当前位置可见
    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("双向注意力\n(BERT 风格)")
    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("因果掩码\n(1 = 允许, 0 = 阻止)")
    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("因果注意力\n(GPT 风格)")
    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("键"); ax.set_ylabel("查询")
    plt.tight_layout(); plt.show()
    
    # 验证:在因果注意力中,位置 i 的输出仅依赖于 <= i 的位置
    print("位置 2 的因果注意力权重(应该只关注 0, 1, 2):")
    print(f"  权重: {causal_weights[2]}")
    print(f"  未来位置权重之和(应该 ~0):{causal_weights[2, 3:].sum():.6f}")
    

  3. 实现 LoRA(低秩适配),并展示它如何以远少于全量微调的可训练参数来修改权重矩阵。

    import jax
    import jax.numpy as jnp
    
    d_model = 256
    rank = 4  # LoRA 秩(远小于 d_model)
    
    key = jax.random.PRNGKey(42)
    k1, k2, k3 = jax.random.split(key, 3)
    
    # 原始冻结的权重矩阵
    W_frozen = jax.random.normal(k1, (d_model, d_model)) * 0.02
    
    # LoRA 矩阵(只有这些是可训练的)
    B = jnp.zeros((d_model, rank))       # 初始化为零
    A = jax.random.normal(k2, (rank, d_model)) * 0.01  # 随机初始化
    
    # 前向传播:W_effective = W_frozen + B @ A
    x = jax.random.normal(k3, (8, d_model))
    
    # 无 LoRA
    y_original = x @ W_frozen.T
    
    # 使用 LoRA
    W_effective = W_frozen + B @ A
    y_lora = x @ W_effective.T
    
    # 参数数量
    full_params = d_model * d_model
    lora_params = d_model * rank + rank * d_model  # B + A
    
    print(f"模型维度: {d_model}")
    print(f"LoRA 秩: {rank}")
    print(f"全量微调参数量: {full_params:,}")
    print(f"LoRA 参数量: {lora_params:,}")
    print(f"参数减少: {full_params / lora_params:.1f} 倍")
    print(f"\n由于 B 初始化为零,初始 LoRA 输出与原始匹配:")
    print(f"  最大差异: {jnp.abs(y_original - y_lora).max():.2e}")
    
    # 模拟训练:只更新 A 和 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)
    
    # 目标: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"\nLoRA 前损失: {loss_before:.6f}")
    print(f"LoRA 后损失: {loss_after:.6f}")
    print(f"有效权重变化的秩: {jnp.linalg.matrix_rank(B @ A)}")