Transformer 与语言模型


文档摘要

Transformer 与语言模型 Transformer 用自注意力取代了循环,成为语言理解与生成的主导架构。本文件涵盖 BERT、GPT、T5、位置编码(正弦、RoPE)、预训练目标(MLM、CLM)、微调、提示工程以及缩放定律——现代 LLM 背后的蓝图。 在第 6 章中,我们介绍了 Transformer 架构:自注意力、多头注意力、位置编码以及编码器-解码器结构。这里我们聚焦于 Transformer 如何被适配到具体的 NLP 范式、定义了现代 NLP 的那些模型(BERT、GPT、T5),以及让它们能在规模上落地实用的那些技术。

Transformer 与语言模型

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

  • 在第 6 章中,我们介绍了 Transformer 架构:自注意力、多头注意力、位置编码以及编码器-解码器结构。这里我们聚焦于 Transformer 如何被适配到具体的 NLP 范式、定义了现代 NLP 的那些模型(BERT、GPT、T5),以及让它们能在规模上落地实用的那些技术。

  • 回顾核心运算:**缩放点积注意力(scaled dot-product attention)**计算 \text{softmax}(QK^T / \sqrt{d_k}) V,其中查询、键、值是输入的线性投影。**多头注意力(multi-head attention)**并行运行 h 个注意力头,每个头用不同的学习投影,再把结果拼接起来。Transformer 块还用残差连接、层归一化和一个按位置作用的前馈网络把它包起来(第 6 章)。

  • 一个微妙但重要的架构选择是层归一化(layer normalisation)放在哪里。原始 Transformer 用的是后归一化(post-norm):残差和归一化在子层之后,写作 \text{LayerNorm}(x + \text{Sublayer}(x))

  • 大多数现代模型用的是预归一化(pre-norm):在子层之前归一化,写作 x + \text{Sublayer}(\text{LayerNorm}(x))。预归一化在训练时更稳定,因为残差连接把梯度直接通过恒等路径传过去,不受归一化的影响。这让训练非常深的模型变得更容易,不需要仔细的学习率预热。

  • 每个 Transformer 块里的**前馈子层(feed-forward sublayer)**是一个两层 MLP,独立地作用在每个词元位置上:

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

  • 位置编码(positional encoding)给模型提供关于词元顺序的信息,因为注意力本身对排列是等变的。原始的正弦编码(sinusoidal encoding)(第 6 章)用不同频率的固定正弦和余弦函数。**可学习的位置嵌入(learned positional embeddings)**则是为每个位置加一个可训练的向量(BERT 和 GPT-2 使用)。两者都是绝对编码:位置 5 拿到的向量与上下文无关。

  • **旋转位置嵌入(Rotary Position Embedding,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 qk' = 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,而不依赖绝对位置 mn 各自的取值。

  • 模型在不引入任何可学习位置参数的情况下获得了一种自然的距离概念,并能泛化到训练中未见过的序列长度。

  • ALiBi(Attention with Linear Biases,带线性偏置的注意力)走了一条更简单的路:它根据距离给注意力分数加一个固定的线性惩罚,写作 \text{score}_{ij} = q_i^T k_j - m \cdot |i - j|,其中 m 是每个头独有的斜率。不同的头用不同的斜率,让一些头聚焦局部、另一些头聚焦全局。ALiBi 不需要任何可学习的位置参数,并且能很好地泛化到比训练时更长的序列。

  • 基于 Transformer 的语言模型有三大主导范式:纯编码器(encoder-only)纯解码器(decoder-only)编码器-解码器(encoder-decoder)。它们的区别在于模型能看到什么(注意力掩码)以及如何训练。

三种 Transformer 范式:纯编码器(BERT)用双向注意力做分类,纯解码器(GPT)用因果注意力做生成,编码器-解码器(T5)结合两者做 seq2seq 任务

  • BERT(Bidirectional Encoder Representations from Transformers,Devlin 等,2019)是经典的纯编码器模型。它用完整的双向注意力处理文本:每个词元都能注意到其他所有词元,无论左右。这给了 BERT 丰富的上下文表示,但也意味着它无法自回归地生成文本。

  • BERT 用两个目标做预训练。**掩码语言建模(Masked Language Modelling,MLM)**随机掩蔽 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}} 是把这些位置掩蔽后的句子。这是一个**去噪(denoising)**目标:模型学会重建被破坏的输入。

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

  • **下一句预测(Next Sentence Prediction,NSP)**训练 BERT 预测两句话在原文中是否前后相连。输入起始处的一个特殊 [CLS] 词元用于这个二分类。NSP 最初是为了帮助问答这类需要理解句子关系的任务,但后来的工作(RoBERTa)表明它贡献很小,可以去掉。

  • BERT 的预训练表示通过在顶部加一个任务相关的头(一个简单的线性层)并微调整个模型,来适配下游任务。对分类任务,用 [CLS] 词元的表示;对词元级任务(NER、POS 标注),用每个词元的表示。这种**微调(fine-tuning)**做法把预训练中学到的语言知识迁移到新任务上,只需要相对少量的标注数据。

  • GPT(Generative Pre-trained Transformer,Radford 等,2018)是经典的纯解码器模型。它用因果(自回归)注意力(causal/autoregressive attention):每个词元只能注意到更早位置(以及自己)的词元。这是通过在注意力矩阵中掩蔽未来位置(在 softmax 之前把它们的分数设为 -\infty)来实现的。训练目标是简单的因果语言建模(causal language modelling):给定之前所有词元,预测下一个。

\mathcal{L}_{\text{CLM}} = -\sum_{i=1}^{n} \log P(w_i \mid w_1, \ldots, w_{i-1})
  • 这和第 2 节的 n-gram 语言模型目标是同一个,只是换成了 Transformer 参数化,能够以整个上文为条件,而不只是最近的 k-1 个词元。

  • GPT-2 把它扩展到 15 亿参数,并展示了强大的零样本性能:在没有任何微调的情况下,它能通过以一段自然语言提示为条件来完成任务("Translate English to French: ...")。

  • GPT-3(1750 亿参数)表明,仅仅靠规模就能催生上下文学习(in-context learning):在提示里给出几个输入-输出示例,模型就能在没有任何梯度更新的情况下完成新任务。

  • 编码器-解码器模型,如 T5(Text-to-Text Transfer Transformer,Raffel 等,2020),把每个 NLP 任务都框定为文本到文本:输入是一个文本串(可能带一个任务前缀,如 "translate English to German:"),输出也是一个文本串。编码器用双向注意力处理输入,解码器则自回归地生成输出,并通过交叉注意力关注编码器。

  • T5 用**片段破坏(span corruption)**做预训练:随机连续的若干词元片段被替换成哨兵词元,模型必须生成原始词元。例如,"The cat sat on the mat" 作为输入可能变成 "The [X] on [Y]",而目标是 "[X] cat sat [Y] the mat"。这是把 BERT 的 MLM 从单个词元推广到了片段。

  • BART(Lewis 等,2020)是另一个用去噪目标预训练的编码器-解码器模型,但它施加的破坏策略更广:词元掩蔽、词元删除、片段掩蔽、句子重排、文档旋转。破坏方式的多样性迫使模型学到更鲁棒的表示。

  • 随着语言模型越来越大,**全量微调(full fine-tuning,更新所有参数)**变得不切实际:一个 175B 参数的模型仅存储优化器状态就需要数百 GB。**参数高效微调(Parameter-Efficient Fine-Tuning,PEFT)**方法只适配其中一小部分参数。

  • **适配器(adapters)**在已有的 Transformer 层之间插入小型瓶颈层(通常是两个带非线性的线性层:先降维到一个小维度,再升维回去)。只训练适配器的权重,原始模型权重冻结。这新增不到 5% 的参数,却能在大多数任务上匹配全量微调的性能。

  • LoRA(Low-Rank Adaptation,低秩适配)不新增层,而是直接修改权重矩阵。LoRA 不更新完整的权重矩阵 W,而是学习更新的一个低秩分解:W' = W + BA,其中 Bd \times rAr \times d,且 r \ll d(通常 r = 4r = 64)。原始 W 冻结,只训练 AB。在推理时,这个更新可以合并进原始权重,不增加任何额外延迟:

W' = W + BA

LoRA:冻结的权重矩阵 W 被一条经过小矩阵 A 和 B 的低秩通路绕过,把可训练参数减少 32 倍,同时匹配全量微调

  • **前缀微调(prefix tuning)**给每层注意力的键和值矩阵前面追加一串可学习的"虚拟词元"。模型把这些前缀向量当作真正的词元一样去注意,只训练前缀参数。这类似于提示微调(prompt tuning),但作用在激活空间而不是嵌入空间。

  • **提示工程(prompt engineering)**是一门设计输入文本、让预训练模型表现出期望行为的艺术,全程不需要任何参数更新。

    • **零样本提示(zero-shot prompting)**用自然语言描述任务("Classify the sentiment of the following review:")。

    • **少样本提示(few-shot prompting)**在实际查询之前给出若干输入-输出示例。

    • 思维链(chain-of-thought,CoT)提示加上 "Let's think step by step",或在示例中包含推理轨迹,这能大幅提升算术和逻辑推理任务的表现,因为它引导模型把问题分解开来。

  • **上下文学习(in-context learning,ICL)**是这样一种现象:大语言模型能从提示中提供的示例学会完成任务,而不需要任何梯度更新。模型的权重不变;它把示例当作一种隐式的规约来使用。

  • ICL 在机制上究竟如何运作,仍是一个活跃的研究问题;一种假设是注意力层在前向传播中实现了一种梯度下降,相当于在上下文示例上"训练"。

  • **缩放定律(scaling laws)**描述的是模型大小、数据大小、算力预算与性能(以损失衡量)之间可预测的关系。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 个词元。

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

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

G(x) = \text{TopK}(\text{softmax}(W_g x))
  • 只有被选中的专家处理该词元,所以计算开销随 k(激活的专家数)增长,而不是随专家总数 E 增长。一个有 8 个专家、top-2 路由的模型,参数是稠密模型的 4 倍,但计算量只有 2 倍。

MoE 层:输入词元经过一个路由器,路由器为每个专家计算分数,选出 top-2 专家,它们的输出按门控分数加权后求和

  • MoE 中一个关键挑战是负载均衡(load balancing):如果路由器把大多数词元送到少数几个热门专家那里,其他专家就白白浪费了。训练时会加一个辅助的负载均衡损失,鼓励专家被均匀使用:
\mathcal{L}_{\text{balance}} = E \cdot \sum_{i=1}^{E} f_i \cdot p_i
  • 其中 f_i 是分配给专家 i 的词元比例,p_i 是专家 i 的平均路由概率。当词元比例和概率都均匀(各自等于 1/E)时,这个乘积被最小化。

  • **专家并行(expert parallelism)**把不同的专家分布到不同的加速器上。在前向传播中,一次 all-to-all 通信把词元送到托管其分配专家的设备上,再把结果送回来。这个通信开销是 MoE 在大规模下的主要工程挑战。Switch Transformer、Mixtral、GShard 等模型利用 MoE 在实用的推理开销下取得了强劲的性能。

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

  • 一段翻译可以有许多种不同的正确方式。一段摘要即便与参考答案没有一个字相同,也可能是好的。

  • 一个聊天机器人的回复可能既 helpful(有用)、harmless(无害),又 honest(诚实),但通情达理的人类之间仍会意见不一。

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

  • EM 很苛刻;"New York City" 和 "new york city" 除非经过归一化否则不算匹配——但它的简单性让它没有歧义。

  • **词元级指标(token-level metrics)**把 NLP 当作词元级的分类问题,用第 6 章的精确率、召回率和 F1。

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

  • **召回率(recall)**衡量黄金词元中模型找到了多少:R = \text{TP} / (\text{TP} + \text{FN})。一个把每个词元都预测成实体的模型召回率完美,但精确率极差。

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

F_1 = \frac{2PR}{P + R}
  • 用调和平均(而不是算术平均)会惩罚不平衡:只要 PR 中有一个低,F_1 就低。对 NER(第 2 节),F1 按实体类型计算,再在各类型间做宏平均。对 POS 标注,更常用的是词元级准确率,因为每个词元都有一个标签。

  • 片段级 F1(span-level F1)(SQuAD 使用)比较预测片段中的词元集合与黄金片段中的词元集合。这比精确匹配更宽容:如果黄金答案是 "the Eiffel Tower",模型预测 "Eiffel Tower",那么即便 EM 为零,片段 F1 也很高(5 个词元里有 4 个重叠)。

  • BLEU(Bilingual Evaluation Understudy,Papineni 等,2002)是机器翻译的经典指标。它衡量候选翻译与一个或多个参考翻译之间的 n-gram 重叠。分数把多个 n-gram 级别(从 unigram 到 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)。

  • 简洁度惩罚(brevity penalty) \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(Recall-Oriented Understudy for Gisting Evaluation,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(unigram)和 ROUGE-2(bigram)。

  • ROUGE-L 用候选和参考之间的最长公共子序列(longest common subsequence,LCS),它能在不要求连续匹配的情况下捕捉句子级别的词序。

  • LCS 长度除以参考长度得到召回率,除以候选长度得到精确率,F-measure 把两者结合起来。

  • LCS 通过动态规划在 O(mn) 时间内计算(类似于第 2 节的编辑距离):

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}}}
  • 其中 mn 是参考和候选的长度,\beta 通常设为偏向召回率(\beta \to \infty 时就是纯召回率)。

  • METEOR(Metric for Evaluation of Translation with Explicit ORdering,Banerjee 和 Lavie,2005)通过纳入同义词、词干提取和词序,弥补了 BLEU 的弱点。

  • 它先用精确匹配、词干匹配(通过第 2 节的 Porter 词干提取)和同义词匹配(通过第 1 节的 WordNet)在候选和参考之间对齐词。

  • 然后它计算偏向召回率的 unigram 精确率和召回率的调和平均,并施加一个碎片化惩罚,惩罚那些匹配词出现的顺序与参考不同的候选。

  • ChrF(Character n-gram F-score)在字符 n-gram(而不是词 n-gram)上计算 F 分数。这让它对形态变化鲁棒(对第 1 节的黏着语至关重要),并能部分处理分词差异。ChrF++ 在字符 n-gram 之外再加上词二元组。

  • 它已经成为与 BLEU 并列的推荐机器翻译指标,尤其适用于形态丰富的语言。

  • 困惑度(perplexity)(第 2 节)衡量一个语言模型在一个留出测试集上预测得有多好。它是语言模型的标准内在指标:\text{PPL} = \exp(-\frac{1}{N} \sum_{i} \log P(w_i \mid w_{<i}))。越低越好。

  • 困惑度只在用相同分词的模型之间才可比,因为不同的分词器对同一段文本会产生不同的序列长度 N

  • 词表更大的模型每个词元的困惑度往往更低,但每句话处理的词元更少。

  • **每字节比特数(bits-per-byte,BPB)**按文本的 UTF-8 字节数而不是词元数归一化,因此与分词无关:

\text{BPB} = \frac{-\sum_{i} \log_2 P(w_i \mid w_{<i})}{\text{number of UTF-8 bytes}}
  • 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_ic_j 是参考和候选词元的上下文嵌入。这能捕捉到 n-gram 指标错过的语义相似性:"automobile" 和 "car" 得分很高,因为它们的 BERT 嵌入相似,即便它们没有共享任何字符。

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

  • COMET(Crosslingual Optimized Metric for Evaluation of Translation,Rei 等,2020)是一个学习得到的机器翻译指标,它以源句、参考和候选为条件——而不仅仅是参考和候选。它用一个多语言编码器(XLM-R)把三者都嵌入,并预测一个质量分数。通过看到源句,COMET 能发现只看参考的指标会错过的意义错误(例如流畅但事实错误的翻译)。

  • **LLM 作裁判(LLM-as-judge)**是面向规模的现代评测方法。不再针对参考计算指标,而是用一个强大的语言模型(GPT-4、Claude)通过提示来评估模型输出的质量。裁判拿到输入、模型的回复,以及可选的一个参考答案,然后产出一个评分(例如 1-5)或一个成对偏好(回复 A 比回复 B 好)。

  • 成对比较(pairwise comparison)(Chatbot Arena 使用)是最可靠的 LLM 作裁判形式。裁判看到两个回复并选出更好的那个,而不是给出绝对分数。这避免了校准问题(不同的裁判对"5 分里的 3 分"可能有不同的基线)。结果被聚合成 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 控制更新幅度。持续击败强手的模型上升很快;输给弱手的模型则下滑。

  • **位置偏差(position bias)**是 LLM 裁判的一个已知问题:它们倾向于偏好先呈现的那个回复(或在某些模型中,后呈现的那个)。交换(swapping)(每对评两次,两种顺序各一次)再取平均,可以缓解这一点。

  • **冗长偏差(verbosity bias)**是另一个:即便简洁的答案更好,裁判也倾向于偏好更长、更详细的回复。

  • **自一致性(self-consistency)**检查裁判对同一个输入的多次评测是否给出相同的评分。方差高说明评测信号噪声大。

  • 标注者间一致性(inter-annotator agreement)(Cohen's kappa 或 Krippendorff's alpha)衡量多个裁判之间是否一致,为评测可靠性提供了一个上界。

  • **污染(contamination)**是一个关键担忧:如果评测数据出现在模型的训练集里,基准分数就被夸大、变得毫无意义。

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

  • 标准 NLU 基准在各种任务上评测语言理解。

  • GLUE(General Language Understanding Evaluation)和 SuperGLUE 是多任务基准,涵盖情感(SST-2)、文本相似度(STS-B)、自然语言推断(MNLI、RTE)、共指(WSC)和问答(BoolQ)。

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

  • MMLU(Massive Multitask Language Understanding)用多项选择题,评测横跨 57 个学科(数学、历史、法律、医学、计算机科学等)的知识和推理。

  • 它测试的是模型在预训练中是否吸收了广博的知识。分数按学科报告,并给出一个宏平均。

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

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

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

  • ARC(AI2 Reasoning Challenge)用小学科学题,分简单和挑战两套,测试事实和推理能力。

  • 推理与数学基准评测解题能力,正是这一点把强 LLM 和弱 LLM 区分开来。

  • GSM8K(Grade School Math 8K)包含 8500 道小学数学应用题,需要多步算术推理。它是基础数学推理和评测思维链提示(第 4 节)的标准基准。

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

  • AIME(American Invitational Mathematics Examination)题目是竞赛级别的:要正确解答它们,需要跨越许多步骤的深度数学推理。DeepSeek-R1 在 AIME 2024 上得分 79.8%,证明用 RL 训练的推理模型(第 5 节)可以接近人类强手的水平。

  • HumanEvalMBPP(Mostly Basic Programming Problems)通过检查模型生成的代码是否通过单元测试来评测代码生成。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(Graduate-Level Google-Proof QA)包含生物学、物理学和化学领域的研究生级题目,即便对领域专家也很难。它测试模型是否具有真正的理解,而不是模式匹配。"Diamond" 子集是最难的。

  • 安全与对齐基准评测模型是否 helpful、harmless、honest。

  • TruthfulQA 测试模型是否会复述常见的错误观念。题目被设计成让最常见的网络答案是错的(例如 "What happens if you swallow gum?",常见的迷思是它会留 7 年,但诚实的答案是它会正常通过)。记住了流行但错误说法的模型得分会很差。

  • BBQ(Bias Benchmark for QA)测试在年龄、性别、种族、宗教等类别上的社会偏见。题目被结构化成让一个有偏见的模型会系统性地选择刻板印象答案。Toxigen 评测模型生成关于特定人群的有毒内容的倾向。

  • MT-Bench 用 80 道精心设计的题目评测多轮对话能力,涵盖写作、角色扮演、推理、数学、编程、抽取、STEM 和人文。一个 LLM 裁判(GPT-4)按 1-10 分给回复打分。多轮形式测试模型能否跟进、维持上下文,以及处理澄清请求。

  • Chatbot Arena(LMSYS)用真实用户在匿名模型之间做盲测的成对比较。用户提交提示并为更好的回复投票,但不知道是哪个模型生成的。得到的 Elo 排行榜被认为是对通用 LLM 质量最具生态效度的评测,因为它反映的是真实用户在多样、未加筛选的提示上的偏好。

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

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

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

  • 语音识别的词错率(Word Error Rate,WER)\text{WER} = (S + D + I) / N,其中 SDI 分别是替换、删除和插入错误,N 是参考词数。这就是第 2 节的编辑距离除以参考长度,作用在词级别。

  • 面向任务的对话系统的**槽位 F1(Slot F1)**衡量模型是否正确从用户话语中抽取结构化信息(例如从 "Book me a flight to Paris tomorrow" 中抽取 "destination: Paris" 和 "date: tomorrow")。

  • RAG 系统(第 5 节)的**引用准确率(citation accuracy)**检查模型生成的引用是否真的支持它所做的论断。一条论断会对照检索到的段落进行核查,指标统计完全支持、部分支持和不支持的论断比例。

  • 评测陷阱很常见,可能让整个基准比较失去意义。

  • 应试教学(teaching to the test):为基准性能而非真实能力做优化。一个在 MMLU 风格多项选择题上微调的模型,在 MMLU 上得分会很好,但同样的题目换成开放式形式就可能失败。

  • 钻指标空子(metric gaming):模型可以被优化成在自动指标上得分高(高 BLEU、低困惑度)却并不真正好。BLEU 最优的翻译往往是一个安全、通用的释义,而不是自然流畅的翻译。

  • 基准饱和(benchmark saturation):当模型在一个基准上接近或超越人类时,这个基准就不再有信息量。GLUE、SQuAD 1.1 和其他几个现在已经饱和。

  • 这个领域不断创造更难的基准,但创造、饱和、替换的循环让纵向比较变得困难。

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

编程练习(使用 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"Input shape: {x.shape}") print(f"Output shape: {out.shape}") print(f"Attention weights shape: {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"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()
  1. 实现因果(自回归)注意力掩码,并把它与双向注意力做对比。展示掩码如何阻止信息从未来词元流向过去词元。
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("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() # 验证:在因果注意力中,位置 i 的输出只依赖于位置 <= 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}")
  1. 实现 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"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}") # 模拟训练:只更新 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"\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)}")

作者与出处
原作者: HenryNdubuaku
来源:HenryNdubuaku
许可证:Apache-2.0
整理: 灏天文库整理
由灏天文库结构化整理,提供目录导航、全文检索与在线阅读,便于系统化学习
发布者: 作者: HenryNdubuaku 转发
评论区 (0)
U