理解这个叫做“世界”的操作系统
灵光一现 → 精巧实现 → 无人知晓 → 自己遗忘,几乎是每个认真思考的工程师都会经历的隐性知识流失
用 PyTorch 从头实现语言模型
《Speech and Language Processing》(2025版草稿)中文版
理解这个叫做“世界”的操作系统
灵光一现 → 精巧实现 → 无人知晓 → 自己遗忘,几乎是每个认真思考的工程师都会经历的隐性知识流失
用 PyTorch 从头实现语言模型
《Speech and Language Processing》(2025版草稿)中文版
Larvatus prodeo(戴上面具,我继续前行) ——笛卡尔 在前两章中,我们介绍了 Transformer 架构,并展示了如何将 Transformer 语言模型以因果型(causal)或从左至右的方式进行预训练。 本章将引入预训练语言模型的另一种范式——双向 Transformer(bidrectional transformer)编码器,以及其中应用最为广泛的模型:BERT(Devlin 等,2019)。 该模型通过掩码语言建模(masked language modeling)进行训练:不同于预测下一个词,而是将句子中的某个词进行掩码处理,要求模型根据其左右两侧的上下文来预测被掩码的词。 因此,这种方法使模型能够同时利用左侧和右侧的上下文信息。 我们在上一章已初步介绍了微调(finetuning)的概念。 本章将进一步描述一种新的微调方式:我们将这些预训练模型所学习到的 Transformer 网络作为基础,在其顶层之后添加一个神经网络分类器,并在额外的标注数据上进行训练,以完成某项下游任务,例如命名实体识别或自然语言推理。 其核心思想与之前一致:预训练阶段学习到的语言模型能够生成对词语语义的丰富表征,从而使得模型更容易学习(即“被微调以满足”)特定下游语言理解任务的需求。 这种“预训练–微调”范式正是机器学习中所谓迁移学习(transfer learning)的一个具体实例,即从一个任务或领域中获取知识,并将其应用于(迁移至)解决新任务。 本章引入的第二个关键概念是上下文嵌入(contextual embeddings):即基于上下文的词表示方法。 第 5 章中介绍的方法(如 word2vec 或 GloVe)为词汇表中的每个唯一词 $w$ 学习一个固定的向量嵌入。 相比之下,上下文嵌入(例如由 BERT 等掩码语言模型所学习到的表示)则为同一个词 $w$ 在不同上下文中生成不同的向量表示。 虽然第 8 章中的因果语言模型也使用了上下文嵌入,但掩码语言模型所产生的嵌入在作为语义表示方面表现尤为出色。 8.9 Transformer 的可解释性 目录 9.1 双向 Transformer 编码器
在第 8 章中,我们通过让因果型 Transformer 语言模型逐词预测文本中的下一个词来进行训练。 然而,一旦我们在注意力机制中移除了因果掩码,这种“预测下一个词”的语言建模任务就变得毫无意义——因为答案已经直接出现在上下文中(模型可以“偷看”未来词)。因此,我们需要一种全新的训练方案。 取而代之的是,模型不再预测下一个词,而是学习完成一种填空任务(fill-in-the-blank task),在技术上被称为 Cloze 任务(Taylor, 1953)。 为理解这一点,让我们回到第 3 章中的那个示例。 传统语言模型会尝试预测下面这句话接下来最可能出现的词: The water of Walden Pond is so beautifully ____ 而在 Cloze 任务中,给定句子中缺失一个或多个词的情况,要求模型根据其余部分预测缺失的内容。例如: The ___ of Walden Pond is so beautifully … 也就是说,给定一个部分被遮蔽的输入序列,学习目标是还原出被遮蔽的元素。 更具体地说,在训练过程中,模型会被剥夺输入序列中的一个或多个词元,并必须为每个缺失位置生成一个在整个词表上的概率分布。 然后,我们利用模型在每个被遮蔽位置上的预测与真实词之间的交叉熵损失来驱动整个学习过程。 这种方法可以推广到多种对训练输入进行破坏(corrupt)后再让模型恢复原始内容的策略。 已被采用的破坏方式包括:掩码(masking)、替换(substitutions)、重排(reorderings)、删除(deletions)、插入干扰项(extraneous insertions)。 这类训练方法统称为 去噪(denoising):我们以某种方式对输入引入噪声(例如遮蔽一个词,或插入一个错误词),而模型的目标就是去除噪声、重建原始干净的输入。 9.2.1 词元掩码 下面我们介绍用于训练双向编码器的掩码语言建模(Masked Language Modeling, MLM)方法(Devlin 等,2019)。 与我们之前看到的语言模型训练方法类似,MLM 也使用大规模语料库中的无标注文本。 在 MLM 训练中,模型接收来自训练语料的一系列句子,其中一定比例的词元(在 BERT 模型中为 15%)会被随机选中,并通过掩码操作进行处理。 以输入句子 lunch was delicious 为例,假设我们随机选择处理第 3 个词元 delicious: 80% 的概率:将该词元替换为特殊的词汇表标记 [MASK],例如:lunch was delicious → lunch was [MASK] 10% 的概率:将该词元替换为从词汇表中根据一元词频分布随机采样的另一个词元,例如:lunch was delicious → lunch was gasp 10% 的概率:保留原词不变,例如:lunch was delicious → lunch was delicious 随后,我们训练模型去预测这些被修改词元的原始正确词元。 为什么要采用这三种修改方式? 引入 [MASK] 标记会导致预训练阶段与下游任务的微调或推理阶段之间出现不匹配,因为当我们使用 MLM 模型执行下游任务时,输入中不会包含任何 [MASK] 标记。 如果我们只是简单地将词元替换为 [MASK],模型可能会只在看到 [MASK] 时才尝试预测词元;而我们真正希望的是,模型始终尝试预测输入中的原始词元。 ...
至此,我们已经介绍了基本的 RNN 模型,学习了其高级组件(如多层堆叠和 LSTM 变体),并探讨了 RNN 在多种任务中的应用方式。现在,让我们对这些应用场景所对应的典型架构做一个简要总结。 图 13.15 展示了我们迄今讨论过的三种主要架构:序列标注、序列分类 和 语言建模。 在序列标注任务中(例如词性标注或命名实体识别),模型为输入序列中的每个词或词元生成一个对应的标签。 在序列分类任务中(例如情感分析),我们忽略中间每个词元的输出,仅使用序列末尾的表示进行最终预测;相应地,模型的训练信号也仅来自最后一个时间步的反向传播。 在语言建模任务中,模型在每个时间步都接受此前的上下文,并被训练用于预测下一个词。 在下一节中,我们将介绍第四种架构——编码器-解码器(encoder-decoder)。 图 13.15 四种 NLP 任务的典型架构。 在序列标注(如词性标注或命名实体识别)中,我们将每个输入词元 $x_i$ 映射到一个输出标签 $y_i$; 在序列分类中,整个输入序列被映射为一个单一类别; 在语言建模中,模型基于先前的词元预测下一个词元; 在编码器-解码器架构中,包含两个独立的 RNN 模型:第一个(编码器)将输入序列 $x$ 映射为一个中间表示(称为上下文或语义向量),第二个(解码器)则基于该上下文生成输出序列 $y$。 13.5 长短期记忆网络(LSTM) 目录 13.7 基于 RNN 的编码器-解码器模型
时间自会说明一切。 ——简·奥斯汀,《劝导》 语言本质上是一种时间性现象。 口语是一连串随时间展开的声学事件,而我们在理解和生成口语及书面语时,也都是将其视为一个顺序输入流。 我们所使用的隐喻也体现了语言的时间特性:例如,我们谈论“对话的流动”、“新闻推送”和“推文流”,这些说法都强调了语言是一种随时间逐步展开的序列。 本章将介绍一种深度学习架构:循环神经网络(Recurrent Neural Networks, RNN)及其变体,如长短期记忆网络(Long Short-Term Memory networks, LSTM),它们表示时间的方式与前馈网络和 transformer 网络不通过。 RNN 有一种机制可以直接处理语言的序列特性,使其无需依赖任意设定的固定窗口即可应对语言的时间性。 循环网络通过其循环连接提供了一种全新的方式来表示先前的上下文信息,从而使模型的决策能够依赖于数百个词之前的上下文。 我们将看到如何将该模型应用于语言建模、序列标注任务(如词性标注)以及文本分类任务(如情感分析)。 12.7 偏见与伦理问题 目录 13.1 循环神经网络
在神经网络发展早期,人们就意识到:神经网络的强大能力——正如启发它的生物神经元一样——来自于将多个单元组合成更大的网络。 对多层网络必要性的一个经典证明,来自明斯基(Minsky)和帕佩特(Papert)于1969年提出的结果:单个神经单元无法计算某些非常简单的输入函数。 考虑用两个二值输入计算基本逻辑函数的任务,例如 AND、OR 和 XOR。 作为回顾,下表列出了这些函数的真值表: x1 x2 AND OR XOR 0 0 0 0 0 0 1 0 1 1 1 0 0 1 1 1 1 1 1 0 这个例子最初是针对感知机(perceptron)提出的。感知机是一种非常简单的神经单元,其输出为二值(0 或 1),并且不包含非线性激活函数。 感知机的输出 $y$ 按如下方式计算(使用与公式 (6.2) 相同的权重 $\mathbf{w}$、输入 $\mathbf{x}$ 和偏置 $b$): $$ y = \begin{cases} 0, & \text{若 } \mathbf{w} \cdot \mathbf{x} + b \leq 0 \\ 1, & \text{若 } \mathbf{w} \cdot \mathbf{x} + b > 0 \end{cases} \tag{6.7} $$构建一个能计算 AND 或 OR 逻辑函数的感知机非常容易;图 6.4 展示了所需的权重。 ...
现在我们来看如何将前馈网络应用于 NLP 分类任务。 实际上,简单的前馈网络并不是当前文本分类的主流方法;在真实应用中,我们会使用更先进的架构,例如第 10 章介绍的 BERT 等 Transformer 模型。 尽管如此,通过构建一个基于前馈网络的文本分类器,我们可以引入若干核心概念,这些概念贯穿全书,包括:嵌入矩阵(embedding matrix)、表示池化(representation pooling)和表示学习(representation learning)。 但在介绍这些概念之前,我们先从一个最简单的分类器开始——仅对第 4 章的情感分类器做最小改动。 与第 4 章一样,我们仍使用人工设计的特征,将其送入分类器,并输出类别概率。 唯一的区别是,我们将分类器从逻辑回归替换为神经网络。 6.4.1 使用人工特征的神经网络分类器 我们从一个简单的两层情感分类器入手:以第 4 章的逻辑回归分类器(对应单层网络)为基础,仅增加一个隐藏层。 输入元素 $\mathbf{x}_i$ 可以是图 4.2 中那样的标量特征,例如$\mathbf{x}_i$ = 文档中的总词数,$\mathbf{x}_2$ = 文档中积极情感词典词的出现次数,如果文档包含“no” 则 $\mathbf{x}_3$ = 1,依此类推,共 $d$ 个特征。 输出层 $\hat{y}$ 可以有两个节点(分别对应正面、负面情感),或三个节点(正面、负面、中性)。此时 $\hat{y}_1$ 表示正面情感的概率, $\hat{y}_2$ 表示负面情感的概率,$\hat{y}_3$ 表示中性情感的概率。 整个模型的计算公式与前述两层网络完全一致(如前所述,我们继续用 $\sigma$ 泛指任意非线性激活函数,无论是 Sigmoid、ReLU 还是其他): $$ \begin{align*} \mathbf{x} &= [\mathbf{x}_i,\mathbf{x}_2,...\mathbf{x}_d ] (\text{each} \mathbf{x}_i \text{is a hand-designed feature}) \\ \mathbf{h} &= \sigma(\mathbf{Wx} + \mathbf{b}) \\ \mathbf{z} &= \mathbf{Uh} \\ \hat{\mathbf{y}} = \mathrm{softmax}(\mathbf{z}) \tag{6.19} \end{align*} $$图 6.10 展示了该架构的示意图。 如前所述,在逻辑回归分类器中加入这个隐藏层,使网络能够捕捉特征之间的非线性交互关系。 仅此一点就可能带来性能更好的情感分类器。 ...
“当单元数量很大时,这类机器可以表现出极为复杂的行为。” ——艾伦·图灵(Alan Turing, 1948),《智能机器》,第6页 神经网络是语言处理的一项基础计算工具,其历史也相当悠久。 之所以称为“神经”网络,是因为它起源于McCulloch-Pitts神经元(McCulloch 和 Pitts,1943)。这是一种基于生物神经元的简化模型,,将神经元视为一种计算单元,并可用命题逻辑来描述。 然而,现代语言处理中使用的神经网络已不再依赖这些早期的生物学启发。 如今的神经网络是由大量小型计算单元组成的网络。每个单元接收一个输入值向量,并输出一个单一数值。 本章将介绍用于分类任务的神经网络。 我们所采用的架构称为前馈网络(feedforward network),因为其计算过程是从一层单元逐层向前传递到下一层。 现代神经网络通常被称为深度学习(deep learning),这是因为现代网络往往是深层的(即包含许多层)。 神经网络与逻辑回归在数学上有很多共通之处。 但神经网络是一种比逻辑回归更强大的分类器。事实上,即使是最小的神经网络(从技术上讲,仅含一个“隐藏层”的网络)也能被证明可以学习任意函数。 神经网络分类器与逻辑回归还存在另一重要区别。 在逻辑回归中,我们通过基于领域知识设计丰富多样的特征模板,将该分类器应用于多种不同任务。 而在使用神经网络时,则通常避免大量依赖人工构造的复杂特征,转而构建直接以原始词(raw words)作为输入的网络,并在学习分类的过程中自动习得特征表示。 我们在第6章中已经看到过这种表示学习(representation learning)的典型例子——词嵌入(embeddings)。 特别地,深层网络在表示学习方面表现尤为出色。 正因如此,深度神经网络成为那些拥有充足数据、足以支持自动特征学习的任务的理想工具。 神经网络分类器与逻辑回归还有另一点不同。 使用逻辑回归时,我们通常基于领域知识设计出丰富多样的特征模板,从而将其应用于多种任务。 而使用神经网络时,则更倾向于避免大量手工构造的复杂特征。取而代之的是,构建直接以原始词语作为输入的神经网络,并让网络在学习分类的过程中自动学习特征表示。 我们在第 5 章中已经看到过这种表示学习的例子——词嵌入(embeddings),一旦开始学些深度 transformer 网络,我们会看到大量此类例子。 特别地,非常深的网络在表示学习方面表现尤为出色。 因此,对于拥有足够数据、能够自动学习特征的任务,深度神经网络正是合适的工具。 本章将介绍作为分类器的前馈网络,并将其应用于一个简单的语言建模任务:为词序列分配概率,并预测下一个词。 在后续章节中,我们将进一步探讨神经模型的诸多其他方面,例如循环神经网络(第8章)、Transformer(第9章)以及掩码语言建模(第11章)。 本章将介绍使用前馈网络的分类器,首先使用手工构造的特征,然后使用使用第 5 章所学的词嵌入。 在后续章节中,我们将介绍更多类型的神经网络模型,其中最重要的是Transformer和注意力机制(第8章),此外还包括循环神经网络(第13章)和卷积神经网络(第15章)。 下一章则将引入神经大语言模型这一范式。 5.9 向量模型的评估 目录 6.1 单元
在构建系统时,我们常常需要比较两个系统的性能。 如何判断我们刚刚构建的新系统是否优于旧系统?或者是否优于文献中描述的其他系统?这属于统计假设检验(statistical hypothesis testing)的范畴。 本节将介绍用于自然语言处理(NLP)分类器的统计显著性检验方法,主要参考了 Dror 等人(2020)与 Berg-Kirkpatrick 等人(2012)的工作。 假设我们要在某个评价指标 $M$(如 $F_1$ 值或准确率)上比较分类器 $A$ 与 $B$ 的性能。 例如,我们想知道:在某个特定测试集 $x$ 上,我们的逻辑回归情感分类器 $A$(见第5章)是否比朴素贝叶斯情感分类器 $B$ 获得更高的 $F_1$ 分数。 记 $M(A, x)$ 为系统 A 在测试集 $x$ 上的得分,$\delta(x)$ 为 A 与 B 在 $x$ 上的性能差异: $$ \delta(x) = M(A,x) - M(B,x) \tag{4.44} $$我们希望判断 $\delta(x) > 0$ 是否成立——即逻辑回归分类器在该测试集上的 $F_1$ 是否确实高于朴素贝叶斯分类器。 $\delta(x)$ 被称为效应量(effect size):$\delta$ 越大,说明 A 明显优于 B;$\delta$ 越小,则说明 A 仅略胜一筹。 那么,为何不能直接看 $\delta(x)$ 是否为正? 假设我们发现 A 的 $F_1$ 比 B 高出 0.04,是否就能断定 A 更好?不能! 因为这种优势可能只是偶然出现在当前测试集 $x$ 上。 我们需要更严谨的判断:A 相对于 B 的优势是否具有可复现性?也就是说,如果换一个测试集 $x'$,或在其他条件下重复实验,A 是否仍能保持优势? ...
文本分类的训练与测试流程与我们在语言建模中所见(第 3.2 节)一致:我们使用训练集(training set)来训练模型,然后使用开发测试集(development test set,也称为 devset)来调整某些参数,并总体上确定哪个模型表现最佳。 一旦我们选定自认为最优的模型,便在此前从未见过的测试集(test set)上运行该模型,并报告其性能。 虽然使用 devset 可以避免对测试集过拟合,但采用固定的训练集、devset 和测试集会带来另一个问题:为了保留足够多的数据用于训练,测试集(或 devset)可能不够大,从而缺乏代表性。 难道不能设法既用全部数据进行训练,又用全部数据进行测试吗?答案是肯定的——我们可以采用交叉验证(cross-validation)。 在交叉验证中,我们先选定一个数字 $k$,并将数据划分为 $k$ 个互不重叠的子集,称为折(folds)。 接着,依次选取其中一折作为测试集,在其余 $k-1$ 折上训练分类器,并在该测试集上计算错误率。 然后换另一折作为测试集,再次在剩下的 $k-1$ 折上训练模型。 如此重复 $k$ 次,每次使用不同的测试折,最后将这 $k$ 次测试得到的错误率取平均,作为模型的平均错误率。 例如,若选择 $k = 10$,我们将训练 10 个不同的模型(每个使用 90% 的数据),测试 10 次,并对这 10 个结果取平均。 这种方法称为10 折交叉验证(10-fold cross-validation)。 交叉验证唯一的缺点在于:由于所有数据都被用于测试,因此整个语料库必须保持“盲态”:不能事先查看任何数据以推测可能的特征,也不能通过观察数据了解其分布情况,否则就相当于“偷看”了测试集。 这种作弊行为会导致对系统性能的估计过于乐观。 然而,在设计自然语言处理系统时,通过观察语料来理解数据特性至关重要!那该怎么办? 为此,一种常见做法是:先划分出固定的训练集和测试集,然后仅在训练集内部进行 10 折交叉验证(用于模型选择或调参),而仍按常规方式在独立的测试集上计算率,如图 4.10 所示。 图 4.10 10 折交叉验证 4.9 评估指标:精确率、召回率与 F 值 目录 4.11 统计显著性检验
为了介绍文本分类的评估方法,我们先考虑一些简单的二元检测任务(detection tasks)。 例如,在垃圾邮件检测中,我们的目标是将每封邮件标记为属于垃圾邮件类别(“正例”,positive)或不属于该类别(“负例”,negative)。 对于每个样本(即每封电子邮件),我们需要知道我们的系统是否将其判定为垃圾邮件。 我们还需要知道该邮件实际上是否为垃圾邮件,即由人工标注的、我们试图匹配的标签。 我们将这些人工标注的标签称为黄金标准标签(gold labels)。 再举一个例子:假设你是“美味派公司”(Delicious Pie Company)的 CEO,你想了解人们在社交媒体上对你们派的评价,于是你构建了一个系统来检测提及“美味派”的推文。 在这个任务中,正例是关于美味派的推文,负例则是所有其他推文。 在这两类场景中,我们都需一种指标来衡量垃圾邮件检测器(或派相关推文检测器)的表现好坏。 要评估任何检测系统,我们首先构建一个如图 4.7 所示的混淆矩阵(confusion matrix)。 混淆矩阵是一个表格,用于可视化算法输出与人工黄金标准标签之间的对应关系。它以两个维度(系统输出 vs. 黄金标签)组织数据,每个单元格代表一类可能的结果。 以垃圾邮件检测为例:真正例(True Positives, TP)代表邮件确实是垃圾邮件(由人工标签确认),且系统也正确地将其判为垃圾邮件。 假负例(False Negatives, FN)表示邮件确实是垃圾邮件,但系统错误地将其标记为非垃圾邮件。 图 4.7 用于可视化二元分类系统相对于黄金标准标签表现的混淆矩阵。 表格右下角给出了准确率(accuracy)的计算公式,即系统正确分类的样本占总样本数(对于垃圾邮件或者派的例子,就是所有邮件或者所有推文)的百分比。 尽管准确率看似自然直观,但通常不用于文本分类任务。 原因在于,当类别分布严重不平衡时(例如垃圾邮件在全部邮件中占绝大多数,或关于派的推文在海量推文中极为稀少),准确率会严重失真。 为更清楚地说明这一点,设想我们分析了 100 万条推文,其中仅有 100 条讨论了对我们派的喜爱(或厌恶),其余 999,900 条则完全无关。 现在考虑一个极其简单的分类器:它一律将所有推文判为“与派无关”。该分类器会产生 999,900 个真负例和 100 个假负例,准确率达到 $\frac{999,900}{1,000,000} = 99.99\%$! 多么惊人的准确率! 我们是否该为此欢呼? 显然不该,因为这个“完美”的分类器一条相关评论都没找到,对我们毫无用处。 换言之,当目标任务是发现稀有事件(或至少是频率不平衡的事件)时,准确率就不是一个合适的评估指标,而这种情况在现实世界中极为普遍。 正因如此,我们通常不使用准确率,而是转向图 4.7 中所示的另外两个指标:精确率(Precision)和召回率(Recall)。 精确率衡量的是:在系统判定为正例的所有样本中(即系统标记为正例),真正为正例的比例(即按照人类的黄金标准标签为正例)。 精确率的定义为: $$ \text{Precision} = \frac{\text{true positives}}{\text{true positives} + \text{false negatives}} $$召回率衡量的是,在所有实际为正例的样本中,被系统正确识别出来的比例。其定义为: $$ \text{Recall} = \frac{\text{true positives}}{\text{true positives} + \text{false negatives}} $$这两个指标能有效解决前述“无派分类器”的问题。 尽管该分类器准确率高达 99.99%,但其召回率为 0(因为真正例为 0,假负例为 100,故召回率是 0/100)。 同时,由于它从未预测任何正例,其精确率在数学上未定义(分母为 0),但在实际应用中也被视为无效。 因此,与准确率不同,精确率和召回率都聚焦于“真正例”,即我们真正想要找的东西。 ...