理解这个叫做“世界”的操作系统
灵光一现 → 精巧实现 → 无人知晓 → 自己遗忘,几乎是每个认真思考的工程师都会经历的隐性知识流失
用 PyTorch 从头实现语言模型
《Speech and Language Processing》(2025版草稿)中文版
理解这个叫做“世界”的操作系统
灵光一现 → 精巧实现 → 无人知晓 → 自己遗忘,几乎是每个认真思考的工程师都会经历的隐性知识流失
用 PyTorch 从头实现语言模型
《Speech and Language Processing》(2025版草稿)中文版
预训练语言模型的强大之处在于,它们能够从海量文本中提取出通用的语言规律,而这些规律对大量下游应用都非常有用。 要将这些通用知识应用于具体任务,主要有两种方式。 最常见的方式是使用自然语言对模型进行提示(prompt),引导模型进入某种状态,使其在上下文中生成我们期望的输出。 本节将探讨另一种利用预训练语言模型解决下游任务的方法:即第 7 章介绍过的微调(finetuning)范式的一种变体。 在针对掩码语言模型的微调方法中,我们会在预训练模型之上添加面向特定任务的模块(通常称为专用“头”,head),以预训练模型的输出作为该模块的输入。 微调过程利用带标签的任务数据来训练这些新增的、任务特定的参数。 通常情况下,这一训练过程会冻结预训练模型的大部分参数,或仅对其做极小幅度的调整。 接下来几节将介绍针对最常见任务类型的微调方法:序列分类(sequence classification)、句子对分类(sentence-pair classification)和序列标注(sequence labeling)。 9.4.1 序列分类 序列分类(sequence classification)任务的目标是为一整段文本序列分配一个单一的类别标签。 这类任务通常统称为文本分类(text classification),例如情感分析或垃圾邮件检测(见附录 K),在这些任务中,我们将一段文本分为两类或三类(如正面、负面);也包括类别数量较多的任务,例如文档级别的主题分类。 在序列分类中,我们用一个向量来表示整个待分类的输入序列。 表示序列的方式有多种。 一种方法是对序列中每个词元在模型最后一层输出的向量进行求和或取平均。 但对于 BERT,我们采用另一种方式:在词汇表中引入一个特殊的唯一标记 [CLS](代表“classification”),在预训练和推理阶段都将该标记添加到所有输入序列的开头。 模型最后一层中对应 [CLS] 标记的输出向量即被用作整个输入序列的表示,并作为后续分类器头部(classifier head)的输入。该分类器通常是一个逻辑回归模型或小型神经网络,用于做出最终的分类决策。 举个例子,回到情感分类问题。 针对该任务进行微调,需要学习一组权重矩阵 $\mathbf{W_C}$,将 [CLS] 标记的输出向量 $\mathbf{h}^L_{\text{CLS}}$ 映射到各个情感类别上的得分。 假设这是一个三分类情感任务(正面、负面、中性),且模型的隐藏维度为 $d$,那么 $\mathbf{W_C}$ 的维度就是 $[d \times 3]$。 要对一篇文档进行分类,我们首先将输入文本送入预训练语言模型,得到 $\mathbf{h}^L_{\text{CLS}}$,然后将其与 $\mathbf{W_C}$ 相乘,并将结果通过 softmax 函数归一化为概率分布: $$ \mathbf{y} = \text{softmax}(\mathbf{h}^L_{\text{CLS}} \mathbf{W_C}) \tag{9.11} $$对 $\mathbf{W_C}$ 的微调需要依赖带标签的监督训练数据,即每条输入序列都标注了正确的情感类别。 训练过程采用标准方式:使用 softmax 输出与真实标签之间的交叉熵损失来驱动 $\mathbf{W_C}$ 的学习。 此外,该损失函数不仅可以用于更新分类器的权重,还可以反向传播以微调预训练语言模型本身的参数。 在实践中,通常只需对语言模型参数做极小幅度的调整就能获得良好的分类性能,且更新往往仅限于 Transformer 的最后几层。 图 9.9 展示了这种序列分类的整体架构。 ...
给定一个预训练好的语言模型和一个新的输入句子,我们可以将模型输出的序列视为输入中每个词元的上下文嵌入(contextual embeddings)。 这些上下文嵌入是向量,用于表示某个词元在其具体上下文中的语义某一方面,可应用于任何需要理解词元或词语含义的任务。 更形式化地,给定一个输入词元序列 $x_1, \cdots, x_n$,我们可以使用模型最终层 $L$ 的输出向量 $\mathbf{h}^L_i$ 作为词元 $x_i$ 在句子 $x_1, \dots, x_n$ 上下文中的语义表示。 或者,除了仅使用最终层的向量 $\mathbf{h}^L_i$ 外,一种常见做法是通过对模型最后四层的输出向量进行平均来构建 $x_i$ 的表示,$\mathbf{h}^L_i$、$\ \mathbf{h}^{L-1}_i$、$\mathbf{h}^{L-2}_i$、$\mathbf{h}^{L-3}_i$。 图 9.5 BERT 类模型的输出是对每个输入词元 $x_i$ 生成的一个上下文嵌入向量 $\mathbf{h}^L_i$。 正如我们在第 5 章中使用 word2vec 等静态嵌入(static embeddings)来表示词语的含义一样,我们也可以将上下文嵌入用作词语在具体语境中含义的表示,以支持任何需要建模词语语义的任务。 静态嵌入表示的是词型(word types,即词汇表条目)的含义,而上下文嵌入表示的是词例(word instances)的含义——即某一特定词型在特定上下文中的具体出现实例。 因此,如果说 word2vec 为每个词型只提供一个固定向量,那么上下文嵌入则为该词型在每一个句子上下文中的每次出现都提供一个独立的向量。 正因如此,上下文嵌入可用于诸如衡量两个词语在各自上下文中的语义相似度等任务,并在需要词语语义建模的语言学任务中发挥重要作用。 9.3.1 上下文嵌入与词义 词语具有歧义性(ambiguous):同一个词可以表达不同的含义。 在第 5 章中我们看到,单词 “mouse” 可以指:(1) 一种小型啮齿动物,或 (2) 一种用于控制光标的手持设备。“bank” 则可能指:(1) 一家金融机构,或 (2) 河岸(即一侧倾斜隆起的土坡)。 我们称像 “mouse” 或 “bank” 这样的词为多义词(polysemous),该词源自希腊语 “many senses”(poly- 表示“多”,sema 表示“符号、标记”)。1 一个词义(sense 或 word sense)是对词语某一特定含义的离散化表示。 我们可以用上标来区分不同词义:如 bank¹ 与 bank²、mouse¹ 与 mouse²。 这些词义可以在在线词典或同义词词典(thesauruses/thesauri)中找到,例如 WordNet(Fellbaum,1998)——它提供了多种语言的数据集,列出了大量词语的不同词义。 在具体上下文中,不同词义很容易区分,以下英文例句展示了英语中典型的一词多义现象: ...
基于 Transformer 的语言模型为何能在各种语言任务上表现如此出色? 可解释性(interpretability)这一子领域——有时也称为机制性可解释性(mechanistic interpretability)——致力于从机制层面理解 Transformer 内部究竟发生了什么。 在接下来的两个小节中,我们将讨论 Transformer 可解释性研究中两个被深入探索的方向。 8.9.1 上下文学习与归纳头(In-Context Learning and Induction Heads) 为了让模型完成我们期望的任务,提示(prompting)与预训练(pretraining)在本质上是两种截然不同的方式。 预训练通过梯度下降更新模型参数,依据某个损失函数进行学习。 而带示例的提示(prompting with demonstrations)却能在不更新任何参数的情况下,教会模型执行新任务。 模型在处理提示的过程中,从这些示例中“学到”了关于任务的某种规律。 即使没有显式示例,提示过程本身也可被视为一种学习形式。 例如,随着模型在提示中读取的位置越靠后,它对后续词元的预测往往就越准确。 上下文中的信息正在提升模型的预测能力。 Brown 等人(2020)在介绍 GPT-3 时首次提出术语上下文学习(in-context learning),用以描述语言模型通过提示进行的这种学些。 上下文学习意味着语言模型在推理阶段仅通过前向传播(不进行任何梯度更新),就能学会执行新任务、更好地预测词元或总体上降低其损失。 那么,上下文学习是如何实现的? 尽管尚无定论,但已有若干引人注目的假说。 其中一种核心观点基于归纳头(induction heads)的概念(Elhage et al., 2021;Olsson et al., 2022)。 归纳头(induction heads)是一种计算回路(circuit)的名称,即网络中实现特定功能的一种抽象组件。 它是在 Transformer 的注意力计算中发现的一种结构,最初通过研究仅含 1–2 个注意力头的微型语言模型而被识别出来。 归纳头的功能是预测重复出现的序列模式。 例如,当输入序列为 AB...A 时,它会预测下一个词应为 B,从而实现一种模式补全(pattern completion)规则 $AB\ldots A \rightarrow B$。 它通过注意力计算中的一个 前缀匹配组件(prefix matching component)来实现这一点:当处理当前词元 A 时,该组件会在上下文中向后搜索,以找到 A 的先前出现位置。 一旦找到之前的 A,归纳头就使用复制机制(copying mechanism)“复制”紧随其后的词 B,通过提升 B 的出现概率来完成预测。 图 8.19 展示了一个实例。 ...
大语言模型确实非常庞大。 例如,Meta 发布的 Llama 3.1 405B Instruct 模型拥有 4050 亿参数(共 126 层,模型维度为 16,384,128 个注意力头),并在 15.6 TB 的文本词元上进行训练(Llama Team, 2024),使用了大小为 128K 的词汇表。 因此,学术界和工业界投入了大量研究来理解 LLM 的扩展规律,尤其是如何在有限计算资源下高效实现和部署这些模型。 在接下来几节中,我们将讨论如何思考模型规模问题(即“扩展定律”,scaling laws),以及一些关键的高效技术,如 KV 缓存(KV cache)和参数高效微调(parameter-efficient fine-tuning)。 8.8.1 扩展定律 研究表明,大语言模型的性能主要由三个因素决定:模型规模(参数量,通常不包括嵌入层参数);数据集规模(训练数据总量);训练所用的计算量(compute budget)。 换句话说,我们可以通过以下任一方式提升模型性能:增加参数(增加层数、扩大上下文或两者兼有);使用更多训练数据;增加训练迭代次数(即投入更多算力)。 这些因素与模型性能之间的关系被称为 扩展定律(scaling laws)。 粗略地说,大语言模型的性能(以损失 $L$ 衡量)与上述三个训练属性均呈幂律关系(power-law relationship)。 例如,Kaplan 等人(2020)发现,在其他两个因素保持不变的前提下,当分别受限于模型规模、数据量或计算预算时,损失 $L$ 与(非嵌入部分的)参数数量 $N$、数据集大小 $D$ 和计算预算 $C$ 的存在如下三种关系: $$ L(N) = \left( \frac{N_c}{N} \right)^{\alpha_N} \tag{8.49} $$$$ L(D) = \left( \frac{D_c}{D} \right)^{\alpha_D} \tag{8.50} $$$$ L(C) = \left( \frac{C_c}{C} \right)^{\alpha_C} \tag{8.51} $$非嵌入参数总数 $N$ 可大致按如下方式估算(忽略偏置项,并设 $d$ 为模型输入/输出维度,$d_{\text{attn}}$ 为自注意力层维度,$d_{\text{ff}}$ 为前馈网络维度): ...
我们在前一章已经介绍了语言模型的训练过程。 回顾一下:*语言模型通常使用交叉熵损失,也称为负对数似然损失(negative log likelihood loss)。 在时间步 $t$,交叉熵损失等于模型对训练序列中下一个词所分配概率的负对数:$-\log p(w_{t+1})$ 图 8.16 展示了通用的训练方法。 在每一步中,给定所有前置词元,Transformer 的最后一层会输出一个覆盖整个词汇表的概率分布。 训练时,模型为正确下一个词分配的概率被用于计算序列中每个位置的交叉熵损失。 一条训练序列的总损失是其所有位置上交叉熵损失的平均值。 随后,通过梯度下降法调整网络中的所有参数,以最小化该序列上的平均交叉熵损失。 图 8.16 将 Transformer 作为语言模型进行训练。 在 transformer训练阶段,序列中的每个位置可以并行处理,因为序列中每个元素的输出是独立计算的。 大型模型通常会填满整个上下文窗口进行训练。例如 GPT-4 使用 4096 个词元的上下文窗口,Llama 3 则使用 8192 个词元。 如果单个文档长度不足,系统会将多个文档拼接(packed)到同一个窗口中,并在文档之间插入特殊的文本结束符(end-of-text token)。 此外,梯度下降所用的批大小(batch size)通常非常大。例如,GPT-3 最大的模型版本使用了高达 320 万词元。 8.6 关于采样方法的更多讨论 目录 8.8 应对规模挑战
下面介绍的采样方法都包含可调节的参数,用于在生成过程中权衡两个关键因素:质量(quality)与多样性(diversity)。 倾向于选择高概率词的方法,通常生成的文本被人类评价为更准确、更连贯、更符合事实,但也更容易显得枯燥、重复。 而给予中等概率词稍高权重的方法,则往往更具创造性与多样性,但可能牺牲事实性,甚至导致语义混乱或整体质量下降。 8.6.1 Top-k 采样(Top-k Sampling) Top-k 采样是对贪心解码(greedy decoding)的一种简单推广。 它不再只选择概率最高的单个词,而是先将整个词汇表的概率分布截断,仅保留概率最高的 $k$ 个词;对这 $k$ 个词的概率重新归一化,形成一个合法的概率分布;然后根据这个新分布从中随机采样一个词。 更正式地描述如下: 预先选定一个整数 $k$; 对词汇表 $V$ 中的每个词,使用语言模型计算其在当前上下文下的条件概率 $p(w_t \mid \mathbf{w}_{< t})$; 将所有词按概率从高到低排序,丢弃排名不在前 $k$ 的词; 将剩余 $k$ 个词的概率值重新归一化(使其和为 1); 根据归一化后的概率,从这 $k$ 个词中随机采样一个作为输出。 当 $k = 1$ 时,top-$k$ 采样就退化为贪心解码。 而当 $k > 1$ 时,模型有时会选择并非最可能但仍然合理的词,从而在保持文本质量的同时提升生成结果的多样性。 8.6.2 核采样(Nucleus Sampling)或 Top-$p$ 采样 Top-$k$ 采样的一个主要问题是:$k$ 是固定值,但不同上下文中词的概率分布形状差异很大。 例如,若设 $k = 10$,在某些上下文中,前 10 个词可能集中了绝大部分概率质量(如 95%),此时截断影响不大;但在另一些上下文中,概率分布可能非常平坦,前 10 个词加起来可能只占 30% 的概率质量,强行保留它们会引入大量低质量候选词。 为解决这一问题,Holtzman 等人(2020)提出了 Top-p 采样(也称核采样,nucleus sampling)。其核心思想不是保留固定的 $k$ 个词,而是保留累积概率达到 $p$ 的最小词集(即覆盖至少 $p$ 比例概率质量的“核心”(nucleus)部分)。 目标不变:剔除极不可能的词。 但通过基于概率质量而非词数来动态调整候选集大小,使得该方法在不同上下文中更稳健,候选池会自动扩大或缩小。 ...
现在我们来讨论输入矩阵 $\mathbf{X}$ 的来源。 给定一个包含 $N$ 个词元的序列($N$ 即上下文长度,以词元为单位),形状为 $[N \times d]$ 的矩阵 $\mathbf{X}$ 为上下文中的每个词元提供一个嵌入(embedding)。 Transformer 通过分别计算两种嵌入来实现这一点:词元嵌入(token embedding)和位置嵌入(positional embedding)。 词元嵌入在第 6 章中已介绍过,它是一个维度为 $d$ 的向量,作为输入词元的初始表示。 (随着向量在残差流中逐层向上流动,该嵌入表示会不断变化和丰富,融入上下文信息,并根据所构建的语言模型类型承担不同角色。) 所有初始词元嵌入存储在一个嵌入矩阵 $\mathbf{E}$ 中,其每一行对应词汇表中 $|V|$ 个词元中的一个。 (注意:此处的 $V$ 指词汇表(vocabulary),与注意力机制中的值向量 $V$ 无关。) 因此,每个词元由一个 $d$ 维行向量表示,$\mathbf{E}$ 的形状为 $[|V| \times d]$。 例如,对于输入字符串 Thanks for all the,我们首先将其转换为词汇表索引(这些索引是在使用 BPE 或 SentencePiece 等分词器对输入进行分词时生成的)。 假设 thanks for all the 对应的索引序列为 $\mathbf{w} = [5, 4000, 10532, 2224]$。 接着我们通过索引从 $\mathbf{E}$ 中选取对应的行(第 5 行、第 4000 行、第 10532 行、第 2224 行),得到每个词元的嵌入。 ...
到目前为止,我们对多头注意力及 Transformer 块其余部分的描述,都是从单个残差流中、在单一时间步 $i$ 上计算单个输出的角度出发的。 但如前所述,为每个词元计算 $\mathbf{a}_i$ 的注意力操作彼此独立;同样,整个 Transformer 块中从输入 $\mathbf{x}_i$ 计算 $\mathbf{h}_i$ 的所有操作也都是相互独立的。 这意味着我们可以轻松地将整个计算并行化,充分利用高效的矩阵乘法运算。 具体做法是:将输入序列中 $N$ 个词元的嵌入向量打包成一个大小为 $[N \times d]$ 的矩阵 $\mathbf{X}$,其中每一行对应一个输入词元的嵌入。 大型语言模型中的 Transformer 通常支持的输入长度 $N$ 从 1K 到 32K 不等;通过调整架构,使用特殊的长上下文之类的机制,甚至可处理长达 128K 或数百万词元的上下文。 因此,对于标准(vanilla)Transformer,我们可以认为矩阵 $\mathbf{X}$ 包含 1K 至 32K 行,每行维度为嵌入维度 $d$(即模型维度)。 并行化注意力计算 我们先从单个注意力头开始,再扩展到多头,最后加入 Transformer 块中的其他组件。 对于单个头,我们将输入矩阵 $\mathbf{X}$ 分别与查询、键、值权重矩阵相乘:$\mathbf{W}^{\mathbf{Q}}$ 形状为 $[d \times d_k]$,$\mathbf{W}^{K}$ 形状为 $[d \times d_k]$,$\mathbf{W}^{V}$ 形状为 $[d \times d_v]$,从而得到三个矩阵:$\mathbf{Q} \in \mathbb{R}^{N \times d_k}$ 包含所有查询向量,$\mathbf{K} \in \mathbb{R}^{N \times d_k}$ 包含所有键向量,$\mathbf{V} \in \mathbb{R}^{N \times d_v}$ 包含所有值向量: ...
自注意力计算是所谓 Transformer 块(transformer block)的核心。除了自注意力层外,一个 Transformer 块还包含另外三种类型的层: (1) 前馈网络层(feedforward layer), (2) 残差连接(residual connections), (3) 归一化层(通常称为“层归一化”,layer norm)。 图 8.6 展示了一个 Transformer 块的结构,并采用了一种被称为 残差流(residual stream)的常见视角来理解该模块(Elhage 等,2021)。 在残差流视角下,我们将单个词元 $i$ 在 Transformer 块中的处理过程视为一条针对位置 $i$ 的、维度为 $d$ 的表示流。 这条残差流始于原始输入向量,各个组件从流中读取输入,并将其输出加回到流中。 图 8.6 Transformer 块的架构,展示了 残差流。 本图展示的是 前置归一化(prenorm)版本的架构,即层归一化发生在注意力层和前馈层之前,而非之后。 流底部的输入是一个词元的嵌入向量,维度为 $d$。 该初始嵌入通过残差连接向上传递,并被 Transformer 的其他组件逐步更新:我们已介绍过的注意力层,以及即将引入的前馈层。 在注意力层和前馈层之前,会先执行一种称为层归一化(layer norm)的计算。 初始向量首先经过一层归一化和注意力层,其结果被加回到残差流中——在此处,是加到原始输入向量 $\mathbf{x}_i$ 上。 随后,这个求和后的向量再次经过另一层归一化和一个前馈层,其输出又被加回到残差流中。 我们将最终得到的输出记为 $\mathbf{h}_i$,表示词元 $i$ 经过整个 Transformer 块后的结果。 (早期描述常将这一机制比喻为残差连接——即将某个组件的输入与其输出相加。但残差流这一视角能更清晰地展现 Transformer 的信息流动方式。) 我们已经了解了注意力层,现在介绍在处理位置 $i$ 的单个输入 $\mathbf{x}_i$ 时,前馈层和层归一化的计算方式。 前馈层(Feedforward Layer) 前馈层是一个全连接的两层网络(即含一个隐藏层,两个权重矩阵),如第 6 章所述。 所有词元位置 $i$ 共享相同的权重(即权重与位置无关);但不同 Transformer 层之间的前馈权重彼此不同。 通常,前馈网络隐藏层的维度 $d_{ff}$ 会大于模型维度 $d$。 (例如,在原始 Transformer 模型中,$d = 512$,而 $d_{ff} = 2048$。) ...
回顾第 5 章的内容,在 word2vec 和其他静态词嵌入方法中,一个词的语义表示始终是同一个向量,与上下文无关:例如,单词 chicken 总是由同一个固定的向量表示。 因此,代词 it 的静态向量可能只能编码“这是一个用于动物或无生命事物的代词”这一信息。 但在实际语境中,它的含义要丰富得多。请考虑以下两个句子中的 it: (8.1) The chicken didn’t cross the road because it was too tired. (8.2) The chicken didn’t cross the road because it was too wide. 在句子 (8.1) 中,it 指的是 chicken(即读者知道是鸡太累了),而在句子 (8.2) 中,it 指的是 road(即读者知道路太宽了)1。 也就是说,如果我们想要计算整个句子的含义,就必须让 it 在第一个句子中与 the chicken 关联,在第二个句子中与 the road 关联,这种关联是依赖于上下文的。 此外,设想我们像一个因果语言模型那样从左到右阅读,处理到单词 it 为止: (8.3) The chicken didn’t cross the road because it 此时,我们尚不清楚 it 最终会指代什么! 因此,在这个时刻,对 it 的表示可能同时包含 chicken 和 road 的某些特征,因为模型正在尝试预测接下来会发生什么。 ...